@kirrosh/zond 0.26.0 → 0.27.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.
Files changed (261) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +22 -21
  3. package/bin/zond.mjs +23 -0
  4. package/package.json +13 -16
  5. package/scripts/npm/postinstall.mjs +76 -0
  6. package/src/CLAUDE.md +0 -112
  7. package/src/bun-types.d.ts +0 -5
  8. package/src/cli/argv.ts +0 -122
  9. package/src/cli/commands/add-api.ts +0 -146
  10. package/src/cli/commands/api/annotate/idempotency.ts +0 -59
  11. package/src/cli/commands/api/annotate/index.ts +0 -880
  12. package/src/cli/commands/api/annotate/lifecycle.ts +0 -74
  13. package/src/cli/commands/api/annotate/overlay.ts +0 -206
  14. package/src/cli/commands/api/annotate/pagination.ts +0 -64
  15. package/src/cli/commands/api/annotate/prompts.ts +0 -220
  16. package/src/cli/commands/api/annotate/readback.ts +0 -58
  17. package/src/cli/commands/api/annotate/resources.ts +0 -91
  18. package/src/cli/commands/api/annotate/seed-bodies.ts +0 -61
  19. package/src/cli/commands/audit.ts +0 -786
  20. package/src/cli/commands/catalog.ts +0 -97
  21. package/src/cli/commands/check.ts +0 -361
  22. package/src/cli/commands/checks.ts +0 -1072
  23. package/src/cli/commands/ci-init.ts +0 -223
  24. package/src/cli/commands/clean.ts +0 -212
  25. package/src/cli/commands/cleanup.ts +0 -236
  26. package/src/cli/commands/completions.ts +0 -192
  27. package/src/cli/commands/coverage.ts +0 -872
  28. package/src/cli/commands/db.ts +0 -611
  29. package/src/cli/commands/describe.ts +0 -120
  30. package/src/cli/commands/discover.ts +0 -1356
  31. package/src/cli/commands/doctor.ts +0 -661
  32. package/src/cli/commands/fixtures.ts +0 -402
  33. package/src/cli/commands/generate.ts +0 -577
  34. package/src/cli/commands/init/agents-md.ts +0 -61
  35. package/src/cli/commands/init/bootstrap.ts +0 -111
  36. package/src/cli/commands/init/index.ts +0 -244
  37. package/src/cli/commands/init/skills.ts +0 -141
  38. package/src/cli/commands/init/templates/agents.md +0 -86
  39. package/src/cli/commands/init/templates/markdown.d.ts +0 -4
  40. package/src/cli/commands/init/templates/skills/warm-up-target.md +0 -122
  41. package/src/cli/commands/init/templates/skills/zond-checks.md +0 -621
  42. package/src/cli/commands/init/templates/skills/zond-seed.md +0 -114
  43. package/src/cli/commands/init/templates/skills/zond-triage.md +0 -272
  44. package/src/cli/commands/init/templates/skills/zond.md +0 -861
  45. package/src/cli/commands/init/templates/zond-config.yml +0 -14
  46. package/src/cli/commands/prepare-fixtures.ts +0 -97
  47. package/src/cli/commands/probe/_seed-bodies.ts +0 -52
  48. package/src/cli/commands/probe/mass-assignment.ts +0 -594
  49. package/src/cli/commands/probe/security.ts +0 -537
  50. package/src/cli/commands/probe/static.ts +0 -255
  51. package/src/cli/commands/probe/webhooks.ts +0 -163
  52. package/src/cli/commands/probe.ts +0 -535
  53. package/src/cli/commands/reference.ts +0 -87
  54. package/src/cli/commands/refresh-api.ts +0 -227
  55. package/src/cli/commands/remove-api.ts +0 -150
  56. package/src/cli/commands/report-bundle.ts +0 -310
  57. package/src/cli/commands/report.ts +0 -241
  58. package/src/cli/commands/request.ts +0 -548
  59. package/src/cli/commands/run.ts +0 -1142
  60. package/src/cli/commands/schema-from-runs.ts +0 -128
  61. package/src/cli/commands/secrets.ts +0 -133
  62. package/src/cli/commands/session.ts +0 -244
  63. package/src/cli/commands/use.ts +0 -74
  64. package/src/cli/index.ts +0 -55
  65. package/src/cli/json-envelope.ts +0 -108
  66. package/src/cli/json-schemas.ts +0 -314
  67. package/src/cli/output.ts +0 -40
  68. package/src/cli/program.ts +0 -219
  69. package/src/cli/resolve.ts +0 -105
  70. package/src/cli/runtime.ts +0 -7
  71. package/src/cli/safe-live.ts +0 -24
  72. package/src/cli/status-filter.ts +0 -114
  73. package/src/cli/util/api-context.ts +0 -85
  74. package/src/cli/version.ts +0 -8
  75. package/src/core/audit/persist.ts +0 -183
  76. package/src/core/checks/budget.ts +0 -59
  77. package/src/core/checks/checks/_crud-helpers.ts +0 -133
  78. package/src/core/checks/checks/_negative_mutator.ts +0 -133
  79. package/src/core/checks/checks/_readback-helpers.ts +0 -133
  80. package/src/core/checks/checks/content_type_conformance.ts +0 -39
  81. package/src/core/checks/checks/cross_call_references.ts +0 -147
  82. package/src/core/checks/checks/cursor_boundary_fuzzing.ts +0 -219
  83. package/src/core/checks/checks/ensure_resource_availability.ts +0 -62
  84. package/src/core/checks/checks/idempotency_replay.ts +0 -242
  85. package/src/core/checks/checks/ignored_auth.ts +0 -254
  86. package/src/core/checks/checks/index.ts +0 -68
  87. package/src/core/checks/checks/lifecycle_transitions.ts +0 -416
  88. package/src/core/checks/checks/missing_required_header.ts +0 -40
  89. package/src/core/checks/checks/negative_data_rejection.ts +0 -148
  90. package/src/core/checks/checks/not_a_server_error.ts +0 -35
  91. package/src/core/checks/checks/open_cors_on_sensitive.ts +0 -160
  92. package/src/core/checks/checks/pagination_invariants.ts +0 -419
  93. package/src/core/checks/checks/positive_data_acceptance.ts +0 -33
  94. package/src/core/checks/checks/rate_limit_headers_absent.ts +0 -77
  95. package/src/core/checks/checks/response_headers_conformance.ts +0 -74
  96. package/src/core/checks/checks/response_schema_conformance.ts +0 -30
  97. package/src/core/checks/checks/status_code_conformance.ts +0 -132
  98. package/src/core/checks/checks/unsupported_method.ts +0 -63
  99. package/src/core/checks/checks/use_after_free.ts +0 -78
  100. package/src/core/checks/index.ts +0 -30
  101. package/src/core/checks/mode.ts +0 -82
  102. package/src/core/checks/recommended-action.ts +0 -68
  103. package/src/core/checks/registry.ts +0 -78
  104. package/src/core/checks/runner.ts +0 -1461
  105. package/src/core/checks/sarif.ts +0 -230
  106. package/src/core/checks/spec-findings.ts +0 -308
  107. package/src/core/checks/stateful.ts +0 -121
  108. package/src/core/checks/types.ts +0 -305
  109. package/src/core/checks/zond-extensions.ts +0 -73
  110. package/src/core/classifier/recommended-action.ts +0 -251
  111. package/src/core/context/current.ts +0 -51
  112. package/src/core/context/session.ts +0 -78
  113. package/src/core/coverage/loader.ts +0 -216
  114. package/src/core/coverage/reasons.ts +0 -300
  115. package/src/core/diagnostics/db-analysis.ts +0 -666
  116. package/src/core/diagnostics/failure-class.ts +0 -140
  117. package/src/core/diagnostics/failure-hints.ts +0 -112
  118. package/src/core/diagnostics/spec-pointer.ts +0 -99
  119. package/src/core/diagnostics/suggested-fixes.ts +0 -155
  120. package/src/core/exporter/case-study/index.ts +0 -270
  121. package/src/core/exporter/curl.ts +0 -40
  122. package/src/core/exporter/exporter.ts +0 -48
  123. package/src/core/exporter/html-report/escape.ts +0 -24
  124. package/src/core/exporter/html-report/index.ts +0 -479
  125. package/src/core/exporter/html-report/script.ts +0 -100
  126. package/src/core/exporter/html-report/styles.ts +0 -408
  127. package/src/core/generator/catalog-builder.ts +0 -179
  128. package/src/core/generator/chunker.ts +0 -78
  129. package/src/core/generator/coverage-phase.ts +0 -0
  130. package/src/core/generator/coverage-scanner.ts +0 -87
  131. package/src/core/generator/data-factory.ts +0 -759
  132. package/src/core/generator/describe.ts +0 -302
  133. package/src/core/generator/endpoint-warnings.ts +0 -52
  134. package/src/core/generator/fixtures-builder.ts +0 -332
  135. package/src/core/generator/index.ts +0 -14
  136. package/src/core/generator/openapi-reader.ts +0 -297
  137. package/src/core/generator/path-param-disambig.ts +0 -140
  138. package/src/core/generator/resources-builder.ts +0 -898
  139. package/src/core/generator/schema-utils.ts +0 -112
  140. package/src/core/generator/serializer.ts +0 -343
  141. package/src/core/generator/suite-generator.ts +0 -1301
  142. package/src/core/generator/types.ts +0 -63
  143. package/src/core/identity/identity-file.ts +0 -0
  144. package/src/core/lint/affects.ts +0 -28
  145. package/src/core/lint/config.ts +0 -96
  146. package/src/core/lint/format.ts +0 -42
  147. package/src/core/lint/index.ts +0 -94
  148. package/src/core/lint/reporter.ts +0 -128
  149. package/src/core/lint/rules/consistency.ts +0 -158
  150. package/src/core/lint/rules/heuristics.ts +0 -97
  151. package/src/core/lint/rules/strictness.ts +0 -109
  152. package/src/core/lint/types.ts +0 -96
  153. package/src/core/lint/walker.ts +0 -248
  154. package/src/core/meta/meta-store.ts +0 -11
  155. package/src/core/output/README.md +0 -73
  156. package/src/core/output/index.ts +0 -13
  157. package/src/core/output/run.ts +0 -91
  158. package/src/core/output/types.ts +0 -122
  159. package/src/core/parser/dynamic-values.ts +0 -160
  160. package/src/core/parser/env-interpolation.ts +0 -104
  161. package/src/core/parser/filter.ts +0 -97
  162. package/src/core/parser/schema.ts +0 -359
  163. package/src/core/parser/types.ts +0 -117
  164. package/src/core/parser/variables.ts +0 -0
  165. package/src/core/parser/yaml-parser.ts +0 -181
  166. package/src/core/probe/bootstrap.ts +0 -34
  167. package/src/core/probe/dry-run-envelope.ts +0 -61
  168. package/src/core/probe/mass-assignment/classify.ts +0 -175
  169. package/src/core/probe/mass-assignment/cleanup.ts +0 -52
  170. package/src/core/probe/mass-assignment/digest.ts +0 -114
  171. package/src/core/probe/mass-assignment/orchestrator.ts +0 -459
  172. package/src/core/probe/mass-assignment/regression.ts +0 -141
  173. package/src/core/probe/mass-assignment/suspects.ts +0 -92
  174. package/src/core/probe/mass-assignment/types.ts +0 -135
  175. package/src/core/probe/mass-assignment-probe-class.ts +0 -198
  176. package/src/core/probe/mass-assignment-probe.ts +0 -27
  177. package/src/core/probe/mass-assignment-template.ts +0 -240
  178. package/src/core/probe/method-probe.ts +0 -164
  179. package/src/core/probe/method-shared.ts +0 -69
  180. package/src/core/probe/negative-probe.ts +0 -691
  181. package/src/core/probe/orphan-tracker.ts +0 -188
  182. package/src/core/probe/path-discovery.ts +0 -439
  183. package/src/core/probe/probe-harness.ts +0 -119
  184. package/src/core/probe/registry.ts +0 -89
  185. package/src/core/probe/runner.ts +0 -136
  186. package/src/core/probe/security/baseline.ts +0 -174
  187. package/src/core/probe/security/classify.ts +0 -341
  188. package/src/core/probe/security/cleanup.ts +0 -125
  189. package/src/core/probe/security/detectors.ts +0 -71
  190. package/src/core/probe/security/digest.ts +0 -104
  191. package/src/core/probe/security/orchestrator.ts +0 -398
  192. package/src/core/probe/security/regression.ts +0 -103
  193. package/src/core/probe/security/types.ts +0 -151
  194. package/src/core/probe/security-probe-class.ts +0 -207
  195. package/src/core/probe/security-probe.ts +0 -32
  196. package/src/core/probe/shared.ts +0 -531
  197. package/src/core/probe/static-probe-class.ts +0 -125
  198. package/src/core/probe/types.ts +0 -165
  199. package/src/core/probe/verdict-aggregator.ts +0 -33
  200. package/src/core/probe/webhooks-probe.ts +0 -282
  201. package/src/core/reporter/console.ts +0 -240
  202. package/src/core/reporter/index.ts +0 -22
  203. package/src/core/reporter/json.ts +0 -22
  204. package/src/core/reporter/junit.ts +0 -93
  205. package/src/core/reporter/ndjson.ts +0 -37
  206. package/src/core/reporter/types.ts +0 -15
  207. package/src/core/runner/assertions.ts +0 -383
  208. package/src/core/runner/async-pool.ts +0 -108
  209. package/src/core/runner/auth-path.ts +0 -8
  210. package/src/core/runner/ci-context.ts +0 -72
  211. package/src/core/runner/executor.ts +0 -652
  212. package/src/core/runner/expr-eval.ts +0 -41
  213. package/src/core/runner/form-encode.ts +0 -41
  214. package/src/core/runner/http-client.ts +0 -224
  215. package/src/core/runner/learn-drift.ts +0 -293
  216. package/src/core/runner/preflight-vars.ts +0 -153
  217. package/src/core/runner/progress-tracker.ts +0 -73
  218. package/src/core/runner/rate-limiter.ts +0 -185
  219. package/src/core/runner/run-kind.ts +0 -45
  220. package/src/core/runner/schema-validator.ts +0 -308
  221. package/src/core/runner/send-request.ts +0 -232
  222. package/src/core/runner/transforms.ts +0 -65
  223. package/src/core/runner/types.ts +0 -94
  224. package/src/core/secrets/registry.ts +0 -164
  225. package/src/core/secrets/secrets-file.ts +0 -115
  226. package/src/core/selectors/operation-filter.ts +0 -144
  227. package/src/core/setup-api.ts +0 -590
  228. package/src/core/severity/category.ts +0 -94
  229. package/src/core/severity/index.ts +0 -58
  230. package/src/core/spec/infer-schema.ts +0 -102
  231. package/src/core/spec/layers.ts +0 -154
  232. package/src/core/spec/merge-specs.ts +0 -156
  233. package/src/core/spec/schema-from-runs.ts +0 -117
  234. package/src/core/spec/schema-overlay.ts +0 -130
  235. package/src/core/util/ajv.ts +0 -13
  236. package/src/core/util/format-eta.ts +0 -21
  237. package/src/core/util/headers.ts +0 -9
  238. package/src/core/util/url.ts +0 -24
  239. package/src/core/utils.ts +0 -13
  240. package/src/core/workspace/config.ts +0 -129
  241. package/src/core/workspace/fixture-gap-report.ts +0 -84
  242. package/src/core/workspace/fixture-gaps.ts +0 -71
  243. package/src/core/workspace/manifest.ts +0 -283
  244. package/src/core/workspace/output-rotation.ts +0 -62
  245. package/src/core/workspace/root.ts +0 -96
  246. package/src/core/workspace/triage-path.ts +0 -87
  247. package/src/db/lint-runs.ts +0 -47
  248. package/src/db/migrate.ts +0 -128
  249. package/src/db/migrations/0001_run_kind.sql +0 -25
  250. package/src/db/migrations/0002_run_kind_request.sql +0 -59
  251. package/src/db/migrations/sql.d.ts +0 -4
  252. package/src/db/queries/collections.ts +0 -133
  253. package/src/db/queries/coverage.ts +0 -9
  254. package/src/db/queries/dashboard.ts +0 -59
  255. package/src/db/queries/results.ts +0 -216
  256. package/src/db/queries/runs.ts +0 -289
  257. package/src/db/queries/sessions.ts +0 -42
  258. package/src/db/queries/settings.ts +0 -28
  259. package/src/db/queries/types.ts +0 -172
  260. package/src/db/queries.ts +0 -75
  261. package/src/db/schema.ts +0 -298
@@ -1,230 +0,0 @@
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
- }
@@ -1,308 +0,0 @@
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
- }
@@ -1,121 +0,0 @@
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
- }