modules/shared/AuditorReportBuilder.ps1

#Requires -Version 7.4
<#
.SYNOPSIS
    Auditor-driven report builder (Track F / issue #434) - SKELETON ONLY.
 
.DESCRIPTION
    Track F redesigns azure-analyzer's report from a finding-centric view
    into a control-centric, auditor-grade view: audit-style executive
    summary, per-control-domain sections (CIS, NIST, MCSB, ISO 27001),
    "Ready to remediate" appendix grouped by Remediation, evidence export,
    and diff vs. previous run.
 
    THIS FILE IS A SKELETON. Every public function throws
    [System.NotImplementedException]. Implementation is held until the
    dependency tracks land:
 
      - Track A (#428) - attack paths
      - Track B (#429) - resilience / blast-radius
      - Track C (#431) - policy coverage vs. ALZ reference
      - Track D (#432) - tool-output fidelity (ComplianceMappings, Pillar,
                         Impact, Effort, RemediationSnippets, DeepLinkUrl)
      - Track E (#433 / #466 / #462) - LLM triage verdicts
      - Track V (#430 / #467) + foundation (#435) - tier picker and
                         report-manifest.json schema
 
    Function signatures here are FROZEN by the design doc at
    docs/design/track-f-auditor-redesign.md. A future implementation PR
    fills the bodies, drops the -Skip placeholders on the tests, and
    flips the wire-up in Invoke-AzureAnalyzer.ps1 (-Profile Auditor).
 
    Pester baseline is preserved by this skeleton (no callers, no tests
    other than the skip-placeholders that assert NotImplementedException).
 
.NOTES
    See docs/design/track-f-auditor-redesign.md for the full architecture,
    layout sketches, mock JSON shapes, tier matrix, and test strategy.
#>


Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$script:AuditorReportBuilderVersion = '0.0.1-skeleton'

function Build-AuditorReport {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $InputPath,
        [Parameter(Mandatory)] [string] $EntitiesPath,
        [Parameter(Mandatory)] [string] $ManifestPath,
        [string]   $TriagePath = '',
        [string]   $PreviousRunPath = '',
        [Parameter(Mandatory)] [string] $OutputDirectory,
        [ValidateSet('auditor')] [string] $Profile = 'auditor',
        [string[]] $ControlFrameworks = @('CIS','NIST','MCSB','ISO27001'),
        [ValidateSet('PureJson','EmbeddedSqlite','SidecarSqlite','PodeViewer')]
        [string]   $Tier,
        [ValidateSet('inline','footnote','workpaper')] [string] $CitationStyle = 'workpaper',
        [switch]   $PassThru
    )
    throw [System.NotImplementedException]::new(
        'Build-AuditorReport: Track F is design-only until Tracks A-E + V land. See docs/design/track-f-auditor-redesign.md.')
}

function Resolve-AuditorContext {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $InputPath,
        [Parameter(Mandatory)] [string] $EntitiesPath,
        [Parameter(Mandatory)] [string] $ManifestPath,
        [string] $TriagePath = '',
        [string] $PreviousRunPath = '',
        [string] $Tier
    )
    
    $findings = Get-Content -Path $InputPath -Raw | ConvertFrom-Json
    $entities = Get-Content -Path $EntitiesPath -Raw | ConvertFrom-Json
    $manifest = Get-Content -Path $ManifestPath -Raw | ConvertFrom-Json
    
    $resolvedTier = if ($manifest.tier) { $manifest.tier } else { $Tier }
    
    $frameworks = if ($manifest.profile.auditor.frameworks) {
        $manifest.profile.auditor.frameworks
    } else {
        @('CIS', 'NIST', 'MCSB', 'ISO27001')
    }
    
    $context = @{
        Findings = $findings
        Entities = $entities
        Manifest = $manifest
        Tier = $resolvedTier
        Frameworks = $frameworks
    }
    
    if ($TriagePath -and (Test-Path $TriagePath)) {
        $context.TriageData = Get-Content -Path $TriagePath -Raw | ConvertFrom-Json
    }
    
    if ($PreviousRunPath -and (Test-Path $PreviousRunPath)) {
        $context.PreviousFindings = Get-Content -Path $PreviousRunPath -Raw | ConvertFrom-Json
    }
    
    return $context
}

function Get-AuditorExecutiveSummary {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [object[]] $Findings,
        [object[]] $PreviousFindings = @(),
        [string[]] $ControlFrameworks = @('CIS','NIST','MCSB','ISO27001')
    )
    
    $severityGroups = $Findings | Group-Object -Property Severity
    $severityCounts = @{}
    foreach ($group in $severityGroups) {
        $severityCounts[$group.Name] = $group.Count
    }
    
    $frameworkCoverage = @{}
    foreach ($framework in $ControlFrameworks) {
        $totalFindings = $Findings.Count
        $covered = 0
        
        foreach ($finding in $Findings) {
            if ($finding.ComplianceMappings) {
                foreach ($mapping in $finding.ComplianceMappings) {
                    if ($mapping -match "^$framework\s") {
                        $covered++
                        break
                    }
                }
            }
        }
        
        $pct = if ($totalFindings -gt 0) {
            [math]::Round(($covered / $totalFindings) * 100, 1)
        } else {
            0
        }
        
        $frameworkCoverage[$framework] = @{
            covered = $covered
            total = $totalFindings
            pct = $pct
        }
    }
    
    $summary = @{
        severityCounts = $severityCounts
        frameworkCoverage = $frameworkCoverage
        collectedAt = (Get-Date).ToUniversalTime().ToString('o')
        scope = 'Tenant scope placeholder'
    }
    
    if ($PreviousFindings.Count -gt 0) {
        $currentIds = $Findings | ForEach-Object { $_.FindingId } | Where-Object { $_ }
        $previousIds = $PreviousFindings | ForEach-Object { $_.FindingId } | Where-Object { $_ }
        
        $added = ($currentIds | Where-Object { $_ -notin $previousIds }).Count
        $resolved = ($previousIds | Where-Object { $_ -notin $currentIds }).Count
        
        $changedSeverity = 0
        $currentLookup = @{}
        foreach ($f in $Findings) {
            if ($f.FindingId) {
                $currentLookup[$f.FindingId] = $f.Severity
            }
        }
        
        foreach ($prevFinding in $PreviousFindings) {
            if ($prevFinding.FindingId -and $currentLookup.ContainsKey($prevFinding.FindingId)) {
                if ($prevFinding.Severity -ne $currentLookup[$prevFinding.FindingId]) {
                    $changedSeverity++
                }
            }
        }
        
        $summary.diffSummary = @{
            added = $added
            resolved = $resolved
            changedSeverity = $changedSeverity
        }
    }
    
    return $summary
}

function Get-AuditorControlDomainSections {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [object[]] $Findings,
        [Parameter(Mandatory)] [string[]] $Frameworks
    )
    throw [System.NotImplementedException]::new('Get-AuditorControlDomainSections: skeleton only.')
}

function Get-AuditorAttackPathSection {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [object] $Entities,
        [Parameter(Mandatory)] [string] $Tier
    )
    throw [System.NotImplementedException]::new('Get-AuditorAttackPathSection: requires Track A (#428).')
}

function Get-AuditorResilienceSection {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [object] $Entities,
        [Parameter(Mandatory)] [string] $Tier
    )
    throw [System.NotImplementedException]::new('Get-AuditorResilienceSection: requires Track B (#429).')
}

