File size: 5,254 Bytes
31ea9b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0af1880
31ea9b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0af1880
31ea9b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
"""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())