@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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-269: shared loader for agent-authored `seed_body` overlays.
|
|
3
|
+
*
|
|
4
|
+
* Stateful checks already consume `.api-resources.local.yaml::seed_body`
|
|
5
|
+
* via `resolveCreateBody` (`core/checks/checks/_crud-helpers.ts`). Before
|
|
6
|
+
* ARV-269 the live probes (mass-assignment, security) did not — every
|
|
7
|
+
* baseline POST fell back to `generateFromSchema`, which strict APIs like
|
|
8
|
+
* Stripe v1 routinely reject as 400 with `INCONCLUSIVE-baseline`. The
|
|
9
|
+
* agent-authored overlay (ARV-187 pipeline) silently lost half its ROI.
|
|
10
|
+
*
|
|
11
|
+
* This helper builds the `Map<"METHOD path", SeedBodyConfig>` the probe
|
|
12
|
+
* runners consume. It's a CLI-layer helper because it crosses into
|
|
13
|
+
* `cli/commands/discover.ts` (the resource-map loader); keeping it under
|
|
14
|
+
* `core/probe` would inject a CLI dependency into the runtime.
|
|
15
|
+
*/
|
|
16
|
+
import { resolveApiCollection } from "../../resolve.ts";
|
|
17
|
+
import { readResourceMap } from "../discover.ts";
|
|
18
|
+
import type { SeedBodyConfig } from "../../../core/generator/resources-builder.ts";
|
|
19
|
+
|
|
20
|
+
/** Parse the "METHOD /path" labels used in `.api-resources.yaml`. */
|
|
21
|
+
function parseLabel(label: string): { method: string; path: string } | null {
|
|
22
|
+
const parts = label.trim().split(/\s+/);
|
|
23
|
+
if (parts.length < 2) return null;
|
|
24
|
+
return { method: parts[0]!.toUpperCase(), path: parts[1]! };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns undefined when no API context is in scope (raw `--spec`
|
|
29
|
+
* invocation) or when no resource declares `seed_body`. Probe runners
|
|
30
|
+
* treat undefined as "no overlay" and keep the legacy generator path.
|
|
31
|
+
*/
|
|
32
|
+
export async function loadSeedBodyOverlays(
|
|
33
|
+
apiName: string | undefined,
|
|
34
|
+
dbPath?: string,
|
|
35
|
+
): Promise<Map<string, SeedBodyConfig> | undefined> {
|
|
36
|
+
if (!apiName) return undefined;
|
|
37
|
+
const col = resolveApiCollection(apiName, dbPath);
|
|
38
|
+
if ("error" in col || !col.baseDir) return undefined;
|
|
39
|
+
const map = await readResourceMap(col.baseDir);
|
|
40
|
+
if (!map) return undefined;
|
|
41
|
+
const out = new Map<string, SeedBodyConfig>();
|
|
42
|
+
for (const r of map.resources) {
|
|
43
|
+
if (!r.seed_body || !r.endpoints?.create) continue;
|
|
44
|
+
const parsed = parseLabel(r.endpoints.create);
|
|
45
|
+
if (!parsed) continue;
|
|
46
|
+
out.set(`${parsed.method} ${parsed.path}`, {
|
|
47
|
+
contentType: r.seed_body.content_type,
|
|
48
|
+
body: r.seed_body.body,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return out.size > 0 ? out : undefined;
|
|
52
|
+
}
|
|
@@ -17,6 +17,7 @@ import { printMutationBanner, countCleanupFailures } from "../../../core/probe/s
|
|
|
17
17
|
import { MassAssignmentProbe } from "../../../core/probe/mass-assignment-probe-class.ts";
|
|
18
18
|
import { summarizeDryRun, formatDryRunDigest } from "../../../core/probe/dry-run-envelope.ts";
|
|
19
19
|
import { compileOperationFilter } from "../../../core/selectors/operation-filter.ts";
|
|
20
|
+
import { loadSeedBodyOverlays } from "./_seed-bodies.ts";
|
|
20
21
|
import type { EndpointVerdict, MassAssignmentResult } from "../../../core/probe/mass-assignment-probe.ts";
|
|
21
22
|
import type { ProbeEndpointResult, ProbeEndpointStatus, ProbeFindingSeverity } from "../../../core/probe/types.ts";
|
|
22
23
|
|
|
@@ -85,6 +86,17 @@ export interface ProbeMassAssignmentOptions {
|
|
|
85
86
|
* spec-extension support (x-zond-suspect-fields) is tracked in
|
|
86
87
|
* ARV-189. */
|
|
87
88
|
suspectField?: string[];
|
|
89
|
+
/** ARV-265: collection name for audit-coverage attribution. Set by
|
|
90
|
+
* the CLI when --api / current-api resolves; left undefined for
|
|
91
|
+
* bare-spec invocations. */
|
|
92
|
+
apiName?: string;
|
|
93
|
+
/** ARV-302: cap the number of endpoints probed in this run (after
|
|
94
|
+
* --include / --exclude / --tag filters). Used by `zond audit
|
|
95
|
+
* --budget` so probe stages stay inside a wall-clock budget instead
|
|
96
|
+
* of unbounded scanning (Stripe: 587 endpoints, 3.6k probes — silent
|
|
97
|
+
* 10+ min). When the cap trims the set, a warning is emitted so the
|
|
98
|
+
* user knows the run was partial. */
|
|
99
|
+
maxEndpoints?: number;
|
|
88
100
|
}
|
|
89
101
|
|
|
90
102
|
export async function probeMassAssignmentCommand(
|
|
@@ -131,6 +143,23 @@ export async function probeMassAssignmentCommand(
|
|
|
131
143
|
}
|
|
132
144
|
endpoints = endpoints.filter(compiled.filter);
|
|
133
145
|
}
|
|
146
|
+
// ARV-302: --max-endpoints cap — applied after include/exclude/tag
|
|
147
|
+
// filters so the user-visible filter intent is preserved. The cap
|
|
148
|
+
// is a coarse time-budget knob (`zond audit --budget standard` →
|
|
149
|
+
// 50 endpoints), not a sampling strategy: take the first N from
|
|
150
|
+
// the filtered list. A warning fires so partial output is obvious.
|
|
151
|
+
let cappedEndpoints = false;
|
|
152
|
+
if (typeof options.maxEndpoints === "number" && options.maxEndpoints > 0
|
|
153
|
+
&& endpoints.length > options.maxEndpoints) {
|
|
154
|
+
const total = endpoints.length;
|
|
155
|
+
endpoints = endpoints.slice(0, options.maxEndpoints);
|
|
156
|
+
cappedEndpoints = true;
|
|
157
|
+
if (!options.json) {
|
|
158
|
+
process.stderr.write(
|
|
159
|
+
`zond: probe mass-assignment capped at ${options.maxEndpoints}/${total} endpoints (--max-endpoints) — pass --max-endpoints <N> or --budget full to widen.\n`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
134
163
|
|
|
135
164
|
// Load env vars (base_url, auth_token, api_key, path-param overrides).
|
|
136
165
|
let vars: Record<string, string> = {};
|
|
@@ -160,6 +189,24 @@ export async function probeMassAssignmentCommand(
|
|
|
160
189
|
options: {},
|
|
161
190
|
});
|
|
162
191
|
const data = summarizeDryRun(plan);
|
|
192
|
+
// ARV-317: persist the planned inventory when --output is given, so it
|
|
193
|
+
// survives beyond stdout (an agent can read it back). --emit-tests is
|
|
194
|
+
// skipped on dry-run — there are no findings to turn into regression
|
|
195
|
+
// suites; the plan digest is the dry-run deliverable.
|
|
196
|
+
// ARV-321: say so explicitly. A silent no-op read as a bug on the live
|
|
197
|
+
// Stripe run (report-zond friction, 2026-07-02) — the target dir stayed
|
|
198
|
+
// empty with zero signal that --emit-tests was ignored.
|
|
199
|
+
if (options.emitTests) {
|
|
200
|
+
printWarning(`--emit-tests skipped: safe/dry-run mode has no live verdicts to lock in as regression suites. Re-run with --live to emit ${options.emitTests}.`);
|
|
201
|
+
}
|
|
202
|
+
if (options.output) {
|
|
203
|
+
const payload = options.report === "json"
|
|
204
|
+
? JSON.stringify(data, null, 2)
|
|
205
|
+
: formatDryRunDigest(plan);
|
|
206
|
+
await mkdir(join(options.output, "..").replace(/\/\.$/, ""), { recursive: true }).catch(() => {});
|
|
207
|
+
rotateOutputTarget(options.output, { overwrite: options.overwrite });
|
|
208
|
+
await writeFile(options.output, payload + "\n", "utf-8");
|
|
209
|
+
}
|
|
163
210
|
if (options.json) {
|
|
164
211
|
printJson(jsonOk("probe-mass-assignment", data));
|
|
165
212
|
} else {
|
|
@@ -180,15 +227,54 @@ export async function probeMassAssignmentCommand(
|
|
|
180
227
|
// is off — this banner is about the cleanup-pass, too.
|
|
181
228
|
printMutationBanner("probe-mass-assignment", vars, { quiet: options.json === true });
|
|
182
229
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
230
|
+
// ARV-265: capture HTTP touches for audit-coverage. Dry-run never
|
|
231
|
+
// reaches this branch (early return above), so AC#5 holds.
|
|
232
|
+
const { withHttpAudit, beginAuditRun, finalizeAuditRun, auditRecordToCase, checksPersistEnabled } =
|
|
233
|
+
await import("../../../core/audit/persist.ts");
|
|
234
|
+
const auditEnabled = checksPersistEnabled();
|
|
235
|
+
// ARV-269: lift agent-authored `seed_body` overlays out of
|
|
236
|
+
// `.api-resources.local.yaml` so create-endpoint baselines win against
|
|
237
|
+
// strict validators. No-op when --api isn't set (raw --spec invocation).
|
|
238
|
+
const seedBodies = await loadSeedBodyOverlays(options.apiName);
|
|
239
|
+
const { value: result, records: auditRecords } = await withHttpAudit(async () =>
|
|
240
|
+
runMassAssignmentProbes({
|
|
241
|
+
endpoints,
|
|
242
|
+
securitySchemes,
|
|
243
|
+
vars,
|
|
244
|
+
noCleanup: options.noCleanup,
|
|
245
|
+
timeoutMs: options.timeoutMs,
|
|
246
|
+
discover: !options.noDiscover,
|
|
247
|
+
extraSuspectFields: parseSuspectFieldFlags(options.suspectField),
|
|
248
|
+
seedBodies,
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
if (auditEnabled && auditRecords.length > 0) {
|
|
253
|
+
try {
|
|
254
|
+
const { getDb } = await import("../../../db/schema.ts");
|
|
255
|
+
const { findCollectionByNameOrId } = await import("../../../db/queries.ts");
|
|
256
|
+
const { readCurrentSession } = await import("../../../core/context/session.ts");
|
|
257
|
+
getDb();
|
|
258
|
+
const collectionId = options.apiName ? findCollectionByNameOrId(options.apiName)?.id : undefined;
|
|
259
|
+
const session = readCurrentSession();
|
|
260
|
+
const runId = beginAuditRun({
|
|
261
|
+
runKind: "probe",
|
|
262
|
+
...(collectionId != null ? { collectionId } : {}),
|
|
263
|
+
...(session?.id ? { sessionId: session.id } : {}),
|
|
264
|
+
tags: ["probe", "mass-assignment"],
|
|
265
|
+
});
|
|
266
|
+
const suiteFile = `apis/${options.apiName ?? "_"}/probes/mass-assignment.yaml`;
|
|
267
|
+
finalizeAuditRun(runId, auditRecords.map((rec) =>
|
|
268
|
+
auditRecordToCase(rec, {
|
|
269
|
+
suiteName: "probe/mass-assignment",
|
|
270
|
+
suiteFile,
|
|
271
|
+
testName: `mass-assignment::${rec.request.method.toUpperCase()} ${rec.request.url}`,
|
|
272
|
+
}),
|
|
273
|
+
));
|
|
274
|
+
} catch (err) {
|
|
275
|
+
process.stderr.write(`zond: audit persistence failed (${(err as Error).message}).\n`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
192
278
|
|
|
193
279
|
// ARV-252: filter verdicts for display under the evidence-chain
|
|
194
280
|
// principle. Silently-ignored (correct framework behaviour) never
|
|
@@ -246,7 +332,12 @@ export async function probeMassAssignmentCommand(
|
|
|
246
332
|
by_status: maByStatus(structuredEndpoints),
|
|
247
333
|
},
|
|
248
334
|
orphans,
|
|
249
|
-
warnings:
|
|
335
|
+
warnings: cappedEndpoints
|
|
336
|
+
? [
|
|
337
|
+
...result.warnings,
|
|
338
|
+
`--max-endpoints capped this run at ${options.maxEndpoints}; pass --max-endpoints <N> or --budget full to widen.`,
|
|
339
|
+
]
|
|
340
|
+
: result.warnings,
|
|
250
341
|
emittedTests: emittedSuites,
|
|
251
342
|
}),
|
|
252
343
|
);
|
|
@@ -20,6 +20,7 @@ import { persistVerdictsAsOrphans } from "../../../core/probe/orphan-tracker.ts"
|
|
|
20
20
|
import { SecurityProbe } from "../../../core/probe/security-probe-class.ts";
|
|
21
21
|
import { summarizeDryRun } from "../../../core/probe/dry-run-envelope.ts";
|
|
22
22
|
import { compileOperationFilter } from "../../../core/selectors/operation-filter.ts";
|
|
23
|
+
import { loadSeedBodyOverlays } from "./_seed-bodies.ts";
|
|
23
24
|
|
|
24
25
|
interface Buckets {
|
|
25
26
|
high: number;
|
|
@@ -95,6 +96,11 @@ export interface ProbeSecurityOptions {
|
|
|
95
96
|
* reflection — sanitization signal only). Hidden by default since
|
|
96
97
|
* they carry single_signal proof with no exploit pathway. */
|
|
97
98
|
verbose?: boolean;
|
|
99
|
+
/** ARV-302: cap the number of endpoints probed in this run (after
|
|
100
|
+
* --include / --exclude / --tag filters). Used by `zond audit
|
|
101
|
+
* --budget` to keep the security stage inside a wall-clock budget
|
|
102
|
+
* on big specs (Stripe: 587 endpoints) instead of unbounded scanning. */
|
|
103
|
+
maxEndpoints?: number;
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
function parseClasses(input: string): SecurityClass[] | string {
|
|
@@ -157,6 +163,21 @@ export async function probeSecurityCommand(
|
|
|
157
163
|
}
|
|
158
164
|
endpoints = endpoints.filter(compiled.filter);
|
|
159
165
|
}
|
|
166
|
+
// ARV-302: --max-endpoints cap — see probe/mass-assignment.ts for the
|
|
167
|
+
// rationale. Applied after user-visible filters; a warning fires so
|
|
168
|
+
// partial output is obvious.
|
|
169
|
+
let cappedEndpoints = false;
|
|
170
|
+
if (typeof options.maxEndpoints === "number" && options.maxEndpoints > 0
|
|
171
|
+
&& endpoints.length > options.maxEndpoints) {
|
|
172
|
+
const total = endpoints.length;
|
|
173
|
+
endpoints = endpoints.slice(0, options.maxEndpoints);
|
|
174
|
+
cappedEndpoints = true;
|
|
175
|
+
if (!options.json) {
|
|
176
|
+
process.stderr.write(
|
|
177
|
+
`zond: probe security capped at ${options.maxEndpoints}/${total} endpoints (--max-endpoints) — pass --max-endpoints <N> or --budget full to widen.\n`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
160
181
|
|
|
161
182
|
let vars: Record<string, string> = {};
|
|
162
183
|
if (options.env) {
|
|
@@ -196,10 +217,26 @@ export async function probeSecurityCommand(
|
|
|
196
217
|
options: { isolated: options.isolated === true },
|
|
197
218
|
});
|
|
198
219
|
const data = summarizeDryRun(plan);
|
|
220
|
+
const { formatDryRunDigest } = await import("../../../core/probe/dry-run-envelope.ts");
|
|
221
|
+
// ARV-317: persist the planned inventory when --output is given (parity
|
|
222
|
+
// with probe mass-assignment). --emit-tests is skipped on dry-run —
|
|
223
|
+
// there are no findings to turn into regression suites.
|
|
224
|
+
// ARV-321: say so explicitly — see mass-assignment.ts for the friction
|
|
225
|
+
// that prompted this.
|
|
226
|
+
if (options.emitTests) {
|
|
227
|
+
printWarning(`--emit-tests skipped: safe/dry-run mode has no live verdicts to lock in as regression suites. Re-run with --live to emit ${options.emitTests}.`);
|
|
228
|
+
}
|
|
229
|
+
if (options.output) {
|
|
230
|
+
const payload = options.report === "json"
|
|
231
|
+
? JSON.stringify(data, null, 2)
|
|
232
|
+
: formatDryRunDigest(plan);
|
|
233
|
+
await mkdir(join(options.output, "..").replace(/\/\.$/, ""), { recursive: true }).catch(() => {});
|
|
234
|
+
rotateOutputTarget(options.output, { overwrite: options.overwrite });
|
|
235
|
+
await writeFile(options.output, payload + "\n", "utf-8");
|
|
236
|
+
}
|
|
199
237
|
if (options.json) {
|
|
200
238
|
printJson(jsonOk("probe-security", data));
|
|
201
239
|
} else {
|
|
202
|
-
const { formatDryRunDigest } = await import("../../../core/probe/dry-run-envelope.ts");
|
|
203
240
|
console.log(formatDryRunDigest(plan));
|
|
204
241
|
}
|
|
205
242
|
return 0;
|
|
@@ -210,17 +247,55 @@ export async function probeSecurityCommand(
|
|
|
210
247
|
// travel in the envelope instead).
|
|
211
248
|
printMutationBanner("probe-security", vars, { quiet: options.json === true });
|
|
212
249
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
250
|
+
// ARV-265: capture every HTTP request the probe sends so audit-coverage
|
|
251
|
+
// sees the touches. Dry-run never reaches this branch (early return
|
|
252
|
+
// above), so AC#5 holds — only live probes pollute the run table.
|
|
253
|
+
const { withHttpAudit, beginAuditRun, finalizeAuditRun, auditRecordToCase, checksPersistEnabled } =
|
|
254
|
+
await import("../../../core/audit/persist.ts");
|
|
255
|
+
const auditEnabled = checksPersistEnabled();
|
|
256
|
+
// ARV-269: same overlay-lift as probe-mass-assignment.
|
|
257
|
+
const seedBodies = await loadSeedBodyOverlays(options.apiName);
|
|
258
|
+
const { value: result, records: auditRecords } = await withHttpAudit(async () =>
|
|
259
|
+
runSecurityProbes({
|
|
260
|
+
endpoints,
|
|
261
|
+
securitySchemes,
|
|
262
|
+
vars,
|
|
263
|
+
classes,
|
|
264
|
+
noCleanup: options.noCleanup,
|
|
265
|
+
timeoutMs: options.timeoutMs,
|
|
266
|
+
dryRun: options.dryRun,
|
|
267
|
+
isolated: options.isolated === true,
|
|
268
|
+
allowLeaks: options.allowLeaks === true,
|
|
269
|
+
seedBodies,
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
if (auditEnabled && auditRecords.length > 0) {
|
|
274
|
+
try {
|
|
275
|
+
const { getDb } = await import("../../../db/schema.ts");
|
|
276
|
+
const { findCollectionByNameOrId } = await import("../../../db/queries.ts");
|
|
277
|
+
const { readCurrentSession } = await import("../../../core/context/session.ts");
|
|
278
|
+
getDb();
|
|
279
|
+
const collectionId = options.apiName ? findCollectionByNameOrId(options.apiName)?.id : undefined;
|
|
280
|
+
const session = readCurrentSession();
|
|
281
|
+
const runId = beginAuditRun({
|
|
282
|
+
runKind: "probe",
|
|
283
|
+
...(collectionId != null ? { collectionId } : {}),
|
|
284
|
+
...(session?.id ? { sessionId: session.id } : {}),
|
|
285
|
+
tags: ["probe", "security", ...classes],
|
|
286
|
+
});
|
|
287
|
+
const suiteFile = `apis/${options.apiName ?? "_"}/probes/security.yaml`;
|
|
288
|
+
finalizeAuditRun(runId, auditRecords.map((rec) =>
|
|
289
|
+
auditRecordToCase(rec, {
|
|
290
|
+
suiteName: "probe/security",
|
|
291
|
+
suiteFile,
|
|
292
|
+
testName: `security::${rec.request.method.toUpperCase()} ${rec.request.url}`,
|
|
293
|
+
}),
|
|
294
|
+
));
|
|
295
|
+
} catch (err) {
|
|
296
|
+
process.stderr.write(`zond: audit persistence failed (${(err as Error).message}).\n`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
224
299
|
|
|
225
300
|
// ARV-253: filter verdicts for display under the evidence-chain
|
|
226
301
|
// principle. INFO-severity findings (CRLF accepted, no reflection
|
|
@@ -328,6 +403,14 @@ export async function probeSecurityCommand(
|
|
|
328
403
|
},
|
|
329
404
|
orphans,
|
|
330
405
|
emittedTests: emittedSuites,
|
|
406
|
+
// ARV-302: surface the partial-run warning so JSON consumers
|
|
407
|
+
// see that --max-endpoints trimmed the set (otherwise the cap
|
|
408
|
+
// is invisible in machine-readable output).
|
|
409
|
+
...(cappedEndpoints ? {
|
|
410
|
+
warnings: [
|
|
411
|
+
`--max-endpoints capped this run at ${options.maxEndpoints}; pass --max-endpoints <N> or --budget full to widen.`,
|
|
412
|
+
],
|
|
413
|
+
} : {}),
|
|
331
414
|
}),
|
|
332
415
|
);
|
|
333
416
|
} else {
|
|
@@ -31,6 +31,8 @@ export interface ProbeWebhooksOptions {
|
|
|
31
31
|
output?: string;
|
|
32
32
|
/** Envelope mode — wraps result in {ok, command, data, errors}. */
|
|
33
33
|
json?: boolean;
|
|
34
|
+
/** API name for audit-coverage attribution. */
|
|
35
|
+
apiName?: string;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
function severityCount(findings: WebhookFinding[]): { high: number; low: number } {
|
|
@@ -26,7 +26,8 @@ import { getApi } from "../util/api-context.ts";
|
|
|
26
26
|
import { existsSync } from "fs";
|
|
27
27
|
import { join, dirname } from "node:path";
|
|
28
28
|
import { parsePositiveInt } from "../argv.ts";
|
|
29
|
-
import { printError } from "../output.ts";
|
|
29
|
+
import { printError, printWarning } from "../output.ts";
|
|
30
|
+
import { SAFE_HELP, LIVE_HELP, resolveLive } from "../safe-live.ts";
|
|
30
31
|
import { jsonError, printJson } from "../json-envelope.ts";
|
|
31
32
|
import { loadEnvMeta } from "../../core/parser/variables.ts";
|
|
32
33
|
import { resolveTimeoutMs } from "../../core/workspace/config.ts";
|
|
@@ -190,16 +191,49 @@ function defineProbeStatic(parent: Command, name: string): void {
|
|
|
190
191
|
.option("--max-per-endpoint <N>", "Cap negative-input probes per endpoint (default 50)", parsePositiveInt("--max-per-endpoint"))
|
|
191
192
|
.option("--no-cleanup", "Skip emission of follow-up DELETE cleanup steps for mutating probes (use in namespace-isolated test envs)")
|
|
192
193
|
.option("--use-synthetic-parents", "Bake synthetic-by-type values into all path params (legacy). By default, non-attacked path params are emitted as {{name}} and resolved from .env.yaml at run time — needed to reach the leaf validator on nested paths (TASK-135).")
|
|
193
|
-
|
|
194
|
-
|
|
194
|
+
// ARV-225: probe static --include is a CLASS LIST ({validation, methods}),
|
|
195
|
+
// while sibling commands (probe security, probe mass-assignment, checks
|
|
196
|
+
// run) use SELECTOR grammar (path:/method:/tag:/operation-id:). Rename the
|
|
197
|
+
// canonical flag to --include-class / --exclude-class for clarity; keep
|
|
198
|
+
// --include / --exclude as deprecated aliases (warn on use).
|
|
199
|
+
.option("--include-class <classes>", "Comma-separated subset of {validation, methods} (default: both)")
|
|
200
|
+
.option("--exclude-class <classes>", "Comma-separated subset to skip (mutually exclusive with --include-class)")
|
|
201
|
+
.option("--include <classes>", "[deprecated, use --include-class] Comma-separated subset of {validation, methods}")
|
|
202
|
+
.option("--exclude <classes>", "[deprecated, use --exclude-class] Comma-separated subset to skip")
|
|
203
|
+
// ARV-299: static only *generates* probe suites (it never sends live
|
|
204
|
+
// traffic), so it is always safe. The flags exist for vocabulary parity
|
|
205
|
+
// with the sibling subcommands; --live is a no-op here and says so.
|
|
206
|
+
.option("--safe", SAFE_HELP)
|
|
207
|
+
.option("--live", LIVE_HELP)
|
|
195
208
|
.action(async (specPos: string | undefined, optsArg, cmdRef: Command) => {
|
|
209
|
+
if (optsArg.live === true) {
|
|
210
|
+
printWarning("probe static only generates suites and never sends live traffic — --safe/--live have no effect here. Run the emitted suites with `zond run … --live` to execute them.");
|
|
211
|
+
}
|
|
196
212
|
// ARV-33: see resolveProbeApi — keep the chain consistent with the
|
|
197
213
|
// sibling subcommands (mass-assignment, security).
|
|
198
214
|
const apiName = resolveProbeApi(optsArg.api, cmdRef);
|
|
199
215
|
const resolved = resolveSpecArg(specPos, apiName, optsArg.db);
|
|
200
216
|
if ("error" in resolved) { printError(resolved.error); process.exitCode = 2; return; }
|
|
201
217
|
|
|
202
|
-
|
|
218
|
+
// ARV-225: prefer --include-class / --exclude-class. Fall back to
|
|
219
|
+
// --include / --exclude with a one-line stderr deprecation note —
|
|
220
|
+
// these are the legacy names that collide semantically with the
|
|
221
|
+
// selector --include on sibling commands.
|
|
222
|
+
let includeClasses: string | undefined = optsArg.includeClass;
|
|
223
|
+
let excludeClasses: string | undefined = optsArg.excludeClass;
|
|
224
|
+
if (!includeClasses && optsArg.include) {
|
|
225
|
+
process.stderr.write(
|
|
226
|
+
"Warning: `probe static --include <classes>` is deprecated (class-list, not a selector — collides with probe security / checks run). Use --include-class.\n",
|
|
227
|
+
);
|
|
228
|
+
includeClasses = optsArg.include;
|
|
229
|
+
}
|
|
230
|
+
if (!excludeClasses && optsArg.exclude) {
|
|
231
|
+
process.stderr.write(
|
|
232
|
+
"Warning: `probe static --exclude <classes>` is deprecated. Use --exclude-class.\n",
|
|
233
|
+
);
|
|
234
|
+
excludeClasses = optsArg.exclude;
|
|
235
|
+
}
|
|
236
|
+
const r = resolveStaticClasses(includeClasses, excludeClasses);
|
|
203
237
|
if ("error" in r) { printError(r.error); process.exitCode = 2; return; }
|
|
204
238
|
|
|
205
239
|
// ARV-30: derive --output from the registered API's base_dir when the
|
|
@@ -245,7 +279,9 @@ function defineProbeMassAssignment(parent: Command, name: string): void {
|
|
|
245
279
|
.option("--list-tags", "List available tags from spec and exit")
|
|
246
280
|
.option("--no-cleanup", "Skip follow-up DELETE for resources accidentally created by 2xx probes")
|
|
247
281
|
.option("--no-discover", "Disable auto-discovery of path-param fixtures via GET-on-list (TASK-92)")
|
|
248
|
-
.option("--dry-run", "Print which endpoints/fields would be attacked without sending requests (m-17 ARV-52)")
|
|
282
|
+
.option("--dry-run", "Print which endpoints/fields would be attacked without sending requests (m-17 ARV-52). Equivalent to the default --safe mode.")
|
|
283
|
+
.option("--safe", SAFE_HELP)
|
|
284
|
+
.option("--live", LIVE_HELP)
|
|
249
285
|
.option(
|
|
250
286
|
"--include <selector>",
|
|
251
287
|
"Filter operations (m-15 ARV-9 grammar: path:/users/.* | tag:Webhooks | method:POST,PATCH | operation-id:create.*). Repeatable.",
|
|
@@ -269,7 +305,8 @@ function defineProbeMassAssignment(parent: Command, name: string): void {
|
|
|
269
305
|
(v: string, prev: string[] = []) => prev.concat(v),
|
|
270
306
|
[] as string[],
|
|
271
307
|
)
|
|
272
|
-
.option("--emit-template <method:path>", "TASK-146: emit a ready-to-edit YAML probe template for one endpoint (e.g. \"POST:/users\") instead of running the live probe. Pairs `--output <file>` to write to disk (default: stdout). Use to drop down to manual catch-up after INCONCLUSIVE / INCONCLUSIVE-5XX verdicts without copy-pasting boilerplate from the skill.")
|
|
308
|
+
.option("--emit-template <method:path>", "TASK-146: emit a ready-to-edit YAML probe template for one endpoint (e.g. \"POST:/users\") instead of running the live probe. Pairs `--output <file>` to write to disk (default: stdout). Use to drop down to manual catch-up after INCONCLUSIVE / INCONCLUSIVE-5XX verdicts without copy-pasting boilerplate from the skill.")
|
|
309
|
+
.option("--max-endpoints <n>", "ARV-302: cap the number of endpoints probed in this run (after --include / --exclude / --tag filters). Used by `zond audit --budget` to keep probe stages inside a wall-clock budget instead of unbounded scanning.", parsePositiveInt("--max-endpoints"));
|
|
273
310
|
addProbeReportOutputOptions(sub);
|
|
274
311
|
sub.action(async (specPos: string | undefined, opts, cmd: Command) => {
|
|
275
312
|
// ARV-33: resolve --api via the same fallback chain as prepare-fixtures /
|
|
@@ -291,11 +328,27 @@ function defineProbeMassAssignment(parent: Command, name: string): void {
|
|
|
291
328
|
return;
|
|
292
329
|
}
|
|
293
330
|
|
|
331
|
+
// ARV-299: safe (default) → plan only, no live mutating traffic. The
|
|
332
|
+
// proven --dry-run path IS the safe path, so we just force it on when
|
|
333
|
+
// --live is absent (and say so, unless the user already asked for a
|
|
334
|
+
// dry-run explicitly).
|
|
335
|
+
const live = resolveLive(opts);
|
|
336
|
+
const effectiveDryRun = opts.dryRun === true || !live;
|
|
337
|
+
// ARV-348: fail loudly (see probe security) — --emit-tests needs live
|
|
338
|
+
// verdicts; safe/dry-run mode would leave an empty dir + exit 0.
|
|
339
|
+
if (opts.emitTests && !live) {
|
|
340
|
+
printError("--emit-tests requires --live: safe/dry-run mode produces no live verdicts to lock into regression suites. Re-run with --live against a throwaway/sandbox account, or drop --emit-tests to just plan.");
|
|
341
|
+
process.exitCode = 2;
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (!live && opts.dryRun !== true && !opts.listTags) {
|
|
345
|
+
printWarning("probe mass-assignment: safe mode (default) — planning only, no live attack traffic. Re-run with --live against a throwaway/sandbox account to send probes.");
|
|
346
|
+
}
|
|
294
347
|
// m-17 / ARV-52 + ARV-58: dry-run and list-tags paths tolerate a
|
|
295
348
|
// missing env file the way probe-security does — the user wants
|
|
296
349
|
// to inspect the plan / available tags, not hit a live API.
|
|
297
350
|
const envFile = resolveProbeEnv(opts.env, apiName, opts.db, {
|
|
298
|
-
tolerateMissing:
|
|
351
|
+
tolerateMissing: effectiveDryRun || opts.listTags === true,
|
|
299
352
|
});
|
|
300
353
|
if ("error" in envFile) { printError(envFile.error); process.exitCode = 2; return; }
|
|
301
354
|
const timeoutMs = await resolveProbeTimeout(opts.timeout, apiName, envFile.env);
|
|
@@ -305,6 +358,7 @@ function defineProbeMassAssignment(parent: Command, name: string): void {
|
|
|
305
358
|
process.exitCode = await probeMassAssignmentCommand({
|
|
306
359
|
specPath: resolved.spec,
|
|
307
360
|
env: envFile.env,
|
|
361
|
+
apiName,
|
|
308
362
|
output: rep.output,
|
|
309
363
|
emitTests: opts.emitTests,
|
|
310
364
|
tag: opts.tag,
|
|
@@ -314,12 +368,13 @@ function defineProbeMassAssignment(parent: Command, name: string): void {
|
|
|
314
368
|
timeoutMs,
|
|
315
369
|
overwrite: opts.overwrite === true,
|
|
316
370
|
json,
|
|
317
|
-
dryRun:
|
|
371
|
+
dryRun: effectiveDryRun,
|
|
318
372
|
include: Array.isArray(opts.include) && opts.include.length > 0 ? opts.include : undefined,
|
|
319
373
|
exclude: Array.isArray(opts.exclude) && opts.exclude.length > 0 ? opts.exclude : undefined,
|
|
320
374
|
report: rep.report,
|
|
321
375
|
verbose: opts.verbose === true,
|
|
322
376
|
suspectField: Array.isArray(opts.suspectField) && opts.suspectField.length > 0 ? opts.suspectField : undefined,
|
|
377
|
+
maxEndpoints: typeof opts.maxEndpoints === "number" ? opts.maxEndpoints : undefined,
|
|
323
378
|
});
|
|
324
379
|
});
|
|
325
380
|
}
|
|
@@ -355,12 +410,15 @@ function defineProbeSecurity(parent: Command, name: string): void {
|
|
|
355
410
|
(v: string, prev: string[] = []) => prev.concat(v),
|
|
356
411
|
[] as string[],
|
|
357
412
|
)
|
|
358
|
-
.option("--dry-run", "Print which endpoints/fields would be attacked without sending requests")
|
|
413
|
+
.option("--dry-run", "Print which endpoints/fields would be attacked without sending requests. Equivalent to the default --safe mode.")
|
|
414
|
+
.option("--safe", SAFE_HELP)
|
|
415
|
+
.option("--live", LIVE_HELP)
|
|
359
416
|
.option("--timeout <ms>", "Per-request timeout in ms (overrides apis/<name>/.env.yaml `timeoutMs` and zond.config.yml `defaults.timeout_ms`; default 30000)", parsePositiveInt("--timeout"))
|
|
360
417
|
.option(
|
|
361
418
|
"--verbose",
|
|
362
419
|
"ARV-253: surface INFO-severity findings (sanitization-signal-only, e.g. CRLF accepted but no reflection observed). Default hides them — they're single-signal proof with no exploit pathway.",
|
|
363
|
-
)
|
|
420
|
+
)
|
|
421
|
+
.option("--max-endpoints <n>", "ARV-302: cap the number of endpoints probed in this run (after --include / --exclude / --tag filters). Used by `zond audit --budget` to keep probe stages inside a wall-clock budget instead of unbounded scanning.", parsePositiveInt("--max-endpoints"));
|
|
364
422
|
addProbeReportOutputOptions(sub);
|
|
365
423
|
sub.action(async (classes: string | undefined, specPos: string | undefined, opts, cmd: Command) => {
|
|
366
424
|
// ARV-36: missing-arg path should list the available classes (parity
|
|
@@ -378,6 +436,22 @@ function defineProbeSecurity(parent: Command, name: string): void {
|
|
|
378
436
|
const apiName = resolveProbeApi(opts.api, cmd);
|
|
379
437
|
const resolved = resolveSpecArg(specPos, apiName, opts.db);
|
|
380
438
|
if ("error" in resolved) { printError(resolved.error); process.exitCode = 2; return; }
|
|
439
|
+
// ARV-299: safe (default) forces plan-only; live attack traffic needs --live.
|
|
440
|
+
const live = resolveLive(opts);
|
|
441
|
+
const effectiveDryRun = opts.dryRun === true || !live;
|
|
442
|
+
// ARV-348: fail loudly instead of silently producing an empty dir. Safe/
|
|
443
|
+
// dry-run mode has no live verdicts to lock into regression suites, so
|
|
444
|
+
// --emit-tests can't do anything — and a later `zond run <dir>` on the
|
|
445
|
+
// empty dir just prints "No test files found". One consistent message
|
|
446
|
+
// (—live), not the old contradictory "Re-run without --dry-run".
|
|
447
|
+
if (opts.emitTests && !live) {
|
|
448
|
+
printError("--emit-tests requires --live: safe/dry-run mode produces no live verdicts to lock into regression suites. Re-run with --live against a throwaway/sandbox account, or drop --emit-tests to just plan.");
|
|
449
|
+
process.exitCode = 2;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (!live && opts.dryRun !== true && !opts.listTags) {
|
|
453
|
+
printWarning("probe security: safe mode (default) — planning only, no live attack traffic. Re-run with --live against a throwaway/sandbox account to send probes.");
|
|
454
|
+
}
|
|
381
455
|
// probe-security tolerates a missing env (--dry-run path), so don't
|
|
382
456
|
// fail when --api is given but the env file isn't on disk yet.
|
|
383
457
|
const envFile = resolveProbeEnv(opts.env, apiName, opts.db, { tolerateMissing: true });
|
|
@@ -395,7 +469,7 @@ function defineProbeSecurity(parent: Command, name: string): void {
|
|
|
395
469
|
tag: opts.tag,
|
|
396
470
|
listTags: opts.listTags,
|
|
397
471
|
noCleanup: opts.cleanup === false,
|
|
398
|
-
dryRun:
|
|
472
|
+
dryRun: effectiveDryRun,
|
|
399
473
|
timeoutMs,
|
|
400
474
|
overwrite: opts.overwrite === true,
|
|
401
475
|
json,
|
|
@@ -406,6 +480,7 @@ function defineProbeSecurity(parent: Command, name: string): void {
|
|
|
406
480
|
include: Array.isArray(opts.include) && opts.include.length > 0 ? opts.include : undefined,
|
|
407
481
|
exclude: Array.isArray(opts.exclude) && opts.exclude.length > 0 ? opts.exclude : undefined,
|
|
408
482
|
verbose: opts.verbose === true,
|
|
483
|
+
maxEndpoints: typeof opts.maxEndpoints === "number" ? opts.maxEndpoints : undefined,
|
|
409
484
|
});
|
|
410
485
|
});
|
|
411
486
|
}
|
|
@@ -443,6 +518,7 @@ function defineProbeWebhooks(parent: Command, name: string): void {
|
|
|
443
518
|
report: rep.report,
|
|
444
519
|
output: rep.output,
|
|
445
520
|
json,
|
|
521
|
+
apiName,
|
|
446
522
|
});
|
|
447
523
|
});
|
|
448
524
|
}
|