@kirrosh/zond 0.21.0 โ 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +758 -3
- package/README.md +78 -15
- package/package.json +17 -10
- package/src/cli/argv.ts +122 -0
- package/src/cli/commands/add-api.ts +134 -0
- package/src/cli/commands/api/annotate/idempotency.ts +59 -0
- package/src/cli/commands/api/annotate/index.ts +525 -0
- package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
- package/src/cli/commands/api/annotate/overlay.ts +206 -0
- package/src/cli/commands/api/annotate/pagination.ts +60 -0
- package/src/cli/commands/api/annotate/prompts.ts +183 -0
- package/src/cli/commands/api/annotate/readback.ts +58 -0
- package/src/cli/commands/api/annotate/resources.ts +91 -0
- package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
- package/src/cli/commands/audit.ts +480 -0
- package/src/cli/commands/bootstrap.ts +710 -0
- package/src/cli/commands/catalog.ts +35 -0
- package/src/cli/commands/check.ts +348 -0
- package/src/cli/commands/checks.ts +756 -0
- package/src/cli/commands/ci-init.ts +55 -6
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +262 -0
- package/src/cli/commands/completions.ts +192 -0
- package/src/cli/commands/coverage.ts +605 -132
- package/src/cli/commands/db.ts +180 -8
- package/src/cli/commands/describe.ts +37 -2
- package/src/cli/commands/discover.ts +1236 -0
- package/src/cli/commands/doctor.ts +607 -0
- package/src/cli/commands/fixtures.ts +402 -0
- package/src/cli/commands/generate.ts +420 -47
- package/src/cli/commands/init/agents-md.ts +61 -0
- package/src/cli/commands/init/bootstrap.ts +108 -0
- package/src/cli/commands/init/index.ts +244 -0
- package/src/cli/commands/init/skills.ts +98 -0
- package/src/cli/commands/init/templates/agents.md +77 -0
- package/src/cli/commands/init/templates/markdown.d.ts +4 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
- package/src/cli/commands/init/templates/skills/zond.md +651 -0
- package/src/cli/commands/init/templates/zond-config.yml +14 -0
- package/src/cli/commands/prepare-fixtures.ts +135 -0
- package/src/cli/commands/probe/mass-assignment.ts +503 -0
- package/src/cli/commands/probe/security.ts +454 -0
- package/src/cli/commands/probe/static.ts +255 -0
- package/src/cli/commands/probe/webhooks.ts +161 -0
- package/src/cli/commands/probe.ts +459 -0
- package/src/cli/commands/reference.ts +87 -0
- package/src/cli/commands/refresh-api.ts +169 -0
- package/src/cli/commands/remove-api.ts +150 -0
- package/src/cli/commands/report-bundle.ts +318 -0
- package/src/cli/commands/report.ts +241 -0
- package/src/cli/commands/request.ts +379 -4
- package/src/cli/commands/run.ts +911 -33
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +74 -0
- package/src/cli/index.ts +36 -607
- package/src/cli/json-envelope.ts +112 -3
- package/src/cli/json-schemas.ts +263 -0
- package/src/cli/program.ts +218 -0
- package/src/cli/resolve.ts +105 -0
- package/src/cli/status-filter.ts +124 -0
- package/src/cli/util/api-context.ts +85 -0
- package/src/cli/version.ts +8 -0
- package/src/core/anti-fp/bootstrap.ts +34 -0
- package/src/core/anti-fp/index.ts +33 -0
- package/src/core/anti-fp/registry.ts +44 -0
- package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
- package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
- package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
- package/src/core/anti-fp/types.ts +68 -0
- package/src/core/checks/checks/_crud-helpers.ts +133 -0
- package/src/core/checks/checks/_negative_mutator.ts +133 -0
- package/src/core/checks/checks/_readback-helpers.ts +133 -0
- package/src/core/checks/checks/content_type_conformance.ts +39 -0
- package/src/core/checks/checks/cross_call_references.ts +134 -0
- package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
- package/src/core/checks/checks/idempotency_replay.ts +246 -0
- package/src/core/checks/checks/ignored_auth.ts +211 -0
- package/src/core/checks/checks/index.ts +65 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
- package/src/core/checks/checks/missing_required_header.ts +40 -0
- package/src/core/checks/checks/negative_data_rejection.ts +45 -0
- package/src/core/checks/checks/not_a_server_error.ts +27 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
- package/src/core/checks/checks/pagination_invariants.ts +238 -0
- package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
- package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
- package/src/core/checks/checks/response_headers_conformance.ts +74 -0
- package/src/core/checks/checks/response_schema_conformance.ts +30 -0
- package/src/core/checks/checks/status_code_conformance.ts +61 -0
- package/src/core/checks/checks/unsupported_method.ts +63 -0
- package/src/core/checks/checks/use_after_free.ts +78 -0
- package/src/core/checks/index.ts +30 -0
- package/src/core/checks/mode.ts +79 -0
- package/src/core/checks/recommended-action.ts +64 -0
- package/src/core/checks/registry.ts +78 -0
- package/src/core/checks/runner.ts +874 -0
- package/src/core/checks/sarif.ts +230 -0
- package/src/core/checks/stateful.ts +121 -0
- package/src/core/checks/types.ts +189 -0
- package/src/core/classifier/recommended-action.ts +222 -0
- package/src/core/context/current.ts +51 -0
- package/src/core/context/session.ts +78 -0
- package/src/core/coverage/loader.ts +185 -0
- package/src/core/coverage/reasons.ts +300 -0
- package/src/core/diagnostics/db-analysis.ts +161 -12
- package/src/core/diagnostics/failure-class.ts +120 -0
- package/src/core/diagnostics/failure-hints.ts +212 -9
- package/src/core/diagnostics/spec-pointer.ts +99 -0
- package/src/core/diagnostics/suggested-fixes.ts +156 -0
- package/src/core/exporter/case-study/index.ts +270 -0
- package/src/core/exporter/curl.ts +40 -0
- package/src/core/exporter/exporter.ts +48 -0
- package/src/core/exporter/html-report/escape.ts +24 -0
- package/src/core/exporter/html-report/index.ts +479 -0
- package/src/core/exporter/html-report/script.ts +100 -0
- package/src/core/exporter/html-report/styles.ts +408 -0
- package/src/core/generator/chunker.ts +53 -15
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/create-body.ts +89 -0
- package/src/core/generator/data-factory.ts +490 -33
- package/src/core/generator/describe.ts +1 -1
- package/src/core/generator/fixtures-builder.ts +325 -0
- package/src/core/generator/index.ts +7 -5
- package/src/core/generator/openapi-reader.ts +55 -3
- package/src/core/generator/path-param-disambig.ts +114 -0
- package/src/core/generator/resources-builder.ts +648 -0
- package/src/core/generator/schema-utils.ts +11 -3
- package/src/core/generator/serializer.ts +114 -15
- package/src/core/generator/suite-generator.ts +484 -77
- package/src/core/generator/types.ts +8 -0
- package/src/core/identity/identity-file.ts +129 -0
- package/src/core/lint/affects.ts +28 -0
- package/src/core/lint/config.ts +96 -0
- package/src/core/lint/format.ts +42 -0
- package/src/core/lint/index.ts +94 -0
- package/src/core/lint/reporter.ts +128 -0
- package/src/core/lint/rules/consistency.ts +158 -0
- package/src/core/lint/rules/heuristics.ts +97 -0
- package/src/core/lint/rules/strictness.ts +109 -0
- package/src/core/lint/types.ts +96 -0
- package/src/core/lint/walker.ts +248 -0
- package/src/core/meta/meta-store.ts +6 -73
- package/src/core/output/README.md +91 -0
- package/src/core/output/index.ts +13 -0
- package/src/core/output/run.ts +126 -0
- package/src/core/output/types.ts +129 -0
- package/src/core/parser/env-interpolation.ts +104 -0
- package/src/core/parser/filter.ts +57 -0
- package/src/core/parser/schema.ts +132 -5
- package/src/core/parser/types.ts +29 -2
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +108 -13
- package/src/core/probe/bootstrap.ts +34 -0
- package/src/core/probe/dry-run-envelope.ts +57 -0
- package/src/core/probe/mass-assignment-probe-class.ts +198 -0
- package/src/core/probe/mass-assignment-probe.ts +1122 -0
- package/src/core/probe/mass-assignment-template.ts +212 -0
- package/src/core/probe/method-probe.ts +164 -0
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +691 -0
- package/src/core/probe/orphan-tracker.ts +188 -0
- package/src/core/probe/path-discovery.ts +440 -0
- package/src/core/probe/probe-harness.ts +120 -0
- package/src/core/probe/registry.ts +89 -0
- package/src/core/probe/runner.ts +136 -0
- package/src/core/probe/security-probe-class.ts +201 -0
- package/src/core/probe/security-probe.ts +1453 -0
- package/src/core/probe/shared.ts +505 -0
- package/src/core/probe/static-probe-class.ts +125 -0
- package/src/core/probe/types.ts +165 -0
- package/src/core/probe/verdict-aggregator.ts +33 -0
- package/src/core/probe/webhooks-probe.ts +284 -0
- package/src/core/reporter/console.ts +69 -4
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +15 -2
- package/src/core/reporter/junit.ts +27 -12
- package/src/core/reporter/ndjson.ts +37 -0
- package/src/core/reporter/types.ts +3 -0
- package/src/core/runner/assertions.ts +62 -2
- package/src/core/runner/async-pool.ts +108 -0
- package/src/core/runner/auth-path.ts +8 -0
- package/src/core/runner/ci-context.ts +72 -0
- package/src/core/runner/executor.ts +391 -52
- package/src/core/runner/form-encode.ts +51 -0
- package/src/core/runner/http-client.ts +115 -7
- package/src/core/runner/learn-drift.ts +293 -0
- package/src/core/runner/preflight-vars.ts +149 -0
- package/src/core/runner/progress-tracker.ts +73 -0
- package/src/core/runner/rate-limiter.ts +203 -0
- package/src/core/runner/run-kind.ts +39 -0
- package/src/core/runner/schema-validator.ts +312 -0
- package/src/core/runner/send-request.ts +153 -20
- package/src/core/runner/types.ts +38 -0
- package/src/core/secrets/registry.ts +164 -0
- package/src/core/secrets/secrets-file.ts +115 -0
- package/src/core/selectors/operation-filter.ts +144 -0
- package/src/core/setup-api.ts +419 -17
- package/src/core/severity/category.ts +94 -0
- package/src/core/severity/index.ts +121 -0
- package/src/core/spec/layers.ts +154 -0
- package/src/core/util/format-eta.ts +21 -0
- package/src/core/utils.ts +5 -1
- package/src/core/workspace/config.ts +129 -0
- package/src/core/workspace/manifest.ts +283 -0
- package/src/core/workspace/output-rotation.ts +62 -0
- package/src/core/workspace/root.ts +94 -0
- package/src/core/workspace/triage-path.ts +87 -0
- package/src/db/lint-runs.ts +47 -0
- package/src/db/migrate.ts +126 -0
- package/src/db/migrations/0001_run_kind.sql +25 -0
- package/src/db/migrations/sql.d.ts +4 -0
- package/src/db/queries/collections.ts +133 -0
- package/src/db/queries/coverage.ts +9 -0
- package/src/db/queries/dashboard.ts +59 -0
- package/src/db/queries/results.ts +128 -0
- package/src/db/queries/runs.ts +235 -0
- package/src/db/queries/sessions.ts +42 -0
- package/src/db/queries/settings.ts +28 -0
- package/src/db/queries/types.ts +172 -0
- package/src/db/queries.ts +72 -802
- package/src/db/schema.ts +179 -48
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init.ts +0 -57
- package/src/cli/commands/serve.ts +0 -81
- package/src/cli/commands/sync.ts +0 -269
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -21
- package/src/core/parser/index.ts +0 -21
- package/src/core/runner/execute-run.ts +0 -132
- package/src/core/runner/index.ts +0 -12
- package/src/core/sync/spec-differ.ts +0 -38
- package/src/web/data/collection-state.ts +0 -362
- package/src/web/routes/api.ts +0 -314
- package/src/web/routes/dashboard.ts +0 -350
- package/src/web/routes/runs.ts +0 -64
- package/src/web/schemas.ts +0 -121
- package/src/web/server.ts +0 -134
- package/src/web/static/htmx.min.cjs +0 -1
- package/src/web/static/style.css +0 -1148
- package/src/web/views/endpoints-tab.ts +0 -174
- package/src/web/views/explorer-tab.ts +0 -402
- package/src/web/views/health-strip.ts +0 -92
- package/src/web/views/layout.ts +0 -48
- package/src/web/views/results.ts +0 -210
- package/src/web/views/runs-tab.ts +0 -126
- package/src/web/views/suites-tab.ts +0 -181
|
@@ -8,6 +8,14 @@ export interface ResponseInfo {
|
|
|
8
8
|
|
|
9
9
|
export interface EndpointInfo {
|
|
10
10
|
path: string;
|
|
11
|
+
/** ARV-183: original spec path before ARV-40 path-param disambiguation
|
|
12
|
+
* renamed `{id}` โ `{<resource>_id}`. Set only when a rename happened;
|
|
13
|
+
* unset means `path` is the original. Used by checks that look up
|
|
14
|
+
* `doc.paths[...]` by string equality (status_code_conformance,
|
|
15
|
+
* response_headers_conformance) โ without this they miss the spec
|
|
16
|
+
* entry and either fire phantom findings (status_code) or silently
|
|
17
|
+
* skip (response_headers). */
|
|
18
|
+
originalPath?: string;
|
|
11
19
|
method: string;
|
|
12
20
|
operationId?: string;
|
|
13
21
|
summary?: string;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.identity.yaml` โ gitignored flat YAML file holding non-secret-but-
|
|
3
|
+
* personally-identifying values for an API (TASK-174, m-10).
|
|
4
|
+
*
|
|
5
|
+
* # apis/<name>/.identity.yaml
|
|
6
|
+
* organization_id_or_slug: "acme-eng"
|
|
7
|
+
* member_id: "12345"
|
|
8
|
+
*
|
|
9
|
+
* # apis/<name>/.env.yaml
|
|
10
|
+
* organization_id_or_slug: "@identity:organization_id_or_slug"
|
|
11
|
+
* auth_token: "@secret:auth_token"
|
|
12
|
+
*
|
|
13
|
+
* Mental model:
|
|
14
|
+
* - `.secrets.yaml` โ values auto-registered with SecretRegistry,
|
|
15
|
+
* replaced with `<redacted:<name>>` in every persisted artifact.
|
|
16
|
+
* - `.identity.yaml` โ values are visible locally and visible in
|
|
17
|
+
* case-study drafts by default. The opt-in `--redact-identity`
|
|
18
|
+
* flag (TASK-173) swaps them for placeholders when sharing
|
|
19
|
+
* outbound. Doctor shows them as plain text.
|
|
20
|
+
*
|
|
21
|
+
* The file is git-invisible (gitignore is amended by setup-api) so a
|
|
22
|
+
* teammate forking the repo doesn't accidentally inherit your org slug.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
26
|
+
import { dirname, join } from "node:path";
|
|
27
|
+
|
|
28
|
+
const IDENTITY_FILENAME = ".identity.yaml";
|
|
29
|
+
const IDENTITY_REF_RE = /^@identity:([A-Za-z_][A-Za-z0-9_.-]*)$/;
|
|
30
|
+
|
|
31
|
+
/** Canonical identity-key vocabulary. The setup-api seeder uses this to
|
|
32
|
+
* decide which placeholders to put in a fresh `.identity.yaml`. */
|
|
33
|
+
export const CANONICAL_IDENTITY_KEYS = new Set<string>([
|
|
34
|
+
"organization_id_or_slug",
|
|
35
|
+
"organization_slug",
|
|
36
|
+
"organization_id",
|
|
37
|
+
"member_id",
|
|
38
|
+
"user_id",
|
|
39
|
+
"project_id_or_slug",
|
|
40
|
+
"project_slug",
|
|
41
|
+
"project_id",
|
|
42
|
+
"team_slug",
|
|
43
|
+
"team_id",
|
|
44
|
+
"account_id",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
export interface IdentityFile {
|
|
48
|
+
filePath: string;
|
|
49
|
+
values: Record<string, string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function loadIdentityFile(dir: string): IdentityFile | null {
|
|
53
|
+
const filePath = join(dir, IDENTITY_FILENAME);
|
|
54
|
+
if (!existsSync(filePath)) return null;
|
|
55
|
+
const text = readFileSync(filePath, "utf-8");
|
|
56
|
+
let parsed: unknown;
|
|
57
|
+
try {
|
|
58
|
+
parsed = (Bun as any).YAML.parse(text);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
throw new Error(`Failed to parse ${filePath}: ${(err as Error).message}`);
|
|
61
|
+
}
|
|
62
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
63
|
+
throw new Error(`${filePath} must contain a flat YAML object of key: "value" entries`);
|
|
64
|
+
}
|
|
65
|
+
const values: Record<string, string> = {};
|
|
66
|
+
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
|
|
67
|
+
if (v == null) continue;
|
|
68
|
+
if (typeof v === "object") {
|
|
69
|
+
throw new Error(`${filePath}: nested values are not supported (key "${k}").`);
|
|
70
|
+
}
|
|
71
|
+
values[k] = String(v);
|
|
72
|
+
}
|
|
73
|
+
return { filePath, values };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function loadIdentityFromAncestor(start: string, stopAt?: string): IdentityFile | null {
|
|
77
|
+
let dir = start;
|
|
78
|
+
for (let i = 0; i < 8; i++) {
|
|
79
|
+
const file = loadIdentityFile(dir);
|
|
80
|
+
if (file) return file;
|
|
81
|
+
if (stopAt && dir === stopAt) return null;
|
|
82
|
+
const parent = dirname(dir);
|
|
83
|
+
if (parent === dir) return null;
|
|
84
|
+
dir = parent;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Replace every value from an identity map with `<identity:<key>>`
|
|
91
|
+
* inside `text`. Used by `--redact-identity` (TASK-173). Mirrors the
|
|
92
|
+
* SecretRegistry's logic โ longest values first so a containing value
|
|
93
|
+
* wins, minimum length 2 (identity slugs can be short like `acme`).
|
|
94
|
+
*/
|
|
95
|
+
export function redactIdentityIn(text: string, values: Record<string, string>): string {
|
|
96
|
+
if (!text || Object.keys(values).length === 0) return text;
|
|
97
|
+
const entries = Object.entries(values)
|
|
98
|
+
.filter(([, v]) => typeof v === "string" && v.length >= 2)
|
|
99
|
+
.sort((a, b) => b[1].length - a[1].length);
|
|
100
|
+
let out = text;
|
|
101
|
+
for (const [name, value] of entries) {
|
|
102
|
+
if (out.indexOf(value) === -1) continue;
|
|
103
|
+
out = out.split(value).join(`<identity:${name}>`);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function resolveIdentityRefs(
|
|
109
|
+
envValues: Record<string, string>,
|
|
110
|
+
identity: IdentityFile | null,
|
|
111
|
+
filePath: string,
|
|
112
|
+
): Record<string, string> {
|
|
113
|
+
const out: Record<string, string> = { ...envValues };
|
|
114
|
+
for (const [k, v] of Object.entries(out)) {
|
|
115
|
+
const m = typeof v === "string" ? v.match(IDENTITY_REF_RE) : null;
|
|
116
|
+
if (!m) continue;
|
|
117
|
+
const refName = m[1]!;
|
|
118
|
+
const value = identity?.values[refName];
|
|
119
|
+
if (value == null) {
|
|
120
|
+
const where = identity ? identity.filePath : `${dirname(filePath)}/${IDENTITY_FILENAME}`;
|
|
121
|
+
throw new Error(
|
|
122
|
+
`${filePath}: key "${k}" references @identity:${refName} but no such entry exists in ${where}. ` +
|
|
123
|
+
`Add \`${refName}: "<value>"\` to ${where} (or remove the @identity: prefix to use a literal value).`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
out[k] = value;
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { RuleId } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cross-reference: which other zond commands are made noisier or less reliable
|
|
5
|
+
* by an unfixed issue of each rule. Surfaced in JSON output as `affects[]` so
|
|
6
|
+
* agents and IDEs can predict which probe runs will produce false-positive 5xx
|
|
7
|
+
* or which `--validate-schema` checks will silently no-op.
|
|
8
|
+
*/
|
|
9
|
+
export const RULE_AFFECTS: Record<RuleId, string[]> = {
|
|
10
|
+
// Group A โ lax examples mislead generators and downstream consumers.
|
|
11
|
+
A1: ["run:--validate-schema", "generate"],
|
|
12
|
+
A2: ["run:--validate-schema", "generate"],
|
|
13
|
+
A3: ["run:--validate-schema", "generate"],
|
|
14
|
+
A4: ["run:--validate-schema", "generate"],
|
|
15
|
+
A5: ["generate"],
|
|
16
|
+
A6: [],
|
|
17
|
+
|
|
18
|
+
// Group B โ loose schema lets the spec accept what the server rejects.
|
|
19
|
+
B1: ["probe-validation:invalid-path-uuid", "probe-methods"],
|
|
20
|
+
B2: ["probe-validation:invalid-path-uuid"],
|
|
21
|
+
B3: ["probe-validation:boundary-string"],
|
|
22
|
+
B4: ["probe-validation:boundary-string"],
|
|
23
|
+
B5: ["run:--validate-schema"],
|
|
24
|
+
B6: ["run:--validate-schema"],
|
|
25
|
+
B7: ["run:--validate-schema"],
|
|
26
|
+
B8: ["probe-mass-assignment"],
|
|
27
|
+
B9: ["probe-validation:missing-required"],
|
|
28
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import type { LintConfig, RuleId, RuleSetting } from "./types.ts";
|
|
3
|
+
import { ALL_RULES, DEFAULT_HEURISTICS, DEFAULT_SEVERITY } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export function defaultConfig(): LintConfig {
|
|
6
|
+
const rules: Partial<Record<RuleId, RuleSetting>> = {};
|
|
7
|
+
for (const r of ALL_RULES) rules[r] = DEFAULT_SEVERITY[r];
|
|
8
|
+
return {
|
|
9
|
+
rules,
|
|
10
|
+
heuristics: { ...DEFAULT_HEURISTICS },
|
|
11
|
+
ignore_paths: [],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface RawConfig {
|
|
16
|
+
rules?: Record<string, string>;
|
|
17
|
+
heuristics?: Partial<typeof DEFAULT_HEURISTICS>;
|
|
18
|
+
ignore_paths?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Merge order: defaults โ file config (.zond-lint.json) โ CLI --rule overrides.
|
|
23
|
+
* --rule format: comma-separated `R1` (enable at default severity), `!R1`
|
|
24
|
+
* (disable), or `R1=high|medium|low` (set severity).
|
|
25
|
+
*/
|
|
26
|
+
export function loadConfig(opts: {
|
|
27
|
+
configPath?: string;
|
|
28
|
+
cliRule?: string;
|
|
29
|
+
includePaths?: string[];
|
|
30
|
+
maxIssues?: number;
|
|
31
|
+
}): LintConfig {
|
|
32
|
+
const cfg = defaultConfig();
|
|
33
|
+
|
|
34
|
+
if (opts.configPath) {
|
|
35
|
+
if (!existsSync(opts.configPath)) {
|
|
36
|
+
throw new Error(`Config file not found: ${opts.configPath}`);
|
|
37
|
+
}
|
|
38
|
+
const raw = JSON.parse(readFileSync(opts.configPath, "utf8")) as RawConfig;
|
|
39
|
+
if (raw.rules) {
|
|
40
|
+
for (const [rule, val] of Object.entries(raw.rules)) {
|
|
41
|
+
if (!ALL_RULES.includes(rule as RuleId)) continue;
|
|
42
|
+
cfg.rules[rule as RuleId] = normaliseSetting(val);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (raw.heuristics) cfg.heuristics = { ...cfg.heuristics, ...raw.heuristics };
|
|
46
|
+
if (raw.ignore_paths) cfg.ignore_paths = raw.ignore_paths;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (opts.cliRule) {
|
|
50
|
+
for (const tok of opts.cliRule.split(",").map(s => s.trim()).filter(Boolean)) {
|
|
51
|
+
if (tok.startsWith("!")) {
|
|
52
|
+
const r = tok.slice(1) as RuleId;
|
|
53
|
+
if (ALL_RULES.includes(r)) cfg.rules[r] = "off";
|
|
54
|
+
} else if (tok.includes("=")) {
|
|
55
|
+
const [r, sev] = tok.split("=") as [RuleId, string];
|
|
56
|
+
if (ALL_RULES.includes(r)) cfg.rules[r] = normaliseSetting(sev);
|
|
57
|
+
} else {
|
|
58
|
+
const r = tok as RuleId;
|
|
59
|
+
if (ALL_RULES.includes(r)) cfg.rules[r] = DEFAULT_SEVERITY[r];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (opts.includePaths && opts.includePaths.length > 0) cfg.include_paths = opts.includePaths;
|
|
65
|
+
if (opts.maxIssues) cfg.max_issues = opts.maxIssues;
|
|
66
|
+
return cfg;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normaliseSetting(raw: string): RuleSetting {
|
|
70
|
+
const v = raw.toLowerCase();
|
|
71
|
+
if (v === "off" || v === "false" || v === "no") return "off";
|
|
72
|
+
// ARV-255: spec-lint is hygiene โ severity capped at LOW/INFO. User
|
|
73
|
+
// overrides via `--rule R=high|medium` are still parsed for back-compat
|
|
74
|
+
// but silently downgraded so the cap is enforced uniformly.
|
|
75
|
+
if (v === "high" || v === "error") return "low";
|
|
76
|
+
if (v === "medium" || v === "warn" || v === "warning") return "low";
|
|
77
|
+
if (v === "low") return "low";
|
|
78
|
+
if (v === "info" || v === "informational") return "info";
|
|
79
|
+
return "off";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Glob matcher โ supports `*` and `**`. Used for `ignore_paths` /
|
|
84
|
+
* `include_paths`.
|
|
85
|
+
*/
|
|
86
|
+
export function matchGlob(glob: string, path: string): boolean {
|
|
87
|
+
const re = new RegExp(
|
|
88
|
+
"^" + glob
|
|
89
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
90
|
+
.replace(/\*\*/g, "<<<DSTAR>>>")
|
|
91
|
+
.replace(/\*/g, "[^/]*")
|
|
92
|
+
.replace(/<<<DSTAR>>>/g, ".*") +
|
|
93
|
+
"$",
|
|
94
|
+
);
|
|
95
|
+
return re.test(path);
|
|
96
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import addFormats from "ajv-formats";
|
|
3
|
+
import { STRICT_RFC3339_DATE_TIME } from "../runner/schema-validator.ts";
|
|
4
|
+
|
|
5
|
+
const ajv = new Ajv({ strict: false, allErrors: false });
|
|
6
|
+
addFormats(ajv);
|
|
7
|
+
ajv.addFormat("date-time", { type: "string", validate: STRICT_RFC3339_DATE_TIME });
|
|
8
|
+
|
|
9
|
+
const SUPPORTED = new Set([
|
|
10
|
+
"date-time", "date", "time",
|
|
11
|
+
"email", "idn-email",
|
|
12
|
+
"uri", "uri-reference", "url",
|
|
13
|
+
"uuid",
|
|
14
|
+
"ipv4", "ipv6",
|
|
15
|
+
"hostname", "idn-hostname",
|
|
16
|
+
"regex",
|
|
17
|
+
"byte", "binary", "password",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const cache = new Map<string, (v: unknown) => boolean>();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* True if `value` satisfies the OpenAPI/JSON-Schema `format`. Returns true for
|
|
24
|
+
* unknown formats (we can't say). Returns true for non-string values
|
|
25
|
+
* (OpenAPI's `format` only constrains strings).
|
|
26
|
+
*/
|
|
27
|
+
export function validateExampleAgainstFormat(value: unknown, format: string): boolean {
|
|
28
|
+
if (typeof value !== "string") return true;
|
|
29
|
+
if (!SUPPORTED.has(format)) return true;
|
|
30
|
+
if (format === "url") format = "uri";
|
|
31
|
+
|
|
32
|
+
let validator = cache.get(format);
|
|
33
|
+
if (!validator) {
|
|
34
|
+
try {
|
|
35
|
+
validator = ajv.compile({ type: "string", format }) as (v: unknown) => boolean;
|
|
36
|
+
cache.set(format, validator);
|
|
37
|
+
} catch {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return validator(value);
|
|
42
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
2
|
+
import type { Issue, RuleId, Severity, LintConfig, LintResult } from "./types.ts";
|
|
3
|
+
import { walk } from "./walker.ts";
|
|
4
|
+
import { matchGlob } from "./config.ts";
|
|
5
|
+
import { runConsistencyRules } from "./rules/consistency.ts";
|
|
6
|
+
import {
|
|
7
|
+
runParamStrictnessRules,
|
|
8
|
+
runResponseStrictnessRules,
|
|
9
|
+
runRequestBodyStrictnessRules,
|
|
10
|
+
runSchemaStrictnessRules,
|
|
11
|
+
} from "./rules/strictness.ts";
|
|
12
|
+
import {
|
|
13
|
+
runParamHeuristics,
|
|
14
|
+
runSchemaHeuristics,
|
|
15
|
+
runRequestBodyHeuristics,
|
|
16
|
+
} from "./rules/heuristics.ts";
|
|
17
|
+
import { RULE_AFFECTS } from "./affects.ts";
|
|
18
|
+
|
|
19
|
+
export type { Issue, LintConfig, LintResult, LintStats, Severity, RuleId } from "./types.ts";
|
|
20
|
+
export { loadConfig, defaultConfig } from "./config.ts";
|
|
21
|
+
export { formatHuman, formatNdjson, formatGrouped, buildRuleSummary } from "./reporter.ts";
|
|
22
|
+
export type { RuleSummaryEntry } from "./reporter.ts";
|
|
23
|
+
|
|
24
|
+
export function lintSpec(doc: OpenAPIV3.Document, config: LintConfig): LintResult {
|
|
25
|
+
const issues: Issue[] = [];
|
|
26
|
+
const endpoints = new Set<string>();
|
|
27
|
+
|
|
28
|
+
const sink = {
|
|
29
|
+
push(rule: RuleId, severity: Severity, message: string, opts: { jsonpointer: string; path?: string; method?: string; fix_hint?: string }) {
|
|
30
|
+
const setting = config.rules[rule];
|
|
31
|
+
if (setting === "off" || setting === undefined) return;
|
|
32
|
+
|
|
33
|
+
// Path-include / ignore filters operate on opts.path when present.
|
|
34
|
+
if (opts.path) {
|
|
35
|
+
if (config.ignore_paths.some(g => matchGlob(g, opts.path!))) return;
|
|
36
|
+
if (config.include_paths && config.include_paths.length > 0
|
|
37
|
+
&& !config.include_paths.some(g => matchGlob(g, opts.path!))) return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const issue: Issue = {
|
|
41
|
+
rule,
|
|
42
|
+
severity: setting as Severity,
|
|
43
|
+
jsonpointer: opts.jsonpointer,
|
|
44
|
+
message,
|
|
45
|
+
recommended_action: "fix_spec",
|
|
46
|
+
};
|
|
47
|
+
if (opts.path) issue.path = opts.path;
|
|
48
|
+
if (opts.method && opts.method !== "*") issue.method = opts.method;
|
|
49
|
+
if (opts.fix_hint) issue.fix_hint = opts.fix_hint;
|
|
50
|
+
const aff = RULE_AFFECTS[rule];
|
|
51
|
+
if (aff && aff.length > 0) issue.affects = aff;
|
|
52
|
+
|
|
53
|
+
if (opts.path) endpoints.add(`${opts.method ?? ""} ${opts.path}`);
|
|
54
|
+
|
|
55
|
+
issues.push(issue);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
walk(doc, ctx => {
|
|
60
|
+
if (config.max_issues && issues.length >= config.max_issues) return;
|
|
61
|
+
switch (ctx.kind) {
|
|
62
|
+
case "parameter":
|
|
63
|
+
runParamStrictnessRules(ctx, sink, config.heuristics);
|
|
64
|
+
runParamHeuristics(ctx, sink, config.heuristics);
|
|
65
|
+
break;
|
|
66
|
+
case "response":
|
|
67
|
+
runResponseStrictnessRules(ctx, sink);
|
|
68
|
+
break;
|
|
69
|
+
case "requestBody":
|
|
70
|
+
runRequestBodyStrictnessRules(ctx, sink);
|
|
71
|
+
runRequestBodyHeuristics(ctx, sink, config.heuristics);
|
|
72
|
+
break;
|
|
73
|
+
case "schema":
|
|
74
|
+
runConsistencyRules(ctx, sink);
|
|
75
|
+
runSchemaStrictnessRules(ctx, sink);
|
|
76
|
+
runSchemaHeuristics(ctx, sink, config.heuristics);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Trim to max_issues if exceeded mid-walk
|
|
82
|
+
const trimmed = config.max_issues ? issues.slice(0, config.max_issues) : issues;
|
|
83
|
+
|
|
84
|
+
const stats = {
|
|
85
|
+
total: trimmed.length,
|
|
86
|
+
critical: trimmed.filter(i => i.severity === "critical").length,
|
|
87
|
+
high: trimmed.filter(i => i.severity === "high").length,
|
|
88
|
+
medium: trimmed.filter(i => i.severity === "medium").length,
|
|
89
|
+
low: trimmed.filter(i => i.severity === "low").length,
|
|
90
|
+
info: trimmed.filter(i => i.severity === "info").length,
|
|
91
|
+
endpoints: endpoints.size,
|
|
92
|
+
};
|
|
93
|
+
return { issues: trimmed, stats };
|
|
94
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { Issue, LintStats, Severity } from "./types.ts";
|
|
2
|
+
import { severityGlyph, rankSeverity } from "../severity/index.ts";
|
|
3
|
+
|
|
4
|
+
const RED = "\x1b[31m";
|
|
5
|
+
const YELLOW = "\x1b[33m";
|
|
6
|
+
const DIM = "\x1b[2m";
|
|
7
|
+
const BOLD = "\x1b[1m";
|
|
8
|
+
const RESET = "\x1b[0m";
|
|
9
|
+
|
|
10
|
+
const useColor = (): boolean => process.stdout.isTTY === true;
|
|
11
|
+
|
|
12
|
+
const ICON: Record<Severity, string> = {
|
|
13
|
+
critical: "๐จ", high: "๐ด", medium: "โ ๏ธ ", low: "โน๏ธ ", info: "ยท ",
|
|
14
|
+
};
|
|
15
|
+
const COLOR: Record<Severity, string> = {
|
|
16
|
+
critical: RED, high: RED, medium: YELLOW, low: DIM, info: DIM,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function formatHuman(issues: Issue[], stats: LintStats): string {
|
|
20
|
+
if (issues.length === 0) {
|
|
21
|
+
return useColor() ? `${BOLD}โ no issues${RESET}\n` : "โ no issues\n";
|
|
22
|
+
}
|
|
23
|
+
const groups: Record<Severity, Issue[]> = {
|
|
24
|
+
critical: [], high: [], medium: [], low: [], info: [],
|
|
25
|
+
};
|
|
26
|
+
for (const i of issues) groups[i.severity].push(i);
|
|
27
|
+
|
|
28
|
+
const lines: string[] = [];
|
|
29
|
+
for (const sev of ["critical", "high", "medium", "low", "info"] as Severity[]) {
|
|
30
|
+
const g = groups[sev];
|
|
31
|
+
if (g.length === 0) continue;
|
|
32
|
+
const header = `${ICON[sev]} ${sev.toUpperCase()} (${g.length})`;
|
|
33
|
+
lines.push(useColor() ? `${COLOR[sev]}${BOLD}${header}${RESET}` : header);
|
|
34
|
+
for (const i of g) {
|
|
35
|
+
const where = formatWhere(i);
|
|
36
|
+
const tail = useColor() ? `${DIM}(${i.rule})${RESET}` : `(${i.rule})`;
|
|
37
|
+
lines.push(` ${where} ${i.message} ${tail}`);
|
|
38
|
+
if (i.fix_hint) {
|
|
39
|
+
lines.push(useColor() ? ` ${DIM}โ ${i.fix_hint}${RESET}` : ` โ ${i.fix_hint}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
lines.push("");
|
|
43
|
+
}
|
|
44
|
+
lines.push(`${stats.total} issue(s) across ${stats.endpoints} endpoint(s)`);
|
|
45
|
+
return lines.join("\n") + "\n";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatWhere(i: Issue): string {
|
|
49
|
+
if (i.path && i.method && i.method !== "*") return `${i.method} ${i.path}`;
|
|
50
|
+
if (i.path) return i.path;
|
|
51
|
+
return i.jsonpointer;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function formatNdjson(issues: Issue[]): string {
|
|
55
|
+
return issues.map(i => JSON.stringify(i)).join("\n") + (issues.length ? "\n" : "");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* TASK-279: rule ร severity rollup. The flat `formatHuman` output has a habit
|
|
60
|
+
* of producing 700+ lines on real-world specs (one large SaaS spec we
|
|
61
|
+
* benchmarked had 385 of 714 issues from a single rule). This collapses
|
|
62
|
+
* them to one row per rule so a human can
|
|
63
|
+
* triage by impact instead of `grep '(B1)' | wc -l`.
|
|
64
|
+
*/
|
|
65
|
+
export interface RuleSummaryEntry {
|
|
66
|
+
rule: string;
|
|
67
|
+
severity: Severity;
|
|
68
|
+
count: number;
|
|
69
|
+
endpoints: number;
|
|
70
|
+
message: string;
|
|
71
|
+
sample?: { method?: string; path?: string; jsonpointer?: string };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function buildRuleSummary(issues: Issue[]): RuleSummaryEntry[] {
|
|
75
|
+
type Bucket = { rule: string; severity: Severity; count: number; endpointSet: Set<string>; message: string; sample?: RuleSummaryEntry["sample"] };
|
|
76
|
+
const map = new Map<string, Bucket>();
|
|
77
|
+
for (const i of issues) {
|
|
78
|
+
const key = `${i.rule}|${i.severity}`;
|
|
79
|
+
let b = map.get(key);
|
|
80
|
+
if (!b) {
|
|
81
|
+
b = { rule: i.rule, severity: i.severity, count: 0, endpointSet: new Set(), message: i.message };
|
|
82
|
+
if (i.path || i.method || i.jsonpointer) {
|
|
83
|
+
b.sample = { method: i.method, path: i.path, jsonpointer: i.jsonpointer };
|
|
84
|
+
}
|
|
85
|
+
map.set(key, b);
|
|
86
|
+
}
|
|
87
|
+
b.count++;
|
|
88
|
+
if (i.path) b.endpointSet.add(`${i.method ?? "*"} ${i.path}`);
|
|
89
|
+
else if (i.jsonpointer) b.endpointSet.add(i.jsonpointer);
|
|
90
|
+
}
|
|
91
|
+
return [...map.values()]
|
|
92
|
+
.sort((a, b) => rankSeverity(a.severity) - rankSeverity(b.severity) || b.count - a.count)
|
|
93
|
+
.map(b => ({
|
|
94
|
+
rule: b.rule,
|
|
95
|
+
severity: b.severity,
|
|
96
|
+
count: b.count,
|
|
97
|
+
endpoints: b.endpointSet.size,
|
|
98
|
+
message: b.message,
|
|
99
|
+
...(b.sample ? { sample: b.sample } : {}),
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function formatGrouped(issues: Issue[], stats: LintStats, opts: { top?: number } = {}): string {
|
|
104
|
+
if (issues.length === 0) {
|
|
105
|
+
return useColor() ? `${BOLD}โ no issues${RESET}\n` : "โ no issues\n";
|
|
106
|
+
}
|
|
107
|
+
const summary = buildRuleSummary(issues);
|
|
108
|
+
const rows = opts.top != null && opts.top > 0 ? summary.slice(0, opts.top) : summary;
|
|
109
|
+
|
|
110
|
+
const lines: string[] = [];
|
|
111
|
+
let lastSev: Severity | null = null;
|
|
112
|
+
for (const r of rows) {
|
|
113
|
+
if (r.severity !== lastSev) {
|
|
114
|
+
const header = `${ICON[r.severity]} ${r.severity.toUpperCase()}`;
|
|
115
|
+
lines.push(useColor() ? `${COLOR[r.severity]}${BOLD}${header}${RESET}` : header);
|
|
116
|
+
lastSev = r.severity;
|
|
117
|
+
}
|
|
118
|
+
const tag = useColor() ? `${COLOR[r.severity]}${r.rule}${RESET}` : r.rule;
|
|
119
|
+
const endpointsLabel = r.endpoints === r.count ? `${r.count}` : `${r.count} (${r.endpoints} endpoints)`;
|
|
120
|
+
lines.push(` ${tag.padEnd(useColor() ? 14 : 4)} ${endpointsLabel.padStart(6)} ${r.message}`);
|
|
121
|
+
}
|
|
122
|
+
lines.push("");
|
|
123
|
+
const truncated = opts.top != null && opts.top > 0 && summary.length > rows.length
|
|
124
|
+
? ` (showing top ${rows.length} of ${summary.length} rules; pass --top 0 or --verbose for all)`
|
|
125
|
+
: "";
|
|
126
|
+
lines.push(`${stats.total} issue(s) across ${stats.endpoints} endpoint(s)${truncated}. Re-run with --verbose for the flat list.`);
|
|
127
|
+
return lines.join("\n") + "\n";
|
|
128
|
+
}
|