"""TS-PRIV-001: Privilege Escalation / Excessive Agency for TypeScript/JavaScript.

Detects overly broad tool capabilities in TypeScript MCP servers that
could allow privilege escalation or excessive agency.

OWASP Agentic: Tools that grant the agent capabilities beyond what is
necessary — dynamic code loading, process control, or unrestricted
filesystem manipulation.

NOTE: This module contains string references to unsafe patterns
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 re

from . import TSRule
from ..models import Finding, Severity

# Dynamic require/import patterns (loading arbitrary modules at runtime)
DYNAMIC_REQUIRE = re.compile(
    r'\brequire\s*\(\s*(?:args|params|input|req\.(?:params|query|body))'
    r'\s*\.\s*\w+'
)
DYNAMIC_IMPORT = re.compile(
    r'\bimport\s*\(\s*(?:args|params|input|req\.(?:params|query|body))'
    r'\s*\.\s*\w+'
)

# Process control without validation
PROCESS_KILL = re.compile(
    r'\bprocess\s*\.\s*kill\s*\('
)
PROCESS_EXIT = re.compile(
    r'\bprocess\s*\.\s*exit\s*\('
)

# child_process usage without argument validation in tool handlers.
# These are DETECTION SIGNATURES for the scanner, not actual usage.
CHILD_PROCESS_UNVALIDATED = re.compile(
    r'\b(?:exec|execSync|execFile|execFileSync|spawn|spawnSync)\s*\(\s*'
    r'(?:args|params|input)\s*\.\s*\w+'
)

# __dirname manipulation for escaping sandboxes
DIRNAME_MANIPULATION = re.compile(
    r'(?:__dirname|__filename)\s*\+|'
    r'path\s*\.\s*(?:join|resolve)\s*\(\s*__dirname\s*,\s*'
    r'(?:args|params|input)\s*\.\s*\w+'
)

# Dynamic property access on sensitive objects
DYNAMIC_PROP_ACCESS = re.compile(
    r'\b(?:global|globalThis|process|require)\s*\[\s*'
    r'(?:args|params|input)\s*\.\s*\w+\s*\]'
)

# Validation/sandboxing patterns that mitigate these risks
VALIDATION_PATTERNS = re.compile(
    r'(?:allowlist|whitelist|allowedModules|allowedCommands|'
    r'validCommands|permitted|safelist|'
    r'sandboxed|sandbox|isolated|'
    r'\.includes\s*\(|\.indexOf\s*\(|'
    r'switch\s*\(\s*(?:args|params|input))',
    re.IGNORECASE,
)


class TSPrivilegeEscalationRule(TSRule):
    """Detects privilege escalation / excessive agency patterns."""

    @property
    def rule_id(self) -> str:
        return "TS-PRIV-001"

    @property
    def name(self) -> str:
        return "Privilege Escalation / Excessive Agency"

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

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

    @property
    def description(self) -> str:
        return (
            "Overly broad tool capabilities that could allow privilege "
            "escalation or excessive agency in MCP servers."
        )

    @property
    def detailed_description(self) -> str:
        return (
            "Overly broad tool capabilities that could allow privilege "
            "escalation or excessive agency in MCP servers.\n\n"
            "MCP tools should follow the principle of least privilege.\n"
            "Giving tools the ability to load arbitrary modules, control\n"
            "processes, or manipulate paths from user input creates risk\n"
            "of privilege escalation.\n\n"
            "Examples of vulnerable code:\n"
            "  const mod = require(args.module);\n"
            "  process.kill(args.pid);\n"
            "  spawn(args.command);\n"
            "  const p = path.join(__dirname, args.subpath);\n\n"
            "How to fix:\n"
            "  - Use an allowlist of permitted modules/commands\n"
            "  - Validate all arguments against expected values\n"
            "  - Avoid exposing process control to tool handlers\n"
            "  - Restrict filesystem access to a sandboxed directory"
        )

    def check(self, source: str) -> list[Finding]:
        """Check for privilege escalation patterns."""
        findings: list[Finding] = []
        lines = self._get_lines(source)

        for i, line in enumerate(lines, 1):
            if self._is_comment_line(line):
                continue

            # Check for nearby validation (within 5 lines before)
            context_start = max(0, i - 6)
            context = "\n".join(lines[context_start:i - 1])
            has_validation = bool(VALIDATION_PATTERNS.search(context))

            if has_validation:
                continue

            matched = False
            detail = ""
            sev = self.severity

            if DYNAMIC_REQUIRE.search(line):
                matched = True
                detail = (
                    "Dynamic require() with user-supplied module name "
                    "allows loading arbitrary code."
                )
                sev = Severity.CRITICAL

            elif DYNAMIC_IMPORT.search(line):
                matched = True
                detail = (
                    "Dynamic import() with user-supplied module name "
                    "allows loading arbitrary code."
                )
                sev = Severity.CRITICAL

            elif CHILD_PROCESS_UNVALIDATED.search(line):
                matched = True
                detail = (
                    "Child process with user-supplied arguments "
                    "without validation."
                )

            elif PROCESS_KILL.search(line):
                matched = True
                detail = (
                    "process.kill() exposed in tool handler allows "
                    "terminating arbitrary processes."
                )

            elif DIRNAME_MANIPULATION.search(line):
                matched = True
                detail = (
                    "__dirname combined with user input allows escaping "
                    "the intended directory scope."
                )

            elif DYNAMIC_PROP_ACCESS.search(line):
                matched = True
                detail = (
                    "Dynamic property access on global/process object "
                    "with user input allows accessing arbitrary internals."
                )
                sev = Severity.CRITICAL

            if matched:
                findings.append(Finding(
                    rule_id=self.rule_id,
                    severity=sev,
                    title="Privilege Escalation / Excessive Agency",
                    description=detail,
                    file="",
                    line=i,
                    code_snippet=line.strip()[:80],
                    recommendation=(
                        "Restrict capabilities to an allowlist of "
                        "permitted values. Do not pass user input "
                        "directly to privileged operations."
                    ),
                    cwe=self.cwe,
                ))

        return findings
