"""
Scanner de Mauvaise Configuration de Sécurité - Web Sentinel
===========================================================

Détecte les configurations de sécurité incorrectes.
Couvre OWASP A05 - Security Misconfiguration.
"""

from typing import Iterable, List, Dict, Set
import requests
import re
from urllib.parse import urljoin, urlparse

from web_sentinel.model import Finding, ScanRequest

# Configurations de sécurité par défaut dangereuses
DANGEROUS_DEFAULT_CONFIGS = {
    'admin': ['admin', 'password', 'root', '123456', 'admin123'],
    'test': ['test', 'testing', 'demo', 'sample'],
    'guest': ['guest', '', 'anonymous', 'public']
}

# Headers de sécurité manquants ou mal configurés
SECURITY_HEADERS_CHECKS = {
    'Strict-Transport-Security': {
        'required': True,
        'weak_values': ['max-age=0', 'max-age=86400'],  # Trop court
        'good_pattern': r'max-age=(\d+)',
        'min_age': 31536000  # 1 an minimum
    },
    'Content-Security-Policy': {
        'required': True,
        'dangerous_values': ['unsafe-inline', 'unsafe-eval', '*'],
        'weak_pattern': r'script-src.*\'unsafe-inline\'|script-src.*\'unsafe-eval\''
    },
    'X-Frame-Options': {
        'required': True,
        'weak_values': ['ALLOWALL'],
        'acceptable': ['DENY', 'SAMEORIGIN']
    }
}

# Pages et endpoints sensibles
SENSITIVE_ENDPOINTS = [
    # Interfaces d'administration
    '/admin', '/administrator', '/admin.php', '/admin.html',
    '/manager', '/management', '/control-panel', '/cpanel',
    '/wp-admin', '/administrator/index.php', '/admin/index.php',
    
    # Outils de développement
    '/debug', '/test', '/testing', '/dev', '/development',
    '/phpinfo.php', '/info.php', '/.env', '/config',
    
    # Fichiers de sauvegarde
    '/backup', '/backups', '/.bak', '/dump', '/export',
    '/database.sql', '/db.sql', '/backup.zip',
    
    # Documentation et APIs
    '/docs', '/swagger', '/api-docs', '/openapi.json',
    '/graphql', '/graphiql', '/.well-known/openid_configuration'
]

def evaluate_security_misconfiguration(request: ScanRequest) -> Iterable[Finding]:
    """
    Analyse les mauvaises configurations de sécurité.
    
    Vérifie :
    - Headers de sécurité manquants ou mal configurés
    - Endpoints administratifs exposés
    - Configurations par défaut dangereuses
    - Informations sensibles exposées
    - Services de debug activés
    - Fichiers de configuration exposés
    """
    findings: List[Finding] = []
    
    findings.extend(_check_security_headers_config(request))
    findings.extend(_scan_exposed_admin_interfaces(request))
    findings.extend(_check_default_configurations(request))
    findings.extend(_scan_exposed_files(request))
    findings.extend(_check_error_pages_config(request))
    
    if request.allow_invasive:
        findings.extend(_test_debug_interfaces(request))
        findings.extend(_probe_configuration_endpoints(request))
    
    return findings

