actionsec
v0.1.0
Published
Scan GitHub Actions workflows for security issues — unpinned actions, script injection, broad permissions, pull_request_target footguns. Zero config, zero dependencies.
Maintainers
Readme
actionsec
Your CI pipeline runs with a write token and your secrets. actionsec checks
it isn't a liability. It scans your GitHub Actions workflows for the handful of
mistakes that actually get repos compromised — unpinned third-party actions,
script injection from untrusted input, over-broad token permissions, and the
pull_request_target footgun. Zero config, zero dependencies.
npx actionsec.github/workflows/ci.yml
✗ critical L12 pull-request-target-checkout `pull_request_target` checks out PR head code — untrusted code runs with a write token and secrets
✗ critical L16 script-injection untrusted `github.event.pull_request.title` in a run step — pass it through an env: var instead
✗ high L5 broad-permissions `permissions: write-all` gives the token full read/write — scope it down
✗ high L13 unpinned-action `some-marketplace/deploy-action@main` is a mutable branch — pin to a full commit SHA
✗ medium L10 unpinned-action `actions/checkout@v4` is a mutable tag — pin to a full commit SHA
✗ 5 issue(s) in 1 of 1 file(s) — 2 critical, 2 high, 1 mediumWhy
The 2025 supply-chain attacks (reviewdog, tj-actions/changed-files) all rode in through the same door: a workflow that trusted a mutable action tag, so when the upstream tag was repointed at malicious code, every consumer ran it — with a token that could push commits and read secrets. 71% of repos never pin actions to a SHA.
actionlint validates workflow syntax; zizmor does deep dataflow analysis but
wants installation and config. actionsec fills the gap between them: a five-second,
zero-config pass over the highest-impact security checks, small enough to drop in a
pre-commit hook or a one-line CI step.
The checks
| Check | Severity | What it catches |
|-------|----------|-----------------|
| unpinned-action | high / medium | uses: a mutable tag or branch (@v4, @main) instead of a 40-char commit SHA. Third-party = high, GitHub-owned actions/* = medium. |
| script-injection | critical | ${{ github.event.*.title \| .body \| .head_ref ... }} interpolated into a run: step, where GitHub substitutes it into the shell before it runs. |
| broad-permissions | high | permissions: write-all — the token gets full read/write across the repo. |
| missing-permissions | low | no permissions: block at all, so the workflow inherits the repo default scope. |
| pull-request-target-checkout | critical | pull_request_target + a checkout of the PR head — untrusted code runs with a privileged token and secrets. |
It is not a YAML validator (use actionlint) and not a deep dataflow analyzer (use zizmor). It's a fast, line-based first pass — which is also why it's zero-dependency: it never needs to parse YAML into a tree.
Usage
actionsec # scan ./.github/workflows
actionsec path/to/repo # scan another repo's workflows
actionsec ci.yml release.yml # scan specific files
actionsec --min-severity high # only high + critical (good for a hard CI gate)
actionsec --format json # machine-readableTry it on the bundled example after cloning:
node bin/cli.js examples # → flags the deliberately-vulnerable demo workflowIn CI
actionsec exits non-zero when it finds issues, so it gates a pipeline directly.
A common setup is to fail only on the serious stuff:
# .github/workflows/security.yml
- run: npx actionsec --min-severity high| Exit code | Meaning |
|-----------|---------|
| 0 | no issues at or above the threshold |
| 1 | issues found |
| 2 | error (no workflows found, unreadable file) |
Options
--min-severity <sev> low | medium | high | critical (default: low)
--format text|json output format (default: text)
-v, --version
-h, --helpAlso available for Python
pip install actionsec
actionsecSame checks, same severities, same exit codes — actionsec-py. Handy in Python-based CI and pre-commit setups.
License
MIT
