From 911a4cdfd766988a29e9515c2070449db1a9273e Mon Sep 17 00:00:00 2001 From: kavin553 Date: Thu, 2 Jul 2026 23:02:51 +0530 Subject: [PATCH] feat : add markdown report export support --- backend/secuscan/reporting.py | 112 ++++++++++++++++++++++++++++++++++ backend/secuscan/routes.py | 54 ++++++++++++++++ 2 files changed, 166 insertions(+) diff --git a/backend/secuscan/reporting.py b/backend/secuscan/reporting.py index f71658f09..a6cc2a7a9 100644 --- a/backend/secuscan/reporting.py +++ b/backend/secuscan/reporting.py @@ -1256,6 +1256,118 @@ def generate_sarif_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> } return json.dumps(sarif_output, indent=2) + @classmethod + def generate_markdown_report(cls, task: Dict[str, Any], result: Dict[str, Any]) -> str: + """Generate a Markdown report.""" + + payload = cls._build_report_payload(task, result) + + findings = payload["findings"] + severity_counts = payload["severity_counts"] + ai_summary = cls._get_ai_summary(findings) + + md = [] + + md.append("# SecuScan Security Report\n") + + md.append(f"**Target:** {payload['target']}") + md.append(f"**Tool:** {payload['tool_name']}") + md.append(f"**Status:** {payload['status'].upper()}") + md.append(f"**Generated:** {payload['generated_at']}") + md.append("") + + md.append("## Executive Summary\n") + + if ai_summary: + md.append("### AI Executive Summary\n") + md.append(ai_summary) + md.append("") + + for line in payload["summary"]: + md.append(f"- {line}") + + md.append("") + + md.append("## Severity Summary\n") + + md.append("| Severity | Count |") + md.append("|----------|------:|") + + for severity in cls.SEVERITY_ORDER: + md.append(f"| {severity} | {severity_counts.get(severity,0)} |") + + md.append("") + + md.append("## Scan Parameters\n") + + for item in payload["scan_parameters"]: + md.append(f"- **{item['label']}**: {item['value']}") + + md.append("") + + md.append("## Technical Findings\n") + + if not findings: + md.append("No structured findings were available.\n") + else: + + for i, finding in enumerate(findings, start=1): + + md.append(f"### {i}. {finding['title']}\n") + + md.append(f"- **Severity:** {finding['severity']}") + md.append(f"- **Category:** {finding['category']}") + md.append(f"- **Target:** {finding['target'] or payload['target']}") + + if finding["cvss"] is not None: + md.append(f"- **CVSS:** {finding['cvss']}") + + if finding["cve"]: + md.append(f"- **CVE:** {finding['cve']}") + + if finding["cwe"]: + md.append(f"- **CWE:** {finding['cwe']}") + + if finding["cpe"]: + md.append(f"- **CPE:** {finding['cpe']}") + + md.append(f"- **Validated:** {'Yes' if finding['validated'] else 'No'}") + + if finding["validation_method"]: + md.append(f"- **Validation Method:** {finding['validation_method']}") + + if finding["confidence_reason"]: + md.append(f"- **Confidence Reason:** {finding['confidence_reason']}") + + md.append("") + + md.append("#### Description") + + md.append(finding["description"]) + md.append("") + + if finding["proof"]: + md.append("#### Evidence") + md.append("```text") + md.append(finding["proof"]) + md.append("```") + md.append("") + + if finding["remediation"]: + md.append("#### Recommended Action") + md.append(finding["remediation"]) + md.append("") + + if finding["references"]: + md.append("#### References") + for ref in finding["references"]: + md.append(f"- {ref}") + md.append("") + + md.append("---") + md.append("") + + return "\n".join(md) reporting = ReportGenerator() diff --git a/backend/secuscan/routes.py b/backend/secuscan/routes.py index 44d1103d6..696fc7bd4 100644 --- a/backend/secuscan/routes.py +++ b/backend/secuscan/routes.py @@ -639,6 +639,60 @@ async def download_html_report(task_id: str, owner: str = Depends(get_current_ow headers={"Content-Disposition": f'attachment; filename="{build_report_filename(dict(task_row), "html")}"'} ) +@router.get("/task/{task_id}/report/md", dependencies=[Depends(report_download_limiter)]) +async def download_markdown_report(task_id: str, owner: str = Depends(get_current_owner)): + """Download task results as a Markdown report.""" + db = await get_db() + task_row = await db.fetchone( + "SELECT id, owner_id, plugin_id, tool_name, target, status, created_at, preset, inputs_json, command_used, structured_json FROM tasks WHERE id = ?", + (task_id,) + ) + + if not task_row: + raise HTTPException(status_code=404, detail="Task not found") + + if task_row["owner_id"] != owner: + raise HTTPException(status_code=403, detail="You do not have access to this task") + + if task_row["status"] not in ["completed", "failed"]: + raise HTTPException(status_code=400, detail="Task is not finished yet") + + try: + structured_data = ( + json.loads(task_row["structured_json"]) + if task_row["structured_json"] + else {} + ) + + markdown_content = reporting.generate_markdown_report( + dict(task_row), + {"structured": structured_data} + ) + + except Exception: + return _report_generation_error_response(task_id, "md") + + await db.log_audit( + "report_downloaded", + f"Markdown report downloaded for task {task_id}", + context={ + "format": "md", + "task_id": task_id, + "plugin_id": task_row["plugin_id"], + }, + task_id=task_id, + plugin_id=task_row["plugin_id"], + ) + + return Response( + content=markdown_content, + media_type="text/markdown", + headers={ + "Content-Disposition": + f'attachment; filename="{build_report_filename(dict(task_row), "md")}"' + }, + ) + @router.get("/task/{task_id}/report/pdf", dependencies=[Depends(report_download_limiter)]) async def download_pdf_report(task_id: str, owner: str = Depends(get_current_owner)): """Download task results as a PDF report."""