CI/CD workflow audit GitHub Actions workflow auditor — W1-W7 covering pwn-request, workflow_run from forks, script-injection interpolations, unpinned third-party actions, persist-credentials, write-all permissions, self-modifying workflows.

CI/CD is the highest-leverage attack surface in modern development. A compromised workflow runs with secrets, publishes to package registries, and re-keys the production environment. The 2024-2026 attack record (Shai-Hulud, Tj-actions/changed-files, Adam Berman's pwn-request catalog, GitGuardian self-replicating workflows) documents the canonical malicious patterns; this detector codifies them as W1-W7 rules.

What gets parsed

The auditor walks .github/workflows/*.yml under any root (defaults to cwd) and parses each workflow as YAML. Per workflow it extracts:

Trusted-owner allowlist

Actions from these owners are not flagged as unpinned- third-party even if they use a moving tag:

actions, github, azure, docker, google-github-actions, aws-actions, anthropic-experimental, anthropics, hashicorp, actions-rs, advanced-security, code-scanning.

Operators extend via DIGGER_CI_TRUSTED_ACTION_OWNERS (comma-separated).

Detection layers (W1–W7)

IDSeverityWhat it catchesMITRE
W1criticalPwn-request: pull_request_target + checkout of PR head ref. Documented by GitHub Security Lab — the canonical CI compromise pattern.T1199
W2highworkflow_run trigger. Re-runs in privileged context with access to the originating workflow's artifacts (often fork-PR test runs).T1199
W3highUntrusted-input interpolation in run: block (PR title, issue body, comment body, head_ref, commit message). Script injection by anyone who can submit content.T1059
W4mediumUnpinned third-party action (moving tag, not trusted owner). The Tj-actions/changed-files compromise shape.T1195.002
W5mediumpersist-credentials: true after checkout — GITHUB_TOKEN baked into .git/config for the rest of the run.T1552.001
W6medium / infoTop-level permissions: = write-all (medium) or no block at all (info — legacy org default).T1078.004
W7criticalSelf-modifying workflow — any step writes to .github/workflows/. The worm-persistence primitive (Shai-Hulud, octofiles).T1195.002

A workflow file that the GitHub runner accepts but a YAML parser rejects produces a workflow_parse_error finding — itself a smell.

CLI

$ digger ci audit-workflows --case-dir /tmp/case [--roots .,vendor/repo-a]

[ci] workflows audited: 12
[ci]   pwn-request pattern:        0
[ci]   workflow_run-triggered:     1
[ci]   injectable interpolations:  2
[ci]   unpinned 3rd-party actions: 4
[ci]   self-modifying:             0
[ci] artifacts emitted: 12

Fixing W3 (the common one)

Replace the interpolation with an env var binding and quote-safe shell. The bash interpreter sees a literal variable, not a templated string:

- run: |
    # WRONG — script injection via PR title:
    echo "${{ github.event.pull_request.title }}"

- run: |
    echo "$PR_TITLE"
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}