agent-security-scanner-mcp 3.10.2 ā 3.11.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 +39 -2
- package/analyzer.py +4 -0
- package/index.js +5 -0
- package/package.json +2 -1
- package/skills/clawhub/CLAWPROOF.md +448 -0
- package/src/cli/scan-clawhub-full.js +518 -0
- package/src/cli/scan-clawhub-safe.js +393 -0
- package/src/cli/scan-clawhub.js +308 -0
- package/src/daemon-client.js +1 -1
- package/src/tools/scan-security.js +23 -1
- package/src/tools/scan-skill-prompt.js +547 -0
- package/src/utils.js +1 -1
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scan-clawhub-safe.js
|
|
5
|
+
*
|
|
6
|
+
* SAFE ClawHub Scanner - No Code Execution
|
|
7
|
+
*
|
|
8
|
+
* Strategy:
|
|
9
|
+
* 1. Fetch skill list from ClawHub CLI (safe - just metadata)
|
|
10
|
+
* 2. For each skill, scrape SKILL.md from web page (read-only)
|
|
11
|
+
* 3. Write SKILL.md to temp files
|
|
12
|
+
* 4. Scan with our engine
|
|
13
|
+
* 5. Clean up temp files
|
|
14
|
+
*
|
|
15
|
+
* SECURITY: No npm install, no code execution, read-only operations
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { exec } from 'child_process';
|
|
19
|
+
import { promisify } from 'util';
|
|
20
|
+
import fs from 'fs/promises';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
import { fileURLToPath } from 'url';
|
|
23
|
+
import https from 'https';
|
|
24
|
+
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname = path.dirname(__filename);
|
|
27
|
+
|
|
28
|
+
const execAsync = promisify(exec);
|
|
29
|
+
|
|
30
|
+
// Configuration
|
|
31
|
+
const SCAN_DIR = path.join(process.cwd(), 'clawhub-scan-safe');
|
|
32
|
+
const SKILLS_DIR = path.join(SCAN_DIR, 'skills');
|
|
33
|
+
const RESULTS_FILE = path.join(SCAN_DIR, 'results.json');
|
|
34
|
+
const REPORT_FILE = path.join(SCAN_DIR, 'report.json');
|
|
35
|
+
|
|
36
|
+
const CONCURRENT_DOWNLOADS = 10;
|
|
37
|
+
const CONCURRENT_SCANS = 10;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fetch from URL (promisified)
|
|
41
|
+
*/
|
|
42
|
+
function fetchUrl(url) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
https.get(url, (res) => {
|
|
45
|
+
let data = '';
|
|
46
|
+
res.on('data', chunk => data += chunk);
|
|
47
|
+
res.on('end', () => resolve({ statusCode: res.statusCode, body: data }));
|
|
48
|
+
}).on('error', reject);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Fetch all skills from ClawHub (SAFE - just metadata)
|
|
54
|
+
*/
|
|
55
|
+
async function fetchAllSkills() {
|
|
56
|
+
console.log('š„ Fetching all skills from ClawHub...');
|
|
57
|
+
|
|
58
|
+
const allSkills = [];
|
|
59
|
+
const seenSlugs = new Set();
|
|
60
|
+
const sortOrders = ['newest', 'downloads', 'installsAllTime'];
|
|
61
|
+
|
|
62
|
+
for (const sortOrder of sortOrders) {
|
|
63
|
+
try {
|
|
64
|
+
const { stdout } = await execAsync(
|
|
65
|
+
`clawhub explore --limit 200 --sort ${sortOrder} --json 2>&1`
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const jsonStr = stdout.split('\n').slice(1).join('\n');
|
|
69
|
+
const data = JSON.parse(jsonStr);
|
|
70
|
+
|
|
71
|
+
if (data.items && Array.isArray(data.items)) {
|
|
72
|
+
for (const skill of data.items) {
|
|
73
|
+
if (!seenSlugs.has(skill.slug)) {
|
|
74
|
+
seenSlugs.add(skill.slug);
|
|
75
|
+
allSkills.push(skill);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(` ā Fetched ${data.items.length} skills (sort: ${sortOrder})`);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error(` ā Error fetching skills (sort: ${sortOrder}):`, error.message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log(`\nā
Total unique skills found: ${allSkills.length}\n`);
|
|
87
|
+
return allSkills;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Download SKILL.md content from web (SAFE - read-only HTTP GET)
|
|
92
|
+
*/
|
|
93
|
+
async function downloadSkillContent(slug, owner) {
|
|
94
|
+
const skillDir = path.join(SKILLS_DIR, slug);
|
|
95
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
96
|
+
|
|
97
|
+
// Check if already downloaded
|
|
98
|
+
try {
|
|
99
|
+
await fs.access(skillFile);
|
|
100
|
+
const content = await fs.readFile(skillFile, 'utf8');
|
|
101
|
+
return { slug, skillFile, content, cached: true };
|
|
102
|
+
} catch {
|
|
103
|
+
// Not cached
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// Try to fetch from ClawHub web page
|
|
108
|
+
// The skill page URL format is: https://clawhub.ai/{owner}/{slug}
|
|
109
|
+
const skillUrl = `https://clawhub.ai/${owner}/${slug}`;
|
|
110
|
+
|
|
111
|
+
const { statusCode, body } = await fetchUrl(skillUrl);
|
|
112
|
+
|
|
113
|
+
if (statusCode !== 200) {
|
|
114
|
+
return { slug, skillFile: null, error: `HTTP ${statusCode}` };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Extract SKILL.md content from HTML
|
|
118
|
+
// ClawHub likely embeds the markdown in the page
|
|
119
|
+
// We'll look for markdown content in the HTML
|
|
120
|
+
|
|
121
|
+
// Simple extraction: look for markdown code blocks or pre tags
|
|
122
|
+
// This is a heuristic - adjust based on actual ClawHub HTML structure
|
|
123
|
+
let skillContent = '';
|
|
124
|
+
|
|
125
|
+
// Try to extract markdown from HTML (basic approach)
|
|
126
|
+
const markdownMatch = body.match(/<pre[^>]*>([\s\S]*?)<\/pre>/i);
|
|
127
|
+
if (markdownMatch) {
|
|
128
|
+
skillContent = markdownMatch[1]
|
|
129
|
+
.replace(/</g, '<')
|
|
130
|
+
.replace(/>/g, '>')
|
|
131
|
+
.replace(/&/g, '&')
|
|
132
|
+
.replace(/"/g, '"')
|
|
133
|
+
.replace(/'/g, "'");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// If no content found, try another pattern
|
|
137
|
+
if (!skillContent) {
|
|
138
|
+
// Look for the skill description/summary as fallback
|
|
139
|
+
const descMatch = body.match(/<meta\s+name="description"\s+content="([^"]+)"/i);
|
|
140
|
+
if (descMatch) {
|
|
141
|
+
skillContent = `# ${slug}\n\n${descMatch[1]}`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!skillContent || skillContent.length < 50) {
|
|
146
|
+
return { slug, skillFile: null, error: 'Could not extract SKILL.md from web page' };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Create directory and write file
|
|
150
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
151
|
+
await fs.writeFile(skillFile, skillContent, 'utf8');
|
|
152
|
+
|
|
153
|
+
return { slug, skillFile, content: skillContent, cached: false };
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return { slug, skillFile: null, error: error.message };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Alternative: Use our own skill file as template
|
|
161
|
+
* Download from GitHub if skill author published it there
|
|
162
|
+
*/
|
|
163
|
+
async function downloadFromGitHub(slug, owner) {
|
|
164
|
+
try {
|
|
165
|
+
// Common patterns: username/skillname, skillname-skill, etc.
|
|
166
|
+
const possibleRepos = [
|
|
167
|
+
`${slug}`,
|
|
168
|
+
`${slug}-skill`,
|
|
169
|
+
`openclaw-${slug}`,
|
|
170
|
+
`${slug.replace(/-/g, '_')}`
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
for (const repo of possibleRepos) {
|
|
174
|
+
const url = `https://raw.githubusercontent.com/${owner}/${repo}/main/SKILL.md`;
|
|
175
|
+
const { statusCode, body } = await fetchUrl(url);
|
|
176
|
+
|
|
177
|
+
if (statusCode === 200 && body.length > 100) {
|
|
178
|
+
return body;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return null;
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Scan a skill
|
|
190
|
+
*/
|
|
191
|
+
async function scanSkill(slug, skillFile) {
|
|
192
|
+
if (!skillFile) {
|
|
193
|
+
return {
|
|
194
|
+
slug,
|
|
195
|
+
grade: 'F',
|
|
196
|
+
error: 'Skill file not available'
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const scannerPath = path.join(__dirname, '..', '..', 'index.js');
|
|
202
|
+
const { stdout } = await execAsync(
|
|
203
|
+
`node "${scannerPath}" scan-skill "${skillFile}" --verbosity compact`,
|
|
204
|
+
{ timeout: 120000 }
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const result = JSON.parse(stdout);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
slug,
|
|
211
|
+
grade: result.grade || 'F',
|
|
212
|
+
findings: result.findings || [],
|
|
213
|
+
findingsCount: result.findings_count || 0,
|
|
214
|
+
recommendation: result.recommendation
|
|
215
|
+
};
|
|
216
|
+
} catch (error) {
|
|
217
|
+
return {
|
|
218
|
+
slug,
|
|
219
|
+
grade: 'F',
|
|
220
|
+
error: error.message,
|
|
221
|
+
findings: []
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Batch download
|
|
228
|
+
*/
|
|
229
|
+
async function downloadSkillsBatch(skills) {
|
|
230
|
+
console.log(`š¦ Downloading ${skills.length} skill SKILL.md files (read-only, safe)...\n`);
|
|
231
|
+
|
|
232
|
+
const results = [];
|
|
233
|
+
let completed = 0;
|
|
234
|
+
|
|
235
|
+
for (let i = 0; i < skills.length; i += CONCURRENT_DOWNLOADS) {
|
|
236
|
+
const batch = skills.slice(i, i + CONCURRENT_DOWNLOADS);
|
|
237
|
+
const promises = batch.map(skill =>
|
|
238
|
+
downloadSkillContent(skill.slug, skill.owner?.handle || 'unknown')
|
|
239
|
+
);
|
|
240
|
+
const batchResults = await Promise.all(promises);
|
|
241
|
+
|
|
242
|
+
results.push(...batchResults);
|
|
243
|
+
completed += batch.length;
|
|
244
|
+
|
|
245
|
+
const progress = ((completed / skills.length) * 100).toFixed(1);
|
|
246
|
+
const cached = batchResults.filter(r => r.cached).length;
|
|
247
|
+
const successful = batchResults.filter(r => r.skillFile !== null).length;
|
|
248
|
+
|
|
249
|
+
console.log(` [${progress}%] ${completed}/${skills.length} (${successful} ok, ${cached} cached)`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const successful = results.filter(r => r.skillFile !== null).length;
|
|
253
|
+
const failed = results.length - successful;
|
|
254
|
+
|
|
255
|
+
console.log(`\nā
Download complete: ${successful} successful, ${failed} failed\n`);
|
|
256
|
+
return results;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Batch scan
|
|
261
|
+
*/
|
|
262
|
+
async function scanSkillsBatch(downloadedSkills) {
|
|
263
|
+
console.log(`š Scanning ${downloadedSkills.length} skills...\n`);
|
|
264
|
+
|
|
265
|
+
const results = [];
|
|
266
|
+
let completed = 0;
|
|
267
|
+
|
|
268
|
+
for (let i = 0; i < downloadedSkills.length; i += CONCURRENT_SCANS) {
|
|
269
|
+
const batch = downloadedSkills.slice(i, i + CONCURRENT_SCANS);
|
|
270
|
+
const promises = batch.map(({ slug, skillFile }) => scanSkill(slug, skillFile));
|
|
271
|
+
const batchResults = await Promise.all(promises);
|
|
272
|
+
|
|
273
|
+
results.push(...batchResults);
|
|
274
|
+
completed += batch.length;
|
|
275
|
+
|
|
276
|
+
const progress = ((completed / downloadedSkills.length) * 100).toFixed(1);
|
|
277
|
+
console.log(` [${progress}%] Scanned ${completed}/${downloadedSkills.length}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
console.log(`\nā
Scan complete!\n`);
|
|
281
|
+
return results;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Generate report
|
|
286
|
+
*/
|
|
287
|
+
function generateReport(scanResults) {
|
|
288
|
+
const gradeDistribution = { A: 0, B: 0, C: 0, D: 0, F: 0 };
|
|
289
|
+
const totalFindings = { critical: 0, warning: 0, info: 0 };
|
|
290
|
+
const topIssues = {};
|
|
291
|
+
|
|
292
|
+
for (const result of scanResults) {
|
|
293
|
+
gradeDistribution[result.grade] = (gradeDistribution[result.grade] || 0) + 1;
|
|
294
|
+
|
|
295
|
+
if (result.findings && Array.isArray(result.findings)) {
|
|
296
|
+
for (const finding of result.findings) {
|
|
297
|
+
const severity = finding.severity?.toLowerCase() || 'info';
|
|
298
|
+
if (severity === 'critical' || severity === 'error') {
|
|
299
|
+
totalFindings.critical++;
|
|
300
|
+
} else if (severity === 'warning') {
|
|
301
|
+
totalFindings.warning++;
|
|
302
|
+
} else {
|
|
303
|
+
totalFindings.info++;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const ruleId = finding.rule_id || finding.ruleId || 'unknown';
|
|
307
|
+
topIssues[ruleId] = (topIssues[ruleId] || 0) + 1;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const topIssuesSorted = Object.entries(topIssues)
|
|
313
|
+
.sort((a, b) => b[1] - a[1])
|
|
314
|
+
.slice(0, 20)
|
|
315
|
+
.map(([rule, count]) => ({ rule, count }));
|
|
316
|
+
|
|
317
|
+
const totalSkills = scanResults.length;
|
|
318
|
+
const vulnerableSkills = scanResults.filter(r => r.findingsCount > 0).length;
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
summary: {
|
|
322
|
+
totalSkills,
|
|
323
|
+
vulnerableSkills,
|
|
324
|
+
vulnerabilityRate: ((vulnerableSkills / totalSkills) * 100).toFixed(1) + '%',
|
|
325
|
+
gradeDistribution,
|
|
326
|
+
totalFindings
|
|
327
|
+
},
|
|
328
|
+
topIssues: topIssuesSorted,
|
|
329
|
+
scannedAt: new Date().toISOString(),
|
|
330
|
+
scannerVersion: '3.10.3',
|
|
331
|
+
scanMethod: 'safe-web-scraping'
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Main
|
|
337
|
+
*/
|
|
338
|
+
async function main() {
|
|
339
|
+
console.log('š”ļø ClawHub Security Scanner (SAFE MODE)\n');
|
|
340
|
+
console.log('ā'.repeat(60));
|
|
341
|
+
console.log('SECURITY: Read-only, no code execution, web scraping only\n');
|
|
342
|
+
console.log('ā'.repeat(60) + '\n');
|
|
343
|
+
|
|
344
|
+
await fs.mkdir(SKILLS_DIR, { recursive: true });
|
|
345
|
+
|
|
346
|
+
const skills = await fetchAllSkills();
|
|
347
|
+
if (skills.length === 0) {
|
|
348
|
+
console.error('ā No skills found');
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const downloadedSkills = await downloadSkillsBatch(skills);
|
|
353
|
+
const validSkills = downloadedSkills.filter(s => s.skillFile !== null);
|
|
354
|
+
|
|
355
|
+
if (validSkills.length === 0) {
|
|
356
|
+
console.error('ā No skills downloaded successfully');
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const scanResults = await scanSkillsBatch(validSkills);
|
|
361
|
+
const report = generateReport(scanResults);
|
|
362
|
+
|
|
363
|
+
await fs.writeFile(RESULTS_FILE, JSON.stringify(scanResults, null, 2));
|
|
364
|
+
await fs.writeFile(REPORT_FILE, JSON.stringify(report, null, 2));
|
|
365
|
+
|
|
366
|
+
console.log('š Results Summary\n');
|
|
367
|
+
console.log('ā'.repeat(60));
|
|
368
|
+
console.log(`Total skills scanned: ${report.summary.totalSkills}`);
|
|
369
|
+
console.log(`Vulnerable skills: ${report.summary.vulnerableSkills} (${report.summary.vulnerabilityRate})`);
|
|
370
|
+
console.log(`\nGrade Distribution:`);
|
|
371
|
+
console.log(` A: ${report.summary.gradeDistribution.A}`);
|
|
372
|
+
console.log(` B: ${report.summary.gradeDistribution.B}`);
|
|
373
|
+
console.log(` C: ${report.summary.gradeDistribution.C}`);
|
|
374
|
+
console.log(` D: ${report.summary.gradeDistribution.D}`);
|
|
375
|
+
console.log(` F: ${report.summary.gradeDistribution.F}`);
|
|
376
|
+
console.log(`\nTotal Findings:`);
|
|
377
|
+
console.log(` Critical: ${report.summary.totalFindings.critical}`);
|
|
378
|
+
console.log(` Warning: ${report.summary.totalFindings.warning}`);
|
|
379
|
+
console.log(` Info: ${report.summary.totalFindings.info}`);
|
|
380
|
+
console.log(`\nTop 10 Issues:`);
|
|
381
|
+
report.topIssues.slice(0, 10).forEach((issue, i) => {
|
|
382
|
+
console.log(` ${i + 1}. ${issue.rule} (${issue.count} occurrences)`);
|
|
383
|
+
});
|
|
384
|
+
console.log('\n' + 'ā'.repeat(60));
|
|
385
|
+
console.log(`\nā
Results saved to:`);
|
|
386
|
+
console.log(` ${RESULTS_FILE}`);
|
|
387
|
+
console.log(` ${REPORT_FILE}\n`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
main().catch(error => {
|
|
391
|
+
console.error('\nā Fatal error:', error);
|
|
392
|
+
process.exit(1);
|
|
393
|
+
});
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scan-clawhub-v2.js
|
|
5
|
+
*
|
|
6
|
+
* Batch scan all ClawHub skills - REVISED STRATEGY
|
|
7
|
+
*
|
|
8
|
+
* Strategy:
|
|
9
|
+
* 1. Use ClawHub CLI to list all skills
|
|
10
|
+
* 2. Install each skill to temp directory using `clawhub install`
|
|
11
|
+
* 3. Scan SKILL.md files from installed skills
|
|
12
|
+
* 4. Generate A-F grades and aggregate findings
|
|
13
|
+
* 5. Output JSON report + stats
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { exec } from 'child_process';
|
|
17
|
+
import { promisify } from 'util';
|
|
18
|
+
import fs from 'fs/promises';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = path.dirname(__filename);
|
|
24
|
+
|
|
25
|
+
const execAsync = promisify(exec);
|
|
26
|
+
|
|
27
|
+
// Configuration
|
|
28
|
+
const SCAN_DIR = path.join(process.cwd(), 'clawhub-scan');
|
|
29
|
+
const SKILLS_DIR = path.join(SCAN_DIR, 'skills-installed');
|
|
30
|
+
const RESULTS_FILE = path.join(SCAN_DIR, 'results.json');
|
|
31
|
+
const REPORT_FILE = path.join(SCAN_DIR, 'report.json');
|
|
32
|
+
|
|
33
|
+
const CONCURRENT_DOWNLOADS = 5; // Conservative to avoid rate limiting
|
|
34
|
+
const CONCURRENT_SCANS = 10;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fetch all skills from ClawHub
|
|
38
|
+
*/
|
|
39
|
+
async function fetchAllSkills() {
|
|
40
|
+
console.log('š„ Fetching all skills from ClawHub...');
|
|
41
|
+
|
|
42
|
+
const allSkills = [];
|
|
43
|
+
const seenSlugs = new Set();
|
|
44
|
+
const sortOrders = ['newest', 'downloads', 'installsAllTime'];
|
|
45
|
+
|
|
46
|
+
for (const sortOrder of sortOrders) {
|
|
47
|
+
try {
|
|
48
|
+
const { stdout } = await execAsync(
|
|
49
|
+
`clawhub explore --limit 200 --sort ${sortOrder} --json 2>&1`
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const jsonStr = stdout.split('\n').slice(1).join('\n');
|
|
53
|
+
const data = JSON.parse(jsonStr);
|
|
54
|
+
|
|
55
|
+
if (data.items && Array.isArray(data.items)) {
|
|
56
|
+
for (const skill of data.items) {
|
|
57
|
+
if (!seenSlugs.has(skill.slug)) {
|
|
58
|
+
seenSlugs.add(skill.slug);
|
|
59
|
+
allSkills.push(skill);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(` ā Fetched ${data.items.length} skills (sort: ${sortOrder})`);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(` ā Error fetching skills (sort: ${sortOrder}):`, error.message);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`\nā
Total unique skills found: ${allSkills.length}\n`);
|
|
71
|
+
return allSkills;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Install a skill using clawhub install
|
|
76
|
+
*/
|
|
77
|
+
async function installSkill(slug) {
|
|
78
|
+
const skillDir = path.join(SKILLS_DIR, slug);
|
|
79
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
80
|
+
|
|
81
|
+
// Check if already installed
|
|
82
|
+
try {
|
|
83
|
+
await fs.access(skillFile);
|
|
84
|
+
return { slug, skillFile, cached: true };
|
|
85
|
+
} catch {
|
|
86
|
+
// Not cached
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// Install skill using ClawHub CLI
|
|
91
|
+
await execAsync(
|
|
92
|
+
`clawhub install ${slug} --workdir "${SKILLS_DIR}" --no-input`,
|
|
93
|
+
{ timeout: 60000 }
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Check if SKILL.md exists
|
|
97
|
+
try {
|
|
98
|
+
await fs.access(skillFile);
|
|
99
|
+
return { slug, skillFile, cached: false };
|
|
100
|
+
} catch {
|
|
101
|
+
return { slug, skillFile: null, error: 'SKILL.md not found after install' };
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return { slug, skillFile: null, error: error.message };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Scan a skill
|
|
110
|
+
*/
|
|
111
|
+
async function scanSkill(slug, skillFile) {
|
|
112
|
+
if (!skillFile) {
|
|
113
|
+
return {
|
|
114
|
+
slug,
|
|
115
|
+
grade: 'F',
|
|
116
|
+
error: 'Skill file not available'
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const scannerPath = path.join(__dirname, '..', '..', 'index.js');
|
|
122
|
+
const { stdout } = await execAsync(
|
|
123
|
+
`node "${scannerPath}" scan-skill "${skillFile}" --verbosity compact`,
|
|
124
|
+
{ timeout: 120000 }
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const result = JSON.parse(stdout);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
slug,
|
|
131
|
+
grade: result.grade || 'F',
|
|
132
|
+
findings: result.findings || [],
|
|
133
|
+
findingsCount: result.findings_count || 0,
|
|
134
|
+
recommendation: result.recommendation
|
|
135
|
+
};
|
|
136
|
+
} catch (error) {
|
|
137
|
+
return {
|
|
138
|
+
slug,
|
|
139
|
+
grade: 'F',
|
|
140
|
+
error: error.message,
|
|
141
|
+
findings: []
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Batch install skills
|
|
148
|
+
*/
|
|
149
|
+
async function installSkillsBatch(skills) {
|
|
150
|
+
console.log(`š¦ Installing ${skills.length} skills...\n`);
|
|
151
|
+
|
|
152
|
+
const results = [];
|
|
153
|
+
let completed = 0;
|
|
154
|
+
|
|
155
|
+
for (let i = 0; i < skills.length; i += CONCURRENT_DOWNLOADS) {
|
|
156
|
+
const batch = skills.slice(i, i + CONCURRENT_DOWNLOADS);
|
|
157
|
+
const promises = batch.map(skill => installSkill(skill.slug));
|
|
158
|
+
const batchResults = await Promise.all(promises);
|
|
159
|
+
|
|
160
|
+
results.push(...batchResults);
|
|
161
|
+
completed += batch.length;
|
|
162
|
+
|
|
163
|
+
const progress = ((completed / skills.length) * 100).toFixed(1);
|
|
164
|
+
const cached = batchResults.filter(r => r.cached).length;
|
|
165
|
+
const successful = batchResults.filter(r => r.skillFile !== null).length;
|
|
166
|
+
|
|
167
|
+
console.log(` [${progress}%] ${completed}/${skills.length} (${successful} ok, ${cached} cached)`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const successful = results.filter(r => r.skillFile !== null).length;
|
|
171
|
+
const failed = results.length - successful;
|
|
172
|
+
|
|
173
|
+
console.log(`\nā
Install complete: ${successful} successful, ${failed} failed\n`);
|
|
174
|
+
return results;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Batch scan skills
|
|
179
|
+
*/
|
|
180
|
+
async function scanSkillsBatch(installedSkills) {
|
|
181
|
+
console.log(`š Scanning ${installedSkills.length} skills...\n`);
|
|
182
|
+
|
|
183
|
+
const results = [];
|
|
184
|
+
let completed = 0;
|
|
185
|
+
|
|
186
|
+
for (let i = 0; i < installedSkills.length; i += CONCURRENT_SCANS) {
|
|
187
|
+
const batch = installedSkills.slice(i, i + CONCURRENT_SCANS);
|
|
188
|
+
const promises = batch.map(({ slug, skillFile }) => scanSkill(slug, skillFile));
|
|
189
|
+
const batchResults = await Promise.all(promises);
|
|
190
|
+
|
|
191
|
+
results.push(...batchResults);
|
|
192
|
+
completed += batch.length;
|
|
193
|
+
|
|
194
|
+
const progress = ((completed / installedSkills.length) * 100).toFixed(1);
|
|
195
|
+
console.log(` [${progress}%] Scanned ${completed}/${installedSkills.length}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log(`\nā
Scan complete!\n`);
|
|
199
|
+
return results;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Generate report
|
|
204
|
+
*/
|
|
205
|
+
function generateReport(scanResults) {
|
|
206
|
+
const gradeDistribution = { A: 0, B: 0, C: 0, D: 0, F: 0 };
|
|
207
|
+
const totalFindings = { critical: 0, warning: 0, info: 0 };
|
|
208
|
+
const topIssues = {};
|
|
209
|
+
|
|
210
|
+
for (const result of scanResults) {
|
|
211
|
+
gradeDistribution[result.grade] = (gradeDistribution[result.grade] || 0) + 1;
|
|
212
|
+
|
|
213
|
+
if (result.findings && Array.isArray(result.findings)) {
|
|
214
|
+
for (const finding of result.findings) {
|
|
215
|
+
const severity = finding.severity?.toLowerCase() || 'info';
|
|
216
|
+
if (severity === 'critical' || severity === 'error') {
|
|
217
|
+
totalFindings.critical++;
|
|
218
|
+
} else if (severity === 'warning') {
|
|
219
|
+
totalFindings.warning++;
|
|
220
|
+
} else {
|
|
221
|
+
totalFindings.info++;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const ruleId = finding.rule_id || finding.ruleId || 'unknown';
|
|
225
|
+
topIssues[ruleId] = (topIssues[ruleId] || 0) + 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const topIssuesSorted = Object.entries(topIssues)
|
|
231
|
+
.sort((a, b) => b[1] - a[1])
|
|
232
|
+
.slice(0, 20)
|
|
233
|
+
.map(([rule, count]) => ({ rule, count }));
|
|
234
|
+
|
|
235
|
+
const totalSkills = scanResults.length;
|
|
236
|
+
const vulnerableSkills = scanResults.filter(r => r.findingsCount > 0).length;
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
summary: {
|
|
240
|
+
totalSkills,
|
|
241
|
+
vulnerableSkills,
|
|
242
|
+
vulnerabilityRate: ((vulnerableSkills / totalSkills) * 100).toFixed(1) + '%',
|
|
243
|
+
gradeDistribution,
|
|
244
|
+
totalFindings
|
|
245
|
+
},
|
|
246
|
+
topIssues: topIssuesSorted,
|
|
247
|
+
scannedAt: new Date().toISOString(),
|
|
248
|
+
scannerVersion: '3.10.3'
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Main
|
|
254
|
+
*/
|
|
255
|
+
async function main() {
|
|
256
|
+
console.log('š”ļø ClawHub Security Scanner v2\n');
|
|
257
|
+
console.log('ā'.repeat(60) + '\n');
|
|
258
|
+
|
|
259
|
+
await fs.mkdir(SKILLS_DIR, { recursive: true });
|
|
260
|
+
|
|
261
|
+
const skills = await fetchAllSkills();
|
|
262
|
+
if (skills.length === 0) {
|
|
263
|
+
console.error('ā No skills found');
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const installedSkills = await installSkillsBatch(skills);
|
|
268
|
+
const validSkills = installedSkills.filter(s => s.skillFile !== null);
|
|
269
|
+
|
|
270
|
+
if (validSkills.length === 0) {
|
|
271
|
+
console.error('ā No skills installed successfully');
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const scanResults = await scanSkillsBatch(validSkills);
|
|
276
|
+
const report = generateReport(scanResults);
|
|
277
|
+
|
|
278
|
+
await fs.writeFile(RESULTS_FILE, JSON.stringify(scanResults, null, 2));
|
|
279
|
+
await fs.writeFile(REPORT_FILE, JSON.stringify(report, null, 2));
|
|
280
|
+
|
|
281
|
+
console.log('š Results Summary\n');
|
|
282
|
+
console.log('ā'.repeat(60));
|
|
283
|
+
console.log(`Total skills scanned: ${report.summary.totalSkills}`);
|
|
284
|
+
console.log(`Vulnerable skills: ${report.summary.vulnerableSkills} (${report.summary.vulnerabilityRate})`);
|
|
285
|
+
console.log(`\nGrade Distribution:`);
|
|
286
|
+
console.log(` A: ${report.summary.gradeDistribution.A}`);
|
|
287
|
+
console.log(` B: ${report.summary.gradeDistribution.B}`);
|
|
288
|
+
console.log(` C: ${report.summary.gradeDistribution.C}`);
|
|
289
|
+
console.log(` D: ${report.summary.gradeDistribution.D}`);
|
|
290
|
+
console.log(` F: ${report.summary.gradeDistribution.F}`);
|
|
291
|
+
console.log(`\nTotal Findings:`);
|
|
292
|
+
console.log(` Critical: ${report.summary.totalFindings.critical}`);
|
|
293
|
+
console.log(` Warning: ${report.summary.totalFindings.warning}`);
|
|
294
|
+
console.log(` Info: ${report.summary.totalFindings.info}`);
|
|
295
|
+
console.log(`\nTop 10 Issues:`);
|
|
296
|
+
report.topIssues.slice(0, 10).forEach((issue, i) => {
|
|
297
|
+
console.log(` ${i + 1}. ${issue.rule} (${issue.count} occurrences)`);
|
|
298
|
+
});
|
|
299
|
+
console.log('\n' + 'ā'.repeat(60));
|
|
300
|
+
console.log(`\nā
Results saved to:`);
|
|
301
|
+
console.log(` ${RESULTS_FILE}`);
|
|
302
|
+
console.log(` ${REPORT_FILE}\n`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
main().catch(error => {
|
|
306
|
+
console.error('\nā Fatal error:', error);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
});
|
package/src/daemon-client.js
CHANGED
|
@@ -13,7 +13,7 @@ try {
|
|
|
13
13
|
|
|
14
14
|
const DAEMON_SCRIPT = join(__dirname, '..', 'daemon.py');
|
|
15
15
|
const READY_TIMEOUT = 15000; // 15s to wait for __ready__ signal
|
|
16
|
-
const REQUEST_TIMEOUT =
|
|
16
|
+
const REQUEST_TIMEOUT = 45000; // 45s per request (increased for large files in Codex/complex analysis)
|
|
17
17
|
const MAX_RESTARTS = 3;
|
|
18
18
|
const RESTART_WINDOW = 60000; // 60s window for restart counting
|
|
19
19
|
|