Skip to main content
Glama

specvet

tests python dependencies rules license

Deterministic security review for OpenAPI and Swagger documents.

36 rules, each mapped to a category of the OWASP API Security Top 10 2023. Zero dependencies. SARIF for pull-request gates. MCP server for agent workflows.

Every finding carries three coordinates — a severity, a JSON Pointer that survives a reflow of the file, and a line number you can jump to.

$ specvet demo --scan

demo/openapi.yaml  OpenAPI 3.0.3 · 11 operations · 2 files
79 findings (10 critical, 24 high, 36 medium, 9 low) · risk 100/100 · auth coverage 18.2%

OWASP API Top 10 coverage
  API1:2023      5 finding(s), worst critical
  API2:2023     19 finding(s), worst critical
  API3:2023      5 finding(s), worst high
  API4:2023     16 finding(s), worst medium
  API5:2023      9 finding(s), worst critical
  API6:2023      2 finding(s), worst high
  API7:2023      2 finding(s), worst high
  API8:2023     18 finding(s), worst high
  API9:2023      2 finding(s), worst low
  API10:2023     1 finding(s), worst medium

CRITICAL  SPV002  Object path readable without authentication  [API1:2023]
           /paths/~1orders~1{orderId}/get
           line 49
           GET /orders/{orderId} takes ['orderId'] and has no security
           | get:

Three steps

# 1. run it against a spec you already have
uvx --from specvet specvet scan openapi.yaml

# 2. or see what it does, with nothing to install and no file to find
uvx --from specvet specvet demo --scan

# 3. put it in CI and fail the build on high or worse (the default)
specvet scan . --format sarif -o specvet.sarif

Exit codes: 0 clean, 1 a finding at or above --fail-on, 2 usage error.

Related MCP server: swagger-api-mcp-server

Why another API linter

Spectral, Redocly and vacuum are excellent at style. They answer "is this document well-formed and consistent?". They are also 3k★ JavaScript and Go projects with plugin systems.

specvet answers a different question: what can an attacker do with this contract? Three consequences follow from taking that seriously:

1. Rules are organised by threat, not by document shape. Every rule is tagged API1:2023 … API10:2023, so the output answers "where are we exposed to broken object-level authorization?" rather than "you have 47 lint warnings". specvet rules --owasp API5:2023 lists just those.

2. The second pass reads relationships, not endpoints. Reading one operation at a time cannot see:

  • SPV021 — a path that authenticates GET but not DELETE. Each endpoint looks defensible alone; together they say the authorization is backwards.

  • SPV005 — a securitySchemes entry no security requirement references. The document looks authenticated and authenticates nothing.

3. Absent is not the same as empty. security: missing is an omission. security: [] is a decision. The loader keeps them apart, so SPV023 (an explicitly public write) is reported as a decision while SPV004 (no security anywhere) is reported as a hole.

Rules

Rule

Severity

OWASP

What it catches

SPV001

high

API1

Object write with no 403/404 — "not yours" and "not there" are indistinguishable

SPV002

critical

API1

Anonymous read on an identifier path: enumeration is the vulnerability

SPV003

medium

API1

No 404 declared for an identifier-addressed read

SPV004

critical

API2

Operation with no security requirement anywhere

SPV005

high

API2

Scheme defined but never required

SPV006

high

API2

API key in the query string (logs, history, Referer)

SPV007

high

API2

HTTP Basic over a plaintext server

SPV008

high

API2

OAuth2 implicit flow

SPV009

medium

API2

OAuth2 flow with no scopes

SPV010

low

API2

Bearer scheme with no bearerFormat

SPV011

high

API2

No top-level security: anonymous by default

SPV012

high

API3

Sensitive-looking property in a 2xx schema

SPV013

medium

API3

Request schema accepts unknown properties (mass assignment)

SPV014

medium

API3

One schema used for both request and 2xx response

SPV015

medium

API4

Array property with no maxItems

SPV016

low

API4

String property with no maxLength

SPV017

medium

API4

Numeric property with no bounds

SPV018

medium

API4

No 429 declared

SPV019

low

API4

Pagination parameter with no maximum

SPV020

critical

API5

Administrative path with no authentication

SPV021

high

API5

Authorization inconsistent within one path

SPV022

high

API5

Anonymous write operation

SPV023

medium

API5

Write operation explicitly public

SPV024

high

API6

