hookwarden
v0.4.0
Published
Webhook security audit CLI — finds signature-verification bugs in JavaScript, TypeScript, Python, and PHP codebases. Local, deterministic, zero-network. Ships rules for Stripe, GitHub, Shopify, Slack, Twilio, and Square; JSON / SARIF 2.1.0 output for CI a
Maintainers
Readme
npx hookwarden scan ./your-appNo traffic leaves your machine. No telemetry. No SaaS sign-up required.
Contents
- Why
- Quickstart
- Real output
- Provider coverage
- CI integration
- Architecture
- vs. other tools
- Advanced usage
- Roadmap
- Contributing
- License
Why
Every dollar of fraud loss that flows through a webhook starts with a verification bug — and verification bugs hide in plain sight.
A handler that accepts an unsigned payload, compares HMACs with ==, or skips the signature check on a ?test=true path will silently route attacker traffic into your business logic. The bug is one line of code in a 50K-line app, and the code looks plausible — not the shape general-purpose SAST tools are tuned to flag. They were built to catch SQL injection and prototype pollution; webhook verification falls between their default rule packs.
Hookwarden does one thing. It walks your repo, parses every webhook handler across Express, Hono, Fastify, Next.js, Flask, FastAPI, Django, Laravel, Symfony, Slim, and vanilla-PHP single-file handlers, and labels each one verified, not-verified, or manual-review — with the exact file, line, and a fix drawn verbatim from provider documentation. That focus is the entire point. The provider catalog (Stripe, GitHub, Shopify, Slack, Twilio, Square — and growing) encodes signature-format quirks no generic scanner has the surface area to know: that Stripe uses HMAC-SHA256 with a 5-minute timestamp tolerance, that Slack uses v0:${ts}:${body} not raw-body, that Twilio is the SHA1 outlier the rest of the catalog has to accommodate.
The three-state verdict is not a hedge. manual-review is what you get when hookwarden can't prove safety or unsafety from the source alone — a handler inside a middleware chain that the analyzer couldn't fully unroll, for example. It's how the false-positive rate stays honest. A tool that reports every gray area as a bug is not a security tool; it's noise.
🌐 Languages & frameworks
3 languages, 11 frameworks. PHP and Python use tree-sitter (WASM); JS/TS use Babel.
| Language | Frameworks | Parser |
|---|---|---|
| JavaScript / TypeScript | Express · Hono · Fastify · Next.js | @babel/parser |
| Python | Flask · FastAPI · Django | tree-sitter-python |
| PHP | Laravel · Symfony · Slim · vanilla-PHP single-file | tree-sitter-php |
PHP 8.0+ syntax floor. Python 3.10+ recommended.
🚀 Quickstart
First use — no install required:
npx hookwarden scan ./your-appMake CI fail on high+ findings:
npx hookwarden scan ./your-app --fail-on high --format json
# Exit codes: 0 clean · 1 findings at threshold · 2 engine error · 3 config error · 4 parse coverage below floorPR-scoped scan + CI gate:
npx hookwarden scan ./your-app --diff-only --diff-base origin/main --fail-on highUpload to GitHub Code Scanning:
npx hookwarden scan ./your-app --format sarif > findings.sarif
gh api -X POST /repos/$REPO/code-scanning/sarifs \
-F [email protected] -F ref=$GITHUB_REFNon-greenfield adoption — accept existing findings, gate only NEW ones:
npx hookwarden scan ./your-app --baseline write
git add .hookwarden.baseline.json && git commit -m "chore: hookwarden baseline"
# Subsequent scans auto-read the baseline; only new findings are reported.
npx hookwarden scan ./your-app --fail-on highList every detected handler (no rule evaluation):
npx hookwarden inventory ./your-appPhased rollout — gate CI on one provider at a time:
npx hookwarden scan ./your-app --provider stripe --fail-on high
# Comma-separated for multiple: --provider stripe,githubMonorepo scoping — exclude / include paths from the command line:
npx hookwarden scan ./your-app --exclude 'packages/legacy/**,vendor/**'
npx hookwarden scan ./your-app --include 'packages/api/**'
# Both compose: --include narrows first, --exclude removes after.Strict suppressions (compliance teams):
npx hookwarden scan ./your-app --strict-suppressionsOr install permanently:
npm install -g hookwarden # global
npm install --save-dev hookwarden # dev dependency (CI-pinnable)Full flag reference: npx hookwarden --help.
Real output
Clean scan — exits 0:
hookwarden scan ./your-app
✓ Scanned 24 candidates · 24 parsed (100.0% coverage) · 0 findingsScan with a bug — exits 1:
hookwarden scan ./your-app
CRITICAL
────────
server.js:10:1
stripe/express-middleware-ordering [not-verified]
Express webhook handler for Stripe has `express.json()` registered before
the webhook route. JSON middleware consumes the request body; by the time
the Stripe handler runs, the raw bytes used for HMAC are gone.
Fix: register `express.json()` AFTER the webhook route, OR mount
`express.raw({ type: 'application/json' })` only on the webhook path.
↳ https://stripe.com/docs/webhooks/signatures
HIGH
────
handlers/slack.ts:34:1
slack/missing-timestamp-validation [not-verified]
Slack handler does not enforce the 5-minute replay window. The
`X-Slack-Request-Timestamp` header is present but not compared against
the current time before signature verification proceeds.
Fix: reject requests where abs(Date.now()/1000 - ts) > 300 before
computing the `v0:${ts}:${body}` signing string.
↳ https://api.slack.com/authentication/verifying-requests-from-slack
────────────
Found 1 critical · 1 high · 0 medium · 0 low · 0 info · 0 manual-review
Scanned in 0.3 s · 24 / 24 candidates parsed (100.0% coverage)JSON envelope shape:
{
"schema_version": "1.0",
"engine": { "version": "0.2.0", "commit_sha": null },
"rule_pack": { "version": "0.2.0", "content_hash": "51c219..." },
"scan": {
"counts": {
"active": { "critical": 1, "high": 1, "medium": 0, "low": 0, "info": 0 },
"suppressed": { "critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0 }
},
"findings": [
{
"rule_id": "stripe/express-middleware-ordering",
"severity": "critical",
"state": "not-verified",
"provider": "stripe",
"file_path": "server.js",
"location": { "line": 10, "col": 1 },
"finding_id": "stripe/express-middleware-ordering@d603a04...",
"primary_location_line_hash": "d603a04...",
"message": "Express webhook handler for Stripe has...",
"redacted_snippet": "app.use(express.json())\napp.post('/webhook', ...",
"suppressed": null
}
],
"scanned_at": "2026-05-05T18:31:33.653Z",
"parsed_files_count": 1,
"parse_candidates_count": 1
},
"suppressions": { "applied": [], "stale": [] }
}Sorted keys, schema-versioned, byte-stable across runs (modulo scanned_at). SARIF output round-trips through GitHub Code Scanning and deduplicates via partialFingerprints on re-upload.
🔐 Provider coverage
45 rules across 6 providers as of v0.4. Every rule carries fix guidance quoted verbatim from the provider's canonical security documentation.
| Provider | Rules | Detection types | Custom predicate |
|---|---|---|---|
| Stripe | 9 | missing-sig-verif, timing-unsafe, raw-body, missing-timestamp, wrong-hmac, unreachable-verif, hardcoded-secret (whsec_), library-verified | — |
| GitHub | 9 | missing-sig-verif, timing-unsafe, raw-body, missing-timestamp, wrong-hmac, unreachable-verif, hardcoded-secret (ghs_, github_pat_), library-verified | — |
| Shopify | 7 | missing-sig-verif, timing-unsafe, raw-body, missing-timestamp (info), wrong-hmac, unreachable-verif, library-verified | — |
| Slack | 7 | missing-sig-verif, timing-unsafe, raw-body, missing-timestamp (high), wrong-hmac, unreachable-verif, library-verified | Parameterized timestamp_dot_body recipe |
| Twilio | 7 | missing-sig-verif, timing-unsafe, raw-body, missing-timestamp (info), wrong-hmac, unreachable-verif, library-verified | predicates/custom/twilio-signing.ts — URL+sorted-params canonical-string + HMAC-SHA1 |
| Square | 6 | missing-sig-verif, timing-unsafe, raw-body, wrong-hmac, unreachable-verif, library-verified | Parameterized custom_field_tuple recipe |
Full per-rule applicability matrix: docs/rule-coverage.md.
CI integration
GitHub Action (recommended)
# .github/workflows/hookwarden.yml
name: hookwarden
on: [pull_request, push]
permissions:
contents: read
pull-requests: write
security-events: write
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: Hookwarden/hookwarden-action@v1
with:
fail-on: highUploads SARIF to Code Scanning automatically. Findings appear as PR annotations.
Raw CLI + SARIF upload
- uses: actions/setup-node@v4
with: { node-version: '22' }
- run: npx hookwarden scan . --format sarif > hookwarden.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: hookwarden.sarifSARIF severity mapping: critical/high → error · medium → warning · low/info → note.
Architecture
hookwarden is a pnpm monorepo with three load-bearing packages and a strict dependency boundary enforced in CI.
@hookwarden/rules → @hookwarden/engine → hookwarden CLI
(YAML data + (pure-functional, (binary, renders
predicate factories) I/O-free) output)| Package | Purpose | License |
|---|---|---|
| @hookwarden/engine | Webhook handler discovery, reachability analysis, evidence collection. Pure-functional, browser-safe — no I/O, no filesystem, no network. | Apache 2.0 |
| @hookwarden/rules | Provider catalog, YAML rule packs, parameterized predicate factories. | Apache 2.0 |
| hookwarden | CLI binary. Reads config, drives the engine, renders text/JSON/SARIF. | Apache 2.0 |
The engine's I/O boundary is the architectural load-bearing constraint. The same engine runs in the CLI, in CI, and — eventually — in a browser playground without modification. dependency-cruiser enforces the boundary in every PR.
vs. other tools
Hookwarden is specialized on purpose. Webhook signature verification is the only thing it does, and that's why it does it better than tools whose surface area covers everything. The general-purpose scanners below are excellent at what they do — they're just not in this fight.
| Tool | What it does well | Webhook verification coverage | |---|---|---| | semgrep | General-purpose SAST; flexible rule authoring | Low signal — generic pattern matching misses body-parsing ordering, timing-safe comparison paths, and SDK-specific verification flows | | snyk Code | Broad vulnerability detection in paid SaaS | No webhook-specific rules; doesn't model HMAC reachability | | GitGuardian | Secret leak detection in git history and CI | Finds hardcoded secrets; does not audit whether verification logic is correct | | TruffleHog | Secret scanning across sources | Same as GitGuardian — leak focus, not logic focus | | Datadog Static Analysis | Broad SAST; good AWS/cloud signal | No webhook verification specialization; high false-positive rate for this class of bug | | hookwarden | Webhook verification logic only | 45 rules, 6 providers, three-state verdicts, <5% FP target measured against a 200-repo OSS corpus |
If you're already running semgrep or snyk: hookwarden is additive, not a replacement. It finds the class of bug those tools were not built to find.
Advanced usage
Three suppression mechanisms, in order of preference:
Inline — best for one-off cases; the comment is grep-able evidence in code review:
// hookwarden-disable-next-line stripe/missing-signature-verification
app.post('/webhook', rawBodyHandler);.hookwardenignore — gitignore syntax; best for path-scoped suppression:
__tests__/
fixtures/**/*.spec.ts
mocks/Baseline — best for adopting on a non-greenfield codebase without failing CI on day one:
# Capture current state
hookwarden scan . --baseline write
# Subsequent runs suppress baselined findings; new findings still fail
hookwarden scan .--format json reports each finding's suppression source (inline / ignorefile / baseline) so suppressions are auditable.
| Code | Meaning |
|------|---------|
| 0 | Clean — no findings at or above the configured --fail-on threshold |
| 1 | Findings at or above threshold |
| 2 | Engine error (parser crash, unreadable input) |
| 3 | Config error (malformed hookwarden.config.yaml) |
| 4 | Parse coverage below parse_coverage_min |
Precedence: 3 > 2 > 4 > 1 > 0. The highest applicable code wins; use this for branching logic in CI pipelines.
Drop a hookwarden.config.yaml at your project root (or any ancestor directory):
schema_version: '1.0'
fail_on: high # critical | high | medium | low | info
parse_coverage_min: 0.9 # fail if < 90% of candidates parsed
baseline:
enabled: true
path: .hookwarden.baseline.jsonPrecedence: CLI flag > hookwarden.config.yaml > built-in defaults.
Inventory mode (lists detected handlers without running rules):
hookwarden inventory ./your-app| hookwarden severity | SARIF level | GitHub Code Scanning |
|---|---|---|
| critical | error | Blocks PR merge (if branch protection configured) |
| high | error | Blocks PR merge |
| medium | warning | Visible annotation, non-blocking |
| low | note | Visible annotation |
| info | note | Visible annotation |
Re-uploading the same scan deduplicates via SARIF partialFingerprints. Full mapping table: packages/cli/docs/sarif-severity-mapping.md.
Roadmap
v0.5 — Auto-remediation engine. hookwarden fix — mechanically rewrites safety: safe findings across JS/TS, Python, and PHP. Dry-run is the default; --write opts into atomic-staging writes with re-scan verification. Three-tier safety gate (safe / all / manual-only-explain). The largest extension of the correctness moat in v1 — hookwarden goes from "tells you the fix" to "applies it."
v0.6 — More providers. Adyen, Zendesk, Mailgun, SendGrid — each measured against the 200-repo OSS regression corpus before release, with a published false-positive rate.
Contributing
Rule-pack PRs are the highest-value contribution. Adding a new provider is a catalog edit plus N rule YAMLs — the factory architecture means most providers ship without any new TypeScript. See the existing six providers in packages/rules/rules/ as worked examples.
Bug reports and feature requests: open an issue.
Local development:
pnpm install
pnpm -r build
pnpm -r testDocumentation
License
Apache 2.0. The CLI, engine, and rule packs in this repo are open source and will remain so. A separate closed-source SaaS tier handles continuous monitoring, secret leak scanning, automated rotation, and SOC 2 evidence export — hookwarden.dev.
