"""
Scanner de Vulnérabilités de Dépendances - Web Sentinel
======================================================

Détecte les composants vulnérables et outdated.
Couvre OWASP A06 - Vulnerable and Outdated Components.
"""

from typing import Iterable, List, Dict, Optional
import requests
import json
import re
from pathlib import Path
import hashlib

from web_sentinel.model import Finding, ScanRequest

# Base de données CVE connues (exemple - en production, utiliser une vraie DB)
KNOWN_VULNERABILITIES = {
    "jquery": {
        "1.6.0": ["CVE-2011-4969"],
        "1.7.1": ["CVE-2012-6662"], 
        "2.2.0": ["CVE-2015-9251"],
        "3.0.0": ["CVE-2019-11358"]
    },
    "bootstrap": {
        "3.0.0": ["CVE-2018-14040", "CVE-2018-14041"],
        "4.0.0": ["CVE-2018-14042"]
    },
    "angular": {
        "1.2.0": ["CVE-2019-10768"],
        "1.6.0": ["CVE-2020-7676"]
    }
}

def evaluate_vulnerable_components(request: ScanRequest) -> Iterable[Finding]:
    """
    Analyse les composants et dépendances pour détecter les vulnérabilités.
    
    Vérifie :
    - Bibliothèques JavaScript outdated
    - Frameworks avec CVE connues
    - Dépendances server-side exposées
    - Headers révélant des versions vulnérables
    """
    findings: List[Finding] = []
    
    findings.extend(_scan_javascript_libraries(request))
    findings.extend(_check_server_headers(request))
    findings.extend(_scan_common_frameworks(request))
    
    if request.allow_invasive:
        findings.extend(_probe_version_endpoints(request))
    
    return findings

def _scan_javascript_libraries(request: ScanRequest) -> List[Finding]:
    """Scanne les bibliothèques JavaScript pour détecter les versions vulnérables."""
    findings = []
    base_url = f"https://{request.domain}"
    
    # Fichiers JavaScript communs à analyser
    js_paths = [
        "/js/jquery.min.js", "/js/jquery.js", "/jquery.min.js",
        "/js/bootstrap.min.js", "/js/bootstrap.js", "/bootstrap.min.js", 
        "/js/angular.min.js", "/js/angular.js", "/angular.min.js",
        "/assets/js/app.js", "/static/js/main.js"
    ]
    
    for js_path in js_paths:
        try:
            response = requests.get(f"{base_url}{js_path}", timeout=request.timeout)
            if response.status_code == 200:
                findings.extend(_analyze_js_content(js_path, response.text))
                
        except requests.exceptions.RequestException:
            continue
    
    return findings

def _analyze_js_content(file_path: str, content: str) -> List[Finding]:
    """Analyse le contenu JavaScript pour identifier les versions et vulnérabilités."""
    findings = []
    
    # Patterns pour détecter les versions de bibliothèques
    version_patterns = {
        "jquery": [
            r'jQuery\s+v?(\d+\.\d+\.\d+)',
            r'jquery[,\s]+(\d+\.\d+\.\d+)',
            r'@version\s+(\d+\.\d+\.\d+)'
        ],
        "bootstrap": [
            r'Bootstrap\s+v?(\d+\.\d+\.\d+)',
            r'bootstrap[,\s]+(\d+\.\d+\.\d+)'
        ],
        "angular": [
            r'AngularJS\s+v?(\d+\.\d+\.\d+)',
            r'angular[,\s]+(\d+\.\d+\.\d+)'
        ]
    }
    
    for library, patterns in version_patterns.items():
        for pattern in patterns:
            matches = re.finditer(pattern, content, re.IGNORECASE)
            for match in matches:
                version = match.group(1)
                vulnerabilities = _check_library_vulnerabilities(library, version)
                
                if vulnerabilities:
                    severity = "critical" if any("CVE" in vuln for vuln in vulnerabilities) else "high"
                    findings.append(Finding(
                        check="vulnerable_components",
                        title=f"Bibliothèque {library} vulnérable : v{version}",
                        severity=severity,
                        description=f"La bibliothèque {library} version {version} contient des vulnérabilités connues.",
                        remediation=f"Mettre à jour {library} vers la dernière version stable.",
                        evidence=f"Fichier: {file_path} - CVEs: {', '.join(vulnerabilities)}"
                    ))
    
    return findings

def _check_library_vulnerabilities(library: str, version: str) -> List[str]:
    """Vérifie si une version de bibliothèque a des vulnérabilités connues."""
    if library.lower() in KNOWN_VULNERABILITIES:
        library_vulns = KNOWN_VULNERABILITIES[library.lower()]
        if version in library_vulns:
            return library_vulns[version]
    
    # Vérification des versions connues comme vulnérables
    vulnerable_versions = {
        "jquery": ["1.6.0", "1.7.1", "2.2.0", "3.0.0"],
        "bootstrap": ["3.0.0", "4.0.0"], 
        "angular": ["1.2.0", "1.6.0"]
    }
    
    if library.lower() in vulnerable_versions:
        if version in vulnerable_versions[library.lower()]:
            return [f"Known vulnerable version: {version}"]
    
    return []

