mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 22:24:56 +03:00
c47eed6a64
- 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
186 lines
6.8 KiB
Python
186 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Agent for implementing AFL++ fuzz testing in CI/CD pipelines."""
|
|
|
|
import json
|
|
import argparse
|
|
import subprocess
|
|
import os
|
|
from pathlib import Path
|
|
|
|
|
|
def compile_target(source_file, output_binary, compiler="afl-clang-fast"):
|
|
"""Compile target binary with AFL++ instrumentation."""
|
|
cmd = [compiler, "-g", "-O1", "-fno-omit-frame-pointer", "-o", output_binary, source_file]
|
|
env = os.environ.copy()
|
|
env["AFL_HARDEN"] = "1"
|
|
result = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=120)
|
|
return {
|
|
"source": source_file,
|
|
"binary": output_binary,
|
|
"compiler": compiler,
|
|
"returncode": result.returncode,
|
|
"stdout": result.stdout[:500],
|
|
"stderr": result.stderr[:500],
|
|
"instrumented": result.returncode == 0,
|
|
}
|
|
|
|
|
|
def prepare_corpus(seed_dir, corpus_dir):
|
|
"""Prepare and minimize seed corpus using afl-cmin."""
|
|
Path(corpus_dir).mkdir(parents=True, exist_ok=True)
|
|
seeds = list(Path(seed_dir).glob("*"))
|
|
if not seeds:
|
|
# Create a minimal seed if none provided
|
|
minimal = Path(seed_dir) / "seed_minimal"
|
|
minimal.write_bytes(b"AAAA")
|
|
seeds = [minimal]
|
|
return {
|
|
"seed_dir": str(seed_dir),
|
|
"corpus_dir": str(corpus_dir),
|
|
"seed_count": len(seeds),
|
|
"seeds": [str(s) for s in seeds[:50]],
|
|
}
|
|
|
|
|
|
def minimize_corpus(binary, input_dir, output_dir, timeout=60):
|
|
"""Minimize seed corpus using afl-cmin."""
|
|
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
cmd = ["afl-cmin", "-i", input_dir, "-o", output_dir, "-t", str(timeout * 1000), "--", binary]
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
minimized = list(Path(output_dir).glob("*"))
|
|
return {
|
|
"input_count": len(list(Path(input_dir).glob("*"))),
|
|
"output_count": len(minimized),
|
|
"returncode": result.returncode,
|
|
}
|
|
|
|
|
|
def run_fuzzer(binary, input_dir, output_dir, duration_seconds=300, memory_limit="512"):
|
|
"""Run AFL++ fuzzer for a specified duration."""
|
|
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
env = os.environ.copy()
|
|
env["AFL_SKIP_CPUFREQ"] = "1"
|
|
env["AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES"] = "1"
|
|
env["AFL_NO_UI"] = "1"
|
|
cmd = [
|
|
"afl-fuzz",
|
|
"-i", input_dir,
|
|
"-o", output_dir,
|
|
"-m", memory_limit,
|
|
"-V", str(duration_seconds),
|
|
"--", binary,
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=duration_seconds + 60)
|
|
stats = parse_fuzzer_stats(os.path.join(output_dir, "default", "fuzzer_stats"))
|
|
crashes_dir = os.path.join(output_dir, "default", "crashes")
|
|
crash_files = list(Path(crashes_dir).glob("id:*")) if os.path.isdir(crashes_dir) else []
|
|
return {
|
|
"binary": binary,
|
|
"duration_seconds": duration_seconds,
|
|
"returncode": result.returncode,
|
|
"stats": stats,
|
|
"crashes_found": len(crash_files),
|
|
"crash_files": [str(f) for f in crash_files[:50]],
|
|
}
|
|
|
|
|
|
def parse_fuzzer_stats(stats_file):
|
|
"""Parse AFL++ fuzzer_stats file into a dict."""
|
|
stats = {}
|
|
try:
|
|
with open(stats_file, "r") as f:
|
|
for line in f:
|
|
if ":" in line:
|
|
key, _, value = line.partition(":")
|
|
stats[key.strip()] = value.strip()
|
|
except FileNotFoundError:
|
|
return {"error": "fuzzer_stats not found"}
|
|
return {
|
|
"execs_done": stats.get("execs_done", "0"),
|
|
"execs_per_sec": stats.get("execs_per_sec", "0"),
|
|
"paths_total": stats.get("paths_total", "0"),
|
|
"paths_found": stats.get("paths_found", "0"),
|
|
"unique_crashes": stats.get("saved_crashes", "0"),
|
|
"unique_hangs": stats.get("saved_hangs", "0"),
|
|
"stability": stats.get("stability", "unknown"),
|
|
"bitmap_cvg": stats.get("bitmap_cvg", "unknown"),
|
|
}
|
|
|
|
|
|
def triage_crashes(binary, crashes_dir):
|
|
"""Triage crash inputs to deduplicate and classify."""
|
|
crash_files = sorted(Path(crashes_dir).glob("id:*"))
|
|
results = []
|
|
for crash_file in crash_files[:100]:
|
|
cmd = [binary]
|
|
try:
|
|
proc = subprocess.run(
|
|
cmd, input=crash_file.read_bytes(),
|
|
capture_output=True, timeout=5
|
|
)
|
|
results.append({
|
|
"file": str(crash_file),
|
|
"returncode": proc.returncode,
|
|
"signal": -proc.returncode if proc.returncode < 0 else None,
|
|
"stderr_snippet": proc.stderr[:200].decode("utf-8", errors="replace"),
|
|
"crash_type": _classify_signal(proc.returncode),
|
|
})
|
|
except subprocess.TimeoutExpired:
|
|
results.append({"file": str(crash_file), "crash_type": "hang/timeout"})
|
|
return {
|
|
"total_crashes": len(crash_files),
|
|
"triaged": len(results),
|
|
"by_type": _count_by(results, "crash_type"),
|
|
"results": results,
|
|
}
|
|
|
|
|
|
def _classify_signal(returncode):
|
|
signal_map = {-6: "SIGABRT", -11: "SIGSEGV", -8: "SIGFPE", -4: "SIGILL", -7: "SIGBUS"}
|
|
return signal_map.get(returncode, f"exit({returncode})")
|
|
|
|
|
|
def _count_by(items, key):
|
|
counts = {}
|
|
for item in items:
|
|
val = item.get(key, "unknown")
|
|
counts[val] = counts.get(val, 0) + 1
|
|
return counts
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="AFL++ Fuzz Testing CI/CD Agent")
|
|
sub = parser.add_subparsers(dest="command")
|
|
c = sub.add_parser("compile", help="Compile target with AFL++ instrumentation")
|
|
c.add_argument("--source", required=True)
|
|
c.add_argument("--output", required=True)
|
|
c.add_argument("--compiler", default="afl-clang-fast")
|
|
f = sub.add_parser("fuzz", help="Run AFL++ fuzzer")
|
|
f.add_argument("--binary", required=True)
|
|
f.add_argument("--input", required=True)
|
|
f.add_argument("--output", required=True)
|
|
f.add_argument("--duration", type=int, default=300, help="Duration in seconds")
|
|
f.add_argument("--memory", default="512", help="Memory limit in MB")
|
|
t = sub.add_parser("triage", help="Triage crash inputs")
|
|
t.add_argument("--binary", required=True)
|
|
t.add_argument("--crashes-dir", required=True)
|
|
s = sub.add_parser("stats", help="Parse fuzzer stats")
|
|
s.add_argument("--stats-file", required=True)
|
|
args = parser.parse_args()
|
|
if args.command == "compile":
|
|
result = compile_target(args.source, args.output, args.compiler)
|
|
elif args.command == "fuzz":
|
|
result = run_fuzzer(args.binary, args.input, args.output, args.duration, args.memory)
|
|
elif args.command == "triage":
|
|
result = triage_crashes(args.binary, args.crashes_dir)
|
|
elif args.command == "stats":
|
|
result = parse_fuzzer_stats(args.stats_file)
|
|
else:
|
|
parser.print_help()
|
|
return
|
|
print(json.dumps(result, indent=2, default=str))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|