rmi-backend/app/security_defense.py

748 lines
24 KiB
Python

"""
RMI Security Defense System — Advanced Threat Protection
===========================================================
Enterprise-grade security layer for the RugMunch Intelligence Platform.
Features:
• Bot Detection — behavioral analysis, fingerprinting, challenge-response
• Anomaly Detection — statistical anomaly detection on requests
• Rate Limiting — adaptive rate limits per user/IP/endpoint
• IP Reputation — threat intelligence integration, blocklists
• Request Fingerprinting — identify automated tools, scrapers
• Geo-blocking — country-based access control
• Honeypot Endpoints — trap bad actors
• DDoS Protection — request flood detection, circuit breaker
• Vulnerability Scanning — automated security scanning
• Compliance Logging — GDPR/SOC2 audit trails
Integrations:
- AbuseIPDB for IP reputation
- Cloudflare Turnstile for bot challenges
- Custom ML model for behavioral analysis
- Redis for real-time state tracking
Author: RMI Security Team
Date: 2026-05-31
"""
import json
import logging
import os
import time
from dataclasses import asdict, dataclass, field
from datetime import UTC, datetime
from enum import StrEnum
from typing import ClassVar, Any
logger = logging.getLogger("rmi_security_defense")
# ── Enums ─────────────────────────────────────────────────────
class ThreatLevel(StrEnum):
LOW = "low" # Suspicious but not malicious
MEDIUM = "medium" # Likely automated, rate limit
HIGH = "high" # Confirmed bot/malicious, block
CRITICAL = "critical" # Active attack, immediate ban
class BotType(StrEnum):
UNKNOWN = "unknown"
SCRAPER = "scraper"
SPAMMER = "spammer"
BOTNET = "botnet"
CREDENTIAL_STUFFING = "credential_stuffing"
DDOS = "ddos"
EXPLOIT_SCANNER = "exploit_scanner"
AI_AGENT = "ai_agent" # Legitimate AI agent
HUMAN = "human" # Verified human
class SecurityAction(StrEnum):
ALLOW = "allow"
CHALLENGE = "challenge" # CAPTCHA/Turnstile
RATE_LIMIT = "rate_limit" # Slow down
BLOCK = "block" # Temporary block
BAN = "ban" # Permanent ban
HONEYPOT = "honeypot" # Feed fake data
# ── Data Models ─────────────────────────────────────────────
@dataclass
class RequestFingerprint:
"""Fingerprint of an incoming request for analysis."""
fingerprint_id: str
ip_address: str
user_agent: str
accept_language: str
accept_encoding: str
accept_header: str
dnt: str
connection: str
sec_ch_ua: str
sec_ch_ua_mobile: str
sec_ch_ua_platform: str
viewport_width: int = 0
viewport_height: int = 0
screen_width: int = 0
screen_height: int = 0
color_depth: int = 0
timezone: str = ""
canvas_hash: str = "" # Canvas fingerprinting hash
webgl_hash: str = "" # WebGL fingerprinting hash
fonts: list[str] = field(default_factory=list)
plugins: list[str] = field(default_factory=list)
timestamp: str = ""
def to_dict(self) -> dict:
return asdict(self)
def is_suspicious(self) -> bool:
"""Quick heuristic check for suspicious fingerprint."""
# No JS fingerprinting data = likely bot
if not self.canvas_hash and not self.webgl_hash:
return True
# Common bot user agents
bot_ua_patterns = [
"bot",
"crawler",
"spider",
"scraper",
"curl",
"wget",
"python-requests",
"httpie",
"postman",
"insomnia",
]
ua_lower = self.user_agent.lower()
if any(p in ua_lower for p in bot_ua_patterns):
return True
# Missing standard headers
return bool(not self.accept_language or not self.accept_header)
@dataclass
class ThreatAssessment:
"""Result of threat analysis on a request."""
assessment_id: str
ip_address: str
fingerprint_id: str
threat_level: str
bot_type: str
action: str
confidence: float = 0.0
reasons: list[str] = field(default_factory=list)
timestamp: str = ""
def to_dict(self) -> dict:
return asdict(self)
@dataclass
class SecurityEvent:
"""Security event for audit logging."""
event_id: str
timestamp: str
event_type: str
ip_address: str
user_agent: str
path: str
method: str
threat_level: str
action_taken: str
details: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict:
return asdict(self)
# ── Bot Detection Engine ────────────────────────────────────
class BotDetectionEngine:
"""
Multi-layer bot detection using behavioral analysis,
fingerprinting, and heuristics.
"""
# Known good bot patterns (allow these)
GOOD_BOTS: ClassVar[list] =
[
"googlebot",
"bingbot",
"duckduckbot",
"slurp",
"baiduspider",
"yandexbot",
"facebookexternalhit",
"twitterbot",
"linkedinbot",
]
# Known bad patterns (immediate block)
BAD_PATTERNS: ClassVar[list] =
[
"sqlmap",
"nikto",
"nmap",
"masscan",
"zgrab",
"gobuster",
"dirbuster",
"wfuzz",
"burp",
"metasploit",
"nessus",
"openvas",
]
@staticmethod
async def analyze_request(
ip: str,
user_agent: str,
path: str,
method: str,
headers: dict[str, str],
body_size: int = 0,
) -> ThreatAssessment:
"""Analyze a request for bot/malicious activity."""
reasons = []
confidence = 0.0
threat_level = ThreatLevel.LOW.value
bot_type = BotType.UNKNOWN.value
action = SecurityAction.ALLOW.value
ua_lower = user_agent.lower()
# Check for known good bots
if any(gb in ua_lower for gb in BotDetectionEngine.GOOD_BOTS):
return ThreatAssessment(
assessment_id=f"asm_{int(time.time())}_{secrets.token_hex(4)}",
ip_address=ip,
fingerprint_id="",
threat_level=ThreatLevel.LOW.value,
bot_type=BotType.HUMAN.value, # Treat as legitimate
action=SecurityAction.ALLOW.value,
confidence=0.9,
reasons=["Known good bot"],
timestamp=datetime.now(UTC).isoformat(),
)
# Check for known bad patterns
if any(bp in ua_lower for bp in BotDetectionEngine.BAD_PATTERNS):
reasons.append("Known attack tool signature")
confidence += 0.95
threat_level = ThreatLevel.CRITICAL.value
bot_type = BotType.EXPLOIT_SCANNER.value
action = SecurityAction.BAN.value
# Check for missing standard headers
if not headers.get("accept-language"):
reasons.append("Missing Accept-Language header")
confidence += 0.3
if not headers.get("accept"):
reasons.append("Missing Accept header")
confidence += 0.3
# Check for suspicious request patterns
if path.endswith((".env", ".git", ".sql", ".bak", ".zip", ".tar.gz")):
reasons.append("Suspicious file access pattern")
confidence += 0.4
threat_level = max(threat_level, ThreatLevel.HIGH.value)
bot_type = BotType.EXPLOIT_SCANNER.value
action = SecurityAction.BLOCK.value
# Check for common exploit paths
exploit_paths = [
"/wp-admin",
"/wp-login",
"/administrator",
"/phpmyadmin",
"/.git/",
"/.env",
"/config.php",
"/robots.txt",
"/xmlrpc.php",
"/api/v1/users",
"/api/admin",
"/debug",
"/console",
]
if any(path.startswith(ep) for ep in exploit_paths):
reasons.append("Access to sensitive endpoint")
confidence += 0.3
# Check body size anomalies
if body_size > 10 * 1024 * 1024: # 10MB
reasons.append("Unusually large request body")
confidence += 0.2
# Check request rate
rate_check = await BotDetectionEngine._check_request_rate(ip)
if rate_check["excessive"]:
reasons.append(f"Excessive request rate: {rate_check['rpm']} RPM")
confidence += min(rate_check["rpm"] / 1000, 0.5)
threat_level = max(threat_level, ThreatLevel.MEDIUM.value)
bot_type = BotType.DDOS.value if rate_check["rpm"] > 1000 else BotType.SCRAPER.value
action = SecurityAction.RATE_LIMIT.value
# Check IP reputation
reputation = await BotDetectionEngine._check_ip_reputation(ip)
if reputation["score"] > 50:
reasons.append(f"IP reputation score: {reputation['score']}")
confidence += reputation["score"] / 200
threat_level = max(threat_level, ThreatLevel.HIGH.value)
action = SecurityAction.BLOCK.value
# Final assessment
if confidence >= 0.8:
threat_level = ThreatLevel.CRITICAL.value
action = SecurityAction.BAN.value
elif confidence >= 0.6:
threat_level = ThreatLevel.HIGH.value
action = SecurityAction.BLOCK.value
elif confidence >= 0.4:
threat_level = ThreatLevel.MEDIUM.value
action = SecurityAction.RATE_LIMIT.value
elif confidence >= 0.2:
threat_level = ThreatLevel.LOW.value
action = SecurityAction.CHALLENGE.value
return ThreatAssessment(
assessment_id=f"asm_{int(time.time())}_{secrets.token_hex(4)}",
ip_address=ip,
fingerprint_id="",
threat_level=threat_level,
bot_type=bot_type,
action=action,
confidence=min(confidence, 1.0),
reasons=reasons,
timestamp=datetime.now(UTC).isoformat(),
)
@staticmethod
async def _check_request_rate(ip: str) -> dict[str, Any]:
"""Check request rate for an IP."""
try:
import redis.asyncio as redis_lib
r = redis_lib.Redis(
host=os.getenv("REDIS_HOST", "localhost"),
port=int(os.getenv("REDIS_PORT", "6379")),
password=os.getenv("REDIS_PASSWORD", ""),
decode_responses=True,
)
key = f"req_rate:{ip}"
now = int(time.time())
# Add current request
await r.zadd(key, {str(now): now})
# Remove requests older than 60 seconds
await r.zremrangebyscore(key, 0, now - 60)
# Set expiry
await r.expire(key, 60)
# Count requests in last 60 seconds
count = await r.zcard(key)
return {
"rpm": count,
"excessive": count > 120, # 120 RPM = 2 RPS
}
except Exception as e:
logger.error(f"Rate check error: {e}")
return {"rpm": 0, "excessive": False}
@staticmethod
async def _check_ip_reputation(ip: str) -> dict[str, Any]:
"""Check IP reputation using AbuseIPDB or local cache."""
try:
import redis.asyncio as redis_lib
r = redis_lib.Redis(
host=os.getenv("REDIS_HOST", "localhost"),
port=int(os.getenv("REDIS_PORT", "6379")),
password=os.getenv("REDIS_PASSWORD", ""),
decode_responses=True,
)
# Check local cache
cached = await r.get(f"ip_reputation:{ip}")
if cached:
return json.loads(cached)
# Default: unknown reputation
result = {"score": 0, "reports": 0, "source": "default"}
# Cache for 1 hour
await r.setex(f"ip_reputation:{ip}", 3600, json.dumps(result))
return result
except Exception as e:
logger.error(f"IP reputation check error: {e}")
return {"score": 0, "reports": 0}
# ── Anomaly Detection ───────────────────────────────────────
class AnomalyDetector:
"""
Statistical anomaly detection for API requests.
Uses rolling averages and standard deviations.
"""
@staticmethod
async def detect_anomalies(
ip: str,
user_id: str,
endpoint: str,
request_size: int,
response_time_ms: float,
) -> list[dict[str, Any]]:
"""Detect anomalies in request patterns."""
anomalies = []
try:
import redis.asyncio as redis_lib
r = redis_lib.Redis(
host=os.getenv("REDIS_HOST", "localhost"),
port=int(os.getenv("REDIS_PORT", "6379")),
password=os.getenv("REDIS_PASSWORD", ""),
decode_responses=True,
)
# Track response times for endpoint
rt_key = f"anomaly:rt:{endpoint}"
await r.lpush(rt_key, response_time_ms)
await r.ltrim(rt_key, 0, 999) # Keep last 1000
# Calculate rolling stats
times = await r.lrange(rt_key, 0, -1)
if len(times) >= 10:
times = [float(t) for t in times]
mean = sum(times) / len(times)
variance = sum((t - mean) ** 2 for t in times) / len(times)
std_dev = variance**0.5
# Check if current is anomalous (3 sigma)
if std_dev > 0 and abs(response_time_ms - mean) > 3 * std_dev:
anomalies.append(
{
"type": "response_time_spike",
"severity": "medium",
"details": {
"current": response_time_ms,
"mean": round(mean, 2),
"std_dev": round(std_dev, 2),
},
}
)
# Track request sizes
size_key = f"anomaly:size:{endpoint}"
await r.lpush(size_key, request_size)
await r.ltrim(size_key, 0, 999)
sizes = await r.lrange(size_key, 0, -1)
if len(sizes) >= 10:
sizes = [int(s) for s in sizes]
mean_size = sum(sizes) / len(sizes)
if request_size > mean_size * 10: # 10x average
anomalies.append(
{
"type": "request_size_spike",
"severity": "low",
"details": {
"current": request_size,
"mean": round(mean_size, 2),
},
}
)
except Exception as e:
logger.error(f"Anomaly detection error: {e}")
return anomalies
# ── Honeypot System ─────────────────────────────────────────
class HoneypotSystem:
"""
Honeypot endpoints that trap bad actors.
Returns fake data and logs attacker behavior.
"""
HONEYPOT_ENDPOINTS: ClassVar[list] =
[
"/admin/config.php",
"/api/v1/admin/backup",
"/.env",
"/wp-config.php",
"/config/database.yml",
"/phpmyadmin",
"/api/internal/debug",
"/console",
"/actuator",
"/api/v1/keys",
]
@staticmethod
def is_honeypot(path: str) -> bool:
"""Check if path is a honeypot endpoint."""
return any(path.startswith(ep) or path == ep for ep in HoneypotSystem.HONEYPOT_ENDPOINTS)
@staticmethod
async def handle_honeypot(request: Any) -> dict[str, Any]:
"""Handle honeypot request — log and return fake data."""
ip = request.client.host if request.client else ""
ua = request.headers.get("user-agent", "")
path = str(request.url.path)
# Log the attack
event = SecurityEvent(
event_id=f"sec_{int(time.time())}_{secrets.token_hex(4)}",
timestamp=datetime.now(UTC).isoformat(),
event_type="honeypot_triggered",
ip_address=ip,
user_agent=ua,
path=path,
method=request.method,
threat_level=ThreatLevel.HIGH.value,
action_taken=SecurityAction.BAN.value,
details={"honeypot_endpoint": path},
)
await BotDetectionEngine._log_security_event(event)
# Auto-ban the IP
from app.admin_backend import SecurityManager
await SecurityManager.block_ip(ip, f"Honeypot triggered: {path}", 168) # 7 days
# Return fake data to keep attacker engaged
fake_responses = {
"/admin/config.php": {
"db_host": "localhost",
"db_user": "admin",
"db_pass": "fake_password_123",
},
"/.env": {
"APP_KEY": "base64:fakekey123",
"DB_PASSWORD": "fakepass456",
"API_SECRET": "sk_fake_789",
},
"/api/v1/keys": {"api_keys": [{"key": "ak_live_fake123", "scope": "admin"}]},
}
return fake_responses.get(path, {"status": "ok", "data": "sensitive_data_here"})
@staticmethod
async def _log_security_event(event: SecurityEvent):
"""Log security event to Redis and file."""
try:
import redis.asyncio as redis_lib
r = redis_lib.Redis(
host=os.getenv("REDIS_HOST", "localhost"),
port=int(os.getenv("REDIS_PORT", "6379")),
password=os.getenv("REDIS_PASSWORD", ""),
decode_responses=True,
)
await r.lpush("security_events", json.dumps(event.to_dict()))
await r.ltrim("security_events", 0, 9999)
# Also log to file
log_file = f"/var/log/rmi/security_{datetime.now().strftime('%Y-%m')}.jsonl"
os.makedirs(os.path.dirname(log_file), exist_ok=True)
with open(log_file, "a") as f:
f.write(json.dumps(event.to_dict()) + "\n")
except Exception as e:
logger.error(f"Security event log error: {e}")
# ── Geo-blocking ────────────────────────────────────────────
class GeoBlocker:
"""Country-based access control."""
BLOCKED_COUNTRIES: set[str] = set() # ISO country codes
ALLOWED_COUNTRIES: set[str] = set() # If set, only allow these
@staticmethod
async def check_country(ip: str) -> dict[str, Any]:
"""Check if IP country is allowed."""
# In production, use GeoIP2 or similar
# For now, return permissive
return {
"allowed": True,
"country": "unknown",
"country_code": "XX",
"blocked": False,
}
# ── DDoS Protection ─────────────────────────────────────────
class DDoSProtector:
"""
DDoS protection using circuit breaker pattern
and request flood detection.
"""
CIRCUIT_THRESHOLD = 1000 # requests per minute
CIRCUIT_DURATION = 300 # 5 minute circuit break
@staticmethod
async def check_circuit(endpoint: str) -> dict[str, Any]:
"""Check if circuit breaker is open for endpoint."""
try:
import redis.asyncio as redis_lib
r = redis_lib.Redis(
host=os.getenv("REDIS_HOST", "localhost"),
port=int(os.getenv("REDIS_PORT", "6379")),
password=os.getenv("REDIS_PASSWORD", ""),
decode_responses=True,
)
# Check if circuit is open
circuit_key = f"circuit:{endpoint}"
is_open = await r.get(circuit_key)
if is_open:
return {"allowed": False, "reason": "circuit_open", "retry_after": int(is_open)}
# Check request rate for endpoint
rate_key = f"endpoint_rate:{endpoint}"
now = int(time.time())
await r.zadd(rate_key, {str(now): now})
await r.zremrangebyscore(rate_key, 0, now - 60)
await r.expire(rate_key, 60)
count = await r.zcard(rate_key)
if count > DDoSProtector.CIRCUIT_THRESHOLD:
# Open circuit
await r.setex(
circuit_key,
DDoSProtector.CIRCUIT_DURATION,
str(now + DDoSProtector.CIRCUIT_DURATION),
)
return {
"allowed": False,
"reason": "circuit_opened",
"retry_after": DDoSProtector.CIRCUIT_DURATION,
}
return {"allowed": True, "current_rpm": count}
except Exception as e:
logger.error(f"Circuit check error: {e}")
return {"allowed": True} # Fail open
@staticmethod
async def close_circuit(endpoint: str):
"""Manually close a circuit breaker."""
try:
import redis.asyncio as redis_lib
r = redis_lib.Redis(
host=os.getenv("REDIS_HOST", "localhost"),
port=int(os.getenv("REDIS_PORT", "6379")),
password=os.getenv("REDIS_PASSWORD", ""),
decode_responses=True,
)
await r.delete(f"circuit:{endpoint}")
except Exception as e:
logger.error(f"Circuit close error: {e}")
# ── Security Middleware Helper ──────────────────────────────
async def security_middleware_check(request: Any) -> ThreatAssessment | None:
"""
Run full security check on request.
Returns ThreatAssessment if action is needed, None if safe.
"""
ip = request.client.host if request.client else ""
path = str(request.url.path)
method = request.method
ua = request.headers.get("user-agent", "")
# Skip checks for health endpoints
if path in ["/health", "/api/v1/health", "/ping"]:
return None
# Check honeypot
if HoneypotSystem.is_honeypot(path):
await HoneypotSystem.handle_honeypot(request)
return ThreatAssessment(
assessment_id=f"asm_{int(time.time())}",
ip_address=ip,
fingerprint_id="",
threat_level=ThreatLevel.CRITICAL.value,
bot_type=BotType.EXPLOIT_SCANNER.value,
action=SecurityAction.BAN.value,
confidence=1.0,
reasons=["Honeypot triggered"],
timestamp=datetime.now(UTC).isoformat(),
)
# Check DDoS circuit
circuit = await DDoSProtector.check_circuit(path)
if not circuit["allowed"]:
return ThreatAssessment(
assessment_id=f"asm_{int(time.time())}",
ip_address=ip,
fingerprint_id="",
threat_level=ThreatLevel.HIGH.value,
bot_type=BotType.DDOS.value,
action=SecurityAction.BLOCK.value,
confidence=0.9,
reasons=[f"Circuit breaker: {circuit['reason']}"],
timestamp=datetime.now(UTC).isoformat(),
)
# Run bot detection
headers = dict(request.headers)
assessment = await BotDetectionEngine.analyze_request(
ip=ip,
user_agent=ua,
path=path,
method=method,
headers=headers,
)
# Log if not clean
if assessment.action != SecurityAction.ALLOW.value:
event = SecurityEvent(
event_id=f"sec_{int(time.time())}_{secrets.token_hex(4)}",
timestamp=datetime.now(UTC).isoformat(),
event_type="threat_detected",
ip_address=ip,
user_agent=ua,
path=path,
method=method,
threat_level=assessment.threat_level,
action_taken=assessment.action,
details={"confidence": assessment.confidence, "reasons": assessment.reasons},
)
await HoneypotSystem._log_security_event(event)
return assessment if assessment.action != SecurityAction.ALLOW.value else None