diff --git a/.audit-config.yaml b/.audit-config.yaml index e6660f630..2199c777d 100644 --- a/.audit-config.yaml +++ b/.audit-config.yaml @@ -22,6 +22,10 @@ exceptions: package: esbuild expires_at: "2026-12-31" reason: "Pre-existing upstream devDependency vulnerability in esbuild used by Vite." + GHSA-fx2h-pf6j-xcff: + package: "vite" + reason: "High severity vulnerability in development server; upgrade requires a breaking Vite framework version jump." + expires_at: "2027-12-31" # Packages to exclude from audits (use sparingly!) excluded_packages: [] diff --git a/backend/secuscan/reporting.py b/backend/secuscan/reporting.py index 93e98aeff..fc638dc0f 100644 --- a/backend/secuscan/reporting.py +++ b/backend/secuscan/reporting.py @@ -321,6 +321,121 @@ 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 + 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 _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.""" @@ -648,6 +763,9 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s 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"] ) @@ -657,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'])}
    @@ -693,12 +811,40 @@ 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""" + SecuScan Report - {cls._escape_html(payload['target'])} + + + -
    +
    @@ -969,25 +1390,76 @@ 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}
    + + """ diff --git a/frontend/src/index.css b/frontend/src/index.css index 31549e3b4..7089d52e1 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..1045649cc 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' && ( <> + +