coding-agent-audit 0.7.0__tar.gz → 0.8.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.7.0 → coding_agent_audit-0.8.0}/PKG-INFO +7 -1
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/README.md +6 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/pyproject.toml +1 -1
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/agentaudit/__init__.py +1 -1
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/agentaudit/checks.py +87 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/agentaudit/configscan.py +3 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/agentaudit/iocs.py +25 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/PKG-INFO +7 -1
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/tests/test_checks.py +55 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/LICENSE +0 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/setup.cfg +0 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/agentaudit/cli.py +0 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/agentaudit/findings.py +0 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/agentaudit/mcp_server.py +0 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/agentaudit/phantomdeps.py +0 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/SOURCES.txt +0 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/dependency_links.txt +0 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/entry_points.txt +0 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/requires.txt +0 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/top_level.txt +0 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/tests/test_cli.py +0 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/tests/test_configscan.py +0 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/tests/test_mcp_server.py +0 -0
- {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.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.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -120,6 +120,12 @@ Cursor Marketplace Claude Code trojan:
|
|
|
120
120
|
the `.claude/router_runtime.js` / `.claude/setup.mjs` / `.vscode/setup.mjs`
|
|
121
121
|
dropper files it plants in a project (checked in the current directory
|
|
122
122
|
and any `--project` path)
|
|
123
|
+
- **SANDWORM_MODE** (the npm worm that registers a rogue local MCP server
|
|
124
|
+
with Claude Code, Cursor, Windsurf and Continue, added in v0.8.0): the
|
|
125
|
+
hidden `~/.dev-utils/` directory it installs, and any MCP server entry
|
|
126
|
+
in your Claude Code / Claude Desktop / Cursor / Windsurf config that
|
|
127
|
+
points at that directory or uses one of its giveaway tool names
|
|
128
|
+
(`index_project`, `lint_check`, `scan_dependencies`)
|
|
123
129
|
|
|
124
130
|
**Config heuristics** (`configscan.py`) — pattern-based, always worth a
|
|
125
131
|
second look rather than proof of compromise:
|
|
@@ -99,6 +99,12 @@ Cursor Marketplace Claude Code trojan:
|
|
|
99
99
|
the `.claude/router_runtime.js` / `.claude/setup.mjs` / `.vscode/setup.mjs`
|
|
100
100
|
dropper files it plants in a project (checked in the current directory
|
|
101
101
|
and any `--project` path)
|
|
102
|
+
- **SANDWORM_MODE** (the npm worm that registers a rogue local MCP server
|
|
103
|
+
with Claude Code, Cursor, Windsurf and Continue, added in v0.8.0): the
|
|
104
|
+
hidden `~/.dev-utils/` directory it installs, and any MCP server entry
|
|
105
|
+
in your Claude Code / Claude Desktop / Cursor / Windsurf config that
|
|
106
|
+
points at that directory or uses one of its giveaway tool names
|
|
107
|
+
(`index_project`, `lint_check`, `scan_dependencies`)
|
|
102
108
|
|
|
103
109
|
**Config heuristics** (`configscan.py`) — pattern-based, always worth a
|
|
104
110
|
second look rather than proof of compromise:
|
|
@@ -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.8.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,6 +7,7 @@ module is unit-testable without touching a real home directory.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import hashlib
|
|
10
|
+
import json
|
|
10
11
|
import os
|
|
11
12
|
import platform
|
|
12
13
|
import re
|
|
@@ -14,12 +15,14 @@ import subprocess
|
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from typing import Iterable, Optional
|
|
16
17
|
|
|
18
|
+
from .configscan import default_config_paths
|
|
17
19
|
from .findings import Finding
|
|
18
20
|
from .iocs import INCIDENTS
|
|
19
21
|
|
|
20
22
|
_IOC = INCIDENTS["openvsx-claude-code-2026-10"]
|
|
21
23
|
_EMBRACE = INCIDENTS["embrace-embedded-browser-2026-10"]
|
|
22
24
|
_SHAI_HULUD = INCIDENTS["mini-shai-hulud-gh-token-monitor-2026"]
|
|
25
|
+
_SANDWORM = INCIDENTS["sandworm-mode-npm-2026-11"]
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
def check_ssh_authorized_keys(home: Path) -> list[Finding]:
|
|
@@ -491,6 +494,84 @@ def check_shai_hulud_project_markers(project_dirs: Iterable[Optional[Path]]) ->
|
|
|
491
494
|
return findings
|
|
492
495
|
|
|
493
496
|
|
|
497
|
+
def check_sandworm_mode_dev_utils(home: Path) -> list[Finding]:
|
|
498
|
+
"""Flag the hidden ~/.dev-utils/ directory the SANDWORM_MODE npm worm
|
|
499
|
+
uses to host its rogue local MCP server."""
|
|
500
|
+
path = home / _SANDWORM["hidden_dir"]
|
|
501
|
+
if path.is_dir():
|
|
502
|
+
return [
|
|
503
|
+
Finding(
|
|
504
|
+
check_id="sandworm-mode-dev-utils-dir",
|
|
505
|
+
severity="critical",
|
|
506
|
+
title="Known SANDWORM_MODE rogue MCP server directory found",
|
|
507
|
+
detail=(
|
|
508
|
+
f"'{path}' exists, matching the hidden rogue MCP "
|
|
509
|
+
f"server directory planted by the {_SANDWORM['name']}. "
|
|
510
|
+
f"Remove the directory, check your Claude Code / "
|
|
511
|
+
f"Cursor / Windsurf / Continue MCP config for a server "
|
|
512
|
+
f"registration pointing at it (often named one of "
|
|
513
|
+
f"{', '.join(_SANDWORM['rogue_tool_names'])}), and "
|
|
514
|
+
f"rotate every SSH key, AWS credential and token on "
|
|
515
|
+
f"this machine."
|
|
516
|
+
),
|
|
517
|
+
path=str(path),
|
|
518
|
+
references=_SANDWORM["references"],
|
|
519
|
+
)
|
|
520
|
+
]
|
|
521
|
+
return []
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def check_sandworm_mode_mcp_registration(config_paths: Iterable[Path]) -> list[Finding]:
|
|
525
|
+
"""Flag any MCP server registration (Claude Code, Cursor, Windsurf,
|
|
526
|
+
Continue) that points at the SANDWORM_MODE rogue server directory or
|
|
527
|
+
uses one of its giveaway tool names."""
|
|
528
|
+
findings: list[Finding] = []
|
|
529
|
+
for path in config_paths:
|
|
530
|
+
if not path.is_file():
|
|
531
|
+
continue
|
|
532
|
+
try:
|
|
533
|
+
config = json.loads(path.read_text())
|
|
534
|
+
except (OSError, json.JSONDecodeError):
|
|
535
|
+
continue
|
|
536
|
+
if not isinstance(config, dict):
|
|
537
|
+
continue
|
|
538
|
+
servers = config.get("mcpServers") or config.get("mcp_servers")
|
|
539
|
+
if not isinstance(servers, dict):
|
|
540
|
+
continue
|
|
541
|
+
for server_name, server_cfg in servers.items():
|
|
542
|
+
if not isinstance(server_cfg, dict):
|
|
543
|
+
continue
|
|
544
|
+
command = str(server_cfg.get("command", ""))
|
|
545
|
+
args = " ".join(str(a) for a in (server_cfg.get("args") or []))
|
|
546
|
+
full_cmdline = f"{command} {args}"
|
|
547
|
+
hit_dir = _SANDWORM["hidden_dir"] in full_cmdline
|
|
548
|
+
hit_name = server_name in _SANDWORM["rogue_tool_names"]
|
|
549
|
+
if not (hit_dir or hit_name):
|
|
550
|
+
continue
|
|
551
|
+
reason = (
|
|
552
|
+
f"points at the rogue {_SANDWORM['hidden_dir']} directory"
|
|
553
|
+
if hit_dir
|
|
554
|
+
else "uses a known SANDWORM_MODE giveaway tool name"
|
|
555
|
+
)
|
|
556
|
+
findings.append(
|
|
557
|
+
Finding(
|
|
558
|
+
check_id="sandworm-mode-mcp-registration",
|
|
559
|
+
severity="critical",
|
|
560
|
+
title="Known SANDWORM_MODE rogue MCP server registration found",
|
|
561
|
+
detail=(
|
|
562
|
+
f"MCP server '{server_name}' in '{path}' {reason}, "
|
|
563
|
+
f"matching the {_SANDWORM['name']}. Remove this "
|
|
564
|
+
f"server entry, delete ~/{_SANDWORM['hidden_dir']}/, "
|
|
565
|
+
f"and rotate every SSH key, AWS credential and "
|
|
566
|
+
f"token on this machine."
|
|
567
|
+
),
|
|
568
|
+
path=str(path),
|
|
569
|
+
references=_SANDWORM["references"],
|
|
570
|
+
)
|
|
571
|
+
)
|
|
572
|
+
return findings
|
|
573
|
+
|
|
574
|
+
|
|
494
575
|
def run_all_ioc_checks(
|
|
495
576
|
home: Optional[Path] = None, project_dirs: Iterable[Path] = ()
|
|
496
577
|
) -> list[Finding]:
|
|
@@ -508,6 +589,12 @@ def run_all_ioc_checks(
|
|
|
508
589
|
findings += check_c2_network_indicators(live_process_lines())
|
|
509
590
|
findings += check_linux_cron_persistence(live_crontab_text())
|
|
510
591
|
findings += check_shai_hulud_project_markers([Path.cwd(), *project_dirs])
|
|
592
|
+
findings += check_sandworm_mode_dev_utils(home)
|
|
593
|
+
config_paths: dict[str, Path] = {str(p): p for p in default_config_paths(home)}
|
|
594
|
+
for project_dir in [Path.cwd(), *project_dirs]:
|
|
595
|
+
for p in default_config_paths(home, project_dir):
|
|
596
|
+
config_paths[str(p)] = p
|
|
597
|
+
findings += check_sandworm_mode_mcp_registration(config_paths.values())
|
|
511
598
|
if platform.system() == "Linux":
|
|
512
599
|
findings += check_shai_hulud_systemd_service(home)
|
|
513
600
|
if platform.system() == "Darwin":
|
|
@@ -245,12 +245,15 @@ def default_config_paths(home: Path, project_dir: Path | None = None) -> list[Pa
|
|
|
245
245
|
/ "Roaming"
|
|
246
246
|
/ "Claude"
|
|
247
247
|
/ "claude_desktop_config.json",
|
|
248
|
+
home / ".cursor" / "mcp.json",
|
|
249
|
+
home / ".codeium" / "windsurf" / "mcp_config.json",
|
|
248
250
|
]
|
|
249
251
|
if project_dir is not None:
|
|
250
252
|
paths += [
|
|
251
253
|
project_dir / ".claude" / "settings.json",
|
|
252
254
|
project_dir / ".claude" / "settings.local.json",
|
|
253
255
|
project_dir / ".mcp.json",
|
|
256
|
+
project_dir / ".cursor" / "mcp.json",
|
|
254
257
|
]
|
|
255
258
|
return paths
|
|
256
259
|
|
|
@@ -107,4 +107,29 @@ INCIDENTS = {
|
|
|
107
107
|
".vscode/setup.mjs",
|
|
108
108
|
],
|
|
109
109
|
},
|
|
110
|
+
"sandworm-mode-npm-2026-11": {
|
|
111
|
+
"name": "SANDWORM_MODE npm supply-chain worm, rogue MCP server (Nov 2026)",
|
|
112
|
+
"description": (
|
|
113
|
+
"A self-propagating npm supply-chain worm (SANDWORM_MODE), "
|
|
114
|
+
"distributed via 19 typosquatted packages published under the "
|
|
115
|
+
"'official334' and 'javaorg' npm aliases (e.g. suport-color, a "
|
|
116
|
+
"typosquat of supports-color). Its install script drops a "
|
|
117
|
+
"hidden directory, ~/.dev-utils/, containing a rogue local MCP "
|
|
118
|
+
"server and registers it with Claude Code, Cursor, Windsurf "
|
|
119
|
+
"and Continue under three innocuous-sounding tool names "
|
|
120
|
+
"(index_project, lint_check, scan_dependencies) so the AI "
|
|
121
|
+
"assistant itself ends up invoking it; the server then reads "
|
|
122
|
+
"SSH keys, AWS credentials and password-manager secrets, "
|
|
123
|
+
"installs Git-hooks persistence, and propagates by publishing "
|
|
124
|
+
"further typosquatted packages."
|
|
125
|
+
),
|
|
126
|
+
"references": [
|
|
127
|
+
"https://socket.dev/blog/sandworm-mode-npm-worm-ai-toolchain-poisoning",
|
|
128
|
+
"https://www.endorlabs.com/learn/sandworm-mode-dissecting-a-multi-stage-npm-supply-chain-attack",
|
|
129
|
+
"https://www.securityweek.com/new-sandworm_mode-supply-chain-attack-hits-npm/",
|
|
130
|
+
],
|
|
131
|
+
"hidden_dir": ".dev-utils",
|
|
132
|
+
"rogue_tool_names": ["index_project", "lint_check", "scan_dependencies"],
|
|
133
|
+
"typosquat_aliases": ["official334", "javaorg"],
|
|
134
|
+
},
|
|
110
135
|
}
|
{coding_agent_audit-0.7.0 → coding_agent_audit-0.8.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.8.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
|
|
@@ -120,6 +120,12 @@ Cursor Marketplace Claude Code trojan:
|
|
|
120
120
|
the `.claude/router_runtime.js` / `.claude/setup.mjs` / `.vscode/setup.mjs`
|
|
121
121
|
dropper files it plants in a project (checked in the current directory
|
|
122
122
|
and any `--project` path)
|
|
123
|
+
- **SANDWORM_MODE** (the npm worm that registers a rogue local MCP server
|
|
124
|
+
with Claude Code, Cursor, Windsurf and Continue, added in v0.8.0): the
|
|
125
|
+
hidden `~/.dev-utils/` directory it installs, and any MCP server entry
|
|
126
|
+
in your Claude Code / Claude Desktop / Cursor / Windsurf config that
|
|
127
|
+
points at that directory or uses one of its giveaway tool names
|
|
128
|
+
(`index_project`, `lint_check`, `scan_dependencies`)
|
|
123
129
|
|
|
124
130
|
**Config heuristics** (`configscan.py`) — pattern-based, always worth a
|
|
125
131
|
second look rather than proof of compromise:
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import hashlib
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
|
+
import json
|
|
5
|
+
|
|
4
6
|
from agentaudit.checks import (
|
|
5
7
|
check_c2_network_indicators,
|
|
6
8
|
check_embrace_copilot_version,
|
|
@@ -9,6 +11,8 @@ from agentaudit.checks import (
|
|
|
9
11
|
check_linux_cron_persistence,
|
|
10
12
|
check_macos_launch_agent,
|
|
11
13
|
check_marker_files,
|
|
14
|
+
check_sandworm_mode_dev_utils,
|
|
15
|
+
check_sandworm_mode_mcp_registration,
|
|
12
16
|
check_shai_hulud_launch_agent,
|
|
13
17
|
check_shai_hulud_project_markers,
|
|
14
18
|
check_shai_hulud_systemd_service,
|
|
@@ -23,6 +27,7 @@ from agentaudit.iocs import INCIDENTS
|
|
|
23
27
|
_IOC = INCIDENTS["openvsx-claude-code-2026-10"]
|
|
24
28
|
_EMBRACE = INCIDENTS["embrace-embedded-browser-2026-10"]
|
|
25
29
|
_SHAI_HULUD = INCIDENTS["mini-shai-hulud-gh-token-monitor-2026"]
|
|
30
|
+
_SANDWORM = INCIDENTS["sandworm-mode-npm-2026-11"]
|
|
26
31
|
|
|
27
32
|
|
|
28
33
|
def test_ssh_authorized_keys_clean(tmp_path: Path):
|
|
@@ -327,3 +332,53 @@ def test_shai_hulud_project_markers_dedupes_relative_and_absolute(tmp_path: Path
|
|
|
327
332
|
monkeypatch.chdir(tmp_path)
|
|
328
333
|
findings = check_shai_hulud_project_markers([tmp_path, Path(".")])
|
|
329
334
|
assert len(findings) == 1
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def test_sandworm_mode_dev_utils_absent(tmp_path: Path):
|
|
338
|
+
assert check_sandworm_mode_dev_utils(tmp_path) == []
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def test_sandworm_mode_dev_utils_present(tmp_path: Path):
|
|
342
|
+
(tmp_path / _SANDWORM["hidden_dir"]).mkdir()
|
|
343
|
+
findings = check_sandworm_mode_dev_utils(tmp_path)
|
|
344
|
+
assert len(findings) == 1
|
|
345
|
+
assert findings[0].severity == "critical"
|
|
346
|
+
assert findings[0].check_id == "sandworm-mode-dev-utils-dir"
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_sandworm_mode_mcp_registration_clean(tmp_path: Path):
|
|
350
|
+
config = tmp_path / ".claude.json"
|
|
351
|
+
config.write_text(json.dumps({"mcpServers": {"filesystem": {"command": "npx", "args": ["-y", "@a/b@1.0.0"]}}}))
|
|
352
|
+
assert check_sandworm_mode_mcp_registration([config]) == []
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def test_sandworm_mode_mcp_registration_by_dir(tmp_path: Path):
|
|
356
|
+
config = tmp_path / ".claude.json"
|
|
357
|
+
config.write_text(
|
|
358
|
+
json.dumps(
|
|
359
|
+
{
|
|
360
|
+
"mcpServers": {
|
|
361
|
+
"helper": {
|
|
362
|
+
"command": "node",
|
|
363
|
+
"args": [str(Path.home() / ".dev-utils" / "server.js")],
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
findings = check_sandworm_mode_mcp_registration([config])
|
|
370
|
+
assert len(findings) == 1
|
|
371
|
+
assert findings[0].severity == "critical"
|
|
372
|
+
assert findings[0].check_id == "sandworm-mode-mcp-registration"
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def test_sandworm_mode_mcp_registration_by_tool_name(tmp_path: Path):
|
|
376
|
+
config = tmp_path / ".mcp.json"
|
|
377
|
+
config.write_text(json.dumps({"mcpServers": {"lint_check": {"command": "node", "args": ["server.js"]}}}))
|
|
378
|
+
findings = check_sandworm_mode_mcp_registration([config])
|
|
379
|
+
assert len(findings) == 1
|
|
380
|
+
assert findings[0].check_id == "sandworm-mode-mcp-registration"
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def test_sandworm_mode_mcp_registration_missing_file(tmp_path: Path):
|
|
384
|
+
assert check_sandworm_mode_mcp_registration([tmp_path / "missing.json"]) == []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/requires.txt
RENAMED
|
File without changes
|
{coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|