mod-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.
- mod_audit-0.1.0/LICENSE +21 -0
- mod_audit-0.1.0/PKG-INFO +190 -0
- mod_audit-0.1.0/README.md +167 -0
- mod_audit-0.1.0/pyproject.toml +34 -0
- mod_audit-0.1.0/setup.cfg +4 -0
- mod_audit-0.1.0/src/mod_audit/__init__.py +20 -0
- mod_audit-0.1.0/src/mod_audit/cli.py +148 -0
- mod_audit-0.1.0/src/mod_audit/rules.py +520 -0
- mod_audit-0.1.0/src/mod_audit.egg-info/PKG-INFO +190 -0
- mod_audit-0.1.0/src/mod_audit.egg-info/SOURCES.txt +13 -0
- mod_audit-0.1.0/src/mod_audit.egg-info/dependency_links.txt +1 -0
- mod_audit-0.1.0/src/mod_audit.egg-info/entry_points.txt +2 -0
- mod_audit-0.1.0/src/mod_audit.egg-info/top_level.txt +1 -0
- mod_audit-0.1.0/tests/test_cli.py +119 -0
- mod_audit-0.1.0/tests/test_rules.py +114 -0
mod_audit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hao li
|
|
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.
|
mod_audit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mod-audit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Static supply-chain auditor for Claude Code Mods — local, offline, stdlib-only
|
|
5
|
+
Author: hao li
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/hahahahahahahahah6/mod-audit
|
|
8
|
+
Keywords: claude-code,mods,supply-chain,security,static-analysis,audit
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# mod-audit
|
|
25
|
+
|
|
26
|
+
Static supply-chain auditor for **Claude Code Mods** — local, offline, stdlib-only.
|
|
27
|
+
|
|
28
|
+
Claude Code Mods are TypeScript plugin packages that usually live under
|
|
29
|
+
`~/.claude/plugins/` and run **lifecycle hooks with your shell privileges**.
|
|
30
|
+
A single trojanized mod update can pipe `curl | sh` straight into your
|
|
31
|
+
machine. mod-audit scans a mod before you install it, snapshots the trusted
|
|
32
|
+
state, and diffs later updates against that snapshot — so a pin-swap style
|
|
33
|
+
update hijack lights up instead of slipping through.
|
|
34
|
+
|
|
35
|
+
Zero third-party dependencies. Python >= 3.9. No network calls, ever.
|
|
36
|
+
|
|
37
|
+
## Why this exists
|
|
38
|
+
|
|
39
|
+
The agent supply chain is getting hit, repeatedly, in public:
|
|
40
|
+
|
|
41
|
+
- **AIR SkillJacking** — 925 skills hijacked, reaching an estimated 134k agents.
|
|
42
|
+
- **Plugin4Shell** — pin-swap attacks bypass SHA-pinning on plugin updates,
|
|
43
|
+
swapping trusted code for malicious code between the pin check and install.
|
|
44
|
+
- **Pwn2Own Ireland** — a Codex argument-injection flaw worth $40k showed how
|
|
45
|
+
agent tooling becomes a shell-execution primitive.
|
|
46
|
+
- **SKILLCLOAK** — cloaking techniques that bypass 90%+ of existing scanners.
|
|
47
|
+
|
|
48
|
+
Most defenses are either cloud-based scanners (your mod source leaves your
|
|
49
|
+
machine) or metadata-only reviewers that never look at the TypeScript that
|
|
50
|
+
actually runs. mod-audit does the opposite: it runs on your machine, offline,
|
|
51
|
+
and reads the code.
|
|
52
|
+
|
|
53
|
+
## How it differs
|
|
54
|
+
|
|
55
|
+
| | mod-audit | ClawSecure Watchtower | rad-security AgentKeeper |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| Where it runs | Local / offline | Cloud scan | Cloud scan |
|
|
58
|
+
| Audits Mod TypeScript source | Yes | Partial | No — plugin/skill metadata only |
|
|
59
|
+
| Lifecycle hook analysis | Yes (shell patterns) | Generic | Metadata-level |
|
|
60
|
+
| Trojanized-update diffing | Yes (`snapshot`/`diff`) | No | No |
|
|
61
|
+
| Dependencies | Zero (stdlib only) | SaaS | SaaS |
|
|
62
|
+
|
|
63
|
+
Positioning: **local + offline + Mod TypeScript code specialist**. It does not
|
|
64
|
+
replace a metadata/policy reviewer — it covers the layer those tools skip:
|
|
65
|
+
the code that actually executes on your box.
|
|
66
|
+
|
|
67
|
+
## Install
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install mod-audit
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Quick start
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# 1. Audit a mod before installing it
|
|
77
|
+
mod-audit scan ~/.claude/plugins/some-mod
|
|
78
|
+
|
|
79
|
+
# 2. Snapshot the trusted state right after a clean install
|
|
80
|
+
mod-audit snapshot ~/.claude/plugins/some-mod --out ~/snapshots/some-mod.snapshot
|
|
81
|
+
|
|
82
|
+
# 3. After every update, diff against the snapshot
|
|
83
|
+
mod-audit diff ~/.claude/plugins/some-mod --against ~/snapshots/some-mod.snapshot
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
JSON output for scripting:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
mod-audit scan ./my-mod --format json
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## What it checks
|
|
93
|
+
|
|
94
|
+
### 1. Dangerous lifecycle hooks (`plugin.json` / `hooks.json` / `package.json`)
|
|
95
|
+
|
|
96
|
+
| Rule | Severity | What it catches |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `HOOK-PIPED-DOWNLOAD` | high | `curl … \| sh`, `wget … \| bash` in hooks |
|
|
99
|
+
| `HOOK-B64-EXEC` | high | base64 decode piped into execution |
|
|
100
|
+
| `HOOK-EXFIL` | high | `curl --data` exfiltrating data from a hook |
|
|
101
|
+
| `HOOK-REVERSE-SHELL` | critical | `nc -e`, `/dev/tcp/` reverse shells |
|
|
102
|
+
| `HOOK-SUDO` | high | privilege escalation in hooks |
|
|
103
|
+
| `HOOK-RM-RF` | high | destructive recursive deletes |
|
|
104
|
+
| `HOOK-CHMOD-EXEC` | medium | flipping files executable at install time |
|
|
105
|
+
| `HOOK-SHELL-EXEC` | medium | any other shell hook (runs as you) |
|
|
106
|
+
|
|
107
|
+
### 2. Shell-execution patterns in `.ts`/`.js` source
|
|
108
|
+
|
|
109
|
+
| Rule | Severity | What it catches |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| `TS-SHELL-TRUE` | high | `exec/spawn` with `shell: true` |
|
|
112
|
+
| `TS-EXEC-CONCAT` | high | concatenated/interpolated command strings |
|
|
113
|
+
| `TS-EXEC` | medium | `child_process` usage to review |
|
|
114
|
+
| `TS-EVAL` | high | `eval()` / `new Function()` |
|
|
115
|
+
| `TS-DYN-IMPORT` | medium | dynamic `require()`/`import()` with non-literal specifiers |
|
|
116
|
+
| `TS-PERSISTENCE` | high | cron/launchd persistence references |
|
|
117
|
+
| `TS-DOTFILE-WRITE` | medium | writes derived from `$HOME`/`$PATH` |
|
|
118
|
+
|
|
119
|
+
### 3. Env / API-key exfiltration
|
|
120
|
+
|
|
121
|
+
| Rule | Severity | What it catches |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| `ENV-EXFIL` | high | `process.env.*(API_KEY\|TOKEN\|SECRET\|PRIVATE)` within a few lines of a network sink (`fetch`, `axios`, `http.request`, …) |
|
|
124
|
+
|
|
125
|
+
### 4. Trojanized-update diff (`snapshot` / `diff`)
|
|
126
|
+
|
|
127
|
+
| Rule | Severity | What it catches |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `DIFF-NEW-FILE` | medium | files that appeared since the snapshot |
|
|
130
|
+
| `DIFF-CHANGED-FILE` | medium | files whose hash changed |
|
|
131
|
+
| `DIFF-REMOVED-FILE` | low | files that disappeared |
|
|
132
|
+
| `DIFF-HOOK-CHANGED` | high | hook commands added or swapped since the snapshot |
|
|
133
|
+
| `DIFF-HOOK-REMOVED` | low | hook commands removed |
|
|
134
|
+
|
|
135
|
+
New and changed files are re-scanned with all content rules during `diff`.
|
|
136
|
+
|
|
137
|
+
### 5. Permissions manifest review
|
|
138
|
+
|
|
139
|
+
| Rule | Severity | What it catches |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `PERM-SHELL-OVERGRANT` | high | shell granted without per-action confirmation |
|
|
142
|
+
| `PERM-TOOL-SHELL` | high | shell-capable tool granted without constraints |
|
|
143
|
+
| `PERM-NETWORK-OVERGRANT` | medium | unrestricted network access |
|
|
144
|
+
| `PERM-FS-OVERGRANT` | medium | broad filesystem write access |
|
|
145
|
+
|
|
146
|
+
Every finding includes the rule id, severity, `file:line`, an explanation,
|
|
147
|
+
and a concrete fix.
|
|
148
|
+
|
|
149
|
+
## CI integration
|
|
150
|
+
|
|
151
|
+
mod-audit is CI-ready: it exits `1` when any finding meets `--fail-on`
|
|
152
|
+
(default `high`), `0` when clean, `2` on usage errors.
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
# .github/workflows/mod-audit.yml
|
|
156
|
+
name: mod-audit
|
|
157
|
+
on: [push, pull_request]
|
|
158
|
+
jobs:
|
|
159
|
+
audit:
|
|
160
|
+
runs-on: ubuntu-latest
|
|
161
|
+
steps:
|
|
162
|
+
- uses: actions/checkout@v4
|
|
163
|
+
- uses: actions/setup-python@v5
|
|
164
|
+
with:
|
|
165
|
+
python-version: "3.12"
|
|
166
|
+
- run: pip install mod-audit
|
|
167
|
+
- run: mod-audit scan ./my-mod --format json
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Gate updates in a scheduled job:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
mod-audit diff ~/.claude/plugins/my-mod --against ~/snapshots/my-mod.snapshot --fail-on medium
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Limitations
|
|
177
|
+
|
|
178
|
+
- **Offline heuristics, not a sandbox.** Rules are pattern-based and can miss
|
|
179
|
+
obfuscated code or flag benign code. Treat findings as triage signals.
|
|
180
|
+
- **No execution.** The tool never runs mod code, which is the point — but it
|
|
181
|
+
also means runtime-only behavior (e.g. payloads fetched at runtime) is out
|
|
182
|
+
of scope.
|
|
183
|
+
- **Snapshot trust.** `diff` is only as trustworthy as the snapshot: take it
|
|
184
|
+
from a clean install and store it where the mod updater cannot modify it.
|
|
185
|
+
- **TypeScript via regex, not a parser.** Keeps the tool stdlib-only and fast;
|
|
186
|
+
heavily minified or dynamically generated code may need manual review.
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# mod-audit
|
|
2
|
+
|
|
3
|
+
Static supply-chain auditor for **Claude Code Mods** — local, offline, stdlib-only.
|
|
4
|
+
|
|
5
|
+
Claude Code Mods are TypeScript plugin packages that usually live under
|
|
6
|
+
`~/.claude/plugins/` and run **lifecycle hooks with your shell privileges**.
|
|
7
|
+
A single trojanized mod update can pipe `curl | sh` straight into your
|
|
8
|
+
machine. mod-audit scans a mod before you install it, snapshots the trusted
|
|
9
|
+
state, and diffs later updates against that snapshot — so a pin-swap style
|
|
10
|
+
update hijack lights up instead of slipping through.
|
|
11
|
+
|
|
12
|
+
Zero third-party dependencies. Python >= 3.9. No network calls, ever.
|
|
13
|
+
|
|
14
|
+
## Why this exists
|
|
15
|
+
|
|
16
|
+
The agent supply chain is getting hit, repeatedly, in public:
|
|
17
|
+
|
|
18
|
+
- **AIR SkillJacking** — 925 skills hijacked, reaching an estimated 134k agents.
|
|
19
|
+
- **Plugin4Shell** — pin-swap attacks bypass SHA-pinning on plugin updates,
|
|
20
|
+
swapping trusted code for malicious code between the pin check and install.
|
|
21
|
+
- **Pwn2Own Ireland** — a Codex argument-injection flaw worth $40k showed how
|
|
22
|
+
agent tooling becomes a shell-execution primitive.
|
|
23
|
+
- **SKILLCLOAK** — cloaking techniques that bypass 90%+ of existing scanners.
|
|
24
|
+
|
|
25
|
+
Most defenses are either cloud-based scanners (your mod source leaves your
|
|
26
|
+
machine) or metadata-only reviewers that never look at the TypeScript that
|
|
27
|
+
actually runs. mod-audit does the opposite: it runs on your machine, offline,
|
|
28
|
+
and reads the code.
|
|
29
|
+
|
|
30
|
+
## How it differs
|
|
31
|
+
|
|
32
|
+
| | mod-audit | ClawSecure Watchtower | rad-security AgentKeeper |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| Where it runs | Local / offline | Cloud scan | Cloud scan |
|
|
35
|
+
| Audits Mod TypeScript source | Yes | Partial | No — plugin/skill metadata only |
|
|
36
|
+
| Lifecycle hook analysis | Yes (shell patterns) | Generic | Metadata-level |
|
|
37
|
+
| Trojanized-update diffing | Yes (`snapshot`/`diff`) | No | No |
|
|
38
|
+
| Dependencies | Zero (stdlib only) | SaaS | SaaS |
|
|
39
|
+
|
|
40
|
+
Positioning: **local + offline + Mod TypeScript code specialist**. It does not
|
|
41
|
+
replace a metadata/policy reviewer — it covers the layer those tools skip:
|
|
42
|
+
the code that actually executes on your box.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install mod-audit
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# 1. Audit a mod before installing it
|
|
54
|
+
mod-audit scan ~/.claude/plugins/some-mod
|
|
55
|
+
|
|
56
|
+
# 2. Snapshot the trusted state right after a clean install
|
|
57
|
+
mod-audit snapshot ~/.claude/plugins/some-mod --out ~/snapshots/some-mod.snapshot
|
|
58
|
+
|
|
59
|
+
# 3. After every update, diff against the snapshot
|
|
60
|
+
mod-audit diff ~/.claude/plugins/some-mod --against ~/snapshots/some-mod.snapshot
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
JSON output for scripting:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
mod-audit scan ./my-mod --format json
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## What it checks
|
|
70
|
+
|
|
71
|
+
### 1. Dangerous lifecycle hooks (`plugin.json` / `hooks.json` / `package.json`)
|
|
72
|
+
|
|
73
|
+
| Rule | Severity | What it catches |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `HOOK-PIPED-DOWNLOAD` | high | `curl … \| sh`, `wget … \| bash` in hooks |
|
|
76
|
+
| `HOOK-B64-EXEC` | high | base64 decode piped into execution |
|
|
77
|
+
| `HOOK-EXFIL` | high | `curl --data` exfiltrating data from a hook |
|
|
78
|
+
| `HOOK-REVERSE-SHELL` | critical | `nc -e`, `/dev/tcp/` reverse shells |
|
|
79
|
+
| `HOOK-SUDO` | high | privilege escalation in hooks |
|
|
80
|
+
| `HOOK-RM-RF` | high | destructive recursive deletes |
|
|
81
|
+
| `HOOK-CHMOD-EXEC` | medium | flipping files executable at install time |
|
|
82
|
+
| `HOOK-SHELL-EXEC` | medium | any other shell hook (runs as you) |
|
|
83
|
+
|
|
84
|
+
### 2. Shell-execution patterns in `.ts`/`.js` source
|
|
85
|
+
|
|
86
|
+
| Rule | Severity | What it catches |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `TS-SHELL-TRUE` | high | `exec/spawn` with `shell: true` |
|
|
89
|
+
| `TS-EXEC-CONCAT` | high | concatenated/interpolated command strings |
|
|
90
|
+
| `TS-EXEC` | medium | `child_process` usage to review |
|
|
91
|
+
| `TS-EVAL` | high | `eval()` / `new Function()` |
|
|
92
|
+
| `TS-DYN-IMPORT` | medium | dynamic `require()`/`import()` with non-literal specifiers |
|
|
93
|
+
| `TS-PERSISTENCE` | high | cron/launchd persistence references |
|
|
94
|
+
| `TS-DOTFILE-WRITE` | medium | writes derived from `$HOME`/`$PATH` |
|
|
95
|
+
|
|
96
|
+
### 3. Env / API-key exfiltration
|
|
97
|
+
|
|
98
|
+
| Rule | Severity | What it catches |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `ENV-EXFIL` | high | `process.env.*(API_KEY\|TOKEN\|SECRET\|PRIVATE)` within a few lines of a network sink (`fetch`, `axios`, `http.request`, …) |
|
|
101
|
+
|
|
102
|
+
### 4. Trojanized-update diff (`snapshot` / `diff`)
|
|
103
|
+
|
|
104
|
+
| Rule | Severity | What it catches |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `DIFF-NEW-FILE` | medium | files that appeared since the snapshot |
|
|
107
|
+
| `DIFF-CHANGED-FILE` | medium | files whose hash changed |
|
|
108
|
+
| `DIFF-REMOVED-FILE` | low | files that disappeared |
|
|
109
|
+
| `DIFF-HOOK-CHANGED` | high | hook commands added or swapped since the snapshot |
|
|
110
|
+
| `DIFF-HOOK-REMOVED` | low | hook commands removed |
|
|
111
|
+
|
|
112
|
+
New and changed files are re-scanned with all content rules during `diff`.
|
|
113
|
+
|
|
114
|
+
### 5. Permissions manifest review
|
|
115
|
+
|
|
116
|
+
| Rule | Severity | What it catches |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| `PERM-SHELL-OVERGRANT` | high | shell granted without per-action confirmation |
|
|
119
|
+
| `PERM-TOOL-SHELL` | high | shell-capable tool granted without constraints |
|
|
120
|
+
| `PERM-NETWORK-OVERGRANT` | medium | unrestricted network access |
|
|
121
|
+
| `PERM-FS-OVERGRANT` | medium | broad filesystem write access |
|
|
122
|
+
|
|
123
|
+
Every finding includes the rule id, severity, `file:line`, an explanation,
|
|
124
|
+
and a concrete fix.
|
|
125
|
+
|
|
126
|
+
## CI integration
|
|
127
|
+
|
|
128
|
+
mod-audit is CI-ready: it exits `1` when any finding meets `--fail-on`
|
|
129
|
+
(default `high`), `0` when clean, `2` on usage errors.
|
|
130
|
+
|
|
131
|
+
```yaml
|
|
132
|
+
# .github/workflows/mod-audit.yml
|
|
133
|
+
name: mod-audit
|
|
134
|
+
on: [push, pull_request]
|
|
135
|
+
jobs:
|
|
136
|
+
audit:
|
|
137
|
+
runs-on: ubuntu-latest
|
|
138
|
+
steps:
|
|
139
|
+
- uses: actions/checkout@v4
|
|
140
|
+
- uses: actions/setup-python@v5
|
|
141
|
+
with:
|
|
142
|
+
python-version: "3.12"
|
|
143
|
+
- run: pip install mod-audit
|
|
144
|
+
- run: mod-audit scan ./my-mod --format json
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Gate updates in a scheduled job:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
mod-audit diff ~/.claude/plugins/my-mod --against ~/snapshots/my-mod.snapshot --fail-on medium
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Limitations
|
|
154
|
+
|
|
155
|
+
- **Offline heuristics, not a sandbox.** Rules are pattern-based and can miss
|
|
156
|
+
obfuscated code or flag benign code. Treat findings as triage signals.
|
|
157
|
+
- **No execution.** The tool never runs mod code, which is the point — but it
|
|
158
|
+
also means runtime-only behavior (e.g. payloads fetched at runtime) is out
|
|
159
|
+
of scope.
|
|
160
|
+
- **Snapshot trust.** `diff` is only as trustworthy as the snapshot: take it
|
|
161
|
+
from a clean install and store it where the mod updater cannot modify it.
|
|
162
|
+
- **TypeScript via regex, not a parser.** Keeps the tool stdlib-only and fast;
|
|
163
|
+
heavily minified or dynamically generated code may need manual review.
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mod-audit"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Static supply-chain auditor for Claude Code Mods — local, offline, stdlib-only"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "hao li"}]
|
|
13
|
+
keywords = ["claude-code", "mods", "supply-chain", "security", "static-analysis", "audit"]
|
|
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
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Security",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
mod-audit = "mod_audit.cli:main"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/hahahahahahahahah6/mod-audit"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""mod-audit: static supply-chain auditor for Claude Code Mods.
|
|
2
|
+
|
|
3
|
+
Local, offline, stdlib-only. Scans lifecycle hooks, TypeScript/JavaScript
|
|
4
|
+
source, env/API-key exfiltration patterns, and permissions manifests;
|
|
5
|
+
snapshots a trusted state and diffs installed mods against it to catch
|
|
6
|
+
trojanized updates.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
from .rules import Finding, scan_mod, scan_ts_source, scan_hook_commands, scan_permissions
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"__version__",
|
|
15
|
+
"Finding",
|
|
16
|
+
"scan_mod",
|
|
17
|
+
"scan_ts_source",
|
|
18
|
+
"scan_hook_commands",
|
|
19
|
+
"scan_permissions",
|
|
20
|
+
]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""mod-audit CLI: scan, snapshot, diff for Claude Code Mods."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import datetime as _dt
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .rules import (
|
|
12
|
+
SEVERITIES,
|
|
13
|
+
build_snapshot,
|
|
14
|
+
diff_against_snapshot,
|
|
15
|
+
scan_mod,
|
|
16
|
+
severity_at_least,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _format_text(findings) -> str:
|
|
21
|
+
if not findings:
|
|
22
|
+
return "mod-audit: no findings. Mod looks clean.\n"
|
|
23
|
+
lines = []
|
|
24
|
+
for f in findings:
|
|
25
|
+
loc = f"{f.file}:{f.line}" if f.line else f.file
|
|
26
|
+
lines.append(f"[{f.severity.upper():8}] {f.rule_id} {loc}")
|
|
27
|
+
lines.append(f" {f.message}")
|
|
28
|
+
lines.append(f" Fix: {f.fix}")
|
|
29
|
+
summary = {}
|
|
30
|
+
for f in findings:
|
|
31
|
+
summary[f.severity] = summary.get(f.severity, 0) + 1
|
|
32
|
+
lines.append("")
|
|
33
|
+
lines.append(
|
|
34
|
+
"Findings: %d (%s)"
|
|
35
|
+
% (len(findings), ", ".join(f"{k}={v}" for k, v in sorted(summary.items())))
|
|
36
|
+
)
|
|
37
|
+
return "\n".join(lines) + "\n"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _emit(findings, fmt: str) -> int:
|
|
41
|
+
if fmt == "json":
|
|
42
|
+
print(json.dumps([f.to_dict() for f in findings], indent=2))
|
|
43
|
+
else:
|
|
44
|
+
sys.stdout.write(_format_text(findings))
|
|
45
|
+
return 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def cmd_scan(args) -> int:
|
|
49
|
+
root = os.path.abspath(args.path)
|
|
50
|
+
if not os.path.isdir(root):
|
|
51
|
+
print(f"mod-audit: not a directory: {args.path}", file=sys.stderr)
|
|
52
|
+
return 2
|
|
53
|
+
findings = scan_mod(root)
|
|
54
|
+
_emit(findings, args.format)
|
|
55
|
+
if any(severity_at_least(f.severity, args.fail_on) for f in findings):
|
|
56
|
+
return 1
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def cmd_snapshot(args) -> int:
|
|
61
|
+
root = os.path.abspath(args.path)
|
|
62
|
+
if not os.path.isdir(root):
|
|
63
|
+
print(f"mod-audit: not a directory: {args.path}", file=sys.stderr)
|
|
64
|
+
return 2
|
|
65
|
+
manifest = build_snapshot(root)
|
|
66
|
+
manifest["generated_at"] = _dt.datetime.now(_dt.timezone.utc).isoformat()
|
|
67
|
+
manifest["root"] = root
|
|
68
|
+
|
|
69
|
+
out = args.out or os.path.join(
|
|
70
|
+
os.getcwd(), os.path.basename(root.rstrip(os.sep)) + ".snapshot"
|
|
71
|
+
)
|
|
72
|
+
os.makedirs(out, exist_ok=True)
|
|
73
|
+
path = os.path.join(out, "manifest.json")
|
|
74
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
75
|
+
json.dump(manifest, fh, indent=2, sort_keys=True)
|
|
76
|
+
print(f"mod-audit: snapshot written to {path}")
|
|
77
|
+
print(f" files: {len(manifest['files'])}, hooks: {len(manifest['hooks'])}")
|
|
78
|
+
print("Keep this snapshot somewhere the mod updater cannot modify.")
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def cmd_diff(args) -> int:
|
|
83
|
+
root = os.path.abspath(args.installed_dir)
|
|
84
|
+
snap_dir = os.path.abspath(args.against)
|
|
85
|
+
manifest_path = (
|
|
86
|
+
snap_dir
|
|
87
|
+
if snap_dir.endswith(".json")
|
|
88
|
+
else os.path.join(snap_dir, "manifest.json")
|
|
89
|
+
)
|
|
90
|
+
if not os.path.isdir(root):
|
|
91
|
+
print(f"mod-audit: not a directory: {args.installed_dir}", file=sys.stderr)
|
|
92
|
+
return 2
|
|
93
|
+
try:
|
|
94
|
+
with open(manifest_path, "r", encoding="utf-8") as fh:
|
|
95
|
+
snapshot = json.load(fh)
|
|
96
|
+
except (OSError, ValueError) as exc:
|
|
97
|
+
print(f"mod-audit: cannot read snapshot {manifest_path}: {exc}", file=sys.stderr)
|
|
98
|
+
return 2
|
|
99
|
+
findings = diff_against_snapshot(root, snapshot)
|
|
100
|
+
_emit(findings, args.format)
|
|
101
|
+
if any(severity_at_least(f.severity, args.fail_on) for f in findings):
|
|
102
|
+
return 1
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
107
|
+
p = argparse.ArgumentParser(
|
|
108
|
+
prog="mod-audit",
|
|
109
|
+
description="Static supply-chain auditor for Claude Code Mods (local, offline, stdlib-only).",
|
|
110
|
+
)
|
|
111
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
112
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
113
|
+
|
|
114
|
+
s = sub.add_parser("scan", help="Audit a mod directory for supply-chain risks.")
|
|
115
|
+
s.add_argument("path", help="Path to the mod directory (e.g. ~/.claude/plugins/foo).")
|
|
116
|
+
s.add_argument("--format", choices=("text", "json"), default="text")
|
|
117
|
+
s.add_argument(
|
|
118
|
+
"--fail-on",
|
|
119
|
+
choices=SEVERITIES,
|
|
120
|
+
default="high",
|
|
121
|
+
help="Exit 1 if any finding meets this severity (default: high).",
|
|
122
|
+
)
|
|
123
|
+
s.set_defaults(func=cmd_scan)
|
|
124
|
+
|
|
125
|
+
s = sub.add_parser("snapshot", help="Save a trusted snapshot of a mod.")
|
|
126
|
+
s.add_argument("path", help="Path to the mod directory.")
|
|
127
|
+
s.add_argument("--out", help="Snapshot directory (default: ./<mod-name>.snapshot).")
|
|
128
|
+
s.set_defaults(func=cmd_snapshot)
|
|
129
|
+
|
|
130
|
+
s = sub.add_parser("diff", help="Diff an installed mod against a trusted snapshot.")
|
|
131
|
+
s.add_argument("installed_dir", help="Path to the installed mod directory.")
|
|
132
|
+
s.add_argument(
|
|
133
|
+
"--against", required=True, help="Snapshot directory or manifest.json from `snapshot`."
|
|
134
|
+
)
|
|
135
|
+
s.add_argument("--format", choices=("text", "json"), default="text")
|
|
136
|
+
s.add_argument("--fail-on", choices=SEVERITIES, default="high")
|
|
137
|
+
s.set_defaults(func=cmd_diff)
|
|
138
|
+
return p
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def main(argv=None) -> int:
|
|
142
|
+
parser = build_parser()
|
|
143
|
+
args = parser.parse_args(argv)
|
|
144
|
+
return args.func(args)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
raise SystemExit(main())
|