From b4f3ed3c3ae9489e4880a54718ce46845a79b25d Mon Sep 17 00:00:00 2001 From: Rafia minhaj Date: Sun, 14 Jun 2026 18:17:26 +0530 Subject: [PATCH 1/8] feat(reporting): Fix HTML f-string curly braces and implement GSSoC '26 Scan Report Exporter --- backend/secuscan/reporting.py | 316 +++++++++++++++++++++++++++++++++- 1 file changed, 309 insertions(+), 7 deletions(-) diff --git a/backend/secuscan/reporting.py b/backend/secuscan/reporting.py index 93e98aeff..134e1623f 100644 --- a/backend/secuscan/reporting.py +++ b/backend/secuscan/reporting.py @@ -321,6 +321,109 @@ def _format_timestamp(value: str) -> str: continue return value + @classmethod + def _generate_severity_chart(cls, severity_counts: Dict[str, int]) -> str: + """Generate a base64 PNG horizontal bar chart representing the vulnerability distribution.""" + width, height = 480, 160 + img = Image.new("RGBA", (width, height), (255, 255, 255, 0)) + draw = ImageDraw.Draw(img) + + colors_map = { + "CRITICAL": (153, 27, 27, 255), + "HIGH": (220, 38, 38, 255), + "MEDIUM": (217, 119, 6, 255), + "LOW": (37, 99, 235, 255), + "INFO": (71, 85, 105, 255) + } + + severities = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"] + max_val = max(severity_counts.values()) if any(severity_counts.values()) else 1 + + # Draw background container + draw.rounded_rectangle([0, 0, width - 1, height - 1], radius=8, fill=(248, 250, 252, 255), outline=(226, 232, 240, 255), width=1) + + y_offset = 12 + bar_height = 16 + spacing = 10 + x_start = 110 + max_bar_width = 280 + + from PIL import ImageFont + try: + font = ImageFont.load_default() + except Exception: + font = None + + for i, sev in enumerate(severities): + count = severity_counts.get(sev, 0) + bar_len = int((count / max_val) * max_bar_width) if count > 0 else 0 + color = colors_map[sev] + + # Draw bar + draw.rectangle([x_start, y_offset, x_start + bar_len, y_offset + bar_height], fill=color) + + # Draw labels + if font: + # Severity label + draw.text((20, y_offset + 2), sev.title(), fill=(71, 85, 105, 255), font=font) + # Count label + draw.text((x_start + bar_len + 10, y_offset + 2), str(count), fill=(15, 23, 42, 255), font=font) + + y_offset += bar_height + spacing + + output = io.BytesIO() + img.save(output, format="PNG") + 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 _generate_pdf_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> str: """Generate conservative HTML/CSS that xhtml2pdf can paginate reliably.""" @@ -647,6 +750,9 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s rows_icon = cls._icon_data_uri("rows", "2563eb") clock_icon = cls._icon_data_uri("clock", "475569") target_html = cls._escape_html_with_breaks(payload["target"]) + + # New base64 Severity Chart + severity_chart_data = cls._generate_severity_chart(severity_counts) summary_markup = "".join( f"
  • {cls._escape_html(line)}
  • " for line in payload["summary"] @@ -692,6 +798,30 @@ 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""" +
  • + +
  • + """ + if not roadmap_html_markup: + roadmap_html_markup = "
  • No remediation actions needed.
  • " return f""" @@ -940,10 +1070,129 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s }} .section, .meta-card, .stat-card, .finding-card {{ box-shadow: none; }} }} + + /* View Switcher Styling */ + .shell.view-executive .developer-only {{ display: none !important; }} + .shell.view-developer .executive-only {{ display: none !important; }} + + /* Roadmap Checklist Styling */ + .roadmap-checklist {{ + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 12px; + }} + .roadmap-item {{ + background: var(--panel); + border: 1px solid var(--line); + border-radius: 16px; + padding: 16px 20px; + box-shadow: 0 4px 12px rgba(15, 23, 42, 0.02); + }} + .roadmap-item:hover {{ + border-color: #cbd5e1; + }} + .checkbox-container {{ + display: flex; + gap: 16px; + align-items: flex-start; + cursor: pointer; + user-select: none; + width: 100%; + }} + .checkbox-container input {{ + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; + }} + .checkmark {{ + flex: 0 0 20px; + height: 20px; + width: 20px; + background-color: #f1f5f9; + border: 2px solid #cbd5e1; + border-radius: 6px; + position: relative; + margin-top: 3px; + }} + .checkbox-container:hover input ~ .checkmark {{ + background-color: #e2e8f0; + }} + .checkbox-container input:checked ~ .checkmark {{ + background-color: #22c55e; + border-color: #22c55e; + }} + .checkmark:after {{ + content: ""; + position: absolute; + display: none; + }} + .checkbox-container input:checked ~ .checkmark:after {{ + display: block; + }} + .checkbox-container .checkmark:after {{ + left: 6px; + top: 2px; + width: 5px; + height: 10px; + border: solid white; + border-width: 0 2.5px 2.5px 0; + transform: rotate(45deg); + }} + .checkbox-container input:checked ~ .roadmap-details .roadmap-title {{ + text-decoration: line-through; + color: var(--subtle); + }} + .roadmap-details {{ + display: flex; + flex-direction: column; + gap: 6px; + flex-grow: 1; + }} + .roadmap-title {{ + font-size: 15px; + font-weight: 600; + color: var(--ink); + }} + .roadmap-meta {{ + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + }} + .badge {{ + font-size: 10px; + font-weight: 700; + padding: 2px 8px; + 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: 12px; + color: var(--subtle); + }} + .roadmap-action {{ + font-size: 12.5px; + color: var(--muted); + margin: 4px 0 0; + }} -
    +
    @@ -969,25 +1218,78 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s
    {len(payload['rows'])}
    -
    + +
    +
    + + +
    +
    + + +

    Executive Overview

    -

    Key takeaways generated from the parsed assessment data.

    - {f'''

    🤖 AI Executive Summary

    {cls._escape_html(ai_summary)}

    ''' if ai_summary else ""} -
      {summary_markup}
    +

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

    +
    +
    + {f'''

    🤖 AI Executive Summary

    {cls._escape_html(ai_summary)}

    ''' if ai_summary else ""} +
      {summary_markup}
    +
    +
    + Severity Distribution Chart +
    +
    +
    + +
    +

    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.

    {parameter_markup}
    -
    +

    Technical Findings

    Detailed finding cards with severity context, supporting evidence, and recommended next actions.

    {finding_markup}
    + + """ From bf46bb0383c338397712e5539d98ddec0e497591 Mon Sep 17 00:00:00 2001 From: Rafia minhaj Date: Sun, 14 Jun 2026 18:21:16 +0530 Subject: [PATCH 2/8] style(reporting): Upgrade HTML report to premium visual design with Google Fonts and animations --- backend/secuscan/reporting.py | 345 ++++++++++++++++++++++++---------- 1 file changed, 241 insertions(+), 104 deletions(-) diff --git a/backend/secuscan/reporting.py b/backend/secuscan/reporting.py index 134e1623f..a26c3a60b 100644 --- a/backend/secuscan/reporting.py +++ b/backend/secuscan/reporting.py @@ -349,18 +349,30 @@ def _generate_severity_chart(cls, severity_counts: Dict[str, int]) -> str: max_bar_width = 280 from PIL import ImageFont - try: - font = ImageFont.load_default() - except Exception: - font = None + font = None + for font_name in ("arial.ttf", "Helvetica.ttf", "segoeui.ttf", "sans-serif.ttf"): + try: + font = ImageFont.truetype(font_name, 12) + break + except Exception: + continue + if font is None: + try: + font = ImageFont.load_default() + except Exception: + font = None for i, sev in enumerate(severities): count = severity_counts.get(sev, 0) bar_len = int((count / max_val) * max_bar_width) if count > 0 else 0 color = colors_map[sev] - # Draw bar - draw.rectangle([x_start, y_offset, x_start + bar_len, y_offset + bar_height], fill=color) + # Draw background progress track + draw.rounded_rectangle([x_start, y_offset, x_start + max_bar_width, y_offset + bar_height], radius=4, fill=(226, 232, 240, 255)) + + # Draw actual severity bar + if count > 0: + draw.rounded_rectangle([x_start, y_offset, x_start + bar_len, y_offset + bar_height], radius=4, fill=color) # Draw labels if font: @@ -763,7 +775,7 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s ) finding_markup = "".join( f""" -
    +
    {cls._escape_html(finding['severity'])}
    @@ -829,6 +841,9 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s SecuScan Report - {cls._escape_html(payload['target'])} + + + @@ -1219,10 +1362,10 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s
    -
    -
    - - +
    +
    + +
    @@ -1232,7 +1375,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}
    @@ -1269,14 +1412,8 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s tabs.forEach(tab => {{ if (tab.innerText.toLowerCase().includes(viewName)) {{ tab.classList.add('active'); - tab.style.background = 'var(--panel)'; - tab.style.color = 'var(--ink)'; - tab.style.boxShadow = '0 4px 10px rgba(0,0,0,0.04)'; }} else {{ tab.classList.remove('active'); - tab.style.background = 'transparent'; - tab.style.color = 'var(--subtle)'; - tab.style.boxShadow = 'none'; }} }}); From d2d4338fd9a7024be703a1e4833108e8d2b9aec4 Mon Sep 17 00:00:00 2001 From: Rafia minhaj Date: Sun, 14 Jun 2026 18:23:23 +0530 Subject: [PATCH 3/8] style(reporting): Add print buttons, SEO meta description, and test IDs --- backend/secuscan/reporting.py | 39 ++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/backend/secuscan/reporting.py b/backend/secuscan/reporting.py index a26c3a60b..031d9ba86 100644 --- a/backend/secuscan/reporting.py +++ b/backend/secuscan/reporting.py @@ -840,6 +840,7 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s + SecuScan Report - {cls._escape_html(payload['target'])} @@ -1304,6 +1305,29 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s margin: 4px 0 0; }} + /* Print Button Styling */ + .print-btn {{ + background: var(--ink); + color: white; + border: none; + padding: 10px 22px; + border-radius: 12px; + font-weight: 600; + font-size: 13.5px; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + transition: opacity 0.2s ease, transform 0.1s ease; + box-shadow: 0 4px 12px rgba(15, 23, 42, 0.15); + }} + .print-btn:hover {{ + opacity: 0.9; + }} + .print-btn:active {{ + transform: scale(0.98); + }} + @page {{ size: A4; margin: 14mm 12mm 16mm; @@ -1331,11 +1355,16 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s break-inside: avoid; }} .section, .meta-card, .stat-card, .finding-card {{ box-shadow: none; }} + + /* Hide controls on PDF print */ + .toolbar, .print-btn, .view-switcher {{ + display: none !important; + }} }} -
    +
    @@ -1364,9 +1393,13 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s
    - - + +
    +
    From 63061afc39df163b5649233fbcf87088a991ad13 Mon Sep 17 00:00:00 2001 From: Rafia minhaj Date: Sun, 14 Jun 2026 18:38:04 +0530 Subject: [PATCH 4/8] style(reporting): Remove trailing whitespaces to pass CI formatting check --- backend/secuscan/reporting.py | 54 +++++++++++++++++------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/backend/secuscan/reporting.py b/backend/secuscan/reporting.py index 031d9ba86..2048f1b0f 100644 --- a/backend/secuscan/reporting.py +++ b/backend/secuscan/reporting.py @@ -327,7 +327,7 @@ def _generate_severity_chart(cls, severity_counts: Dict[str, int]) -> str: width, height = 480, 160 img = Image.new("RGBA", (width, height), (255, 255, 255, 0)) draw = ImageDraw.Draw(img) - + colors_map = { "CRITICAL": (153, 27, 27, 255), "HIGH": (220, 38, 38, 255), @@ -335,19 +335,19 @@ def _generate_severity_chart(cls, severity_counts: Dict[str, int]) -> str: "LOW": (37, 99, 235, 255), "INFO": (71, 85, 105, 255) } - + severities = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"] max_val = max(severity_counts.values()) if any(severity_counts.values()) else 1 - + # Draw background container draw.rounded_rectangle([0, 0, width - 1, height - 1], radius=8, fill=(248, 250, 252, 255), outline=(226, 232, 240, 255), width=1) - + y_offset = 12 bar_height = 16 spacing = 10 x_start = 110 max_bar_width = 280 - + from PIL import ImageFont font = None for font_name in ("arial.ttf", "Helvetica.ttf", "segoeui.ttf", "sans-serif.ttf"): @@ -361,28 +361,28 @@ def _generate_severity_chart(cls, severity_counts: Dict[str, int]) -> str: font = ImageFont.load_default() except Exception: font = None - + for i, sev in enumerate(severities): count = severity_counts.get(sev, 0) bar_len = int((count / max_val) * max_bar_width) if count > 0 else 0 color = colors_map[sev] - + # Draw background progress track draw.rounded_rectangle([x_start, y_offset, x_start + max_bar_width, y_offset + bar_height], radius=4, fill=(226, 232, 240, 255)) - + # Draw actual severity bar if count > 0: draw.rounded_rectangle([x_start, y_offset, x_start + bar_len, y_offset + bar_height], radius=4, fill=color) - + # Draw labels if font: # Severity label draw.text((20, y_offset + 2), sev.title(), fill=(71, 85, 105, 255), font=font) # Count label draw.text((x_start + bar_len + 10, y_offset + 2), str(count), fill=(15, 23, 42, 255), font=font) - + y_offset += bar_height + spacing - + output = io.BytesIO() img.save(output, format="PNG") encoded = base64.b64encode(output.getvalue()).decode("ascii") @@ -397,7 +397,7 @@ def _build_remediation_roadmap(cls, findings: List[Dict[str, Any]]) -> List[Dict 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" @@ -408,7 +408,7 @@ def _build_remediation_roadmap(cls, findings: List[Dict[str, Any]]) -> List[Dict 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")): @@ -420,7 +420,7 @@ def _build_remediation_roadmap(cls, findings: List[Dict[str, Any]]) -> List[Dict else: difficulty = "Standard Fix" difficulty_val = 2 - + roadmap.append({ "title": title, "severity": severity, @@ -431,7 +431,7 @@ def _build_remediation_roadmap(cls, findings: List[Dict[str, Any]]) -> List[Dict "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 @@ -762,7 +762,7 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s rows_icon = cls._icon_data_uri("rows", "2563eb") clock_icon = cls._icon_data_uri("clock", "475569") target_html = cls._escape_html_with_breaks(payload["target"]) - + # New base64 Severity Chart severity_chart_data = cls._generate_severity_chart(severity_counts) @@ -810,7 +810,7 @@ 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 = "" @@ -946,7 +946,7 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s }} .meta-grid {{ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }} .stat-grid {{ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); margin-top: 28px; }} - + .meta-card, .stat-card, .finding-card {{ background: var(--panel); border: 1px solid var(--line); @@ -1032,12 +1032,12 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s .finding-card.severity-medium {{ border-left-color: var(--medium); }} .finding-card.severity-low {{ border-left-color: var(--low); }} .finding-card.severity-info {{ border-left-color: var(--info); }} - + .finding-card:hover {{ transform: translateY(-2px); box-shadow: 0 16px 36px rgba(15, 23, 42, 0.08); }} - + .finding-top {{ display: flex; gap: 16px; @@ -1074,7 +1074,7 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s .severity-medium {{ background: var(--medium); }} .severity-low {{ background: var(--low); }} .severity-info {{ background: var(--info); }} - + .finding-body {{ padding: 24px; display: grid; @@ -1112,7 +1112,7 @@ 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; color: var(--subtle); padding: 40px 20px; }} - + /* Toolbar & Segmented Switcher */ .toolbar {{ margin-top: 28px; @@ -1149,7 +1149,7 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s color: var(--ink); box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08); }} - + /* View Switcher Display Rules */ .shell.view-executive .developer-only {{ display: none !important; }} .shell.view-developer .executive-only {{ display: none !important; }} @@ -1290,11 +1290,11 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s .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); @@ -1355,7 +1355,7 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s break-inside: avoid; }} .section, .meta-card, .stat-card, .finding-card {{ box-shadow: none; }} - + /* Hide controls on PDF print */ .toolbar, .print-btn, .view-switcher {{ display: none !important; @@ -1449,7 +1449,7 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s tab.classList.remove('active'); }} }}); - + const shell = document.querySelector('.shell'); if (viewName === 'executive') {{ shell.classList.remove('view-developer'); From c21dffa288b8e93295c775207eeb8c59c7255494 Mon Sep 17 00:00:00 2001 From: Rafia minhaj Date: Mon, 15 Jun 2026 22:13:14 +0530 Subject: [PATCH 5/8] feat(reporting): Implement frontend Markdown and Print PDF export capability for scans --- frontend/src/index.css | 50 +++++++++++++++++++++++ frontend/src/pages/TaskDetails.tsx | 64 ++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/frontend/src/index.css b/frontend/src/index.css index 31549e3b4..a362686e9 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -3855,3 +3855,53 @@ button, white-space: nowrap; } } + +@media print { + /* Hide UI navigation, sidebar, and export buttons */ + aside, [role="navigation"], header .flex-wrap, footer, button, .no-print, nav { + display: none !important; + } + + /* Reset layout for clean paper printing */ + body, html, main, .bg-charcoal-dark, .bg-charcoal, .bg-black\/20 { + background: white !important; + color: black !important; + } + + /* Force high contrast black text for readability */ + .text-silver-bright, .text-silver, h1, h2, h3, h4, span, p, div { + color: black !important; + } + + /* Reset layout constraints and widths */ + main { + margin-left: 0 !important; + padding: 0 !important; + } + + .min-h-screen { + min-height: auto !important; + padding: 0 !important; + } + + /* Force grid containers into full-width column layouts */ + .grid { + display: block !important; + } + + .grid > * { + margin-bottom: 20px !important; + width: 100% !important; + } + + /* Clean headings page-breaking behavior */ + h1, h2, h3, h4 { + page-break-after: avoid; + page-break-inside: avoid; + } + + section, .border { + page-break-inside: avoid; + border-color: #ccc !important; + } +} diff --git a/frontend/src/pages/TaskDetails.tsx b/frontend/src/pages/TaskDetails.tsx index 2daf86f0f..eaa990e26 100644 --- a/frontend/src/pages/TaskDetails.tsx +++ b/frontend/src/pages/TaskDetails.tsx @@ -10,6 +10,7 @@ import { Download01Icon, HtmlFile02Icon, Pdf02Icon, + PrinterIcon, Refresh01Icon, } from '@hugeicons/core-free-icons' import { API_BASE, getPluginSchema, getTaskResult, getTaskStatus, PluginFieldSchema, PluginSchemaResponse, startTask, ExecutionContext } from '../api' @@ -769,6 +770,55 @@ export default function TaskDetails() { setExpandedFindingRows(prev => ({ ...prev, [index]: !prev[index] })) } + const handleExportMarkdown = () => { + if (!task) return + + let md = `# SecuScan Security Report\n\n` + md += `## Scan Summary\n` + md += `- **Target:** ${task.target}\n` + md += `- **Tool:** ${toolLabel}\n` + md += `- **Plugin ID:** ${task.plugin_id || 'N/A'}\n` + md += `- **Status:** ${task.status.toUpperCase()}\n` + md += `- **Duration:** ${durationLabel}\n` + md += `- **Start Time:** ${task.started_at ? formatDateLong(task.started_at) : 'N/A'}\n` + md += `- **Finish Time:** ${task.completed_at ? formatDateLong(task.completed_at) : 'N/A'}\n\n` + + md += `## Threat Level: ${dominantSeverity.toUpperCase()}\n\n` + + md += `## Threat Distribution\n` + md += `- **Critical:** ${severityCounts.critical || 0}\n` + md += `- **High:** ${severityCounts.high || 0}\n` + md += `- **Medium:** ${severityCounts.medium || 0}\n` + md += `- **Low:** ${severityCounts.low || 0}\n` + md += `- **Info:** ${severityCounts.info || 0}\n\n` + + md += `## Vulnerability Findings (${findings.length})\n\n` + if (findings.length > 0) { + findings.forEach((f: any, idx: number) => { + md += `### ${idx + 1}. [${f.severity.toUpperCase()}] ${stripAnsi(f.title)}\n` + md += `- **Severity:** ${f.severity}\n` + if (f.description) { + md += `- **Description:** ${stripAnsi(f.description)}\n` + } + if (f.remediation) { + md += `- **Remediation:** ${stripAnsi(f.remediation)}\n` + } + md += `\n` + }) + } else { + md += `No vulnerabilities identified.\n` + } + + const blob = new Blob([md], { type: 'text/markdown;charset=utf-8;' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.setAttribute('download', `secuscan_report_${task.task_id}.md`) + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + } + const DetailCard = ({ label, value, subValue }: { label: string, value: string, subValue?: string }) => (
    @@ -842,6 +892,20 @@ export default function TaskDetails() { )} {task.status === 'completed' && ( <> + +