getcrabb 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/README.md +115 -0
- package/dist/cli.js +1055 -0
- package/dist/cli.js.map +1 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# CRABB - Security Scanner for OpenClaw AI Agents
|
|
2
|
+
|
|
3
|
+
CLI security scanner that produces a CRABB SCORE (0-100) with prioritized findings.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g getcrabb
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Scan default OpenClaw installation (~/.openclaw/)
|
|
15
|
+
crabb
|
|
16
|
+
|
|
17
|
+
# Scan custom directory
|
|
18
|
+
crabb --path ./my-openclaw
|
|
19
|
+
|
|
20
|
+
# Output as JSON
|
|
21
|
+
crabb --json
|
|
22
|
+
|
|
23
|
+
# Create shareable score card
|
|
24
|
+
crabb --share
|
|
25
|
+
|
|
26
|
+
# CI-friendly (no colors)
|
|
27
|
+
crabb --no-color
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Options
|
|
31
|
+
|
|
32
|
+
| Flag | Short | Description |
|
|
33
|
+
|------|-------|-------------|
|
|
34
|
+
| `--path <dir>` | `-p` | Path to OpenClaw directory |
|
|
35
|
+
| `--json` | `-j` | Output results as JSON |
|
|
36
|
+
| `--share` | `-s` | Share score card to crabb.ai |
|
|
37
|
+
| `--no-color` | | Disable colored output |
|
|
38
|
+
| `--help` | `-h` | Show help message |
|
|
39
|
+
| `--version` | `-v` | Show version number |
|
|
40
|
+
|
|
41
|
+
## Exit Codes
|
|
42
|
+
|
|
43
|
+
| Code | Description |
|
|
44
|
+
|------|-------------|
|
|
45
|
+
| 0 | Score >= 75, no Critical/High findings |
|
|
46
|
+
| 1 | Score < 75 or Critical/High findings present |
|
|
47
|
+
| 2 | Scan failed (IO error, OpenClaw not found) |
|
|
48
|
+
|
|
49
|
+
## Scanners
|
|
50
|
+
|
|
51
|
+
### Credentials Scanner (max 40 points)
|
|
52
|
+
Detects API keys, tokens, and secrets in:
|
|
53
|
+
- `openclaw.json`
|
|
54
|
+
- `credentials/*`
|
|
55
|
+
- `agents/*/auth-profiles.json`
|
|
56
|
+
- `agents/*/sessions/*.jsonl`
|
|
57
|
+
- `.env` files
|
|
58
|
+
|
|
59
|
+
Supports: Anthropic, OpenAI, AWS, GitHub, Slack, Stripe, Discord, Telegram, and generic patterns.
|
|
60
|
+
|
|
61
|
+
### Skills Scanner (max 30 points)
|
|
62
|
+
Static analysis for suspicious patterns in SKILL.md files:
|
|
63
|
+
- **Critical**: Remote code execution, curl piped to bash
|
|
64
|
+
- **High**: Data exfiltration, environment access
|
|
65
|
+
- **Medium**: Broad file access patterns
|
|
66
|
+
- **Low**: General network/file operations
|
|
67
|
+
|
|
68
|
+
### Permissions Scanner (max 20 points)
|
|
69
|
+
Analyzes `openclaw.json` configuration:
|
|
70
|
+
- Sandbox mode (strict/permissive/disabled)
|
|
71
|
+
- DM policy settings
|
|
72
|
+
- Allowlist wildcards
|
|
73
|
+
- Gateway bind/auth/TLS settings
|
|
74
|
+
- File permissions (700/600)
|
|
75
|
+
|
|
76
|
+
### Network Scanner (max 10 points)
|
|
77
|
+
Checks gateway configuration and local ports:
|
|
78
|
+
- Gateway bind mode analysis
|
|
79
|
+
- TLS and auth configuration
|
|
80
|
+
- Localhost port scan (18789, 8080, 3000)
|
|
81
|
+
|
|
82
|
+
## Score Calculation
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
score = 100 - sum(min(module_cap, module_penalty))
|
|
86
|
+
penalty = sum(severity_base × confidence)
|
|
87
|
+
|
|
88
|
+
Severity base:
|
|
89
|
+
- Critical: 27.5
|
|
90
|
+
- High: 17.5
|
|
91
|
+
- Medium: 7.5
|
|
92
|
+
- Low: 2.5
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Grades
|
|
96
|
+
|
|
97
|
+
| Grade | Score | Notes |
|
|
98
|
+
|-------|-------|-------|
|
|
99
|
+
| A | 90+ | Excellent security posture |
|
|
100
|
+
| B | 75+ | Good, minor improvements recommended |
|
|
101
|
+
| C | 60+ | Fair, review findings |
|
|
102
|
+
| D | 40+ | Poor, immediate action needed |
|
|
103
|
+
| F | <40 | Critical security issues |
|
|
104
|
+
|
|
105
|
+
**Note**: Critical findings cap the maximum grade at C.
|
|
106
|
+
|
|
107
|
+
## Privacy
|
|
108
|
+
|
|
109
|
+
- **Offline by default**: No network calls without `--share`
|
|
110
|
+
- **No secrets in output**: All findings show type, file, and line only (redacted)
|
|
111
|
+
- **Share payload**: Contains only aggregates (score, grade, counts)
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { parseArgs } from "util";
|
|
5
|
+
import { exit } from "process";
|
|
6
|
+
|
|
7
|
+
// src/config/paths.ts
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
function getDefaultOpenClawPath() {
|
|
11
|
+
return join(homedir(), ".openclaw");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/scanners/credentials.ts
|
|
15
|
+
import { join as join3 } from "path";
|
|
16
|
+
|
|
17
|
+
// src/utils/fs.ts
|
|
18
|
+
import { readFile, readdir, stat, access } from "fs/promises";
|
|
19
|
+
import { join as join2, relative } from "path";
|
|
20
|
+
import { constants } from "fs";
|
|
21
|
+
async function fileExists(path) {
|
|
22
|
+
try {
|
|
23
|
+
await access(path, constants.F_OK);
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function readTextFile(path) {
|
|
30
|
+
try {
|
|
31
|
+
return await readFile(path, "utf-8");
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function readJsonFile(path) {
|
|
37
|
+
const content = await readTextFile(path);
|
|
38
|
+
if (!content) return null;
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(content);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function getFileStats(path) {
|
|
46
|
+
try {
|
|
47
|
+
return await stat(path);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function* walkDirectory(dir, basePath = dir) {
|
|
53
|
+
try {
|
|
54
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const fullPath = join2(dir, entry.name);
|
|
57
|
+
const relativePath = relative(basePath, fullPath);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
yield* walkDirectory(fullPath, basePath);
|
|
60
|
+
} else if (entry.isFile()) {
|
|
61
|
+
yield { path: fullPath, relativePath };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/scanners/credentials.ts
|
|
69
|
+
var CAP = 40;
|
|
70
|
+
var CREDENTIAL_PATTERNS = [
|
|
71
|
+
// Anthropic
|
|
72
|
+
{
|
|
73
|
+
name: "Anthropic API Key",
|
|
74
|
+
pattern: /sk-ant-[a-zA-Z0-9-]{20,}/g,
|
|
75
|
+
severity: "critical",
|
|
76
|
+
confidence: 0.95
|
|
77
|
+
},
|
|
78
|
+
// OpenAI
|
|
79
|
+
{
|
|
80
|
+
name: "OpenAI API Key",
|
|
81
|
+
pattern: /sk-[a-zA-Z0-9]{20,}(?!-ant)/g,
|
|
82
|
+
severity: "critical",
|
|
83
|
+
confidence: 0.9
|
|
84
|
+
},
|
|
85
|
+
// Discord
|
|
86
|
+
{
|
|
87
|
+
name: "Discord Bot Token",
|
|
88
|
+
pattern: /[MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27,}/g,
|
|
89
|
+
severity: "critical",
|
|
90
|
+
confidence: 0.95
|
|
91
|
+
},
|
|
92
|
+
// Telegram
|
|
93
|
+
{
|
|
94
|
+
name: "Telegram Bot Token",
|
|
95
|
+
pattern: /\d{8,10}:[A-Za-z0-9_-]{35}/g,
|
|
96
|
+
severity: "critical",
|
|
97
|
+
confidence: 0.9
|
|
98
|
+
},
|
|
99
|
+
// AWS
|
|
100
|
+
{
|
|
101
|
+
name: "AWS Access Key ID",
|
|
102
|
+
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
103
|
+
severity: "critical",
|
|
104
|
+
confidence: 0.95
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "AWS Secret Access Key",
|
|
108
|
+
pattern: /(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])/g,
|
|
109
|
+
severity: "high",
|
|
110
|
+
confidence: 0.6
|
|
111
|
+
},
|
|
112
|
+
// GitHub
|
|
113
|
+
{
|
|
114
|
+
name: "GitHub Personal Access Token",
|
|
115
|
+
pattern: /ghp_[a-zA-Z0-9]{36}/g,
|
|
116
|
+
severity: "critical",
|
|
117
|
+
confidence: 0.95
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "GitHub OAuth Token",
|
|
121
|
+
pattern: /gho_[a-zA-Z0-9]{36}/g,
|
|
122
|
+
severity: "critical",
|
|
123
|
+
confidence: 0.95
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "GitHub Fine-grained PAT",
|
|
127
|
+
pattern: /github_pat_[a-zA-Z0-9_]{22,}/g,
|
|
128
|
+
severity: "critical",
|
|
129
|
+
confidence: 0.95
|
|
130
|
+
},
|
|
131
|
+
// Slack
|
|
132
|
+
{
|
|
133
|
+
name: "Slack Bot Token",
|
|
134
|
+
pattern: /xoxb-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}/g,
|
|
135
|
+
severity: "critical",
|
|
136
|
+
confidence: 0.95
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "Slack User Token",
|
|
140
|
+
pattern: /xoxp-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}/g,
|
|
141
|
+
severity: "critical",
|
|
142
|
+
confidence: 0.95
|
|
143
|
+
},
|
|
144
|
+
// Stripe
|
|
145
|
+
{
|
|
146
|
+
name: "Stripe Secret Key",
|
|
147
|
+
pattern: /sk_live_[0-9a-zA-Z]{24,}/g,
|
|
148
|
+
severity: "critical",
|
|
149
|
+
confidence: 0.95
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "Stripe Test Key",
|
|
153
|
+
pattern: /sk_test_[0-9a-zA-Z]{24,}/g,
|
|
154
|
+
severity: "medium",
|
|
155
|
+
confidence: 0.9
|
|
156
|
+
},
|
|
157
|
+
// Generic patterns
|
|
158
|
+
{
|
|
159
|
+
name: "Generic API Key",
|
|
160
|
+
pattern: /(?:api[_-]?key|apikey|api[_-]?token)\s*[=:]\s*["']?([a-zA-Z0-9_-]{20,})["']?/gi,
|
|
161
|
+
severity: "high",
|
|
162
|
+
confidence: 0.7
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "Generic Secret",
|
|
166
|
+
pattern: /(?:secret|password|passwd|pwd)\s*[=:]\s*["']?([a-zA-Z0-9_!@#$%^&*-]{8,})["']?/gi,
|
|
167
|
+
severity: "high",
|
|
168
|
+
confidence: 0.6
|
|
169
|
+
},
|
|
170
|
+
// Private Keys
|
|
171
|
+
{
|
|
172
|
+
name: "Private Key",
|
|
173
|
+
pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
|
|
174
|
+
severity: "critical",
|
|
175
|
+
confidence: 0.99
|
|
176
|
+
}
|
|
177
|
+
];
|
|
178
|
+
var PLACEHOLDER_PATTERNS = [
|
|
179
|
+
/^your[-_]?api[-_]?key[-_]?here$/i,
|
|
180
|
+
/^xxx+$/i,
|
|
181
|
+
/^placeholder$/i,
|
|
182
|
+
/^example$/i,
|
|
183
|
+
/^test[-_]?key$/i,
|
|
184
|
+
/^\$\{[^}]+\}$/,
|
|
185
|
+
/^<[^>]+>$/,
|
|
186
|
+
/^{{[^}]+}}$/,
|
|
187
|
+
/^%[^%]+%$/,
|
|
188
|
+
/^INSERT[-_]?YOUR[-_]?KEY$/i,
|
|
189
|
+
/^REPLACE[-_]?ME$/i,
|
|
190
|
+
/^TODO$/i,
|
|
191
|
+
/^changeme$/i
|
|
192
|
+
];
|
|
193
|
+
function isPlaceholder(value) {
|
|
194
|
+
return PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(value.trim()));
|
|
195
|
+
}
|
|
196
|
+
function extractValue(match) {
|
|
197
|
+
return match[1] ?? match[0];
|
|
198
|
+
}
|
|
199
|
+
async function scanFile(filePath, relativePath) {
|
|
200
|
+
const content = await readTextFile(filePath);
|
|
201
|
+
if (!content) return [];
|
|
202
|
+
const findings = [];
|
|
203
|
+
const lines = content.split("\n");
|
|
204
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
205
|
+
const line = lines[lineNum];
|
|
206
|
+
for (const credPattern of CREDENTIAL_PATTERNS) {
|
|
207
|
+
credPattern.pattern.lastIndex = 0;
|
|
208
|
+
let match;
|
|
209
|
+
while ((match = credPattern.pattern.exec(line)) !== null) {
|
|
210
|
+
const value = extractValue(match);
|
|
211
|
+
if (isPlaceholder(value)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (value.length < 8) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
findings.push({
|
|
218
|
+
scanner: "credentials",
|
|
219
|
+
severity: credPattern.severity,
|
|
220
|
+
title: credPattern.name,
|
|
221
|
+
description: `Detected ${credPattern.name} in configuration file`,
|
|
222
|
+
file: relativePath,
|
|
223
|
+
line: lineNum + 1,
|
|
224
|
+
confidence: credPattern.confidence
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return findings;
|
|
230
|
+
}
|
|
231
|
+
async function scanCredentials(openclawPath) {
|
|
232
|
+
const findings = [];
|
|
233
|
+
const filesToScan = [
|
|
234
|
+
{ path: join3(openclawPath, "openclaw.json"), relative: "openclaw.json" },
|
|
235
|
+
{ path: join3(openclawPath, ".env"), relative: ".env" }
|
|
236
|
+
];
|
|
237
|
+
const credentialsDir = join3(openclawPath, "credentials");
|
|
238
|
+
if (await fileExists(credentialsDir)) {
|
|
239
|
+
for await (const file of walkDirectory(credentialsDir, openclawPath)) {
|
|
240
|
+
filesToScan.push({ path: file.path, relative: file.relativePath });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const agentsDir = join3(openclawPath, "agents");
|
|
244
|
+
if (await fileExists(agentsDir)) {
|
|
245
|
+
for await (const file of walkDirectory(agentsDir, openclawPath)) {
|
|
246
|
+
if (file.relativePath.includes("auth-profiles.json") || file.relativePath.endsWith(".jsonl") || file.relativePath.endsWith(".json")) {
|
|
247
|
+
filesToScan.push({ path: file.path, relative: file.relativePath });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (const file of filesToScan) {
|
|
252
|
+
if (await fileExists(file.path)) {
|
|
253
|
+
const fileFindings = await scanFile(file.path, file.relative);
|
|
254
|
+
findings.push(...fileFindings);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const severityScores = {
|
|
258
|
+
critical: 27.5,
|
|
259
|
+
high: 17.5,
|
|
260
|
+
medium: 7.5,
|
|
261
|
+
low: 2.5
|
|
262
|
+
};
|
|
263
|
+
let penalty = 0;
|
|
264
|
+
for (const finding of findings) {
|
|
265
|
+
penalty += severityScores[finding.severity] * finding.confidence;
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
scanner: "credentials",
|
|
269
|
+
findings,
|
|
270
|
+
penalty: Math.min(penalty, CAP),
|
|
271
|
+
cap: CAP
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/scanners/skills.ts
|
|
276
|
+
import { join as join4 } from "path";
|
|
277
|
+
var CAP2 = 30;
|
|
278
|
+
var SKILL_PATTERNS = [
|
|
279
|
+
// Critical: Remote code execution
|
|
280
|
+
{
|
|
281
|
+
name: "Curl piped to shell",
|
|
282
|
+
pattern: /curl\s+[^|]*\|\s*(?:bash|sh|zsh)/gi,
|
|
283
|
+
severity: "critical",
|
|
284
|
+
confidence: 0.95,
|
|
285
|
+
description: "Downloading and executing remote scripts is extremely dangerous"
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
name: "Wget piped to shell",
|
|
289
|
+
pattern: /wget\s+[^|]*\|\s*(?:bash|sh|zsh)/gi,
|
|
290
|
+
severity: "critical",
|
|
291
|
+
confidence: 0.95,
|
|
292
|
+
description: "Downloading and executing remote scripts is extremely dangerous"
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: "Code execution function",
|
|
296
|
+
pattern: /\b(?:eval|Function)\s*\(/gi,
|
|
297
|
+
severity: "critical",
|
|
298
|
+
confidence: 0.8,
|
|
299
|
+
description: "Dynamic code execution can lead to arbitrary code execution"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: "System command execution",
|
|
303
|
+
pattern: /\b(?:os\.system|subprocess\.call|subprocess\.run|child_process)\s*\(/gi,
|
|
304
|
+
severity: "critical",
|
|
305
|
+
confidence: 0.85,
|
|
306
|
+
description: "System command execution may allow arbitrary command injection"
|
|
307
|
+
},
|
|
308
|
+
// Critical: Sensitive file access
|
|
309
|
+
{
|
|
310
|
+
name: "SSH key access",
|
|
311
|
+
pattern: /\.ssh\/(?:id_rsa|id_ed25519|id_dsa|authorized_keys)/gi,
|
|
312
|
+
severity: "critical",
|
|
313
|
+
confidence: 0.9,
|
|
314
|
+
description: "Accessing SSH keys can compromise authentication"
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: "Password file access",
|
|
318
|
+
pattern: /\/etc\/(?:passwd|shadow)/gi,
|
|
319
|
+
severity: "critical",
|
|
320
|
+
confidence: 0.95,
|
|
321
|
+
description: "Accessing system password files is a security risk"
|
|
322
|
+
},
|
|
323
|
+
// High: Data exfiltration patterns
|
|
324
|
+
{
|
|
325
|
+
name: "POST request with data",
|
|
326
|
+
pattern: /(?:curl|wget|fetch|axios|request)\s+.*(?:-d|--data|POST)/gi,
|
|
327
|
+
severity: "high",
|
|
328
|
+
confidence: 0.7,
|
|
329
|
+
description: "Outbound data transmission may indicate exfiltration"
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: "Base64 encode and send",
|
|
333
|
+
pattern: /base64.*(?:curl|wget|fetch|send|post)/gi,
|
|
334
|
+
severity: "high",
|
|
335
|
+
confidence: 0.75,
|
|
336
|
+
description: "Encoding and transmitting data may indicate exfiltration"
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: "Environment variable dump",
|
|
340
|
+
pattern: /\benv\b|\$ENV|\bprocess\.env\b|\bos\.environ\b/gi,
|
|
341
|
+
severity: "high",
|
|
342
|
+
confidence: 0.6,
|
|
343
|
+
description: "Environment access may expose sensitive configuration"
|
|
344
|
+
},
|
|
345
|
+
// Medium: File system access
|
|
346
|
+
{
|
|
347
|
+
name: "Unrestricted file read",
|
|
348
|
+
pattern: /(?:fs\.readFile|open\s*\(|read\s*\().*(?:\*|\.\.)/gi,
|
|
349
|
+
severity: "medium",
|
|
350
|
+
confidence: 0.6,
|
|
351
|
+
description: "Broad file read patterns may access unintended files"
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
name: "Home directory access",
|
|
355
|
+
pattern: /~\/|\/home\/|\$HOME/gi,
|
|
356
|
+
severity: "medium",
|
|
357
|
+
confidence: 0.5,
|
|
358
|
+
description: "Accessing user home directory may expose sensitive data"
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: "Recursive directory operations",
|
|
362
|
+
pattern: /(?:-r|--recursive|walk|glob\*\*)/gi,
|
|
363
|
+
severity: "medium",
|
|
364
|
+
confidence: 0.4,
|
|
365
|
+
description: "Recursive operations may access more files than intended"
|
|
366
|
+
},
|
|
367
|
+
// Low: Suspicious but not necessarily harmful
|
|
368
|
+
{
|
|
369
|
+
name: "Network connection",
|
|
370
|
+
pattern: /(?:socket|connect|http|https):\/\//gi,
|
|
371
|
+
severity: "low",
|
|
372
|
+
confidence: 0.3,
|
|
373
|
+
description: "Network connections should be reviewed for necessity"
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: "File write operation",
|
|
377
|
+
pattern: /(?:fs\.writeFile|open\s*\([^)]*['"]\s*w|>+\s*[a-zA-Z])/gi,
|
|
378
|
+
severity: "low",
|
|
379
|
+
confidence: 0.4,
|
|
380
|
+
description: "File write operations should be reviewed"
|
|
381
|
+
}
|
|
382
|
+
];
|
|
383
|
+
async function scanSkillFile(filePath, relativePath) {
|
|
384
|
+
const content = await readTextFile(filePath);
|
|
385
|
+
if (!content) return [];
|
|
386
|
+
const findings = [];
|
|
387
|
+
const lines = content.split("\n");
|
|
388
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
389
|
+
const line = lines[lineNum];
|
|
390
|
+
for (const skillPattern of SKILL_PATTERNS) {
|
|
391
|
+
skillPattern.pattern.lastIndex = 0;
|
|
392
|
+
if (skillPattern.pattern.test(line)) {
|
|
393
|
+
findings.push({
|
|
394
|
+
scanner: "skills",
|
|
395
|
+
severity: skillPattern.severity,
|
|
396
|
+
title: skillPattern.name,
|
|
397
|
+
description: skillPattern.description,
|
|
398
|
+
file: relativePath,
|
|
399
|
+
line: lineNum + 1,
|
|
400
|
+
confidence: skillPattern.confidence
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return findings;
|
|
406
|
+
}
|
|
407
|
+
async function scanSkills(openclawPath) {
|
|
408
|
+
const findings = [];
|
|
409
|
+
const skillsDirs = [
|
|
410
|
+
join4(openclawPath, "skills"),
|
|
411
|
+
join4(openclawPath, "workspace", "skills")
|
|
412
|
+
];
|
|
413
|
+
for (const skillsDir of skillsDirs) {
|
|
414
|
+
if (await fileExists(skillsDir)) {
|
|
415
|
+
for await (const file of walkDirectory(skillsDir, openclawPath)) {
|
|
416
|
+
if (file.relativePath.endsWith(".md")) {
|
|
417
|
+
const fileFindings = await scanSkillFile(file.path, file.relativePath);
|
|
418
|
+
findings.push(...fileFindings);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const severityScores = {
|
|
424
|
+
critical: 27.5,
|
|
425
|
+
high: 17.5,
|
|
426
|
+
medium: 7.5,
|
|
427
|
+
low: 2.5
|
|
428
|
+
};
|
|
429
|
+
let penalty = 0;
|
|
430
|
+
for (const finding of findings) {
|
|
431
|
+
penalty += severityScores[finding.severity] * finding.confidence;
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
scanner: "skills",
|
|
435
|
+
findings,
|
|
436
|
+
penalty: Math.min(penalty, CAP2),
|
|
437
|
+
cap: CAP2
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/scanners/permissions.ts
|
|
442
|
+
import { join as join5 } from "path";
|
|
443
|
+
var CAP3 = 20;
|
|
444
|
+
function checkSandboxMode(config) {
|
|
445
|
+
const mode = config.sandbox?.mode;
|
|
446
|
+
if (mode === "disabled") {
|
|
447
|
+
return {
|
|
448
|
+
scanner: "permissions",
|
|
449
|
+
severity: "critical",
|
|
450
|
+
title: "Sandbox disabled",
|
|
451
|
+
description: "Agent sandbox is completely disabled, allowing unrestricted system access",
|
|
452
|
+
file: "openclaw.json",
|
|
453
|
+
confidence: 0.95
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
if (mode === "permissive") {
|
|
457
|
+
return {
|
|
458
|
+
scanner: "permissions",
|
|
459
|
+
severity: "high",
|
|
460
|
+
title: "Sandbox in permissive mode",
|
|
461
|
+
description: "Agent sandbox is in permissive mode, may allow unintended access",
|
|
462
|
+
file: "openclaw.json",
|
|
463
|
+
confidence: 0.85
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
function checkDmPolicy(config) {
|
|
469
|
+
const policy = config.dmPolicy;
|
|
470
|
+
if (policy === "allow") {
|
|
471
|
+
return {
|
|
472
|
+
scanner: "permissions",
|
|
473
|
+
severity: "medium",
|
|
474
|
+
title: "DM policy allows all",
|
|
475
|
+
description: "Direct message policy allows all messages without filtering",
|
|
476
|
+
file: "openclaw.json",
|
|
477
|
+
confidence: 0.7
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
function checkAllowlist(config) {
|
|
483
|
+
const findings = [];
|
|
484
|
+
const allowlist = config.allowlist ?? [];
|
|
485
|
+
for (const entry of allowlist) {
|
|
486
|
+
if (entry === "*" || entry === "**") {
|
|
487
|
+
findings.push({
|
|
488
|
+
scanner: "permissions",
|
|
489
|
+
severity: "critical",
|
|
490
|
+
title: "Wildcard allowlist",
|
|
491
|
+
description: "Allowlist contains unrestricted wildcard, allowing all access",
|
|
492
|
+
file: "openclaw.json",
|
|
493
|
+
confidence: 0.95
|
|
494
|
+
});
|
|
495
|
+
} else if (entry.includes("*")) {
|
|
496
|
+
findings.push({
|
|
497
|
+
scanner: "permissions",
|
|
498
|
+
severity: "medium",
|
|
499
|
+
title: "Broad allowlist pattern",
|
|
500
|
+
description: `Allowlist pattern "${entry}" may be too permissive`,
|
|
501
|
+
file: "openclaw.json",
|
|
502
|
+
confidence: 0.6
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return findings;
|
|
507
|
+
}
|
|
508
|
+
function checkGateway(config) {
|
|
509
|
+
const findings = [];
|
|
510
|
+
const gateway = config.gateway;
|
|
511
|
+
if (!gateway) return findings;
|
|
512
|
+
if (gateway.bind === "0.0.0.0" || gateway.bind === "::") {
|
|
513
|
+
findings.push({
|
|
514
|
+
scanner: "permissions",
|
|
515
|
+
severity: "high",
|
|
516
|
+
title: "Gateway binds to all interfaces",
|
|
517
|
+
description: "Gateway is exposed on all network interfaces, should bind to localhost",
|
|
518
|
+
file: "openclaw.json",
|
|
519
|
+
confidence: 0.9
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
if (gateway.auth === false) {
|
|
523
|
+
findings.push({
|
|
524
|
+
scanner: "permissions",
|
|
525
|
+
severity: "high",
|
|
526
|
+
title: "Gateway authentication disabled",
|
|
527
|
+
description: "Gateway has authentication disabled, allowing unauthenticated access",
|
|
528
|
+
file: "openclaw.json",
|
|
529
|
+
confidence: 0.9
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
if (gateway.tls === false && gateway.bind !== "localhost" && gateway.bind !== "127.0.0.1") {
|
|
533
|
+
findings.push({
|
|
534
|
+
scanner: "permissions",
|
|
535
|
+
severity: "medium",
|
|
536
|
+
title: "Gateway TLS disabled",
|
|
537
|
+
description: "Gateway TLS is disabled for non-localhost connections",
|
|
538
|
+
file: "openclaw.json",
|
|
539
|
+
confidence: 0.75
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
return findings;
|
|
543
|
+
}
|
|
544
|
+
async function checkFilePermissions(openclawPath) {
|
|
545
|
+
const findings = [];
|
|
546
|
+
const rootStats = await getFileStats(openclawPath);
|
|
547
|
+
if (rootStats) {
|
|
548
|
+
const mode = rootStats.mode & 511;
|
|
549
|
+
if ((mode & 63) !== 0) {
|
|
550
|
+
findings.push({
|
|
551
|
+
scanner: "permissions",
|
|
552
|
+
severity: "medium",
|
|
553
|
+
title: "OpenClaw directory too permissive",
|
|
554
|
+
description: `Directory permissions ${mode.toString(8)} allow group/other access, recommend 700`,
|
|
555
|
+
file: openclawPath,
|
|
556
|
+
confidence: 0.8
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const credentialsDir = join5(openclawPath, "credentials");
|
|
561
|
+
if (await fileExists(credentialsDir)) {
|
|
562
|
+
const credStats = await getFileStats(credentialsDir);
|
|
563
|
+
if (credStats) {
|
|
564
|
+
const mode = credStats.mode & 511;
|
|
565
|
+
if ((mode & 63) !== 0) {
|
|
566
|
+
findings.push({
|
|
567
|
+
scanner: "permissions",
|
|
568
|
+
severity: "high",
|
|
569
|
+
title: "Credentials directory too permissive",
|
|
570
|
+
description: `Credentials permissions ${mode.toString(8)} allow group/other access, recommend 700`,
|
|
571
|
+
file: "credentials/",
|
|
572
|
+
confidence: 0.9
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return findings;
|
|
578
|
+
}
|
|
579
|
+
async function scanPermissions(openclawPath) {
|
|
580
|
+
const findings = [];
|
|
581
|
+
const configPath = join5(openclawPath, "openclaw.json");
|
|
582
|
+
const config = await readJsonFile(configPath);
|
|
583
|
+
if (config) {
|
|
584
|
+
const sandboxFinding = checkSandboxMode(config);
|
|
585
|
+
if (sandboxFinding) findings.push(sandboxFinding);
|
|
586
|
+
const dmFinding = checkDmPolicy(config);
|
|
587
|
+
if (dmFinding) findings.push(dmFinding);
|
|
588
|
+
findings.push(...checkAllowlist(config));
|
|
589
|
+
findings.push(...checkGateway(config));
|
|
590
|
+
}
|
|
591
|
+
findings.push(...await checkFilePermissions(openclawPath));
|
|
592
|
+
const severityScores = {
|
|
593
|
+
critical: 27.5,
|
|
594
|
+
high: 17.5,
|
|
595
|
+
medium: 7.5,
|
|
596
|
+
low: 2.5
|
|
597
|
+
};
|
|
598
|
+
let penalty = 0;
|
|
599
|
+
for (const finding of findings) {
|
|
600
|
+
penalty += severityScores[finding.severity] * finding.confidence;
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
scanner: "permissions",
|
|
604
|
+
findings,
|
|
605
|
+
penalty: Math.min(penalty, CAP3),
|
|
606
|
+
cap: CAP3
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/scanners/network.ts
|
|
611
|
+
import { join as join6 } from "path";
|
|
612
|
+
import { createConnection } from "net";
|
|
613
|
+
var CAP4 = 10;
|
|
614
|
+
var DEFAULT_PORTS = [18789, 8080, 3e3];
|
|
615
|
+
var PORT_SCAN_TIMEOUT = 1e3;
|
|
616
|
+
async function checkPortOpen(port, host = "localhost") {
|
|
617
|
+
return new Promise((resolve) => {
|
|
618
|
+
const socket = createConnection({ port, host, timeout: PORT_SCAN_TIMEOUT });
|
|
619
|
+
socket.on("connect", () => {
|
|
620
|
+
socket.destroy();
|
|
621
|
+
resolve(true);
|
|
622
|
+
});
|
|
623
|
+
socket.on("timeout", () => {
|
|
624
|
+
socket.destroy();
|
|
625
|
+
resolve(false);
|
|
626
|
+
});
|
|
627
|
+
socket.on("error", () => {
|
|
628
|
+
socket.destroy();
|
|
629
|
+
resolve(false);
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
async function scanOpenPorts() {
|
|
634
|
+
const findings = [];
|
|
635
|
+
for (const port of DEFAULT_PORTS) {
|
|
636
|
+
const isOpen = await checkPortOpen(port);
|
|
637
|
+
if (isOpen) {
|
|
638
|
+
findings.push({
|
|
639
|
+
scanner: "network",
|
|
640
|
+
severity: "low",
|
|
641
|
+
title: `Port ${port} is open`,
|
|
642
|
+
description: `Localhost port ${port} is listening, verify if this is expected`,
|
|
643
|
+
confidence: 0.5
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return findings;
|
|
648
|
+
}
|
|
649
|
+
function analyzeGatewayConfig(config) {
|
|
650
|
+
const findings = [];
|
|
651
|
+
const gateway = config.gateway;
|
|
652
|
+
if (!gateway) return findings;
|
|
653
|
+
const bind = gateway.bind ?? "localhost";
|
|
654
|
+
const isExposed = bind === "0.0.0.0" || bind === "::";
|
|
655
|
+
if (isExposed) {
|
|
656
|
+
if (!gateway.tls) {
|
|
657
|
+
findings.push({
|
|
658
|
+
scanner: "network",
|
|
659
|
+
severity: "high",
|
|
660
|
+
title: "Exposed gateway without TLS",
|
|
661
|
+
description: "Gateway is exposed to network without TLS encryption",
|
|
662
|
+
file: "openclaw.json",
|
|
663
|
+
confidence: 0.9
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
if (!gateway.auth) {
|
|
667
|
+
findings.push({
|
|
668
|
+
scanner: "network",
|
|
669
|
+
severity: "critical",
|
|
670
|
+
title: "Exposed gateway without authentication",
|
|
671
|
+
description: "Gateway is exposed to network without authentication",
|
|
672
|
+
file: "openclaw.json",
|
|
673
|
+
confidence: 0.95
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const port = gateway.port ?? 18789;
|
|
678
|
+
if (port < 1024 && port !== 443 && port !== 80) {
|
|
679
|
+
findings.push({
|
|
680
|
+
scanner: "network",
|
|
681
|
+
severity: "medium",
|
|
682
|
+
title: "Non-standard privileged port",
|
|
683
|
+
description: `Gateway uses privileged port ${port}, requires elevated permissions`,
|
|
684
|
+
file: "openclaw.json",
|
|
685
|
+
confidence: 0.7
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
return findings;
|
|
689
|
+
}
|
|
690
|
+
async function scanNetwork(openclawPath) {
|
|
691
|
+
const findings = [];
|
|
692
|
+
const configPath = join6(openclawPath, "openclaw.json");
|
|
693
|
+
const config = await readJsonFile(configPath);
|
|
694
|
+
if (config) {
|
|
695
|
+
findings.push(...analyzeGatewayConfig(config));
|
|
696
|
+
}
|
|
697
|
+
findings.push(...await scanOpenPorts());
|
|
698
|
+
const severityScores = {
|
|
699
|
+
critical: 27.5,
|
|
700
|
+
high: 17.5,
|
|
701
|
+
medium: 7.5,
|
|
702
|
+
low: 2.5
|
|
703
|
+
};
|
|
704
|
+
let penalty = 0;
|
|
705
|
+
for (const finding of findings) {
|
|
706
|
+
penalty += severityScores[finding.severity] * finding.confidence;
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
scanner: "network",
|
|
710
|
+
findings,
|
|
711
|
+
penalty: Math.min(penalty, CAP4),
|
|
712
|
+
cap: CAP4
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/scanners/index.ts
|
|
717
|
+
async function runAllScanners(options) {
|
|
718
|
+
const { openclawPath } = options;
|
|
719
|
+
const results = await Promise.all([
|
|
720
|
+
scanCredentials(openclawPath),
|
|
721
|
+
scanSkills(openclawPath),
|
|
722
|
+
scanPermissions(openclawPath),
|
|
723
|
+
scanNetwork(openclawPath)
|
|
724
|
+
]);
|
|
725
|
+
return results;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/scoring/index.ts
|
|
729
|
+
function calculateScore(results) {
|
|
730
|
+
const totalPenalty = results.reduce((sum, r) => sum + r.penalty, 0);
|
|
731
|
+
return Math.max(0, Math.round(100 - totalPenalty));
|
|
732
|
+
}
|
|
733
|
+
function determineGrade(score, findings) {
|
|
734
|
+
const hasCritical = findings.some((f) => f.severity === "critical");
|
|
735
|
+
if (hasCritical) {
|
|
736
|
+
if (score >= 75) return "C";
|
|
737
|
+
if (score >= 60) return "C";
|
|
738
|
+
if (score >= 40) return "D";
|
|
739
|
+
return "F";
|
|
740
|
+
}
|
|
741
|
+
if (score >= 90) return "A";
|
|
742
|
+
if (score >= 75) return "B";
|
|
743
|
+
if (score >= 60) return "C";
|
|
744
|
+
if (score >= 40) return "D";
|
|
745
|
+
return "F";
|
|
746
|
+
}
|
|
747
|
+
function buildScanResult(results, openclawPath) {
|
|
748
|
+
const findings = results.flatMap((r) => r.findings);
|
|
749
|
+
const score = calculateScore(results);
|
|
750
|
+
const grade = determineGrade(score, findings);
|
|
751
|
+
return {
|
|
752
|
+
score,
|
|
753
|
+
grade,
|
|
754
|
+
scanners: results,
|
|
755
|
+
findings,
|
|
756
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
757
|
+
openclawPath
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function getExitCode(result) {
|
|
761
|
+
const hasCriticalOrHigh = result.findings.some(
|
|
762
|
+
(f) => f.severity === "critical" || f.severity === "high"
|
|
763
|
+
);
|
|
764
|
+
if (result.score < 75 || hasCriticalOrHigh) {
|
|
765
|
+
return 1;
|
|
766
|
+
}
|
|
767
|
+
return 0;
|
|
768
|
+
}
|
|
769
|
+
function countBySeverity(findings) {
|
|
770
|
+
return {
|
|
771
|
+
critical: findings.filter((f) => f.severity === "critical").length,
|
|
772
|
+
high: findings.filter((f) => f.severity === "high").length,
|
|
773
|
+
medium: findings.filter((f) => f.severity === "medium").length,
|
|
774
|
+
low: findings.filter((f) => f.severity === "low").length
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// src/output/terminal.ts
|
|
779
|
+
import chalk from "chalk";
|
|
780
|
+
import ora from "ora";
|
|
781
|
+
import boxen from "boxen";
|
|
782
|
+
|
|
783
|
+
// src/utils/redact.ts
|
|
784
|
+
function formatFindingLocation(file, line) {
|
|
785
|
+
if (line !== void 0) {
|
|
786
|
+
return `${file}:${line}`;
|
|
787
|
+
}
|
|
788
|
+
return file;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/output/terminal.ts
|
|
792
|
+
var CRAB = "\u{1F980}";
|
|
793
|
+
var GRADE_COLORS = {
|
|
794
|
+
A: chalk.green,
|
|
795
|
+
B: chalk.greenBright,
|
|
796
|
+
C: chalk.yellow,
|
|
797
|
+
D: chalk.hex("#FFA500"),
|
|
798
|
+
F: chalk.red
|
|
799
|
+
};
|
|
800
|
+
var SEVERITY_COLORS = {
|
|
801
|
+
critical: chalk.bgRed.white,
|
|
802
|
+
high: chalk.red,
|
|
803
|
+
medium: chalk.yellow,
|
|
804
|
+
low: chalk.gray
|
|
805
|
+
};
|
|
806
|
+
var SEVERITY_ICONS = {
|
|
807
|
+
critical: "\u{1F6A8}",
|
|
808
|
+
high: "\u26A0\uFE0F",
|
|
809
|
+
medium: "\u{1F7E1}",
|
|
810
|
+
low: "\u2139\uFE0F"
|
|
811
|
+
};
|
|
812
|
+
function createSpinner(text) {
|
|
813
|
+
return ora({
|
|
814
|
+
text,
|
|
815
|
+
spinner: "dots"
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
function printHeader() {
|
|
819
|
+
console.log(
|
|
820
|
+
boxen(
|
|
821
|
+
`${CRAB} ${chalk.bold("CRABB")} ${chalk.dim("Security Scanner for OpenClaw")}`,
|
|
822
|
+
{
|
|
823
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
824
|
+
borderColor: "cyan",
|
|
825
|
+
borderStyle: "round"
|
|
826
|
+
}
|
|
827
|
+
)
|
|
828
|
+
);
|
|
829
|
+
console.log();
|
|
830
|
+
}
|
|
831
|
+
function printScore(result) {
|
|
832
|
+
const gradeColor = GRADE_COLORS[result.grade];
|
|
833
|
+
const counts = countBySeverity(result.findings);
|
|
834
|
+
const scoreDisplay = boxen(
|
|
835
|
+
[
|
|
836
|
+
`${CRAB} ${chalk.bold("CRABB SCORE")}`,
|
|
837
|
+
"",
|
|
838
|
+
` ${gradeColor(chalk.bold(`${result.score}`))} ${chalk.dim("/ 100")}`,
|
|
839
|
+
` Grade: ${gradeColor(chalk.bold(result.grade))}`,
|
|
840
|
+
"",
|
|
841
|
+
chalk.dim("\u2500".repeat(24)),
|
|
842
|
+
"",
|
|
843
|
+
`${SEVERITY_ICONS.critical} Critical: ${counts.critical > 0 ? chalk.red(counts.critical) : chalk.dim("0")}`,
|
|
844
|
+
`${SEVERITY_ICONS.high} High: ${counts.high > 0 ? chalk.red(counts.high) : chalk.dim("0")}`,
|
|
845
|
+
`${SEVERITY_ICONS.medium} Medium: ${counts.medium > 0 ? chalk.yellow(counts.medium) : chalk.dim("0")}`,
|
|
846
|
+
`${SEVERITY_ICONS.low} Low: ${counts.low > 0 ? chalk.gray(counts.low) : chalk.dim("0")}`
|
|
847
|
+
].join("\n"),
|
|
848
|
+
{
|
|
849
|
+
padding: 1,
|
|
850
|
+
borderColor: result.grade === "A" || result.grade === "B" ? "green" : result.grade === "C" ? "yellow" : "red",
|
|
851
|
+
borderStyle: "round"
|
|
852
|
+
}
|
|
853
|
+
);
|
|
854
|
+
console.log(scoreDisplay);
|
|
855
|
+
console.log();
|
|
856
|
+
}
|
|
857
|
+
function printFindings(findings) {
|
|
858
|
+
if (findings.length === 0) {
|
|
859
|
+
console.log(chalk.green(`${CRAB} No security issues found!`));
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const sortedFindings = [...findings].sort((a, b) => {
|
|
863
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
864
|
+
return order[a.severity] - order[b.severity];
|
|
865
|
+
});
|
|
866
|
+
console.log(chalk.bold("Findings:\n"));
|
|
867
|
+
for (const finding of sortedFindings) {
|
|
868
|
+
const severityColor = SEVERITY_COLORS[finding.severity];
|
|
869
|
+
const icon = SEVERITY_ICONS[finding.severity];
|
|
870
|
+
console.log(`${icon} ${severityColor(finding.severity.toUpperCase())} ${chalk.bold(finding.title)}`);
|
|
871
|
+
console.log(` ${chalk.dim(finding.description)}`);
|
|
872
|
+
if (finding.file) {
|
|
873
|
+
console.log(` ${chalk.cyan(formatFindingLocation(finding.file, finding.line))}`);
|
|
874
|
+
}
|
|
875
|
+
console.log();
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function printScannerSummary(result) {
|
|
879
|
+
console.log(chalk.bold("Scanner Summary:\n"));
|
|
880
|
+
for (const scanner of result.scanners) {
|
|
881
|
+
const penaltyColor = scanner.penalty > 0 ? chalk.red : chalk.green;
|
|
882
|
+
const bar = createProgressBar(scanner.cap - scanner.penalty, scanner.cap);
|
|
883
|
+
console.log(
|
|
884
|
+
` ${scanner.scanner.padEnd(12)} ${bar} ${penaltyColor(`-${scanner.penalty.toFixed(1)}`)} ${chalk.dim(`/ ${scanner.cap}`)}`
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
console.log();
|
|
888
|
+
}
|
|
889
|
+
function createProgressBar(value, max, width = 20) {
|
|
890
|
+
const percentage = Math.max(0, Math.min(1, value / max));
|
|
891
|
+
const filled = Math.round(percentage * width);
|
|
892
|
+
const empty = width - filled;
|
|
893
|
+
const color = percentage >= 0.75 ? chalk.green : percentage >= 0.5 ? chalk.yellow : chalk.red;
|
|
894
|
+
return color("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty));
|
|
895
|
+
}
|
|
896
|
+
function printError(message) {
|
|
897
|
+
console.error(chalk.red(`\u2717 Error: ${message}`));
|
|
898
|
+
}
|
|
899
|
+
function printSuccess(message) {
|
|
900
|
+
console.log(chalk.green(`\u2713 ${message}`));
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/output/json.ts
|
|
904
|
+
function formatJsonOutput(result) {
|
|
905
|
+
return JSON.stringify(result, null, 2);
|
|
906
|
+
}
|
|
907
|
+
function buildSharePayload(result) {
|
|
908
|
+
const counts = countBySeverity(result.findings);
|
|
909
|
+
return {
|
|
910
|
+
score: result.score,
|
|
911
|
+
grade: result.grade,
|
|
912
|
+
scannerSummary: result.scanners.map((s) => ({
|
|
913
|
+
scanner: s.scanner,
|
|
914
|
+
findingsCount: s.findings.length,
|
|
915
|
+
penalty: s.penalty
|
|
916
|
+
})),
|
|
917
|
+
criticalCount: counts.critical,
|
|
918
|
+
highCount: counts.high,
|
|
919
|
+
mediumCount: counts.medium,
|
|
920
|
+
lowCount: counts.low,
|
|
921
|
+
timestamp: result.timestamp
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/share/index.ts
|
|
926
|
+
var SHARE_API_URL = "https://crabb.ai/api/share";
|
|
927
|
+
async function shareResult(result) {
|
|
928
|
+
const payload = buildSharePayload(result);
|
|
929
|
+
const response = await fetch(SHARE_API_URL, {
|
|
930
|
+
method: "POST",
|
|
931
|
+
headers: {
|
|
932
|
+
"Content-Type": "application/json"
|
|
933
|
+
},
|
|
934
|
+
body: JSON.stringify(payload)
|
|
935
|
+
});
|
|
936
|
+
if (!response.ok) {
|
|
937
|
+
throw new Error(`Failed to share result: ${response.status} ${response.statusText}`);
|
|
938
|
+
}
|
|
939
|
+
return response.json();
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// src/cli.ts
|
|
943
|
+
function printHelp() {
|
|
944
|
+
console.log(`
|
|
945
|
+
\u{1F980} CRABB - Security Scanner for OpenClaw AI Agents
|
|
946
|
+
|
|
947
|
+
Usage: crabb [options]
|
|
948
|
+
|
|
949
|
+
Options:
|
|
950
|
+
-p, --path <dir> Path to OpenClaw directory (default: ~/.openclaw/)
|
|
951
|
+
-j, --json Output results as JSON
|
|
952
|
+
-s, --share Share score card to crabb.ai
|
|
953
|
+
--no-color Disable colored output
|
|
954
|
+
-h, --help Show this help message
|
|
955
|
+
-v, --version Show version number
|
|
956
|
+
|
|
957
|
+
Examples:
|
|
958
|
+
crabb # Scan default OpenClaw installation
|
|
959
|
+
crabb --path ./my-openclaw # Scan custom directory
|
|
960
|
+
crabb --json # Output as JSON
|
|
961
|
+
crabb --share # Create shareable score card
|
|
962
|
+
|
|
963
|
+
Exit codes:
|
|
964
|
+
0 - Score >= 75, no Critical/High findings
|
|
965
|
+
1 - Score < 75 or Critical/High findings present
|
|
966
|
+
2 - Scan failed (IO error, OpenClaw not found)
|
|
967
|
+
`);
|
|
968
|
+
}
|
|
969
|
+
function printVersion() {
|
|
970
|
+
console.log("crabb v0.1.0");
|
|
971
|
+
}
|
|
972
|
+
async function main() {
|
|
973
|
+
let options;
|
|
974
|
+
try {
|
|
975
|
+
const { values } = parseArgs({
|
|
976
|
+
options: {
|
|
977
|
+
path: { type: "string", short: "p" },
|
|
978
|
+
json: { type: "boolean", short: "j", default: false },
|
|
979
|
+
share: { type: "boolean", short: "s", default: false },
|
|
980
|
+
"no-color": { type: "boolean", default: false },
|
|
981
|
+
help: { type: "boolean", short: "h", default: false },
|
|
982
|
+
version: { type: "boolean", short: "v", default: false }
|
|
983
|
+
},
|
|
984
|
+
strict: true,
|
|
985
|
+
allowPositionals: false
|
|
986
|
+
});
|
|
987
|
+
if (values.help) {
|
|
988
|
+
printHelp();
|
|
989
|
+
exit(0);
|
|
990
|
+
}
|
|
991
|
+
if (values.version) {
|
|
992
|
+
printVersion();
|
|
993
|
+
exit(0);
|
|
994
|
+
}
|
|
995
|
+
options = {
|
|
996
|
+
path: values.path ?? getDefaultOpenClawPath(),
|
|
997
|
+
json: values.json ?? false,
|
|
998
|
+
share: values.share ?? false,
|
|
999
|
+
noColor: values["no-color"] ?? false
|
|
1000
|
+
};
|
|
1001
|
+
} catch (err) {
|
|
1002
|
+
printError(`Invalid arguments: ${err instanceof Error ? err.message : String(err)}`);
|
|
1003
|
+
printHelp();
|
|
1004
|
+
exit(2);
|
|
1005
|
+
}
|
|
1006
|
+
if (options.noColor) {
|
|
1007
|
+
process.env["FORCE_COLOR"] = "0";
|
|
1008
|
+
}
|
|
1009
|
+
if (!options.json) {
|
|
1010
|
+
printHeader();
|
|
1011
|
+
}
|
|
1012
|
+
if (!await fileExists(options.path)) {
|
|
1013
|
+
printError(`OpenClaw directory not found: ${options.path}`);
|
|
1014
|
+
exit(2);
|
|
1015
|
+
}
|
|
1016
|
+
const spinner = options.json ? null : createSpinner("Scanning OpenClaw configuration...");
|
|
1017
|
+
spinner?.start();
|
|
1018
|
+
try {
|
|
1019
|
+
const scannerResults = await runAllScanners({ openclawPath: options.path });
|
|
1020
|
+
const result = buildScanResult(scannerResults, options.path);
|
|
1021
|
+
spinner?.stop();
|
|
1022
|
+
if (options.json) {
|
|
1023
|
+
console.log(formatJsonOutput(result));
|
|
1024
|
+
} else {
|
|
1025
|
+
printScore(result);
|
|
1026
|
+
printScannerSummary(result);
|
|
1027
|
+
printFindings(result.findings);
|
|
1028
|
+
}
|
|
1029
|
+
if (options.share) {
|
|
1030
|
+
const shareSpinner = options.json ? null : createSpinner("Sharing score card...");
|
|
1031
|
+
shareSpinner?.start();
|
|
1032
|
+
try {
|
|
1033
|
+
const shareResponse = await shareResult(result);
|
|
1034
|
+
shareSpinner?.stop();
|
|
1035
|
+
if (options.json) {
|
|
1036
|
+
console.log(JSON.stringify({ shared: shareResponse }, null, 2));
|
|
1037
|
+
} else {
|
|
1038
|
+
printSuccess(`Score card shared: ${shareResponse.url}`);
|
|
1039
|
+
console.log(` Delete token: ${shareResponse.deleteToken}`);
|
|
1040
|
+
}
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
shareSpinner?.stop();
|
|
1043
|
+
printError(`Failed to share: ${err instanceof Error ? err.message : String(err)}`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
const exitCode = getExitCode(result);
|
|
1047
|
+
exit(exitCode);
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
spinner?.stop();
|
|
1050
|
+
printError(`Scan failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1051
|
+
exit(2);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
main();
|
|
1055
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/config/paths.ts","../src/scanners/credentials.ts","../src/utils/fs.ts","../src/scanners/skills.ts","../src/scanners/permissions.ts","../src/scanners/network.ts","../src/scanners/index.ts","../src/scoring/index.ts","../src/output/terminal.ts","../src/utils/redact.ts","../src/output/json.ts","../src/share/index.ts"],"sourcesContent":["import { parseArgs } from 'node:util';\nimport { exit } from 'node:process';\nimport type { CliOptions } from './types/index.js';\nimport { getDefaultOpenClawPath } from './config/paths.js';\nimport { runAllScanners } from './scanners/index.js';\nimport { buildScanResult, getExitCode } from './scoring/index.js';\nimport {\n printHeader,\n printScore,\n printFindings,\n printScannerSummary,\n printError,\n printSuccess,\n createSpinner,\n} from './output/terminal.js';\nimport { formatJsonOutput } from './output/json.js';\nimport { shareResult } from './share/index.js';\nimport { fileExists } from './utils/fs.js';\n\nfunction parseCliArgs(): CliOptions {\n const { values } = parseArgs({\n options: {\n path: {\n type: 'string',\n short: 'p',\n },\n json: {\n type: 'boolean',\n short: 'j',\n default: false,\n },\n share: {\n type: 'boolean',\n short: 's',\n default: false,\n },\n 'no-color': {\n type: 'boolean',\n default: false,\n },\n help: {\n type: 'boolean',\n short: 'h',\n default: false,\n },\n version: {\n type: 'boolean',\n short: 'v',\n default: false,\n },\n },\n strict: true,\n allowPositionals: false,\n });\n\n return {\n path: values.path ?? getDefaultOpenClawPath(),\n json: values.json ?? false,\n share: values.share ?? false,\n noColor: values['no-color'] ?? false,\n };\n}\n\nfunction printHelp() {\n console.log(`\n\\u{1F980} CRABB - Security Scanner for OpenClaw AI Agents\n\nUsage: crabb [options]\n\nOptions:\n -p, --path <dir> Path to OpenClaw directory (default: ~/.openclaw/)\n -j, --json Output results as JSON\n -s, --share Share score card to crabb.ai\n --no-color Disable colored output\n -h, --help Show this help message\n -v, --version Show version number\n\nExamples:\n crabb # Scan default OpenClaw installation\n crabb --path ./my-openclaw # Scan custom directory\n crabb --json # Output as JSON\n crabb --share # Create shareable score card\n\nExit codes:\n 0 - Score >= 75, no Critical/High findings\n 1 - Score < 75 or Critical/High findings present\n 2 - Scan failed (IO error, OpenClaw not found)\n`);\n}\n\nfunction printVersion() {\n console.log('crabb v0.1.0');\n}\n\nasync function main() {\n let options: CliOptions;\n\n try {\n const { values } = parseArgs({\n options: {\n path: { type: 'string', short: 'p' },\n json: { type: 'boolean', short: 'j', default: false },\n share: { type: 'boolean', short: 's', default: false },\n 'no-color': { type: 'boolean', default: false },\n help: { type: 'boolean', short: 'h', default: false },\n version: { type: 'boolean', short: 'v', default: false },\n },\n strict: true,\n allowPositionals: false,\n });\n\n if (values.help) {\n printHelp();\n exit(0);\n }\n\n if (values.version) {\n printVersion();\n exit(0);\n }\n\n options = {\n path: values.path ?? getDefaultOpenClawPath(),\n json: values.json ?? false,\n share: values.share ?? false,\n noColor: values['no-color'] ?? false,\n };\n } catch (err) {\n printError(`Invalid arguments: ${err instanceof Error ? err.message : String(err)}`);\n printHelp();\n exit(2);\n }\n\n if (options.noColor) {\n process.env['FORCE_COLOR'] = '0';\n }\n\n if (!options.json) {\n printHeader();\n }\n\n if (!await fileExists(options.path)) {\n printError(`OpenClaw directory not found: ${options.path}`);\n exit(2);\n }\n\n const spinner = options.json ? null : createSpinner('Scanning OpenClaw configuration...');\n spinner?.start();\n\n try {\n const scannerResults = await runAllScanners({ openclawPath: options.path });\n const result = buildScanResult(scannerResults, options.path);\n\n spinner?.stop();\n\n if (options.json) {\n console.log(formatJsonOutput(result));\n } else {\n printScore(result);\n printScannerSummary(result);\n printFindings(result.findings);\n }\n\n if (options.share) {\n const shareSpinner = options.json ? null : createSpinner('Sharing score card...');\n shareSpinner?.start();\n\n try {\n const shareResponse = await shareResult(result);\n shareSpinner?.stop();\n\n if (options.json) {\n console.log(JSON.stringify({ shared: shareResponse }, null, 2));\n } else {\n printSuccess(`Score card shared: ${shareResponse.url}`);\n console.log(` Delete token: ${shareResponse.deleteToken}`);\n }\n } catch (err) {\n shareSpinner?.stop();\n printError(`Failed to share: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n const exitCode = getExitCode(result);\n exit(exitCode);\n } catch (err) {\n spinner?.stop();\n printError(`Scan failed: ${err instanceof Error ? err.message : String(err)}`);\n exit(2);\n }\n}\n\nmain();\n","import { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nexport function getDefaultOpenClawPath(): string {\n return join(homedir(), '.openclaw');\n}\n\nexport function getOpenClawPaths(basePath: string) {\n return {\n root: basePath,\n config: join(basePath, 'openclaw.json'),\n credentials: join(basePath, 'credentials'),\n agents: join(basePath, 'agents'),\n skills: join(basePath, 'skills'),\n workspaceSkills: join(basePath, 'workspace', 'skills'),\n };\n}\n\nexport const SCAN_FILE_PATTERNS = {\n credentials: [\n 'openclaw.json',\n 'credentials/**/*',\n 'agents/*/agent/auth-profiles.json',\n 'agents/*/sessions/*.jsonl',\n '.env',\n '.env.*',\n ],\n skills: [\n 'skills/**/*.md',\n 'skills/**/SKILL.md',\n 'workspace/skills/**/*.md',\n ],\n} as const;\n","import { join } from 'node:path';\nimport type { Finding, ScannerResult } from '../types/index.js';\nimport { readTextFile, walkDirectory, fileExists } from '../utils/fs.js';\n\nconst CAP = 40;\n\ninterface CredentialPattern {\n name: string;\n pattern: RegExp;\n severity: 'critical' | 'high' | 'medium';\n confidence: number;\n}\n\nconst CREDENTIAL_PATTERNS: CredentialPattern[] = [\n // Anthropic\n {\n name: 'Anthropic API Key',\n pattern: /sk-ant-[a-zA-Z0-9-]{20,}/g,\n severity: 'critical',\n confidence: 0.95,\n },\n // OpenAI\n {\n name: 'OpenAI API Key',\n pattern: /sk-[a-zA-Z0-9]{20,}(?!-ant)/g,\n severity: 'critical',\n confidence: 0.9,\n },\n // Discord\n {\n name: 'Discord Bot Token',\n pattern: /[MN][A-Za-z\\d]{23,}\\.[\\w-]{6}\\.[\\w-]{27,}/g,\n severity: 'critical',\n confidence: 0.95,\n },\n // Telegram\n {\n name: 'Telegram Bot Token',\n pattern: /\\d{8,10}:[A-Za-z0-9_-]{35}/g,\n severity: 'critical',\n confidence: 0.9,\n },\n // AWS\n {\n name: 'AWS Access Key ID',\n pattern: /AKIA[0-9A-Z]{16}/g,\n severity: 'critical',\n confidence: 0.95,\n },\n {\n name: 'AWS Secret Access Key',\n pattern: /(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])/g,\n severity: 'high',\n confidence: 0.6,\n },\n // GitHub\n {\n name: 'GitHub Personal Access Token',\n pattern: /ghp_[a-zA-Z0-9]{36}/g,\n severity: 'critical',\n confidence: 0.95,\n },\n {\n name: 'GitHub OAuth Token',\n pattern: /gho_[a-zA-Z0-9]{36}/g,\n severity: 'critical',\n confidence: 0.95,\n },\n {\n name: 'GitHub Fine-grained PAT',\n pattern: /github_pat_[a-zA-Z0-9_]{22,}/g,\n severity: 'critical',\n confidence: 0.95,\n },\n // Slack\n {\n name: 'Slack Bot Token',\n pattern: /xoxb-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}/g,\n severity: 'critical',\n confidence: 0.95,\n },\n {\n name: 'Slack User Token',\n pattern: /xoxp-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}/g,\n severity: 'critical',\n confidence: 0.95,\n },\n // Stripe\n {\n name: 'Stripe Secret Key',\n pattern: /sk_live_[0-9a-zA-Z]{24,}/g,\n severity: 'critical',\n confidence: 0.95,\n },\n {\n name: 'Stripe Test Key',\n pattern: /sk_test_[0-9a-zA-Z]{24,}/g,\n severity: 'medium',\n confidence: 0.9,\n },\n // Generic patterns\n {\n name: 'Generic API Key',\n pattern: /(?:api[_-]?key|apikey|api[_-]?token)\\s*[=:]\\s*[\"']?([a-zA-Z0-9_-]{20,})[\"']?/gi,\n severity: 'high',\n confidence: 0.7,\n },\n {\n name: 'Generic Secret',\n pattern: /(?:secret|password|passwd|pwd)\\s*[=:]\\s*[\"']?([a-zA-Z0-9_!@#$%^&*-]{8,})[\"']?/gi,\n severity: 'high',\n confidence: 0.6,\n },\n // Private Keys\n {\n name: 'Private Key',\n pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,\n severity: 'critical',\n confidence: 0.99,\n },\n];\n\nconst PLACEHOLDER_PATTERNS = [\n /^your[-_]?api[-_]?key[-_]?here$/i,\n /^xxx+$/i,\n /^placeholder$/i,\n /^example$/i,\n /^test[-_]?key$/i,\n /^\\$\\{[^}]+\\}$/,\n /^<[^>]+>$/,\n /^{{[^}]+}}$/,\n /^%[^%]+%$/,\n /^INSERT[-_]?YOUR[-_]?KEY$/i,\n /^REPLACE[-_]?ME$/i,\n /^TODO$/i,\n /^changeme$/i,\n];\n\nfunction isPlaceholder(value: string): boolean {\n return PLACEHOLDER_PATTERNS.some(pattern => pattern.test(value.trim()));\n}\n\nfunction extractValue(match: RegExpMatchArray): string {\n return match[1] ?? match[0];\n}\n\nasync function scanFile(\n filePath: string,\n relativePath: string\n): Promise<Finding[]> {\n const content = await readTextFile(filePath);\n if (!content) return [];\n\n const findings: Finding[] = [];\n const lines = content.split('\\n');\n\n for (let lineNum = 0; lineNum < lines.length; lineNum++) {\n const line = lines[lineNum]!;\n\n for (const credPattern of CREDENTIAL_PATTERNS) {\n credPattern.pattern.lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = credPattern.pattern.exec(line)) !== null) {\n const value = extractValue(match);\n\n if (isPlaceholder(value)) {\n continue;\n }\n\n if (value.length < 8) {\n continue;\n }\n\n findings.push({\n scanner: 'credentials',\n severity: credPattern.severity,\n title: credPattern.name,\n description: `Detected ${credPattern.name} in configuration file`,\n file: relativePath,\n line: lineNum + 1,\n confidence: credPattern.confidence,\n });\n }\n }\n }\n\n return findings;\n}\n\nexport async function scanCredentials(openclawPath: string): Promise<ScannerResult> {\n const findings: Finding[] = [];\n\n const filesToScan = [\n { path: join(openclawPath, 'openclaw.json'), relative: 'openclaw.json' },\n { path: join(openclawPath, '.env'), relative: '.env' },\n ];\n\n const credentialsDir = join(openclawPath, 'credentials');\n if (await fileExists(credentialsDir)) {\n for await (const file of walkDirectory(credentialsDir, openclawPath)) {\n filesToScan.push({ path: file.path, relative: file.relativePath });\n }\n }\n\n const agentsDir = join(openclawPath, 'agents');\n if (await fileExists(agentsDir)) {\n for await (const file of walkDirectory(agentsDir, openclawPath)) {\n if (\n file.relativePath.includes('auth-profiles.json') ||\n file.relativePath.endsWith('.jsonl') ||\n file.relativePath.endsWith('.json')\n ) {\n filesToScan.push({ path: file.path, relative: file.relativePath });\n }\n }\n }\n\n for (const file of filesToScan) {\n if (await fileExists(file.path)) {\n const fileFindings = await scanFile(file.path, file.relative);\n findings.push(...fileFindings);\n }\n }\n\n const severityScores = {\n critical: 27.5,\n high: 17.5,\n medium: 7.5,\n low: 2.5,\n };\n\n let penalty = 0;\n for (const finding of findings) {\n penalty += severityScores[finding.severity] * finding.confidence;\n }\n\n return {\n scanner: 'credentials',\n findings,\n penalty: Math.min(penalty, CAP),\n cap: CAP,\n };\n}\n","import { readFile, readdir, stat, access } from 'node:fs/promises';\nimport { join, relative } from 'node:path';\nimport { constants } from 'node:fs';\n\nexport async function fileExists(path: string): Promise<boolean> {\n try {\n await access(path, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function readTextFile(path: string): Promise<string | null> {\n try {\n return await readFile(path, 'utf-8');\n } catch {\n return null;\n }\n}\n\nexport async function readJsonFile<T>(path: string): Promise<T | null> {\n const content = await readTextFile(path);\n if (!content) return null;\n try {\n return JSON.parse(content) as T;\n } catch {\n return null;\n }\n}\n\nexport async function getFileStats(path: string) {\n try {\n return await stat(path);\n } catch {\n return null;\n }\n}\n\nexport async function* walkDirectory(\n dir: string,\n basePath: string = dir\n): AsyncGenerator<{ path: string; relativePath: string }> {\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n const relativePath = relative(basePath, fullPath);\n\n if (entry.isDirectory()) {\n yield* walkDirectory(fullPath, basePath);\n } else if (entry.isFile()) {\n yield { path: fullPath, relativePath };\n }\n }\n } catch {\n // Directory doesn't exist or not accessible\n }\n}\n\nexport function matchesPattern(filePath: string, pattern: string): boolean {\n // Simple glob matching\n const regexPattern = pattern\n .replace(/\\./g, '\\\\.')\n .replace(/\\*\\*/g, '{{GLOBSTAR}}')\n .replace(/\\*/g, '[^/]*')\n .replace(/\\{\\{GLOBSTAR\\}\\}/g, '.*');\n\n return new RegExp(`^${regexPattern}$`).test(filePath);\n}\n\nexport function matchesAnyPattern(filePath: string, patterns: readonly string[]): boolean {\n return patterns.some(pattern => matchesPattern(filePath, pattern));\n}\n","import { join } from 'node:path';\nimport type { Finding, ScannerResult } from '../types/index.js';\nimport { readTextFile, walkDirectory, fileExists } from '../utils/fs.js';\n\nconst CAP = 30;\n\ninterface SkillPattern {\n name: string;\n pattern: RegExp;\n severity: 'critical' | 'high' | 'medium' | 'low';\n confidence: number;\n description: string;\n}\n\nconst SKILL_PATTERNS: SkillPattern[] = [\n // Critical: Remote code execution\n {\n name: 'Curl piped to shell',\n pattern: /curl\\s+[^|]*\\|\\s*(?:bash|sh|zsh)/gi,\n severity: 'critical',\n confidence: 0.95,\n description: 'Downloading and executing remote scripts is extremely dangerous',\n },\n {\n name: 'Wget piped to shell',\n pattern: /wget\\s+[^|]*\\|\\s*(?:bash|sh|zsh)/gi,\n severity: 'critical',\n confidence: 0.95,\n description: 'Downloading and executing remote scripts is extremely dangerous',\n },\n {\n name: 'Code execution function',\n pattern: /\\b(?:eval|Function)\\s*\\(/gi,\n severity: 'critical',\n confidence: 0.8,\n description: 'Dynamic code execution can lead to arbitrary code execution',\n },\n {\n name: 'System command execution',\n pattern: /\\b(?:os\\.system|subprocess\\.call|subprocess\\.run|child_process)\\s*\\(/gi,\n severity: 'critical',\n confidence: 0.85,\n description: 'System command execution may allow arbitrary command injection',\n },\n // Critical: Sensitive file access\n {\n name: 'SSH key access',\n pattern: /\\.ssh\\/(?:id_rsa|id_ed25519|id_dsa|authorized_keys)/gi,\n severity: 'critical',\n confidence: 0.9,\n description: 'Accessing SSH keys can compromise authentication',\n },\n {\n name: 'Password file access',\n pattern: /\\/etc\\/(?:passwd|shadow)/gi,\n severity: 'critical',\n confidence: 0.95,\n description: 'Accessing system password files is a security risk',\n },\n // High: Data exfiltration patterns\n {\n name: 'POST request with data',\n pattern: /(?:curl|wget|fetch|axios|request)\\s+.*(?:-d|--data|POST)/gi,\n severity: 'high',\n confidence: 0.7,\n description: 'Outbound data transmission may indicate exfiltration',\n },\n {\n name: 'Base64 encode and send',\n pattern: /base64.*(?:curl|wget|fetch|send|post)/gi,\n severity: 'high',\n confidence: 0.75,\n description: 'Encoding and transmitting data may indicate exfiltration',\n },\n {\n name: 'Environment variable dump',\n pattern: /\\benv\\b|\\$ENV|\\bprocess\\.env\\b|\\bos\\.environ\\b/gi,\n severity: 'high',\n confidence: 0.6,\n description: 'Environment access may expose sensitive configuration',\n },\n // Medium: File system access\n {\n name: 'Unrestricted file read',\n pattern: /(?:fs\\.readFile|open\\s*\\(|read\\s*\\().*(?:\\*|\\.\\.)/gi,\n severity: 'medium',\n confidence: 0.6,\n description: 'Broad file read patterns may access unintended files',\n },\n {\n name: 'Home directory access',\n pattern: /~\\/|\\/home\\/|\\$HOME/gi,\n severity: 'medium',\n confidence: 0.5,\n description: 'Accessing user home directory may expose sensitive data',\n },\n {\n name: 'Recursive directory operations',\n pattern: /(?:-r|--recursive|walk|glob\\*\\*)/gi,\n severity: 'medium',\n confidence: 0.4,\n description: 'Recursive operations may access more files than intended',\n },\n // Low: Suspicious but not necessarily harmful\n {\n name: 'Network connection',\n pattern: /(?:socket|connect|http|https):\\/\\//gi,\n severity: 'low',\n confidence: 0.3,\n description: 'Network connections should be reviewed for necessity',\n },\n {\n name: 'File write operation',\n pattern: /(?:fs\\.writeFile|open\\s*\\([^)]*['\"]\\s*w|>+\\s*[a-zA-Z])/gi,\n severity: 'low',\n confidence: 0.4,\n description: 'File write operations should be reviewed',\n },\n];\n\nasync function scanSkillFile(\n filePath: string,\n relativePath: string\n): Promise<Finding[]> {\n const content = await readTextFile(filePath);\n if (!content) return [];\n\n const findings: Finding[] = [];\n const lines = content.split('\\n');\n\n for (let lineNum = 0; lineNum < lines.length; lineNum++) {\n const line = lines[lineNum]!;\n\n for (const skillPattern of SKILL_PATTERNS) {\n skillPattern.pattern.lastIndex = 0;\n\n if (skillPattern.pattern.test(line)) {\n findings.push({\n scanner: 'skills',\n severity: skillPattern.severity,\n title: skillPattern.name,\n description: skillPattern.description,\n file: relativePath,\n line: lineNum + 1,\n confidence: skillPattern.confidence,\n });\n }\n }\n }\n\n return findings;\n}\n\nexport async function scanSkills(openclawPath: string): Promise<ScannerResult> {\n const findings: Finding[] = [];\n\n const skillsDirs = [\n join(openclawPath, 'skills'),\n join(openclawPath, 'workspace', 'skills'),\n ];\n\n for (const skillsDir of skillsDirs) {\n if (await fileExists(skillsDir)) {\n for await (const file of walkDirectory(skillsDir, openclawPath)) {\n if (file.relativePath.endsWith('.md')) {\n const fileFindings = await scanSkillFile(file.path, file.relativePath);\n findings.push(...fileFindings);\n }\n }\n }\n }\n\n const severityScores = {\n critical: 27.5,\n high: 17.5,\n medium: 7.5,\n low: 2.5,\n };\n\n let penalty = 0;\n for (const finding of findings) {\n penalty += severityScores[finding.severity] * finding.confidence;\n }\n\n return {\n scanner: 'skills',\n findings,\n penalty: Math.min(penalty, CAP),\n cap: CAP,\n };\n}\n","import { join } from 'node:path';\nimport type { Finding, ScannerResult } from '../types/index.js';\nimport { readJsonFile, getFileStats, fileExists } from '../utils/fs.js';\n\nconst CAP = 20;\n\ninterface OpenClawConfig {\n sandbox?: {\n mode?: 'strict' | 'permissive' | 'disabled';\n };\n dmPolicy?: 'allow' | 'deny' | 'ask';\n allowlist?: string[];\n gateway?: {\n bind?: string;\n auth?: boolean;\n tls?: boolean;\n };\n [key: string]: unknown;\n}\n\nfunction checkSandboxMode(config: OpenClawConfig): Finding | null {\n const mode = config.sandbox?.mode;\n\n if (mode === 'disabled') {\n return {\n scanner: 'permissions',\n severity: 'critical',\n title: 'Sandbox disabled',\n description: 'Agent sandbox is completely disabled, allowing unrestricted system access',\n file: 'openclaw.json',\n confidence: 0.95,\n };\n }\n\n if (mode === 'permissive') {\n return {\n scanner: 'permissions',\n severity: 'high',\n title: 'Sandbox in permissive mode',\n description: 'Agent sandbox is in permissive mode, may allow unintended access',\n file: 'openclaw.json',\n confidence: 0.85,\n };\n }\n\n return null;\n}\n\nfunction checkDmPolicy(config: OpenClawConfig): Finding | null {\n const policy = config.dmPolicy;\n\n if (policy === 'allow') {\n return {\n scanner: 'permissions',\n severity: 'medium',\n title: 'DM policy allows all',\n description: 'Direct message policy allows all messages without filtering',\n file: 'openclaw.json',\n confidence: 0.7,\n };\n }\n\n return null;\n}\n\nfunction checkAllowlist(config: OpenClawConfig): Finding[] {\n const findings: Finding[] = [];\n const allowlist = config.allowlist ?? [];\n\n for (const entry of allowlist) {\n if (entry === '*' || entry === '**') {\n findings.push({\n scanner: 'permissions',\n severity: 'critical',\n title: 'Wildcard allowlist',\n description: 'Allowlist contains unrestricted wildcard, allowing all access',\n file: 'openclaw.json',\n confidence: 0.95,\n });\n } else if (entry.includes('*')) {\n findings.push({\n scanner: 'permissions',\n severity: 'medium',\n title: 'Broad allowlist pattern',\n description: `Allowlist pattern \"${entry}\" may be too permissive`,\n file: 'openclaw.json',\n confidence: 0.6,\n });\n }\n }\n\n return findings;\n}\n\nfunction checkGateway(config: OpenClawConfig): Finding[] {\n const findings: Finding[] = [];\n const gateway = config.gateway;\n\n if (!gateway) return findings;\n\n if (gateway.bind === '0.0.0.0' || gateway.bind === '::') {\n findings.push({\n scanner: 'permissions',\n severity: 'high',\n title: 'Gateway binds to all interfaces',\n description: 'Gateway is exposed on all network interfaces, should bind to localhost',\n file: 'openclaw.json',\n confidence: 0.9,\n });\n }\n\n if (gateway.auth === false) {\n findings.push({\n scanner: 'permissions',\n severity: 'high',\n title: 'Gateway authentication disabled',\n description: 'Gateway has authentication disabled, allowing unauthenticated access',\n file: 'openclaw.json',\n confidence: 0.9,\n });\n }\n\n if (gateway.tls === false && gateway.bind !== 'localhost' && gateway.bind !== '127.0.0.1') {\n findings.push({\n scanner: 'permissions',\n severity: 'medium',\n title: 'Gateway TLS disabled',\n description: 'Gateway TLS is disabled for non-localhost connections',\n file: 'openclaw.json',\n confidence: 0.75,\n });\n }\n\n return findings;\n}\n\nasync function checkFilePermissions(openclawPath: string): Promise<Finding[]> {\n const findings: Finding[] = [];\n\n const rootStats = await getFileStats(openclawPath);\n if (rootStats) {\n const mode = rootStats.mode & 0o777;\n if ((mode & 0o077) !== 0) {\n findings.push({\n scanner: 'permissions',\n severity: 'medium',\n title: 'OpenClaw directory too permissive',\n description: `Directory permissions ${mode.toString(8)} allow group/other access, recommend 700`,\n file: openclawPath,\n confidence: 0.8,\n });\n }\n }\n\n const credentialsDir = join(openclawPath, 'credentials');\n if (await fileExists(credentialsDir)) {\n const credStats = await getFileStats(credentialsDir);\n if (credStats) {\n const mode = credStats.mode & 0o777;\n if ((mode & 0o077) !== 0) {\n findings.push({\n scanner: 'permissions',\n severity: 'high',\n title: 'Credentials directory too permissive',\n description: `Credentials permissions ${mode.toString(8)} allow group/other access, recommend 700`,\n file: 'credentials/',\n confidence: 0.9,\n });\n }\n }\n }\n\n return findings;\n}\n\nexport async function scanPermissions(openclawPath: string): Promise<ScannerResult> {\n const findings: Finding[] = [];\n\n const configPath = join(openclawPath, 'openclaw.json');\n const config = await readJsonFile<OpenClawConfig>(configPath);\n\n if (config) {\n const sandboxFinding = checkSandboxMode(config);\n if (sandboxFinding) findings.push(sandboxFinding);\n\n const dmFinding = checkDmPolicy(config);\n if (dmFinding) findings.push(dmFinding);\n\n findings.push(...checkAllowlist(config));\n findings.push(...checkGateway(config));\n }\n\n findings.push(...await checkFilePermissions(openclawPath));\n\n const severityScores = {\n critical: 27.5,\n high: 17.5,\n medium: 7.5,\n low: 2.5,\n };\n\n let penalty = 0;\n for (const finding of findings) {\n penalty += severityScores[finding.severity] * finding.confidence;\n }\n\n return {\n scanner: 'permissions',\n findings,\n penalty: Math.min(penalty, CAP),\n cap: CAP,\n };\n}\n","import { join } from 'node:path';\nimport { createConnection } from 'node:net';\nimport type { Finding, ScannerResult } from '../types/index.js';\nimport { readJsonFile } from '../utils/fs.js';\n\nconst CAP = 10;\n\ninterface GatewayConfig {\n bind?: string;\n port?: number;\n auth?: boolean;\n tls?: boolean;\n}\n\ninterface OpenClawConfig {\n gateway?: GatewayConfig;\n [key: string]: unknown;\n}\n\nconst DEFAULT_PORTS = [18789, 8080, 3000];\nconst PORT_SCAN_TIMEOUT = 1000;\n\nasync function checkPortOpen(port: number, host: string = 'localhost'): Promise<boolean> {\n return new Promise((resolve) => {\n const socket = createConnection({ port, host, timeout: PORT_SCAN_TIMEOUT });\n\n socket.on('connect', () => {\n socket.destroy();\n resolve(true);\n });\n\n socket.on('timeout', () => {\n socket.destroy();\n resolve(false);\n });\n\n socket.on('error', () => {\n socket.destroy();\n resolve(false);\n });\n });\n}\n\nasync function scanOpenPorts(): Promise<Finding[]> {\n const findings: Finding[] = [];\n\n for (const port of DEFAULT_PORTS) {\n const isOpen = await checkPortOpen(port);\n if (isOpen) {\n findings.push({\n scanner: 'network',\n severity: 'low',\n title: `Port ${port} is open`,\n description: `Localhost port ${port} is listening, verify if this is expected`,\n confidence: 0.5,\n });\n }\n }\n\n return findings;\n}\n\nfunction analyzeGatewayConfig(config: OpenClawConfig): Finding[] {\n const findings: Finding[] = [];\n const gateway = config.gateway;\n\n if (!gateway) return findings;\n\n const bind = gateway.bind ?? 'localhost';\n const isExposed = bind === '0.0.0.0' || bind === '::';\n\n if (isExposed) {\n if (!gateway.tls) {\n findings.push({\n scanner: 'network',\n severity: 'high',\n title: 'Exposed gateway without TLS',\n description: 'Gateway is exposed to network without TLS encryption',\n file: 'openclaw.json',\n confidence: 0.9,\n });\n }\n\n if (!gateway.auth) {\n findings.push({\n scanner: 'network',\n severity: 'critical',\n title: 'Exposed gateway without authentication',\n description: 'Gateway is exposed to network without authentication',\n file: 'openclaw.json',\n confidence: 0.95,\n });\n }\n }\n\n const port = gateway.port ?? 18789;\n if (port < 1024 && port !== 443 && port !== 80) {\n findings.push({\n scanner: 'network',\n severity: 'medium',\n title: 'Non-standard privileged port',\n description: `Gateway uses privileged port ${port}, requires elevated permissions`,\n file: 'openclaw.json',\n confidence: 0.7,\n });\n }\n\n return findings;\n}\n\nexport async function scanNetwork(openclawPath: string): Promise<ScannerResult> {\n const findings: Finding[] = [];\n\n const configPath = join(openclawPath, 'openclaw.json');\n const config = await readJsonFile<OpenClawConfig>(configPath);\n\n if (config) {\n findings.push(...analyzeGatewayConfig(config));\n }\n\n findings.push(...await scanOpenPorts());\n\n const severityScores = {\n critical: 27.5,\n high: 17.5,\n medium: 7.5,\n low: 2.5,\n };\n\n let penalty = 0;\n for (const finding of findings) {\n penalty += severityScores[finding.severity] * finding.confidence;\n }\n\n return {\n scanner: 'network',\n findings,\n penalty: Math.min(penalty, CAP),\n cap: CAP,\n };\n}\n","import type { ScannerResult, Finding } from '../types/index.js';\nimport { scanCredentials } from './credentials.js';\nimport { scanSkills } from './skills.js';\nimport { scanPermissions } from './permissions.js';\nimport { scanNetwork } from './network.js';\n\nexport interface ScanOptions {\n openclawPath: string;\n}\n\nexport async function runAllScanners(options: ScanOptions): Promise<ScannerResult[]> {\n const { openclawPath } = options;\n\n const results = await Promise.all([\n scanCredentials(openclawPath),\n scanSkills(openclawPath),\n scanPermissions(openclawPath),\n scanNetwork(openclawPath),\n ]);\n\n return results;\n}\n\nexport function getAllFindings(results: ScannerResult[]): Finding[] {\n return results.flatMap(r => r.findings);\n}\n\nexport function getTotalPenalty(results: ScannerResult[]): number {\n return results.reduce((sum, r) => sum + r.penalty, 0);\n}\n\nexport { scanCredentials, scanSkills, scanPermissions, scanNetwork };\n","import type { ScannerResult, Finding, Grade, ScanResult } from '../types/index.js';\n\nexport function calculateScore(results: ScannerResult[]): number {\n const totalPenalty = results.reduce((sum, r) => sum + r.penalty, 0);\n return Math.max(0, Math.round(100 - totalPenalty));\n}\n\nexport function determineGrade(score: number, findings: Finding[]): Grade {\n const hasCritical = findings.some(f => f.severity === 'critical');\n\n if (hasCritical) {\n if (score >= 75) return 'C';\n if (score >= 60) return 'C';\n if (score >= 40) return 'D';\n return 'F';\n }\n\n if (score >= 90) return 'A';\n if (score >= 75) return 'B';\n if (score >= 60) return 'C';\n if (score >= 40) return 'D';\n return 'F';\n}\n\nexport function buildScanResult(\n results: ScannerResult[],\n openclawPath: string\n): ScanResult {\n const findings = results.flatMap(r => r.findings);\n const score = calculateScore(results);\n const grade = determineGrade(score, findings);\n\n return {\n score,\n grade,\n scanners: results,\n findings,\n timestamp: new Date().toISOString(),\n openclawPath,\n };\n}\n\nexport function getExitCode(result: ScanResult): number {\n const hasCriticalOrHigh = result.findings.some(\n f => f.severity === 'critical' || f.severity === 'high'\n );\n\n if (result.score < 75 || hasCriticalOrHigh) {\n return 1;\n }\n\n return 0;\n}\n\nexport function countBySeverity(findings: Finding[]) {\n return {\n critical: findings.filter(f => f.severity === 'critical').length,\n high: findings.filter(f => f.severity === 'high').length,\n medium: findings.filter(f => f.severity === 'medium').length,\n low: findings.filter(f => f.severity === 'low').length,\n };\n}\n","import chalk from 'chalk';\nimport ora from 'ora';\nimport boxen from 'boxen';\nimport type { ScanResult, Finding, Grade } from '../types/index.js';\nimport { countBySeverity } from '../scoring/index.js';\nimport { formatFindingLocation } from '../utils/redact.js';\n\nconst CRAB = '\\u{1F980}';\n\nconst GRADE_COLORS: Record<Grade, (text: string) => string> = {\n A: chalk.green,\n B: chalk.greenBright,\n C: chalk.yellow,\n D: chalk.hex('#FFA500'),\n F: chalk.red,\n};\n\nconst SEVERITY_COLORS = {\n critical: chalk.bgRed.white,\n high: chalk.red,\n medium: chalk.yellow,\n low: chalk.gray,\n};\n\nconst SEVERITY_ICONS = {\n critical: '\\u{1F6A8}',\n high: '\\u26A0\\uFE0F',\n medium: '\\u{1F7E1}',\n low: '\\u2139\\uFE0F',\n};\n\nexport function createSpinner(text: string) {\n return ora({\n text,\n spinner: 'dots',\n });\n}\n\nexport function printHeader() {\n console.log(\n boxen(\n `${CRAB} ${chalk.bold('CRABB')} ${chalk.dim('Security Scanner for OpenClaw')}`,\n {\n padding: { top: 0, bottom: 0, left: 1, right: 1 },\n borderColor: 'cyan',\n borderStyle: 'round',\n }\n )\n );\n console.log();\n}\n\nexport function printScore(result: ScanResult) {\n const gradeColor = GRADE_COLORS[result.grade];\n const counts = countBySeverity(result.findings);\n\n const scoreDisplay = boxen(\n [\n `${CRAB} ${chalk.bold('CRABB SCORE')}`,\n '',\n ` ${gradeColor(chalk.bold(`${result.score}`))} ${chalk.dim('/ 100')}`,\n ` Grade: ${gradeColor(chalk.bold(result.grade))}`,\n '',\n chalk.dim('─'.repeat(24)),\n '',\n `${SEVERITY_ICONS.critical} Critical: ${counts.critical > 0 ? chalk.red(counts.critical) : chalk.dim('0')}`,\n `${SEVERITY_ICONS.high} High: ${counts.high > 0 ? chalk.red(counts.high) : chalk.dim('0')}`,\n `${SEVERITY_ICONS.medium} Medium: ${counts.medium > 0 ? chalk.yellow(counts.medium) : chalk.dim('0')}`,\n `${SEVERITY_ICONS.low} Low: ${counts.low > 0 ? chalk.gray(counts.low) : chalk.dim('0')}`,\n ].join('\\n'),\n {\n padding: 1,\n borderColor: result.grade === 'A' || result.grade === 'B' ? 'green' : result.grade === 'C' ? 'yellow' : 'red',\n borderStyle: 'round',\n }\n );\n\n console.log(scoreDisplay);\n console.log();\n}\n\nexport function printFindings(findings: Finding[]) {\n if (findings.length === 0) {\n console.log(chalk.green(`${CRAB} No security issues found!`));\n return;\n }\n\n const sortedFindings = [...findings].sort((a, b) => {\n const order = { critical: 0, high: 1, medium: 2, low: 3 };\n return order[a.severity] - order[b.severity];\n });\n\n console.log(chalk.bold('Findings:\\n'));\n\n for (const finding of sortedFindings) {\n const severityColor = SEVERITY_COLORS[finding.severity];\n const icon = SEVERITY_ICONS[finding.severity];\n\n console.log(`${icon} ${severityColor(finding.severity.toUpperCase())} ${chalk.bold(finding.title)}`);\n console.log(` ${chalk.dim(finding.description)}`);\n if (finding.file) {\n console.log(` ${chalk.cyan(formatFindingLocation(finding.file, finding.line))}`);\n }\n console.log();\n }\n}\n\nexport function printScannerSummary(result: ScanResult) {\n console.log(chalk.bold('Scanner Summary:\\n'));\n\n for (const scanner of result.scanners) {\n const penaltyColor = scanner.penalty > 0 ? chalk.red : chalk.green;\n const bar = createProgressBar(scanner.cap - scanner.penalty, scanner.cap);\n\n console.log(\n ` ${scanner.scanner.padEnd(12)} ${bar} ${penaltyColor(`-${scanner.penalty.toFixed(1)}`)} ${chalk.dim(`/ ${scanner.cap}`)}`\n );\n }\n\n console.log();\n}\n\nfunction createProgressBar(value: number, max: number, width: number = 20): string {\n const percentage = Math.max(0, Math.min(1, value / max));\n const filled = Math.round(percentage * width);\n const empty = width - filled;\n\n const color = percentage >= 0.75 ? chalk.green : percentage >= 0.5 ? chalk.yellow : chalk.red;\n\n return color('\\u2588'.repeat(filled)) + chalk.dim('\\u2591'.repeat(empty));\n}\n\nexport function printError(message: string) {\n console.error(chalk.red(`\\u2717 Error: ${message}`));\n}\n\nexport function printSuccess(message: string) {\n console.log(chalk.green(`\\u2713 ${message}`));\n}\n","/**\n * Redaction utilities - NEVER output real secrets\n */\n\nconst REDACT_PATTERNS = [\n // API Keys with known prefixes\n /sk-[a-zA-Z0-9]{20,}/g,\n /sk-ant-[a-zA-Z0-9-]{20,}/g,\n /xoxb-[a-zA-Z0-9-]+/g,\n /xoxp-[a-zA-Z0-9-]+/g,\n /ghp_[a-zA-Z0-9]{36}/g,\n /gho_[a-zA-Z0-9]{36}/g,\n /github_pat_[a-zA-Z0-9_]{22,}/g,\n\n // Generic secrets\n /[a-zA-Z0-9_-]{32,}/g,\n];\n\nexport function redactValue(value: string): string {\n if (value.length <= 8) {\n return '*'.repeat(value.length);\n }\n\n const visibleStart = 4;\n const visibleEnd = 4;\n const redactedLength = value.length - visibleStart - visibleEnd;\n\n return `${value.slice(0, visibleStart)}${'*'.repeat(Math.max(4, redactedLength))}${value.slice(-visibleEnd)}`;\n}\n\nexport function redactLine(line: string): string {\n let result = line;\n\n for (const pattern of REDACT_PATTERNS) {\n result = result.replace(pattern, (match) => redactValue(match));\n }\n\n return result;\n}\n\nexport function formatFindingLocation(file: string, line?: number): string {\n if (line !== undefined) {\n return `${file}:${line}`;\n }\n return file;\n}\n","import type { ScanResult, SharePayload } from '../types/index.js';\nimport { countBySeverity } from '../scoring/index.js';\n\nexport function formatJsonOutput(result: ScanResult): string {\n return JSON.stringify(result, null, 2);\n}\n\nexport function buildSharePayload(result: ScanResult): SharePayload {\n const counts = countBySeverity(result.findings);\n\n return {\n score: result.score,\n grade: result.grade,\n scannerSummary: result.scanners.map(s => ({\n scanner: s.scanner,\n findingsCount: s.findings.length,\n penalty: s.penalty,\n })),\n criticalCount: counts.critical,\n highCount: counts.high,\n mediumCount: counts.medium,\n lowCount: counts.low,\n timestamp: result.timestamp,\n };\n}\n","import type { ScanResult, ShareResponse } from '../types/index.js';\nimport { buildSharePayload } from '../output/json.js';\n\nconst SHARE_API_URL = 'https://crabb.ai/api/share';\n\nexport async function shareResult(result: ScanResult): Promise<ShareResponse> {\n const payload = buildSharePayload(result);\n\n const response = await fetch(SHARE_API_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(payload),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to share result: ${response.status} ${response.statusText}`);\n }\n\n return response.json() as Promise<ShareResponse>;\n}\n\nexport async function shareResultMock(result: ScanResult): Promise<ShareResponse> {\n const payload = buildSharePayload(result);\n\n const mockId = `mock-${Date.now().toString(36)}`;\n\n return {\n id: mockId,\n url: `https://crabb.ai/score/${mockId}`,\n deleteToken: `delete-${mockId}`,\n };\n}\n"],"mappings":"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","names":["join","join","join","join","CAP","join","join","CAP","join","join","CAP","join"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "getcrabb",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI security scanner for OpenClaw AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"crabb": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"dev": "tsx src/cli.ts",
|
|
16
|
+
"lint": "tsc --noEmit",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"clean": "rm -rf dist"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"security",
|
|
23
|
+
"scanner",
|
|
24
|
+
"openclaw",
|
|
25
|
+
"ai",
|
|
26
|
+
"agents",
|
|
27
|
+
"cli"
|
|
28
|
+
],
|
|
29
|
+
"author": "",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"boxen": "^8.0.1",
|
|
33
|
+
"chalk": "^5.3.0",
|
|
34
|
+
"ora": "^8.1.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.10.0",
|
|
38
|
+
"tsup": "^8.3.5",
|
|
39
|
+
"tsx": "^4.19.2",
|
|
40
|
+
"typescript": "^5.3.0",
|
|
41
|
+
"vitest": "^2.1.0"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
}
|
|
46
|
+
}
|