Public/Export-RemediationPlaybook.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-RemediationPlaybook {
    <#
    .SYNOPSIS
        Generates a step-by-step remediation guide grouped by category and priority.
    .DESCRIPTION
        Produces an HTML playbook organized into phases (critical first, then high, medium, low).
        Each finding includes prerequisites, step-by-step remediation, validation steps,
        effort estimates, and cost tier information.
    .PARAMETER Findings
        Array of audit finding objects. If not provided, reads from latest state.
    .PARAMETER OutputPath
        File path for the HTML output. Default: PSGuerrilla-Remediation-Playbook.html
    .PARAMETER OrganizationName
        Organization name for the report header.
    .PARAMETER MaxCostTier
        Maximum cost tier to include. Default: Medium.
    .EXAMPLE
        Export-RemediationPlaybook -OrganizationName 'Springfield USD'
    .EXAMPLE
        Export-RemediationPlaybook -MaxCostTier Free -OutputPath ./free-fixes.html
    #>

    [CmdletBinding()]
    param(
        [PSCustomObject[]]$Findings,
        [string]$OutputPath,
        [string]$OrganizationName = 'Organization',
        [ValidateSet('Free', 'Low', 'Medium', 'High', 'Enterprise')]
        [string]$MaxCostTier = 'Medium'
    )

    if (-not $OutputPath) { $OutputPath = Join-Path (Get-Location) 'PSGuerrilla-Remediation-Playbook.html' }

    $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 }
    }

    # Load remediation costs
    $remPath = Join-Path $PSScriptRoot '../Data/RemediationCosts.json'
    $remData = $null
    if (Test-Path $remPath) {
        $remData = Get-Content -Path $remPath -Raw | ConvertFrom-Json -AsHashtable
    }

    $tierOrder = @{ 'Free' = 0; 'Low' = 1; 'Medium' = 2; 'High' = 3; 'Enterprise' = 4 }
    $maxTierIndex = $tierOrder[$MaxCostTier] ?? 2

    $esc = { param([string]$s) [System.Web.HttpUtility]::HtmlEncode($s) }
    $timestamp = [datetime]::UtcNow.ToString('yyyy-MM-dd HH:mm:ss')
    $html = [System.Text.StringBuilder]::new(65536)

    # Filter to actionable findings with cost lookup
    $actionable = @($Findings | Where-Object Status -in @('FAIL', 'WARN') | ForEach-Object {
        $checkId = $_.CheckId ?? $_.Id ?? ''
        $prefix = if ($checkId -match '^([A-Z0-9]+)-') { $Matches[1] } else { '' }
        $costInfo = $remData.overrides.$checkId ?? $remData.categoryDefaults.$prefix
        $tier = $costInfo.costTier ?? 'Medium'
        if ($tierOrder[$tier] -le $maxTierIndex) {
            $_ | Add-Member -NotePropertyName '_CostTier' -NotePropertyValue $tier -PassThru -Force |
                 Add-Member -NotePropertyName '_Effort' -NotePropertyValue ($costInfo.effort ?? 'Medium') -PassThru -Force |
                 Add-Member -NotePropertyName '_Notes' -NotePropertyValue ($costInfo.notes ?? '') -PassThru -Force
        }
    })

    # Group by severity phase
    $phases = @(
        @{ Name = 'Phase 1: Critical Fixes'; Severity = 'Critical'; Color = 'var(--dark-red)' }
        @{ Name = 'Phase 2: High Priority'; Severity = 'High'; Color = 'var(--deep-orange)' }
        @{ Name = 'Phase 3: Medium Priority'; Severity = 'Medium'; Color = 'var(--gold)' }
        @{ Name = 'Phase 4: Low Priority'; Severity = 'Low'; Color = 'var(--sage)' }
    )

    [void]$html.Append(@"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Remediation Playbook - $(& $esc $OrganizationName)</title>
<style>
:root { --bg:#1a1f16; --surface:#242b1e; --surface-alt:#2d3526; --border:#3d4a35; --text:#d4c9a8; --text-muted:#8a8468; --olive:#a8b58b; --amber:#d4883a; --sage:#6b9b6b; --parchment:#d4c4a0; --gold:#c9a84c; --dim:#6b6b5a; --deep-orange:#c75c2e; --dark-red:#8b2500; }
body { font-family:'Segoe UI',Tahoma,sans-serif; background:var(--bg); color:var(--text); margin:0; padding:20px; }
.container { max-width:900px; margin:0 auto; }
h1 { color:var(--olive); border-bottom:2px solid var(--border); padding-bottom:10px; }
h2 { margin-top:30px; padding:8px 12px; border-radius:4px; }
.item { background:var(--surface); border:1px solid var(--border); border-radius:6px; margin:12px 0; padding:15px; }
.item-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; }
.item-header .title { font-weight:bold; }
.meta { display:flex; gap:12px; color:var(--text-muted); font-size:0.85em; }
.steps { margin:8px 0; padding-left:20px; }
.steps li { margin:6px 0; }
pre { background:var(--surface-alt); padding:8px; border-radius:4px; font-size:0.85em; overflow-x:auto; }
.badge { display:inline-block; padding:2px 8px; border-radius:3px; font-size:0.75em; font-weight:bold; }
.footer { color:var(--dim); font-size:0.8em; margin-top:40px; border-top:1px solid var(--border); padding-top:10px; }
@media print { body { background:#fff; color:#333; } .item { page-break-inside:avoid; } :root { --bg:#fff; --surface:#f9f9f9; --surface-alt:#eee; --border:#ccc; --text:#333; --text-muted:#666; --olive:#5a6b3a; --sage:#3a7a3a; --gold:#8a7a2a; --amber:#aa6a1a; --deep-orange:#aa3a0a; --dark-red:#7a1a00; --dim:#999; } }
</style>
</head>
<body>
<div class="container">
<h1>Remediation Playbook</h1>
<p>$(& $esc $OrganizationName) | $($actionable.Count) actionable item(s) | Max cost: $MaxCostTier | $timestamp UTC</p>
"@
)

    $itemNum = 0
    foreach ($phase in $phases) {
        $phaseItems = @($actionable | Where-Object Severity -eq $phase.Severity |
            Sort-Object @{Expression={$tierOrder[$_._CostTier]}}, CheckId)
        if ($phaseItems.Count -eq 0) { continue }

        [void]$html.Append("<h2 style='background:var(--surface);border-left:4px solid $($phase.Color);color:$($phase.Color);'>$($phase.Name) ($($phaseItems.Count) items)</h2>`n")

        foreach ($item in $phaseItems) {
            $itemNum++
            $checkId = $item.CheckId ?? $item.Id ?? ''
            $effortHours = switch ($item._Effort) { 'Minimal' { '~15min' } 'Low' { '~1h' } 'Medium' { '~4h' } 'High' { '~2d' } 'Major' { '~2w' } default { '~4h' } }

            [void]$html.Append(@"
<div class="item">
<div class="item-header">
<div class="title">$itemNum. $(& $esc ($item.Name ?? $item.CheckName ?? $checkId))</div>
<div><span class="badge" style="background:$($phase.Color);color:#fff;">$($item.Severity)</span></div>
</div>
<div class="meta">
<span>ID: $checkId</span>
<span>Cost: $($item._CostTier)</span>
<span>Effort: $effortHours</span>
<span>Category: $(& $esc ($item.Category ?? ''))</span>
</div>
$(if ($item.Description) { "<p style='margin:8px 0;'>$(& $esc $item.Description)</p>" })
$(if ($item.RemediationSteps) {
    "<p style='margin:8px 0;'><strong>Steps:</strong> $(& $esc $item.RemediationSteps)</p>"
})
$(if ($item.RecommendedValue) { "<p style='margin:4px 0;color:var(--text-muted);'><strong>Target:</strong> $(& $esc $item.RecommendedValue)</p>" })
$(if ($item._Notes) { "<p style='margin:4px 0;color:var(--text-muted);font-style:italic;'>Note: $(& $esc $item._Notes)</p>" })
$(if ($item.RemediationUrl) { "<p style='margin:4px 0;'><a href='$(& $esc $item.RemediationUrl)' style='color:var(--olive);font-size:0.85em;'>$(& $esc $item.RemediationUrl)</a></p>" })
</div>
"@
)
        }
    }

    [void]$html.Append(@"
<div class="footer">
<p>Generated by PSGuerrilla v2.1.0 | $timestamp UTC</p>
<p style="font-style:italic;">Review each remediation step before implementing. Test changes in a non-production environment first.</p>
</div>
</div>
</body>
</html>
"@
)

    $html.ToString() | Set-Content -Path $OutputPath -Encoding UTF8

    return [PSCustomObject]@{
        PSTypeName = 'PSGuerrilla.RemediationPlaybook'
        Success    = $true
        Path       = (Resolve-Path $OutputPath).Path
        Message    = "Remediation playbook exported to $OutputPath"
        ItemCount  = $itemNum
    }
}