@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,398 +0,0 @@
1
- /**
2
- * `zond probe-security <classes>` — live SSRF / CRLF / open-redirect probes.
3
- *
4
- * Mirrors the `probe-mass-assignment` shape: live runner, optional regression
5
- * YAML emission, idempotent cleanup. Where mass-assignment injects extra
6
- * suspect fields, this probe replaces a single benign field with a security
7
- * payload (SSRF / CRLF / open-redirect) and classifies the response.
8
- */
9
- import type { EndpointInfo, SecuritySchemeInfo } from "../../generator/types.ts";
10
- import { executeRequest } from "../../runner/http-client.ts";
11
- import {
12
- classifyPostSemantics,
13
- findDeleteCounterpart,
14
- pathTouchesSeededVar,
15
- } from "../shared.ts";
16
- import {
17
- buildBaselineFromSpec,
18
- buildBodyAuthHeaders,
19
- buildProbeUrl,
20
- hasProbeBody,
21
- serializeProbeBody,
22
- } from "../probe-harness.ts";
23
- import { detectFields, PAYLOADS } from "./detectors.ts";
24
- import {
25
- restoreOriginal,
26
- sendBaseline,
27
- snapshotOriginal,
28
- } from "./baseline.ts";
29
- import { classify } from "./classify.ts";
30
- import { tryCleanup } from "./cleanup.ts";
31
- import type {
32
- CleanupFeasibility,
33
- ProbeStepOpts,
34
- SecurityFieldHit,
35
- SecurityProbeOptions,
36
- SecurityProbeResult,
37
- SecuritySeverity,
38
- SecurityVerdict,
39
- } from "./types.ts";
40
-
41
- export async function runSecurityProbes(
42
- opts: SecurityProbeOptions,
43
- ): Promise<SecurityProbeResult> {
44
- const verdicts: SecurityVerdict[] = [];
45
- const warnings: string[] = [];
46
- let totalEndpoints = 0;
47
-
48
- // ARV-140: pre-flight cleanup-feasibility scan. For each POST target, look
49
- // up the DELETE counterpart in the spec once. Without --allow-leaks any
50
- // attack against a POST-without-DELETE is dropped — orphan tracker can't
51
- // clean it (no DELETE path to retry) so it would linger in the user's
52
- // tenant indefinitely (feedback round-01/02 Sentry: 18 manual cleanups).
53
- const feasibility: CleanupFeasibility = {
54
- status: {},
55
- skippedNoCleanup: 0,
56
- forcedNoCleanup: 0,
57
- actionNoCleanupNeeded: 0,
58
- };
59
- for (const ep of opts.endpoints) {
60
- if (ep.deprecated) continue;
61
- if (ep.method.toUpperCase() !== "POST") continue;
62
- const key = `POST ${ep.path}`;
63
- // ARV-153: action POSTs (`/capture`, `/verify`, `/cancel`, …) don't
64
- // allocate a new resource — there is nothing to DELETE. Attacking them
65
- // without `--allow-leaks` is safe; classifying them up front prevents
66
- // the feasibility pre-flight from masking 18/22 Stripe action endpoints.
67
- const semantics = classifyPostSemantics(ep);
68
- if (semantics === "action") {
69
- feasibility.status[key] = "action";
70
- feasibility.actionNoCleanupNeeded += 1;
71
- continue;
72
- }
73
- const hasDelete = findDeleteCounterpart(ep, opts.endpoints) !== undefined;
74
- feasibility.status[key] = hasDelete ? "has-delete" : "no-delete-counterpart";
75
- if (!hasDelete) {
76
- if (opts.allowLeaks) feasibility.forcedNoCleanup += 1;
77
- else feasibility.skippedNoCleanup += 1;
78
- }
79
- }
80
-
81
- for (const ep of opts.endpoints) {
82
- if (ep.deprecated) continue;
83
- const m = ep.method.toUpperCase();
84
- if (m !== "POST" && m !== "PUT" && m !== "PATCH") continue;
85
- totalEndpoints++;
86
-
87
- // ARV-140: cleanup-feasibility gate. POST without a DELETE counterpart
88
- // (and without --allow-leaks) is dropped before any live request fires.
89
- // PUT/PATCH have snapshot/restore so they're unaffected here.
90
- if (m === "POST" && !opts.allowLeaks) {
91
- const status = feasibility.status[`POST ${ep.path}`];
92
- if (status === "no-delete-counterpart") {
93
- verdicts.push(skipped(ep, "skipped: no DELETE counterpart in spec (cleanup-feasibility pre-flight; pass --allow-leaks to override)"));
94
- continue;
95
- }
96
- }
97
-
98
- // TASK-264: --isolated guard. Mutation on a seeded fixture would corrupt
99
- // user data the next `zond run` depends on; skip the endpoint instead.
100
- if (opts.isolated && (m === "PUT" || m === "PATCH") && pathTouchesSeededVar(ep.path, opts.vars)) {
101
- verdicts.push(skipped(ep, "skipped: --isolated mode protects seeded fixtures (PUT/PATCH on seeded path-params)"));
102
- continue;
103
- }
104
-
105
- // ARV-161 (round-08 F18): parity with mass-assignment — accept
106
- // application/x-www-form-urlencoded endpoints too. Stripe v1 declares
107
- // user-controlled URL fields (webhook url, return_url, ...) only on
108
- // form-encoded bodies; the previous JSON-only gate hid 78+ POSTs from
109
- // SSRF/CRLF/open-redirect probing.
110
- if (!hasProbeBody(ep)) {
111
- verdicts.push(skipped(ep, "no JSON or form-urlencoded request body"));
112
- continue;
113
- }
114
-
115
- const detected = detectFields(ep, opts.classes);
116
- if (detected.length === 0) {
117
- verdicts.push(skipped(ep, `no fields matched classes: ${opts.classes.join(",")}`));
118
- continue;
119
- }
120
-
121
- if (opts.dryRun) {
122
- verdicts.push({
123
- method: m,
124
- path: ep.path,
125
- severity: "skipped",
126
- summary: "dry-run: would attack " + detected.map(d => `${d.field}/${d.class}`).join(", "),
127
- detectedFields: detected,
128
- findings: [],
129
- skipReason: "dry-run",
130
- });
131
- continue;
132
- }
133
-
134
- const verdict = await probeOneEndpoint(
135
- ep,
136
- opts.endpoints,
137
- opts.securitySchemes,
138
- opts.vars,
139
- detected,
140
- {
141
- noCleanup: opts.noCleanup === true,
142
- timeoutMs: opts.timeoutMs,
143
- cleanupRetryDelaysMs: opts.cleanupRetryDelaysMs,
144
- seedBody: opts.seedBodies?.get(`${ep.method.toUpperCase()} ${ep.path}`),
145
- },
146
- );
147
- verdicts.push(verdict);
148
- }
149
-
150
- return {
151
- classes: opts.classes,
152
- totalEndpoints,
153
- specProbed: verdicts.length,
154
- verdicts,
155
- warnings,
156
- cleanupFeasibility: feasibility,
157
- };
158
- }
159
-
160
- async function probeOneEndpoint(
161
- ep: EndpointInfo,
162
- allEndpoints: EndpointInfo[],
163
- schemes: SecuritySchemeInfo[],
164
- vars: Record<string, string>,
165
- detected: SecurityFieldHit[],
166
- opts: ProbeStepOpts,
167
- ): Promise<SecurityVerdict> {
168
- const m = ep.method.toUpperCase();
169
- const verdict: SecurityVerdict = {
170
- method: m,
171
- path: ep.path,
172
- severity: "ok",
173
- summary: "",
174
- detectedFields: detected,
175
- findings: [],
176
- };
177
-
178
- // Build baseline body. Same recipe as mass-assignment: spec → generators → vars.
179
- // ARV-269: agent-authored `seed_body` overlay wins when present.
180
- const baseline = buildBaselineFromSpec(ep, vars, opts.seedBody);
181
- if (baseline === null) {
182
- return skipped(ep, "request body not a JSON object");
183
- }
184
-
185
- const { url, unresolved } = buildProbeUrl(ep, vars);
186
- if (unresolved.length > 0) {
187
- return skipped(ep, `cannot resolve path placeholders: ${unresolved.join(", ")}`);
188
- }
189
-
190
- // ARV-161: Content-Type follows the spec — form-urlencoded for Stripe v1,
191
- // JSON otherwise. All outbound payloads in this function (baseline, per-
192
- // attack, restore-PUT) flow through serializeProbeBody for matching wire
193
- // encoding.
194
- const headers = buildBodyAuthHeaders(ep, schemes, vars);
195
-
196
- // ── Snapshot original state (TASK-151) ────────────────────────────────
197
- // For PUT/PATCH we MUST capture original state before any mutation. The
198
- // old DELETE-cleanup is wrong for rename'ы — it can't undo a renamed
199
- // DSN-key / team-name / webhook URL. Snapshot first, restore after each
200
- // 2xx. POST falls back to DELETE-cleanup (correct semantics there).
201
- const isUpdate = m === "PUT" || m === "PATCH";
202
- const snapshot = isUpdate && !opts.noCleanup
203
- ? await snapshotOriginal(ep, allEndpoints, schemes, vars, opts)
204
- : null;
205
-
206
- // ── Baseline-OK gate ────────────────────────────────────────────────────
207
- // Eliminates the "5 × 404" output the markdown template produced in the
208
- // audit. If baseline isn't 2xx, attacks would just hit the same 4xx
209
- // wall and tell us nothing.
210
- const fullBaseline = await sendBaseline(ep, m, url, headers, baseline, opts);
211
- if (fullBaseline.kind === "network") {
212
- verdict.severity = "high";
213
- verdict.summary = `baseline network error: ${fullBaseline.reason}`;
214
- return verdict;
215
- }
216
- verdict.baseline = { status: fullBaseline.status };
217
-
218
- // ── Partial-body fallback (TASK-152) ──────────────────────────────────
219
- // common SaaS-style APIs accept partial PUT — full bodies
220
- // generated from spec get rejected (422 / 400). Walking each detected
221
- // field with a single-key body recovers the proven-HIGH cases that
222
- // otherwise fall into INCONCLUSIVE-BASELINE.
223
- let fullOk = fullBaseline.kind === "ok" && fullBaseline.status >= 200 && fullBaseline.status < 300;
224
- const perFieldBaseline = new Map<string, Record<string, unknown>>();
225
- if (!fullOk && isUpdate && fullBaseline.kind === "ok") {
226
- for (const hit of detected) {
227
- // Reuse spec value when present; otherwise fall back to the substituted
228
- // generator output for the field. Either way the partial body has
229
- // exactly one key, which is what partial-PUT APIs accept.
230
- const partial: Record<string, unknown> = {};
231
- if (hit.field in baseline) partial[hit.field] = baseline[hit.field];
232
- else partial[hit.field] = "";
233
- const partResp = await sendBaseline(ep, m, url, headers, partial, opts);
234
- if (partResp.kind === "ok" && partResp.status >= 200 && partResp.status < 300) {
235
- perFieldBaseline.set(hit.field, partial);
236
- }
237
- }
238
- if (perFieldBaseline.size > 0 && snapshot) {
239
- // Each successful partial baseline mutated only its single key; restore
240
- // exactly those before attacks start.
241
- await restoreOriginal(
242
- ep, snapshot, headers, schemes, vars, opts, verdict,
243
- perFieldBaseline.keys(),
244
- );
245
- }
246
- }
247
-
248
- if (!fullOk && perFieldBaseline.size === 0) {
249
- // fullBaseline.kind === "network" was already returned above; here it
250
- // must be "ok" with non-2xx status.
251
- const status = fullBaseline.kind === "ok" ? fullBaseline.status : 0;
252
- verdict.severity = "inconclusive-baseline";
253
- verdict.summary = isUpdate
254
- ? `baseline ${status} on full body; partial-body per-field also rejected — fixture/scope issue`
255
- : `baseline ${status} — endpoint unreachable or fixture invalid; skipping attacks`;
256
- return verdict;
257
- }
258
-
259
- // Cleanup state mutated by the (full) baseline, before issuing attacks.
260
- // With a snapshot → restore PUT (full baseline mutated every key).
261
- // Without snapshot → DELETE-counterpart (POST flow).
262
- if (fullOk && fullBaseline.kind === "ok" && !opts.noCleanup) {
263
- if (snapshot) {
264
- await restoreOriginal(
265
- ep, snapshot, headers, schemes, vars, opts, verdict,
266
- Object.keys(baseline),
267
- );
268
- } else {
269
- await tryCleanup(
270
- ep, allEndpoints, schemes, vars,
271
- fullBaseline.body, verdict, opts,
272
- );
273
- }
274
- }
275
-
276
- // ── Attacks ──────────────────────────────────────────────────────────────
277
- for (const hit of detected) {
278
- // Pick the body shape that this endpoint actually accepts.
279
- let baseBody: Record<string, unknown> | undefined;
280
- let mode: "full" | "partial" | "none" = "none";
281
- if (fullOk) {
282
- baseBody = baseline;
283
- mode = "full";
284
- } else if (perFieldBaseline.has(hit.field)) {
285
- baseBody = perFieldBaseline.get(hit.field)!;
286
- mode = "partial";
287
- }
288
-
289
- if (mode === "none" || !baseBody) {
290
- // Field doesn't have a usable baseline body shape — record one
291
- // INCONCLUSIVE per payload so the digest still exposes the field.
292
- for (const payload of PAYLOADS[hit.class]) {
293
- verdict.findings.push({
294
- field: hit.field,
295
- class: hit.class,
296
- payload,
297
- status: 0,
298
- echoed: false,
299
- severity: "inconclusive",
300
- reason: "no baseline body shape accepted (full+partial both rejected)",
301
- });
302
- }
303
- continue;
304
- }
305
-
306
- for (const payload of PAYLOADS[hit.class]) {
307
- const body = { ...baseBody, [hit.field]: payload };
308
- let resp;
309
- try {
310
- resp = await executeRequest(
311
- { method: m, url, headers, body: serializeProbeBody(ep, body).content },
312
- { timeout: opts.timeoutMs ?? 30000, retries: 0 },
313
- );
314
- } catch (err) {
315
- verdict.findings.push({
316
- field: hit.field,
317
- class: hit.class,
318
- payload,
319
- status: 0,
320
- echoed: false,
321
- severity: "inconclusive",
322
- reason: `network error: ${err instanceof Error ? err.message : String(err)}`,
323
- });
324
- continue;
325
- }
326
- const finding = classify(hit, payload, resp, { endpoint: ep });
327
- // Annotate which body shape was used for this attack — useful for
328
- // case-studies and emit-tests.
329
- finding.reason = mode === "partial"
330
- ? `${finding.reason} [partial-body]`
331
- : finding.reason;
332
- verdict.findings.push(finding);
333
-
334
- // Per-finding cleanup. Snapshot path takes precedence — DELETE on a
335
- // PUT-rename'd resource would wipe a live entity, restore-PUT puts
336
- // it back to the captured original. Only restore the single field
337
- // this attack mutated — sending a multi-key body trips
338
- // `422 use partial PUT` on common SaaS-shaped APIs.
339
- if (resp.status >= 200 && resp.status < 300 && !opts.noCleanup) {
340
- if (snapshot) {
341
- await restoreOriginal(
342
- ep, snapshot, headers, schemes, vars, opts, verdict,
343
- [hit.field],
344
- );
345
- } else {
346
- await tryCleanup(
347
- ep, allEndpoints, schemes, vars,
348
- resp.body_parsed ?? resp.body, verdict, opts,
349
- );
350
- }
351
- }
352
- }
353
- }
354
-
355
- verdict.severity = rollupSecuritySeverity(verdict.findings);
356
- verdict.summary = summaryLine(verdict);
357
- return verdict;
358
- }
359
-
360
- /**
361
- * Roll up per-finding severities to the worst verdict-level severity.
362
- * ARV-253: "info" sits below "low" (single_signal sanitization-only).
363
- * ARV-254: "medium" sits between "high" and "low" (SSRF accept on an
364
- * endpoint declaring delivery). Exported so callers can recompute the
365
- * verdict rollup after adjusting individual finding severities.
366
- */
367
- export function rollupSecuritySeverity(
368
- findings: readonly { severity: SecuritySeverity }[],
369
- ): SecuritySeverity {
370
- const severities = findings.map((f) => f.severity);
371
- if (severities.includes("high")) return "high";
372
- if (severities.includes("inconclusive")) return "inconclusive";
373
- if (severities.includes("medium")) return "medium";
374
- if (severities.includes("low")) return "low";
375
- if (severities.includes("info")) return "info";
376
- return "ok";
377
- }
378
-
379
- function summaryLine(v: SecurityVerdict): string {
380
- const counts: Record<SecuritySeverity, number> = {
381
- high: 0, medium: 0, low: 0, info: 0, inconclusive: 0, "inconclusive-baseline": 0, ok: 0, skipped: 0,
382
- };
383
- for (const f of v.findings) counts[f.severity]++;
384
- const fields = Array.from(new Set(v.detectedFields.map(d => d.field))).join(", ");
385
- return `fields=[${fields}] · HIGH=${counts.high} MED=${counts.medium} LOW=${counts.low} INFO=${counts.info} INCONCLUSIVE=${counts.inconclusive} OK=${counts.ok}`;
386
- }
387
-
388
- function skipped(ep: EndpointInfo, reason: string): SecurityVerdict {
389
- return {
390
- method: ep.method.toUpperCase(),
391
- path: ep.path,
392
- severity: "skipped",
393
- summary: `skipped: ${reason}`,
394
- detectedFields: [],
395
- findings: [],
396
- skipReason: reason,
397
- };
398
- }
@@ -1,103 +0,0 @@
1
- import type { EndpointInfo, SecuritySchemeInfo } from "../../generator/types.ts";
2
- import { generateFromSchema } from "../../generator/data-factory.ts";
3
- import type { RawStep, RawSuite } from "../../generator/serializer.ts";
4
- import { flattenToFormFields } from "../../runner/form-encode.ts";
5
- import {
6
- captureFieldFor,
7
- convertPath,
8
- endpointStem,
9
- findDeleteCounterpart,
10
- getAuthHeaders,
11
- } from "../shared.ts";
12
- import type { SecurityProbeResult } from "./types.ts";
13
-
14
- const ATTACK_EXPECTED_STATUS = [400, 403, 404, 405, 409, 415, 422];
15
-
16
- function shortPayload(s: string): string {
17
- return s.length > 40 ? s.slice(0, 37) + "…" : s;
18
- }
19
-
20
- export function emitSecurityRegressionSuites(
21
- result: SecurityProbeResult,
22
- endpoints: EndpointInfo[],
23
- schemes: SecuritySchemeInfo[],
24
- ): RawSuite[] {
25
- const suites: RawSuite[] = [];
26
- for (const v of result.verdicts) {
27
- // ARV-247 (R-04/F18): `high` findings are 2xx-with-echoed-payload — the
28
- // strongest regression signal we have. Skipping them meant CI had nothing
29
- // to gate on for confirmed stored injections. Treat them like `ok` here:
30
- // the suite locks in the *expected* state (attack rejected) once the API
31
- // owner ships a fix, and fails loud while it's still broken.
32
- if (v.severity !== "ok" && v.severity !== "low" && v.severity !== "high") continue;
33
- const ep = endpoints.find(
34
- e => e.path === v.path && e.method.toUpperCase() === v.method,
35
- );
36
- if (!ep) continue;
37
- const suiteHeaders = getAuthHeaders(ep, schemes);
38
- const tests: RawStep[] = [];
39
- for (const f of v.findings) {
40
- // `ok` = attack already rejected; `high` = attack accepted+echoed (the
41
- // regression target is rejection, same expected set as `ok`). `low` =
42
- // attack accepted but no echo — lock in the 2xx-without-echo shape.
43
- const expected = (f.severity === "ok" || f.severity === "high") ? ATTACK_EXPECTED_STATUS : [200, 201, 202, 204];
44
- const body = ep.requestBodySchema ? generateFromSchema(ep.requestBodySchema) : {};
45
- if (typeof body === "object" && body !== null && !Array.isArray(body)) {
46
- (body as Record<string, unknown>)[f.field] = f.payload;
47
- }
48
- // ARV-161: mirror mass-assignment-probe — emit `form:` instead of
49
- // `json:` for form-urlencoded endpoints (Stripe v1), otherwise the
50
- // regression suite gets rejected with 400 "check that your POST
51
- // content type is application/x-www-form-urlencoded" and never
52
- // exercises the actual attack vector.
53
- const bodyField =
54
- ep.requestBodyContentType === "application/x-www-form-urlencoded"
55
- ? { form: flattenToFormFields(body) }
56
- : { json: body };
57
- const step: RawStep = {
58
- name: `${f.class}: ${f.field}=${shortPayload(f.payload)} must ${f.severity === "ok" ? "be rejected" : "not echo"}`,
59
- source: {
60
- generator: "probe-security",
61
- endpoint: `${v.method} ${v.path}`,
62
- response_branch: expected.map(String).join("|"),
63
- },
64
- [v.method]: convertPath(ep.path),
65
- ...bodyField,
66
- expect: { status: expected },
67
- };
68
- tests.push(step);
69
- }
70
- if (tests.length === 0) continue;
71
- // Attach a generic cleanup step keyed off `created_id` (only fires when
72
- // a previous step captured one — same `always:true` semantics other
73
- // probes use).
74
- const delEp = findDeleteCounterpart(ep, endpoints);
75
- if (delEp) {
76
- const idField = captureFieldFor(ep);
77
- tests[0]!.expect.body = { ...(tests[0]!.expect.body ?? {}), [idField]: { capture: "created_id" } };
78
- const idParam = (delEp.path.match(/\{([^}]+)\}/) ?? [])[1] ?? "id";
79
- const delStep: RawStep = {
80
- name: "cleanup",
81
- source: { generator: "probe-security-cleanup", endpoint: `DELETE ${delEp.path}` },
82
- always: true,
83
- DELETE: convertPath(delEp.path).replace(`{{${idParam}}}`, "{{created_id}}"),
84
- expect: { status: [200, 202, 204, 404] },
85
- } as RawStep & { always: boolean };
86
- tests.push(delStep);
87
- }
88
- suites.push({
89
- name: `probe-security ${v.method} ${v.path}`,
90
- tags: ["probe-security", ...result.classes],
91
- source: {
92
- type: "probe-suite",
93
- generator: "probe-security",
94
- endpoint: `${v.method} ${v.path}`,
95
- },
96
- fileStem: `probe-security-${endpointStem(ep)}`,
97
- base_url: "{{base_url}}",
98
- ...(suiteHeaders ? { headers: suiteHeaders } : {}),
99
- tests,
100
- });
101
- }
102
- return suites;
103
- }
@@ -1,151 +0,0 @@
1
- import type { EndpointInfo, SecuritySchemeInfo } from "../../generator/types.ts";
2
- import type { SeedBodyConfig } from "../../generator/resources-builder.ts";
3
- import type { RecommendedAction } from "../../diagnostics/failure-hints.ts";
4
-
5
- export type SecurityClass = "ssrf" | "crlf" | "open-redirect";
6
-
7
- export const SECURITY_CLASSES: SecurityClass[] = ["ssrf", "crlf", "open-redirect"];
8
-
9
- /**
10
- * Security-probe severity ladder. Includes 'info' (ARV-253) for
11
- * sanitization-only signals (CRLF accept-without-reflection) and
12
- * 'medium' (ARV-254) for SSRF accept on endpoints declaring delivery
13
- * semantics. The full m-21 matrix governs the cap: HIGH requires
14
- * evidence_chain proof, OOB-backed SSRF lands here only when ARV-177
15
- * lifts.
16
- */
17
- export type SecuritySeverity =
18
- | "high"
19
- | "medium"
20
- | "low"
21
- | "info"
22
- | "inconclusive"
23
- | "inconclusive-baseline"
24
- | "ok"
25
- | "skipped";
26
-
27
- export interface SecurityFieldHit {
28
- /** Field name in the request body. */
29
- field: string;
30
- /** Class that triggered (a field can hit multiple — we record all). */
31
- class: SecurityClass;
32
- }
33
-
34
- export interface SecurityFinding {
35
- field: string;
36
- class: SecurityClass;
37
- payload: string;
38
- /** Raw HTTP status of the attack request. */
39
- status: number;
40
- /** Whether the response body echoes the payload (suggesting stored injection). */
41
- echoed: boolean;
42
- /** PASS / FAIL classification per finding. */
43
- severity: SecuritySeverity;
44
- reason: string;
45
- /** TASK-294: agent-routable action. FAIL/WARN → `report_backend_bug`;
46
- * PASS → undefined (no action needed). */
47
- recommended_action?: RecommendedAction;
48
- /** Reserved suppression trace (audit-trail). Currently unset — the
49
- * agent triages severity from the raw finding. */
50
- suppressed_by?: { source: string; rule_index: number; reason: string };
51
- }
52
-
53
- export interface SecurityVerdict {
54
- method: string;
55
- path: string;
56
- /** Most-severe finding wins. */
57
- severity: SecuritySeverity;
58
- summary: string;
59
- /** Field hits detected on this endpoint (some may have produced no findings). */
60
- detectedFields: SecurityFieldHit[];
61
- /** All attempted attacks. Empty for SKIPPED endpoints. */
62
- findings: SecurityFinding[];
63
- baseline?: { status: number };
64
- cleanup?: {
65
- attempted: boolean;
66
- status?: number;
67
- error?: string;
68
- /** TASK-278: created resource id (slug/uuid/...) so `zond cleanup --orphans`
69
- * can retry DELETE without re-running the probe. */
70
- id?: string | number;
71
- /** TASK-278: concrete DELETE URL path with the id substituted. */
72
- deletePath?: string;
73
- };
74
- skipReason?: string;
75
- }
76
-
77
- export interface SecurityProbeOptions {
78
- endpoints: EndpointInfo[];
79
- securitySchemes: SecuritySchemeInfo[];
80
- vars: Record<string, string>;
81
- classes: SecurityClass[];
82
- noCleanup?: boolean;
83
- timeoutMs?: number;
84
- /** When true, only print which endpoints/fields would be attacked. */
85
- dryRun?: boolean;
86
- /**
87
- * DELETE-cleanup retry delays in ms (round-5: handles eventual
88
- * consistency between write replica and read replica). Default
89
- * `[200, 1000]` — two retries on 404, total worst-case ~1.2s. Tests
90
- * pass `[]` to disable; ops can pass longer for laggier replicas.
91
- */
92
- cleanupRetryDelaysMs?: number[];
93
- /** TASK-264: when true, refuse to attack PUT/PATCH/DELETE endpoints whose
94
- * path-params are filled from `.env.yaml` (a.k.a. seeded fixtures). The
95
- * trade-off: lower coverage (those endpoints get SKIPPED), but a
96
- * guaranteed «probe doesn't mutate fixtures the user spent time
97
- * bootstrapping» property. POST endpoints still run — they create their
98
- * own resources, so isolation is automatic, with cleanup falling back to
99
- * the existing DELETE-counterpart + orphan-tracker flow (TASK-278). */
100
- isolated?: boolean;
101
- /** ARV-140: opt-in to attacks that have no cleanup path (POSTs without a
102
- * DELETE counterpart). By default we now skip them — round-01/02 Sentry
103
- * runs left ~18 manually-cleanable orphans in prod because the probe
104
- * happily POSTed to `/teams/`, `/symbol-sources/`, etc., where the spec
105
- * has no DELETE. The pre-flight feasibility map drops these unless the
106
- * caller explicitly accepts the leak. */
107
- allowLeaks?: boolean;
108
- /** ARV-269: agent-authored `seed_body` overlays from `.api-resources.local.yaml`,
109
- * keyed by `"METHOD /path"`. Wins over `generateFromSchema` when the
110
- * endpoint matches — see `MassAssignmentOptions.seedBodies` for the
111
- * rationale (strict APIs reject random-scalar baselines). */
112
- seedBodies?: Map<string, SeedBodyConfig>;
113
- }
114
-
115
- /** ARV-140: cleanup-feasibility map. Built once before the live loop so
116
- * every POST verdict can see whether the spec has a DELETE counterpart;
117
- * the summary digest also reports counts for skipped/forced endpoints.
118
- *
119
- * ARV-153 extends the status enum with "action": POSTs whose last path
120
- * segment is a known action verb (`/capture`, `/verify`, `/cancel`, …)
121
- * operate on an existing resource and never allocate a new one, so a
122
- * DELETE counterpart isn't meaningful. These are attacked the same way
123
- * as POSTs with a real DELETE — without `--allow-leaks` — because there
124
- * is no resource to leak. */
125
- export interface CleanupFeasibility {
126
- status: Record<string, "has-delete" | "no-delete-counterpart" | "action">;
127
- skippedNoCleanup: number;
128
- forcedNoCleanup: number;
129
- /** ARV-153: POSTs we attacked even though no DELETE counterpart exists,
130
- * because the operation is semantically an action (no resource created). */
131
- actionNoCleanupNeeded: number;
132
- }
133
-
134
- export interface SecurityProbeResult {
135
- classes: SecurityClass[];
136
- totalEndpoints: number;
137
- specProbed: number;
138
- verdicts: SecurityVerdict[];
139
- warnings: string[];
140
- /** ARV-140: cleanup-feasibility digest (POSTs without DELETE counterpart). */
141
- cleanupFeasibility?: CleanupFeasibility;
142
- }
143
-
144
- /** Internal: per-step options shared between orchestrator/baseline/cleanup. */
145
- export interface ProbeStepOpts {
146
- noCleanup: boolean;
147
- timeoutMs?: number;
148
- cleanupRetryDelaysMs?: number[];
149
- /** ARV-269: optional `seed_body` overlay for this endpoint. */
150
- seedBody?: SeedBodyConfig;
151
- }