@kirrosh/zond 0.26.0 → 0.27.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.
Files changed (261) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +22 -21
  3. package/bin/zond.mjs +23 -0
  4. package/package.json +13 -16
  5. package/scripts/npm/postinstall.mjs +76 -0
  6. package/src/CLAUDE.md +0 -112
  7. package/src/bun-types.d.ts +0 -5
  8. package/src/cli/argv.ts +0 -122
  9. package/src/cli/commands/add-api.ts +0 -146
  10. package/src/cli/commands/api/annotate/idempotency.ts +0 -59
  11. package/src/cli/commands/api/annotate/index.ts +0 -880
  12. package/src/cli/commands/api/annotate/lifecycle.ts +0 -74
  13. package/src/cli/commands/api/annotate/overlay.ts +0 -206
  14. package/src/cli/commands/api/annotate/pagination.ts +0 -64
  15. package/src/cli/commands/api/annotate/prompts.ts +0 -220
  16. package/src/cli/commands/api/annotate/readback.ts +0 -58
  17. package/src/cli/commands/api/annotate/resources.ts +0 -91
  18. package/src/cli/commands/api/annotate/seed-bodies.ts +0 -61
  19. package/src/cli/commands/audit.ts +0 -786
  20. package/src/cli/commands/catalog.ts +0 -97
  21. package/src/cli/commands/check.ts +0 -361
  22. package/src/cli/commands/checks.ts +0 -1072
  23. package/src/cli/commands/ci-init.ts +0 -223
  24. package/src/cli/commands/clean.ts +0 -212
  25. package/src/cli/commands/cleanup.ts +0 -236
  26. package/src/cli/commands/completions.ts +0 -192
  27. package/src/cli/commands/coverage.ts +0 -872
  28. package/src/cli/commands/db.ts +0 -611
  29. package/src/cli/commands/describe.ts +0 -120
  30. package/src/cli/commands/discover.ts +0 -1356
  31. package/src/cli/commands/doctor.ts +0 -661
  32. package/src/cli/commands/fixtures.ts +0 -402
  33. package/src/cli/commands/generate.ts +0 -577
  34. package/src/cli/commands/init/agents-md.ts +0 -61
  35. package/src/cli/commands/init/bootstrap.ts +0 -111
  36. package/src/cli/commands/init/index.ts +0 -244
  37. package/src/cli/commands/init/skills.ts +0 -141
  38. package/src/cli/commands/init/templates/agents.md +0 -86
  39. package/src/cli/commands/init/templates/markdown.d.ts +0 -4
  40. package/src/cli/commands/init/templates/skills/warm-up-target.md +0 -122
  41. package/src/cli/commands/init/templates/skills/zond-checks.md +0 -621
  42. package/src/cli/commands/init/templates/skills/zond-seed.md +0 -114
  43. package/src/cli/commands/init/templates/skills/zond-triage.md +0 -272
  44. package/src/cli/commands/init/templates/skills/zond.md +0 -861
  45. package/src/cli/commands/init/templates/zond-config.yml +0 -14
  46. package/src/cli/commands/prepare-fixtures.ts +0 -97
  47. package/src/cli/commands/probe/_seed-bodies.ts +0 -52
  48. package/src/cli/commands/probe/mass-assignment.ts +0 -594
  49. package/src/cli/commands/probe/security.ts +0 -537
  50. package/src/cli/commands/probe/static.ts +0 -255
  51. package/src/cli/commands/probe/webhooks.ts +0 -163
  52. package/src/cli/commands/probe.ts +0 -535
  53. package/src/cli/commands/reference.ts +0 -87
  54. package/src/cli/commands/refresh-api.ts +0 -227
  55. package/src/cli/commands/remove-api.ts +0 -150
  56. package/src/cli/commands/report-bundle.ts +0 -310
  57. package/src/cli/commands/report.ts +0 -241
  58. package/src/cli/commands/request.ts +0 -548
  59. package/src/cli/commands/run.ts +0 -1142
  60. package/src/cli/commands/schema-from-runs.ts +0 -128
  61. package/src/cli/commands/secrets.ts +0 -133
  62. package/src/cli/commands/session.ts +0 -244
  63. package/src/cli/commands/use.ts +0 -74
  64. package/src/cli/index.ts +0 -55
  65. package/src/cli/json-envelope.ts +0 -108
  66. package/src/cli/json-schemas.ts +0 -314
  67. package/src/cli/output.ts +0 -40
  68. package/src/cli/program.ts +0 -219
  69. package/src/cli/resolve.ts +0 -105
  70. package/src/cli/runtime.ts +0 -7
  71. package/src/cli/safe-live.ts +0 -24
  72. package/src/cli/status-filter.ts +0 -114
  73. package/src/cli/util/api-context.ts +0 -85
  74. package/src/cli/version.ts +0 -8
  75. package/src/core/audit/persist.ts +0 -183
  76. package/src/core/checks/budget.ts +0 -59
  77. package/src/core/checks/checks/_crud-helpers.ts +0 -133
  78. package/src/core/checks/checks/_negative_mutator.ts +0 -133
  79. package/src/core/checks/checks/_readback-helpers.ts +0 -133
  80. package/src/core/checks/checks/content_type_conformance.ts +0 -39
  81. package/src/core/checks/checks/cross_call_references.ts +0 -147
  82. package/src/core/checks/checks/cursor_boundary_fuzzing.ts +0 -219
  83. package/src/core/checks/checks/ensure_resource_availability.ts +0 -62
  84. package/src/core/checks/checks/idempotency_replay.ts +0 -242
  85. package/src/core/checks/checks/ignored_auth.ts +0 -254
  86. package/src/core/checks/checks/index.ts +0 -68
  87. package/src/core/checks/checks/lifecycle_transitions.ts +0 -416
  88. package/src/core/checks/checks/missing_required_header.ts +0 -40
  89. package/src/core/checks/checks/negative_data_rejection.ts +0 -148
  90. package/src/core/checks/checks/not_a_server_error.ts +0 -35
  91. package/src/core/checks/checks/open_cors_on_sensitive.ts +0 -160
  92. package/src/core/checks/checks/pagination_invariants.ts +0 -419
  93. package/src/core/checks/checks/positive_data_acceptance.ts +0 -33
  94. package/src/core/checks/checks/rate_limit_headers_absent.ts +0 -77
  95. package/src/core/checks/checks/response_headers_conformance.ts +0 -74
  96. package/src/core/checks/checks/response_schema_conformance.ts +0 -30
  97. package/src/core/checks/checks/status_code_conformance.ts +0 -132
  98. package/src/core/checks/checks/unsupported_method.ts +0 -63
  99. package/src/core/checks/checks/use_after_free.ts +0 -78
  100. package/src/core/checks/index.ts +0 -30
  101. package/src/core/checks/mode.ts +0 -82
  102. package/src/core/checks/recommended-action.ts +0 -68
  103. package/src/core/checks/registry.ts +0 -78
  104. package/src/core/checks/runner.ts +0 -1461
  105. package/src/core/checks/sarif.ts +0 -230
  106. package/src/core/checks/spec-findings.ts +0 -308
  107. package/src/core/checks/stateful.ts +0 -121
  108. package/src/core/checks/types.ts +0 -305
  109. package/src/core/checks/zond-extensions.ts +0 -73
  110. package/src/core/classifier/recommended-action.ts +0 -251
  111. package/src/core/context/current.ts +0 -51
  112. package/src/core/context/session.ts +0 -78
  113. package/src/core/coverage/loader.ts +0 -216
  114. package/src/core/coverage/reasons.ts +0 -300
  115. package/src/core/diagnostics/db-analysis.ts +0 -666
  116. package/src/core/diagnostics/failure-class.ts +0 -140
  117. package/src/core/diagnostics/failure-hints.ts +0 -112
  118. package/src/core/diagnostics/spec-pointer.ts +0 -99
  119. package/src/core/diagnostics/suggested-fixes.ts +0 -155
  120. package/src/core/exporter/case-study/index.ts +0 -270
  121. package/src/core/exporter/curl.ts +0 -40
  122. package/src/core/exporter/exporter.ts +0 -48
  123. package/src/core/exporter/html-report/escape.ts +0 -24
  124. package/src/core/exporter/html-report/index.ts +0 -479
  125. package/src/core/exporter/html-report/script.ts +0 -100
  126. package/src/core/exporter/html-report/styles.ts +0 -408
  127. package/src/core/generator/catalog-builder.ts +0 -179
  128. package/src/core/generator/chunker.ts +0 -78
  129. package/src/core/generator/coverage-phase.ts +0 -0
  130. package/src/core/generator/coverage-scanner.ts +0 -87
  131. package/src/core/generator/data-factory.ts +0 -759
  132. package/src/core/generator/describe.ts +0 -302
  133. package/src/core/generator/endpoint-warnings.ts +0 -52
  134. package/src/core/generator/fixtures-builder.ts +0 -332
  135. package/src/core/generator/index.ts +0 -14
  136. package/src/core/generator/openapi-reader.ts +0 -297
  137. package/src/core/generator/path-param-disambig.ts +0 -140
  138. package/src/core/generator/resources-builder.ts +0 -898
  139. package/src/core/generator/schema-utils.ts +0 -112
  140. package/src/core/generator/serializer.ts +0 -343
  141. package/src/core/generator/suite-generator.ts +0 -1301
  142. package/src/core/generator/types.ts +0 -63
  143. package/src/core/identity/identity-file.ts +0 -0
  144. package/src/core/lint/affects.ts +0 -28
  145. package/src/core/lint/config.ts +0 -96
  146. package/src/core/lint/format.ts +0 -42
  147. package/src/core/lint/index.ts +0 -94
  148. package/src/core/lint/reporter.ts +0 -128
  149. package/src/core/lint/rules/consistency.ts +0 -158
  150. package/src/core/lint/rules/heuristics.ts +0 -97
  151. package/src/core/lint/rules/strictness.ts +0 -109
  152. package/src/core/lint/types.ts +0 -96
  153. package/src/core/lint/walker.ts +0 -248
  154. package/src/core/meta/meta-store.ts +0 -11
  155. package/src/core/output/README.md +0 -73
  156. package/src/core/output/index.ts +0 -13
  157. package/src/core/output/run.ts +0 -91
  158. package/src/core/output/types.ts +0 -122
  159. package/src/core/parser/dynamic-values.ts +0 -160
  160. package/src/core/parser/env-interpolation.ts +0 -104
  161. package/src/core/parser/filter.ts +0 -97
  162. package/src/core/parser/schema.ts +0 -359
  163. package/src/core/parser/types.ts +0 -117
  164. package/src/core/parser/variables.ts +0 -0
  165. package/src/core/parser/yaml-parser.ts +0 -181
  166. package/src/core/probe/bootstrap.ts +0 -34
  167. package/src/core/probe/dry-run-envelope.ts +0 -61
  168. package/src/core/probe/mass-assignment/classify.ts +0 -175
  169. package/src/core/probe/mass-assignment/cleanup.ts +0 -52
  170. package/src/core/probe/mass-assignment/digest.ts +0 -114
  171. package/src/core/probe/mass-assignment/orchestrator.ts +0 -459
  172. package/src/core/probe/mass-assignment/regression.ts +0 -141
  173. package/src/core/probe/mass-assignment/suspects.ts +0 -92
  174. package/src/core/probe/mass-assignment/types.ts +0 -135
  175. package/src/core/probe/mass-assignment-probe-class.ts +0 -198
  176. package/src/core/probe/mass-assignment-probe.ts +0 -27
  177. package/src/core/probe/mass-assignment-template.ts +0 -240
  178. package/src/core/probe/method-probe.ts +0 -164
  179. package/src/core/probe/method-shared.ts +0 -69
  180. package/src/core/probe/negative-probe.ts +0 -691
  181. package/src/core/probe/orphan-tracker.ts +0 -188
  182. package/src/core/probe/path-discovery.ts +0 -439
  183. package/src/core/probe/probe-harness.ts +0 -119
  184. package/src/core/probe/registry.ts +0 -89
  185. package/src/core/probe/runner.ts +0 -136
  186. package/src/core/probe/security/baseline.ts +0 -174
  187. package/src/core/probe/security/classify.ts +0 -341
  188. package/src/core/probe/security/cleanup.ts +0 -125
  189. package/src/core/probe/security/detectors.ts +0 -71
  190. package/src/core/probe/security/digest.ts +0 -104
  191. package/src/core/probe/security/orchestrator.ts +0 -398
  192. package/src/core/probe/security/regression.ts +0 -103
  193. package/src/core/probe/security/types.ts +0 -151
  194. package/src/core/probe/security-probe-class.ts +0 -207
  195. package/src/core/probe/security-probe.ts +0 -32
  196. package/src/core/probe/shared.ts +0 -531
  197. package/src/core/probe/static-probe-class.ts +0 -125
  198. package/src/core/probe/types.ts +0 -165
  199. package/src/core/probe/verdict-aggregator.ts +0 -33
  200. package/src/core/probe/webhooks-probe.ts +0 -282
  201. package/src/core/reporter/console.ts +0 -240
  202. package/src/core/reporter/index.ts +0 -22
  203. package/src/core/reporter/json.ts +0 -22
  204. package/src/core/reporter/junit.ts +0 -93
  205. package/src/core/reporter/ndjson.ts +0 -37
  206. package/src/core/reporter/types.ts +0 -15
  207. package/src/core/runner/assertions.ts +0 -383
  208. package/src/core/runner/async-pool.ts +0 -108
  209. package/src/core/runner/auth-path.ts +0 -8
  210. package/src/core/runner/ci-context.ts +0 -72
  211. package/src/core/runner/executor.ts +0 -652
  212. package/src/core/runner/expr-eval.ts +0 -41
  213. package/src/core/runner/form-encode.ts +0 -41
  214. package/src/core/runner/http-client.ts +0 -224
  215. package/src/core/runner/learn-drift.ts +0 -293
  216. package/src/core/runner/preflight-vars.ts +0 -153
  217. package/src/core/runner/progress-tracker.ts +0 -73
  218. package/src/core/runner/rate-limiter.ts +0 -185
  219. package/src/core/runner/run-kind.ts +0 -45
  220. package/src/core/runner/schema-validator.ts +0 -308
  221. package/src/core/runner/send-request.ts +0 -232
  222. package/src/core/runner/transforms.ts +0 -65
  223. package/src/core/runner/types.ts +0 -94
  224. package/src/core/secrets/registry.ts +0 -164
  225. package/src/core/secrets/secrets-file.ts +0 -115
  226. package/src/core/selectors/operation-filter.ts +0 -144
  227. package/src/core/setup-api.ts +0 -590
  228. package/src/core/severity/category.ts +0 -94
  229. package/src/core/severity/index.ts +0 -58
  230. package/src/core/spec/infer-schema.ts +0 -102
  231. package/src/core/spec/layers.ts +0 -154
  232. package/src/core/spec/merge-specs.ts +0 -156
  233. package/src/core/spec/schema-from-runs.ts +0 -117
  234. package/src/core/spec/schema-overlay.ts +0 -130
  235. package/src/core/util/ajv.ts +0 -13
  236. package/src/core/util/format-eta.ts +0 -21
  237. package/src/core/util/headers.ts +0 -9
  238. package/src/core/util/url.ts +0 -24
  239. package/src/core/utils.ts +0 -13
  240. package/src/core/workspace/config.ts +0 -129
  241. package/src/core/workspace/fixture-gap-report.ts +0 -84
  242. package/src/core/workspace/fixture-gaps.ts +0 -71
  243. package/src/core/workspace/manifest.ts +0 -283
  244. package/src/core/workspace/output-rotation.ts +0 -62
  245. package/src/core/workspace/root.ts +0 -96
  246. package/src/core/workspace/triage-path.ts +0 -87
  247. package/src/db/lint-runs.ts +0 -47
  248. package/src/db/migrate.ts +0 -128
  249. package/src/db/migrations/0001_run_kind.sql +0 -25
  250. package/src/db/migrations/0002_run_kind_request.sql +0 -59
  251. package/src/db/migrations/sql.d.ts +0 -4
  252. package/src/db/queries/collections.ts +0 -133
  253. package/src/db/queries/coverage.ts +0 -9
  254. package/src/db/queries/dashboard.ts +0 -59
  255. package/src/db/queries/results.ts +0 -216
  256. package/src/db/queries/runs.ts +0 -289
  257. package/src/db/queries/sessions.ts +0 -42
  258. package/src/db/queries/settings.ts +0 -28
  259. package/src/db/queries/types.ts +0 -172
  260. package/src/db/queries.ts +0 -75
  261. package/src/db/schema.ts +0 -298
