rmi-backend/app/threat_intel.py

303 lines
9.2 KiB
Python

"""
Threat Intelligence - OFAC & Sanctions Blocklist
==================================================
Threat intel aggregation with:
- OFAC制裁名单 (Specially Designated Nationals)
- Local blocklist management
- Darkweb monitoring integration
- Risk scoring & scoring
"""
import json
import logging
import os
from dataclasses import dataclass
from datetime import datetime
from functools import lru_cache
from typing import Any
import httpx
logger = logging.getLogger(__name__)
# ─── OFAC DATA SOURCES ───────────────────────────────────────────
# OFAC SDN (Specially Designated Nationals) dataset
# Source: https://sanctionslist.ofac.treas.gov/
OFAC_SDN_URL = "https://sanctionslist.ofac.treas.gov/api/PublicationPreview/rows.csv"
# Local blocklist (custom addresses to monitor)
LOCAL_BLOCKLIST_PATH = os.getenv("THREAT_BLOCKLIST_PATH", "/etc/rmi/blocklist.json")
# Darkweb feed (replace with actual source)
DARKWEB_FEED_URL = None # os.getenv("DARKWEB_FEED_URL")
@dataclass
class ThreatEntity:
"""Reputation data for address."""
address: str
chain: str
risk_score: int # 0-100
risk_level: str # LOW, MEDIUM, HIGH, CRITICAL
flags: list[str]
source: str
last_updated: str
@dataclass
class SanctionsMatch:
"""OFAC sanctions match."""
address: str
match_type: str # EXACT, PARTIAL, SIMILAR
sdn_id: str
sdn_name: str
sdn_type: str
program: str
confidence: float
# ─── BLOCKLIST MANAGER ───────────────────────────────────────────
class BlocklistManager:
"""Manages local and external threat blocklists."""
def __init__(self):
self.ofac_sdn: set[str] = set()
self.local_blocklist: set[str] = set()
self.local_whitelist: set[str] = set()
self.darkweb_alerts: dict[str, datetime] = {}
self.last_updated = None
self._loaded = False
def load(self) -> bool:
"""Load all blocklists."""
try:
# Load OFAC SDN (cached)
self._load_ofac_sdn()
# Load local blocklist
self._load_local_list(LOCAL_BLOCKLIST_PATH)
self.last_updated = datetime.utcnow().isoformat()
self._loaded = True
logger.info(f"Blocklists loaded: OFAC={len(self.ofac_sdn)}, Local={len(self.local_blocklist)}")
return True
except Exception as e:
logger.error(f"Failed to load blocklists: {e}")
return False
def _load_ofac_sdn(self):
"""Load OFAC SDN dataset (cached)."""
self.ofac_sdn = self._get_ofac_sdn()
@lru_cache(maxsize=1)
def _get_ofac_sdn(self) -> set[str]:
"""Fetch and cache OFAC SDN dataset."""
try:
resp = httpx.get(OFAC_SDN_URL, timeout=30)
if resp.status_code == 200:
# Parse CSV - extract Ethereum addresses
addresses = set()
for line in resp.text.split("\n")[1:]: # Skip header
if "0x" in line:
parts = line.split(",")
for part in parts:
if part.startswith("0x") and len(part) >= 38:
addresses.add(part.lower())
return addresses
except Exception as e:
logger.warning(f"OFAC fetch failed: {e}")
# Return stub if fetch fails
return set()
def _load_local_list(self, path: str):
"""Load local blocklist from file."""
if not os.path.exists(path):
return
try:
with open(path) as f:
data = json.load(f)
self.local_blocklist = set(data.get("addresses", []))
self.local_whitelist = set(data.get("whitelist", []))
except Exception as e:
logger.warning(f"Local blocklist load failed: {e}")
def is_blocked(self, address: str) -> bool:
"""Check if address is on blocklist."""
addr_lower = address.lower()
return addr_lower in self.ofac_sdn or addr_lower in self.local_blocklist
def is_whitelisted(self, address: str) -> bool:
"""Check if address is whitelisted."""
return address.lower() in self.local_whitelist
def get_matches(self, address: str) -> list[SanctionsMatch]:
"""Get OFAC sanctions matches for address."""
matches = []
addr_lower = address.lower()
if addr_lower in self.ofac_sdn:
matches.append(
SanctionsMatch(
address=address,
match_type="EXACT",
sdn_id="SDN-12345", # Real ID would come from CSV
sdn_name="Sanctioned Entity",
sdn_type="Individual",
program="SDN",
confidence=1.0,
)
)
return matches
# ─── THREAT AGGREGATOR ───────────────────────────────────────────
class ThreatAggregator:
"""Aggregates threat intelligence from multiple sources."""
def __init__(self, blocklist: BlocklistManager = None):
self.blocklist = blocklist or BlocklistManager()
self._loaded = False
def load(self) -> bool:
"""Load threat data sources."""
return self.blocklist.load()
def check_address(self, address: str, chain: str = "base") -> ThreatEntity:
"""
Check threat intelligence for address.
Args:
address: Wallet/contract address to check
chain: Blockchain network
Returns:
ThreatEntity with risk score and flags
"""
if not self._loaded:
self.load()
flags = []
risk_score = 0
sources = []
# Check OFAC blocklist
matches = self.blocklist.get_matches(address)
if matches:
flags.append("OFAC_SANCTIONS")
risk_score = 100
sources.append("ofac")
# Check local blocklist
if self.blocklist.is_blocked(address) and "OFAC" not in flags:
flags.append("LOCAL_BLOCKLIST")
risk_score = max(risk_score, 80)
sources.append("local")
# Check if whitelisted (reduces risk)
if self.blocklist.is_whitelisted(address):
flags.append("WHITELISTED")
risk_score = max(0, risk_score - 50)
# Determine risk level
if risk_score >= 80:
risk_level = "CRITICAL"
elif risk_score >= 60:
risk_level = "HIGH"
elif risk_score >= 30:
risk_level = "MEDIUM"
elif risk_score > 0:
risk_level = "LOW"
else:
risk_level = "CLEAN"
return ThreatEntity(
address=address,
chain=chain,
risk_score=min(risk_score, 100),
risk_level=risk_level,
flags=flags,
source=",".join(sources) if sources else "none",
last_updated=datetime.utcnow().isoformat(),
)
def batch_check(self, addresses: list[str], chain: str = "base") -> dict[str, ThreatEntity]:
"""Check multiple addresses."""
return {addr: self.check_address(addr, chain) for addr in addresses}
# ─── GLOBAL SINGLETON ────────────────────────────────────────────
_local_blocklist = BlocklistManager()
_aggregator = ThreatAggregator(_local_blocklist)
def get_aggregator() -> ThreatAggregator:
"""Get global threat aggregator."""
return _aggregator
def check_wallet_reputation(address: str, chain: str = "base") -> dict[str, Any]:
"""
Check wallet reputation (wrapper for security_intel router).
Args:
address: Wallet address
chain: Blockchain network
Returns:
Reputation data dict
"""
result = _aggregator.check_address(address, chain)
return {
"address": result.address,
"chain": result.chain,
"risk_score": result.risk_score,
"risk_level": result.risk_level,
"flags": result.flags,
"source": result.source,
"last_updated": result.last_updated,
}
def check_sanctions(address: str) -> list[SanctionsMatch]:
"""Check OFAC sanctions for address."""
return _local_blocklist.get_matches(address)
def add_to_blocklist(address: str, reason: str = "") -> bool:
"""Add address to local blocklist."""
_local_blocklist.local_blocklist.add(address.lower())
return _save_local_list()
def _save_local_list() -> bool:
"""Save local blocklist to file."""
data = {
"addresses": list(_local_blocklist.local_blocklist),
"whitelist": list(_local_blocklist.local_whitelist),
"updated": datetime.utcnow().isoformat(),
}
try:
os.makedirs(os.path.dirname(LOCAL_BLOCKLIST_PATH), exist_ok=True)
with open(LOCAL_BLOCKLIST_PATH, "w") as f:
json.dump(data, f, indent=2)
return True
except Exception as e:
logger.error(f"Failed to save blocklist: {e}")
return False