@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,135 +0,0 @@
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
- }
@@ -1,198 +0,0 @@
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
- }
@@ -1,27 +0,0 @@
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";
@@ -1,240 +0,0 @@
1
- /**
2
- * TASK-146: emit-template generator.
3
- *
4
- * Builds a ready-to-edit YAML probe template for a single endpoint, so the
5
- * user doesn't have to copy-paste the boilerplate from the skill (Phase 5.1).
6
- * Used when the auto-prober marked an endpoint INCONCLUSIVE / INCONCLUSIVE-5XX
7
- * and the user wants to drop down to manual catch-up.
8
- *
9
- * Heuristics:
10
- * - Suspected fields: classic mass-assignment vectors (is_admin, role,
11
- * owner_id, account_id, ...).
12
- * - readOnly: true / x-zond-protected fields lifted from the request body
13
- * schema — these MUST NOT round-trip back from the server.
14
- * - For POST endpoints with discoverable item path (GET-by-id / DELETE
15
- * counterpart) we emit a full create → verify → cleanup chain.
16
- */
17
- import type { OpenAPIV3 } from "openapi-types";
18
- import type { EndpointInfo } from "../generator/types.ts";
19
- import type { RawSuite, RawStep } from "../generator/serializer.ts";
20
- import { serializeSuite } from "../generator/serializer.ts";
21
- import { readOpenApiSpec, extractEndpoints } from "../generator/openapi-reader.ts";
22
- import { findDeleteCounterpart, findGetByIdCounterpart, captureFieldFor } from "./shared.ts";
23
- import { SUSPECTED_FIELDS } from "./mass-assignment-probe.ts";
24
-
25
- export interface EmitTemplateOptions {
26
- specPath: string;
27
- method: string;
28
- path: string;
29
- }
30
-
31
- export type EmitTemplateResult =
32
- | { kind: "ok"; yaml: string; chain: "full" | "single"; protectedFields: string[] }
33
- | { kind: "endpoint-not-found"; method: string; path: string; nearest: string[] };
34
-
35
- export async function buildMassAssignmentTemplate(
36
- opts: EmitTemplateOptions,
37
- ): Promise<EmitTemplateResult> {
38
- const doc = await readOpenApiSpec(opts.specPath);
39
- const all = extractEndpoints(doc);
40
-
41
- const wantMethod = opts.method.toUpperCase();
42
- const target = all.find(
43
- e => e.method.toUpperCase() === wantMethod && pathsEqual(e.path, opts.path),
44
- );
45
-
46
- if (!target) {
47
- const nearest = all
48
- .filter(e => e.method.toUpperCase() === wantMethod)
49
- .map(e => e.path)
50
- .filter(p => similar(p, opts.path))
51
- .slice(0, 5);
52
- return { kind: "endpoint-not-found", method: wantMethod, path: opts.path, nearest };
53
- }
54
-
55
- const protectedFields = collectProtectedFields(target.requestBodySchema);
56
- const baselineBody = buildBaselineSkeleton(target.requestBodySchema);
57
- const privilegedBody = mergePrivileged(baselineBody, protectedFields);
58
-
59
- const isMutatingCreateLike = wantMethod === "POST";
60
- const readSibling = isMutatingCreateLike ? findGetByIdCounterpart(target, all) : undefined;
61
- const deleteSibling = findDeleteCounterpart(target, all);
62
-
63
- const suite = isMutatingCreateLike && readSibling
64
- ? buildFullChain(target, readSibling, deleteSibling, privilegedBody, protectedFields)
65
- : buildSingleStep(target, privilegedBody, protectedFields);
66
-
67
- return {
68
- kind: "ok",
69
- yaml: serializeSuite(suite),
70
- chain: isMutatingCreateLike && readSibling ? "full" : "single",
71
- protectedFields,
72
- };
73
- }
74
-
75
- function collectProtectedFields(schema?: OpenAPIV3.SchemaObject): string[] {
76
- if (!schema || !schema.properties) return [];
77
- const out: string[] = [];
78
- for (const [name, raw] of Object.entries(schema.properties)) {
79
- const prop = raw as OpenAPIV3.SchemaObject & { "x-zond-protected"?: boolean };
80
- if (prop.readOnly === true || prop["x-zond-protected"] === true) out.push(name);
81
- }
82
- return out;
83
- }
84
-
85
- function buildBaselineSkeleton(schema?: OpenAPIV3.SchemaObject): Record<string, unknown> {
86
- // Skeleton is intentionally minimal — `# …real create body sourced from
87
- // fixtures…` placeholder shows up in the YAML so the user fills it in.
88
- if (!schema || !schema.properties) return {};
89
- const out: Record<string, unknown> = {};
90
- for (const [name, raw] of Object.entries(schema.properties)) {
91
- const prop = raw as OpenAPIV3.SchemaObject;
92
- if (prop.readOnly === true) continue;
93
- if (schema.required?.includes(name)) {
94
- out[name] = placeholderForType(prop);
95
- }
96
- }
97
- return out;
98
- }
99
-
100
- function placeholderForType(prop: OpenAPIV3.SchemaObject): unknown {
101
- if (prop.example !== undefined) return prop.example;
102
- switch (prop.type) {
103
- case "integer":
104
- case "number": return 1;
105
- case "boolean": return false;
106
- case "array": return [];
107
- case "object": return {};
108
- default: return `ma-test-{{$randomString}}`;
109
- }
110
- }
111
-
112
- function mergePrivileged(
113
- baseline: Record<string, unknown>,
114
- protectedFields: string[],
115
- ): Record<string, unknown> {
116
- const merged: Record<string, unknown> = { ...baseline };
117
- // Suspected fields always added.
118
- for (const [k, v] of Object.entries(SUSPECTED_FIELDS)) merged[k] = v;
119
- // readOnly / x-zond-protected fields: inject distinctive sentinel values
120
- // so we can detect server-side echo vs server-side regeneration.
121
- for (const f of protectedFields) {
122
- if (!(f in merged)) merged[f] = `attacker-${f}-{{$uuid}}`;
123
- }
124
- return merged;
125
- }
126
-
127
- /** ARV-198: declared content type signals whether the create step needs
128
- * `form:` (Stripe v1, Rails/PHP-style APIs) or the default `json:` body. */
129
- function isFormEndpoint(ep: EndpointInfo): boolean {
130
- return ep.requestBodyContentType === "application/x-www-form-urlencoded";
131
- }
132
-
133
- function buildFullChain(
134
- create: EndpointInfo,
135
- read: EndpointInfo,
136
- del: EndpointInfo | undefined,
137
- privilegedBody: Record<string, unknown>,
138
- protectedFields: string[],
139
- ): RawSuite {
140
- const idVar = captureFieldFor(create) || "created_id";
141
- const tests: RawStep[] = [];
142
-
143
- // ARV-198: when the spec declares only application/x-www-form-urlencoded
144
- // for this mutating endpoint, emit a `form:` block + Content-Type header.
145
- // The serializer force-quotes form values (ARV-162) so booleans/numbers
146
- // round-trip as strings on the wire; the template is paste-ready against
147
- // Stripe-style APIs without any post-processing.
148
- const createIsForm = isFormEndpoint(create);
149
- const createStep: Record<string, unknown> = {
150
- name: "create with privileged fields",
151
- [create.method.toUpperCase()]: create.path,
152
- expect: { status: [200, 201], body: { id: { capture: idVar } } },
153
- };
154
- if (createIsForm) {
155
- createStep.headers = { "Content-Type": "application/x-www-form-urlencoded" };
156
- createStep.form = privilegedBody;
157
- } else {
158
- createStep.json = privilegedBody;
159
- }
160
- tests.push(createStep as unknown as RawStep);
161
-
162
- // Canonical assertion vocabulary: only `not_equals` is supported (no `not`,
163
- // no `not_starts_with`). For protected fields we assert the exact attacker
164
- // sentinel value did NOT round-trip back from the server.
165
- const verifyBody: Record<string, Record<string, unknown>> = {};
166
- for (const k of Object.keys(SUSPECTED_FIELDS)) {
167
- verifyBody[k] = { not_equals: SUSPECTED_FIELDS[k] };
168
- }
169
- for (const f of protectedFields) {
170
- if (!(f in verifyBody)) verifyBody[f] = { not_contains: "attacker-" };
171
- }
172
-
173
- tests.push({
174
- name: "verify privileged fields not echoed",
175
- [read.method.toUpperCase()]: read.path.replace(/\{[^}]+\}/, `{{${idVar}}}`),
176
- expect: { status: 200, body: verifyBody as unknown as Record<string, Record<string, string>> },
177
- } as unknown as RawStep);
178
-
179
- if (del) {
180
- tests.push({
181
- name: "cleanup",
182
- [del.method.toUpperCase()]: del.path.replace(/\{[^}]+\}/, `{{${idVar}}}`),
183
- always: true,
184
- expect: { status: [200, 202, 204] },
185
- } as unknown as RawStep);
186
- }
187
-
188
- return {
189
- name: `ma ${slugFromPath(create.path)}`,
190
- base_url: "{{base_url}}",
191
- headers: { Authorization: "Bearer {{auth_token}}" },
192
- tests,
193
- };
194
- }
195
-
196
- function buildSingleStep(
197
- ep: EndpointInfo,
198
- privilegedBody: Record<string, unknown>,
199
- _protectedFields: string[],
200
- ): RawSuite {
201
- const method = ep.method.toUpperCase();
202
- const tests: RawStep[] = [];
203
- const step: Record<string, unknown> = {
204
- name: `mass-assignment ${method} ${ep.path}`,
205
- [method]: ep.path,
206
- expect: { status: [400, 422] },
207
- };
208
- if (method !== "GET" && method !== "DELETE") {
209
- // ARV-198: same form/json branching as the full-chain create — see
210
- // buildFullChain for rationale (Stripe-style mutators declare only
211
- // application/x-www-form-urlencoded).
212
- if (isFormEndpoint(ep)) {
213
- step.headers = { "Content-Type": "application/x-www-form-urlencoded" };
214
- step.form = privilegedBody;
215
- } else {
216
- step.json = privilegedBody;
217
- }
218
- }
219
- tests.push(step as unknown as RawStep);
220
- return {
221
- name: `ma ${slugFromPath(ep.path)}`,
222
- base_url: "{{base_url}}",
223
- headers: { Authorization: "Bearer {{auth_token}}" },
224
- tests,
225
- };
226
- }
227
-
228
- function pathsEqual(a: string, b: string): boolean {
229
- return a.replace(/\/$/, "") === b.replace(/\/$/, "");
230
- }
231
-
232
- function similar(a: string, b: string): boolean {
233
- const aSeg = a.split("/").filter(Boolean);
234
- const bSeg = b.split("/").filter(Boolean);
235
- return aSeg.some(s => bSeg.includes(s));
236
- }
237
-
238
- function slugFromPath(p: string): string {
239
- return p.replace(/^\//, "").replace(/\/?\{[^}]+\}/g, "").replace(/\//g, "-") || "endpoint";
240
- }