@xaccefy/pi-casefile 0.7.3 → 0.7.4

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.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "Offensive security case tracker for Pi Agent — bug bounties, CTFs, security audits",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/ledger.ts CHANGED
@@ -1577,7 +1577,13 @@ function buildScratchpadSection(caseId: string): string {
1577
1577
  const resume = scratchpad_resume(entry.name);
1578
1578
  if (!resume) continue;
1579
1579
  const allIds = Object.values(resume.checkpoint.phase_ids ?? {}).flat() as string[];
1580
- if (!allIds.includes(caseId)) continue;
1580
+ // Gate on the case id appearing in phase_ids OR in any artifact filename —
1581
+ // checkpoint ids are often empty for recon/hunt, while artifact names like
1582
+ // skeptic_case_<id>.json / trace_case_<id>.json are equally valid evidence.
1583
+ const namedInArtifact = Object.values(resume.artifacts)
1584
+ .flat()
1585
+ .some((n) => n.includes(caseId));
1586
+ if (!allIds.includes(caseId) && !namedInArtifact) continue;
1581
1587
 
1582
1588
  sections.push(`### Run: ${entry.name} (project root: ${resume.checkpoint.project_root})`);
1583
1589
  for (const phase of CONTEXT_PHASES) {
@@ -251,11 +251,20 @@ function parseOutput(output: unknown): { obj?: Record<string, unknown>; error?:
251
251
  }
252
252
 
253
253
  /** Stable repair-bucket key for a submission. */
254
+ /** Placeholder-y ids that carry no identity (observed in the wild: every
255
+ * submission in a run keyed "false"). Trusting them makes distinct findings
256
+ * share one key — one artifact name, one repair-budget bucket — so the last
257
+ * write clobbers the rest. Fall back to the content hash instead. */
258
+ const JUNK_ID_RE =
259
+ /^(false|true|null|none|undefined|n\/?a|na|unknown|missing|empty|todo|tbd|pending|not-set)$/i;
260
+
254
261
  function submissionKey(stage: SubmitStage, obj: Record<string, unknown>): string {
262
+ const candidate =
263
+ (typeof obj.finding_id === "string" && obj.finding_id.trim()) ||
264
+ (typeof obj.title === "string" && obj.title.trim()) ||
265
+ (typeof obj.id === "string" && obj.id.trim());
255
266
  const id =
256
- (typeof obj.finding_id === "string" && obj.finding_id) ||
257
- (typeof obj.title === "string" && obj.title) ||
258
- (typeof obj.id === "string" && obj.id);
267
+ candidate && candidate.length >= 3 && !JUNK_ID_RE.test(candidate) ? candidate : undefined;
259
268
  const tail = id ?? createHash("sha1").update(JSON.stringify(obj)).digest("hex").slice(0, 8);
260
269
  return `${stage}:${tail}`;
261
270
  }
package/src/scratchpad.ts CHANGED
@@ -150,6 +150,19 @@ export function getStatePath(runId: string, projectRoot?: string): string {
150
150
  return join(getRunDir(runId, projectRoot), "state.json");
151
151
  }
152
152
 
153
+ /**
154
+ * Sanitize an artifact name into a safe filename. Same allowlist as run_ids,
155
+ * plus dot-only rejection: a name like `..` or `.` would otherwise let join()
156
+ * point at the phase/run directory itself (EISDIR crash on write/read).
157
+ */
158
+ function sanitizeArtifactName(artifactName: string): string {
159
+ const safe = artifactName.replace(/[^a-zA-Z0-9._-]/g, "_");
160
+ if (!safe || /^\.+$/.test(safe)) {
161
+ throw new Error(`Invalid artifact name: "${artifactName}" — nothing left after sanitization`);
162
+ }
163
+ return safe;
164
+ }
165
+
153
166
  function emptyCheckpoint(runId: string, projectRoot: string): ScratchpadCheckpoint {
154
167
  const now = new Date().toISOString();
155
168
  return {
@@ -229,8 +242,8 @@ export function scratchpad_write(
229
242
  const runDir = getRunDir(runId, root);
230
243
  ensureRunDirs(runDir);
231
244
 
232
- // Sanitize artifact name: no path traversal.
233
- const safeName = artifactName.replace(/[^a-zA-Z0-9._-]/g, "_");
245
+ // Sanitize artifact name: no path traversal, no dot-only escape.
246
+ const safeName = sanitizeArtifactName(artifactName);
234
247
  const dir = join(runDir, PHASE_DIRS[phase]);
235
248
  const filePath = join(dir, safeName);
236
249
  writeFileSync(filePath, content, "utf8");
@@ -247,7 +260,7 @@ export function scratchpad_read(
247
260
  projectRoot?: string,
248
261
  ): string | null {
249
262
  const root = projectRoot ?? detectWorkspaceRoot();
250
- const safeName = artifactName.replace(/[^a-zA-Z0-9._-]/g, "_");
263
+ const safeName = sanitizeArtifactName(artifactName);
251
264
  const filePath = join(getRunDir(runId, root), PHASE_DIRS[phase], safeName);
252
265
  if (!existsSync(filePath)) return null;
253
266
  return readFileSync(filePath, "utf8");