UNPKG

pury

Version:

đŸ›Ąī¸ AI-powered security scanner with advanced threat detection, dual reporting system (detailed & summary), and comprehensive code analysis

325 lines â€ĸ 14.3 kB
import { Severity } from '../types/index.js'; export class SummaryReporter { async output(report) { return this.generateSummaryReport(report); } async writeToFile(report, filePath) { const fs = await import('fs/promises'); const path = await import('path'); // Ensure directory exists await fs.mkdir(path.dirname(filePath), { recursive: true }); const summary = this.generateSummaryReport(report); await fs.writeFile(filePath, summary, 'utf8'); } generateSummaryReport(report) { const lines = []; const basePath = process.cwd(); // Enhanced visual header with better design lines.push(''); lines.push('<div align="center">'); lines.push(''); lines.push('# đŸ›Ąī¸ PuryAI Security Summary'); lines.push(''); lines.push('**✨ Executive Security Report ✨**'); lines.push(''); lines.push('---'); lines.push(''); lines.push('</div>'); lines.push(''); // Quick status indicator const riskLevel = this.calculateRiskLevel(report.summary.severityCount); const statusEmoji = this.getStatusEmoji(report.summary.severityCount); lines.push('<div align="center">'); lines.push(''); lines.push(`## ${statusEmoji} Security Status: ${riskLevel}`); lines.push(''); lines.push('</div>'); lines.push(''); // Scan metadata in a nice box lines.push('> 📅 **Scan Date:** ' + new Date(report.metadata.timestamp).toLocaleString()); lines.push('> âąī¸ **Duration:** ' + (report.summary.scanDuration / 1000).toFixed(2) + 's'); lines.push('> đŸŽ¯ **Analysis Engine:** AI-Powered Multi-Layer Detection'); lines.push(''); lines.push('---'); lines.push(''); // Enhanced visual stats with cards lines.push('## 📊 Analysis Overview'); lines.push(''); // Stats cards layout lines.push('<table align="center">'); lines.push('<tr>'); lines.push('<td align="center" width="150">'); lines.push('<img src="https://img.shields.io/badge/Files-' + report.summary.filesScanned + '-blue?style=for-the-badge&logo=files"/>'); lines.push('<br><b>Files Analyzed</b>'); lines.push('</td>'); lines.push('<td align="center" width="150">'); const issueColor = report.summary.threatsFound > 0 ? 'red' : 'green'; lines.push('<img src="https://img.shields.io/badge/Issues-' + report.summary.threatsFound + '-' + issueColor + '?style=for-the-badge&logo=shield"/>'); lines.push('<br><b>Total Issues</b>'); lines.push('</td>'); lines.push('<td align="center" width="150">'); lines.push('<img src="https://img.shields.io/badge/Duration-' + (report.summary.scanDuration / 1000).toFixed(2) + 's-lightgrey?style=for-the-badge&logo=clock"/>'); lines.push('<br><b>Scan Time</b>'); lines.push('</td>'); lines.push('</tr>'); lines.push('</table>'); lines.push(''); if (report.summary.threatsFound > 0) { const { severityCount } = report.summary; lines.push('### đŸŽ¯ Security Risk Breakdown'); lines.push(''); // Visual severity breakdown with progress bars const total = report.summary.threatsFound; if (severityCount.critical > 0) { const percent = Math.round((severityCount.critical / total) * 100); lines.push(`🚨 **Critical:** ${severityCount.critical} issues (${percent}%)`); lines.push(this.createProgressBar(percent, 'critical')); lines.push(''); } if (severityCount.high > 0) { const percent = Math.round((severityCount.high / total) * 100); lines.push(`âš ī¸ **High:** ${severityCount.high} issues (${percent}%)`); lines.push(this.createProgressBar(percent, 'high')); lines.push(''); } if (severityCount.medium > 0) { const percent = Math.round((severityCount.medium / total) * 100); lines.push(`📋 **Medium:** ${severityCount.medium} issues (${percent}%)`); lines.push(this.createProgressBar(percent, 'medium')); lines.push(''); } if (severityCount.low > 0) { const percent = Math.round((severityCount.low / total) * 100); lines.push(`â„šī¸ **Low:** ${severityCount.low} issues (${percent}%)`); lines.push(this.createProgressBar(percent, 'low')); lines.push(''); } } else { lines.push('<div align="center">'); lines.push(''); lines.push('## ✅ Clean Codebase!'); lines.push(''); lines.push('🎉 **No security issues detected** 🎉'); lines.push(''); lines.push('</div>'); } lines.push(''); lines.push('---'); lines.push(''); if (report.findings.length > 0) { // Top Issues by File const fileIssueCount = this.getFileIssueCounts(report.findings); const topFiles = Array.from(fileIssueCount.entries()) .sort(([, a], [, b]) => b.total - a.total) .slice(0, 10); if (topFiles.length > 0) { lines.push('## 📄 Top Files with Issues'); lines.push(''); topFiles.forEach(([filePath, counts], index) => { const relativePath = this.getRelativePath(filePath, basePath); lines.push(`${index + 1}. **\`${relativePath}\`** - ${counts.total} issues`); const issueBreakdown = []; if (counts.critical > 0) issueBreakdown.push(`🚨 ${counts.critical} Critical`); if (counts.high > 0) issueBreakdown.push(`âš ī¸ ${counts.high} High`); if (counts.medium > 0) issueBreakdown.push(`📋 ${counts.medium} Medium`); if (counts.low > 0) issueBreakdown.push(`â„šī¸ ${counts.low} Low`); if (issueBreakdown.length > 0) { lines.push(` - ${issueBreakdown.join(', ')}`); } lines.push(''); }); } // Critical & High Issues Summary const criticalHighIssues = report.findings.filter(f => f.severity === Severity.CRITICAL || f.severity === Severity.HIGH); if (criticalHighIssues.length > 0) { lines.push('## 🚨 Priority Issues (Critical & High)'); lines.push(''); const groupedByType = this.groupFindingsByType(criticalHighIssues); for (const [issueType, issues] of groupedByType.entries()) { const severityIcon = issues[0]?.severity === Severity.CRITICAL ? '🚨' : 'âš ī¸'; lines.push(`### ${severityIcon} ${issueType}`); lines.push(''); lines.push(`**Found in ${issues.length} location${issues.length > 1 ? 's' : ''}:**`); const fileGroups = this.groupFindingsByFile(issues); for (const [filePath, fileIssues] of fileGroups.entries()) { const relativePath = this.getRelativePath(filePath, basePath); const lineNumbers = fileIssues .map(f => f.line) .filter(Boolean) .sort((a, b) => a - b) .slice(0, 5); if (lineNumbers.length > 0) { const lineDisplay = lineNumbers.length > 3 ? `Lines ${lineNumbers.slice(0, 3).join(', ')}...` : `Line${lineNumbers.length > 1 ? 's' : ''} ${lineNumbers.join(', ')}`; lines.push(`- \`${relativePath}\` - ${lineDisplay}`); } else { lines.push(`- \`${relativePath}\``); } } if (issues[0]?.suggestion) { lines.push(''); lines.push(`**💡 Recommendation:** ${issues[0].suggestion}`); } lines.push(''); } } // Issue Type Breakdown lines.push('## 🔍 Issue Types Overview'); lines.push(''); const typeGroups = this.groupFindingsByType(report.findings); const sortedTypes = Array.from(typeGroups.entries()) .sort(([, a], [, b]) => b.length - a.length) .slice(0, 10); sortedTypes.forEach(([type, issues]) => { const severityIcon = this.getMostSevereIcon(issues); lines.push(`- ${severityIcon} **${type}**: ${issues.length} occurrence${issues.length > 1 ? 's' : ''}`); }); lines.push(''); } else { lines.push('## ✅ Clean Scan Results'); lines.push(''); lines.push('🎉 **No security issues found!** Your code appears clean and secure.'); lines.push(''); } // Recommendations if (report.summary.threatsFound > 0) { lines.push('## 💡 Next Steps'); lines.push(''); const criticalCount = report.summary.severityCount.critical || 0; const highCount = report.summary.severityCount.high || 0; if (criticalCount > 0) { lines.push(`1. **🚨 URGENT:** Address ${criticalCount} critical issue${criticalCount > 1 ? 's' : ''} immediately`); } if (highCount > 0) { lines.push(`${criticalCount > 0 ? '2' : '1'}. **âš ī¸ HIGH PRIORITY:** Review ${highCount} high severity issue${highCount > 1 ? 's' : ''}`); } lines.push(`${criticalCount + highCount > 0 ? '3' : '1'}. Run \`pury scan --format json\` for detailed analysis`); lines.push(`${criticalCount + highCount > 0 ? '4' : '2'}. Check detailed report: \`.pury/scan-results-detailed-*.md\``); } lines.push(''); lines.push('---'); lines.push(''); lines.push(`*Generated by PuryAI v${report.metadata.version} | ${new Date().toISOString()}*`); return lines.join('\n'); } getFileIssueCounts(findings) { const counts = new Map(); for (const finding of findings) { if (!counts.has(finding.file)) { counts.set(finding.file, { total: 0, critical: 0, high: 0, medium: 0, low: 0 }); } const fileCount = counts.get(finding.file); fileCount.total++; fileCount[finding.severity]++; } return counts; } groupFindingsByFile(findings) { const grouped = new Map(); for (const finding of findings) { if (!grouped.has(finding.file)) { grouped.set(finding.file, []); } grouped.get(finding.file).push(finding); } return grouped; } groupFindingsByType(findings) { const grouped = new Map(); for (const finding of findings) { const key = finding.title; if (!grouped.has(key)) { grouped.set(key, []); } grouped.get(key).push(finding); } return grouped; } getMostSevereIcon(issues) { const severities = issues.map(i => i.severity); if (severities.includes(Severity.CRITICAL)) return '🚨'; if (severities.includes(Severity.HIGH)) return 'âš ī¸'; if (severities.includes(Severity.MEDIUM)) return '📋'; return 'â„šī¸'; } getRelativePath(filePath, basePath) { if (filePath.startsWith(basePath)) { const relativePath = filePath.substring(basePath.length); return relativePath.startsWith('/') ? relativePath.substring(1) : relativePath; } return filePath; } calculateRiskLevel(severityCount) { const critical = severityCount.critical || 0; const high = severityCount.high || 0; const medium = severityCount.medium || 0; if (critical > 0) return 'CRITICAL RISK'; if (high > 5) return 'HIGH RISK'; if (high > 0 || medium > 10) return 'MEDIUM RISK'; if (medium > 0) return 'LOW RISK'; return 'SECURE'; } getStatusEmoji(severityCount) { const critical = severityCount.critical || 0; const high = severityCount.high || 0; const medium = severityCount.medium || 0; if (critical > 0) return '🚨'; if (high > 5) return 'âš ī¸'; if (high > 0 || medium > 10) return '📋'; if (medium > 0) return 'â„šī¸'; return '✅'; } createProgressBar(percentage, severity) { const width = 20; const filled = Math.round((percentage / 100) * width); const empty = width - filled; let emoji = ''; switch (severity) { case 'critical': emoji = '🔴'; break; case 'high': emoji = '🟠'; break; case 'medium': emoji = '🟡'; break; case 'low': emoji = 'đŸ”ĩ'; break; default: emoji = 'âšĒ'; } const filledBar = emoji.repeat(filled); const emptyBar = 'âšĢ'.repeat(empty); return `${filledBar}${emptyBar} **${percentage}%**`; } } //# sourceMappingURL=summary.js.map