modules/normalizers/Normalize-Gitleaks.ps1

#Requires -Version 7.4
<#
.SYNOPSIS
    Normalizer for gitleaks findings.
.DESCRIPTION
    Converts raw gitleaks wrapper output to v3 FindingRow objects.
    Platform=GitHub, EntityType=Repository.
#>

[CmdletBinding()]
param ()

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

function Get-PropertyValue {
    param ([object]$Obj, [string]$Name, [object]$Default = $null)
    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-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 }
    } else {
        foreach ($item in @($Value)) {
            if ($null -eq $item) { continue }
            $text = [string]$item
            if (-not [string]::IsNullOrWhiteSpace($text)) { $items.Add($text.Trim()) | Out-Null }
        }
    }
    return @($items)
}

function Convert-ToHashtableArray {
    param ([object]$Value)
    $items = [System.Collections.Generic.List[hashtable]]::new()
    foreach ($entry in @($Value)) {
        if ($null -eq $entry) { continue }
        if ($entry -is [System.Collections.IDictionary]) {
            $map = @{}
            foreach ($key in $entry.Keys) { $map[[string]$key] = $entry[$key] }
            $items.Add($map) | Out-Null
            continue
        }
        if ($entry.PSObject) {
            $map = @{}
            foreach ($prop in @($entry.PSObject.Properties)) { $map[$prop.Name] = $prop.Value }
            if ($map.Count -gt 0) { $items.Add($map) | Out-Null }
        }
    }
    return @($items)
}

function Resolve-RepositoryEntityId {
    param (
        [object]$ToolResult
    )

    $candidate = [string](Get-PropertyValue -Obj $ToolResult -Name 'RepositoryEntityId' -Default '')
    if (-not [string]::IsNullOrWhiteSpace($candidate)) {
        return $candidate.ToLowerInvariant()
    }

    $rawRepo = @(
        [string](Get-PropertyValue -Obj $ToolResult -Name 'RepositoryId' -Default ''),
        [string](Get-PropertyValue -Obj $ToolResult -Name 'RepositoryUrl' -Default ''),
        [string](Get-PropertyValue -Obj $ToolResult -Name 'Repository' -Default ''),
        [string](Get-PropertyValue -Obj $ToolResult -Name 'RemoteUrl' -Default '')
    ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1

    if ([string]::IsNullOrWhiteSpace($rawRepo)) { return 'github.com/local/local' }
    $normalized = $rawRepo.Trim().ToLowerInvariant() -replace '\.git$', '' -replace '^https?://', '' -replace '^git@', '' -replace '^([^/:]+):', '$1/' -replace '^www\.', ''
    if ($normalized -match '^[^/]+/[^/]+/[^/]+$') { return $normalized }
    return 'github.com/local/local'
}

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

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

    $runId = [guid]::NewGuid().ToString()
    $canonicalRepoId = Resolve-RepositoryEntityId -ToolResult $ToolResult

    $normalized = [System.Collections.Generic.List[PSCustomObject]]::new()

    foreach ($finding in $ToolResult.Findings) {
        $rawId = [string](Get-PropertyValue -Obj $finding -Name 'ResourceId' -Default '')

        $canonicalPath = ''
        if ($rawId) {
            $canonicalPath = $rawId.Trim().ToLowerInvariant() -replace '\\', '/'
            $canonicalPath = $canonicalPath -replace '^\.\/', ''
        }
        if (-not $canonicalPath) {
            $canonicalPath = "unknown/$([guid]::NewGuid().ToString())"
        }

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

        $title = [string](Get-PropertyValue -Obj $finding -Name 'Title' -Default 'Secret detected')
        $category = [string](Get-PropertyValue -Obj $finding -Name 'Category' -Default 'Secret Detection')

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

        $compliant = [bool](Get-PropertyValue -Obj $finding -Name 'Compliant' -Default $false)
        $detail = [string](Get-PropertyValue -Obj $finding -Name 'Detail' -Default '')
        $remediation = [string](Get-PropertyValue -Obj $finding -Name 'Remediation' -Default '')
        $learnMore = [string](Get-PropertyValue -Obj $finding -Name 'LearnMoreUrl' -Default '')
        $ruleId = [string](Get-PropertyValue -Obj $finding -Name 'RuleId' -Default '')
        $pillar = [string](Get-PropertyValue -Obj $finding -Name 'Pillar' -Default 'Security')
        $impact = [string](Get-PropertyValue -Obj $finding -Name 'Impact' -Default '')
        $effort = [string](Get-PropertyValue -Obj $finding -Name 'Effort' -Default '')
        $deepLinkUrl = [string](Get-PropertyValue -Obj $finding -Name 'DeepLinkUrl' -Default '')
        $frameworks = @(Convert-ToHashtableArray (Get-PropertyValue -Obj $finding -Name 'Frameworks' -Default @()))
        $remediationSnippets = @(Convert-ToHashtableArray (Get-PropertyValue -Obj $finding -Name 'RemediationSnippets' -Default @()))
        $evidenceUris = @(Convert-ToStringArray (Get-PropertyValue -Obj $finding -Name 'EvidenceUris' -Default @()))
        $baselineTags = @(Convert-ToStringArray (Get-PropertyValue -Obj $finding -Name 'BaselineTags' -Default @()))
        $entityRefs = @(Convert-ToStringArray (Get-PropertyValue -Obj $finding -Name 'EntityRefs' -Default @()))
        $toolVersion = [string](Get-PropertyValue -Obj $finding -Name 'ToolVersion' -Default (Get-PropertyValue -Obj $ToolResult -Name 'ToolVersion' -Default ''))

        $row = New-FindingRow -Id $findingId `
            -Source 'gitleaks' -EntityId $canonicalRepoId -EntityType 'Repository' `
            -Title $title -Compliant ([bool]$compliant) -ProvenanceRunId $runId `
            -Platform 'GitHub' -Category $category -Severity $severity `
            -Detail $detail -Remediation $remediation `
            -LearnMoreUrl $learnMore -ResourceId $canonicalPath `
            -RuleId $ruleId -Pillar $pillar -Impact $impact -Effort $effort `
            -DeepLinkUrl $deepLinkUrl -Frameworks $frameworks `
            -RemediationSnippets $remediationSnippets -EvidenceUris $evidenceUris `
            -BaselineTags $baselineTags -EntityRefs $entityRefs -ToolVersion $toolVersion
        # Skip null rows (validation failed)
        if ($null -ne $row) {
            $normalized.Add($row)
        }
    }

    return @($normalized)
}