Public/Test-EgressControl.ps1

function Test-EgressControl {
    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param(
        [Parameter(Mandatory)]
        [PSCustomObject[]]$WorkflowFiles
    )

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

    # Known egress control actions
    $egressActions = @(
        @{ Pattern = 'step-security/harden-runner'; Name = 'StepSecurity Harden-Runner' }
        @{ Pattern = 'code-cargo/cargowall-action'; Name = 'CargoWall Action' }
        @{ Pattern = 'bullfrogsec/bullfrog'; Name = 'BullFrog' }
    )

    # Patterns indicating network calls in run steps
    $networkCallPatterns = @(
        '(?m)^\s*-?\s*run:.*\b(curl|wget)\b'
        '(?m)^\s*(curl|wget)\s'
        '(?m)\bInvoke-WebRequest\b'
        '(?m)\bInvoke-RestMethod\b'
        '(?m)\biwr\b\s'
    )

    foreach ($wf in $WorkflowFiles) {
        # Strip YAML comment lines to avoid false positives
        $strippedLines = ($wf.Content -split "`n") | Where-Object { $_ -notmatch '^\s*#' }
        $stripped = $strippedLines -join "`n"

        $foundControls = [System.Collections.Generic.List[string]]::new()
        $hasAuditOnly = $false
        $hasBullfrog = $false

        foreach ($action in $egressActions) {
            if ($stripped -match [regex]::Escape($action.Pattern)) {
                $foundControls.Add($action.Name)

                if ($action.Pattern -eq 'bullfrogsec/bullfrog') {
                    $hasBullfrog = $true
                }
            }
        }

        # Check egress-policy setting
        if ($stripped -match '(?i)egress-policy:\s*audit') {
            $hasAuditOnly = $true
        }
        $hasBlockPolicy = $stripped -match '(?i)egress-policy:\s*block'

        # Check for network calls without egress controls
        $hasNetworkCalls = $false
        foreach ($pattern in $networkCallPatterns) {
            if ($stripped -match $pattern) {
                $hasNetworkCalls = $true
                break
            }
        }

        if ($foundControls.Count -gt 0) {
            $controlList = $foundControls -join ', '

            if ($hasBlockPolicy) {
                $detail = "Workflow '$($wf.Name)' has egress controls ($controlList) with block enforcement."
                $status = 'Pass'
                $severity = 'Info'

                if ($hasBullfrog) {
                    $detail += ' Note: BullFrog has a known DNS-over-TCP bypass disclosed Feb 2026 (see https://devansh.bearblog.dev/bullfrog-dns-pipelining/). Consider layering with additional controls.'
                }

                $results.Add((Format-FylgyrResult `
                    -CheckName 'EgressControl' `
                    -Status $status `
                    -Severity $severity `
                    -Resource $wf.Path `
                    -Detail $detail `
                    -Remediation 'No action needed. Continue monitoring egress control effectiveness.' `
                    -Target $null))
            }
            elseif ($hasAuditOnly) {
                $detail = "Workflow '$($wf.Name)' has egress controls ($controlList) in audit-only mode. This provides visibility into network calls but does not block unauthorized egress."

                if ($hasBullfrog) {
                    $detail += ' Note: BullFrog has a known DNS-over-TCP bypass disclosed Feb 2026.'
                }

                $results.Add((Format-FylgyrResult `
                    -CheckName 'EgressControl' `
                    -Status 'Info' `
                    -Severity 'Low' `
                    -Resource $wf.Path `
                    -Detail $detail `
                    -Remediation "Switch egress-policy from 'audit' to 'block' to enforce network restrictions. Audit mode is a good first step but does not prevent exfiltration." `
                    -AttackMapping @('tj-actions-shai-hulud', 'codecov-bash-uploader') `
                    -Target $null))
            }
            else {
                $results.Add((Format-FylgyrResult `
                    -CheckName 'EgressControl' `
                    -Status 'Pass' `
                    -Severity 'Info' `
                    -Resource $wf.Path `
                    -Detail "Workflow '$($wf.Name)' has egress controls ($controlList)." `
                    -Remediation 'No action needed.' `
                    -Target $null))
            }
        }
        else {
            $severity = 'Medium'
            $detail = "Workflow '$($wf.Name)' has no egress controls. Compromised actions or injected code can freely exfiltrate secrets over the network, as seen in the tj-actions/changed-files and Trivy supply chain attacks."

            if ($hasNetworkCalls) {
                $detail += ' This workflow contains network calls (curl, wget, Invoke-WebRequest, or Invoke-RestMethod) that could be exploited without egress filtering.'
            }

            $results.Add((Format-FylgyrResult `
                -CheckName 'EgressControl' `
                -Status 'Warning' `
                -Severity $severity `
                -Resource $wf.Path `
                -Detail $detail `
                -Remediation "Add step-security/harden-runner with egress-policy: block as the first step in each job. Free for public repos, trusted by 11,000+ projects including Microsoft and Google. GitHub's 2026 roadmap includes a native Layer 7 egress firewall (public preview in 3-6 months). Azure VNet integration is available now for Enterprise Cloud customers." `
                -AttackMapping @('tj-actions-shai-hulud', 'trivy-supply-chain-2026', 'codecov-bash-uploader') `
                -Target $null))
        }
    }

    $results.ToArray()
}