@kirrosh/zond 0.22.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 +648 -0
- package/README.md +58 -6
- package/package.json +9 -6
- 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 +43 -0
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +262 -0
- package/src/cli/commands/completions.ts +16 -0
- package/src/cli/commands/coverage.ts +605 -132
- package/src/cli/commands/db.ts +178 -7
- 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 -46
- package/src/cli/commands/init/bootstrap.ts +30 -1
- package/src/cli/commands/{init.ts → init/index.ts} +99 -5
- package/src/cli/commands/init/skills.ts +56 -3
- package/src/cli/commands/init/templates/agents.md +65 -61
- 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 +592 -125
- package/src/cli/commands/init/templates/zond-config.yml +8 -9
- 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 +842 -53
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +18 -1
- package/src/cli/index.ts +20 -3
- package/src/cli/json-envelope.ts +112 -3
- package/src/cli/json-schemas.ts +263 -0
- package/src/cli/program.ts +198 -635
- 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 +5 -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 +22 -6
- 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 +151 -11
- 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 +42 -16
- 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 +445 -19
- 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 +37 -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 +103 -13
- package/src/core/generator/suite-generator.ts +419 -111
- 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 +129 -4
- package/src/core/parser/types.ts +19 -1
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +58 -12
- 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 +43 -76
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +183 -149
- 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 +41 -2
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +11 -1
- 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 +58 -1
- 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 +264 -20
- package/src/core/runner/form-encode.ts +51 -0
- package/src/core/runner/http-client.ts +75 -2
- 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 +89 -17
- 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 +415 -16
- 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/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 +178 -50
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
- package/src/cli/commands/probe-methods.ts +0 -108
- package/src/cli/commands/probe-validation.ts +0 -124
- package/src/cli/commands/serve.ts +0 -114
- package/src/cli/commands/sync.ts +0 -268
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/diagnostics/render-md.ts +0 -112
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -19
- 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
|
@@ -6,10 +6,16 @@
|
|
|
6
6
|
* is simple: any client-supplied invalid input MUST produce a 4xx, never a 5xx.
|
|
7
7
|
*
|
|
8
8
|
* For each endpoint we generate a suite of probe steps. Each step expects a
|
|
9
|
-
* "no 5xx" response (status in [400, 401, 403, 404, 405, 409, 415, 422]).
|
|
9
|
+
* "no 5xx" response (status in [400, 401, 403, 404, 405, 409, 415, 422, 429]).
|
|
10
10
|
* If the API returns 500/502/503 — the test fails and the runner logs it as
|
|
11
11
|
* a bug candidate via the regular reporter / `zond db diagnose` flow.
|
|
12
12
|
*
|
|
13
|
+
* ARV-34: 429 is in the allow-set because rate-limiting is itself a valid
|
|
14
|
+
* server-side rejection of the request — the API refused to process invalid
|
|
15
|
+
* input, the contract is satisfied. Throttled probe runs were producing
|
|
16
|
+
* hundreds of false failures with the warning "N requests hit rate limit"
|
|
17
|
+
* already saying the same thing.
|
|
18
|
+
*
|
|
13
19
|
* The probes are deterministic — same spec → same suites — so the generated
|
|
14
20
|
* YAML can be committed as a regression test.
|
|
15
21
|
*/
|
|
@@ -17,6 +23,17 @@ import type { OpenAPIV3 } from "openapi-types";
|
|
|
17
23
|
import type { EndpointInfo, SecuritySchemeInfo } from "../generator/types.ts";
|
|
18
24
|
import type { RawSuite, RawStep } from "../generator/serializer.ts";
|
|
19
25
|
import { generateFromSchema } from "../generator/data-factory.ts";
|
|
26
|
+
import {
|
|
27
|
+
convertPath,
|
|
28
|
+
endpointStem,
|
|
29
|
+
getAuthHeaders,
|
|
30
|
+
renderPath,
|
|
31
|
+
isMutating,
|
|
32
|
+
findDeleteCounterpart,
|
|
33
|
+
captureFieldFor,
|
|
34
|
+
headersEqual,
|
|
35
|
+
hasJsonBody,
|
|
36
|
+
} from "./shared.ts";
|
|
20
37
|
|
|
21
38
|
// ──────────────────────────────────────────────
|
|
22
39
|
// Constants
|
|
@@ -24,8 +41,9 @@ import { generateFromSchema } from "../generator/data-factory.ts";
|
|
|
24
41
|
|
|
25
42
|
/** Statuses we consider an *acceptable* response to invalid input. Anything
|
|
26
43
|
* outside this set (notably 5xx, but also 200/201 which would mean the API
|
|
27
|
-
* silently accepted the bad input) is a probe failure.
|
|
28
|
-
|
|
44
|
+
* silently accepted the bad input) is a probe failure. ARV-34: 429 stays in
|
|
45
|
+
* the allow-set — server-side throttling is a valid rejection. */
|
|
46
|
+
const ACCEPTABLE_4XX = [400, 401, 403, 404, 405, 409, 415, 422, 429];
|
|
29
47
|
|
|
30
48
|
/** Long string for boundary probes — 10_000 chars. */
|
|
31
49
|
const LONG_STRING = "a".repeat(10_000);
|
|
@@ -34,12 +52,13 @@ const LONG_STRING = "a".repeat(10_000);
|
|
|
34
52
|
const UNICODE_MIX = "Mix🌐مرحبا\u200B";
|
|
35
53
|
|
|
36
54
|
/** Sentinel non-UUID inputs for path/UUID probes. */
|
|
37
|
-
const
|
|
55
|
+
export const INVALID_UUID_SENTINELS = [
|
|
38
56
|
"not-a-uuid",
|
|
39
57
|
"12345",
|
|
40
58
|
"00000000",
|
|
41
59
|
"../../etc/passwd",
|
|
42
|
-
];
|
|
60
|
+
] as const;
|
|
61
|
+
const INVALID_UUID_VALUES = INVALID_UUID_SENTINELS;
|
|
43
62
|
|
|
44
63
|
/** Sentinel invalid emails. */
|
|
45
64
|
const INVALID_EMAIL_VALUES = [
|
|
@@ -77,6 +96,15 @@ export interface ProbeOptions {
|
|
|
77
96
|
* (staging dump-and-reset) where cleanup is handled out of band.
|
|
78
97
|
*/
|
|
79
98
|
noCleanup?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* TASK-135: when true (default), non-attacked path-params are emitted as
|
|
101
|
+
* runtime placeholders `{{name}}` so `zond run` substitutes them from
|
|
102
|
+
* `.env.yaml`. This avoids the short-circuit where every probe targeting a
|
|
103
|
+
* nested path resolved `{org}=nonexistent-zzzzz` and got 404 before the
|
|
104
|
+
* leaf validator ever fired. Set to false to keep the legacy behaviour
|
|
105
|
+
* (synthetic-by-type for every param).
|
|
106
|
+
*/
|
|
107
|
+
useRealParents?: boolean;
|
|
80
108
|
}
|
|
81
109
|
|
|
82
110
|
export interface ProbeResult {
|
|
@@ -99,66 +127,6 @@ export interface ProbeResult {
|
|
|
99
127
|
// Helpers
|
|
100
128
|
// ──────────────────────────────────────────────
|
|
101
129
|
|
|
102
|
-
function convertPath(path: string): string {
|
|
103
|
-
return path.replace(/\{([^}]+)\}/g, "{{$1}}");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function slugify(s: string): string {
|
|
107
|
-
return s
|
|
108
|
-
.toLowerCase()
|
|
109
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
110
|
-
.replace(/^-|-$/g, "");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function endpointStem(ep: EndpointInfo): string {
|
|
114
|
-
const path = ep.path
|
|
115
|
-
.replace(/\{[^}]+\}/g, "by-id")
|
|
116
|
-
.replace(/^\//, "")
|
|
117
|
-
.replace(/\//g, "-");
|
|
118
|
-
return slugify(`${ep.method.toLowerCase()}-${path}`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function getAuthHeaders(
|
|
122
|
-
ep: EndpointInfo,
|
|
123
|
-
schemes: SecuritySchemeInfo[],
|
|
124
|
-
): Record<string, string> | undefined {
|
|
125
|
-
if (ep.security.length === 0) return undefined;
|
|
126
|
-
for (const secName of ep.security) {
|
|
127
|
-
const scheme = schemes.find((s) => s.name === secName);
|
|
128
|
-
if (!scheme) continue;
|
|
129
|
-
if (scheme.type === "http") {
|
|
130
|
-
if (scheme.scheme === "bearer" || !scheme.scheme) {
|
|
131
|
-
return { Authorization: "Bearer {{auth_token}}" };
|
|
132
|
-
}
|
|
133
|
-
if (scheme.scheme === "basic") {
|
|
134
|
-
return { Authorization: "Basic {{auth_token}}" };
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
if (scheme.type === "apiKey" && scheme.in === "header" && scheme.apiKeyName) {
|
|
138
|
-
if (scheme.apiKeyName === "Authorization") {
|
|
139
|
-
return { Authorization: "Bearer {{auth_token}}" };
|
|
140
|
-
}
|
|
141
|
-
return { [scheme.apiKeyName]: "{{api_key}}" };
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return undefined;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Path with placeholders replaced by valid-but-nonexistent IDs (for body probes
|
|
148
|
-
* on PUT/PATCH/DELETE — we don't want path validation to mask body errors). */
|
|
149
|
-
function pathWithPlaceholders(ep: EndpointInfo, badId: string): string {
|
|
150
|
-
return ep.path.replace(/\{([^}]+)\}/g, (_, name: string) => {
|
|
151
|
-
const param = ep.parameters.find((p) => p.name === name && p.in === "path");
|
|
152
|
-
const schema = param?.schema as OpenAPIV3.SchemaObject | undefined;
|
|
153
|
-
if (badId === "valid-shape") {
|
|
154
|
-
if (schema?.format === "uuid") return "00000000-0000-0000-0000-000000000000";
|
|
155
|
-
if (schema?.type === "integer" || schema?.type === "number") return "999999999";
|
|
156
|
-
return "nonexistent-zzzzz";
|
|
157
|
-
}
|
|
158
|
-
return badId;
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
130
|
function findUuidPathParams(ep: EndpointInfo): OpenAPIV3.ParameterObject[] {
|
|
163
131
|
return ep.parameters.filter((p) => {
|
|
164
132
|
if (p.in !== "path") return false;
|
|
@@ -207,6 +175,11 @@ function collectAllProps(
|
|
|
207
175
|
* Build a step that targets `endpoint`, but with an arbitrary body override.
|
|
208
176
|
* Authentication and required path params are populated with valid placeholders
|
|
209
177
|
* so the request reaches the body-validation layer.
|
|
178
|
+
*
|
|
179
|
+
* `pathOverride` (if provided) is treated as already-final — no further
|
|
180
|
+
* placeholder conversion is applied. Otherwise the path is rendered via
|
|
181
|
+
* `renderPath` with no attack target (so all path-params become runtime
|
|
182
|
+
* placeholders / synthetic sentinels depending on `useRealParents`).
|
|
210
183
|
*/
|
|
211
184
|
function buildStep(
|
|
212
185
|
ep: EndpointInfo,
|
|
@@ -216,17 +189,25 @@ function buildStep(
|
|
|
216
189
|
json?: unknown;
|
|
217
190
|
pathOverride?: string;
|
|
218
191
|
expectStatusOk?: number[];
|
|
192
|
+
useRealParents: boolean;
|
|
219
193
|
},
|
|
220
194
|
): RawStep {
|
|
221
195
|
const method = ep.method.toUpperCase();
|
|
222
|
-
const path = opts.pathOverride ??
|
|
196
|
+
const path = opts.pathOverride ?? renderPath(ep, null, { useRealParents: opts.useRealParents });
|
|
223
197
|
const headers = getAuthHeaders(ep, schemes);
|
|
224
198
|
|
|
199
|
+
const expectedStatus = opts.expectStatusOk ?? ACCEPTABLE_4XX;
|
|
200
|
+
const responseBranch = Array.isArray(expectedStatus) ? expectedStatus.map(String).join("|") : String(expectedStatus);
|
|
225
201
|
const step: RawStep = {
|
|
226
202
|
name: opts.name,
|
|
227
|
-
|
|
203
|
+
source: {
|
|
204
|
+
generator: "negative-probe",
|
|
205
|
+
endpoint: `${method} ${ep.path}`,
|
|
206
|
+
response_branch: responseBranch,
|
|
207
|
+
},
|
|
208
|
+
[method]: path,
|
|
228
209
|
expect: {
|
|
229
|
-
status:
|
|
210
|
+
status: expectedStatus,
|
|
230
211
|
},
|
|
231
212
|
};
|
|
232
213
|
if (headers) step.headers = headers;
|
|
@@ -234,7 +215,11 @@ function buildStep(
|
|
|
234
215
|
return step;
|
|
235
216
|
}
|
|
236
217
|
|
|
237
|
-
function probeEmptyBody(
|
|
218
|
+
function probeEmptyBody(
|
|
219
|
+
ep: EndpointInfo,
|
|
220
|
+
schemes: SecuritySchemeInfo[],
|
|
221
|
+
useRealParents: boolean,
|
|
222
|
+
): RawStep | null {
|
|
238
223
|
if (!hasJsonBody(ep)) return null;
|
|
239
224
|
const required = collectRequiredFields(ep.requestBodySchema);
|
|
240
225
|
// Only meaningful when there *is* required data — otherwise {} is valid.
|
|
@@ -242,6 +227,7 @@ function probeEmptyBody(ep: EndpointInfo, schemes: SecuritySchemeInfo[]): RawSte
|
|
|
242
227
|
return buildStep(ep, schemes, {
|
|
243
228
|
name: "empty body — must reject (no 5xx)",
|
|
244
229
|
json: {},
|
|
230
|
+
useRealParents,
|
|
245
231
|
});
|
|
246
232
|
}
|
|
247
233
|
|
|
@@ -249,6 +235,7 @@ function probeMissingRequired(
|
|
|
249
235
|
ep: EndpointInfo,
|
|
250
236
|
schemes: SecuritySchemeInfo[],
|
|
251
237
|
budget: number,
|
|
238
|
+
useRealParents: boolean,
|
|
252
239
|
): RawStep[] {
|
|
253
240
|
if (!hasJsonBody(ep) || !ep.requestBodySchema) return [];
|
|
254
241
|
const required = collectRequiredFields(ep.requestBodySchema);
|
|
@@ -267,6 +254,7 @@ function probeMissingRequired(
|
|
|
267
254
|
buildStep(ep, schemes, {
|
|
268
255
|
name: `missing required field "${field.name}" — must reject (no 5xx)`,
|
|
269
256
|
json: variant,
|
|
257
|
+
useRealParents,
|
|
270
258
|
}),
|
|
271
259
|
);
|
|
272
260
|
}
|
|
@@ -277,6 +265,7 @@ function probeBoundaryString(
|
|
|
277
265
|
ep: EndpointInfo,
|
|
278
266
|
schemes: SecuritySchemeInfo[],
|
|
279
267
|
budget: number,
|
|
268
|
+
useRealParents: boolean,
|
|
280
269
|
): RawStep[] {
|
|
281
270
|
if (!hasJsonBody(ep) || !ep.requestBodySchema) return [];
|
|
282
271
|
const props = collectAllProps(ep.requestBodySchema).filter(
|
|
@@ -295,14 +284,17 @@ function probeBoundaryString(
|
|
|
295
284
|
buildStep(ep, schemes, {
|
|
296
285
|
name: `${field.name}: empty string — must reject (no 5xx)`,
|
|
297
286
|
json: { ...baseline, [field.name]: "" },
|
|
287
|
+
useRealParents,
|
|
298
288
|
}),
|
|
299
289
|
buildStep(ep, schemes, {
|
|
300
290
|
name: `${field.name}: 10000-char string — must reject or accept (no 5xx)`,
|
|
301
291
|
json: { ...baseline, [field.name]: LONG_STRING },
|
|
292
|
+
useRealParents,
|
|
302
293
|
}),
|
|
303
294
|
buildStep(ep, schemes, {
|
|
304
295
|
name: `${field.name}: unicode/emoji/RTL — must not 5xx`,
|
|
305
296
|
json: { ...baseline, [field.name]: UNICODE_MIX },
|
|
297
|
+
useRealParents,
|
|
306
298
|
}),
|
|
307
299
|
);
|
|
308
300
|
}
|
|
@@ -313,6 +305,7 @@ function probeTypeConfusion(
|
|
|
313
305
|
ep: EndpointInfo,
|
|
314
306
|
schemes: SecuritySchemeInfo[],
|
|
315
307
|
budget: number,
|
|
308
|
+
useRealParents: boolean,
|
|
316
309
|
): RawStep[] {
|
|
317
310
|
if (!hasJsonBody(ep) || !ep.requestBodySchema) return [];
|
|
318
311
|
const props = collectAllProps(ep.requestBodySchema);
|
|
@@ -330,6 +323,7 @@ function probeTypeConfusion(
|
|
|
330
323
|
buildStep(ep, schemes, {
|
|
331
324
|
name: `${field.name}: wrong type (${describeType(field.schema)} → ${typeof wrongValue}) — must reject (no 5xx)`,
|
|
332
325
|
json: { ...baseline, [field.name]: wrongValue },
|
|
326
|
+
useRealParents,
|
|
333
327
|
}),
|
|
334
328
|
);
|
|
335
329
|
}
|
|
@@ -340,6 +334,7 @@ function probeInvalidFormat(
|
|
|
340
334
|
ep: EndpointInfo,
|
|
341
335
|
schemes: SecuritySchemeInfo[],
|
|
342
336
|
budget: number,
|
|
337
|
+
useRealParents: boolean,
|
|
343
338
|
): RawStep[] {
|
|
344
339
|
if (!hasJsonBody(ep) || !ep.requestBodySchema) return [];
|
|
345
340
|
const props = collectAllProps(ep.requestBodySchema);
|
|
@@ -360,6 +355,7 @@ function probeInvalidFormat(
|
|
|
360
355
|
buildStep(ep, schemes, {
|
|
361
356
|
name: `${field.name}: invalid ${fmt} (${JSON.stringify(badValue)}) — must reject (no 5xx)`,
|
|
362
357
|
json: { ...baseline, [field.name]: badValue },
|
|
358
|
+
useRealParents,
|
|
363
359
|
}),
|
|
364
360
|
);
|
|
365
361
|
}
|
|
@@ -370,6 +366,7 @@ function probeInvalidEnum(
|
|
|
370
366
|
ep: EndpointInfo,
|
|
371
367
|
schemes: SecuritySchemeInfo[],
|
|
372
368
|
budget: number,
|
|
369
|
+
useRealParents: boolean,
|
|
373
370
|
): RawStep[] {
|
|
374
371
|
if (!hasJsonBody(ep) || !ep.requestBodySchema) return [];
|
|
375
372
|
const baseline = generateFromSchema(ep.requestBodySchema) as Record<string, unknown>;
|
|
@@ -384,6 +381,7 @@ function probeInvalidEnum(
|
|
|
384
381
|
buildStep(ep, schemes, {
|
|
385
382
|
name: `${field.name}: unknown enum value "zond_invalid_value" — must reject (no 5xx)`,
|
|
386
383
|
json: { ...baseline, [field.name]: "zond_invalid_value" },
|
|
384
|
+
useRealParents,
|
|
387
385
|
}),
|
|
388
386
|
);
|
|
389
387
|
}
|
|
@@ -399,6 +397,7 @@ function probeInvalidEnum(
|
|
|
399
397
|
buildStep(ep, schemes, {
|
|
400
398
|
name: `${field.name}: array with unknown value ["zond.nonexistent.event"] — must reject (no 5xx)`,
|
|
401
399
|
json: { ...baseline, [field.name]: ["zond.nonexistent.event"] },
|
|
400
|
+
useRealParents,
|
|
402
401
|
}),
|
|
403
402
|
);
|
|
404
403
|
}
|
|
@@ -411,6 +410,7 @@ function probeInvalidPathId(
|
|
|
411
410
|
ep: EndpointInfo,
|
|
412
411
|
schemes: SecuritySchemeInfo[],
|
|
413
412
|
budget: number,
|
|
413
|
+
useRealParents: boolean,
|
|
414
414
|
): RawStep[] {
|
|
415
415
|
const params = findUuidPathParams(ep);
|
|
416
416
|
if (params.length === 0) return [];
|
|
@@ -419,18 +419,91 @@ function probeInvalidPathId(
|
|
|
419
419
|
for (const param of params) {
|
|
420
420
|
for (const bad of INVALID_UUID_VALUES) {
|
|
421
421
|
if (out.length >= budget) break;
|
|
422
|
-
const path = ep
|
|
423
|
-
if (name === param.name) return bad;
|
|
424
|
-
const other = ep.parameters.find((p) => p.name === name && p.in === "path");
|
|
425
|
-
const schema = other?.schema as OpenAPIV3.SchemaObject | undefined;
|
|
426
|
-
if (schema?.format === "uuid") return "00000000-0000-0000-0000-000000000000";
|
|
427
|
-
if (schema?.type === "integer" || schema?.type === "number") return "999999999";
|
|
428
|
-
return "nonexistent-zzzzz";
|
|
429
|
-
});
|
|
422
|
+
const path = renderPath(ep, { name: param.name, value: bad }, { useRealParents });
|
|
430
423
|
out.push(
|
|
431
424
|
buildStep(ep, schemes, {
|
|
432
425
|
name: `path param ${param.name}=${JSON.stringify(bad)} — must reject (no 5xx)`,
|
|
433
426
|
pathOverride: path,
|
|
427
|
+
useRealParents,
|
|
428
|
+
}),
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** TASK-67: numeric coercion probes for integer/number params (query + path).
|
|
436
|
+
* Round-2 found `GET /emails?limit=1.5` → 500 — the bug class probe-validation
|
|
437
|
+
* exists for. T49 covered numeric type-confusion in body, this extends to
|
|
438
|
+
* query/path. */
|
|
439
|
+
const NUMERIC_BAD_VALUES: Array<{ value: string; label: string }> = [
|
|
440
|
+
{ value: "1.5", label: "float on integer" },
|
|
441
|
+
{ value: "-1", label: "negative" },
|
|
442
|
+
{ value: "0", label: "zero" },
|
|
443
|
+
{ value: "abc", label: "non-numeric" },
|
|
444
|
+
{ value: "", label: "empty string" },
|
|
445
|
+
// null on a query param means literal "null" string — most parsers treat it as bad input
|
|
446
|
+
{ value: "null", label: "literal null" },
|
|
447
|
+
// Number.MAX_SAFE_INTEGER + 1 = 9007199254740992
|
|
448
|
+
{ value: "9007199254740992", label: "MAX_SAFE_INTEGER+1" },
|
|
449
|
+
];
|
|
450
|
+
|
|
451
|
+
function isNumericSchema(schema: OpenAPIV3.SchemaObject | undefined): boolean {
|
|
452
|
+
return schema?.type === "integer" || schema?.type === "number";
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function probeNumericQueryParams(
|
|
456
|
+
ep: EndpointInfo,
|
|
457
|
+
schemes: SecuritySchemeInfo[],
|
|
458
|
+
budget: number,
|
|
459
|
+
useRealParents: boolean,
|
|
460
|
+
): RawStep[] {
|
|
461
|
+
const numericQuery = ep.parameters.filter(
|
|
462
|
+
(p) => p.in === "query" && isNumericSchema(p.schema as OpenAPIV3.SchemaObject | undefined),
|
|
463
|
+
);
|
|
464
|
+
if (numericQuery.length === 0) return [];
|
|
465
|
+
|
|
466
|
+
const out: RawStep[] = [];
|
|
467
|
+
for (const param of numericQuery) {
|
|
468
|
+
for (const { value, label } of NUMERIC_BAD_VALUES) {
|
|
469
|
+
if (out.length >= budget) break;
|
|
470
|
+
const basePath = renderPath(ep, null, { useRealParents });
|
|
471
|
+
const sep = basePath.includes("?") ? "&" : "?";
|
|
472
|
+
const pathWithQuery = `${basePath}${sep}${param.name}=${encodeURIComponent(value)}`;
|
|
473
|
+
out.push(
|
|
474
|
+
buildStep(ep, schemes, {
|
|
475
|
+
name: `query ${param.name}=${JSON.stringify(value)} (${label}) — must reject (no 5xx)`,
|
|
476
|
+
pathOverride: pathWithQuery,
|
|
477
|
+
useRealParents,
|
|
478
|
+
}),
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return out;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function probeNumericPathParams(
|
|
486
|
+
ep: EndpointInfo,
|
|
487
|
+
schemes: SecuritySchemeInfo[],
|
|
488
|
+
budget: number,
|
|
489
|
+
useRealParents: boolean,
|
|
490
|
+
): RawStep[] {
|
|
491
|
+
const numericPath = ep.parameters.filter(
|
|
492
|
+
(p) => p.in === "path" && isNumericSchema(p.schema as OpenAPIV3.SchemaObject | undefined),
|
|
493
|
+
);
|
|
494
|
+
if (numericPath.length === 0) return [];
|
|
495
|
+
|
|
496
|
+
const out: RawStep[] = [];
|
|
497
|
+
for (const param of numericPath) {
|
|
498
|
+
for (const { value, label } of NUMERIC_BAD_VALUES) {
|
|
499
|
+
if (value === "") continue; // empty path segment yields a different endpoint
|
|
500
|
+
if (out.length >= budget) break;
|
|
501
|
+
const overriddenPath = renderPath(ep, { name: param.name, value }, { useRealParents });
|
|
502
|
+
out.push(
|
|
503
|
+
buildStep(ep, schemes, {
|
|
504
|
+
name: `path param ${param.name}=${JSON.stringify(value)} (${label}) — must reject (no 5xx)`,
|
|
505
|
+
pathOverride: overriddenPath,
|
|
506
|
+
useRealParents,
|
|
434
507
|
}),
|
|
435
508
|
);
|
|
436
509
|
}
|
|
@@ -465,62 +538,6 @@ function describeType(schema: OpenAPIV3.SchemaObject): string {
|
|
|
465
538
|
return schema.type ?? "any";
|
|
466
539
|
}
|
|
467
540
|
|
|
468
|
-
function hasJsonBody(ep: EndpointInfo): boolean {
|
|
469
|
-
return (
|
|
470
|
-
ep.method !== "GET" &&
|
|
471
|
-
ep.method !== "DELETE" &&
|
|
472
|
-
ep.requestBodyContentType === "application/json" &&
|
|
473
|
-
ep.requestBodySchema !== undefined
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function escapeRegex(s: string): string {
|
|
478
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function isMutating(method: string): boolean {
|
|
482
|
-
const m = method.toUpperCase();
|
|
483
|
-
return m === "POST" || m === "PUT" || m === "PATCH";
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Find the DELETE endpoint that owns resources created by `ep`:
|
|
488
|
-
* - POST /collection → DELETE /collection/{id}
|
|
489
|
-
* - PUT /collection/{id} → DELETE /collection/{id} (same path)
|
|
490
|
-
* - PATCH /collection/{id} → DELETE /collection/{id} (same path)
|
|
491
|
-
*/
|
|
492
|
-
function findDeleteCounterpart(
|
|
493
|
-
ep: EndpointInfo,
|
|
494
|
-
all: EndpointInfo[],
|
|
495
|
-
): EndpointInfo | undefined {
|
|
496
|
-
const m = ep.method.toUpperCase();
|
|
497
|
-
if (m === "POST") {
|
|
498
|
-
const re = new RegExp(`^${escapeRegex(ep.path)}/\\{[^}]+\\}$`);
|
|
499
|
-
return all.find(e => e.method.toUpperCase() === "DELETE" && !e.deprecated && re.test(e.path));
|
|
500
|
-
}
|
|
501
|
-
if (m === "PUT" || m === "PATCH") {
|
|
502
|
-
return all.find(e => e.method.toUpperCase() === "DELETE" && !e.deprecated && e.path === ep.path);
|
|
503
|
-
}
|
|
504
|
-
return undefined;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* Pick the response field that holds the new resource's id. Defaults to "id";
|
|
509
|
-
* falls back to the first integer / uuid property when no `id` field exists.
|
|
510
|
-
*/
|
|
511
|
-
function captureFieldFor(ep: EndpointInfo): string {
|
|
512
|
-
const success = ep.responses.find(r => r.statusCode >= 200 && r.statusCode < 300 && r.schema);
|
|
513
|
-
const schema = success?.schema;
|
|
514
|
-
if (schema?.properties) {
|
|
515
|
-
if ("id" in schema.properties) return "id";
|
|
516
|
-
for (const [name, propSchema] of Object.entries(schema.properties)) {
|
|
517
|
-
const s = propSchema as OpenAPIV3.SchemaObject;
|
|
518
|
-
if (s.type === "integer" || s.format === "uuid") return name;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
return "id";
|
|
522
|
-
}
|
|
523
|
-
|
|
524
541
|
/** Build a cleanup-DELETE step for a single mutating probe. The capture var
|
|
525
542
|
* must come from the paired probe step. If the probe didn't capture (e.g.
|
|
526
543
|
* the API correctly returned 4xx and no resource was created), the runner
|
|
@@ -537,6 +554,10 @@ function buildCleanupStep(
|
|
|
537
554
|
const headers = getAuthHeaders(deleteEp, schemes);
|
|
538
555
|
const step: RawStep = {
|
|
539
556
|
name: `cleanup leaked resource from "${probeStepName}"`,
|
|
557
|
+
source: {
|
|
558
|
+
generator: "negative-probe-cleanup",
|
|
559
|
+
endpoint: `DELETE ${deleteEp.path}`,
|
|
560
|
+
},
|
|
540
561
|
always: true,
|
|
541
562
|
DELETE: path,
|
|
542
563
|
expect: {
|
|
@@ -547,14 +568,6 @@ function buildCleanupStep(
|
|
|
547
568
|
return step;
|
|
548
569
|
}
|
|
549
570
|
|
|
550
|
-
function headersEqual(a: Record<string, string>, b: Record<string, string>): boolean {
|
|
551
|
-
const ka = Object.keys(a);
|
|
552
|
-
const kb = Object.keys(b);
|
|
553
|
-
if (ka.length !== kb.length) return false;
|
|
554
|
-
for (const k of ka) if (a[k] !== b[k]) return false;
|
|
555
|
-
return true;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
571
|
// ──────────────────────────────────────────────
|
|
559
572
|
// Public API
|
|
560
573
|
// ──────────────────────────────────────────────
|
|
@@ -563,6 +576,9 @@ export function generateNegativeProbes(opts: ProbeOptions): ProbeResult {
|
|
|
563
576
|
const { endpoints, securitySchemes } = opts;
|
|
564
577
|
const cap = opts.maxProbesPerEndpoint ?? 50;
|
|
565
578
|
const noCleanup = opts.noCleanup === true;
|
|
579
|
+
// TASK-135: default ON. Non-attacked path-params are emitted as runtime
|
|
580
|
+
// placeholders `{{name}}` and resolved from `.env.yaml` by `zond run`.
|
|
581
|
+
const useRealParents = opts.useRealParents !== false;
|
|
566
582
|
|
|
567
583
|
const suites: RawSuite[] = [];
|
|
568
584
|
const warnings: string[] = [];
|
|
@@ -577,17 +593,25 @@ export function generateNegativeProbes(opts: ProbeOptions): ProbeResult {
|
|
|
577
593
|
const remaining = () => Math.max(0, cap - steps.length);
|
|
578
594
|
|
|
579
595
|
// 1. Path-id probes (cheap, deterministic)
|
|
580
|
-
steps.push(...probeInvalidPathId(ep, securitySchemes, remaining()));
|
|
596
|
+
steps.push(...probeInvalidPathId(ep, securitySchemes, remaining(), useRealParents));
|
|
597
|
+
|
|
598
|
+
// 1b. Numeric query / path coercion probes (T67) — float-on-integer,
|
|
599
|
+
// negative, non-numeric, etc. Catches `GET /x?limit=1.5` → 500.
|
|
600
|
+
const numericQueryProbes = probeNumericQueryParams(ep, securitySchemes, remaining(), useRealParents);
|
|
601
|
+
steps.push(...numericQueryProbes);
|
|
602
|
+
const numericPathProbes = probeNumericPathParams(ep, securitySchemes, remaining(), useRealParents);
|
|
603
|
+
steps.push(...numericPathProbes);
|
|
604
|
+
const hasNumericCoercion = numericQueryProbes.length + numericPathProbes.length > 0;
|
|
581
605
|
|
|
582
606
|
// 2. Body probes (only for body-bearing methods)
|
|
583
|
-
const empty = probeEmptyBody(ep, securitySchemes);
|
|
607
|
+
const empty = probeEmptyBody(ep, securitySchemes, useRealParents);
|
|
584
608
|
if (empty && steps.length < cap) steps.push(empty);
|
|
585
609
|
|
|
586
|
-
steps.push(...probeMissingRequired(ep, securitySchemes, remaining()));
|
|
587
|
-
steps.push(...probeTypeConfusion(ep, securitySchemes, remaining()));
|
|
588
|
-
steps.push(...probeInvalidFormat(ep, securitySchemes, remaining()));
|
|
589
|
-
steps.push(...probeBoundaryString(ep, securitySchemes, remaining()));
|
|
590
|
-
steps.push(...probeInvalidEnum(ep, securitySchemes, remaining()));
|
|
610
|
+
steps.push(...probeMissingRequired(ep, securitySchemes, remaining(), useRealParents));
|
|
611
|
+
steps.push(...probeTypeConfusion(ep, securitySchemes, remaining(), useRealParents));
|
|
612
|
+
steps.push(...probeInvalidFormat(ep, securitySchemes, remaining(), useRealParents));
|
|
613
|
+
steps.push(...probeBoundaryString(ep, securitySchemes, remaining(), useRealParents));
|
|
614
|
+
steps.push(...probeInvalidEnum(ep, securitySchemes, remaining(), useRealParents));
|
|
591
615
|
|
|
592
616
|
if (steps.length === 0) {
|
|
593
617
|
skippedEndpoints++;
|
|
@@ -644,7 +668,17 @@ export function generateNegativeProbes(opts: ProbeOptions): ProbeResult {
|
|
|
644
668
|
const stem = endpointStem(ep);
|
|
645
669
|
const suite: RawSuite = {
|
|
646
670
|
name: `probe ${ep.method} ${ep.path}`,
|
|
647
|
-
tags: [
|
|
671
|
+
tags: [
|
|
672
|
+
"probe-validation",
|
|
673
|
+
"negative-input",
|
|
674
|
+
"no-5xx",
|
|
675
|
+
...(hasNumericCoercion ? ["query-coercion"] : []),
|
|
676
|
+
],
|
|
677
|
+
source: {
|
|
678
|
+
type: "probe-suite",
|
|
679
|
+
generator: "negative-probe",
|
|
680
|
+
endpoint: `${ep.method.toUpperCase()} ${ep.path}`,
|
|
681
|
+
},
|
|
648
682
|
fileStem: `probe-${stem}`,
|
|
649
683
|
base_url: "{{base_url}}",
|
|
650
684
|
...(suiteHeaders ? { headers: suiteHeaders } : {}),
|