#!/usr/bin/env python3
"""
Donjon - Vulnerability Scanner
Comprehensive vulnerability assessment using nuclei and nmap scripts.
"""

import re
import json
import sys
from pathlib import Path
from datetime import datetime, timezone
from typing import Dict, List, Optional

sys.path.insert(0, str(Path(__file__).parent.parent / 'lib'))

try:
    from .base import BaseScanner
except ImportError:
    from base import BaseScanner


class VulnerabilityScanner(BaseScanner):
    """Vulnerability assessment scanner."""

    SCANNER_NAME = "vulnerability"
    SCANNER_DESCRIPTION = "Vulnerability assessment using nuclei and nmap"

    # Severity mapping for nuclei
    NUCLEI_SEVERITY = {
        'critical': 'CRITICAL',
        'high': 'HIGH',
        'medium': 'MEDIUM',
        'low': 'LOW',
        'info': 'INFO'
    }

    # CVSS score mapping for severities
    CVSS_MAP = {
        'CRITICAL': 9.5,
        'HIGH': 7.5,
        'MEDIUM': 5.5,
        'LOW': 3.0,
        'INFO': 0.0
    }

    # Timeout map per scan type (seconds)
    TIMEOUTS = {
        'quick':    {'nuclei': 120, 'nmap_vuln': 60,  'nmap_script': '30s'},
        'standard': {'nuclei': 900, 'nmap_vuln': 600, 'nmap_script': '60s'},
        'deep':     {'nuclei': 1800, 'nmap_vuln': 1200, 'nmap_script': '120s'},
    }

    def __init__(self, session_id: Optional[str] = None):
        super().__init__(session_id)
        self.nuclei_path = None
        self.nmap_path = None

    def scan(self, targets: List[str], scan_type: str = 'standard',
             include_cves: bool = True, **kwargs) -> Dict:
        """
        Execute vulnerability scan.

        Args:
            targets: List of hosts/IPs to scan
            scan_type: 'quick', 'standard', or 'deep'
            include_cves: Include CVE-specific checks
        """
        self.start_time = datetime.now(timezone.utc)
        self.scan_status = 'running'
        timeouts = self.TIMEOUTS.get(scan_type, self.TIMEOUTS['standard'])

        # Defensive: wrap string in list
        if isinstance(targets, str):
            targets = [targets]

        results = {
            'scan_type': scan_type,
            'targets': targets,
            'vulnerabilities': [],
            'summary': {}
        }

        # Check available tools
        has_nuclei = self.check_tool('nuclei')
        has_nmap = self.check_tool('nmap')

        if not has_nuclei and not has_nmap:
            self.scan_logger.error("Neither nuclei nor nmap available")
            self.set_status('failed', 'No vulnerability scanning tools available (nuclei/nmap)')
            results['scan_status'] = self.scan_status
            results['warnings'] = self.warnings
            results['findings'] = self.findings
            return results

        # Phase 1: Nuclei scan (if available)
        if has_nuclei:
            self.nuclei_path = self.paths.find_tool('nuclei')
            self.scan_logger.info("Phase 1: Nuclei vulnerability scan")

            for target in targets:
                vulns = self._run_nuclei(target, scan_type, include_cves, timeouts['nuclei'])
                results['vulnerabilities'].extend(vulns)
                self.human_delay()

        # Phase 2: Nmap vulnerability scripts (if available)
        if has_nmap:
            self.nmap_path = self.paths.find_tool('nmap')
            self.scan_logger.info("Phase 2: Nmap vulnerability scripts")

            for target in targets:
                vulns = self._run_nmap_vuln(target, scan_type, timeouts)
                results['vulnerabilities'].extend(vulns)
                self.human_delay()

        # Generate summary
        results['summary'] = self._generate_summary(results['vulnerabilities'])

        self.end_time = datetime.now(timezone.utc)
        self.set_status('complete')
        self.save_results()

        results['scan_status'] = self.scan_status
        results['warnings'] = self.warnings
        results['findings'] = self.findings

        return results

    def _run_nuclei(self, target: str, scan_type: str, include_cves: bool,
                    timeout: int = 900) -> List[Dict]:
        """Run nuclei vulnerability scanner."""
        self.scan_logger.info(f"Running nuclei on {target}")

        vulnerabilities = []

        # Build nuclei command
        cmd = [
            str(self.nuclei_path),
            '-target', target,
            '-json',
            '-silent',
            '-no-color'
        ]

        # Add severity filter based on scan type
        if scan_type == 'quick':
            cmd.extend(['-severity', 'critical,high'])
        elif scan_type == 'standard':
            cmd.extend(['-severity', 'critical,high,medium'])
        # 'deep' includes all severities

        # Add template categories
        if include_cves:
            cmd.extend(['-tags', 'cve'])
        else:
            cmd.extend(['-tags', 'misconfig,exposure,default-login'])

        # Rate limiting for human-like behavior
        cmd.extend(['-rate-limit', '10'])

        try:
            result = self.run_tool(cmd, timeout=timeout, description=f"Nuclei scan on {target}")

            # Parse JSON lines output
            for line in result.stdout.split('\n'):
                if line.strip():
                    try:
                        finding = json.loads(line)
                        vuln = self._parse_nuclei_finding(finding, target)
                        if vuln:
                            vulnerabilities.append(vuln)
                            self._add_vuln_finding(vuln)
                    except json.JSONDecodeError:
                        continue

        except Exception as e:
            self.scan_logger.warning(f"Nuclei scan error: {e}")

        return vulnerabilities

    def _parse_nuclei_finding(self, finding: Dict, target: str) -> Optional[Dict]:
        """Parse a nuclei finding into standard format."""
        try:
            template_id = finding.get('template-id', 'unknown')
            info = finding.get('info', {})

            severity = self.NUCLEI_SEVERITY.get(
                info.get('severity', 'info').lower(), 'INFO'
            )

            # Extract CVE IDs if present
            cve_ids = []
            classification = info.get('classification', {})
            if 'cve-id' in classification:
                cve_ids = classification['cve-id'] if isinstance(classification['cve-id'], list) else [classification['cve-id']]

            # Get CVSS score
            cvss = 0.0
            if 'cvss-score' in classification:
                cvss = float(classification['cvss-score'])
            elif not cvss:
                cvss = self.CVSS_MAP.get(severity, 0.0)

            return {
                'id': template_id,
                'title': info.get('name', template_id),
                'severity': severity,
                'description': info.get('description', ''),
                'affected_host': finding.get('host', target),
                'matched_at': finding.get('matched-at', ''),
                'cve_ids': cve_ids,
                'cvss_score': cvss,
                'remediation': info.get('remediation', ''),
                'references': info.get('reference', []),
                'tags': info.get('tags', []),
                'tool': 'nuclei'
            }
        except Exception as e:
            self.scan_logger.warning(f"Error parsing nuclei finding: {e}")
            return None

    def _run_nmap_vuln(self, target: str, scan_type: str,
                       timeouts: Dict = None) -> List[Dict]:
        """Run nmap vulnerability scripts."""
        self.scan_logger.info(f"Running nmap vuln scripts on {target}")
        if timeouts is None:
            timeouts = self.TIMEOUTS.get(scan_type, self.TIMEOUTS['standard'])

        vulnerabilities = []

        # Select scripts based on scan type
        if scan_type == 'quick':
            scripts = 'vuln'
        elif scan_type == 'standard':
            scripts = 'vuln,exploit'
        else:
            scripts = 'vuln,exploit,auth'

        cmd = [
            str(self.nmap_path),
            '-sV',
            '--script', scripts,
            '--script-timeout', timeouts['nmap_script'],
            '-T3',
            target
        ]

        try:
            result = self.run_tool(cmd, timeout=timeouts['nmap_vuln'], description=f"Nmap vuln scan on {target}")
            vulnerabilities = self._parse_nmap_vuln(result.stdout, target)
        except Exception as e:
            self.scan_logger.warning(f"Nmap vuln scan error: {e}")

        return vulnerabilities

    def _parse_nmap_vuln(self, output: str, target: str) -> List[Dict]:
        """Parse nmap vulnerability script output."""
        vulnerabilities = []
        current_host = target
        current_port = None
        current_vuln = None

        for line in output.split('\n'):
            # Host line
            if 'Nmap scan report for' in line:
                match = re.search(r'(\d+\.\d+\.\d+\.\d+)', line)
                if match:
                    current_host = match.group(1)

            # Port line
            port_match = re.match(r'^(\d+)/(tcp|udp)\s+open\s+(\S+)', line)
            if port_match:
                current_port = port_match.group(1)

            # Vulnerability indicator lines
            if '| ' in line or '|_' in line:
                vuln_line = line.replace('|_', '|').replace('| ', '').strip()

                # Look for CVE references
                cve_match = re.search(r'(CVE-\d{4}-\d+)', vuln_line, re.IGNORECASE)
                if cve_match:
                    cve_id = cve_match.group(1).upper()

                    vuln = {
                        'id': cve_id,
                        'title': f"Potential vulnerability: {cve_id}",
                        'severity': 'MEDIUM',  # Default, would need CVE lookup for accurate
                        'description': vuln_line,
                        'affected_host': current_host,
                        'affected_port': current_port,
                        'cve_ids': [cve_id],
                        'cvss_score': 5.0,  # Default
                        'tool': 'nmap'
                    }
                    vulnerabilities.append(vuln)
                    self._add_vuln_finding(vuln)

                # Look for VULNERABLE keyword
                elif 'VULNERABLE' in vuln_line.upper():
                    vuln = {
                        'id': f"nmap-vuln-{current_port}",
                        'title': vuln_line[:100],
                        'severity': 'HIGH',
                        'description': vuln_line,
                        'affected_host': current_host,
                        'affected_port': current_port,
                        'cve_ids': [],
                        'cvss_score': 7.0,
                        'tool': 'nmap'
                    }
                    vulnerabilities.append(vuln)
                    self._add_vuln_finding(vuln)

        return vulnerabilities

    def _add_vuln_finding(self, vuln: Dict):
        """Add vulnerability as a finding."""
        tool = vuln.get('tool', 'unknown')
        if tool == 'nuclei':
            detection_method = 'active_check' if vuln.get('cve_ids') else 'misconfig'
        elif tool == 'nmap':
            detection_method = 'script_vuln'
        else:
            detection_method = ''

        self.add_finding(
            severity=vuln['severity'],
            title=vuln['title'],
            description=vuln.get('description', ''),
            affected_asset=vuln['affected_host'],
            finding_type='cve_vulnerability' if vuln.get('cve_ids') else 'web_vulnerability',
            cvss_score=vuln.get('cvss_score', 0.0),
            cve_ids=vuln.get('cve_ids', []),
            remediation=vuln.get('remediation', 'Review and patch this vulnerability.'),
            raw_data=vuln,
            detection_method=detection_method
        )

    def _generate_summary(self, vulnerabilities: List[Dict]) -> Dict:
        """Generate vulnerability scan summary."""
        summary = {
            'total': len(vulnerabilities),
            'by_severity': {'CRITICAL': 0, 'HIGH': 0, 'MEDIUM': 0, 'LOW': 0, 'INFO': 0},
            'by_tool': {},
            'unique_cves': set(),
            'affected_hosts': set()
        }

        for vuln in vulnerabilities:
            sev = vuln.get('severity', 'INFO')
            summary['by_severity'][sev] = summary['by_severity'].get(sev, 0) + 1

            tool = vuln.get('tool', 'unknown')
            summary['by_tool'][tool] = summary['by_tool'].get(tool, 0) + 1

            for cve in vuln.get('cve_ids', []):
                summary['unique_cves'].add(cve)

            summary['affected_hosts'].add(vuln.get('affected_host', ''))

        summary['unique_cves'] = list(summary['unique_cves'])
        summary['affected_hosts'] = list(summary['affected_hosts'])

        return summary


if __name__ == '__main__':
    scanner = VulnerabilityScanner()
    print(f"Vulnerability Scanner initialized")
    print(f"Nuclei available: {scanner.check_tool('nuclei')}")
    print(f"Nmap available: {scanner.check_tool('nmap')}")
