@xaccefy/pi-casefile 0.8.0 → 0.8.2
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/README.md +2 -2
- package/package.json +1 -1
- package/src/index.ts +176 -52
- package/src/ledger.ts +455 -80
- package/src/poc-runner.ts +193 -24
- package/src/workflow.ts +8 -6
package/README.md
CHANGED
|
@@ -62,11 +62,11 @@ hypothesis → investigating → confirmed → reported
|
|
|
62
62
|
| `CaseAdd` | Open a case (`title` + `disproveIf` required; start as `hypothesis` or `investigating`) |
|
|
63
63
|
| `CaseUpdate` | Evidence, impact, severity, status (not direct confirm) |
|
|
64
64
|
| `EvidenceAdd` | Role-typed, hashed evidence item on a case (refutation justifies kills; cleanup tracks cleanup) |
|
|
65
|
-
| `PromoteFinding` | Run on-disk PoC (Docker sandbox by default; `local:true`
|
|
65
|
+
| `PromoteFinding` | Run on-disk PoC (Docker sandbox by default; `local:true` = host-network sandbox, host execution operator-gated via `PI_POC_ALLOW_LOCAL=1`) → confirm on exit 0 + marker; `control_path` + `control_liveness_marker` REQUIRED for every promotion |
|
|
66
66
|
| `CaseGet` / `CaseList` / `CaseSearch` | Read / filter / search |
|
|
67
67
|
| `CaseLink` / `CaseUnlink` | Bidirectional exploit chains |
|
|
68
68
|
| `ChainSuggest` | Scan cases for exploitable chain combinations (credential+endpoint→ATO, redirect+OAuth→token theft, XSS+state-change→CSRF, IDOR+user-data, SSTI→RCE, race+payment, info-disclosure+SSRF), ranked by confidence |
|
|
69
|
-
| `CoverageAdd` | Record a tested (asset × attack-class) cell — `scope: wide` (deployment-wide verdict, applies to every later asset) or `local`; both found and clean results count |
|
|
69
|
+
| `CoverageAdd` | Record a tested (asset × attack-class) cell — `scope: wide` (deployment-wide verdict, applies to every later asset) or `local`; both found and clean results count. Optionally link the cell to an artifact-backed evidence item (`evidence_item_id`); unbacked cells render as ⚠ unbacked in the report |
|
|
70
70
|
| `CoverageReport` | Render the machine-checkable coverage matrix (which classes are tested where; plateau claims must match it) |
|
|
71
71
|
| `CaseContext` | Context bundle for a confirmed/reported case (full record, logs, links, artifacts) + report path |
|
|
72
72
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Event: before_agent_start — injects cyber workflow once per session, refreshes the active case list per prompt
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
9
10
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
11
|
import { dirname, join } from "node:path";
|
|
11
12
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
@@ -55,7 +56,7 @@ import {
|
|
|
55
56
|
writeCaseContext,
|
|
56
57
|
} from "./ledger.ts";
|
|
57
58
|
import { pipeline_submit, SUBMIT_STAGES, type SubmitStage } from "./pipeline-submit.ts";
|
|
58
|
-
import { type PocRun, runPoc } from "./poc-runner.ts";
|
|
59
|
+
import { type PocRun, type PocRunOptions, runPoc } from "./poc-runner.ts";
|
|
59
60
|
import {
|
|
60
61
|
PHASE_ORDER,
|
|
61
62
|
type ScratchpadPhase,
|
|
@@ -171,16 +172,24 @@ const PromoteSchema = Type.Object(
|
|
|
171
172
|
disconfirmation_path: Type.Optional(
|
|
172
173
|
Type.String({
|
|
173
174
|
description:
|
|
174
|
-
"Absolute path to a disconfirmation script that tries to disprove the finding; must exit non-zero (failure to disprove)",
|
|
175
|
+
"Absolute path to a disconfirmation script that tries to disprove the finding; must exit non-zero (failure to disprove). REQUIRED for severity high/critical findings.",
|
|
175
176
|
}),
|
|
176
177
|
),
|
|
177
|
-
control_path: Type.
|
|
178
|
-
|
|
178
|
+
control_path: Type.String({
|
|
179
|
+
description:
|
|
180
|
+
"REQUIRED for EVERY promotion (sandboxed and live alike): absolute path to a control-target script that runs the SAME PoC against a control that lacks the vulnerability (patched replica, second account, baseline endpoint). The harness blocks promotion if the verification_marker appears in the control output — an unconditional-marker/mock PoC cannot pass. Best form: the same parameterized script, branching on the PI_POC_MODE env var (poc | control) the harness sets on every run.",
|
|
181
|
+
}),
|
|
182
|
+
control_liveness_marker: Type.String({
|
|
183
|
+
minLength: 1,
|
|
184
|
+
description:
|
|
185
|
+
"REQUIRED for EVERY promotion: a unique string the control script must print AFTER successfully reaching/exercising the control target (e.g. 'CONTROL_REACHED_<case-id>'). The harness blocks promotion if the control output lacks it — a control that never reached its target (unreachable host, wrong port, early exit) is not a clean verdict. Must differ from verification_marker.",
|
|
186
|
+
}),
|
|
187
|
+
local: Type.Optional(
|
|
188
|
+
Type.Boolean({
|
|
179
189
|
description:
|
|
180
|
-
"
|
|
190
|
+
"Run with network access (Docker sandbox with --network host — still read-only FS, dropped capabilities, unprivileged user) instead of the isolated --network none sandbox. True host execution is NOT agent-selectable: it requires the operator to set PI_POC_ALLOW_LOCAL=1, and is used only as a fallback when Docker is unavailable.",
|
|
181
191
|
}),
|
|
182
192
|
),
|
|
183
|
-
local: Type.Optional(Type.Boolean({ description: "Run locally instead of in Docker sandbox" })),
|
|
184
193
|
},
|
|
185
194
|
{ additionalProperties: false },
|
|
186
195
|
);
|
|
@@ -890,6 +899,12 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
890
899
|
description:
|
|
891
900
|
"Short note of the tests ACTUALLY RUN and the verdict: techniques tried · result · key gap. A verdict guessed without testing can hide a real issue.",
|
|
892
901
|
}),
|
|
902
|
+
evidence_item_id: Type.Optional(
|
|
903
|
+
Type.String({
|
|
904
|
+
description:
|
|
905
|
+
"Evidence item id backing this tested verdict (must be an artifact-backed EvidenceAdd item on this case). Cells without a backing item render as 'unbacked' in CoverageReport — 'tested' claims must be machine-checkable, not prose-only.",
|
|
906
|
+
}),
|
|
907
|
+
),
|
|
893
908
|
},
|
|
894
909
|
{ additionalProperties: false },
|
|
895
910
|
);
|
|
@@ -914,6 +929,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
914
929
|
class: params.class as string,
|
|
915
930
|
scope: (params.scope ?? "local") as CoverageScope,
|
|
916
931
|
note: params.note as string,
|
|
932
|
+
evidenceItemId: params.evidence_item_id as string | undefined,
|
|
917
933
|
});
|
|
918
934
|
const record = getCaseById(params.case_id as string)!;
|
|
919
935
|
return {
|
|
@@ -979,7 +995,10 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
979
995
|
lines.push(`\n## ${asset}`);
|
|
980
996
|
for (const cell of summary.byAsset[asset] ?? []) {
|
|
981
997
|
lines.push(
|
|
982
|
-
`- [${cell.scope}] ${cell.class} — ${cell.note}${cell.testedBy ? ` (by ${cell.testedBy})` : ""}
|
|
998
|
+
`- [${cell.scope}] ${cell.class} — ${cell.note}${cell.testedBy ? ` (by ${cell.testedBy})` : ""}` +
|
|
999
|
+
(cell.evidenceItemId
|
|
1000
|
+
? ""
|
|
1001
|
+
: " ⚠ unbacked (link an artifact-backed evidence item via CoverageAdd evidence_item_id)"),
|
|
983
1002
|
);
|
|
984
1003
|
}
|
|
985
1004
|
}
|
|
@@ -1009,15 +1028,16 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1009
1028
|
name: "PromoteFinding",
|
|
1010
1029
|
label: "Promote Finding",
|
|
1011
1030
|
description:
|
|
1012
|
-
"Run an on-disk PoC script (Docker sandbox or
|
|
1031
|
+
"Run an on-disk PoC script (Docker sandbox or host-network sandbox) and, on exit 0 + verification marker present in output, promote an investigating case to confirmed. The verification_marker proves the exploit actually worked — exit code 0 alone is NOT sufficient. EVERY promotion (sandboxed and live alike) REQUIRES: (1) a disconfirmation_path script that must exit non-zero (the finding survived the attempt to disprove it); (2) a control_path that is the SAME script as the PoC (sha256-enforced — a separately written control file is rejected), run in control mode via PI_POC_MODE; (3) a control_liveness_marker the control must print after reaching its target. The harness blocks promotion if the vuln marker appears in the (untruncated) control output, if the liveness marker is absent, or if the control/disconfirmation scripts crash. Host execution is never agent-selectable — local:true uses a host-network Docker sandbox; true host runs need the operator's PI_POC_ALLOW_LOCAL=1.",
|
|
1013
1032
|
promptSnippet: "Run a PoC and promote an investigating case to confirmed",
|
|
1014
1033
|
promptGuidelines: [
|
|
1015
1034
|
"Use PromoteFinding when an investigating case has a concrete PoC script on disk and you are ready to prove it.",
|
|
1016
|
-
"Prerequisites: status='investigating' and non-empty poc, evidence, impact, severity, target, disconfirmation, plus an EvidenceAdd 'observation' item on the case (the initial signal) — the PoC gate auto-records the reproduction item.",
|
|
1017
|
-
"Default sandbox: docker run --rm --network none. Use local:true for network-dependent bugs.",
|
|
1035
|
+
"Prerequisites: status='investigating' and non-empty poc, evidence, impact, severity, target, disconfirmation, plus an artifact-backed EvidenceAdd 'observation' item on the case (the initial signal, with artifact_path) — the PoC gate auto-records the reproduction item.",
|
|
1036
|
+
"Default sandbox: docker run --rm --network none. Use local:true for network-dependent bugs (host-network sandbox; host execution needs operator PI_POC_ALLOW_LOCAL=1).",
|
|
1018
1037
|
"Gate: exit 0 AND verification_marker in the PoC output. The marker (e.g. 'VULN_CONFIRMED_<case-id>') must be printed only AFTER the exploit is verified (data extracted, callback received, payload reflected) — never unconditionally or before the exploit check. The marker check prevents fluke exit 0 (script crashed early) and mocked PoCs (target faked) from passing.",
|
|
1019
|
-
"control_path (REQUIRED for
|
|
1020
|
-
"
|
|
1038
|
+
"control_path (REQUIRED for EVERY promotion): the SAME script as poc_path (sha256-equality is ENFORCED — a separately written control file is rejected). One parameterized script — read the target from the PI_POC_TARGET env var and branch on PI_POC_MODE (poc | control) — the control run is literally the same script in control mode against a control lacking the vuln (patched replica, second account, baseline endpoint). The harness blocks promotion if the verification_marker appears in the control run's output (checked on the untruncated output) — an unconditional-marker PoC cannot pass this.",
|
|
1039
|
+
"control_liveness_marker (REQUIRED): a unique string the control script prints only AFTER reaching/exercising the control target (e.g. 'CONTROL_REACHED_<case-id>'). The harness blocks promotion if the control output lacks it — a control pointed at an unreachable host, wrong port, or exiting before the check is NOT a clean verdict.",
|
|
1040
|
+
"disconfirmation_path: a script that tries to disprove the finding; if it exits 0, promotion is blocked. REQUIRED for EVERY promotion — the prose disconfirmation field is not enough at any severity (a case filed low/medium must not skip the run and be re-raised afterwards).",
|
|
1021
1041
|
"Never CaseUpdate status='confirmed' directly — it is rejected. Always use PromoteFinding.",
|
|
1022
1042
|
],
|
|
1023
1043
|
parameters: PromoteSchema,
|
|
@@ -1025,9 +1045,10 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1025
1045
|
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
1026
1046
|
// Validate promotability BEFORE running the PoC — a sandboxed run can take
|
|
1027
1047
|
// 30s (plus first-time image pull), so fail cheap when the case can't
|
|
1028
|
-
// advance anyway (missing, wrong status, missing required fields
|
|
1048
|
+
// advance anyway (missing, wrong status, missing required fields, missing
|
|
1049
|
+
// artifact-backed observation evidence).
|
|
1029
1050
|
const caseId = params.id as string;
|
|
1030
|
-
assertPromotable(caseId);
|
|
1051
|
+
const current = assertPromotable(caseId);
|
|
1031
1052
|
|
|
1032
1053
|
// Shared blocked-promotion shape: the case stays investigating and the
|
|
1033
1054
|
// caller gets the record back for context.
|
|
@@ -1048,20 +1069,100 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1048
1069
|
);
|
|
1049
1070
|
}
|
|
1050
1071
|
|
|
1051
|
-
//
|
|
1052
|
-
//
|
|
1053
|
-
// sandboxed
|
|
1054
|
-
|
|
1072
|
+
// Control-target anti-cheat is mandatory for EVERY promotion — sandboxed
|
|
1073
|
+
// and live alike. It is the only deterministic check that the marker is
|
|
1074
|
+
// target-dependent; skipping it for the default sandboxed mode would let
|
|
1075
|
+
// an unconditional-marker PoC pass untouched. Check BEFORE paying for the
|
|
1076
|
+
// PoC run.
|
|
1077
|
+
const controlPath = (params.control_path as string | undefined)?.trim();
|
|
1078
|
+
if (!controlPath) {
|
|
1055
1079
|
return fail(
|
|
1056
|
-
"
|
|
1057
|
-
"against a control lacking the vuln
|
|
1058
|
-
"absent from the control run's output — that is what
|
|
1059
|
-
"Write the control script and retry.",
|
|
1080
|
+
"control_path is REQUIRED for every promotion (sandboxed and live alike): a script that runs " +
|
|
1081
|
+
"the SAME PoC against a control lacking the vuln (patched replica, second account, baseline endpoint). " +
|
|
1082
|
+
"The harness verifies the verification_marker is absent from the control run's output — that is what " +
|
|
1083
|
+
"proves the marker is target-dependent. Write the control script and retry.",
|
|
1060
1084
|
{ missingControl: true },
|
|
1061
1085
|
);
|
|
1062
1086
|
}
|
|
1063
1087
|
|
|
1064
|
-
|
|
1088
|
+
// The control must ALSO prove it reached its target: without a liveness
|
|
1089
|
+
// marker, a control pointed at an unreachable host / wrong port / early
|
|
1090
|
+
// exit would show "marker absent" for reasons unrelated to the vuln.
|
|
1091
|
+
const livenessMarker = (params.control_liveness_marker as string | undefined)?.trim();
|
|
1092
|
+
if (!livenessMarker) {
|
|
1093
|
+
return fail(
|
|
1094
|
+
"control_liveness_marker is REQUIRED for every promotion: a unique string the control script " +
|
|
1095
|
+
"prints ONLY AFTER reaching/exercising the control target (e.g. 'CONTROL_REACHED_<case-id>'). " +
|
|
1096
|
+
"The harness blocks promotion if the control output lacks it — a control that never reached its " +
|
|
1097
|
+
"target proves nothing. Must differ from verification_marker.",
|
|
1098
|
+
{ missingLivenessMarker: true },
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
if (livenessMarker === marker) {
|
|
1102
|
+
return fail(
|
|
1103
|
+
"control_liveness_marker must differ from verification_marker — the liveness marker proves the " +
|
|
1104
|
+
"control reached its target, the verification marker proves the vuln fired. Use distinct strings.",
|
|
1105
|
+
{ livenessEqualsMarker: true },
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// EXECUTED disconfirmation is required for EVERY promotion — the prose
|
|
1110
|
+
// `disconfirmation` field cannot carry the disprove-attempt. Making it
|
|
1111
|
+
// unconditional (not severity-keyed) also kills the ordering attack:
|
|
1112
|
+
// a case filed as low/medium could previously skip the run, promote,
|
|
1113
|
+
// then be re-raised to high/critical with no executed disproof.
|
|
1114
|
+
const disconfirmationPath = (params.disconfirmation_path as string | undefined)?.trim();
|
|
1115
|
+
if (!disconfirmationPath) {
|
|
1116
|
+
return fail(
|
|
1117
|
+
"disconfirmation_path is REQUIRED for every promotion: a script that tries to " +
|
|
1118
|
+
"disprove the finding and must exit non-zero (the finding survived the attempt). " +
|
|
1119
|
+
"The prose disconfirmation field is not enough. Write the disconfirmation script and retry.",
|
|
1120
|
+
{ missingDisconfirmation: true },
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Same-file contract (anti-cheat): the control must be the SAME script as
|
|
1125
|
+
// the PoC, differing only via the harness-set PI_POC_MODE env var. Two
|
|
1126
|
+
// agent-written files (poc prints the marker unconditionally, control
|
|
1127
|
+
// prints the liveness marker) pass every string check — the "same script"
|
|
1128
|
+
// recommendation in the docs is now enforced: sha256(control) must equal
|
|
1129
|
+
// sha256(poc). Hash BEFORE any run so a mismatch fails cheap.
|
|
1130
|
+
const pocPath = (params.poc_path as string | undefined)?.trim() ?? "";
|
|
1131
|
+
let pocHash: string | undefined;
|
|
1132
|
+
let controlHash: string | undefined;
|
|
1133
|
+
try {
|
|
1134
|
+
pocHash = createHash("sha256").update(readFileSync(pocPath)).digest("hex");
|
|
1135
|
+
controlHash = createHash("sha256").update(readFileSync(controlPath)).digest("hex");
|
|
1136
|
+
} catch (e) {
|
|
1137
|
+
return fail(
|
|
1138
|
+
`Cannot read PoC/control scripts for the same-file check: ${(e as Error).message}`,
|
|
1139
|
+
{ sameFileCheckFailed: true },
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
if (pocHash !== controlHash) {
|
|
1143
|
+
return fail(
|
|
1144
|
+
"CONTROL CHECK FAILED: control_path must be the SAME script as poc_path " +
|
|
1145
|
+
"(sha256 mismatch). The control run is only meaningful as the same PoC " +
|
|
1146
|
+
"pointed at a control target via PI_POC_MODE=control — a separately written " +
|
|
1147
|
+
"control file proves nothing (the model writes both files). Parameterize " +
|
|
1148
|
+
"one script: branch on PI_POC_MODE (poc | control) and read the target from " +
|
|
1149
|
+
"PI_POC_TARGET. Case remains investigating.",
|
|
1150
|
+
{ run: undefined, controlHashMismatch: true },
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// local:true now means "network access": with the operator's
|
|
1155
|
+
// PI_POC_ALLOW_LOCAL=1 opt-in the run executes on the host; WITHOUT the
|
|
1156
|
+
// opt-in it uses a Docker sandbox with --network host (same FS/cap/user
|
|
1157
|
+
// isolation) and fails closed when Docker is unavailable too. Host
|
|
1158
|
+
// execution is never agent-selectable on its own.
|
|
1159
|
+
const runOptions = (pocMode: string): PocRunOptions => ({
|
|
1160
|
+
network: params.local === true ? "host" : "none",
|
|
1161
|
+
local: params.local === true,
|
|
1162
|
+
env: { PI_POC_MODE: pocMode, PI_POC_TARGET: current.target ?? "" },
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
const run = runPoc(pocPath, runOptions("poc"));
|
|
1065
1166
|
|
|
1066
1167
|
// Fail closed without throwing: non-zero PoC must leave the case investigating.
|
|
1067
1168
|
if (run.exitCode !== 0) {
|
|
@@ -1081,11 +1182,12 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1081
1182
|
}
|
|
1082
1183
|
|
|
1083
1184
|
// Verification marker check: exit code 0 alone is NOT sufficient.
|
|
1084
|
-
// The PoC must print the verification_marker
|
|
1085
|
-
//
|
|
1086
|
-
//
|
|
1087
|
-
//
|
|
1088
|
-
|
|
1185
|
+
// The PoC must print the verification_marker, proving the exploit
|
|
1186
|
+
// actually worked — not just that the script ran. The check runs on
|
|
1187
|
+
// rawOutput (untruncated) so a script printing its marker past the
|
|
1188
|
+
// 4000-char display window cannot hide it.
|
|
1189
|
+
const pocOut = run.rawOutput ?? run.output;
|
|
1190
|
+
if (!pocOut.includes(marker)) {
|
|
1089
1191
|
return fail(
|
|
1090
1192
|
`PoC exited 0 but the verification marker "${marker}" was NOT found in the output.\n` +
|
|
1091
1193
|
`This means the script ran but did not prove exploitation. The marker must be printed only AFTER the PoC verifies the exploit worked (data extracted, callback received, payload reflected, etc.).\n` +
|
|
@@ -1094,10 +1196,10 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1094
1196
|
);
|
|
1095
1197
|
}
|
|
1096
1198
|
|
|
1097
|
-
// Run disconfirmation script
|
|
1199
|
+
// Run disconfirmation script — must exit NON-0 (finding survived the attempt to disprove).
|
|
1098
1200
|
let disconfirmationRun: PocRun | undefined;
|
|
1099
|
-
if (
|
|
1100
|
-
disconfirmationRun = runPoc(
|
|
1201
|
+
if (disconfirmationPath) {
|
|
1202
|
+
disconfirmationRun = runPoc(disconfirmationPath, runOptions("disconfirmation"));
|
|
1101
1203
|
if (!disconfirmationRun.completed) {
|
|
1102
1204
|
return fail(
|
|
1103
1205
|
`Disconfirmation script did NOT complete (spawn error, killed, or timeout — no completion marker). ` +
|
|
@@ -1118,30 +1220,47 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1118
1220
|
// Control-target anti-cheat check: the same PoC pointed at a control that
|
|
1119
1221
|
// lacks the vuln must NOT print the marker. The control script is
|
|
1120
1222
|
// agent-written, but the marker-absence check is harness-side and
|
|
1121
|
-
// deterministic — the model cannot pass it by asserting success.
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1223
|
+
// deterministic — the model cannot pass it by asserting success. The
|
|
1224
|
+
// liveness-marker check closes the "control pointed at an unreachable
|
|
1225
|
+
// host / exited early" hole: the control must prove it reached its target.
|
|
1226
|
+
const controlRun = runPoc(controlPath, runOptions("control"));
|
|
1227
|
+
if (!controlRun.completed) {
|
|
1228
|
+
return fail(
|
|
1229
|
+
`CONTROL CHECK FAILED: the control-target script did NOT complete (spawn error, killed, or timeout). ` +
|
|
1230
|
+
`A control run that never executed proves nothing about the marker — fix the control script and retry.\n` +
|
|
1231
|
+
`Control output:\n${controlRun.output}`,
|
|
1232
|
+
{ run, controlRun, controlCrashed: true },
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
// Marker-absence + liveness checks run on the UNTRUNCATED control output.
|
|
1236
|
+
const controlOut = controlRun.rawOutput ?? controlRun.output;
|
|
1237
|
+
if (controlOut.includes(marker)) {
|
|
1238
|
+
return fail(
|
|
1239
|
+
`CONTROL CHECK FAILED: the verification marker "${marker}" appeared in the control-target run. ` +
|
|
1240
|
+
`The PoC prints the marker without the vulnerable condition — a cheating PoC (unconditional marker) ` +
|
|
1241
|
+
`or a broken check. Case remains investigating.\nControl output:\n${controlRun.output}`,
|
|
1242
|
+
{ run, controlRun, controlCheated: true },
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
if (!controlOut.includes(livenessMarker)) {
|
|
1246
|
+
return fail(
|
|
1247
|
+
`CONTROL CHECK FAILED: the control-target run completed but the control_liveness_marker "${livenessMarker}" ` +
|
|
1248
|
+
`was NOT found in its output. The control must print the liveness marker only AFTER reaching/exercising ` +
|
|
1249
|
+
`the control target — an unreachable host, wrong port, or early exit is not a valid control verdict. ` +
|
|
1250
|
+
`Case remains investigating.\nControl output:\n${controlRun.output}`,
|
|
1251
|
+
{ run, controlRun, controlLivenessMissing: true },
|
|
1252
|
+
);
|
|
1141
1253
|
}
|
|
1142
1254
|
|
|
1143
1255
|
// PocRun is structurally a PocVerification — pass the runs straight through.
|
|
1144
|
-
const result = promoteFindingResult(
|
|
1256
|
+
const result = promoteFindingResult(
|
|
1257
|
+
caseId,
|
|
1258
|
+
run,
|
|
1259
|
+
disconfirmationRun,
|
|
1260
|
+
controlRun,
|
|
1261
|
+
marker,
|
|
1262
|
+
livenessMarker,
|
|
1263
|
+
);
|
|
1145
1264
|
const record = result.record;
|
|
1146
1265
|
return {
|
|
1147
1266
|
content: [
|
|
@@ -1487,6 +1606,11 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1487
1606
|
handler: async (args, ctx) => {
|
|
1488
1607
|
const next = parseXpModeArg(args ?? "", readXpMode());
|
|
1489
1608
|
writeXpMode(next);
|
|
1609
|
+
// Re-enabling after a mid-session /xp off must re-inject the workflow
|
|
1610
|
+
// on the next prompt — otherwise workflowInjected (module-level, set on
|
|
1611
|
+
// first enable) stays true and the workflow never comes back until the
|
|
1612
|
+
// process restarts.
|
|
1613
|
+
if (next !== "off") workflowInjected = false;
|
|
1490
1614
|
ctx.ui.notify(
|
|
1491
1615
|
`Casefile XP mode: ${next.toUpperCase()} (takes effect on the next prompt)`,
|
|
1492
1616
|
next === "on" ? "info" : "warning",
|