@xaccefy/pi-casefile 0.10.0 → 0.11.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/README.md +6 -6
- package/package.json +1 -2
- package/src/confirmation.ts +239 -323
- package/src/evidence.ts +101 -135
- package/src/harness-verify.ts +54 -247
- package/src/index.ts +188 -745
- package/src/ledger-internal.ts +118 -4
- package/src/ledger.ts +459 -175
- package/src/poc-runner.ts +43 -14
- package/src/scratchpad.ts +88 -143
- package/src/workflow.ts +6 -2
- package/src/oob-oracle.ts +0 -279
package/src/evidence.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Evidence contract for PoC confirmation.
|
|
3
3
|
*
|
|
4
4
|
* A PoC must write `evidence.json` into $PI_POC_EVIDENCE_DIR: a nonce-bound,
|
|
5
|
-
* schema-validated record of what it claims
|
|
6
|
-
*
|
|
7
|
-
* binds it to the run via $PI_POC_NONCE, and acquires
|
|
5
|
+
* schema-validated record of what it claims, the attack request spec, and a
|
|
6
|
+
* legitimate same-host baseline request. The harness validates the file,
|
|
7
|
+
* binds it to the run via $PI_POC_NONCE, and acquires both responses itself.
|
|
8
8
|
* The main/coordinator agent remains the semantic reviewer after the machine differential.
|
|
9
9
|
*
|
|
10
10
|
* Exit zero is required run integrity, but never vulnerability proof.
|
|
@@ -21,43 +21,26 @@ export type VerifyExpect = {
|
|
|
21
21
|
body_regex?: string[];
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
/** Replaced only inside the harness replay, after the PoC process has exited. */
|
|
25
|
-
export const POC_CANARY_PLACEHOLDER = "{{PI_POC_CANARY}}";
|
|
26
|
-
|
|
27
|
-
export type VerifyCanary = {
|
|
28
|
-
/** Reflection is machine-checked: target must return the fresh token and control must not. */
|
|
29
|
-
mode: "reflection";
|
|
30
|
-
/** Fixed literal; callers cannot choose or predict the harness-generated token. */
|
|
31
|
-
placeholder: typeof POC_CANARY_PLACEHOLDER;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
24
|
export type PoCEvidence = {
|
|
35
25
|
/** Must equal the run's PI_POC_NONCE (harness-verified). */
|
|
36
26
|
nonce: string;
|
|
37
27
|
/** What the exploit asserts, e.g. "read /etc/passwd of target". */
|
|
38
28
|
claim: string;
|
|
39
|
-
/**
|
|
29
|
+
/** Attack request spec the harness executes to acquire machine evidence. */
|
|
40
30
|
verify: {
|
|
41
31
|
method: string;
|
|
42
32
|
url: string;
|
|
43
33
|
headers?: Record<string, string>;
|
|
44
34
|
body?: string;
|
|
45
35
|
expect: VerifyExpect;
|
|
46
|
-
/** Optional stronger causality dimension, independent of the authored predicate. */
|
|
47
|
-
canary?: VerifyCanary;
|
|
48
|
-
/**
|
|
49
|
-
* Differential shape. "inter_host" (default) = same request to target vs a
|
|
50
|
-
* distinct patched control host (body-carried proof). "intra_target" = attack
|
|
51
|
-
* request vs a legitimate same-host `baseline` request (access-control /
|
|
52
|
-
* business-logic classes, where the discriminating variable is identity or a
|
|
53
|
-
* parameter, not the host) — requires `baseline`.
|
|
54
|
-
*/
|
|
55
|
-
mode?: "inter_host" | "intra_target";
|
|
56
36
|
};
|
|
57
37
|
/** What the script itself saw — corroboration only, never proof. */
|
|
58
38
|
observations: string[];
|
|
59
|
-
/**
|
|
60
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Legitimate same-host request whose response must NOT satisfy the attack
|
|
41
|
+
* predicate — the differential control (identity or parameter varies, not host).
|
|
42
|
+
*/
|
|
43
|
+
baseline: {
|
|
61
44
|
method: string;
|
|
62
45
|
url: string;
|
|
63
46
|
headers?: Record<string, string>;
|
|
@@ -184,10 +167,6 @@ function hasDiscriminatingBodyExpectation(expect: Record<string, unknown>): bool
|
|
|
184
167
|
);
|
|
185
168
|
}
|
|
186
169
|
|
|
187
|
-
function countOccurrences(value: string, needle: string): number {
|
|
188
|
-
return value.split(needle).length - 1;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
170
|
/**
|
|
192
171
|
* Parse + validate a PoC's evidence.json. Returns the validated object or a
|
|
193
172
|
* field-level error. Deliberately strict: an invalid evidence file means the
|
|
@@ -230,48 +209,18 @@ export function parsePoCEvidence(
|
|
|
230
209
|
error: `evidence.json verify.body must be a string no longer than ${MAX_REQUEST_BODY_CHARS} characters`,
|
|
231
210
|
};
|
|
232
211
|
}
|
|
233
|
-
if (verify.
|
|
234
|
-
if (
|
|
235
|
-
!isRecord(verify.canary) ||
|
|
236
|
-
verify.canary.mode !== "reflection" ||
|
|
237
|
-
verify.canary.placeholder !== POC_CANARY_PLACEHOLDER
|
|
238
|
-
) {
|
|
239
|
-
return {
|
|
240
|
-
ok: false,
|
|
241
|
-
error: `evidence.json verify.canary must be {"mode":"reflection","placeholder":"${POC_CANARY_PLACEHOLDER}"}`,
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
const canaryLocations = [
|
|
245
|
-
verify.url,
|
|
246
|
-
typeof verify.body === "string" ? verify.body : "",
|
|
247
|
-
...(isRecord(verify.headers)
|
|
248
|
-
? Object.values(verify.headers).filter(
|
|
249
|
-
(value): value is string => typeof value === "string",
|
|
250
|
-
)
|
|
251
|
-
: []),
|
|
252
|
-
];
|
|
253
|
-
const count = canaryLocations.reduce(
|
|
254
|
-
(total, value) => total + countOccurrences(value, POC_CANARY_PLACEHOLDER),
|
|
255
|
-
0,
|
|
256
|
-
);
|
|
257
|
-
if (count !== 1) {
|
|
258
|
-
return {
|
|
259
|
-
ok: false,
|
|
260
|
-
error: `evidence.json verify.canary requires exactly one ${POC_CANARY_PLACEHOLDER} placeholder across url, body, or header values (got ${count})`,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
if (verify.mode !== undefined && verify.mode !== "inter_host" && verify.mode !== "intra_target") {
|
|
212
|
+
if (verify.mode !== undefined) {
|
|
265
213
|
return {
|
|
266
214
|
ok: false,
|
|
267
|
-
error:
|
|
215
|
+
error:
|
|
216
|
+
"evidence.json verify.mode is removed — the attack-vs-baseline differential is the only confirmation model. Drop verify.mode and declare evidence.baseline.",
|
|
268
217
|
};
|
|
269
218
|
}
|
|
270
|
-
if (verify.
|
|
219
|
+
if (verify.canary !== undefined) {
|
|
271
220
|
return {
|
|
272
221
|
ok: false,
|
|
273
222
|
error:
|
|
274
|
-
"evidence.json verify.
|
|
223
|
+
"evidence.json verify.canary is removed — the reflection canary tier was retired. Remove verify.canary and any {{PI_POC_CANARY}} placeholder.",
|
|
275
224
|
};
|
|
276
225
|
}
|
|
277
226
|
const expect = verify.expect;
|
|
@@ -332,41 +281,45 @@ export function parsePoCEvidence(
|
|
|
332
281
|
) {
|
|
333
282
|
return { ok: false, error: "evidence.json observations exceed the bounded string-array limit" };
|
|
334
283
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
284
|
+
const b = raw.baseline;
|
|
285
|
+
if (!isRecord(b)) {
|
|
286
|
+
return {
|
|
287
|
+
ok: false,
|
|
288
|
+
error:
|
|
289
|
+
"evidence.json requires baseline — a legitimate same-host request whose response must NOT satisfy the attack predicate",
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (
|
|
293
|
+
!nonEmptyString(b.method) ||
|
|
294
|
+
!HTTP_METHODS.includes(b.method.toUpperCase()) ||
|
|
295
|
+
!httpUrl(b.url)
|
|
296
|
+
) {
|
|
297
|
+
return {
|
|
298
|
+
ok: false,
|
|
299
|
+
error: "evidence.json baseline needs an http(s) url and a valid HTTP method",
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (b.headers !== undefined && !headerRecord(b.headers)) {
|
|
303
|
+
return {
|
|
304
|
+
ok: false,
|
|
305
|
+
error:
|
|
306
|
+
"evidence.json baseline.headers must be bounded valid end-to-end HTTP headers; authority, framing, proxy, and hop-by-hop headers are forbidden",
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (
|
|
310
|
+
b.body !== undefined &&
|
|
311
|
+
(typeof b.body !== "string" || b.body.length > MAX_REQUEST_BODY_CHARS)
|
|
312
|
+
) {
|
|
313
|
+
return {
|
|
314
|
+
ok: false,
|
|
315
|
+
error: `evidence.json baseline.body must be no longer than ${MAX_REQUEST_BODY_CHARS} characters`,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (
|
|
319
|
+
b.body_contains !== undefined &&
|
|
320
|
+
!boundedStringArray(b.body_contains, MAX_EXPECT_VALUES, MAX_EXPECT_CHARS)
|
|
321
|
+
) {
|
|
322
|
+
return { ok: false, error: "evidence.json baseline.body_contains exceeds limits" };
|
|
370
323
|
}
|
|
371
324
|
return { ok: true, evidence: raw as unknown as PoCEvidence };
|
|
372
325
|
}
|
|
@@ -385,6 +338,48 @@ export function normalizeEvidence(e: PoCEvidence): string {
|
|
|
385
338
|
return JSON.stringify({ claim: e.claim, verify: e.verify, baseline: e.baseline });
|
|
386
339
|
}
|
|
387
340
|
|
|
341
|
+
// ── Artifact secret scanning (defense in depth) ──────────────────────
|
|
342
|
+
//
|
|
343
|
+
// Evidence artifacts are raw target responses and logs — they routinely
|
|
344
|
+
// contain live credentials. The gate never blocks storage (an engaged
|
|
345
|
+
// finding must keep its proof), it FLAGS the item so every later view can
|
|
346
|
+
// redact or warn. Best-effort pattern matching only: labels are recorded,
|
|
347
|
+
// matched VALUES are never persisted by the scanner itself.
|
|
348
|
+
|
|
349
|
+
/** Label → pattern. Linear regexes only (artifacts reach 10 MiB). */
|
|
350
|
+
const SECRET_PATTERNS: ReadonlyArray<{ label: string; pattern: RegExp }> = [
|
|
351
|
+
{ label: "aws-access-key", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
352
|
+
{ label: "google-api-key", pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
353
|
+
{ label: "github-token", pattern: /\bgh[pousr]_[A-Za-z0-9]{36,255}\b/g },
|
|
354
|
+
{ label: "slack-token", pattern: /\bxox[baprs]-[0-9A-Za-z-]{10,}\b/g },
|
|
355
|
+
{
|
|
356
|
+
label: "private-key-block",
|
|
357
|
+
pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/g,
|
|
358
|
+
},
|
|
359
|
+
{ label: "bearer-token", pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/gi },
|
|
360
|
+
{ label: "jwt", pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
|
|
361
|
+
{
|
|
362
|
+
label: "credential-assignment",
|
|
363
|
+
pattern:
|
|
364
|
+
/\b(?:api[_-]?key|apikey|secret|token|passwd|password)\b["']?\s*[:=]\s*["'][^"'\s]{12,}["']/gi,
|
|
365
|
+
},
|
|
366
|
+
];
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Scan artifact bytes for embedded secret material. Returns the LABELS of the
|
|
370
|
+
* patterns that matched (deduplicated, order of first match) — never the
|
|
371
|
+
* matched values themselves.
|
|
372
|
+
*/
|
|
373
|
+
export function scanArtifactForSecrets(bytes: Buffer): string[] {
|
|
374
|
+
const text = bytes.toString("utf8");
|
|
375
|
+
const labels: string[] = [];
|
|
376
|
+
for (const { label, pattern } of SECRET_PATTERNS) {
|
|
377
|
+
pattern.lastIndex = 0;
|
|
378
|
+
if (pattern.test(text)) labels.push(label);
|
|
379
|
+
}
|
|
380
|
+
return labels;
|
|
381
|
+
}
|
|
382
|
+
|
|
388
383
|
// ── Main-agent confirmation verdict ─────────────────────────────────
|
|
389
384
|
|
|
390
385
|
// INCONCLUSIVE is the fail-safe verdict: the reviewer could neither reproduce
|
|
@@ -402,25 +397,20 @@ export const CONFIRM_DIFFERENTIAL_VALUES = [
|
|
|
402
397
|
export type ConfirmDifferential = (typeof CONFIRM_DIFFERENTIAL_VALUES)[number];
|
|
403
398
|
|
|
404
399
|
export const SEVERITY_MATCH_VALUES = ["under", "over", "ok"] as const;
|
|
405
|
-
export const CANARY_ASSESSMENT_VALUES = ["verified", "not_applicable"] as const;
|
|
406
400
|
|
|
407
401
|
export type MainAgentVerdict = {
|
|
408
402
|
verdict: ConfirmVerdict;
|
|
409
403
|
reasoning: string;
|
|
410
404
|
/** Files/evidence the main agent actually reviewed. */
|
|
411
405
|
evidence_reviewed: string[];
|
|
412
|
-
/** What the main agent observed during
|
|
406
|
+
/** What the main agent observed during review of the runs and transcripts. */
|
|
413
407
|
re_execution_note?: string;
|
|
414
|
-
/**
|
|
408
|
+
/** Attack vs baseline evidence comparison. CONFIRMED requires target_only. */
|
|
415
409
|
differential: ConfirmDifferential;
|
|
416
410
|
/** Claimed severity vs what the evidence shows. */
|
|
417
411
|
severity_match?: (typeof SEVERITY_MATCH_VALUES)[number];
|
|
418
412
|
/** The main agent's own failed attempt to disprove — becomes the case's disconfirmation. */
|
|
419
413
|
disconfirmation_attempt?: string;
|
|
420
|
-
/** Whether the machine replay carried a harness-generated causal canary. */
|
|
421
|
-
canary_assessment?: (typeof CANARY_ASSESSMENT_VALUES)[number];
|
|
422
|
-
/** Why no meaningful canary oracle exists for this exploit class. */
|
|
423
|
-
canary_reason?: string;
|
|
424
414
|
/** Which model judged (recorded for the accuracy ledger). */
|
|
425
415
|
model?: string;
|
|
426
416
|
};
|
|
@@ -428,8 +418,8 @@ export type MainAgentVerdict = {
|
|
|
428
418
|
/**
|
|
429
419
|
* Validate the main-agent verdict. CONFIRMED additionally requires a target-only
|
|
430
420
|
* differential, a concrete review note, and a disconfirmation attempt. The
|
|
431
|
-
*
|
|
432
|
-
* caller-supplied `re_executed` checkbox.
|
|
421
|
+
* verdict is judged against the still-valid phase-1 evidence bundle; there is
|
|
422
|
+
* no caller-supplied `re_executed` checkbox.
|
|
433
423
|
*/
|
|
434
424
|
export function validateMainAgentVerdict(
|
|
435
425
|
raw: unknown,
|
|
@@ -460,18 +450,6 @@ export function validateMainAgentVerdict(
|
|
|
460
450
|
error: `verdict differential must be one of ${CONFIRM_DIFFERENTIAL_VALUES.join(" | ")}`,
|
|
461
451
|
};
|
|
462
452
|
}
|
|
463
|
-
if (
|
|
464
|
-
raw.canary_assessment !== undefined &&
|
|
465
|
-
!CANARY_ASSESSMENT_VALUES.includes(raw.canary_assessment as never)
|
|
466
|
-
) {
|
|
467
|
-
return {
|
|
468
|
-
ok: false,
|
|
469
|
-
error: `verdict canary_assessment must be one of ${CANARY_ASSESSMENT_VALUES.join(" | ")}`,
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
if (raw.canary_reason !== undefined && !nonEmptyString(raw.canary_reason)) {
|
|
473
|
-
return { ok: false, error: "verdict canary_reason must be a non-empty string" };
|
|
474
|
-
}
|
|
475
453
|
if (
|
|
476
454
|
raw.severity_match !== undefined &&
|
|
477
455
|
!SEVERITY_MATCH_VALUES.includes(raw.severity_match as never)
|
|
@@ -486,14 +464,14 @@ export function validateMainAgentVerdict(
|
|
|
486
464
|
return {
|
|
487
465
|
ok: false,
|
|
488
466
|
error:
|
|
489
|
-
'CONFIRMED requires differential "target_only" — the
|
|
467
|
+
'CONFIRMED requires differential "target_only" — the same-host baseline must not demonstrate the claimed impact',
|
|
490
468
|
};
|
|
491
469
|
}
|
|
492
470
|
if (!nonEmptyString(raw.re_execution_note)) {
|
|
493
471
|
return {
|
|
494
472
|
ok: false,
|
|
495
473
|
error:
|
|
496
|
-
"CONFIRMED requires re_execution_note — record what the main agent observed during review
|
|
474
|
+
"CONFIRMED requires re_execution_note — record what the main agent observed during review of the runs and transcripts",
|
|
497
475
|
};
|
|
498
476
|
}
|
|
499
477
|
if (!nonEmptyString(raw.disconfirmation_attempt)) {
|
|
@@ -503,18 +481,6 @@ export function validateMainAgentVerdict(
|
|
|
503
481
|
"CONFIRMED requires disconfirmation_attempt — the main agent's own failed attempt to disprove",
|
|
504
482
|
};
|
|
505
483
|
}
|
|
506
|
-
if (!CANARY_ASSESSMENT_VALUES.includes(raw.canary_assessment as never)) {
|
|
507
|
-
return {
|
|
508
|
-
ok: false,
|
|
509
|
-
error: `CONFIRMED requires canary_assessment (${CANARY_ASSESSMENT_VALUES.join(" | ")})`,
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
if (raw.canary_assessment === "not_applicable" && !nonEmptyString(raw.canary_reason)) {
|
|
513
|
-
return {
|
|
514
|
-
ok: false,
|
|
515
|
-
error: "CONFIRMED with canary_assessment not_applicable requires canary_reason",
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
484
|
}
|
|
519
485
|
return { ok: true, verdict: raw as unknown as MainAgentVerdict };
|
|
520
486
|
}
|