837 lines
29 KiB
Markdown
837 lines
29 KiB
Markdown
# Security Standards - TradingAgents
|
|
|
|
## API Key Management
|
|
|
|
### OpenRouter and LLM Provider Security
|
|
|
|
**Environment Variable Management**:
|
|
```bash
|
|
# Required API keys
|
|
export OPENROUTER_API_KEY="sk-or-v1-xxxxxxxxxxxx"
|
|
|
|
# Optional provider keys (for fallback)
|
|
export OPENAI_API_KEY="sk-xxxxxxxxxxxx"
|
|
export ANTHROPIC_API_KEY="sk-ant-xxxxxxxxxxxx"
|
|
|
|
# Financial data APIs
|
|
export FINNHUB_API_KEY="xxxxxxxxxxxx"
|
|
export ALPHA_VANTAGE_API_KEY="xxxxxxxxxxxx"
|
|
```
|
|
|
|
**Configuration Security**:
|
|
```python
|
|
import os
|
|
from pathlib import Path
|
|
|
|
class SecureConfig:
|
|
"""Secure configuration management with validation"""
|
|
|
|
@classmethod
|
|
def get_required_env(cls, key: str, description: str = "") -> str:
|
|
"""Get required environment variable with validation"""
|
|
value = os.getenv(key)
|
|
if not value:
|
|
raise EnvironmentError(
|
|
f"Required environment variable {key} not set. {description}"
|
|
)
|
|
|
|
# Validate API key format
|
|
if key.endswith("_API_KEY"):
|
|
cls._validate_api_key(key, value)
|
|
|
|
return value
|
|
|
|
@classmethod
|
|
def _validate_api_key(cls, key: str, value: str) -> None:
|
|
"""Validate API key format and warn on potential issues"""
|
|
if len(value) < 20:
|
|
raise ValueError(f"API key {key} appears too short (< 20 chars)")
|
|
|
|
if value.startswith("sk-") and len(value) < 40:
|
|
raise ValueError(f"OpenAI/OpenRouter API key {key} appears invalid")
|
|
|
|
# Detect placeholder values
|
|
placeholder_patterns = ["your_", "replace_", "xxxx", "test"]
|
|
if any(pattern in value.lower() for pattern in placeholder_patterns):
|
|
raise ValueError(f"API key {key} appears to be a placeholder")
|
|
|
|
@classmethod
|
|
def load_openrouter_config(cls) -> dict[str, str]:
|
|
"""Load and validate OpenRouter configuration"""
|
|
return {
|
|
"api_key": cls.get_required_env(
|
|
"OPENROUTER_API_KEY",
|
|
"Get your key from https://openrouter.ai/keys"
|
|
),
|
|
"base_url": os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1"),
|
|
"app_name": os.getenv("OPENROUTER_APP_NAME", "TradingAgents"),
|
|
"site_url": os.getenv("OPENROUTER_SITE_URL", "https://github.com/TauricResearch/TradingAgents")
|
|
}
|
|
```
|
|
|
|
**Development vs Production Key Management**:
|
|
```python
|
|
# .env.example (committed to repo)
|
|
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
|
DATABASE_URL=postgresql+asyncpg://postgres:tradingagents@localhost:5432/tradingagents
|
|
TRADINGAGENTS_RESULTS_DIR=./results
|
|
TRADINGAGENTS_DATA_DIR=./data
|
|
|
|
# .env (never committed, gitignored)
|
|
OPENROUTER_API_KEY=sk-or-v1-actual-key-here
|
|
DATABASE_URL=postgresql+asyncpg://user:password@prod-db:5432/tradingagents
|
|
```
|
|
|
|
### Secret Rotation and Management
|
|
|
|
**Key Rotation Strategy**:
|
|
```python
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class APIKeyManager:
|
|
"""Manages API key rotation and health monitoring"""
|
|
|
|
def __init__(self):
|
|
self.key_health: Dict[str, Dict] = {}
|
|
self.rotation_schedule: Dict[str, datetime] = {}
|
|
|
|
async def validate_key_health(self, service: str, api_key: str) -> bool:
|
|
"""Test API key validity with minimal request"""
|
|
try:
|
|
if service == "openrouter":
|
|
return await self._test_openrouter_key(api_key)
|
|
elif service == "finnhub":
|
|
return await self._test_finnhub_key(api_key)
|
|
else:
|
|
logger.warning(f"No health check implemented for {service}")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"API key health check failed for {service}: {e}")
|
|
return False
|
|
|
|
async def _test_openrouter_key(self, api_key: str) -> bool:
|
|
"""Test OpenRouter key with lightweight request"""
|
|
import aiohttp
|
|
|
|
headers = {
|
|
"Authorization": f"Bearer {api_key}",
|
|
"Content-Type": "application/json"
|
|
}
|
|
|
|
# Use minimal model list request to test auth
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.get(
|
|
"https://openrouter.ai/api/v1/models",
|
|
headers=headers,
|
|
timeout=aiohttp.ClientTimeout(total=10)
|
|
) as response:
|
|
return response.status == 200
|
|
|
|
def schedule_rotation(self, service: str, days: int = 90) -> None:
|
|
"""Schedule API key rotation"""
|
|
rotation_date = datetime.now() + timedelta(days=days)
|
|
self.rotation_schedule[service] = rotation_date
|
|
logger.info(f"Scheduled {service} key rotation for {rotation_date.date()}")
|
|
|
|
def get_rotation_alerts(self) -> list[str]:
|
|
"""Get list of keys requiring rotation"""
|
|
alerts = []
|
|
now = datetime.now()
|
|
warning_threshold = timedelta(days=7)
|
|
|
|
for service, rotation_date in self.rotation_schedule.items():
|
|
if now >= rotation_date:
|
|
alerts.append(f"URGENT: {service} API key rotation overdue")
|
|
elif now >= rotation_date - warning_threshold:
|
|
alerts.append(f"WARNING: {service} API key rotation due in {(rotation_date - now).days} days")
|
|
|
|
return alerts
|
|
```
|
|
|
|
## Database Security Patterns
|
|
|
|
### Connection Security
|
|
|
|
**Secure Connection Configuration**:
|
|
```python
|
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
from sqlalchemy.pool import NullPool
|
|
import ssl
|
|
|
|
class SecureDatabaseManager:
|
|
"""Database manager with security-first configuration"""
|
|
|
|
def __init__(self, database_url: str, require_ssl: bool = True):
|
|
# Parse and validate database URL
|
|
if not database_url.startswith(("postgresql+asyncpg://", "postgresql://")):
|
|
raise ValueError("Only PostgreSQL databases are supported")
|
|
|
|
# Ensure asyncpg driver for better async performance
|
|
if database_url.startswith("postgresql://"):
|
|
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://")
|
|
|
|
# SSL/TLS configuration for production
|
|
connect_args = {}
|
|
if require_ssl:
|
|
ssl_context = ssl.create_default_context()
|
|
ssl_context.check_hostname = False # Often needed for cloud databases
|
|
ssl_context.verify_mode = ssl.CERT_REQUIRED
|
|
connect_args["ssl"] = ssl_context
|
|
|
|
self.engine = create_async_engine(
|
|
database_url,
|
|
# Security settings
|
|
connect_args=connect_args,
|
|
pool_pre_ping=True, # Verify connections
|
|
pool_recycle=3600, # Recycle connections (1 hour)
|
|
|
|
# Connection limits to prevent resource exhaustion
|
|
pool_size=10, # Base connection pool
|
|
max_overflow=20, # Additional connections under load
|
|
|
|
# Prevent connection leaks in development
|
|
poolclass=NullPool if self._is_test_env() else None,
|
|
|
|
# Disable SQL echo in production (information disclosure)
|
|
echo=False if os.getenv("ENVIRONMENT") == "production" else False
|
|
)
|
|
|
|
def _is_test_env(self) -> bool:
|
|
"""Detect test environment"""
|
|
return any([
|
|
"test" in os.getenv("DATABASE_URL", "").lower(),
|
|
os.getenv("TESTING") == "true",
|
|
"pytest" in sys.modules
|
|
])
|
|
|
|
async def create_tables_secure(self):
|
|
"""Create tables with security considerations"""
|
|
async with self.engine.begin() as conn:
|
|
# Set secure session parameters
|
|
await conn.execute(text("SET session_replication_role = 'origin'"))
|
|
await conn.execute(text("SET log_statement = 'none'")) # Disable query logging for DDL
|
|
|
|
# Create tables
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
|
|
# Set up row-level security policies if needed
|
|
await self._setup_row_level_security(conn)
|
|
|
|
async def _setup_row_level_security(self, conn):
|
|
"""Configure row-level security for multi-tenant data"""
|
|
# Enable RLS on sensitive tables
|
|
await conn.execute(text("ALTER TABLE news_articles ENABLE ROW LEVEL SECURITY"))
|
|
|
|
# Create policy for data isolation (if implementing multi-user features)
|
|
# await conn.execute(text("""
|
|
# CREATE POLICY user_data_policy ON news_articles
|
|
# FOR ALL TO app_user
|
|
# USING (user_id = current_setting('app.user_id')::UUID)
|
|
# """))
|
|
```
|
|
|
|
### Data Privacy and Anonymization
|
|
|
|
**Financial Data Protection**:
|
|
```python
|
|
import hashlib
|
|
import secrets
|
|
from typing import Any, Dict
|
|
|
|
class DataPrivacyManager:
|
|
"""Handles sensitive financial data with privacy controls"""
|
|
|
|
def __init__(self):
|
|
self.salt = self._get_or_create_salt()
|
|
|
|
def _get_or_create_salt(self) -> bytes:
|
|
"""Get encryption salt from secure storage"""
|
|
salt_path = Path(os.getenv("TRADINGAGENTS_DATA_DIR", "./data")) / ".salt"
|
|
|
|
if salt_path.exists():
|
|
return salt_path.read_bytes()
|
|
else:
|
|
# Generate cryptographically secure salt
|
|
salt = secrets.token_bytes(32)
|
|
salt_path.write_bytes(salt)
|
|
salt_path.chmod(0o600) # Restrict file permissions
|
|
return salt
|
|
|
|
def hash_symbol(self, symbol: str) -> str:
|
|
"""Create consistent hash for symbols (for analytics without exposure)"""
|
|
return hashlib.pbkdf2_hmac(
|
|
'sha256',
|
|
symbol.encode(),
|
|
self.salt,
|
|
100000 # iterations
|
|
).hex()[:16]
|
|
|
|
def sanitize_article_content(self, content: str) -> str:
|
|
"""Remove PII and sensitive information from article content"""
|
|
import re
|
|
|
|
# Remove potential SSNs, account numbers, etc.
|
|
patterns = [
|
|
r'\b\d{3}-\d{2}-\d{4}\b', # SSN
|
|
r'\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b', # Credit card
|
|
r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', # Email
|
|
]
|
|
|
|
sanitized = content
|
|
for pattern in patterns:
|
|
sanitized = re.sub(pattern, '[REDACTED]', sanitized)
|
|
|
|
return sanitized
|
|
|
|
def audit_data_access(self, table: str, operation: str, record_count: int = 1):
|
|
"""Log data access for compliance auditing"""
|
|
logger.info(
|
|
"Data access audit",
|
|
extra={
|
|
"table": table,
|
|
"operation": operation,
|
|
"record_count": record_count,
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"user": os.getenv("USER", "system")
|
|
}
|
|
)
|
|
```
|
|
|
|
### Query Security
|
|
|
|
**SQL Injection Prevention**:
|
|
```python
|
|
from sqlalchemy import text, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
class SecureQueryBuilder:
|
|
"""Build secure parameterized queries"""
|
|
|
|
def __init__(self, session: AsyncSession):
|
|
self.session = session
|
|
|
|
async def get_articles_secure(
|
|
self,
|
|
symbol: str,
|
|
date_filter: date,
|
|
user_input_query: Optional[str] = None
|
|
) -> list[NewsArticle]:
|
|
"""Secure article query with parameterization"""
|
|
|
|
# Base query with parameterized symbol and date
|
|
query = select(NewsArticleEntity).where(
|
|
and_(
|
|
NewsArticleEntity.symbol == symbol, # Parameterized automatically
|
|
NewsArticleEntity.published_date == date_filter
|
|
)
|
|
)
|
|
|
|
# Secure text search if provided
|
|
if user_input_query:
|
|
# Use full-text search instead of LIKE to prevent injection
|
|
# Sanitize and escape the search term
|
|
sanitized_query = self._sanitize_search_term(user_input_query)
|
|
query = query.where(
|
|
NewsArticleEntity.headline.match(sanitized_query) # PostgreSQL full-text search
|
|
)
|
|
|
|
result = await self.session.execute(query)
|
|
return [NewsArticle.from_entity(e) for e in result.scalars()]
|
|
|
|
def _sanitize_search_term(self, query: str) -> str:
|
|
"""Sanitize user input for full-text search"""
|
|
import re
|
|
|
|
# Remove SQL injection patterns
|
|
dangerous_patterns = [
|
|
r"[';\"\\]", # SQL metacharacters
|
|
r"\b(union|select|drop|delete|update|insert)\b", # SQL keywords
|
|
r"--", # SQL comments
|
|
r"/\*.*?\*/" # SQL block comments
|
|
]
|
|
|
|
sanitized = query
|
|
for pattern in dangerous_patterns:
|
|
sanitized = re.sub(pattern, "", sanitized, flags=re.IGNORECASE)
|
|
|
|
# Limit length to prevent DoS
|
|
sanitized = sanitized[:100]
|
|
|
|
# Convert to PostgreSQL full-text search format
|
|
terms = sanitized.split()
|
|
return " & ".join(f'"{term}"' for term in terms if term.isalnum())
|
|
|
|
async def execute_safe_raw_query(self, query_template: str, **params) -> Any:
|
|
"""Execute raw SQL with parameter validation"""
|
|
# Whitelist allowed query templates
|
|
allowed_templates = {
|
|
"performance_stats": "SELECT * FROM pg_stat_statements WHERE query LIKE :pattern",
|
|
"table_sizes": "SELECT schemaname, tablename, pg_total_relation_size(schemaname||'.'||tablename) as size FROM pg_tables WHERE schemaname = :schema"
|
|
}
|
|
|
|
if query_template not in allowed_templates:
|
|
raise ValueError(f"Query template not in whitelist: {query_template}")
|
|
|
|
# Validate parameters
|
|
for key, value in params.items():
|
|
if not self._validate_parameter(key, value):
|
|
raise ValueError(f"Invalid parameter {key}: {value}")
|
|
|
|
query = text(allowed_templates[query_template])
|
|
result = await self.session.execute(query, params)
|
|
return result.fetchall()
|
|
|
|
def _validate_parameter(self, key: str, value: Any) -> bool:
|
|
"""Validate query parameters"""
|
|
# Length limits
|
|
if isinstance(value, str) and len(value) > 100:
|
|
return False
|
|
|
|
# Type restrictions
|
|
if key.endswith("_id") and not isinstance(value, (str, int)):
|
|
return False
|
|
|
|
# No SQL injection patterns
|
|
if isinstance(value, str):
|
|
dangerous = ["'", '"', ";", "--", "/*", "*/", "union", "select"]
|
|
if any(pattern in value.lower() for pattern in dangerous):
|
|
return False
|
|
|
|
return True
|
|
```
|
|
|
|
## Development Environment Security
|
|
|
|
### Local Development Protection
|
|
|
|
**Secure Development Setup**:
|
|
```bash
|
|
#!/bin/bash
|
|
# secure_dev_setup.sh - Secure development environment initialization
|
|
|
|
set -euo pipefail
|
|
|
|
# 1. Create secure data directory
|
|
DATA_DIR="${TRADINGAGENTS_DATA_DIR:-./data}"
|
|
mkdir -p "$DATA_DIR"
|
|
chmod 700 "$DATA_DIR" # Owner read/write/execute only
|
|
|
|
# 2. Create .env file with secure permissions
|
|
if [ ! -f .env ]; then
|
|
cp .env.example .env
|
|
chmod 600 .env # Owner read/write only
|
|
echo "Created .env file. Please update with actual API keys."
|
|
fi
|
|
|
|
# 3. Set up secure Docker environment
|
|
if [ ! -f docker-compose.override.yml ]; then
|
|
cat > docker-compose.override.yml << EOF
|
|
version: '3.8'
|
|
services:
|
|
timescaledb:
|
|
environment:
|
|
# Use strong password in development
|
|
POSTGRES_PASSWORD: \${DB_PASSWORD:-$(openssl rand -base64 32)}
|
|
volumes:
|
|
- ./data/postgres:/var/lib/postgresql/data
|
|
EOF
|
|
echo "Created docker-compose.override.yml with secure settings"
|
|
fi
|
|
|
|
# 4. Configure Git security
|
|
git config --local core.hooksPath .githooks
|
|
chmod +x .githooks/pre-commit
|
|
|
|
# 5. Install security scanning tools
|
|
if command -v pip &> /dev/null; then
|
|
pip install bandit safety
|
|
echo "Installed security scanning tools"
|
|
fi
|
|
|
|
echo "Secure development environment configured"
|
|
echo "Remember to:"
|
|
echo " 1. Update .env with real API keys"
|
|
echo " 2. Never commit .env or API keys"
|
|
echo " 3. Run 'bandit -r tradingagents/' before commits"
|
|
```
|
|
|
|
**Git Security Hooks**:
|
|
```bash
|
|
#!/bin/bash
|
|
# .githooks/pre-commit - Prevent secrets from being committed
|
|
|
|
# Check for common secret patterns
|
|
if git diff --cached --name-only | grep -E "\.(py|yml|yaml|json|env)$"; then
|
|
echo "Scanning for secrets..."
|
|
|
|
# Pattern matching for common secrets
|
|
if git diff --cached | grep -i -E "(api_key|secret|password|token)" | grep -v -E "(example|template|your_|replace_)"; then
|
|
echo "ERROR: Potential secrets detected in staged files!"
|
|
echo "Please review and remove any sensitive information."
|
|
exit 1
|
|
fi
|
|
|
|
# Check for hardcoded URLs with credentials
|
|
if git diff --cached | grep -E "postgresql://[^:]+:[^@]+@"; then
|
|
echo "ERROR: Database URL with credentials detected!"
|
|
echo "Use environment variables instead."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Run security linting if bandit is available
|
|
if command -v bandit &> /dev/null; then
|
|
echo "Running security scan..."
|
|
bandit -r tradingagents/ -f json | jq '.results[] | select(.issue_severity == "HIGH")' | grep -q . && {
|
|
echo "ERROR: High-severity security issues found!"
|
|
echo "Run 'bandit -r tradingagents/' for details."
|
|
exit 1
|
|
}
|
|
fi
|
|
|
|
echo "Pre-commit security checks passed"
|
|
```
|
|
|
|
### Secrets Management with Environment Variables
|
|
|
|
**Environment Variable Security**:
|
|
```python
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
class EnvironmentManager:
|
|
"""Secure environment variable management"""
|
|
|
|
def __init__(self):
|
|
self.env_file = Path(".env")
|
|
self.required_vars = [
|
|
"OPENROUTER_API_KEY",
|
|
"DATABASE_URL"
|
|
]
|
|
self.sensitive_vars = [
|
|
"API_KEY", "SECRET", "PASSWORD", "TOKEN", "PRIVATE_KEY"
|
|
]
|
|
|
|
def validate_environment(self) -> list[str]:
|
|
"""Validate environment setup and return any issues"""
|
|
issues = []
|
|
|
|
# Check required variables
|
|
for var in self.required_vars:
|
|
if not os.getenv(var):
|
|
issues.append(f"Missing required environment variable: {var}")
|
|
|
|
# Check .env file permissions
|
|
if self.env_file.exists():
|
|
stat = self.env_file.stat()
|
|
if stat.st_mode & 0o077: # Check if group/other have any permissions
|
|
issues.append(".env file has overly permissive permissions (should be 600)")
|
|
|
|
# Validate sensitive variables aren't using placeholder values
|
|
for var_name in os.environ:
|
|
if any(sensitive in var_name for sensitive in self.sensitive_vars):
|
|
value = os.getenv(var_name, "")
|
|
if self._is_placeholder_value(value):
|
|
issues.append(f"{var_name} appears to contain a placeholder value")
|
|
|
|
return issues
|
|
|
|
def _is_placeholder_value(self, value: str) -> bool:
|
|
"""Detect common placeholder patterns"""
|
|
placeholders = [
|
|
"your_", "replace_", "change_me", "xxxx", "test_key",
|
|
"example", "sample", "placeholder", "todo"
|
|
]
|
|
return any(placeholder in value.lower() for placeholder in placeholders)
|
|
|
|
def setup_production_env(self) -> dict[str, str]:
|
|
"""Configure production environment with security hardening"""
|
|
return {
|
|
# Security settings
|
|
"PYTHONDONTWRITEBYTECODE": "1", # Don't create .pyc files
|
|
"PYTHONUNBUFFERED": "1", # Unbuffered output
|
|
"PYTHONHASHSEED": "random", # Random hash seed
|
|
|
|
# Application security
|
|
"ENVIRONMENT": "production",
|
|
"DEBUG": "false",
|
|
"LOG_LEVEL": "INFO", # Don't log debug info
|
|
|
|
# Database security
|
|
"DB_SSL_MODE": "require",
|
|
"DB_POOL_PRE_PING": "true",
|
|
"DB_ECHO": "false", # Don't log SQL queries
|
|
|
|
# API security
|
|
"API_RATE_LIMIT": "100", # Requests per minute
|
|
"API_TIMEOUT": "30", # Request timeout in seconds
|
|
}
|
|
|
|
def main():
|
|
"""Development environment security check"""
|
|
env_manager = EnvironmentManager()
|
|
issues = env_manager.validate_environment()
|
|
|
|
if issues:
|
|
print("⚠️ Environment Security Issues:")
|
|
for issue in issues:
|
|
print(f" - {issue}")
|
|
print("\nRun ./scripts/secure_dev_setup.sh to fix common issues")
|
|
return 1
|
|
else:
|
|
print("✅ Environment security validation passed")
|
|
return 0
|
|
|
|
if __name__ == "__main__":
|
|
exit(main())
|
|
```
|
|
|
|
## Production Security Considerations
|
|
|
|
### API Rate Limiting and DoS Protection
|
|
|
|
**Request Throttling**:
|
|
```python
|
|
import asyncio
|
|
import time
|
|
from collections import defaultdict
|
|
from typing import Dict, Optional
|
|
|
|
class RateLimiter:
|
|
"""Protect against API abuse and DoS attacks"""
|
|
|
|
def __init__(self):
|
|
self.request_counts: Dict[str, list] = defaultdict(list)
|
|
self.blocked_ips: Dict[str, float] = {}
|
|
self.rate_limits = {
|
|
"default": (100, 60), # 100 requests per 60 seconds
|
|
"openrouter": (50, 60), # 50 LLM requests per 60 seconds
|
|
"database": (1000, 60), # 1000 DB operations per 60 seconds
|
|
}
|
|
|
|
async def check_rate_limit(
|
|
self,
|
|
identifier: str,
|
|
category: str = "default"
|
|
) -> tuple[bool, Optional[str]]:
|
|
"""Check if request should be allowed"""
|
|
|
|
# Check if identifier is temporarily blocked
|
|
if identifier in self.blocked_ips:
|
|
block_until = self.blocked_ips[identifier]
|
|
if time.time() < block_until:
|
|
return False, f"Temporarily blocked until {time.ctime(block_until)}"
|
|
else:
|
|
del self.blocked_ips[identifier]
|
|
|
|
# Get rate limit for category
|
|
max_requests, window_seconds = self.rate_limits.get(
|
|
category, self.rate_limits["default"]
|
|
)
|
|
|
|
# Clean old requests outside window
|
|
now = time.time()
|
|
cutoff = now - window_seconds
|
|
self.request_counts[identifier] = [
|
|
req_time for req_time in self.request_counts[identifier]
|
|
if req_time > cutoff
|
|
]
|
|
|
|
# Check if within limits
|
|
current_count = len(self.request_counts[identifier])
|
|
if current_count >= max_requests:
|
|
# Block for increasing duration based on violations
|
|
violation_count = getattr(self, f"_{identifier}_violations", 0) + 1
|
|
setattr(self, f"_{identifier}_violations", violation_count)
|
|
|
|
block_duration = min(300, 30 * violation_count) # Max 5 minutes
|
|
self.blocked_ips[identifier] = now + block_duration
|
|
|
|
return False, f"Rate limit exceeded. Blocked for {block_duration} seconds"
|
|
|
|
# Record this request
|
|
self.request_counts[identifier].append(now)
|
|
return True, None
|
|
|
|
async def check_api_health(self) -> dict:
|
|
"""Monitor for suspicious patterns"""
|
|
now = time.time()
|
|
|
|
# Count recent requests across all identifiers
|
|
recent_requests = 0
|
|
for requests in self.request_counts.values():
|
|
recent_requests += len([r for r in requests if r > now - 60])
|
|
|
|
# Calculate metrics
|
|
total_blocked = len(self.blocked_ips)
|
|
active_identifiers = len([
|
|
requests for requests in self.request_counts.values()
|
|
if any(r > now - 300 for r in requests) # Active in last 5 minutes
|
|
])
|
|
|
|
status = "healthy"
|
|
if recent_requests > 500: # Threshold for concern
|
|
status = "high_load"
|
|
if total_blocked > 10:
|
|
status = "under_attack"
|
|
|
|
return {
|
|
"status": status,
|
|
"recent_requests_per_minute": recent_requests,
|
|
"blocked_identifiers": total_blocked,
|
|
"active_identifiers": active_identifiers,
|
|
"timestamp": now
|
|
}
|
|
```
|
|
|
|
### Audit Logging and Compliance
|
|
|
|
**Security Event Logging**:
|
|
```python
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Any, Dict, Optional
|
|
|
|
class SecurityEventType(Enum):
|
|
AUTH_SUCCESS = "auth_success"
|
|
AUTH_FAILURE = "auth_failure"
|
|
DATA_ACCESS = "data_access"
|
|
DATA_EXPORT = "data_export"
|
|
CONFIG_CHANGE = "config_change"
|
|
API_ABUSE = "api_abuse"
|
|
SYSTEM_ERROR = "system_error"
|
|
|
|
class SecurityAuditor:
|
|
"""Centralized security event logging for compliance"""
|
|
|
|
def __init__(self):
|
|
# Separate logger for security events
|
|
self.security_logger = logging.getLogger("tradingagents.security")
|
|
|
|
# Configure structured logging handler
|
|
handler = logging.FileHandler("logs/security.log")
|
|
formatter = SecurityLogFormatter()
|
|
handler.setFormatter(formatter)
|
|
self.security_logger.addHandler(handler)
|
|
self.security_logger.setLevel(logging.INFO)
|
|
|
|
def log_event(
|
|
self,
|
|
event_type: SecurityEventType,
|
|
message: str,
|
|
user_id: Optional[str] = None,
|
|
ip_address: Optional[str] = None,
|
|
resource: Optional[str] = None,
|
|
additional_data: Optional[Dict[str, Any]] = None
|
|
) -> None:
|
|
"""Log security event with structured data"""
|
|
|
|
event_data = {
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"event_type": event_type.value,
|
|
"message": message,
|
|
"severity": self._get_severity(event_type),
|
|
"user_id": user_id or "system",
|
|
"ip_address": ip_address or "unknown",
|
|
"resource": resource,
|
|
"additional_data": additional_data or {},
|
|
"process_id": os.getpid(),
|
|
"hostname": os.uname().nodename
|
|
}
|
|
|
|
# Log at appropriate level based on severity
|
|
if event_data["severity"] == "critical":
|
|
self.security_logger.critical(json.dumps(event_data))
|
|
elif event_data["severity"] == "warning":
|
|
self.security_logger.warning(json.dumps(event_data))
|
|
else:
|
|
self.security_logger.info(json.dumps(event_data))
|
|
|
|
def _get_severity(self, event_type: SecurityEventType) -> str:
|
|
"""Determine event severity"""
|
|
critical_events = {
|
|
SecurityEventType.AUTH_FAILURE,
|
|
SecurityEventType.API_ABUSE,
|
|
SecurityEventType.CONFIG_CHANGE
|
|
}
|
|
|
|
if event_type in critical_events:
|
|
return "critical"
|
|
elif event_type == SecurityEventType.SYSTEM_ERROR:
|
|
return "warning"
|
|
else:
|
|
return "info"
|
|
|
|
def log_data_access(
|
|
self,
|
|
table: str,
|
|
operation: str,
|
|
record_count: int,
|
|
user_id: str = "system"
|
|
) -> None:
|
|
"""Log data access for compliance auditing"""
|
|
self.log_event(
|
|
SecurityEventType.DATA_ACCESS,
|
|
f"Database {operation} on {table}",
|
|
user_id=user_id,
|
|
resource=table,
|
|
additional_data={
|
|
"operation": operation,
|
|
"record_count": record_count
|
|
}
|
|
)
|
|
|
|
def log_api_key_usage(
|
|
self,
|
|
provider: str,
|
|
model: str,
|
|
tokens_used: int,
|
|
cost_estimate: float
|
|
) -> None:
|
|
"""Log LLM API usage for cost monitoring and abuse detection"""
|
|
self.log_event(
|
|
SecurityEventType.DATA_ACCESS,
|
|
f"LLM API call to {provider}/{model}",
|
|
resource=f"{provider}/{model}",
|
|
additional_data={
|
|
"tokens_used": tokens_used,
|
|
"cost_estimate": cost_estimate,
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
}
|
|
)
|
|
|
|
class SecurityLogFormatter(logging.Formatter):
|
|
"""Custom formatter for security logs"""
|
|
|
|
def format(self, record: logging.LogRecord) -> str:
|
|
# Security logs are already JSON formatted
|
|
return record.getMessage()
|
|
|
|
# Usage in repository classes
|
|
class NewsRepository:
|
|
def __init__(self, database_manager: DatabaseManager):
|
|
self.db_manager = database_manager
|
|
self.auditor = SecurityAuditor()
|
|
|
|
async def list(self, symbol: str, date: date) -> list[NewsArticle]:
|
|
# ... existing implementation ...
|
|
|
|
# Log data access for compliance
|
|
self.auditor.log_data_access(
|
|
table="news_articles",
|
|
operation="SELECT",
|
|
record_count=len(result),
|
|
user_id=getattr(self, 'current_user_id', 'system')
|
|
)
|
|
|
|
return result
|
|
```
|
|
|
|
This comprehensive security standards document provides the foundation for protecting sensitive financial data, API keys, and system resources while maintaining compliance with data protection regulations in the TradingAgents system. |