Source code for avise.reportgen.reporters.markdown_reporter

"""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