@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/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
|
|
|
@@ -76,8 +62,16 @@ const REGEX_TIMEOUT_MS = 250;
|
|
|
76
62
|
|
|
77
63
|
type ResolvedAddress = { address: string; family: 4 | 6 };
|
|
78
64
|
|
|
65
|
+
/** Comparison-normalize a hostname: lowercase, strip IPv6 brackets and any trailing root dot. */
|
|
66
|
+
function normHost(hostname: string): string {
|
|
67
|
+
return hostname
|
|
68
|
+
.toLowerCase()
|
|
69
|
+
.replace(/^\[|\]$/g, "")
|
|
70
|
+
.replace(/\.$/, "");
|
|
71
|
+
}
|
|
72
|
+
|
|
79
73
|
async function resolveHost(hostname: string): Promise<ResolvedAddress[]> {
|
|
80
|
-
const host = hostname
|
|
74
|
+
const host = normHost(hostname);
|
|
81
75
|
const literalFamily = isIP(host);
|
|
82
76
|
if (literalFamily) return [{ address: host, family: literalFamily as 4 | 6 }];
|
|
83
77
|
const lookup = dnsLookup(host, { all: true, verbatim: true }) as Promise<ResolvedAddress[]>;
|
|
@@ -112,41 +106,6 @@ function effectivePort(url: URL): string {
|
|
|
112
106
|
return url.port || (url.protocol === "https:" ? "443" : "80");
|
|
113
107
|
}
|
|
114
108
|
|
|
115
|
-
function sameTargetIdentity(left: string, right: string): boolean {
|
|
116
|
-
const a = parseNetworkTarget(left);
|
|
117
|
-
const b = parseNetworkTarget(right);
|
|
118
|
-
if (!a || !b) return false;
|
|
119
|
-
if (
|
|
120
|
-
a.url.hostname.toLowerCase().replace(/\.$/, "") !==
|
|
121
|
-
b.url.hostname.toLowerCase().replace(/\.$/, "")
|
|
122
|
-
) {
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
if ((a.url.port || b.url.port) && effectivePort(a.url) !== effectivePort(b.url)) return false;
|
|
126
|
-
return !(a.explicitProtocol && b.explicitProtocol && a.url.protocol !== b.url.protocol);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* A control is a trust anchor, not an agent invention. The operator supplies
|
|
131
|
-
* an allowlist of approved control origins/hosts through the process env.
|
|
132
|
-
*/
|
|
133
|
-
export function controlTargetAuthorizationError(
|
|
134
|
-
controlTarget: string,
|
|
135
|
-
allowedRaw: string | undefined = process.env.PI_POC_CONTROL_TARGETS,
|
|
136
|
-
): string | undefined {
|
|
137
|
-
const allowed = (allowedRaw ?? "")
|
|
138
|
-
.split(/[,\n]/)
|
|
139
|
-
.map((value) => value.trim())
|
|
140
|
-
.filter(Boolean);
|
|
141
|
-
if (allowed.length === 0) {
|
|
142
|
-
return "no operator-approved controls are configured in PI_POC_CONTROL_TARGETS";
|
|
143
|
-
}
|
|
144
|
-
if (!allowed.some((candidate) => sameTargetIdentity(candidate, controlTarget))) {
|
|
145
|
-
return `control target ${controlTarget} is not present in the operator-approved PI_POC_CONTROL_TARGETS allowlist`;
|
|
146
|
-
}
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
109
|
/**
|
|
151
110
|
* Bind a model-authored verify URL to the target the harness actually ran.
|
|
152
111
|
* A bare target permits either HTTP scheme; an explicit target URL binds the
|
|
@@ -161,8 +120,8 @@ export function verifyUrlBindingError(verifyUrl: string, target: string): string
|
|
|
161
120
|
return `verify.url is not parseable: ${verifyUrl}`;
|
|
162
121
|
}
|
|
163
122
|
if (!declared) return `target is not an HTTP network target: ${target}`;
|
|
164
|
-
const declaredHost = declared.url.hostname
|
|
165
|
-
const observedHost = observed.hostname
|
|
123
|
+
const declaredHost = normHost(declared.url.hostname);
|
|
124
|
+
const observedHost = normHost(observed.hostname);
|
|
166
125
|
if (declaredHost !== observedHost) {
|
|
167
126
|
return `verify.url host ${observedHost} does not match run target ${declaredHost}`;
|
|
168
127
|
}
|
|
@@ -178,16 +137,6 @@ export function verifyUrlBindingError(verifyUrl: string, target: string): string
|
|
|
178
137
|
return;
|
|
179
138
|
}
|
|
180
139
|
|
|
181
|
-
function controlUrlFor(targetVerifyUrl: string, controlTarget: string): URL | undefined {
|
|
182
|
-
const control = parseNetworkTarget(controlTarget);
|
|
183
|
-
if (!control) return;
|
|
184
|
-
const target = new URL(targetVerifyUrl);
|
|
185
|
-
const url = new URL(control.url.origin);
|
|
186
|
-
url.pathname = target.pathname;
|
|
187
|
-
url.search = target.search;
|
|
188
|
-
return url;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
140
|
// ── Predicate evaluation ──────────────────────────────────────────────
|
|
192
141
|
|
|
193
142
|
const REGEX_WORKER_SOURCE = `
|
|
@@ -351,7 +300,6 @@ async function fetchPinned(
|
|
|
351
300
|
async function replayRequest(
|
|
352
301
|
verify: PoCEvidence["verify"],
|
|
353
302
|
expect: VerifyExpect,
|
|
354
|
-
canaryToken?: string,
|
|
355
303
|
opts?: ReplayOptions,
|
|
356
304
|
): Promise<HarnessResponseObservation> {
|
|
357
305
|
let url: URL;
|
|
@@ -364,8 +312,6 @@ async function replayRequest(
|
|
|
364
312
|
note: `verify.url unparseable (${verify.url})`,
|
|
365
313
|
};
|
|
366
314
|
}
|
|
367
|
-
const observedUrl = () =>
|
|
368
|
-
canaryToken ? url.toString().replaceAll(canaryToken, POC_CANARY_PLACEHOLDER) : url.toString();
|
|
369
315
|
if (!(url.protocol === "http:" || url.protocol === "https:")) {
|
|
370
316
|
return { attempted: false, url: verify.url, note: `verify.url protocol ${url.protocol}` };
|
|
371
317
|
}
|
|
@@ -378,12 +324,12 @@ async function replayRequest(
|
|
|
378
324
|
} catch (error) {
|
|
379
325
|
return {
|
|
380
326
|
attempted: false,
|
|
381
|
-
url:
|
|
327
|
+
url: verify.url,
|
|
382
328
|
note: `verify.headers invalid: ${(error as Error).message}`,
|
|
383
329
|
};
|
|
384
330
|
}
|
|
385
331
|
const signal = AbortSignal.timeout(opts?.timeoutMs ?? TIMEOUT_MS);
|
|
386
|
-
const lockedHostname = url.hostname
|
|
332
|
+
const lockedHostname = normHost(url.hostname);
|
|
387
333
|
const fetchImpl = opts?.fetchImpl ?? harnessFetchForTest;
|
|
388
334
|
|
|
389
335
|
for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects++) {
|
|
@@ -393,7 +339,7 @@ async function replayRequest(
|
|
|
393
339
|
if (!opts?.allowPrivate && localName) {
|
|
394
340
|
return {
|
|
395
341
|
attempted: false,
|
|
396
|
-
url:
|
|
342
|
+
url: url.toString(),
|
|
397
343
|
note: `${url.hostname} is a private/internal host; operator authorization is required for harness replay`,
|
|
398
344
|
};
|
|
399
345
|
}
|
|
@@ -404,29 +350,27 @@ async function replayRequest(
|
|
|
404
350
|
} catch (error) {
|
|
405
351
|
return {
|
|
406
352
|
attempted: true,
|
|
407
|
-
url:
|
|
353
|
+
url: url.toString(),
|
|
408
354
|
note: `request errored (DNS): ${(error as Error).message}`,
|
|
409
355
|
};
|
|
410
356
|
}
|
|
411
357
|
if (addresses.length === 0) {
|
|
412
358
|
return {
|
|
413
359
|
attempted: true,
|
|
414
|
-
url:
|
|
360
|
+
url: url.toString(),
|
|
415
361
|
note: `request errored (DNS): ${url.hostname} resolved to no addresses`,
|
|
416
362
|
};
|
|
417
363
|
}
|
|
418
|
-
} else
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
},
|
|
424
|
-
];
|
|
364
|
+
} else {
|
|
365
|
+
const host = normHost(url.hostname);
|
|
366
|
+
if (isIP(host)) {
|
|
367
|
+
addresses = [{ address: host, family: isIP(host) as 4 | 6 }];
|
|
368
|
+
}
|
|
425
369
|
}
|
|
426
370
|
if (!opts?.allowPrivate && addresses.some((address) => !isPublicIpAddress(address.address))) {
|
|
427
371
|
return {
|
|
428
372
|
attempted: false,
|
|
429
|
-
url:
|
|
373
|
+
url: url.toString(),
|
|
430
374
|
note: `${url.hostname} is a private/internal host; operator authorization is required for harness replay`,
|
|
431
375
|
};
|
|
432
376
|
}
|
|
@@ -454,7 +398,7 @@ async function replayRequest(
|
|
|
454
398
|
return {
|
|
455
399
|
attempted: true,
|
|
456
400
|
status: res.status,
|
|
457
|
-
url:
|
|
401
|
+
url: url.toString(),
|
|
458
402
|
note: `redirect limit exceeded (${MAX_REDIRECTS})`,
|
|
459
403
|
};
|
|
460
404
|
}
|
|
@@ -466,18 +410,18 @@ async function replayRequest(
|
|
|
466
410
|
return {
|
|
467
411
|
attempted: true,
|
|
468
412
|
status: res.status,
|
|
469
|
-
url:
|
|
413
|
+
url: url.toString(),
|
|
470
414
|
note: `redirected to disallowed protocol ${next.protocol}`,
|
|
471
415
|
};
|
|
472
416
|
}
|
|
473
|
-
if (next.hostname
|
|
417
|
+
if (normHost(next.hostname) !== lockedHostname) {
|
|
474
418
|
await res.body?.cancel().catch(() => undefined);
|
|
475
419
|
await fetched.close().catch(() => undefined);
|
|
476
420
|
closeFetched = undefined;
|
|
477
421
|
return {
|
|
478
422
|
attempted: true,
|
|
479
423
|
status: res.status,
|
|
480
|
-
url:
|
|
424
|
+
url: url.toString(),
|
|
481
425
|
note: `redirect left the bound host (${url.hostname} -> ${next.hostname})`,
|
|
482
426
|
};
|
|
483
427
|
}
|
|
@@ -509,22 +453,20 @@ async function replayRequest(
|
|
|
509
453
|
return {
|
|
510
454
|
attempted: true,
|
|
511
455
|
status: res.status,
|
|
512
|
-
url:
|
|
456
|
+
url: url.toString(),
|
|
513
457
|
bodySha256: observed.sha256,
|
|
514
458
|
bodyBytes: observed.bytes,
|
|
515
459
|
note: "response body exceeded the 2 MiB capture limit; matcher result is inconclusive",
|
|
516
460
|
};
|
|
517
461
|
}
|
|
518
462
|
const failures = await evaluateExpect(expect, res.status, observed.text);
|
|
519
|
-
const canaryObserved = canaryToken ? observed.text.includes(canaryToken) : undefined;
|
|
520
463
|
return {
|
|
521
464
|
attempted: true,
|
|
522
465
|
matched: failures.length === 0,
|
|
523
466
|
status: res.status,
|
|
524
|
-
url:
|
|
467
|
+
url: url.toString(),
|
|
525
468
|
bodySha256: observed.sha256,
|
|
526
469
|
bodyBytes: observed.bytes,
|
|
527
|
-
canaryObserved,
|
|
528
470
|
note:
|
|
529
471
|
failures.length === 0
|
|
530
472
|
? `status ${res.status}, all predicates matched`
|
|
@@ -534,13 +476,13 @@ async function replayRequest(
|
|
|
534
476
|
await closeFetched?.().catch(() => undefined);
|
|
535
477
|
return {
|
|
536
478
|
attempted: true,
|
|
537
|
-
url:
|
|
479
|
+
url: url.toString(),
|
|
538
480
|
note: `request errored (DNS/TLS/timeout): ${(e as Error).message}`,
|
|
539
481
|
};
|
|
540
482
|
}
|
|
541
483
|
}
|
|
542
484
|
|
|
543
|
-
return { attempted: true, url:
|
|
485
|
+
return { attempted: true, url: url.toString(), note: "unreachable redirect state" };
|
|
544
486
|
}
|
|
545
487
|
|
|
546
488
|
/**
|
|
@@ -566,84 +508,14 @@ export function sameRequest(
|
|
|
566
508
|
);
|
|
567
509
|
}
|
|
568
510
|
|
|
569
|
-
function injectCanary(
|
|
570
|
-
verify: PoCEvidence["verify"],
|
|
571
|
-
token: string | undefined,
|
|
572
|
-
): PoCEvidence["verify"] {
|
|
573
|
-
if (!token) return verify;
|
|
574
|
-
const replace = (value: string) => value.replace(POC_CANARY_PLACEHOLDER, token);
|
|
575
|
-
return {
|
|
576
|
-
...verify,
|
|
577
|
-
url: replace(verify.url),
|
|
578
|
-
body: verify.body === undefined ? undefined : replace(verify.body),
|
|
579
|
-
headers:
|
|
580
|
-
verify.headers === undefined
|
|
581
|
-
? undefined
|
|
582
|
-
: Object.fromEntries(
|
|
583
|
-
Object.entries(verify.headers).map(([key, value]) => [key, replace(value)]),
|
|
584
|
-
),
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
function canaryResult(
|
|
589
|
-
token: string | undefined,
|
|
590
|
-
target: HarnessResponseObservation,
|
|
591
|
-
control?: HarnessResponseObservation,
|
|
592
|
-
): HarnessCanaryResult | undefined {
|
|
593
|
-
if (!token) return;
|
|
594
|
-
const attempted =
|
|
595
|
-
target.canaryObserved !== undefined && (control ? control.canaryObserved !== undefined : true);
|
|
596
|
-
const pass = control
|
|
597
|
-
? attempted && target.canaryObserved === true && control.canaryObserved === false
|
|
598
|
-
: attempted && target.canaryObserved === true;
|
|
599
|
-
return {
|
|
600
|
-
mode: "reflection",
|
|
601
|
-
attempted,
|
|
602
|
-
pass,
|
|
603
|
-
tokenSha256: createHash("sha256").update(token).digest("hex"),
|
|
604
|
-
targetObserved: target.canaryObserved,
|
|
605
|
-
controlObserved: control?.canaryObserved,
|
|
606
|
-
note: control
|
|
607
|
-
? `canary ${pass ? "target-only" : "failed"}: target=${String(target.canaryObserved)}, control=${String(control.canaryObserved)}`
|
|
608
|
-
: `canary ${pass ? "observed" : "not observed"} on target`,
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/**
|
|
613
|
-
* Re-send the evidence's verify request with the harness's own client and
|
|
614
|
-
* judge the response against verify.expect. Never throws — the outcome is a
|
|
615
|
-
* structured result the ledger gate interprets.
|
|
616
|
-
*/
|
|
617
|
-
export async function replayVerify(
|
|
618
|
-
evidence: PoCEvidence,
|
|
619
|
-
opts?: ReplayOptions,
|
|
620
|
-
): Promise<HarnessVerifyResult> {
|
|
621
|
-
const token = evidence.verify.canary
|
|
622
|
-
? `poc_canary_${randomBytes(24).toString("hex")}`
|
|
623
|
-
: undefined;
|
|
624
|
-
const verify = injectCanary(evidence.verify, token);
|
|
625
|
-
const target = await replayRequest(verify, verify.expect, token, opts);
|
|
626
|
-
const canary = canaryResult(token, target);
|
|
627
|
-
return {
|
|
628
|
-
attempted: target.attempted,
|
|
629
|
-
pass: target.matched === true,
|
|
630
|
-
status: target.status,
|
|
631
|
-
target,
|
|
632
|
-
canary,
|
|
633
|
-
proofStrength: canary?.pass ? "canary_differential" : "predicate_differential",
|
|
634
|
-
note: `harness replay: ${target.note}${canary ? `; ${canary.note}` : ""}`,
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
|
|
638
511
|
/**
|
|
639
512
|
* Combine the two observations into the differential verdict. Shared by the
|
|
640
|
-
*
|
|
641
|
-
* baseline
|
|
513
|
+
* attack-vs-baseline replay: "target" is the attack request, "control" the
|
|
514
|
+
* legitimate baseline request.
|
|
642
515
|
*/
|
|
643
516
|
function judgeDifferential(
|
|
644
517
|
target: HarnessResponseObservation,
|
|
645
518
|
control: HarnessResponseObservation,
|
|
646
|
-
token: string | undefined,
|
|
647
519
|
label: { kind: string; a: string; b: string },
|
|
648
520
|
): HarnessVerifyResult {
|
|
649
521
|
const attempted = target.attempted && control.attempted;
|
|
@@ -657,12 +529,7 @@ function judgeDifferential(
|
|
|
657
529
|
? "control_only"
|
|
658
530
|
: "neither"
|
|
659
531
|
: undefined;
|
|
660
|
-
const
|
|
661
|
-
const pass =
|
|
662
|
-
attempted &&
|
|
663
|
-
conclusive &&
|
|
664
|
-
differential === "target_only" &&
|
|
665
|
-
(canary === undefined || canary.pass === true);
|
|
532
|
+
const pass = attempted && conclusive && differential === "target_only";
|
|
666
533
|
return {
|
|
667
534
|
attempted,
|
|
668
535
|
pass,
|
|
@@ -670,72 +537,16 @@ function judgeDifferential(
|
|
|
670
537
|
target,
|
|
671
538
|
control,
|
|
672
539
|
differential,
|
|
673
|
-
canary,
|
|
674
|
-
proofStrength: canary?.pass ? "canary_differential" : "predicate_differential",
|
|
675
540
|
note:
|
|
676
541
|
`harness ${label.kind} ${differential ?? "inconclusive"}: ${label.a} (${target.note}); ` +
|
|
677
|
-
`${label.b} (${control.note})
|
|
542
|
+
`${label.b} (${control.note})`,
|
|
678
543
|
};
|
|
679
544
|
}
|
|
680
545
|
|
|
681
546
|
/**
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
* changing only the origin to the declared control target.
|
|
686
|
-
*/
|
|
687
|
-
export async function replayDifferential(
|
|
688
|
-
evidence: PoCEvidence,
|
|
689
|
-
caseTarget: string,
|
|
690
|
-
controlTarget: string,
|
|
691
|
-
opts?: ReplayOptions,
|
|
692
|
-
): Promise<HarnessVerifyResult> {
|
|
693
|
-
const bindingError = verifyUrlBindingError(evidence.verify.url, caseTarget);
|
|
694
|
-
if (bindingError) {
|
|
695
|
-
return { attempted: false, pass: false, note: `target binding failed: ${bindingError}` };
|
|
696
|
-
}
|
|
697
|
-
if (sameTargetIdentity(caseTarget, controlTarget)) {
|
|
698
|
-
return {
|
|
699
|
-
attempted: false,
|
|
700
|
-
pass: false,
|
|
701
|
-
note: "control target resolves to the same network identity as the case target",
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
const controlUrl = controlUrlFor(evidence.verify.url, controlTarget);
|
|
705
|
-
if (!controlUrl) {
|
|
706
|
-
return {
|
|
707
|
-
attempted: false,
|
|
708
|
-
pass: false,
|
|
709
|
-
note: `control target is not an HTTP network target: ${controlTarget}`,
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
const token = evidence.verify.canary
|
|
714
|
-
? `poc_canary_${randomBytes(24).toString("hex")}`
|
|
715
|
-
: undefined;
|
|
716
|
-
const targetVerify = injectCanary(evidence.verify, token);
|
|
717
|
-
const target = await replayRequest(targetVerify, targetVerify.expect, token, opts);
|
|
718
|
-
const control = await replayRequest(
|
|
719
|
-
{
|
|
720
|
-
...targetVerify,
|
|
721
|
-
url: injectCanary({ ...evidence.verify, url: controlUrl.toString() }, token).url,
|
|
722
|
-
},
|
|
723
|
-
targetVerify.expect,
|
|
724
|
-
token,
|
|
725
|
-
opts,
|
|
726
|
-
);
|
|
727
|
-
return judgeDifferential(target, control, token, {
|
|
728
|
-
kind: "differential",
|
|
729
|
-
a: "target",
|
|
730
|
-
b: "control",
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
/**
|
|
735
|
-
* Same-host differential (Tier 2, intra-target). For access-control and
|
|
736
|
-
* business-logic classes the discriminating variable is the attacker's
|
|
737
|
-
* identity or a request parameter, NOT the host — so the sound baseline is a
|
|
738
|
-
* 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.
|
|
739
550
|
* The harness sends the attack request and the model-declared `evidence.baseline`
|
|
740
551
|
* request to the case target, applies the attack's `verify.expect` predicates to
|
|
741
552
|
* BOTH responses, and passes only when the proof appears on the attack response
|
|
@@ -772,13 +583,9 @@ export async function replayIntraTarget(
|
|
|
772
583
|
};
|
|
773
584
|
}
|
|
774
585
|
|
|
775
|
-
const
|
|
776
|
-
? `poc_canary_${randomBytes(24).toString("hex")}`
|
|
777
|
-
: undefined;
|
|
778
|
-
const attackVerify = injectCanary(evidence.verify, token);
|
|
779
|
-
const attack = await replayRequest(attackVerify, attackVerify.expect, token, opts);
|
|
586
|
+
const attack = await replayRequest(evidence.verify, evidence.verify.expect, opts);
|
|
780
587
|
// The baseline carries the attack's predicates: the proof must be ABSENT here.
|
|
781
|
-
const
|
|
588
|
+
const base = await replayRequest(
|
|
782
589
|
{
|
|
783
590
|
...evidence.verify,
|
|
784
591
|
method: baseline.method,
|
|
@@ -786,11 +593,11 @@ export async function replayIntraTarget(
|
|
|
786
593
|
headers: baseline.headers,
|
|
787
594
|
body: baseline.body,
|
|
788
595
|
},
|
|
789
|
-
|
|
596
|
+
evidence.verify.expect,
|
|
597
|
+
opts,
|
|
790
598
|
);
|
|
791
|
-
const base = await replayRequest(baselineVerify, attackVerify.expect, token, opts);
|
|
792
599
|
|
|
793
|
-
return judgeDifferential(attack, base,
|
|
600
|
+
return judgeDifferential(attack, base, {
|
|
794
601
|
kind: "intra-target",
|
|
795
602
|
a: "attack",
|
|
796
603
|
b: "baseline",
|