rmi-backend/app/security_defense.py
cryptorugmunch 0a8c73d99b feat(domains): consolidate bulletin, intelligence, markets, admin, referral, mcp + mypy gate
- Make app/domains/auth/ and app/core/redis.py mypy-clean under strict.
- Add mypy-gate.ini and Makefile mypy-gate target; promote typecheck-gate in CI.
- Consolidate domains into app/domains/: bulletin, admin, intelligence, markets.
- Extract referral domain incl. DeFi partner DEX ref links; keep Telegram bot wired.
- Move app/mcp/ package and app/api/v1/mcp/router into app/domains/mcp/.
- Archive dead app/mcp_router.py.
2026-07-07 16:43:49 +07:00

746 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 secrets
import time
from dataclasses import asdict, dataclass, field
from datetime import UTC, datetime
from enum import StrEnum
from typing import Any, ClassVar
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.domains.admin 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 # noqa: RUF012
ALLOWED_COUNTRIES: set[str] = set() # If set, only allow these # noqa: RUF012
@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