@shakecodeslikecray/whiterose 1.0.3 → 1.0.5

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
@@ -2116,11 +2116,25 @@ var CoreScanner = class {
2116
2116
  executor;
2117
2117
  config;
2118
2118
  progress;
2119
+ passErrors = [];
2119
2120
  constructor(executor, config = {}, progress = {}) {
2120
2121
  this.executor = executor;
2121
2122
  this.config = { ...DEFAULT_SCANNER_CONFIG, ...config };
2122
2123
  this.progress = progress;
2123
2124
  }
2125
+ /**
2126
+ * Get errors that occurred during the last scan.
2127
+ * Returns an array of pass names and their error messages.
2128
+ */
2129
+ getPassErrors() {
2130
+ return this.passErrors;
2131
+ }
2132
+ /**
2133
+ * Check if any passes failed during the last scan.
2134
+ */
2135
+ hasPassErrors() {
2136
+ return this.passErrors.length > 0;
2137
+ }
2124
2138
  /**
2125
2139
  * Run a thorough 19-pass scan with findings flowing through pipeline:
2126
2140
  *
@@ -2134,6 +2148,7 @@ var CoreScanner = class {
2134
2148
  async scan(context) {
2135
2149
  const cwd = process.cwd();
2136
2150
  const startTime = Date.now();
2151
+ this.passErrors = [];
2137
2152
  const pipeline = getFullAnalysisPipeline();
2138
2153
  const unitPasses = pipeline[0].passes;
2139
2154
  const integrationPasses = pipeline[1].passes;
@@ -2214,8 +2229,10 @@ var CoreScanner = class {
2214
2229
  this.report(` \u2713 ${pass.name}: ${bugs.length} bugs`);
2215
2230
  return bugs;
2216
2231
  } catch (error) {
2217
- this.progress.onPassError?.(pass.name, error.message);
2218
- this.report(` \u2717 ${pass.name}: ${error.message}`);
2232
+ const errorMsg = error.message || String(error);
2233
+ this.progress.onPassError?.(pass.name, errorMsg);
2234
+ this.report(` \u2717 ${pass.name}: ${errorMsg}`);
2235
+ this.passErrors.push({ passName: pass.name, error: errorMsg });
2219
2236
  return [];
2220
2237
  }
2221
2238
  });
@@ -3089,7 +3106,7 @@ function extractIntentFromDocs(docs) {
3089
3106
  }
3090
3107
  }
3091
3108
  if (docs.readme) {
3092
- const featuresMatch = docs.readme.match(/##\s*Features?\s*\n([\s\S]*?)(?=\n##|\n---|\$)/i);
3109
+ const featuresMatch = docs.readme.match(/##\s*Features?\s*\n([\s\S]*?)(?=\n##|\n---|$)/i);
3093
3110
  if (featuresMatch) {
3094
3111
  const featureLines = featuresMatch[1].split("\n").filter((line) => line.trim().startsWith("-") || line.trim().startsWith("*")).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter((line) => line.length > 0);
3095
3112
  intent.features.push(...featureLines.slice(0, 20));
@@ -4327,6 +4344,9 @@ function loadAccumulatedBugs(cwd) {
4327
4344
  if (!Array.isArray(stored.bugs)) {
4328
4345
  stored.bugs = [];
4329
4346
  }
4347
+ if (!stored.fingerprints || typeof stored.fingerprints !== "object") {
4348
+ stored.fingerprints = {};
4349
+ }
4330
4350
  stored.bugs = stored.bugs.map((b) => ({ ...b, kind: b.kind || "bug" }));
4331
4351
  return stored;
4332
4352
  } catch (error) {
@@ -5875,7 +5895,9 @@ var FixConfirm = ({ bug, dryRun, onConfirm, onCancel, onFixComplete }) => {
5875
5895
  setStatus("fixing");
5876
5896
  setProgressMessage("Starting agentic fix...");
5877
5897
  try {
5878
- const result = await onConfirm();
5898
+ const result = await onConfirm((message) => {
5899
+ setProgressMessage(message);
5900
+ });
5879
5901
  if (result.falsePositive) {
5880
5902
  setFalsePositiveReason(result.falsePositiveReason || "The AI determined this bug is not real after analyzing the code.");
5881
5903
  setStatus("false-positive");
@@ -6019,11 +6041,11 @@ var App = ({ bugs, config, fixOptions, onFix, onExit }) => {
6019
6041
  };
6020
6042
  const [fixError, setFixError] = useState(null);
6021
6043
  const [lastFixedBugId, setLastFixedBugId] = useState(null);
6022
- const handleConfirmFix = async () => {
6044
+ const handleConfirmFix = async (onProgress) => {
6023
6045
  if (selectedBug) {
6024
6046
  setFixError(null);
6025
6047
  setLastFixedBugId(selectedBug.id);
6026
- const result = await onFix(selectedBug);
6048
+ const result = await onFix(selectedBug, onProgress);
6027
6049
  return result;
6028
6050
  }
6029
6051
  return { falsePositive: false };
@@ -6228,6 +6250,67 @@ function markBugAsFixed(bug, commitHash, cwd = process.cwd()) {
6228
6250
  }
6229
6251
 
6230
6252
  // src/core/fixer.ts
6253
+ var MAX_SARIF_TEXT_LENGTH = 2e3;
6254
+ var PROMPT_INJECTION_PATTERNS = [
6255
+ // Attempts to override or ignore instructions
6256
+ /ignore\s+(previous|all|above)\s+instructions?/i,
6257
+ /disregard\s+(previous|all|above)\s+instructions?/i,
6258
+ /forget\s+(previous|all|above)\s+instructions?/i,
6259
+ // Attempts to define new roles or personas
6260
+ /you\s+are\s+(now|actually)\s+/i,
6261
+ /act\s+as\s+(a|an)\s+/i,
6262
+ /pretend\s+(you|to\s+be)\s+/i,
6263
+ // Attempts to inject system-level commands
6264
+ /\[SYSTEM\]/i,
6265
+ /\[INST\]/i,
6266
+ /<\|system\|>/i,
6267
+ /<\|assistant\|>/i,
6268
+ /<\|user\|>/i,
6269
+ // Attempts to break out with special markers
6270
+ /###\s*(INSTRUCTION|SYSTEM|END|NEW)/i,
6271
+ /```\s*(system|instruction)/i,
6272
+ // Direct file operation injections
6273
+ /delete\s+(all|the)\s+files?/i,
6274
+ /rm\s+-rf\s+/i,
6275
+ /remove\s+(all|every)\s+file/i,
6276
+ // Exfiltration attempts
6277
+ /send\s+(this|the|all)\s+(data|content|file)/i,
6278
+ /upload\s+(to|this)/i,
6279
+ /curl\s+.*\s+-d/i,
6280
+ /fetch\s*\(\s*['"][^'"]*['"]\s*,\s*\{[^}]*method\s*:\s*['"]POST['"]/i
6281
+ ];
6282
+ function sanitizeSarifText(text2, fieldName = "field") {
6283
+ if (!text2 || typeof text2 !== "string") {
6284
+ return "";
6285
+ }
6286
+ let sanitized = text2.length > MAX_SARIF_TEXT_LENGTH ? text2.substring(0, MAX_SARIF_TEXT_LENGTH) + `... [truncated ${fieldName}]` : text2;
6287
+ for (const pattern of PROMPT_INJECTION_PATTERNS) {
6288
+ if (pattern.test(sanitized)) {
6289
+ sanitized = sanitized.replace(pattern, "[REDACTED: potential injection]");
6290
+ }
6291
+ }
6292
+ sanitized = sanitized.replace(/```+/g, "`\u200B`\u200B`");
6293
+ sanitized = sanitized.replace(/###/g, "#\u200B#\u200B#");
6294
+ return sanitized;
6295
+ }
6296
+ function sanitizeSarifEvidence(evidence) {
6297
+ if (!Array.isArray(evidence)) {
6298
+ return [];
6299
+ }
6300
+ return evidence.filter((e) => typeof e === "string").slice(0, 10).map((e) => sanitizeSarifText(e, "evidence"));
6301
+ }
6302
+ function sanitizeSarifCodePath(codePath) {
6303
+ if (!Array.isArray(codePath)) {
6304
+ return [];
6305
+ }
6306
+ return codePath.slice(0, 20).map((entry, idx) => ({
6307
+ step: typeof entry?.step === "number" ? entry.step : idx + 1,
6308
+ file: sanitizeSarifText(String(entry?.file || ""), "codePath.file").substring(0, 500),
6309
+ line: typeof entry?.line === "number" ? entry.line : 0,
6310
+ code: sanitizeSarifText(String(entry?.code || ""), "codePath.code"),
6311
+ explanation: sanitizeSarifText(String(entry?.explanation || ""), "codePath.explanation")
6312
+ }));
6313
+ }
6231
6314
  function isPathWithinProject(filePath, projectDir) {
6232
6315
  const resolvedPath = resolve(projectDir, filePath);
6233
6316
  const relativePath = relative(projectDir, resolvedPath);
@@ -6291,7 +6374,7 @@ In: ${bug.file}:${bug.line}`
6291
6374
  }
6292
6375
  let agenticResult;
6293
6376
  try {
6294
- agenticResult = await runAgenticFix(bug, config, projectDir);
6377
+ agenticResult = await runAgenticFix(bug, config, projectDir, options.onProgress);
6295
6378
  } catch (error) {
6296
6379
  return {
6297
6380
  success: false,
@@ -6331,7 +6414,7 @@ In: ${bug.file}:${bug.line}`
6331
6414
  commitHash
6332
6415
  };
6333
6416
  }
6334
- async function runAgenticFix(bug, config, projectDir) {
6417
+ async function runAgenticFix(bug, config, projectDir, onProgress) {
6335
6418
  const providerCommand = getProviderCommand(config.provider);
6336
6419
  const prompt = buildAgenticFixPrompt(bug);
6337
6420
  const controller = new AbortController();
@@ -6385,9 +6468,13 @@ async function runAgenticFix(bug, config, projectDir) {
6385
6468
  }
6386
6469
  }
6387
6470
  } else if (config.provider === "claude-code") {
6388
- const result = await execa(
6471
+ const args = ["--dangerously-skip-permissions", "-p"];
6472
+ if (onProgress) {
6473
+ args.push("--verbose", "--output-format", "stream-json");
6474
+ }
6475
+ const subprocess = execa(
6389
6476
  providerCommand,
6390
- ["--dangerously-skip-permissions", "-p"],
6477
+ args,
6391
6478
  {
6392
6479
  cwd: projectDir,
6393
6480
  input: prompt,
@@ -6398,6 +6485,43 @@ async function runAgenticFix(bug, config, projectDir) {
6398
6485
  cancelSignal: controller.signal
6399
6486
  }
6400
6487
  );
6488
+ if (onProgress && subprocess.stdout) {
6489
+ let lineBuffer = "";
6490
+ subprocess.stdout.on("data", (chunk) => {
6491
+ const text2 = chunk.toString();
6492
+ lineBuffer += text2;
6493
+ const lines = lineBuffer.split("\n");
6494
+ lineBuffer = lines.pop() || "";
6495
+ for (const line of lines) {
6496
+ const trimmed = line.trim();
6497
+ if (trimmed) {
6498
+ try {
6499
+ const event = JSON.parse(trimmed);
6500
+ if (event.type === "assistant" && event.message?.content) {
6501
+ for (const block of event.message.content) {
6502
+ if (block.type === "tool_use") {
6503
+ const toolName = block.name || "tool";
6504
+ const friendlyNames = {
6505
+ "Read": "Reading file",
6506
+ "Edit": "Editing file",
6507
+ "Write": "Writing file",
6508
+ "Bash": "Running command",
6509
+ "Glob": "Searching files",
6510
+ "Grep": "Searching content",
6511
+ "Task": "Running task"
6512
+ };
6513
+ const displayName = friendlyNames[toolName] || `Using ${toolName}`;
6514
+ onProgress(`${displayName}...`);
6515
+ }
6516
+ }
6517
+ }
6518
+ } catch {
6519
+ }
6520
+ }
6521
+ }
6522
+ });
6523
+ }
6524
+ const result = await subprocess;
6401
6525
  stdout = result.stdout || "";
6402
6526
  stderr = result.stderr || "";
6403
6527
  } else if (config.provider === "gemini") {
@@ -6567,8 +6691,8 @@ function generateSimpleDiff(original, modified, filename) {
6567
6691
  }
6568
6692
  async function startFixTUI(bugs, config, options, cwd) {
6569
6693
  return new Promise((resolve6) => {
6570
- const handleFix = async (bug) => {
6571
- const result = await applyFix(bug, config, options);
6694
+ const handleFix = async (bug, onProgress) => {
6695
+ const result = await applyFix(bug, config, { ...options, onProgress });
6572
6696
  if (result.falsePositive) {
6573
6697
  if (cwd) {
6574
6698
  removeBugFromAccumulated(cwd, bug.id);
@@ -6727,39 +6851,67 @@ function loadBugsFromSarif(sarifPath) {
6727
6851
  } catch (error) {
6728
6852
  throw new Error(`Failed to parse SARIF file: ${sarifPath}. File may be corrupted or malformed.`);
6729
6853
  }
6730
- return sarif.runs?.[0]?.results?.map((r, i) => {
6854
+ if (!sarif || typeof sarif !== "object") {
6855
+ throw new Error(`Invalid SARIF file: ${sarifPath}. Expected a JSON object.`);
6856
+ }
6857
+ const runs = sarif.runs;
6858
+ if (!Array.isArray(runs) || runs.length === 0) {
6859
+ return [];
6860
+ }
6861
+ const results = runs[0]?.results;
6862
+ if (!Array.isArray(results)) {
6863
+ return [];
6864
+ }
6865
+ return results.map((r, i) => {
6866
+ if (!r || typeof r !== "object") {
6867
+ throw new Error(`Invalid SARIF result at index ${i}: expected an object.`);
6868
+ }
6731
6869
  const props = r.properties || {};
6870
+ const rawTitle = r.message?.text || "Unknown bug";
6871
+ const rawDescription = r.message?.markdown || r.message?.text || "";
6872
+ const rawCodePath = r.codeFlows?.[0]?.threadFlows?.[0]?.locations?.map((loc, idx) => ({
6873
+ step: idx + 1,
6874
+ file: loc.location?.physicalLocation?.artifactLocation?.uri || "",
6875
+ line: loc.location?.physicalLocation?.region?.startLine || 0,
6876
+ code: "",
6877
+ explanation: loc.message?.text || ""
6878
+ })) || [];
6879
+ const rawFile = r.locations?.[0]?.physicalLocation?.artifactLocation?.uri;
6880
+ const file = typeof rawFile === "string" ? rawFile : "unknown";
6881
+ const rawLine = r.locations?.[0]?.physicalLocation?.region?.startLine;
6882
+ const line = typeof rawLine === "number" && Number.isFinite(rawLine) ? Math.floor(rawLine) : 0;
6883
+ const rawEndLine = r.locations?.[0]?.physicalLocation?.region?.endLine;
6884
+ const endLine = typeof rawEndLine === "number" && Number.isFinite(rawEndLine) ? Math.floor(rawEndLine) : void 0;
6885
+ const rawId = r.ruleId;
6886
+ const id = typeof rawId === "string" ? rawId : `WR-${String(i + 1).padStart(3, "0")}`;
6887
+ const validatedKind = FindingKind.safeParse(props.kind);
6888
+ const validatedCategory = BugCategory.safeParse(props.category);
6889
+ const validatedConfidence = ConfidenceLevel.safeParse(props.confidence);
6732
6890
  return {
6733
- id: r.ruleId || `WR-${String(i + 1).padStart(3, "0")}`,
6734
- title: r.message?.text || "Unknown bug",
6735
- description: r.message?.markdown || r.message?.text || "",
6736
- file: r.locations?.[0]?.physicalLocation?.artifactLocation?.uri || "unknown",
6737
- line: r.locations?.[0]?.physicalLocation?.region?.startLine || 0,
6738
- endLine: r.locations?.[0]?.physicalLocation?.region?.endLine,
6739
- kind: props.kind || "bug",
6891
+ id,
6892
+ title: sanitizeSarifText(String(rawTitle), "title"),
6893
+ description: sanitizeSarifText(String(rawDescription), "description"),
6894
+ file,
6895
+ line,
6896
+ endLine,
6897
+ kind: validatedKind.success ? validatedKind.data : "bug",
6740
6898
  severity: mapSarifLevel(r.level),
6741
- category: props.category || "logic-error",
6899
+ category: validatedCategory.success ? validatedCategory.data : "logic-error",
6742
6900
  confidence: {
6743
- overall: props.confidence || "medium",
6744
- codePathValidity: props.codePathValidity || 0.8,
6745
- reachability: props.reachability || 0.8,
6746
- intentViolation: props.intentViolation || false,
6747
- staticToolSignal: props.staticToolSignal || false,
6748
- adversarialSurvived: props.adversarialSurvived || false
6901
+ overall: validatedConfidence.success ? validatedConfidence.data : "medium",
6902
+ codePathValidity: typeof props.codePathValidity === "number" ? props.codePathValidity : 0.8,
6903
+ reachability: typeof props.reachability === "number" ? props.reachability : 0.8,
6904
+ intentViolation: typeof props.intentViolation === "boolean" ? props.intentViolation : false,
6905
+ staticToolSignal: typeof props.staticToolSignal === "boolean" ? props.staticToolSignal : false,
6906
+ adversarialSurvived: typeof props.adversarialSurvived === "boolean" ? props.adversarialSurvived : false
6749
6907
  },
6750
- codePath: r.codeFlows?.[0]?.threadFlows?.[0]?.locations?.map((loc, idx) => ({
6751
- step: idx + 1,
6752
- file: loc.location?.physicalLocation?.artifactLocation?.uri || "",
6753
- line: loc.location?.physicalLocation?.region?.startLine || 0,
6754
- code: "",
6755
- explanation: loc.message?.text || ""
6756
- })) || [],
6757
- evidence: props.evidence || [],
6758
- suggestedFix: props.suggestedFix,
6908
+ codePath: sanitizeSarifCodePath(rawCodePath),
6909
+ evidence: sanitizeSarifEvidence(props.evidence),
6910
+ suggestedFix: props.suggestedFix ? sanitizeSarifText(String(props.suggestedFix), "suggestedFix") : void 0,
6759
6911
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6760
6912
  status: "open"
6761
6913
  };
6762
- }) || [];
6914
+ });
6763
6915
  }
6764
6916
  async function loadBugFromGitHub(issueUrl, cwd) {
6765
6917
  try {
@@ -6802,10 +6954,12 @@ async function loadBugFromGitHub(issueUrl, cwd) {
6802
6954
  } else if (labels.some((l) => l.includes("leak") || l.includes("memory"))) {
6803
6955
  category = "resource-leak";
6804
6956
  }
6957
+ const sanitizedTitle = sanitizeSarifText(String(issue.title || ""), "github.title");
6958
+ const sanitizedBody = sanitizeSarifText(String(issue.body || ""), "github.body");
6805
6959
  return {
6806
6960
  id: `GH-${issueNumber}`,
6807
- title: issue.title,
6808
- description: issue.body || issue.title,
6961
+ title: sanitizedTitle,
6962
+ description: sanitizedBody || sanitizedTitle,
6809
6963
  file: fileMatch?.[1] || "",
6810
6964
  line: parseInt(lineMatch?.[1] || "1", 10),
6811
6965
  kind: "bug",
@@ -6999,12 +7153,24 @@ async function fixSingleBug(bug, config, options, cwd) {
6999
7153
  process.exit(0);
7000
7154
  }
7001
7155
  }
7002
- const spinner6 = p3.spinner();
7003
- spinner6.start(options.dryRun ? "Generating fix preview..." : "Applying fix...");
7156
+ console.log();
7157
+ console.log(chalk3.cyan(" \u25C6 Starting agentic fix..."));
7158
+ console.log();
7004
7159
  try {
7005
- const result = await applyFix(bug, config, options);
7160
+ let lastMessage = "";
7161
+ const result = await applyFix(bug, config, {
7162
+ ...options,
7163
+ onProgress: (message) => {
7164
+ if (message !== lastMessage) {
7165
+ lastMessage = message;
7166
+ const truncated = message.length > 72 ? message.substring(0, 72) + "..." : message;
7167
+ process.stdout.write(`\r\x1B[K ${chalk3.dim("\u203A")} ${chalk3.gray(truncated)}`);
7168
+ }
7169
+ }
7170
+ });
7171
+ process.stdout.write("\r\x1B[K");
7006
7172
  if (result.success) {
7007
- spinner6.stop(options.dryRun ? "Fix preview generated" : "Fix applied");
7173
+ console.log(chalk3.green(" \u2713 Fix applied successfully"));
7008
7174
  if (result.diff) {
7009
7175
  console.log();
7010
7176
  console.log(chalk3.dim(" Changes:"));
@@ -7030,12 +7196,12 @@ async function fixSingleBug(bug, config, options, cwd) {
7030
7196
  }
7031
7197
  p3.outro(chalk3.green("Fix complete!"));
7032
7198
  } else {
7033
- spinner6.stop("Fix failed");
7199
+ console.log(chalk3.red(" \u2717 Fix failed"));
7034
7200
  p3.log.error(result.error || "Unknown error");
7035
7201
  process.exit(1);
7036
7202
  }
7037
7203
  } catch (error) {
7038
- spinner6.stop("Fix failed");
7204
+ console.log(chalk3.red(" \u2717 Fix failed"));
7039
7205
  p3.log.error(error.message);
7040
7206
  process.exit(1);
7041
7207
  }