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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """coding-agent-audit: local indicator-of-compromise and risky-config scanner
2
+ for AI coding assistants (Claude Code, Cursor, Windsurf, VS Code forks, MCP
3
+ servers).
4
+ """
5
+
6
+ __version__ = "0.1.0"
@@ -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())