@kirrosh/zond 0.22.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.
Files changed (282) hide show
  1. package/CHANGELOG.md +811 -0
  2. package/README.md +59 -6
  3. package/package.json +9 -7
  4. package/src/CLAUDE.md +112 -0
  5. package/src/cli/argv.ts +122 -0
  6. package/src/cli/commands/add-api.ts +146 -0
  7. package/src/cli/commands/api/annotate/idempotency.ts +59 -0
  8. package/src/cli/commands/api/annotate/index.ts +880 -0
  9. package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
  10. package/src/cli/commands/api/annotate/overlay.ts +206 -0
  11. package/src/cli/commands/api/annotate/pagination.ts +64 -0
  12. package/src/cli/commands/api/annotate/prompts.ts +220 -0
  13. package/src/cli/commands/api/annotate/readback.ts +58 -0
  14. package/src/cli/commands/api/annotate/resources.ts +91 -0
  15. package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
  16. package/src/cli/commands/audit.ts +786 -0
  17. package/src/cli/commands/catalog.ts +35 -0
  18. package/src/cli/commands/check.ts +361 -0
  19. package/src/cli/commands/checks.ts +1072 -0
  20. package/src/cli/commands/ci-init.ts +43 -0
  21. package/src/cli/commands/clean.ts +212 -0
  22. package/src/cli/commands/cleanup.ts +236 -0
  23. package/src/cli/commands/completions.ts +16 -0
  24. package/src/cli/commands/coverage.ts +823 -132
  25. package/src/cli/commands/db.ts +486 -12
  26. package/src/cli/commands/describe.ts +37 -2
  27. package/src/cli/commands/discover.ts +1356 -0
  28. package/src/cli/commands/doctor.ts +661 -0
  29. package/src/cli/commands/fixtures.ts +402 -0
  30. package/src/cli/commands/generate.ts +438 -47
  31. package/src/cli/commands/init/bootstrap.ts +34 -2
  32. package/src/cli/commands/{init.ts → init/index.ts} +99 -5
  33. package/src/cli/commands/init/skills.ts +99 -3
  34. package/src/cli/commands/init/templates/agents.md +77 -64
  35. package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
  36. package/src/cli/commands/init/templates/skills/zond-checks.md +621 -0
  37. package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
  38. package/src/cli/commands/init/templates/skills/zond-triage.md +272 -0
  39. package/src/cli/commands/init/templates/skills/zond.md +802 -125
  40. package/src/cli/commands/init/templates/zond-config.yml +8 -9
  41. package/src/cli/commands/prepare-fixtures.ts +97 -0
  42. package/src/cli/commands/probe/_seed-bodies.ts +52 -0
  43. package/src/cli/commands/probe/mass-assignment.ts +594 -0
  44. package/src/cli/commands/probe/security.ts +537 -0
  45. package/src/cli/commands/probe/static.ts +255 -0
  46. package/src/cli/commands/probe/webhooks.ts +163 -0
  47. package/src/cli/commands/probe.ts +535 -0
  48. package/src/cli/commands/reference.ts +87 -0
  49. package/src/cli/commands/refresh-api.ts +227 -0
  50. package/src/cli/commands/remove-api.ts +150 -0
  51. package/src/cli/commands/report-bundle.ts +310 -0
  52. package/src/cli/commands/report.ts +241 -0
  53. package/src/cli/commands/request.ts +495 -4
  54. package/src/cli/commands/run.ts +870 -53
  55. package/src/cli/commands/schema-from-runs.ts +128 -0
  56. package/src/cli/commands/secrets.ts +133 -0
  57. package/src/cli/commands/session.ts +244 -0
  58. package/src/cli/commands/use.ts +18 -1
  59. package/src/cli/index.ts +20 -3
  60. package/src/cli/json-envelope.ts +92 -3
  61. package/src/cli/json-schemas.ts +314 -0
  62. package/src/cli/output.ts +17 -1
  63. package/src/cli/program.ts +199 -635
  64. package/src/cli/resolve.ts +105 -0
  65. package/src/cli/safe-live.ts +24 -0
  66. package/src/cli/status-filter.ts +114 -0
  67. package/src/cli/util/api-context.ts +85 -0
  68. package/src/cli/version.ts +5 -0
  69. package/src/core/audit/persist.ts +183 -0
  70. package/src/core/checks/budget.ts +59 -0
  71. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  72. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  73. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  74. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  75. package/src/core/checks/checks/cross_call_references.ts +147 -0
  76. package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
  77. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  78. package/src/core/checks/checks/idempotency_replay.ts +242 -0
  79. package/src/core/checks/checks/ignored_auth.ts +254 -0
  80. package/src/core/checks/checks/index.ts +68 -0
  81. package/src/core/checks/checks/lifecycle_transitions.ts +416 -0
  82. package/src/core/checks/checks/missing_required_header.ts +40 -0
  83. package/src/core/checks/checks/negative_data_rejection.ts +148 -0
  84. package/src/core/checks/checks/not_a_server_error.ts +35 -0
  85. package/src/core/checks/checks/open_cors_on_sensitive.ts +160 -0
  86. package/src/core/checks/checks/pagination_invariants.ts +419 -0
  87. package/src/core/checks/checks/positive_data_acceptance.ts +33 -0
  88. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  89. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  90. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  91. package/src/core/checks/checks/status_code_conformance.ts +132 -0
  92. package/src/core/checks/checks/unsupported_method.ts +63 -0
  93. package/src/core/checks/checks/use_after_free.ts +78 -0
  94. package/src/core/checks/index.ts +30 -0
  95. package/src/core/checks/mode.ts +82 -0
  96. package/src/core/checks/recommended-action.ts +68 -0
  97. package/src/core/checks/registry.ts +78 -0
  98. package/src/core/checks/runner.ts +1461 -0
  99. package/src/core/checks/sarif.ts +230 -0
  100. package/src/core/checks/spec-findings.ts +308 -0
  101. package/src/core/checks/stateful.ts +121 -0
  102. package/src/core/checks/types.ts +305 -0
  103. package/src/core/checks/zond-extensions.ts +73 -0
  104. package/src/core/classifier/recommended-action.ts +251 -0
  105. package/src/core/context/current.ts +22 -6
  106. package/src/core/context/session.ts +78 -0
  107. package/src/core/coverage/loader.ts +216 -0
  108. package/src/core/coverage/reasons.ts +300 -0
  109. package/src/core/diagnostics/db-analysis.ts +293 -59
  110. package/src/core/diagnostics/failure-class.ts +140 -0
  111. package/src/core/diagnostics/failure-hints.ts +88 -89
  112. package/src/core/diagnostics/spec-pointer.ts +99 -0
  113. package/src/core/diagnostics/suggested-fixes.ts +155 -0
  114. package/src/core/exporter/case-study/index.ts +270 -0
  115. package/src/core/exporter/curl.ts +40 -0
  116. package/src/core/exporter/exporter.ts +48 -0
  117. package/src/core/exporter/html-report/escape.ts +24 -0
  118. package/src/core/exporter/html-report/index.ts +479 -0
  119. package/src/core/exporter/html-report/script.ts +100 -0
  120. package/src/core/exporter/html-report/styles.ts +408 -0
  121. package/src/core/generator/chunker.ts +38 -19
  122. package/src/core/generator/coverage-phase.ts +0 -0
  123. package/src/core/generator/data-factory.ts +586 -22
  124. package/src/core/generator/describe.ts +1 -1
  125. package/src/core/generator/fixtures-builder.ts +332 -0
  126. package/src/core/generator/index.ts +5 -5
  127. package/src/core/generator/openapi-reader.ts +135 -7
  128. package/src/core/generator/path-param-disambig.ts +140 -0
  129. package/src/core/generator/resources-builder.ts +898 -0
  130. package/src/core/generator/schema-utils.ts +33 -3
  131. package/src/core/generator/serializer.ts +103 -13
  132. package/src/core/generator/suite-generator.ts +583 -122
  133. package/src/core/generator/types.ts +14 -0
  134. package/src/core/identity/identity-file.ts +0 -0
  135. package/src/core/lint/affects.ts +28 -0
  136. package/src/core/lint/config.ts +96 -0
  137. package/src/core/lint/format.ts +42 -0
  138. package/src/core/lint/index.ts +94 -0
  139. package/src/core/lint/reporter.ts +128 -0
  140. package/src/core/lint/rules/consistency.ts +158 -0
  141. package/src/core/lint/rules/heuristics.ts +97 -0
  142. package/src/core/lint/rules/strictness.ts +109 -0
  143. package/src/core/lint/types.ts +96 -0
  144. package/src/core/lint/walker.ts +248 -0
  145. package/src/core/meta/meta-store.ts +6 -73
  146. package/src/core/output/README.md +73 -0
  147. package/src/core/output/index.ts +13 -0
  148. package/src/core/output/run.ts +91 -0
  149. package/src/core/output/types.ts +122 -0
  150. package/src/core/parser/dynamic-values.ts +160 -0
  151. package/src/core/parser/env-interpolation.ts +104 -0
  152. package/src/core/parser/filter.ts +57 -0
  153. package/src/core/parser/schema.ts +129 -4
  154. package/src/core/parser/types.ts +19 -1
  155. package/src/core/parser/variables.ts +0 -0
  156. package/src/core/parser/yaml-parser.ts +58 -12
  157. package/src/core/probe/bootstrap.ts +34 -0
  158. package/src/core/probe/dry-run-envelope.ts +61 -0
  159. package/src/core/probe/mass-assignment/classify.ts +175 -0
  160. package/src/core/probe/mass-assignment/cleanup.ts +52 -0
  161. package/src/core/probe/mass-assignment/digest.ts +114 -0
  162. package/src/core/probe/mass-assignment/orchestrator.ts +459 -0
  163. package/src/core/probe/mass-assignment/regression.ts +141 -0
  164. package/src/core/probe/mass-assignment/suspects.ts +92 -0
  165. package/src/core/probe/mass-assignment/types.ts +135 -0
  166. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  167. package/src/core/probe/mass-assignment-probe.ts +27 -0
  168. package/src/core/probe/mass-assignment-template.ts +240 -0
  169. package/src/core/probe/method-probe.ts +43 -76
  170. package/src/core/probe/method-shared.ts +69 -0
  171. package/src/core/probe/negative-probe.ts +183 -149
  172. package/src/core/probe/orphan-tracker.ts +188 -0
  173. package/src/core/probe/path-discovery.ts +439 -0
  174. package/src/core/probe/probe-harness.ts +119 -0
  175. package/src/core/probe/registry.ts +89 -0
  176. package/src/core/probe/runner.ts +136 -0
  177. package/src/core/probe/security/baseline.ts +174 -0
  178. package/src/core/probe/security/classify.ts +341 -0
  179. package/src/core/probe/security/cleanup.ts +125 -0
  180. package/src/core/probe/security/detectors.ts +71 -0
  181. package/src/core/probe/security/digest.ts +104 -0
  182. package/src/core/probe/security/orchestrator.ts +398 -0
  183. package/src/core/probe/security/regression.ts +103 -0
  184. package/src/core/probe/security/types.ts +151 -0
  185. package/src/core/probe/security-probe-class.ts +207 -0
  186. package/src/core/probe/security-probe.ts +32 -0
  187. package/src/core/probe/shared.ts +531 -0
  188. package/src/core/probe/static-probe-class.ts +125 -0
  189. package/src/core/probe/types.ts +165 -0
  190. package/src/core/probe/verdict-aggregator.ts +33 -0
  191. package/src/core/probe/webhooks-probe.ts +282 -0
  192. package/src/core/reporter/console.ts +41 -2
  193. package/src/core/reporter/index.ts +2 -3
  194. package/src/core/reporter/json.ts +11 -1
  195. package/src/core/reporter/junit.ts +27 -12
  196. package/src/core/reporter/ndjson.ts +37 -0
  197. package/src/core/reporter/types.ts +3 -0
  198. package/src/core/runner/assertions.ts +59 -2
  199. package/src/core/runner/async-pool.ts +108 -0
  200. package/src/core/runner/auth-path.ts +8 -0
  201. package/src/core/runner/ci-context.ts +72 -0
  202. package/src/core/runner/executor.ts +265 -36
  203. package/src/core/runner/form-encode.ts +41 -0
  204. package/src/core/runner/http-client.ts +112 -2
  205. package/src/core/runner/learn-drift.ts +293 -0
  206. package/src/core/runner/preflight-vars.ts +153 -0
  207. package/src/core/runner/progress-tracker.ts +73 -0
  208. package/src/core/runner/rate-limiter.ts +87 -33
  209. package/src/core/runner/run-kind.ts +45 -0
  210. package/src/core/runner/schema-validator.ts +308 -0
  211. package/src/core/runner/send-request.ts +158 -20
  212. package/src/core/runner/types.ts +44 -0
  213. package/src/core/secrets/registry.ts +164 -0
  214. package/src/core/secrets/secrets-file.ts +115 -0
  215. package/src/core/selectors/operation-filter.ts +144 -0
  216. package/src/core/setup-api.ts +457 -20
  217. package/src/core/severity/category.ts +94 -0
  218. package/src/core/severity/index.ts +58 -0
  219. package/src/core/spec/infer-schema.ts +102 -0
  220. package/src/core/spec/layers.ts +154 -0
  221. package/src/core/spec/merge-specs.ts +156 -0
  222. package/src/core/spec/schema-from-runs.ts +117 -0
  223. package/src/core/spec/schema-overlay.ts +130 -0
  224. package/src/core/util/ajv.ts +13 -0
  225. package/src/core/util/format-eta.ts +21 -0
  226. package/src/core/util/headers.ts +9 -0
  227. package/src/core/util/url.ts +24 -0
  228. package/src/core/utils.ts +5 -1
  229. package/src/core/workspace/config.ts +129 -0
  230. package/src/core/workspace/fixture-gap-report.ts +84 -0
  231. package/src/core/workspace/fixture-gaps.ts +71 -0
  232. package/src/core/workspace/manifest.ts +283 -0
  233. package/src/core/workspace/output-rotation.ts +62 -0
  234. package/src/core/workspace/root.ts +13 -11
  235. package/src/core/workspace/triage-path.ts +87 -0
  236. package/src/db/lint-runs.ts +47 -0
  237. package/src/db/migrate.ts +128 -0
  238. package/src/db/migrations/0001_run_kind.sql +25 -0
  239. package/src/db/migrations/0002_run_kind_request.sql +59 -0
  240. package/src/db/migrations/sql.d.ts +4 -0
  241. package/src/db/queries/collections.ts +133 -0
  242. package/src/db/queries/coverage.ts +9 -0
  243. package/src/db/queries/dashboard.ts +59 -0
  244. package/src/db/queries/results.ts +216 -0
  245. package/src/db/queries/runs.ts +289 -0
  246. package/src/db/queries/sessions.ts +42 -0
  247. package/src/db/queries/settings.ts +28 -0
  248. package/src/db/queries/types.ts +172 -0
  249. package/src/db/queries.ts +75 -802
  250. package/src/db/schema.ts +178 -50
  251. package/src/cli/commands/export.ts +0 -144
  252. package/src/cli/commands/guide.ts +0 -127
  253. package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
  254. package/src/cli/commands/probe-methods.ts +0 -108
  255. package/src/cli/commands/probe-validation.ts +0 -124
  256. package/src/cli/commands/serve.ts +0 -114
  257. package/src/cli/commands/sync.ts +0 -268
  258. package/src/cli/commands/update.ts +0 -189
  259. package/src/cli/commands/validate.ts +0 -34
  260. package/src/core/diagnostics/render-md.ts +0 -112
  261. package/src/core/exporter/postman.ts +0 -963
  262. package/src/core/generator/guide-builder.ts +0 -253
  263. package/src/core/meta/types.ts +0 -19
  264. package/src/core/parser/index.ts +0 -21
  265. package/src/core/runner/execute-run.ts +0 -132
  266. package/src/core/runner/index.ts +0 -12
  267. package/src/core/sync/spec-differ.ts +0 -38
  268. package/src/web/data/collection-state.ts +0 -362
  269. package/src/web/routes/api.ts +0 -314
  270. package/src/web/routes/dashboard.ts +0 -350
  271. package/src/web/routes/runs.ts +0 -64
  272. package/src/web/schemas.ts +0 -121
  273. package/src/web/server.ts +0 -134
  274. package/src/web/static/htmx.min.cjs +0 -1
  275. package/src/web/static/style.css +0 -1148
  276. package/src/web/views/endpoints-tab.ts +0 -174
  277. package/src/web/views/explorer-tab.ts +0 -402
  278. package/src/web/views/health-strip.ts +0 -92
  279. package/src/web/views/layout.ts +0 -48
  280. package/src/web/views/results.ts +0 -210
  281. package/src/web/views/runs-tab.ts +0 -126
  282. package/src/web/views/suites-tab.ts +0 -181
@@ -0,0 +1,594 @@
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
+