@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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live probe-runtime primitives shared by mass-assignment and security
|
|
3
|
+
* probes. TASK-185 (m-11) extracted the static spec/scaffold half into
|
|
4
|
+
* `runner.ts`; this module covers the per-endpoint primitives that
|
|
5
|
+
* were still duplicated between the two probe entry points.
|
|
6
|
+
*
|
|
7
|
+
* Scope: small, pure helpers — URL building, baseline body generation,
|
|
8
|
+
* JSON+auth header construction. Cleanup logic is intentionally NOT
|
|
9
|
+
* unified: mass-assignment uses fire-and-forget DELETE before attacks,
|
|
10
|
+
* security-probe uses snapshot/restore + retry-aware DELETE after
|
|
11
|
+
* attacks. Different invariants, different shapes — see TASK-189 notes.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { EndpointInfo, SecuritySchemeInfo } from "../generator/types.ts";
|
|
15
|
+
import { generateFromSchema } from "../generator/data-factory.ts";
|
|
16
|
+
import { substituteDeep, substituteString } from "../parser/variables.ts";
|
|
17
|
+
import { convertPath, liveAuthHeaders } from "./shared.ts";
|
|
18
|
+
import { encodeFormBody } from "../runner/form-encode.ts";
|
|
19
|
+
|
|
20
|
+
/** ARV-150: form-encoded mutating endpoint (Stripe v1 pattern).
|
|
21
|
+
* Stripe and other Rails/PHP APIs declare requestBody.content with ONLY
|
|
22
|
+
* application/x-www-form-urlencoded — the probes previously skipped
|
|
23
|
+
* every such endpoint, masking real mass-assignment vectors. */
|
|
24
|
+
export function isFormBody(ep: EndpointInfo): boolean {
|
|
25
|
+
return (
|
|
26
|
+
ep.requestBodyContentType === "application/x-www-form-urlencoded"
|
|
27
|
+
&& ep.requestBodySchema !== undefined
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Probes can drive either application/json or application/x-www-form-urlencoded
|
|
32
|
+
* endpoints. Anything else (multipart, octet-stream, …) still gets skipped —
|
|
33
|
+
* no general way to construct attack payloads without a body schema. */
|
|
34
|
+
export function hasProbeBody(ep: EndpointInfo): boolean {
|
|
35
|
+
if (ep.method === "GET" || ep.method === "DELETE") return false;
|
|
36
|
+
if (!ep.requestBodySchema) return false;
|
|
37
|
+
return (
|
|
38
|
+
ep.requestBodyContentType === "application/json"
|
|
39
|
+
|| ep.requestBodyContentType === "application/x-www-form-urlencoded"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Serialise an attack body using whichever content type the endpoint
|
|
44
|
+
* declares. Returns the wire-format string + the Content-Type to set. */
|
|
45
|
+
export function serializeProbeBody(
|
|
46
|
+
ep: EndpointInfo,
|
|
47
|
+
body: Record<string, unknown>,
|
|
48
|
+
): { content: string; contentType: string } {
|
|
49
|
+
if (isFormBody(ep)) {
|
|
50
|
+
return { content: encodeFormBody(body), contentType: "application/x-www-form-urlencoded" };
|
|
51
|
+
}
|
|
52
|
+
return { content: JSON.stringify(body), contentType: "application/json" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve an endpoint's URL against the live `base_url` + path-param
|
|
57
|
+
* substitutions. Returns the resolved URL and any leftover `{{var}}`
|
|
58
|
+
* markers the caller couldn't fill — use those to skip the endpoint
|
|
59
|
+
* with a meaningful reason.
|
|
60
|
+
*/
|
|
61
|
+
export function buildProbeUrl(
|
|
62
|
+
ep: EndpointInfo,
|
|
63
|
+
vars: Record<string, string>,
|
|
64
|
+
): { url: string; unresolved: string[] } {
|
|
65
|
+
const baseUrl = (vars["base_url"] ?? "").replace(/\/+$/, "");
|
|
66
|
+
const templated = `${baseUrl}${convertPath(ep.path)}`;
|
|
67
|
+
const url = String(substituteString(templated, vars));
|
|
68
|
+
const unresolved = Array.from(url.matchAll(/\{\{([^}]+)\}\}/g)).map(m => m[1]!);
|
|
69
|
+
return { url, unresolved };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Standard probe headers: JSON content-type/accept plus the resolved
|
|
74
|
+
* auth header for the endpoint. Empty `liveAuthHeaders` is fine — the
|
|
75
|
+
* spread is a no-op for unauthenticated endpoints.
|
|
76
|
+
*/
|
|
77
|
+
export function buildJsonAuthHeaders(
|
|
78
|
+
ep: EndpointInfo,
|
|
79
|
+
schemes: SecuritySchemeInfo[],
|
|
80
|
+
vars: Record<string, string>,
|
|
81
|
+
): Record<string, string> {
|
|
82
|
+
return {
|
|
83
|
+
"content-type": "application/json",
|
|
84
|
+
accept: "application/json",
|
|
85
|
+
...liveAuthHeaders(ep, schemes, vars),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** ARV-150: like buildJsonAuthHeaders but picks the Content-Type from the
|
|
90
|
+
* endpoint's spec (form-urlencoded for Stripe v1, JSON otherwise). Accept
|
|
91
|
+
* stays JSON — the server still answers in JSON even when the body is
|
|
92
|
+
* form-encoded. */
|
|
93
|
+
export function buildBodyAuthHeaders(
|
|
94
|
+
ep: EndpointInfo,
|
|
95
|
+
schemes: SecuritySchemeInfo[],
|
|
96
|
+
vars: Record<string, string>,
|
|
97
|
+
): Record<string, string> {
|
|
98
|
+
const ct = isFormBody(ep) ? "application/x-www-form-urlencoded" : "application/json";
|
|
99
|
+
return {
|
|
100
|
+
"content-type": ct,
|
|
101
|
+
accept: "application/json",
|
|
102
|
+
...liveAuthHeaders(ep, schemes, vars),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Synthesize a baseline body from the endpoint's request schema and
|
|
108
|
+
* substitute live vars. Returns null when the result isn't a JSON
|
|
109
|
+
* object (array / scalar / null) — both probes treat that as a skip
|
|
110
|
+
* reason ("request body not a JSON object").
|
|
111
|
+
*/
|
|
112
|
+
export function buildBaselineFromSpec(
|
|
113
|
+
ep: EndpointInfo,
|
|
114
|
+
vars: Record<string, string>,
|
|
115
|
+
): Record<string, unknown> | null {
|
|
116
|
+
const raw = ep.requestBodySchema ? generateFromSchema(ep.requestBodySchema) : {};
|
|
117
|
+
const sub = substituteDeep(raw, vars);
|
|
118
|
+
if (typeof sub !== "object" || sub === null || Array.isArray(sub)) return null;
|
|
119
|
+
return sub as Record<string, unknown>;
|
|
120
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Probe registry + boot-time validator (m-17 / ARV-49).
|
|
3
|
+
*
|
|
4
|
+
* The CLI bootstrap calls `bootstrapProbes()` exactly once — that
|
|
5
|
+
* function imports each registered probe class and pushes it through
|
|
6
|
+
* `registerProbe`, which throws if the contract from `types.ts` is
|
|
7
|
+
* not fully implemented. Boot fails loud with a list of missing slots,
|
|
8
|
+
* so adding a new probe class without --dry-run / --report support is
|
|
9
|
+
* impossible (replaces the conventions-then-drift status quo that
|
|
10
|
+
* produced F1-15 / F2-15 / F3-15 in feedback round 15).
|
|
11
|
+
*/
|
|
12
|
+
import type { Probe, ProbeFlags } from "./types.ts";
|
|
13
|
+
|
|
14
|
+
const REQUIRED_METHODS: Array<keyof Probe> = ["dryRun", "run", "report"];
|
|
15
|
+
const REQUIRED_FLAGS: Array<keyof ProbeFlags> = [
|
|
16
|
+
"api",
|
|
17
|
+
"tag",
|
|
18
|
+
"include",
|
|
19
|
+
"exclude",
|
|
20
|
+
"dryRun",
|
|
21
|
+
"listTags",
|
|
22
|
+
"json",
|
|
23
|
+
"output",
|
|
24
|
+
"report",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export interface ValidationResult {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
errors: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Pure validator — used by both `registerProbe` and the contract test
|
|
33
|
+
* in `tests/contracts/probe-interface.test.ts`. */
|
|
34
|
+
export function validateProbe(p: unknown): ValidationResult {
|
|
35
|
+
const errors: string[] = [];
|
|
36
|
+
if (p === null || typeof p !== "object") {
|
|
37
|
+
return { ok: false, errors: ["Probe is not an object"] };
|
|
38
|
+
}
|
|
39
|
+
const probe = p as Partial<Probe>;
|
|
40
|
+
const label = probe.name ?? "<anonymous>";
|
|
41
|
+
if (typeof probe.name !== "string" || probe.name.length === 0) {
|
|
42
|
+
errors.push("Probe is missing required field name");
|
|
43
|
+
}
|
|
44
|
+
if (typeof probe.description !== "string" || probe.description.length === 0) {
|
|
45
|
+
errors.push(`Probe "${label}" is missing required field description`);
|
|
46
|
+
}
|
|
47
|
+
for (const m of REQUIRED_METHODS) {
|
|
48
|
+
if (typeof (probe as Record<string, unknown>)[m] !== "function") {
|
|
49
|
+
errors.push(`Probe "${label}" is missing required method ${m}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (probe.commonFlags === undefined || probe.commonFlags === null || typeof probe.commonFlags !== "object") {
|
|
53
|
+
errors.push(`Probe "${label}" is missing required field commonFlags`);
|
|
54
|
+
} else {
|
|
55
|
+
const flags = probe.commonFlags as unknown as Record<string, unknown>;
|
|
56
|
+
for (const f of REQUIRED_FLAGS) {
|
|
57
|
+
const v = flags[f as string];
|
|
58
|
+
if (typeof v !== "boolean") {
|
|
59
|
+
errors.push(`Probe "${label}" commonFlags is missing slot ${f} (must be boolean)`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { ok: errors.length === 0, errors };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const PROBES = new Map<string, Probe>();
|
|
67
|
+
|
|
68
|
+
export function registerProbe(probe: Probe): void {
|
|
69
|
+
const r = validateProbe(probe);
|
|
70
|
+
if (!r.ok) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Invalid probe registration:\n - ${r.errors.join("\n - ")}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (PROBES.has(probe.name)) {
|
|
76
|
+
throw new Error(`Probe "${probe.name}" is already registered`);
|
|
77
|
+
}
|
|
78
|
+
PROBES.set(probe.name, probe);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function listProbes(): readonly Probe[] {
|
|
82
|
+
return Array.from(PROBES.values());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Test helper — wipes the registry between unit tests. NOT exported
|
|
86
|
+
* through `index.ts`; tests import this module directly. */
|
|
87
|
+
export function clearProbes(): void {
|
|
88
|
+
PROBES.clear();
|
|
89
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared scaffolding for the four probe commands (`probe validation`,
|
|
3
|
+
* `probe methods`, `probe mass-assignment`, `probe security`).
|
|
4
|
+
*
|
|
5
|
+
* Each command historically duplicated the same boilerplate: read the
|
|
6
|
+
* spec, optionally filter by tag, mkdir the output dir, write the
|
|
7
|
+
* generated suites with an autogen header, and record them in the
|
|
8
|
+
* workspace manifest. This module collapses that scaffolding into two
|
|
9
|
+
* helpers (`loadSpecForProbe` / `writeProbeSuites`) so the cli layer
|
|
10
|
+
* stays thin.
|
|
11
|
+
*
|
|
12
|
+
* Live-runner commands (`mass-assignment`, `security`) reuse the
|
|
13
|
+
* write-suites half for their `--emit-tests` flag; the actual HTTP
|
|
14
|
+
* orchestration lives in `mass-assignment-probe.ts` /
|
|
15
|
+
* `security-probe.ts`.
|
|
16
|
+
*
|
|
17
|
+
* Goals are documented in TASK-185 (m-11).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { mkdir } from "node:fs/promises";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import {
|
|
23
|
+
readOpenApiSpec,
|
|
24
|
+
extractEndpoints,
|
|
25
|
+
extractSecuritySchemes,
|
|
26
|
+
serializeSuite,
|
|
27
|
+
} from "../generator/index.ts";
|
|
28
|
+
import type { EndpointInfo, SecuritySchemeInfo, RawSuite } from "../generator/index.ts";
|
|
29
|
+
import { collectTags, filterByTag } from "../generator/chunker.ts";
|
|
30
|
+
import {
|
|
31
|
+
recordGeneratedFiles,
|
|
32
|
+
inferApiName,
|
|
33
|
+
autoGenHeader,
|
|
34
|
+
type RecordInput,
|
|
35
|
+
} from "../workspace/manifest.ts";
|
|
36
|
+
import { findWorkspaceRoot } from "../workspace/root.ts";
|
|
37
|
+
|
|
38
|
+
export interface LoadSpecForProbeOptions {
|
|
39
|
+
specPath: string;
|
|
40
|
+
tag?: string;
|
|
41
|
+
/** When true, the caller wants the available-tags list, not endpoints. */
|
|
42
|
+
listTags?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type LoadSpecResult =
|
|
46
|
+
| { kind: "endpoints"; endpoints: EndpointInfo[]; securitySchemes: SecuritySchemeInfo[] }
|
|
47
|
+
| { kind: "tags"; tags: string[] }
|
|
48
|
+
| { kind: "tag-not-found"; tag: string; available: string[] };
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read the spec, optionally filter by tag, and surface either the
|
|
52
|
+
* filtered endpoints or the list of available tags. Centralises the
|
|
53
|
+
* tag-not-found error path so probe commands all produce identical
|
|
54
|
+
* messaging.
|
|
55
|
+
*/
|
|
56
|
+
export async function loadSpecForProbe(opts: LoadSpecForProbeOptions): Promise<LoadSpecResult> {
|
|
57
|
+
const doc = await readOpenApiSpec(opts.specPath);
|
|
58
|
+
const allEndpoints = extractEndpoints(doc);
|
|
59
|
+
const securitySchemes = extractSecuritySchemes(doc);
|
|
60
|
+
|
|
61
|
+
if (opts.listTags) {
|
|
62
|
+
return { kind: "tags", tags: collectTags(allEndpoints) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (opts.tag) {
|
|
66
|
+
const filtered = filterByTag(allEndpoints, opts.tag);
|
|
67
|
+
if (filtered.length === 0) {
|
|
68
|
+
return { kind: "tag-not-found", tag: opts.tag, available: collectTags(allEndpoints) };
|
|
69
|
+
}
|
|
70
|
+
return { kind: "endpoints", endpoints: filtered, securitySchemes };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { kind: "endpoints", endpoints: allEndpoints, securitySchemes };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface WriteProbeSuitesOptions {
|
|
77
|
+
/** Directory to write suites into. Created recursively if missing. */
|
|
78
|
+
output: string;
|
|
79
|
+
/** Suites produced by the underlying probe generator. */
|
|
80
|
+
suites: RawSuite[];
|
|
81
|
+
/** Manifest `by` field — e.g. `"zond probe-methods --emit"`. */
|
|
82
|
+
command: string;
|
|
83
|
+
/** First arg of `autoGenHeader` (label). Defaults to `command`. */
|
|
84
|
+
headerLabel?: string;
|
|
85
|
+
/** Concrete repro command shown in the autogen header. */
|
|
86
|
+
headerExample?: string;
|
|
87
|
+
/** Manifest category (defaults to `"probes"`). */
|
|
88
|
+
category?: RecordInput["category"];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface WroteProbeSuites {
|
|
92
|
+
files: Array<{ file: string; suite: string; tests: number }>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Materialise generator output to disk and register the files in the
|
|
97
|
+
* workspace manifest. Safe on empty input — returns an empty result and
|
|
98
|
+
* avoids creating an empty directory (m-9 P5).
|
|
99
|
+
*/
|
|
100
|
+
export async function writeProbeSuites(
|
|
101
|
+
opts: WriteProbeSuitesOptions,
|
|
102
|
+
): Promise<WroteProbeSuites> {
|
|
103
|
+
if (opts.suites.length === 0) return { files: [] };
|
|
104
|
+
|
|
105
|
+
await mkdir(opts.output, { recursive: true });
|
|
106
|
+
|
|
107
|
+
const files: WroteProbeSuites["files"] = [];
|
|
108
|
+
const manifestEntries: RecordInput[] = [];
|
|
109
|
+
const inferredApi = inferApiName(opts.output);
|
|
110
|
+
const headerLabel = opts.headerLabel ?? opts.command;
|
|
111
|
+
const headerExample = opts.headerExample ?? opts.command;
|
|
112
|
+
|
|
113
|
+
for (const suite of opts.suites) {
|
|
114
|
+
const fileName = `${suite.fileStem ?? suite.name}.yaml`;
|
|
115
|
+
const filePath = join(opts.output, fileName);
|
|
116
|
+
await Bun.write(filePath, autoGenHeader(headerLabel, headerExample) + serializeSuite(suite));
|
|
117
|
+
files.push({ file: filePath, suite: suite.name, tests: suite.tests.length });
|
|
118
|
+
manifestEntries.push({
|
|
119
|
+
path: filePath,
|
|
120
|
+
by: opts.command,
|
|
121
|
+
api: inferredApi,
|
|
122
|
+
category: opts.category ?? "probes",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const ws = findWorkspaceRoot();
|
|
128
|
+
if (!ws.fromFallback && manifestEntries.length > 0) {
|
|
129
|
+
recordGeneratedFiles(ws.root, manifestEntries);
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
/* best-effort: manifest is observability, never fail probe emit on it */
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { files };
|
|
136
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SecurityProbe` — Probe-contract wrapper around the existing
|
|
3
|
+
* `runSecurityProbes` engine in `security-probe.ts` (m-17 / ARV-49).
|
|
4
|
+
*
|
|
5
|
+
* Behavior is unchanged on this step. dryRun() returns the structured
|
|
6
|
+
* EndpointPlan[] shape (used by the new dry-run envelope in ARV-50);
|
|
7
|
+
* run() delegates to runSecurityProbes(); report() is a thin renderer
|
|
8
|
+
* that converts the SecurityProbeResult to either a markdown digest
|
|
9
|
+
* (existing formatter) or the structured per-endpoint shape (ARV-51).
|
|
10
|
+
*/
|
|
11
|
+
import type { Probe, ProbeContext, ProbeFlags, EndpointPlan, ProbeResult, ProbeReportFormat, ProbeEndpointResult, ProbeEndpointStatus } from "./types.ts";
|
|
12
|
+
import {
|
|
13
|
+
runSecurityProbes,
|
|
14
|
+
formatSecurityDigest,
|
|
15
|
+
detectFields,
|
|
16
|
+
SECURITY_CLASSES,
|
|
17
|
+
type SecurityClass,
|
|
18
|
+
type SecurityProbeResult,
|
|
19
|
+
type SecurityVerdict,
|
|
20
|
+
} from "./security-probe.ts";
|
|
21
|
+
import { hasJsonBody, pathTouchesSeededVar } from "./shared.ts";
|
|
22
|
+
|
|
23
|
+
const FLAGS: ProbeFlags = {
|
|
24
|
+
api: true,
|
|
25
|
+
tag: true,
|
|
26
|
+
include: true,
|
|
27
|
+
exclude: true,
|
|
28
|
+
dryRun: true,
|
|
29
|
+
listTags: true,
|
|
30
|
+
json: true,
|
|
31
|
+
output: true,
|
|
32
|
+
report: true,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function planForSecurity(
|
|
36
|
+
ctx: ProbeContext,
|
|
37
|
+
classes: SecurityClass[],
|
|
38
|
+
isolated: boolean,
|
|
39
|
+
): EndpointPlan[] {
|
|
40
|
+
const out: EndpointPlan[] = [];
|
|
41
|
+
for (const ep of ctx.endpoints) {
|
|
42
|
+
if (ep.deprecated) continue;
|
|
43
|
+
const m = ep.method.toUpperCase();
|
|
44
|
+
if (m !== "POST" && m !== "PUT" && m !== "PATCH") continue;
|
|
45
|
+
|
|
46
|
+
if (isolated && (m === "PUT" || m === "PATCH") && pathTouchesSeededVar(ep.path, ctx.vars)) {
|
|
47
|
+
out.push({
|
|
48
|
+
path: ep.path,
|
|
49
|
+
method: m,
|
|
50
|
+
planned: false,
|
|
51
|
+
classes_planned: [],
|
|
52
|
+
fields_planned: [],
|
|
53
|
+
skip_reason: "isolated-protected",
|
|
54
|
+
});
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!hasJsonBody(ep)) {
|
|
59
|
+
out.push({
|
|
60
|
+
path: ep.path,
|
|
61
|
+
method: m,
|
|
62
|
+
planned: false,
|
|
63
|
+
classes_planned: [],
|
|
64
|
+
fields_planned: [],
|
|
65
|
+
skip_reason: "no-body",
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const detected = detectFields(ep, classes);
|
|
71
|
+
if (detected.length === 0) {
|
|
72
|
+
out.push({
|
|
73
|
+
path: ep.path,
|
|
74
|
+
method: m,
|
|
75
|
+
planned: false,
|
|
76
|
+
classes_planned: [],
|
|
77
|
+
fields_planned: [],
|
|
78
|
+
skip_reason: "no-matched-field",
|
|
79
|
+
});
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const classesPlanned = Array.from(new Set(detected.map((d) => d.class)));
|
|
84
|
+
const fieldsPlanned = Array.from(new Set(detected.map((d) => d.field)));
|
|
85
|
+
out.push({
|
|
86
|
+
path: ep.path,
|
|
87
|
+
method: m,
|
|
88
|
+
planned: true,
|
|
89
|
+
classes_planned: classesPlanned,
|
|
90
|
+
fields_planned: fieldsPlanned,
|
|
91
|
+
skip_reason: null,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function statusFromSeverity(s: SecurityVerdict["severity"]): ProbeEndpointStatus {
|
|
98
|
+
if (s === "high") return "high";
|
|
99
|
+
if (s === "low") return "low";
|
|
100
|
+
if (s === "ok") return "ok";
|
|
101
|
+
if (s === "skipped") return "skipped";
|
|
102
|
+
return "inconclusive";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function evidenceFromFinding(f: SecurityVerdict["findings"][number]): Record<string, unknown> {
|
|
106
|
+
return {
|
|
107
|
+
field: f.field,
|
|
108
|
+
payload: f.payload,
|
|
109
|
+
status: f.status,
|
|
110
|
+
echoed: f.echoed,
|
|
111
|
+
reason: f.reason,
|
|
112
|
+
...(f.recommended_action ? { recommended_action: f.recommended_action } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function toProbeResult(sec: SecurityProbeResult): ProbeResult {
|
|
117
|
+
const endpoints: ProbeEndpointResult[] = sec.verdicts.map((v) => ({
|
|
118
|
+
path: v.path,
|
|
119
|
+
method: v.method,
|
|
120
|
+
classes_run: Array.from(new Set(v.detectedFields.map((d) => d.class))),
|
|
121
|
+
findings: v.findings.map((f) => ({
|
|
122
|
+
class: f.class,
|
|
123
|
+
severity:
|
|
124
|
+
f.severity === "inconclusive" || f.severity === "inconclusive-baseline"
|
|
125
|
+
? "inconclusive"
|
|
126
|
+
: f.severity === "skipped"
|
|
127
|
+
? "ok"
|
|
128
|
+
: f.severity === "info"
|
|
129
|
+
// ARV-253: ProbeFindingSeverity has no "info" tier. Collapse
|
|
130
|
+
// info → low for the public probe-result envelope; the digest
|
|
131
|
+
// / structured per-endpoint shape preserves the distinction.
|
|
132
|
+
? "low"
|
|
133
|
+
: f.severity === "medium"
|
|
134
|
+
// ARV-254: ProbeFindingSeverity has no "medium" tier — collapse
|
|
135
|
+
// to "low" for the wire shape. MEDIUM is a digest-only severity
|
|
136
|
+
// marker (SSRF accept on endpoint declaring delivery, no OOB);
|
|
137
|
+
// by design it must NOT gate CI as a HIGH would.
|
|
138
|
+
? "low"
|
|
139
|
+
: f.severity,
|
|
140
|
+
evidence: evidenceFromFinding(f),
|
|
141
|
+
})),
|
|
142
|
+
status: statusFromSeverity(v.severity),
|
|
143
|
+
...(v.skipReason ? { skip_reason: v.skipReason } : {}),
|
|
144
|
+
}));
|
|
145
|
+
const by_status: Record<ProbeEndpointStatus, number> = {
|
|
146
|
+
ok: 0, high: 0, low: 0, inconclusive: 0, skipped: 0,
|
|
147
|
+
};
|
|
148
|
+
for (const ep of endpoints) by_status[ep.status]++;
|
|
149
|
+
return {
|
|
150
|
+
endpoints,
|
|
151
|
+
summary: {
|
|
152
|
+
totalEndpoints: sec.totalEndpoints,
|
|
153
|
+
probed: sec.specProbed,
|
|
154
|
+
by_status,
|
|
155
|
+
},
|
|
156
|
+
warnings: sec.warnings,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export class SecurityProbe implements Probe {
|
|
161
|
+
readonly name = "security";
|
|
162
|
+
readonly description =
|
|
163
|
+
"Live security probes: SSRF / CRLF / open-redirect. Spec-driven field detection + baseline-OK gate.";
|
|
164
|
+
readonly commonFlags = FLAGS;
|
|
165
|
+
|
|
166
|
+
async dryRun(ctx: ProbeContext): Promise<EndpointPlan[]> {
|
|
167
|
+
const classes = (ctx.classes ?? SECURITY_CLASSES) as SecurityClass[];
|
|
168
|
+
const isolated = ctx.options["isolated"] === true;
|
|
169
|
+
return planForSecurity(ctx, classes, isolated);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async run(ctx: ProbeContext): Promise<ProbeResult> {
|
|
173
|
+
const classes = (ctx.classes ?? SECURITY_CLASSES) as SecurityClass[];
|
|
174
|
+
const sec = await runSecurityProbes({
|
|
175
|
+
endpoints: ctx.endpoints,
|
|
176
|
+
securitySchemes: ctx.securitySchemes,
|
|
177
|
+
vars: ctx.vars,
|
|
178
|
+
classes,
|
|
179
|
+
noCleanup: ctx.options["noCleanup"] === true,
|
|
180
|
+
timeoutMs: typeof ctx.options["timeoutMs"] === "number" ? (ctx.options["timeoutMs"] as number) : undefined,
|
|
181
|
+
isolated: ctx.options["isolated"] === true,
|
|
182
|
+
});
|
|
183
|
+
const result = toProbeResult(sec);
|
|
184
|
+
// Pass through the raw sec result for legacy markdown rendering and
|
|
185
|
+
// for orphan-tracker consumers that need the full SecurityVerdict[].
|
|
186
|
+
result.extras = { raw: sec };
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
report(format: ProbeReportFormat, result: ProbeResult): string | object {
|
|
191
|
+
if (format === "markdown") {
|
|
192
|
+
const raw = (result.extras?.["raw"] as SecurityProbeResult | undefined);
|
|
193
|
+
if (raw) return formatSecurityDigest(raw, "");
|
|
194
|
+
return "(no markdown digest available)";
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
endpoints: result.endpoints,
|
|
198
|
+
summary: result.summary,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|