@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.
- package/CHANGELOG.md +164 -1
- package/README.md +8 -7
- package/package.json +2 -3
- package/src/CLAUDE.md +112 -0
- package/src/cli/commands/add-api.ts +19 -7
- package/src/cli/commands/api/annotate/index.ts +359 -4
- package/src/cli/commands/api/annotate/lifecycle.ts +1 -1
- package/src/cli/commands/api/annotate/overlay.ts +1 -1
- package/src/cli/commands/api/annotate/pagination.ts +10 -6
- package/src/cli/commands/api/annotate/prompts.ts +39 -2
- package/src/cli/commands/audit.ts +360 -54
- package/src/cli/commands/check.ts +15 -2
- package/src/cli/commands/checks.ts +352 -36
- package/src/cli/commands/cleanup.ts +4 -30
- package/src/cli/commands/coverage.ts +275 -57
- package/src/cli/commands/db.ts +311 -8
- package/src/cli/commands/discover.ts +281 -161
- package/src/cli/commands/doctor.ts +57 -3
- package/src/cli/commands/fixtures.ts +1 -1
- package/src/cli/commands/generate.ts +24 -7
- package/src/cli/commands/init/bootstrap.ts +4 -1
- package/src/cli/commands/init/index.ts +2 -2
- package/src/cli/commands/init/skills.ts +43 -0
- package/src/cli/commands/init/templates/agents.md +12 -3
- package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +268 -44
- package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +88 -26
- package/src/cli/commands/init/templates/skills/zond.md +274 -64
- package/src/cli/commands/init/templates/zond-config.yml +1 -1
- package/src/cli/commands/prepare-fixtures.ts +14 -52
- package/src/cli/commands/probe/_seed-bodies.ts +52 -0
- package/src/cli/commands/probe/mass-assignment.ts +101 -10
- package/src/cli/commands/probe/security.ts +95 -12
- package/src/cli/commands/probe/webhooks.ts +2 -0
- package/src/cli/commands/probe.ts +87 -11
- package/src/cli/commands/refresh-api.ts +59 -1
- package/src/cli/commands/report-bundle.ts +3 -11
- package/src/cli/commands/request.ts +116 -0
- package/src/cli/commands/run.ts +33 -5
- package/src/cli/commands/schema-from-runs.ts +128 -0
- package/src/cli/commands/secrets.ts +133 -0
- package/src/cli/json-envelope.ts +0 -20
- package/src/cli/json-schemas.ts +51 -0
- package/src/cli/output.ts +17 -1
- package/src/cli/program.ts +5 -4
- package/src/cli/safe-live.ts +24 -0
- package/src/cli/status-filter.ts +0 -10
- package/src/core/audit/persist.ts +183 -0
- package/src/core/checks/budget.ts +59 -0
- package/src/core/checks/checks/cross_call_references.ts +17 -4
- package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
- package/src/core/checks/checks/idempotency_replay.ts +1 -5
- package/src/core/checks/checks/ignored_auth.ts +44 -1
- package/src/core/checks/checks/index.ts +3 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +169 -26
- package/src/core/checks/checks/negative_data_rejection.ts +119 -16
- package/src/core/checks/checks/not_a_server_error.ts +8 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +47 -18
- package/src/core/checks/checks/pagination_invariants.ts +298 -117
- package/src/core/checks/checks/positive_data_acceptance.ts +1 -4
- package/src/core/checks/checks/status_code_conformance.ts +78 -7
- package/src/core/checks/mode.ts +3 -0
- package/src/core/checks/recommended-action.ts +5 -1
- package/src/core/checks/runner.ts +614 -27
- package/src/core/checks/spec-findings.ts +308 -0
- package/src/core/checks/types.ts +117 -1
- package/src/core/checks/zond-extensions.ts +73 -0
- package/src/core/classifier/recommended-action.ts +35 -6
- package/src/core/coverage/loader.ts +31 -0
- package/src/core/diagnostics/db-analysis.ts +200 -106
- package/src/core/diagnostics/failure-class.ts +21 -1
- package/src/core/diagnostics/failure-hints.ts +4 -208
- package/src/core/diagnostics/suggested-fixes.ts +2 -3
- package/src/core/generator/chunker.ts +1 -8
- package/src/core/generator/data-factory.ts +199 -61
- package/src/core/generator/fixtures-builder.ts +38 -31
- package/src/core/generator/index.ts +0 -2
- package/src/core/generator/openapi-reader.ts +98 -4
- package/src/core/generator/path-param-disambig.ts +30 -4
- package/src/core/generator/resources-builder.ts +276 -26
- package/src/core/generator/schema-utils.ts +22 -0
- package/src/core/generator/suite-generator.ts +168 -15
- package/src/core/generator/types.ts +6 -0
- package/src/core/identity/identity-file.ts +0 -0
- package/src/core/output/README.md +11 -29
- package/src/core/output/index.ts +1 -1
- package/src/core/output/run.ts +0 -35
- package/src/core/output/types.ts +0 -7
- package/src/core/parser/dynamic-values.ts +160 -0
- package/src/core/parser/variables.ts +0 -0
- package/src/core/probe/dry-run-envelope.ts +4 -0
- package/src/core/probe/mass-assignment/classify.ts +175 -0
- package/src/core/probe/mass-assignment/cleanup.ts +52 -0
- package/src/core/probe/mass-assignment/digest.ts +114 -0
- package/src/core/probe/mass-assignment/orchestrator.ts +459 -0
- package/src/core/probe/mass-assignment/regression.ts +141 -0
- package/src/core/probe/mass-assignment/suspects.ts +92 -0
- package/src/core/probe/mass-assignment/types.ts +135 -0
- package/src/core/probe/mass-assignment-probe.ts +23 -1118
- package/src/core/probe/mass-assignment-template.ts +32 -4
- package/src/core/probe/path-discovery.ts +3 -4
- package/src/core/probe/probe-harness.ts +21 -22
- package/src/core/probe/security/baseline.ts +174 -0
- package/src/core/probe/security/classify.ts +341 -0
- package/src/core/probe/security/cleanup.ts +125 -0
- package/src/core/probe/security/detectors.ts +71 -0
- package/src/core/probe/security/digest.ts +104 -0
- package/src/core/probe/security/orchestrator.ts +398 -0
- package/src/core/probe/security/regression.ts +103 -0
- package/src/core/probe/security/types.ts +151 -0
- package/src/core/probe/security-probe-class.ts +8 -2
- package/src/core/probe/security-probe.ts +28 -1449
- package/src/core/probe/shared.ts +26 -0
- package/src/core/probe/webhooks-probe.ts +5 -7
- package/src/core/runner/assertions.ts +1 -1
- package/src/core/runner/executor.ts +3 -18
- package/src/core/runner/form-encode.ts +8 -18
- package/src/core/runner/http-client.ts +38 -1
- package/src/core/runner/preflight-vars.ts +19 -15
- package/src/core/runner/rate-limiter.ts +11 -29
- package/src/core/runner/run-kind.ts +7 -1
- package/src/core/runner/schema-validator.ts +2 -6
- package/src/core/runner/send-request.ts +11 -6
- package/src/core/runner/types.ts +6 -0
- package/src/core/setup-api.ts +53 -15
- package/src/core/severity/index.ts +0 -63
- package/src/core/spec/infer-schema.ts +102 -0
- package/src/core/spec/merge-specs.ts +156 -0
- package/src/core/spec/schema-from-runs.ts +117 -0
- package/src/core/spec/schema-overlay.ts +130 -0
- package/src/core/util/ajv.ts +13 -0
- package/src/core/util/headers.ts +9 -0
- package/src/core/util/url.ts +24 -0
- package/src/core/workspace/fixture-gap-report.ts +84 -0
- package/src/core/workspace/fixture-gaps.ts +71 -0
- package/src/core/workspace/root.ts +13 -11
- package/src/db/migrate.ts +2 -0
- package/src/db/migrations/0002_run_kind_request.sql +59 -0
- package/src/db/queries/collections.ts +2 -2
- package/src/db/queries/results.ts +88 -0
- package/src/db/queries/runs.ts +56 -2
- package/src/db/queries.ts +3 -0
- package/src/db/schema.ts +7 -7
- package/src/cli/commands/bootstrap.ts +0 -710
- package/src/core/anti-fp/bootstrap.ts +0 -34
- package/src/core/anti-fp/index.ts +0 -33
- package/src/core/anti-fp/registry.ts +0 -44
- package/src/core/anti-fp/rules/baseline-echo.ts +0 -74
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +0 -52
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +0 -38
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +0 -35
- package/src/core/anti-fp/rules/schemathesis/index.ts +0 -24
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +0 -53
- package/src/core/anti-fp/rules/subscription-gated/index.ts +0 -11
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +0 -75
- package/src/core/anti-fp/types.ts +0 -68
- 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
|
-
}
|