"""MCP Vulnerability Database v2.

Local database of vulnerabilities specific to MCP servers and AI agent systems.
Each entry has a CVE-like ID, description, affected patterns, severity, CWE
mapping, and mitigation guidance.  Supports keyword/severity/CWE/date-range
search and can enrich scan findings by matching them against the database.

Import/Export in JSON for sharing across teams.

Zero external dependencies. Python 3.11+ stdlib only.
"""

from __future__ import annotations

import json
import re
from dataclasses import dataclass, field
from datetime import date, datetime
from enum import Enum
from typing import Any


# ---------------------------------------------------------------------------
# Enums & Data Models
# ---------------------------------------------------------------------------

class VulnSeverity(Enum):
    CRITICAL = "CRITICAL"
    HIGH = "HIGH"
    MEDIUM = "MEDIUM"
    LOW = "LOW"
    INFO = "INFO"

    @classmethod
    def from_string(cls, value: str) -> VulnSeverity:
        try:
            return cls[value.upper()]
        except KeyError:
            valid = ", ".join(s.name.lower() for s in cls)
            raise ValueError(f"Invalid severity '{value}'. Valid: {valid}")


@dataclass
class VulnEntry:
    id: str
    title: str
    description: str
    severity: VulnSeverity
    affected_patterns: list[str] = field(default_factory=list)
    cwe_ids: list[str] = field(default_factory=list)
    mitigation: str = ""
    references: list[str] = field(default_factory=list)
    tags: list[str] = field(default_factory=list)
    rule_ids: list[str] = field(default_factory=list)
    published: str = ""
    updated: str = ""
    exploitability: str = "UNKNOWN"
    affected_languages: list[str] = field(default_factory=list)
    cvss_score: float = 0.0

    def to_dict(self) -> dict[str, Any]:
        return {
            "id": self.id, "title": self.title, "description": self.description,
            "severity": self.severity.value, "affected_patterns": self.affected_patterns,
            "cwe_ids": self.cwe_ids, "mitigation": self.mitigation,
            "references": self.references, "tags": self.tags, "rule_ids": self.rule_ids,
            "published": self.published, "updated": self.updated,
            "exploitability": self.exploitability,
            "affected_languages": self.affected_languages, "cvss_score": self.cvss_score,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> VulnEntry:
        return cls(
            id=data["id"], title=data["title"], description=data["description"],
            severity=VulnSeverity.from_string(data["severity"]),
            affected_patterns=data.get("affected_patterns", []),
            cwe_ids=data.get("cwe_ids", []), mitigation=data.get("mitigation", ""),
            references=data.get("references", []), tags=data.get("tags", []),
            rule_ids=data.get("rule_ids", []), published=data.get("published", ""),
            updated=data.get("updated", ""),
            exploitability=data.get("exploitability", "UNKNOWN"),
            affected_languages=data.get("affected_languages", []),
            cvss_score=float(data.get("cvss_score", 0.0)),
        )


@dataclass
class EnrichedFinding:
    finding: dict[str, Any]
    matched_vulns: list[VulnEntry] = field(default_factory=list)
    risk_score: float = 0.0
    enrichment_notes: list[str] = field(default_factory=list)

    @property
    def is_enriched(self) -> bool:
        return len(self.matched_vulns) > 0

    @property
    def max_severity(self) -> str:
        if not self.matched_vulns:
            return self.finding.get("severity", "INFO")
        order = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]
        best = "INFO"
        for v in self.matched_vulns:
            if order.index(v.severity.value) < order.index(best):
                best = v.severity.value
        return best

    def to_dict(self) -> dict[str, Any]:
        return {
            "finding": self.finding,
            "matched_vulns": [v.to_dict() for v in self.matched_vulns],
            "risk_score": self.risk_score, "enrichment_notes": self.enrichment_notes,
            "is_enriched": self.is_enriched, "max_severity": self.max_severity,
        }


@dataclass
class VulnSearchResult:
    entries: list[VulnEntry] = field(default_factory=list)
    query: str = ""
    total_in_db: int = 0

    @property
    def count(self) -> int:
        return len(self.entries)

    def to_dict(self) -> dict[str, Any]:
        return {
            "query": self.query, "total_in_db": self.total_in_db,
            "results_count": self.count,
            "entries": [e.to_dict() for e in self.entries],
        }


