Private/Get-FylgyrConfigSuppression.ps1

function Get-FylgyrConfigSuppression {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [switch]$IgnoreConfig
    )

    $rules = [System.Collections.Generic.List[PSCustomObject]]::new()
    $diagnostics = [System.Collections.Generic.List[PSCustomObject]]::new()
    $defaultTarget = ''

    if ($IgnoreConfig) {
        return [PSCustomObject]@{
            Rules = $rules.ToArray()
            Diagnostics = $diagnostics.ToArray()
        }
    }

    $configPath = Join-Path -Path (Get-Location) -ChildPath '.fylgyr.yml'
    if (-not (Test-Path -Path $configPath -PathType Leaf)) {
        return [PSCustomObject]@{
            Rules = $rules.ToArray()
            Diagnostics = $diagnostics.ToArray()
        }
    }

    $configText = Get-Content -Path $configPath -Raw
    if ([string]::IsNullOrWhiteSpace($configText)) {
        $diagnostics.Add([PSCustomObject]@{
            Status = 'Warning'
            Severity = 'Low'
            Detail = "Suppression config file '$configPath' is empty."
            Remediation = 'Add a suppressions array or remove the empty file.'
        })

        return [PSCustomObject]@{
            Rules = $rules.ToArray()
            Diagnostics = $diagnostics.ToArray()
        }
    }

    try {
        Import-Module -Name powershell-yaml -ErrorAction Stop
    }
    catch {
        $diagnostics.Add([PSCustomObject]@{
            Status = 'Warning'
            Severity = 'Low'
            Detail = "Unable to load powershell-yaml while reading '$configPath': $($_.Exception.Message)"
            Remediation = "Install the 'powershell-yaml' module and rerun."
        })

        return [PSCustomObject]@{
            Rules = $rules.ToArray()
            Diagnostics = $diagnostics.ToArray()
        }
    }

    try {
        $parsed = ConvertFrom-Yaml -Yaml $configText -ErrorAction Stop
    }
    catch {
        $diagnostics.Add([PSCustomObject]@{
            Status = 'Warning'
            Severity = 'Low'
            Detail = "Failed to parse suppression config '$configPath': $($_.Exception.Message)"
            Remediation = 'Fix YAML syntax. Expected top-level key: suppressions.'
        })

        return [PSCustomObject]@{
            Rules = $rules.ToArray()
            Diagnostics = $diagnostics.ToArray()
        }
    }

    # Best-effort default target scoping from local git remote.
    # This keeps repository-local suppressions from bleeding across repos in
    # org-wide scans executed from a single workspace.
    try {
        $originUrl = (& git config --get remote.origin.url 2>$null)
        if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($originUrl)) {
            $originUrl = $originUrl.Trim()
            if ($originUrl -match 'github\.com[:/](?<owner>[A-Za-z0-9._-]+)/(?<repo>[A-Za-z0-9._-]+?)(?:\.git)?$') {
                $defaultTarget = "$($Matches.owner)/$($Matches.repo)"
            }
        }
    }
    catch {
        Write-Debug "Unable to derive default suppression target from git origin URL: $($_.Exception.Message)"
    }

    $suppressionEntries = @()
    if ($parsed -is [System.Collections.IDictionary] -and $parsed.Contains('suppressions')) {
        $suppressionEntries = @($parsed['suppressions'])
    }
    elseif ($parsed -and $parsed.PSObject.Properties['suppressions']) {
        $suppressionEntries = @($parsed.suppressions)
    }
    else {
        $diagnostics.Add([PSCustomObject]@{
            Status = 'Warning'
            Severity = 'Low'
            Detail = "Suppression config '$configPath' is missing top-level 'suppressions'."
            Remediation = "Define suppressions under 'suppressions:' as a YAML array."
        })

        return [PSCustomObject]@{
            Rules = $rules.ToArray()
            Diagnostics = $diagnostics.ToArray()
        }
    }

    for ($index = 0; $index -lt $suppressionEntries.Count; $index++) {
        $entry = $suppressionEntries[$index]
        $position = $index + 1

        if (-not $entry) {
            $diagnostics.Add([PSCustomObject]@{
                Status = 'Warning'
                Severity = 'Low'
                Detail = "Suppression entry #$position is null."
                Remediation = 'Provide check, resource, and reason fields.'
            })
            continue
        }

        $checkValue = ''
        $resourceValue = ''
        $reasonValue = ''
        $expiresValue = ''
        $targetValue = ''

        if ($entry -is [System.Collections.IDictionary]) {
            foreach ($key in $entry.Keys) {
                $keyName = [string]$key
                if ($keyName -ieq 'check') {
                    $checkValue = [string]$entry[$key]
                    continue
                }

                if ($keyName -ieq 'resource') {
                    $resourceValue = [string]$entry[$key]
                    continue
                }

                if ($keyName -ieq 'reason') {
                    $reasonValue = [string]$entry[$key]
                    continue
                }

                if ($keyName -ieq 'expires') {
                    $expiresValue = [string]$entry[$key]
                    continue
                }
                if ($keyName -ieq 'target') {
                    $targetValue = [string]$entry[$key]
                    continue
                }
            }
        }
        else {
            $checkValue = if ($entry.PSObject.Properties['check']) { [string]$entry.check } else { '' }
            $resourceValue = if ($entry.PSObject.Properties['resource']) { [string]$entry.resource } else { '' }
            $reasonValue = if ($entry.PSObject.Properties['reason']) { [string]$entry.reason } else { '' }
            $expiresValue = if ($entry.PSObject.Properties['expires']) { [string]$entry.expires } else { '' }
            $targetValue = if ($entry.PSObject.Properties['target']) { [string]$entry.target } else { '' }
        }

        if ([string]::IsNullOrWhiteSpace($checkValue) -or [string]::IsNullOrWhiteSpace($resourceValue) -or [string]::IsNullOrWhiteSpace($reasonValue)) {
            $diagnostics.Add([PSCustomObject]@{
                Status = 'Warning'
                Severity = 'Low'
                Detail = "Suppression entry #$position is invalid: check/resource/reason are required."
                Remediation = 'Set check, resource, and reason for each suppression entry.'
            })
            continue
        }

        $expiresUtc = $null
        if (-not [string]::IsNullOrWhiteSpace($expiresValue)) {
            [datetime]$parsedExpiry = [datetime]::MinValue
            $styles = [System.Globalization.DateTimeStyles]::AssumeUniversal -bor [System.Globalization.DateTimeStyles]::AdjustToUniversal
            if (-not [datetime]::TryParse($expiresValue, [System.Globalization.CultureInfo]::InvariantCulture, $styles, [ref]$parsedExpiry)) {
                $diagnostics.Add([PSCustomObject]@{
                    Status = 'Warning'
                    Severity = 'Low'
                    Detail = "Suppression entry #$position has invalid expires value '$expiresValue'."
                    Remediation = 'Use an ISO date value, for example 2026-07-01.'
                })
                continue
            }

            $expiresUtc = $parsedExpiry.ToUniversalTime()
        }

        $rules.Add([PSCustomObject]@{
            Check = $checkValue
            Resource = $resourceValue
            Reason = $reasonValue
            ExpiresUtc = $expiresUtc
            Target = if ([string]::IsNullOrWhiteSpace($targetValue)) { $defaultTarget } else { $targetValue }
        })
    }

    return [PSCustomObject]@{
        Rules = $rules.ToArray()
        Diagnostics = $diagnostics.ToArray()
    }
}