Public/Test-SecretScanning.ps1

function Test-SecretScanning {
    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param(
        [Parameter(Mandatory)]
        [ValidatePattern('^[a-zA-Z0-9._-]+$')]
        [string]$Owner,

        [Parameter(Mandatory)]
        [ValidatePattern('^[a-zA-Z0-9._-]+$')]
        [string]$Repo,

        [Parameter(Mandatory)]
        [string]$Token
    )

    $target = "$Owner/$Repo"
    $results = [System.Collections.Generic.List[PSCustomObject]]::new()
    $resource = $target
    $attackMap = @('committed-credentials-exposure', 'axios-npm-token-leak', 'uber-credential-leak')

    try {
        $alerts = Invoke-GitHubApi -Endpoint "repos/$Owner/$Repo/secret-scanning/alerts?state=open&per_page=100" -Token $Token -AllPages
    }
    catch {
        $msg = $_.Exception.Message

        if ($msg -match '404' -or ($msg -match '403' -and $msg -match '(?i)disabled')) {
            $results.Add((Format-FylgyrResult `
                -CheckName 'SecretScanning' `
                -Status 'Fail' `
                -Severity 'Medium' `
                -Resource $resource `
                -Detail 'Secret Scanning is not enabled on this repository.' `
                -Remediation 'Enable Secret Scanning in Settings → Security → Code security and analysis.' `
                -AttackMapping $attackMap `
                -Target $target))
            return $results.ToArray()
        }

        if ($msg -match '403') {
            $featureEnabled = $null

            try {
                $repoInfo = Invoke-GitHubApi -Endpoint "repos/$Owner/$Repo" -Token $Token
                if ($repoInfo -and
                    $repoInfo.PSObject.Properties['security_and_analysis'] -and
                    $repoInfo.security_and_analysis -and
                    $repoInfo.security_and_analysis.PSObject.Properties['secret_scanning'] -and
                    $repoInfo.security_and_analysis.secret_scanning -and
                    $repoInfo.security_and_analysis.secret_scanning.PSObject.Properties['status']) {
                    $featureEnabled = $repoInfo.security_and_analysis.secret_scanning.status -eq 'enabled'
                }
            }
            catch {
                Write-Debug "Could not resolve secret_scanning feature state: $($_.Exception.Message)"
            }

            if ($featureEnabled -eq $false) {
                $results.Add((Format-FylgyrResult `
                    -CheckName 'SecretScanning' `
                    -Status 'Fail' `
                    -Severity 'Medium' `
                    -Resource $resource `
                    -Detail 'Secret Scanning is not enabled on this repository.' `
                    -Remediation 'Enable Secret Scanning in Settings → Security → Code security and analysis.' `
                    -AttackMapping $attackMap `
                    -Target $target))
                return $results.ToArray()
            }

            $results.Add((Format-FylgyrResult `
                -CheckName 'SecretScanning' `
                -Status 'Info' `
                -Severity 'Info' `
                -Resource $resource `
                -Detail 'Secret Scanning appears enabled, but open-alert telemetry is unavailable with the current token scope.' `
                -Remediation 'Enable secret_scanning_alerts:read on a fine-grained token (or security_events on a classic token) to surface open alert count, highest severity, and oldest alert age.' `
                -Target $target))
            return $results.ToArray()
        }

        $results.Add((Format-FylgyrResult `
            -CheckName 'SecretScanning' `
            -Status 'Error' `
            -Severity 'Medium' `
            -Resource $resource `
            -Detail "Unexpected error reading Secret Scanning alerts: $($_.Exception.Message)" `
            -Remediation 'Re-run with a valid token and verify network access to api.github.com.' `
            -Target $target))
        return $results.ToArray()
    }

    if ($alerts.Count -eq 0) {
        $results.Add((Format-FylgyrResult `
            -CheckName 'SecretScanning' `
            -Status 'Pass' `
            -Severity 'Info' `
            -Resource $resource `
            -Detail 'Secret Scanning is enabled and no open alerts found.' `
            -Remediation 'No action needed.' `
            -AttackMapping $attackMap `
            -Target $target))
        return $results.ToArray()
    }

    $severityRank = @{
        low = 1
        medium = 2
        high = 3
        critical = 4
    }

    $highestSeverity = 'unknown'
    $highestRank = 0
    $oldestAlert = $null

    foreach ($alert in @($alerts)) {
        if ($alert -and $alert.PSObject.Properties['severity'] -and $alert.severity) {
            $sev = ([string]$alert.severity).ToLowerInvariant()
            if ($severityRank.ContainsKey($sev) -and $severityRank[$sev] -gt $highestRank) {
                $highestRank = $severityRank[$sev]
                $highestSeverity = $sev
            }
        }

        if ($alert -and $alert.PSObject.Properties['created_at'] -and $alert.created_at) {
            try {
                $parsedDate = [datetime]$alert.created_at
                if ($null -eq $oldestAlert -or $parsedDate -lt $oldestAlert) {
                    $oldestAlert = $parsedDate
                }
            }
            catch {
                Write-Debug "Could not parse secret scanning alert created_at value '$($alert.created_at)': $($_.Exception.Message)"
            }
        }
    }

    $oldestAge = 'unknown'
    if ($oldestAlert) {
        $oldestAge = [math]::Floor(([datetime]::UtcNow - $oldestAlert.ToUniversalTime()).TotalDays)
    }

    $status = if ($highestRank -ge 3) { 'Fail' } else { 'Warning' }
    $severity = if ($highestRank -ge 3) { 'High' } else { 'Medium' }
    $detail = "$($alerts.Count) open Secret Scanning alert(s) found. Highest severity: $highestSeverity. Oldest open alert age: $oldestAge day(s)."
    $remediation = if ($highestRank -ge 3) {
        'Prioritize High/Critical secret alerts immediately. Rotate affected credentials, remove leaked secrets from active code paths, and close alerts after remediation. Use TruffleHog or GitLeaks for deeper historical secret hunting.'
    }
    else {
        'Triage and resolve open secret alerts to reduce credential exposure risk. Rotate exposed credentials and use TruffleHog or GitLeaks for deeper historical secret hunting.'
    }

    $results.Add((Format-FylgyrResult `
        -CheckName 'SecretScanning' `
        -Status $status `
        -Severity $severity `
        -Resource $resource `
        -Detail $detail `
        -Remediation $remediation `
        -AttackMapping $attackMap `
        -Target $target))

    $results.ToArray()
}