Filter ransomware.live to LV/EE/LT, dedicated Telegram topic, harden fetcher

- New alert topic (/on_ransomware) separate from RSS malware articles,
  so a busy leak day doesn't bury other malware coverage.
- Filter victims to Latvia/Estonia/Lithuania before they ever touch the
  seen-DB — the global feed is already on ransomware.live's own site.
- Retention bumped to 14 days (bot_config.SEEN_RETENTION_DAYS).
- Add is_first_run(): lets the caller distinguish a genuinely fresh
  seen-DB from a restart of an already-running bot.
- Fix a naive-vs-aware datetime comparison that would TypeError-crash
  a poll cycle if the API ever returned a timestamp without a UTC offset.
- Drop the unused _victim_id/iocs fields from to_article()'s output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
bot
2026-08-25 12:19:44 +03:00
parent 63f0ac74bc
commit 1b9a5de9e3
+20 -5
View File
@@ -14,6 +14,8 @@ from typing import Dict, List, Optional, Tuple
from urllib.parse import urlparse
from html import escape
from bot_config import SEEN_RETENTION_DAYS, ALLOWED_RANSOMWARE_COUNTRIES
logger = logging.getLogger(__name__)
API_BASE = "https://api-pro.ransomware.live"
@@ -45,9 +47,15 @@ class RansomwareFetcher:
"SELECT 1 FROM seen_victims WHERE victim_id = ?", (victim_id,)
).fetchone() is not None
def is_first_run(self) -> bool:
"""True if no victims have ever been recorded as seen (fresh DB)."""
with sqlite3.connect(self.seen_file) as conn:
count = conn.execute("SELECT COUNT(1) FROM seen_victims").fetchone()[0]
return count == 0
def mark_seen(self, victim_id: str):
now = datetime.now(timezone.utc).isoformat()
cutoff = (datetime.now(timezone.utc) - timedelta(days=7)).isoformat()
cutoff = (datetime.now(timezone.utc) - timedelta(days=SEEN_RETENTION_DAYS)).isoformat()
with sqlite3.connect(self.seen_file) as conn:
conn.execute("DELETE FROM seen_victims WHERE seen_at < ?", (cutoff,))
conn.execute(
@@ -95,6 +103,13 @@ class RansomwareFetcher:
victims = await self._fetch_month(session, year, month)
all_victims.extend(victims)
# Only care about victims in these countries — the general global feed
# is available directly on ransomware.live's own site.
all_victims = [
v for v in all_victims
if (v.get("country") or "").strip().upper() in ALLOWED_RANSOMWARE_COUNTRIES
]
new_victims = []
for v in all_victims:
discovered_str = v.get("discovered", "")
@@ -104,6 +119,8 @@ class RansomwareFetcher:
discovered_dt = datetime.fromisoformat(discovered_str.replace("Z", "+00:00"))
except ValueError:
continue
if discovered_dt.tzinfo is None:
discovered_dt = discovered_dt.replace(tzinfo=timezone.utc)
if discovered_dt < cutoff:
continue
@@ -167,13 +184,11 @@ class RansomwareFetcher:
"url": victim.get("permalink", ""),
"published_human": published_human,
"source": "ransomware.live",
"category": "malware",
"feed_type": "malware",
"category": "ransomware",
"feed_type": "ransomware",
"thumbnail": screenshot,
"cves": [],
"threat_actors": [],
"malware_families": [group.title()],
"mitre_techniques": [],
"iocs": {},
"_victim_id": victim.get("id", ""),
}