Public/Export-CISRemediationScript.ps1
|
function Export-CISRemediationScript { <# .SYNOPSIS Generates a PowerShell remediation script from CIS benchmark results. .DESCRIPTION Takes benchmark results (from Invoke-CISAzureBenchmark or a saved JSON file) and generates a .ps1 script with remediation guidance and commands for each failed control. The script includes -WhatIf support and is grouped by section. .PARAMETER Results Results object from Invoke-CISAzureBenchmark. .PARAMETER JsonPath Path to a saved JSON report file. .PARAMETER OutputPath Path for the generated remediation script. Defaults to ./CIS-Remediation.ps1. .EXAMPLE $scan = Invoke-CISAzureBenchmark -OutputDirectory ./reports Export-CISRemediationScript -Results $scan.Results -OutputPath ./remediate.ps1 .EXAMPLE Export-CISRemediationScript -JsonPath ./reports/scan.json -OutputPath ./remediate.ps1 #> [CmdletBinding()] param( [Parameter(Mandatory, ParameterSetName = 'FromResults')] [PSCustomObject[]]$Results, [Parameter(Mandatory, ParameterSetName = 'FromJson')] [ValidateScript({ Test-Path $_ -PathType Leaf })] [string]$JsonPath, [Parameter()] [string]$OutputPath = './CIS-Remediation.ps1' ) # Load results if ($JsonPath) { $json = Get-Content -Path $JsonPath -Raw | ConvertFrom-Json $Results = $json.results } if (-not $Results -or $Results.Count -eq 0) { Write-Warning "No results provided. Use -Results or -JsonPath parameter." return } # Filter to FAIL results only $failures = @($Results | Where-Object { $status = if ($_.Status) { $_.Status } else { $_.status } $status -eq 'FAIL' }) if ($failures.Count -eq 0) { Write-Host " No failed controls found. No remediation script needed." -ForegroundColor Green return } # Helper to escape strings for safe inclusion in generated PowerShell code function Escape-ForPSString { param([string]$Value) if (-not $Value) { return '' } # Escape single quotes by doubling them for safe single-quoted strings return $Value -replace "'", "''" } # Helper to sanitize strings for safe inclusion in PowerShell comments function Sanitize-ForComment { param([string]$Value) if (-not $Value) { return '' } # Remove newlines/carriage returns that could break out of comment context return ($Value -replace '[\r\n]+', ' ').Trim() } $sb = [System.Text.StringBuilder]::new() [void]$sb.AppendLine('#Requires -Version 5.1') [void]$sb.AppendLine('#Requires -Modules Az.Accounts') [void]$sb.AppendLine('<#') [void]$sb.AppendLine('.SYNOPSIS') $bmkVersion = if ($script:CISBenchmarkVersion) { $script:CISBenchmarkVersion } else { 'v5.0.0' } [void]$sb.AppendLine(" CIS Azure Foundations Benchmark $bmkVersion - Remediation Script") [void]$sb.AppendLine('.DESCRIPTION') [void]$sb.AppendLine(" Auto-generated remediation guidance for $($failures.Count) failed control(s).") [void]$sb.AppendLine(" Generated: $([DateTime]::UtcNow.ToString('o'))") [void]$sb.AppendLine('') [void]$sb.AppendLine(' IMPORTANT: Review each section before executing.') [void]$sb.AppendLine(' Use -WhatIf where available to preview changes.') [void]$sb.AppendLine('#>') [void]$sb.AppendLine('[CmdletBinding(SupportsShouldProcess)]') [void]$sb.AppendLine('param()') [void]$sb.AppendLine('') [void]$sb.AppendLine("Write-Host 'CIS Azure Benchmark $bmkVersion - Remediation Script' -ForegroundColor Cyan") $safeCount = $failures.Count [void]$sb.AppendLine("Write-Host '$safeCount failed control(s) to remediate' -ForegroundColor Yellow") [void]$sb.AppendLine('Write-Host ''''') [void]$sb.AppendLine('') # Group by section $grouped = $failures | Group-Object { $section = if ($_.Section) { $_.Section } else { $_.section } if ($section) { $section } else { 'Unknown' } } | Sort-Object Name foreach ($group in $grouped) { $safeGroupName = Sanitize-ForComment -Value $group.Name [void]$sb.AppendLine("# $('=' * 70)") [void]$sb.AppendLine("# Section: $safeGroupName") [void]$sb.AppendLine("# $('=' * 70)") [void]$sb.AppendLine('') foreach ($fail in $group.Group) { $controlId = if ($fail.ControlId) { $fail.ControlId } else { $fail.controlId } $title = if ($fail.Title) { $fail.Title } else { $fail.title } $details = if ($fail.Details) { $fail.Details } else { $fail.details } $remediation = if ($fail.Remediation) { $fail.Remediation } else { $fail.remediation } $severity = if ($fail.Severity) { $fail.Severity } else { $fail.severity } $affected = if ($fail.AffectedResources) { $fail.AffectedResources } else { $fail.affectedResources } # Sanitize all values for safe code generation $safeControlId = Sanitize-ForComment -Value $controlId $safeTitle = Sanitize-ForComment -Value $title $safeSeverity = Sanitize-ForComment -Value $severity [void]$sb.AppendLine("# --- ${safeControlId}: ${safeTitle} ---") [void]$sb.AppendLine("# Severity: $safeSeverity") if ($details) { # Wrap details in comment, sanitize each line $detailLines = $details -split "`n" foreach ($line in $detailLines) { $safeLine = Sanitize-ForComment -Value $line [void]$sb.AppendLine("# Finding: $safeLine") } } if ($affected -and $affected.Count -gt 0) { [void]$sb.AppendLine("# Affected Resources:") $maxShow = [math]::Min($affected.Count, 10) for ($i = 0; $i -lt $maxShow; $i++) { $safeResource = Sanitize-ForComment -Value $affected[$i] [void]$sb.AppendLine("# - $safeResource") } if ($affected.Count -gt 10) { [void]$sb.AppendLine("# ... and $($affected.Count - 10) more") } } [void]$sb.AppendLine('#') if ($remediation) { [void]$sb.AppendLine('# Remediation Steps:') $remLines = $remediation -split "`n" foreach ($line in $remLines) { $safeLine = Sanitize-ForComment -Value $line [void]$sb.AppendLine("# $safeLine") } } else { [void]$sb.AppendLine('# No specific remediation guidance available for this control.') } [void]$sb.AppendLine('') # Use single-quoted strings with escaped values to prevent code injection $escapedTitle = Escape-ForPSString -Value $safeTitle $escapedId = Escape-ForPSString -Value $safeControlId $escapedSeverity = Escape-ForPSString -Value $safeSeverity [void]$sb.AppendLine("Write-Host '[$escapedId] $escapedTitle' -ForegroundColor Yellow") [void]$sb.AppendLine("Write-Host ' Status: FAIL | Severity: $escapedSeverity' -ForegroundColor Red") if ($remediation) { [void]$sb.AppendLine("Write-Host ' See remediation steps in comments above.' -ForegroundColor DarkGray") } [void]$sb.AppendLine('# TODO: Add specific remediation commands for your environment below') [void]$sb.AppendLine('# Example: Set-AzSecurityPricing -Name "VirtualMachines" -PricingTier "Standard"') [void]$sb.AppendLine('') } } [void]$sb.AppendLine('Write-Host '''' ') [void]$sb.AppendLine('Write-Host ''Remediation script complete. Review and execute each section carefully.'' -ForegroundColor Cyan') # Write output try { $outputDir = Split-Path -Parent $OutputPath if ($outputDir -and -not (Test-Path $outputDir)) { New-Item -ItemType Directory -Path $outputDir -Force | Out-Null } $sb.ToString() | Out-File -FilePath $OutputPath -Encoding UTF8 -Force Write-Host " Remediation script written to: $OutputPath" -ForegroundColor Green Write-Host " Contains guidance for $($failures.Count) failed control(s)" -ForegroundColor White } catch { Write-Error "Failed to write remediation script to '$OutputPath': $($_.Exception.Message)" return } return $OutputPath } |