Sensitive flow with neither auth nor 429

SPV025

medium

API6

Money-moving POST with no idempotency key

SPV026

high

API7

Caller-supplied URL parameter with no format constraint

SPV027

medium

API7

URL-accepting endpoint with no authentication

SPV028

high

API8

http:// server URL

SPV029

medium

API8

No error response declared

SPV030

low

API8

2xx response with no content

SPV031

medium

API8

Swagger 2.0 — a retired format

SPV032

high

API8

No securitySchemes declared at all

SPV033

low

API8

Wildcard host or unresolved server template

SPV034

low

API9

Deprecated operation still published

SPV035

low

API9

No info.version

SPV036

medium

API10

$ref resolving to a remote URL

specvet explain SPV021 prints the reasoning and the fix for any of them.

Usage

specvet scan openapi.yaml                     # text report
specvet scan ./specs --format json            # walk a directory
specvet scan openapi.yaml --fail-on critical  # only block on the worst
specvet scan openapi.yaml --ignore SPV018     # accept a risk, explicitly
specvet rules --owasp API4:2023
specvet coverage openapi.yaml                 # what share of ops are authenticated
specvet demo > openapi.yaml                   # a spec with every mistake in it

Output is deterministic: the same document produces byte-identical findings. There is no LLM in the loop and no confidence score, because a gate that answers differently on two runs of the same commit is not a gate.

Suppressions

  /health:
    get:                 # specvet: ignore[SPV004] - public by design
      responses: ...

# specvet: ignore[SPV004] drops that rule on that line; a bare # specvet: ignore drops everything on it. Prefer --ignore in CI so the accepted risk is visible in the command, not buried in a comment.

CI

- name: Audit the API contract
  run: |
    pip install specvet
    specvet scan . --fail-on high --format sarif -o specvet.sarif
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: specvet.sarif

MCP

uvx --from "specvet[mcp]" specvet-mcp

Six tools: specvet_scan, specvet_scan_text (audit a spec an agent just generated, no filesystem round trip), specvet_rules, specvet_explain, specvet_coverage, specvet_demo.

Install

uvx --from specvet specvet --version
pip install specvet
git clone https://github.com/xuange-hu/specvet && cd specvet && pip install -e .

Python 3.10+. No runtime dependencies — the YAML and JSON readers are part of the package, which is why pip install specvet adds nothing to your environment.

PyPI status. The Publish job is red for one known reason: PyPI Trusted Publishing has to be registered once in the PyPI UI before the first upload can succeed, and that step is still pending. The tool itself is unaffected — the git clone line above works today.

Design notes

  • Zero dependencies is a feature, not an accident. A contract gate runs in slim containers and on air-gapped runners. Both readers (yamlscan, jsonloc) keep line numbers, which is the only reason they exist instead of PyYAML and json.

  • Evidence over opinion. A finding without a pointer and a line is a suggestion. test_samples.py gives every rule a spec that triggers it, so a rule cannot silently become unreachable.

  • Deterministic or useless. No network, no model, no sampling.

Contributing

git clone https://github.com/xuange-hu/specvet && cd specvet
pytest -q                       # 307 tests
ruff check .

Adding a rule means: an entry in RULES with an OWASP tag, a check function, a sample in tests/test_samples.py, and a row in the table above. The sample is not optional — it is how the rule proves it still works.

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Discovers, inspects, and executes requests against OpenAPI or Swagger APIs directly from documentation URLs or specification files. It enables users to trace parameter usage across an API and perform authenticated HTTP requests through a structured toolset.
    8
    20 npm
    5
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Parses Swagger 2.0 and OpenAPI 3.x specifications, exposing API endpoints, schemas, and authentication through MCP tools with local caching to reduce token usage.
    11
    10 npm
    1
    MIT
  • F
    license
    A
    quality
    B
    maintenance
    Enables LLMs to dereference and query OpenAPI/Swagger specifications, search endpoints and schemas, validate payloads, extract security schemes, and generate production-ready integration code in multiple languages.
    8
    -
  • A
    license
    A
    quality
    C
    maintenance
    Enables local analysis of OpenAPI 3 and Swagger 2 JSON/YAML spec files on disk, with no remote fetching of specs or credentials. It groups endpoints by tag, flags operations that appear to lack security, lists deprecated operations, and produces short markdown cheat sheets for agents.
    4
    MIT