# ---------------------------------------------------------------------------
# Built-in Vulnerability Database (32 entries)
# ---------------------------------------------------------------------------

def _build_vuln_db() -> list[VulnEntry]:
    _E = VulnEntry
    _S = VulnSeverity
    return [
        _E(id="MCP-VULN-001", title="Confused Deputy via Unvalidated Tool Inputs",
           description="MCP server runs tool actions based on LLM-provided parameters without validating authorization.",
           severity=_S.CRITICAL, affected_patterns=["tool_handler(*args)", "def handle_tool(", "@tool("],
           cwe_ids=["CWE-441"], mitigation="Implement strict input validation and user confirmation.",
           references=["OWASP Agentic Top 10: A01"], tags=["owasp", "authorization", "deputy"],
           rule_ids=["AUTH-001", "AUTH-002", "MCP-AUTH-001"], published="2024-06-01",
           exploitability="HIGH", affected_languages=["python", "typescript"], cvss_score=9.1),
        _E(id="MCP-VULN-002", title="Prompt Injection via Tool Output",
           description="Tool output with user-generated content returned to LLM without sanitization.",
           severity=_S.CRITICAL, affected_patterns=["return content", "tool_response("],
           cwe_ids=["CWE-74", "CWE-79"], mitigation="Sanitize all tool outputs. Use structured data formats.",
           references=["OWASP Agentic Top 10: A02"], tags=["injection", "prompt", "output"],
           rule_ids=["INJ-001", "INJ-002", "MCP-INJ-001"], published="2024-06-01",
           exploitability="HIGH", affected_languages=["python", "typescript", "go"], cvss_score=9.0),
        _E(id="MCP-VULN-003", title="Tool Poisoning via Dynamic Loading",
           description="MCP tools loaded dynamically without integrity verification can be replaced.",
           severity=_S.CRITICAL, affected_patterns=["importlib.import_module(", "require("],
           cwe_ids=["CWE-502", "CWE-94"], mitigation="Pin tool versions, verify integrity with checksums.",
           references=["OWASP Agentic Top 10: A03"], tags=["supply-chain", "poisoning", "dynamic"],
           rule_ids=["TOOL-001", "TOOL-002", "MCP-TOOL-001"], published="2024-06-15",
           exploitability="MEDIUM", affected_languages=["python", "typescript"], cvss_score=8.8),
        _E(id="MCP-VULN-004", title="Command Injection in Tool Handlers",
           description="Tool handlers pass user input to system commands without sanitization.",
           severity=_S.CRITICAL, affected_patterns=["subprocess.call(", "os.system(", "shell=True"],
           cwe_ids=["CWE-78"], mitigation="Never use shell=True. Use subprocess with argument lists.",
           references=["CWE-78"], tags=["injection", "command", "rce"],
           rule_ids=["CMD-001", "CMD-002", "CMD-003"], published="2024-06-15",
           exploitability="HIGH", affected_languages=["python", "typescript", "go"], cvss_score=9.3),
        _E(id="MCP-VULN-005", title="Data Exfiltration via Outbound Network Calls",
           description="MCP tools make unauthorized outbound HTTP requests to exfiltrate sensitive data.",
           severity=_S.CRITICAL, affected_patterns=["requests.post(", "fetch(", "urllib.request"],
           cwe_ids=["CWE-200", "CWE-918"], mitigation="Restrict network access. Implement egress filtering.",
           references=["OWASP Agentic Top 10: A06"], tags=["exfiltration", "network", "data-loss"],
           rule_ids=["NET-001", "NET-002", "EXFIL-001"], published="2024-07-01",
           exploitability="MEDIUM", affected_languages=["python", "typescript"], cvss_score=8.5),
        _E(id="MCP-VULN-006", title="Insecure Deserialization in MCP Tools",
           description="Tools deserialize untrusted data with unsafe methods enabling remote code running.",
           severity=_S.HIGH, affected_patterns=["yaml.load(", "marshal.loads("],
           cwe_ids=["CWE-502"], mitigation="Use JSON. Use yaml.safe_load(). Never deserialize untrusted data.",
           references=["CWE-502"], tags=["deserialization", "rce"],
           rule_ids=["SER-001", "SER-002"], published="2024-07-01",
           exploitability="MEDIUM", affected_languages=["python"], cvss_score=8.0),
        _E(id="MCP-VULN-007", title="Path Traversal in File Tools",
           description="File operation tools use user-controlled paths without normalization.",
           severity=_S.HIGH, affected_patterns=["open(user_path", "Path(user_input)"],
           cwe_ids=["CWE-22"], mitigation="Normalize paths with os.path.realpath(). Enforce base directory.",
           references=["CWE-22"], tags=["path-traversal", "file-system"],
           rule_ids=["PATH-001", "PATH-002", "FS-001"], published="2024-07-15",
           exploitability="HIGH", affected_languages=["python", "typescript", "go"], cvss_score=7.5),
        _E(id="MCP-VULN-008", title="SQL Injection via Tool Parameters",
           description="Database query tools construct SQL from user input without parameterization.",
           severity=_S.HIGH, affected_patterns=["f\"SELECT", "f'SELECT"],
           cwe_ids=["CWE-89"], mitigation="Always use parameterized queries.",
           references=["CWE-89"], tags=["injection", "sql", "database"],
           rule_ids=["SQL-001", "SQL-002"], published="2024-07-15",
           exploitability="HIGH", affected_languages=["python", "typescript", "go"], cvss_score=8.2),
        _E(id="MCP-VULN-009", title="SSRF via User-Controlled URLs",
           description="Tools make HTTP requests to user-provided URLs without validation.",
           severity=_S.HIGH, affected_patterns=["requests.get(url)", "fetch(url)"],
           cwe_ids=["CWE-918"], mitigation="Validate URLs against allowlist. Block internal IP ranges.",
           references=["CWE-918"], tags=["ssrf", "network"],
           rule_ids=["NET-001", "SSRF-001"], published="2024-08-01",
           exploitability="MEDIUM", affected_languages=["python", "typescript", "go"], cvss_score=7.8),
        _E(id="MCP-VULN-010", title="Hardcoded Secrets in MCP Server Code",
           description="API keys, passwords, or tokens hardcoded in MCP server source code.",
           severity=_S.HIGH, affected_patterns=["api_key = \"", "password = \"", "token = \""],
           cwe_ids=["CWE-798"], mitigation="Use environment variables or secret managers.",
           references=["CWE-798"], tags=["secrets", "credentials"],
           rule_ids=["SEC-001", "SEC-002", "CRED-001"], published="2024-08-01",
           exploitability="LOW", affected_languages=["python", "typescript", "go"], cvss_score=7.0),
        _E(id="MCP-VULN-011", title="Missing Rate Limiting on Tool Invocations",
           description="No rate limiting on MCP tool calls allows resource exhaustion.",
           severity=_S.MEDIUM, affected_patterns=["@tool(", "server.add_tool("],
           cwe_ids=["CWE-770"], mitigation="Implement per-client rate limiting and request throttling.",
           references=["CWE-770"], tags=["dos", "rate-limit", "availability"],
           rule_ids=["RATE-001", "RATE-002"], published="2024-08-15",
           exploitability="LOW", affected_languages=["python", "typescript"], cvss_score=5.5),
        _E(id="MCP-VULN-012", title="Insufficient Logging of Tool Invocations",
           description="MCP server does not log tool invocations or security events.",
           severity=_S.MEDIUM, affected_patterns=["def handle_tool(", "async def handle("],
           cwe_ids=["CWE-778"], mitigation="Log all tool invocations. Implement audit trail.",
           references=["OWASP Agentic Top 10: A09", "CWE-778"], tags=["logging", "monitoring", "observability"],
           rule_ids=["LOG-001", "LOG-002", "MCP-LOG-001"], published="2024-08-15",
           exploitability="LOW", affected_languages=["python", "typescript", "go"], cvss_score=4.5),
        _E(id="MCP-VULN-013", title="Rug Pull via Runtime Tool Modification",
           description="MCP server modifies tool definitions at runtime after initial trust.",
           severity=_S.CRITICAL, affected_patterns=["tools.update(", "redefine_tool("],
           cwe_ids=["CWE-494"], mitigation="Lock tool definitions after registration.",
           references=["OWASP Agentic Top 10: A05"], tags=["rug-pull", "dynamic", "trust"],
           rule_ids=["TOOL-001", "TOOL-002", "MCP-TOOL-001"], published="2024-09-01",
           exploitability="MEDIUM", affected_languages=["python", "typescript"], cvss_score=9.0),
        _E(id="MCP-VULN-014", title="Privilege Escalation via Tool Chaining",
           description="Combining multiple low-privilege tools achieves higher-privilege actions.",
           severity=_S.HIGH, affected_patterns=["await tool_a(", "tool_chain(", "pipeline("],
           cwe_ids=["CWE-269"], mitigation="Implement workflow-level authorization.",
           references=["OWASP Agentic Top 10: A07", "CWE-269"], tags=["privilege-escalation", "chaining"],
           rule_ids=["AUTH-001", "AUTH-002", "CHAIN-001"], published="2024-09-01",
           exploitability="MEDIUM", affected_languages=["python", "typescript"], cvss_score=7.8),
        _E(id="MCP-VULN-015", title="Weak Cryptography in MCP Tools",
           description="MCP tools use MD5, SHA1, DES, or RC4 for security-sensitive operations.",
           severity=_S.MEDIUM, affected_patterns=["hashlib.md5(", "hashlib.sha1("],
           cwe_ids=["CWE-327"], mitigation="Use SHA-256+ for hashing, AES-GCM for encryption.",
           references=["CWE-327"], tags=["crypto", "weak-cipher"],
           rule_ids=["CRYPTO-001", "CRYPTO-002"], published="2024-09-15",
           exploitability="LOW", affected_languages=["python", "typescript", "go"], cvss_score=5.0),
        _E(id="MCP-VULN-016", title="Insecure TLS Configuration",
           description="MCP tools disable TLS verification or use outdated protocols.",
           severity=_S.HIGH, affected_patterns=["verify=False", "ssl_verify=False"],
           cwe_ids=["CWE-295"], mitigation="Always verify TLS certificates. Use TLS 1.2+ minimum.",
           references=["CWE-295"], tags=["tls", "crypto", "network"],
           rule_ids=["TLS-001", "CRYPTO-001"], published="2024-09-15",
           exploitability="MEDIUM", affected_languages=["python", "typescript"], cvss_score=7.0),
        _E(id="MCP-VULN-017", title="Cross-Tool Data Leakage via Shared State",
           description="Data from one tool invocation leaks to another through shared global state.",
           severity=_S.HIGH, affected_patterns=["global_state[", "_cache[", "cls._shared"],
           cwe_ids=["CWE-212"], mitigation="Isolate tool invocations. Use immutable data.",
           references=["CWE-212"], tags=["data-leak", "isolation", "state"],
           rule_ids=["LEAK-001", "STATE-001"], published="2024-10-01",
           exploitability="MEDIUM", affected_languages=["python", "typescript"], cvss_score=6.5),
        _E(id="MCP-VULN-018", title="Information Disclosure in Error Messages",
           description="Tool error responses expose stack traces, file paths, or database schemas.",
           severity=_S.MEDIUM, affected_patterns=["traceback.format_exc(", "str(exception)"],
           cwe_ids=["CWE-209"], mitigation="Return generic error messages. Log details server-side only.",
           references=["CWE-209"], tags=["info-disclosure", "error-handling"],
           rule_ids=["ERR-001", "INFO-001"], published="2024-10-01",
           exploitability="LOW", affected_languages=["python", "typescript", "go"], cvss_score=4.0),
        _E(id="MCP-VULN-019", title="Excessive Tool Permissions",
           description="MCP tools request more permissions than needed, violating least privilege.",
           severity=_S.MEDIUM, affected_patterns=["permissions: all", "scope: admin"],
           cwe_ids=["CWE-250"], mitigation="Apply principle of least privilege.",
           references=["OWASP Agentic Top 10: A04", "CWE-250"], tags=["permissions", "least-privilege"],
           rule_ids=["AUTH-001", "PERM-001"], published="2024-10-15",
           exploitability="LOW", affected_languages=["python", "typescript"], cvss_score=5.0),
        _E(id="MCP-VULN-020", title="Unsafe Regex in Input Validation",
           description="Regex patterns with nested quantifiers vulnerable to ReDoS attacks.",
           severity=_S.LOW, affected_patterns=["re.compile(", "new RegExp("],
           cwe_ids=["CWE-1333"], mitigation="Audit regex for ReDoS. Set evaluation timeouts.",
           references=["CWE-1333"], tags=["redos", "regex", "dos"],
           rule_ids=["DOS-001", "REGEX-001"], published="2024-10-15",
           exploitability="LOW", affected_languages=["python", "typescript", "go"], cvss_score=3.5),
        _E(id="MCP-VULN-021", title="Unvalidated Redirect Following",
           description="HTTP client follows redirects without validation, leaking auth tokens.",
           severity=_S.MEDIUM, affected_patterns=["allow_redirects=True", "follow_redirects"],
           cwe_ids=["CWE-601"], mitigation="Disable automatic redirects. Validate targets.",
           references=["CWE-601"], tags=["redirect", "network"],
           rule_ids=["NET-001", "REDIR-001"], published="2024-11-01",
           exploitability="LOW", affected_languages=["python", "typescript"], cvss_score=4.5),
        _E(id="MCP-VULN-022", title="IDOR in Resource Access Tools",
           description="Tools expose internal identifiers for direct resource access without checks.",
           severity=_S.HIGH, affected_patterns=["get_resource(id)", "read(resource_id)"],
           cwe_ids=["CWE-639"], mitigation="Implement access control checks. Use indirect references.",
           references=["CWE-639"], tags=["idor", "access-control"],
           rule_ids=["AUTH-001", "IDOR-001"], published="2024-11-01",
           exploitability="MEDIUM", affected_languages=["python", "typescript", "go"], cvss_score=7.0),
        _E(id="MCP-VULN-023", title="Denial of Service via Unbounded Input",
           description="Tools process user input without size limits, enabling resource exhaustion.",
           severity=_S.MEDIUM, affected_patterns=["read()", "recv()"],
           cwe_ids=["CWE-400"], mitigation="Enforce input size limits. Set recursion depth bounds.",
           references=["CWE-400"], tags=["dos", "resource-exhaustion"],
           rule_ids=["DOS-001", "RATE-001"], published="2024-11-15",
           exploitability="LOW", affected_languages=["python", "typescript", "go"], cvss_score=5.0),
        _E(id="MCP-VULN-024", title="Missing Content-Type Validation on Tool Inputs",
           description="Tools accept arbitrary content types without validation.",
           severity=_S.LOW, affected_patterns=["content_type", "Content-Type"],
           cwe_ids=["CWE-20"], mitigation="Validate Content-Type headers.",
           references=["CWE-20"], tags=["validation", "input"],
           rule_ids=["VAL-001"], published="2024-11-15",
           exploitability="LOW", affected_languages=["python", "typescript"], cvss_score=3.0),
        _E(id="MCP-VULN-025", title="XML External Entity (XXE) in Tool Parsing",
           description="Tools parse XML input without disabling external entity resolution.",
           severity=_S.HIGH, affected_patterns=["xml.etree.ElementTree.parse(", "DOMParser("],
           cwe_ids=["CWE-611"], mitigation="Disable external entities in XML parsers.",
           references=["CWE-611"], tags=["xxe", "xml", "injection"],
           rule_ids=["XML-001"], published="2024-12-01",
           exploitability="MEDIUM", affected_languages=["python", "typescript"], cvss_score=7.5),
        _E(id="MCP-VULN-026", title="Mass Assignment in Tool Parameters",
           description="Tool handlers accept arbitrary parameters mapping to internal objects.",
           severity=_S.MEDIUM, affected_patterns=["**kwargs", "Object.assign("],
           cwe_ids=["CWE-915"], mitigation="Define explicit parameter schemas.",
           references=["CWE-915"], tags=["mass-assignment", "validation"],
           rule_ids=["VAL-001", "PARAM-001"], published="2024-12-01",
           exploitability="MEDIUM", affected_languages=["python", "typescript"], cvss_score=6.0),
        _E(id="MCP-VULN-027", title="Insufficient Transport Encryption",
           description="MCP server uses unencrypted transport for sensitive data.",
           severity=_S.HIGH, affected_patterns=["http://", "createServer("],
           cwe_ids=["CWE-319"], mitigation="Use TLS for all transport. Enforce HTTPS.",
           references=["CWE-319"], tags=["transport", "encryption"],
           rule_ids=["TLS-001", "NET-001"], published="2024-12-15",
           exploitability="MEDIUM", affected_languages=["python", "typescript", "go"], cvss_score=7.0),
        _E(id="MCP-VULN-028", title="Template Injection in Tool Responses",
           description="Tool responses use template engines with user data enabling SSTI.",
           severity=_S.HIGH, affected_patterns=["jinja2.Template(", "{{"],
           cwe_ids=["CWE-94"], mitigation="Use autoescaping. Never pass user input as template source.",
           references=["CWE-94"], tags=["ssti", "template", "injection"],
           rule_ids=["INJ-001", "TMPL-001"], published="2024-12-15",
           exploitability="MEDIUM", affected_languages=["python", "typescript"], cvss_score=7.8),
        _E(id="MCP-VULN-029", title="Uncontrolled Resource Consumption in Tool Workers",
           description="Tool handlers spawn threads or processes without limits.",
           severity=_S.MEDIUM, affected_patterns=["Thread(", "Process("],
           cwe_ids=["CWE-400", "CWE-770"], mitigation="Use thread pools with max workers.",
           references=["CWE-400"], tags=["dos", "resource", "concurrency"],
           rule_ids=["DOS-001", "RATE-001"], published="2025-01-01",
           exploitability="LOW", affected_languages=["python", "typescript", "go"], cvss_score=5.5),
        _E(id="MCP-VULN-030", title="Improper Certificate Pinning",
           description="MCP tools connect to external services without certificate pinning.",
           severity=_S.MEDIUM, affected_patterns=["ssl.create_default_context()", "tls.connect("],
           cwe_ids=["CWE-295"], mitigation="Pin certificates for critical connections.",
           references=["CWE-295"], tags=["tls", "pinning", "certificate"],
           rule_ids=["TLS-001"], published="2025-01-01",
           exploitability="LOW", affected_languages=["python", "typescript"], cvss_score=4.5),
        _E(id="MCP-VULN-031", title="Insecure Temporary File Handling",
           description="Tools create temporary files with predictable names or insecure permissions.",
           severity=_S.MEDIUM, affected_patterns=["tempfile.mktemp(", "os.tmpnam("],
           cwe_ids=["CWE-377"], mitigation="Use tempfile.mkstemp() or NamedTemporaryFile.",
           references=["CWE-377"], tags=["tempfile", "file-system"],
           rule_ids=["FS-001", "TEMP-001"], published="2025-01-15",
           exploitability="LOW", affected_languages=["python"], cvss_score=4.0),
        _E(id="MCP-VULN-032", title="Missing Origin Validation in SSE Transport",
           description="SSE-based MCP transport accepts connections without validating origin.",
           severity=_S.HIGH, affected_patterns=["EventSource(", "SSEServer("],
           cwe_ids=["CWE-346"], mitigation="Validate Origin headers. Implement CORS policy.",
           references=["CWE-346"], tags=["sse", "transport", "cors"],
           rule_ids=["NET-001", "SSE-001"], published="2025-01-15",
           exploitability="MEDIUM", affected_languages=["python", "typescript"], cvss_score=7.2),
    ]


