"""
Scanner de Conception Non Sécurisée - Web Sentinel
==================================================

Détecte les défauts de conception et architecture de sécurité.
Couvre OWASP A04 - Insecure Design.
"""

from typing import Iterable, List, Dict, Optional
import requests
import re
import json
from urllib.parse import urljoin, urlparse
import time

from web_sentinel.model import Finding, ScanRequest

# Patterns de mauvaises pratiques de conception
INSECURE_DESIGN_PATTERNS = {
    'weak_rate_limiting': [
        r'rate.*limit.*\d+.*per.*second',
        r'throttle.*\d+.*requests',
        r'limit.*1000.*per.*minute'  # Limites trop élevées
    ],
    'weak_session_management': [
        r'session.*timeout.*\d+.*hours',  # Sessions trop longues
        r'remember.*me.*forever',
        r'auto.*login.*permanent'
    ],
    'information_disclosure': [
        r'debug.*mode.*true',
        r'verbose.*error.*true', 
        r'stack.*trace.*enabled',
        r'development.*environment'
    ]
}

# Endpoints critiques nécessitant une conception sécurisée
CRITICAL_ENDPOINTS = [
    '/admin', '/administrator', '/manager', '/control-panel',
    '/api/admin', '/api/management', '/api/internal',
    '/reset-password', '/change-password', '/forgot-password',
    '/backup', '/dump', '/export', '/download',
    '/upload', '/file-manager', '/files'
]

def evaluate_insecure_design(request: ScanRequest) -> Iterable[Finding]:
    """
    Analyse les défauts de conception sécurisée.
    
    Vérifie :
    - Architecture de sécurité défaillante
    - Absence de rate limiting approprié
    - Gestion de session non sécurisée
    - Validation insuffisante des entrées
    - Modèle de menace inadéquat
    - Design patterns non sécurisés
    """
    findings: List[Finding] = []
    
    findings.extend(_check_security_architecture(request))
    findings.extend(_analyze_rate_limiting(request))
    findings.extend(_check_session_design(request))
    findings.extend(_evaluate_input_validation_design(request))
    findings.extend(_scan_error_handling_design(request))
    
    if request.allow_invasive:
        findings.extend(_test_business_logic_flaws(request))
        findings.extend(_probe_workflow_security(request))
    
    return findings

def _check_security_architecture(request: ScanRequest) -> List[Finding]:
    """Vérifie l'architecture de sécurité globale."""
    findings = []
    base_url = f"https://{request.domain}"
    
    try:
        # Analyser la page d'accueil pour des indices d'architecture
        response = requests.get(base_url, timeout=request.timeout)
        
        if response.status_code == 200:
            findings.extend(_analyze_security_headers_design(response.headers))
            findings.extend(_analyze_application_architecture(response.text))
            
    except requests.exceptions.RequestException:
        pass
    
    # Vérifier les endpoints critiques
    findings.extend(_check_critical_endpoints_design(request))
    
    return findings

def _analyze_security_headers_design(headers: dict) -> List[Finding]:
    """Analyse la conception des headers de sécurité."""
    findings = []
    
    # Headers de sécurité essentiels manquants
    essential_headers = {
        'X-Frame-Options': 'Protection contre le clickjacking',
        'X-Content-Type-Options': 'Protection contre le MIME sniffing',
        'X-XSS-Protection': 'Protection XSS basique',
        'Referrer-Policy': 'Contrôle des informations de référence',
        'Permissions-Policy': 'Contrôle des permissions du navigateur'
    }
    
    for header, description in essential_headers.items():
        if header not in headers:
            findings.append(Finding(
                check="insecure_design",
                title=f"Header de sécurité manquant : {header}",
                severity="medium",
                description=f"Le header {header} n'est pas configuré. {description}.",
                remediation=f"Implémenter le header {header} avec une politique appropriée.",
                evidence=f"Header manquant: {header}"
            ))
    
    # Vérifier les valeurs des headers présents
    csp_header = headers.get('Content-Security-Policy', '')
    if csp_header and 'unsafe-inline' in csp_header:
        findings.append(Finding(
            check="insecure_design",
            title="Content Security Policy permissive",
            severity="medium",
            description="La CSP autorise 'unsafe-inline' qui réduit la protection XSS.",
            remediation="Utiliser des nonces ou des hash au lieu d'unsafe-inline.",
            evidence=f"CSP: {csp_header[:100]}..."
        ))
    
    return findings

