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
112 changes: 112 additions & 0 deletions backend/secuscan/reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,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()
54 changes: 54 additions & 0 deletions backend/secuscan/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,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."""
Expand Down
Loading