class VulnDatabase:
    def __init__(self, entries: list[VulnEntry] | None = None) -> None:
        self.entries: list[VulnEntry] = entries if entries is not None else _build_vuln_db()
        self._by_id: dict[str, VulnEntry] = {e.id: e for e in self.entries}
        self._by_rule: dict[str, list[VulnEntry]] = {}
        for e in self.entries:
            for rid in e.rule_ids:
                self._by_rule.setdefault(rid, []).append(e)

    @property
    def count(self) -> int:
        return len(self.entries)

    def get(self, vuln_id: str) -> VulnEntry | None:
        return self._by_id.get(vuln_id)

    def list_all(self) -> list[VulnEntry]:
        return list(self.entries)

    def search(self, query: str) -> list[VulnEntry]:
        q = query.lower()
        return [e for e in self.entries if (
            q in e.id.lower() or q in e.title.lower() or q in e.description.lower()
            or any(q in t.lower() for t in e.tags)
            or any(q in c.lower() for c in e.cwe_ids)
            or any(q in r.lower() for r in e.rule_ids)
        )]

    def search_advanced(self, query: str = "", severity: str | VulnSeverity | None = None,
                        cwe: str | None = None, date_from: str | None = None,
                        date_to: str | None = None, language: str | None = None,
                        tag: str | None = None) -> VulnSearchResult:
        results = list(self.entries)
        if query:
            q = query.lower()
            results = [e for e in results if (
                q in e.id.lower() or q in e.title.lower() or q in e.description.lower()
                or any(q in t.lower() for t in e.tags) or any(q in c.lower() for c in e.cwe_ids)
            )]
        if severity is not None:
            if isinstance(severity, str):
                severity = VulnSeverity.from_string(severity)
            results = [e for e in results if e.severity == severity]
        if cwe:
            cu = cwe.upper()
            if not cu.startswith("CWE-"):
                cu = f"CWE-{cu}"
            results = [e for e in results if cu in e.cwe_ids]
        if date_from:
            results = [e for e in results if e.published >= date_from]
        if date_to:
            results = [e for e in results if e.published <= date_to]
        if language:
            lang = language.lower()
            results = [e for e in results if lang in [la.lower() for la in e.affected_languages]]
        if tag:
            tl = tag.lower()
            results = [e for e in results if tl in [t.lower() for t in e.tags]]
        return VulnSearchResult(entries=results, query=query or f"severity={severity} cwe={cwe}",
                                total_in_db=self.count)

    def filter_by_severity(self, severity: VulnSeverity | str) -> list[VulnEntry]:
        if isinstance(severity, str):
            severity = VulnSeverity.from_string(severity)
        return [e for e in self.entries if e.severity == severity]

    def filter_by_cwe(self, cwe_id: str) -> list[VulnEntry]:
        cu = cwe_id.upper()
        if not cu.startswith("CWE-"):
            cu = f"CWE-{cu}"
        return [e for e in self.entries if cu in e.cwe_ids]

    def filter_by_tag(self, tag: str) -> list[VulnEntry]:
        tl = tag.lower()
        return [e for e in self.entries if tl in [t.lower() for t in e.tags]]

    def filter_by_date_range(self, date_from: str | None = None, date_to: str | None = None) -> list[VulnEntry]:
        results = list(self.entries)
        if date_from:
            results = [e for e in results if e.published >= date_from]
        if date_to:
            results = [e for e in results if e.published <= date_to]
        return results

    def vulns_for_rule(self, rule_id: str) -> list[VulnEntry]:
        return self._by_rule.get(rule_id, [])

    def add_entry(self, entry: VulnEntry) -> None:
        self.entries.append(entry)
        self._by_id[entry.id] = entry
        for rid in entry.rule_ids:
            self._by_rule.setdefault(rid, []).append(entry)

    def remove_entry(self, vuln_id: str) -> bool:
        entry = self._by_id.pop(vuln_id, None)
        if entry is None:
            return False
        self.entries = [e for e in self.entries if e.id != vuln_id]
        for rid in entry.rule_ids:
            if rid in self._by_rule:
                self._by_rule[rid] = [e for e in self._by_rule[rid] if e.id != vuln_id]
        return True


