rmi-backend/app/contract_deepscan.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

380 lines
13 KiB
Python

"""
Slither + Mythril Contract Scanner
==================================
Integration with Slither (static analysis) and Mythril (symbolic execution)
for comprehensive smart contract vulnerability detection.
"""
import json
import logging
import os
import subprocess
import tempfile
from dataclasses import asdict, dataclass
from typing import Any
from app.telegram_bot.requirements import httpx
logger = logging.getLogger(__name__)
# ─── VULNERABILITY CATEGORIES ─────────────────────────────────────
VULNERABILITY_TYPES = {
"reentrancy": "Reentrancy Attack",
"overflow": "Integer Overflow/Underflow",
"unchecked_call": "Unchecked Call Return Value",
"tx_origin": "Use of tx.origin",
"delegatecall": "Dangerous delegatecall",
"selfdestruct": "Unprotected selfdestruct",
"shadowing": "Variable Shadowing",
"timestamp": "Timestamp Dependency",
"call_depth": "Call Depth Limit Exceeded",
"low_level_call": "Low-level call",
}
# Severity levels
SEVERITY = {
"High": 3,
"Medium": 2,
"Low": 1,
"Informational": 0,
}
# ─── SCAN RESULT MODELS ──────────────────────────────────────────
@dataclass
class Finding:
"""Individual vulnerability finding."""
title: str
description: str
severity: str
contract: str
function: str
source_map: dict[str, Any]
swc_id: str = ""
reference: str = ""
@dataclass
class ScanResult:
"""Complete scan result."""
contract_address: str
chain: str
scanned: bool
timestamp: str
vulnerabilities: list[Finding]
summary: dict[str, int]
metrics: dict[str, Any]
# ─── SLITHER INTEGRATION ──────────────────────────────────────────
class SlitherScanner:
"""Slither static analysis scanner."""
def __init__(self, solc_version: str = "0.8.0"):
self.solc_version = solc_version
self._available = self._check_available()
def _check_available(self) -> bool:
"""Check if Slither is installed."""
try:
result = subprocess.run(["slither", "--version"], capture_output=True, timeout=5)
return result.returncode == 0
except (subprocess.TimeoutExpired, FileNotFoundError):
return False
def scan_file(self, file_path: str, **kwargs) -> dict[str, Any]:
"""
Scan a Solidity file with Slither.
Args:
file_path: Path to .sol file
**kwargs: Additional slither arguments
Returns:
Slither JSON output
"""
if not self._available:
return {"success": False, "error": "Slither not installed"}
cmd = ["slither", file_path, "--json", "-"]
if kwargs.get("solc"):
cmd.extend(["--solc", kwargs["solc"]])
if kwargs.get("solc_args"):
cmd.extend(["--solc-args", kwargs["solc_args"]])
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
return json.loads(result.stdout) if result.stdout else {}
except (subprocess.TimeoutExpired, json.JSONDecodeError) as e:
return {"success": False, "error": str(e)}
def scan_contract(self, contract_address: str, chain: str = "base") -> ScanResult:
"""
Scan an on-chain contract using Slither.
Args:
contract_address: Contract address to scan
chain: Blockchain network
Returns:
ScanResult with vulnerabilities
"""
if not self._available:
return self._stub_result(contract_address, chain)
# Get source code from blockchain explorer API
source_code = self._fetch_source_code(contract_address, chain)
if not source_code:
return self._stub_result(contract_address, chain, error="Could not fetch source")
# Write to temp file and scan
with tempfile.NamedTemporaryFile(mode="w", suffix=".sol", delete=False) as f:
f.write(source_code)
temp_path = f.name
try:
result = self.scan_file(temp_path)
# Parse findings
findings = []
if "results" in result:
for detector in result["results"].get("detectors", []):
finding = Finding(
title=detector["check"],
description=detector["description"],
severity=detector["impact"],
contract=contract_address,
function=detector.get("function", "unknown"),
source_map=detector.get("source_mapping", {}),
swc_id=detector.get("swc_id", ""),
)
findings.append(finding)
return ScanResult(
contract_address=contract_address,
chain=chain,
scanned=True,
timestamp=json.dumps("2026-05-12T00:00:00Z"), # Use real timestamp in prod
vulnerabilities=findings,
summary=self._count_by_severity(findings),
metrics={"lines_of_code": len(source_code.splitlines())},
)
finally:
os.unlink(temp_path)
def _fetch_source_code(self, address: str, chain: str) -> str | None:
"""Fetch contract source from block explorer."""
# Base explorer
if chain == "base":
url = f"https://api.basescan.org/api?module=contract&action=getsourcecode&address={address}&apikey={os.getenv('BASESCAN_API_KEY', '')}"
elif chain == "ethereum":
url = f"https://api.etherscan.io/api?module=contract&action=getsourcecode&address={address}&apikey={os.getenv('ETHERSCAN_API_KEY', '')}"
else:
url = None
if not url:
return None
try:
resp = httpx.get(url, timeout=10)
data = resp.json()
if data.get("status") == "1":
source = data["result"][0].get("SourceCode", "")
# Handle API response format
if source.startswith("{{") and source.endswith("}}"):
source = json.loads(source[1:-1])["SourceCode"]
return source
except Exception as e:
logger.error(f"Error fetching source: {e}")
return None
def _stub_result(self, address: str, chain: str, error: str = "") -> ScanResult:
"""Return stub result when Slither unavailable."""
return ScanResult(
contract_address=address,
chain=chain,
scanned=False,
timestamp="stub",
vulnerabilities=[],
summary={"High": 0, "Medium": 0, "Low": 0, "Informational": 0},
metrics={"error": error or "Slither not available", "lines_of_code": 0},
)
def _count_by_severity(self, findings: list[Finding]) -> dict[str, int]:
"""Count findings by severity."""
counts = {"High": 0, "Medium": 0, "Low": 0, "Informational": 0}
for f in findings:
counts[f.severity] = counts.get(f.severity, 0) + 1
return counts
# ─── MYTHRIL INTEGRATION ──────────────────────────────────────────
class MythrilScanner:
"""Mythril symbolic execution scanner."""
def __init__(self):
self._available = self._check_available()
def _check_available(self) -> bool:
"""Check if Mythril is installed."""
try:
result = subprocess.run(["myth", "--version"], capture_output=True, timeout=5)
return result.returncode == 0
except (subprocess.TimeoutExpired, FileNotFoundError):
return False
def scan_bytecode(self, bytecode: str, **kwargs) -> dict[str, Any]:
"""
Scan contract bytecode with Mythril.
Args:
bytecode: Contract bytecode (0x prefixed)
**kwargs: Additional myth arguments
Returns:
Mythril JSON output
"""
if not self._available:
return {"success": False, "error": "Mythril not installed", "issues": []}
cmd = ["myth", "-x", "-a", bytecode]
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=300, # 5 minutes for symbolic execution
)
# Parse output (Mythril outputs in YAML/JSON mixed format)
# For now, return simple structure
issues = []
for line in result.stdout.split("\n"):
if "Vulnerability" in line or "Error" in line:
issues.append({"type": "Detected", "description": line.strip()})
return {"success": True, "issues": issues}
except (subprocess.TimeoutExpired, Exception) as e:
return {"success": False, "error": str(e), "issues": []}
def scan_contract(self, contract_address: str, chain: str = "base") -> dict[str, Any]:
"""Scan contract using Mythril."""
if not self._available:
return {"error": "Mythril not installed", "contract": contract_address}
# Fetch bytecode
bytecode = self._fetch_bytecode(contract_address, chain)
if not bytecode:
return {"error": "Could not fetch bytecode", "contract": contract_address}
return self.scan_bytecode(bytecode)
def _fetch_bytecode(self, address: str, chain: str) -> str | None:
"""Get contract bytecode from blockchain."""
# This would use web3 to get bytecode directly
# For now, return placeholder
return "0x608060405234801561001057600080fd5b50600080546001600160a01b03191633179055610132806100326000396000f3fe608060405234801561001057600080fd5b50600080546001600160a01b031916331790556100c8806100326000396000f3fe608060405260043610601f5760003560e01c80633b3b5fde146026575b600080fd5b60366038565b005b600054604080513381526020602083015282517faf1358d3b491c7503447674258b676119e830f6325bad0377071868593a1348099fb5b1560355760336053565b565b6001548156fea164736f6c6343000819000a"
def scan_file(self, file_path: str, **kwargs) -> dict[str, Any]:
"""Scan Solidity file with Mythril."""
if not self._available:
return {"success": False, "error": "Mythril not installed"}
cmd = ["myth", "-x", "-f", file_path]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
return {"success": True, "output": result.stdout}
except subprocess.TimeoutExpired:
return {"success": False, "error": "Scan timed out"}
# ─── COMBINED SCANNER ─────────────────────────────────────────────
class ContractScanner:
"""Combined Slither + Mythril scanner."""
def __init__(self):
self.slither = SlitherScanner()
self.mythril = MythrilScanner()
def scan(self, contract_address: str, chain: str = "base", deep: bool = False) -> dict[str, Any]:
"""
Scan contract using available tools.
Args:
contract_address: Contract to scan
chain: Blockchain network
deep: Run both Slither and Mythril
Returns:
Combined scan result
"""
result = {
"contract": contract_address,
"chain": chain,
"timestamp": "2026-05-12T00:00:00Z", # Real timestamp in prod
"tools": [],
}
# Always run Slither
slither_result = self.slither.scan_contract(contract_address, chain)
if slither_result.scanned:
result["tools"].append("slither")
result["vulnerabilities"] = [asdict(f) for f in slither_result.vulnerabilities]
result["summary"] = slither_result.summary
else:
result["vulnerabilities"] = []
result["summary"] = {}
# Optionally run Mythril for deep analysis
if deep and self.mythril._available:
myth_result = self.mythril.scan_contract(contract_address, chain)
if myth_result.get("success"):
result["tools"].append("mythril")
if "issues" in myth_result:
for issue in myth_result["issues"]:
result["vulnerabilities"].append(
{
"title": issue.get("type", "Detected"),
"description": issue.get("description", ""),
"severity": "High",
}
)
return result
# ─── SINGLETON INSTANCE ───────────────────────────────────────────
_scanner = ContractScanner()
def deep_scan_contract(contract_address: str, chain: str = "base") -> dict[str, Any]:
"""Deep scan single contract (Slither + Mythril)."""
return _scanner.scan(contract_address, chain, deep=True)
def batch_deep_scan(contracts: list[str], chain: str = "base") -> dict[str, Any]:
"""Batch scan multiple contracts."""
results = []
for addr in contracts:
result = _scanner.scan(addr, chain, deep=False)
results.append(result)
return {"contracts": results, "total": len(results)}