From 9e78e0409cb527abde7cffbfd9e15617d33ffaf0 Mon Sep 17 00:00:00 2001 From: Rafia minhaj Date: Wed, 17 Jun 2026 20:41:26 +0530 Subject: [PATCH 1/2] feat(reporting): Add remediation roadmap heuristics and checklist UI Introduce priority and difficulty metrics for finding remediation actions. Add the roadmap checklist section to the HTML report. Add unit tests for the roadmap heuristics. --- backend/secuscan/reporting.py | 144 +++++++++++++++++- .../backend/unit/test_reporting_roadmap.py | 80 ++++++++++ 2 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 testing/backend/unit/test_reporting_roadmap.py 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 ""}
      {summary_markup}
    @@ -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.

    +
      + {roadmap_html_markup} +
    +
    +

    Scan Parameters

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

    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 From 739422ed6a88cdb6a54a734ddcb3413c6f86cf11 Mon Sep 17 00:00:00 2001 From: Rafia minhaj Date: Tue, 23 Jun 2026 12:31:53 +0530 Subject: [PATCH 2/2] feat(report): implement client-side markdown and pdf report exporter #944 --- frontend/src/index.css | 114 ++++++++++++++++++ frontend/src/pages/Dashboard.tsx | 78 +++++++++++- frontend/src/utils/reportBuilder.ts | 107 ++++++++++++++++ .../testing/unit/utils/reportBuilder.test.ts | 105 ++++++++++++++++ 4 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 frontend/src/utils/reportBuilder.ts create mode 100644 frontend/testing/unit/utils/reportBuilder.test.ts 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."); + }); +});