modules/shared/Policy/AzAdvertizerLookup.ps1
|
# AzAdvertizerLookup.ps1 # Track C scaffold (#431). Stub only. # Deterministic finding-type to policy lookup. No live fetch, no telemetry. # Catalog vendored SHA-pinned (lands in implementation PR after Foundation #435). Set-StrictMode -Version Latest $script:PolicyCatalogCache = @{} $script:FindingMapCache = @{} function Get-PolicyCatalogPath { param([string] $Leaf) return Join-Path (Join-Path $PSScriptRoot 'catalogs') $Leaf } function Import-PolicyCatalog { param( [Parameter(Mandatory)] [string] $Path, [Parameter(Mandatory)] [string] $Name ) if (-not (Test-Path -LiteralPath $Path)) { throw "$Name catalog not found at '$Path'." } $resolved = (Resolve-Path -LiteralPath $Path).ProviderPath if ($script:PolicyCatalogCache.ContainsKey($resolved)) { return $script:PolicyCatalogCache[$resolved] } $raw = Get-Content -LiteralPath $resolved -Raw -Encoding UTF8 $catalog = $raw | ConvertFrom-Json if (-not $catalog.PSObject.Properties['entries']) { throw "$Name catalog at '$resolved' is missing entries[]." } $script:PolicyCatalogCache[$resolved] = $catalog return $catalog } function Invoke-AzAdvertizerLookup { [CmdletBinding()] param( [Parameter(Mandatory)] [string] $PolicyId, [string] $CatalogPath = (Get-PolicyCatalogPath -Leaf 'azadvertizer-catalog.json'), [object] $Catalog ) if ([string]::IsNullOrWhiteSpace($PolicyId)) { return $null } if (-not $Catalog) { $Catalog = Import-PolicyCatalog -Path $CatalogPath -Name 'AzAdvertizer' } return ($Catalog.entries | Where-Object { [string]$_.policyId -ieq [string]$PolicyId } | Select-Object -First 1) } function Get-PolicySuggestionsForFinding { <# .SYNOPSIS Return up to N suggested policies for a given finding. .PARAMETER Finding v2 FindingRow. .PARAMETER MapPath Path to finding-to-policy-map.json. Defaults to module-relative path. .PARAMETER MaxSuggestions Maximum number of suggestions to return. Default 3. .PARAMETER AlzActivation Full | Partial | Fallback. Controls whether ALZ-source entries are returned. .PARAMETER Map Optional preloaded finding-to-policy map object (skips disk read + JSON parse). .PARAMETER AlzCatalog Optional preloaded ALZ policy catalog object (skips disk read + JSON parse). .PARAMETER AzAdvertizerCatalog Optional preloaded AzAdvertizer policy catalog object (skips disk read + JSON parse). .OUTPUTS Array of PSCustomObject { PolicyId, DisplayName, Source, ScopeHint, Url, Pill }. #> [CmdletBinding()] param( [Parameter(Mandatory)] [object] $Finding, [string] $MapPath, [int] $MaxSuggestions = 3, [ValidateSet('Full','Partial','Fallback')] [string] $AlzActivation = 'Fallback', [object] $Map, [object] $AlzCatalog, [object] $AzAdvertizerCatalog ) if ($MaxSuggestions -lt 1) { return @() } if (-not $Map) { $Map = Import-FindingToPolicyMap -MapPath $MapPath } $findingType = '' foreach ($candidateProp in 'FindingType', 'findingType', 'Type', 'Category') { if ($Finding.PSObject.Properties[$candidateProp] -and -not [string]::IsNullOrWhiteSpace([string]$Finding.$candidateProp)) { $findingType = [string]$Finding.$candidateProp break } } if ([string]::IsNullOrWhiteSpace($findingType)) { return @() } $entry = @($Map.entries | Where-Object { [string]$_.findingType -ieq $findingType } | Select-Object -First 1) if (-not $entry) { return @() } $allowAlz = $AlzActivation -in @('Full', 'Partial') $suggestions = @( @($entry.suggestions) | Where-Object { $_ -and ( ([string]$_.source -eq 'AzAdvertizer') -or ($allowAlz -and [string]$_.source -eq 'ALZ') ) } | Sort-Object @{ Expression = { [int]$_.priority }; Ascending = $true }, @{ Expression = { [string]$_.source }; Ascending = $true }, @{ Expression = { [string]$_.displayName }; Ascending = $true } | Select-Object -First $MaxSuggestions ) if (-not $AlzCatalog) { $AlzCatalog = Import-PolicyCatalog -Path (Get-PolicyCatalogPath -Leaf 'alz-policy-catalog.json') -Name 'ALZ' } $rows = [System.Collections.Generic.List[object]]::new() foreach ($s in $suggestions) { $source = [string]$s.source $policyId = [string]$s.policyId $displayName = [string]$s.displayName $scopeHint = [string]$s.scopeHint $url = '' if ($source -eq 'AzAdvertizer') { $catalogHit = if ($AzAdvertizerCatalog) { Invoke-AzAdvertizerLookup -PolicyId $policyId -Catalog $AzAdvertizerCatalog } else { Invoke-AzAdvertizerLookup -PolicyId $policyId } if ($catalogHit) { if (-not [string]::IsNullOrWhiteSpace([string]$catalogHit.displayName)) { $displayName = [string]$catalogHit.displayName } $url = [string]$catalogHit.url } } elseif ($source -eq 'ALZ') { $catalogHit = @($AlzCatalog.entries | Where-Object { [string]$_.policyId -ieq $policyId } | Select-Object -First 1) if ($catalogHit) { if (-not [string]::IsNullOrWhiteSpace([string]$catalogHit.displayName)) { $displayName = [string]$catalogHit.displayName } $url = [string]$catalogHit.url } } if ([string]::IsNullOrWhiteSpace($url) -and $source -eq 'AzAdvertizer') { if ($policyId -match '([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})') { $url = "https://www.azadvertizer.net/azpolicyadvertizer/$($Matches[1].ToLowerInvariant()).html" } } $rows.Add([pscustomobject]@{ PolicyId = $policyId DisplayName = $displayName Source = $source ScopeHint = $scopeHint Url = $url Pill = if ($source -eq 'ALZ') { 'ALZ' } elseif ($source -eq 'AzAdvertizer') { 'AzAdvertizer' } else { 'built-in' } }) | Out-Null } return @($rows) } function Import-FindingToPolicyMap { <# .SYNOPSIS Load and validate the curated finding-to-policy mapping table. #> [CmdletBinding()] param([string] $MapPath) if ([string]::IsNullOrWhiteSpace($MapPath)) { $MapPath = Join-Path $PSScriptRoot 'finding-to-policy-map.json' } if (-not (Test-Path -LiteralPath $MapPath)) { throw "finding-to-policy map not found at '$MapPath'." } $resolved = (Resolve-Path -LiteralPath $MapPath).ProviderPath if ($script:FindingMapCache.ContainsKey($resolved)) { return $script:FindingMapCache[$resolved] } $raw = Get-Content -LiteralPath $resolved -Raw -Encoding UTF8 $map = $raw | ConvertFrom-Json if (-not $map.PSObject.Properties['entries']) { throw "finding-to-policy map '$resolved' is missing entries[]." } foreach ($entry in @($map.entries)) { if (-not $entry.PSObject.Properties['findingType'] -or [string]::IsNullOrWhiteSpace([string]$entry.findingType)) { throw "finding-to-policy map '$resolved' contains an entry without findingType." } if (-not $entry.PSObject.Properties['suggestions']) { throw "finding-to-policy map '$resolved' entry '$($entry.findingType)' is missing suggestions[]." } } $script:FindingMapCache[$resolved] = $map return $map } function Get-CatalogVintage { <# .SYNOPSIS Return catalog SHA + vintage date for both AzAdvertizer and ALZ. #> [CmdletBinding()] param() $map = Import-FindingToPolicyMap $azCatalog = Import-PolicyCatalog -Path (Get-PolicyCatalogPath -Leaf 'azadvertizer-catalog.json') -Name 'AzAdvertizer' $alzCatalog = Import-PolicyCatalog -Path (Get-PolicyCatalogPath -Leaf 'alz-policy-catalog.json') -Name 'ALZ' $alzVintage = Get-Content -LiteralPath (Get-PolicyCatalogPath -Leaf 'alz-vintage.json') -Raw -Encoding UTF8 | ConvertFrom-Json return [pscustomobject]@{ azAdvertizer = [pscustomobject]@{ catalogVintage = [string]$map.catalogVintage.azAdvertizer.date catalogSha = [string]$azCatalog.source.sha } alz = [pscustomobject]@{ catalogVintage = [string]$alzVintage.date catalogSha = [string]$alzCatalog.source.sha } } } if ($MyInvocation.MyCommand.Module) { Export-ModuleMember -Function Invoke-AzAdvertizerLookup, Get-PolicySuggestionsForFinding, Import-FindingToPolicyMap, Get-CatalogVintage } |