modules/normalizers/Normalize-Falco.ps1

#Requires -Version 7.4
<#
.SYNOPSIS
    Normalizer for Falco wrapper output.
.DESCRIPTION
    Converts v1 Falco findings to v2 FindingRows on AKS AzureResource entities.
    Falco priority mapping:
      - Critical -> Critical
      - Error -> High
      - Warning -> Medium
      - Notice -> Low
#>

[CmdletBinding()]
param ()

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

function Normalize-Falco {
    [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()

    function New-FalcoRuleId {
        param([string]$RuleName)
        if ([string]::IsNullOrWhiteSpace($RuleName)) { return 'falco:runtime-alert' }
        $slug = ($RuleName.ToLowerInvariant() -replace '[^a-z0-9]+', '-').Trim('-')
        if ([string]::IsNullOrWhiteSpace($slug)) { $slug = 'runtime-alert' }
        return "falco:$slug"
    }

    function Get-FalcoMitre {
        param(
            [string]$RuleName,
            [string]$Priority,
            [string]$Detail
        )
        $text = "$RuleName $Detail".ToLowerInvariant()
        if ($text -match 'shell|exec') {
            return @{ Tactics = @('Execution'); Techniques = @('T1059') }
        }
        if ($text -match 'capabilit|privilege|root|escalat') {
            return @{ Tactics = @('PrivilegeEscalation'); Techniques = @('T1068') }
        }
        if ($text -match 'write|modify|executable|binary|filesystem') {
            return @{ Tactics = @('DefenseEvasion'); Techniques = @('T1070') }
        }
        $p = ($Priority ?? '').ToLowerInvariant()
        if ($p -eq 'critical' -or $p -eq 'error') {
            return @{ Tactics = @('Execution'); Techniques = @('T1059') }
        }
        return @{ Tactics = @(); Techniques = @() }
    }

    function Get-FalcoFrameworks {
        param(
            [object]$RawFrameworks,
            [string]$RuleId
        )
        if ($RawFrameworks) { return @($RawFrameworks) }
        return @(
            @{
                Name      = 'CIS Kubernetes Benchmark'
                ControlId = $RuleId
                Controls  = @($RuleId)
            }
        )
    }

    function Get-FalcoImpact {
        param([string]$Severity)
        switch ($Severity) {
            'Critical' { return 'High' }
            'High'     { return 'High' }
            'Medium'   { return 'Medium' }
            'Low'      { return 'Low' }
            default    { return 'Low' }
        }
    }

    function Get-FalcoEffort {
        param([string]$Severity)
        switch ($Severity) {
            'Critical' { return 'Medium' }
            'High'     { return 'Medium' }
            'Medium'   { return 'Low' }
            'Low'      { return 'Low' }
            default    { return 'Low' }
        }
    }

    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 '/subscriptions/([^/]+)') { $subId = $Matches[1] }
        if ($rawId -match '/resourceGroups/([^/]+)') { $rg    = $Matches[1] }

        try   { $canonicalId = (ConvertTo-CanonicalEntityId -RawId $rawId -EntityType 'AzureResource').CanonicalId }
        catch { $canonicalId = $rawId.ToLowerInvariant() }

        $priority = if ($f.PSObject.Properties['Priority'] -and $f.Priority) { [string]$f.Priority } else { '' }
        $sev = switch -Regex ($priority) {
            '^(?i)critical$' { 'Critical' }
            '^(?i)error$'    { 'High' }
            '^(?i)warning$'  { 'Medium' }
            '^(?i)notice$'   { 'Low' }
            default {
                if ($f.PSObject.Properties['Severity'] -and $f.Severity) { [string]$f.Severity } else { 'Info' }
            }
        }

        $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 { '' }
        $ruleName = if ($f.PSObject.Properties['RuleName'] -and $f.RuleName) { [string]$f.RuleName } else { '' }
        $ruleId = if ($f.PSObject.Properties['RuleId'] -and $f.RuleId) { [string]$f.RuleId } else { New-FalcoRuleId -RuleName $ruleName }
        $mitre = if (
            $f.PSObject.Properties['MitreTactics'] -and
            $f.PSObject.Properties['MitreTechniques'] -and
            $f.MitreTactics -and
            $f.MitreTechniques
        ) {
            @{
                Tactics    = @([string[]]$f.MitreTactics)
                Techniques = @([string[]]$f.MitreTechniques)
            }
        } else {
            Get-FalcoMitre -RuleName $ruleName -Priority $priority -Detail ([string]$f.Detail)
        }
        $frameworks = Get-FalcoFrameworks -RawFrameworks $(if ($f.PSObject.Properties['Frameworks']) { $f.Frameworks } else { $null }) -RuleId $ruleId
        $pillar = if ($f.PSObject.Properties['Pillar'] -and $f.Pillar) { [string]$f.Pillar } else { 'Security' }
        $impact = if ($f.PSObject.Properties['Impact'] -and $f.Impact) { [string]$f.Impact } else { Get-FalcoImpact -Severity $sev }
        $effort = if ($f.PSObject.Properties['Effort'] -and $f.Effort) { [string]$f.Effort } else { Get-FalcoEffort -Severity $sev }
        $learnMore = if ($f.PSObject.Properties['LearnMoreUrl']) { [string]$f.LearnMoreUrl } else { '' }
        $deepLinkUrl = if ($f.PSObject.Properties['DeepLinkUrl'] -and $f.DeepLinkUrl) {
            [string]$f.DeepLinkUrl
        } elseif (-not [string]::IsNullOrWhiteSpace($learnMore)) {
            $learnMore
        } else {
            ''
        }
        $remediationSnippets = if ($f.PSObject.Properties['RemediationSnippets'] -and $f.RemediationSnippets) {
            @($f.RemediationSnippets | ForEach-Object {
                    if ($_ -is [hashtable]) { return $_ }
                    @{
                        language = [string]$_.language
                        code     = [string]$_.code
                    }
                })
        } elseif (-not [string]::IsNullOrWhiteSpace($remediation)) {
            @(@{
                    language = 'text'
                    code     = $remediation
                })
        } else {
            @()
        }
        $evidenceUris = if ($f.PSObject.Properties['EvidenceUris'] -and $f.EvidenceUris) {
            @($f.EvidenceUris | ForEach-Object { [string]$_ })
        } else {
            @()
        }
        if (@($evidenceUris).Count -eq 0 -and -not [string]::IsNullOrWhiteSpace($learnMore)) {
            $evidenceUris = @($learnMore)
        }
        if (@($evidenceUris).Count -eq 0) {
            $evidenceUris = @($rawId)
        }
        $baselineTags = if ($f.PSObject.Properties['BaselineTags'] -and $f.BaselineTags) {
            @($f.BaselineTags | ForEach-Object { [string]$_ })
        } else {
            @('falco', 'aks-runtime-threat', $ruleId)
        }
        $entityRefs = if ($f.PSObject.Properties['EntityRefs'] -and $f.EntityRefs) {
            @($f.EntityRefs | ForEach-Object { [string]$_ })
        } else {
            @([string]$canonicalId)
        }
        $toolVersion = if ($f.PSObject.Properties['ToolVersion'] -and $f.ToolVersion) { [string]$f.ToolVersion } else { 'falco-alert-pipeline' }
        $scoreDelta = if ($f.PSObject.Properties['ScoreDelta']) {
            [Nullable[double]]$f.ScoreDelta
        } else {
            $null
        }

        $row = New-FindingRow -Id $findingId `
            -Source 'falco' -EntityId $canonicalId -EntityType 'AzureResource' `
            -Title ([string]$f.Title) -RuleId $ruleId -Compliant $false -ProvenanceRunId $runId `
            -Platform 'Azure' -Category 'KubernetesRuntimeThreatDetection' -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 -RemediationSnippets @($remediationSnippets) `
            -EvidenceUris @($evidenceUris) -BaselineTags @($baselineTags) `
            -ScoreDelta $scoreDelta -MitreTactics @($mitre.Tactics) `
            -MitreTechniques @($mitre.Techniques) -EntityRefs @($entityRefs) `
            -ToolVersion $toolVersion

        foreach ($extra in 'RuleName', 'Pod', 'Process', 'Priority') {
            if ($f.PSObject.Properties[$extra] -and $f.$extra) {
                $row | Add-Member -NotePropertyName $extra -NotePropertyValue ([string]$f.$extra) -Force
            }
        }

        # Skip null rows (validation failed)

        if ($null -ne $row) {

            $normalized.Add($row)

        }
    }

    return @($normalized)
}