#!/usr/bin/env python3 """Smart Contract Security Agent - runs Slither and Mythril analysis on Solidity contracts.""" import json import argparse import logging import subprocess from collections import defaultdict from datetime import datetime logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger(__name__) SWC_REGISTRY = { "SWC-101": "Integer Overflow and Underflow", "SWC-104": "Unchecked Call Return Value", "SWC-106": "Unprotected SELFDESTRUCT", "SWC-107": "Reentrancy", "SWC-110": "Assert Violation", "SWC-112": "Delegatecall to Untrusted Callee", "SWC-113": "DoS with Failed Call", "SWC-115": "Authorization through tx.origin", "SWC-116": "Block values as a proxy for time", "SWC-120": "Weak Sources of Randomness", } def run_slither(contract_path): """Run Slither static analysis on Solidity contract.""" cmd = ["slither", contract_path, "--json", "-"] result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) try: return json.loads(result.stdout) if result.stdout else {} except json.JSONDecodeError: logger.error("Slither JSON parse failed") return {} def run_mythril(contract_path, timeout=300): """Run Mythril symbolic execution analysis.""" cmd = ["myth", "analyze", contract_path, "--execution-timeout", str(timeout), "-o", "json"] result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 60) try: return json.loads(result.stdout) if result.stdout else {} except json.JSONDecodeError: logger.error("Mythril JSON parse failed") return {} def analyze_slither_results(slither_output): """Parse and categorize Slither detector findings.""" findings = [] by_severity = defaultdict(int) by_detector = defaultdict(int) for detector in slither_output.get("results", {}).get("detectors", []): severity = detector.get("impact", "informational").lower() by_severity[severity] += 1 det_name = detector.get("check", "unknown") by_detector[det_name] += 1 elements = detector.get("elements", []) location = "" if elements: elem = elements[0] location = f"{elem.get('source_mapping', {}).get('filename_short', '')}:" \ f"L{elem.get('source_mapping', {}).get('lines', [0])[0] if elem.get('source_mapping', {}).get('lines') else 0}" findings.append({ "detector": det_name, "severity": severity, "description": detector.get("description", "")[:200], "location": location, "confidence": detector.get("confidence", ""), }) return { "total": len(findings), "by_severity": dict(by_severity), "by_detector": dict(sorted(by_detector.items(), key=lambda x: x[1], reverse=True)[:15]), "findings": sorted(findings, key=lambda x: {"high": 0, "medium": 1, "low": 2, "informational": 3}.get(x["severity"], 4)), } def analyze_mythril_results(mythril_output): """Parse Mythril symbolic execution findings.""" findings = [] by_swc = defaultdict(int) for issue in mythril_output.get("issues", []): swc_id = issue.get("swc-id", "") swc_key = f"SWC-{swc_id}" if swc_id else "unknown" by_swc[swc_key] += 1 severity = issue.get("severity", "Medium").lower() findings.append({ "swc_id": swc_key, "swc_title": SWC_REGISTRY.get(swc_key, issue.get("title", "")), "severity": severity, "description": issue.get("description", "")[:200], "contract": issue.get("contract", ""), "function": issue.get("function", ""), "line_number": issue.get("lineno", 0), }) return { "total": len(findings), "by_swc": dict(by_swc), "findings": findings, } def deduplicate_findings(slither_findings, mythril_findings): """Merge and deduplicate findings from both tools.""" combined = [] seen = set() for f in slither_findings.get("findings", []): key = (f.get("location", ""), f.get("detector", "")) if key not in seen: seen.add(key) combined.append({**f, "source": "slither"}) for f in mythril_findings.get("findings", []): key = (f.get("contract", "") + str(f.get("line_number", 0)), f.get("swc_id", "")) if key not in seen: seen.add(key) combined.append({**f, "source": "mythril"}) return combined def generate_report(contract_path, slither_analysis, mythril_analysis, combined): critical_high = sum(1 for f in combined if f.get("severity") in ("high", "critical")) return { "timestamp": datetime.utcnow().isoformat(), "contract": contract_path, "slither_analysis": { "total_findings": slither_analysis["total"], "by_severity": slither_analysis["by_severity"], "top_detectors": slither_analysis["by_detector"], }, "mythril_analysis": { "total_findings": mythril_analysis["total"], "by_swc": mythril_analysis["by_swc"], }, "combined_findings": len(combined), "critical_high_findings": critical_high, "audit_result": "FAIL" if critical_high > 0 else "PASS", "findings": combined[:30], } def main(): parser = argparse.ArgumentParser(description="Solidity Smart Contract Security Analysis Agent") parser.add_argument("--contract", required=True, help="Path to Solidity contract or project directory") parser.add_argument("--mythril-timeout", type=int, default=300, help="Mythril execution timeout (seconds)") parser.add_argument("--skip-mythril", action="store_true", help="Skip Mythril (slow symbolic execution)") parser.add_argument("--output", default="smart_contract_audit_report.json") args = parser.parse_args() slither_output = run_slither(args.contract) slither_analysis = analyze_slither_results(slither_output) mythril_analysis = {"total": 0, "by_swc": {}, "findings": []} if not args.skip_mythril: mythril_output = run_mythril(args.contract, args.mythril_timeout) mythril_analysis = analyze_mythril_results(mythril_output) combined = deduplicate_findings(slither_analysis, mythril_analysis) report = generate_report(args.contract, slither_analysis, mythril_analysis, combined) with open(args.output, "w") as f: json.dump(report, f, indent=2, default=str) logger.info("Smart contract audit: %d findings (%d critical/high), result: %s", report["combined_findings"], report["critical_high_findings"], report["audit_result"]) print(json.dumps(report, indent=2, default=str)) if __name__ == "__main__": main()