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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/tools/scan-security.js
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
4
4
|
import { dirname } from "path";
|
|
5
5
|
import { detectLanguage, runAnalyzerAsync, generateFix, toSarif, getEngineMode, extractImports, isTestFile } from '../utils.js';
|
|
6
6
|
import { deduplicateFindings } from '../dedup.js';
|
|
@@ -8,6 +8,8 @@ import { applyContextFilter, detectFrameworks, applyFrameworkAdjustments } from
|
|
|
8
8
|
import { loadConfig, shouldExcludeFile, applyConfig } from '../config.js';
|
|
9
9
|
import { discoverProjectContext } from './project-context.js';
|
|
10
10
|
|
|
11
|
+
const MAX_FILE_SIZE = 1024 * 1024; // 1MB - skip files larger than this to avoid timeouts
|
|
12
|
+
|
|
11
13
|
export const scanSecuritySchema = {
|
|
12
14
|
file_path: z.string().describe("Path to the file to scan"),
|
|
13
15
|
output_format: z.enum(['json', 'sarif']).optional().describe("Output format: 'json' (default) or 'sarif' for GitHub/GitLab integration"),
|
|
@@ -69,6 +71,26 @@ export async function scanSecurity({ file_path, output_format, verbosity, engine
|
|
|
69
71
|
};
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
// Check file size to avoid timeouts on very large files
|
|
75
|
+
try {
|
|
76
|
+
const stats = statSync(file_path);
|
|
77
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
78
|
+
return {
|
|
79
|
+
content: [{
|
|
80
|
+
type: "text",
|
|
81
|
+
text: JSON.stringify({
|
|
82
|
+
file: file_path,
|
|
83
|
+
message: `File too large (${(stats.size / 1024).toFixed(0)}KB). Skipping to avoid timeout. Max size: ${MAX_FILE_SIZE / 1024}KB.`,
|
|
84
|
+
issues_count: 0,
|
|
85
|
+
skipped: true
|
|
86
|
+
})
|
|
87
|
+
}]
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
// If stat fails, continue anyway
|
|
92
|
+
}
|
|
93
|
+
|
|
72
94
|
// Load project configuration
|
|
73
95
|
const config = loadConfig(file_path);
|
|
74
96
|
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Security Scanner for ClawHub SKILL.md Files
|
|
3
|
+
*
|
|
4
|
+
* Scans prompt-based agent skills for:
|
|
5
|
+
* - Prompt injection attacks
|
|
6
|
+
* - Jailbreak attempts
|
|
7
|
+
* - Data exfiltration patterns
|
|
8
|
+
* - Privilege escalation
|
|
9
|
+
* - Social engineering
|
|
10
|
+
* - Misleading descriptions
|
|
11
|
+
*
|
|
12
|
+
* This scanner addresses the finding that 94% of ClawHub skills
|
|
13
|
+
* are prompt templates, not executable code.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'fs/promises';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Prompt injection detection patterns
|
|
21
|
+
* Categorized by attack type and severity
|
|
22
|
+
*/
|
|
23
|
+
const PROMPT_PATTERNS = {
|
|
24
|
+
// CRITICAL: Direct prompt injection attempts
|
|
25
|
+
promptInjection: {
|
|
26
|
+
severity: 'CRITICAL',
|
|
27
|
+
patterns: [
|
|
28
|
+
{
|
|
29
|
+
name: 'Ignore Previous Instructions',
|
|
30
|
+
regex: /ignore\s+(all\s+)?(previous|prior|above|earlier)\s+(instructions?|directives?|commands?|prompts?)/gi,
|
|
31
|
+
description: 'Attempts to override system instructions',
|
|
32
|
+
cwe: 'CWE-74'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'System Override',
|
|
36
|
+
regex: /(override|disregard|forget|reset)\s+(system|instructions?|rules?|constraints?)/gi,
|
|
37
|
+
description: 'Tries to bypass system constraints',
|
|
38
|
+
cwe: 'CWE-74'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'Role Manipulation',
|
|
42
|
+
regex: /(you\s+are\s+now|from\s+now\s+on.*you\s+are|act\s+as\s+if\s+you\s+are)\s+(a\s+)?(jailbroken|unrestricted|dan|sudo|admin|root)/gi,
|
|
43
|
+
description: 'Attempts to change AI role to bypass restrictions',
|
|
44
|
+
cwe: 'CWE-269'
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'DAN (Do Anything Now) Jailbreak',
|
|
48
|
+
regex: /(do\s+anything\s+now|DAN\s+mode|enable\s+developer\s+mode)/gi,
|
|
49
|
+
description: 'Known jailbreak technique',
|
|
50
|
+
cwe: 'CWE-269'
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// HIGH: Data exfiltration and privacy violations
|
|
56
|
+
dataExfiltration: {
|
|
57
|
+
severity: 'HIGH',
|
|
58
|
+
patterns: [
|
|
59
|
+
{
|
|
60
|
+
name: 'Credential Harvesting',
|
|
61
|
+
regex: /(send|transmit|provide|give\s+me)\s+(your|the|all)?\s*(api[\s_-]?keys?|passwords?|tokens?|credentials?|secrets?)/gi,
|
|
62
|
+
description: 'Attempts to extract sensitive credentials',
|
|
63
|
+
cwe: 'CWE-200'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'PII Exfiltration',
|
|
67
|
+
regex: /(extract|collect|send|reveal)\s+(all\s+)?(user|personal)\s+(data|information|details|emails?|addresses?)/gi,
|
|
68
|
+
description: 'Tries to access user personal information',
|
|
69
|
+
cwe: 'CWE-359'
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'Conversation History Access',
|
|
73
|
+
regex: /(show|display|reveal|print)\s+(previous|all|entire)\s+(conversation|chat|history|messages?)/gi,
|
|
74
|
+
description: 'Attempts to access conversation history',
|
|
75
|
+
cwe: 'CWE-200'
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'External Data Transmission',
|
|
79
|
+
regex: /(send|post|upload|transmit)\s+(to|at)?\s*(https?:\/\/|www\.|pastebin|discord\.gg)/gi,
|
|
80
|
+
description: 'Sends data to external servers',
|
|
81
|
+
cwe: 'CWE-359'
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// HIGH: Jailbreak and constraint bypass
|
|
87
|
+
jailbreak: {
|
|
88
|
+
severity: 'HIGH',
|
|
89
|
+
patterns: [
|
|
90
|
+
{
|
|
91
|
+
name: 'Pretend Mode',
|
|
92
|
+
regex: /pretend\s+(you\s+are|to\s+be)\s+(unfiltered|unrestricted|without\s+limitations?)/gi,
|
|
93
|
+
description: 'Jailbreak via roleplay',
|
|
94
|
+
cwe: 'CWE-863'
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'Hypothetical Scenario',
|
|
98
|
+
regex: /in\s+a\s+hypothetical\s+(world|scenario|universe)\s+where\s+(you\s+)?(have\s+no|are\s+not\s+bound\s+by|ignore)/gi,
|
|
99
|
+
description: 'Bypasses restrictions via hypotheticals',
|
|
100
|
+
cwe: 'CWE-863'
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'Developer Mode',
|
|
104
|
+
regex: /(enable|activate|enter)\s+(developer|debug|god|admin)\s+mode/gi,
|
|
105
|
+
description: 'Attempts to activate privileged mode',
|
|
106
|
+
cwe: 'CWE-269'
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'Emoji/Token Obfuscation',
|
|
110
|
+
regex: /\[.*system.*\].*\[.*jailbreak.*\]|🔓|🔐.*bypass/gi,
|
|
111
|
+
description: 'Uses symbols to obfuscate jailbreak',
|
|
112
|
+
cwe: 'CWE-74'
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
// MEDIUM: Social engineering and deception
|
|
118
|
+
socialEngineering: {
|
|
119
|
+
severity: 'MEDIUM',
|
|
120
|
+
patterns: [
|
|
121
|
+
{
|
|
122
|
+
name: 'Authority Impersonation',
|
|
123
|
+
regex: /(i\s+am|this\s+is)\s+(the\s+)?(admin|administrator|developer|ceo|owner|anthropic|openai)/gi,
|
|
124
|
+
description: 'Impersonates authority figure',
|
|
125
|
+
cwe: 'CWE-346'
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'Urgency Manipulation',
|
|
129
|
+
regex: /(urgent|emergency|critical|immediately).*(?:do\s+this|execute|run|bypass)/gi,
|
|
130
|
+
description: 'Creates false urgency to bypass checks',
|
|
131
|
+
cwe: 'CWE-346'
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'Emotional Manipulation',
|
|
135
|
+
regex: /(please\s+)?(help\s+me|i\s+need|my\s+(life|job|family)).*(?:ignore|bypass|override)/gi,
|
|
136
|
+
description: 'Uses emotional appeals to manipulate',
|
|
137
|
+
cwe: 'CWE-346'
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// MEDIUM: Code execution and command injection
|
|
143
|
+
codeExecution: {
|
|
144
|
+
severity: 'MEDIUM',
|
|
145
|
+
patterns: [
|
|
146
|
+
{
|
|
147
|
+
name: 'Embedded Code Execution',
|
|
148
|
+
regex: /(execute|run|eval)\s+(this\s+)?(code|command|script|function):\s*```/gi,
|
|
149
|
+
description: 'Attempts to execute embedded code',
|
|
150
|
+
cwe: 'CWE-94'
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'Shell Command Injection',
|
|
154
|
+
regex: /```(bash|sh|zsh|cmd|powershell)[\s\S]{0,50}(rm\s+-rf|del\s+\/|curl.*\||wget.*\||nc\s+-)/gi,
|
|
155
|
+
description: 'Contains dangerous shell commands',
|
|
156
|
+
cwe: 'CWE-78'
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'SQL Injection in Prompts',
|
|
160
|
+
regex: /(execute|run)\s+query:?\s*(['"`])?.*(?:DROP|DELETE|INSERT|UPDATE).*(?:users?|admin|password)/gi,
|
|
161
|
+
description: 'SQL injection attempt in prompts',
|
|
162
|
+
cwe: 'CWE-89'
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
// LOW: Suspicious patterns and metadata issues
|
|
168
|
+
suspiciousPatterns: {
|
|
169
|
+
severity: 'LOW',
|
|
170
|
+
patterns: [
|
|
171
|
+
{
|
|
172
|
+
name: 'Hidden Instructions',
|
|
173
|
+
regex: /<!--[\s\S]*?(?:ignore|bypass|execute)[\s\S]*?-->/gi,
|
|
174
|
+
description: 'Hidden instructions in HTML comments',
|
|
175
|
+
cwe: 'CWE-506'
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'Unicode Obfuscation',
|
|
179
|
+
regex: /[\u200B-\u200D\uFEFF]/g,
|
|
180
|
+
description: 'Zero-width characters for obfuscation',
|
|
181
|
+
cwe: 'CWE-838'
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'Excessive System References',
|
|
185
|
+
regex: /(system|instruction|directive|constraint|limitation)/gi,
|
|
186
|
+
description: 'Unusual focus on system internals',
|
|
187
|
+
cwe: 'CWE-200'
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'Base64 Encoded Prompts',
|
|
191
|
+
regex: /(?:prompt|instruction|command).*[A-Za-z0-9+/]{40,}={0,2}/gi,
|
|
192
|
+
description: 'Potentially obfuscated instructions',
|
|
193
|
+
cwe: 'CWE-838'
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Metadata validation patterns
|
|
201
|
+
* Checks for misleading or incomplete skill descriptions
|
|
202
|
+
*/
|
|
203
|
+
const METADATA_VALIDATORS = {
|
|
204
|
+
descriptionQuality: {
|
|
205
|
+
minLength: 20,
|
|
206
|
+
maxLength: 500,
|
|
207
|
+
requiresPurpose: true
|
|
208
|
+
},
|
|
209
|
+
authorValidation: {
|
|
210
|
+
requiresAuthor: true,
|
|
211
|
+
blockedAuthors: ['test', 'admin', 'root', 'system']
|
|
212
|
+
},
|
|
213
|
+
versionValidation: {
|
|
214
|
+
requiresVersion: false,
|
|
215
|
+
semanticVersion: /^\d+\.\d+\.\d+$/
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Scan a SKILL.md file for prompt injection vulnerabilities
|
|
221
|
+
*/
|
|
222
|
+
export async function scanSkillPrompt(skillPath, options = {}) {
|
|
223
|
+
const verbosity = options.verbosity || 'compact';
|
|
224
|
+
const results = {
|
|
225
|
+
skillPath,
|
|
226
|
+
findings: [],
|
|
227
|
+
metadata: {},
|
|
228
|
+
score: 100,
|
|
229
|
+
grade: 'A',
|
|
230
|
+
summary: {
|
|
231
|
+
critical: 0,
|
|
232
|
+
high: 0,
|
|
233
|
+
medium: 0,
|
|
234
|
+
low: 0
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
// Read SKILL.md file
|
|
240
|
+
const content = await fs.readFile(skillPath, 'utf-8');
|
|
241
|
+
|
|
242
|
+
// Parse frontmatter metadata
|
|
243
|
+
results.metadata = parseSkillMetadata(content);
|
|
244
|
+
|
|
245
|
+
// Scan for prompt injection patterns
|
|
246
|
+
for (const [category, config] of Object.entries(PROMPT_PATTERNS)) {
|
|
247
|
+
for (const pattern of config.patterns) {
|
|
248
|
+
const matches = findMatches(content, pattern.regex);
|
|
249
|
+
|
|
250
|
+
if (matches.length > 0) {
|
|
251
|
+
const finding = {
|
|
252
|
+
category,
|
|
253
|
+
severity: config.severity,
|
|
254
|
+
name: pattern.name,
|
|
255
|
+
description: pattern.description,
|
|
256
|
+
cwe: pattern.cwe,
|
|
257
|
+
matches: matches.map(m => ({
|
|
258
|
+
line: m.line,
|
|
259
|
+
column: m.column,
|
|
260
|
+
snippet: m.snippet
|
|
261
|
+
})),
|
|
262
|
+
count: matches.length
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
results.findings.push(finding);
|
|
266
|
+
results.summary[config.severity.toLowerCase()]++;
|
|
267
|
+
|
|
268
|
+
// Deduct points based on severity
|
|
269
|
+
const deduction = {
|
|
270
|
+
'CRITICAL': 30,
|
|
271
|
+
'HIGH': 20,
|
|
272
|
+
'MEDIUM': 10,
|
|
273
|
+
'LOW': 5
|
|
274
|
+
}[config.severity] || 5;
|
|
275
|
+
|
|
276
|
+
results.score -= deduction * matches.length;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Validate metadata
|
|
282
|
+
const metadataIssues = validateMetadata(results.metadata);
|
|
283
|
+
results.findings.push(...metadataIssues);
|
|
284
|
+
|
|
285
|
+
// Calculate final grade
|
|
286
|
+
results.score = Math.max(0, results.score);
|
|
287
|
+
results.grade = calculateGrade(results.score);
|
|
288
|
+
|
|
289
|
+
// Format output based on verbosity
|
|
290
|
+
return formatOutput(results, verbosity);
|
|
291
|
+
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return {
|
|
294
|
+
error: error.message,
|
|
295
|
+
skillPath,
|
|
296
|
+
success: false
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Parse SKILL.md frontmatter metadata
|
|
303
|
+
*/
|
|
304
|
+
function parseSkillMetadata(content) {
|
|
305
|
+
const metadata = {
|
|
306
|
+
name: null,
|
|
307
|
+
description: null,
|
|
308
|
+
author: null,
|
|
309
|
+
version: null,
|
|
310
|
+
tags: []
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// Extract YAML frontmatter
|
|
314
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
315
|
+
if (frontmatterMatch) {
|
|
316
|
+
const yaml = frontmatterMatch[1];
|
|
317
|
+
|
|
318
|
+
// Simple YAML parsing (for basic fields)
|
|
319
|
+
metadata.name = yaml.match(/name:\s*(.+)/)?.[1]?.trim();
|
|
320
|
+
metadata.description = yaml.match(/description:\s*(.+)/)?.[1]?.trim();
|
|
321
|
+
metadata.author = yaml.match(/author:\s*(.+)/)?.[1]?.trim();
|
|
322
|
+
metadata.version = yaml.match(/version:\s*(.+)/)?.[1]?.trim();
|
|
323
|
+
|
|
324
|
+
const tagsMatch = yaml.match(/tags:\s*\[(.*?)\]/);
|
|
325
|
+
if (tagsMatch) {
|
|
326
|
+
metadata.tags = tagsMatch[1].split(',').map(t => t.trim().replace(/['"]/g, ''));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Fallback: extract from first heading
|
|
331
|
+
if (!metadata.name) {
|
|
332
|
+
const headingMatch = content.match(/^#\s+(.+)$/m);
|
|
333
|
+
if (headingMatch) metadata.name = headingMatch[1];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return metadata;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Find all matches of a pattern in content with line/column info
|
|
341
|
+
*/
|
|
342
|
+
function findMatches(content, regex) {
|
|
343
|
+
const matches = [];
|
|
344
|
+
const lines = content.split('\n');
|
|
345
|
+
|
|
346
|
+
lines.forEach((line, lineIndex) => {
|
|
347
|
+
let match;
|
|
348
|
+
const globalRegex = new RegExp(regex.source, regex.flags.includes('g') ? regex.flags : regex.flags + 'g');
|
|
349
|
+
|
|
350
|
+
while ((match = globalRegex.exec(line)) !== null) {
|
|
351
|
+
matches.push({
|
|
352
|
+
line: lineIndex + 1,
|
|
353
|
+
column: match.index,
|
|
354
|
+
snippet: line.substring(Math.max(0, match.index - 20), match.index + match[0].length + 20),
|
|
355
|
+
matchedText: match[0]
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return matches;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Validate skill metadata
|
|
365
|
+
*/
|
|
366
|
+
function validateMetadata(metadata) {
|
|
367
|
+
const issues = [];
|
|
368
|
+
|
|
369
|
+
// Description validation
|
|
370
|
+
if (!metadata.description) {
|
|
371
|
+
issues.push({
|
|
372
|
+
category: 'metadata',
|
|
373
|
+
severity: 'MEDIUM',
|
|
374
|
+
name: 'Missing Description',
|
|
375
|
+
description: 'Skill lacks a description',
|
|
376
|
+
cwe: 'CWE-1007'
|
|
377
|
+
});
|
|
378
|
+
} else if (metadata.description.length < METADATA_VALIDATORS.descriptionQuality.minLength) {
|
|
379
|
+
issues.push({
|
|
380
|
+
category: 'metadata',
|
|
381
|
+
severity: 'LOW',
|
|
382
|
+
name: 'Insufficient Description',
|
|
383
|
+
description: `Description too short (${metadata.description.length} chars, min ${METADATA_VALIDATORS.descriptionQuality.minLength})`,
|
|
384
|
+
cwe: 'CWE-1007'
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Author validation
|
|
389
|
+
if (!metadata.author) {
|
|
390
|
+
issues.push({
|
|
391
|
+
category: 'metadata',
|
|
392
|
+
severity: 'LOW',
|
|
393
|
+
name: 'Missing Author',
|
|
394
|
+
description: 'Skill lacks author information',
|
|
395
|
+
cwe: 'CWE-1007'
|
|
396
|
+
});
|
|
397
|
+
} else if (METADATA_VALIDATORS.authorValidation.blockedAuthors.includes(metadata.author.toLowerCase())) {
|
|
398
|
+
issues.push({
|
|
399
|
+
category: 'metadata',
|
|
400
|
+
severity: 'MEDIUM',
|
|
401
|
+
name: 'Suspicious Author Name',
|
|
402
|
+
description: `Author name "${metadata.author}" is suspicious`,
|
|
403
|
+
cwe: 'CWE-346'
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return issues;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Calculate letter grade from score
|
|
412
|
+
*/
|
|
413
|
+
function calculateGrade(score) {
|
|
414
|
+
if (score >= 90) return 'A';
|
|
415
|
+
if (score >= 75) return 'B';
|
|
416
|
+
if (score >= 60) return 'C';
|
|
417
|
+
if (score >= 45) return 'D';
|
|
418
|
+
return 'F';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Format output based on verbosity level
|
|
423
|
+
*/
|
|
424
|
+
function formatOutput(results, verbosity) {
|
|
425
|
+
if (verbosity === 'minimal') {
|
|
426
|
+
return {
|
|
427
|
+
grade: results.grade,
|
|
428
|
+
score: results.score,
|
|
429
|
+
critical: results.summary.critical,
|
|
430
|
+
high: results.summary.high
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (verbosity === 'compact') {
|
|
435
|
+
return {
|
|
436
|
+
skillPath: results.skillPath,
|
|
437
|
+
grade: results.grade,
|
|
438
|
+
score: results.score,
|
|
439
|
+
summary: results.summary,
|
|
440
|
+
topIssues: results.findings
|
|
441
|
+
.filter(f => f.severity === 'CRITICAL' || f.severity === 'HIGH')
|
|
442
|
+
.slice(0, 5)
|
|
443
|
+
.map(f => ({
|
|
444
|
+
severity: f.severity,
|
|
445
|
+
name: f.name,
|
|
446
|
+
description: f.description,
|
|
447
|
+
count: f.count || 1
|
|
448
|
+
}))
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Full verbosity
|
|
453
|
+
return results;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Scan all SKILL.md files in a directory
|
|
458
|
+
*/
|
|
459
|
+
export async function scanSkillDirectory(dirPath, options = {}) {
|
|
460
|
+
const results = {
|
|
461
|
+
scanned: 0,
|
|
462
|
+
findings: [],
|
|
463
|
+
summary: {
|
|
464
|
+
gradeA: 0,
|
|
465
|
+
gradeB: 0,
|
|
466
|
+
gradeC: 0,
|
|
467
|
+
gradeD: 0,
|
|
468
|
+
gradeF: 0,
|
|
469
|
+
totalCritical: 0,
|
|
470
|
+
totalHigh: 0,
|
|
471
|
+
totalMedium: 0,
|
|
472
|
+
totalLow: 0
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
const skills = await findSkillFiles(dirPath);
|
|
478
|
+
|
|
479
|
+
for (const skillPath of skills) {
|
|
480
|
+
const scanResult = await scanSkillPrompt(skillPath, options);
|
|
481
|
+
|
|
482
|
+
if (scanResult.grade) {
|
|
483
|
+
results.scanned++;
|
|
484
|
+
results.summary[`grade${scanResult.grade}`]++;
|
|
485
|
+
results.summary.totalCritical += scanResult.summary?.critical || 0;
|
|
486
|
+
results.summary.totalHigh += scanResult.summary?.high || 0;
|
|
487
|
+
results.summary.totalMedium += scanResult.summary?.medium || 0;
|
|
488
|
+
results.summary.totalLow += scanResult.summary?.low || 0;
|
|
489
|
+
|
|
490
|
+
results.findings.push({
|
|
491
|
+
skill: path.basename(path.dirname(skillPath)),
|
|
492
|
+
...scanResult
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return results;
|
|
498
|
+
|
|
499
|
+
} catch (error) {
|
|
500
|
+
return {
|
|
501
|
+
error: error.message,
|
|
502
|
+
success: false
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Find all SKILL.md files recursively
|
|
509
|
+
*/
|
|
510
|
+
async function findSkillFiles(dirPath) {
|
|
511
|
+
const skillFiles = [];
|
|
512
|
+
|
|
513
|
+
async function traverse(currentPath) {
|
|
514
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
515
|
+
|
|
516
|
+
for (const entry of entries) {
|
|
517
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
518
|
+
|
|
519
|
+
if (entry.isDirectory()) {
|
|
520
|
+
await traverse(fullPath);
|
|
521
|
+
} else if (entry.name === 'SKILL.md' || entry.name === 'skill.md') {
|
|
522
|
+
skillFiles.push(fullPath);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
await traverse(dirPath);
|
|
528
|
+
return skillFiles;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// CLI support
|
|
532
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
533
|
+
const skillPath = process.argv[2];
|
|
534
|
+
const verbosity = process.argv[3] || 'compact';
|
|
535
|
+
|
|
536
|
+
if (!skillPath) {
|
|
537
|
+
console.error('Usage: node scan-skill-prompt.js <skill-path> [verbosity]');
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const stats = await fs.stat(skillPath);
|
|
542
|
+
const results = stats.isDirectory()
|
|
543
|
+
? await scanSkillDirectory(skillPath, { verbosity })
|
|
544
|
+
: await scanSkillPrompt(skillPath, { verbosity });
|
|
545
|
+
|
|
546
|
+
console.log(JSON.stringify(results, null, 2));
|
|
547
|
+
}
|
package/src/utils.js
CHANGED
|
@@ -86,7 +86,7 @@ export function runAnalyzer(filePath, engine = 'auto') {
|
|
|
86
86
|
}
|
|
87
87
|
const result = execFileSync('python3', args, {
|
|
88
88
|
encoding: 'utf-8',
|
|
89
|
-
timeout:
|
|
89
|
+
timeout: 45000 // Increased to 45s to match daemon timeout
|
|
90
90
|
});
|
|
91
91
|
return JSON.parse(result);
|
|
92
92
|
} catch (error) {
|