internal/Audit/Get-GuerrillaFailedCategoryFinding.ps1
|
# Guerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0 # https://github.com/jimrtyler/Guerrilla | https://creativecommons.org/licenses/by/4.0/ # AI/LLM use: see AI-USAGE.md for required attribution function Get-GuerrillaCategoryDefinitionName { <# .SYNOPSIS Maps a category check function name to its Data/AuditChecks definition file base name. .DESCRIPTION Convention: Invoke-<Name> reads Data/AuditChecks/<Name>.json (verified for every shipped category function). The one exception is the data-driven EIDSCA dispatcher, whose catalog is EidscaChecks.json. #> [CmdletBinding()] param([Parameter(Mandatory)][string]$CategoryFunction) switch ($CategoryFunction) { 'Invoke-EntraEidscaChecks' { return 'EidscaChecks' } default { return ($CategoryFunction -replace '^Invoke-', '') } } } function Get-GuerrillaFailedCategoryFinding { <# .SYNOPSIS Synthesizes Not-Assessed (ERROR) findings for every check a failed category function would have produced. .DESCRIPTION When a category check function THROWS, it contributes no findings at all — and in the next run-over-run comparison its checks would classify as benign "retired" (present before, absent now) instead of lost visibility. That is the exact failure mode the run history exists to expose: a broken collector must never look like a retired check set. This helper loads the failed function's check definitions and emits one ERROR finding per check ("Not Assessed" in the run record), so the checks stay present in the record and classify as LostVisibility / StillNotAssessed. If the definitions genuinely cannot be loaded, it falls back to a single synthesized ERROR finding whose id is derived from the category — the failure is then still visible in the diff (as one new/lost check) rather than silent. Findings are emitted to the pipeline (zero or more objects). #> [CmdletBinding()] param( # The category check function that failed, e.g. 'Invoke-ADTrustChecks'. [Parameter(Mandatory)][string]$CategoryFunction, # Why it failed (exception message); recorded in the finding evidence. [Parameter(Mandatory)][AllowEmptyString()][string]$Reason, # Org unit scope the category would have been evaluated at (GWS audits pass a TargetOU). [string]$OrgUnitPath = '/' ) $defCategory = Get-GuerrillaCategoryDefinitionName -CategoryFunction $CategoryFunction $defs = $null try { $defs = Get-AuditCategoryDefinitions -Category $defCategory } catch { Write-Verbose "Get-GuerrillaFailedCategoryFinding: no definitions for '$defCategory': $_" } $currentValue = "Not Assessed — check category '$defCategory' failed to run ($Reason). " + 'These controls were not evaluated; absence of evidence is not compliance.' $details = @{ NotAssessed = $true FailedCategoryFunction = $CategoryFunction CollectionError = "$Reason" } if ($defs -and @($defs.checks).Count -gt 0) { foreach ($check in @($defs.checks)) { New-AuditFinding -CheckDefinition $check -Status 'ERROR' ` -CurrentValue $currentValue -OrgUnitPath $OrgUnitPath -Details $details } return } # Fallback: the check set could not be determined. One loud synthetic ERROR # finding per failed category, so the failure surfaces in the diff instead of # the whole category silently vanishing. $syntheticDef = @{ id = "CATFAIL-$($defCategory.ToUpperInvariant())" name = "Check category '$defCategory' failed to run" severity = 'High' zeroTrustPillar = '' description = "The category function $CategoryFunction threw before producing findings, and its check definitions could not be loaded." _categoryName = $defCategory } New-AuditFinding -CheckDefinition $syntheticDef -Status 'ERROR' ` -CurrentValue $currentValue -OrgUnitPath $OrgUnitPath -Details $details } function Get-GuerrillaPlatformCheckFunction { <# .SYNOPSIS The full list of category check functions a platform audit runs (Categories = All). .DESCRIPTION Used by Invoke-Campaign to synthesize Not-Assessed findings for EVERY check of a platform whose sub-audit failed outright, so the platform's checks appear in the campaign run record (and classify as lost visibility) instead of vanishing. Kept in one place next to Get-GuerrillaFailedCategoryFinding; must track the categoryMap tables in Invoke-ADAudit / Invoke-EntraAudit / Invoke-GWSAudit. #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet('AD', 'Entra', 'GWS')] [string]$Platform ) switch ($Platform) { 'AD' { @('Invoke-ADDomainForestChecks', 'Invoke-ADTrustChecks', 'Invoke-ADPrivilegedAccountChecks', 'Invoke-ADPasswordPolicyChecks', 'Invoke-ADKerberosChecks', 'Invoke-ADAclDelegationChecks', 'Invoke-ADGroupPolicyChecks', 'Invoke-ADLogonScriptChecks', 'Invoke-ADCertificateServicesChecks', 'Invoke-ADStaleObjectChecks', 'Invoke-ADNetworkChecks', 'Invoke-TierZeroChecks', 'Invoke-ADLoggingChecks', 'Invoke-ADTradecraftChecks', 'Invoke-ADAttackPathChecks') } 'Entra' { @('Invoke-EntraCAChecks', 'Invoke-EntraAuthChecks', 'Invoke-EntraPIMChecks', 'Invoke-EntraAppChecks', 'Invoke-EntraFedChecks', 'Invoke-EntraTenantChecks', 'Invoke-EntraEidscaChecks', 'Invoke-EntraGovernanceChecks', 'Invoke-EntraAIAgentChecks', 'Invoke-AzureIAMChecks', 'Invoke-IntuneChecks', 'Invoke-M365ExchangeChecks', 'Invoke-M365SharePointChecks', 'Invoke-M365TeamsChecks', 'Invoke-M365DefenderChecks', 'Invoke-M365AuditChecks', 'Invoke-M365PowerPlatformChecks') } 'GWS' { @('Invoke-AuthenticationChecks', 'Invoke-EmailSecurityChecks', 'Invoke-DriveSecurityChecks', 'Invoke-OAuthSecurityChecks', 'Invoke-AdminManagementChecks', 'Invoke-CollaborationChecks', 'Invoke-DeviceManagementChecks', 'Invoke-LoggingAlertingChecks', 'Invoke-GwsServiceChecks', 'Invoke-GoogleTradecraftChecks', 'Invoke-K12Checks') } } } |