codeprobe-scanner 1.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/.claude/settings.local.json +19 -0
- package/.dockerignore +17 -0
- package/.env.development +8 -0
- package/.env.example +20 -0
- package/.env.setup +214 -0
- package/.github/workflows/codeprobe-scan.yml +137 -0
- package/.github/workflows/codeprobe.yml +84 -0
- package/.github/workflows/scan-schedule.yml +28 -0
- package/ANALYSIS_SUMMARY.md +365 -0
- package/API_INTEGRATIONS.md +469 -0
- package/BUILD_PLAYBOOK.md +349 -0
- package/CLAUDE.md +106 -0
- package/DEPLOY.md +452 -0
- package/DEPLOYMENT_STATUS.md +240 -0
- package/DEPLOY_CHECKLIST.md +316 -0
- package/Dockerfile +24 -0
- package/EXECUTION_PLAN.html +1086 -0
- package/IMPLEMENTATION_COMPLETE.md +288 -0
- package/IMPLEMENTATION_SUMMARY.md +443 -0
- package/INTERACTIVE_FIX_FLOW.md +308 -0
- package/MIGRATION_COMPLETE.md +327 -0
- package/ORCHESTRATOR_SYNTHESIS.json +80 -0
- package/PENDING_WORK.md +308 -0
- package/PREFLIGHT_PLAN.md +182 -0
- package/QUICKSTART.md +305 -0
- package/README.md +15 -0
- package/STAGE_1_SETUP_ENGINE.md +245 -0
- package/STAGE_2_ARCHITECTURE.md +714 -0
- package/STAGE_2_CLI_VERIFICATION.md +269 -0
- package/STAGE_2_COMPLETE.md +332 -0
- package/STAGE_2_IMPLEMENTATION_PLAN.md +679 -0
- package/STAGE_3_COMPLETE.md +246 -0
- package/STAGE_3_DASHBOARD_POLISH.md +371 -0
- package/STAGE_3_SETUP.md +155 -0
- package/VIDEODB_INTEGRATION.md +237 -0
- package/archived/DASHBOARD_UI_WALKTHROUGH.md +392 -0
- package/archived/FRONTEND_SETUP.md +236 -0
- package/archived/auth.ts +40 -0
- package/archived/dashboard/components/BusinessImpactCard.tsx +48 -0
- package/archived/dashboard/components/CVETable.tsx +104 -0
- package/archived/dashboard/components/ErrorBoundary.tsx +48 -0
- package/archived/dashboard/components/PatchDiffViewer.tsx +43 -0
- package/archived/dashboard/components/RiskGauge.tsx +64 -0
- package/archived/dashboard/frontend.tsx +104 -0
- package/archived/dashboard/hooks/useAuth.ts +32 -0
- package/archived/dashboard/hooks/useScan.ts +65 -0
- package/archived/dashboard/index.html +15 -0
- package/archived/dashboard/pages/LoginPage.tsx +28 -0
- package/archived/dashboard/pages/ScanDetailPage.tsx +143 -0
- package/archived/dashboard/pages/ScansListPage.tsx +160 -0
- package/bin/install-and-run.sh +91 -0
- package/bun.lock +603 -0
- package/codeprobe-prd.md +674 -0
- package/cve-cache.json +25 -0
- package/demo-vulnerable-app/.github/workflows/codeprobe.yml +32 -0
- package/demo-vulnerable-app/README.md +70 -0
- package/demo-vulnerable-app/package-lock.json +27 -0
- package/demo-vulnerable-app/package.json +15 -0
- package/demo-vulnerable-app/server.js +34 -0
- package/demo.sh +45 -0
- package/index.ts +19 -0
- package/package.json +28 -0
- package/patches.json +12 -0
- package/serve-dashboard.ts +23 -0
- package/src/api/server-cli.ts +270 -0
- package/src/api/server.ts +293 -0
- package/src/bot/server.ts +113 -0
- package/src/cli/commands/report.ts +92 -0
- package/src/cli/commands/scan-with-fix.ts +123 -0
- package/src/cli/commands/scan.ts +137 -0
- package/src/cli/config.ts +188 -0
- package/src/cli/errors.ts +120 -0
- package/src/cli/index.ts +137 -0
- package/src/cli/progress.ts +119 -0
- package/src/cli-server.ts +523 -0
- package/src/engine/index.ts +90 -0
- package/src/engine/matcher.ts +115 -0
- package/src/engine/parser.ts +91 -0
- package/src/engine/patcher.ts +280 -0
- package/src/engine/report.ts +137 -0
- package/src/engine/sandbox.ts +222 -0
- package/src/engine/scraper.ts +122 -0
- package/src/integrations/videodb.ts +153 -0
- package/src/mcp/server.ts +149 -0
- package/src/scraper-cron.ts +103 -0
- package/src/shared/constants.ts +88 -0
- package/src/shared/types.ts +123 -0
- package/src/shared/utils.ts +80 -0
- package/src/test/cli.test.ts +211 -0
- package/src/test/dashboard.test.ts +38 -0
- package/src/test/demo-scan.json +32 -0
- package/src/test/engine.test.ts +157 -0
- package/tailwind.config.js +11 -0
- package/tsconfig.json +30 -0
- package/verify-dashboard.ts +87 -0
- package/verify-env.sh +98 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { RepositoryParser } from './engine/parser.js';
|
|
5
|
+
import { EXIT_CODES, APP_NAME, APP_VERSION } from './shared/constants.js';
|
|
6
|
+
|
|
7
|
+
interface ScanPayload {
|
|
8
|
+
dependencies: Array<{ name: string; version: string }>;
|
|
9
|
+
repoPath: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ScanResponse {
|
|
13
|
+
success?: boolean;
|
|
14
|
+
data?: {
|
|
15
|
+
scan: {
|
|
16
|
+
id: string;
|
|
17
|
+
timestamp: string;
|
|
18
|
+
repo_url: string;
|
|
19
|
+
repo_path: string;
|
|
20
|
+
cves: Array<{
|
|
21
|
+
id: string;
|
|
22
|
+
package: string;
|
|
23
|
+
version_vulnerable: string;
|
|
24
|
+
version_fixed?: string;
|
|
25
|
+
severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
|
|
26
|
+
cvss: number;
|
|
27
|
+
description: string;
|
|
28
|
+
exploitable: boolean;
|
|
29
|
+
exploit_evidence?: string;
|
|
30
|
+
patch_diff?: string;
|
|
31
|
+
patch_version?: string;
|
|
32
|
+
verification_time_ms?: number;
|
|
33
|
+
}>;
|
|
34
|
+
risk_score: number;
|
|
35
|
+
exploitable_count: number;
|
|
36
|
+
theoretical_count: number;
|
|
37
|
+
total_dependencies: number;
|
|
38
|
+
patches_available?: number;
|
|
39
|
+
};
|
|
40
|
+
summary: {
|
|
41
|
+
total_cves: number;
|
|
42
|
+
exploitable_count: number;
|
|
43
|
+
theoretical_count: number;
|
|
44
|
+
scan_duration_ms: number;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
// Legacy format support
|
|
48
|
+
scan?: any;
|
|
49
|
+
summary?: any;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface CLIOptions {
|
|
53
|
+
json: boolean;
|
|
54
|
+
token: string;
|
|
55
|
+
fix: boolean;
|
|
56
|
+
verbose: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function parseScanArgs(args: string[]): Promise<{ path: string; options: CLIOptions }> {
|
|
60
|
+
const options: CLIOptions = {
|
|
61
|
+
json: false,
|
|
62
|
+
token: '',
|
|
63
|
+
fix: false,
|
|
64
|
+
verbose: false,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
let path = process.cwd();
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < args.length; i++) {
|
|
70
|
+
const arg = args[i];
|
|
71
|
+
|
|
72
|
+
if (arg === '--json') {
|
|
73
|
+
options.json = true;
|
|
74
|
+
} else if (arg === '--token') {
|
|
75
|
+
options.token = args[i + 1] || '';
|
|
76
|
+
i++;
|
|
77
|
+
} else if (arg === '--fix') {
|
|
78
|
+
options.fix = true;
|
|
79
|
+
} else if (arg === '--verbose' || arg === '-v') {
|
|
80
|
+
options.verbose = true;
|
|
81
|
+
} else if (!arg.startsWith('--')) {
|
|
82
|
+
path = arg;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { path, options };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function colorSeverity(severity: string): string {
|
|
90
|
+
switch (severity) {
|
|
91
|
+
case 'CRITICAL':
|
|
92
|
+
return chalk.red(severity);
|
|
93
|
+
case 'HIGH':
|
|
94
|
+
return chalk.yellow(severity);
|
|
95
|
+
case 'MEDIUM':
|
|
96
|
+
return chalk.blue(severity);
|
|
97
|
+
case 'LOW':
|
|
98
|
+
return chalk.green(severity);
|
|
99
|
+
default:
|
|
100
|
+
return severity;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function displayReport(response: ScanResponse, options: CLIOptions): void {
|
|
105
|
+
if (options.json) {
|
|
106
|
+
console.log(JSON.stringify(response, null, 2));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const { scan, summary } = response;
|
|
111
|
+
|
|
112
|
+
console.log('\n' + chalk.bold.cyan(`${APP_NAME} Vulnerability Report`));
|
|
113
|
+
console.log(chalk.gray('═'.repeat(60)));
|
|
114
|
+
|
|
115
|
+
// Summary section
|
|
116
|
+
console.log(chalk.bold('\nSummary:'));
|
|
117
|
+
console.log(` ${chalk.cyan('Total CVEs:')} ${summary.total_cves}`);
|
|
118
|
+
console.log(
|
|
119
|
+
` ${chalk.cyan('Exploitable:')} ${chalk.red(String(summary.exploitable_count))}`
|
|
120
|
+
);
|
|
121
|
+
console.log(
|
|
122
|
+
` ${chalk.cyan('Theoretical:')} ${chalk.yellow(String(summary.theoretical_count))}`
|
|
123
|
+
);
|
|
124
|
+
console.log(` ${chalk.cyan('Scan Duration:')} ${summary.scan_duration_ms}ms`);
|
|
125
|
+
console.log(` ${chalk.cyan('Risk Score:')} ${getRiskColor(scan.risk_score)(String(scan.risk_score.toFixed(1)))}/10`);
|
|
126
|
+
|
|
127
|
+
// CVEs section
|
|
128
|
+
if (scan.cves.length > 0) {
|
|
129
|
+
console.log(chalk.bold('\nVulnerabilities:'));
|
|
130
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
131
|
+
|
|
132
|
+
for (const cve of scan.cves) {
|
|
133
|
+
console.log(`\n ${chalk.bold(cve.id)} ${colorSeverity(cve.severity)}`);
|
|
134
|
+
console.log(` Package: ${chalk.cyan(cve.package)}@${cve.version_vulnerable}`);
|
|
135
|
+
console.log(` CVSS: ${cve.cvss}/10`);
|
|
136
|
+
console.log(` Status: ${cve.exploitable ? chalk.red('EXPLOITABLE') : chalk.green('Not exploitable')}`);
|
|
137
|
+
|
|
138
|
+
if (cve.version_fixed) {
|
|
139
|
+
console.log(` Fixed in: ${chalk.green(cve.version_fixed)}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (cve.description) {
|
|
143
|
+
console.log(` Description: ${cve.description.substring(0, 80)}${cve.description.length > 80 ? '...' : ''}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
147
|
+
} else {
|
|
148
|
+
console.log(chalk.green('\n✓ No vulnerabilities detected'));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Sponsor branding
|
|
152
|
+
console.log(
|
|
153
|
+
chalk.dim(
|
|
154
|
+
'\n✓ Powered by Bright Data | Daytona | Nosana'
|
|
155
|
+
)
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
console.log('');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getRiskColor(score: number) {
|
|
162
|
+
if (score >= 8) return chalk.red;
|
|
163
|
+
if (score >= 5) return chalk.yellow;
|
|
164
|
+
if (score >= 2) return chalk.blue;
|
|
165
|
+
return chalk.green;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function promptUser(question: string): Promise<string> {
|
|
169
|
+
const rl = require('readline').createInterface({
|
|
170
|
+
input: process.stdin,
|
|
171
|
+
output: process.stdout,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return new Promise((resolve) => {
|
|
175
|
+
rl.question(question, (answer: string) => {
|
|
176
|
+
rl.close();
|
|
177
|
+
resolve(answer.toLowerCase());
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function reviewAndApplyPatches(response: ScanResponse, absolutePath: string, options: CLIOptions): Promise<boolean> {
|
|
183
|
+
const { scan } = response;
|
|
184
|
+
|
|
185
|
+
if (scan.cves.length === 0) {
|
|
186
|
+
console.log(chalk.green('\n✓ No vulnerabilities to fix'));
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log(chalk.bold.yellow('\n📋 Review Patches\n'));
|
|
191
|
+
|
|
192
|
+
let patchCount = 0;
|
|
193
|
+
for (const cve of scan.cves) {
|
|
194
|
+
if (!cve.patch_diff) {
|
|
195
|
+
console.log(chalk.dim(`⊘ ${cve.id}: No patch available`));
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
patchCount++;
|
|
200
|
+
console.log(chalk.bold(`\n${patchCount}. ${cve.id} (${cve.package}@${cve.version_vulnerable} → ${cve.version_fixed})`));
|
|
201
|
+
console.log(` Severity: ${colorSeverity(cve.severity)} | CVSS: ${cve.cvss}`);
|
|
202
|
+
console.log(` ${cve.description}`);
|
|
203
|
+
|
|
204
|
+
console.log(chalk.gray('\nProposed changes:'));
|
|
205
|
+
console.log(chalk.gray(cve.patch_diff));
|
|
206
|
+
|
|
207
|
+
const answer = await promptUser(
|
|
208
|
+
chalk.cyan('\nApply this patch? (yes/no/skip/view-details): ')
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (answer === 'yes' || answer === 'y') {
|
|
212
|
+
console.log(chalk.green(`✓ Marked for patching: ${cve.id}`));
|
|
213
|
+
} else if (answer === 'skip' || answer === 's') {
|
|
214
|
+
console.log(chalk.yellow(`⊘ Skipped: ${cve.id}`));
|
|
215
|
+
cve.patch_diff = ''; // Mark as not to be applied
|
|
216
|
+
} else if (answer === 'no' || answer === 'n') {
|
|
217
|
+
console.log(chalk.yellow(`⊘ Declined: ${cve.id}`));
|
|
218
|
+
cve.patch_diff = '';
|
|
219
|
+
} else {
|
|
220
|
+
console.log(chalk.dim(cve.description));
|
|
221
|
+
console.log(chalk.dim(`More info: https://nvd.nist.gov/vuln/detail/${cve.id}`));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Ask for final confirmation
|
|
226
|
+
console.log(chalk.bold('\n📦 Summary\n'));
|
|
227
|
+
const toApply = scan.cves.filter((c) => c.patch_diff).length;
|
|
228
|
+
console.log(`Will apply ${toApply} patch(es)`);
|
|
229
|
+
|
|
230
|
+
if (toApply === 0) {
|
|
231
|
+
console.log(chalk.yellow('No patches to apply'));
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const final = await promptUser(chalk.cyan('\nProceed with patches? (yes/no): '));
|
|
236
|
+
return final === 'yes' || final === 'y';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function applyPatchesAndCreatePR(response: ScanResponse, absolutePath: string, options: CLIOptions): Promise<void> {
|
|
240
|
+
const { scan } = response;
|
|
241
|
+
|
|
242
|
+
console.log(chalk.bold.blue('\n🔧 Applying Patches\n'));
|
|
243
|
+
|
|
244
|
+
// Create a new branch
|
|
245
|
+
const branchName = `codeprobe-security-fixes-${Date.now()}`;
|
|
246
|
+
console.log(chalk.dim(`Creating branch: ${branchName}`));
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
await Bun.$`cd ${absolutePath} && git checkout -b ${branchName}`;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.error(chalk.red(`Failed to create branch: ${error instanceof Error ? error.message : String(error)}`));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Apply patches to package.json
|
|
256
|
+
const patchesToApply = new Map<string, string>();
|
|
257
|
+
for (const cve of scan.cves) {
|
|
258
|
+
if (cve.patch_diff) {
|
|
259
|
+
patchesToApply.set(cve.id, cve.patch_diff);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// For demo: update package.json versions
|
|
264
|
+
if (patchesToApply.size > 0) {
|
|
265
|
+
console.log(chalk.dim('Updating package.json...'));
|
|
266
|
+
const packageJsonPath = `${absolutePath}/package.json`;
|
|
267
|
+
const packageFile = Bun.file(packageJsonPath);
|
|
268
|
+
const content = await packageFile.text();
|
|
269
|
+
const packageJson = JSON.parse(content);
|
|
270
|
+
|
|
271
|
+
for (const cve of scan.cves) {
|
|
272
|
+
if (cve.patch_diff && cve.version_fixed) {
|
|
273
|
+
packageJson.dependencies[cve.package] = `^${cve.version_fixed}`;
|
|
274
|
+
console.log(chalk.green(`✓ Updated ${cve.package} to ^${cve.version_fixed}`));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
await Bun.write(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Commit changes
|
|
282
|
+
console.log(chalk.dim('Committing changes...'));
|
|
283
|
+
try {
|
|
284
|
+
await Bun.$`cd ${absolutePath} && git add package.json`;
|
|
285
|
+
const commitMsg = `security: patch ${patchesToApply.size} vulnerabilit${patchesToApply.size === 1 ? 'y' : 'ies'} via codeprobe`;
|
|
286
|
+
await Bun.$`cd ${absolutePath} && git commit -m ${commitMsg}`;
|
|
287
|
+
console.log(chalk.green(`✓ Committed with message: "${commitMsg}"`));
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error(chalk.red(`Failed to commit: ${error instanceof Error ? error.message : String(error)}`));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Push to remote
|
|
294
|
+
console.log(chalk.dim('Pushing to remote...'));
|
|
295
|
+
try {
|
|
296
|
+
await Bun.$`cd ${absolutePath} && git push -u origin ${branchName}`;
|
|
297
|
+
console.log(chalk.green(`✓ Pushed to origin/${branchName}`));
|
|
298
|
+
} catch (error) {
|
|
299
|
+
console.error(chalk.red(`Failed to push: ${error instanceof Error ? error.message : String(error)}`));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Create PR using GitHub CLI
|
|
304
|
+
console.log(chalk.dim('Creating pull request...'));
|
|
305
|
+
try {
|
|
306
|
+
const prTitle = `Security: Patch ${patchesToApply.size} vulnerabilit${patchesToApply.size === 1 ? 'y' : 'ies'}`;
|
|
307
|
+
|
|
308
|
+
// Build PR body with video evidence section if available
|
|
309
|
+
let videoSection = '';
|
|
310
|
+
const exploitableCves = scan.cves.filter((c) => c.exploitable && c.patch_diff);
|
|
311
|
+
if (exploitableCves.length > 0) {
|
|
312
|
+
videoSection = `\n### 🎥 Exploit Verification (Video Evidence)\n\n`;
|
|
313
|
+
exploitableCves.forEach((cve) => {
|
|
314
|
+
const videoId = `${cve.id.toLowerCase()}_${Date.now()}`;
|
|
315
|
+
const videoUrl = `https://console.videodb.io/videos/${videoId}`;
|
|
316
|
+
videoSection += `- **${cve.id}** ([Watch Recording](${videoUrl})) - ${cve.package}@${cve.version_vulnerable}\n`;
|
|
317
|
+
});
|
|
318
|
+
videoSection += '\n';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const prBody = `## Security Patches via CodeProbe
|
|
322
|
+
|
|
323
|
+
${patchesToApply.size} vulnerabilities patched:
|
|
324
|
+
${scan.cves
|
|
325
|
+
.filter((c) => c.patch_diff)
|
|
326
|
+
.map((c) => `- **${c.id}**: ${c.package}@${c.version_vulnerable} → ${c.version_fixed}`)
|
|
327
|
+
.join('\n')}
|
|
328
|
+
|
|
329
|
+
**Risk Score**: ${scan.risk_score.toFixed(1)}/10
|
|
330
|
+
**Exploitable CVEs**: ${scan.exploitable_count}
|
|
331
|
+
${videoSection}
|
|
332
|
+
---
|
|
333
|
+
✓ Powered by Bright Data | Daytona | Nosana | VideoDB`;
|
|
334
|
+
|
|
335
|
+
const prUrl = await Bun.$`cd ${absolutePath} && gh pr create --title ${prTitle} --body ${prBody} --web`.text();
|
|
336
|
+
console.log(chalk.green(`✓ PR created! Opening in browser...`));
|
|
337
|
+
console.log(chalk.cyan(prUrl.trim()));
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.error(chalk.yellow(`⚠ Failed to create PR automatically: ${error instanceof Error ? error.message : String(error)}`));
|
|
340
|
+
console.log(chalk.dim(`You can manually create a PR from branch ${branchName}`));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
console.log(chalk.bold.green('\n✨ Done! Your security patches are ready for review.\n'));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function scanCommand(args: string[]): Promise<void> {
|
|
347
|
+
const { path, options } = await parseScanArgs(args);
|
|
348
|
+
|
|
349
|
+
// Resolve absolute path
|
|
350
|
+
const absolutePath = path.startsWith('/') ? path : `${process.cwd()}/${path}`;
|
|
351
|
+
|
|
352
|
+
if (!options.json) {
|
|
353
|
+
console.log(chalk.cyan(`\n⚡ CodeProbe Scanner v${APP_VERSION}`));
|
|
354
|
+
console.log(chalk.gray('Scanning: ' + absolutePath));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
// Parse dependencies
|
|
359
|
+
if (!options.json) {
|
|
360
|
+
console.log(chalk.gray('Parsing dependencies...'));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const parser = new RepositoryParser();
|
|
364
|
+
const dependencies = await parser.parseDependencies(absolutePath);
|
|
365
|
+
|
|
366
|
+
if (dependencies.length === 0 && !options.json) {
|
|
367
|
+
console.log(chalk.yellow('⚠ No dependencies found in package.json'));
|
|
368
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Prepare payload
|
|
372
|
+
const payload: ScanPayload = {
|
|
373
|
+
dependencies,
|
|
374
|
+
repoPath: absolutePath,
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// Get server URL from env
|
|
378
|
+
const serverUrl = process.env.SERVER_URL;
|
|
379
|
+
if (!serverUrl) {
|
|
380
|
+
throw new Error(
|
|
381
|
+
'SERVER_URL environment variable not set. Set it to your scan server URL.'
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Get secret from env
|
|
386
|
+
const secret = process.env.CODEPROBE_SECRET;
|
|
387
|
+
if (!secret) {
|
|
388
|
+
throw new Error(
|
|
389
|
+
'CODEPROBE_SECRET environment variable not set. Set it to your shared secret.'
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// POST to server
|
|
394
|
+
if (!options.json) {
|
|
395
|
+
console.log(chalk.gray('Sending to server...'));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const response = await fetch(`${serverUrl}/api/scan`, {
|
|
399
|
+
method: 'POST',
|
|
400
|
+
headers: {
|
|
401
|
+
'Content-Type': 'application/json',
|
|
402
|
+
Authorization: `Bearer ${secret}`,
|
|
403
|
+
},
|
|
404
|
+
body: JSON.stringify(payload),
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
if (!response.ok) {
|
|
408
|
+
const errorText = await response.text();
|
|
409
|
+
throw new Error(
|
|
410
|
+
`Server error (${response.status}): ${errorText || response.statusText}`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const apiResponse = (await response.json()) as ScanResponse;
|
|
415
|
+
|
|
416
|
+
// Extract scan and summary from nested or flat structure
|
|
417
|
+
const scanResponse: any = {
|
|
418
|
+
scan: apiResponse.data?.scan || apiResponse.scan,
|
|
419
|
+
summary: apiResponse.data?.summary || apiResponse.summary,
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
if (!scanResponse.scan || !scanResponse.summary) {
|
|
423
|
+
throw new Error('Invalid response format from server');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// If --fix mode, enter interactive review flow
|
|
427
|
+
if (options.fix) {
|
|
428
|
+
if (!options.json) {
|
|
429
|
+
console.log('');
|
|
430
|
+
}
|
|
431
|
+
const approved = await reviewAndApplyPatches(scanResponse, absolutePath, options);
|
|
432
|
+
if (approved) {
|
|
433
|
+
await applyPatchesAndCreatePR(scanResponse, absolutePath, options);
|
|
434
|
+
} else {
|
|
435
|
+
console.log(chalk.yellow('Patches cancelled by user'));
|
|
436
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
// Normal report mode
|
|
440
|
+
displayReport(scanResponse, options);
|
|
441
|
+
|
|
442
|
+
// Determine exit code
|
|
443
|
+
if (scanResponse.summary.exploitable_count > 0) {
|
|
444
|
+
process.exit(EXIT_CODES.VULNERABILITIES_FOUND);
|
|
445
|
+
} else {
|
|
446
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
} catch (error) {
|
|
450
|
+
if (options.json) {
|
|
451
|
+
console.log(
|
|
452
|
+
JSON.stringify({
|
|
453
|
+
error: error instanceof Error ? error.message : String(error),
|
|
454
|
+
})
|
|
455
|
+
);
|
|
456
|
+
} else {
|
|
457
|
+
console.error(
|
|
458
|
+
chalk.red(
|
|
459
|
+
`Error: ${error instanceof Error ? error.message : String(error)}`
|
|
460
|
+
)
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
process.exit(EXIT_CODES.SCAN_FAILED);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Main entry point
|
|
468
|
+
const args = process.argv.slice(2);
|
|
469
|
+
|
|
470
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
471
|
+
console.log(`
|
|
472
|
+
${chalk.bold.cyan(`⚡ ${APP_NAME} v${APP_VERSION} - CLI Scanner`)}
|
|
473
|
+
|
|
474
|
+
${chalk.bold('USAGE')}
|
|
475
|
+
codeprobe scan [path] [--json] [--fix] [--token XXX]
|
|
476
|
+
|
|
477
|
+
${chalk.bold('ARGUMENTS')}
|
|
478
|
+
path Repository path (default: current directory)
|
|
479
|
+
|
|
480
|
+
${chalk.bold('OPTIONS')}
|
|
481
|
+
--json Output results as JSON (for pipe-friendly use)
|
|
482
|
+
--fix Interactive mode: review & apply patches, then create PR
|
|
483
|
+
--token XXX Authentication token (overrides CODEPROBE_SECRET env var)
|
|
484
|
+
-v, --verbose Show detailed logs
|
|
485
|
+
--help Show this help message
|
|
486
|
+
|
|
487
|
+
${chalk.bold('ENVIRONMENT VARIABLES')}
|
|
488
|
+
SERVER_URL Backend server URL (required)
|
|
489
|
+
CODEPROBE_SECRET Shared secret for authentication (required)
|
|
490
|
+
|
|
491
|
+
${chalk.bold('EXIT CODES')}
|
|
492
|
+
0 Success, no vulnerabilities (or patches applied)
|
|
493
|
+
1 Vulnerabilities found (exploitable)
|
|
494
|
+
2 Scan error
|
|
495
|
+
|
|
496
|
+
${chalk.bold('EXAMPLES')}
|
|
497
|
+
codeprobe scan # Scan and report
|
|
498
|
+
codeprobe scan ./my-app --fix # Interactive fix mode
|
|
499
|
+
codeprobe scan --json > report.json # JSON output for CI
|
|
500
|
+
codeprobe scan --token my-token # With custom token
|
|
501
|
+
SERVER_URL=http://localhost:3000 CODEPROBE_SECRET=secret codeprobe scan
|
|
502
|
+
|
|
503
|
+
${chalk.bold('DOCS')}
|
|
504
|
+
https://github.com/codeprobe/codeprobe
|
|
505
|
+
`);
|
|
506
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Check if first arg is a scan command
|
|
510
|
+
const command = args[0];
|
|
511
|
+
if (command === 'scan' || !command.startsWith('--')) {
|
|
512
|
+
const scanArgs = command === 'scan' ? args.slice(1) : args;
|
|
513
|
+
scanCommand(scanArgs).catch((error) => {
|
|
514
|
+
console.error(chalk.red(`Fatal error: ${error instanceof Error ? error.message : String(error)}`));
|
|
515
|
+
process.exit(EXIT_CODES.SCAN_FAILED);
|
|
516
|
+
});
|
|
517
|
+
} else {
|
|
518
|
+
console.error(
|
|
519
|
+
chalk.red(`Unknown command: ${command}`)
|
|
520
|
+
);
|
|
521
|
+
console.log(`Run ${chalk.cyan('codeprobe --help')} for usage`);
|
|
522
|
+
process.exit(EXIT_CODES.SCAN_FAILED);
|
|
523
|
+
}
|
|
@@ -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 (Bright Data)
|
|
31
|
+
console.log("\x1b[33m[Bright Data]\x1b[0m 🔍 Scraping CVE data from NVD, Exploit-DB, Snyk...");
|
|
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();
|