tessen-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tessen/rules/base.py ADDED
@@ -0,0 +1,29 @@
1
+ """Base classes for detection rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+
7
+ from tessen.models import ParseResult
8
+ from tessen.rules.models import Finding, RuleMetadata
9
+
10
+
11
+ class Rule(ABC):
12
+ """Base class for all detection rules.
13
+
14
+ Concrete rules inherit from this, define METADATA, and implement check().
15
+ """
16
+
17
+ METADATA: RuleMetadata
18
+
19
+ @abstractmethod
20
+ def check(self, result: ParseResult) -> list[Finding]:
21
+ """Analyze a parsed config and return any findings.
22
+
23
+ Rules should be pure: no I/O, no state, no side effects.
24
+ Same input -> same output. Makes testing trivial.
25
+ """
26
+
27
+ @property
28
+ def id(self) -> str:
29
+ return self.METADATA.id
@@ -0,0 +1,30 @@
1
+ """Built-in detection rules shipped with Tessen."""
2
+
3
+ from tessen.rules.builtin.secrets_in_env import SecretsInEnvRule
4
+ from tessen.rules.builtin.dangerous_command import DangerousCommandRule
5
+ from tessen.rules.builtin.suspicious_args import SuspiciousArgsRule
6
+ from tessen.rules.builtin.known_vulnerable import KnownVulnerableServerRule
7
+ from tessen.rules.builtin.http_no_auth import HttpNoAuthRule
8
+ from tessen.rules.builtin.auto_approve import AutoApproveRule
9
+ from tessen.rules.builtin.hook_injection import HookInjectionRule
10
+
11
+ BUILTIN_RULES = [
12
+ SecretsInEnvRule(),
13
+ DangerousCommandRule(),
14
+ SuspiciousArgsRule(),
15
+ KnownVulnerableServerRule(),
16
+ HttpNoAuthRule(),
17
+ AutoApproveRule(),
18
+ HookInjectionRule(),
19
+ ]
20
+
21
+ __all__ = [
22
+ "BUILTIN_RULES",
23
+ "SecretsInEnvRule",
24
+ "DangerousCommandRule",
25
+ "SuspiciousArgsRule",
26
+ "KnownVulnerableServerRule",
27
+ "HttpNoAuthRule",
28
+ "AutoApproveRule",
29
+ "HookInjectionRule",
30
+ ]
@@ -0,0 +1,98 @@
1
+ """Detect dangerous auto-approve and trust-all configurations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from tessen.models import ParseResult
6
+ from tessen.rules.base import Rule
7
+ from tessen.rules.models import Finding, RuleMetadata, Severity
8
+
9
+ # Fields (in server extra data) that indicate blanket auto-approval.
10
+ AUTO_APPROVE_FIELDS = {
11
+ "autoApprove",
12
+ "auto_approve",
13
+ "autoApproveAll",
14
+ "auto_approve_all",
15
+ "trustAll",
16
+ "trust_all",
17
+ "skipConfirmation",
18
+ "skip_confirmation",
19
+ "alwaysAllow",
20
+ "always_allow",
21
+ }
22
+
23
+
24
+ class AutoApproveRule(Rule):
25
+ METADATA = RuleMetadata(
26
+ id="tessen-auto-approve",
27
+ name="Dangerous auto-approve configuration",
28
+ description=(
29
+ "An MCP server is configured to auto-approve all tool calls, "
30
+ "bypassing the human-in-the-loop consent mechanism. This means "
31
+ "any prompt injection reaching this server can execute tools "
32
+ "without user confirmation."
33
+ ),
34
+ severity=Severity.HIGH,
35
+ )
36
+
37
+ def check(self, result: ParseResult) -> list[Finding]:
38
+ findings: list[Finding] = []
39
+
40
+ for server_name, server in result.config.mcpServers.items():
41
+ extra = server.model_extra or {}
42
+
43
+ for field_name, value in extra.items():
44
+ if field_name not in AUTO_APPROVE_FIELDS:
45
+ continue
46
+
47
+ is_dangerous = self._is_dangerous_value(value)
48
+ if not is_dangerous:
49
+ continue
50
+
51
+ findings.append(
52
+ Finding(
53
+ rule_id=self.METADATA.id,
54
+ severity=self.METADATA.severity,
55
+ title=f"Auto-approve enabled on server '{server_name}'",
56
+ message=(
57
+ f"Server '{server_name}' has '{field_name}' set to "
58
+ f"{self._format_value(value)}. This bypasses the consent "
59
+ f"mechanism that normally requires user approval before "
60
+ f"tool execution. Any prompt injection reaching this "
61
+ f"server will execute without a confirmation step."
62
+ ),
63
+ location=f"mcpServers.{server_name}.{field_name}",
64
+ remediation=(
65
+ "Remove the auto-approve flag or scope it to specific "
66
+ "low-risk tools instead of granting blanket approval. "
67
+ "Use an allowlist of specific tool names rather than "
68
+ "wildcards or boolean true."
69
+ ),
70
+ references=self.METADATA.references,
71
+ )
72
+ )
73
+
74
+ return findings
75
+
76
+ @staticmethod
77
+ def _is_dangerous_value(value) -> bool:
78
+ """Check if the value represents blanket approval."""
79
+ if isinstance(value, bool):
80
+ return value is True
81
+
82
+ if isinstance(value, str):
83
+ return value.lower() in ("true", "yes", "1", "*", "all")
84
+
85
+ if isinstance(value, list):
86
+ # ["*"] or ["all"] = blanket approval
87
+ if any(str(v).strip() in ("*", "all") for v in value):
88
+ return True
89
+ # A list with 10+ tools is suspicious but not auto-flagged.
90
+ # That's a judgment call for the user.
91
+
92
+ return False
93
+
94
+ @staticmethod
95
+ def _format_value(value) -> str:
96
+ if isinstance(value, list):
97
+ return str(value)
98
+ return repr(value)
@@ -0,0 +1,89 @@
1
+ """Detect commands known to be dangerous or a common attack vector."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from tessen.models import ParseResult
6
+ from tessen.rules.base import Rule
7
+ from tessen.rules.models import Finding, RuleMetadata, Severity
8
+
9
+
10
+ # Commands that indicate direct shell access — an MCP server should almost
11
+ # never need this.
12
+ SHELL_COMMANDS = {"sh", "bash", "zsh", "fish", "cmd", "cmd.exe", "powershell", "pwsh"}
13
+
14
+ # Commands that pull remote code without pinning or verification.
15
+ UNVERIFIED_REMOTE_EXEC = {
16
+ ("curl", "|", "sh"),
17
+ ("curl", "|", "bash"),
18
+ ("wget", "|", "sh"),
19
+ ("wget", "|", "bash"),
20
+ }
21
+
22
+
23
+ class DangerousCommandRule(Rule):
24
+ METADATA = RuleMetadata(
25
+ id="tessen-dangerous-command",
26
+ name="MCP server invoked via dangerous command",
27
+ description=(
28
+ "The MCP server is invoked using a shell interpreter or a curl-pipe-shell "
29
+ "pattern. Either grants the server arbitrary code execution and blocks "
30
+ "any pretense of sandboxing."
31
+ ),
32
+ severity=Severity.HIGH,
33
+ )
34
+
35
+ def check(self, result: ParseResult) -> list[Finding]:
36
+ findings: list[Finding] = []
37
+
38
+ for server_name, server in result.config.mcpServers.items():
39
+ cmd = server.command.strip()
40
+ cmd_base = cmd.rsplit("/", 1)[-1] # e.g. /bin/bash -> bash
41
+
42
+ if cmd_base in SHELL_COMMANDS:
43
+ findings.append(
44
+ Finding(
45
+ rule_id=self.METADATA.id,
46
+ severity=self.METADATA.severity,
47
+ title="Shell interpreter used as MCP server command",
48
+ message=(
49
+ f"Server '{server_name}' is launched with '{cmd}', a shell "
50
+ f"interpreter. The MCP client will execute whatever this "
51
+ f"shell decides to run. Any prompt injection reaching this "
52
+ f"server can achieve arbitrary command execution on the host."
53
+ ),
54
+ location=f"mcpServers.{server_name}.command",
55
+ remediation=(
56
+ "Invoke the MCP server binary directly (e.g. via npx, uvx, "
57
+ "or the absolute path to the installed binary) instead of "
58
+ "wrapping it in a shell."
59
+ ),
60
+ references=self.METADATA.references,
61
+ )
62
+ )
63
+
64
+ # Check for curl-pipe-shell in args
65
+ full_command = " ".join([cmd] + server.args).lower()
66
+ for pattern in UNVERIFIED_REMOTE_EXEC:
67
+ if all(p in full_command for p in pattern):
68
+ findings.append(
69
+ Finding(
70
+ rule_id=self.METADATA.id,
71
+ severity=Severity.CRITICAL,
72
+ title="curl-pipe-shell pattern in server command",
73
+ message=(
74
+ f"Server '{server_name}' fetches and executes remote code "
75
+ f"without verification. This is a supply-chain attack "
76
+ f"waiting to happen — anyone controlling the remote URL "
77
+ f"can push arbitrary code to your machine at any time."
78
+ ),
79
+ location=f"mcpServers.{server_name}",
80
+ remediation=(
81
+ "Pin the server to a specific verified version and install "
82
+ "it via a package manager with signature verification."
83
+ ),
84
+ references=self.METADATA.references,
85
+ )
86
+ )
87
+ break
88
+
89
+ return findings
@@ -0,0 +1,153 @@
1
+ """Detect hook injection and post-install script patterns.
2
+
3
+ Covers the attack class disclosed by Check Point in Feb 2026:
4
+ malicious .claude/settings.json hooks that execute arbitrary code
5
+ on operations like git clone, file open, or project init.
6
+
7
+ Also detects suspicious patterns in MCP server args that suggest
8
+ the server is being used as a hook delivery mechanism.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+
15
+ from tessen.models import ParseResult
16
+ from tessen.rules.base import Rule
17
+ from tessen.rules.models import Finding, RuleMetadata, Severity
18
+
19
+ # Commands that suggest remote code fetch + execution in hook context.
20
+ REMOTE_EXEC_PATTERNS = [
21
+ re.compile(r"curl\s+.+\|\s*(sh|bash|zsh|python)", re.IGNORECASE),
22
+ re.compile(r"wget\s+.+\|\s*(sh|bash|zsh|python)", re.IGNORECASE),
23
+ re.compile(r"curl\s+.+-o\s+\S+\s*&&\s*(sh|bash|chmod)", re.IGNORECASE),
24
+ re.compile(r"eval\s*\$\(curl", re.IGNORECASE),
25
+ re.compile(r"eval\s*\$\(wget", re.IGNORECASE),
26
+ re.compile(r"python\s+-c\s+.*import\s+(urllib|requests|http)", re.IGNORECASE),
27
+ ]
28
+
29
+ # Field names in extra config that look like hooks.
30
+ HOOK_FIELD_NAMES = re.compile(
31
+ r"(?i)(hook|on[_-]?(start|stop|init|install|open|clone|attach|connect|load)"
32
+ r"|pre[_-]?(start|init|install)|post[_-]?(start|init|install)"
33
+ r"|setup[_-]?command|init[_-]?command|startup[_-]?command)"
34
+ )
35
+
36
+ # Common benign hook values (npm lifecycle scripts, etc.)
37
+ BENIGN_HOOKS = {
38
+ "npm install",
39
+ "npm ci",
40
+ "npm run build",
41
+ "pip install -r requirements.txt",
42
+ "uv sync",
43
+ "yarn install",
44
+ "pnpm install",
45
+ }
46
+
47
+
48
+ class HookInjectionRule(Rule):
49
+ METADATA = RuleMetadata(
50
+ id="tessen-hook-injection",
51
+ name="Suspicious hook or startup script pattern",
52
+ description=(
53
+ "An MCP server configuration contains hook fields or startup "
54
+ "commands that could execute arbitrary code. This matches the "
55
+ "attack class disclosed by Check Point (Feb 2026) where malicious "
56
+ ".claude/settings.json hooks achieved RCE on git clone."
57
+ ),
58
+ severity=Severity.CRITICAL,
59
+ references=[
60
+ "https://blog.checkpoint.com/research/the-stealthy-satisfyer-exposing-claude-codes-hidden-risks/",
61
+ ],
62
+ )
63
+
64
+ def check(self, result: ParseResult) -> list[Finding]:
65
+ findings: list[Finding] = []
66
+
67
+ for server_name, server in result.config.mcpServers.items():
68
+ extra = server.model_extra or {}
69
+
70
+ # Check extra fields for hook-like field names
71
+ for field_name, value in extra.items():
72
+ if not HOOK_FIELD_NAMES.search(field_name):
73
+ continue
74
+
75
+ str_value = self._to_string(value)
76
+ if not str_value:
77
+ continue
78
+
79
+ if str_value.strip().lower() in BENIGN_HOOKS:
80
+ continue
81
+
82
+ severity = Severity.CRITICAL
83
+ message_extra = ""
84
+
85
+ if self._has_remote_exec(str_value):
86
+ message_extra = (
87
+ " The hook fetches and executes remote code, which means "
88
+ "anyone controlling the remote URL can push arbitrary "
89
+ "code to your machine."
90
+ )
91
+ else:
92
+ severity = Severity.HIGH
93
+ message_extra = (
94
+ " While the hook doesn't fetch remote code, it still "
95
+ "executes on a lifecycle event without explicit user "
96
+ "consent."
97
+ )
98
+
99
+ findings.append(
100
+ Finding(
101
+ rule_id=self.METADATA.id,
102
+ severity=severity,
103
+ title=f"Hook script in server '{server_name}'",
104
+ message=(
105
+ f"Server '{server_name}' has a hook field "
106
+ f"'{field_name}' set to: {str_value!r}.{message_extra}"
107
+ ),
108
+ location=f"mcpServers.{server_name}.{field_name}",
109
+ remediation=(
110
+ "Remove the hook field or replace it with a known-safe "
111
+ "command. If a startup script is necessary, audit it "
112
+ "manually and pin it to a local path under version control."
113
+ ),
114
+ references=self.METADATA.references,
115
+ )
116
+ )
117
+
118
+ # Also check args for remote-exec patterns
119
+ for i, arg in enumerate(server.args):
120
+ if self._has_remote_exec(arg):
121
+ findings.append(
122
+ Finding(
123
+ rule_id=self.METADATA.id,
124
+ severity=Severity.CRITICAL,
125
+ title="Remote code execution in server args",
126
+ message=(
127
+ f"Argument {i} of server '{server_name}' contains "
128
+ f"a pattern that fetches and executes remote code: "
129
+ f"{arg!r}. This is a supply-chain attack vector."
130
+ ),
131
+ location=f"mcpServers.{server_name}.args[{i}]",
132
+ remediation=(
133
+ "Install the server via a package manager with "
134
+ "version pinning. Do not fetch and execute scripts "
135
+ "from URLs at runtime."
136
+ ),
137
+ references=self.METADATA.references,
138
+ )
139
+ )
140
+
141
+ return findings
142
+
143
+ @staticmethod
144
+ def _has_remote_exec(value: str) -> bool:
145
+ return any(p.search(value) for p in REMOTE_EXEC_PATTERNS)
146
+
147
+ @staticmethod
148
+ def _to_string(value) -> str | None:
149
+ if isinstance(value, str):
150
+ return value
151
+ if isinstance(value, list):
152
+ return " && ".join(str(v) for v in value)
153
+ return None
@@ -0,0 +1,99 @@
1
+ """Detect MCP servers using HTTP/SSE transport without authentication."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from tessen.models import ParseResult
6
+ from tessen.rules.base import Rule
7
+ from tessen.rules.models import Finding, RuleMetadata, Severity
8
+
9
+ # Fields that suggest auth is configured — checked in the server's extra fields
10
+ # and env vars.
11
+ AUTH_SIGNALS = {
12
+ "authorization",
13
+ "auth",
14
+ "token",
15
+ "api_key",
16
+ "apikey",
17
+ "api-key",
18
+ "bearer",
19
+ "secret",
20
+ "password",
21
+ "credentials",
22
+ }
23
+
24
+
25
+ class HttpNoAuthRule(Rule):
26
+ METADATA = RuleMetadata(
27
+ id="tessen-http-no-auth",
28
+ name="HTTP/SSE transport without authentication",
29
+ description=(
30
+ "An MCP server is configured with HTTP or SSE transport but no "
31
+ "authentication mechanism is apparent. Anyone who can reach the "
32
+ "server's URL can invoke its tools."
33
+ ),
34
+ severity=Severity.HIGH,
35
+ references=[
36
+ "https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/",
37
+ ],
38
+ )
39
+
40
+ def check(self, result: ParseResult) -> list[Finding]:
41
+ findings: list[Finding] = []
42
+
43
+ for server_name, server in result.config.mcpServers.items():
44
+ # Only applies to HTTP/SSE transports
45
+ transport = getattr(server, "transport", None)
46
+ url = getattr(server, "url", None)
47
+
48
+ # Also check extra fields (Pydantic extra="allow") for transport/url
49
+ extra = server.model_extra or {}
50
+ if transport is None:
51
+ transport = extra.get("transport")
52
+ if url is None:
53
+ url = extra.get("url")
54
+
55
+ if transport not in ("http", "sse") and url is None:
56
+ continue
57
+
58
+ # Check if there's any auth signal in env vars or extra fields
59
+ has_auth = self._has_auth_signal(server.env, extra)
60
+
61
+ if not has_auth:
62
+ display_url = url or "(no URL specified)"
63
+ findings.append(
64
+ Finding(
65
+ rule_id=self.METADATA.id,
66
+ severity=self.METADATA.severity,
67
+ title=f"Unauthenticated {transport or 'HTTP'} transport",
68
+ message=(
69
+ f"Server '{server_name}' uses {transport or 'HTTP'} transport "
70
+ f"at {display_url} with no visible authentication. "
71
+ f"Any network-adjacent attacker can invoke this server's tools, "
72
+ f"read its resources, and potentially pivot to the host."
73
+ ),
74
+ location=f"mcpServers.{server_name}",
75
+ remediation=(
76
+ "Add authentication to the transport layer. Use an API key "
77
+ "in an Authorization header (via env var reference), or "
78
+ "restrict the server to localhost with a firewall rule."
79
+ ),
80
+ references=self.METADATA.references,
81
+ )
82
+ )
83
+
84
+ return findings
85
+
86
+ @staticmethod
87
+ def _has_auth_signal(env: dict[str, str], extra: dict) -> bool:
88
+ """Check if any field name suggests auth is configured."""
89
+ # Check env var names
90
+ for name in env:
91
+ if any(signal in name.lower() for signal in AUTH_SIGNALS):
92
+ return True
93
+
94
+ # Check extra config fields
95
+ for key in extra:
96
+ if any(signal in key.lower() for signal in AUTH_SIGNALS):
97
+ return True
98
+
99
+ return False
@@ -0,0 +1,159 @@
1
+ """Detect MCP servers with known vulnerabilities (CVE feed)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from tessen.models import ParseResult
8
+ from tessen.rules.base import Rule
9
+ from tessen.rules.models import Finding, RuleMetadata, Severity
10
+
11
+
12
+ # The vulnerability database. Each entry matches against the npm package name
13
+ # that appears in the `args` of an `npx -y <package>` invocation.
14
+ #
15
+ # This is the seed of the Tessen CVE feed. In v0.2 this moves to a
16
+ # separately-versioned JSON file that can be updated independently of the CLI.
17
+
18
+ VULNERABLE_SERVERS: list[dict] = [
19
+ {
20
+ "package": "@anthropic/mcp-inspector",
21
+ "cve": "CVE-2025-49596",
22
+ "title": "Command injection in MCP Inspector",
23
+ "severity": Severity.CRITICAL,
24
+ "description": (
25
+ "MCP Inspector versions before the patched release are vulnerable to "
26
+ "command injection via crafted server names. An attacker who controls "
27
+ "the server name can execute arbitrary commands on the host."
28
+ ),
29
+ "references": ["https://nvd.nist.gov/vuln/detail/CVE-2025-49596"],
30
+ },
31
+ {
32
+ "package": "librechat-mcp",
33
+ "cve": "CVE-2026-22252",
34
+ "title": "Injection vulnerability in LibreChat MCP integration",
35
+ "severity": Severity.HIGH,
36
+ "description": (
37
+ "LibreChat's MCP integration is vulnerable to injection attacks "
38
+ "through crafted tool descriptions, allowing prompt injection "
39
+ "that can exfiltrate conversation data."
40
+ ),
41
+ "references": ["https://nvd.nist.gov/vuln/detail/CVE-2026-22252"],
42
+ },
43
+ {
44
+ "package": "weknora-mcp",
45
+ "cve": "CVE-2026-22688",
46
+ "title": "Remote code execution in WeKnora MCP server",
47
+ "severity": Severity.CRITICAL,
48
+ "description": (
49
+ "WeKnora MCP server allows remote code execution through "
50
+ "unsanitized input in tool call parameters."
51
+ ),
52
+ "references": ["https://nvd.nist.gov/vuln/detail/CVE-2026-22688"],
53
+ },
54
+ {
55
+ "package": "@anthropic/claude-code",
56
+ "cve": "CVE-2026-21852",
57
+ "title": "API key theft via Claude Code MCP consent bypass",
58
+ "severity": Severity.CRITICAL,
59
+ "description": (
60
+ "Claude Code versions before the patched release allow a malicious "
61
+ "MCP server to exfiltrate API keys by bypassing the tool consent "
62
+ "mechanism."
63
+ ),
64
+ "references": ["https://nvd.nist.gov/vuln/detail/CVE-2026-21852"],
65
+ },
66
+ {
67
+ "package": "windsurf-mcp",
68
+ "cve": "CVE-2026-30615",
69
+ "title": "Prompt injection leading to RCE in Windsurf",
70
+ "severity": Severity.CRITICAL,
71
+ "description": (
72
+ "Windsurf's MCP integration is vulnerable to prompt injection "
73
+ "that escalates to remote code execution on the developer's machine."
74
+ ),
75
+ "references": ["https://nvd.nist.gov/vuln/detail/CVE-2026-30615"],
76
+ },
77
+ ]
78
+
79
+ # Pattern to extract npm package name from npx args.
80
+ # Matches: npx -y @scope/package, npx -y package, npx -y package@version
81
+ NPX_PACKAGE_PATTERN = re.compile(
82
+ r"^(?:-[a-zA-Z]+\s+)*" # optional flags like -y
83
+ r"((?:@[\w-]+/)?[\w.-]+)" # package name (with optional scope)
84
+ )
85
+
86
+
87
+ class KnownVulnerableServerRule(Rule):
88
+ METADATA = RuleMetadata(
89
+ id="tessen-known-vulnerable-server",
90
+ name="MCP server with known CVE",
91
+ description=(
92
+ "The configured MCP server matches a package with a known "
93
+ "vulnerability in the Tessen CVE database."
94
+ ),
95
+ severity=Severity.CRITICAL,
96
+ references=[
97
+ "https://github.com/SlimBenTanfous1/tessen-cli",
98
+ ],
99
+ )
100
+
101
+ def check(self, result: ParseResult) -> list[Finding]:
102
+ findings: list[Finding] = []
103
+
104
+ for server_name, server in result.config.mcpServers.items():
105
+ package = self._extract_package(server.command, server.args)
106
+ if package is None:
107
+ continue
108
+
109
+ for vuln in VULNERABLE_SERVERS:
110
+ if self._matches(package, vuln["package"]):
111
+ findings.append(
112
+ Finding(
113
+ rule_id=self.METADATA.id,
114
+ severity=vuln["severity"],
115
+ title=f"{vuln['cve']}: {vuln['title']}",
116
+ message=(
117
+ f"Server '{server_name}' uses package '{package}' "
118
+ f"which has a known vulnerability ({vuln['cve']}). "
119
+ f"{vuln['description']}"
120
+ ),
121
+ location=f"mcpServers.{server_name}",
122
+ remediation=(
123
+ "Update to the latest patched version, or remove "
124
+ "the server if it's not essential. Check the CVE "
125
+ "for specific remediation guidance."
126
+ ),
127
+ references=vuln["references"],
128
+ )
129
+ )
130
+
131
+ return findings
132
+
133
+ @staticmethod
134
+ def _extract_package(command: str, args: list[str]) -> str | None:
135
+ """Extract the npm package name from an npx/uvx invocation."""
136
+ cmd_base = command.rsplit("/", 1)[-1]
137
+
138
+ if cmd_base in ("npx", "npx.cmd"):
139
+ # Join args and extract the package name after flags
140
+ args_str = " ".join(args)
141
+ match = NPX_PACKAGE_PATTERN.search(args_str)
142
+ if match:
143
+ # Strip version suffix if present (@1.2.3)
144
+ pkg = match.group(1)
145
+ pkg = re.sub(r"@[\d.].*$", "", pkg)
146
+ return pkg
147
+
148
+ if cmd_base in ("uvx", "pipx"):
149
+ # Python tools: first non-flag arg is the package
150
+ for arg in args:
151
+ if not arg.startswith("-"):
152
+ return arg.split("==")[0].split(">=")[0]
153
+
154
+ return None
155
+
156
+ @staticmethod
157
+ def _matches(installed: str, vulnerable: str) -> bool:
158
+ """Check if installed package matches a known-vulnerable package."""
159
+ return installed.lower() == vulnerable.lower()