Public/Export-RemediationScripts.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function Export-RemediationScripts {
    <#
    .SYNOPSIS
        Generates runnable PowerShell remediation scripts from audit findings.
    .DESCRIPTION
        Produces .ps1 script files with remediation commands for failed audit checks.
        Scripts are grouped by category and include safety checks, confirmation prompts,
        and rollback comments.
 
        Only generates scripts for checks that have known PowerShell remediation commands.
    .PARAMETER Findings
        Array of audit finding objects. If not provided, reads from latest state.
    .PARAMETER OutputDirectory
        Directory for script output. Default: ./PSGuerrilla-Remediation-Scripts/
    .PARAMETER Force
        Overwrite existing scripts without prompting.
    .EXAMPLE
        Export-RemediationScripts
    .EXAMPLE
        Export-RemediationScripts -OutputDirectory ./scripts -Force
    #>

    [CmdletBinding()]
    param(
        [PSCustomObject[]]$Findings,
        [string]$OutputDirectory,
        [switch]$Force
    )

    if (-not $OutputDirectory) { $OutputDirectory = Join-Path (Get-Location) 'PSGuerrilla-Remediation-Scripts' }

    $dataDir = Get-PSGuerrillaDataRoot
    if (-not $Findings -or $Findings.Count -eq 0) {
        if (Test-Path $dataDir) {
            foreach ($f in (Get-ChildItem -Path $dataDir -Filter '*.findings.json' -ErrorAction SilentlyContinue)) {
                try { $Findings += @(Get-Content $f.FullName -Raw | ConvertFrom-Json) } catch { }
            }
        }
    }

    if (-not $Findings -or $Findings.Count -eq 0) {
        Write-Warning 'No audit findings available. Run a scan first.'
        return [PSCustomObject]@{ Success = $false; Message = 'No findings'; Path = $null; ScriptCount = 0 }
    }

    if (-not (Test-Path $OutputDirectory)) {
        New-Item -Path $OutputDirectory -ItemType Directory -Force | Out-Null
    }

    # Known remediation command templates keyed by check ID prefix
    $remediationTemplates = @{
        'ADPWD'  = @{ Module = 'ActiveDirectory'; Description = 'Active Directory Password Policy' }
        'ADPRIV' = @{ Module = 'ActiveDirectory'; Description = 'AD Privileged Account Remediation' }
        'ADGPO'  = @{ Module = 'GroupPolicy'; Description = 'Group Policy Remediation' }
        'ADSTALE' = @{ Module = 'ActiveDirectory'; Description = 'AD Stale Object Cleanup' }
        'AUTH'   = @{ Module = 'GoogleWorkspace'; Description = 'Google Authentication Settings' }
        'ADMIN'  = @{ Module = 'GoogleWorkspace'; Description = 'Google Admin Management' }
        'EMAIL'  = @{ Module = 'GoogleWorkspace'; Description = 'Email Security Settings' }
        'DRIVE'  = @{ Module = 'GoogleWorkspace'; Description = 'Drive Security Settings' }
        'OAUTH'  = @{ Module = 'GoogleWorkspace'; Description = 'OAuth Security Settings' }
        'M365EXO' = @{ Module = 'ExchangeOnlineManagement'; Description = 'Exchange Online Remediation' }
        'EIDAUTH' = @{ Module = 'Microsoft.Graph'; Description = 'Entra Authentication Remediation' }
        'EIDCA'  = @{ Module = 'Microsoft.Graph'; Description = 'Conditional Access Remediation' }
    }

    # Group findings by category prefix
    $failedFindings = @($Findings | Where-Object Status -in @('FAIL', 'WARN'))
    $grouped = @($failedFindings | Group-Object {
        $id = $_.CheckId ?? $_.Id ?? ''
        if ($id -match '^([A-Z0-9]+)-') { $Matches[1] } else { 'OTHER' }
    })

    $scriptCount = 0
    $generatedScripts = [System.Collections.Generic.List[string]]::new()
    $timestamp = [datetime]::UtcNow.ToString('yyyy-MM-dd HH:mm:ss')

    foreach ($group in $grouped) {
        $prefix = $group.Name
        $template = $remediationTemplates[$prefix]
        if (-not $template) { continue }

        $scriptName = "Remediate-$prefix.ps1"
        $scriptPath = Join-Path $OutputDirectory $scriptName

        if ((Test-Path $scriptPath) -and -not $Force) {
            Write-Verbose "Skipping $scriptName (already exists, use -Force to overwrite)"
            continue
        }

        $sb = [System.Text.StringBuilder]::new(4096)

        [void]$sb.AppendLine(@"
<#
.SYNOPSIS
    $($template.Description) - Auto-generated remediation script
.DESCRIPTION
    Generated by PSGuerrilla on $timestamp UTC.
    Contains remediation actions for $($group.Count) finding(s) in the $prefix category.
 
    IMPORTANT: Review each command before executing. Test in a non-production environment first.
.NOTES
    Required Module: $($template.Module)
    Generated: $timestamp UTC
#>
 
#Requires -Version 7.0
 
`$ErrorActionPreference = 'Stop'
 
Write-Host '=== PSGuerrilla Remediation: $($template.Description) ===' -ForegroundColor Cyan
Write-Host "Generated: $timestamp UTC" -ForegroundColor DarkGray
Write-Host ''
Write-Host 'WARNING: Review each action carefully before proceeding.' -ForegroundColor Yellow
Write-Host ''
 
`$confirm = Read-Host 'Do you want to proceed? (yes/no)'
if (`$confirm -ne 'yes') {
    Write-Host 'Aborted.' -ForegroundColor Red
    return
}
"@
)

        $actionNum = 0
        foreach ($finding in $group.Group) {
            $actionNum++
            $checkId = $finding.CheckId ?? $finding.Id ?? 'Unknown'
            $name = ($finding.Name ?? $finding.CheckName ?? $checkId) -replace "'", "''"
            $desc = ($finding.Description ?? '') -replace "'", "''"
            $steps = ($finding.RemediationSteps ?? 'See documentation for manual remediation steps.') -replace "'", "''"
            $sev = $finding.Severity ?? 'Medium'

            [void]$sb.AppendLine(@"
 
# ──────────────────────────────────────────────────
# [$actionNum] $checkId - $name
# Severity: $sev | Status: $($finding.Status)
# ──────────────────────────────────────────────────
Write-Host ''
Write-Host '[$actionNum/$($group.Count)] $checkId - $name' -ForegroundColor $(if ($sev -eq 'Critical') { 'Red' } elseif ($sev -eq 'High') { 'DarkYellow' } else { 'Yellow' })
Write-Host 'Severity: $sev' -ForegroundColor DarkGray
Write-Host 'Steps: $steps' -ForegroundColor Gray
Write-Host ''
 
# TODO: Add specific remediation commands for $checkId
# Remediation steps: $steps
$(if ($finding.RecommendedValue) { "# Recommended value: $($finding.RecommendedValue -replace "'", "''")" })
"@
)
        }

        [void]$sb.AppendLine(@"
 
Write-Host ''
Write-Host '=== Remediation script complete ===' -ForegroundColor Green
Write-Host "Processed $actionNum action(s) for $prefix category." -ForegroundColor DarkGray
"@
)

        $sb.ToString() | Set-Content -Path $scriptPath -Encoding UTF8
        $scriptCount++
        $generatedScripts.Add($scriptPath)
    }

    return [PSCustomObject]@{
        PSTypeName      = 'PSGuerrilla.RemediationScripts'
        Success         = $true
        Path            = (Resolve-Path $OutputDirectory).Path
        Message         = "Generated $scriptCount remediation script(s) in $OutputDirectory"
        ScriptCount     = $scriptCount
        Scripts         = @($generatedScripts)
    }
}