mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 06:04:56 +03:00
176 lines
5.9 KiB
Python
176 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
AFL++ Fuzzing Results Analyzer
|
|
|
|
Parses AFL++ output directories and generates reports on
|
|
crash findings, corpus growth, and coverage statistics.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from collections import defaultdict
|
|
|
|
|
|
def parse_fuzzer_stats(stats_file: str) -> dict:
|
|
stats = {}
|
|
if not os.path.exists(stats_file):
|
|
return stats
|
|
with open(stats_file) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if ":" in line:
|
|
key, value = line.split(":", 1)
|
|
stats[key.strip()] = value.strip()
|
|
return stats
|
|
|
|
|
|
def count_files_in_dir(directory: str) -> int:
|
|
if not os.path.isdir(directory):
|
|
return 0
|
|
return len([f for f in os.listdir(directory) if f != "README.txt" and os.path.isfile(os.path.join(directory, f))])
|
|
|
|
|
|
def analyze_fuzzer_instance(instance_dir: str) -> dict:
|
|
name = os.path.basename(instance_dir)
|
|
stats = parse_fuzzer_stats(os.path.join(instance_dir, "fuzzer_stats"))
|
|
|
|
return {
|
|
"name": name,
|
|
"start_time": stats.get("start_time", ""),
|
|
"last_update": stats.get("last_update", ""),
|
|
"execs_done": int(stats.get("execs_done", 0)),
|
|
"execs_per_sec": float(stats.get("execs_per_sec", 0)),
|
|
"corpus_count": count_files_in_dir(os.path.join(instance_dir, "queue")),
|
|
"crashes_total": count_files_in_dir(os.path.join(instance_dir, "crashes")),
|
|
"hangs_total": count_files_in_dir(os.path.join(instance_dir, "hangs")),
|
|
"paths_total": int(stats.get("paths_total", 0)),
|
|
"paths_found": int(stats.get("paths_found", 0)),
|
|
"stability": stats.get("stability", ""),
|
|
"cycles_done": int(stats.get("cycles_done", 0)),
|
|
"bitmap_cvg": stats.get("bitmap_cvg", ""),
|
|
"command_line": stats.get("command_line", ""),
|
|
}
|
|
|
|
|
|
def collect_crash_info(instance_dir: str) -> list:
|
|
crashes_dir = os.path.join(instance_dir, "crashes")
|
|
crashes = []
|
|
if not os.path.isdir(crashes_dir):
|
|
return crashes
|
|
for fname in sorted(os.listdir(crashes_dir)):
|
|
if fname == "README.txt":
|
|
continue
|
|
fpath = os.path.join(crashes_dir, fname)
|
|
if os.path.isfile(fpath):
|
|
crashes.append({
|
|
"file": fname,
|
|
"path": fpath,
|
|
"size": os.path.getsize(fpath),
|
|
"instance": os.path.basename(instance_dir),
|
|
})
|
|
return crashes
|
|
|
|
|
|
def analyze_campaign(findings_dir: str) -> dict:
|
|
report = {
|
|
"findings_dir": findings_dir,
|
|
"analyzed_at": datetime.utcnow().isoformat() + "Z",
|
|
"instances": [],
|
|
"total_execs": 0,
|
|
"total_crashes": 0,
|
|
"total_hangs": 0,
|
|
"total_corpus": 0,
|
|
"all_crashes": [],
|
|
"avg_execs_per_sec": 0,
|
|
}
|
|
|
|
instance_dirs = []
|
|
for entry in sorted(os.listdir(findings_dir)):
|
|
full_path = os.path.join(findings_dir, entry)
|
|
if os.path.isdir(full_path) and os.path.exists(os.path.join(full_path, "fuzzer_stats")):
|
|
instance_dirs.append(full_path)
|
|
|
|
if not instance_dirs:
|
|
print(f"No fuzzer instances found in {findings_dir}")
|
|
return report
|
|
|
|
exec_speeds = []
|
|
for inst_dir in instance_dirs:
|
|
inst = analyze_fuzzer_instance(inst_dir)
|
|
report["instances"].append(inst)
|
|
report["total_execs"] += inst["execs_done"]
|
|
report["total_crashes"] += inst["crashes_total"]
|
|
report["total_hangs"] += inst["hangs_total"]
|
|
report["total_corpus"] += inst["corpus_count"]
|
|
if inst["execs_per_sec"] > 0:
|
|
exec_speeds.append(inst["execs_per_sec"])
|
|
|
|
crashes = collect_crash_info(inst_dir)
|
|
report["all_crashes"].extend(crashes)
|
|
|
|
if exec_speeds:
|
|
report["avg_execs_per_sec"] = round(sum(exec_speeds) / len(exec_speeds), 1)
|
|
|
|
return report
|
|
|
|
|
|
def print_report(report: dict) -> None:
|
|
print(f"\n{'='*60}")
|
|
print(f"AFL++ Fuzzing Campaign Report")
|
|
print(f"{'='*60}")
|
|
print(f"Findings directory: {report['findings_dir']}")
|
|
print(f"Analyzed at: {report['analyzed_at']}")
|
|
print(f"Fuzzer instances: {len(report['instances'])}")
|
|
print(f"\nAggregate Statistics:")
|
|
print(f" Total executions: {report['total_execs']:,}")
|
|
print(f" Avg exec/sec: {report['avg_execs_per_sec']:,.1f}")
|
|
print(f" Total corpus entries: {report['total_corpus']}")
|
|
print(f" Total unique crashes: {report['total_crashes']}")
|
|
print(f" Total hangs: {report['total_hangs']}")
|
|
|
|
print(f"\nInstance Details:")
|
|
for inst in report["instances"]:
|
|
print(f" {inst['name']:20s} | Execs: {inst['execs_done']:>12,} | "
|
|
f"Speed: {inst['execs_per_sec']:>8.1f}/s | "
|
|
f"Crashes: {inst['crashes_total']:3d} | "
|
|
f"Corpus: {inst['corpus_count']:5d} | "
|
|
f"Cycles: {inst['cycles_done']}")
|
|
|
|
if report["all_crashes"]:
|
|
print(f"\nCrash Files ({len(report['all_crashes'])} total):")
|
|
for crash in report["all_crashes"][:20]:
|
|
print(f" [{crash['instance']}] {crash['file']} ({crash['size']} bytes)")
|
|
if len(report["all_crashes"]) > 20:
|
|
print(f" ... and {len(report['all_crashes']) - 20} more")
|
|
|
|
verdict = "PASS" if report["total_crashes"] == 0 else "FAIL"
|
|
print(f"\nCI Verdict: {verdict}")
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python process.py <findings_directory>")
|
|
sys.exit(1)
|
|
|
|
findings_dir = sys.argv[1]
|
|
if not os.path.isdir(findings_dir):
|
|
print(f"Directory not found: {findings_dir}")
|
|
sys.exit(1)
|
|
|
|
report = analyze_campaign(findings_dir)
|
|
print_report(report)
|
|
|
|
output = os.path.join(findings_dir, "campaign_report.json")
|
|
with open(output, "w") as f:
|
|
json.dump(report, f, indent=2, default=str)
|
|
print(f"\nReport saved to: {output}")
|
|
|
|
sys.exit(1 if report["total_crashes"] > 0 else 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|