Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

api-pentest

AI-driven OWASP API security testing for our own NestJS APIs, run before an ISO 27001 audit. Claude is the reasoning engine; every safety guardrail is enforced in deterministic code it cannot bypass. Findings map OWASP API Top 10 2023 → OWASP ASVS 5.0 → ISO 27001:2022 Annex A.

The API edition is 2023 — there is no OWASP API Top 10 2025 (the Nov-2025 list is the separate web-app Top 10; its supply-chain item maps to API10 here).

Safety model

src/http/client.ts is the only code that sends a network request, and it calls src/guardrails/scope.ts before every send. The model's send_request tool routes through it, so it cannot exceed scope no matter what it decides or what a response tells it to do. Hardened against:

  • redirect-based host escape (maxRedirects: 0),
  • path-based origin escape (absolute / protocol-relative paths rejected; ../ normalized then host re-checked),
  • readOnly vs allowMethods contradiction (readOnly wins: only GET/HEAD survive),
  • deny-path encoding bypass (matched on raw and decoded path),
  • token leakage (identity tokens injected from config, never seen by the model; response set-cookie/authorization redacted; configured secret values scrubbed from bodies).

Setup

pnpm install
cp config/targets.example.json config/targets.json   # fill in local tokens + authorized hosts
pnpm run tsc

config/targets.json is gitignored — keep real tokens and hosts out of version control.

Configuration (targets.json)

Zod-validated on load. Full schema:

{
  "baseUrl": "https://staging.example.test",   // required; its host must appear in scope.allowHosts
  "specPath": "./openapi.json",                // required; compiled OpenAPI 3.x spec of the target
  "sourcePath": "../our-nest-api/src",         // optional; enables the NestJS static scanner (S1–S6)

  // One entry per role you want to test as. "headers" is sent VERBATIM on every request, so any
  // auth scheme works: bearer JWT, API key header, cookie, basic auth. role "none" = unauthenticated.
  "identities": [
    { "name": "admin", "role": "admin", "headers": { "Authorization": "Bearer <ADMIN_JWT>" } },
    { "name": "user",  "role": "user",  "headers": { "Authorization": "Bearer <USER_JWT>" } },
    { "name": "anonymous", "role": "none", "headers": {} }
  ],

  // For BOLA (API1): ids each identity legitimately owns. The tool has one identity request another
  // identity's object to check object-level authorization. Keys must match the path parameter names.
  "ownedObjects": {
    "user":  { "orderId": "1001" },
    "admin": { "orderId": "2002" }
  },

  "scope": {
    "allowHosts": ["staging.example.test"],    // required; every request host is checked against this
    "denyPathPatterns": ["*/delete-all*"],     // glob; matched on raw and percent-decoded path
    "readOnly": true,                          // true = only GET/HEAD are ever sent (default)
    "allowMethods": ["GET", "HEAD"]            // intersected with readOnly
  },

  "limits": {
    "maxRequestsPerSecond": 5,
    "requestTimeoutMs": 10000,
    "maxAgentStepsPerRoute": 12,               // hard cap on each AI agent loop
    "maxAgentConcurrency": 2,
    "maxFindingTokens": 4000
  },

  "ai": {
    "reasoningModel": "claude-opus-5-5",
    "triageModel": "claude-haiku-5-5",
    "effort": "high",
    "taskBudgetTokens": 20000                  // optional; per-run token budget (>= 20000)
  }
}

Why it needs credentials (authenticated testing)

This is deliberate and standard. The top API risks — API1 BOLA and API5 BFLA — are, in OWASP's words, cases where by design a legitimate user can reach the endpoint; the failure is at the object or function level. You can only test that with a valid session, by authenticating as one identity and trying to reach another's data. An unauthenticated scan only sees the public surface and misses the entire authorization layer, which is where the real risk lives.

Industry practice ("authenticated" / "credentialed" DAST) keeps it safe:

  • Use dedicated test accounts on staging, never a real customer's token and never production.
  • Prefer short-lived, revocable tokens; rotate or revoke them after the run.
  • Give the least privilege per role needed to exercise the flows.

The tool is built to match: tokens live only in the gitignored targets.json, are injected by the HTTP layer, are never shown to the AI model, and are scrubbed from every report.

Usage

pnpm run dev -- spec  --config config/targets.json               # print route inventory (M1)
pnpm run dev -- scan  --config config/targets.json --out ./out    # deterministic baseline (M2)
pnpm run dev -- scan  --config config/targets.json --ai           # + AI engine (M3/M5): authz + Haiku-triaged API3/4/8/6
pnpm run dev -- scan  --config config/targets.json --static-only  # NestJS source scan only (M6, needs sourcePath)
pnpm run dev -- scan  --config config/targets.json --html --pdf   # also emit report.html / report.pdf
pnpm run dev -- report --in ./out/report.json --html --pdf        # render a prior run

The static scanner (S1–S6) runs automatically when sourcePath is set in targets.json, unless --dynamic-only is passed. It is deterministic (confidence: "confirmed") with file:line evidence, and suppresses false positives: no S1 when a global guard (APP_GUARD) is present or a route is @Public(), no S3 for DTOs that already carry class-validator decorators.

--ai is the only command that spends on the Anthropic API — it is opt-in. It needs credentials (ANTHROPIC_API_KEY, or an ant auth login profile). Cost is bounded by maxAgentStepsPerRoute (hard max_iterations cap) × routes × maxAgentConcurrency; the run prints its agent-run count.

