UNPKG

tops-bmad

Version:

CLI tool to install BMAD workflow files into any project with integrated Shai-Hulud 2.0 security scanning

255 lines (213 loc) 7.17 kB
#!/usr/bin/env node /** * TOPS BMAD Security Scanner - Unified Scanner * * Scans projects - can scan a single project (by path) or all projects recursively * * Usage: * # Scan specific project (positional argument) * node scan-workspace.js <project-path> [options] * * # Scan specific project (flag) * node scan-workspace.js --project <path> [options] * * # Scan all projects * node scan-workspace.js --recursive [options] * * # Scan current directory (default) * node scan-workspace.js [options] * * Options: * <project-path> Project path as positional argument * --project <path> Scan a specific project * --recursive Scan all projects in workspace * --output-format <text|json|sarif> Output format (default: text) * --scan-lockfiles Scan lockfiles (default: true) * --fail-on-critical Fail on critical findings (default: false) * --fail-on-high Fail on high/critical findings (default: false) * --fail-on-any Fail on any findings (default: false) */ import { readFileSync, existsSync } from 'fs'; import { resolve, join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { spawn } from 'child_process'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Get paths const workspaceRoot = resolve(__dirname, '..', '..'); const detectorPath = resolve(__dirname, '..', 'Shai-Hulud-2.0-Detector'); const detectorIndexPath = join(detectorPath, 'dist', 'index.js'); const configPath = join(__dirname, '..', 'config', 'organization-config.json'); // Load organization config if available let orgConfig = { projects: [] }; if (existsSync(configPath)) { try { orgConfig = JSON.parse(readFileSync(configPath, 'utf8')); } catch (error) { console.warn('⚠️ Could not load organization config:', error.message); } } // Parse command line arguments function parseArgs() { const args = process.argv.slice(2); const options = { project: null, recursive: false, outputFormat: 'text', scanLockfiles: true, failOnCritical: false, failOnHigh: false, failOnAny: false }; // Check for positional argument (project path) first if (args.length > 0 && !args[0].startsWith('--')) { options.project = resolve(args[0]); args.shift(); // Remove the positional argument } for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--project' && i + 1 < args.length) { options.project = resolve(workspaceRoot, args[++i]); } else if (arg === '--recursive') { options.recursive = true; } else if (arg === '--output-format' && i + 1 < args.length) { const format = args[++i]; if (['text', 'json', 'sarif'].includes(format)) { options.outputFormat = format; } } else if (arg === '--scan-lockfiles') { options.scanLockfiles = true; } else if (arg === '--no-scan-lockfiles') { options.scanLockfiles = false; } else if (arg === '--fail-on-critical') { options.failOnCritical = true; } else if (arg === '--fail-on-high') { options.failOnHigh = true; } else if (arg === '--fail-on-any') { options.failOnAny = true; } } // Default to current directory if no project specified if (!options.project && !options.recursive) { options.project = process.cwd(); } return options; } // Scan a single project async function scanSingleProject(projectPath, options) { if (!existsSync(projectPath)) { console.error(`❌ Project path does not exist: ${projectPath}`); return { success: false, error: 'Path not found' }; } const stats = await import('fs').then(m => m.promises.stat(projectPath)); if (!stats.isDirectory()) { console.error(`❌ Project path is not a directory: ${projectPath}`); return { success: false, error: 'Not a directory' }; } // Build command arguments const args = [ detectorIndexPath, '--working-directory', projectPath, '--output-format', options.outputFormat ]; if (options.scanLockfiles) { args.push('--scan-lockfiles'); } else { args.push('--no-scan-lockfiles'); } if (options.failOnCritical) { args.push('--fail-on-critical'); } if (options.failOnHigh) { args.push('--fail-on-high'); } if (options.failOnAny) { args.push('--fail-on-any'); } return new Promise((resolve) => { const detectorProcess = spawn('node', args, { stdio: 'inherit', cwd: detectorPath }); detectorProcess.on('close', (code) => { resolve({ success: code === 0, exitCode: code, projectPath }); }); detectorProcess.on('error', (error) => { resolve({ success: false, error: error.message, projectPath }); }); }); } // Scan all projects recursively async function scanAllProjects(options) { console.log('🛡️ TOPS BMAD Security Scanner - Workspace Scan'); console.log('================================================\n'); const projects = orgConfig.projects || []; if (projects.length === 0) { console.log('📦 No projects configured. Scanning current directory...\n'); return scanSingleProject(process.cwd(), options); } console.log(`📦 Found ${projects.length} projects to scan\n`); const results = []; let totalAffected = 0; let totalFindings = 0; let failedScans = 0; for (const project of projects) { const projectPath = resolve(workspaceRoot, project.path || project); const projectName = project.name || project.path || project; console.log(`\n${'='.repeat(70)}`); console.log(`🔍 Scanning: ${projectName}`); console.log(` Path: ${projectPath}`); console.log('='.repeat(70)); const result = await scanSingleProject(projectPath, options); results.push({ ...result, projectName }); if (!result.success) { failedScans++; } } // Summary console.log('\n' + '='.repeat(70)); console.log('📊 SCAN SUMMARY'); console.log('='.repeat(70)); for (const result of results) { if (!result.success) { console.log(`\n❌ ${result.projectName}: FAILED`); if (result.error) console.log(` Error: ${result.error}`); } else { console.log(`\n✅ ${result.projectName}: COMPLETED`); } } console.log('\n' + '-'.repeat(70)); console.log(`Total projects scanned: ${results.length}`); console.log(`Failed scans: ${failedScans}`); console.log('='.repeat(70)); // Exit with error if any scans failed if (failedScans > 0) { process.exit(1); } return results; } // Main function async function main() { const options = parseArgs(); if (options.recursive) { await scanAllProjects(options); } else { const projectPath = options.project || process.cwd(); const result = await scanSingleProject(projectPath, options); if (!result.success) { process.exit(1); } } } main().catch(error => { console.error('Fatal error:', error); process.exit(1); });