@kirrosh/zond 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +648 -0
- package/README.md +58 -6
- package/package.json +9 -6
- package/src/cli/argv.ts +122 -0
- package/src/cli/commands/add-api.ts +134 -0
- package/src/cli/commands/api/annotate/idempotency.ts +59 -0
- package/src/cli/commands/api/annotate/index.ts +525 -0
- package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
- package/src/cli/commands/api/annotate/overlay.ts +206 -0
- package/src/cli/commands/api/annotate/pagination.ts +60 -0
- package/src/cli/commands/api/annotate/prompts.ts +183 -0
- package/src/cli/commands/api/annotate/readback.ts +58 -0
- package/src/cli/commands/api/annotate/resources.ts +91 -0
- package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
- package/src/cli/commands/audit.ts +480 -0
- package/src/cli/commands/bootstrap.ts +710 -0
- package/src/cli/commands/catalog.ts +35 -0
- package/src/cli/commands/check.ts +348 -0
- package/src/cli/commands/checks.ts +756 -0
- package/src/cli/commands/ci-init.ts +43 -0
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +262 -0
- package/src/cli/commands/completions.ts +16 -0
- package/src/cli/commands/coverage.ts +605 -132
- package/src/cli/commands/db.ts +178 -7
- package/src/cli/commands/describe.ts +37 -2
- package/src/cli/commands/discover.ts +1236 -0
- package/src/cli/commands/doctor.ts +607 -0
- package/src/cli/commands/fixtures.ts +402 -0
- package/src/cli/commands/generate.ts +420 -46
- package/src/cli/commands/init/bootstrap.ts +30 -1
- package/src/cli/commands/{init.ts → init/index.ts} +99 -5
- package/src/cli/commands/init/skills.ts +56 -3
- package/src/cli/commands/init/templates/agents.md +65 -61
- package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
- package/src/cli/commands/init/templates/skills/zond.md +592 -125
- package/src/cli/commands/init/templates/zond-config.yml +8 -9
- package/src/cli/commands/prepare-fixtures.ts +135 -0
- package/src/cli/commands/probe/mass-assignment.ts +503 -0
- package/src/cli/commands/probe/security.ts +454 -0
- package/src/cli/commands/probe/static.ts +255 -0
- package/src/cli/commands/probe/webhooks.ts +161 -0
- package/src/cli/commands/probe.ts +459 -0
- package/src/cli/commands/reference.ts +87 -0
- package/src/cli/commands/refresh-api.ts +169 -0
- package/src/cli/commands/remove-api.ts +150 -0
- package/src/cli/commands/report-bundle.ts +318 -0
- package/src/cli/commands/report.ts +241 -0
- package/src/cli/commands/request.ts +379 -4
- package/src/cli/commands/run.ts +842 -53
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +18 -1
- package/src/cli/index.ts +20 -3
- package/src/cli/json-envelope.ts +112 -3
- package/src/cli/json-schemas.ts +263 -0
- package/src/cli/program.ts +198 -635
- package/src/cli/resolve.ts +105 -0
- package/src/cli/status-filter.ts +124 -0
- package/src/cli/util/api-context.ts +85 -0
- package/src/cli/version.ts +5 -0
- package/src/core/anti-fp/bootstrap.ts +34 -0
- package/src/core/anti-fp/index.ts +33 -0
- package/src/core/anti-fp/registry.ts +44 -0
- package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
- package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
- package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
- package/src/core/anti-fp/types.ts +68 -0
- package/src/core/checks/checks/_crud-helpers.ts +133 -0
- package/src/core/checks/checks/_negative_mutator.ts +133 -0
- package/src/core/checks/checks/_readback-helpers.ts +133 -0
- package/src/core/checks/checks/content_type_conformance.ts +39 -0
- package/src/core/checks/checks/cross_call_references.ts +134 -0
- package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
- package/src/core/checks/checks/idempotency_replay.ts +246 -0
- package/src/core/checks/checks/ignored_auth.ts +211 -0
- package/src/core/checks/checks/index.ts +65 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
- package/src/core/checks/checks/missing_required_header.ts +40 -0
- package/src/core/checks/checks/negative_data_rejection.ts +45 -0
- package/src/core/checks/checks/not_a_server_error.ts +27 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
- package/src/core/checks/checks/pagination_invariants.ts +238 -0
- package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
- package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
- package/src/core/checks/checks/response_headers_conformance.ts +74 -0
- package/src/core/checks/checks/response_schema_conformance.ts +30 -0
- package/src/core/checks/checks/status_code_conformance.ts +61 -0
- package/src/core/checks/checks/unsupported_method.ts +63 -0
- package/src/core/checks/checks/use_after_free.ts +78 -0
- package/src/core/checks/index.ts +30 -0
- package/src/core/checks/mode.ts +79 -0
- package/src/core/checks/recommended-action.ts +64 -0
- package/src/core/checks/registry.ts +78 -0
- package/src/core/checks/runner.ts +874 -0
- package/src/core/checks/sarif.ts +230 -0
- package/src/core/checks/stateful.ts +121 -0
- package/src/core/checks/types.ts +189 -0
- package/src/core/classifier/recommended-action.ts +222 -0
- package/src/core/context/current.ts +22 -6
- package/src/core/context/session.ts +78 -0
- package/src/core/coverage/loader.ts +185 -0
- package/src/core/coverage/reasons.ts +300 -0
- package/src/core/diagnostics/db-analysis.ts +151 -11
- package/src/core/diagnostics/failure-class.ts +120 -0
- package/src/core/diagnostics/failure-hints.ts +212 -9
- package/src/core/diagnostics/spec-pointer.ts +99 -0
- package/src/core/diagnostics/suggested-fixes.ts +156 -0
- package/src/core/exporter/case-study/index.ts +270 -0
- package/src/core/exporter/curl.ts +40 -0
- package/src/core/exporter/exporter.ts +48 -0
- package/src/core/exporter/html-report/escape.ts +24 -0
- package/src/core/exporter/html-report/index.ts +479 -0
- package/src/core/exporter/html-report/script.ts +100 -0
- package/src/core/exporter/html-report/styles.ts +408 -0
- package/src/core/generator/chunker.ts +42 -16
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/create-body.ts +89 -0
- package/src/core/generator/data-factory.ts +445 -19
- package/src/core/generator/describe.ts +1 -1
- package/src/core/generator/fixtures-builder.ts +325 -0
- package/src/core/generator/index.ts +7 -5
- package/src/core/generator/openapi-reader.ts +37 -3
- package/src/core/generator/path-param-disambig.ts +114 -0
- package/src/core/generator/resources-builder.ts +648 -0
- package/src/core/generator/schema-utils.ts +11 -3
- package/src/core/generator/serializer.ts +103 -13
- package/src/core/generator/suite-generator.ts +419 -111
- package/src/core/generator/types.ts +8 -0
- package/src/core/identity/identity-file.ts +129 -0
- package/src/core/lint/affects.ts +28 -0
- package/src/core/lint/config.ts +96 -0
- package/src/core/lint/format.ts +42 -0
- package/src/core/lint/index.ts +94 -0
- package/src/core/lint/reporter.ts +128 -0
- package/src/core/lint/rules/consistency.ts +158 -0
- package/src/core/lint/rules/heuristics.ts +97 -0
- package/src/core/lint/rules/strictness.ts +109 -0
- package/src/core/lint/types.ts +96 -0
- package/src/core/lint/walker.ts +248 -0
- package/src/core/meta/meta-store.ts +6 -73
- package/src/core/output/README.md +91 -0
- package/src/core/output/index.ts +13 -0
- package/src/core/output/run.ts +126 -0
- package/src/core/output/types.ts +129 -0
- package/src/core/parser/env-interpolation.ts +104 -0
- package/src/core/parser/filter.ts +57 -0
- package/src/core/parser/schema.ts +129 -4
- package/src/core/parser/types.ts +19 -1
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +58 -12
- package/src/core/probe/bootstrap.ts +34 -0
- package/src/core/probe/dry-run-envelope.ts +57 -0
- package/src/core/probe/mass-assignment-probe-class.ts +198 -0
- package/src/core/probe/mass-assignment-probe.ts +1122 -0
- package/src/core/probe/mass-assignment-template.ts +212 -0
- package/src/core/probe/method-probe.ts +43 -76
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +183 -149
- package/src/core/probe/orphan-tracker.ts +188 -0
- package/src/core/probe/path-discovery.ts +440 -0
- package/src/core/probe/probe-harness.ts +120 -0
- package/src/core/probe/registry.ts +89 -0
- package/src/core/probe/runner.ts +136 -0
- package/src/core/probe/security-probe-class.ts +201 -0
- package/src/core/probe/security-probe.ts +1453 -0
- package/src/core/probe/shared.ts +505 -0
- package/src/core/probe/static-probe-class.ts +125 -0
- package/src/core/probe/types.ts +165 -0
- package/src/core/probe/verdict-aggregator.ts +33 -0
- package/src/core/probe/webhooks-probe.ts +284 -0
- package/src/core/reporter/console.ts +41 -2
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +11 -1
- package/src/core/reporter/junit.ts +27 -12
- package/src/core/reporter/ndjson.ts +37 -0
- package/src/core/reporter/types.ts +3 -0
- package/src/core/runner/assertions.ts +58 -1
- package/src/core/runner/async-pool.ts +108 -0
- package/src/core/runner/auth-path.ts +8 -0
- package/src/core/runner/ci-context.ts +72 -0
- package/src/core/runner/executor.ts +264 -20
- package/src/core/runner/form-encode.ts +51 -0
- package/src/core/runner/http-client.ts +75 -2
- package/src/core/runner/learn-drift.ts +293 -0
- package/src/core/runner/preflight-vars.ts +149 -0
- package/src/core/runner/progress-tracker.ts +73 -0
- package/src/core/runner/rate-limiter.ts +89 -17
- package/src/core/runner/run-kind.ts +39 -0
- package/src/core/runner/schema-validator.ts +312 -0
- package/src/core/runner/send-request.ts +153 -20
- package/src/core/runner/types.ts +38 -0
- package/src/core/secrets/registry.ts +164 -0
- package/src/core/secrets/secrets-file.ts +115 -0
- package/src/core/selectors/operation-filter.ts +144 -0
- package/src/core/setup-api.ts +415 -16
- package/src/core/severity/category.ts +94 -0
- package/src/core/severity/index.ts +121 -0
- package/src/core/spec/layers.ts +154 -0
- package/src/core/util/format-eta.ts +21 -0
- package/src/core/utils.ts +5 -1
- package/src/core/workspace/config.ts +129 -0
- package/src/core/workspace/manifest.ts +283 -0
- package/src/core/workspace/output-rotation.ts +62 -0
- package/src/core/workspace/triage-path.ts +87 -0
- package/src/db/lint-runs.ts +47 -0
- package/src/db/migrate.ts +126 -0
- package/src/db/migrations/0001_run_kind.sql +25 -0
- package/src/db/migrations/sql.d.ts +4 -0
- package/src/db/queries/collections.ts +133 -0
- package/src/db/queries/coverage.ts +9 -0
- package/src/db/queries/dashboard.ts +59 -0
- package/src/db/queries/results.ts +128 -0
- package/src/db/queries/runs.ts +235 -0
- package/src/db/queries/sessions.ts +42 -0
- package/src/db/queries/settings.ts +28 -0
- package/src/db/queries/types.ts +172 -0
- package/src/db/queries.ts +72 -802
- package/src/db/schema.ts +178 -50
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
- package/src/cli/commands/probe-methods.ts +0 -108
- package/src/cli/commands/probe-validation.ts +0 -124
- package/src/cli/commands/serve.ts +0 -114
- package/src/cli/commands/sync.ts +0 -268
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/diagnostics/render-md.ts +0 -112
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -19
- package/src/core/parser/index.ts +0 -21
- package/src/core/runner/execute-run.ts +0 -132
- package/src/core/runner/index.ts +0 -12
- package/src/core/sync/spec-differ.ts +0 -38
- package/src/web/data/collection-state.ts +0 -362
- package/src/web/routes/api.ts +0 -314
- package/src/web/routes/dashboard.ts +0 -350
- package/src/web/routes/runs.ts +0 -64
- package/src/web/schemas.ts +0 -121
- package/src/web/server.ts +0 -134
- package/src/web/static/htmx.min.cjs +0 -1
- package/src/web/static/style.css +0 -1148
- package/src/web/views/endpoints-tab.ts +0 -174
- package/src/web/views/explorer-tab.ts +0 -402
- package/src/web/views/health-strip.ts +0 -92
- package/src/web/views/layout.ts +0 -48
- package/src/web/views/results.ts +0 -210
- package/src/web/views/runs-tab.ts +0 -126
- package/src/web/views/suites-tab.ts +0 -181
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `zond audit --api X` — macro-команда для полного pipeline (TASK-262).
|
|
3
|
+
*
|
|
4
|
+
* Оборачивает 8-10 ручных шагов (prepare-fixtures → generate → probes
|
|
5
|
+
* → session-wrapped run → coverage → HTML report) в одну команду:
|
|
6
|
+
*
|
|
7
|
+
* 1. `prepare-fixtures --apply` (или `--cascade --seed --apply` при
|
|
8
|
+
* `--seed`) — заполняет `.env.yaml` FK-идентификаторами.
|
|
9
|
+
* 2. `generate` — пропускается если `apis/<name>/tests/` свежее, чем
|
|
10
|
+
* `spec.json` (mtime-эвристика; `--force` отключает skip).
|
|
11
|
+
* 3. `probe static` (validation+methods, всегда). `mass-assignment` и
|
|
12
|
+
* `security` — за `--with-mass-assignment` / `--with-security`.
|
|
13
|
+
* 4. `session start` → `run apis/<name>/tests` + `run apis/<name>/probes`
|
|
14
|
+
* → `session end`. Все runs наследуют один session_id.
|
|
15
|
+
* 5. `coverage --api X --union session --json` для embed'a в репорт.
|
|
16
|
+
* 6. Запись `audit-report.html` (или `--out`) с таблицей stages,
|
|
17
|
+
* coverage-сводкой и подсказками для drill-down.
|
|
18
|
+
*
|
|
19
|
+
* Каждая stage спавнится как отдельный subprocess `zond ...`. Failure
|
|
20
|
+
* любой stage НЕ останавливает pipeline — финальный exit 1 если хоть одна
|
|
21
|
+
* упала, 0 если все ok. `--dry-run` печатает план без выполнения.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync, statSync } from "node:fs";
|
|
25
|
+
import { writeFile } from "node:fs/promises";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import type { Command } from "commander";
|
|
28
|
+
import { globalJson } from "../resolve.ts";
|
|
29
|
+
import { getDb } from "../../db/schema.ts";
|
|
30
|
+
import { findCollectionByNameOrId } from "../../db/queries.ts";
|
|
31
|
+
import { resolveCollectionSpec } from "../../core/setup-api.ts";
|
|
32
|
+
import { printSuccess, printWarning, printError } from "../output.ts";
|
|
33
|
+
import { getApi, MISSING_API_MESSAGE } from "../util/api-context.ts";
|
|
34
|
+
import { jsonOk, printJson } from "../json-envelope.ts";
|
|
35
|
+
import { VERSION } from "../version.ts";
|
|
36
|
+
|
|
37
|
+
interface Stage {
|
|
38
|
+
key: string;
|
|
39
|
+
name: string;
|
|
40
|
+
args: string[];
|
|
41
|
+
/** If returns string, stage is skipped with that reason. */
|
|
42
|
+
skip?: () => string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface StageResult {
|
|
46
|
+
key: string;
|
|
47
|
+
name: string;
|
|
48
|
+
status: "ok" | "failed" | "skipped";
|
|
49
|
+
exit_code: number | null;
|
|
50
|
+
duration_ms: number;
|
|
51
|
+
reason?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface AuditOptions {
|
|
55
|
+
api: string;
|
|
56
|
+
dbPath?: string;
|
|
57
|
+
seed?: boolean;
|
|
58
|
+
withMassAssignment?: boolean;
|
|
59
|
+
withSecurity?: boolean;
|
|
60
|
+
out?: string;
|
|
61
|
+
dryRun?: boolean;
|
|
62
|
+
force?: boolean;
|
|
63
|
+
json?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build the prefix for self-spawning `zond ...`. When the binary is
|
|
68
|
+
* compiled, `process.execPath` IS the zond binary. In dev, `bun` runs the
|
|
69
|
+
* script directly — fall back to `[bun, src/cli/index.ts]`.
|
|
70
|
+
*/
|
|
71
|
+
function zondInvoker(): string[] {
|
|
72
|
+
const exec = process.execPath;
|
|
73
|
+
const base = exec.replace(/\\/g, "/");
|
|
74
|
+
if (base.endsWith("/zond") || base.endsWith("/zond.exe")) return [exec];
|
|
75
|
+
const script = process.argv[1] || "src/cli/index.ts";
|
|
76
|
+
return [exec, script];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildStages(opts: AuditOptions, apiDir: string, specPath: string | null): Stage[] {
|
|
80
|
+
const api = opts.api;
|
|
81
|
+
const stages: Stage[] = [];
|
|
82
|
+
|
|
83
|
+
if (opts.seed) {
|
|
84
|
+
stages.push({
|
|
85
|
+
key: "prepare-fixtures-cascade",
|
|
86
|
+
name: "prepare-fixtures (cascade discover + seed)",
|
|
87
|
+
args: ["prepare-fixtures", "--api", api, "--apply", "--seed"],
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
stages.push({
|
|
91
|
+
key: "prepare-fixtures",
|
|
92
|
+
name: "prepare-fixtures (path-FK fixtures)",
|
|
93
|
+
args: ["prepare-fixtures", "--api", api, "--apply"],
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
stages.push({
|
|
98
|
+
key: "generate",
|
|
99
|
+
name: "generate (smoke + crud)",
|
|
100
|
+
args: ["generate", "--api", api, "--output", join(apiDir, "tests")],
|
|
101
|
+
skip: () => {
|
|
102
|
+
if (opts.force) return null;
|
|
103
|
+
if (!specPath || !existsSync(specPath)) return null;
|
|
104
|
+
const testsDir = join(apiDir, "tests");
|
|
105
|
+
if (!existsSync(testsDir)) return null;
|
|
106
|
+
try {
|
|
107
|
+
const specMtime = statSync(specPath).mtimeMs;
|
|
108
|
+
const testsMtime = statSync(testsDir).mtimeMs;
|
|
109
|
+
if (testsMtime > specMtime) return "tests/ newer than spec — pass --force to regenerate";
|
|
110
|
+
} catch {
|
|
111
|
+
// ignore — fall through to running generate
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
stages.push({
|
|
118
|
+
key: "probe-static",
|
|
119
|
+
name: "probe static (validation+methods)",
|
|
120
|
+
args: ["probe", "static", "--api", api, "--output", join(apiDir, "probes", "static")],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (opts.withMassAssignment) {
|
|
124
|
+
stages.push({
|
|
125
|
+
key: "probe-mass-assignment",
|
|
126
|
+
name: "probe mass-assignment",
|
|
127
|
+
args: [
|
|
128
|
+
"probe", "mass-assignment", "--api", api,
|
|
129
|
+
"--output", join(apiDir, "probes", "mass-assignment-digest.md"),
|
|
130
|
+
"--emit-tests", join(apiDir, "probes", "mass-assignment"),
|
|
131
|
+
"--overwrite",
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (opts.withSecurity) {
|
|
136
|
+
stages.push({
|
|
137
|
+
key: "probe-security",
|
|
138
|
+
name: "probe security (ssrf,crlf,open-redirect)",
|
|
139
|
+
args: [
|
|
140
|
+
"probe", "security", "ssrf,crlf,open-redirect", "--api", api,
|
|
141
|
+
"--output", join(apiDir, "probes", "security-digest.md"),
|
|
142
|
+
"--emit-tests", join(apiDir, "probes", "security"),
|
|
143
|
+
"--overwrite",
|
|
144
|
+
],
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const sessionLabel = `audit-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)}`;
|
|
149
|
+
stages.push({ key: "session-start", name: `session start (${sessionLabel})`, args: ["session", "start", "--label", sessionLabel] });
|
|
150
|
+
stages.push({ key: "run-tests", name: "run tests", args: ["run", join(apiDir, "tests"), "--api", api] });
|
|
151
|
+
stages.push({ key: "run-probes", name: "run probes", args: ["run", join(apiDir, "probes"), "--api", api] });
|
|
152
|
+
stages.push({ key: "session-end", name: "session end", args: ["session", "end"] });
|
|
153
|
+
// ARV-108: surface the post-stage coverage capture in the plan so the
|
|
154
|
+
// dry-run listing matches the actual pipeline. The stage is special-cased
|
|
155
|
+
// in auditCommand — we keep stdout for JSON parsing rather than inheriting.
|
|
156
|
+
stages.push({
|
|
157
|
+
key: "coverage",
|
|
158
|
+
name: "coverage (session union)",
|
|
159
|
+
args: ["coverage", "--api", api, "--union", "session", "--json"],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return stages;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function runStage(stage: Stage, idx: number, total: number, json: boolean): Promise<StageResult> {
|
|
166
|
+
const skipReason = stage.skip?.();
|
|
167
|
+
if (skipReason) {
|
|
168
|
+
if (!json) console.log(`==> Stage ${idx}/${total}: ${stage.name} — skipped (${skipReason})`);
|
|
169
|
+
return { key: stage.key, name: stage.name, status: "skipped", exit_code: null, duration_ms: 0, reason: skipReason };
|
|
170
|
+
}
|
|
171
|
+
if (!json) console.log(`==> Stage ${idx}/${total}: ${stage.name}`);
|
|
172
|
+
const t0 = Date.now();
|
|
173
|
+
const cmd = [...zondInvoker(), ...stage.args];
|
|
174
|
+
const proc = Bun.spawn(cmd, { stdout: "inherit", stderr: "inherit" });
|
|
175
|
+
const code = await proc.exited;
|
|
176
|
+
const ms = Date.now() - t0;
|
|
177
|
+
return {
|
|
178
|
+
key: stage.key,
|
|
179
|
+
name: stage.name,
|
|
180
|
+
status: code === 0 ? "ok" : "failed",
|
|
181
|
+
exit_code: code,
|
|
182
|
+
duration_ms: ms,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface CoverageCapture {
|
|
187
|
+
data: unknown | null;
|
|
188
|
+
exitCode: number | null;
|
|
189
|
+
parseError: string | null;
|
|
190
|
+
durationMs: number;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function captureCoverage(api: string): Promise<CoverageCapture> {
|
|
194
|
+
const t0 = Date.now();
|
|
195
|
+
try {
|
|
196
|
+
const cmd = [...zondInvoker(), "coverage", "--api", api, "--union", "session", "--json"];
|
|
197
|
+
const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
198
|
+
const stdout = await new Response(proc.stdout).text();
|
|
199
|
+
const code = await proc.exited;
|
|
200
|
+
const ms = Date.now() - t0;
|
|
201
|
+
if (code !== 0) {
|
|
202
|
+
return { data: null, exitCode: code, parseError: null, durationMs: ms };
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
return { data: JSON.parse(stdout), exitCode: code, parseError: null, durationMs: ms };
|
|
206
|
+
} catch (e) {
|
|
207
|
+
return { data: null, exitCode: code, parseError: (e as Error).message, durationMs: ms };
|
|
208
|
+
}
|
|
209
|
+
} catch (e) {
|
|
210
|
+
return { data: null, exitCode: null, parseError: (e as Error).message, durationMs: Date.now() - t0 };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function auditCommand(options: AuditOptions): Promise<number> {
|
|
215
|
+
// Like bootstrap: fall back to apis/<name>/ when no DB/collection — the
|
|
216
|
+
// workspace-on-disk shape is enough for the macro to drive subprocesses.
|
|
217
|
+
let apiDir = `apis/${options.api}`;
|
|
218
|
+
let specPath: string | null = null;
|
|
219
|
+
try {
|
|
220
|
+
getDb(options.dbPath);
|
|
221
|
+
const col = findCollectionByNameOrId(options.api);
|
|
222
|
+
if (col) {
|
|
223
|
+
apiDir = col.base_dir ?? apiDir;
|
|
224
|
+
specPath = col.openapi_spec ? resolveCollectionSpec(col.openapi_spec) : null;
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
// No DB — keep filesystem-only fallback.
|
|
228
|
+
}
|
|
229
|
+
if (!specPath) {
|
|
230
|
+
const guess = join(apiDir, "spec.json");
|
|
231
|
+
if (existsSync(guess)) specPath = guess;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const stages = buildStages(options, apiDir, specPath);
|
|
235
|
+
const out = options.out ?? "audit-report.html";
|
|
236
|
+
|
|
237
|
+
if (options.dryRun) {
|
|
238
|
+
if (options.json) {
|
|
239
|
+
printJson(jsonOk("audit", {
|
|
240
|
+
plan: stages.map((s) => ({ key: s.key, name: s.name, args: s.args })),
|
|
241
|
+
out,
|
|
242
|
+
}));
|
|
243
|
+
} else {
|
|
244
|
+
console.log(`Plan: zond audit --api ${options.api} (${stages.length} stages)`);
|
|
245
|
+
stages.forEach((s, i) => {
|
|
246
|
+
console.log(` ${(i + 1).toString().padStart(2)}. ${s.name}`);
|
|
247
|
+
console.log(` zond ${s.args.join(" ")}`);
|
|
248
|
+
});
|
|
249
|
+
console.log(`\nReport will be written to: ${out}`);
|
|
250
|
+
}
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const t0 = Date.now();
|
|
255
|
+
const results: StageResult[] = [];
|
|
256
|
+
let coverageJson: unknown = null;
|
|
257
|
+
let coverageCapture: CoverageCapture | null = null;
|
|
258
|
+
for (let i = 0; i < stages.length; i++) {
|
|
259
|
+
const stage = stages[i]!;
|
|
260
|
+
if (stage.key === "coverage") {
|
|
261
|
+
// ARV-108: coverage runs via captureCoverage so we keep stdout JSON.
|
|
262
|
+
if (!options.json) console.log(`==> Stage ${i + 1}/${stages.length}: ${stage.name}`);
|
|
263
|
+
coverageCapture = await captureCoverage(options.api);
|
|
264
|
+
coverageJson = coverageCapture.data;
|
|
265
|
+
const status: StageResult["status"] = coverageCapture.data
|
|
266
|
+
? "ok"
|
|
267
|
+
: coverageCapture.exitCode === 0 && coverageCapture.parseError
|
|
268
|
+
? "failed"
|
|
269
|
+
: coverageCapture.exitCode === 0
|
|
270
|
+
? "skipped"
|
|
271
|
+
: "failed";
|
|
272
|
+
results.push({
|
|
273
|
+
key: stage.key,
|
|
274
|
+
name: stage.name,
|
|
275
|
+
status,
|
|
276
|
+
exit_code: coverageCapture.exitCode,
|
|
277
|
+
duration_ms: coverageCapture.durationMs,
|
|
278
|
+
reason: status === "skipped"
|
|
279
|
+
? "no runs in session"
|
|
280
|
+
: coverageCapture.parseError
|
|
281
|
+
? `non-JSON output: ${coverageCapture.parseError}`
|
|
282
|
+
: status === "failed"
|
|
283
|
+
? `coverage exited ${coverageCapture.exitCode}`
|
|
284
|
+
: undefined,
|
|
285
|
+
});
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
results.push(await runStage(stage, i + 1, stages.length, options.json === true));
|
|
289
|
+
}
|
|
290
|
+
const totalMs = Date.now() - t0;
|
|
291
|
+
|
|
292
|
+
await writeAuditReport(out, {
|
|
293
|
+
api: options.api,
|
|
294
|
+
apiDir,
|
|
295
|
+
stages: results,
|
|
296
|
+
totalMs,
|
|
297
|
+
coverage: coverageJson,
|
|
298
|
+
coverageStage: results.find((r) => r.key === "coverage") ?? null,
|
|
299
|
+
options,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// ARV-108: coverage is informational — keep it out of the fail count so we
|
|
303
|
+
// don't regress the "non-fatal coverage" contract.
|
|
304
|
+
const failedStages = results.filter((r) => r.status === "failed" && r.key !== "coverage");
|
|
305
|
+
const failed = failedStages.length;
|
|
306
|
+
|
|
307
|
+
if (options.json) {
|
|
308
|
+
printJson(jsonOk("audit", {
|
|
309
|
+
api: options.api,
|
|
310
|
+
stages: results,
|
|
311
|
+
total_ms: totalMs,
|
|
312
|
+
failed_stages: failed,
|
|
313
|
+
report: out,
|
|
314
|
+
coverage: coverageJson,
|
|
315
|
+
}));
|
|
316
|
+
} else {
|
|
317
|
+
console.log("");
|
|
318
|
+
const summary = `Audit complete (${results.length} stages, ${(totalMs / 1000).toFixed(1)}s) → ${out}`;
|
|
319
|
+
if (failed === 0) {
|
|
320
|
+
printSuccess(summary);
|
|
321
|
+
} else {
|
|
322
|
+
printWarning(`${summary} — ${failed} failed: ${failedStages.map((s) => s.key).join(", ")}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return failed === 0 ? 0 : 1;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
interface ReportInput {
|
|
329
|
+
api: string;
|
|
330
|
+
apiDir: string;
|
|
331
|
+
stages: StageResult[];
|
|
332
|
+
totalMs: number;
|
|
333
|
+
coverage: unknown;
|
|
334
|
+
/** ARV-108: outcome of the post-stage coverage capture, so the HTML can
|
|
335
|
+
* distinguish "no session runs" from "coverage subcommand failed". */
|
|
336
|
+
coverageStage: StageResult | null;
|
|
337
|
+
options: AuditOptions;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function escapeHtml(s: string): string {
|
|
341
|
+
return s.replace(/[&<>"']/g, (c) => {
|
|
342
|
+
const map: Record<string, string> = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
343
|
+
return map[c]!;
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
interface CoverageEnvelope {
|
|
348
|
+
data?: {
|
|
349
|
+
totals?: { all?: number; covered2xx?: number; coveredButNon2xx?: number; unhit?: number };
|
|
350
|
+
pass_coverage?: { ratio?: number };
|
|
351
|
+
hit_coverage?: { ratio?: number };
|
|
352
|
+
coveredButNon2xxEndpoints?: Array<{ endpoint?: string }>;
|
|
353
|
+
unhitEndpoints?: Array<{ endpoint?: string }>;
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function writeAuditReport(outPath: string, data: ReportInput): Promise<void> {
|
|
358
|
+
const cov = data.coverage as CoverageEnvelope | null;
|
|
359
|
+
const totals = cov?.data?.totals;
|
|
360
|
+
const pass = cov?.data?.pass_coverage?.ratio;
|
|
361
|
+
const hit = cov?.data?.hit_coverage?.ratio;
|
|
362
|
+
|
|
363
|
+
const stageRows = data.stages.map((s) => {
|
|
364
|
+
const cls = s.status === "ok" ? "ok" : s.status === "failed" ? "fail" : "skip";
|
|
365
|
+
const ms = s.duration_ms === 0 ? "—" : `${(s.duration_ms / 1000).toFixed(1)}s`;
|
|
366
|
+
return `<tr class="${cls}"><td>${escapeHtml(s.name)}</td><td>${s.status}</td><td>${s.exit_code ?? "—"}</td><td>${ms}</td><td>${escapeHtml(s.reason ?? "")}</td></tr>`;
|
|
367
|
+
}).join("\n");
|
|
368
|
+
|
|
369
|
+
const reruncmd = `zond audit --api ${data.api}`
|
|
370
|
+
+ (data.options.seed ? " --seed" : "")
|
|
371
|
+
+ (data.options.withMassAssignment ? " --with-mass-assignment" : "")
|
|
372
|
+
+ (data.options.withSecurity ? " --with-security" : "");
|
|
373
|
+
|
|
374
|
+
const covStage = data.coverageStage;
|
|
375
|
+
// ARV-108: tailor the warning to what actually happened so the HTML stops
|
|
376
|
+
// misreporting "stage failed" when the stage was skipped (no runs in the
|
|
377
|
+
// session) or simply produced unparseable output.
|
|
378
|
+
const coverageWarning = covStage
|
|
379
|
+
? covStage.status === "skipped"
|
|
380
|
+
? "No session runs to summarise. Add `--with-mass-assignment` / `--with-security`, or run tests/probes that succeed."
|
|
381
|
+
: covStage.reason
|
|
382
|
+
? `Coverage stage ${covStage.status}: ${escapeHtml(covStage.reason)}.`
|
|
383
|
+
: `Coverage stage ${covStage.status} (exit ${covStage.exit_code ?? "?"}).`
|
|
384
|
+
: "Coverage stage was not part of this audit (older binary?).";
|
|
385
|
+
|
|
386
|
+
const coverageBlock = totals
|
|
387
|
+
? `<h2>Coverage (session union)</h2>
|
|
388
|
+
<div class="cov">
|
|
389
|
+
<div><div class="num">${totals.covered2xx ?? 0}/${totals.all ?? 0}</div><div class="lbl">covered2xx</div></div>
|
|
390
|
+
<div><div class="num">${totals.coveredButNon2xx ?? 0}</div><div class="lbl">covered but non-2xx</div></div>
|
|
391
|
+
<div><div class="num">${totals.unhit ?? 0}</div><div class="lbl">unhit</div></div>
|
|
392
|
+
${typeof pass === "number" ? `<div><div class="num">${(pass * 100).toFixed(0)}%</div><div class="lbl">pass coverage</div></div>` : ""}
|
|
393
|
+
${typeof hit === "number" ? `<div><div class="num">${(hit * 100).toFixed(0)}%</div><div class="lbl">hit coverage</div></div>` : ""}
|
|
394
|
+
</div>`
|
|
395
|
+
: `<h2>Coverage</h2><div class="warn">${coverageWarning}</div>`;
|
|
396
|
+
|
|
397
|
+
const html = `<!doctype html>
|
|
398
|
+
<html lang="en"><head><meta charset="utf-8"><title>zond audit — ${escapeHtml(data.api)}</title>
|
|
399
|
+
<style>
|
|
400
|
+
body { font: 14px -apple-system, system-ui, sans-serif; max-width: 960px; margin: 2em auto; padding: 0 1em; color: #222; }
|
|
401
|
+
h1 { font-size: 1.4em; margin-bottom: 0.2em; }
|
|
402
|
+
h2 { font-size: 1.05em; margin-top: 2em; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
|
|
403
|
+
.meta { color: #666; font-size: 0.9em; margin-bottom: 1em; }
|
|
404
|
+
table { border-collapse: collapse; width: 100%; margin: 1em 0; font-size: 0.92em; }
|
|
405
|
+
th, td { text-align: left; padding: 6px 10px; border-bottom: 1px solid #eee; }
|
|
406
|
+
th { background: #f7f7f7; }
|
|
407
|
+
tr.ok td:nth-child(2) { color: #0a7; }
|
|
408
|
+
tr.fail td:nth-child(2) { color: #c33; font-weight: 600; }
|
|
409
|
+
tr.skip td:nth-child(2) { color: #888; font-style: italic; }
|
|
410
|
+
.cov { display: flex; gap: 2em; margin: 1em 0; flex-wrap: wrap; }
|
|
411
|
+
.cov .num { font-size: 1.6em; font-weight: 600; }
|
|
412
|
+
.cov .lbl { font-size: 0.8em; color: #666; }
|
|
413
|
+
.warn { background: #fef9e7; padding: 8px 12px; border-left: 3px solid #f0c040; margin: 1em 0; }
|
|
414
|
+
code { background: #f4f4f4; padding: 1px 5px; border-radius: 3px; font-size: 0.9em; }
|
|
415
|
+
ul { line-height: 1.6; }
|
|
416
|
+
</style></head>
|
|
417
|
+
<body>
|
|
418
|
+
<h1>zond audit — ${escapeHtml(data.api)}</h1>
|
|
419
|
+
<div class="meta">
|
|
420
|
+
zond ${escapeHtml(VERSION)} · ${new Date().toISOString()} · total ${(data.totalMs / 1000).toFixed(1)}s · apiDir <code>${escapeHtml(data.apiDir)}</code>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
<h2>Stages</h2>
|
|
424
|
+
<table><thead><tr><th>Stage</th><th>Status</th><th>Exit</th><th>Duration</th><th>Note</th></tr></thead><tbody>
|
|
425
|
+
${stageRows}
|
|
426
|
+
</tbody></table>
|
|
427
|
+
|
|
428
|
+
${coverageBlock}
|
|
429
|
+
|
|
430
|
+
<h2>Drill-down</h2>
|
|
431
|
+
<ul>
|
|
432
|
+
<li>Per-run HTML: <code>zond report export <run-id></code></li>
|
|
433
|
+
<li>Diagnose failures: <code>zond db diagnose <run-id> --json</code></li>
|
|
434
|
+
<li>Re-run audit: <code>${escapeHtml(reruncmd)}</code></li>
|
|
435
|
+
</ul>
|
|
436
|
+
</body></html>`;
|
|
437
|
+
|
|
438
|
+
await writeFile(outPath, html, "utf-8");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function registerAudit(program: Command): void {
|
|
442
|
+
program
|
|
443
|
+
.command("audit")
|
|
444
|
+
.description("Macro: prepare-fixtures → generate → probes → run → coverage → HTML report (TASK-262)")
|
|
445
|
+
// ARV-29: not `requiredOption` — same regression that hit prepare-fixtures
|
|
446
|
+
// (TASK-20) and checks run (TASK-17). Commander routes `--api` to the
|
|
447
|
+
// program-level option, so the subcommand's opts.api ends up undefined and
|
|
448
|
+
// requiredOption rejects every form (`--api foo`, `--api=foo`, even
|
|
449
|
+
// `zond --api foo audit`). Fall back the same way: explicit > program-level
|
|
450
|
+
// mirror > .zond/current-api.
|
|
451
|
+
.option("--api <name>", "Registered API to audit. Falls back to ZOND_API / .zond/current-api.")
|
|
452
|
+
.option("--db <path>", "Path to SQLite database file")
|
|
453
|
+
.option("--seed", "Use 'prepare-fixtures --cascade --seed --apply' instead of the plain single-pass prep stage")
|
|
454
|
+
.option("--with-mass-assignment", "Include 'probe mass-assignment' as an extra stage")
|
|
455
|
+
.option("--with-security", "Include 'probe security ssrf,crlf,open-redirect' as an extra stage")
|
|
456
|
+
.option("--out <path>", "HTML report output path (default: audit-report.html)")
|
|
457
|
+
.option("--dry-run", "Print the stage plan without executing anything")
|
|
458
|
+
.option("--force", "Disable mtime-based skip (always regenerate, even if tests/ newer than spec)")
|
|
459
|
+
.action(async (opts, cmd: Command) => {
|
|
460
|
+
// ARV-53.
|
|
461
|
+
const apiName = getApi(cmd, opts);
|
|
462
|
+
if (!apiName) {
|
|
463
|
+
printError(MISSING_API_MESSAGE);
|
|
464
|
+
process.exitCode = 2;
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
opts.api = apiName;
|
|
468
|
+
process.exitCode = await auditCommand({
|
|
469
|
+
api: opts.api,
|
|
470
|
+
dbPath: opts.db,
|
|
471
|
+
seed: opts.seed === true,
|
|
472
|
+
withMassAssignment: opts.withMassAssignment === true,
|
|
473
|
+
withSecurity: opts.withSecurity === true,
|
|
474
|
+
out: opts.out,
|
|
475
|
+
dryRun: opts.dryRun === true,
|
|
476
|
+
force: opts.force === true,
|
|
477
|
+
json: globalJson(cmd),
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
}
|