@shakecodeslikecray/whiterose 1.0.2 → 1.0.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/dist/cli/index.js CHANGED
@@ -4327,9 +4327,23 @@ function loadAccumulatedBugs(cwd) {
4327
4327
  if (!Array.isArray(stored.bugs)) {
4328
4328
  stored.bugs = [];
4329
4329
  }
4330
+ if (!stored.fingerprints || typeof stored.fingerprints !== "object") {
4331
+ stored.fingerprints = {};
4332
+ }
4330
4333
  stored.bugs = stored.bugs.map((b) => ({ ...b, kind: b.kind || "bug" }));
4331
4334
  return stored;
4332
- } catch {
4335
+ } catch (error) {
4336
+ const backupPath = `${bugsPath}.corrupted.${Date.now()}`;
4337
+ try {
4338
+ const corruptedContent = readFileSync(bugsPath, "utf-8");
4339
+ writeFileSync(backupPath, corruptedContent);
4340
+ console.warn(`Warning: ${BUGS_FILENAME} is corrupted and could not be parsed.`);
4341
+ console.warn(`Corrupted file backed up to: ${backupPath}`);
4342
+ console.warn("Bug history has been reset. Previous bugs will appear as new.");
4343
+ } catch {
4344
+ console.warn(`Warning: ${BUGS_FILENAME} is corrupted and could not be parsed.`);
4345
+ console.warn("Bug history has been reset. Previous bugs will appear as new.");
4346
+ }
4333
4347
  return {
4334
4348
  version: STORAGE_VERSION,
4335
4349
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
@@ -5864,7 +5878,9 @@ var FixConfirm = ({ bug, dryRun, onConfirm, onCancel, onFixComplete }) => {
5864
5878
  setStatus("fixing");
5865
5879
  setProgressMessage("Starting agentic fix...");
5866
5880
  try {
5867
- const result = await onConfirm();
5881
+ const result = await onConfirm((message) => {
5882
+ setProgressMessage(message);
5883
+ });
5868
5884
  if (result.falsePositive) {
5869
5885
  setFalsePositiveReason(result.falsePositiveReason || "The AI determined this bug is not real after analyzing the code.");
5870
5886
  setStatus("false-positive");
@@ -6008,11 +6024,11 @@ var App = ({ bugs, config, fixOptions, onFix, onExit }) => {
6008
6024
  };
6009
6025
  const [fixError, setFixError] = useState(null);
6010
6026
  const [lastFixedBugId, setLastFixedBugId] = useState(null);
6011
- const handleConfirmFix = async () => {
6027
+ const handleConfirmFix = async (onProgress) => {
6012
6028
  if (selectedBug) {
6013
6029
  setFixError(null);
6014
6030
  setLastFixedBugId(selectedBug.id);
6015
- const result = await onFix(selectedBug);
6031
+ const result = await onFix(selectedBug, onProgress);
6016
6032
  return result;
6017
6033
  }
6018
6034
  return { falsePositive: false };
@@ -6217,6 +6233,67 @@ function markBugAsFixed(bug, commitHash, cwd = process.cwd()) {
6217
6233
  }
6218
6234
 
6219
6235
  // src/core/fixer.ts
6236
+ var MAX_SARIF_TEXT_LENGTH = 2e3;
6237
+ var PROMPT_INJECTION_PATTERNS = [
6238
+ // Attempts to override or ignore instructions
6239
+ /ignore\s+(previous|all|above)\s+instructions?/i,
6240
+ /disregard\s+(previous|all|above)\s+instructions?/i,
6241
+ /forget\s+(previous|all|above)\s+instructions?/i,
6242
+ // Attempts to define new roles or personas
6243
+ /you\s+are\s+(now|actually)\s+/i,
6244
+ /act\s+as\s+(a|an)\s+/i,
6245
+ /pretend\s+(you|to\s+be)\s+/i,
6246
+ // Attempts to inject system-level commands
6247
+ /\[SYSTEM\]/i,
6248
+ /\[INST\]/i,
6249
+ /<\|system\|>/i,
6250
+ /<\|assistant\|>/i,
6251
+ /<\|user\|>/i,
6252
+ // Attempts to break out with special markers
6253
+ /###\s*(INSTRUCTION|SYSTEM|END|NEW)/i,
6254
+ /```\s*(system|instruction)/i,
6255
+ // Direct file operation injections
6256
+ /delete\s+(all|the)\s+files?/i,
6257
+ /rm\s+-rf\s+/i,
6258
+ /remove\s+(all|every)\s+file/i,
6259
+ // Exfiltration attempts
6260
+ /send\s+(this|the|all)\s+(data|content|file)/i,
6261
+ /upload\s+(to|this)/i,
6262
+ /curl\s+.*\s+-d/i,
6263
+ /fetch\s*\(\s*['"][^'"]*['"]\s*,\s*\{[^}]*method\s*:\s*['"]POST['"]/i
6264
+ ];
6265
+ function sanitizeSarifText(text2, fieldName = "field") {
6266
+ if (!text2 || typeof text2 !== "string") {
6267
+ return "";
6268
+ }
6269
+ let sanitized = text2.length > MAX_SARIF_TEXT_LENGTH ? text2.substring(0, MAX_SARIF_TEXT_LENGTH) + `... [truncated ${fieldName}]` : text2;
6270
+ for (const pattern of PROMPT_INJECTION_PATTERNS) {
6271
+ if (pattern.test(sanitized)) {
6272
+ sanitized = sanitized.replace(pattern, "[REDACTED: potential injection]");
6273
+ }
6274
+ }
6275
+ sanitized = sanitized.replace(/```+/g, "`\u200B`\u200B`");
6276
+ sanitized = sanitized.replace(/###/g, "#\u200B#\u200B#");
6277
+ return sanitized;
6278
+ }
6279
+ function sanitizeSarifEvidence(evidence) {
6280
+ if (!Array.isArray(evidence)) {
6281
+ return [];
6282
+ }
6283
+ return evidence.filter((e) => typeof e === "string").slice(0, 10).map((e) => sanitizeSarifText(e, "evidence"));
6284
+ }
6285
+ function sanitizeSarifCodePath(codePath) {
6286
+ if (!Array.isArray(codePath)) {
6287
+ return [];
6288
+ }
6289
+ return codePath.slice(0, 20).map((entry, idx) => ({
6290
+ step: typeof entry?.step === "number" ? entry.step : idx + 1,
6291
+ file: sanitizeSarifText(String(entry?.file || ""), "codePath.file").substring(0, 500),
6292
+ line: typeof entry?.line === "number" ? entry.line : 0,
6293
+ code: sanitizeSarifText(String(entry?.code || ""), "codePath.code"),
6294
+ explanation: sanitizeSarifText(String(entry?.explanation || ""), "codePath.explanation")
6295
+ }));
6296
+ }
6220
6297
  function isPathWithinProject(filePath, projectDir) {
6221
6298
  const resolvedPath = resolve(projectDir, filePath);
6222
6299
  const relativePath = relative(projectDir, resolvedPath);
@@ -6280,7 +6357,7 @@ In: ${bug.file}:${bug.line}`
6280
6357
  }
6281
6358
  let agenticResult;
6282
6359
  try {
6283
- agenticResult = await runAgenticFix(bug, config, projectDir);
6360
+ agenticResult = await runAgenticFix(bug, config, projectDir, options.onProgress);
6284
6361
  } catch (error) {
6285
6362
  return {
6286
6363
  success: false,
@@ -6320,76 +6397,153 @@ In: ${bug.file}:${bug.line}`
6320
6397
  commitHash
6321
6398
  };
6322
6399
  }
6323
- async function runAgenticFix(bug, config, projectDir) {
6400
+ async function runAgenticFix(bug, config, projectDir, onProgress) {
6324
6401
  const providerCommand = getProviderCommand(config.provider);
6325
6402
  const prompt = buildAgenticFixPrompt(bug);
6326
- const args = [];
6327
- if (config.provider === "claude-code") {
6328
- args.push("-p", prompt, "--dangerously-skip-permissions");
6329
- } else if (config.provider === "gemini") {
6330
- args.push("-p", prompt);
6331
- } else if (config.provider === "aider") {
6332
- args.push("--message", prompt, bug.file);
6333
- } else if (config.provider === "codex") ; else {
6334
- args.push("-p", prompt);
6335
- }
6403
+ const controller = new AbortController();
6404
+ const timeoutId = setTimeout(() => controller.abort(), 3e5);
6336
6405
  let stdout = "";
6337
6406
  let stderr = "";
6338
- if (config.provider === "codex") {
6339
- const tempDir = mkdtempSync(join(tmpdir(), "whiterose-fix-"));
6340
- const outputFile = join(tempDir, "output.txt");
6341
- try {
6342
- const result = await execa(
6407
+ try {
6408
+ if (config.provider === "codex") {
6409
+ const tempDir = mkdtempSync(join(tmpdir(), "whiterose-fix-"));
6410
+ const outputFile = join(tempDir, "output.txt");
6411
+ try {
6412
+ const result = await execa(
6413
+ providerCommand,
6414
+ [
6415
+ "exec",
6416
+ "--full-auto",
6417
+ // Allow workspace writes without approval prompts
6418
+ "--skip-git-repo-check",
6419
+ "-C",
6420
+ projectDir,
6421
+ // Set working directory for codex
6422
+ "-o",
6423
+ outputFile,
6424
+ "-"
6425
+ // Read prompt from stdin
6426
+ ],
6427
+ {
6428
+ cwd: projectDir,
6429
+ input: prompt,
6430
+ // Pass prompt via stdin
6431
+ timeout: 3e5,
6432
+ env: { ...process.env, NO_COLOR: "1" },
6433
+ reject: false,
6434
+ cancelSignal: controller.signal
6435
+ }
6436
+ );
6437
+ stderr = result.stderr || "";
6438
+ if (existsSync(outputFile)) {
6439
+ try {
6440
+ stdout = readFileSync(outputFile, "utf-8");
6441
+ } catch {
6442
+ stdout = result.stdout || "";
6443
+ }
6444
+ } else {
6445
+ stdout = result.stdout || "";
6446
+ }
6447
+ } finally {
6448
+ try {
6449
+ rmSync(tempDir, { recursive: true, force: true });
6450
+ } catch {
6451
+ }
6452
+ }
6453
+ } else if (config.provider === "claude-code") {
6454
+ const args = ["--dangerously-skip-permissions", "-p"];
6455
+ if (onProgress) {
6456
+ args.push("--verbose", "--output-format", "stream-json");
6457
+ }
6458
+ const subprocess = execa(
6343
6459
  providerCommand,
6344
- [
6345
- "exec",
6346
- "--full-auto",
6347
- // Allow workspace writes without approval prompts
6348
- "--skip-git-repo-check",
6349
- "-C",
6350
- projectDir,
6351
- // Set working directory for codex
6352
- "-o",
6353
- outputFile,
6354
- "-"
6355
- // Read prompt from stdin
6356
- ],
6460
+ args,
6357
6461
  {
6358
6462
  cwd: projectDir,
6359
6463
  input: prompt,
6360
- // Pass prompt via stdin
6464
+ // Pass prompt via stdin (Claude reads from stdin when no prompt arg provided)
6361
6465
  timeout: 3e5,
6362
6466
  env: { ...process.env, NO_COLOR: "1" },
6363
- reject: false
6467
+ reject: false,
6468
+ cancelSignal: controller.signal
6364
6469
  }
6365
6470
  );
6366
- stderr = result.stderr || "";
6367
- if (existsSync(outputFile)) {
6368
- try {
6369
- stdout = readFileSync(outputFile, "utf-8");
6370
- } catch {
6371
- stdout = result.stdout || "";
6372
- }
6373
- } else {
6374
- stdout = result.stdout || "";
6375
- }
6376
- } finally {
6377
- try {
6378
- rmSync(tempDir, { recursive: true, force: true });
6379
- } catch {
6471
+ if (onProgress && subprocess.stdout) {
6472
+ let lineBuffer = "";
6473
+ subprocess.stdout.on("data", (chunk) => {
6474
+ const text2 = chunk.toString();
6475
+ lineBuffer += text2;
6476
+ const lines = lineBuffer.split("\n");
6477
+ lineBuffer = lines.pop() || "";
6478
+ for (const line of lines) {
6479
+ const trimmed = line.trim();
6480
+ if (trimmed) {
6481
+ try {
6482
+ const event = JSON.parse(trimmed);
6483
+ if (event.type === "assistant" && event.message?.content) {
6484
+ for (const block of event.message.content) {
6485
+ if (block.type === "tool_use") {
6486
+ const toolName = block.name || "tool";
6487
+ onProgress(`Using ${toolName}...`);
6488
+ } else if (block.type === "text" && block.text) {
6489
+ const preview = block.text.substring(0, 80).replace(/\n/g, " ").trim();
6490
+ if (preview) {
6491
+ onProgress(preview + (block.text.length > 80 ? "..." : ""));
6492
+ }
6493
+ }
6494
+ }
6495
+ }
6496
+ } catch {
6497
+ if (trimmed.length > 3 && trimmed.length < 100) {
6498
+ onProgress(trimmed);
6499
+ }
6500
+ }
6501
+ }
6502
+ }
6503
+ });
6380
6504
  }
6505
+ const result = await subprocess;
6506
+ stdout = result.stdout || "";
6507
+ stderr = result.stderr || "";
6508
+ } else if (config.provider === "gemini") {
6509
+ const result = await execa(providerCommand, ["-p", prompt], {
6510
+ cwd: projectDir,
6511
+ timeout: 3e5,
6512
+ env: { ...process.env, NO_COLOR: "1" },
6513
+ reject: false,
6514
+ stdin: "ignore",
6515
+ // Prevent stdin hangs
6516
+ cancelSignal: controller.signal
6517
+ });
6518
+ stdout = result.stdout || "";
6519
+ stderr = result.stderr || "";
6520
+ } else if (config.provider === "aider") {
6521
+ const result = await execa(providerCommand, ["--message", prompt, bug.file], {
6522
+ cwd: projectDir,
6523
+ timeout: 3e5,
6524
+ env: { ...process.env, NO_COLOR: "1" },
6525
+ reject: false,
6526
+ stdin: "ignore",
6527
+ // Prevent stdin hangs
6528
+ cancelSignal: controller.signal
6529
+ });
6530
+ stdout = result.stdout || "";
6531
+ stderr = result.stderr || "";
6532
+ } else {
6533
+ const result = await execa(providerCommand, ["-p", prompt], {
6534
+ cwd: projectDir,
6535
+ timeout: 3e5,
6536
+ env: { ...process.env, NO_COLOR: "1" },
6537
+ reject: false,
6538
+ stdin: "ignore",
6539
+ // Prevent stdin hangs
6540
+ cancelSignal: controller.signal
6541
+ });
6542
+ stdout = result.stdout || "";
6543
+ stderr = result.stderr || "";
6381
6544
  }
6382
- } else {
6383
- const result = await execa(providerCommand, args, {
6384
- cwd: projectDir,
6385
- timeout: 3e5,
6386
- // 5 minute timeout for agentic operations
6387
- env: { ...process.env, NO_COLOR: "1" },
6388
- reject: false
6389
- // Don't throw on non-zero exit
6390
- });
6391
- stdout = result.stdout || "";
6392
- stderr = result.stderr || "";
6545
+ } finally {
6546
+ clearTimeout(timeoutId);
6393
6547
  }
6394
6548
  if (stderr) {
6395
6549
  const lowerStderr = stderr.toLowerCase();
@@ -6518,8 +6672,8 @@ function generateSimpleDiff(original, modified, filename) {
6518
6672
  }
6519
6673
  async function startFixTUI(bugs, config, options, cwd) {
6520
6674
  return new Promise((resolve6) => {
6521
- const handleFix = async (bug) => {
6522
- const result = await applyFix(bug, config, options);
6675
+ const handleFix = async (bug, onProgress) => {
6676
+ const result = await applyFix(bug, config, { ...options, onProgress });
6523
6677
  if (result.falsePositive) {
6524
6678
  if (cwd) {
6525
6679
  removeBugFromAccumulated(cwd, bug.id);
@@ -6678,39 +6832,67 @@ function loadBugsFromSarif(sarifPath) {
6678
6832
  } catch (error) {
6679
6833
  throw new Error(`Failed to parse SARIF file: ${sarifPath}. File may be corrupted or malformed.`);
6680
6834
  }
6681
- return sarif.runs?.[0]?.results?.map((r, i) => {
6835
+ if (!sarif || typeof sarif !== "object") {
6836
+ throw new Error(`Invalid SARIF file: ${sarifPath}. Expected a JSON object.`);
6837
+ }
6838
+ const runs = sarif.runs;
6839
+ if (!Array.isArray(runs) || runs.length === 0) {
6840
+ return [];
6841
+ }
6842
+ const results = runs[0]?.results;
6843
+ if (!Array.isArray(results)) {
6844
+ return [];
6845
+ }
6846
+ return results.map((r, i) => {
6847
+ if (!r || typeof r !== "object") {
6848
+ throw new Error(`Invalid SARIF result at index ${i}: expected an object.`);
6849
+ }
6682
6850
  const props = r.properties || {};
6851
+ const rawTitle = r.message?.text || "Unknown bug";
6852
+ const rawDescription = r.message?.markdown || r.message?.text || "";
6853
+ const rawCodePath = r.codeFlows?.[0]?.threadFlows?.[0]?.locations?.map((loc, idx) => ({
6854
+ step: idx + 1,
6855
+ file: loc.location?.physicalLocation?.artifactLocation?.uri || "",
6856
+ line: loc.location?.physicalLocation?.region?.startLine || 0,
6857
+ code: "",
6858
+ explanation: loc.message?.text || ""
6859
+ })) || [];
6860
+ const rawFile = r.locations?.[0]?.physicalLocation?.artifactLocation?.uri;
6861
+ const file = typeof rawFile === "string" ? rawFile : "unknown";
6862
+ const rawLine = r.locations?.[0]?.physicalLocation?.region?.startLine;
6863
+ const line = typeof rawLine === "number" && Number.isFinite(rawLine) ? Math.floor(rawLine) : 0;
6864
+ const rawEndLine = r.locations?.[0]?.physicalLocation?.region?.endLine;
6865
+ const endLine = typeof rawEndLine === "number" && Number.isFinite(rawEndLine) ? Math.floor(rawEndLine) : void 0;
6866
+ const rawId = r.ruleId;
6867
+ const id = typeof rawId === "string" ? rawId : `WR-${String(i + 1).padStart(3, "0")}`;
6868
+ const validatedKind = FindingKind.safeParse(props.kind);
6869
+ const validatedCategory = BugCategory.safeParse(props.category);
6870
+ const validatedConfidence = ConfidenceLevel.safeParse(props.confidence);
6683
6871
  return {
6684
- id: r.ruleId || `WR-${String(i + 1).padStart(3, "0")}`,
6685
- title: r.message?.text || "Unknown bug",
6686
- description: r.message?.markdown || r.message?.text || "",
6687
- file: r.locations?.[0]?.physicalLocation?.artifactLocation?.uri || "unknown",
6688
- line: r.locations?.[0]?.physicalLocation?.region?.startLine || 0,
6689
- endLine: r.locations?.[0]?.physicalLocation?.region?.endLine,
6690
- kind: props.kind || "bug",
6872
+ id,
6873
+ title: sanitizeSarifText(String(rawTitle), "title"),
6874
+ description: sanitizeSarifText(String(rawDescription), "description"),
6875
+ file,
6876
+ line,
6877
+ endLine,
6878
+ kind: validatedKind.success ? validatedKind.data : "bug",
6691
6879
  severity: mapSarifLevel(r.level),
6692
- category: props.category || "logic-error",
6880
+ category: validatedCategory.success ? validatedCategory.data : "logic-error",
6693
6881
  confidence: {
6694
- overall: props.confidence || "medium",
6695
- codePathValidity: props.codePathValidity || 0.8,
6696
- reachability: props.reachability || 0.8,
6697
- intentViolation: props.intentViolation || false,
6698
- staticToolSignal: props.staticToolSignal || false,
6699
- adversarialSurvived: props.adversarialSurvived || false
6882
+ overall: validatedConfidence.success ? validatedConfidence.data : "medium",
6883
+ codePathValidity: typeof props.codePathValidity === "number" ? props.codePathValidity : 0.8,
6884
+ reachability: typeof props.reachability === "number" ? props.reachability : 0.8,
6885
+ intentViolation: typeof props.intentViolation === "boolean" ? props.intentViolation : false,
6886
+ staticToolSignal: typeof props.staticToolSignal === "boolean" ? props.staticToolSignal : false,
6887
+ adversarialSurvived: typeof props.adversarialSurvived === "boolean" ? props.adversarialSurvived : false
6700
6888
  },
6701
- codePath: r.codeFlows?.[0]?.threadFlows?.[0]?.locations?.map((loc, idx) => ({
6702
- step: idx + 1,
6703
- file: loc.location?.physicalLocation?.artifactLocation?.uri || "",
6704
- line: loc.location?.physicalLocation?.region?.startLine || 0,
6705
- code: "",
6706
- explanation: loc.message?.text || ""
6707
- })) || [],
6708
- evidence: props.evidence || [],
6709
- suggestedFix: props.suggestedFix,
6889
+ codePath: sanitizeSarifCodePath(rawCodePath),
6890
+ evidence: sanitizeSarifEvidence(props.evidence),
6891
+ suggestedFix: props.suggestedFix ? sanitizeSarifText(String(props.suggestedFix), "suggestedFix") : void 0,
6710
6892
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6711
6893
  status: "open"
6712
6894
  };
6713
- }) || [];
6895
+ });
6714
6896
  }
6715
6897
  async function loadBugFromGitHub(issueUrl, cwd) {
6716
6898
  try {
@@ -6950,12 +7132,24 @@ async function fixSingleBug(bug, config, options, cwd) {
6950
7132
  process.exit(0);
6951
7133
  }
6952
7134
  }
6953
- const spinner6 = p3.spinner();
6954
- spinner6.start(options.dryRun ? "Generating fix preview..." : "Applying fix...");
7135
+ console.log();
7136
+ console.log(chalk3.cyan(" \u25C6 Starting agentic fix..."));
7137
+ console.log();
6955
7138
  try {
6956
- const result = await applyFix(bug, config, options);
7139
+ let lastMessage = "";
7140
+ const result = await applyFix(bug, config, {
7141
+ ...options,
7142
+ onProgress: (message) => {
7143
+ if (message !== lastMessage) {
7144
+ lastMessage = message;
7145
+ const truncated = message.length > 72 ? message.substring(0, 72) + "..." : message;
7146
+ process.stdout.write(`\r\x1B[K ${chalk3.dim("\u203A")} ${chalk3.gray(truncated)}`);
7147
+ }
7148
+ }
7149
+ });
7150
+ process.stdout.write("\r\x1B[K");
6957
7151
  if (result.success) {
6958
- spinner6.stop(options.dryRun ? "Fix preview generated" : "Fix applied");
7152
+ console.log(chalk3.green(" \u2713 Fix applied successfully"));
6959
7153
  if (result.diff) {
6960
7154
  console.log();
6961
7155
  console.log(chalk3.dim(" Changes:"));
@@ -6981,12 +7175,12 @@ async function fixSingleBug(bug, config, options, cwd) {
6981
7175
  }
6982
7176
  p3.outro(chalk3.green("Fix complete!"));
6983
7177
  } else {
6984
- spinner6.stop("Fix failed");
7178
+ console.log(chalk3.red(" \u2717 Fix failed"));
6985
7179
  p3.log.error(result.error || "Unknown error");
6986
7180
  process.exit(1);
6987
7181
  }
6988
7182
  } catch (error) {
6989
- spinner6.stop("Fix failed");
7183
+ console.log(chalk3.red(" \u2717 Fix failed"));
6990
7184
  p3.log.error(error.message);
6991
7185
  process.exit(1);
6992
7186
  }