@xaccefy/pi-casefile 0.10.1 → 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 +2 -3
- package/package.json +1 -2
- package/src/confirmation.ts +45 -351
- package/src/evidence.ts +59 -220
- package/src/harness-verify.ts +36 -206
- package/src/index.ts +44 -366
- package/src/ledger.ts +24 -71
- package/src/poc-runner.ts +3 -3
- package/src/oob-oracle.ts +0 -279
package/src/harness-verify.ts
CHANGED
|
@@ -1,60 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Harness-side
|
|
2
|
+
* Harness-side attack/baseline replay.
|
|
3
3
|
*
|
|
4
4
|
* The machine floor cannot trust a caller's self-reported `re_executed`
|
|
5
5
|
* boolean. This module makes the HARNESS re-send the evidence's `verify`
|
|
6
|
-
* request
|
|
7
|
-
*
|
|
8
|
-
* main agent supplies the predicate
|
|
9
|
-
* and predicate execution before the later
|
|
6
|
+
* (attack) request and the evidence's `baseline` (legitimate) request with
|
|
7
|
+
* its own HTTP client, applying the attack's `expect` predicates to BOTH
|
|
8
|
+
* responses. The main agent supplies the predicate and the baseline; the
|
|
9
|
+
* harness owns evidence acquisition and predicate execution before the later
|
|
10
|
+
* semantic review.
|
|
10
11
|
*
|
|
11
12
|
* Policy:
|
|
12
13
|
* - Private/internal hosts require explicit operator authorization; otherwise
|
|
13
14
|
* replay fails closed.
|
|
14
15
|
* - Redirects are manual and every hop is checked before it is fetched.
|
|
15
|
-
* -
|
|
16
|
+
* - The attack request must match while the baseline request must not.
|
|
16
17
|
*
|
|
17
18
|
* Undici's custom dispatcher pins the approved DNS result through connect;
|
|
18
19
|
* node:dns and node:net provide resolution and address classification.
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
|
-
import { createHash
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
22
23
|
import { lookup as dnsLookup } from "node:dns/promises";
|
|
23
24
|
import { isIP } from "node:net";
|
|
24
25
|
import { Worker } from "node:worker_threads";
|
|
25
26
|
import { isPublicIpAddress } from "@xaccefy/pi-shared";
|
|
26
27
|
import { Agent, fetch as undiciFetch } from "undici";
|
|
27
|
-
import {
|
|
28
|
+
import type { PoCEvidence, VerifyExpect } from "./evidence.ts";
|
|
28
29
|
|
|
29
30
|
// Public re-export makes the single-source classifier identity testable across
|
|
30
31
|
// the web tool and confirmation replay paths.
|
|
31
32
|
export { isPublicIpAddress } from "@xaccefy/pi-shared";
|
|
32
33
|
|
|
33
34
|
export type HarnessVerifyResult = {
|
|
34
|
-
/** true = the harness sent both
|
|
35
|
+
/** true = the harness sent both attack and baseline requests and judged them. */
|
|
35
36
|
attempted: boolean;
|
|
36
|
-
/** Present when attempted:
|
|
37
|
+
/** Present when attempted: attack matched and baseline did not. */
|
|
37
38
|
pass?: boolean;
|
|
38
39
|
/** Backward-compatible target status summary. */
|
|
39
40
|
status?: number;
|
|
40
|
-
/** Machine-observed
|
|
41
|
+
/** Machine-observed attack/baseline response summaries. */
|
|
41
42
|
target?: HarnessResponseObservation;
|
|
42
43
|
control?: HarnessResponseObservation;
|
|
43
44
|
differential?: "target_only" | "both" | "control_only" | "neither";
|
|
44
|
-
/** Independent harness-generated reflection signal, when the template supports one. */
|
|
45
|
-
canary?: HarnessCanaryResult;
|
|
46
|
-
/** Honest machine claim: predicates alone, or predicates plus a causal canary. */
|
|
47
|
-
proofStrength?: "predicate_differential" | "canary_differential";
|
|
48
|
-
note: string;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export type HarnessCanaryResult = {
|
|
52
|
-
mode: "reflection";
|
|
53
|
-
attempted: boolean;
|
|
54
|
-
pass?: boolean;
|
|
55
|
-
tokenSha256: string;
|
|
56
|
-
targetObserved?: boolean;
|
|
57
|
-
controlObserved?: boolean;
|
|
58
45
|
note: string;
|
|
59
46
|
};
|
|
60
47
|
|
|
@@ -65,7 +52,6 @@ export type HarnessResponseObservation = {
|
|
|
65
52
|
url: string;
|
|
66
53
|
bodySha256?: string;
|
|
67
54
|
bodyBytes?: number;
|
|
68
|
-
canaryObserved?: boolean;
|
|
69
55
|
note: string;
|
|
70
56
|
};
|
|
71
57
|
|
|
@@ -120,38 +106,6 @@ function effectivePort(url: URL): string {
|
|
|
120
106
|
return url.port || (url.protocol === "https:" ? "443" : "80");
|
|
121
107
|
}
|
|
122
108
|
|
|
123
|
-
function sameTargetIdentity(left: string, right: string): boolean {
|
|
124
|
-
const a = parseNetworkTarget(left);
|
|
125
|
-
const b = parseNetworkTarget(right);
|
|
126
|
-
if (!a || !b) return false;
|
|
127
|
-
if (normHost(a.url.hostname) !== normHost(b.url.hostname)) {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
if ((a.url.port || b.url.port) && effectivePort(a.url) !== effectivePort(b.url)) return false;
|
|
131
|
-
return !(a.explicitProtocol && b.explicitProtocol && a.url.protocol !== b.url.protocol);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* A control is a trust anchor, not an agent invention. The operator supplies
|
|
136
|
-
* an allowlist of approved control origins/hosts through the process env.
|
|
137
|
-
*/
|
|
138
|
-
export function controlTargetAuthorizationError(
|
|
139
|
-
controlTarget: string,
|
|
140
|
-
allowedRaw: string | undefined = process.env.PI_POC_CONTROL_TARGETS,
|
|
141
|
-
): string | undefined {
|
|
142
|
-
const allowed = (allowedRaw ?? "")
|
|
143
|
-
.split(/[,\n]/)
|
|
144
|
-
.map((value) => value.trim())
|
|
145
|
-
.filter(Boolean);
|
|
146
|
-
if (allowed.length === 0) {
|
|
147
|
-
return "no operator-approved controls are configured in PI_POC_CONTROL_TARGETS";
|
|
148
|
-
}
|
|
149
|
-
if (!allowed.some((candidate) => sameTargetIdentity(candidate, controlTarget))) {
|
|
150
|
-
return `control target ${controlTarget} is not present in the operator-approved PI_POC_CONTROL_TARGETS allowlist`;
|
|
151
|
-
}
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
109
|
/**
|
|
156
110
|
* Bind a model-authored verify URL to the target the harness actually ran.
|
|
157
111
|
* A bare target permits either HTTP scheme; an explicit target URL binds the
|
|
@@ -183,16 +137,6 @@ export function verifyUrlBindingError(verifyUrl: string, target: string): string
|
|
|
183
137
|
return;
|
|
184
138
|
}
|
|
185
139
|
|
|
186
|
-
function controlUrlFor(targetVerifyUrl: string, controlTarget: string): URL | undefined {
|
|
187
|
-
const control = parseNetworkTarget(controlTarget);
|
|
188
|
-
if (!control) return;
|
|
189
|
-
const target = new URL(targetVerifyUrl);
|
|
190
|
-
const url = new URL(control.url.origin);
|
|
191
|
-
url.pathname = target.pathname;
|
|
192
|
-
url.search = target.search;
|
|
193
|
-
return url;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
140
|
// ── Predicate evaluation ──────────────────────────────────────────────
|
|
197
141
|
|
|
198
142
|
const REGEX_WORKER_SOURCE = `
|
|
@@ -356,7 +300,6 @@ async function fetchPinned(
|
|
|
356
300
|
async function replayRequest(
|
|
357
301
|
verify: PoCEvidence["verify"],
|
|
358
302
|
expect: VerifyExpect,
|
|
359
|
-
canaryToken?: string,
|
|
360
303
|
opts?: ReplayOptions,
|
|
361
304
|
): Promise<HarnessResponseObservation> {
|
|
362
305
|
let url: URL;
|
|
@@ -369,8 +312,6 @@ async function replayRequest(
|
|
|
369
312
|
note: `verify.url unparseable (${verify.url})`,
|
|
370
313
|
};
|
|
371
314
|
}
|
|
372
|
-
const observedUrl = () =>
|
|
373
|
-
canaryToken ? url.toString().replaceAll(canaryToken, POC_CANARY_PLACEHOLDER) : url.toString();
|
|
374
315
|
if (!(url.protocol === "http:" || url.protocol === "https:")) {
|
|
375
316
|
return { attempted: false, url: verify.url, note: `verify.url protocol ${url.protocol}` };
|
|
376
317
|
}
|
|
@@ -383,7 +324,7 @@ async function replayRequest(
|
|
|
383
324
|
} catch (error) {
|
|
384
325
|
return {
|
|
385
326
|
attempted: false,
|
|
386
|
-
url:
|
|
327
|
+
url: verify.url,
|
|
387
328
|
note: `verify.headers invalid: ${(error as Error).message}`,
|
|
388
329
|
};
|
|
389
330
|
}
|
|
@@ -398,7 +339,7 @@ async function replayRequest(
|
|
|
398
339
|
if (!opts?.allowPrivate && localName) {
|
|
399
340
|
return {
|
|
400
341
|
attempted: false,
|
|
401
|
-
url:
|
|
342
|
+
url: url.toString(),
|
|
402
343
|
note: `${url.hostname} is a private/internal host; operator authorization is required for harness replay`,
|
|
403
344
|
};
|
|
404
345
|
}
|
|
@@ -409,14 +350,14 @@ async function replayRequest(
|
|
|
409
350
|
} catch (error) {
|
|
410
351
|
return {
|
|
411
352
|
attempted: true,
|
|
412
|
-
url:
|
|
353
|
+
url: url.toString(),
|
|
413
354
|
note: `request errored (DNS): ${(error as Error).message}`,
|
|
414
355
|
};
|
|
415
356
|
}
|
|
416
357
|
if (addresses.length === 0) {
|
|
417
358
|
return {
|
|
418
359
|
attempted: true,
|
|
419
|
-
url:
|
|
360
|
+
url: url.toString(),
|
|
420
361
|
note: `request errored (DNS): ${url.hostname} resolved to no addresses`,
|
|
421
362
|
};
|
|
422
363
|
}
|
|
@@ -429,7 +370,7 @@ async function replayRequest(
|
|
|
429
370
|
if (!opts?.allowPrivate && addresses.some((address) => !isPublicIpAddress(address.address))) {
|
|
430
371
|
return {
|
|
431
372
|
attempted: false,
|
|
432
|
-
url:
|
|
373
|
+
url: url.toString(),
|
|
433
374
|
note: `${url.hostname} is a private/internal host; operator authorization is required for harness replay`,
|
|
434
375
|
};
|
|
435
376
|
}
|
|
@@ -457,7 +398,7 @@ async function replayRequest(
|
|
|
457
398
|
return {
|
|
458
399
|
attempted: true,
|
|
459
400
|
status: res.status,
|
|
460
|
-
url:
|
|
401
|
+
url: url.toString(),
|
|
461
402
|
note: `redirect limit exceeded (${MAX_REDIRECTS})`,
|
|
462
403
|
};
|
|
463
404
|
}
|
|
@@ -469,7 +410,7 @@ async function replayRequest(
|
|
|
469
410
|
return {
|
|
470
411
|
attempted: true,
|
|
471
412
|
status: res.status,
|
|
472
|
-
url:
|
|
413
|
+
url: url.toString(),
|
|
473
414
|
note: `redirected to disallowed protocol ${next.protocol}`,
|
|
474
415
|
};
|
|
475
416
|
}
|
|
@@ -480,7 +421,7 @@ async function replayRequest(
|
|
|
480
421
|
return {
|
|
481
422
|
attempted: true,
|
|
482
423
|
status: res.status,
|
|
483
|
-
url:
|
|
424
|
+
url: url.toString(),
|
|
484
425
|
note: `redirect left the bound host (${url.hostname} -> ${next.hostname})`,
|
|
485
426
|
};
|
|
486
427
|
}
|
|
@@ -512,22 +453,20 @@ async function replayRequest(
|
|
|
512
453
|
return {
|
|
513
454
|
attempted: true,
|
|
514
455
|
status: res.status,
|
|
515
|
-
url:
|
|
456
|
+
url: url.toString(),
|
|
516
457
|
bodySha256: observed.sha256,
|
|
517
458
|
bodyBytes: observed.bytes,
|
|
518
459
|
note: "response body exceeded the 2 MiB capture limit; matcher result is inconclusive",
|
|
519
460
|
};
|
|
520
461
|
}
|
|
521
462
|
const failures = await evaluateExpect(expect, res.status, observed.text);
|
|
522
|
-
const canaryObserved = canaryToken ? observed.text.includes(canaryToken) : undefined;
|
|
523
463
|
return {
|
|
524
464
|
attempted: true,
|
|
525
465
|
matched: failures.length === 0,
|
|
526
466
|
status: res.status,
|
|
527
|
-
url:
|
|
467
|
+
url: url.toString(),
|
|
528
468
|
bodySha256: observed.sha256,
|
|
529
469
|
bodyBytes: observed.bytes,
|
|
530
|
-
canaryObserved,
|
|
531
470
|
note:
|
|
532
471
|
failures.length === 0
|
|
533
472
|
? `status ${res.status}, all predicates matched`
|
|
@@ -537,13 +476,13 @@ async function replayRequest(
|
|
|
537
476
|
await closeFetched?.().catch(() => undefined);
|
|
538
477
|
return {
|
|
539
478
|
attempted: true,
|
|
540
|
-
url:
|
|
479
|
+
url: url.toString(),
|
|
541
480
|
note: `request errored (DNS/TLS/timeout): ${(e as Error).message}`,
|
|
542
481
|
};
|
|
543
482
|
}
|
|
544
483
|
}
|
|
545
484
|
|
|
546
|
-
return { attempted: true, url:
|
|
485
|
+
return { attempted: true, url: url.toString(), note: "unreachable redirect state" };
|
|
547
486
|
}
|
|
548
487
|
|
|
549
488
|
/**
|
|
@@ -569,58 +508,14 @@ export function sameRequest(
|
|
|
569
508
|
);
|
|
570
509
|
}
|
|
571
510
|
|
|
572
|
-
function injectCanary(
|
|
573
|
-
verify: PoCEvidence["verify"],
|
|
574
|
-
token: string | undefined,
|
|
575
|
-
): PoCEvidence["verify"] {
|
|
576
|
-
if (!token) return verify;
|
|
577
|
-
const replace = (value: string) => value.replace(POC_CANARY_PLACEHOLDER, token);
|
|
578
|
-
return {
|
|
579
|
-
...verify,
|
|
580
|
-
url: replace(verify.url),
|
|
581
|
-
body: verify.body === undefined ? undefined : replace(verify.body),
|
|
582
|
-
headers:
|
|
583
|
-
verify.headers === undefined
|
|
584
|
-
? undefined
|
|
585
|
-
: Object.fromEntries(
|
|
586
|
-
Object.entries(verify.headers).map(([key, value]) => [key, replace(value)]),
|
|
587
|
-
),
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function canaryResult(
|
|
592
|
-
token: string | undefined,
|
|
593
|
-
target: HarnessResponseObservation,
|
|
594
|
-
control?: HarnessResponseObservation,
|
|
595
|
-
): HarnessCanaryResult | undefined {
|
|
596
|
-
if (!token) return;
|
|
597
|
-
const attempted =
|
|
598
|
-
target.canaryObserved !== undefined && (control ? control.canaryObserved !== undefined : true);
|
|
599
|
-
const pass = control
|
|
600
|
-
? attempted && target.canaryObserved === true && control.canaryObserved === false
|
|
601
|
-
: attempted && target.canaryObserved === true;
|
|
602
|
-
return {
|
|
603
|
-
mode: "reflection",
|
|
604
|
-
attempted,
|
|
605
|
-
pass,
|
|
606
|
-
tokenSha256: createHash("sha256").update(token).digest("hex"),
|
|
607
|
-
targetObserved: target.canaryObserved,
|
|
608
|
-
controlObserved: control?.canaryObserved,
|
|
609
|
-
note: control
|
|
610
|
-
? `canary ${pass ? "target-only" : "failed"}: target=${String(target.canaryObserved)}, control=${String(control.canaryObserved)}`
|
|
611
|
-
: `canary ${pass ? "observed" : "not observed"} on target`,
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
|
|
615
511
|
/**
|
|
616
512
|
* Combine the two observations into the differential verdict. Shared by the
|
|
617
|
-
*
|
|
618
|
-
* baseline
|
|
513
|
+
* attack-vs-baseline replay: "target" is the attack request, "control" the
|
|
514
|
+
* legitimate baseline request.
|
|
619
515
|
*/
|
|
620
516
|
function judgeDifferential(
|
|
621
517
|
target: HarnessResponseObservation,
|
|
622
518
|
control: HarnessResponseObservation,
|
|
623
|
-
token: string | undefined,
|
|
624
519
|
label: { kind: string; a: string; b: string },
|
|
625
520
|
): HarnessVerifyResult {
|
|
626
521
|
const attempted = target.attempted && control.attempted;
|
|
@@ -634,12 +529,7 @@ function judgeDifferential(
|
|
|
634
529
|
? "control_only"
|
|
635
530
|
: "neither"
|
|
636
531
|
: undefined;
|
|
637
|
-
const
|
|
638
|
-
const pass =
|
|
639
|
-
attempted &&
|
|
640
|
-
conclusive &&
|
|
641
|
-
differential === "target_only" &&
|
|
642
|
-
(canary === undefined || canary.pass === true);
|
|
532
|
+
const pass = attempted && conclusive && differential === "target_only";
|
|
643
533
|
return {
|
|
644
534
|
attempted,
|
|
645
535
|
pass,
|
|
@@ -647,72 +537,16 @@ function judgeDifferential(
|
|
|
647
537
|
target,
|
|
648
538
|
control,
|
|
649
539
|
differential,
|
|
650
|
-
canary,
|
|
651
|
-
proofStrength: canary?.pass ? "canary_differential" : "predicate_differential",
|
|
652
540
|
note:
|
|
653
541
|
`harness ${label.kind} ${differential ?? "inconclusive"}: ${label.a} (${target.note}); ` +
|
|
654
|
-
`${label.b} (${control.note})
|
|
542
|
+
`${label.b} (${control.note})`,
|
|
655
543
|
};
|
|
656
544
|
}
|
|
657
545
|
|
|
658
546
|
/**
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
* changing only the origin to the declared control target.
|
|
663
|
-
*/
|
|
664
|
-
export async function replayDifferential(
|
|
665
|
-
evidence: PoCEvidence,
|
|
666
|
-
caseTarget: string,
|
|
667
|
-
controlTarget: string,
|
|
668
|
-
opts?: ReplayOptions,
|
|
669
|
-
): Promise<HarnessVerifyResult> {
|
|
670
|
-
const bindingError = verifyUrlBindingError(evidence.verify.url, caseTarget);
|
|
671
|
-
if (bindingError) {
|
|
672
|
-
return { attempted: false, pass: false, note: `target binding failed: ${bindingError}` };
|
|
673
|
-
}
|
|
674
|
-
if (sameTargetIdentity(caseTarget, controlTarget)) {
|
|
675
|
-
return {
|
|
676
|
-
attempted: false,
|
|
677
|
-
pass: false,
|
|
678
|
-
note: "control target resolves to the same network identity as the case target",
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
const controlUrl = controlUrlFor(evidence.verify.url, controlTarget);
|
|
682
|
-
if (!controlUrl) {
|
|
683
|
-
return {
|
|
684
|
-
attempted: false,
|
|
685
|
-
pass: false,
|
|
686
|
-
note: `control target is not an HTTP network target: ${controlTarget}`,
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
const token = evidence.verify.canary
|
|
691
|
-
? `poc_canary_${randomBytes(24).toString("hex")}`
|
|
692
|
-
: undefined;
|
|
693
|
-
const targetVerify = injectCanary(evidence.verify, token);
|
|
694
|
-
const target = await replayRequest(targetVerify, targetVerify.expect, token, opts);
|
|
695
|
-
const control = await replayRequest(
|
|
696
|
-
{
|
|
697
|
-
...targetVerify,
|
|
698
|
-
url: injectCanary({ ...evidence.verify, url: controlUrl.toString() }, token).url,
|
|
699
|
-
},
|
|
700
|
-
targetVerify.expect,
|
|
701
|
-
token,
|
|
702
|
-
opts,
|
|
703
|
-
);
|
|
704
|
-
return judgeDifferential(target, control, token, {
|
|
705
|
-
kind: "differential",
|
|
706
|
-
a: "target",
|
|
707
|
-
b: "control",
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
/**
|
|
712
|
-
* Same-host differential (Tier 2, intra-target). For access-control and
|
|
713
|
-
* business-logic classes the discriminating variable is the attacker's
|
|
714
|
-
* identity or a request parameter, NOT the host — so the sound baseline is a
|
|
715
|
-
* legitimate request to the SAME target, not the same request to another host.
|
|
547
|
+
* Same-host differential (intra-target). The discriminating variable is the
|
|
548
|
+
* attacker's identity or a request parameter, not the host — the sound
|
|
549
|
+
* baseline is a legitimate request to the SAME target.
|
|
716
550
|
* The harness sends the attack request and the model-declared `evidence.baseline`
|
|
717
551
|
* request to the case target, applies the attack's `verify.expect` predicates to
|
|
718
552
|
* BOTH responses, and passes only when the proof appears on the attack response
|
|
@@ -749,13 +583,9 @@ export async function replayIntraTarget(
|
|
|
749
583
|
};
|
|
750
584
|
}
|
|
751
585
|
|
|
752
|
-
const
|
|
753
|
-
? `poc_canary_${randomBytes(24).toString("hex")}`
|
|
754
|
-
: undefined;
|
|
755
|
-
const attackVerify = injectCanary(evidence.verify, token);
|
|
756
|
-
const attack = await replayRequest(attackVerify, attackVerify.expect, token, opts);
|
|
586
|
+
const attack = await replayRequest(evidence.verify, evidence.verify.expect, opts);
|
|
757
587
|
// The baseline carries the attack's predicates: the proof must be ABSENT here.
|
|
758
|
-
const
|
|
588
|
+
const base = await replayRequest(
|
|
759
589
|
{
|
|
760
590
|
...evidence.verify,
|
|
761
591
|
method: baseline.method,
|
|
@@ -763,11 +593,11 @@ export async function replayIntraTarget(
|
|
|
763
593
|
headers: baseline.headers,
|
|
764
594
|
body: baseline.body,
|
|
765
595
|
},
|
|
766
|
-
|
|
596
|
+
evidence.verify.expect,
|
|
597
|
+
opts,
|
|
767
598
|
);
|
|
768
|
-
const base = await replayRequest(baselineVerify, attackVerify.expect, token, opts);
|
|
769
599
|
|
|
770
|
-
return judgeDifferential(attack, base,
|
|
600
|
+
return judgeDifferential(attack, base, {
|
|
771
601
|
kind: "intra-target",
|
|
772
602
|
a: "attack",
|
|
773
603
|
b: "baseline",
|