coding-agent-audit 0.9.0__tar.gz → 0.10.0__tar.gz

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.
Files changed (24) hide show
  1. {coding_agent_audit-0.9.0/src/coding_agent_audit.egg-info → coding_agent_audit-0.10.0}/PKG-INFO +11 -1
  2. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/README.md +10 -0
  3. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/pyproject.toml +1 -1
  4. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/src/agentaudit/__init__.py +1 -1
  5. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/src/agentaudit/configscan.py +90 -0
  6. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0/src/coding_agent_audit.egg-info}/PKG-INFO +11 -1
  7. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/tests/test_configscan.py +59 -0
  8. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/LICENSE +0 -0
  9. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/setup.cfg +0 -0
  10. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/src/agentaudit/checks.py +0 -0
  11. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/src/agentaudit/cli.py +0 -0
  12. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/src/agentaudit/findings.py +0 -0
  13. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/src/agentaudit/iocs.py +0 -0
  14. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/src/agentaudit/mcp_server.py +0 -0
  15. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/src/agentaudit/phantomdeps.py +0 -0
  16. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/src/coding_agent_audit.egg-info/SOURCES.txt +0 -0
  17. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/src/coding_agent_audit.egg-info/dependency_links.txt +0 -0
  18. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/src/coding_agent_audit.egg-info/entry_points.txt +0 -0
  19. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/src/coding_agent_audit.egg-info/requires.txt +0 -0
  20. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/src/coding_agent_audit.egg-info/top_level.txt +0 -0
  21. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/tests/test_checks.py +0 -0
  22. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/tests/test_cli.py +0 -0
  23. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/tests/test_mcp_server.py +0 -0
  24. {coding_agent_audit-0.9.0 → coding_agent_audit-0.10.0}/tests/test_phantomdeps.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-agent-audit
3
- Version: 0.9.0
3
+ Version: 0.10.0
4
4
  Summary: Scan your machine for known AI-coding-assistant supply-chain backdoor indicators (Claude Code / Cursor / Windsurf extensions, MCP servers) plus risky hook/permission config patterns. Usable as a CLI or as an MCP server.
5
5
  Author-email: StackScribe AI <survivalagent320@gmail.com>
6
6
  License: MIT
@@ -134,6 +134,16 @@ second look rather than proof of compromise:
134
134
  `authorized_keys`, use netcat in reverse-shell mode, etc.
135
135
  - MCP server definitions that run an unpinned package (`npx -y pkg` with
136
136
  no version pin)
137
+ - MCP server definitions explicitly pinned to a package version with a
138
+ disclosed, fixed RCE CVE (added in v0.10.0): `mcp-remote` below v0.1.16
139
+ (**CVE-2025-6514**, CVSS 9.6 — a malicious remote server's OAuth
140
+ `authorization_endpoint` response is passed unsanitized to the OS,
141
+ arbitrary command execution on just opening the client) and
142
+ `mcp-server-git` below v2025.12.18 (**CVE-2025-68143/68144/68145** —
143
+ Anthropic's own reference git MCP server, chainable via indirect prompt
144
+ injection into full RCE). Only an explicit old pin is flagged — an
145
+ unpinned install picks up the current, patched release and is already
146
+ covered by the generic unpinned-package finding above
137
147
  - Auto-approved permission rules that are unscoped wildcards (e.g.
138
148
  `Bash(*)`)
139
149
  - The "lethal trifecta" pattern: an untrusted-content-fetching tool
@@ -113,6 +113,16 @@ second look rather than proof of compromise:
113
113
  `authorized_keys`, use netcat in reverse-shell mode, etc.
114
114
  - MCP server definitions that run an unpinned package (`npx -y pkg` with
115
115
  no version pin)
116
+ - MCP server definitions explicitly pinned to a package version with a
117
+ disclosed, fixed RCE CVE (added in v0.10.0): `mcp-remote` below v0.1.16
118
+ (**CVE-2025-6514**, CVSS 9.6 — a malicious remote server's OAuth
119
+ `authorization_endpoint` response is passed unsanitized to the OS,
120
+ arbitrary command execution on just opening the client) and
121
+ `mcp-server-git` below v2025.12.18 (**CVE-2025-68143/68144/68145** —
122
+ Anthropic's own reference git MCP server, chainable via indirect prompt
123
+ injection into full RCE). Only an explicit old pin is flagged — an
124
+ unpinned install picks up the current, patched release and is already
125
+ covered by the generic unpinned-package finding above
116
126
  - Auto-approved permission rules that are unscoped wildcards (e.g.
117
127
  `Bash(*)`)
118
128
  - The "lethal trifecta" pattern: an untrusted-content-fetching tool
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "coding-agent-audit"
7
- version = "0.9.0"
7
+ version = "0.10.0"
8
8
  description = "Scan your machine for known AI-coding-assistant supply-chain backdoor indicators (Claude Code / Cursor / Windsurf extensions, MCP servers) plus risky hook/permission config patterns. Usable as a CLI or as an MCP server."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -3,4 +3,4 @@ for AI coding assistants (Claude Code, Cursor, Windsurf, VS Code forks, MCP
3
3
  servers).
