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.') } |