Src/Private/Compliance-Helpers.ps1
|
#region --- Compliance Framework Loader --- # # Compliance-Helpers.ps1 # --------------------- # Loads ACSC E8 and CIS M365 (Intune) compliance check definitions from JSON files in # Src/Compliance/ and exposes helper functions used by each Get-AbrIntune* section. # # WHY JSON DEFINITIONS? # - Control text, remediation guidance, and metadata live in one place. # - When ACSC or CIS publishes an update, you only edit the JSON -- no PS1 changes needed. # - Each check entry has a stable 'id' field for traceability across report versions. # - 'staticStatus' null means the PS1 section will compute the status at runtime via # the local variables already gathered; '[INFO]'/[OK]'/etc. means the check is always # that status regardless of tenant data. # # USAGE (called once from Invoke-AsBuiltReport.Microsoft.Intune.ps1, before the tenant loop): # Initialize-AbrIntuneComplianceFrameworks # # Then in each section, retrieve check definitions and resolve status: # $E8Checks = Build-AbrIntuneComplianceChecks -Definitions (Get-AbrIntuneE8Checks -Section 'DeviceCompliance') -Framework E8 -CallerVariables $vars # $CISChecks = Build-AbrIntuneComplianceChecks -Definitions (Get-AbrIntuneCISChecks -Section 'DeviceCompliance') -Framework CIS -CallerVariables $vars # #endregion #region --- Loader --- function Initialize-AbrIntuneComplianceFrameworks { <# .SYNOPSIS Loads ACSC E8 and CIS M365 Intune compliance definitions from JSON files. Populates $script:IntuneE8Definitions and $script:IntuneCISDefinitions. Call once before the tenant report loop. #> [CmdletBinding()] param() # Resolve the Compliance folder. # Strategy 1: walk from loaded module base # Strategy 2: walk from the function's own file $ModuleRoot = $null if ($script:IntuneModuleRoot -and (Test-Path $script:IntuneModuleRoot)) { $ModuleRoot = $script:IntuneModuleRoot } if (-not $ModuleRoot) { $thisFuncFile = $MyInvocation.MyCommand.Module.ModuleBase if ($thisFuncFile -and (Test-Path $thisFuncFile)) { $ModuleRoot = $thisFuncFile } } if (-not $ModuleRoot) { $loadedModule = Get-Module -Name 'AsBuiltReport.Microsoft.Intune' -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 $ModuleRoot 'Src\Compliance' # Also try forward-slash variant for Linux/macOS if (-not (Test-Path $ComplianceDir)) { $ComplianceDir = 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:IntuneE8Definitions = Get-Content -Path $E8Path -Raw -ErrorAction Stop | ConvertFrom-Json $E8SectionCount = @($script:IntuneE8Definitions.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 from '$E8Path': $($_.Exception.Message)" $script:IntuneE8Definitions = $null } } else { Write-Warning " [COMPLIANCE] ACSC E8 definitions file not found at: $E8Path" $script:IntuneE8Definitions = $null } # --- CIS Microsoft 365 (Intune) --- $CISPath = Join-Path $ComplianceDir 'CIS.M365.Intune.json' if (Test-Path $CISPath) { try { $script:IntuneCISDefinitions = Get-Content -Path $CISPath -Raw -ErrorAction Stop | ConvertFrom-Json $CISSectionCount = @($script:IntuneCISDefinitions.PSObject.Properties | Where-Object { $_.Name -notlike '_*' }).Count Write-Host " - CIS M365 Intune definitions loaded ($CISSectionCount sections)" -ForegroundColor Cyan } catch { Write-Warning " [COMPLIANCE] Failed to load CIS definitions from '$CISPath': $($_.Exception.Message)" $script:IntuneCISDefinitions = $null } } else { Write-Warning " [COMPLIANCE] CIS M365 Intune definitions file not found at: $CISPath" $script:IntuneCISDefinitions = $null } } #endregion #region --- Check Accessors --- function Get-AbrIntuneE8Checks { <# .SYNOPSIS Returns the array of ACSC E8 check definitions for a named section. Returns empty array if definitions are not loaded or section not found. .EXAMPLE $defs = Get-AbrIntuneE8Checks -Section 'DeviceCompliance' #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Section ) if (-not $script:IntuneE8Definitions) { Write-Warning " [COMPLIANCE] E8 definitions not loaded -- JSON file may be missing. Check Src\Compliance\ACSC.E8.json" return @() } $sectionDef = $script:IntuneE8Definitions.$Section if (-not $sectionDef) { Write-Warning " [COMPLIANCE] E8 section '$Section' not found in ACSC.E8.json" return @() } return $sectionDef.checks } function Get-AbrIntuneCISChecks { <# .SYNOPSIS Returns the array of CIS M365 check definitions for a named section. Returns empty array if definitions are not loaded or section not found. .EXAMPLE $defs = Get-AbrIntuneCISChecks -Section 'DeviceCompliance' #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Section ) if (-not $script:IntuneCISDefinitions) { Write-Warning " [COMPLIANCE] CIS definitions not loaded -- JSON file may be missing. Check Src\Compliance\CIS.M365.Intune.json" return @() } $sectionDef = $script:IntuneCISDefinitions.$Section if (-not $sectionDef) { Write-Warning " [COMPLIANCE] CIS section '$Section' not found in CIS.M365.Intune.json" return @() } return $sectionDef.checks } #endregion #region --- Runtime Check Builder --- function Build-AbrIntuneComplianceChecks { <# .SYNOPSIS Resolves a set of compliance check definitions into [pscustomobject] rows ready for New-AbrIntuneE8AssessmentTable or New-AbrIntuneCISAssessmentTable. Each definition entry has: - staticStatus: if non-null, this is the row's Status regardless of runtime data. - statusExpression: a PowerShell expression evaluated using CallerVariables when staticStatus is null. - detail / detailTemplate: detail text. Templates use {VariableName} placeholders resolved against CallerVariables. .PARAMETER Definitions Array of check definition objects from Get-AbrIntuneE8Checks or Get-AbrIntuneCISChecks. .PARAMETER Framework 'E8' or 'CIS' -- controls which property names are used in the output object. .PARAMETER CallerVariables Hashtable of name=>value pairs from the calling scope for expression evaluation and template substitution. .EXAMPLE $vars = @{ TotalCompliancePolicies = 3; UnassignedPolicies = 0; NonCompliantDevices = 5; NonCompliantPct = 2 } $checks = Build-AbrIntuneComplianceChecks -Definitions (Get-AbrIntuneE8Checks 'DeviceCompliance') -Framework E8 -CallerVariables $vars #> [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-AbrIntuneComplianceChecks: no definitions supplied for Framework=$Framework (JSON may not have loaded)" 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-AbrIntuneE8AssessmentTable { <# .SYNOPSIS Renders a standardised ACSC Essential Eight Maturity Level assessment table for Intune. Only renders if Options.ComplianceFrameworks.ACSCe8 = true in the report JSON. Pass an array of [pscustomobject] with properties: ML, Control, Status, Detail Status values: [OK], [FAIL], [WARN], [INFO] #> [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.Intune.DeviceCompliance -or $HealthCheck.Intune.EndpointSecurity -or $HealthCheck.Intune.Devices) { $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-AbrIntuneCISAssessmentTable { <# .SYNOPSIS Renders a standardised CIS Microsoft 365 Foundations Benchmark assessment table for Intune. Only renders if Options.ComplianceFrameworks.CISBaseline = true in the report JSON. Pass an array of [pscustomobject] with properties: CISControl, Level, Status, Detail CISControl: CIS control ID e.g. "6.1.1" Level: L1 or L2 Status: [OK], [FAIL], [WARN], [INFO] #> [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.Intune.DeviceCompliance -or $HealthCheck.Intune.EndpointSecurity -or $HealthCheck.Intune.Devices) { $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 |