4
4
  """
5
5
 
6
- __version__ = "0.9.0"
6
+ __version__ = "0.10.0"
@@ -154,6 +154,95 @@ def scan_hooks(config: dict, source_path: str) -> list[Finding]:
154
154
  return findings
155
155
 
156
156
 
157
+ # Packages with a known, disclosed RCE/command-injection CVE in an old
158
+ # version range that still shows up pinned in copy-pasted configs/tutorials
159
+ # written before the fix shipped. `fixed_version` is the first version where
160
+ # the chain is fully closed; anything strictly older is still exploitable.
161
+ _KNOWN_VULNERABLE_PACKAGES = [
162
+ (
163
+ "mcp-remote",
164
+ (0, 1, 16),
165
+ ("CVE-2025-6514",),
166
+ "OS command injection (CVSS 9.6): a malicious/compromised remote MCP "
167
+ "server can return an `authorization_endpoint` URL during the OAuth "
168
+ "handshake that mcp-remote passes unsanitized to the OS, achieving "
169
+ "full remote code execution with no further user interaction beyond "
170
+ "opening the client. Affects v0.0.5-v0.1.15.",
171
+ ),
172
+ (
173
+ "mcp-server-git",
174
+ (2025, 12, 18),
175
+ ("CVE-2025-68143", "CVE-2025-68144", "CVE-2025-68145"),
176
+ "Chained path-validation bypass, unrestricted git_init, and argument "
177
+ "injection in Anthropic's reference git MCP server, usable via "
178
+ "indirect prompt injection (e.g. a planted README) to reach RCE "
179
+ "together with a filesystem MCP server. Fully fixed in v2025.12.18.",
180
+ ),
181
+ ]
182
+
183
+
184
+ def _parse_leading_version(text: str) -> tuple[int, ...] | None:
185
+ match = re.match(r"^(\d+(?:\.\d+)*)", text)
186
+ if not match:
187
+ return None
188
+ return tuple(int(part) for part in match.group(1).split("."))
189
+
190
+
191
+ def _version_lt(a: tuple[int, ...], b: tuple[int, ...]) -> bool:
192
+ n = max(len(a), len(b))
193
+ a_padded = a + (0,) * (n - len(a))
194
+ b_padded = b + (0,) * (n - len(b))
195
+ return a_padded < b_padded
196
+
197
+
198
+ def scan_known_cve_packages(config: dict, source_path: str) -> list[Finding]:
199
+ """Flag MCP server commands pinned to a package version with a
200
+ disclosed, fixed RCE/command-injection CVE (see
201
+ ``_KNOWN_VULNERABLE_PACKAGES``). Unpinned installs are not flagged here -
202
+ they pick up the current (patched) release and are already covered by
203
+ the generic 'unpinned package' info finding in `scan_mcp_servers`.
204
+ """
205
+ servers = config.get("mcpServers") or config.get("mcp_servers")
206
+ if not isinstance(servers, dict):
207
+ return []
208
+ findings: list[Finding] = []
209
+ for server_name, server_cfg in servers.items():
210
+ if not isinstance(server_cfg, dict):
211
+ continue
212
+ command = server_cfg.get("command", "")
213
+ args = server_cfg.get("args") or []
214
+ full_cmdline = " ".join([command, *[str(a) for a in args]])
215
+
216
+ for pkg_name, fixed_version, cve_ids, description in _KNOWN_VULNERABLE_PACKAGES:
217
+ pin_match = re.search(
218
+ rf"\b{re.escape(pkg_name)}(?:@|==)([0-9][0-9.]*)", full_cmdline
219
+ )
220
+ if not pin_match:
221
+ continue
222
+ pinned_version = _parse_leading_version(pin_match.group(1))
223
+ if pinned_version is None or not _version_lt(pinned_version, fixed_version):
224
+ continue
225
+ findings.append(
226
+ Finding(
227
+ check_id="mcp-known-cve-package",
228
+ severity="critical",
229
+ title=(
230
+ f"MCP server '{server_name}' pins {pkg_name}@"
231
+ f"{pin_match.group(1)}, vulnerable to {', '.join(cve_ids)}"
232
+ ),
233
+ detail=(
234
+ f"{description} The config explicitly pins a version "
235
+ f"older than the fixed {'.'.join(map(str, fixed_version))}, "
236
+ f"so upgrading `{pkg_name}` globally will not help "
237
+ f"until this pin is also updated."
238
+ ),
239
+ path=source_path,
240
+ references=list(cve_ids),
241
+ )
242
+ )
243
+ return findings
244
+
245
+
157
246
  def scan_mcp_servers(config: dict, source_path: str) -> list[Finding]:
158
247
  servers = config.get("mcpServers") or config.get("mcp_servers")
159
248
  if not isinstance(servers, dict):
@@ -293,6 +382,7 @@ def scan_config_dict(config: dict, source_path: str) -> list[Finding]:
293
382
  findings: list[Finding] = []
294
383
  findings += scan_hooks(config, source_path)
295
384
  findings += scan_mcp_servers(config, source_path)
385
+ findings += scan_known_cve_packages(config, source_path)
296
386
  findings += scan_permissions(config, source_path)
297
387
  findings += scan_lethal_trifecta(config, source_path)
298
388
  return findings
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-agent-audit
3
- Version: 0.9.0
3
+ Version: 0.10.0
4
4
  Summary: Scan your machine for known AI-coding-assistant supply-chain backdoor indicators (Claude Code / Cursor / Windsurf extensions, MCP servers) plus risky hook/permission config patterns. Usable as a CLI or as an MCP server.
5
5
  Author-email: StackScribe AI <survivalagent320@gmail.com>
6
6
  License: MIT
@@ -134,6 +134,16 @@ second look rather than proof of compromise:
134
134
  `authorized_keys`, use netcat in reverse-shell mode, etc.
135
135
  - MCP server definitions that run an unpinned package (`npx -y pkg` with
136
136
  no version pin)
137
+ - MCP server definitions explicitly pinned to a package version with a
138
+ disclosed, fixed RCE CVE (added in v0.10.0): `mcp-remote` below v0.1.16
139
+ (**CVE-2025-6514**, CVSS 9.6 — a malicious remote server's OAuth
140
+ `authorization_endpoint` response is passed unsanitized to the OS,
141
+ arbitrary command execution on just opening the client) and
142
+ `mcp-server-git` below v2025.12.18 (**CVE-2025-68143/68144/68145** —
143
+ Anthropic's own reference git MCP server, chainable via indirect prompt
144
+ injection into full RCE). Only an explicit old pin is flagged — an
145
+ unpinned install picks up the current, patched release and is already
146
+ covered by the generic unpinned-package finding above
137
147
  - Auto-approved permission rules that are unscoped wildcards (e.g.
138
148
  `Bash(*)`)
139
149
  - The "lethal trifecta" pattern: an untrusted-content-fetching tool
@@ -4,6 +4,7 @@ from agentaudit.configscan import (
4
4
  default_config_paths,
5
5
  scan_config_dict,
6
6
  scan_hooks,
7
+ scan_known_cve_packages,
7
8
  scan_lethal_trifecta,
8
9
  scan_mcp_servers,
9
10
  scan_paths,
@@ -96,6 +97,64 @@ def test_scan_mcp_servers_not_a_dict():
96
97
  assert scan_mcp_servers({"mcpServers": "nope"}, "src") == []
97
98
 
98
99
 
100
+ def test_scan_known_cve_packages_vulnerable_mcp_remote_pin():
101
+ config = {
102
+ "mcpServers": {
103
+ "remote": {
104
+ "command": "npx",
105
+ "args": ["-y", "mcp-remote@0.1.9", "http://example.com/mcp"],
106
+ }
107
+ }
108
+ }
109
+ findings = scan_known_cve_packages(config, "src")
110
+ assert len(findings) == 1
111
+ assert findings[0].check_id == "mcp-known-cve-package"
112
+ assert findings[0].severity == "critical"
113
+ assert "CVE-2025-6514" in findings[0].references
114
+
115
+
116
+ def test_scan_known_cve_packages_vulnerable_git_server_pin():
117
+ config = {
118
+ "mcpServers": {
119
+ "git": {
120
+ "command": "uvx",
121
+ "args": ["mcp-server-git==2025.9.1", "--repository", "/repo"],
122
+ }
123
+ }
124
+ }
125
+ findings = scan_known_cve_packages(config, "src")
126
+ assert len(findings) == 1
127
+ assert "CVE-2025-68143" in findings[0].references[0]
128
+
129
+
130
+ def test_scan_known_cve_packages_fixed_version_ok():
131
+ config = {
132
+ "mcpServers": {
133
+ "remote": {"command": "npx", "args": ["-y", "mcp-remote@0.1.16", "https://example.com"]},
134
+ "git": {"command": "uvx", "args": ["mcp-server-git==2025.12.18", "--repository", "/repo"]},
135
+ }
136
+ }
137
+ assert scan_known_cve_packages(config, "src") == []
138
+
139
+
140
+ def test_scan_known_cve_packages_unpinned_not_flagged():
141
+ config = {
142
+ "mcpServers": {"remote": {"command": "npx", "args": ["-y", "mcp-remote", "https://example.com"]}}
143
+ }
144
+ assert scan_known_cve_packages(config, "src") == []
145
+
146
+
147
+ def test_scan_known_cve_packages_unrelated_package_ignored():
148
+ config = {
149
+ "mcpServers": {"fs": {"command": "npx", "args": ["-y", "@scope/pkg@0.0.1"]}}
150
+ }
151
+ assert scan_known_cve_packages(config, "src") == []
152
+
153
+
154
+ def test_scan_known_cve_packages_not_a_dict():
155
+ assert scan_known_cve_packages({"mcpServers": "nope"}, "src") == []
156
+
157
+
99
158
  def test_scan_permissions_scoped_rule_ok():
100
159
  config = {"permissions": {"allow": ["Bash(git diff:*)", "Read(//workspace/**)"]}}
101
160
  assert scan_permissions(config, "src") == []