modules/normalizers/Normalize-Prowler.ps1

#Requires -Version 7.4
[CmdletBinding()]
param ()

. "$PSScriptRoot\..\shared\Schema.ps1"
. "$PSScriptRoot\..\shared\Canonicalize.ps1"

function Get-PropertyValue {
    param ([object]$Obj, [string]$Name, [object]$Default = '')
    if ($null -eq $Obj) { return $Default }
    $p = $Obj.PSObject.Properties[$Name]
    if ($null -eq $p -or $null -eq $p.Value) { return $Default }
    return $p.Value
}

function Convert-ProwlerFrameworks {
    param (
        [object[]] $Frameworks,
        [string] $RuleId
    )

    $converted = [System.Collections.Generic.List[hashtable]]::new()
    foreach ($fw in @($Frameworks)) {
        $name = [string](Get-PropertyValue -Obj $fw -Name 'Name' (Get-PropertyValue -Obj $fw -Name 'kind' ''))
        if ([string]::IsNullOrWhiteSpace($name)) { continue }
        $controls = @(Get-PropertyValue -Obj $fw -Name 'Controls' @())
        if (@($controls).Count -eq 0) {
            $singleControl = [string](Get-PropertyValue -Obj $fw -Name 'controlId' $RuleId)
            if (-not [string]::IsNullOrWhiteSpace($singleControl)) {
                $controls = @($singleControl)
            }
        }
        if (@($controls).Count -eq 0) { $controls = @($RuleId) }

        foreach ($control in @($controls)) {
            $controlId = [string]$control
            if ([string]::IsNullOrWhiteSpace($controlId)) { continue }
            $converted.Add(@{
                    kind      = $name
                    controlId = $controlId
                }) | Out-Null
        }
    }
    return @($converted)
}

function Convert-ProwlerRemediationSnippets {
    param ([object[]] $Snippets)
    $converted = [System.Collections.Generic.List[hashtable]]::new()
    foreach ($snippet in @($Snippets)) {
        $code = [string](Get-PropertyValue -Obj $snippet -Name 'Code' '')
        if ([string]::IsNullOrWhiteSpace($code)) { continue }
        $type = [string](Get-PropertyValue -Obj $snippet -Name 'Type' '')
        if ([string]::IsNullOrWhiteSpace($type)) { $type = 'General' }
        $converted.Add(@{
                Type = $type
                Code = $code
            }) | Out-Null
    }
    return @($converted)
}

function Normalize-Prowler {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [PSCustomObject] $ToolResult
    )

    if ($ToolResult.Status -ne 'Success' -or -not $ToolResult.Findings) {
        return @()
    }

    $runId = [guid]::NewGuid().ToString()
    $toolVersion = [string](Get-PropertyValue -Obj $ToolResult -Name 'ToolVersion' '')
    $normalized = [System.Collections.Generic.List[PSCustomObject]]::new()

    foreach ($finding in $ToolResult.Findings) {
        $rawId = [string](Get-PropertyValue -Obj $finding -Name 'ResourceId' '')
        $resourceArn = [string](Get-PropertyValue -Obj $finding -Name 'ResourceArn' '')
        $ruleId = [string](Get-PropertyValue -Obj $finding -Name 'RuleId' (Get-PropertyValue -Obj $finding -Name 'Id' ([guid]::NewGuid().ToString())))

        $subId = ''
        $rg = ''
        $canonicalId = ''
        if ($rawId -and $rawId -match '^/subscriptions/') {
            try {
                $canonicalId = (ConvertTo-CanonicalEntityId -RawId $rawId -EntityType 'AzureResource').CanonicalId
            } catch {
                $canonicalId = $rawId.ToLowerInvariant()
            }
            if ($rawId -match '(?i)/subscriptions/([^/]+)') { $subId = $Matches[1].ToLowerInvariant() }
            if ($rawId -match '(?i)/resourcegroups/([^/]+)') { $rg = $Matches[1] }
        }

        if (-not $canonicalId) {
            $fallbackSub = if ($subId -match '^[0-9a-fA-F-]{36}$') { $subId } else { '00000000-0000-0000-0000-000000000000' }
            $fallbackArmId = "/subscriptions/$fallbackSub/providers/microsoft.security/prowlerfindings/$ruleId"
            $canonicalId = (ConvertTo-CanonicalEntityId -RawId $fallbackArmId -EntityType 'AzureResource').CanonicalId
        }

        $severityRaw = [string](Get-PropertyValue -Obj $finding -Name 'Severity' 'Medium')
        $severity = switch -Regex ($severityRaw.ToLowerInvariant()) {
            'critical' { 'Critical' }
            '^high$' { 'High' }
            '^medium$' { 'Medium' }
            '^low$' { 'Low' }
            '^info' { 'Info' }
            default { 'Medium' }
        }

        $compliant = [bool](Get-PropertyValue -Obj $finding -Name 'Compliant' $false)
        $frameworks = Convert-ProwlerFrameworks -Frameworks @(Get-PropertyValue -Obj $finding -Name 'Frameworks' @()) -RuleId $ruleId
        $toolVersionResolved = [string](Get-PropertyValue -Obj $finding -Name 'ToolVersion' $toolVersion)

        $evidenceUris = [System.Collections.Generic.List[string]]::new()
        foreach ($uri in @(Get-PropertyValue -Obj $finding -Name 'EvidenceUris' @())) {
            if (-not [string]::IsNullOrWhiteSpace([string]$uri)) { $evidenceUris.Add([string]$uri) | Out-Null }
        }
        if (-not [string]::IsNullOrWhiteSpace($resourceArn)) {
            $evidenceUris.Add($resourceArn) | Out-Null
        }

        $row = New-FindingRow -Id ([string](Get-PropertyValue -Obj $finding -Name 'Id' $ruleId)) `
            -Source 'prowler' -EntityId $canonicalId -EntityType 'AzureResource' `
            -Title ([string](Get-PropertyValue -Obj $finding -Name 'Title' $ruleId)) `
            -RuleId $ruleId -Compliant $compliant -ProvenanceRunId $runId `
            -Platform 'Azure' -Category ([string](Get-PropertyValue -Obj $finding -Name 'Category' 'SecurityPosture')) `
            -Severity $severity -Detail ([string](Get-PropertyValue -Obj $finding -Name 'Detail' '')) `
            -Remediation ([string](Get-PropertyValue -Obj $finding -Name 'Remediation' '')) `
            -LearnMoreUrl ([string](Get-PropertyValue -Obj $finding -Name 'LearnMoreUrl' '')) `
            -ResourceId $rawId -SubscriptionId $subId -ResourceGroup $rg `
            -Pillar ([string](Get-PropertyValue -Obj $finding -Name 'Pillar' 'Security')) `
            -Frameworks $frameworks `
            -DeepLinkUrl ([string](Get-PropertyValue -Obj $finding -Name 'DeepLinkUrl' '')) `
            -RemediationSnippets @(Convert-ProwlerRemediationSnippets -Snippets @(Get-PropertyValue -Obj $finding -Name 'RemediationSnippets' @())) `
            -EvidenceUris @($evidenceUris) `
            -BaselineTags @(Get-PropertyValue -Obj $finding -Name 'BaselineTags' @()) `
            -MitreTactics @(Get-PropertyValue -Obj $finding -Name 'MitreTactics' @()) `
            -MitreTechniques @(Get-PropertyValue -Obj $finding -Name 'MitreTechniques' @()) `
            -ToolVersion $toolVersionResolved

        if ($null -ne $row) {
            $normalized.Add($row)
        }
    }

    return @($normalized)
}