@shakecodeslikecray/whiterose 1.0.7 → 1.0.9
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 +160 -43
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +140 -39
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -307,14 +307,24 @@ var ClaudeCodeExecutor = class {
|
|
|
307
307
|
}
|
|
308
308
|
async runPrompt(prompt, options) {
|
|
309
309
|
const claudeCommand = getProviderCommand("claude-code");
|
|
310
|
+
if (process.env.WHITEROSE_DEBUG) {
|
|
311
|
+
console.log("\n[DEBUG] Running Claude Code command:", claudeCommand);
|
|
312
|
+
console.log("[DEBUG] Prompt length:", prompt.length);
|
|
313
|
+
console.log("[DEBUG] CWD:", options.cwd);
|
|
314
|
+
console.log("[DEBUG] First 500 chars of prompt:");
|
|
315
|
+
console.log(prompt.substring(0, 500));
|
|
316
|
+
}
|
|
310
317
|
try {
|
|
311
318
|
const { stdout, stderr } = await execa(
|
|
312
319
|
claudeCommand,
|
|
313
320
|
[
|
|
314
321
|
"-p",
|
|
315
322
|
prompt,
|
|
316
|
-
"--dangerously-skip-permissions"
|
|
323
|
+
"--dangerously-skip-permissions",
|
|
317
324
|
// Allow file reads without prompts
|
|
325
|
+
"--output-format",
|
|
326
|
+
"text"
|
|
327
|
+
// Ensure non-interactive output
|
|
318
328
|
],
|
|
319
329
|
{
|
|
320
330
|
cwd: options.cwd,
|
|
@@ -323,7 +333,9 @@ var ClaudeCodeExecutor = class {
|
|
|
323
333
|
...process.env,
|
|
324
334
|
NO_COLOR: "1"
|
|
325
335
|
},
|
|
326
|
-
reject: false
|
|
336
|
+
reject: false,
|
|
337
|
+
stdin: "ignore"
|
|
338
|
+
// Prevent waiting for stdin
|
|
327
339
|
}
|
|
328
340
|
);
|
|
329
341
|
if (stderr) {
|
|
@@ -340,6 +352,13 @@ var ClaudeCodeExecutor = class {
|
|
|
340
352
|
throw new Error(`Claude Code error: ${stderr.substring(0, 200)}`);
|
|
341
353
|
}
|
|
342
354
|
}
|
|
355
|
+
if (process.env.WHITEROSE_DEBUG) {
|
|
356
|
+
console.log("\n[DEBUG] Claude Code stdout length:", stdout?.length || 0);
|
|
357
|
+
console.log("[DEBUG] Claude Code stderr:", stderr?.substring(0, 300) || "(none)");
|
|
358
|
+
console.log("[DEBUG] First 1000 chars of stdout:");
|
|
359
|
+
console.log(stdout?.substring(0, 1e3) || "(empty)");
|
|
360
|
+
console.log("[DEBUG] End response\n");
|
|
361
|
+
}
|
|
343
362
|
return {
|
|
344
363
|
output: stdout || "",
|
|
345
364
|
error: stderr || void 0
|
|
@@ -443,6 +462,17 @@ var GeminiExecutor = class {
|
|
|
443
462
|
reject: false
|
|
444
463
|
}
|
|
445
464
|
);
|
|
465
|
+
if (stderr) {
|
|
466
|
+
if (stderr.includes("429") || stderr.includes("rate limit") || stderr.includes("quota exceeded")) {
|
|
467
|
+
throw new Error("Gemini API rate limit reached. Try again later.");
|
|
468
|
+
}
|
|
469
|
+
if (stderr.includes("401") || stderr.includes("403") || stderr.includes("unauthorized") || stderr.includes("invalid api key")) {
|
|
470
|
+
throw new Error("Gemini API authentication failed. Check your API key.");
|
|
471
|
+
}
|
|
472
|
+
if ((stderr.includes("Error") || stderr.includes("error")) && !stdout) {
|
|
473
|
+
throw new Error(`Gemini error: ${stderr.substring(0, 200)}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
446
476
|
return {
|
|
447
477
|
output: stdout || "",
|
|
448
478
|
error: stderr || void 0
|
|
@@ -483,6 +513,20 @@ var AiderExecutor = class {
|
|
|
483
513
|
reject: false
|
|
484
514
|
}
|
|
485
515
|
);
|
|
516
|
+
if (stderr) {
|
|
517
|
+
if (stderr.includes("429") || stderr.includes("rate limit") || stderr.includes("RateLimitError")) {
|
|
518
|
+
throw new Error("Aider API rate limit reached. Try again later.");
|
|
519
|
+
}
|
|
520
|
+
if (stderr.includes("401") || stderr.includes("AuthenticationError") || stderr.includes("invalid api key")) {
|
|
521
|
+
throw new Error("Aider API authentication failed. Check your API key.");
|
|
522
|
+
}
|
|
523
|
+
if (stderr.includes("402") || stderr.includes("insufficient") || stderr.includes("billing")) {
|
|
524
|
+
throw new Error("Aider API billing error. Check your account credits.");
|
|
525
|
+
}
|
|
526
|
+
if ((stderr.includes("Error") || stderr.includes("error")) && !stdout) {
|
|
527
|
+
throw new Error(`Aider error: ${stderr.substring(0, 200)}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
486
530
|
return {
|
|
487
531
|
output: stdout || "",
|
|
488
532
|
error: stderr || void 0
|
|
@@ -522,6 +566,20 @@ var OllamaExecutor = class {
|
|
|
522
566
|
reject: false
|
|
523
567
|
}
|
|
524
568
|
);
|
|
569
|
+
if (stderr) {
|
|
570
|
+
if (stderr.includes("connection refused") || stderr.includes("ECONNREFUSED")) {
|
|
571
|
+
throw new Error("Ollama server not running. Start it with: ollama serve");
|
|
572
|
+
}
|
|
573
|
+
if (stderr.includes("model") && (stderr.includes("not found") || stderr.includes("does not exist"))) {
|
|
574
|
+
throw new Error(`Ollama model '${this.model}' not found. Run: ollama pull ${this.model}`);
|
|
575
|
+
}
|
|
576
|
+
if (stderr.includes("out of memory") || stderr.includes("OOM")) {
|
|
577
|
+
throw new Error("Ollama out of memory. Try a smaller model or increase system memory.");
|
|
578
|
+
}
|
|
579
|
+
if ((stderr.includes("Error") || stderr.includes("error")) && !stdout) {
|
|
580
|
+
throw new Error(`Ollama error: ${stderr.substring(0, 200)}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
525
583
|
return {
|
|
526
584
|
output: stdout || "",
|
|
527
585
|
error: stderr || void 0
|
|
@@ -1156,23 +1214,18 @@ ${pass.falsePositiveHints.map((h) => `- ${h}`).join("\n")}
|
|
|
1156
1214
|
${cwePatterns ? `## KNOWN VULNERABILITY PATTERNS
|
|
1157
1215
|
${cwePatterns}` : ""}
|
|
1158
1216
|
|
|
1159
|
-
##
|
|
1217
|
+
## REPORTING GUIDELINES
|
|
1160
1218
|
|
|
1161
|
-
1. **ONLY ${pass.name.toUpperCase()}
|
|
1162
|
-
2. **
|
|
1163
|
-
3. **
|
|
1164
|
-
4. **
|
|
1165
|
-
5. **BE SPECIFIC** -
|
|
1166
|
-
6. **
|
|
1167
|
-
- Look for early returns: \`if (x.length === 0) return;\` makes later \`x[0]\` SAFE
|
|
1168
|
-
- Look for throws: \`if (!user) throw new Error()\` makes later \`user.name\` SAFE
|
|
1169
|
-
- Look for guard clauses that exit before the buggy line
|
|
1170
|
-
- If a condition causes function exit, code after is UNREACHABLE in that case
|
|
1171
|
-
- Example FALSE POSITIVE: \`if (arr.length === 0) return false; ... arr[0].name\` - the access is SAFE because empty array caused early return
|
|
1219
|
+
1. **ONLY ${pass.name.toUpperCase()} ISSUES** - Focus on this category
|
|
1220
|
+
2. **REPORT POTENTIAL ISSUES** - If something looks suspicious, report it. Better to flag potential issues than miss real bugs.
|
|
1221
|
+
3. **INCLUDE CODE SMELLS** - Report risky patterns even if not immediately exploitable (set kind: "smell")
|
|
1222
|
+
4. **TRACE THE DATA** - Follow user input to potentially vulnerable sinks
|
|
1223
|
+
5. **BE SPECIFIC** - Include exact file, line number, and what makes it suspicious
|
|
1224
|
+
6. **SUGGESTED FIX IS OPTIONAL** - Nice to have but not required. Report the issue even without a fix.
|
|
1172
1225
|
|
|
1173
1226
|
## REPORTING FORMAT
|
|
1174
1227
|
|
|
1175
|
-
When you find a
|
|
1228
|
+
When you find a ${pass.name} issue (bug or code smell):
|
|
1176
1229
|
|
|
1177
1230
|
<json>
|
|
1178
1231
|
{
|
|
@@ -1181,25 +1234,23 @@ When you find a CONFIRMED ${pass.name} bug:
|
|
|
1181
1234
|
"file": "src/api/users.ts",
|
|
1182
1235
|
"line": 42,
|
|
1183
1236
|
"endLine": 45,
|
|
1184
|
-
"title": "Short description of the
|
|
1185
|
-
"description": "
|
|
1237
|
+
"title": "Short description of the issue",
|
|
1238
|
+
"description": "Explanation of why this is problematic and potential impact.",
|
|
1239
|
+
"kind": "bug|smell",
|
|
1186
1240
|
"category": "${pass.category}",
|
|
1187
1241
|
"severity": "critical|high|medium|low",
|
|
1188
1242
|
"confidence": "high|medium|low",
|
|
1189
|
-
"triggerInput": "Exact input that triggers the bug",
|
|
1190
|
-
"codePath": [
|
|
1191
|
-
{"step": 1, "file": "src/api/users.ts", "line": 38, "code": "const name = req.query.name", "explanation": "User input enters here"},
|
|
1192
|
-
{"step": 2, "file": "src/api/users.ts", "line": 42, "code": "db.execute(query)", "explanation": "Used unsafely here"}
|
|
1193
|
-
],
|
|
1194
1243
|
"evidence": [
|
|
1195
1244
|
"Evidence point 1",
|
|
1196
1245
|
"Evidence point 2"
|
|
1197
1246
|
],
|
|
1198
|
-
"suggestedFix": "
|
|
1247
|
+
"suggestedFix": "Optional: how to fix it"
|
|
1199
1248
|
}
|
|
1200
1249
|
}
|
|
1201
1250
|
</json>
|
|
1202
1251
|
|
|
1252
|
+
Use kind="bug" for confirmed vulnerabilities, kind="smell" for risky patterns that need review.
|
|
1253
|
+
|
|
1203
1254
|
Progress updates:
|
|
1204
1255
|
###SCANNING:path/to/file.ts
|
|
1205
1256
|
|
|
@@ -1208,9 +1259,13 @@ When done:
|
|
|
1208
1259
|
|
|
1209
1260
|
## BEGIN
|
|
1210
1261
|
|
|
1211
|
-
Start by searching for ${pass.name} patterns using grep.
|
|
1262
|
+
Start by searching for ${pass.name} patterns using grep. Read at least 10-15 files that match the patterns. Report issues as you find them.
|
|
1212
1263
|
|
|
1213
|
-
|
|
1264
|
+
IMPORTANT:
|
|
1265
|
+
- Report ANYTHING suspicious - we'll filter false positives later
|
|
1266
|
+
- Include code smells and risky patterns, not just confirmed exploits
|
|
1267
|
+
- If unsure, report it with confidence="low"
|
|
1268
|
+
- Aim for thoroughness - finding 10 potential issues is better than finding 0 confirmed bugs`;
|
|
1214
1269
|
}
|
|
1215
1270
|
|
|
1216
1271
|
// src/providers/prompts/constants.ts
|
|
@@ -2515,31 +2570,46 @@ var CoreScanner = class {
|
|
|
2515
2570
|
return jobs;
|
|
2516
2571
|
}
|
|
2517
2572
|
buildQuickScanPrompt(context) {
|
|
2518
|
-
const { understanding, staticResults } = context;
|
|
2573
|
+
const { understanding, staticResults, files } = context;
|
|
2519
2574
|
const staticSignals = staticResults.length > 0 ? `
|
|
2520
2575
|
Static analysis signals:
|
|
2521
2576
|
${staticResults.slice(0, 30).map((r) => `- ${r.file}:${r.line}: ${r.message}`).join("\n")}` : "";
|
|
2522
|
-
|
|
2577
|
+
const fileList = files.slice(0, 50).map((f) => `- ${f}`).join("\n");
|
|
2578
|
+
const moreFiles = files.length > 50 ? `
|
|
2579
|
+
... and ${files.length - 50} more files` : "";
|
|
2580
|
+
return `You are a security auditor. Analyze this ${understanding.summary.type} codebase for bugs and code smells.
|
|
2523
2581
|
|
|
2524
2582
|
Project: ${understanding.summary.description || "Unknown"}
|
|
2525
2583
|
Framework: ${understanding.summary.framework || "None"}
|
|
2526
2584
|
Language: ${understanding.summary.language}
|
|
2527
|
-
${staticSignals}
|
|
2528
|
-
|
|
2529
|
-
Find bugs in these categories:
|
|
2530
|
-
1. Injection (SQL, command, XSS)
|
|
2531
|
-
2. Auth bypass
|
|
2532
|
-
3. Null/undefined dereference
|
|
2533
|
-
4. Logic errors
|
|
2534
|
-
5. Async/race conditions
|
|
2535
|
-
6. Resource leaks
|
|
2536
|
-
7. Data validation issues
|
|
2537
|
-
8. Secrets exposure
|
|
2538
2585
|
|
|
2539
|
-
|
|
2540
|
-
|
|
2586
|
+
## FILES TO ANALYZE
|
|
2587
|
+
Read and analyze these files for security issues:
|
|
2588
|
+
${fileList}${moreFiles}
|
|
2589
|
+
${staticSignals}
|
|
2541
2590
|
|
|
2542
|
-
|
|
2591
|
+
## INSTRUCTIONS
|
|
2592
|
+
1. Read each file listed above
|
|
2593
|
+
2. Look for security issues, bugs, and code smells
|
|
2594
|
+
3. Report ALL suspicious patterns - false positives will be filtered later
|
|
2595
|
+
4. Use kind="smell" for risky patterns, kind="bug" for confirmed issues
|
|
2596
|
+
|
|
2597
|
+
## CATEGORIES TO CHECK
|
|
2598
|
+
- Injection (SQL, command, XSS) - unsanitized user input
|
|
2599
|
+
- Auth bypass - missing auth checks, IDOR
|
|
2600
|
+
- Null/undefined dereference - accessing properties on potentially null values
|
|
2601
|
+
- Logic errors - wrong conditions, off-by-one, inverted checks
|
|
2602
|
+
- Async/race conditions - unhandled promises, race conditions
|
|
2603
|
+
- Resource leaks - unclosed handles, missing cleanup
|
|
2604
|
+
- Data validation - missing input validation, type coercion issues
|
|
2605
|
+
- Secrets exposure - hardcoded keys, leaked credentials
|
|
2606
|
+
- Error handling - swallowed errors, missing try/catch
|
|
2607
|
+
|
|
2608
|
+
## OUTPUT FORMAT
|
|
2609
|
+
Output a JSON array (one object per issue found):
|
|
2610
|
+
[{"file": "path", "line": 42, "title": "Issue title", "description": "Details", "kind": "bug|smell", "severity": "critical|high|medium|low", "category": "logic-error", "evidence": ["evidence"]}]
|
|
2611
|
+
|
|
2612
|
+
If no issues found after reading all files, return: []`;
|
|
2543
2613
|
}
|
|
2544
2614
|
// ─────────────────────────────────────────────────────────────
|
|
2545
2615
|
// Response Parsing
|
|
@@ -2560,6 +2630,23 @@ If no bugs, return: []`;
|
|
|
2560
2630
|
} catch {
|
|
2561
2631
|
}
|
|
2562
2632
|
}
|
|
2633
|
+
if (bugs.length === 0) {
|
|
2634
|
+
const codeBlockMatches = output.matchAll(/```(?:json)?\s*([\s\S]*?)```/g);
|
|
2635
|
+
for (const match of codeBlockMatches) {
|
|
2636
|
+
try {
|
|
2637
|
+
const parsed = JSON.parse(match[1].trim());
|
|
2638
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
2639
|
+
for (const item of items) {
|
|
2640
|
+
const bug = this.parseBugData(item, startIndex + bugs.length, files, passName);
|
|
2641
|
+
if (bug) {
|
|
2642
|
+
bugs.push(bug);
|
|
2643
|
+
this.progress.onBugFound?.(bug);
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
} catch {
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2563
2650
|
if (bugs.length === 0) {
|
|
2564
2651
|
const arrayMatch = output.match(/\[[\s\S]*\]/);
|
|
2565
2652
|
if (arrayMatch) {
|
|
@@ -2578,6 +2665,20 @@ If no bugs, return: []`;
|
|
|
2578
2665
|
}
|
|
2579
2666
|
}
|
|
2580
2667
|
}
|
|
2668
|
+
if (bugs.length === 0) {
|
|
2669
|
+
const objectMatches = output.matchAll(/\{[^{}]*"file"[^{}]*"line"[^{}]*\}/g);
|
|
2670
|
+
for (const match of objectMatches) {
|
|
2671
|
+
try {
|
|
2672
|
+
const parsed = JSON.parse(match[0]);
|
|
2673
|
+
const bug = this.parseBugData(parsed, startIndex + bugs.length, files, passName);
|
|
2674
|
+
if (bug) {
|
|
2675
|
+
bugs.push(bug);
|
|
2676
|
+
this.progress.onBugFound?.(bug);
|
|
2677
|
+
}
|
|
2678
|
+
} catch {
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2581
2682
|
return bugs;
|
|
2582
2683
|
}
|
|
2583
2684
|
parseBugData(data, index, files, passName) {
|
|
@@ -5274,10 +5375,26 @@ async function scanCommand(paths, options) {
|
|
|
5274
5375
|
if (!isQuiet) {
|
|
5275
5376
|
const spinner6 = p3.spinner();
|
|
5276
5377
|
spinner6.start("Scanning files...");
|
|
5277
|
-
|
|
5378
|
+
if (paths.length > 0) {
|
|
5379
|
+
filesToScan = await fg3(paths, {
|
|
5380
|
+
cwd,
|
|
5381
|
+
ignore: ["node_modules/**", "dist/**", "build/**", ".next/**"],
|
|
5382
|
+
absolute: false
|
|
5383
|
+
});
|
|
5384
|
+
} else {
|
|
5385
|
+
filesToScan = await scanCodebase(cwd, config);
|
|
5386
|
+
}
|
|
5278
5387
|
spinner6.stop(`Found ${filesToScan.length} files to scan`);
|
|
5279
5388
|
} else {
|
|
5280
|
-
|
|
5389
|
+
if (paths.length > 0) {
|
|
5390
|
+
filesToScan = await fg3(paths, {
|
|
5391
|
+
cwd,
|
|
5392
|
+
ignore: ["node_modules/**", "dist/**", "build/**", ".next/**"],
|
|
5393
|
+
absolute: false
|
|
5394
|
+
});
|
|
5395
|
+
} else {
|
|
5396
|
+
filesToScan = await scanCodebase(cwd, config);
|
|
5397
|
+
}
|
|
5281
5398
|
}
|
|
5282
5399
|
} else {
|
|
5283
5400
|
if (!config) {
|