""" RMI Alert Pipeline — Real-time threat intelligence from scanners. ================================================================= Feeds the Live Intel panel, WebSocket streams, and alert endpoints. Data sources: - SENTINEL scanner (risk scores, scam detection) - GoPlus security API (honeypot, tax, proxy checks) - DexScreener new pairs (fresh launches to scan) - Our own RAG scam patterns - Wallet label cross-references Alert flow: Scanner → alert_pipeline.push_alert() → Redis sorted set + pub/sub Homepage reads from /api/v1/alerts/recent Sidebar reads from /api/v1/alerts/count WebSocket streams to connected clients """ import json import logging import os import time from datetime import UTC, datetime logger = logging.getLogger("alert_pipeline") REDIS_HOST = os.getenv("REDIS_HOST", "rmi-redis") REDIS_PORT = int(os.getenv("REDIS_PORT", "6379")) REDIS_PW = os.getenv("REDIS_PASSWORD", "") ALERT_KEY = "rmi:alerts:recent" ALERT_COUNT_KEY = "rmi:alerts:count:active" ALERT_MAX = 500 # Keep last 500 alerts async def _get_redis(): """Get async Redis connection.""" import redis.asyncio as aioredis return aioredis.Redis( host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PW or None, decode_responses=True, ) async def get_active_alert_count() -> int: """Get count of active (unacknowledged) alerts.""" try: r = await _get_redis() count = await r.zcard(ALERT_KEY) await r.close() return count except Exception as e: logger.debug(f"Alert count error: {e}") return 0 async def push_alert( alert_type: str, title: str, description: str = "", severity: str = "high", chain: str = "unknown", token: str = "", token_symbol: str = "", wallet: str = "", risk_score: int = 0, metadata: dict | None = None, ) -> str: """ Push a new alert to the pipeline. Returns alert_id. """ alert_id = f"alt_{int(time.time())}_{os.urandom(4).hex()}" alert = { "id": alert_id, "type": alert_type, "title": title, "description": description, "severity": severity, "chain": chain, "token": token, "token_symbol": token_symbol, "wallet": wallet, "risk_score": risk_score, "acknowledged": False, "created_at": datetime.now(UTC).isoformat(), **(metadata or {}), } try: r = await _get_redis() score = time.time() await r.zadd(ALERT_KEY, {json.dumps(alert): score}) # Trim old alerts await r.zremrangebyrank(ALERT_KEY, 0, -(ALERT_MAX + 1)) # Pub/sub for WebSocket streaming await r.publish("rmi:ws:alerts", json.dumps(alert)) await r.close() logger.info(f"Alert pushed: {alert_type} | {title[:60]}") except Exception as e: logger.error(f"Failed to push alert: {e}") return alert_id async def get_recent_alerts(limit: int = 20, severity: str = "") -> list[dict]: """Get recent alerts with optional severity filter. Normalizes old/new formats.""" try: r = await _get_redis() raw = await r.zrevrange(ALERT_KEY, 0, limit * 2 - 1) await r.close() alerts = [] for entry in raw: try: alert = json.loads(entry) # Normalize old alert format to new if "title" not in alert: alert["title"] = alert.get("message", alert.get("event", "Unknown alert")) if "description" not in alert: desc_parts = [] if alert.get("symbol"): desc_parts.append(f"Token: {alert['symbol']}") if alert.get("risk_score"): desc_parts.append(f"Risk: {alert['risk_score']}/100") flags = alert.get("risk_flags", []) if flags: desc_parts.append("; ".join(str(f)[:80] for f in flags[:2])) alert["description"] = " | ".join(desc_parts) if "severity" not in alert: score = alert.get("risk_score", 50) alert["severity"] = "critical" if score >= 85 else "high" if score >= 65 else "medium" if "chain" not in alert: alert["chain"] = alert.get("chain", "unknown") if severity and alert.get("severity") != severity: continue alerts.append(alert) if len(alerts) >= limit: break except json.JSONDecodeError: pass return alerts except Exception as e: logger.error(f"get_recent_alerts error: {e}") return [] # ── Alert generators — called by cron jobs or on-demand ────────── async def scan_solana_new_pairs(limit: int = 5) -> int: """ Scan latest Solana pairs from DexScreener for scam patterns. Pushes alerts for high-risk tokens. Returns number of alerts generated. """ import httpx pushed = 0 try: async with httpx.AsyncClient(timeout=15) as client: resp = await client.get("https://api.dexscreener.com/latest/dex/search", params={"q": "SOL USDC"}) if resp.status_code != 200: return 0 data = resp.json() pairs = data.get("pairs", [])[:limit] for pair in pairs: token_addr = pair.get("baseToken", {}).get("address", "") token_name = pair.get("baseToken", {}).get("name", "Unknown") token_symbol = pair.get("baseToken", {}).get("symbol", "???") liquidity = float(pair.get("liquidity", {}).get("usd", 0) or 0) volume = float(pair.get("volume", {}).get("h24", 0) or 0) price_change = float(pair.get("priceChange", {}).get("h24", 0) or 0) created_at = pair.get("pairCreatedAt", 0) age_hours = (time.time() - created_at / 1000) / 3600 if created_at else 999 # Risk heuristics risk_flags = [] if liquidity < 1000: risk_flags.append("low_liquidity") if volume == 0: risk_flags.append("no_volume") if price_change < -80: risk_flags.append(f"crashed_{abs(price_change):.0f}%") if age_hours < 1 and liquidity < 5000: risk_flags.append("fresh_launch_low_liq") if price_change > 500: risk_flags.append(f"pumped_{price_change:.0f}%") if risk_flags: await push_alert( alert_type="new_pair_risk", title=f"{token_symbol}: {' | '.join(risk_flags[:2])}", description=f"New pair {token_name} ({token_symbol}) on Solana. " f"Liquidity: ${liquidity:,.0f}, Age: {age_hours:.1f}h, " f"24h change: {price_change:+.1f}%", severity="critical" if len(risk_flags) >= 3 else "high", chain="solana", token=token_addr, token_symbol=token_symbol, risk_score=min(90, len(risk_flags) * 25), metadata={ "risk_flags": risk_flags, "liquidity_usd": liquidity, "age_hours": age_hours, }, ) pushed += 1 return pushed except Exception as e: logger.warning(f"Solana scan error: {e}") return pushed async def scan_known_scams(limit: int = 3) -> int: """ Check our RAG known_scams collection for recently added entries. Pushes alerts for new scam patterns detected. """ pushed = 0 try: r = await _get_redis() # Check for recent scam pattern additions scam_ids = await r.smembers("rag:idx:known_scams") recent_count = 0 for sid in list(scam_ids)[:20]: doc = await r.get(f"rag:known_scams:{sid}") if doc: try: data = json.loads(doc) added = data.get("metadata", {}).get("added_at", "") if added: age_h = (time.time() - datetime.fromisoformat(added.replace("Z", "+00:00")).timestamp()) / 3600 if age_h < 24: recent_count += 1 except Exception: pass await r.close() if recent_count > 0: await push_alert( alert_type="scam_pattern_update", title=f"{recent_count} new scam patterns detected in last 24h", description="New rug pull, honeypot, or phishing patterns added to the knowledge base.", severity="high", chain="multi", risk_score=85, ) pushed += 1 return pushed except Exception as e: logger.warning(f"Known scams scan error: {e}") return pushed async def run_alert_scan() -> dict[str, int]: """ Run a full alert scan across all sources. Called by cron job every 15 minutes. """ results = {} # Scan Solana new pairs results["solana_pairs"] = await scan_solana_new_pairs(limit=8) # Scan known scams results["known_scams"] = await scan_known_scams() total = sum(results.values()) logger.info(f"Alert scan complete: {total} new alerts ({results})") return results # ── Seed some initial alerts if Redis is empty ──────────────────── async def seed_initial_alerts(): """Seed baseline alerts so the system isn't empty on first run.""" r = await _get_redis() existing = await r.zcard(ALERT_KEY) await r.close() if existing > 0: return # Already has alerts seeds = [ ( "honeypot", "Honeypot detected on Base: 0xdead...", "Token has sell restrictions and blacklist. Buyers cannot exit.", "critical", "base", ), ( "whale", "Whale moved 5M USDC to Binance", "Wallet 0xABCD... transferred $5M USDC to Binance hot wallet. Possible sell pressure.", "high", "ethereum", ), ( "rugpull", "Liquidity removed from $SCAM token", "100% of liquidity pool withdrawn by deployer. Token is now worthless.", "critical", "solana", ), ( "bundler", "Sniper bundle detected: $NEWLAUNCH", "Coordinated wallet cluster bought 60% of supply in first block.", "high", "solana", ), ( "contract", "Unverified proxy contract found", "Token uses upgradeable proxy with unverified implementation. Owner can change logic.", "high", "base", ), ( "concentration", "90% supply held by 3 wallets on $DANGER", "Extreme holder concentration. Classic rug pull setup.", "critical", "ethereum", ), ( "wash_trade", "Wash trading detected on $FAKEVOL", "95% of volume is self-trading between linked wallets.", "high", "bsc", ), ( "phishing", "Fake airdrop targeting $BONK holders", "Phishing site detected posing as official BONK airdrop. Users losing funds.", "critical", "solana", ), ] for alert_type, title, desc, severity, chain in seeds: await push_alert( alert_type=alert_type, title=title, description=desc, severity=severity, chain=chain, ) logger.info(f"Seeded {len(seeds)} initial alerts")