| |
| """ |
| Crash Report Aggregation Script |
| |
| This script aggregates crash reports from multiple fuzzing campaigns, |
| generates markdown and JSON reports with crash statistics and trends. |
| |
| Usage: |
| python aggregate_crash_reports.py --crash-dirs "backend/tests/fuzzing/campaigns/crashes/*" |
| python aggregate_crash_reports.py --crash-dirs "crashes/*" --output report.md |
| |
| Environment Variables: |
| None |
| """ |
|
|
| import argparse |
| import glob |
| import json |
| import os |
| import sys |
| from datetime import datetime |
| from pathlib import Path |
| from typing import Dict, List, Any |
|
|
| |
| sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) |
|
|
| from tests.fuzzing.campaigns.crash_deduplicator import CrashDeduplicator |
|
|
|
|
| def aggregate_crashes(crash_dirs: List[Path]) -> Dict[str, List[Path]]: |
| """ |
| Aggregate crashes from multiple campaign directories. |
| |
| Args: |
| crash_dirs: List of crash directory paths |
| |
| Returns: |
| Dict mapping signature_hash to list of crash files |
| """ |
| deduplicator = CrashDeduplicator() |
| crashes_by_signature = {} |
|
|
| for crash_dir in crash_dirs: |
| if not crash_dir.exists(): |
| print(f"Warning: Crash directory not found: {crash_dir}") |
| continue |
|
|
| print(f"Aggregating crashes from: {crash_dir}") |
|
|
| |
| dir_crashes = deduplicator.deduplicate_crashes(crash_dir) |
|
|
| |
| for signature_hash, crash_files in dir_crashes.items(): |
| if signature_hash not in crashes_by_signature: |
| crashes_by_signature[signature_hash] = [] |
|
|
| crashes_by_signature[signature_hash].extend(crash_files) |
|
|
| return crashes_by_signature |
|
|
|
|
| def generate_report(crashes_by_signature: Dict[str, List[Path]], output_path: str, crash_dirs: List[Path] = None): |
| """ |
| Generate markdown and JSON crash reports. |
| |
| Args: |
| crashes_by_signature: Dict mapping signature_hash to list of crash files |
| output_path: Path to output markdown report |
| crash_dirs: List of crash directories (for context) |
| """ |
| |
| total_crashes = sum(len(files) for files in crashes_by_signature.values()) |
| unique_crashes = len(crashes_by_signature) |
|
|
| |
| crashes_by_endpoint = {} |
| for signature_hash, crash_files in crashes_by_signature.items(): |
| for crash_file in crash_files: |
| |
| dir_name = crash_file.parent.name |
| |
| parts = dir_name.split("_") |
| endpoint_parts = [] |
| for part in parts: |
| if part.isdigit() or part.startswith("2026-"): |
| break |
| endpoint_parts.append(part) |
|
|
| endpoint = "_".join(endpoint_parts).replace("_", " ") |
|
|
| if endpoint not in crashes_by_endpoint: |
| crashes_by_endpoint[endpoint] = {"files": [], "unique_signatures": set()} |
|
|
| crashes_by_endpoint[endpoint]["files"].extend(crash_files) |
| crashes_by_endpoint[endpoint]["unique_signatures"].add(signature_hash) |
|
|
| |
| signature_summary = [] |
| for signature_hash, crash_files in crashes_by_signature.items(): |
| signature_summary.append({ |
| "signature_hash": signature_hash, |
| "count": len(crash_files), |
| "example_crash": str(crash_files[0]) |
| }) |
|
|
| signature_summary.sort(key=lambda x: x["count"], reverse=True) |
|
|
| |
| markdown_lines = [] |
| markdown_lines.append("# Fuzzing Crash Report") |
| markdown_lines.append("") |
| markdown_lines.append(f"**Generated:** {datetime.utcnow().isoformat()}Z") |
| markdown_lines.append("") |
|
|
| |
| markdown_lines.append("## Summary") |
| markdown_lines.append("") |
| markdown_lines.append(f"- **Total crashes:** {total_crashes}") |
| markdown_lines.append(f"- **Unique crashes:** {unique_crashes}") |
| markdown_lines.append(f"- **Affected endpoints:** {len(crashes_by_endpoint)}") |
| markdown_lines.append("") |
|
|
| |
| markdown_lines.append("## Crashes by Endpoint") |
| markdown_lines.append("") |
| for endpoint, data in sorted(crashes_by_endpoint.items()): |
| total = len(data["files"]) |
| unique = len(data["unique_signatures"]) |
| markdown_lines.append(f"### {endpoint}") |
| markdown_lines.append("") |
| markdown_lines.append(f"- Total crashes: {total}") |
| markdown_lines.append(f"- Unique crashes: {unique}") |
| markdown_lines.append("") |
|
|
| |
| markdown_lines.append("## Top Crash Signatures") |
| markdown_lines.append("") |
| for i, sig in enumerate(signature_summary[:20], 1): |
| markdown_lines.append(f"### {i}. Signature: `{sig['signature_hash'][:16]}...`") |
| markdown_lines.append("") |
| markdown_lines.append(f"- **Frequency:** {sig['count']} crashes") |
| markdown_lines.append(f"- **Example:** `{sig['example_crash']}`") |
| markdown_lines.append("") |
|
|
| |
| markdown_lines.append("## Bug Filing Status") |
| markdown_lines.append("") |
| markdown_lines.append("Bug filing status is tracked by GitHub issues created via BugFilingService.") |
| markdown_lines.append("Check GitHub repository for filed bugs with label `bug-discovery`.") |
| markdown_lines.append("") |
|
|
| |
| if crash_dirs: |
| markdown_lines.append("## Trend Analysis") |
| markdown_lines.append("") |
| markdown_lines.append(f"Crash directories analyzed: {len(crash_dirs)}") |
| markdown_lines.append("") |
| for crash_dir in crash_dirs: |
| markdown_lines.append(f"- `{crash_dir}`") |
| markdown_lines.append("") |
| markdown_lines.append("*Trend comparison with previous week requires baseline data.*") |
| markdown_lines.append("") |
|
|
| |
| with open(output_path, "w") as f: |
| f.write("\n".join(markdown_lines)) |
|
|
| print(f"Markdown report generated: {output_path}") |
|
|
| |
| json_output_path = output_path.replace(".md", ".json") |
| json_data = { |
| "generated_at": datetime.utcnow().isoformat() + "Z", |
| "summary": { |
| "total_crashes": total_crashes, |
| "unique_crashes": unique_crashes, |
| "affected_endpoints": len(crashes_by_endpoint) |
| }, |
| "crashes_by_endpoint": { |
| endpoint: { |
| "total_crashes": len(data["files"]), |
| "unique_crashes": len(data["unique_signatures"]) |
| } |
| for endpoint, data in crashes_by_endpoint.items() |
| }, |
| "top_signatures": signature_summary[:20] |
| } |
|
|
| with open(json_output_path, "w") as f: |
| json.dump(json_data, f, indent=2) |
|
|
| print(f"JSON summary generated: {json_output_path}") |
|
|
| |
| print("\n" + "="*60) |
| print("Crash Aggregation Summary") |
| print("="*60) |
| print(f"Total crashes: {total_crashes}") |
| print(f"Unique crashes: {unique_crashes}") |
| print(f"Affected endpoints: {len(crashes_by_endpoint)}") |
| print("="*60) |
|
|
|
|
| def main(): |
| """Main entry point.""" |
| parser = argparse.ArgumentParser( |
| description="Aggregate crash reports from fuzzing campaigns", |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| epilog=""" |
| Examples: |
| # Aggregate crashes from all campaign directories |
| python aggregate_crash_reports.py --crash-dirs "backend/tests/fuzzing/campaigns/crashes/*" |
| |
| # Specify output file |
| python aggregate_crash_reports.py --crash-dirs "crashes/*" --output /tmp/report.md |
| |
| # Multiple directories |
| python aggregate_crash_reports.py --crash-dirs "crashes/*" --crash-dirs "old/crashes/*" |
| """ |
| ) |
|
|
| parser.add_argument( |
| "--crash-dirs", |
| action="append", |
| required=True, |
| help="Glob pattern for crash directories (can be specified multiple times)" |
| ) |
|
|
| parser.add_argument( |
| "--output", |
| type=str, |
| default="fuzzing-report.md", |
| help="Output report path (default: fuzzing-report.md)" |
| ) |
|
|
| args = parser.parse_args() |
|
|
| |
| crash_dirs = [] |
| for pattern in args.crash_dirs: |
| expanded = glob.glob(pattern) |
| if not expanded: |
| print(f"Warning: No directories found for pattern: {pattern}") |
| crash_dirs.extend([Path(d) for d in expanded]) |
|
|
| if not crash_dirs: |
| print("Error: No crash directories found") |
| sys.exit(1) |
|
|
| print(f"Found {len(crash_dirs)} crash directory(s)") |
|
|
| |
| crashes_by_signature = aggregate_crashes(crash_dirs) |
|
|
| if not crashes_by_signature: |
| print("No crashes found in any directory") |
| |
| generate_report({}, args.output, crash_dirs) |
| sys.exit(0) |
|
|
| |
| generate_report(crashes_by_signature, args.output, crash_dirs) |
|
|
| |
| total_crashes = sum(len(files) for files in crashes_by_signature.values()) |
| if total_crashes == 0: |
| sys.exit(1) |
| else: |
| sys.exit(0) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|