@kirrosh/zond 0.23.0 → 0.26.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 (158) hide show
  1. package/CHANGELOG.md +164 -1
  2. package/README.md +8 -7
  3. package/package.json +2 -3
  4. package/src/CLAUDE.md +112 -0
  5. package/src/cli/commands/add-api.ts +19 -7
  6. package/src/cli/commands/api/annotate/index.ts +359 -4
  7. package/src/cli/commands/api/annotate/lifecycle.ts +1 -1
  8. package/src/cli/commands/api/annotate/overlay.ts +1 -1
  9. package/src/cli/commands/api/annotate/pagination.ts +10 -6
  10. package/src/cli/commands/api/annotate/prompts.ts +39 -2
  11. package/src/cli/commands/audit.ts +360 -54
  12. package/src/cli/commands/check.ts +15 -2
  13. package/src/cli/commands/checks.ts +352 -36
  14. package/src/cli/commands/cleanup.ts +4 -30
  15. package/src/cli/commands/coverage.ts +275 -57
  16. package/src/cli/commands/db.ts +311 -8
  17. package/src/cli/commands/discover.ts +281 -161
  18. package/src/cli/commands/doctor.ts +57 -3
  19. package/src/cli/commands/fixtures.ts +1 -1
  20. package/src/cli/commands/generate.ts +24 -7
  21. package/src/cli/commands/init/bootstrap.ts +4 -1
  22. package/src/cli/commands/init/index.ts +2 -2
  23. package/src/cli/commands/init/skills.ts +43 -0
  24. package/src/cli/commands/init/templates/agents.md +12 -3
  25. package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
  26. package/src/cli/commands/init/templates/skills/zond-checks.md +268 -44
  27. package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
  28. package/src/cli/commands/init/templates/skills/zond-triage.md +88 -26
  29. package/src/cli/commands/init/templates/skills/zond.md +274 -64
  30. package/src/cli/commands/init/templates/zond-config.yml +1 -1
  31. package/src/cli/commands/prepare-fixtures.ts +14 -52
  32. package/src/cli/commands/probe/_seed-bodies.ts +52 -0
  33. package/src/cli/commands/probe/mass-assignment.ts +101 -10
  34. package/src/cli/commands/probe/security.ts +95 -12
  35. package/src/cli/commands/probe/webhooks.ts +2 -0
  36. package/src/cli/commands/probe.ts +87 -11
  37. package/src/cli/commands/refresh-api.ts +59 -1
  38. package/src/cli/commands/report-bundle.ts +3 -11
  39. package/src/cli/commands/request.ts +116 -0
  40. package/src/cli/commands/run.ts +33 -5
  41. package/src/cli/commands/schema-from-runs.ts +128 -0
  42. package/src/cli/commands/secrets.ts +133 -0
  43. package/src/cli/json-envelope.ts +0 -20
  44. package/src/cli/json-schemas.ts +51 -0
  45. package/src/cli/output.ts +17 -1
  46. package/src/cli/program.ts +5 -4
  47. package/src/cli/safe-live.ts +24 -0
  48. package/src/cli/status-filter.ts +0 -10
  49. package/src/core/audit/persist.ts +183 -0
  50. package/src/core/checks/budget.ts +59 -0
  51. package/src/core/checks/checks/cross_call_references.ts +17 -4
  52. package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
  53. package/src/core/checks/checks/idempotency_replay.ts +1 -5
  54. package/src/core/checks/checks/ignored_auth.ts +44 -1
  55. package/src/core/checks/checks/index.ts +3 -0
  56. package/src/core/checks/checks/lifecycle_transitions.ts +169 -26
  57. package/src/core/checks/checks/negative_data_rejection.ts +119 -16
  58. package/src/core/checks/checks/not_a_server_error.ts +8 -0
  59. package/src/core/checks/checks/open_cors_on_sensitive.ts +47 -18
  60. package/src/core/checks/checks/pagination_invariants.ts +298 -117
  61. package/src/core/checks/checks/positive_data_acceptance.ts +1 -4
  62. package/src/core/checks/checks/status_code_conformance.ts +78 -7
  63. package/src/core/checks/mode.ts +3 -0
  64. package/src/core/checks/recommended-action.ts +5 -1
  65. package/src/core/checks/runner.ts +614 -27
  66. package/src/core/checks/spec-findings.ts +308 -0
  67. package/src/core/checks/types.ts +117 -1
  68. package/src/core/checks/zond-extensions.ts +73 -0
  69. package/src/core/classifier/recommended-action.ts +35 -6
  70. package/src/core/coverage/loader.ts +31 -0
  71. package/src/core/diagnostics/db-analysis.ts +200 -106
  72. package/src/core/diagnostics/failure-class.ts +21 -1
  73. package/src/core/diagnostics/failure-hints.ts +4 -208
  74. package/src/core/diagnostics/suggested-fixes.ts +2 -3
  75. package/src/core/generator/chunker.ts +1 -8
  76. package/src/core/generator/data-factory.ts +199 -61
  77. package/src/core/generator/fixtures-builder.ts +38 -31
  78. package/src/core/generator/index.ts +0 -2
  79. package/src/core/generator/openapi-reader.ts +98 -4
  80. package/src/core/generator/path-param-disambig.ts +30 -4
  81. package/src/core/generator/resources-builder.ts +276 -26
  82. package/src/core/generator/schema-utils.ts +22 -0
  83. package/src/core/generator/suite-generator.ts +168 -15
  84. package/src/core/generator/types.ts +6 -0
  85. package/src/core/identity/identity-file.ts +0 -0
  86. package/src/core/output/README.md +11 -29
  87. package/src/core/output/index.ts +1 -1
  88. package/src/core/output/run.ts +0 -35
  89. package/src/core/output/types.ts +0 -7
  90. package/src/core/parser/dynamic-values.ts +160 -0
  91. package/src/core/parser/variables.ts +0 -0
  92. package/src/core/probe/dry-run-envelope.ts +4 -0
  93. package/src/core/probe/mass-assignment/classify.ts +175 -0
  94. package/src/core/probe/mass-assignment/cleanup.ts +52 -0
  95. package/src/core/probe/mass-assignment/digest.ts +114 -0
  96. package/src/core/probe/mass-assignment/orchestrator.ts +459 -0
  97. package/src/core/probe/mass-assignment/regression.ts +141 -0
  98. package/src/core/probe/mass-assignment/suspects.ts +92 -0
  99. package/src/core/probe/mass-assignment/types.ts +135 -0
  100. package/src/core/probe/mass-assignment-probe.ts +23 -1118
  101. package/src/core/probe/mass-assignment-template.ts +32 -4
  102. package/src/core/probe/path-discovery.ts +3 -4
  103. package/src/core/probe/probe-harness.ts +21 -22
  104. package/src/core/probe/security/baseline.ts +174 -0
  105. package/src/core/probe/security/classify.ts +341 -0
  106. package/src/core/probe/security/cleanup.ts +125 -0
  107. package/src/core/probe/security/detectors.ts +71 -0
  108. package/src/core/probe/security/digest.ts +104 -0
  109. package/src/core/probe/security/orchestrator.ts +398 -0
  110. package/src/core/probe/security/regression.ts +103 -0
  111. package/src/core/probe/security/types.ts +151 -0
  112. package/src/core/probe/security-probe-class.ts +8 -2
  113. package/src/core/probe/security-probe.ts +28 -1449
  114. package/src/core/probe/shared.ts +26 -0
  115. package/src/core/probe/webhooks-probe.ts +5 -7
  116. package/src/core/runner/assertions.ts +1 -1
  117. package/src/core/runner/executor.ts +3 -18
  118. package/src/core/runner/form-encode.ts +8 -18
  119. package/src/core/runner/http-client.ts +38 -1
  120. package/src/core/runner/preflight-vars.ts +19 -15
  121. package/src/core/runner/rate-limiter.ts +11 -29
  122. package/src/core/runner/run-kind.ts +7 -1
  123. package/src/core/runner/schema-validator.ts +2 -6
  124. package/src/core/runner/send-request.ts +11 -6
  125. package/src/core/runner/types.ts +6 -0
  126. package/src/core/setup-api.ts +53 -15
  127. package/src/core/severity/index.ts +0 -63
  128. package/src/core/spec/infer-schema.ts +102 -0
  129. package/src/core/spec/merge-specs.ts +156 -0
  130. package/src/core/spec/schema-from-runs.ts +117 -0
  131. package/src/core/spec/schema-overlay.ts +130 -0
  132. package/src/core/util/ajv.ts +13 -0
  133. package/src/core/util/headers.ts +9 -0
  134. package/src/core/util/url.ts +24 -0
  135. package/src/core/workspace/fixture-gap-report.ts +84 -0
  136. package/src/core/workspace/fixture-gaps.ts +71 -0
  137. package/src/core/workspace/root.ts +13 -11
  138. package/src/db/migrate.ts +2 -0
  139. package/src/db/migrations/0002_run_kind_request.sql +59 -0
  140. package/src/db/queries/collections.ts +2 -2
  141. package/src/db/queries/results.ts +88 -0
  142. package/src/db/queries/runs.ts +56 -2
  143. package/src/db/queries.ts +3 -0
  144. package/src/db/schema.ts +7 -7
  145. package/src/cli/commands/bootstrap.ts +0 -710
  146. package/src/core/anti-fp/bootstrap.ts +0 -34
  147. package/src/core/anti-fp/index.ts +0 -33
  148. package/src/core/anti-fp/registry.ts +0 -44
  149. package/src/core/anti-fp/rules/baseline-echo.ts +0 -74
  150. package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +0 -52
  151. package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +0 -38
  152. package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +0 -35
  153. package/src/core/anti-fp/rules/schemathesis/index.ts +0 -24
  154. package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +0 -53
  155. package/src/core/anti-fp/rules/subscription-gated/index.ts +0 -11
  156. package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +0 -75
  157. package/src/core/anti-fp/types.ts +0 -68
  158. package/src/core/generator/create-body.ts +0 -89
