"""Command-line interface for solidity-audit-ai. Examples -------- python -m auditor.cli samples/vulnerable/Reentrancy.sol python -m auditor.cli samples/ --html report.html --md report.md python -m auditor.cli contracts/ --format json """ from __future__ import annotations import argparse import json import sys import webbrowser from pathlib import Path from auditor.engine import audit_path from auditor.llm import get_provider from auditor.report import to_html, to_markdown # ANSI colors for the terminal summary. _COLOR = { "Critical": "\033[97;41m", "High": "\033[91m", "Medium": "\033[93m", "Low": "\033[96m", "Informational": "\033[90m", } _RESET = "\033[0m" def _supports_color() -> bool: return sys.stdout.isatty() def _print_terminal(result) -> None: color = _supports_color() print() print("=" * 60) print(" solidity-audit-ai: audit summary") print("=" * 60) counts = result.counts_by_severity() for sev_label, n in counts.items(): c = _COLOR.get(sev_label, "") if color else "" r = _RESET if color else "" bar = "█" * min(n, 40) print(f" {c}{sev_label:>14}{r} | {n:>3} {bar}") print("-" * 60) print(f" {'TOTAL':>14} | {result.total:>3}") print(f" files scanned: {len(result.files)}") print("=" * 60) for i, f in enumerate(result.sorted_findings(), 1): c = _COLOR.get(f.severity.label, "") if color else "" r = _RESET if color else "" print( f"\n[{i}] {c}{f.severity.label}{r} {f.title} " f"({f.swc_id}), {f.location}" ) if f.code: print(f" {f.code}") print(f" → {f.remediation}") if result.errors: print("\nWarnings:") for e in result.errors: print(f" - {e}") print() def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( prog="solidity-audit-ai", description="AI-assisted static security auditor for Solidity smart contracts.", ) p.add_argument("path", help="Path to a .sol file or a directory of contracts.") p.add_argument("--md", metavar="FILE", help="Write a Markdown report to FILE.") p.add_argument("--html", metavar="FILE", help="Write a self-contained HTML report to FILE.") p.add_argument( "--format", choices=["terminal", "md", "json"], default="terminal", help="What to print to stdout (default: terminal).", ) p.add_argument( "--provider", default=None, help="LLM provider: offline | openai | anthropic (default: auto/offline).", ) p.add_argument( "--no-enrich", action="store_true", help="Skip LLM-assisted explanations/fixes (static findings only).", ) p.add_argument( "--slither", action="store_true", help="Also run Slither if installed (degrades to no-op otherwise).", ) p.add_argument("--open", action="store_true", help="Open the HTML report in a browser.") p.add_argument( "--fail-on", choices=["critical", "high", "medium", "low", "informational", "none"], default="none", help="Exit non-zero if a finding of this severity or higher exists (for CI).", ) p.add_argument("--title", default="Smart Contract Audit Report", help="Report title.") return p _SEV_ORDER = {"critical": 5, "high": 4, "medium": 3, "low": 2, "informational": 1, "none": 0} def main(argv: list[str] | None = None) -> int: args = build_parser().parse_args(argv) provider = get_provider(args.provider) result = audit_path( args.path, provider=provider, enrich=not args.no_enrich, use_slither=args.slither, ) # Output files. Status messages go to stderr so stdout stays clean for # machine consumption (e.g. `--format json > out.json`). if args.md: Path(args.md).write_text(to_markdown(result, args.title), encoding="utf-8") print(f"Wrote Markdown report: {args.md}", file=sys.stderr) if args.html: Path(args.html).write_text(to_html(result, args.title), encoding="utf-8") print(f"Wrote HTML report: {args.html}", file=sys.stderr) if args.open: webbrowser.open(Path(args.html).resolve().as_uri()) # Stdout. if args.format == "terminal": _print_terminal(result) elif args.format == "md": print(to_markdown(result, args.title)) elif args.format == "json": print( json.dumps( { "summary": result.counts_by_severity(), "total": result.total, "files": result.files, "provider": result.provider, "errors": result.errors, "findings": [f.to_dict() for f in result.sorted_findings()], }, indent=2, ) ) # CI gate. threshold = _SEV_ORDER[args.fail_on] if threshold: worst = max((int(f.severity) for f in result.findings), default=0) if worst >= threshold: return 1 return 0 if __name__ == "__main__": # pragma: no cover sys.exit(main())