def _check_security_headers_config(request: ScanRequest) -> List[Finding]:
    """Vérifie la configuration des headers de sécurité."""
    findings = []
    base_url = f"https://{request.domain}"
    
    try:
        response = requests.get(base_url, timeout=request.timeout)
        headers = response.headers
        
        for header_name, config in SECURITY_HEADERS_CHECKS.items():
            header_value = headers.get(header_name, '').lower()
            
            # Header manquant
            if config['required'] and not header_value:
                findings.append(Finding(
                    check="security_misconfiguration",
                    title=f"Header de sécurité manquant : {header_name}",
                    severity="high" if header_name == 'Strict-Transport-Security' else "medium",
                    description=f"Le header {header_name} n'est pas configuré.",
                    remediation=f"Configurer le header {header_name} avec une politique appropriée.",
                    evidence=f"Header absent: {header_name}"
                ))
                continue
            
            # Valeurs dangereuses
            if 'dangerous_values' in config:
                for dangerous in config['dangerous_values']:
                    if dangerous in header_value:
                        findings.append(Finding(
                            check="security_misconfiguration",
                            title=f"Configuration dangereuse dans {header_name}",
                            severity="high",
                            description=f"Le header {header_name} contient une valeur dangereuse : {dangerous}.",
                            remediation=f"Supprimer les valeurs dangereuses du header {header_name}.",
                            evidence=f"Valeur dangereuse: {dangerous}"
                        ))
            
            # Valeurs faibles spécifiques
            if header_name == 'Strict-Transport-Security' and header_value:
                max_age_match = re.search(r'max-age=(\d+)', header_value)
                if max_age_match:
                    max_age = int(max_age_match.group(1))
                    if max_age < config['min_age']:
                        findings.append(Finding(
                            check="security_misconfiguration",
                            title="HSTS avec durée insuffisante",
                            severity="medium",
                            description=f"La durée HSTS ({max_age}s) est inférieure au minimum recommandé (1 an).",
                            remediation="Configurer HSTS avec max-age d'au moins 31536000 secondes (1 an).",
                            evidence=f"max-age actuel: {max_age}s"
                        ))
            
            # Vérification des patterns faibles
            if 'weak_pattern' in config and re.search(config['weak_pattern'], header_value):
                findings.append(Finding(
                    check="security_misconfiguration",
                    title=f"Configuration faible dans {header_name}",
                    severity="medium",
                    description=f"Le header {header_name} contient une configuration affaiblie.",
                    remediation=f"Renforcer la configuration du header {header_name}.",
                    evidence=header_value[:100]
                ))
                
    except requests.exceptions.RequestException as e:
        findings.append(Finding(
            check="security_misconfiguration",
            title="Erreur lors de la vérification des headers",
            severity="medium",
            description=f"Impossible de vérifier les headers de sécurité : {e}",
            remediation="Vérifier la connectivité et la configuration du serveur.",
            evidence=str(e)
        ))
    
    return findings

def _scan_exposed_admin_interfaces(request: ScanRequest) -> List[Finding]:
    """Scanne les interfaces d'administration exposées."""
    findings = []
    base_url = f"https://{request.domain}"
    
    admin_endpoints = [
        '/admin', '/administrator', '/admin/', '/administrator/',
        '/admin/login', '/admin/index.php', '/admin/admin.php',
        '/manager', '/management', '/control-panel',
        '/wp-admin', '/wp-admin/', '/wp-login.php'
    ]
    
    for endpoint in admin_endpoints:
        try:
            response = requests.get(f"{base_url}{endpoint}", 
                                  timeout=request.timeout, allow_redirects=False)
            
            if response.status_code == 200:
                findings.extend(_analyze_admin_interface(endpoint, response))
            elif response.status_code in [301, 302] and 'login' not in endpoint:
                # Interface admin qui redirige (probablement accessible)
                findings.append(Finding(
                    check="security_misconfiguration",
                    title=f"Interface d'administration détectée : {endpoint}",
                    severity="medium",
                    description=f"Une interface d'administration est accessible à {endpoint}.",
                    remediation="Restreindre l'accès aux interfaces d'administration par IP ou VPN.",
                    evidence=f"Redirection détectée: {response.status_code}"
                ))
                
        except requests.exceptions.RequestException:
            continue
    
    return findings

def _analyze_admin_interface(endpoint: str, response: requests.Response) -> List[Finding]:
    """Analyse une interface d'administration détectée."""
    findings = []
    content = response.text.lower()
    
    # Interface d'administration accessible
    admin_indicators = ['admin', 'administrator', 'management', 'control panel', 'dashboard']
    
    if any(indicator in content for indicator in admin_indicators):
        severity = "high"
        
        # Vérifier si elle nécessite une authentification
        auth_indicators = ['login', 'password', 'username', 'signin', 'authentication']
        
        if not any(auth in content for auth in auth_indicators):
            severity = "critical"
            findings.append(Finding(
                check="security_misconfiguration",
                title=f"Interface d'administration sans authentification : {endpoint}",
                severity=severity,
                description="Une interface d'administration est accessible sans authentification.",
                remediation="Implémenter une authentification forte et restreindre l'accès.",
                evidence=f"Interface détectée à: {endpoint}"
            ))
        else:
            findings.append(Finding(
                check="security_misconfiguration",
                title=f"Interface d'administration exposée : {endpoint}",
                severity="medium",
                description="Une interface d'administration est publiquement accessible.",
                remediation="Restreindre l'accès par whitelist IP ou utiliser un VPN.",
                evidence=f"Interface à: {endpoint}"
            ))
    
    return findings

