Spaces:
Running
Running
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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> | |
| ); | |
| } | |