LaelaZ's picture
Sync package to GitHub source: em-dashes out of rendered output; no API/logic change
0af1880 verified
"""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())