Skip to main content

pyapicheck

Discover an API inventory from an OpenAPI spec, flag likely-sensitive fields, and score every endpoint's risk — with every point on the score traced back to a named, human-readable reason. No opaque number.

This is the free-to-use discovery core for a broader idea (a control plane for authorizing what AI agents are allowed to do with the APIs they call) — see the accompanying product vision doc. pyapicheck itself stands alone: point it at an OpenAPI spec and it tells you, in about a second, which endpoints are unauthenticated, which touch PII/financial/credential data, and which combination of the two is the actual emergency.

The parsing, classification, and scoring engine is Rust (core/); this package is a thin Python CLI/SDK wrapper over it (bindings/ + python/), built with PyO3 and maturin.

Install (from source, for now)

uv venv .venv --python 3.12
source .venv/bin/activate
uv pip install maturin
maturin develop --release

Use

pyapicheck discover examples/sample-openapi.yaml
Commerce API (1.3.0)
source: examples/sample-openapi.yaml

7 endpoints discovered  |  2 high/critical  |  2 unauthenticated  |  4 touch sensitive data

POST    /api/v1/refunds              CRITICAL           score=90
    - [HIGH] No authentication scheme declared for POST /api/v1/refunds
    - [HIGH] Endpoint handles fields classified as: financial
    - [CRITICAL] Sensitive data is reachable without authentication
    fields: account_number (financial, request)

DELETE  /api/v1/users/{id}           HIGH               score=40
    - [HIGH] No authentication scheme declared for DELETE /api/v1/users/{id}
...

Every finding lists why — that's the point. A security engineer should never have to trust a score they can't check.

As a library

import pyapicheck

inventory = pyapicheck.discover("openapi.yaml")
for endpoint in inventory["endpoints"]:
    if endpoint["risk"]["level"] in ("HIGH", "CRITICAL"):
        print(endpoint["method"], endpoint["path"], endpoint["risk"]["factors"])

Discovering a whole repo, or a Postman collection

discover isn't limited to one hand-picked OpenAPI file:

pyapicheck discover ./services/          # walks the tree, discovers every openapi/swagger file it finds
pyapicheck discover collection.json      # Postman Collection v2.1 export — same risk-scored report

A directory with multiple services prints one report per spec plus an aggregate total; a Postman collection is auto-detected by shape (no flag needed) and normalized into the same report format as an OpenAPI spec — sensitive fields are classified from the collection's example request bodies/query params instead of a schema, since Postman collections don't carry one.

Drift detection

pyapicheck diff old-openapi.yaml new-openapi.yaml

Reports endpoints added, removed, or changed (authentication, deprecation, or sensitive-field differences) between two spec snapshots — e.g. two Git revisions checked out to disk. This is the first "behavior changed" signal, still fully static (no traffic required).

Observed vs. declared: shadow and zombie endpoints

pyapicheck report openapi.yaml access.log

Cross-references the declared spec against a gateway access log (NDJSON, NGINX- or Envoy-shaped JSON log lines) and flags zombie endpoints (declared, zero observed requests) and shadow endpoints (observed traffic hitting something not in the spec at all) — the first "what's actually happening" signal, on top of the purely-declared risk report.

Persisting to Postgres (optional)

By default pyapicheck is entirely file-in, report-out — no database required. Add --db-url to discover or report to also persist the result (each run creates a new history row rather than overwriting the last one):

pyapicheck report openapi.yaml access.log --db-url postgres://user:pass@host/db
import pyapicheck
inventory_id = pyapicheck.persist("postgres://user:pass@host/db", "openapi.yaml")
inventory = pyapicheck.load_inventory("postgres://user:pass@host/db", inventory_id)

In CI

pyapicheck discover openapi.yaml --fail-on-high   # exit 2 if anything is HIGH/CRITICAL
pyapicheck discover openapi.yaml --json           # machine-readable output

Automated remediation

For the subset of findings that have one safe, mechanical fix, remediate generates (and can apply) a real patch to the spec — not just a report:

