Public/Test-CacheIntegrity.ps1

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

    $results = [System.Collections.Generic.List[PSCustomObject]]::new()
    $unsafeKeyPatterns = @(
        'github\.event\.pull_request\.head\.ref'
        'github\.head_ref'
        'github\.event\.pull_request\.head\.label'
        'github\.event\.workflow_run\.head_branch'
    )

    foreach ($wf in $WorkflowFiles) {
        $lines = @(($wf.Content -split "`n") | Where-Object { $_ -notmatch '^\s*#' })
        $content = $lines -join "`n"
        $hasPullRequestTrigger = $content -match '(?im)(^|\s)pull_request(\s|:|$)'

        $unsafeKeyHits = [System.Collections.Generic.List[string]]::new()

        for ($i = 0; $i -lt $lines.Count; $i++) {
            $line = $lines[$i]
            if ($line -notmatch '(?i)^\s*-\s*uses\s*:\s*actions/(cache|setup-[a-z0-9._-]+)@') {
                continue
            }

            $j = $i + 1
            while ($j -lt $lines.Count) {
                $next = $lines[$j]
                if ($next -match '(?i)^\s*-\s*(uses|run)\s*:') {
                    break
                }

                if ($next -match '(?i)^\s*(key|cache-key)\s*:') {
                    foreach ($unsafePattern in $unsafeKeyPatterns) {
                        if ($next -match $unsafePattern) {
                            $unsafeKeyHits.Add($next.Trim())
                            break
                        }
                    }
                }

                $j++
            }

            $i = $j - 1
        }

        if ($unsafeKeyHits.Count -gt 0) {
            $severity = if ($hasPullRequestTrigger) { 'High' } else { 'Medium' }
            $status = if ($hasPullRequestTrigger) { 'Fail' } else { 'Warning' }

            $results.Add((Format-FylgyrResult `
                -CheckName 'CacheIntegrity' `
                -Status $status `
                -Severity $severity `
                -Resource $wf.Path `
                -Detail "Workflow '$($wf.Name)' uses cache keys derived from potentially attacker-controlled refs. Detected key lines: $((@($unsafeKeyHits | Select-Object -Unique)) -join ' | '). This can enable cache poisoning across branches/runs." `
                -Remediation 'Build cache keys from immutable inputs (lockfiles, dependency hashes, github.sha) rather than head_ref or PR branch refs. Scope cache restore keys to trusted branches only.' `
                -AttackMapping @('cache-poisoning-pr-branch')))
            continue
        }

        $results.Add((Format-FylgyrResult `
            -CheckName 'CacheIntegrity' `
            -Status 'Pass' `
            -Severity 'Info' `
            -Resource $wf.Path `
            -Detail "Workflow '$($wf.Name)' has no detected unsafe cache key patterns." `
            -Remediation 'No action needed.'))
    }

    return $results.ToArray()
}