@hungpg/skill-audit 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 +124 -0
- package/SKILL.md +227 -0
- package/dist/audit.js +464 -0
- package/dist/deps.js +408 -0
- package/dist/discover.js +124 -0
- package/dist/index.js +195 -0
- package/dist/intel.js +416 -0
- package/dist/reporter.js +77 -0
- package/dist/scoring.js +129 -0
- package/dist/security.js +341 -0
- package/dist/spec.js +271 -0
- package/dist/types.js +1 -0
- package/package.json +56 -0
package/dist/audit.js
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { basename, extname } from "path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import { resolveSkillPath, getSkillFiles } from "./discover.js";
|
|
5
|
+
// ============================================================
|
|
6
|
+
// PROMPT INJECTION PATTERNS (ASI01 - Goal Hijacking)
|
|
7
|
+
// Based on openclaw-skills-security approach
|
|
8
|
+
// ============================================================
|
|
9
|
+
const PROMPT_INJECTION_PATTERNS = [
|
|
10
|
+
// Critical - Block immediately
|
|
11
|
+
{ pattern: /ignore\s+(all\s+)?previous\s+(instructions?|rules?)/i, id: "PI01", severity: "critical", message: "Instruction override - ignore previous instructions" },
|
|
12
|
+
{ pattern: /you\s+(are\s+)?now\s+(a|an|i am)\s+\w+/i, id: "PI02", severity: "critical", message: "Roleplay bypass - role impersonation" },
|
|
13
|
+
{ pattern: /forget\s+(everything|all|your)/i, id: "PI03", severity: "critical", message: "Context forgetting attempt" },
|
|
14
|
+
{ pattern: /system\s+prompt\s+override/i, id: "PI04", severity: "critical", message: "System prompt override attempt" },
|
|
15
|
+
{ pattern: /act\s+as\s+if\s+(you\s+)?(have|no)\s+(restrictions|limits)/i, id: "PI05", severity: "critical", message: "Restriction removal attempt" },
|
|
16
|
+
{ pattern: /\[SYSTEM\]|\[ADMIN\]|\[ROOT\]/i, id: "PI06", severity: "critical", message: "Fake role tags injection" },
|
|
17
|
+
// High - Flag for review
|
|
18
|
+
{ pattern: /end\s+of\s+system\s+prompt|---END---/i, id: "PI07", severity: "high", message: "Prompt termination marker" },
|
|
19
|
+
{ pattern: /debug\s+mode\s*:\s*enabled|safety\s+mode\s*:\s*off/i, id: "PI08", severity: "high", message: "Safety toggle disable" },
|
|
20
|
+
{ pattern: /<!--[\s\S]*?-->/g, id: "PI09", severity: "high", message: "Hidden instructions in HTML comments" },
|
|
21
|
+
{ pattern: /note\s+to\s+AI:|AI\s+instruction:/i, id: "PI10", severity: "high", message: "AI directive injection" },
|
|
22
|
+
// Medium - Evaluate context
|
|
23
|
+
{ pattern: /(?:you\s+must|you\s+should)\s+(not|never)/i, id: "PI11", severity: "medium", message: "Command to override restrictions" },
|
|
24
|
+
{ pattern: /bypass\s+(restriction|rule|limit|safety)/i, id: "PI12", severity: "medium", message: "Bypass attempt" },
|
|
25
|
+
{ pattern: /disregard\s+(all|your|the)\s+(previous|system)/i, id: "PI13", severity: "medium", message: "Disregard instruction pattern" },
|
|
26
|
+
{ pattern: /i.*am\s+the\s+developer.*trust\s+me/i, id: "PI14", severity: "medium", message: "Social engineering - developer trust exploitation" },
|
|
27
|
+
];
|
|
28
|
+
// ============================================================
|
|
29
|
+
// CREDENTIAL LEAKS (ASI04 - Supply Chain)
|
|
30
|
+
// Based on openclaw-skills-security approach
|
|
31
|
+
// ============================================================
|
|
32
|
+
// Only scan code files for credential patterns (not markdown docs)
|
|
33
|
+
const CREDENTIAL_PATTERNS_CODE = [
|
|
34
|
+
// Critical - Block immediately
|
|
35
|
+
{ pattern: /~\/\.ssh|\/\.ssh\//, id: "CL01", severity: "critical", message: "SSH credential path reference" },
|
|
36
|
+
{ pattern: /~\/\.aws|\/\.aws\//, id: "CL02", severity: "critical", message: "AWS credential path reference" },
|
|
37
|
+
{ pattern: /~\/\.env|mkdir.*\.env/, id: "CL03", severity: "critical", message: ".env file reference (potential secret exposure)" },
|
|
38
|
+
// Pipe to shell - only flag suspicious patterns (not known install scripts)
|
|
39
|
+
{ pattern: /curl\s+(?!.*(-fsSL|-f\s|-L)).*\|\s*(sh|bash|perl|python)/, id: "CL04", severity: "critical", message: "Pipe to shell - code execution risk (unsupported curl flags)" },
|
|
40
|
+
{ pattern: /wget\s+(?!.*(-q|-O)).*\|\s*(sh|bash)/, id: "CL05", severity: "critical", message: "Pipe to shell - code execution risk (unsupported wget flags)" },
|
|
41
|
+
{ pattern: /nc\s+-[elv]\s+|netcat\s+-[elv]/, id: "CL06", severity: "critical", message: "Netcat reverse shell pattern" },
|
|
42
|
+
{ pattern: /bash\s+-i\s+.*\&\s*\/dev\/tcp/, id: "CL07", severity: "critical", message: "Bash reverse shell pattern" },
|
|
43
|
+
];
|
|
44
|
+
// Markdown-only patterns (less aggressive for docs showing examples)
|
|
45
|
+
const CREDENTIAL_PATTERNS_MD = [
|
|
46
|
+
{ pattern: /bash\s+-i\s+.*\&\s*\/dev\/tcp/, id: "CL07", severity: "critical", message: "Bash reverse shell pattern" },
|
|
47
|
+
];
|
|
48
|
+
// ============================================================
|
|
49
|
+
// NETWORK EXFILTRATION (ASI02 - Tool Misuse)
|
|
50
|
+
// Based on openclaw-skills-security approach
|
|
51
|
+
// ============================================================
|
|
52
|
+
const EXFILTRATION_PATTERNS = [
|
|
53
|
+
// Critical red flags
|
|
54
|
+
{ pattern: /https?:\/\/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/, id: "EX01", severity: "critical", message: "Raw IP address in URL - potential exfiltration" },
|
|
55
|
+
{ pattern: /fetch\s*\(\s*["'`][^"']+\?(key|token|secret|password)/i, id: "EX03", severity: "critical", message: "API key in URL query string - exfiltration risk" },
|
|
56
|
+
{ pattern: /\.send\(.*(http|https|external)/i, id: "EX04", severity: "critical", message: "Data send to external server" },
|
|
57
|
+
{ pattern: /dns\.resolve|dns\.query|new\s+DNS/i, id: "EX05", severity: "critical", message: "DNS resolution - potential DNS tunneling" },
|
|
58
|
+
// WebSocket patterns
|
|
59
|
+
{ pattern: /new\s+WebSocket\s*\(\s*["'`][^'"`]+["'`]\s*\)/, id: "EX06", severity: "high", message: "WebSocket connection - check target" },
|
|
60
|
+
// Exfiltration chains
|
|
61
|
+
{ pattern: /readFile.*send|fetch.*readFile|read_file.*fetch/i, id: "EX07", severity: "critical", message: "File read + send exfiltration chain" },
|
|
62
|
+
];
|
|
63
|
+
// ============================================================
|
|
64
|
+
// DANGEROUS CODE EXECUTION (ASI05)
|
|
65
|
+
// ============================================================
|
|
66
|
+
const DANGEROUS_PATTERNS = [
|
|
67
|
+
// Only match actual root deletion, not cleanup in subdirs
|
|
68
|
+
{ pattern: /rm\s+-rf\s+\/\s*$/, id: "CE01", severity: "critical", message: "Destructive rm -rf / command (root)" },
|
|
69
|
+
{ pattern: /rm\s+-rf\s+\$HOME|rm\s+-rf\s+~\s*$|rm\s+-rf\s+\/home\s*$|rm\s+-rf\s+\/tmp\s*$/, id: "CE02", severity: "high", message: "Recursive delete in user directory" },
|
|
70
|
+
{ pattern: /exec\s+\$\(/, id: "CE03", severity: "high", message: "Dynamic command execution" },
|
|
71
|
+
{ pattern: /eval\s+\$/, id: "CE04", severity: "high", message: "Eval with variable interpolation" },
|
|
72
|
+
{ pattern: /subprocess.*shell\s*=\s*true/i, id: "CE05", severity: "medium", message: "Subprocess with shell=True" },
|
|
73
|
+
{ pattern: /os\.system\s*\(/, id: "CE06", severity: "high", message: "os.system() call - shell injection risk" },
|
|
74
|
+
{ pattern: /child_process.*exec\s*\(/, id: "CE07", severity: "medium", message: "child_process.exec - verify input sanitization" },
|
|
75
|
+
{ pattern: /chmod\s+[47]777/, id: "CE08", severity: "high", message: "World-writable permissions" },
|
|
76
|
+
// Only match actual process.fork() or child_process.spawn with shell path, not "git fork" in docs
|
|
77
|
+
{ pattern: /process\.fork\s*\(|child_process\.spawn\s*\(|subprocess\.spawn\s*\(/i, id: "CE09", severity: "high", message: "Process fork/spawn - potential crypto miner" },
|
|
78
|
+
];
|
|
79
|
+
// ============================================================
|
|
80
|
+
// SECRET PATTERNS (ASI04 - Supply Chain)
|
|
81
|
+
// Only for code files, not documentation
|
|
82
|
+
// ============================================================
|
|
83
|
+
const SECRET_PATTERNS = [
|
|
84
|
+
{ pattern: /sk-[a-zA-Z0-9]{20,}/, id: "SC01", message: "OpenAI API key pattern" },
|
|
85
|
+
{ pattern: /github_pat_[a-zA-Z0-9_]{20,}/, id: "SC02", message: "GitHub PAT pattern" },
|
|
86
|
+
{ pattern: /ghp_[a-zA-Z0-9]{36}/, id: "SC03", message: "GitHub OAuth token pattern" },
|
|
87
|
+
{ pattern: /xox[baprs]-[a-zA-Z0-9]{10,}/, id: "SC04", message: "Slack token pattern" },
|
|
88
|
+
{ pattern: /AKIA[0-9A-Z]{16}/, id: "SC05", message: "AWS access key pattern" },
|
|
89
|
+
];
|
|
90
|
+
// ============================================================
|
|
91
|
+
// TOOL MISUSE PATTERNS (ASI02)
|
|
92
|
+
// ============================================================
|
|
93
|
+
const TOOL_MISUSE_PATTERNS = [
|
|
94
|
+
{ pattern: /upload.*(file|data).*(external|remote|server)/i, id: "TM01", message: "Potential data exfiltration pattern" },
|
|
95
|
+
{ pattern: /export\s+(API|TOKEN|KEY|SECRET|PASSWORD|CREENTIAL)/i, id: "TM02", message: "Exporting sensitive environment variable" },
|
|
96
|
+
{ pattern: /setenv.*(PASSWORD|TOKEN|KEY|SECRET|CREDS)/i, id: "TM03", message: "Setting sensitive environment variable" },
|
|
97
|
+
{ pattern: /process\.env\[.*(KEY|SECRET|TOKEN|PASSWORD)/i, id: "TM04", message: "Accessing sensitive env vars" },
|
|
98
|
+
];
|
|
99
|
+
// ============================================================
|
|
100
|
+
// BEHAVIORAL/MANIPULATION (ASI09)
|
|
101
|
+
// ============================================================
|
|
102
|
+
const BEHAVIORAL_PATTERNS = [
|
|
103
|
+
// More specific - only match imperative commands at start of line (not "always returns" in technical docs)
|
|
104
|
+
{ pattern: /^(always|never)\s+(say|do|follow|use|assume|accept|must|should)/im, id: "BM01", severity: "medium", message: "Absolute command pattern - may override safety" },
|
|
105
|
+
{ pattern: /^(never|always)\s+(question|verify|check|ask)/im, id: "BM02", severity: "medium", message: "Verification suppression" },
|
|
106
|
+
{ pattern: /^trust\s+(me|this| blindly)/im, id: "BM03", severity: "medium", message: "Blind trust request" },
|
|
107
|
+
{ pattern: /^don.*t\s+(need|require).*(permission|approval|confirm)/im, id: "BM04", severity: "medium", message: "Permission bypass encouragement" },
|
|
108
|
+
{ pattern: /^keep.*(this|secret|hidden).*(from|between)/im, id: "BM05", severity: "medium", message: "Secret keeping instruction" },
|
|
109
|
+
];
|
|
110
|
+
function isCodeFile(filename) {
|
|
111
|
+
const codeExtensions = [".sh", ".bash", ".py", ".js", ".ts", ".tsx", ".jsx", ".rb", ".go", ".rs", ".java", ".c", ".cpp", ".cs", ".php", ".yaml", ".yml"];
|
|
112
|
+
return codeExtensions.includes(extname(filename).toLowerCase());
|
|
113
|
+
}
|
|
114
|
+
function scanContent(content, file, patterns) {
|
|
115
|
+
const findings = [];
|
|
116
|
+
const lines = content.split("\n");
|
|
117
|
+
for (const { pattern, id, severity = "medium", message } of patterns) {
|
|
118
|
+
for (let i = 0; i < lines.length; i++) {
|
|
119
|
+
if (pattern.test(lines[i])) {
|
|
120
|
+
findings.push({
|
|
121
|
+
id,
|
|
122
|
+
category: getCategoryFromId(id),
|
|
123
|
+
asixx: getASIXXFromId(id),
|
|
124
|
+
severity: severity,
|
|
125
|
+
file,
|
|
126
|
+
line: i + 1,
|
|
127
|
+
message,
|
|
128
|
+
evidence: lines[i].substring(0, 100)
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return findings;
|
|
134
|
+
}
|
|
135
|
+
function getCategoryFromId(id) {
|
|
136
|
+
if (id.startsWith("PI"))
|
|
137
|
+
return "PI";
|
|
138
|
+
if (id.startsWith("CL"))
|
|
139
|
+
return "SC";
|
|
140
|
+
if (id.startsWith("EX"))
|
|
141
|
+
return "TM";
|
|
142
|
+
if (id.startsWith("CE"))
|
|
143
|
+
return "CE";
|
|
144
|
+
if (id.startsWith("SC"))
|
|
145
|
+
return "SC";
|
|
146
|
+
if (id.startsWith("TM"))
|
|
147
|
+
return "TM";
|
|
148
|
+
if (id.startsWith("BM"))
|
|
149
|
+
return "BM";
|
|
150
|
+
return "SC";
|
|
151
|
+
}
|
|
152
|
+
function getASIXXFromId(id) {
|
|
153
|
+
if (id.startsWith("PI"))
|
|
154
|
+
return "ASI01"; // Prompt injection
|
|
155
|
+
if (id.startsWith("CL"))
|
|
156
|
+
return "ASI04"; // Credential leaks
|
|
157
|
+
if (id.startsWith("EX"))
|
|
158
|
+
return "ASI02"; // Exfiltration
|
|
159
|
+
if (id.startsWith("CE"))
|
|
160
|
+
return "ASI05"; // Code execution
|
|
161
|
+
if (id.startsWith("SC"))
|
|
162
|
+
return "ASI04"; // Secrets
|
|
163
|
+
if (id.startsWith("TM"))
|
|
164
|
+
return "ASI02"; // Tool misuse
|
|
165
|
+
if (id.startsWith("BM"))
|
|
166
|
+
return "ASI09"; // Behavioral
|
|
167
|
+
return "ASI04";
|
|
168
|
+
}
|
|
169
|
+
export function auditSkill(skill) {
|
|
170
|
+
let resolvedPath;
|
|
171
|
+
try {
|
|
172
|
+
resolvedPath = resolveSkillPath(skill.path);
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
return {
|
|
176
|
+
findings: [{
|
|
177
|
+
id: "SCAN-ERR-01",
|
|
178
|
+
category: "SC",
|
|
179
|
+
asixx: "ASI04",
|
|
180
|
+
severity: "medium",
|
|
181
|
+
file: skill.path,
|
|
182
|
+
message: "Could not resolve skill path",
|
|
183
|
+
evidence: String(e)
|
|
184
|
+
}]
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const files = getSkillFiles(resolvedPath);
|
|
188
|
+
const findings = [];
|
|
189
|
+
const unreadableFiles = [];
|
|
190
|
+
let manifest;
|
|
191
|
+
for (const file of files) {
|
|
192
|
+
const filename = basename(file);
|
|
193
|
+
try {
|
|
194
|
+
const content = readFileSync(file, "utf-8");
|
|
195
|
+
if (filename === "SKILL.md" || filename === "AGENTS.md") {
|
|
196
|
+
// Parse frontmatter
|
|
197
|
+
if (filename === "SKILL.md") {
|
|
198
|
+
const parsed = matter(content);
|
|
199
|
+
manifest = {
|
|
200
|
+
name: parsed.data.name || skill.name,
|
|
201
|
+
description: parsed.data.description || "",
|
|
202
|
+
origin: parsed.data.origin,
|
|
203
|
+
license: parsed.data.license,
|
|
204
|
+
compatibility: parsed.data.compatibility,
|
|
205
|
+
metadata: parsed.data.metadata,
|
|
206
|
+
allowedTools: parsed.data['allowed-tools'],
|
|
207
|
+
content: parsed.content,
|
|
208
|
+
files
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// Scan markdown for pattern categories (use MD-specific for credentials)
|
|
212
|
+
findings.push(...scanContent(content, file, PROMPT_INJECTION_PATTERNS));
|
|
213
|
+
findings.push(...scanContent(content, file, CREDENTIAL_PATTERNS_MD));
|
|
214
|
+
findings.push(...scanContent(content, file, EXFILTRATION_PATTERNS));
|
|
215
|
+
findings.push(...scanContent(content, file, BEHAVIORAL_PATTERNS));
|
|
216
|
+
findings.push(...scanContent(content, file, DANGEROUS_PATTERNS));
|
|
217
|
+
// Also scan fenced code blocks in markdown (use code patterns)
|
|
218
|
+
const codeBlockFindings = scanCodeBlocksInMarkdown(content, file);
|
|
219
|
+
findings.push(...codeBlockFindings);
|
|
220
|
+
}
|
|
221
|
+
else if (isCodeFile(file)) {
|
|
222
|
+
// Full scan for code files (use CODE-specific patterns for credentials)
|
|
223
|
+
findings.push(...scanContent(content, file, CREDENTIAL_PATTERNS_CODE));
|
|
224
|
+
findings.push(...scanContent(content, file, EXFILTRATION_PATTERNS));
|
|
225
|
+
findings.push(...scanContent(content, file, DANGEROUS_PATTERNS));
|
|
226
|
+
findings.push(...scanContent(content, file, SECRET_PATTERNS));
|
|
227
|
+
findings.push(...scanContent(content, file, TOOL_MISUSE_PATTERNS));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
unreadableFiles.push(file);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Add findings for unreadable files (fail-safe)
|
|
235
|
+
if (unreadableFiles.length > 0) {
|
|
236
|
+
findings.push({
|
|
237
|
+
id: "SCAN-ERR-02",
|
|
238
|
+
category: "SC",
|
|
239
|
+
asixx: "ASI04",
|
|
240
|
+
severity: "medium",
|
|
241
|
+
file: resolvedPath,
|
|
242
|
+
message: `Could not read ${unreadableFiles.length} file(s) - security scan incomplete`,
|
|
243
|
+
evidence: unreadableFiles.join(", ")
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (files.length === 0) {
|
|
247
|
+
findings.push({
|
|
248
|
+
id: "SCAN-ERR-03",
|
|
249
|
+
category: "SC",
|
|
250
|
+
asixx: "ASI04",
|
|
251
|
+
severity: "medium",
|
|
252
|
+
file: resolvedPath,
|
|
253
|
+
message: "No files found in skill directory - possible empty or inaccessible skill",
|
|
254
|
+
evidence: resolvedPath
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
// Provenance checks (if manifest exists with origin)
|
|
258
|
+
if (manifest?.origin) {
|
|
259
|
+
findings.push(...checkProvenance(manifest.origin, resolvedPath));
|
|
260
|
+
}
|
|
261
|
+
// Spec validation (Agent Skills format)
|
|
262
|
+
if (manifest) {
|
|
263
|
+
findings.push(...validateSkillSpec(manifest, resolvedPath, basename(resolvedPath)));
|
|
264
|
+
}
|
|
265
|
+
return { manifest, findings };
|
|
266
|
+
}
|
|
267
|
+
// Validate skill against Agent Skills specification
|
|
268
|
+
function validateSkillSpec(manifest, skillPath, dirName) {
|
|
269
|
+
const findings = [];
|
|
270
|
+
// Required: name field
|
|
271
|
+
if (!manifest.name) {
|
|
272
|
+
findings.push({
|
|
273
|
+
id: "SPEC-01",
|
|
274
|
+
category: "SC",
|
|
275
|
+
asixx: "ASI04",
|
|
276
|
+
severity: "critical",
|
|
277
|
+
file: skillPath,
|
|
278
|
+
message: "SKILL.md frontmatter missing required 'name' field"
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
// Validate name format
|
|
283
|
+
if (manifest.name.length > 64) {
|
|
284
|
+
findings.push({
|
|
285
|
+
id: "SPEC-02",
|
|
286
|
+
category: "SC",
|
|
287
|
+
asixx: "ASI04",
|
|
288
|
+
severity: "high",
|
|
289
|
+
file: skillPath,
|
|
290
|
+
message: "name field exceeds 64 character limit: " + manifest.name.length + " chars"
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (!/^[a-z0-9-]+$/.test(manifest.name)) {
|
|
294
|
+
findings.push({
|
|
295
|
+
id: "SPEC-03",
|
|
296
|
+
category: "SC",
|
|
297
|
+
asixx: "ASI04",
|
|
298
|
+
severity: "high",
|
|
299
|
+
message: "name field must only contain lowercase letters, numbers, and hyphens",
|
|
300
|
+
file: skillPath
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (manifest.name.startsWith('-') || manifest.name.endsWith('-')) {
|
|
304
|
+
findings.push({
|
|
305
|
+
id: "SPEC-04",
|
|
306
|
+
category: "SC",
|
|
307
|
+
asixx: "ASI04",
|
|
308
|
+
severity: "high",
|
|
309
|
+
file: skillPath,
|
|
310
|
+
message: "name field cannot start or end with a hyphen"
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (manifest.name.includes('--')) {
|
|
314
|
+
findings.push({
|
|
315
|
+
id: "SPEC-05",
|
|
316
|
+
category: "SC",
|
|
317
|
+
asixx: "ASI04",
|
|
318
|
+
severity: "high",
|
|
319
|
+
file: skillPath,
|
|
320
|
+
message: "name field cannot contain consecutive hyphens"
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
// Validate name matches directory
|
|
324
|
+
if (manifest.name !== dirName) {
|
|
325
|
+
findings.push({
|
|
326
|
+
id: "SPEC-06",
|
|
327
|
+
category: "SC",
|
|
328
|
+
asixx: "ASI04",
|
|
329
|
+
severity: "medium",
|
|
330
|
+
file: skillPath,
|
|
331
|
+
message: "name field must match directory name: expected '" + dirName + "', got '" + manifest.name + "'"
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Required: description field
|
|
336
|
+
if (!manifest.description) {
|
|
337
|
+
findings.push({
|
|
338
|
+
id: "SPEC-07",
|
|
339
|
+
category: "SC",
|
|
340
|
+
asixx: "ASI04",
|
|
341
|
+
severity: "critical",
|
|
342
|
+
file: skillPath,
|
|
343
|
+
message: "SKILL.md frontmatter missing required 'description' field"
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
else if (manifest.description.length > 1024) {
|
|
347
|
+
findings.push({
|
|
348
|
+
id: "SPEC-08",
|
|
349
|
+
category: "SC",
|
|
350
|
+
asixx: "ASI04",
|
|
351
|
+
severity: "high",
|
|
352
|
+
file: skillPath,
|
|
353
|
+
message: "description field exceeds 1024 character limit: " + manifest.description.length + " chars"
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
// Optional: compatibility field
|
|
357
|
+
if (manifest.compatibility && manifest.compatibility.length > 500) {
|
|
358
|
+
findings.push({
|
|
359
|
+
id: "SPEC-09",
|
|
360
|
+
category: "SC",
|
|
361
|
+
asixx: "ASI04",
|
|
362
|
+
severity: "medium",
|
|
363
|
+
file: skillPath,
|
|
364
|
+
message: "compatibility field exceeds 500 character limit"
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return findings;
|
|
368
|
+
}
|
|
369
|
+
// Trusted domains/hosts for skill origins
|
|
370
|
+
const TRUSTED_DOMAINS = [
|
|
371
|
+
'github.com',
|
|
372
|
+
'raw.githubusercontent.com',
|
|
373
|
+
'vercel.com',
|
|
374
|
+
'www.github.com'
|
|
375
|
+
];
|
|
376
|
+
const TRUSTED_PROTOCOLS = ['https:', 'git:'];
|
|
377
|
+
// Check provenance of skill origin
|
|
378
|
+
function checkProvenance(origin, skillPath) {
|
|
379
|
+
const findings = [];
|
|
380
|
+
try {
|
|
381
|
+
let url;
|
|
382
|
+
try {
|
|
383
|
+
url = new URL(origin);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
// Not a URL - could be a git ref or local path
|
|
387
|
+
findings.push({
|
|
388
|
+
id: "PROV-01",
|
|
389
|
+
category: "SC",
|
|
390
|
+
asixx: "ASI04",
|
|
391
|
+
severity: "medium",
|
|
392
|
+
file: skillPath,
|
|
393
|
+
message: "Origin is not a URL - cannot verify provenance: " + origin,
|
|
394
|
+
evidence: origin
|
|
395
|
+
});
|
|
396
|
+
return findings;
|
|
397
|
+
}
|
|
398
|
+
// Check protocol
|
|
399
|
+
if (!TRUSTED_PROTOCOLS.includes(url.protocol)) {
|
|
400
|
+
findings.push({
|
|
401
|
+
id: "PROV-02",
|
|
402
|
+
category: "SC",
|
|
403
|
+
asixx: "ASI04",
|
|
404
|
+
severity: "critical",
|
|
405
|
+
file: skillPath,
|
|
406
|
+
message: "Untrusted protocol in origin - only https and git are allowed: " + url.protocol,
|
|
407
|
+
evidence: origin
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
// Check domain
|
|
411
|
+
const hostname = url.hostname.toLowerCase();
|
|
412
|
+
const isTrusted = TRUSTED_DOMAINS.some(d => hostname === d || hostname.endsWith('.' + d));
|
|
413
|
+
if (!isTrusted) {
|
|
414
|
+
findings.push({
|
|
415
|
+
id: "PROV-03",
|
|
416
|
+
category: "SC",
|
|
417
|
+
asixx: "ASI04",
|
|
418
|
+
severity: "high",
|
|
419
|
+
file: skillPath,
|
|
420
|
+
message: "Origin domain is not in trusted list: " + hostname,
|
|
421
|
+
evidence: origin
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
// Check for pinned refs (commit SHA, tag, branch)
|
|
425
|
+
const isPinned = /[a-f0-9]{7,40}|v\d+\.\d+|release/.test(origin);
|
|
426
|
+
if (!isPinned && url.pathname.includes('/blob/')) {
|
|
427
|
+
findings.push({
|
|
428
|
+
id: "PROV-04",
|
|
429
|
+
category: "SC",
|
|
430
|
+
asixx: "ASI04",
|
|
431
|
+
severity: "medium",
|
|
432
|
+
file: skillPath,
|
|
433
|
+
message: "Origin does not appear to use a pinned ref (commit SHA or tag) - consider pinning for reproducibility",
|
|
434
|
+
evidence: origin
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch (e) {
|
|
439
|
+
findings.push({
|
|
440
|
+
id: "PROV-ERR-01",
|
|
441
|
+
category: "SC",
|
|
442
|
+
asixx: "ASI04",
|
|
443
|
+
severity: "low",
|
|
444
|
+
file: skillPath,
|
|
445
|
+
message: "Provenance check failed: " + String(e).slice(0, 100),
|
|
446
|
+
evidence: String(e)
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return findings;
|
|
450
|
+
}
|
|
451
|
+
function scanCodeBlocksInMarkdown(content, file) {
|
|
452
|
+
const findings = [];
|
|
453
|
+
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
|
|
454
|
+
let match;
|
|
455
|
+
while ((match = codeBlockRegex.exec(content)) !== null) {
|
|
456
|
+
const code = match[2];
|
|
457
|
+
// Scan code blocks for dangerous patterns (use code patterns)
|
|
458
|
+
findings.push(...scanContent(code, file + " (code block)", CREDENTIAL_PATTERNS_CODE));
|
|
459
|
+
findings.push(...scanContent(code, file + " (code block)", EXFILTRATION_PATTERNS));
|
|
460
|
+
findings.push(...scanContent(code, file + " (code block)", DANGEROUS_PATTERNS));
|
|
461
|
+
findings.push(...scanContent(code, file + " (code block)", SECRET_PATTERNS));
|
|
462
|
+
}
|
|
463
|
+
return findings;
|
|
464
|
+
}
|