Public/Test-ActionPinning.ps1

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

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

    foreach ($wf in $WorkflowFiles) {
        $lines = $wf.Content -split "`n"
        $unpinnedFound = $false

        for ($i = 0; $i -lt $lines.Count; $i++) {
            $line = $lines[$i]

            # Skip YAML comment lines
            if ($line -match '^\s*#') { continue }

            if ($line -notmatch '^\s*-?\s*uses:\s*(.+)$') { continue }

            $target = $Matches[1].Trim().Trim("'").Trim('"')

            # Strip trailing YAML inline comments (e.g., "actions/checkout@sha # v4.2.2")
            if ($target -match '^([^#]+)\s+#') {
                $target = $Matches[1].Trim()
            }

            # Skip local actions and Docker references
            if ($target -match '^\./' -or $target -match '^\.\.' -or $target -match '^docker://') {
                continue
            }

            # Third-party action: expect owner/repo@40-hex-char SHA
            if ($target -match '@[0-9a-fA-F]{40}$') {
                continue
            }

            $unpinnedFound = $true
            $lineNum = $i + 1
            $results.Add((Format-FylgyrResult `
                -CheckName 'ActionPinning' `
                -Status 'Fail' `
                -Severity 'High' `
                -Resource "$($wf.Path):$lineNum" `
                -Detail "Unpinned action reference: $target" `
                -Remediation 'Pin this action to a full 40-character commit SHA instead of a tag or branch.' `
                -AttackMapping @('trivy-tag-poisoning', 'tj-actions-shai-hulud')))
        }

        if (-not $unpinnedFound) {
            $results.Add((Format-FylgyrResult `
                -CheckName 'ActionPinning' `
                -Status 'Pass' `
                -Severity 'Info' `
                -Resource $wf.Path `
                -Detail 'All action references are SHA-pinned.' `
                -Remediation 'None.'))
        }
    }

    return $results.ToArray()
}