| |
| """ |
| Ingest CryptoScamDB blacklist data into RAG known_scams collection. |
| Source: https://github.com/CryptoScamDB/blacklist |
| """ |
|
|
| import asyncio |
| import hashlib |
| import logging |
| import os |
| import sys |
|
|
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s") |
| logger = logging.getLogger("ingest_cryptoscamdb_blacklist") |
|
|
| RAG_API = "http://localhost:8000/api/v1/rag/ingest" |
| COLLECTION = "known_scams" |
| DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data") |
|
|
|
|
| def make_doc_id(address: str, source: str) -> str: |
| return hashlib.sha256(f"known_scams:{address.lower()}:{source}".encode()).hexdigest()[:16] |
|
|
|
|
| async def ingest_via_api(content: str, metadata: dict) -> bool: |
| try: |
| import httpx |
|
|
| async with httpx.AsyncClient(timeout=15) as client: |
| resp = await client.post(RAG_API, json={"collection": COLLECTION, "content": content, "metadata": metadata}) |
| return resp.status_code == 200 |
| except Exception: |
| return False |
|
|
|
|
| async def ingest_yaml(): |
| """Parse the YAML blacklist and ingest entries with addresses.""" |
| yaml_path = os.path.join(DATA_DIR, "cryptoscamdb_urls.yaml") |
| if not os.path.exists(yaml_path): |
| logger.error(f"YAML file not found: {yaml_path}") |
| return {"ingested": 0, "no_address": 0} |
|
|
| |
| entries = [] |
| current = {} |
| with open(yaml_path, encoding="utf-8", errors="replace") as f: |
| for line in f: |
| stripped = line.rstrip() |
| if not stripped or stripped.startswith("#"): |
| continue |
| if stripped.startswith("- name:"): |
| if current: |
| entries.append(current) |
| current = {"name": stripped[7:].strip().strip('"')} |
| elif stripped.startswith(" url:"): |
| current["url"] = stripped[6:].strip().strip('"') |
| elif stripped.startswith(" category:"): |
| current["category"] = stripped[11:].strip().strip('"') |
| elif stripped.startswith(" subcategory:"): |
| current["subcategory"] = stripped[14:].strip().strip('"') |
| elif stripped.startswith(" description:"): |
| current["description"] = stripped[14:].strip().strip('"') |
| elif stripped.startswith(" reporter:"): |
| current["reporter"] = stripped[11:].strip().strip('"') |
| elif stripped.startswith(" ETH:"): |
| current["chain"] = "ethereum" |
| elif stripped.startswith(' - "0x') or stripped.startswith(" - 0x"): |
| addr = stripped.split('"')[-2] if '"' in stripped else stripped.split("-")[-1].strip() |
| current.setdefault("addresses", []).append(addr) |
| elif stripped.startswith(" BTC:"): |
| current["chain"] = "bitcoin" |
| elif stripped.startswith(" BSC:") or stripped.startswith(" BNB:"): |
| current["chain"] = "bsc" |
| if current: |
| entries.append(current) |
|
|
| logger.info(f"Parsed {len(entries)} entries from YAML") |
|
|
| ingested = 0 |
| no_address = 0 |
|
|
| for i, entry in enumerate(entries): |
| name = entry.get("name", "") |
| category = entry.get("category", "unknown") |
| subcategory = entry.get("subcategory", "") |
| description = entry.get("description", "") |
| url = entry.get("url", "") |
| addresses = entry.get("addresses", []) |
|
|
| if not addresses: |
| |
| content = f"CryptoScamDB: {name}. Category: {category}/{subcategory}. URL: {url}. {description}" |
| metadata = { |
| "source": "cryptoscamdb", |
| "category": category.lower(), |
| "subcategory": subcategory.lower(), |
| "name": name, |
| "url": url, |
| "severity": "high" if category == "Phishing" else "medium", |
| } |
| if await ingest_via_api(content, metadata): |
| no_address += 1 |
| else: |
| for addr in addresses: |
| if not addr or len(addr) < 10: |
| continue |
| chain = entry.get("chain", "ethereum") |
| content = f"CryptoScamDB flagged address: {addr} on {chain}. Project: {name}. Category: {category}/{subcategory}. {description}" |
| metadata = { |
| "source": "cryptoscamdb", |
| "address": addr.lower(), |
| "chain": chain, |
| "category": category.lower(), |
| "subcategory": subcategory.lower(), |
| "name": name, |
| "url": url, |
| "severity": "high" if category in ("Phishing", "Scam") else "medium", |
| } |
| if await ingest_via_api(content, metadata): |
| ingested += 1 |
|
|
| if (i + 1) % 500 == 0: |
| logger.info(f"Progress: {i + 1}/{len(entries)} | addr_entries: {ingested}, url_only: {no_address}") |
| await asyncio.sleep(0.1) |
|
|
| logger.info(f"COMPLETE: addr_entries={ingested}, url_only={no_address}") |
| return {"ingested": ingested, "no_address": no_address} |
|
|
|
|
| async def main(): |
| result = await ingest_yaml() |
| logger.info(f"Final: {result}") |
|
|
|
|
| if __name__ == "__main__": |
| asyncio.run(main()) |
|
|