"""
Scanner de Défaillances Cryptographiques - Web Sentinel
=======================================================

Détecte les échecs cryptographiques et algorithmes faibles.
Couvre OWASP A02 - Cryptographic Failures.
"""

from typing import Iterable, List, Set
import requests
import ssl
import socket
import hashlib
import base64
import re
from urllib.parse import urlparse, urljoin

from web_sentinel.model import Finding, ScanRequest

# Algorithmes cryptographiques faibles ou obsolètes
WEAK_CIPHERS = {
    'DES', '3DES', 'RC4', 'MD5', 'SHA1', 
    'NULL', 'aNULL', 'eNULL', 'EXPORT'
}

WEAK_TLS_VERSIONS = {'SSLv2', 'SSLv3', 'TLSv1.0', 'TLSv1.1'}

WEAK_HASH_ALGORITHMS = {'md5', 'sha1', 'md4'}

def evaluate_cryptographic_failures(request: ScanRequest) -> Iterable[Finding]:
    """
    Analyse les défaillances cryptographiques.
    
    Vérifie :
    - Protocoles SSL/TLS obsolètes
    - Chiffrements faibles 
    - Hachage d'mots de passe faibles
    - Données sensibles transmises en plain text
    - Certificats SSL invalides ou faibles
    - Génération de nombres aléatoires prévisibles
    """
    findings: List[Finding] = []
    
    findings.extend(_check_ssl_tls_configuration(request))
    findings.extend(_check_certificate_strength(request))
    findings.extend(_check_data_transmission(request))
    findings.extend(_scan_weak_encryption_patterns(request))
    
    if request.allow_invasive:
        findings.extend(_test_password_storage(request))
        findings.extend(_probe_encryption_endpoints(request))
    
    return findings

def _check_ssl_tls_configuration(request: ScanRequest) -> List[Finding]:
    """Vérifie la configuration SSL/TLS du serveur."""
    findings = []
    hostname = request.domain
    port = 443
    
    try:
        # Test des protocoles SSL/TLS supportés
        supported_protocols = _get_supported_tls_protocols(hostname, port)
        
        for protocol in supported_protocols:
            if protocol in WEAK_TLS_VERSIONS:
                findings.append(Finding(
                    check="cryptographic_failures",
                    title=f"Protocole TLS obsolète supporté : {protocol}",
                    severity="high" if protocol.startswith('SSL') else "medium",
                    description=f"Le serveur supporte le protocole obsolète {protocol} qui contient des vulnérabilités.",
                    remediation="Désactiver les protocoles SSLv2, SSLv3, TLSv1.0 et TLSv1.1. Utiliser uniquement TLS 1.2+.",
                    evidence=f"Protocole détecté: {protocol}"
                ))
        
        # Vérification des cipher suites
        weak_ciphers = _get_weak_cipher_suites(hostname, port)
        
        for cipher in weak_ciphers:
            findings.append(Finding(
                check="cryptographic_failures",
                title=f"Cipher suite faible : {cipher}",
                severity="high",
                description=f"Le serveur supporte le cipher suite faible {cipher}.",
                remediation="Configurer uniquement des cipher suites sécurisées (AES, ChaCha20).",
                evidence=f"Cipher faible: {cipher}"
            ))
            
    except Exception as e:
        findings.append(Finding(
            check="cryptographic_failures",
            title="Erreur lors de l'analyse SSL/TLS",
            severity="medium",
            description=f"Impossible d'analyser la configuration SSL/TLS : {e}",
            remediation="Vérifier la connectivité et la configuration SSL du serveur.",
            evidence=str(e)
        ))
    
    return findings

