@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,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SARIF v2.1.0 reporter for `zond checks` (m-15 ARV-5).
|
|
3
|
+
*
|
|
4
|
+
* Maps `CheckFinding[]` into a SARIF log that GitHub Code Scanning can
|
|
5
|
+
* ingest via `github/codeql-action/upload-sarif@v3`. Two invariants the
|
|
6
|
+
* format relies on:
|
|
7
|
+
*
|
|
8
|
+
* - `tool.driver.rules` — descriptors for every registered check, so
|
|
9
|
+
* even a finding-less run carries the catalog. ruleId follows the
|
|
10
|
+
* `<category>-<check_id>` form that oasdiff uses.
|
|
11
|
+
* - `partialFingerprints.primary` — sha1(ruleId + jsonPointer +
|
|
12
|
+
* spec_hash). Stable across re-runs of the same spec, so GitHub
|
|
13
|
+
* dedupes rather than re-opening alerts every push (42Crunch-style).
|
|
14
|
+
*
|
|
15
|
+
* The reporter is deliberately schema-only — it builds the JSON
|
|
16
|
+
* document, the CLI handles writing it to disk.
|
|
17
|
+
*/
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
|
|
20
|
+
import { listChecks } from "./registry.ts";
|
|
21
|
+
import { listStatefulChecks } from "./stateful.ts";
|
|
22
|
+
import type { CheckFinding, Severity } from "./types.ts";
|
|
23
|
+
import { categoryFor, type Category } from "../severity/category.ts";
|
|
24
|
+
import { severityToSarifLevel } from "../severity/index.ts";
|
|
25
|
+
|
|
26
|
+
export { categoryFor };
|
|
27
|
+
|
|
28
|
+
export function ruleIdFor(checkId: string): string {
|
|
29
|
+
return `${categoryFor(checkId)}-${checkId}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const severityToLevel = severityToSarifLevel;
|
|
33
|
+
|
|
34
|
+
/** RFC 6901 JSON Pointer for the operation: `/paths/<escaped>/<method>`.
|
|
35
|
+
* Escapes `~` → `~0` and `/` → `~1` so paths like `/users/{id}` survive
|
|
36
|
+
* serialization. */
|
|
37
|
+
export function jsonPointerForOperation(path: string, method: string): string {
|
|
38
|
+
const escaped = path.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
39
|
+
return `/paths/${escaped}/${method.toLowerCase()}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sha1(s: string): string {
|
|
43
|
+
return createHash("sha1").update(s).digest("hex");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function specHashOf(specContent: string): string {
|
|
47
|
+
return sha1(specContent);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function partialFingerprintFor(ruleId: string, jsonPointer: string, specHash: string): string {
|
|
51
|
+
return sha1(`${ruleId}\n${jsonPointer}\n${specHash}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface SarifReportingDescriptor {
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
shortDescription: { text: string };
|
|
58
|
+
defaultConfiguration: { level: "error" | "warning" | "note" };
|
|
59
|
+
helpUri?: string;
|
|
60
|
+
properties: {
|
|
61
|
+
category: Category;
|
|
62
|
+
severity: Severity;
|
|
63
|
+
references: string[];
|
|
64
|
+
tags: string[];
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface SarifResult {
|
|
69
|
+
ruleId: string;
|
|
70
|
+
ruleIndex: number;
|
|
71
|
+
level: "error" | "warning" | "note";
|
|
72
|
+
message: { text: string };
|
|
73
|
+
locations: Array<{
|
|
74
|
+
physicalLocation: {
|
|
75
|
+
artifactLocation: { uri: string; uriBaseId?: string };
|
|
76
|
+
// SARIF requires region to specify at least one of startLine/charOffset/
|
|
77
|
+
// byteOffset. We don't parse the spec into lines — startLine: 1 keeps
|
|
78
|
+
// GitHub Code Scanning happy and the JSON Pointer travels in the
|
|
79
|
+
// logicalLocations + properties below.
|
|
80
|
+
region: { startLine: number; snippet: { text: string } };
|
|
81
|
+
};
|
|
82
|
+
logicalLocations: Array<{ fullyQualifiedName: string; kind: string }>;
|
|
83
|
+
}>;
|
|
84
|
+
partialFingerprints: { primary: string };
|
|
85
|
+
properties: Record<string, unknown>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface SarifLog {
|
|
89
|
+
$schema: string;
|
|
90
|
+
version: "2.1.0";
|
|
91
|
+
runs: Array<{
|
|
92
|
+
tool: {
|
|
93
|
+
driver: {
|
|
94
|
+
name: string;
|
|
95
|
+
version: string;
|
|
96
|
+
informationUri: string;
|
|
97
|
+
rules: SarifReportingDescriptor[];
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
results: SarifResult[];
|
|
101
|
+
}>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface SarifReportOptions {
|
|
105
|
+
findings: CheckFinding[];
|
|
106
|
+
/** Raw spec text — fed into `sha1` for the partial-fingerprint salt
|
|
107
|
+
* so two runs against the same spec produce identical fingerprints. */
|
|
108
|
+
specContent: string;
|
|
109
|
+
/** SARIF artifactLocation.uri. Defaults to "spec.json"; CLI sets the
|
|
110
|
+
* spec's relative path so GitHub Code Scanning links findings to the
|
|
111
|
+
* spec source file. */
|
|
112
|
+
specUri?: string;
|
|
113
|
+
toolVersion: string;
|
|
114
|
+
toolInformationUri?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildRules(): SarifReportingDescriptor[] {
|
|
118
|
+
const all = [
|
|
119
|
+
...listChecks().map((c) => ({
|
|
120
|
+
id: c.id,
|
|
121
|
+
severity: c.severity,
|
|
122
|
+
defaultExpected: c.defaultExpected,
|
|
123
|
+
references: c.references,
|
|
124
|
+
})),
|
|
125
|
+
...listStatefulChecks().map((c) => ({
|
|
126
|
+
id: c.id,
|
|
127
|
+
severity: c.severity,
|
|
128
|
+
defaultExpected: c.defaultExpected,
|
|
129
|
+
references: c.references,
|
|
130
|
+
})),
|
|
131
|
+
];
|
|
132
|
+
const seen = new Set<string>();
|
|
133
|
+
const rules: SarifReportingDescriptor[] = [];
|
|
134
|
+
for (const c of all) {
|
|
135
|
+
const ruleId = ruleIdFor(c.id);
|
|
136
|
+
if (seen.has(ruleId)) continue;
|
|
137
|
+
seen.add(ruleId);
|
|
138
|
+
const cat = categoryFor(c.id);
|
|
139
|
+
const helpUri = c.references.find((r) => r.url)?.url;
|
|
140
|
+
const descriptor: SarifReportingDescriptor = {
|
|
141
|
+
id: ruleId,
|
|
142
|
+
name: c.id,
|
|
143
|
+
shortDescription: { text: c.defaultExpected },
|
|
144
|
+
defaultConfiguration: { level: severityToLevel(c.severity) },
|
|
145
|
+
properties: {
|
|
146
|
+
category: cat,
|
|
147
|
+
severity: c.severity,
|
|
148
|
+
references: c.references.map((r) => r.id),
|
|
149
|
+
tags: [cat, "openapi", "zond"],
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
if (helpUri) descriptor.helpUri = helpUri;
|
|
153
|
+
rules.push(descriptor);
|
|
154
|
+
}
|
|
155
|
+
return rules.sort((a, b) => a.id.localeCompare(b.id));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function generateSarifReport(opts: SarifReportOptions): SarifLog {
|
|
159
|
+
const specHash = specHashOf(opts.specContent);
|
|
160
|
+
const specUri = opts.specUri ?? "spec.json";
|
|
161
|
+
const rules = buildRules();
|
|
162
|
+
const ruleIndex = new Map(rules.map((r, i) => [r.id, i] as const));
|
|
163
|
+
|
|
164
|
+
// Sort findings deterministically so two runs over the same spec emit
|
|
165
|
+
// byte-identical SARIF — GitHub diffs the file across pushes and any
|
|
166
|
+
// reordering churn would re-open and re-close alerts spuriously.
|
|
167
|
+
const sorted = [...opts.findings].sort((a, b) => {
|
|
168
|
+
const aId = ruleIdFor(a.check);
|
|
169
|
+
const bId = ruleIdFor(b.check);
|
|
170
|
+
if (aId !== bId) return aId.localeCompare(bId);
|
|
171
|
+
if (a.operation.path !== b.operation.path) return a.operation.path.localeCompare(b.operation.path);
|
|
172
|
+
if (a.operation.method !== b.operation.method) return a.operation.method.localeCompare(b.operation.method);
|
|
173
|
+
return a.message.localeCompare(b.message);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const results: SarifResult[] = sorted.map((f) => {
|
|
177
|
+
const ruleId = ruleIdFor(f.check);
|
|
178
|
+
const ptr = jsonPointerForOperation(f.operation.path, f.operation.method);
|
|
179
|
+
const idx = ruleIndex.get(ruleId);
|
|
180
|
+
if (idx === undefined) {
|
|
181
|
+
throw new Error(`SARIF: no rule descriptor registered for check "${f.check}" (ruleId "${ruleId}")`);
|
|
182
|
+
}
|
|
183
|
+
const properties: Record<string, unknown> = {
|
|
184
|
+
severity: f.severity,
|
|
185
|
+
method: f.operation.method,
|
|
186
|
+
path: f.operation.path,
|
|
187
|
+
request_signature: f.request_signature,
|
|
188
|
+
response_status: f.response_summary.status,
|
|
189
|
+
};
|
|
190
|
+
if (f.operation.operationId) properties.operationId = f.operation.operationId;
|
|
191
|
+
if (f.response_summary.content_type) properties.response_content_type = f.response_summary.content_type;
|
|
192
|
+
if (f.evidence) properties.evidence = f.evidence;
|
|
193
|
+
if (f.recommended_action) properties.recommendedAction = f.recommended_action;
|
|
194
|
+
return {
|
|
195
|
+
ruleId,
|
|
196
|
+
ruleIndex: idx,
|
|
197
|
+
level: severityToLevel(f.severity),
|
|
198
|
+
message: { text: f.message },
|
|
199
|
+
locations: [
|
|
200
|
+
{
|
|
201
|
+
physicalLocation: {
|
|
202
|
+
artifactLocation: { uri: specUri },
|
|
203
|
+
region: { startLine: 1, snippet: { text: ptr } },
|
|
204
|
+
},
|
|
205
|
+
logicalLocations: [{ fullyQualifiedName: ptr, kind: "object" }],
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
partialFingerprints: { primary: partialFingerprintFor(ruleId, ptr, specHash) },
|
|
209
|
+
properties,
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
$schema: "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json",
|
|
215
|
+
version: "2.1.0",
|
|
216
|
+
runs: [
|
|
217
|
+
{
|
|
218
|
+
tool: {
|
|
219
|
+
driver: {
|
|
220
|
+
name: "zond",
|
|
221
|
+
version: opts.toolVersion,
|
|
222
|
+
informationUri: opts.toolInformationUri ?? "https://github.com/kirrosh/zond",
|
|
223
|
+
rules,
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
results,
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stateful checks (m-15 ARV-3) — security-flavored checks that need
|
|
3
|
+
* to orchestrate multiple HTTP requests against a single operation
|
|
4
|
+
* (auth probes) or a CRUD chain (use-after-free / availability).
|
|
5
|
+
*
|
|
6
|
+
* Kept in a parallel registry from the per-response `Check`s so the
|
|
7
|
+
* single-response runner stays simple. `runChecks` calls
|
|
8
|
+
* `runStateful(...)` after the per-op response phase.
|
|
9
|
+
*/
|
|
10
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
11
|
+
import type { CrudGroup, EndpointInfo } from "../generator/types.ts";
|
|
12
|
+
import type { HttpRequest, HttpResponse } from "../runner/types.ts";
|
|
13
|
+
import { executeRequest } from "../runner/http-client.ts";
|
|
14
|
+
import type { CheckOutcome, CheckReference, CheckRuntimeOptions, Severity } from "./types.ts";
|
|
15
|
+
|
|
16
|
+
export interface StatefulHarness {
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
doc: OpenAPIV3.Document;
|
|
19
|
+
/** Headers that constitute "real auth" for the run. Empty when the
|
|
20
|
+
* caller didn't pass --auth-header / no env vars. */
|
|
21
|
+
authHeaders: Record<string, string>;
|
|
22
|
+
/** When true, security checks should skip with a warning (ARV-3 AC #6). */
|
|
23
|
+
bootstrapCleanupFailed: boolean;
|
|
24
|
+
/** ARV-181: real path-param fixtures from `.env.yaml`. Mirrors what
|
|
25
|
+
* the per-response runner already does via ARV-141 — without this
|
|
26
|
+
* the stateful harness rebuilds URLs with literal `{event_id}`
|
|
27
|
+
* placeholders, gets routed to 403/404, and the broken-baseline
|
|
28
|
+
* guard silently skips real auth checks. Optional so unit tests
|
|
29
|
+
* can stub without it; production callers always pass them. */
|
|
30
|
+
pathVars?: Record<string, string>;
|
|
31
|
+
/** ARV-181: per-run knobs (e.g. strict401). Mirrors CheckContext.options
|
|
32
|
+
* for stateful checks so they can read the same flags as per-response
|
|
33
|
+
* ones. */
|
|
34
|
+
options?: CheckRuntimeOptions;
|
|
35
|
+
/** ARV-169 (m-20): per-resource overrides for cross-call probes,
|
|
36
|
+
* keyed by `resource` name from `.api-resources.yaml`. Today only
|
|
37
|
+
* `cross_call_references` reads `readbackDiff`; future m-20 probes
|
|
38
|
+
* (idempotency, pagination, lifecycle) will append their own keys
|
|
39
|
+
* to the per-resource entry. Optional — when absent each probe
|
|
40
|
+
* falls back to its built-in defaults. */
|
|
41
|
+
resourceConfigs?: Map<string, {
|
|
42
|
+
readbackDiff?: import("../generator/resources-builder.ts").ReadbackDiffConfig;
|
|
43
|
+
idempotency?: import("../generator/resources-builder.ts").IdempotencyConfig;
|
|
44
|
+
pagination?: import("../generator/resources-builder.ts").PaginationConfig;
|
|
45
|
+
lifecycle?: import("../generator/resources-builder.ts").LifecycleConfig;
|
|
46
|
+
/** ARV-187: LLM-authored example POST body — preferred over
|
|
47
|
+
* generateFromSchema(create) by stateful CRUD checks. */
|
|
48
|
+
seedBody?: import("../generator/resources-builder.ts").SeedBodyConfig;
|
|
49
|
+
}>;
|
|
50
|
+
send(req: HttpRequest, opts?: { timeoutMs?: number }): Promise<HttpResponse>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface BaseStatefulCheck {
|
|
54
|
+
id: string;
|
|
55
|
+
severity: Severity;
|
|
56
|
+
defaultExpected: string;
|
|
57
|
+
references: CheckReference[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface AuthStatefulCheck extends BaseStatefulCheck {
|
|
61
|
+
phase: "auth";
|
|
62
|
+
applies(op: EndpointInfo): boolean;
|
|
63
|
+
run(op: EndpointInfo, h: StatefulHarness): Promise<CheckOutcome>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface CrudStatefulCheck extends BaseStatefulCheck {
|
|
67
|
+
phase: "crud";
|
|
68
|
+
applies(group: CrudGroup): boolean;
|
|
69
|
+
run(group: CrudGroup, h: StatefulHarness): Promise<CheckOutcome>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type StatefulCheck = AuthStatefulCheck | CrudStatefulCheck;
|
|
73
|
+
|
|
74
|
+
const STATEFUL_REGISTRY = new Map<string, StatefulCheck>();
|
|
75
|
+
|
|
76
|
+
export function registerStatefulCheck(c: StatefulCheck): void {
|
|
77
|
+
if (STATEFUL_REGISTRY.has(c.id)) throw new Error(`Stateful check "${c.id}" already registered`);
|
|
78
|
+
STATEFUL_REGISTRY.set(c.id, c);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function listStatefulChecks(): StatefulCheck[] {
|
|
82
|
+
return [...STATEFUL_REGISTRY.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function makeHarness(
|
|
86
|
+
baseUrl: string,
|
|
87
|
+
doc: OpenAPIV3.Document,
|
|
88
|
+
opts: {
|
|
89
|
+
authHeaders?: Record<string, string>;
|
|
90
|
+
bootstrapCleanupFailed?: boolean;
|
|
91
|
+
timeoutMs?: number;
|
|
92
|
+
pathVars?: Record<string, string>;
|
|
93
|
+
options?: CheckRuntimeOptions;
|
|
94
|
+
resourceConfigs?: StatefulHarness["resourceConfigs"];
|
|
95
|
+
/** ARV-227: shared with the per-response phase so a single
|
|
96
|
+
* `--max-requests` cap bounds the whole run. Throws (caught by
|
|
97
|
+
* the runner's per-check try/catch and converted to skip) once
|
|
98
|
+
* the budget is exhausted, so a stateful probe mid-chain doesn't
|
|
99
|
+
* silently spin past the cap. */
|
|
100
|
+
requestBudget?: import("../runner/executor.ts").RequestBudget;
|
|
101
|
+
} = {},
|
|
102
|
+
): StatefulHarness {
|
|
103
|
+
return {
|
|
104
|
+
baseUrl,
|
|
105
|
+
doc,
|
|
106
|
+
authHeaders: opts.authHeaders ?? {},
|
|
107
|
+
bootstrapCleanupFailed: opts.bootstrapCleanupFailed ?? false,
|
|
108
|
+
pathVars: opts.pathVars,
|
|
109
|
+
options: opts.options,
|
|
110
|
+
resourceConfigs: opts.resourceConfigs,
|
|
111
|
+
send: async (req, sendOpts) => {
|
|
112
|
+
if (opts.requestBudget) {
|
|
113
|
+
const { reserveRequest, MAX_REQUESTS_SKIP_REASON } = await import("../runner/executor.ts");
|
|
114
|
+
if (!reserveRequest(opts.requestBudget)) {
|
|
115
|
+
throw new Error(MAX_REQUESTS_SKIP_REASON);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return executeRequest(req, { timeout: sendOpts?.timeoutMs ?? opts.timeoutMs ?? 30000 });
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type contracts for the `zond checks` framework (m-15 ARV-1).
|
|
3
|
+
*
|
|
4
|
+
* A `Check` is a single named conformance/security probe applied to one
|
|
5
|
+
* operation × one HTTP response. The `runner` resolves the spec into
|
|
6
|
+
* operations, generates a request via `core/generator/data-factory`,
|
|
7
|
+
* sends it via `core/runner/send-request`, and feeds each (case,
|
|
8
|
+
* response) pair to every active check.
|
|
9
|
+
*
|
|
10
|
+
* Names mirror schemathesis V4 1-to-1 so benchmarks and findings carry
|
|
11
|
+
* across (see `backlog/milestones/m-15`). New checks register themselves
|
|
12
|
+
* in `checks/index.ts` via `registerCheck`.
|
|
13
|
+
*/
|
|
14
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
15
|
+
import type { EndpointInfo } from "../generator/types.ts";
|
|
16
|
+
import type { SchemaValidator } from "../runner/schema-validator.ts";
|
|
17
|
+
import type { RecommendedAction } from "../diagnostics/failure-hints.ts";
|
|
18
|
+
|
|
19
|
+
// Severity unified in src/core/severity (ARV-250). Re-exported here for
|
|
20
|
+
// backwards-compatible imports across the checks subsystem; the canonical
|
|
21
|
+
// definition is in core/severity. Adds 'info' tier (previously absent in
|
|
22
|
+
// checks, now needed for hygiene-class findings per m-21 pivot).
|
|
23
|
+
import type { Severity } from "../severity/index.ts";
|
|
24
|
+
import { emptySeverityBuckets } from "../severity/index.ts";
|
|
25
|
+
import type { Category } from "../severity/category.ts";
|
|
26
|
+
import { emptyCategoryBuckets } from "../severity/category.ts";
|
|
27
|
+
export type { Severity, Category };
|
|
28
|
+
|
|
29
|
+
export type Phase = "examples" | "coverage" | "all";
|
|
30
|
+
|
|
31
|
+
/** Probe shapes a check may need. ARV-1 shipped only `positive`; ARV-2
|
|
32
|
+
* adds two more for header/method-rejection checks; ARV-4 will add
|
|
33
|
+
* `negative_data` for the data-rejection pair. The runner generates a
|
|
34
|
+
* case for each kind that at least one active check declares. */
|
|
35
|
+
export type CaseKind =
|
|
36
|
+
| "positive"
|
|
37
|
+
| "missing_required_header"
|
|
38
|
+
| "unsupported_method"
|
|
39
|
+
| "negative_data";
|
|
40
|
+
|
|
41
|
+
export interface CheckReference {
|
|
42
|
+
/** CWE / OWASP / RFC identifier — free form, agent-readable. */
|
|
43
|
+
id: string;
|
|
44
|
+
/** Optional URL for the human reader. */
|
|
45
|
+
url?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface CheckResponse {
|
|
49
|
+
status: number;
|
|
50
|
+
headers: Record<string, string>;
|
|
51
|
+
body: unknown;
|
|
52
|
+
duration_ms: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CheckCase {
|
|
56
|
+
/** The operation under test (path + method + parameters). */
|
|
57
|
+
operation: EndpointInfo;
|
|
58
|
+
/** Resolved request that produced the response below. */
|
|
59
|
+
request: {
|
|
60
|
+
method: string;
|
|
61
|
+
url: string;
|
|
62
|
+
headers: Record<string, string>;
|
|
63
|
+
body?: string;
|
|
64
|
+
};
|
|
65
|
+
/** Mode the case was generated under — positive (valid) or negative
|
|
66
|
+
* (intentionally invalid). Drives the ARV-7 `--mode` filter. */
|
|
67
|
+
mode: "positive" | "negative";
|
|
68
|
+
/** Probe shape — what the case is *built* to exercise. A check only
|
|
69
|
+
* runs against a case whose `kind` it declared (or all if it
|
|
70
|
+
* declared none — defaults to `positive`). */
|
|
71
|
+
kind: CaseKind;
|
|
72
|
+
/** For probe-style cases, an opaque hint the check itself produced
|
|
73
|
+
* (e.g. the header name that was dropped, the method that was
|
|
74
|
+
* swapped in). Lets a check emit a precise finding without parsing
|
|
75
|
+
* the request back out. */
|
|
76
|
+
meta?: Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** ARV-179: per-run knobs surfaced from `RunCheckOptions` into the
|
|
80
|
+
* check itself. Add new fields here when a check needs an opt-in
|
|
81
|
+
* behaviour that doesn't deserve its own check id. Keep this lean —
|
|
82
|
+
* most knobs belong upstream in the case-generator, not in the
|
|
83
|
+
* check's response-side logic. */
|
|
84
|
+
export interface CheckRuntimeOptions {
|
|
85
|
+
/** ARV-179: require strict 405 for `unsupported_method` (matches
|
|
86
|
+
* schemathesis V4 default). Off by default — zond's pragmatic policy
|
|
87
|
+
* accepts 401/403/404 as legitimate rejections of an undeclared
|
|
88
|
+
* method (common nginx/gateway behaviour). */
|
|
89
|
+
strict405?: boolean;
|
|
90
|
+
/** ARV-181: require strict 401 for `ignored_auth` no-auth / bogus-auth
|
|
91
|
+
* variants (matches schemathesis V4 default). Off by default — zond's
|
|
92
|
+
* pragmatic policy accepts any 4xx as a legitimate auth-reject (403,
|
|
93
|
+
* 404, 422 are common). With this on, only 401 passes — a 403 on
|
|
94
|
+
* no_auth becomes a finding "expected 401, got 403". */
|
|
95
|
+
strict401?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface CheckContext {
|
|
99
|
+
case: CheckCase;
|
|
100
|
+
response: CheckResponse;
|
|
101
|
+
/** Pre-built once per run so checks don't pay AJV compile cost
|
|
102
|
+
* per-case. Optional so unit tests can stub a context without one. */
|
|
103
|
+
schemaValidator?: SchemaValidator;
|
|
104
|
+
/** Original spec doc — checks that need declared headers /
|
|
105
|
+
* content-types / status codes look them up here. */
|
|
106
|
+
doc?: OpenAPIV3.Document;
|
|
107
|
+
/** ARV-179: per-run knobs (see CheckRuntimeOptions). */
|
|
108
|
+
options?: CheckRuntimeOptions;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type CheckOutcome =
|
|
112
|
+
| { kind: "pass" }
|
|
113
|
+
| { kind: "skip"; reason: string }
|
|
114
|
+
| { kind: "fail"; message: string; evidence?: Record<string, unknown> };
|
|
115
|
+
|
|
116
|
+
export interface Check {
|
|
117
|
+
/** Stable identifier — must match schemathesis name where possible. */
|
|
118
|
+
id: string;
|
|
119
|
+
severity: Severity;
|
|
120
|
+
/** Default expected outcome ("server should NOT 5xx", etc) — surfaced
|
|
121
|
+
* by `zond checks list` so an agent can read the contract. */
|
|
122
|
+
defaultExpected: string;
|
|
123
|
+
references: CheckReference[];
|
|
124
|
+
/** Probe shapes this check consumes. Default `["positive"]` — most
|
|
125
|
+
* conformance checks just inspect the standard response. ARV-2's
|
|
126
|
+
* `missing_required_header` / `unsupported_method` declare their
|
|
127
|
+
* own kinds so the runner generates the matching probe case. */
|
|
128
|
+
caseKinds?: CaseKind[];
|
|
129
|
+
/** Whether this check is meaningful for the given operation. Used by
|
|
130
|
+
* the runner to skip checks that don't apply (e.g. auth-related
|
|
131
|
+
* checks on operations with no security requirement). */
|
|
132
|
+
applies(operation: EndpointInfo): boolean;
|
|
133
|
+
run(ctx: CheckContext): CheckOutcome;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface CheckFinding {
|
|
137
|
+
check: string;
|
|
138
|
+
severity: Severity;
|
|
139
|
+
/** ARV-251: finding category drives per-section roll-up in reports.
|
|
140
|
+
* Optional for backwards compat — when absent, downstream code derives
|
|
141
|
+
* it via `categoryFor(check)`. Storing it on the finding keeps probe
|
|
142
|
+
* emitters and check emitters using the same shape. */
|
|
143
|
+
category?: Category;
|
|
144
|
+
operation: { path: string; method: string; operationId?: string };
|
|
145
|
+
request_signature: string;
|
|
146
|
+
response_summary: { status: number; content_type?: string };
|
|
147
|
+
message: string;
|
|
148
|
+
evidence?: Record<string, unknown>;
|
|
149
|
+
/** ARV-11 — closed enum so agents can route on it without parsing
|
|
150
|
+
* the message. Resolved by `recommendForCheck()` keyed on the
|
|
151
|
+
* check id (and response status for `network_error`). Optional
|
|
152
|
+
* because synthetic findings (e.g. unit-test fakes) may skip it. */
|
|
153
|
+
recommended_action?: RecommendedAction;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface CheckRunSummary {
|
|
157
|
+
operations: number;
|
|
158
|
+
cases: number;
|
|
159
|
+
checks_run: number;
|
|
160
|
+
findings: number;
|
|
161
|
+
by_severity: Record<Severity, number>;
|
|
162
|
+
/** ARV-251: per-category roll-up — small teams use this to triage
|
|
163
|
+
* "0 security, 12 reliability, 40 contract, 200 hygiene" instead of
|
|
164
|
+
* reading one flat severity pile. */
|
|
165
|
+
by_category: Record<Category, number>;
|
|
166
|
+
/** ARV-26: count of `kind: "skip"` outcomes returned by checks, keyed by
|
|
167
|
+
* `"<check_id>: <reason>"`. Surfaces the gap between probe and runtime
|
|
168
|
+
* validators — e.g. `response_schema_conformance: no JSON Schema on this
|
|
169
|
+
* response branch ×2` tells the user why "0 findings" doesn't mean "all
|
|
170
|
+
* green" (probe got 4xx, response schema only declared on 2xx). */
|
|
171
|
+
skipped_outcomes: Record<string, number>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface CheckRunData {
|
|
175
|
+
findings: CheckFinding[];
|
|
176
|
+
summary: CheckRunSummary;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function emptySummary(): CheckRunSummary {
|
|
180
|
+
return {
|
|
181
|
+
operations: 0,
|
|
182
|
+
cases: 0,
|
|
183
|
+
checks_run: 0,
|
|
184
|
+
findings: 0,
|
|
185
|
+
by_severity: emptySeverityBuckets(),
|
|
186
|
+
by_category: emptyCategoryBuckets(),
|
|
187
|
+
skipped_outcomes: {},
|
|
188
|
+
};
|
|
189
|
+
}
|