@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,691 +0,0 @@
1
- /**
2
- * Negative-input probe generator (T49).
3
- *
4
- * Goal: catch the class of bugs where an API returns 5xx (unhandled exception)
5
- * instead of 4xx (validation error) when given malformed input. The contract
6
- * is simple: any client-supplied invalid input MUST produce a 4xx, never a 5xx.
7
- *
8
- * For each endpoint we generate a suite of probe steps. Each step expects a
9
- * "no 5xx" response (status in [400, 401, 403, 404, 405, 409, 415, 422, 429]).
10
- * If the API returns 500/502/503 — the test fails and the runner logs it as
11
- * a bug candidate via the regular reporter / `zond db diagnose` flow.
12
- *
13
- * ARV-34: 429 is in the allow-set because rate-limiting is itself a valid
14
- * server-side rejection of the request — the API refused to process invalid
15
- * input, the contract is satisfied. Throttled probe runs were producing
16
- * hundreds of false failures with the warning "N requests hit rate limit"
17
- * already saying the same thing.
18
- *
19
- * The probes are deterministic — same spec → same suites — so the generated
20
- * YAML can be committed as a regression test.
21
- */
22
- import type { OpenAPIV3 } from "openapi-types";
23
- import type { EndpointInfo, SecuritySchemeInfo } from "../generator/types.ts";
24
- import type { RawSuite, RawStep } from "../generator/serializer.ts";
25
- import { generateFromSchema } from "../generator/data-factory.ts";
26
- import {
27
- convertPath,
28
- endpointStem,
29
- getAuthHeaders,
30
- renderPath,
31
- isMutating,
32
- findDeleteCounterpart,
33
- captureFieldFor,
34
- headersEqual,
35
- hasJsonBody,
36
- } from "./shared.ts";
37
-
38
- // ──────────────────────────────────────────────
39
- // Constants
40
- // ──────────────────────────────────────────────
41
-
42
- /** Statuses we consider an *acceptable* response to invalid input. Anything
43
- * outside this set (notably 5xx, but also 200/201 which would mean the API
44
- * silently accepted the bad input) is a probe failure. ARV-34: 429 stays in
45
- * the allow-set — server-side throttling is a valid rejection. */
46
- const ACCEPTABLE_4XX = [400, 401, 403, 404, 405, 409, 415, 422, 429];
47
-
48
- /** Long string for boundary probes — 10_000 chars. */
49
- const LONG_STRING = "a".repeat(10_000);
50
-
51
- /** Mixed unicode + emoji + RTL for charset probes. */
52
- const UNICODE_MIX = "Mix🌐مرحبا\u200B";
53
-
54
- /** Sentinel non-UUID inputs for path/UUID probes. */
55
- export const INVALID_UUID_SENTINELS = [
56
- "not-a-uuid",
57
- "12345",
58
- "00000000",
59
- "../../etc/passwd",
60
- ] as const;
61
- const INVALID_UUID_VALUES = INVALID_UUID_SENTINELS;
62
-
63
- /** Sentinel invalid emails. */
64
- const INVALID_EMAIL_VALUES = [
65
- "not-an-email",
66
- "@no-local.example.com",
67
- "spaces in@email.com",
68
- ];
69
-
70
- /** Sentinel invalid URIs. */
71
- const INVALID_URI_VALUES = [
72
- "not a url",
73
- "javascript:alert(1)",
74
- "ftp:/missing-slash",
75
- ];
76
-
77
- /** Sentinel invalid date-time strings. */
78
- const INVALID_DATETIME_VALUES = [
79
- "yesterday",
80
- "2023-13-45T99:99:99Z",
81
- "2023-10-06:23:47:56.678Z", // colon-instead-of-T (real bug we caught)
82
- ];
83
-
84
- // ──────────────────────────────────────────────
85
- // Types
86
- // ──────────────────────────────────────────────
87
-
88
- export interface ProbeOptions {
89
- endpoints: EndpointInfo[];
90
- securitySchemes: SecuritySchemeInfo[];
91
- /** Cap probes per endpoint (default 50). Hard cutoff for huge schemas. */
92
- maxProbesPerEndpoint?: number;
93
- /**
94
- * Skip emission of follow-up DELETE cleanup steps for mutating probes
95
- * (POST/PUT/PATCH). Useful for namespace-isolated test environments
96
- * (staging dump-and-reset) where cleanup is handled out of band.
97
- */
98
- noCleanup?: boolean;
99
- /**
100
- * TASK-135: when true (default), non-attacked path-params are emitted as
101
- * runtime placeholders `{{name}}` so `zond run` substitutes them from
102
- * `.env.yaml`. This avoids the short-circuit where every probe targeting a
103
- * nested path resolved `{org}=nonexistent-zzzzz` and got 404 before the
104
- * leaf validator ever fired. Set to false to keep the legacy behaviour
105
- * (synthetic-by-type for every param).
106
- */
107
- useRealParents?: boolean;
108
- }
109
-
110
- export interface ProbeResult {
111
- suites: RawSuite[];
112
- /** Number of endpoints that received probes. */
113
- probedEndpoints: number;
114
- /** Endpoints we skipped (no body & no UUID path params). */
115
- skippedEndpoints: number;
116
- /** Total generated probe steps. */
117
- totalProbes: number;
118
- /**
119
- * Generation-time warnings — typically about mutating endpoints whose
120
- * probes might leak resources because the spec defines no DELETE
121
- * counterpart. CLI surfaces these to the user.
122
- */
123
- warnings: string[];
124
- }
125
-
126
- // ──────────────────────────────────────────────
127
- // Helpers
128
- // ──────────────────────────────────────────────
129
-
130
- function findUuidPathParams(ep: EndpointInfo): OpenAPIV3.ParameterObject[] {
131
- return ep.parameters.filter((p) => {
132
- if (p.in !== "path") return false;
133
- const schema = p.schema as OpenAPIV3.SchemaObject | undefined;
134
- if (!schema) return false;
135
- if (schema.format === "uuid") return true;
136
- // also probe path params named like *_id / *_uuid
137
- const lower = p.name.toLowerCase();
138
- return lower === "id" || lower.endsWith("_id") || lower === "uuid";
139
- });
140
- }
141
-
142
- /** Walk schema and collect required-field paths up to depth 1 with their schema. */
143
- function collectRequiredFields(
144
- schema: OpenAPIV3.SchemaObject | undefined,
145
- ): Array<{ name: string; schema: OpenAPIV3.SchemaObject }> {
146
- if (!schema || !schema.properties) return [];
147
- const required = new Set(schema.required ?? []);
148
- const out: Array<{ name: string; schema: OpenAPIV3.SchemaObject }> = [];
149
- for (const [name, propSchema] of Object.entries(schema.properties)) {
150
- if (required.has(name)) {
151
- out.push({ name, schema: propSchema as OpenAPIV3.SchemaObject });
152
- }
153
- }
154
- return out;
155
- }
156
-
157
- /** Walk schema (depth 1) and collect all properties with their schema. */
158
- function collectAllProps(
159
- schema: OpenAPIV3.SchemaObject | undefined,
160
- ): Array<{ name: string; schema: OpenAPIV3.SchemaObject; required: boolean }> {
161
- if (!schema || !schema.properties) return [];
162
- const required = new Set(schema.required ?? []);
163
- return Object.entries(schema.properties).map(([name, s]) => ({
164
- name,
165
- schema: s as OpenAPIV3.SchemaObject,
166
- required: required.has(name),
167
- }));
168
- }
169
-
170
- // ──────────────────────────────────────────────
171
- // Probe generators
172
- // ──────────────────────────────────────────────
173
-
174
- /**
175
- * Build a step that targets `endpoint`, but with an arbitrary body override.
176
- * Authentication and required path params are populated with valid placeholders
177
- * so the request reaches the body-validation layer.
178
- *
179
- * `pathOverride` (if provided) is treated as already-final — no further
180
- * placeholder conversion is applied. Otherwise the path is rendered via
181
- * `renderPath` with no attack target (so all path-params become runtime
182
- * placeholders / synthetic sentinels depending on `useRealParents`).
183
- */
184
- function buildStep(
185
- ep: EndpointInfo,
186
- schemes: SecuritySchemeInfo[],
187
- opts: {
188
- name: string;
189
- json?: unknown;
190
- pathOverride?: string;
191
- expectStatusOk?: number[];
192
- useRealParents: boolean;
193
- },
194
- ): RawStep {
195
- const method = ep.method.toUpperCase();
196
- const path = opts.pathOverride ?? renderPath(ep, null, { useRealParents: opts.useRealParents });
197
- const headers = getAuthHeaders(ep, schemes);
198
-
199
- const expectedStatus = opts.expectStatusOk ?? ACCEPTABLE_4XX;
200
- const responseBranch = Array.isArray(expectedStatus) ? expectedStatus.map(String).join("|") : String(expectedStatus);
201
- const step: RawStep = {
202
- name: opts.name,
203
- source: {
204
- generator: "negative-probe",
205
- endpoint: `${method} ${ep.path}`,
206
- response_branch: responseBranch,
207
- },
208
- [method]: path,
209
- expect: {
210
- status: expectedStatus,
211
- },
212
- };
213
- if (headers) step.headers = headers;
214
- if (opts.json !== undefined) (step as any).json = opts.json;
215
- return step;
216
- }
217
-
218
- function probeEmptyBody(
219
- ep: EndpointInfo,
220
- schemes: SecuritySchemeInfo[],
221
- useRealParents: boolean,
222
- ): RawStep | null {
223
- if (!hasJsonBody(ep)) return null;
224
- const required = collectRequiredFields(ep.requestBodySchema);
225
- // Only meaningful when there *is* required data — otherwise {} is valid.
226
- if (required.length === 0) return null;
227
- return buildStep(ep, schemes, {
228
- name: "empty body — must reject (no 5xx)",
229
- json: {},
230
- useRealParents,
231
- });
232
- }
233
-
234
- function probeMissingRequired(
235
- ep: EndpointInfo,
236
- schemes: SecuritySchemeInfo[],
237
- budget: number,
238
- useRealParents: boolean,
239
- ): RawStep[] {
240
- if (!hasJsonBody(ep) || !ep.requestBodySchema) return [];
241
- const required = collectRequiredFields(ep.requestBodySchema);
242
- if (required.length === 0) return [];
243
-
244
- // Build a baseline valid object, then drop one required field at a time.
245
- const baseline = generateFromSchema(ep.requestBodySchema) as Record<string, unknown>;
246
- if (typeof baseline !== "object" || baseline === null) return [];
247
-
248
- const out: RawStep[] = [];
249
- for (const field of required) {
250
- if (out.length >= budget) break;
251
- const variant = { ...baseline };
252
- delete variant[field.name];
253
- out.push(
254
- buildStep(ep, schemes, {
255
- name: `missing required field "${field.name}" — must reject (no 5xx)`,
256
- json: variant,
257
- useRealParents,
258
- }),
259
- );
260
- }
261
- return out;
262
- }
263
-
264
- function probeBoundaryString(
265
- ep: EndpointInfo,
266
- schemes: SecuritySchemeInfo[],
267
- budget: number,
268
- useRealParents: boolean,
269
- ): RawStep[] {
270
- if (!hasJsonBody(ep) || !ep.requestBodySchema) return [];
271
- const props = collectAllProps(ep.requestBodySchema).filter(
272
- (p) => p.schema.type === "string",
273
- );
274
- if (props.length === 0) return [];
275
-
276
- const baseline = generateFromSchema(ep.requestBodySchema) as Record<string, unknown>;
277
- if (typeof baseline !== "object" || baseline === null) return [];
278
-
279
- const out: RawStep[] = [];
280
- // Only probe the first N string fields to stay within budget
281
- for (const field of props.slice(0, Math.max(1, Math.floor(budget / 3)))) {
282
- if (out.length + 3 > budget) break;
283
- out.push(
284
- buildStep(ep, schemes, {
285
- name: `${field.name}: empty string — must reject (no 5xx)`,
286
- json: { ...baseline, [field.name]: "" },
287
- useRealParents,
288
- }),
289
- buildStep(ep, schemes, {
290
- name: `${field.name}: 10000-char string — must reject or accept (no 5xx)`,
291
- json: { ...baseline, [field.name]: LONG_STRING },
292
- useRealParents,
293
- }),
294
- buildStep(ep, schemes, {
295
- name: `${field.name}: unicode/emoji/RTL — must not 5xx`,
296
- json: { ...baseline, [field.name]: UNICODE_MIX },
297
- useRealParents,
298
- }),
299
- );
300
- }
301
- return out;
302
- }
303
-
304
- function probeTypeConfusion(
305
- ep: EndpointInfo,
306
- schemes: SecuritySchemeInfo[],
307
- budget: number,
308
- useRealParents: boolean,
309
- ): RawStep[] {
310
- if (!hasJsonBody(ep) || !ep.requestBodySchema) return [];
311
- const props = collectAllProps(ep.requestBodySchema);
312
- if (props.length === 0) return [];
313
-
314
- const baseline = generateFromSchema(ep.requestBodySchema) as Record<string, unknown>;
315
- if (typeof baseline !== "object" || baseline === null) return [];
316
-
317
- const out: RawStep[] = [];
318
- for (const field of props) {
319
- if (out.length >= budget) break;
320
- const wrongValue = pickWrongType(field.schema);
321
- if (wrongValue === undefined) continue;
322
- out.push(
323
- buildStep(ep, schemes, {
324
- name: `${field.name}: wrong type (${describeType(field.schema)} → ${typeof wrongValue}) — must reject (no 5xx)`,
325
- json: { ...baseline, [field.name]: wrongValue },
326
- useRealParents,
327
- }),
328
- );
329
- }
330
- return out;
331
- }
332
-
333
- function probeInvalidFormat(
334
- ep: EndpointInfo,
335
- schemes: SecuritySchemeInfo[],
336
- budget: number,
337
- useRealParents: boolean,
338
- ): RawStep[] {
339
- if (!hasJsonBody(ep) || !ep.requestBodySchema) return [];
340
- const props = collectAllProps(ep.requestBodySchema);
341
- const baseline = generateFromSchema(ep.requestBodySchema) as Record<string, unknown>;
342
- if (typeof baseline !== "object" || baseline === null) return [];
343
-
344
- const out: RawStep[] = [];
345
- for (const field of props) {
346
- if (out.length >= budget) break;
347
- const fmt = field.schema.format;
348
- let badValue: string | undefined;
349
- if (fmt === "email") badValue = INVALID_EMAIL_VALUES[0];
350
- else if (fmt === "uri" || fmt === "url") badValue = INVALID_URI_VALUES[0];
351
- else if (fmt === "date-time") badValue = INVALID_DATETIME_VALUES[0];
352
- else if (fmt === "uuid") badValue = INVALID_UUID_VALUES[0];
353
- if (badValue === undefined) continue;
354
- out.push(
355
- buildStep(ep, schemes, {
356
- name: `${field.name}: invalid ${fmt} (${JSON.stringify(badValue)}) — must reject (no 5xx)`,
357
- json: { ...baseline, [field.name]: badValue },
358
- useRealParents,
359
- }),
360
- );
361
- }
362
- return out;
363
- }
364
-
365
- function probeInvalidEnum(
366
- ep: EndpointInfo,
367
- schemes: SecuritySchemeInfo[],
368
- budget: number,
369
- useRealParents: boolean,
370
- ): RawStep[] {
371
- if (!hasJsonBody(ep) || !ep.requestBodySchema) return [];
372
- const baseline = generateFromSchema(ep.requestBodySchema) as Record<string, unknown>;
373
- if (typeof baseline !== "object" || baseline === null) return [];
374
-
375
- const out: RawStep[] = [];
376
- // Walk depth 1 for plain enum strings
377
- for (const field of collectAllProps(ep.requestBodySchema)) {
378
- if (out.length >= budget) break;
379
- if (Array.isArray(field.schema.enum) && field.schema.enum.length > 0) {
380
- out.push(
381
- buildStep(ep, schemes, {
382
- name: `${field.name}: unknown enum value "zond_invalid_value" — must reject (no 5xx)`,
383
- json: { ...baseline, [field.name]: "zond_invalid_value" },
384
- useRealParents,
385
- }),
386
- );
387
- }
388
- // enum-of-strings inside an array (e.g. webhooks.events: [enum])
389
- if (field.schema.type === "array" && field.schema.items) {
390
- const items = field.schema.items as OpenAPIV3.SchemaObject;
391
- const enumLike = Array.isArray(items.enum) && items.enum.length > 0;
392
- const isStringArray = items.type === "string";
393
- if (enumLike || isStringArray) {
394
- // even when no enum is declared, names like "events"/"types"/"channels"
395
- // strongly imply a backing whitelist — bug #05B
396
- out.push(
397
- buildStep(ep, schemes, {
398
- name: `${field.name}: array with unknown value ["zond.nonexistent.event"] — must reject (no 5xx)`,
399
- json: { ...baseline, [field.name]: ["zond.nonexistent.event"] },
400
- useRealParents,
401
- }),
402
- );
403
- }
404
- }
405
- }
406
- return out;
407
- }
408
-
409
- function probeInvalidPathId(
410
- ep: EndpointInfo,
411
- schemes: SecuritySchemeInfo[],
412
- budget: number,
413
- useRealParents: boolean,
414
- ): RawStep[] {
415
- const params = findUuidPathParams(ep);
416
- if (params.length === 0) return [];
417
- // Skip POST /resource (no path id) — covered by body probes
418
- const out: RawStep[] = [];
419
- for (const param of params) {
420
- for (const bad of INVALID_UUID_VALUES) {
421
- if (out.length >= budget) break;
422
- const path = renderPath(ep, { name: param.name, value: bad }, { useRealParents });
423
- out.push(
424
- buildStep(ep, schemes, {
425
- name: `path param ${param.name}=${JSON.stringify(bad)} — must reject (no 5xx)`,
426
- pathOverride: path,
427
- useRealParents,
428
- }),
429
- );
430
- }
431
- }
432
- return out;
433
- }
434
-
435
- /** TASK-67: numeric coercion probes for integer/number params (query + path).
436
- * Round-2 found `GET /emails?limit=1.5` → 500 — the bug class probe-validation
437
- * exists for. T49 covered numeric type-confusion in body, this extends to
438
- * query/path. */
439
- const NUMERIC_BAD_VALUES: Array<{ value: string; label: string }> = [
440
- { value: "1.5", label: "float on integer" },
441
- { value: "-1", label: "negative" },
442
- { value: "0", label: "zero" },
443
- { value: "abc", label: "non-numeric" },
444
- { value: "", label: "empty string" },
445
- // null on a query param means literal "null" string — most parsers treat it as bad input
446
- { value: "null", label: "literal null" },
447
- // Number.MAX_SAFE_INTEGER + 1 = 9007199254740992
448
- { value: "9007199254740992", label: "MAX_SAFE_INTEGER+1" },
449
- ];
450
-
451
- function isNumericSchema(schema: OpenAPIV3.SchemaObject | undefined): boolean {
452
- return schema?.type === "integer" || schema?.type === "number";
453
- }
454
-
455
- function probeNumericQueryParams(
456
- ep: EndpointInfo,
457
- schemes: SecuritySchemeInfo[],
458
- budget: number,
459
- useRealParents: boolean,
460
- ): RawStep[] {
461
- const numericQuery = ep.parameters.filter(
462
- (p) => p.in === "query" && isNumericSchema(p.schema as OpenAPIV3.SchemaObject | undefined),
463
- );
464
- if (numericQuery.length === 0) return [];
465
-
466
- const out: RawStep[] = [];
467
- for (const param of numericQuery) {
468
- for (const { value, label } of NUMERIC_BAD_VALUES) {
469
- if (out.length >= budget) break;
470
- const basePath = renderPath(ep, null, { useRealParents });
471
- const sep = basePath.includes("?") ? "&" : "?";
472
- const pathWithQuery = `${basePath}${sep}${param.name}=${encodeURIComponent(value)}`;
473
- out.push(
474
- buildStep(ep, schemes, {
475
- name: `query ${param.name}=${JSON.stringify(value)} (${label}) — must reject (no 5xx)`,
476
- pathOverride: pathWithQuery,
477
- useRealParents,
478
- }),
479
- );
480
- }
481
- }
482
- return out;
483
- }
484
-
485
- function probeNumericPathParams(
486
- ep: EndpointInfo,
487
- schemes: SecuritySchemeInfo[],
488
- budget: number,
489
- useRealParents: boolean,
490
- ): RawStep[] {
491
- const numericPath = ep.parameters.filter(
492
- (p) => p.in === "path" && isNumericSchema(p.schema as OpenAPIV3.SchemaObject | undefined),
493
- );
494
- if (numericPath.length === 0) return [];
495
-
496
- const out: RawStep[] = [];
497
- for (const param of numericPath) {
498
- for (const { value, label } of NUMERIC_BAD_VALUES) {
499
- if (value === "") continue; // empty path segment yields a different endpoint
500
- if (out.length >= budget) break;
501
- const overriddenPath = renderPath(ep, { name: param.name, value }, { useRealParents });
502
- out.push(
503
- buildStep(ep, schemes, {
504
- name: `path param ${param.name}=${JSON.stringify(value)} (${label}) — must reject (no 5xx)`,
505
- pathOverride: overriddenPath,
506
- useRealParents,
507
- }),
508
- );
509
- }
510
- }
511
- return out;
512
- }
513
-
514
- // ──────────────────────────────────────────────
515
- // Type-confusion helpers
516
- // ──────────────────────────────────────────────
517
-
518
- function pickWrongType(schema: OpenAPIV3.SchemaObject): unknown | undefined {
519
- switch (schema.type) {
520
- case "string":
521
- return 12345; // number where string expected
522
- case "integer":
523
- case "number":
524
- return "five"; // string where number expected
525
- case "boolean":
526
- return "true"; // string where boolean expected
527
- case "array":
528
- return { not: "an-array" }; // object where array expected
529
- case "object":
530
- return ["not", "an", "object"]; // array where object expected
531
- default:
532
- return undefined;
533
- }
534
- }
535
-
536
- function describeType(schema: OpenAPIV3.SchemaObject): string {
537
- if (schema.format) return `${schema.type ?? "any"}/${schema.format}`;
538
- return schema.type ?? "any";
539
- }
540
-
541
- /** Build a cleanup-DELETE step for a single mutating probe. The capture var
542
- * must come from the paired probe step. If the probe didn't capture (e.g.
543
- * the API correctly returned 4xx and no resource was created), the runner
544
- * skips this step via the standard "missing capture" path — exactly the
545
- * semantics we want. */
546
- function buildCleanupStep(
547
- deleteEp: EndpointInfo,
548
- schemes: SecuritySchemeInfo[],
549
- captureVar: string,
550
- probeStepName: string,
551
- ): RawStep {
552
- // Replace the DELETE's path-id placeholder with our captured var.
553
- const path = convertPath(deleteEp.path).replace(/\{\{[^}]+\}\}/, `{{${captureVar}}}`);
554
- const headers = getAuthHeaders(deleteEp, schemes);
555
- const step: RawStep = {
556
- name: `cleanup leaked resource from "${probeStepName}"`,
557
- source: {
558
- generator: "negative-probe-cleanup",
559
- endpoint: `DELETE ${deleteEp.path}`,
560
- },
561
- always: true,
562
- DELETE: path,
563
- expect: {
564
- status: [200, 202, 204, 404],
565
- },
566
- };
567
- if (headers) step.headers = headers;
568
- return step;
569
- }
570
-
571
- // ──────────────────────────────────────────────
572
- // Public API
573
- // ──────────────────────────────────────────────
574
-
575
- export function generateNegativeProbes(opts: ProbeOptions): ProbeResult {
576
- const { endpoints, securitySchemes } = opts;
577
- const cap = opts.maxProbesPerEndpoint ?? 50;
578
- const noCleanup = opts.noCleanup === true;
579
- // TASK-135: default ON. Non-attacked path-params are emitted as runtime
580
- // placeholders `{{name}}` and resolved from `.env.yaml` by `zond run`.
581
- const useRealParents = opts.useRealParents !== false;
582
-
583
- const suites: RawSuite[] = [];
584
- const warnings: string[] = [];
585
- let probedEndpoints = 0;
586
- let skippedEndpoints = 0;
587
- let totalProbes = 0;
588
-
589
- for (const ep of endpoints) {
590
- if (ep.deprecated) continue;
591
-
592
- const steps: RawStep[] = [];
593
- const remaining = () => Math.max(0, cap - steps.length);
594
-
595
- // 1. Path-id probes (cheap, deterministic)
596
- steps.push(...probeInvalidPathId(ep, securitySchemes, remaining(), useRealParents));
597
-
598
- // 1b. Numeric query / path coercion probes (T67) — float-on-integer,
599
- // negative, non-numeric, etc. Catches `GET /x?limit=1.5` → 500.
600
- const numericQueryProbes = probeNumericQueryParams(ep, securitySchemes, remaining(), useRealParents);
601
- steps.push(...numericQueryProbes);
602
- const numericPathProbes = probeNumericPathParams(ep, securitySchemes, remaining(), useRealParents);
603
- steps.push(...numericPathProbes);
604
- const hasNumericCoercion = numericQueryProbes.length + numericPathProbes.length > 0;
605
-
606
- // 2. Body probes (only for body-bearing methods)
607
- const empty = probeEmptyBody(ep, securitySchemes, useRealParents);
608
- if (empty && steps.length < cap) steps.push(empty);
609
-
610
- steps.push(...probeMissingRequired(ep, securitySchemes, remaining(), useRealParents));
611
- steps.push(...probeTypeConfusion(ep, securitySchemes, remaining(), useRealParents));
612
- steps.push(...probeInvalidFormat(ep, securitySchemes, remaining(), useRealParents));
613
- steps.push(...probeBoundaryString(ep, securitySchemes, remaining(), useRealParents));
614
- steps.push(...probeInvalidEnum(ep, securitySchemes, remaining(), useRealParents));
615
-
616
- if (steps.length === 0) {
617
- skippedEndpoints++;
618
- continue;
619
- }
620
-
621
- // T79: Cleanup for mutating probes. If a probe accidentally returns 2xx
622
- // (the bug class probe-validation hunts for), the resource sticks around
623
- // unless we follow up with a DELETE. We pair each probe step with a
624
- // cleanup-DELETE marked `always: true`; the runner skips the DELETE
625
- // automatically when no id was captured (i.e. the probe correctly got 4xx).
626
- const cleanupSteps: RawStep[] = [];
627
- if (isMutating(ep.method) && !noCleanup) {
628
- const deleteEp = findDeleteCounterpart(ep, endpoints);
629
- if (deleteEp) {
630
- const idField = captureFieldFor(ep);
631
- for (let i = 0; i < steps.length; i++) {
632
- const probeStep = steps[i]!;
633
- const captureVar = `leaked_id_${i}`;
634
- const probeExpect = probeStep.expect as { body?: Record<string, unknown> };
635
- if (!probeExpect.body) probeExpect.body = {};
636
- // capture-only rule: doesn't add an assertion, just extracts the id
637
- // when the response body has one. extractCaptures is a no-op when
638
- // the field is absent (the typical 4xx case).
639
- probeExpect.body[idField] = { capture: captureVar };
640
- cleanupSteps.push(buildCleanupStep(deleteEp, securitySchemes, captureVar, probeStep.name));
641
- }
642
- } else {
643
- warnings.push(
644
- `${ep.method.toUpperCase()} ${ep.path}: probe-validation may create resources but spec defines no DELETE counterpart — manual cleanup required if any probe unexpectedly returns 2xx`,
645
- );
646
- }
647
- }
648
- if (cleanupSteps.length > 0) {
649
- steps.push(...cleanupSteps);
650
- }
651
-
652
- probedEndpoints++;
653
- totalProbes += steps.length;
654
-
655
- // Hoist auth headers to suite level — every probe in this suite hits the
656
- // same endpoint, so per-step headers are pure duplication. Dropping them
657
- // here keeps generated YAML small and makes suite-level overrides
658
- // (e.g. switching auth tokens) work as expected.
659
- const suiteHeaders = getAuthHeaders(ep, securitySchemes);
660
- if (suiteHeaders) {
661
- for (const step of steps) {
662
- if (step.headers && headersEqual(step.headers as Record<string, string>, suiteHeaders)) {
663
- delete (step as { headers?: unknown }).headers;
664
- }
665
- }
666
- }
667
-
668
- const stem = endpointStem(ep);
669
- const suite: RawSuite = {
670
- name: `probe ${ep.method} ${ep.path}`,
671
- tags: [
672
- "probe-validation",
673
- "negative-input",
674
- "no-5xx",
675
- ...(hasNumericCoercion ? ["query-coercion"] : []),
676
- ],
677
- source: {
678
- type: "probe-suite",
679
- generator: "negative-probe",
680
- endpoint: `${ep.method.toUpperCase()} ${ep.path}`,
681
- },
682
- fileStem: `probe-${stem}`,
683
- base_url: "{{base_url}}",
684
- ...(suiteHeaders ? { headers: suiteHeaders } : {}),
685
- tests: steps,
686
- };
687
- suites.push(suite);
688
- }
689
-
690
- return { suites, probedEndpoints, skippedEndpoints, totalProbes, warnings };
691
- }