@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,165 +0,0 @@
1
- /**
2
- * Probe contract (m-17 / ARV-49).
3
- *
4
- * `zond probe <class>` originally grew as three independent commands —
5
- * static, mass-assignment, security — that ad-hoc-агree on flags and
6
- * output shape. The agent-readable contract started drifting (security
7
- * has --dry-run, mass-assignment doesn't; security --json packages
8
- * markdown into `data.digest.stdout`, run --report json returns
9
- * structured per-endpoint findings; ARV-9 AC#6 deferred --include/--exclude
10
- * for the probe family). m-17 raises this from "convention" to
11
- * "TS-interface validated at boot".
12
- *
13
- * `Probe` is the contract every registered probe class MUST satisfy.
14
- * `commonFlags` is a declarative slot table — the harness uses it both
15
- * for boot-validation (registry refuses to start if a slot is missing)
16
- * and for help/feature-detection. dry-run and run return DIFFERENT
17
- * shapes on purpose (ARV-50): dry-run answers "what would I attack",
18
- * run answers "what did I find". Severity is undefined in dry-run.
19
- */
20
- import type { EndpointInfo, SecuritySchemeInfo } from "../generator/types.ts";
21
-
22
- /**
23
- * Common-flag manifest. Boot-validator checks each registered probe
24
- * declares every slot — `true` means "this probe's CLI exposes the
25
- * flag", `false` means "intentionally not supported". We don't allow
26
- * undefined: forcing a boolean makes "we forgot to wire it" obvious in
27
- * code review (ARV-9 AC#6, F2-15).
28
- */
29
- export interface ProbeFlags {
30
- /** `--api <name>` */
31
- api: boolean;
32
- /** `--tag <tag>` */
33
- tag: boolean;
34
- /** Repeatable `--include <selector:value>` (m-15 ARV-9 grammar). */
35
- include: boolean;
36
- /** Repeatable `--exclude <selector:value>`. */
37
- exclude: boolean;
38
- /** `--dry-run` — list planned attacks without sending requests. */
39
- dryRun: boolean;
40
- /** `--list-tags` — print spec tags and exit. */
41
- listTags: boolean;
42
- /** `--json` — emit single JSON envelope on stdout. */
43
- json: boolean;
44
- /** `--output <file>` — write markdown / SARIF digest to file. */
45
- output: boolean;
46
- /** `--report <markdown|json|sarif>` — choose the structured report
47
- * format (m-17 ARV-51). */
48
- report: boolean;
49
- }
50
-
51
- /**
52
- * Per-endpoint dry-run record. Returned from `Probe.dryRun()`. Severity
53
- * is intentionally absent: nothing has been classified yet (ARV-50).
54
- *
55
- * `planned: true` means the probe would send live traffic at this
56
- * endpoint; `planned: false` + `skip_reason` means we identified the
57
- * endpoint but won't probe it (no body, isolated path-param, …).
58
- */
59
- export interface EndpointPlan {
60
- path: string;
61
- method: string;
62
- planned: boolean;
63
- /** Probe-class IDs we'd run (e.g. ["ssrf","crlf"] or ["mass-assignment"]). */
64
- classes_planned: string[];
65
- /** Suspect fields the probe would touch (mass-assignment / security only). */
66
- fields_planned: string[];
67
- /** Null when planned, populated when planned:false. Closed string set
68
- * per-probe (security: 'no-body'|'no-matched-field'|'isolated-protected'|
69
- * 'unresolved-path'; mass-assignment: 'no-body'|'isolated-protected'). */
70
- skip_reason: string | null;
71
- }
72
-
73
- /**
74
- * Severity classifier outcome for a finding. `inconclusive` covers both
75
- * baseline-failure and 5xx-on-attack — sub-classes carry the detail in
76
- * `evidence`. Mirrors the existing union in security-probe.ts so we
77
- * don't double-up on enums.
78
- */
79
- export type ProbeFindingSeverity =
80
- | "high"
81
- | "low"
82
- | "inconclusive"
83
- | "ok";
84
-
85
- export interface ProbeFinding {
86
- /** Probe-class id (e.g. "ssrf", "open-redirect", "mass-assignment"). */
87
- class: string;
88
- severity: ProbeFindingSeverity;
89
- /** Free-form evidence: request signature, response signature, baseline
90
- * diff, etc. Schema is per-probe, but stays structured (no markdown). */
91
- evidence: Record<string, unknown>;
92
- }
93
-
94
- export type ProbeEndpointStatus = "ok" | "high" | "low" | "inconclusive" | "skipped";
95
-
96
- export interface ProbeEndpointResult {
97
- path: string;
98
- method: string;
99
- /** Probe classes that actually ran on this endpoint. */
100
- classes_run: string[];
101
- findings: ProbeFinding[];
102
- status: ProbeEndpointStatus;
103
- skip_reason?: string;
104
- }
105
-
106
- export interface ProbeRunSummary {
107
- totalEndpoints: number;
108
- probed: number;
109
- /** Per-status tally; identical to existing severity buckets, but with
110
- * closed shape (ARV-51). */
111
- by_status: Record<ProbeEndpointStatus, number>;
112
- }
113
-
114
- /**
115
- * Result of a live `Probe.run()`. `endpoints[]` is the agent-routable
116
- * structure; markdown digest is rendered separately by `Probe.report()`.
117
- */
118
- export interface ProbeResult {
119
- endpoints: ProbeEndpointResult[];
120
- summary: ProbeRunSummary;
121
- warnings: string[];
122
- /** Optional probe-specific extras (e.g. orphans, emittedTests) that
123
- * don't fit the per-endpoint shape but agents still need access to. */
124
- extras?: Record<string, unknown>;
125
- }
126
-
127
- export type ProbeReportFormat = "markdown" | "json";
128
-
129
- export interface ProbeContext {
130
- specPath: string;
131
- /** Pre-loaded endpoints (probe harness loads spec once and shares). */
132
- endpoints: EndpointInfo[];
133
- securitySchemes: SecuritySchemeInfo[];
134
- /** Resolved env vars (`base_url`, `auth_token`, fixture vars). Empty
135
- * for dry-run when the env file is absent. */
136
- vars: Record<string, string>;
137
- /** Selector strings (m-15 ARV-9 grammar: `path:`, `method:`, `tag:`,
138
- * `operation-id:`). Pre-applied to `endpoints` before this context
139
- * reaches the probe. Carried for diagnostics. */
140
- filter?: { includes: string[]; excludes: string[] };
141
- /** Probe-class subset (e.g. for security: ["ssrf","crlf"]). */
142
- classes?: string[];
143
- /** Probe-specific options bag — kept opaque so the harness doesn't
144
- * need to know each probe's flag inventory. */
145
- options: Record<string, unknown>;
146
- }
147
-
148
- /**
149
- * The contract. Every registered probe MUST implement all four
150
- * required methods; missing one trips boot-validation in
151
- * `registry.ts`. listTags is optional — most probes share the same
152
- * loadSpecForProbe shortcut via the harness, so we don't force it.
153
- */
154
- export interface Probe {
155
- readonly name: string;
156
- readonly description: string;
157
- readonly commonFlags: ProbeFlags;
158
- /** List endpoints + classes the probe would attack (no live traffic). */
159
- dryRun(ctx: ProbeContext): Promise<EndpointPlan[]>;
160
- /** Run the probe live and return structured per-endpoint findings. */
161
- run(ctx: ProbeContext): Promise<ProbeResult>;
162
- /** Render a structured (json) or human (markdown) digest. */
163
- report(format: ProbeReportFormat, result: ProbeResult): string | object;
164
- }
165
-
@@ -1,33 +0,0 @@
1
- /**
2
- * Tally verdicts by severity into a caller-shaped bucket object and
3
- * format a one-line summary. Extracted from probe-mass-assignment and
4
- * probe-security CLI commands which had near-identical countBuckets +
5
- * Summary printers — they differ only in the severity vocabulary
6
- * ("medium" vs "inconclusive") and the label/order of the summary line.
7
- *
8
- * Generic over the bucket shape so each command keeps its own JSON-
9
- * envelope keys (camelCase) without rewriting the rest of the pipeline.
10
- */
11
-
12
- export function tallyBySeverity<T extends object>(
13
- verdicts: ReadonlyArray<{ severity: string }>,
14
- mapping: ReadonlyArray<readonly [severity: string, bucket: keyof T & string]>,
15
- zero: T,
16
- ): T {
17
- const out: Record<string, number> = { ...(zero as Record<string, number>) };
18
- const lookup = new Map<string, string>(mapping);
19
- for (const v of verdicts) {
20
- const bucket = lookup.get(v.severity);
21
- if (bucket !== undefined && bucket in out) out[bucket]! += 1;
22
- }
23
- return out as T;
24
- }
25
-
26
- export function formatSummaryLine<T extends object>(
27
- counts: T,
28
- pairs: ReadonlyArray<readonly [label: string, bucket: keyof T & string]>,
29
- ): string {
30
- const indexed = counts as Record<string, number>;
31
- const parts = pairs.map(([label, key]) => `${label} ${indexed[key] ?? 0}`);
32
- return `Summary: ${parts.join(" · ")}`;
33
- }
@@ -1,282 +0,0 @@
1
- /**
2
- * `zond probe webhooks` (m-20 ARV-173) — webhook shape-conformance.
3
- *
4
- * The probe is **offline**: it reads an ndjson event log captured by
5
- * the recipe (`docs/recipes/webhook-receiver.md`, e.g. via
6
- * `stripe listen --print-json`) and validates each event payload
7
- * against the schema declared in `spec.webhooks.<event>.post.requestBody`.
8
- *
9
- * Why offline? m-20 explicitly puts live HTTP infrastructure (tunnels,
10
- * port binding, receiver servers) in recipes, not core zond:
11
- *
12
- * • A live receiver requires a public URL — that's a recipe concern
13
- * (Stripe CLI, ngrok, smee.io are all out-of-band).
14
- * • Capture is bursty (events trickle in seconds-to-minutes after a
15
- * trigger); the probe can't reliably wait for them inside a CLI
16
- * invocation without nasty timeout knobs.
17
- * • Decoupling capture from verification lets the same probe run
18
- * against logs from prod tap, mitm-proxy dumps, CI artifacts, etc.
19
- *
20
- * Recipe captures; probe verifies. Same pattern as quicktype (capture
21
- * → schema infer) and interactsh (capture → OOB-detect) in m-18.
22
- *
23
- * Event log format: ndjson, one event per line. Two recognised shapes:
24
- *
25
- * • Stripe-style — `{type, data: {object: {...}}}` (the payload is
26
- * `data.object`; everything else is envelope metadata).
27
- * • Generic — `{type|event, body|payload, ...}` (the payload is
28
- * whichever of `body`/`payload` is an object; falls back to the
29
- * event itself when neither is present).
30
- *
31
- * Severity policy: HIGH on shape drift (server announced an event
32
- * shape via `webhooks:` and is now sending something else). Unknown
33
- * event types and missing payloads surface at LOW — they're noise
34
- * categories, not contract bugs the API owner promised against.
35
- */
36
- import type { OpenAPIV3, OpenAPIV3_1 } from "openapi-types";
37
- import type { ValidateFunction, ErrorObject } from "ajv";
38
- import { makeAjv } from "../util/ajv.ts";
39
-
40
- interface SingleSchemaValidator {
41
- validate(value: unknown): boolean;
42
- errors: ErrorObject[] | null;
43
- }
44
-
45
- /** Compile a single JSON-Schema-shaped object into a callable validator.
46
- * 3.1-flavour by default (`webhooks:` is OpenAPI 3.1) but tolerates
47
- * pre-3.1 specs using `x-webhooks` — they ship Draft-7-ish schemas. */
48
- function compileSingleSchema(schema: OpenAPIV3.SchemaObject, isV31: boolean): SingleSchemaValidator {
49
- const ajv = makeAjv(isV31, { strict: false, allErrors: true });
50
- const validate: ValidateFunction = ajv.compile(schema);
51
- return {
52
- validate(value) { return validate(value) as boolean; },
53
- get errors() { return validate.errors ?? null; },
54
- };
55
- }
56
-
57
- export type WebhookFindingKind =
58
- | "shape_drift"
59
- | "unknown_event_type"
60
- | "missing_payload"
61
- | "malformed_event";
62
-
63
- export interface WebhookFinding {
64
- /** Line number in the event log (1-indexed) for traceability. */
65
- line: number;
66
- kind: WebhookFindingKind;
67
- severity: "high" | "low";
68
- event_type: string | null;
69
- message: string;
70
- evidence: Record<string, unknown>;
71
- /** Reserved suppression trace (audit-trail). Currently unset — the
72
- * agent triages severity from the raw finding. */
73
- suppressed_by?: { source: string; rule_index: number; reason: string };
74
- }
75
-
76
- export interface WebhookProbeResult {
77
- total_events: number;
78
- by_type: Record<string, { ok: number; drift: number; unknown: number }>;
79
- declared_events: string[];
80
- findings: WebhookFinding[];
81
- /** Reason the probe short-circuited without inspecting any event,
82
- * e.g. spec has no webhooks block. Empty string when normal. */
83
- skip_reason: string;
84
- }
85
-
86
- /** Extract the webhooks block. Tries OpenAPI 3.1 `webhooks:` first
87
- * (the canonical location), falls back to `x-webhooks` for specs
88
- * shipped before OpenAPI 3.1 had ratified the field. Returns an
89
- * empty object when neither exists. */
90
- export function readWebhooksMap(spec: unknown): Record<string, OpenAPIV3.PathItemObject> {
91
- if (!spec || typeof spec !== "object") return {};
92
- const s = spec as Record<string, unknown>;
93
- const candidate = (s.webhooks ?? s["x-webhooks"]) as Record<string, OpenAPIV3.PathItemObject> | undefined;
94
- if (!candidate || typeof candidate !== "object") return {};
95
- return candidate;
96
- }
97
-
98
- /** Pull the request-body schema for the POST operation under a
99
- * webhook entry. Returns null when the entry doesn't declare a POST,
100
- * or when the POST doesn't carry a JSON requestBody schema. */
101
- function schemaForEvent(item: OpenAPIV3.PathItemObject | OpenAPIV3_1.PathItemObject): OpenAPIV3.SchemaObject | null {
102
- const post = item.post;
103
- if (!post) return null;
104
- const rb = post.requestBody;
105
- if (!rb || (rb as OpenAPIV3.ReferenceObject).$ref) return null;
106
- const content = (rb as OpenAPIV3.RequestBodyObject).content ?? {};
107
- const json = content["application/json"];
108
- if (!json?.schema) return null;
109
- return json.schema as OpenAPIV3.SchemaObject;
110
- }
111
-
112
- /** Extract `type` from an event in a tolerant way: try `type` first
113
- * (Stripe / GitHub style), then `event` (legacy / SaaS-style), then
114
- * give up. Numbers are coerced to strings so an integer-valued
115
- * `type` field doesn't masquerade as null. */
116
- function readEventType(event: Record<string, unknown>): string | null {
117
- for (const k of ["type", "event", "event_type"]) {
118
- const v = event[k];
119
- if (typeof v === "string" && v.length > 0) return v;
120
- if (typeof v === "number") return String(v);
121
- }
122
- return null;
123
- }
124
-
125
- /** Extract the payload from an event. Recognised envelopes (in
126
- * priority order): `data.object` (Stripe), `body`, `payload`. Returns
127
- * null when none of those carry an object — missing_payload then
128
- * surfaces as a LOW finding so the operator can fix the capture
129
- * step (and the probe doesn't validate envelope metadata as payload). */
130
- function readEventPayload(event: Record<string, unknown>): unknown {
131
- if (event.data && typeof event.data === "object" && !Array.isArray(event.data)) {
132
- const inner = (event.data as Record<string, unknown>).object;
133
- if (inner && typeof inner === "object") return inner;
134
- }
135
- for (const k of ["body", "payload"]) {
136
- const v = event[k];
137
- if (v && typeof v === "object") return v;
138
- }
139
- return null;
140
- }
141
-
142
- export interface RunOptions {
143
- /** Pre-parsed events. One Record per line; non-object lines should
144
- * surface as malformed_event findings before reaching this layer. */
145
- events: Array<{ line: number; event: Record<string, unknown> }>;
146
- spec: unknown;
147
- /** Optional restriction — only validate events whose `type` is in
148
- * this list. Empty/undefined ⇒ validate everything declared. */
149
- onlyTypes?: string[];
150
- }
151
-
152
- export function runWebhooksProbe(opts: RunOptions): WebhookProbeResult {
153
- const webhooksMap = readWebhooksMap(opts.spec);
154
- const declared = Object.keys(webhooksMap).sort();
155
-
156
- const out: WebhookProbeResult = {
157
- total_events: opts.events.length,
158
- by_type: {},
159
- declared_events: declared,
160
- findings: [],
161
- skip_reason: "",
162
- };
163
-
164
- if (declared.length === 0) {
165
- out.skip_reason = "spec declares no `webhooks:` (or `x-webhooks`) entries — nothing to validate against";
166
- return out;
167
- }
168
-
169
- const isV31 = typeof (opts.spec as { openapi?: string })?.openapi === "string"
170
- && (opts.spec as { openapi: string }).openapi.startsWith("3.1");
171
- // Compile each schema once; events sharing a type reuse the validator.
172
- const validators = new Map<string, SingleSchemaValidator | null>();
173
- for (const [name, item] of Object.entries(webhooksMap)) {
174
- const schema = schemaForEvent(item);
175
- if (!schema) { validators.set(name, null); continue; }
176
- try {
177
- validators.set(name, compileSingleSchema(schema, isV31));
178
- } catch {
179
- validators.set(name, null);
180
- }
181
- }
182
-
183
- const onlyTypes = opts.onlyTypes && opts.onlyTypes.length > 0 ? new Set(opts.onlyTypes) : null;
184
-
185
- for (const { line, event } of opts.events) {
186
- const type = readEventType(event);
187
- if (!type) {
188
- out.findings.push({
189
- line, kind: "malformed_event", severity: "low",
190
- event_type: null,
191
- message: `event has no recognisable type field (tried "type", "event", "event_type")`,
192
- evidence: { event_keys: Object.keys(event) },
193
- });
194
- continue;
195
- }
196
- if (onlyTypes && !onlyTypes.has(type)) continue;
197
- const bucket = out.by_type[type] ?? (out.by_type[type] = { ok: 0, drift: 0, unknown: 0 });
198
- const validator = validators.get(type);
199
- if (validator === undefined) {
200
- bucket.unknown += 1;
201
- out.findings.push({
202
- line, kind: "unknown_event_type", severity: "low",
203
- event_type: type,
204
- message: `event type "${type}" is not declared in spec.webhooks (${declared.length} declared)`,
205
- evidence: { declared_sample: declared.slice(0, 5) },
206
- });
207
- continue;
208
- }
209
- if (validator === null) {
210
- // Declared but no schema to validate against → silent pass for
211
- // that event type; surfacing every such case would be noise.
212
- bucket.ok += 1;
213
- continue;
214
- }
215
- const payload = readEventPayload(event);
216
- if (payload == null || typeof payload !== "object" || Array.isArray(payload)) {
217
- out.findings.push({
218
- line, kind: "missing_payload", severity: "low",
219
- event_type: type,
220
- message: `event "${type}" carries no object payload (data.object / body / payload)`,
221
- evidence: { event_keys: Object.keys(event) },
222
- });
223
- continue;
224
- }
225
- const valid = validator.validate(payload);
226
- if (valid) { bucket.ok += 1; continue; }
227
- bucket.drift += 1;
228
- const errs = validator.errors ?? [];
229
- out.findings.push({
230
- line, kind: "shape_drift", severity: "high",
231
- event_type: type,
232
- message: `event "${type}" does not conform to declared schema (${errs.length} error(s))`,
233
- evidence: {
234
- errors: errs.slice(0, 5).map((e) => ({
235
- path: e.instancePath ?? "",
236
- keyword: e.keyword ?? "",
237
- message: e.message ?? "",
238
- params: e.params ?? {},
239
- })),
240
- },
241
- });
242
- }
243
- return out;
244
- }
245
-
246
- /** Parse an ndjson event log. Each line that is non-empty and parses
247
- * to an object yields one `{line, event}`. Bad lines surface as
248
- * malformed_event findings in the result so the operator gets
249
- * pinpointed feedback. */
250
- export function parseEventLog(text: string): {
251
- events: Array<{ line: number; event: Record<string, unknown> }>;
252
- malformed: WebhookFinding[];
253
- } {
254
- const events: Array<{ line: number; event: Record<string, unknown> }> = [];
255
- const malformed: WebhookFinding[] = [];
256
- const lines = text.split(/\r?\n/);
257
- for (let i = 0; i < lines.length; i++) {
258
- const raw = lines[i]!.trim();
259
- if (raw.length === 0) continue;
260
- try {
261
- const parsed = JSON.parse(raw);
262
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
263
- events.push({ line: i + 1, event: parsed as Record<string, unknown> });
264
- } else {
265
- malformed.push({
266
- line: i + 1, kind: "malformed_event", severity: "low",
267
- event_type: null,
268
- message: `event line is not a JSON object`,
269
- evidence: { sample: raw.slice(0, 60) },
270
- });
271
- }
272
- } catch (e) {
273
- malformed.push({
274
- line: i + 1, kind: "malformed_event", severity: "low",
275
- event_type: null,
276
- message: `ndjson parse failed: ${(e as Error).message}`,
277
- evidence: { sample: raw.slice(0, 60) },
278
- });
279
- }
280
- }
281
- return { events, malformed };
282
- }