Security-aware OpenAPI contract drift detection that humans can read and CI can enforce.
SpecSentinel compares two OpenAPI 3.x documents and flags client-contract incompatibilities plus declared security access broadening before a change reaches an SDK or production consumer. It understands operations, parameters, recursive request and response schemas, local $ref components, and OpenAPI security alternatives. Reports work in a terminal, pull request, GitHub Code Scanning workflow, or self-contained HTML file.
Explore a live contract-diff report · Run the zero-setup demo · Propose a rule
- Useful signal, not text noise. It compares API semantics instead of diffing YAML lines.
- Security-aware in both directions. It flags newly required credentials as client breaks, and newly anonymous access, removed OAuth/OpenID scopes, or weaker OR alternatives as declared access broadening.
- CI-native. Severity thresholds, stable rule IDs, scoped suppressions, SARIF, and deterministic exit codes are built in.
- Portable. JSON and YAML input, five report formats, Node.js 20+, and one small runtime dependency.
- Embeddable. Use the CLI or import the typed diff engine in a governance tool.
| Compared with | SpecSentinel's focus |
|---|---|
| A line-by-line YAML diff | OpenAPI semantics and client compatibility |
| A generic schema validator | Changes between two valid contracts |
| A breaking-change list only | Directional security alternatives, OAuth/OpenID scopes, stable rule IDs, and actionable locations |
| A CI-only service | The same deterministic engine locally, in Actions, or as a library |
Node.js 20+ is the only requirement. The demo analyzes two bundled OpenAPI contracts, so there are no files to download or configure:
npx --yes github:mockingbird777/specsentinel demoAbridged output:
SpecSentinel 0.2.0
Comparing demo/baseline.yaml → demo/candidate.yaml
[CRITICAL] PATH_REMOVED #/paths/~1legacy
Path '/legacy' was removed.
[HIGH] SECURITY_STRENGTHENED #/paths/~1pets/get/security
Security requirements became stricter for previously valid requests.
[HIGH] SECURITY_ACCESS_BROADENED #/paths/~1reports/get/security
Declared security access broadened: the candidate accepts a credential or scope alternative not accepted by the baseline OpenAPI contract.
…
17 findings (2 critical, 15 high)
The showcase exits successfully so it is safe to paste into a shell. Add --fail-on high to exercise the CI gate and receive exit code 1.
Compare a committed or released contract with the candidate produced by your branch:
npx --yes github:mockingbird777/specsentinel \
api/openapi.baseline.yaml api/openapi.yaml \
--fail-on highFor a pinned project dependency:
npm install --save-dev github:mockingbird777/specsentinel#v0.3.0
npx specsentinel api/openapi.baseline.yaml api/openapi.yaml --fail-on highGenerate a reviewable artifact without changing the gate behavior:
npx specsentinel old.yaml new.yaml --format html --output contract-report.html| Rule ID | Default | What it catches |
|---|---|---|
PATH_REMOVED |
critical | A baseline path disappeared |
OPERATION_REMOVED |
critical | A GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, or TRACE operation disappeared |
PARAM_REQUIRED_ADDED |
high | A required parameter was added or an existing parameter became required |
PARAM_TYPE_CHANGED |
high | An existing parameter changed type |
PARAM_ENUM_NARROWED |
high | Accepted parameter values were removed or an unrestricted parameter gained an enum |
REQUEST_BODY_REQUIRED |
high | A request body became mandatory |
REQUEST_CONTENT_REMOVED |
high | An accepted request media type disappeared |
REQUEST_PROPERTY_REQUIRED |
high | A request property became mandatory, including through local $ref schemas |
REQUEST_TYPE_CHANGED |
high | A request schema type changed recursively |
RESPONSE_REMOVED |
high | A documented status response disappeared |
RESPONSE_CONTENT_REMOVED |
high | A response media type or schema disappeared |
RESPONSE_PROPERTY_REMOVED |
high | A response property disappeared recursively |
RESPONSE_TYPE_CHANGED |
high | A response schema type changed recursively |
SECURITY_STRENGTHENED |
high | Anonymous access/auth alternatives were removed, schemes were added, or OAuth scopes became stricter |
SECURITY_ACCESS_BROADENED |
high | The declared contract newly permits anonymous access, removes required OAuth/OpenID scopes, or adds a weaker authentication alternative |
Every finding contains ruleId, severity, an RFC 6901-style OpenAPI location, a plain-English message, and structured before / after values where applicable.
Security requirement objects are treated as AND requirements and the surrounding security array as OR alternatives. An empty security array or an empty {} alternative permits anonymous access under OpenAPI semantics. SECURITY_ACCESS_BROADENED means only that the candidate contract declares a credential or scope combination that the baseline did not; it is a review signal, not proof that the deployed server has an authorization vulnerability.
OpenAPI 3.1 type arrays are compared as order-insensitive sets for parameters and recursive request/response schemas, so a union reorder is not reported as a change.
# Human-friendly terminal (default)
specsentinel old.yaml new.yaml
# Stable automation payload
specsentinel old.yaml new.yaml --format json --output report.json
# pipe a report in CI ('-' writes to stdout)
specsentinel old.yaml new.yaml --format json --output - | jq .summary
# Pull-request summary
specsentinel old.yaml new.yaml --format markdown --output report.md
# GitHub Code Scanning / security tooling
specsentinel old.yaml new.yaml --format sarif --output report.sarif
# Portable, styled report with no server or assets
specsentinel old.yaml new.yaml --format html --output report.htmlPass a YAML or JSON config with --config. Suppressions should be narrow, reviewed, and temporary where possible.
failOn: high
format: terminal
# Whole-rule suppression
ignoreRules:
- RESPONSE_REMOVED
# Location-scoped suppression; `*` is a wildcard
ignores:
- rule: RESPONSE_PROPERTY_REMOVED
location: '#/paths/~1internal/*'Command-line suppressions are useful for one-off investigations:
specsentinel old.yaml new.yaml --ignore RESPONSE_REMOVED --ignore SECURITY_STRENGTHENEDThe repository ships a Node 20 action whose dependency is bundled into dist/action.cjs:
name: API compatibility
on: [pull_request]
jobs:
contract:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Materialize baseline from the target branch
run: git show "origin/${{ github.base_ref }}:api/openapi.yaml" > /tmp/openapi.baseline.yaml
- name: Guard the contract
uses: mockingbird777/specsentinel@v0.3.0
with:
baseline: /tmp/openapi.baseline.yaml
candidate: api/openapi.yaml
fail-on: high
format: terminalFor Code Scanning, use the CLI to create SARIF and upload it even when findings are present:
- uses: actions/setup-node@v4
with: { node-version: 20 }
- name: Create SARIF
continue-on-error: true
run: npx --yes github:mockingbird777/specsentinel old.yaml new.yaml --format sarif --output specsentinel.sarif
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: specsentinel.sarif }| Code | Meaning |
|---|---|
0 |
No unsuppressed finding meets --fail-on |
1 |
At least one finding meets the severity threshold |
2 |
Invalid CLI usage, unreadable input, malformed config, unsupported external $ref, or invalid OpenAPI document |
The default threshold is high. Choose from info, low, medium, high, or critical.
import { diffOpenApi, parseOpenApi } from 'specsentinel';
const baseline = parseOpenApi(baselineSource, 'baseline.yaml');
const candidate = parseOpenApi(candidateSource, 'candidate.yaml');
const result = diffOpenApi({ baseline, candidate });
for (const change of result.changes) {
console.log(change.ruleId, change.location, change.message);
}SpecSentinel resolves internal JSON Pointer references such as #/components/schemas/Pet and preserves OpenAPI 3.1 $ref siblings. External file and URL references intentionally fail with exit code 2 instead of silently producing an incomplete analysis.
- External multi-file and URL reference graphs with an explicit trust policy
- Discriminator, composition (
allOf/oneOf/anyOf), numeric-bound, and nullable compatibility rules - Baseline acquisition from Git tags and registries
- Inline suppression metadata with expiry dates and ownership
- Policy packs and custom rule plug-ins
Suggested GitHub description: Catch OpenAPI breaking changes and security-sensitive contract drift before they ship.
Suggested topics: openapi, api-governance, contract-testing, breaking-changes, devsecops, sarif, github-actions, typescript. Machine-readable values live in REPO_META.json.
The most useful first contributions are a minimal baseline/candidate pair for a missing compatibility edge case, a false-positive report with a counterexample, or a focused reporter improvement. Start with CONTRIBUTING.md or propose a rule. Read the Code of Conduct, and use the private process in SECURITY.md for vulnerabilities.
If SpecSentinel catches a client break or an unintended access-policy change in a real API, a GitHub star helps other API teams find it.
Released under the MIT License.