Files
Anthropic-Cybersecurity-Skills/skills/performing-fuzzing-with-aflplusplus/scripts/agent.py
T
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- Fix 25 shell=True subprocess calls with list-based commands
- Fix 49 verify=False in defensive skills (env-var override)
- Add timeout to 231 HTTP/subprocess/socket calls
- Fix 6 SQL injection patterns with whitelist validation
- Replace 8 __import__() with standard imports
- Remove 701 unused imports across 442 files
- Add authorized-testing disclaimers to all offensive skills
- Complete 11 incomplete skill directories
- Expand 10 stub SKILL.md files with full content
- Fix 2 YAML parse errors in frontmatter
- Fix 5 pre-existing syntax errors
- Convert 22 hardcoded paths/ports to environment variables
- Back up 21 redundant skill pairs to .bak
- Fix 2 global declaration errors
- 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE)
- 0 compile errors across all 724 agent.py files
2026-03-19 13:26:49 +01:00

188 lines
7.0 KiB
Python

#!/usr/bin/env python3
# For authorized testing only
"""AFL++ fuzzing campaign management and crash triage agent."""
import json
import argparse
import os
import subprocess
from datetime import datetime
def instrument_target(source_path, output_path, compiler="afl-clang-fast",
sanitizer=None):
"""Compile target with AFL++ instrumentation."""
cmd = [compiler, "-o", output_path, source_path]
if sanitizer == "asan":
cmd.insert(1, "-fsanitize=address")
elif sanitizer == "ubsan":
cmd.insert(1, "-fsanitize=undefined")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
return {
"compiler": compiler,
"source": source_path,
"output": output_path,
"sanitizer": sanitizer,
"success": result.returncode == 0,
"stderr": result.stderr[:500] if result.stderr else "",
}
def minimize_corpus(afl_cmin_path, target_binary, input_dir, output_dir):
"""Minimize seed corpus using afl-cmin."""
cmd = [afl_cmin_path or "afl-cmin", "-i", input_dir, "-o", output_dir,
"--", target_binary]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
before = len(os.listdir(input_dir)) if os.path.isdir(input_dir) else 0
after = len(os.listdir(output_dir)) if os.path.isdir(output_dir) else 0
return {
"before": before,
"after": after,
"reduction_pct": round((1 - after / max(before, 1)) * 100, 1),
"success": result.returncode == 0,
}
def parse_fuzzer_stats(output_dir):
"""Parse afl-fuzz fuzzer_stats file for campaign metrics."""
stats_path = os.path.join(output_dir, "default", "fuzzer_stats")
if not os.path.exists(stats_path):
stats_path = os.path.join(output_dir, "fuzzer_stats")
if not os.path.exists(stats_path):
return {"error": f"fuzzer_stats not found in {output_dir}"}
stats = {}
with open(stats_path, "r") as f:
for line in f:
if ":" in line:
key, val = line.split(":", 1)
stats[key.strip()] = val.strip()
return {
"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)),
"paths_total": int(stats.get("corpus_count", stats.get("paths_total", 0))),
"paths_found": int(stats.get("paths_found", 0)),
"unique_crashes": int(stats.get("saved_crashes", stats.get("unique_crashes", 0))),
"unique_hangs": int(stats.get("saved_hangs", stats.get("unique_hangs", 0))),
"stability": stats.get("stability", ""),
"bitmap_cvg": stats.get("bitmap_cvg", ""),
"command_line": stats.get("command_line", ""),
}
def triage_crashes(output_dir):
"""Enumerate and classify crash files from AFL++ output."""
crash_dirs = [
os.path.join(output_dir, "default", "crashes"),
os.path.join(output_dir, "crashes"),
]
crash_dir = None
for d in crash_dirs:
if os.path.isdir(d):
crash_dir = d
break
if not crash_dir:
return {"crashes": [], "total": 0}
crashes = []
for filename in sorted(os.listdir(crash_dir)):
if filename.startswith("README") or filename == ".state":
continue
filepath = os.path.join(crash_dir, filename)
size = os.path.getsize(filepath)
sig_parts = filename.split(",")
signal = ""
for part in sig_parts:
if part.startswith("sig:"):
signal = part.split(":")[1]
crashes.append({
"filename": filename,
"size_bytes": size,
"signal": signal,
"path": filepath,
})
return {"crashes": crashes, "total": len(crashes)}
def minimize_crash(afl_tmin_path, target_binary, crash_file, output_file):
"""Minimize a crash test case with afl-tmin."""
cmd = [afl_tmin_path or "afl-tmin", "-i", crash_file, "-o", output_file,
"--", target_binary]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
orig_size = os.path.getsize(crash_file) if os.path.exists(crash_file) else 0
min_size = os.path.getsize(output_file) if os.path.exists(output_file) else 0
return {
"original_size": orig_size,
"minimized_size": min_size,
"reduction_pct": round((1 - min_size / max(orig_size, 1)) * 100, 1),
"success": result.returncode == 0,
}
def run_whatsup(output_dir):
"""Run afl-whatsup to get multi-instance campaign summary."""
cmd = ["afl-whatsup", "-s", output_dir]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
return {"output": result.stdout[:2000] if result.stdout else result.stderr[:500]}
def run_audit(args):
"""Execute AFL++ fuzzing campaign audit."""
print(f"\n{'='*60}")
print(f" AFL++ FUZZING CAMPAIGN AUDIT")
print(f" Generated: {datetime.utcnow().isoformat()} UTC")
print(f"{'='*60}\n")
report = {}
if args.output_dir:
stats = parse_fuzzer_stats(args.output_dir)
report["fuzzer_stats"] = stats
print(f"--- FUZZER STATS ---")
print(f" Executions: {stats.get('execs_done', 0):,}")
print(f" Exec/sec: {stats.get('execs_per_sec', 0)}")
print(f" Paths: {stats.get('paths_total', 0)}")
print(f" Crashes: {stats.get('unique_crashes', 0)}")
print(f" Hangs: {stats.get('unique_hangs', 0)}")
print(f" Stability: {stats.get('stability', '')}")
print(f" Coverage: {stats.get('bitmap_cvg', '')}")
crash_data = triage_crashes(args.output_dir)
report["crash_triage"] = crash_data
print(f"\n--- CRASH TRIAGE ({crash_data['total']} crashes) ---")
for c in crash_data["crashes"][:20]:
print(f" {c['filename']} ({c['size_bytes']}B) signal={c['signal']}")
if args.instrument_src and args.instrument_out:
inst = instrument_target(args.instrument_src, args.instrument_out,
sanitizer=args.sanitizer)
report["instrumentation"] = inst
print(f"\n--- INSTRUMENTATION ---")
print(f" {'SUCCESS' if inst['success'] else 'FAILED'}: {inst['source']}")
return report
def main():
parser = argparse.ArgumentParser(description="AFL++ Fuzzing Campaign Agent")
parser.add_argument("--output-dir", help="AFL++ output directory to analyze")
parser.add_argument("--instrument-src", help="Source file to instrument")
parser.add_argument("--instrument-out", help="Output path for instrumented binary")
parser.add_argument("--sanitizer", choices=["asan", "ubsan"],
help="Address or undefined behavior sanitizer")
parser.add_argument("--output", help="Save report to JSON file")
args = parser.parse_args()
report = run_audit(args)
if args.output:
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
print(f"\n[+] Report saved to {args.output}")
if __name__ == "__main__":
main()