modules/normalizers/Normalize-IdentityCorrelation.ps1
|
#Requires -Version 7.4 <# .SYNOPSIS Normalizer for identity correlator findings. .DESCRIPTION Converts raw identity correlator findings into v2 FindingRow objects via New-FindingRow, with canonical entity IDs for User/ServicePrincipal. #> [CmdletBinding()] param () . "$PSScriptRoot\..\shared\Schema.ps1" . "$PSScriptRoot\..\shared\Canonicalize.ps1" function Get-IdentityCorrelationSeverity { param([string] $Severity) $raw = if ($Severity) { $Severity } else { 'Info' } switch -Regex ($raw.ToLowerInvariant()) { 'critical' { return 'Critical' } '^high$' { return 'High' } 'medium|moderate' { return 'Medium' } '^low$' { return 'Low' } 'info|informational' { return 'Info' } default { return 'Info' } } } function Get-IdentityCorrelationImpact { param([string] $Severity) switch ($Severity) { 'Critical' { return 'High' } 'High' { return 'High' } 'Medium' { return 'Medium' } default { return 'Low' } } } function Get-IdentityCorrelationEffort { param([string] $Severity) switch ($Severity) { 'Critical' { return 'High' } 'High' { return 'Medium' } 'Medium' { return 'Medium' } default { return 'Low' } } } function Get-IdentityCorrelationEntityRefs { param( [PSCustomObject] $Finding, [string] $CanonicalEntityId ) $refs = [System.Collections.Generic.List[string]]::new() if ($CanonicalEntityId) { $refs.Add($CanonicalEntityId) | Out-Null } if ($Finding.PSObject.Properties['EntityRefs'] -and $Finding.EntityRefs) { foreach ($ref in @($Finding.EntityRefs)) { if ($ref) { $refs.Add(([string]$ref)) | Out-Null } } } foreach ($prop in @('AppId', 'ObjectId')) { if (-not $Finding.PSObject.Properties[$prop] -or -not $Finding.$prop) { continue } $id = ([string]$Finding.$prop).ToLowerInvariant() if ($id -notmatch '^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$') { continue } $prefix = if ($prop -eq 'AppId') { 'appId' } else { 'objectId' } $refs.Add("$prefix`:$id") | Out-Null } $detail = if ($Finding.PSObject.Properties['Detail']) { [string]$Finding.Detail } else { '' } foreach ($match in [regex]::Matches($detail, '(?i)\b(appid|objectid)[:=\s]+([0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})')) { $refs.Add("$($match.Groups[1].Value.ToLowerInvariant()):$($match.Groups[2].Value.ToLowerInvariant())") | Out-Null } $seen = @{} $ordered = [System.Collections.Generic.List[string]]::new() foreach ($ref in $refs) { $candidate = [string]$ref if ([string]::IsNullOrWhiteSpace($candidate)) { continue } $key = $candidate.ToLowerInvariant() if ($seen.ContainsKey($key)) { continue } $seen[$key] = $true $ordered.Add($candidate) | Out-Null } return @($ordered) } function Get-IdentityCorrelationDeepLinkUrl { param([string[]] $EntityRefs) $appRef = @($EntityRefs | Where-Object { $_ -match '^appId:' }) | Select-Object -First 1 if ($appRef) { $appId = $appRef -replace '^appId:', '' return "https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/$appId" } $objRef = @($EntityRefs | Where-Object { $_ -match '^objectId:' }) | Select-Object -First 1 if ($objRef) { $objId = $objRef -replace '^objectId:', '' return "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/$objId" } return '' } function Get-IdentityCorrelationRemediationSnippets { param([string] $Remediation) if ([string]::IsNullOrWhiteSpace($Remediation)) { return @( @{ language = 'text' code = 'Review correlated identity blast radius and enforce least privilege for linked credentials.' } ) } return @( @{ language = 'text' code = $Remediation.Trim() } ) } function Normalize-IdentityCorrelation { [CmdletBinding()] param ( [Parameter(Mandatory)] [PSCustomObject] $ToolResult ) if ($ToolResult.Status -ne 'Success' -or -not $ToolResult.Findings) { return @() } $normalized = [System.Collections.Generic.List[PSCustomObject]]::new() $runId = if ($ToolResult.PSObject.Properties['RunId'] -and $ToolResult.RunId) { $ToolResult.RunId } else { [guid]::NewGuid().ToString() } foreach ($finding in $ToolResult.Findings) { if (-not $finding) { continue } # Determine EntityType from the finding's PrincipalType (or fall back) $entityType = 'ServicePrincipal' if ($finding.PSObject.Properties['EntityType'] -and $finding.EntityType) { $entityType = [string]$finding.EntityType } elseif ($finding.PSObject.Properties['PrincipalType'] -and $finding.PrincipalType) { $pt = ([string]$finding.PrincipalType).ToLowerInvariant() if ($pt -match 'user') { $entityType = 'User' } } $rawEntityId = if ($finding.PSObject.Properties['EntityId'] -and $finding.EntityId) { [string]$finding.EntityId } else { '' } $canonicalId = $rawEntityId if ($rawEntityId) { try { $canonicalId = (ConvertTo-CanonicalEntityId -RawId $rawEntityId -EntityType $entityType).CanonicalId } catch { $canonicalId = $rawEntityId.ToLowerInvariant() } } $findingId = if ($finding.PSObject.Properties['Id'] -and $finding.Id) { [string]$finding.Id } else { [guid]::NewGuid().ToString() } $title = if ($finding.PSObject.Properties['Title'] -and $finding.Title) { [string]$finding.Title } else { 'Identity correlation finding' } $compliant = if ($finding.PSObject.Properties['Compliant'] -and $null -ne $finding.Compliant) { [bool]$finding.Compliant } else { $false } $rawSeverity = if ($finding.PSObject.Properties['Severity']) { [string]$finding.Severity } else { 'Info' } $severity = Get-IdentityCorrelationSeverity -Severity $rawSeverity $category = if ($finding.PSObject.Properties['Category'] -and $finding.Category) { [string]$finding.Category } else { 'Identity' } $detail = if ($finding.PSObject.Properties['Detail'] -and $finding.Detail) { [string]$finding.Detail } else { '' } $remediation = if ($finding.PSObject.Properties['Remediation'] -and $finding.Remediation) { [string]$finding.Remediation } else { '' } $learnMore = if ($finding.PSObject.Properties['LearnMoreUrl'] -and $finding.LearnMoreUrl) { [string]$finding.LearnMoreUrl } else { '' } $resourceId = if ($finding.PSObject.Properties['ResourceId'] -and $finding.ResourceId) { [string]$finding.ResourceId } else { '' } $frameworks = if ($finding.PSObject.Properties['Frameworks'] -and $finding.Frameworks) { @($finding.Frameworks) } else { @( @{ Name = 'NIST 800-53'; Controls = @('AC-2', 'AC-6', 'IA-5') }, @{ Name = 'CIS Controls v8'; Controls = @('5.3', '6.3', '6.7') } ) } $pillar = if ($finding.PSObject.Properties['Pillar'] -and $finding.Pillar) { [string]$finding.Pillar } else { 'Security' } $impact = if ($finding.PSObject.Properties['Impact'] -and $finding.Impact) { [string]$finding.Impact } else { Get-IdentityCorrelationImpact -Severity $severity } $effort = if ($finding.PSObject.Properties['Effort'] -and $finding.Effort) { [string]$finding.Effort } else { Get-IdentityCorrelationEffort -Severity $severity } $entityRefs = Get-IdentityCorrelationEntityRefs -Finding $finding -CanonicalEntityId $canonicalId $deepLinkUrl = if ($finding.PSObject.Properties['DeepLinkUrl'] -and $finding.DeepLinkUrl) { [string]$finding.DeepLinkUrl } else { Get-IdentityCorrelationDeepLinkUrl -EntityRefs $entityRefs } $remediationSnippets = if ($finding.PSObject.Properties['RemediationSnippets'] -and $finding.RemediationSnippets) { @($finding.RemediationSnippets) } else { Get-IdentityCorrelationRemediationSnippets -Remediation $remediation } $evidenceUris = [System.Collections.Generic.List[string]]::new() if ($finding.PSObject.Properties['EvidenceUris'] -and $finding.EvidenceUris) { foreach ($uri in @($finding.EvidenceUris)) { if ($uri) { $evidenceUris.Add(([string]$uri)) | Out-Null } } } if ($learnMore -and $learnMore -match '^https://') { $evidenceUris.Add($learnMore) | Out-Null } if ($deepLinkUrl -and $deepLinkUrl -match '^https://') { $evidenceUris.Add($deepLinkUrl) | Out-Null } $baselineTags = if ($finding.PSObject.Properties['BaselineTags'] -and $finding.BaselineTags) { @($finding.BaselineTags | ForEach-Object { [string]$_ }) } else { @('identity-correlator', 'attack-path-correlation', ("category/{0}" -f $category.ToLowerInvariant().Replace(' ', '-'))) } $mitreTactics = if ($finding.PSObject.Properties['MitreTactics'] -and $finding.MitreTactics) { @($finding.MitreTactics | ForEach-Object { [string]$_ }) } else { @('TA0001', 'TA0006', 'TA0008') } $mitreTechniques = if ($finding.PSObject.Properties['MitreTechniques'] -and $finding.MitreTechniques) { @($finding.MitreTechniques | ForEach-Object { [string]$_ }) } else { @('T1078', 'T1550', 'T1021') } $toolVersion = if ($finding.PSObject.Properties['ToolVersion'] -and $finding.ToolVersion) { [string]$finding.ToolVersion } elseif ($ToolResult.PSObject.Properties['ToolVersion'] -and $ToolResult.ToolVersion) { [string]$ToolResult.ToolVersion } else { 'identity-correlator' } $row = New-FindingRow -Id $findingId ` -Source 'identity-correlator' -EntityId $canonicalId -EntityType $entityType ` -Title $title -Compliant $compliant -ProvenanceRunId $runId ` -Category $category -Severity $severity ` -Detail $detail -Remediation $remediation ` -LearnMoreUrl $learnMore -ResourceId $resourceId ` -Frameworks $frameworks -Pillar $pillar -Impact $impact -Effort $effort ` -DeepLinkUrl $deepLinkUrl -RemediationSnippets $remediationSnippets ` -EvidenceUris @($evidenceUris) -BaselineTags $baselineTags ` -MitreTactics $mitreTactics -MitreTechniques $mitreTechniques ` -EntityRefs $entityRefs -ToolVersion $toolVersion if ($null -ne $row) { $normalized.Add($row) } } return @($normalized) } |