pury
Version:
đĄī¸ AI-powered security scanner with advanced threat detection, dual reporting system (detailed & summary), and comprehensive code analysis
325 lines âĸ 14.3 kB
JavaScript
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