@xaccefy/pi-casefile 0.9.1 → 0.9.3

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 x4cc3
3
+ Copyright (c) 2025 xaccefy
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -16,13 +16,17 @@ The attack-mode text stays **quiet by default** so your normal coding isn't buri
16
16
 
17
17
  | Control | Effect |
18
18
  |---------|--------|
19
- | `/xp` | Toggle ON/OFF |
20
- | `/xp on` / `/xp off` / `/xp lite` | Set explicitly |
21
- | `PI_XP_MODE=on` | Force ON for this process (overrides file) |
19
+ | `/xp` | Toggle SWARM/OFF |
20
+ | `/xp on` | Default enabled mode: SWARM |
21
+ | `/xp swarm` | Bounded multi-agent workflow: auditor/tracer/skeptic/chain only |
22
+ | `/xp lite` | Focused workflow: single-agent, no subagent dispatch |
23
+ | `/xp off` | Quiet mode explicitly |
24
+ | `PI_XP_MODE=on` | Force SWARM for this process (same as `swarm`) |
25
+ | `PI_XP_MODE=swarm` | Force SWARM for this process (overrides file) |
22
26
  | `PI_XP_MODE=lite` | Force LITE (single-agent, no subagent dispatch) |
23
27
  | `PI_XP_MODE=off` | Force OFF |
24
28
 
25
- When **ON**, every prompt gets the attacker-minded workflow plus any open cases. **LITE** is the same discipline done by the main agent alone — no `subagent` dispatch (CTF / single-shot engagements). When **OFF**, nothing is added; tools still work.
29
+ When **LITE**, every prompt gets the attacker-minded workflow plus any open cases, done by the main agent alone — no `subagent` dispatch (CTF / single-shot engagements). When **SWARM**, the bounded pipeline is injected and only auditor/tracer/skeptic/chain are delegated; validation, patching, reporting, and ConfirmFinding stay with the main agent. When **OFF**, nothing is added; tools still work.
26
30
 
27
31
  State is persisted next to the ledger as `xp-mode` (e.g. `.pi/xp-mode`).
28
32
 
@@ -48,10 +52,10 @@ hypothesis → investigating → confirmed → reported
48
52
 
49
53
  - **investigating** needs `evidence` + `confidence`
50
54
  - **confirmed** only through the two-phase gate — `PromoteFinding` runs the PoC 2× target + 1× control, requires complete zero-exit runs and nonce-bound discriminating response-body evidence, then the harness performs a DNS-pinned identical replay and requires two conclusive responses with `target_only`. Reflection-capable requests may add a post-PoC harness-generated canary that must appear only on target. Status-only/trivial matchers and incomplete response capture are rejected. Exit zero is necessary but never proof. The main agent performs semantic review and calls `ConfirmFinding`, which captures a second fresh harness replay and binds it to the verdict; worker processes are rejected. Blind/OOB claims fail closed without a source-separated oracle.
51
- - **Every promotion requires `control_path`** (the same bytes as the PoC — sha256 enforced) and a distinct `control_target` pre-approved by the operator in `PI_POC_CONTROL_TARGETS`; an agent cannot invent its own easy control. The control run is stored as `controlVerified`. Crashes, transport-inconclusive controls, status-only evidence, and missing evidence all block promotion.
55
+ - **Every promotion requires a distinct `control_target`** pre-approved by the operator in `PI_POC_CONTROL_TARGETS`; an agent cannot invent its own easy control. `control_path` defaults to `poc_path` and exists only as an override — if supplied, it must contain the same bytes as the PoC (sha256 enforced). The control run is stored as `controlVerified`. Crashes, transport-inconclusive controls, status-only evidence, and missing evidence all block promotion.
52
56
  - **New cases require `disproveIf`** — falsification conditions (what would disprove this hypothesis). A hypothesis that can't say what kills it isn't one yet.
53
57
  - **A kill must be justified**: either an EvidenceAdd `refutation` item, or a canonical kill-reason token (intended_behavior, duplicate, framework_protection, out_of_scope, insufficient_impact, no_attack_path, ...) in assumptions/nextStep. Bare `status: "killed"` is rejected.
54
- - **reported** needs `CaseContext` first (records the report path; the report writer produces the final file)
58
+ - **reported** needs `CaseContext` first (records the report path; the main agent produces the final file)
55
59
  - **killed** / **reported** are final (no more edits)
56
60
 
57
61
  ## Evidence items
@@ -65,8 +69,8 @@ hypothesis → investigating → confirmed → reported
65
69
  | `CaseAdd` | Open a case (`title` + `disproveIf` required; start as `hypothesis` or `investigating`) |
66
70
  | `CaseUpdate` | Evidence, impact, severity, status (not direct confirm) |