pyapicheck remediate openapi.yaml            # dry run: prints a diff, changes nothing
pyapicheck remediate openapi.yaml --apply    # writes the fix back to the file
2 fixable finding(s):

  [no_auth] POST    /api/v1/refunds              Add `security: [{bearerAuth: []}]` to POST /api/v1/refunds (references the already-declared 'bearerAuth' scheme)
  [no_auth] DELETE  /api/v1/users/{id}           Add `security: [{bearerAuth: []}]` to DELETE /api/v1/users/{id} (references the already-declared 'bearerAuth' scheme)

--- a/openapi.yaml
+++ b/openapi.yaml
@@ -108,7 +108,7 @@
       # BUG: no security override here, and the top-level default is
       # accidentally excluded from this build's deploy config — this endpoint
       # ships with no auth in production despite handling financial data.
-      security: []
+      security: [{bearerAuth: []}]
       operationId: createRefund

Two findings are fixable today:

  • no_auth — adds a security requirement, but only referencing a scheme the spec already declares in components.securitySchemes. It never invents an auth mechanism, only wires up one the API author set up and forgot to apply on that operation.
  • missing_metadata — adds a deterministic operationId derived from the method + path.

sensitive_data, unauthenticated_sensitive_data, and deprecated_still_live stay advisory-only by design — deciding what a sensitive field should do, or whether a deprecated endpoint can be removed, is a business decision this tool has no basis to make for you. The patch is a targeted, format-preserving text edit, not a full parse-and-re-serialize round trip — comments, key order, and quote style elsewhere in the file are untouched, so the diff stays minimal and reviewable.

Security graph: MCP/agent discovery, reachability, blast radius

Beyond individual API specs, pyapicheck can build a security graph (Postgres + Apache AGE) of User/Agent/ Tool/Endpoint/Resource/Role nodes and answer graph questions a config file can't answer directly:

# Discover MCP servers from a config file, live-introspect each one's real
# tool list over stdio JSON-RPC, and write them into the graph as Tool nodes.
pyapicheck graph load-mcp claude_desktop_config.json --db-url postgres://user:pass@host/db

# Declare an agent's identity and what it's allowed to call.
pyapicheck graph add-agent finance-agent --owner alice \
  --tool refunds-api --scope "process customer refunds" \
  --db-url postgres://user:pass@host/db

# "What can this agent reach" -- multi-hop traversal, not a config guess.
pyapicheck graph reachable finance-agent --db-url postgres://user:pass@host/db

# "What's the blast radius if this leaks" -- reverse traversal.
pyapicheck graph blast-radius accounts_table --db-url postgres://user:pass@host/db

MCP tool discovery is real, not config-trusting: each configured server is actually spawned and asked for its tool list over the real MCP JSON-RPC handshake (initializetools/list). A server that fails to start or doesn't answer is reported unavailable: <reason>, never silently treated as "zero tools." An agent can only be linked to a tool/API that's already been discovered -- graph add-agent fails loudly if you reference one that isn't in the graph yet, rather than silently no-op-ing.

Behavioral baselining: BOLA-shaped access and first-time operations

pyapicheck baseline openapi.yaml historical-access.log current-access.log --agent finance-agent

Given a caller identity in the traffic (extracted from common log fields like user_id/agent_id/sub -- not every gateway log carries one out of the box), this computes per-identity baselines (request volume, error rate, distinct resources touched, timing regularity) and two concrete, checkable findings — not fuzzy anomaly scores:

  • Sequential-ID access (BOLA-shaped finding): an identity hitting a single-numeric-ID endpoint with a run of near-sequential IDs (1, 2, 3, 4, ...) — the classic enumeration signature.
  • First-time-observed operation: an identity calling a declared endpoint it has never called before, compared against the historical log — the exact "a known agent does something it's never done before" trigger from the product vision's worked scenario. This is keyed on the endpoint template, not the concrete resource path, so touching a new resource ID on an already-familiar endpoint doesn't create noise.

