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 asecurityrequirement, but only referencing a scheme the spec already declares incomponents.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 deterministicoperationIdderived 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 (initialize → tools/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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c259f37446e180a58b2b4f854b3edcef6cea210c2dec7286d4f7d8299866dc13
|
|
| MD5 |
7ff6e7e13b7d3e0c7dcb896fe7336ebb
|
|
| BLAKE2b-256 |
9a5130feec4b20830f07178ba0210b25a1c9b0fab80ed1c962b240d2913750f1
|
File details
Details for the file pyapicheck-0.7.0-cp39-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: pyapicheck-0.7.0-cp39-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 4.1 MB
- Tags: CPython 3.9+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.16
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ebf4f6e3d12bcacdaa62d921f434417795149d8c69d526393e980a0248ce1c99
|
|
| MD5 |
d15cbc11fcb6a91f6a2770903fe11e73
|
|
| BLAKE2b-256 |
f9d33fda757383c9f24ff91da2d1e270b2307376868389e5aa064483124b200a
|