From 43a4088e307d16d08e852d011265974017accf8f Mon Sep 17 00:00:00 2001 From: Rafia minhaj Date: Wed, 17 Jun 2026 20:35:47 +0530 Subject: [PATCH 1/2] feat(reporting): Implement core backend report generation Introduce core exporters (HTML, PDF, CSV, SARIF) and dedicated unit tests verifying core report generation. Add severity border color styling to findings cards. --- backend/secuscan/reporting.py | 13 +- testing/backend/unit/test_reporting_core.py | 137 ++++++++++++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 testing/backend/unit/test_reporting_core.py diff --git a/backend/secuscan/reporting.py b/backend/secuscan/reporting.py index 93e98aeff..81ab75afa 100644 --- a/backend/secuscan/reporting.py +++ b/backend/secuscan/reporting.py @@ -657,7 +657,7 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s ) finding_markup = "".join( f""" -
+
{cls._escape_html(finding['severity'])}
@@ -846,6 +846,17 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s }} .finding-card {{ overflow: hidden; + border-left: 6px solid var(--info); + }} + .finding-card.severity-critical {{ border-left-color: var(--critical); }} + .finding-card.severity-high {{ border-left-color: var(--high); }} + .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; diff --git a/testing/backend/unit/test_reporting_core.py b/testing/backend/unit/test_reporting_core.py new file mode 100644 index 000000000..597f423f7 --- /dev/null +++ b/testing/backend/unit/test_reporting_core.py @@ -0,0 +1,137 @@ +import json +import csv +import io +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", + "threads": 4, + "enable_ssl": True + } + } + +@pytest.fixture +def sample_result(): + return { + "findings": [ + { + "id": "finding-1", + "title": "SQL Injection", + "category": "Injection", + "severity": "CRITICAL", + "target": "example.com/login.php", + "cvss": 9.8, + "cve": "CVE-2026-0001", + "cwe": "CWE-89", + "cpe": "cpe:/a:test:login:1.0", + "validated": True, + "validation_method": "Exploitation payload sent", + "confidence_reason": "Vulnerability confirmed via active database response.", + "description": "A SQL injection vulnerability exists in the login form.", + "proof": "UNION SELECT username, password FROM users;", + "remediation": "Use parameterized SQL queries and input sanitization.", + "metadata": { + "payload": "' OR 1=1 --" + } + }, + { + "id": "finding-2", + "title": "XSS Vulnerability", + "category": "XSS", + "severity": "HIGH", + "target": "example.com/search.php", + "cvss": 7.5, + "cve": "", + "cwe": "CWE-79", + "validated": False, + "description": "Reflected Cross-Site Scripting via query parameter.", + "proof": "", + "remediation": "Escape user input in output HTML rendering." + } + ], + "structured": { + "open_ports": [80, 443], + "technologies": ["Apache", "PHP"], + "rows": [ + {"port": 80, "service": "http"}, + {"port": 443, "service": "https"} + ] + }, + "summary": [ + "The scan completed successfully.", + "Found two high-severity vulnerabilities." + ], + "errors": [] + } + +def test_generate_csv_report(sample_task, sample_result): + csv_report = ReportGenerator.generate_csv_report(sample_task, sample_result) + assert isinstance(csv_report, str) + + # Read CSV + f = io.StringIO(csv_report) + reader = csv.reader(f) + rows = list(reader) + + # Check headers + assert len(rows) > 0 + headers = rows[0] + expected_headers = [ + "Severity", "Title", "Category", "Target", "CVSS", "CVE", "CPE", + "Validated", "Validation Method", "Confidence Reason", "Description", + "Evidence", "Remediation" + ] + assert headers == expected_headers + + # Check rows + assert len(rows) == 3 # Header + 2 findings + assert rows[1][0] == "CRITICAL" + assert rows[1][1] == "SQL Injection" + assert rows[1][7] == "yes" + assert rows[2][0] == "HIGH" + assert rows[2][7] == "no" + +def test_generate_html_report(sample_task, sample_result): + html_report = ReportGenerator.generate_html_report(sample_task, sample_result) + assert isinstance(html_report, str) + assert "" in html_report + assert "SecuScan Report" in html_report + assert "SQL Injection" in html_report + assert "XSS Vulnerability" in html_report + assert "example.com" in html_report + +def test_generate_pdf_report(sample_task, sample_result): + pdf_report = ReportGenerator.generate_pdf_report(sample_task, sample_result) + assert isinstance(pdf_report, bytes) + # PDF header signature + assert pdf_report.startswith(b"%PDF") + +def test_generate_sarif_report(sample_task, sample_result): + sarif_report = ReportGenerator.generate_sarif_report(sample_task, sample_result) + assert isinstance(sarif_report, str) + + # Parse JSON + sarif_data = json.loads(sarif_report) + assert sarif_data["version"] == "2.1.0" + assert "runs" in sarif_data + run = sarif_data["runs"][0] + assert run["tool"]["driver"]["name"] == "Test Scanner" + + # Check results + results = run["results"] + assert len(results) == 2 + assert results[0]["ruleId"] == "cve-2026-0001" # Derived from CVE + assert results[0]["level"] == "error" + assert results[1]["ruleId"] == "cwe-79" # Derived from CWE From 33c39c7830819f885cdd46530c06d9237cae135d Mon Sep 17 00:00:00 2001 From: Rafia minhaj Date: Wed, 17 Jun 2026 20:38:12 +0530 Subject: [PATCH 2/2] feat(reporting): Add severity distribution chart generation Introduce PIL-based severity bar chart generator and embed it in the HTML report Executive Overview. Add unit tests for the chart generator. --- backend/secuscan/reporting.py | 81 ++++++++++++++++++- testing/backend/unit/test_reporting_charts.py | 69 ++++++++++++++++ 2 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 testing/backend/unit/test_reporting_charts.py diff --git a/backend/secuscan/reporting.py b/backend/secuscan/reporting.py index 81ab75afa..f71658f09 100644 --- a/backend/secuscan/reporting.py +++ b/backend/secuscan/reporting.py @@ -29,6 +29,73 @@ class ReportGenerator: "INFO": (71, 85, 105), } + @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 + 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 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") + return f"data:image/png;base64,{encoded}" + @classmethod def _get_ai_summary(cls, findings): """Return an AI executive summary, or '' when the feature is disabled.""" @@ -647,6 +714,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"]) + severity_chart_data = cls._generate_severity_chart(severity_counts) summary_markup = "".join( f"
  • {cls._escape_html(line)}
  • " for line in payload["summary"] @@ -982,9 +1050,16 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s

    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 +
    +
    diff --git a/testing/backend/unit/test_reporting_charts.py b/testing/backend/unit/test_reporting_charts.py new file mode 100644 index 000000000..05bfbd970 --- /dev/null +++ b/testing/backend/unit/test_reporting_charts.py @@ -0,0 +1,69 @@ +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_result(): + return { + "findings": [ + { + "id": "finding-1", + "title": "SQL Injection", + "category": "Injection", + "severity": "CRITICAL", + "target": "example.com/login.php", + "description": "A SQL injection vulnerability exists in the login form.", + "proof": "UNION SELECT username, password FROM users;", + "remediation": "Use parameterized SQL queries and input sanitization.", + "validated": True, + }, + { + "id": "finding-2", + "title": "XSS Vulnerability", + "category": "XSS", + "severity": "HIGH", + "target": "example.com/search.php", + "description": "Reflected Cross-Site Scripting via query parameter.", + "proof": "", + "remediation": "Escape user input in output HTML rendering.", + "validated": False, + } + ], + "structured": { + "rows": [] + }, + "summary": ["Scan completed successfully."], + "errors": [] + } + +def test_generate_severity_chart(): + severity_counts = { + "CRITICAL": 1, + "HIGH": 2, + "MEDIUM": 0, + "LOW": 4, + "INFO": 0 + } + chart_data = ReportGenerator._generate_severity_chart(severity_counts) + assert isinstance(chart_data, str) + assert chart_data.startswith("data:image/png;base64,") + +def test_html_report_contains_chart(sample_task, sample_result): + html_report = ReportGenerator.generate_html_report(sample_task, sample_result) + assert "Severity Distribution Chart" in html_report + assert "data:image/png;base64," in html_report