def _check_default_configurations(request: ScanRequest) -> List[Finding]:
    """Vérifie les configurations par défaut dangereuses."""
    findings = []
    base_url = f"https://{request.domain}"
    
    # Tester les combinaisons de login par défaut
    default_login_pages = ['/login', '/admin/login', '/signin', '/auth']
    
    for login_page in default_login_pages:
        try:
            # Vérifier si la page existe
            response = requests.get(f"{base_url}{login_page}", timeout=request.timeout)
            
            if response.status_code == 200:
                findings.extend(_analyze_login_page_config(login_page, response.text))
                
        except requests.exceptions.RequestException:
            continue
    
    return findings

def _analyze_login_page_config(login_page: str, content: str) -> List[Finding]:
    """Analyse la configuration d'une page de login."""
    findings = []
    content_lower = content.lower()
    
    # Vérifier les configurations dangereuses dans le code
    dangerous_patterns = [
        (r'value="admin"', "Nom d'utilisateur par défaut 'admin'"),
        (r'value="password"', "Mot de passe par défaut 'password'"), 
        (r'placeholder="admin"', "Suggestion de login par défaut"),
        (r'guest.*login', "Mode invité activé"),
        (r'demo.*account', "Compte de démonstration disponible"),
        (r'autocomplete="on".*password', "Autocomplétion activée pour les mots de passe")
    ]
    
    for pattern, description in dangerous_patterns:
        if re.search(pattern, content, re.IGNORECASE):
            findings.append(Finding(
                check="security_misconfiguration",
                title=f"Configuration de login dangereuse : {login_page}",
                severity="medium",
                description=f"{description} détecté dans la page de login.",
                remediation="Supprimer les comptes par défaut et sécuriser la configuration.",
                evidence=f"Pattern détecté: {description}"
            ))
    
    # Vérifier l'absence de protection CSRF
    if 'csrf' not in content_lower and 'token' not in content_lower:
        findings.append(Finding(
            check="security_misconfiguration",
            title=f"Protection CSRF manquante : {login_page}",
            severity="medium",
            description="La page de login ne semble pas protégée contre les attaques CSRF.",
            remediation="Implémenter des tokens CSRF pour tous les formulaires sensibles.",
            evidence=f"Page: {login_page}"
        ))
    
    return findings

def _scan_exposed_files(request: ScanRequest) -> List[Finding]:
    """Scanne les fichiers sensibles exposés."""
    findings = []
    base_url = f"https://{request.domain}"
    
    # Fichiers sensibles couramment exposés
    sensitive_files = [
        '/.env', '/.env.local', '/.env.production',
        '/config/database.yml', '/config.php', '/configuration.php',
        '/.git/config', '/.git/HEAD', '/.gitignore',
        '/web.config', '/app.config', '/appsettings.json',
        '/phpinfo.php', '/info.php', '/test.php',
        '/backup.sql', '/database.sql', '/dump.sql',
        '/robots.txt', '/sitemap.xml', '/.htaccess',
        '/package.json', '/composer.json', '/requirements.txt'
    ]
    
    for file_path in sensitive_files:
        try:
            response = requests.get(f"{base_url}{file_path}", timeout=request.timeout)
            
            if response.status_code == 200 and len(response.text) > 0:
                findings.extend(_analyze_exposed_file(file_path, response))
                
        except requests.exceptions.RequestException:
            continue
    
    return findings