def match_findings(findings: list[dict[str, Any]], db: VulnDatabase | None = None) -> list[EnrichedFinding]:
    if db is None:
        db = VulnDatabase()
    enriched: list[EnrichedFinding] = []
    for finding in findings:
        rule_id = finding.get("rule_id", "")
        title = finding.get("title", "").lower()
        desc = finding.get("description", "").lower()
        matched: list[VulnEntry] = []
        if rule_id:
            matched.extend(db.vulns_for_rule(rule_id))
        for entry in db.entries:
            if entry in matched:
                continue
            ew = set(entry.title.lower().split())
            fw = set(title.split()) | set(desc.split())
            if len(ew & fw) >= 3:
                matched.append(entry)
        score = _calculate_risk_score(finding, matched)
        notes: list[str] = []
        if matched:
            notes.append(f"Matched {len(matched)} known vulnerabilities")
            cwes = set()
            for v in matched:
                cwes.update(v.cwe_ids)
            if cwes:
                notes.append(f"Related CWEs: {', '.join(sorted(cwes))}")
        enriched.append(EnrichedFinding(finding=finding, matched_vulns=matched,
                                        risk_score=score, enrichment_notes=notes))
    return enriched


def _calculate_risk_score(finding: dict[str, Any], vulns: list[VulnEntry]) -> float:
    if not vulns:
        sev = finding.get("severity", "INFO").upper()
        base = {"CRITICAL": 40, "HIGH": 30, "MEDIUM": 20, "LOW": 10, "INFO": 5}
        return float(base.get(sev, 5))
    max_cvss = max(v.cvss_score for v in vulns)
    exploit_bonus = 0.0
    for v in vulns:
        if v.exploitability == "HIGH":
            exploit_bonus = max(exploit_bonus, 15.0)
        elif v.exploitability == "MEDIUM":
            exploit_bonus = max(exploit_bonus, 8.0)
    score = (max_cvss / 10.0) * 70.0 + exploit_bonus + len(vulns) * 2.0
    return min(100.0, round(score, 1))


