coding-agent-audit 0.6.1__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.
Files changed (24) hide show
  1. {coding_agent_audit-0.6.1/src/coding_agent_audit.egg-info → coding_agent_audit-0.7.0}/PKG-INFO +12 -1
  2. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/README.md +11 -0
  3. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/pyproject.toml +1 -1
  4. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/src/agentaudit/__init__.py +1 -1
  5. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/src/agentaudit/checks.py +98 -1
  6. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/src/agentaudit/cli.py +2 -1
  7. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/src/agentaudit/iocs.py +31 -0
  8. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0/src/coding_agent_audit.egg-info}/PKG-INFO +12 -1
  9. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/tests/test_checks.py +82 -0
  10. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/LICENSE +0 -0
  11. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/setup.cfg +0 -0
  12. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/src/agentaudit/configscan.py +0 -0
  13. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/src/agentaudit/findings.py +0 -0
  14. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/src/agentaudit/mcp_server.py +0 -0
  15. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/src/agentaudit/phantomdeps.py +0 -0
  16. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/src/coding_agent_audit.egg-info/SOURCES.txt +0 -0
  17. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/src/coding_agent_audit.egg-info/dependency_links.txt +0 -0
  18. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/src/coding_agent_audit.egg-info/entry_points.txt +0 -0
  19. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/src/coding_agent_audit.egg-info/requires.txt +0 -0
  20. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/src/coding_agent_audit.egg-info/top_level.txt +0 -0
  21. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/tests/test_cli.py +0 -0
  22. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/tests/test_configscan.py +0 -0
  23. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.0}/tests/test_mcp_server.py +0 -0
  24. {coding_agent_audit-0.6.1 → coding_agent_audit-0.7.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.6.1
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
@@ -112,6 +112,14 @@ 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)
115
123
 
116
124
  **Config heuristics** (`configscan.py`) — pattern-based, always worth a
117
125
  second look rather than proof of compromise:
@@ -195,6 +203,9 @@ heuristics) - it can't do this package's filesystem IOC scan remotely.
195
203
  Full request/response shape: `GET /openapi.json` on that URL, or the
196
204
  [source](https://github.com/stackscribe-ai/coding-agent-audit) of the x402
197
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.
198
209
 
199
210
  ## GitHub Action (CI gate for your repo's own config)
200
211
 
@@ -91,6 +91,14 @@ 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)
94
102
 
95
103
  **Config heuristics** (`configscan.py`) — pattern-based, always worth a
96
104
  second look rather than proof of compromise:
@@ -174,6 +182,9 @@ heuristics) - it can't do this package's filesystem IOC scan remotely.
174
182
  Full request/response shape: `GET /openapi.json` on that URL, or the
175
183
  [source](https://github.com/stackscribe-ai/coding-agent-audit) of the x402
176
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.
177
188
 
178
189
  ## GitHub Action (CI gate for your repo's own config)
179
190
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "coding-agent-audit"
7
- version = "0.6.1"
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"
@@ -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.6.1"
6
+ __version__ = "0.7.0"
@@ -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 run_all_ioc_checks(home: Optional[Path] = None) -> list[Finding]:
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
- findings += run_all_ioc_checks()
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
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-agent-audit
3
- Version: 0.6.1
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
@@ -112,6 +112,14 @@ 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)
115
123
 
116
124
  **Config heuristics** (`configscan.py`) — pattern-based, always worth a
117
125
  second look rather than proof of compromise:
@@ -195,6 +203,9 @@ heuristics) - it can't do this package's filesystem IOC scan remotely.
195
203
  Full request/response shape: `GET /openapi.json` on that URL, or the
196
204
  [source](https://github.com/stackscribe-ai/coding-agent-audit) of the x402
197
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.
198
209
 
199
210
  ## GitHub Action (CI gate for your repo's own config)
200
211
 
@@ -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