@houysengleang/apicheck
v0.1.0
Published
Non-destructive CLI that scans your own HTTP APIs for common security and hygiene problems, mapped to the OWASP API Security Top 10.
Maintainers
Readme
apicheck
Catch the boring-but-critical security holes in your own APIs before you ship — mapped to the OWASP API Security Top 10, non-destructive by design.
A small, non-destructive command-line tool that scans your own HTTP APIs for common security and hygiene problems — mapped to the OWASP API Security Top 10. Point it at a base URL plus an OpenAPI spec; it auto-discovers every endpoint and runs 11 checks across 6 OWASP categories, reporting to your terminal, as JSON, or as SARIF for CI dashboards.
It fills the gap between "run curl by hand" and heavyweight scanners like OWASP ZAP.
⚠️ Only scan systems you own or are explicitly authorized to test. apicheck observes and reports; it never exploits, modifies, or exfiltrates data. You must pass
--i-am-authorizedto run a scan.
Install
npm install -g @hoysengleang/apicheck # installs the `apicheck` command
# or run without installing:
npx @hoysengleang/apicheck scan …From source (for development):
npm install # dev deps
npm run build # produces dist/
npm link # optional: makes `apicheck` available globallyRequires Node.js 20+.
Usage
apicheck scan <baseUrl> --spec <openapi.json|url> --i-am-authorized [options]Example:
apicheck scan https://staging.example.com \
--spec ./openapi.json \
--token-a "$TOKEN_A" \
--token-b "$TOKEN_B" \
--i-am-authorizedbaseUrl and --spec may instead come from a config file.
Options:
| Flag | Meaning |
|------|---------|
| --spec <path> | OpenAPI/Swagger spec (file path or URL) — required |
| --i-am-authorized | Affirm you may scan the target — required |
| --token-a, --token-b | Bearer tokens for two identities (auth / IDOR checks) |
| --auth-header-a, --auth-header-b | Custom auth header per identity, "Name: Value" (repeatable) — for API-key/cookie/custom schemes |
| --include <glob> | Only scan paths matching this glob (repeatable) |
| --exclude <glob> | Skip paths matching this glob (repeatable) |
| --concurrency <n> | Max concurrent requests (default 8, max 20) |
| --rate-limit-burst <n> | Burst size for the rate-limit check (default 20, max 100) |
| --timeout <ms> | Per-request timeout (default 10000) |
| --fail-on <fail\|warn> | Exit non-zero on this severity or higher (default fail) |
| --json | Emit machine-readable JSON to stdout (for CI) |
| --sarif | Emit SARIF 2.1.0 to stdout (for code-scanning dashboards) |
| --show-passes | Also print passing findings |
| --config <path> | Config file (default apicheck.config.json) |
| --ignore-file <path> | Suppression file (default .apicheckignore) |
Globs support * (within a path segment), ** (across segments), and ?.
--exclude wins over --include, and both also scope the shadow-endpoint
probe. --sarif takes precedence over --json; in both machine-output modes
stdout is a single valid document (spinner/logs go to stderr).
Exit code is 1 when any finding meets --fail-on, so it works as a CI gate.
Configuration or spec errors exit 2.
Config file
Commit shared settings instead of passing long flag lines. apicheck reads
apicheck.config.json from the working directory (or --config <path>); CLI
flags override the file.
{
"baseUrl": "https://staging.example.com",
"specPath": "./openapi.json",
"tokens": { "a": "…", "b": "…" },
"authHeaders": { "a": { "X-API-Key": "…" } },
"include": ["/v1/**"],
"exclude": ["/admin/**"],
"ignore": ["rate-limit:GET /health"],
"rateLimitBurst": 20,
"failOn": "warn",
"authorized": true
}Suppressing findings
Keep a gate green against known/accepted issues with .apicheckignore (or
--ignore-file, or an ignore array in the config). Each rule is
<check>:<endpointGlob>; a bare <check> suppresses it everywhere. Suppressed
findings are dropped before the summary and the exit code.
# .apicheckignore
rate-limit:GET /health # health is intentionally unlimited
cors:* # CORS handled at the gateway
*:GET /legacy # legacy route, scheduled for removalChecks
Per-endpoint checks:
| Check | OWASP | What it finds | |-------|-------|---------------| | security-headers | API8:2023 | Missing HSTS / nosniff / frame protection / cache-control | | missing-auth | API5:2023 / API2:2023 | Auth-required endpoints returning data unauthenticated | | rate-limit | API4:2023 | No throttling under a bounded (unpaced, hard-capped) burst | | idor | API1:2023 | Token B reading token A's object (broken object-level auth) | | excessive-data | API3:2023 | Sensitive fields (password, secret, token, …) in responses | | cors | API8:2023 | Reflective / wildcard CORS (worst with credentials) | | cookie-flags | API8:2023 | Set-Cookie missing Secure / HttpOnly / SameSite | | info-disclosure | API8:2023 | Server/version-leaking headers and leaked stack traces | | bfla | API5:2023 | Lower-privilege identity reaching privileged functions |
Scan-level checks (run once over the whole API):
| Check | OWASP | What it finds | |-------|-------|---------------| | shadow-endpoints | API9:2023 | Undocumented paths that respond (curated, bounded probe) | | inventory-drift | API9:2023 | Deprecated-but-published operations and API-version sprawl |
That's 11 checks across 6 of the OWASP API Top 10 (API1, API3, API4, API5, API8, API9).
Injection, SSRF, brute-force, and business-logic abuse are intentionally out of scope — they require sending attack payloads, which conflicts with the non-destructive model. See docs/safety.md.
Example output
⚠ GET /users missing security headers: X-Content-Type-Options: nosniff (API8:2023) [security-headers]
✖ GET /users returned 200 with no Authorization header — endpoint should require auth (API5:2023) [missing-auth]
✔ GET /health rate limiting observed within 5 requests (API4:2023) [rate-limit]
✖ GET /users/{id} token B read token A's resource at /users/alice (identical response body) — broken object level authorization (API1:2023) [idor]
Scanned 5 endpoints · 5 fail · 5 warn · 3 passCI integration
Exit code 1 on findings makes apicheck a gate in any pipeline. With --sarif
you can also feed results into GitHub code scanning so they show up in the
Security tab and as PR annotations:
# .github/workflows/api-scan.yml
- run: npx @hoysengleang/apicheck scan "$STAGING_URL" --spec openapi.json
--token-a "$TOKEN_A" --token-b "$TOKEN_B"
--i-am-authorized --sarif > apicheck.sarif
continue-on-error: true # let the upload run even when findings fail the step
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: apicheck.sarifDevelopment
npm run dev -- scan http://127.0.0.1:4010 --spec ./test/fixtures/openapi.json --i-am-authorized
npm test # vitest (unit + end-to-end against a local mock API)
npm run typecheck # tsc --noEmit (strict)
npm run lint # eslint
npm run build # tsup -> dist/The end-to-end test spins up a deliberately-flawed mock API (test/e2e/mock-api.ts) and asserts that each check produces the expected finding. To poke at it by hand:
npx tsx test/e2e/serve.ts 4010 # start the mock API
node dist/index.js scan http://127.0.0.1:4010 \
--spec test/fixtures/openapi.json \
--token-a TOKEN_A --token-b TOKEN_B --i-am-authorized --show-passesArchitecture
One data flow, three independent stages: discover → run checks → report.
OpenAPI spec ─▶ discovery ─▶ Endpoint[] ─▶ [ Check … ] ─▶ Finding[] ─▶ report (terminal + json)
▲
HttpClient + ScanConfig| Path | Responsibility |
|------|----------------|
| src/index.ts | CLI entry (commander) + scan orchestration |
| src/runner.ts | Runs each check over each endpoint, collects findings |
| src/config.ts | zod-validated ScanConfig + authorization gate |
| src/discovery/openapi.ts | Spec → normalized Endpoint[] |
| src/http/client.ts | undici wrapper: auth, timeout, retry, self-throttle, safe-method guard |
| src/checks/* | One file per check, all implementing the Check contract |
| src/report/* | Terminal (chalk) and JSON reporters |
The full design, conventions, and safety rationale live in docs/
(SKILL.md, architecture.md, checks.md, safety.md). Adding a check is one
new file in src/checks/ plus one line in src/checks/index.ts.
Safety model
apicheck is observational and non-destructive by design:
- Checks observe and report — never exploit, modify, or exfiltrate.
- The HTTP client permits only GET / HEAD / OPTIONS by default; any other method
requires the separate, greppable
unsafeRequest()(which v1 checks never call). - Authorization is affirmed via
--i-am-authorizedand enforced structurally in config validation. - The scanner self-throttles; the rate-limit check is the one intentional, hard-capped (≤100) burst, and never targets a mutating endpoint.
See docs/safety.md for the full rationale.
Contributing
Contributions are welcome — especially new checks. A check is one file + one registry line + one test; scaffold it with:
npm run new-check -- <kebab-name> <OWASP-ref> # e.g. verbose-errors API8:2023See CONTRIBUTING.md for the full recipe, the safety rules every check must honor, and the dev workflow. Please also read our Code of Conduct. To report a security issue in apicheck itself, see SECURITY.md.
