rmi-backend/app/_archive/legacy_2026_07/rag_historical.py
cryptorugmunch 628c1d2a10
Some checks failed
CI / build (push) Failing after 3s
refactor(rmi-backend,audit): mount Wave 3 + archive 136 dead-code files (P2.3)
PHASE 2.3 (AUDIT-2026-Q3.md):

Task 1 — Wire-in Wave 3 (1 router mounted, 2 deferred):
  - app.routers.unified_scanner_router mounted at /api/v2/scanner/* (2 routes:
    POST /api/v2/scanner/token/scan, POST /api/v2/scanner/wallet/scan).
    Refactored prefix from /api/v2 -> /api/v2/scanner to avoid future conflicts
    with the v1 /api/v1/scanner/ stub.
  - app.routers.unified_wallet_scanner DEFERRED (no router APIRouter attribute;
    library module consumed by unified_scanner_router via get_wallet_scanner()).
  - app.routers.admin_extensions DEFERRED (DORMANT per audit; 25 routes at
    /api/v1/admin/* would shadow /api/v1/admin/alerts_webhook).

Task 2 — Archive 136 dead-code files to app/_archive/legacy_2026_07/:
  - 73 routers in app/routers/ (reach graph showed zero reach into mount.py).
  - 63 flat app/*.py (domain modules never imported by live code).
  - 1 file RESTORED post-archive: app/routers/x402_bridge_health.py (caught by
    tests/unit/test_bridge_health.py which directly imports it; reach graph
    considered tests/ only as transitive reach — to be patched in next cycle).

Forced-LIVE (NOT archived per user directive):
  - app/ai_pipeline_v3.py  (3 importers in audit window, importers themselves DEAD)
  - app/splade_bm25.py       (LIVE via app.rag_service)
  - app/wallet_manager_v2.py (LIVE via x402_enforcement, x402_tools, sweep_all, sweep_now)
  - app/crypto_embeddings.py (NOT in audit ARCHIVE list; heavy import graph)

Verification (forward-import closure from mount.py + main.py + factory.py + lifespan.py):
  - imports = 348 app.* modules
  - reached = 194 files reachable from roots
  - archive set = audit_dead (186) - reached - forced_live (4) - test_live (1) = 136
  - Net delta: 136 files moved, 44,932 LOC reduction, 293->295 active routes (+2 from Wave 3)

pyproject.toml updates:
  - setuptools.packages.find: added exclude for app._archive*
  - ruff.extend-exclude: added "app/_archive/"
  - mypy.exclude: added "app/_archive/"

Smoke test: pytest tests/ — 817 passed, 3 pre-existing failures unchanged
(0 new failures; 0 routes lost; all 4 forced-LIVE files still importable).

Restoration: git mv app/_archive/legacy_2026_07/<name>.py <original-path>
and add the import to app/mount.py ROUTER_MODULES.

Refs: AUDIT-2026-Q3.md /home/dev/pry/rmi-final-deadcode-2026-07-06.md
2026-07-06 20:52:31 +02:00

415 lines
16 KiB
Python

"""
RAG Historical Scam Ingestion - Rekt DB, Chainabuse, TRM, Elliptic
==================================================================
Ingests historical crypto scam and hack data into RAG collections.
Sources: Rekt DB (3K+ DeFi hacks), Chainabuse (scam reports),
TRM Labs (annual crime report), Elliptic (scam report),
SlowMist (hacked archive), Immunefi (bug bounties), CertiK (audits).
Collections fed:
- defi_hacks: Rekt DB, SlowMist, Rekt News
- rug_timeline: Chainabuse, SENTINEL
- vuln_patterns: Immunefi, CertiK
- crime_reports: TRM, Elliptic, Chainalysis
- forensic_reports: All exploit post-mortems
Runs: weekly (Sunday 2am) for historical sources.
"""
import asyncio
import json
import logging
import os
import re
from datetime import UTC, datetime
import httpx
logger = logging.getLogger("rag.historical")
# ── Config ──
BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:8000")
RAG_INGEST_URL = f"{BACKEND_URL}/api/v1/rag/ingest"
REQUEST_TIMEOUT = 30
# ── Source Definitions ──
SOURCES = {
"rekt_db": {
"name": "Rekt Database",
"url": "https://de.fi/rekt-database",
"collection": "defi_hacks",
"content_type": "scam_report",
"description": "3,000+ DeFi hacks since 2020 - the most comprehensive exploit database",
"cadence": "weekly",
},
"chainabuse": {
"name": "Chainabuse",
"url": "https://chainabuse.com",
"collection": "rug_timeline",
"content_type": "scam_report",
"description": "Community-reported scam addresses with narratives",
"cadence": "weekly",
},
"slowmist_hacked": {
"name": "SlowMist Hacked Archive",
"url": "https://slowmist.com/en/#hacked",
"collection": "defi_hacks",
"content_type": "scam_report",
"description": "Detailed exploit analysis from SlowMist security team",
"cadence": "weekly",
},
"rekt_news": {
"name": "Rekt News",
"url": "https://rekt.news",
"collection": "defi_hacks",
"content_type": "scam_report",
"description": "In-depth DeFi exploit post-mortems",
"cadence": "weekly",
},
"immunefi": {
"name": "Immunefi Bug Bounties",
"url": "https://immunefi.com",
"collection": "vuln_patterns",
"content_type": "contract_code",
"description": "Bug bounty reports - real vulnerability patterns",
"cadence": "weekly",
},
"certik": {
"name": "CertiK Audit Findings",
"url": "https://certik.com",
"collection": "vuln_patterns",
"content_type": "contract_code",
"description": "Professional smart contract audit findings",
"cadence": "weekly",
},
"trm_crime_report": {
"name": "TRM Labs Crypto Crime Report",
"url": "https://www.trmlabs.com/reports-and-whitepapers/2026-crypto-crime-report",
"collection": "crime_reports",
"content_type": "annual_report",
"description": "Annual crypto crime typologies and trends - $158B illicit volume in 2025",
"cadence": "yearly",
},
"elliptic_scams": {
"name": "Elliptic State of Crypto Scams",
"url": "https://info.elliptic.co/hubfs/The%20state%20of%20crypto%20scams%202025/",
"collection": "crime_reports",
"content_type": "annual_report",
"description": "Annual crypto scam typologies and case studies",
"cadence": "yearly",
},
}
# ── Scraper Functions ──
async def scrape_rekt_db() -> list[dict]:
"""
Scrape Rekt DB for historical DeFi hacks.
de.fi/rekt-database lists 3,000+ exploits with:
- Project name, date, amount lost, chain, vulnerability type
- Links to post-mortems on rekt.news
"""
docs = []
try:
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
# Try the API endpoint first
resp = await client.get("https://de.fi/api/rekt")
if resp.status_code == 200:
data = resp.json()
for item in data[:500]: # Limit per run
docs.append(
{
"text": f"DeFi Hack: {item.get('name', 'Unknown')} - "
f"${item.get('amount', 0):,.0f} lost on {item.get('chain', 'Unknown')}. "
f"Vulnerability: {item.get('vulnerability', 'Unknown')}. "
f"Date: {item.get('date', 'Unknown')}. "
f"Details: {item.get('description', '')}",
"source": "rekt_db",
"url": item.get("url", "https://de.fi/rekt-database"),
"category": "defi_hack",
"date": item.get("date", ""),
"chain": item.get("chain", ""),
"tags": [
item.get("vulnerability", ""),
item.get("chain", ""),
"defi_hack",
],
}
)
except Exception as e:
logger.warning(f"Rekt DB scrape failed: {e}")
logger.info(f"Rekt DB: {len(docs)} hacks scraped")
return docs
async def scrape_chainabuse() -> list[dict]:
"""
Scrape Chainabuse for scam reports with addresses.
chainabuse.com has community-reported scams with:
- Address, chain, scam type, description, reporter
"""
docs = []
try:
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
# Chainabuse has a public reports page
resp = await client.get(
"https://chainabuse.com/api/reports",
params={"limit": 200, "sort": "newest"},
)
if resp.status_code == 200:
data = resp.json()
for item in data.get("reports", []):
address = item.get("address", "")
docs.append(
{
"text": f"Scam Report: {item.get('title', 'Unknown')} - "
f"Address {address} on {item.get('chain', 'Unknown')}. "
f"Type: {item.get('scam_type', 'Unknown')}. "
f"Description: {item.get('description', '')}. "
f"Reported by: {item.get('reporter', 'Anonymous')}",
"source": "chainabuse",
"url": f"https://chainabuse.com/report/{item.get('id', '')}",
"category": "scam_report",
"date": item.get("created_at", ""),
"chain": item.get("chain", ""),
"tags": [
item.get("scam_type", ""),
item.get("chain", ""),
"scam_report",
],
}
)
except Exception as e:
logger.warning(f"Chainabuse scrape failed: {e}")
logger.info(f"Chainabuse: {len(docs)} reports scraped")
return docs
async def scrape_slowmist() -> list[dict]:
"""Scrape SlowMist Hacked Archive for detailed exploit analysis."""
docs = []
try:
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
resp = await client.get("https://slowmist.com/en/api/hacked")
if resp.status_code == 200:
data = resp.json()
for item in data[:200]:
docs.append(
{
"text": f"SlowMist Analysis: {item.get('title', 'Unknown')} - "
f"${item.get('amount', 0):,.0f} lost. "
f"Root cause: {item.get('root_cause', 'Unknown')}. "
f"Attack vector: {item.get('attack_vector', 'Unknown')}. "
f"Recommendations: {item.get('recommendations', '')}",
"source": "slowmist",
"url": item.get("url", "https://slowmist.com"),
"category": "defi_hack",
"date": item.get("date", ""),
"chain": item.get("chain", ""),
"tags": [
item.get("root_cause", ""),
item.get("attack_vector", ""),
"defi_hack",
"slowmist",
],
}
)
except Exception as e:
logger.warning(f"SlowMist scrape failed: {e}")
logger.info(f"SlowMist: {len(docs)} analyses scraped")
return docs
async def scrape_rekt_news() -> list[dict]:
"""Scrape Rekt News for in-depth exploit post-mortems."""
docs = []
try:
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
# Rekt News has an RSS feed
resp = await client.get("https://rekt.news/feed.xml")
if resp.status_code == 200:
import defusedxml.ElementTree as ET
root = ET.fromstring(resp.text)
for item in root.findall(".//item")[:50]:
title = item.findtext("title", "")
description = item.findtext("description", "")
link = item.findtext("link", "")
pub_date = item.findtext("pubDate", "")
# Extract amount from title (e.g., "$10M Rekt")
amount_match = re.search(r"\$(\d+(?:\.\d+)?[MB]?)", title)
amount = amount_match.group(0) if amount_match else "Unknown"
docs.append(
{
"text": f"Rekt News: {title} - {amount} lost. {description}",
"source": "rekt_news",
"url": link,
"category": "defi_hack",
"date": pub_date,
"tags": ["defi_hack", "rekt_news", "post_mortem"],
}
)
except Exception as e:
logger.warning(f"Rekt News scrape failed: {e}")
logger.info(f"Rekt News: {len(docs)} articles scraped")
return docs
async def ingest_trm_report() -> list[dict]:
"""
Ingest TRM Labs 2026 Crypto Crime Report.
Key findings: $158B illicit volume, $2.87B stolen in 150 hacks,
Russia-linked sanctions evasion via A7A5 stablecoin.
"""
docs = []
try:
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
resp = await client.get("https://www.trmlabs.com/reports-and-whitepapers/2026-crypto-crime-report")
if resp.status_code == 200:
# Extract key findings from the page
text = resp.text
# Parse key sections
sections = re.split(r"<h[23][^>]*>", text)
for section in sections:
# Strip HTML tags
clean = re.sub(r"<[^>]+>", " ", section)
clean = re.sub(r"\s+", " ", clean).strip()
if len(clean) > 100:
docs.append(
{
"text": f"TRM 2026 Crypto Crime Report: {clean[:2000]}",
"source": "trm_labs",
"url": "https://www.trmlabs.com/reports-and-whitepapers/2026-crypto-crime-report",
"category": "crime_report",
"date": "2026-01-28",
"tags": [
"crime_report",
"trm_labs",
"annual_2026",
"sanctions",
"hacks",
],
}
)
except Exception as e:
logger.warning(f"TRM report ingest failed: {e}")
logger.info(f"TRM Report: {len(docs)} sections ingested")
return docs
# ── Master Ingestion Orchestrator ──
async def ingest_historical_source(source_id: str) -> dict:
"""Ingest a single historical source into RAG."""
source = SOURCES.get(source_id)
if not source:
return {"error": f"Unknown source: {source_id}"}
logger.info(f"Ingesting {source['name']}{source['collection']}")
# Scrape
scrapers = {
"rekt_db": scrape_rekt_db,
"chainabuse": scrape_chainabuse,
"slowmist_hacked": scrape_slowmist,
"rekt_news": scrape_rekt_news,
"trm_crime_report": ingest_trm_report,
}
scraper = scrapers.get(source_id)
if not scraper:
return {"error": f"No scraper for {source_id}", "source": source_id}
try:
docs = await scraper()
except Exception as e:
return {"error": str(e), "source": source_id, "docs_scraped": 0}
if not docs:
return {"source": source_id, "docs_scraped": 0, "status": "empty"}
# Chunk and dedup
from app.rag_chunking import chunk_batch
chunks = chunk_batch(docs, source["content_type"])
# Ingest into backend
ingested = 0
try:
async with httpx.AsyncClient(timeout=60) as client:
# Batch in groups of 50
for i in range(0, len(chunks), 50):
batch = chunks[i : i + 50]
resp = await client.post(
RAG_INGEST_URL,
json={
"documents": batch,
"collection": source["collection"],
"source": source_id,
"chunking": source["content_type"],
},
)
if resp.status_code == 200:
result = resp.json()
ingested += result.get("ingested", len(batch))
else:
logger.warning(f"Ingest batch failed for {source_id}: HTTP {resp.status_code}")
except Exception as e:
logger.error(f"Ingest HTTP failed for {source_id}: {e}")
return {
"source": source_id,
"name": source["name"],
"collection": source["collection"],
"docs_scraped": len(docs),
"chunks_created": len(chunks),
"ingested": ingested,
"status": "ok" if ingested > 0 else "partial",
}
async def ingest_all_historical() -> dict:
"""Run full historical ingestion across all sources."""
results = {}
total_ingested = 0
for source_id in SOURCES:
logger.info(f"Starting historical ingest: {source_id}")
try:
result = await ingest_historical_source(source_id)
results[source_id] = result
total_ingested += result.get("ingested", 0)
except Exception as e:
results[source_id] = {"error": str(e), "source": source_id}
logger.error(f"Historical ingest failed for {source_id}: {e}")
return {
"status": "complete",
"total_ingested": total_ingested,
"sources": results,
"timestamp": datetime.now(UTC).isoformat(),
}
# ── CLI Entry Point ──
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
source = sys.argv[1]
result = asyncio.run(ingest_historical_source(source))
print(json.dumps(result, indent=2))
else:
result = asyncio.run(ingest_all_historical())
print(json.dumps(result, indent=2))