modules/normalizers/Normalize-DefenderForCloud.ps1
|
#Requires -Version 7.4 <# .SYNOPSIS Normalizer for Microsoft Defender for Cloud wrapper output. .DESCRIPTION Converts v1 defender-for-cloud wrapper output to v2 FindingRows. - Secure Score roll-up -> EntityType=Subscription, Severity=Info, Compliant=true, with ScoreCurrent / ScoreMax / ScorePercent attached via Add-Member. - Each non-healthy assessment -> EntityType=AzureResource, Severity mapped from Defender's metadata.severity (High/Medium/Low -> High/Medium/Low), Compliant=false. The normalizer emits an entity-scoped finding on the canonical ARM ID so EntityStore folds it next to existing azqr/PSRule findings on the same resource. #> [CmdletBinding()] param () . "$PSScriptRoot\..\shared\Schema.ps1" . "$PSScriptRoot\..\shared\Canonicalize.ps1" function Normalize-DefenderForCloud { [CmdletBinding()] param ( [Parameter(Mandatory)] [PSCustomObject] $ToolResult ) if ($ToolResult.Status -ne 'Success' -or -not $ToolResult.Findings) { return @() } $runId = [guid]::NewGuid().ToString() $normalized = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($f in $ToolResult.Findings) { $rawId = if ($f.PSObject.Properties['ResourceId'] -and $f.ResourceId) { [string]$f.ResourceId } else { '' } if (-not $rawId) { continue } $subId = '' $rg = '' if ($rawId -match '^[0-9a-fA-F-]{36}$') { $subId = $rawId } elseif ($rawId -match '/subscriptions/([^/]+)') { $subId = $Matches[1] } if ($rawId -match '/resourceGroups/([^/]+)') { $rg = $Matches[1] } $isSubscription = ($rawId -match '^[0-9a-fA-F-]{36}$') -or ` ($rawId -match '^/subscriptions/[^/]+/?$') -or ` ($f.PSObject.Properties['ResourceType'] -and $f.ResourceType -eq 'Microsoft.Resources/subscriptions') if ($isSubscription) { $entityType = 'Subscription' $canonicalId = $subId } else { $entityType = 'AzureResource' try { $canonicalId = (ConvertTo-CanonicalEntityId -RawId $rawId -EntityType 'AzureResource').CanonicalId } catch { $canonicalId = $rawId.ToLowerInvariant() } } # Map Defender severity casing -> schema casing (Critical/High/Medium/Low/Info). $sevRaw = if ($f.PSObject.Properties['Severity'] -and $f.Severity) { [string]$f.Severity } else { 'Medium' } $sev = switch -Regex ($sevRaw) { '^(?i)critical$' { 'Critical' } '^(?i)high$' { 'High' } '^(?i)medium$' { 'Medium' } '^(?i)low$' { 'Low' } '^(?i)info.*' { 'Info' } default { 'Medium' } } $compliant = $false if ($f.PSObject.Properties['Compliant']) { $compliant = [bool]$f.Compliant } $findingId = if ($f.PSObject.Properties['Id'] -and $f.Id) { [string]$f.Id } else { [guid]::NewGuid().ToString() } $remediation = if ($f.PSObject.Properties['Remediation']) { [string]$f.Remediation } else { '' } $ruleId = '' if ($f.PSObject.Properties['RuleId'] -and $f.RuleId) { $ruleId = [string]$f.RuleId } elseif ($f.PSObject.Properties['AssessmentId'] -and $f.AssessmentId) { $ruleId = [string]$f.AssessmentId } elseif ($f.PSObject.Properties['AlertId'] -and $f.AlertId) { $ruleId = [string]$f.AlertId } $frameworks = @() if ($f.PSObject.Properties['Frameworks'] -and $f.Frameworks) { $frameworks = @($f.Frameworks) } $pillar = if ($f.PSObject.Properties['Pillar']) { [string]$f.Pillar } else { '' } $impact = if ($f.PSObject.Properties['Impact']) { [string]$f.Impact } else { '' } $effort = if ($f.PSObject.Properties['Effort']) { [string]$f.Effort } else { '' } $deepLinkUrl = if ($f.PSObject.Properties['DeepLinkUrl']) { [string]$f.DeepLinkUrl } else { '' } $toolVersion = if ($f.PSObject.Properties['ToolVersion']) { [string]$f.ToolVersion } else { '' } $scoreDelta = $null if ($f.PSObject.Properties['ScoreDelta'] -and $null -ne $f.ScoreDelta) { try { $scoreDelta = [double]$f.ScoreDelta } catch {} } $evidenceUris = @() if ($f.PSObject.Properties['EvidenceUris'] -and $f.EvidenceUris) { $evidenceUris = @($f.EvidenceUris | ForEach-Object { [string]$_ } | Where-Object { $_ }) } $mitreTactics = @() if ($f.PSObject.Properties['MitreTactics'] -and $f.MitreTactics) { $mitreTactics = @($f.MitreTactics | ForEach-Object { [string]$_ } | Where-Object { $_ }) } $mitreTechniques = @() if ($f.PSObject.Properties['MitreTechniques'] -and $f.MitreTechniques) { $mitreTechniques = @($f.MitreTechniques | ForEach-Object { [string]$_ } | Where-Object { $_ }) } $category = if ($f.PSObject.Properties['Category'] -and $f.Category) { [string]$f.Category } else { 'SecurityPosture' } $row = New-FindingRow -Id $findingId ` -Source 'defender-for-cloud' -EntityId $canonicalId -EntityType $entityType ` -Title ([string]$f.Title) -RuleId $ruleId -Compliant $compliant -ProvenanceRunId $runId ` -Platform 'Azure' -Category $category -Severity $sev ` -Detail ([string]$f.Detail) ` -Remediation $remediation ` -LearnMoreUrl ([string]$f.LearnMoreUrl) -ResourceId $rawId ` -SubscriptionId $subId -ResourceGroup $rg ` -Frameworks $frameworks ` -Pillar $pillar ` -Impact $impact ` -Effort $effort ` -DeepLinkUrl $deepLinkUrl ` -EvidenceUris $evidenceUris ` -ScoreDelta $scoreDelta ` -MitreTactics $mitreTactics ` -MitreTechniques $mitreTechniques ` -ToolVersion $toolVersion if ($null -ne $row) { $normalized.Add($row) } } return @($normalized) } |