""" Security Configuration Management Gère la configuration sécurisée du système, particulièrement en production. Exigence 7.1: Forcer la configuration sécurisée en production Exigence 7.5: Refus de démarrage avec configuration insécurisée """ import os import logging import secrets import hashlib from dataclasses import dataclass from typing import Dict, List, Optional, Set from pathlib import Path logger = logging.getLogger(__name__) @dataclass class SecurityConfig: """Configuration de sécurité du système.""" # Environnement environment: str = "development" # Chiffrement encryption_password: Optional[str] = None encryption_key_file: Optional[str] = None # Clés secrètes secret_key: Optional[str] = None jwt_secret: Optional[str] = None # Base de données db_encryption_enabled: bool = False db_password: Optional[str] = None # API api_key_required: bool = False api_keys: List[str] = None # Logging log_sensitive_data: bool = True # False en production # Validation strict_input_validation: bool = False # True en production def __post_init__(self): """Post-initialisation pour valider la configuration.""" if self.api_keys is None: self.api_keys = [] class SecurityConfigError(Exception): """Erreur de configuration de sécurité.""" pass class ProductionSecurityError(SecurityConfigError): """Erreur de sécurité critique en production.""" pass def is_production_environment() -> bool: """ Détermine si nous sommes en environnement de production. Returns: True si en production """ env = os.getenv("ENVIRONMENT", "development").lower() return env in ["production", "prod"] def get_security_config() -> SecurityConfig: """ Charge la configuration de sécurité depuis les variables d'environnement. Returns: Configuration de sécurité """ config = SecurityConfig( environment=os.getenv("ENVIRONMENT", "development").lower(), encryption_password=os.getenv("ENCRYPTION_PASSWORD"), encryption_key_file=os.getenv("ENCRYPTION_KEY_FILE"), secret_key=os.getenv("SECRET_KEY"), jwt_secret=os.getenv("JWT_SECRET"), db_encryption_enabled=os.getenv("DB_ENCRYPTION_ENABLED", "false").lower() == "true", db_password=os.getenv("DB_PASSWORD"), api_key_required=os.getenv("API_KEY_REQUIRED", "false").lower() == "true", api_keys=os.getenv("API_KEYS", "").split(",") if os.getenv("API_KEYS") else [], log_sensitive_data=os.getenv("LOG_SENSITIVE_DATA", "true").lower() == "true", strict_input_validation=os.getenv("STRICT_INPUT_VALIDATION", "false").lower() == "true" ) return config def _is_default_key(key: str, known_defaults: Set[str]) -> bool: """ Vérifie si une clé est une clé par défaut connue. Args: key: Clé à vérifier known_defaults: Ensemble des clés par défaut connues Returns: True si c'est une clé par défaut """ if not key: return True return key in known_defaults def _is_weak_key(key: str, min_length: int = 32) -> bool: """ Vérifie si une clé est faible. Args: key: Clé à vérifier min_length: Longueur minimale requise Returns: True si la clé est faible """ if not key or len(key) < min_length: return True # Si la clé semble être générée automatiquement (base64-like), être moins strict if len(key) >= min_length and all(c.isalnum() or c in "+-_=" for c in key): # Clé générée automatiquement, probablement sécurisée return False # Vérifier la complexité pour les clés manuelles has_upper = any(c.isupper() for c in key) has_lower = any(c.islower() for c in key) has_digit = any(c.isdigit() for c in key) has_special = any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in key) complexity_score = sum([has_upper, has_lower, has_digit, has_special]) return complexity_score < 2 # Au moins 2 types de caractères (moins strict) def ensure_dev_config(config: SecurityConfig) -> SecurityConfig: """ S'assure que la config de développement a des valeurs par défaut sécurisées. Génère automatiquement des clés temporaires si non définies. Args: config: Configuration à compléter Returns: Configuration avec valeurs par défaut """ if is_production_environment(): return config # Générer des clés temporaires pour le développement si non définies if not config.encryption_password: config.encryption_password = f"dev_encryption_{secrets.token_hex(16)}" logger.info("Generated temporary ENCRYPTION_PASSWORD for development") os.environ["ENCRYPTION_PASSWORD"] = config.encryption_password if not config.secret_key: config.secret_key = f"dev_secret_{secrets.token_hex(16)}" logger.info("Generated temporary SECRET_KEY for development") os.environ["SECRET_KEY"] = config.secret_key return config def validate_production_security(config: SecurityConfig, strict: bool = True) -> None: """ Valide la configuration de sécurité pour la production. Args: config: Configuration à valider strict: Si False, ne lève pas d'exception (warnings seulement) Raises: ProductionSecurityError: Si la configuration n'est pas sécurisée (mode strict + production) """ if not is_production_environment(): logger.info("Development environment - applying permissive security validation") # En dev, on génère des clés temporaires si nécessaire ensure_dev_config(config) return logger.info("Validating production security configuration...") errors = [] warnings = [] # Clés par défaut connues default_encryption_keys = { "rpa_vision_v3_default_key", "default_password", "changeme", "password", "secret" } default_secret_keys = { "dev-key-change-in-production", "development-secret", "default-secret", "changeme" } # 1. Validation du mot de passe de chiffrement if not config.encryption_password: errors.append("ENCRYPTION_PASSWORD must be set in production") elif _is_default_key(config.encryption_password, default_encryption_keys): errors.append("ENCRYPTION_PASSWORD cannot use default value in production") elif _is_weak_key(config.encryption_password, 32): errors.append("ENCRYPTION_PASSWORD is too weak (minimum 32 characters with complexity required)") # 2. Validation de la clé secrète if not config.secret_key: errors.append("SECRET_KEY must be set in production") elif _is_default_key(config.secret_key, default_secret_keys): errors.append("SECRET_KEY cannot use default value in production") elif _is_weak_key(config.secret_key, 32): errors.append("SECRET_KEY is too weak (minimum 32 characters with complexity required)") # 3. Validation JWT si utilisé if config.jwt_secret: if _is_default_key(config.jwt_secret, default_secret_keys): errors.append("JWT_SECRET cannot use default value in production") elif _is_weak_key(config.jwt_secret, 32): errors.append("JWT_SECRET is too weak") # 4. Validation base de données if config.db_password and _is_weak_key(config.db_password, 16): warnings.append("DB_PASSWORD appears to be weak") # 5. Configuration de sécurité if config.log_sensitive_data: warnings.append("LOG_SENSITIVE_DATA should be false in production") if not config.strict_input_validation: warnings.append("STRICT_INPUT_VALIDATION should be true in production") # 6. Validation des clés API if config.api_key_required and not config.api_keys: errors.append("API_KEY_REQUIRED is true but no API_KEYS provided") for api_key in config.api_keys: if _is_weak_key(api_key, 24): warnings.append(f"API key appears to be weak: {api_key[:8]}...") # 7. Validation des fichiers de clés if config.encryption_key_file: key_path = Path(config.encryption_key_file) if not key_path.exists(): errors.append(f"Encryption key file not found: {config.encryption_key_file}") elif key_path.stat().st_mode & 0o077: warnings.append(f"Encryption key file has overly permissive permissions: {config.encryption_key_file}") # Afficher les warnings for warning in warnings: logger.warning(f"Security warning: {warning}") # Lever une erreur si des erreurs critiques (mode strict uniquement) if errors: error_msg = "Production security validation failed:\n" + "\n".join(f" - {error}" for error in errors) logger.error(error_msg) if strict: raise ProductionSecurityError(error_msg) else: logger.warning("Strict mode disabled - continuing despite security errors") return logger.info("Production security validation passed") def generate_secure_key(length: int = 32) -> str: """ Génère une clé sécurisée. Args: length: Longueur de la clé Returns: Clé sécurisée """ return secrets.token_urlsafe(length) def hash_sensitive_value(value: str) -> str: """ Hash une valeur sensible pour le logging sécurisé. Args: value: Valeur à hasher Returns: Hash de la valeur """ return hashlib.sha256(value.encode()).hexdigest()[:16] def validate_and_get_config() -> SecurityConfig: """ Valide et retourne la configuration de sécurité. Returns: Configuration validée Raises: ProductionSecurityError: Si la validation échoue en production """ config = get_security_config() validate_production_security(config) return config def check_security_requirements() -> Dict[str, bool]: """ Vérifie les exigences de sécurité. Returns: Dict avec le statut de chaque exigence """ config = get_security_config() is_prod = is_production_environment() requirements = { "production_environment": is_prod, "encryption_password_set": bool(config.encryption_password), "encryption_password_secure": not _is_default_key( config.encryption_password or "", {"rpa_vision_v3_default_key", "default_password", "changeme"} ), "secret_key_set": bool(config.secret_key), "secret_key_secure": not _is_default_key( config.secret_key or "", {"dev-key-change-in-production", "development-secret"} ), "logging_secure": not config.log_sensitive_data if is_prod else True, "input_validation_strict": config.strict_input_validation if is_prod else True } return requirements