UNPKG

web-vuln-scanner

Version:

Advanced, lightweight web vulnerability scanner with smart detection and easy-to-use interface

902 lines (791 loc) 30.8 kB
/** * 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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', '\'': '&#x27;', '/': '&#x2F;' }; return text.replace(/[&<>"'\/]/g, (match) => htmlEscapes[match]); } getTemplate() { return ` <!DOCTYPE html> <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();