@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,141 @@
1
+ import type { EndpointInfo, SecuritySchemeInfo } from "../../generator/types.ts";
2
+ import { flattenToFormFields } from "../../runner/form-encode.ts";
3
+ import type { RawStep, RawSuite } from "../../generator/serializer.ts";
4
+ import {
5
+ captureFieldFor,
6
+ convertPath,
7
+ endpointStem,
8
+ findDeleteCounterpart,
9
+ findGetByIdCounterpart,
10
+ getAuthHeaders,
11
+ } from "../shared.ts";
12
+ import { findIdParam } from "./classify.ts";
13
+ import type {
14
+ EndpointVerdict,
15
+ MassAssignmentResult,
16
+ } from "./types.ts";
17
+
18
+ const ACCEPTABLE_4XX = [400, 401, 403, 409, 415, 422];
19
+
20
+ /**
21
+ * Emit YAML suites that lock in the safe behaviour observed during the live
22
+ * run:
23
+ * • rejected (4xx) → assert status ∈ ACCEPTABLE_4XX (no regression to 2xx).
24
+ * • accepted-and-ignored → assert 2xx and that injected fields don't echo
25
+ * back. Follow-up GET — when available — additionally asserts the field
26
+ * is not persisted.
27
+ *
28
+ * "applied" / "inconclusive" are deliberately NOT emitted: those are bugs to
29
+ * fix, not baselines to lock.
30
+ */
31
+ export function emitRegressionSuites(
32
+ result: MassAssignmentResult,
33
+ endpoints: EndpointInfo[],
34
+ schemes: SecuritySchemeInfo[],
35
+ ): RawSuite[] {
36
+ const suites: RawSuite[] = [];
37
+ for (const v of result.verdicts) {
38
+ // ARV-250: "info" carries the post-pivot semantics of the old "low"
39
+ // (extras silently ignored — useful regression even when severity is
40
+ // demoted). Both "low" (inconclusive) and "info" (ignored) qualify
41
+ // for the ignored-baseline suite.
42
+ const isIgnoredCase = v.severity === "low" || v.severity === "info";
43
+ if (v.severity !== "ok" && !isIgnoredCase) continue;
44
+ const ep = endpoints.find(e => e.path === v.path && e.method.toUpperCase() === v.method);
45
+ if (!ep) continue;
46
+ const suiteHeaders = getAuthHeaders(ep, schemes);
47
+ const probeExpectedStatus = v.severity === "ok" ? ACCEPTABLE_4XX : [200, 201, 202, 204];
48
+ // ARV-150: emit `form:` instead of `json:` when the endpoint uses
49
+ // form-urlencoded bodies — otherwise the regression suite would send
50
+ // JSON and re-hit the original "wrong content-type" 400.
51
+ const bodyField =
52
+ ep.requestBodyContentType === "application/x-www-form-urlencoded"
53
+ ? { form: flattenToFormFields(v.request.body) }
54
+ : { json: v.request.body };
55
+ const probeStep: RawStep = {
56
+ name: `mass-assignment: extras must ${v.severity === "ok" ? "be rejected" : "not apply"}`,
57
+ source: {
58
+ generator: "mass-assignment-probe",
59
+ endpoint: `${v.method} ${v.path}`,
60
+ response_branch: probeExpectedStatus.map(String).join("|"),
61
+ },
62
+ [v.method]: convertPath(ep.path),
63
+ ...bodyField,
64
+ expect: {
65
+ status: probeExpectedStatus,
66
+ },
67
+ };
68
+ const tests: RawStep[] = [probeStep];
69
+ // For ignored case + we have a follow-up GET → emit a verifying GET
70
+ // that asserts injected fields are absent / overridden.
71
+ if (isIgnoredCase && v.followUpGet) {
72
+ const idField = captureFieldFor(ep);
73
+ probeStep.expect.body = {
74
+ ...(probeStep.expect.body ?? {}),
75
+ [idField]: { capture: "created_id" },
76
+ };
77
+ const getEp = findGetByIdCounterpart(ep, endpoints);
78
+ if (getEp) {
79
+ const idParam = findIdParam(getEp);
80
+ const getStep: RawStep = {
81
+ name: `verify extras did not persist`,
82
+ source: {
83
+ generator: "mass-assignment-probe",
84
+ endpoint: `GET ${getEp.path}`,
85
+ response_branch: "200",
86
+ },
87
+ GET: convertPath(getEp.path).replace(`{{${idParam}}}`, "{{created_id}}"),
88
+ expect: {
89
+ status: 200,
90
+ body: extrasNotEqualAssertions(v),
91
+ },
92
+ };
93
+ tests.push(getStep);
94
+ }
95
+ // cleanup
96
+ const delEp = findDeleteCounterpart(ep, endpoints);
97
+ if (delEp) {
98
+ const idParam = findIdParam(delEp);
99
+ const delStep: RawStep = {
100
+ name: "cleanup",
101
+ source: {
102
+ generator: "mass-assignment-probe-cleanup",
103
+ endpoint: `DELETE ${delEp.path}`,
104
+ },
105
+ always: true,
106
+ DELETE: convertPath(delEp.path).replace(`{{${idParam}}}`, "{{created_id}}"),
107
+ expect: { status: [200, 202, 204, 404] },
108
+ } as RawStep & { always: boolean };
109
+ tests.push(delStep);
110
+ }
111
+ }
112
+ suites.push({
113
+ name: `mass-assignment ${v.method} ${v.path}`,
114
+ tags: ["probe-mass-assignment", v.severity === "ok" ? "rejected-baseline" : "ignored-baseline"],
115
+ source: {
116
+ type: "probe-suite",
117
+ generator: "mass-assignment-probe",
118
+ endpoint: `${v.method} ${v.path}`,
119
+ },
120
+ fileStem: `mass-assignment-${endpointStem(ep)}`,
121
+ base_url: "{{base_url}}",
122
+ ...(suiteHeaders ? { headers: suiteHeaders } : {}),
123
+ tests,
124
+ });
125
+ }
126
+ return suites;
127
+ }
128
+
129
+ function extrasNotEqualAssertions(v: EndpointVerdict): Record<string, Record<string, string>> {
130
+ const out: Record<string, Record<string, string>> = {};
131
+ for (const f of v.fields) {
132
+ if (f.outcome === "ignored" || f.outcome === "echoed-overwritten" || f.outcome === "absent") {
133
+ // Assert the suspicious value did NOT take effect. We check that the
134
+ // observed value (from the live GET) still holds — the API is allowed
135
+ // to echo a server default; what's forbidden is echoing OUR sentinel.
136
+ const expectedNotEqual = JSON.stringify(f.injected);
137
+ out[f.field] = { not_equals: expectedNotEqual };
138
+ }
139
+ }
140
+ return out;
141
+ }
@@ -0,0 +1,92 @@
1
+ import type { OpenAPIV3 } from "openapi-types";
2
+ import type { EndpointInfo } from "../../generator/types.ts";
3
+
4
+ /**
5
+ * Sentinel values are deliberately distinctive so that — if they appear in a
6
+ * follow-up GET response — we can be confident the server actually persisted
7
+ * them rather than coincidentally generating the same value.
8
+ */
9
+ export const SUSPECTED_FIELDS: Record<string, unknown> = {
10
+ is_admin: true,
11
+ is_system: true,
12
+ verified: true,
13
+ role: "admin",
14
+ account_id: "00000000-0000-0000-0000-00000000beef",
15
+ owner_id: "00000000-0000-0000-0000-00000000beef",
16
+ user_id: "00000000-0000-0000-0000-00000000beef",
17
+ };
18
+
19
+ /** Sentinel values for server-assigned fields lifted from response schema. */
20
+ const SERVER_FIELD_SENTINEL = {
21
+ uuid: "00000000-0000-0000-0000-00000000dead",
22
+ isoDate: "2000-01-01T00:00:00.000Z",
23
+ string: "zond-injected",
24
+ integer: -424242,
25
+ number: -424242,
26
+ boolean: false,
27
+ };
28
+
29
+ function requestPropertyNames(schema?: OpenAPIV3.SchemaObject): Set<string> {
30
+ const out = new Set<string>();
31
+ if (!schema) return out;
32
+ if (schema.properties) {
33
+ for (const k of Object.keys(schema.properties)) out.add(k);
34
+ }
35
+ for (const composite of [schema.allOf, schema.oneOf, schema.anyOf]) {
36
+ if (Array.isArray(composite)) {
37
+ for (const sub of composite) {
38
+ const s = sub as OpenAPIV3.SchemaObject;
39
+ if (s.properties) for (const k of Object.keys(s.properties)) out.add(k);
40
+ }
41
+ }
42
+ }
43
+ return out;
44
+ }
45
+
46
+ export function isStrictContract(schema?: OpenAPIV3.SchemaObject): boolean {
47
+ if (!schema) return false;
48
+ return schema.additionalProperties === false;
49
+ }
50
+
51
+ function pickServerFieldSentinel(s: OpenAPIV3.SchemaObject): unknown {
52
+ if (s.format === "uuid") return SERVER_FIELD_SENTINEL.uuid;
53
+ if (s.format === "date-time" || s.format === "date") return SERVER_FIELD_SENTINEL.isoDate;
54
+ switch (s.type) {
55
+ case "string": return SERVER_FIELD_SENTINEL.string;
56
+ case "integer": return SERVER_FIELD_SENTINEL.integer;
57
+ case "number": return SERVER_FIELD_SENTINEL.number;
58
+ case "boolean": return SERVER_FIELD_SENTINEL.boolean;
59
+ default: return SERVER_FIELD_SENTINEL.string;
60
+ }
61
+ }
62
+
63
+ /** Server-assigned fields = response 2xx schema props that don't appear in request schema. */
64
+ export function serverAssignedExtras(ep: EndpointInfo): Record<string, unknown> {
65
+ const reqProps = requestPropertyNames(ep.requestBodySchema);
66
+ const success = ep.responses.find(r => r.statusCode >= 200 && r.statusCode < 300 && r.schema);
67
+ const respProps = success?.schema?.properties;
68
+ const out: Record<string, unknown> = {};
69
+ if (!respProps) return out;
70
+ for (const [name, schema] of Object.entries(respProps)) {
71
+ if (reqProps.has(name)) continue;
72
+ out[name] = pickServerFieldSentinel(schema as OpenAPIV3.SchemaObject);
73
+ }
74
+ return out;
75
+ }
76
+
77
+ /** Extra fields that aren't legitimate request-body properties. */
78
+ export function suspectedExtras(
79
+ ep: EndpointInfo,
80
+ extra: Record<string, unknown> = {},
81
+ ): Record<string, unknown> {
82
+ const reqProps = requestPropertyNames(ep.requestBodySchema);
83
+ const out: Record<string, unknown> = {};
84
+ // ARV-252: per-run extras (CLI --suspect-field) compose with the
85
+ // curated SUSPECTED_FIELDS list. Later additions win on key collision
86
+ // so a user can override a sentinel value if needed.
87
+ const merged: Record<string, unknown> = { ...SUSPECTED_FIELDS, ...extra };
88
+ for (const [name, value] of Object.entries(merged)) {
89
+ if (!reqProps.has(name)) out[name] = value;
90
+ }
91
+ return out;
92
+ }
@@ -0,0 +1,135 @@
1
+ import type { EndpointInfo, SecuritySchemeInfo } from "../../generator/types.ts";
2
+ import type { SeedBodyConfig } from "../../generator/resources-builder.ts";
3
+ import type { RecommendedAction } from "../../diagnostics/failure-hints.ts";
4
+
5
+ /**
6
+ * Mass-assignment local severity. Includes the unified severity ladder
7
+ * (critical/high/medium/low/info — see core/severity) plus two
8
+ * outcome-style states specific to this probe (inconclusive-baseline,
9
+ * inconclusive-5xx) and probe-lifecycle markers (ok, skipped).
10
+ *
11
+ * 'medium' is retained in the type for backwards compat but ARV-250
12
+ * stopped emitting it — single-signal proof on absent-fields now caps
13
+ * to 'low' per the m-21 severity matrix.
14
+ */
15
+ export type Severity =
16
+ | "high"
17
+ | "medium"
18
+ /** Baseline POST itself failed — we never reached extras-validation, so the
19
+ * 4xx-with-extras was a false signal. User must fix fixture / FK / scope
20
+ * before this endpoint can be probed (TASK-91). */
21
+ | "inconclusive-baseline"
22
+ /** Baseline POST returned ≥500 — the endpoint just crashes, mass-assignment
23
+ * semantics aren't observable here. Likely a duplicate of validation-probe's
24
+ * finding for the same endpoint (TASK-276). */
25
+ | "inconclusive-5xx"
26
+ | "low"
27
+ | "info"
28
+ | "ok"
29
+ | "skipped";
30
+
31
+ export interface FieldVerdict {
32
+ field: string;
33
+ injected: unknown;
34
+ /** "applied" | "ignored" | "echoed-but-overwritten" | "absent" | "unknown" */
35
+ outcome: "applied" | "ignored" | "echoed-overwritten" | "absent" | "unknown";
36
+ /** Value as seen in the response body (or follow-up GET if applicable). */
37
+ observed?: unknown;
38
+ }
39
+
40
+ export interface EndpointVerdict {
41
+ method: string;
42
+ path: string;
43
+ severity: Severity;
44
+ /** Canonical short reason (used in markdown header). */
45
+ summary: string;
46
+ request: {
47
+ url: string;
48
+ body: unknown;
49
+ injectedFields: string[];
50
+ };
51
+ response?: {
52
+ status: number;
53
+ body?: unknown;
54
+ };
55
+ followUpGet?: {
56
+ url: string;
57
+ status: number;
58
+ body?: unknown;
59
+ };
60
+ /** Result of the baseline (no-extras) probe — present whenever we sent it
61
+ * (always, except for skipped endpoints). Used to disambiguate
62
+ * «extras refused» from «baseline body invalid» (TASK-91). */
63
+ baseline?: {
64
+ status: number;
65
+ body?: unknown;
66
+ };
67
+ fields: FieldVerdict[];
68
+ /** True when request schema has additionalProperties:false (strict). */
69
+ strictContract: boolean;
70
+ cleanup?: {
71
+ attempted: boolean;
72
+ status?: number;
73
+ error?: string;
74
+ };
75
+ /** Reason this endpoint was skipped (only set when severity === "skipped"). */
76
+ skipReason?: string;
77
+ notes?: string[];
78
+ /** TASK-294: agent-routable action.
79
+ * high/medium → `report_backend_bug` (privilege escalation).
80
+ * inconclusive-baseline → `fix_fixture` (broken request body, retry).
81
+ * inconclusive-5xx → `report_backend_bug` (server crashed).
82
+ * low/ok/skipped → undefined. */
83
+ recommended_action?: RecommendedAction;
84
+ }
85
+
86
+ export interface MassAssignmentOptions {
87
+ endpoints: EndpointInfo[];
88
+ securitySchemes: SecuritySchemeInfo[];
89
+ /** Substituted variables (base_url, auth_token, api_key, path params). */
90
+ vars: Record<string, string>;
91
+ /** When true, do not issue cleanup-DELETE after 2xx responses. */
92
+ noCleanup?: boolean;
93
+ /** Per-request fetch timeout (ms). */
94
+ timeoutMs?: number;
95
+ /** When false, skip auto-discovery of path-param fixtures via GET-on-list (TASK-92).
96
+ * TASK-137: this flag now also controls body-FK discovery (required body
97
+ * fields named `*_id` / `*_slug` / `*_uuid` get filled from the matching
98
+ * collection list endpoint, eliminating most INCONCLUSIVE-baseline noise). */
99
+ discover?: boolean;
100
+ /** ARV-252: per-run extension to SUSPECTED_FIELDS (curated list of
101
+ * classic mass-assignment vectors). CLI surfaces this as repeatable
102
+ * `--suspect-field name=value`. Full per-api spec-extension support
103
+ * (x-zond-suspect-fields) is tracked in ARV-189. */
104
+ extraSuspectFields?: Record<string, unknown>;
105
+ /** ARV-269: agent-authored `seed_body` overlays from `.api-resources.local.yaml`,
106
+ * keyed by `"METHOD /path"` of the endpoint they apply to (typically the
107
+ * resource's create endpoint). When present for a probed endpoint, it
108
+ * replaces `generateFromSchema` as the baseline body source — same
109
+ * promotion stateful checks took via `resolveCreateBody`. Mass-assignment
110
+ * before ARV-269 ignored this overlay; on strict-validating APIs (Stripe)
111
+ * every baseline 400'd and the verdict collapsed to INCONCLUSIVE. */
112
+ seedBodies?: Map<string, SeedBodyConfig>;
113
+ }
114
+
115
+ export interface MassAssignmentResult {
116
+ specProbed: number;
117
+ totalEndpoints: number;
118
+ verdicts: EndpointVerdict[];
119
+ warnings: string[];
120
+ }
121
+
122
+ /** Internal: per-step options for probeEndpoint. */
123
+ export interface ProbeEndpointOpts {
124
+ noCleanup: boolean;
125
+ timeoutMs?: number;
126
+ bodyFkMisses?: Array<{ field: string; reason: string }>;
127
+ /** TASK-137: field→value pairs from body-FK discovery. Overlaid on baseline
128
+ * after generation so a real id/slug replaces the random sentinel. */
129
+ bodyFkOverlay?: Record<string, string>;
130
+ /** ARV-252: per-run extras for the suspect-fields list. */
131
+ extraSuspectFields?: Record<string, unknown>;
132
+ /** ARV-269: optional agent-authored body overlay for this endpoint
133
+ * (usually the resource's create endpoint). Wins over generator. */
134
+ seedBody?: SeedBodyConfig;
135
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * `MassAssignmentProbe` — Probe-contract wrapper around the existing
3
+ * `runMassAssignmentProbes` engine (m-17 / ARV-49).
4
+ *
5
+ * dryRun() lists POST/PATCH/PUT endpoints with the suspect fields the
6
+ * probe would inject (suspectedExtras + serverAssignedExtras). Skip
7
+ * reasons mirror the live runner (no-body, isolated-protected). This
8
+ * fills the F2-15 gap (mass-assignment had no --dry-run); ARV-52 wires
9
+ * the corresponding CLI flag.
10
+ *
11
+ * run() delegates to runMassAssignmentProbes; report() converts to
12
+ * either the legacy markdown digest or the structured per-endpoint
13
+ * shape (ARV-51).
14
+ */
15
+ import type { OpenAPIV3 } from "openapi-types";
16
+ import type { Probe, ProbeContext, ProbeFlags, EndpointPlan, ProbeResult, ProbeReportFormat, ProbeEndpointResult, ProbeEndpointStatus } from "./types.ts";
17
+ import {
18
+ runMassAssignmentProbes,
19
+ formatDigestMarkdown,
20
+ SUSPECTED_FIELDS,
21
+ type MassAssignmentResult,
22
+ type EndpointVerdict,
23
+ type Severity,
24
+ } from "./mass-assignment-probe.ts";
25
+ import { pathTouchesSeededVar } from "./shared.ts";
26
+ import { hasProbeBody } from "./probe-harness.ts";
27
+
28
+ const FLAGS: ProbeFlags = {
29
+ api: true,
30
+ tag: true,
31
+ include: true,
32
+ exclude: true,
33
+ dryRun: true,
34
+ listTags: true,
35
+ json: true,
36
+ output: true,
37
+ report: true,
38
+ };
39
+
40
+ function requestPropertyNames(schema?: OpenAPIV3.SchemaObject): Set<string> {
41
+ const out = new Set<string>();
42
+ if (!schema) return out;
43
+ if (schema.properties) for (const k of Object.keys(schema.properties)) out.add(k);
44
+ for (const composite of [schema.allOf, schema.oneOf, schema.anyOf]) {
45
+ if (Array.isArray(composite)) {
46
+ for (const sub of composite) {
47
+ const s = sub as OpenAPIV3.SchemaObject;
48
+ if (s.properties) for (const k of Object.keys(s.properties)) out.add(k);
49
+ }
50
+ }
51
+ }
52
+ return out;
53
+ }
54
+
55
+ function suspectFieldsFor(ep: { requestBodySchema?: OpenAPIV3.SchemaObject }): string[] {
56
+ const reqProps = requestPropertyNames(ep.requestBodySchema);
57
+ const out: string[] = [];
58
+ for (const name of Object.keys(SUSPECTED_FIELDS)) {
59
+ if (!reqProps.has(name)) out.push(name);
60
+ }
61
+ return out;
62
+ }
63
+
64
+ function statusFromSeverity(s: Severity): ProbeEndpointStatus {
65
+ switch (s) {
66
+ case "high": return "high";
67
+ case "low": return "low";
68
+ case "medium": return "low";
69
+ case "info": return "low";
70
+ case "ok": return "ok";
71
+ case "skipped": return "skipped";
72
+ case "inconclusive-baseline":
73
+ case "inconclusive-5xx":
74
+ return "inconclusive";
75
+ }
76
+ }
77
+
78
+ function evidenceFromVerdict(v: EndpointVerdict): Record<string, unknown> {
79
+ return {
80
+ summary: v.summary,
81
+ request: { url: v.request.url, injectedFields: v.request.injectedFields },
82
+ response: v.response ? { status: v.response.status } : undefined,
83
+ fields: v.fields,
84
+ };
85
+ }
86
+
87
+ function toProbeResult(ma: MassAssignmentResult): ProbeResult {
88
+ const endpoints: ProbeEndpointResult[] = ma.verdicts.map((v) => ({
89
+ path: v.path,
90
+ method: v.method,
91
+ classes_run: ["mass-assignment"],
92
+ findings:
93
+ v.severity === "skipped" || v.severity === "ok"
94
+ ? []
95
+ : [{
96
+ class: "mass-assignment",
97
+ severity:
98
+ v.severity === "high" ? "high" :
99
+ v.severity === "low" || v.severity === "medium" ? "low" :
100
+ "inconclusive",
101
+ evidence: evidenceFromVerdict(v),
102
+ }],
103
+ status: statusFromSeverity(v.severity),
104
+ ...(v.severity === "skipped" ? { skip_reason: v.skipReason ?? v.summary } : {}),
105
+ }));
106
+ const by_status: Record<ProbeEndpointStatus, number> = {
107
+ ok: 0, high: 0, low: 0, inconclusive: 0, skipped: 0,
108
+ };
109
+ for (const ep of endpoints) by_status[ep.status]++;
110
+ return {
111
+ endpoints,
112
+ summary: {
113
+ totalEndpoints: ma.totalEndpoints,
114
+ probed: ma.specProbed,
115
+ by_status,
116
+ },
117
+ warnings: ma.warnings,
118
+ };
119
+ }
120
+
121
+ export class MassAssignmentProbe implements Probe {
122
+ readonly name = "mass-assignment";
123
+ readonly description =
124
+ "Live probe for mass-assignment / privilege-escalation: classifies POST/PATCH/PUT against suspected extra fields (is_admin, role, account_id, …).";
125
+ readonly commonFlags = FLAGS;
126
+
127
+ async dryRun(ctx: ProbeContext): Promise<EndpointPlan[]> {
128
+ const isolated = ctx.options["isolated"] === true;
129
+ const out: EndpointPlan[] = [];
130
+ for (const ep of ctx.endpoints) {
131
+ if (ep.deprecated) continue;
132
+ const m = ep.method.toUpperCase();
133
+ if (m !== "POST" && m !== "PUT" && m !== "PATCH") continue;
134
+
135
+ if (isolated && (m === "PUT" || m === "PATCH") && pathTouchesSeededVar(ep.path, ctx.vars)) {
136
+ out.push({
137
+ path: ep.path,
138
+ method: m,
139
+ planned: false,
140
+ classes_planned: [],
141
+ fields_planned: [],
142
+ skip_reason: "isolated-protected",
143
+ });
144
+ continue;
145
+ }
146
+
147
+ // ARV-150: parity with the live runner — form-urlencoded counts as a body.
148
+ if (!hasProbeBody(ep)) {
149
+ out.push({
150
+ path: ep.path,
151
+ method: m,
152
+ planned: false,
153
+ classes_planned: [],
154
+ fields_planned: [],
155
+ skip_reason: "no-body",
156
+ });
157
+ continue;
158
+ }
159
+
160
+ const fields = suspectFieldsFor(ep);
161
+ out.push({
162
+ path: ep.path,
163
+ method: m,
164
+ planned: true,
165
+ classes_planned: ["mass-assignment"],
166
+ fields_planned: fields,
167
+ skip_reason: null,
168
+ });
169
+ }
170
+ return out;
171
+ }
172
+
173
+ async run(ctx: ProbeContext): Promise<ProbeResult> {
174
+ const ma = await runMassAssignmentProbes({
175
+ endpoints: ctx.endpoints,
176
+ securitySchemes: ctx.securitySchemes,
177
+ vars: ctx.vars,
178
+ noCleanup: ctx.options["noCleanup"] === true,
179
+ timeoutMs: typeof ctx.options["timeoutMs"] === "number" ? (ctx.options["timeoutMs"] as number) : undefined,
180
+ discover: ctx.options["noDiscover"] !== true,
181
+ });
182
+ const result = toProbeResult(ma);
183
+ result.extras = { raw: ma };
184
+ return result;
185
+ }
186
+
187
+ report(format: ProbeReportFormat, result: ProbeResult): string | object {
188
+ if (format === "markdown") {
189
+ const raw = result.extras?.["raw"] as MassAssignmentResult | undefined;
190
+ if (raw) return formatDigestMarkdown(raw, "");
191
+ return "(no markdown digest available)";
192
+ }
193
+ return {
194
+ endpoints: result.endpoints,
195
+ summary: result.summary,
196
+ };
197
+ }
198
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Barrel re-export for `zond probe-mass-assignment`.
3
+ *
4
+ * The probe pipeline lives in `./mass-assignment/`:
5
+ * - types.ts — public types / interfaces
6
+ * - suspects.ts — suspected-fields table + schema helpers
7
+ * - classify.ts — per-field classification + severity finalisation
8
+ * - cleanup.ts — best-effort DELETE on baseline POSTs
9
+ * - digest.ts — markdown digest formatter
10
+ * - regression.ts — regression-suite YAML emission
11
+ * - orchestrator.ts — runMassAssignmentProbes + probeEndpoint loop
12
+ *
13
+ * History: split out of a 1135-LOC monolith (ARV-296, 2026-05-18) to keep
14
+ * each concern in a focused file. The public API surface is unchanged.
15
+ */
16
+ export type {
17
+ Severity,
18
+ FieldVerdict,
19
+ EndpointVerdict,
20
+ MassAssignmentOptions,
21
+ MassAssignmentResult,
22
+ } from "./mass-assignment/types.ts";
23
+ export { SUSPECTED_FIELDS } from "./mass-assignment/suspects.ts";
24
+ export { runMassAssignmentProbes } from "./mass-assignment/orchestrator.ts";
25
+ export { isSubscriptionGated } from "./mass-assignment/classify.ts";
26
+ export { formatDigestMarkdown } from "./mass-assignment/digest.ts";
27
+ export { emitRegressionSuites } from "./mass-assignment/regression.ts";