250 lines
7.9 KiB
Python
Executable file
250 lines
7.9 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""
|
|
Ingest rekt.news hack data into RAG known_scams collection.
|
|
|
|
Usage:
|
|
# Via HTTP API (from host or any networked client):
|
|
python3 scripts/ingest_rekt_hacks.py --api
|
|
|
|
# Direct Python import (run inside Docker container):
|
|
docker exec rmi-backend python3 scripts/ingest_rekt_hacks.py --direct
|
|
|
|
# Dry run (print what would be ingested):
|
|
python3 scripts/ingest_rekt_hacks.py --dry-run
|
|
"""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
# Add project root to path
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
DATA_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "rekt_hacks.json")
|
|
API_URL = os.getenv("RAG_API_URL", "http://localhost:8000/api/v1/rag/ingest")
|
|
COLLECTION = "known_scams"
|
|
|
|
|
|
def load_hacks():
|
|
with open(DATA_FILE) as f:
|
|
return json.load(f)
|
|
|
|
|
|
def build_doc_text(hack):
|
|
"""Build a rich text document for semantic search."""
|
|
parts = []
|
|
parts.append(f"Project: {hack.get('name')}")
|
|
if hack.get("date"):
|
|
parts.append(f"Date: {hack['date']}")
|
|
parts.append(f"Amount Lost: ${hack['amount_lost']:,.0f}")
|
|
if hack.get("chain"):
|
|
parts.append(f"Chain: {hack['chain']}")
|
|
if hack.get("attack_type"):
|
|
parts.append(f"Attack Type: {hack['attack_type']}")
|
|
if hack.get("audit_status"):
|
|
parts.append(f"Audit Status: {hack['audit_status']}")
|
|
if hack.get("description"):
|
|
parts.append(f"Description: {hack['description']}")
|
|
if hack.get("tags"):
|
|
parts.append(f"Tags: {', '.join(hack['tags'])}")
|
|
if hack.get("url"):
|
|
parts.append(f"URL: {hack['url']}")
|
|
return "\n".join(parts)
|
|
|
|
|
|
def build_metadata(hack):
|
|
"""Build metadata dict for the RAG document."""
|
|
meta = {
|
|
"source": "rekt_news",
|
|
"name": hack.get("name"),
|
|
"amount_lost": hack["amount_lost"], # Required by embed_scam_pattern
|
|
}
|
|
if hack.get("date"):
|
|
meta["date"] = hack["date"]
|
|
if hack.get("chain"):
|
|
meta["chain"] = hack["chain"]
|
|
if hack.get("attack_type"):
|
|
meta["attack_type"] = hack["attack_type"]
|
|
if hack.get("audit_status"):
|
|
meta["audit_status"] = hack["audit_status"]
|
|
if hack.get("url"):
|
|
meta["url"] = hack["url"]
|
|
if hack.get("slug"):
|
|
meta["slug"] = hack["slug"]
|
|
if hack.get("tags"):
|
|
meta["tags"] = hack["tags"]
|
|
|
|
# Severity classification
|
|
amt = hack["amount_lost"]
|
|
if amt >= 100_000_000:
|
|
meta["severity"] = "critical"
|
|
elif amt >= 10_000_000:
|
|
meta["severity"] = "high"
|
|
elif amt >= 1_000_000:
|
|
meta["severity"] = "medium"
|
|
else:
|
|
meta["severity"] = "low"
|
|
|
|
return meta
|
|
|
|
|
|
def make_doc_id(hack):
|
|
"""Generate deterministic doc ID from slug."""
|
|
slug = hack.get("slug") or hack.get("name").lower().replace(" ", "-").replace("/", "-")
|
|
# Clean slug
|
|
slug = "".join(c for c in slug if c.isalnum() or c in "-_").strip("-")
|
|
return f"rekt-{slug}"
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Method 1: HTTP API
|
|
# ─────────────────────────────────────────────────────────────
|
|
async def ingest_via_api(hacks, dry_run=False):
|
|
import urllib.request
|
|
|
|
success = 0
|
|
failed = 0
|
|
|
|
for i, hack in enumerate(hacks):
|
|
doc_id = make_doc_id(hack)
|
|
text = build_doc_text(hack)
|
|
metadata = build_metadata(hack)
|
|
|
|
payload = json.dumps(
|
|
{
|
|
"collection": COLLECTION,
|
|
"content": text,
|
|
"metadata": metadata,
|
|
"doc_id": doc_id,
|
|
}
|
|
).encode("utf-8")
|
|
|
|
if dry_run:
|
|
if i < 3:
|
|
print(f"[DRY RUN] Would ingest: {doc_id} | {hack.get('name')} | ${hack['amount_lost']:,.0f}")
|
|
continue
|
|
|
|
req = urllib.request.Request(
|
|
API_URL,
|
|
data=payload,
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST",
|
|
)
|
|
try:
|
|
resp = urllib.request.urlopen(req, timeout=30)
|
|
json.loads(resp.read())
|
|
success += 1
|
|
if (i + 1) % 20 == 0:
|
|
print(f" Progress: {i + 1}/{len(hacks)} ingested ({success} ok, {failed} fail)")
|
|
except Exception as e:
|
|
failed += 1
|
|
print(f" FAILED {doc_id}: {e}")
|
|
|
|
# Rate limit: small delay between requests
|
|
if (i + 1) % 10 == 0:
|
|
time.sleep(0.5)
|
|
|
|
return success, failed
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Method 2: Direct Python import (run inside Docker)
|
|
# ─────────────────────────────────────────────────────────────
|
|
async def ingest_direct(hacks, dry_run=False):
|
|
from app.rag_service import ingest_document
|
|
|
|
success = 0
|
|
failed = 0
|
|
|
|
for i, hack in enumerate(hacks):
|
|
doc_id = make_doc_id(hack)
|
|
text = build_doc_text(hack)
|
|
metadata = build_metadata(hack)
|
|
|
|
if dry_run:
|
|
if i < 3:
|
|
print(f"[DRY RUN] Would ingest: {doc_id} | {hack.get('name')} | ${hack['amount_lost']:,.0f}")
|
|
continue
|
|
|
|
try:
|
|
await ingest_document(
|
|
collection=COLLECTION,
|
|
content=text,
|
|
metadata=metadata,
|
|
doc_id=doc_id,
|
|
)
|
|
success += 1
|
|
if (i + 1) % 20 == 0:
|
|
print(f" Progress: {i + 1}/{len(hacks)} ingested ({success} ok, {failed} fail)")
|
|
except Exception as e:
|
|
failed += 1
|
|
print(f" FAILED {doc_id}: {e}")
|
|
|
|
# Yield control periodically
|
|
if (i + 1) % 10 == 0:
|
|
await asyncio.sleep(0.1)
|
|
|
|
return success, failed
|
|
|
|
|
|
async def main():
|
|
parser = argparse.ArgumentParser(description="Ingest rekt.news hacks into RAG")
|
|
parser.add_argument("--api", action="store_true", help="Use HTTP API (default if running from host)")
|
|
parser.add_argument(
|
|
"--direct",
|
|
action="store_true",
|
|
help="Use direct Python import (run inside Docker container)",
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Print what would be ingested without actually ingesting",
|
|
)
|
|
parser.add_argument("--limit", type=int, default=0, help="Limit number of hacks to ingest (0 = all)")
|
|
args = parser.parse_args()
|
|
|
|
hacks = load_hacks()
|
|
print(f"Loaded {len(hacks)} hacks from {DATA_FILE}")
|
|
|
|
if args.limit > 0:
|
|
hacks = hacks[: args.limit]
|
|
print(f"Limiting to first {args.limit} hacks")
|
|
|
|
# Default to API if no method specified
|
|
if not args.direct:
|
|
args.api = True
|
|
|
|
if args.dry_run:
|
|
print("\n=== DRY RUN ===")
|
|
for hack in hacks[:5]:
|
|
doc_id = make_doc_id(hack)
|
|
text = build_doc_text(hack)
|
|
metadata = build_metadata(hack)
|
|
print(f"\n--- {doc_id} ---")
|
|
print(f"Text preview: {text[:200]}...")
|
|
print(f"Metadata: {json.dumps(metadata, indent=2)[:300]}...")
|
|
if len(hacks) > 5:
|
|
print(f"\n... and {len(hacks) - 5} more")
|
|
return
|
|
|
|
method = "HTTP API" if args.api else "Direct Python"
|
|
print(f"\nIngesting {len(hacks)} hacks via {method}...")
|
|
print(f"Collection: {COLLECTION}")
|
|
print()
|
|
|
|
if args.api:
|
|
success, failed = await ingest_via_api(hacks)
|
|
else:
|
|
success, failed = await ingest_direct(hacks)
|
|
|
|
print("\n=== DONE ===")
|
|
print(f"Success: {success}")
|
|
print(f"Failed: {failed}")
|
|
print(f"Total: {len(hacks)}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|