@kirrosh/zond 0.22.0 → 0.23.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 (256) hide show
  1. package/CHANGELOG.md +648 -0
  2. package/README.md +58 -6
  3. package/package.json +9 -6
  4. package/src/cli/argv.ts +122 -0
  5. package/src/cli/commands/add-api.ts +134 -0
  6. package/src/cli/commands/api/annotate/idempotency.ts +59 -0
  7. package/src/cli/commands/api/annotate/index.ts +525 -0
  8. package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
  9. package/src/cli/commands/api/annotate/overlay.ts +206 -0
  10. package/src/cli/commands/api/annotate/pagination.ts +60 -0
  11. package/src/cli/commands/api/annotate/prompts.ts +183 -0
  12. package/src/cli/commands/api/annotate/readback.ts +58 -0
  13. package/src/cli/commands/api/annotate/resources.ts +91 -0
  14. package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
  15. package/src/cli/commands/audit.ts +480 -0
  16. package/src/cli/commands/bootstrap.ts +710 -0
  17. package/src/cli/commands/catalog.ts +35 -0
  18. package/src/cli/commands/check.ts +348 -0
  19. package/src/cli/commands/checks.ts +756 -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 +262 -0
  23. package/src/cli/commands/completions.ts +16 -0
  24. package/src/cli/commands/coverage.ts +605 -132
  25. package/src/cli/commands/db.ts +178 -7
  26. package/src/cli/commands/describe.ts +37 -2
  27. package/src/cli/commands/discover.ts +1236 -0
  28. package/src/cli/commands/doctor.ts +607 -0
  29. package/src/cli/commands/fixtures.ts +402 -0
  30. package/src/cli/commands/generate.ts +420 -46
  31. package/src/cli/commands/init/bootstrap.ts +30 -1
  32. package/src/cli/commands/{init.ts → init/index.ts} +99 -5
  33. package/src/cli/commands/init/skills.ts +56 -3
  34. package/src/cli/commands/init/templates/agents.md +65 -61
  35. package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
  36. package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
  37. package/src/cli/commands/init/templates/skills/zond.md +592 -125
  38. package/src/cli/commands/init/templates/zond-config.yml +8 -9
  39. package/src/cli/commands/prepare-fixtures.ts +135 -0
  40. package/src/cli/commands/probe/mass-assignment.ts +503 -0
  41. package/src/cli/commands/probe/security.ts +454 -0
  42. package/src/cli/commands/probe/static.ts +255 -0
  43. package/src/cli/commands/probe/webhooks.ts +161 -0
  44. package/src/cli/commands/probe.ts +459 -0
  45. package/src/cli/commands/reference.ts +87 -0
  46. package/src/cli/commands/refresh-api.ts +169 -0
  47. package/src/cli/commands/remove-api.ts +150 -0
  48. package/src/cli/commands/report-bundle.ts +318 -0
  49. package/src/cli/commands/report.ts +241 -0
  50. package/src/cli/commands/request.ts +379 -4
  51. package/src/cli/commands/run.ts +842 -53
  52. package/src/cli/commands/session.ts +244 -0
  53. package/src/cli/commands/use.ts +18 -1
  54. package/src/cli/index.ts +20 -3
  55. package/src/cli/json-envelope.ts +112 -3
  56. package/src/cli/json-schemas.ts +263 -0
  57. package/src/cli/program.ts +198 -635
  58. package/src/cli/resolve.ts +105 -0
  59. package/src/cli/status-filter.ts +124 -0
  60. package/src/cli/util/api-context.ts +85 -0
  61. package/src/cli/version.ts +5 -0
  62. package/src/core/anti-fp/bootstrap.ts +34 -0
  63. package/src/core/anti-fp/index.ts +33 -0
  64. package/src/core/anti-fp/registry.ts +44 -0
  65. package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
  66. package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
  67. package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
  68. package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
  69. package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
  70. package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
  71. package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
  72. package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
  73. package/src/core/anti-fp/types.ts +68 -0
  74. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  75. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  76. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  77. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  78. package/src/core/checks/checks/cross_call_references.ts +134 -0
  79. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  80. package/src/core/checks/checks/idempotency_replay.ts +246 -0
  81. package/src/core/checks/checks/ignored_auth.ts +211 -0
  82. package/src/core/checks/checks/index.ts +65 -0
  83. package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
  84. package/src/core/checks/checks/missing_required_header.ts +40 -0
  85. package/src/core/checks/checks/negative_data_rejection.ts +45 -0
  86. package/src/core/checks/checks/not_a_server_error.ts +27 -0
  87. package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
  88. package/src/core/checks/checks/pagination_invariants.ts +238 -0
  89. package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
  90. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  91. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  92. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  93. package/src/core/checks/checks/status_code_conformance.ts +61 -0
  94. package/src/core/checks/checks/unsupported_method.ts +63 -0
  95. package/src/core/checks/checks/use_after_free.ts +78 -0
  96. package/src/core/checks/index.ts +30 -0
  97. package/src/core/checks/mode.ts +79 -0
  98. package/src/core/checks/recommended-action.ts +64 -0
  99. package/src/core/checks/registry.ts +78 -0
  100. package/src/core/checks/runner.ts +874 -0
  101. package/src/core/checks/sarif.ts +230 -0
  102. package/src/core/checks/stateful.ts +121 -0
  103. package/src/core/checks/types.ts +189 -0
  104. package/src/core/classifier/recommended-action.ts +222 -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 +185 -0
  108. package/src/core/coverage/reasons.ts +300 -0
  109. package/src/core/diagnostics/db-analysis.ts +151 -11
  110. package/src/core/diagnostics/failure-class.ts +120 -0
  111. package/src/core/diagnostics/failure-hints.ts +212 -9
  112. package/src/core/diagnostics/spec-pointer.ts +99 -0
  113. package/src/core/diagnostics/suggested-fixes.ts +156 -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 +42 -16
  122. package/src/core/generator/coverage-phase.ts +0 -0
  123. package/src/core/generator/create-body.ts +89 -0
  124. package/src/core/generator/data-factory.ts +445 -19
  125. package/src/core/generator/describe.ts +1 -1
  126. package/src/core/generator/fixtures-builder.ts +325 -0
  127. package/src/core/generator/index.ts +7 -5
  128. package/src/core/generator/openapi-reader.ts +37 -3
  129. package/src/core/generator/path-param-disambig.ts +114 -0
  130. package/src/core/generator/resources-builder.ts +648 -0
  131. package/src/core/generator/schema-utils.ts +11 -3
  132. package/src/core/generator/serializer.ts +103 -13
  133. package/src/core/generator/suite-generator.ts +419 -111
  134. package/src/core/generator/types.ts +8 -0
  135. package/src/core/identity/identity-file.ts +129 -0
  136. package/src/core/lint/affects.ts +28 -0
  137. package/src/core/lint/config.ts +96 -0
  138. package/src/core/lint/format.ts +42 -0
  139. package/src/core/lint/index.ts +94 -0
  140. package/src/core/lint/reporter.ts +128 -0
  141. package/src/core/lint/rules/consistency.ts +158 -0
  142. package/src/core/lint/rules/heuristics.ts +97 -0
  143. package/src/core/lint/rules/strictness.ts +109 -0
  144. package/src/core/lint/types.ts +96 -0
  145. package/src/core/lint/walker.ts +248 -0
  146. package/src/core/meta/meta-store.ts +6 -73
  147. package/src/core/output/README.md +91 -0
  148. package/src/core/output/index.ts +13 -0
  149. package/src/core/output/run.ts +126 -0
  150. package/src/core/output/types.ts +129 -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 +57 -0
  159. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  160. package/src/core/probe/mass-assignment-probe.ts +1122 -0
  161. package/src/core/probe/mass-assignment-template.ts +212 -0
  162. package/src/core/probe/method-probe.ts +43 -76
  163. package/src/core/probe/method-shared.ts +69 -0
  164. package/src/core/probe/negative-probe.ts +183 -149
  165. package/src/core/probe/orphan-tracker.ts +188 -0
  166. package/src/core/probe/path-discovery.ts +440 -0
  167. package/src/core/probe/probe-harness.ts +120 -0
  168. package/src/core/probe/registry.ts +89 -0
  169. package/src/core/probe/runner.ts +136 -0
  170. package/src/core/probe/security-probe-class.ts +201 -0
  171. package/src/core/probe/security-probe.ts +1453 -0
  172. package/src/core/probe/shared.ts +505 -0
  173. package/src/core/probe/static-probe-class.ts +125 -0
  174. package/src/core/probe/types.ts +165 -0
  175. package/src/core/probe/verdict-aggregator.ts +33 -0
  176. package/src/core/probe/webhooks-probe.ts +284 -0
  177. package/src/core/reporter/console.ts +41 -2
  178. package/src/core/reporter/index.ts +2 -3
  179. package/src/core/reporter/json.ts +11 -1
  180. package/src/core/reporter/junit.ts +27 -12
  181. package/src/core/reporter/ndjson.ts +37 -0
  182. package/src/core/reporter/types.ts +3 -0
  183. package/src/core/runner/assertions.ts +58 -1
  184. package/src/core/runner/async-pool.ts +108 -0
  185. package/src/core/runner/auth-path.ts +8 -0
  186. package/src/core/runner/ci-context.ts +72 -0
  187. package/src/core/runner/executor.ts +264 -20
  188. package/src/core/runner/form-encode.ts +51 -0
  189. package/src/core/runner/http-client.ts +75 -2
  190. package/src/core/runner/learn-drift.ts +293 -0
  191. package/src/core/runner/preflight-vars.ts +149 -0
  192. package/src/core/runner/progress-tracker.ts +73 -0
  193. package/src/core/runner/rate-limiter.ts +89 -17
  194. package/src/core/runner/run-kind.ts +39 -0
  195. package/src/core/runner/schema-validator.ts +312 -0
  196. package/src/core/runner/send-request.ts +153 -20
  197. package/src/core/runner/types.ts +38 -0
  198. package/src/core/secrets/registry.ts +164 -0
  199. package/src/core/secrets/secrets-file.ts +115 -0
  200. package/src/core/selectors/operation-filter.ts +144 -0
  201. package/src/core/setup-api.ts +415 -16
  202. package/src/core/severity/category.ts +94 -0
  203. package/src/core/severity/index.ts +121 -0
  204. package/src/core/spec/layers.ts +154 -0
  205. package/src/core/util/format-eta.ts +21 -0
  206. package/src/core/utils.ts +5 -1
  207. package/src/core/workspace/config.ts +129 -0
  208. package/src/core/workspace/manifest.ts +283 -0
  209. package/src/core/workspace/output-rotation.ts +62 -0
  210. package/src/core/workspace/triage-path.ts +87 -0
  211. package/src/db/lint-runs.ts +47 -0
  212. package/src/db/migrate.ts +126 -0
  213. package/src/db/migrations/0001_run_kind.sql +25 -0
  214. package/src/db/migrations/sql.d.ts +4 -0
  215. package/src/db/queries/collections.ts +133 -0
  216. package/src/db/queries/coverage.ts +9 -0
  217. package/src/db/queries/dashboard.ts +59 -0
  218. package/src/db/queries/results.ts +128 -0
  219. package/src/db/queries/runs.ts +235 -0
  220. package/src/db/queries/sessions.ts +42 -0
  221. package/src/db/queries/settings.ts +28 -0
  222. package/src/db/queries/types.ts +172 -0
  223. package/src/db/queries.ts +72 -802
  224. package/src/db/schema.ts +178 -50
  225. package/src/cli/commands/export.ts +0 -144
  226. package/src/cli/commands/guide.ts +0 -127
  227. package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
  228. package/src/cli/commands/probe-methods.ts +0 -108
  229. package/src/cli/commands/probe-validation.ts +0 -124
  230. package/src/cli/commands/serve.ts +0 -114
  231. package/src/cli/commands/sync.ts +0 -268
  232. package/src/cli/commands/update.ts +0 -189
  233. package/src/cli/commands/validate.ts +0 -34
  234. package/src/core/diagnostics/render-md.ts +0 -112
  235. package/src/core/exporter/postman.ts +0 -963
  236. package/src/core/generator/guide-builder.ts +0 -253
  237. package/src/core/meta/types.ts +0 -19
  238. package/src/core/parser/index.ts +0 -21
  239. package/src/core/runner/execute-run.ts +0 -132
  240. package/src/core/runner/index.ts +0 -12
  241. package/src/core/sync/spec-differ.ts +0 -38
  242. package/src/web/data/collection-state.ts +0 -362
  243. package/src/web/routes/api.ts +0 -314
  244. package/src/web/routes/dashboard.ts +0 -350
  245. package/src/web/routes/runs.ts +0 -64
  246. package/src/web/schemas.ts +0 -121
  247. package/src/web/server.ts +0 -134
  248. package/src/web/static/htmx.min.cjs +0 -1
  249. package/src/web/static/style.css +0 -1148
  250. package/src/web/views/endpoints-tab.ts +0 -174
  251. package/src/web/views/explorer-tab.ts +0 -402
  252. package/src/web/views/health-strip.ts +0 -92
  253. package/src/web/views/layout.ts +0 -48
  254. package/src/web/views/results.ts +0 -210
  255. package/src/web/views/runs-tab.ts +0 -126
  256. package/src/web/views/suites-tab.ts +0 -181
