Ransomware.live integration, 48h window, CISA feeds, validation updates, README
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
#!/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.")
|
||||
Reference in New Issue
Block a user