/** * Scan command - Run security scans */ import { Command } from 'commander' import * as path from 'path' interface ScanOptions { tools?: string output?: string format?: string severity?: string } export const scanCommand = new Command('scan') .description('Run security scans on your codebase') .argument('[path]', 'Path to scan', '.') .option('-t, --tools ', 'Comma-separated list of tools (gitleaks,semgrep,bandit,trivy,grype,syft)', 'all') .option('-o, --output ', 'Output file path') .option('-f, --format ', 'Output format (json, sarif, table)', 'table') .option('-s, --severity ', 'Minimum severity to report (low, medium, high, critical)', 'low') .action(async (scanPath: string, options: ScanOptions) => { const chalk = (await import('chalk')).default const ora = (await import('ora')).default console.log(chalk.bold.blue('\nšŸ”’ BreakPilot Security Scanner\n')) const targetPath = path.resolve(scanPath) console.log(chalk.gray(`Scanning: ${targetPath}\n`)) const tools = options.tools === 'all' ? ['gitleaks', 'semgrep', 'bandit', 'trivy', 'grype', 'syft'] : options.tools!.split(',').map(t => t.trim()) const results: ScanResult[] = [] for (const tool of tools) { const spinner = ora(`Running ${tool}...`).start() try { // Simulate running the tool await sleep(1500) const toolResult = simulateToolResult(tool) results.push(toolResult) if (toolResult.findings.length > 0) { spinner.warn(`${tool}: ${toolResult.findings.length} findings`) } else { spinner.succeed(`${tool}: No issues found`) } } catch (error) { spinner.fail(`${tool}: Failed`) } } // Summary console.log(chalk.bold('\nšŸ“Š Scan Summary\n')) const allFindings = results.flatMap(r => r.findings) const bySeverity = { critical: allFindings.filter(f => f.severity === 'CRITICAL'), high: allFindings.filter(f => f.severity === 'HIGH'), medium: allFindings.filter(f => f.severity === 'MEDIUM'), low: allFindings.filter(f => f.severity === 'LOW'), } if (bySeverity.critical.length > 0) { console.log(chalk.red(` šŸ”“ Critical: ${bySeverity.critical.length}`)) } if (bySeverity.high.length > 0) { console.log(chalk.red(` 🟠 High: ${bySeverity.high.length}`)) } if (bySeverity.medium.length > 0) { console.log(chalk.yellow(` 🟔 Medium: ${bySeverity.medium.length}`)) } if (bySeverity.low.length > 0) { console.log(chalk.gray(` 🟢 Low: ${bySeverity.low.length}`)) } if (allFindings.length === 0) { console.log(chalk.green(' āœ… No security issues found!')) } else { console.log(chalk.gray(`\n Total: ${allFindings.length} findings`)) // Show top findings if (options.format === 'table') { console.log(chalk.bold('\nšŸ“‹ Top Findings\n')) const topFindings = allFindings .sort((a, b) => severityOrder(b.severity) - severityOrder(a.severity)) .slice(0, 10) for (const finding of topFindings) { const severityColor = getSeverityColor(finding.severity) console.log( ` ${severityColor(finding.severity.padEnd(8))} ` + `${chalk.gray(finding.tool.padEnd(10))} ` + `${finding.title}` ) if (finding.file) { console.log(chalk.gray(` └─ ${finding.file}:${finding.line || '?'}`)) } } if (allFindings.length > 10) { console.log(chalk.gray(`\n ... and ${allFindings.length - 10} more findings`)) } } } // Write output if requested if (options.output) { const fs = await import('fs') const output = { scanDate: new Date().toISOString(), targetPath, tools, summary: { total: allFindings.length, critical: bySeverity.critical.length, high: bySeverity.high.length, medium: bySeverity.medium.length, low: bySeverity.low.length, }, findings: allFindings, } fs.writeFileSync(options.output, JSON.stringify(output, null, 2)) console.log(chalk.gray(`\nResults written to: ${options.output}`)) } // Exit with error if critical findings if (bySeverity.critical.length > 0 || bySeverity.high.length > 0) { process.exit(1) } }) interface Finding { id: string tool: string severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' title: string description?: string file?: string line?: number recommendation?: string } interface ScanResult { tool: string findings: Finding[] } function simulateToolResult(tool: string): ScanResult { // Simulate some findings for demonstration const findings: Finding[] = [] switch (tool) { case 'gitleaks': // Secrets detection - usually finds nothing in clean repos break case 'semgrep': findings.push({ id: 'semgrep-1', tool: 'semgrep', severity: 'MEDIUM', title: 'Potential SQL injection', description: 'User input used in SQL query without parameterization', file: 'src/db/queries.ts', line: 42, recommendation: 'Use parameterized queries instead of string concatenation', }) break case 'bandit': // Python security - skip if not a Python project break case 'trivy': findings.push({ id: 'trivy-1', tool: 'trivy', severity: 'HIGH', title: 'CVE-2024-1234 in lodash@4.17.20', description: 'Prototype pollution vulnerability', recommendation: 'Upgrade to lodash@4.17.21 or higher', }) break case 'grype': findings.push({ id: 'grype-1', tool: 'grype', severity: 'LOW', title: 'Outdated dependency: axios@0.21.0', recommendation: 'Update to latest version', }) break case 'syft': // SBOM generation - no findings, just metadata break } return { tool, findings } } function severityOrder(severity: string): number { switch (severity) { case 'CRITICAL': return 4 case 'HIGH': return 3 case 'MEDIUM': return 2 case 'LOW': return 1 default: return 0 } } function getSeverityColor(severity: string): (text: string) => string { const chalk = require('chalk') switch (severity) { case 'CRITICAL': return chalk.red.bold case 'HIGH': return chalk.red case 'MEDIUM': return chalk.yellow case 'LOW': return chalk.gray default: return chalk.white } } function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)) }