DEV Community

Dark Master
Dark Master

Posted on

I built a CLI that catches shadow APIs in Express.js repos before they merge to production

Shadow audit

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

Installation
Global CLI (recommended)
npm install -g shadowaudit
npx (no install needed)
npx shadowaudit --dir ./src --spec ./openapi.json
GitHub Action
Add to .github/workflows/security.yml:

  • uses: darkmaster0345/shadow-Audit@v0.1.0-beta-2 with: dir: './src' spec: './openapi.json' fail-on: 'critical' 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 supported now; FastAPI & Django coming soon)
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 ✅ (full support — AST-based route extraction via Babel)
FastAPI 🚧 (coming soon)
Django 🚧 (coming soon)
Installation
npm install -g shadowaudit
Or use it locally in your project:

npm install --save-dev shadowaudit
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

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
CLI Options
Usage: shadowaudit [options]

Static API security scanner for undocumented routes

Options:
-V, --version output the version number
--dir Directory to scan (default: current directory)
--spec Path to OpenAPI/Swagger spec file
--format Output format: table, json, sarif (default: "table")
--fail-on Fail CI pipeline on: critical, high, info (default: "critical")
--framework Force framework: express, fastapi, django (default: "auto")
-h, --help display help for command
Exit 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.yml or .shadowauditrc.yml in your project root
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": "0.1.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
╔════════════════════════════════╗
║ shadowaudit v0.1.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
│ ├── config.ts # cosmiconfig loader (CLI flags + .shadowaudit.yml)
│ ├── comparator.ts # Delta comparator (route diffing + severity scoring)
│ ├── types.ts # Shared TypeScript interfaces
│ ├── scanners/
│ │ ├── express.ts # Express.js route extractor (Babel AST)
│ │ └── auth.ts # Auth middleware detector
│ ├── 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
│ └── utils/
│ └── logger.ts # Colored console output
├── tests/
│ ├── config.test.ts
│ ├── comparator.test.ts
│ ├── scanners/
│ │ ├── express.test.ts
│ │ └── auth.test.ts
│ ├── parsers/
│ │ └── spec.test.ts
│ └── formatters/
│ ├── table.test.ts
│ └── sarif.test.ts
├── .github/workflows/ci.yml
├── package.json
├── tsconfig.json
├── LICENSE
└── README.md
Development

Install dependencies

npm install

Build

npm run build

Run tests (45 tests across 8 test files)

npm test

Run in dev mode

npm run dev -- --dir ./src --spec ./openapi.json

Lint

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
License
MIT © Ubaid ur Rehman 2026

Top comments (0)