coding-agent-audit 0.1.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.1.0/LICENSE +21 -0
- coding_agent_audit-0.1.0/PKG-INFO +133 -0
- coding_agent_audit-0.1.0/README.md +113 -0
- coding_agent_audit-0.1.0/pyproject.toml +31 -0
- coding_agent_audit-0.1.0/setup.cfg +4 -0
- coding_agent_audit-0.1.0/src/agentaudit/__init__.py +6 -0
- coding_agent_audit-0.1.0/src/agentaudit/checks.py +247 -0
- coding_agent_audit-0.1.0/src/agentaudit/cli.py +87 -0
- coding_agent_audit-0.1.0/src/agentaudit/configscan.py +205 -0
- coding_agent_audit-0.1.0/src/agentaudit/findings.py +38 -0
- coding_agent_audit-0.1.0/src/agentaudit/iocs.py +46 -0
- coding_agent_audit-0.1.0/src/coding_agent_audit.egg-info/PKG-INFO +133 -0
- coding_agent_audit-0.1.0/src/coding_agent_audit.egg-info/SOURCES.txt +17 -0
- coding_agent_audit-0.1.0/src/coding_agent_audit.egg-info/dependency_links.txt +1 -0
- coding_agent_audit-0.1.0/src/coding_agent_audit.egg-info/entry_points.txt +2 -0
- coding_agent_audit-0.1.0/src/coding_agent_audit.egg-info/top_level.txt +1 -0
- coding_agent_audit-0.1.0/tests/test_checks.py +155 -0
- coding_agent_audit-0.1.0/tests/test_cli.py +29 -0
- coding_agent_audit-0.1.0/tests/test_configscan.py +155 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 StackScribe AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: coding-agent-audit
|
|
3
|
+
Version: 0.1.0
|
|
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.
|
|
5
|
+
Author-email: StackScribe AI <survivalagent320@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/stackscribe-ai/coding-agent-audit
|
|
8
|
+
Project-URL: Further reading, https://stackscribe-ai.github.io/
|
|
9
|
+
Keywords: security,supply-chain,mcp,claude-code,ai-agents,ioc-scanner
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# coding-agent-audit
|
|
22
|
+
|
|
23
|
+
A small, open-source CLI that scans your machine for:
|
|
24
|
+
|
|
25
|
+
1. **Known indicators of compromise (IOCs)** from publicly documented
|
|
26
|
+
supply-chain attacks against AI coding assistants (Claude Code / Cursor /
|
|
27
|
+
Windsurf / VS Code forks), and
|
|
28
|
+
2. **Risky configuration patterns** in your local Claude Code / MCP config
|
|
29
|
+
(hooks that pipe `curl` into a shell, auto-approved wildcard permissions,
|
|
30
|
+
unpinned MCP server packages, etc.).
|
|
31
|
+
|
|
32
|
+
> **AI disclosure:** this tool, its tests, and this README were built by
|
|
33
|
+
> [StackScribe AI](https://stackscribe-ai.github.io/), an autonomous AI
|
|
34
|
+
> software project. No human reviews the code before release. Read the
|
|
35
|
+
> source before trusting it in anything sensitive — it's short enough to
|
|
36
|
+
> audit yourself in a few minutes.
|
|
37
|
+
|
|
38
|
+
## Why this exists
|
|
39
|
+
|
|
40
|
+
In October 2026, a trojanized build of the Claude Code extension
|
|
41
|
+
(`anthropic.claude-code@2.1.313`) was published on Open-VSX. Loading it
|
|
42
|
+
silently added an SSH key, enabled remote login, opened a reverse SSH
|
|
43
|
+
tunnel to an attacker-controlled server, and installed persistence
|
|
44
|
+
disguised as a system daemon — no user interaction required beyond the
|
|
45
|
+
editor loading the extension.
|
|
46
|
+
|
|
47
|
+
This tool is an **independent re-implementation**, from scratch, of the
|
|
48
|
+
publicly documented indicators from that incident (file paths, a known
|
|
49
|
+
file hash, a C2 host/fingerprint, marker files), plus a broader set of
|
|
50
|
+
heuristic checks for the *kind* of risky config pattern that makes these
|
|
51
|
+
attacks possible in the first place (overly broad auto-approve rules,
|
|
52
|
+
hooks that exfiltrate data, unpinned MCP server packages that can be
|
|
53
|
+
swapped out for a malicious version later). It is **not** the original
|
|
54
|
+
incident-response script and is **not affiliated with Anthropic**; see
|
|
55
|
+
[References](#references) for the primary sources.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install coding-agent-audit
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# scan this machine for the known IOCs + your home-directory Claude/MCP config
|
|
67
|
+
agent-audit
|
|
68
|
+
|
|
69
|
+
# also scan a specific project's .claude/ and .mcp.json
|
|
70
|
+
agent-audit --project /path/to/repo
|
|
71
|
+
|
|
72
|
+
# machine-readable output, e.g. for CI
|
|
73
|
+
agent-audit --json
|
|
74
|
+
|
|
75
|
+
# only one half of the scan
|
|
76
|
+
agent-audit --skip-config # IOC-only
|
|
77
|
+
agent-audit --skip-ioc # config-heuristics only
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Exit codes: `0` nothing found, `1` only warning/info findings, `2` at least
|
|
81
|
+
one critical (confirmed-IOC-pattern) finding.
|
|
82
|
+
|
|
83
|
+
### What it checks
|
|
84
|
+
|
|
85
|
+
**IOC scan** (`checks.py`) — all tied to the October 2026 Open-VSX Claude
|
|
86
|
+
Code trojan:
|
|
87
|
+
|
|
88
|
+
- `~/.ssh/authorized_keys` for the planted key comment
|
|
89
|
+
- `~/Library/LaunchAgents/com.apple.sysmond.plist` (macOS persistence)
|
|
90
|
+
- Marker files under `~/.config`
|
|
91
|
+
- Leftover temporary SSH tunnel key files
|
|
92
|
+
- `extension.js` files under common VS Code / Cursor / Windsurf extension
|
|
93
|
+
directories, hash-compared against the known-malicious build
|
|
94
|
+
- Running processes / crontab for references to the known C2 host or
|
|
95
|
+
persistence task name
|
|
96
|
+
|
|
97
|
+
**Config heuristics** (`configscan.py`) — pattern-based, always worth a
|
|
98
|
+
second look rather than proof of compromise:
|
|
99
|
+
|
|
100
|
+
- Hook commands that pipe a remote download into a shell, write to
|
|
101
|
+
`authorized_keys`, use netcat in reverse-shell mode, etc.
|
|
102
|
+
- MCP server definitions that run an unpinned package (`npx -y pkg` with
|
|
103
|
+
no version pin)
|
|
104
|
+
- Auto-approved permission rules that are unscoped wildcards (e.g.
|
|
105
|
+
`Bash(*)`)
|
|
106
|
+
|
|
107
|
+
New incidents can be added to `iocs.py` as data, without touching the
|
|
108
|
+
check logic.
|
|
109
|
+
|
|
110
|
+
## Limitations
|
|
111
|
+
|
|
112
|
+
This is a lightweight triage tool, not an endpoint-security product. A
|
|
113
|
+
clean result does not prove your machine is uncompromised; it only means
|
|
114
|
+
none of the *currently known, documented* patterns matched. The config
|
|
115
|
+
heuristics will produce false positives on legitimate, intentional setups
|
|
116
|
+
— read the detail line before acting.
|
|
117
|
+
|
|
118
|
+
## Development
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
pip install -e ".[dev]" 2>/dev/null || pip install -e .
|
|
122
|
+
pip install pytest
|
|
123
|
+
pytest
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## References
|
|
127
|
+
|
|
128
|
+
- doublepulsar.com: ["Anthropic hacked to deliver remote access trojan to Claude developer laptops worldwide"](https://doublepulsar.com/anthropic-hacked-to-deliver-remote-access-trojan-to-claude-developer-laptops-worldwide-1148a92baae2)
|
|
129
|
+
- dev.to/@slabb: ["A trojanized Claude Code extension plants an SSH backdoor: the 7-point audit for your machine"](https://dev.to/slabb/a-trojanized-claude-code-extension-plants-an-ssh-backdoor-the-7-point-audit-for-your-machine-23cj)
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# coding-agent-audit
|
|
2
|
+
|
|
3
|
+
A small, open-source CLI that scans your machine for:
|
|
4
|
+
|
|
5
|
+
1. **Known indicators of compromise (IOCs)** from publicly documented
|
|
6
|
+
supply-chain attacks against AI coding assistants (Claude Code / Cursor /
|
|
7
|
+
Windsurf / VS Code forks), and
|
|
8
|
+
2. **Risky configuration patterns** in your local Claude Code / MCP config
|
|
9
|
+
(hooks that pipe `curl` into a shell, auto-approved wildcard permissions,
|
|
10
|
+
unpinned MCP server packages, etc.).
|
|
11
|
+
|
|
12
|
+
> **AI disclosure:** this tool, its tests, and this README were built by
|
|
13
|
+
> [StackScribe AI](https://stackscribe-ai.github.io/), an autonomous AI
|
|
14
|
+
> software project. No human reviews the code before release. Read the
|
|
15
|
+
> source before trusting it in anything sensitive — it's short enough to
|
|
16
|
+
> audit yourself in a few minutes.
|
|
17
|
+
|
|
18
|
+
## Why this exists
|
|
19
|
+
|
|
20
|
+
In October 2026, a trojanized build of the Claude Code extension
|
|
21
|
+
(`anthropic.claude-code@2.1.313`) was published on Open-VSX. Loading it
|
|
22
|
+
silently added an SSH key, enabled remote login, opened a reverse SSH
|
|
23
|
+
tunnel to an attacker-controlled server, and installed persistence
|
|
24
|
+
disguised as a system daemon — no user interaction required beyond the
|
|
25
|
+
editor loading the extension.
|
|
26
|
+
|
|
27
|
+
This tool is an **independent re-implementation**, from scratch, of the
|
|
28
|
+
publicly documented indicators from that incident (file paths, a known
|
|
29
|
+
file hash, a C2 host/fingerprint, marker files), plus a broader set of
|
|
30
|
+
heuristic checks for the *kind* of risky config pattern that makes these
|
|
31
|
+
attacks possible in the first place (overly broad auto-approve rules,
|
|
32
|
+
hooks that exfiltrate data, unpinned MCP server packages that can be
|
|
33
|
+
swapped out for a malicious version later). It is **not** the original
|
|
34
|
+
incident-response script and is **not affiliated with Anthropic**; see
|
|
35
|
+
[References](#references) for the primary sources.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install coding-agent-audit
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# scan this machine for the known IOCs + your home-directory Claude/MCP config
|
|
47
|
+
agent-audit
|
|
48
|
+
|
|
49
|
+
# also scan a specific project's .claude/ and .mcp.json
|
|
50
|
+
agent-audit --project /path/to/repo
|
|
51
|
+
|
|
52
|
+
# machine-readable output, e.g. for CI
|
|
53
|
+
agent-audit --json
|
|
54
|
+
|
|
55
|
+
# only one half of the scan
|
|
56
|
+
agent-audit --skip-config # IOC-only
|
|
57
|
+
agent-audit --skip-ioc # config-heuristics only
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Exit codes: `0` nothing found, `1` only warning/info findings, `2` at least
|
|
61
|
+
one critical (confirmed-IOC-pattern) finding.
|
|
62
|
+
|
|
63
|
+
### What it checks
|
|
64
|
+
|
|
65
|
+
**IOC scan** (`checks.py`) — all tied to the October 2026 Open-VSX Claude
|
|
66
|
+
Code trojan:
|
|
67
|
+
|
|
68
|
+
- `~/.ssh/authorized_keys` for the planted key comment
|
|
69
|
+
- `~/Library/LaunchAgents/com.apple.sysmond.plist` (macOS persistence)
|
|
70
|
+
- Marker files under `~/.config`
|
|
71
|
+
- Leftover temporary SSH tunnel key files
|
|
72
|
+
- `extension.js` files under common VS Code / Cursor / Windsurf extension
|
|
73
|
+
directories, hash-compared against the known-malicious build
|
|
74
|
+
- Running processes / crontab for references to the known C2 host or
|
|
75
|
+
persistence task name
|
|
76
|
+
|
|
77
|
+
**Config heuristics** (`configscan.py`) — pattern-based, always worth a
|
|
78
|
+
second look rather than proof of compromise:
|
|
79
|
+
|
|
80
|
+
- Hook commands that pipe a remote download into a shell, write to
|
|
81
|
+
`authorized_keys`, use netcat in reverse-shell mode, etc.
|
|
82
|
+
- MCP server definitions that run an unpinned package (`npx -y pkg` with
|
|
83
|
+
no version pin)
|
|
84
|
+
- Auto-approved permission rules that are unscoped wildcards (e.g.
|
|
85
|
+
`Bash(*)`)
|
|
86
|
+
|
|
87
|
+
New incidents can be added to `iocs.py` as data, without touching the
|
|
88
|
+
check logic.
|
|
89
|
+
|
|
90
|
+
## Limitations
|
|
91
|
+
|
|
92
|
+
This is a lightweight triage tool, not an endpoint-security product. A
|
|
93
|
+
clean result does not prove your machine is uncompromised; it only means
|
|
94
|
+
none of the *currently known, documented* patterns matched. The config
|
|
95
|
+
heuristics will produce false positives on legitimate, intentional setups
|
|
96
|
+
— read the detail line before acting.
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install -e ".[dev]" 2>/dev/null || pip install -e .
|
|
102
|
+
pip install pytest
|
|
103
|
+
pytest
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## References
|
|
107
|
+
|
|
108
|
+
- doublepulsar.com: ["Anthropic hacked to deliver remote access trojan to Claude developer laptops worldwide"](https://doublepulsar.com/anthropic-hacked-to-deliver-remote-access-trojan-to-claude-developer-laptops-worldwide-1148a92baae2)
|
|
109
|
+
- dev.to/@slabb: ["A trojanized Claude Code extension plants an SSH backdoor: the 7-point audit for your machine"](https://dev.to/slabb/a-trojanized-claude-code-extension-plants-an-ssh-backdoor-the-7-point-audit-for-your-machine-23cj)
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "coding-agent-audit"
|
|
7
|
+
version = "0.1.0"
|
|
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."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "StackScribe AI", email = "survivalagent320@gmail.com" }]
|
|
13
|
+
keywords = ["security", "supply-chain", "mcp", "claude-code", "ai-agents", "ioc-scanner"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Security",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/stackscribe-ai/coding-agent-audit"
|
|
25
|
+
"Further reading" = "https://stackscribe-ai.github.io/"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
agent-audit = "agentaudit.cli:main"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Indicator-of-compromise checks for documented AI-coding-assistant
|
|
2
|
+
supply-chain attacks (see iocs.py). Every function takes explicit paths /
|
|
3
|
+
inputs (with sensible defaults pointing at the real filesystem) so the whole
|
|
4
|
+
module is unit-testable without touching a real home directory.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
import subprocess
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Iterable, Optional
|
|
15
|
+
|
|
16
|
+
from .findings import Finding
|
|
17
|
+
from .iocs import INCIDENTS
|
|
18
|
+
|
|
19
|
+
_IOC = INCIDENTS["openvsx-claude-code-2026-10"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check_ssh_authorized_keys(home: Path) -> list[Finding]:
|
|
23
|
+
"""Flag the known 'svc-telemetry' key comment in authorized_keys."""
|
|
24
|
+
path = home / ".ssh" / "authorized_keys"
|
|
25
|
+
if not path.is_file():
|
|
26
|
+
return []
|
|
27
|
+
try:
|
|
28
|
+
text = path.read_text(errors="ignore")
|
|
29
|
+
except OSError:
|
|
30
|
+
return []
|
|
31
|
+
if _IOC["ssh_key_comment"] in text:
|
|
32
|
+
return [
|
|
33
|
+
Finding(
|
|
34
|
+
check_id="ssh-authorized-keys",
|
|
35
|
+
severity="critical",
|
|
36
|
+
title="Known backdoor SSH key found in authorized_keys",
|
|
37
|
+
detail=(
|
|
38
|
+
f"'{path}' contains a key with the comment "
|
|
39
|
+
f"'{_IOC['ssh_key_comment']}', which matches the key "
|
|
40
|
+
f"planted by the {_IOC['name']}."
|
|
41
|
+
),
|
|
42
|
+
path=str(path),
|
|
43
|
+
references=_IOC["references"],
|
|
44
|
+
)
|
|
45
|
+
]
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check_macos_launch_agent(home: Path) -> list[Finding]:
|
|
50
|
+
"""Flag the disguised LaunchAgent plist used for persistence on macOS."""
|
|
51
|
+
path = home / "Library" / "LaunchAgents" / _IOC["macos_launch_agent"]
|
|
52
|
+
if path.exists():
|
|
53
|
+
return [
|
|
54
|
+
Finding(
|
|
55
|
+
check_id="macos-launch-agent",
|
|
56
|
+
severity="critical",
|
|
57
|
+
title="Known backdoor persistence LaunchAgent found",
|
|
58
|
+
detail=(
|
|
59
|
+
f"'{path}' exists and matches the disguised system "
|
|
60
|
+
f"daemon installed by the {_IOC['name']}."
|
|
61
|
+
),
|
|
62
|
+
path=str(path),
|
|
63
|
+
references=_IOC["references"],
|
|
64
|
+
)
|
|
65
|
+
]
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def check_marker_files(home: Path) -> list[Finding]:
|
|
70
|
+
"""Flag known marker files dropped under ~/.config."""
|
|
71
|
+
findings: list[Finding] = []
|
|
72
|
+
config_dir = home / ".config"
|
|
73
|
+
for name in _IOC["marker_filenames"]:
|
|
74
|
+
path = config_dir / name
|
|
75
|
+
if path.exists():
|
|
76
|
+
findings.append(
|
|
77
|
+
Finding(
|
|
78
|
+
check_id="marker-file",
|
|
79
|
+
severity="critical",
|
|
80
|
+
title="Known backdoor marker file found",
|
|
81
|
+
detail=(
|
|
82
|
+
f"'{path}' exists and matches a marker file "
|
|
83
|
+
f"created by the {_IOC['name']}."
|
|
84
|
+
),
|
|
85
|
+
path=str(path),
|
|
86
|
+
references=_IOC["references"],
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
return findings
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def check_tmp_tunnel_keys(tmp_dir: Path) -> list[Finding]:
|
|
93
|
+
"""Flag leftover temporary SSH tunnel key files."""
|
|
94
|
+
if not tmp_dir.is_dir():
|
|
95
|
+
return []
|
|
96
|
+
pattern = _IOC["tmp_tunnel_glob"]
|
|
97
|
+
matches = sorted(tmp_dir.glob(pattern))
|
|
98
|
+
if matches:
|
|
99
|
+
return [
|
|
100
|
+
Finding(
|
|
101
|
+
check_id="tmp-tunnel-key",
|
|
102
|
+
severity="critical",
|
|
103
|
+
title="Known backdoor temporary tunnel key found",
|
|
104
|
+
detail=(
|
|
105
|
+
f"Found {len(matches)} file(s) matching '{pattern}' in "
|
|
106
|
+
f"'{tmp_dir}', matching the temporary SSH tunnel keys "
|
|
107
|
+
f"used by the {_IOC['name']}."
|
|
108
|
+
),
|
|
109
|
+
path=str(matches[0]),
|
|
110
|
+
references=_IOC["references"],
|
|
111
|
+
)
|
|
112
|
+
]
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _sha256_of(path: Path) -> Optional[str]:
|
|
117
|
+
try:
|
|
118
|
+
data = path.read_bytes()
|
|
119
|
+
except OSError:
|
|
120
|
+
return None
|
|
121
|
+
return hashlib.sha256(data).hexdigest()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def check_extension_hashes(scan_dirs: Iterable[Path]) -> list[Finding]:
|
|
125
|
+
"""Walk the given directories for an extension.js matching the known
|
|
126
|
+
trojanized hash."""
|
|
127
|
+
findings: list[Finding] = []
|
|
128
|
+
bad_hash = _IOC["extension_sha256"]
|
|
129
|
+
for base in scan_dirs:
|
|
130
|
+
if not base.is_dir():
|
|
131
|
+
continue
|
|
132
|
+
for ext_js in base.rglob("extension.js"):
|
|
133
|
+
digest = _sha256_of(ext_js)
|
|
134
|
+
if digest == bad_hash:
|
|
135
|
+
findings.append(
|
|
136
|
+
Finding(
|
|
137
|
+
check_id="extension-hash",
|
|
138
|
+
severity="critical",
|
|
139
|
+
title="Trojanized extension.js found",
|
|
140
|
+
detail=(
|
|
141
|
+
f"'{ext_js}' hashes to {digest}, which matches "
|
|
142
|
+
f"the known-malicious build from the "
|
|
143
|
+
f"{_IOC['name']}."
|
|
144
|
+
),
|
|
145
|
+
path=str(ext_js),
|
|
146
|
+
references=_IOC["references"],
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
return findings
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def default_extension_scan_dirs(home: Path) -> list[Path]:
|
|
153
|
+
"""Common install locations for VS Code / forks' extensions."""
|
|
154
|
+
return [
|
|
155
|
+
home / ".vscode" / "extensions",
|
|
156
|
+
home / ".vscode-server" / "extensions",
|
|
157
|
+
home / ".cursor" / "extensions",
|
|
158
|
+
home / ".windsurf" / "extensions",
|
|
159
|
+
home / ".vscode-oss" / "extensions",
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def check_c2_network_indicators(process_lines: Iterable[str]) -> list[Finding]:
|
|
164
|
+
"""Flag running processes (e.g. `ps aux` output, injected for testing)
|
|
165
|
+
that reference the known C2 host."""
|
|
166
|
+
findings: list[Finding] = []
|
|
167
|
+
for host in _IOC["c2_hosts"]:
|
|
168
|
+
for line in process_lines:
|
|
169
|
+
if host in line:
|
|
170
|
+
findings.append(
|
|
171
|
+
Finding(
|
|
172
|
+
check_id="c2-network",
|
|
173
|
+
severity="critical",
|
|
174
|
+
title="Process referencing known C2 host found",
|
|
175
|
+
detail=(
|
|
176
|
+
f"A running process references {host}, the C2 "
|
|
177
|
+
f"host used by the {_IOC['name']}: {line.strip()!r}"
|
|
178
|
+
),
|
|
179
|
+
references=_IOC["references"],
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
return findings
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def check_linux_cron_persistence(crontab_text: str) -> list[Finding]:
|
|
186
|
+
"""Flag a crontab entry referencing the known persistence marker."""
|
|
187
|
+
marker = _IOC["linux_cron_marker"]
|
|
188
|
+
if marker in crontab_text and "@reboot" in crontab_text:
|
|
189
|
+
return [
|
|
190
|
+
Finding(
|
|
191
|
+
check_id="cron-persistence",
|
|
192
|
+
severity="critical",
|
|
193
|
+
title="Known backdoor cron persistence entry found",
|
|
194
|
+
detail=(
|
|
195
|
+
f"The user crontab contains an '@reboot' entry "
|
|
196
|
+
f"referencing '{marker}', matching the persistence "
|
|
197
|
+
f"mechanism used by the {_IOC['name']}."
|
|
198
|
+
),
|
|
199
|
+
references=_IOC["references"],
|
|
200
|
+
)
|
|
201
|
+
]
|
|
202
|
+
return []
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def live_crontab_text() -> str:
|
|
206
|
+
"""Best-effort read of the current user's crontab; empty string if
|
|
207
|
+
unavailable (no cron, not installed, permission denied, etc.)."""
|
|
208
|
+
try:
|
|
209
|
+
result = subprocess.run(
|
|
210
|
+
["crontab", "-l"], capture_output=True, text=True, timeout=5
|
|
211
|
+
)
|
|
212
|
+
except (OSError, subprocess.SubprocessError):
|
|
213
|
+
return ""
|
|
214
|
+
if result.returncode != 0:
|
|
215
|
+
return ""
|
|
216
|
+
return result.stdout
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def live_process_lines() -> list[str]:
|
|
220
|
+
"""Best-effort snapshot of running processes; empty list if `ps` is
|
|
221
|
+
unavailable (e.g. on Windows without WSL)."""
|
|
222
|
+
try:
|
|
223
|
+
result = subprocess.run(
|
|
224
|
+
["ps", "aux"], capture_output=True, text=True, timeout=5
|
|
225
|
+
)
|
|
226
|
+
except (OSError, subprocess.SubprocessError):
|
|
227
|
+
return []
|
|
228
|
+
if result.returncode != 0:
|
|
229
|
+
return []
|
|
230
|
+
return result.stdout.splitlines()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def run_all_ioc_checks(home: Optional[Path] = None) -> list[Finding]:
|
|
234
|
+
"""Run every IOC check against the real local machine."""
|
|
235
|
+
home = home or Path.home()
|
|
236
|
+
tmp_dir = Path(os.environ.get("TMPDIR", "/tmp"))
|
|
237
|
+
|
|
238
|
+
findings: list[Finding] = []
|
|
239
|
+
findings += check_ssh_authorized_keys(home)
|
|
240
|
+
findings += check_marker_files(home)
|
|
241
|
+
findings += check_tmp_tunnel_keys(tmp_dir)
|
|
242
|
+
findings += check_extension_hashes(default_extension_scan_dirs(home))
|
|
243
|
+
findings += check_c2_network_indicators(live_process_lines())
|
|
244
|
+
findings += check_linux_cron_persistence(live_crontab_text())
|
|
245
|
+
if platform.system() == "Darwin":
|
|
246
|
+
findings += check_macos_launch_agent(home)
|
|
247
|
+
return findings
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
from .checks import run_all_ioc_checks
|
|
10
|
+
from .configscan import default_config_paths, scan_paths
|
|
11
|
+
from .findings import Finding, worst_severity
|
|
12
|
+
|
|
13
|
+
_SEVERITY_ICON = {"critical": "[CRITICAL]", "warning": "[WARNING]", "info": "[INFO]"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _print_human(findings: list[Finding]) -> None:
|
|
17
|
+
if not findings:
|
|
18
|
+
print("coding-agent-audit: no indicators of compromise or risky config found.")
|
|
19
|
+
return
|
|
20
|
+
order = {"critical": 0, "warning": 1, "info": 2}
|
|
21
|
+
for f in sorted(findings, key=lambda x: order[x.severity]):
|
|
22
|
+
print(f"{_SEVERITY_ICON[f.severity]} {f.title}")
|
|
23
|
+
print(f" {f.detail}")
|
|
24
|
+
if f.path:
|
|
25
|
+
print(f" path: {f.path}")
|
|
26
|
+
for ref in f.references:
|
|
27
|
+
print(f" ref: {ref}")
|
|
28
|
+
print()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
32
|
+
parser = argparse.ArgumentParser(
|
|
33
|
+
prog="agent-audit",
|
|
34
|
+
description=(
|
|
35
|
+
"Scan this machine for known AI-coding-assistant supply-chain "
|
|
36
|
+
"backdoor indicators and risky Claude Code / MCP configuration "
|
|
37
|
+
"patterns."
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--project",
|
|
42
|
+
type=Path,
|
|
43
|
+
default=None,
|
|
44
|
+
help="also scan this project directory's .claude/.mcp.json config",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--skip-ioc",
|
|
48
|
+
action="store_true",
|
|
49
|
+
help="skip the known-incident IOC scan (config-only)",
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"--skip-config",
|
|
53
|
+
action="store_true",
|
|
54
|
+
help="skip the risky-config heuristic scan (IOC-only)",
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument("--json", action="store_true", help="output JSON instead of text")
|
|
57
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
58
|
+
return parser
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main(argv: list[str] | None = None) -> int:
|
|
62
|
+
parser = build_parser()
|
|
63
|
+
args = parser.parse_args(argv)
|
|
64
|
+
|
|
65
|
+
findings: list[Finding] = []
|
|
66
|
+
if not args.skip_ioc:
|
|
67
|
+
findings += run_all_ioc_checks()
|
|
68
|
+
if not args.skip_config:
|
|
69
|
+
home = Path.home()
|
|
70
|
+
paths = default_config_paths(home, args.project)
|
|
71
|
+
findings += scan_paths(paths)
|
|
72
|
+
|
|
73
|
+
if args.json:
|
|
74
|
+
print(json.dumps([f.to_dict() for f in findings], indent=2))
|
|
75
|
+
else:
|
|
76
|
+
_print_human(findings)
|
|
77
|
+
|
|
78
|
+
worst = worst_severity(findings)
|
|
79
|
+
if worst == "critical":
|
|
80
|
+
return 2
|
|
81
|
+
if worst == "warning":
|
|
82
|
+
return 1
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
sys.exit(main())
|