From 5ae1a2b7c4ee6272018767dde39b6a30ef39966c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 00:01:41 +0000 Subject: [PATCH] chore: remove generate_report.py, which aggregates artifacts of CI jobs that do not exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The script's only function is to merge JSON artifacts (bandit/semgrep/pip-audit/safety/gitleaks results) produced by a security-audit workflow that was never committed — .github/workflows/ has no such jobs, so there is nothing for it to aggregate and no way to run it usefully. Its siblings stay: prove_security.py and audit_plugins.py both run standalone (verified), and .codacy.yml stays because the Codacy service (README badge) reads it server-side without a workflow file. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01SXb4mKcAkVaxkeTb3YnAdr --- scripts/generate_report.py | 356 ------------------------------------- 1 file changed, 356 deletions(-) delete mode 100644 scripts/generate_report.py diff --git a/scripts/generate_report.py b/scripts/generate_report.py deleted file mode 100644 index 753ea35b..00000000 --- a/scripts/generate_report.py +++ /dev/null @@ -1,356 +0,0 @@ -#!/usr/bin/env python3 -""" -Security Report Generator - -Aggregates JSON output from all CI security audit jobs into a single -Markdown report suitable for PR comments and artifact storage. - -Expected artifact layout (from actions/download-artifact@v4): - / - sast-results/ - bandit-results.json - semgrep-results.json - dependency-audit-results/ - pip-audit-results.json - safety-results.json - secrets-scan-results/ - gitleaks-results.json - security-proofs-results/ - security-proofs-results.json - plugin-audit-results/ - plugin-audit-results.json - -Usage: - python scripts/generate_report.py --artifact-dir audit-artifacts/ --output report.md - python scripts/generate_report.py --artifact-dir audit-artifacts/ --output report.md --verbose -""" - -import argparse -import json -import sys -from pathlib import Path -from datetime import datetime, timezone - -PROJECT_ROOT = Path(__file__).resolve().parent.parent - -# Gitleaks matches exactly equal to one of these (not a substring match -- a -# real secret that merely contains one of these words as part of its actual -# value must still be reported) are known template placeholders. -_GITLEAKS_SUPPRESS_EXACT_VALUES = { - "YOUR_YOUTUBE_API_KEY", - "YOUR_YOUTUBE_CHANNEL_ID", - "YOUR_GITHUB_PERSONAL_ACCESS_TOKEN", -} - -# Findings in these files are suppressed regardless of value -- they are -# template/example files that are expected to only ever contain placeholders. -_GITLEAKS_SUPPRESS_PATHS = [ - "config_secrets.template.json", - "config.template.json", -] - - -# ───────────────────────────────────────────────────────────────────────────── -# Helpers -# ───────────────────────────────────────────────────────────────────────────── - -def _load(path: Path) -> tuple[dict | list | None, str | None]: - """Load a JSON artifact file. - - Returns (data, error): error is None on success (data is whatever was - parsed, which may legitimately be an empty list/dict for a clean scan); - otherwise error is a human-readable reason the artifact is unavailable, - distinguishing "missing/malformed artifact" from "valid empty result" so - callers don't silently treat a broken CI job as a clean pass. - """ - if not path.exists(): - return None, f"artifact not found: {path}" - try: - return json.loads(path.read_text(encoding="utf-8")), None - except (json.JSONDecodeError, OSError) as exc: - return None, f"could not read/parse {path}: {exc}" - - -def _md_sanitize_cell(value: object) -> str: - """Escape/normalize a value so scanner-controlled content (a matched - secret, a bandit issue_text, a file path) can't alter the Markdown - table's structure: pipes would add bogus columns, newlines would break - out of the row (or forge a fake header/separator line).""" - text = str(value) - text = text.replace("\\", "\\\\").replace("|", "\\|") - text = text.replace("\r\n", " ").replace("\n", " ").replace("\r", " ") - return text - - -def _md_table_row(*cells: str) -> str: - return "| " + " | ".join(_md_sanitize_cell(c) for c in cells) + " |" - - -# ───────────────────────────────────────────────────────────────────────────── -# Per-tool summarizers -# Returns: (markdown_lines: list[str], critical_count: int, available: bool) -# `available=False` means the artifact was missing or malformed -- distinct -# from a valid scan that simply found nothing -- so the caller can report -# INCOMPLETE instead of silently counting it as a clean pass. -# ───────────────────────────────────────────────────────────────────────────── - -def _summarize_bandit(artifact_dir: Path) -> tuple[list[str], int, bool]: - data, error = _load(artifact_dir / "sast-results" / "bandit-results.json") - if error: - return [f"_bandit results unavailable: {error}_"], 0, False - - results = data.get("results", []) - high = [r for r in results if r.get("issue_severity") == "HIGH"] - medium = [r for r in results if r.get("issue_severity") == "MEDIUM"] - low = [r for r in results if r.get("issue_severity") == "LOW"] - - lines = [ - f"**Bandit**: {len(high)} HIGH · {len(medium)} MEDIUM · {len(low)} LOW" - ] - - if high: - lines += [ - "", - "| Severity | File | Line | Issue |", - "| --- | --- | --- | --- |", - ] - for r in high[:10]: - fname = Path(r.get("filename", "")).name - lines.append(_md_table_row( - "HIGH", f"`{fname}`", - str(r.get("line_number", "?")), - r.get("issue_text", "") - )) - if len(high) > 10: - lines.append(f"_… and {len(high) - 10} more HIGH findings_") - - return lines, len(high), True - - -def _summarize_pip_audit(artifact_dir: Path) -> tuple[list[str], int, bool]: - data, error = _load(artifact_dir / "dependency-audit-results" / "pip-audit-results.json") - if error: - return [f"_pip-audit results unavailable: {error}_"], 0, False - - # pip-audit JSON format: {"dependencies": [{"name": ..., "vulns": [...]}]} - vulns: list[dict] = [] - for dep in data.get("dependencies", []): - for v in dep.get("vulns", []): - vulns.append({"package": dep.get("name", "?"), **v}) - - lines = [f"**pip-audit**: {len(vulns)} vulnerabilities found"] - - if vulns: - lines += ["", "| Package | ID | Fix |", "| --- | --- | --- |"] - for v in vulns[:10]: - fix = v.get("fix_versions", ["none"]) - fix_str = ", ".join(fix) if fix else "none" - lines.append(_md_table_row( - v.get("package", "?"), - v.get("id", "?"), - fix_str, - )) - - # Treat known vulnerabilities as warnings, not critical (they may be unavoidable) - return lines, 0, True - - -def _summarize_gitleaks(artifact_dir: Path) -> tuple[list[str], int, bool]: - data, error = _load(artifact_dir / "secrets-scan-results" / "gitleaks-results.json") - if error: - return [f"_gitleaks results unavailable: {error}_"], 0, False - - if not isinstance(data, list): - data = [] - - real_findings = [] - suppressed = 0 - for finding in data: - secret_val = str(finding.get("Secret", "") or finding.get("Match", "")) - file_name = Path(finding.get("File", "")).name - if (secret_val in _GITLEAKS_SUPPRESS_EXACT_VALUES - or file_name in _GITLEAKS_SUPPRESS_PATHS): - suppressed += 1 - else: - real_findings.append(finding) - - lines = [ - f"**Gitleaks**: {len(real_findings)} finding(s) " - f"({suppressed} suppressed as template placeholders)" - ] - - if real_findings: - lines += ["", "| Rule | File | Line | Description |", "| --- | --- | --- | --- |"] - for f in real_findings[:10]: - fname = Path(f.get("File", "")).name - lines.append(_md_table_row( - f.get("RuleID", "?"), - f"`{fname}`", - str(f.get("StartLine", "?")), - f.get("Description", ""), - )) - - critical = len(real_findings) # any real secret is critical - return lines, critical, True - - -def _summarize_security_proofs(artifact_dir: Path) -> tuple[list[str], int, bool]: - data, error = _load(artifact_dir / "security-proofs-results" / "security-proofs-results.json") - if error: - return [f"_security proofs results unavailable: {error}_"], 0, False - - if not isinstance(data, list): - data = [] - - critical = [r for r in data if r.get("severity") == "CRITICAL"] - warnings = [r for r in data if r.get("severity") == "WARNING"] - passed = [r for r in data if r.get("severity") == "PASS"] - skipped = [r for r in data if r.get("severity") == "SKIP"] - - lines = [ - f"**Security Proofs**: " - f"{len(passed)} PASS · {len(warnings)} WARN · " - f"{len(critical)} CRITICAL · {len(skipped)} SKIP", - "", - ] - - _icon = {"PASS": "✅", "INFO": "ℹ️", "WARNING": "⚠️", # nosec B105 - severity labels, not credentials - "CRITICAL": "🚨", "SKIP": "⏭️"} - for r in data: - icon = _icon.get(r.get("severity", ""), "❓") - lines.append( - f"- {icon} **{r.get('test_id', '?')}**: {r.get('message', '')}" - ) - if r.get("details") and r.get("severity") in ("CRITICAL", "WARNING"): - lines.append(f" - _{r['details']}_") - - return lines, len(critical), True - - -def _summarize_plugin_audit(artifact_dir: Path) -> tuple[list[str], int, bool]: - data, error = _load(artifact_dir / "plugin-audit-results" / "plugin-audit-results.json") - if error: - return [f"_plugin audit results unavailable: {error}_"], 0, False - - summary = data.get("summary", {}) - findings = data.get("findings", []) - critical_findings = [f for f in findings if f.get("severity") == "CRITICAL"] - warning_findings = [f for f in findings if f.get("severity") == "WARNING"] - - lines = [ - f"**Plugin Audit**: {data.get('plugins_scanned', '?')} plugins scanned — " - f"{summary.get('critical', 0)} CRITICAL · {summary.get('warnings', 0)} WARNINGS" - ] - - if critical_findings: - lines += ["", "| Plugin | File | Line | Rule | Message |", - "| --- | --- | --- | --- | --- |"] - for f in critical_findings[:10]: - fname = Path(f.get("file", "")).name - lines.append(_md_table_row( - f.get("plugin_id", "?"), - f"`{fname}`", - str(f.get("line", "?")), - f.get("rule", "?"), - f.get("message", ""), - )) - - if warning_findings and not critical_findings: - lines.append(f"\n_{len(warning_findings)} warning(s) found — see artifact for details_") - - return lines, summary.get("critical", 0), True - - -# ───────────────────────────────────────────────────────────────────────────── -# Main -# ───────────────────────────────────────────────────────────────────────────── - -def main() -> int: - parser = argparse.ArgumentParser( - description="Generate consolidated security audit report", - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - parser.add_argument("--artifact-dir", required=True, - help="Directory containing downloaded CI artifacts") - parser.add_argument("--output", "-o", required=True, - help="Output Markdown file path") - parser.add_argument("--verbose", "-v", action="store_true") - args = parser.parse_args() - - artifact_dir = Path(args.artifact_dir) - timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") - - bandit_lines, bandit_crit, bandit_ok = _summarize_bandit(artifact_dir) - pip_audit_lines, pip_audit_crit, pip_audit_ok = _summarize_pip_audit(artifact_dir) - gitleaks_lines, gitleaks_crit, gitleaks_ok = _summarize_gitleaks(artifact_dir) - proofs_lines, proofs_crit, proofs_ok = _summarize_security_proofs(artifact_dir) - plugins_lines, plugins_crit, plugins_ok = _summarize_plugin_audit(artifact_dir) - - unavailable_tools = [ - name for name, ok in [ - ("bandit", bandit_ok), ("pip-audit", pip_audit_ok), - ("gitleaks", gitleaks_ok), ("security-proofs", proofs_ok), - ("plugin-audit", plugins_ok), - ] if not ok - ] - - total_critical = bandit_crit + pip_audit_crit + gitleaks_crit + proofs_crit + plugins_crit - if unavailable_tools: - # A missing/malformed artifact means that tool's checks never - # actually ran -- this must not be reported as a clean PASS just - # because the *artifacts that did load* found nothing. - overall = "INCOMPLETE ⚠️" - elif total_critical > 0: - overall = "ACTION REQUIRED 🚨" - else: - overall = "PASSED ✅" - - def section(title: str, lines: list[str]) -> str: - return f"### {title}\n\n" + "\n".join(lines) + "\n" - - incomplete_note = ( - f"\n_⚠️ Incomplete: results unavailable for {', '.join(unavailable_tools)} " - f"— see the corresponding section(s) below for details_\n" - if unavailable_tools else "" - ) - - report = f"""## 🔒 Security Audit — {overall} - -_Generated: {timestamp}_ -{incomplete_note} -| Critical | High/Warn | Overall | -| :---: | :---: | :---: | -| {'🚨 ' + str(total_critical) if total_critical else '✅ 0'} | ⚠️ see below | {overall} | - ---- - -{section('SAST — Bandit', bandit_lines)} -{section('Dependencies — pip-audit', pip_audit_lines)} -{section('Secrets — Gitleaks', gitleaks_lines)} -{section('LEDMatrix Security Proofs', proofs_lines)} -{section('Plugin Security Audit', plugins_lines)} ---- - -_Total critical findings: **{total_critical}**_ -""" - - output_path = Path(args.output) - output_path.write_text(report, encoding="utf-8") - - if args.verbose: - print(f" Report written to: {output_path}") - print(f" Status: {overall}") - print(f" Critical findings: {total_critical}") - print(f" bandit={bandit_crit} pip-audit={pip_audit_crit} " - f"gitleaks={gitleaks_crit} proofs={proofs_crit} plugins={plugins_crit}") - if unavailable_tools: - print(f" Unavailable: {', '.join(unavailable_tools)}") - - if unavailable_tools: - return 1 - - return 0 - - -if __name__ == "__main__": - sys.exit(main())