def _check_certificate_strength(request: ScanRequest) -> List[Finding]:
    """Vérifie la robustesse du certificat SSL."""
    findings = []
    hostname = request.domain
    
    try:
        # Récupérer le certificat
        context = ssl.create_default_context()
        with socket.create_connection((hostname, 443), timeout=request.timeout) as sock:
            with context.wrap_socket(sock, server_hostname=hostname) as ssock:
                cert = ssock.getpeercert()
                cert_der = ssock.getpeercert_raw()
                
                # Analyser la force du certificat
                findings.extend(_analyze_certificate_strength(cert, cert_der))
                
    except Exception as e:
        findings.append(Finding(
            check="cryptographic_failures", 
            title="Certificat SSL inaccessible",
            severity="medium",
            description=f"Impossible d'analyser le certificat SSL : {e}",
            remediation="Vérifier la validité et l'accessibilité du certificat SSL.",
            evidence=str(e)
        ))
    
    return findings

def _analyze_certificate_strength(cert_info: dict, cert_der: bytes) -> List[Finding]:
    """Analyse la robustesse d'un certificat SSL."""
    findings = []
    
    # Vérifier l'algorithme de signature
    if 'signatureAlgorithm' in cert_info:
        sig_algo = cert_info['signatureAlgorithm'].lower()
        if any(weak in sig_algo for weak in ['md5', 'sha1']):
            findings.append(Finding(
                check="cryptographic_failures",
                title=f"Algorithme de signature faible : {sig_algo}",
                severity="high",
                description="Le certificat utilise un algorithme de signature cryptographiquement faible.",
                remediation="Renouveler le certificat avec un algorithme SHA-256 ou supérieur.",
                evidence=f"Algorithme détecté: {sig_algo}"
            ))
    
    # Vérifier la taille de clé RSA
    cert_pem = ssl.DER_cert_to_PEM_cert(cert_der)
    key_size = _extract_rsa_key_size(cert_pem)
    
    if key_size and key_size < 2048:
        findings.append(Finding(
            check="cryptographic_failures",
            title=f"Taille de clé RSA insuffisante : {key_size} bits",
            severity="high",
            description=f"La clé RSA de {key_size} bits est insuffisante selon les standards actuels.",
            remediation="Utiliser une clé RSA d'au moins 2048 bits, recommandé 4096 bits.",
            evidence=f"Taille de clé: {key_size} bits"
        ))
    
    return findings

def _check_data_transmission(request: ScanRequest) -> List[Finding]:
    """Vérifie la transmission sécurisée des données."""
    findings = []
    base_url = f"https://{request.domain}"
    
    # Pages sensibles à vérifier
    sensitive_pages = [
        "/login", "/signin", "/auth", "/register", "/signup",
        "/admin", "/dashboard", "/profile", "/account", "/password"
    ]
    
    for page in sensitive_pages:
        try:
            # Vérifier si la page existe en HTTPS
            https_response = requests.get(f"{base_url}{page}", timeout=request.timeout, allow_redirects=False)
            
            # Tenter l'accès en HTTP pour vérifier les redirections
            try:
                http_response = requests.get(f"http://{request.domain}{page}", 
                                           timeout=request.timeout, allow_redirects=False)
                
                if http_response.status_code == 200:
                    findings.append(Finding(
                        check="cryptographic_failures",
                        title=f"Page sensible accessible en HTTP : {page}",
                        severity="critical",
                        description=f"La page {page} contenant potentiellement des données sensibles est accessible en HTTP non chiffré.",
                        remediation="Forcer HTTPS pour toutes les pages sensibles avec redirection automatique.",
                        evidence=f"HTTP Status: {http_response.status_code}"
                    ))
                        
            except requests.exceptions.RequestException:
                pass  # HTTP non disponible, c'est bien
                
        except requests.exceptions.RequestException:
            continue
    
    return findings

def _scan_weak_encryption_patterns(request: ScanRequest) -> List[Finding]:
    """Scanne les patterns d'encryption faible dans le code source."""
    findings = []
    base_url = f"https://{request.domain}"
    
    # Fichiers JavaScript à analyser pour des patterns faibles
    js_files = [
        "/js/main.js", "/js/app.js", "/assets/js/application.js",
        "/static/js/script.js", "/js/crypto.js", "/js/security.js"
    ]
    
    for js_file in js_files:
        try:
            response = requests.get(f"{base_url}{js_file}", timeout=request.timeout)
            if response.status_code == 200:
                findings.extend(_analyze_crypto_patterns(js_file, response.text))
                
        except requests.exceptions.RequestException:
            continue
    
    return findings

