Skip to main content
Glama
SecurityTestReporter.ts11.7 kB
/** * Security Test Reporter * * Handles report generation and notifications for security test results */ import { promises as fs } from 'fs'; import path from 'path'; import { Logger } from '../../utils/logger'; import { SecurityTestResult, SecurityTestReport, SecurityTestSummary, SecurityTestMetrics, ReportingConfig, SecurityTestConfig } from '../types'; export class SecurityTestReporter { private logger: Logger; private config: ReportingConfig; private outputDir: string; private lastRunTime?: Date; constructor(config: ReportingConfig) { this.config = config; this.logger = new Logger({ component: 'SecurityTestReporter' }); this.outputDir = config.outputDir || 'reports/security'; } async initialize(): Promise<void> { this.logger.info('Initializing Security Test Reporter'); if (!this.config.enabled) { this.logger.info('Security Test Reporter disabled'); return; } try { // Ensure output directory exists await fs.mkdir(this.outputDir, { recursive: true }); this.logger.info(`Reports will be saved to: ${this.outputDir}`); } catch (error) { this.logger.error('Failed to initialize Security Test Reporter', undefined, error instanceof Error ? error : new Error(String(error))); throw error; } } async generateReport( results: SecurityTestResult[], summary: SecurityTestSummary, testConfig?: Partial<SecurityTestConfig> ): Promise<void> { this.logger.info('Generating security test report'); if (!this.config.enabled) { return; } try { const report = await this.buildReport(results, summary, testConfig); const formats = this.config.formats || ['json', 'html']; // Generate reports in requested formats for (const format of formats) { await this.generateReportFormat(report, format); } // Send notifications if configured if (this.config.notifications) { await this.sendNotifications(); } // Send webhooks if configured if (this.config.webhooks) { await this.sendWebhooks(); } this.lastRunTime = new Date(); this.logger.info('Security test report generated successfully'); } catch (error) { this.logger.error('Failed to generate security test report', undefined, error instanceof Error ? error : new Error(String(error))); throw error; } } async cleanup(): Promise<void> { this.logger.info('Cleaning up Security Test Reporter'); // No cleanup required } getLastRunTime(): Date | undefined { return this.lastRunTime; } private async buildReport( results: SecurityTestResult[], summary: SecurityTestSummary, testConfig?: Partial<SecurityTestConfig> ): Promise<SecurityTestReport> { const metrics = this.calculateMetrics(results, summary); return { id: `security-report-${Date.now()}`, timestamp: new Date(), summary, results, metrics, config: testConfig || {}, environment: { platform: process.platform, version: process.version, hostname: require('os').hostname(), user: process.env.USER || process.env.USERNAME || 'unknown' } }; } private calculateMetrics(results: SecurityTestResult[], summary: SecurityTestSummary): SecurityTestMetrics { const vulnerabilities = results.filter(r => r.status === 'failed'); return { testExecutionTime: summary.duration, vulnerabilitiesFound: vulnerabilities.length, criticalIssues: vulnerabilities.filter(v => v.severity === 'critical').length, highSeverityIssues: vulnerabilities.filter(v => v.severity === 'high').length, mediumSeverityIssues: vulnerabilities.filter(v => v.severity === 'medium').length, lowSeverityIssues: vulnerabilities.filter(v => v.severity === 'low').length, falsePositives: 0, // Would need manual classification coverage: this.calculateCoverage(results) }; } private calculateCoverage(results: SecurityTestResult[]): number { // Simple coverage calculation based on test suites const suites = new Set(results.map(r => r.suite)); const expectedSuites = ['custom', 'owasp-zap', 'snyk']; return (suites.size / expectedSuites.length) * 100; } private async generateReportFormat(report: SecurityTestReport, format: string): Promise<void> { const timestamp = report.timestamp.toISOString().replace(/[:.]/g, '-'); const filename = `security-report-${timestamp}`; switch (format) { case 'json': await this.generateJSONReport(report, filename); break; case 'html': await this.generateHTMLReport(report, filename); break; case 'csv': await this.generateCSVReport(report, filename); break; case 'xml': await this.generateXMLReport(report, filename); break; case 'junit': await this.generateJUnitReport(report, filename); break; default: this.logger.warn(`Unsupported report format: ${format}`); } } private async generateJSONReport(report: SecurityTestReport, filename: string): Promise<void> { const filepath = path.join(this.outputDir, `${filename}.json`); const content = JSON.stringify(report, null, 2); await fs.writeFile(filepath, content, 'utf8'); this.logger.info(`JSON report saved: ${filepath}`); } private async generateHTMLReport(report: SecurityTestReport, filename: string): Promise<void> { const filepath = path.join(this.outputDir, `${filename}.html`); const content = this.buildHTMLReport(report); await fs.writeFile(filepath, content, 'utf8'); this.logger.info(`HTML report saved: ${filepath}`); } private async generateCSVReport(report: SecurityTestReport, filename: string): Promise<void> { const filepath = path.join(this.outputDir, `${filename}.csv`); const content = this.buildCSVReport(report); await fs.writeFile(filepath, content, 'utf8'); this.logger.info(`CSV report saved: ${filepath}`); } private async generateXMLReport(report: SecurityTestReport, filename: string): Promise<void> { const filepath = path.join(this.outputDir, `${filename}.xml`); const content = this.buildXMLReport(report); await fs.writeFile(filepath, content, 'utf8'); this.logger.info(`XML report saved: ${filepath}`); } private async generateJUnitReport(report: SecurityTestReport, filename: string): Promise<void> { const filepath = path.join(this.outputDir, `${filename}.junit.xml`); const content = this.buildJUnitReport(report); await fs.writeFile(filepath, content, 'utf8'); this.logger.info(`JUnit report saved: ${filepath}`); } private buildHTMLReport(report: SecurityTestReport): string { const { summary, results, metrics } = report; return ` <!DOCTYPE html> <html> <head> <title>Security Test Report</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } .header { background: #f4f4f4; padding: 20px; border-radius: 5px; } .metrics { display: flex; gap: 20px; margin: 20px 0; } .metric { background: #e8f4f8; padding: 15px; border-radius: 5px; text-align: center; } .results { margin: 20px 0; } .result { margin: 10px 0; padding: 15px; border-radius: 5px; } .failed { background: #ffe6e6; border-left: 4px solid #ff4444; } .passed { background: #e6ffe6; border-left: 4px solid #44ff44; } .warning { background: #fff5e6; border-left: 4px solid #ffaa44; } .severity { font-weight: bold; padding: 2px 8px; border-radius: 3px; color: white; } .critical { background: #cc0000; } .high { background: #ff4444; } .medium { background: #ffaa44; } .low { background: #4488ff; } </style> </head> <body> <div class="header"> <h1>Security Test Report</h1> <p>Generated: ${report.timestamp.toISOString()}</p> <p>Duration: ${summary.duration}ms</p> </div> <div class="metrics"> <div class="metric"> <h3>${summary.totalTests}</h3> <p>Total Tests</p> </div> <div class="metric"> <h3>${summary.passed}</h3> <p>Passed</p> </div> <div class="metric"> <h3>${summary.failed}</h3> <p>Failed</p> </div> <div class="metric"> <h3>${metrics.vulnerabilitiesFound}</h3> <p>Vulnerabilities</p> </div> </div> <div class="results"> <h2>Test Results</h2> ${results.map(result => ` <div class="result ${result.status}"> <h3>${result.name} <span class="severity ${result.severity}">${result.severity.toUpperCase()}</span></h3> <p><strong>Suite:</strong> ${result.suite}</p> <p><strong>Description:</strong> ${result.description}</p> ${result.details ? `<p><strong>Details:</strong> ${result.details}</p>` : ''} ${result.remediation ? `<p><strong>Remediation:</strong> ${result.remediation}</p>` : ''} </div> `).join('')} </div> </body> </html>`; } private buildCSVReport(report: SecurityTestReport): string { const headers = ['ID', 'Name', 'Suite', 'Status', 'Severity', 'Description', 'Duration']; const rows = report.results.map(result => [ result.id, result.name, result.suite, result.status, result.severity, result.description.replace(/,/g, ';'), result.duration.toString() ]); return [headers, ...rows].map(row => row.join(',')).join('\n'); } private buildXMLReport(report: SecurityTestReport): string { const results = report.results.map(result => ` <test id="${result.id}" name="${result.name}" suite="${result.suite}" status="${result.status}" severity="${result.severity}" duration="${result.duration}"> <description><![CDATA[${result.description}]]></description> ${result.details ? `<details><![CDATA[${result.details}]]></details>` : ''} ${result.remediation ? `<remediation><![CDATA[${result.remediation}]]></remediation>` : ''} </test>`).join(''); return `<?xml version="1.0" encoding="UTF-8"?> <security-report timestamp="${report.timestamp.toISOString()}" id="${report.id}"> <summary total="${report.summary.totalTests}" passed="${report.summary.passed}" failed="${report.summary.failed}" duration="${report.summary.duration}" /> <tests>${results} </tests> </security-report>`; } private buildJUnitReport(report: SecurityTestReport): string { const testCases = report.results.map(result => { const failure = result.status === 'failed' ? `<failure message="${result.description}" type="${result.severity}">${result.details || ''}</failure>` : ''; return `<testcase classname="${result.suite}" name="${result.name}" time="${result.duration / 1000}">${failure}</testcase>`; }).join('\n '); return `<?xml version="1.0" encoding="UTF-8"?> <testsuite name="Security Tests" tests="${report.summary.totalTests}" failures="${report.summary.failed}" time="${report.summary.duration / 1000}" timestamp="${report.timestamp.toISOString()}"> ${testCases} </testsuite>`; } private async sendNotifications(): Promise<void> { // TODO: Implement notification sending logic (e.g., Slack, Email) this.logger.info('Notification sending is not yet implemented.'); } private async sendWebhooks(): Promise<void> { // TODO: Implement webhook sending logic this.logger.info('Webhook sending is not yet implemented.'); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/learnwithcc/tally-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server