UNPKG

supamend

Version:

Pluggable DevSecOps Security Scanner with 10+ scanners and multiple reporting channels

385 lines 16.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const plugin_manager_1 = require("./plugin-manager"); const git_manager_1 = require("./git-manager"); const logger_1 = require("./logger"); const errors_1 = require("./errors"); const retry_1 = require("./retry"); const fs = __importStar(require("fs-extra")); class SupaMend { constructor() { this.pluginManager = new plugin_manager_1.PluginManager(); this.gitManager = new git_manager_1.GitManager(); this.logger = new logger_1.Logger(); // For tests, if logger is undefined, provide a mock if (!this.logger.resetCounters) { this.logger.resetCounters = () => { }; } if (!this.logger.info) { this.logger.info = () => { }; } } /** * Load configuration from file */ async loadConfig(configPath) { if (configPath && await fs.pathExists(configPath)) { const configContent = await fs.readFile(configPath, 'utf-8'); this.config = JSON.parse(configContent); } } /** * Run a security scan on a repository */ async scan(options) { this.logger.resetCounters(); this.logger.info('Starting SupaMend security scan...'); let repoPath; try { // Load configuration if (options.config) { await this.loadConfigWithRetry(options.config); } // If repo is a local path, use it directly; otherwise, clone if (options.repo && !/^https?:\/\//.test(options.repo)) { // Local path repoPath = options.repo; this.logger.info(`Using local repository at: ${repoPath}`); } else { // Clone repository with retry repoPath = await this.cloneRepositoryWithRetry(options.repo, options.token); this.logger.info(`Repository cloned to: ${repoPath}`); } // Auto-detect project and suggest scanners if not specified let finalScanners = options.scanners; if (!finalScanners || finalScanners.length === 0) { this.logger.info('No scanners specified, analyzing repository...'); const { ProjectDetector } = await Promise.resolve().then(() => __importStar(require('../cli/project-detector'))); const projectInfo = await ProjectDetector.detectProject(repoPath); this.logger.info(`Detected project type: ${projectInfo.description}`); this.logger.info(`Suggested scanners: ${projectInfo.suggestedScanners.join(', ')}`); finalScanners = projectInfo.suggestedScanners; } // Get enabled scanners const scanners = await this.getEnabledScanners(finalScanners); this.logger.info(`Using scanners: ${scanners.map(s => s.name).join(', ')}`); // Run scans in parallel with progress tracking const allResults = []; let completedScanners = 0; this.logger.info(`🚀 Running ${scanners.length} scanners in parallel...`); const scanPromises = scanners.map(async (scanner) => { const result = await this.runScannerWithRecovery(scanner, repoPath); completedScanners++; this.logger.info(`📈 Progress: ${completedScanners}/${scanners.length} scanners completed`); return result; }); const scanResults = await Promise.allSettled(scanPromises); scanResults.forEach((result, index) => { if (result.status === 'fulfilled') { allResults.push(...result.value); } else { this.logger.warn(`Scanner ${scanners[index]?.name || 'unknown'} failed: ${result.reason}`); } }); this.logger.info(`🏁 All scanners completed. Total issues found: ${allResults.length}`); // Get enabled reporters const reporters = await this.getEnabledReporters(options.reporters); // Report results in parallel with error recovery if (reporters.length > 0) { this.logger.info(`Running ${reporters.length} reporters in parallel...`); const reportPromises = reporters.map(reporter => this.runReporterWithRecovery(reporter, allResults, options)); await Promise.allSettled(reportPromises); } // Log final statistics const stats = this.logger.getErrorStats(); this.logger.info(`Scan completed. Found ${allResults.length} total issues.`, { errors: stats.errors, warnings: stats.warnings }); return allResults; } catch (error) { const scanError = error instanceof Error ? error : new Error(String(error)); this.logger.error('Critical error during scan', scanError); throw scanError; } finally { // Always cleanup, even if there was an error if (repoPath) { await this.cleanupWithRecovery(repoPath); } } } /** * Load configuration with retry */ async loadConfigWithRetry(configPath) { const result = await (0, retry_1.retry)(async () => { if (!(await fs.pathExists(configPath))) { throw new errors_1.ConfigurationError(`Configuration file not found: ${configPath}`, configPath, { recoverable: true, retryable: false }); } const configContent = await fs.readFile(configPath, 'utf-8'); try { this.config = JSON.parse(configContent); } catch (parseError) { throw new errors_1.ConfigurationError(`Invalid JSON in configuration file: ${configPath}`, configPath, { recoverable: true, retryable: false, ...(parseError instanceof Error && { cause: parseError }) }); } }, { maxAttempts: 2, context: { operation: 'loadConfig', file: configPath }, logger: this.logger }); if (!result.success) { this.logger.errorWithRecovery('Failed to load configuration', result.error, errors_1.RecoverySuggestions[errors_1.ErrorCodes.CONFIG_LOAD_FAILED], { file: configPath }); } } /** * Clone repository with retry */ async cloneRepositoryWithRetry(repoUrl, token) { const result = await (0, retry_1.retryWithCondition)(async () => this.gitManager.cloneRepository(repoUrl, token), (error) => { // Retry on network errors or temporary git issues return error.message.includes('network') || error.message.includes('timeout') || error.message.includes('temporary'); }, { maxAttempts: 3, baseDelay: 2000, context: { operation: 'cloneRepository', repo: repoUrl }, logger: this.logger }); if (!result.success) { throw new errors_1.GitError(`Failed to clone repository after ${result.attempts} attempts`, 'clone', { recoverable: false, retryable: false, ...(result.error && { cause: result.error }) }); } return result.result; } /** * Run scanner with error recovery and timeout */ async runScannerWithRecovery(scanner, repoPath) { const startTime = Date.now(); try { this.logger.info(`🔍 Starting ${scanner.name}...`, { scanner: scanner.name }); const result = await (0, retry_1.retry)(async () => { const scanPromise = scanner.scan(repoPath); const timeoutMs = 300000; // 5 minutes default timeout return Promise.race([ scanPromise, new Promise((_, reject) => setTimeout(() => reject(new Error(`Scanner timeout after ${timeoutMs}ms`)), timeoutMs)) ]); }, { maxAttempts: 2, context: { operation: 'scan', scanner: scanner.name }, logger: this.logger }); if (result.success) { const duration = Date.now() - startTime; this.logger.info(`✅ ${scanner.name} completed in ${duration}ms - Found ${result.result.length} issues`, { scanner: scanner.name }); return result.result; } else { throw result.error; } } catch (error) { const duration = Date.now() - startTime; const scannerError = error instanceof Error ? error : new Error(String(error)); this.logger.errorWithRecovery(`❌ ${scanner.name} failed after ${duration}ms`, scannerError, errors_1.RecoverySuggestions[errors_1.ErrorCodes.SCANNER_EXECUTION_FAILED], { scanner: scanner.name }); return []; } } /** * Run reporter with error recovery */ async runReporterWithRecovery(reporter, results, options) { try { this.logger.info(`Reporting results with: ${reporter.name}`, { reporter: reporter.name }); const result = await (0, retry_1.retry)(async () => reporter.report(results, options), { maxAttempts: 2, context: { operation: 'report', reporter: reporter.name }, logger: this.logger }); if (!result.success) { throw result.error; } } catch (error) { const reporterError = error instanceof Error ? error : new Error(String(error)); this.logger.errorWithRecovery(`Reporter ${reporter.name} failed`, reporterError, errors_1.RecoverySuggestions[errors_1.ErrorCodes.REPORTER_EXECUTION_FAILED], { reporter: reporter.name }); } } /** * Cleanup with error recovery */ async cleanupWithRecovery(repoPath) { try { // Skip cleanup for read-only mounted directories (Docker) if (repoPath === '/scan' || repoPath.includes('/scan')) { this.logger.info('Skipping cleanup for read-only mounted directory'); return; } await this.gitManager.cleanup(repoPath); } catch (error) { const cleanupError = error instanceof Error ? error : new Error(String(error)); // Don't treat cleanup failures as critical errors this.logger.warn(`Failed to cleanup ${repoPath}: ${cleanupError.message}`); } } /** * Get enabled scanners based on configuration */ async getEnabledScanners(scannerNames) { const scanners = []; const failedScanners = []; for (const name of scannerNames) { try { // Check if scanner exists first if (!this.pluginManager.hasScanner(name)) { this.logger.warn(`Scanner '${name}' not found, skipping...`); failedScanners.push(name); continue; } const scanner = await this.pluginManager.getScanner(name); if (scanner) { const config = this.getScannerConfig(name); await scanner.init(config); scanners.push(scanner); } else { // Scanner exists but is not available this.logger.warn(`Scanner '${name}' is not available, skipping...`); failedScanners.push(name); } } catch (error) { const scannerError = error instanceof Error ? error : new Error(String(error)); this.logger.warn(`Failed to initialize scanner '${name}': ${scannerError.message}`); failedScanners.push(name); } } if (scanners.length === 0) { throw new errors_1.ScannerError(`No scanners available. Failed scanners: ${failedScanners.join(', ')}`, 'all', { recoverable: false, retryable: false }); } if (failedScanners.length > 0) { this.logger.warn(`Skipped unavailable scanners: ${failedScanners.join(', ')}`); } return scanners; } /** * Get enabled reporters based on configuration */ async getEnabledReporters(reporterNames) { const reporters = []; const failedReporters = []; for (const name of reporterNames) { try { // Check if reporter exists first if (!this.pluginManager.hasReporter(name)) { this.logger.warn(`Reporter '${name}' not found, skipping...`); failedReporters.push(name); continue; } const reporter = await this.pluginManager.getReporter(name); if (reporter) { const config = this.getReporterConfig(name); await reporter.init(config); reporters.push(reporter); } else { // Reporter exists but is not available this.logger.warn(`Reporter '${name}' is not available, skipping...`); failedReporters.push(name); } } catch (error) { const reporterError = error instanceof Error ? error : new Error(String(error)); this.logger.warn(`Failed to initialize reporter '${name}': ${reporterError.message}`); failedReporters.push(name); } } if (reporters.length === 0) { this.logger.warn(`No reporters available. Failed reporters: ${failedReporters.join(', ')}. Results will not be reported.`); } else if (failedReporters.length > 0) { this.logger.warn(`Skipped unavailable reporters: ${failedReporters.join(', ')}`); } return reporters; } /** * Get scanner configuration from global config */ getScannerConfig(scannerName) { if (!this.config?.scanners) return undefined; const scannerConfig = this.config.scanners.find(s => s.name === scannerName); return scannerConfig?.options; } /** * Get reporter configuration from global config */ getReporterConfig(reporterName) { if (!this.config?.reporters) return undefined; const reporterConfig = this.config.reporters.find(r => r.name === reporterName); return reporterConfig?.options; } /** * List available scanners */ async listScanners() { return this.pluginManager.listScanners(); } /** * List available reporters */ async listReporters() { return this.pluginManager.listReporters(); } } exports.default = SupaMend; //# sourceMappingURL=supamend.js.map