modules/normalizers/Normalize-WARA.ps1

#Requires -Version 7.4
<#
.SYNOPSIS
    Normalizer for Well-Architected Reliability Assessment (WARA) findings.
.DESCRIPTION
    Converts raw WARA wrapper output to v3 FindingRow objects.
    Platform=Azure, EntityType=AzureResource.
    WARA only emits non-compliant findings (Compliant is always $false).
#>

[CmdletBinding()]
param ()

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

function Normalize-WARA {
    [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 Get-Text {
        param([object] $Object, [string[]] $Names)
        foreach ($name in $Names) {
            if ($Object.PSObject.Properties[$name]) {
                $value = $Object.$name
                if ($null -ne $value -and -not [string]::IsNullOrWhiteSpace([string]$value)) {
                    return [string]$value
                }
            }
        }
        return ''
    }

    function Normalize-Pillar {
        param([string] $Value)
        if ([string]::IsNullOrWhiteSpace($Value)) { return '' }
        $input = $Value.Trim().ToLowerInvariant()
        if ($input -match 'reliab') { return 'Reliability' }
        if ($input -match 'secur') { return 'Security' }
        if ($input -match 'cost') { return 'Cost' }
        if ($input -match 'perform') { return 'Performance' }
        if ($input -match 'operat') { return 'Operational' }
        return ''
    }

    foreach ($finding in $ToolResult.Findings) {
        $rawId = ''
        if ($finding.PSObject.Properties['ResourceId'] -and $finding.ResourceId) {
            $rawId = [string]$finding.ResourceId
        }

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

        $findingId = if ($finding.PSObject.Properties['Id'] -and $finding.Id) {
            [string]$finding.Id
        } else {
            [guid]::NewGuid().ToString()
        }
        if (-not $canonicalId) {
            $canonicalId = "wara/$findingId"
        }

        $title = Get-Text -Object $finding -Names @('Title')
        if ([string]::IsNullOrWhiteSpace($title)) { $title = 'Unknown' }
        $category = Get-Text -Object $finding -Names @('Category')
        if ([string]::IsNullOrWhiteSpace($category)) { $category = 'Reliability' }

        $rawSev = Get-Text -Object $finding -Names @('Severity')
        if ([string]::IsNullOrWhiteSpace($rawSev)) { $rawSev = 'Medium' }
        $severity = switch -Regex ($rawSev.ToString().ToLowerInvariant()) {
            'critical'         { 'Critical' }
            'high'             { 'High' }
            'medium|moderate'  { 'Medium' }
            'low'              { 'Low' }
            'info'             { 'Info' }
            default            { 'Medium' }
        }

        $detail = Get-Text -Object $finding -Names @('Detail')
        $remediation = Get-Text -Object $finding -Names @('Remediation')
        $learnMore = Get-Text -Object $finding -Names @('LearnMoreUrl')
        $deepLink = Get-Text -Object $finding -Names @('DeepLinkUrl', 'LearnMoreUrl')
        $pillar = Normalize-Pillar (Get-Text -Object $finding -Names @('Pillar', 'Category'))
        $impact = Get-Text -Object $finding -Names @('Impact')
        $effort = Get-Text -Object $finding -Names @('Effort')
        $recommendationId = Get-Text -Object $finding -Names @('RecommendationId', 'Id')
        $controls = @()
        if (-not [string]::IsNullOrWhiteSpace($recommendationId)) { $controls = @($recommendationId) }

        $frameworks = @()
        if (-not [string]::IsNullOrWhiteSpace($pillar) -or $controls.Count -gt 0) {
            $frameworks = @(@{
                    Name     = 'WAF'
                    Pillars  = if ($pillar) { @($pillar) } else { @() }
                    Controls = $controls
                })
        }

        $baselineTags = @()
        if ($finding.PSObject.Properties['BaselineTags'] -and $finding.BaselineTags) {
            $baselineTags = @($finding.BaselineTags | ForEach-Object { [string]$_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
        }
        if ($baselineTags.Count -eq 0 -and $finding.PSObject.Properties['ServiceCategory'] -and $finding.ServiceCategory) {
            $baselineTags = @("service-category:$([string]$finding.ServiceCategory)")
        }

        $entityRefs = @()
        if ($finding.PSObject.Properties['EntityRefs'] -and $finding.EntityRefs) {
            foreach ($entityRef in @($finding.EntityRefs)) {
                $value = [string]$entityRef
                if ([string]::IsNullOrWhiteSpace($value)) { continue }
                if ($value -match '^/subscriptions/') {
                    try {
                        $entityRefs += (ConvertTo-CanonicalEntityId -RawId $value -EntityType 'AzureResource').CanonicalId
                    } catch {
                        $entityRefs += $value.ToLowerInvariant()
                    }
                } else {
                    $entityRefs += $value
                }
            }
            $entityRefs = @($entityRefs | Select-Object -Unique)
        }

        $remediationSnippets = @()
        if ($finding.PSObject.Properties['RemediationSteps'] -and $finding.RemediationSteps) {
            foreach ($step in @($finding.RemediationSteps)) {
                $text = [string]$step
                if ([string]::IsNullOrWhiteSpace($text)) { continue }
                $remediationSnippets += @{
                    language = 'text'
                    code     = $text.Trim()
                }
            }
        }

        $toolVersion = Get-Text -Object $finding -Names @('ToolVersion')
        if ([string]::IsNullOrWhiteSpace($toolVersion) -and $ToolResult.PSObject.Properties['ToolVersion']) {
            $toolVersion = [string]$ToolResult.ToolVersion
        }

        $row = New-FindingRow -Id $findingId `
            -Source 'wara' -EntityId $canonicalId -EntityType 'AzureResource' `
            -Title $title -Compliant $false -ProvenanceRunId $runId `
            -Platform 'Azure' -Category $category -Severity $severity `
            -Detail $detail -Remediation $remediation `
            -LearnMoreUrl $learnMore -ResourceId ($rawId ?? '') `
            -SubscriptionId $subId -ResourceGroup $rg `
            -Pillar $pillar -Impact $impact -Effort $effort `
            -DeepLinkUrl $deepLink -RemediationSnippets $remediationSnippets `
            -Frameworks $frameworks -Controls $controls `
            -BaselineTags $baselineTags -EntityRefs $entityRefs `
            -ToolVersion $toolVersion
        # Skip null rows (validation failed)
        if ($null -ne $row) {
            $normalized.Add($row)
        }
    }

    return @($normalized)
}