def _analyze_crypto_patterns(file_path: str, content: str) -> List[Finding]:
    """Analyse les patterns cryptographiques faibles dans le code."""
    findings = []
    
    # Patterns dangereux dans le JavaScript
    weak_crypto_patterns = [
        (r'Math\.random\(\)', "Générateur de nombres pseudo-aléatoires faible"),
        (r'btoa\(.*password', "Encodage Base64 pour mot de passe (non sécurisé)"),
        (r'md5\(', "Utilisation de MD5 (algorithme compromis)"),
        (r'sha1\(', "Utilisation de SHA1 (algorithme faible)"), 
        (r'DES\.|3DES\.', "Utilisation de DES/3DES (chiffrement obsolète)"),
        (r'RC4\.', "Utilisation de RC4 (chiffrement cassé)"),
        (r'eval\(.*crypto', "Utilisation d'eval avec crypto (dangereux)"),
        (r'localStorage\.setItem.*password', "Stockage mot de passe en localStorage"),
        (r'sessionStorage\.setItem.*password', "Stockage mot de passe en sessionStorage")
    ]
    
    for pattern, description in weak_crypto_patterns:
        matches = re.finditer(pattern, content, re.IGNORECASE)
        for match in matches:
            findings.append(Finding(
                check="cryptographic_failures",
                title=f"Pattern cryptographique faible détecté",
                severity="high" if "password" in description.lower() else "medium",
                description=f"{description} trouvé dans {file_path}.",
                remediation="Utiliser des bibliothèques cryptographiques sécurisées (Web Crypto API, bcrypt).",
                evidence=f"Pattern: {match.group(0)}"
            ))
    
    return findings

def _test_password_storage(request: ScanRequest) -> List[Finding]:
    """Teste la sécurité du stockage des mots de passe (mode invasif)."""
    findings = []
    base_url = f"https://{request.domain}"
    
    # Endpoints d'API à tester
    password_endpoints = [
        "/api/password", "/api/auth/password", "/api/user/password",
        "/password-reset", "/change-password", "/forgot-password"
    ]
    
    for endpoint in password_endpoints:
        try:
            # Test avec payload bénin pour analyser les erreurs
            test_payload = {"password": "test123", "old_password": "old123"}
            
            response = requests.post(f"{base_url}{endpoint}", 
                                   json=test_payload, timeout=request.timeout)
            
            # Analyser la réponse pour des indices sur le stockage
            if response.status_code in [200, 400, 401]:
                findings.extend(_analyze_password_response(endpoint, response))
                
        except requests.exceptions.RequestException:
            continue
    
    return findings

def _analyze_password_response(endpoint: str, response: requests.Response) -> List[Finding]:
    """Analyse la réponse pour détecter des problèmes de stockage de mot de passe."""
    findings = []
    
    response_text = response.text.lower()
    
    # Patterns indiquant un stockage non sécurisé
    insecure_patterns = [
        ("password in plain text", "Mot de passe en texte clair"),
        ("md5", "Hachage MD5 utilisé"),
        ("sha1", "Hachage SHA1 utilisé"),
        ("base64", "Encodage Base64 (non sécurisé)"),
        ("current password:", "Mot de passe actuel révélé")
    ]
    
    for pattern, description in insecure_patterns:
        if pattern in response_text:
            findings.append(Finding(
                check="cryptographic_failures",
                title=f"Stockage de mot de passe non sécurisé : {endpoint}",
                severity="critical",
                description=f"{description} détecté dans la réponse de {endpoint}.",
                remediation="Utiliser bcrypt, scrypt ou Argon2 pour hasher les mots de passe.",
                evidence=f"Pattern détecté: {pattern}"
            ))
    
    return findings

