@kirrosh/zond 0.23.0 → 0.26.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 +164 -1
- package/README.md +8 -7
- package/package.json +2 -3
- package/src/CLAUDE.md +112 -0
- package/src/cli/commands/add-api.ts +19 -7
- package/src/cli/commands/api/annotate/index.ts +359 -4
- package/src/cli/commands/api/annotate/lifecycle.ts +1 -1
- package/src/cli/commands/api/annotate/overlay.ts +1 -1
- package/src/cli/commands/api/annotate/pagination.ts +10 -6
- package/src/cli/commands/api/annotate/prompts.ts +39 -2
- package/src/cli/commands/audit.ts +360 -54
- package/src/cli/commands/check.ts +15 -2
- package/src/cli/commands/checks.ts +352 -36
- package/src/cli/commands/cleanup.ts +4 -30
- package/src/cli/commands/coverage.ts +275 -57
- package/src/cli/commands/db.ts +311 -8
- package/src/cli/commands/discover.ts +281 -161
- package/src/cli/commands/doctor.ts +57 -3
- package/src/cli/commands/fixtures.ts +1 -1
- package/src/cli/commands/generate.ts +24 -7
- package/src/cli/commands/init/bootstrap.ts +4 -1
- package/src/cli/commands/init/index.ts +2 -2
- package/src/cli/commands/init/skills.ts +43 -0
- package/src/cli/commands/init/templates/agents.md +12 -3
- package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +268 -44
- package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +88 -26
- package/src/cli/commands/init/templates/skills/zond.md +274 -64
- package/src/cli/commands/init/templates/zond-config.yml +1 -1
- package/src/cli/commands/prepare-fixtures.ts +14 -52
- package/src/cli/commands/probe/_seed-bodies.ts +52 -0
- package/src/cli/commands/probe/mass-assignment.ts +101 -10
- package/src/cli/commands/probe/security.ts +95 -12
- package/src/cli/commands/probe/webhooks.ts +2 -0
- package/src/cli/commands/probe.ts +87 -11
- package/src/cli/commands/refresh-api.ts +59 -1
- package/src/cli/commands/report-bundle.ts +3 -11
- package/src/cli/commands/request.ts +116 -0
- package/src/cli/commands/run.ts +33 -5
- package/src/cli/commands/schema-from-runs.ts +128 -0
- package/src/cli/commands/secrets.ts +133 -0
- package/src/cli/json-envelope.ts +0 -20
- package/src/cli/json-schemas.ts +51 -0
- package/src/cli/output.ts +17 -1
- package/src/cli/program.ts +5 -4
- package/src/cli/safe-live.ts +24 -0
- package/src/cli/status-filter.ts +0 -10
- package/src/core/audit/persist.ts +183 -0
- package/src/core/checks/budget.ts +59 -0
- package/src/core/checks/checks/cross_call_references.ts +17 -4
- package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
- package/src/core/checks/checks/idempotency_replay.ts +1 -5
- package/src/core/checks/checks/ignored_auth.ts +44 -1
- package/src/core/checks/checks/index.ts +3 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +169 -26
- package/src/core/checks/checks/negative_data_rejection.ts +119 -16
- package/src/core/checks/checks/not_a_server_error.ts +8 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +47 -18
- package/src/core/checks/checks/pagination_invariants.ts +298 -117
- package/src/core/checks/checks/positive_data_acceptance.ts +1 -4
- package/src/core/checks/checks/status_code_conformance.ts +78 -7
- package/src/core/checks/mode.ts +3 -0
- package/src/core/checks/recommended-action.ts +5 -1
- package/src/core/checks/runner.ts +614 -27
- package/src/core/checks/spec-findings.ts +308 -0
- package/src/core/checks/types.ts +117 -1
- package/src/core/checks/zond-extensions.ts +73 -0
- package/src/core/classifier/recommended-action.ts +35 -6
- package/src/core/coverage/loader.ts +31 -0
- package/src/core/diagnostics/db-analysis.ts +200 -106
- package/src/core/diagnostics/failure-class.ts +21 -1
- package/src/core/diagnostics/failure-hints.ts +4 -208
- package/src/core/diagnostics/suggested-fixes.ts +2 -3
- package/src/core/generator/chunker.ts +1 -8
- package/src/core/generator/data-factory.ts +199 -61
- package/src/core/generator/fixtures-builder.ts +38 -31
- package/src/core/generator/index.ts +0 -2
- package/src/core/generator/openapi-reader.ts +98 -4
- package/src/core/generator/path-param-disambig.ts +30 -4
- package/src/core/generator/resources-builder.ts +276 -26
- package/src/core/generator/schema-utils.ts +22 -0
- package/src/core/generator/suite-generator.ts +168 -15
- package/src/core/generator/types.ts +6 -0
- package/src/core/identity/identity-file.ts +0 -0
- package/src/core/output/README.md +11 -29
- package/src/core/output/index.ts +1 -1
- package/src/core/output/run.ts +0 -35
- package/src/core/output/types.ts +0 -7
- package/src/core/parser/dynamic-values.ts +160 -0
- package/src/core/parser/variables.ts +0 -0
- package/src/core/probe/dry-run-envelope.ts +4 -0
- package/src/core/probe/mass-assignment/classify.ts +175 -0
- package/src/core/probe/mass-assignment/cleanup.ts +52 -0
- package/src/core/probe/mass-assignment/digest.ts +114 -0
- package/src/core/probe/mass-assignment/orchestrator.ts +459 -0
- package/src/core/probe/mass-assignment/regression.ts +141 -0
- package/src/core/probe/mass-assignment/suspects.ts +92 -0
- package/src/core/probe/mass-assignment/types.ts +135 -0
- package/src/core/probe/mass-assignment-probe.ts +23 -1118
- package/src/core/probe/mass-assignment-template.ts +32 -4
- package/src/core/probe/path-discovery.ts +3 -4
- package/src/core/probe/probe-harness.ts +21 -22
- package/src/core/probe/security/baseline.ts +174 -0
- package/src/core/probe/security/classify.ts +341 -0
- package/src/core/probe/security/cleanup.ts +125 -0
- package/src/core/probe/security/detectors.ts +71 -0
- package/src/core/probe/security/digest.ts +104 -0
- package/src/core/probe/security/orchestrator.ts +398 -0
- package/src/core/probe/security/regression.ts +103 -0
- package/src/core/probe/security/types.ts +151 -0
- package/src/core/probe/security-probe-class.ts +8 -2
- package/src/core/probe/security-probe.ts +28 -1449
- package/src/core/probe/shared.ts +26 -0
- package/src/core/probe/webhooks-probe.ts +5 -7
- package/src/core/runner/assertions.ts +1 -1
- package/src/core/runner/executor.ts +3 -18
- package/src/core/runner/form-encode.ts +8 -18
- package/src/core/runner/http-client.ts +38 -1
- package/src/core/runner/preflight-vars.ts +19 -15
- package/src/core/runner/rate-limiter.ts +11 -29
- package/src/core/runner/run-kind.ts +7 -1
- package/src/core/runner/schema-validator.ts +2 -6
- package/src/core/runner/send-request.ts +11 -6
- package/src/core/runner/types.ts +6 -0
- package/src/core/setup-api.ts +53 -15
- package/src/core/severity/index.ts +0 -63
- package/src/core/spec/infer-schema.ts +102 -0
- package/src/core/spec/merge-specs.ts +156 -0
- package/src/core/spec/schema-from-runs.ts +117 -0
- package/src/core/spec/schema-overlay.ts +130 -0
- package/src/core/util/ajv.ts +13 -0
- package/src/core/util/headers.ts +9 -0
- package/src/core/util/url.ts +24 -0
- package/src/core/workspace/fixture-gap-report.ts +84 -0
- package/src/core/workspace/fixture-gaps.ts +71 -0
- package/src/core/workspace/root.ts +13 -11
- package/src/db/migrate.ts +2 -0
- package/src/db/migrations/0002_run_kind_request.sql +59 -0
- package/src/db/queries/collections.ts +2 -2
- package/src/db/queries/results.ts +88 -0
- package/src/db/queries/runs.ts +56 -2
- package/src/db/queries.ts +3 -0
- package/src/db/schema.ts +7 -7
- package/src/cli/commands/bootstrap.ts +0 -710
- package/src/core/anti-fp/bootstrap.ts +0 -34
- package/src/core/anti-fp/index.ts +0 -33
- package/src/core/anti-fp/registry.ts +0 -44
- package/src/core/anti-fp/rules/baseline-echo.ts +0 -74
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +0 -52
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +0 -38
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +0 -35
- package/src/core/anti-fp/rules/schemathesis/index.ts +0 -24
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +0 -53
- package/src/core/anti-fp/rules/subscription-gated/index.ts +0 -11
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +0 -75
- package/src/core/anti-fp/types.ts +0 -68
- package/src/core/generator/create-body.ts +0 -89
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Оборачивает 8-10 ручных шагов (prepare-fixtures → generate → probes
|
|
5
5
|
* → session-wrapped run → coverage → HTML report) в одну команду:
|
|
6
6
|
*
|
|
7
|
-
* 1. `prepare-fixtures --apply`
|
|
8
|
-
*
|
|
7
|
+
* 1. `prepare-fixtures --apply` — single-pass, заполняет `.env.yaml`
|
|
8
|
+
* FK-идентификаторами, что нашлись детерминированно.
|
|
9
9
|
* 2. `generate` — пропускается если `apis/<name>/tests/` свежее, чем
|
|
10
10
|
* `spec.json` (mtime-эвристика; `--force` отключает skip).
|
|
11
11
|
* 3. `probe static` (validation+methods, всегда). `mass-assignment` и
|
|
@@ -23,16 +23,19 @@
|
|
|
23
23
|
|
|
24
24
|
import { existsSync, statSync } from "node:fs";
|
|
25
25
|
import { writeFile } from "node:fs/promises";
|
|
26
|
-
import { join } from "node:path";
|
|
26
|
+
import { join, resolve } from "node:path";
|
|
27
27
|
import type { Command } from "commander";
|
|
28
28
|
import { globalJson } from "../resolve.ts";
|
|
29
29
|
import { getDb } from "../../db/schema.ts";
|
|
30
|
-
import { findCollectionByNameOrId } from "../../db/queries.ts";
|
|
30
|
+
import { findCollectionByNameOrId, listSessions, listRunsBySession } from "../../db/queries.ts";
|
|
31
31
|
import { resolveCollectionSpec } from "../../core/setup-api.ts";
|
|
32
32
|
import { printSuccess, printWarning, printError } from "../output.ts";
|
|
33
33
|
import { getApi, MISSING_API_MESSAGE } from "../util/api-context.ts";
|
|
34
34
|
import { jsonOk, printJson } from "../json-envelope.ts";
|
|
35
35
|
import { VERSION } from "../version.ts";
|
|
36
|
+
import { diagnoseRun, type DiagnoseResult } from "../../core/diagnostics/db-analysis.ts";
|
|
37
|
+
import { readCurrentSession } from "../../core/context/session.ts";
|
|
38
|
+
import { resolveBudget, isBudget, BUDGETS, type Budget } from "../../core/checks/budget.ts";
|
|
36
39
|
|
|
37
40
|
interface Stage {
|
|
38
41
|
key: string;
|
|
@@ -54,13 +57,19 @@ interface StageResult {
|
|
|
54
57
|
export interface AuditOptions {
|
|
55
58
|
api: string;
|
|
56
59
|
dbPath?: string;
|
|
57
|
-
seed?: boolean;
|
|
58
60
|
withMassAssignment?: boolean;
|
|
59
61
|
withSecurity?: boolean;
|
|
60
62
|
out?: string;
|
|
61
63
|
dryRun?: boolean;
|
|
62
64
|
force?: boolean;
|
|
63
65
|
json?: boolean;
|
|
66
|
+
/** ARV-264: opt-in to the full pipeline against a real-traffic API.
|
|
67
|
+
* When false (default), audit runs in safe mode — no mass-assignment /
|
|
68
|
+
* security probes, no destructive probes. */
|
|
69
|
+
live?: boolean;
|
|
70
|
+
/** ARV-292: adaptive cap and stateful gating tier. Translates to
|
|
71
|
+
* `--max-requests N` on every spawned `run` stage. */
|
|
72
|
+
budget?: Budget;
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
/**
|
|
@@ -79,20 +88,34 @@ function zondInvoker(): string[] {
|
|
|
79
88
|
function buildStages(opts: AuditOptions, apiDir: string, specPath: string | null): Stage[] {
|
|
80
89
|
const api = opts.api;
|
|
81
90
|
const stages: Stage[] = [];
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
const budgetResolved = resolveBudget(opts.budget, undefined);
|
|
92
|
+
const runMaxRequestsArgs: string[] = budgetResolved.maxRequests !== undefined
|
|
93
|
+
? ["--max-requests", String(budgetResolved.maxRequests)]
|
|
94
|
+
// placeholder to keep the binding shape stable
|
|
95
|
+
: [];
|
|
96
|
+
// ARV-302: probes don't share `zond run`'s --max-requests vocabulary
|
|
97
|
+
// (probes scan endpoint-by-endpoint, not request-by-request). Map the
|
|
98
|
+
// budget tier to a coarse `--max-endpoints` cap so probe stages stay
|
|
99
|
+
// inside the same wall-clock budget. `full` keeps the legacy
|
|
100
|
+
// uncapped behaviour.
|
|
101
|
+
const probeMaxEndpointsByTier: Record<string, number | undefined> = {
|
|
102
|
+
quick: 10,
|
|
103
|
+
standard: 50,
|
|
104
|
+
full: undefined,
|
|
105
|
+
};
|
|
106
|
+
const probeMaxEndpoints = opts.budget ? probeMaxEndpointsByTier[opts.budget] : undefined;
|
|
107
|
+
const probeMaxEndpointsArgs: string[] = probeMaxEndpoints !== undefined
|
|
108
|
+
? ["--max-endpoints", String(probeMaxEndpoints)]
|
|
109
|
+
: [];
|
|
110
|
+
|
|
111
|
+
// ARV-336: prep is always a single-pass `prepare-fixtures --apply`. It
|
|
112
|
+
// fills what discover resolves deterministically and reports the rest as
|
|
113
|
+
// gaps for the agent/user to fill by hand — no autonomous seed/cascade.
|
|
114
|
+
stages.push({
|
|
115
|
+
key: "prepare-fixtures",
|
|
116
|
+
name: "prepare-fixtures (path-FK fixtures)",
|
|
117
|
+
args: ["prepare-fixtures", "--api", api, "--apply"],
|
|
118
|
+
});
|
|
96
119
|
|
|
97
120
|
stages.push({
|
|
98
121
|
key: "generate",
|
|
@@ -120,7 +143,11 @@ function buildStages(opts: AuditOptions, apiDir: string, specPath: string | null
|
|
|
120
143
|
args: ["probe", "static", "--api", api, "--output", join(apiDir, "probes", "static")],
|
|
121
144
|
});
|
|
122
145
|
|
|
123
|
-
|
|
146
|
+
// ARV-264: mass-assignment + security probes send real POST/PUT/DELETE
|
|
147
|
+
// traffic. Gate them behind --live so the default (safe) audit can't
|
|
148
|
+
// pollute a production API.
|
|
149
|
+
const liveProbesEnabled = opts.live === true;
|
|
150
|
+
if (opts.withMassAssignment && liveProbesEnabled) {
|
|
124
151
|
stages.push({
|
|
125
152
|
key: "probe-mass-assignment",
|
|
126
153
|
name: "probe mass-assignment",
|
|
@@ -129,10 +156,11 @@ function buildStages(opts: AuditOptions, apiDir: string, specPath: string | null
|
|
|
129
156
|
"--output", join(apiDir, "probes", "mass-assignment-digest.md"),
|
|
130
157
|
"--emit-tests", join(apiDir, "probes", "mass-assignment"),
|
|
131
158
|
"--overwrite",
|
|
159
|
+
...probeMaxEndpointsArgs,
|
|
132
160
|
],
|
|
133
161
|
});
|
|
134
162
|
}
|
|
135
|
-
if (opts.withSecurity) {
|
|
163
|
+
if (opts.withSecurity && liveProbesEnabled) {
|
|
136
164
|
stages.push({
|
|
137
165
|
key: "probe-security",
|
|
138
166
|
name: "probe security (ssrf,crlf,open-redirect)",
|
|
@@ -141,15 +169,39 @@ function buildStages(opts: AuditOptions, apiDir: string, specPath: string | null
|
|
|
141
169
|
"--output", join(apiDir, "probes", "security-digest.md"),
|
|
142
170
|
"--emit-tests", join(apiDir, "probes", "security"),
|
|
143
171
|
"--overwrite",
|
|
172
|
+
...probeMaxEndpointsArgs,
|
|
144
173
|
],
|
|
145
174
|
});
|
|
146
175
|
}
|
|
147
176
|
|
|
177
|
+
// ARV-65: if the user already has an active session, REUSE it — don't
|
|
178
|
+
// clobber their .zond/current-session by spawning audit's own. Skipping
|
|
179
|
+
// session-end is the critical half: otherwise audit clears the user's
|
|
180
|
+
// session even when start was a no-op.
|
|
181
|
+
const existingSession = readCurrentSession();
|
|
148
182
|
const sessionLabel = `audit-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)}`;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
183
|
+
if (existingSession) {
|
|
184
|
+
const reuseReason = `reusing active session ${existingSession.id}${existingSession.label ? ` (${existingSession.label})` : ""}`;
|
|
185
|
+
stages.push({
|
|
186
|
+
key: "session-start",
|
|
187
|
+
name: "session start (reused)",
|
|
188
|
+
args: ["session", "start", "--label", sessionLabel],
|
|
189
|
+
skip: () => reuseReason,
|
|
190
|
+
});
|
|
191
|
+
stages.push({ key: "run-tests", name: "run tests", args: ["run", join(apiDir, "tests"), "--api", api, ...runMaxRequestsArgs] });
|
|
192
|
+
stages.push({ key: "run-probes", name: "run probes", args: ["run", join(apiDir, "probes"), "--api", api, ...runMaxRequestsArgs] });
|
|
193
|
+
stages.push({
|
|
194
|
+
key: "session-end",
|
|
195
|
+
name: "session end (reused — kept active)",
|
|
196
|
+
args: ["session", "end"],
|
|
197
|
+
skip: () => reuseReason,
|
|
198
|
+
});
|
|
199
|
+
} else {
|
|
200
|
+
stages.push({ key: "session-start", name: `session start (${sessionLabel})`, args: ["session", "start", "--label", sessionLabel] });
|
|
201
|
+
stages.push({ key: "run-tests", name: "run tests", args: ["run", join(apiDir, "tests"), "--api", api, ...runMaxRequestsArgs] });
|
|
202
|
+
stages.push({ key: "run-probes", name: "run probes", args: ["run", join(apiDir, "probes"), "--api", api, ...runMaxRequestsArgs] });
|
|
203
|
+
stages.push({ key: "session-end", name: "session end", args: ["session", "end"] });
|
|
204
|
+
}
|
|
153
205
|
// ARV-108: surface the post-stage coverage capture in the plan so the
|
|
154
206
|
// dry-run listing matches the actual pipeline. The stage is special-cased
|
|
155
207
|
// in auditCommand — we keep stdout for JSON parsing rather than inheriting.
|
|
@@ -174,13 +226,15 @@ async function runStage(stage: Stage, idx: number, total: number, json: boolean)
|
|
|
174
226
|
const proc = Bun.spawn(cmd, { stdout: "inherit", stderr: "inherit" });
|
|
175
227
|
const code = await proc.exited;
|
|
176
228
|
const ms = Date.now() - t0;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
229
|
+
const status: StageResult["status"] = code === 0 ? "ok" : "failed";
|
|
230
|
+
// ARV-66: print a per-stage completion line so the user sees OK/FAIL inline
|
|
231
|
+
// next to "Stage N/M" instead of inferring from the final summary. Subprocess
|
|
232
|
+
// stdio is inherited above, so this line lands AFTER the stage's own output.
|
|
233
|
+
if (!json) {
|
|
234
|
+
const tag = status === "ok" ? "OK" : `FAIL (exit ${code})`;
|
|
235
|
+
console.log(` └─ ${tag} · ${(ms / 1000).toFixed(1)}s`);
|
|
236
|
+
}
|
|
237
|
+
return { key: stage.key, name: stage.name, status, exit_code: code, duration_ms: ms };
|
|
184
238
|
}
|
|
185
239
|
|
|
186
240
|
interface CoverageCapture {
|
|
@@ -190,22 +244,63 @@ interface CoverageCapture {
|
|
|
190
244
|
durationMs: number;
|
|
191
245
|
}
|
|
192
246
|
|
|
193
|
-
|
|
247
|
+
/**
|
|
248
|
+
* ARV-301: build the coverage-stage CLI args. Exported so unit tests
|
|
249
|
+
* can assert that audit pins `--session-id <id>` rather than
|
|
250
|
+
* `--union session` whenever a session id was captured ahead of
|
|
251
|
+
* `session end` (the latter selector rejects closed sessions).
|
|
252
|
+
*/
|
|
253
|
+
export function buildCoverageStageArgs(api: string, sessionId?: string): string[] {
|
|
254
|
+
const sel = sessionId ? ["--session-id", sessionId] : ["--union", "session"];
|
|
255
|
+
return ["coverage", "--api", api, ...sel, "--json"];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* ARV-301 follow-up: judge audit's coverage stage on the JSON envelope's
|
|
260
|
+
* `ok` field, not the subprocess exit code.
|
|
261
|
+
*
|
|
262
|
+
* `zond coverage` exits 1 when there are uncovered endpoints (legacy
|
|
263
|
+
* CI-gate behaviour) — that is a normal report state for audit's
|
|
264
|
+
* purposes, the envelope is well-formed and carries valid coverage
|
|
265
|
+
* data. Only exit 2 / non-JSON output / `ok:false` envelopes mean a
|
|
266
|
+
* real coverage failure.
|
|
267
|
+
*
|
|
268
|
+
* Exported pure function so unit tests can lock the contract without
|
|
269
|
+
* spawning a subprocess.
|
|
270
|
+
*/
|
|
271
|
+
export function interpretCoverageOutput(
|
|
272
|
+
stdout: string,
|
|
273
|
+
exitCode: number | null,
|
|
274
|
+
): { data: unknown | null; parseError: string | null } {
|
|
275
|
+
let parsed: unknown = null;
|
|
276
|
+
let parseError: string | null = null;
|
|
277
|
+
try {
|
|
278
|
+
parsed = stdout.trim() ? JSON.parse(stdout) : null;
|
|
279
|
+
} catch (e) {
|
|
280
|
+
parseError = (e as Error).message;
|
|
281
|
+
}
|
|
282
|
+
if (parsed && typeof parsed === "object" && (parsed as { ok?: boolean }).ok === true) {
|
|
283
|
+
// Envelope says everything is fine; treat coverage as captured
|
|
284
|
+
// even if exit was 1 due to "has uncovered endpoints".
|
|
285
|
+
return { data: parsed, parseError: null };
|
|
286
|
+
}
|
|
287
|
+
// ok:false, or no envelope at all — propagate the failure signal.
|
|
288
|
+
// exitCode is intentionally not consulted here; callers attach it
|
|
289
|
+
// to the stage result for the user.
|
|
290
|
+
void exitCode;
|
|
291
|
+
return { data: null, parseError };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function captureCoverage(api: string, sessionId?: string): Promise<CoverageCapture> {
|
|
194
295
|
const t0 = Date.now();
|
|
195
296
|
try {
|
|
196
|
-
const cmd = [...zondInvoker(),
|
|
297
|
+
const cmd = [...zondInvoker(), ...buildCoverageStageArgs(api, sessionId)];
|
|
197
298
|
const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
198
299
|
const stdout = await new Response(proc.stdout).text();
|
|
199
300
|
const code = await proc.exited;
|
|
200
301
|
const ms = Date.now() - t0;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
try {
|
|
205
|
-
return { data: JSON.parse(stdout), exitCode: code, parseError: null, durationMs: ms };
|
|
206
|
-
} catch (e) {
|
|
207
|
-
return { data: null, exitCode: code, parseError: (e as Error).message, durationMs: ms };
|
|
208
|
-
}
|
|
302
|
+
const { data, parseError } = interpretCoverageOutput(stdout, code);
|
|
303
|
+
return { data, exitCode: code, parseError, durationMs: ms };
|
|
209
304
|
} catch (e) {
|
|
210
305
|
return { data: null, exitCode: null, parseError: (e as Error).message, durationMs: Date.now() - t0 };
|
|
211
306
|
}
|
|
@@ -231,6 +326,13 @@ export async function auditCommand(options: AuditOptions): Promise<number> {
|
|
|
231
326
|
if (existsSync(guess)) specPath = guess;
|
|
232
327
|
}
|
|
233
328
|
|
|
329
|
+
// ARV-264: surface what safe mode silently drops and run pre-flight
|
|
330
|
+
// checks (auth + security schemes) before any subprocess fires.
|
|
331
|
+
const preflightWarnings = runSafePreflight(options, specPath, apiDir);
|
|
332
|
+
if (!options.json) {
|
|
333
|
+
for (const w of preflightWarnings) printWarning(w);
|
|
334
|
+
}
|
|
335
|
+
|
|
234
336
|
const stages = buildStages(options, apiDir, specPath);
|
|
235
337
|
const out = options.out ?? "audit-report.html";
|
|
236
338
|
|
|
@@ -255,12 +357,19 @@ export async function auditCommand(options: AuditOptions): Promise<number> {
|
|
|
255
357
|
const results: StageResult[] = [];
|
|
256
358
|
let coverageJson: unknown = null;
|
|
257
359
|
let coverageCapture: CoverageCapture | null = null;
|
|
360
|
+
// ARV-301: capture the active session id BEFORE session-end runs, so
|
|
361
|
+
// coverage can target it explicitly (--session-id) instead of via
|
|
362
|
+
// --union session, which would reject the now-closed session.
|
|
363
|
+
let pinnedSessionId: string | undefined;
|
|
258
364
|
for (let i = 0; i < stages.length; i++) {
|
|
259
365
|
const stage = stages[i]!;
|
|
366
|
+
if (stage.key === "session-end" && !pinnedSessionId) {
|
|
367
|
+
pinnedSessionId = readCurrentSession()?.id;
|
|
368
|
+
}
|
|
260
369
|
if (stage.key === "coverage") {
|
|
261
370
|
// ARV-108: coverage runs via captureCoverage so we keep stdout JSON.
|
|
262
371
|
if (!options.json) console.log(`==> Stage ${i + 1}/${stages.length}: ${stage.name}`);
|
|
263
|
-
coverageCapture = await captureCoverage(options.api);
|
|
372
|
+
coverageCapture = await captureCoverage(options.api, pinnedSessionId);
|
|
264
373
|
coverageJson = coverageCapture.data;
|
|
265
374
|
const status: StageResult["status"] = coverageCapture.data
|
|
266
375
|
? "ok"
|
|
@@ -283,12 +392,48 @@ export async function auditCommand(options: AuditOptions): Promise<number> {
|
|
|
283
392
|
? `coverage exited ${coverageCapture.exitCode}`
|
|
284
393
|
: undefined,
|
|
285
394
|
});
|
|
395
|
+
// ARV-66: per-stage completion line for the coverage special-case too.
|
|
396
|
+
if (!options.json) {
|
|
397
|
+
const tag = status === "ok" ? "OK" : status === "skipped" ? "SKIPPED" : `FAIL (exit ${coverageCapture.exitCode})`;
|
|
398
|
+
console.log(` └─ ${tag} · ${(coverageCapture.durationMs / 1000).toFixed(1)}s`);
|
|
399
|
+
}
|
|
286
400
|
continue;
|
|
287
401
|
}
|
|
288
402
|
results.push(await runStage(stage, i + 1, stages.length, options.json === true));
|
|
289
403
|
}
|
|
290
404
|
const totalMs = Date.now() - t0;
|
|
291
405
|
|
|
406
|
+
// ARV-158: collect per-run drill-down from the audit session so the HTML
|
|
407
|
+
// can answer "WHICH 271 findings" instead of just "3 failed stages".
|
|
408
|
+
// The session was started by `session-start` and closed by `session-end`,
|
|
409
|
+
// so it's the most recent session in the DB at this point. listSessions(1)
|
|
410
|
+
// surfaces it; we then diagnose each run with failures > 0 (passed-only
|
|
411
|
+
// runs need no triage).
|
|
412
|
+
const drilldown: Array<{ run: { id: number; failed: number; total: number; passed: number }; diagnose: DiagnoseResult }> = [];
|
|
413
|
+
try {
|
|
414
|
+
const recent = listSessions(1);
|
|
415
|
+
const session = recent[0];
|
|
416
|
+
if (session?.session_id) {
|
|
417
|
+
const runs = listRunsBySession(session.session_id);
|
|
418
|
+
for (const r of runs) {
|
|
419
|
+
if (r.failed <= 0) continue;
|
|
420
|
+
try {
|
|
421
|
+
const diag = diagnoseRun(r.id, false, options.dbPath);
|
|
422
|
+
drilldown.push({
|
|
423
|
+
run: { id: r.id, failed: r.failed, total: r.total, passed: r.passed },
|
|
424
|
+
diagnose: diag,
|
|
425
|
+
});
|
|
426
|
+
} catch {
|
|
427
|
+
// diagnose of a single run failing should not break the report —
|
|
428
|
+
// skip and keep the rest.
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
// DB unreachable / no sessions — leave drilldown empty; HTML degrades
|
|
434
|
+
// to the previous summary-only form.
|
|
435
|
+
}
|
|
436
|
+
|
|
292
437
|
await writeAuditReport(out, {
|
|
293
438
|
api: options.api,
|
|
294
439
|
apiDir,
|
|
@@ -297,6 +442,7 @@ export async function auditCommand(options: AuditOptions): Promise<number> {
|
|
|
297
442
|
coverage: coverageJson,
|
|
298
443
|
coverageStage: results.find((r) => r.key === "coverage") ?? null,
|
|
299
444
|
options,
|
|
445
|
+
drilldown,
|
|
300
446
|
});
|
|
301
447
|
|
|
302
448
|
// ARV-108: coverage is informational — keep it out of the fail count so we
|
|
@@ -315,7 +461,10 @@ export async function auditCommand(options: AuditOptions): Promise<number> {
|
|
|
315
461
|
}));
|
|
316
462
|
} else {
|
|
317
463
|
console.log("");
|
|
318
|
-
|
|
464
|
+
// ARV-80: print absolute path so the user can open the HTML report
|
|
465
|
+
// without traversing — `audit-report.html` (relative) hid the location
|
|
466
|
+
// behind cwd, especially when audit was invoked from a parent dir.
|
|
467
|
+
const summary = `Audit complete (${results.length} stages, ${(totalMs / 1000).toFixed(1)}s) → ${resolve(out)}`;
|
|
319
468
|
if (failed === 0) {
|
|
320
469
|
printSuccess(summary);
|
|
321
470
|
} else {
|
|
@@ -325,7 +474,7 @@ export async function auditCommand(options: AuditOptions): Promise<number> {
|
|
|
325
474
|
return failed === 0 ? 0 : 1;
|
|
326
475
|
}
|
|
327
476
|
|
|
328
|
-
interface ReportInput {
|
|
477
|
+
export interface ReportInput {
|
|
329
478
|
api: string;
|
|
330
479
|
apiDir: string;
|
|
331
480
|
stages: StageResult[];
|
|
@@ -335,6 +484,15 @@ interface ReportInput {
|
|
|
335
484
|
* distinguish "no session runs" from "coverage subcommand failed". */
|
|
336
485
|
coverageStage: StageResult | null;
|
|
337
486
|
options: AuditOptions;
|
|
487
|
+
/** ARV-158: per-run diagnose envelopes for runs in this audit's session
|
|
488
|
+
* that had failures. Each entry feeds a collapsible drill-down block
|
|
489
|
+
* in the HTML so the report answers "WHICH findings" inline. Empty
|
|
490
|
+
* array when no runs failed or when DB lookup couldn't reach the
|
|
491
|
+
* session. */
|
|
492
|
+
drilldown: Array<{
|
|
493
|
+
run: { id: number; failed: number; total: number; passed: number };
|
|
494
|
+
diagnose: DiagnoseResult;
|
|
495
|
+
}>;
|
|
338
496
|
}
|
|
339
497
|
|
|
340
498
|
function escapeHtml(s: string): string {
|
|
@@ -354,7 +512,10 @@ interface CoverageEnvelope {
|
|
|
354
512
|
};
|
|
355
513
|
}
|
|
356
514
|
|
|
357
|
-
|
|
515
|
+
/** ARV-158: exported for regression-test on HTML markup (drill-down
|
|
516
|
+
* sections per failed run). Same signature as before — the export
|
|
517
|
+
* doesn't expose anything CLI-internal. */
|
|
518
|
+
export async function writeAuditReport(outPath: string, data: ReportInput): Promise<void> {
|
|
358
519
|
const cov = data.coverage as CoverageEnvelope | null;
|
|
359
520
|
const totals = cov?.data?.totals;
|
|
360
521
|
const pass = cov?.data?.pass_coverage?.ratio;
|
|
@@ -367,7 +528,6 @@ async function writeAuditReport(outPath: string, data: ReportInput): Promise<voi
|
|
|
367
528
|
}).join("\n");
|
|
368
529
|
|
|
369
530
|
const reruncmd = `zond audit --api ${data.api}`
|
|
370
|
-
+ (data.options.seed ? " --seed" : "")
|
|
371
531
|
+ (data.options.withMassAssignment ? " --with-mass-assignment" : "")
|
|
372
532
|
+ (data.options.withSecurity ? " --with-security" : "");
|
|
373
533
|
|
|
@@ -383,6 +543,59 @@ async function writeAuditReport(outPath: string, data: ReportInput): Promise<voi
|
|
|
383
543
|
: `Coverage stage ${covStage.status} (exit ${covStage.exit_code ?? "?"}).`
|
|
384
544
|
: "Coverage stage was not part of this audit (older binary?).";
|
|
385
545
|
|
|
546
|
+
// ARV-158: per-run drill-down — collapsible sections, one per failed
|
|
547
|
+
// run, surfacing by_recommended_action buckets (count + first example).
|
|
548
|
+
// Concrete commands replace the previous "type <run-id> manually" hint.
|
|
549
|
+
const drilldownBlocks = data.drilldown.length === 0
|
|
550
|
+
? ""
|
|
551
|
+
: `<h2>Failures by run</h2>\n` + data.drilldown.map(({ run, diagnose }) => {
|
|
552
|
+
const buckets = diagnose.by_recommended_action ?? {};
|
|
553
|
+
// Sort buckets by priority (report_backend_bug first, then by count)
|
|
554
|
+
// — matches the zond-triage skill priority order.
|
|
555
|
+
const priorityOrder = [
|
|
556
|
+
"report_backend_bug",
|
|
557
|
+
"fix_spec",
|
|
558
|
+
"fix_auth_config",
|
|
559
|
+
"fix_env",
|
|
560
|
+
"fix_fixture",
|
|
561
|
+
"fix_network_config",
|
|
562
|
+
"regenerate_suite",
|
|
563
|
+
"tighten_validation",
|
|
564
|
+
"add_required_header",
|
|
565
|
+
"fix_test_logic",
|
|
566
|
+
"wontfix_known_limitation",
|
|
567
|
+
];
|
|
568
|
+
const sortedKeys = Object.keys(buckets).sort((a, b) => {
|
|
569
|
+
const ai = priorityOrder.indexOf(a);
|
|
570
|
+
const bi = priorityOrder.indexOf(b);
|
|
571
|
+
if (ai === -1 && bi === -1) return (buckets[b]!.count) - (buckets[a]!.count);
|
|
572
|
+
if (ai === -1) return 1;
|
|
573
|
+
if (bi === -1) return -1;
|
|
574
|
+
return ai - bi;
|
|
575
|
+
});
|
|
576
|
+
const bucketsHtml = sortedKeys.length === 0
|
|
577
|
+
? `<p class="muted">No <code>recommended_action</code> buckets — see raw failures via the commands below.</p>`
|
|
578
|
+
: `<ul class="buckets">` + sortedKeys.map((key) => {
|
|
579
|
+
const b = buckets[key]!;
|
|
580
|
+
const first = b.examples[0];
|
|
581
|
+
const sample = first
|
|
582
|
+
? `<code>${escapeHtml(first.method)} ${escapeHtml(first.path)}</code> → <strong>${first.status}</strong>${first.reason ? ` — ${escapeHtml(first.reason)}` : ""}`
|
|
583
|
+
: "";
|
|
584
|
+
const moreExamples = b.examples.length > 1
|
|
585
|
+
? ` <span class="muted">(+${b.examples.length - 1} more)</span>`
|
|
586
|
+
: "";
|
|
587
|
+
return `<li><strong>${escapeHtml(key)}</strong> ×${b.count}${sample ? ` — ${sample}${moreExamples}` : ""}</li>`;
|
|
588
|
+
}).join("\n") + `</ul>`;
|
|
589
|
+
return `<details>
|
|
590
|
+
<summary>Run #${run.id} — ${run.failed}/${run.total} failed (${run.passed} passed)</summary>
|
|
591
|
+
${bucketsHtml}
|
|
592
|
+
<p class="cmds">
|
|
593
|
+
Drill in: <code>zond db diagnose --run-id ${run.id} --json</code> ·
|
|
594
|
+
<code>zond report export ${run.id}</code>
|
|
595
|
+
</p>
|
|
596
|
+
</details>`;
|
|
597
|
+
}).join("\n");
|
|
598
|
+
|
|
386
599
|
const coverageBlock = totals
|
|
387
600
|
? `<h2>Coverage (session union)</h2>
|
|
388
601
|
<div class="cov">
|
|
@@ -413,6 +626,13 @@ tr.skip td:nth-child(2) { color: #888; font-style: italic; }
|
|
|
413
626
|
.warn { background: #fef9e7; padding: 8px 12px; border-left: 3px solid #f0c040; margin: 1em 0; }
|
|
414
627
|
code { background: #f4f4f4; padding: 1px 5px; border-radius: 3px; font-size: 0.9em; }
|
|
415
628
|
ul { line-height: 1.6; }
|
|
629
|
+
details { margin: 0.8em 0; border: 1px solid #eee; border-radius: 4px; padding: 0.3em 0.8em; }
|
|
630
|
+
details summary { cursor: pointer; font-weight: 600; padding: 0.3em 0; }
|
|
631
|
+
details[open] summary { border-bottom: 1px solid #eee; margin-bottom: 0.6em; }
|
|
632
|
+
ul.buckets { padding-left: 1.2em; margin: 0.4em 0; }
|
|
633
|
+
ul.buckets li { margin: 0.25em 0; }
|
|
634
|
+
p.cmds { font-size: 0.88em; color: #555; margin-top: 0.8em; }
|
|
635
|
+
.muted { color: #888; }
|
|
416
636
|
</style></head>
|
|
417
637
|
<body>
|
|
418
638
|
<h1>zond audit — ${escapeHtml(data.api)}</h1>
|
|
@@ -427,21 +647,93 @@ ${stageRows}
|
|
|
427
647
|
|
|
428
648
|
${coverageBlock}
|
|
429
649
|
|
|
650
|
+
${drilldownBlocks}
|
|
651
|
+
|
|
430
652
|
<h2>Drill-down</h2>
|
|
431
653
|
<ul>
|
|
432
|
-
<li>Per-run HTML: <code>zond report export <run-id></code></li>
|
|
433
|
-
<li>Diagnose failures: <code>zond db diagnose <run-id> --json</code></li>
|
|
434
654
|
<li>Re-run audit: <code>${escapeHtml(reruncmd)}</code></li>
|
|
655
|
+
<li>List recent runs: <code>zond db runs --limit 20</code></li>
|
|
656
|
+
<li>Per-run report: <code>zond report export <run-id></code></li>
|
|
435
657
|
</ul>
|
|
436
658
|
</body></html>`;
|
|
437
659
|
|
|
438
660
|
await writeFile(outPath, html, "utf-8");
|
|
439
661
|
}
|
|
440
662
|
|
|
663
|
+
/**
|
|
664
|
+
* ARV-264: pre-flight checks for safe-mode auditing.
|
|
665
|
+
*
|
|
666
|
+
* 1. Warn when the user passed `--seed` / `--with-mass-assignment` /
|
|
667
|
+
* `--with-security` without `--live` — those stages are silently
|
|
668
|
+
* dropped in safe mode and the user deserves to know.
|
|
669
|
+
* 2. Warn when the spec declares `components.securitySchemes` but the
|
|
670
|
+
* workspace has no `auth_token` set — every probe will skip with
|
|
671
|
+
* 401 and the report will read as "all green" misleadingly.
|
|
672
|
+
* 3. Warn when no security schemes are declared at all (open API or
|
|
673
|
+
* incomplete spec) — same misleading-green risk on auth gates.
|
|
674
|
+
*/
|
|
675
|
+
export function runSafePreflight(
|
|
676
|
+
opts: AuditOptions,
|
|
677
|
+
specPath: string | null,
|
|
678
|
+
apiDir: string,
|
|
679
|
+
): string[] {
|
|
680
|
+
const out: string[] = [];
|
|
681
|
+
const safe = opts.live !== true;
|
|
682
|
+
|
|
683
|
+
if (safe) {
|
|
684
|
+
if (opts.withMassAssignment) {
|
|
685
|
+
out.push(
|
|
686
|
+
"audit --safe (default): --with-mass-assignment ignored. Mass-assignment probes POST mutated " +
|
|
687
|
+
"bodies and follow up with GET/DELETE — high blast radius. Re-run with --live to enable.",
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
if (opts.withSecurity) {
|
|
691
|
+
out.push(
|
|
692
|
+
"audit --safe (default): --with-security ignored. Security probes send live SSRF/CRLF/redirect " +
|
|
693
|
+
"payloads to real endpoints. Re-run with --live to enable.",
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Spec-level checks (cheap synchronous read; spec.json is JSON).
|
|
699
|
+
if (specPath && existsSync(specPath)) {
|
|
700
|
+
try {
|
|
701
|
+
const spec = JSON.parse(require("node:fs").readFileSync(specPath, "utf-8")) as {
|
|
702
|
+
components?: { securitySchemes?: Record<string, unknown> };
|
|
703
|
+
};
|
|
704
|
+
const hasSchemes = Boolean(
|
|
705
|
+
spec.components?.securitySchemes && Object.keys(spec.components.securitySchemes).length > 0,
|
|
706
|
+
);
|
|
707
|
+
const envPath = join(apiDir, ".env.yaml");
|
|
708
|
+
let authTokenSet = false;
|
|
709
|
+
if (existsSync(envPath)) {
|
|
710
|
+
const env = require("node:fs").readFileSync(envPath, "utf-8") as string;
|
|
711
|
+
// Match `auth_token: ...` lines whose value isn't an unfilled placeholder.
|
|
712
|
+
authTokenSet = /^\s*auth_token\s*:\s*(?!["']?\s*(<|UNSET|TODO|REPLACE|null|~)\b)\S/m.test(env);
|
|
713
|
+
}
|
|
714
|
+
if (hasSchemes && !authTokenSet) {
|
|
715
|
+
out.push(
|
|
716
|
+
"spec declares securitySchemes but auth_token is unset in .env.yaml — probes will likely hit " +
|
|
717
|
+
"401/403 and the report can read misleadingly green. Run `zond doctor --api " + opts.api + "` to fix.",
|
|
718
|
+
);
|
|
719
|
+
} else if (!hasSchemes) {
|
|
720
|
+
out.push(
|
|
721
|
+
"spec declares no securitySchemes — either the API is open or the spec is incomplete. " +
|
|
722
|
+
"Auth-related checks (ignored_auth, open_cors_on_sensitive) will skip; verify before sharing the report.",
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
} catch {
|
|
726
|
+
// Spec unreadable — separate failure mode, surfaced by other stages.
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return out;
|
|
731
|
+
}
|
|
732
|
+
|
|
441
733
|
export function registerAudit(program: Command): void {
|
|
442
734
|
program
|
|
443
735
|
.command("audit")
|
|
444
|
-
.description("
|
|
736
|
+
.description("Smoke + breadth-coverage macro: prepare-fixtures → generate → probe static → session-wrapped run → coverage → HTML report. For depth-checks / security probes / stateful invariants, drive the `zond` skill — `audit` is the breadth pass, depth is the skill's job.")
|
|
445
737
|
// ARV-29: not `requiredOption` — same regression that hit prepare-fixtures
|
|
446
738
|
// (TASK-20) and checks run (TASK-17). Commander routes `--api` to the
|
|
447
739
|
// program-level option, so the subcommand's opts.api ends up undefined and
|
|
@@ -450,12 +742,16 @@ export function registerAudit(program: Command): void {
|
|
|
450
742
|
// mirror > .zond/current-api.
|
|
451
743
|
.option("--api <name>", "Registered API to audit. Falls back to ZOND_API / .zond/current-api.")
|
|
452
744
|
.option("--db <path>", "Path to SQLite database file")
|
|
453
|
-
.option("--
|
|
454
|
-
.option("--with-
|
|
455
|
-
.option("--
|
|
745
|
+
.option("--with-mass-assignment", "Include 'probe mass-assignment' as an extra stage. Requires --live.")
|
|
746
|
+
.option("--with-security", "Include 'probe security ssrf,crlf,open-redirect' as an extra stage. Requires --live.")
|
|
747
|
+
.option("--live", "ARV-264: opt into the full pipeline against a real-traffic API. Without this flag, audit runs in safe mode: no mass-assignment / security probes, no destructive traffic. Use only against throwaway/sandbox accounts.")
|
|
456
748
|
.option("--out <path>", "HTML report output path (default: audit-report.html)")
|
|
457
749
|
.option("--dry-run", "Print the stage plan without executing anything")
|
|
458
750
|
.option("--force", "Disable mtime-based skip (always regenerate, even if tests/ newer than spec)")
|
|
751
|
+
.option(
|
|
752
|
+
"--budget <tier>",
|
|
753
|
+
"ARV-292: adaptive request cap for spawned `run` stages. `quick` (50 req, ~60-sec gate), `standard` (500 req), `full` (uncapped). Omitted ⇒ legacy uncapped pipeline.",
|
|
754
|
+
)
|
|
459
755
|
.action(async (opts, cmd: Command) => {
|
|
460
756
|
// ARV-53.
|
|
461
757
|
const apiName = getApi(cmd, opts);
|
|
@@ -465,16 +761,26 @@ export function registerAudit(program: Command): void {
|
|
|
465
761
|
return;
|
|
466
762
|
}
|
|
467
763
|
opts.api = apiName;
|
|
764
|
+
let budget: Budget | undefined;
|
|
765
|
+
if (opts.budget !== undefined) {
|
|
766
|
+
if (!isBudget(opts.budget)) {
|
|
767
|
+
printError(`--budget must be one of: ${BUDGETS.join(", ")}; got '${opts.budget}'`);
|
|
768
|
+
process.exitCode = 2;
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
budget = opts.budget;
|
|
772
|
+
}
|
|
468
773
|
process.exitCode = await auditCommand({
|
|
469
774
|
api: opts.api,
|
|
470
775
|
dbPath: opts.db,
|
|
471
|
-
seed: opts.seed === true,
|
|
472
776
|
withMassAssignment: opts.withMassAssignment === true,
|
|
473
777
|
withSecurity: opts.withSecurity === true,
|
|
474
778
|
out: opts.out,
|
|
475
779
|
dryRun: opts.dryRun === true,
|
|
476
780
|
force: opts.force === true,
|
|
477
781
|
json: globalJson(cmd),
|
|
782
|
+
live: opts.live === true,
|
|
783
|
+
budget,
|
|
478
784
|
});
|
|
479
785
|
});
|
|
480
786
|
}
|
|
@@ -303,6 +303,10 @@ function defineCheckSpec(parent: Command, name: string): void {
|
|
|
303
303
|
.option("--api <name>", "Use the registered API's spec (apis/<name>/spec.json)")
|
|
304
304
|
.option("--strict", "Exit non-zero even on LOW-severity issues")
|
|
305
305
|
.option("--ndjson", "Stream issues as one JSON per line (NDJSON), instead of the wrapped envelope")
|
|
306
|
+
.option(
|
|
307
|
+
"--report <format>",
|
|
308
|
+
"ARV-204: output format alias for parity with `checks run`. Accepts: console (default human summary), json (equivalent to --json envelope), ndjson (equivalent to --ndjson). Last writer wins if combined with --json/--ndjson.",
|
|
309
|
+
)
|
|
306
310
|
.option(
|
|
307
311
|
"--rule <list>",
|
|
308
312
|
"Unified rule selector (TASK-291). Comma-separated items: 'B1' (whitelist), " +
|
|
@@ -329,10 +333,19 @@ function defineCheckSpec(parent: Command, name: string): void {
|
|
|
329
333
|
|
|
330
334
|
const merged = mergeRuleFlags(opts.rule, opts.filterRule);
|
|
331
335
|
|
|
336
|
+
// ARV-204: --report ndjson|json|console aliases the existing booleans
|
|
337
|
+
// so scripts can keep one flag shape across `checks run` and `check spec`.
|
|
338
|
+
let report = typeof opts.report === "string" ? opts.report.toLowerCase() : undefined;
|
|
339
|
+
if (report && !["console", "json", "ndjson"].includes(report)) {
|
|
340
|
+
printError(`--report: unknown format '${opts.report}'. Use one of: console, json, ndjson.`);
|
|
341
|
+
process.exitCode = 2;
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
332
345
|
process.exitCode = await checkSpecCommand({
|
|
333
346
|
specPath: resolved.spec,
|
|
334
|
-
json: globalJson(cmd),
|
|
335
|
-
ndjson: opts.ndjson === true,
|
|
347
|
+
json: globalJson(cmd) || report === "json",
|
|
348
|
+
ndjson: opts.ndjson === true || report === "ndjson",
|
|
336
349
|
strict: opts.strict === true,
|
|
337
350
|
rule: merged.cliRule,
|
|
338
351
|
config: opts.config,
|