@shakecodeslikecray/whiterose 1.0.8 → 1.0.10
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 +516 -264
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +251 -37
- package/dist/index.js +134 -58
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, readdirSync,
|
|
3
|
-
import { join, dirname, isAbsolute, resolve, basename
|
|
2
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, readdirSync, mkdtempSync, statSync, realpathSync } from 'fs';
|
|
3
|
+
import { join, dirname, relative, isAbsolute, resolve, basename } from 'path';
|
|
4
4
|
import chalk3 from 'chalk';
|
|
5
5
|
import * as readline from 'readline';
|
|
6
6
|
import { Command } from 'commander';
|
|
@@ -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
|
|
@@ -1195,23 +1214,18 @@ ${pass.falsePositiveHints.map((h) => `- ${h}`).join("\n")}
|
|
|
1195
1214
|
${cwePatterns ? `## KNOWN VULNERABILITY PATTERNS
|
|
1196
1215
|
${cwePatterns}` : ""}
|
|
1197
1216
|
|
|
1198
|
-
##
|
|
1217
|
+
## REPORTING GUIDELINES
|
|
1199
1218
|
|
|
1200
|
-
1. **ONLY ${pass.name.toUpperCase()}
|
|
1201
|
-
2. **
|
|
1202
|
-
3. **
|
|
1203
|
-
4. **
|
|
1204
|
-
5. **BE SPECIFIC** -
|
|
1205
|
-
6. **
|
|
1206
|
-
- Look for early returns: \`if (x.length === 0) return;\` makes later \`x[0]\` SAFE
|
|
1207
|
-
- Look for throws: \`if (!user) throw new Error()\` makes later \`user.name\` SAFE
|
|
1208
|
-
- Look for guard clauses that exit before the buggy line
|
|
1209
|
-
- If a condition causes function exit, code after is UNREACHABLE in that case
|
|
1210
|
-
- 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.
|
|
1211
1225
|
|
|
1212
1226
|
## REPORTING FORMAT
|
|
1213
1227
|
|
|
1214
|
-
When you find a
|
|
1228
|
+
When you find a ${pass.name} issue (bug or code smell):
|
|
1215
1229
|
|
|
1216
1230
|
<json>
|
|
1217
1231
|
{
|
|
@@ -1220,25 +1234,23 @@ When you find a CONFIRMED ${pass.name} bug:
|
|
|
1220
1234
|
"file": "src/api/users.ts",
|
|
1221
1235
|
"line": 42,
|
|
1222
1236
|
"endLine": 45,
|
|
1223
|
-
"title": "Short description of the
|
|
1224
|
-
"description": "
|
|
1237
|
+
"title": "Short description of the issue",
|
|
1238
|
+
"description": "Explanation of why this is problematic and potential impact.",
|
|
1239
|
+
"kind": "bug|smell",
|
|
1225
1240
|
"category": "${pass.category}",
|
|
1226
1241
|
"severity": "critical|high|medium|low",
|
|
1227
1242
|
"confidence": "high|medium|low",
|
|
1228
|
-
"triggerInput": "Exact input that triggers the bug",
|
|
1229
|
-
"codePath": [
|
|
1230
|
-
{"step": 1, "file": "src/api/users.ts", "line": 38, "code": "const name = req.query.name", "explanation": "User input enters here"},
|
|
1231
|
-
{"step": 2, "file": "src/api/users.ts", "line": 42, "code": "db.execute(query)", "explanation": "Used unsafely here"}
|
|
1232
|
-
],
|
|
1233
1243
|
"evidence": [
|
|
1234
1244
|
"Evidence point 1",
|
|
1235
1245
|
"Evidence point 2"
|
|
1236
1246
|
],
|
|
1237
|
-
"suggestedFix": "
|
|
1247
|
+
"suggestedFix": "Optional: how to fix it"
|
|
1238
1248
|
}
|
|
1239
1249
|
}
|
|
1240
1250
|
</json>
|
|
1241
1251
|
|
|
1252
|
+
Use kind="bug" for confirmed vulnerabilities, kind="smell" for risky patterns that need review.
|
|
1253
|
+
|
|
1242
1254
|
Progress updates:
|
|
1243
1255
|
###SCANNING:path/to/file.ts
|
|
1244
1256
|
|
|
@@ -1247,9 +1259,13 @@ When done:
|
|
|
1247
1259
|
|
|
1248
1260
|
## BEGIN
|
|
1249
1261
|
|
|
1250
|
-
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.
|
|
1251
1263
|
|
|
1252
|
-
|
|
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`;
|
|
1253
1269
|
}
|
|
1254
1270
|
|
|
1255
1271
|
// src/providers/prompts/constants.ts
|
|
@@ -2554,31 +2570,46 @@ var CoreScanner = class {
|
|
|
2554
2570
|
return jobs;
|
|
2555
2571
|
}
|
|
2556
2572
|
buildQuickScanPrompt(context) {
|
|
2557
|
-
const { understanding, staticResults } = context;
|
|
2573
|
+
const { understanding, staticResults, files } = context;
|
|
2558
2574
|
const staticSignals = staticResults.length > 0 ? `
|
|
2559
2575
|
Static analysis signals:
|
|
2560
2576
|
${staticResults.slice(0, 30).map((r) => `- ${r.file}:${r.line}: ${r.message}`).join("\n")}` : "";
|
|
2561
|
-
|
|
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.
|
|
2562
2581
|
|
|
2563
2582
|
Project: ${understanding.summary.description || "Unknown"}
|
|
2564
2583
|
Framework: ${understanding.summary.framework || "None"}
|
|
2565
2584
|
Language: ${understanding.summary.language}
|
|
2566
|
-
${staticSignals}
|
|
2567
|
-
|
|
2568
|
-
Find bugs in these categories:
|
|
2569
|
-
1. Injection (SQL, command, XSS)
|
|
2570
|
-
2. Auth bypass
|
|
2571
|
-
3. Null/undefined dereference
|
|
2572
|
-
4. Logic errors
|
|
2573
|
-
5. Async/race conditions
|
|
2574
|
-
6. Resource leaks
|
|
2575
|
-
7. Data validation issues
|
|
2576
|
-
8. Secrets exposure
|
|
2577
2585
|
|
|
2578
|
-
|
|
2579
|
-
|
|
2586
|
+
## FILES TO ANALYZE
|
|
2587
|
+
Read and analyze these files for security issues:
|
|
2588
|
+
${fileList}${moreFiles}
|
|
2589
|
+
${staticSignals}
|
|
2580
2590
|
|
|
2581
|
-
|
|
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: []`;
|
|
2582
2613
|
}
|
|
2583
2614
|
// ─────────────────────────────────────────────────────────────
|
|
2584
2615
|
// Response Parsing
|
|
@@ -2599,6 +2630,23 @@ If no bugs, return: []`;
|
|
|
2599
2630
|
} catch {
|
|
2600
2631
|
}
|
|
2601
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
|
+
}
|
|
2602
2650
|
if (bugs.length === 0) {
|
|
2603
2651
|
const arrayMatch = output.match(/\[[\s\S]*\]/);
|
|
2604
2652
|
if (arrayMatch) {
|
|
@@ -2617,6 +2665,20 @@ If no bugs, return: []`;
|
|
|
2617
2665
|
}
|
|
2618
2666
|
}
|
|
2619
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
|
+
}
|
|
2620
2682
|
return bugs;
|
|
2621
2683
|
}
|
|
2622
2684
|
parseBugData(data, index, files, passName) {
|
|
@@ -3039,8 +3101,8 @@ function splitIntoSections(content) {
|
|
|
3039
3101
|
function parseListItems(content) {
|
|
3040
3102
|
const items = [];
|
|
3041
3103
|
const lines = content.split("\n");
|
|
3042
|
-
for (const
|
|
3043
|
-
const match =
|
|
3104
|
+
for (const line2 of lines) {
|
|
3105
|
+
const match = line2.match(/^[-*]\s+(.+)/);
|
|
3044
3106
|
if (match) {
|
|
3045
3107
|
const item = match[1].trim();
|
|
3046
3108
|
if (!item.includes("Add your") && !item.includes("Add files") && item !== "") {
|
|
@@ -3066,8 +3128,8 @@ function parseFeatureSection(section) {
|
|
|
3066
3128
|
const constraintsMatch = section.match(/\*\*Constraints:\*\*\n((?:[-*]\s+[^\n]+\n?)+)/);
|
|
3067
3129
|
if (constraintsMatch) {
|
|
3068
3130
|
const constraintLines = constraintsMatch[1].split("\n");
|
|
3069
|
-
for (const
|
|
3070
|
-
const match =
|
|
3131
|
+
for (const line2 of constraintLines) {
|
|
3132
|
+
const match = line2.match(/^[-*]\s+(.+)/);
|
|
3071
3133
|
if (match) {
|
|
3072
3134
|
constraints.push(match[1].trim());
|
|
3073
3135
|
}
|
|
@@ -3207,12 +3269,12 @@ function extractIntentFromDocs(docs) {
|
|
|
3207
3269
|
if (docs.readme) {
|
|
3208
3270
|
const featuresMatch = docs.readme.match(/##\s*Features?\s*\n([\s\S]*?)(?=\n##|\n---|$)/i);
|
|
3209
3271
|
if (featuresMatch) {
|
|
3210
|
-
const featureLines = featuresMatch[1].split("\n").filter((
|
|
3272
|
+
const featureLines = featuresMatch[1].split("\n").filter((line2) => line2.trim().startsWith("-") || line2.trim().startsWith("*")).map((line2) => line2.replace(/^[-*]\s*/, "").trim()).filter((line2) => line2.length > 0);
|
|
3211
3273
|
intent.features.push(...featureLines.slice(0, 20));
|
|
3212
3274
|
}
|
|
3213
3275
|
}
|
|
3214
3276
|
if (docs.envExample) {
|
|
3215
|
-
const envLines = docs.envExample.split("\n").filter((
|
|
3277
|
+
const envLines = docs.envExample.split("\n").filter((line2) => line2.includes("=") && !line2.startsWith("#")).map((line2) => line2.split("=")[0].trim()).filter((line2) => line2.length > 0);
|
|
3216
3278
|
intent.envVariables.push(...envLines);
|
|
3217
3279
|
}
|
|
3218
3280
|
for (const apiDoc of docs.apiDocs) {
|
|
@@ -3222,7 +3284,7 @@ function extractIntentFromDocs(docs) {
|
|
|
3222
3284
|
}
|
|
3223
3285
|
}
|
|
3224
3286
|
if (docs.contributing) {
|
|
3225
|
-
const conventionLines = docs.contributing.split("\n").filter((
|
|
3287
|
+
const conventionLines = docs.contributing.split("\n").filter((line2) => line2.trim().startsWith("-") || line2.trim().startsWith("*")).map((line2) => line2.replace(/^[-*]\s*/, "").trim()).filter((line2) => line2.length > 10 && line2.length < 200).slice(0, 10);
|
|
3226
3288
|
intent.conventions.push(...conventionLines);
|
|
3227
3289
|
}
|
|
3228
3290
|
return intent;
|
|
@@ -3716,6 +3778,26 @@ z.object({
|
|
|
3716
3778
|
lastIncrementalScan: z.string().datetime().optional(),
|
|
3717
3779
|
fileHashes: z.array(FileHash)
|
|
3718
3780
|
});
|
|
3781
|
+
var SeverityBreakdown = z.object({
|
|
3782
|
+
critical: z.number(),
|
|
3783
|
+
high: z.number(),
|
|
3784
|
+
medium: z.number(),
|
|
3785
|
+
low: z.number(),
|
|
3786
|
+
total: z.number()
|
|
3787
|
+
});
|
|
3788
|
+
var ScanSummary = z.object({
|
|
3789
|
+
bugs: SeverityBreakdown,
|
|
3790
|
+
smells: SeverityBreakdown,
|
|
3791
|
+
total: z.number()
|
|
3792
|
+
});
|
|
3793
|
+
var ScanMeta = z.object({
|
|
3794
|
+
repoName: z.string(),
|
|
3795
|
+
provider: z.string(),
|
|
3796
|
+
duration: z.number(),
|
|
3797
|
+
// ms
|
|
3798
|
+
filesScanned: z.number(),
|
|
3799
|
+
linesOfCode: z.number()
|
|
3800
|
+
});
|
|
3719
3801
|
z.object({
|
|
3720
3802
|
id: z.string(),
|
|
3721
3803
|
timestamp: z.string().datetime(),
|
|
@@ -3724,16 +3806,10 @@ z.object({
|
|
|
3724
3806
|
filesChanged: z.number().optional(),
|
|
3725
3807
|
duration: z.number(),
|
|
3726
3808
|
// ms
|
|
3809
|
+
linesOfCode: z.number().optional(),
|
|
3727
3810
|
bugs: z.array(Bug),
|
|
3728
|
-
summary:
|
|
3729
|
-
|
|
3730
|
-
high: z.number(),
|
|
3731
|
-
medium: z.number(),
|
|
3732
|
-
low: z.number(),
|
|
3733
|
-
total: z.number(),
|
|
3734
|
-
bugs: z.number(),
|
|
3735
|
-
smells: z.number()
|
|
3736
|
-
})
|
|
3811
|
+
summary: ScanSummary,
|
|
3812
|
+
meta: ScanMeta.optional()
|
|
3737
3813
|
});
|
|
3738
3814
|
|
|
3739
3815
|
// src/core/config.ts
|
|
@@ -3792,8 +3868,8 @@ async function runTypeScript(cwd) {
|
|
|
3792
3868
|
} catch (error) {
|
|
3793
3869
|
const output = [error.stdout, error.stderr].filter(Boolean).join("\n");
|
|
3794
3870
|
const lines = output.split("\n");
|
|
3795
|
-
for (const
|
|
3796
|
-
const match =
|
|
3871
|
+
for (const line2 of lines) {
|
|
3872
|
+
const match = line2.match(/^(.+)\((\d+),(\d+)\):\s+(error|warning)\s+TS(\d+):\s+(.+)$/);
|
|
3797
3873
|
if (match) {
|
|
3798
3874
|
const filePath = normalizeFilePath(match[1], cwd);
|
|
3799
3875
|
results.push({
|
|
@@ -4012,15 +4088,15 @@ function outputMarkdown(result) {
|
|
|
4012
4088
|
lines.push("");
|
|
4013
4089
|
lines.push("## Summary");
|
|
4014
4090
|
lines.push("");
|
|
4015
|
-
lines.push(`|
|
|
4016
|
-
lines.push(
|
|
4017
|
-
lines.push(`| Critical | ${result.summary.critical} |`);
|
|
4018
|
-
lines.push(`| High | ${result.summary.high} |`);
|
|
4019
|
-
lines.push(`| Medium | ${result.summary.medium} |`);
|
|
4020
|
-
lines.push(`| Low | ${result.summary.low} |`);
|
|
4021
|
-
lines.push(`| **
|
|
4022
|
-
lines.push(
|
|
4023
|
-
lines.push(
|
|
4091
|
+
lines.push(`| | Bugs | Smells |`);
|
|
4092
|
+
lines.push(`|----------|-------|-------|`);
|
|
4093
|
+
lines.push(`| Critical | ${result.summary.bugs.critical} | ${result.summary.smells.critical} |`);
|
|
4094
|
+
lines.push(`| High | ${result.summary.bugs.high} | ${result.summary.smells.high} |`);
|
|
4095
|
+
lines.push(`| Medium | ${result.summary.bugs.medium} | ${result.summary.smells.medium} |`);
|
|
4096
|
+
lines.push(`| Low | ${result.summary.bugs.low} | ${result.summary.smells.low} |`);
|
|
4097
|
+
lines.push(`| **Total** | **${result.summary.bugs.total}** | **${result.summary.smells.total}** |`);
|
|
4098
|
+
lines.push("");
|
|
4099
|
+
lines.push(`**Total Findings:** ${result.summary.total}`);
|
|
4024
4100
|
lines.push("");
|
|
4025
4101
|
lines.push(`- **Scan Type:** ${result.scanType}`);
|
|
4026
4102
|
lines.push(`- **Files Scanned:** ${result.filesScanned}`);
|
|
@@ -4338,18 +4414,19 @@ function outputHumanReadableMarkdown(result) {
|
|
|
4338
4414
|
sections.push(`# Bug Report
|
|
4339
4415
|
|
|
4340
4416
|
> Human-readable summary generated by whiterose on ${new Date(result.timestamp).toLocaleDateString()}
|
|
4341
|
-
> **${result.summary.bugs} bugs found** in ${result.filesScanned} files
|
|
4417
|
+
> **${result.summary.bugs.total} bugs found** in ${result.filesScanned} files
|
|
4342
4418
|
|
|
4343
4419
|
---
|
|
4344
4420
|
|
|
4345
4421
|
## Summary
|
|
4346
4422
|
|
|
4347
|
-
| Severity |
|
|
4348
|
-
|
|
4349
|
-
|
|
|
4350
|
-
|
|
|
4351
|
-
|
|
|
4352
|
-
|
|
|
4423
|
+
| Severity | Bugs | Smells |
|
|
4424
|
+
|----------|------|--------|
|
|
4425
|
+
| Critical | ${result.summary.bugs.critical} | ${result.summary.smells.critical} |
|
|
4426
|
+
| High | ${result.summary.bugs.high} | ${result.summary.smells.high} |
|
|
4427
|
+
| Medium | ${result.summary.bugs.medium} | ${result.summary.smells.medium} |
|
|
4428
|
+
| Low | ${result.summary.bugs.low} | ${result.summary.smells.low} |
|
|
4429
|
+
| **Total** | **${result.summary.bugs.total}** | **${result.summary.smells.total}** |
|
|
4353
4430
|
|
|
4354
4431
|
---
|
|
4355
4432
|
`);
|
|
@@ -4627,18 +4704,18 @@ function extractFileEffects(filePath, content) {
|
|
|
4627
4704
|
let currentFunction = "";
|
|
4628
4705
|
const functionRegex = /(?:async\s+)?(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[^=])\s*=>|(\w+)\s*\([^)]*\)\s*(?::\s*[^{]+)?\s*\{)/;
|
|
4629
4706
|
for (let i = 0; i < lines.length; i++) {
|
|
4630
|
-
const
|
|
4707
|
+
const line2 = lines[i];
|
|
4631
4708
|
const lineNum = i + 1;
|
|
4632
|
-
const trimmed =
|
|
4709
|
+
const trimmed = line2.trim();
|
|
4633
4710
|
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
|
4634
4711
|
continue;
|
|
4635
4712
|
}
|
|
4636
|
-
const funcMatch =
|
|
4713
|
+
const funcMatch = line2.match(functionRegex);
|
|
4637
4714
|
if (funcMatch) {
|
|
4638
4715
|
currentFunction = funcMatch[1] || funcMatch[2] || funcMatch[3] || "";
|
|
4639
4716
|
}
|
|
4640
4717
|
for (const pattern of patterns) {
|
|
4641
|
-
const match =
|
|
4718
|
+
const match = line2.match(pattern.regex);
|
|
4642
4719
|
if (match) {
|
|
4643
4720
|
let target = match[1] || "unknown";
|
|
4644
4721
|
target = target.trim().replace(/['"`,]/g, "");
|
|
@@ -4654,8 +4731,8 @@ function extractFileEffects(filePath, content) {
|
|
|
4654
4731
|
/cache/
|
|
4655
4732
|
];
|
|
4656
4733
|
for (const pp of pathPatterns) {
|
|
4657
|
-
if (
|
|
4658
|
-
target =
|
|
4734
|
+
if (line2.match(pp)) {
|
|
4735
|
+
target = line2.match(pp)?.[0] || target;
|
|
4659
4736
|
break;
|
|
4660
4737
|
}
|
|
4661
4738
|
}
|
|
@@ -4664,7 +4741,7 @@ function extractFileEffects(filePath, content) {
|
|
|
4664
4741
|
type: pattern.type,
|
|
4665
4742
|
target,
|
|
4666
4743
|
line: lineNum,
|
|
4667
|
-
code:
|
|
4744
|
+
code: line2.trim(),
|
|
4668
4745
|
functionName: currentFunction
|
|
4669
4746
|
});
|
|
4670
4747
|
}
|
|
@@ -4949,14 +5026,14 @@ function detectWeakValidation(block) {
|
|
|
4949
5026
|
}
|
|
4950
5027
|
for (let i = 0; i < lines.length; i++) {
|
|
4951
5028
|
if (reportedLines.has(i)) continue;
|
|
4952
|
-
const
|
|
5029
|
+
const line2 = lines[i];
|
|
4953
5030
|
const weakPatterns = [
|
|
4954
5031
|
{ regex: /matchCount\s*>=?\s*\d/, issue: "Accepts content if only a few lines match" },
|
|
4955
5032
|
{ regex: /\.length\s*>\s*\w+\.length\s*\*\s*0\.\d/, issue: "Accepts content based only on length ratio" }
|
|
4956
5033
|
];
|
|
4957
5034
|
const issues = [];
|
|
4958
5035
|
for (const pattern of weakPatterns) {
|
|
4959
|
-
if (
|
|
5036
|
+
if (line2.match(pattern.regex)) {
|
|
4960
5037
|
issues.push(pattern.issue);
|
|
4961
5038
|
}
|
|
4962
5039
|
}
|
|
@@ -4970,7 +5047,7 @@ function detectWeakValidation(block) {
|
|
|
4970
5047
|
line: block.startLine + i,
|
|
4971
5048
|
evidence: [
|
|
4972
5049
|
`Function: ${block.functionName}`,
|
|
4973
|
-
`Pattern: ${
|
|
5050
|
+
`Pattern: ${line2.trim()}`,
|
|
4974
5051
|
`Issues: ${issues.join(", ")}`
|
|
4975
5052
|
],
|
|
4976
5053
|
severity: "medium"
|
|
@@ -4986,14 +5063,14 @@ function detectPartialFailure(block) {
|
|
|
4986
5063
|
let forLoopStart = 0;
|
|
4987
5064
|
let forLoopItem = "";
|
|
4988
5065
|
for (let i = 0; i < lines.length; i++) {
|
|
4989
|
-
const
|
|
4990
|
-
const forMatch =
|
|
5066
|
+
const line2 = lines[i];
|
|
5067
|
+
const forMatch = line2.match(/for\s*\(\s*(?:const|let|var)\s+(\w+)\s+of/);
|
|
4991
5068
|
if (forMatch) {
|
|
4992
5069
|
inForLoop = true;
|
|
4993
5070
|
forLoopStart = i;
|
|
4994
5071
|
forLoopItem = forMatch[1];
|
|
4995
5072
|
}
|
|
4996
|
-
if (inForLoop &&
|
|
5073
|
+
if (inForLoop && line2.includes("break")) {
|
|
4997
5074
|
const hasFailureCheck = lines.slice(Math.max(0, i - 3), i + 1).join("\n").includes("!result.success") || lines.slice(Math.max(0, i - 3), i + 1).join("\n").includes("error") || lines.slice(Math.max(0, i - 3), i + 1).join("\n").includes("failed");
|
|
4998
5075
|
const hasSkippedReport = block.content.includes("skipped") || block.content.includes("remaining") || block.content.includes("not attempted");
|
|
4999
5076
|
if (hasFailureCheck && !hasSkippedReport) {
|
|
@@ -5013,7 +5090,7 @@ function detectPartialFailure(block) {
|
|
|
5013
5090
|
});
|
|
5014
5091
|
}
|
|
5015
5092
|
}
|
|
5016
|
-
if (inForLoop &&
|
|
5093
|
+
if (inForLoop && line2.trim() === "}" && i > forLoopStart + 2) {
|
|
5017
5094
|
inForLoop = false;
|
|
5018
5095
|
}
|
|
5019
5096
|
}
|
|
@@ -5025,21 +5102,21 @@ function extractFunctions(filePath, content) {
|
|
|
5025
5102
|
const funcRegex = /(?:export\s+)?(?:async\s+)?function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>/;
|
|
5026
5103
|
let currentFunc = null;
|
|
5027
5104
|
for (let i = 0; i < lines.length; i++) {
|
|
5028
|
-
const
|
|
5029
|
-
const trimmed =
|
|
5105
|
+
const line2 = lines[i];
|
|
5106
|
+
const trimmed = line2.trim();
|
|
5030
5107
|
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
|
5031
5108
|
continue;
|
|
5032
5109
|
}
|
|
5033
5110
|
if (!currentFunc) {
|
|
5034
|
-
const match =
|
|
5111
|
+
const match = line2.match(funcRegex);
|
|
5035
5112
|
if (match) {
|
|
5036
5113
|
const funcName = match[1] || match[2];
|
|
5037
5114
|
currentFunc = { name: funcName, start: i, braceCount: 0 };
|
|
5038
5115
|
}
|
|
5039
5116
|
}
|
|
5040
5117
|
if (currentFunc) {
|
|
5041
|
-
currentFunc.braceCount += (
|
|
5042
|
-
currentFunc.braceCount -= (
|
|
5118
|
+
currentFunc.braceCount += (line2.match(/{/g) || []).length;
|
|
5119
|
+
currentFunc.braceCount -= (line2.match(/}/g) || []).length;
|
|
5043
5120
|
if (currentFunc.braceCount <= 0 && i > currentFunc.start) {
|
|
5044
5121
|
blocks.push({
|
|
5045
5122
|
file: filePath,
|
|
@@ -5248,10 +5325,146 @@ function makeIntentBug(cwd, contract, reason) {
|
|
|
5248
5325
|
};
|
|
5249
5326
|
}
|
|
5250
5327
|
|
|
5328
|
+
// src/cli/components/progress.ts
|
|
5329
|
+
function formatDuration(ms) {
|
|
5330
|
+
const seconds = Math.floor(ms / 1e3);
|
|
5331
|
+
const hours = Math.floor(seconds / 3600);
|
|
5332
|
+
const mins = Math.floor(seconds % 3600 / 60);
|
|
5333
|
+
const secs = seconds % 60;
|
|
5334
|
+
if (hours > 0) {
|
|
5335
|
+
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
|
5336
|
+
}
|
|
5337
|
+
if (mins > 0) {
|
|
5338
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
5339
|
+
}
|
|
5340
|
+
return `${secs}s`;
|
|
5341
|
+
}
|
|
5342
|
+
|
|
5343
|
+
// src/cli/components/card.ts
|
|
5344
|
+
var BOX = {
|
|
5345
|
+
topLeft: "\u250C",
|
|
5346
|
+
topRight: "\u2510",
|
|
5347
|
+
bottomLeft: "\u2514",
|
|
5348
|
+
bottomRight: "\u2518",
|
|
5349
|
+
horizontal: "\u2500",
|
|
5350
|
+
vertical: "\u2502",
|
|
5351
|
+
teeRight: "\u251C",
|
|
5352
|
+
teeLeft: "\u2524"
|
|
5353
|
+
};
|
|
5354
|
+
function line(char, width) {
|
|
5355
|
+
return char.repeat(width);
|
|
5356
|
+
}
|
|
5357
|
+
function padRight(str, width) {
|
|
5358
|
+
const stripped = str.replace(/\u001b\[\d+(;\d+)*m/g, "");
|
|
5359
|
+
const padding = Math.max(0, width - stripped.length);
|
|
5360
|
+
return str + " ".repeat(padding);
|
|
5361
|
+
}
|
|
5362
|
+
function row(content, width) {
|
|
5363
|
+
return BOX.vertical + " " + padRight(content, width - 4) + " " + BOX.vertical;
|
|
5364
|
+
}
|
|
5365
|
+
function divider(width) {
|
|
5366
|
+
return BOX.teeRight + line(BOX.horizontal, width) + BOX.teeLeft;
|
|
5367
|
+
}
|
|
5368
|
+
function topBorder(width) {
|
|
5369
|
+
return BOX.topLeft + line(BOX.horizontal, width) + BOX.topRight;
|
|
5370
|
+
}
|
|
5371
|
+
function bottomBorder(width) {
|
|
5372
|
+
return BOX.bottomLeft + line(BOX.horizontal, width) + BOX.bottomRight;
|
|
5373
|
+
}
|
|
5374
|
+
function severityDot(severity) {
|
|
5375
|
+
const colors = {
|
|
5376
|
+
critical: chalk3.red,
|
|
5377
|
+
high: chalk3.yellow,
|
|
5378
|
+
medium: chalk3.blue,
|
|
5379
|
+
low: chalk3.dim
|
|
5380
|
+
};
|
|
5381
|
+
return colors[severity]("\u25CF");
|
|
5382
|
+
}
|
|
5383
|
+
function formatNumber(n) {
|
|
5384
|
+
return String(n).padStart(3);
|
|
5385
|
+
}
|
|
5386
|
+
function renderScanCard(data) {
|
|
5387
|
+
const width = 61;
|
|
5388
|
+
const lines = [];
|
|
5389
|
+
lines.push(topBorder(width));
|
|
5390
|
+
lines.push(row(chalk3.bold.red("WHITEROSE SCAN COMPLETE"), width));
|
|
5391
|
+
lines.push(divider(width));
|
|
5392
|
+
const locFormatted = data.meta.linesOfCode.toLocaleString();
|
|
5393
|
+
lines.push(row(`${chalk3.dim("Repository")} ${data.meta.repoName}`, width));
|
|
5394
|
+
lines.push(row(`${chalk3.dim("Provider")} ${data.meta.provider}`, width));
|
|
5395
|
+
lines.push(row(`${chalk3.dim("Duration")} ${formatDuration(data.meta.duration)}`, width));
|
|
5396
|
+
lines.push(row(`${chalk3.dim("Files")} ${data.meta.filesScanned} files | ${locFormatted} LoC`, width));
|
|
5397
|
+
lines.push(divider(width));
|
|
5398
|
+
lines.push(row(`${chalk3.bold("BUGS")} ${chalk3.bold("SMELLS")}`, width));
|
|
5399
|
+
const severities = ["critical", "high", "medium", "low"];
|
|
5400
|
+
for (const sev of severities) {
|
|
5401
|
+
const bugLine = `${severityDot(sev)} ${sev.charAt(0).toUpperCase() + sev.slice(1).padEnd(8)} ${formatNumber(data.bugs[sev])}`;
|
|
5402
|
+
const smellLine = `${severityDot(sev)} ${sev.charAt(0).toUpperCase() + sev.slice(1).padEnd(8)} ${formatNumber(data.smells[sev])}`;
|
|
5403
|
+
lines.push(row(`${bugLine} ${smellLine}`, width));
|
|
5404
|
+
}
|
|
5405
|
+
lines.push(row(`${chalk3.dim("\u2500".repeat(13))} ${chalk3.dim("\u2500".repeat(13))}`, width));
|
|
5406
|
+
const bugTotal = `Total ${formatNumber(data.bugs.total)}`;
|
|
5407
|
+
const smellTotal = `Total ${formatNumber(data.smells.total)}`;
|
|
5408
|
+
lines.push(row(`${chalk3.bold(bugTotal)} ${chalk3.bold(smellTotal)}`, width));
|
|
5409
|
+
lines.push(divider(width));
|
|
5410
|
+
lines.push(row(`${chalk3.dim("Reports:")} ${chalk3.cyan(data.reportPath)}`, width));
|
|
5411
|
+
if (data.bugs.total > 0 || data.smells.total > 0) {
|
|
5412
|
+
lines.push(row(`${chalk3.dim("Run:")} ${chalk3.cyan("whiterose fix")}`, width));
|
|
5413
|
+
}
|
|
5414
|
+
lines.push(bottomBorder(width));
|
|
5415
|
+
return lines.join("\n");
|
|
5416
|
+
}
|
|
5417
|
+
function renderStatusCard(repoName, provider, totalBugs, totalSmells, lastScanDate) {
|
|
5418
|
+
const width = 45;
|
|
5419
|
+
const lines = [];
|
|
5420
|
+
lines.push(topBorder(width));
|
|
5421
|
+
lines.push(row(chalk3.bold.red("WHITEROSE STATUS"), width));
|
|
5422
|
+
lines.push(divider(width));
|
|
5423
|
+
lines.push(row(`${chalk3.dim("Repository")} ${repoName}`, width));
|
|
5424
|
+
lines.push(row(`${chalk3.dim("Provider")} ${provider}`, width));
|
|
5425
|
+
if (lastScanDate) {
|
|
5426
|
+
lines.push(row(`${chalk3.dim("Last scan")} ${lastScanDate}`, width));
|
|
5427
|
+
}
|
|
5428
|
+
lines.push(divider(width));
|
|
5429
|
+
lines.push(row(`${chalk3.bold("Open bugs:")} ${totalBugs}`, width));
|
|
5430
|
+
lines.push(row(`${chalk3.bold("Open smells:")} ${totalSmells}`, width));
|
|
5431
|
+
lines.push(bottomBorder(width));
|
|
5432
|
+
return lines.join("\n");
|
|
5433
|
+
}
|
|
5434
|
+
|
|
5251
5435
|
// src/cli/commands/scan.ts
|
|
5436
|
+
function getRepoName(cwd) {
|
|
5437
|
+
try {
|
|
5438
|
+
const gitConfigPath = join(cwd, ".git", "config");
|
|
5439
|
+
if (existsSync(gitConfigPath)) {
|
|
5440
|
+
const config = readFileSync(gitConfigPath, "utf-8");
|
|
5441
|
+
const match = config.match(/url\s*=\s*.*[/:]([^/]+?)(?:\.git)?$/m);
|
|
5442
|
+
if (match) return match[1];
|
|
5443
|
+
}
|
|
5444
|
+
} catch {
|
|
5445
|
+
}
|
|
5446
|
+
return basename(cwd);
|
|
5447
|
+
}
|
|
5448
|
+
async function countLinesOfCode(cwd, files) {
|
|
5449
|
+
let total = 0;
|
|
5450
|
+
for (const file of files.slice(0, 500)) {
|
|
5451
|
+
try {
|
|
5452
|
+
const content = readFileSync(join(cwd, file), "utf-8");
|
|
5453
|
+
const lines = content.split("\n").filter((line2) => {
|
|
5454
|
+
const trimmed = line2.trim();
|
|
5455
|
+
return trimmed.length > 0 && !trimmed.startsWith("//") && !trimmed.startsWith("/*") && !trimmed.startsWith("*");
|
|
5456
|
+
});
|
|
5457
|
+
total += lines.length;
|
|
5458
|
+
} catch {
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
return total;
|
|
5462
|
+
}
|
|
5252
5463
|
async function scanCommand(paths, options) {
|
|
5464
|
+
const scanStartTime = Date.now();
|
|
5253
5465
|
const cwd = process.cwd();
|
|
5254
5466
|
const whiterosePath = join(cwd, ".whiterose");
|
|
5467
|
+
const repoName = getRepoName(cwd);
|
|
5255
5468
|
const isQuickScan = options.quick || options.ci;
|
|
5256
5469
|
const isQuiet = options.json || options.sarif || options.ci;
|
|
5257
5470
|
if (!isQuickScan && !existsSync(whiterosePath)) {
|
|
@@ -5264,8 +5477,10 @@ async function scanCommand(paths, options) {
|
|
|
5264
5477
|
process.exit(1);
|
|
5265
5478
|
}
|
|
5266
5479
|
if (!isQuiet) {
|
|
5267
|
-
const scanMode = isQuickScan ? "quick
|
|
5268
|
-
|
|
5480
|
+
const scanMode = isQuickScan ? "quick" : "full";
|
|
5481
|
+
console.log();
|
|
5482
|
+
console.log(chalk3.red.bold("whiterose") + chalk3.dim(` v1.0.9 | ${scanMode} scan`));
|
|
5483
|
+
console.log();
|
|
5269
5484
|
}
|
|
5270
5485
|
let config;
|
|
5271
5486
|
let understanding;
|
|
@@ -5311,12 +5526,19 @@ async function scanCommand(paths, options) {
|
|
|
5311
5526
|
if (options.full || paths.length > 0) {
|
|
5312
5527
|
scanType = "full";
|
|
5313
5528
|
if (!isQuiet) {
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5529
|
+
console.log(chalk3.dim("\u2502") + " Discovering files...");
|
|
5530
|
+
}
|
|
5531
|
+
if (paths.length > 0) {
|
|
5532
|
+
filesToScan = await fg3(paths, {
|
|
5533
|
+
cwd,
|
|
5534
|
+
ignore: ["node_modules/**", "dist/**", "build/**", ".next/**"],
|
|
5535
|
+
absolute: false
|
|
5536
|
+
});
|
|
5318
5537
|
} else {
|
|
5319
|
-
filesToScan =
|
|
5538
|
+
filesToScan = await scanCodebase(cwd, config);
|
|
5539
|
+
}
|
|
5540
|
+
if (!isQuiet) {
|
|
5541
|
+
console.log(chalk3.dim("\u2502") + ` Found ${chalk3.cyan(filesToScan.length)} files`);
|
|
5320
5542
|
}
|
|
5321
5543
|
} else {
|
|
5322
5544
|
if (!config) {
|
|
@@ -5342,12 +5564,11 @@ async function scanCommand(paths, options) {
|
|
|
5342
5564
|
}
|
|
5343
5565
|
let staticResults;
|
|
5344
5566
|
if (!isQuiet) {
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
staticResults = await runStaticAnalysis(cwd, filesToScan, config);
|
|
5567
|
+
console.log(chalk3.dim("\u2502") + " Running static analysis (tsc, eslint)...");
|
|
5568
|
+
}
|
|
5569
|
+
staticResults = await runStaticAnalysis(cwd, filesToScan, config);
|
|
5570
|
+
if (!isQuiet) {
|
|
5571
|
+
console.log(chalk3.dim("\u2502") + ` Static analysis: ${chalk3.cyan(staticResults.length)} signals`);
|
|
5351
5572
|
}
|
|
5352
5573
|
const providerName = options.provider || config?.provider || "claude-code";
|
|
5353
5574
|
const executor = getExecutor(providerName);
|
|
@@ -5359,13 +5580,12 @@ async function scanCommand(paths, options) {
|
|
|
5359
5580
|
}
|
|
5360
5581
|
let bugs;
|
|
5361
5582
|
if (!isQuiet) {
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5583
|
+
console.log(chalk3.dim("\u2502"));
|
|
5584
|
+
console.log(chalk3.cyan("\u2550\u2550\u2550 Analyzing with " + providerName + " \u2550\u2550\u2550"));
|
|
5585
|
+
console.log();
|
|
5365
5586
|
const scanner = new CoreScanner(executor, {}, {
|
|
5366
5587
|
onProgress: (message) => {
|
|
5367
5588
|
if (message.trim()) {
|
|
5368
|
-
llmSpinner.stop("");
|
|
5369
5589
|
if (message.includes("\u2550\u2550\u2550\u2550")) {
|
|
5370
5590
|
console.log(chalk3.cyan(message));
|
|
5371
5591
|
} else if (message.includes("\u2713")) {
|
|
@@ -5377,13 +5597,11 @@ async function scanCommand(paths, options) {
|
|
|
5377
5597
|
} else {
|
|
5378
5598
|
console.log(chalk3.dim(message));
|
|
5379
5599
|
}
|
|
5380
|
-
llmSpinner.start("Scanning...");
|
|
5381
5600
|
}
|
|
5382
5601
|
},
|
|
5383
5602
|
onBugFound: (bug) => {
|
|
5384
|
-
|
|
5385
|
-
console.log(chalk3.magenta(
|
|
5386
|
-
llmSpinner.start("Scanning...");
|
|
5603
|
+
const severityColor = bug.severity === "critical" ? chalk3.red : bug.severity === "high" ? chalk3.yellow : bug.severity === "medium" ? chalk3.blue : chalk3.dim;
|
|
5604
|
+
console.log(chalk3.magenta("\u2605") + ` ${severityColor("[" + bug.severity + "]")} ${bug.title}`);
|
|
5387
5605
|
}
|
|
5388
5606
|
});
|
|
5389
5607
|
try {
|
|
@@ -5402,11 +5620,11 @@ async function scanCommand(paths, options) {
|
|
|
5402
5620
|
config
|
|
5403
5621
|
});
|
|
5404
5622
|
}
|
|
5405
|
-
|
|
5406
|
-
|
|
5623
|
+
console.log();
|
|
5624
|
+
console.log(chalk3.dim("\u2502") + ` Found ${chalk3.cyan(bugs.length)} potential bugs`);
|
|
5407
5625
|
if (scanner.hasPassErrors()) {
|
|
5408
5626
|
const errors = scanner.getPassErrors();
|
|
5409
|
-
|
|
5627
|
+
console.log(chalk3.yellow("\u26A0") + ` ${errors.length} analysis pass(es) failed:`);
|
|
5410
5628
|
for (const err of errors.slice(0, 5)) {
|
|
5411
5629
|
console.log(chalk3.yellow(` - ${err.passName}: ${err.error}`));
|
|
5412
5630
|
}
|
|
@@ -5415,8 +5633,8 @@ async function scanCommand(paths, options) {
|
|
|
5415
5633
|
}
|
|
5416
5634
|
}
|
|
5417
5635
|
} catch (error) {
|
|
5418
|
-
|
|
5419
|
-
|
|
5636
|
+
console.log(chalk3.red("\u2717") + " Analysis failed");
|
|
5637
|
+
console.error(String(error));
|
|
5420
5638
|
process.exit(1);
|
|
5421
5639
|
}
|
|
5422
5640
|
} else {
|
|
@@ -5449,61 +5667,53 @@ async function scanCommand(paths, options) {
|
|
|
5449
5667
|
}
|
|
5450
5668
|
if (!isQuickScan) {
|
|
5451
5669
|
if (!isQuiet) {
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
bugs.push(...crossFileBugs);
|
|
5458
|
-
crossFileSpinner.stop(`Cross-file analysis: ${crossFileBugs.length} issues found`);
|
|
5459
|
-
} else {
|
|
5460
|
-
crossFileSpinner.stop("Cross-file analysis: no issues");
|
|
5461
|
-
}
|
|
5462
|
-
} catch {
|
|
5463
|
-
crossFileSpinner.stop("Cross-file analysis: skipped");
|
|
5464
|
-
}
|
|
5465
|
-
} else {
|
|
5466
|
-
try {
|
|
5467
|
-
const crossFileBugs = await analyzeCrossFile(cwd);
|
|
5670
|
+
console.log(chalk3.dim("\u2502") + " Running cross-file analysis...");
|
|
5671
|
+
}
|
|
5672
|
+
try {
|
|
5673
|
+
const crossFileBugs = await analyzeCrossFile(cwd);
|
|
5674
|
+
if (crossFileBugs.length > 0) {
|
|
5468
5675
|
bugs.push(...crossFileBugs);
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
console.error(JSON.stringify({
|
|
5472
|
-
error: "Cross-file analysis failed",
|
|
5473
|
-
message: err instanceof Error ? err.message : String(err)
|
|
5474
|
-
}));
|
|
5475
|
-
process.exit(1);
|
|
5676
|
+
if (!isQuiet) {
|
|
5677
|
+
console.log(chalk3.dim("\u2502") + ` Cross-file: ${chalk3.cyan(crossFileBugs.length)} issues`);
|
|
5476
5678
|
}
|
|
5679
|
+
} else if (!isQuiet) {
|
|
5680
|
+
console.log(chalk3.dim("\u2502") + " Cross-file: no issues");
|
|
5681
|
+
}
|
|
5682
|
+
} catch (err) {
|
|
5683
|
+
if (!isQuiet) {
|
|
5684
|
+
console.log(chalk3.dim("\u2502") + chalk3.dim(" Cross-file: skipped"));
|
|
5685
|
+
} else if (options.ci) {
|
|
5686
|
+
console.error(JSON.stringify({
|
|
5687
|
+
error: "Cross-file analysis failed",
|
|
5688
|
+
message: err instanceof Error ? err.message : String(err)
|
|
5689
|
+
}));
|
|
5690
|
+
process.exit(1);
|
|
5477
5691
|
}
|
|
5478
5692
|
}
|
|
5479
5693
|
}
|
|
5480
5694
|
if (!isQuickScan) {
|
|
5481
5695
|
if (!isQuiet) {
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
bugs.push(...contractBugs);
|
|
5488
|
-
contractSpinner.stop(`Contract analysis: ${contractBugs.length} issues found`);
|
|
5489
|
-
} else {
|
|
5490
|
-
contractSpinner.stop("Contract analysis: no issues");
|
|
5491
|
-
}
|
|
5492
|
-
} catch {
|
|
5493
|
-
contractSpinner.stop("Contract analysis: skipped");
|
|
5494
|
-
}
|
|
5495
|
-
} else {
|
|
5496
|
-
try {
|
|
5497
|
-
const contractBugs = await analyzeContracts(cwd);
|
|
5696
|
+
console.log(chalk3.dim("\u2502") + " Running contract analysis...");
|
|
5697
|
+
}
|
|
5698
|
+
try {
|
|
5699
|
+
const contractBugs = await analyzeContracts(cwd);
|
|
5700
|
+
if (contractBugs.length > 0) {
|
|
5498
5701
|
bugs.push(...contractBugs);
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
console.error(JSON.stringify({
|
|
5502
|
-
error: "Contract analysis failed",
|
|
5503
|
-
message: err instanceof Error ? err.message : String(err)
|
|
5504
|
-
}));
|
|
5505
|
-
process.exit(1);
|
|
5702
|
+
if (!isQuiet) {
|
|
5703
|
+
console.log(chalk3.dim("\u2502") + ` Contract: ${chalk3.cyan(contractBugs.length)} issues`);
|
|
5506
5704
|
}
|
|
5705
|
+
} else if (!isQuiet) {
|
|
5706
|
+
console.log(chalk3.dim("\u2502") + " Contract: no issues");
|
|
5707
|
+
}
|
|
5708
|
+
} catch (err) {
|
|
5709
|
+
if (!isQuiet) {
|
|
5710
|
+
console.log(chalk3.dim("\u2502") + chalk3.dim(" Contract: skipped"));
|
|
5711
|
+
} else if (options.ci) {
|
|
5712
|
+
console.error(JSON.stringify({
|
|
5713
|
+
error: "Contract analysis failed",
|
|
5714
|
+
message: err instanceof Error ? err.message : String(err)
|
|
5715
|
+
}));
|
|
5716
|
+
process.exit(1);
|
|
5507
5717
|
}
|
|
5508
5718
|
}
|
|
5509
5719
|
}
|
|
@@ -5543,25 +5753,45 @@ async function scanCommand(paths, options) {
|
|
|
5543
5753
|
);
|
|
5544
5754
|
}
|
|
5545
5755
|
const allBugs = mergeResult.bugs;
|
|
5756
|
+
const linesOfCode = await countLinesOfCode(cwd, filesToScan);
|
|
5757
|
+
const scanDuration = Date.now() - scanStartTime;
|
|
5758
|
+
const bugItems = allBugs.filter((b) => b.kind === "bug");
|
|
5759
|
+
const smellItems = allBugs.filter((b) => b.kind === "smell");
|
|
5760
|
+
const summary = {
|
|
5761
|
+
bugs: {
|
|
5762
|
+
critical: bugItems.filter((b) => b.severity === "critical").length,
|
|
5763
|
+
high: bugItems.filter((b) => b.severity === "high").length,
|
|
5764
|
+
medium: bugItems.filter((b) => b.severity === "medium").length,
|
|
5765
|
+
low: bugItems.filter((b) => b.severity === "low").length,
|
|
5766
|
+
total: bugItems.length
|
|
5767
|
+
},
|
|
5768
|
+
smells: {
|
|
5769
|
+
critical: smellItems.filter((b) => b.severity === "critical").length,
|
|
5770
|
+
high: smellItems.filter((b) => b.severity === "high").length,
|
|
5771
|
+
medium: smellItems.filter((b) => b.severity === "medium").length,
|
|
5772
|
+
low: smellItems.filter((b) => b.severity === "low").length,
|
|
5773
|
+
total: smellItems.length
|
|
5774
|
+
},
|
|
5775
|
+
total: allBugs.length
|
|
5776
|
+
};
|
|
5777
|
+
const meta = {
|
|
5778
|
+
repoName,
|
|
5779
|
+
provider: providerName,
|
|
5780
|
+
duration: scanDuration,
|
|
5781
|
+
filesScanned: filesToScan.length,
|
|
5782
|
+
linesOfCode
|
|
5783
|
+
};
|
|
5546
5784
|
const result = {
|
|
5547
5785
|
id: `scan-${Date.now()}`,
|
|
5548
5786
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5549
5787
|
scanType,
|
|
5550
5788
|
filesScanned: filesToScan.length,
|
|
5551
5789
|
filesChanged: scanType === "incremental" ? filesToScan.length : void 0,
|
|
5552
|
-
duration:
|
|
5553
|
-
|
|
5790
|
+
duration: scanDuration,
|
|
5791
|
+
linesOfCode,
|
|
5554
5792
|
bugs: allBugs,
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
critical: allBugs.filter((b) => b.kind === "bug" && b.severity === "critical").length,
|
|
5558
|
-
high: allBugs.filter((b) => b.kind === "bug" && b.severity === "high").length,
|
|
5559
|
-
medium: allBugs.filter((b) => b.kind === "bug" && b.severity === "medium").length,
|
|
5560
|
-
low: allBugs.filter((b) => b.kind === "bug" && b.severity === "low").length,
|
|
5561
|
-
total: allBugs.length,
|
|
5562
|
-
bugs: allBugs.filter((b) => b.kind === "bug").length,
|
|
5563
|
-
smells: allBugs.filter((b) => b.kind === "smell").length
|
|
5564
|
-
}
|
|
5793
|
+
summary,
|
|
5794
|
+
meta
|
|
5565
5795
|
};
|
|
5566
5796
|
if (pendingHashState) {
|
|
5567
5797
|
try {
|
|
@@ -5571,12 +5801,12 @@ async function scanCommand(paths, options) {
|
|
|
5571
5801
|
}
|
|
5572
5802
|
if (options.json || options.ci && !options.sarif) {
|
|
5573
5803
|
console.log(JSON.stringify(result, null, 2));
|
|
5574
|
-
if (options.ci && result.summary.bugs > 0) {
|
|
5804
|
+
if (options.ci && result.summary.bugs.total > 0) {
|
|
5575
5805
|
process.exit(1);
|
|
5576
5806
|
}
|
|
5577
5807
|
} else if (options.sarif) {
|
|
5578
5808
|
console.log(JSON.stringify(outputSarif(result), null, 2));
|
|
5579
|
-
if (options.ci && result.summary.bugs > 0) {
|
|
5809
|
+
if (options.ci && result.summary.bugs.total > 0) {
|
|
5580
5810
|
process.exit(1);
|
|
5581
5811
|
}
|
|
5582
5812
|
} else {
|
|
@@ -5599,32 +5829,24 @@ async function scanCommand(paths, options) {
|
|
|
5599
5829
|
writeFileSync(sarifPath, JSON.stringify(outputSarif(result), null, 2));
|
|
5600
5830
|
const jsonPath = join(outputDir, "bugs.json");
|
|
5601
5831
|
writeFileSync(jsonPath, JSON.stringify(result, null, 2));
|
|
5832
|
+
const lastScanPath = join(whiterosePath, "last-scan.json");
|
|
5833
|
+
writeFileSync(lastScanPath, JSON.stringify(result, null, 2));
|
|
5602
5834
|
writeFileSync(join(reportsDir, `${timestamp}.sarif`), JSON.stringify(outputSarif(result), null, 2));
|
|
5603
5835
|
console.log();
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
console.log();
|
|
5611
|
-
if (newBugsThisScan > 0) {
|
|
5612
|
-
console.log(` ${chalk3.green("+")} New this scan: ${newBugsThisScan}`);
|
|
5613
|
-
}
|
|
5614
|
-
console.log(
|
|
5615
|
-
` ${chalk3.bold("Total findings:")} ${result.summary.total} (bugs: ${result.summary.bugs}, smells: ${result.summary.smells})`
|
|
5616
|
-
);
|
|
5836
|
+
const cardData = {
|
|
5837
|
+
meta,
|
|
5838
|
+
bugs: summary.bugs,
|
|
5839
|
+
smells: summary.smells,
|
|
5840
|
+
reportPath: "./" + relative(cwd, humanPath)
|
|
5841
|
+
};
|
|
5842
|
+
console.log(renderScanCard(cardData));
|
|
5617
5843
|
console.log();
|
|
5618
|
-
|
|
5619
|
-
console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan(humanPath)} ${chalk3.dim("(tester-friendly)")}`);
|
|
5620
|
-
console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan(mdPath)} ${chalk3.dim("(technical)")}`);
|
|
5621
|
-
console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan(sarifPath)}`);
|
|
5622
|
-
console.log(` ${chalk3.dim("\u2514")} ${chalk3.cyan(jsonPath)}`);
|
|
5844
|
+
console.log(chalk3.dim("Reports:"));
|
|
5845
|
+
console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan("./" + relative(cwd, humanPath))} ${chalk3.dim("(tester-friendly)")}`);
|
|
5846
|
+
console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan("./" + relative(cwd, mdPath))} ${chalk3.dim("(technical)")}`);
|
|
5847
|
+
console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan("./" + relative(cwd, sarifPath))}`);
|
|
5848
|
+
console.log(` ${chalk3.dim("\u2514")} ${chalk3.cyan("./" + relative(cwd, jsonPath))}`);
|
|
5623
5849
|
console.log();
|
|
5624
|
-
if (result.summary.total > 0) {
|
|
5625
|
-
p3.log.info(`Run ${chalk3.cyan("whiterose fix")} to fix bugs interactively.`);
|
|
5626
|
-
}
|
|
5627
|
-
p3.outro(chalk3.green("Scan complete"));
|
|
5628
5850
|
}
|
|
5629
5851
|
}
|
|
5630
5852
|
var Dashboard = ({ bugs, onSelectCategory }) => {
|
|
@@ -6655,8 +6877,8 @@ async function runAgenticFix(bug, config, projectDir, onProgress) {
|
|
|
6655
6877
|
lineBuffer += text2;
|
|
6656
6878
|
const lines = lineBuffer.split("\n");
|
|
6657
6879
|
lineBuffer = lines.pop() || "";
|
|
6658
|
-
for (const
|
|
6659
|
-
const trimmed =
|
|
6880
|
+
for (const line2 of lines) {
|
|
6881
|
+
const trimmed = line2.trim();
|
|
6660
6882
|
if (trimmed) {
|
|
6661
6883
|
try {
|
|
6662
6884
|
const event = JSON.parse(trimmed);
|
|
@@ -7042,7 +7264,7 @@ function loadBugsFromSarif(sarifPath) {
|
|
|
7042
7264
|
const rawFile = r.locations?.[0]?.physicalLocation?.artifactLocation?.uri;
|
|
7043
7265
|
const file = typeof rawFile === "string" ? rawFile : "unknown";
|
|
7044
7266
|
const rawLine = r.locations?.[0]?.physicalLocation?.region?.startLine;
|
|
7045
|
-
const
|
|
7267
|
+
const line2 = typeof rawLine === "number" && Number.isFinite(rawLine) ? Math.floor(rawLine) : 0;
|
|
7046
7268
|
const rawEndLine = r.locations?.[0]?.physicalLocation?.region?.endLine;
|
|
7047
7269
|
const endLine = typeof rawEndLine === "number" && Number.isFinite(rawEndLine) ? Math.floor(rawEndLine) : void 0;
|
|
7048
7270
|
const rawId = r.ruleId;
|
|
@@ -7055,7 +7277,7 @@ function loadBugsFromSarif(sarifPath) {
|
|
|
7055
7277
|
title: sanitizeSarifText(String(rawTitle), "title"),
|
|
7056
7278
|
description: sanitizeSarifText(String(rawDescription), "description"),
|
|
7057
7279
|
file,
|
|
7058
|
-
line,
|
|
7280
|
+
line: line2,
|
|
7059
7281
|
endLine,
|
|
7060
7282
|
kind: validatedKind.success ? validatedKind.data : "bug",
|
|
7061
7283
|
severity: mapSarifLevel(r.level),
|
|
@@ -7337,13 +7559,13 @@ async function fixSingleBug(bug, config, options, cwd) {
|
|
|
7337
7559
|
if (result.diff) {
|
|
7338
7560
|
console.log();
|
|
7339
7561
|
console.log(chalk3.dim(" Changes:"));
|
|
7340
|
-
for (const
|
|
7341
|
-
if (
|
|
7342
|
-
console.log(chalk3.green(` ${
|
|
7343
|
-
} else if (
|
|
7344
|
-
console.log(chalk3.red(` ${
|
|
7562
|
+
for (const line2 of result.diff.split("\n")) {
|
|
7563
|
+
if (line2.startsWith("+")) {
|
|
7564
|
+
console.log(chalk3.green(` ${line2}`));
|
|
7565
|
+
} else if (line2.startsWith("-")) {
|
|
7566
|
+
console.log(chalk3.red(` ${line2}`));
|
|
7345
7567
|
} else {
|
|
7346
|
-
console.log(chalk3.dim(` ${
|
|
7568
|
+
console.log(chalk3.dim(` ${line2}`));
|
|
7347
7569
|
}
|
|
7348
7570
|
}
|
|
7349
7571
|
console.log();
|
|
@@ -7427,6 +7649,18 @@ async function refreshCommand(_options) {
|
|
|
7427
7649
|
}
|
|
7428
7650
|
p3.outro(chalk3.green("Refresh complete!"));
|
|
7429
7651
|
}
|
|
7652
|
+
function getRepoName2(cwd) {
|
|
7653
|
+
try {
|
|
7654
|
+
const gitConfigPath = join(cwd, ".git", "config");
|
|
7655
|
+
if (existsSync(gitConfigPath)) {
|
|
7656
|
+
const config = readFileSync(gitConfigPath, "utf-8");
|
|
7657
|
+
const match = config.match(/url\s*=\s*.*[/:]([^/]+?)(?:\.git)?$/m);
|
|
7658
|
+
if (match) return match[1];
|
|
7659
|
+
}
|
|
7660
|
+
} catch {
|
|
7661
|
+
}
|
|
7662
|
+
return basename(cwd);
|
|
7663
|
+
}
|
|
7430
7664
|
async function statusCommand() {
|
|
7431
7665
|
const cwd = process.cwd();
|
|
7432
7666
|
const whiterosePath = join(cwd, ".whiterose");
|
|
@@ -7435,67 +7669,75 @@ async function statusCommand() {
|
|
|
7435
7669
|
p3.log.info('Run "whiterose init" first.');
|
|
7436
7670
|
process.exit(1);
|
|
7437
7671
|
}
|
|
7438
|
-
|
|
7672
|
+
console.log();
|
|
7673
|
+
console.log(chalk3.red.bold("whiterose") + chalk3.dim(" status"));
|
|
7674
|
+
console.log();
|
|
7439
7675
|
const config = await loadConfig(cwd);
|
|
7440
|
-
const
|
|
7441
|
-
const
|
|
7676
|
+
const repoName = getRepoName2(cwd);
|
|
7677
|
+
const lastScanPath = join(whiterosePath, "last-scan.json");
|
|
7678
|
+
let lastScan = null;
|
|
7679
|
+
if (existsSync(lastScanPath)) {
|
|
7680
|
+
try {
|
|
7681
|
+
lastScan = JSON.parse(readFileSync(lastScanPath, "utf-8"));
|
|
7682
|
+
} catch {
|
|
7683
|
+
}
|
|
7684
|
+
}
|
|
7685
|
+
if (lastScan && lastScan.meta) {
|
|
7686
|
+
const cardData = {
|
|
7687
|
+
meta: lastScan.meta,
|
|
7688
|
+
bugs: lastScan.summary.bugs,
|
|
7689
|
+
smells: lastScan.summary.smells,
|
|
7690
|
+
reportPath: "./whiterose-output/bugs-human.md"
|
|
7691
|
+
};
|
|
7692
|
+
console.log(renderScanCard(cardData));
|
|
7693
|
+
console.log();
|
|
7694
|
+
console.log(chalk3.dim(`Last scan: ${new Date(lastScan.timestamp).toLocaleString()}`));
|
|
7695
|
+
} else {
|
|
7696
|
+
const bugStats2 = getAccumulatedBugsStats(cwd);
|
|
7697
|
+
const totalBugs = bugStats2.bySeverity ? Object.values(bugStats2.bySeverity).reduce((a, b) => a + b, 0) : 0;
|
|
7698
|
+
console.log(renderStatusCard(
|
|
7699
|
+
repoName,
|
|
7700
|
+
config.provider,
|
|
7701
|
+
totalBugs,
|
|
7702
|
+
0,
|
|
7703
|
+
// No smell tracking in accumulated bugs
|
|
7704
|
+
bugStats2.lastUpdated ? new Date(bugStats2.lastUpdated).toLocaleDateString() : void 0
|
|
7705
|
+
));
|
|
7706
|
+
}
|
|
7442
7707
|
console.log();
|
|
7443
|
-
|
|
7444
|
-
console.log(
|
|
7445
|
-
console.log(`
|
|
7708
|
+
const availableProviders = await detectProvider();
|
|
7709
|
+
console.log(chalk3.dim("Configuration"));
|
|
7710
|
+
console.log(` Provider: ${config.provider}`);
|
|
7711
|
+
console.log(` Available: ${availableProviders.join(", ") || "none"}`);
|
|
7446
7712
|
console.log();
|
|
7713
|
+
const understanding = await loadUnderstanding(cwd);
|
|
7447
7714
|
if (understanding) {
|
|
7448
|
-
console.log(chalk3.
|
|
7449
|
-
console.log(`
|
|
7450
|
-
console.log(`
|
|
7451
|
-
console.log(`
|
|
7452
|
-
console.log(`
|
|
7453
|
-
console.log(`
|
|
7454
|
-
console.log(` ${chalk3.dim("Generated:")} ${understanding.generatedAt}`);
|
|
7715
|
+
console.log(chalk3.dim("Codebase"));
|
|
7716
|
+
console.log(` Type: ${understanding.summary.type}`);
|
|
7717
|
+
console.log(` Framework: ${understanding.summary.framework || "none"}`);
|
|
7718
|
+
console.log(` Files: ${understanding.structure.totalFiles}`);
|
|
7719
|
+
console.log(` Features: ${understanding.features.length}`);
|
|
7720
|
+
console.log(` Contracts: ${understanding.contracts.length}`);
|
|
7455
7721
|
console.log();
|
|
7456
7722
|
}
|
|
7457
7723
|
const hashesPath = join(whiterosePath, "cache", "file-hashes.json");
|
|
7458
7724
|
if (existsSync(hashesPath)) {
|
|
7459
7725
|
try {
|
|
7460
7726
|
const hashes = JSON.parse(readFileSync(hashesPath, "utf-8"));
|
|
7461
|
-
console.log(chalk3.
|
|
7462
|
-
console.log(`
|
|
7463
|
-
console.log(`
|
|
7727
|
+
console.log(chalk3.dim("Cache"));
|
|
7728
|
+
console.log(` Files tracked: ${hashes.fileHashes?.length || 0}`);
|
|
7729
|
+
console.log(` Last full: ${hashes.lastFullScan ? new Date(hashes.lastFullScan).toLocaleDateString() : "never"}`);
|
|
7464
7730
|
console.log();
|
|
7465
7731
|
} catch {
|
|
7466
7732
|
}
|
|
7467
7733
|
}
|
|
7468
7734
|
const bugStats = getAccumulatedBugsStats(cwd);
|
|
7469
7735
|
if (bugStats.total > 0) {
|
|
7470
|
-
console.log(chalk3.
|
|
7471
|
-
console.log(` ${chalk3.dim("Total:")} ${bugStats.total}`);
|
|
7472
|
-
if (Object.keys(bugStats.bySeverity).length > 0) {
|
|
7473
|
-
for (const [severity, count] of Object.entries(bugStats.bySeverity)) {
|
|
7474
|
-
const color = severity === "critical" ? "red" : severity === "high" ? "yellow" : severity === "medium" ? "blue" : "dim";
|
|
7475
|
-
console.log(` ${chalk3[color]("\u25CF")} ${severity}: ${count}`);
|
|
7476
|
-
}
|
|
7477
|
-
}
|
|
7478
|
-
console.log(` ${chalk3.dim("Last updated:")} ${new Date(bugStats.lastUpdated).toLocaleString()}`);
|
|
7479
|
-
console.log();
|
|
7480
|
-
}
|
|
7481
|
-
const reportsDir = join(whiterosePath, "reports");
|
|
7482
|
-
if (existsSync(reportsDir)) {
|
|
7483
|
-
const reports = readdirSync(reportsDir).filter((f) => f.endsWith(".sarif"));
|
|
7484
|
-
if (reports.length > 0) {
|
|
7485
|
-
const latestReport = reports.sort().reverse()[0];
|
|
7486
|
-
const reportPath = join(reportsDir, latestReport);
|
|
7487
|
-
const stats = statSync(reportPath);
|
|
7488
|
-
console.log(chalk3.bold(" Last Scan"));
|
|
7489
|
-
console.log(` ${chalk3.dim("Report:")} ${latestReport}`);
|
|
7490
|
-
console.log(` ${chalk3.dim("Date:")} ${stats.mtime.toISOString()}`);
|
|
7491
|
-
console.log();
|
|
7492
|
-
}
|
|
7493
|
-
}
|
|
7494
|
-
if (bugStats.total > 0) {
|
|
7495
|
-
p3.outro(chalk3.dim('Run "whiterose fix" to fix bugs, or "whiterose clear" to reset'));
|
|
7736
|
+
console.log(chalk3.dim('Run "whiterose fix" to fix bugs, or "whiterose clear" to reset'));
|
|
7496
7737
|
} else {
|
|
7497
|
-
|
|
7738
|
+
console.log(chalk3.dim('Run "whiterose scan" to scan for bugs'));
|
|
7498
7739
|
}
|
|
7740
|
+
console.log();
|
|
7499
7741
|
}
|
|
7500
7742
|
async function reportCommand(options) {
|
|
7501
7743
|
const cwd = process.cwd();
|
|
@@ -7538,6 +7780,8 @@ async function reportCommand(options) {
|
|
|
7538
7780
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7539
7781
|
status: "open"
|
|
7540
7782
|
})) || [];
|
|
7783
|
+
const bugItems = bugs.filter((b) => b.kind === "bug");
|
|
7784
|
+
const smellItems = bugs.filter((b) => b.kind === "smell");
|
|
7541
7785
|
const result = {
|
|
7542
7786
|
id: "report",
|
|
7543
7787
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -7546,13 +7790,21 @@ async function reportCommand(options) {
|
|
|
7546
7790
|
duration: 0,
|
|
7547
7791
|
bugs,
|
|
7548
7792
|
summary: {
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7793
|
+
bugs: {
|
|
7794
|
+
critical: bugItems.filter((b) => b.severity === "critical").length,
|
|
7795
|
+
high: bugItems.filter((b) => b.severity === "high").length,
|
|
7796
|
+
medium: bugItems.filter((b) => b.severity === "medium").length,
|
|
7797
|
+
low: bugItems.filter((b) => b.severity === "low").length,
|
|
7798
|
+
total: bugItems.length
|
|
7799
|
+
},
|
|
7800
|
+
smells: {
|
|
7801
|
+
critical: smellItems.filter((b) => b.severity === "critical").length,
|
|
7802
|
+
high: smellItems.filter((b) => b.severity === "high").length,
|
|
7803
|
+
medium: smellItems.filter((b) => b.severity === "medium").length,
|
|
7804
|
+
low: smellItems.filter((b) => b.severity === "low").length,
|
|
7805
|
+
total: smellItems.length
|
|
7806
|
+
},
|
|
7807
|
+
total: bugs.length
|
|
7556
7808
|
}
|
|
7557
7809
|
};
|
|
7558
7810
|
let output;
|