def _check_server_headers(request: ScanRequest) -> List[Finding]:
    """Vérifie les headers HTTP pour des versions de serveur vulnérables."""
    findings = []
    
    try:
        response = requests.head(f"https://{request.domain}", timeout=request.timeout)
        headers = response.headers
        
        # Headers révélant des versions de serveur
        version_headers = {
            "Server": _analyze_server_header,
            "X-Powered-By": _analyze_powered_by_header,
            "X-AspNet-Version": _analyze_aspnet_header,
            "X-Generator": _analyze_generator_header
        }
        
        for header, analyzer in version_headers.items():
            if header in headers:
                vulns = analyzer(headers[header])
                findings.extend(vulns)
                
    except requests.exceptions.RequestException:
        pass
    
    return findings

def _analyze_server_header(server_value: str) -> List[Finding]:
    """Analyse le header Server pour détecter des versions vulnérables."""
    findings = []
    
    # Patterns pour extraire les versions
    server_patterns = [
        (r'Apache/(\d+\.\d+\.\d+)', 'apache'),
        (r'nginx/(\d+\.\d+\.\d+)', 'nginx'),
        (r'Microsoft-IIS/(\d+\.\d+)', 'iis'),
        (r'lighttpd/(\d+\.\d+\.\d+)', 'lighttpd')
    ]
    
    for pattern, server_type in server_patterns:
        match = re.search(pattern, server_value)
        if match:
            version = match.group(1)
            if _is_vulnerable_server_version(server_type, version):
                findings.append(Finding(
                    check="vulnerable_components",
                    title=f"Version de serveur {server_type} vulnérable : {version}",
                    severity="high",
                    description=f"Le serveur {server_type} version {version} contient des vulnérabilités connues.",
                    remediation=f"Mettre à jour {server_type} vers la dernière version stable.",
                    evidence=f"Header Server: {server_value}"
                ))
    
    return findings

def _analyze_powered_by_header(powered_by_value: str) -> List[Finding]:
    """Analyse le header X-Powered-By."""
    findings = []
    
    # Versions PHP vulnérables connues
    php_match = re.search(r'PHP/(\d+\.\d+\.\d+)', powered_by_value)
    if php_match:
        php_version = php_match.group(1)
        if _is_vulnerable_php_version(php_version):
            findings.append(Finding(
                check="vulnerable_components",
                title=f"Version PHP vulnérable : {php_version}",
                severity="critical",
                description=f"PHP {php_version} contient des vulnérabilités de sécurité connues.",
                remediation="Mettre à jour PHP vers une version supportée et récente.",
                evidence=f"Header X-Powered-By: {powered_by_value}"
            ))
    
    return findings

def _analyze_aspnet_header(aspnet_value: str) -> List[Finding]:
    """Analyse le header X-AspNet-Version."""
    findings = []
    
    # Versions ASP.NET vulnérables
    vulnerable_aspnet = ["1.1.4322", "2.0.50727", "4.0.30319"]
    
    if aspnet_value in vulnerable_aspnet:
        findings.append(Finding(
            check="vulnerable_components",
            title=f"Version ASP.NET obsolète : {aspnet_value}",
            severity="high", 
            description="Cette version d'ASP.NET n'est plus supportée et contient des vulnérabilités.",
            remediation="Migrer vers une version récente de .NET/.NET Core.",
            evidence=f"Header X-AspNet-Version: {aspnet_value}"
        ))
    
    return findings

def _analyze_generator_header(generator_value: str) -> List[Finding]:
    """Analyse le header X-Generator (CMS, frameworks)."""
    findings = []
    
    # CMS et leurs versions vulnérables
    cms_patterns = [
        (r'WordPress\s+(\d+\.\d+\.\d+)', 'wordpress'),
        (r'Drupal\s+(\d+\.\d+)', 'drupal'),
        (r'Joomla!\s+(\d+\.\d+\.\d+)', 'joomla')
    ]
    
    for pattern, cms_type in cms_patterns:
        match = re.search(pattern, generator_value)
        if match:
            version = match.group(1)
            if _is_vulnerable_cms_version(cms_type, version):
                findings.append(Finding(
                    check="vulnerable_components",
                    title=f"CMS {cms_type} vulnérable : v{version}",
                    severity="critical",
                    description=f"{cms_type} {version} contient des vulnérabilités connues.",
                    remediation=f"Mettre à jour {cms_type} et tous ses plugins.",
                    evidence=f"Header X-Generator: {generator_value}"
                ))
    
    return findings

