Src/Private/Compliance-Helpers.ps1
|
#region --- Compliance Framework Loader --- # # Compliance-Helpers.ps1 # --------------------- # Loads ACSC E8 and CIS M365 compliance check definitions from JSON files in # Src/Compliance/ and exposes helper functions used by each Get-AbrExo* section. # #endregion #region --- Loader --- function Initialize-AbrExoComplianceFrameworks { <# .SYNOPSIS Loads ACSC E8 and CIS M365 compliance definitions from JSON files. Populates $script:E8Definitions and $script:CISDefinitions. Call once before the tenant report loop. #> [CmdletBinding()] param() $ModuleRoot = $null if ($script:ExoModuleRoot -and (Test-Path $script:ExoModuleRoot)) { $ModuleRoot = $script:ExoModuleRoot } if (-not $ModuleRoot) { $loadedModule = Get-Module -Name 'AsBuiltReport.Microsoft.ExchangeOnline' -ErrorAction SilentlyContinue if ($loadedModule -and $loadedModule.ModuleBase) { $ModuleRoot = $loadedModule.ModuleBase } } if (-not $ModuleRoot) { Write-Warning ' [COMPLIANCE] Cannot determine module root path -- compliance JSON will not load.' return } $ComplianceDir = Join-Path (Join-Path $ModuleRoot 'Src') 'Compliance' Write-Host " - Loading compliance definitions from: $ComplianceDir" -ForegroundColor Cyan # --- ACSC Essential Eight --- $E8Path = Join-Path $ComplianceDir 'ACSC.E8.json' if (Test-Path $E8Path) { try { $script:E8Definitions = Get-Content -Path $E8Path -Raw -ErrorAction Stop | ConvertFrom-Json $E8SectionCount = @($script:E8Definitions.PSObject.Properties | Where-Object { $_.Name -notlike '_*' }).Count Write-Host " - ACSC E8 definitions loaded ($E8SectionCount sections)" -ForegroundColor Cyan } catch { Write-Warning " [COMPLIANCE] Failed to load ACSC E8 definitions: $($_.Exception.Message)" $script:E8Definitions = $null } } else { Write-Warning " [COMPLIANCE] ACSC E8 definitions file not found at: $E8Path" $script:E8Definitions = $null } # --- CIS Microsoft 365 --- $CISPath = Join-Path $ComplianceDir 'CIS.M365.json' if (Test-Path $CISPath) { try { $script:CISDefinitions = Get-Content -Path $CISPath -Raw -ErrorAction Stop | ConvertFrom-Json $CISSectionCount = @($script:CISDefinitions.PSObject.Properties | Where-Object { $_.Name -notlike '_*' }).Count Write-Host " - CIS M365 definitions loaded ($CISSectionCount sections)" -ForegroundColor Cyan } catch { Write-Warning " [COMPLIANCE] Failed to load CIS M365 definitions: $($_.Exception.Message)" $script:CISDefinitions = $null } } else { Write-Warning " [COMPLIANCE] CIS M365 definitions file not found at: $CISPath" $script:CISDefinitions = $null } } #endregion #region --- Check Accessors --- function Get-AbrExoE8Checks { [CmdletBinding()] param( [Parameter(Mandatory)][string]$Section ) if (-not $script:E8Definitions) { Write-Warning " [COMPLIANCE] E8 definitions not loaded -- Src\Compliance\ACSC.E8.json missing?" return @() } $sectionDef = $script:E8Definitions.$Section if (-not $sectionDef) { Write-Warning " [COMPLIANCE] E8 section '$Section' not found in ACSC.E8.json" return @() } return $sectionDef.checks } function Get-AbrExoCISChecks { [CmdletBinding()] param( [Parameter(Mandatory)][string]$Section ) if (-not $script:CISDefinitions) { Write-Warning " [COMPLIANCE] CIS definitions not loaded -- Src\Compliance\CIS.M365.json missing?" return @() } $sectionDef = $script:CISDefinitions.$Section if (-not $sectionDef) { Write-Warning " [COMPLIANCE] CIS section '$Section' not found in CIS.M365.json" return @() } return $sectionDef.checks } #endregion #region --- Runtime Check Builder --- function Build-AbrExoComplianceChecks { <# .SYNOPSIS Resolves a set of compliance check definitions into [pscustomobject] rows ready for New-AbrExoE8AssessmentTable or New-AbrExoCISAssessmentTable. #> [CmdletBinding()] param( [Parameter()][object[]]$Definitions, [Parameter(Mandatory)][ValidateSet('E8','CIS')][string]$Framework, [Parameter(Mandatory)][hashtable]$CallerVariables ) if (-not $Definitions -or $Definitions.Count -eq 0) { Write-Warning " [COMPLIANCE] Build-AbrExoComplianceChecks: no definitions supplied for Framework=$Framework" return @() } $result = [System.Collections.ArrayList]::new() foreach ($def in $Definitions) { # --- Resolve Status --- $Status = $null if ($def.staticStatus) { $Status = $def.staticStatus } elseif ($def.statusExpression) { try { $sb = [scriptblock]::Create($def.statusExpression) $Status = & { foreach ($kv in $CallerVariables.GetEnumerator()) { Set-Variable -Name $kv.Key -Value $kv.Value -Scope Local } & $sb } } catch { $Status = '[INFO]' Write-AbrDebugLog "Status expression eval failed for check '$($def.id)': $($_.Exception.Message)" 'WARN' 'COMPLIANCE' } } else { $Status = '[INFO]' } # --- Resolve Detail --- $Detail = '' $rawDetail = if ($def.detail) { $def.detail } elseif ($def.detailTemplate) { $def.detailTemplate } else { '' } if ($rawDetail) { $Detail = $rawDetail foreach ($kv in $CallerVariables.GetEnumerator()) { $Detail = $Detail -replace "\{$([regex]::Escape($kv.Key))\}", [string]$kv.Value } } # --- Build output object --- $row = if ($Framework -eq 'E8') { [pscustomobject]@{ 'ML' = $def.ML 'Control' = $def.control 'Status' = $Status 'Detail' = $Detail } } else { [pscustomobject]@{ 'CISControl' = $def.CISControl 'Level' = $def.Level 'Status' = $Status 'Detail' = $Detail } } $null = $result.Add($row) } return $result.ToArray() } #endregion #region --- Table Renderers --- function New-AbrExoE8AssessmentTable { <# .SYNOPSIS Renders a standardised ACSC Essential Eight assessment table for Exchange Online. Only renders if Options.ComplianceFrameworks.ACSCe8 = true. #> [CmdletBinding()] param( [Parameter()][AllowNull()][AllowEmptyCollection()][object[]]$Checks, [Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][string]$TenantId ) if (-not $script:IncludeACSCe8) { return } if (-not $Checks -or $Checks.Count -eq 0) { return } $null = (& { if ($HealthCheck.ExchangeOnline.AntiSpam -or $HealthCheck.ExchangeOnline.AntiMalware -or $HealthCheck.ExchangeOnline.AntiPhishing -or $HealthCheck.ExchangeOnline.DKIM -or $HealthCheck.ExchangeOnline.DMARC -or $HealthCheck.ExchangeOnline.TransportRules) { $null = ($Checks | Where-Object { $_.'Status' -eq '[FAIL]' } | Set-Style -Style Critical | Out-Null) $null = ($Checks | Where-Object { $_.'Status' -eq '[WARN]' } | Set-Style -Style Warning | Out-Null) $null = ($Checks | Where-Object { $_.'Status' -eq '[OK]' } | Set-Style -Style OK | Out-Null) } }) $TableParams = @{ Name = "ACSC E8 $Name - $TenantId"; List = $false; ColumnWidths = 8, 32, 9, 51 } if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } $Checks | Table @TableParams } function New-AbrExoCISAssessmentTable { <# .SYNOPSIS Renders a standardised CIS Microsoft 365 Foundations Benchmark assessment table for Exchange Online. Only renders if Options.ComplianceFrameworks.CISBaseline = true. #> [CmdletBinding()] param( [Parameter()][AllowNull()][AllowEmptyCollection()][object[]]$Checks, [Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][string]$TenantId ) if (-not $script:IncludeCISBaseline) { return } if (-not $Checks -or $Checks.Count -eq 0) { return } $null = (& { if ($HealthCheck.ExchangeOnline.AntiSpam -or $HealthCheck.ExchangeOnline.AntiMalware -or $HealthCheck.ExchangeOnline.AntiPhishing -or $HealthCheck.ExchangeOnline.DKIM -or $HealthCheck.ExchangeOnline.DMARC) { $null = ($Checks | Where-Object { $_.'Status' -eq '[FAIL]' } | Set-Style -Style Critical | Out-Null) $null = ($Checks | Where-Object { $_.'Status' -eq '[WARN]' } | Set-Style -Style Warning | Out-Null) $null = ($Checks | Where-Object { $_.'Status' -eq '[OK]' } | Set-Style -Style OK | Out-Null) } }) $TableParams = @{ Name = "CIS Baseline $Name - $TenantId"; List = $false; ColumnWidths = 10, 7, 9, 74 } if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } $Checks | Table @TableParams } #endregion |