diff --git a/backend/secuscan/reporting.py b/backend/secuscan/reporting.py index f71658f09..56395ce82 100644 --- a/backend/secuscan/reporting.py +++ b/backend/secuscan/reporting.py @@ -96,6 +96,54 @@ def _generate_severity_chart(cls, severity_counts: Dict[str, int]) -> str: encoded = base64.b64encode(output.getvalue()).decode("ascii") return f"data:image/png;base64,{encoded}" + @classmethod + def _build_remediation_roadmap(cls, findings: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Categorize and prioritize findings by priority and fix difficulty.""" + roadmap = [] + for finding in findings: + title = finding.get("title", "Untitled finding") + severity = finding.get("severity", "INFO").upper() + category = finding.get("category", "General").lower() + description = finding.get("description", "") + + # Priority mapping + if severity in ("CRITICAL", "HIGH"): + priority = "Immediate" + priority_val = 1 + elif severity == "MEDIUM": + priority = "Scheduled" + priority_val = 2 + else: + priority = "Backlog" + priority_val = 3 + + # Difficulty mapping heuristics + text_to_search = (title + " " + category + " " + description).lower() + if any(kw in text_to_search for kw in ("injection", "rce", "bypass", "exec", "auth", "privilege", "deserialization", "crlf", "cryptographic")): + difficulty = "Complex Fix" + difficulty_val = 3 + elif any(kw in text_to_search for kw in ("version", "outdated", "header", "deprecate", "secret", "token", "password", "key", "config", "ciphers")): + difficulty = "Quick Fix" + difficulty_val = 1 + else: + difficulty = "Standard Fix" + difficulty_val = 2 + + roadmap.append({ + "title": title, + "severity": severity, + "priority": priority, + "priority_val": priority_val, + "difficulty": difficulty, + "difficulty_val": difficulty_val, + "remediation": finding.get("remediation", "No remediation actions provided."), + "target": finding.get("target") or "General target" + }) + + # Sort roadmap: priority_val asc (Immediate -> Scheduled -> Backlog), difficulty_val asc (Quick -> Standard -> Complex) + roadmap.sort(key=lambda x: (x["priority_val"], x["difficulty_val"])) + return roadmap + @classmethod def _get_ai_summary(cls, findings): """Return an AI executive summary, or '' when the feature is disabled.""" @@ -761,6 +809,26 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s """ + # Build Remediation Roadmap HTML Markup + roadmap = cls._build_remediation_roadmap(findings) + roadmap_html_markup = "" + for i, item in enumerate(roadmap): + roadmap_html_markup += f""" +
  • +
    + {cls._escape_html(item['title'])} +
    + {item['priority']} Priority + {item['difficulty']} + Target: {cls._escape_html(item['target'])} +
    +

    Remediation action: {cls._escape_html(item['remediation'])}

    +
    +
  • + """ + if not roadmap_html_markup: + roadmap_html_markup = "
  • No remediation actions needed.
  • " + return f""" @@ -991,6 +1059,72 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s }} .remediation p, .remediation h4 {{ color: var(--success-ink); }} .empty-state {{ text-align: center; }} + + /* Roadmap Checklist Styling */ + .roadmap-checklist {{ + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 14px; + }} + .roadmap-item {{ + background: var(--panel); + border: 1px solid var(--line); + border-radius: 18px; + padding: 20px 24px; + box-shadow: 0 4px 16px rgba(15, 23, 42, 0.02); + transition: border-color 0.2s ease, box-shadow 0.2s ease; + }} + .roadmap-item:hover {{ + border-color: #cbd5e1; + box-shadow: 0 8px 20px rgba(15, 23, 42, 0.05); + }} + .roadmap-details {{ + display: flex; + flex-direction: column; + gap: 6px; + flex-grow: 1; + }} + .roadmap-title {{ + font-size: 15.5px; + font-weight: 600; + color: var(--ink); + transition: color 0.25s ease; + }} + .roadmap-meta {{ + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + }} + .badge {{ + font-size: 10px; + font-weight: 700; + padding: 3px 10px; + border-radius: 99px; + text-transform: uppercase; + letter-spacing: 0.05em; + }} + .badge.priority-immediate {{ background: rgba(239, 68, 68, 0.1); color: var(--high); }} + .badge.priority-scheduled {{ background: rgba(245, 158, 11, 0.1); color: var(--medium); }} + .badge.priority-backlog {{ background: rgba(100, 116, 139, 0.1); color: var(--subtle); }} + + .badge.difficulty-quick-fix {{ background: rgba(34, 197, 94, 0.1); color: #16a34a; }} + .badge.difficulty-standard-fix {{ background: rgba(37, 99, 235, 0.1); color: var(--low); }} + .badge.difficulty-complex-fix {{ background: rgba(217, 119, 6, 0.1); color: var(--medium); }} + + .roadmap-target {{ + font-size: 12.5px; + color: var(--subtle); + }} + .roadmap-action {{ + font-size: 13px; + color: var(--muted); + margin: 4px 0 0; + }} + @page {{ size: A4; margin: 14mm 12mm 16mm; @@ -1053,7 +1187,7 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s

    Key takeaways and severity distribution generated from the parsed assessment data.

    - {f'''

    🤖 AI Executive Summary

    {cls._escape_html(ai_summary)}

    ''' if ai_summary else ""} + {f'''

    🤖 AI Executive Summary

    {cls._escape_html(ai_summary)}

    ''' if ai_summary else ""}
    @@ -1062,6 +1196,14 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s
    +
    +

    Remediation Roadmap

    +

    Actionable steps grouped by implementation priority and complexity. Mark off completed items to track your progress.

    + +
    +

    Scan Parameters

    Runtime configuration captured for this task, including the selected Nikto flags and SecuScan preset context.

    diff --git a/frontend/src/index.css b/frontend/src/index.css index 31549e3b4..f05bcf879 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -3855,3 +3855,117 @@ button, white-space: nowrap; } } + +/* ============================================ + Print / PDF Export Styling + ============================================ */ +@media print { + /* Hide navigation, button toolbars, and dynamic status bars */ + aside, + header .no-print, + .no-print, + footer, + button, + .btn, + .topbar, + .sidebar, + nav, + .app-shell > aside, + .app-shell > div:first-child, + .material-symbols-outlined { + display: none !important; + } + + /* Reset layout for print */ + body, + .app-shell, + .app-main, + main, + #root, + .min-h-screen, + .flex.flex-col { + background: #ffffff !important; + color: #000000 !important; + width: 100% !important; + margin: 0 !important; + padding: 0 !important; + display: block !important; + overflow: visible !important; + } + + /* Reset typography colors, text shadow, and borders */ + h1, h2, h3, h4, h5, h6, p, span, div, td, th { + color: #000000 !important; + text-shadow: none !important; + box-shadow: none !important; + } + + /* Change grid layout to normal blocks to prevent weird clipping in print pages */ + .grid { + display: block !important; + } + + .grid > * { + margin-bottom: 2rem !important; + page-break-inside: avoid !important; + } + + /* Convert charcoal components to clean white panels with basic borders */ + .bg-charcoal, + .bg-charcoal\/30, + .bg-charcoal-dark, + .bg-charcoal\/80, + .charcoal-gradient, + div[class*="bg-charcoal"] { + background: #ffffff !important; + border: 1px solid #000000 !important; + color: #000000 !important; + } + + /* Adjust border line colors for layout elements */ + .border, + .border-b, + .border-t, + .border-l, + .border-r, + .border-accent-silver\/5, + .border-silver-bright\/10, + div[class*="border-"] { + border-color: #cccccc !important; + } + + /* Ensure text colors for threats and statuses are readable and bold */ + .text-rag-red { + color: #c00000 !important; + font-weight: bold !important; + } + .text-rag-amber { + color: #d97706 !important; + font-weight: bold !important; + } + .text-rag-green { + color: #16a34a !important; + font-weight: bold !important; + } + .text-rag-blue { + color: #1e40af !important; + font-weight: bold !important; + } + + /* Enable color printing for background progress bars or fills */ + .bg-rag-red, + .bg-rag-amber, + .bg-rag-green, + .bg-rag-blue { + -webkit-print-color-adjust: exact !important; + print-color-adjust: exact !important; + } + + /* Prevent print elements from clipping across pages */ + section, + .card, + .bg-charcoal { + page-break-inside: avoid !important; + } +} + diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 52a26fd65..1426a60be 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -5,6 +5,7 @@ import { getDashboardSummary, getHealth, cancelTask } from '../api' import { ExecutiveStatsBar } from '../components/ExecutiveStatsBar' import { routePath, routes } from '../routes' import { formatBriefingDate, formatTaskInit, formatLocaleDate, formatLocaleTime } from '../utils/date' +import { generateMarkdownReport } from '../utils/reportBuilder' type Finding = { id: string @@ -176,6 +177,7 @@ const itemVariants = { } export default function Dashboard() { + const [showExportDropdown, setShowExportDropdown] = useState(false) const [summary, setSummary] = useState(emptySummary) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -239,6 +241,30 @@ export default function Dashboard() { } } + const handleDownloadMarkdown = () => { + try { + const markdown = generateMarkdownReport(summary, risk.label) + const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8;' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.setAttribute('download', `secuscan_report_${new Date().toISOString().split('T')[0]}.md`) + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + } catch (err) { + console.error('Failed to generate or download Markdown report:', err) + } + setShowExportDropdown(false) + } + + const handlePrintPDF = () => { + setShowExportDropdown(false) + setTimeout(() => { + window.print() + }, 150) + } + const risk = getRiskProfile(summary) const criticalHigh = summary.critical_findings + summary.high_findings @@ -281,8 +307,58 @@ export default function Dashboard() { initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ delay: 0.2, duration: 0.8, ease: [0.19, 1, 0.22, 1] }} - className="flex flex-col md:flex-row items-start md:items-center gap-8 md:gap-10" + className="flex flex-col md:flex-row items-start md:items-center gap-6 md:gap-8 no-print" > + {/* Export Report Dropdown Button */} +
    + + + + {showExportDropdown && ( + <> +
    setShowExportDropdown(false)} + /> + + + + + + )} + +
    + {/* Integrity Metric - Live Status Panel */}
    {/* Subtle top glow */} diff --git a/frontend/src/utils/reportBuilder.ts b/frontend/src/utils/reportBuilder.ts new file mode 100644 index 000000000..6544dcacf --- /dev/null +++ b/frontend/src/utils/reportBuilder.ts @@ -0,0 +1,107 @@ +import { formatDateLong } from './date' + +export interface Finding { + id: string + severity: string + title: string + target: string + discovered_at: string +} + +export interface Task { + id: string + plugin_id: string + tool_name: string + target: string + status: string + created_at: string + duration_seconds?: number | null +} + +export interface Summary { + total_findings: number + critical_findings: number + high_findings: number + medium_findings: number + low_findings: number + info_findings: number + last_scan_time: string | null + recent_findings: Finding[] + running_tasks: Task[] + recent_tasks: Task[] + scan_activity: { total: number; completed: number; running: number } +} + +export function generateMarkdownReport(summary: Summary, riskLabel: string): string { + const dateStr = new Date().toISOString() + const formattedDate = formatDateLong(dateStr) + + let md = `# SecuScan Security Audit Report\n\n` + md += `**Generated At:** ${formattedDate}\n` + md += `**Overall Threat Level:** ${riskLabel.toUpperCase()}\n\n` + + md += `## 1. Executive Summary & Metrics\n\n` + md += `This section contains a summary of the security vulnerability findings and scanning activity details.\n\n` + md += `| Metric | Count |\n` + md += `| :--- | :--- |\n` + md += `| **Total Findings** | ${summary.total_findings} |\n` + md += `| Critical Risk | ${summary.critical_findings} |\n` + md += `| High Severity | ${summary.high_findings} |\n` + md += `| Medium Alert | ${summary.medium_findings} |\n` + md += `| Low Exposure | ${summary.low_findings} |\n` + md += `| Informational | ${summary.info_findings} |\n\n` + + md += `### Scan Activity Summary\n` + md += `- **Total Tasks Executed:** ${summary.scan_activity.total}\n` + md += `- **Completed Tasks:** ${summary.scan_activity.completed}\n` + md += `- **Active/Running Tasks:** ${summary.scan_activity.running}\n\n` + + md += `## 2. Recent Audit Findings\n\n` + if (summary.recent_findings && summary.recent_findings.length > 0) { + md += `| Title | Severity | Target | Discovered At |\n` + md += `| :--- | :--- | :--- | :--- |\n` + summary.recent_findings.forEach((finding) => { + const discoveredDate = finding.discovered_at ? formatDateLong(finding.discovered_at) : 'N/A' + md += `| ${finding.title} | **${finding.severity.toUpperCase()}** | \`${finding.target}\` | ${discoveredDate} |\n` + }) + md += `\n` + } else { + md += `No recent findings detected.\n\n` + } + + md += `## 3. Recent Scans & Task Activity\n\n` + if (summary.recent_tasks && summary.recent_tasks.length > 0) { + md += `| Tool/Plugin | Target | Status | Created At | Duration |\n` + md += `| :--- | :--- | :--- | :--- | :--- |\n` + summary.recent_tasks.forEach((task) => { + const createdDate = task.created_at ? formatDateLong(task.created_at) : 'N/A' + const duration = task.duration_seconds != null && task.duration_seconds > 0 + ? `${Math.round(task.duration_seconds)}s` + : 'N/A' + const name = task.tool_name || task.plugin_id + md += `| ${name.toUpperCase()} | \`${task.target}\` | ${task.status.toUpperCase()} | ${createdDate} | ${duration} |\n` + }) + md += `\n` + } else { + md += `No recent task activity logged.\n\n` + } + + md += `## 4. Remediation Roadmap & Recommendations\n\n` + const hasCriticalOrHigh = summary.critical_findings > 0 || summary.high_findings > 0 + if (hasCriticalOrHigh) { + md += `### High Priority Action Items\n\n` + if (summary.critical_findings > 0) { + md += `- [ ] **Critical Alert:** Address the ${summary.critical_findings} critical vulnerability/vulnerabilities immediately to prevent host or network compromise.\n` + } + if (summary.high_findings > 0) { + md += `- [ ] **High Alert:** Review and remediate the ${summary.high_findings} high-severity issue(s).\n` + } + md += `- [ ] Implement restrictive access controls and verify input validation across targets.\n` + } else { + md += `No critical or high severity vulnerabilities found. Continue regular scanning cycles to maintain security posture.\n` + } + + md += `\n---\n*Report generated by SecuScan Workspace.*` + + return md +} diff --git a/frontend/testing/unit/utils/reportBuilder.test.ts b/frontend/testing/unit/utils/reportBuilder.test.ts new file mode 100644 index 000000000..af1b6c17f --- /dev/null +++ b/frontend/testing/unit/utils/reportBuilder.test.ts @@ -0,0 +1,105 @@ +import { describe, test, expect } from "vitest"; +import { generateMarkdownReport, Summary } from "../../../src/utils/reportBuilder"; + +describe("reportBuilder utility", () => { + const sampleSummary: Summary = { + total_findings: 5, + critical_findings: 1, + high_findings: 2, + medium_findings: 1, + low_findings: 1, + info_findings: 0, + last_scan_time: "2026-05-12T10:30:00Z", + recent_findings: [ + { + id: "f1", + severity: "critical", + title: "SQL Injection", + target: "http://target1.local", + discovered_at: "2026-05-12T10:30:00Z" + }, + { + id: "f2", + severity: "high", + title: "Cross-Site Scripting", + target: "http://target2.local", + discovered_at: "2026-05-12T10:35:00Z" + } + ], + running_tasks: [], + recent_tasks: [ + { + id: "t1", + plugin_id: "sqlmap", + tool_name: "SQLMap Scanner", + target: "http://target1.local", + status: "completed", + created_at: "2026-05-12T10:29:00Z", + duration_seconds: 45 + } + ], + scan_activity: { + total: 10, + completed: 9, + running: 1 + } + }; + + test("generates correct report structure under threat condition", () => { + const report = generateMarkdownReport(sampleSummary, "Severe"); + + // Header checks + expect(report).toContain("# SecuScan Security Audit Report"); + expect(report).toContain("Overall Threat Level:** SEVERE"); + + // Executive summary checks + expect(report).toContain("| Metric | Count |"); + expect(report).toContain("| Critical Risk | 1 |"); + expect(report).toContain("| High Severity | 2 |"); + + // Scan activity details + expect(report).toContain("**Total Tasks Executed:** 10"); + expect(report).toContain("**Completed Tasks:** 9"); + expect(report).toContain("**Active/Running Tasks:** 1"); + + // Recent findings checks + expect(report).toContain("| Title | Severity | Target | Discovered At |"); + expect(report).toContain("| SQL Injection | **CRITICAL** | `http://target1.local` |"); + + // Task activity checks + expect(report).toContain("| Tool/Plugin | Target | Status | Created At | Duration |"); + expect(report).toContain("| SQLMAP SCANNER | `http://target1.local` | COMPLETED |"); + + // Remediation Roadmap checks + expect(report).toContain("### High Priority Action Items"); + expect(report).toContain("**Critical Alert:** Address the 1 critical vulnerability/vulnerabilities immediately"); + expect(report).toContain("**High Alert:** Review and remediate the 2 high-severity issue(s)"); + }); + + test("handles empty lists gracefully", () => { + const emptySummary: Summary = { + total_findings: 0, + critical_findings: 0, + high_findings: 0, + medium_findings: 0, + low_findings: 0, + info_findings: 0, + last_scan_time: null, + recent_findings: [], + running_tasks: [], + recent_tasks: [], + scan_activity: { + total: 0, + completed: 0, + running: 0 + } + }; + + const report = generateMarkdownReport(emptySummary, "Stable"); + + expect(report).toContain("Overall Threat Level:** STABLE"); + expect(report).toContain("No recent findings detected."); + expect(report).toContain("No recent task activity logged."); + expect(report).toContain("No critical or high severity vulnerabilities found. Continue regular scanning cycles to maintain security posture."); + }); +}); diff --git a/testing/backend/unit/test_reporting_roadmap.py b/testing/backend/unit/test_reporting_roadmap.py new file mode 100644 index 000000000..ca7729ecf --- /dev/null +++ b/testing/backend/unit/test_reporting_roadmap.py @@ -0,0 +1,80 @@ +import pytest +from backend.secuscan.reporting import ReportGenerator + +@pytest.fixture +def sample_task(): + return { + "id": "task-123", + "tool_name": "Test Scanner", + "plugin_id": "test_plugin", + "target": "example.com", + "status": "completed", + "created_at": "2026-06-17T12:00:00.000000Z", + "preset": "Full Scan", + "command_used": "test-scanner --target example.com", + "inputs": {"depth": "deep"} + } + +@pytest.fixture +def sample_findings(): + return [ + { + "id": "finding-1", + "title": "SQL Injection in Login Form", + "category": "Injection", + "severity": "CRITICAL", + "description": "SQL Injection vulnerability.", + "remediation": "Use parameterized queries.", + "target": "example.com/login.php" + }, + { + "id": "finding-2", + "title": "Outdated Software Version Detected", + "category": "Version Scan", + "severity": "LOW", + "description": "The remote server runs an outdated software version.", + "remediation": "Upgrade the server software.", + "target": "example.com" + }, + { + "id": "finding-3", + "title": "Missing Security Headers", + "category": "Headers", + "severity": "MEDIUM", + "description": "Vulnerability due to missing security headers.", + "remediation": "Add security headers to response.", + "target": "example.com" + } + ] + +def test_build_remediation_roadmap(sample_findings): + roadmap = ReportGenerator._build_remediation_roadmap(sample_findings) + assert len(roadmap) == 3 + + # First item: SQL Injection (CRITICAL -> Immediate Priority, Complex Fix) + assert roadmap[0]["title"] == "SQL Injection in Login Form" + assert roadmap[0]["priority"] == "Immediate" + assert roadmap[0]["difficulty"] == "Complex Fix" + + # Second item: Missing Security Headers (MEDIUM -> Scheduled Priority, Quick Fix due to 'header' keyword) + assert roadmap[1]["title"] == "Missing Security Headers" + assert roadmap[1]["priority"] == "Scheduled" + assert roadmap[1]["difficulty"] == "Quick Fix" + + # Third item: Outdated Software Version (LOW -> Backlog Priority, Quick Fix due to 'outdated' / 'version' keyword) + assert roadmap[2]["title"] == "Outdated Software Version Detected" + assert roadmap[2]["priority"] == "Backlog" + assert roadmap[2]["difficulty"] == "Quick Fix" + +def test_html_report_contains_roadmap(sample_task, sample_findings): + result = { + "findings": sample_findings, + "structured": {"rows": []}, + "summary": ["Scan completed successfully."], + "errors": [] + } + html_report = ReportGenerator.generate_html_report(sample_task, result) + assert "Remediation Roadmap" in html_report + assert "SQL Injection in Login Form" in html_report + assert "Immediate Priority" in html_report + assert "Complex Fix" in html_report