actions-guardian
v0.1.1
Published
Scan .github/workflows for GitHub Actions referenced by a mutable tag/branch instead of a pinned commit SHA, and auto-pin them.
Maintainers
Readme
actions-guardian
Scan .github/workflows/*.yml for GitHub Actions referenced by a mutable
tag or branch (uses: actions/checkout@v4) instead of a pinned commit SHA,
and auto-pin them.
Why this matters
A tag like @v4 can be repointed by anyone with write access to that
action's repo — including an attacker who compromises a maintainer's
account. Your workflow then silently runs different code the next time it
fires, with whatever permissions (GITHUB_TOKEN, secrets) it has. This is
exactly how the tj-actions/changed-files compromise
and the ultralytics npm/PyPI supply-chain attack played out in 2025.
GitHub's own security hardening guide recommends pinning third-party
actions to a full commit SHA for exactly this reason.
Same "Guardian" family pattern as
postinstall-guardian,
secrets-guardian, and
vuln-guardian: scan → review
→ approve into a committed allowlist → gate CI on anything new.
How it works
actions-guardian scanreads every workflow file and finds eachuses:step.- A reference whose ref (the part after
@) isn't a 40-character commit SHA is flagged as unpinned. Local (./action) and Docker (docker://image) references are skipped — the SHA-pinning risk is specific to versioned GitHub Actions. actions-guardian pinresolves each unpinned ref to its current commit SHA via the GitHub API and rewrites the line in place, keeping the original tag as a trailing comment (@<sha> # v4) — the same convention Dependabot and GitHub's own docs use, so the human-readable version stays visible.- Flagged references are checked against
.actions-guardian-allowlist.jsonfor ones you've reviewed and intentionally left unpinned.
Install
npm install --save-dev actions-guardianCLI
actions-guardian scan
actions-guardian scan --dir . --report report.mdactions-guardian pin
actions-guardian pinResolves and rewrites every unpinned reference. Uses the GitHub REST API —
unauthenticated calls are rate-limited to 60/hour; set GITHUB_TOKEN (CI
runners already have one) or GH_TOKEN to raise that to 5000/hour.
GITHUB_TOKEN=$(gh auth token) actions-guardian pinactions-guardian approve
actions-guardian approveAdds every currently-unpinned reference to the allowlist. Use this for a
reference you've reviewed and are intentionally leaving unpinned (rare —
usually pin is the right move instead).
actions-guardian ci
actions-guardian ci --report actions-guardian-report.mdScans, writes a report, and exits 1 if anything unpinned isn't approved.
actions-guardian init-workflow
actions-guardian init-workflow
# writes .github/workflows/actions-guardian.ymlRuns on every push/PR to main plus a daily backstop. The generated
workflow's own actions/checkout and actions/setup-node steps are
pre-pinned to a commit SHA — a tool that flags unpinned actions shouldn't
generate a template full of them.
Library API
import { scan, pinFindings } from 'actions-guardian';
const result = await scan('./my-project');
if (result.unapproved.length > 0) {
await pinFindings('./my-project', result.unapproved);
}Limitations
- Only GitHub-hosted actions (
owner/repo[/path]@ref) are checked. Docker and local actions have different pinning mechanisms (image digest, git history) and aren't covered. uses:values are matched with a regex, not a full YAML parser — this covers every realistic workflow file, but a deliberately unusual YAML structure (e.g. a multi-line flow-style step) could be missed.