def _analyze_application_architecture(content: str) -> List[Finding]:
    """Analyse l'architecture applicative visible."""
    findings = []
    
    # Détecter les frameworks et leur configuration
    framework_patterns = [
        (r'express.*session.*{.*}', 'Express.js session configuration'),
        (r'django.*settings.*debug.*=.*true', 'Django debug mode'),
        (r'spring.*security.*config', 'Spring Security configuration'),
        (r'passport.*initialize', 'Passport.js authentication')
    ]
    
    for pattern, description in framework_patterns:
        if re.search(pattern, content, re.IGNORECASE | re.DOTALL):
            findings.append(Finding(
                check="insecure_design", 
                title=f"Configuration de framework exposée",
                severity="medium",
                description=f"{description} visible dans le code source client.",
                remediation="Ne pas exposer les détails de configuration côté client.",
                evidence=f"Pattern détecté: {description}"
            ))
    
    return findings

def _check_critical_endpoints_design(request: ScanRequest) -> List[Finding]:
    """Vérifie la conception sécurisée des endpoints critiques."""
    findings = []
    base_url = f"https://{request.domain}"
    
    for endpoint in CRITICAL_ENDPOINTS:
        try:
            response = requests.get(f"{base_url}{endpoint}", 
                                  timeout=request.timeout, allow_redirects=False)
            
            # Un endpoint critique ne devrait pas être accessible sans authentification
            if response.status_code == 200:
                findings.append(Finding(
                    check="insecure_design",
                    title=f"Endpoint critique accessible : {endpoint}",
                    severity="high",
                    description=f"L'endpoint {endpoint} est accessible sans authentification apparente.",
                    remediation="Implémenter une authentification forte et une autorisation appropriée.",
                    evidence=f"HTTP {response.status_code} sur {endpoint}"
                ))
            
            # Vérifier les redirections de sécurité
            elif response.status_code in [301, 302, 307, 308]:
                location = response.headers.get('Location', '')
                if not location.startswith('https://'):
                    findings.append(Finding(
                        check="insecure_design",
                        title=f"Redirection non sécurisée : {endpoint}",
                        severity="medium",
                        description=f"L'endpoint {endpoint} redirige vers un protocole non sécurisé.",
                        remediation="Toutes les redirections doivent utiliser HTTPS.",
                        evidence=f"Redirection vers: {location}"
                    ))
                    
        except requests.exceptions.RequestException:
            continue
    
    return findings

def _analyze_rate_limiting(request: ScanRequest) -> List[Finding]:
    """Analyse la conception du rate limiting."""
    findings = []
    base_url = f"https://{request.domain}"
    
    # Tester les endpoints sensibles au rate limiting
    sensitive_endpoints = [
        '/login', '/api/login', '/auth', '/signin',
        '/register', '/api/register', '/signup',
        '/password-reset', '/forgot-password',
        '/api/search', '/search'
    ]
    
    for endpoint in sensitive_endpoints:
        try:
            # Effectuer plusieurs requêtes pour tester le rate limiting
            rate_limit_detected = False
            
            for i in range(5):  # Test bénin avec 5 requêtes
                response = requests.get(f"{base_url}{endpoint}", timeout=request.timeout)
                
                # Vérifier les headers de rate limiting
                if any(header in response.headers for header in 
                       ['X-RateLimit-Limit', 'X-Rate-Limit', 'Retry-After']):
                    rate_limit_detected = True
                    break
                
                if response.status_code == 429:  # Too Many Requests
                    rate_limit_detected = True
                    break
                    
                time.sleep(0.1)  # Petite pause entre les requêtes
            
            if not rate_limit_detected and endpoint in ['/login', '/api/login']:
                findings.append(Finding(
                    check="insecure_design",
                    title=f"Rate limiting absent : {endpoint}",
                    severity="high",
                    description=f"L'endpoint critique {endpoint} ne semble pas implémenter de rate limiting.",
                    remediation="Implémenter un rate limiting approprié pour les endpoints sensibles.",
                    evidence=f"Aucune limitation détectée sur {endpoint}"
                ))
                
        except requests.exceptions.RequestException:
            continue
    
    return findings

