YashashviAlva's picture
Initial commit for HF Spaces deploy
7b4f5dd
/* ═══════════════════════════════════════════════════════════════
ReportView β€” Full report page with charts, table, and exports
═══════════════════════════════════════════════════════════════ */
import { useMemo, useCallback } from 'react';
import { useScan, VIEWS } from '../context/ScanContext';
import SeverityBadge from './SeverityBadge';
import PrivacyCertificate from './PrivacyCertificate';
import SeverityChart from './SeverityChart';
import AMDMigrationPanel from './AMDMigrationPanel';
import './ReportView.css';
function formatDuration(ms) {
const seconds = Math.floor(ms / 1000);
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${minutes}m ${secs}s`;
}
export default function ReportView() {
const {
findings, fixes, summary, elapsedTime, scanId,
setView, resetScan, amdMigration,
} = useScan();
// Also check complete event for migration data
const migrationData = amdMigration
|| (summary?.amd_migration_guide)
|| null;
// Severity breakdown
const severityCounts = useMemo(() => {
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
findings.forEach(f => {
if (counts[f.severity] !== undefined) counts[f.severity]++;
});
return counts;
}, [findings]);
// Fix lookup
const fixMap = useMemo(() => {
const map = {};
fixes.forEach(f => { map[f.findingId] = f; });
return map;
}, [fixes]);
// Generate JSON report
const generateJsonReport = useCallback(() => {
const report = {
scanId: scanId || 'cs-report',
timestamp: new Date().toISOString(),
summary: {
totalFindings: findings.length,
...severityCounts,
fixesGenerated: fixes.length,
scanDuration: elapsedTime,
},
findings: findings.map(f => ({
id: f.id,
title: f.title,
severity: f.severity,
cwe: f.cwe,
description: f.description,
file: f.file,
line: f.line,
code: f.code,
suggestion: f.suggestion,
fix: fixMap[f.id] || null,
})),
privacyCertificate: {
localInference: true,
dataRetention: 'none',
externalApiCalls: 'none',
},
};
const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'codesentry_report.json';
a.click();
URL.revokeObjectURL(url);
}, [findings, fixes, severityCounts, scanId, elapsedTime, fixMap]);
// Generate Markdown report
const generateMarkdownReport = useCallback(() => {
let md = `# πŸ›‘οΈ CodeSentry Security Report\n\n`;
md += `**Scan ID:** ${scanId || 'N/A'}\n`;
md += `**Date:** ${new Date().toLocaleString()}\n`;
md += `**Duration:** ${formatDuration(elapsedTime)}\n\n`;
md += `## Summary\n\n`;
md += `| Severity | Count |\n|----------|-------|\n`;
md += `| πŸ”΄ Critical | ${severityCounts.critical} |\n`;
md += `| 🟠 High | ${severityCounts.high} |\n`;
md += `| 🟑 Medium | ${severityCounts.medium} |\n`;
md += `| 🟒 Low | ${severityCounts.low} |\n`;
md += `| **Total** | **${findings.length}** |\n\n`;
md += `## Findings\n\n`;
findings.forEach((f, i) => {
md += `### ${i + 1}. ${f.title}\n\n`;
md += `- **Severity:** ${f.severity.toUpperCase()}\n`;
if (f.cwe) md += `- **CWE:** ${f.cwe}\n`;
md += `- **File:** \`${f.file}:${f.line}\`\n`;
md += `- **Description:** ${f.description}\n\n`;
if (f.code) md += `\`\`\`\n${f.code}\n\`\`\`\n\n`;
if (f.suggestion) md += `**Recommendation:** ${f.suggestion}\n\n`;
const fix = fixMap[f.id];
if (fix) {
md += `**AI-Generated Fix:**\n\n`;
md += `Before:\n\`\`\`\n${fix.before}\n\`\`\`\n\n`;
md += `After:\n\`\`\`\n${fix.after}\n\`\`\`\n\n`;
if (fix.explanation) md += `${fix.explanation}\n\n`;
}
md += `---\n\n`;
});
md += `## Privacy Certificate\n\n`;
md += `- βœ… All analysis performed locally\n`;
md += `- βœ… Zero data retention\n`;
md += `- βœ… No external API calls during scan\n`;
md += `- βœ… Code never left this machine\n\n`;
md += `---\n\n*Generated by CodeSentry β€” AI Security Copilot*\n`;
const blob = new Blob([md], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'SECURITY_REPORT.md';
a.click();
URL.revokeObjectURL(url);
}, [findings, fixes, severityCounts, scanId, elapsedTime, fixMap]);
// Generate PR Description
const copyPrDescription = useCallback(() => {
let pr = `## πŸ›‘οΈ Security Scan Results β€” CodeSentry\n\n`;
pr += `**${findings.length} issues found** (${severityCounts.critical} critical, ${severityCounts.high} high, ${severityCounts.medium} medium, ${severityCounts.low} low)\n\n`;
if (severityCounts.critical > 0) {
pr += `### πŸ”΄ Critical Issues\n`;
findings.filter(f => f.severity === 'critical').forEach(f => {
pr += `- **${f.title}** β€” \`${f.file}:${f.line}\` ${f.cwe ? `(${f.cwe})` : ''}\n`;
pr += ` - **Problem:** ${f.description}\n`;
const fix = fixMap[f.id];
if (fix && fix.explanation) pr += ` - **Solution:** ${fix.explanation}\n`;
else if (f.suggestion) pr += ` - **Solution:** ${f.suggestion}\n`;
});
pr += `\n`;
}
if (severityCounts.high > 0) {
pr += `### 🟠 High Issues\n`;
findings.filter(f => f.severity === 'high').forEach(f => {
pr += `- **${f.title}** β€” \`${f.file}:${f.line}\` ${f.cwe ? `(${f.cwe})` : ''}\n`;
pr += ` - **Problem:** ${f.description}\n`;
const fix = fixMap[f.id];
if (fix && fix.explanation) pr += ` - **Solution:** ${fix.explanation}\n`;
else if (f.suggestion) pr += ` - **Solution:** ${f.suggestion}\n`;
});
pr += `\n`;
}
if (severityCounts.medium > 0) {
pr += `### 🟑 Medium Issues\n`;
findings.filter(f => f.severity === 'medium').forEach(f => {
pr += `- **${f.title}** β€” \`${f.file}:${f.line}\` ${f.cwe ? `(${f.cwe})` : ''}\n`;
pr += ` - **Problem:** ${f.description}\n`;
const fix = fixMap[f.id];
if (fix && fix.explanation) pr += ` - **Solution:** ${fix.explanation}\n`;
else if (f.suggestion) pr += ` - **Solution:** ${f.suggestion}\n`;
});
pr += `\n`;
}
if (severityCounts.low > 0) {
pr += `### 🟒 Low Issues\n`;
findings.filter(f => f.severity === 'low').forEach(f => {
pr += `- **${f.title}** β€” \`${f.file}:${f.line}\` ${f.cwe ? `(${f.cwe})` : ''}\n`;
pr += ` - **Problem:** ${f.description}\n`;
const fix = fixMap[f.id];
if (fix && fix.explanation) pr += ` - **Solution:** ${fix.explanation}\n`;
else if (f.suggestion) pr += ` - **Solution:** ${f.suggestion}\n`;
});
pr += `\n`;
}
pr += `---\n*Scanned by [CodeSentry](https://github.com) β€” AI Security Copilot*\n`;
navigator.clipboard.writeText(pr).then(() => {
alert('PR description copied to clipboard!');
});
}, [findings, severityCounts]);
return (
<div className="report-view">
{/* Header */}
<header className="header-bar">
<div className="header-logo" onClick={resetScan} style={{ cursor: 'pointer' }}>
<span className="shield-icon">πŸ›‘οΈ</span>
<span className="logo-text">CodeSentry</span>
</div>
<div className="header-actions">
<button
className="btn btn-ghost btn-sm"
onClick={() => setView(VIEWS.ANALYSIS)}
>
← Back to Analysis
</button>
<button className="btn btn-ghost btn-sm" onClick={resetScan}>
↻ New Scan
</button>
</div>
</header>
<div className="report-content container">
{/* Report Header */}
<div className="report-header animate-fade-in-up">
<h1>Security Report</h1>
<p className="report-subtitle text-secondary">
Scan completed in {formatDuration(elapsedTime)} β€’ {summary?.filesAnalyzed || 24} files analyzed β€’ {summary?.linesScanned || 4872} lines scanned
</p>
</div>
{/* Summary Cards */}
<div className="summary-grid animate-fade-in-up" style={{ animationDelay: '0.1s' }}>
<div className="summary-card glass-card-static critical-card">
<span className="summary-value">{severityCounts.critical}</span>
<span className="summary-label">Critical</span>
</div>
<div className="summary-card glass-card-static high-card">
<span className="summary-value">{severityCounts.high}</span>
<span className="summary-label">High</span>
</div>
<div className="summary-card glass-card-static medium-card">
<span className="summary-value">{severityCounts.medium}</span>
<span className="summary-label">Medium</span>
</div>
<div className="summary-card glass-card-static low-card">
<span className="summary-value">{severityCounts.low}</span>
<span className="summary-label">Low</span>
</div>
<div className="summary-card glass-card-static total-card">
<span className="summary-value">{findings.length}</span>
<span className="summary-label">Total</span>
</div>
<div className="summary-card glass-card-static fixes-card">
<span className="summary-value">{fixes.length}</span>
<span className="summary-label">Fixes</span>
</div>
</div>
{/* Chart + Export Row */}
<div className="report-row animate-fade-in-up" style={{ animationDelay: '0.2s' }}>
{/* Donut Chart */}
<div className="chart-section glass-card-static">
<h3>Severity Distribution</h3>
<div className="chart-container">
<SeverityChart counts={severityCounts} />
</div>
</div>
{/* Export Section */}
<div className="export-section glass-card-static">
<h3>Export Report</h3>
<p className="text-secondary" style={{ fontSize: '0.85rem', marginBottom: 'var(--space-lg)' }}>
Download your security analysis in multiple formats
</p>
<div className="export-buttons">
<button id="export-json" className="btn btn-secondary" onClick={generateJsonReport}>
πŸ“„ JSON Report
</button>
<button id="export-md" className="btn btn-secondary" onClick={generateMarkdownReport}>
πŸ“ SECURITY_REPORT.md
</button>
<button id="export-pr" className="btn btn-secondary" onClick={copyPrDescription}>
πŸ“‹ Copy PR Description
</button>
</div>
</div>
</div>
{/* Findings Table */}
<div className="findings-table-section glass-card-static animate-fade-in-up" style={{ animationDelay: '0.3s' }}>
<h3>All Findings</h3>
<div className="table-wrapper">
<table className="findings-table">
<thead>
<tr>
<th>ID</th>
<th>Severity</th>
<th>Title</th>
<th>File</th>
<th>CWE</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
{findings.map((f, i) => (
<tr key={f.id || i}>
<td className="mono">{f.id || `F-${i + 1}`}</td>
<td><SeverityBadge severity={f.severity} /></td>
<td>{f.title}</td>
<td className="mono file-cell">{f.file}:{f.line}</td>
<td className="mono">{f.cwe || 'β€”'}</td>
<td>
{fixMap[f.id] ? (
<span className="fix-ready-tag"><span>βœ…</span> Ready</span>
) : f.fixAvailable ? (
<span className="fix-available-tag"><span>πŸ”§</span> Available</span>
) : (
<span className="text-tertiary">β€”</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Fixes Section */}
{fixes.length > 0 && (
<div className="fixes-section animate-fade-in-up" style={{ animationDelay: '0.4s' }}>
<h3>AI-Generated Fixes</h3>
<div className="fixes-list">
{fixes.map((fix, i) => (
<div key={i} className="fix-card glass-card-static">
<div className="fix-card-header">
<span className="fix-card-icon">πŸ”§</span>
<div>
<h4>{fix.title}</h4>
<span className="mono text-secondary" style={{ fontSize: '0.75rem' }}>
Fixing: {fix.findingId}
</span>
</div>
</div>
<div className="diff-container">
<div className="diff-panel diff-before">
<div className="diff-header">Before</div>
<div className="code-block">
<pre><code>{fix.before}</code></pre>
</div>
</div>
<div className="diff-arrow">β†’</div>
<div className="diff-panel diff-after">
<div className="diff-header">After</div>
<div className="code-block">
<pre><code>{fix.after}</code></pre>
</div>
</div>
</div>
{fix.explanation && (
<p className="fix-explanation">{fix.explanation}</p>
)}
</div>
))}
</div>
</div>
)}
{/* AMD ROCm Migration Advisor */}
{migrationData && (
<div className="animate-fade-in-up" style={{ animationDelay: '0.45s' }}>
<AMDMigrationPanel migrationData={migrationData} />
</div>
)}
{/* Privacy Certificate */}
<div className="animate-fade-in-up" style={{ animationDelay: '0.5s' }}>
<PrivacyCertificate scanId={scanId} />
</div>
</div>
</div>
);
}