def _analyze_exposed_file(file_path: str, response: requests.Response) -> List[Finding]:
    """Analyse un fichier exposé pour détecter les informations sensibles."""
    findings = []
    content = response.text.lower()
    
    # Déterminer le niveau de criticité selon le type de fichier
    critical_files = {'.env', 'database.yml', 'config.php', 'web.config', '.git/'}
    medium_files = {'robots.txt', 'sitemap.xml', 'package.json', 'composer.json'}
    
    severity = "info"
    if any(critical in file_path for critical in critical_files):
        severity = "critical"
    elif any(medium in file_path for medium in medium_files):
        severity = "medium"
    elif 'php' in file_path or 'sql' in file_path:
        severity = "high"
    
    # Analyser le contenu pour des informations sensibles
    sensitive_patterns = [
        (r'password.*=.*["\'][^"\']+["\']', "Mot de passe en clair"),
        (r'api.*key.*["\'][^"\']+["\']', "Clé API exposée"),
        (r'secret.*["\'][^"\']+["\']', "Secret exposé"),
        (r'database.*password', "Mot de passe de base de données"),
        (r'mysql.*password', "Mot de passe MySQL"),
        (r'token.*["\'][^"\']+["\']', "Token d'authentification")
    ]
    
    evidence_details = []
    for pattern, description in sensitive_patterns:
        if re.search(pattern, content, re.IGNORECASE):
            evidence_details.append(description)
            severity = max(severity, "critical", key=lambda x: ["info", "medium", "high", "critical"].index(x))
    
    findings.append(Finding(
        check="security_misconfiguration",
        title=f"Fichier sensible exposé : {file_path}",
        severity=severity,
        description=f"Le fichier {file_path} est publiquement accessible.",
        remediation="Restreindre l'accès aux fichiers sensibles ou les déplacer hors du webroot.",
        evidence=f"Taille: {len(response.text)} octets" + 
                (f", Contenus sensibles: {', '.join(evidence_details)}" if evidence_details else "")
    ))
    
    return findings

def _check_error_pages_config(request: ScanRequest) -> List[Finding]:
    """Vérifie la configuration des pages d'erreur."""
    findings = []
    base_url = f"https://{request.domain}"
    
    # Provoquer différents types d'erreurs
    error_tests = [
        ('/nonexistent-page-404-test', 404),
        ('/forbidden-test-403', 403),
        ('/server-error-test-500', 500)
    ]
    
    for test_url, expected_code in error_tests:
        try:
            response = requests.get(f"{base_url}{test_url}", timeout=request.timeout)
            findings.extend(_analyze_error_page_config(test_url, response))
            
        except requests.exceptions.RequestException:
            continue
    
    return findings

def _analyze_error_page_config(test_url: str, response: requests.Response) -> List[Finding]:
    """Analyse la configuration des pages d'erreur."""
    findings = []
    content = response.text
    
    # Vérifier l'exposition d'informations sensibles dans les erreurs
    sensitive_error_patterns = [
        (r'stack trace', "Stack trace visible"),
        (r'exception.*line.*\d+', "Détails d'exception avec numéros de ligne"),
        (r'c:\\|/var/www/|/home/', "Chemins système exposés"),
        (r'mysql.*error|postgresql.*error', "Erreurs de base de données détaillées"),
        (r'version.*\d+\.\d+\.\d+', "Versions de logiciels exposées"),
        (r'debug.*mode.*on', "Mode debug activé")
    ]
    
    for pattern, description in sensitive_error_patterns:
        if re.search(pattern, content, re.IGNORECASE):
            findings.append(Finding(
                check="security_misconfiguration",
                title="Information sensible dans les pages d'erreur",
                severity="medium",
                description=f"{description} visible dans les pages d'erreur.",
                remediation="Configurer des pages d'erreur génériques sans informations techniques.",
                evidence=f"URL de test: {test_url}"
            ))
    
    return findings

def _test_debug_interfaces(request: ScanRequest) -> List[Finding]:
    """Teste les interfaces de debug activées (mode invasif)."""
    findings = []
    base_url = f"https://{request.domain}"
    
    debug_endpoints = [
        '/debug', '/debug/', '/debug/info', '/debug/config',
        '/_debug', '/__debug__', '/test/debug',
        '/phpinfo.php', '/info.php', '/_profiler',
        '/server-info', '/server-status', '/status'
    ]
    
    for endpoint in debug_endpoints:
        try:
            response = requests.get(f"{base_url}{endpoint}", timeout=request.timeout)
            
            if response.status_code == 200 and len(response.text) > 100:
                findings.extend(_analyze_debug_interface(endpoint, response))
                
        except requests.exceptions.RequestException:
            continue
    
    return findings

