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""" +
Remediation action: {cls._escape_html(item['remediation'])}
+Key takeaways and severity distribution generated from the parsed assessment data.
🤖 AI Executive Summary
{cls._escape_html(ai_summary)}
{cls._escape_html(ai_summary)}
Actionable steps grouped by implementation priority and complexity. Mark off completed items to track your progress.
+Runtime configuration captured for this task, including the selected Nikto flags and SecuScan preset context.
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