Ransomware.live integration, 48h window, CISA feeds, validation updates, README
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ransomware.live PRO API fetcher.
|
||||
Polls for new ransomware victims and converts them to article dicts
|
||||
compatible with the existing send_alert pipeline.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import sqlite3
|
||||
import logging
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
from html import escape
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
API_BASE = "https://api-pro.ransomware.live"
|
||||
|
||||
|
||||
class RansomwareFetcher:
|
||||
|
||||
def __init__(self, api_key: str, seen_file: str = "seen_victims.db"):
|
||||
self.api_key = api_key
|
||||
self.seen_file = seen_file
|
||||
self._ensure_db()
|
||||
|
||||
def _ensure_db(self):
|
||||
with sqlite3.connect(self.seen_file) as conn:
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS seen_victims (
|
||||
victim_id TEXT PRIMARY KEY,
|
||||
seen_at TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_seen_victims_seen_at ON seen_victims (seen_at)"
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def _is_seen(self, victim_id: str) -> bool:
|
||||
with sqlite3.connect(self.seen_file) as conn:
|
||||
return conn.execute(
|
||||
"SELECT 1 FROM seen_victims WHERE victim_id = ?", (victim_id,)
|
||||
).fetchone() is not None
|
||||
|
||||
def mark_seen(self, victim_id: str):
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
cutoff = (datetime.now(timezone.utc) - timedelta(days=7)).isoformat()
|
||||
with sqlite3.connect(self.seen_file) as conn:
|
||||
conn.execute("DELETE FROM seen_victims WHERE seen_at < ?", (cutoff,))
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO seen_victims (victim_id, seen_at) VALUES (?, ?)",
|
||||
(victim_id, now)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
async def _fetch_month(
|
||||
self, session: aiohttp.ClientSession, year: int, month: int
|
||||
) -> List[Dict]:
|
||||
url = f"{API_BASE}/victims/"
|
||||
params = {"year": year, "month": f"{month:02d}"}
|
||||
headers = {"X-Api-Key": self.api_key}
|
||||
try:
|
||||
async with session.get(url, params=params, headers=headers, timeout=aiohttp.ClientTimeout(total=30)) as resp:
|
||||
if resp.status == 200:
|
||||
data = await resp.json()
|
||||
return data.get("victims", [])
|
||||
elif resp.status == 401:
|
||||
logger.error("ransomware.live: invalid API key")
|
||||
elif resp.status == 429:
|
||||
logger.warning("ransomware.live: rate limited, skipping this poll")
|
||||
else:
|
||||
logger.error(f"ransomware.live: HTTP {resp.status}")
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("ransomware.live: request timed out")
|
||||
except Exception as e:
|
||||
logger.error(f"ransomware.live: {e}")
|
||||
return []
|
||||
|
||||
async def fetch_new_victims(
|
||||
self, session: aiohttp.ClientSession, initial_run: bool = False
|
||||
) -> List[Dict]:
|
||||
now = datetime.now(timezone.utc)
|
||||
cutoff = now - timedelta(hours=48)
|
||||
|
||||
months = [(now.year, now.month)]
|
||||
if now.day <= 2:
|
||||
prev = (now.replace(day=1) - timedelta(days=1))
|
||||
months.append((prev.year, prev.month))
|
||||
|
||||
all_victims: List[Dict] = []
|
||||
for year, month in months:
|
||||
victims = await self._fetch_month(session, year, month)
|
||||
all_victims.extend(victims)
|
||||
|
||||
new_victims = []
|
||||
for v in all_victims:
|
||||
discovered_str = v.get("discovered", "")
|
||||
if not discovered_str:
|
||||
continue
|
||||
try:
|
||||
discovered_dt = datetime.fromisoformat(discovered_str.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
continue
|
||||
if discovered_dt < cutoff:
|
||||
continue
|
||||
|
||||
vid = v.get("id", "")
|
||||
if not vid:
|
||||
continue
|
||||
if self._is_seen(vid):
|
||||
continue
|
||||
|
||||
if initial_run:
|
||||
self.mark_seen(vid)
|
||||
else:
|
||||
new_victims.append(v)
|
||||
|
||||
return new_victims
|
||||
|
||||
@staticmethod
|
||||
def _extract_company(victim: Dict) -> str:
|
||||
raw = victim.get("victim", "")
|
||||
if raw and not raw.startswith("http"):
|
||||
return raw
|
||||
domain = victim.get("website", "")
|
||||
if domain:
|
||||
return domain
|
||||
if raw.startswith("http"):
|
||||
return urlparse(raw).netloc or raw
|
||||
return "Unknown"
|
||||
|
||||
@staticmethod
|
||||
def to_article(victim: Dict) -> Dict:
|
||||
company = RansomwareFetcher._extract_company(victim)
|
||||
group = victim.get("group", "unknown")
|
||||
country = victim.get("country", "")
|
||||
sector = victim.get("activity", "")
|
||||
raw_desc = victim.get("description") or ""
|
||||
|
||||
# Drop AI-generated placeholder descriptions
|
||||
description = "" if "[AI generated]" in raw_desc else raw_desc[:300]
|
||||
|
||||
meta_parts = []
|
||||
if sector: meta_parts.append(f"Sector: {sector}")
|
||||
if country: meta_parts.append(f"Country: {country}")
|
||||
full_desc = " | ".join(meta_parts)
|
||||
if description:
|
||||
full_desc = f"{full_desc}\n{description}" if full_desc else description
|
||||
|
||||
discovered = victim.get("discovered", "")
|
||||
try:
|
||||
dt = datetime.fromisoformat(discovered.replace("Z", "+00:00"))
|
||||
published_human = dt.strftime("%Y-%m-%d %H:%M UTC")
|
||||
except (ValueError, AttributeError):
|
||||
published_human = discovered[:10] if discovered else "Unknown"
|
||||
|
||||
screenshot = victim.get("screenshot") or None
|
||||
if screenshot and not screenshot.startswith("http"):
|
||||
screenshot = None
|
||||
|
||||
return {
|
||||
"title": f"{company} claimed by {group.title()}",
|
||||
"description": full_desc,
|
||||
"url": victim.get("permalink", ""),
|
||||
"published_human": published_human,
|
||||
"source": "ransomware.live",
|
||||
"category": "malware",
|
||||
"feed_type": "malware",
|
||||
"thumbnail": screenshot,
|
||||
"cves": [],
|
||||
"threat_actors": [],
|
||||
"malware_families": [group.title()],
|
||||
"mitre_techniques": [],
|
||||
"iocs": {},
|
||||
"_victim_id": victim.get("id", ""),
|
||||
}
|
||||
Reference in New Issue
Block a user