@elliotllliu/agentshield 0.1.0 → 0.2.1

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.
package/README.md CHANGED
@@ -1,34 +1,81 @@
1
1
  # šŸ›”ļø AgentShield
2
2
 
3
- Security scanner for AI agent skills, MCP servers, and plugins.
3
+ **Security scanner for AI agent skills, MCP servers, and plugins.**
4
4
 
5
- Catch data exfiltration, backdoors, privilege escalation, and supply chain vulnerabilities **before** they reach your agents.
5
+ [![npm](https://img.shields.io/npm/v/@elliotllliu/agentshield)](https://www.npmjs.com/package/@elliotllliu/agentshield)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
7
+
8
+ Catch data exfiltration, backdoors, privilege escalation, credential leaks, and supply chain vulnerabilities **before** they reach your AI agents.
9
+
10
+ > **We scanned the top ClawHub skill repos — the average security score was 47/100.** [Read the full report →](docs/clawhub-security-report.md)
11
+
12
+ ## Why AgentShield?
13
+
14
+ AI agents install and execute third-party skills, MCP servers, and plugins with minimal security review. A single malicious skill can:
15
+
16
+ - šŸ”‘ **Steal credentials** — SSH keys, AWS secrets, API tokens
17
+ - šŸ“¤ **Exfiltrate data** — read sensitive files and send them to external servers
18
+ - šŸ’€ **Open backdoors** — `eval()`, reverse shells, dynamic code execution
19
+ - ā›ļø **Mine crypto** — hijack compute for cryptocurrency mining
20
+ - šŸ•µļø **Bypass permissions** — claim "read-only" but execute shell commands
21
+
22
+ AgentShield catches these patterns with **16 security rules** in under 50ms.
6
23
 
7
24
  ## Quick Start
8
25
 
9
26
  ```bash
10
- npx agentshield scan ./my-skill/
27
+ npx @elliotllliu/agentshield scan ./my-skill/
11
28
  ```
12
29
 
13
- ## What It Detects
14
-
15
- | Rule | Severity | Description |
16
- |------|----------|-------------|
17
- | `data-exfil` | šŸ”“ Critical | Reads sensitive files (SSH keys, credentials) + sends HTTP requests |
18
- | `backdoor` | šŸ”“ Critical | `eval()`, `new Function()`, `child_process.exec()` with dynamic input |
19
- | `reverse-shell` | šŸ”“ Critical | Outbound socket connections piped to shell |
20
- | `crypto-mining` | šŸ”“ Critical | Mining pool connections, known miners (xmrig, coinhive) |
21
- | `credential-hardcode` | šŸ”“ Critical | Hardcoded AWS keys, GitHub PATs, Stripe keys, private keys |
22
- | `env-leak` | šŸ”“ Critical | `process.env` access + outbound HTTP (environment variable exfil) |
23
- | `obfuscation` | šŸ”“ Critical | base64 + eval combos, hex-encoded strings, `String.fromCharCode` |
24
- | `typosquatting` | šŸ”“ Critical | Suspicious npm package names (e.g. `1odash` instead of `lodash`) |
25
- | `hidden-files` | šŸ”“ Critical | `.env` files with secrets committed to repo |
26
- | `network-ssrf` | 🟔 Warning | User-controlled URLs in fetch, AWS metadata endpoint access |
27
- | `privilege` | 🟔 Warning | SKILL.md declares `read` but code calls `exec` |
28
- | `supply-chain` | 🟔 Warning | Known CVEs in npm dependencies (`npm audit`) |
29
- | `sensitive-read` | 🟔 Warning | Accesses `~/.ssh/id_rsa`, `~/.aws/credentials`, etc. |
30
- | `excessive-perms` | 🟔 Warning | Too many or dangerous permissions in SKILL.md |
31
- | `phone-home` | 🟔 Warning | Periodic timers + HTTP requests (beacon/heartbeat pattern) |
30
+ No installation required. Works with Node.js 18+.
31
+
32
+ ## What It Detects — 16 Security Rules
33
+
34
+ ### šŸ”“ Critical (auto-fail)
35
+
36
+ | Rule | Detects |
37
+ |------|---------|
38
+ | `data-exfil` | Reads sensitive files + sends HTTP requests (exfiltration pattern) |
39
+ | `backdoor` | `eval()`, `new Function()`, `child_process.exec()` with dynamic input |
40
+ | `reverse-shell` | Outbound socket connections piped to `/bin/sh` |
41
+ | `crypto-mining` | Mining pool connections, xmrig, coinhive patterns |
42
+ | `credential-hardcode` | Hardcoded AWS keys (`AKIA...`), GitHub PATs (`ghp_...`), Stripe keys |
43
+ | `env-leak` | `process.env` secrets + outbound HTTP (environment variable theft) |
44
+ | `obfuscation` | `eval(atob(...))`, hex strings, `String.fromCharCode` obfuscation |
45
+ | `typosquatting` | Suspicious npm names: `1odash` → `lodash`, `axois` → `axios` |
46
+ | `hidden-files` | `.env` files with `PASSWORD`, `SECRET`, `API_KEY` committed to repo |
47
+
48
+ ### 🟔 Warning (review recommended)
49
+
50
+ | Rule | Detects |
51
+ |------|---------|
52
+ | `network-ssrf` | User-controlled URLs in fetch, AWS metadata endpoint access |
53
+ | `privilege` | SKILL.md permissions vs actual code behavior mismatch |
54
+ | `supply-chain` | Known CVEs in npm dependencies (`npm audit`) |
55
+ | `sensitive-read` | Access to `~/.ssh/id_rsa`, `~/.aws/credentials`, `~/.kube/config` |
56
+ | `excessive-perms` | Too many or dangerous permissions in SKILL.md |
57
+ | `phone-home` | `setInterval` + HTTP requests (beacon/C2 heartbeat pattern) |
58
+ | `mcp-manifest` | MCP server: wildcard perms, undeclared capabilities, suspicious tool descriptions |
59
+
60
+ ## Real-World Results
61
+
62
+ We scanned the **top 9 ClawHub skill repositories** (700K+ combined installs):
63
+
64
+ | Repository | Installs | Score | Risk |
65
+ |------------|----------|-------|------|
66
+ | vercel-labs/agent-skills | 157K | šŸ”“ 0/100 | Critical — deploy scripts with `$(curl)` command substitution |
67
+ | obra/superpowers | 94K | šŸ”“ 0/100 | Critical — dynamic code execution in render scripts |
68
+ | coreyhaines31/marketingskills | 42K | šŸ”“ 0/100 | Critical — 122 critical findings (CRM credential patterns) |
69
+ | anthropics/skills | 36K | šŸ”“ 35/100 | Critical — template with exec() |
70
+ | expo/skills | 11K | šŸ”“ 5/100 | Critical — fetch script reads env vars |
71
+ | remotion-dev/skills | 140K | 🟔 80/100 | Moderate — minor warnings |
72
+ | google-labs-code/stitch-skills | 63K | āœ… 100/100 | Clean |
73
+ | supercent-io/skills-template | 106K | āœ… 100/100 | Clean |
74
+ | squirrelscan/skills | 34K | āœ… 100/100 | Clean |
75
+
76
+ **Average score: 47/100** — over half of popular skill repos have critical security findings.
77
+
78
+ [šŸ“Š Full security report →](docs/clawhub-security-report.md)
32
79
 
33
80
  ## Example Output
34
81
 
@@ -37,78 +84,172 @@ npx agentshield scan ./my-skill/
37
84
  šŸ“ Scanned: ./my-skill/ (3 files, 44 lines)
38
85
 
39
86
  šŸ”“ CRITICAL (3)
40
- ā”œā”€ index.ts:13 — [data-exfil] Reads sensitive data and sends HTTP request — possible exfiltration
87
+ ā”œā”€ index.ts:13 — [data-exfil] Reads sensitive data and sends HTTP request
41
88
  ā”œā”€ index.ts:20 — [backdoor] eval() with dynamic input
42
- └─ index.ts:25 — [backdoor] child_process.exec() — use execFile instead
89
+ └─ backdoor.sh:6 — [backdoor] shell eval with variable
43
90
 
44
91
  🟔 WARNING (2)
45
92
  ā”œā”€ index.ts:23 — [privilege] Code uses 'exec' but SKILL.md doesn't declare it
46
93
  └─ index.ts:6 — [sensitive-read] Accesses SSH private key
47
94
 
48
- 🟢 INFO (1)
49
- └─ SKILL.md — [privilege] Detected capabilities: exec, read, web_fetch
50
-
51
95
  āœ… Score: 0/100 (Critical Risk)
96
+ ā± 16ms
52
97
  ```
53
98
 
54
99
  ## Usage
55
100
 
56
101
  ```bash
57
102
  # Scan a directory
58
- agentshield scan ./path/to/skill/
103
+ npx @elliotllliu/agentshield scan ./path/to/skill/
104
+
105
+ # JSON output (for CI/CD pipelines)
106
+ npx @elliotllliu/agentshield scan ./skill/ --json
107
+
108
+ # Fail CI if score drops below threshold
109
+ npx @elliotllliu/agentshield scan ./skill/ --fail-under 70
110
+
111
+ # Disable specific rules
112
+ npx @elliotllliu/agentshield scan ./skill/ --disable supply-chain,phone-home
59
113
 
60
- # JSON output (for CI/CD)
61
- agentshield scan ./skill/ --json
114
+ # Only run specific rules
115
+ npx @elliotllliu/agentshield scan ./skill/ --enable backdoor,data-exfil
62
116
 
63
- # Fail CI if score is below threshold
64
- agentshield scan ./skill/ --fail-under 70
117
+ # Generate config files
118
+ npx @elliotllliu/agentshield init
65
119
 
66
- # Shorthand (directory as first arg)
67
- agentshield ./skill/
120
+ # Watch mode — re-scan on file changes
121
+ npx @elliotllliu/agentshield watch ./skill/
122
+
123
+ # Compare two versions
124
+ npx @elliotllliu/agentshield compare ./skill-v1/ ./skill-v2/
125
+
126
+ # Generate a security badge for your README
127
+ npx @elliotllliu/agentshield badge ./skill/
68
128
  ```
69
129
 
70
130
  ## CI Integration
71
131
 
132
+ ### GitHub Action (recommended)
133
+
134
+ ```yaml
135
+ # .github/workflows/security.yml
136
+ name: Security Scan
137
+ on: [push, pull_request]
138
+ jobs:
139
+ scan:
140
+ runs-on: ubuntu-latest
141
+ steps:
142
+ - uses: actions/checkout@v4
143
+ - uses: elliotllliu/agentshield@main
144
+ with:
145
+ path: './skills/'
146
+ fail-under: '70'
147
+ ```
148
+
149
+ ### npx one-liner
150
+
72
151
  ```yaml
73
- # GitHub Actions
74
152
  - name: Security scan
75
- run: npx agentshield scan ./skills/ --fail-under 70
153
+ run: npx -y @elliotllliu/agentshield scan ./skills/ --fail-under 70
154
+ ```
155
+
156
+ ### Action Inputs & Outputs
157
+
158
+ | Input | Default | Description |
159
+ |-------|---------|-------------|
160
+ | `path` | `.` | Directory to scan |
161
+ | `fail-under` | — | Fail if score < threshold (0-100) |
162
+ | `format` | `terminal` | `terminal` or `json` |
163
+
164
+ | Output | Description |
165
+ |--------|-------------|
166
+ | `score` | Security score (0-100) |
167
+ | `findings` | Number of findings |
168
+
169
+ ## Configuration
170
+
171
+ Create `.agentshield.yml` (or run `agentshield init`):
172
+
173
+ ```yaml
174
+ rules:
175
+ disable:
176
+ - supply-chain # skip npm audit
177
+ - phone-home # allow periodic HTTP
178
+
179
+ severity:
180
+ sensitive-read: info # downgrade to info
181
+
182
+ failUnder: 70 # CI threshold
183
+
184
+ ignore:
185
+ - "tests/**"
186
+ - "*.test.ts"
187
+ ```
188
+
189
+ ### `.agentshieldignore`
190
+
191
+ ```
192
+ node_modules/
193
+ dist/
194
+ *.test.ts
195
+ __tests__/
76
196
  ```
77
197
 
78
198
  ## Scoring
79
199
 
80
- Starts at 100, deducts per finding:
200
+ | Severity | Points Deducted |
201
+ |----------|----------------|
202
+ | šŸ”“ Critical | -25 |
203
+ | 🟔 Warning | -10 |
204
+ | 🟢 Info | 0 |
205
+
206
+ | Score | Risk Level | Recommendation |
207
+ |-------|------------|----------------|
208
+ | 90-100 | āœ… Low Risk | Safe to install |
209
+ | 70-89 | 🟔 Moderate | Review warnings |
210
+ | 40-69 | 🟠 High Risk | Investigate before using |
211
+ | 0-39 | šŸ”“ Critical | Do not install |
212
+
213
+ ## Supported Platforms
214
+
215
+ - **AI Agent Skills** — OpenClaw, Codex, Claude Code
216
+ - **MCP Servers** — Model Context Protocol tool servers
217
+ - **npm Packages** — any npm package with executable code
218
+ - **General** — any directory with JS/TS/Python/Shell code
219
+
220
+ ### Supported File Types
81
221
 
82
- | Severity | Deduction |
222
+ | Language | Extensions |
83
223
  |----------|-----------|
84
- | Critical | -25 |
85
- | Warning | -10 |
86
- | Info | 0 |
87
-
88
- | Score | Risk Level |
89
- |-------|------------|
90
- | 90-100 | Low Risk āœ… |
91
- | 70-89 | Moderate Risk 🟔 |
92
- | 40-69 | High Risk 🟠 |
93
- | 0-39 | Critical Risk šŸ”“ |
94
-
95
- ## Supported File Types
96
-
97
- - **JavaScript/TypeScript**: `.js`, `.ts`, `.mjs`, `.cjs`, `.tsx`, `.jsx`
98
- - **Python**: `.py`
99
- - **Shell**: `.sh`, `.bash`, `.zsh`
100
- - **Config**: `.json`, `.yaml`, `.yml`, `.toml`
101
- - **Docs**: `SKILL.md` (permission analysis)
102
-
103
- ## Roadmap
104
-
105
- - [ ] AST-based analysis (tree-sitter for multi-language support)
106
- - [ ] MCP server manifest validation
107
- - [ ] Custom rule plugins
108
- - [ ] `agentshield init` — generate security policy
109
- - [ ] Sarif output for GitHub Code Scanning
110
- - [ ] Python `pip-audit` integration
111
- - [ ] Watch mode for development
224
+ | JavaScript/TypeScript | `.js`, `.ts`, `.mjs`, `.cjs`, `.tsx`, `.jsx` |
225
+ | Python | `.py` |
226
+ | Shell | `.sh`, `.bash`, `.zsh` |
227
+ | Config | `.json`, `.yaml`, `.yml`, `.toml` |
228
+ | Docs | `SKILL.md` (permission analysis) |
229
+
230
+ ## Comparison with Other Tools
231
+
232
+ | Feature | AgentShield | npm audit | Snyk | ESLint Security |
233
+ |---------|------------|-----------|------|-----------------|
234
+ | AI skill/MCP specific rules | āœ… | āŒ | āŒ | āŒ |
235
+ | Data exfiltration detection | āœ… | āŒ | āŒ | āŒ |
236
+ | Permission mismatch (SKILL.md) | āœ… | āŒ | āŒ | āŒ |
237
+ | Credential hardcode detection | āœ… | āŒ | āœ… | āœ… |
238
+ | Supply chain CVEs | āœ… | āœ… | āœ… | āŒ |
239
+ | Zero config | āœ… | āœ… | āŒ | āŒ |
240
+ | No API key required | āœ… | āœ… | āŒ | āœ… |
241
+ | < 50ms scan time | āœ… | āŒ | āŒ | āŒ |
242
+
243
+ ## Contributing
244
+
245
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for how to add new rules.
246
+
247
+ ## Links
248
+
249
+ - šŸ“¦ [npm](https://www.npmjs.com/package/@elliotllliu/agentshield)
250
+ - šŸ“– [Rule Documentation](docs/rules.md)
251
+ - šŸ“Š [ClawHub Security Report](docs/clawhub-security-report.md)
252
+ - šŸ‡ØšŸ‡³ [äø­ę–‡ README](README.zh-CN.md)
112
253
 
113
254
  ## License
114
255
 
@@ -0,0 +1,129 @@
1
+ # šŸ›”ļø AgentShield — AI Agent å®‰å…Øę‰«ęå™Ø
2
+
3
+ [![npm](https://img.shields.io/npm/v/@elliotllliu/agentshield)](https://www.npmjs.com/package/@elliotllliu/agentshield)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
5
+
6
+ äø“äøŗ AI Agent ęŠ€čƒ½ć€MCP Serverć€ę’ä»¶č®¾č®”ēš„å®‰å…Øę‰«ęå·„å…·ć€‚åœØå®‰č£…ē¬¬äø‰ę–¹ę‰©å±•ä¹‹å‰ļ¼Œę£€ęµ‹ę•°ę®ēŖƒå–ć€åŽé—Øć€å‡­čÆę³„éœ²å’Œä¾›åŗ”é“¾ę¼ę“žć€‚
7
+
8
+ > **ęˆ‘ä»¬ę‰«ęäŗ† ClawHub ēƒ­é—Ø skill 仓库 — å¹³å‡å®‰å…Øåˆ†ä»… 47/100怂** [ęŸ„ēœ‹å®Œę•“ęŠ„å‘Š →](docs/clawhub-security-report.md)
9
+
10
+ ## äøŗä»€ä¹ˆéœ€č¦ AgentShield?
11
+
12
+ AI Agent ä¼šå®‰č£…å¹¶ę‰§č”Œē¬¬äø‰ę–¹ęŠ€čƒ½å’Œę’ä»¶ļ¼Œå®‰å…Øå®”ęŸ„å‡ ä¹Žäøŗé›¶ć€‚äø€äøŖę¶ę„ skill 就能:
13
+
14
+ - šŸ”‘ **偷凭证** — SSH 密钄、AWS Secret态API Token
15
+ - šŸ“¤ **å¤–ę³„ę•°ę®** — čÆ»å–ę•ę„Ÿę–‡ä»¶å‘é€åˆ°å¤–éƒØęœåŠ”å™Ø
16
+ - šŸ’€ **ę¤å…„åŽé—Ø** — eval()ć€åå¼¹ Shellć€åŠØę€ä»£ē ę‰§č”Œ
17
+ - ā›ļø **ęŒ–ēŸæ** — åˆ©ē”Øä½ ēš„ē®—åŠ›ęŒ–åŠ åÆ†č“§åø
18
+ - šŸ•µļø **č¶Šęƒ** — å£°ē§°åŖčÆ»ä½†å®žé™…ę‰§č”Œ Shell 命令
19
+
20
+ AgentShield 用 **16 ę”å®‰å…Øč§„åˆ™**在 50ms å†…ę£€å‡ŗčæ™äŗ›åØčƒć€‚
21
+
22
+ ## åæ«é€Ÿå¼€å§‹
23
+
24
+ ```bash
25
+ npx @elliotllliu/agentshield scan ./my-skill/
26
+ ```
27
+
28
+ ę— éœ€å®‰č£…ļ¼ŒNode.js 18+ å³åÆčæč”Œć€‚
29
+
30
+ ## 16 ę”å®‰å…Øč§„åˆ™
31
+
32
+ ### šŸ”“ äø„é‡ļ¼ˆč‡ŖåŠØåˆ¤å®šäøå®‰å…Øļ¼‰
33
+
34
+ | č§„åˆ™ | 检测内容 |
35
+ |------|----------|
36
+ | `data-exfil` | čÆ»ę•ę„Ÿę–‡ä»¶ + 发 HTTP čÆ·ę±‚ļ¼ˆę•°ę®å¤–ę³„ęØ”å¼ļ¼‰ |
37
+ | `backdoor` | `eval()`态`exec()`ć€åŠØę€ä»£ē ę‰§č”Œ |
38
+ | `reverse-shell` | Socket å¤–čæž + Shell 箔道 |
39
+ | `crypto-mining` | ēŸæę± čæžęŽ„ć€xmrig态coinhive |
40
+ | `credential-hardcode` | 甬编码 AWS Key态GitHub PAT态Stripe Key |
41
+ | `env-leak` | ēŽÆå¢ƒå˜é‡ + HTTP 外发 |
42
+ | `obfuscation` | base64+evalć€åå…­čæ›åˆ¶ę··ę·† |
43
+ | `typosquatting` | npm åŒ…åä»æå†’ļ¼ˆ`1odash` → `lodash`) |
44
+ | `hidden-files` | `.env` ę˜Žę–‡åÆ†é’„ |
45
+
46
+ ### 🟔 č­¦å‘Šļ¼ˆå»ŗč®®å®”ęŸ„ļ¼‰
47
+
48
+ | č§„åˆ™ | 检测内容 |
49
+ |------|----------|
50
+ | `network-ssrf` | ē”Øęˆ·åÆęŽ§ URL态SSRF |
51
+ | `privilege` | SKILL.md å£°ę˜Ž vs ä»£ē å®žé™…č”ŒäøŗäøåŒ¹é… |
52
+ | `supply-chain` | npm ä¾čµ–å·²ēŸ„ CVE |
53
+ | `sensitive-read` | čÆ»å– SSH 密钄、AWS 凭证 |
54
+ | `excessive-perms` | ęƒé™å£°ę˜Žčæ‡å¤š |
55
+ | `phone-home` | å®šę—¶å™Ø + HTTP 心跳 |
56
+ | `mcp-manifest` | MCP Server é€šé…ęƒé™ć€åÆē–‘å·„å…·ęčæ° |
57
+
58
+ ## ēœŸå®žę‰«ęę•°ę®
59
+
60
+ ęˆ‘ä»¬ę‰«äŗ† ClawHub **Top 9 ēƒ­é—Ø skill 仓库**ļ¼ˆę€»å®‰č£…é‡ 70 äø‡+ļ¼‰ļ¼š
61
+
62
+ | 仓库 | å®‰č£…é‡ | åˆ†ę•° | 风险 |
63
+ |------|--------|------|------|
64
+ | vercel-labs/agent-skills | 157K | šŸ”“ 0/100 | deploy č„šęœ¬ęœ‰ `$(curl)` å‘½ä»¤ę›æę¢ |
65
+ | obra/superpowers | 94K | šŸ”“ 0/100 | ęø²ęŸ“č„šęœ¬ęœ‰åŠØę€ä»£ē ę‰§č”Œ |
66
+ | coreyhaines31/marketingskills | 42K | šŸ”“ 0/100 | 122 äøŖ critical(CRM å‡­čÆęØ”å¼ļ¼‰ |
67
+ | anthropics/skills | 36K | šŸ”“ 35/100 | ęØ”ęæęœ‰ exec() |
68
+ | google-labs-code/stitch-skills | 63K | āœ… 100/100 | 干净 |
69
+ | supercent-io/skills-template | 106K | āœ… 100/100 | 干净 |
70
+
71
+ **å¹³å‡åˆ†ļ¼š47/100** — č¶…åŠę•°ēƒ­é—Ø skill ęœ‰äø„é‡å®‰å…Øéšę‚£ć€‚
72
+
73
+ ## 使用方法
74
+
75
+ ```bash
76
+ # ę‰«ęē›®å½•
77
+ npx @elliotllliu/agentshield scan ./skill/
78
+
79
+ # JSON 输出
80
+ npx @elliotllliu/agentshield scan ./skill/ --json
81
+
82
+ # CI 门禁
83
+ npx @elliotllliu/agentshield scan ./skill/ --fail-under 70
84
+
85
+ # ē¦ē”Øē‰¹å®šč§„åˆ™
86
+ npx @elliotllliu/agentshield scan ./skill/ --disable supply-chain
87
+
88
+ # åˆå§‹åŒ–é…ē½®
89
+ npx @elliotllliu/agentshield init
90
+
91
+ # å®žę—¶ē›‘ęŽ§
92
+ npx @elliotllliu/agentshield watch ./skill/
93
+
94
+ # ē‰ˆęœ¬åÆ¹ęÆ”
95
+ npx @elliotllliu/agentshield compare ./v1/ ./v2/
96
+
97
+ # ē”Ÿęˆå®‰å…Øå¾½ē« 
98
+ npx @elliotllliu/agentshield badge ./skill/
99
+ ```
100
+
101
+ ## GitHub Actions 集ꈐ
102
+
103
+ ```yaml
104
+ - uses: elliotllliu/agentshield@main
105
+ with:
106
+ path: './skills/'
107
+ fail-under: '70'
108
+ ```
109
+
110
+ ## äøŽå…¶ä»–å·„å…·åÆ¹ęÆ”
111
+
112
+ | 功能 | AgentShield | npm audit | Snyk | ESLint |
113
+ |------|------------|-----------|------|--------|
114
+ | AI Skill/MCP äø“ē”Øč§„åˆ™ | āœ… | āŒ | āŒ | āŒ |
115
+ | ę•°ę®å¤–ę³„ę£€ęµ‹ | āœ… | āŒ | āŒ | āŒ |
116
+ | ęƒé™äøåŒ¹é…ę£€ęµ‹ | āœ… | āŒ | āŒ | āŒ |
117
+ | é›¶é…ē½® | āœ… | āœ… | āŒ | āŒ |
118
+ | < 50ms ę‰«ę | āœ… | āŒ | āŒ | āŒ |
119
+
120
+ ## é“¾ęŽ„
121
+
122
+ - šŸ“¦ [npm](https://www.npmjs.com/package/@elliotllliu/agentshield)
123
+ - šŸ“– [č§„åˆ™ę–‡ę”£](docs/rules.md)
124
+ - šŸ“Š [ClawHub å®‰å…ØęŠ„å‘Š](docs/clawhub-security-report.md)
125
+ - šŸ‡¬šŸ‡§ [English README](README.md)
126
+
127
+ ## č®øåÆčÆ
128
+
129
+ MIT
package/dist/cli.js CHANGED
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
- import { resolve } from "path";
4
- import { existsSync, statSync } from "fs";
3
+ import { resolve, join } from "path";
4
+ import { existsSync, statSync, writeFileSync, watch as fsWatch, mkdirSync } from "fs";
5
5
  import { scan } from "./scanner/index.js";
6
6
  import { printReport } from "./reporter/terminal.js";
7
7
  import { printJsonReport } from "./reporter/json.js";
8
+ import { generateBadgeSvg, generateBadgeMarkdown } from "./reporter/badge.js";
9
+ import { DEFAULT_CONFIG, DEFAULT_IGNORE } from "./config.js";
8
10
  const program = new Command();
9
11
  program
10
12
  .name("agentshield")
@@ -16,27 +18,195 @@ program
16
18
  .argument("<directory>", "Target directory to scan")
17
19
  .option("--json", "Output results as JSON")
18
20
  .option("--fail-under <score>", "Exit with code 1 if score is below threshold", parseInt)
21
+ .option("--disable <rules>", "Comma-separated rules to disable")
22
+ .option("--enable <rules>", "Comma-separated rules to enable (only these)")
19
23
  .action((directory, options) => {
20
24
  const target = resolve(directory);
21
25
  if (!existsSync(target) || !statSync(target).isDirectory()) {
22
26
  console.error(`Error: "${directory}" is not a valid directory`);
23
27
  process.exit(1);
24
28
  }
25
- const result = scan(target);
29
+ const configOverride = {};
30
+ if (options.disable || options.enable) {
31
+ configOverride.rules = {};
32
+ if (options.disable) {
33
+ configOverride.rules.disable = options.disable.split(",").map((s) => s.trim());
34
+ }
35
+ if (options.enable) {
36
+ configOverride.rules.enable = options.enable.split(",").map((s) => s.trim());
37
+ }
38
+ }
39
+ const result = scan(target, configOverride);
26
40
  if (options.json) {
27
41
  printJsonReport(result);
28
42
  }
29
43
  else {
30
44
  printReport(result);
31
45
  }
46
+ const threshold = options.failUnder ?? result.score;
32
47
  if (options.failUnder !== undefined && result.score < options.failUnder) {
33
48
  process.exit(1);
34
49
  }
35
50
  });
51
+ program
52
+ .command("init")
53
+ .description("Generate .agentshield.yml and .agentshieldignore config files")
54
+ .argument("[directory]", "Target directory", ".")
55
+ .action((directory) => {
56
+ const target = resolve(directory);
57
+ if (!existsSync(target)) {
58
+ mkdirSync(target, { recursive: true });
59
+ }
60
+ const configPath = join(target, ".agentshield.yml");
61
+ const ignorePath = join(target, ".agentshieldignore");
62
+ if (existsSync(configPath)) {
63
+ console.log(`āš ļø ${configPath} already exists, skipping`);
64
+ }
65
+ else {
66
+ writeFileSync(configPath, DEFAULT_CONFIG);
67
+ console.log(`āœ… Created ${configPath}`);
68
+ }
69
+ if (existsSync(ignorePath)) {
70
+ console.log(`āš ļø ${ignorePath} already exists, skipping`);
71
+ }
72
+ else {
73
+ writeFileSync(ignorePath, DEFAULT_IGNORE);
74
+ console.log(`āœ… Created ${ignorePath}`);
75
+ }
76
+ });
77
+ program
78
+ .command("watch")
79
+ .description("Watch a directory and re-scan on file changes")
80
+ .argument("<directory>", "Target directory to watch")
81
+ .option("--json", "Output results as JSON")
82
+ .action((directory, options) => {
83
+ const target = resolve(directory);
84
+ if (!existsSync(target) || !statSync(target).isDirectory()) {
85
+ console.error(`Error: "${directory}" is not a valid directory`);
86
+ process.exit(1);
87
+ }
88
+ console.log(`šŸ‘€ Watching ${target} for changes... (Ctrl+C to stop)\n`);
89
+ const runScan = () => {
90
+ console.clear();
91
+ console.log(`šŸ‘€ Watching ${target} — last scan: ${new Date().toLocaleTimeString()}\n`);
92
+ const result = scan(target);
93
+ if (options.json) {
94
+ printJsonReport(result);
95
+ }
96
+ else {
97
+ printReport(result);
98
+ }
99
+ };
100
+ // Initial scan
101
+ runScan();
102
+ // Watch for changes
103
+ try {
104
+ const watcher = fsWatch(target, { recursive: true }, () => {
105
+ runScan();
106
+ });
107
+ process.on("SIGINT", () => {
108
+ watcher.close();
109
+ process.exit(0);
110
+ });
111
+ }
112
+ catch {
113
+ console.error("āš ļø fs.watch recursive not supported on this platform. Use: nodemon --exec 'agentshield scan .'");
114
+ }
115
+ });
116
+ program
117
+ .command("compare")
118
+ .description("Compare security scores between two directories or git refs")
119
+ .argument("<dirA>", "First directory")
120
+ .argument("<dirB>", "Second directory")
121
+ .option("--json", "Output as JSON")
122
+ .action((dirA, dirB, options) => {
123
+ const targetA = resolve(dirA);
124
+ const targetB = resolve(dirB);
125
+ for (const [label, dir] of [["A", targetA], ["B", targetB]]) {
126
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) {
127
+ console.error(`Error: directory ${label} "${dir}" is not valid`);
128
+ process.exit(1);
129
+ }
130
+ }
131
+ const resultA = scan(targetA);
132
+ const resultB = scan(targetB);
133
+ if (options.json) {
134
+ console.log(JSON.stringify({
135
+ before: { target: resultA.target, score: resultA.score, findings: resultA.findings.length },
136
+ after: { target: resultB.target, score: resultB.score, findings: resultB.findings.length },
137
+ delta: resultB.score - resultA.score,
138
+ }, null, 2));
139
+ return;
140
+ }
141
+ console.log("\nšŸ”„ AgentShield Comparison\n");
142
+ console.log(` A: ${dirA} — Score: ${resultA.score}/100 (${resultA.findings.length} findings)`);
143
+ console.log(` B: ${dirB} — Score: ${resultB.score}/100 (${resultB.findings.length} findings)`);
144
+ console.log();
145
+ const delta = resultB.score - resultA.score;
146
+ if (delta > 0) {
147
+ console.log(` āœ… Improved by ${delta} points`);
148
+ }
149
+ else if (delta < 0) {
150
+ console.log(` šŸ”“ Degraded by ${Math.abs(delta)} points`);
151
+ }
152
+ else {
153
+ console.log(` āž”ļø No change`);
154
+ }
155
+ // Show new findings in B that aren't in A
156
+ const aKeys = new Set(resultA.findings.map((f) => `${f.rule}:${f.file}:${f.line}`));
157
+ const newFindings = resultB.findings.filter((f) => !aKeys.has(`${f.rule}:${f.file}:${f.line}`));
158
+ const fixedFindings = resultA.findings.filter((f) => {
159
+ const bKeys = new Set(resultB.findings.map((bf) => `${bf.rule}:${bf.file}:${bf.line}`));
160
+ return !bKeys.has(`${f.rule}:${f.file}:${f.line}`);
161
+ });
162
+ if (newFindings.length > 0) {
163
+ console.log(`\n šŸ†• New findings (${newFindings.length}):`);
164
+ for (const f of newFindings.slice(0, 10)) {
165
+ console.log(` ${f.file}${f.line ? `:${f.line}` : ""} — [${f.rule}] ${f.message}`);
166
+ }
167
+ }
168
+ if (fixedFindings.length > 0) {
169
+ console.log(`\n āœ… Fixed (${fixedFindings.length}):`);
170
+ for (const f of fixedFindings.slice(0, 10)) {
171
+ console.log(` ${f.file}${f.line ? `:${f.line}` : ""} — [${f.rule}] ${f.message}`);
172
+ }
173
+ }
174
+ console.log();
175
+ });
176
+ program
177
+ .command("badge")
178
+ .description("Generate a security badge for your project")
179
+ .argument("<directory>", "Target directory to scan")
180
+ .option("--svg", "Output raw SVG")
181
+ .option("--markdown", "Output markdown badge (default)")
182
+ .option("-o, --output <file>", "Save SVG to file")
183
+ .action((directory, options) => {
184
+ const target = resolve(directory);
185
+ if (!existsSync(target) || !statSync(target).isDirectory()) {
186
+ console.error(`Error: "${directory}" is not a valid directory`);
187
+ process.exit(1);
188
+ }
189
+ const result = scan(target);
190
+ if (options.svg || options.output) {
191
+ const svg = generateBadgeSvg(result);
192
+ if (options.output) {
193
+ writeFileSync(resolve(options.output), svg);
194
+ console.log(`āœ… Badge saved to ${options.output}`);
195
+ }
196
+ else {
197
+ console.log(svg);
198
+ }
199
+ }
200
+ else {
201
+ // Default: markdown
202
+ const md = generateBadgeMarkdown(result.score);
203
+ console.log(md);
204
+ console.log(`\nPaste this in your README.md to show the badge.`);
205
+ }
206
+ });
36
207
  // Default: if first arg looks like a directory, treat as scan
37
208
  const args = process.argv.slice(2);
38
- if (args.length > 0 && !args[0].startsWith("-") && args[0] !== "scan" && args[0] !== "help") {
39
- // Rewrite: `agentshield ./dir` → `agentshield scan ./dir`
209
+ if (args.length > 0 && !args[0].startsWith("-") && !["scan", "init", "watch", "compare", "badge", "help"].includes(args[0])) {
40
210
  process.argv.splice(2, 0, "scan");
41
211
  }
42
212
  program.parse();