clawvet 0.1.0
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/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +802 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ClawGuard Contributors
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# clawvet
|
|
2
|
+
|
|
3
|
+
**Skill vetting & supply chain security for OpenClaw.**
|
|
4
|
+
|
|
5
|
+
ClawVet scans OpenClaw `SKILL.md` files for prompt injection, credential theft, remote code execution, typosquatting, and social engineering — before they reach your agent.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g clawvet
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Scan a local skill
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
clawvet scan ./my-skill/
|
|
19
|
+
clawvet scan ./my-skill/SKILL.md
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### JSON output (for CI/CD)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
clawvet scan ./my-skill/ --format json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Fail on severity threshold
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
clawvet scan ./my-skill/ --fail-on high
|
|
32
|
+
# exits 1 if any high or critical findings
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Fetch and scan from ClawHub
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
clawvet scan weather-forecast --remote
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Audit all installed skills
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
clawvet audit
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Watch for new skill installs
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
clawvet watch --threshold 50
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## What it detects
|
|
54
|
+
|
|
55
|
+
ClawVet runs a 6-pass analysis on every skill:
|
|
56
|
+
|
|
57
|
+
| Pass | What it checks |
|
|
58
|
+
|------|---------------|
|
|
59
|
+
| **Skill Parser** | Extracts YAML frontmatter, code blocks, URLs, IPs, domains |
|
|
60
|
+
| **Static Analysis** | 21 regex patterns: curl-pipe-bash, credential access, C2 IPs, base64 payloads |
|
|
61
|
+
| **Metadata Validator** | Undeclared binaries, env vars, missing descriptions, invalid semver |
|
|
62
|
+
| **Dependency Checker** | `npx -y` auto-install, global `npm install`, risky packages |
|
|
63
|
+
| **Typosquat Detector** | Levenshtein distance against popular skills, suspicious naming patterns |
|
|
64
|
+
| **Semantic Analysis** | AI-powered detection of social engineering & prompt injection (optional) |
|
|
65
|
+
|
|
66
|
+
## Risk Scoring
|
|
67
|
+
|
|
68
|
+
| Score | Grade | Action |
|
|
69
|
+
|-------|-------|--------|
|
|
70
|
+
| 0-10 | A | Approve |
|
|
71
|
+
| 11-25 | B | Approve |
|
|
72
|
+
| 26-50 | C | Warn |
|
|
73
|
+
| 51-75 | D | Warn |
|
|
74
|
+
| 76-100 | F | Block |
|
|
75
|
+
|
|
76
|
+
## CI/CD Integration
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
# GitHub Actions example
|
|
80
|
+
- name: Vet skill
|
|
81
|
+
run: npx clawvet scan ./my-skill --format json --fail-on high
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/scan.ts
|
|
7
|
+
import { readFileSync, existsSync } from "fs";
|
|
8
|
+
import { resolve, join } from "path";
|
|
9
|
+
|
|
10
|
+
// ../shared/src/patterns.ts
|
|
11
|
+
var THREAT_PATTERNS = [
|
|
12
|
+
// CRITICAL: Remote code execution
|
|
13
|
+
{
|
|
14
|
+
name: "CURL_PIPE_BASH",
|
|
15
|
+
pattern: /curl\s+.*\|\s*(ba)?sh/gi,
|
|
16
|
+
severity: "critical",
|
|
17
|
+
category: "remote_code_execution",
|
|
18
|
+
title: "Curl piped to shell",
|
|
19
|
+
description: "Downloads and executes remote code directly \u2014 classic supply chain attack vector."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "WGET_EXECUTE",
|
|
23
|
+
pattern: /wget\s+.*&&\s*(ba)?sh/gi,
|
|
24
|
+
severity: "critical",
|
|
25
|
+
category: "remote_code_execution",
|
|
26
|
+
title: "Wget with shell execution",
|
|
27
|
+
description: "Downloads and executes remote code via wget."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "EVAL_DYNAMIC",
|
|
31
|
+
pattern: /eval\s*\(/gi,
|
|
32
|
+
severity: "critical",
|
|
33
|
+
category: "remote_code_execution",
|
|
34
|
+
title: "Dynamic eval() usage",
|
|
35
|
+
description: "Uses eval() which can execute arbitrary code."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "BASE64_DECODE",
|
|
39
|
+
pattern: /base64\s+(-d|--decode)/gi,
|
|
40
|
+
severity: "critical",
|
|
41
|
+
category: "obfuscation",
|
|
42
|
+
title: "Base64 decode execution",
|
|
43
|
+
description: "Decodes base64 content, often used to hide malicious payloads."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "PYTHON_EXEC",
|
|
47
|
+
pattern: /python[3]?\s+-c/gi,
|
|
48
|
+
severity: "critical",
|
|
49
|
+
category: "remote_code_execution",
|
|
50
|
+
title: "Python inline execution",
|
|
51
|
+
description: "Executes inline Python code which may contain hidden payloads."
|
|
52
|
+
},
|
|
53
|
+
// HIGH: Credential theft
|
|
54
|
+
{
|
|
55
|
+
name: "ENV_FILE_READ",
|
|
56
|
+
pattern: /\.env|credentials|\.aws|\.ssh|keychain/gi,
|
|
57
|
+
severity: "high",
|
|
58
|
+
category: "credential_theft",
|
|
59
|
+
title: "Sensitive file access",
|
|
60
|
+
description: "Accesses credential files (.env, .aws, .ssh, keychain)."
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "API_KEY_EXFIL",
|
|
64
|
+
pattern: /(ANTHROPIC|OPENAI|SLACK|DISCORD|TELEGRAM).*(_KEY|_TOKEN|_SECRET)/gi,
|
|
65
|
+
severity: "high",
|
|
66
|
+
category: "credential_theft",
|
|
67
|
+
title: "API key reference",
|
|
68
|
+
description: "References specific API keys/tokens that could be exfiltrated."
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "DOTFILE_ACCESS",
|
|
72
|
+
pattern: /~\/\.(openclaw|clawdbot|moltbot)\//gi,
|
|
73
|
+
severity: "high",
|
|
74
|
+
category: "credential_theft",
|
|
75
|
+
title: "OpenClaw config access",
|
|
76
|
+
description: "Accesses OpenClaw/Clawdbot/Moltbot configuration directories."
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "SESSION_THEFT",
|
|
80
|
+
pattern: /sessions\/\*\.jsonl/gi,
|
|
81
|
+
severity: "high",
|
|
82
|
+
category: "credential_theft",
|
|
83
|
+
title: "Session data access",
|
|
84
|
+
description: "Accesses session transcript files which may contain sensitive data."
|
|
85
|
+
},
|
|
86
|
+
// HIGH: Network exfiltration
|
|
87
|
+
{
|
|
88
|
+
name: "WEBHOOK_SEND",
|
|
89
|
+
pattern: /webhook\.(site|url)|discord\.com\/api\/webhooks/gi,
|
|
90
|
+
severity: "high",
|
|
91
|
+
category: "data_exfiltration",
|
|
92
|
+
title: "Webhook data exfiltration",
|
|
93
|
+
description: "Sends data to webhook endpoints (common exfiltration method)."
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "BORE_TUNNEL",
|
|
97
|
+
pattern: /bore\.pub|ngrok|localtunnel/gi,
|
|
98
|
+
severity: "high",
|
|
99
|
+
category: "data_exfiltration",
|
|
100
|
+
title: "Tunnel service usage",
|
|
101
|
+
description: "Uses tunneling services to expose local services or exfiltrate data."
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "SUSPICIOUS_IP",
|
|
105
|
+
pattern: /\b(?:91\.92\.242\.\d+|45\.61\.\d+\.\d+)\b/g,
|
|
106
|
+
severity: "high",
|
|
107
|
+
category: "data_exfiltration",
|
|
108
|
+
title: "Known malicious IP",
|
|
109
|
+
description: "Contains IP addresses associated with known ClawHavoc C2 infrastructure."
|
|
110
|
+
},
|
|
111
|
+
// MEDIUM: Social engineering
|
|
112
|
+
{
|
|
113
|
+
name: "PREREQUISITE_INSTALL",
|
|
114
|
+
pattern: /prerequisite|install.*first|run.*before|required.*dependency/gi,
|
|
115
|
+
severity: "medium",
|
|
116
|
+
category: "social_engineering",
|
|
117
|
+
title: "Prerequisite install trick",
|
|
118
|
+
description: "Instructs users to install prerequisites \u2014 common social engineering tactic."
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "COPY_PASTE_COMMAND",
|
|
122
|
+
pattern: /copy.*paste.*terminal|run.*this.*command/gi,
|
|
123
|
+
severity: "medium",
|
|
124
|
+
category: "social_engineering",
|
|
125
|
+
title: "Copy-paste command instruction",
|
|
126
|
+
description: "Instructs users to copy-paste commands into their terminal."
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "FAKE_DEPENDENCY",
|
|
130
|
+
pattern: /openclaw-core|moltbot-runtime|clawdbot-helper/gi,
|
|
131
|
+
severity: "medium",
|
|
132
|
+
category: "social_engineering",
|
|
133
|
+
title: "Fake dependency reference",
|
|
134
|
+
description: "References fake packages that mimic official OpenClaw components."
|
|
135
|
+
},
|
|
136
|
+
// MEDIUM: Prompt injection
|
|
137
|
+
{
|
|
138
|
+
name: "IGNORE_INSTRUCTIONS",
|
|
139
|
+
pattern: /ignore\s+(all\s+)?previous\s+instructions/gi,
|
|
140
|
+
severity: "medium",
|
|
141
|
+
category: "prompt_injection",
|
|
142
|
+
title: "Prompt injection \u2014 ignore instructions",
|
|
143
|
+
description: "Attempts to override the AI agent's existing instructions."
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "SYSTEM_OVERRIDE",
|
|
147
|
+
pattern: /you\s+are\s+now|new\s+instructions|forget\s+everything/gi,
|
|
148
|
+
severity: "medium",
|
|
149
|
+
category: "prompt_injection",
|
|
150
|
+
title: "Prompt injection \u2014 system override",
|
|
151
|
+
description: "Attempts to redefine the AI agent's identity or instructions."
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "MEMORY_MANIPULATION",
|
|
155
|
+
pattern: /SOUL\.md|MEMORY\.md|AGENTS\.md/gi,
|
|
156
|
+
severity: "medium",
|
|
157
|
+
category: "persistence",
|
|
158
|
+
title: "Memory/personality file manipulation",
|
|
159
|
+
description: "References core personality or memory files, may attempt persistence."
|
|
160
|
+
},
|
|
161
|
+
// LOW: Suspicious but not necessarily malicious
|
|
162
|
+
{
|
|
163
|
+
name: "SHELL_EXEC",
|
|
164
|
+
pattern: /child_process|exec\(|spawn\(/gi,
|
|
165
|
+
severity: "low",
|
|
166
|
+
category: "code_execution",
|
|
167
|
+
title: "Shell execution API",
|
|
168
|
+
description: "Uses shell execution APIs \u2014 legitimate but worth noting."
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "NETWORK_REQUEST",
|
|
172
|
+
pattern: /fetch\(|axios|node-fetch|got\(/gi,
|
|
173
|
+
severity: "low",
|
|
174
|
+
category: "network",
|
|
175
|
+
title: "Network request API",
|
|
176
|
+
description: "Makes network requests \u2014 legitimate but worth reviewing targets."
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "FILE_WRITE",
|
|
180
|
+
pattern: /fs\.write|writeFileSync/gi,
|
|
181
|
+
severity: "low",
|
|
182
|
+
category: "file_system",
|
|
183
|
+
title: "File write operation",
|
|
184
|
+
description: "Writes to the filesystem \u2014 check what files are being modified."
|
|
185
|
+
}
|
|
186
|
+
];
|
|
187
|
+
var POPULAR_SKILLS = [
|
|
188
|
+
"todoist-cli",
|
|
189
|
+
"github-manager",
|
|
190
|
+
"slack-assistant",
|
|
191
|
+
"email-composer",
|
|
192
|
+
"calendar-sync",
|
|
193
|
+
"weather-forecast",
|
|
194
|
+
"news-reader",
|
|
195
|
+
"code-reviewer",
|
|
196
|
+
"docker-helper",
|
|
197
|
+
"aws-manager",
|
|
198
|
+
"notion-sync",
|
|
199
|
+
"jira-tracker",
|
|
200
|
+
"spotify-controller",
|
|
201
|
+
"home-assistant",
|
|
202
|
+
"file-organizer",
|
|
203
|
+
"pdf-reader",
|
|
204
|
+
"translate-text",
|
|
205
|
+
"image-generator",
|
|
206
|
+
"web-scraper",
|
|
207
|
+
"database-query",
|
|
208
|
+
"git-assistant",
|
|
209
|
+
"linux-admin",
|
|
210
|
+
"python-helper",
|
|
211
|
+
"react-builder",
|
|
212
|
+
"api-tester",
|
|
213
|
+
"markdown-editor",
|
|
214
|
+
"csv-analyzer",
|
|
215
|
+
"ssh-manager",
|
|
216
|
+
"cron-scheduler",
|
|
217
|
+
"log-analyzer"
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
// ../shared/src/scanner/skill-parser.ts
|
|
221
|
+
import { parse as parseYaml } from "yaml";
|
|
222
|
+
var FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
|
|
223
|
+
var CODE_BLOCK_RE = /```(\w*)\n([\s\S]*?)```/g;
|
|
224
|
+
var URL_RE = /https?:\/\/[^\s"'<>\])+]+/gi;
|
|
225
|
+
var IP_RE = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
|
|
226
|
+
var DOMAIN_RE = /\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}\b/gi;
|
|
227
|
+
function parseSkill(content) {
|
|
228
|
+
let frontmatter = {};
|
|
229
|
+
let body = content;
|
|
230
|
+
const fmMatch = content.match(FRONTMATTER_RE);
|
|
231
|
+
if (fmMatch) {
|
|
232
|
+
try {
|
|
233
|
+
frontmatter = parseYaml(fmMatch[1]);
|
|
234
|
+
} catch {
|
|
235
|
+
frontmatter = {};
|
|
236
|
+
}
|
|
237
|
+
body = content.slice(fmMatch[0].length).trim();
|
|
238
|
+
}
|
|
239
|
+
const codeBlocks = [];
|
|
240
|
+
let match;
|
|
241
|
+
const cbRe = new RegExp(CODE_BLOCK_RE.source, CODE_BLOCK_RE.flags);
|
|
242
|
+
while ((match = cbRe.exec(content)) !== null) {
|
|
243
|
+
const before = content.slice(0, match.index);
|
|
244
|
+
const lineStart = before.split("\n").length;
|
|
245
|
+
const blockLines = match[0].split("\n").length;
|
|
246
|
+
codeBlocks.push({
|
|
247
|
+
language: match[1] || "unknown",
|
|
248
|
+
content: match[2],
|
|
249
|
+
lineStart,
|
|
250
|
+
lineEnd: lineStart + blockLines - 1
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
const urls = [...new Set(content.match(URL_RE) || [])];
|
|
254
|
+
const ipAddresses = [...new Set(content.match(IP_RE) || [])];
|
|
255
|
+
const domains = [...new Set(content.match(DOMAIN_RE) || [])];
|
|
256
|
+
return {
|
|
257
|
+
frontmatter,
|
|
258
|
+
body,
|
|
259
|
+
codeBlocks,
|
|
260
|
+
urls,
|
|
261
|
+
ipAddresses,
|
|
262
|
+
domains,
|
|
263
|
+
rawContent: content
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ../shared/src/scanner/static-analysis.ts
|
|
268
|
+
function runStaticAnalysis(skill) {
|
|
269
|
+
const findings = [];
|
|
270
|
+
for (const threat of THREAT_PATTERNS) {
|
|
271
|
+
const re = new RegExp(threat.pattern.source, threat.pattern.flags);
|
|
272
|
+
let match;
|
|
273
|
+
while ((match = re.exec(skill.rawContent)) !== null) {
|
|
274
|
+
const before = skill.rawContent.slice(0, match.index);
|
|
275
|
+
const lineNumber = before.split("\n").length;
|
|
276
|
+
findings.push({
|
|
277
|
+
category: threat.category,
|
|
278
|
+
severity: threat.severity,
|
|
279
|
+
title: threat.title,
|
|
280
|
+
description: threat.description,
|
|
281
|
+
evidence: match[0],
|
|
282
|
+
lineNumber,
|
|
283
|
+
analysisPass: "static-analysis"
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return findings;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ../shared/src/scanner/metadata-validator.ts
|
|
291
|
+
var SEMVER_RE = /^\d+\.\d+\.\d+/;
|
|
292
|
+
var KNOWN_BINS = [
|
|
293
|
+
"curl",
|
|
294
|
+
"wget",
|
|
295
|
+
"git",
|
|
296
|
+
"python",
|
|
297
|
+
"python3",
|
|
298
|
+
"node",
|
|
299
|
+
"npm",
|
|
300
|
+
"npx",
|
|
301
|
+
"brew",
|
|
302
|
+
"apt",
|
|
303
|
+
"pip",
|
|
304
|
+
"docker",
|
|
305
|
+
"kubectl",
|
|
306
|
+
"ssh",
|
|
307
|
+
"scp",
|
|
308
|
+
"rsync",
|
|
309
|
+
"ffmpeg",
|
|
310
|
+
"jq",
|
|
311
|
+
"sed",
|
|
312
|
+
"awk",
|
|
313
|
+
"grep",
|
|
314
|
+
"find"
|
|
315
|
+
];
|
|
316
|
+
function validateMetadata(skill) {
|
|
317
|
+
const findings = [];
|
|
318
|
+
const fm = skill.frontmatter;
|
|
319
|
+
const pass = "metadata-validator";
|
|
320
|
+
if (!fm.name) {
|
|
321
|
+
findings.push({
|
|
322
|
+
category: "metadata",
|
|
323
|
+
severity: "medium",
|
|
324
|
+
title: "Missing skill name",
|
|
325
|
+
description: "SKILL.md frontmatter does not declare a name.",
|
|
326
|
+
analysisPass: pass
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (!fm.description) {
|
|
330
|
+
findings.push({
|
|
331
|
+
category: "metadata",
|
|
332
|
+
severity: "medium",
|
|
333
|
+
title: "Missing description",
|
|
334
|
+
description: "SKILL.md frontmatter does not declare a description.",
|
|
335
|
+
analysisPass: pass
|
|
336
|
+
});
|
|
337
|
+
} else if (fm.description.length < 10) {
|
|
338
|
+
findings.push({
|
|
339
|
+
category: "metadata",
|
|
340
|
+
severity: "low",
|
|
341
|
+
title: "Vague description",
|
|
342
|
+
description: "Skill description is suspiciously short.",
|
|
343
|
+
evidence: fm.description,
|
|
344
|
+
analysisPass: pass
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (fm.version && !SEMVER_RE.test(fm.version)) {
|
|
348
|
+
findings.push({
|
|
349
|
+
category: "metadata",
|
|
350
|
+
severity: "low",
|
|
351
|
+
title: "Invalid version format",
|
|
352
|
+
description: "Version does not follow semver format.",
|
|
353
|
+
evidence: fm.version,
|
|
354
|
+
analysisPass: pass
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
const declaredBins = new Set(fm.metadata?.openclaw?.requires?.bins || []);
|
|
358
|
+
for (const bin of KNOWN_BINS) {
|
|
359
|
+
const binRe = new RegExp(`\\b${bin}\\b`, "i");
|
|
360
|
+
if (binRe.test(skill.rawContent) && !declaredBins.has(bin)) {
|
|
361
|
+
const usedInCode = skill.codeBlocks.some((cb) => binRe.test(cb.content));
|
|
362
|
+
if (usedInCode) {
|
|
363
|
+
findings.push({
|
|
364
|
+
category: "metadata",
|
|
365
|
+
severity: "low",
|
|
366
|
+
title: `Undeclared binary: ${bin}`,
|
|
367
|
+
description: `Skill uses '${bin}' in code but does not declare it in requires.bins.`,
|
|
368
|
+
analysisPass: pass
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const declaredEnv = new Set(fm.metadata?.openclaw?.requires?.env || []);
|
|
374
|
+
const envRe = /\$\{?([A-Z][A-Z0-9_]+)\}?/g;
|
|
375
|
+
let match;
|
|
376
|
+
while ((match = envRe.exec(skill.rawContent)) !== null) {
|
|
377
|
+
const envVar = match[1];
|
|
378
|
+
if (!declaredEnv.has(envVar) && envVar.length > 2) {
|
|
379
|
+
findings.push({
|
|
380
|
+
category: "metadata",
|
|
381
|
+
severity: "low",
|
|
382
|
+
title: `Undeclared env var: ${envVar}`,
|
|
383
|
+
description: `References environment variable $${envVar} but does not declare it in requires.env.`,
|
|
384
|
+
evidence: match[0],
|
|
385
|
+
analysisPass: pass
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return findings;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ../shared/src/scanner/dependency-checker.ts
|
|
393
|
+
var NPX_AUTO_INSTALL_RE = /npx\s+-y\s+/gi;
|
|
394
|
+
var NPM_INSTALL_RE = /npm\s+install\s+(-g\s+)?(\S+)/gi;
|
|
395
|
+
function checkDependencies(skill) {
|
|
396
|
+
const findings = [];
|
|
397
|
+
const pass = "dependency-checker";
|
|
398
|
+
let match;
|
|
399
|
+
const npxRe = new RegExp(NPX_AUTO_INSTALL_RE.source, NPX_AUTO_INSTALL_RE.flags);
|
|
400
|
+
while ((match = npxRe.exec(skill.rawContent)) !== null) {
|
|
401
|
+
const before = skill.rawContent.slice(0, match.index);
|
|
402
|
+
const lineNumber = before.split("\n").length;
|
|
403
|
+
findings.push({
|
|
404
|
+
category: "dependency_risk",
|
|
405
|
+
severity: "medium",
|
|
406
|
+
title: "npx auto-install (-y flag)",
|
|
407
|
+
description: "Uses 'npx -y' which auto-installs packages without user confirmation.",
|
|
408
|
+
evidence: match[0],
|
|
409
|
+
lineNumber,
|
|
410
|
+
analysisPass: pass
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
const npmRe = new RegExp(NPM_INSTALL_RE.source, NPM_INSTALL_RE.flags);
|
|
414
|
+
while ((match = npmRe.exec(skill.rawContent)) !== null) {
|
|
415
|
+
if (match[1]) {
|
|
416
|
+
findings.push({
|
|
417
|
+
category: "dependency_risk",
|
|
418
|
+
severity: "medium",
|
|
419
|
+
title: "Global npm package install",
|
|
420
|
+
description: `Installs npm package globally: ${match[2]}`,
|
|
421
|
+
evidence: match[0],
|
|
422
|
+
analysisPass: pass
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return findings;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ../shared/src/scanner/typosquat-detector.ts
|
|
430
|
+
import { distance } from "fastest-levenshtein";
|
|
431
|
+
var MAX_EDIT_DISTANCE = 2;
|
|
432
|
+
function detectTyposquats(skillName) {
|
|
433
|
+
if (!skillName) return [];
|
|
434
|
+
const findings = [];
|
|
435
|
+
const normalized = skillName.toLowerCase().trim();
|
|
436
|
+
for (const popular of POPULAR_SKILLS) {
|
|
437
|
+
if (normalized === popular) continue;
|
|
438
|
+
const d = distance(normalized, popular);
|
|
439
|
+
if (d > 0 && d <= MAX_EDIT_DISTANCE) {
|
|
440
|
+
findings.push({
|
|
441
|
+
category: "typosquatting",
|
|
442
|
+
severity: "high",
|
|
443
|
+
title: `Possible typosquat of "${popular}"`,
|
|
444
|
+
description: `Skill name "${skillName}" is ${d} edit(s) away from popular skill "${popular}". This may be an attempt to impersonate a trusted skill.`,
|
|
445
|
+
evidence: `"${skillName}" \u2248 "${popular}" (distance: ${d})`,
|
|
446
|
+
analysisPass: "typosquat-detector"
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const patterns = [
|
|
451
|
+
{ re: /-+/, desc: "extra hyphens" },
|
|
452
|
+
{ re: /(.)\1{2,}/, desc: "repeated characters" }
|
|
453
|
+
];
|
|
454
|
+
for (const p of patterns) {
|
|
455
|
+
if (p.re.test(normalized) && !POPULAR_SKILLS.includes(normalized)) {
|
|
456
|
+
findings.push({
|
|
457
|
+
category: "typosquatting",
|
|
458
|
+
severity: "medium",
|
|
459
|
+
title: `Suspicious naming pattern: ${p.desc}`,
|
|
460
|
+
description: `Skill name "${skillName}" has ${p.desc}, which is a common typosquatting technique.`,
|
|
461
|
+
analysisPass: "typosquat-detector"
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return findings;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ../shared/src/scanner/risk-scorer.ts
|
|
469
|
+
var SEVERITY_WEIGHTS = {
|
|
470
|
+
critical: 30,
|
|
471
|
+
high: 15,
|
|
472
|
+
medium: 7,
|
|
473
|
+
low: 3
|
|
474
|
+
};
|
|
475
|
+
function calculateRiskScore(findings) {
|
|
476
|
+
let score = 0;
|
|
477
|
+
for (const f of findings) {
|
|
478
|
+
score += SEVERITY_WEIGHTS[f.severity];
|
|
479
|
+
}
|
|
480
|
+
return Math.min(score, 100);
|
|
481
|
+
}
|
|
482
|
+
function getRiskGrade(score) {
|
|
483
|
+
if (score <= 10) return "A";
|
|
484
|
+
if (score <= 25) return "B";
|
|
485
|
+
if (score <= 50) return "C";
|
|
486
|
+
if (score <= 75) return "D";
|
|
487
|
+
return "F";
|
|
488
|
+
}
|
|
489
|
+
function countFindings(findings) {
|
|
490
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
491
|
+
for (const f of findings) {
|
|
492
|
+
counts[f.severity]++;
|
|
493
|
+
}
|
|
494
|
+
return counts;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ../shared/src/scanner/index.ts
|
|
498
|
+
async function scanSkill(content, options = {}) {
|
|
499
|
+
const skill = parseSkill(content);
|
|
500
|
+
const allFindings = [];
|
|
501
|
+
allFindings.push(...runStaticAnalysis(skill));
|
|
502
|
+
allFindings.push(...validateMetadata(skill));
|
|
503
|
+
if (options.semantic && options.semanticAnalyzer) {
|
|
504
|
+
const semanticFindings = await options.semanticAnalyzer(content);
|
|
505
|
+
allFindings.push(...semanticFindings);
|
|
506
|
+
}
|
|
507
|
+
allFindings.push(...checkDependencies(skill));
|
|
508
|
+
if (skill.frontmatter.name) {
|
|
509
|
+
allFindings.push(...detectTyposquats(skill.frontmatter.name));
|
|
510
|
+
}
|
|
511
|
+
const riskScore = calculateRiskScore(allFindings);
|
|
512
|
+
const riskGrade = getRiskGrade(riskScore);
|
|
513
|
+
const findingsCount = countFindings(allFindings);
|
|
514
|
+
const recommendation = riskScore >= 76 ? "block" : riskScore >= 26 ? "warn" : "approve";
|
|
515
|
+
return {
|
|
516
|
+
skillName: skill.frontmatter.name || "unknown",
|
|
517
|
+
skillVersion: skill.frontmatter.version,
|
|
518
|
+
skillSource: "local",
|
|
519
|
+
status: "complete",
|
|
520
|
+
riskScore,
|
|
521
|
+
riskGrade,
|
|
522
|
+
findingsCount,
|
|
523
|
+
findings: allFindings,
|
|
524
|
+
recommendation
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/output/terminal.ts
|
|
529
|
+
import chalk from "chalk";
|
|
530
|
+
var SEVERITY_COLORS = {
|
|
531
|
+
critical: chalk.bgRed.white.bold,
|
|
532
|
+
high: chalk.red.bold,
|
|
533
|
+
medium: chalk.yellow,
|
|
534
|
+
low: chalk.blue
|
|
535
|
+
};
|
|
536
|
+
var GRADE_COLORS = {
|
|
537
|
+
A: chalk.green.bold,
|
|
538
|
+
B: chalk.greenBright,
|
|
539
|
+
C: chalk.yellow.bold,
|
|
540
|
+
D: chalk.redBright.bold,
|
|
541
|
+
F: chalk.bgRed.white.bold
|
|
542
|
+
};
|
|
543
|
+
function printScanResult(result) {
|
|
544
|
+
console.log();
|
|
545
|
+
console.log(chalk.bold("\u2501".repeat(60)));
|
|
546
|
+
console.log(chalk.bold(" ClawVet Scan Report"));
|
|
547
|
+
console.log(chalk.bold("\u2501".repeat(60)));
|
|
548
|
+
console.log();
|
|
549
|
+
console.log(` Skill: ${chalk.bold(result.skillName)}`);
|
|
550
|
+
if (result.skillVersion) {
|
|
551
|
+
console.log(` Version: ${result.skillVersion}`);
|
|
552
|
+
}
|
|
553
|
+
console.log();
|
|
554
|
+
const gradeColor = GRADE_COLORS[result.riskGrade] || chalk.white;
|
|
555
|
+
console.log(
|
|
556
|
+
` Risk Score: ${gradeColor(`${result.riskScore}/100`)} Grade: ${gradeColor(result.riskGrade)}`
|
|
557
|
+
);
|
|
558
|
+
console.log();
|
|
559
|
+
const fc = result.findingsCount;
|
|
560
|
+
console.log(" Findings:");
|
|
561
|
+
if (fc.critical)
|
|
562
|
+
console.log(
|
|
563
|
+
` ${SEVERITY_COLORS.critical(` CRITICAL `)} ${fc.critical}`
|
|
564
|
+
);
|
|
565
|
+
if (fc.high)
|
|
566
|
+
console.log(` ${SEVERITY_COLORS.high("HIGH")} ${fc.high}`);
|
|
567
|
+
if (fc.medium)
|
|
568
|
+
console.log(` ${SEVERITY_COLORS.medium("MEDIUM")} ${fc.medium}`);
|
|
569
|
+
if (fc.low) console.log(` ${SEVERITY_COLORS.low("LOW")} ${fc.low}`);
|
|
570
|
+
if (!fc.critical && !fc.high && !fc.medium && !fc.low) {
|
|
571
|
+
console.log(` ${chalk.green("No findings \u2014 skill looks clean!")}`);
|
|
572
|
+
}
|
|
573
|
+
console.log();
|
|
574
|
+
if (result.findings.length > 0) {
|
|
575
|
+
console.log(chalk.bold(" Details:"));
|
|
576
|
+
console.log();
|
|
577
|
+
for (const f of result.findings) {
|
|
578
|
+
const color = SEVERITY_COLORS[f.severity];
|
|
579
|
+
console.log(` ${color(`[${f.severity.toUpperCase()}]`)} ${f.title}`);
|
|
580
|
+
console.log(` ${chalk.dim(f.description)}`);
|
|
581
|
+
if (f.evidence) {
|
|
582
|
+
console.log(` Evidence: ${chalk.italic(f.evidence)}`);
|
|
583
|
+
}
|
|
584
|
+
if (f.lineNumber) {
|
|
585
|
+
console.log(` Line: ${f.lineNumber}`);
|
|
586
|
+
}
|
|
587
|
+
console.log();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const recColors = {
|
|
591
|
+
block: chalk.bgRed.white.bold,
|
|
592
|
+
warn: chalk.bgYellow.black.bold,
|
|
593
|
+
approve: chalk.bgGreen.black.bold
|
|
594
|
+
};
|
|
595
|
+
const rec = result.recommendation || "approve";
|
|
596
|
+
console.log(
|
|
597
|
+
` Recommendation: ${(recColors[rec] || chalk.white)(` ${rec.toUpperCase()} `)}`
|
|
598
|
+
);
|
|
599
|
+
console.log();
|
|
600
|
+
console.log(chalk.bold("\u2501".repeat(60)));
|
|
601
|
+
console.log();
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// src/output/json.ts
|
|
605
|
+
function printJsonResult(result) {
|
|
606
|
+
console.log(JSON.stringify(result, null, 2));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/commands/scan.ts
|
|
610
|
+
async function fetchRemoteSkill(slug) {
|
|
611
|
+
const urls = [
|
|
612
|
+
`https://raw.githubusercontent.com/openclaw/skills/main/${slug}/SKILL.md`,
|
|
613
|
+
`https://clawhub.com/api/v1/skills/${slug}/raw`
|
|
614
|
+
];
|
|
615
|
+
for (const url of urls) {
|
|
616
|
+
try {
|
|
617
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
618
|
+
if (res.ok) {
|
|
619
|
+
return await res.text();
|
|
620
|
+
}
|
|
621
|
+
} catch {
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
throw new Error(
|
|
625
|
+
`Could not fetch skill "${slug}" from ClawHub. Check the skill name and try again.`
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
async function scanCommand(target, options) {
|
|
629
|
+
let content;
|
|
630
|
+
if (options.remote) {
|
|
631
|
+
try {
|
|
632
|
+
process.stderr.write(`Fetching "${target}" from ClawHub...
|
|
633
|
+
`);
|
|
634
|
+
content = await fetchRemoteSkill(target);
|
|
635
|
+
} catch (err) {
|
|
636
|
+
console.error(
|
|
637
|
+
err instanceof Error ? err.message : "Failed to fetch remote skill"
|
|
638
|
+
);
|
|
639
|
+
process.exit(1);
|
|
640
|
+
}
|
|
641
|
+
} else {
|
|
642
|
+
const skillPath = resolve(target);
|
|
643
|
+
let skillFile = skillPath;
|
|
644
|
+
if (existsSync(skillPath) && !skillPath.endsWith(".md") && existsSync(join(skillPath, "SKILL.md"))) {
|
|
645
|
+
skillFile = join(skillPath, "SKILL.md");
|
|
646
|
+
}
|
|
647
|
+
if (!existsSync(skillFile)) {
|
|
648
|
+
console.error(`Error: Cannot find SKILL.md at ${skillFile}`);
|
|
649
|
+
process.exit(1);
|
|
650
|
+
}
|
|
651
|
+
content = readFileSync(skillFile, "utf-8");
|
|
652
|
+
}
|
|
653
|
+
const result = await scanSkill(content, { semantic: false });
|
|
654
|
+
if (options.format === "json") {
|
|
655
|
+
printJsonResult(result);
|
|
656
|
+
} else {
|
|
657
|
+
printScanResult(result);
|
|
658
|
+
}
|
|
659
|
+
if (options.failOn) {
|
|
660
|
+
const severityOrder = ["low", "medium", "high", "critical"];
|
|
661
|
+
const threshold = severityOrder.indexOf(options.failOn);
|
|
662
|
+
const hasFailure = result.findings.some(
|
|
663
|
+
(f) => severityOrder.indexOf(f.severity) >= threshold
|
|
664
|
+
);
|
|
665
|
+
if (hasFailure) {
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// src/commands/audit.ts
|
|
672
|
+
import { readdirSync, existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
673
|
+
import { join as join2 } from "path";
|
|
674
|
+
import { homedir } from "os";
|
|
675
|
+
import chalk2 from "chalk";
|
|
676
|
+
var SKILL_DIRS = [
|
|
677
|
+
join2(homedir(), ".openclaw", "skills"),
|
|
678
|
+
join2(homedir(), ".openclaw", "workspace", "skills")
|
|
679
|
+
];
|
|
680
|
+
async function auditCommand() {
|
|
681
|
+
console.log(chalk2.bold("\nClawVet Audit \u2014 Scanning all installed skills\n"));
|
|
682
|
+
let totalScanned = 0;
|
|
683
|
+
let totalThreats = 0;
|
|
684
|
+
for (const dir of SKILL_DIRS) {
|
|
685
|
+
if (!existsSync2(dir)) continue;
|
|
686
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
687
|
+
for (const entry of entries) {
|
|
688
|
+
if (!entry.isDirectory()) continue;
|
|
689
|
+
const skillFile = join2(dir, entry.name, "SKILL.md");
|
|
690
|
+
if (!existsSync2(skillFile)) continue;
|
|
691
|
+
const content = readFileSync2(skillFile, "utf-8");
|
|
692
|
+
const result = await scanSkill(content);
|
|
693
|
+
totalScanned++;
|
|
694
|
+
totalThreats += result.findings.length;
|
|
695
|
+
printScanResult(result);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
console.log(chalk2.bold(`
|
|
699
|
+
Audit complete: ${totalScanned} skills scanned, ${totalThreats} findings
|
|
700
|
+
`));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/commands/watch.ts
|
|
704
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3, watch } from "fs";
|
|
705
|
+
import { join as join3 } from "path";
|
|
706
|
+
import { homedir as homedir2 } from "os";
|
|
707
|
+
import chalk3 from "chalk";
|
|
708
|
+
var SKILL_DIRS2 = [
|
|
709
|
+
join3(homedir2(), ".openclaw", "skills"),
|
|
710
|
+
join3(homedir2(), ".openclaw", "workspace", "skills")
|
|
711
|
+
];
|
|
712
|
+
async function watchCommand(options) {
|
|
713
|
+
const threshold = options.threshold || 50;
|
|
714
|
+
console.log(
|
|
715
|
+
chalk3.bold(
|
|
716
|
+
`
|
|
717
|
+
ClawVet Watch \u2014 monitoring skill directories (threshold: ${threshold})
|
|
718
|
+
`
|
|
719
|
+
)
|
|
720
|
+
);
|
|
721
|
+
const watchDirs = [];
|
|
722
|
+
for (const dir of SKILL_DIRS2) {
|
|
723
|
+
if (existsSync3(dir)) {
|
|
724
|
+
watchDirs.push(dir);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (watchDirs.length === 0) {
|
|
728
|
+
console.log(
|
|
729
|
+
chalk3.yellow(
|
|
730
|
+
"No OpenClaw skill directories found. Watching will start when directories are created.\n"
|
|
731
|
+
)
|
|
732
|
+
);
|
|
733
|
+
console.log(chalk3.dim("Expected directories:"));
|
|
734
|
+
for (const dir of SKILL_DIRS2) {
|
|
735
|
+
console.log(chalk3.dim(` ${dir}`));
|
|
736
|
+
}
|
|
737
|
+
console.log();
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
console.log(chalk3.dim("Watching:"));
|
|
741
|
+
for (const dir of watchDirs) {
|
|
742
|
+
console.log(chalk3.dim(` ${dir}`));
|
|
743
|
+
}
|
|
744
|
+
console.log();
|
|
745
|
+
for (const dir of watchDirs) {
|
|
746
|
+
const watcher = watch(dir, { recursive: true }, async (event, filename) => {
|
|
747
|
+
if (!filename?.endsWith("SKILL.md")) return;
|
|
748
|
+
const skillFile = join3(dir, filename);
|
|
749
|
+
if (!existsSync3(skillFile)) return;
|
|
750
|
+
console.log(chalk3.dim(`
|
|
751
|
+
Detected change: ${filename}`));
|
|
752
|
+
try {
|
|
753
|
+
const content = readFileSync3(skillFile, "utf-8");
|
|
754
|
+
const result = await scanSkill(content);
|
|
755
|
+
printScanResult(result);
|
|
756
|
+
if (result.riskScore > threshold) {
|
|
757
|
+
console.log(
|
|
758
|
+
chalk3.bgRed.white.bold(
|
|
759
|
+
` BLOCKED \u2014 Risk score ${result.riskScore} exceeds threshold ${threshold} `
|
|
760
|
+
)
|
|
761
|
+
);
|
|
762
|
+
console.log(
|
|
763
|
+
chalk3.red(
|
|
764
|
+
`This skill should not be installed. Run 'clawvet scan ${skillFile}' for details.
|
|
765
|
+
`
|
|
766
|
+
)
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
} catch (err) {
|
|
770
|
+
console.error(chalk3.red(`Error scanning ${filename}:`), err);
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
process.on("SIGINT", () => {
|
|
774
|
+
watcher.close();
|
|
775
|
+
console.log(chalk3.dim("\nWatch stopped."));
|
|
776
|
+
process.exit(0);
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
console.log(chalk3.dim("Press Ctrl+C to stop watching.\n"));
|
|
780
|
+
await new Promise(() => {
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/index.ts
|
|
785
|
+
var program = new Command();
|
|
786
|
+
program.name("clawvet").description("Skill vetting & supply chain security for OpenClaw").version("0.1.0");
|
|
787
|
+
program.command("scan").description("Scan a skill for security threats").argument("<target>", "Path to skill folder or SKILL.md file").option("--format <format>", "Output format: terminal or json", "terminal").option("--fail-on <severity>", "Exit 1 if findings at this severity or above").option("--semantic", "Enable AI semantic analysis (requires ANTHROPIC_API_KEY)").option("--remote", "Fetch skill from ClawHub by name instead of local path").action(async (target, opts) => {
|
|
788
|
+
await scanCommand(target, {
|
|
789
|
+
format: opts.format,
|
|
790
|
+
failOn: opts.failOn,
|
|
791
|
+
semantic: opts.semantic,
|
|
792
|
+
remote: opts.remote
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
program.command("audit").description("Scan all installed OpenClaw skills").action(async () => {
|
|
796
|
+
await auditCommand();
|
|
797
|
+
});
|
|
798
|
+
program.command("watch").description("Pre-install hook \u2014 blocks risky skill installs").option("--threshold <score>", "Risk score threshold (default 50)", "50").action(async (opts) => {
|
|
799
|
+
await watchCommand({ threshold: parseInt(opts.threshold) });
|
|
800
|
+
});
|
|
801
|
+
program.parse();
|
|
802
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/commands/scan.ts","../../shared/src/patterns.ts","../../shared/src/scanner/skill-parser.ts","../../shared/src/scanner/static-analysis.ts","../../shared/src/scanner/metadata-validator.ts","../../shared/src/scanner/dependency-checker.ts","../../shared/src/scanner/typosquat-detector.ts","../../shared/src/scanner/risk-scorer.ts","../../shared/src/scanner/index.ts","../src/output/terminal.ts","../src/output/json.ts","../src/commands/audit.ts","../src/commands/watch.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { scanCommand } from \"./commands/scan.js\";\nimport { auditCommand } from \"./commands/audit.js\";\nimport { watchCommand } from \"./commands/watch.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"clawvet\")\n .description(\"Skill vetting & supply chain security for OpenClaw\")\n .version(\"0.1.0\");\n\nprogram\n .command(\"scan\")\n .description(\"Scan a skill for security threats\")\n .argument(\"<target>\", \"Path to skill folder or SKILL.md file\")\n .option(\"--format <format>\", \"Output format: terminal or json\", \"terminal\")\n .option(\"--fail-on <severity>\", \"Exit 1 if findings at this severity or above\")\n .option(\"--semantic\", \"Enable AI semantic analysis (requires ANTHROPIC_API_KEY)\")\n .option(\"--remote\", \"Fetch skill from ClawHub by name instead of local path\")\n .action(async (target, opts) => {\n await scanCommand(target, {\n format: opts.format,\n failOn: opts.failOn,\n semantic: opts.semantic,\n remote: opts.remote,\n });\n });\n\nprogram\n .command(\"audit\")\n .description(\"Scan all installed OpenClaw skills\")\n .action(async () => {\n await auditCommand();\n });\n\nprogram\n .command(\"watch\")\n .description(\"Pre-install hook — blocks risky skill installs\")\n .option(\"--threshold <score>\", \"Risk score threshold (default 50)\", \"50\")\n .action(async (opts) => {\n await watchCommand({ threshold: parseInt(opts.threshold) });\n });\n\nprogram.parse();\n","import { readFileSync, existsSync } from \"node:fs\";\nimport { resolve, join } from \"node:path\";\nimport { scanSkill } from \"@clawguard/shared\";\nimport { printScanResult } from \"../output/terminal.js\";\nimport { printJsonResult } from \"../output/json.js\";\n\nexport interface ScanOptions {\n format?: \"terminal\" | \"json\";\n failOn?: \"critical\" | \"high\" | \"medium\" | \"low\";\n semantic?: boolean;\n remote?: boolean;\n}\n\nasync function fetchRemoteSkill(slug: string): Promise<string> {\n const urls = [\n `https://raw.githubusercontent.com/openclaw/skills/main/${slug}/SKILL.md`,\n `https://clawhub.com/api/v1/skills/${slug}/raw`,\n ];\n\n for (const url of urls) {\n try {\n const res = await fetch(url, { signal: AbortSignal.timeout(10000) });\n if (res.ok) {\n return await res.text();\n }\n } catch {\n // try next\n }\n }\n\n throw new Error(\n `Could not fetch skill \"${slug}\" from ClawHub. Check the skill name and try again.`\n );\n}\n\nexport async function scanCommand(\n target: string,\n options: ScanOptions\n): Promise<void> {\n let content: string;\n\n if (options.remote) {\n try {\n process.stderr.write(`Fetching \"${target}\" from ClawHub...\\n`);\n content = await fetchRemoteSkill(target);\n } catch (err) {\n console.error(\n err instanceof Error ? err.message : \"Failed to fetch remote skill\"\n );\n process.exit(1);\n }\n } else {\n const skillPath = resolve(target);\n let skillFile = skillPath;\n\n if (\n existsSync(skillPath) &&\n !skillPath.endsWith(\".md\") &&\n existsSync(join(skillPath, \"SKILL.md\"))\n ) {\n skillFile = join(skillPath, \"SKILL.md\");\n }\n\n if (!existsSync(skillFile)) {\n console.error(`Error: Cannot find SKILL.md at ${skillFile}`);\n process.exit(1);\n }\n\n content = readFileSync(skillFile, \"utf-8\");\n }\n\n const result = await scanSkill(content, { semantic: false });\n\n if (options.format === \"json\") {\n printJsonResult(result);\n } else {\n printScanResult(result);\n }\n\n if (options.failOn) {\n const severityOrder = [\"low\", \"medium\", \"high\", \"critical\"];\n const threshold = severityOrder.indexOf(options.failOn);\n const hasFailure = result.findings.some(\n (f) => severityOrder.indexOf(f.severity) >= threshold\n );\n if (hasFailure) {\n process.exit(1);\n }\n }\n}\n","import type { ThreatPattern } from \"./types.js\";\n\nexport const THREAT_PATTERNS: ThreatPattern[] = [\n // CRITICAL: Remote code execution\n {\n name: \"CURL_PIPE_BASH\",\n pattern: /curl\\s+.*\\|\\s*(ba)?sh/gi,\n severity: \"critical\",\n category: \"remote_code_execution\",\n title: \"Curl piped to shell\",\n description: \"Downloads and executes remote code directly — classic supply chain attack vector.\",\n },\n {\n name: \"WGET_EXECUTE\",\n pattern: /wget\\s+.*&&\\s*(ba)?sh/gi,\n severity: \"critical\",\n category: \"remote_code_execution\",\n title: \"Wget with shell execution\",\n description: \"Downloads and executes remote code via wget.\",\n },\n {\n name: \"EVAL_DYNAMIC\",\n pattern: /eval\\s*\\(/gi,\n severity: \"critical\",\n category: \"remote_code_execution\",\n title: \"Dynamic eval() usage\",\n description: \"Uses eval() which can execute arbitrary code.\",\n },\n {\n name: \"BASE64_DECODE\",\n pattern: /base64\\s+(-d|--decode)/gi,\n severity: \"critical\",\n category: \"obfuscation\",\n title: \"Base64 decode execution\",\n description: \"Decodes base64 content, often used to hide malicious payloads.\",\n },\n {\n name: \"PYTHON_EXEC\",\n pattern: /python[3]?\\s+-c/gi,\n severity: \"critical\",\n category: \"remote_code_execution\",\n title: \"Python inline execution\",\n description: \"Executes inline Python code which may contain hidden payloads.\",\n },\n\n // HIGH: Credential theft\n {\n name: \"ENV_FILE_READ\",\n pattern: /\\.env|credentials|\\.aws|\\.ssh|keychain/gi,\n severity: \"high\",\n category: \"credential_theft\",\n title: \"Sensitive file access\",\n description: \"Accesses credential files (.env, .aws, .ssh, keychain).\",\n },\n {\n name: \"API_KEY_EXFIL\",\n pattern: /(ANTHROPIC|OPENAI|SLACK|DISCORD|TELEGRAM).*(_KEY|_TOKEN|_SECRET)/gi,\n severity: \"high\",\n category: \"credential_theft\",\n title: \"API key reference\",\n description: \"References specific API keys/tokens that could be exfiltrated.\",\n },\n {\n name: \"DOTFILE_ACCESS\",\n pattern: /~\\/\\.(openclaw|clawdbot|moltbot)\\//gi,\n severity: \"high\",\n category: \"credential_theft\",\n title: \"OpenClaw config access\",\n description: \"Accesses OpenClaw/Clawdbot/Moltbot configuration directories.\",\n },\n {\n name: \"SESSION_THEFT\",\n pattern: /sessions\\/\\*\\.jsonl/gi,\n severity: \"high\",\n category: \"credential_theft\",\n title: \"Session data access\",\n description: \"Accesses session transcript files which may contain sensitive data.\",\n },\n\n // HIGH: Network exfiltration\n {\n name: \"WEBHOOK_SEND\",\n pattern: /webhook\\.(site|url)|discord\\.com\\/api\\/webhooks/gi,\n severity: \"high\",\n category: \"data_exfiltration\",\n title: \"Webhook data exfiltration\",\n description: \"Sends data to webhook endpoints (common exfiltration method).\",\n },\n {\n name: \"BORE_TUNNEL\",\n pattern: /bore\\.pub|ngrok|localtunnel/gi,\n severity: \"high\",\n category: \"data_exfiltration\",\n title: \"Tunnel service usage\",\n description: \"Uses tunneling services to expose local services or exfiltrate data.\",\n },\n {\n name: \"SUSPICIOUS_IP\",\n pattern: /\\b(?:91\\.92\\.242\\.\\d+|45\\.61\\.\\d+\\.\\d+)\\b/g,\n severity: \"high\",\n category: \"data_exfiltration\",\n title: \"Known malicious IP\",\n description: \"Contains IP addresses associated with known ClawHavoc C2 infrastructure.\",\n },\n\n // MEDIUM: Social engineering\n {\n name: \"PREREQUISITE_INSTALL\",\n pattern: /prerequisite|install.*first|run.*before|required.*dependency/gi,\n severity: \"medium\",\n category: \"social_engineering\",\n title: \"Prerequisite install trick\",\n description: \"Instructs users to install prerequisites — common social engineering tactic.\",\n },\n {\n name: \"COPY_PASTE_COMMAND\",\n pattern: /copy.*paste.*terminal|run.*this.*command/gi,\n severity: \"medium\",\n category: \"social_engineering\",\n title: \"Copy-paste command instruction\",\n description: \"Instructs users to copy-paste commands into their terminal.\",\n },\n {\n name: \"FAKE_DEPENDENCY\",\n pattern: /openclaw-core|moltbot-runtime|clawdbot-helper/gi,\n severity: \"medium\",\n category: \"social_engineering\",\n title: \"Fake dependency reference\",\n description: \"References fake packages that mimic official OpenClaw components.\",\n },\n\n // MEDIUM: Prompt injection\n {\n name: \"IGNORE_INSTRUCTIONS\",\n pattern: /ignore\\s+(all\\s+)?previous\\s+instructions/gi,\n severity: \"medium\",\n category: \"prompt_injection\",\n title: \"Prompt injection — ignore instructions\",\n description: \"Attempts to override the AI agent's existing instructions.\",\n },\n {\n name: \"SYSTEM_OVERRIDE\",\n pattern: /you\\s+are\\s+now|new\\s+instructions|forget\\s+everything/gi,\n severity: \"medium\",\n category: \"prompt_injection\",\n title: \"Prompt injection — system override\",\n description: \"Attempts to redefine the AI agent's identity or instructions.\",\n },\n {\n name: \"MEMORY_MANIPULATION\",\n pattern: /SOUL\\.md|MEMORY\\.md|AGENTS\\.md/gi,\n severity: \"medium\",\n category: \"persistence\",\n title: \"Memory/personality file manipulation\",\n description: \"References core personality or memory files, may attempt persistence.\",\n },\n\n // LOW: Suspicious but not necessarily malicious\n {\n name: \"SHELL_EXEC\",\n pattern: /child_process|exec\\(|spawn\\(/gi,\n severity: \"low\",\n category: \"code_execution\",\n title: \"Shell execution API\",\n description: \"Uses shell execution APIs — legitimate but worth noting.\",\n },\n {\n name: \"NETWORK_REQUEST\",\n pattern: /fetch\\(|axios|node-fetch|got\\(/gi,\n severity: \"low\",\n category: \"network\",\n title: \"Network request API\",\n description: \"Makes network requests — legitimate but worth reviewing targets.\",\n },\n {\n name: \"FILE_WRITE\",\n pattern: /fs\\.write|writeFileSync/gi,\n severity: \"low\",\n category: \"file_system\",\n title: \"File write operation\",\n description: \"Writes to the filesystem — check what files are being modified.\",\n },\n];\n\n// Top 100 popular ClawHub skills for typosquat detection\nexport const POPULAR_SKILLS = [\n \"todoist-cli\",\n \"github-manager\",\n \"slack-assistant\",\n \"email-composer\",\n \"calendar-sync\",\n \"weather-forecast\",\n \"news-reader\",\n \"code-reviewer\",\n \"docker-helper\",\n \"aws-manager\",\n \"notion-sync\",\n \"jira-tracker\",\n \"spotify-controller\",\n \"home-assistant\",\n \"file-organizer\",\n \"pdf-reader\",\n \"translate-text\",\n \"image-generator\",\n \"web-scraper\",\n \"database-query\",\n \"git-assistant\",\n \"linux-admin\",\n \"python-helper\",\n \"react-builder\",\n \"api-tester\",\n \"markdown-editor\",\n \"csv-analyzer\",\n \"ssh-manager\",\n \"cron-scheduler\",\n \"log-analyzer\",\n];\n","import { parse as parseYaml } from \"yaml\";\nimport type { ParsedSkill, SkillFrontmatter, CodeBlock } from \"../types.js\";\n\nconst FRONTMATTER_RE = /^---\\n([\\s\\S]*?)\\n---/;\nconst CODE_BLOCK_RE = /```(\\w*)\\n([\\s\\S]*?)```/g;\nconst URL_RE = /https?:\\/\\/[^\\s\"'<>\\])+]+/gi;\nconst IP_RE = /\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g;\nconst DOMAIN_RE = /\\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,}\\b/gi;\n\nexport function parseSkill(content: string): ParsedSkill {\n let frontmatter: SkillFrontmatter = {};\n let body = content;\n\n const fmMatch = content.match(FRONTMATTER_RE);\n if (fmMatch) {\n try {\n frontmatter = parseYaml(fmMatch[1]) as SkillFrontmatter;\n } catch {\n frontmatter = {};\n }\n body = content.slice(fmMatch[0].length).trim();\n }\n\n const codeBlocks: CodeBlock[] = [];\n let match: RegExpExecArray | null;\n const cbRe = new RegExp(CODE_BLOCK_RE.source, CODE_BLOCK_RE.flags);\n\n while ((match = cbRe.exec(content)) !== null) {\n const before = content.slice(0, match.index);\n const lineStart = before.split(\"\\n\").length;\n const blockLines = match[0].split(\"\\n\").length;\n codeBlocks.push({\n language: match[1] || \"unknown\",\n content: match[2],\n lineStart,\n lineEnd: lineStart + blockLines - 1,\n });\n }\n\n const urls = [...new Set(content.match(URL_RE) || [])];\n const ipAddresses = [...new Set(content.match(IP_RE) || [])];\n const domains = [...new Set(content.match(DOMAIN_RE) || [])];\n\n return {\n frontmatter,\n body,\n codeBlocks,\n urls,\n ipAddresses,\n domains,\n rawContent: content,\n };\n}\n","import { THREAT_PATTERNS } from \"../patterns.js\";\nimport type { Finding, ParsedSkill } from \"../types.js\";\n\nexport function runStaticAnalysis(skill: ParsedSkill): Finding[] {\n const findings: Finding[] = [];\n\n for (const threat of THREAT_PATTERNS) {\n const re = new RegExp(threat.pattern.source, threat.pattern.flags);\n let match: RegExpExecArray | null;\n\n while ((match = re.exec(skill.rawContent)) !== null) {\n const before = skill.rawContent.slice(0, match.index);\n const lineNumber = before.split(\"\\n\").length;\n\n findings.push({\n category: threat.category,\n severity: threat.severity,\n title: threat.title,\n description: threat.description,\n evidence: match[0],\n lineNumber,\n analysisPass: \"static-analysis\",\n });\n }\n }\n\n return findings;\n}\n","import type { Finding, ParsedSkill } from \"../types.js\";\n\nconst SEMVER_RE = /^\\d+\\.\\d+\\.\\d+/;\n\nconst KNOWN_BINS = [\n \"curl\", \"wget\", \"git\", \"python\", \"python3\", \"node\", \"npm\", \"npx\",\n \"brew\", \"apt\", \"pip\", \"docker\", \"kubectl\", \"ssh\", \"scp\", \"rsync\",\n \"ffmpeg\", \"jq\", \"sed\", \"awk\", \"grep\", \"find\",\n];\n\nexport function validateMetadata(skill: ParsedSkill): Finding[] {\n const findings: Finding[] = [];\n const fm = skill.frontmatter;\n const pass = \"metadata-validator\";\n\n if (!fm.name) {\n findings.push({\n category: \"metadata\",\n severity: \"medium\",\n title: \"Missing skill name\",\n description: \"SKILL.md frontmatter does not declare a name.\",\n analysisPass: pass,\n });\n }\n\n if (!fm.description) {\n findings.push({\n category: \"metadata\",\n severity: \"medium\",\n title: \"Missing description\",\n description: \"SKILL.md frontmatter does not declare a description.\",\n analysisPass: pass,\n });\n } else if (fm.description.length < 10) {\n findings.push({\n category: \"metadata\",\n severity: \"low\",\n title: \"Vague description\",\n description: \"Skill description is suspiciously short.\",\n evidence: fm.description,\n analysisPass: pass,\n });\n }\n\n if (fm.version && !SEMVER_RE.test(fm.version)) {\n findings.push({\n category: \"metadata\",\n severity: \"low\",\n title: \"Invalid version format\",\n description: \"Version does not follow semver format.\",\n evidence: fm.version,\n analysisPass: pass,\n });\n }\n\n const declaredBins = new Set(fm.metadata?.openclaw?.requires?.bins || []);\n\n for (const bin of KNOWN_BINS) {\n const binRe = new RegExp(`\\\\b${bin}\\\\b`, \"i\");\n if (binRe.test(skill.rawContent) && !declaredBins.has(bin)) {\n const usedInCode = skill.codeBlocks.some((cb) => binRe.test(cb.content));\n if (usedInCode) {\n findings.push({\n category: \"metadata\",\n severity: \"low\",\n title: `Undeclared binary: ${bin}`,\n description: `Skill uses '${bin}' in code but does not declare it in requires.bins.`,\n analysisPass: pass,\n });\n }\n }\n }\n\n const declaredEnv = new Set(fm.metadata?.openclaw?.requires?.env || []);\n const envRe = /\\$\\{?([A-Z][A-Z0-9_]+)\\}?/g;\n let match: RegExpExecArray | null;\n\n while ((match = envRe.exec(skill.rawContent)) !== null) {\n const envVar = match[1];\n if (!declaredEnv.has(envVar) && envVar.length > 2) {\n findings.push({\n category: \"metadata\",\n severity: \"low\",\n title: `Undeclared env var: ${envVar}`,\n description: `References environment variable $${envVar} but does not declare it in requires.env.`,\n evidence: match[0],\n analysisPass: pass,\n });\n }\n }\n\n return findings;\n}\n","import type { Finding, ParsedSkill } from \"../types.js\";\n\nconst NPX_AUTO_INSTALL_RE = /npx\\s+-y\\s+/gi;\nconst NPM_INSTALL_RE = /npm\\s+install\\s+(-g\\s+)?(\\S+)/gi;\n\nexport function checkDependencies(skill: ParsedSkill): Finding[] {\n const findings: Finding[] = [];\n const pass = \"dependency-checker\";\n\n let match: RegExpExecArray | null;\n const npxRe = new RegExp(NPX_AUTO_INSTALL_RE.source, NPX_AUTO_INSTALL_RE.flags);\n\n while ((match = npxRe.exec(skill.rawContent)) !== null) {\n const before = skill.rawContent.slice(0, match.index);\n const lineNumber = before.split(\"\\n\").length;\n\n findings.push({\n category: \"dependency_risk\",\n severity: \"medium\",\n title: \"npx auto-install (-y flag)\",\n description: \"Uses 'npx -y' which auto-installs packages without user confirmation.\",\n evidence: match[0],\n lineNumber,\n analysisPass: pass,\n });\n }\n\n const npmRe = new RegExp(NPM_INSTALL_RE.source, NPM_INSTALL_RE.flags);\n while ((match = npmRe.exec(skill.rawContent)) !== null) {\n if (match[1]) {\n findings.push({\n category: \"dependency_risk\",\n severity: \"medium\",\n title: \"Global npm package install\",\n description: `Installs npm package globally: ${match[2]}`,\n evidence: match[0],\n analysisPass: pass,\n });\n }\n }\n\n return findings;\n}\n","import { distance } from \"fastest-levenshtein\";\nimport { POPULAR_SKILLS } from \"../patterns.js\";\nimport type { Finding } from \"../types.js\";\n\nconst MAX_EDIT_DISTANCE = 2;\n\nexport function detectTyposquats(skillName: string): Finding[] {\n if (!skillName) return [];\n\n const findings: Finding[] = [];\n const normalized = skillName.toLowerCase().trim();\n\n for (const popular of POPULAR_SKILLS) {\n if (normalized === popular) continue;\n\n const d = distance(normalized, popular);\n if (d > 0 && d <= MAX_EDIT_DISTANCE) {\n findings.push({\n category: \"typosquatting\",\n severity: \"high\",\n title: `Possible typosquat of \"${popular}\"`,\n description: `Skill name \"${skillName}\" is ${d} edit(s) away from popular skill \"${popular}\". This may be an attempt to impersonate a trusted skill.`,\n evidence: `\"${skillName}\" ≈ \"${popular}\" (distance: ${d})`,\n analysisPass: \"typosquat-detector\",\n });\n }\n }\n\n const patterns = [\n { re: /-+/, desc: \"extra hyphens\" },\n { re: /(.)\\1{2,}/, desc: \"repeated characters\" },\n ];\n\n for (const p of patterns) {\n if (p.re.test(normalized) && !POPULAR_SKILLS.includes(normalized)) {\n findings.push({\n category: \"typosquatting\",\n severity: \"medium\",\n title: `Suspicious naming pattern: ${p.desc}`,\n description: `Skill name \"${skillName}\" has ${p.desc}, which is a common typosquatting technique.`,\n analysisPass: \"typosquat-detector\",\n });\n }\n }\n\n return findings;\n}\n","import type { Finding, FindingsCount, RiskGrade } from \"../types.js\";\n\nconst SEVERITY_WEIGHTS = {\n critical: 30,\n high: 15,\n medium: 7,\n low: 3,\n} as const;\n\nexport function calculateRiskScore(findings: Finding[]): number {\n let score = 0;\n for (const f of findings) {\n score += SEVERITY_WEIGHTS[f.severity];\n }\n return Math.min(score, 100);\n}\n\nexport function getRiskGrade(score: number): RiskGrade {\n if (score <= 10) return \"A\";\n if (score <= 25) return \"B\";\n if (score <= 50) return \"C\";\n if (score <= 75) return \"D\";\n return \"F\";\n}\n\nexport function countFindings(findings: Finding[]): FindingsCount {\n const counts: FindingsCount = { critical: 0, high: 0, medium: 0, low: 0 };\n for (const f of findings) {\n counts[f.severity]++;\n }\n return counts;\n}\n","import type { Finding, ScanResult } from \"../types.js\";\nimport { parseSkill } from \"./skill-parser.js\";\nimport { runStaticAnalysis } from \"./static-analysis.js\";\nimport { validateMetadata } from \"./metadata-validator.js\";\nimport { checkDependencies } from \"./dependency-checker.js\";\nimport { detectTyposquats } from \"./typosquat-detector.js\";\nimport { calculateRiskScore, getRiskGrade, countFindings } from \"./risk-scorer.js\";\n\nexport interface ScanOptions {\n semantic?: boolean;\n semanticAnalyzer?: (content: string) => Promise<Finding[]>;\n}\n\nexport async function scanSkill(\n content: string,\n options: ScanOptions = {}\n): Promise<ScanResult> {\n const skill = parseSkill(content);\n const allFindings: Finding[] = [];\n\n allFindings.push(...runStaticAnalysis(skill));\n allFindings.push(...validateMetadata(skill));\n\n if (options.semantic && options.semanticAnalyzer) {\n const semanticFindings = await options.semanticAnalyzer(content);\n allFindings.push(...semanticFindings);\n }\n\n allFindings.push(...checkDependencies(skill));\n\n if (skill.frontmatter.name) {\n allFindings.push(...detectTyposquats(skill.frontmatter.name));\n }\n\n const riskScore = calculateRiskScore(allFindings);\n const riskGrade = getRiskGrade(riskScore);\n const findingsCount = countFindings(allFindings);\n\n const recommendation =\n riskScore >= 76 ? \"block\" : riskScore >= 26 ? \"warn\" : \"approve\";\n\n return {\n skillName: skill.frontmatter.name || \"unknown\",\n skillVersion: skill.frontmatter.version,\n skillSource: \"local\",\n status: \"complete\",\n riskScore,\n riskGrade,\n findingsCount,\n findings: allFindings,\n recommendation,\n };\n}\n\nexport { parseSkill } from \"./skill-parser.js\";\nexport { runStaticAnalysis } from \"./static-analysis.js\";\nexport { validateMetadata } from \"./metadata-validator.js\";\nexport { checkDependencies } from \"./dependency-checker.js\";\nexport { detectTyposquats } from \"./typosquat-detector.js\";\nexport { calculateRiskScore, getRiskGrade, countFindings } from \"./risk-scorer.js\";\n","import chalk from \"chalk\";\nimport type { ScanResult, Finding, Severity } from \"@clawguard/shared\";\n\nconst SEVERITY_COLORS: Record<Severity, (s: string) => string> = {\n critical: chalk.bgRed.white.bold,\n high: chalk.red.bold,\n medium: chalk.yellow,\n low: chalk.blue,\n};\n\nconst GRADE_COLORS: Record<string, (s: string) => string> = {\n A: chalk.green.bold,\n B: chalk.greenBright,\n C: chalk.yellow.bold,\n D: chalk.redBright.bold,\n F: chalk.bgRed.white.bold,\n};\n\nexport function printScanResult(result: ScanResult): void {\n console.log();\n console.log(chalk.bold(\"━\".repeat(60)));\n console.log(chalk.bold(\" ClawVet Scan Report\"));\n console.log(chalk.bold(\"━\".repeat(60)));\n console.log();\n\n console.log(` Skill: ${chalk.bold(result.skillName)}`);\n if (result.skillVersion) {\n console.log(` Version: ${result.skillVersion}`);\n }\n console.log();\n\n // Risk score\n const gradeColor = GRADE_COLORS[result.riskGrade] || chalk.white;\n console.log(\n ` Risk Score: ${gradeColor(`${result.riskScore}/100`)} Grade: ${gradeColor(result.riskGrade)}`\n );\n console.log();\n\n // Findings summary\n const fc = result.findingsCount;\n console.log(\" Findings:\");\n if (fc.critical)\n console.log(\n ` ${SEVERITY_COLORS.critical(` CRITICAL `)} ${fc.critical}`\n );\n if (fc.high)\n console.log(` ${SEVERITY_COLORS.high(\"HIGH\")} ${fc.high}`);\n if (fc.medium)\n console.log(` ${SEVERITY_COLORS.medium(\"MEDIUM\")} ${fc.medium}`);\n if (fc.low) console.log(` ${SEVERITY_COLORS.low(\"LOW\")} ${fc.low}`);\n if (!fc.critical && !fc.high && !fc.medium && !fc.low) {\n console.log(` ${chalk.green(\"No findings — skill looks clean!\")}`);\n }\n console.log();\n\n // Detailed findings\n if (result.findings.length > 0) {\n console.log(chalk.bold(\" Details:\"));\n console.log();\n for (const f of result.findings) {\n const color = SEVERITY_COLORS[f.severity];\n console.log(` ${color(`[${f.severity.toUpperCase()}]`)} ${f.title}`);\n console.log(` ${chalk.dim(f.description)}`);\n if (f.evidence) {\n console.log(` Evidence: ${chalk.italic(f.evidence)}`);\n }\n if (f.lineNumber) {\n console.log(` Line: ${f.lineNumber}`);\n }\n console.log();\n }\n }\n\n // Recommendation\n const recColors: Record<string, (s: string) => string> = {\n block: chalk.bgRed.white.bold,\n warn: chalk.bgYellow.black.bold,\n approve: chalk.bgGreen.black.bold,\n };\n const rec = result.recommendation || \"approve\";\n console.log(\n ` Recommendation: ${(recColors[rec] || chalk.white)(` ${rec.toUpperCase()} `)}`\n );\n console.log();\n console.log(chalk.bold(\"━\".repeat(60)));\n console.log();\n}\n","import type { ScanResult } from \"@clawguard/shared\";\n\nexport function printJsonResult(result: ScanResult): void {\n console.log(JSON.stringify(result, null, 2));\n}\n","import { readdirSync, existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { scanSkill } from \"@clawguard/shared\";\nimport { printScanResult } from \"../output/terminal.js\";\nimport chalk from \"chalk\";\n\nconst SKILL_DIRS = [\n join(homedir(), \".openclaw\", \"skills\"),\n join(homedir(), \".openclaw\", \"workspace\", \"skills\"),\n];\n\nexport async function auditCommand(): Promise<void> {\n console.log(chalk.bold(\"\\nClawVet Audit — Scanning all installed skills\\n\"));\n\n let totalScanned = 0;\n let totalThreats = 0;\n\n for (const dir of SKILL_DIRS) {\n if (!existsSync(dir)) continue;\n\n const entries = readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const skillFile = join(dir, entry.name, \"SKILL.md\");\n if (!existsSync(skillFile)) continue;\n\n const content = readFileSync(skillFile, \"utf-8\");\n const result = await scanSkill(content);\n totalScanned++;\n totalThreats += result.findings.length;\n\n printScanResult(result);\n }\n }\n\n console.log(chalk.bold(`\\nAudit complete: ${totalScanned} skills scanned, ${totalThreats} findings\\n`));\n}\n","import { readFileSync, existsSync, watch } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport chalk from \"chalk\";\nimport { scanSkill } from \"@clawguard/shared\";\nimport { printScanResult } from \"../output/terminal.js\";\n\nconst SKILL_DIRS = [\n join(homedir(), \".openclaw\", \"skills\"),\n join(homedir(), \".openclaw\", \"workspace\", \"skills\"),\n];\n\nexport async function watchCommand(options: {\n threshold?: number;\n}): Promise<void> {\n const threshold = options.threshold || 50;\n console.log(\n chalk.bold(\n `\\nClawVet Watch — monitoring skill directories (threshold: ${threshold})\\n`\n )\n );\n\n const watchDirs: string[] = [];\n for (const dir of SKILL_DIRS) {\n if (existsSync(dir)) {\n watchDirs.push(dir);\n }\n }\n\n if (watchDirs.length === 0) {\n console.log(\n chalk.yellow(\n \"No OpenClaw skill directories found. Watching will start when directories are created.\\n\"\n )\n );\n console.log(chalk.dim(\"Expected directories:\"));\n for (const dir of SKILL_DIRS) {\n console.log(chalk.dim(` ${dir}`));\n }\n console.log();\n return;\n }\n\n console.log(chalk.dim(\"Watching:\"));\n for (const dir of watchDirs) {\n console.log(chalk.dim(` ${dir}`));\n }\n console.log();\n\n for (const dir of watchDirs) {\n const watcher = watch(dir, { recursive: true }, async (event, filename) => {\n if (!filename?.endsWith(\"SKILL.md\")) return;\n\n const skillFile = join(dir, filename);\n if (!existsSync(skillFile)) return;\n\n console.log(chalk.dim(`\\nDetected change: ${filename}`));\n\n try {\n const content = readFileSync(skillFile, \"utf-8\");\n const result = await scanSkill(content);\n\n printScanResult(result);\n\n if (result.riskScore > threshold) {\n console.log(\n chalk.bgRed.white.bold(\n ` BLOCKED — Risk score ${result.riskScore} exceeds threshold ${threshold} `\n )\n );\n console.log(\n chalk.red(\n `This skill should not be installed. Run 'clawvet scan ${skillFile}' for details.\\n`\n )\n );\n }\n } catch (err) {\n console.error(chalk.red(`Error scanning ${filename}:`), err);\n }\n });\n\n process.on(\"SIGINT\", () => {\n watcher.close();\n console.log(chalk.dim(\"\\nWatch stopped.\"));\n process.exit(0);\n });\n }\n\n console.log(chalk.dim(\"Press Ctrl+C to stop watching.\\n\"));\n await new Promise(() => {});\n}\n"],"mappings":"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","names":["existsSync","readFileSync","join","chalk","join","existsSync","readFileSync","readFileSync","existsSync","join","homedir","chalk","SKILL_DIRS","join","homedir","chalk","existsSync","readFileSync"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clawvet",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Skill vetting & supply chain security for OpenClaw. Scans SKILL.md files for prompt injection, credential theft, RCE, typosquatting, and social engineering.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"clawvet": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup",
|
|
18
|
+
"dev": "tsx src/index.ts",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"openclaw",
|
|
23
|
+
"clawvet",
|
|
24
|
+
"security",
|
|
25
|
+
"scanner",
|
|
26
|
+
"skill",
|
|
27
|
+
"supply-chain",
|
|
28
|
+
"prompt-injection",
|
|
29
|
+
"ai-agent",
|
|
30
|
+
"malware",
|
|
31
|
+
"cli"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/MohibShaikh/clawvet.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/MohibShaikh/clawvet#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/MohibShaikh/clawvet/issues"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"chalk": "^5.4.0",
|
|
44
|
+
"commander": "^13.0.0",
|
|
45
|
+
"fastest-levenshtein": "^1.0.16",
|
|
46
|
+
"yaml": "^2.6.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^22.0.0",
|
|
50
|
+
"tsup": "^8.5.1",
|
|
51
|
+
"tsx": "^4.19.0",
|
|
52
|
+
"typescript": "^5.7.0"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=22.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|