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.
@@ -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(/&lt;/g, '<')
130
+ .replace(/&gt;/g, '>')
131
+ .replace(/&amp;/g, '&')
132
+ .replace(/&quot;/g, '"')
133
+ .replace(/&#39;/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
+ });
@@ -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 = 30000; // 30s per request
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