shadowaudit
v0.7.0
Published
Static API security scanner — detects undocumented and unauthenticated Express.js routes before they reach production
Maintainers
Readme
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 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
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, 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),@login_requiredauth detection) - NestJS ✅ (
@Controller()+@Get()/@Post()decorator extraction,@UseGuards()auth detection,@Public()override, controller prefix + method path combination)
Installation
npm install -g shadowauditOr use it locally in your project:
npm install --save-dev shadowauditQuick 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.sarifCLI 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 (default: "auto")
--generate-spec Generate OpenAPI spec from scanned routes (no comparison)
--diff Show only new findings since last scan (requires --spec)
-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.
# 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
--generate-spec (v0.4.0+)
Don't have an OpenAPI spec? Generate one from your code:
shadowaudit --dir ./src --framework express --generate-spec > openapi.jsonExit codes
| 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 |
Configuration
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": "0.6.1",
"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
╔════════════════════════════════╗
║ shadowaudit v0.7.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: 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 criticalPre-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=)
│ │ └── 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.7.0 → v1.0.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 (139 tests across 16 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
