supamend
Version:
Pluggable DevSecOps Security Scanner with 10+ scanners and multiple reporting channels
385 lines • 16.6 kB
JavaScript
;
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