OWASP API coverage

All 10 categories have at least one check. Depth varies — deterministic dynamic/static checks are confidence: "confirmed"; AI checks are needs-human-review. ✅ full, 🟡 partial.

Category Depth How it is tested
API1 BOLA ✅ dynamic (cross-identity object read) + AI authz agent
API2 Broken Authentication ✅ dynamic (auth-required route accepts a corrupted token)
API3 Property-level authz ✅ AI (triage-gated) + static S2/S3
API4 Resource consumption ✅ AI (triage-gated) + static S4
API5 Function-level authz ✅ dynamic (anon on auth-required route) + AI authz + static S1
API6 Sensitive business flows ✅ AI (triage-gated)
API7 SSRF 🟡 dynamic (URL-valued query param triggers a server-side fetch of an unresolvable probe); body-based SSRF not reachable under readOnly
API8 Security misconfiguration ✅ AI (triage-gated) + static S5/S6
API9 Improper inventory 🟡 static (source route not in the OpenAPI spec = shadow endpoint); needs sourcePath
API10 Unsafe consumption 🟡 static S7 (plaintext/non-TLS upstream calls) + AI source review: a read_source-equipped agent reads the outbound-HTTP files and judges whether upstream data is validated (needs-human-review, needs --ai + sourcePath)

AI source review (API10): when --ai runs and sourcePath is set, one app-level agent reads the files that make outbound HTTP calls (found by a quick scan) via a read_source tool confined to the source root, and records API10 findings with file:line + code evidence. This is the "you provide the source, the AI does the review" path — findings are needs-human-review.

Known gaps (by design): API7 misses body/POST SSRF under readOnly; API9 needs sourcePath and can false-flag if a setGlobalPrefix isn't in the spec; API10's AI review is decision-support (needs-human-review), not a deterministic verdict. Called out so the audit story stays honest.

Build order

Milestone Scope State
M1 Spec parser + CLI + config/zod + guardrails + HTTP choke point done
M2 Mechanical replay matrix (identity × status) done
M3 AI engine: tools + API1/API5 agent loop done
M4 Report (JSON + HTML + PDF) with OWASP/ISO mapping done
M5 Remaining AI categories (API3/4/8/6) + Haiku triage done
M6 NestJS static scanner S1–S6 done
M7 Prompt-cache tuning, task budgets, token-redaction audit, CI codes done

AI surface (verified against the Anthropic TS SDK)

  • Reasoning + report: claude-opus-5-5; bulk triage: claude-haiku-5-5.
  • Agent loop: client.beta.messages.toolRunner + betaZodTool (from @anthropic-ai/sdk/helpers/beta/zod).
  • Thinking {type: "adaptive"}; output_config.effort set to high explicitly (Opus 5.5 defaults to medium).
  • Haiku triage uses structured outputs (client.messages.parse + zodOutputFormat), not the deprecated output_format; it gates which routes earn a full Opus run for API3/4/8/6.
  • Source review (API10): a read_source tool confined to sourcePath (path-traversal blocked) lets one agent read the outbound-HTTP files and reason over the code — the model does the review, you supply the source.
  • PDF via pdfkit (pure-JS, no headless Chromium) — one runtime dep; renders text literally, so no HTML-injection surface.

OWASP framework definition

The framework findings are verified against lives in src/report/framework.ts — the single source of truth. Each OWASP API Top 10 2023 category (API1–API10) is defined with a summary, common weaknesses, test approach, the evidence that confirms it, and its ASVS 5.0 / ISO 27001:2022 mapping. src/report/mapping.ts derives the slim lookup from it; the report appendix and every finding cite it. Inspect or export it:

pnpm run dev -- framework                          # print the full definition
pnpm run dev -- framework --json data/owasp-api-2023.json   # export for audit verification

CI gate & cost controls

  • scan exits non-zero when any finding is at or above --fail-on <severity> (default high) — the CI gate.
  • Set ai.taskBudgetTokens (≥ 20000) in targets.json to give each agent run a token budget (beta task-budgets-2026-03-13) on top of the hard maxAgentStepsPerRoute cap.
  • The stable per-category system prompt is cached (cache_control); per-route context sits after the breakpoint so only the volatile part is re-sent.
  • Identity tokens are injected by the HTTP layer and never reach the model; response set-cookie/authorization are redacted, and configured secret values are scrubbed from every finding's evidence before the report is written.

Testing

pnpm test

Deterministic units (no network, no Anthropic calls): scope guardrails (allowlist, deny-path encoding, readOnly precedence, host escape), config resolution, secret redaction, the static scanner against test/fixtures/nest-sample (asserts all six rules fire and the false-positive guards hold), and the AI engine with an injected fake client (asserts it runs the authz agent, routes send_request through the HTTP client, and records an AI finding, all without spending on the API).

Data provenance

  • data/asvs-5.0.csv is the official OWASP ASVS 5.0.0 requirement set — 345 requirements across chapters V1–V17, sourced from OWASP/ASVS (5.0/docs_en/OWASP_Application_Security_Verification_Standard_5.0.0_en.csv), reduced to chapter,req_id,level (official descriptions omitted — not used by the tool; add them back if a report needs the full text).
  • OWASP reference URLs in src/report/framework.ts are verified against the live OWASP API Top 10 2023 index (api-security.owasp.org).

Caveats

  • Dependency versions in package.json are best-effort; pnpm install resolves the real ones — re-run pnpm run tsc after.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages