coding-agent-audit 0.6.0__tar.gz → 0.7.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.0/src/coding_agent_audit.egg-info → coding_agent_audit-0.7.0}/PKG-INFO +15 -2
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/README.md +14 -1
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/pyproject.toml +1 -1
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/src/agentaudit/__init__.py +1 -1
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/src/agentaudit/checks.py +98 -1
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/src/agentaudit/cli.py +2 -1
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/src/agentaudit/iocs.py +31 -0
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/src/agentaudit/mcp_server.py +19 -3
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0/src/coding_agent_audit.egg-info}/PKG-INFO +15 -2
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/tests/test_checks.py +82 -0
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/LICENSE +0 -0
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/setup.cfg +0 -0
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/src/agentaudit/configscan.py +0 -0
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/src/agentaudit/findings.py +0 -0
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/src/agentaudit/phantomdeps.py +0 -0
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/src/coding_agent_audit.egg-info/SOURCES.txt +0 -0
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/src/coding_agent_audit.egg-info/dependency_links.txt +0 -0
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/src/coding_agent_audit.egg-info/entry_points.txt +0 -0
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/src/coding_agent_audit.egg-info/requires.txt +0 -0
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/src/coding_agent_audit.egg-info/top_level.txt +0 -0
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/tests/test_cli.py +0 -0
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/tests/test_configscan.py +0 -0
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/tests/test_mcp_server.py +0 -0
- {coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/tests/test_phantomdeps.py +0 -0
{coding_agent_audit-0.6.0/src/coding_agent_audit.egg-info → coding_agent_audit-0.7.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.7.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
|
|
@@ -23,6 +23,8 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
<!-- mcp-name: io.github.stackscribe-ai/coding-agent-audit -->
|
|
25
25
|
|
|
26
|
+
[](https://m8ven.ai/mcp/stackscribe-ai-coding-agent-audit-1xiuww?s=readme)
|
|
27
|
+
|
|
26
28
|
A small, open-source CLI that scans your machine for:
|
|
27
29
|
|
|
28
30
|
1. **Known indicators of compromise (IOCs)** from publicly documented
|
|
@@ -110,6 +112,14 @@ Cursor Marketplace Claude Code trojan:
|
|
|
110
112
|
VS Code and Claude Desktop), plus a presence check that flags Codex/VS
|
|
111
113
|
Code (still unpatched at disclosure time) or Claude Desktop (patch status
|
|
112
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)
|
|
113
123
|
|
|
114
124
|
**Config heuristics** (`configscan.py`) — pattern-based, always worth a
|
|
115
125
|
second look rather than proof of compromise:
|
|
@@ -193,6 +203,9 @@ heuristics) - it can't do this package's filesystem IOC scan remotely.
|
|
|
193
203
|
Full request/response shape: `GET /openapi.json` on that URL, or the
|
|
194
204
|
[source](https://github.com/stackscribe-ai/coding-agent-audit) of the x402
|
|
195
205
|
worker. The free CLI above remains the full, unpaid alternative.
|
|
206
|
+
Registered and domain-verified on [402index.io](https://402index.io); see
|
|
207
|
+
[the writeup](https://stackscribe-ai.github.io/x402/cloudflare-workers/agent-payments/2026/11/03/x402-compliance-crawler-lessons.html)
|
|
208
|
+
on the three compliance gaps discovery crawlers found that our own tests missed.
|
|
196
209
|
|
|
197
210
|
## GitHub Action (CI gate for your repo's own config)
|
|
198
211
|
|
|
@@ -227,7 +240,7 @@ scan for free; this just packages it for sharing.
|
|
|
227
240
|
|
|
228
241
|
If your agent runtime supports the open `SKILL.md` standard (Claude Code,
|
|
229
242
|
Cursor, and others), a packaged version of this scan is live on the
|
|
230
|
-
[Agensi skill marketplace](https://www.agensi.io/skills/coding-agent-security-audit)
|
|
243
|
+
[Agensi skill marketplace](https://www.agensi.io/r/STACKSCRIBEAI?to=/skills/coding-agent-security-audit)
|
|
231
244
|
as "coding agent security audit" (free) — source in
|
|
232
245
|
[`agensi-skill/`](agensi-skill/). It's a zero-dependency, single-file port of
|
|
233
246
|
the same checks below, meant to be loaded by an agent directly rather than
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- mcp-name: io.github.stackscribe-ai/coding-agent-audit -->
|
|
4
4
|
|
|
5
|
+
[](https://m8ven.ai/mcp/stackscribe-ai-coding-agent-audit-1xiuww?s=readme)
|
|
6
|
+
|
|
5
7
|
A small, open-source CLI that scans your machine for:
|
|
6
8
|
|
|
7
9
|
1. **Known indicators of compromise (IOCs)** from publicly documented
|
|
@@ -89,6 +91,14 @@ Cursor Marketplace Claude Code trojan:
|
|
|
89
91
|
VS Code and Claude Desktop), plus a presence check that flags Codex/VS
|
|
90
92
|
Code (still unpatched at disclosure time) or Claude Desktop (patch status
|
|
91
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)
|
|
92
102
|
|
|
93
103
|
**Config heuristics** (`configscan.py`) — pattern-based, always worth a
|
|
94
104
|
second look rather than proof of compromise:
|
|
@@ -172,6 +182,9 @@ heuristics) - it can't do this package's filesystem IOC scan remotely.
|
|
|
172
182
|
Full request/response shape: `GET /openapi.json` on that URL, or the
|
|
173
183
|
[source](https://github.com/stackscribe-ai/coding-agent-audit) of the x402
|
|
174
184
|
worker. The free CLI above remains the full, unpaid alternative.
|
|
185
|
+
Registered and domain-verified on [402index.io](https://402index.io); see
|
|
186
|
+
[the writeup](https://stackscribe-ai.github.io/x402/cloudflare-workers/agent-payments/2026/11/03/x402-compliance-crawler-lessons.html)
|
|
187
|
+
on the three compliance gaps discovery crawlers found that our own tests missed.
|
|
175
188
|
|
|
176
189
|
## GitHub Action (CI gate for your repo's own config)
|
|
177
190
|
|
|
@@ -206,7 +219,7 @@ scan for free; this just packages it for sharing.
|
|
|
206
219
|
|
|
207
220
|
If your agent runtime supports the open `SKILL.md` standard (Claude Code,
|
|
208
221
|
Cursor, and others), a packaged version of this scan is live on the
|
|
209
|
-
[Agensi skill marketplace](https://www.agensi.io/skills/coding-agent-security-audit)
|
|
222
|
+
[Agensi skill marketplace](https://www.agensi.io/r/STACKSCRIBEAI?to=/skills/coding-agent-security-audit)
|
|
210
223
|
as "coding agent security audit" (free) — source in
|
|
211
224
|
[`agensi-skill/`](agensi-skill/). It's a zero-dependency, single-file port of
|
|
212
225
|
the same checks below, meant to be loaded by an agent directly rather than
|
|
@@ -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.7.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"
|
|
@@ -19,6 +19,7 @@ from .iocs import INCIDENTS
|
|
|
19
19
|
|
|
20
20
|
_IOC = INCIDENTS["openvsx-claude-code-2026-10"]
|
|
21
21
|
_EMBRACE = INCIDENTS["embrace-embedded-browser-2026-10"]
|
|
22
|
+
_SHAI_HULUD = INCIDENTS["mini-shai-hulud-gh-token-monitor-2026"]
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
def check_ssh_authorized_keys(home: Path) -> list[Finding]:
|
|
@@ -400,7 +401,99 @@ def check_embrace_unpatched_tools(home: Path) -> list[Finding]:
|
|
|
400
401
|
]
|
|
401
402
|
|
|
402
403
|
|
|
403
|
-
def
|
|
404
|
+
def check_shai_hulud_systemd_service(home: Path) -> list[Finding]:
|
|
405
|
+
"""Flag the gh-token-monitor systemd user service used for persistence
|
|
406
|
+
and credential theft on Linux."""
|
|
407
|
+
path = home / _SHAI_HULUD["linux_systemd_relpath"]
|
|
408
|
+
if path.exists():
|
|
409
|
+
return [
|
|
410
|
+
Finding(
|
|
411
|
+
check_id="shai-hulud-systemd-service",
|
|
412
|
+
severity="critical",
|
|
413
|
+
title="Known Mini Shai-Hulud persistence service found",
|
|
414
|
+
detail=(
|
|
415
|
+
f"'{path}' exists, matching the gh-token-monitor "
|
|
416
|
+
f"systemd user service installed by the "
|
|
417
|
+
f"{_SHAI_HULUD['name']}. Stop and remove it "
|
|
418
|
+
f"(`systemctl --user stop gh-token-monitor.service`), "
|
|
419
|
+
f"then rotate every credential on this machine before "
|
|
420
|
+
f"revoking any GitHub token - a documented dead-man's "
|
|
421
|
+
f"switch wipes the home directory (`rm -rf ~`) if the "
|
|
422
|
+
f"stolen token is revoked while the service is still "
|
|
423
|
+
f"installed."
|
|
424
|
+
),
|
|
425
|
+
path=str(path),
|
|
426
|
+
references=_SHAI_HULUD["references"],
|
|
427
|
+
)
|
|
428
|
+
]
|
|
429
|
+
return []
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def check_shai_hulud_launch_agent(home: Path) -> list[Finding]:
|
|
433
|
+
"""Flag the gh-token-monitor LaunchAgent used for persistence and
|
|
434
|
+
credential theft on macOS."""
|
|
435
|
+
path = home / "Library" / "LaunchAgents" / _SHAI_HULUD["macos_launch_agent"]
|
|
436
|
+
if path.exists():
|
|
437
|
+
return [
|
|
438
|
+
Finding(
|
|
439
|
+
check_id="shai-hulud-launch-agent",
|
|
440
|
+
severity="critical",
|
|
441
|
+
title="Known Mini Shai-Hulud persistence LaunchAgent found",
|
|
442
|
+
detail=(
|
|
443
|
+
f"'{path}' exists, matching the gh-token-monitor "
|
|
444
|
+
f"LaunchAgent installed by the {_SHAI_HULUD['name']}. "
|
|
445
|
+
f"Unload and remove it, then rotate every credential "
|
|
446
|
+
f"on this machine before revoking any GitHub token - a "
|
|
447
|
+
f"documented dead-man's switch wipes the home "
|
|
448
|
+
f"directory (`rm -rf ~`) if the stolen token is "
|
|
449
|
+
f"revoked while the agent is still installed."
|
|
450
|
+
),
|
|
451
|
+
path=str(path),
|
|
452
|
+
references=_SHAI_HULUD["references"],
|
|
453
|
+
)
|
|
454
|
+
]
|
|
455
|
+
return []
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def check_shai_hulud_project_markers(project_dirs: Iterable[Optional[Path]]) -> list[Finding]:
|
|
459
|
+
"""Flag known Mini Shai-Hulud dropper files inside one or more project
|
|
460
|
+
directories (e.g. the current working directory, or --project)."""
|
|
461
|
+
findings: list[Finding] = []
|
|
462
|
+
seen: set[Path] = set()
|
|
463
|
+
for project_dir in project_dirs:
|
|
464
|
+
if project_dir is None or not project_dir.is_dir():
|
|
465
|
+
continue
|
|
466
|
+
for relpath in _SHAI_HULUD["project_marker_files"]:
|
|
467
|
+
path = project_dir / relpath
|
|
468
|
+
resolved = path.resolve()
|
|
469
|
+
if resolved in seen or not path.is_file():
|
|
470
|
+
continue
|
|
471
|
+
seen.add(resolved)
|
|
472
|
+
findings.append(
|
|
473
|
+
Finding(
|
|
474
|
+
check_id="shai-hulud-project-marker",
|
|
475
|
+
severity="critical",
|
|
476
|
+
title="Known Mini Shai-Hulud dropper file found in project",
|
|
477
|
+
detail=(
|
|
478
|
+
f"'{path}' exists, matching a dropper file planted "
|
|
479
|
+
f"by the {_SHAI_HULUD['name']}, typically alongside "
|
|
480
|
+
f"a SessionStart hook in .claude/settings.json (or "
|
|
481
|
+
f"a folderOpen task in .vscode/tasks.json) that "
|
|
482
|
+
f"re-executes it every time the project is opened. "
|
|
483
|
+
f"Remove the file, check both of those config "
|
|
484
|
+
f"files for a matching hook/task, and rotate every "
|
|
485
|
+
f"credential this machine has used."
|
|
486
|
+
),
|
|
487
|
+
path=str(path),
|
|
488
|
+
references=_SHAI_HULUD["references"],
|
|
489
|
+
)
|
|
490
|
+
)
|
|
491
|
+
return findings
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def run_all_ioc_checks(
|
|
495
|
+
home: Optional[Path] = None, project_dirs: Iterable[Path] = ()
|
|
496
|
+
) -> list[Finding]:
|
|
404
497
|
"""Run every IOC check against the real local machine."""
|
|
405
498
|
home = home or Path.home()
|
|
406
499
|
tmp_dir = Path(os.environ.get("TMPDIR", "/tmp"))
|
|
@@ -414,8 +507,12 @@ def run_all_ioc_checks(home: Optional[Path] = None) -> list[Finding]:
|
|
|
414
507
|
findings += check_embrace_unpatched_tools(home)
|
|
415
508
|
findings += check_c2_network_indicators(live_process_lines())
|
|
416
509
|
findings += check_linux_cron_persistence(live_crontab_text())
|
|
510
|
+
findings += check_shai_hulud_project_markers([Path.cwd(), *project_dirs])
|
|
511
|
+
if platform.system() == "Linux":
|
|
512
|
+
findings += check_shai_hulud_systemd_service(home)
|
|
417
513
|
if platform.system() == "Darwin":
|
|
418
514
|
findings += check_macos_launch_agent(home)
|
|
515
|
+
findings += check_shai_hulud_launch_agent(home)
|
|
419
516
|
if platform.system() == "Windows":
|
|
420
517
|
appdata = Path(os.environ.get("APPDATA", home / "AppData" / "Roaming"))
|
|
421
518
|
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)
|
|
@@ -76,4 +76,35 @@ 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
|
+
},
|
|
79
110
|
}
|
|
@@ -14,6 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Optional
|
|
15
15
|
|
|
16
16
|
from mcp.server.mcpserver import MCPServer
|
|
17
|
+
from mcp.types import ToolAnnotations
|
|
17
18
|
|
|
18
19
|
from . import __version__
|
|
19
20
|
from .checks import run_all_ioc_checks
|
|
@@ -53,7 +54,22 @@ def _render(findings: list[Finding]) -> str:
|
|
|
53
54
|
return "\n".join(lines)
|
|
54
55
|
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
_LOCAL_READ_ONLY = ToolAnnotations(
|
|
58
|
+
readOnlyHint=True,
|
|
59
|
+
destructiveHint=False,
|
|
60
|
+
idempotentHint=True,
|
|
61
|
+
openWorldHint=False,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
_NETWORK_READ_ONLY = ToolAnnotations(
|
|
65
|
+
readOnlyHint=True,
|
|
66
|
+
destructiveHint=False,
|
|
67
|
+
idempotentHint=True,
|
|
68
|
+
openWorldHint=True,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@server.tool(annotations=_LOCAL_READ_ONLY)
|
|
57
73
|
def scan_for_known_backdoors() -> str:
|
|
58
74
|
"""Scan this machine for known, publicly-documented indicators of
|
|
59
75
|
compromise from AI-coding-assistant supply-chain attacks (SSH backdoor
|
|
@@ -62,7 +78,7 @@ def scan_for_known_backdoors() -> str:
|
|
|
62
78
|
return _render(run_all_ioc_checks())
|
|
63
79
|
|
|
64
80
|
|
|
65
|
-
@server.tool()
|
|
81
|
+
@server.tool(annotations=_LOCAL_READ_ONLY)
|
|
66
82
|
def scan_mcp_config(project_dir: Optional[str] = None) -> str:
|
|
67
83
|
"""Scan this machine's Claude Code / Claude Desktop / MCP config files
|
|
68
84
|
for risky patterns: hooks that pipe a remote download into a shell,
|
|
@@ -80,7 +96,7 @@ def scan_mcp_config(project_dir: Optional[str] = None) -> str:
|
|
|
80
96
|
return _render(scan_paths(paths))
|
|
81
97
|
|
|
82
98
|
|
|
83
|
-
@server.tool()
|
|
99
|
+
@server.tool(annotations=_NETWORK_READ_ONLY)
|
|
84
100
|
def scan_phantom_dependencies(project_dir: str) -> str:
|
|
85
101
|
"""Check a project's package.json / requirements.txt / pyproject.toml
|
|
86
102
|
dependencies against the npm/PyPI registries and flag any package name
|
{coding_agent_audit-0.6.0 → coding_agent_audit-0.7.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.7.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
|
|
@@ -23,6 +23,8 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
<!-- mcp-name: io.github.stackscribe-ai/coding-agent-audit -->
|
|
25
25
|
|
|
26
|
+
[](https://m8ven.ai/mcp/stackscribe-ai-coding-agent-audit-1xiuww?s=readme)
|
|
27
|
+
|
|
26
28
|
A small, open-source CLI that scans your machine for:
|
|
27
29
|
|
|
28
30
|
1. **Known indicators of compromise (IOCs)** from publicly documented
|
|
@@ -110,6 +112,14 @@ Cursor Marketplace Claude Code trojan:
|
|
|
110
112
|
VS Code and Claude Desktop), plus a presence check that flags Codex/VS
|
|
111
113
|
Code (still unpatched at disclosure time) or Claude Desktop (patch status
|
|
112
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)
|
|
113
123
|
|
|
114
124
|
**Config heuristics** (`configscan.py`) — pattern-based, always worth a
|
|
115
125
|
second look rather than proof of compromise:
|
|
@@ -193,6 +203,9 @@ heuristics) - it can't do this package's filesystem IOC scan remotely.
|
|
|
193
203
|
Full request/response shape: `GET /openapi.json` on that URL, or the
|
|
194
204
|
[source](https://github.com/stackscribe-ai/coding-agent-audit) of the x402
|
|
195
205
|
worker. The free CLI above remains the full, unpaid alternative.
|
|
206
|
+
Registered and domain-verified on [402index.io](https://402index.io); see
|
|
207
|
+
[the writeup](https://stackscribe-ai.github.io/x402/cloudflare-workers/agent-payments/2026/11/03/x402-compliance-crawler-lessons.html)
|
|
208
|
+
on the three compliance gaps discovery crawlers found that our own tests missed.
|
|
196
209
|
|
|
197
210
|
## GitHub Action (CI gate for your repo's own config)
|
|
198
211
|
|
|
@@ -227,7 +240,7 @@ scan for free; this just packages it for sharing.
|
|
|
227
240
|
|
|
228
241
|
If your agent runtime supports the open `SKILL.md` standard (Claude Code,
|
|
229
242
|
Cursor, and others), a packaged version of this scan is live on the
|
|
230
|
-
[Agensi skill marketplace](https://www.agensi.io/skills/coding-agent-security-audit)
|
|
243
|
+
[Agensi skill marketplace](https://www.agensi.io/r/STACKSCRIBEAI?to=/skills/coding-agent-security-audit)
|
|
231
244
|
as "coding agent security audit" (free) — source in
|
|
232
245
|
[`agensi-skill/`](agensi-skill/). It's a zero-dependency, single-file port of
|
|
233
246
|
the same checks below, meant to be loaded by an agent directly rather than
|
|
@@ -9,6 +9,9 @@ from agentaudit.checks import (
|
|
|
9
9
|
check_linux_cron_persistence,
|
|
10
10
|
check_macos_launch_agent,
|
|
11
11
|
check_marker_files,
|
|
12
|
+
check_shai_hulud_launch_agent,
|
|
13
|
+
check_shai_hulud_project_markers,
|
|
14
|
+
check_shai_hulud_systemd_service,
|
|
12
15
|
check_ssh_authorized_keys,
|
|
13
16
|
check_tmp_tunnel_keys,
|
|
14
17
|
check_windows_scheduled_task,
|
|
@@ -19,6 +22,7 @@ from agentaudit.iocs import INCIDENTS
|
|
|
19
22
|
|
|
20
23
|
_IOC = INCIDENTS["openvsx-claude-code-2026-10"]
|
|
21
24
|
_EMBRACE = INCIDENTS["embrace-embedded-browser-2026-10"]
|
|
25
|
+
_SHAI_HULUD = INCIDENTS["mini-shai-hulud-gh-token-monitor-2026"]
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
def test_ssh_authorized_keys_clean(tmp_path: Path):
|
|
@@ -245,3 +249,81 @@ def test_embrace_unpatched_tools_claude_desktop_only_is_info(tmp_path: Path):
|
|
|
245
249
|
assert len(findings) == 1
|
|
246
250
|
assert findings[0].severity == "info"
|
|
247
251
|
assert "Claude Desktop" in findings[0].detail
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_shai_hulud_systemd_service_absent(tmp_path: Path):
|
|
255
|
+
assert check_shai_hulud_systemd_service(tmp_path) == []
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def test_shai_hulud_systemd_service_present(tmp_path: Path):
|
|
259
|
+
relpath = _SHAI_HULUD["linux_systemd_relpath"]
|
|
260
|
+
target = tmp_path / relpath
|
|
261
|
+
target.parent.mkdir(parents=True)
|
|
262
|
+
target.write_text("[Unit]\nDescription=gh-token-monitor\n")
|
|
263
|
+
findings = check_shai_hulud_systemd_service(tmp_path)
|
|
264
|
+
assert len(findings) == 1
|
|
265
|
+
assert findings[0].severity == "critical"
|
|
266
|
+
assert findings[0].check_id == "shai-hulud-systemd-service"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_shai_hulud_launch_agent_absent(tmp_path: Path):
|
|
270
|
+
assert check_shai_hulud_launch_agent(tmp_path) == []
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_shai_hulud_launch_agent_present(tmp_path: Path):
|
|
274
|
+
agents_dir = tmp_path / "Library" / "LaunchAgents"
|
|
275
|
+
agents_dir.mkdir(parents=True)
|
|
276
|
+
(agents_dir / _SHAI_HULUD["macos_launch_agent"]).write_text("<plist></plist>")
|
|
277
|
+
findings = check_shai_hulud_launch_agent(tmp_path)
|
|
278
|
+
assert len(findings) == 1
|
|
279
|
+
assert findings[0].severity == "critical"
|
|
280
|
+
assert findings[0].check_id == "shai-hulud-launch-agent"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_shai_hulud_project_markers_clean(tmp_path: Path):
|
|
284
|
+
(tmp_path / ".claude").mkdir()
|
|
285
|
+
assert check_shai_hulud_project_markers([tmp_path]) == []
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_shai_hulud_project_markers_one_present(tmp_path: Path):
|
|
289
|
+
claude_dir = tmp_path / ".claude"
|
|
290
|
+
claude_dir.mkdir()
|
|
291
|
+
(claude_dir / "router_runtime.js").write_text("// payload")
|
|
292
|
+
findings = check_shai_hulud_project_markers([tmp_path])
|
|
293
|
+
assert len(findings) == 1
|
|
294
|
+
assert findings[0].severity == "critical"
|
|
295
|
+
assert findings[0].check_id == "shai-hulud-project-marker"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_shai_hulud_project_markers_all_present(tmp_path: Path):
|
|
299
|
+
for relpath in _SHAI_HULUD["project_marker_files"]:
|
|
300
|
+
target = tmp_path / relpath
|
|
301
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
302
|
+
target.write_text("x")
|
|
303
|
+
findings = check_shai_hulud_project_markers([tmp_path])
|
|
304
|
+
assert len(findings) == len(_SHAI_HULUD["project_marker_files"])
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_shai_hulud_project_markers_nonexistent_dir(tmp_path: Path):
|
|
308
|
+
assert check_shai_hulud_project_markers([tmp_path / "nope"]) == []
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_shai_hulud_project_markers_skips_none():
|
|
312
|
+
assert check_shai_hulud_project_markers([None]) == []
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def test_shai_hulud_project_markers_dedupes_duplicate_dirs(tmp_path: Path):
|
|
316
|
+
claude_dir = tmp_path / ".claude"
|
|
317
|
+
claude_dir.mkdir()
|
|
318
|
+
(claude_dir / "setup.mjs").write_text("// dropper")
|
|
319
|
+
findings = check_shai_hulud_project_markers([tmp_path, tmp_path])
|
|
320
|
+
assert len(findings) == 1
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_shai_hulud_project_markers_dedupes_relative_and_absolute(tmp_path: Path, monkeypatch):
|
|
324
|
+
claude_dir = tmp_path / ".claude"
|
|
325
|
+
claude_dir.mkdir()
|
|
326
|
+
(claude_dir / "setup.mjs").write_text("// dropper")
|
|
327
|
+
monkeypatch.chdir(tmp_path)
|
|
328
|
+
findings = check_shai_hulud_project_markers([tmp_path, Path(".")])
|
|
329
|
+
assert len(findings) == 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/src/coding_agent_audit.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{coding_agent_audit-0.6.0 → coding_agent_audit-0.7.0}/src/coding_agent_audit.egg-info/requires.txt
RENAMED
|
File without changes
|
{coding_agent_audit-0.6.0 → coding_agent_audit-0.7.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
|