@kirrosh/zond 0.22.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +811 -0
- package/README.md +59 -6
- package/package.json +9 -7
- package/src/CLAUDE.md +112 -0
- package/src/cli/argv.ts +122 -0
- package/src/cli/commands/add-api.ts +146 -0
- package/src/cli/commands/api/annotate/idempotency.ts +59 -0
- package/src/cli/commands/api/annotate/index.ts +880 -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 +64 -0
- package/src/cli/commands/api/annotate/prompts.ts +220 -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 +786 -0
- package/src/cli/commands/catalog.ts +35 -0
- package/src/cli/commands/check.ts +361 -0
- package/src/cli/commands/checks.ts +1072 -0
- package/src/cli/commands/ci-init.ts +43 -0
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +236 -0
- package/src/cli/commands/completions.ts +16 -0
- package/src/cli/commands/coverage.ts +823 -132
- package/src/cli/commands/db.ts +486 -12
- package/src/cli/commands/describe.ts +37 -2
- package/src/cli/commands/discover.ts +1356 -0
- package/src/cli/commands/doctor.ts +661 -0
- package/src/cli/commands/fixtures.ts +402 -0
- package/src/cli/commands/generate.ts +438 -47
- package/src/cli/commands/init/bootstrap.ts +34 -2
- package/src/cli/commands/{init.ts → init/index.ts} +99 -5
- package/src/cli/commands/init/skills.ts +99 -3
- package/src/cli/commands/init/templates/agents.md +77 -64
- package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +621 -0
- package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +272 -0
- package/src/cli/commands/init/templates/skills/zond.md +802 -125
- package/src/cli/commands/init/templates/zond-config.yml +8 -9
- package/src/cli/commands/prepare-fixtures.ts +97 -0
- package/src/cli/commands/probe/_seed-bodies.ts +52 -0
- package/src/cli/commands/probe/mass-assignment.ts +594 -0
- package/src/cli/commands/probe/security.ts +537 -0
- package/src/cli/commands/probe/static.ts +255 -0
- package/src/cli/commands/probe/webhooks.ts +163 -0
- package/src/cli/commands/probe.ts +535 -0
- package/src/cli/commands/reference.ts +87 -0
- package/src/cli/commands/refresh-api.ts +227 -0
- package/src/cli/commands/remove-api.ts +150 -0
- package/src/cli/commands/report-bundle.ts +310 -0
- package/src/cli/commands/report.ts +241 -0
- package/src/cli/commands/request.ts +495 -4
- package/src/cli/commands/run.ts +870 -53
- package/src/cli/commands/schema-from-runs.ts +128 -0
- package/src/cli/commands/secrets.ts +133 -0
- 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 +92 -3
- package/src/cli/json-schemas.ts +314 -0
- package/src/cli/output.ts +17 -1
- package/src/cli/program.ts +199 -635
- package/src/cli/resolve.ts +105 -0
- package/src/cli/safe-live.ts +24 -0
- package/src/cli/status-filter.ts +114 -0
- package/src/cli/util/api-context.ts +85 -0
- package/src/cli/version.ts +5 -0
- package/src/core/audit/persist.ts +183 -0
- package/src/core/checks/budget.ts +59 -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 +147 -0
- package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
- package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
- package/src/core/checks/checks/idempotency_replay.ts +242 -0
- package/src/core/checks/checks/ignored_auth.ts +254 -0
- package/src/core/checks/checks/index.ts +68 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +416 -0
- package/src/core/checks/checks/missing_required_header.ts +40 -0
- package/src/core/checks/checks/negative_data_rejection.ts +148 -0
- package/src/core/checks/checks/not_a_server_error.ts +35 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +160 -0
- package/src/core/checks/checks/pagination_invariants.ts +419 -0
- package/src/core/checks/checks/positive_data_acceptance.ts +33 -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 +132 -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 +82 -0
- package/src/core/checks/recommended-action.ts +68 -0
- package/src/core/checks/registry.ts +78 -0
- package/src/core/checks/runner.ts +1461 -0
- package/src/core/checks/sarif.ts +230 -0
- package/src/core/checks/spec-findings.ts +308 -0
- package/src/core/checks/stateful.ts +121 -0
- package/src/core/checks/types.ts +305 -0
- package/src/core/checks/zond-extensions.ts +73 -0
- package/src/core/classifier/recommended-action.ts +251 -0
- package/src/core/context/current.ts +22 -6
- package/src/core/context/session.ts +78 -0
- package/src/core/coverage/loader.ts +216 -0
- package/src/core/coverage/reasons.ts +300 -0
- package/src/core/diagnostics/db-analysis.ts +293 -59
- package/src/core/diagnostics/failure-class.ts +140 -0
- package/src/core/diagnostics/failure-hints.ts +88 -89
- package/src/core/diagnostics/spec-pointer.ts +99 -0
- package/src/core/diagnostics/suggested-fixes.ts +155 -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 +38 -19
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/data-factory.ts +586 -22
- package/src/core/generator/describe.ts +1 -1
- package/src/core/generator/fixtures-builder.ts +332 -0
- package/src/core/generator/index.ts +5 -5
- package/src/core/generator/openapi-reader.ts +135 -7
- package/src/core/generator/path-param-disambig.ts +140 -0
- package/src/core/generator/resources-builder.ts +898 -0
- package/src/core/generator/schema-utils.ts +33 -3
- package/src/core/generator/serializer.ts +103 -13
- package/src/core/generator/suite-generator.ts +583 -122
- package/src/core/generator/types.ts +14 -0
- package/src/core/identity/identity-file.ts +0 -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 +73 -0
- package/src/core/output/index.ts +13 -0
- package/src/core/output/run.ts +91 -0
- package/src/core/output/types.ts +122 -0
- package/src/core/parser/dynamic-values.ts +160 -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 +61 -0
- package/src/core/probe/mass-assignment/classify.ts +175 -0
- package/src/core/probe/mass-assignment/cleanup.ts +52 -0
- package/src/core/probe/mass-assignment/digest.ts +114 -0
- package/src/core/probe/mass-assignment/orchestrator.ts +459 -0
- package/src/core/probe/mass-assignment/regression.ts +141 -0
- package/src/core/probe/mass-assignment/suspects.ts +92 -0
- package/src/core/probe/mass-assignment/types.ts +135 -0
- package/src/core/probe/mass-assignment-probe-class.ts +198 -0
- package/src/core/probe/mass-assignment-probe.ts +27 -0
- package/src/core/probe/mass-assignment-template.ts +240 -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 +439 -0
- package/src/core/probe/probe-harness.ts +119 -0
- package/src/core/probe/registry.ts +89 -0
- package/src/core/probe/runner.ts +136 -0
- package/src/core/probe/security/baseline.ts +174 -0
- package/src/core/probe/security/classify.ts +341 -0
- package/src/core/probe/security/cleanup.ts +125 -0
- package/src/core/probe/security/detectors.ts +71 -0
- package/src/core/probe/security/digest.ts +104 -0
- package/src/core/probe/security/orchestrator.ts +398 -0
- package/src/core/probe/security/regression.ts +103 -0
- package/src/core/probe/security/types.ts +151 -0
- package/src/core/probe/security-probe-class.ts +207 -0
- package/src/core/probe/security-probe.ts +32 -0
- package/src/core/probe/shared.ts +531 -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 +282 -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 +59 -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 +265 -36
- package/src/core/runner/form-encode.ts +41 -0
- package/src/core/runner/http-client.ts +112 -2
- package/src/core/runner/learn-drift.ts +293 -0
- package/src/core/runner/preflight-vars.ts +153 -0
- package/src/core/runner/progress-tracker.ts +73 -0
- package/src/core/runner/rate-limiter.ts +87 -33
- package/src/core/runner/run-kind.ts +45 -0
- package/src/core/runner/schema-validator.ts +308 -0
- package/src/core/runner/send-request.ts +158 -20
- package/src/core/runner/types.ts +44 -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 +457 -20
- package/src/core/severity/category.ts +94 -0
- package/src/core/severity/index.ts +58 -0
- package/src/core/spec/infer-schema.ts +102 -0
- package/src/core/spec/layers.ts +154 -0
- package/src/core/spec/merge-specs.ts +156 -0
- package/src/core/spec/schema-from-runs.ts +117 -0
- package/src/core/spec/schema-overlay.ts +130 -0
- package/src/core/util/ajv.ts +13 -0
- package/src/core/util/format-eta.ts +21 -0
- package/src/core/util/headers.ts +9 -0
- package/src/core/util/url.ts +24 -0
- package/src/core/utils.ts +5 -1
- package/src/core/workspace/config.ts +129 -0
- package/src/core/workspace/fixture-gap-report.ts +84 -0
- package/src/core/workspace/fixture-gaps.ts +71 -0
- package/src/core/workspace/manifest.ts +283 -0
- package/src/core/workspace/output-rotation.ts +62 -0
- package/src/core/workspace/root.ts +13 -11
- package/src/core/workspace/triage-path.ts +87 -0
- package/src/db/lint-runs.ts +47 -0
- package/src/db/migrate.ts +128 -0
- package/src/db/migrations/0001_run_kind.sql +25 -0
- package/src/db/migrations/0002_run_kind_request.sql +59 -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 +216 -0
- package/src/db/queries/runs.ts +289 -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 +75 -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,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,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-60: spec-level rollup of systemic gaps.
|
|
3
|
+
*
|
|
4
|
+
* Many findings are a single spec-level fact (no 401 declared anywhere,
|
|
5
|
+
* no response schemas, no DELETE+GET pair detectable) smeared across N
|
|
6
|
+
* operations. The flat finding list reads as "83 problems" when it's
|
|
7
|
+
* really "1 problem × 83 sites". Small teams reading the report can't
|
|
8
|
+
* tell whether to act on the spec or on the server — and the actionable
|
|
9
|
+
* line tends to be the same for every row.
|
|
10
|
+
*
|
|
11
|
+
* This module computes `SpecFinding[]` from the runner's primary outputs:
|
|
12
|
+
*
|
|
13
|
+
* 1. **status_drift** — group existing findings by (check, status); if
|
|
14
|
+
* a group covers ≥80% of the check's applicable operations, emit
|
|
15
|
+
* one rollup row. Per-op findings stay in `data.findings` (so SARIF
|
|
16
|
+
* and `--verbose` keep the long form).
|
|
17
|
+
* 2. **missing_declaration** — a single skipped_outcome reason covers
|
|
18
|
+
* ≥80% of the check's applicable cases. Typical: response schema /
|
|
19
|
+
* header schema not declared on this API.
|
|
20
|
+
* 3. **no_detector** — check is applicable to ≥5 operations but ran
|
|
21
|
+
* zero cases. Typical: `use_after_free` without DELETE+GET pair.
|
|
22
|
+
* Different from skip — the check itself produced no cases.
|
|
23
|
+
*
|
|
24
|
+
* Threshold is hard-coded at 0.8 (AC #1). Lower would create false
|
|
25
|
+
* rollups on small (<10-op) APIs where a 3/4 incidental cluster doesn't
|
|
26
|
+
* indicate a systemic gap.
|
|
27
|
+
*/
|
|
28
|
+
import type {
|
|
29
|
+
CheckFinding,
|
|
30
|
+
SpecFinding,
|
|
31
|
+
} from "./types.ts";
|
|
32
|
+
import { categoryFor } from "../severity/category.ts";
|
|
33
|
+
|
|
34
|
+
export interface PerCheckObservations {
|
|
35
|
+
/** Distinct operations where `check.applies(op) === true`. */
|
|
36
|
+
applicable: number;
|
|
37
|
+
/** Count of cases the check actually ran (passed + failed + skipped). */
|
|
38
|
+
cases: number;
|
|
39
|
+
/** ARV-26-style "check: reason" → count, restricted to this check. */
|
|
40
|
+
skipped: Record<string, number>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SPEC_CLUSTER_RATIO = 0.8;
|
|
44
|
+
const NO_DETECTOR_FLOOR = 5;
|
|
45
|
+
|
|
46
|
+
/** Mapping table: check id + response status → human reason + actionable
|
|
47
|
+
* fix hint. Centralised so a future check that opts into rollup can just
|
|
48
|
+
* register its hint here. */
|
|
49
|
+
function explainStatusDrift(checkId: string, status: number): { reason: string; fix: string } {
|
|
50
|
+
if (checkId === "status_code_conformance") {
|
|
51
|
+
return {
|
|
52
|
+
reason: `Status ${status} not declared in spec`,
|
|
53
|
+
fix: `Add ${status} to the response declarations for the affected operations, or pass --tolerate-undeclared ${status}.`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (checkId === "ignored_auth") {
|
|
57
|
+
return {
|
|
58
|
+
reason: `Auth probes did not produce ${status >= 400 ? "the expected rejection" : "a 4xx"} (got ${status})`,
|
|
59
|
+
fix: `Verify the security scheme is enforced server-side, or relax with --strict-401=false.`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (checkId === "negative_data_rejection") {
|
|
63
|
+
return {
|
|
64
|
+
reason: `Negative payloads accepted with ${status} on most operations`,
|
|
65
|
+
fix: `Server is not validating inputs — fix request-body validation, or downgrade by adjusting tolerated statuses in your gate.`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (checkId === "unsupported_method") {
|
|
69
|
+
return {
|
|
70
|
+
reason: `Undeclared methods returned ${status} instead of 405`,
|
|
71
|
+
fix: `Configure the gateway to emit 405 for undeclared verbs, or pass --strict-405=false.`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (checkId === "missing_required_header") {
|
|
75
|
+
return {
|
|
76
|
+
reason: `Required-header omission returned ${status}`,
|
|
77
|
+
fix: `Server should reject with 400/415 when required headers are missing.`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// Generic fallback — better than "(unknown reason)".
|
|
81
|
+
return {
|
|
82
|
+
reason: `Response status ${status} clustered across most operations for ${checkId}`,
|
|
83
|
+
fix: `Inspect a sample finding for context, or run with --verbose for per-op detail.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function explainSkipCluster(checkId: string, reason: string): { reason: string; fix: string } | null {
|
|
88
|
+
if (checkId === "response_schema_conformance") {
|
|
89
|
+
return {
|
|
90
|
+
reason: `Response schemas not declared on this API (${reason})`,
|
|
91
|
+
fix: `Add response schemas to spec.json, or run \`zond api annotate dump readback\` to capture them from live runs.`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (checkId === "response_headers_conformance") {
|
|
95
|
+
return {
|
|
96
|
+
reason: `Response headers not declared on this API (${reason})`,
|
|
97
|
+
fix: `Add response header declarations to spec.json — without them this check is a no-op.`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (checkId === "not_a_server_error" && /skipped|max_requests/.test(reason)) {
|
|
101
|
+
return null; // budget-skip, not a spec gap
|
|
102
|
+
}
|
|
103
|
+
if (/max_requests|max-requests/.test(reason)) {
|
|
104
|
+
return null; // ARV-227 budget cap is not a spec finding
|
|
105
|
+
}
|
|
106
|
+
// Other skip clusters fall through — surfaced as `other` kind.
|
|
107
|
+
return {
|
|
108
|
+
reason: `Most cases for ${checkId} skipped (${reason})`,
|
|
109
|
+
fix: `Inspect one sample (zond db diagnose --run-id <id>) to confirm the gap is intentional.`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function explainNoDetector(checkId: string): { reason: string; fix: string } {
|
|
114
|
+
if (checkId === "use_after_free") {
|
|
115
|
+
return {
|
|
116
|
+
reason: `No DELETE+GET pair detectable from spec — check ran 0 cases`,
|
|
117
|
+
fix: `Annotate resources (\`zond api annotate dump readback\`) or add explicit lifecycle declarations.`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (checkId === "ensure_resource_availability") {
|
|
121
|
+
return {
|
|
122
|
+
reason: `No CRUD pair detected — check ran 0 cases`,
|
|
123
|
+
fix: `Run \`zond api annotate dump readback\` to capture resource boundaries.`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (checkId === "cross_call_references") {
|
|
127
|
+
return {
|
|
128
|
+
reason: `No POST→GET follow-up pair detected — check ran 0 cases`,
|
|
129
|
+
fix: `Annotate resources with readback_diff in .api-resources.yaml.`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
reason: `${checkId} applicable but produced 0 cases on this API`,
|
|
134
|
+
fix: `Inspect the case-generator for this check or add resource annotations.`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* ARV-307: broken-baseline guard for conformance checks.
|
|
140
|
+
*
|
|
141
|
+
* On a degenerate baseline (e.g. a fully auth-rejected scan where every
|
|
142
|
+
* response is 401/404, zero 2xx), `status_code_conformance` and
|
|
143
|
+
* `content_type_conformance` fire on every undeclared status/content-type
|
|
144
|
+
* and emit thousands of findings that are pure baseline artifacts — the
|
|
145
|
+
* status_drift rollup in computeSpecFindings can't collapse them because the
|
|
146
|
+
* undeclared statuses are diverse (401 here, 404 there), so no single
|
|
147
|
+
* (check,status) group crosses the 80% threshold.
|
|
148
|
+
*
|
|
149
|
+
* Stateful checks already skip on a per-case broken-baseline guard; this is
|
|
150
|
+
* the run-level equivalent for the conformance family: if the positive
|
|
151
|
+
* (expected-success) probes overwhelmingly failed, the whole conformance
|
|
152
|
+
* signal is untrustworthy, so we roll it up into one `broken_baseline`
|
|
153
|
+
* spec_finding and drop the per-op conformance findings.
|
|
154
|
+
*
|
|
155
|
+
* We gate on the POSITIVE-probe baseline, not all responses — negative /
|
|
156
|
+
* boundary cases legitimately return 4xx on a healthy API, so counting them
|
|
157
|
+
* would false-trip the guard. `positiveTwoxx / positiveTotal` is the success
|
|
158
|
+
* rate of the probes that are supposed to succeed.
|
|
159
|
+
*
|
|
160
|
+
* Threshold: >90% of positive probes non-2xx, with ≥10 positive probes so a
|
|
161
|
+
* tiny run doesn't trip on a single failure. When fewer than 10 positive
|
|
162
|
+
* probes ran (e.g. negative-only mode) the baseline can't be judged and the
|
|
163
|
+
* guard is a no-op.
|
|
164
|
+
*/
|
|
165
|
+
export const BASELINE_GATED_CHECKS: ReadonlySet<string> = new Set([
|
|
166
|
+
"status_code_conformance",
|
|
167
|
+
"content_type_conformance",
|
|
168
|
+
]);
|
|
169
|
+
export const BROKEN_BASELINE_NON2XX_RATIO = 0.9;
|
|
170
|
+
export const BROKEN_BASELINE_MIN_POSITIVE = 10;
|
|
171
|
+
|
|
172
|
+
export function applyBrokenBaselineGuard(input: {
|
|
173
|
+
findings: CheckFinding[];
|
|
174
|
+
positiveTotal: number;
|
|
175
|
+
positiveTwoxx: number;
|
|
176
|
+
}): { kept: CheckFinding[]; removed: CheckFinding[]; specFinding: SpecFinding | null } {
|
|
177
|
+
const { findings, positiveTotal, positiveTwoxx } = input;
|
|
178
|
+
const noop = { kept: findings, removed: [] as CheckFinding[], specFinding: null };
|
|
179
|
+
if (positiveTotal < BROKEN_BASELINE_MIN_POSITIVE) return noop;
|
|
180
|
+
const nonTwoxxRatio = (positiveTotal - positiveTwoxx) / positiveTotal;
|
|
181
|
+
if (nonTwoxxRatio < BROKEN_BASELINE_NON2XX_RATIO) return noop;
|
|
182
|
+
|
|
183
|
+
const kept: CheckFinding[] = [];
|
|
184
|
+
const removed: CheckFinding[] = [];
|
|
185
|
+
for (const f of findings) {
|
|
186
|
+
// Suppressed findings are already out of the CI-gating tallies; leave
|
|
187
|
+
// them in the audit trail untouched.
|
|
188
|
+
if (!f.suppressed_by && BASELINE_GATED_CHECKS.has(f.check)) removed.push(f);
|
|
189
|
+
else kept.push(f);
|
|
190
|
+
}
|
|
191
|
+
if (removed.length === 0) return noop;
|
|
192
|
+
|
|
193
|
+
const affected = new Map<string, { path: string; method: string; operationId?: string }>();
|
|
194
|
+
for (const f of removed) {
|
|
195
|
+
affected.set(`${f.operation.method} ${f.operation.path}`, f.operation);
|
|
196
|
+
}
|
|
197
|
+
const pct = Math.round(nonTwoxxRatio * 100);
|
|
198
|
+
const specFinding: SpecFinding = {
|
|
199
|
+
check: "status_code_conformance",
|
|
200
|
+
kind: "broken_baseline",
|
|
201
|
+
severity: "info",
|
|
202
|
+
category: categoryFor("status_code_conformance"),
|
|
203
|
+
reason:
|
|
204
|
+
`Degenerate baseline: ${pct}% of ${positiveTotal} positive probes returned non-2xx ` +
|
|
205
|
+
`(only ${positiveTwoxx} succeeded). ${removed.length} conformance finding(s) suppressed as ` +
|
|
206
|
+
`baseline artifacts.`,
|
|
207
|
+
fix_hint:
|
|
208
|
+
`Fix the baseline first — supply valid auth (--auth-header / apis/<name>/.env.yaml) and ` +
|
|
209
|
+
`seed path-param fixtures (\`zond prepare-fixtures\`) so positive probes reach 2xx, then re-run ` +
|
|
210
|
+
`conformance. Undeclared statuses on an all-4xx scan are not real spec drift.`,
|
|
211
|
+
affected_operations: [...affected.values()],
|
|
212
|
+
count: removed.length,
|
|
213
|
+
applicable: positiveTotal,
|
|
214
|
+
};
|
|
215
|
+
return { kept, removed, specFinding };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function computeSpecFindings(
|
|
219
|
+
findings: CheckFinding[],
|
|
220
|
+
perCheck: Map<string, PerCheckObservations>,
|
|
221
|
+
): SpecFinding[] {
|
|
222
|
+
const out: SpecFinding[] = [];
|
|
223
|
+
|
|
224
|
+
// --- 1. status_drift: cluster findings by (check, status). -----------
|
|
225
|
+
type Group = {
|
|
226
|
+
severity: CheckFinding["severity"];
|
|
227
|
+
check: string;
|
|
228
|
+
status: number;
|
|
229
|
+
ops: Map<string, CheckFinding["operation"]>;
|
|
230
|
+
};
|
|
231
|
+
const groups = new Map<string, Group>();
|
|
232
|
+
for (const f of findings) {
|
|
233
|
+
const status = f.response_summary?.status ?? 0;
|
|
234
|
+
if (status <= 0) continue; // network errors etc — not a status drift
|
|
235
|
+
const key = `${f.check}|${status}`;
|
|
236
|
+
let g = groups.get(key);
|
|
237
|
+
if (!g) {
|
|
238
|
+
g = { severity: f.severity, check: f.check, status, ops: new Map() };
|
|
239
|
+
groups.set(key, g);
|
|
240
|
+
}
|
|
241
|
+
const opKey = `${f.operation.method} ${f.operation.path}`;
|
|
242
|
+
if (!g.ops.has(opKey)) g.ops.set(opKey, f.operation);
|
|
243
|
+
}
|
|
244
|
+
for (const g of groups.values()) {
|
|
245
|
+
const obs = perCheck.get(g.check);
|
|
246
|
+
const applicable = obs?.applicable ?? g.ops.size;
|
|
247
|
+
if (g.ops.size < 2) continue; // single-op rows aren't a rollup
|
|
248
|
+
if (g.ops.size / Math.max(applicable, 1) < SPEC_CLUSTER_RATIO) continue;
|
|
249
|
+
const { reason, fix } = explainStatusDrift(g.check, g.status);
|
|
250
|
+
out.push({
|
|
251
|
+
check: g.check,
|
|
252
|
+
kind: "status_drift",
|
|
253
|
+
severity: g.severity,
|
|
254
|
+
category: categoryFor(g.check),
|
|
255
|
+
reason,
|
|
256
|
+
fix_hint: fix,
|
|
257
|
+
affected_operations: [...g.ops.values()],
|
|
258
|
+
count: g.ops.size,
|
|
259
|
+
applicable,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// --- 2. missing_declaration: skip cluster. ---------------------------
|
|
264
|
+
for (const [checkId, obs] of perCheck) {
|
|
265
|
+
if (obs.cases <= 0) continue;
|
|
266
|
+
for (const [rawReason, count] of Object.entries(obs.skipped)) {
|
|
267
|
+
if (count / obs.cases < SPEC_CLUSTER_RATIO) continue;
|
|
268
|
+
// skipped key is "<checkId>: <reason>" — strip the prefix when it
|
|
269
|
+
// matches; otherwise treat the whole thing as the reason.
|
|
270
|
+
const reason = rawReason.startsWith(`${checkId}: `) ? rawReason.slice(checkId.length + 2) : rawReason;
|
|
271
|
+
const expl = explainSkipCluster(checkId, reason);
|
|
272
|
+
if (!expl) continue;
|
|
273
|
+
out.push({
|
|
274
|
+
check: checkId,
|
|
275
|
+
kind: /not declared|not declar/i.test(expl.reason) || /\bschema\b|\bheaders?\b/i.test(reason)
|
|
276
|
+
? "missing_declaration"
|
|
277
|
+
: "other",
|
|
278
|
+
severity: "info",
|
|
279
|
+
category: categoryFor(checkId),
|
|
280
|
+
reason: expl.reason,
|
|
281
|
+
fix_hint: expl.fix,
|
|
282
|
+
affected_operations: [],
|
|
283
|
+
count,
|
|
284
|
+
applicable: obs.applicable,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// --- 3. no_detector: applicable ≥5 but 0 cases. ----------------------
|
|
290
|
+
for (const [checkId, obs] of perCheck) {
|
|
291
|
+
if (obs.cases > 0) continue;
|
|
292
|
+
if (obs.applicable < NO_DETECTOR_FLOOR) continue;
|
|
293
|
+
const expl = explainNoDetector(checkId);
|
|
294
|
+
out.push({
|
|
295
|
+
check: checkId,
|
|
296
|
+
kind: "no_detector",
|
|
297
|
+
severity: "info",
|
|
298
|
+
category: categoryFor(checkId),
|
|
299
|
+
reason: expl.reason,
|
|
300
|
+
fix_hint: expl.fix,
|
|
301
|
+
affected_operations: [],
|
|
302
|
+
count: 0,
|
|
303
|
+
applicable: obs.applicable,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
@@ -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
|
+
}
|