function Get-AuditorPolicyCoverageSection {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [object] $Entities,
        [Parameter(Mandatory)] [object[]] $Findings
    )
    throw [System.NotImplementedException]::new('Get-AuditorPolicyCoverageSection: requires Track C (#431).')
}

function Get-AuditorTriageAnnotations {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [object[]] $Findings,
        [string] $TriagePath = ''
    )
    throw [System.NotImplementedException]::new('Get-AuditorTriageAnnotations: requires Track E (#433/#466).')
}

function Get-AuditorRemediationAppendix {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [object[]] $Findings
    )
    throw [System.NotImplementedException]::new('Get-AuditorRemediationAppendix: skeleton only.')
}

function Get-AuditorEvidenceExport {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [object[]] $Findings,
        [Parameter(Mandatory)] [string]   $OutputDirectory,
        [string[]] $Formats = @('csv','json')
    )
    throw [System.NotImplementedException]::new('Get-AuditorEvidenceExport: skeleton only.')
}

function Write-AuditorRenderTier {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [hashtable] $Context,
        [Parameter(Mandatory)] [string]    $OutputDirectory,
        [Parameter(Mandatory)]
        [ValidateSet('PureJson','EmbeddedSqlite','SidecarSqlite','PodeViewer')]
        [string] $Tier
    )
    throw [System.NotImplementedException]::new('Write-AuditorRenderTier: requires Track V (#430) tier contract.')
}

function New-AuditorCitation {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [object] $Finding,
        [ValidateSet('inline','footnote','workpaper')] [string] $Style = 'workpaper'
    )
    throw [System.NotImplementedException]::new('New-AuditorCitation: skeleton only.')
}