Static API security scanner — finds undocumented and unauthenticated routes before they reach production
🌐 Documentation & Demo → · 🛒 GitHub Marketplace →
npm install -g shadowauditnpx shadowaudit --dir ./src --spec ./openapi.jsonDon'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.
Add to .github/workflows/security.yml:
- uses: darkmaster0345/shadow-Audit@v1.0.5
with:
dir: './src'
spec: './openapi.json'
fail-on: 'critical'shadowaudit has been tested against real production codebases to validate accuracy and guide the roadmap:
- 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)
- 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
- 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]
- 3 internal routes detected in Flask's own source code
- Flask
<param>syntax correctly converted to{param} - Blueprint routes (
@bp.route()) detected
- 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
- 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)
- 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.
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.
- 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 | 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 |
- 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)
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 (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)
-h, --help display help for command
| 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.) |
npm install -g shadowauditOr use it locally in your project:
npm install --save-dev shadowaudit# 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.sarifUsage: 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
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
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)
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)
Don't have an OpenAPI spec? Generate one from your code:
shadowaudit --dir ./src --framework express --generate-spec > openapi.json| Condition | Exit code |
|---|---|
| No findings | 0 |
Findings below --fail-on threshold |
0 |
Findings at or above --fail-on threshold |
1 |
--fail-on value |
Fails when |
|---|---|
critical (default) |
Any CRITICAL finding |
high |
Any CRITICAL or HIGH finding |
info |
Any finding at all |
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
- buildEven without custom config, shadowaudit detects these auth middleware patterns:
.authenticate( .authorize( requireAuth isAuthenticated
verifyToken checkAuth ensureLoggedIn passport.authenticate
jwt.verify bearerAuth apiKeyAuth basicAuth
Human-readable colored table with severity, method, path, file, line, and auth status. Includes a summary box and pipeline recommendation.
Structured JSON for CI dashboards and log aggregators:
{
"scanMeta": {
"tool": "shadowaudit",
"version": "1.0.5",
"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": [...]
}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 |
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.5 ║
║ 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
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 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
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#!/bin/bash
shadowaudit --dir ./src --spec ./openapi.json --fail-on criticalshadowaudit/
├── 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 # v1.0.5 → v1.1.0 roadmap
├── CONTRIBUTING.md # How to add framework scanners
├── package.json
├── tsconfig.json
├── LICENSE
└── README.md
# Install dependencies
npm install
# Build
npm run build
# Run tests (139 tests across 16 test files)
npm test
# Run in dev mode
npm run dev -- --dir ./src --spec ./openapi.json
# Lint
npm run lint- 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
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
MIT © Ubaid ur Rehman 2026