codeprobe-scanner 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/commands/scan.ts +26 -18
- package/src/cli/index.ts +137 -0
- package/src/engine/index.ts +90 -0
- package/src/engine/scraper.ts +118 -90
package/package.json
CHANGED
package/src/cli/commands/scan.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { handleError, CodeProbeError } from '../errors.js';
|
|
|
8
8
|
import { generateScanId, formatRiskScore, msToHuman } from '../../shared/utils.js';
|
|
9
9
|
import { Report } from '../../shared/types.js';
|
|
10
10
|
import { createEngine } from '../../engine/index.js';
|
|
11
|
-
import {
|
|
11
|
+
import { createScraper } from '../../engine/scraper.js';
|
|
12
12
|
|
|
13
13
|
interface ScanOptions {
|
|
14
14
|
fix: boolean;
|
|
@@ -105,23 +105,6 @@ export async function scanCommand(args: string[]): Promise<void> {
|
|
|
105
105
|
|
|
106
106
|
logger.printHeader();
|
|
107
107
|
|
|
108
|
-
// Check for required API key
|
|
109
|
-
const brightDataKey = process.env.BRIGHT_DATA_API_KEY || await getConfig("bright_data_api_key");
|
|
110
|
-
if (!brightDataKey) {
|
|
111
|
-
console.log(chalk.yellow('\n⚠️ No Bright Data API key configured.'));
|
|
112
|
-
console.log(chalk.dim(' CVE lookup requires a Bright Data account (free tier available).'));
|
|
113
|
-
console.log(chalk.dim(' Get a key at: https://brightdata.com'));
|
|
114
|
-
console.log('');
|
|
115
|
-
console.log(chalk.cyan(' To configure:'));
|
|
116
|
-
console.log(chalk.white(' codeprobe config set bright_data_api_key <your-key>'));
|
|
117
|
-
console.log('');
|
|
118
|
-
console.log(chalk.dim(' Optional keys for full functionality:'));
|
|
119
|
-
console.log(chalk.dim(' codeprobe config set daytona_api_key <key> # exploit verification'));
|
|
120
|
-
console.log(chalk.dim(' codeprobe config set kimi_api_key <key> # AI patch generation'));
|
|
121
|
-
console.log('');
|
|
122
|
-
process.exit(EXIT_CODES.SCAN_FAILED);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
108
|
const startTime = Date.now();
|
|
126
109
|
|
|
127
110
|
try {
|
|
@@ -139,6 +122,31 @@ export async function scanCommand(args: string[]): Promise<void> {
|
|
|
139
122
|
// Display results
|
|
140
123
|
displayReport(report, options.json, duration);
|
|
141
124
|
|
|
125
|
+
// Show recent npm threats from GitHub Advisory Database
|
|
126
|
+
if (!options.json) {
|
|
127
|
+
try {
|
|
128
|
+
const scraper = createScraper();
|
|
129
|
+
const threats = await scraper.fetchRecentThreats();
|
|
130
|
+
if (threats.length > 0) {
|
|
131
|
+
console.log(chalk.bold('\n🌐 Recent npm Security Threats (GitHub Advisory Database):'));
|
|
132
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
133
|
+
for (const t of threats.slice(0, 5)) {
|
|
134
|
+
const sev = t.severity === 'CRITICAL' ? chalk.red(t.severity)
|
|
135
|
+
: t.severity === 'HIGH' ? chalk.yellow(t.severity)
|
|
136
|
+
: chalk.blue(t.severity);
|
|
137
|
+
console.log(` ${sev} ${chalk.bold(t.title)}`);
|
|
138
|
+
if (t.packages.length > 0) {
|
|
139
|
+
console.log(chalk.dim(` Packages: ${t.packages.join(', ')}`));
|
|
140
|
+
}
|
|
141
|
+
console.log(chalk.dim(` ${t.published?.slice(0, 10)} · ${t.url}`));
|
|
142
|
+
}
|
|
143
|
+
console.log('');
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// Non-fatal — threats feed is best-effort
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
142
150
|
// If --fix, handle that next
|
|
143
151
|
if (options.fix) {
|
|
144
152
|
const { scanWithFixCommand } = await import('./scan-with-fix.js');
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { APP_NAME, APP_VERSION, EXIT_CODES } from '../shared/constants.js';
|
|
5
|
+
import { ProgressLogger } from './progress.js';
|
|
6
|
+
import { handleError } from './errors.js';
|
|
7
|
+
import { scanCommand } from './commands/scan.js';
|
|
8
|
+
import { reportCommand } from './commands/report.js';
|
|
9
|
+
|
|
10
|
+
const logger = new ProgressLogger();
|
|
11
|
+
|
|
12
|
+
function showHelp(): void {
|
|
13
|
+
console.log(`
|
|
14
|
+
${chalk.bold.cyan(`⚡ ${APP_NAME} v${APP_VERSION}`)}
|
|
15
|
+
|
|
16
|
+
${chalk.bold('USAGE')}
|
|
17
|
+
codeprobe <command> [options] [arguments]
|
|
18
|
+
|
|
19
|
+
${chalk.bold('COMMANDS')}
|
|
20
|
+
scan [path] Scan a repository for vulnerabilities (default: current dir)
|
|
21
|
+
report Display last scan results
|
|
22
|
+
config Manage configuration
|
|
23
|
+
help Show this help message
|
|
24
|
+
|
|
25
|
+
${chalk.bold('OPTIONS')}
|
|
26
|
+
--fix Auto-fix vulnerabilities (creates git branch & commits)
|
|
27
|
+
--json Output results as JSON
|
|
28
|
+
--verbose Show detailed logs
|
|
29
|
+
--help Show help
|
|
30
|
+
|
|
31
|
+
${chalk.bold('EXAMPLES')}
|
|
32
|
+
codeprobe scan
|
|
33
|
+
codeprobe scan ./my-app
|
|
34
|
+
codeprobe scan --fix
|
|
35
|
+
codeprobe scan --json > report.json
|
|
36
|
+
codeprobe report
|
|
37
|
+
codeprobe config set bright_data_api_key <key>
|
|
38
|
+
|
|
39
|
+
${chalk.bold('DOCS')}
|
|
40
|
+
https://github.com/codeprobe/codeprobe
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function main(): Promise<void> {
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
|
|
47
|
+
// No args = show help
|
|
48
|
+
if (args.length === 0) {
|
|
49
|
+
showHelp();
|
|
50
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const command = args[0];
|
|
54
|
+
const restArgs = args.slice(1);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
switch (command) {
|
|
58
|
+
case 'scan':
|
|
59
|
+
await scanCommand(restArgs);
|
|
60
|
+
break;
|
|
61
|
+
|
|
62
|
+
case 'report':
|
|
63
|
+
await reportCommand(restArgs);
|
|
64
|
+
break;
|
|
65
|
+
|
|
66
|
+
case 'config':
|
|
67
|
+
await handleConfigCommand(restArgs);
|
|
68
|
+
break;
|
|
69
|
+
|
|
70
|
+
case '--help':
|
|
71
|
+
case '-h':
|
|
72
|
+
case 'help':
|
|
73
|
+
showHelp();
|
|
74
|
+
break;
|
|
75
|
+
|
|
76
|
+
default:
|
|
77
|
+
console.error(chalk.red(`Unknown command: ${command}`));
|
|
78
|
+
console.log(`Run ${chalk.cyan('codeprobe --help')} for usage`);
|
|
79
|
+
process.exit(EXIT_CODES.SCAN_FAILED);
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
handleError(error, logger, true);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function handleConfigCommand(args: string[]): Promise<void> {
|
|
87
|
+
const { getConfig, setConfig, clearConfig } = await import('./config.js');
|
|
88
|
+
|
|
89
|
+
const subcommand = args[0];
|
|
90
|
+
|
|
91
|
+
switch (subcommand) {
|
|
92
|
+
case 'get':
|
|
93
|
+
{
|
|
94
|
+
const key = args[1];
|
|
95
|
+
if (!key) {
|
|
96
|
+
const config = await getConfig();
|
|
97
|
+
console.log(JSON.stringify(config, null, 2));
|
|
98
|
+
} else {
|
|
99
|
+
const value = await getConfig(key);
|
|
100
|
+
console.log(value || `${key} not set`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
|
|
105
|
+
case 'set':
|
|
106
|
+
{
|
|
107
|
+
const key = args[1];
|
|
108
|
+
const value = args[2];
|
|
109
|
+
if (!key || !value) {
|
|
110
|
+
console.error(chalk.red('Usage: codeprobe config set <key> <value>'));
|
|
111
|
+
process.exit(EXIT_CODES.SCAN_FAILED);
|
|
112
|
+
}
|
|
113
|
+
await setConfig(key, value);
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case 'clear':
|
|
118
|
+
{
|
|
119
|
+
const key = args[1];
|
|
120
|
+
if (!key) {
|
|
121
|
+
console.error(chalk.red('Usage: codeprobe config clear <key>'));
|
|
122
|
+
process.exit(EXIT_CODES.SCAN_FAILED);
|
|
123
|
+
}
|
|
124
|
+
await clearConfig(key);
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
|
|
128
|
+
default:
|
|
129
|
+
console.error(chalk.red(`Unknown config subcommand: ${subcommand}`));
|
|
130
|
+
console.log(`Usage: codeprobe config [get|set|clear]`);
|
|
131
|
+
process.exit(EXIT_CODES.SCAN_FAILED);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
main().catch((error) => {
|
|
136
|
+
handleError(error, logger, true);
|
|
137
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createParser } from "./parser";
|
|
2
|
+
import { createScraper } from "./scraper";
|
|
3
|
+
import { createSandbox } from "./sandbox";
|
|
4
|
+
import { createMatcher } from "./matcher";
|
|
5
|
+
import { createPatcher } from "./patcher";
|
|
6
|
+
import { createReportBuilder } from "./report";
|
|
7
|
+
import { Report } from "../shared/types";
|
|
8
|
+
|
|
9
|
+
export class CodeProbeEngine {
|
|
10
|
+
private parser = createParser();
|
|
11
|
+
private scraper = createScraper();
|
|
12
|
+
private sandbox = createSandbox();
|
|
13
|
+
private matcher = createMatcher();
|
|
14
|
+
private patcher = createPatcher();
|
|
15
|
+
private reportBuilder = createReportBuilder();
|
|
16
|
+
|
|
17
|
+
getVideoRecorder() {
|
|
18
|
+
return this.sandbox.getVideoRecorder();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async scan(repoPath: string): Promise<Report> {
|
|
22
|
+
const startTime = Date.now();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Step 1: Parse dependencies
|
|
26
|
+
console.log("📦 Parsing dependencies...");
|
|
27
|
+
const dependencies = await this.parser.parseDependencies(repoPath);
|
|
28
|
+
console.log(` Found ${dependencies.length} dependencies`);
|
|
29
|
+
|
|
30
|
+
// Step 2: Scrape CVEs from OSV.dev + npm audit
|
|
31
|
+
console.log("🔍 Checking OSV.dev + npm advisory database...");
|
|
32
|
+
const cves = await this.scraper.scrapeAll(dependencies);
|
|
33
|
+
console.log(` Found ${cves.length} CVEs`);
|
|
34
|
+
|
|
35
|
+
// Step 3: Match dependencies to CVEs
|
|
36
|
+
console.log("🎯 Matching dependencies to CVEs...");
|
|
37
|
+
const matchedCves = this.matcher.matchDependenciesToCVEs(dependencies, cves);
|
|
38
|
+
console.log(` Matched ${matchedCves.length} CVEs`);
|
|
39
|
+
|
|
40
|
+
// Step 4: Filter CRITICAL/HIGH for sandbox verification
|
|
41
|
+
const criticalCves = this.matcher.filterBySeverity(matchedCves, "HIGH");
|
|
42
|
+
console.log(` Testing ${criticalCves.length} critical/high severity CVEs...`);
|
|
43
|
+
|
|
44
|
+
// Step 5: Run exploit verification in sandboxes (Daytona)
|
|
45
|
+
const exploits = criticalCves.map((cve) => ({
|
|
46
|
+
packageName: cve.package,
|
|
47
|
+
version: cve.version_vulnerable,
|
|
48
|
+
cveId: cve.id,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
console.log("\x1b[33m[Daytona]\x1b[0m 🏗️ Spawning isolated sandboxes for exploit verification...");
|
|
52
|
+
const sandboxResults = await this.sandbox.parallelRun(exploits);
|
|
53
|
+
|
|
54
|
+
// Step 6: Update CVEs with sandbox results
|
|
55
|
+
for (const cve of matchedCves) {
|
|
56
|
+
const sandboxResult = sandboxResults.get(cve.id);
|
|
57
|
+
if (sandboxResult) {
|
|
58
|
+
cve.exploitable = sandboxResult.success;
|
|
59
|
+
cve.exploit_evidence = sandboxResult.stdout;
|
|
60
|
+
cve.verification_time_ms = sandboxResult.time_ms;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Step 7: Generate patches (Nosana)
|
|
65
|
+
console.log("\x1b[33m[Nosana]\x1b[0m 🔧 Generating patches with LLM...");
|
|
66
|
+
await this.patcher.loadPrebakedPatches();
|
|
67
|
+
const patches = await this.patcher.generateAllPatches(matchedCves.filter((c) => c.exploitable));
|
|
68
|
+
for (const cve of matchedCves) {
|
|
69
|
+
if (patches.has(cve.id)) {
|
|
70
|
+
cve.patch_diff = patches.get(cve.id);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Step 8: Calculate risk score
|
|
75
|
+
const riskScore = this.matcher.calculateRiskScore(matchedCves);
|
|
76
|
+
|
|
77
|
+
// Step 9: Build and save report
|
|
78
|
+
const scanDuration = Date.now() - startTime;
|
|
79
|
+
const report = await this.reportBuilder.buildReport(repoPath, matchedCves, riskScore, scanDuration, dependencies.length);
|
|
80
|
+
|
|
81
|
+
await this.reportBuilder.saveReport(report);
|
|
82
|
+
|
|
83
|
+
return report;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new Error(`Scan failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const createEngine = () => new CodeProbeEngine();
|
package/src/engine/scraper.ts
CHANGED
|
@@ -1,34 +1,111 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
|
-
import { CVE
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { CVE } from "../shared/types";
|
|
3
|
+
import { DEMO_CVE } from "../shared/constants";
|
|
4
|
+
|
|
5
|
+
const OSV_API = "https://api.osv.dev/v1";
|
|
6
|
+
const GITHUB_ADVISORY_API = "https://api.github.com/advisories";
|
|
7
|
+
|
|
8
|
+
export interface ThreatIntel {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
severity: string;
|
|
12
|
+
packages: string[];
|
|
13
|
+
published: string;
|
|
14
|
+
url: string;
|
|
15
|
+
summary: string;
|
|
16
|
+
}
|
|
5
17
|
|
|
6
18
|
export class CVEScraper {
|
|
7
|
-
|
|
8
|
-
private
|
|
19
|
+
// Query OSV.dev — exact package+version match, no false positives
|
|
20
|
+
private async queryOSV(packageName: string, version: string): Promise<CVE[]> {
|
|
21
|
+
try {
|
|
22
|
+
const response = await axios.post(
|
|
23
|
+
`${OSV_API}/query`,
|
|
24
|
+
{
|
|
25
|
+
package: { name: packageName, ecosystem: "npm" },
|
|
26
|
+
version,
|
|
27
|
+
},
|
|
28
|
+
{ timeout: 10000, headers: { "Content-Type": "application/json" } }
|
|
29
|
+
);
|
|
9
30
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
31
|
+
const vulns = response.data?.vulns;
|
|
32
|
+
if (!Array.isArray(vulns)) return [];
|
|
33
|
+
|
|
34
|
+
return vulns.map((v: any) => {
|
|
35
|
+
const severity = v.severity?.[0]?.score || v.database_specific?.severity || "MEDIUM";
|
|
36
|
+
const cvss = typeof severity === "number" ? severity : this.severityToScore(severity);
|
|
37
|
+
const aliases: string[] = v.aliases || [];
|
|
38
|
+
const cveId = aliases.find((a: string) => a.startsWith("CVE-")) || v.id;
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
id: cveId,
|
|
42
|
+
package: packageName,
|
|
43
|
+
affected_versions: [version],
|
|
44
|
+
fixed_version: this.extractFixedVersion(v, packageName),
|
|
45
|
+
severity: this.normalizeSeverity(severity),
|
|
46
|
+
cvss,
|
|
47
|
+
description: v.details || v.summary || "",
|
|
48
|
+
cwe: v.database_specific?.cwe_ids?.[0] || "",
|
|
49
|
+
exploit_url: `https://osv.dev/vulnerability/${v.id}`,
|
|
50
|
+
} as CVE;
|
|
51
|
+
});
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
14
55
|
}
|
|
15
56
|
|
|
16
|
-
|
|
17
|
-
|
|
57
|
+
// Fetch recent npm security threats from GitHub Advisory Database
|
|
58
|
+
async fetchRecentThreats(): Promise<ThreatIntel[]> {
|
|
59
|
+
try {
|
|
60
|
+
const response = await axios.get(GITHUB_ADVISORY_API, {
|
|
61
|
+
params: {
|
|
62
|
+
ecosystem: "npm",
|
|
63
|
+
per_page: 10,
|
|
64
|
+
sort: "published",
|
|
65
|
+
direction: "desc",
|
|
66
|
+
},
|
|
67
|
+
timeout: 10000,
|
|
68
|
+
headers: { Accept: "application/vnd.github+json" },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return (response.data || []).map((a: any) => ({
|
|
72
|
+
id: a.ghsa_id,
|
|
73
|
+
title: a.summary || a.ghsa_id,
|
|
74
|
+
severity: a.severity?.toUpperCase() || "UNKNOWN",
|
|
75
|
+
packages: (a.vulnerabilities || []).map((v: any) => v.package?.name).filter(Boolean),
|
|
76
|
+
published: a.published_at,
|
|
77
|
+
url: a.html_url || `https://github.com/advisories/${a.ghsa_id}`,
|
|
78
|
+
summary: a.description?.substring(0, 200) || "",
|
|
79
|
+
}));
|
|
80
|
+
} catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
18
84
|
|
|
85
|
+
async scrapeForCVEs(packageName: string, version: string): Promise<CVE[]> {
|
|
19
86
|
if (packageName === DEMO_CVE.package) {
|
|
20
87
|
return [this.buildDemoCVE()];
|
|
21
88
|
}
|
|
89
|
+
return await this.queryOSV(packageName, version);
|
|
90
|
+
}
|
|
22
91
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
92
|
+
async scrapeAll(
|
|
93
|
+
dependencies: Array<{ name: string; version: string }>
|
|
94
|
+
): Promise<CVE[]> {
|
|
95
|
+
const results = await Promise.all(
|
|
96
|
+
dependencies.map(dep => this.scrapeForCVEs(dep.name, dep.version))
|
|
97
|
+
);
|
|
98
|
+
const seen = new Set<string>();
|
|
99
|
+
const allCVEs: CVE[] = [];
|
|
100
|
+
for (const cves of results) {
|
|
101
|
+
for (const cve of cves) {
|
|
102
|
+
if (!seen.has(cve.id)) {
|
|
103
|
+
seen.add(cve.id);
|
|
104
|
+
allCVEs.push(cve);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
31
107
|
}
|
|
108
|
+
return allCVEs;
|
|
32
109
|
}
|
|
33
110
|
|
|
34
111
|
private buildDemoCVE(): CVE {
|
|
@@ -41,87 +118,38 @@ export class CVEScraper {
|
|
|
41
118
|
cvss: DEMO_CVE.cvss,
|
|
42
119
|
description: DEMO_CVE.description,
|
|
43
120
|
cwe: "CWE-94",
|
|
44
|
-
exploit_url:
|
|
121
|
+
exploit_url: `https://nvd.nist.gov/vuln/detail/${DEMO_CVE.id}`,
|
|
45
122
|
};
|
|
46
123
|
}
|
|
47
124
|
|
|
48
|
-
private
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
{
|
|
57
|
-
headers: {
|
|
58
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
59
|
-
"Accept": "application/json",
|
|
60
|
-
},
|
|
61
|
-
timeout: TIMEOUTS.BRIGHT_DATA_SCRAPE,
|
|
125
|
+
private extractFixedVersion(vuln: any, packageName: string): string {
|
|
126
|
+
const affected = vuln.affected || [];
|
|
127
|
+
for (const a of affected) {
|
|
128
|
+
if (a.package?.name === packageName) {
|
|
129
|
+
for (const range of a.ranges || []) {
|
|
130
|
+
for (const event of range.events || []) {
|
|
131
|
+
if (event.fixed) return event.fixed;
|
|
132
|
+
}
|
|
62
133
|
}
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
if (response.data?.vulnerabilities && Array.isArray(response.data.vulnerabilities)) {
|
|
66
|
-
return response.data.vulnerabilities
|
|
67
|
-
.filter((vuln: any) => {
|
|
68
|
-
const affectedProducts = vuln.cve?.configurations?.[0]?.nodes?.flatMap((n: any) =>
|
|
69
|
-
n.cpeMatch?.map((m: any) => m.criteria) || []
|
|
70
|
-
) || [];
|
|
71
|
-
return affectedProducts.some((p: string) => p?.includes(packageName));
|
|
72
|
-
})
|
|
73
|
-
.map((vuln: any) => {
|
|
74
|
-
const descriptions = vuln.cve?.descriptions || [];
|
|
75
|
-
return {
|
|
76
|
-
id: vuln.id,
|
|
77
|
-
package: packageName,
|
|
78
|
-
affected_versions: [version],
|
|
79
|
-
fixed_version: "",
|
|
80
|
-
severity: vuln.impact?.baseSeverity || "MEDIUM",
|
|
81
|
-
cvss: vuln.impact?.baseScore || 5.0,
|
|
82
|
-
description: descriptions[0]?.value || "",
|
|
83
|
-
cwe: vuln.cve?.weaknesses?.[0]?.source || "",
|
|
84
|
-
exploit_url: `https://nvd.nist.gov/vuln/detail/${vuln.id}`,
|
|
85
|
-
};
|
|
86
|
-
})
|
|
87
|
-
.slice(0, 5); // Limit to top 5 results
|
|
88
134
|
}
|
|
89
|
-
|
|
90
|
-
return [];
|
|
91
|
-
} catch (error) {
|
|
92
|
-
// Bright Data failed, will fall back to cache
|
|
93
|
-
console.warn(`[Bright Data] Scraping failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
94
|
-
throw new Error(`Bright Data API call failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
95
135
|
}
|
|
136
|
+
return "";
|
|
96
137
|
}
|
|
97
138
|
|
|
98
|
-
private
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const content = await cacheFile.text();
|
|
105
|
-
const cacheData = JSON.parse(content);
|
|
106
|
-
return cacheData.cves || [];
|
|
107
|
-
}
|
|
108
|
-
} catch (error) {
|
|
109
|
-
console.warn("Failed to load CVE cache:", error);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Return at least the demo CVE
|
|
113
|
-
return [this.buildDemoCVE()];
|
|
139
|
+
private normalizeSeverity(s: string): "CRITICAL" | "HIGH" | "MEDIUM" | "LOW" {
|
|
140
|
+
const upper = (s || "").toUpperCase();
|
|
141
|
+
if (upper === "CRITICAL") return "CRITICAL";
|
|
142
|
+
if (upper === "HIGH") return "HIGH";
|
|
143
|
+
if (upper === "LOW") return "LOW";
|
|
144
|
+
return "MEDIUM";
|
|
114
145
|
}
|
|
115
146
|
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return allCVEs;
|
|
147
|
+
private severityToScore(s: string): number {
|
|
148
|
+
const upper = (s || "").toUpperCase();
|
|
149
|
+
if (upper === "CRITICAL") return 9.0;
|
|
150
|
+
if (upper === "HIGH") return 7.5;
|
|
151
|
+
if (upper === "LOW") return 3.0;
|
|
152
|
+
return 5.0;
|
|
125
153
|
}
|
|
126
154
|
}
|
|
127
155
|
|