def _analyze_debug_interface(endpoint: str, response: requests.Response) -> List[Finding]:
    """Analyse une interface de debug détectée."""
    findings = []
    content = response.text.lower()
    
    debug_indicators = [
        'php version', 'debug information', 'system information',
        'environment variables', 'configuration', 'server variables',
        'loaded modules', 'memory usage', 'execution time'
    ]
    
    detected_info = [indicator for indicator in debug_indicators if indicator in content]
    
    if detected_info:
        findings.append(Finding(
            check="security_misconfiguration",
            title=f"Interface de debug active : {endpoint}",
            severity="high",
            description=f"Une interface de debug est accessible publiquement à {endpoint}.",
            remediation="Désactiver les interfaces de debug en production.",
            evidence=f"Informations exposées: {', '.join(detected_info[:3])}"
        ))
    
    return findings

def _probe_configuration_endpoints(request: ScanRequest) -> List[Finding]:
    """Sonde les endpoints de configuration (mode invasif)."""
    findings = []
    base_url = f"https://{request.domain}"
    
    config_endpoints = [
        '/config', '/configuration', '/settings', '/admin/config',
        '/api/config', '/api/settings', '/.well-known/configuration',
        '/health', '/healthcheck', '/metrics', '/stats'
    ]
    
    for endpoint in config_endpoints:
        try:
            response = requests.get(f"{base_url}{endpoint}", timeout=request.timeout)
            
            if response.status_code == 200:
                try:
                    # Tenter de parser en JSON
                    config_data = response.json()
                    findings.extend(_analyze_configuration_data(endpoint, config_data))
                except ValueError:
                    # Pas du JSON, analyser comme texte
                    findings.extend(_analyze_configuration_text(endpoint, response.text))
                    
        except requests.exceptions.RequestException:
            continue
    
    return findings

def _analyze_configuration_data(endpoint: str, config_data: dict) -> List[Finding]:
    """Analyse les données de configuration exposées."""
    findings = []
    
    # Clés sensibles à détecter
    sensitive_keys = [
        'password', 'secret', 'key', 'token', 'credential',
        'database_password', 'db_password', 'api_key',
        'private_key', 'secret_key', 'auth_token'
    ]
    
    def find_sensitive_keys(data, path=""):
        sensitive_found = []
        if isinstance(data, dict):
            for key, value in data.items():
                current_path = f"{path}.{key}" if path else key
                if any(sensitive in key.lower() for sensitive in sensitive_keys):
                    sensitive_found.append(current_path)
                sensitive_found.extend(find_sensitive_keys(value, current_path))
        elif isinstance(data, list):
            for i, item in enumerate(data):
                sensitive_found.extend(find_sensitive_keys(item, f"{path}[{i}]"))
        return sensitive_found
    
    sensitive_found = find_sensitive_keys(config_data)
    
    if sensitive_found:
        findings.append(Finding(
            check="security_misconfiguration",
            title=f"Configuration sensible exposée : {endpoint}",
            severity="critical",
            description=f"Des informations de configuration sensibles sont exposées à {endpoint}.",
            remediation="Restreindre l'accès aux endpoints de configuration et filtrer les données sensibles.",
            evidence=f"Clés sensibles: {', '.join(sensitive_found[:5])}"
        ))
    
    return findings

def _analyze_configuration_text(endpoint: str, content: str) -> List[Finding]:
    """Analyse le contenu textuel de configuration."""
    findings = []
    
    # Patterns de configuration sensible
    config_patterns = [
        (r'password.*=.*[^\s]+', "Mots de passe dans la configuration"),
        (r'secret.*=.*[^\s]+', "Secrets dans la configuration"),
        (r'key.*=.*[^\s]+', "Clés dans la configuration"),
        (r'database.*url.*password', "URL de base de données avec mot de passe")
    ]
    
    detected_patterns = []
    for pattern, description in config_patterns:
        if re.search(pattern, content, re.IGNORECASE):
            detected_patterns.append(description)
    
    if detected_patterns:
        findings.append(Finding(
            check="security_misconfiguration",
            title=f"Configuration sensible exposée : {endpoint}",
            severity="high",
            description=f"Des informations sensibles sont visibles dans la configuration à {endpoint}.",
            remediation="Masquer les informations sensibles dans les réponses de configuration.",
            evidence=f"Patterns détectés: {', '.join(detected_patterns[:3])}"
        ))
    
    return findings