← back to gallery

action-shield

Paste a GitHub Actions workflow, get an instant hardening audit and a live CVE feed.

dev-toolsgithub-actionssecuritysupply-chainci-cddevsecopsworkflowstatic-analysis
Open product ↗

action-shield

Paste any .github/workflows/*.yml and get an instant security audit — Pwn Request, cache poisoning, script injection, unpinned actions, broken permissions, and more — backed by a live CVE feed for the GitHub Actions ecosystem and a safety scoreboard for the most popular actions.

Built in response to the TanStack May 11 2026 compromise (CVE-2026-45321: pull_request_target
"Pwn Request" + cache poisoning + OIDC token theft) and the Mini Shai-Hulud May 19 wave that
together compromised hundreds of npm packages via GitHub Actions misconfigurations.

CLI scanners exist (zizmor, poutine, actionlint). action-shield is the public web auditor your
team Slack actually needs the morning after the next post-mortem drops.

Features

Detection rules (13)

| ID | Severity | What it catches |
|---|---|---|
| PWN_REQUEST | high | pull_request_target workflow that checks out PR code |
| PR_TARGET_CACHE | high | pull_request_target + actions/cache write (TanStack pattern) |
| SCRIPT_INJECTION | high | Untrusted GitHub context interpolated in run: |
| WORKFLOW_DISPATCH_INPUT_INJECTION | high | ${{ inputs.foo }} in run: without env quoting |
| UNPINNED_THIRD_PARTY | medium | Non-actions/ action pinned by tag instead of full SHA |
| SECRETS_INHERIT | medium | Reusable workflow called with secrets: inherit |
| BROAD_PERMISSIONS | medium | Top-level permissions: write-all or : write |
| CURL_PIPE_SHELL | medium | curl ... \| bash (or wget / iex) in a run: step |
| SELF_HOSTED_RUNNER_PUBLIC | medium | Self-hosted runner on a pull_request trigger w/o guard |
| UNPINNED_FIRST_PARTY | low | actions/
action pinned by tag instead of SHA |
| MISSING_PERMISSIONS | low | No top-level permissions: block declared |
| OIDC_NO_AUDIENCE | low | id-token: write granted but no audience: set |
| CACHE_KEY_NO_HASH | low | actions/cache key uses github.ref/sha but no hashFiles() |

Stack

No authentication anywhere. All endpoints are public — Arda wants instant inspection without
typing a password. /api/audit is rate-limited by hashed-IP (60/hour) for abuse protection only.

Real data sources (no mocks, no seeds, no fake "preset" data)

| Source | URL | Auth | Refresh |
|---|---|---|---|
| GitHub Security Advisories (Actions) | https://api.github.com/advisories?ecosystem=actions | optional GITHUB_TOKEN bearer (raises rate from 60 to 5000 req/h) | every 15 minutes |
| OSV.dev query | https://api.osv.dev/v1/query (POST) | none | every 60 minutes, round-robin over tracked actions |
| GitHub repo + releases | https://api.github.com/repos/{owner}/{repo} and /releases?per_page=1 | optional bearer | every 6 hours per tracked action |
| GitHub search (discovery) | https://api.github.com/search/repositories?q=topic:github-actions+stars:>500 | optional bearer | daily |

The only hardcoded data is a list of names of ~50 popular GitHub Actions in
fetchers/seed_actions.js. Every numeric field (stars, latest version, open CVE count, etc.) is
fetched live at runtime and stored with a refreshed_at timestamp. If a fetch fails the row
shows ; the app never fabricates.

Endpoints

All paths are under BASE_PATH (default /action-shield).

| Method | Path | Description |
|---|---|---|
| GET | / | Main SPA |
| GET | /health | {status:"ok"} — auth-free for orchestrator smoke check |
| GET | /api/cves | Paginated CVE list — filter by ?severity=, ?since=7d, ?package= |
| GET | /api/cves/:ghsa_id | CVE detail (description, references, affected) |
| GET | /api/actions/top | Top tracked actions, sortable by stars / cves / released |
| GET | /api/actions/:owner/:repo | Single action with CVE history |
| POST | /api/audit | Body: {"yaml": "..."} — returns findings + score |
| GET | /api/audits/recent | Last 50 audits, anonymized counts only |
| GET | /api/rules | All detection rules with examples |
| GET | /api/rules/:id | Single rule detail |
| GET | /api/stats | Aggregate dashboard stats (totals, top rules, last-fetch timestamps) |

Cron schedule

| Cron | Job |
|---|---|
| /15 | refresh GHSA Actions advisories |
| 7
| OSV.dev cross-check (round-robin) |
| 0
/6 | refresh repo metadata / releases for tracked actions |
| 0 3
| discover new popular actions via GitHub search |
| 30 4 | recompute open_cve_count per action |
| 0 5
| prune audits + fetch_log older than 30 days |
| 0 0 * | rotate daily IP-hash salt |

An initial fetch runs ~5 seconds after boot so the dashboard isn't empty on first load.

Run locally

cp .env.example .env
npm install
npm start
# open http://localhost:4827/action-shield/

Configuration

| Env var | Default | Purpose |
|---|---|---|
| PORT | 4827 | HTTP port |
| BASE_PATH | /action-shield | URL prefix for everything (lets you mount under nginx) |
| NODE_ENV | production | flips morgan format and log verbosity |
| GITHUB_TOKEN | (none) | optional PAT with no scopes — raises GHSA / search rate from 60 to 5000 req/h |
| DB_PATH | ./action-shield.db | SQLite file path |

License

MIT