"""TS-CMD-001: Command Injection detection for TypeScript/JavaScript.

Detects usage of child_process.exec, execSync, and similar functions
that enable command injection in Node.js MCP servers.

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

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

# Patterns for dangerous child_process and shell execution functions.
# These are detection signatures, not actual usage.
EXEC_PATTERN = re.compile(
    r'\b(exec|execSync|execFile|execFileSync|spawn|spawnSync)\s*\('
)
SHELL_OPTION_PATTERN = re.compile(
    r'shell\s*:\s*true'
)
CHILD_PROCESS_IMPORT = re.compile(
    r'''(?:require\s*\(\s*['"]child_process['"]\s*\)|'''
    r'''from\s+['"]child_process['"])'''
)
TEMPLATE_IN_EXEC = re.compile(
    r'\b(?:exec|execSync)\s*\(\s*`'
)
CONCAT_IN_EXEC = re.compile(
    r'\b(?:exec|execSync)\s*\([^)]*\+'
)


class TSCommandInjectionRule(TSRule):
    """Detects command injection patterns in TypeScript/JavaScript."""

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

    @property
    def name(self) -> str:
        return "Command Injection (child_process)"

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

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

    @property
    def description(self) -> str:
        return (
            "child_process exec/execSync with dynamic arguments "
            "enables command injection."
        )

    @property
    def detailed_description(self) -> str:
        return (
            "child_process exec/execSync with dynamic arguments "
            "enables command injection.\n\n"
            "In Node.js, child_process.exec() and execSync() pass commands\n"
            "through a shell. If any part of the command string comes from\n"
            "user input, an attacker can inject arbitrary shell commands.\n\n"
            "Examples of vulnerable code:\n"
            "  exec(`ls ${userDir}`);\n"
            "  execSync('grep ' + pattern + ' file.txt');\n\n"
            "How to fix:\n"
            "  - Use execFile/execFileSync instead (no shell)\n"
            "  - Use spawn with an argument array\n"
            "  - Sanitize all user input with shell-escape libraries"
        )

    def check(self, source: str) -> list[Finding]:
        """Check for command injection patterns."""
        findings: list[Finding] = []
        has_child_process = bool(CHILD_PROCESS_IMPORT.search(source))

        for lineno, line, _match in self._search_lines(
            source, EXEC_PATTERN
        ):
            # exec/execSync with template literal
            if TEMPLATE_IN_EXEC.search(line):
                findings.append(self._make_finding(
                    lineno, line,
                    "exec/execSync with template literal (dynamic command)",
                ))
                continue

            # exec/execSync with string concatenation
            if CONCAT_IN_EXEC.search(line):
                findings.append(self._make_finding(
                    lineno, line,
                    "exec/execSync with string concatenation",
                ))
                continue

            # Plain exec/execSync call (only flag if child_process imported)
            if has_child_process and re.search(
                r'\b(exec|execSync)\s*\(', line
            ):
                findings.append(self._make_finding(
                    lineno, line,
                    "exec/execSync usage (potential command injection)",
                ))

        # spawn with shell: true
        for lineno, line, _match in self._search_lines(
            source, SHELL_OPTION_PATTERN
        ):
            if re.search(r'\bspawn', line) or has_child_process:
                findings.append(Finding(
                    rule_id=self.rule_id,
                    severity=Severity.HIGH,
                    title="spawn with shell: true",
                    description=(
                        "spawn() with shell: true passes commands "
                        "through the shell."
                    ),
                    file="",
                    line=lineno,
                    code_snippet=line.strip()[:80],
                    recommendation="Remove shell: true, use argument array.",
                    cwe=self.cwe,
                ))
        return findings

    def _make_finding(
        self, lineno: int, line: str, desc: str
    ) -> Finding:
        """Create a command injection finding."""
        return Finding(
            rule_id=self.rule_id,
            severity=self.severity,
            title="Command Injection",
            description=desc,
            file="",
            line=lineno,
            code_snippet=line.strip()[:80],
            recommendation=(
                "Use execFile/spawn with argument array instead of "
                "exec/execSync."
            ),
            cwe=self.cwe,
        )