def _probe_encryption_endpoints(request: ScanRequest) -> List[Finding]:
    """Sonde les endpoints liés au chiffrement (mode invasif)."""
    findings = []
    base_url = f"https://{request.domain}"
    
    # Endpoints crypto à tester
    crypto_endpoints = [
        "/api/encrypt", "/api/decrypt", "/api/hash", "/api/crypto",
        "/encrypt", "/decrypt", "/hash", "/security/crypto"
    ]
    
    for endpoint in crypto_endpoints:
        try:
            # Test avec données bénignes
            test_data = {"data": "test123", "key": "testkey"}
            
            response = requests.post(f"{base_url}{endpoint}",
                                   json=test_data, timeout=request.timeout)
            
            if response.status_code == 200:
                findings.extend(_analyze_crypto_endpoint(endpoint, response))
                
        except requests.exceptions.RequestException:
            continue
    
    return findings

def _analyze_crypto_endpoint(endpoint: str, response: requests.Response) -> List[Finding]:
    """Analyse un endpoint cryptographique pour détecter des faiblesses."""
    findings = []
    
    try:
        response_data = response.json()
        
        # Vérifier si des clés ou algorithmes sont exposés
        sensitive_keys = ['key', 'secret', 'algorithm', 'cipher', 'hash']
        
        for key in sensitive_keys:
            if key in response_data:
                findings.append(Finding(
                    check="cryptographic_failures",
                    title=f"Information cryptographique exposée : {endpoint}",
                    severity="high",
                    description=f"L'endpoint {endpoint} expose des informations cryptographiques sensibles.",
                    remediation="Ne jamais exposer les clés, algorithmes ou détails d'implémentation crypto.",
                    evidence=f"Clé exposée: {key}"
                ))
                
    except (ValueError, KeyError):
        pass
    
    return findings

# Fonctions utilitaires

def _get_supported_tls_protocols(hostname: str, port: int) -> Set[str]:
    """Récupère les protocoles TLS supportés par le serveur."""
    supported = set()
    
    protocols = [
        ('SSLv2', ssl.PROTOCOL_SSLv23),
        ('SSLv3', ssl.PROTOCOL_SSLv23), 
        ('TLSv1.0', ssl.PROTOCOL_TLSv1),
        ('TLSv1.1', ssl.PROTOCOL_TLSv1_1),
        ('TLSv1.2', ssl.PROTOCOL_TLSv1_2)
    ]
    
    try:
        protocols.append(('TLSv1.3', ssl.PROTOCOL_TLS))
    except AttributeError:
        pass
    
    for name, protocol in protocols:
        try:
            context = ssl.SSLContext(protocol)
            with socket.create_connection((hostname, port), timeout=5) as sock:
                with context.wrap_socket(sock, server_hostname=hostname):
                    supported.add(name)
        except (ssl.SSLError, socket.error, OSError):
            continue
    
    return supported

def _get_weak_cipher_suites(hostname: str, port: int) -> Set[str]:
    """Récupère les cipher suites faibles supportées."""
    weak_ciphers = set()
    
    try:
        context = ssl.create_default_context()
        context.set_ciphers('ALL:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA')
        
        with socket.create_connection((hostname, port), timeout=5) as sock:
            with context.wrap_socket(sock, server_hostname=hostname) as ssock:
                cipher = ssock.cipher()
                if cipher:
                    cipher_name = cipher[0]
                    # Vérifier si le cipher contient des éléments faibles
                    for weak in WEAK_CIPHERS:
                        if weak.lower() in cipher_name.lower():
                            weak_ciphers.add(cipher_name)
                            
    except (ssl.SSLError, socket.error, OSError):
        pass
    
    return weak_ciphers

def _extract_rsa_key_size(cert_pem: str) -> int:
    """Extrait la taille de la clé RSA du certificat PEM."""
    try:
        # Pattern pour extraire la taille de clé RSA
        rsa_pattern = r'RSA Public Key: \((\d+) bit\)'
        match = re.search(rsa_pattern, cert_pem)
        if match:
            return int(match.group(1))
    except Exception:
        pass
    
    return None