@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/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 and the request spec the harness
6
- * executes against both target and control. The harness validates the file,
7
- * binds it to the run via $PI_POC_NONCE, and acquires the responses itself.
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
- /** Request spec the harness executes in phase 1 and again from the main-agent phase-2 call. */
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
- /** Optional baseline request for the main agent's differential review. */
60
- baseline?: {
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.canary !== undefined) {
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: 'evidence.json verify.mode must be "inter_host" or "intra_target"',
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.mode === "intra_target" && !isRecord(raw.baseline)) {
219
+ if (verify.canary !== undefined) {
271
220
  return {
272
221
  ok: false,
273
222
  error:
274
- "evidence.json verify.mode intra_target requires baseline a legitimate same-host request whose response must NOT satisfy the attack predicate",
223
+ "evidence.json verify.canary is removedthe 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
- if (raw.baseline !== undefined) {
336
- const b = raw.baseline;
337
- if (!isRecord(b)) return { ok: false, error: "evidence.json baseline must be an object" };
338
- if (
339
- !nonEmptyString(b.method) ||
340
- !HTTP_METHODS.includes(b.method.toUpperCase()) ||
341
- !httpUrl(b.url)
342
- ) {
343
- return {
344
- ok: false,
345
- error: "evidence.json baseline needs an http(s) url and a valid HTTP method",
346
- };
347
- }
348
- if (b.headers !== undefined && !headerRecord(b.headers)) {
349
- return {
350
- ok: false,
351
- error:
352
- "evidence.json baseline.headers must be bounded valid end-to-end HTTP headers; authority, framing, proxy, and hop-by-hop headers are forbidden",
353
- };
354
- }
355
- if (
356
- b.body !== undefined &&
357
- (typeof b.body !== "string" || b.body.length > MAX_REQUEST_BODY_CHARS)
358
- ) {
359
- return {
360
- ok: false,
361
- error: `evidence.json baseline.body must be no longer than ${MAX_REQUEST_BODY_CHARS} characters`,
362
- };
363
- }
364
- if (
365
- b.body_contains !== undefined &&
366
- !boundedStringArray(b.body_contains, MAX_EXPECT_VALUES, MAX_EXPECT_CHARS)
367
- ) {
368
- return { ok: false, error: "evidence.json baseline.body_contains exceeds limits" };
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 its review and fresh harness replay. */
406
+ /** What the main agent observed during review of the runs and transcripts. */
413
407
  re_execution_note?: string;
414
- /** Target vs control evidence comparison. CONFIRMED requires target_only. */
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
- * ledger separately requires a fresh harness-owned phase-2 replay; there is no
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 control run must not demonstrate the claimed impact',
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 and the fresh harness replay",
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
  }