@kirrosh/zond 0.22.0 → 0.26.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 +811 -0
- package/README.md +59 -6
- package/package.json +9 -7
- package/src/CLAUDE.md +112 -0
- package/src/cli/argv.ts +122 -0
- package/src/cli/commands/add-api.ts +146 -0
- package/src/cli/commands/api/annotate/idempotency.ts +59 -0
- package/src/cli/commands/api/annotate/index.ts +880 -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 +64 -0
- package/src/cli/commands/api/annotate/prompts.ts +220 -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 +786 -0
- package/src/cli/commands/catalog.ts +35 -0
- package/src/cli/commands/check.ts +361 -0
- package/src/cli/commands/checks.ts +1072 -0
- package/src/cli/commands/ci-init.ts +43 -0
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +236 -0
- package/src/cli/commands/completions.ts +16 -0
- package/src/cli/commands/coverage.ts +823 -132
- package/src/cli/commands/db.ts +486 -12
- package/src/cli/commands/describe.ts +37 -2
- package/src/cli/commands/discover.ts +1356 -0
- package/src/cli/commands/doctor.ts +661 -0
- package/src/cli/commands/fixtures.ts +402 -0
- package/src/cli/commands/generate.ts +438 -47
- package/src/cli/commands/init/bootstrap.ts +34 -2
- package/src/cli/commands/{init.ts → init/index.ts} +99 -5
- package/src/cli/commands/init/skills.ts +99 -3
- package/src/cli/commands/init/templates/agents.md +77 -64
- package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +621 -0
- package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +272 -0
- package/src/cli/commands/init/templates/skills/zond.md +802 -125
- package/src/cli/commands/init/templates/zond-config.yml +8 -9
- package/src/cli/commands/prepare-fixtures.ts +97 -0
- package/src/cli/commands/probe/_seed-bodies.ts +52 -0
- package/src/cli/commands/probe/mass-assignment.ts +594 -0
- package/src/cli/commands/probe/security.ts +537 -0
- package/src/cli/commands/probe/static.ts +255 -0
- package/src/cli/commands/probe/webhooks.ts +163 -0
- package/src/cli/commands/probe.ts +535 -0
- package/src/cli/commands/reference.ts +87 -0
- package/src/cli/commands/refresh-api.ts +227 -0
- package/src/cli/commands/remove-api.ts +150 -0
- package/src/cli/commands/report-bundle.ts +310 -0
- package/src/cli/commands/report.ts +241 -0
- package/src/cli/commands/request.ts +495 -4
- package/src/cli/commands/run.ts +870 -53
- package/src/cli/commands/schema-from-runs.ts +128 -0
- package/src/cli/commands/secrets.ts +133 -0
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +18 -1
- package/src/cli/index.ts +20 -3
- package/src/cli/json-envelope.ts +92 -3
- package/src/cli/json-schemas.ts +314 -0
- package/src/cli/output.ts +17 -1
- package/src/cli/program.ts +199 -635
- package/src/cli/resolve.ts +105 -0
- package/src/cli/safe-live.ts +24 -0
- package/src/cli/status-filter.ts +114 -0
- package/src/cli/util/api-context.ts +85 -0
- package/src/cli/version.ts +5 -0
- package/src/core/audit/persist.ts +183 -0
- package/src/core/checks/budget.ts +59 -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 +147 -0
- package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
- package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
- package/src/core/checks/checks/idempotency_replay.ts +242 -0
- package/src/core/checks/checks/ignored_auth.ts +254 -0
- package/src/core/checks/checks/index.ts +68 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +416 -0
- package/src/core/checks/checks/missing_required_header.ts +40 -0
- package/src/core/checks/checks/negative_data_rejection.ts +148 -0
- package/src/core/checks/checks/not_a_server_error.ts +35 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +160 -0
- package/src/core/checks/checks/pagination_invariants.ts +419 -0
- package/src/core/checks/checks/positive_data_acceptance.ts +33 -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 +132 -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 +82 -0
- package/src/core/checks/recommended-action.ts +68 -0
- package/src/core/checks/registry.ts +78 -0
- package/src/core/checks/runner.ts +1461 -0
- package/src/core/checks/sarif.ts +230 -0
- package/src/core/checks/spec-findings.ts +308 -0
- package/src/core/checks/stateful.ts +121 -0
- package/src/core/checks/types.ts +305 -0
- package/src/core/checks/zond-extensions.ts +73 -0
- package/src/core/classifier/recommended-action.ts +251 -0
- package/src/core/context/current.ts +22 -6
- package/src/core/context/session.ts +78 -0
- package/src/core/coverage/loader.ts +216 -0
- package/src/core/coverage/reasons.ts +300 -0
- package/src/core/diagnostics/db-analysis.ts +293 -59
- package/src/core/diagnostics/failure-class.ts +140 -0
- package/src/core/diagnostics/failure-hints.ts +88 -89
- package/src/core/diagnostics/spec-pointer.ts +99 -0
- package/src/core/diagnostics/suggested-fixes.ts +155 -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 +38 -19
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/data-factory.ts +586 -22
- package/src/core/generator/describe.ts +1 -1
- package/src/core/generator/fixtures-builder.ts +332 -0
- package/src/core/generator/index.ts +5 -5
- package/src/core/generator/openapi-reader.ts +135 -7
- package/src/core/generator/path-param-disambig.ts +140 -0
- package/src/core/generator/resources-builder.ts +898 -0
- package/src/core/generator/schema-utils.ts +33 -3
- package/src/core/generator/serializer.ts +103 -13
- package/src/core/generator/suite-generator.ts +583 -122
- package/src/core/generator/types.ts +14 -0
- package/src/core/identity/identity-file.ts +0 -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 +73 -0
- package/src/core/output/index.ts +13 -0
- package/src/core/output/run.ts +91 -0
- package/src/core/output/types.ts +122 -0
- package/src/core/parser/dynamic-values.ts +160 -0
- package/src/core/parser/env-interpolation.ts +104 -0
- package/src/core/parser/filter.ts +57 -0
- package/src/core/parser/schema.ts +129 -4
- package/src/core/parser/types.ts +19 -1
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +58 -12
- package/src/core/probe/bootstrap.ts +34 -0
- package/src/core/probe/dry-run-envelope.ts +61 -0
- package/src/core/probe/mass-assignment/classify.ts +175 -0
- package/src/core/probe/mass-assignment/cleanup.ts +52 -0
- package/src/core/probe/mass-assignment/digest.ts +114 -0
- package/src/core/probe/mass-assignment/orchestrator.ts +459 -0
- package/src/core/probe/mass-assignment/regression.ts +141 -0
- package/src/core/probe/mass-assignment/suspects.ts +92 -0
- package/src/core/probe/mass-assignment/types.ts +135 -0
- package/src/core/probe/mass-assignment-probe-class.ts +198 -0
- package/src/core/probe/mass-assignment-probe.ts +27 -0
- package/src/core/probe/mass-assignment-template.ts +240 -0
- package/src/core/probe/method-probe.ts +43 -76
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +183 -149
- package/src/core/probe/orphan-tracker.ts +188 -0
- package/src/core/probe/path-discovery.ts +439 -0
- package/src/core/probe/probe-harness.ts +119 -0
- package/src/core/probe/registry.ts +89 -0
- package/src/core/probe/runner.ts +136 -0
- package/src/core/probe/security/baseline.ts +174 -0
- package/src/core/probe/security/classify.ts +341 -0
- package/src/core/probe/security/cleanup.ts +125 -0
- package/src/core/probe/security/detectors.ts +71 -0
- package/src/core/probe/security/digest.ts +104 -0
- package/src/core/probe/security/orchestrator.ts +398 -0
- package/src/core/probe/security/regression.ts +103 -0
- package/src/core/probe/security/types.ts +151 -0
- package/src/core/probe/security-probe-class.ts +207 -0
- package/src/core/probe/security-probe.ts +32 -0
- package/src/core/probe/shared.ts +531 -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 +282 -0
- package/src/core/reporter/console.ts +41 -2
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +11 -1
- package/src/core/reporter/junit.ts +27 -12
- package/src/core/reporter/ndjson.ts +37 -0
- package/src/core/reporter/types.ts +3 -0
- package/src/core/runner/assertions.ts +59 -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 +265 -36
- package/src/core/runner/form-encode.ts +41 -0
- package/src/core/runner/http-client.ts +112 -2
- package/src/core/runner/learn-drift.ts +293 -0
- package/src/core/runner/preflight-vars.ts +153 -0
- package/src/core/runner/progress-tracker.ts +73 -0
- package/src/core/runner/rate-limiter.ts +87 -33
- package/src/core/runner/run-kind.ts +45 -0
- package/src/core/runner/schema-validator.ts +308 -0
- package/src/core/runner/send-request.ts +158 -20
- package/src/core/runner/types.ts +44 -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 +457 -20
- package/src/core/severity/category.ts +94 -0
- package/src/core/severity/index.ts +58 -0
- package/src/core/spec/infer-schema.ts +102 -0
- package/src/core/spec/layers.ts +154 -0
- package/src/core/spec/merge-specs.ts +156 -0
- package/src/core/spec/schema-from-runs.ts +117 -0
- package/src/core/spec/schema-overlay.ts +130 -0
- package/src/core/util/ajv.ts +13 -0
- package/src/core/util/format-eta.ts +21 -0
- package/src/core/util/headers.ts +9 -0
- package/src/core/util/url.ts +24 -0
- package/src/core/utils.ts +5 -1
- package/src/core/workspace/config.ts +129 -0
- package/src/core/workspace/fixture-gap-report.ts +84 -0
- package/src/core/workspace/fixture-gaps.ts +71 -0
- package/src/core/workspace/manifest.ts +283 -0
- package/src/core/workspace/output-rotation.ts +62 -0
- package/src/core/workspace/root.ts +13 -11
- package/src/core/workspace/triage-path.ts +87 -0
- package/src/db/lint-runs.ts +47 -0
- package/src/db/migrate.ts +128 -0
- package/src/db/migrations/0001_run_kind.sql +25 -0
- package/src/db/migrations/0002_run_kind_request.sql +59 -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 +216 -0
- package/src/db/queries/runs.ts +289 -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 +75 -802
- package/src/db/schema.ts +178 -50
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
- package/src/cli/commands/probe-methods.ts +0 -108
- package/src/cli/commands/probe-validation.ts +0 -124
- package/src/cli/commands/serve.ts +0 -114
- package/src/cli/commands/sync.ts +0 -268
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/diagnostics/render-md.ts +0 -112
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -19
- package/src/core/parser/index.ts +0 -21
- package/src/core/runner/execute-run.ts +0 -132
- package/src/core/runner/index.ts +0 -12
- package/src/core/sync/spec-differ.ts +0 -38
- package/src/web/data/collection-state.ts +0 -362
- package/src/web/routes/api.ts +0 -314
- package/src/web/routes/dashboard.ts +0 -350
- package/src/web/routes/runs.ts +0 -64
- package/src/web/schemas.ts +0 -121
- package/src/web/server.ts +0 -134
- package/src/web/static/htmx.min.cjs +0 -1
- package/src/web/static/style.css +0 -1148
- package/src/web/views/endpoints-tab.ts +0 -174
- package/src/web/views/explorer-tab.ts +0 -402
- package/src/web/views/health-strip.ts +0 -92
- package/src/web/views/layout.ts +0 -48
- package/src/web/views/results.ts +0 -210
- package/src/web/views/runs-tab.ts +0 -126
- package/src/web/views/suites-tab.ts +0 -181
|
@@ -0,0 +1,594 @@
|
|
|
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 { loadSeedBodyOverlays } from "./_seed-bodies.ts";
|
|
21
|
+
import type { EndpointVerdict, MassAssignmentResult } from "../../../core/probe/mass-assignment-probe.ts";
|
|
22
|
+
import type { ProbeEndpointResult, ProbeEndpointStatus, ProbeFindingSeverity } from "../../../core/probe/types.ts";
|
|
23
|
+
|
|
24
|
+
interface BucketCounts {
|
|
25
|
+
high: number;
|
|
26
|
+
inconclusiveBaseline: number;
|
|
27
|
+
inconclusive5xx: number;
|
|
28
|
+
medium: number;
|
|
29
|
+
low: number;
|
|
30
|
+
ok: number;
|
|
31
|
+
skipped: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const MA_BUCKETS: ReadonlyArray<readonly [string, keyof BucketCounts & string]> = [
|
|
35
|
+
["high", "high"],
|
|
36
|
+
["inconclusive-baseline", "inconclusiveBaseline"],
|
|
37
|
+
["inconclusive-5xx", "inconclusive5xx"],
|
|
38
|
+
["medium", "medium"],
|
|
39
|
+
["low", "low"],
|
|
40
|
+
["ok", "ok"],
|
|
41
|
+
["skipped", "skipped"],
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const MA_SUMMARY: ReadonlyArray<readonly [string, keyof BucketCounts & string]> = [
|
|
45
|
+
["HIGH", "high"],
|
|
46
|
+
["INCONCLUSIVE", "inconclusiveBaseline"],
|
|
47
|
+
["INCONCLUSIVE-5XX", "inconclusive5xx"],
|
|
48
|
+
["MED", "medium"],
|
|
49
|
+
["LOW", "low"],
|
|
50
|
+
["OK", "ok"],
|
|
51
|
+
["SKIPPED", "skipped"],
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const MA_ZERO: BucketCounts = {
|
|
55
|
+
high: 0, inconclusiveBaseline: 0, inconclusive5xx: 0, medium: 0, low: 0, ok: 0, skipped: 0,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export interface ProbeMassAssignmentOptions {
|
|
59
|
+
specPath: string;
|
|
60
|
+
env?: string;
|
|
61
|
+
/** Markdown digest output file. If omitted — print to stdout. */
|
|
62
|
+
output?: string;
|
|
63
|
+
/** Emit regression YAML suites into this directory. */
|
|
64
|
+
emitTests?: string;
|
|
65
|
+
tag?: string;
|
|
66
|
+
noCleanup?: boolean;
|
|
67
|
+
noDiscover?: boolean;
|
|
68
|
+
timeoutMs?: number;
|
|
69
|
+
json?: boolean;
|
|
70
|
+
listTags?: boolean;
|
|
71
|
+
overwrite?: boolean;
|
|
72
|
+
/** m-17 / ARV-52: list which endpoints + fields would be attacked
|
|
73
|
+
* without sending live traffic. */
|
|
74
|
+
dryRun?: boolean;
|
|
75
|
+
/** m-17 / ARV-52: m-15 ARV-9 selector grammar (`path:`/`method:`/`tag:`/`operation-id:`). */
|
|
76
|
+
include?: string[];
|
|
77
|
+
exclude?: string[];
|
|
78
|
+
/** m-17 / ARV-51: format for --output / non-json stdout. */
|
|
79
|
+
report?: "markdown" | "json";
|
|
80
|
+
/** ARV-252: surface INFO-severity inconclusive verdicts (absent-but-
|
|
81
|
+
* unverifiable). Silently-ignored verdicts are never shown — they
|
|
82
|
+
* represent correct framework behaviour. */
|
|
83
|
+
verbose?: boolean;
|
|
84
|
+
/** ARV-252: additional suspect fields to inject, in `name=value`
|
|
85
|
+
* form. Extends the curated SUSPECTED_FIELDS list per-run. Full
|
|
86
|
+
* spec-extension support (x-zond-suspect-fields) is tracked in
|
|
87
|
+
* ARV-189. */
|
|
88
|
+
suspectField?: string[];
|
|
89
|
+
/** ARV-265: collection name for audit-coverage attribution. Set by
|
|
90
|
+
* the CLI when --api / current-api resolves; left undefined for
|
|
91
|
+
* bare-spec invocations. */
|
|
92
|
+
apiName?: string;
|
|
93
|
+
/** ARV-302: cap the number of endpoints probed in this run (after
|
|
94
|
+
* --include / --exclude / --tag filters). Used by `zond audit
|
|
95
|
+
* --budget` so probe stages stay inside a wall-clock budget instead
|
|
96
|
+
* of unbounded scanning (Stripe: 587 endpoints, 3.6k probes — silent
|
|
97
|
+
* 10+ min). When the cap trims the set, a warning is emitted so the
|
|
98
|
+
* user knows the run was partial. */
|
|
99
|
+
maxEndpoints?: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function probeMassAssignmentCommand(
|
|
103
|
+
options: ProbeMassAssignmentOptions,
|
|
104
|
+
): Promise<number> {
|
|
105
|
+
try {
|
|
106
|
+
const loaded = await loadSpecForProbe({
|
|
107
|
+
specPath: options.specPath,
|
|
108
|
+
tag: options.tag,
|
|
109
|
+
listTags: options.listTags,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (loaded.kind === "tags") {
|
|
113
|
+
if (options.json) {
|
|
114
|
+
printJson(jsonOk("probe-mass-assignment", { tags: loaded.tags }));
|
|
115
|
+
} else if (loaded.tags.length === 0) {
|
|
116
|
+
console.log("No tags found in spec.");
|
|
117
|
+
} else {
|
|
118
|
+
console.log("Available tags:");
|
|
119
|
+
for (const t of loaded.tags) console.log(` - ${t}`);
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
if (loaded.kind === "tag-not-found") {
|
|
124
|
+
const msg = `No endpoints tagged "${loaded.tag}". Available tags: ${loaded.available.length ? loaded.available.join(", ") : "(none)"}`;
|
|
125
|
+
if (options.json) printJson(jsonError("probe-mass-assignment", [msg]));
|
|
126
|
+
else printWarning(msg);
|
|
127
|
+
return 2;
|
|
128
|
+
}
|
|
129
|
+
const { endpoints: rawEndpoints, securitySchemes } = loaded;
|
|
130
|
+
|
|
131
|
+
// m-17 / ARV-52: apply --include / --exclude through the unified
|
|
132
|
+
// operation filter (m-15 ARV-9). probe-family was deferred at AC#6;
|
|
133
|
+
// wiring it here closes that and gives mass-assignment parity with
|
|
134
|
+
// probe-static / probe-security.
|
|
135
|
+
let endpoints = rawEndpoints;
|
|
136
|
+
if (options.include?.length || options.exclude?.length) {
|
|
137
|
+
const compiled = compileOperationFilter({ includes: options.include, excludes: options.exclude });
|
|
138
|
+
if (compiled.errors.length > 0) {
|
|
139
|
+
const message = compiled.errors.join("\n");
|
|
140
|
+
if (options.json) printJson(jsonError("probe-mass-assignment", [message]));
|
|
141
|
+
else printError(message);
|
|
142
|
+
return 2;
|
|
143
|
+
}
|
|
144
|
+
endpoints = endpoints.filter(compiled.filter);
|
|
145
|
+
}
|
|
146
|
+
// ARV-302: --max-endpoints cap — applied after include/exclude/tag
|
|
147
|
+
// filters so the user-visible filter intent is preserved. The cap
|
|
148
|
+
// is a coarse time-budget knob (`zond audit --budget standard` →
|
|
149
|
+
// 50 endpoints), not a sampling strategy: take the first N from
|
|
150
|
+
// the filtered list. A warning fires so partial output is obvious.
|
|
151
|
+
let cappedEndpoints = false;
|
|
152
|
+
if (typeof options.maxEndpoints === "number" && options.maxEndpoints > 0
|
|
153
|
+
&& endpoints.length > options.maxEndpoints) {
|
|
154
|
+
const total = endpoints.length;
|
|
155
|
+
endpoints = endpoints.slice(0, options.maxEndpoints);
|
|
156
|
+
cappedEndpoints = true;
|
|
157
|
+
if (!options.json) {
|
|
158
|
+
process.stderr.write(
|
|
159
|
+
`zond: probe mass-assignment capped at ${options.maxEndpoints}/${total} endpoints (--max-endpoints) — pass --max-endpoints <N> or --budget full to widen.\n`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Load env vars (base_url, auth_token, api_key, path-param overrides).
|
|
165
|
+
let vars: Record<string, string> = {};
|
|
166
|
+
if (options.env) {
|
|
167
|
+
const fromFile = await loadEnvFile(options.env);
|
|
168
|
+
if (!fromFile) {
|
|
169
|
+
const msg = `Environment file not found: ${options.env}`;
|
|
170
|
+
if (options.json) printJson(jsonError("probe-mass-assignment", [msg]));
|
|
171
|
+
else printError(msg);
|
|
172
|
+
return 2;
|
|
173
|
+
}
|
|
174
|
+
vars = fromFile;
|
|
175
|
+
} else {
|
|
176
|
+
vars = await loadEnvironment();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// m-17 / ARV-52: --dry-run lists which endpoints + suspect fields the
|
|
180
|
+
// probe would touch without sending live traffic. base_url is not
|
|
181
|
+
// required on this path (mirrors probe-security).
|
|
182
|
+
if (options.dryRun) {
|
|
183
|
+
const probe = new MassAssignmentProbe();
|
|
184
|
+
const plan = await probe.dryRun({
|
|
185
|
+
specPath: options.specPath,
|
|
186
|
+
endpoints,
|
|
187
|
+
securitySchemes,
|
|
188
|
+
vars,
|
|
189
|
+
options: {},
|
|
190
|
+
});
|
|
191
|
+
const data = summarizeDryRun(plan);
|
|
192
|
+
// ARV-317: persist the planned inventory when --output is given, so it
|
|
193
|
+
// survives beyond stdout (an agent can read it back). --emit-tests is
|
|
194
|
+
// skipped on dry-run — there are no findings to turn into regression
|
|
195
|
+
// suites; the plan digest is the dry-run deliverable.
|
|
196
|
+
// ARV-321: say so explicitly. A silent no-op read as a bug on the live
|
|
197
|
+
// Stripe run (report-zond friction, 2026-07-02) — the target dir stayed
|
|
198
|
+
// empty with zero signal that --emit-tests was ignored.
|
|
199
|
+
if (options.emitTests) {
|
|
200
|
+
printWarning(`--emit-tests skipped: safe/dry-run mode has no live verdicts to lock in as regression suites. Re-run with --live to emit ${options.emitTests}.`);
|
|
201
|
+
}
|
|
202
|
+
if (options.output) {
|
|
203
|
+
const payload = options.report === "json"
|
|
204
|
+
? JSON.stringify(data, null, 2)
|
|
205
|
+
: formatDryRunDigest(plan);
|
|
206
|
+
await mkdir(join(options.output, "..").replace(/\/\.$/, ""), { recursive: true }).catch(() => {});
|
|
207
|
+
rotateOutputTarget(options.output, { overwrite: options.overwrite });
|
|
208
|
+
await writeFile(options.output, payload + "\n", "utf-8");
|
|
209
|
+
}
|
|
210
|
+
if (options.json) {
|
|
211
|
+
printJson(jsonOk("probe-mass-assignment", data));
|
|
212
|
+
} else {
|
|
213
|
+
console.log(formatDryRunDigest(plan));
|
|
214
|
+
}
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!vars["base_url"]) {
|
|
219
|
+
const msg = "base_url is required (set in .env.yaml or via --env file). Probing requires a live API.";
|
|
220
|
+
if (options.json) printJson(jsonError("probe-mass-assignment", [msg]));
|
|
221
|
+
else printError(msg);
|
|
222
|
+
return 2;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// TASK-259: tell the user *before* we mutate anything. Suppressed in
|
|
226
|
+
// --json mode (warnings already in envelope) and when --no-cleanup
|
|
227
|
+
// is off — this banner is about the cleanup-pass, too.
|
|
228
|
+
printMutationBanner("probe-mass-assignment", vars, { quiet: options.json === true });
|
|
229
|
+
|
|
230
|
+
// ARV-265: capture HTTP touches for audit-coverage. Dry-run never
|
|
231
|
+
// reaches this branch (early return above), so AC#5 holds.
|
|
232
|
+
const { withHttpAudit, beginAuditRun, finalizeAuditRun, auditRecordToCase, checksPersistEnabled } =
|
|
233
|
+
await import("../../../core/audit/persist.ts");
|
|
234
|
+
const auditEnabled = checksPersistEnabled();
|
|
235
|
+
// ARV-269: lift agent-authored `seed_body` overlays out of
|
|
236
|
+
// `.api-resources.local.yaml` so create-endpoint baselines win against
|
|
237
|
+
// strict validators. No-op when --api isn't set (raw --spec invocation).
|
|
238
|
+
const seedBodies = await loadSeedBodyOverlays(options.apiName);
|
|
239
|
+
const { value: result, records: auditRecords } = await withHttpAudit(async () =>
|
|
240
|
+
runMassAssignmentProbes({
|
|
241
|
+
endpoints,
|
|
242
|
+
securitySchemes,
|
|
243
|
+
vars,
|
|
244
|
+
noCleanup: options.noCleanup,
|
|
245
|
+
timeoutMs: options.timeoutMs,
|
|
246
|
+
discover: !options.noDiscover,
|
|
247
|
+
extraSuspectFields: parseSuspectFieldFlags(options.suspectField),
|
|
248
|
+
seedBodies,
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
if (auditEnabled && auditRecords.length > 0) {
|
|
253
|
+
try {
|
|
254
|
+
const { getDb } = await import("../../../db/schema.ts");
|
|
255
|
+
const { findCollectionByNameOrId } = await import("../../../db/queries.ts");
|
|
256
|
+
const { readCurrentSession } = await import("../../../core/context/session.ts");
|
|
257
|
+
getDb();
|
|
258
|
+
const collectionId = options.apiName ? findCollectionByNameOrId(options.apiName)?.id : undefined;
|
|
259
|
+
const session = readCurrentSession();
|
|
260
|
+
const runId = beginAuditRun({
|
|
261
|
+
runKind: "probe",
|
|
262
|
+
...(collectionId != null ? { collectionId } : {}),
|
|
263
|
+
...(session?.id ? { sessionId: session.id } : {}),
|
|
264
|
+
tags: ["probe", "mass-assignment"],
|
|
265
|
+
});
|
|
266
|
+
const suiteFile = `apis/${options.apiName ?? "_"}/probes/mass-assignment.yaml`;
|
|
267
|
+
finalizeAuditRun(runId, auditRecords.map((rec) =>
|
|
268
|
+
auditRecordToCase(rec, {
|
|
269
|
+
suiteName: "probe/mass-assignment",
|
|
270
|
+
suiteFile,
|
|
271
|
+
testName: `mass-assignment::${rec.request.method.toUpperCase()} ${rec.request.url}`,
|
|
272
|
+
}),
|
|
273
|
+
));
|
|
274
|
+
} catch (err) {
|
|
275
|
+
process.stderr.write(`zond: audit persistence failed (${(err as Error).message}).\n`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ARV-252: filter verdicts for display under the evidence-chain
|
|
280
|
+
// principle. Silently-ignored (correct framework behaviour) never
|
|
281
|
+
// surfaces; absent-but-unverifiable surfaces only under --verbose.
|
|
282
|
+
// HIGH and inconclusive-baseline/5xx always show. JSON envelope
|
|
283
|
+
// always carries the full unfiltered list (agents triage explicitly).
|
|
284
|
+
const displayResult: MassAssignmentResult = {
|
|
285
|
+
...result,
|
|
286
|
+
verdicts: filterVerdictsForDisplay(result.verdicts, { verbose: options.verbose === true }),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// TASK-168 (m-10): vars came from .env.yaml — register them so any
|
|
290
|
+
// echoed token (URL, body, header) gets redacted in the digest.
|
|
291
|
+
getSecretRegistry().registerAll(vars);
|
|
292
|
+
const md = applySanitizer(formatDigestMarkdown(displayResult, options.specPath));
|
|
293
|
+
|
|
294
|
+
// m-17 / ARV-51: --output writes whichever format `--report` selected
|
|
295
|
+
// (default markdown). `--json` envelope is always structured.
|
|
296
|
+
const reportFmt: "markdown" | "json" = options.report ?? "markdown";
|
|
297
|
+
const structuredEndpoints = buildMaStructuredEndpoints(result);
|
|
298
|
+
if (options.output) {
|
|
299
|
+
await mkdir(join(options.output, "..").replace(/\/\.$/, ""), { recursive: true }).catch(() => {});
|
|
300
|
+
// TASK-162 (m-9 P6): rotate previous digest to <stem>-vN.md instead
|
|
301
|
+
// of silent overwrite. --overwrite opts back into the old behaviour.
|
|
302
|
+
rotateOutputTarget(options.output, { overwrite: options.overwrite });
|
|
303
|
+
const payload = reportFmt === "json"
|
|
304
|
+
? JSON.stringify(maStructuredReport(result, structuredEndpoints), null, 2) + "\n"
|
|
305
|
+
: md;
|
|
306
|
+
await writeFile(options.output, payload, "utf-8");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let emittedSuites: Array<{ file: string; suite: string; tests: number }> = [];
|
|
310
|
+
if (options.emitTests) {
|
|
311
|
+
const suites = emitRegressionSuites(result, endpoints, securitySchemes);
|
|
312
|
+
const written = await writeProbeSuites({
|
|
313
|
+
output: options.emitTests,
|
|
314
|
+
suites,
|
|
315
|
+
command: "zond probe-mass-assignment --emit-tests",
|
|
316
|
+
headerExample: `zond probe-mass-assignment --api <name> --emit-tests ${options.emitTests}`,
|
|
317
|
+
});
|
|
318
|
+
emittedSuites = written.files;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const counts = tallyBySeverity(result.verdicts, MA_BUCKETS, MA_ZERO);
|
|
322
|
+
const orphans = countCleanupFailures(result.verdicts);
|
|
323
|
+
|
|
324
|
+
if (options.json) {
|
|
325
|
+
// m-17 / ARV-51: structured envelope; no `data.digest.stdout`.
|
|
326
|
+
printJson(
|
|
327
|
+
jsonOk("probe-mass-assignment", {
|
|
328
|
+
endpoints: structuredEndpoints,
|
|
329
|
+
summary: {
|
|
330
|
+
totalEndpoints: result.totalEndpoints,
|
|
331
|
+
probed: result.specProbed,
|
|
332
|
+
by_status: maByStatus(structuredEndpoints),
|
|
333
|
+
},
|
|
334
|
+
orphans,
|
|
335
|
+
warnings: cappedEndpoints
|
|
336
|
+
? [
|
|
337
|
+
...result.warnings,
|
|
338
|
+
`--max-endpoints capped this run at ${options.maxEndpoints}; pass --max-endpoints <N> or --budget full to widen.`,
|
|
339
|
+
]
|
|
340
|
+
: result.warnings,
|
|
341
|
+
emittedTests: emittedSuites,
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
} else {
|
|
345
|
+
if (!options.output) {
|
|
346
|
+
if (reportFmt === "json") {
|
|
347
|
+
process.stdout.write(JSON.stringify(maStructuredReport(result, structuredEndpoints), null, 2) + "\n");
|
|
348
|
+
} else {
|
|
349
|
+
console.log(md);
|
|
350
|
+
}
|
|
351
|
+
} else printSuccess(`${reportFmt === "json" ? "Structured report" : "Digest"} written to ${options.output}`);
|
|
352
|
+
console.log("");
|
|
353
|
+
console.log(formatSummaryLine(counts, MA_SUMMARY));
|
|
354
|
+
if (emittedSuites.length > 0) {
|
|
355
|
+
printSuccess(`Emitted ${emittedSuites.length} regression suite(s) in ${options.emitTests}`);
|
|
356
|
+
console.log(` Run them on CI: zond run ${options.emitTests} --env ${options.env ?? ".env.yaml"}`);
|
|
357
|
+
} else if (options.emitTests) {
|
|
358
|
+
console.log(`No findings to emit. Directory ${options.emitTests} not created.`);
|
|
359
|
+
}
|
|
360
|
+
if (counts.high > 0) {
|
|
361
|
+
printWarning(`${counts.high} HIGH-severity finding(s) — privilege escalation candidates. Review the digest.`);
|
|
362
|
+
}
|
|
363
|
+
if (counts.inconclusiveBaseline > 0) {
|
|
364
|
+
printWarning(
|
|
365
|
+
`${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.`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
// TASK-259: cleanup-failure surfaces as "orphans" in summary. 404 was
|
|
369
|
+
// already filtered out (resource gone is success). Prompt for manual
|
|
370
|
+
// cleanup so the user doesn't discover the leak only via 5xx in CI.
|
|
371
|
+
if (orphans > 0) {
|
|
372
|
+
printWarning(
|
|
373
|
+
`${orphans} orphan resource(s): cleanup DELETE failed (non-404). Manual cleanup may be needed — see digest "Cleanup DELETE: …" lines.`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
// Stale-fixture hint when probes successfully cleaned up at least one
|
|
377
|
+
// resource: that means we POSTed (and re-DELETEd) — `.env.yaml` slug/id
|
|
378
|
+
// values for that resource type may now point at a tombstone.
|
|
379
|
+
const cleanedCount = result.verdicts.filter(v => v.cleanup?.attempted && v.cleanup.status != null && v.cleanup.status < 400).length;
|
|
380
|
+
if (cleanedCount > 0) {
|
|
381
|
+
printWarning(
|
|
382
|
+
`${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.`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Non-zero exit when HIGH findings — useful for CI gating.
|
|
388
|
+
return counts.high > 0 ? 1 : 0;
|
|
389
|
+
} catch (err) {
|
|
390
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
391
|
+
if (options.json) printJson(jsonError("probe-mass-assignment", [message]));
|
|
392
|
+
else printError(message);
|
|
393
|
+
return 2;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ──────────────────────────────────────────────
|
|
398
|
+
// TASK-146: --emit-template short-circuit
|
|
399
|
+
// ──────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
import { buildMassAssignmentTemplate } from "../../../core/probe/mass-assignment-template.ts";
|
|
402
|
+
|
|
403
|
+
export interface EmitTemplateCliOptions {
|
|
404
|
+
specPath: string;
|
|
405
|
+
/** "METHOD:/path", e.g. "POST:/users" or "POST /users". */
|
|
406
|
+
methodPath: string;
|
|
407
|
+
output?: string;
|
|
408
|
+
json?: boolean;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export async function emitMassAssignmentTemplateCommand(
|
|
412
|
+
options: EmitTemplateCliOptions,
|
|
413
|
+
): Promise<number> {
|
|
414
|
+
const parsed = parseMethodPath(options.methodPath);
|
|
415
|
+
if (!parsed) {
|
|
416
|
+
const msg = `--emit-template expects "METHOD:/path" (e.g. "POST:/users"), got: ${options.methodPath}`;
|
|
417
|
+
if (options.json) printJson(jsonError("probe-mass-assignment", [msg]));
|
|
418
|
+
else printError(msg);
|
|
419
|
+
return 2;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
const result = await buildMassAssignmentTemplate({
|
|
424
|
+
specPath: options.specPath,
|
|
425
|
+
method: parsed.method,
|
|
426
|
+
path: parsed.path,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
if (result.kind === "endpoint-not-found") {
|
|
430
|
+
const lines = [`endpoint not found: ${parsed.method} ${parsed.path}`];
|
|
431
|
+
if (result.nearest.length > 0) {
|
|
432
|
+
lines.push(`nearest paths with method ${parsed.method}: ${result.nearest.join(", ")}`);
|
|
433
|
+
}
|
|
434
|
+
const msg = lines.join("\n");
|
|
435
|
+
if (options.json) printJson(jsonError("probe-mass-assignment", [msg]));
|
|
436
|
+
else printError(msg);
|
|
437
|
+
return 2;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (options.output) {
|
|
441
|
+
await mkdir(join(options.output, "..").replace(/[^/]+$/, ""), { recursive: true }).catch(() => {});
|
|
442
|
+
await writeFile(options.output, result.yaml, "utf-8");
|
|
443
|
+
if (options.json) {
|
|
444
|
+
printJson(
|
|
445
|
+
jsonOk("probe-mass-assignment", {
|
|
446
|
+
template: { file: options.output, chain: result.chain, protectedFields: result.protectedFields },
|
|
447
|
+
}),
|
|
448
|
+
);
|
|
449
|
+
} else {
|
|
450
|
+
printSuccess(`Template written to ${options.output} (chain=${result.chain})`);
|
|
451
|
+
if (result.protectedFields.length > 0) {
|
|
452
|
+
console.log(` readOnly/x-zond-protected fields injected: ${result.protectedFields.join(", ")}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
if (options.json) {
|
|
457
|
+
printJson(
|
|
458
|
+
jsonOk("probe-mass-assignment", {
|
|
459
|
+
template: { yaml: result.yaml, chain: result.chain, protectedFields: result.protectedFields },
|
|
460
|
+
}),
|
|
461
|
+
);
|
|
462
|
+
} else {
|
|
463
|
+
process.stdout.write(result.yaml);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return 0;
|
|
467
|
+
} catch (err) {
|
|
468
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
469
|
+
if (options.json) printJson(jsonError("probe-mass-assignment", [message]));
|
|
470
|
+
else printError(message);
|
|
471
|
+
return 2;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function parseMethodPath(s: string): { method: string; path: string } | null {
|
|
476
|
+
const m = s.match(/^\s*([A-Za-z]+)\s*[: ]\s*(\/.*?)\s*$/);
|
|
477
|
+
if (!m) return null;
|
|
478
|
+
return { method: m[1]!.toUpperCase(), path: m[2]! };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// m-17 / ARV-51: structured per-endpoint shape for mass-assignment.
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* ARV-252: parse repeatable `--suspect-field name=value` flags into the
|
|
485
|
+
* extra-fields map. Values are kept as strings — generateFromSchema /
|
|
486
|
+
* sentinel inference happens server-side via the suspect-fields machinery.
|
|
487
|
+
* Malformed entries (no `=`) are skipped silently rather than failing the
|
|
488
|
+
* run — this keeps ad-hoc CLI usage forgiving.
|
|
489
|
+
*/
|
|
490
|
+
function parseSuspectFieldFlags(raw: string[] | undefined): Record<string, unknown> | undefined {
|
|
491
|
+
if (!raw || raw.length === 0) return undefined;
|
|
492
|
+
const out: Record<string, unknown> = {};
|
|
493
|
+
for (const entry of raw) {
|
|
494
|
+
const eq = entry.indexOf("=");
|
|
495
|
+
if (eq <= 0) continue;
|
|
496
|
+
const name = entry.slice(0, eq).trim();
|
|
497
|
+
const value = entry.slice(eq + 1);
|
|
498
|
+
if (name) out[name] = value;
|
|
499
|
+
}
|
|
500
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* ARV-252: filter verdicts for the digest/console display under the
|
|
505
|
+
* evidence-chain principle.
|
|
506
|
+
*
|
|
507
|
+
* - HIGH (applied) — always show; this is the actual finding.
|
|
508
|
+
* - inconclusive-baseline / inconclusive-5xx / ok / skipped — always
|
|
509
|
+
* show; operator needs them to triage probe coverage.
|
|
510
|
+
* - INFO with at least one `absent` outcome (couldn't verify via
|
|
511
|
+
* follow-up GET) — show only under --verbose. This is the "single
|
|
512
|
+
* signal, no proof" case.
|
|
513
|
+
* - INFO with only `ignored` outcomes (silently dropped — correct
|
|
514
|
+
* framework behaviour) — NEVER show. Reports must not noise-floor
|
|
515
|
+
* on intentional behaviour.
|
|
516
|
+
*
|
|
517
|
+
* JSON envelope is unfiltered; this is a display-layer transform only.
|
|
518
|
+
*/
|
|
519
|
+
function filterVerdictsForDisplay(
|
|
520
|
+
verdicts: EndpointVerdict[],
|
|
521
|
+
opts: { verbose: boolean },
|
|
522
|
+
): EndpointVerdict[] {
|
|
523
|
+
return verdicts.filter((v) => {
|
|
524
|
+
if (v.severity !== "info") return true;
|
|
525
|
+
const hasAbsent = v.fields.some((f) => f.outcome === "absent");
|
|
526
|
+
if (!hasAbsent) return false; // silently-ignored: always hidden
|
|
527
|
+
return opts.verbose;
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function maStatusFromSeverity(s: EndpointVerdict["severity"]): ProbeEndpointStatus {
|
|
532
|
+
switch (s) {
|
|
533
|
+
case "high": return "high";
|
|
534
|
+
case "low":
|
|
535
|
+
case "medium":
|
|
536
|
+
case "info":
|
|
537
|
+
return "low";
|
|
538
|
+
case "ok": return "ok";
|
|
539
|
+
case "skipped": return "skipped";
|
|
540
|
+
case "inconclusive-baseline":
|
|
541
|
+
case "inconclusive-5xx":
|
|
542
|
+
return "inconclusive";
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function maFindingSeverity(s: EndpointVerdict["severity"]): ProbeFindingSeverity {
|
|
547
|
+
if (s === "high") return "high";
|
|
548
|
+
if (s === "low" || s === "medium") return "low";
|
|
549
|
+
if (s === "ok") return "ok";
|
|
550
|
+
return "inconclusive";
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function buildMaStructuredEndpoints(result: MassAssignmentResult): ProbeEndpointResult[] {
|
|
554
|
+
return result.verdicts.map((v) => ({
|
|
555
|
+
path: v.path,
|
|
556
|
+
method: v.method,
|
|
557
|
+
classes_run: ["mass-assignment"],
|
|
558
|
+
findings: v.severity === "skipped" || v.severity === "ok"
|
|
559
|
+
? []
|
|
560
|
+
: [{
|
|
561
|
+
class: "mass-assignment",
|
|
562
|
+
severity: maFindingSeverity(v.severity),
|
|
563
|
+
evidence: {
|
|
564
|
+
summary: v.summary,
|
|
565
|
+
request: { url: v.request.url, injectedFields: v.request.injectedFields },
|
|
566
|
+
...(v.response ? { response: { status: v.response.status } } : {}),
|
|
567
|
+
...(v.fields ? { fields: v.fields } : {}),
|
|
568
|
+
...(v.recommended_action ? { recommended_action: v.recommended_action } : {}),
|
|
569
|
+
},
|
|
570
|
+
}],
|
|
571
|
+
status: maStatusFromSeverity(v.severity),
|
|
572
|
+
...(v.severity === "skipped" ? { skip_reason: v.skipReason ?? v.summary } : {}),
|
|
573
|
+
}));
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function maByStatus(endpoints: ProbeEndpointResult[]): Record<ProbeEndpointStatus, number> {
|
|
577
|
+
const out: Record<ProbeEndpointStatus, number> = {
|
|
578
|
+
ok: 0, high: 0, low: 0, inconclusive: 0, skipped: 0,
|
|
579
|
+
};
|
|
580
|
+
for (const e of endpoints) out[e.status]++;
|
|
581
|
+
return out;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function maStructuredReport(result: MassAssignmentResult, endpoints: ProbeEndpointResult[]): object {
|
|
585
|
+
return {
|
|
586
|
+
endpoints,
|
|
587
|
+
summary: {
|
|
588
|
+
totalEndpoints: result.totalEndpoints,
|
|
589
|
+
probed: result.specProbed,
|
|
590
|
+
by_status: maByStatus(endpoints),
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|