67
71
  | `EvidenceAdd` | Role-typed, hashed evidence item on a case (refutation justifies kills; cleanup tracks cleanup) |
68
- | `PromoteFinding` | Phase 1: PoC 2× target + 1× operator-approved control, then DNS-pinned harness-owned replay requiring conclusive `target_only`; optional reflection canary upgrades the recorded proof strength when observed only on target. `local:true` and private replay are operator-gated; blind/OOB proof fails closed without source separation |
69
- | `ConfirmFinding` | Phase 2: main-agent-only semantic decision plus a fresh harness-owned target/control replay (CONFIRMED promotes; NOT_CONFIRMED keeps investigating; worker calls are rejected) |
72
+ | `PromoteFinding` | Phase 1: main-agent-only PoC 2× target + 1× operator-approved control, then DNS-pinned harness-owned replay requiring conclusive `target_only`; optional reflection canary upgrades the recorded proof strength when observed only on target. `local:true` and private replay are operator-gated; blind/OOB proof fails closed without source separation |
73
+ | `ConfirmFinding` | Phase 2: main-agent-only semantic decision plus a fresh harness-owned target/control replay (CONFIRMED promotes; NOT_CONFIRMED keeps investigating; worker/subagent gate calls are rejected) |
70
74
  | `CaseGet` / `CaseList` / `CaseSearch` | Read / filter / search |
71
75
  | `CaseLink` / `CaseUnlink` | Bidirectional exploit chains |
