modules/normalizers/Normalize-AzureLoadTesting.ps1

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

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

function Get-LoadTestingSeverity {
    param([string] $Severity)
    switch -Regex ($Severity) {
        '^(?i)critical$' { 'Critical' }
        '^(?i)high$'     { 'High' }
        '^(?i)medium$'   { 'Medium' }
        '^(?i)low$'      { 'Low' }
        '^(?i)info.*'    { 'Info' }
        default          { 'Medium' }
    }
}

function Get-LoadTestingTitle {
    param([pscustomobject] $Finding)

    if ($Finding.PSObject.Properties['Title'] -and $Finding.Title) {
        return [string]$Finding.Title
    }

    $testName = if ($Finding.PSObject.Properties['TestName']) { [string]$Finding.TestName } else { 'unknown-test' }
    $runId = if ($Finding.PSObject.Properties['TestRunId']) { [string]$Finding.TestRunId } else { 'unknown-run' }
    $metric = if ($Finding.PSObject.Properties['MetricDisplayName']) { [string]$Finding.MetricDisplayName } elseif ($Finding.PSObject.Properties['MetricName']) { [string]$Finding.MetricName } else { 'metric' }
    $delta = if ($Finding.PSObject.Properties['RegressionPercent']) { [math]::Round([double]$Finding.RegressionPercent, 2) } else { 0 }
    $cause = if ($Finding.PSObject.Properties['FailureCause']) { [string]$Finding.FailureCause } else { 'No failure cause provided by the API.' }

    if ($Finding.Id -like '*/failed') {
        return "Load test '$testName' run $runId failed: $cause"
    }
    if ($Finding.Id -like '*/regression/*') {
        return "Load test '$testName' regressed by $delta% in $metric"
    }
    if ($Finding.Id -like '*/no-runs') {
        return "Load test '$testName' has no recent runs"
    }
    if ($Finding.Id -like '*/healthy') {
        return "Load test '$testName' run $runId is healthy"
    }
    return "Load test '$testName' finding"
}

function Convert-ToStringArray {
    param([object] $Value)

    if ($null -eq $Value) { return @() }

    $items = [System.Collections.Generic.List[string]]::new()
    if ($Value -is [string]) {
        if (-not [string]::IsNullOrWhiteSpace($Value)) {
            $items.Add($Value.Trim()) | Out-Null
        }
        return @($items.ToArray())
    }

    foreach ($item in @($Value)) {
        if ($null -eq $item) { continue }
        $text = [string]$item
        if ([string]::IsNullOrWhiteSpace($text)) { continue }
        $items.Add($text.Trim()) | Out-Null
    }

    return @($items.ToArray())
}

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

    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()
        }

        $title = Get-LoadTestingTitle -Finding $f
        $detail = if ($f.PSObject.Properties['Detail']) { [string]$f.Detail } else { '' }
        $severityRaw = ''
        if ($f.PSObject.Properties['Severity']) { $severityRaw = [string]$f.Severity }
        $severity = Get-LoadTestingSeverity -Severity $severityRaw
        $compliant = $false
        if ($f.PSObject.Properties['Compliant']) { $compliant = [bool]$f.Compliant }
        $findingId = if ($f.PSObject.Properties['Id'] -and $f.Id) { [string]$f.Id } else { [guid]::NewGuid().ToString() }
        $category = if ($f.PSObject.Properties['Category'] -and $f.Category) { [string]$f.Category } else { 'Performance' }
        $remediation = if ($f.PSObject.Properties['Remediation']) { [string]$f.Remediation } else { '' }
        $learnMoreUrl = if ($f.PSObject.Properties['LearnMoreUrl']) { [string]$f.LearnMoreUrl } else { '' }
        $source = if ($f.PSObject.Properties['Source'] -and $f.Source) { [string]$f.Source } else { 'loadtesting' }
        $pillar = if ($f.PSObject.Properties['Pillar']) { [string]$f.Pillar } else { 'Performance Efficiency' }
        $impact = if ($f.PSObject.Properties['Impact']) { [string]$f.Impact } else { '' }
        $effort = if ($f.PSObject.Properties['Effort']) { [string]$f.Effort } else { '' }
        $deepLinkUrl = if ($f.PSObject.Properties['DeepLinkUrl'] -and $f.DeepLinkUrl) { [string]$f.DeepLinkUrl } else { $learnMoreUrl }
        $evidenceUris = if ($f.PSObject.Properties['EvidenceUris']) { @(Convert-ToStringArray -Value $f.EvidenceUris) } else { @() }
        $baselineTags = if ($f.PSObject.Properties['BaselineTags']) { @(Convert-ToStringArray -Value $f.BaselineTags) } else { @() }
        $entityRefs = if ($f.PSObject.Properties['EntityRefs']) { @(Convert-ToStringArray -Value $f.EntityRefs) } else { @() }
        $toolVersion = if ($f.PSObject.Properties['ToolVersion']) { [string]$f.ToolVersion } elseif ($ToolResult.PSObject.Properties['ToolVersion']) { [string]$ToolResult.ToolVersion } else { '' }
        $scoreDelta = $null
        if ($f.PSObject.Properties['ScoreDelta'] -and $null -ne $f.ScoreDelta) {
            try { $scoreDelta = [double]$f.ScoreDelta } catch { $scoreDelta = $null }
        } elseif ($f.PSObject.Properties['RegressionPercent'] -and $null -ne $f.RegressionPercent) {
            try { $scoreDelta = [double]$f.RegressionPercent } catch { $scoreDelta = $null }
        }

        $row = New-FindingRow -Id $findingId `
            -Source $source -EntityId $canonicalId -EntityType 'AzureResource' `
            -Title $title -Compliant $compliant -ProvenanceRunId $runId `
            -Platform 'Azure' -Category $category -Severity $severity `
            -Detail $detail -Remediation $remediation `
            -LearnMoreUrl $learnMoreUrl -ResourceId $rawId `
            -SubscriptionId $subId -ResourceGroup $rg `
            -Pillar $pillar -Impact $impact -Effort $effort `
            -DeepLinkUrl $deepLinkUrl -EvidenceUris $evidenceUris `
            -BaselineTags $baselineTags -ScoreDelta $scoreDelta `
            -EntityRefs $entityRefs -ToolVersion $toolVersion

        if ($null -eq $row) { continue }

        foreach ($extra in @(
                'LoadTestResourceName',
                'TestName',
                'TestRunId',
                'RunStatus',
                'FailureCause',
                'PassFailCriteriaFailed',
                'MetricName',
                'MetricDisplayName',
                'BaselineValue',
                'CurrentValue',
                'RegressionPercent',
                'ThresholdPercent',
                'DaysBack'
            )) {
            if ($f.PSObject.Properties[$extra] -and $null -ne $f.$extra) {
                $row | Add-Member -NotePropertyName $extra -NotePropertyValue $f.$extra -Force
            }
        }

        $normalized.Add($row)
    }

    return @($normalized)
}