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
JavaScript
/**
* 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);
});