@@ -1,594 +0,0 @@
1
- import { join } from "path";
2
- import { mkdir, writeFile } from "fs/promises";
3
- import { loadEnvironment, loadEnvFile } from "../../../core/parser/variables.ts";
4
- import {
5
- runMassAssignmentProbes,
6
- formatDigestMarkdown,
7
- emitRegressionSuites,
8
- } from "../../../core/probe/mass-assignment-probe.ts";
9
- import { loadSpecForProbe, writeProbeSuites } from "../../../core/probe/runner.ts";
10
- import { printError, printSuccess, printWarning } from "../../output.ts";
11
- import { jsonOk, jsonError, printJson } from "../../json-envelope.ts";
12
- import { getSecretRegistry } from "../../../core/secrets/registry.ts";
13
- import { applySanitizer } from "../../../core/exporter/exporter.ts";
14
- import { rotateOutputTarget } from "../../../core/workspace/output-rotation.ts";
15
- import { tallyBySeverity, formatSummaryLine } from "../../../core/probe/verdict-aggregator.ts";
16
- import { printMutationBanner, countCleanupFailures } from "../../../core/probe/shared.ts";
17
- import { MassAssignmentProbe } from "../../../core/probe/mass-assignment-probe-class.ts";
18
- import { summarizeDryRun, formatDryRunDigest } from "../../../core/probe/dry-run-envelope.ts";
19
- import { compileOperationFilter } from "../../../core/selectors/operation-filter.ts";
20
- import { loadSeedBodyOverlays } from "./_seed-bodies.ts";
21
- import type { EndpointVerdict, MassAssignmentResult } from "../../../core/probe/mass-assignment-probe.ts";
22
- import type { ProbeEndpointResult, ProbeEndpointStatus, ProbeFindingSeverity } from "../../../core/probe/types.ts";
23
-
24
- interface BucketCounts {
25
- high: number;
26
- inconclusiveBaseline: number;
27
- inconclusive5xx: number;
28
- medium: number;
29
- low: number;
30
- ok: number;
31
- skipped: number;
32
- }
33
-
34
- const MA_BUCKETS: ReadonlyArray<readonly [string, keyof BucketCounts & string]> = [
35
- ["high", "high"],
36
- ["inconclusive-baseline", "inconclusiveBaseline"],
37
- ["inconclusive-5xx", "inconclusive5xx"],
38
- ["medium", "medium"],
39
- ["low", "low"],
40
- ["ok", "ok"],
41
- ["skipped", "skipped"],
42
- ];
43
-
44
- const MA_SUMMARY: ReadonlyArray<readonly [string, keyof BucketCounts & string]> = [
45
- ["HIGH", "high"],
46
- ["INCONCLUSIVE", "inconclusiveBaseline"],
47
- ["INCONCLUSIVE-5XX", "inconclusive5xx"],
48
- ["MED", "medium"],
49
- ["LOW", "low"],
50
- ["OK", "ok"],
51
- ["SKIPPED", "skipped"],
52
- ];
53
-
54
- const MA_ZERO: BucketCounts = {
55
- high: 0, inconclusiveBaseline: 0, inconclusive5xx: 0, medium: 0, low: 0, ok: 0, skipped: 0,
56
- };
57
-
58
- export interface ProbeMassAssignmentOptions {
59
- specPath: string;
60
- env?: string;
61
- /** Markdown digest output file. If omitted — print to stdout. */
62
- output?: string;
63
- /** Emit regression YAML suites into this directory. */
64
- emitTests?: string;
65
- tag?: string;
66
- noCleanup?: boolean;
67
- noDiscover?: boolean;
68
- timeoutMs?: number;
69
- json?: boolean;
70
- listTags?: boolean;
71
- overwrite?: boolean;
72
- /** m-17 / ARV-52: list which endpoints + fields would be attacked
73
- * without sending live traffic. */
74
- dryRun?: boolean;
75
- /** m-17 / ARV-52: m-15 ARV-9 selector grammar (`path:`/`method:`/`tag:`/`operation-id:`). */
76
- include?: string[];
77
- exclude?: string[];
78
- /** m-17 / ARV-51: format for --output / non-json stdout. */
79
- report?: "markdown" | "json";
80
- /** ARV-252: surface INFO-severity inconclusive verdicts (absent-but-
81
- * unverifiable). Silently-ignored verdicts are never shown — they
82
- * represent correct framework behaviour. */
83
- verbose?: boolean;
84
- /** ARV-252: additional suspect fields to inject, in `name=value`
85
- * form. Extends the curated SUSPECTED_FIELDS list per-run. Full
86
- * spec-extension support (x-zond-suspect-fields) is tracked in
87
- * ARV-189. */
88
- suspectField?: string[];
89
- /** ARV-265: collection name for audit-coverage attribution. Set by
90
- * the CLI when --api / current-api resolves; left undefined for
91
- * bare-spec invocations. */
92
- apiName?: string;
93
- /** ARV-302: cap the number of endpoints probed in this run (after
94
- * --include / --exclude / --tag filters). Used by `zond audit
95
- * --budget` so probe stages stay inside a wall-clock budget instead
96
- * of unbounded scanning (Stripe: 587 endpoints, 3.6k probes — silent
97
- * 10+ min). When the cap trims the set, a warning is emitted so the
98
- * user knows the run was partial. */
99
- maxEndpoints?: number;
100
- }
101
-
102
- export async function probeMassAssignmentCommand(
103
- options: ProbeMassAssignmentOptions,
104
- ): Promise<number> {
105
- try {
106
- const loaded = await loadSpecForProbe({
107
- specPath: options.specPath,
108
- tag: options.tag,
109
- listTags: options.listTags,
110
- });
111
-
112
- if (loaded.kind === "tags") {
113
- if (options.json) {
114
- printJson(jsonOk("probe-mass-assignment", { tags: loaded.tags }));
115
- } else if (loaded.tags.length === 0) {
116
- console.log("No tags found in spec.");
117
- } else {
118
- console.log("Available tags:");
119
- for (const t of loaded.tags) console.log(` - ${t}`);
120
- }
121
- return 0;
122
- }
123
- if (loaded.kind === "tag-not-found") {
124
- const msg = `No endpoints tagged "${loaded.tag}". Available tags: ${loaded.available.length ? loaded.available.join(", ") : "(none)"}`;
125
- if (options.json) printJson(jsonError("probe-mass-assignment", [msg]));
126
- else printWarning(msg);
127
- return 2;
128
- }
129
- const { endpoints: rawEndpoints, securitySchemes } = loaded;
130
-
131
- // m-17 / ARV-52: apply --include / --exclude through the unified
132
- // operation filter (m-15 ARV-9). probe-family was deferred at AC#6;
133
- // wiring it here closes that and gives mass-assignment parity with
134
- // probe-static / probe-security.
135
- let endpoints = rawEndpoints;
136
- if (options.include?.length || options.exclude?.length) {
137
- const compiled = compileOperationFilter({ includes: options.include, excludes: options.exclude });
138
- if (compiled.errors.length > 0) {
139
- const message = compiled.errors.join("\n");
140
- if (options.json) printJson(jsonError("probe-mass-assignment", [message]));
141
- else printError(message);
142
- return 2;
143
- }
144
- endpoints = endpoints.filter(compiled.filter);
145
- }
146
- // ARV-302: --max-endpoints cap — applied after include/exclude/tag
147
- // filters so the user-visible filter intent is preserved. The cap
148
- // is a coarse time-budget knob (`zond audit --budget standard` →
149
- // 50 endpoints), not a sampling strategy: take the first N from
150
- // the filtered list. A warning fires so partial output is obvious.
151
- let cappedEndpoints = false;
152
- if (typeof options.maxEndpoints === "number" && options.maxEndpoints > 0
153
- && endpoints.length > options.maxEndpoints) {
154
- const total = endpoints.length;
155
- endpoints = endpoints.slice(0, options.maxEndpoints);
156
- cappedEndpoints = true;
157
- if (!options.json) {
158
- process.stderr.write(
159
- `zond: probe mass-assignment capped at ${options.maxEndpoints}/${total} endpoints (--max-endpoints) — pass --max-endpoints <N> or --budget full to widen.\n`,
160
- );
161
- }
162
- }
163
-
164
- // Load env vars (base_url, auth_token, api_key, path-param overrides).
165
- let vars: Record<string, string> = {};
166
- if (options.env) {
167
- const fromFile = await loadEnvFile(options.env);
168
- if (!fromFile) {
169
- const msg = `Environment file not found: ${options.env}`;
170
- if (options.json) printJson(jsonError("probe-mass-assignment", [msg]));
171
- else printError(msg);
172
- return 2;
173
- }
174
- vars = fromFile;
175
- } else {
176
- vars = await loadEnvironment();
177
- }
178
-
179
- // m-17 / ARV-52: --dry-run lists which endpoints + suspect fields the
180
- // probe would touch without sending live traffic. base_url is not
181
- // required on this path (mirrors probe-security).
182
- if (options.dryRun) {
183
- const probe = new MassAssignmentProbe();
184
- const plan = await probe.dryRun({
185
- specPath: options.specPath,
186
- endpoints,
187
- securitySchemes,
188
- vars,
189
- options: {},
190
- });
191
- const data = summarizeDryRun(plan);
192
- // ARV-317: persist the planned inventory when --output is given, so it
193
- // survives beyond stdout (an agent can read it back). --emit-tests is
194
- // skipped on dry-run — there are no findings to turn into regression
195
- // suites; the plan digest is the dry-run deliverable.
196
- // ARV-321: say so explicitly. A silent no-op read as a bug on the live
197
- // Stripe run (report-zond friction, 2026-07-02) — the target dir stayed
198
- // empty with zero signal that --emit-tests was ignored.
199
- if (options.emitTests) {
200
- printWarning(`--emit-tests skipped: safe/dry-run mode has no live verdicts to lock in as regression suites. Re-run with --live to emit ${options.emitTests}.`);
201
- }
202
- if (options.output) {
203
- const payload = options.report === "json"
204
- ? JSON.stringify(data, null, 2)
205
- : formatDryRunDigest(plan);
206
- await mkdir(join(options.output, "..").replace(/\/\.$/, ""), { recursive: true }).catch(() => {});
207
- rotateOutputTarget(options.output, { overwrite: options.overwrite });
208
- await writeFile(options.output, payload + "\n", "utf-8");
209
- }
210
- if (options.json) {
211
- printJson(jsonOk("probe-mass-assignment", data));
212
- } else {
213
- console.log(formatDryRunDigest(plan));
214
- }
215
- return 0;
216
- }
217
-
218
- if (!vars["base_url"]) {
219
- const msg = "base_url is required (set in .env.yaml or via --env file). Probing requires a live API.";
220
- if (options.json) printJson(jsonError("probe-mass-assignment", [msg]));
221
- else printError(msg);
222
- return 2;
223
- }
224
-
225
- // TASK-259: tell the user *before* we mutate anything. Suppressed in
226
- // --json mode (warnings already in envelope) and when --no-cleanup
227
- // is off — this banner is about the cleanup-pass, too.
228
- printMutationBanner("probe-mass-assignment", vars, { quiet: options.json === true });
229
-
230
- // ARV-265: capture HTTP touches for audit-coverage. Dry-run never
231
- // reaches this branch (early return above), so AC#5 holds.
232
- const { withHttpAudit, beginAuditRun, finalizeAuditRun, auditRecordToCase, checksPersistEnabled } =
233
- await import("../../../core/audit/persist.ts");
234
- const auditEnabled = checksPersistEnabled();
235
- // ARV-269: lift agent-authored `seed_body` overlays out of
236
- // `.api-resources.local.yaml` so create-endpoint baselines win against
237
- // strict validators. No-op when --api isn't set (raw --spec invocation).
238
- const seedBodies = await loadSeedBodyOverlays(options.apiName);
239
- const { value: result, records: auditRecords } = await withHttpAudit(async () =>
240
- runMassAssignmentProbes({
241
- endpoints,
242
- securitySchemes,
243
- vars,
244
- noCleanup: options.noCleanup,
245
- timeoutMs: options.timeoutMs,
246
- discover: !options.noDiscover,
247
- extraSuspectFields: parseSuspectFieldFlags(options.suspectField),
248
- seedBodies,
249
- }),
250
- );
251
-
252
- if (auditEnabled && auditRecords.length > 0) {
253
- try {
254
- const { getDb } = await import("../../../db/schema.ts");
255
- const { findCollectionByNameOrId } = await import("../../../db/queries.ts");
256
- const { readCurrentSession } = await import("../../../core/context/session.ts");
257
- getDb();
258
- const collectionId = options.apiName ? findCollectionByNameOrId(options.apiName)?.id : undefined;
259
- const session = readCurrentSession();
260
- const runId = beginAuditRun({
261
- runKind: "probe",
262
- ...(collectionId != null ? { collectionId } : {}),
263
- ...(session?.id ? { sessionId: session.id } : {}),
264
- tags: ["probe", "mass-assignment"],
265
- });
266
- const suiteFile = `apis/${options.apiName ?? "_"}/probes/mass-assignment.yaml`;
267
- finalizeAuditRun(runId, auditRecords.map((rec) =>
268
- auditRecordToCase(rec, {
269
- suiteName: "probe/mass-assignment",
270
- suiteFile,
271
- testName: `mass-assignment::${rec.request.method.toUpperCase()} ${rec.request.url}`,
272
- }),
273
- ));
274
- } catch (err) {
275
- process.stderr.write(`zond: audit persistence failed (${(err as Error).message}).\n`);
276
- }
277
- }
278
-
279
- // ARV-252: filter verdicts for display under the evidence-chain
280
- // principle. Silently-ignored (correct framework behaviour) never
281
- // surfaces; absent-but-unverifiable surfaces only under --verbose.
282
- // HIGH and inconclusive-baseline/5xx always show. JSON envelope
283
- // always carries the full unfiltered list (agents triage explicitly).
284
- const displayResult: MassAssignmentResult = {
285
- ...result,
286
- verdicts: filterVerdictsForDisplay(result.verdicts, { verbose: options.verbose === true }),
287
- };
288
-
289
- // TASK-168 (m-10): vars came from .env.yaml — register them so any
290
- // echoed token (URL, body, header) gets redacted in the digest.
291
- getSecretRegistry().registerAll(vars);
292
- const md = applySanitizer(formatDigestMarkdown(displayResult, options.specPath));
293
-
294
- // m-17 / ARV-51: --output writes whichever format `--report` selected
295
- // (default markdown). `--json` envelope is always structured.
296
- const reportFmt: "markdown" | "json" = options.report ?? "markdown";
297
- const structuredEndpoints = buildMaStructuredEndpoints(result);
298
- if (options.output) {
299
- await mkdir(join(options.output, "..").replace(/\/\.$/, ""), { recursive: true }).catch(() => {});
300
- // TASK-162 (m-9 P6): rotate previous digest to <stem>-vN.md instead
301
- // of silent overwrite. --overwrite opts back into the old behaviour.
302
- rotateOutputTarget(options.output, { overwrite: options.overwrite });
303
- const payload = reportFmt === "json"
304
- ? JSON.stringify(maStructuredReport(result, structuredEndpoints), null, 2) + "\n"
305
- : md;
306
- await writeFile(options.output, payload, "utf-8");
307
- }
308
-
309
- let emittedSuites: Array<{ file: string; suite: string; tests: number }> = [];
310
- if (options.emitTests) {
311
- const suites = emitRegressionSuites(result, endpoints, securitySchemes);
312
- const written = await writeProbeSuites({
313
- output: options.emitTests,
314
- suites,
315
- command: "zond probe-mass-assignment --emit-tests",
316
- headerExample: `zond probe-mass-assignment --api <name> --emit-tests ${options.emitTests}`,
317
- });
318
- emittedSuites = written.files;
319
- }
320
-
321
- const counts = tallyBySeverity(result.verdicts, MA_BUCKETS, MA_ZERO);
322
- const orphans = countCleanupFailures(result.verdicts);
323
-
324
- if (options.json) {
325
- // m-17 / ARV-51: structured envelope; no `data.digest.stdout`.
326
- printJson(
327
- jsonOk("probe-mass-assignment", {
328
- endpoints: structuredEndpoints,
329
- summary: {
330
- totalEndpoints: result.totalEndpoints,
331
- probed: result.specProbed,
332
- by_status: maByStatus(structuredEndpoints),
333
- },
334
- orphans,
335
- warnings: cappedEndpoints
336
- ? [
337
- ...result.warnings,
338
- `--max-endpoints capped this run at ${options.maxEndpoints}; pass --max-endpoints <N> or --budget full to widen.`,
339
- ]
340
- : result.warnings,
341
- emittedTests: emittedSuites,
342
- }),
343
- );
344
- } else {
345
- if (!options.output) {
346
- if (reportFmt === "json") {
347
- process.stdout.write(JSON.stringify(maStructuredReport(result, structuredEndpoints), null, 2) + "\n");
348
- } else {
349
- console.log(md);
350
- }
351
- } else printSuccess(`${reportFmt === "json" ? "Structured report" : "Digest"} written to ${options.output}`);
352
- console.log("");
353
- console.log(formatSummaryLine(counts, MA_SUMMARY));
354
- if (emittedSuites.length > 0) {
355
- printSuccess(`Emitted ${emittedSuites.length} regression suite(s) in ${options.emitTests}`);
356
- console.log(` Run them on CI: zond run ${options.emitTests} --env ${options.env ?? ".env.yaml"}`);
357
- } else if (options.emitTests) {
358
- console.log(`No findings to emit. Directory ${options.emitTests} not created.`);
359
- }
360
- if (counts.high > 0) {
361
- printWarning(`${counts.high} HIGH-severity finding(s) — privilege escalation candidates. Review the digest.`);
362
- }
363
- if (counts.inconclusiveBaseline > 0) {
364
- printWarning(
365
- `${counts.inconclusiveBaseline} endpoint(s) had baseline POST failures — fix env fixtures (FK ids / path-params) and re-run. These are excluded from --emit-tests on purpose.`,
366
- );
367
- }
368
- // TASK-259: cleanup-failure surfaces as "orphans" in summary. 404 was
369
- // already filtered out (resource gone is success). Prompt for manual
370
- // cleanup so the user doesn't discover the leak only via 5xx in CI.
371
- if (orphans > 0) {
372
- printWarning(
373
- `${orphans} orphan resource(s): cleanup DELETE failed (non-404). Manual cleanup may be needed — see digest "Cleanup DELETE: …" lines.`,
374
- );
375
- }
376
- // Stale-fixture hint when probes successfully cleaned up at least one
377
- // resource: that means we POSTed (and re-DELETEd) — `.env.yaml` slug/id
378
- // values for that resource type may now point at a tombstone.
379
- const cleanedCount = result.verdicts.filter(v => v.cleanup?.attempted && v.cleanup.status != null && v.cleanup.status < 400).length;
380
- if (cleanedCount > 0) {
381
- printWarning(
382
- `${cleanedCount} resource(s) created and deleted by probes. FK fixtures in .env.yaml may be stale — re-run \`zond prepare-fixtures --api <name>\` before next CRUD run.`,
383
- );
384
- }
385
- }
386
-
387
- // Non-zero exit when HIGH findings — useful for CI gating.
388
- return counts.high > 0 ? 1 : 0;
389
- } catch (err) {
390
- const message = err instanceof Error ? err.message : String(err);
391
- if (options.json) printJson(jsonError("probe-mass-assignment", [message]));
392
- else printError(message);
393
- return 2;
394
- }
395
- }
396
-
397
- // ──────────────────────────────────────────────
398
- // TASK-146: --emit-template short-circuit
399
- // ──────────────────────────────────────────────
400
-
401
- import { buildMassAssignmentTemplate } from "../../../core/probe/mass-assignment-template.ts";
402
-
403
- export interface EmitTemplateCliOptions {
404
- specPath: string;
405
- /** "METHOD:/path", e.g. "POST:/users" or "POST /users". */
406
- methodPath: string;
407
- output?: string;
408
- json?: boolean;
409
- }
410
-
411
- export async function emitMassAssignmentTemplateCommand(
412
- options: EmitTemplateCliOptions,
413
- ): Promise<number> {
414
- const parsed = parseMethodPath(options.methodPath);
415
- if (!parsed) {
416
- const msg = `--emit-template expects "METHOD:/path" (e.g. "POST:/users"), got: ${options.methodPath}`;
417
- if (options.json) printJson(jsonError("probe-mass-assignment", [msg]));
418
- else printError(msg);
419
- return 2;
420
- }
421
-
422
- try {
423
- const result = await buildMassAssignmentTemplate({
424
- specPath: options.specPath,
425
- method: parsed.method,
426
- path: parsed.path,
427
- });
428
-
429
- if (result.kind === "endpoint-not-found") {
430
- const lines = [`endpoint not found: ${parsed.method} ${parsed.path}`];
431
- if (result.nearest.length > 0) {
432
- lines.push(`nearest paths with method ${parsed.method}: ${result.nearest.join(", ")}`);
433
- }
434
- const msg = lines.join("\n");
435
- if (options.json) printJson(jsonError("probe-mass-assignment", [msg]));
436
- else printError(msg);
437
- return 2;
438
- }
439
-
440
- if (options.output) {
441
- await mkdir(join(options.output, "..").replace(/[^/]+$/, ""), { recursive: true }).catch(() => {});
442
- await writeFile(options.output, result.yaml, "utf-8");
443
- if (options.json) {
444
- printJson(
445
- jsonOk("probe-mass-assignment", {
446
- template: { file: options.output, chain: result.chain, protectedFields: result.protectedFields },
447
- }),
448
- );
449
- } else {
450
- printSuccess(`Template written to ${options.output} (chain=${result.chain})`);
451
- if (result.protectedFields.length > 0) {
452
- console.log(` readOnly/x-zond-protected fields injected: ${result.protectedFields.join(", ")}`);
453
- }
454
- }
455
- } else {
456
- if (options.json) {
457
- printJson(
458
- jsonOk("probe-mass-assignment", {
459
- template: { yaml: result.yaml, chain: result.chain, protectedFields: result.protectedFields },
460
- }),
461
- );
462
- } else {
463
- process.stdout.write(result.yaml);
464
- }
465
- }
466
- return 0;
467
- } catch (err) {
468
- const message = err instanceof Error ? err.message : String(err);
469
- if (options.json) printJson(jsonError("probe-mass-assignment", [message]));
470
- else printError(message);
471
- return 2;
472
- }
473
- }
474
-
475
- function parseMethodPath(s: string): { method: string; path: string } | null {
476
- const m = s.match(/^\s*([A-Za-z]+)\s*[: ]\s*(\/.*?)\s*$/);
477
- if (!m) return null;
478
- return { method: m[1]!.toUpperCase(), path: m[2]! };
479
- }
480
-
481
- // m-17 / ARV-51: structured per-endpoint shape for mass-assignment.
482
-
483
- /**
484
- * ARV-252: parse repeatable `--suspect-field name=value` flags into the
485
- * extra-fields map. Values are kept as strings — generateFromSchema /
486
- * sentinel inference happens server-side via the suspect-fields machinery.
487
- * Malformed entries (no `=`) are skipped silently rather than failing the
488
- * run — this keeps ad-hoc CLI usage forgiving.
489
- */
490
- function parseSuspectFieldFlags(raw: string[] | undefined): Record<string, unknown> | undefined {
491
- if (!raw || raw.length === 0) return undefined;
492
- const out: Record<string, unknown> = {};
493
- for (const entry of raw) {
494
- const eq = entry.indexOf("=");
495
- if (eq <= 0) continue;
496
- const name = entry.slice(0, eq).trim();
497
- const value = entry.slice(eq + 1);
498
- if (name) out[name] = value;
499
- }
500
- return Object.keys(out).length > 0 ? out : undefined;
501
- }
502
-
503
- /**
504
- * ARV-252: filter verdicts for the digest/console display under the
505
- * evidence-chain principle.
506
- *
507
- * - HIGH (applied) — always show; this is the actual finding.
508
- * - inconclusive-baseline / inconclusive-5xx / ok / skipped — always
509
- * show; operator needs them to triage probe coverage.
510
- * - INFO with at least one `absent` outcome (couldn't verify via
511
- * follow-up GET) — show only under --verbose. This is the "single
512
- * signal, no proof" case.
513
- * - INFO with only `ignored` outcomes (silently dropped — correct
514
- * framework behaviour) — NEVER show. Reports must not noise-floor
515
- * on intentional behaviour.
516
- *
517
- * JSON envelope is unfiltered; this is a display-layer transform only.
518
- */
519
- function filterVerdictsForDisplay(
520
- verdicts: EndpointVerdict[],
521
- opts: { verbose: boolean },
522
- ): EndpointVerdict[] {
523
- return verdicts.filter((v) => {
524
- if (v.severity !== "info") return true;
525
- const hasAbsent = v.fields.some((f) => f.outcome === "absent");
526
- if (!hasAbsent) return false; // silently-ignored: always hidden
527
- return opts.verbose;
528
- });
529
- }
530
-
531
- function maStatusFromSeverity(s: EndpointVerdict["severity"]): ProbeEndpointStatus {
532
- switch (s) {
533
- case "high": return "high";
534
- case "low":
535
- case "medium":
536
- case "info":
537
- return "low";
538
- case "ok": return "ok";
539
- case "skipped": return "skipped";
540
- case "inconclusive-baseline":
541
- case "inconclusive-5xx":
542
- return "inconclusive";
543
- }
544
- }
545
-
546
- function maFindingSeverity(s: EndpointVerdict["severity"]): ProbeFindingSeverity {
547
- if (s === "high") return "high";
548
- if (s === "low" || s === "medium") return "low";
549
- if (s === "ok") return "ok";
550
- return "inconclusive";
551
- }
552
-
553
- function buildMaStructuredEndpoints(result: MassAssignmentResult): ProbeEndpointResult[] {
554
- return result.verdicts.map((v) => ({
555
- path: v.path,
556
- method: v.method,
557
- classes_run: ["mass-assignment"],
558
- findings: v.severity === "skipped" || v.severity === "ok"
559
- ? []
560
- : [{
561
- class: "mass-assignment",
562
- severity: maFindingSeverity(v.severity),
563
- evidence: {
564
- summary: v.summary,
565
- request: { url: v.request.url, injectedFields: v.request.injectedFields },
566
- ...(v.response ? { response: { status: v.response.status } } : {}),
567
- ...(v.fields ? { fields: v.fields } : {}),
568
- ...(v.recommended_action ? { recommended_action: v.recommended_action } : {}),
569
- },
570
- }],
571
- status: maStatusFromSeverity(v.severity),
572
- ...(v.severity === "skipped" ? { skip_reason: v.skipReason ?? v.summary } : {}),
573
- }));
574
- }
575
-
576
- function maByStatus(endpoints: ProbeEndpointResult[]): Record<ProbeEndpointStatus, number> {
577
- const out: Record<ProbeEndpointStatus, number> = {
578
- ok: 0, high: 0, low: 0, inconclusive: 0, skipped: 0,
579
- };
580
- for (const e of endpoints) out[e.status]++;
581
- return out;
582
- }
583
-
584
- function maStructuredReport(result: MassAssignmentResult, endpoints: ProbeEndpointResult[]): object {
585
- return {
586
- endpoints,
587
- summary: {
588
- totalEndpoints: result.totalEndpoints,
589
- probed: result.specProbed,
590
- by_status: maByStatus(endpoints),
591
- },
592
- };
593
- }
594
-