Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 143 additions & 1 deletion backend/secuscan/reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -761,6 +809,26 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s
</article>
"""

# Build Remediation Roadmap HTML Markup
roadmap = cls._build_remediation_roadmap(findings)
roadmap_html_markup = ""
for i, item in enumerate(roadmap):
roadmap_html_markup += f"""
<li class="roadmap-item">
<div class="roadmap-details">
<span class="roadmap-title">{cls._escape_html(item['title'])}</span>
<div class="roadmap-meta">
<span class="badge priority-{item['priority'].lower()}">{item['priority']} Priority</span>
<span class="badge difficulty-{item['difficulty'].lower().replace(' ', '-')}">{item['difficulty']}</span>
<span class="roadmap-target">Target: {cls._escape_html(item['target'])}</span>
</div>
<p class="roadmap-action"><b>Remediation action:</b> {cls._escape_html(item['remediation'])}</p>
</div>
</li>
"""
if not roadmap_html_markup:
roadmap_html_markup = "<li class='empty-state'>No remediation actions needed.</li>"

return f"""<!DOCTYPE html>
<html lang="en">
<head>
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1053,7 +1187,7 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s
<p class="section-copy">Key takeaways and severity distribution generated from the parsed assessment data.</p>
<div class="executive-container" style="display: flex; gap: 24px; align-items: flex-start; flex-wrap: wrap;">
<div style="flex: 1; min-width: 300px;">
{f'''<div style="margin:0 0 18px;padding:16px 20px;background:#eff6ff;border-left:4px solid #2563eb;border-radius:14px;"><p style="margin:0 0 6px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:#1d4ed8;">&#129302; AI Executive Summary</p><p style="margin:0;color:#1e293b;line-height:1.65;">{cls._escape_html(ai_summary)}</p></div>''' if ai_summary else ""}
{f'''<div class="ai-summary-card"><h4>🤖 AI Executive Summary</h4><p>{cls._escape_html(ai_summary)}</p></div>''' if ai_summary else ""}
<ul class="summary-list">{summary_markup}</ul>
</div>
<div class="chart-container" style="flex: 0 0 400px; max-width: 100%;">
Expand All @@ -1062,6 +1196,14 @@ def generate_html_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> s
</div>
</section>

<section class="section">
<h2><img class="section-icon" src="{shield_icon}" alt="">Remediation Roadmap</h2>
<p class="section-copy">Actionable steps grouped by implementation priority and complexity. Mark off completed items to track your progress.</p>
<ul class="roadmap-checklist">
{roadmap_html_markup}
</ul>
</section>

<section class="section">
<h2><img class="section-icon" src="{target_icon}" alt="">Scan Parameters</h2>
<p class="section-copy">Runtime configuration captured for this task, including the selected Nikto flags and SecuScan preset context.</p>
Expand Down
114 changes: 114 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Loading
Loading