@@ -0,0 +1,459 @@
1
+ /**
2
+ * Probe umbrella.
3
+ *
4
+ * TASK-182 (m-11) introduced `zond probe <class>` as the canonical entry
5
+ * point. TASK-300 (m-13) consolidated the two static-input classes —
6
+ * validation and methods — under `zond probe static [--include …]`; the
7
+ * old `probe validation` / `probe methods` subcommands were removed
8
+ * outright (no deprecation alias).
9
+ *
10
+ * Extracted from program.ts (TASK-190 round 2e) so the registration tree
11
+ * lives next to the action functions it dispatches into.
12
+ */
13
+
14
+ import type { Command } from "commander";
15
+
16
+ // ARV-129: action handlers relocated from top-level commands/probe-*.ts
17
+ // into commands/probe/ — the orchestrator (this file) stays at top level,
18
+ // the per-subcommand modules are no longer mistaken for siblings.
19
+ import { probeStaticCommand, resolveStaticClasses } from "./probe/static.ts";
20
+ import { probeMassAssignmentCommand, emitMassAssignmentTemplateCommand } from "./probe/mass-assignment.ts";
21
+ import { probeSecurityCommand } from "./probe/security.ts";
22
+ import { probeWebhooksCommand } from "./probe/webhooks.ts";
23
+ import { SECURITY_CLASSES } from "../../core/probe/security-probe.ts";
24
+ import { globalJson, resolveSpecArg, resolveApiEnv, resolveApiCollection } from "../resolve.ts";
25
+ import { getApi } from "../util/api-context.ts";
26
+ import { existsSync } from "fs";
27
+ import { join, dirname } from "node:path";
28
+ import { parsePositiveInt } from "../argv.ts";
29
+ import { printError } from "../output.ts";
30
+ import { jsonError, printJson } from "../json-envelope.ts";
31
+ import { loadEnvMeta } from "../../core/parser/variables.ts";
32
+ import { resolveTimeoutMs } from "../../core/workspace/config.ts";
33
+ import { resolveOutput, OutputSpecError, type OutputSpec, type ResolvedOutput } from "../../core/output/index.ts";
34
+
35
+ /**
36
+ * ARV-119 (m-19): typed declaration of the `--report` / `--output`
37
+ * surface shared by the live-probe subcommands (mass-assignment +
38
+ * security). Both render a markdown digest by default, with `--report
39
+ * json` switching to a structured JSON file body. `--output <path>`
40
+ * routes the rendered body to a file; without it the body lands on
41
+ * stdout (when `--json` is not set — see m-17 / ARV-51: the `--json`
42
+ * envelope is a separate channel that wraps the structured result).
43
+ *
44
+ * `probe static` is *not* on this spec — its `--output` is a directory
45
+ * where YAML probe suites are written, semantics not output-format.
46
+ */
47
+ export const PROBE_OUTPUT_SPEC: OutputSpec<unknown> = {
48
+ command: "probe",
49
+ defaultFormat: "markdown",
50
+ formats: {
51
+ markdown: { defaultChannel: "stdout", description: "Human-readable digest (default)" },
52
+ json: { defaultChannel: "stdout", description: "Structured JSON body — same shape as the --json envelope's data." },
53
+ },
54
+ };
55
+
56
+ /**
57
+ * ARV-119: shared `--report` / `--output` / `--overwrite` option set
58
+ * for the two live-probe subcommands. Resolution goes through
59
+ * `resolveProbeOutputFlags` so unknown formats and mutually-exclusive
60
+ * combinations surface the same error for both subcommands instead of
61
+ * each one reimplementing the validation.
62
+ */
63
+ function addProbeReportOutputOptions(cmd: Command): Command {
64
+ return cmd
65
+ .option("--output <file>", "ARV-119: write the rendered report to this file (default: stdout). Pairs with --report to pick the format.")
66
+ .option(
67
+ "--report <format>",
68
+ "ARV-119: format for --output / non-json stdout (markdown|json). Default markdown. The --json envelope (m-17 ARV-51) is a separate channel and is always structured.",
69
+ "markdown",
70
+ )
71
+ .option("--overwrite", "Overwrite existing --output file in place (default: rotate to <stem>-vN.<ext>)");
72
+ }
73
+
74
+ interface ProbeReportOutputOpts {
75
+ report?: string;
76
+ output?: string;
77
+ }
78
+
79
+ /**
80
+ * ARV-119: resolve --report / --output through PROBE_OUTPUT_SPEC. On
81
+ * unknown format / mutual-exclusion violation, prints the consistent
82
+ * error (envelope when --json) and returns null — caller exits 2.
83
+ */
84
+ function resolveProbeOutputFlags(
85
+ command: string,
86
+ opts: ProbeReportOutputOpts,
87
+ json: boolean,
88
+ ): { resolved: ResolvedOutput; report: "markdown" | "json"; output?: string } | null {
89
+ let resolved: ResolvedOutput;
90
+ try {
91
+ resolved = resolveOutput(PROBE_OUTPUT_SPEC, { report: opts.report, output: opts.output });
92
+ } catch (err) {
93
+ if (err instanceof OutputSpecError) {
94
+ if (json) printJson(jsonError(command, [err.message]));
95
+ else printError(err.message);
96
+ return null;
97
+ }
98
+ throw err;
99
+ }
100
+ const report: "markdown" | "json" = resolved.format === "json" ? "json" : "markdown";
101
+ const output = resolved.channel === "file" ? resolved.path : undefined;
102
+ return { resolved, report, output };
103
+ }
104
+
105
+ /**
106
+ * ARV-53: thin wrapper kept for the existing call-sites — the real chain
107
+ * (local → ancestor → ZOND_API_GLOBAL/ZOND_API/.zond/current-api) lives in
108
+ * cli/util/api-context.ts. `resolveProbeApi` predates that helper (ARV-33).
109
+ */
110
+ export function resolveProbeApi(
111
+ optsApi: string | undefined,
112
+ cmd: { opts?: () => Record<string, unknown>; parent?: { opts(): Record<string, unknown> } | null } | undefined,
113
+ ): string | undefined {
114
+ // Adapt the loose mock shape used at the old call-sites to CommandLike.
115
+ if (cmd === undefined) {
116
+ return getApi(undefined, { api: optsApi });
117
+ }
118
+ const adapted = {
119
+ opts: () => (typeof cmd.opts === "function" ? cmd.opts() : {}),
120
+ parent: cmd.parent ?? null,
121
+ };
122
+ return getApi(adapted, { api: optsApi });
123
+ }
124
+
125
+ /**
126
+ * TASK-233: pick the env file to feed live-probe commands.
127
+ * - Explicit --env wins (legacy behaviour).
128
+ * - Otherwise --api <name> derives `apis/<name>/.env.yaml` via the registered
129
+ * collection's base_dir.
130
+ * - With `tolerateMissing` (probe security/--dry-run), an absent file is
131
+ * quietly turned into "no env" — the command will fall back to cwd.
132
+ */
133
+ function resolveProbeEnv(
134
+ envFlag: string | undefined,
135
+ apiFlag: string | undefined,
136
+ dbPath: string | undefined,
137
+ opts: { tolerateMissing?: boolean } = {},
138
+ ): { env: string | undefined } | { error: string } {
139
+ if (envFlag) return { env: envFlag };
140
+ if (!apiFlag) {
141
+ if (opts.tolerateMissing) return { env: undefined };
142
+ return { error: "Missing --env <file> (or pass --api <name> to derive it from apis/<name>/.env.yaml)" };
143
+ }
144
+ const resolved = resolveApiEnv(apiFlag, dbPath);
145
+ if ("error" in resolved) return resolved;
146
+ if (!existsSync(resolved.env)) {
147
+ if (opts.tolerateMissing) return { env: undefined };
148
+ return { error: `Env file not found: ${resolved.env} (derived from --api ${apiFlag})` };
149
+ }
150
+ return { env: resolved.env };
151
+ }
152
+
153
+ /**
154
+ * Resolve `--timeout` for live-probe commands. Reads the per-API
155
+ * `.env.yaml` `timeoutMs:` meta when `--api` is set (or when the env
156
+ * file is on disk), then falls back to workspace `defaults.timeout_ms`.
157
+ */
158
+ async function resolveProbeTimeout(
159
+ cliFlag: number | undefined,
160
+ apiFlag: string | undefined,
161
+ envFile: string | undefined,
162
+ ): Promise<number> {
163
+ let envTimeout: number | undefined;
164
+ try {
165
+ if (apiFlag) {
166
+ const meta = await loadEnvMeta(undefined, `apis/${apiFlag}`);
167
+ envTimeout = meta.timeoutMs;
168
+ } else if (envFile) {
169
+ const meta = await loadEnvMeta(undefined, dirname(envFile));
170
+ envTimeout = meta.timeoutMs;
171
+ }
172
+ } catch { /* meta is best-effort */ }
173
+ return resolveTimeoutMs(cliFlag, envTimeout);
174
+ }
175
+
176
+ function defineProbeStatic(parent: Command, name: string): void {
177
+ parent
178
+ .command(`${name} [spec]`)
179
+ .description(
180
+ "Generate static-input probe suites (validation: bogus types/values; methods: undeclared HTTP methods). Defaults to both; restrict via --include or --exclude.",
181
+ )
182
+ .option("--api <name>", "Use the registered API's spec (apis/<name>/spec.json)")
183
+ .option("--db <path>", "Path to SQLite database file")
184
+ // ARV-30: --output is optional when --api (or current-api) is set —
185
+ // probes land in apis/<name>/probes/static/ alongside generate's tests/.
186
+ // Required only when probing a bare spec with no registered collection.
187
+ .option("--output <dir>", "Output directory for generated probe files (default: apis/<api>/probes/static when --api / current-api is set)")
188
+ .option("--tag <tag>", "Probe only endpoints with this tag")
189
+ .option("--list-tags", "List available tags from spec and exit")
190
+ .option("--max-per-endpoint <N>", "Cap negative-input probes per endpoint (default 50)", parsePositiveInt("--max-per-endpoint"))
191
+ .option("--no-cleanup", "Skip emission of follow-up DELETE cleanup steps for mutating probes (use in namespace-isolated test envs)")
192
+ .option("--use-synthetic-parents", "Bake synthetic-by-type values into all path params (legacy). By default, non-attacked path params are emitted as {{name}} and resolved from .env.yaml at run time — needed to reach the leaf validator on nested paths (TASK-135).")
193
+ .option("--include <classes>", "Comma-separated subset of {validation, methods} (default: both)")
194
+ .option("--exclude <classes>", "Comma-separated subset to skip (mutually exclusive with --include)")
195
+ .action(async (specPos: string | undefined, optsArg, cmdRef: Command) => {
196
+ // ARV-33: see resolveProbeApi — keep the chain consistent with the
197
+ // sibling subcommands (mass-assignment, security).
198
+ const apiName = resolveProbeApi(optsArg.api, cmdRef);
199
+ const resolved = resolveSpecArg(specPos, apiName, optsArg.db);
200
+ if ("error" in resolved) { printError(resolved.error); process.exitCode = 2; return; }
201
+
202
+ const r = resolveStaticClasses(optsArg.include, optsArg.exclude);
203
+ if ("error" in r) { printError(r.error); process.exitCode = 2; return; }
204
+
205
+ // ARV-30: derive --output from the registered API's base_dir when the
206
+ // user didn't pass one. Bare-spec invocations (positional only, no --api,
207
+ // no current-api) still must pass --output explicitly.
208
+ let outputDir: string | undefined = optsArg.output;
209
+ if (!outputDir && apiName) {
210
+ const col = resolveApiCollection(apiName, optsArg.db);
211
+ if (!("error" in col) && col.baseDir) outputDir = join(col.baseDir, "probes", "static");
212
+ }
213
+ if (!outputDir) {
214
+ printError("--output <dir> is required when no --api / current-api can resolve apis/<name>/probes/static.");
215
+ process.exitCode = 2;
216
+ return;
217
+ }
218
+
219
+ const useReal = optsArg.useSyntheticParents !== true;
220
+ process.exitCode = await probeStaticCommand({
221
+ specPath: resolved.spec,
222
+ output: outputDir,
223
+ tag: optsArg.tag,
224
+ maxPerEndpoint: optsArg.maxPerEndpoint,
225
+ noCleanup: optsArg.cleanup === false,
226
+ useRealParents: useReal,
227
+ json: globalJson(cmdRef),
228
+ listTags: optsArg.listTags,
229
+ include: r.classes,
230
+ });
231
+ });
232
+ }
233
+
234
+ function defineProbeMassAssignment(parent: Command, name: string): void {
235
+ const sub = parent
236
+ .command(`${name} [spec]`)
237
+ .description(
238
+ "Live probe for mass-assignment / privilege-escalation: classifies POST/PATCH/PUT against suspected extra fields (is_admin, role, account_id, owner_id, user_id, verified, is_system) as rejected (4xx) | accepted-and-applied (HIGH) | accepted-and-ignored (LOW) via follow-up GET",
239
+ )
240
+ .option("--api <name>", "Use the registered API's spec (apis/<name>/spec.json)")
241
+ .option("--db <path>", "Path to SQLite database file")
242
+ .option("--env <file>", "Env YAML with base_url + auth_token (live calls require this; auto-derived from apis/<name>/.env.yaml when --api is given)")
243
+ .option("--emit-tests <dir>", "Also emit YAML regression suites locking in safe behaviour for CI")
244
+ .option("--tag <tag>", "Probe only endpoints with this tag")
245
+ .option("--list-tags", "List available tags from spec and exit")
246
+ .option("--no-cleanup", "Skip follow-up DELETE for resources accidentally created by 2xx probes")
247
+ .option("--no-discover", "Disable auto-discovery of path-param fixtures via GET-on-list (TASK-92)")
248
+ .option("--dry-run", "Print which endpoints/fields would be attacked without sending requests (m-17 ARV-52)")
249
+ .option(
250
+ "--include <selector>",
251
+ "Filter operations (m-15 ARV-9 grammar: path:/users/.* | tag:Webhooks | method:POST,PATCH | operation-id:create.*). Repeatable.",
252
+ (v: string, prev: string[] = []) => prev.concat(v),
253
+ [] as string[],
254
+ )
255
+ .option(
256
+ "--exclude <selector>",
257
+ "Drop operations matching <selector>. Repeatable. Same grammar as --include.",
258
+ (v: string, prev: string[] = []) => prev.concat(v),
259
+ [] as string[],
260
+ )
261
+ .option("--timeout <ms>", "Per-request timeout in ms (overrides apis/<name>/.env.yaml `timeoutMs` and zond.config.yml `defaults.timeout_ms`; default 30000)", parsePositiveInt("--timeout"))
262
+ .option(
263
+ "--verbose",
264
+ "ARV-252: surface INFO-severity inconclusive verdicts (absent-but-unverifiable). Silently-ignored verdicts (correct framework behaviour) stay hidden even with this flag — they're never finding-worthy.",
265
+ )
266
+ .option(
267
+ "--suspect-field <name=value>",
268
+ "ARV-252: extend the curated suspect-fields list (is_admin, role, owner_id, …) with a custom field. Repeatable. Full per-api spec-extension support tracked in ARV-189.",
269
+ (v: string, prev: string[] = []) => prev.concat(v),
270
+ [] as string[],
271
+ )
272
+ .option("--emit-template <method:path>", "TASK-146: emit a ready-to-edit YAML probe template for one endpoint (e.g. \"POST:/users\") instead of running the live probe. Pairs `--output <file>` to write to disk (default: stdout). Use to drop down to manual catch-up after INCONCLUSIVE / INCONCLUSIVE-5XX verdicts without copy-pasting boilerplate from the skill.");
273
+ addProbeReportOutputOptions(sub);
274
+ sub.action(async (specPos: string | undefined, opts, cmd: Command) => {
275
+ // ARV-33: resolve --api via the same fallback chain as prepare-fixtures /
276
+ // ARV-29 — direct opts, then parent opts, then ZOND_API_GLOBAL /
277
+ // .zond/current-api. Otherwise `zond probe mass-assignment --api foo`
278
+ // hits commander's global-option absorption and `opts.api` is empty.
279
+ const apiName = resolveProbeApi(opts.api, cmd);
280
+ const resolved = resolveSpecArg(specPos, apiName, opts.db);
281
+ if ("error" in resolved) { printError(resolved.error); process.exitCode = 2; return; }
282
+
283
+ // --emit-template short-circuits the live probe.
284
+ if (opts.emitTemplate) {
285
+ process.exitCode = await emitMassAssignmentTemplateCommand({
286
+ specPath: resolved.spec,
287
+ methodPath: opts.emitTemplate,
288
+ output: opts.output,
289
+ json: globalJson(cmd),
290
+ });
291
+ return;
292
+ }
293
+
294
+ // m-17 / ARV-52 + ARV-58: dry-run and list-tags paths tolerate a
295
+ // missing env file the way probe-security does — the user wants
296
+ // to inspect the plan / available tags, not hit a live API.
297
+ const envFile = resolveProbeEnv(opts.env, apiName, opts.db, {
298
+ tolerateMissing: opts.dryRun === true || opts.listTags === true,
299
+ });
300
+ if ("error" in envFile) { printError(envFile.error); process.exitCode = 2; return; }
301
+ const timeoutMs = await resolveProbeTimeout(opts.timeout, apiName, envFile.env);
302
+ const json = globalJson(cmd);
303
+ const rep = resolveProbeOutputFlags("probe-mass-assignment", opts, json);
304
+ if (!rep) { process.exitCode = 2; return; }
305
+ process.exitCode = await probeMassAssignmentCommand({
306
+ specPath: resolved.spec,
307
+ env: envFile.env,
308
+ output: rep.output,
309
+ emitTests: opts.emitTests,
310
+ tag: opts.tag,
311
+ listTags: opts.listTags,
312
+ noCleanup: opts.cleanup === false,
313
+ noDiscover: opts.discover === false,
314
+ timeoutMs,
315
+ overwrite: opts.overwrite === true,
316
+ json,
317
+ dryRun: opts.dryRun === true,
318
+ include: Array.isArray(opts.include) && opts.include.length > 0 ? opts.include : undefined,
319
+ exclude: Array.isArray(opts.exclude) && opts.exclude.length > 0 ? opts.exclude : undefined,
320
+ report: rep.report,
321
+ verbose: opts.verbose === true,
322
+ suspectField: Array.isArray(opts.suspectField) && opts.suspectField.length > 0 ? opts.suspectField : undefined,
323
+ });
324
+ });
325
+ }
326
+
327
+ function defineProbeSecurity(parent: Command, name: string): void {
328
+ const sub = parent
329
+ // ARV-36: classes is technically required but kept optional in commander
330
+ // so the missing-arg branch can produce the same actionable list of
331
+ // available classes that --unknown-class already prints (data already in
332
+ // SECURITY_CLASSES — no reason to force a --help read for first-time users).
333
+ .command(`${name} [classes] [spec]`)
334
+ .description(
335
+ "Live security probes (TASK-138): SSRF / CRLF / open-redirect. Detects vulnerable fields by name+format, sends a baseline-OK then per-field payloads, classifies HIGH (5xx or echo) / LOW (2xx no echo) / OK (4xx). <classes> is a comma-separated subset of: ssrf, crlf, open-redirect.",
336
+ )
337
+ .option("--api <name>", "Use the registered API's spec (apis/<name>/spec.json)")
338
+ .option("--db <path>", "Path to SQLite database file")
339
+ .option("--env <file>", "Env YAML with base_url + auth_token (live calls require this; --dry-run can run without; auto-derived from apis/<name>/.env.yaml when --api is given)")
340
+ .option("--emit-tests <dir>", "Also emit YAML regression suites locking in safe behaviour for CI")
341
+ .option("--tag <tag>", "Probe only endpoints with this tag")
342
+ .option("--list-tags", "List available tags from spec and exit")
343
+ .option("--no-cleanup", "Skip follow-up DELETE on resources created by baseline / 2xx attacks")
344
+ .option("--isolated", "TASK-264: refuse to attack PUT/PATCH endpoints whose path-params come from .env.yaml — protects seeded fixtures from probe-induced mutation. Lower coverage in exchange for guaranteed fixture safety.")
345
+ .option("--allow-leaks", "ARV-140: attack POST endpoints even when the spec has no DELETE counterpart. Default: skip — without DELETE there is no cleanup path and resources accumulate in the target tenant (round-01/02 Sentry left 18 manual orphans). Use when you've vetted manual cleanup or are in a throwaway env.")
346
+ .option(
347
+ "--include <selector>",
348
+ "Filter operations (m-15 ARV-9 grammar: path:/users/.* | tag:Webhooks | method:POST,PATCH). Repeatable.",
349
+ (v: string, prev: string[] = []) => prev.concat(v),
350
+ [] as string[],
351
+ )
352
+ .option(
353
+ "--exclude <selector>",
354
+ "Drop operations matching <selector>. Repeatable.",
355
+ (v: string, prev: string[] = []) => prev.concat(v),
356
+ [] as string[],
357
+ )
358
+ .option("--dry-run", "Print which endpoints/fields would be attacked without sending requests")
359
+ .option("--timeout <ms>", "Per-request timeout in ms (overrides apis/<name>/.env.yaml `timeoutMs` and zond.config.yml `defaults.timeout_ms`; default 30000)", parsePositiveInt("--timeout"))
360
+ .option(
361
+ "--verbose",
362
+ "ARV-253: surface INFO-severity findings (sanitization-signal-only, e.g. CRLF accepted but no reflection observed). Default hides them — they're single-signal proof with no exploit pathway.",
363
+ );
364
+ addProbeReportOutputOptions(sub);
365
+ sub.action(async (classes: string | undefined, specPos: string | undefined, opts, cmd: Command) => {
366
+ // ARV-36: missing-arg path should list the available classes (parity
367
+ // with the unknown-class error). Commander's default `missing required
368
+ // argument` doesn't include them; once we made <classes> optional, we
369
+ // surface the same hint here.
370
+ if (typeof classes !== "string" || classes.length === 0) {
371
+ printError(`Missing required argument <classes>. Available: ${SECURITY_CLASSES.join(", ")}`);
372
+ process.exitCode = 2;
373
+ return;
374
+ }
375
+ // ARV-33: same fallback chain as mass-assignment so `zond probe security
376
+ // ssrf --api foo` doesn't fall through to a confusing "base_url is
377
+ // required" when commander absorbs --api at the global scope.
378
+ const apiName = resolveProbeApi(opts.api, cmd);
379
+ const resolved = resolveSpecArg(specPos, apiName, opts.db);
380
+ if ("error" in resolved) { printError(resolved.error); process.exitCode = 2; return; }
381
+ // probe-security tolerates a missing env (--dry-run path), so don't
382
+ // fail when --api is given but the env file isn't on disk yet.
383
+ const envFile = resolveProbeEnv(opts.env, apiName, opts.db, { tolerateMissing: true });
384
+ if ("error" in envFile) { printError(envFile.error); process.exitCode = 2; return; }
385
+ const timeoutMs = await resolveProbeTimeout(opts.timeout, apiName, envFile.env);
386
+ const json = globalJson(cmd);
387
+ const rep = resolveProbeOutputFlags("probe-security", opts, json);
388
+ if (!rep) { process.exitCode = 2; return; }
389
+ process.exitCode = await probeSecurityCommand({
390
+ specPath: resolved.spec,
391
+ classes,
392
+ env: envFile.env,
393
+ output: rep.output,
394
+ emitTests: opts.emitTests,
395
+ tag: opts.tag,
396
+ listTags: opts.listTags,
397
+ noCleanup: opts.cleanup === false,
398
+ dryRun: opts.dryRun === true,
399
+ timeoutMs,
400
+ overwrite: opts.overwrite === true,
401
+ json,
402
+ apiName,
403
+ isolated: opts.isolated === true,
404
+ allowLeaks: opts.allowLeaks === true,
405
+ report: rep.report,
406
+ include: Array.isArray(opts.include) && opts.include.length > 0 ? opts.include : undefined,
407
+ exclude: Array.isArray(opts.exclude) && opts.exclude.length > 0 ? opts.exclude : undefined,
408
+ verbose: opts.verbose === true,
409
+ });
410
+ });
411
+ }
412
+
413
+ /**
414
+ * ARV-173 (m-20): `zond probe webhooks` — offline shape-conformance for
415
+ * webhook events captured by `docs/recipes/webhook-receiver.md`.
416
+ *
417
+ * Live HTTP infrastructure (tunnels, listeners) lives in the recipe,
418
+ * not in core zond. The CLI takes a pre-captured ndjson log + the
419
+ * API's spec and validates each event's payload against
420
+ * `spec.webhooks.<event>.post.requestBody`. Same recipe/probe split as
421
+ * m-18's quicktype and interactsh.
422
+ */
423
+ function defineProbeWebhooks(parent: Command, name: string): void {
424
+ const sub = parent
425
+ .command(`${name} [spec]`)
426
+ .description("Shape-conform captured webhook events (ndjson) against spec.webhooks. Recipe: docs/recipes/webhook-receiver.md")
427
+ .option("--api <name>", "Use the registered API's spec (apis/<name>/spec.json)")
428
+ .option("--db <path>", "Path to SQLite database file")
429
+ .requiredOption("--event-log <file>", "ndjson event log captured by the recipe (one JSON event per line)")
430
+ .option("--only <types>", "Comma-separated event types to validate (default: all declared)");
431
+ addProbeReportOutputOptions(sub);
432
+ sub.action(async (specPos: string | undefined, opts, cmd: Command) => {
433
+ const apiName = resolveProbeApi(opts.api, cmd);
434
+ const resolved = resolveSpecArg(specPos, apiName, opts.db);
435
+ if ("error" in resolved) { printError(resolved.error); process.exitCode = 2; return; }
436
+ const json = globalJson(cmd);
437
+ const rep = resolveProbeOutputFlags("probe-webhooks", opts, json);
438
+ if (!rep) { process.exitCode = 2; return; }
439
+ process.exitCode = await probeWebhooksCommand({
440
+ specPath: resolved.spec,
441
+ eventLog: opts.eventLog,
442
+ only: opts.only,
443
+ report: rep.report,
444
+ output: rep.output,
445
+ json,
446
+ });
447
+ });
448
+ }
449
+
450
+ export function registerProbes(program: Command): void {
451
+ const probeCmd = program
452
+ .command("probe")
453
+ .description("Run a probe class — pick one of: static, mass-assignment, security, webhooks");
454
+
455
+ defineProbeStatic(probeCmd, "static");
456
+ defineProbeMassAssignment(probeCmd, "mass-assignment");
457
+ defineProbeSecurity(probeCmd, "security");
458
+ defineProbeWebhooks(probeCmd, "webhooks");
459
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `zond reference <topic>` — printable cheat-sheets for built-ins that aren't
3
+ * surfaced anywhere else (TASK-267).
4
+ *
5
+ * Currently:
6
+ * reference random-helpers # all `{{$random*}}` generators with examples
7
+ *
8
+ * Designed to be discoverable from `--help` AND scriptable: `--json` returns
9
+ * an array of `{ name, example }` entries so the `zond-triage` / generator
10
+ * skills can prompt with concrete values.
11
+ */
12
+ import type { Command } from "commander";
13
+ import { GENERATORS } from "../../core/parser/variables.ts";
14
+ import { jsonOk, printJson } from "../json-envelope.ts";
15
+ import { globalJson } from "../resolve.ts";
16
+
17
+ interface HelperEntry {
18
+ name: string;
19
+ example: string;
20
+ use_for: string;
21
+ }
22
+
23
+ const HELPER_NOTES: Record<string, string> = {
24
+ "$uuid": "RFC 4122 v4 — Idempotency-Key, opaque resource ids",
25
+ "$timestamp": "UNIX seconds — created_at, monotonic seeds",
26
+ "$isoTimestamp": "RFC 3339 timestamps",
27
+ "$randomName": "display names",
28
+ "$randomEmail": "unique e-mail body fields",
29
+ "$randomInt": "0–9999, small numeric ids",
30
+ "$randomString": "8 chars mixed-case + digits — opaque tokens",
31
+ "$randomSlug": "8 chars lowercase + digits — slug / handle / URL-safe ids",
32
+ "$randomUrl": "webhook / callback URL fields",
33
+ "$randomFqdn": "DNS / hostname inputs",
34
+ "$randomDomain": "alias of $randomFqdn",
35
+ "$randomIpv4": "RFC 1918 range — client_ip / source_ip",
36
+ "$randomDate": "calendar dates (YYYY-MM-DD)",
37
+ "$randomIsoDate": "ISO-8601 datetime",
38
+ "$nullByte": "single space — placeholder for fields that reject empty strings",
39
+ };
40
+
41
+ function collectEntries(): HelperEntry[] {
42
+ return Object.keys(GENERATORS)
43
+ .sort()
44
+ .map((name) => ({
45
+ name,
46
+ example: String(GENERATORS[name]!()),
47
+ use_for: HELPER_NOTES[name] ?? "",
48
+ }));
49
+ }
50
+
51
+ function renderTable(entries: HelperEntry[]): string {
52
+ const nameWidth = Math.max(8, ...entries.map((e) => e.name.length));
53
+ const exampleWidth = Math.max(10, ...entries.map((e) => e.example.length));
54
+ const lines: string[] = [];
55
+ lines.push(`${pad("Helper", nameWidth)} ${pad("Example", exampleWidth)} Use for`);
56
+ lines.push(`${"-".repeat(nameWidth)} ${"-".repeat(exampleWidth)} ${"-".repeat(20)}`);
57
+ for (const e of entries) {
58
+ lines.push(`${pad(`{{${e.name}}}`, nameWidth + 4)} ${pad(e.example, exampleWidth)} ${e.use_for}`);
59
+ }
60
+ return lines.join("\n");
61
+ }
62
+
63
+ function pad(s: string, width: number): string {
64
+ return s.length >= width ? s : s + " ".repeat(width - s.length);
65
+ }
66
+
67
+ export function registerReference(program: Command): void {
68
+ const ref = program
69
+ .command("reference")
70
+ .description("Printable references for built-ins (e.g. `random-helpers`).");
71
+
72
+ ref
73
+ .command("random-helpers")
74
+ .description("List every `{{$random*}}` / `{{$uuid}}` / `{{$timestamp}}` helper, with a sample value and typical use. Pair `--json` for machine-readable output (TASK-267).")
75
+ .action((opts: unknown, cmd: Command) => {
76
+ const entries = collectEntries();
77
+ if (globalJson(cmd)) {
78
+ printJson(jsonOk("reference random-helpers", { helpers: entries }));
79
+ return;
80
+ }
81
+ process.stdout.write(renderTable(entries) + "\n");
82
+ process.stdout.write(
83
+ "\nFor field-name → helper mapping used by `zond generate`, see docs/random-helpers.md.\n",
84
+ );
85
+ void opts;
86
+ });
87
+ }