@@ -3,13 +3,39 @@
3
3
  * The runner builds a single-site negative case (one mutation against
4
4
  * a valid body, see `_negative_mutator.ts`); if the server still
5
5
  * accepts it (status outside 4xx/5xx + 401/403/404 admin set), we
6
- * raise a finding *unless* an anti-FP guard fires (see `_anti_fp.ts`).
6
+ * raise a finding with raw evidence. The agent triages / re-severitizes.
7
7
  *
8
8
  * Default expected: 400 / 401 / 403 / 404 / 422 / 428 / 5xx.
9
9
  * 2xx and 3xx with our payload are findings.
10
+ *
11
+ * Severity matrix (ARV-284, dispatched per finding via outcome.severity
12
+ * — see `severityForEvidence` below):
13
+ *
14
+ * - MEDIUM: concrete schema breach silently accepted (maxLength+1,
15
+ * type-mismatch, pattern-violation, format-invalid,
16
+ * drop-required, uniqueItems-violation, drop-required-query).
17
+ * Server should reject; evidence single-signal but breach
18
+ * is concrete.
19
+ * - LOW: `additionalProperties-violation` (unknown body fields
20
+ * silently dropped is documented forward-compat in many
21
+ * APIs — Stripe by design) and `wrong-type` query params
22
+ * on GET list endpoints (often "invalid id → empty
23
+ * result" documented behaviour). Single-signal, ambiguous
24
+ * intent.
25
+ *
26
+ * Note: 5xx on a negative mutation does NOT escalate this check —
27
+ * `not_a_server_error` (separate check, severity HIGH by design) owns
28
+ * the 5xx signal, and `ACCEPTABLE()` below treats 5xx as a non-silent
29
+ * accept so this check passes for those cases. Avoids double-counting.
30
+ *
31
+ * Per ARV-250's proof-cap principle (no evidence → no high severity):
32
+ * single-signal proof caps at LOW; concrete schema breach escalates to
33
+ * MEDIUM. The declared `severity: "low"` is the natural fallback /
34
+ * proof-cap baseline; stronger findings use `outcome.severity` to
35
+ * override. The agent re-severitizes from the raw evidence.
10
36
  */
11
- import type { Check } from "../types.ts";
12
- import { applyAntiFp } from "../../anti-fp/index.ts";
37
+ import type { Check, CheckOutcome } from "../types.ts";
38
+ import type { Severity } from "../../severity/index.ts";
13
39
 
14
40
  const ACCEPTABLE = (status: number): boolean => {
15
41
  if (status === 400 || status === 401 || status === 403 || status === 404 || status === 422 || status === 428) return true;
@@ -17,29 +43,106 @@ const ACCEPTABLE = (status: number): boolean => {
17
43
  return false;
18
44
  };
19
45
 
46
+ /** Body-boundary labels we treat as LOW (often by-design vendor
47
+ * behaviour, single-signal, ambiguous intent). All other boundary
48
+ * labels emitted by `coverage-phase.ts:enumerateBoundaryCases` —
49
+ * maxLength+1, pattern-violation, uuid-invalid, drop-required:X, etc.
50
+ * — are concrete schema breaches and stay MEDIUM. */
51
+ const LOW_BODY_BOUNDARIES: ReadonlySet<string> = new Set([
52
+ "additionalProperties-violation",
53
+ ]);
54
+
55
+ /** Param-scenario labels (`coverage-phase.ts:enumerateParamBoundaryCases`)
56
+ * we treat as LOW. `wrong-type` is the classic "Stripe returns empty
57
+ * list for invalid id" pattern — many APIs document this; flagging
58
+ * HIGH/MEDIUM produces noise. `drop-required-query` stays MEDIUM (a
59
+ * declared-required param being silently optional is a concrete
60
+ * contract gap, not vendor convention). */
61
+ const LOW_PARAM_SCENARIOS_ON_GET: ReadonlySet<string> = new Set([
62
+ "wrong-type",
63
+ ]);
64
+
65
+ interface MutationMeta {
66
+ mutation?: string;
67
+ boundary?: string;
68
+ param_scenario?: string;
69
+ param_location?: string;
70
+ }
71
+
72
+ function severityForEvidence(
73
+ meta: MutationMeta | undefined,
74
+ method: string,
75
+ ): Severity {
76
+ if (!meta) return "low";
77
+
78
+ // Param-side mutations (query / path wrong-type, drop-required-query).
79
+ if (meta.mutation === "param-boundary") {
80
+ const scenario = meta.param_scenario ?? "";
81
+ if (
82
+ meta.param_location === "query"
83
+ && method.toUpperCase() === "GET"
84
+ && LOW_PARAM_SCENARIOS_ON_GET.has(scenario)
85
+ ) {
86
+ return "low";
87
+ }
88
+ // drop-required-query, wrong-type on non-GET, wrong-type on path
89
+ // params: concrete contract violations server should reject.
90
+ return "medium";
91
+ }
92
+
93
+ // Body-boundary mutations (additionalProperties, maxLength+1, etc).
94
+ if (meta.mutation === "boundary") {
95
+ const boundary = meta.boundary ?? "";
96
+ if (LOW_BODY_BOUNDARIES.has(boundary)) return "low";
97
+ return "medium";
98
+ }
99
+
100
+ // Unknown mutation kind — single-signal fallback.
101
+ return "low";
102
+ }
103
+
20
104
  export const negativeDataRejection: Check = {
21
105
  id: "negative_data_rejection",
22
- severity: "high",
106
+ // ARV-284: declared severity is the *natural* tier (proof-cap baseline
107
+ // per ARV-250 — single-signal caps at LOW). Per-finding severity is
108
+ // dispatched via `outcome.severity` in `run()` below, so summary
109
+ // tables can show HIGH for 5xx and MEDIUM for concrete schema breach
110
+ // without globally setting the check to HIGH (which masks calibration).
111
+ severity: "low",
23
112
  defaultExpected: "Server must reject invalid bodies with 400/401/403/404/422/428 (or 5xx)",
24
113
  references: [{ id: "OWASP-API-08" }],
25
114
  caseKinds: ["negative_data"],
26
115
  applies: (op) => Boolean(op.requestBodySchema),
27
- run({ case: c, response }) {
28
- if (ACCEPTABLE(response.status)) return { kind: "pass" };
29
- const skip = applyAntiFp(c, "check:negative_data_rejection");
30
- if (skip) {
31
- return { kind: "skip", reason: `${skip.ruleId}: ${skip.reason}` };
116
+ run({ case: c, response }): CheckOutcome {
117
+ const meta = c.meta as MutationMeta | undefined;
118
+ const method = c.operation.method.toUpperCase();
119
+ const bodyless = method === "GET" || method === "HEAD" || method === "DELETE";
120
+ // ARV-345 (scope fix, like ARV-340): don't manufacture "accepted an invalid
121
+ // body" findings on methods with no request-body semantics. A GET/HEAD/DELETE
122
+ // that 2xx's a mutated body simply ignored the body — correct, not a gap.
123
+ if (bodyless && meta?.mutation === "boundary") {
124
+ return { kind: "skip", reason: "body mutation not applicable on GET/HEAD/DELETE (no request-body semantics)" };
125
+ }
126
+ // ARV-345: an unknown / wrong-type QUERY param a GET tolerates (2xx) is
127
+ // documented leniency ("invalid id → empty list"), not a rejectable invalid.
128
+ // drop-required-query stays in scope — a declared-required param silently
129
+ // optional IS a concrete contract gap.
130
+ if (
131
+ bodyless
132
+ && meta?.mutation === "param-boundary"
133
+ && meta?.param_location === "query"
134
+ && LOW_PARAM_SCENARIOS_ON_GET.has(meta?.param_scenario ?? "")
135
+ ) {
136
+ return { kind: "skip", reason: "unknown/wrong-type query param on GET is documented leniency, not a validation gap" };
32
137
  }
138
+ if (ACCEPTABLE(response.status)) return { kind: "pass" };
33
139
  return {
34
140
  kind: "fail",
35
141
  message: `Server accepted an invalid body (status ${response.status}) — single-site mutation: ${
36
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
- (c.meta as any)?.mutation
38
- } @ ${
39
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
- (c.meta as any)?.field_path
41
- }`,
42
- evidence: { status: response.status, mutation: c.meta },
142
+ meta?.mutation ?? "unknown"
143
+ } @ ${meta?.boundary ?? meta?.param_scenario ?? "?"}`,
144
+ evidence: { status: response.status, mutation: meta },
145
+ severity: severityForEvidence(meta, c.operation.method),
43
146
  };
44
147
  },
45
148
  };
@@ -13,6 +13,14 @@ export const notAServerError: Check = {
13
13
  references: [
14
14
  { id: "RFC-9110-15.6", url: "https://www.rfc-editor.org/rfc/rfc9110#name-server-error-5xx" },
15
15
  ],
16
+ // ARV-340: evaluate malformed-input cases too, not just the positive
17
+ // baseline. A negative_data / missing-header request is still a
18
+ // well-formed HTTP request the server must handle with a 4xx — a 5xx
19
+ // there is an unhandled crash (live Stripe: GET /v1/billing/alerts 500
20
+ // on a bad query param slipped through when this defaulted to positive
21
+ // only). `unsupported_method` is excluded on purpose: 501/405 to an
22
+ // undeclared method is legitimate, not a server error.
23
+ caseKinds: ["positive", "negative_data", "missing_required_header"],
16
24
  applies: () => true,
17
25
  run({ response }) {
18
26
  if (response.status >= 500 && response.status < 600) {
@@ -99,30 +99,59 @@ export const openCorsOnSensitive: AuthStatefulCheck = {
99
99
  const originIsStar = allowOrigin === "*";
100
100
  const originReflects = allowOrigin === PROBE_ORIGIN;
101
101
 
102
- // The smoking guns.
103
- if (credsTrue && originIsStar) {
104
- return {
105
- kind: "fail",
106
- message:
107
- "CORS misconfiguration: Allow-Origin: * with Allow-Credentials: true allows cross-origin reads of authenticated data",
108
- evidence: {
109
- request_origin: PROBE_ORIGIN,
110
- access_control_allow_origin: allowOrigin,
111
- access_control_allow_credentials: allowCreds,
112
- variant: "wildcard+credentials",
113
- },
114
- };
115
- }
116
- if (credsTrue && originReflects) {
102
+ // HIGH requires two independent pieces of evidence that authed data is
103
+ // actually CORS-readable by an attacker site:
104
+ //
105
+ // ARV-312: a 2xx response served real content under the reflected
106
+ // Origin. On a non-2xx (401/403/4xx/5xx) *this* response exposed no
107
+ // authenticated payload. Recording the real status also kills the
108
+ // phantom `response_summary.status: 0` (auth checks don't otherwise
109
+ // thread their response back to the runner).
110
+ //
111
+ // ARV-316: an AMBIENT credential the browser auto-attaches cross-origin
112
+ // — i.e. a cookie (apiKey-in-cookie scheme, or a Set-Cookie observed
113
+ // on the probe). `Allow-Credentials: true` is only exploitable with
114
+ // one: for bearer/header/oauth2 token auth the attacker's page can't
115
+ // set the victim's Authorization header, so a reflected Origin is a
116
+ // hygiene nit, not a data leak (Stripe et al are bearer-auth).
117
+ //
118
+ // Without both, cap at LOW per the m-21 "no evidence → no high" matrix.
119
+ const status = resp.status;
120
+ const isTwoXx = status >= 200 && status < 300;
121
+ const schemes = (h.doc?.components?.securitySchemes ?? {}) as Record<
122
+ string,
123
+ OpenAPIV3.SecuritySchemeObject | OpenAPIV3.ReferenceObject
124
+ >;
125
+ const usesCookieAuth = op.security.some((name) => {
126
+ const s = schemes[name];
127
+ return !!s && !("$ref" in s) && s.type === "apiKey" && s.in === "cookie";
128
+ });
129
+ const setCookie = getHeader(resp.headers, "set-cookie");
130
+ const ambientCredential = usesCookieAuth || setCookie !== undefined;
131
+ const authExposed = isTwoXx && ambientCredential;
132
+ const impactNote = authExposed
133
+ ? "any attacker site can read authed cross-origin responses"
134
+ : !isTwoXx
135
+ ? `observed on a ${status} response — cross-origin CORS is misconfigured but authed-data exposure is unproven`
136
+ : "no ambient (cookie) credential — bearer/token auth isn't auto-attached cross-origin, so the reflection isn't exploitable as a data leak";
137
+
138
+ if (credsTrue && (originIsStar || originReflects)) {
139
+ const variant = originIsStar ? "wildcard+credentials" : "reflected+credentials";
140
+ const shape = originIsStar
141
+ ? "Allow-Origin: * with Allow-Credentials: true"
142
+ : "response reflects arbitrary Origin with Allow-Credentials: true";
117
143
  return {
118
144
  kind: "fail",
119
- message:
120
- "CORS misconfiguration: response reflects arbitrary Origin with Allow-Credentials: true — any attacker site can read authed responses",
145
+ severity: authExposed ? "high" : "low",
146
+ responseStatus: status,
147
+ message: `CORS misconfiguration: ${shape} — ${impactNote}`,
121
148
  evidence: {
122
149
  request_origin: PROBE_ORIGIN,
150
+ response_status: status,
123
151
  access_control_allow_origin: allowOrigin,
124
152
  access_control_allow_credentials: allowCreds,
125
- variant: "reflected+credentials",
153
+ ambient_credential: ambientCredential,
154
+ variant,
126
155
  },
127
156
  };
128
157
  }