@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,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `zond prepare-fixtures` — unified fixture-pack command.
|
|
3
|
+
*
|
|
4
|
+
* Consolidates the former `discover` and `bootstrap` (TASK-299, m-13 D):
|
|
5
|
+
*
|
|
6
|
+
* - default → single-pass discover (auto-fill FK ids
|
|
7
|
+
* from list endpoints).
|
|
8
|
+
* - --cascade → multi-pass cascade (former bootstrap).
|
|
9
|
+
* - --seed → cascade + POST-create when discover misses
|
|
10
|
+
* (implies --cascade).
|
|
11
|
+
* - --verify / --refresh → revalidate fixtures via read-by-id
|
|
12
|
+
* (former `discover --verify/--refresh`).
|
|
13
|
+
*
|
|
14
|
+
* The imperative cores (`discoverCommand`, `bootstrapCommand`) live in
|
|
15
|
+
* the original modules and are still consumed directly by tests. This
|
|
16
|
+
* module only owns the CLI surface.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { Command } from "commander";
|
|
20
|
+
import { globalJson, resolveSpecArg } from "../resolve.ts";
|
|
21
|
+
import { parsePositiveInt } from "../argv.ts";
|
|
22
|
+
import { getDb } from "../../db/schema.ts";
|
|
23
|
+
import { findCollectionByNameOrId } from "../../db/queries.ts";
|
|
24
|
+
import { printError } from "../output.ts";
|
|
25
|
+
import { discoverCommand } from "./discover.ts";
|
|
26
|
+
import { bootstrapCommand } from "./bootstrap.ts";
|
|
27
|
+
import { loadEnvMeta } from "../../core/parser/variables.ts";
|
|
28
|
+
import { resolveTimeoutMs } from "../../core/workspace/config.ts";
|
|
29
|
+
import { getApi, MISSING_API_MESSAGE } from "../util/api-context.ts";
|
|
30
|
+
|
|
31
|
+
export function registerPrepareFixtures(program: Command): void {
|
|
32
|
+
program
|
|
33
|
+
.command("prepare-fixtures")
|
|
34
|
+
.description(
|
|
35
|
+
"Auto-fill apis/<name>/.env.yaml — single-pass discover by default, " +
|
|
36
|
+
"or `--cascade` for the multi-pass discover+seed flow (replaces the legacy " +
|
|
37
|
+
"`discover` and `bootstrap` commands; TASK-299).",
|
|
38
|
+
)
|
|
39
|
+
// Not `requiredOption` — the value can also come from the program-level
|
|
40
|
+
// --api flag (parsed by program.ts and mirrored into ZOND_API_GLOBAL),
|
|
41
|
+
// ZOND_API env, or .zond/current-api. Commander would otherwise reject
|
|
42
|
+
// `zond prepare-fixtures --api foo` because it routes `--api` to the
|
|
43
|
+
// global option, leaving the subcommand's opts.api undefined.
|
|
44
|
+
.option("--api <name>", "Registered API to prepare (apis/<name>/.env.yaml). Falls back to ZOND_API / .zond/current-api.")
|
|
45
|
+
.option("--db <path>", "Path to SQLite database file")
|
|
46
|
+
.option("--api-dir <path>", "Override apis/<name>/ root (defaults to the collection's base_dir)")
|
|
47
|
+
.option("--env <path>", "Override .env.yaml path (defaults to <api-dir>/.env.yaml)")
|
|
48
|
+
.option("--apply", "Write discovered values to .env.yaml (with .env.yaml.bak backup). Default: dry-run.")
|
|
49
|
+
.option("--cascade", "Multi-pass cascade discover (former `bootstrap`). Required for --seed / --force / --max-passes.")
|
|
50
|
+
.option("--seed", "POST-create resources when discover can't find an existing record (implies --cascade)")
|
|
51
|
+
.option("--force", "Re-discover/re-seed even if a fixture is already filled (cascade only)")
|
|
52
|
+
.option("--verify", "GET each fixture's read-by-id endpoint and classify live/stale/unknown (single-pass only). Combine with --apply (or use --refresh) to drop stale fixtures and re-resolve them. (TASK-281)")
|
|
53
|
+
.option("--refresh", "Shortcut for --verify --apply (single-pass only). (TASK-281)")
|
|
54
|
+
.option("--timeout <ms>", "Per-request timeout in ms (overrides apis/<name>/.env.yaml `timeoutMs` and zond.config.yml `defaults.timeout_ms`; default 30000)", parsePositiveInt("--timeout"))
|
|
55
|
+
.option("--max-passes <n>", "Cap on cascade passes (default 8; cascade only)", parsePositiveInt("--max-passes"))
|
|
56
|
+
.action(async (opts, cmd: Command) => {
|
|
57
|
+
// ARV-53: --api resolution lives in cli/util/api-context.ts —
|
|
58
|
+
// local opt > ancestor opt > ZOND_API_GLOBAL/ZOND_API/.zond/current-api.
|
|
59
|
+
const apiName = getApi(cmd, opts);
|
|
60
|
+
if (!apiName) {
|
|
61
|
+
printError(MISSING_API_MESSAGE);
|
|
62
|
+
process.exitCode = 2;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
opts.api = apiName;
|
|
66
|
+
|
|
67
|
+
const cascade = opts.cascade === true || opts.seed === true;
|
|
68
|
+
const refresh = opts.refresh === true;
|
|
69
|
+
const verify = opts.verify === true || refresh;
|
|
70
|
+
|
|
71
|
+
// Flag combos that don't make sense — fail fast with a clear hint.
|
|
72
|
+
if (cascade && verify) {
|
|
73
|
+
printError("--verify / --refresh are single-pass options; drop --cascade/--seed or drop --verify.");
|
|
74
|
+
process.exitCode = 2;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!cascade && (opts.force === true || typeof opts.maxPasses === "number")) {
|
|
78
|
+
printError("--force / --max-passes only apply with --cascade (or --seed).");
|
|
79
|
+
process.exitCode = 2;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const resolved = resolveSpecArg(undefined, opts.api, opts.db);
|
|
84
|
+
if ("error" in resolved) { printError(resolved.error); process.exitCode = 2; return; }
|
|
85
|
+
|
|
86
|
+
let apiDir = opts.apiDir as string | undefined;
|
|
87
|
+
if (!apiDir) {
|
|
88
|
+
try {
|
|
89
|
+
getDb(opts.db);
|
|
90
|
+
const col = findCollectionByNameOrId(opts.api);
|
|
91
|
+
apiDir = col?.base_dir ?? `apis/${opts.api}`;
|
|
92
|
+
} catch {
|
|
93
|
+
apiDir = `apis/${opts.api}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let envTimeout: number | undefined;
|
|
98
|
+
try {
|
|
99
|
+
envTimeout = (await loadEnvMeta(undefined, apiDir)).timeoutMs;
|
|
100
|
+
} catch { /* meta is best-effort */ }
|
|
101
|
+
const timeoutMs = resolveTimeoutMs(opts.timeout, envTimeout);
|
|
102
|
+
|
|
103
|
+
if (cascade) {
|
|
104
|
+
process.exitCode = await bootstrapCommand({
|
|
105
|
+
specPath: resolved.spec,
|
|
106
|
+
apiDir,
|
|
107
|
+
envPath: opts.env,
|
|
108
|
+
apply: opts.apply === true,
|
|
109
|
+
seed: opts.seed === true,
|
|
110
|
+
force: opts.force === true,
|
|
111
|
+
timeoutMs,
|
|
112
|
+
maxPasses: opts.maxPasses,
|
|
113
|
+
json: globalJson(cmd),
|
|
114
|
+
// ARV-205 (R10/F6, R13/F19): surface the user-facing command name
|
|
115
|
+
// in the JSON envelope. Without this the user sees command="bootstrap"
|
|
116
|
+
// even though they typed `zond prepare-fixtures …`.
|
|
117
|
+
commandName: "prepare-fixtures",
|
|
118
|
+
});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
process.exitCode = await discoverCommand({
|
|
123
|
+
specPath: resolved.spec,
|
|
124
|
+
apiDir,
|
|
125
|
+
envPath: opts.env,
|
|
126
|
+
apply: opts.apply === true || refresh,
|
|
127
|
+
verify,
|
|
128
|
+
timeoutMs,
|
|
129
|
+
json: globalJson(cmd),
|
|
130
|
+
// ARV-205 (R10/F6, R13/F19, R14): single-pass branch also delegates,
|
|
131
|
+
// so surface the user-facing command name in the JSON envelope.
|
|
132
|
+
commandName: "prepare-fixtures",
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
3
|
+
import { loadEnvironment, loadEnvFile } from "../../../core/parser/variables.ts";
|
|
4
|
+
import {
|
|
5
|
+
runMassAssignmentProbes,
|
|
6
|
+
formatDigestMarkdown,
|
|
7
|
+
emitRegressionSuites,
|
|
8
|
+
} from "../../../core/probe/mass-assignment-probe.ts";
|
|
9
|
+
import { loadSpecForProbe, writeProbeSuites } from "../../../core/probe/runner.ts";
|
|
10
|
+
import { printError, printSuccess, printWarning } from "../../output.ts";
|
|
11
|
+
import { jsonOk, jsonError, printJson } from "../../json-envelope.ts";
|
|
12
|
+
import { getSecretRegistry } from "../../../core/secrets/registry.ts";
|
|
13
|
+
import { applySanitizer } from "../../../core/exporter/exporter.ts";
|
|
14
|
+
import { rotateOutputTarget } from "../../../core/workspace/output-rotation.ts";
|
|
15
|
+
import { tallyBySeverity, formatSummaryLine } from "../../../core/probe/verdict-aggregator.ts";
|
|
16
|
+
import { printMutationBanner, countCleanupFailures } from "../../../core/probe/shared.ts";
|
|
17
|
+
import { MassAssignmentProbe } from "../../../core/probe/mass-assignment-probe-class.ts";
|
|
18
|
+
import { summarizeDryRun, formatDryRunDigest } from "../../../core/probe/dry-run-envelope.ts";
|
|
19
|
+
import { compileOperationFilter } from "../../../core/selectors/operation-filter.ts";
|
|
20
|
+
import type { EndpointVerdict, MassAssignmentResult } from "../../../core/probe/mass-assignment-probe.ts";
|
|
21
|
+
import type { ProbeEndpointResult, ProbeEndpointStatus, ProbeFindingSeverity } from "../../../core/probe/types.ts";
|
|
22
|
+
|
|
23
|
+
interface BucketCounts {
|
|
24
|
+
high: number;
|
|
25
|
+
inconclusiveBaseline: number;
|
|
26
|
+
inconclusive5xx: number;
|
|
27
|
+
medium: number;
|
|
28
|
+
low: number;
|
|
29
|
+
ok: number;
|
|
30
|
+
skipped: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const MA_BUCKETS: ReadonlyArray<readonly [string, keyof BucketCounts & string]> = [
|
|
34
|
+
["high", "high"],
|
|
35
|
+
["inconclusive-baseline", "inconclusiveBaseline"],
|
|
36
|
+
["inconclusive-5xx", "inconclusive5xx"],
|
|
37
|
+
["medium", "medium"],
|
|
38
|
+
["low", "low"],
|
|
39
|
+
["ok", "ok"],
|
|
40
|
+
["skipped", "skipped"],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const MA_SUMMARY: ReadonlyArray<readonly [string, keyof BucketCounts & string]> = [
|
|
44
|
+
["HIGH", "high"],
|
|
45
|
+
["INCONCLUSIVE", "inconclusiveBaseline"],
|
|
46
|
+
["INCONCLUSIVE-5XX", "inconclusive5xx"],
|
|
47
|
+
["MED", "medium"],
|
|
48
|
+
["LOW", "low"],
|
|
49
|
+
["OK", "ok"],
|
|
50
|
+
["SKIPPED", "skipped"],
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const MA_ZERO: BucketCounts = {
|
|
54
|
+
high: 0, inconclusiveBaseline: 0, inconclusive5xx: 0, medium: 0, low: 0, ok: 0, skipped: 0,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export interface ProbeMassAssignmentOptions {
|
|
58
|
+
specPath: string;
|
|
59
|
+
env?: string;
|
|
60
|
+
/** Markdown digest output file. If omitted — print to stdout. */
|
|
61
|
+
output?: string;
|
|
62
|
+
/** Emit regression YAML suites into this directory. */
|
|
63
|
+
emitTests?: string;
|
|
64
|
+
tag?: string;
|
|
65
|
+
noCleanup?: boolean;
|
|
66
|
+
noDiscover?: boolean;
|
|
67
|
+
timeoutMs?: number;
|
|
68
|
+
json?: boolean;
|
|
69
|
+
listTags?: boolean;
|
|
70
|
+
overwrite?: boolean;
|
|
71
|
+
/** m-17 / ARV-52: list which endpoints + fields would be attacked
|
|
72
|
+
* without sending live traffic. */
|
|
73
|
+
dryRun?: boolean;
|
|
74
|
+
/** m-17 / ARV-52: m-15 ARV-9 selector grammar (`path:`/`method:`/`tag:`/`operation-id:`). */
|
|
75
|
+
include?: string[];
|
|
76
|
+
exclude?: string[];
|
|
77
|
+
/** m-17 / ARV-51: format for --output / non-json stdout. */
|
|
78
|
+
report?: "markdown" | "json";
|
|
79
|
+
/** ARV-252: surface INFO-severity inconclusive verdicts (absent-but-
|
|
80
|
+
* unverifiable). Silently-ignored verdicts are never shown — they
|
|
81
|
+
* represent correct framework behaviour. */
|
|
82
|
+
verbose?: boolean;
|
|
83
|
+
/** ARV-252: additional suspect fields to inject, in `name=value`
|
|
84
|
+
* form. Extends the curated SUSPECTED_FIELDS list per-run. Full
|
|
85
|
+
* spec-extension support (x-zond-suspect-fields) is tracked in
|
|
86
|
+
* ARV-189. */
|
|
87
|
+
suspectField?: string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function probeMassAssignmentCommand(
|
|
91
|
+
options: ProbeMassAssignmentOptions,
|
|
92
|
+
): Promise<number> {
|
|
93
|
+
try {
|
|
94
|
+
const loaded = await loadSpecForProbe({
|
|
95
|
+
specPath: options.specPath,
|
|
96
|
+
tag: options.tag,
|
|
97
|
+
listTags: options.listTags,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (loaded.kind === "tags") {
|
|
101
|
+
if (options.json) {
|
|
102
|
+
printJson(jsonOk("probe-mass-assignment", { tags: loaded.tags }));
|
|
103
|
+
} else if (loaded.tags.length === 0) {
|
|
104
|
+
console.log("No tags found in spec.");
|
|
105
|
+
} else {
|
|
106
|
+
console.log("Available tags:");
|
|
107
|
+
for (const t of loaded.tags) console.log(` - ${t}`);
|
|
108
|
+
}
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
if (loaded.kind === "tag-not-found") {
|
|
112
|
+
const msg = `No endpoints tagged "${loaded.tag}". Available tags: ${loaded.available.length ? loaded.available.join(", ") : "(none)"}`;
|
|
113
|
+
if (options.json) printJson(jsonError("probe-mass-assignment", [msg]));
|
|
114
|
+
else printWarning(msg);
|
|
115
|
+
return 2;
|
|
116
|
+
}
|
|
117
|
+
const { endpoints: rawEndpoints, securitySchemes } = loaded;
|
|
118
|
+
|
|
119
|
+
// m-17 / ARV-52: apply --include / --exclude through the unified
|
|
120
|
+
// operation filter (m-15 ARV-9). probe-family was deferred at AC#6;
|
|
121
|
+
// wiring it here closes that and gives mass-assignment parity with
|
|
122
|
+
// probe-static / probe-security.
|
|
123
|
+
let endpoints = rawEndpoints;
|
|
124
|
+
if (options.include?.length || options.exclude?.length) {
|
|
125
|
+
const compiled = compileOperationFilter({ includes: options.include, excludes: options.exclude });
|
|
126
|
+
if (compiled.errors.length > 0) {
|
|
127
|
+
const message = compiled.errors.join("\n");
|
|
128
|
+
if (options.json) printJson(jsonError("probe-mass-assignment", [message]));
|
|
129
|
+
else printError(message);
|
|
130
|
+
return 2;
|
|
131
|
+
}
|
|
132
|
+
endpoints = endpoints.filter(compiled.filter);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Load env vars (base_url, auth_token, api_key, path-param overrides).
|
|
136
|
+
let vars: Record<string, string> = {};
|
|
137
|
+
if (options.env) {
|
|
138
|
+
const fromFile = await loadEnvFile(options.env);
|
|
139
|
+
if (!fromFile) {
|
|
140
|
+
const msg = `Environment file not found: ${options.env}`;
|
|
141
|
+
if (options.json) printJson(jsonError("probe-mass-assignment", [msg]));
|
|
142
|
+
else printError(msg);
|
|
143
|
+
return 2;
|
|
144
|
+
}
|
|
145
|
+
vars = fromFile;
|
|
146
|
+
} else {
|
|
147
|
+
vars = await loadEnvironment();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// m-17 / ARV-52: --dry-run lists which endpoints + suspect fields the
|
|
151
|
+
// probe would touch without sending live traffic. base_url is not
|
|
152
|
+
// required on this path (mirrors probe-security).
|
|
153
|
+
if (options.dryRun) {
|
|
154
|
+
const probe = new MassAssignmentProbe();
|
|
155
|
+
const plan = await probe.dryRun({
|
|
156
|
+
specPath: options.specPath,
|
|
157
|
+
endpoints,
|
|
158
|
+
securitySchemes,
|
|
159
|
+
vars,
|
|
160
|
+
options: {},
|
|
161
|
+
});
|
|
162
|
+
const data = summarizeDryRun(plan);
|
|
163
|
+
if (options.json) {
|
|
164
|
+
printJson(jsonOk("probe-mass-assignment", data));
|
|
165
|
+
} else {
|
|
166
|
+
console.log(formatDryRunDigest(plan));
|
|
167
|
+
}
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!vars["base_url"]) {
|
|
172
|
+
const msg = "base_url is required (set in .env.yaml or via --env file). Probing requires a live API.";
|
|
173
|
+
if (options.json) printJson(jsonError("probe-mass-assignment", [msg]));
|
|
174
|
+
else printError(msg);
|
|
175
|
+
return 2;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// TASK-259: tell the user *before* we mutate anything. Suppressed in
|
|
179
|
+
// --json mode (warnings already in envelope) and when --no-cleanup
|
|
180
|
+
// is off — this banner is about the cleanup-pass, too.
|
|
181
|
+
printMutationBanner("probe-mass-assignment", vars, { quiet: options.json === true });
|
|
182
|
+
|
|
183
|
+
const result = await runMassAssignmentProbes({
|
|
184
|
+
endpoints,
|
|
185
|
+
securitySchemes,
|
|
186
|
+
vars,
|
|
187
|
+
noCleanup: options.noCleanup,
|
|
188
|
+
timeoutMs: options.timeoutMs,
|
|
189
|
+
discover: !options.noDiscover,
|
|
190
|
+
extraSuspectFields: parseSuspectFieldFlags(options.suspectField),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ARV-252: filter verdicts for display under the evidence-chain
|
|
194
|
+
// principle. Silently-ignored (correct framework behaviour) never
|
|
195
|
+
// surfaces; absent-but-unverifiable surfaces only under --verbose.
|
|
196
|
+
// HIGH and inconclusive-baseline/5xx always show. JSON envelope
|
|
197
|
+
// always carries the full unfiltered list (agents triage explicitly).
|
|
198
|
+
const displayResult: MassAssignmentResult = {
|
|
199
|
+
...result,
|
|
200
|
+
verdicts: filterVerdictsForDisplay(result.verdicts, { verbose: options.verbose === true }),
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// TASK-168 (m-10): vars came from .env.yaml — register them so any
|
|
204
|
+
// echoed token (URL, body, header) gets redacted in the digest.
|
|
205
|
+
getSecretRegistry().registerAll(vars);
|
|
206
|
+
const md = applySanitizer(formatDigestMarkdown(displayResult, options.specPath));
|
|
207
|
+
|
|
208
|
+
// m-17 / ARV-51: --output writes whichever format `--report` selected
|
|
209
|
+
// (default markdown). `--json` envelope is always structured.
|
|
210
|
+
const reportFmt: "markdown" | "json" = options.report ?? "markdown";
|
|
211
|
+
const structuredEndpoints = buildMaStructuredEndpoints(result);
|
|
212
|
+
if (options.output) {
|
|
213
|
+
await mkdir(join(options.output, "..").replace(/\/\.$/, ""), { recursive: true }).catch(() => {});
|
|
214
|
+
// TASK-162 (m-9 P6): rotate previous digest to <stem>-vN.md instead
|
|
215
|
+
// of silent overwrite. --overwrite opts back into the old behaviour.
|
|
216
|
+
rotateOutputTarget(options.output, { overwrite: options.overwrite });
|
|
217
|
+
const payload = reportFmt === "json"
|
|
218
|
+
? JSON.stringify(maStructuredReport(result, structuredEndpoints), null, 2) + "\n"
|
|
219
|
+
: md;
|
|
220
|
+
await writeFile(options.output, payload, "utf-8");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let emittedSuites: Array<{ file: string; suite: string; tests: number }> = [];
|
|
224
|
+
if (options.emitTests) {
|
|
225
|
+
const suites = emitRegressionSuites(result, endpoints, securitySchemes);
|
|
226
|
+
const written = await writeProbeSuites({
|
|
227
|
+
output: options.emitTests,
|
|
228
|
+
suites,
|
|
229
|
+
command: "zond probe-mass-assignment --emit-tests",
|
|
230
|
+
headerExample: `zond probe-mass-assignment --api <name> --emit-tests ${options.emitTests}`,
|
|
231
|
+
});
|
|
232
|
+
emittedSuites = written.files;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const counts = tallyBySeverity(result.verdicts, MA_BUCKETS, MA_ZERO);
|
|
236
|
+
const orphans = countCleanupFailures(result.verdicts);
|
|
237
|
+
|
|
238
|
+
if (options.json) {
|
|
239
|
+
// m-17 / ARV-51: structured envelope; no `data.digest.stdout`.
|
|
240
|
+
printJson(
|
|
241
|
+
jsonOk("probe-mass-assignment", {
|
|
242
|
+
endpoints: structuredEndpoints,
|
|
243
|
+
summary: {
|
|
244
|
+
totalEndpoints: result.totalEndpoints,
|
|
245
|
+
probed: result.specProbed,
|
|
246
|
+
by_status: maByStatus(structuredEndpoints),
|
|
247
|
+
},
|
|
248
|
+
orphans,
|
|
249
|
+
warnings: result.warnings,
|
|
250
|
+
emittedTests: emittedSuites,
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
} else {
|
|
254
|
+
if (!options.output) {
|
|
255
|
+
if (reportFmt === "json") {
|
|
256
|
+
process.stdout.write(JSON.stringify(maStructuredReport(result, structuredEndpoints), null, 2) + "\n");
|
|
257
|
+
} else {
|
|
258
|
+
console.log(md);
|
|
259
|
+
}
|
|
260
|
+
} else printSuccess(`${reportFmt === "json" ? "Structured report" : "Digest"} written to ${options.output}`);
|
|
261
|
+
console.log("");
|
|
262
|
+
console.log(formatSummaryLine(counts, MA_SUMMARY));
|
|
263
|
+
if (emittedSuites.length > 0) {
|
|
264
|
+
printSuccess(`Emitted ${emittedSuites.length} regression suite(s) in ${options.emitTests}`);
|
|
265
|
+
console.log(` Run them on CI: zond run ${options.emitTests} --env ${options.env ?? ".env.yaml"}`);
|
|
266
|
+
} else if (options.emitTests) {
|
|
267
|
+
console.log(`No findings to emit. Directory ${options.emitTests} not created.`);
|
|
268
|
+
}
|
|
269
|
+
if (counts.high > 0) {
|
|
270
|
+
printWarning(`${counts.high} HIGH-severity finding(s) — privilege escalation candidates. Review the digest.`);
|
|
271
|
+
}
|
|
272
|
+
if (counts.inconclusiveBaseline > 0) {
|
|
273
|
+
printWarning(
|
|
274
|
+
`${counts.inconclusiveBaseline} endpoint(s) had baseline POST failures — fix env fixtures (FK ids / path-params) and re-run. These are excluded from --emit-tests on purpose.`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
// TASK-259: cleanup-failure surfaces as "orphans" in summary. 404 was
|
|
278
|
+
// already filtered out (resource gone is success). Prompt for manual
|
|
279
|
+
// cleanup so the user doesn't discover the leak only via 5xx in CI.
|
|
280
|
+
if (orphans > 0) {
|
|
281
|
+
printWarning(
|
|
282
|
+
`${orphans} orphan resource(s): cleanup DELETE failed (non-404). Manual cleanup may be needed — see digest "Cleanup DELETE: …" lines.`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
// Stale-fixture hint when probes successfully cleaned up at least one
|
|
286
|
+
// resource: that means we POSTed (and re-DELETEd) — `.env.yaml` slug/id
|
|
287
|
+
// values for that resource type may now point at a tombstone.
|
|
288
|
+
const cleanedCount = result.verdicts.filter(v => v.cleanup?.attempted && v.cleanup.status != null && v.cleanup.status < 400).length;
|
|
289
|
+
if (cleanedCount > 0) {
|
|
290
|
+
printWarning(
|
|
291
|
+
`${cleanedCount} resource(s) created and deleted by probes. FK fixtures in .env.yaml may be stale — re-run \`zond prepare-fixtures --api <name>\` before next CRUD run.`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Non-zero exit when HIGH findings — useful for CI gating.
|
|
297
|
+
return counts.high > 0 ? 1 : 0;
|
|
298
|
+
} catch (err) {
|
|
299
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
300
|
+
if (options.json) printJson(jsonError("probe-mass-assignment", [message]));
|
|
301
|
+
else printError(message);
|
|
302
|
+
return 2;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ──────────────────────────────────────────────
|
|
307
|
+
// TASK-146: --emit-template short-circuit
|
|
308
|
+
// ──────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
import { buildMassAssignmentTemplate } from "../../../core/probe/mass-assignment-template.ts";
|
|
311
|
+
|
|
312
|
+
export interface EmitTemplateCliOptions {
|
|
313
|
+
specPath: string;
|
|
314
|
+
/** "METHOD:/path", e.g. "POST:/users" or "POST /users". */
|
|
315
|
+
methodPath: string;
|
|
316
|
+
output?: string;
|
|
317
|
+
json?: boolean;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function emitMassAssignmentTemplateCommand(
|
|
321
|
+
options: EmitTemplateCliOptions,
|
|
322
|
+
): Promise<number> {
|
|
323
|
+
const parsed = parseMethodPath(options.methodPath);
|
|
324
|
+
if (!parsed) {
|
|
325
|
+
const msg = `--emit-template expects "METHOD:/path" (e.g. "POST:/users"), got: ${options.methodPath}`;
|
|
326
|
+
if (options.json) printJson(jsonError("probe-mass-assignment", [msg]));
|
|
327
|
+
else printError(msg);
|
|
328
|
+
return 2;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const result = await buildMassAssignmentTemplate({
|
|
333
|
+
specPath: options.specPath,
|
|
334
|
+
method: parsed.method,
|
|
335
|
+
path: parsed.path,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
if (result.kind === "endpoint-not-found") {
|
|
339
|
+
const lines = [`endpoint not found: ${parsed.method} ${parsed.path}`];
|
|
340
|
+
if (result.nearest.length > 0) {
|
|
341
|
+
lines.push(`nearest paths with method ${parsed.method}: ${result.nearest.join(", ")}`);
|
|
342
|
+
}
|
|
343
|
+
const msg = lines.join("\n");
|
|
344
|
+
if (options.json) printJson(jsonError("probe-mass-assignment", [msg]));
|
|
345
|
+
else printError(msg);
|
|
346
|
+
return 2;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (options.output) {
|
|
350
|
+
await mkdir(join(options.output, "..").replace(/[^/]+$/, ""), { recursive: true }).catch(() => {});
|
|
351
|
+
await writeFile(options.output, result.yaml, "utf-8");
|
|
352
|
+
if (options.json) {
|
|
353
|
+
printJson(
|
|
354
|
+
jsonOk("probe-mass-assignment", {
|
|
355
|
+
template: { file: options.output, chain: result.chain, protectedFields: result.protectedFields },
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
} else {
|
|
359
|
+
printSuccess(`Template written to ${options.output} (chain=${result.chain})`);
|
|
360
|
+
if (result.protectedFields.length > 0) {
|
|
361
|
+
console.log(` readOnly/x-zond-protected fields injected: ${result.protectedFields.join(", ")}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
if (options.json) {
|
|
366
|
+
printJson(
|
|
367
|
+
jsonOk("probe-mass-assignment", {
|
|
368
|
+
template: { yaml: result.yaml, chain: result.chain, protectedFields: result.protectedFields },
|
|
369
|
+
}),
|
|
370
|
+
);
|
|
371
|
+
} else {
|
|
372
|
+
process.stdout.write(result.yaml);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return 0;
|
|
376
|
+
} catch (err) {
|
|
377
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
378
|
+
if (options.json) printJson(jsonError("probe-mass-assignment", [message]));
|
|
379
|
+
else printError(message);
|
|
380
|
+
return 2;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function parseMethodPath(s: string): { method: string; path: string } | null {
|
|
385
|
+
const m = s.match(/^\s*([A-Za-z]+)\s*[: ]\s*(\/.*?)\s*$/);
|
|
386
|
+
if (!m) return null;
|
|
387
|
+
return { method: m[1]!.toUpperCase(), path: m[2]! };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// m-17 / ARV-51: structured per-endpoint shape for mass-assignment.
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* ARV-252: parse repeatable `--suspect-field name=value` flags into the
|
|
394
|
+
* extra-fields map. Values are kept as strings — generateFromSchema /
|
|
395
|
+
* sentinel inference happens server-side via the suspect-fields machinery.
|
|
396
|
+
* Malformed entries (no `=`) are skipped silently rather than failing the
|
|
397
|
+
* run — this keeps ad-hoc CLI usage forgiving.
|
|
398
|
+
*/
|
|
399
|
+
function parseSuspectFieldFlags(raw: string[] | undefined): Record<string, unknown> | undefined {
|
|
400
|
+
if (!raw || raw.length === 0) return undefined;
|
|
401
|
+
const out: Record<string, unknown> = {};
|
|
402
|
+
for (const entry of raw) {
|
|
403
|
+
const eq = entry.indexOf("=");
|
|
404
|
+
if (eq <= 0) continue;
|
|
405
|
+
const name = entry.slice(0, eq).trim();
|
|
406
|
+
const value = entry.slice(eq + 1);
|
|
407
|
+
if (name) out[name] = value;
|
|
408
|
+
}
|
|
409
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* ARV-252: filter verdicts for the digest/console display under the
|
|
414
|
+
* evidence-chain principle.
|
|
415
|
+
*
|
|
416
|
+
* - HIGH (applied) — always show; this is the actual finding.
|
|
417
|
+
* - inconclusive-baseline / inconclusive-5xx / ok / skipped — always
|
|
418
|
+
* show; operator needs them to triage probe coverage.
|
|
419
|
+
* - INFO with at least one `absent` outcome (couldn't verify via
|
|
420
|
+
* follow-up GET) — show only under --verbose. This is the "single
|
|
421
|
+
* signal, no proof" case.
|
|
422
|
+
* - INFO with only `ignored` outcomes (silently dropped — correct
|
|
423
|
+
* framework behaviour) — NEVER show. Reports must not noise-floor
|
|
424
|
+
* on intentional behaviour.
|
|
425
|
+
*
|
|
426
|
+
* JSON envelope is unfiltered; this is a display-layer transform only.
|
|
427
|
+
*/
|
|
428
|
+
function filterVerdictsForDisplay(
|
|
429
|
+
verdicts: EndpointVerdict[],
|
|
430
|
+
opts: { verbose: boolean },
|
|
431
|
+
): EndpointVerdict[] {
|
|
432
|
+
return verdicts.filter((v) => {
|
|
433
|
+
if (v.severity !== "info") return true;
|
|
434
|
+
const hasAbsent = v.fields.some((f) => f.outcome === "absent");
|
|
435
|
+
if (!hasAbsent) return false; // silently-ignored: always hidden
|
|
436
|
+
return opts.verbose;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function maStatusFromSeverity(s: EndpointVerdict["severity"]): ProbeEndpointStatus {
|
|
441
|
+
switch (s) {
|
|
442
|
+
case "high": return "high";
|
|
443
|
+
case "low":
|
|
444
|
+
case "medium":
|
|
445
|
+
case "info":
|
|
446
|
+
return "low";
|
|
447
|
+
case "ok": return "ok";
|
|
448
|
+
case "skipped": return "skipped";
|
|
449
|
+
case "inconclusive-baseline":
|
|
450
|
+
case "inconclusive-5xx":
|
|
451
|
+
return "inconclusive";
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function maFindingSeverity(s: EndpointVerdict["severity"]): ProbeFindingSeverity {
|
|
456
|
+
if (s === "high") return "high";
|
|
457
|
+
if (s === "low" || s === "medium") return "low";
|
|
458
|
+
if (s === "ok") return "ok";
|
|
459
|
+
return "inconclusive";
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function buildMaStructuredEndpoints(result: MassAssignmentResult): ProbeEndpointResult[] {
|
|
463
|
+
return result.verdicts.map((v) => ({
|
|
464
|
+
path: v.path,
|
|
465
|
+
method: v.method,
|
|
466
|
+
classes_run: ["mass-assignment"],
|
|
467
|
+
findings: v.severity === "skipped" || v.severity === "ok"
|
|
468
|
+
? []
|
|
469
|
+
: [{
|
|
470
|
+
class: "mass-assignment",
|
|
471
|
+
severity: maFindingSeverity(v.severity),
|
|
472
|
+
evidence: {
|
|
473
|
+
summary: v.summary,
|
|
474
|
+
request: { url: v.request.url, injectedFields: v.request.injectedFields },
|
|
475
|
+
...(v.response ? { response: { status: v.response.status } } : {}),
|
|
476
|
+
...(v.fields ? { fields: v.fields } : {}),
|
|
477
|
+
...(v.recommended_action ? { recommended_action: v.recommended_action } : {}),
|
|
478
|
+
},
|
|
479
|
+
}],
|
|
480
|
+
status: maStatusFromSeverity(v.severity),
|
|
481
|
+
...(v.severity === "skipped" ? { skip_reason: v.skipReason ?? v.summary } : {}),
|
|
482
|
+
}));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function maByStatus(endpoints: ProbeEndpointResult[]): Record<ProbeEndpointStatus, number> {
|
|
486
|
+
const out: Record<ProbeEndpointStatus, number> = {
|
|
487
|
+
ok: 0, high: 0, low: 0, inconclusive: 0, skipped: 0,
|
|
488
|
+
};
|
|
489
|
+
for (const e of endpoints) out[e.status]++;
|
|
490
|
+
return out;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function maStructuredReport(result: MassAssignmentResult, endpoints: ProbeEndpointResult[]): object {
|
|
494
|
+
return {
|
|
495
|
+
endpoints,
|
|
496
|
+
summary: {
|
|
497
|
+
totalEndpoints: result.totalEndpoints,
|
|
498
|
+
probed: result.specProbed,
|
|
499
|
+
by_status: maByStatus(endpoints),
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|