@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,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `zond probe static` — TASK-300.
|
|
3
|
+
*
|
|
4
|
+
* Consolidates the two static-input probe classes (`validation` and
|
|
5
|
+
* `methods`) under one entry point. Both read the spec on disk and emit
|
|
6
|
+
* YAML suites without making HTTP calls; their old top-level subcommands
|
|
7
|
+
* are kept as deprecated aliases (one release window) that warn and
|
|
8
|
+
* dispatch through this command.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { generateNegativeProbes } from "../../../core/probe/negative-probe.ts";
|
|
12
|
+
import { generateMethodProbes } from "../../../core/probe/method-probe.ts";
|
|
13
|
+
import { loadSpecForProbe, writeProbeSuites } from "../../../core/probe/runner.ts";
|
|
14
|
+
import { printError, printSuccess, printWarning } from "../../output.ts";
|
|
15
|
+
import { jsonOk, jsonError, printJson } from "../../json-envelope.ts";
|
|
16
|
+
import { formatEta } from "../../../core/util/format-eta.ts";
|
|
17
|
+
|
|
18
|
+
export type ProbeStaticClass = "validation" | "methods";
|
|
19
|
+
const ALL_CLASSES: ProbeStaticClass[] = ["validation", "methods"];
|
|
20
|
+
|
|
21
|
+
/** ARV-249: print a scale block when probe generation lands a huge suite.
|
|
22
|
+
* A 10k-probe set under `--rate-limit 30` is >6 min of silent work in
|
|
23
|
+
* `zond run` — users assume it's hung and SIGKILL. Surfacing the math
|
|
24
|
+
* here is cheaper than wiring a progress reporter (which we do anyway in
|
|
25
|
+
* slice B), and points at `--max-per-endpoint` for sampling. */
|
|
26
|
+
export const LARGE_PROBE_THRESHOLD = 2000;
|
|
27
|
+
const ETA_RATE_LIMITS = [10, 30, 60] as const;
|
|
28
|
+
|
|
29
|
+
export function buildLargeProbeNotice(
|
|
30
|
+
totalProbes: number,
|
|
31
|
+
probedEndpoints: number,
|
|
32
|
+
): string[] {
|
|
33
|
+
if (totalProbes < LARGE_PROBE_THRESHOLD || probedEndpoints <= 0) return [];
|
|
34
|
+
const etaLine = ETA_RATE_LIMITS
|
|
35
|
+
.map((rl) => `--rate-limit ${rl} → ~${formatEta(totalProbes / rl)}`)
|
|
36
|
+
.join(" ");
|
|
37
|
+
const sampleK = 3;
|
|
38
|
+
const sampleTotal = Math.min(totalProbes, sampleK * probedEndpoints);
|
|
39
|
+
return [
|
|
40
|
+
`Large probe set: ${totalProbes} probe(s) across ${probedEndpoints} endpoint(s).`,
|
|
41
|
+
` Estimated zond run time at common rate-limits:`,
|
|
42
|
+
` ${etaLine}`,
|
|
43
|
+
` To sample, re-run with --max-per-endpoint ${sampleK} (~${sampleTotal} probe(s)).`,
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ProbeStaticOptions {
|
|
48
|
+
specPath: string;
|
|
49
|
+
output: string;
|
|
50
|
+
tag?: string;
|
|
51
|
+
maxPerEndpoint?: number;
|
|
52
|
+
noCleanup?: boolean;
|
|
53
|
+
useRealParents?: boolean;
|
|
54
|
+
json?: boolean;
|
|
55
|
+
listTags?: boolean;
|
|
56
|
+
/** Subset of {validation, methods}. Defaults to both. */
|
|
57
|
+
include?: ProbeStaticClass[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse `--include`/`--exclude` CSV into a set of {validation, methods}.
|
|
62
|
+
* Returns either the resolved list (preserving canonical order) or an
|
|
63
|
+
* error string suitable for `printError` / `jsonError`.
|
|
64
|
+
*/
|
|
65
|
+
export function resolveStaticClasses(
|
|
66
|
+
include: string | undefined,
|
|
67
|
+
exclude: string | undefined,
|
|
68
|
+
): { classes: ProbeStaticClass[] } | { error: string } {
|
|
69
|
+
if (include && exclude) {
|
|
70
|
+
return { error: "--include and --exclude are mutually exclusive" };
|
|
71
|
+
}
|
|
72
|
+
const parse = (csv: string): { ok: ProbeStaticClass[] } | { error: string } => {
|
|
73
|
+
const tokens = csv.split(",").map((s) => s.trim()).filter(Boolean);
|
|
74
|
+
if (tokens.length === 0) return { error: "empty class list" };
|
|
75
|
+
const out: ProbeStaticClass[] = [];
|
|
76
|
+
for (const t of tokens) {
|
|
77
|
+
if (t !== "validation" && t !== "methods") {
|
|
78
|
+
return { error: `unknown probe class "${t}" (allowed: validation, methods)` };
|
|
79
|
+
}
|
|
80
|
+
if (!out.includes(t)) out.push(t);
|
|
81
|
+
}
|
|
82
|
+
return { ok: out };
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (include) {
|
|
86
|
+
const r = parse(include);
|
|
87
|
+
if ("error" in r) return r;
|
|
88
|
+
return { classes: ALL_CLASSES.filter((c) => r.ok.includes(c)) };
|
|
89
|
+
}
|
|
90
|
+
if (exclude) {
|
|
91
|
+
const r = parse(exclude);
|
|
92
|
+
if ("error" in r) return r;
|
|
93
|
+
return { classes: ALL_CLASSES.filter((c) => !r.ok.includes(c)) };
|
|
94
|
+
}
|
|
95
|
+
return { classes: [...ALL_CLASSES] };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function probeStaticCommand(
|
|
99
|
+
options: ProbeStaticOptions,
|
|
100
|
+
): Promise<number> {
|
|
101
|
+
const include: ProbeStaticClass[] = options.include ?? [...ALL_CLASSES];
|
|
102
|
+
if (include.length === 0) {
|
|
103
|
+
const msg = "No probe classes selected (use --include or drop --exclude).";
|
|
104
|
+
if (options.json) printJson(jsonError("probe-static", [msg]));
|
|
105
|
+
else printError(msg);
|
|
106
|
+
return 2;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const loaded = await loadSpecForProbe({
|
|
111
|
+
specPath: options.specPath,
|
|
112
|
+
tag: options.tag,
|
|
113
|
+
listTags: options.listTags,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (loaded.kind === "tags") {
|
|
117
|
+
if (options.json) {
|
|
118
|
+
printJson(jsonOk("probe-static", { tags: loaded.tags }));
|
|
119
|
+
} else if (loaded.tags.length === 0) {
|
|
120
|
+
console.log("No tags found in spec.");
|
|
121
|
+
} else {
|
|
122
|
+
console.log("Available tags:");
|
|
123
|
+
for (const t of loaded.tags) console.log(` - ${t}`);
|
|
124
|
+
}
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (loaded.kind === "tag-not-found") {
|
|
129
|
+
const msg = `No endpoints tagged "${loaded.tag}". Available tags: ${loaded.available.length ? loaded.available.join(", ") : "(none)"}`;
|
|
130
|
+
if (options.json) printJson(jsonError("probe-static", [msg]));
|
|
131
|
+
else printWarning(msg);
|
|
132
|
+
return 2;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { endpoints, securitySchemes } = loaded;
|
|
136
|
+
if (endpoints.length === 0) {
|
|
137
|
+
const message = "No endpoints to probe.";
|
|
138
|
+
if (options.json) {
|
|
139
|
+
printJson(jsonOk("probe-static", { include, files: [], message }));
|
|
140
|
+
} else {
|
|
141
|
+
console.log(message);
|
|
142
|
+
}
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const data: {
|
|
147
|
+
include: ProbeStaticClass[];
|
|
148
|
+
outputDir: string;
|
|
149
|
+
validation?: {
|
|
150
|
+
files: Array<{ file: string; suite: string; tests: number }>;
|
|
151
|
+
probedEndpoints: number;
|
|
152
|
+
skippedEndpoints: number;
|
|
153
|
+
totalProbes: number;
|
|
154
|
+
warnings: string[];
|
|
155
|
+
};
|
|
156
|
+
methods?: {
|
|
157
|
+
files: Array<{ file: string; suite: string; tests: number }>;
|
|
158
|
+
probedPaths: number;
|
|
159
|
+
skippedPaths: number;
|
|
160
|
+
totalProbes: number;
|
|
161
|
+
message?: string;
|
|
162
|
+
};
|
|
163
|
+
} = { include, outputDir: options.output };
|
|
164
|
+
|
|
165
|
+
if (include.includes("validation")) {
|
|
166
|
+
const r = generateNegativeProbes({
|
|
167
|
+
endpoints,
|
|
168
|
+
securitySchemes,
|
|
169
|
+
maxProbesPerEndpoint: options.maxPerEndpoint,
|
|
170
|
+
noCleanup: options.noCleanup,
|
|
171
|
+
useRealParents: options.useRealParents,
|
|
172
|
+
});
|
|
173
|
+
const w = await writeProbeSuites({
|
|
174
|
+
output: options.output,
|
|
175
|
+
suites: r.suites,
|
|
176
|
+
command: "zond probe static --emit",
|
|
177
|
+
headerExample: `zond probe static --api <name> --output ${options.output}`,
|
|
178
|
+
});
|
|
179
|
+
data.validation = {
|
|
180
|
+
files: w.files,
|
|
181
|
+
probedEndpoints: r.probedEndpoints,
|
|
182
|
+
skippedEndpoints: r.skippedEndpoints,
|
|
183
|
+
totalProbes: r.totalProbes,
|
|
184
|
+
warnings: r.warnings,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (include.includes("methods")) {
|
|
189
|
+
const r = generateMethodProbes({ endpoints, securitySchemes });
|
|
190
|
+
const w = await writeProbeSuites({
|
|
191
|
+
output: options.output,
|
|
192
|
+
suites: r.suites,
|
|
193
|
+
command: "zond probe static --emit",
|
|
194
|
+
headerExample: `zond probe static --api <name> --output ${options.output}`,
|
|
195
|
+
});
|
|
196
|
+
const methodsBlock: NonNullable<typeof data.methods> = {
|
|
197
|
+
files: w.files,
|
|
198
|
+
probedPaths: r.probedPaths,
|
|
199
|
+
skippedPaths: r.skippedPaths,
|
|
200
|
+
totalProbes: r.totalProbes,
|
|
201
|
+
};
|
|
202
|
+
if (r.suites.length === 0) {
|
|
203
|
+
methodsBlock.message =
|
|
204
|
+
"Every path declares all of GET/POST/PUT/PATCH/DELETE — nothing to probe.";
|
|
205
|
+
}
|
|
206
|
+
data.methods = methodsBlock;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (options.json) {
|
|
210
|
+
printJson(jsonOk("probe-static", data));
|
|
211
|
+
} else {
|
|
212
|
+
const totalSuites =
|
|
213
|
+
(data.validation?.files.length ?? 0) + (data.methods?.files.length ?? 0);
|
|
214
|
+
const totalProbes =
|
|
215
|
+
(data.validation?.totalProbes ?? 0) + (data.methods?.totalProbes ?? 0);
|
|
216
|
+
printSuccess(
|
|
217
|
+
`Generated ${totalSuites} probe suite(s) with ${totalProbes} probe(s) in ${options.output}`,
|
|
218
|
+
);
|
|
219
|
+
if (data.validation) {
|
|
220
|
+
console.log(
|
|
221
|
+
` validation: ${data.validation.probedEndpoints} endpoint(s) probed, ${data.validation.skippedEndpoints} skipped (no probable surface)`,
|
|
222
|
+
);
|
|
223
|
+
for (const w of data.validation.warnings) printWarning(w);
|
|
224
|
+
}
|
|
225
|
+
if (data.methods) {
|
|
226
|
+
if (data.methods.message) {
|
|
227
|
+
console.log(` methods: ${data.methods.message}`);
|
|
228
|
+
} else {
|
|
229
|
+
console.log(
|
|
230
|
+
` methods: ${data.methods.probedPaths} path(s) probed, ${data.methods.skippedPaths} skipped (full method coverage)`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const probedEndpoints =
|
|
235
|
+
(data.validation?.probedEndpoints ?? 0) + (data.methods?.probedPaths ?? 0);
|
|
236
|
+
const notice = buildLargeProbeNotice(totalProbes, probedEndpoints);
|
|
237
|
+
if (notice.length > 0) {
|
|
238
|
+
console.log("");
|
|
239
|
+
printWarning(notice[0]!);
|
|
240
|
+
for (const line of notice.slice(1)) console.log(line);
|
|
241
|
+
}
|
|
242
|
+
console.log("");
|
|
243
|
+
console.log("Next steps:");
|
|
244
|
+
console.log(` zond run ${options.output} --report json # any 5xx → bug candidate`);
|
|
245
|
+
console.log(` zond db diagnose <run-id> # inspect failures`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return 0;
|
|
249
|
+
} catch (err) {
|
|
250
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
251
|
+
if (options.json) printJson(jsonError("probe-static", [message]));
|
|
252
|
+
else printError(message);
|
|
253
|
+
return 2;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `zond probe webhooks` (m-20 ARV-173) — offline shape-conformance for
|
|
3
|
+
* captured webhook events.
|
|
4
|
+
*
|
|
5
|
+
* Wire-up note: this probe is offline — it reads an ndjson event log,
|
|
6
|
+
* never opens a socket. m-20 keeps live receivers in the recipe
|
|
7
|
+
* (docs/recipes/webhook-receiver.md). The CLI surface is thin:
|
|
8
|
+
*
|
|
9
|
+
* zond probe webhooks --api stripe --event-log events.jsonl
|
|
10
|
+
* zond probe webhooks --api stripe --event-log events.jsonl --json
|
|
11
|
+
* zond probe webhooks --api stripe --event-log events.jsonl --report json --output drift.json
|
|
12
|
+
*
|
|
13
|
+
* Exit code 0 when zero findings, 1 when any HIGH (shape drift) is
|
|
14
|
+
* present, 2 on CLI / IO error. Low-severity findings (unknown event
|
|
15
|
+
* type, missing payload, malformed line) don't gate CI by default.
|
|
16
|
+
*/
|
|
17
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
18
|
+
import { resolve as resolvePath } from "node:path";
|
|
19
|
+
import { jsonOk, jsonError, printJson } from "../../json-envelope.ts";
|
|
20
|
+
import { printError, printSuccess, printWarning } from "../../output.ts";
|
|
21
|
+
import { parseEventLog, runWebhooksProbe, type WebhookFinding } from "../../../core/probe/webhooks-probe.ts";
|
|
22
|
+
|
|
23
|
+
export interface ProbeWebhooksOptions {
|
|
24
|
+
specPath: string;
|
|
25
|
+
eventLog: string;
|
|
26
|
+
/** Comma-separated event types to restrict to. Empty ⇒ all declared. */
|
|
27
|
+
only?: string;
|
|
28
|
+
/** OutputSpec — render markdown by default; `--report json` swaps. */
|
|
29
|
+
report?: "markdown" | "json";
|
|
30
|
+
/** When set, the rendered body lands in this file instead of stdout. */
|
|
31
|
+
output?: string;
|
|
32
|
+
/** Envelope mode — wraps result in {ok, command, data, errors}. */
|
|
33
|
+
json?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function severityCount(findings: WebhookFinding[]): { high: number; low: number } {
|
|
37
|
+
let high = 0, low = 0;
|
|
38
|
+
for (const f of findings) (f.severity === "high" ? high += 1 : low += 1);
|
|
39
|
+
return { high, low };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatMarkdown(spec: unknown, result: ReturnType<typeof runWebhooksProbe>, eventLog: string): string {
|
|
43
|
+
const lines: string[] = [];
|
|
44
|
+
lines.push(`# zond probe webhooks — ${eventLog}`);
|
|
45
|
+
lines.push("");
|
|
46
|
+
if (result.skip_reason) {
|
|
47
|
+
lines.push(`> skipped: ${result.skip_reason}`);
|
|
48
|
+
return lines.join("\n") + "\n";
|
|
49
|
+
}
|
|
50
|
+
const sev = severityCount(result.findings);
|
|
51
|
+
lines.push(`**Events analysed**: ${result.total_events}`);
|
|
52
|
+
lines.push(`**Declared event types**: ${result.declared_events.length} (${result.declared_events.slice(0, 5).join(", ")}${result.declared_events.length > 5 ? ", …" : ""})`);
|
|
53
|
+
lines.push(`**Findings**: ${result.findings.length} (HIGH: ${sev.high}, LOW: ${sev.low})`);
|
|
54
|
+
lines.push("");
|
|
55
|
+
if (Object.keys(result.by_type).length > 0) {
|
|
56
|
+
lines.push("## Per-type breakdown");
|
|
57
|
+
lines.push("");
|
|
58
|
+
lines.push("| Event | ok | drift | unknown |");
|
|
59
|
+
lines.push("|---|---:|---:|---:|");
|
|
60
|
+
const types = Object.keys(result.by_type).sort();
|
|
61
|
+
for (const t of types) {
|
|
62
|
+
const b = result.by_type[t]!;
|
|
63
|
+
lines.push(`| ${t} | ${b.ok} | ${b.drift} | ${b.unknown} |`);
|
|
64
|
+
}
|
|
65
|
+
lines.push("");
|
|
66
|
+
}
|
|
67
|
+
if (result.findings.length > 0) {
|
|
68
|
+
lines.push("## Findings");
|
|
69
|
+
lines.push("");
|
|
70
|
+
for (const f of result.findings.slice(0, 20)) {
|
|
71
|
+
lines.push(`- **${f.kind}** [${f.severity}] (line ${f.line}, type=\`${f.event_type ?? "?"}\`): ${f.message}`);
|
|
72
|
+
}
|
|
73
|
+
if (result.findings.length > 20) lines.push(`\n…and ${result.findings.length - 20} more.`);
|
|
74
|
+
}
|
|
75
|
+
// unused param: keep `spec` to keep the call-site informative; the
|
|
76
|
+
// probe core already mined what it needed.
|
|
77
|
+
void spec;
|
|
78
|
+
return lines.join("\n") + "\n";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function probeWebhooksCommand(options: ProbeWebhooksOptions): Promise<number> {
|
|
82
|
+
try {
|
|
83
|
+
const eventLogPath = resolvePath(options.eventLog);
|
|
84
|
+
let eventLogText: string;
|
|
85
|
+
try {
|
|
86
|
+
eventLogText = await readFile(eventLogPath, "utf-8");
|
|
87
|
+
} catch (e) {
|
|
88
|
+
const msg = `Cannot read --event-log "${options.eventLog}": ${(e as Error).message}`;
|
|
89
|
+
if (options.json) printJson(jsonError("probe-webhooks", [msg]));
|
|
90
|
+
else printError(msg);
|
|
91
|
+
return 2;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let specText: string;
|
|
95
|
+
try {
|
|
96
|
+
specText = await readFile(options.specPath, "utf-8");
|
|
97
|
+
} catch (e) {
|
|
98
|
+
const msg = `Cannot read spec at "${options.specPath}": ${(e as Error).message}`;
|
|
99
|
+
if (options.json) printJson(jsonError("probe-webhooks", [msg]));
|
|
100
|
+
else printError(msg);
|
|
101
|
+
return 2;
|
|
102
|
+
}
|
|
103
|
+
let spec: unknown;
|
|
104
|
+
try {
|
|
105
|
+
spec = JSON.parse(specText);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
const msg = `Spec is not valid JSON: ${(e as Error).message}`;
|
|
108
|
+
if (options.json) printJson(jsonError("probe-webhooks", [msg]));
|
|
109
|
+
else printError(msg);
|
|
110
|
+
return 2;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const { events, malformed } = parseEventLog(eventLogText);
|
|
114
|
+
const onlyTypes = options.only ? options.only.split(",").map(s => s.trim()).filter(Boolean) : undefined;
|
|
115
|
+
const result = runWebhooksProbe({ events, spec, onlyTypes });
|
|
116
|
+
// Prepend malformed findings — they came from the ndjson parser,
|
|
117
|
+
// not the schema validator, but the operator wants them in the
|
|
118
|
+
// same digest (one place to look).
|
|
119
|
+
result.findings = [...malformed, ...result.findings];
|
|
120
|
+
|
|
121
|
+
const sev = severityCount(result.findings);
|
|
122
|
+
const fmt: "markdown" | "json" = options.report ?? "markdown";
|
|
123
|
+
|
|
124
|
+
if (options.output) {
|
|
125
|
+
const payload = fmt === "json"
|
|
126
|
+
? JSON.stringify({ event_log: options.eventLog, ...result }, null, 2) + "\n"
|
|
127
|
+
: formatMarkdown(spec, result, options.eventLog);
|
|
128
|
+
await writeFile(options.output, payload, "utf-8");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (options.json) {
|
|
132
|
+
printJson(jsonOk("probe-webhooks", {
|
|
133
|
+
event_log: options.eventLog,
|
|
134
|
+
total_events: result.total_events,
|
|
135
|
+
declared_events: result.declared_events,
|
|
136
|
+
by_type: result.by_type,
|
|
137
|
+
summary: { high: sev.high, low: sev.low, total: result.findings.length },
|
|
138
|
+
skip_reason: result.skip_reason,
|
|
139
|
+
findings: result.findings,
|
|
140
|
+
}));
|
|
141
|
+
} else if (!options.output) {
|
|
142
|
+
if (fmt === "json") {
|
|
143
|
+
process.stdout.write(JSON.stringify({ event_log: options.eventLog, ...result }, null, 2) + "\n");
|
|
144
|
+
} else {
|
|
145
|
+
process.stdout.write(formatMarkdown(spec, result, options.eventLog));
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
printSuccess(`${fmt === "json" ? "Structured report" : "Digest"} written to ${options.output}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!options.json && sev.high > 0) {
|
|
152
|
+
printWarning(`${sev.high} HIGH-severity finding(s) — webhook payloads drift from declared schema.`);
|
|
153
|
+
}
|
|
154
|
+
return sev.high > 0 ? 1 : 0;
|
|
155
|
+
} catch (err) {
|
|
156
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
157
|
+
if (options.json) printJson(jsonError("probe-webhooks", [message]));
|
|
158
|
+
else printError(message);
|
|
159
|
+
return 2;
|
|
160
|
+
}
|
|
161
|
+
}
|