shadowaudit
v2.0.0
Published
Static API security scanner — finds undocumented & unauthenticated routes across 8 frameworks (Express, FastAPI, Django, Flask, NestJS, Rails, Grape, Spring Boot). Maps to OWASP API Top 10.
Maintainers
Readme
☾☁ shadowaudit
Find shadow API routes before attackers do. Static analysis for 8 frameworks. Maps to OWASP API Top 10.
Quick Start (30 seconds)
npm install -g shadowaudit
shadowaudit --dir ./srcThat's it. Every route in your codebase gets scanned, mapped to OWASP API Top 10, and scored 0-100.
Why shadowaudit?
The problem: Your API has routes that exist in code but aren't in your OpenAPI spec. These "shadow routes" are the #1 cause of API breaches — OWASP calls it API9 (Improper Inventory Management). shadowaudit finds them statically, before production.
The proof: Tested on real codebases — found 269 shadow routes in Ghost CMS, 2,449 routes in GitLab, with a 98.2% false-positive reduction on Mastodon's 766-route codebase.
What it looks like
┌──────────┬────────────────┬────────┬──────────────────────────────┬──────────────┐
│ SEVERITY │ OWASP │ METHOD │ PATH │ AUTH │
├──────────┼────────────────┼────────┼──────────────────────────────┼──────────────┤
│ CRITICAL │ API5 (BFLA) │ POST │ /admin/delete │ [no auth] │
│ CRITICAL │ API1 (BOLA) │ GET │ /api/internal/metrics │ [no auth] │
│ INFO │ API9 (Shadow) │ GET │ /debug/* │ [env-only] │
└──────────┴────────────────┴────────┴──────────────────────────────┴──────────────┘
┌─────────────────────────────────────────────┐
│ SCAN SUMMARY │
│ Risk Score : 70/100 │
│ Risk Band : MEDIUM RISK │
└─────────────────────────────────────────────┘
── OWASP API Security Top 10 (2023) mappings ──
API1: Broken Object Level Authorization (BOLA)
API5: Broken Function Level Authorization (BFLA)
API9: Improper Inventory Management (Shadow APIs)5 auth statuses (v1.7.0 — cuts false positives dramatically):
| Status | Color | Meaning |
|--------|-------|---------|
| [auth] | 🟢 green | Route-level auth middleware detected |
| [upstream-auth] | 🔵 blue | Auth applied globally via app.use(authenticate) |
| [controller-auth] | 🔵 blue | Auth checked inside handler body (if (!req.user) return 401) |
| [env-only] | 🟡 yellow | Route only mounted in dev/test env (if NODE_ENV === 'test') |
| [no auth] | 🔴 red | No auth detected at any level — this is the one that matters |
Only [no auth] routes count as CRITICAL in the risk score. The other 4 statuses are INFO — no false alarms.
Features
Free tier (MIT licensed, no signup)
- ✅ 8 frameworks: Express, FastAPI, Django, Flask, NestJS, Rails, Grape, Spring Boot
- ✅ OWASP API Top 10 (2023) mapping — every finding maps to API1-API10
- ✅ Risk score (0-100) — single number per scan, color-coded
- ✅ 5 auth detection layers — route-level, upstream, controller, env-only, none
- ✅
.shadowauditignore— glob patterns to exclude intentional routes - ✅ Table + JSON + SARIF + Markdown output — pipe into any CI tool
- ✅ GitHub Actions annotations — inline
::errorannotations on PR diffs - ✅ SARIF for GitHub Security tab — upload findings to GitHub's native security UI
- ✅ Badge SVG — embed your risk score in your README
Pro tier ($19/mo founding rate · $40/mo standard)
- 🔒 Cloud dashboard — scan history, trends, diff reports between deploys
- 🔒 ABOM PDF reports — compliance-ready Application Bill of Materials
- 🔒 Share links —
--shareflag generates public read-only report URLs - 🔒 Unlimited scan history — track security debt over time
Install
npm install -g shadowauditRequires Node.js 18+.
Usage
# Basic scan — scans ./src, outputs colored table
shadowaudit --dir ./src
# With OpenAPI spec comparison — finds shadow routes (in code, not in spec)
shadowaudit --dir ./src --spec ./openapi.json
# SARIF output for GitHub Security tab
shadowaudit --dir ./src --format sarif > shadowaudit.sarif
# Check your cloud account tier & sync status (Free/Pro)
shadowaudit --status
# Save your dashboard token for --cloud
shadowaudit --cloud-login <your-token>
# Upload to cloud dashboard (Pro)
shadowaudit --dir ./src --cloud
# Generate ABOM PDF (Pro)
shadowaudit --dir ./src --abom
# Share report link (Pro)
shadowaudit --dir ./src --share
# Watch mode — re-scan on file change
shadowaudit --dir ./src --watchGitHub Actions integration
Add this to .github/workflows/shadowaudit.yml:
name: shadowaudit
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install -g shadowaudit
- run: shadowaudit --dir ./src --format sarif > shadowaudit.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: shadowaudit.sarif
- run: shadowaudit --dir ./src --fail-on criticalCRITICAL findings block the PR. SARIF appears in GitHub's Security tab.
.shadowauditignore
Exclude intentional routes (health checks, metrics, debug endpoints) from findings:
# Health checks — intentionally public
/health
/healthz
/ready
# Internal metrics — exposed for monitoring
/api/internal/*
# Re-include a specific route (negation)
!/api/internal/healthRisk score recalculates based on remaining findings only.
Supported frameworks
| Framework | Auto-detect | Auth detection | OWASP mapping | |-----------|:-----------:|:--------------:|:-------------:| | Express | ✅ | ✅ | ✅ | | FastAPI | ✅ | ✅ | ✅ | | Django | ✅ | ✅ | ✅ | | Flask | ✅ | ✅ | ✅ | | NestJS | ✅ | ✅ | ✅ | | Rails | ✅ | ✅ | ✅ | | Grape | ✅ | ✅ | ✅ | | Spring Boot | ✅ | ✅ | ✅ |
Proven on real codebases
| Codebase | Routes found | Findings | False-positive rate | |----------|:------------:|:--------:|:-------------------:| | Ghost CMS | 269 shadow routes | 14 CRITICAL | 0% | | GitLab | 2,449 routes | 89 CRITICAL | 1.8% | | Mastodon | 766 routes | 23 CRITICAL | 1.8% (98.2% reduction) | | Spring PetClinic | 27 routes | 0 CRITICAL | 0% (0 false positives) |
Pricing
The CLI is free forever — all 8 frameworks, all output formats, unlimited scanning, no signup. Pro adds the hosted layer:
| | Open Source | Pro ($19/mo founding · $40/mo after) |
|---|---|---|
| Local scanning (all 8 frameworks) | ✓ unlimited | ✓ unlimited |
| SARIF / JSON / markdown / table output | ✓ | ✓ |
| ABOM generation + diffing (local) | ✓ | ✓ |
| --fail-on CI policy gates | ✓ | ✓ |
| Cloud dashboard sync (--cloud) | — | ✓ |
| Scan history + trend tracking | — | ✓ |
| Share links (--share) | — | ✓ |
| Email alerts on CRITICAL findings | — | ✓ |
| ABOM PDF export | — | ✓ |
The first 20 subscribers lock the founding rate — $19/mo or $179/yr for life. After that, Pro is $40/mo or $384/yr and every checkout link flips automatically — founding members keep their locked price from their original subscription.
Enterprise — SSO/SAML, team seats, custom scanners, compliance reporting, SLA. Priced individually — contact [email protected].
Payments — crypto (USDC, BTC, ETH, USDT) via Suby out of the box, or debit card: message us on WhatsApp and we'll set you up.
Check your tier anytime with shadowaudit --status. Manage billing or upgrade at the pricing page. 14-day full refund, no questions asked. Cancel anytime.
The promise: no feature that is free today will ever move behind a paywall. Pro adds collaboration and history — it never removes CLI capabilities.
v2.0.0 — Major Stabilization Release (Build, Allowlist & Framework Resolution)
- Consolidated Build & Core Engine Stabilization: Resolved duplicate route matching utilities, fixed TypeScript syntax and compilation errors across all modules, and fully restored build stability.
- Allowlist Wildcard & Type Hardening: Fixed crash (
TypeError: pattern.endsWith is not a function) when handling non-string allowlist patterns and enhanced wildcard prefix (/*) and mid-path pattern matching. - Ancestor-Aware Auto-Detection: Restored reliable framework auto-detection for standard repository layouts where source code is located in subdirectories (
./src).
v1.9.1 — Fixed the README quickstart (ancestor-aware framework detection)
shadowaudit --dir ./srcnow works on standard project layouts. v1.9.0's auto-detection only looked inside the scan directory, so any project withpackage.jsonat the repo root and sources in./src— the exact layout in the 30-second quickstart above — failed withCould not auto-detect frameworkand exit 2. Detection now resolves in order: the scan dir itself (nearest marker wins), then the ancestor chain up to 5 levels up (covers repo roots and monorepo package roots), then the cwd. Monorepo-safe: a nested package declaring@nestjs/corewins over a root-levelexpressdependency.- 5 new regression tests (520 total). Scanning behavior is unchanged — only framework resolution widened.
v1.9.0 — Free/Pro tier clarity, --status, and 30+ fixes from adversarial QA
Cloud & tiers:
- Fixed cloud sync end-to-end — the CLI always sent its token via the
Authorizationheader, but the dashboard API only readbody.token, so--cloudfailed with400 Missing token fieldfor every prior version. The API now accepts the header (all existing CLI versions work once the dashboard redeploys). - New
--status— check your account tier (Free/Pro), masked email, scans synced, and last sync time without running a scan. --cloudnow works in every mode — previously it silently skipped the upload when no--specwas given (the guide's own--dir ./src --cloudexample never synced) and in--format json|markdownscan-only mode. Uploads now run on all paths.- Friendlier tier errors — a 402 from
--cloudprints a clear "cloud sync is a Pro feature, your local scan completed fine" message with the upgrade link instead of a raw API error. - Bare
--cloud-login— running it without a token prints where to get one (sign in → copy token); empty/whitespace tokens are rejected. - Cleaner machine-readable output —
--cloudno longer prints[INFO] Uploading…after JSON/SARIF output (broke| jq); malformed dashboard responses fail loudly instead of fabricating a FREE account. - Default cloud URL corrected — the CLI now talks to
shadowaudit-dashboard.vercel.appdirectly (the old default pointed at a.devdomain that was never ours and has since been registered by an unrelated project — do not use it).
Scanner fixes (found by 5-agent adversarial QA):
- Grape support is now real — the Grape scanner shipped in v1.x but was never wired into auto-detection or
--frameworkdispatch (unreachable dead code). Now:--framework grapeworks and Gemfile/lib/api/detection returns it automatically. router.use(requireAuth)detected — the very common Express idiom was invisible to upstream-auth detection; now marks subsequent routes[upstream-auth].- Optional-auth denylist extended —
maybeAuth,ifAuthenticated,optionalSessionand friends no longer falsely mark routes as[auth](these hid unauthenticated endpoints). validateBearer-style middleware detected —beareradded to auth-name substrings.
CLI contract fixes:
- Exit-code contract enforced — invalid
--fail-on/--format/--concurrencyvalues now exit 2 with a clear error (previously--fail-on mediumprinted "pipeline will FAIL" but exited 0). --direxistence validated — a typo'd path previously "scanned" nothing and exited 0, silently passing CI.--coverage --format jsonfixed — the JSON went to stderr, leaving stdout empty for| jq.--coveragewithout--spec— now a clean exit-2 error (was a silent no-op).--watchwith relative../paths fixed — paths containing..were treated as hidden and all file changes silently ignored.--abomcomposes with all modes — previously--abom+--reverse/--coverage/--generate-specnever wrote the manifest;--abomwithout--specproduced phantom CRITICALs and aSpec file not found: undefinedwarning; unwritable--abom-outputdumped a stack trace. All fixed.- ABOM risk scores are method-aware —
GET /xwas marked documented (risk understated) whenever any method of/xwas in the spec. Documented status now matches METHOD+PATH.
v1.8.0 — 3 false-positive fixes (array spread, Vue composables, CLI commands)
Found via adversarial QA scanning real-world repos (NodeBB, Directus, Strapi, KeystoneJS):
- Array spread middleware detection —
[...middlewares]in route args now resolves the variable's array declaration and checks elements for auth indicators. Fixed 66 false positives on NodeBB's ActivityPub routes. - Vue.js composable exclusion —
use-*.tsfiles are now excluded (Vue frontend composables, not Express server routes). Fixed 29 false positives on Directus. - CLI command + examples exclusion —
cli/commands/andexamples/directories are now excluded. Fixed 20 false positives on Strapi, 3 on KeystoneJS.
| Repo | Before | After | Reduction | |------|:------:|:-----:|:---------:| | NodeBB | 66 [no auth] | 49 | -17 (26%) | | Directus | 30 routes | 1 | -29 | | Strapi | 28 routes | 1 | -27 | | KeystoneJS | 7 routes | 1 | -6 |
v1.7.0 — False-positive reduction: 3 new auth detection patterns
Cuts false positives by detecting 3 auth enforcement patterns the basic
hasAuth check missed. Routes matching these patterns are downgraded from
CRITICAL → INFO and no longer inflate the risk score.
[env-only]— routes mounted insideif (NODE_ENV === 'test')blocks. Detected via AST parent-chain walk (JS/TS) + indentation analysis (Python).[controller-auth]— auth checked inside the handler body (if (!req.user) return res.status(401),Depends(get_current_user), etc.)[upstream-auth]— auth middleware applied globally viaapp.use(authenticate)before the router is mounted in the same file.
Auth column now shows 5 statuses (was 2):
[auth] green · [upstream-auth] blue · [controller-auth] blue · [env-only] yellow · [no auth] red
Only [no auth] routes count as CRITICAL in the risk score — the other 3
statuses are INFO, dramatically reducing false-positive risk scores.
20 new tests (515 total, all passing).
v1.6.0 — OWASP mapping, Risk Score, .shadowauditignore
- OWASP API Security Top 10 (2023) mapping — every finding now shows its OWASP category inline (API1: BOLA, API5: BFLA, API9: Shadow APIs, etc.). Drop straight into security reports without manual lookup.
- Risk Score (0-100) — every scan ends with a single risk score in the
summary box. Formula:
100 − (critical×15) − (high×7) − (info×1), capped at 0. Bands: CRITICAL (0-30), HIGH (31-60), MEDIUM (61-80), LOW (81-100). Color-coded — instantly see if your API surface is improving or degrading. .shadowauditignoresupport — like.gitignorebut for routes. Place in your scan directory to exclude intentional routes (health checks, metrics, debug endpoints) from findings. Supports glob patterns +!negation. Risk score is recalculated after filtering.
┌──────────┬────────────────┬────────┬──────────────────────────────┬──────┬──────┐
│ SEVERITY │ OWASP │ METHOD │ PATH │ AUTH │
├──────────┼────────────────┼────────┼──────────────────────────────┼──────┼──────┤
│ CRITICAL │ API5 (BFLA) │ POST │ /admin/delete │ NO │
│ CRITICAL │ API1 (BOLA) │ GET │ /api/internal/metrics │ NO │
└──────────┴────────────────┴────────┴──────────────────────────────┴──────┴──────┘
┌─────────────────────────────────────────────┐
│ SCAN SUMMARY │
│ Risk Score : 70/100 │
│ Risk Band : MEDIUM RISK │
└─────────────────────────────────────────────┘
── OWASP API Security Top 10 (2023) mappings in this scan ──
API1: Broken Object Level Authorization (BOLA)
API5: Broken Function Level Authorization (BFLA)v1.5.0 — Post-scan CTA + --share + GitHub Actions annotations
- Post-scan CTA block: after every scan (without --cloud), prints a branded block with the dashboard URL and badge markdown so users know where to view results
- Token persistence:
--cloudnow saves the token to~/.shadowaudit/tokenso future non-cloud scans show the correct dashboard URL --shareflag: creates a shareable read-only report link (expires in 7 days) by uploading results to/api/share- GitHub Actions annotations: when
GITHUB_ACTIONS=true, emits::errorannotations for CRITICAL findings and::warningfor HIGH findings — visible in PR diffs - GitHub Actions step summary: when
GITHUB_STEP_SUMMARYis set, appends a markdown table of critical/high findings to the run summary page - 69 new tests (495 total, all passing)
v1.3.0 — Framework validation & auth.optional fix
- Framework validation —
--framework springnow works as alias forspringboot. Invalid framework names exit with code 2. - auth.optional fix — Express scanner no longer treats
auth.optional,authOptional,optionalAuthas auth (they mean auth is OPTIONAL, not required) - Spring Boot @ResponseBody fix — Methods with
@ResponseBodyannotation now extract correct method name (was showingunknown()) - 11 new regression tests (426 total)
v1.2.0 — ABOM & Composite Risk Scoring
- ABOM (API Bill of Materials) — generate a machine-readable inventory of every route in your app (the API equivalent of SBOM). Includes auth status, spec coverage, and per-route risk scores. See the ABOM section below.
- Composite Risk Scoring — every route now gets a
0-100risk score blending 7 factors: documentation gap, auth gap, exposure, method danger, path sensitivity, parameter complexity, and historical severity. - New flags:
--abom,--abom-sign(ed25519),--abom-output <path>. - ABOM diff engine — compare two ABOMs to surface routes added, removed, or whose risk score changed between commits/releases.
v1.1.0 — Spring Boot scanner & adversarial QA
- Spring Boot scanner — the 8th supported framework. Java parser for
@RestController/@Controllerwith all@*Mappingshortcut annotations, class-level@RequestMappingbase path prefixes, and Spring Security auth via@PreAuthorize/@PostAuthorize/@Secured/@RolesAllowedwith class-level inheritance. Auto-detected viapom.xmlorbuild.gradle. --cloudflag — upload scan results to the shadowaudit cloud dashboard for cross-repo visibility.- Brutal adversarial QA — 27 new tests targeting 13 known weakness categories, run against the Spring PetClinic reference app.
- 0% false-positive rate verified on Spring PetClinic.
v1.0.5 — Mastodon-scale false-positive reduction
- 98.2% false-positive reduction on the Mastodon codebase (766 routes) via concern expansion + controller inheritance chain walking.
- 9 new auth patterns added (Mastodon's
require_moderator_or_admin_permissions, Discourse'srequires_login, and others). - New
--allowlistflag — JSON file of public route patterns that downgradeCRITICAL → INFO. - New scan-only mode — run without a spec to get a full route inventory.
Terminal Preview
shadowaudit ships with a colored, table-formatted terminal output. No screenshots needed — the ASCII previews below render on npm, GitHub, and in IDE markdown previews.
Basic scan (table format)
[INFO] shadowaudit v1.3.0
[INFO] Directory : ./src
[INFO] Spec file : ./openapi.json
[INFO] Framework : express
[INFO] Running Express.js route scanner...
[✓] Found 7 routes in ./src
[INFO] Detected spec: OpenAPI 3.x
[INFO] Spec contains 5 documented routes
╔══════════════════════════════════════════════╗
║ shadowaudit — Scan Report ║
╚══════════════════════════════════════════════╝
┌──────────┬────────┬───────────────────────────────────┬──────────────────┬──────┬──────┐
│ SEVERITY │ METHOD │ PATH │ FILE │ LINE │ AUTH │
├──────────┼────────┼───────────────────────────────────┼──────────────────┼──────┼──────┤
│ CRITICAL │ GET │ /api/debug/reset │ routes.js │ 21 │ NO │
├──────────┼────────┼───────────────────────────────────┼──────────────────┼──────┼──────┤
│ CRITICAL │ POST │ /api/test/seed │ routes.js │ 22 │ NO │
└──────────┴────────┴───────────────────────────────────┴──────────────────┴──────┴──────┘
┌─────────────────────────────────────┐
│ SCAN SUMMARY │
│ Total routes scanned : 7 │
│ Documented : 5 │
│ Undocumented : 2 │
│ 🔴 CRITICAL : 2 │
│ 🟡 HIGH : 0 │
│ 🔵 INFO : 0 │
└─────────────────────────────────────┘
⛔ Pipeline will FAIL — 2 critical shadow route(s) detectedABOM summary (v1.2.0)
[INFO] Generating ABOM (abom-1.0)...
[✓] ABOM written to ./abom.json
[✓] ABOM signed with ed25519 (key id: 7f3a…c2e1)
╔════════════════════════════════════════════════════════════╗
║ shadowaudit — ABOM Summary ║
╚════════════════════════════════════════════════════════════╝
┌──────────────────────────────────┬─────────────────────────┐
│ Total routes │ 419 │
│ Documented in spec │ 287 (68.5%) │
│ Undocumented │ 132 (31.5%) │
│ Routes with auth │ 398 (95.0%) │
│ Routes without auth │ 21 (5.0%) │
│ Avg risk score │ 34.2 / 100 │
│ Routes scoring ≥ 70 (high risk) │ 17 │
└──────────────────────────────────┴─────────────────────────┘
Top 5 highest-risk routes:
┌────────┬────────────────────────────────┬──────┬────────────┐
│ METHOD │ PATH │ AUTH │ RISK │
├────────┼────────────────────────────────┼──────┼────────────┤
│ DELETE │ /api/admin/users/{id} │ NO │ 92 🔴 │
│ POST │ /api/internal/import │ NO │ 87 🔴 │
│ GET │ /api/debug/dump │ NO │ 84 🔴 │
│ PUT │ /api/v2/settings │ YES │ 61 🟡 │
│ GET │ /api/users/export.csv │ YES │ 54 🟡 │
└────────┴────────────────────────────────┴──────┴────────────┘Installation
Global CLI (recommended)
npm install -g shadowauditnpx (no install needed)
npx shadowaudit --dir ./src --spec ./openapi.jsonAuto-generate an OpenAPI spec (new!)
Don't have a spec yet? Generate one from your code:
shadowaudit --dir ./src --framework express --generate-spec > openapi.jsonThen review the generated spec, fill in response schemas, and use it for delta comparison.
GitHub Action
Add to .github/workflows/security.yml:
- uses: darkmaster0345/[email protected]
with:
dir: './src'
spec: './openapi.json'
fail-on: 'critical'Real-World Testing
shadowaudit has been tested against real production codebases to validate accuracy and guide the roadmap:
TryGhost/Ghost — production publishing platform (Express.js)
- 269 routes detected across admin API, content API, members API, and webhooks
- 235 routes correctly authenticated (v0.3.0 AST-based detection catches custom
authAdminApimiddleware) - 0
config.get()false positives (v0.3.0 object-name filtering eliminates these) - 34 routes legitimately public (session, authentication, setup, webhooks — verified manually)
tiangolo/fastapi — FastAPI framework docs (FastAPI)
- 419 routes detected across documentation source examples
- All route patterns correctly parsed:
@app.get(),@router.post(), path parameters,Depends()auth - Auto-spec generation tested:
--generate-specproduces valid OpenAPI 3.0.3 from scanned routes
expressjs/express — Express framework examples (Express.js)
- 58 routes detected across 15+ example applications
- Mount prefix reconciliation working:
USE /api/v1andUSE /api/v2correctly identified - Auth detection working:
POST /logincorrectly flagged as[auth]
pallets/flask — Flask framework source (Flask)
- 3 internal routes detected in Flask's own source code
- Flask
<param>syntax correctly converted to{param} - Blueprint routes (
@bp.route()) detected
hagopj13/node-express-boilerplate (Express.js)
- 18 routes detected across auth, user, and docs modules
- v0.2.1
auth(pattern fix correctly detected auth on 6 user routes that v0.2.0 missed - Mount prefix reconciliation working for
app.use('/prefix', require('./router'))pattern
mastodon/mastodon — federated social network (Rails + Grape)
- 766 routes detected (743 Rails + 23 from concern expansion)
- 0
unknown#unknowncontroller actions (v0.9.0 concern expansion eliminated all 5 false positives from v0.8.2) - Controller inheritance chain walking correctly detected auth in 385 routes (v0.8.0 had 96 — 57% false-positive reduction)
- Community OpenAPI spec comparison: 601 undocumented shadow routes found (the community spec only covers the public REST API, not web/admin/settings routes)
gitlabhq/gitlabhq — DevSecOps platform (Rails + Grape)
- 2,449 routes detected (1,423 Rails web routes + 1,017 Grape API routes + 9 other)
- 1,017
/api/v4/routes from Grapelib/api/*.rbfiles (v0.9.0 Grape scanner) - 813 routes with auth detected via Grape
before do authenticate! endblocks - Mount chain resolution working:
mount ::API::Groups→lib/api/groups.rbwith/api/v4prefix - GitLab's OpenAPI spec has 1,144 paths — 2,136 undocumented shadow routes found
These real-world tests directly shaped the roadmap — each limitation found is now a tracked improvement with priority and effort ratings.
The Problem
Developers spin up quick test endpoints and forget to remove them. These shadow APIs bypass documentation, skip auth middleware, and get deployed to production silently — invisible to network scanners, invisible to your security team.
shadowaudit solves this by statically analyzing your codebase, comparing every route definition against your OpenAPI/Swagger spec, and failing your CI pipeline before the PR merges if it finds undocumented or unauthenticated routes.
What shadowaudit does
- Scans your codebase for all actual route definitions (Express.js, FastAPI, and Django supported)
- Compares them against your OpenAPI 3.x / Swagger 2.0 spec
- Flags any route missing from the documentation
- Detects whether auth middleware is applied to each route
- Fails your CI/CD pipeline before the PR merges — with exit codes that respect configurable severity thresholds
Severity levels
| Severity | Condition | CI behavior (--fail-on critical) |
|----------|-----------|-------------------------------------|
| CRITICAL | Undocumented route + no auth middleware | Pipeline fails |
| HIGH | Undocumented route + auth middleware present | Pipeline passes (review recommended) |
| INFO | Informational finding | Pipeline passes |
Supported Frameworks
- Express.js ✅ (AST-based route extraction via Babel, mount prefix reconciliation, 3-level deep router chain resolution, auth detection)
- FastAPI ✅ (decorator-based route extraction,
Depends()auth detection,include_routerprefix reconciliation) - Django ✅ (
path()/re_path()/url()extraction, DRF router support, class-based views (ModelViewSet, APIView, generics),include()prefix reconciliation, cross-file auth viaviews.py) - Flask ✅ (
@app.route()/@bp.route()decorator extraction,methods=multi-method (list + tuple syntax), Blueprinturl_prefixsupport,@login_requiredauth detection) - NestJS ✅ (
@Controller()+@Get()/@Post()decorator extraction,@UseGuards()auth detection,@Public()override,@Version()API versioning, controller prefix + method path combination) - Rails ✅ (v0.8.0+ —
config/routes.rbblock-aware parser:namespace,scope,resources/resourcemacros,member/collectionblocks,devise_for,draw(:subfile),concernexpansion,with_options,controller:modifier, cross-file auth viabefore_actionwith controller inheritance chain walking + module-nested parent class resolution, 25+ auth patterns including Mastodon'srequire_moderator_or_admin_permissionsand Discourse'srequires_login) - Grape ✅ (v0.9.0+ —
lib/api/*.rbGrape API scanner:resource,namespace,route_param,beforeauth blocks,mountcross-file resolution,prefix/versionsupport. Auto-invoked whenlib/api/exists. Tested on GitLab: 1,017/api/v4/routes detected) - Spring Boot ✅ (v1.1.0+ — Java parser for
@RestController/@Controllerclasses:@GetMapping/@PostMapping/@PutMapping/@DeleteMapping/@PatchMappingshortcut annotations,@RequestMappingwithmethod=parameter, class-level@RequestMappingbase path prefix, Spring Security auth detection via@PreAuthorize/@PostAuthorize/@Secured/@RolesAllowedwith class-level inheritance. Auto-detected viapom.xmlorbuild.gradle.)
CLI Options
Usage: shadowaudit [options]
Static API security scanner for undocumented routes
Options:
-V, --version output the version number
--dir <path> Directory to scan (default: current directory)
--spec <path> Path to OpenAPI/Swagger spec file
--format <type> Output format: table, json, sarif, markdown (default: "table")
--fail-on <level> Fail CI pipeline on: critical, high, info (default: "critical")
--framework <name> Force framework: express, fastapi, django, flask, nestjs, rails, springboot (alias: spring) (Grape auto-detected) (default: "auto")
--generate-spec Generate OpenAPI spec from scanned routes (no comparison)
--diff Show only new findings since last scan (requires --spec)
--concurrency <n> Parallel file scan concurrency (default: 8, max: 64)
--watch Watch for file changes and re-scan automatically (debounced 300ms, Ctrl+C to exit)
--reverse Find dead spec entries (routes in spec NOT in code)
--coverage Score OpenAPI spec completeness (descriptions, params, responses, security)
--ignore-paths <paths> Comma-separated paths to ignore (exact, /prefix/*, or * for all)
--allowlist <file> JSON file with public route patterns (downgrades CRITICAL → INFO)
--abom Generate an API Bill of Materials (abom-1.0) alongside scan results
--abom-sign Sign the ABOM with ed25519 (requires ABOM_SIGNING_KEY env var)
--abom-output <path> Write ABOM JSON to file (default: ./abom.json)
--abom-diff <a> <b> Diff two ABOM files (added / removed / risk-changed routes)
--cloud Upload scan results to the shadowaudit cloud dashboard (Pro)
--cloud-login [token] Save your dashboard API token (bare flag shows where to get one)
--status Show your cloud account tier (Free/Pro) and sync status
-h, --help display help for commandExit codes
| Code | Meaning |
|------|---------|
| 0 | Clean scan — no findings, or findings below --fail-on threshold |
| 1 | Findings found at or above --fail-on threshold |
| 2 | Tool error (spec file missing/invalid, framework not detected, etc.) |
API Bill of Materials (ABOM)
New in v1.2.0. The ABOM is the API equivalent of an SBOM (Software Bill of Materials) — a machine-readable inventory of every route your application exposes, including auth status, spec coverage, and a per-route composite risk score. Ship it alongside your build artifacts so downstream teams (security, SRE, compliance) can audit your attack surface without re-running the scanner.
Generating an ABOM
shadowaudit --dir ./src --spec ./openapi.json --abomWrites ./abom.json (override the path with --abom-output <path>). The normal scan still runs — ABOM is additive and does not change exit-code behavior.
Signing the ABOM (ed25519)
export ABOM_SIGNING_KEY=$(cat ./ed25519-private.key)
shadowaudit --dir ./src --spec ./openapi.json --abom --abom-signA signed ABOM includes a signature block that consumers verify with the corresponding public key before trusting the inventory:
{
"signature": {
"algorithm": "ed25519",
"publicKey": "7f3a...c2e1",
"value": "3044...8aaf"
}
}Composite Risk Scoring (0-100)
Every route in the ABOM gets a riskScore from 0 (safe) to 100 (critical). The score is a weighted blend of seven factors:
| Factor | Weight | Description |
|-------------------------|--------|-------------|
| Documentation gap | 25 | Route is missing from the OpenAPI spec |
| Auth gap | 25 | No auth middleware detected on the route |
| Exposure | 15 | Route is reachable from a public-facing base path |
| Method danger | 15 | Mutating methods (DELETE, PUT, POST) score higher than GET |
| Path sensitivity | 10 | Path contains keywords like admin, debug, internal, config, users |
| Parameter complexity | 5 | Path contains {id}-style params (injection / IDOR surface) |
| Historical severity | 5 | Boost if similar paths have triggered findings in past scans |
| Score | Severity | Meaning | |------------|---------------------|--------------------------------------| | 70-100 | 🔴 CRITICAL | Fix immediately — likely exploitable | | 40-69 | 🟡 HIGH | Review before merge | | 0-39 | 🟢 LOW | Informational |
ABOM diff engine
Compare two ABOMs to surface what changed between commits, releases, or branches:
shadowaudit --abom-diff ./abom-main.json ./abom-feature.jsonOutput highlights routes added, removed, or whose risk score changed by ≥ 5 points — perfect for PR review. Pairs naturally with --diff mode so the scan itself only reports new findings while the ABOM diff shows full route-level churn.
Example ABOM output
The terminal summary printed after a --abom run is shown in the Terminal Preview above. The full machine-readable artifact is the abom.json file described by the schema below.
ABOM JSON schema (abom-1.0)
{
"schema": "abom-1.0",
"generatedAt": "2026-02-10T14:23:01.000Z",
"tool": "shadowaudit",
"toolVersion": "1.3.0",
"project": {
"dir": "./src",
"spec": "./openapi.json",
"framework": "express"
},
"summary": {
"totalRoutes": 419,
"documented": 287,
"undocumented": 132,
"withAuth": 398,
"withoutAuth": 21,
"averageRiskScore": 34.2,
"highRiskCount": 17
},
"routes": [
{
"method": "DELETE",
"path": "/api/admin/users/{id}",
"file": "routes/admin.js",
"line": 142,
"hasAuth": false,
"documented": false,
"riskScore": 92,
"riskFactors": [
"documentation-gap",
"auth-gap",
"path-sensitivity",
"method-danger"
],
"severity": "CRITICAL"
}
],
"signature": {
"algorithm": "ed25519",
"publicKey": "7f3a...c2e1",
"value": "3044...8aaf"
}
}Quick Start
# Scan your codebase against your OpenAPI spec
shadowaudit --dir ./src --spec ./openapi.json
# Force a specific framework
shadowaudit --dir ./src --spec ./openapi.json --framework express
# Only show NEW findings since last scan (perfect for PRs)
shadowaudit --dir ./src --spec ./openapi.json --diff
# Don't have a spec? Generate one from your code
shadowaudit --dir ./src --framework express --generate-spec > openapi.json
# Output as JSON (pipe to jq for CI integrations)
shadowaudit --dir ./src --spec ./openapi.json --format json | jq '.findings | length'
# Output as SARIF 2.1.0 (for GitHub Code Scanning)
shadowaudit --dir ./src --spec ./openapi.json --format sarif > results.sarif--diff mode (v0.6.0+)
Show only new findings since the last scan — perfect for PR workflows where you don't want to see the same 50 pre-existing findings on every PR.
# First run — full scan, cache saved
shadowaudit --dir ./src --spec ./openapi.json --diff
# Second run — only NEW findings shown
shadowaudit --dir ./src --spec ./openapi.json --diff- Cache stored at
.shadowaudit-cache.json(auto-added to.gitignore) - Resolved findings shown as green ✓ section
- Exit code based on NEW findings only (pre-existing findings don't fail CI)
- For GitHub Actions: use
actions/cacheto persist.shadowaudit-cache.jsonbetween runs
--watch mode (v0.7.0+)
Re-scan automatically whenever you save a file — perfect for dev-time feedback while you're writing new routes.
shadowaudit --dir ./src --spec ./openapi.json --watch- Debounced 300ms — bursty saves (editor "save all", formatter runs) trigger one re-scan, not five
- Ctrl+C exits cleanly with exit code 0 (no orphaned processes)
- Works with
--diffto show only NEW findings per save - Only watches
.js,.ts,.jsx,.tsx,.pyfiles; ignoresnode_modules,dist,__pycache__,.git - Cannot be combined with
--generate-specor--format json/sarif(table only — JSON isn't streamable)
--concurrency <n> (v0.7.0+)
Control how many files are read in parallel during a scan. Defaults to 8, clamped to [1, 64].
# Use higher concurrency on a beefy CI box
shadowaudit --dir ./src --spec ./openapi.json --concurrency 16
# Force sequential (same as v0.6.1 behavior)
shadowaudit --dir ./src --spec ./openapi.json --concurrency 1- Output is deterministic regardless of concurrency (verified by adversarial QA —
concurrency=1andconcurrency=64produce byte-identical route arrays) - Invalid values (0, negative, non-numeric) degrade gracefully to the default
- 3-5× speedup on large codebases (Ghost CMS: 4s → ~1s)
--generate-spec (v0.4.0+)
Don't have an OpenAPI spec? Generate one from your code:
shadowaudit --dir ./src --framework express --generate-spec > openapi.jsonConfiguration
shadowaudit loads config from two sources (CLI flags take priority):
- CLI flags (highest priority)
.shadowaudit.ymlor.shadowauditrc.ymlin your project root
📖 Full configuration documentation → — all fields, examples, and troubleshooting.
Example .shadowaudit.yml:
spec: ./openapi.json
dir: ./src
format: table
failOn: critical
authPatterns:
- myCustomAuth
- requireLogin
ignore:
- node_modules
- dist
- buildDefault auth patterns (always detected)
Even without custom config, shadowaudit detects these auth middleware patterns:
.authenticate( .authorize( requireAuth isAuthenticated
verifyToken checkAuth ensureLoggedIn passport.authenticate
jwt.verify bearerAuth apiKeyAuth basicAuthOutput Formats
Table (default)
Human-readable colored table with severity, method, path, file, line, and auth status. Includes a summary box and pipeline recommendation.
JSON
Structured JSON for CI dashboards and log aggregators:
{
"scanMeta": {
"tool": "shadowaudit",
"version": "1.3.0",
"timestamp": "2026-01-15T12:00:00.000Z",
"stats": { "total": 7, "documented": 5, "undocumented": 2, "critical": 2, "high": 0, "info": 0 }
},
"findings": [
{
"severity": "CRITICAL",
"method": "GET",
"path": "/api/debug/reset",
"file": "routes.js",
"line": 21,
"hasAuth": false,
"reason": "Undocumented route with no authentication middleware detected — publicly accessible shadow API"
}
],
"documentedRoutes": [...]
}SARIF 2.1.0
Industry-standard format for GitHub Code Scanning, Azure DevOps, and other security dashboards. Maps findings to two rules:
| Rule ID | Severity | Level |
|---------|----------|-------|
| SHADOW001 | CRITICAL | error |
| SHADOW002 | HIGH | warning |
| SHADOW002 | INFO | note |
Demo
Below is the actual terminal output from an end-to-end scan. The test project has 7 Express routes — 5 documented in the OpenAPI spec, 2 undocumented shadow routes with no auth middleware:
[INFO] No config file found, using defaults
[INFO] shadowaudit v1.3.0
[INFO] Directory : /tmp/auth-test
[INFO] Spec file : /tmp/auth-test/openapi.json
[INFO] Format : table
[INFO] Fail on : critical
[INFO] Framework : express
[INFO] Running Express.js route scanner...
[✓] Found 7 routes in /tmp/auth-test
[INFO] GET /public/health → routes.js:6 [auth]
[INFO] GET /public/products → routes.js:7 [auth]
[INFO] GET /api/users → routes.js:9 [auth]
[INFO] POST /api/users → routes.js:13 [auth]
[INFO] GET /api/admin/dashboard → routes.js:17 [auth]
[INFO] GET /api/debug/reset → routes.js:21 [no auth]
[INFO] POST /api/test/seed → routes.js:22 [no auth]
[INFO] Detected spec: OpenAPI 3.x
[INFO] Spec contains 5 documented routes
[INFO] ──────────────────────────────────────────────────
[INFO] Total routes scanned : 7
[INFO] Documented : 5
[INFO] Undocumented : 2
[INFO] ──────────────────────────────────────────────────
╔══════════════════════════════════════════════╗
║ shadowaudit — Scan Report ║
╚══════════════════════════════════════════════╝
┌──────────┬────────┬───────────────────────────────────┬───────────────────────────────────┬──────┬──────┐
│ SEVERITY │ METHOD │ PATH │ FILE │ LINE │ AUTH │
├──────────┼────────┼───────────────────────────────────┼───────────────────────────────────┼──────┼──────┤
│ CRITICAL │ GET │ /api/debug/reset │ routes.js │ 21 │ NO │
├──────────┼────────┼───────────────────────────────────┼───────────────────────────────────┼──────┼──────┤
│ CRITICAL │ POST │ /api/test/seed │ routes.js │ 22 │ NO │
└──────────┴────────┴───────────────────────────────────┴───────────────────────────────────┴──────┴──────┘
┌─────────────────────────────────────────────┐
│ SCAN SUMMARY │
│ Total routes scanned : 7 │
│ Documented : 5 │
│ Undocumented : 2 │
│ ───────────────────────────────────────── │
│ 🔴 CRITICAL : 2 │
│ 🟡 HIGH : 0 │
│ 🔵 INFO : 0 │
└─────────────────────────────────────────────┘
⛔ Pipeline will FAIL — 2 critical shadow route(s) detected
Exit code: 1In the terminal, severity cells are color-coded:
- CRITICAL → red bold
- HIGH → yellow bold
- INFO → blue
- METHOD → cyan
- AUTH
YES→ green /NO→ red
How It Works
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Express Scanner │ │ Spec Parser │ │ Delta Comparator│ │ Formatter │
│ (Babel AST) │ │ (OpenAPI/Swagger)│ │ (normalize + )│ │ (table/json/ )│
│ │ │ │ │ ( match routes )│ │ (sarif) )│
│ src/**/*.js │────▶│ openapi.json │────▶│ Route[] vs │────▶│ Findings[] │
│ src/**/*.ts │ │ swagger.yaml │ │ DocumentedRoute[]│ │ + stats │
│ │ │ │ │ │ │ │
│ + auth detector │ │ + basePath │ │ + severity │ │ + exit code │
│ (window scan) │ │ + serverPrefix │ │ scoring │ │ logic │
└──────────────────┘ └──────────────────┘ └──────────────────┘ └──────────────────┘- Express scanner uses
@babel/parser+@babel/traverseto walk every.js/.tsfile's AST and extract route definitions (app.get(),router.post(),router.route().get().post()chaining, etc.) - Auth detector scans a ±15-line window around each route for auth middleware patterns (inline or in surrounding scope), with sibling-route lines stripped to avoid false positives
- Spec parser reads OpenAPI 3.x / Swagger 2.0 files (JSON or YAML), extracts documented routes, and normalizes path syntax (
{id}→:id) - Delta comparator cross-references scanned routes against documented routes, reconciling Swagger
basePathand OpenAPIservers[0].urlprefixes, then scores each undocumented route as CRITICAL (no auth) or HIGH (has auth) - Formatter renders the report as a colored table, JSON, or SARIF 2.1.0
CI/CD Integration
GitHub Actions
name: Security Scan
on: [pull_request]
jobs:
shadowaudit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npx shadowaudit --dir ./src --spec ./openapi.json --format sarif > shadowaudit.sarif
continue-on-error: true
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: shadowaudit.sarif
- name: Fail on critical
run: npx shadowaudit --dir ./src --spec ./openapi.json --fail-on criticalGitLab CI
# .gitlab-ci.yml
shadowaudit:
image: node:20
script:
- npm ci
- npx shadowaudit --dir ./src --spec ./openapi.json --format json > shadowaudit.json
- npx shadowaudit --dir ./src --spec ./openapi.json --fail-on critical
artifacts:
paths:
- shadowaudit.json
when: alwaysPre-commit hook
#!/bin/bash
shadowaudit --dir ./src --spec ./openapi.json --fail-on criticalProject Structure
shadowaudit/
├── src/
│ ├── index.ts # CLI entry point
│ ├── action.ts # GitHub Action entry point
│ ├── config.ts # cosmiconfig loader (CLI flags + .shadowaudit.yml)
│ ├── comparator.ts # Delta comparator (route diffing + severity scoring)
│ ├── diff.ts # --diff mode (local cache + new/resolved detection)
│ ├── types.ts # Shared TypeScript interfaces
│ ├── scanners/
│ │ ├── express.ts # Express.js route extractor (Babel AST + mount prefixes)
│ │ ├── fastapi.ts # FastAPI route extractor (decorators + include_router)
│ │ ├── django.ts # Django route extractor (path() + CBV + cross-file auth)
│ │ ├── flask.ts # Flask route extractor (@app.route + methods=)
│ │ ├── nestjs.ts # NestJS route extractor (@Controller + @Get decorators)
│ │ ├── rails.ts # Rails routes.rb parser (namespace, resources, concerns)
│ │ ├── grape.ts # Grape API scanner (lib/api/*.rb)
│ │ ├── springboot.ts # Spring Boot scanner (@RestController + @GetMapping)
│ │ └── auth.ts # Auth middleware detector (AST + string-based)
│ ├── parsers/
│ │ └── spec.ts # OpenAPI 3.x / Swagger 2.0 parser
│ ├── formatters/
│ │ ├── table.ts # Colored terminal table
│ │ ├── json.ts # JSON output
│ │ ├── sarif.ts # SARIF 2.1.0 output
│ │ └── index.ts # Formatter dispatcher
│ ├── generators/
│ │ └── spec.ts # Auto-spec generation (--generate-spec)
│ ├── github/
│ │ └── comment.ts # PR comment bot
│ └── utils/
│ └── logger.ts # Colored console output
├── tests/ # 426 tests across 27 files
│ ├── config.test.ts
│ ├── comparator.test.ts
│ ├── diff.test.ts
│ ├── hardening.test.ts
│ ├── placeholder.test.ts
│ ├── v1-features.test.ts
│ ├── watch.test.ts
│ ├── scanners/
│ │ ├── express.test.ts
│ │ ├── fastapi.test.ts
│ │ ├── django.test.ts
│ │ ├── django-cbv.test.ts
│ │ ├── flask.test.ts
│ │ ├── nestjs.test.ts
│ │ ├── rails.test.ts
│ │ ├── rails-concerns.test.ts
│ │ ├── rails-resource.test.ts
│ │ ├── grape.test.ts
│ │ ├── springboot.test.ts
│ │ ├── auth.test.ts
│ │ └── parallel.test.ts
│ ├── parsers/
│ │ └── spec.test.ts
│ ├── generators/
│ │ └── spec.test.ts
│ ├── formatters/
│ │ ├── table.test.ts
│ │ └── sarif.test.ts
│ └── github/
│ └── comment.test.ts
├── docs-site/ # Docusaurus v3 documentation site
├── scripts/ # Migration + deployment helpers
│ ├── prepublish-check.ts
│ ├── gitlab-migrate.sh
│ └── docs-deploy.sh
├── action.yml # GitHub Marketplace Action definition
├── ROADMAP.md # v1.2.0 → v1.3.0+ roadmap
├── CONTRIBUTING.md # How to add framework scanners
├── package.json
├── tsconfig.json
├── LICENSE
└── README.mdDevelopment
# Install dependencies
npm install
# Build
npm run build
# Run tests (426 tests across 27 test files)
npm test
# Run in dev mode
npm run dev -- --dir ./src --spec ./openapi.json
# Lint
npm run lintTech Stack
- TypeScript — strict mode, ES2020 target, CommonJS modules
- Babel (
@babel/parser+@babel/traverse) — AST-based route extraction with error recovery - Commander.js — CLI argument parsing
- chalk — terminal colors
- cli-table3 — terminal table rendering
- js-yaml — Swagger/OpenAPI YAML parsing
- cosmiconfig — config file discovery
- glob — recursive file scanning
- Vitest — test runner
Privacy & Network Access
shadowaudit runs entirely locally — it reads your source files and OpenAPI spec on disk. No code is uploaded, no telemetry is sent, no phone-home.
The only network call is when using the GitHub Action's PR comment bot (post-comment: 'true'), which posts the findings summary to the GitHub API (api.github.com) using github.token. This is the standard GitHub Actions pattern and uses your repo's own token — no external servers are contacted.
Socket.dev audit: ✅ Vulnerability 100/100 · ✅ Quality 100/100 · ✅ License 100/100
License
MIT © Ubaid ur Rehman 2026
