Exporters/Export-Sarif.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    SARIF 2.1.0 exporter — for GitHub Code Scanning integration.
.DESCRIPTION
    Generates SARIF format output compatible with GitHub's code scanning
    alerts feature. Each failed BSI control becomes a SARIF result with
    severity mapping, remediation guidance, and source location.
.LINK
    https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning
#>

function Export-Sarif {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][array]$Results,
        [string]$OutputPath = '',
        [string]$Version = '2.0.0'
    )

    # Build rules from check manifest
    $rules = @()
    $manifestPath = Join-Path $PSScriptRoot '..' 'Checks' '_CheckManifest.json'
    if (Test-Path -LiteralPath $manifestPath) {
        $manifest = Get-Content -Raw -Path $manifestPath | ConvertFrom-Json
        foreach ($check in $manifest.checks) {
            $level = if ($check.severity -in @('Critical', 'High')) { 'error' } else { 'warning' }
            $rules += [PSCustomObject]@{
                id          = $check.id
                name        = $check.function
                shortDescription = [PSCustomObject]@{ text = $check.description }
                defaultConfiguration = [PSCustomObject]@{ level = $level }
                properties  = [PSCustomObject]@{
                    category       = $check.category
                    severity       = $check.severity
                    baseline       = $check.baseline
                    bsiReference   = $check.bsiReference
                }
            }
        }
    } else {
        Write-Warning "[Export-Sarif] Check manifest not found at $manifestPath — SARIF output will not include rule definitions"
    }
    $sarifResults = @()
    foreach ($r in $Results) {
        $level = switch ($r.Status) {
            ([BsiCheckStatus]::Pass)  { 'none' }
            ([BsiCheckStatus]::Skip)  { 'none' }
            default {
                if ($r.Severity -in @([BsiSeverity]::Critical, [BsiSeverity]::High)) { 'error' } else { 'warning' }
            }
        }

        $msgText = "$($r.ControlId): $($r.Details)"
        if ($r.Remediation) { $msgText += " | Remediation: $($r.Remediation)" }

        $resultObj = [PSCustomObject]@{
            ruleId      = $r.ControlId
            level       = $level
            message     = [PSCustomObject]@{ text = $msgText }
            properties  = [PSCustomObject]@{
                status     = $r.Status.ToString()
                severity   = $r.Severity.ToString()
                baseline   = $r.Baseline.ToString()
                category   = $r.Category
                evidence   = $r.Evidence
            }
        }

        if ($r.SourceFile) {
            $resultObj | Add-Member -NotePropertyName 'locations' -NotePropertyValue @(
                [PSCustomObject]@{
                    physicalLocation = [PSCustomObject]@{
                        artifactLocation = [PSCustomObject]@{
                            uri = $r.SourceFile
                        }
                        region = [PSCustomObject]@{
                            startLine = [Math]::Max($r.LineNumber, 1)
                        }
                    }
                }
            )
        }

        $sarifResults += $resultObj
    }

    # Build SARIF document
    $sarif = [PSCustomObject]@{
        '$schema' = 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json'
        version   = '2.1.0'
        runs      = @(
            [PSCustomObject]@{
                tool = [PSCustomObject]@{
                    driver = [PSCustomObject]@{
                        name           = 'BSI.AzCompliance'
                        version        = $Version
                        semanticVersion = $Version
                        informationUri = 'https://github.com/chicagoist/BSI-AzCompliance'
                        rules          = $rules
                    }
                }
                results = $sarifResults
            }
        )
    }

    $json = $sarif | ConvertTo-Json -Depth 10

    if ($OutputPath) {
        $dir = Split-Path -Parent $OutputPath
        if ($dir -and -not (Test-Path -LiteralPath $dir)) {
            New-Item -ItemType Directory -Path $dir -Force | Out-Null
        }
        $json | Set-Content -Path $OutputPath -Encoding UTF8
        Write-Verbose "[Export-Sarif] Written to $OutputPath"
    }

    return $json
}