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.
Files changed (24) hide show
  1. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/PKG-INFO +7 -1
  2. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/README.md +6 -0
  3. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/pyproject.toml +1 -1
  4. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/agentaudit/__init__.py +1 -1
  5. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/agentaudit/checks.py +87 -0
  6. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/agentaudit/configscan.py +3 -0
  7. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/agentaudit/iocs.py +25 -0
  8. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/PKG-INFO +7 -1
  9. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/tests/test_checks.py +55 -0
  10. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/LICENSE +0 -0
  11. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/setup.cfg +0 -0
  12. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/agentaudit/cli.py +0 -0
  13. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/agentaudit/findings.py +0 -0
  14. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/agentaudit/mcp_server.py +0 -0
  15. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/agentaudit/phantomdeps.py +0 -0
  16. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/SOURCES.txt +0 -0
  17. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/dependency_links.txt +0 -0
  18. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/entry_points.txt +0 -0
  19. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/requires.txt +0 -0
  20. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/top_level.txt +0 -0
  21. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/tests/test_cli.py +0 -0
  22. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/tests/test_configscan.py +0 -0
  23. {coding_agent_audit-0.7.0 → coding_agent_audit-0.8.0}/tests/test_mcp_server.py +0 -0
  24. {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.7.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.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"
@@ -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.7.0"
6
+ __version__ = "0.8.0"
@@ -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
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-agent-audit
3
- Version: 0.7.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"]) == []