codeslick-cli 1.0.4 → 1.1.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 +31 -13
- package/bin/codeslick.cjs +19 -1
- package/dist/packages/cli/src/commands/scan.d.ts +3 -0
- package/dist/packages/cli/src/commands/scan.d.ts.map +1 -1
- package/dist/packages/cli/src/commands/scan.js +103 -24
- package/dist/packages/cli/src/commands/scan.js.map +1 -1
- package/dist/packages/cli/src/reporters/cli-reporter.d.ts +28 -2
- package/dist/packages/cli/src/reporters/cli-reporter.d.ts.map +1 -1
- package/dist/packages/cli/src/reporters/cli-reporter.js +393 -4
- package/dist/packages/cli/src/reporters/cli-reporter.js.map +1 -1
- package/dist/packages/cli/src/scanner/local-scanner.d.ts +5 -1
- package/dist/packages/cli/src/scanner/local-scanner.d.ts.map +1 -1
- package/dist/packages/cli/src/scanner/local-scanner.js +110 -16
- package/dist/packages/cli/src/scanner/local-scanner.js.map +1 -1
- package/dist/src/lib/analyzers/java/security-checks/hardcoded-credentials.d.ts.map +1 -1
- package/dist/src/lib/analyzers/java/security-checks/hardcoded-credentials.js +24 -16
- package/dist/src/lib/analyzers/java/security-checks/hardcoded-credentials.js.map +1 -1
- package/dist/src/lib/analyzers/javascript/security-checks/ai-generated-code.d.ts.map +1 -1
- package/dist/src/lib/analyzers/javascript/security-checks/ai-generated-code.js +4 -12
- package/dist/src/lib/analyzers/javascript/security-checks/ai-generated-code.js.map +1 -1
- package/dist/src/lib/analyzers/javascript/security-checks/credential-crypto.d.ts.map +1 -1
- package/dist/src/lib/analyzers/javascript/security-checks/credential-crypto.js +22 -9
- package/dist/src/lib/analyzers/javascript/security-checks/credential-crypto.js.map +1 -1
- package/dist/src/lib/analyzers/javascript-analyzer.d.ts.map +1 -1
- package/dist/src/lib/analyzers/javascript-analyzer.js +28 -13
- package/dist/src/lib/analyzers/javascript-analyzer.js.map +1 -1
- package/dist/src/lib/analyzers/python/security-checks/credentials-crypto.d.ts.map +1 -1
- package/dist/src/lib/analyzers/python/security-checks/credentials-crypto.js +44 -18
- package/dist/src/lib/analyzers/python/security-checks/credentials-crypto.js.map +1 -1
- package/dist/src/lib/analyzers/python-analyzer.d.ts.map +1 -1
- package/dist/src/lib/analyzers/python-analyzer.js +21 -13
- package/dist/src/lib/analyzers/python-analyzer.js.map +1 -1
- package/dist/src/lib/analyzers/secrets/validators/context-checker.d.ts.map +1 -1
- package/dist/src/lib/analyzers/secrets/validators/context-checker.js +21 -0
- package/dist/src/lib/analyzers/secrets/validators/context-checker.js.map +1 -1
- package/dist/src/lib/analyzers/typescript/security-checks/ai-generated-code.d.ts.map +1 -1
- package/dist/src/lib/analyzers/typescript/security-checks/ai-generated-code.js +4 -12
- package/dist/src/lib/analyzers/typescript/security-checks/ai-generated-code.js.map +1 -1
- package/dist/src/lib/analyzers/typescript/security-checks/credentials-crypto.d.ts.map +1 -1
- package/dist/src/lib/analyzers/typescript/security-checks/credentials-crypto.js +25 -9
- package/dist/src/lib/analyzers/typescript/security-checks/credentials-crypto.js.map +1 -1
- package/dist/src/lib/analyzers/typescript/security-checks/security-misconfiguration.d.ts.map +1 -1
- package/dist/src/lib/analyzers/typescript/security-checks/security-misconfiguration.js +14 -4
- package/dist/src/lib/analyzers/typescript/security-checks/security-misconfiguration.js.map +1 -1
- package/dist/src/lib/analyzers/typescript/type-checker.d.ts +32 -0
- package/dist/src/lib/analyzers/typescript/type-checker.d.ts.map +1 -1
- package/dist/src/lib/analyzers/typescript/type-checker.js +264 -22
- package/dist/src/lib/analyzers/typescript/type-checker.js.map +1 -1
- package/dist/src/lib/analyzers/typescript-analyzer.d.ts.map +1 -1
- package/dist/src/lib/analyzers/typescript-analyzer.js +27 -23
- package/dist/src/lib/analyzers/typescript-analyzer.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/scan.ts +77 -25
- package/src/reporters/cli-reporter.ts +449 -4
- package/src/scanner/local-scanner.ts +132 -19
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import chalk from 'chalk';
|
|
18
18
|
import Table from 'cli-table3';
|
|
19
|
+
import { writeFileSync } from 'fs';
|
|
20
|
+
import { join } from 'path';
|
|
19
21
|
import type { FileScanResult } from '../scanner/local-scanner';
|
|
20
22
|
import type { SecurityVulnerability } from '../../../../src/lib/analyzers/types';
|
|
21
23
|
|
|
@@ -100,17 +102,28 @@ export function printSummaryTable(results: FileScanResult[]): void {
|
|
|
100
102
|
/**
|
|
101
103
|
* Print detailed vulnerabilities for a file
|
|
102
104
|
*/
|
|
103
|
-
export function printFileVulnerabilities(result: FileScanResult): void {
|
|
105
|
+
export function printFileVulnerabilities(result: FileScanResult, showAll = false): void {
|
|
104
106
|
const vulnerabilities = result.result.security?.vulnerabilities || [];
|
|
105
107
|
|
|
106
108
|
if (vulnerabilities.length === 0) {
|
|
107
109
|
return;
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
// Filter to only HIGH and CRITICAL by default (unless showAll is true)
|
|
113
|
+
const importantVulns = showAll
|
|
114
|
+
? vulnerabilities
|
|
115
|
+
: vulnerabilities.filter((v: SecurityVulnerability) =>
|
|
116
|
+
v.severity.toLowerCase() === 'critical' || v.severity.toLowerCase() === 'high'
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (importantVulns.length === 0) {
|
|
120
|
+
return; // Skip file if no important vulnerabilities
|
|
121
|
+
}
|
|
122
|
+
|
|
110
123
|
console.log('');
|
|
111
124
|
console.log(chalk.bold(`📄 ${result.relativePath}`) + chalk.gray(` (${result.language})`));
|
|
112
125
|
|
|
113
|
-
|
|
126
|
+
importantVulns.forEach((vuln: SecurityVulnerability) => {
|
|
114
127
|
const colorFn = getSeverityColor(vuln.severity);
|
|
115
128
|
const symbol = getSeveritySymbol(vuln.severity);
|
|
116
129
|
|
|
@@ -141,8 +154,10 @@ export function printFileVulnerabilities(result: FileScanResult): void {
|
|
|
141
154
|
|
|
142
155
|
/**
|
|
143
156
|
* Print all scan results with details
|
|
157
|
+
* @param results - Scan results
|
|
158
|
+
* @param verbose - If true, show all severities; if false, only HIGH and CRITICAL
|
|
144
159
|
*/
|
|
145
|
-
export function printDetailedResults(results: FileScanResult[]): void {
|
|
160
|
+
export function printDetailedResults(results: FileScanResult[], verbose = false): void {
|
|
146
161
|
console.log('');
|
|
147
162
|
console.log(chalk.bold.underline('Detailed Results'));
|
|
148
163
|
|
|
@@ -157,9 +172,21 @@ export function printDetailedResults(results: FileScanResult[]): void {
|
|
|
157
172
|
return;
|
|
158
173
|
}
|
|
159
174
|
|
|
175
|
+
// Count total issues by severity
|
|
176
|
+
const totalMedium = results.reduce((sum, r) => sum + r.medium, 0);
|
|
177
|
+
const totalLow = results.reduce((sum, r) => sum + r.low, 0);
|
|
178
|
+
|
|
160
179
|
resultsWithIssues.forEach((result) => {
|
|
161
|
-
printFileVulnerabilities(result);
|
|
180
|
+
printFileVulnerabilities(result, verbose);
|
|
162
181
|
});
|
|
182
|
+
|
|
183
|
+
// Show note about hidden issues if not verbose
|
|
184
|
+
if (!verbose && (totalMedium > 0 || totalLow > 0)) {
|
|
185
|
+
console.log('');
|
|
186
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
187
|
+
console.log(chalk.gray(` Also found: ${totalMedium} MEDIUM, ${totalLow} LOW issues`));
|
|
188
|
+
console.log(chalk.gray(` Use ${chalk.white('--verbose')} to see all issues`));
|
|
189
|
+
}
|
|
163
190
|
}
|
|
164
191
|
|
|
165
192
|
/**
|
|
@@ -251,6 +278,201 @@ export function printCommitAllowed(): void {
|
|
|
251
278
|
console.log('');
|
|
252
279
|
}
|
|
253
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Print summary table grouped by language
|
|
283
|
+
* Shows files scanned, issues found, and critical count per language
|
|
284
|
+
*/
|
|
285
|
+
export function printLanguageSummary(results: FileScanResult[]): void {
|
|
286
|
+
// Group results by language
|
|
287
|
+
const byLanguage = new Map<string, { files: number; issues: number; critical: number; high: number; medium: number; low: number }>();
|
|
288
|
+
|
|
289
|
+
for (const result of results) {
|
|
290
|
+
const lang = result.language;
|
|
291
|
+
const existing = byLanguage.get(lang) || { files: 0, issues: 0, critical: 0, high: 0, medium: 0, low: 0 };
|
|
292
|
+
existing.files++;
|
|
293
|
+
existing.critical += result.critical;
|
|
294
|
+
existing.high += result.high;
|
|
295
|
+
existing.medium += result.medium;
|
|
296
|
+
existing.low += result.low;
|
|
297
|
+
existing.issues += result.critical + result.high + result.medium + result.low;
|
|
298
|
+
byLanguage.set(lang, existing);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (byLanguage.size === 0) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
console.log('');
|
|
306
|
+
console.log(chalk.bold('Language Breakdown'));
|
|
307
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
308
|
+
|
|
309
|
+
const table = new Table({
|
|
310
|
+
head: [
|
|
311
|
+
chalk.bold('Language'),
|
|
312
|
+
chalk.bold('Files'),
|
|
313
|
+
chalk.bold('Issues'),
|
|
314
|
+
chalk.bold.red('Critical'),
|
|
315
|
+
chalk.bold.red('High'),
|
|
316
|
+
chalk.bold.yellow('Medium')
|
|
317
|
+
],
|
|
318
|
+
colWidths: [14, 8, 10, 10, 8, 9],
|
|
319
|
+
style: {
|
|
320
|
+
head: [],
|
|
321
|
+
border: ['gray'],
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Sort by critical count (highest first)
|
|
326
|
+
const sorted = Array.from(byLanguage.entries()).sort((a, b) => b[1].critical - a[1].critical);
|
|
327
|
+
|
|
328
|
+
for (const [lang, stats] of sorted) {
|
|
329
|
+
const langDisplay = lang.charAt(0).toUpperCase() + lang.slice(1);
|
|
330
|
+
table.push([
|
|
331
|
+
chalk.white(langDisplay),
|
|
332
|
+
chalk.white(stats.files.toString()),
|
|
333
|
+
stats.issues > 0 ? chalk.yellow(stats.issues.toString()) : chalk.gray('0'),
|
|
334
|
+
stats.critical > 0 ? chalk.red.bold(stats.critical.toString()) : chalk.gray('0'),
|
|
335
|
+
stats.high > 0 ? chalk.red(stats.high.toString()) : chalk.gray('0'),
|
|
336
|
+
stats.medium > 0 ? chalk.yellow(stats.medium.toString()) : chalk.gray('0')
|
|
337
|
+
]);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
console.log(table.toString());
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Print unsupported/skipped files summary
|
|
345
|
+
* Groups by extension and shows counts
|
|
346
|
+
*/
|
|
347
|
+
export function printUnsupportedFiles(skippedFiles: string[]): void {
|
|
348
|
+
if (skippedFiles.length === 0) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Group by extension
|
|
353
|
+
const byExtension = new Map<string, number>();
|
|
354
|
+
|
|
355
|
+
for (const file of skippedFiles) {
|
|
356
|
+
const ext = file.includes('.') ? '.' + file.split('.').pop()?.toLowerCase() : '(no ext)';
|
|
357
|
+
byExtension.set(ext, (byExtension.get(ext) || 0) + 1);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
console.log('');
|
|
361
|
+
console.log(chalk.bold('Skipped Files') + chalk.gray(` (${skippedFiles.length} total)`));
|
|
362
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
363
|
+
|
|
364
|
+
// Sort by count (highest first)
|
|
365
|
+
const sorted = Array.from(byExtension.entries()).sort((a, b) => b[1] - a[1]);
|
|
366
|
+
|
|
367
|
+
// Show top 5 extensions
|
|
368
|
+
const top5 = sorted.slice(0, 5);
|
|
369
|
+
for (const [ext, count] of top5) {
|
|
370
|
+
const description = getExtensionDescription(ext);
|
|
371
|
+
console.log(chalk.gray(` ${ext.padEnd(10)} ${count.toString().padStart(4)} files`) + chalk.gray.dim(` (${description})`));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (sorted.length > 5) {
|
|
375
|
+
const remaining = sorted.slice(5).reduce((sum, [, count]) => sum + count, 0);
|
|
376
|
+
console.log(chalk.gray(` ... ${remaining.toString().padStart(4)} files (other)`));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
console.log('');
|
|
380
|
+
console.log(chalk.gray.dim(' Supported: .js, .jsx, .ts, .tsx, .py, .java'));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get description for file extension
|
|
385
|
+
*/
|
|
386
|
+
function getExtensionDescription(ext: string): string {
|
|
387
|
+
const descriptions: Record<string, string> = {
|
|
388
|
+
'.md': 'documentation',
|
|
389
|
+
'.json': 'config',
|
|
390
|
+
'.css': 'styles',
|
|
391
|
+
'.scss': 'styles',
|
|
392
|
+
'.html': 'markup',
|
|
393
|
+
'.yml': 'config',
|
|
394
|
+
'.yaml': 'config',
|
|
395
|
+
'.lock': 'lockfile',
|
|
396
|
+
'.svg': 'image',
|
|
397
|
+
'.png': 'image',
|
|
398
|
+
'.jpg': 'image',
|
|
399
|
+
'.gif': 'image',
|
|
400
|
+
'.txt': 'text',
|
|
401
|
+
'.sh': 'shell script',
|
|
402
|
+
'.env': 'environment',
|
|
403
|
+
'.gitignore': 'git config',
|
|
404
|
+
'(no ext)': 'no extension',
|
|
405
|
+
};
|
|
406
|
+
return descriptions[ext] || 'other';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Print Top 10 most critical issues
|
|
411
|
+
* Shows the highest priority issues that should be fixed first
|
|
412
|
+
*/
|
|
413
|
+
export function printTop10Critical(results: FileScanResult[]): void {
|
|
414
|
+
// Collect all vulnerabilities with file info
|
|
415
|
+
const allVulns: Array<{ file: string; vuln: SecurityVulnerability }> = [];
|
|
416
|
+
|
|
417
|
+
for (const result of results) {
|
|
418
|
+
const vulns = result.result.security?.vulnerabilities || [];
|
|
419
|
+
for (const vuln of vulns) {
|
|
420
|
+
allVulns.push({ file: result.relativePath, vuln });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (allVulns.length === 0) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Sort by severity (critical > high > medium > low) then by CVSS score
|
|
429
|
+
const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
430
|
+
allVulns.sort((a, b) => {
|
|
431
|
+
const severityDiff = (severityOrder[a.vuln.severity.toLowerCase()] || 4) - (severityOrder[b.vuln.severity.toLowerCase()] || 4);
|
|
432
|
+
if (severityDiff !== 0) return severityDiff;
|
|
433
|
+
return (b.vuln.cvssScore || 0) - (a.vuln.cvssScore || 0);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Take top 10
|
|
437
|
+
const top10 = allVulns.slice(0, 10);
|
|
438
|
+
|
|
439
|
+
console.log('');
|
|
440
|
+
console.log(chalk.bold('Top 10 Critical Issues') + chalk.gray(' (Fix These First)'));
|
|
441
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
442
|
+
|
|
443
|
+
top10.forEach((item, index) => {
|
|
444
|
+
const colorFn = getSeverityColor(item.vuln.severity);
|
|
445
|
+
const symbol = getSeveritySymbol(item.vuln.severity);
|
|
446
|
+
const cvss = item.vuln.cvssScore ? chalk.gray(` CVSS ${item.vuln.cvssScore}`) : '';
|
|
447
|
+
|
|
448
|
+
console.log('');
|
|
449
|
+
console.log(
|
|
450
|
+
chalk.white.bold(`${(index + 1).toString().padStart(2)}.`) +
|
|
451
|
+
` ${colorFn(symbol)} ${colorFn(item.vuln.severity.toUpperCase())}` +
|
|
452
|
+
cvss
|
|
453
|
+
);
|
|
454
|
+
console.log(chalk.cyan(` ${item.file}:${item.vuln.line}`));
|
|
455
|
+
console.log(chalk.white(` ${truncateMessage(item.vuln.message, 60)}`));
|
|
456
|
+
|
|
457
|
+
if (item.vuln.suggestion) {
|
|
458
|
+
console.log(chalk.green(` Fix: ${truncateMessage(item.vuln.suggestion, 55)}`));
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
if (allVulns.length > 10) {
|
|
463
|
+
console.log('');
|
|
464
|
+
console.log(chalk.gray(` ... and ${allVulns.length - 10} more issues`));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Truncate message to max length
|
|
470
|
+
*/
|
|
471
|
+
function truncateMessage(message: string, maxLength: number): string {
|
|
472
|
+
if (message.length <= maxLength) return message;
|
|
473
|
+
return message.substring(0, maxLength - 3) + '...';
|
|
474
|
+
}
|
|
475
|
+
|
|
254
476
|
/**
|
|
255
477
|
* Output results as JSON
|
|
256
478
|
*/
|
|
@@ -280,3 +502,226 @@ export function printJSONResults(results: FileScanResult[]): void {
|
|
|
280
502
|
|
|
281
503
|
console.log(JSON.stringify(output, null, 2));
|
|
282
504
|
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Generate Markdown report file
|
|
508
|
+
* Returns the path to the generated report
|
|
509
|
+
*/
|
|
510
|
+
export function generateMarkdownReport(
|
|
511
|
+
results: FileScanResult[],
|
|
512
|
+
skippedFiles: string[],
|
|
513
|
+
duration: number
|
|
514
|
+
): string {
|
|
515
|
+
const now = new Date();
|
|
516
|
+
const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
517
|
+
const filename = `codeslick-report-${timestamp}.md`;
|
|
518
|
+
const filepath = join(process.cwd(), filename);
|
|
519
|
+
|
|
520
|
+
// Calculate totals
|
|
521
|
+
const totalFiles = results.length;
|
|
522
|
+
const totalCritical = results.reduce((sum, r) => sum + r.critical, 0);
|
|
523
|
+
const totalHigh = results.reduce((sum, r) => sum + r.high, 0);
|
|
524
|
+
const totalMedium = results.reduce((sum, r) => sum + r.medium, 0);
|
|
525
|
+
const totalLow = results.reduce((sum, r) => sum + r.low, 0);
|
|
526
|
+
const totalVulns = totalCritical + totalHigh + totalMedium + totalLow;
|
|
527
|
+
const filesWithIssues = results.filter(r => r.critical + r.high + r.medium + r.low > 0).length;
|
|
528
|
+
|
|
529
|
+
// Group by language
|
|
530
|
+
const byLanguage = new Map<string, { files: number; critical: number; high: number; medium: number; low: number }>();
|
|
531
|
+
for (const result of results) {
|
|
532
|
+
const lang = result.language;
|
|
533
|
+
const existing = byLanguage.get(lang) || { files: 0, critical: 0, high: 0, medium: 0, low: 0 };
|
|
534
|
+
existing.files++;
|
|
535
|
+
existing.critical += result.critical;
|
|
536
|
+
existing.high += result.high;
|
|
537
|
+
existing.medium += result.medium;
|
|
538
|
+
existing.low += result.low;
|
|
539
|
+
byLanguage.set(lang, existing);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Collect all vulnerabilities for Top 10
|
|
543
|
+
const allVulns: Array<{ file: string; line: number; vuln: SecurityVulnerability }> = [];
|
|
544
|
+
for (const result of results) {
|
|
545
|
+
const vulns = result.result.security?.vulnerabilities || [];
|
|
546
|
+
for (const vuln of vulns) {
|
|
547
|
+
allVulns.push({ file: result.relativePath, line: vuln.line || 0, vuln });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Sort by severity
|
|
552
|
+
const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
553
|
+
allVulns.sort((a, b) => {
|
|
554
|
+
const diff = (severityOrder[a.vuln.severity.toLowerCase()] || 4) - (severityOrder[b.vuln.severity.toLowerCase()] || 4);
|
|
555
|
+
if (diff !== 0) return diff;
|
|
556
|
+
return (b.vuln.cvssScore || 0) - (a.vuln.cvssScore || 0);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// Build markdown content
|
|
560
|
+
let md = `# CodeSlick Security Report
|
|
561
|
+
|
|
562
|
+
**Date:** ${now.toLocaleDateString()} ${now.toLocaleTimeString()}
|
|
563
|
+
**Repository:** ${process.cwd()}
|
|
564
|
+
**Scan Duration:** ${(duration / 1000).toFixed(1)}s
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
## Summary
|
|
569
|
+
|
|
570
|
+
| Metric | Count |
|
|
571
|
+
|--------|-------|
|
|
572
|
+
| Files Scanned | ${totalFiles} |
|
|
573
|
+
| Files with Issues | ${filesWithIssues} |
|
|
574
|
+
| Total Vulnerabilities | ${totalVulns} |
|
|
575
|
+
| **CRITICAL** | ${totalCritical} |
|
|
576
|
+
| **HIGH** | ${totalHigh} |
|
|
577
|
+
| MEDIUM | ${totalMedium} |
|
|
578
|
+
| LOW | ${totalLow} |
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
## By Language
|
|
583
|
+
|
|
584
|
+
| Language | Files | Critical | High | Medium | Low |
|
|
585
|
+
|----------|-------|----------|------|--------|-----|
|
|
586
|
+
`;
|
|
587
|
+
|
|
588
|
+
// Add language rows
|
|
589
|
+
const sortedLangs = Array.from(byLanguage.entries()).sort((a, b) => b[1].critical - a[1].critical);
|
|
590
|
+
for (const [lang, stats] of sortedLangs) {
|
|
591
|
+
md += `| ${lang.charAt(0).toUpperCase() + lang.slice(1)} | ${stats.files} | ${stats.critical} | ${stats.high} | ${stats.medium} | ${stats.low} |\n`;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Top 10 Critical Issues
|
|
595
|
+
md += `
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
## Top 10 Critical Issues (Fix These First)
|
|
599
|
+
|
|
600
|
+
`;
|
|
601
|
+
|
|
602
|
+
const top10 = allVulns.slice(0, 10);
|
|
603
|
+
if (top10.length === 0) {
|
|
604
|
+
md += `No critical issues found.\n`;
|
|
605
|
+
} else {
|
|
606
|
+
for (let i = 0; i < top10.length; i++) {
|
|
607
|
+
const item = top10[i];
|
|
608
|
+
const cvss = item.vuln.cvssScore ? ` (CVSS ${item.vuln.cvssScore})` : '';
|
|
609
|
+
md += `### ${i + 1}. ${item.vuln.severity.toUpperCase()}${cvss}
|
|
610
|
+
|
|
611
|
+
**File:** \`${item.file}:${item.line}\`
|
|
612
|
+
|
|
613
|
+
**Issue:** ${item.vuln.message}
|
|
614
|
+
|
|
615
|
+
`;
|
|
616
|
+
if (item.vuln.suggestion) {
|
|
617
|
+
md += `**Fix:** ${item.vuln.suggestion}\n\n`;
|
|
618
|
+
}
|
|
619
|
+
if (item.vuln.owasp && item.vuln.owasp !== 'N/A') {
|
|
620
|
+
md += `**OWASP:** ${item.vuln.owasp}\n\n`;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (allVulns.length > 10) {
|
|
626
|
+
md += `\n*...and ${allVulns.length - 10} more issues*\n`;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Skipped files
|
|
630
|
+
if (skippedFiles.length > 0) {
|
|
631
|
+
const byExt = new Map<string, number>();
|
|
632
|
+
for (const file of skippedFiles) {
|
|
633
|
+
const ext = file.includes('.') ? '.' + file.split('.').pop()?.toLowerCase() : '(no ext)';
|
|
634
|
+
byExt.set(ext, (byExt.get(ext) || 0) + 1);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
md += `
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## Skipped Files (${skippedFiles.length} total)
|
|
641
|
+
|
|
642
|
+
| Extension | Count |
|
|
643
|
+
|-----------|-------|
|
|
644
|
+
`;
|
|
645
|
+
const sortedExts = Array.from(byExt.entries()).sort((a, b) => b[1] - a[1]);
|
|
646
|
+
for (const [ext, count] of sortedExts.slice(0, 10)) {
|
|
647
|
+
md += `| ${ext} | ${count} |\n`;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
md += `\n*Supported: .js, .jsx, .ts, .tsx, .py, .java*\n`;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Files with issues (detailed)
|
|
654
|
+
const filesWithProblems = results.filter(r => r.critical + r.high + r.medium + r.low > 0);
|
|
655
|
+
if (filesWithProblems.length > 0) {
|
|
656
|
+
md += `
|
|
657
|
+
---
|
|
658
|
+
|
|
659
|
+
## Files with Issues (${filesWithProblems.length} files)
|
|
660
|
+
|
|
661
|
+
<details>
|
|
662
|
+
<summary>Click to expand full file list</summary>
|
|
663
|
+
|
|
664
|
+
| File | Critical | High | Medium | Low |
|
|
665
|
+
|------|----------|------|--------|-----|
|
|
666
|
+
`;
|
|
667
|
+
// Sort by critical count
|
|
668
|
+
filesWithProblems.sort((a, b) => b.critical - a.critical || b.high - a.high);
|
|
669
|
+
for (const f of filesWithProblems) {
|
|
670
|
+
md += `| ${f.relativePath} | ${f.critical} | ${f.high} | ${f.medium} | ${f.low} |\n`;
|
|
671
|
+
}
|
|
672
|
+
md += `\n</details>\n`;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Footer
|
|
676
|
+
md += `
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
*Generated by [CodeSlick](https://codeslick.dev) Security Scanner*
|
|
680
|
+
`;
|
|
681
|
+
|
|
682
|
+
// Write file
|
|
683
|
+
writeFileSync(filepath, md, 'utf-8');
|
|
684
|
+
|
|
685
|
+
return filename;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Print brief summary for screen (when report is generated)
|
|
690
|
+
*/
|
|
691
|
+
export function printBriefSummary(
|
|
692
|
+
results: FileScanResult[],
|
|
693
|
+
reportPath: string,
|
|
694
|
+
duration: number
|
|
695
|
+
): void {
|
|
696
|
+
const totalCritical = results.reduce((sum, r) => sum + r.critical, 0);
|
|
697
|
+
const totalHigh = results.reduce((sum, r) => sum + r.high, 0);
|
|
698
|
+
const totalMedium = results.reduce((sum, r) => sum + r.medium, 0);
|
|
699
|
+
const totalLow = results.reduce((sum, r) => sum + r.low, 0);
|
|
700
|
+
const totalVulns = totalCritical + totalHigh + totalMedium + totalLow;
|
|
701
|
+
|
|
702
|
+
console.log('');
|
|
703
|
+
console.log(chalk.bold('Scan Complete') + chalk.gray(` (${(duration / 1000).toFixed(1)}s)`));
|
|
704
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
705
|
+
console.log('');
|
|
706
|
+
console.log(` Files scanned: ${chalk.white(results.length.toString())}`);
|
|
707
|
+
console.log(` Vulnerabilities: ${totalVulns > 0 ? chalk.red(totalVulns.toString()) : chalk.green('0')}`);
|
|
708
|
+
console.log('');
|
|
709
|
+
|
|
710
|
+
if (totalCritical > 0) {
|
|
711
|
+
console.log(chalk.red.bold(` ✖ ${totalCritical} CRITICAL`));
|
|
712
|
+
}
|
|
713
|
+
if (totalHigh > 0) {
|
|
714
|
+
console.log(chalk.red(` ⚠ ${totalHigh} HIGH`));
|
|
715
|
+
}
|
|
716
|
+
if (totalMedium > 0) {
|
|
717
|
+
console.log(chalk.yellow(` ◆ ${totalMedium} MEDIUM`));
|
|
718
|
+
}
|
|
719
|
+
if (totalLow > 0) {
|
|
720
|
+
console.log(chalk.blue(` ○ ${totalLow} LOW`));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
console.log('');
|
|
724
|
+
console.log(chalk.green.bold(` Report: ${reportPath}`));
|
|
725
|
+
console.log(chalk.gray(` Open: code ${reportPath}`));
|
|
726
|
+
console.log('');
|
|
727
|
+
}
|