@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
@@ -1,34 +0,0 @@
1
- /**
2
- * ARV-124: register every shipped anti-FP rule.
3
- *
4
- * Mirrors `core/probe/bootstrap.ts` — called once at CLI startup so
5
- * checks/probes can rely on the registry being populated. Idempotent:
6
- * repeated calls are no-ops. Tests should pair `resetAntiFpBootstrap()`
7
- * with `reset()` from the registry to start from a clean slate.
8
- */
9
- import { register, reset } from "./registry.ts";
10
- import { SCHEMATHESIS_RULES } from "./rules/schemathesis/index.ts";
11
- import { SUBSCRIPTION_GATED_RULES } from "./rules/subscription-gated/index.ts";
12
- import { BASELINE_ECHO_RULE } from "./rules/baseline-echo.ts";
13
-
14
- let bootstrapped = false;
15
-
16
- export function bootstrapAntiFp(): void {
17
- if (bootstrapped) return;
18
- for (const rule of SCHEMATHESIS_RULES) register(rule);
19
- // ARV-125: subscription/scope-gated 403 wontfix tail in mass-assignment
20
- // baseline summaries.
21
- for (const rule of SUBSCRIPTION_GATED_RULES) register(rule);
22
- // ARV-126: probe:security baseline-echo FP guard. The
23
- // coverage-phase-boundary rule is shared with checks via its
24
- // canonical re-export and is already covered by SCHEMATHESIS_RULES.
25
- register(BASELINE_ECHO_RULE);
26
- bootstrapped = true;
27
- }
28
-
29
- /** Test helper — clears the registry and the bootstrap flag so the
30
- * next call re-registers from scratch. */
31
- export function resetAntiFpBootstrap(): void {
32
- reset();
33
- bootstrapped = false;
34
- }
@@ -1,33 +0,0 @@
1
- /**
2
- * ARV-123 (m-19): public surface of the anti-FP registry.
3
- *
4
- * Callers (checks / probes) interact with a single helper —
5
- * `applyAntiFp(ctx, scope)` — which walks every rule registered for
6
- * `scope`, returns the first suppression that fires, or null. The
7
- * suppression object carries the rule id, the resolved scope, a
8
- * human reason, and the upstream references the rule was attributed
9
- * to.
10
- *
11
- * The registry itself is exported for migration tooling (ARV-124..126)
12
- * and for tests; production callers should prefer the helper.
13
- */
14
- export type { FpRule, FpScope, FpSuppression } from "./types.ts";
15
- export { register, get, list, reset, matchesScope } from "./registry.ts";
16
-
17
- import { list } from "./registry.ts";
18
- import type { FpScope, FpSuppression } from "./types.ts";
19
-
20
- export function applyAntiFp<Ctx>(ctx: Ctx, scope: FpScope): FpSuppression | null {
21
- for (const rule of list(scope)) {
22
- const hit = rule.applies(ctx);
23
- if (hit) {
24
- return {
25
- ruleId: hit.ruleId || rule.id,
26
- scope,
27
- reason: hit.reason,
28
- references: hit.references ?? rule.references,
29
- };
30
- }
31
- }
32
- return null;
33
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * ARV-123 (m-19): in-process registry for anti-FP rules.
3
- *
4
- * Module-level mutable state on purpose — rules are registered at
5
- * bootstrap time (similar to `core/probe/bootstrap.ts`) and read by
6
- * checks/probes during a run. `reset()` exists for tests; production
7
- * code never calls it.
8
- */
9
- import type { FpRule, FpScope, FpSuppression } from "./types.ts";
10
-
11
- const rules = new Map<string, FpRule<unknown>>();
12
-
13
- /** Register a rule. Re-registering with the same `id` replaces the
14
- * prior entry — this keeps test setups simple (swap in a stub) and
15
- * matches how the probe-bootstrap pattern handles dedup. */
16
- export function register<Ctx>(rule: FpRule<Ctx>): void {
17
- rules.set(rule.id, rule as FpRule<unknown>);
18
- }
19
-
20
- /** Lookup by id. Used mostly by tests and the `list` filter. */
21
- export function get(id: string): FpRule<unknown> | undefined {
22
- return rules.get(id);
23
- }
24
-
25
- /** List rules in registration order. Optional scope filter keeps the
26
- * hot path (checks/probes) from re-implementing the scope-match
27
- * predicate. Pass a `scope` like `"check:positive_data_acceptance"`
28
- * to get only rules that declared that scope. */
29
- export function list(scope?: FpScope): FpRule<unknown>[] {
30
- const all = Array.from(rules.values());
31
- if (!scope) return all;
32
- return all.filter(r => matchesScope(r, scope));
33
- }
34
-
35
- /** Drop every registered rule. Call only from test setup. */
36
- export function reset(): void {
37
- rules.clear();
38
- }
39
-
40
- export function matchesScope(rule: FpRule<unknown>, scope: FpScope): boolean {
41
- if (Array.isArray(rule.scope)) return rule.scope.includes(scope);
42
- return rule.scope === scope;
43
- }
44
-
@@ -1,74 +0,0 @@
1
- /**
2
- * ARV-126: baseline-echo FP guard for `probe:security`.
3
- *
4
- * Context: the live security probe sends a mutated body against a
5
- * 2xx-able baseline. When the response body is byte-for-byte identical
6
- * to the baseline response — same URL bouncing back unchanged — the
7
- * server effectively ignored the mutation. classifyInner currently
8
- * lands such findings at `severity: "low"` with the reason "2xx
9
- * accepted but no echo observed — verify side-effects manually",
10
- * which floods the digest with sites that have nothing to verify.
11
- *
12
- * This rule consumes a `{responseBody, baselineBody}` context and
13
- * fires when the two bodies are deeply equal. The probe consults
14
- * `applyAntiFp(ctx, "probe:security")` after classifyInner returns a
15
- * low-severity 2xx no-echo finding and, on a hit, downgrades the
16
- * finding to OK with the rule's reason as the wontfix banner.
17
- *
18
- * The deep-equality check is intentionally narrow — referential or
19
- * shape-only matches would over-suppress (a generic "ok: true" body
20
- * trivially equals across many endpoints). The probe is responsible
21
- * for passing the *full* parsed response, not a digest.
22
- */
23
- import type { FpRule } from "../types.ts";
24
-
25
- export interface BaselineEchoCtx {
26
- /** Parsed response body for the mutated request. */
27
- responseBody: unknown;
28
- /** Parsed response body for the pre-mutation baseline. May be
29
- * `undefined` when the probe didn't retain a baseline (in which
30
- * case the rule never fires — fail-open). */
31
- baselineBody: unknown;
32
- }
33
-
34
- function deepEqual(a: unknown, b: unknown): boolean {
35
- if (a === b) return true;
36
- if (a === null || b === null) return false;
37
- if (typeof a !== typeof b) return false;
38
- if (typeof a !== "object") return false;
39
- if (Array.isArray(a) !== Array.isArray(b)) return false;
40
- if (Array.isArray(a) && Array.isArray(b)) {
41
- if (a.length !== b.length) return false;
42
- for (let i = 0; i < a.length; i++) {
43
- if (!deepEqual(a[i], b[i])) return false;
44
- }
45
- return true;
46
- }
47
- const ao = a as Record<string, unknown>;
48
- const bo = b as Record<string, unknown>;
49
- const keys = Object.keys(ao);
50
- if (keys.length !== Object.keys(bo).length) return false;
51
- for (const k of keys) {
52
- if (!Object.prototype.hasOwnProperty.call(bo, k)) return false;
53
- if (!deepEqual(ao[k], bo[k])) return false;
54
- }
55
- return true;
56
- }
57
-
58
- export const BASELINE_ECHO_RULE: FpRule<BaselineEchoCtx> = {
59
- id: "baseline-echo",
60
- scope: "probe:security",
61
- references: ["ARV-126"],
62
- applies(ctx) {
63
- if (ctx.baselineBody === undefined) return null;
64
- if (!deepEqual(ctx.responseBody, ctx.baselineBody)) return null;
65
- return {
66
- ruleId: "baseline-echo",
67
- scope: "probe:security",
68
- reason:
69
- "response body identical to the pre-mutation baseline — server " +
70
- "ignored the attack payload; no side-effect to verify",
71
- references: ["ARV-126"],
72
- };
73
- },
74
- };
@@ -1,52 +0,0 @@
1
- /**
2
- * ARV-124: migrated from `src/core/checks/checks/_anti_fp.ts` (guard #1).
3
- *
4
- * Form-encoded / multipart bodies often *re-validate* after wire
5
- * serialisation: empty strings round-trip as missing, dropped optional
6
- * fields default at the server, numeric strings coerce. When the
7
- * mutation is a drop/empty-string on a form-shaped request, the
8
- * negative_data_rejection finding would be a false positive — the
9
- * mutation is a no-op on the wire.
10
- *
11
- * Sources: schemathesis #2482, #2726, #3712.
12
- */
13
- import type { CheckCase } from "../../../checks/types.ts";
14
- import type { MutationMeta } from "../../../checks/checks/_negative_mutator.ts";
15
- import type { FpRule } from "../../types.ts";
16
-
17
- function getMutation(c: CheckCase): MutationMeta | undefined {
18
- const m = c.meta as { mutation?: MutationMeta["mutation"] } | undefined;
19
- if (!m || typeof m.mutation !== "string") return undefined;
20
- return c.meta as unknown as MutationMeta;
21
- }
22
-
23
- function isFormLike(contentType: string | undefined): boolean {
24
- if (!contentType) return false;
25
- const ct = contentType.toLowerCase();
26
- return (
27
- ct.includes("application/x-www-form-urlencoded") ||
28
- ct.includes("multipart/form-data")
29
- );
30
- }
31
-
32
- export const bodyNegationBecomesValidRule: FpRule<CheckCase> = {
33
- id: "_body_negation_becomes_valid_after_serialization",
34
- scope: "check:negative_data_rejection",
35
- references: ["#2482", "#2726", "#3712"],
36
- applies(c) {
37
- const m = getMutation(c);
38
- if (!m) return null;
39
- const ct =
40
- c.request.headers["Content-Type"] ?? c.request.headers["content-type"];
41
- if (!isFormLike(ct)) return null;
42
- if (m.mutation === "drop_required" || m.mutation === "constraint_violation") {
43
- return {
44
- ruleId: "_body_negation_becomes_valid_after_serialization",
45
- scope: "check:negative_data_rejection",
46
- reason: `mutation "${m.mutation}" on a ${ct} body re-validates after wire serialisation`,
47
- references: ["#2482", "#2726", "#3712"],
48
- };
49
- }
50
- return null;
51
- },
52
- };
@@ -1,38 +0,0 @@
1
- /**
2
- * ARV-124: migrated from `src/core/checks/checks/_anti_fp.ts` (guard #4).
3
- *
4
- * `--phase coverage` enumerates boundary values across the body schema:
5
- * - shortest/longest string, min/max int, every enum option, ...
6
- * Those bodies are JSON-Schema-valid but semantically synthetic — they
7
- * sit on the contract edge. Real APIs reject them with 422 for reasons
8
- * that have nothing to do with the contract:
9
- * - "from" email must be on a verified-sending-domain,
10
- * - "broadcast.from_audience_id" must exist on this tenant,
11
- * - rate-limited resource (a plan_limit).
12
- * Treating each one as `positive_data_acceptance` fail floods the
13
- * report (171/349 findings on a benchmark run) and drowns real depth
14
- * signal. Skip when the case is a coverage-phase positive — keep the
15
- * examples-phase positive (one realistic baseline body) as the strict
16
- * signal.
17
- *
18
- * Source: feedback round-03 F20 / ARV-77.
19
- */
20
- import type { CheckCase } from "../../../checks/types.ts";
21
- import type { FpRule } from "../../types.ts";
22
-
23
- export const coveragePhaseBoundaryPositiveRule: FpRule<CheckCase> = {
24
- id: "_coverage_phase_boundary_positive",
25
- scope: "check:positive_data_acceptance",
26
- references: ["ARV-77"],
27
- applies(c) {
28
- const meta = c.meta as { phase?: string } | undefined;
29
- if (!meta || meta.phase !== "coverage") return null;
30
- if (c.kind !== "positive") return null;
31
- return {
32
- ruleId: "_coverage_phase_boundary_positive",
33
- scope: "check:positive_data_acceptance",
34
- reason:
35
- "boundary-positive bodies are synthetic — server may reject for semantic reasons unrelated to the contract",
36
- };
37
- },
38
- };
@@ -1,35 +0,0 @@
1
- /**
2
- * ARV-124: migrated from `src/core/checks/checks/_anti_fp.ts` (guard #3).
3
- *
4
- * Multiple disjoint mutations make accept/reject ambiguous: the server
5
- * might accept due to one site even while rejecting another. Our
6
- * single-site mutator emits exactly one mutation, so the guard fires
7
- * only when callers attach `mutation_count > 1` to `case.meta` — used
8
- * by future shrinkers / batched probes.
9
- *
10
- * Scope covers both data-rejection checks so a multi-site mutation
11
- * payload that survives into either side gets suppressed consistently.
12
- *
13
- * Source: schemathesis #2713.
14
- */
15
- import type { CheckCase } from "../../../checks/types.ts";
16
- import type { FpRule } from "../../types.ts";
17
-
18
- export const hasUnverifiableMutationsRule: FpRule<CheckCase> = {
19
- id: "_has_unverifiable_mutations",
20
- scope: ["check:negative_data_rejection", "check:positive_data_acceptance"],
21
- references: ["#2713"],
22
- applies(c) {
23
- const meta = c.meta as { mutation_count?: number } | undefined;
24
- if (!meta) return null;
25
- if (typeof meta.mutation_count === "number" && meta.mutation_count > 1) {
26
- return {
27
- ruleId: "_has_unverifiable_mutations",
28
- scope: "check:negative_data_rejection",
29
- reason: `${meta.mutation_count} mutations on disjoint sites — finding can't be attributed`,
30
- references: ["#2713"],
31
- };
32
- }
33
- return null;
34
- },
35
- };
@@ -1,24 +0,0 @@
1
- /**
2
- * ARV-124: schemathesis-attributed rule bundle. Each export is a
3
- * standalone FpRule for testing/introspection; the side-effect-free
4
- * list is re-exported as `SCHEMATHESIS_RULES` so the bootstrap can
5
- * register them in one batch.
6
- */
7
- import { bodyNegationBecomesValidRule } from "./body_negation_becomes_valid.ts";
8
- import { coveragePhaseBoundaryPositiveRule } from "./coverage_phase_boundary_positive.ts";
9
- import { hasUnverifiableMutationsRule } from "./has_unverifiable_mutations.ts";
10
- import { stringTypeMutationBecomesValidRule } from "./string_type_mutation_becomes_valid.ts";
11
-
12
- export {
13
- bodyNegationBecomesValidRule,
14
- coveragePhaseBoundaryPositiveRule,
15
- hasUnverifiableMutationsRule,
16
- stringTypeMutationBecomesValidRule,
17
- };
18
-
19
- export const SCHEMATHESIS_RULES = [
20
- bodyNegationBecomesValidRule,
21
- stringTypeMutationBecomesValidRule,
22
- hasUnverifiableMutationsRule,
23
- coveragePhaseBoundaryPositiveRule,
24
- ] as const;
@@ -1,53 +0,0 @@
1
- /**
2
- * ARV-124: migrated from `src/core/checks/checks/_anti_fp.ts` (guard #2).
3
- *
4
- * The classic "stringified primitive" trap: schema says `integer`, our
5
- * mutation flips to `"42"` (string), but servers using Express,
6
- * FastAPI, Rails coerce the string back to int. The mutation is a
7
- * no-op on the wire, so a 2xx isn't a real silent-accept.
8
- *
9
- * Sources: schemathesis #2312, #2978.
10
- */
11
- import type { CheckCase } from "../../../checks/types.ts";
12
- import type { MutationMeta } from "../../../checks/checks/_negative_mutator.ts";
13
- import type { FpRule } from "../../types.ts";
14
-
15
- function getMutation(c: CheckCase): MutationMeta | undefined {
16
- const m = c.meta as { mutation?: MutationMeta["mutation"] } | undefined;
17
- if (!m || typeof m.mutation !== "string") return undefined;
18
- return c.meta as unknown as MutationMeta;
19
- }
20
-
21
- export const stringTypeMutationBecomesValidRule: FpRule<CheckCase> = {
22
- id: "_string_type_mutation_becomes_valid_after_serialization",
23
- scope: "check:negative_data_rejection",
24
- references: ["#2312", "#2978"],
25
- applies(c) {
26
- const m = getMutation(c);
27
- if (!m || m.mutation !== "type_mutation") return null;
28
- const fromNumeric = m.from_type === "integer" || m.from_type === "number";
29
- const fromBoolean = m.from_type === "boolean";
30
- const toString = m.to_type === "string" || typeof m.to_value === "string";
31
- if (!toString) return null;
32
- if (fromNumeric || fromBoolean) {
33
- const v = String(m.to_value);
34
- if (fromNumeric && /^-?\d+(\.\d+)?$/.test(v)) {
35
- return {
36
- ruleId: "_string_type_mutation_becomes_valid_after_serialization",
37
- scope: "check:negative_data_rejection",
38
- reason: `value "${v}" is numerically coerceable — server may auto-cast`,
39
- references: ["#2312", "#2978"],
40
- };
41
- }
42
- if (fromBoolean && (v === "true" || v === "false")) {
43
- return {
44
- ruleId: "_string_type_mutation_becomes_valid_after_serialization",
45
- scope: "check:negative_data_rejection",
46
- reason: `value "${v}" is boolean-coerceable — server may auto-cast`,
47
- references: ["#2312"],
48
- };
49
- }
50
- }
51
- return null;
52
- },
53
- };
@@ -1,11 +0,0 @@
1
- /**
2
- * ARV-125 / ARV-126: subscription/scope-gated anti-FP rule bundle.
3
- * Same pattern as `rules/schemathesis/index.ts` — each rule is
4
- * exported individually for tests, the side-effect-free list is
5
- * consumed by `bootstrapAntiFp`.
6
- */
7
- import { PAID_PLAN_403_RULE } from "./paid-plan-403.ts";
8
-
9
- export { PAID_PLAN_403_RULE };
10
-
11
- export const SUBSCRIPTION_GATED_RULES = [PAID_PLAN_403_RULE] as const;
@@ -1,75 +0,0 @@
1
- /**
2
- * ARV-125: migrated from `core/probe/mass-assignment-probe.ts` —
3
- * inline pattern match for subscription/scope-gated 403 responses.
4
- *
5
- * Background (ARV-104 / F9): mass-assignment probing against a
6
- * paid-plan-gated API slice produced an INCONCLUSIVE baseline on
7
- * every gated endpoint. The default `inconclusiveBaselineSummary`
8
- * tail tells the triage agent to "fix fixture / FK / path-params
9
- * and re-probe" — but there's nothing to fix: the endpoint is gated
10
- * by subscription/scope, and the agent will crank-turn fixture edits
11
- * forever. The pattern match swaps the tail to a wontfix banner.
12
- *
13
- * Lives in the anti-FP registry as `subscription-gated/paid-plan-403`.
14
- * Scope is `probe:mass-assignment` (with `probe:security` listed too —
15
- * the live security probe hits the same surface and surfaces the same
16
- * gated bodies through ARV-126's migration of its baseline-echo check).
17
- *
18
- * Context payload: `{ status, message }`. The mass-assignment probe
19
- * already extracts the hint string from the response body; passing
20
- * the extracted string keeps the rule body-format-agnostic so future
21
- * callers (security probe) can reuse it without replicating the
22
- * extractor.
23
- */
24
- import type { FpRule } from "../../types.ts";
25
-
26
- export interface PaidPlan403Ctx {
27
- /** HTTP status of the baseline response. Rule applies only at 403. */
28
- status: number;
29
- /** Server-supplied message extracted from the response body. The
30
- * rule does not parse JSON — callers extract their preferred field
31
- * (commonly `detail` / `message`) and pass the string. */
32
- message?: string;
33
- }
34
-
35
- /** Lower-cased anchored fragments for the SaaS-flavoured wordings we
36
- * encounter in the wild. Each entry is one independent signal — a
37
- * body matching any one of them is treated as subscription-gated. */
38
- export const SUBSCRIPTION_GATED_PATTERNS: RegExp[] = [
39
- /\bpaid plan\b/i,
40
- /\bsubscription (?:required|needed)\b/i,
41
- /\bnot (?:available|enabled) (?:on|for) your\b/i,
42
- /\bplan (?:does not include|doesn['']?t include)\b/i,
43
- /\brequires? (?:the )?[\w:-]+ scope\b/i,
44
- /\bmissing (?:the )?[\w:-]+ scope\b/i,
45
- /\bfeature (?:is )?(?:not enabled|disabled|not available)\b/i,
46
- /\binsufficient (?:permissions?|scope)\b/i,
47
- ];
48
-
49
- /** Exported predicate for callers that want a quick yes/no without
50
- * composing an `applyAntiFp` call (the probe still exposes its own
51
- * re-export of this for back-compat with pre-ARV-125 tests). */
52
- export function matchesSubscriptionGated(message: string): boolean {
53
- for (const re of SUBSCRIPTION_GATED_PATTERNS) {
54
- if (re.test(message)) return true;
55
- }
56
- return false;
57
- }
58
-
59
- export const PAID_PLAN_403_RULE: FpRule<PaidPlan403Ctx> = {
60
- id: "subscription-gated/paid-plan-403",
61
- scope: ["probe:mass-assignment", "probe:security"],
62
- references: ["ARV-104"],
63
- applies(ctx) {
64
- if (ctx.status !== 403) return null;
65
- if (!ctx.message || !matchesSubscriptionGated(ctx.message)) return null;
66
- return {
67
- ruleId: "subscription-gated/paid-plan-403",
68
- scope: "probe:mass-assignment",
69
- reason:
70
- "endpoint is env/subscription-gated (paid plan, role/scope, feature flag); " +
71
- "not a fixture issue — wontfix unless scope changes",
72
- references: ["ARV-104"],
73
- };
74
- },
75
- };
@@ -1,68 +0,0 @@
1
- /**
2
- * ARV-123 (m-19): typed registry for anti-FP guards.
3
- *
4
- * Background: anti-FP logic is scattered across the codebase —
5
- * - `core/checks/checks/_anti_fp.ts` ships 4 schemathesis-attributed
6
- * guards for the data-rejection family;
7
- * - `core/probe/mass-assignment-probe.ts` carries inline regex
8
- * suppressions (paid-plan / subscription / scope-gating);
9
- * - `core/probe/security-probe.ts` does a baseline-echo / boundary
10
- * check inline.
11
- *
12
- * They share the same shape — "given a finding plus its context, return
13
- * a structured suppression with attribution" — but each has its own
14
- * ad-hoc API, which makes it hard to (a) discover the full set, (b)
15
- * attribute a suppression to its source (schemathesis #N, vendor
16
- * plan-limit doc, etc.), and (c) test rules in isolation.
17
- *
18
- * This module gives them a common contract. Migration of existing rules
19
- * lives in ARV-124/125/126 — this task only ships the registry.
20
- */
21
-
22
- /**
23
- * Scope identifies the family of checks/probes a rule applies to.
24
- * Convention: `<kind>:<name>`. Examples:
25
- * - `check:negative_data_rejection` / `check:positive_data_acceptance`
26
- * - `probe:mass-assignment`
27
- * - `probe:security` (baseline-echo / boundary)
28
- *
29
- * A rule may declare a single scope or an array of scopes. `applyAntiFp`
30
- * filters the registry by the caller's scope before running rules, so
31
- * a mass-assignment-only rule never gets evaluated against a data
32
- * rejection finding.
33
- */
34
- export type FpScope = string;
35
-
36
- export interface FpRule<Ctx = unknown> {
37
- /** Stable identifier — used for dedup, logs, and downstream
38
- * attribution. Convention mirrors schemathesis: snake_case prefixed
39
- * with the family (`_body_negation_becomes_valid_after_serialization`).
40
- * Last-writer wins on re-register, so test setups can swap rules. */
41
- id: string;
42
- /** Single scope or set of scopes this rule covers. */
43
- scope: FpScope | FpScope[];
44
- /** Decide whether the rule fires for a given context. Return a
45
- * populated suppression to claim the finding, or null to pass. */
46
- applies(ctx: Ctx): FpSuppression | null;
47
- /** Static reason used when the rule's logic just wants to flag the
48
- * context without composing a runtime string. Optional — most rules
49
- * prefer to build a context-specific reason inside `applies`. */
50
- reason?: string;
51
- /** Backing evidence — schemathesis issue numbers, vendor docs, etc.
52
- * Surfaced verbatim in the suppression so an agent reading the
53
- * output can locate the upstream debate. */
54
- references?: string[];
55
- }
56
-
57
- export interface FpSuppression {
58
- /** The rule that fired. */
59
- ruleId: string;
60
- /** The scope under which the rule fired (resolved scope, not the
61
- * rule's declared scope set). */
62
- scope: FpScope;
63
- /** Human-readable reason. Built by the rule's `applies` function. */
64
- reason: string;
65
- /** Copied through from the rule definition unless the rule overrode
66
- * it inside `applies`. */
67
- references?: string[];
68
- }
@@ -1,89 +0,0 @@
1
- /**
2
- * Build a request body for a POST/create endpoint that can actually pass
3
- * the API's validators on a live target.
4
- *
5
- * `generateFromSchema` is purely schema-driven: it emits `{{$uuid}}` for
6
- * `audience_id`-shaped fields, so when `prepare-fixtures --seed` POSTs a
7
- * generated body the API replies 422 ("audience aud_xyz not found") and
8
- * the seed loop quits with no progress (F1-14 in feedback-round 14).
9
- *
10
- * `buildCreateRequestBody` wraps `generateFromSchema` and walks the
11
- * resulting object: any property name that looks like a foreign-key id
12
- * (`*_id` / `*Id` / `*_uuid`) AND has a real value in `knownFixtures`
13
- * (typically loaded from `.env.yaml` plus values captured earlier in the
14
- * same cascade pass) gets that real value substituted in. Random
15
- * placeholders only survive when no env value exists.
16
- *
17
- * Used by `prepare-fixtures --seed --apply` (former `bootstrap`) so each
18
- * cascade step that POSTs a child resource pulls its parent ids from
19
- * what discover already filled. Without this, every nested resource is
20
- * a guaranteed 422.
21
- */
22
-
23
- import type { OpenAPIV3 } from "openapi-types";
24
- import { generateFromSchema } from "./data-factory.ts";
25
- import { canonicalVarName } from "./fixtures-builder.ts";
26
-
27
- const FK_FIELD_RE = /(?:_id|Id|_uuid)$/;
28
-
29
- function substituteFkFields(value: unknown, knownFixtures: Record<string, string>): unknown {
30
- if (Array.isArray(value)) {
31
- return value.map(v => substituteFkFields(v, knownFixtures));
32
- }
33
- if (value && typeof value === "object") {
34
- const obj = value as Record<string, unknown>;
35
- const out: Record<string, unknown> = {};
36
- for (const [k, v] of Object.entries(obj)) {
37
- // Recurse first — nested objects may carry their own FKs.
38
- const recursed = substituteFkFields(v, knownFixtures);
39
- if (FK_FIELD_RE.test(k)) {
40
- // ARV-138: look up by raw field name first (back-compat with envs
41
- // that still key off `issueId`), then by canonical snake_case form
42
- // (`issue_id`) — which is the only form the manifest emits since
43
- // ARV-138. This keeps both old `.env.yaml`s and new ones working
44
- // during the rollout window.
45
- const fixture = knownFixtures[k] ?? knownFixtures[canonicalVarName(k)];
46
- if (typeof fixture === "string" && fixture.length > 0) {
47
- out[k] = fixture;
48
- continue;
49
- }
50
- }
51
- out[k] = recursed;
52
- }
53
- return out;
54
- }
55
- return value;
56
- }
57
-
58
- export interface BuildCreateRequestBodyOptions {
59
- /**
60
- * Real values from `.env.yaml` (and prior cascade captures) keyed by
61
- * variable name. When a body field's name matches a key here AND the
62
- * field looks FK-shaped, that value replaces the schema-derived random
63
- * placeholder.
64
- */
65
- knownFixtures?: Record<string, string>;
66
- }
67
-
68
- /**
69
- * Spec-aware body builder for live-API seed POSTs. Returns a JSON-shaped
70
- * object ready to `JSON.stringify` and send.
71
- *
72
- * - Schema → object via `generateFromSchema(forRequest: true)`. This
73
- * already strips `readOnly` and bare `id` fields the server assigns.
74
- * - Walks the result and swaps FK-shaped fields for `knownFixtures`
75
- * values when present.
76
- *
77
- * Tokens like `{{$randomEmail}}` / `{{$uuid}}` produced by the schema
78
- * layer are intentionally left in place — the live runner resolves them
79
- * via `substituteDeep` right before the request is sent.
80
- */
81
- export function buildCreateRequestBody(
82
- schema: OpenAPIV3.SchemaObject,
83
- options: BuildCreateRequestBodyOptions = {},
84
- ): unknown {
85
- const generated = generateFromSchema(schema, undefined, { forRequest: true });
86
- const known = options.knownFixtures ?? {};
87
- if (Object.keys(known).length === 0) return generated;
88
- return substituteFkFields(generated, known);
89
- }