coding-agent-audit 0.6.1__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.6.1/src/coding_agent_audit.egg-info → coding_agent_audit-0.8.0}/PKG-INFO +18 -1
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/README.md +17 -0
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/pyproject.toml +1 -1
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/src/agentaudit/__init__.py +1 -1
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/src/agentaudit/checks.py +185 -1
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/src/agentaudit/cli.py +2 -1
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/src/agentaudit/configscan.py +3 -0
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/src/agentaudit/iocs.py +56 -0
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0/src/coding_agent_audit.egg-info}/PKG-INFO +18 -1
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/tests/test_checks.py +137 -0
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/LICENSE +0 -0
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/setup.cfg +0 -0
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/src/agentaudit/findings.py +0 -0
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/src/agentaudit/mcp_server.py +0 -0
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/src/agentaudit/phantomdeps.py +0 -0
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/SOURCES.txt +0 -0
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/dependency_links.txt +0 -0
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/entry_points.txt +0 -0
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/requires.txt +0 -0
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/top_level.txt +0 -0
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/tests/test_cli.py +0 -0
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/tests/test_configscan.py +0 -0
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/tests/test_mcp_server.py +0 -0
- {coding_agent_audit-0.6.1 → coding_agent_audit-0.8.0}/tests/test_phantomdeps.py +0 -0
{coding_agent_audit-0.6.1/src/coding_agent_audit.egg-info → coding_agent_audit-0.8.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.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
|
|
@@ -112,6 +112,20 @@ Cursor Marketplace Claude Code trojan:
|
|
|
112
112
|
VS Code and Claude Desktop), plus a presence check that flags Codex/VS
|
|
113
113
|
Code (still unpatched at disclosure time) or Claude Desktop (patch status
|
|
114
114
|
unconfirmed) if installed, with mitigation guidance
|
|
115
|
+
- **Mini Shai-Hulud** (the npm worm that targets Claude Code / VS Code
|
|
116
|
+
specifically, added in v0.7.0): the `gh-token-monitor` persistence
|
|
117
|
+
service (systemd user service on Linux, LaunchAgent on macOS — both
|
|
118
|
+
carry a documented dead-man's switch that wipes your home directory if
|
|
119
|
+
the stolen GitHub token is revoked before the service is removed), and
|
|
120
|
+
the `.claude/router_runtime.js` / `.claude/setup.mjs` / `.vscode/setup.mjs`
|
|
121
|
+
dropper files it plants in a project (checked in the current directory
|
|
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`)
|
|
115
129
|
|
|
116
130
|
**Config heuristics** (`configscan.py`) — pattern-based, always worth a
|
|
117
131
|
second look rather than proof of compromise:
|
|
@@ -195,6 +209,9 @@ heuristics) - it can't do this package's filesystem IOC scan remotely.
|
|
|
195
209
|
Full request/response shape: `GET /openapi.json` on that URL, or the
|
|
196
210
|
[source](https://github.com/stackscribe-ai/coding-agent-audit) of the x402
|
|
197
211
|
worker. The free CLI above remains the full, unpaid alternative.
|
|
212
|
+
Registered and domain-verified on [402index.io](https://402index.io); see
|
|
213
|
+
[the writeup](https://stackscribe-ai.github.io/x402/cloudflare-workers/agent-payments/2026/11/03/x402-compliance-crawler-lessons.html)
|
|
214
|
+
on the three compliance gaps discovery crawlers found that our own tests missed.
|
|
198
215
|
|
|
199
216
|
## GitHub Action (CI gate for your repo's own config)
|
|
200
217
|
|
|
@@ -91,6 +91,20 @@ Cursor Marketplace Claude Code trojan:
|
|
|
91
91
|
VS Code and Claude Desktop), plus a presence check that flags Codex/VS
|
|
92
92
|
Code (still unpatched at disclosure time) or Claude Desktop (patch status
|
|
93
93
|
unconfirmed) if installed, with mitigation guidance
|
|
94
|
+
- **Mini Shai-Hulud** (the npm worm that targets Claude Code / VS Code
|
|
95
|
+
specifically, added in v0.7.0): the `gh-token-monitor` persistence
|
|
96
|
+
service (systemd user service on Linux, LaunchAgent on macOS — both
|
|
97
|
+
carry a documented dead-man's switch that wipes your home directory if
|
|
98
|
+
the stolen GitHub token is revoked before the service is removed), and
|
|
99
|
+
the `.claude/router_runtime.js` / `.claude/setup.mjs` / `.vscode/setup.mjs`
|
|
100
|
+
dropper files it plants in a project (checked in the current directory
|
|
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`)
|
|
94
108
|
|
|
95
109
|
**Config heuristics** (`configscan.py`) — pattern-based, always worth a
|
|
96
110
|
second look rather than proof of compromise:
|
|
@@ -174,6 +188,9 @@ heuristics) - it can't do this package's filesystem IOC scan remotely.
|
|
|
174
188
|
Full request/response shape: `GET /openapi.json` on that URL, or the
|
|
175
189
|
[source](https://github.com/stackscribe-ai/coding-agent-audit) of the x402
|
|
176
190
|
worker. The free CLI above remains the full, unpaid alternative.
|
|
191
|
+
Registered and domain-verified on [402index.io](https://402index.io); see
|
|
192
|
+
[the writeup](https://stackscribe-ai.github.io/x402/cloudflare-workers/agent-payments/2026/11/03/x402-compliance-crawler-lessons.html)
|
|
193
|
+
on the three compliance gaps discovery crawlers found that our own tests missed.
|
|
177
194
|
|
|
178
195
|
## GitHub Action (CI gate for your repo's own config)
|
|
179
196
|
|
|
@@ -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,11 +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"]
|
|
24
|
+
_SHAI_HULUD = INCIDENTS["mini-shai-hulud-gh-token-monitor-2026"]
|
|
25
|
+
_SANDWORM = INCIDENTS["sandworm-mode-npm-2026-11"]
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
def check_ssh_authorized_keys(home: Path) -> list[Finding]:
|
|
@@ -400,7 +404,177 @@ def check_embrace_unpatched_tools(home: Path) -> list[Finding]:
|
|
|
400
404
|
]
|
|
401
405
|
|
|
402
406
|
|
|
403
|
-
def
|
|
407
|
+
def check_shai_hulud_systemd_service(home: Path) -> list[Finding]:
|
|
408
|
+
"""Flag the gh-token-monitor systemd user service used for persistence
|
|
409
|
+
and credential theft on Linux."""
|
|
410
|
+
path = home / _SHAI_HULUD["linux_systemd_relpath"]
|
|
411
|
+
if path.exists():
|
|
412
|
+
return [
|
|
413
|
+
Finding(
|
|
414
|
+
check_id="shai-hulud-systemd-service",
|
|
415
|
+
severity="critical",
|
|
416
|
+
title="Known Mini Shai-Hulud persistence service found",
|
|
417
|
+
detail=(
|
|
418
|
+
f"'{path}' exists, matching the gh-token-monitor "
|
|
419
|
+
f"systemd user service installed by the "
|
|
420
|
+
f"{_SHAI_HULUD['name']}. Stop and remove it "
|
|
421
|
+
f"(`systemctl --user stop gh-token-monitor.service`), "
|
|
422
|
+
f"then rotate every credential on this machine before "
|
|
423
|
+
f"revoking any GitHub token - a documented dead-man's "
|
|
424
|
+
f"switch wipes the home directory (`rm -rf ~`) if the "
|
|
425
|
+
f"stolen token is revoked while the service is still "
|
|
426
|
+
f"installed."
|
|
427
|
+
),
|
|
428
|
+
path=str(path),
|
|
429
|
+
references=_SHAI_HULUD["references"],
|
|
430
|
+
)
|
|
431
|
+
]
|
|
432
|
+
return []
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def check_shai_hulud_launch_agent(home: Path) -> list[Finding]:
|
|
436
|
+
"""Flag the gh-token-monitor LaunchAgent used for persistence and
|
|
437
|
+
credential theft on macOS."""
|
|
438
|
+
path = home / "Library" / "LaunchAgents" / _SHAI_HULUD["macos_launch_agent"]
|
|
439
|
+
if path.exists():
|
|
440
|
+
return [
|
|
441
|
+
Finding(
|
|
442
|
+
check_id="shai-hulud-launch-agent",
|
|
443
|
+
severity="critical",
|
|
444
|
+
title="Known Mini Shai-Hulud persistence LaunchAgent found",
|
|
445
|
+
detail=(
|
|
446
|
+
f"'{path}' exists, matching the gh-token-monitor "
|
|
447
|
+
f"LaunchAgent installed by the {_SHAI_HULUD['name']}. "
|
|
448
|
+
f"Unload and remove it, then rotate every credential "
|
|
449
|
+
f"on this machine before revoking any GitHub token - a "
|
|
450
|
+
f"documented dead-man's switch wipes the home "
|
|
451
|
+
f"directory (`rm -rf ~`) if the stolen token is "
|
|
452
|
+
f"revoked while the agent is still installed."
|
|
453
|
+
),
|
|
454
|
+
path=str(path),
|
|
455
|
+
references=_SHAI_HULUD["references"],
|
|
456
|
+
)
|
|
457
|
+
]
|
|
458
|
+
return []
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def check_shai_hulud_project_markers(project_dirs: Iterable[Optional[Path]]) -> list[Finding]:
|
|
462
|
+
"""Flag known Mini Shai-Hulud dropper files inside one or more project
|
|
463
|
+
directories (e.g. the current working directory, or --project)."""
|
|
464
|
+
findings: list[Finding] = []
|
|
465
|
+
seen: set[Path] = set()
|
|
466
|
+
for project_dir in project_dirs:
|
|
467
|
+
if project_dir is None or not project_dir.is_dir():
|
|
468
|
+
continue
|
|
469
|
+
for relpath in _SHAI_HULUD["project_marker_files"]:
|
|
470
|
+
path = project_dir / relpath
|
|
471
|
+
resolved = path.resolve()
|
|
472
|
+
if resolved in seen or not path.is_file():
|
|
473
|
+
continue
|
|
474
|
+
seen.add(resolved)
|
|
475
|
+
findings.append(
|
|
476
|
+
Finding(
|
|
477
|
+
check_id="shai-hulud-project-marker",
|
|
478
|
+
severity="critical",
|
|
479
|
+
title="Known Mini Shai-Hulud dropper file found in project",
|
|
480
|
+
detail=(
|
|
481
|
+
f"'{path}' exists, matching a dropper file planted "
|
|
482
|
+
f"by the {_SHAI_HULUD['name']}, typically alongside "
|
|
483
|
+
f"a SessionStart hook in .claude/settings.json (or "
|
|
484
|
+
f"a folderOpen task in .vscode/tasks.json) that "
|
|
485
|
+
f"re-executes it every time the project is opened. "
|
|
486
|
+
f"Remove the file, check both of those config "
|
|
487
|
+
f"files for a matching hook/task, and rotate every "
|
|
488
|
+
f"credential this machine has used."
|
|
489
|
+
),
|
|
490
|
+
path=str(path),
|
|
491
|
+
references=_SHAI_HULUD["references"],
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
return findings
|
|
495
|
+
|
|
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
|
+
|
|
575
|
+
def run_all_ioc_checks(
|
|
576
|
+
home: Optional[Path] = None, project_dirs: Iterable[Path] = ()
|
|
577
|
+
) -> list[Finding]:
|
|
404
578
|
"""Run every IOC check against the real local machine."""
|
|
405
579
|
home = home or Path.home()
|
|
406
580
|
tmp_dir = Path(os.environ.get("TMPDIR", "/tmp"))
|
|
@@ -414,8 +588,18 @@ def run_all_ioc_checks(home: Optional[Path] = None) -> list[Finding]:
|
|
|
414
588
|
findings += check_embrace_unpatched_tools(home)
|
|
415
589
|
findings += check_c2_network_indicators(live_process_lines())
|
|
416
590
|
findings += check_linux_cron_persistence(live_crontab_text())
|
|
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())
|
|
598
|
+
if platform.system() == "Linux":
|
|
599
|
+
findings += check_shai_hulud_systemd_service(home)
|
|
417
600
|
if platform.system() == "Darwin":
|
|
418
601
|
findings += check_macos_launch_agent(home)
|
|
602
|
+
findings += check_shai_hulud_launch_agent(home)
|
|
419
603
|
if platform.system() == "Windows":
|
|
420
604
|
appdata = Path(os.environ.get("APPDATA", home / "AppData" / "Roaming"))
|
|
421
605
|
findings += check_windows_startup_folder(appdata)
|
|
@@ -85,7 +85,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
85
85
|
|
|
86
86
|
findings: list[Finding] = []
|
|
87
87
|
if not args.skip_ioc:
|
|
88
|
-
|
|
88
|
+
project_dirs = [args.project] if args.project is not None else []
|
|
89
|
+
findings += run_all_ioc_checks(project_dirs=project_dirs)
|
|
89
90
|
if not args.skip_config:
|
|
90
91
|
home = Path.home()
|
|
91
92
|
paths = default_config_paths(home, args.project)
|
|
@@ -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
|
|
|
@@ -76,4 +76,60 @@ INCIDENTS = {
|
|
|
76
76
|
# As of the references above; re-check before treating as current.
|
|
77
77
|
"unpatched_products": ["OpenAI Codex", "VS Code"],
|
|
78
78
|
},
|
|
79
|
+
"mini-shai-hulud-gh-token-monitor-2026": {
|
|
80
|
+
"name": "Mini Shai-Hulud npm worm, gh-token-monitor persistence (2026)",
|
|
81
|
+
"description": (
|
|
82
|
+
"A self-propagating npm supply-chain worm (tracked by several "
|
|
83
|
+
"vendors as Mini Shai-Hulud) that runs from a compromised "
|
|
84
|
+
"package's install script, drops an obfuscated Bun payload "
|
|
85
|
+
"(.claude/router_runtime.js, .claude/setup.mjs) in the "
|
|
86
|
+
"repo it's invoked in, and writes a SessionStart hook into "
|
|
87
|
+
"that repo's .claude/settings.json so the payload re-executes "
|
|
88
|
+
"every time Claude Code opens the project; a matching "
|
|
89
|
+
".vscode/tasks.json folderOpen task does the same for VS Code. "
|
|
90
|
+
"On the host it also installs an OS-level persistence service "
|
|
91
|
+
"named gh-token-monitor (a systemd user service on Linux, a "
|
|
92
|
+
"LaunchAgent on macOS) that harvests GitHub/npm/cloud "
|
|
93
|
+
"credentials and, per a documented dead-man's switch, runs "
|
|
94
|
+
"`rm -rf ~` if the stolen GitHub token is revoked before the "
|
|
95
|
+
"service is removed."
|
|
96
|
+
),
|
|
97
|
+
"references": [
|
|
98
|
+
"https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem",
|
|
99
|
+
"https://www.mend.io/blog/shai-hulud-sap-cap-supply-chain-attack-claude-code/",
|
|
100
|
+
"https://www.wiz.io/blog/mini-shai-hulud-strikes-again-tanstack-more-npm-packages-compromised",
|
|
101
|
+
],
|
|
102
|
+
"linux_systemd_relpath": ".config/systemd/user/gh-token-monitor.service",
|
|
103
|
+
"macos_launch_agent": "com.user.gh-token-monitor.plist",
|
|
104
|
+
"project_marker_files": [
|
|
105
|
+
".claude/router_runtime.js",
|
|
106
|
+
".claude/setup.mjs",
|
|
107
|
+
".vscode/setup.mjs",
|
|
108
|
+
],
|
|
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
|
+
},
|
|
79
135
|
}
|
{coding_agent_audit-0.6.1 → 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
|
|
@@ -112,6 +112,20 @@ Cursor Marketplace Claude Code trojan:
|
|
|
112
112
|
VS Code and Claude Desktop), plus a presence check that flags Codex/VS
|
|
113
113
|
Code (still unpatched at disclosure time) or Claude Desktop (patch status
|
|
114
114
|
unconfirmed) if installed, with mitigation guidance
|
|
115
|
+
- **Mini Shai-Hulud** (the npm worm that targets Claude Code / VS Code
|
|
116
|
+
specifically, added in v0.7.0): the `gh-token-monitor` persistence
|
|
117
|
+
service (systemd user service on Linux, LaunchAgent on macOS — both
|
|
118
|
+
carry a documented dead-man's switch that wipes your home directory if
|
|
119
|
+
the stolen GitHub token is revoked before the service is removed), and
|
|
120
|
+
the `.claude/router_runtime.js` / `.claude/setup.mjs` / `.vscode/setup.mjs`
|
|
121
|
+
dropper files it plants in a project (checked in the current directory
|
|
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`)
|
|
115
129
|
|
|
116
130
|
**Config heuristics** (`configscan.py`) — pattern-based, always worth a
|
|
117
131
|
second look rather than proof of compromise:
|
|
@@ -195,6 +209,9 @@ heuristics) - it can't do this package's filesystem IOC scan remotely.
|
|
|
195
209
|
Full request/response shape: `GET /openapi.json` on that URL, or the
|
|
196
210
|
[source](https://github.com/stackscribe-ai/coding-agent-audit) of the x402
|
|
197
211
|
worker. The free CLI above remains the full, unpaid alternative.
|
|
212
|
+
Registered and domain-verified on [402index.io](https://402index.io); see
|
|
213
|
+
[the writeup](https://stackscribe-ai.github.io/x402/cloudflare-workers/agent-payments/2026/11/03/x402-compliance-crawler-lessons.html)
|
|
214
|
+
on the three compliance gaps discovery crawlers found that our own tests missed.
|
|
198
215
|
|
|
199
216
|
## GitHub Action (CI gate for your repo's own config)
|
|
200
217
|
|
|
@@ -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,11 @@ 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,
|
|
16
|
+
check_shai_hulud_launch_agent,
|
|
17
|
+
check_shai_hulud_project_markers,
|
|
18
|
+
check_shai_hulud_systemd_service,
|
|
12
19
|
check_ssh_authorized_keys,
|
|
13
20
|
check_tmp_tunnel_keys,
|
|
14
21
|
check_windows_scheduled_task,
|
|
@@ -19,6 +26,8 @@ from agentaudit.iocs import INCIDENTS
|
|
|
19
26
|
|
|
20
27
|
_IOC = INCIDENTS["openvsx-claude-code-2026-10"]
|
|
21
28
|
_EMBRACE = INCIDENTS["embrace-embedded-browser-2026-10"]
|
|
29
|
+
_SHAI_HULUD = INCIDENTS["mini-shai-hulud-gh-token-monitor-2026"]
|
|
30
|
+
_SANDWORM = INCIDENTS["sandworm-mode-npm-2026-11"]
|
|
22
31
|
|
|
23
32
|
|
|
24
33
|
def test_ssh_authorized_keys_clean(tmp_path: Path):
|
|
@@ -245,3 +254,131 @@ def test_embrace_unpatched_tools_claude_desktop_only_is_info(tmp_path: Path):
|
|
|
245
254
|
assert len(findings) == 1
|
|
246
255
|
assert findings[0].severity == "info"
|
|
247
256
|
assert "Claude Desktop" in findings[0].detail
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_shai_hulud_systemd_service_absent(tmp_path: Path):
|
|
260
|
+
assert check_shai_hulud_systemd_service(tmp_path) == []
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_shai_hulud_systemd_service_present(tmp_path: Path):
|
|
264
|
+
relpath = _SHAI_HULUD["linux_systemd_relpath"]
|
|
265
|
+
target = tmp_path / relpath
|
|
266
|
+
target.parent.mkdir(parents=True)
|
|
267
|
+
target.write_text("[Unit]\nDescription=gh-token-monitor\n")
|
|
268
|
+
findings = check_shai_hulud_systemd_service(tmp_path)
|
|
269
|
+
assert len(findings) == 1
|
|
270
|
+
assert findings[0].severity == "critical"
|
|
271
|
+
assert findings[0].check_id == "shai-hulud-systemd-service"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_shai_hulud_launch_agent_absent(tmp_path: Path):
|
|
275
|
+
assert check_shai_hulud_launch_agent(tmp_path) == []
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_shai_hulud_launch_agent_present(tmp_path: Path):
|
|
279
|
+
agents_dir = tmp_path / "Library" / "LaunchAgents"
|
|
280
|
+
agents_dir.mkdir(parents=True)
|
|
281
|
+
(agents_dir / _SHAI_HULUD["macos_launch_agent"]).write_text("<plist></plist>")
|
|
282
|
+
findings = check_shai_hulud_launch_agent(tmp_path)
|
|
283
|
+
assert len(findings) == 1
|
|
284
|
+
assert findings[0].severity == "critical"
|
|
285
|
+
assert findings[0].check_id == "shai-hulud-launch-agent"
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_shai_hulud_project_markers_clean(tmp_path: Path):
|
|
289
|
+
(tmp_path / ".claude").mkdir()
|
|
290
|
+
assert check_shai_hulud_project_markers([tmp_path]) == []
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_shai_hulud_project_markers_one_present(tmp_path: Path):
|
|
294
|
+
claude_dir = tmp_path / ".claude"
|
|
295
|
+
claude_dir.mkdir()
|
|
296
|
+
(claude_dir / "router_runtime.js").write_text("// payload")
|
|
297
|
+
findings = check_shai_hulud_project_markers([tmp_path])
|
|
298
|
+
assert len(findings) == 1
|
|
299
|
+
assert findings[0].severity == "critical"
|
|
300
|
+
assert findings[0].check_id == "shai-hulud-project-marker"
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def test_shai_hulud_project_markers_all_present(tmp_path: Path):
|
|
304
|
+
for relpath in _SHAI_HULUD["project_marker_files"]:
|
|
305
|
+
target = tmp_path / relpath
|
|
306
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
307
|
+
target.write_text("x")
|
|
308
|
+
findings = check_shai_hulud_project_markers([tmp_path])
|
|
309
|
+
assert len(findings) == len(_SHAI_HULUD["project_marker_files"])
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def test_shai_hulud_project_markers_nonexistent_dir(tmp_path: Path):
|
|
313
|
+
assert check_shai_hulud_project_markers([tmp_path / "nope"]) == []
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def test_shai_hulud_project_markers_skips_none():
|
|
317
|
+
assert check_shai_hulud_project_markers([None]) == []
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def test_shai_hulud_project_markers_dedupes_duplicate_dirs(tmp_path: Path):
|
|
321
|
+
claude_dir = tmp_path / ".claude"
|
|
322
|
+
claude_dir.mkdir()
|
|
323
|
+
(claude_dir / "setup.mjs").write_text("// dropper")
|
|
324
|
+
findings = check_shai_hulud_project_markers([tmp_path, tmp_path])
|
|
325
|
+
assert len(findings) == 1
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_shai_hulud_project_markers_dedupes_relative_and_absolute(tmp_path: Path, monkeypatch):
|
|
329
|
+
claude_dir = tmp_path / ".claude"
|
|
330
|
+
claude_dir.mkdir()
|
|
331
|
+
(claude_dir / "setup.mjs").write_text("// dropper")
|
|
332
|
+
monkeypatch.chdir(tmp_path)
|
|
333
|
+
findings = check_shai_hulud_project_markers([tmp_path, Path(".")])
|
|
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
|
{coding_agent_audit-0.6.1 → 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.6.1 → coding_agent_audit-0.8.0}/src/coding_agent_audit.egg-info/requires.txt
RENAMED
|
File without changes
|
{coding_agent_audit-0.6.1 → 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
|