@xaccefy/pi-casefile 0.8.0 → 0.8.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xaccefy/pi-casefile",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Offensive security case tracker for Pi Agent — bug bounties, CTFs, security audits",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/index.ts CHANGED
@@ -1487,6 +1487,11 @@ export default function casefileExtension(pi: ExtensionAPI) {
1487
1487
  handler: async (args, ctx) => {
1488
1488
  const next = parseXpModeArg(args ?? "", readXpMode());
1489
1489
  writeXpMode(next);
1490
+ // Re-enabling after a mid-session /xp off must re-inject the workflow
1491
+ // on the next prompt — otherwise workflowInjected (module-level, set on
1492
+ // first enable) stays true and the workflow never comes back until the
1493
+ // process restarts.
1494
+ if (next !== "off") workflowInjected = false;
1490
1495
  ctx.ui.notify(
1491
1496
  `Casefile XP mode: ${next.toUpperCase()} (takes effect on the next prompt)`,
1492
1497
  next === "on" ? "info" : "warning",
package/src/ledger.ts CHANGED
@@ -530,6 +530,32 @@ function mapRow(
530
530
  };
531
531
  }
532
532
 
533
+ /** Map raw snake_case DB rows to their camelCase item types. */
534
+ function mapEvidenceRow(row: any): EvidenceItem {
535
+ return {
536
+ id: row.id,
537
+ caseId: row.case_id,
538
+ role: row.role,
539
+ artifactPath: row.artifact_path ?? undefined,
540
+ sha256: row.sha256 ?? undefined,
541
+ summary: row.summary,
542
+ createdAt: row.created_at,
543
+ };
544
+ }
545
+
546
+ function mapCoverageRow(row: any): CoverageItem {
547
+ return {
548
+ id: row.id,
549
+ caseId: row.case_id,
550
+ asset: row.asset,
551
+ class: row.class,
552
+ scope: row.scope,
553
+ note: row.note,
554
+ testedBy: row.tested_by ?? undefined,
555
+ createdAt: row.created_at,
556
+ };
557
+ }
558
+
533
559
  /** Batch-fetch per-case item tables (evidence / coverage) for a set of ids. */
534
560
  function fetchItemMap<T extends { caseId: string }>(
535
561
  db: DatabaseSync,
@@ -540,12 +566,14 @@ function fetchItemMap<T extends { caseId: string }>(
540
566
  const placeholders = ids.map(() => "?").join(",");
541
567
  const rows = db
542
568
  .prepare(`SELECT * FROM ${table} WHERE case_id IN (${placeholders}) ORDER BY created_at`)
543
- .all(...ids) as T[];
569
+ .all(...ids) as any[];
570
+ const mapRow = table === "evidence_items" ? mapEvidenceRow : mapCoverageRow;
544
571
  const map = new Map<string, T[]>();
545
572
  for (const row of rows) {
546
- const bucket = map.get(row.caseId);
547
- if (bucket) bucket.push(row);
548
- else map.set(row.caseId, [row]);
573
+ const item = mapRow(row) as unknown as T;
574
+ const bucket = map.get(item.caseId);
575
+ if (bucket) bucket.push(item);
576
+ else map.set(item.caseId, [item]);
549
577
  }
550
578
  return map;
551
579
  }
@@ -603,12 +631,16 @@ export function getCaseById(id: string): CaseRecord | undefined {
603
631
 
604
632
  const linkStmt = db.prepare("SELECT target_id, kind FROM case_links WHERE source_id = ?");
605
633
  const links = linkStmt.all(id) as { target_id: string; kind: string }[];
606
- const evidence = db
607
- .prepare("SELECT * FROM evidence_items WHERE case_id = ? ORDER BY created_at")
608
- .all(id) as EvidenceItem[];
609
- const coverage = db
610
- .prepare("SELECT * FROM coverage_items WHERE case_id = ? ORDER BY created_at")
611
- .all(id) as CoverageItem[];
634
+ const evidence = (
635
+ db
636
+ .prepare("SELECT * FROM evidence_items WHERE case_id = ? ORDER BY created_at")
637
+ .all(id) as any[]
638
+ ).map(mapEvidenceRow);
639
+ const coverage = (
640
+ db
641
+ .prepare("SELECT * FROM coverage_items WHERE case_id = ? ORDER BY created_at")
642
+ .all(id) as any[]
643
+ ).map(mapCoverageRow);
612
644
 
613
645
  return mapRow(
614
646
  row,
@@ -1238,9 +1270,11 @@ export function addEvidenceItemResult(
1238
1270
 
1239
1271
  export function listEvidenceItems(caseId: string): EvidenceItem[] {
1240
1272
  const db = getDb();
1241
- return db
1242
- .prepare("SELECT * FROM evidence_items WHERE case_id = ? ORDER BY created_at")
1243
- .all(caseId) as EvidenceItem[];
1273
+ return (
1274
+ db
1275
+ .prepare("SELECT * FROM evidence_items WHERE case_id = ? ORDER BY created_at")
1276
+ .all(caseId) as any[]
1277
+ ).map(mapEvidenceRow);
1244
1278
  }
1245
1279
 
1246
1280
  // ── Coverage items ──────────────────────────────────────────────────
@@ -1311,9 +1345,11 @@ export function recordCoverageResult(
1311
1345
 
1312
1346
  export function listCoverage(caseId: string): CoverageItem[] {
1313
1347
  const db = getDb();
1314
- return db
1315
- .prepare("SELECT * FROM coverage_items WHERE case_id = ? ORDER BY created_at")
1316
- .all(caseId) as CoverageItem[];
1348
+ return (
1349
+ db
1350
+ .prepare("SELECT * FROM coverage_items WHERE case_id = ? ORDER BY created_at")
1351
+ .all(caseId) as any[]
1352
+ ).map(mapCoverageRow);
1317
1353
  }
1318
1354
 
1319
1355
  export type CoverageSummary = {
@@ -1699,13 +1735,26 @@ function hasChainClass(c: CaseRecord, re: RegExp): boolean {
1699
1735
  return re.test(chainText(c));
1700
1736
  }
1701
1737
 
1738
+ /** Reduce a target string to a bare hostname (strip scheme, port, path). */
1739
+ function normalizeTargetHost(target: string): string {
1740
+ let h = target
1741
+ .toLowerCase()
1742
+ .trim()
1743
+ .replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
1744
+ h = h.split("?")[0].split("/")[0].split(":")[0];
1745
+ return h.trim();
1746
+ }
1747
+
1702
1748
  /** Same asset or related (same eTLD+1) — chains only pair cases on one target. */
1703
1749
  function sameAssetOrRelated(a: CaseRecord, b: CaseRecord): boolean {
1704
- const ta = (a.target ?? "").toLowerCase().trim();
1705
- const tb = (b.target ?? "").toLowerCase().trim();
1750
+ const ta = normalizeTargetHost(a.target ?? "");
1751
+ const tb = normalizeTargetHost(b.target ?? "");
1706
1752
  if (!ta || !tb) return false;
1707
1753
  if (ta === tb) return true;
1708
- if (ta.includes(tb) || tb.includes(ta)) return true;
1754
+ // Subdomain relation requires a label boundary: "api.example.com" vs
1755
+ // "example.com" pair, but "myshop.io" vs "shop.io" do NOT — a bare
1756
+ // substring check pairs unrelated targets whose names merely overlap.
1757
+ if (ta.endsWith(`.${tb}`) || tb.endsWith(`.${ta}`)) return true;
1709
1758
  return eTLDPlus1(ta) === eTLDPlus1(tb);
1710
1759
  }
1711
1760
 
@@ -2122,6 +2171,17 @@ export function formatCaseDetail(record: CaseRecord): string {
2122
2171
  display = (val as { id: string; kind: string }[])
2123
2172
  .map((l) => `${l.id} (${l.kind})`)
2124
2173
  .join(", ");
2174
+ } else if (key === "evidenceItems") {
2175
+ display = (val as EvidenceItem[])
2176
+ .map(
2177
+ (e) =>
2178
+ `[${e.role}] ${e.summary}${e.artifactPath ? ` — \`${e.artifactPath}\` sha256:\`${e.sha256?.slice(0, 12) ?? "?"}\`` : ""} (${e.createdAt})`,
2179
+ )
2180
+ .join("\n");
2181
+ } else if (key === "coverageItems") {
2182
+ display = (val as CoverageItem[])
2183
+ .map((c) => `[${c.scope}] ${c.asset} × ${c.class} — ${c.note}`)
2184
+ .join("\n");
2125
2185
  } else if (Array.isArray(val)) {
2126
2186
  display = val.join(", ");
2127
2187
  } else if (typeof val === "object") {