def _check_session_design(request: ScanRequest) -> List[Finding]:
    """Vérifie la conception de la gestion de session."""
    findings = []
    base_url = f"https://{request.domain}"
    
    try:
        response = requests.get(base_url, timeout=request.timeout)
        
        # Analyser les cookies de session
        for cookie in response.cookies:
            findings.extend(_analyze_session_cookie_design(cookie))
            
        # Vérifier la politique de session dans les scripts
        if 'sessionStorage' in response.text or 'localStorage' in response.text:
            session_storage_usage = re.findall(r'sessionStorage\.setItem\([^)]+\)', response.text)
            local_storage_usage = re.findall(r'localStorage\.setItem\([^)]+\)', response.text)
            
            if session_storage_usage or local_storage_usage:
                findings.append(Finding(
                    check="insecure_design",
                    title="Stockage de session côté client détecté",
                    severity="medium",
                    description="L'application utilise le stockage local/session du navigateur.",
                    remediation="Préférer les sessions côté serveur avec cookies sécurisés.",
                    evidence=f"Usage détecté: {len(session_storage_usage + local_storage_usage)} occurrences"
                ))
                
    except requests.exceptions.RequestException:
        pass
    
    return findings

def _analyze_session_cookie_design(cookie) -> List[Finding]:
    """Analyse la conception d'un cookie de session."""
    findings = []
    
    # Vérifier si c'est probablement un cookie de session
    session_indicators = ['session', 'sess', 'sid', 'jsessionid', 'phpsessid']
    
    if any(indicator in cookie.name.lower() for indicator in session_indicators):
        # Vérifier les attributs de sécurité
        if not cookie.secure:
            findings.append(Finding(
                check="insecure_design",
                title=f"Cookie de session non sécurisé : {cookie.name}",
                severity="high",
                description="Le cookie de session n'a pas l'attribut Secure.",
                remediation="Configurer l'attribut Secure pour tous les cookies de session.",
                evidence=f"Cookie: {cookie.name}"
            ))
        
        if not hasattr(cookie, 'httponly') or not cookie.httponly:
            findings.append(Finding(
                check="insecure_design",
                title=f"Cookie de session accessible en JavaScript : {cookie.name}",
                severity="medium",
                description="Le cookie de session n'a pas l'attribut HttpOnly.",
                remediation="Configurer l'attribut HttpOnly pour empêcher l'accès JavaScript.",
                evidence=f"Cookie: {cookie.name}"
            ))
    
    return findings

def _evaluate_input_validation_design(request: ScanRequest) -> List[Finding]:
    """Évalue la conception de la validation des entrées."""
    findings = []
    base_url = f"https://{request.domain}"
    
    # Chercher des formulaires pour analyser leur validation
    try:
        response = requests.get(base_url, timeout=request.timeout)
        
        if response.status_code == 200:
            findings.extend(_analyze_form_validation_design(response.text))
            
    except requests.exceptions.RequestException:
        pass
    
    return findings

def _analyze_form_validation_design(content: str) -> List[Finding]:
    """Analyse la conception de la validation des formulaires."""
    findings = []
    
    # Extraire les formulaires
    form_pattern = r'<form[^>]*>.*?</form>'
    forms = re.findall(form_pattern, content, re.IGNORECASE | re.DOTALL)
    
    for form in forms:
        # Vérifier la validation côté client uniquement
        if 'required' in form and 'onsubmit' not in form:
            # Formulaire avec seulement validation HTML5
            findings.append(Finding(
                check="insecure_design",
                title="Validation uniquement côté client détectée",
                severity="medium",
                description="Un formulaire semble n'avoir que la validation HTML5 côté client.",
                remediation="Implémenter une validation côté serveur en plus de la validation cliente.",
                evidence="Validation HTML5 sans validation JavaScript visible"
            ))
        
        # Détecter les patterns de validation faible
        weak_patterns = [
            (r'minlength="[1-3]"', "Longueur minimale trop faible"),
            (r'pattern="[^"]{1,10}"', "Expression régulière trop simple"),
            (r'type="text".*password', "Champ mot de passe en texte")
        ]
        
        for pattern, description in weak_patterns:
            if re.search(pattern, form, re.IGNORECASE):
                findings.append(Finding(
                    check="insecure_design",
                    title=f"Validation faible détectée",
                    severity="medium",
                    description=f"{description} dans un formulaire.",
                    remediation="Renforcer les critères de validation.",
                    evidence=f"Pattern détecté: {pattern}"
                ))
    
    return findings

