@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,590 +0,0 @@
1
- import { resolve, join, relative } from "path";
2
- import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "fs";
3
- import { getDb } from "../db/schema.ts";
4
- import { createCollection, deleteCollection, findCollectionByNameOrId, normalizePath } from "../db/queries.ts";
5
- import {
6
- readOpenApiSpec,
7
- extractEndpoints,
8
- extractSecuritySchemes,
9
- buildCatalog,
10
- serializeCatalog,
11
- buildApiResourceMap,
12
- serializeApiResourceMap,
13
- buildApiFixtureManifest,
14
- serializeApiFixtureManifest,
15
- } from "./generator/index.ts";
16
- import { decycleSchema } from "./generator/schema-utils.ts";
17
- import { mergeOpenApiDocs } from "./spec/merge-specs.ts";
18
- import { schemeVarName } from "./generator/suite-generator.ts";
19
- import type { SecuritySchemeInfo } from "./generator/types.ts";
20
- import { hashSpec } from "./meta/meta-store.ts";
21
- import { findWorkspaceRoot } from "./workspace/root.ts";
22
- import { recordGeneratedFiles, type RecordInput } from "./workspace/manifest.ts";
23
- import { CANONICAL_IDENTITY_KEYS } from "./identity/identity-file.ts";
24
-
25
- /** Filename of the dereferenced spec snapshot inside `apis/<name>/`. */
26
- export const SPEC_SNAPSHOT_FILENAME = "spec.json";
27
-
28
- interface WriteArtifactsParams {
29
- /** Dereferenced OpenAPI document. */
30
- doc: unknown;
31
- /** Absolute path to apis/<name>/. */
32
- baseDir: string;
33
- /** Collection name (goes into the catalog header). */
34
- apiName: string;
35
- /** Resolved server URL or "". */
36
- baseUrl: string;
37
- /** Absolute workspace root, used to compute the relative specSource. */
38
- workspaceRoot: string;
39
- /** Caller label for manifest entries (defaults to "zond add api"). */
40
- by?: string;
41
- }
42
-
43
- /**
44
- * Snapshot the dereferenced spec into `apis/<name>/spec.json` and emit the
45
- * three derived artifacts (`.api-catalog.yaml`, `.api-resources.yaml`,
46
- * `.api-fixtures.yaml`). Pure side-effect; safe to call from `setupApi` at
47
- * register time and from `refreshApi` for re-snapshot.
48
- */
49
- export function writeArtifactsFromDoc(params: WriteArtifactsParams): void {
50
- const { doc, baseDir, apiName, baseUrl, workspaceRoot, by = "zond add api" } = params;
51
- const localSpecAbsPath = join(baseDir, SPEC_SNAPSHOT_FILENAME);
52
- // Pass through decycleSchema first — large specs (Stripe, GitHub) contain
53
- // mutually-recursive `$ref` chains that resolve to true object cycles after
54
- // dereference, and raw JSON.stringify crashes on those with "cannot
55
- // serialize cyclic structures" (ARV-145). decycleSchema collapses the
56
- // second visit to `{ "x-circular": true }` (vendor-extension sentinel —
57
- // NOT `$ref`, otherwise the parser tries to resolve "[Circular]" as a
58
- // file path when re-reading spec.json, ARV-146) so the on-disk snapshot
59
- // is self-contained, parser-safe JSON.
60
- let serialized: string;
61
- try {
62
- serialized = JSON.stringify(decycleSchema(doc), null, 2);
63
- } catch (err) {
64
- const m = (err as Error).message;
65
- throw new Error(
66
- `spec_serialize_failed: could not serialize dereferenced spec for '${apiName}' — ${m}. ` +
67
- `This usually means the spec contains a structure decycleSchema could not collapse; please open an issue with the spec URL.`,
68
- );
69
- }
70
- writeFileSync(localSpecAbsPath, serialized + "\n", "utf-8");
71
-
72
- const endpoints = extractEndpoints(doc as any);
73
- const securitySchemes = extractSecuritySchemes(doc as any);
74
- // Hash the on-disk file bytes — this is what `zond doctor` re-hashes when
75
- // checking artifact freshness (TASK-215). Both sides now read the decycled
76
- // form: setup-api writes it here, doctor re-reads the same file.
77
- const specHash = hashSpec(readFileSync(localSpecAbsPath, "utf-8"));
78
- const localSpecRelPath = relative(workspaceRoot, localSpecAbsPath).replace(/\\/g, "/");
79
-
80
- const catalog = buildCatalog({
81
- endpoints,
82
- securitySchemes,
83
- specSource: localSpecRelPath,
84
- specHash,
85
- apiName,
86
- apiVersion: (doc as any).info?.version,
87
- baseUrl,
88
- });
89
- const catalogPath = join(baseDir, ".api-catalog.yaml");
90
- writeFileSync(catalogPath, serializeCatalog(catalog), "utf-8");
91
-
92
- const resources = buildApiResourceMap({ endpoints, specHash });
93
- const resourcesPath = join(baseDir, ".api-resources.yaml");
94
- writeFileSync(resourcesPath, serializeApiResourceMap(resources), "utf-8");
95
-
96
- const fixtures = buildApiFixtureManifest({
97
- endpoints,
98
- securitySchemes,
99
- baseUrl: baseUrl || undefined,
100
- specHash,
101
- resourceMap: resources,
102
- });
103
- const fixturesPath = join(baseDir, ".api-fixtures.yaml");
104
- writeFileSync(fixturesPath, serializeApiFixtureManifest(fixtures), "utf-8");
105
-
106
- // Record artifacts in .zond/manifest.json (TASK-156).
107
- try {
108
- const entries: RecordInput[] = [
109
- { path: localSpecAbsPath, by, api: apiName, category: "spec" },
110
- { path: catalogPath, by, api: apiName, category: "catalog" },
111
- { path: resourcesPath, by, api: apiName, category: "resources" },
112
- { path: fixturesPath, by, api: apiName, category: "fixtures" },
113
- ];
114
- recordGeneratedFiles(workspaceRoot, entries);
115
- } catch {
116
- // best-effort
117
- }
118
- }
119
-
120
- /**
121
- * Resolve a `collections.openapi_spec` value to a concrete file path the
122
- * caller can read. Throws on a legacy / broken workspace so the user gets
123
- * a single clear instruction instead of a downstream ENOENT.
124
- *
125
- * Resolution order:
126
- * 1. URL (http/https) — return as-is.
127
- * 2. Workspace-relative path (e.g. `apis/<name>/spec.json`) that exists.
128
- * 3. Absolute filesystem path that exists. Treated as legacy: the spec
129
- * is outside the workspace and not snapshotted into `apis/<name>/`.
130
- * We let it through, but `assertLocalSpec` (used by run/report/doctor)
131
- * will reject it.
132
- * 4. Otherwise — throw a "legacy / stale workspace" error pointing at
133
- * `zond refresh-api`.
134
- */
135
- export function resolveCollectionSpec(specRef: string): string {
136
- if (/^https?:\/\//i.test(specRef)) return specRef;
137
- const root = findWorkspaceRoot().root;
138
- const local = resolve(root, specRef);
139
- if (existsSync(local)) return local;
140
- if (specRef.startsWith("/") && existsSync(specRef)) return specRef;
141
- throw new Error(
142
- `Spec for this API is missing at ${local}` +
143
- (specRef.startsWith("/") ? ` (DB recorded an external path: ${specRef})` : "") +
144
- `. The workspace looks legacy or stale — run \`zond refresh-api <name> [--spec <path|url>]\` to re-snapshot.`,
145
- );
146
- }
147
-
148
- /**
149
- * Strict variant for code paths that must read the workspace-local
150
- * snapshot (run/report/doctor). Returns the local absolute path or
151
- * throws — never returns an external URL or path.
152
- */
153
- export function assertLocalSpec(specRef: string, apiName: string): string {
154
- if (/^https?:\/\//i.test(specRef)) {
155
- throw new Error(
156
- `API '${apiName}' has a remote spec recorded (${specRef}) but no local snapshot. ` +
157
- `Run \`zond refresh-api ${apiName}\` to materialise apis/${apiName}/${SPEC_SNAPSHOT_FILENAME}.`,
158
- );
159
- }
160
- const root = findWorkspaceRoot().root;
161
- const local = resolve(root, specRef);
162
- if (!existsSync(local)) {
163
- throw new Error(
164
- `Local spec missing for API '${apiName}' (expected ${local}). ` +
165
- `Run \`zond refresh-api ${apiName}\` to regenerate it.`,
166
- );
167
- }
168
- return local;
169
- }
170
-
171
- function toYaml(vars: Record<string, string>): string {
172
- const lines: string[] = [];
173
- for (const [k, v] of Object.entries(vars)) {
174
- const needsQuote = /[:#\[\]{}&*!|>'"@`,%]/.test(v) || v.includes(" ") || v === "";
175
- lines.push(`${k}: ${needsQuote ? `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : v}`);
176
- }
177
- return lines.join("\n");
178
- }
179
-
180
- export interface SetupApiOptions {
181
- name?: string;
182
- spec?: string;
183
- /** ARV-375: additional specs to union into one merged document. When
184
- * set (length > 1), takes precedence over `spec`; the specs are
185
- * dereferenced individually then merged deterministically (last-wins on
186
- * path/component collision, collisions surfaced as warnings). */
187
- specs?: string[];
188
- dir?: string;
189
- envVars?: Record<string, string>;
190
- dbPath?: string;
191
- force?: boolean;
192
- insecure?: boolean;
193
- caPath?: string;
194
- }
195
-
196
- export interface SetupApiResult {
197
- created: true;
198
- collectionId: number;
199
- baseDir: string;
200
- testPath: string;
201
- baseUrl: string;
202
- specEndpoints: number;
203
- pathParams?: Record<string, string>;
204
- /** Auth-related env-var names auto-seeded as `@secret:<name>` (TASK-209). */
205
- authVars?: string[];
206
- warnings?: string[];
207
- }
208
-
209
- /**
210
- * Walk the security schemes and derive the env-var names that
211
- * `@readme/openapi-parser`-derived suites/probes will reference for auth
212
- * tokens. Mirrors `getAuthHeaders` in src/core/probe/shared.ts:
213
- * - HTTP bearer/basic/empty-scheme → schemeVarName(...) (default "auth_token")
214
- * - apiKey in header named "Authorization" → schemeVarName(...)
215
- * - apiKey in header (other name) → "api_key"
216
- */
217
- function deriveAuthVarNames(schemes: SecuritySchemeInfo[]): string[] {
218
- const vars = new Set<string>();
219
- for (const s of schemes) {
220
- if (s.type === "http" && (s.scheme === "bearer" || s.scheme === "basic" || !s.scheme)) {
221
- vars.add(schemeVarName(s, schemes));
222
- } else if (s.type === "apiKey" && s.in === "header" && s.apiKeyName) {
223
- if (s.apiKeyName === "Authorization") vars.add(schemeVarName(s, schemes));
224
- else vars.add("api_key");
225
- }
226
- }
227
- return [...vars];
228
- }
229
-
230
- export async function setupApi(options: SetupApiOptions): Promise<SetupApiResult> {
231
- const { dbPath } = options;
232
- // ARV-375: `--spec` may be repeated. `specs` (from the variadic CLI
233
- // collector) wins; fall back to the single `spec` for back-compat.
234
- const specList = options.specs && options.specs.length > 0
235
- ? options.specs
236
- : (options.spec ? [options.spec] : []);
237
- const spec = specList[0];
238
-
239
- getDb(dbPath);
240
-
241
- // Try to load and validate spec, extract base_url
242
- let openapiSpec: string | null = null;
243
- let baseUrl = "";
244
- let endpointCount = 0;
245
- const pathParams = new Map<string, string>();
246
- const warnings: string[] = [];
247
- let specTitle: string | undefined;
248
- // The dereferenced doc is captured here so we can copy it into the
249
- // workspace after the target dir is computed (below). We snapshot the
250
- // *dereferenced* form so all consumers (probe-*, generate, describe) read
251
- // a self-contained file — no external $ref resolution at runtime.
252
- let dereferencedDoc: unknown = null;
253
- let authVarNames: string[] = [];
254
- if (specList.length > 0) {
255
- // Read + validate each spec independently. dereference() happily
256
- // round-trips arbitrary JSON (e.g. a marketing-site landing payload),
257
- // so without this guard `zond add api foo --spec https://example.com`
258
- // silently registers a 0-endpoint API.
259
- const loaded: { source: string; doc: any }[] = [];
260
- for (const s of specList) {
261
- const d = await readOpenApiSpec(s, { insecure: options.insecure, caPath: options.caPath });
262
- const dAny = d as any;
263
- if (typeof dAny?.openapi !== "string" && typeof dAny?.swagger !== "string") {
264
- throw new Error(
265
- `Spec at ${s} is not an OpenAPI/Swagger document — missing top-level 'openapi' (3.x) or 'swagger' (2.x) field. Check the URL points to the JSON spec, not the API root.`,
266
- );
267
- }
268
- loaded.push({ source: s, doc: d });
269
- }
270
-
271
- let doc: any;
272
- if (loaded.length === 1) {
273
- doc = loaded[0]!.doc;
274
- openapiSpec = spec!;
275
- } else {
276
- // ARV-375: deterministic union of N specs into one audit target.
277
- const { merged, summary } = mergeOpenApiDocs(loaded);
278
- doc = merged;
279
- openapiSpec = null; // no single source path — rely on the local snapshot
280
- const per = summary.sources.map((s) => `${s.paths} from ${s.source}`).join(", ");
281
- warnings.push(`Merged ${summary.sources.length} specs → ${summary.totalPaths} paths (${per}).`);
282
- if (summary.pathCollisions.length > 0) {
283
- warnings.push(
284
- `${summary.pathCollisions.length} path collision(s) — later spec wins: ${summary.pathCollisions.join(", ")}.`,
285
- );
286
- }
287
- if (summary.schemaConflicts.length > 0) {
288
- warnings.push(
289
- `${summary.schemaConflicts.length} component collision(s) with differing shapes (later wins, verify): ${summary.schemaConflicts.join(", ")}.`,
290
- );
291
- }
292
- }
293
- dereferencedDoc = doc;
294
- const docAny = doc as any;
295
- if ((doc as any).servers?.[0]?.url) {
296
- baseUrl = (doc as any).servers[0].url;
297
- // Resolve OpenAPI server variables (e.g. {region}) using their declared defaults.
298
- // Without this, the raw placeholder ends up in .env.yaml and causes cryptic TLS
299
- // errors because the hostname literally contains "{region}".
300
- const serverVars = (doc as any).servers[0].variables as
301
- Record<string, { default?: string }> | undefined;
302
- if (serverVars && baseUrl.includes("{")) {
303
- baseUrl = baseUrl.replace(/\{([^}]+)\}/g, (_: string, name: string) =>
304
- serverVars[name]?.default ?? `{${name}}`
305
- );
306
- }
307
- // Warn if any placeholder remains unresolved (spec didn't provide a default).
308
- const unresolved = [...baseUrl.matchAll(/\{([^}]+)\}/g)].map(m => m[1]);
309
- if (unresolved.length > 0) {
310
- warnings.push(
311
- `base_url contains unresolved server variable${unresolved.length === 1 ? "" : "s"}: ${unresolved.map(v => `{${v}}`).join(", ")}. Edit .env.yaml and replace with a concrete value.`,
312
- );
313
- }
314
- }
315
- if (baseUrl && !baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) {
316
- warnings.push(`Spec server URL "${baseUrl}" is relative — requests will fail without a host. Override with envVars: {"base_url": "https://your-host${baseUrl}"}`);
317
- }
318
- specTitle = (doc as any).info?.title;
319
- const endpoints = extractEndpoints(doc);
320
- endpointCount = endpoints.length;
321
- authVarNames = deriveAuthVarNames(extractSecuritySchemes(doc));
322
-
323
- if (endpointCount === 0) {
324
- const hasPaths = docAny?.paths && typeof docAny.paths === "object" && Object.keys(docAny.paths).length > 0;
325
- warnings.push(
326
- hasPaths
327
- ? `Spec declares paths but no operations were extracted — every method may be filtered out (deprecated, unsupported method, etc.). Verify with \`zond catalog --api <name>\`.`
328
- : `Spec contains 0 endpoints — 'paths' field is empty or missing. generate/probe/checks will produce nothing until the spec is fixed or replaced.`,
329
- );
330
- }
331
-
332
- // Collect unique path parameters. The default is empty string so that
333
- // generated `skip_if: "{{<id>}} =="` checks auto-skip until the user
334
- // fills the value in .env.yaml (TASK-210). Spec-provided examples are
335
- // kept verbatim so they are still useful as concrete fixtures.
336
- for (const ep of endpoints) {
337
- for (const param of (ep.parameters ?? []).filter(p => p.in === "path")) {
338
- if (pathParams.has(param.name)) continue;
339
- const schema = param.schema as any;
340
- if (param.example !== undefined) pathParams.set(param.name, String(param.example));
341
- else if (schema?.example !== undefined) pathParams.set(param.name, String(schema.example));
342
- else pathParams.set(param.name, "");
343
- }
344
- }
345
- }
346
-
347
- // Derive name: explicit > spec title > filename
348
- const name = options.name
349
- ?? specTitle?.replace(/[^a-zA-Z0-9_\-\.]/g, "-").toLowerCase()
350
- ?? spec?.split(/[/\\]/).pop()?.replace(/\.\w+$/, "")
351
- ?? "api";
352
-
353
- // Validate name uniqueness (or force-replace)
354
- const existing = findCollectionByNameOrId(name);
355
- if (existing) {
356
- if (options.force) {
357
- deleteCollection(existing.id, true);
358
- } else {
359
- throw new Error(`API '${name}' already exists (id=${existing.id})`);
360
- }
361
- }
362
-
363
- // Sanitize name for directory use
364
- const dirName = name.replace(/[^a-zA-Z0-9_\-\.]/g, "-").toLowerCase();
365
- const baseDir = options.dir
366
- ? resolve(options.dir)
367
- : resolve(findWorkspaceRoot().root, `apis/${dirName}/`);
368
- const testPath = join(baseDir, "tests");
369
-
370
- // Track whether we created baseDir from scratch so we can clean it up on
371
- // failure — without this, a crash mid-setup (e.g. JSON.stringify on a
372
- // cyclic spec, ARV-145) leaves apis/<slug>/tests/ behind and confuses the
373
- // next `zond add api` invocation.
374
- const baseDirPreExisted = existsSync(baseDir);
375
-
376
- // Create directories
377
- mkdirSync(testPath, { recursive: true });
378
-
379
- try {
380
- return await finalizeSetup({
381
- name,
382
- baseDir,
383
- testPath,
384
- baseUrl,
385
- pathParams,
386
- authVarNames,
387
- envVarsOverride: options.envVars,
388
- spec,
389
- dereferencedDoc,
390
- openapiSpec,
391
- endpointCount,
392
- warnings,
393
- });
394
- } catch (err) {
395
- // Roll back partial filesystem state (apis/<slug>/tests/, spec.json, etc.)
396
- // when we created the dir from scratch. Without this, the next
397
- // `zond add api <same-name>` would still find a stale dir and demand
398
- // --force, even though no collection was actually registered. ARV-145.
399
- if (!baseDirPreExisted) {
400
- try { rmSync(baseDir, { recursive: true, force: true }); } catch { /* best-effort */ }
401
- }
402
- throw err;
403
- }
404
- }
405
-
406
- interface FinalizeSetupParams {
407
- name: string;
408
- baseDir: string;
409
- testPath: string;
410
- baseUrl: string;
411
- pathParams: Map<string, string>;
412
- authVarNames: string[];
413
- envVarsOverride?: Record<string, string>;
414
- spec?: string;
415
- dereferencedDoc: unknown;
416
- openapiSpec: string | null;
417
- endpointCount: number;
418
- warnings: string[];
419
- }
420
-
421
- async function finalizeSetup(p: FinalizeSetupParams): Promise<SetupApiResult> {
422
- const {
423
- name, baseDir, testPath, baseUrl, pathParams, authVarNames,
424
- envVarsOverride, spec, dereferencedDoc, openapiSpec, endpointCount, warnings,
425
- } = p;
426
-
427
- // Build environment variables
428
- const envVars: Record<string, string> = {};
429
- if (baseUrl) envVars.base_url = baseUrl;
430
- // Add path parameter defaults (before user overrides)
431
- for (const [k, v] of pathParams) {
432
- if (!(k in envVars)) envVars[k] = v;
433
- }
434
- // Auto-wire auth env-vars to .secrets.yaml so generated suites and probes
435
- // resolve `{{auth_token}}` (etc.) without manual editing of .env.yaml
436
- // (TASK-209). The matching `<var>: ""` placeholder is seeded into
437
- // .secrets.yaml below — the user only fills the secret value.
438
- for (const v of authVarNames) {
439
- if (!(v in envVars)) envVars[v] = `@secret:${v}`;
440
- }
441
- // ARV-201 (R10/F2): when the spec declares no `components.securitySchemes`
442
- // (GitHub publishes its OpenAPI this way), `deriveAuthVarNames` returns []
443
- // and the loop above is a no-op — yet `zond request --api <name>` knows
444
- // to attach `Authorization: Bearer <auth_token>` if the env carries an
445
- // `auth_token`. Mirror the `.secrets.yaml` fallback (which already seeds
446
- // `auth_token: ""` when authVarNames is empty) into `.env.yaml` so users
447
- // do not need to hand-add `auth_token: "@secret:auth_token"` just to
448
- // surface the Bearer header on bare specs.
449
- if (authVarNames.length === 0 && !("auth_token" in envVars)) {
450
- envVars.auth_token = "@secret:auth_token";
451
- }
452
- if (envVarsOverride) {
453
- Object.assign(envVars, envVarsOverride);
454
- }
455
-
456
- // Spec-less registration is allowed, but we need a base_url from somewhere
457
- // (server URL extracted from the spec, or envVars.base_url passed in by the
458
- // caller). Without it the API is useless — `zond run` can't resolve {{base_url}}.
459
- if (!spec && !envVars.base_url) {
460
- throw new Error("setupApi requires --spec or envVars.base_url to register an API");
461
- }
462
-
463
- // Write .env.yaml in base_dir
464
- if (Object.keys(envVars).length > 0) {
465
- const envFilePath = join(baseDir, ".env.yaml");
466
- writeFileSync(envFilePath, toYaml(envVars) + "\n", "utf-8");
467
- }
468
-
469
- // Create/update .gitignore to exclude env / secret files
470
- const gitignorePath = join(baseDir, ".gitignore");
471
- let gitignoreContent = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : "";
472
- let gitignoreDirty = false;
473
- if (!gitignoreContent.includes(".env*.yaml")) {
474
- gitignoreContent +=
475
- (gitignoreContent.endsWith("\n") || !gitignoreContent ? "" : "\n") + ".env*.yaml\n";
476
- gitignoreDirty = true;
477
- }
478
- // TASK-170 (m-10): keep `.secrets.yaml` git-invisible. Older `.env*.yaml`
479
- // pattern matched it accidentally; pin it explicitly so a future glob
480
- // narrowing can't regress.
481
- if (!gitignoreContent.includes(".secrets.yaml")) {
482
- gitignoreContent += ".secrets.yaml\n";
483
- gitignoreDirty = true;
484
- }
485
- // TASK-174 (m-10): identity values are not secrets but they identify
486
- // the user's account; keep them out of git too.
487
- if (!gitignoreContent.includes(".identity.yaml")) {
488
- gitignoreContent += ".identity.yaml\n";
489
- gitignoreDirty = true;
490
- }
491
- if (gitignoreDirty) {
492
- writeFileSync(gitignorePath, gitignoreContent, "utf-8");
493
- }
494
-
495
- // Seed `.secrets.yaml` placeholder once. The file lives gitignored
496
- // alongside `.env.yaml`; values placed here are auto-registered with
497
- // the SecretRegistry on load and never appear in artifacts.
498
- const secretsPath = join(baseDir, ".secrets.yaml");
499
- if (!existsSync(secretsPath)) {
500
- const seedKeys = authVarNames.length > 0 ? authVarNames : ["auth_token"];
501
- const lines = [
502
- "# .secrets.yaml — gitignored, holds raw secret values.",
503
- "# Reference these in .env.yaml as @secret:<key>.",
504
- "# Values here are auto-registered for redaction in DB writes,",
505
- "# HTML/JSON/JUnit reports, case-studies, and probe digests.",
506
- ];
507
- for (const k of seedKeys) lines.push(`${k}: "" # required for live probes`);
508
- lines.push("");
509
- writeFileSync(secretsPath, lines.join("\n"), "utf-8");
510
- }
511
-
512
- // TASK-174 (m-10): seed `.identity.yaml` with placeholders for any
513
- // canonical identity-keys that appear as path-params in the spec. The
514
- // file is gitignored — values are visible locally for triage and
515
- // hidden from outbound shares only when --redact-identity is set.
516
- const identityKeys = [...pathParams.keys()].filter((k) =>
517
- CANONICAL_IDENTITY_KEYS.has(k),
518
- );
519
- if (identityKeys.length > 0) {
520
- const identityPath = join(baseDir, ".identity.yaml");
521
- if (!existsSync(identityPath)) {
522
- const lines = [
523
- "# .identity.yaml — gitignored, holds non-secret-but-identifying values.",
524
- "# Reference these in .env.yaml as @identity:<key>.",
525
- "# Values are visible locally and in case-study drafts; pass",
526
- "# --redact-identity (TASK-173) to swap them for placeholders when",
527
- "# sharing reports outbound.",
528
- ];
529
- for (const k of identityKeys.sort()) {
530
- lines.push(`${k}: "" # fill with your ${k}`);
531
- }
532
- lines.push("");
533
- writeFileSync(identityPath, lines.join("\n"), "utf-8");
534
- }
535
- }
536
-
537
- const workspaceRoot = findWorkspaceRoot().root;
538
-
539
- // Snapshot the dereferenced spec into apis/<name>/spec.json so all later
540
- // commands (catalog, describe, generate, probe-*) read a self-contained
541
- // local file. The spec lives inside the workspace and is git-trackable;
542
- // an external --spec path is only consulted at register/refresh time.
543
- let localSpecAbsPath: string | null = null;
544
- if (dereferencedDoc) {
545
- localSpecAbsPath = join(baseDir, SPEC_SNAPSHOT_FILENAME);
546
- writeArtifactsFromDoc({
547
- doc: dereferencedDoc,
548
- baseDir,
549
- apiName: name,
550
- baseUrl,
551
- workspaceRoot,
552
- });
553
- }
554
-
555
- const normalizedTestPath = normalizePath(testPath);
556
- const normalizedBaseDir = normalizePath(baseDir);
557
-
558
- // Persist the workspace-relative path to the local snapshot in
559
- // collections.openapi_spec so we don't rely on the user's external path
560
- // sticking around. Falls back to the external path only when the snapshot
561
- // could not be created (no spec given to setupApi).
562
- // Don't run normalizePath on the relative form — it calls resolve() and
563
- // would re-absolutize the path. Posix-style separators are enough for
564
- // SQLite + Windows compat.
565
- const dbSpecPath = localSpecAbsPath
566
- ? relative(workspaceRoot, localSpecAbsPath).replace(/\\/g, "/")
567
- : (openapiSpec ?? undefined);
568
-
569
- // Create collection in DB
570
- const collectionId = createCollection({
571
- name,
572
- base_dir: normalizedBaseDir,
573
- test_path: normalizedTestPath,
574
- openapi_spec: dbSpecPath,
575
- });
576
-
577
- const pathParamsObj = pathParams.size > 0 ? Object.fromEntries(pathParams) : undefined;
578
-
579
- return {
580
- created: true,
581
- collectionId,
582
- baseDir: normalizedBaseDir,
583
- testPath: normalizedTestPath,
584
- baseUrl,
585
- specEndpoints: endpointCount,
586
- ...(pathParamsObj ? { pathParams: pathParamsObj } : {}),
587
- ...(authVarNames.length > 0 ? { authVars: authVarNames } : {}),
588
- ...(warnings.length > 0 ? { warnings } : {}),
589
- };
590
- }
@@ -1,94 +0,0 @@
1
- /**
2
- * Finding category taxonomy (ARV-251, m-21 pivot).
3
- *
4
- * Four categories replace the old SARIF-internal trio
5
- * (conformance/security/data-rejection/other). Each finding belongs to
6
- * exactly one category. Categories drive the per-section roll-up in
7
- * reports — a small team sees `0 security, 12 reliability, 40 contract,
8
- * 200 hygiene` and knows where to start, instead of one flat HIGH/LOW
9
- * pile.
10
- *
11
- * Definitions:
12
- * - `security`: exploit / auth / data-exposure / injection signals.
13
- * IDOR, mass-assignment with persistence, missing-auth, open CORS,
14
- * reflected XSS / CRLF in dangerous context.
15
- * - `reliability`: server crashes / 5xx on valid input / rate-limit
16
- * absent / timeouts. Not security per se but production-impact.
17
- * - `contract`: spec ↔ runtime drift. Schema mismatch, wrong status
18
- * codes, content-type negotiation failures, data-rejection contract
19
- * violations, missing required headers.
20
- * - `hygiene`: static spec-lint, accept-without-impact, framework-level
21
- * "could be intentional" signals, naming/style. Bulk volume lives here.
22
- */
23
-
24
- export type Category = "security" | "reliability" | "contract" | "hygiene";
25
-
26
- export const CATEGORY_ORDER: readonly Category[] = [
27
- "security",
28
- "reliability",
29
- "contract",
30
- "hygiene",
31
- ] as const;
32
-
33
- /**
34
- * Category lookup by check-id / probe-class-id. Adding a new
35
- * finding-producer requires extending this map — the SARIF + reporter
36
- * tests assert full coverage so a missing entry fails loudly rather
37
- * than silently routing to a fallback.
38
- */
39
- export const CATEGORY_BY_ID: Record<string, Category> = {
40
- // ── reliability ──────────────────────────────────────────────
41
- // 5xx on valid input is a server crash, not a security issue.
42
- not_a_server_error: "reliability",
43
-
44
- // ── contract ─────────────────────────────────────────────────
45
- // Spec-conformance checks. Server behaviour drifts from declared
46
- // contract — fix-worthy, but rarely security per se.
47
- status_code_conformance: "contract",
48
- content_type_conformance: "contract",
49
- response_headers_conformance: "contract",
50
- response_schema_conformance: "contract",
51
- missing_required_header: "contract",
52
- unsupported_method: "contract",
53
- // Data-rejection: server should reject malformed bodies per spec.
54
- // Falls under contract (spec said "reject", server accepted).
55
- negative_data_rejection: "contract",
56
- positive_data_acceptance: "contract",
57
- // m-20 state-aware probes — cross-resource contract invariants.
58
- cross_call_references: "contract",
59
- idempotency_replay: "contract",
60
- pagination_invariants: "contract",
61
- lifecycle_transitions: "contract",
62
- // ARV-256 (m-21) — small-team value-add. Rate-limit absence is a
63
- // production reliability concern, not a security exploit.
64
- rate_limit_headers_absent: "reliability",
65
- open_cors_on_sensitive: "security",
66
-
67
- // ── security ─────────────────────────────────────────────────
68
- ignored_auth: "security",
69
- use_after_free: "security",
70
- ensure_resource_availability: "security",
71
- // Probe classes
72
- "mass-assignment": "security",
73
- ssrf: "security",
74
- crlf: "security",
75
- xss: "security",
76
- sqli: "security",
77
- "open-redirect": "security",
78
- "path-traversal": "security",
79
- webhooks: "security",
80
- };
81
-
82
- /**
83
- * Map check-id / probe-class-id to category. Falls back to "hygiene"
84
- * for unknown ids — the SARIF reporter & tests assert this fallback
85
- * never triggers for registered checks/probes.
86
- */
87
- export function categoryFor(id: string): Category {
88
- return CATEGORY_BY_ID[id] ?? "hygiene";
89
- }
90
-
91
- export function emptyCategoryBuckets(): Record<Category, number> {
92
- return { security: 0, reliability: 0, contract: 0, hygiene: 0 };
93
- }
94
-