UNPKG

supamend

Version:

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

264 lines 11.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DiscordReporter = void 0; const errors_1 = require("../core/errors"); const retry_1 = require("../core/retry"); const axios_1 = __importDefault(require("axios")); class DiscordReporter { constructor() { this.name = 'discord'; this.description = 'Send security scan results to Discord channel via webhook'; this.version = '1.0.0'; } async init(config) { const webhookUrl = config?.webhookUrl || process.env.DISCORD_WEBHOOK_URL; if (!webhookUrl) { throw new Error('Discord webhook URL is required. Set DISCORD_WEBHOOK_URL environment variable or provide in config.'); } this.config = { webhookUrl, username: config?.username || 'SupaMend Security Scanner', avatarUrl: config?.avatarUrl || 'https://cdn.discordapp.com/emojis/🛡️.png', threadId: config?.threadId }; // Validate webhook URL format if (!webhookUrl.includes('discord.com/api/webhooks/')) { throw new Error('Invalid Discord webhook URL format'); } } async report(results, options) { if (!this.config) { throw new Error('Discord reporter not initialized'); } try { const message = this.generateDiscordMessage(results, options); const result = await (0, retry_1.retryWithCondition)(async () => { const response = await axios_1.default.post(this.config.webhookUrl, message, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 // 30 seconds timeout }); if (response.status !== 204) { throw new Error(`Discord API error: ${response.status} ${response.statusText}`); } return response.data; }, (error) => { const retryableErrors = ['timeout', 'network', 'connection', 'temporary', 'rate limit']; return retryableErrors.some(err => error.message.toLowerCase().includes(err)); }, { maxAttempts: 3, baseDelay: 1000, maxDelay: 10000 }); if (!result.success) { throw result.error || new Error('Failed to send Discord message'); } console.log(`Security scan results sent to Discord channel`); } catch (error) { const reporterError = error instanceof Error ? error : new Error(String(error)); throw new errors_1.ReporterError(`Failed to send Discord report: ${reporterError.message}`, this.name, { recoverable: true, retryable: this.isRetryableError(reporterError), cause: reporterError, context: { operation: 'report', webhookUrl: this.config?.webhookUrl } }); } } async isAvailable() { return !!process.env.DISCORD_WEBHOOK_URL; } getConfigSchema() { return { type: 'object', properties: { webhookUrl: { type: 'string', description: 'Discord webhook URL' }, username: { type: 'string', description: 'Custom username for the bot' }, avatarUrl: { type: 'string', description: 'Custom avatar URL for the bot' }, threadId: { type: 'string', description: 'Thread ID to send message in thread' } }, required: ['webhookUrl'] }; } generateDiscordMessage(results, options) { const summary = this.generateSummary(results); const severityColors = { critical: 0xdc3545, // Red high: 0xfd7e14, // Orange medium: 0xffc107, // Yellow low: 0x28a745 // Green }; const embeds = []; // Main summary embed const summaryEmbed = { title: '🔒 Security Scan Results', description: `Found **${results.length}** security issues in the latest scan.`, color: results.length > 0 ? severityColors.critical : 0x28a745, timestamp: new Date().toISOString(), fields: [] }; // Add severity breakdown if (Object.keys(summary.severity).length > 0) { const severityText = Object.entries(summary.severity) .map(([sev, count]) => `${sev.toUpperCase()}: **${count}**`) .join(' | '); summaryEmbed.fields.push({ name: 'Severity Breakdown', value: severityText, inline: false }); } // Add scanner breakdown if (Object.keys(summary.scanners).length > 0) { const scannerText = Object.entries(summary.scanners) .map(([scanner, count]) => `**${scanner}**: ${count}`) .join(' • '); summaryEmbed.fields.push({ name: '🔍 Scanners Used', value: scannerText, inline: false }); } // Add repository info if available const repoUrl = options?.repo; if (repoUrl && typeof repoUrl === 'string') { let repoName = repoUrl; if (repoUrl.includes('github.com')) { const match = repoUrl.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?(?:\/|$)/); if (match && match[1]) { repoName = match[1]; } } summaryEmbed.fields.push({ name: '📦 Repository', value: repoUrl.startsWith('http') ? `[${repoName}](${repoUrl})` : repoName, inline: true }); } // Add scan timestamp summaryEmbed.fields.push({ name: '⏰ Scan Time', value: new Date().toLocaleString(), inline: true }); // Add total files scanned if available const uniqueFiles = [...new Set(results.map(r => r.file).filter(Boolean))]; if (uniqueFiles.length > 0) { summaryEmbed.fields.push({ name: '📁 Files Affected', value: `${uniqueFiles.length} files`, inline: true }); } embeds.push(summaryEmbed); // Add critical issues embed const criticalIssues = results.filter(r => r.severity === 'critical'); if (criticalIssues.length > 0) { const criticalEmbed = { title: '🚨 Critical Issues', color: severityColors.critical, fields: criticalIssues.slice(0, 5).map(issue => ({ name: `${issue.title}`, value: `**Scanner:** ${issue.scanner}\n**Type:** ${issue.type}\n**Description:** ${issue.description?.substring(0, 100) || 'No description'}${issue.description && issue.description.length > 100 ? '...' : ''}\n**File:** \`${issue.file || 'N/A'}${issue.line ? `:${issue.line}` : ''}\`${issue.rule ? `\n**Rule:** ${issue.rule}` : ''}`, inline: false })), footer: { text: `Showing ${Math.min(criticalIssues.length, 5)} of ${criticalIssues.length} critical issues` } }; embeds.push(criticalEmbed); } // Add high priority issues embed const highIssues = results.filter(r => r.severity === 'high'); if (highIssues.length > 0) { const highEmbed = { title: '⚠️ High Priority Issues', color: severityColors.high, fields: highIssues.slice(0, 5).map(issue => ({ name: `${issue.title}`, value: `**Scanner:** ${issue.scanner}\n**Type:** ${issue.type}\n**Description:** ${issue.description?.substring(0, 100) || 'No description'}${issue.description && issue.description.length > 100 ? '...' : ''}\n**File:** \`${issue.file || 'N/A'}${issue.line ? `:${issue.line}` : ''}\`${issue.rule ? `\n**Rule:** ${issue.rule}` : ''}`, inline: false })), footer: { text: `Showing ${Math.min(highIssues.length, 5)} of ${highIssues.length} high priority issues` } }; embeds.push(highEmbed); } // Add medium priority issues if there are few const mediumIssues = results.filter(r => r.severity === 'medium'); if (mediumIssues.length > 0 && mediumIssues.length <= 3) { const mediumEmbed = { title: '⚠️ Medium Priority Issues', color: severityColors.medium, fields: mediumIssues.map(issue => ({ name: `${issue.title}`, value: `**Scanner:** ${issue.scanner}\n**Type:** ${issue.type}\n**Description:** ${issue.description?.substring(0, 100) || 'No description'}${issue.description && issue.description.length > 100 ? '...' : ''}\n**File:** \`${issue.file || 'N/A'}${issue.line ? `:${issue.line}` : ''}\`${issue.rule ? `\n**Rule:** ${issue.rule}` : ''}`, inline: false })) }; embeds.push(mediumEmbed); } else if (mediumIssues.length > 3) { summaryEmbed.fields.push({ name: 'Medium Issues', value: `${mediumIssues.length} medium priority issues found. Check full report for details.`, inline: false }); } return { ...(this.config.username && { username: this.config.username }), ...(this.config.avatarUrl && { avatar_url: this.config.avatarUrl }), content: results.length > 0 ? 'Security scan completed with issues found!' : 'Security scan completed - no issues found! 🎉', embeds, ...(this.config.threadId && { thread_id: this.config.threadId }) }; } 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', 'network', 'connection', 'temporary', 'rate limit', 'resource temporarily unavailable' ]; return retryablePatterns.some(pattern => error.message.toLowerCase().includes(pattern)); } } exports.DiscordReporter = DiscordReporter; exports.default = new DiscordReporter(); //# sourceMappingURL=discord.js.map