Files

202 lines
7.8 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""
Validation script — continuously fetches RSS feeds + ransomware.live victims,
classifies articles, appends everything to results.jsonl.
Runs independently from the bot using its own seen DBs.
Usage:
python3 validation/run_validation.py
Ctrl+C to stop
"""
import asyncio
import aiohttp
import json
import os
import sys
from datetime import datetime, timezone, timedelta
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
# Load .env from project root
env_file = ROOT / ".env"
if env_file.exists():
with open(env_file) as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1)
os.environ.setdefault(k.strip(), v.strip().strip("\"'"))
from rss_manager import RSSFeedManager
from content_classifier import ContentClassifier
from ransomware_fetcher import RansomwareFetcher
from bot_config import CATEGORY_FEEDS as CATEGORY_CONFIG, POLL_INTERVAL_SECONDS as POLL_INTERVAL, SEEN_RETENTION_DAYS
OUTPUT_FILE = Path(__file__).parent / "results.jsonl"
SEEN_FILE = str(Path(__file__).parent / "seen_validation.db")
SEEN_VICTIMS_FILE = str(Path(__file__).parent / "seen_validation_victims.db")
def save_record(f, record: dict):
f.write(json.dumps(record) + "\n")
flags = []
if record.get("cves"): flags.append(f"CVEs: {', '.join(record['cves'][:3])}")
if record.get("threat_actors"): flags.append(f"Actors: {', '.join(record['threat_actors'][:2])}")
if record.get("malware_families"): flags.append(f"Malware: {', '.join(record['malware_families'][:2])}")
flag_str = f" → {' | '.join(flags)}" if flags else ""
category = record.get("category", "?").upper()
print(f" [{category:12s}] {record.get('title','')[:70]}{flag_str}")
async def poll_rss(classifier: ContentClassifier) -> int:
all_articles = []
for category, feeds_file in CATEGORY_CONFIG.items():
feeds_path = str(ROOT / feeds_file)
async with RSSFeedManager(feeds_file=feeds_path, seen_file=SEEN_FILE, feed_type=category) as manager:
articles = await manager.fetch_all_feeds(initial_run=False)
all_articles.extend(articles)
if not all_articles:
return 0
unique = []
seen_urls, seen_content = set(), set()
for article in all_articles:
fp = RSSFeedManager.get_article_fingerprints(article)
url_key, content_key = fp["url_key"], fp["content_key"]
if (url_key and url_key in seen_urls) or content_key in seen_content:
continue
if url_key:
seen_urls.add(url_key)
seen_content.add(content_key)
unique.append(article)
with open(OUTPUT_FILE, "a") as f:
for article in unique:
classified = classifier.classify_article(article)
RSSFeedManager.mark_article_as_sent(classified, seen_file=SEEN_FILE)
record = {
"run_at": datetime.now(timezone.utc).isoformat(),
"type": "article",
"title": classified.get("title"),
"source": classified.get("source"),
"url": classified.get("url"),
"published": classified.get("published_human"),
"category": classified.get("category"),
"cves": classified.get("cves", []),
"threat_actors": classified.get("threat_actors", []),
"malware_families": classified.get("malware_families", []),
"mitre_techniques": classified.get("mitre_techniques", []),
"description": classified.get("description", ""),
}
save_record(f, record)
return len(unique)
async def poll_victims(fetcher: RansomwareFetcher) -> int:
async with aiohttp.ClientSession() as session:
new_victims = await fetcher.fetch_new_victims(session)
if not new_victims:
return 0
with open(OUTPUT_FILE, "a") as f:
for victim in new_victims:
article = RansomwareFetcher.to_article(victim)
fetcher.mark_seen(victim.get("id", ""))
record = {
"run_at": datetime.now(timezone.utc).isoformat(),
"type": "victim",
"title": article.get("title"),
"source": "ransomware.live",
"url": article.get("url"),
"published": article.get("published_human"),
"category": article.get("category"),
"cves": [],
"threat_actors": [],
"malware_families": article.get("malware_families", []),
"mitre_techniques": [],
"description": article.get("description", ""),
}
save_record(f, record)
return len(new_victims)
def prune_results_file():
"""Keep results.jsonl bounded to the same window dedup actually needs — this is a
testing aid, not an archive, so nothing here needs to outlive SEEN_RETENTION_DAYS."""
if not OUTPUT_FILE.exists():
return
cutoff = (datetime.now(timezone.utc) - timedelta(days=SEEN_RETENTION_DAYS)).isoformat()
kept = []
dropped = 0
with open(OUTPUT_FILE) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
record = json.loads(line)
except json.JSONDecodeError:
continue
if record.get("run_at", "") >= cutoff:
kept.append(line)
else:
dropped += 1
if dropped:
with open(OUTPUT_FILE, "w") as f:
f.write("\n".join(kept) + ("\n" if kept else ""))
print(f" Pruned {dropped} record(s) older than {SEEN_RETENTION_DAYS} days")
async def main():
classifier = ContentClassifier()
ransomware_key = os.getenv("RANSOMWARE_LIVE_API_KEY", "")
fetcher = RansomwareFetcher(ransomware_key, seen_file=SEEN_VICTIMS_FILE) if ransomware_key else None
print(f"Validation monitor started — polling every {POLL_INTERVAL}s. Ctrl+C to stop.")
print(f"RSS feeds: {'enabled' if CATEGORY_CONFIG else 'disabled'}")
print(f"ransomware.live: {'enabled' if fetcher else 'disabled (no API key)'}")
print(f"Output: {OUTPUT_FILE}\n")
# Initial scan — mark everything currently visible as seen
print(f"[{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}] Initial scan...")
for category, feeds_file in CATEGORY_CONFIG.items():
feeds_path = str(ROOT / feeds_file)
async with RSSFeedManager(feeds_file=feeds_path, seen_file=SEEN_FILE, feed_type=category) as manager:
await manager.fetch_all_feeds(initial_run=True)
if fetcher:
async with aiohttp.ClientSession() as session:
await fetcher.fetch_new_victims(session, initial_run=True)
print(" Done. Watching for new items from now on.\n")
while True:
await asyncio.sleep(POLL_INTERVAL)
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
print(f"[{ts}] Polling...")
try:
rss_count = await poll_rss(classifier)
victim_count = await poll_victims(fetcher) if fetcher else 0
total = rss_count + victim_count
if total:
print(f" Saved {rss_count} articles + {victim_count} victims")
else:
print(f" Nothing new")
prune_results_file()
print()
except Exception as e:
print(f" Error: {e}\n")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nStopped.")