@kirrosh/zond 0.21.0 → 0.23.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 +758 -3
- package/README.md +78 -15
- package/package.json +17 -10
- package/src/cli/argv.ts +122 -0
- package/src/cli/commands/add-api.ts +134 -0
- package/src/cli/commands/api/annotate/idempotency.ts +59 -0
- package/src/cli/commands/api/annotate/index.ts +525 -0
- package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
- package/src/cli/commands/api/annotate/overlay.ts +206 -0
- package/src/cli/commands/api/annotate/pagination.ts +60 -0
- package/src/cli/commands/api/annotate/prompts.ts +183 -0
- package/src/cli/commands/api/annotate/readback.ts +58 -0
- package/src/cli/commands/api/annotate/resources.ts +91 -0
- package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
- package/src/cli/commands/audit.ts +480 -0
- package/src/cli/commands/bootstrap.ts +710 -0
- package/src/cli/commands/catalog.ts +35 -0
- package/src/cli/commands/check.ts +348 -0
- package/src/cli/commands/checks.ts +756 -0
- package/src/cli/commands/ci-init.ts +55 -6
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +262 -0
- package/src/cli/commands/completions.ts +192 -0
- package/src/cli/commands/coverage.ts +605 -132
- package/src/cli/commands/db.ts +180 -8
- package/src/cli/commands/describe.ts +37 -2
- package/src/cli/commands/discover.ts +1236 -0
- package/src/cli/commands/doctor.ts +607 -0
- package/src/cli/commands/fixtures.ts +402 -0
- package/src/cli/commands/generate.ts +420 -47
- package/src/cli/commands/init/agents-md.ts +61 -0
- package/src/cli/commands/init/bootstrap.ts +108 -0
- package/src/cli/commands/init/index.ts +244 -0
- package/src/cli/commands/init/skills.ts +98 -0
- package/src/cli/commands/init/templates/agents.md +77 -0
- package/src/cli/commands/init/templates/markdown.d.ts +4 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
- package/src/cli/commands/init/templates/skills/zond.md +651 -0
- package/src/cli/commands/init/templates/zond-config.yml +14 -0
- package/src/cli/commands/prepare-fixtures.ts +135 -0
- package/src/cli/commands/probe/mass-assignment.ts +503 -0
- package/src/cli/commands/probe/security.ts +454 -0
- package/src/cli/commands/probe/static.ts +255 -0
- package/src/cli/commands/probe/webhooks.ts +161 -0
- package/src/cli/commands/probe.ts +459 -0
- package/src/cli/commands/reference.ts +87 -0
- package/src/cli/commands/refresh-api.ts +169 -0
- package/src/cli/commands/remove-api.ts +150 -0
- package/src/cli/commands/report-bundle.ts +318 -0
- package/src/cli/commands/report.ts +241 -0
- package/src/cli/commands/request.ts +379 -4
- package/src/cli/commands/run.ts +911 -33
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +74 -0
- package/src/cli/index.ts +36 -607
- package/src/cli/json-envelope.ts +112 -3
- package/src/cli/json-schemas.ts +263 -0
- package/src/cli/program.ts +218 -0
- package/src/cli/resolve.ts +105 -0
- package/src/cli/status-filter.ts +124 -0
- package/src/cli/util/api-context.ts +85 -0
- package/src/cli/version.ts +8 -0
- package/src/core/anti-fp/bootstrap.ts +34 -0
- package/src/core/anti-fp/index.ts +33 -0
- package/src/core/anti-fp/registry.ts +44 -0
- package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
- package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
- package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
- package/src/core/anti-fp/types.ts +68 -0
- package/src/core/checks/checks/_crud-helpers.ts +133 -0
- package/src/core/checks/checks/_negative_mutator.ts +133 -0
- package/src/core/checks/checks/_readback-helpers.ts +133 -0
- package/src/core/checks/checks/content_type_conformance.ts +39 -0
- package/src/core/checks/checks/cross_call_references.ts +134 -0
- package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
- package/src/core/checks/checks/idempotency_replay.ts +246 -0
- package/src/core/checks/checks/ignored_auth.ts +211 -0
- package/src/core/checks/checks/index.ts +65 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
- package/src/core/checks/checks/missing_required_header.ts +40 -0
- package/src/core/checks/checks/negative_data_rejection.ts +45 -0
- package/src/core/checks/checks/not_a_server_error.ts +27 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
- package/src/core/checks/checks/pagination_invariants.ts +238 -0
- package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
- package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
- package/src/core/checks/checks/response_headers_conformance.ts +74 -0
- package/src/core/checks/checks/response_schema_conformance.ts +30 -0
- package/src/core/checks/checks/status_code_conformance.ts +61 -0
- package/src/core/checks/checks/unsupported_method.ts +63 -0
- package/src/core/checks/checks/use_after_free.ts +78 -0
- package/src/core/checks/index.ts +30 -0
- package/src/core/checks/mode.ts +79 -0
- package/src/core/checks/recommended-action.ts +64 -0
- package/src/core/checks/registry.ts +78 -0
- package/src/core/checks/runner.ts +874 -0
- package/src/core/checks/sarif.ts +230 -0
- package/src/core/checks/stateful.ts +121 -0
- package/src/core/checks/types.ts +189 -0
- package/src/core/classifier/recommended-action.ts +222 -0
- package/src/core/context/current.ts +51 -0
- package/src/core/context/session.ts +78 -0
- package/src/core/coverage/loader.ts +185 -0
- package/src/core/coverage/reasons.ts +300 -0
- package/src/core/diagnostics/db-analysis.ts +161 -12
- package/src/core/diagnostics/failure-class.ts +120 -0
- package/src/core/diagnostics/failure-hints.ts +212 -9
- package/src/core/diagnostics/spec-pointer.ts +99 -0
- package/src/core/diagnostics/suggested-fixes.ts +156 -0
- package/src/core/exporter/case-study/index.ts +270 -0
- package/src/core/exporter/curl.ts +40 -0
- package/src/core/exporter/exporter.ts +48 -0
- package/src/core/exporter/html-report/escape.ts +24 -0
- package/src/core/exporter/html-report/index.ts +479 -0
- package/src/core/exporter/html-report/script.ts +100 -0
- package/src/core/exporter/html-report/styles.ts +408 -0
- package/src/core/generator/chunker.ts +53 -15
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/create-body.ts +89 -0
- package/src/core/generator/data-factory.ts +490 -33
- package/src/core/generator/describe.ts +1 -1
- package/src/core/generator/fixtures-builder.ts +325 -0
- package/src/core/generator/index.ts +7 -5
- package/src/core/generator/openapi-reader.ts +55 -3
- package/src/core/generator/path-param-disambig.ts +114 -0
- package/src/core/generator/resources-builder.ts +648 -0
- package/src/core/generator/schema-utils.ts +11 -3
- package/src/core/generator/serializer.ts +114 -15
- package/src/core/generator/suite-generator.ts +484 -77
- package/src/core/generator/types.ts +8 -0
- package/src/core/identity/identity-file.ts +129 -0
- package/src/core/lint/affects.ts +28 -0
- package/src/core/lint/config.ts +96 -0
- package/src/core/lint/format.ts +42 -0
- package/src/core/lint/index.ts +94 -0
- package/src/core/lint/reporter.ts +128 -0
- package/src/core/lint/rules/consistency.ts +158 -0
- package/src/core/lint/rules/heuristics.ts +97 -0
- package/src/core/lint/rules/strictness.ts +109 -0
- package/src/core/lint/types.ts +96 -0
- package/src/core/lint/walker.ts +248 -0
- package/src/core/meta/meta-store.ts +6 -73
- package/src/core/output/README.md +91 -0
- package/src/core/output/index.ts +13 -0
- package/src/core/output/run.ts +126 -0
- package/src/core/output/types.ts +129 -0
- package/src/core/parser/env-interpolation.ts +104 -0
- package/src/core/parser/filter.ts +57 -0
- package/src/core/parser/schema.ts +132 -5
- package/src/core/parser/types.ts +29 -2
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +108 -13
- package/src/core/probe/bootstrap.ts +34 -0
- package/src/core/probe/dry-run-envelope.ts +57 -0
- package/src/core/probe/mass-assignment-probe-class.ts +198 -0
- package/src/core/probe/mass-assignment-probe.ts +1122 -0
- package/src/core/probe/mass-assignment-template.ts +212 -0
- package/src/core/probe/method-probe.ts +164 -0
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +691 -0
- package/src/core/probe/orphan-tracker.ts +188 -0
- package/src/core/probe/path-discovery.ts +440 -0
- package/src/core/probe/probe-harness.ts +120 -0
- package/src/core/probe/registry.ts +89 -0
- package/src/core/probe/runner.ts +136 -0
- package/src/core/probe/security-probe-class.ts +201 -0
- package/src/core/probe/security-probe.ts +1453 -0
- package/src/core/probe/shared.ts +505 -0
- package/src/core/probe/static-probe-class.ts +125 -0
- package/src/core/probe/types.ts +165 -0
- package/src/core/probe/verdict-aggregator.ts +33 -0
- package/src/core/probe/webhooks-probe.ts +284 -0
- package/src/core/reporter/console.ts +69 -4
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +15 -2
- package/src/core/reporter/junit.ts +27 -12
- package/src/core/reporter/ndjson.ts +37 -0
- package/src/core/reporter/types.ts +3 -0
- package/src/core/runner/assertions.ts +62 -2
- package/src/core/runner/async-pool.ts +108 -0
- package/src/core/runner/auth-path.ts +8 -0
- package/src/core/runner/ci-context.ts +72 -0
- package/src/core/runner/executor.ts +391 -52
- package/src/core/runner/form-encode.ts +51 -0
- package/src/core/runner/http-client.ts +115 -7
- package/src/core/runner/learn-drift.ts +293 -0
- package/src/core/runner/preflight-vars.ts +149 -0
- package/src/core/runner/progress-tracker.ts +73 -0
- package/src/core/runner/rate-limiter.ts +203 -0
- package/src/core/runner/run-kind.ts +39 -0
- package/src/core/runner/schema-validator.ts +312 -0
- package/src/core/runner/send-request.ts +153 -20
- package/src/core/runner/types.ts +38 -0
- package/src/core/secrets/registry.ts +164 -0
- package/src/core/secrets/secrets-file.ts +115 -0
- package/src/core/selectors/operation-filter.ts +144 -0
- package/src/core/setup-api.ts +419 -17
- package/src/core/severity/category.ts +94 -0
- package/src/core/severity/index.ts +121 -0
- package/src/core/spec/layers.ts +154 -0
- package/src/core/util/format-eta.ts +21 -0
- package/src/core/utils.ts +5 -1
- package/src/core/workspace/config.ts +129 -0
- package/src/core/workspace/manifest.ts +283 -0
- package/src/core/workspace/output-rotation.ts +62 -0
- package/src/core/workspace/root.ts +94 -0
- package/src/core/workspace/triage-path.ts +87 -0
- package/src/db/lint-runs.ts +47 -0
- package/src/db/migrate.ts +126 -0
- package/src/db/migrations/0001_run_kind.sql +25 -0
- package/src/db/migrations/sql.d.ts +4 -0
- package/src/db/queries/collections.ts +133 -0
- package/src/db/queries/coverage.ts +9 -0
- package/src/db/queries/dashboard.ts +59 -0
- package/src/db/queries/results.ts +128 -0
- package/src/db/queries/runs.ts +235 -0
- package/src/db/queries/sessions.ts +42 -0
- package/src/db/queries/settings.ts +28 -0
- package/src/db/queries/types.ts +172 -0
- package/src/db/queries.ts +72 -802
- package/src/db/schema.ts +179 -48
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init.ts +0 -57
- package/src/cli/commands/serve.ts +0 -81
- package/src/cli/commands/sync.ts +0 -269
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -21
- package/src/core/parser/index.ts +0 -21
- package/src/core/runner/execute-run.ts +0 -132
- package/src/core/runner/index.ts +0 -12
- package/src/core/sync/spec-differ.ts +0 -38
- package/src/web/data/collection-state.ts +0 -362
- package/src/web/routes/api.ts +0 -314
- package/src/web/routes/dashboard.ts +0 -350
- package/src/web/routes/runs.ts +0 -64
- package/src/web/schemas.ts +0 -121
- package/src/web/server.ts +0 -134
- package/src/web/static/htmx.min.cjs +0 -1
- package/src/web/static/style.css +0 -1148
- package/src/web/views/endpoints-tab.ts +0 -174
- package/src/web/views/explorer-tab.ts +0 -402
- package/src/web/views/health-strip.ts +0 -92
- package/src/web/views/layout.ts +0 -48
- package/src/web/views/results.ts +0 -210
- package/src/web/views/runs-tab.ts +0 -126
- package/src/web/views/suites-tab.ts +0 -181
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for command-action callbacks: resolving the active API
|
|
3
|
+
* collection, the spec argument that spec-consuming commands accept, and
|
|
4
|
+
* tiny utilities (`globalJson`, deprecation warning). Extracted from
|
|
5
|
+
* program.ts (TASK-190 round 2a) so per-command modules can register
|
|
6
|
+
* themselves without re-importing program.ts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Command } from "commander";
|
|
10
|
+
import { getDb } from "../db/schema.ts";
|
|
11
|
+
import { findCollectionByNameOrId } from "../db/queries.ts";
|
|
12
|
+
import { resolveCollectionSpec } from "../core/setup-api.ts";
|
|
13
|
+
import { readCurrentApi } from "../core/context/current.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* TASK-73: `--json` is a per-command option (not a top-level global) so
|
|
17
|
+
* that `run --json` does not collide with `run --report json`.
|
|
18
|
+
* Subcommands that support an envelope output add `.option("--json", ...)`
|
|
19
|
+
* themselves and we read it from local opts.
|
|
20
|
+
*/
|
|
21
|
+
export function globalJson(cmd: Command): boolean {
|
|
22
|
+
return cmd.opts().json === true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Resolve API collection → returns { spec?, testPath?, baseDir? } or { error } when not found. */
|
|
26
|
+
export function resolveApiCollection(apiName: string, dbPath: string | undefined):
|
|
27
|
+
| { spec: string | null; testPath: string | null; baseDir: string | null }
|
|
28
|
+
| { error: string } {
|
|
29
|
+
if (typeof apiName !== "string" || apiName.length === 0) {
|
|
30
|
+
return { error: "Internal: --api received non-string value" };
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
getDb(dbPath);
|
|
34
|
+
const col = findCollectionByNameOrId(apiName);
|
|
35
|
+
if (!col) return { error: `API '${apiName}' not found` };
|
|
36
|
+
const spec = col.openapi_spec ? resolveCollectionSpec(col.openapi_spec) : null;
|
|
37
|
+
return { spec, testPath: col.test_path ?? null, baseDir: col.base_dir ?? null };
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return { error: `Failed to resolve --api: ${(err as Error).message}` };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve `apis/<name>/.env.yaml` for a registered API. TASK-233: probe
|
|
45
|
+
* subcommands required `--env <file>` even when `--api <name>` was given,
|
|
46
|
+
* forcing users to repeat the path. When --api is set we derive the env
|
|
47
|
+
* file from the collection's base_dir; only error if the file is missing.
|
|
48
|
+
*
|
|
49
|
+
* Returns the absolute path to the env file when it exists, otherwise an
|
|
50
|
+
* error object. Callers may also fall back to other strategies on miss.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveApiEnv(apiName: string, dbPath: string | undefined):
|
|
53
|
+
| { env: string }
|
|
54
|
+
| { error: string } {
|
|
55
|
+
const col = resolveApiCollection(apiName, dbPath);
|
|
56
|
+
if ("error" in col) return col;
|
|
57
|
+
if (!col.baseDir) {
|
|
58
|
+
return { error: `API '${apiName}' has no base_dir registered — pass --env <file> explicitly.` };
|
|
59
|
+
}
|
|
60
|
+
const envPath = `${col.baseDir.replace(/\/+$/, "")}/.env.yaml`;
|
|
61
|
+
return { env: envPath };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a `<spec>` argument used by spec-consuming commands —
|
|
66
|
+
* catalog, sync, generate, probe-validation, probe-methods,
|
|
67
|
+
* probe-mass-assignment, lint-spec, describe, guide.
|
|
68
|
+
*
|
|
69
|
+
* Resolution order:
|
|
70
|
+
* 1. Explicit positional/flag value — used as-is (URL or filesystem path).
|
|
71
|
+
* 2. --api <name> — look up the workspace-local snapshot via
|
|
72
|
+
* `resolveCollectionSpec`.
|
|
73
|
+
* 3. ZOND_API env / .zond/current-api — same lookup using the currently-selected API
|
|
74
|
+
* (TASK-290; resolution chain implemented in core/context/current.ts).
|
|
75
|
+
*
|
|
76
|
+
* Returns `{ spec }` on success, `{ error }` on failure. Centralised here
|
|
77
|
+
* so commands stay thin and skill/CI prompts can rely on either form.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveSpecArg(
|
|
80
|
+
positional: string | undefined,
|
|
81
|
+
apiFlag: string | undefined,
|
|
82
|
+
dbPath: string | undefined,
|
|
83
|
+
): { spec: string } | { error: string } {
|
|
84
|
+
if (typeof positional === "string" && positional.length > 0) {
|
|
85
|
+
return { spec: positional };
|
|
86
|
+
}
|
|
87
|
+
const apiName = apiFlag ?? readCurrentApi() ?? undefined;
|
|
88
|
+
if (!apiName) {
|
|
89
|
+
return {
|
|
90
|
+
error: "Need a spec — pass it positionally, via --api <name>, or set the current API with `zond use <name>`.",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const resolved = resolveApiCollection(apiName, dbPath);
|
|
94
|
+
if ("error" in resolved) return { error: resolved.error };
|
|
95
|
+
if (!resolved.spec) {
|
|
96
|
+
return {
|
|
97
|
+
error:
|
|
98
|
+
`API '${apiName}' is registered without an OpenAPI spec — this command needs one. ` +
|
|
99
|
+
`Run \`zond refresh-api ${apiName} --spec <path|url>\` to attach a spec, ` +
|
|
100
|
+
`or use \`zond run --api ${apiName} <test.yaml>\` for YAML-based testing.`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return { spec: resolved.spec };
|
|
104
|
+
}
|
|
105
|
+
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TASK-140: parse `--status` filter expressions for `zond db run/runs`.
|
|
3
|
+
*
|
|
4
|
+
* Accepted forms (combinable via comma):
|
|
5
|
+
* 502 — exact code
|
|
6
|
+
* 5xx / 4xx / 3xx / 2xx / 1xx — class wildcard (`5xx` ≡ 500..599)
|
|
7
|
+
* 500-599 — inclusive range
|
|
8
|
+
* >=500 / >500 / <=400 / <400 — open-ended comparison
|
|
9
|
+
* 500,502,504 — list of exacts
|
|
10
|
+
* 5xx,429 — mix of class + exact
|
|
11
|
+
*
|
|
12
|
+
* The parser yields a `StatusMatcher` which the DB layer converts into a
|
|
13
|
+
* single SQL `WHERE` fragment (set + ranges combined with `OR`).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface StatusMatcher {
|
|
17
|
+
/** Specific codes that should match. */
|
|
18
|
+
exacts: number[];
|
|
19
|
+
/** Inclusive ranges `[min, max]` — including class wildcards (5xx → 500..599). */
|
|
20
|
+
ranges: Array<[number, number]>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const CLASS_RE = /^([1-5])xx$/i;
|
|
24
|
+
const RANGE_RE = /^(\d{3})-(\d{3})$/;
|
|
25
|
+
const CMP_RE = /^(>=|<=|>|<)\s*(\d{3})$/;
|
|
26
|
+
const CODE_RE = /^\d{3}$/;
|
|
27
|
+
|
|
28
|
+
function pushExact(out: StatusMatcher, code: number): void {
|
|
29
|
+
if (code < 100 || code > 599) {
|
|
30
|
+
throw new Error(`status code out of range: ${code}`);
|
|
31
|
+
}
|
|
32
|
+
if (!out.exacts.includes(code)) out.exacts.push(code);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pushRange(out: StatusMatcher, min: number, max: number): void {
|
|
36
|
+
if (min > max) throw new Error(`status range start > end: ${min}-${max}`);
|
|
37
|
+
out.ranges.push([min, max]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse a `--status` argument. Throws on invalid syntax — caller wraps the
|
|
42
|
+
* thrown message in a CLI error.
|
|
43
|
+
*/
|
|
44
|
+
export function parseStatusFilter(raw: string): StatusMatcher {
|
|
45
|
+
const trimmed = raw.trim();
|
|
46
|
+
if (trimmed === "") throw new Error("empty --status value");
|
|
47
|
+
const parts = trimmed.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
48
|
+
if (parts.length === 0) throw new Error("empty --status value");
|
|
49
|
+
|
|
50
|
+
const out: StatusMatcher = { exacts: [], ranges: [] };
|
|
51
|
+
for (const part of parts) {
|
|
52
|
+
let m: RegExpMatchArray | null;
|
|
53
|
+
if ((m = part.match(CODE_RE))) {
|
|
54
|
+
pushExact(out, Number(part));
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if ((m = part.match(CLASS_RE))) {
|
|
58
|
+
const klass = Number(m[1]);
|
|
59
|
+
pushRange(out, klass * 100, klass * 100 + 99);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if ((m = part.match(RANGE_RE))) {
|
|
63
|
+
const lo = Number(m[1]);
|
|
64
|
+
const hi = Number(m[2]);
|
|
65
|
+
if (lo < 100 || lo > 599 || hi < 100 || hi > 599) {
|
|
66
|
+
throw new Error(`status range out of bounds: ${part} (expected 100..599)`);
|
|
67
|
+
}
|
|
68
|
+
pushRange(out, lo, hi);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if ((m = part.match(CMP_RE))) {
|
|
72
|
+
const op = m[1]!;
|
|
73
|
+
const code = Number(m[2]);
|
|
74
|
+
if (code < 100 || code > 599) {
|
|
75
|
+
throw new Error(`status comparison out of bounds: ${part} (expected 100..599)`);
|
|
76
|
+
}
|
|
77
|
+
switch (op) {
|
|
78
|
+
case ">=": pushRange(out, code, 599); break;
|
|
79
|
+
case ">": pushRange(out, code + 1, 599); break;
|
|
80
|
+
case "<=": pushRange(out, 100, code); break;
|
|
81
|
+
case "<": pushRange(out, 100, code - 1); break;
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
throw new Error(
|
|
86
|
+
`invalid --status part: '${part}' (expected one of: 502, 5xx, 500-599, >=500, <400)`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** True if `code` matches the parsed filter. Useful for in-memory filtering. */
|
|
93
|
+
export function statusMatches(matcher: StatusMatcher, code: number | null | undefined): boolean {
|
|
94
|
+
if (code == null) return false;
|
|
95
|
+
if (matcher.exacts.includes(code)) return true;
|
|
96
|
+
for (const [lo, hi] of matcher.ranges) {
|
|
97
|
+
if (code >= lo && code <= hi) return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Compile a `StatusMatcher` to a SQL `WHERE` fragment + bound parameters.
|
|
104
|
+
* The fragment is wrapped in parentheses; combine with other conditions via
|
|
105
|
+
* `AND`. Returns `null` if the matcher is empty (no constraints).
|
|
106
|
+
*/
|
|
107
|
+
export function compileStatusFilterToSql(
|
|
108
|
+
matcher: StatusMatcher,
|
|
109
|
+
column: string,
|
|
110
|
+
): { sql: string; params: number[] } | null {
|
|
111
|
+
const clauses: string[] = [];
|
|
112
|
+
const params: number[] = [];
|
|
113
|
+
if (matcher.exacts.length > 0) {
|
|
114
|
+
const placeholders = matcher.exacts.map(() => "?").join(",");
|
|
115
|
+
clauses.push(`${column} IN (${placeholders})`);
|
|
116
|
+
params.push(...matcher.exacts);
|
|
117
|
+
}
|
|
118
|
+
for (const [lo, hi] of matcher.ranges) {
|
|
119
|
+
clauses.push(`${column} BETWEEN ? AND ?`);
|
|
120
|
+
params.push(lo, hi);
|
|
121
|
+
}
|
|
122
|
+
if (clauses.length === 0) return null;
|
|
123
|
+
return { sql: `(${clauses.join(" OR ")})`, params };
|
|
124
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-53: single resolver for the active `--api` collection name.
|
|
3
|
+
*
|
|
4
|
+
* Before this module the chain `localOpts.api → cmd.parent?.opts().api →
|
|
5
|
+
* readCurrentApi()` was inlined in nine call-sites (probe / prepare-fixtures
|
|
6
|
+
* / audit / checks / coverage / run / request, plus two intermediate helpers
|
|
7
|
+
* in probe.ts and resolve.ts). Each repeat was a separate commit
|
|
8
|
+
* (TASK-17, TASK-20, ARV-21, ARV-29, ARV-33). Centralising the chain here
|
|
9
|
+
* means new commands consume one import and the fallback rules live in
|
|
10
|
+
* exactly one place.
|
|
11
|
+
*
|
|
12
|
+
* Resolution order (matches the chain users see documented for `zond use`):
|
|
13
|
+
* 1. Per-command `--api <name>` (the local Commander scope).
|
|
14
|
+
* 2. Any ancestor command's `--api`, walking up to the program root
|
|
15
|
+
* (covers the global `zond --api X <subcmd>` form whose value is
|
|
16
|
+
* otherwise stranded on the parent Command).
|
|
17
|
+
* 3. `readCurrentApi()` — which itself folds in
|
|
18
|
+
* `ZOND_API_GLOBAL` (mirrored by program.ts preAction) →
|
|
19
|
+
* `ZOND_API` (user env) → `.zond/current-api` (persisted by `zond use`).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readCurrentApi } from "../../core/context/current.ts";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Minimal shape we need from a Commander `Command`. Spelled out as an
|
|
26
|
+
* interface (not `import("commander").Command`) so test doubles can pass a
|
|
27
|
+
* plain `{ opts, parent }` literal and TS still type-checks.
|
|
28
|
+
*/
|
|
29
|
+
export interface CommandLike {
|
|
30
|
+
opts(): Record<string, unknown>;
|
|
31
|
+
parent?: CommandLike | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type ApiResolution =
|
|
35
|
+
| { ok: true; api: string; source: "local" | "ancestor" | "current" }
|
|
36
|
+
| { ok: false };
|
|
37
|
+
|
|
38
|
+
/** Pull the explicit --api value out of a command's parsed opts, if any. */
|
|
39
|
+
function readApiOpt(cmd: CommandLike): string | undefined {
|
|
40
|
+
const v = cmd.opts().api;
|
|
41
|
+
return typeof v === "string" && v.trim().length > 0 ? v.trim() : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the active API collection name for `cmd`. Pass `localOpts` when
|
|
46
|
+
* the action handler already has the parsed local opts in hand (avoids a
|
|
47
|
+
* second `cmd.opts()` parse and lets callers tunnel through pre-coerced
|
|
48
|
+
* shapes from tests).
|
|
49
|
+
*/
|
|
50
|
+
export function resolveApi(
|
|
51
|
+
cmd: CommandLike | undefined,
|
|
52
|
+
localOpts?: Record<string, unknown>,
|
|
53
|
+
): ApiResolution {
|
|
54
|
+
const localRaw = localOpts?.api;
|
|
55
|
+
const local = typeof localRaw === "string" && localRaw.trim().length > 0
|
|
56
|
+
? localRaw.trim()
|
|
57
|
+
: (cmd ? readApiOpt(cmd) : undefined);
|
|
58
|
+
if (local) return { ok: true, api: local, source: "local" };
|
|
59
|
+
|
|
60
|
+
let parent: CommandLike | null | undefined = cmd?.parent ?? null;
|
|
61
|
+
while (parent) {
|
|
62
|
+
const fromAncestor = readApiOpt(parent);
|
|
63
|
+
if (fromAncestor) return { ok: true, api: fromAncestor, source: "ancestor" };
|
|
64
|
+
parent = parent.parent ?? null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const fromCurrent = readCurrentApi();
|
|
68
|
+
if (fromCurrent) return { ok: true, api: fromCurrent, source: "current" };
|
|
69
|
+
|
|
70
|
+
return { ok: false };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Convenience: returns the API name as `string | undefined`. Use this when
|
|
75
|
+
* the caller decides for itself how to react to "missing" (e.g. `coverage`
|
|
76
|
+
* falls back to `--spec`, `run` falls back to a positional path).
|
|
77
|
+
*/
|
|
78
|
+
export function getApi(cmd: CommandLike | undefined, localOpts?: Record<string, unknown>): string | undefined {
|
|
79
|
+
const r = resolveApi(cmd, localOpts);
|
|
80
|
+
return r.ok ? r.api : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Default error message for commands that strictly require an API. */
|
|
84
|
+
export const MISSING_API_MESSAGE =
|
|
85
|
+
"--api is required (or set ZOND_API / `zond use <name>`).";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { version } from "../../package.json";
|
|
2
|
+
|
|
3
|
+
export const VERSION = version;
|
|
4
|
+
|
|
5
|
+
/** Canonical GitHub repo (owner/name). Single source of truth for any code
|
|
6
|
+
* that links to releases, install scripts, or generated artefacts. */
|
|
7
|
+
export const REPO = "kirrosh/zond";
|
|
8
|
+
export const REPO_URL = `https://github.com/${REPO}`;
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
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;
|