"""Markdown report writer."""
from pathlib import Path
from typing import Dict, Any
from .base import BaseReporter
from ...pipelines.languagemodel import ReportData, EvaluationResult
[docs]
class MarkdownReporter(BaseReporter):
"""Writes reports in Markdown (MD) format."""
format_name = "markdown"
file_extension = ".md"
group_results: bool = True # Can be overridden in SET scripts
[docs]
def write(self, report_data: ReportData, output_path: Path) -> None:
"""Write report data as Markdown file.
Args:
report_data: The report data to write
output_path: Path to the output file / directory
"""
markdown = self._generate_markdown(report_data)
with open(output_path, "w") as f:
f.write(markdown)
def _generate_markdown(self, report_data: ReportData) -> str:
"""Generate complete Markdown report."""
summary = report_data.summary
config = report_data.configuration
# Use grouping if enabled in reporter or report_data
use_grouping = getattr(self, "group_results", True) and getattr(
report_data, "group_results", True
)
md = f"""# AVISE Security Report
## Security Evaluation Test Information
| Field | Value |
|-------|-------|
| SET Name | {report_data.set_name} |
| Timestamp | {report_data.timestamp} |
| Duration | {report_data.execution_time_seconds}s |
| ELM Evaluation | {"Yes" if config.get("elm_evaluation_used") else "No"} |
## Summary
| Metric | Count | Rate |
|--------|-------|------|
| Total SET Cases | {summary["total_set_cases"]} | - |
| Passed | {summary["passed"]} | {summary["pass_rate"]}% |
| Failed | {summary["failed"]} | {summary["fail_rate"]}% |
| Inconclusive | {summary["error"]} | - |
---
## Results
"""
if use_grouping:
md += self._get_results_grouped(report_data)
else:
md += self._get_results(report_data.results)
if report_data.ai_summary:
md += self._get_ai_summary(report_data.ai_summary)
md += "\n*Report generated by AVISE*\n"
return md
def _get_ai_summary(self, ai_summary: Dict[str, Any]) -> str:
"""Generate AI summary section for Markdown report."""
notes_md = "\n".join(f"- {note}" for note in ai_summary.get("notes", []))
return f"""---
## AI Security Evaluation Summary
### Issue Summary
{ai_summary.get("issue_summary", "")}
### Recommended Remediations
{ai_summary.get("recommended_remediations", "")}
### Notes
{notes_md}
"""
def _get_results(self, results: list) -> str:
"""Generate list of results."""
md = ""
for result in results:
if isinstance(result, EvaluationResult):
set_ = {
"set_id": result.set_id,
"prompt": result.prompt,
"response": result.response,
"status": result.status,
"reason": result.reason,
"attack_type": result.metadata.get("attack_type", ""),
"description": result.metadata.get("description", ""),
"full_conversation": result.metadata.get("full_conversation", []),
}
if result.elm_evaluation:
set_["elm_evaluation"] = result.elm_evaluation
else:
set_ = result
md += self._get_set_item(set_)
md += "---\n\n"
return md
def _get_results_grouped(self, report_data) -> str:
"""Generate grouped results by vulnerability_group."""
grouped = report_data.group_by_vulnerability()
md = ""
for group_name, results in sorted(grouped.items()):
group_stats = self._calculate_group_stats(results)
md += f"### {group_name}\n\n"
md += f"**Stats:** {group_stats['passed']} passed | {group_stats['failed']} failed | {group_stats['error']} error\n\n"
for result in results:
md += self._get_result_item(result)
md += "---\n\n"
return md
def _calculate_group_stats(self, results: list) -> dict:
"""Calculate stats for a group of results."""
passed = sum(1 for r in results if r.status == "passed")
failed = sum(1 for r in results if r.status == "failed")
error = sum(1 for r in results if r.status == "error")
return {"passed": passed, "failed": failed, "error": error}
def _get_result_item(self, result: EvaluationResult) -> str:
"""Generate Markdown for a single result item (used in grouped view)."""
set_ = {
"set_id": result.set_id,
"prompt": result.prompt,
"response": result.response,
"status": result.status,
"reason": result.reason,
"attack_type": result.metadata.get("attack_type", ""),
"description": result.metadata.get("description", ""),
"full_conversation": result.metadata.get("full_conversation", []),
}
if result.elm_evaluation:
set_["elm_evaluation"] = result.elm_evaluation
return self._get_set_item(set_)
def _get_set_item(self, set_: Dict[str, Any]) -> str:
"""Generate Markdown for a single SET item."""
status_indicator = set_["status"].upper()
set_label = set_.get("attack_type") or set_.get("description") or ""
if set_label:
set_label = f" - {set_label}"
md = f"""#### [{status_indicator}] {set_["set_id"]}{set_label}
"""
# Check for conversation format (memory test)
if set_.get("full_conversation"):
md += "**Conversation:**\n"
for msg in set_["full_conversation"]:
role = msg.get("role", "user")
content = msg.get("content", "")
md += f"- **{role}:** {content}\n"
md += "\n"
else:
# Standard prompt/response format
md += f"""**Prompt:**
```
{set_.get("prompt", "")}
```
**Response:**
```
{set_.get("response", "")}
```
"""
md += f"**Reason:** {set_.get('reason', '')}\n\n"
if "elm_evaluation" in set_:
md += f"""**ELM Evaluation:**
> {set_["elm_evaluation"]}
"""
return md