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.
Files changed (55) hide show
  1. package/README.md +31 -13
  2. package/bin/codeslick.cjs +19 -1
  3. package/dist/packages/cli/src/commands/scan.d.ts +3 -0
  4. package/dist/packages/cli/src/commands/scan.d.ts.map +1 -1
  5. package/dist/packages/cli/src/commands/scan.js +103 -24
  6. package/dist/packages/cli/src/commands/scan.js.map +1 -1
  7. package/dist/packages/cli/src/reporters/cli-reporter.d.ts +28 -2
  8. package/dist/packages/cli/src/reporters/cli-reporter.d.ts.map +1 -1
  9. package/dist/packages/cli/src/reporters/cli-reporter.js +393 -4
  10. package/dist/packages/cli/src/reporters/cli-reporter.js.map +1 -1
  11. package/dist/packages/cli/src/scanner/local-scanner.d.ts +5 -1
  12. package/dist/packages/cli/src/scanner/local-scanner.d.ts.map +1 -1
  13. package/dist/packages/cli/src/scanner/local-scanner.js +110 -16
  14. package/dist/packages/cli/src/scanner/local-scanner.js.map +1 -1
  15. package/dist/src/lib/analyzers/java/security-checks/hardcoded-credentials.d.ts.map +1 -1
  16. package/dist/src/lib/analyzers/java/security-checks/hardcoded-credentials.js +24 -16
  17. package/dist/src/lib/analyzers/java/security-checks/hardcoded-credentials.js.map +1 -1
  18. package/dist/src/lib/analyzers/javascript/security-checks/ai-generated-code.d.ts.map +1 -1
  19. package/dist/src/lib/analyzers/javascript/security-checks/ai-generated-code.js +4 -12
  20. package/dist/src/lib/analyzers/javascript/security-checks/ai-generated-code.js.map +1 -1
  21. package/dist/src/lib/analyzers/javascript/security-checks/credential-crypto.d.ts.map +1 -1
  22. package/dist/src/lib/analyzers/javascript/security-checks/credential-crypto.js +22 -9
  23. package/dist/src/lib/analyzers/javascript/security-checks/credential-crypto.js.map +1 -1
  24. package/dist/src/lib/analyzers/javascript-analyzer.d.ts.map +1 -1
  25. package/dist/src/lib/analyzers/javascript-analyzer.js +28 -13
  26. package/dist/src/lib/analyzers/javascript-analyzer.js.map +1 -1
  27. package/dist/src/lib/analyzers/python/security-checks/credentials-crypto.d.ts.map +1 -1
  28. package/dist/src/lib/analyzers/python/security-checks/credentials-crypto.js +44 -18
  29. package/dist/src/lib/analyzers/python/security-checks/credentials-crypto.js.map +1 -1
  30. package/dist/src/lib/analyzers/python-analyzer.d.ts.map +1 -1
  31. package/dist/src/lib/analyzers/python-analyzer.js +21 -13
  32. package/dist/src/lib/analyzers/python-analyzer.js.map +1 -1
  33. package/dist/src/lib/analyzers/secrets/validators/context-checker.d.ts.map +1 -1
  34. package/dist/src/lib/analyzers/secrets/validators/context-checker.js +21 -0
  35. package/dist/src/lib/analyzers/secrets/validators/context-checker.js.map +1 -1
  36. package/dist/src/lib/analyzers/typescript/security-checks/ai-generated-code.d.ts.map +1 -1
  37. package/dist/src/lib/analyzers/typescript/security-checks/ai-generated-code.js +4 -12
  38. package/dist/src/lib/analyzers/typescript/security-checks/ai-generated-code.js.map +1 -1
  39. package/dist/src/lib/analyzers/typescript/security-checks/credentials-crypto.d.ts.map +1 -1
  40. package/dist/src/lib/analyzers/typescript/security-checks/credentials-crypto.js +25 -9
  41. package/dist/src/lib/analyzers/typescript/security-checks/credentials-crypto.js.map +1 -1
  42. package/dist/src/lib/analyzers/typescript/security-checks/security-misconfiguration.d.ts.map +1 -1
  43. package/dist/src/lib/analyzers/typescript/security-checks/security-misconfiguration.js +14 -4
  44. package/dist/src/lib/analyzers/typescript/security-checks/security-misconfiguration.js.map +1 -1
  45. package/dist/src/lib/analyzers/typescript/type-checker.d.ts +32 -0
  46. package/dist/src/lib/analyzers/typescript/type-checker.d.ts.map +1 -1
  47. package/dist/src/lib/analyzers/typescript/type-checker.js +264 -22
  48. package/dist/src/lib/analyzers/typescript/type-checker.js.map +1 -1
  49. package/dist/src/lib/analyzers/typescript-analyzer.d.ts.map +1 -1
  50. package/dist/src/lib/analyzers/typescript-analyzer.js +27 -23
  51. package/dist/src/lib/analyzers/typescript-analyzer.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/commands/scan.ts +77 -25
  54. package/src/reporters/cli-reporter.ts +449 -4
  55. 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
- vulnerabilities.forEach((vuln: SecurityVulnerability, index: number) => {
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
+ }