--agent NAME marks an identity as a declared agent (rather than guessing from timing); undeclared identities' timing regularity is reported as a raw statistic, not classified as "bot" or "human" for you.

Cedar policy: recommendations and drift detection

# Real Cedar syntax validation
pyapicheck policies validate agent-policy.cedar

# Turn Phase 4 findings into ready-to-use Cedar policy text
pyapicheck policies recommend openapi.yaml historical.log current.log --agent finance-agent

# Find findings an EXISTING policy would currently allow, with fixes
pyapicheck policies diff agent-policy.cedar openapi.yaml historical.log current.log --agent finance-agent

Uses Cedar (Amazon's policy language) for real parsing and evaluation — not a bespoke rules format. Cedar only has two effects, permit/forbid, no native "require approval" — every recommendation here is a forbid, tagged @effect_hint("deny") (a strong signal, like BOLA enumeration) or @effect_hint("require_approval") (a first-time operation that merits review, not an automatic verdict).

policies diff doesn't guess whether your existing policy covers a finding — it actually evaluates the finding's exact (principal, action, resource) through Cedar's real authorizer against your policy file. A finding Cedar would currently Allow (e.g. a broad permit for an agent with no carve-out) is a genuine gap, reported with the exact forbid text that closes it.

Emitting an enforcement artifact (Envoy)

pyapicheck policies emit-envoy agent-policy.cedar --out rbac-filter.yaml

Translates a Cedar policy's forbid rules into an Envoy envoy.filters.http.rbac HTTP filter config snippet — a config artifact for a human to splice into a real Envoy deployment's http_filters chain, not something this command deploys or wires into a live request path itself. The generated schema was verified against a real Envoy instance (envoyproxy/envoy, Docker): envoy --mode validate accepts it, and a live container genuinely returns 403 for the exact (principal, method, path) a policy targets — and does not 403 a different principal or a different endpoint for the same principal.

What this is (and isn't) — yet

pyapicheck today parses declared API surface from an OpenAPI spec, cross-references it against real traffic, builds a security graph of agents/tools/resources, baselines per-identity behavior, generates/ validates Cedar policy, and can emit an Envoy enforcement artifact from that policy. It does not yet wire that artifact into a live gateway itself, or have an AI analyst layer — those are the next layers. See ROADMAP.md for the concrete, phase-by-phase plan from here to the full product vision (an authorization and behavior control plane for AI agents and the APIs/MCP servers they call). Sensitive-field classification is a lightweight keyword heuristic (core/src/classify.rs), not an NLP model — it's designed to be swapped for something like Microsoft Presidio without changing the public API.

Development

cargo test -p pyapicheck-core   # Rust unit + integration tests
maturin develop                 # rebuild the extension into .venv after Rust changes

License

Proprietary License — Free to use with explicit attribution. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyapicheck-0.7.0.tar.gz (89.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyapicheck-0.7.0-cp39-abi3-macosx_11_0_arm64.whl (4.1 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

File details

Details for the file pyapicheck-0.7.0.tar.gz.

File metadata

  • Download URL: pyapicheck-0.7.0.tar.gz
  • Upload date:
  • Size: 89.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.16

File hashes

Hashes for pyapicheck-0.7.0.tar.gz
Algorithm Hash digest
SHA256 c259f37446e180a58b2b4f854b3edcef6cea210c2dec7286d4f7d8299866dc13
MD5 7ff6e7e13b7d3e0c7dcb896fe7336ebb
BLAKE2b-256 9a5130feec4b20830f07178ba0210b25a1c9b0fab80ed1c962b240d2913750f1

See more details on using hashes here.

File details

Details for the file pyapicheck-0.7.0-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for pyapicheck-0.7.0-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 ebf4f6e3d12bcacdaa62d921f434417795149d8c69d526393e980a0248ce1c99
MD5 d15cbc11fcb6a91f6a2770903fe11e73
BLAKE2b-256 f9d33fda757383c9f24ff91da2d1e270b2307376868389e5aa064483124b200a

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.7.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page