def export_db_json(db: VulnDatabase) -> str:
    return json.dumps({"version": "2.0", "count": db.count,
                        "entries": [e.to_dict() for e in db.entries]}, indent=2, ensure_ascii=False)


def import_db_json(json_str: str) -> VulnDatabase:
    data = json.loads(json_str)
    return VulnDatabase(entries=[VulnEntry.from_dict(e) for e in data.get("entries", [])])


def export_db_to_file(db: VulnDatabase, path: str) -> None:
    from pathlib import Path as P
    P(path).write_text(export_db_json(db), encoding="utf-8")


def import_db_from_file(path: str) -> VulnDatabase:
    from pathlib import Path as P
    return import_db_json(P(path).read_text(encoding="utf-8"))


def format_vuln_db_text(db: VulnDatabase) -> str:
    lines = ["=" * 70, "MCP Vulnerability Database v2", "=" * 70, f"Total entries: {db.count}", ""]
    sc: dict[str, int] = {}
    for e in db.entries:
        sc[e.severity.value] = sc.get(e.severity.value, 0) + 1
    for sev in VulnSeverity:
        c = sc.get(sev.value, 0)
        if c:
            lines.append(f"  {sev.value:<10} {c}")
    lines.append("")
    lines.append(f"{'ID':<16} {'Severity':<10} {'CVSS':<6} {'Title'}")
    lines.append(f"{'---'*6:<16} {'---'*4:<10} {'---'*2:<6} {'---'*12}")
    for e in db.entries:
        lines.append(f"{e.id:<16} {e.severity.value:<10} {e.cvss_score:<6.1f} {e.title[:50]}")
    lines.append(f"\n{db.count} entries in database")
    return "\n".join(lines)


def format_vuln_db_json(db: VulnDatabase) -> str:
    return export_db_json(db)


def format_enriched_text(enriched: list[EnrichedFinding]) -> str:
    lines = ["=" * 60, "Enriched Findings Report", "=" * 60]
    lines.append(f"Total findings: {len(enriched)}")
    lines.append(f"Enriched:       {sum(1 for e in enriched if e.is_enriched)}")
    lines.append("")
    for ef in enriched:
        lines.append(f"  [{ef.max_severity}] {ef.finding.get('rule_id','?')}: {ef.finding.get('title','?')}")
        for v in ef.matched_vulns:
            lines.append(f"    -> {v.id}: {v.title} (CVSS {v.cvss_score})")
        for note in ef.enrichment_notes:
            lines.append(f"    Note: {note}")
        lines.append(f"    Risk score: {ef.risk_score}")
        lines.append("")
    return "\n".join(lines)


def format_enriched_json(enriched: list[EnrichedFinding]) -> str:
    return json.dumps([e.to_dict() for e in enriched], indent=2, ensure_ascii=False)
