181 lines
6.9 KiB
Python
181 lines
6.9 KiB
Python
|
|
#!/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
|
||
|
|
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
|
||
|
|
|
||
|
|
CATEGORY_CONFIG = {
|
||
|
|
"news": "feeds/news_feeds.json",
|
||
|
|
"malware": "feeds/malware_feeds.json",
|
||
|
|
"threat_intel": "feeds/threat_intel_feeds.json",
|
||
|
|
"osint": "feeds/osint_feeds.json",
|
||
|
|
"research": "feeds/research_feeds.json",
|
||
|
|
}
|
||
|
|
|
||
|
|
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")
|
||
|
|
POLL_INTERVAL = 300
|
||
|
|
|
||
|
|
|
||
|
|
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": "malware",
|
||
|
|
"cves": [],
|
||
|
|
"threat_actors": [],
|
||
|
|
"malware_families": article.get("malware_families", []),
|
||
|
|
"mitre_techniques": [],
|
||
|
|
"description": article.get("description", ""),
|
||
|
|
}
|
||
|
|
save_record(f, record)
|
||
|
|
|
||
|
|
return len(new_victims)
|
||
|
|
|
||
|
|
|
||
|
|
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\n")
|
||
|
|
else:
|
||
|
|
print(f" Nothing new\n")
|
||
|
|
except Exception as e:
|
||
|
|
print(f" Error: {e}\n")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
try:
|
||
|
|
asyncio.run(main())
|
||
|
|
except KeyboardInterrupt:
|
||
|
|
print("\nStopped.")
|