@shakecodeslikecray/whiterose 1.0.11 → 2.0.0
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/README.md +25 -8
- package/dist/cli/index.js +985 -25
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +145 -1
- package/dist/index.js +208 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -595,6 +595,52 @@ var OllamaExecutor = class {
|
|
|
595
595
|
}
|
|
596
596
|
}
|
|
597
597
|
};
|
|
598
|
+
var OPENCODE_TIMEOUT = 3e5;
|
|
599
|
+
var OpenCodeExecutor = class {
|
|
600
|
+
name = "opencode";
|
|
601
|
+
async isAvailable() {
|
|
602
|
+
return isProviderAvailable("opencode");
|
|
603
|
+
}
|
|
604
|
+
async runPrompt(prompt, options) {
|
|
605
|
+
const command = getProviderCommand("opencode");
|
|
606
|
+
try {
|
|
607
|
+
const { stdout, stderr } = await execa(
|
|
608
|
+
command,
|
|
609
|
+
["run", prompt],
|
|
610
|
+
{
|
|
611
|
+
cwd: options.cwd,
|
|
612
|
+
timeout: options.timeout || OPENCODE_TIMEOUT,
|
|
613
|
+
env: {
|
|
614
|
+
...process.env,
|
|
615
|
+
NO_COLOR: "1"
|
|
616
|
+
},
|
|
617
|
+
reject: false,
|
|
618
|
+
stdin: "ignore"
|
|
619
|
+
}
|
|
620
|
+
);
|
|
621
|
+
if (stderr) {
|
|
622
|
+
if (stderr.includes("429") || stderr.includes("rate limit") || stderr.includes("quota exceeded")) {
|
|
623
|
+
throw new Error("OpenCode API rate limit reached. Try again later.");
|
|
624
|
+
}
|
|
625
|
+
if (stderr.includes("401") || stderr.includes("403") || stderr.includes("unauthorized") || stderr.includes("invalid api key")) {
|
|
626
|
+
throw new Error("OpenCode API authentication failed. Check your API key.");
|
|
627
|
+
}
|
|
628
|
+
if ((stderr.includes("Error") || stderr.includes("error")) && !stdout) {
|
|
629
|
+
throw new Error(`OpenCode error: ${stderr.substring(0, 200)}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
output: stdout || "",
|
|
634
|
+
error: stderr || void 0
|
|
635
|
+
};
|
|
636
|
+
} catch (error) {
|
|
637
|
+
if (error.message?.includes("ENOENT")) {
|
|
638
|
+
throw new Error("OpenCode CLI not found. Install: curl -fsSL https://opencode.ai/install | bash");
|
|
639
|
+
}
|
|
640
|
+
throw error;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
};
|
|
598
644
|
|
|
599
645
|
// src/providers/executors/index.ts
|
|
600
646
|
var executors = {
|
|
@@ -602,7 +648,8 @@ var executors = {
|
|
|
602
648
|
"codex": () => new CodexExecutor(),
|
|
603
649
|
"gemini": () => new GeminiExecutor(),
|
|
604
650
|
"aider": () => new AiderExecutor(),
|
|
605
|
-
"ollama": () => new OllamaExecutor()
|
|
651
|
+
"ollama": () => new OllamaExecutor(),
|
|
652
|
+
"opencode": () => new OpenCodeExecutor()
|
|
606
653
|
};
|
|
607
654
|
function getExecutor(name) {
|
|
608
655
|
const factory = executors[name];
|
|
@@ -655,7 +702,7 @@ ${pass.whatToFind.map((w) => `- ${w}`).join("\n")}
|
|
|
655
702
|
${pass.traceInstructions}
|
|
656
703
|
|
|
657
704
|
## ENTRY POINTS TO START FROM
|
|
658
|
-
${pass.entryPointPatterns.map((
|
|
705
|
+
${pass.entryPointPatterns.map((p10) => `- ${p10}`).join("\n")}
|
|
659
706
|
|
|
660
707
|
## EXAMPLE VULNERABILITY
|
|
661
708
|
This is what a real finding looks like for this pass:
|
|
@@ -1195,12 +1242,12 @@ ${entryPointsSection}
|
|
|
1195
1242
|
${staticSection}
|
|
1196
1243
|
## WHAT TO LOOK FOR
|
|
1197
1244
|
|
|
1198
|
-
${pass.searchPatterns.map((
|
|
1245
|
+
${pass.searchPatterns.map((p10) => `- ${p10}`).join("\n")}
|
|
1199
1246
|
|
|
1200
1247
|
## SEARCH STRATEGY
|
|
1201
1248
|
|
|
1202
1249
|
Use these grep patterns to find potential issues:
|
|
1203
|
-
${pass.grepPatterns.map((
|
|
1250
|
+
${pass.grepPatterns.map((p10) => `\`${p10}\``).join("\n")}
|
|
1204
1251
|
|
|
1205
1252
|
## METHODOLOGY
|
|
1206
1253
|
|
|
@@ -1267,6 +1314,67 @@ IMPORTANT:
|
|
|
1267
1314
|
- If unsure, report it with confidence="low"
|
|
1268
1315
|
- Aim for thoroughness - finding 10 potential issues is better than finding 0 confirmed bugs`;
|
|
1269
1316
|
}
|
|
1317
|
+
function buildCustomPassPrompt(customPass, ctx) {
|
|
1318
|
+
const { projectType, framework, language, totalFiles } = ctx;
|
|
1319
|
+
const staticSection = ctx.staticFindings?.length ? `
|
|
1320
|
+
## STATIC ANALYSIS SIGNALS
|
|
1321
|
+
${ctx.staticFindings.slice(0, 15).map((f) => `- ${f.tool}: ${f.file}:${f.line} - ${f.message}`).join("\n")}
|
|
1322
|
+
` : "";
|
|
1323
|
+
return `You are a security specialist performing a TARGETED analysis: ${customPass.id.toUpperCase()}.
|
|
1324
|
+
|
|
1325
|
+
## YOUR SINGLE MISSION
|
|
1326
|
+
${customPass.description}
|
|
1327
|
+
|
|
1328
|
+
## PROJECT CONTEXT
|
|
1329
|
+
- Type: ${projectType}
|
|
1330
|
+
- Framework: ${framework || "Unknown"}
|
|
1331
|
+
- Language: ${language}
|
|
1332
|
+
- Size: ${totalFiles} files
|
|
1333
|
+
${staticSection}
|
|
1334
|
+
## METHODOLOGY
|
|
1335
|
+
|
|
1336
|
+
1. Search the codebase for patterns relevant to: ${customPass.category}
|
|
1337
|
+
2. Read files that match and analyze for the specific issues described above
|
|
1338
|
+
3. Trace data flow to confirm the issue is real
|
|
1339
|
+
4. Report all confirmed and suspected issues
|
|
1340
|
+
|
|
1341
|
+
## REPORTING FORMAT
|
|
1342
|
+
|
|
1343
|
+
When you find an issue:
|
|
1344
|
+
|
|
1345
|
+
<json>
|
|
1346
|
+
{
|
|
1347
|
+
"type": "bug",
|
|
1348
|
+
"data": {
|
|
1349
|
+
"file": "src/path/to/file.ts",
|
|
1350
|
+
"line": 42,
|
|
1351
|
+
"endLine": 45,
|
|
1352
|
+
"title": "Short description of the issue",
|
|
1353
|
+
"description": "Explanation of why this is problematic and potential impact.",
|
|
1354
|
+
"kind": "bug|smell",
|
|
1355
|
+
"category": "${customPass.category}",
|
|
1356
|
+
"severity": "critical|high|medium|low",
|
|
1357
|
+
"confidence": "high|medium|low",
|
|
1358
|
+
"evidence": [
|
|
1359
|
+
"Evidence point 1",
|
|
1360
|
+
"Evidence point 2"
|
|
1361
|
+
],
|
|
1362
|
+
"suggestedFix": "Optional: how to fix it"
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
</json>
|
|
1366
|
+
|
|
1367
|
+
Progress updates:
|
|
1368
|
+
###SCANNING:path/to/file.ts
|
|
1369
|
+
|
|
1370
|
+
When done:
|
|
1371
|
+
###COMPLETE
|
|
1372
|
+
|
|
1373
|
+
## BEGIN
|
|
1374
|
+
|
|
1375
|
+
Search for ${customPass.id} patterns. Read files that match. Report issues as you find them.
|
|
1376
|
+
This is a TARGETED pass - focus exclusively on: ${customPass.description}`;
|
|
1377
|
+
}
|
|
1270
1378
|
|
|
1271
1379
|
// src/providers/prompts/constants.ts
|
|
1272
1380
|
var PROJECT_TYPES_PROMPT = `PROJECT TYPE OPTIONS (pick the best fit):
|
|
@@ -1689,7 +1797,7 @@ var SCAN_PASSES = [
|
|
|
1689
1797
|
}
|
|
1690
1798
|
];
|
|
1691
1799
|
function getPassConfig(name) {
|
|
1692
|
-
return SCAN_PASSES.find((
|
|
1800
|
+
return SCAN_PASSES.find((p10) => p10.name === name);
|
|
1693
1801
|
}
|
|
1694
1802
|
|
|
1695
1803
|
// src/core/flow-analyzer.ts
|
|
@@ -2179,7 +2287,7 @@ const user = await User.create(req.body); // No validation! role:'admin' accept
|
|
|
2179
2287
|
}
|
|
2180
2288
|
];
|
|
2181
2289
|
function getFlowPassConfig(name) {
|
|
2182
|
-
return FLOW_PASSES.find((
|
|
2290
|
+
return FLOW_PASSES.find((p10) => p10.name === name);
|
|
2183
2291
|
}
|
|
2184
2292
|
|
|
2185
2293
|
// src/core/utils.ts
|
|
@@ -2232,38 +2340,68 @@ var CoreScanner = class {
|
|
|
2232
2340
|
const startTime = Date.now();
|
|
2233
2341
|
this.passErrors = [];
|
|
2234
2342
|
const pipeline = getFullAnalysisPipeline();
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
const
|
|
2343
|
+
let unitPasses = pipeline[0].passes;
|
|
2344
|
+
let integrationPasses = pipeline[1].passes;
|
|
2345
|
+
let e2ePasses = pipeline[2].passes;
|
|
2346
|
+
const rp = context.riskProfile;
|
|
2347
|
+
const skippedNames = new Set(rp?.skippedPasses.map((s) => s.passName) || []);
|
|
2348
|
+
const customUnitPasses = rp?.customPasses.filter((p10) => p10.phase === "unit") || [];
|
|
2349
|
+
const customIntegrationPasses = rp?.customPasses.filter((p10) => p10.phase === "integration") || [];
|
|
2350
|
+
const customE2EPasses = rp?.customPasses.filter((p10) => p10.phase === "e2e") || [];
|
|
2351
|
+
if (skippedNames.size > 0) {
|
|
2352
|
+
unitPasses = unitPasses.filter((p10) => !skippedNames.has(p10));
|
|
2353
|
+
integrationPasses = integrationPasses.filter((p10) => !skippedNames.has(p10));
|
|
2354
|
+
e2ePasses = e2ePasses.filter((p10) => !skippedNames.has(p10));
|
|
2355
|
+
}
|
|
2356
|
+
const totalStandard = unitPasses.length + integrationPasses.length + e2ePasses.length;
|
|
2357
|
+
const totalCustom = customUnitPasses.length + customIntegrationPasses.length + customE2EPasses.length;
|
|
2358
|
+
const totalPasses = totalStandard + totalCustom;
|
|
2239
2359
|
this.report(`
|
|
2240
2360
|
\u2550\u2550\u2550\u2550 CORE SCANNER (PIPELINE MODE) \u2550\u2550\u2550\u2550`);
|
|
2241
2361
|
this.report(` Provider: ${this.executor.name}`);
|
|
2242
|
-
this.report(` Passes: ${totalPasses} (${unitPasses.length} unit \u2192 ${integrationPasses.length} integration \u2192 ${e2ePasses.length} E2E)`);
|
|
2362
|
+
this.report(` Passes: ${totalPasses} (${unitPasses.length + customUnitPasses.length} unit \u2192 ${integrationPasses.length + customIntegrationPasses.length} integration \u2192 ${e2ePasses.length + customE2EPasses.length} E2E)`);
|
|
2363
|
+
if (totalCustom > 0) {
|
|
2364
|
+
this.report(` Risk profile: ${totalCustom} custom passes, ${skippedNames.size} skipped`);
|
|
2365
|
+
}
|
|
2243
2366
|
this.report(` Findings flow: Unit \u2192 Integration \u2192 E2E`);
|
|
2244
2367
|
let globalBugIndex = 0;
|
|
2245
2368
|
this.report(`
|
|
2246
2369
|
\u2550\u2550\u2550\u2550 PHASE 1: UNIT ANALYSIS \u2550\u2550\u2550\u2550`);
|
|
2247
2370
|
this.report(` Looking for: injection, null refs, auth bypass, etc.`);
|
|
2371
|
+
const customUnitJobs = this.buildCustomPassJobs(context, customUnitPasses);
|
|
2372
|
+
const customUnitFindings = await this.runPassBatch(customUnitJobs, cwd, context.files, globalBugIndex);
|
|
2373
|
+
this.boostCustomPassConfidence(customUnitFindings);
|
|
2374
|
+
globalBugIndex += customUnitFindings.length;
|
|
2248
2375
|
const unitJobs = this.buildUnitPassJobs(context, unitPasses);
|
|
2249
|
-
const
|
|
2250
|
-
globalBugIndex +=
|
|
2376
|
+
const standardUnitFindings = await this.runPassBatch(unitJobs, cwd, context.files, globalBugIndex);
|
|
2377
|
+
globalBugIndex += standardUnitFindings.length;
|
|
2378
|
+
const unitFindings = [...customUnitFindings, ...standardUnitFindings];
|
|
2251
2379
|
this.report(` Phase 1 complete: ${unitFindings.length} findings`);
|
|
2252
2380
|
this.report(`
|
|
2253
2381
|
\u2550\u2550\u2550\u2550 PHASE 2: INTEGRATION ANALYSIS \u2550\u2550\u2550\u2550`);
|
|
2254
2382
|
this.report(` Building on ${unitFindings.length} unit findings`);
|
|
2255
2383
|
this.report(` Looking for: auth flows, data flows, trust boundaries`);
|
|
2384
|
+
const customIntJobs = this.buildCustomPassJobs(context, customIntegrationPasses);
|
|
2385
|
+
const customIntFindings = await this.runPassBatch(customIntJobs, cwd, context.files, globalBugIndex);
|
|
2386
|
+
this.boostCustomPassConfidence(customIntFindings);
|
|
2387
|
+
globalBugIndex += customIntFindings.length;
|
|
2256
2388
|
const integrationJobs = this.buildIntegrationPassJobs(context, integrationPasses, unitFindings);
|
|
2257
|
-
const
|
|
2258
|
-
globalBugIndex +=
|
|
2389
|
+
const standardIntFindings = await this.runPassBatch(integrationJobs, cwd, context.files, globalBugIndex);
|
|
2390
|
+
globalBugIndex += standardIntFindings.length;
|
|
2391
|
+
const integrationFindings = [...customIntFindings, ...standardIntFindings];
|
|
2259
2392
|
this.report(` Phase 2 complete: ${integrationFindings.length} findings`);
|
|
2260
2393
|
this.report(`
|
|
2261
2394
|
\u2550\u2550\u2550\u2550 PHASE 3: E2E ANALYSIS \u2550\u2550\u2550\u2550`);
|
|
2262
2395
|
this.report(` Building on ${unitFindings.length} unit + ${integrationFindings.length} integration findings`);
|
|
2263
2396
|
this.report(` Looking for: attack chains, privilege escalation, session bugs`);
|
|
2397
|
+
const customE2EJobs = this.buildCustomPassJobs(context, customE2EPasses);
|
|
2398
|
+
const customE2EFindings = await this.runPassBatch(customE2EJobs, cwd, context.files, globalBugIndex);
|
|
2399
|
+
this.boostCustomPassConfidence(customE2EFindings);
|
|
2400
|
+
globalBugIndex += customE2EFindings.length;
|
|
2264
2401
|
const allPreviousFindings = [...unitFindings, ...integrationFindings];
|
|
2265
2402
|
const e2eJobs = this.buildE2EPassJobs(context, e2ePasses, allPreviousFindings);
|
|
2266
|
-
const
|
|
2403
|
+
const standardE2EFindings = await this.runPassBatch(e2eJobs, cwd, context.files, globalBugIndex);
|
|
2404
|
+
const e2eFindings = [...customE2EFindings, ...standardE2EFindings];
|
|
2267
2405
|
this.report(` Phase 3 complete: ${e2eFindings.length} findings`);
|
|
2268
2406
|
this.report(`
|
|
2269
2407
|
\u2550\u2550\u2550\u2550 POST-PROCESSING \u2550\u2550\u2550\u2550`);
|
|
@@ -2297,7 +2435,7 @@ var CoreScanner = class {
|
|
|
2297
2435
|
const batchNum = Math.floor(i / this.config.batchSize) + 1;
|
|
2298
2436
|
const totalBatches = Math.ceil(passes.length / this.config.batchSize);
|
|
2299
2437
|
this.report(`
|
|
2300
|
-
[Batch ${batchNum}/${totalBatches}] ${batch.map((
|
|
2438
|
+
[Batch ${batchNum}/${totalBatches}] ${batch.map((p10) => p10.name).join(", ")}`);
|
|
2301
2439
|
const batchPromises = batch.map(async (pass) => {
|
|
2302
2440
|
this.progress.onPassStart?.(pass.name);
|
|
2303
2441
|
try {
|
|
@@ -2569,6 +2707,37 @@ var CoreScanner = class {
|
|
|
2569
2707
|
}
|
|
2570
2708
|
return jobs;
|
|
2571
2709
|
}
|
|
2710
|
+
/**
|
|
2711
|
+
* Build custom pass jobs from risk profile
|
|
2712
|
+
*/
|
|
2713
|
+
buildCustomPassJobs(context, customPasses) {
|
|
2714
|
+
const jobs = [];
|
|
2715
|
+
const { understanding, staticResults } = context;
|
|
2716
|
+
for (const customPass of customPasses) {
|
|
2717
|
+
jobs.push({
|
|
2718
|
+
name: customPass.id,
|
|
2719
|
+
prompt: buildCustomPassPrompt(customPass, {
|
|
2720
|
+
pass: { name: customPass.id, category: customPass.category, description: customPass.description},
|
|
2721
|
+
projectType: understanding.summary.type,
|
|
2722
|
+
framework: understanding.summary.framework || "",
|
|
2723
|
+
language: understanding.summary.language,
|
|
2724
|
+
totalFiles: understanding.structure.totalFiles,
|
|
2725
|
+
staticFindings: staticResults
|
|
2726
|
+
})
|
|
2727
|
+
});
|
|
2728
|
+
}
|
|
2729
|
+
return jobs;
|
|
2730
|
+
}
|
|
2731
|
+
/**
|
|
2732
|
+
* Boost confidence for custom pass findings (targeted = higher signal-to-noise)
|
|
2733
|
+
*/
|
|
2734
|
+
boostCustomPassConfidence(bugs) {
|
|
2735
|
+
for (const bug of bugs) {
|
|
2736
|
+
if (bug.confidence.overall === "medium") {
|
|
2737
|
+
bug.confidence.overall = "high";
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2572
2741
|
buildQuickScanPrompt(context) {
|
|
2573
2742
|
const { understanding, staticResults, files } = context;
|
|
2574
2743
|
const staticSignals = staticResults.length > 0 ? `
|
|
@@ -3767,6 +3936,33 @@ var CodebaseUnderstanding = z.object({
|
|
|
3767
3936
|
packages: z.array(z.string()).optional()
|
|
3768
3937
|
})
|
|
3769
3938
|
});
|
|
3939
|
+
var DomainType = z.string();
|
|
3940
|
+
var RiskLevel = z.enum(["critical", "high", "medium"]);
|
|
3941
|
+
var HotPath = z.object({
|
|
3942
|
+
file: z.string(),
|
|
3943
|
+
reason: z.string(),
|
|
3944
|
+
riskLevel: RiskLevel
|
|
3945
|
+
});
|
|
3946
|
+
var CustomPassConfig = z.object({
|
|
3947
|
+
id: z.string(),
|
|
3948
|
+
phase: z.enum(["unit", "integration", "e2e"]),
|
|
3949
|
+
category: z.string(),
|
|
3950
|
+
description: z.string()
|
|
3951
|
+
});
|
|
3952
|
+
var SkipReason = z.object({
|
|
3953
|
+
passName: z.string(),
|
|
3954
|
+
reason: z.string()
|
|
3955
|
+
});
|
|
3956
|
+
var RiskProfile = z.object({
|
|
3957
|
+
version: z.string(),
|
|
3958
|
+
generatedAt: z.string().datetime(),
|
|
3959
|
+
domains: z.array(DomainType),
|
|
3960
|
+
sensitiveDataTypes: z.array(z.string()),
|
|
3961
|
+
externalDependencies: z.array(z.string()),
|
|
3962
|
+
hotPaths: z.array(HotPath),
|
|
3963
|
+
customPasses: z.array(CustomPassConfig),
|
|
3964
|
+
skippedPasses: z.array(SkipReason)
|
|
3965
|
+
});
|
|
3770
3966
|
var FileHash = z.object({
|
|
3771
3967
|
path: z.string(),
|
|
3772
3968
|
hash: z.string(),
|
|
@@ -3811,8 +4007,6 @@ z.object({
|
|
|
3811
4007
|
summary: ScanSummary,
|
|
3812
4008
|
meta: ScanMeta.optional()
|
|
3813
4009
|
});
|
|
3814
|
-
|
|
3815
|
-
// src/core/config.ts
|
|
3816
4010
|
async function loadConfig(cwd) {
|
|
3817
4011
|
const configPath = join(cwd, ".whiterose", "config.yml");
|
|
3818
4012
|
if (!existsSync(configPath)) {
|
|
@@ -5297,7 +5491,7 @@ function functionExists(content, functionName) {
|
|
|
5297
5491
|
new RegExp(`\\blet\\s+${escaped}\\s*=\\s*`),
|
|
5298
5492
|
new RegExp(`\\b${escaped}\\s*\\(`)
|
|
5299
5493
|
];
|
|
5300
|
-
return patterns.some((
|
|
5494
|
+
return patterns.some((p10) => p10.test(content));
|
|
5301
5495
|
}
|
|
5302
5496
|
function makeIntentBug(cwd, contract, reason) {
|
|
5303
5497
|
return {
|
|
@@ -5564,6 +5758,25 @@ async function scanCommand(paths, options) {
|
|
|
5564
5758
|
p3.log.info(`Incremental scan: ${filesToScan.length} changed files`);
|
|
5565
5759
|
}
|
|
5566
5760
|
}
|
|
5761
|
+
let riskProfile;
|
|
5762
|
+
if (!isQuickScan) {
|
|
5763
|
+
const riskProfilePath = join(whiterosePath, "risk-profile.json");
|
|
5764
|
+
if (existsSync(riskProfilePath)) {
|
|
5765
|
+
try {
|
|
5766
|
+
const rpContent = JSON.parse(readFileSync(riskProfilePath, "utf-8"));
|
|
5767
|
+
const rpResult = RiskProfile.safeParse(rpContent);
|
|
5768
|
+
if (rpResult.success) {
|
|
5769
|
+
riskProfile = rpResult.data;
|
|
5770
|
+
if (!isQuiet) {
|
|
5771
|
+
const customCount = riskProfile.customPasses.length;
|
|
5772
|
+
const skipCount = riskProfile.skippedPasses.length;
|
|
5773
|
+
console.log(chalk3.dim("\u2502") + ` Risk profile: ${chalk3.cyan(customCount)} custom passes, ${chalk3.cyan(skipCount)} skipped`);
|
|
5774
|
+
}
|
|
5775
|
+
}
|
|
5776
|
+
} catch {
|
|
5777
|
+
}
|
|
5778
|
+
}
|
|
5779
|
+
}
|
|
5567
5780
|
let staticResults;
|
|
5568
5781
|
if (!isQuiet) {
|
|
5569
5782
|
console.log(chalk3.dim("\u2502") + " Running static analysis (tsc, eslint)...");
|
|
@@ -5619,7 +5832,8 @@ async function scanCommand(paths, options) {
|
|
|
5619
5832
|
files: filesToScan,
|
|
5620
5833
|
understanding,
|
|
5621
5834
|
staticResults: staticResults || [],
|
|
5622
|
-
config
|
|
5835
|
+
config,
|
|
5836
|
+
riskProfile
|
|
5623
5837
|
});
|
|
5624
5838
|
}
|
|
5625
5839
|
console.log();
|
|
@@ -5653,7 +5867,8 @@ async function scanCommand(paths, options) {
|
|
|
5653
5867
|
files: filesToScan,
|
|
5654
5868
|
understanding,
|
|
5655
5869
|
staticResults: staticResults || [],
|
|
5656
|
-
config
|
|
5870
|
+
config,
|
|
5871
|
+
riskProfile
|
|
5657
5872
|
});
|
|
5658
5873
|
}
|
|
5659
5874
|
if (options.ci && scanner.hasPassErrors()) {
|
|
@@ -5814,10 +6029,10 @@ async function scanCommand(paths, options) {
|
|
|
5814
6029
|
} else {
|
|
5815
6030
|
const outputDir = join(cwd, "whiterose-output");
|
|
5816
6031
|
const reportsDir = join(whiterosePath, "reports");
|
|
5817
|
-
const { mkdirSync:
|
|
6032
|
+
const { mkdirSync: mkdirSync5 } = await import('fs');
|
|
5818
6033
|
try {
|
|
5819
|
-
|
|
5820
|
-
|
|
6034
|
+
mkdirSync5(outputDir, { recursive: true });
|
|
6035
|
+
mkdirSync5(reportsDir, { recursive: true });
|
|
5821
6036
|
} catch {
|
|
5822
6037
|
}
|
|
5823
6038
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
@@ -7887,6 +8102,750 @@ async function clearCommand(options) {
|
|
|
7887
8102
|
p3.outro(chalk3.green("Done"));
|
|
7888
8103
|
}
|
|
7889
8104
|
|
|
8105
|
+
// src/core/risk-profiler.ts
|
|
8106
|
+
var DOMAINS = {
|
|
8107
|
+
// ═══════════════════════════════════════════════════════════
|
|
8108
|
+
// IDENTITY & ACCESS
|
|
8109
|
+
// ═══════════════════════════════════════════════════════════
|
|
8110
|
+
"auth": {
|
|
8111
|
+
keywords: ["auth", "login", "signin", "sign-in", "signup", "sign-up", "register", "password", "credential", "authenticate", "identity"],
|
|
8112
|
+
knownDeps: ["bcrypt", "bcryptjs", "argon2", "passport", "next-auth", "@auth/core", "lucia", "better-auth", "@supabase/auth-helpers-nextjs"],
|
|
8113
|
+
depSegments: ["auth", "login", "credential", "identity"],
|
|
8114
|
+
pathPatterns: [/auth[/\\]/, /login[/\\]/, /signin[/\\]/, /signup[/\\]/, /register[/\\]/],
|
|
8115
|
+
passes: [
|
|
8116
|
+
{ id: "credential-handling", phase: "unit", category: "auth-bypass", description: "Check credential handling safety. Look for: passwords stored in plaintext or weak hashes (MD5, SHA1), missing salt in hashing, timing-safe comparison not used for password/token verification, credentials logged or included in error messages." },
|
|
8117
|
+
{ id: "brute-force-protection", phase: "integration", category: "auth-bypass", description: "Check for brute force protection on authentication endpoints. Look for: login endpoints without rate limiting, missing account lockout after failed attempts, no CAPTCHA or progressive delays, enumeration possible via different error messages for valid vs invalid usernames." }
|
|
8118
|
+
]
|
|
8119
|
+
},
|
|
8120
|
+
"session": {
|
|
8121
|
+
keywords: ["session", "jwt", "token", "cookie", "refresh token"],
|
|
8122
|
+
knownDeps: ["jsonwebtoken", "jose", "express-session", "cookie-session", "iron-session", "koa-session", "fastify-secure-session", "connect-redis", "connect-mongo"],
|
|
8123
|
+
depSegments: ["session", "jwt", "token"],
|
|
8124
|
+
pathPatterns: [/sessions?[/\\]/, /tokens?[/\\]/],
|
|
8125
|
+
passes: [
|
|
8126
|
+
{ id: "session-fixation", phase: "integration", category: "auth-bypass", description: "Check for session fixation vulnerabilities. Look for: session ID not regenerated after login, pre-authentication session IDs reused after authentication, session tokens set before credential verification, session cookies missing Secure/HttpOnly/SameSite flags." },
|
|
8127
|
+
{ id: "token-lifecycle", phase: "e2e", category: "auth-bypass", description: "Trace full token lifecycle from creation to invalidation. Look for: tokens that never expire, logout not invalidating server-side sessions, refresh tokens not rotated on use, JWT with weak or missing signature verification, tokens stored in localStorage (XSS accessible)." }
|
|
8128
|
+
]
|
|
8129
|
+
},
|
|
8130
|
+
"oauth": {
|
|
8131
|
+
keywords: ["oauth", "openid", "oidc", "sso", "saml", "single sign-on", "social login", "federated"],
|
|
8132
|
+
knownDeps: ["oauth2-server", "openid-client", "grant", "simple-oauth2", "passport-google-oauth20", "passport-github2", "passport-facebook", "@auth0/nextjs-auth0", "next-auth"],
|
|
8133
|
+
depSegments: ["oauth", "openid", "oidc", "saml", "sso"],
|
|
8134
|
+
pathPatterns: [/oauth[/\\]/, /sso[/\\]/, /openid[/\\]/],
|
|
8135
|
+
passes: [
|
|
8136
|
+
{ id: "oauth-redirect-validation", phase: "integration", category: "auth-bypass", description: "Check OAuth redirect URI validation. Look for: redirect_uri not validated against allowlist, partial matching allowing subdomain takeover (example.com.evil.com), open redirects in OAuth callback flow, missing state parameter for CSRF protection, PKCE not implemented for public clients." }
|
|
8137
|
+
]
|
|
8138
|
+
},
|
|
8139
|
+
"access-control": {
|
|
8140
|
+
keywords: ["authorization", "rbac", "role", "permission", "acl", "access control", "privilege", "policy", "guard", "middleware"],
|
|
8141
|
+
knownDeps: ["casl", "@casl/ability", "accesscontrol", "casbin", "node-casbin", "role-acl"],
|
|
8142
|
+
depSegments: ["rbac", "acl", "permission", "policy", "guard"],
|
|
8143
|
+
pathPatterns: [/permissions?[/\\]/, /roles?[/\\]/, /policies[/\\]/, /guards?[/\\]/, /middleware[/\\]/, /authorization[/\\]/],
|
|
8144
|
+
passes: [
|
|
8145
|
+
{ id: "horizontal-access", phase: "integration", category: "auth-bypass", description: "Check for horizontal privilege escalation (IDOR). Look for: resource access using user-supplied IDs without ownership verification, missing tenant/user scoping on database queries, API endpoints that accept arbitrary user/resource IDs without checking the requester owns them." },
|
|
8146
|
+
{ id: "vertical-escalation", phase: "e2e", category: "auth-bypass", description: "Check for vertical privilege escalation. Look for: role field modifiable via mass assignment, role checks using client-supplied data, admin endpoints with inconsistent permission checks across layers, permission cached and not re-validated after role change." }
|
|
8147
|
+
]
|
|
8148
|
+
},
|
|
8149
|
+
"multi-tenancy": {
|
|
8150
|
+
keywords: ["tenant", "multi-tenant", "organization", "workspace", "team", "namespace"],
|
|
8151
|
+
knownDeps: [],
|
|
8152
|
+
depSegments: ["tenant", "multitenancy"],
|
|
8153
|
+
pathPatterns: [/tenants?[/\\]/, /organizations?[/\\]/, /workspaces?[/\\]/],
|
|
8154
|
+
passes: [
|
|
8155
|
+
{ id: "tenant-isolation", phase: "integration", category: "auth-bypass", description: "Check tenant data isolation. Look for: database queries without tenant ID filter, cross-tenant data leakage via shared caches, tenant ID from user input instead of authenticated session, missing tenant context in background jobs/queues, shared resources without tenant scoping." }
|
|
8156
|
+
]
|
|
8157
|
+
},
|
|
8158
|
+
"mfa": {
|
|
8159
|
+
keywords: ["mfa", "two-factor", "2fa", "totp", "otp", "authenticator", "verification code"],
|
|
8160
|
+
knownDeps: ["otplib", "speakeasy", "notp", "node-2fa"],
|
|
8161
|
+
depSegments: ["otp", "totp", "mfa", "2fa"],
|
|
8162
|
+
pathPatterns: [/mfa[/\\]/, /2fa[/\\]/, /otp[/\\]/, /verification[/\\]/],
|
|
8163
|
+
passes: [
|
|
8164
|
+
{ id: "mfa-bypass", phase: "integration", category: "auth-bypass", description: "Check for MFA bypass vulnerabilities. Look for: MFA step skippable by directly accessing post-MFA endpoints, OTP codes not rate-limited (brute-forceable), backup codes not single-use, MFA enrollment not required for sensitive operations, TOTP window too large." }
|
|
8165
|
+
]
|
|
8166
|
+
},
|
|
8167
|
+
// ═══════════════════════════════════════════════════════════
|
|
8168
|
+
// DATA LAYER
|
|
8169
|
+
// ═══════════════════════════════════════════════════════════
|
|
8170
|
+
"database": {
|
|
8171
|
+
keywords: ["database", "sql", "nosql", "orm", "migration", "repository", "persistence", "data layer", "data store"],
|
|
8172
|
+
knownDeps: ["prisma", "@prisma/client", "typeorm", "sequelize", "knex", "mongoose", "drizzle-orm", "pg", "mysql2", "better-sqlite3", "mongodb", "neo4j-driver", "cassandra-driver", "couchbase", "dynamodb", "@aws-sdk/client-dynamodb", "mikro-orm", "objection", "bookshelf", "waterline", "massive"],
|
|
8173
|
+
depSegments: ["db", "sql", "mongo", "postgres", "mysql", "sqlite", "dynamo", "cassandra", "couch", "neo4j", "orm"],
|
|
8174
|
+
pathPatterns: [/database[/\\]/, /db[/\\]/, /models?[/\\]/, /migrations?[/\\]/, /repositories?[/\\]/, /entities[/\\]/, /schemas?[/\\]/],
|
|
8175
|
+
passes: [
|
|
8176
|
+
{ id: "query-safety", phase: "unit", category: "injection", description: "Check for unsafe database query construction. Look for: string concatenation/interpolation in SQL queries, raw queries with user input, ORM methods that accept raw SQL fragments (e.g., Sequelize.literal, knex.raw), missing parameterization, NoSQL injection via object injection in MongoDB queries ($gt, $ne operators from user input)." },
|
|
8177
|
+
{ id: "mass-assignment", phase: "unit", category: "data-validation", description: "Check for mass assignment vulnerabilities. Look for: Object.assign(model, req.body), spread operator on user input into models, ORM create/update with unfiltered user input, missing field allowlists/denylists on model operations." }
|
|
8178
|
+
]
|
|
8179
|
+
},
|
|
8180
|
+
"cache": {
|
|
8181
|
+
keywords: ["cache", "caching", "memoize", "ttl", "invalidation"],
|
|
8182
|
+
knownDeps: ["ioredis", "redis", "node-cache", "lru-cache", "keyv", "catbox", "cache-manager", "memcached"],
|
|
8183
|
+
depSegments: ["cache", "redis", "memcache"],
|
|
8184
|
+
pathPatterns: [/cache[/\\]/, /caching[/\\]/],
|
|
8185
|
+
passes: [
|
|
8186
|
+
{ id: "cache-poisoning", phase: "integration", category: "injection", description: "Check for cache poisoning vulnerabilities. Look for: cache keys derived from user-controlled headers (Host, X-Forwarded-Host), cached responses including user-specific data served to other users, missing cache key scoping by user/session, cache stampede allowing timing attacks." }
|
|
8187
|
+
]
|
|
8188
|
+
},
|
|
8189
|
+
"search": {
|
|
8190
|
+
keywords: ["search", "full-text", "indexing", "autocomplete", "typeahead", "facet"],
|
|
8191
|
+
knownDeps: ["@elastic/elasticsearch", "algoliasearch", "meilisearch", "typesense", "lunr", "flexsearch", "solr-client", "minisearch"],
|
|
8192
|
+
depSegments: ["search", "elastic", "solr"],
|
|
8193
|
+
pathPatterns: [/search[/\\]/, /indexing[/\\]/],
|
|
8194
|
+
passes: [
|
|
8195
|
+
{ id: "search-injection", phase: "unit", category: "injection", description: "Check for search query injection. Look for: user input passed directly to Elasticsearch/Lucene query DSL, unescaped special characters in search queries (*, ?, ~), search queries that can access unauthorized indices or fields, autocomplete endpoints leaking private data." }
|
|
8196
|
+
]
|
|
8197
|
+
},
|
|
8198
|
+
"serialization": {
|
|
8199
|
+
keywords: ["serialize", "deserialize", "marshal", "unmarshal", "parse", "protobuf", "msgpack", "avro"],
|
|
8200
|
+
knownDeps: ["protobufjs", "msgpack5", "@msgpack/msgpack", "avsc", "bson", "flatbuffers", "js-yaml", "yaml", "fast-xml-parser", "class-transformer"],
|
|
8201
|
+
depSegments: ["serial", "proto", "msgpack", "avro", "marshal"],
|
|
8202
|
+
pathPatterns: [/serializ[/\\]/, /marshal[/\\]/, /proto[/\\]/],
|
|
8203
|
+
passes: [
|
|
8204
|
+
{ id: "deserialization-safety", phase: "unit", category: "injection", description: "Check for unsafe deserialization. Look for: eval() or Function() used for deserialization, YAML.load (unsafe) instead of YAML.safeLoad, JSON.parse of untrusted input without schema validation, prototype pollution via __proto__ or constructor.prototype in parsed objects, object spread of deserialized untrusted data." }
|
|
8205
|
+
]
|
|
8206
|
+
},
|
|
8207
|
+
"storage": {
|
|
8208
|
+
keywords: ["storage", "object storage", "blob", "bucket", "file system", "artifact"],
|
|
8209
|
+
knownDeps: ["@aws-sdk/client-s3", "@google-cloud/storage", "@azure/storage-blob", "minio", "cloudinary"],
|
|
8210
|
+
depSegments: ["storage", "s3", "blob", "bucket", "minio"],
|
|
8211
|
+
pathPatterns: [/storage[/\\]/, /blobs?[/\\]/, /buckets?[/\\]/, /artifacts?[/\\]/],
|
|
8212
|
+
passes: [
|
|
8213
|
+
{ id: "storage-access-control", phase: "integration", category: "auth-bypass", description: "Check cloud storage access controls. Look for: pre-signed URLs with excessive expiry times, public bucket/container access misconfiguration, missing ACL checks on file download endpoints, user-controlled storage paths allowing access to other users files, SSRF via storage URL construction." }
|
|
8214
|
+
]
|
|
8215
|
+
},
|
|
8216
|
+
// ═══════════════════════════════════════════════════════════
|
|
8217
|
+
// COMMUNICATION
|
|
8218
|
+
// ═══════════════════════════════════════════════════════════
|
|
8219
|
+
"api": {
|
|
8220
|
+
keywords: ["api", "rest", "endpoint", "route", "controller", "handler", "middleware", "http"],
|
|
8221
|
+
knownDeps: ["express", "fastify", "@nestjs/core", "koa", "hapi", "@hapi/hapi", "restify", "polka", "h3", "hono", "elysia", "@trpc/server", "tsoa", "routing-controllers"],
|
|
8222
|
+
depSegments: ["express", "fastify", "koa", "hapi", "server", "router", "http"],
|
|
8223
|
+
pathPatterns: [/api[/\\]/, /routes?[/\\]/, /controllers?[/\\]/, /endpoints?[/\\]/, /handlers?[/\\]/],
|
|
8224
|
+
passes: [
|
|
8225
|
+
{ id: "api-input-validation", phase: "unit", category: "data-validation", description: "Check API input validation completeness. Look for: req.body/query/params used without schema validation, numeric parameters parsed without NaN/range checks, array inputs without length limits, nested object inputs without depth limits, missing Content-Type enforcement." }
|
|
8226
|
+
]
|
|
8227
|
+
},
|
|
8228
|
+
"graphql": {
|
|
8229
|
+
keywords: ["graphql", "gql", "schema", "resolver", "mutation", "subscription"],
|
|
8230
|
+
knownDeps: ["graphql", "apollo-server", "@apollo/server", "apollo-server-express", "graphql-yoga", "mercurius", "type-graphql", "nexus", "pothos", "@graphql-tools/schema", "graphql-ws"],
|
|
8231
|
+
depSegments: ["graphql", "gql", "apollo"],
|
|
8232
|
+
pathPatterns: [/graphql[/\\]/, /gql[/\\]/, /resolvers?[/\\]/, /mutations?[/\\]/],
|
|
8233
|
+
passes: [
|
|
8234
|
+
{ id: "graphql-depth-limiting", phase: "unit", category: "resource-leak", description: "Check for GraphQL denial of service vectors. Look for: missing query depth limiting, missing query complexity analysis, no limit on batch/alias operations, introspection enabled in production, missing pagination limits on list fields, N+1 query patterns in resolvers without dataloader." },
|
|
8235
|
+
{ id: "graphql-authorization", phase: "integration", category: "auth-bypass", description: "Check GraphQL authorization. Look for: resolvers without auth directives/guards, sensitive fields queryable without permission checks, mutations accessible without proper role verification, subscription channels leaking data across users." }
|
|
8236
|
+
]
|
|
8237
|
+
},
|
|
8238
|
+
"grpc": {
|
|
8239
|
+
keywords: ["grpc", "protobuf", "rpc", "proto", "service definition"],
|
|
8240
|
+
knownDeps: ["@grpc/grpc-js", "@grpc/proto-loader", "grpc", "protobufjs", "ts-proto", "nice-grpc", "@connectrpc/connect"],
|
|
8241
|
+
depSegments: ["grpc", "proto"],
|
|
8242
|
+
pathPatterns: [/grpc[/\\]/, /protos?[/\\]/, /rpc[/\\]/],
|
|
8243
|
+
passes: [
|
|
8244
|
+
{ id: "grpc-validation", phase: "unit", category: "data-validation", description: "Check gRPC input validation. Look for: missing field validation beyond protobuf types (length, range, format), unary calls without deadline/timeout, streaming RPCs without message size limits, missing TLS/mTLS configuration, reflection service enabled in production." }
|
|
8245
|
+
]
|
|
8246
|
+
},
|
|
8247
|
+
"realtime": {
|
|
8248
|
+
keywords: ["realtime", "real-time", "websocket", "socket", "live", "sse", "server-sent events", "long-polling", "pub/sub", "pubsub"],
|
|
8249
|
+
knownDeps: ["socket.io", "ws", "pusher", "ably", "@supabase/realtime-js", "sockjs", "engine.io", "faye", "primus", "@fastify/websocket", "graphql-ws"],
|
|
8250
|
+
depSegments: ["socket", "realtime", "sse", "pubsub", "websocket"],
|
|
8251
|
+
pathPatterns: [/realtime[/\\]/, /websockets?[/\\]/, /sockets?[/\\]/, /live[/\\]/, /events?[/\\]/, /sse[/\\]/],
|
|
8252
|
+
passes: [
|
|
8253
|
+
{ id: "realtime-auth", phase: "integration", category: "auth-bypass", description: "Check realtime connection authentication. Look for: WebSocket/SSE connections accepted without auth verification, missing per-message authorization (auth checked only on connect), missing origin validation on WebSocket upgrade, event subscriptions allowing access to other users channels." },
|
|
8254
|
+
{ id: "event-injection", phase: "unit", category: "injection", description: "Check for event injection in realtime systems. Look for: user-controlled event names passed to emit/broadcast, unvalidated payloads broadcast to other clients, missing input sanitization on WebSocket messages, client able to trigger server-side events by name." }
|
|
8255
|
+
]
|
|
8256
|
+
},
|
|
8257
|
+
"email": {
|
|
8258
|
+
keywords: ["email sending", "smtp", "mailer", "newsletter", "transactional email", "email service", "email template", "send email", "email notification"],
|
|
8259
|
+
knownDeps: ["nodemailer", "@sendgrid/mail", "postmark", "mailgun-js", "@aws-sdk/client-ses", "resend", "react-email", "mjml", "email-templates", "bull-board"],
|
|
8260
|
+
depSegments: ["mail", "email", "smtp", "sendgrid", "postmark"],
|
|
8261
|
+
pathPatterns: [/emails?[/\\]/, /mailers?[/\\]/, /newsletters?[/\\]/],
|
|
8262
|
+
passes: [
|
|
8263
|
+
{ id: "email-header-injection", phase: "unit", category: "injection", description: "Check for email header injection. Look for: user input in email headers (To, From, CC, BCC, Subject) without newline stripping, template injection in email bodies (SSTI), user-controlled URLs in email templates (phishing), missing SPF/DKIM/DMARC consideration, email content including unsanitized user data." }
|
|
8264
|
+
]
|
|
8265
|
+
},
|
|
8266
|
+
"webhook": {
|
|
8267
|
+
keywords: ["webhook", "callback url", "event notification", "http callback"],
|
|
8268
|
+
knownDeps: ["svix", "@octokit/webhooks", "stripe"],
|
|
8269
|
+
depSegments: ["webhook", "callback"],
|
|
8270
|
+
pathPatterns: [/webhooks?[/\\]/, /callbacks?[/\\]/],
|
|
8271
|
+
passes: [
|
|
8272
|
+
{ id: "webhook-verification", phase: "integration", category: "auth-bypass", description: "Check webhook security. Look for: incoming webhooks without signature verification (HMAC, asymmetric), webhook endpoints without IP allowlisting or authentication, missing replay protection (no timestamp validation), SSRF via outgoing webhook URLs controlled by users, webhook payloads processed without validation." }
|
|
8273
|
+
]
|
|
8274
|
+
},
|
|
8275
|
+
"queue": {
|
|
8276
|
+
keywords: ["queue", "job", "worker", "background job", "task queue", "message broker", "consumer", "producer"],
|
|
8277
|
+
knownDeps: ["bull", "bullmq", "amqplib", "kafkajs", "@aws-sdk/client-sqs", "bee-queue", "agenda", "pg-boss", "faktory-worker", "celery-node", "rsmq"],
|
|
8278
|
+
depSegments: ["queue", "worker", "amqp", "kafka", "sqs", "rabbit", "bull"],
|
|
8279
|
+
pathPatterns: [/queues?[/\\]/, /jobs?[/\\]/, /workers?[/\\]/, /consumers?[/\\]/, /producers?[/\\]/],
|
|
8280
|
+
passes: [
|
|
8281
|
+
{ id: "queue-message-safety", phase: "unit", category: "injection", description: "Check queue message safety. Look for: deserialization of untrusted queue messages without schema validation, job data used in shell commands or SQL queries without sanitization, missing dead-letter queue handling, job retry without idempotency, user-controlled job types or queue names enabling unauthorized actions." }
|
|
8282
|
+
]
|
|
8283
|
+
},
|
|
8284
|
+
"notification": {
|
|
8285
|
+
keywords: ["notification", "push notification", "alert", "fcm", "apns"],
|
|
8286
|
+
knownDeps: ["firebase-admin", "web-push", "@aws-sdk/client-sns", "onesignal-node", "expo-server-sdk", "apn", "node-pushnotifications"],
|
|
8287
|
+
depSegments: ["notification", "push", "fcm", "apns", "sns"],
|
|
8288
|
+
pathPatterns: [/notifications?[/\\]/, /push[/\\]/, /alerts?[/\\]/],
|
|
8289
|
+
passes: [
|
|
8290
|
+
{ id: "notification-auth", phase: "integration", category: "auth-bypass", description: "Check notification subscription authorization. Look for: push notification subscriptions without user authentication, ability to subscribe to other users notification channels, notification payloads containing sensitive data sent to unverified devices, missing subscription ownership validation on unsubscribe." }
|
|
8291
|
+
]
|
|
8292
|
+
},
|
|
8293
|
+
// ═══════════════════════════════════════════════════════════
|
|
8294
|
+
// SECURITY INFRASTRUCTURE
|
|
8295
|
+
// ═══════════════════════════════════════════════════════════
|
|
8296
|
+
"crypto": {
|
|
8297
|
+
keywords: ["encrypt", "decrypt", "cipher", "hash", "hmac", "signing", "signature", "key management", "kms", "vault"],
|
|
8298
|
+
knownDeps: ["crypto-js", "tweetnacl", "libsodium-wrappers", "@aws-sdk/client-kms", "@google-cloud/kms", "node-forge", "openpgp", "sodium-native", "jose"],
|
|
8299
|
+
depSegments: ["crypto", "cipher", "encrypt", "hash", "kms", "vault", "sodium", "nacl"],
|
|
8300
|
+
pathPatterns: [/crypto[/\\]/, /encryption[/\\]/, /keys?[/\\]/, /vault[/\\]/, /certs?[/\\]/],
|
|
8301
|
+
passes: [
|
|
8302
|
+
{ id: "weak-crypto", phase: "unit", category: "secrets-exposure", description: "Check for weak cryptographic practices. Look for: MD5/SHA1 used for security purposes (password hashing, signatures), ECB mode encryption, static/hardcoded IVs or salts, Math.random() for security-sensitive values, insufficient key lengths (RSA < 2048, AES < 128), deprecated algorithms (DES, 3DES, RC4)." },
|
|
8303
|
+
{ id: "key-management", phase: "integration", category: "secrets-exposure", description: "Check key management practices. Look for: encryption keys hardcoded in source, keys stored in environment variables without KMS wrapping, missing key rotation mechanism, private keys committed to repository, symmetric keys shared across services, keys logged or included in error messages." }
|
|
8304
|
+
]
|
|
8305
|
+
},
|
|
8306
|
+
"cors": {
|
|
8307
|
+
keywords: ["cors", "cross-origin", "origin policy", "access-control-allow"],
|
|
8308
|
+
knownDeps: ["cors", "@fastify/cors", "@koa/cors"],
|
|
8309
|
+
depSegments: ["cors"],
|
|
8310
|
+
pathPatterns: [/cors[/\\]/],
|
|
8311
|
+
passes: [
|
|
8312
|
+
{ id: "cors-misconfiguration", phase: "unit", category: "auth-bypass", description: "Check CORS configuration safety. Look for: Access-Control-Allow-Origin set to wildcard (*) with credentials, origin reflected without allowlist validation, Access-Control-Allow-Methods too permissive, Access-Control-Max-Age too long, missing Vary: Origin header when origin is dynamic, regex-based origin matching with bypasses." }
|
|
8313
|
+
]
|
|
8314
|
+
},
|
|
8315
|
+
"csrf": {
|
|
8316
|
+
keywords: ["csrf", "xsrf", "cross-site request forgery", "anti-forgery", "csrf token"],
|
|
8317
|
+
knownDeps: ["csurf", "csrf-csrf", "lusca", "csrf"],
|
|
8318
|
+
depSegments: ["csrf", "xsrf"],
|
|
8319
|
+
pathPatterns: [/csrf[/\\]/],
|
|
8320
|
+
passes: [
|
|
8321
|
+
{ id: "csrf-protection", phase: "integration", category: "auth-bypass", description: "Check CSRF protection on state-changing endpoints. Look for: POST/PUT/DELETE endpoints without CSRF token validation, CSRF tokens not bound to user session, SameSite cookie attribute not set, token transmitted in URL query parameter (leaked via Referer), missing CSRF protection on logout/password-change/email-change endpoints." }
|
|
8322
|
+
]
|
|
8323
|
+
},
|
|
8324
|
+
"redirect": {
|
|
8325
|
+
keywords: ["redirect", "redirect_uri", "return_url", "next_url", "callback"],
|
|
8326
|
+
knownDeps: [],
|
|
8327
|
+
depSegments: [],
|
|
8328
|
+
pathPatterns: [],
|
|
8329
|
+
passes: [
|
|
8330
|
+
{ id: "open-redirect", phase: "unit", category: "injection", description: "Check for open redirect vulnerabilities. Look for: user-controlled redirect URLs without allowlist validation, URL parsing that can be bypassed (e.g., //evil.com, /\\evil.com, /%09/evil.com), redirect after login using unvalidated return_url parameter, JavaScript protocol in redirect URLs (javascript:)." }
|
|
8331
|
+
]
|
|
8332
|
+
},
|
|
8333
|
+
"rate-limiting": {
|
|
8334
|
+
keywords: ["rate limit", "throttle", "throttling", "rate-limit", "quota", "ddos protection"],
|
|
8335
|
+
knownDeps: ["express-rate-limit", "rate-limiter-flexible", "bottleneck", "@fastify/rate-limit", "limiter", "p-throttle", "p-limit"],
|
|
8336
|
+
depSegments: ["ratelimit", "throttle", "limiter"],
|
|
8337
|
+
pathPatterns: [/rate-limit[/\\]/, /throttl[/\\]/, /limiters?[/\\]/],
|
|
8338
|
+
passes: [
|
|
8339
|
+
{ id: "rate-limit-bypass", phase: "integration", category: "auth-bypass", description: "Check rate limiting effectiveness. Look for: rate limits based on IP only (bypassable via proxies/rotating IPs), missing rate limits on login/password-reset/OTP-verification endpoints, rate limit state in local memory (not shared across instances), X-Forwarded-For header trusted without validation, rate limit key not including API key or user ID." }
|
|
8340
|
+
]
|
|
8341
|
+
},
|
|
8342
|
+
"config": {
|
|
8343
|
+
keywords: ["config", "configuration", "environment", "settings", "env var", "dotenv"],
|
|
8344
|
+
knownDeps: ["dotenv", "config", "convict", "envalid", "@nestjs/config", "env-var", "nconf"],
|
|
8345
|
+
depSegments: ["config", "dotenv", "env"],
|
|
8346
|
+
pathPatterns: [/config[/\\]/, /settings[/\\]/],
|
|
8347
|
+
passes: [
|
|
8348
|
+
{ id: "config-exposure", phase: "unit", category: "secrets-exposure", description: "Check configuration security. Look for: default credentials in config files, debug mode enabled by default, secrets with fallback values in code, config files with overly permissive file permissions, environment-specific configs committed to repository, API keys or database URLs in client-side bundles." }
|
|
8349
|
+
]
|
|
8350
|
+
},
|
|
8351
|
+
"logging": {
|
|
8352
|
+
keywords: ["logging", "logger", "audit trail", "audit log", "access log", "activity log", "structured logging", "log aggregation"],
|
|
8353
|
+
knownDeps: ["winston", "pino", "bunyan", "morgan", "log4js", "signale", "tslog", "roarr", "loglevel"],
|
|
8354
|
+
depSegments: ["log", "logger", "logging", "audit"],
|
|
8355
|
+
pathPatterns: [/loggers?[/\\]/, /logging[/\\]/, /audit[/\\]/],
|
|
8356
|
+
passes: [
|
|
8357
|
+
{ id: "log-injection", phase: "unit", category: "injection", description: "Check for log injection and data leakage. Look for: user input written to logs without sanitization (newline injection for log forging), PII/passwords/tokens/credit-card numbers in log output, stack traces with sensitive info logged in production, ANSI escape sequences in log output (terminal injection)." }
|
|
8358
|
+
]
|
|
8359
|
+
},
|
|
8360
|
+
// ═══════════════════════════════════════════════════════════
|
|
8361
|
+
// CONTENT & FILES
|
|
8362
|
+
// ═══════════════════════════════════════════════════════════
|
|
8363
|
+
"file-upload": {
|
|
8364
|
+
keywords: ["upload", "file upload", "multipart", "attachment", "media upload", "image upload", "document upload"],
|
|
8365
|
+
knownDeps: ["multer", "busboy", "formidable", "multiparty", "@fastify/multipart", "express-fileupload"],
|
|
8366
|
+
depSegments: ["upload", "multipart"],
|
|
8367
|
+
pathPatterns: [/uploads?[/\\]/, /attachments?[/\\]/, /media[/\\]/],
|
|
8368
|
+
passes: [
|
|
8369
|
+
{ id: "upload-safety", phase: "unit", category: "injection", description: "Check file upload safety. Look for: file type validation using extension only (not magic bytes/MIME), no file size limits, uploaded files stored with user-controlled names (path traversal), executable files accepted (.php, .jsp, .aspx, .sh), no antivirus/malware scanning, files served from same origin as application." },
|
|
8370
|
+
{ id: "path-traversal-upload", phase: "integration", category: "injection", description: "Check for path traversal in file operations. Look for: user-controlled file paths passed to fs.readFile/writeFile/unlink, directory traversal via ../.. in filenames, symlink following in upload/download directories, zip slip in archive extraction, file path construction from user input without canonicalization." }
|
|
8371
|
+
]
|
|
8372
|
+
},
|
|
8373
|
+
"templating": {
|
|
8374
|
+
keywords: ["template", "render", "view engine", "handlebars", "mustache", "ejs", "pug", "jinja", "server-side rendering"],
|
|
8375
|
+
knownDeps: ["ejs", "pug", "handlebars", "mustache", "nunjucks", "eta", "liquidjs", "marko", "hbs", "consolidate"],
|
|
8376
|
+
depSegments: ["template", "ejs", "pug", "handlebars", "mustache", "nunjucks"],
|
|
8377
|
+
pathPatterns: [/templates?[/\\]/, /views?[/\\]/, /layouts?[/\\]/, /partials?[/\\]/],
|
|
8378
|
+
passes: [
|
|
8379
|
+
{ id: "ssti-check", phase: "unit", category: "injection", description: "Check for Server-Side Template Injection (SSTI). Look for: user input directly embedded in template strings before rendering, template compilation with user-controlled template source, sandbox escape in template engines, client-side template injection in Angular/Vue, eval-like constructs in template expressions." }
|
|
8380
|
+
]
|
|
8381
|
+
},
|
|
8382
|
+
"xml": {
|
|
8383
|
+
keywords: ["xml", "soap", "wsdl", "xslt", "xpath", "rss", "atom", "svg"],
|
|
8384
|
+
knownDeps: ["fast-xml-parser", "xml2js", "xmlbuilder", "xmldom", "libxmljs", "soap", "xpath", "sax", "cheerio"],
|
|
8385
|
+
depSegments: ["xml", "soap", "xslt", "xpath"],
|
|
8386
|
+
pathPatterns: [/xml[/\\]/, /soap[/\\]/],
|
|
8387
|
+
passes: [
|
|
8388
|
+
{ id: "xxe-check", phase: "unit", category: "injection", description: "Check for XML External Entity (XXE) injection. Look for: XML parsers with external entity processing enabled, DTD processing not disabled, XSLT processing of user-supplied stylesheets, XPath queries with user input, billion laughs/entity expansion not limited, SVG files parsed without sanitization." }
|
|
8389
|
+
]
|
|
8390
|
+
},
|
|
8391
|
+
"pdf-generation": {
|
|
8392
|
+
keywords: ["pdf", "document generation", "report generation", "invoice generation", "receipt"],
|
|
8393
|
+
knownDeps: ["puppeteer", "pdfkit", "jspdf", "pdfmake", "html-pdf", "wkhtmltopdf", "@react-pdf/renderer", "pdf-lib", "chromiumly"],
|
|
8394
|
+
depSegments: ["pdf", "pdfkit", "wkhtml"],
|
|
8395
|
+
pathPatterns: [/pdf[/\\]/, /reports?[/\\]/, /documents?[/\\]/],
|
|
8396
|
+
passes: [
|
|
8397
|
+
{ id: "pdf-ssrf", phase: "integration", category: "injection", description: "Check for SSRF via PDF generation. Look for: HTML-to-PDF converters rendering user-supplied HTML (can access internal URLs via <img>, <link>, <iframe>), user-controlled URLs in PDF generation (fetch internal resources), CSS @import/@font-face with user-controlled URLs, JavaScript execution in headless browser PDF generation." }
|
|
8398
|
+
]
|
|
8399
|
+
},
|
|
8400
|
+
"image-processing": {
|
|
8401
|
+
keywords: ["image processing", "image resize", "thumbnail", "image manipulation", "image conversion"],
|
|
8402
|
+
knownDeps: ["sharp", "jimp", "gm", "imagemagick", "image-size", "@squoosh/lib", "canvas", "pica", "blurhash"],
|
|
8403
|
+
depSegments: ["image", "sharp", "jimp", "gm", "imagemagick", "canvas", "thumbnail"],
|
|
8404
|
+
pathPatterns: [/images?[/\\]/, /thumbnails?[/\\]/, /processing[/\\]/],
|
|
8405
|
+
passes: [
|
|
8406
|
+
{ id: "image-processing-safety", phase: "unit", category: "injection", description: "Check image processing safety. Look for: ImageMagick/GraphicsMagick command injection via filenames, SVG processing enabling script execution, image bombs (decompression bombs) without size/dimension limits, EXIF data containing scripts not stripped, SSRF via image URL fetching, image processing of untrusted formats without sandboxing." }
|
|
8407
|
+
]
|
|
8408
|
+
},
|
|
8409
|
+
"cms": {
|
|
8410
|
+
keywords: ["cms", "content management", "headless cms", "blog", "article", "editorial"],
|
|
8411
|
+
knownDeps: ["strapi", "@strapi/strapi", "ghost", "keystone", "@keystone-6/core", "sanity", "contentful", "directus", "payload"],
|
|
8412
|
+
depSegments: ["cms", "strapi", "ghost", "keystone", "contentful", "sanity"],
|
|
8413
|
+
pathPatterns: [/cms[/\\]/, /content[/\\]/, /articles?[/\\]/, /posts?[/\\]/, /editorial[/\\]/],
|
|
8414
|
+
passes: [
|
|
8415
|
+
{ id: "cms-xss", phase: "unit", category: "injection", description: "Check for XSS in CMS content rendering. Look for: rich text/markdown rendered without sanitization (dangerouslySetInnerHTML, v-html), user-generated content displayed without HTML encoding, custom embed codes executed without sandboxing, WYSIWYG editor output rendered raw, missing Content-Security-Policy for user content pages." }
|
|
8416
|
+
]
|
|
8417
|
+
},
|
|
8418
|
+
// ═══════════════════════════════════════════════════════════
|
|
8419
|
+
// BUSINESS LOGIC
|
|
8420
|
+
// ═══════════════════════════════════════════════════════════
|
|
8421
|
+
"payments": {
|
|
8422
|
+
keywords: ["payment", "checkout", "billing", "charge", "refund", "invoice", "transaction", "payout", "settlement"],
|
|
8423
|
+
knownDeps: ["stripe", "@stripe/stripe-js", "@stripe/react-stripe-js", "braintree", "paypal-rest-sdk", "@paypal/checkout-server-sdk", "square", "adyen-api", "razorpay", "mollie-api-node", "coinbase-commerce-node", "paddle-sdk"],
|
|
8424
|
+
depSegments: ["payment", "pay", "billing", "checkout", "stripe", "braintree", "paypal", "razorpay"],
|
|
8425
|
+
pathPatterns: [/payments?[/\\]/, /billing[/\\]/, /checkout[/\\]/, /transactions?[/\\]/, /invoices?[/\\]/],
|
|
8426
|
+
passes: [
|
|
8427
|
+
{ id: "decimal-precision", phase: "unit", category: "logic-error", description: "Check for floating-point arithmetic on monetary values. Look for: Number type for prices/amounts (IEEE 754 precision loss), Math.round on currency calculations, division before multiplication causing rounding errors, missing use of integer cents or Decimal/BigNumber libraries, currency conversion with floating-point." },
|
|
8428
|
+
{ id: "idempotency-check", phase: "integration", category: "logic-error", description: "Verify payment endpoints use idempotency keys. Look for: POST endpoints that charge money without idempotency key validation, missing duplicate payment detection, retry logic that could double-charge, webhook handlers that reprocess already-completed payments." },
|
|
8429
|
+
{ id: "race-condition-financial", phase: "e2e", category: "concurrency", description: "Find race conditions in financial operations. Look for: balance check-then-debit without database transactions/locks, concurrent transfers that could overdraw, non-atomic read-modify-write on account balances, coupon/discount applied multiple times concurrently, parallel refund requests for same order." }
|
|
8430
|
+
]
|
|
8431
|
+
},
|
|
8432
|
+
"e-commerce": {
|
|
8433
|
+
keywords: ["e-commerce", "ecommerce", "shopping cart", "catalog", "product", "inventory", "order", "storefront", "marketplace"],
|
|
8434
|
+
knownDeps: ["shopify-api-node", "@shopify/shopify-api", "medusa-core", "saleor", "commercetools"],
|
|
8435
|
+
depSegments: ["shop", "commerce", "cart", "catalog", "inventory", "storefront"],
|
|
8436
|
+
pathPatterns: [/shop[/\\]/, /cart[/\\]/, /products?[/\\]/, /orders?[/\\]/, /catalog[/\\]/, /inventory[/\\]/],
|
|
8437
|
+
passes: [
|
|
8438
|
+
{ id: "price-manipulation", phase: "integration", category: "logic-error", description: "Check for price manipulation vulnerabilities. Look for: product price sent from client and used directly (not re-fetched from server), discount/coupon amount validated client-side only, total calculated on frontend and trusted by backend, negative quantity or price accepted, race condition between price check and order placement." },
|
|
8439
|
+
{ id: "inventory-race", phase: "e2e", category: "concurrency", description: "Check for inventory race conditions. Look for: stock check-then-decrement without atomic operation, concurrent purchases of last item in stock, overselling via parallel API requests, missing reservation/hold mechanism during checkout flow." }
|
|
8440
|
+
]
|
|
8441
|
+
},
|
|
8442
|
+
"subscription": {
|
|
8443
|
+
keywords: ["subscription", "recurring", "plan", "tier", "billing cycle", "trial", "upgrade", "downgrade"],
|
|
8444
|
+
knownDeps: [],
|
|
8445
|
+
depSegments: ["subscription", "billing", "plan"],
|
|
8446
|
+
pathPatterns: [/subscriptions?[/\\]/, /plans?[/\\]/, /tiers?[/\\]/],
|
|
8447
|
+
passes: [
|
|
8448
|
+
{ id: "subscription-bypass", phase: "integration", category: "auth-bypass", description: "Check for subscription/plan bypass. Look for: premium features accessible without active subscription check, plan limits enforced client-side only, trial period manipulation (re-registration), downgrade not revoking access to higher-tier features, subscription status cached and not re-verified." }
|
|
8449
|
+
]
|
|
8450
|
+
},
|
|
8451
|
+
"workflow": {
|
|
8452
|
+
keywords: ["workflow", "state machine", "pipeline", "approval", "process", "step", "transition"],
|
|
8453
|
+
knownDeps: ["xstate", "bull", "temporal-sdk", "@temporalio/client", "inngest", "trigger.dev"],
|
|
8454
|
+
depSegments: ["workflow", "state-machine", "pipeline", "xstate"],
|
|
8455
|
+
pathPatterns: [/workflows?[/\\]/, /pipelines?[/\\]/, /state-machines?[/\\]/, /processes?[/\\]/],
|
|
8456
|
+
passes: [
|
|
8457
|
+
{ id: "state-machine-bypass", phase: "integration", category: "logic-error", description: "Check for state machine/workflow bypass. Look for: direct API access to skip workflow steps (e.g., complete order without payment), missing validation of state transitions (can go from any state to any state), authorization not checked on each transition, concurrent transitions causing invalid states, rollback not cleaning up side effects." }
|
|
8458
|
+
]
|
|
8459
|
+
},
|
|
8460
|
+
"feature-flags": {
|
|
8461
|
+
keywords: ["feature flag", "feature toggle", "feature gate", "experiment", "a/b test", "canary", "rollout"],
|
|
8462
|
+
knownDeps: ["launchdarkly-node-server-sdk", "@unleash/proxy-client-react", "flagsmith", "growthbook", "@growthbook/growthbook", "posthog-node", "@happykit/flags", "flipt-node"],
|
|
8463
|
+
depSegments: ["feature", "flag", "toggle", "experiment"],
|
|
8464
|
+
pathPatterns: [/feature-flags?[/\\]/, /flags?[/\\]/, /toggles?[/\\]/, /experiments?[/\\]/],
|
|
8465
|
+
passes: [
|
|
8466
|
+
{ id: "feature-flag-bypass", phase: "unit", category: "auth-bypass", description: "Check for feature flag bypass. Look for: flag values settable via query parameters or headers, flag evaluation using client-supplied user context without validation, admin features hidden behind flags but not behind auth, flag configuration exposed to client, default flag values granting access." }
|
|
8467
|
+
]
|
|
8468
|
+
},
|
|
8469
|
+
// ═══════════════════════════════════════════════════════════
|
|
8470
|
+
// DATA MOVEMENT
|
|
8471
|
+
// ═══════════════════════════════════════════════════════════
|
|
8472
|
+
"export": {
|
|
8473
|
+
keywords: ["export", "download", "csv export", "excel", "spreadsheet", "report export", "data dump"],
|
|
8474
|
+
knownDeps: ["exceljs", "xlsx", "json2csv", "csv-writer", "papaparse", "archiver", "jszip"],
|
|
8475
|
+
depSegments: ["export", "csv", "excel", "xlsx"],
|
|
8476
|
+
pathPatterns: [/exports?[/\\]/, /downloads?[/\\]/],
|
|
8477
|
+
passes: [
|
|
8478
|
+
{ id: "export-injection", phase: "unit", category: "injection", description: "Check for injection in data exports. Look for: CSV injection via formulas (=CMD(), +cmd, @SUM) in cell values, Excel macro injection, user data included in exports without sanitization, export endpoints without authorization (data exfiltration), large export without pagination causing DoS, export containing more data than user should access." }
|
|
8479
|
+
]
|
|
8480
|
+
},
|
|
8481
|
+
"import": {
|
|
8482
|
+
keywords: ["import", "upload csv", "bulk import", "data import", "ingest", "data migration"],
|
|
8483
|
+
knownDeps: ["csv-parse", "papaparse", "xlsx", "exceljs", "multer", "fast-csv"],
|
|
8484
|
+
depSegments: ["import", "ingest", "csv-parse"],
|
|
8485
|
+
pathPatterns: [/imports?[/\\]/, /ingest[/\\]/, /migrations?[/\\]/],
|
|
8486
|
+
passes: [
|
|
8487
|
+
{ id: "import-safety", phase: "unit", category: "injection", description: "Check import/ingestion safety. Look for: CSV/Excel parsing without row limits (DoS via huge files), imported data used in queries without sanitization, missing validation of imported data format/schema, zip bomb in archive imports, imported file paths used in file operations (path traversal), deserialization of imported objects." }
|
|
8488
|
+
]
|
|
8489
|
+
},
|
|
8490
|
+
// ═══════════════════════════════════════════════════════════
|
|
8491
|
+
// ADVANCED
|
|
8492
|
+
// ═══════════════════════════════════════════════════════════
|
|
8493
|
+
"ml-ai": {
|
|
8494
|
+
keywords: ["machine learning", "artificial intelligence", "llm", "ai model", "ai agent", "inference", "prediction", "embedding", "vector database", "prompt engineering", "chatbot", "rag pipeline", "large language model", "generative ai"],
|
|
8495
|
+
knownDeps: ["@anthropic-ai/sdk", "openai", "@google/generative-ai", "langchain", "@langchain/core", "tensorflow", "@tensorflow/tfjs", "onnxruntime-node", "transformers", "pinecone", "@pinecone-database/pinecone", "chromadb", "weaviate-ts-client", "replicate", "cohere-ai", "ai"],
|
|
8496
|
+
depSegments: ["ml", "ai", "llm", "openai", "anthropic", "langchain", "vector", "embedding", "tensorflow", "onnx"],
|
|
8497
|
+
pathPatterns: [/ml[/\\]/, /\bai[/\\]/, /inference[/\\]/, /embeddings?[/\\]/, /llm[/\\]/, /rag[/\\]/],
|
|
8498
|
+
passes: [
|
|
8499
|
+
{ id: "prompt-injection", phase: "unit", category: "injection", description: "Check for prompt injection in LLM/AI features. Look for: user input concatenated directly into LLM prompts without sanitization, missing system prompt boundaries, tool/function calls executed without user confirmation, model outputs used in SQL/command/template rendering without validation, embedding search returning unauthorized content, RAG context leaking across users." },
|
|
8500
|
+
{ id: "ai-output-safety", phase: "integration", category: "injection", description: "Check AI output safety. Look for: LLM responses rendered as HTML without sanitization (XSS), model output used in database queries, AI-generated code executed without sandboxing, model hallucinations used for access control decisions, PII from training data leaked in responses, unlimited token generation (cost/DoS)." }
|
|
8501
|
+
]
|
|
8502
|
+
},
|
|
8503
|
+
"proxy": {
|
|
8504
|
+
keywords: ["proxy", "reverse proxy", "api gateway", "load balancer", "ingress", "edge"],
|
|
8505
|
+
knownDeps: ["http-proxy", "http-proxy-middleware", "express-http-proxy", "@fastify/http-proxy", "redbird", "node-http-proxy"],
|
|
8506
|
+
depSegments: ["proxy", "gateway"],
|
|
8507
|
+
pathPatterns: [/proxy[/\\]/, /gateway[/\\]/],
|
|
8508
|
+
passes: [
|
|
8509
|
+
{ id: "proxy-ssrf", phase: "integration", category: "injection", description: "Check for SSRF via proxy functionality. Look for: user-controlled URLs passed to proxy/fetch without allowlist, internal service URLs accessible via proxy endpoint, DNS rebinding attacks via proxy, HTTP header injection in proxied requests (CRLF), request smuggling via inconsistent parsing between proxy and backend." }
|
|
8510
|
+
]
|
|
8511
|
+
},
|
|
8512
|
+
"cron": {
|
|
8513
|
+
keywords: ["cron", "scheduled", "scheduler", "periodic", "timer", "recurring task", "background task"],
|
|
8514
|
+
knownDeps: ["node-cron", "cron", "node-schedule", "agenda", "bree", "croner", "toad-scheduler"],
|
|
8515
|
+
depSegments: ["cron", "scheduler", "schedule"],
|
|
8516
|
+
pathPatterns: [/cron[/\\]/, /schedulers?[/\\]/, /scheduled[/\\]/, /tasks?[/\\]/],
|
|
8517
|
+
passes: [
|
|
8518
|
+
{ id: "cron-safety", phase: "unit", category: "injection", description: "Check scheduled task safety. Look for: cron job commands constructed from user input, scheduled tasks running with elevated privileges unnecessarily, cron schedule modifiable by non-admin users, missing locking for distributed cron (duplicate execution), long-running cron tasks without timeout, cron jobs accessing stale credentials/tokens." }
|
|
8519
|
+
]
|
|
8520
|
+
},
|
|
8521
|
+
"admin": {
|
|
8522
|
+
keywords: ["admin", "backoffice", "back-office", "dashboard", "management panel", "control panel", "superuser"],
|
|
8523
|
+
knownDeps: ["adminjs", "@adminjs/express", "react-admin", "ra-data-simple-rest"],
|
|
8524
|
+
depSegments: ["admin", "backoffice", "dashboard"],
|
|
8525
|
+
pathPatterns: [/admin[/\\]/, /backoffice[/\\]/, /dashboard[/\\]/, /panel[/\\]/],
|
|
8526
|
+
passes: [
|
|
8527
|
+
{ id: "privilege-boundary", phase: "integration", category: "auth-bypass", description: "Verify admin privilege boundaries. Look for: admin endpoints accessible without role verification, admin actions not re-verified at service layer (only checked at route level), internal admin APIs callable from public network, admin impersonation feature without audit logging, admin tools exposing raw database queries." }
|
|
8528
|
+
]
|
|
8529
|
+
},
|
|
8530
|
+
"analytics": {
|
|
8531
|
+
keywords: ["analytics", "tracking", "metrics", "telemetry", "usage", "event tracking"],
|
|
8532
|
+
knownDeps: ["mixpanel", "segment", "@segment/analytics-node", "posthog-js", "posthog-node", "amplitude-js", "@amplitude/analytics-node", "plausible-tracker", "umami"],
|
|
8533
|
+
depSegments: ["analytics", "tracking", "metrics", "telemetry"],
|
|
8534
|
+
pathPatterns: [/analytics[/\\]/, /tracking[/\\]/, /metrics[/\\]/, /telemetry[/\\]/],
|
|
8535
|
+
passes: [
|
|
8536
|
+
{ id: "analytics-pii", phase: "unit", category: "secrets-exposure", description: "Check for PII in analytics tracking. Look for: user email/name/IP sent to analytics services, session tokens or auth headers included in tracking events, form input values tracked without sanitization, URL parameters containing sensitive data sent to analytics, analytics scripts loading third-party code without integrity checks." }
|
|
8537
|
+
]
|
|
8538
|
+
}
|
|
8539
|
+
};
|
|
8540
|
+
var CRITICAL_DOMAINS = /* @__PURE__ */ new Set([
|
|
8541
|
+
"auth",
|
|
8542
|
+
"session",
|
|
8543
|
+
"oauth",
|
|
8544
|
+
"access-control",
|
|
8545
|
+
"mfa",
|
|
8546
|
+
"payments",
|
|
8547
|
+
"crypto",
|
|
8548
|
+
"multi-tenancy"
|
|
8549
|
+
]);
|
|
8550
|
+
var SKIP_RULES = [
|
|
8551
|
+
{
|
|
8552
|
+
passName: "secrets-exposure",
|
|
8553
|
+
condition: (ctx) => {
|
|
8554
|
+
const hasEnvFiles = ctx.projectFiles.some((f) => /\.env($|\.)/.test(f));
|
|
8555
|
+
const hasConfigFiles = ctx.projectFiles.some((f) => /config\.(json|yml|yaml|toml)$/.test(f));
|
|
8556
|
+
return !hasEnvFiles && !hasConfigFiles;
|
|
8557
|
+
},
|
|
8558
|
+
reason: "No env files or config files detected in project"
|
|
8559
|
+
},
|
|
8560
|
+
{
|
|
8561
|
+
passName: "auth-bypass",
|
|
8562
|
+
condition: (ctx) => {
|
|
8563
|
+
return !hasAnyDomain(ctx.domains, ["auth", "session", "oauth", "access-control", "mfa"]);
|
|
8564
|
+
},
|
|
8565
|
+
reason: "No auth/session/access-control domain detected"
|
|
8566
|
+
},
|
|
8567
|
+
{
|
|
8568
|
+
passName: "auth-flow-trace",
|
|
8569
|
+
condition: (ctx) => {
|
|
8570
|
+
return !hasAnyDomain(ctx.domains, ["auth", "session", "oauth", "access-control", "mfa"]);
|
|
8571
|
+
},
|
|
8572
|
+
reason: "No auth/session/access-control domain detected"
|
|
8573
|
+
},
|
|
8574
|
+
{
|
|
8575
|
+
passName: "session-lifecycle-trace",
|
|
8576
|
+
condition: (ctx) => {
|
|
8577
|
+
return !hasAnyDomain(ctx.domains, ["session", "auth", "oauth"]);
|
|
8578
|
+
},
|
|
8579
|
+
reason: "No session/auth/oauth domain detected"
|
|
8580
|
+
},
|
|
8581
|
+
{
|
|
8582
|
+
passName: "api-contract-verification",
|
|
8583
|
+
condition: (ctx) => {
|
|
8584
|
+
const hasOpenApi = ctx.projectFiles.some(
|
|
8585
|
+
(f) => /openapi\.(json|yml|yaml)$/.test(f) || /swagger\.(json|yml|yaml)$/.test(f) || /api-spec\.(json|yml|yaml)$/.test(f)
|
|
8586
|
+
);
|
|
8587
|
+
return !hasOpenApi;
|
|
8588
|
+
},
|
|
8589
|
+
reason: "No OpenAPI/Swagger specification files detected"
|
|
8590
|
+
}
|
|
8591
|
+
];
|
|
8592
|
+
function hasAnyDomain(detected, needed) {
|
|
8593
|
+
return needed.some((d) => detected.includes(d));
|
|
8594
|
+
}
|
|
8595
|
+
var SENSITIVE_DATA_PATTERNS = [
|
|
8596
|
+
{ type: "PII", keywords: ["email", "phone", "address", "name", "ssn", "social security", "date of birth", "national id"], domainIndicators: ["analytics"] },
|
|
8597
|
+
{ type: "financial", keywords: ["credit card", "bank", "account number", "routing", "payment", "transaction", "balance", "billing"], domainIndicators: ["payments", "e-commerce", "subscription"] },
|
|
8598
|
+
{ type: "health", keywords: ["medical", "health", "patient", "diagnosis", "prescription", "hipaa", "ehr", "clinical"], domainIndicators: [] },
|
|
8599
|
+
{ type: "credentials", keywords: ["password", "api key", "secret key", "credential", "access key", "private key"], domainIndicators: ["auth", "crypto", "config"] },
|
|
8600
|
+
{ type: "authentication-tokens", keywords: ["token", "jwt", "session id", "bearer", "refresh token", "oauth token"], domainIndicators: ["session", "oauth"] },
|
|
8601
|
+
{ type: "location", keywords: ["gps", "latitude", "longitude", "geolocation", "location tracking", "coordinates"], domainIndicators: [] },
|
|
8602
|
+
{ type: "biometric", keywords: ["fingerprint", "face id", "biometric", "retina", "voice print"], domainIndicators: ["mfa"] },
|
|
8603
|
+
{ type: "legal", keywords: ["gdpr", "ccpa", "compliance", "consent", "data retention", "right to deletion", "dpa"], domainIndicators: [] }
|
|
8604
|
+
];
|
|
8605
|
+
function generateRiskProfile(understanding, projectFiles, packageJsonDeps) {
|
|
8606
|
+
const deps = packageJsonDeps || understanding.dependencies || {};
|
|
8607
|
+
const descriptionLower = understanding.summary.description.toLowerCase();
|
|
8608
|
+
const featureNames = understanding.features.map((f) => f.name.toLowerCase());
|
|
8609
|
+
const featureDescriptions = understanding.features.map((f) => f.description.toLowerCase());
|
|
8610
|
+
const relatedFiles = understanding.features.flatMap((f) => f.relatedFiles || []);
|
|
8611
|
+
const domains = detectDomains(descriptionLower, featureNames, featureDescriptions, deps, projectFiles, relatedFiles);
|
|
8612
|
+
const sensitiveDataTypes = detectSensitiveData(descriptionLower, featureDescriptions, domains);
|
|
8613
|
+
const externalDependencies = Object.keys(deps).filter(
|
|
8614
|
+
(dep) => !dep.startsWith("@types/") && !dep.startsWith("eslint") && !dep.startsWith("vitest") && !dep.startsWith("typescript") && !dep.startsWith("prettier")
|
|
8615
|
+
);
|
|
8616
|
+
const hotPaths = detectHotPaths(projectFiles, relatedFiles, domains);
|
|
8617
|
+
const customPasses = generateCustomPasses(domains);
|
|
8618
|
+
const skippedPasses = determineSkippedPasses({
|
|
8619
|
+
domains,
|
|
8620
|
+
projectFiles,
|
|
8621
|
+
deps,
|
|
8622
|
+
understanding
|
|
8623
|
+
});
|
|
8624
|
+
return {
|
|
8625
|
+
version: "1",
|
|
8626
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8627
|
+
domains,
|
|
8628
|
+
sensitiveDataTypes,
|
|
8629
|
+
externalDependencies,
|
|
8630
|
+
hotPaths,
|
|
8631
|
+
customPasses,
|
|
8632
|
+
skippedPasses
|
|
8633
|
+
};
|
|
8634
|
+
}
|
|
8635
|
+
function matchesKeyword(text2, keyword) {
|
|
8636
|
+
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8637
|
+
if (keyword.length <= 3) {
|
|
8638
|
+
return new RegExp(`\\b${escaped}\\b`, "i").test(text2);
|
|
8639
|
+
}
|
|
8640
|
+
return new RegExp(`\\b${escaped}`, "i").test(text2);
|
|
8641
|
+
}
|
|
8642
|
+
function depNameSegments(depName) {
|
|
8643
|
+
return depName.replace(/^@/, "").split(/[-/._]/).filter((s) => s.length > 1).map((s) => s.toLowerCase());
|
|
8644
|
+
}
|
|
8645
|
+
function detectDomains(descriptionLower, featureNames, featureDescriptions, deps, projectFiles, relatedFiles) {
|
|
8646
|
+
const detected = /* @__PURE__ */ new Set();
|
|
8647
|
+
const depNames = Object.keys(deps);
|
|
8648
|
+
const allDepSegments = depNames.flatMap((d) => depNameSegments(d));
|
|
8649
|
+
const allFeatureText = [...featureNames, ...featureDescriptions];
|
|
8650
|
+
const allFiles = [...projectFiles, ...relatedFiles];
|
|
8651
|
+
for (const [domain, def] of Object.entries(DOMAINS)) {
|
|
8652
|
+
if (def.keywords.some((kw) => matchesKeyword(descriptionLower, kw))) {
|
|
8653
|
+
detected.add(domain);
|
|
8654
|
+
continue;
|
|
8655
|
+
}
|
|
8656
|
+
if (def.keywords.some((kw) => allFeatureText.some((ft) => matchesKeyword(ft, kw)))) {
|
|
8657
|
+
detected.add(domain);
|
|
8658
|
+
continue;
|
|
8659
|
+
}
|
|
8660
|
+
if (def.knownDeps.some((dep) => depNames.includes(dep))) {
|
|
8661
|
+
detected.add(domain);
|
|
8662
|
+
continue;
|
|
8663
|
+
}
|
|
8664
|
+
if (def.depSegments.length > 0 && def.depSegments.some((seg) => allDepSegments.includes(seg))) {
|
|
8665
|
+
detected.add(domain);
|
|
8666
|
+
continue;
|
|
8667
|
+
}
|
|
8668
|
+
if (def.pathPatterns.length > 0 && def.pathPatterns.some((pattern) => allFiles.some((f) => pattern.test(f)))) {
|
|
8669
|
+
detected.add(domain);
|
|
8670
|
+
continue;
|
|
8671
|
+
}
|
|
8672
|
+
}
|
|
8673
|
+
return Array.from(detected).sort();
|
|
8674
|
+
}
|
|
8675
|
+
function detectSensitiveData(descriptionLower, featureDescriptions, domains) {
|
|
8676
|
+
const detected = /* @__PURE__ */ new Set();
|
|
8677
|
+
const allText = [descriptionLower, ...featureDescriptions].join(" ");
|
|
8678
|
+
for (const pattern of SENSITIVE_DATA_PATTERNS) {
|
|
8679
|
+
if (pattern.keywords.some((kw) => matchesKeyword(allText, kw))) {
|
|
8680
|
+
detected.add(pattern.type);
|
|
8681
|
+
}
|
|
8682
|
+
if (pattern.domainIndicators.some((d) => domains.includes(d))) {
|
|
8683
|
+
detected.add(pattern.type);
|
|
8684
|
+
}
|
|
8685
|
+
}
|
|
8686
|
+
return Array.from(detected).sort();
|
|
8687
|
+
}
|
|
8688
|
+
function detectHotPaths(projectFiles, relatedFiles, domains) {
|
|
8689
|
+
const hotPaths = [];
|
|
8690
|
+
const allFiles = [.../* @__PURE__ */ new Set([...projectFiles, ...relatedFiles])];
|
|
8691
|
+
for (const file of allFiles) {
|
|
8692
|
+
const touchedDomains = [];
|
|
8693
|
+
for (const [domain, def] of Object.entries(DOMAINS)) {
|
|
8694
|
+
if (!domains.includes(domain)) continue;
|
|
8695
|
+
if (def.pathPatterns.length === 0) continue;
|
|
8696
|
+
if (def.pathPatterns.some((pattern) => pattern.test(file))) {
|
|
8697
|
+
touchedDomains.push(domain);
|
|
8698
|
+
}
|
|
8699
|
+
}
|
|
8700
|
+
if (touchedDomains.length >= 2) {
|
|
8701
|
+
const riskLevel = touchedDomains.some((d) => CRITICAL_DOMAINS.has(d)) ? "critical" : "high";
|
|
8702
|
+
hotPaths.push({
|
|
8703
|
+
file,
|
|
8704
|
+
reason: `Touches ${touchedDomains.join(" + ")} domains`,
|
|
8705
|
+
riskLevel
|
|
8706
|
+
});
|
|
8707
|
+
}
|
|
8708
|
+
}
|
|
8709
|
+
return hotPaths;
|
|
8710
|
+
}
|
|
8711
|
+
function generateCustomPasses(domains) {
|
|
8712
|
+
const passes = [];
|
|
8713
|
+
for (const domain of domains) {
|
|
8714
|
+
const def = DOMAINS[domain];
|
|
8715
|
+
if (def && def.passes.length > 0) {
|
|
8716
|
+
passes.push(...def.passes);
|
|
8717
|
+
}
|
|
8718
|
+
}
|
|
8719
|
+
return passes;
|
|
8720
|
+
}
|
|
8721
|
+
function determineSkippedPasses(ctx) {
|
|
8722
|
+
const skipped = [];
|
|
8723
|
+
for (const rule of SKIP_RULES) {
|
|
8724
|
+
if (rule.condition(ctx)) {
|
|
8725
|
+
skipped.push({
|
|
8726
|
+
passName: rule.passName,
|
|
8727
|
+
reason: rule.reason
|
|
8728
|
+
});
|
|
8729
|
+
}
|
|
8730
|
+
}
|
|
8731
|
+
return skipped;
|
|
8732
|
+
}
|
|
8733
|
+
|
|
8734
|
+
// src/cli/commands/profile.ts
|
|
8735
|
+
async function profileCommand(options) {
|
|
8736
|
+
const cwd = process.cwd();
|
|
8737
|
+
const whiterosePath = join(cwd, ".whiterose");
|
|
8738
|
+
if (!existsSync(whiterosePath)) {
|
|
8739
|
+
p3.log.error("whiterose is not initialized in this directory.");
|
|
8740
|
+
p3.log.info('Run "whiterose init" first.');
|
|
8741
|
+
process.exit(1);
|
|
8742
|
+
}
|
|
8743
|
+
const spinner6 = p3.spinner();
|
|
8744
|
+
spinner6.start("Loading codebase understanding...");
|
|
8745
|
+
const understanding = await loadUnderstanding(cwd);
|
|
8746
|
+
if (!understanding) {
|
|
8747
|
+
spinner6.stop("No understanding found");
|
|
8748
|
+
p3.log.error('No understanding.json found. Run "whiterose init" or "whiterose refresh" first.');
|
|
8749
|
+
process.exit(1);
|
|
8750
|
+
}
|
|
8751
|
+
spinner6.stop("Understanding loaded");
|
|
8752
|
+
let packageJsonDeps;
|
|
8753
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
8754
|
+
if (existsSync(packageJsonPath)) {
|
|
8755
|
+
try {
|
|
8756
|
+
const pkg2 = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
8757
|
+
packageJsonDeps = {
|
|
8758
|
+
...pkg2.dependencies || {},
|
|
8759
|
+
...pkg2.devDependencies || {}
|
|
8760
|
+
};
|
|
8761
|
+
} catch {
|
|
8762
|
+
}
|
|
8763
|
+
}
|
|
8764
|
+
const projectFiles = understanding.features.flatMap((f) => f.relatedFiles || []);
|
|
8765
|
+
try {
|
|
8766
|
+
const rootFiles = readdirSync(cwd);
|
|
8767
|
+
for (const file of rootFiles) {
|
|
8768
|
+
if (/^\.env($|\.)/.test(file) || /^config\.(json|yml|yaml|toml)$/.test(file) || file === "openapi.json" || file === "openapi.yml" || file === "openapi.yaml" || file === "swagger.json" || file === "swagger.yml" || file === "swagger.yaml") {
|
|
8769
|
+
projectFiles.push(file);
|
|
8770
|
+
}
|
|
8771
|
+
}
|
|
8772
|
+
} catch {
|
|
8773
|
+
}
|
|
8774
|
+
spinner6.start("Generating risk profile...");
|
|
8775
|
+
const profile = generateRiskProfile(understanding, projectFiles, packageJsonDeps);
|
|
8776
|
+
const cachePath = join(whiterosePath, "cache");
|
|
8777
|
+
mkdirSync(cachePath, { recursive: true });
|
|
8778
|
+
const profilePath = join(whiterosePath, "risk-profile.json");
|
|
8779
|
+
writeFileSync(profilePath, JSON.stringify(profile, null, 2), "utf-8");
|
|
8780
|
+
spinner6.stop("Risk profile generated");
|
|
8781
|
+
if (options.json) {
|
|
8782
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
8783
|
+
return;
|
|
8784
|
+
}
|
|
8785
|
+
displayProfile(profile);
|
|
8786
|
+
console.log();
|
|
8787
|
+
p3.log.success(`Saved to ${chalk3.cyan(".whiterose/risk-profile.json")}`);
|
|
8788
|
+
p3.log.info(`Run ${chalk3.cyan("whiterose scan --full")} to use this profile.`);
|
|
8789
|
+
}
|
|
8790
|
+
function displayProfile(profile) {
|
|
8791
|
+
console.log();
|
|
8792
|
+
if (profile.domains.length > 0) {
|
|
8793
|
+
p3.log.step(chalk3.bold("Detected Domains"));
|
|
8794
|
+
for (const domain of profile.domains) {
|
|
8795
|
+
const icon = getDomainIcon(domain);
|
|
8796
|
+
console.log(` ${icon} ${chalk3.cyan(domain)}`);
|
|
8797
|
+
}
|
|
8798
|
+
} else {
|
|
8799
|
+
p3.log.info("No specific domains detected");
|
|
8800
|
+
}
|
|
8801
|
+
if (profile.sensitiveDataTypes.length > 0) {
|
|
8802
|
+
console.log();
|
|
8803
|
+
p3.log.step(chalk3.bold("Sensitive Data Types"));
|
|
8804
|
+
for (const dt of profile.sensitiveDataTypes) {
|
|
8805
|
+
console.log(` ${chalk3.yellow("\u25CF")} ${dt}`);
|
|
8806
|
+
}
|
|
8807
|
+
}
|
|
8808
|
+
if (profile.hotPaths.length > 0) {
|
|
8809
|
+
console.log();
|
|
8810
|
+
p3.log.step(chalk3.bold("Hot Paths") + chalk3.dim(" (files touching 2+ domains)"));
|
|
8811
|
+
for (const hp of profile.hotPaths) {
|
|
8812
|
+
const color = hp.riskLevel === "critical" ? chalk3.red : hp.riskLevel === "high" ? chalk3.yellow : chalk3.blue;
|
|
8813
|
+
console.log(` ${color("[" + hp.riskLevel + "]")} ${hp.file}`);
|
|
8814
|
+
console.log(` ${chalk3.dim(hp.reason)}`);
|
|
8815
|
+
}
|
|
8816
|
+
}
|
|
8817
|
+
if (profile.customPasses.length > 0) {
|
|
8818
|
+
console.log();
|
|
8819
|
+
p3.log.step(chalk3.bold("Custom Passes") + chalk3.dim(` (${profile.customPasses.length} domain-specific)`));
|
|
8820
|
+
for (const pass of profile.customPasses) {
|
|
8821
|
+
const phaseColor = pass.phase === "unit" ? chalk3.green : pass.phase === "integration" ? chalk3.blue : chalk3.magenta;
|
|
8822
|
+
console.log(` ${chalk3.cyan("+")} ${pass.id} ${phaseColor("[" + pass.phase + "]")}`);
|
|
8823
|
+
}
|
|
8824
|
+
}
|
|
8825
|
+
if (profile.skippedPasses.length > 0) {
|
|
8826
|
+
console.log();
|
|
8827
|
+
p3.log.step(chalk3.bold("Skipped Passes") + chalk3.dim(` (${profile.skippedPasses.length} not relevant)`));
|
|
8828
|
+
for (const skip of profile.skippedPasses) {
|
|
8829
|
+
console.log(` ${chalk3.dim("-")} ${skip.passName} ${chalk3.dim("(" + skip.reason + ")")}`);
|
|
8830
|
+
}
|
|
8831
|
+
}
|
|
8832
|
+
}
|
|
8833
|
+
function getDomainIcon(domain) {
|
|
8834
|
+
const icons = {
|
|
8835
|
+
"payments": "\u25B6",
|
|
8836
|
+
"auth": "\u25B6",
|
|
8837
|
+
"file-upload": "\u25B6",
|
|
8838
|
+
"messaging": "\u25B6",
|
|
8839
|
+
"search": "\u25B6",
|
|
8840
|
+
"analytics": "\u25B6",
|
|
8841
|
+
"admin": "\u25B6",
|
|
8842
|
+
"api": "\u25B6",
|
|
8843
|
+
"database": "\u25B6",
|
|
8844
|
+
"realtime": "\u25B6"
|
|
8845
|
+
};
|
|
8846
|
+
return icons[domain] || "\u25B6";
|
|
8847
|
+
}
|
|
8848
|
+
|
|
7890
8849
|
// src/cli/index.ts
|
|
7891
8850
|
var __filename$1 = fileURLToPath(import.meta.url);
|
|
7892
8851
|
var __dirname$1 = dirname(__filename$1);
|
|
@@ -7916,6 +8875,7 @@ program.command("refresh").description("Rebuild codebase understanding from scra
|
|
|
7916
8875
|
program.command("status").description("Show whiterose status (cache, last scan, provider)").action(statusCommand);
|
|
7917
8876
|
program.command("report").description("Generate BUGS.md from last scan").option("-o, --output <path>", "Output path", "BUGS.md").option("--format <format>", "Output format (markdown, sarif, json)", "markdown").action(reportCommand);
|
|
7918
8877
|
program.command("clear").description("Clear accumulated bug list (start fresh)").option("--force", "Skip confirmation prompt").action(clearCommand);
|
|
8878
|
+
program.command("profile").description("Generate risk profile for targeted scanning").option("--json", "Output as JSON").action(profileCommand);
|
|
7919
8879
|
async function autoRun() {
|
|
7920
8880
|
console.log(BANNER);
|
|
7921
8881
|
p3.intro(chalk3.red("whiterose") + chalk3.dim(" - AI Bug Hunter"));
|