Common/Export-ComplianceMatrix.ps1
|
<# .SYNOPSIS Exports compliance overview data as a formatted XLSX workbook. .DESCRIPTION Reads security config CSVs from an assessment folder, looks up each CheckId in the control registry, and generates an XLSX file with up to four sheets: Sheet 1 - Compliance Matrix (one row per check; framework columns + SCF impact/domain) Sheet 2 - Summary (pass/fail counts and coverage per framework) Sheet 3 - Grouped by Profile (CIS M365 profile-level breakdown) Sheet 4 - Verification (one row per SCF assessment objective -- audit guidance) Framework columns are auto-discovered from JSON definitions in controls/frameworks/. SCF impact and verification data require CheckID v2.0.0 registry entries. Requires the ImportExcel module. If not available, logs a warning and returns. .PARAMETER AssessmentFolder Path to the assessment output folder containing collector CSVs and the summary file. .PARAMETER TenantName Optional tenant name used in the output filename. If omitted, derived from the summary CSV filename. .EXAMPLE .\Common\Export-ComplianceMatrix.ps1 -AssessmentFolder .\M365-Assessment\Assessment_20260311_033912_contoso .NOTES Requires: ImportExcel module (Install-Module ImportExcel -Scope CurrentUser) #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$AssessmentFolder, [Parameter()] [string]$TenantName ) $ErrorActionPreference = 'Stop' # ------------------------------------------------------------------ # Check for ImportExcel module # ------------------------------------------------------------------ if (-not (Get-Module -ListAvailable -Name ImportExcel)) { Write-Warning "ImportExcel module not available — skipping XLSX compliance matrix export. Install with: Install-Module ImportExcel -Scope CurrentUser" return } Import-Module ImportExcel -ErrorAction Stop # ------------------------------------------------------------------ # Validate input # ------------------------------------------------------------------ if (-not (Test-Path -Path $AssessmentFolder -PathType Container)) { Write-Error "Assessment folder not found: $AssessmentFolder" return } # ------------------------------------------------------------------ # Load control registry + framework definitions # ------------------------------------------------------------------ $projectRoot = Split-Path -Parent (Split-Path -Parent $PSCommandPath) . (Join-Path -Path $PSScriptRoot -ChildPath 'Import-ControlRegistry.ps1') $controlsPath = Join-Path -Path $projectRoot -ChildPath 'controls' $controlRegistry = Import-ControlRegistry -ControlsPath $controlsPath $riskSeverityPath = Join-Path -Path $controlsPath -ChildPath 'risk-severity.json' $riskSeverity = @{} if (Test-Path -Path $riskSeverityPath) { $riskJson = Get-Content -Path $riskSeverityPath -Raw | ConvertFrom-Json -AsHashtable if ($riskJson.ContainsKey('checks')) { $riskSeverity = $riskJson['checks'] } } if ($controlRegistry.Count -eq 0) { Write-Warning "Control registry is empty — cannot generate compliance matrix." return } . (Join-Path -Path $PSScriptRoot -ChildPath 'Import-FrameworkDefinitions.ps1') $allFrameworks = Import-FrameworkDefinitions -FrameworksPath (Join-Path -Path $projectRoot -ChildPath 'controls/frameworks') # ------------------------------------------------------------------ # Derive tenant name if not provided # ------------------------------------------------------------------ if (-not $TenantName) { $summaryFile = Get-ChildItem -Path $AssessmentFolder -Filter '_Assessment-Summary*.csv' -ErrorAction SilentlyContinue | Select-Object -First 1 if ($summaryFile -and $summaryFile.Name -match '_Assessment-Summary_(.+)\.csv$') { $TenantName = $Matches[1] } else { $TenantName = 'tenant' } } # ------------------------------------------------------------------ # Load assessment summary to identify collector CSVs # ------------------------------------------------------------------ $summaryFile = Get-ChildItem -Path $AssessmentFolder -Filter '_Assessment-Summary*.csv' -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $summaryFile) { Write-Error "Assessment summary CSV not found in: $AssessmentFolder" return } $summary = Import-Csv -Path $summaryFile.FullName # ------------------------------------------------------------------ # Scan CSVs and build findings with dynamic framework columns # ------------------------------------------------------------------ $findings = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($c in $summary) { if ($c.Status -ne 'Complete' -or [int]$c.Items -eq 0) { continue } $csvFile = Join-Path -Path $AssessmentFolder -ChildPath $c.FileName if (-not (Test-Path -Path $csvFile)) { continue } $data = Import-Csv -Path $csvFile if (-not $data -or @($data).Count -eq 0) { continue } $columns = @($data[0].PSObject.Properties.Name) if ($columns -notcontains 'CheckId') { continue } foreach ($row in $data) { if (-not $row.CheckId -or $row.CheckId -eq '') { continue } $baseCheckId = $row.CheckId -replace '\.\d+$', '' $entry = if ($controlRegistry.ContainsKey($baseCheckId)) { $controlRegistry[$baseCheckId] } else { $null } $fw = if ($entry) { $entry.frameworks } else { @{} } # Fixed columns $finding = [ordered]@{ CheckId = $row.CheckId Setting = $row.Setting Category = $row.Category Status = $row.Status RiskSeverity = if ($riskSeverity.ContainsKey($baseCheckId)) { $riskSeverity[$baseCheckId] } else { '' } ImpactSeverity = if ($entry -and $entry.impactRating) { $entry.impactRating.severity } else { '' } ImpactRationale = if ($entry -and $entry.impactRating) { $entry.impactRating.rationale } else { '' } SCFDomain = if ($entry -and $entry.scf) { $entry.scf.domain } else { '' } CSFFunction = if ($entry -and $entry.scf) { $entry.scf.csfFunction } else { '' } SCFWeight = if ($entry -and $entry.scf) { $entry.scf.relativeWeighting } else { '' } Source = $c.Collector Remediation = $row.Remediation } # Dynamic framework columns (one per framework, sorted by displayOrder) foreach ($fwDef in $allFrameworks) { $fwData = $fw.($fwDef.frameworkId) if ($fwData -and $fwData.controlId) { $cellValue = $fwData.controlId # Profile-based frameworks: append inline profile tags if ($fwData.profiles -and @($fwData.profiles).Count -gt 0) { $tags = @($fwData.profiles | ForEach-Object { "[$_]" }) -join '' $cellValue = "$cellValue $tags" } $finding[$fwDef.label] = $cellValue } else { $finding[$fwDef.label] = '' } } $findings.Add([PSCustomObject]$finding) } } if ($findings.Count -eq 0) { Write-Warning "No CheckId-mapped findings found — skipping XLSX export." return } # Sort by CheckId $sortedFindings = $findings | Sort-Object -Property CheckId # ------------------------------------------------------------------ # Build summary data (one row per framework) # ------------------------------------------------------------------ $summaryData = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($fwDef in $allFrameworks) { $colLabel = $fwDef.label $mapped = @($sortedFindings | Where-Object { $_.$colLabel -and $_.$colLabel -ne '' -and $_.Status -ne 'Info' }) $totalMapped = $mapped.Count if ($totalMapped -eq 0) { $summaryData.Add([PSCustomObject][ordered]@{ Framework = $colLabel 'Total Mapped' = 0 Pass = 0 Fail = 0 Warning = 0 Review = 0 'Pass Rate %' = 'N/A' }) continue } $pass = @($mapped | Where-Object { $_.Status -eq 'Pass' }).Count $fail = @($mapped | Where-Object { $_.Status -eq 'Fail' }).Count $warn = @($mapped | Where-Object { $_.Status -eq 'Warning' }).Count $review = @($mapped | Where-Object { $_.Status -eq 'Review' }).Count $pct = [math]::Round(($pass / $totalMapped) * 100, 1) $summaryData.Add([PSCustomObject][ordered]@{ Framework = $colLabel 'Total Mapped' = $totalMapped Pass = $pass Fail = $fail Warning = $warn Review = $review 'Pass Rate %' = $pct }) } # ------------------------------------------------------------------ # Load scoring engine + build catalog findings (shared by Summary sub-rows + Sheet 3) # ------------------------------------------------------------------ $catalogPath = Join-Path -Path $PSScriptRoot -ChildPath 'Export-FrameworkCatalog.ps1' $catalogFindings = $null if ((Test-Path -Path $catalogPath) -and $findings.Count -gt 0) { . $catalogPath $catalogFindings = @($sortedFindings | ForEach-Object { $fwHash = @{} foreach ($fwDef in $allFrameworks) { $baseId = $_.CheckId -replace '\.\d+$', '' if ($controlRegistry.ContainsKey($baseId) -and $controlRegistry[$baseId].frameworks) { $fwObj = $controlRegistry[$baseId].frameworks if ($fwObj -is [hashtable] -and $fwObj.ContainsKey($fwDef.frameworkId)) { $fwHash[$fwDef.frameworkId] = $fwObj[$fwDef.frameworkId] } elseif ($fwObj -and $fwObj.PSObject.Properties.Name -contains $fwDef.frameworkId) { $fwHash[$fwDef.frameworkId] = $fwObj.($fwDef.frameworkId) } } } [PSCustomObject]@{ CheckId = $_.CheckId Setting = $_.Setting Status = $_.Status RiskSeverity = 'Medium' Section = $_.Source Frameworks = $fwHash } }) } # Build expanded summary: parent framework row + sub-rows for profile/maturity frameworks # profile-compliance (CIS): individual profile rows + a Combined row per license tier # maturity-level (CMMC): individual level rows only (already cumulative per level) $summaryExpanded = [System.Collections.Generic.List[PSCustomObject]]::new() $groupedByFwCache = @{} foreach ($summaryRow in $summaryData) { $summaryExpanded.Add($summaryRow) if ($null -eq $catalogFindings) { continue } $fwDef = $allFrameworks | Where-Object { $_.label -eq $summaryRow.Framework } | Select-Object -First 1 if (-not $fwDef -or $fwDef.scoringMethod -notin @('profile-compliance', 'maturity-level')) { continue } $grpResult = Export-FrameworkCatalog -Findings $catalogFindings -Framework $fwDef -ControlRegistry $controlRegistry -Mode Grouped -WarningAction SilentlyContinue if (-not $grpResult -or -not $grpResult.Groups) { continue } $groupedByFwCache[$fwDef.frameworkId] = $grpResult if ($fwDef.scoringMethod -eq 'profile-compliance') { # Detect tier groups: keys like 'E3-L1','E3-L2' share tier prefix 'E3' $tierMap = [ordered]@{} foreach ($grp in $grpResult.Groups) { if ($grp.IsGap) { continue } if ($grp.Key -match '^(.+)-L\d+$') { $tk = $Matches[1] if (-not $tierMap.ContainsKey($tk)) { $tierMap[$tk] = [System.Collections.Generic.List[hashtable]]::new() } $tierMap[$tk].Add($grp) } } $emittedTiers = [System.Collections.Generic.HashSet[string]]::new() foreach ($group in $grpResult.Groups) { if ($group.IsGap) { continue } $subRate = if ($group.Mapped -gt 0) { [math]::Round(($group.Passed / $group.Mapped) * 100, 1) } else { 0 } $summaryExpanded.Add([PSCustomObject][ordered]@{ Framework = " $($group.Key) — $($group.Label)" 'Total Mapped' = $group.Mapped Pass = $group.Passed Fail = $group.Failed Warning = $group.Warning Review = $group.Review 'Pass Rate %' = $subRate }) if ($group.Key -match '^(.+)-L\d+$') { $tierKey = $Matches[1] if ($tierMap.ContainsKey($tierKey) -and -not $emittedTiers.Contains($tierKey)) { $sortedTierLevels = @($tierMap[$tierKey] | Sort-Object { $_.Key }) if ($group.Key -eq $sortedTierLevels[-1].Key) { [void]$emittedTiers.Add($tierKey) # Filter unique findings tagged with this tier; avoids L1/L2 double-counting $colLabel = $fwDef.label $tierPattern = "\[$tierKey-" $tierFindings = @($sortedFindings | Where-Object { $_.$colLabel -match $tierPattern -and $_.Status -ne 'Info' }) if ($tierFindings.Count -gt 0) { $cPass = @($tierFindings | Where-Object Status -eq 'Pass').Count $cFail = @($tierFindings | Where-Object Status -eq 'Fail').Count $cWarn = @($tierFindings | Where-Object Status -eq 'Warning').Count $cReview = @($tierFindings | Where-Object Status -eq 'Review').Count $cRate = [math]::Round(($cPass / $tierFindings.Count) * 100, 1) $levelKeys = @($sortedTierLevels | ForEach-Object { ($_.Key -split '-')[-1] }) -join '+' $summaryExpanded.Add([PSCustomObject][ordered]@{ Framework = " $tierKey Combined ($levelKeys)" 'Total Mapped' = $tierFindings.Count Pass = $cPass Fail = $cFail Warning = $cWarn Review = $cReview 'Pass Rate %' = $cRate }) } } } } } } else { # maturity-level: CMMC is already cumulative per level, emit groups as-is foreach ($group in $grpResult.Groups) { if ($group.IsGap) { continue } $subRate = if ($group.Mapped -gt 0) { [math]::Round(($group.Passed / $group.Mapped) * 100, 1) } else { 0 } $summaryExpanded.Add([PSCustomObject][ordered]@{ Framework = " $($group.Key) — $($group.Label)" 'Total Mapped' = $group.Mapped Pass = $group.Passed Fail = $group.Failed Warning = $group.Warning Review = $group.Review 'Pass Rate %' = $subRate }) } } } # ------------------------------------------------------------------ # Export to XLSX # ------------------------------------------------------------------ $outputFile = Join-Path -Path $AssessmentFolder -ChildPath "_Compliance-Matrix_$TenantName.xlsx" # Remove existing file to avoid append issues if (Test-Path -Path $outputFile) { Remove-Item -Path $outputFile -Force } # Sheet 1 - Compliance Matrix $matrixParams = @{ Path = $outputFile WorksheetName = 'Compliance Matrix' AutoSize = $true AutoFilter = $true FreezeTopRow = $true BoldTopRow = $true TableStyle = 'Medium2' } $sortedFindings | Export-Excel @matrixParams # Sheet 2 - Summary (with profile/maturity sub-rows for CIS and CMMC) $summaryParams = @{ Path = $outputFile WorksheetName = 'Summary' AutoSize = $true FreezeTopRow = $true BoldTopRow = $true TableStyle = 'Medium6' } $summaryExpanded | Export-Excel @summaryParams # Sheet 3 - Grouped by Profile (CIS M365 profile-compliance breakdown) $cisFw = $allFrameworks | Where-Object { $_.frameworkId -like 'cis-m365-*' } | Select-Object -First 1 if ($cisFw -and $null -ne $catalogFindings) { $groupedResult = if ($groupedByFwCache.ContainsKey($cisFw.frameworkId)) { $groupedByFwCache[$cisFw.frameworkId] } else { Export-FrameworkCatalog -Findings $catalogFindings -Framework $cisFw -ControlRegistry $controlRegistry -Mode Grouped -WarningAction SilentlyContinue } if ($groupedResult -and $groupedResult.Groups) { $groupedRows = [System.Collections.Generic.List[PSCustomObject]]::new() $sheet3TierMap = [ordered]@{} foreach ($grp in $groupedResult.Groups) { if ($grp.IsGap) { continue } if ($grp.Key -match '^(.+)-L\d+$') { $tk = $Matches[1] if (-not $sheet3TierMap.ContainsKey($tk)) { $sheet3TierMap[$tk] = [System.Collections.Generic.List[hashtable]]::new() } $sheet3TierMap[$tk].Add($grp) } } $sheet3EmittedTiers = [System.Collections.Generic.HashSet[string]]::new() foreach ($group in $groupedResult.Groups) { if ($group.IsGap) { continue } $grpPassRate = if ($group.Mapped -gt 0) { [math]::Round(($group.Passed / $group.Mapped) * 100, 1) } else { 0 } $groupedRows.Add([PSCustomObject][ordered]@{ Profile = $group.Key Label = $group.Label Total = $group.Total Mapped = $group.Mapped Passed = $group.Passed Failed = $group.Failed Warning = $group.Warning Review = $group.Review 'Pass Rate %' = $grpPassRate }) if ($group.Key -match '^(.+)-L\d+$') { $tierKey = $Matches[1] if ($sheet3TierMap.ContainsKey($tierKey) -and -not $sheet3EmittedTiers.Contains($tierKey)) { $sortedTierLevels = @($sheet3TierMap[$tierKey] | Sort-Object { $_.Key }) if ($group.Key -eq $sortedTierLevels[-1].Key) { [void]$sheet3EmittedTiers.Add($tierKey) $colLabel = $cisFw.label $tierPattern = "\[$tierKey-" $tierFindings = @($sortedFindings | Where-Object { $_.$colLabel -match $tierPattern -and $_.Status -ne 'Info' }) if ($tierFindings.Count -gt 0) { $cPass = @($tierFindings | Where-Object Status -eq 'Pass').Count $cFail = @($tierFindings | Where-Object Status -eq 'Fail').Count $cWarn = @($tierFindings | Where-Object Status -eq 'Warning').Count $cReview = @($tierFindings | Where-Object Status -eq 'Review').Count $cRate = [math]::Round(($cPass / $tierFindings.Count) * 100, 1) $levelKeys = @($sortedTierLevels | ForEach-Object { ($_.Key -split '-')[-1] }) -join '+' $groupedRows.Add([PSCustomObject][ordered]@{ Profile = "$tierKey Combined ($levelKeys)" Label = "All $tierKey controls (L1 + L2)" Total = ($sortedTierLevels | Measure-Object { $_.Total } -Sum).Sum Mapped = $tierFindings.Count Passed = $cPass Failed = $cFail Warning = $cWarn Review = $cReview 'Pass Rate %' = $cRate }) } } } } } $groupedParams = @{ Path = $outputFile WorksheetName = 'Grouped by Profile' AutoSize = $true FreezeTopRow = $true BoldTopRow = $true TableStyle = 'Medium9' } $groupedRows | Export-Excel @groupedParams } } # Sheet 4 - Verification (one row per SCF assessment objective) $verificationRows = [System.Collections.Generic.List[PSCustomObject]]::new() $seenVerifIds = [System.Collections.Generic.HashSet[string]]::new() foreach ($vFinding in $sortedFindings) { $vBaseId = $vFinding.CheckId -replace '\.\d+$', '' if (-not $seenVerifIds.Add($vBaseId)) { continue } $vEntry = if ($controlRegistry.ContainsKey($vBaseId)) { $controlRegistry[$vBaseId] } else { $null } if (-not $vEntry -or -not $vEntry.scf -or -not $vEntry.scf.assessmentObjectives) { continue } foreach ($ao in $vEntry.scf.assessmentObjectives) { $verificationRows.Add([PSCustomObject][ordered]@{ CheckId = $vBaseId 'Check Name' = $vEntry.name 'AO ID' = $ao.aoId Objective = $ao.text }) } } if ($verificationRows.Count -gt 0) { $verifParams = @{ Path = $outputFile WorksheetName = 'Verification' AutoSize = $true FreezeTopRow = $true BoldTopRow = $true TableStyle = 'Medium15' } $verificationRows | Export-Excel @verifParams } # ------------------------------------------------------------------ # Apply conditional formatting # ------------------------------------------------------------------ $pkg = Open-ExcelPackage -Path $outputFile # Matrix sheet - color-code Status, RiskSeverity, and ImpactSeverity columns $matrixSheet = $pkg.Workbook.Worksheets['Compliance Matrix'] $statusCol = 4 # Column D = Status $riskSevCol = 5 # Column E = RiskSeverity $impactSevCol = 6 # Column F = ImpactSeverity $lastRow = $matrixSheet.Dimension.End.Row for ($r = 2; $r -le $lastRow; $r++) { $val = $matrixSheet.Cells[$r, $statusCol].Value switch ($val) { 'Pass' { $matrixSheet.Cells[$r, $statusCol].Style.Font.Color.SetColor([System.Drawing.Color]::FromArgb(21, 128, 61)); $matrixSheet.Cells[$r, $statusCol].Style.Fill.PatternType = 'Solid'; $matrixSheet.Cells[$r, $statusCol].Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::FromArgb(220, 252, 231)) } 'Fail' { $matrixSheet.Cells[$r, $statusCol].Style.Font.Color.SetColor([System.Drawing.Color]::FromArgb(185, 28, 28)); $matrixSheet.Cells[$r, $statusCol].Style.Fill.PatternType = 'Solid'; $matrixSheet.Cells[$r, $statusCol].Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::FromArgb(254, 226, 226)) } 'Warning' { $matrixSheet.Cells[$r, $statusCol].Style.Font.Color.SetColor([System.Drawing.Color]::FromArgb(146, 64, 14)); $matrixSheet.Cells[$r, $statusCol].Style.Fill.PatternType = 'Solid'; $matrixSheet.Cells[$r, $statusCol].Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::FromArgb(254, 243, 199)) } 'Review' { $matrixSheet.Cells[$r, $statusCol].Style.Font.Color.SetColor([System.Drawing.Color]::FromArgb(30, 64, 175)); $matrixSheet.Cells[$r, $statusCol].Style.Fill.PatternType = 'Solid'; $matrixSheet.Cells[$r, $statusCol].Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::FromArgb(219, 234, 254)) } 'Info' { $matrixSheet.Cells[$r, $statusCol].Style.Font.Color.SetColor([System.Drawing.Color]::FromArgb(107, 114, 128)); $matrixSheet.Cells[$r, $statusCol].Style.Fill.PatternType = 'Solid'; $matrixSheet.Cells[$r, $statusCol].Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::FromArgb(243, 244, 246)) } } $sevVal = $matrixSheet.Cells[$r, $riskSevCol].Value switch ($sevVal) { 'Critical' { $matrixSheet.Cells[$r, $riskSevCol].Style.Font.Color.SetColor([System.Drawing.Color]::FromArgb(185, 28, 28)); $matrixSheet.Cells[$r, $riskSevCol].Style.Fill.PatternType = 'Solid'; $matrixSheet.Cells[$r, $riskSevCol].Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::FromArgb(254, 226, 226)) } 'High' { $matrixSheet.Cells[$r, $riskSevCol].Style.Font.Color.SetColor([System.Drawing.Color]::FromArgb(154, 52, 18)); $matrixSheet.Cells[$r, $riskSevCol].Style.Fill.PatternType = 'Solid'; $matrixSheet.Cells[$r, $riskSevCol].Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::FromArgb(255, 237, 213)) } 'Medium' { $matrixSheet.Cells[$r, $riskSevCol].Style.Font.Color.SetColor([System.Drawing.Color]::FromArgb(146, 64, 14)); $matrixSheet.Cells[$r, $riskSevCol].Style.Fill.PatternType = 'Solid'; $matrixSheet.Cells[$r, $riskSevCol].Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::FromArgb(254, 243, 199)) } 'Low' { $matrixSheet.Cells[$r, $riskSevCol].Style.Font.Color.SetColor([System.Drawing.Color]::FromArgb(21, 128, 61)); $matrixSheet.Cells[$r, $riskSevCol].Style.Fill.PatternType = 'Solid'; $matrixSheet.Cells[$r, $riskSevCol].Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::FromArgb(220, 252, 231)) } 'Info' { $matrixSheet.Cells[$r, $riskSevCol].Style.Font.Color.SetColor([System.Drawing.Color]::FromArgb(107, 114, 128)); $matrixSheet.Cells[$r, $riskSevCol].Style.Fill.PatternType = 'Solid'; $matrixSheet.Cells[$r, $riskSevCol].Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::FromArgb(243, 244, 246)) } } $impactVal = $matrixSheet.Cells[$r, $impactSevCol].Value switch ($impactVal) { 'Critical' { $matrixSheet.Cells[$r, $impactSevCol].Style.Font.Color.SetColor([System.Drawing.Color]::FromArgb(185, 28, 28)); $matrixSheet.Cells[$r, $impactSevCol].Style.Fill.PatternType = 'Solid'; $matrixSheet.Cells[$r, $impactSevCol].Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::FromArgb(254, 226, 226)) } 'High' { $matrixSheet.Cells[$r, $impactSevCol].Style.Font.Color.SetColor([System.Drawing.Color]::FromArgb(154, 52, 18)); $matrixSheet.Cells[$r, $impactSevCol].Style.Fill.PatternType = 'Solid'; $matrixSheet.Cells[$r, $impactSevCol].Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::FromArgb(255, 237, 213)) } 'Medium' { $matrixSheet.Cells[$r, $impactSevCol].Style.Font.Color.SetColor([System.Drawing.Color]::FromArgb(146, 64, 14)); $matrixSheet.Cells[$r, $impactSevCol].Style.Fill.PatternType = 'Solid'; $matrixSheet.Cells[$r, $impactSevCol].Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::FromArgb(254, 243, 199)) } 'Low' { $matrixSheet.Cells[$r, $impactSevCol].Style.Font.Color.SetColor([System.Drawing.Color]::FromArgb(21, 128, 61)); $matrixSheet.Cells[$r, $impactSevCol].Style.Fill.PatternType = 'Solid'; $matrixSheet.Cells[$r, $impactSevCol].Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::FromArgb(220, 252, 231)) } } } Close-ExcelPackage $pkg Write-Host " Compliance matrix exported: $outputFile" -ForegroundColor Green |