specshield 3.3.6 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # SpecShield CLI changelog
2
2
 
3
+ ## 3.4.0 — 2026-07-29 — `govern` command (API governance in the CLI)
4
+
5
+ ### Added
6
+
7
+ - **`specshield govern <spec>` — API governance & linting from the CLI.** Scores an
8
+ OpenAPI spec against SpecShield's governance rules (OWASP + design) via the hosted
9
+ gate (`POST /api/governance/gate`) and prints findings (grouped by severity, with
10
+ rule id + location), a compliance score + A–F grade, and a PASS/FAIL verdict.
11
+ Exit codes match the other gates: `0` pass · `1` fail · `2` error.
12
+ - Flags: `--ruleset <path>` (lint against a Spectral-format ruleset file),
13
+ `--min-score <n>`, `--fail-on-warning`, `--no-fail-on-error`, `--org <key>`
14
+ (apply the org's active waivers), `--advisory` (report but never fail CI),
15
+ `--json`, `--output <file>`, `--api-token`, `--server`.
16
+ - Team plan and above (needs an API key); free/unauthed callers get a friendly
17
+ "requires a paid plan" message and exit `2`.
18
+ - README: new **Governance** section + command reference; the "API governance"
19
+ use case now points at `specshield govern` instead of "not a CLI command yet".
20
+
3
21
  ## 3.3.6 — 2026-07-28 — Docs & platform positioning
4
22
 
5
23
  Documentation-only release — **no CLI behavior change**; existing CI keeps working.
package/README.md CHANGED
@@ -17,6 +17,9 @@
17
17
  **Commands — local & hosted compare**
18
18
  - [Local Compare](#local-compare) · [Remote Compare](#remote-compare) · [Comparison History](#comparison-history) · [Share a Comparison](#share-a-comparison) · [GitHub App PR Checks](#github-app--pr-checks)
19
19
 
20
+ **Governance — lint & gate**
21
+ - [`specshield govern`](#governance--lint-a-spec-and-gate-the-pr)
22
+
20
23
  **Contract Compatibility Testing — the `bdct` commands**
21
24
  - [Overview & full command reference](#bi-directional-contract-testing-bdct)
22
25
  - [`bdct capture from-har` — HAR → consumer contract](#bdct-capture-from-har--turn-real-traffic-into-a-consumer-contract)
@@ -34,7 +37,7 @@
34
37
 
35
38
  ---
36
39
 
37
- > **Contract Compatibility Testing · OpenAPI Diff · Breaking-Change Detection · `can-i-deploy` Deploy Gate · Pact-File Ingest · Live-Traffic Capture · Spec-vs-Production Conformance · GitHub PR Checks**
40
+ > **Contract Compatibility Testing · OpenAPI Diff · Breaking-Change Detection · `can-i-deploy` Deploy Gate · API Governance & Linting · Pact-File Ingest · Live-Traffic Capture · Spec-vs-Production Conformance · GitHub PR Checks**
38
41
 
39
42
  ---
40
43
 
@@ -42,15 +45,16 @@
42
45
 
43
46
  **SpecShield™ is contract compatibility testing for APIs** — catch breaking changes before they reach your consumers, and gate every deploy with `can-i-deploy`. *(Contract compatibility testing is also known as bidirectional contract testing.)*
44
47
 
45
- It's the one CLI that does four things to keep your API safe:
48
+ It's the one CLI that does five things to keep your API safe:
46
49
 
47
50
  1. **Diff** two OpenAPI specs and fail CI on breaking changes.
48
51
  2. **Contract compatibility testing** with `can-i-deploy` — block a deploy that would break a consumer.
49
- 3. **`bdct capture from-har`** — turn recorded traffic into an accurate consumer contract (no Pact DSL).
50
- 4. **`bdct verify-provider`** — prove the running provider actually matches its OpenAPI spec.
52
+ 3. **`govern`** — lint your spec against OWASP + design rules and gate the PR on violations.
53
+ 4. **`bdct capture from-har`** — turn recorded traffic into an accurate consumer contract (no Pact DSL).
54
+ 5. **`bdct verify-provider`** — prove the running provider actually matches its OpenAPI spec.
51
55
 
52
56
  ```
53
- OpenAPI diff + contract compatibility checks + HAR → consumer contract + spec-vs-production conformance — in one CLI.
57
+ OpenAPI diff + contract compatibility checks + API governance + HAR → consumer contract + spec-vs-production conformance — in one CLI.
54
58
  ```
55
59
 
56
60
  No broker. No Pact DSL. Language-agnostic. Works in 30 seconds. Local mode never uploads your specs.
@@ -210,7 +214,7 @@ specshield bdct can-i-deploy --version $GITHUB_SHA
210
214
  - **CI/CD gating** — exit code `1` stops the pipeline automatically.
211
215
  - **Microservices contract safety** — consumers publish what they expect, providers verify they deliver it, no cross-team surprises.
212
216
  - **API drift tracking** — track how your specs change over time across the platform; know what changed, when, and by whom.
213
- - **API governance & linting** — score every spec against OWASP + design rules, adopt standard rule packs or bring your own ruleset, and gate PRs on violations. Available through the [IntelliJ plugin](https://plugins.jetbrains.com/plugin/33137-specshield), the [GitHub Action](https://github.com/marketplace/actions/specshield-bdct), the [MCP server](https://github.com/specshield26/specshield-mcp-server), and the [dashboard](https://specshield.io). *(Not a `specshield` CLI command yet.)*
217
+ - **API governance & linting** — score every spec against OWASP + design rules, bring your own Spectral ruleset, and gate PRs on violations with [`specshield govern`](#governance--lint-a-spec-and-gate-the-pr). The same engine also powers the [IntelliJ plugin](https://plugins.jetbrains.com/plugin/33137-specshield), the [GitHub Action](https://github.com/marketplace/actions/specshield-bdct), the [MCP server](https://github.com/specshield26/specshield-mcp-server), and the [dashboard](https://specshield.io).
214
218
  - **Provider conformance** — make sure your *running* service actually matches its published OpenAPI spec, not just on paper.
215
219
  - **Pact-free consumer contracts** — record real traffic with `capture from-har` and get an OpenAPI consumer contract without writing a single line of Pact DSL.
216
220
 
@@ -357,6 +361,68 @@ github:
357
361
 
358
362
  ---
359
363
 
364
+ ## Governance — lint a spec and gate the PR
365
+
366
+ **Static API governance: score your OpenAPI spec against OWASP + design rules and fail CI on violations.** *(Team plan and above — runs on SpecShield's hosted engine, so it needs an API key.)*
367
+
368
+ ```bash
369
+ # Lint against SpecShield's built-in governance rules
370
+ specshield govern api/openapi.yaml
371
+
372
+ # Lint against a Spectral-format ruleset (OWASP pack, your house style, …)
373
+ specshield govern api/openapi.yaml --ruleset .specshield/ruleset.yaml
374
+
375
+ # Gate CI: require a minimum score, and fail on warnings too
376
+ specshield govern api/openapi.yaml --min-score 80 --fail-on-warning
377
+
378
+ # Report only — print findings but never fail the build
379
+ specshield govern api/openapi.yaml --advisory
380
+ ```
381
+
382
+ Sample output:
383
+
384
+ ```
385
+ SpecShield™ Governance Report
386
+ ─────────────────────────────────────────
387
+ error security-scheme-missing $.paths./payments.post
388
+ operation has no security scheme
389
+ fix: add a security requirement
390
+ info operation-operation-id $.paths./health.get
391
+ operation is missing an operationId
392
+ ─────────────────────────────────────────
393
+ Score 74/100 · grade C · 1 error · 0 warning · 1 info
394
+
395
+ ✖ FAIL — governance policy not met:
396
+ • 1 error-severity finding
397
+ ```
398
+
399
+ Exit codes: `0` = passed the gate · `1` = failed the gate · `2` = error (invalid spec, missing token, or paid-plan required).
400
+
401
+ | Flag | Purpose | Default |
402
+ |---|---|---|
403
+ | `--ruleset <path>` | Lint against a Spectral-format ruleset file (uses the ruleset engine). | built-in rules |
404
+ | `--min-score <n>` | Minimum compliance score (0–100) required to pass. | `70` |
405
+ | `--fail-on-warning` | Fail the gate when any warning-severity finding is present. | off |
406
+ | `--no-fail-on-error` | Do **not** fail the gate on error-severity findings. | fails on error |
407
+ | `--org <key>` | Apply your org's active governance waivers to the verdict. | none |
408
+ | `--advisory` | Report findings but always exit `0` (never fail CI). | off |
409
+ | `--json` | Machine-readable JSON output (the full gate response). | off |
410
+ | `--output <file>` | Also save the JSON result to a file. | — |
411
+ | `--api-token <token>` | API token (else `SPECSHIELD_API_KEY` / stored config). | — |
412
+ | `--server <url>` | Override the SpecShield server URL (self-hosted / staging). | `https://specshield.io` |
413
+
414
+ Run it in CI right after your build:
415
+
416
+ ```yaml
417
+ - name: Govern the API spec
418
+ env: { SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }} }
419
+ run: specshield govern api/openapi.yaml --min-score 80
420
+ ```
421
+
422
+ > The same governance engine powers the [IntelliJ plugin](https://plugins.jetbrains.com/plugin/33137-specshield), the [GitHub Action](https://github.com/marketplace/actions/specshield-bdct), the [MCP server](https://github.com/specshield26/specshield-mcp-server), and the [dashboard](https://specshield.io) — with catalog scorecards, waivers, and SARIF/JUnit/HTML reports.
423
+
424
+ ---
425
+
360
426
  # Bi-Directional Contract Testing (BDCT)
361
427
 
362
428
  **Compatibility without Pact's broker overhead or DSL.**
@@ -1347,6 +1413,23 @@ specshield share <reportId | base.yaml target.yaml> [options]
1347
1413
  | `--expires <days>` | Make the link expire after N days (default: never) |
1348
1414
  | `--api-key <key>` | Override stored API key |
1349
1415
 
1416
+ ```bash
1417
+ specshield govern <spec> [options]
1418
+ ```
1419
+
1420
+ | Option | Description |
1421
+ |---|---|
1422
+ | `--ruleset <path>` | Lint against a Spectral-format ruleset file |
1423
+ | `--min-score <n>` | Minimum compliance score (0–100) to pass (default 70) |
1424
+ | `--fail-on-warning` | Fail the gate on any warning-severity finding |
1425
+ | `--no-fail-on-error` | Do not fail the gate on error-severity findings |
1426
+ | `--org <key>` | Apply the org's active governance waivers |
1427
+ | `--advisory` | Report findings but always exit 0 |
1428
+ | `--json` | Machine-readable JSON output |
1429
+ | `--output <file>` | Save the JSON result to a file |
1430
+ | `--api-token <token>` | API token (overrides env / stored config) |
1431
+ | `--server <url>` | Override SpecShield server URL |
1432
+
1350
1433
  ```bash
1351
1434
  specshield bdct <subcommand> [options]
1352
1435
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specshield",
3
- "version": "3.3.6",
3
+ "version": "3.4.0",
4
4
  "description": "Contract compatibility testing for APIs — catch breaking OpenAPI changes before they reach your consumers, with can-i-deploy deploy gating and GitHub PR checks. (a.k.a. bidirectional contract testing.)",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -108,6 +108,19 @@ async function listConsumerContracts(server, apiToken, { org, consumer, provider
108
108
  } catch (err) { throw apiError(err); }
109
109
  }
110
110
 
111
+ /**
112
+ * Run the governance deploy gate on a spec. Always returns HTTP 200 with a
113
+ * pass/fail verdict in the body (a failed gate is a valid result, not an error),
114
+ * so only real failures (400 invalid spec, 402 paid-plan, 403 org, 503 disabled,
115
+ * network) throw. `payload` = { spec, ruleset?, policy?, orgKey? }.
116
+ */
117
+ async function governanceGate(server, apiToken, payload) {
118
+ try {
119
+ const res = await buildClient(server, apiToken).post('/api/governance/gate', payload);
120
+ return res.data;
121
+ } catch (err) { throw apiError(err); }
122
+ }
123
+
111
124
  module.exports = {
112
125
  publishProviderSpec,
113
126
  publishConsumerContract,
@@ -117,4 +130,5 @@ module.exports = {
117
130
  getMatrix,
118
131
  listProviderSpecs,
119
132
  listConsumerContracts,
133
+ governanceGate,
120
134
  };
package/src/cli.js CHANGED
@@ -7,6 +7,7 @@ const initCommand = require('./commands/init');
7
7
  const loginCommand = require('./commands/login');
8
8
  const logoutCommand = require('./commands/logout');
9
9
  const bdctCommand = require('./commands/bdct');
10
+ const governCommand = require('./commands/govern');
10
11
  const historyCommand = require('./commands/history');
11
12
  const shareCommand = require('./commands/share');
12
13
  const whoamiCommand = require('./commands/whoami');
@@ -27,6 +28,7 @@ program.addCommand(initCommand);
27
28
  program.addCommand(loginCommand);
28
29
  program.addCommand(logoutCommand);
29
30
  program.addCommand(bdctCommand);
31
+ program.addCommand(governCommand);
30
32
  program.addCommand(historyCommand);
31
33
  program.addCommand(shareCommand);
32
34
  program.addCommand(whoamiCommand);
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+
3
+ const { Command } = require('commander');
4
+ const chalk = require('chalk');
5
+ const ora = require('ora');
6
+ const fsExtra = require('fs-extra');
7
+ const logger = require('../utils/logger');
8
+ const { loadSpec } = require('../core/loadSpec');
9
+ const { getStoredApiKey } = require('../config/localConfig');
10
+ const { governanceGate } = require('../api/bdctClient');
11
+
12
+ // Token precedence mirrors the bdct subcommands.
13
+ async function resolveApiToken(opts) {
14
+ return opts.apiToken || process.env.SPECSHIELD_API_KEY || (await getStoredApiKey()) || null;
15
+ }
16
+
17
+ const SEV_ORDER = { error: 0, warning: 1, info: 2 };
18
+
19
+ function sevLabel(sev) {
20
+ const s = String(sev || '').toLowerCase();
21
+ if (s === 'error') return chalk.red('error ');
22
+ if (s === 'warning') return chalk.yellow('warning');
23
+ return chalk.blue('info ');
24
+ }
25
+
26
+ function gradeColor(grade) {
27
+ const g = String(grade || '—').toUpperCase();
28
+ if (g === 'A' || g === 'B') return chalk.green(g);
29
+ if (g === 'C') return chalk.yellow(g);
30
+ return chalk.red(g);
31
+ }
32
+
33
+ const govern = new Command('govern')
34
+ .description('Lint an OpenAPI spec against governance rules (OWASP + design) and gate the PR')
35
+ .argument('<spec>', 'Path to the OpenAPI spec to govern (YAML or JSON)')
36
+ .option('--ruleset <path>', 'Lint against a Spectral-format ruleset file (uses the ruleset engine)')
37
+ .option('--min-score <n>', 'Minimum compliance score (0–100) required to pass (default 70)', (v) => parseInt(v, 10))
38
+ .option('--fail-on-warning', 'Fail the gate when any warning-severity finding is present')
39
+ .option('--no-fail-on-error', 'Do NOT fail the gate on error-severity findings')
40
+ .option('--org <key>', 'Org key — applies your org’s active governance waivers to the verdict')
41
+ .option('--advisory', 'Report findings but always exit 0 (never fail CI)')
42
+ .option('--json', 'Output machine-readable JSON')
43
+ .option('--output <file>', 'Also save the JSON result to this file')
44
+ .option('--server <url>', 'SpecShield server URL')
45
+ .option('--api-token <token>', 'API token (overrides env / stored config)')
46
+ .action(async (specPath, opts) => {
47
+ try {
48
+ const token = await resolveApiToken(opts);
49
+ if (!token) {
50
+ logger.error('No API token found. Pass --api-token, set SPECSHIELD_API_KEY, or run: specshield login --api-key <KEY>');
51
+ process.exit(2);
52
+ }
53
+
54
+ const spinner = opts.json ? null : ora('Loading spec...').start();
55
+ const specText = await loadSpec(specPath);
56
+ const ruleset = opts.ruleset ? await loadSpec(opts.ruleset) : null;
57
+
58
+ // Only send policy fields the user overrode; the server applies documented
59
+ // defaults (minScore 70, failOnError true, failOnWarning false) otherwise.
60
+ const policy = {};
61
+ if (opts.minScore !== undefined && !Number.isNaN(opts.minScore)) policy.minScore = opts.minScore;
62
+ if (opts.failOnWarning) policy.failOnWarning = true;
63
+ if (opts.failOnError === false) policy.failOnError = false; // set by --no-fail-on-error
64
+
65
+ if (spinner) spinner.text = 'Running governance review...';
66
+ const resp = await governanceGate(opts.server, token, {
67
+ spec: specText,
68
+ ruleset,
69
+ policy: Object.keys(policy).length ? policy : null,
70
+ orgKey: opts.org || null,
71
+ });
72
+ if (spinner) spinner.stop();
73
+
74
+ if (opts.json) {
75
+ const out = JSON.stringify(resp, null, 2);
76
+ process.stdout.write(out + '\n');
77
+ if (opts.output) await fsExtra.outputFile(opts.output, out);
78
+ } else {
79
+ renderHuman(resp);
80
+ if (opts.output) await fsExtra.outputFile(opts.output, JSON.stringify(resp, null, 2));
81
+ }
82
+
83
+ if (opts.advisory) process.exit(0);
84
+ process.exit(resp.passed ? 0 : 1);
85
+ } catch (err) {
86
+ if (err.status === 402) {
87
+ logger.error('API governance requires a paid plan (Team or above). See https://specshield.io/pricing');
88
+ process.exit(2);
89
+ }
90
+ if (err.status === 503) {
91
+ logger.error('Governance is not enabled on this SpecShield server.');
92
+ process.exit(2);
93
+ }
94
+ logger.error(`Error: ${err.message}`);
95
+ process.exit(2);
96
+ }
97
+ });
98
+
99
+ function renderHuman(resp) {
100
+ const review = resp.review || {};
101
+ const findings = Array.isArray(review.findings) ? review.findings.slice() : [];
102
+ const score = review.score || {};
103
+
104
+ process.stdout.write('\n ' + chalk.bold('SpecShield™ Governance Report') + '\n');
105
+ process.stdout.write(' ' + chalk.gray('─────────────────────────────────────────') + '\n');
106
+
107
+ if (findings.length === 0) {
108
+ process.stdout.write(' ' + chalk.green('✔ No governance findings.') + '\n');
109
+ } else {
110
+ findings.sort((a, b) =>
111
+ (SEV_ORDER[String(a.severity).toLowerCase()] ?? 3) - (SEV_ORDER[String(b.severity).toLowerCase()] ?? 3));
112
+ for (const f of findings) {
113
+ const loc = f.location ? chalk.gray(' ' + f.location) : '';
114
+ process.stdout.write(` ${sevLabel(f.severity)} ${chalk.cyan(f.ruleId || '')}${loc}\n`);
115
+ if (f.message) process.stdout.write(` ${f.message}\n`);
116
+ if (f.suggestedFix) process.stdout.write(` ${chalk.gray('fix: ' + f.suggestedFix)}\n`);
117
+ }
118
+ }
119
+
120
+ process.stdout.write(' ' + chalk.gray('─────────────────────────────────────────') + '\n');
121
+ const scoreVal = (score.value === undefined || score.value === null) ? '—' : score.value;
122
+ process.stdout.write(
123
+ ` Score ${chalk.bold(scoreVal)}/100 · grade ${gradeColor(score.grade)} · `
124
+ + `${chalk.red((review.errorCount || 0) + ' error')} · `
125
+ + `${chalk.yellow((review.warningCount || 0) + ' warning')} · `
126
+ + `${chalk.blue((review.infoCount || 0) + ' info')}`
127
+ + (resp.waivedCount ? ` · ${chalk.gray(resp.waivedCount + ' waived')}` : '')
128
+ + '\n'
129
+ );
130
+
131
+ if (resp.passed) {
132
+ process.stdout.write('\n ' + chalk.green.bold('✔ PASS')
133
+ + chalk.gray(` — meets the governance policy (min score ${resp.scoreThreshold}).`) + '\n\n');
134
+ } else {
135
+ process.stdout.write('\n ' + chalk.red.bold('✖ FAIL') + chalk.gray(' — governance policy not met:') + '\n');
136
+ for (const r of (resp.reasons || [])) process.stdout.write(' ' + chalk.red('•') + ' ' + r + '\n');
137
+ if (resp.recommendedAction) process.stdout.write('\n ' + chalk.gray(resp.recommendedAction) + '\n');
138
+ process.stdout.write('\n');
139
+ }
140
+ }
141
+
142
+ module.exports = govern;