@kirrosh/zond 0.23.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 +164 -1
- package/README.md +8 -7
- package/package.json +2 -3
- package/src/CLAUDE.md +112 -0
- package/src/cli/commands/add-api.ts +19 -7
- package/src/cli/commands/api/annotate/index.ts +359 -4
- package/src/cli/commands/api/annotate/lifecycle.ts +1 -1
- package/src/cli/commands/api/annotate/overlay.ts +1 -1
- package/src/cli/commands/api/annotate/pagination.ts +10 -6
- package/src/cli/commands/api/annotate/prompts.ts +39 -2
- package/src/cli/commands/audit.ts +360 -54
- package/src/cli/commands/check.ts +15 -2
- package/src/cli/commands/checks.ts +352 -36
- package/src/cli/commands/cleanup.ts +4 -30
- package/src/cli/commands/coverage.ts +275 -57
- package/src/cli/commands/db.ts +311 -8
- package/src/cli/commands/discover.ts +281 -161
- package/src/cli/commands/doctor.ts +57 -3
- package/src/cli/commands/fixtures.ts +1 -1
- package/src/cli/commands/generate.ts +24 -7
- package/src/cli/commands/init/bootstrap.ts +4 -1
- package/src/cli/commands/init/index.ts +2 -2
- package/src/cli/commands/init/skills.ts +43 -0
- package/src/cli/commands/init/templates/agents.md +12 -3
- package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +268 -44
- package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +88 -26
- package/src/cli/commands/init/templates/skills/zond.md +274 -64
- package/src/cli/commands/init/templates/zond-config.yml +1 -1
- package/src/cli/commands/prepare-fixtures.ts +14 -52
- package/src/cli/commands/probe/_seed-bodies.ts +52 -0
- package/src/cli/commands/probe/mass-assignment.ts +101 -10
- package/src/cli/commands/probe/security.ts +95 -12
- package/src/cli/commands/probe/webhooks.ts +2 -0
- package/src/cli/commands/probe.ts +87 -11
- package/src/cli/commands/refresh-api.ts +59 -1
- package/src/cli/commands/report-bundle.ts +3 -11
- package/src/cli/commands/request.ts +116 -0
- package/src/cli/commands/run.ts +33 -5
- package/src/cli/commands/schema-from-runs.ts +128 -0
- package/src/cli/commands/secrets.ts +133 -0
- package/src/cli/json-envelope.ts +0 -20
- package/src/cli/json-schemas.ts +51 -0
- package/src/cli/output.ts +17 -1
- package/src/cli/program.ts +5 -4
- package/src/cli/safe-live.ts +24 -0
- package/src/cli/status-filter.ts +0 -10
- package/src/core/audit/persist.ts +183 -0
- package/src/core/checks/budget.ts +59 -0
- package/src/core/checks/checks/cross_call_references.ts +17 -4
- package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
- package/src/core/checks/checks/idempotency_replay.ts +1 -5
- package/src/core/checks/checks/ignored_auth.ts +44 -1
- package/src/core/checks/checks/index.ts +3 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +169 -26
- package/src/core/checks/checks/negative_data_rejection.ts +119 -16
- package/src/core/checks/checks/not_a_server_error.ts +8 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +47 -18
- package/src/core/checks/checks/pagination_invariants.ts +298 -117
- package/src/core/checks/checks/positive_data_acceptance.ts +1 -4
- package/src/core/checks/checks/status_code_conformance.ts +78 -7
- package/src/core/checks/mode.ts +3 -0
- package/src/core/checks/recommended-action.ts +5 -1
- package/src/core/checks/runner.ts +614 -27
- package/src/core/checks/spec-findings.ts +308 -0
- package/src/core/checks/types.ts +117 -1
- package/src/core/checks/zond-extensions.ts +73 -0
- package/src/core/classifier/recommended-action.ts +35 -6
- package/src/core/coverage/loader.ts +31 -0
- package/src/core/diagnostics/db-analysis.ts +200 -106
- package/src/core/diagnostics/failure-class.ts +21 -1
- package/src/core/diagnostics/failure-hints.ts +4 -208
- package/src/core/diagnostics/suggested-fixes.ts +2 -3
- package/src/core/generator/chunker.ts +1 -8
- package/src/core/generator/data-factory.ts +199 -61
- package/src/core/generator/fixtures-builder.ts +38 -31
- package/src/core/generator/index.ts +0 -2
- package/src/core/generator/openapi-reader.ts +98 -4
- package/src/core/generator/path-param-disambig.ts +30 -4
- package/src/core/generator/resources-builder.ts +276 -26
- package/src/core/generator/schema-utils.ts +22 -0
- package/src/core/generator/suite-generator.ts +168 -15
- package/src/core/generator/types.ts +6 -0
- package/src/core/identity/identity-file.ts +0 -0
- package/src/core/output/README.md +11 -29
- package/src/core/output/index.ts +1 -1
- package/src/core/output/run.ts +0 -35
- package/src/core/output/types.ts +0 -7
- package/src/core/parser/dynamic-values.ts +160 -0
- package/src/core/parser/variables.ts +0 -0
- package/src/core/probe/dry-run-envelope.ts +4 -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.ts +23 -1118
- package/src/core/probe/mass-assignment-template.ts +32 -4
- package/src/core/probe/path-discovery.ts +3 -4
- package/src/core/probe/probe-harness.ts +21 -22
- 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 +8 -2
- package/src/core/probe/security-probe.ts +28 -1449
- package/src/core/probe/shared.ts +26 -0
- package/src/core/probe/webhooks-probe.ts +5 -7
- package/src/core/runner/assertions.ts +1 -1
- package/src/core/runner/executor.ts +3 -18
- package/src/core/runner/form-encode.ts +8 -18
- package/src/core/runner/http-client.ts +38 -1
- package/src/core/runner/preflight-vars.ts +19 -15
- package/src/core/runner/rate-limiter.ts +11 -29
- package/src/core/runner/run-kind.ts +7 -1
- package/src/core/runner/schema-validator.ts +2 -6
- package/src/core/runner/send-request.ts +11 -6
- package/src/core/runner/types.ts +6 -0
- package/src/core/setup-api.ts +53 -15
- package/src/core/severity/index.ts +0 -63
- package/src/core/spec/infer-schema.ts +102 -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/headers.ts +9 -0
- package/src/core/util/url.ts +24 -0
- package/src/core/workspace/fixture-gap-report.ts +84 -0
- package/src/core/workspace/fixture-gaps.ts +71 -0
- package/src/core/workspace/root.ts +13 -11
- package/src/db/migrate.ts +2 -0
- package/src/db/migrations/0002_run_kind_request.sql +59 -0
- package/src/db/queries/collections.ts +2 -2
- package/src/db/queries/results.ts +88 -0
- package/src/db/queries/runs.ts +56 -2
- package/src/db/queries.ts +3 -0
- package/src/db/schema.ts +7 -7
- package/src/cli/commands/bootstrap.ts +0 -710
- package/src/core/anti-fp/bootstrap.ts +0 -34
- package/src/core/anti-fp/index.ts +0 -33
- package/src/core/anti-fp/registry.ts +0 -44
- package/src/core/anti-fp/rules/baseline-echo.ts +0 -74
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +0 -52
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +0 -38
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +0 -35
- package/src/core/anti-fp/rules/schemathesis/index.ts +0 -24
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +0 -53
- package/src/core/anti-fp/rules/subscription-gated/index.ts +0 -11
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +0 -75
- package/src/core/anti-fp/types.ts +0 -68
- package/src/core/generator/create-body.ts +0 -89
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { classify as classifyRecommendedAction } from "../../classifier/recommended-action.ts";
|
|
2
|
+
import type { EndpointInfo } from "../../generator/types.ts";
|
|
3
|
+
import type { EndpointVerdict } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
/** Lower-cased anchored fragments for the SaaS-flavoured 403 wordings we
|
|
6
|
+
* encounter in the wild (paid plan / role-scope / feature-flag gates).
|
|
7
|
+
* Each entry is one independent signal โ a body matching any one of them
|
|
8
|
+
* is treated as subscription-gated. Formerly the anti-FP registry rule
|
|
9
|
+
* `subscription-gated/paid-plan-403` (ARV-125); now inlined as a plain
|
|
10
|
+
* evidence signal on the baseline summary โ NOT a suppressor. */
|
|
11
|
+
const SUBSCRIPTION_GATED_PATTERNS: RegExp[] = [
|
|
12
|
+
/\bpaid plan\b/i,
|
|
13
|
+
/\bsubscription (?:required|needed)\b/i,
|
|
14
|
+
/\bnot (?:available|enabled) (?:on|for) your\b/i,
|
|
15
|
+
/\bplan (?:does not include|doesn['']?t include)\b/i,
|
|
16
|
+
/\brequires? (?:the )?[\w:-]+ scope\b/i,
|
|
17
|
+
/\bmissing (?:the )?[\w:-]+ scope\b/i,
|
|
18
|
+
/\bfeature (?:is )?(?:not enabled|disabled|not available)\b/i,
|
|
19
|
+
/\binsufficient (?:permissions?|scope)\b/i,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/** Reason text surfaced on the baseline summary when a 403 body names a
|
|
23
|
+
* subscription/scope gate โ signals to the triage agent that fixture
|
|
24
|
+
* edits won't help. */
|
|
25
|
+
const SUBSCRIPTION_GATED_REASON =
|
|
26
|
+
"endpoint is env/subscription-gated (paid plan, role/scope, feature flag); " +
|
|
27
|
+
"not a fixture issue โ wontfix unless scope changes";
|
|
28
|
+
|
|
29
|
+
function matchesSubscriptionGated(message: string): boolean {
|
|
30
|
+
for (const re of SUBSCRIPTION_GATED_PATTERNS) {
|
|
31
|
+
if (re.test(message)) return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** ARV-56: route through the single classifier instead of carrying the
|
|
37
|
+
* severityโaction switch inline. */
|
|
38
|
+
export function stampRecommendedAction(verdict: EndpointVerdict): void {
|
|
39
|
+
const action = classifyRecommendedAction({
|
|
40
|
+
finding_class: "probe:mass_assignment",
|
|
41
|
+
severity: verdict.severity as Parameters<typeof classifyRecommendedAction>[0]["severity"],
|
|
42
|
+
});
|
|
43
|
+
if (action) verdict.recommended_action = action;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build a one-line summary for INCONCLUSIVE-baseline verdicts. We surface
|
|
48
|
+
* the server's error code/name when present so the user can immediately
|
|
49
|
+
* see *which* FK / scope / fixture failed and fix it before re-probing.
|
|
50
|
+
*/
|
|
51
|
+
export function inconclusiveBaselineSummary(
|
|
52
|
+
status: number,
|
|
53
|
+
body: unknown,
|
|
54
|
+
bodyFkMisses?: Array<{ field: string; reason: string }>,
|
|
55
|
+
): string {
|
|
56
|
+
const hint = extractBaselineHint(body);
|
|
57
|
+
const base = `baseline body invalid โ server returned ${status}`;
|
|
58
|
+
// ARV-104 (F9): when status is 403 and the response body names a
|
|
59
|
+
// subscription/scope gate (paid plan, feature flag, role/scope
|
|
60
|
+
// insufficient), the right answer isn't "fix fixture" โ there's
|
|
61
|
+
// nothing to fix. Surface a wontfix reason on the summary as raw
|
|
62
|
+
// evidence for the triage agent (this is a hint, NOT a suppressor).
|
|
63
|
+
const gated = status === 403 && hint !== undefined && matchesSubscriptionGated(hint);
|
|
64
|
+
const tail = gated
|
|
65
|
+
? ` โ ${SUBSCRIPTION_GATED_REASON}`
|
|
66
|
+
: " โ fix fixture / FK value / path-params and re-probe";
|
|
67
|
+
// TASK-137: if body-FK auto-discovery couldn't fill required FK fields, name
|
|
68
|
+
// them in the summary so the user knows exactly what to add to env (or
|
|
69
|
+
// why discover-fk missed โ e.g. nested list endpoint, 403 from scope).
|
|
70
|
+
let fkClause = "";
|
|
71
|
+
if (bodyFkMisses && bodyFkMisses.length > 0) {
|
|
72
|
+
const names = bodyFkMisses.map(m => m.field).join(", ");
|
|
73
|
+
fkClause = ` โ unresolved body FKs: ${names}`;
|
|
74
|
+
}
|
|
75
|
+
return hint
|
|
76
|
+
? `${base} (${hint})${fkClause}${tail}`
|
|
77
|
+
: `${base}${fkClause}${tail}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** ARV-104 (F9): quick yes/no on whether a 403 body names a
|
|
81
|
+
* subscription/scope gate. Public predicate for callers (and tests)
|
|
82
|
+
* that want the signal without composing a summary string. */
|
|
83
|
+
export const isSubscriptionGated = matchesSubscriptionGated;
|
|
84
|
+
|
|
85
|
+
function extractBaselineHint(body: unknown): string | undefined {
|
|
86
|
+
if (typeof body === "string") {
|
|
87
|
+
const trimmed = body.trim();
|
|
88
|
+
if (trimmed.length === 0) return undefined;
|
|
89
|
+
return trimmed.length > 120 ? `${trimmed.slice(0, 120)}โฆ` : trimmed;
|
|
90
|
+
}
|
|
91
|
+
if (typeof body !== "object" || body === null) return undefined;
|
|
92
|
+
const obj = body as Record<string, unknown>;
|
|
93
|
+
// Common error-envelope fields across SaaS APIs.
|
|
94
|
+
const candidates = [
|
|
95
|
+
obj.message,
|
|
96
|
+
obj.error,
|
|
97
|
+
(obj.error as Record<string, unknown> | undefined)?.message,
|
|
98
|
+
obj.detail,
|
|
99
|
+
obj.title,
|
|
100
|
+
obj.name,
|
|
101
|
+
obj.code,
|
|
102
|
+
];
|
|
103
|
+
for (const c of candidates) {
|
|
104
|
+
if (typeof c === "string" && c.length > 0) {
|
|
105
|
+
return c.length > 120 ? `${c.slice(0, 120)}โฆ` : c;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function needsFollowUp(verdict: EndpointVerdict): boolean {
|
|
112
|
+
return verdict.fields.some(f => f.outcome === "absent" || f.outcome === "unknown");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function classifyFromBody(
|
|
116
|
+
verdict: EndpointVerdict,
|
|
117
|
+
body: Record<string, unknown> | undefined,
|
|
118
|
+
fromGet = false,
|
|
119
|
+
): void {
|
|
120
|
+
if (!body) return;
|
|
121
|
+
for (const field of verdict.fields) {
|
|
122
|
+
// Once a field is decisively classified (applied/echoed-overwritten),
|
|
123
|
+
// don't downgrade. But "absent" on POST may still flip to applied/ignored
|
|
124
|
+
// after GET โ so only re-check those.
|
|
125
|
+
if (field.outcome === "applied" || field.outcome === "echoed-overwritten") continue;
|
|
126
|
+
if (!(field.field in body)) {
|
|
127
|
+
// GET also missing โ ignored. POST missing โ keep "absent" so we GET later.
|
|
128
|
+
field.outcome = fromGet ? "ignored" : "absent";
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const observed = body[field.field];
|
|
132
|
+
field.observed = observed;
|
|
133
|
+
if (Bun.deepEquals(observed, field.injected)) {
|
|
134
|
+
field.outcome = "applied";
|
|
135
|
+
} else if (fromGet) {
|
|
136
|
+
field.outcome = "ignored";
|
|
137
|
+
} else {
|
|
138
|
+
field.outcome = "echoed-overwritten";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function findIdParam(ep: EndpointInfo): string {
|
|
144
|
+
const m = ep.path.match(/\{([^}]+)\}/);
|
|
145
|
+
return m ? m[1]! : "id";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function finaliseSeverity(v: EndpointVerdict, strict: boolean): void {
|
|
149
|
+
const applied = v.fields.filter(f => f.outcome === "applied");
|
|
150
|
+
const absent = v.fields.filter(f => f.outcome === "absent");
|
|
151
|
+
|
|
152
|
+
if (applied.length > 0) {
|
|
153
|
+
v.severity = "high";
|
|
154
|
+
v.summary = `accepted-and-applied: ${applied.map(f => f.field).join(", ")}`;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (absent.length > 0) {
|
|
158
|
+
// ARV-252: absent-but-unverifiable carries single_signal proof.
|
|
159
|
+
// Surfaced as INFO and only shown under --verbose so the report
|
|
160
|
+
// stays clean; the verdict still travels through the JSON envelope
|
|
161
|
+
// for agents that want to triage it explicitly.
|
|
162
|
+
v.severity = "info";
|
|
163
|
+
v.summary = `inconclusive โ could not verify via follow-up GET (${absent.map(f => f.field).join(", ")})`;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// ARV-252: silently-ignored = correct framework behaviour (Rails
|
|
167
|
+
// strong params / FastAPI extra=ignore). Severity stays INFO so it
|
|
168
|
+
// never gates CI, AND the CLI display layer suppresses it entirely
|
|
169
|
+
// (even under --verbose). Reports must not be noise-floored by
|
|
170
|
+
// correct behaviour. Verdicts still travel through the JSON envelope
|
|
171
|
+
// for agents that explicitly want to inspect them.
|
|
172
|
+
v.severity = "info";
|
|
173
|
+
const status = v.response?.status ?? 0;
|
|
174
|
+
v.summary = `accepted ${status} but extras silently ignored${strict ? " (despite additionalProperties:false โ server should reject)" : ""}`;
|
|
175
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { EndpointInfo, SecuritySchemeInfo } from "../../generator/types.ts";
|
|
2
|
+
import { executeRequest } from "../../runner/http-client.ts";
|
|
3
|
+
import {
|
|
4
|
+
captureFieldFor,
|
|
5
|
+
findDeleteCounterpart,
|
|
6
|
+
liveAuthHeaders,
|
|
7
|
+
} from "../shared.ts";
|
|
8
|
+
import { buildProbeUrl } from "../probe-harness.ts";
|
|
9
|
+
import { findIdParam } from "./classify.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Best-effort DELETE on the baseline (no-extras) probe so the injected
|
|
13
|
+
* POST doesn't trip a unique-constraint and we don't leak resources.
|
|
14
|
+
*/
|
|
15
|
+
export async function tryCleanupBaseline(
|
|
16
|
+
ep: EndpointInfo,
|
|
17
|
+
allEndpoints: EndpointInfo[],
|
|
18
|
+
schemes: SecuritySchemeInfo[],
|
|
19
|
+
vars: Record<string, string>,
|
|
20
|
+
baselineBody: unknown,
|
|
21
|
+
opts: { timeoutMs?: number },
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const body =
|
|
24
|
+
typeof baselineBody === "object" && baselineBody !== null
|
|
25
|
+
? (baselineBody as Record<string, unknown>)
|
|
26
|
+
: undefined;
|
|
27
|
+
if (!body) return;
|
|
28
|
+
const idField = captureFieldFor(ep);
|
|
29
|
+
const id = body[idField];
|
|
30
|
+
if (id === undefined) return;
|
|
31
|
+
const delEp = findDeleteCounterpart(ep, allEndpoints);
|
|
32
|
+
if (!delEp) return;
|
|
33
|
+
const delVars = { ...vars, [findIdParam(delEp)]: String(id), id: String(id) };
|
|
34
|
+
const delUrl = buildProbeUrl(delEp, delVars);
|
|
35
|
+
if (delUrl.unresolved.length > 0) return;
|
|
36
|
+
try {
|
|
37
|
+
await executeRequest(
|
|
38
|
+
{
|
|
39
|
+
method: "DELETE",
|
|
40
|
+
url: delUrl.url,
|
|
41
|
+
headers: {
|
|
42
|
+
accept: "application/json",
|
|
43
|
+
...liveAuthHeaders(delEp, schemes, vars),
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{ timeout: opts.timeoutMs ?? 30000, retries: 0 },
|
|
47
|
+
);
|
|
48
|
+
} catch {
|
|
49
|
+
// best-effort โ if cleanup fails we'll leak a baseline resource, but
|
|
50
|
+
// that's a deployment problem, not a probe bug.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { SUSPECTED_FIELDS } from "./suspects.ts";
|
|
2
|
+
import type {
|
|
3
|
+
EndpointVerdict,
|
|
4
|
+
MassAssignmentResult,
|
|
5
|
+
Severity,
|
|
6
|
+
} from "./types.ts";
|
|
7
|
+
|
|
8
|
+
const SEVERITY_ORDER: Severity[] = [
|
|
9
|
+
"high",
|
|
10
|
+
"inconclusive-baseline",
|
|
11
|
+
"inconclusive-5xx",
|
|
12
|
+
"medium",
|
|
13
|
+
"low",
|
|
14
|
+
"info",
|
|
15
|
+
"ok",
|
|
16
|
+
"skipped",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const SEVERITY_HEADER: Record<Severity, string> = {
|
|
20
|
+
high: "๐จ HIGH โ privilege escalation candidates",
|
|
21
|
+
"inconclusive-baseline": "โ ๏ธ INCONCLUSIVE โ baseline body invalid (fix fixture / FK / scope and re-probe)",
|
|
22
|
+
"inconclusive-5xx": "โ ๏ธ INCONCLUSIVE โ baseline 5xx (endpoint crashes โ likely duplicate of validation-probe)",
|
|
23
|
+
medium: "โ ๏ธ MEDIUM โ inconclusive (no follow-up GET available)",
|
|
24
|
+
low: "โน๏ธ LOW โ inconclusive (single-signal, follow-up GET unavailable)",
|
|
25
|
+
info: "ยท INFO โ accepted-and-ignored (correct framework behaviour, often ineligible to report)",
|
|
26
|
+
ok: "โ
OK โ rejected 4xx (best behaviour)",
|
|
27
|
+
skipped: "โญ๏ธ SKIPPED",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function formatDigestMarkdown(
|
|
31
|
+
result: MassAssignmentResult,
|
|
32
|
+
specPath: string,
|
|
33
|
+
): string {
|
|
34
|
+
const lines: string[] = [];
|
|
35
|
+
lines.push(`# Mass-assignment probe digest`);
|
|
36
|
+
lines.push("");
|
|
37
|
+
lines.push(`**Spec:** \`${specPath}\``);
|
|
38
|
+
lines.push(`**Endpoints probed:** ${result.specProbed} of ${result.totalEndpoints} mutating endpoints`);
|
|
39
|
+
lines.push("");
|
|
40
|
+
lines.push(`**Suspected fields tested:** ${Object.keys(SUSPECTED_FIELDS).join(", ")}`);
|
|
41
|
+
lines.push("");
|
|
42
|
+
|
|
43
|
+
const buckets = groupBySeverity(result.verdicts);
|
|
44
|
+
for (const sev of SEVERITY_ORDER) {
|
|
45
|
+
const items = buckets[sev];
|
|
46
|
+
if (!items || items.length === 0) continue;
|
|
47
|
+
lines.push(`## ${SEVERITY_HEADER[sev]} (${items.length})`);
|
|
48
|
+
lines.push("");
|
|
49
|
+
for (const v of items) {
|
|
50
|
+
lines.push(`### ${v.method} ${v.path}`);
|
|
51
|
+
lines.push("");
|
|
52
|
+
if (v.severity === "skipped") {
|
|
53
|
+
lines.push(`- Skipped: ${v.skipReason ?? v.summary}`);
|
|
54
|
+
lines.push("");
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
lines.push(`- ${v.summary}`);
|
|
58
|
+
lines.push(`- Injected: ${v.request.injectedFields.map(n => `\`${n}\``).join(", ")}`);
|
|
59
|
+
if (v.baseline) {
|
|
60
|
+
lines.push(`- Baseline (no extras): ${v.baseline.status}`);
|
|
61
|
+
}
|
|
62
|
+
if (v.response) {
|
|
63
|
+
lines.push(`- With extras: ${v.response.status}`);
|
|
64
|
+
}
|
|
65
|
+
if (v.followUpGet) {
|
|
66
|
+
lines.push(`- Follow-up GET โ ${v.followUpGet.status}`);
|
|
67
|
+
}
|
|
68
|
+
const interesting = v.fields.filter(f => f.outcome !== "ignored" && f.outcome !== "absent");
|
|
69
|
+
if (interesting.length > 0) {
|
|
70
|
+
lines.push(`- Per-field outcomes:`);
|
|
71
|
+
for (const f of interesting) {
|
|
72
|
+
const obs = f.observed === undefined ? "n/a" : JSON.stringify(f.observed);
|
|
73
|
+
lines.push(` - \`${f.field}\` โ **${f.outcome}** (injected ${JSON.stringify(f.injected)}, observed ${obs})`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (v.cleanup) {
|
|
77
|
+
if (v.cleanup.attempted) {
|
|
78
|
+
lines.push(`- Cleanup DELETE: ${v.cleanup.status ?? "errored"}${v.cleanup.error ? ` โ ${v.cleanup.error}` : ""}`);
|
|
79
|
+
} else {
|
|
80
|
+
lines.push(`- Cleanup skipped: ${v.cleanup.error ?? "unknown"}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (v.notes && v.notes.length > 0) {
|
|
84
|
+
for (const n of v.notes) lines.push(`- Note: ${n}`);
|
|
85
|
+
}
|
|
86
|
+
if (v.severity === "high") {
|
|
87
|
+
lines.push(`- **Action:** treat as P0 โ server should reject or strip these fields.`);
|
|
88
|
+
}
|
|
89
|
+
if (v.severity === "inconclusive-baseline") {
|
|
90
|
+
lines.push(
|
|
91
|
+
`- **Action:** the baseline POST itself failed โ set the right fixture / FK / path-params in your env (e.g. \`domain_id\`, \`account_id\`) and re-run.`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (v.severity === "inconclusive-5xx") {
|
|
95
|
+
lines.push(
|
|
96
|
+
`- **Action:** baseline crashed with 5xx โ fix the underlying server bug (validation-probe likely reported it for the same endpoint) before mass-assignment can be observed here.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
lines.push("");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (result.warnings.length > 0) {
|
|
104
|
+
lines.push(`## Warnings`);
|
|
105
|
+
lines.push("");
|
|
106
|
+
for (const w of result.warnings) lines.push(`- ${w}`);
|
|
107
|
+
lines.push("");
|
|
108
|
+
}
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function groupBySeverity(verdicts: EndpointVerdict[]): Partial<Record<Severity, EndpointVerdict[]>> {
|
|
113
|
+
return Object.groupBy(verdicts, (v) => v.severity);
|
|
114
|
+
}
|