Ransomware.live integration, 48h window, CISA feeds, validation updates, README
This commit is contained in:
@@ -6,6 +6,12 @@ subscribers.json
|
||||
seen_articles.json
|
||||
seen_articles.db
|
||||
seen_articles.db-journal
|
||||
seen_victims.db
|
||||
|
||||
# Validation output
|
||||
validation/results.jsonl
|
||||
validation/seen_validation.db
|
||||
validation/seen_validation_victims.db
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# telegram-rss-bot
|
||||
|
||||
Telegram bot that monitors cybersecurity RSS feeds and delivers real-time alerts. Articles are classified by severity and quality before delivery.
|
||||
Telegram bot that monitors cybersecurity RSS feeds and ransomware.live victim data, delivering real-time alerts to Telegram topics. Articles are enriched with CVEs, threat actors, and malware families extracted from content.
|
||||
|
||||
## Project Structure
|
||||
|
||||
@@ -8,17 +8,22 @@ Telegram bot that monitors cybersecurity RSS feeds and delivers real-time alerts
|
||||
rss_telegram_bot/
|
||||
├── threat_intel_bot.py # Main bot — commands, subscriptions, alert dispatch
|
||||
├── rss_manager.py # Feed fetching, dedup, message formatting
|
||||
├── content_classifier.py # Severity/quality scoring, CVE/actor/malware extraction
|
||||
├── content_classifier.py # CVE/actor/malware extraction
|
||||
├── ransomware_fetcher.py # ransomware.live PRO API — victim feed
|
||||
├── check_feeds.py # CLI utility to check feed health
|
||||
├── feeds/
|
||||
│ ├── news_feeds.json
|
||||
│ ├── malware_feeds.json
|
||||
│ ├── threat_intel_feeds.json
|
||||
│ ├── threat_intel_feeds.json # includes CISA Advisories, Alerts, ICS
|
||||
│ ├── osint_feeds.json
|
||||
│ └── research_feeds.json
|
||||
├── .env # Not committed — BOT_TOKEN + optional filters
|
||||
├── subscribers.json # Auto-managed — chat/topic subscriptions
|
||||
└── seen_articles.db # SQLite — tracks sent articles (7-day retention)
|
||||
├── validation/
|
||||
│ ├── run_validation.py # continuous validation monitor (saves to results.jsonl)
|
||||
│ └── review.py # pretty-print and filter results.jsonl
|
||||
├── .env # not committed — see Setup
|
||||
├── subscribers.json # auto-managed — chat/topic subscriptions
|
||||
├── seen_articles.db # SQLite — tracks sent RSS articles (7-day retention)
|
||||
└── seen_victims.db # SQLite — tracks sent ransomware victims (7-day retention)
|
||||
```
|
||||
|
||||
## Setup
|
||||
@@ -31,6 +36,7 @@ Create `.env`:
|
||||
|
||||
```env
|
||||
BOT_TOKEN=your_token_here
|
||||
RANSOMWARE_LIVE_API_KEY=your_key_here # optional — register at api-pro.ransomware.live
|
||||
```
|
||||
|
||||
Run:
|
||||
@@ -52,23 +58,9 @@ Categories: `news`, `malware`, `threat_intel`, `osint`, `research`
|
||||
|
||||
**Telegram Topics:** Run `/on_<category>` inside each topic to route categories to separate threads.
|
||||
|
||||
## Feed Management
|
||||
|
||||
Each `feeds/*.json` file follows this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"category_name": {
|
||||
"Feed Display Name": "https://example.com/rss.xml"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add or remove feeds by editing the JSON, then restart the bot. Run `python3 check_feeds.py` to verify feed health before deploying.
|
||||
|
||||
## Message Format
|
||||
|
||||
Each alert follows this structure:
|
||||
RSS articles:
|
||||
|
||||
```
|
||||
{emoji} Title
|
||||
@@ -83,19 +75,69 @@ Description
|
||||
🔗 Read Full Article
|
||||
```
|
||||
|
||||
The enrichment lines (CVEs, actors, malware) only appear when the classifier finds something — clean articles stay minimal.
|
||||
Ransomware victims (posted to the malware topic):
|
||||
|
||||
```
|
||||
🦠 company.com claimed by LockBit
|
||||
|
||||
Sector: Finance | Country: US
|
||||
Data description from leak post
|
||||
|
||||
📡 ransomware.live · Published (UTC)
|
||||
🦠 Lockbit
|
||||
|
||||
🔗 View on ransomware.live
|
||||
```
|
||||
|
||||
Enrichment lines only appear when the classifier finds something — clean articles stay minimal.
|
||||
|
||||
## Data Sources
|
||||
|
||||
### RSS Feeds (64 feeds across 5 categories)
|
||||
|
||||
| Category | Sources |
|
||||
|---|---|
|
||||
| `news` | Krebs, Bleeping Computer, The Hacker News, Security Week, Dark Reading, The Record, Cyberscoop, NCSC UK, and others |
|
||||
| `threat_intel` | CISA Advisories, CISA Alerts, CISA ICS, SANS ISC, Unit42, Mandiant, Cisco Talos, Microsoft MSRC, and others |
|
||||
| `malware` | Malware Traffic Analysis, Kaspersky Securelist, Security Affairs, Sekoia, and others |
|
||||
| `osint` | DataBreaches.net, Have I Been Pwned, UpGuard, Vulmon Research |
|
||||
| `research` | Google Project Zero, watchTowr Labs, SpecterOps, Synacktiv, Doyensec, NCC Group, and others |
|
||||
|
||||
Add or remove feeds by editing the relevant `feeds/*.json` file, then restart the bot. Run `python3 check_feeds.py` to verify health before deploying.
|
||||
|
||||
### ransomware.live PRO API
|
||||
|
||||
When `RANSOMWARE_LIVE_API_KEY` is set, the bot polls the PRO API every 5 minutes for newly discovered ransomware victims. New entries are posted to the `malware` topic. Victims are tracked in `seen_victims.db` with 7-day retention.
|
||||
|
||||
## Classification
|
||||
|
||||
`content_classifier.py` extracts the following from article title + description:
|
||||
|
||||
**CVEs** — standard `CVE-YYYY-NNNNN` pattern matching.
|
||||
**CVEs** — `CVE-YYYY-NNNNN` regex pattern.
|
||||
|
||||
**Threat actors** — APT groups and named adversaries: APT28/29/40/41, Lazarus, Sandworm, Volt Typhoon, Scattered Spider, FIN7, and others defined in `THREAT_ACTORS`.
|
||||
**Threat actors** — APT groups and named adversaries including APT28/29/40/41, Lazarus, Sandworm, Volt Typhoon, Scattered Spider, FIN7, and others. Full list in `THREAT_ACTORS`.
|
||||
|
||||
**Malware families** — ransomware, C2 frameworks, loaders, stealers, and APT tooling: LockBit, Cobalt Strike, Emotet, QakBot, Sliver, PlugX, and others defined in `MALWARE_FAMILIES`. To add a family, append its display name to the set — matching is case-insensitive with word-boundary checking to avoid false positives.
|
||||
**Malware families** — ransomware, C2 frameworks, loaders, stealers, APT tooling: LockBit, Cobalt Strike, Emotet, QakBot, Sliver, PlugX, and others. Full list in `MALWARE_FAMILIES`. Matching is case-insensitive with word-boundary checking. To add a family, append its display name to the set.
|
||||
|
||||
Severity and quality scoring were intentionally removed — the extracted enrichment fields (CVEs, actors, malware) give the reader enough context to judge importance themselves.
|
||||
Severity and quality scoring are intentionally absent — the enrichment fields give enough context for the reader to judge importance.
|
||||
|
||||
## Validation
|
||||
|
||||
A separate monitor for testing without affecting the production bot:
|
||||
|
||||
```bash
|
||||
# runs continuously, same poll cycle as the bot
|
||||
python3 validation/run_validation.py
|
||||
|
||||
# review collected results
|
||||
python3 validation/review.py # all results
|
||||
python3 validation/review.py --category research # filter by category
|
||||
python3 validation/review.py --has cves # only articles with CVEs
|
||||
python3 validation/review.py --has malware # only articles with malware hits
|
||||
python3 validation/review.py --today # only today's run
|
||||
```
|
||||
|
||||
Results are saved to `validation/results.jsonl`. Each line is a JSON object with a `type` field (`article` or `victim`) so RSS articles and ransomware victims can be filtered separately.
|
||||
|
||||
## Systemd Service (VPS)
|
||||
|
||||
@@ -108,10 +150,10 @@ After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=your_user
|
||||
WorkingDirectory=/home/your_user/rss_telegram_bot
|
||||
EnvironmentFile=/home/your_user/rss_telegram_bot/.env
|
||||
ExecStart=/usr/bin/python3 threat_intel_bot.py
|
||||
User=root
|
||||
WorkingDirectory=/root/tele-bots/rss_splited_bot
|
||||
EnvironmentFile=/root/tele-bots/rss_splited_bot/.env
|
||||
ExecStart=/root/tele-bots/rss_splited_bot/venv/bin/python3 threat_intel_bot.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
@@ -125,9 +167,18 @@ sudo systemctl enable --now rss-bot
|
||||
sudo journalctl -u rss-bot -f
|
||||
```
|
||||
|
||||
## Deploying Updates
|
||||
|
||||
From the local project directory:
|
||||
|
||||
```bash
|
||||
rsync -avz ransomware_fetcher.py threat_intel_bot.py rss_manager.py content_classifier.py feeds/threat_intel_feeds.json feeds/news_feeds.json feeds/research_feeds.json root@deployer:/root/tele-bots/rss_splited_bot/
|
||||
ssh root@deployer "systemctl restart rss-bot"
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- First run marks all current articles as seen — no flood on startup
|
||||
- Polling interval: 5 minutes (`asyncio.sleep(300)` in `threat_intel_bot.py`)
|
||||
- Only today's UTC articles are processed (strict date gate in `rss_manager.py`)
|
||||
- `seen_articles.db` auto-purges entries older than 7 days
|
||||
- First run marks all current articles and victims as seen — no flood on startup
|
||||
- Polling interval: 5 minutes
|
||||
- Articles published within the last 48 hours are processed (rolling window, not calendar day)
|
||||
- `seen_articles.db` and `seen_victims.db` auto-purge entries older than 7 days
|
||||
|
||||
@@ -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", ""),
|
||||
}
|
||||
+31
-11
@@ -24,8 +24,10 @@ from telegram.ext import (
|
||||
from telegram.constants import ParseMode
|
||||
from telegram.error import TelegramError
|
||||
|
||||
import aiohttp
|
||||
from rss_manager import RSSFeedManager
|
||||
from content_classifier import ContentClassifier
|
||||
from ransomware_fetcher import RansomwareFetcher
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
@@ -54,6 +56,8 @@ class ThreatIntelBot:
|
||||
self.classifier = ContentClassifier()
|
||||
self.application = None
|
||||
self.monitoring_task = None
|
||||
ransomware_key = os.getenv("RANSOMWARE_LIVE_API_KEY", "")
|
||||
self.ransomware_fetcher = RansomwareFetcher(ransomware_key) if ransomware_key else None
|
||||
self.load_subscribers()
|
||||
|
||||
def load_subscribers(self):
|
||||
@@ -393,19 +397,23 @@ class ThreatIntelBot:
|
||||
"""Background task to monitor RSS feeds by category."""
|
||||
logger.info("Starting RSS feed monitoring for category feeds...")
|
||||
|
||||
# First run - just mark existing articles as seen, don't send alerts
|
||||
# First run - mark existing articles and victims as seen, don't send alerts
|
||||
try:
|
||||
for category, cfg in CATEGORY_CONFIG.items():
|
||||
async with RSSFeedManager(feeds_file=cfg["feeds_file"], feed_type=category) as manager:
|
||||
logger.info(f"Initial {category.upper()} feed scan - marking existing articles as seen...")
|
||||
existing_articles = await manager.fetch_all_feeds(initial_run=True)
|
||||
logger.info(
|
||||
f"Completed initial {category.upper()} scan. "
|
||||
f"Found {len(existing_articles)} existing articles (no alerts sent)"
|
||||
)
|
||||
await manager.fetch_all_feeds(initial_run=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in initial feed scan: {e}")
|
||||
|
||||
if self.ransomware_fetcher:
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
await self.ransomware_fetcher.fetch_new_victims(session, initial_run=True)
|
||||
logger.info("Initial ransomware scan complete — existing victims marked as seen")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in initial ransomware scan: {e}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
all_articles = []
|
||||
@@ -417,7 +425,7 @@ class ThreatIntelBot:
|
||||
if category_articles:
|
||||
logger.info(f"Found {len(category_articles)} new {category.upper()} articles")
|
||||
|
||||
# Deduplicate within this polling batch (cross-feed/source duplicates).
|
||||
# Deduplicate within this polling batch
|
||||
unique_articles = []
|
||||
batch_seen_url = set()
|
||||
batch_seen_content = set()
|
||||
@@ -425,16 +433,14 @@ class ThreatIntelBot:
|
||||
fp = RSSFeedManager.get_article_fingerprints(article)
|
||||
url_key = fp['url_key']
|
||||
content_key = fp['content_key']
|
||||
|
||||
if (url_key and url_key in batch_seen_url) or (content_key in batch_seen_content):
|
||||
continue
|
||||
|
||||
if url_key:
|
||||
batch_seen_url.add(url_key)
|
||||
batch_seen_content.add(content_key)
|
||||
unique_articles.append(article)
|
||||
|
||||
# Classify and send all unique articles
|
||||
# Classify and send RSS articles
|
||||
sent_count = 0
|
||||
for article in unique_articles:
|
||||
classified = self.classifier.classify_article(article)
|
||||
@@ -448,10 +454,24 @@ class ThreatIntelBot:
|
||||
else:
|
||||
logger.info("No new articles found")
|
||||
|
||||
# Poll ransomware.live for new victims
|
||||
if self.ransomware_fetcher:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
new_victims = await self.ransomware_fetcher.fetch_new_victims(session)
|
||||
victim_count = 0
|
||||
for victim in new_victims:
|
||||
article = RansomwareFetcher.to_article(victim)
|
||||
sent = await self.send_alert(article)
|
||||
if sent:
|
||||
self.ransomware_fetcher.mark_seen(victim.get("id", ""))
|
||||
victim_count += 1
|
||||
await asyncio.sleep(1)
|
||||
if new_victims:
|
||||
logger.info(f"Ransomware victims: {len(new_victims)} new, {victim_count} delivered")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in feed monitoring: {e}")
|
||||
|
||||
# Wait 5 minutes before next check
|
||||
await asyncio.sleep(300)
|
||||
|
||||
def build_application(self):
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Review validation results — pretty-prints results.jsonl with optional filters.
|
||||
|
||||
Usage:
|
||||
python3 validation/review.py # all results
|
||||
python3 validation/review.py --category research # filter by category
|
||||
python3 validation/review.py --severity critical # filter by severity
|
||||
python3 validation/review.py --has cves # only articles with CVEs
|
||||
python3 validation/review.py --has malware # only articles with malware hits
|
||||
python3 validation/review.py --has actors # only articles with threat actors
|
||||
python3 validation/review.py --today # only today's run
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
|
||||
RESULTS_FILE = Path(__file__).parent / "results.jsonl"
|
||||
|
||||
SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "unknown": 4}
|
||||
|
||||
|
||||
def parse_args():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--category", help="Filter by category (news, malware, threat_intel, osint, research)")
|
||||
p.add_argument("--severity", help="Filter by severity (critical, high, medium, low)")
|
||||
p.add_argument("--has", choices=["cves", "malware", "actors"], help="Only show articles with these detections")
|
||||
p.add_argument("--today", action="store_true", help="Only show results from today")
|
||||
p.add_argument("--limit", type=int, default=0, help="Max articles to show (0 = all)")
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def load_results(args):
|
||||
if not RESULTS_FILE.exists():
|
||||
print("No results.jsonl found — run run_validation.py first.")
|
||||
sys.exit(0)
|
||||
|
||||
today = datetime.now(timezone.utc).date().isoformat()
|
||||
records = []
|
||||
|
||||
with open(RESULTS_FILE) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
r = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
if args.today and not r.get("run_at", "").startswith(today):
|
||||
continue
|
||||
if args.category and r.get("category") != args.category:
|
||||
continue
|
||||
if args.severity and r.get("severity") != args.severity:
|
||||
continue
|
||||
if args.has == "cves" and not r.get("cves"):
|
||||
continue
|
||||
if args.has == "malware" and not r.get("malware_families"):
|
||||
continue
|
||||
if args.has == "actors" and not r.get("threat_actors"):
|
||||
continue
|
||||
|
||||
records.append(r)
|
||||
|
||||
records.sort(key=lambda x: SEVERITY_ORDER.get(x.get("severity", "unknown"), 4))
|
||||
return records
|
||||
|
||||
|
||||
def print_record(r):
|
||||
sev = r.get("severity", "unknown").upper()
|
||||
sev_icons = {"CRITICAL": "🚨", "HIGH": "🔴", "MEDIUM": "🟠", "LOW": "🟡"}
|
||||
icon = sev_icons.get(sev, "⚪")
|
||||
|
||||
print(f"\n{'─' * 80}")
|
||||
print(f"{icon} [{r.get('category', '?').upper()}] {r.get('title', 'No title')}")
|
||||
print(f" 📡 {r.get('source')} · {r.get('published')} · Score: {r.get('quality_score', '?')}/100")
|
||||
|
||||
if r.get("cves"):
|
||||
print(f" 🆔 {', '.join(r['cves'])}")
|
||||
if r.get("threat_actors"):
|
||||
print(f" 👤 {', '.join(r['threat_actors'])}")
|
||||
if r.get("malware_families"):
|
||||
print(f" 🦠 {', '.join(r['malware_families'])}")
|
||||
if r.get("mitre_techniques"):
|
||||
print(f" 🎯 {', '.join(r['mitre_techniques'][:5])}")
|
||||
if r.get("description"):
|
||||
print(f" {r['description'][:200]}...")
|
||||
print(f" 🔗 {r.get('url', '')}")
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
records = load_results(args)
|
||||
|
||||
if not records:
|
||||
print("No records match the filters.")
|
||||
return
|
||||
|
||||
if args.limit:
|
||||
records = records[:args.limit]
|
||||
|
||||
total = len(records)
|
||||
sev_counts = {}
|
||||
for r in records:
|
||||
s = r.get("severity", "unknown")
|
||||
sev_counts[s] = sev_counts.get(s, 0) + 1
|
||||
|
||||
print(f"\n=== Validation Results ({total} articles) ===")
|
||||
print(" " + " ".join(f"{s.upper()}: {c}" for s, c in sorted(sev_counts.items(), key=lambda x: SEVERITY_ORDER.get(x[0], 4))))
|
||||
|
||||
for r in records:
|
||||
print_record(r)
|
||||
|
||||
print(f"\n{'─' * 80}")
|
||||
print(f"Total: {total} articles")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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