"""MCP-003: Confused Deputy detection rule.

Detects patterns where MCP tool handlers pass user-controlled input directly
to privileged operations (file I/O, network requests, subprocess) without
authorization checks, enabling confused deputy attacks (CWE-441).

NOTE: This module contains string references to unsafe functions because it is
a SECURITY SCANNER that detects these patterns in target code. These are
detection signatures, not actual usage.
"""

from __future__ import annotations

import ast

from . import Rule
from ..models import Finding, Severity

# Detection signatures for privileged operations in scanned code.
# These are pattern names the scanner looks for - not executed.
PRIVILEGED_FILE_OPS = {
    "open", "Path.read_text", "Path.write_text", "Path.read_bytes",
    "Path.write_bytes", "os.remove", "os.unlink", "os.rename",
    "shutil.copy", "shutil.move", "shutil.rmtree",
}

PRIVILEGED_NET_OPS = {
    "requests.get", "requests.post", "requests.put", "requests.delete",
    "httpx.get", "httpx.post", "httpx.put", "httpx.delete",
    "urllib.request.urlopen",
}

# Detection signatures — subprocess function names to match against.
PRIVILEGED_EXEC_OPS = {
    "subprocess.run", "subprocess.call", "subprocess.Popen",
    "subprocess.check_output", "subprocess.check_call",
}

ALL_PRIVILEGED_OPS = PRIVILEGED_FILE_OPS | PRIVILEGED_NET_OPS | PRIVILEGED_EXEC_OPS

# Tokens indicating authorization/validation is present
AUTH_TOKENS = {
    "authorize", "authorise", "auth_check", "check_auth",
    "check_permission", "has_permission", "is_authorized",
    "is_allowed", "verify_access", "validate_access",
    "permission", "rbac", "acl",
}


class ConfusedDeputyRule(Rule):
    """Detects confused deputy patterns in MCP tool handlers.

    Flags tool handlers that pass user-controlled input directly to
    privileged operations without authorization checks.
    """

    @property
    def rule_id(self) -> str:
        return "MCP-003"

    @property
    def name(self) -> str:
        return "Confused Deputy Attack"

    @property
    def severity(self) -> Severity:
        return Severity.CRITICAL

    @property
    def cwe(self) -> str:
        return "CWE-441"

    @property
    def description(self) -> str:
        return (
            "Tool handler passes user input to privileged operations "
            "without authorization checks (confused deputy)."
        )

    @property
    def detailed_description(self) -> str:
        return (
            "Tool handler passes user input to privileged operations "
            "without authorization checks (confused deputy).\n\n"
            "In the MCP architecture, the LLM acts as a proxy between the user\n"
            "and tool handlers. If a tool handler blindly executes privileged\n"
            "operations with user-supplied arguments, an attacker can trick the\n"
            "LLM into performing unintended actions.\n\n"
            "How to fix:\n"
            "  - Add authorization checks before privileged operations\n"
            "  - Validate and constrain input (allowlists, path prefixes)\n"
            "  - Implement least-privilege access controls per tool"
        )

    def check(self, tree: ast.Module, source: str) -> list[Finding]:
        """Check for confused deputy patterns in tool handlers."""
        findings: list[Finding] = []
        for node in ast.walk(tree):
            if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            if not self._is_tool_handler(node):
                continue
            if self._has_auth_check(node, source):
                continue

            privileged_calls = self._find_privileged_calls(node)
            for call_node, func_name in privileged_calls:
                if self._uses_parameter_input(call_node, node):
                    op_type = self._classify_operation(func_name)
                    findings.append(self._make_finding(
                        node, call_node, func_name, op_type, source,
                    ))
        return findings

    def _make_finding(
        self,
        func_node: ast.FunctionDef | ast.AsyncFunctionDef,
        call_node: ast.Call,
        func_name: str,
        op_type: str,
        source: str,
    ) -> Finding:
        """Create a finding for a confused deputy pattern."""
        return Finding(
            rule_id=self.rule_id,
            severity=self.severity,
            title=f"Confused deputy: {op_type} with user input",
            description=(
                f"Tool handler '{func_node.name}' passes user input "
                f"to {func_name}() without authorization."
            ),
            file="",
            line=call_node.lineno,
            code_snippet=self._get_snippet(source, call_node.lineno),
            recommendation=(
                "Add authorization checks before privileged "
                "operations in tool handlers."
            ),
            cwe=self.cwe,
            confidence="HIGH",
            remediation=(
                f"Add an authorization check before calling "
                f"{func_name}(). For file operations, validate "
                f"paths against an allowlist. For network/exec "
                f"operations, implement RBAC or capability checks."
            ),
        )

    def _is_tool_handler(
        self, node: ast.FunctionDef | ast.AsyncFunctionDef
    ) -> bool:
        """Check if function is an MCP tool handler."""
        name_lower = node.name.lower()
        if any(kw in name_lower for kw in ("tool", "handle")):
            return True
        for dec in node.decorator_list:
            if "tool" in ast.dump(dec).lower():
                return True
        return False

    def _has_auth_check(
        self,
        node: ast.FunctionDef | ast.AsyncFunctionDef,
        source: str,
    ) -> bool:
        """Check if function body contains authorization logic."""
        body_src = ast.get_source_segment(source, node) or ""
        body_lower = body_src.lower()
        return any(token in body_lower for token in AUTH_TOKENS)

    def _find_privileged_calls(
        self, node: ast.FunctionDef | ast.AsyncFunctionDef
    ) -> list[tuple[ast.Call, str]]:
        """Find calls to privileged operations within a function."""
        results: list[tuple[ast.Call, str]] = []
        for child in ast.walk(node):
            if not isinstance(child, ast.Call):
                continue
            func_name = self._get_func_name(child)
            if func_name in ALL_PRIVILEGED_OPS:
                results.append((child, func_name))
            # Also catch bare open() calls
            if isinstance(child.func, ast.Name) and child.func.id == "open":
                if func_name not in ALL_PRIVILEGED_OPS:
                    results.append((child, "open"))
        return results

    def _uses_parameter_input(
        self,
        call_node: ast.Call,
        func_node: ast.FunctionDef | ast.AsyncFunctionDef,
    ) -> bool:
        """Check if a call uses function parameters as arguments."""
        param_names = {arg.arg for arg in func_node.args.args}
        for arg in call_node.args:
            if self._references_name(arg, param_names):
                return True
        for kw in call_node.keywords:
            if kw.value and self._references_name(kw.value, param_names):
                return True
        return False

    def _references_name(
        self, node: ast.AST, names: set[str]
    ) -> bool:
        """Check if an AST node references any of the given names."""
        if isinstance(node, ast.Name) and node.id in names:
            return True
        for child in ast.walk(node):
            if isinstance(child, ast.Name) and child.id in names:
                return True
        return False

    def _classify_operation(self, func_name: str) -> str:
        """Classify a privileged operation by type."""
        if func_name in PRIVILEGED_FILE_OPS or func_name == "open":
            return "file operation"
        if func_name in PRIVILEGED_NET_OPS:
            return "network request"
        if func_name in PRIVILEGED_EXEC_OPS:
            return "command execution"
        return "privileged operation"