72
76
  | `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 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xaccefy/pi-casefile",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Offensive security case tracker for Pi Agent — bug bounties, CTFs, security audits",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -49,7 +49,7 @@
49
49
  ],
50
50
  "main": "src/index.ts",
51
51
  "dependencies": {
52
- "@xaccefy/pi-shared": "0.9.1",
52
+ "@xaccefy/pi-shared": "0.9.3",
53
53
  "undici": "^8.9.0"
54
54
  },
55
55
  "pi": {
@@ -13,9 +13,9 @@ Use Casefile to maintain durable security investigation state across agent turns
13
13
  1. Check existing cases before opening a new one with CaseList or CaseSearch.
14
14
  2. Open new leads with CaseAdd as `hypothesis` or `investigating`.
15
15
  3. Promote cases with CaseUpdate only after materially new evidence, proof, impact, blockers, remediation, or status changes.
16
- 4. Mark `confirmed` only via the two-phase gate — `PromoteFinding` (PoC 2× target + 1× operator-approved control, nonce-bound body evidence, then a DNS-pinned conclusive `target_only` replay; use the post-PoC harness-generated canary for reflection-capable requests) → the main agent personally reviews → `ConfirmFinding`, which captures a fresh second harness replay before commit. Never delegate phase 2; worker calls are rejected. Exit zero is run integrity, never vulnerability proof. A predicate differential is evidence, not an automatic exploit verdict. Blind/OOB findings remain investigating without a source-separated oracle.
16
+ 4. Mark `confirmed` only via the two-phase gate — main-agent `PromoteFinding` (PoC 2× target + 1× operator-approved control, nonce-bound body evidence, then a DNS-pinned conclusive `target_only` replay; use the post-PoC harness-generated canary for reflection-capable requests) → the main agent personally reviews → `ConfirmFinding`, which captures a fresh second harness replay before commit. Never delegate validation or confirmation; worker/subagent gate calls are rejected. Exit zero is run integrity, never vulnerability proof. A predicate differential is evidence, not an automatic exploit verdict. Blind/OOB findings remain investigating without a source-separated oracle.
17
17
  5. Use CaseLink and CaseUnlink for exploit chains. Do not edit linked case IDs directly.
18
- 6. Use CaseContext only for confirmed or already reported cases: it writes the full context bundle (complete record, verification logs, links, pipeline artifacts) and records the report path. Then have the report written (reporter agent in the full pipeline; yourself in lite mode) and CaseUpdate status=`reported`.
18
+ 6. Use CaseContext only for confirmed or already reported cases: it writes the full context bundle (complete record, verification logs, links, pipeline artifacts) and records the report path. Then write the report and CaseUpdate status=`reported`.
19
19
  7. Use `killed` for disproven, duplicate, or dead-end leads, and include evidence, blockers, next step, or assumptions explaining why.
20
20
 
21
21
  ## State machine
@@ -41,4 +41,4 @@ hypothesis → investigating → confirmed → reported
41
41
  - `CaseSearch`: search all fields or a scoped field.
42
42
  - `CaseLink`: bidirectionally link two cases.
43
43
  - `CaseUnlink`: remove a bidirectional case link.
44
- - `CaseContext`: write the case context bundle (complete record, PoC/disconfirmation logs, links, pipeline artifacts) for a confirmed or reported case and record the report path for the report writer.
44
+ - `CaseContext`: write the case context bundle (complete record, PoC/disconfirmation logs, links, pipeline artifacts) for a confirmed or reported case and record the report path for the main agent.
package/src/evidence.ts CHANGED
@@ -45,6 +45,14 @@ export type PoCEvidence = {
45
45
  expect: VerifyExpect;
46
46
  /** Optional stronger causality dimension, independent of the authored predicate. */
47
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";
48
56
  };
49
57
  /** What the script itself saw — corroboration only, never proof. */
50
58
  observations: string[];
@@ -137,6 +145,7 @@ function validRegexArray(v: unknown): v is string[] {
137
145
  boundedStringArray(v, MAX_REGEX_VALUES, MAX_REGEX_CHARS) &&
138
146
  v.every((re) => {
139
147
  try {
148
+ // nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp -- compile-only validation; evaluation runs in a worker with a hard timeout.
140
149
  new RegExp(re);
141
150
  return true;
142
151
  } catch {
@@ -252,6 +261,19 @@ export function parsePoCEvidence(
252
261
  };
253
262
  }
254
263
  }
264
+ if (verify.mode !== undefined && verify.mode !== "inter_host" && verify.mode !== "intra_target") {
265
+ return {
266
+ ok: false,
267
+ error: 'evidence.json verify.mode must be "inter_host" or "intra_target"',
268
+ };
269
+ }
270
+ if (verify.mode === "intra_target" && !isRecord(raw.baseline)) {
271
+ return {
272
+ ok: false,
273
+ error:
274
+ "evidence.json verify.mode intra_target requires baseline — a legitimate same-host request whose response must NOT satisfy the attack predicate",
275
+ };
276
+ }
255
277
  const expect = verify.expect;
256
278
  if (!isRecord(expect))
257
279
  return { ok: false, error: "evidence.json verify.expect must be an object" };
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Harness-side target/control replay — Tier 2 of docs/poc-trust-model.md.
3
3
  *
4
- * The machine floor cannot trust a worker's self-reported `re_executed`
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
6
  * request with its own HTTP client and apply the same `expect` predicates
7
7
  * (status / body_contains / body_regex) to target and control responses. The
8
- * worker supplies the predicate; the harness owns both evidence acquisition
9
- * and predicate execution. The main agent performs the later semantic review.
8
+ * main agent supplies the predicate; the harness owns both evidence acquisition
9
+ * and predicate execution before the later semantic review.
10
10
  *
11
11
  * Policy:
12
12
  * - Private/internal hosts require explicit operator authorization; otherwise
@@ -543,6 +543,29 @@ async function replayRequest(
543
543
  return { attempted: true, url: observedUrl(), note: "unreachable redirect state" };
544
544
  }
545
545
 
546
+ /**
547
+ * Two requests are "the same" when method, URL, header set, and body all match.
548
+ * An intra-target differential whose attack and baseline are identical proves
549
+ * nothing — the discriminating variable must actually differ.
550
+ */
551
+ export function sameRequest(
552
+ a: { method: string; url: string; headers?: Record<string, string>; body?: string },
553
+ b: { method: string; url: string; headers?: Record<string, string>; body?: string },
554
+ ): boolean {
555
+ const norm = (h?: Record<string, string>) =>
556
+ JSON.stringify(
557
+ Object.entries(h ?? {})
558
+ .map(([k, v]) => [k.toLowerCase(), v] as const)
559
+ .sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0)),
560
+ );
561
+ return (
562
+ a.method.toUpperCase() === b.method.toUpperCase() &&
563
+ a.url === b.url &&
564
+ (a.body ?? "") === (b.body ?? "") &&
565
+ norm(a.headers) === norm(b.headers)
566
+ );
567
+ }
568
+
546
569
  function injectCanary(
547
570
  verify: PoCEvidence["verify"],
548
571
  token: string | undefined,
@@ -612,6 +635,49 @@ export async function replayVerify(
612
635
  };
613
636
  }
614
637
 
638
+ /**
639
+ * Combine the two observations into the differential verdict. Shared by the
640
+ * inter-host (target vs control host) and intra-target (attack vs same-host
641
+ * baseline) replays — only the note labels differ.
642
+ */
643
+ function judgeDifferential(
644
+ target: HarnessResponseObservation,
645
+ control: HarnessResponseObservation,
646
+ token: string | undefined,
647
+ label: { kind: string; a: string; b: string },
648
+ ): HarnessVerifyResult {
649
+ const attempted = target.attempted && control.attempted;
650
+ const conclusive = target.matched !== undefined && control.matched !== undefined;
651
+ const differential = conclusive
652
+ ? target.matched === true
653
+ ? control.matched === true
654
+ ? "both"
655
+ : "target_only"
656
+ : control.matched === true
657
+ ? "control_only"
658
+ : "neither"
659
+ : undefined;
660
+ const canary = canaryResult(token, target, control);
661
+ const pass =
662
+ attempted &&
663
+ conclusive &&
664
+ differential === "target_only" &&
665
+ (canary === undefined || canary.pass === true);
666
+ return {
667
+ attempted,
668
+ pass,
669
+ status: target.status,
670
+ target,
671
+ control,
672
+ differential,
673
+ canary,
674
+ proofStrength: canary?.pass ? "canary_differential" : "predicate_differential",
675
+ note:
676
+ `harness ${label.kind} ${differential ?? "inconclusive"}: ${label.a} (${target.note}); ` +
677
+ `${label.b} (${control.note})${canary ? `; ${canary.note}` : ""}`,
678
+ };
679
+ }
680
+
615
681
  /**
616
682
  * Execute one harness-owned request template against both the case target and
617
683
  * a distinct control origin. The PoC cannot weaken the control request: the
@@ -658,36 +724,75 @@ export async function replayDifferential(
658
724
  token,
659
725
  opts,
660
726
  );
661
- const attempted = target.attempted && control.attempted;
662
- const conclusive = target.matched !== undefined && control.matched !== undefined;
663
- const targetMatched = target.matched === true;
664
- const controlMatched = control.matched === true;
665
- const differential = conclusive
666
- ? targetMatched
667
- ? controlMatched
668
- ? "both"
669
- : "target_only"
670
- : controlMatched
671
- ? "control_only"
672
- : "neither"
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.
739
+ * The harness sends the attack request and the model-declared `evidence.baseline`
740
+ * request to the case target, applies the attack's `verify.expect` predicates to
741
+ * BOTH responses, and passes only when the proof appears on the attack response
742
+ * and is absent from the baseline (`target_only`, where "target" = attack and
743
+ * "control" = baseline). The baseline is bound to the case target so it cannot
744
+ * be redirected to a weaker origin, and it must differ from the attack request.
745
+ */
746
+ export async function replayIntraTarget(
747
+ evidence: PoCEvidence,
748
+ caseTarget: string,
749
+ opts?: ReplayOptions,
750
+ ): Promise<HarnessVerifyResult> {
751
+ const baseline = evidence.baseline;
752
+ if (!baseline) {
753
+ return {
754
+ attempted: false,
755
+ pass: false,
756
+ note: "intra-target differential requires evidence.baseline (a legitimate same-host request)",
757
+ };
758
+ }
759
+ const attackBinding = verifyUrlBindingError(evidence.verify.url, caseTarget);
760
+ if (attackBinding) {
761
+ return { attempted: false, pass: false, note: `attack binding failed: ${attackBinding}` };
762
+ }
763
+ const baselineBinding = verifyUrlBindingError(baseline.url, caseTarget);
764
+ if (baselineBinding) {
765
+ return { attempted: false, pass: false, note: `baseline binding failed: ${baselineBinding}` };
766
+ }
767
+ if (sameRequest(evidence.verify, baseline)) {
768
+ return {
769
+ attempted: false,
770
+ pass: false,
771
+ note: "attack and baseline requests are identical — an intra-target differential must vary identity or a parameter",
772
+ };
773
+ }
774
+
775
+ const token = evidence.verify.canary
776
+ ? `poc_canary_${randomBytes(24).toString("hex")}`
673
777
  : undefined;
674
- const canary = canaryResult(token, target, control);
675
- const pass =
676
- attempted &&
677
- conclusive &&
678
- differential === "target_only" &&
679
- (canary === undefined || canary.pass === true);
680
- return {
681
- attempted,
682
- pass,
683
- status: target.status,
684
- target,
685
- control,
686
- differential,
687
- canary,
688
- proofStrength: canary?.pass ? "canary_differential" : "predicate_differential",
689
- note:
690
- `harness differential ${differential ?? "inconclusive"}: target (${target.note}); ` +
691
- `control (${control.note})${canary ? `; ${canary.note}` : ""}`,
692
- };
778
+ const attackVerify = injectCanary(evidence.verify, token);
779
+ const attack = await replayRequest(attackVerify, attackVerify.expect, token, opts);
780
+ // The baseline carries the attack's predicates: the proof must be ABSENT here.
781
+ const baselineVerify = injectCanary(
782
+ {
783
+ ...evidence.verify,
784
+ method: baseline.method,
785
+ url: baseline.url,
786
+ headers: baseline.headers,
787
+ body: baseline.body,
788
+ },
789
+ token,
790
+ );
791
+ const base = await replayRequest(baselineVerify, attackVerify.expect, token, opts);
792
+
793
+ return judgeDifferential(attack, base, token, {
794
+ kind: "intra-target",
795
+ a: "attack",
796
+ b: "baseline",
797
+ });
693
798
  }