supamend
Version:
Pluggable DevSecOps Security Scanner with 10+ scanners and multiple reporting channels
343 lines (337 loc) • 13.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 });
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