web-vuln-scanner
Version:
Advanced, lightweight web vulnerability scanner with smart detection and easy-to-use interface
902 lines (791 loc) • 30.8 kB
JavaScript
/**
* Enhanced Professional HTML Reporter
* Advanced reporting with executive summary, detailed analysis, and professional formatting
*/
const fs = require('fs');
const path = require('path');
class EnhancedHTMLReporter {
constructor() {
this.template = this.getTemplate();
}
generateReport(results, options = {}) {
const reportData = this.processResults(results);
const html = this.template
.replace(/{{TITLE}}/g, options.title || 'Web Vulnerability Assessment Report')
.replace(/{{TARGET}}/g, reportData.target)
.replace(/{{TIMESTAMP}}/g, reportData.timestamp)
.replace(/{{SCAN_DURATION}}/g, reportData.scanDuration)
.replace(/{{TOTAL_VULNERABILITIES}}/g, reportData.totalVulnerabilities)
.replace(/{{CRITICAL_COUNT}}/g, reportData.severityCounts.critical)
.replace(/{{HIGH_COUNT}}/g, reportData.severityCounts.high)
.replace(/{{MEDIUM_COUNT}}/g, reportData.severityCounts.medium)
.replace(/{{LOW_COUNT}}/g, reportData.severityCounts.low)
.replace(/{{INFO_COUNT}}/g, reportData.severityCounts.info)
.replace(/{{RISK_SCORE}}/g, reportData.riskScore)
.replace(/{{COMPLIANCE_STATUS}}/g, reportData.complianceStatus)
.replace(/{{EXECUTIVE_SUMMARY}}/g, this.generateExecutiveSummary(reportData))
.replace(/{{RISK_MATRIX}}/g, this.generateRiskMatrix(reportData))
.replace(/{{VULNERABILITY_DETAILS}}/g, this.generateVulnerabilityDetails(reportData))
.replace(/{{RECOMMENDATIONS}}/g, this.generateRecommendations(reportData))
.replace(/{{TECHNICAL_APPENDIX}}/g, this.generateTechnicalAppendix(reportData))
.replace(/{{COMPLIANCE_MAPPING}}/g, this.generateComplianceMapping(reportData));
return html;
}
processResults(results) {
const severityCounts = {
critical: 0,
high: 0,
medium: 0,
low: 0,
info: 0
};
const vulnerabilities = [];
let totalVulnerabilities = 0;
// Handle flat structure (test format) vs nested structure (scanner format)
if (results.vulnerabilities && Array.isArray(results.vulnerabilities)) {
// Direct vulnerabilities array (test format)
results.vulnerabilities.forEach(vuln => {
vulnerabilities.push({
...vuln,
timestamp: new Date().toISOString()
});
severityCounts[vuln.severity] = (severityCounts[vuln.severity] || 0) + 1;
totalVulnerabilities++;
});
} else {
// Process each scanner result (scanner format)
Object.keys(results).forEach(scannerType => {
if (scannerType === 'target' || scannerType === 'timestamp' || scannerType === 'duration' ||
scannerType === 'url' || scannerType === 'scanId' || scannerType === 'summary' ||
scannerType === 'scannedUrls' || scannerType === 'scanDuration' || scannerType === 'metadata') return;
const scanResult = results[scannerType];
if (scanResult && scanResult.vulnerabilities) {
scanResult.vulnerabilities.forEach(vuln => {
vulnerabilities.push({
...vuln,
scanner: scannerType,
timestamp: new Date().toISOString()
});
severityCounts[vuln.severity] = (severityCounts[vuln.severity] || 0) + 1;
totalVulnerabilities++;
});
}
});
}
// Calculate risk score (0-100)
const riskScore = this.calculateRiskScore(severityCounts);
return {
target: results.target || results.url || 'Unknown Target',
timestamp: new Date().toLocaleString(),
scanDuration: results.duration || results.scanDuration || 'Unknown',
totalVulnerabilities,
severityCounts,
vulnerabilities,
riskScore,
complianceStatus: this.getComplianceStatus(severityCounts)
};
}
calculateRiskScore(severityCounts) {
const weights = { critical: 25, high: 15, medium: 8, low: 3, info: 1 };
const score = Object.keys(weights).reduce((total, severity) => {
return total + (severityCounts[severity] * weights[severity]);
}, 0);
return Math.min(100, score);
}
getComplianceStatus(severityCounts) {
if (severityCounts.critical > 0) return 'Non-Compliant';
if (severityCounts.high > 0) return 'Partially Compliant';
if (severityCounts.medium > 0) return 'Mostly Compliant';
return 'Compliant';
}
generateExecutiveSummary(data) {
const riskLevel = data.riskScore >= 70 ? 'High' : data.riskScore >= 40 ? 'Medium' : 'Low';
return `
<div class="executive-summary">
<h3>Executive Summary</h3>
<div class="summary-grid">
<div class="summary-card">
<h4>Risk Assessment</h4>
<div class="risk-indicator risk-${riskLevel.toLowerCase()}">
<span class="risk-score">${data.riskScore}</span>
<span class="risk-label">${riskLevel} Risk</span>
</div>
</div>
<div class="summary-card">
<h4>Vulnerabilities Found</h4>
<div class="vuln-count">
<span class="count">${data.totalVulnerabilities}</span>
<span class="label">Total Issues</span>
</div>
</div>
<div class="summary-card">
<h4>Compliance Status</h4>
<div class="compliance-status">
<span class="status ${data.complianceStatus.toLowerCase().replace(/\\s/g, '-')}">${data.complianceStatus}</span>
</div>
</div>
</div>
<div class="key-findings">
<h4>Key Findings</h4>
<ul>
${data.severityCounts.critical > 0 ? `<li><strong>Critical Issues:</strong> ${data.severityCounts.critical} critical vulnerabilities require immediate attention</li>` : ''}
${data.severityCounts.high > 0 ? `<li><strong>High Risk:</strong> ${data.severityCounts.high} high-severity vulnerabilities should be addressed within 24-48 hours</li>` : ''}
${data.severityCounts.medium > 0 ? `<li><strong>Medium Risk:</strong> ${data.severityCounts.medium} medium-severity issues should be remediated within 1-2 weeks</li>` : ''}
${data.totalVulnerabilities === 0 ? '<li><strong>No Vulnerabilities:</strong> No security issues were identified in this scan</li>' : ''}
</ul>
</div>
</div>
`;
}
generateRiskMatrix(data) {
return `
<div class="risk-matrix">
<h3>Risk Distribution</h3>
<div class="chart-container">
<canvas id="riskChart" width="400" height="300"></canvas>
</div>
<div class="severity-breakdown">
<div class="severity-item critical">
<span class="severity-dot"></span>
<span class="severity-label">Critical</span>
<span class="severity-count">${data.severityCounts.critical}</span>
</div>
<div class="severity-item high">
<span class="severity-dot"></span>
<span class="severity-label">High</span>
<span class="severity-count">${data.severityCounts.high}</span>
</div>
<div class="severity-item medium">
<span class="severity-dot"></span>
<span class="severity-label">Medium</span>
<span class="severity-count">${data.severityCounts.medium}</span>
</div>
<div class="severity-item low">
<span class="severity-dot"></span>
<span class="severity-label">Low</span>
<span class="severity-count">${data.severityCounts.low}</span>
</div>
<div class="severity-item info">
<span class="severity-dot"></span>
<span class="severity-label">Info</span>
<span class="severity-count">${data.severityCounts.info}</span>
</div>
</div>
</div>
`;
}
generateVulnerabilityDetails(data) {
if (data.vulnerabilities.length === 0) {
return '<div class="no-vulnerabilities"><h3>No Vulnerabilities Found</h3><p>The security scan did not identify any vulnerabilities in the target application.</p></div>';
}
// Group vulnerabilities by severity
const groupedVulns = data.vulnerabilities.reduce((groups, vuln) => {
if (!groups[vuln.severity]) groups[vuln.severity] = [];
groups[vuln.severity].push(vuln);
return groups;
}, {});
let html = '<div class="vulnerability-details"><h3>Vulnerability Details</h3>';
const severityOrder = ['critical', 'high', 'medium', 'low', 'info'];
severityOrder.forEach(severity => {
if (groupedVulns[severity] && groupedVulns[severity].length > 0) {
html += `
<div class="severity-section">
<h4 class="severity-header ${severity}">${severity.charAt(0).toUpperCase() + severity.slice(1)} Severity (${groupedVulns[severity].length})</h4>
<div class="vulnerabilities-list">
`;
groupedVulns[severity].forEach((vuln, index) => {
html += `
<div class="vulnerability-card">
<div class="vuln-header">
<h5>${vuln.type || vuln.name || 'Security Issue'}</h5>
<span class="severity-badge ${severity}">${severity.toUpperCase()}</span>
</div>
<div class="vuln-content">
<div class="vuln-description">
<strong>Description:</strong>
<p>${vuln.description || 'No description available'}</p>
</div>
${vuln.location ? `
<div class="vuln-location">
<strong>Location:</strong> <code>${vuln.location}</code>
</div>
` : ''}
${vuln.payload ? `
<div class="vuln-payload">
<strong>Proof of Concept:</strong>
<pre class="code-block">${this.escapeHtml(vuln.payload)}</pre>
</div>
` : ''}
${vuln.impact ? `
<div class="vuln-impact">
<strong>Impact:</strong>
<p>${vuln.impact}</p>
</div>
` : ''}
<div class="vuln-recommendation">
<strong>Recommendation:</strong>
<p>${vuln.recommendation || this.getDefaultRecommendation(vuln.type)}</p>
</div>
<div class="vuln-references">
<strong>References:</strong>
<ul>
${this.getReferences(vuln.type).map(ref => `<li><a href="${ref.url}" target="_blank">${ref.title}</a></li>`).join('')}
</ul>
</div>
</div>
</div>
`;
});
html += '</div></div>';
}
});
html += '</div>';
return html;
}
generateRecommendations(data) {
const recommendations = this.getRecommendationsByPriority(data);
return `
<div class="recommendations">
<h3>Remediation Recommendations</h3>
<div class="timeline">
<div class="timeline-item immediate">
<div class="timeline-marker"></div>
<div class="timeline-content">
<h4>Immediate Actions (0-24 hours)</h4>
<ul>
${recommendations.immediate.map(rec => `<li>${rec}</li>`).join('')}
</ul>
</div>
</div>
<div class="timeline-item short-term">
<div class="timeline-marker"></div>
<div class="timeline-content">
<h4>Short-term Actions (1-7 days)</h4>
<ul>
${recommendations.shortTerm.map(rec => `<li>${rec}</li>`).join('')}
</ul>
</div>
</div>
<div class="timeline-item long-term">
<div class="timeline-marker"></div>
<div class="timeline-content">
<h4>Long-term Actions (1-4 weeks)</h4>
<ul>
${recommendations.longTerm.map(rec => `<li>${rec}</li>`).join('')}
</ul>
</div>
</div>
</div>
</div>
`;
}
generateTechnicalAppendix(data) {
return `
<div class="technical-appendix">
<h3>Technical Appendix</h3>
<div class="scan-details">
<h4>Scan Configuration</h4>
<table class="technical-table">
<tr><td>Target URL</td><td>${data.target}</td></tr>
<tr><td>Scan Duration</td><td>${data.scanDuration}</td></tr>
<tr><td>Scanner Version</td><td>Web Vulnerability Scanner v1.2.1</td></tr>
<tr><td>Scan Timestamp</td><td>${data.timestamp}</td></tr>
</table>
</div>
<div class="methodology">
<h4>Testing Methodology</h4>
<p>This assessment was conducted using automated vulnerability scanning techniques including:</p>
<ul>
<li>Cross-Site Scripting (XSS) detection with advanced payload testing</li>
<li>SQL Injection testing with database-specific payloads</li>
<li>Security header analysis and misconfiguration detection</li>
<li>SSL/TLS configuration assessment</li>
<li>CSRF protection validation</li>
<li>Directory traversal and path injection testing</li>
<li>WAF detection and bypass attempts</li>
</ul>
</div>
</div>
`;
}
generateComplianceMapping(data) {
return `
<div class="compliance-mapping">
<h3>Compliance and Standards Mapping</h3>
<div class="compliance-grid">
<div class="compliance-item">
<h4>OWASP Top 10 2021</h4>
<div class="compliance-status">${this.getOWASPCompliance(data)}</div>
</div>
<div class="compliance-item">
<h4>PCI DSS</h4>
<div class="compliance-status">${this.getPCICompliance(data)}</div>
</div>
<div class="compliance-item">
<h4>ISO 27001</h4>
<div class="compliance-status">${this.getISOCompliance(data)}</div>
</div>
</div>
</div>
`;
}
getRecommendationsByPriority(data) {
const immediate = [];
const shortTerm = [];
const longTerm = [];
if (data.severityCounts.critical > 0) {
immediate.push('Address all critical vulnerabilities immediately');
immediate.push('Implement emergency security patches');
}
if (data.severityCounts.high > 0) {
immediate.push('Review and fix high-severity vulnerabilities');
shortTerm.push('Implement additional security controls');
}
if (data.severityCounts.medium > 0) {
shortTerm.push('Address medium-severity vulnerabilities');
shortTerm.push('Review security configurations');
}
if (data.severityCounts.low > 0) {
longTerm.push('Address low-severity findings');
}
longTerm.push('Implement regular security scanning');
longTerm.push('Establish security development lifecycle (SDLC)');
longTerm.push('Conduct security awareness training');
return { immediate, shortTerm, longTerm };
}
getDefaultRecommendation(vulnerabilityType) {
const recommendations = {
'xss': 'Implement proper input validation and output encoding. Use Content Security Policy (CSP) headers.',
'sql_injection': 'Use parameterized queries and prepared statements. Implement proper input validation.',
'csrf': 'Implement CSRF tokens and verify referrer headers. Use SameSite cookie attributes.',
'ssl_tls': 'Update SSL/TLS configuration to use secure protocols and cipher suites.',
'headers': 'Configure proper security headers including CSP, HSTS, and X-Frame-Options.',
'directory_traversal': 'Implement proper path validation and restrict file access.',
'default': 'Review and implement appropriate security controls for this vulnerability type.'
};
return recommendations[vulnerabilityType] || recommendations.default;
}
getReferences(vulnerabilityType) {
const references = {
'xss': [
{ title: 'OWASP XSS Prevention Cheat Sheet', url: 'https://owasp.org/www-community/xss-filter-evasion-cheatsheet' },
{ title: 'MDN Content Security Policy', url: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP' }
],
'sql_injection': [
{ title: 'OWASP SQL Injection Prevention', url: 'https://owasp.org/www-community/attacks/SQL_Injection' },
{ title: 'SQL Injection Cheat Sheet', url: 'https://owasp.org/www-community/attacks/SQL_Injection' }
],
'default': [
{ title: 'OWASP Top 10', url: 'https://owasp.org/www-project-top-ten/' },
{ title: 'Web Security Guidelines', url: 'https://owasp.org/www-project-web-security-testing-guide/' }
]
};
return references[vulnerabilityType] || references.default;
}
getOWASPCompliance(data) {
if (data.severityCounts.critical > 0 || data.severityCounts.high > 0) {
return '<span class="non-compliant">Non-Compliant</span>';
}
return '<span class="compliant">Compliant</span>';
}
getPCICompliance(data) {
if (data.severityCounts.critical > 0) {
return '<span class="non-compliant">Non-Compliant</span>';
}
return '<span class="partially-compliant">Review Required</span>';
}
getISOCompliance(data) {
if (data.riskScore > 50) {
return '<span class="non-compliant">Non-Compliant</span>';
}
return '<span class="compliant">Compliant</span>';
}
escapeHtml(text) {
if (typeof text !== 'string') return text;
const htmlEscapes = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
'\'': ''',
'/': '/'
};
return text.replace(/[&<>"'\/]/g, (match) => htmlEscapes[match]);
}
getTemplate() {
return `
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{TITLE}}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background: #f8f9fa;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px;
border-radius: 10px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
font-weight: 300;
}
.header-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
opacity: 0.9;
}
.info-item {
display: flex;
flex-direction: column;
}
.info-label {
font-size: 0.9em;
opacity: 0.8;
margin-bottom: 5px;
}
.info-value {
font-size: 1.1em;
font-weight: 500;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.summary-card {
background: white;
padding: 25px;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
transition: transform 0.2s;
}
.summary-card:hover {
transform: translateY(-2px);
}
.summary-card h4 {
color: #666;
margin-bottom: 15px;
font-size: 1em;
text-transform: uppercase;
letter-spacing: 1px;
}
.risk-indicator {
display: flex;
flex-direction: column;
align-items: center;
}
.risk-score {
font-size: 3em;
font-weight: bold;
margin-bottom: 5px;
}
.risk-high { color: #dc3545; }
.risk-medium { color: #fd7e14; }
.risk-low { color: #28a745; }
.vuln-count .count {
font-size: 2.5em;
font-weight: bold;
color: #6f42c1;
}
.compliance-status .status {
font-size: 1.2em;
font-weight: bold;
padding: 8px 16px;
border-radius: 20px;
}
.compliant { background: #d4edda; color: #155724; }
.partially-compliant { background: #fff3cd; color: #856404; }
.non-compliant { background: #f8d7da; color: #721c24; }
.severity-breakdown {
display: flex;
justify-content: space-around;
margin-top: 20px;
flex-wrap: wrap;
}
.severity-item {
display: flex;
flex-direction: column;
align-items: center;
margin: 10px;
}
.severity-dot {
width: 20px;
height: 20px;
border-radius: 50%;
margin-bottom: 5px;
}
.severity-item.critical .severity-dot { background: #dc3545; }
.severity-item.high .severity-dot { background: #fd7e14; }
.severity-item.medium .severity-dot { background: #ffc107; }
.severity-item.low .severity-dot { background: #28a745; }
.severity-item.info .severity-dot { background: #17a2b8; }
.severity-count {
font-size: 1.5em;
font-weight: bold;
margin-top: 5px;
}
.vulnerability-card {
background: white;
border-radius: 8px;
margin-bottom: 20px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.vuln-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #eee;
}
.vuln-header h5 {
font-size: 1.2em;
color: #333;
}
.severity-badge {
padding: 4px 12px;
border-radius: 15px;
font-size: 0.8em;
font-weight: bold;
text-transform: uppercase;
}
.severity-badge.critical { background: #dc3545; color: white; }
.severity-badge.high { background: #fd7e14; color: white; }
.severity-badge.medium { background: #ffc107; color: black; }
.severity-badge.low { background: #28a745; color: white; }
.severity-badge.info { background: #17a2b8; color: white; }
.vuln-content {
padding: 20px;
}
.vuln-content > div {
margin-bottom: 15px;
}
.code-block {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 10px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
overflow-x: auto;
}
.timeline {
position: relative;
padding-left: 30px;
}
.timeline::before {
content: '';
position: absolute;
left: 15px;
top: 0;
bottom: 0;
width: 2px;
background: #dee2e6;
}
.timeline-item {
position: relative;
margin-bottom: 30px;
}
.timeline-marker {
position: absolute;
left: -23px;
top: 5px;
width: 16px;
height: 16px;
border-radius: 50%;
border: 3px solid white;
box-shadow: 0 0 0 2px #dee2e6;
}
.timeline-item.immediate .timeline-marker { background: #dc3545; }
.timeline-item.short-term .timeline-marker { background: #fd7e14; }
.timeline-item.long-term .timeline-marker { background: #28a745; }
.timeline-content {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.technical-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.technical-table td {
padding: 10px;
border: 1px solid #dee2e6;
}
.technical-table td:first-child {
background: #f8f9fa;
font-weight: bold;
width: 200px;
}
.compliance-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.compliance-item {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
}
.section {
background: white;
margin-bottom: 30px;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.section h3 {
background: #f8f9fa;
padding: 20px;
margin: 0;
border-bottom: 1px solid #dee2e6;
color: #495057;
}
.section-content {
padding: 20px;
}
.no-vulnerabilities {
text-align: center;
padding: 40px;
color: #28a745;
}
.no-vulnerabilities h3 {
margin-bottom: 10px;
}
@media (max-width: 768px) {
.container { padding: 10px; }
.header { padding: 20px; }
.header h1 { font-size: 2em; }
.summary-grid { grid-template-columns: 1fr; }
.severity-breakdown { flex-direction: column; }
}
@media print {
body { background: white; }
.container { max-width: none; }
.section { break-inside: avoid; }
.vulnerability-card { break-inside: avoid; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>{{TITLE}}</h1>
<div class="header-info">
<div class="info-item">
<span class="info-label">Target</span>
<span class="info-value">{{TARGET}}</span>
</div>
<div class="info-item">
<span class="info-label">Scan Date</span>
<span class="info-value">{{TIMESTAMP}}</span>
</div>
<div class="info-item">
<span class="info-label">Duration</span>
<span class="info-value">{{SCAN_DURATION}}</span>
</div>
<div class="info-item">
<span class="info-label">Risk Score</span>
<span class="info-value">{{RISK_SCORE}}/100</span>
</div>
</div>
</div>
<div class="section">
{{EXECUTIVE_SUMMARY}}
</div>
<div class="section">
{{RISK_MATRIX}}
</div>
<div class="section">
<div class="section-content">
{{VULNERABILITY_DETAILS}}
</div>
</div>
<div class="section">
<div class="section-content">
{{RECOMMENDATIONS}}
</div>
</div>
<div class="section">
<div class="section-content">
{{COMPLIANCE_MAPPING}}
</div>
</div>
<div class="section">
<div class="section-content">
{{TECHNICAL_APPENDIX}}
</div>
</div>
</div>
<script>
// Simple chart rendering for risk distribution
document.addEventListener('DOMContentLoaded', function() {
const canvas = document.getElementById('riskChart');
if (canvas) {
const ctx = canvas.getContext('2d');
const data = [
{{CRITICAL_COUNT}}, {{HIGH_COUNT}}, {{MEDIUM_COUNT}},
{{LOW_COUNT}}, {{INFO_COUNT}}
];
const colors = ['#dc3545', '#fd7e14', '#ffc107', '#28a745', '#17a2b8'];
const labels = ['Critical', 'High', 'Medium', 'Low', 'Info'];
// Simple pie chart
let total = data.reduce((a, b) => a + b, 0);
if (total > 0) {
let currentAngle = 0;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = Math.min(centerX, centerY) - 20;
data.forEach((value, index) => {
if (value > 0) {
const sliceAngle = (value / total) * 2 * Math.PI;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + sliceAngle);
ctx.lineTo(centerX, centerY);
ctx.fillStyle = colors[index];
ctx.fill();
currentAngle += sliceAngle;
}
});
} else {
// No data message
ctx.fillStyle = '#666';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.fillText('No vulnerabilities found', canvas.width / 2, canvas.height / 2);
}
}
});
</script>
</body>
</html>
`;
}
}
module.exports = new EnhancedHTMLReporter();