def _scan_common_frameworks(request: ScanRequest) -> List[Finding]:
    """Scanne les frameworks web courants."""
    findings = []
    base_url = f"https://{request.domain}"
    
    # Endpoints révélant des frameworks
    framework_endpoints = [
        "/package.json", "/.well-known/security.txt",
        "/composer.json", "/bower.json", "/yarn.lock"
    ]
    
    for endpoint in framework_endpoints:
        try:
            response = requests.get(f"{base_url}{endpoint}", timeout=request.timeout)
            if response.status_code == 200:
                findings.extend(_analyze_dependency_file(endpoint, response.text))
                
        except requests.exceptions.RequestException:
            continue
    
    return findings

def _analyze_dependency_file(file_path: str, content: str) -> List[Finding]:
    """Analyse les fichiers de dépendances (package.json, composer.json, etc.)."""
    findings = []
    
    try:
        if file_path.endswith('.json'):
            data = json.loads(content)
            
            # Analyser package.json
            if 'dependencies' in data or 'devDependencies' in data:
                deps = {**(data.get('dependencies', {})), **(data.get('devDependencies', {}))}
                for package, version in deps.items():
                    if _is_vulnerable_npm_package(package, version):
                        findings.append(Finding(
                            check="vulnerable_components",
                            title=f"Package npm vulnérable : {package}@{version}",
                            severity="high",
                            description=f"Le package {package} version {version} contient des vulnérabilités.",
                            remediation=f"Mettre à jour {package} vers une version sécurisée.",
                            evidence=f"Fichier: {file_path}"
                        ))
                        
    except json.JSONDecodeError:
        pass
    
    return findings

def _probe_version_endpoints(request: ScanRequest) -> List[Finding]:
    """Sonde des endpoints qui révèlent des versions (mode invasif)."""
    findings = []
    base_url = f"https://{request.domain}"
    
    # Endpoints qui révèlent souvent des versions
    version_endpoints = [
        "/version", "/api/version", "/admin/version",
        "/status", "/health", "/info", "/debug"
    ]
    
    for endpoint in version_endpoints:
        try:
            response = requests.get(f"{base_url}{endpoint}", timeout=request.timeout)
            if response.status_code == 200:
                # Rechercher des patterns de version dans la réponse
                version_patterns = [
                    r'version["\s:]+(\d+\.\d+\.\d+)',
                    r'v(\d+\.\d+\.\d+)',
                    r'build["\s:]+(\d+\.\d+\.\d+)'
                ]
                
                for pattern in version_patterns:
                    matches = re.finditer(pattern, response.text, re.IGNORECASE)
                    for match in matches:
                        findings.append(Finding(
                            check="vulnerable_components",
                            title=f"Version d'application exposée : {endpoint}",
                            severity="medium",
                            description=f"L'endpoint {endpoint} révèle des informations de version.",
                            remediation="Masquer les informations de version dans les endpoints publics.",
                            evidence=f"Version trouvée: {match.group(1)}"
                        ))
                        
        except requests.exceptions.RequestException:
            continue
    
    return findings

# Fonctions d'aide pour vérifier les versions vulnérables

def _is_vulnerable_server_version(server_type: str, version: str) -> bool:
    """Vérifie si une version de serveur est vulnérable."""
    vulnerable_versions = {
        "apache": ["2.4.1", "2.4.2", "2.4.3"],  # Exemples
        "nginx": ["1.3.9", "1.5.0"],
        "iis": ["6.0", "7.0"]
    }
    return version in vulnerable_versions.get(server_type, [])

def _is_vulnerable_php_version(version: str) -> bool:
    """Vérifie si une version PHP est vulnérable."""
    # Versions PHP non supportées ou avec CVE critiques
    vulnerable_php = ["5.6", "7.0", "7.1", "7.2"]
    major_minor = ".".join(version.split(".")[:2])
    return major_minor in vulnerable_php

def _is_vulnerable_cms_version(cms_type: str, version: str) -> bool:
    """Vérifie si une version de CMS est vulnérable."""
    vulnerable_cms = {
        "wordpress": ["4.9.0", "5.0.0", "5.1.0"],
        "drupal": ["7.0", "8.0", "8.5"],
        "joomla": ["3.4.0", "3.6.0"]
    }
    return version in vulnerable_cms.get(cms_type, [])

def _is_vulnerable_npm_package(package: str, version: str) -> bool:
    """Vérifie si un package npm est vulnérable."""
    # Base de données simplifiée - en production, utiliser une vraie CVE database
    vulnerable_packages = {
        "lodash": ["4.17.11", "4.17.12"],
        "moment": ["2.19.0", "2.19.1"],
        "request": ["2.88.0"]
    }
    return package in vulnerable_packages and version in vulnerable_packages[package]