coding-agent-audit 0.10.0__tar.gz → 0.11.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.
- {coding_agent_audit-0.10.0/src/coding_agent_audit.egg-info → coding_agent_audit-0.11.0}/PKG-INFO +11 -1
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/README.md +10 -0
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/pyproject.toml +1 -1
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/src/agentaudit/__init__.py +1 -1
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/src/agentaudit/cli.py +19 -10
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/src/agentaudit/configscan.py +95 -0
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/src/agentaudit/mcp_server.py +25 -1
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0/src/coding_agent_audit.egg-info}/PKG-INFO +11 -1
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/tests/test_configscan.py +93 -0
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/LICENSE +0 -0
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/setup.cfg +0 -0
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/src/agentaudit/checks.py +0 -0
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/src/agentaudit/findings.py +0 -0
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/src/agentaudit/iocs.py +0 -0
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/src/agentaudit/phantomdeps.py +0 -0
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/src/coding_agent_audit.egg-info/SOURCES.txt +0 -0
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/src/coding_agent_audit.egg-info/dependency_links.txt +0 -0
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/src/coding_agent_audit.egg-info/entry_points.txt +0 -0
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/src/coding_agent_audit.egg-info/requires.txt +0 -0
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/src/coding_agent_audit.egg-info/top_level.txt +0 -0
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/tests/test_checks.py +0 -0
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/tests/test_cli.py +0 -0
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/tests/test_mcp_server.py +0 -0
- {coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/tests/test_phantomdeps.py +0 -0
{coding_agent_audit-0.10.0/src/coding_agent_audit.egg-info → coding_agent_audit-0.11.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-agent-audit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.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
|
|
@@ -183,6 +183,16 @@ next `npm install` / `pip install` ("slopsquatting"). Network errors are
|
|
|
183
183
|
treated as inconclusive, not as a finding, so a flaky connection can't
|
|
184
184
|
produce a false positive.
|
|
185
185
|
|
|
186
|
+
**MCP phantom-package scan** (`scan_mcp_phantom_packages`, also opt-in via
|
|
187
|
+
`--check-deps`) — the same registry-lookup idea, applied to the MCP server
|
|
188
|
+
commands themselves: any `npx`/`uvx`/`pipx run` invocation in a scanned
|
|
189
|
+
config is checked against npm/PyPI. This is the "MCPhantom" pattern flagged
|
|
190
|
+
by Glow Labs in November 2026: a coding agent fills in a plausible but
|
|
191
|
+
nonexistent MCP server package name from training data (or copies one from
|
|
192
|
+
a long-archived tutorial whose package was since renamed/unpublished), and
|
|
193
|
+
nothing stops an attacker from registering that exact name and having it
|
|
194
|
+
run as the MCP server next time the config loads.
|
|
195
|
+
|
|
186
196
|
New incidents can be added to `iocs.py` as data, without touching the
|
|
187
197
|
check logic.
|
|
188
198
|
|
|
@@ -162,6 +162,16 @@ next `npm install` / `pip install` ("slopsquatting"). Network errors are
|
|
|
162
162
|
treated as inconclusive, not as a finding, so a flaky connection can't
|
|
163
163
|
produce a false positive.
|
|
164
164
|
|
|
165
|
+
**MCP phantom-package scan** (`scan_mcp_phantom_packages`, also opt-in via
|
|
166
|
+
`--check-deps`) — the same registry-lookup idea, applied to the MCP server
|
|
167
|
+
commands themselves: any `npx`/`uvx`/`pipx run` invocation in a scanned
|
|
168
|
+
config is checked against npm/PyPI. This is the "MCPhantom" pattern flagged
|
|
169
|
+
by Glow Labs in November 2026: a coding agent fills in a plausible but
|
|
170
|
+
nonexistent MCP server package name from training data (or copies one from
|
|
171
|
+
a long-archived tutorial whose package was since renamed/unpublished), and
|
|
172
|
+
nothing stops an attacker from registering that exact name and having it
|
|
173
|
+
run as the MCP server next time the config loads.
|
|
174
|
+
|
|
165
175
|
New incidents can be added to `iocs.py` as data, without touching the
|
|
166
176
|
check logic.
|
|
167
177
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "coding-agent-audit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.11.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"
|
|
@@ -7,7 +7,12 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from . import __version__
|
|
9
9
|
from .checks import run_all_ioc_checks
|
|
10
|
-
from .configscan import
|
|
10
|
+
from .configscan import (
|
|
11
|
+
default_config_paths,
|
|
12
|
+
scan_paths,
|
|
13
|
+
scan_paths_for_phantom_mcp_packages,
|
|
14
|
+
scan_tool_descriptions,
|
|
15
|
+
)
|
|
11
16
|
from .findings import Finding, worst_severity
|
|
12
17
|
from .phantomdeps import check_phantom_dependencies
|
|
13
18
|
|
|
@@ -65,10 +70,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
65
70
|
"--check-deps",
|
|
66
71
|
action="store_true",
|
|
67
72
|
help=(
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
73
|
+
"check package names against npm/PyPI and flag any that don't "
|
|
74
|
+
"exist (possible AI-hallucinated/slopsquattable or 'MCPhantom' "
|
|
75
|
+
"package name): with --project, also covers package.json/"
|
|
76
|
+
"requirements.txt/pyproject.toml; always covers npx/uvx/pipx "
|
|
77
|
+
"MCP server commands in the scanned config files. Makes "
|
|
78
|
+
"read-only network requests."
|
|
72
79
|
),
|
|
73
80
|
)
|
|
74
81
|
parser.add_argument(
|
|
@@ -95,17 +102,19 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
95
102
|
parser = build_parser()
|
|
96
103
|
args = parser.parse_args(argv)
|
|
97
104
|
|
|
105
|
+
config_paths = default_config_paths(Path.home(), args.project)
|
|
106
|
+
|
|
98
107
|
findings: list[Finding] = []
|
|
99
108
|
if not args.skip_ioc:
|
|
100
109
|
project_dirs = [args.project] if args.project is not None else []
|
|
101
110
|
findings += run_all_ioc_checks(project_dirs=project_dirs)
|
|
102
111
|
if not args.skip_config:
|
|
103
|
-
|
|
104
|
-
paths = default_config_paths(home, args.project)
|
|
105
|
-
findings += scan_paths(paths)
|
|
112
|
+
findings += scan_paths(config_paths)
|
|
106
113
|
|
|
107
|
-
if args.check_deps
|
|
108
|
-
|
|
114
|
+
if args.check_deps:
|
|
115
|
+
if args.project is not None:
|
|
116
|
+
findings += check_phantom_dependencies(args.project)
|
|
117
|
+
findings += scan_paths_for_phantom_mcp_packages(config_paths)
|
|
109
118
|
|
|
110
119
|
if args.tools_json is not None:
|
|
111
120
|
try:
|
|
@@ -14,6 +14,7 @@ import re
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Iterable
|
|
16
16
|
|
|
17
|
+
from . import phantomdeps
|
|
17
18
|
from .findings import Finding
|
|
18
19
|
|
|
19
20
|
# Commands/strings that show up in real exfiltration or persistence attempts.
|
|
@@ -287,6 +288,83 @@ def scan_mcp_servers(config: dict, source_path: str) -> list[Finding]:
|
|
|
287
288
|
return findings
|
|
288
289
|
|
|
289
290
|
|
|
291
|
+
_MCP_RUNNER_RE = re.compile(r"\b(npx|uvx|pipx\s+run)\b", re.IGNORECASE)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _extract_mcp_package_spec(full_cmdline: str) -> tuple[str, str] | None:
|
|
295
|
+
"""Return ``(ecosystem, package_name)`` for the package a `npx`/`uvx`/
|
|
296
|
+
`pipx run` MCP server command would fetch, or ``None`` if no such
|
|
297
|
+
runner (or no package argument after it) is found."""
|
|
298
|
+
match = _MCP_RUNNER_RE.search(full_cmdline)
|
|
299
|
+
if not match:
|
|
300
|
+
return None
|
|
301
|
+
ecosystem = "npm" if match.group(1).lower() == "npx" else "pypi"
|
|
302
|
+
for token in full_cmdline[match.end():].split():
|
|
303
|
+
if token.startswith("-"):
|
|
304
|
+
continue
|
|
305
|
+
if token.startswith("@"):
|
|
306
|
+
# scoped npm package, optionally pinned: @scope/name[@version]
|
|
307
|
+
scoped, _, _version = token[1:].partition("@")
|
|
308
|
+
return ecosystem, f"@{scoped}"
|
|
309
|
+
name, _, _version = token.partition("@")
|
|
310
|
+
name = name.split("==", 1)[0]
|
|
311
|
+
return ecosystem, name
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def scan_mcp_phantom_packages(config: dict, source_path: str) -> list[Finding]:
|
|
316
|
+
"""Flag MCP servers launched via `npx`/`uvx`/`pipx run` that point at a
|
|
317
|
+
package name which doesn't exist on the registry - the "MCPhantom"
|
|
318
|
+
pattern (Glow Labs research, Nov 2026): coding agents fill in a
|
|
319
|
+
plausible-sounding MCP server name from training data, or copy one
|
|
320
|
+
from a long-archived tutorial/repo whose package was since renamed or
|
|
321
|
+
unpublished. Either way, nothing stops an attacker from registering
|
|
322
|
+
that exact name and having it run with the user's own permissions on
|
|
323
|
+
the very next launch. Makes read-only registry lookups; network
|
|
324
|
+
errors are treated as "unknown", never as "phantom" (see phantomdeps).
|
|
325
|
+
"""
|
|
326
|
+
servers = config.get("mcpServers") or config.get("mcp_servers")
|
|
327
|
+
if not isinstance(servers, dict):
|
|
328
|
+
return []
|
|
329
|
+
findings: list[Finding] = []
|
|
330
|
+
for server_name, server_cfg in servers.items():
|
|
331
|
+
if not isinstance(server_cfg, dict):
|
|
332
|
+
continue
|
|
333
|
+
command = server_cfg.get("command", "")
|
|
334
|
+
args = server_cfg.get("args") or []
|
|
335
|
+
full_cmdline = " ".join([command, *[str(a) for a in args]])
|
|
336
|
+
spec = _extract_mcp_package_spec(full_cmdline)
|
|
337
|
+
if spec is None:
|
|
338
|
+
continue
|
|
339
|
+
ecosystem, name = spec
|
|
340
|
+
if ecosystem == "npm":
|
|
341
|
+
if not phantomdeps._is_plausible_npm_name(name):
|
|
342
|
+
continue
|
|
343
|
+
exists = phantomdeps.check_npm_package_exists(name)
|
|
344
|
+
else:
|
|
345
|
+
exists = phantomdeps.check_pypi_package_exists(name)
|
|
346
|
+
if exists is not False:
|
|
347
|
+
continue
|
|
348
|
+
findings.append(
|
|
349
|
+
Finding(
|
|
350
|
+
check_id="mcp-phantom-package",
|
|
351
|
+
severity="critical",
|
|
352
|
+
title=f"MCP server '{server_name}' launches a package that doesn't exist ('{name}')",
|
|
353
|
+
detail=(
|
|
354
|
+
f"Command '{full_cmdline}' fetches '{name}' from "
|
|
355
|
+
f"{'npm' if ecosystem == 'npm' else 'PyPI'} on every launch, but no "
|
|
356
|
+
f"package by that name currently exists there (the "
|
|
357
|
+
f"'MCPhantom' pattern: a hallucinated or long-deprecated "
|
|
358
|
+
f"MCP server name). Anyone can register it and have "
|
|
359
|
+
f"their code run as this MCP server, with no update or "
|
|
360
|
+
f"confirmation step in between."
|
|
361
|
+
),
|
|
362
|
+
path=source_path,
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
return findings
|
|
366
|
+
|
|
367
|
+
|
|
290
368
|
def scan_permissions(config: dict, source_path: str) -> list[Finding]:
|
|
291
369
|
permissions = config.get("permissions")
|
|
292
370
|
if not isinstance(permissions, dict):
|
|
@@ -429,3 +507,20 @@ def scan_paths(paths: Iterable[Path]) -> list[Finding]:
|
|
|
429
507
|
if isinstance(config, dict):
|
|
430
508
|
findings += scan_config_dict(config, str(path))
|
|
431
509
|
return findings
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def scan_paths_for_phantom_mcp_packages(paths: Iterable[Path]) -> list[Finding]:
|
|
513
|
+
"""Like `scan_paths`, but only runs `scan_mcp_phantom_packages` (the
|
|
514
|
+
network-dependent check), for callers that opt in with `--check-deps`.
|
|
515
|
+
"""
|
|
516
|
+
findings: list[Finding] = []
|
|
517
|
+
for path in paths:
|
|
518
|
+
if not path.is_file():
|
|
519
|
+
continue
|
|
520
|
+
try:
|
|
521
|
+
config = json.loads(path.read_text())
|
|
522
|
+
except (OSError, json.JSONDecodeError):
|
|
523
|
+
continue
|
|
524
|
+
if isinstance(config, dict):
|
|
525
|
+
findings += scan_mcp_phantom_packages(config, str(path))
|
|
526
|
+
return findings
|
|
@@ -18,7 +18,7 @@ from mcp.types import ToolAnnotations
|
|
|
18
18
|
|
|
19
19
|
from . import __version__
|
|
20
20
|
from .checks import run_all_ioc_checks
|
|
21
|
-
from .configscan import default_config_paths, scan_paths
|
|
21
|
+
from .configscan import default_config_paths, scan_paths, scan_paths_for_phantom_mcp_packages
|
|
22
22
|
from .findings import Finding, worst_severity
|
|
23
23
|
from .phantomdeps import check_phantom_dependencies
|
|
24
24
|
|
|
@@ -113,6 +113,30 @@ def scan_phantom_dependencies(project_dir: str) -> str:
|
|
|
113
113
|
return _render(check_phantom_dependencies(Path(project_dir)))
|
|
114
114
|
|
|
115
115
|
|
|
116
|
+
@server.tool(annotations=_NETWORK_READ_ONLY)
|
|
117
|
+
def scan_mcp_phantom_packages(project_dir: Optional[str] = None) -> str:
|
|
118
|
+
"""Check the `npx`/`uvx`/`pipx run` commands in this machine's (and
|
|
119
|
+
optionally a project's) MCP server config against the npm/PyPI
|
|
120
|
+
registries, and flag any package name that doesn't actually exist
|
|
121
|
+
there - the "MCPhantom" pattern (Glow Labs, Nov 2026): a coding agent
|
|
122
|
+
fills in a plausible-sounding MCP server name from training data, or
|
|
123
|
+
copies one from a long-archived tutorial whose package was since
|
|
124
|
+
renamed or unpublished. An attacker can register that exact name and
|
|
125
|
+
have it run as the MCP server, with the user's own permissions, the
|
|
126
|
+
next time the config is loaded. Read-only HTTP lookups only; this
|
|
127
|
+
never connects to or executes the MCP server itself.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
project_dir: optional path to a project whose .claude/ and
|
|
131
|
+
.mcp.json should also be scanned, in addition to the user's
|
|
132
|
+
home-directory config.
|
|
133
|
+
"""
|
|
134
|
+
home = Path.home()
|
|
135
|
+
project = Path(project_dir) if project_dir else None
|
|
136
|
+
paths = default_config_paths(home, project)
|
|
137
|
+
return _render(scan_paths_for_phantom_mcp_packages(paths))
|
|
138
|
+
|
|
139
|
+
|
|
116
140
|
@server.tool(annotations=_LOCAL_READ_ONLY)
|
|
117
141
|
def scan_tool_descriptions(tools: list[dict]) -> str:
|
|
118
142
|
"""Scan a list of MCP tool definitions (the 'tools/list' response an
|
{coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0/src/coding_agent_audit.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-agent-audit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.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
|
|
@@ -183,6 +183,16 @@ next `npm install` / `pip install` ("slopsquatting"). Network errors are
|
|
|
183
183
|
treated as inconclusive, not as a finding, so a flaky connection can't
|
|
184
184
|
produce a false positive.
|
|
185
185
|
|
|
186
|
+
**MCP phantom-package scan** (`scan_mcp_phantom_packages`, also opt-in via
|
|
187
|
+
`--check-deps`) — the same registry-lookup idea, applied to the MCP server
|
|
188
|
+
commands themselves: any `npx`/`uvx`/`pipx run` invocation in a scanned
|
|
189
|
+
config is checked against npm/PyPI. This is the "MCPhantom" pattern flagged
|
|
190
|
+
by Glow Labs in November 2026: a coding agent fills in a plausible but
|
|
191
|
+
nonexistent MCP server package name from training data (or copies one from
|
|
192
|
+
a long-archived tutorial whose package was since renamed/unpublished), and
|
|
193
|
+
nothing stops an attacker from registering that exact name and having it
|
|
194
|
+
run as the MCP server next time the config loads.
|
|
195
|
+
|
|
186
196
|
New incidents can be added to `iocs.py` as data, without touching the
|
|
187
197
|
check logic.
|
|
188
198
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
+
from unittest.mock import patch
|
|
2
3
|
|
|
3
4
|
from agentaudit.configscan import (
|
|
4
5
|
default_config_paths,
|
|
@@ -6,8 +7,10 @@ from agentaudit.configscan import (
|
|
|
6
7
|
scan_hooks,
|
|
7
8
|
scan_known_cve_packages,
|
|
8
9
|
scan_lethal_trifecta,
|
|
10
|
+
scan_mcp_phantom_packages,
|
|
9
11
|
scan_mcp_servers,
|
|
10
12
|
scan_paths,
|
|
13
|
+
scan_paths_for_phantom_mcp_packages,
|
|
11
14
|
scan_permissions,
|
|
12
15
|
scan_tool_descriptions,
|
|
13
16
|
)
|
|
@@ -155,6 +158,96 @@ def test_scan_known_cve_packages_not_a_dict():
|
|
|
155
158
|
assert scan_known_cve_packages({"mcpServers": "nope"}, "src") == []
|
|
156
159
|
|
|
157
160
|
|
|
161
|
+
def test_scan_mcp_phantom_packages_flags_nonexistent_npm():
|
|
162
|
+
config = {
|
|
163
|
+
"mcpServers": {
|
|
164
|
+
"ghost": {"command": "npx", "args": ["-y", "mcp-server-totally-made-up-9x7z"]}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
with patch("agentaudit.phantomdeps.check_npm_package_exists", return_value=False):
|
|
168
|
+
findings = scan_mcp_phantom_packages(config, "src")
|
|
169
|
+
assert len(findings) == 1
|
|
170
|
+
assert findings[0].check_id == "mcp-phantom-package"
|
|
171
|
+
assert findings[0].severity == "critical"
|
|
172
|
+
assert "mcp-server-totally-made-up-9x7z" in findings[0].title
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_scan_mcp_phantom_packages_flags_nonexistent_pypi():
|
|
176
|
+
config = {
|
|
177
|
+
"mcpServers": {"ghost": {"command": "uvx", "args": ["mcp-server-ghost-xyz"]}}
|
|
178
|
+
}
|
|
179
|
+
with patch("agentaudit.phantomdeps.check_pypi_package_exists", return_value=False):
|
|
180
|
+
findings = scan_mcp_phantom_packages(config, "src")
|
|
181
|
+
assert len(findings) == 1
|
|
182
|
+
assert "mcp-server-ghost-xyz" in findings[0].title
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_scan_mcp_phantom_packages_real_package_not_flagged():
|
|
186
|
+
config = {
|
|
187
|
+
"mcpServers": {
|
|
188
|
+
"fs": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem"]}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
with patch("agentaudit.phantomdeps.check_npm_package_exists", return_value=True):
|
|
192
|
+
findings = scan_mcp_phantom_packages(config, "src")
|
|
193
|
+
assert findings == []
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_scan_mcp_phantom_packages_network_unknown_not_flagged():
|
|
197
|
+
config = {"mcpServers": {"ghost": {"command": "npx", "args": ["-y", "some-pkg"]}}}
|
|
198
|
+
with patch("agentaudit.phantomdeps.check_npm_package_exists", return_value=None):
|
|
199
|
+
findings = scan_mcp_phantom_packages(config, "src")
|
|
200
|
+
assert findings == []
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_scan_mcp_phantom_packages_versioned_pin_strips_version():
|
|
204
|
+
config = {
|
|
205
|
+
"mcpServers": {
|
|
206
|
+
"ghost": {"command": "npx", "args": ["-y", "mcp-server-ghost-xyz@1.2.3"]}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
with patch("agentaudit.phantomdeps.check_npm_package_exists", return_value=False) as mock_check:
|
|
210
|
+
findings = scan_mcp_phantom_packages(config, "src")
|
|
211
|
+
mock_check.assert_called_once_with("mcp-server-ghost-xyz")
|
|
212
|
+
assert len(findings) == 1
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_scan_mcp_phantom_packages_scoped_versioned_pin():
|
|
216
|
+
config = {
|
|
217
|
+
"mcpServers": {
|
|
218
|
+
"ghost": {"command": "npx", "args": ["-y", "@scope/ghost-pkg@2.0.0"]}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
with patch("agentaudit.phantomdeps.check_npm_package_exists", return_value=False) as mock_check:
|
|
222
|
+
findings = scan_mcp_phantom_packages(config, "src")
|
|
223
|
+
mock_check.assert_called_once_with("@scope/ghost-pkg")
|
|
224
|
+
assert len(findings) == 1
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_scan_mcp_phantom_packages_no_runner_not_flagged():
|
|
228
|
+
config = {"mcpServers": {"local": {"command": "/usr/local/bin/my-server"}}}
|
|
229
|
+
assert scan_mcp_phantom_packages(config, "src") == []
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_scan_mcp_phantom_packages_not_a_dict():
|
|
233
|
+
assert scan_mcp_phantom_packages({"mcpServers": "nope"}, "src") == []
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_scan_paths_for_phantom_mcp_packages(tmp_path: Path):
|
|
237
|
+
config_path = tmp_path / ".mcp.json"
|
|
238
|
+
config_path.write_text(
|
|
239
|
+
'{"mcpServers": {"ghost": {"command": "npx", "args": ["-y", "mcp-ghost-xyz"]}}}'
|
|
240
|
+
)
|
|
241
|
+
with patch("agentaudit.phantomdeps.check_npm_package_exists", return_value=False):
|
|
242
|
+
findings = scan_paths_for_phantom_mcp_packages([config_path])
|
|
243
|
+
assert len(findings) == 1
|
|
244
|
+
assert findings[0].check_id == "mcp-phantom-package"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_scan_paths_for_phantom_mcp_packages_missing_file(tmp_path: Path):
|
|
248
|
+
assert scan_paths_for_phantom_mcp_packages([tmp_path / "missing.json"]) == []
|
|
249
|
+
|
|
250
|
+
|
|
158
251
|
def test_scan_permissions_scoped_rule_ok():
|
|
159
252
|
config = {"permissions": {"allow": ["Bash(git diff:*)", "Read(//workspace/**)"]}}
|
|
160
253
|
assert scan_permissions(config, "src") == []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/src/coding_agent_audit.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{coding_agent_audit-0.10.0 → coding_agent_audit-0.11.0}/src/coding_agent_audit.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|