@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
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
2
|
+
import type { Issue, RuleId, Severity } from "../types.ts";
|
|
3
|
+
import { DEFAULT_SEVERITY } from "../types.ts";
|
|
4
|
+
import { RULE_AFFECTS } from "../affects.ts";
|
|
5
|
+
import type { SchemaContext } from "../walker.ts";
|
|
6
|
+
import { normalisedTypes } from "../walker.ts";
|
|
7
|
+
import { validateExampleAgainstFormat } from "../format.ts";
|
|
8
|
+
|
|
9
|
+
interface RuleSink {
|
|
10
|
+
push(rule: RuleId, severity: Severity, message: string, opts: Partial<Pick<Issue, "path" | "method" | "fix_hint">> & { jsonpointer: string }): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function runConsistencyRules(ctx: SchemaContext, sink: RuleSink): void {
|
|
14
|
+
const s = ctx.schema;
|
|
15
|
+
if (!s || typeof s !== "object") return;
|
|
16
|
+
|
|
17
|
+
const types = normalisedTypes(s);
|
|
18
|
+
const isStringy = types.includes("string");
|
|
19
|
+
const isNumeric = types.includes("number") || types.includes("integer");
|
|
20
|
+
|
|
21
|
+
const examples = collectExamples(s);
|
|
22
|
+
for (const { value, pointer } of examples) {
|
|
23
|
+
checkValueAgainstSchema(value, s, pointer, "example", isStringy, isNumeric, ctx, sink);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (s.default !== undefined) {
|
|
27
|
+
checkValueAgainstSchema(s.default, s, `${ctx.jsonpointer}/default`, "default", isStringy, isNumeric, ctx, sink);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// A6: enum values pairwise unique.
|
|
31
|
+
if (Array.isArray(s.enum) && s.enum.length > 0) {
|
|
32
|
+
const seen = new Set<string>();
|
|
33
|
+
let dupAt = -1;
|
|
34
|
+
for (let i = 0; i < s.enum.length; i++) {
|
|
35
|
+
const k = stableKey(s.enum[i]);
|
|
36
|
+
if (seen.has(k)) { dupAt = i; break; }
|
|
37
|
+
seen.add(k);
|
|
38
|
+
}
|
|
39
|
+
if (dupAt >= 0) {
|
|
40
|
+
sink.push("A6", DEFAULT_SEVERITY.A6, `enum has duplicate value at index ${dupAt}`, {
|
|
41
|
+
jsonpointer: `${ctx.jsonpointer}/enum/${dupAt}`,
|
|
42
|
+
path: ctx.path, method: ctx.method,
|
|
43
|
+
fix_hint: "remove the duplicate enum entry",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function collectExamples(s: OpenAPIV3.SchemaObject): Array<{ value: unknown; pointer: string }> {
|
|
50
|
+
const out: Array<{ value: unknown; pointer: string }> = [];
|
|
51
|
+
if ((s as { example?: unknown }).example !== undefined) {
|
|
52
|
+
out.push({ value: (s as { example: unknown }).example, pointer: "example" });
|
|
53
|
+
}
|
|
54
|
+
// OpenAPI 3.1 allows examples[]
|
|
55
|
+
const arr = (s as { examples?: unknown }).examples;
|
|
56
|
+
if (Array.isArray(arr)) {
|
|
57
|
+
arr.forEach((v, i) => out.push({ value: v, pointer: `examples/${i}` }));
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function checkValueAgainstSchema(
|
|
63
|
+
value: unknown,
|
|
64
|
+
s: OpenAPIV3.SchemaObject,
|
|
65
|
+
basePointer: string,
|
|
66
|
+
kind: "example" | "default",
|
|
67
|
+
isStringy: boolean,
|
|
68
|
+
isNumeric: boolean,
|
|
69
|
+
ctx: SchemaContext,
|
|
70
|
+
sink: RuleSink,
|
|
71
|
+
): void {
|
|
72
|
+
// basePointer for example/default is sometimes a relative segment like "example".
|
|
73
|
+
// Prepend ctx.jsonpointer if needed.
|
|
74
|
+
const jsonpointer = basePointer.startsWith("/")
|
|
75
|
+
? basePointer
|
|
76
|
+
: `${ctx.jsonpointer}/${basePointer}`;
|
|
77
|
+
const ruleA1 = kind === "example" ? "A1" : "A5";
|
|
78
|
+
const ruleA2 = kind === "example" ? "A2" : "A5";
|
|
79
|
+
const ruleA3 = kind === "example" ? "A3" : "A5";
|
|
80
|
+
const ruleA4 = kind === "example" ? "A4" : "A5";
|
|
81
|
+
|
|
82
|
+
// Format check
|
|
83
|
+
const fmt = (s as { format?: string }).format;
|
|
84
|
+
if (fmt && isStringy && typeof value === "string") {
|
|
85
|
+
if (!validateExampleAgainstFormat(value, fmt)) {
|
|
86
|
+
sink.push(ruleA1, DEFAULT_SEVERITY[ruleA1], `${kind} ${JSON.stringify(value)} violates format: ${fmt}`, {
|
|
87
|
+
jsonpointer, path: ctx.path, method: ctx.method,
|
|
88
|
+
fix_hint: `make ${kind} match RFC for format: ${fmt}`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Enum check
|
|
94
|
+
if (Array.isArray(s.enum) && s.enum.length > 0) {
|
|
95
|
+
const key = stableKey(value);
|
|
96
|
+
if (!s.enum.some(e => stableKey(e) === key)) {
|
|
97
|
+
sink.push(ruleA2, DEFAULT_SEVERITY[ruleA2], `${kind} ${JSON.stringify(value)} is not in enum`, {
|
|
98
|
+
jsonpointer, path: ctx.path, method: ctx.method,
|
|
99
|
+
fix_hint: `pick a value from enum: ${JSON.stringify(s.enum)}`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Pattern check
|
|
105
|
+
const pattern = (s as { pattern?: string }).pattern;
|
|
106
|
+
if (pattern && typeof value === "string") {
|
|
107
|
+
let re: RegExp | null = null;
|
|
108
|
+
try { re = new RegExp(pattern); } catch { /* invalid regex — skip silently */ }
|
|
109
|
+
if (re && !re.test(value)) {
|
|
110
|
+
sink.push(ruleA3, DEFAULT_SEVERITY[ruleA3], `${kind} ${JSON.stringify(value)} does not match pattern ${pattern}`, {
|
|
111
|
+
jsonpointer, path: ctx.path, method: ctx.method,
|
|
112
|
+
fix_hint: "adjust the example to match the regex",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Length / range
|
|
118
|
+
if (isStringy && typeof value === "string") {
|
|
119
|
+
const min = (s as { minLength?: number }).minLength;
|
|
120
|
+
const max = (s as { maxLength?: number }).maxLength;
|
|
121
|
+
if (typeof min === "number" && value.length < min) {
|
|
122
|
+
sink.push(ruleA4, DEFAULT_SEVERITY[ruleA4], `${kind} length ${value.length} < minLength ${min}`, {
|
|
123
|
+
jsonpointer, path: ctx.path, method: ctx.method,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (typeof max === "number" && value.length > max) {
|
|
127
|
+
sink.push(ruleA4, DEFAULT_SEVERITY[ruleA4], `${kind} length ${value.length} > maxLength ${max}`, {
|
|
128
|
+
jsonpointer, path: ctx.path, method: ctx.method,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (isNumeric && typeof value === "number") {
|
|
133
|
+
const minimum = (s as { minimum?: number }).minimum;
|
|
134
|
+
const maximum = (s as { maximum?: number }).maximum;
|
|
135
|
+
if (typeof minimum === "number" && value < minimum) {
|
|
136
|
+
sink.push(ruleA4, DEFAULT_SEVERITY[ruleA4], `${kind} ${value} < minimum ${minimum}`, {
|
|
137
|
+
jsonpointer, path: ctx.path, method: ctx.method,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (typeof maximum === "number" && value > maximum) {
|
|
141
|
+
sink.push(ruleA4, DEFAULT_SEVERITY[ruleA4], `${kind} ${value} > maximum ${maximum}`, {
|
|
142
|
+
jsonpointer, path: ctx.path, method: ctx.method,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function stableKey(v: unknown): string {
|
|
149
|
+
if (v === null || typeof v !== "object") return JSON.stringify(v);
|
|
150
|
+
if (Array.isArray(v)) return "[" + v.map(stableKey).join(",") + "]";
|
|
151
|
+
const obj = v as Record<string, unknown>;
|
|
152
|
+
const keys = Object.keys(obj).sort();
|
|
153
|
+
return "{" + keys.map(k => JSON.stringify(k) + ":" + stableKey(obj[k])).join(",") + "}";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Suppress unused-export warning: RULE_AFFECTS imported here for future inline
|
|
157
|
+
// affects-tagging if rules grow more local context.
|
|
158
|
+
void RULE_AFFECTS;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
2
|
+
import type { Issue, RuleId, Severity, HeuristicConfig } from "../types.ts";
|
|
3
|
+
import { DEFAULT_SEVERITY } from "../types.ts";
|
|
4
|
+
import type { ParamContext, SchemaContext, RequestBodyContext } from "../walker.ts";
|
|
5
|
+
import { normalisedTypes } from "../walker.ts";
|
|
6
|
+
|
|
7
|
+
interface RuleSink {
|
|
8
|
+
push(rule: RuleId, severity: Severity, message: string, opts: Partial<Pick<Issue, "path" | "method" | "fix_hint">> & { jsonpointer: string }): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* B2 — path/query param named like an id (`*_id`, `id`) without `format: uuid`
|
|
13
|
+
* or `pattern`. Heuristic: only on params whose name matches the id-suffix list.
|
|
14
|
+
*/
|
|
15
|
+
export function runParamHeuristics(ctx: ParamContext, sink: RuleSink, h: HeuristicConfig): void {
|
|
16
|
+
const p = ctx.param;
|
|
17
|
+
if (p.in !== "path" && p.in !== "query") return;
|
|
18
|
+
const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject;
|
|
19
|
+
const types = normalisedTypes(schema);
|
|
20
|
+
// B2 only applies to string-shaped ids (uuid). Integer ids are constrained
|
|
21
|
+
// by minimum/maximum (B3 territory), not by format: uuid.
|
|
22
|
+
if (types.length > 0 && !types.includes("string")) return;
|
|
23
|
+
const looksLikeId = p.name === "id" || h.id_suffixes.some(s => p.name.endsWith(s));
|
|
24
|
+
if (!looksLikeId) return;
|
|
25
|
+
const fmt = (schema as { format?: string }).format;
|
|
26
|
+
const hasPattern = !!(schema as { pattern?: string }).pattern;
|
|
27
|
+
if (fmt !== "uuid" && !hasPattern) {
|
|
28
|
+
sink.push("B2", DEFAULT_SEVERITY.B2, `id-like param "${p.name}" missing format: uuid or pattern (heuristic)`, {
|
|
29
|
+
jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
|
|
30
|
+
fix_hint: "if the id is a UUID, add format: uuid; otherwise add a pattern",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* B5/B6 — schema property name suggests a known semantic type, but `format`
|
|
37
|
+
* is missing.
|
|
38
|
+
* - `*_at`, `*_date`, `*_time`, `created`, `updated`, `timestamp` → `date-time`
|
|
39
|
+
* - `email`, `url`, `website`, `homepage` → `email` / `uri`
|
|
40
|
+
*/
|
|
41
|
+
export function runSchemaHeuristics(ctx: SchemaContext, sink: RuleSink, h: HeuristicConfig): void {
|
|
42
|
+
if (ctx.origin !== "property" || !ctx.propertyName) return;
|
|
43
|
+
const s = ctx.schema;
|
|
44
|
+
const types = normalisedTypes(s);
|
|
45
|
+
if (!types.includes("string")) return;
|
|
46
|
+
const fmt = (s as { format?: string }).format;
|
|
47
|
+
const name = ctx.propertyName;
|
|
48
|
+
|
|
49
|
+
// B5 — timestamp fields
|
|
50
|
+
const looksLikeTimestamp =
|
|
51
|
+
h.timestamp_suffixes.some(suf => name.endsWith(suf)) ||
|
|
52
|
+
["created", "updated", "timestamp"].includes(name);
|
|
53
|
+
if (looksLikeTimestamp && fmt !== "date-time" && fmt !== "date") {
|
|
54
|
+
sink.push("B5", DEFAULT_SEVERITY.B5, `field "${name}" looks like a timestamp but has no format: date-time (heuristic)`, {
|
|
55
|
+
jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
|
|
56
|
+
fix_hint: "add format: date-time so --validate-schema enforces RFC3339",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// B6 — email / url
|
|
61
|
+
if (name === "email" && fmt !== "email") {
|
|
62
|
+
sink.push("B6", DEFAULT_SEVERITY.B6, `field "${name}" missing format: email (heuristic)`, {
|
|
63
|
+
jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
|
|
64
|
+
fix_hint: "add format: email",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (h.url_names.includes(name) && fmt !== "uri" && fmt !== "url") {
|
|
68
|
+
sink.push("B6", DEFAULT_SEVERITY.B6, `field "${name}" missing format: uri (heuristic)`, {
|
|
69
|
+
jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
|
|
70
|
+
fix_hint: "add format: uri",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* B9 — request-body schema declares semantically-required-looking properties
|
|
77
|
+
* (`name`, `email`, `title`) but `required: []` is empty / absent. Heuristic.
|
|
78
|
+
*/
|
|
79
|
+
export function runRequestBodyHeuristics(ctx: RequestBodyContext, sink: RuleSink, h: HeuristicConfig): void {
|
|
80
|
+
if (!ctx.requestBody.content) return;
|
|
81
|
+
for (const [ct, mt] of Object.entries(ctx.requestBody.content)) {
|
|
82
|
+
if (!ct.includes("json")) continue;
|
|
83
|
+
const schema = mt.schema as OpenAPIV3.SchemaObject | undefined;
|
|
84
|
+
if (!schema || !schema.properties) continue;
|
|
85
|
+
const required = (schema.required ?? []) as string[];
|
|
86
|
+
const propNames = Object.keys(schema.properties);
|
|
87
|
+
const semanticPresent = h.semantic_required.filter(n => propNames.includes(n));
|
|
88
|
+
const semanticMissing = semanticPresent.filter(n => !required.includes(n));
|
|
89
|
+
if (semanticPresent.length > 0 && semanticMissing.length === semanticPresent.length) {
|
|
90
|
+
sink.push("B9", DEFAULT_SEVERITY.B9, `request body has properties [${semanticPresent.join(", ")}] but none are required (heuristic)`, {
|
|
91
|
+
jsonpointer: `${ctx.jsonpointer}/content/${ct.replace(/~/g, "~0").replace(/\//g, "~1")}/schema/required`,
|
|
92
|
+
path: ctx.path, method: ctx.method,
|
|
93
|
+
fix_hint: `consider adding to required: [${semanticMissing.map(n => `"${n}"`).join(", ")}]`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
2
|
+
import type { Issue, RuleId, Severity, HeuristicConfig } from "../types.ts";
|
|
3
|
+
import { DEFAULT_SEVERITY } from "../types.ts";
|
|
4
|
+
import type { ParamContext, ResponseContext, RequestBodyContext, SchemaContext } from "../walker.ts";
|
|
5
|
+
import { normalisedTypes } from "../walker.ts";
|
|
6
|
+
|
|
7
|
+
interface RuleSink {
|
|
8
|
+
push(rule: RuleId, severity: Severity, message: string, opts: Partial<Pick<Issue, "path" | "method" | "fix_hint">> & { jsonpointer: string }): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Group B — formal strictness gaps on parameters.
|
|
13
|
+
* B1 (path-param without format/pattern) → high.
|
|
14
|
+
* B3 (integer-param without min/max — pagination names get medium, others low).
|
|
15
|
+
* B4 (cursor-name string-param without minLength: 1) → low.
|
|
16
|
+
*/
|
|
17
|
+
export function runParamStrictnessRules(ctx: ParamContext, sink: RuleSink, h: HeuristicConfig): void {
|
|
18
|
+
const p = ctx.param;
|
|
19
|
+
const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject;
|
|
20
|
+
const types = normalisedTypes(schema);
|
|
21
|
+
|
|
22
|
+
if (p.in === "path" && types.includes("string")) {
|
|
23
|
+
// Only flag string path-params: integer/number params have type-level
|
|
24
|
+
// constraints (minimum/maximum, multipleOf), so missing format/pattern
|
|
25
|
+
// is benign there.
|
|
26
|
+
const hasFormat = !!(schema as { format?: string }).format;
|
|
27
|
+
const hasPattern = !!(schema as { pattern?: string }).pattern;
|
|
28
|
+
if (!hasFormat && !hasPattern) {
|
|
29
|
+
sink.push("B1", DEFAULT_SEVERITY.B1, `path-param "${p.name}" missing format/pattern`, {
|
|
30
|
+
jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
|
|
31
|
+
fix_hint: "add format: uuid (or pattern: ^...$) so SDKs reject malformed values client-side",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (types.includes("integer") || types.includes("number")) {
|
|
37
|
+
const hasMin = typeof (schema as { minimum?: unknown }).minimum === "number";
|
|
38
|
+
const hasMax = typeof (schema as { maximum?: unknown }).maximum === "number";
|
|
39
|
+
if (!hasMin || !hasMax) {
|
|
40
|
+
const isPagination = h.pagination_names.some(n => n.toLowerCase() === p.name.toLowerCase());
|
|
41
|
+
const sev: Severity = isPagination ? "medium" : "low";
|
|
42
|
+
const which = !hasMin && !hasMax ? "minimum/maximum" : !hasMin ? "minimum" : "maximum";
|
|
43
|
+
sink.push("B3", sev, `${p.in}-param "${p.name}" (${types.join("|")}) missing ${which}`, {
|
|
44
|
+
jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
|
|
45
|
+
fix_hint: `add ${which} so out-of-range values are rejected before reaching the server`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (types.includes("string") && h.cursor_names.some(n => n.toLowerCase() === p.name.toLowerCase())) {
|
|
51
|
+
const minLen = (schema as { minLength?: unknown }).minLength;
|
|
52
|
+
if (typeof minLen !== "number" || minLen < 1) {
|
|
53
|
+
sink.push("B4", DEFAULT_SEVERITY.B4, `cursor-param "${p.name}" missing minLength: 1`, {
|
|
54
|
+
jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
|
|
55
|
+
fix_hint: "add minLength: 1 so empty cursor strings are rejected client-side",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* B7 — 2xx response without a JSON schema. `--validate-schema` silently skips
|
|
63
|
+
* such endpoints, masking real type drift.
|
|
64
|
+
*/
|
|
65
|
+
export function runResponseStrictnessRules(ctx: ResponseContext, sink: RuleSink): void {
|
|
66
|
+
const status = parseInt(ctx.status, 10);
|
|
67
|
+
if (!Number.isFinite(status) || status < 200 || status >= 300) return;
|
|
68
|
+
// 204 No Content / 205 Reset Content / 304 Not Modified by definition carry no body.
|
|
69
|
+
if (status === 204 || status === 205) return;
|
|
70
|
+
const r = ctx.response;
|
|
71
|
+
const hasJsonSchema = r.content && Object.entries(r.content).some(([ct, mt]) => {
|
|
72
|
+
return ct.includes("json") && (mt.schema !== undefined);
|
|
73
|
+
});
|
|
74
|
+
if (!hasJsonSchema) {
|
|
75
|
+
sink.push("B7", DEFAULT_SEVERITY.B7, `${ctx.status} response missing JSON schema`, {
|
|
76
|
+
jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
|
|
77
|
+
fix_hint: "declare content.application/json.schema so --validate-schema can verify the response",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* B8 — request-body root schema without explicit `additionalProperties`.
|
|
84
|
+
* Informational; tied to mass-assignment risk (T58).
|
|
85
|
+
*/
|
|
86
|
+
export function runRequestBodyStrictnessRules(ctx: RequestBodyContext, sink: RuleSink): void {
|
|
87
|
+
if (!ctx.requestBody.content) return;
|
|
88
|
+
for (const [ct, mt] of Object.entries(ctx.requestBody.content)) {
|
|
89
|
+
if (!ct.includes("json")) continue;
|
|
90
|
+
const schema = mt.schema as OpenAPIV3.SchemaObject | undefined;
|
|
91
|
+
if (!schema) continue;
|
|
92
|
+
const ap = (schema as { additionalProperties?: unknown }).additionalProperties;
|
|
93
|
+
if (ap === undefined) {
|
|
94
|
+
sink.push("B8", DEFAULT_SEVERITY.B8, `request body schema does not set additionalProperties`, {
|
|
95
|
+
jsonpointer: `${ctx.jsonpointer}/content/${ct.replace(/~/g, "~0").replace(/\//g, "~1")}/schema`,
|
|
96
|
+
path: ctx.path, method: ctx.method,
|
|
97
|
+
fix_hint: "set additionalProperties: false to make mass-assignment vectors explicit",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Schema-level B-rules that aren't on parameters: currently empty placeholder
|
|
105
|
+
* for future expansion (e.g. response-body B5 picked up via heuristics).
|
|
106
|
+
*/
|
|
107
|
+
export function runSchemaStrictnessRules(_ctx: SchemaContext, _sink: RuleSink): void {
|
|
108
|
+
// intentionally empty — heuristic schema-level checks live in heuristics.ts
|
|
109
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { RecommendedAction } from "../diagnostics/failure-hints.ts";
|
|
2
|
+
|
|
3
|
+
// Severity unified in src/core/severity (ARV-250). Lint historically used a
|
|
4
|
+
// 3-tier ladder (high/medium/low) without 'critical' or 'info'. ARV-255
|
|
5
|
+
// will downgrade most lint findings to info/low; this re-export aligns the
|
|
6
|
+
// type but does not yet change DEFAULT_SEVERITY values per rule.
|
|
7
|
+
import type { Severity } from "../severity/index.ts";
|
|
8
|
+
export type { Severity };
|
|
9
|
+
|
|
10
|
+
export type { RecommendedAction };
|
|
11
|
+
|
|
12
|
+
export type RuleId =
|
|
13
|
+
| "A1" | "A2" | "A3" | "A4" | "A5" | "A6"
|
|
14
|
+
| "B1" | "B2" | "B3" | "B4" | "B5" | "B6" | "B7" | "B8" | "B9";
|
|
15
|
+
|
|
16
|
+
export const ALL_RULES: RuleId[] = [
|
|
17
|
+
"A1", "A2", "A3", "A4", "A5", "A6",
|
|
18
|
+
"B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* ARV-255 (m-21 pivot): all lint findings cap at LOW/INFO. Static spec
|
|
23
|
+
* analysis is hygiene — no runtime evidence, no exploit pathway, no
|
|
24
|
+
* security or contract drift. The old "HIGH on missing additionalProperties"
|
|
25
|
+
* inflation made the audit report unreadable; now lint lives in the
|
|
26
|
+
* hygiene category and surfaces via `zond lint` separately.
|
|
27
|
+
*
|
|
28
|
+
* Tier assignment:
|
|
29
|
+
* - `low`: real spec violations (format mismatch in example, missing
|
|
30
|
+
* path-param format, response without schema). Worth fixing, but not
|
|
31
|
+
* security.
|
|
32
|
+
* - `info`: style and documentation gaps (additionalProperties, naming,
|
|
33
|
+
* missing examples, optional descriptions). Could be intentional.
|
|
34
|
+
*/
|
|
35
|
+
export const DEFAULT_SEVERITY: Record<RuleId, Severity> = {
|
|
36
|
+
A1: "low", A2: "low", A3: "info", A4: "info", A5: "info", A6: "info",
|
|
37
|
+
B1: "low", B2: "info", B3: "info", B4: "info", B5: "info",
|
|
38
|
+
B6: "info", B7: "low", B8: "info", B9: "info",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export interface Issue {
|
|
42
|
+
rule: RuleId;
|
|
43
|
+
severity: Severity;
|
|
44
|
+
path?: string;
|
|
45
|
+
method?: string;
|
|
46
|
+
jsonpointer: string;
|
|
47
|
+
message: string;
|
|
48
|
+
fix_hint?: string;
|
|
49
|
+
affects?: string[];
|
|
50
|
+
/** TASK-294: agent-routable action — always `fix_spec` for lint issues
|
|
51
|
+
* (the spec is the source of truth and the only thing to edit). */
|
|
52
|
+
recommended_action: RecommendedAction;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type RuleSetting = "off" | Severity;
|
|
56
|
+
|
|
57
|
+
export interface HeuristicConfig {
|
|
58
|
+
id_suffixes: string[];
|
|
59
|
+
timestamp_suffixes: string[];
|
|
60
|
+
url_names: string[];
|
|
61
|
+
cursor_names: string[];
|
|
62
|
+
pagination_names: string[];
|
|
63
|
+
semantic_required: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface LintConfig {
|
|
67
|
+
rules: Partial<Record<RuleId, RuleSetting>>;
|
|
68
|
+
heuristics: HeuristicConfig;
|
|
69
|
+
ignore_paths: string[];
|
|
70
|
+
include_paths?: string[];
|
|
71
|
+
max_issues?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface LintStats {
|
|
75
|
+
total: number;
|
|
76
|
+
critical: number;
|
|
77
|
+
high: number;
|
|
78
|
+
medium: number;
|
|
79
|
+
low: number;
|
|
80
|
+
info: number;
|
|
81
|
+
endpoints: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface LintResult {
|
|
85
|
+
issues: Issue[];
|
|
86
|
+
stats: LintStats;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const DEFAULT_HEURISTICS: HeuristicConfig = {
|
|
90
|
+
id_suffixes: ["_id", "Id", "ID"],
|
|
91
|
+
timestamp_suffixes: ["_at", "_date", "_time"],
|
|
92
|
+
url_names: ["url", "website", "homepage", "callback_url", "webhook_url"],
|
|
93
|
+
cursor_names: ["after", "before", "cursor", "token", "page_token", "next_token"],
|
|
94
|
+
pagination_names: ["limit", "offset", "page", "size", "count", "per_page", "page_size"],
|
|
95
|
+
semantic_required: ["name", "email", "title"],
|
|
96
|
+
};
|