""" Report Generator for Shield Agents. Generates HTML reports with interactive visualizations, severity breakdowns, and detailed findings. Also supports JSON and plain text output. """ import json import logging import os from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional from .sarif import SARIFGenerator from ..utils.helpers import calculate_risk_score logger = logging.getLogger("shield_agents.report") class ReportGenerator: """Generate security scan reports in multiple formats. Supported formats: - HTML: Interactive web report with charts and filtering - SARIF: For GitHub Security tab integration - JSON: Machine-readable structured data """ def __init__(self, config=None): self.config = config self.sarif_generator = SARIFGenerator(config) def generate_report( self, findings: List[Dict[str, Any]], target_path: str = ".", output_dir: str = "./shield-reports", formats: Optional[List[str]] = None, scan_stats: Optional[Dict[str, Any]] = None, ) -> Dict[str, str]: """Generate reports in specified formats. Args: findings: List of finding dictionaries. target_path: Path that was scanned. output_dir: Directory to save reports. formats: List of formats to generate (html, sarif, json). scan_stats: Optional scan statistics. Returns: Dictionary of format -> file path. """ if formats is None: formats = ["html", "sarif", "json"] # Ensure output directory exists Path(output_dir).mkdir(parents=True, exist_ok=True) # Calculate summary stats risk_score = calculate_risk_score(findings) summary = self._build_summary(findings, risk_score, scan_stats) output_files = {} for fmt in formats: try: if fmt == "html": path = self._generate_html(findings, summary, target_path, output_dir) output_files["html"] = path elif fmt == "sarif": path = self._generate_sarif(findings, target_path, output_dir) output_files["sarif"] = path elif fmt == "json": path = self._generate_json(findings, summary, target_path, output_dir) output_files["json"] = path else: logger.warning(f"Unknown report format: {fmt}") except Exception as e: logger.error(f"Failed to generate {fmt} report: {e}") logger.info(f"Generated reports: {list(output_files.keys())}") return output_files def _build_summary( self, findings: List[Dict[str, Any]], risk_score: int, scan_stats: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """Build summary statistics from findings.""" severity_counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0} category_counts: Dict[str, int] = {} source_counts: Dict[str, int] = {} file_counts: Dict[str, int] = {} for f in findings: sev = f.get("severity", "MEDIUM").upper() severity_counts[sev] = severity_counts.get(sev, 0) + 1 cat = f.get("category", "unknown") category_counts[cat] = category_counts.get(cat, 0) + 1 source = f.get("source", f.get("agent", "unknown")) source_counts[source] = source_counts.get(source, 0) + 1 file = f.get("file", "unknown") file_counts[file] = file_counts.get(file, 0) + 1 summary = { "total_findings": len(findings), "risk_score": risk_score, "severity_counts": severity_counts, "category_counts": category_counts, "source_counts": source_counts, "most_affected_files": sorted(file_counts.items(), key=lambda x: x[1], reverse=True)[:10], "generated_at": datetime.now().isoformat(), } if scan_stats: summary["scan_stats"] = scan_stats return summary def _generate_sarif(self, findings: List[Dict[str, Any]], target_path: str, output_dir: str) -> str: """Generate SARIF report.""" output_path = os.path.join(output_dir, "results.sarif") return self.sarif_generator.save(findings, output_path, target_path) def _generate_json( self, findings: List[Dict[str, Any]], summary: Dict[str, Any], target_path: str, output_dir: str, ) -> str: """Generate JSON report.""" output_path = os.path.join(output_dir, "results.json") report = { "shield_agents_version": "2.0.0", "target_path": target_path, "summary": summary, "findings": findings, } with open(output_path, "w") as f: json.dump(report, f, indent=2) return output_path def _generate_html( self, findings: List[Dict[str, Any]], summary: Dict[str, Any], target_path: str, output_dir: str, ) -> str: """Generate HTML report with interactive visualizations.""" output_path = os.path.join(output_dir, "report.html") severity_colors = { "CRITICAL": "#dc3545", "HIGH": "#fd7e14", "MEDIUM": "#ffc107", "LOW": "#0dcaf0", "INFO": "#6c757d", } # Risk level text if summary["risk_score"] >= 75: risk_level = "CRITICAL" risk_color = "#dc3545" elif summary["risk_score"] >= 50: risk_level = "HIGH" risk_color = "#fd7e14" elif summary["risk_score"] >= 25: risk_level = "MEDIUM" risk_color = "#ffc107" else: risk_level = "LOW" risk_color = "#0dcaf0" # Build severity breakdown bars total = max(summary["total_findings"], 1) severity_bars = "" for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]: count = summary["severity_counts"].get(sev, 0) pct = (count / total) * 100 color = severity_colors.get(sev, "#6c757d") severity_bars += f"""
{sev}
{count}
""" # Build findings table findings_rows = "" for i, f in enumerate(findings[:200]): # Limit to 200 rows sev = f.get("severity", "MEDIUM").upper() color = severity_colors.get(sev, "#6c757d") title = f.get("title", "Unknown").replace("<", "<").replace(">", ">") desc = f.get("description", "").replace("<", "<").replace(">", ">")[:150] file = f.get("file", "N/A").replace("<", "<").replace(">", ">") line = f.get("line", "N/A") source = f.get("source", f.get("agent", "unknown")) remediation = f.get("remediation", "").replace("<", "<").replace(">", ">")[:200] sources = ", ".join(f.get("sources", [source])) cwe = f.get("cwe", "") findings_rows += f""" {sev} {title} {desc}... {file} {line} {sources} {cwe} {remediation}... """ # Category breakdown category_items = "" for cat, count in sorted(summary["category_counts"].items(), key=lambda x: x[1], reverse=True): category_items += f'
  • {cat}: {count}
  • ' # Most affected files file_items = "" for filepath, count in summary["most_affected_files"][:10]: file_items += f'
  • {filepath}: {count} findings
  • ' html = f""" Shield Agents - Security Report

    🛡 Shield Agents Security Report

    Target: {target_path} | Generated: {summary['generated_at']}

    Risk Score

    {summary['risk_score']}
    {risk_level} RISK
    {summary['total_findings']}
    Findings
    {summary['severity_counts'].get('CRITICAL', 0)}
    Critical
    {summary['severity_counts'].get('HIGH', 0)}
    High
    {len(summary['category_counts'])}
    Categories

    Severity Distribution

    {severity_bars}

    Categories

      {category_items}

    Most Affected Files

      {file_items}

    Findings Detail ({summary['total_findings']} total)

    {findings_rows}
    Severity Title Description File Line Source CWE Remediation
    """ with open(output_path, "w", encoding="utf-8") as f: f.write(html) return output_path