def _scan_error_handling_design(request: ScanRequest) -> List[Finding]:
    """Scanne la conception de la gestion d'erreurs."""
    findings = []
    base_url = f"https://{request.domain}"
    
    # Tester des URLs qui génèrent typiquement des erreurs
    error_test_urls = [
        '/nonexistent-page-12345',
        '/api/nonexistent',
        '/../../../etc/passwd',
        '/admin/../../secret'
    ]
    
    for test_url in error_test_urls:
        try:
            response = requests.get(f"{base_url}{test_url}", timeout=request.timeout)
            
            if response.status_code in [404, 500, 403]:
                findings.extend(_analyze_error_response_design(test_url, response))
                
        except requests.exceptions.RequestException:
            continue
    
    return findings

def _analyze_error_response_design(url: str, response: requests.Response) -> List[Finding]:
    """Analyse la conception des réponses d'erreur."""
    findings = []
    
    error_content = response.text.lower()
    
    # Patterns révélant trop d'informations
    information_disclosure_patterns = [
        (r'stack trace', 'Stack trace'),
        (r'exception.*at.*line', 'Détails d\'exception'),
        (r'mysql.*error', 'Erreur de base de données'),
        (r'postgresql.*error', 'Erreur PostgreSQL'),
        (r'file.*not.*found.*/', 'Chemin de fichier exposé'),
        (r'debug.*mode', 'Mode debug activé')
    ]
    
    for pattern, description in information_disclosure_patterns:
        if re.search(pattern, error_content):
            findings.append(Finding(
                check="insecure_design",
                title=f"Divulgation d'informations dans les erreurs",
                severity="medium",
                description=f"{description} visible dans les pages d'erreur.",
                remediation="Configurer des pages d'erreur génériques sans détails techniques.",
                evidence=f"URL: {url}, Pattern: {description}"
            ))
    
    return findings

def _test_business_logic_flaws(request: ScanRequest) -> List[Finding]:
    """Teste les défauts de logique métier (mode invasif)."""
    findings = []
    base_url = f"https://{request.domain}"
    
    # Tests de logique métier basiques
    business_logic_tests = [
        ('/api/users/-1', 'ID utilisateur négatif'),
        ('/api/products/0', 'ID produit zéro'),
        ('/api/orders/999999999', 'ID commande très élevé'),
        ('/download?file=../../../etc/passwd', 'Traversée de répertoire')
    ]
    
    for test_url, test_description in business_logic_tests:
        try:
            response = requests.get(f"{base_url}{test_url}", timeout=request.timeout)
            
            # Analyser les réponses inattendues
            if response.status_code == 200 and len(response.text) > 0:
                findings.append(Finding(
                    check="insecure_design",
                    title=f"Défaut de logique métier potentiel : {test_description}",
                    severity="medium",
                    description=f"L'application répond de manière inattendue au test : {test_description}.",
                    remediation="Implémenter une validation stricte des paramètres métier.",
                    evidence=f"URL: {test_url}, Status: {response.status_code}"
                ))
                
        except requests.exceptions.RequestException:
            continue
    
    return findings

def _probe_workflow_security(request: ScanRequest) -> List[Finding]:
    """Sonde la sécurité des workflows (mode invasif)."""
    findings = []
    base_url = f"https://{request.domain}"
    
    # Endpoints de workflow critiques
    workflow_endpoints = [
        '/api/approve', '/api/reject', '/api/submit',
        '/api/publish', '/api/activate', '/api/confirm',
        '/workflow/next', '/workflow/skip', '/workflow/complete'
    ]
    
    for endpoint in workflow_endpoints:
        try:
            # Tenter d'accéder sans authentification
            response = requests.get(f"{base_url}{endpoint}", timeout=request.timeout)
            
            if response.status_code in [200, 405]:  # 405 = Method Not Allowed (endpoint exists)
                findings.append(Finding(
                    check="insecure_design",
                    title=f"Endpoint de workflow accessible : {endpoint}",
                    severity="medium",
                    description=f"L'endpoint de workflow {endpoint} est accessible.",
                    remediation="Sécuriser tous les endpoints de workflow avec authentification et autorisation.",
                    evidence=f"Status: {response.status_code}"
                ))
                
        except requests.exceptions.RequestException:
            continue
    
    return findings