shadowaudit
Static API security scanner — finds undocumented and unauthenticated routes before they reach production

🌐 Documentation & Demo → · 🛒 GitHub Marketplace →
Installation
Global CLI (recommended)
npm install -g shadowaudit
npx (no install needed)
npx shadowaudit --dir ./src --spec ./openapi.json
Auto-generate an OpenAPI spec (new!)
Don't have a spec yet? Generate one from your code:
shadowaudit --dir ./src --framework express --generate-spec > openapi.json
Then review the generated spec, fill in response schemas, and use it for delta comparison.
GitHub Action
Add to .github/workflows/security.yml:
- uses: darkmaster0345/shadow-Audit@v1.0.0
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
authAdminApi middleware)
- 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-spec produces 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/v1 and USE /api/v2 correctly identified
- Auth detection working:
POST /login correctly 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#unknown controller 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 Grape lib/api/*.rb files (v0.9.0 Grape scanner)
- 813 routes with auth detected via Grape
before do authenticate! end blocks
- Mount chain resolution working:
mount ::API::Groups → lib/api/groups.rb with /api/v4 prefix
- 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
| 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, auth detection)
- FastAPI ✅ (decorator-based route extraction,
Depends() auth detection, include_router prefix reconciliation)
- Django ✅ (
path()/re_path()/url() extraction, DRF router support, class-based views (ModelViewSet, APIView, generics), include() prefix reconciliation, cross-file auth via views.py)
- Flask ✅ (
@app.route() / @bp.route() decorator extraction, methods= multi-method (list + tuple syntax), @login_required auth detection)
- NestJS ✅ (
@Controller() + @Get() / @Post() decorator extraction, @UseGuards() auth detection, @Public() override, controller prefix + method path combination)
- Rails ✅ (v0.8.0+ —
config/routes.rb block-aware parser: namespace, scope, resources/resource macros, member/collection blocks, devise_for, draw(:subfile), cross-file auth via before_action with controller inheritance chain walking, concern expansion)
- Grape ✅ (v0.9.0+ —
lib/api/*.rb Grape API scanner: resource, namespace, route_param, before auth blocks, mount cross-file resolution, prefix/version support. Auto-invoked when lib/api/ exists. Tested on GitLab: 1,017 /api/v4/ routes detected)
Installation
npm install -g shadowaudit
Or use it locally in your project:
npm install --save-dev shadowaudit
Quick Start
shadowaudit --dir ./src --spec ./openapi.json
shadowaudit --dir ./src --spec ./openapi.json --framework express
shadowaudit --dir ./src --spec ./openapi.json --diff
shadowaudit --dir ./src --framework express --generate-spec > openapi.json
shadowaudit --dir ./src --spec ./openapi.json --format json | jq '.findings | length'
shadowaudit --dir ./src --spec ./openapi.json --format sarif > results.sarif
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 (default: "table")
--fail-on <level> Fail CI pipeline on: critical, high, info (default: "critical")
--framework <name> Force framework: express, fastapi, django, flask, nestjs, rails (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) [v0.7.0+]
--watch Watch for file changes and re-scan automatically [v0.7.0+]
-h, --help display help for command
--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.
shadowaudit --dir ./src --spec ./openapi.json --diff
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/cache to persist .shadowaudit-cache.json between 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
--diff to show only NEW findings per save
- Only watches
.js, .ts, .jsx, .tsx, .py files; ignores node_modules, dist, __pycache__, .git
- Cannot be combined with
--generate-spec or --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].
shadowaudit --dir ./src --spec ./openapi.json --concurrency 16
shadowaudit --dir ./src --spec ./openapi.json --concurrency 1
- Output is deterministic regardless of concurrency (verified by adversarial QA —
concurrency=1 and concurrency=64 produce 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.json
Exit codes
| No findings | 0 |
Findings below --fail-on threshold | 0 |
Findings at or above --fail-on threshold | 1 |
critical (default) | Any CRITICAL finding |
high | Any CRITICAL or HIGH finding |
info | Any finding at all |
Configuration
shadowaudit loads config from two sources (CLI flags take priority):
- CLI flags (highest priority)
.shadowaudit.yml or .shadowauditrc.yml in 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
- build
Default 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 basicAuth
Output 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.0.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:
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
╔════════════════════════════════╗
║ shadowaudit v1.0.0 ║
║ API Shadow Route Scanner ║
╚════════════════════════════════╝
[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: 1
In 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/traverse to walk every .js/.ts file'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
basePath and OpenAPI servers[0].url prefixes, 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 critical
Pre-commit hook
#!/bin/bash
shadowaudit --dir ./src --spec ./openapi.json --fail-on critical
Project 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=)
│ │ └── 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/ # 139 tests across 16 files
│ ├── config.test.ts
│ ├── comparator.test.ts
│ ├── diff.test.ts
│ ├── scanners/
│ │ ├── express.test.ts
│ │ ├── fastapi.test.ts
│ │ ├── django.test.ts
│ │ ├── django-cbv.test.ts
│ │ ├── flask.test.ts
│ │ └── auth.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
├── .github/workflows/
│ ├── shadowaudit.yml # CI: build + test + self-scan
│ └── deploy-docs.yml # Docs auto-deploy to GitHub Pages
├── action.yml # GitHub Marketplace Action definition
├── ROADMAP.md # v0.8.0 → v1.0.0 roadmap
├── CONTRIBUTING.md # How to add framework scanners
├── package.json
├── tsconfig.json
├── LICENSE
└── README.md
Development
npm install
npm run build
npm test
npm run dev -- --dir ./src --spec ./openapi.json
npm run lint
Tech 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