UNPKG

supamend

Version:

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

343 lines (337 loc) 13.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 }); exports.EmailReporter = void 0; const errors_1 = require("../core/errors"); const retry_1 = require("../core/retry"); const nodemailer = __importStar(require("nodemailer")); const handlebars = __importStar(require("handlebars")); class EmailReporter { constructor() { this.name = 'email'; this.description = 'Send security scan results via email'; this.version = '1.0.0'; this.defaultTemplate = ` <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Security Scan Results</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } .header { background-color: #f8f9fa; padding: 20px; border-radius: 5px; } .summary { margin: 20px 0; } .severity-critical { color: #dc3545; font-weight: bold; } .severity-high { color: #fd7e14; font-weight: bold; } .severity-medium { color: #ffc107; font-weight: bold; } .severity-low { color: #28a745; font-weight: bold; } .severity-info { color: #17a2b8; font-weight: bold; } .issue { margin: 10px 0; padding: 10px; border-left: 4px solid #dee2e6; } .issue.critical { border-left-color: #dc3545; background-color: #f8d7da; } .issue.high { border-left-color: #fd7e14; background-color: #fff3cd; } .issue.medium { border-left-color: #ffc107; background-color: #fff3cd; } .issue.low { border-left-color: #28a745; background-color: #d4edda; } .issue.info { border-left-color: #17a2b8; background-color: #d1ecf1; } .footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #dee2e6; color: #6c757d; } </style> </head> <body> <div class="header"> <h1>🔒 Security Scan Results</h1> <p><strong>Scan Time:</strong> {{timestamp}}</p> <p><strong>Total Issues Found:</strong> {{totalIssues}}</p> </div> <div class="summary"> <h2>Summary</h2> <p><strong>Severity Breakdown:</strong></p> <ul> {{#each summary.severity}} <li class="severity-{{@key}}">{{@key}}: {{this}}</li> {{/each}} </ul> <p><strong>Scanners Used:</strong></p> <ul> {{#each summary.scanners}} <li>{{@key}}: {{this}} issues</li> {{/each}} </ul> </div> {{#if criticalIssues.length}} <h2 class="severity-critical">🚨 Critical Issues ({{criticalIssues.length}})</h2> {{#each criticalIssues}} <div class="issue critical"> <h3>{{title}}</h3> <p><strong>Scanner:</strong> {{scanner}}</p> <p><strong>Type:</strong> {{type}}</p> <p><strong>Description:</strong> {{description}}</p> {{#if file}}<p><strong>File:</strong> {{file}}{{#if line}}:{{line}}{{/if}}</p>{{/if}} {{#if rule}}<p><strong>Rule:</strong> {{rule}}</p>{{/if}} </div> {{/each}} {{/if}} {{#if highIssues.length}} <h2 class="severity-high">⚠️ High Priority Issues ({{highIssues.length}})</h2> {{#each highIssues}} <div class="issue high"> <h3>{{title}}</h3> <p><strong>Scanner:</strong> {{scanner}}</p> <p><strong>Type:</strong> {{type}}</p> <p><strong>Description:</strong> {{description}}</p> {{#if file}}<p><strong>File:</strong> {{file}}{{#if line}}:{{line}}{{/if}}</p>{{/if}} {{#if rule}}<p><strong>Rule:</strong> {{rule}}</p>{{/if}} </div> {{/each}} {{/if}} {{#if mediumIssues.length}} <h2 class="severity-medium">⚠️ Medium Priority Issues ({{mediumIssues.length}})</h2> {{#each mediumIssues}} <div class="issue medium"> <h3>{{title}}</h3> <p><strong>Scanner:</strong> {{scanner}}</p> <p><strong>Type:</strong> {{type}}</p> <p><strong>Description:</strong> {{description}}</p> {{#if file}}<p><strong>File:</strong> {{file}}{{#if line}}:{{line}}{{/if}}</p>{{/if}} {{#if rule}}<p><strong>Rule:</strong> {{rule}}</p>{{/if}} </div> {{/each}} {{/if}} <div class="footer"> <p>Reported by SupaMend Security Scanner</p> <p>This is an automated security scan report. Please review and address the issues as appropriate.</p> </div> </body> </html>`; } async init(config) { const host = config?.host || process.env.EMAIL_HOST || ''; const port = config?.port || process.env.EMAIL_PORT || 587; const secure = config?.secure || process.env.EMAIL_SECURE === 'true'; const user = config?.user || process.env.EMAIL_USER || ''; const pass = config?.pass || process.env.EMAIL_PASS || ''; const from = config?.from || process.env.EMAIL_FROM || ''; const to = config?.to || (process.env.EMAIL_TO ? process.env.EMAIL_TO.split(',') : []); if (!host || !user || !pass || !from || to.length === 0) { const missing = []; if (!host) missing.push('EMAIL_HOST'); if (!user) missing.push('EMAIL_USER'); if (!pass) missing.push('EMAIL_PASS'); if (!from) missing.push('EMAIL_FROM'); if (to.length === 0) missing.push('EMAIL_TO'); throw new Error(`Email configuration incomplete. Missing: ${missing.join(', ')}. Please set these environment variables or provide them in the config.`); } this.config = { host, port: Number(port), secure, auth: { user, pass }, from, to, subject: config?.subject || 'Security Scan Results - SupaMend', template: config?.template }; this.transporter = nodemailer.createTransport({ host: this.config.host, port: this.config.port, secure: this.config.secure, auth: this.config.auth }); // Test the connection try { await this.transporter.verify(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to connect to email server: ${errorMessage}`); } } async report(results, options) { if (!this.transporter || !this.config) { throw new Error('Email reporter not initialized'); } try { const templateData = this.generateTemplateData(results); const html = this.generateEmailHtml(templateData); const result = await (0, retry_1.retryWithCondition)(async () => { const mailOptions = { from: this.config.from, to: this.config.to.join(', '), subject: options?.subject || this.config.subject, html }; const info = await this.transporter.sendMail(mailOptions); return info; }, (error) => { const retryableErrors = ['timeout', 'connection', 'temporary', 'rate limit']; return retryableErrors.some(err => error.message.toLowerCase().includes(err)); }, { maxAttempts: 3, baseDelay: 2000, maxDelay: 15000 }); if (!result.success) { throw result.error || new Error('Failed to send email'); } console.log(`Security scan results sent via email to ${this.config.to.join(', ')}`); } catch (error) { const reporterError = error instanceof Error ? error : new Error(String(error)); console.error(`Email send failed: ${reporterError.message}`); throw new errors_1.ReporterError(`Failed to send email report: ${reporterError.message}`, this.name, { recoverable: true, retryable: this.isRetryableError(reporterError), cause: reporterError, context: { operation: 'report', recipients: this.config?.to } }); } } async isAvailable() { const hasConfig = !!(process.env.EMAIL_HOST && process.env.EMAIL_USER && process.env.EMAIL_PASS && process.env.EMAIL_FROM && process.env.EMAIL_TO); if (!hasConfig) { const missing = []; if (!process.env.EMAIL_HOST) missing.push('EMAIL_HOST'); if (!process.env.EMAIL_USER) missing.push('EMAIL_USER'); if (!process.env.EMAIL_PASS) missing.push('EMAIL_PASS'); if (!process.env.EMAIL_FROM) missing.push('EMAIL_FROM'); if (!process.env.EMAIL_TO) missing.push('EMAIL_TO'); console.warn(`Email reporter not available: Missing environment variables: ${missing.join(', ')}`); console.warn('Set these variables or configure via config file. See .supamend.email-example.json for reference.'); } return hasConfig; } getConfigSchema() { return { type: 'object', properties: { host: { type: 'string', description: 'SMTP server host' }, port: { type: 'number', description: 'SMTP server port' }, secure: { type: 'boolean', description: 'Use SSL/TLS' }, user: { type: 'string', description: 'SMTP username' }, pass: { type: 'string', description: 'SMTP password' }, from: { type: 'string', description: 'From email address' }, to: { type: 'array', items: { type: 'string' }, description: 'To email addresses' }, subject: { type: 'string', description: 'Email subject' }, template: { type: 'string', description: 'Custom Handlebars template' } }, required: ['host', 'user', 'pass', 'from', 'to'] }; } generateTemplateData(results) { const summary = this.generateSummary(results); return { timestamp: new Date().toLocaleString(), totalIssues: results.length, summary, criticalIssues: results.filter(r => r.severity === 'critical'), highIssues: results.filter(r => r.severity === 'high'), mediumIssues: results.filter(r => r.severity === 'medium'), lowIssues: results.filter(r => r.severity === 'low'), allIssues: results }; } generateEmailHtml(templateData) { const template = this.config?.template || this.defaultTemplate; const compiledTemplate = handlebars.compile(template); return compiledTemplate(templateData); } generateSummary(results) { const severityCounts = results.reduce((acc, result) => { acc[result.severity] = (acc[result.severity] || 0) + 1; return acc; }, {}); const scannerCounts = results.reduce((acc, result) => { acc[result.scanner] = (acc[result.scanner] || 0) + 1; return acc; }, {}); return { severity: severityCounts, scanners: scannerCounts }; } /** * Check if an error is retryable */ isRetryableError(error) { const retryablePatterns = [ 'timeout', 'connection', 'temporary', 'rate limit', 'quota exceeded', 'server busy' ]; return retryablePatterns.some(pattern => error.message.toLowerCase().includes(pattern)); } } exports.EmailReporter = EmailReporter; exports.default = new EmailReporter(); //# sourceMappingURL=email.js.map