rmi-backend/app/routers/security_intel.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

682 lines
23 KiB
Python

"""
Security Intelligence Router v2 - Complete Crypto Security Stack
===================================================================
All crypto security modules exposed via REST:
• Contract: bytecode scan + deep scan (Slither + Mythril)
• Wallet: reputation + threat intel + anomaly detection
• Graph: clustering + fund flow + cross-chain correlation
• Mempool: real-time attack detection + sentinel control
• Threat: OFAC + local blocklist + batch lookup
• Dashboard: alert aggregation + monitoring + scoring
• ML: anomaly detection on wallets + token metrics
• CrossChain: wallet linking across chains
Updated 2026-05-08 for world-class crypto intelligence.
"""
from __future__ import annotations
# ═══ LAZY IMPORTS (deferred to first use - saves ~2.2s cold start) ═══
import importlib as _il
import os
from datetime import UTC, datetime
from typing import Any
_lazy_cache = {}
def _L(module_path):
if module_path not in _lazy_cache:
_lazy_cache[module_path] = _il.import_module(module_path)
return _lazy_cache[module_path]
# Lightweight imports (eager - these are fast)
from fastapi import APIRouter # noqa: E402
from pydantic import BaseModel, Field # noqa: E402
from app.cross_chain_correlator import ChainFingerprint, CrossChainCorrelator # noqa: E402
router = APIRouter(prefix="/api/v1/security", tags=["security"])
# ─── SINGLETONS ────────────────────────────────────────────
_sentinel_manager = None
_wallet_detector = None
_token_detector = None
async def get_sentinel() -> SentinelManager: # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue
global _sentinel_manager
if _sentinel_manager is None:
_sentinel_manager = _L("app.mempool_sentinel").SentinelManager()
return _sentinel_manager
async def get_wallet_detector():
global _wallet_detector
if _wallet_detector is None:
_ml = _L("app.ml_anomaly")
_wallet_detector = _ml.WalletAnomalyDetector(contamination=0.05)
return _wallet_detector
async def get_token_detector() -> TokenMetricAnomalyDetector: # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue
global _token_detector
if _token_detector is None:
_ml = _L("app.ml_anomaly")
_token_detector = _ml.TokenMetricAnomalyDetector()
return _token_detector
# ─── MODELS ────────────────────────────────────────────────
class ContractScanRequest(BaseModel):
address: str = Field(..., min_length=32, max_length=44)
chain: str = Field(default="ethereum")
deep: bool = Field(default=False, description="Run Slither + Mythril deep scan")
class BatchContractRequest(BaseModel):
addresses: list[str] = Field(..., min_length=1, max_length=50)
chain: str = Field(default="ethereum")
deep: bool = Field(default=False)
class WalletCheckRequest(BaseModel):
address: str = Field(..., min_length=32, max_length=44)
chain: str = Field(default="auto")
class WalletAnalyzeRequest(BaseModel):
address: str = Field(..., min_length=32, max_length=44)
transactions: list[dict[str, Any]] = Field(default_factory=list, description="Raw tx list for ML analysis")
chain: str = Field(default="auto")
class GraphAnalyzeRequest(BaseModel):
transactions: list[dict[str, Any]] = Field(..., min_length=2, max_length=1000)
chain: str = Field(default="ethereum")
analysis_types: list[str] = Field(default=["clusters", "flows", "anomalies"])
class ThreatBatchRequest(BaseModel):
addresses: list[str] = Field(..., min_length=1, max_length=100)
chain: str = Field(default="evm")
class CrossChainRequest(BaseModel):
fingerprints: list[dict[str, Any]] = Field(..., min_length=2, description="Chain fingerprints")
chains: list[str] = Field(default=["ethereum", "base", "solana"])
class TokenMetricRequest(BaseModel):
token: str = Field(...)
metrics: dict[str, float] = Field(..., description="Current metric values")
class AlertAckRequest(BaseModel):
alert_id: str = Field(...)
# ─── CONTRACT ANALYSIS ─────────────────────────────────────
@router.get("/contract/{address}")
async def contract_analysis(address: str, chain: str = "ethereum", deep: bool = False):
"""Analyze contract - bytecode scan + optional Slither/Mythril deep scan."""
# Fast bytecode scan
_oca = _L("app.onchain_analyzer")
result = await _oca.analyze_contract(address, chain)
response = {
"address": result.address,
"chain": result.chain,
"is_contract": result.is_contract,
"is_proxy": result.is_proxy,
"proxy_type": result.proxy_type,
"proxy_implementation": result.proxy_implementation,
"risk_score": result.risk_score,
"risk_flags": result.risk_flags,
"detected_interfaces": result.detected_interfaces,
"has_selfdestruct": result.has_selfdestruct,
"has_delegatecall": result.has_delegatecall,
"has_mint": result.has_mint,
"has_burn": result.has_burn,
"has_renounce_ownership": result.has_renounce_ownership,
"has_reentrancy_guard": result.has_reentrancy_guard,
"uses_tx_origin": result.uses_tx_origin,
"uses_timestamp": result.uses_timestamp,
"honeypot_indicators": result.honeypot_indicators,
"anti_bot_detected": result.anti_bot_detected,
"uniswap_pair": result.uniswap_pair,
"analysis_time": result.analysis_time,
}
# Deep scan (if requested)
if deep:
try:
_deep = _L("app.contract_deepscan")
deep_result = await _deep.deep_scan_contract(address, chain, use_cache=True)
response["deep_scan"] = deep_result.to_dict()
# Merge scores
response["risk_score"] = max(response["risk_score"], deep_result.risk_score)
response["deep_vulnerabilities"] = len(deep_result.vulnerabilities)
except Exception as e:
response["deep_scan_error"] = str(e)
return response
@router.post("/scan/batch")
async def batch_contract_scan(req: BatchContractRequest):
"""Batch contract scan - fast or deep."""
if req.deep:
_deep = _L("app.contract_deepscan")
results = await _deep.batch_deep_scan(req.addresses, req.chain)
return {
"scanned": len(results),
"chain": req.chain,
"high_risk": [r.address for r in results if r.risk_score >= 70],
"critical": [r.address for r in results if r.risk_score >= 90],
"results": [r.to_dict() for r in results],
}
else:
_oca = _L("app.onchain_analyzer")
results = await _oca.batch_analyze_contracts(req.addresses, req.chain)
return {
"scanned": len(results),
"chain": req.chain,
"high_risk": [r.address for r in results if r.risk_score >= 70],
"proxies": [r.address for r in results if r.is_proxy],
"honeypots": [r.address for r in results if r.honeypot_indicators],
"results": [
{
"address": r.address,
"risk_score": r.risk_score,
"risk_flags": r.risk_flags,
"is_proxy": r.is_proxy,
"honeypot": r.honeypot_indicators,
}
for r in results
],
}
# ─── WALLET / THREAT INTEL ──────────────────────────────────
@router.get("/wallet/{address}")
async def wallet_security_check(address: str, chain: str = "auto"):
"""Full wallet security check: reputation + threat intel."""
_cg = _L("app.crypto_guard")
reputation = await _cg.check_wallet_reputation(address, chain)
_ti = _L("app.threat_intel")
agg = await _ti.get_aggregator()
threat_rep = await agg.check_address(address, chain)
final_score = max(reputation.get("score", 0), threat_rep.risk_score)
return {
"address": address,
"chain": chain,
"risk_score": final_score,
"blocked": reputation.get("blocked", False) or threat_rep.is_blocked,
"sanctioned": threat_rep.is_sanctioned,
"scam": threat_rep.is_scam,
"mixer": threat_rep.is_mixer,
"categories": list(threat_rep.categories),
"tags": list(threat_rep.tags),
"threat_reports": len(threat_rep.threat_reports),
"reputation_flags": reputation.get("flags", []),
"first_flagged": threat_rep.first_flagged,
"last_flagged": threat_rep.last_flagged,
}
@router.post("/wallet/analyze")
async def wallet_ml_analysis(req: WalletAnalyzeRequest):
"""ML behavioral analysis of wallet transactions."""
from app.ml_anomaly import extract_wallet_features
features = extract_wallet_features(req.transactions, req.address)
detector = await get_wallet_detector()
# Fit detector if we have comparison data
if req.transactions and len(req.transactions) > 5:
detector.fit([features])
result = detector.predict(features)
else:
result = {"error": "Insufficient transactions for ML analysis (need >5)"}
return {
"address": req.address,
"features": {
"tx_count": features.tx_count,
"avg_tx_value": features.avg_tx_value,
"unique_counterparties": features.unique_counterparties,
"dex_swaps": features.dex_swaps,
"burst_ratio": features.burst_ratio,
"night_ratio": features.night_tx_ratio,
},
"ml_result": result,
}
@router.post("/threat/check")
async def batch_threat_check(req: ThreatBatchRequest):
"""Batch threat intel lookup."""
_ti = _L("app.threat_intel")
agg = await _ti.get_aggregator()
results = await agg.batch_check(req.addresses, req.chain)
return {
"checked": len(results),
"blocked": [a for a, r in results.items() if r.is_blocked],
"sanctioned": [a for a, r in results.items() if r.is_sanctioned],
"scams": [a for a, r in results.items() if r.is_scam],
"mixers": [a for a, r in results.items() if r.is_mixer],
"results": {a: r.to_dict() for a, r in results.items()},
}
# ─── GRAPH ANALYSIS ─────────────────────────────────────────
@router.post("/graph/analyze")
async def graph_analyze(req: GraphAnalyzeRequest):
"""Transaction graph: clusters, flows, anomalies."""
_tga = _L("app.tx_graph_analyzer")
graph = await _tga.build_graph_from_transactions(req.transactions, req.chain)
response = {
"nodes": len(graph.nodes),
"edges": len(graph.edges),
"wallets": list(graph.nodes.keys())[:50],
}
if "clusters" in req.analysis_types:
clusters = graph.cluster_by_louvain()
response["clusters"] = [
{
"id": c.id,
"wallet_count": len(c.wallets),
"total_volume": c.total_volume,
}
for c in clusters[:10]
]
if "shared_funding" in req.analysis_types:
shared = graph.cluster_by_shared_funding(min_common=2)
response["shared_funding_clusters"] = [
{
"id": c.id,
"wallet_count": len(c.wallets),
"shared_funders": c.shared_funders[:5],
}
for c in shared[:10]
]
if "flows" in req.analysis_types:
cycles = graph.detect_circular_flows(max_cycle_len=4)
response["circular_flows"] = len(cycles)
response["wash_trading_suspected"] = len(cycles) > 2
if "anomalies" in req.analysis_types:
bursts = graph.detect_burst_transfers(min_count=15)
sudden = graph.detect_sudden_wealth(multiplier=20.0)
response["burst_activity"] = bursts[:5]
response["sudden_wealth"] = sudden[:5]
if "export" in req.analysis_types:
import tempfile
path = tempfile.mktemp(suffix=".gexf")
graph.export_gexf(path)
response["gexf_export_path"] = path
return response
# ─── CROSS-CHAIN ────────────────────────────────────────────
@router.post("/crosschain/correlate")
async def crosschain_correlate(req: CrossChainRequest):
"""Find cross-chain wallet correlations."""
correlator = CrossChainCorrelator(confidence_threshold=0.6)
# Add fingerprints
for fp_data in req.fingerprints:
import numpy as np
fp = ChainFingerprint(
address=fp_data["address"],
chain=fp_data["chain"],
feature_vector=np.array(fp_data.get("features", []), dtype=np.float64),
first_active=fp_data.get("first_active"),
last_active=fp_data.get("last_active"),
total_txs=fp_data.get("total_txs", 0),
total_volume=fp_data.get("total_volume", 0.0),
known_tags=fp_data.get("tags", []),
)
correlator.add_fingerprint(fp)
# Find links
links = correlator.find_all_links(req.chains)
return {
"fingerprints_analyzed": len(req.fingerprints),
"links_found": len(links),
"high_confidence_links": [line.to_dict() for line in links if line.confidence >= 0.8],
"all_links": [line.to_dict() for line in links[:20]],
}
# ─── ML / ANOMALY ───────────────────────────────────────────
@router.post("/ml/token/anomaly")
async def token_metric_anomaly(req: TokenMetricRequest):
"""Detect token metric anomalies vs baseline."""
detector = await get_token_detector()
detector.update_baseline(req.token, req.metrics)
result = detector.detect_anomaly(req.token, req.metrics)
return result
# ─── MEMPOOL ────────────────────────────────────────────────
@router.get("/mempool/status")
async def mempool_status():
"""Mempool sentinel status + recent detections."""
sentinel = await get_sentinel()
stats = sentinel.get_stats()
patterns = sentinel.sentinel.get_recent_patterns(20) if sentinel.sentinel else []
return {
**stats,
"recent_patterns": patterns,
}
@router.post("/mempool/start")
async def mempool_start(chain: str = "ethereum"):
sentinel = await get_sentinel()
await sentinel.start(chain)
return {"status": "started", "chain": chain}
@router.post("/mempool/stop")
async def mempool_stop():
sentinel = await get_sentinel()
await sentinel.stop()
return {"status": "stopped"}
# ─── DASHBOARD ──────────────────────────────────────────────
@router.get("/dashboard/stats")
async def dashboard_stats():
"""Security dashboard statistics."""
_sd = _L("app.security_dashboard")
dash = _sd.get_dashboard()
return dash.get_stats()
@router.get("/dashboard/alerts")
async def dashboard_alerts(
severity: str | None = None,
source: str | None = None,
limit: int = 100,
unacknowledged: bool = False,
):
"""Query security alerts."""
_sd = _L("app.security_dashboard")
dash = _sd.get_dashboard()
return dash.get_alerts(severity, source, limit, unacknowledged)
@router.post("/dashboard/acknowledge")
async def dashboard_acknowledge(req: AlertAckRequest):
"""Acknowledge an alert."""
_sd = _L("app.security_dashboard")
dash = _sd.get_dashboard()
success = await dash.acknowledge(req.alert_id)
return {"acknowledged": success}
@router.get("/dashboard/score")
async def dashboard_score(hours: int = 24):
"""Get rolling security score."""
_sd = _L("app.security_dashboard")
dash = _sd.get_dashboard()
return {
"security_score": dash.get_security_score(hours),
"window_hours": hours,
"max": 100,
}
@router.get("/dashboard/timeline/{target}")
async def dashboard_timeline(target: str):
"""Get incident timeline for a target."""
_sd = _L("app.security_dashboard")
dash = _sd.get_dashboard()
return {"timeline": dash.get_incident_timeline(target)}
# ─── ENTITY LABELING ─────────────────────────────────────────
@router.get("/entity/label/{address}")
async def entity_label(address: str):
"""Lookup entity label for an address."""
_el = _L("app.entity_labeler")
entity = await _el.label_address(address)
if not entity:
return {"address": address, "entity": None, "known": False}
return {"address": address, "entity": entity.to_dict(), "known": True}
@router.post("/entity/batch")
async def entity_batch_label(addresses: list[str]):
"""Batch entity label lookup."""
_el = _L("app.entity_labeler")
return await _el.batch_label_addresses(addresses)
@router.get("/entity/search")
async def entity_search(q: str = "", category: str | None = None, limit: int = 50):
"""Search entity database."""
_el = _L("app.entity_labeler")
db = await _el.get_entity_labeler()
results = await db.search_entities(query=q, category=category, limit=limit)
return {"results": [r.to_dict() for r in results], "count": len(results)}
@router.get("/entity/stats")
async def entity_stats():
"""Entity database statistics."""
_el = _L("app.entity_labeler")
db = await _el.get_entity_labeler()
return await db.get_stats()
@router.get("/entity/{entity_id}")
async def entity_detail(entity_id: str):
"""Full entity graph with addresses and relationships."""
_el = _L("app.entity_labeler")
db = await _el.get_entity_labeler()
return await db.get_entity_graph(entity_id)
# ─── PORTFOLIO TRACKER ──────────────────────────────────────
@router.get("/portfolio/{wallet}")
async def portfolio_single(wallet: str, chain: str = "ethereum"):
"""Get portfolio for a single wallet."""
_pt = _L("app.portfolio_tracker")
portfolio = await _pt.get_portfolio(wallet, chain)
return portfolio.to_dict()
@router.post("/portfolio/multi")
async def portfolio_multi(wallets: list[dict[str, str]]):
"""Aggregate portfolio across multiple wallets/chains."""
_pt = _L("app.portfolio_tracker")
result = await _pt.get_multi_wallet_portfolio(wallets)
return result.to_dict()
@router.get("/portfolio/history/{wallet}")
async def portfolio_history(wallet: str, chain: str = "ethereum", hours: int = 24):
"""Historical net worth chart data."""
_pt = _L("app.portfolio_tracker")
history = await _pt.get_portfolio_history(wallet, chain, hours)
return {
"wallet": wallet,
"chain": chain,
"hours": hours,
"data_points": len(history),
"history": history,
}
# ─── WALLET MONITOR ─────────────────────────────────────────
@router.post("/monitor/watch")
async def monitor_watch(address: str, chain: str = "ethereum", tags: str = ""):
"""Add a wallet to the watchlist."""
_wm = _L("app.wallet_monitor")
manager = _wm.WalletMonitorManager()
await manager.add_watch(address, chain, tags.split(",") if tags else [])
return {"watched": True, "address": address, "chain": chain}
@router.post("/monitor/unwatch")
async def monitor_unwatch(address: str, chain: str = "ethereum"):
"""Remove a wallet from the watchlist."""
_wm = _L("app.wallet_monitor")
manager = _wm.WalletMonitorManager()
await manager.remove_watch(address, chain)
return {"unwatched": True, "address": address, "chain": chain}
@router.get("/monitor/watches")
async def monitor_list():
"""List all watched wallets."""
_wm = _L("app.wallet_monitor")
manager = _wm.WalletMonitorManager()
watches = await manager.list_watches()
return {"watches": watches, "count": len(watches)}
# ─── EXCHANGE FLOW ANALYZER ─────────────────────────────────
@router.post("/flows/analyze")
async def flows_analyze(
entity_addresses: list[str],
entity_name: str,
entity_id: str,
chain: str,
transactions: list[dict[str, Any]],
hours: int = 24,
):
"""Analyze fund flows for an entity."""
_efa = _L("app.exchange_flow_analyzer")
return await _efa.analyze_entity_flows(entity_addresses, entity_name, entity_id, chain, transactions, hours)
@router.post("/flows/reserves")
async def flows_reserves(
entity_addresses: list[str],
entity_name: str,
entity_id: str,
chain: str,
):
"""Get reserve snapshot for entity addresses."""
_efa = _L("app.exchange_flow_analyzer")
return await _efa.get_reserves(entity_addresses, entity_name, entity_id, chain)
@router.post("/flows/compare")
async def flows_compare(
entity_a: dict[str, Any],
entity_b: dict[str, Any],
transactions: list[dict[str, Any]],
):
"""Compare fund flows between two entities."""
_efa = _L("app.exchange_flow_analyzer")
return await _efa.compare_flows(entity_a, entity_b, transactions)
# ─── HEALTH ────────────────────────────────────────────────
@router.get("/health")
async def security_health():
"""Full subsystem health check."""
# Check all modules
modules = {}
try:
from app.onchain_analyzer import get_w3
w3 = get_w3("ethereum")
modules["onchain_analyzer"] = "connected" if w3 and w3.is_connected() else "disconnected"
except Exception as e:
modules["onchain_analyzer"] = f"error: {str(e)[:30]}"
try:
import sklearn
modules["ml_anomaly"] = f"sklearn {sklearn.__version__}"
except ImportError:
modules["ml_anomaly"] = "sklearn not installed"
try:
import slither_analyzer
modules["contract_deepscan"] = f"slither {slither_analyzer.__version__}"
except ImportError:
modules["contract_deepscan"] = "slither not installed"
# Redis
try:
r = await _get_redis()
modules["redis"] = "connected" if r and await r.ping() else "disconnected"
except Exception:
modules["redis"] = "disconnected"
return {
"status": "operational",
"modules": modules,
"version": "2.0.0",
"timestamp": datetime.now(UTC).isoformat(),
}
# Redis helper for health check
async def _get_redis():
try:
import redis.asyncio as redis_async
return redis_async.Redis(
host=os.getenv("REDIS_HOST", "127.0.0.1"),
port=int(os.getenv("REDIS_PORT", "6379")),
password=os.getenv("REDIS_PASSWORD") or None,
db=int(os.getenv("REDIS_DB", "0")),
decode_responses=True,
)
except Exception:
return None