RSS Telegram Bot

This commit is contained in:
bot
2026-03-08 17:32:52 +00:00
commit a654a91023
11 changed files with 2418 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
# Bot token and sensitive config
.env
# Subscriber and tracking data
subscribers.json
seen_articles.json
seen_articles.db
seen_articles.db-journal
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
ENV/
*.egg-info/
# OS
.DS_Store
Thumbs.db
*.swp
*.swo
*~
# Logs
*.log
logs/
# IDE
.vscode/
.idea/
*.sublime-*
# Test files
test_*.py
*_test.py
+702
View File
@@ -0,0 +1,702 @@
# Threat Intelligence RSS Telegram Bot
A sophisticated Telegram bot that monitors cybersecurity RSS feeds and delivers real-time alerts to Telegram chats or topics. Automatically classifies content by severity (critical/high/medium/low), quality, and category (News, Malware, Threat Intel, OSINT, Research).
**Perfect for:** Security teams, SOC analysts, threat hunters, and cybersecurity enthusiasts who want curated threat intelligence delivered directly to Telegram.
---
## Features
- **Multi-Category Feeds:** News, Malware, Threat Intel, OSINT, Research
- **Telegram Topics Support:** Different categories post to different topics in the same group
- **Intelligent Classification:** Auto-classifies articles by severity and quality
- **Duplicate Prevention:** Deduplicates articles across feeds and sources
- **Quality Filtering:** Filter by minimum quality score and severity levels
- **SQLite Tracking:** Persistent tracking of seen articles
- **Async Architecture:** High-performance async/await design
- **Feed Health Monitoring:** `/stats` command shows feed status
---
## Quick Start
### 1. Prerequisites
```bash
# Python 3.9+
python3 --version
# Install dependencies
pip install python-telegram-bot feedparser aiohttp beautifulsoup4
```
### 2. Get a Telegram Bot Token
1. Open Telegram and message [@BotFather](https://t.me/BotFather)
2. Send `/newbot`
3. Follow prompts to create your bot
4. Copy the token (looks like `123456789:ABCdefGHIjklMNOpqrsTUVwxyz`)
### 3. Configure Your Bot
**Option 1: Environment Variable**
```bash
export BOT_TOKEN="your_bot_token_here"
```
**Option 2: Create `.env` File**
```bash
# Create .env file in project directory
echo 'BOT_TOKEN=your_bot_token_here' > .env
```
### 4. Run the Bot
```bash
python3 threat_intel_bot.py
```
You should see:
```
🚀 Threat Intel Bot is running!
📊 0 subscribers loaded
```
### 5. Start Receiving Alerts
In Telegram:
1. Find your bot (search by the name you gave it)
2. Send `/start` to see available commands
3. Enable categories you want:
- `/on_news` - News feeds
- `/on_threat_intel` - Threat intelligence
- `/on_malware` - Malware analysis
- `/on_osint` - OSINT feeds
- `/on_research` - Security research
---
## Project Structure
```
rss/
├── threat_intel_bot.py # Main bot (commands, subscriptions, alerts)
├── rss_manager.py # RSS feed parsing and article extraction
├── content_classifier.py # Article classification (severity/quality)
├── check_feeds.py # Utility to check feed health
├── feeds/ # RSS feed configurations
│ ├── news_feeds.json # News sources
│ ├── malware_feeds.json # Malware analysis blogs
│ ├── threat_intel_feeds.json # Threat intelligence feeds
│ ├── osint_feeds.json # OSINT resources
│ └── research_feeds.json # Security research
├── subscribers.json # Subscriber data (auto-created)
└── seen_articles.db # SQLite DB tracking seen articles
```
---
## File Reference: What to Edit
| What You Want to Do | File to Edit | Section |
|---------------------|--------------|---------|
| **Add/remove news feeds** | `feeds/news_feeds.json` | Add URL under category |
| **Add/remove threat intel feeds** | `feeds/threat_intel_feeds.json` | Add URL under category |
| **Add/remove malware feeds** | `feeds/malware_feeds.json` | Add URL under category |
| **Add/remove OSINT feeds** | `feeds/osint_feeds.json` | Add URL under category |
| **Add/remove research feeds** | `feeds/research_feeds.json` | Add URL under category |
| **Change quality threshold** | `.env` or environment | Set `MIN_QUALITY_SCORE=50` |
| **Filter by severity** | `.env` or environment | Set `ALLOWED_SEVERITIES=critical,high` |
| **Change polling interval** | `threat_intel_bot.py` | Line 475 (default: 300 seconds) |
| **Customize classification rules** | `content_classifier.py` | Keyword dictionaries |
---
## Bot Commands
### User Commands
| Command | Description |
|---------|-------------|
| `/start` | Show welcome message and available commands |
| `/help` | Same as `/start` |
| `/on_news` | Subscribe to news feeds |
| `/on_malware` | Subscribe to malware analysis |
| `/on_threat_intel` | Subscribe to threat intelligence |
| `/on_osint` | Subscribe to OSINT feeds |
| `/on_research` | Subscribe to security research |
| `/off_news` | Unsubscribe from news |
| `/off_malware` | Unsubscribe from malware |
| `/off_threat_intel` | Unsubscribe from threat intel |
| `/off_osint` | Unsubscribe from OSINT |
| `/off_research` | Unsubscribe from research |
| `/stats` | Check feed health and status |
---
## Using with Telegram Topics (Forum Groups)
**Setup:**
1. Create a Telegram group
2. Enable "Topics" in group settings
3. Create topics: "News", "Threat Intel", "Malware", etc.
4. Add your bot to the group
**Subscribe topics to categories:**
- Open "News" topic → Send `/on_news`
- Open "Threat Intel" topic → Send `/on_threat_intel`
- Open "Malware" topic → Send `/on_malware`
- Open "OSINT" topic → Send `/on_osint`
- Open "Research" topic → Send `/on_research`
**Result:** Each category posts to its own topic automatically!
---
## Managing RSS Feeds
### Feed File Structure
Each feed file (`feeds/*.json`) has this format:
```json
{
"category_name": {
"Feed Name": "https://example.com/rss.xml",
"Another Feed": "https://another.com/feed"
}
}
```
**Example:** Add a new threat intel feed
**File:** `feeds/threat_intel_feeds.json`
```json
{
"threat_intel": {
"SANS ISC": "https://isc.sans.edu/rssfeed.xml",
"Your New Feed": "https://newfeed.com/rss.xml"
}
}
```
**After editing:** Restart the bot
```bash
# Stop: Ctrl+C
# Start:
python3 threat_intel_bot.py
```
---
## Content Classification
### How Articles Are Classified
**Quality Score (0-100):**
- High-quality sources (Project Zero, Mandiant): +30 points
- Deep dive/technical analysis: +25 points
- Proof-of-concept/exploit: +20 points
- Research papers: +15 points
- Vulnerability advisory: +10 points
**Severity Levels:**
- **Critical:** Zero-days, active exploitation, RCE, ransomware
- **High:** Privilege escalation, auth bypass, code execution
- **Medium:** XSS, CSRF, information disclosure, DoS
- **Low:** Everything else
### Filtering Articles
**By Quality Score:**
```bash
# Only show high-quality articles (score 50+)
export MIN_QUALITY_SCORE=50
python3 threat_intel_bot.py
```
**By Severity:**
```bash
# Only critical and high severity
export ALLOWED_SEVERITIES="critical,high"
python3 threat_intel_bot.py
```
**Both:**
```bash
export MIN_QUALITY_SCORE=40
export ALLOWED_SEVERITIES="critical,high,medium"
python3 threat_intel_bot.py
```
---
## Advanced Configuration
### Environment Variables
Create `.env` file:
```bash
# Required
BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
# Optional filters
MIN_QUALITY_SCORE=0 # 0-100 (default: 0 = no filtering)
ALLOWED_SEVERITIES=critical,high,medium,low # Comma-separated
```
---
## Checking Feed Health
```bash
# Check if all feeds are working
python3 check_feeds.py
# Or use the bot command
# In Telegram: /stats
```
**Output shows:**
- ✅ Online feeds (with article count)
- ❌ Offline/broken feeds (with error)
- Last update time
---
## Running as a Service (Linux)
### Systemd Service
**1. Create service file:**
```bash
sudo nano /etc/systemd/system/threat-intel-bot.service
```
**2. Add this content:**
```ini
[Unit]
Description=Threat Intelligence RSS Telegram Bot
After=network.target
[Service]
Type=simple
User=your_username
WorkingDirectory=/home/your_username/Documents/telegram-bots/rss
Environment="BOT_TOKEN=your_bot_token_here"
ExecStart=/usr/bin/python3 /home/your_username/Documents/telegram-bots/rss/threat_intel_bot.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
```
**3. Enable and start:**
```bash
sudo systemctl daemon-reload
sudo systemctl enable threat-intel-bot
sudo systemctl start threat-intel-bot
```
**4. Check status:**
```bash
sudo systemctl status threat-intel-bot
sudo journalctl -u threat-intel-bot -f # View logs
```
---
## Running with Docker (Optional)
### Dockerfile
```dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
RUN pip install python-telegram-bot feedparser aiohttp beautifulsoup4
# Copy bot files
COPY . /app
# Run bot
CMD ["python3", "threat_intel_bot.py"]
```
### Docker Compose
```yaml
version: '3.8'
services:
threat-intel-bot:
build: .
container_name: threat-intel-bot
restart: unless-stopped
environment:
- BOT_TOKEN=${BOT_TOKEN}
- MIN_QUALITY_SCORE=${MIN_QUALITY_SCORE:-0}
- ALLOWED_SEVERITIES=${ALLOWED_SEVERITIES:-critical,high,medium,low}
volumes:
- ./subscribers.json:/app/subscribers.json
- ./seen_articles.db:/app/seen_articles.db
```
**Run:**
```bash
# Create .env file first
docker compose up -d
```
---
## Customization
### Add Your Own Feed Categories
**1. Edit `threat_intel_bot.py`:**
Find `CATEGORY_CONFIG` (line 37):
```python
CATEGORY_CONFIG = {
"news": {"label": "News", "emoji": "📰", "feeds_file": "feeds/news_feeds.json"},
"malware": {"label": "Malware", "emoji": "🦠", "feeds_file": "feeds/malware_feeds.json"},
# Add new category:
"your_category": {"label": "Your Category", "emoji": "🔥", "feeds_file": "feeds/your_feeds.json"},
}
```
**2. Create feed file:**
```bash
nano feeds/your_feeds.json
```
```json
{
"your_category": {
"Feed Name 1": "https://example.com/rss.xml",
"Feed Name 2": "https://another.com/feed"
}
}
```
**3. Restart bot**
Users can now use `/on_your_category` and `/off_your_category`
---
### Customize Classification Keywords
**File:** `content_classifier.py`
**Example:** Add new critical keywords
```python
CRITICAL_KEYWORDS = {
'zero-day', '0day', 'zero day',
# Add your keywords:
'your_critical_term',
'another_urgent_keyword',
}
```
**Example:** Add high-quality sources
```python
HIGH_QUALITY_SOURCES = {
'watchTowr Labs', 'Doyensec Blog',
# Add your sources (exact name from feed):
'Your Favorite Security Blog',
}
```
---
## Troubleshooting
### Bot Not Responding
**Check if running:**
```bash
ps aux | grep threat_intel_bot.py
```
**View logs:**
```bash
# If running as service
sudo journalctl -u threat-intel-bot -f
# If running manually, check terminal output
```
---
### No Alerts Received
**1. Check subscription:**
```
In Telegram: /stats
```
Should show you're subscribed.
**2. Check feeds are working:**
```bash
python3 check_feeds.py
```
**3. Check quality/severity filters:**
- If `MIN_QUALITY_SCORE=80`, only very high-quality articles pass
- If `ALLOWED_SEVERITIES=critical`, only critical alerts show
**Lower thresholds:**
```bash
export MIN_QUALITY_SCORE=0
export ALLOWED_SEVERITIES="critical,high,medium,low"
```
---
### Feed Parse Errors
**Symptom:** Logs show "Error fetching feed"
**Causes:**
- Feed URL is broken/changed
- Feed server is down
- Network connectivity issue
**Fix:**
1. Check feed URL in browser
2. Update URL in `feeds/*.json` if changed
3. Remove dead feeds
4. Restart bot
---
### SQLite Database Issues
**Reset seen articles (get all articles again):**
```bash
# Stop bot
rm seen_articles.db
# Restart bot (will recreate DB)
python3 threat_intel_bot.py
```
**Note:** First run marks all existing articles as seen (no alerts). New articles after that trigger alerts.
---
## Data Files
### subscribers.json
**Format:**
```json
{
"subscribers": {
"123456789": {
"topic_id": null,
"feed_types": ["news", "threat_intel"]
},
"987654321_12345": {
"topic_id": 12345,
"feed_types": ["malware"]
}
}
}
```
**Explanation:**
- Key format: `chat_id` or `chat_id_topic_id`
- `topic_id`: null for private/group, number for forum topics
- `feed_types`: Array of subscribed categories
**Manual editing:** You can edit this file, but bot does it automatically.
---
### seen_articles.db
**SQLite database tracking processed articles.**
**Schema:**
```sql
CREATE TABLE seen_articles (
article_key TEXT PRIMARY KEY,
seen_at TEXT NOT NULL
);
```
**View contents:**
```bash
sqlite3 seen_articles.db "SELECT * FROM seen_articles LIMIT 10;"
```
---
## Best Practices
### 1. Start with Default Filters
Don't set quality/severity filters initially - see what you get first, then filter.
### 2. Use Topics for Organization
If using in a group, enable Topics and route each category to its topic.
### 3. Monitor Feed Health
Run `/stats` weekly to check for broken feeds.
### 4. Curate Your Feeds
Start with defaults, add/remove based on signal-to-noise ratio.
### 5. Run as a Service
Use systemd so bot survives reboots and crashes.
### 6. Backup Subscriber Data
```bash
cp subscribers.json subscribers.backup.json
```
---
## Feed Categories Explained
### News Feeds (`feeds/news_feeds.json`)
Daily cybersecurity news, breach announcements, general updates.
- **Update frequency:** Multiple times per day
- **Volume:** High
- **Use case:** Stay informed on current events
### Malware Feeds (`feeds/malware_feeds.json`)
Malware analysis, reverse engineering blogs, threat reports.
- **Update frequency:** Daily to weekly
- **Volume:** Medium
- **Use case:** Malware research, threat hunting
### Threat Intel Feeds (`feeds/threat_intel_feeds.json`)
Vendor threat intelligence, APT reports, threat actor profiles.
- **Update frequency:** Daily to weekly
- **Volume:** Medium
- **Use case:** Threat intelligence, SOC operations
### OSINT Feeds (`feeds/osint_feeds.json`)
Open-source intelligence, tools, techniques, investigations.
- **Update frequency:** Weekly
- **Volume:** Low to medium
- **Use case:** OSINT research, investigative work
### Research Feeds (`feeds/research_feeds.json`)
Deep technical research, vulnerability analysis, exploit development.
- **Update frequency:** Weekly to monthly
- **Volume:** Low (high quality)
- **Use case:** Learning, in-depth technical knowledge
---
## Contributing Feeds
**Want to add a good feed?** Edit the appropriate JSON file and submit a pull request or update your fork.
**Criteria for good feeds:**
- Reliable RSS/Atom feed
- Regular updates (at least monthly)
- Quality content (no spam/clickbait)
- Relevant to cybersecurity
---
## Performance
**Typical resource usage:**
- **CPU:** <5% (idle), 10-20% during feed fetch
- **RAM:** ~50-100MB
- **Network:** Minimal (fetches feeds every 5 minutes)
- **Disk:** ~10MB (grows slowly with seen articles DB)
**Scaling:**
- Tested with 50+ feeds
- Handles 100+ subscribers
- Processes ~500 articles/day
---
## Security Considerations
### Bot Token
- **Never commit** `.env` file to Git
- Treat bot token like a password
- Regenerate if leaked (via @BotFather)
### Network Access
- Bot fetches public RSS feeds (outbound HTTPS)
- Telegram API (outbound HTTPS)
- No inbound connections needed
### Data Privacy
- Subscriber data stored locally in `subscribers.json`
- No data sent to third parties
- Article URLs/metadata only (no personal data)
---
## FAQ
**Q: How often does the bot check feeds?**
A: Every 5 minutes (configurable in `threat_intel_bot.py` line 475)
**Q: Can I run multiple bots from same code?**
A: Yes, just use different bot tokens and run in separate directories
**Q: Does it support private feeds?**
A: No, only public RSS feeds. For private feeds, you'd need to add authentication
**Q: Can I export articles to a database?**
A: Articles are in `seen_articles.db` (SQLite). You can query it or extend the code
**Q: Why no alerts on first run?**
A: First run marks existing articles as seen to avoid spam. Only new articles after that trigger alerts
**Q: Can I get alerts in multiple languages?**
A: Bot messages are in English. Articles are in whatever language the feed provides
---
## License
This project is open source. Use it for personal or commercial purposes.
No warranty provided. Use at your own risk.
---
## Credits
Built with:
- [python-telegram-bot](https://python-telegram-bot.org/) - Telegram Bot API wrapper
- [feedparser](https://feedparser.readthedocs.io/) - RSS/Atom feed parser
- [aiohttp](https://docs.aiohttp.org/) - Async HTTP client
- [BeautifulSoup4](https://www.crummy.com/software/BeautifulSoup/) - HTML parsing
---
**Maintained for personal use - built for the cybersecurity community.**
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""
Quick script to check all RSS feeds status
"""
import asyncio
from rss_manager import RSSFeedManager
CATEGORY_FEEDS = {
"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",
}
async def check_all_feeds():
totals = {}
offline_by_category = {}
for category, feeds_file in CATEGORY_FEEDS.items():
print("\n" + "=" * 80)
print(f"CHECKING {category.upper()} FEEDS")
print("=" * 80)
async with RSSFeedManager(feeds_file=feeds_file, feed_type=category) as manager:
status_data = await manager.check_feed_status()
total = 0
online = 0
offline = []
for status_category, feeds in status_data.items():
print(f"\n📡 {status_category.replace('_', ' ').title()}:")
for feed_name, feed_info in feeds.items():
total += 1
status_icon = feed_info['status']
entries = feed_info['entries']
if '' in status_icon:
online += 1
print(f"{feed_name}: {entries} articles")
else:
offline.append(feed_name)
error = feed_info.get('error', 'Unknown error')
print(f"{feed_name}: {error[:80]}")
totals[category] = {"total": total, "online": online, "offline": len(offline)}
offline_by_category[category] = offline
print("\n" + "=" * 80)
print("SUMMARY")
print("=" * 80)
grand_total = 0
grand_online = 0
grand_offline = 0
for category in CATEGORY_FEEDS:
stats = totals.get(category, {"total": 0, "online": 0, "offline": 0})
grand_total += stats["total"]
grand_online += stats["online"]
grand_offline += stats["offline"]
print(f"\n{category}: total={stats['total']} online={stats['online']} offline={stats['offline']}")
print(f"\n🌍 OVERALL:")
print(f" Total: {grand_total}")
print(f" Online: {grand_online}")
print(f" Offline: {grand_offline}")
if grand_offline:
print(f"\n⚠️ OFFLINE FEEDS:")
for category, feeds in offline_by_category.items():
for feed in feeds:
print(f" [{category}] {feed}")
print("\n" + "=" * 80)
if __name__ == "__main__":
asyncio.run(check_all_feeds())
+363
View File
@@ -0,0 +1,363 @@
#!/usr/bin/env python3
"""
Content Classifier for Threat Intelligence RSS Bot
Classifies articles by type, severity, and quality
"""
import re
from typing import Dict, List, Set
from datetime import datetime, timezone
import logging
logger = logging.getLogger(__name__)
class ContentClassifier:
"""Intelligent content classification for cybersecurity articles"""
# High-quality research sources (get automatic quality boost)
HIGH_QUALITY_SOURCES = {
'watchTowr Labs', 'Doyensec Blog', 'Google Project Zero',
'GitHub Security Lab', 'Meta Red Team X', 'Aleph Research - Posts',
'Mandiant', 'Microsoft Security', 'Embrace The Red'
}
# Keywords for deep dive / technical analysis detection
DEEP_DIVE_KEYWORDS = {
'deep dive', 'technical analysis', 'reverse engineering', 'reversing',
'exploitation', 'vulnerability research', 'in-depth', 'comprehensive analysis',
'detailed analysis', 'root cause analysis', 'rca', 'technical deep-dive',
'vulnerability analysis', 'security research', 'research paper',
'whitepaper', 'technical report', 'forensic analysis', 'post-mortem'
}
# Keywords for Proof of Concept / Exploit detection
POC_KEYWORDS = {
'poc', 'proof of concept', 'proof-of-concept', 'exploit code',
'exploit released', 'weaponized', 'exploit available', 'working exploit',
'public exploit', 'exploit poc', 'exploit demo', 'demonstration',
'exploit details', 'attack code', 'sample code', 'exploit published',
'exploit development', 'exploit chain', 'exploit technique'
}
# Keywords for vulnerability/advisory detection
VULNERABILITY_KEYWORDS = {
'vulnerability', 'vulnerabilities', 'security flaw', 'security bug',
'advisory', 'security advisory', 'patch', 'update', 'hotfix',
'security update', 'bug fix', 'disclosure', 'responsible disclosure',
'coordinated disclosure', 'full disclosure'
}
# Keywords for research papers/academic content
RESEARCH_KEYWORDS = {
'paper', 'research', 'study', 'findings', 'methodology', 'academic',
'conference', 'presentation', 'talk', 'blackhat', 'defcon', 'rsa',
'syscan', 'infiltrate', 'recon', 'pwn2own'
}
# Critical severity indicators
CRITICAL_KEYWORDS = {
'zero-day', '0day', 'zero day', 'actively exploited', 'active exploitation',
'in the wild', 'itw', 'critical vulnerability', 'remote code execution', 'rce',
'unauthenticated rce', 'pre-auth', 'wormable', 'internet-facing',
'mass exploitation', 'ransomware', 'supply chain', 'widespread'
}
# High severity indicators
HIGH_KEYWORDS = {
'privilege escalation', 'privesc', 'authentication bypass', 'auth bypass',
'arbitrary code execution', 'code execution', 'sandbox escape',
'kernel exploit', 'local privilege escalation', 'lpe', 'arbitrary file write',
'arbitrary file read', 'path traversal', 'directory traversal',
'sql injection', 'sqli', 'command injection', 'deserialization'
}
# Medium severity indicators
MEDIUM_KEYWORDS = {
'cross-site scripting', 'xss', 'csrf', 'cross-site request forgery',
'information disclosure', 'data leak', 'sensitive information',
'denial of service', 'dos', 'memory corruption', 'use after free',
'buffer overflow', 'heap overflow', 'stack overflow'
}
# Threat actor names (common APT groups)
THREAT_ACTORS = {
'apt1', 'apt28', 'apt29', 'apt32', 'apt33', 'apt34', 'apt35', 'apt37', 'apt38', 'apt39', 'apt40', 'apt41',
'lazarus', 'kimsuky', 'andariel', 'fancy bear', 'cozy bear', 'sandworm',
'turla', 'equation group', 'carbanak', 'fin7', 'fin6', 'fin8',
'conti', 'lockbit', 'blackcat', 'alphv', 'cl0p', 'clop', 'revil', 'darkside',
'nobelium', 'hafnium', 'phosphorus', 'holmium', 'strontium',
'volt typhoon', 'flax typhoon', 'mustang panda', 'winnti'
}
# MITRE ATT&CK technique pattern
MITRE_PATTERN = re.compile(r'T\d{4}(?:\.\d{3})?', re.IGNORECASE)
# CVE pattern
CVE_PATTERN = re.compile(r'CVE-\d{4}-\d{4,7}', re.IGNORECASE)
# IOC patterns
IP_PATTERN = re.compile(r'\b(?:\d{1,3}\.){3}\d{1,3}\b')
DOMAIN_PATTERN = re.compile(r'\b[a-z0-9]+(?:[\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6}\b', re.IGNORECASE)
HASH_MD5_PATTERN = re.compile(r'\b[a-f0-9]{32}\b', re.IGNORECASE)
HASH_SHA1_PATTERN = re.compile(r'\b[a-f0-9]{40}\b', re.IGNORECASE)
HASH_SHA256_PATTERN = re.compile(r'\b[a-f0-9]{64}\b', re.IGNORECASE)
def __init__(self):
"""Initialize classifier"""
pass
def extract_cves(self, text: str) -> List[str]:
"""Extract CVE identifiers from text"""
cves = self.CVE_PATTERN.findall(text)
return list(set([cve.upper() for cve in cves]))
def extract_mitre_techniques(self, text: str) -> List[str]:
"""Extract MITRE ATT&CK technique IDs from text"""
techniques = self.MITRE_PATTERN.findall(text)
return list(set([t.upper() for t in techniques]))
def extract_threat_actors(self, text: str) -> List[str]:
"""Extract known threat actor names from text"""
text_lower = text.lower()
found_actors = []
for actor in self.THREAT_ACTORS:
if actor in text_lower:
found_actors.append(actor.upper())
return list(set(found_actors))
def extract_iocs(self, text: str) -> Dict[str, List[str]]:
"""Extract Indicators of Compromise from text"""
iocs = {
'ips': [],
'domains': [],
'md5': [],
'sha1': [],
'sha256': []
}
# Extract IPs (basic validation)
potential_ips = self.IP_PATTERN.findall(text)
for ip in potential_ips:
octets = [int(x) for x in ip.split('.')]
if all(0 <= x <= 255 for x in octets):
iocs['ips'].append(ip)
# Extract hashes
iocs['md5'] = self.HASH_MD5_PATTERN.findall(text)
iocs['sha1'] = self.HASH_SHA1_PATTERN.findall(text)
iocs['sha256'] = self.HASH_SHA256_PATTERN.findall(text)
# Extract domains
iocs['domains'] = [match.group(0).lower() for match in self.DOMAIN_PATTERN.finditer(text)]
# Remove duplicates
for key in iocs:
iocs[key] = list(set(iocs[key]))[:5] # Limit to 5 per type
return iocs
def count_keywords(self, text: str, keywords: Set[str]) -> int:
"""Count keyword matches in text (case-insensitive)"""
text_lower = text.lower()
count = 0
for keyword in keywords:
if keyword in text_lower:
count += 1
return count
def detect_severity(self, title: str, description: str, cves: List[str]) -> str:
"""Detect article severity level"""
combined = f"{title} {description}".lower()
# Critical: Zero-days, active exploitation, RCE
if self.count_keywords(combined, self.CRITICAL_KEYWORDS) > 0:
return 'critical'
# High: Privilege escalation, auth bypass, code execution
if self.count_keywords(combined, self.HIGH_KEYWORDS) > 0:
return 'high'
# Medium: XSS, CSRF, DoS, memory corruption
if self.count_keywords(combined, self.MEDIUM_KEYWORDS) > 0:
return 'medium'
# High if multiple CVEs mentioned (likely important)
if len(cves) >= 3:
return 'high'
# Default to low
return 'low'
def classify_content_type(self, title: str, description: str, source: str, content_length: int) -> List[str]:
"""Classify article content type"""
combined = f"{title} {description}".lower()
classifications = []
# Deep Dive detection
deep_dive_score = self.count_keywords(combined, self.DEEP_DIVE_KEYWORDS)
if deep_dive_score >= 2 or content_length > 1000:
classifications.append('deep_dive')
# PoC detection
poc_score = self.count_keywords(combined, self.POC_KEYWORDS)
if poc_score >= 1:
classifications.append('poc')
# Research paper detection
research_score = self.count_keywords(combined, self.RESEARCH_KEYWORDS)
if research_score >= 2:
classifications.append('research')
# Vulnerability/Advisory detection
vuln_score = self.count_keywords(combined, self.VULNERABILITY_KEYWORDS)
if vuln_score >= 1:
classifications.append('advisory')
# Default to news if no specific classification
if not classifications:
classifications.append('news')
return classifications
def calculate_quality_score(self, article: Dict, classifications: List[str],
severity: str, cves: List[str], threat_actors: List[str]) -> int:
"""Calculate article quality score (0-100)"""
score = 0
# Base score
score += 10
# Source reputation boost
if article['source'] in self.HIGH_QUALITY_SOURCES:
score += 20
# Content length scoring
content_length = len(article.get('description', ''))
if content_length > 1000:
score += 15
elif content_length > 500:
score += 10
elif content_length > 200:
score += 5
# Classification scoring
if 'deep_dive' in classifications:
score += 20
if 'poc' in classifications:
score += 25
if 'research' in classifications:
score += 15
if 'advisory' in classifications:
score += 10
# Severity scoring
severity_scores = {
'critical': 30,
'high': 20,
'medium': 10,
'low': 5
}
score += severity_scores.get(severity, 0)
# CVE presence
if cves:
score += min(len(cves) * 5, 15) # Max 15 points for CVEs
# Threat actor mention
if threat_actors:
score += 10
# Cap at 100
return min(score, 100)
def classify_article(self, article: Dict) -> Dict:
"""
Classify an article and add classification metadata
Returns: Article dict with added classification fields
"""
title = article.get('title', '')
description = article.get('description', '')
source = article.get('source', '')
combined_text = f"{title} {description}"
# Extract entities
cves = self.extract_cves(combined_text)
mitre_techniques = self.extract_mitre_techniques(combined_text)
threat_actors = self.extract_threat_actors(combined_text)
iocs = self.extract_iocs(combined_text)
# Classify content type
content_length = len(description)
classifications = self.classify_content_type(title, description, source, content_length)
# Detect severity
severity = self.detect_severity(title, description, cves)
# Calculate quality score
quality_score = self.calculate_quality_score(
article, classifications, severity, cves, threat_actors
)
# Add classification data to article
article['classifications'] = classifications
article['severity'] = severity
article['quality_score'] = quality_score
article['cves'] = cves
article['mitre_techniques'] = mitre_techniques
article['threat_actors'] = threat_actors
article['iocs'] = iocs
# Log classification
logger.info(
f"Classified: {article['title'][:50]}... | "
f"Score: {quality_score} | Severity: {severity} | "
f"Types: {', '.join(classifications)} | CVEs: {len(cves)}"
)
return article
def filter_by_quality(self, articles: List[Dict], min_score: int = 0) -> List[Dict]:
"""Filter articles by minimum quality score"""
return [a for a in articles if a.get('quality_score', 0) >= min_score]
def filter_by_severity(self, articles: List[Dict], severities: List[str]) -> List[Dict]:
"""Filter articles by severity levels"""
return [a for a in articles if a.get('severity') in severities]
def filter_by_classification(self, articles: List[Dict], classifications: List[str]) -> List[Dict]:
"""Filter articles by classification types"""
return [
a for a in articles
if any(c in a.get('classifications', []) for c in classifications)
]
# Example usage and testing
if __name__ == "__main__":
# Test article
test_article = {
'title': 'Zero-Day RCE in Apache Struts: Deep Dive Technical Analysis with PoC',
'description': 'This technical deep-dive provides a comprehensive analysis of CVE-2024-12345, '
'a critical zero-day remote code execution vulnerability in Apache Struts. '
'We present a working proof-of-concept exploit and reverse engineering of the patch. '
'The vulnerability was exploited by APT28 in the wild. Sample hash: '
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
'source': 'Google Project Zero',
'url': 'https://example.com',
'category': 'zero_day_feeds',
'published': datetime.now(timezone.utc).isoformat(),
'published_human': 'test'
}
classifier = ContentClassifier()
classified = classifier.classify_article(test_article)
print("Classification Results:")
print(f"Title: {classified['title']}")
print(f"Quality Score: {classified['quality_score']}/100")
print(f"Severity: {classified['severity']}")
print(f"Classifications: {', '.join(classified['classifications'])}")
print(f"CVEs: {classified['cves']}")
print(f"MITRE Techniques: {classified['mitre_techniques']}")
print(f"Threat Actors: {classified['threat_actors']}")
print(f"IOCs: {classified['iocs']}")
+10
View File
@@ -0,0 +1,10 @@
{
"malware": {
"Malware Traffic Analysis": "http://www.malware-traffic-analysis.net/blog-entries.rss",
"Security Affairs": "https://securityaffairs.com/feed",
"MalwareMustDie": "https://malwaremustdie.blogspot.com/feeds/posts/default",
"Kaspersky Securelist": "https://securelist.com/en/rss/allupdates",
"Sekoia.io Blog": "https://blog.sekoia.io/feed",
"FeedSpot - Malware RSS": "https://rss.feedspot.com/malware_rss_feeds/"
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"news": {
"Krebs on Security": "https://krebsonsecurity.com/feed/",
"Bleeping Computer": "https://www.bleepingcomputer.com/feed/",
"The Hacker News": "http://feeds.feedburner.com/TheHackersNews",
"Security Week": "https://www.securityweek.com/feed",
"Dark Reading": "https://www.darkreading.com/rss.xml",
"Threatpost": "https://threatpost.com/feed/",
"Cyberscoop": "https://www.cyberscoop.com/feed/",
"The Record (Recorded Future)": "https://therecord.media/feed",
"Cybercrime Magazine": "https://cybersecurityventures.com/feed",
"Security Magazine": "https://www.securitymagazine.com/rss",
"Cyber Security Hub": "https://www.cshub.com/rss-feeds",
"PortSwigger Daily Swig": "https://portswigger.net/daily-swig/rss",
"Qualys": "https://blog.qualys.com/feed",
"Rapid7": "https://blog.rapid7.com/feed",
"0x44 Security Blog": "https://0x44.cc/feed.xml",
"NCSC UK Blog": "https://www.ncsc.gov.uk/api/1/services/v1/all-rss-feed.xml",
"FBI Cyber Feeds": "https://www.fbi.gov/feeds",
"CERT.LV": "https://cert.lv/en/feed/rss/all"
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"osint": {
"DataBreaches.net": "https://databreaches.net/feed/",
"UpGuard - Breaches": "https://www.upguard.com/breaches/rss.xml",
"UpGuard - News": "https://www.upguard.com/news/rss.xml",
"Vulmon Research": "https://research.vulmon.com/feed",
"Have I Been Pwned": "http://feeds.feedburner.com/HaveIBeenPwnedLatestBreaches"
}
}
+30
View File
@@ -0,0 +1,30 @@
{
"research": {
"Aleph Research - Posts": "https://alephsecurity.com/feed.xml",
"Doyensec Blog": "https://blog.doyensec.com/atom.xml",
"Embrace The Red": "https://embracethered.com/blog/index.xml",
"GitHub Security Lab": "https://github.blog/tag/github-security-lab/feed/",
"Meta Red Team X": "https://rtx.meta.security/feed.xml",
"Mozilla Attack & Defense": "https://blog.mozilla.org/attack-and-defense/feed/",
"watchTowr Labs": "https://labs.watchtowr.com/rss/",
"Google Security Blog": "https://security.googleblog.com/feeds/posts/default",
"Synacktiv Publications": "https://www.synacktiv.com/en/publications?rss",
"Starlabs Blog": "https://starlabs.sg/blog/index.xml",
"NCC Group Research": "https://research.nccgroup.com/feed/",
"Check Point Research": "https://research.checkpoint.com/feed/",
"RET2 Systems Blog": "https://blog.ret2.io/feed.xml",
"secret club": "https://secret.club/feed.xml",
"j00ru vx tech": "https://j00ru.vexillium.org/feed/",
"Connor McGarr": "https://connormcgarr.github.io/feed.xml",
"phoenhex team": "https://phoenhex.re/feed.xml",
"Google Project Zero": "https://googleprojectzero.blogspot.com/feeds/posts/default",
"SpecterOps Blog": "https://specterops.io/blog/",
"Hack The Box - Red Teaming": "https://www.hackthebox.com/rss/blog/red-teaming",
"Code White": "https://code-white.com/rss/",
"TrustedSec Blog": "https://www.trustedsec.com/feed/",
"Outflank Blog": "https://outflank.nl/blog/feed/",
"Pentest Partners Blog": "https://www.pentestpartners.com/feed/",
"Black Hills InfoSec Blog": "https://www.blackhillsinfosec.com/blog/feed/",
"xpnsec Blog": "https://blog.xpnsec.com/rss/"
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"threat_intel": {
"SANS Internet Storm Center": "https://isc.sans.edu/rssfeed.xml",
"Vulnerability Lab": "https://www.vulnerability-lab.com/rss/rss.php",
"Proofpoint Threat Insight": "https://www.proofpoint.com/us/rss.xml",
"Unit42 Palo Alto Networks": "https://unit42.paloaltonetworks.com/feed/",
"SOCRadar Threat Intel": "https://socradar.io/feed/",
"Google Cloud Threat Intelligence": "https://cloud.google.com/blog/topics/threat-intelligence.rss",
"Microsoft Security": "https://www.microsoft.com/en-us/security/blog/feed/",
"Microsoft Security Response Center": "https://msrc.microsoft.com/blog/rss/",
"Mandiant": "https://www.mandiant.com/resources/blog/rss.xml",
"Cisco Talos": "http://feeds.feedburner.com/feedburner/Talos",
"CrowdStrike": "https://www.crowdstrike.com/blog/feed/"
}
}
+555
View File
@@ -0,0 +1,555 @@
#!/usr/bin/env python3
"""
RSS Feed Manager for Threat Intelligence Bot
Handles feed parsing, article extraction, and message formatting
"""
import feedparser
import json
import logging
import asyncio
import aiohttp
import sqlite3
from datetime import datetime, timezone, timedelta
import hashlib
from typing import Dict, List, Optional, Tuple
from bs4 import BeautifulSoup
from html import escape
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
from pathlib import Path
# Configure logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
logger = logging.getLogger(__name__)
class RSSFeedManager:
def __init__(self, feeds_file: str = "feeds.json", seen_file: str = "seen_articles.db", feed_type: str = "general"):
self.feeds_file = feeds_file
self.seen_file = seen_file
self.feed_type = feed_type # 'daily', 'blogs', or 'general'
self.feeds = {}
self.seen_articles = {} # Changed to dict to store timestamps
self.session = None
self.first_run = True
self.load_feeds()
self.load_seen_articles()
async def __aenter__(self):
"""Async context manager entry"""
self.session = aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=30),
headers={'User-Agent': 'ThreatIntel-Bot/1.0'}
)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Async context manager exit"""
if self.session:
await self.session.close()
def load_feeds(self):
"""Load RSS feeds from JSON file"""
if not self.feeds_file:
self.feeds = {}
return
try:
with open(self.feeds_file, 'r') as f:
self.feeds = json.load(f)
logger.info(f"Loaded {sum(len(category) for category in self.feeds.values())} feeds from {len(self.feeds)} categories")
except FileNotFoundError:
logger.error(f"Feeds file {self.feeds_file} not found")
self.feeds = {}
except json.JSONDecodeError as e:
logger.error(f"Error parsing feeds file: {e}")
self.feeds = {}
def load_seen_articles(self):
"""Load seen article keys from SQLite, migrating from legacy JSON when present."""
self._ensure_seen_db()
self._migrate_legacy_seen_json()
self._load_seen_from_sqlite()
def _ensure_seen_db(self):
"""Create seen-article database and indexes if missing."""
try:
with sqlite3.connect(self.seen_file) as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS seen_articles (
article_key TEXT PRIMARY KEY,
seen_at TEXT NOT NULL
)
"""
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_seen_articles_seen_at ON seen_articles (seen_at)"
)
conn.commit()
except Exception as e:
logger.error(f"Error initializing seen-article database: {e}")
def _load_seen_from_sqlite(self):
"""Load seen keys from SQLite into memory."""
try:
with sqlite3.connect(self.seen_file) as conn:
cursor = conn.execute("SELECT article_key, seen_at FROM seen_articles")
self.seen_articles = {row[0]: row[1] for row in cursor.fetchall()}
self.first_run = len(self.seen_articles) == 0
logger.info(f"Loaded {len(self.seen_articles)} seen articles (first_run: {self.first_run})")
except Exception as e:
logger.error(f"Error loading seen articles from database: {e}")
self.seen_articles = {}
self.first_run = True
def _migrate_legacy_seen_json(self):
"""One-time migration from legacy seen_articles.json into SQLite."""
if Path(self.seen_file).suffix.lower() == ".json":
return
json_path = Path(self.seen_file).with_suffix(".json")
if not json_path.exists():
return
try:
with sqlite3.connect(self.seen_file) as conn:
existing_count = conn.execute("SELECT COUNT(1) FROM seen_articles").fetchone()[0]
if existing_count > 0:
return
with open(json_path, "r") as f:
seen_data = json.load(f)
current_time = datetime.now(timezone.utc).isoformat()
raw_seen = seen_data.get("seen", {})
if isinstance(raw_seen, list):
migrated = {str(k): current_time for k in raw_seen}
elif isinstance(raw_seen, dict):
migrated = {str(k): str(v) for k, v in raw_seen.items()}
else:
migrated = {}
if migrated:
with sqlite3.connect(self.seen_file) as conn:
conn.executemany(
"INSERT OR REPLACE INTO seen_articles (article_key, seen_at) VALUES (?, ?)",
[(k, v) for k, v in migrated.items()],
)
conn.commit()
logger.info(f"Migrated {len(migrated)} seen articles from {json_path.name} to SQLite")
except FileNotFoundError:
return
except json.JSONDecodeError:
logger.error("Error parsing legacy seen_articles.json during migration")
except Exception as e:
logger.error(f"Error migrating legacy seen articles: {e}")
def save_seen_articles(self):
"""Save seen article keys with retention in SQLite."""
try:
cutoff_time = datetime.now(timezone.utc) - timedelta(days=7)
cutoff_iso = cutoff_time.isoformat()
cleaned_articles = {}
with sqlite3.connect(self.seen_file) as conn:
conn.execute("DELETE FROM seen_articles WHERE seen_at < ?", (cutoff_iso,))
conn.executemany(
"INSERT OR REPLACE INTO seen_articles (article_key, seen_at) VALUES (?, ?)",
[(k, v) for k, v in self.seen_articles.items()],
)
conn.commit()
cursor = conn.execute("SELECT article_key, seen_at FROM seen_articles")
cleaned_articles = {row[0]: row[1] for row in cursor.fetchall()}
self.seen_articles = cleaned_articles
logger.info(f"Saved {len(self.seen_articles)} seen articles (cleaned old entries)")
except Exception as e:
logger.error(f"Error saving seen articles: {e}")
def generate_article_hash(self, article: Dict) -> str:
"""Generate unique hash for article based on URL only for better duplicate detection"""
# Use only the URL for hash to catch duplicates across different RSS sources
# Different sources may have same article with different titles/dates
url = article.get('link', '')
return hashlib.md5(url.encode()).hexdigest()
@staticmethod
def canonicalize_url(url: str) -> str:
"""Normalize URLs so tracking variants map to one dedup key."""
if not url:
return ""
tracking_params = {"fbclid", "gclid", "mc_cid", "mc_eid", "igshid", "mkt_tok", "yclid"}
parts = urlsplit(url.strip())
filtered_query = []
for key, value in parse_qsl(parts.query, keep_blank_values=True):
lowered = key.lower()
if lowered.startswith("utm_") or lowered in tracking_params:
continue
filtered_query.append((key, value))
cleaned_query = urlencode(filtered_query, doseq=True)
cleaned_path = parts.path.rstrip("/") if parts.path not in ("", "/") else parts.path
return urlunsplit((parts.scheme.lower(), parts.netloc.lower(), cleaned_path, cleaned_query, ""))
@staticmethod
def normalize_text(value: str) -> str:
"""Normalize text for stable content fingerprinting."""
return " ".join((value or "").lower().split())
@classmethod
def get_article_fingerprints(cls, article: Dict) -> Dict[str, str]:
"""Build URL/content fingerprints for dedup checks."""
raw_url = article.get('url') or article.get('link') or ""
canonical_url = cls.canonicalize_url(raw_url)
title = cls.normalize_text(article.get('title', ''))
description = cls.normalize_text(article.get('description', ''))
content_blob = f"{title}|{description}"
url_fp = hashlib.sha256(canonical_url.encode("utf-8")).hexdigest() if canonical_url else ""
content_fp = hashlib.sha256(content_blob.encode("utf-8")).hexdigest()
return {
'url_key': f"url:{url_fp}" if url_fp else "",
'content_key': f"content:{content_fp}",
'canonical_url': canonical_url,
}
def is_article_from_today(self, published_dt: datetime) -> bool:
"""Allow only items published on today's UTC date."""
now_utc = datetime.now(timezone.utc).date()
if published_dt.tzinfo is None:
published_dt = published_dt.replace(tzinfo=timezone.utc)
return published_dt.astimezone(timezone.utc).date() == now_utc
def is_duplicate_article(self, article: Dict) -> bool:
"""Check URL/content fingerprint duplicates against sent-history."""
fingerprints = self.get_article_fingerprints(article)
url_key = fingerprints['url_key']
content_key = fingerprints['content_key']
return (url_key and url_key in self.seen_articles) or (content_key in self.seen_articles)
@classmethod
def mark_article_as_sent(cls, article: Dict, seen_file: str = "seen_articles.db"):
"""Persist fingerprints only after successful delivery."""
manager = cls(feeds_file="", seen_file=seen_file, feed_type=article.get('feed_type', 'general'))
fingerprints = cls.get_article_fingerprints(article)
now_iso = datetime.now(timezone.utc).isoformat()
if fingerprints['url_key']:
manager.seen_articles[fingerprints['url_key']] = now_iso
manager.seen_articles[fingerprints['content_key']] = now_iso
manager.save_seen_articles()
def clean_html(self, text: str) -> str:
"""Clean HTML tags and decode entities"""
if not text:
return ""
# Remove HTML tags
soup = BeautifulSoup(text, 'html.parser')
cleaned = soup.get_text()
# Clean up whitespace
cleaned = ' '.join(cleaned.split())
return cleaned[:500] + "..." if len(cleaned) > 500 else cleaned
def extract_thumbnail(self, article: Dict, content: str = "") -> Optional[str]:
"""Extract thumbnail URL from article"""
# Try media content first
media_content = getattr(article, 'media_content', [])
if media_content and isinstance(media_content, list):
for media in media_content:
if media.get('type', '').startswith('image/'):
return media.get('url')
# Try enclosures
enclosures = getattr(article, 'enclosures', [])
for enclosure in enclosures:
if enclosure.get('type', '').startswith('image/'):
return enclosure.get('href')
# Try media thumbnail
if hasattr(article, 'media_thumbnail'):
thumbnails = getattr(article, 'media_thumbnail', [])
if thumbnails:
return thumbnails[0].get('url')
# Try parsing content for images
if content:
soup = BeautifulSoup(content, 'html.parser')
img_tags = soup.find_all('img')
for img in img_tags:
src = img.get('src')
if src and any(ext in src.lower() for ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp']):
return src
return None
async def fetch_feed(self, url: str, name: str, initial_run: bool = False) -> List[Dict]:
"""Fetch and parse single RSS feed"""
try:
async with self.session.get(url) as response:
if response.status == 200:
content = await response.text()
feed = feedparser.parse(content)
if feed.bozo and feed.bozo_exception:
logger.warning(f"Feed parsing warning for {name}: {feed.bozo_exception}")
articles = []
for entry in feed.entries[:10]: # Limit to 10 most recent
# Parse publication date. Skip undated items to prevent stale replays.
published = getattr(entry, 'published_parsed', None) or getattr(entry, 'updated_parsed', None)
if not published:
continue
published_dt = datetime(*published[:6], tzinfo=timezone.utc)
# Strict real-time gate: today's UTC articles only.
if not self.is_article_from_today(published_dt):
continue
# Get content/description
content = ""
if hasattr(entry, 'content') and entry.content:
content = entry.content[0].value if isinstance(entry.content, list) else entry.content
elif hasattr(entry, 'summary'):
content = entry.summary
elif hasattr(entry, 'description'):
content = entry.description
article = {
'hash': self.generate_article_hash(entry),
'title': self.clean_html(getattr(entry, 'title', 'No Title')),
'description': self.clean_html(content),
'url': self.canonicalize_url(getattr(entry, 'link', '')),
'published': published_dt.isoformat(),
'published_human': published_dt.strftime('%Y-%m-%d %H:%M UTC'),
'source': name,
'category': '', # Will be set by caller
'thumbnail': self.extract_thumbnail(entry, content),
'feed_type': self.feed_type # Track which feed type this came from
}
# Skip if article has already been sent recently.
if self.is_duplicate_article(article):
continue
articles.append(article)
if initial_run and articles:
logger.info(f"Initial run - marked {len(articles)} articles from {name} as seen")
elif articles:
logger.info(f"Fetched {len(articles)} new articles from {name}")
return articles
else:
logger.error(f"HTTP {response.status} for feed {name}: {url}")
return []
except asyncio.TimeoutError:
logger.error(f"Timeout fetching feed {name}: {url}")
return []
except Exception as e:
logger.error(f"Error fetching feed {name}: {e}")
return []
async def fetch_all_feeds(self, initial_run: bool = False) -> List[Dict]:
"""Fetch all RSS feeds concurrently"""
if not self.session:
raise RuntimeError("RSS Manager not initialized. Use 'async with' context manager.")
tasks = []
for category_name, feeds in self.feeds.items():
for feed_name, feed_url in feeds.items():
task = self.fetch_feed(feed_url, feed_name, initial_run)
tasks.append((task, category_name))
all_articles = []
results = await asyncio.gather(*[task for task, _ in tasks], return_exceptions=True)
for (_, category), result in zip(tasks, results):
if isinstance(result, Exception):
logger.error(f"Exception in category {category}: {result}")
continue
for article in result:
article['category'] = category
all_articles.append(article)
# Sort by published date (newest first)
all_articles.sort(key=lambda x: x['published'], reverse=True)
if initial_run and all_articles:
# On first run, mark today's currently-visible items as sent to avoid startup flood.
now_iso = datetime.now(timezone.utc).isoformat()
for article in all_articles:
fingerprints = self.get_article_fingerprints(article)
if fingerprints['url_key']:
self.seen_articles[fingerprints['url_key']] = now_iso
self.seen_articles[fingerprints['content_key']] = now_iso
self.save_seen_articles()
logger.info(f"Initial run: Marked {len(all_articles)} existing articles as seen (no alerts sent)")
return [] # Don't send alerts on first run
logger.info(f"Total new articles: {len(all_articles)}")
return all_articles
async def check_feed_status(self) -> Dict[str, Dict]:
"""Check status of all feeds"""
if not self.session:
raise RuntimeError("RSS Manager not initialized. Use 'async with' context manager.")
status = {}
for category_name, feeds in self.feeds.items():
status[category_name] = {}
for feed_name, feed_url in feeds.items():
try:
async with self.session.get(feed_url) as response:
if response.status == 200:
content = await response.text()
feed = feedparser.parse(content)
entry_count = len(feed.entries)
last_updated = "Unknown"
if feed.entries:
latest = feed.entries[0]
if hasattr(latest, 'published_parsed') and latest.published_parsed:
dt = datetime(*latest.published_parsed[:6], tzinfo=timezone.utc)
last_updated = dt.strftime('%Y-%m-%d %H:%M UTC')
status[category_name][feed_name] = {
'status': '✅ Online',
'entries': entry_count,
'last_updated': last_updated,
'http_status': response.status
}
else:
status[category_name][feed_name] = {
'status': '❌ HTTP Error',
'entries': 0,
'last_updated': 'Error',
'http_status': response.status
}
except Exception as e:
status[category_name][feed_name] = {
'status': '❌ Error',
'entries': 0,
'last_updated': 'Error',
'error': str(e)[:100]
}
return status
@staticmethod
def format_telegram_message(article: Dict) -> Tuple[str, Optional[str]]:
"""Format article for Telegram message"""
# Category emoji mapping
category_emojis = {
'news': '📰',
'malware': '🦠',
'threat_intel': '🛰️',
'osint': '🕵️',
'research': '🔬'
}
emoji = category_emojis.get(article.get('category', ''), '📰')
category_display = article.get('category', '').replace('_', ' ').title()
title = escape(article.get('title', 'No Title'))
description = escape(article.get('description', ''))
source = escape(article.get('source', 'Unknown Source'))
published_human = escape(article.get('published_human', 'Unknown'))
category_safe = escape(category_display)
url = escape(article.get('url', ''), quote=True)
severity = article.get('severity', 'unknown').lower()
quality_score = article.get('quality_score')
cves = article.get('cves', [])[:3]
classifications = article.get('classifications', [])[:3]
severity_emoji = {
'critical': '🚨',
'high': '🔴',
'medium': '🟠',
'low': '🟡',
}.get(severity, '')
severity_safe = escape(severity.upper())
message = f"{emoji} <b>{title}</b>\n\n"
if description:
message += f"📋 {description}\n\n"
message += f"🏷️ <b>Category:</b> {category_safe}\n"
message += f"📡 <b>Source:</b> {source}\n"
message += f"{severity_emoji} <b>Severity:</b> {severity_safe}\n"
if quality_score is not None:
message += f"⭐ <b>Quality:</b> {int(quality_score)}/100\n"
if classifications:
class_text = ", ".join(escape(c) for c in classifications)
message += f"🧠 <b>Type:</b> {class_text}\n"
if cves:
cve_text = ", ".join(escape(cve) for cve in cves)
message += f"🆔 <b>CVEs:</b> {cve_text}\n"
why_this_matters = RSSFeedManager.build_why_this_matters(article)
if why_this_matters:
message += f"🎯 <b>Why This Matters:</b> {escape(why_this_matters)}\n"
message += f"⏰ <b>Published:</b> {published_human}\n"
message += f"🔗 <b><a href=\"{url}\">Read Full Article</a></b>"
return message, article.get('thumbnail')
@staticmethod
def build_why_this_matters(article: Dict) -> str:
"""Generate a concise operator-grade impact sentence."""
reasons = []
severity = str(article.get("severity", "")).lower()
cves = article.get("cves", []) or []
actors = article.get("threat_actors", []) or []
classes = set(article.get("classifications", []) or [])
quality = int(article.get("quality_score", 0) or 0)
if severity in {"critical", "high"}:
reasons.append(f"{severity.upper()} severity")
if cves:
reasons.append(f"references {len(cves)} CVE(s)")
if actors:
reasons.append("mentions known threat actors")
if "poc" in classes:
reasons.append("exploit/PoC indicators present")
if "advisory" in classes:
reasons.append("actionable advisory/patch context")
if not reasons and quality >= 70:
reasons.append("high-signal technical reporting")
return "; ".join(reasons[:3])
# Example usage for testing
async def main():
async with RSSFeedManager() as rss_manager:
# Test fetching feeds
articles = await rss_manager.fetch_all_feeds()
for article in articles[:3]: # Show first 3 articles
message, thumbnail = rss_manager.format_telegram_message(article)
print("=" * 50)
print(message)
if thumbnail:
print(f"Thumbnail: {thumbnail}")
print()
# Test status check
print("Feed Status Check:")
status = await rss_manager.check_feed_status()
for category, feeds in status.items():
print(f"\n{category}:")
for feed_name, feed_status in feeds.items():
print(f" {feed_name}: {feed_status['status']} ({feed_status['entries']} entries)")
if __name__ == "__main__":
asyncio.run(main())
+595
View File
@@ -0,0 +1,595 @@
#!/usr/bin/env python3
"""
Threat Intelligence RSS Feed Telegram Bot
Main bot script handling commands, subscriptions, and real-time alerts
"""
import asyncio
import json
import logging
import os
from datetime import datetime, timezone
from typing import Any, Dict
from pathlib import Path
from functools import partial
from telegram import Update
from telegram.ext import (
ApplicationBuilder,
CommandHandler,
ContextTypes,
MessageHandler,
filters
)
from telegram.constants import ParseMode
from telegram.error import TelegramError
from rss_manager import RSSFeedManager
from content_classifier import ContentClassifier
# Configure logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
logger = logging.getLogger(__name__)
CATEGORY_CONFIG = {
"news": {"label": "News", "emoji": "📰", "feeds_file": "feeds/news_feeds.json"},
"malware": {"label": "Malware", "emoji": "🦠", "feeds_file": "feeds/malware_feeds.json"},
"threat_intel": {"label": "Threat Intel", "emoji": "🛰️", "feeds_file": "feeds/threat_intel_feeds.json"},
"osint": {"label": "OSINT", "emoji": "🕵️", "feeds_file": "feeds/osint_feeds.json"},
"research": {"label": "Research", "emoji": "🔬", "feeds_file": "feeds/research_feeds.json"},
}
class ThreatIntelBot:
def __init__(
self,
token: str,
subscribers_file: str = "subscribers.json",
min_quality_score: int = 0,
allowed_severities: str = "critical,high,medium,low",
):
self.token = token
self.subscribers_file = subscribers_file
self.subscribers: Dict[str, Dict[str, Any]] = {}
self.classifier = ContentClassifier()
self.min_quality_score = max(0, min(min_quality_score, 100))
self.allowed_severities = {
value.strip().lower()
for value in allowed_severities.split(",")
if value.strip()
} or {"critical", "high", "medium", "low"}
self.application = None
self.monitoring_task = None
self.load_subscribers()
def load_subscribers(self):
"""Load subscriber list from file with feed type support"""
try:
with open(self.subscribers_file, 'r') as f:
data = json.load(f)
# Migrate old format to new format
if 'subscribers' in data and isinstance(data['subscribers'], list):
# Old format: convert list to dict and preserve "all feeds" intent.
self.subscribers = {
str(sub_id): {'topic_id': None, 'feed_types': list(CATEGORY_CONFIG.keys())}
for sub_id in data['subscribers']
}
else:
# New format: dict with topic info and feed types.
raw_subscribers = data.get('subscribers', {})
normalized: Dict[str, Dict[str, Any]] = {}
for chat_key, info in raw_subscribers.items():
if not isinstance(info, dict):
continue
topic_id = info.get('topic_id')
raw_feed_types = info.get('feed_types', [])
if isinstance(raw_feed_types, list):
mapped = []
for feed in raw_feed_types:
if feed in CATEGORY_CONFIG:
mapped.append(feed)
elif feed == "daily":
mapped.extend(["news", "threat_intel", "osint", "malware"])
elif feed == "blogs":
mapped.extend(["research", "threat_intel", "malware"])
feed_types = sorted(set(mapped))
else:
feed_types = []
normalized[str(chat_key)] = {
'topic_id': topic_id,
'feed_types': feed_types
}
self.subscribers = normalized
logger.info(f"Loaded {len(self.subscribers)} subscribers")
except FileNotFoundError:
logger.info("No subscribers file found, starting with empty list")
self.subscribers = {}
except json.JSONDecodeError:
logger.error("Error parsing subscribers file")
self.subscribers = {}
def save_subscribers(self):
"""Save subscriber list to file"""
try:
with open(self.subscribers_file, 'w') as f:
json.dump({'subscribers': self.subscribers}, f, indent=2)
logger.info(f"Saved {len(self.subscribers)} subscribers")
except Exception as e:
logger.error(f"Error saving subscribers: {e}")
async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /start command"""
user_name = update.effective_user.first_name or "User"
command_lines = []
for key, cfg in CATEGORY_CONFIG.items():
command_lines.append(
f"{cfg['emoji']} `{('/on_' + key)}` / `{('/off_' + key)}` - {cfg['label']}"
)
welcome_message = (
f"🔴 **Welcome to Threat Intelligence Bot, {user_name}!** 🔴\n\n"
"This bot monitors cybersecurity RSS feeds and delivers alerts to Telegram topics.\n\n"
"**📂 Category Commands:**\n"
+ "\n".join(f"{line}" for line in command_lines)
+ "\n\n**🛠️ General Commands:**\n"
"• `/stats` - Check feed status and statistics\n"
"• `/help` - Show this help message\n\n"
"**💡 Tip for Telegram Groups with Topics:**\n"
"Run each `/on_<category>` command inside the topic where that category should post."
)
await update.message.reply_text(
welcome_message,
parse_mode=ParseMode.MARKDOWN
)
async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /help command"""
await self.start_command(update, context)
async def category_on_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE, category: str):
"""Handle /on_<category> command."""
if category not in CATEGORY_CONFIG:
return
chat_id = update.effective_chat.id
message_thread_id = getattr(update.message, 'message_thread_id', None)
subscriber_key = f"{chat_id}_{message_thread_id}" if message_thread_id else str(chat_id)
is_forum_topic = message_thread_id is not None
cfg = CATEGORY_CONFIG[category]
if subscriber_key not in self.subscribers:
self.subscribers[subscriber_key] = {'topic_id': message_thread_id, 'feed_types': []}
if category not in self.subscribers[subscriber_key]['feed_types']:
self.subscribers[subscriber_key]['feed_types'].append(category)
self.save_subscribers()
chat_type = "forum topic" if is_forum_topic else ("group" if update.effective_chat.type in ['group', 'supergroup'] else "private chat")
await update.message.reply_text(
f"{cfg['emoji']} **{cfg['label']} Activated!**\n\n"
f"You will now receive **{cfg['label'].lower()}** alerts in this {chat_type}.\n"
f"Type `/off_{category}` to unsubscribe.",
parse_mode=ParseMode.MARKDOWN,
message_thread_id=message_thread_id if is_forum_topic else None
)
logger.info(f"Subscriber {subscriber_key} enabled {category.upper()} - Topic ID: {message_thread_id}")
else:
await update.message.reply_text(
f"{cfg['label']} is already enabled for this {('topic' if is_forum_topic else 'chat')}.",
parse_mode=ParseMode.MARKDOWN,
message_thread_id=message_thread_id if is_forum_topic else None
)
async def category_off_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE, category: str):
"""Handle /off_<category> command."""
if category not in CATEGORY_CONFIG:
return
chat_id = update.effective_chat.id
message_thread_id = getattr(update.message, 'message_thread_id', None)
subscriber_key = f"{chat_id}_{message_thread_id}" if message_thread_id else str(chat_id)
is_forum_topic = message_thread_id is not None
cfg = CATEGORY_CONFIG[category]
if subscriber_key in self.subscribers and category in self.subscribers[subscriber_key].get('feed_types', []):
self.subscribers[subscriber_key]['feed_types'].remove(category)
if not self.subscribers[subscriber_key]['feed_types']:
del self.subscribers[subscriber_key]
self.save_subscribers()
chat_type = "topic" if is_forum_topic else "chat"
await update.message.reply_text(
f"✋ **{cfg['label']} disabled.**\n\n"
f"This {chat_type} will no longer receive {cfg['label'].lower()} alerts.\n"
f"Type `/on_{category}` to re-subscribe.",
parse_mode=ParseMode.MARKDOWN,
message_thread_id=message_thread_id if is_forum_topic else None
)
logger.info(f"Subscriber {subscriber_key} disabled {category.upper()} - Topic ID: {message_thread_id}")
else:
await update.message.reply_text(
f"❌ This chat is not subscribed to {cfg['label'].lower()} alerts.",
parse_mode=ParseMode.MARKDOWN,
message_thread_id=message_thread_id if is_forum_topic else None
)
async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /stats command - show feed status for all categories."""
chat_id = update.effective_chat.id
message_thread_id = getattr(update.message, 'message_thread_id', None)
subscriber_key = f"{chat_id}_{message_thread_id}" if message_thread_id else str(chat_id)
if subscriber_key not in self.subscribers:
await update.message.reply_text(
"This topic is not subscribed yet. Use /on_news, /on_threat_intel, /on_malware, /on_osint, or /on_research first.",
message_thread_id=message_thread_id if message_thread_id else None
)
return
await update.message.reply_text(
"🔍 **Checking feed status...** Please wait...",
parse_mode=ParseMode.MARKDOWN,
message_thread_id=message_thread_id if message_thread_id else None
)
try:
total_feeds = 0
online_feeds = 0
# Send header message
await update.message.reply_text(
"📊 **Feed Status Report**\n",
parse_mode=ParseMode.MARKDOWN,
message_thread_id=message_thread_id if message_thread_id else None
)
await asyncio.sleep(0.5)
for category, cfg in CATEGORY_CONFIG.items():
async with RSSFeedManager(feeds_file=cfg["feeds_file"], feed_type=category) as manager:
status_data = await manager.check_feed_status()
await update.message.reply_text(
f"{cfg['emoji']} **{cfg['label'].upper()} FEEDS:**\n",
parse_mode=ParseMode.MARKDOWN,
message_thread_id=message_thread_id if message_thread_id else None
)
await asyncio.sleep(0.2)
for feed_category, feeds in status_data.items():
category_display = feed_category.replace('_', ' ').title()
category_message = f"**{category_display}:**\n"
for feed_name, feed_info in feeds.items():
total_feeds += 1
status_icon = feed_info['status']
entries = feed_info['entries']
last_updated = feed_info['last_updated']
if '' in status_icon:
online_feeds += 1
feed_line = f"{feed_name}: {status_icon}\n"
if entries > 0:
feed_line += f"{entries} articles, last: {last_updated}\n"
if 'error' in feed_info:
feed_line += f" └ Error: {feed_info['error'][:50]}\n"
category_message += feed_line
await update.message.reply_text(
category_message,
parse_mode=ParseMode.MARKDOWN,
message_thread_id=message_thread_id if message_thread_id else None
)
await asyncio.sleep(0.2)
# Send summary as final message
summary = f"📈 **Summary:**\n"
summary += f"• Total Feeds: {total_feeds}\n"
summary += f"• Online: {online_feeds}\n"
summary += f"• Offline: {total_feeds - online_feeds}\n"
summary += f"• Subscribers: {len(self.subscribers)}\n"
summary += f"• Last Check: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}"
await update.message.reply_text(
summary,
parse_mode=ParseMode.MARKDOWN,
message_thread_id=message_thread_id if message_thread_id else None
)
except Exception as e:
logger.error(f"Error in stats command: {e}")
await update.message.reply_text(
"❌ Error checking feed status. Please try again later.",
parse_mode=ParseMode.MARKDOWN,
message_thread_id=message_thread_id if message_thread_id else None
)
async def handle_non_subscribers(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle messages from non-subscribers - completely silent for groups"""
# Bot remains silent for all non-command messages
# Only responds to slash commands, ignores everything else
pass
async def send_alert(self, article: Dict, context: ContextTypes.DEFAULT_TYPE = None) -> bool:
"""Send threat intel alert to subscribers based on feed type."""
if not self.subscribers:
return False
try:
message, thumbnail = RSSFeedManager.format_telegram_message(article)
failed_sends = []
article_feed_type = article.get('feed_type', 'general')
successful_sends = 0
for subscriber_key, sub_info in self.subscribers.copy().items():
# Check if subscriber wants this feed type
feed_types = sub_info.get('feed_types', [])
if article_feed_type not in feed_types:
continue # Skip if subscriber doesn't want this feed type
# Parse chat_id from subscriber_key (format: "chatid_topicid" or "chatid")
chat_id_part = subscriber_key.split('_')[0]
chat_id = int(chat_id_part)
topic_id = sub_info.get('topic_id') if isinstance(sub_info, dict) else None
try:
if thumbnail:
# Try to send with photo
try:
await self.application.bot.send_photo(
chat_id=chat_id,
photo=thumbnail,
caption=message,
parse_mode=ParseMode.HTML,
message_thread_id=topic_id
)
successful_sends += 1
except TelegramError:
# Fallback to text if photo fails
await self.application.bot.send_message(
chat_id=chat_id,
text=message,
parse_mode=ParseMode.HTML,
disable_web_page_preview=False,
message_thread_id=topic_id
)
successful_sends += 1
else:
await self.application.bot.send_message(
chat_id=chat_id,
text=message,
parse_mode=ParseMode.HTML,
disable_web_page_preview=False,
message_thread_id=topic_id
)
successful_sends += 1
except Exception as e:
logger.warning(f"Failed to send to {chat_id}: {e}")
if "bot was blocked" in str(e).lower() or "chat not found" in str(e).lower():
failed_sends.append(subscriber_key)
# Remove blocked/invalid subscribers
for failed_key in failed_sends:
self.subscribers.pop(failed_key, None)
logger.info(f"Removed blocked/invalid subscriber: {failed_key}")
if failed_sends:
self.save_subscribers()
if successful_sends > 0:
# Mark as sent only after at least one Telegram delivery succeeded.
RSSFeedManager.mark_article_as_sent(article, seen_file="seen_articles.db")
logger.info(f"Alert sent to {successful_sends} subscribers: {article['title'][:50]}...")
return True
logger.info(f"No eligible subscribers for article: {article['title'][:50]}...")
return False
except Exception as e:
logger.error(f"Error sending alert: {e}")
return False
async def monitor_feeds(self):
"""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
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)"
)
except Exception as e:
logger.error(f"Error in initial feed scan: {e}")
while True:
try:
all_articles = []
for category, cfg in CATEGORY_CONFIG.items():
async with RSSFeedManager(feeds_file=cfg["feeds_file"], feed_type=category) as manager:
category_articles = await manager.fetch_all_feeds(initial_run=False)
all_articles.extend(category_articles)
if category_articles:
logger.info(f"Found {len(category_articles)} new {category.upper()} articles")
# Deduplicate within this polling batch (cross-feed/source duplicates).
unique_articles = []
batch_seen_url = set()
batch_seen_content = set()
for article in all_articles:
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)
# Send alerts for new articles
sent_count = 0
filtered_articles = []
for article in unique_articles:
classified = self.classifier.classify_article(article)
if classified.get("quality_score", 0) < self.min_quality_score:
continue
if classified.get("severity", "low") not in self.allowed_severities:
continue
filtered_articles.append(classified)
for article in filtered_articles:
sent = await self.send_alert(article)
if sent:
sent_count += 1
# Small delay to avoid rate limiting
await asyncio.sleep(1)
if unique_articles:
logger.info(
f"Processed {len(unique_articles)} unique new articles; "
f"after filters {len(filtered_articles)}; delivered {sent_count}"
)
else:
logger.info("No new articles found")
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):
"""Build the Telegram application"""
# Build application
self.application = ApplicationBuilder().token(self.token).build()
# Add command handlers
self.application.add_handler(CommandHandler("start", self.start_command))
self.application.add_handler(CommandHandler("help", self.help_command))
for category in CATEGORY_CONFIG:
self.application.add_handler(
CommandHandler(f"on_{category}", partial(self.category_on_command, category=category))
)
self.application.add_handler(
CommandHandler(f"off_{category}", partial(self.category_off_command, category=category))
)
self.application.add_handler(CommandHandler("stats", self.stats_command))
# Handle all other messages (for non-subscribers)
self.application.add_handler(
MessageHandler(filters.TEXT & ~filters.COMMAND, self.handle_non_subscribers)
)
logger.info("Telegram bot application built successfully")
async def start_monitoring(self):
"""Start the background monitoring task"""
if not self.monitoring_task:
self.monitoring_task = asyncio.create_task(self.monitor_feeds())
logger.info("RSS monitoring task started")
async def stop_monitoring(self):
"""Stop the background monitoring task"""
if self.monitoring_task:
self.monitoring_task.cancel()
try:
await self.monitoring_task
except asyncio.CancelledError:
pass
self.monitoring_task = None
logger.info("RSS monitoring task stopped")
async def run(self):
"""Run the bot"""
self.build_application()
try:
# Initialize and start the application
await self.application.initialize()
await self.application.start()
# Start background monitoring
await self.start_monitoring()
# Start polling
await self.application.updater.start_polling()
logger.info("🚀 Threat Intel Bot is running!")
logger.info(f"📊 {len(self.subscribers)} subscribers loaded")
# Keep running until interrupted
while True:
await asyncio.sleep(1)
except KeyboardInterrupt:
logger.info("Received interrupt, shutting down...")
except Exception as e:
logger.error(f"Error running bot: {e}")
raise
finally:
# Cleanup
await self.stop_monitoring()
await self.application.stop()
await self.application.shutdown()
def main():
"""Main function"""
# Load .env file if it exists
env_file = Path('.env')
if env_file.exists():
with open(env_file, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
os.environ[key.strip()] = value.strip().strip('"\'')
# Get bot token from environment variable
bot_token = os.getenv('BOT_TOKEN')
if not bot_token:
logger.error("BOT_TOKEN not found!")
logger.info("Either:")
logger.info("1. Set environment variable: export BOT_TOKEN='your_token'")
logger.info("2. Create .env file with: BOT_TOKEN=your_token")
return
# Create and run bot
try:
min_quality_score = int(os.getenv("MIN_QUALITY_SCORE", "0"))
except ValueError:
logger.warning("Invalid MIN_QUALITY_SCORE; defaulting to 0")
min_quality_score = 0
allowed_severities = os.getenv("ALLOWED_SEVERITIES", "critical,high,medium,low")
bot = ThreatIntelBot(
bot_token,
min_quality_score=min_quality_score,
allowed_severities=allowed_severities,
)
try:
asyncio.run(bot.run())
except KeyboardInterrupt:
logger.info("Bot stopped by user")
except Exception as e:
logger.error(f"Bot crashed: {e}")
if __name__ == "__main__":
main()