@shakecodeslikecray/whiterose 1.0.9 → 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 +413 -239
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +251 -37
- package/dist/index.js +33 -19
- 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';
|
|
@@ -3101,8 +3101,8 @@ function splitIntoSections(content) {
|
|
|
3101
3101
|
function parseListItems(content) {
|
|
3102
3102
|
const items = [];
|
|
3103
3103
|
const lines = content.split("\n");
|
|
3104
|
-
for (const
|
|
3105
|
-
const match =
|
|
3104
|
+
for (const line2 of lines) {
|
|
3105
|
+
const match = line2.match(/^[-*]\s+(.+)/);
|
|
3106
3106
|
if (match) {
|
|
3107
3107
|
const item = match[1].trim();
|
|
3108
3108
|
if (!item.includes("Add your") && !item.includes("Add files") && item !== "") {
|
|
@@ -3128,8 +3128,8 @@ function parseFeatureSection(section) {
|
|
|
3128
3128
|
const constraintsMatch = section.match(/\*\*Constraints:\*\*\n((?:[-*]\s+[^\n]+\n?)+)/);
|
|
3129
3129
|
if (constraintsMatch) {
|
|
3130
3130
|
const constraintLines = constraintsMatch[1].split("\n");
|
|
3131
|
-
for (const
|
|
3132
|
-
const match =
|
|
3131
|
+
for (const line2 of constraintLines) {
|
|
3132
|
+
const match = line2.match(/^[-*]\s+(.+)/);
|
|
3133
3133
|
if (match) {
|
|
3134
3134
|
constraints.push(match[1].trim());
|
|
3135
3135
|
}
|
|
@@ -3269,12 +3269,12 @@ function extractIntentFromDocs(docs) {
|
|
|
3269
3269
|
if (docs.readme) {
|
|
3270
3270
|
const featuresMatch = docs.readme.match(/##\s*Features?\s*\n([\s\S]*?)(?=\n##|\n---|$)/i);
|
|
3271
3271
|
if (featuresMatch) {
|
|
3272
|
-
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);
|
|
3273
3273
|
intent.features.push(...featureLines.slice(0, 20));
|
|
3274
3274
|
}
|
|
3275
3275
|
}
|
|
3276
3276
|
if (docs.envExample) {
|
|
3277
|
-
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);
|
|
3278
3278
|
intent.envVariables.push(...envLines);
|
|
3279
3279
|
}
|
|
3280
3280
|
for (const apiDoc of docs.apiDocs) {
|
|
@@ -3284,7 +3284,7 @@ function extractIntentFromDocs(docs) {
|
|
|
3284
3284
|
}
|
|
3285
3285
|
}
|
|
3286
3286
|
if (docs.contributing) {
|
|
3287
|
-
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);
|
|
3288
3288
|
intent.conventions.push(...conventionLines);
|
|
3289
3289
|
}
|
|
3290
3290
|
return intent;
|
|
@@ -3778,6 +3778,26 @@ z.object({
|
|
|
3778
3778
|
lastIncrementalScan: z.string().datetime().optional(),
|
|
3779
3779
|
fileHashes: z.array(FileHash)
|
|
3780
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
|
+
});
|
|
3781
3801
|
z.object({
|
|
3782
3802
|
id: z.string(),
|
|
3783
3803
|
timestamp: z.string().datetime(),
|
|
@@ -3786,16 +3806,10 @@ z.object({
|
|
|
3786
3806
|
filesChanged: z.number().optional(),
|
|
3787
3807
|
duration: z.number(),
|
|
3788
3808
|
// ms
|
|
3809
|
+
linesOfCode: z.number().optional(),
|
|
3789
3810
|
bugs: z.array(Bug),
|
|
3790
|
-
summary:
|
|
3791
|
-
|
|
3792
|
-
high: z.number(),
|
|
3793
|
-
medium: z.number(),
|
|
3794
|
-
low: z.number(),
|
|
3795
|
-
total: z.number(),
|
|
3796
|
-
bugs: z.number(),
|
|
3797
|
-
smells: z.number()
|
|
3798
|
-
})
|
|
3811
|
+
summary: ScanSummary,
|
|
3812
|
+
meta: ScanMeta.optional()
|
|
3799
3813
|
});
|
|
3800
3814
|
|
|
3801
3815
|
// src/core/config.ts
|
|
@@ -3854,8 +3868,8 @@ async function runTypeScript(cwd) {
|
|
|
3854
3868
|
} catch (error) {
|
|
3855
3869
|
const output = [error.stdout, error.stderr].filter(Boolean).join("\n");
|
|
3856
3870
|
const lines = output.split("\n");
|
|
3857
|
-
for (const
|
|
3858
|
-
const match =
|
|
3871
|
+
for (const line2 of lines) {
|
|
3872
|
+
const match = line2.match(/^(.+)\((\d+),(\d+)\):\s+(error|warning)\s+TS(\d+):\s+(.+)$/);
|
|
3859
3873
|
if (match) {
|
|
3860
3874
|
const filePath = normalizeFilePath(match[1], cwd);
|
|
3861
3875
|
results.push({
|
|
@@ -4074,15 +4088,15 @@ function outputMarkdown(result) {
|
|
|
4074
4088
|
lines.push("");
|
|
4075
4089
|
lines.push("## Summary");
|
|
4076
4090
|
lines.push("");
|
|
4077
|
-
lines.push(`|
|
|
4078
|
-
lines.push(
|
|
4079
|
-
lines.push(`| Critical | ${result.summary.critical} |`);
|
|
4080
|
-
lines.push(`| High | ${result.summary.high} |`);
|
|
4081
|
-
lines.push(`| Medium | ${result.summary.medium} |`);
|
|
4082
|
-
lines.push(`| Low | ${result.summary.low} |`);
|
|
4083
|
-
lines.push(`| **
|
|
4084
|
-
lines.push(
|
|
4085
|
-
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}`);
|
|
4086
4100
|
lines.push("");
|
|
4087
4101
|
lines.push(`- **Scan Type:** ${result.scanType}`);
|
|
4088
4102
|
lines.push(`- **Files Scanned:** ${result.filesScanned}`);
|
|
@@ -4400,18 +4414,19 @@ function outputHumanReadableMarkdown(result) {
|
|
|
4400
4414
|
sections.push(`# Bug Report
|
|
4401
4415
|
|
|
4402
4416
|
> Human-readable summary generated by whiterose on ${new Date(result.timestamp).toLocaleDateString()}
|
|
4403
|
-
> **${result.summary.bugs} bugs found** in ${result.filesScanned} files
|
|
4417
|
+
> **${result.summary.bugs.total} bugs found** in ${result.filesScanned} files
|
|
4404
4418
|
|
|
4405
4419
|
---
|
|
4406
4420
|
|
|
4407
4421
|
## Summary
|
|
4408
4422
|
|
|
4409
|
-
| Severity |
|
|
4410
|
-
|
|
4411
|
-
|
|
|
4412
|
-
|
|
|
4413
|
-
|
|
|
4414
|
-
|
|
|
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}** |
|
|
4415
4430
|
|
|
4416
4431
|
---
|
|
4417
4432
|
`);
|
|
@@ -4689,18 +4704,18 @@ function extractFileEffects(filePath, content) {
|
|
|
4689
4704
|
let currentFunction = "";
|
|
4690
4705
|
const functionRegex = /(?:async\s+)?(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[^=])\s*=>|(\w+)\s*\([^)]*\)\s*(?::\s*[^{]+)?\s*\{)/;
|
|
4691
4706
|
for (let i = 0; i < lines.length; i++) {
|
|
4692
|
-
const
|
|
4707
|
+
const line2 = lines[i];
|
|
4693
4708
|
const lineNum = i + 1;
|
|
4694
|
-
const trimmed =
|
|
4709
|
+
const trimmed = line2.trim();
|
|
4695
4710
|
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
|
4696
4711
|
continue;
|
|
4697
4712
|
}
|
|
4698
|
-
const funcMatch =
|
|
4713
|
+
const funcMatch = line2.match(functionRegex);
|
|
4699
4714
|
if (funcMatch) {
|
|
4700
4715
|
currentFunction = funcMatch[1] || funcMatch[2] || funcMatch[3] || "";
|
|
4701
4716
|
}
|
|
4702
4717
|
for (const pattern of patterns) {
|
|
4703
|
-
const match =
|
|
4718
|
+
const match = line2.match(pattern.regex);
|
|
4704
4719
|
if (match) {
|
|
4705
4720
|
let target = match[1] || "unknown";
|
|
4706
4721
|
target = target.trim().replace(/['"`,]/g, "");
|
|
@@ -4716,8 +4731,8 @@ function extractFileEffects(filePath, content) {
|
|
|
4716
4731
|
/cache/
|
|
4717
4732
|
];
|
|
4718
4733
|
for (const pp of pathPatterns) {
|
|
4719
|
-
if (
|
|
4720
|
-
target =
|
|
4734
|
+
if (line2.match(pp)) {
|
|
4735
|
+
target = line2.match(pp)?.[0] || target;
|
|
4721
4736
|
break;
|
|
4722
4737
|
}
|
|
4723
4738
|
}
|
|
@@ -4726,7 +4741,7 @@ function extractFileEffects(filePath, content) {
|
|
|
4726
4741
|
type: pattern.type,
|
|
4727
4742
|
target,
|
|
4728
4743
|
line: lineNum,
|
|
4729
|
-
code:
|
|
4744
|
+
code: line2.trim(),
|
|
4730
4745
|
functionName: currentFunction
|
|
4731
4746
|
});
|
|
4732
4747
|
}
|
|
@@ -5011,14 +5026,14 @@ function detectWeakValidation(block) {
|
|
|
5011
5026
|
}
|
|
5012
5027
|
for (let i = 0; i < lines.length; i++) {
|
|
5013
5028
|
if (reportedLines.has(i)) continue;
|
|
5014
|
-
const
|
|
5029
|
+
const line2 = lines[i];
|
|
5015
5030
|
const weakPatterns = [
|
|
5016
5031
|
{ regex: /matchCount\s*>=?\s*\d/, issue: "Accepts content if only a few lines match" },
|
|
5017
5032
|
{ regex: /\.length\s*>\s*\w+\.length\s*\*\s*0\.\d/, issue: "Accepts content based only on length ratio" }
|
|
5018
5033
|
];
|
|
5019
5034
|
const issues = [];
|
|
5020
5035
|
for (const pattern of weakPatterns) {
|
|
5021
|
-
if (
|
|
5036
|
+
if (line2.match(pattern.regex)) {
|
|
5022
5037
|
issues.push(pattern.issue);
|
|
5023
5038
|
}
|
|
5024
5039
|
}
|
|
@@ -5032,7 +5047,7 @@ function detectWeakValidation(block) {
|
|
|
5032
5047
|
line: block.startLine + i,
|
|
5033
5048
|
evidence: [
|
|
5034
5049
|
`Function: ${block.functionName}`,
|
|
5035
|
-
`Pattern: ${
|
|
5050
|
+
`Pattern: ${line2.trim()}`,
|
|
5036
5051
|
`Issues: ${issues.join(", ")}`
|
|
5037
5052
|
],
|
|
5038
5053
|
severity: "medium"
|
|
@@ -5048,14 +5063,14 @@ function detectPartialFailure(block) {
|
|
|
5048
5063
|
let forLoopStart = 0;
|
|
5049
5064
|
let forLoopItem = "";
|
|
5050
5065
|
for (let i = 0; i < lines.length; i++) {
|
|
5051
|
-
const
|
|
5052
|
-
const forMatch =
|
|
5066
|
+
const line2 = lines[i];
|
|
5067
|
+
const forMatch = line2.match(/for\s*\(\s*(?:const|let|var)\s+(\w+)\s+of/);
|
|
5053
5068
|
if (forMatch) {
|
|
5054
5069
|
inForLoop = true;
|
|
5055
5070
|
forLoopStart = i;
|
|
5056
5071
|
forLoopItem = forMatch[1];
|
|
5057
5072
|
}
|
|
5058
|
-
if (inForLoop &&
|
|
5073
|
+
if (inForLoop && line2.includes("break")) {
|
|
5059
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");
|
|
5060
5075
|
const hasSkippedReport = block.content.includes("skipped") || block.content.includes("remaining") || block.content.includes("not attempted");
|
|
5061
5076
|
if (hasFailureCheck && !hasSkippedReport) {
|
|
@@ -5075,7 +5090,7 @@ function detectPartialFailure(block) {
|
|
|
5075
5090
|
});
|
|
5076
5091
|
}
|
|
5077
5092
|
}
|
|
5078
|
-
if (inForLoop &&
|
|
5093
|
+
if (inForLoop && line2.trim() === "}" && i > forLoopStart + 2) {
|
|
5079
5094
|
inForLoop = false;
|
|
5080
5095
|
}
|
|
5081
5096
|
}
|
|
@@ -5087,21 +5102,21 @@ function extractFunctions(filePath, content) {
|
|
|
5087
5102
|
const funcRegex = /(?:export\s+)?(?:async\s+)?function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>/;
|
|
5088
5103
|
let currentFunc = null;
|
|
5089
5104
|
for (let i = 0; i < lines.length; i++) {
|
|
5090
|
-
const
|
|
5091
|
-
const trimmed =
|
|
5105
|
+
const line2 = lines[i];
|
|
5106
|
+
const trimmed = line2.trim();
|
|
5092
5107
|
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
|
5093
5108
|
continue;
|
|
5094
5109
|
}
|
|
5095
5110
|
if (!currentFunc) {
|
|
5096
|
-
const match =
|
|
5111
|
+
const match = line2.match(funcRegex);
|
|
5097
5112
|
if (match) {
|
|
5098
5113
|
const funcName = match[1] || match[2];
|
|
5099
5114
|
currentFunc = { name: funcName, start: i, braceCount: 0 };
|
|
5100
5115
|
}
|
|
5101
5116
|
}
|
|
5102
5117
|
if (currentFunc) {
|
|
5103
|
-
currentFunc.braceCount += (
|
|
5104
|
-
currentFunc.braceCount -= (
|
|
5118
|
+
currentFunc.braceCount += (line2.match(/{/g) || []).length;
|
|
5119
|
+
currentFunc.braceCount -= (line2.match(/}/g) || []).length;
|
|
5105
5120
|
if (currentFunc.braceCount <= 0 && i > currentFunc.start) {
|
|
5106
5121
|
blocks.push({
|
|
5107
5122
|
file: filePath,
|
|
@@ -5310,10 +5325,146 @@ function makeIntentBug(cwd, contract, reason) {
|
|
|
5310
5325
|
};
|
|
5311
5326
|
}
|
|
5312
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
|
+
|
|
5313
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
|
+
}
|
|
5314
5463
|
async function scanCommand(paths, options) {
|
|
5464
|
+
const scanStartTime = Date.now();
|
|
5315
5465
|
const cwd = process.cwd();
|
|
5316
5466
|
const whiterosePath = join(cwd, ".whiterose");
|
|
5467
|
+
const repoName = getRepoName(cwd);
|
|
5317
5468
|
const isQuickScan = options.quick || options.ci;
|
|
5318
5469
|
const isQuiet = options.json || options.sarif || options.ci;
|
|
5319
5470
|
if (!isQuickScan && !existsSync(whiterosePath)) {
|
|
@@ -5326,8 +5477,10 @@ async function scanCommand(paths, options) {
|
|
|
5326
5477
|
process.exit(1);
|
|
5327
5478
|
}
|
|
5328
5479
|
if (!isQuiet) {
|
|
5329
|
-
const scanMode = isQuickScan ? "quick
|
|
5330
|
-
|
|
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();
|
|
5331
5484
|
}
|
|
5332
5485
|
let config;
|
|
5333
5486
|
let understanding;
|
|
@@ -5373,28 +5526,19 @@ async function scanCommand(paths, options) {
|
|
|
5373
5526
|
if (options.full || paths.length > 0) {
|
|
5374
5527
|
scanType = "full";
|
|
5375
5528
|
if (!isQuiet) {
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
} else {
|
|
5385
|
-
filesToScan = await scanCodebase(cwd, config);
|
|
5386
|
-
}
|
|
5387
|
-
spinner6.stop(`Found ${filesToScan.length} files to scan`);
|
|
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
|
+
});
|
|
5388
5537
|
} else {
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
absolute: false
|
|
5394
|
-
});
|
|
5395
|
-
} else {
|
|
5396
|
-
filesToScan = await scanCodebase(cwd, config);
|
|
5397
|
-
}
|
|
5538
|
+
filesToScan = await scanCodebase(cwd, config);
|
|
5539
|
+
}
|
|
5540
|
+
if (!isQuiet) {
|
|
5541
|
+
console.log(chalk3.dim("\u2502") + ` Found ${chalk3.cyan(filesToScan.length)} files`);
|
|
5398
5542
|
}
|
|
5399
5543
|
} else {
|
|
5400
5544
|
if (!config) {
|
|
@@ -5420,12 +5564,11 @@ async function scanCommand(paths, options) {
|
|
|
5420
5564
|
}
|
|
5421
5565
|
let staticResults;
|
|
5422
5566
|
if (!isQuiet) {
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
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`);
|
|
5429
5572
|
}
|
|
5430
5573
|
const providerName = options.provider || config?.provider || "claude-code";
|
|
5431
5574
|
const executor = getExecutor(providerName);
|
|
@@ -5437,13 +5580,12 @@ async function scanCommand(paths, options) {
|
|
|
5437
5580
|
}
|
|
5438
5581
|
let bugs;
|
|
5439
5582
|
if (!isQuiet) {
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5583
|
+
console.log(chalk3.dim("\u2502"));
|
|
5584
|
+
console.log(chalk3.cyan("\u2550\u2550\u2550 Analyzing with " + providerName + " \u2550\u2550\u2550"));
|
|
5585
|
+
console.log();
|
|
5443
5586
|
const scanner = new CoreScanner(executor, {}, {
|
|
5444
5587
|
onProgress: (message) => {
|
|
5445
5588
|
if (message.trim()) {
|
|
5446
|
-
llmSpinner.stop("");
|
|
5447
5589
|
if (message.includes("\u2550\u2550\u2550\u2550")) {
|
|
5448
5590
|
console.log(chalk3.cyan(message));
|
|
5449
5591
|
} else if (message.includes("\u2713")) {
|
|
@@ -5455,13 +5597,11 @@ async function scanCommand(paths, options) {
|
|
|
5455
5597
|
} else {
|
|
5456
5598
|
console.log(chalk3.dim(message));
|
|
5457
5599
|
}
|
|
5458
|
-
llmSpinner.start("Scanning...");
|
|
5459
5600
|
}
|
|
5460
5601
|
},
|
|
5461
5602
|
onBugFound: (bug) => {
|
|
5462
|
-
|
|
5463
|
-
console.log(chalk3.magenta(
|
|
5464
|
-
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}`);
|
|
5465
5605
|
}
|
|
5466
5606
|
});
|
|
5467
5607
|
try {
|
|
@@ -5480,11 +5620,11 @@ async function scanCommand(paths, options) {
|
|
|
5480
5620
|
config
|
|
5481
5621
|
});
|
|
5482
5622
|
}
|
|
5483
|
-
|
|
5484
|
-
|
|
5623
|
+
console.log();
|
|
5624
|
+
console.log(chalk3.dim("\u2502") + ` Found ${chalk3.cyan(bugs.length)} potential bugs`);
|
|
5485
5625
|
if (scanner.hasPassErrors()) {
|
|
5486
5626
|
const errors = scanner.getPassErrors();
|
|
5487
|
-
|
|
5627
|
+
console.log(chalk3.yellow("\u26A0") + ` ${errors.length} analysis pass(es) failed:`);
|
|
5488
5628
|
for (const err of errors.slice(0, 5)) {
|
|
5489
5629
|
console.log(chalk3.yellow(` - ${err.passName}: ${err.error}`));
|
|
5490
5630
|
}
|
|
@@ -5493,8 +5633,8 @@ async function scanCommand(paths, options) {
|
|
|
5493
5633
|
}
|
|
5494
5634
|
}
|
|
5495
5635
|
} catch (error) {
|
|
5496
|
-
|
|
5497
|
-
|
|
5636
|
+
console.log(chalk3.red("\u2717") + " Analysis failed");
|
|
5637
|
+
console.error(String(error));
|
|
5498
5638
|
process.exit(1);
|
|
5499
5639
|
}
|
|
5500
5640
|
} else {
|
|
@@ -5527,61 +5667,53 @@ async function scanCommand(paths, options) {
|
|
|
5527
5667
|
}
|
|
5528
5668
|
if (!isQuickScan) {
|
|
5529
5669
|
if (!isQuiet) {
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
bugs.push(...crossFileBugs);
|
|
5536
|
-
crossFileSpinner.stop(`Cross-file analysis: ${crossFileBugs.length} issues found`);
|
|
5537
|
-
} else {
|
|
5538
|
-
crossFileSpinner.stop("Cross-file analysis: no issues");
|
|
5539
|
-
}
|
|
5540
|
-
} catch {
|
|
5541
|
-
crossFileSpinner.stop("Cross-file analysis: skipped");
|
|
5542
|
-
}
|
|
5543
|
-
} else {
|
|
5544
|
-
try {
|
|
5545
|
-
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) {
|
|
5546
5675
|
bugs.push(...crossFileBugs);
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
console.error(JSON.stringify({
|
|
5550
|
-
error: "Cross-file analysis failed",
|
|
5551
|
-
message: err instanceof Error ? err.message : String(err)
|
|
5552
|
-
}));
|
|
5553
|
-
process.exit(1);
|
|
5676
|
+
if (!isQuiet) {
|
|
5677
|
+
console.log(chalk3.dim("\u2502") + ` Cross-file: ${chalk3.cyan(crossFileBugs.length)} issues`);
|
|
5554
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);
|
|
5555
5691
|
}
|
|
5556
5692
|
}
|
|
5557
5693
|
}
|
|
5558
5694
|
if (!isQuickScan) {
|
|
5559
5695
|
if (!isQuiet) {
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
bugs.push(...contractBugs);
|
|
5566
|
-
contractSpinner.stop(`Contract analysis: ${contractBugs.length} issues found`);
|
|
5567
|
-
} else {
|
|
5568
|
-
contractSpinner.stop("Contract analysis: no issues");
|
|
5569
|
-
}
|
|
5570
|
-
} catch {
|
|
5571
|
-
contractSpinner.stop("Contract analysis: skipped");
|
|
5572
|
-
}
|
|
5573
|
-
} else {
|
|
5574
|
-
try {
|
|
5575
|
-
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) {
|
|
5576
5701
|
bugs.push(...contractBugs);
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
console.error(JSON.stringify({
|
|
5580
|
-
error: "Contract analysis failed",
|
|
5581
|
-
message: err instanceof Error ? err.message : String(err)
|
|
5582
|
-
}));
|
|
5583
|
-
process.exit(1);
|
|
5702
|
+
if (!isQuiet) {
|
|
5703
|
+
console.log(chalk3.dim("\u2502") + ` Contract: ${chalk3.cyan(contractBugs.length)} issues`);
|
|
5584
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);
|
|
5585
5717
|
}
|
|
5586
5718
|
}
|
|
5587
5719
|
}
|
|
@@ -5621,25 +5753,45 @@ async function scanCommand(paths, options) {
|
|
|
5621
5753
|
);
|
|
5622
5754
|
}
|
|
5623
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
|
+
};
|
|
5624
5784
|
const result = {
|
|
5625
5785
|
id: `scan-${Date.now()}`,
|
|
5626
5786
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5627
5787
|
scanType,
|
|
5628
5788
|
filesScanned: filesToScan.length,
|
|
5629
5789
|
filesChanged: scanType === "incremental" ? filesToScan.length : void 0,
|
|
5630
|
-
duration:
|
|
5631
|
-
|
|
5790
|
+
duration: scanDuration,
|
|
5791
|
+
linesOfCode,
|
|
5632
5792
|
bugs: allBugs,
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
critical: allBugs.filter((b) => b.kind === "bug" && b.severity === "critical").length,
|
|
5636
|
-
high: allBugs.filter((b) => b.kind === "bug" && b.severity === "high").length,
|
|
5637
|
-
medium: allBugs.filter((b) => b.kind === "bug" && b.severity === "medium").length,
|
|
5638
|
-
low: allBugs.filter((b) => b.kind === "bug" && b.severity === "low").length,
|
|
5639
|
-
total: allBugs.length,
|
|
5640
|
-
bugs: allBugs.filter((b) => b.kind === "bug").length,
|
|
5641
|
-
smells: allBugs.filter((b) => b.kind === "smell").length
|
|
5642
|
-
}
|
|
5793
|
+
summary,
|
|
5794
|
+
meta
|
|
5643
5795
|
};
|
|
5644
5796
|
if (pendingHashState) {
|
|
5645
5797
|
try {
|
|
@@ -5649,12 +5801,12 @@ async function scanCommand(paths, options) {
|
|
|
5649
5801
|
}
|
|
5650
5802
|
if (options.json || options.ci && !options.sarif) {
|
|
5651
5803
|
console.log(JSON.stringify(result, null, 2));
|
|
5652
|
-
if (options.ci && result.summary.bugs > 0) {
|
|
5804
|
+
if (options.ci && result.summary.bugs.total > 0) {
|
|
5653
5805
|
process.exit(1);
|
|
5654
5806
|
}
|
|
5655
5807
|
} else if (options.sarif) {
|
|
5656
5808
|
console.log(JSON.stringify(outputSarif(result), null, 2));
|
|
5657
|
-
if (options.ci && result.summary.bugs > 0) {
|
|
5809
|
+
if (options.ci && result.summary.bugs.total > 0) {
|
|
5658
5810
|
process.exit(1);
|
|
5659
5811
|
}
|
|
5660
5812
|
} else {
|
|
@@ -5677,32 +5829,24 @@ async function scanCommand(paths, options) {
|
|
|
5677
5829
|
writeFileSync(sarifPath, JSON.stringify(outputSarif(result), null, 2));
|
|
5678
5830
|
const jsonPath = join(outputDir, "bugs.json");
|
|
5679
5831
|
writeFileSync(jsonPath, JSON.stringify(result, null, 2));
|
|
5832
|
+
const lastScanPath = join(whiterosePath, "last-scan.json");
|
|
5833
|
+
writeFileSync(lastScanPath, JSON.stringify(result, null, 2));
|
|
5680
5834
|
writeFileSync(join(reportsDir, `${timestamp}.sarif`), JSON.stringify(outputSarif(result), null, 2));
|
|
5681
5835
|
console.log();
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
console.log();
|
|
5689
|
-
if (newBugsThisScan > 0) {
|
|
5690
|
-
console.log(` ${chalk3.green("+")} New this scan: ${newBugsThisScan}`);
|
|
5691
|
-
}
|
|
5692
|
-
console.log(
|
|
5693
|
-
` ${chalk3.bold("Total findings:")} ${result.summary.total} (bugs: ${result.summary.bugs}, smells: ${result.summary.smells})`
|
|
5694
|
-
);
|
|
5836
|
+
const cardData = {
|
|
5837
|
+
meta,
|
|
5838
|
+
bugs: summary.bugs,
|
|
5839
|
+
smells: summary.smells,
|
|
5840
|
+
reportPath: "./" + relative(cwd, humanPath)
|
|
5841
|
+
};
|
|
5842
|
+
console.log(renderScanCard(cardData));
|
|
5695
5843
|
console.log();
|
|
5696
|
-
|
|
5697
|
-
console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan(humanPath)} ${chalk3.dim("(tester-friendly)")}`);
|
|
5698
|
-
console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan(mdPath)} ${chalk3.dim("(technical)")}`);
|
|
5699
|
-
console.log(` ${chalk3.dim("\u251C")} ${chalk3.cyan(sarifPath)}`);
|
|
5700
|
-
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))}`);
|
|
5701
5849
|
console.log();
|
|
5702
|
-
if (result.summary.total > 0) {
|
|
5703
|
-
p3.log.info(`Run ${chalk3.cyan("whiterose fix")} to fix bugs interactively.`);
|
|
5704
|
-
}
|
|
5705
|
-
p3.outro(chalk3.green("Scan complete"));
|
|
5706
5850
|
}
|
|
5707
5851
|
}
|
|
5708
5852
|
var Dashboard = ({ bugs, onSelectCategory }) => {
|
|
@@ -6733,8 +6877,8 @@ async function runAgenticFix(bug, config, projectDir, onProgress) {
|
|
|
6733
6877
|
lineBuffer += text2;
|
|
6734
6878
|
const lines = lineBuffer.split("\n");
|
|
6735
6879
|
lineBuffer = lines.pop() || "";
|
|
6736
|
-
for (const
|
|
6737
|
-
const trimmed =
|
|
6880
|
+
for (const line2 of lines) {
|
|
6881
|
+
const trimmed = line2.trim();
|
|
6738
6882
|
if (trimmed) {
|
|
6739
6883
|
try {
|
|
6740
6884
|
const event = JSON.parse(trimmed);
|
|
@@ -7120,7 +7264,7 @@ function loadBugsFromSarif(sarifPath) {
|
|
|
7120
7264
|
const rawFile = r.locations?.[0]?.physicalLocation?.artifactLocation?.uri;
|
|
7121
7265
|
const file = typeof rawFile === "string" ? rawFile : "unknown";
|
|
7122
7266
|
const rawLine = r.locations?.[0]?.physicalLocation?.region?.startLine;
|
|
7123
|
-
const
|
|
7267
|
+
const line2 = typeof rawLine === "number" && Number.isFinite(rawLine) ? Math.floor(rawLine) : 0;
|
|
7124
7268
|
const rawEndLine = r.locations?.[0]?.physicalLocation?.region?.endLine;
|
|
7125
7269
|
const endLine = typeof rawEndLine === "number" && Number.isFinite(rawEndLine) ? Math.floor(rawEndLine) : void 0;
|
|
7126
7270
|
const rawId = r.ruleId;
|
|
@@ -7133,7 +7277,7 @@ function loadBugsFromSarif(sarifPath) {
|
|
|
7133
7277
|
title: sanitizeSarifText(String(rawTitle), "title"),
|
|
7134
7278
|
description: sanitizeSarifText(String(rawDescription), "description"),
|
|
7135
7279
|
file,
|
|
7136
|
-
line,
|
|
7280
|
+
line: line2,
|
|
7137
7281
|
endLine,
|
|
7138
7282
|
kind: validatedKind.success ? validatedKind.data : "bug",
|
|
7139
7283
|
severity: mapSarifLevel(r.level),
|
|
@@ -7415,13 +7559,13 @@ async function fixSingleBug(bug, config, options, cwd) {
|
|
|
7415
7559
|
if (result.diff) {
|
|
7416
7560
|
console.log();
|
|
7417
7561
|
console.log(chalk3.dim(" Changes:"));
|
|
7418
|
-
for (const
|
|
7419
|
-
if (
|
|
7420
|
-
console.log(chalk3.green(` ${
|
|
7421
|
-
} else if (
|
|
7422
|
-
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}`));
|
|
7423
7567
|
} else {
|
|
7424
|
-
console.log(chalk3.dim(` ${
|
|
7568
|
+
console.log(chalk3.dim(` ${line2}`));
|
|
7425
7569
|
}
|
|
7426
7570
|
}
|
|
7427
7571
|
console.log();
|
|
@@ -7505,6 +7649,18 @@ async function refreshCommand(_options) {
|
|
|
7505
7649
|
}
|
|
7506
7650
|
p3.outro(chalk3.green("Refresh complete!"));
|
|
7507
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
|
+
}
|
|
7508
7664
|
async function statusCommand() {
|
|
7509
7665
|
const cwd = process.cwd();
|
|
7510
7666
|
const whiterosePath = join(cwd, ".whiterose");
|
|
@@ -7513,67 +7669,75 @@ async function statusCommand() {
|
|
|
7513
7669
|
p3.log.info('Run "whiterose init" first.');
|
|
7514
7670
|
process.exit(1);
|
|
7515
7671
|
}
|
|
7516
|
-
|
|
7672
|
+
console.log();
|
|
7673
|
+
console.log(chalk3.red.bold("whiterose") + chalk3.dim(" status"));
|
|
7674
|
+
console.log();
|
|
7517
7675
|
const config = await loadConfig(cwd);
|
|
7518
|
-
const
|
|
7519
|
-
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
|
+
}
|
|
7520
7707
|
console.log();
|
|
7521
|
-
|
|
7522
|
-
console.log(
|
|
7523
|
-
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"}`);
|
|
7524
7712
|
console.log();
|
|
7713
|
+
const understanding = await loadUnderstanding(cwd);
|
|
7525
7714
|
if (understanding) {
|
|
7526
|
-
console.log(chalk3.
|
|
7527
|
-
console.log(`
|
|
7528
|
-
console.log(`
|
|
7529
|
-
console.log(`
|
|
7530
|
-
console.log(`
|
|
7531
|
-
console.log(`
|
|
7532
|
-
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}`);
|
|
7533
7721
|
console.log();
|
|
7534
7722
|
}
|
|
7535
7723
|
const hashesPath = join(whiterosePath, "cache", "file-hashes.json");
|
|
7536
7724
|
if (existsSync(hashesPath)) {
|
|
7537
7725
|
try {
|
|
7538
7726
|
const hashes = JSON.parse(readFileSync(hashesPath, "utf-8"));
|
|
7539
|
-
console.log(chalk3.
|
|
7540
|
-
console.log(`
|
|
7541
|
-
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"}`);
|
|
7542
7730
|
console.log();
|
|
7543
7731
|
} catch {
|
|
7544
7732
|
}
|
|
7545
7733
|
}
|
|
7546
7734
|
const bugStats = getAccumulatedBugsStats(cwd);
|
|
7547
7735
|
if (bugStats.total > 0) {
|
|
7548
|
-
console.log(chalk3.
|
|
7549
|
-
console.log(` ${chalk3.dim("Total:")} ${bugStats.total}`);
|
|
7550
|
-
if (Object.keys(bugStats.bySeverity).length > 0) {
|
|
7551
|
-
for (const [severity, count] of Object.entries(bugStats.bySeverity)) {
|
|
7552
|
-
const color = severity === "critical" ? "red" : severity === "high" ? "yellow" : severity === "medium" ? "blue" : "dim";
|
|
7553
|
-
console.log(` ${chalk3[color]("\u25CF")} ${severity}: ${count}`);
|
|
7554
|
-
}
|
|
7555
|
-
}
|
|
7556
|
-
console.log(` ${chalk3.dim("Last updated:")} ${new Date(bugStats.lastUpdated).toLocaleString()}`);
|
|
7557
|
-
console.log();
|
|
7558
|
-
}
|
|
7559
|
-
const reportsDir = join(whiterosePath, "reports");
|
|
7560
|
-
if (existsSync(reportsDir)) {
|
|
7561
|
-
const reports = readdirSync(reportsDir).filter((f) => f.endsWith(".sarif"));
|
|
7562
|
-
if (reports.length > 0) {
|
|
7563
|
-
const latestReport = reports.sort().reverse()[0];
|
|
7564
|
-
const reportPath = join(reportsDir, latestReport);
|
|
7565
|
-
const stats = statSync(reportPath);
|
|
7566
|
-
console.log(chalk3.bold(" Last Scan"));
|
|
7567
|
-
console.log(` ${chalk3.dim("Report:")} ${latestReport}`);
|
|
7568
|
-
console.log(` ${chalk3.dim("Date:")} ${stats.mtime.toISOString()}`);
|
|
7569
|
-
console.log();
|
|
7570
|
-
}
|
|
7571
|
-
}
|
|
7572
|
-
if (bugStats.total > 0) {
|
|
7573
|
-
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'));
|
|
7574
7737
|
} else {
|
|
7575
|
-
|
|
7738
|
+
console.log(chalk3.dim('Run "whiterose scan" to scan for bugs'));
|
|
7576
7739
|
}
|
|
7740
|
+
console.log();
|
|
7577
7741
|
}
|
|
7578
7742
|
async function reportCommand(options) {
|
|
7579
7743
|
const cwd = process.cwd();
|
|
@@ -7616,6 +7780,8 @@ async function reportCommand(options) {
|
|
|
7616
7780
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7617
7781
|
status: "open"
|
|
7618
7782
|
})) || [];
|
|
7783
|
+
const bugItems = bugs.filter((b) => b.kind === "bug");
|
|
7784
|
+
const smellItems = bugs.filter((b) => b.kind === "smell");
|
|
7619
7785
|
const result = {
|
|
7620
7786
|
id: "report",
|
|
7621
7787
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -7624,13 +7790,21 @@ async function reportCommand(options) {
|
|
|
7624
7790
|
duration: 0,
|
|
7625
7791
|
bugs,
|
|
7626
7792
|
summary: {
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
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
|
|
7634
7808
|
}
|
|
7635
7809
|
};
|
|
7636
7810
|
let output;
|