annator-command-center / tests /fuzzing /scripts /aggregate_crash_reports.py
techprotrade's picture
Deploy ATOM FastAPI command center runtime (part 8)
aef804e verified
Raw
History Blame Contribute Delete
9.41 kB
#!/usr/bin/env python3
"""
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
# Add backend to path
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}")
# Deduplicate crashes in this directory
dir_crashes = deduplicator.deduplicate_crashes(crash_dir)
# Merge into overall crashes_by_signature
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)
"""
# Calculate statistics
total_crashes = sum(len(files) for files in crashes_by_signature.values())
unique_crashes = len(crashes_by_signature)
# Group crashes by endpoint (extract from directory name)
crashes_by_endpoint = {}
for signature_hash, crash_files in crashes_by_signature.items():
for crash_file in crash_files:
# Extract endpoint from path: .../crashes/POST_api_auth_login_2026-03-24/*.input
dir_name = crash_file.parent.name
# Parse endpoint from directory name (remove timestamp)
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)
# Get top crash signatures by frequency
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)
# Generate markdown report
markdown_lines = []
markdown_lines.append("# Fuzzing Crash Report")
markdown_lines.append("")
markdown_lines.append(f"**Generated:** {datetime.utcnow().isoformat()}Z")
markdown_lines.append("")
# Summary section
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("")
# Crashes by endpoint
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("")
# Top crash signatures
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("")
# Bug filing status
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("")
# Trend analysis (if baseline exists)
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("")
# Write markdown report
with open(output_path, "w") as f:
f.write("\n".join(markdown_lines))
print(f"Markdown report generated: {output_path}")
# Generate JSON summary
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 summary to stdout
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()
# Expand glob patterns
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)")
# Aggregate crashes
crashes_by_signature = aggregate_crashes(crash_dirs)
if not crashes_by_signature:
print("No crashes found in any directory")
# Generate empty report
generate_report({}, args.output, crash_dirs)
sys.exit(0)
# Generate report
generate_report(crashes_by_signature, args.output, crash_dirs)
# Exit code: 0 if success, 1 if no crashes
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()