@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/spec.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
/**
|
|
5
|
+
* Validate a skill directory against Agent Skills specification
|
|
6
|
+
*/
|
|
7
|
+
export function validateSkillSpec(skillPath, dirName) {
|
|
8
|
+
const findings = [];
|
|
9
|
+
let manifest;
|
|
10
|
+
// SPEC-01: Check SKILL.md exists
|
|
11
|
+
const skillMdPath = join(skillPath, "SKILL.md");
|
|
12
|
+
let skillMdContent;
|
|
13
|
+
try {
|
|
14
|
+
skillMdContent = readFileSync(skillMdPath, "utf-8");
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
findings.push({
|
|
18
|
+
id: "SPEC-01",
|
|
19
|
+
category: "SPEC",
|
|
20
|
+
asixx: "SPEC",
|
|
21
|
+
severity: "critical",
|
|
22
|
+
file: skillPath,
|
|
23
|
+
message: "SKILL.md is required but not found",
|
|
24
|
+
evidence: skillMdPath
|
|
25
|
+
});
|
|
26
|
+
return { valid: false, findings };
|
|
27
|
+
}
|
|
28
|
+
// SPEC-02: Parse frontmatter
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = matter(skillMdContent);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
findings.push({
|
|
35
|
+
id: "SPEC-02",
|
|
36
|
+
category: "SPEC",
|
|
37
|
+
asixx: "SPEC",
|
|
38
|
+
severity: "critical",
|
|
39
|
+
file: skillMdPath,
|
|
40
|
+
message: "Failed to parse SKILL.md frontmatter",
|
|
41
|
+
evidence: String(e).slice(0, 100)
|
|
42
|
+
});
|
|
43
|
+
return { valid: false, findings };
|
|
44
|
+
}
|
|
45
|
+
manifest = {
|
|
46
|
+
name: parsed.data.name || "",
|
|
47
|
+
description: parsed.data.description || "",
|
|
48
|
+
origin: parsed.data.origin, // Custom metadata (not spec-required)
|
|
49
|
+
license: parsed.data.license,
|
|
50
|
+
compatibility: parsed.data.compatibility,
|
|
51
|
+
metadata: parsed.data.metadata,
|
|
52
|
+
allowedTools: parsed.data["allowed-tools"],
|
|
53
|
+
content: parsed.content,
|
|
54
|
+
files: []
|
|
55
|
+
};
|
|
56
|
+
// SPEC-03: Validate required 'name' field
|
|
57
|
+
if (!manifest.name) {
|
|
58
|
+
findings.push({
|
|
59
|
+
id: "SPEC-03",
|
|
60
|
+
category: "SPEC",
|
|
61
|
+
asixx: "SPEC",
|
|
62
|
+
severity: "critical",
|
|
63
|
+
file: skillMdPath,
|
|
64
|
+
message: "Frontmatter missing required 'name' field"
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// SPEC-04: name length check (<=64 chars)
|
|
69
|
+
if (manifest.name.length > 64) {
|
|
70
|
+
findings.push({
|
|
71
|
+
id: "SPEC-04",
|
|
72
|
+
category: "SPEC",
|
|
73
|
+
asixx: "SPEC",
|
|
74
|
+
severity: "high",
|
|
75
|
+
file: skillMdPath,
|
|
76
|
+
message: `name exceeds 64 char limit (${manifest.name.length} chars)`
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// SPEC-05: name format (lowercase a-z0-9-)
|
|
80
|
+
if (!/^[a-z0-9-]+$/.test(manifest.name)) {
|
|
81
|
+
findings.push({
|
|
82
|
+
id: "SPEC-05",
|
|
83
|
+
category: "SPEC",
|
|
84
|
+
asixx: "SPEC",
|
|
85
|
+
severity: "high",
|
|
86
|
+
file: skillMdPath,
|
|
87
|
+
message: "name must only contain lowercase letters, numbers, and hyphens"
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// SPEC-06: no leading/trailing hyphen
|
|
91
|
+
if (manifest.name.startsWith('-') || manifest.name.endsWith('-')) {
|
|
92
|
+
findings.push({
|
|
93
|
+
id: "SPEC-06",
|
|
94
|
+
category: "SPEC",
|
|
95
|
+
asixx: "SPEC",
|
|
96
|
+
severity: "high",
|
|
97
|
+
file: skillMdPath,
|
|
98
|
+
message: "name cannot start or end with a hyphen"
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// SPEC-07: no consecutive hyphens
|
|
102
|
+
if (manifest.name.includes('--')) {
|
|
103
|
+
findings.push({
|
|
104
|
+
id: "SPEC-07",
|
|
105
|
+
category: "SPEC",
|
|
106
|
+
asixx: "SPEC",
|
|
107
|
+
severity: "high",
|
|
108
|
+
file: skillMdPath,
|
|
109
|
+
message: "name cannot contain consecutive hyphens"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// SPEC-08: name must match directory
|
|
113
|
+
if (manifest.name !== dirName) {
|
|
114
|
+
findings.push({
|
|
115
|
+
id: "SPEC-08",
|
|
116
|
+
category: "SPEC",
|
|
117
|
+
asixx: "SPEC",
|
|
118
|
+
severity: "high",
|
|
119
|
+
file: skillMdPath,
|
|
120
|
+
message: `name '${manifest.name}' must match directory '${dirName}'`
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// SPEC-09: Validate required 'description' field
|
|
125
|
+
if (!manifest.description) {
|
|
126
|
+
findings.push({
|
|
127
|
+
id: "SPEC-09",
|
|
128
|
+
category: "SPEC",
|
|
129
|
+
asixx: "SPEC",
|
|
130
|
+
severity: "critical",
|
|
131
|
+
file: skillMdPath,
|
|
132
|
+
message: "Frontmatter missing required 'description' field"
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
else if (manifest.description.length > 1024) {
|
|
136
|
+
findings.push({
|
|
137
|
+
id: "SPEC-10",
|
|
138
|
+
category: "SPEC",
|
|
139
|
+
asixx: "SPEC",
|
|
140
|
+
severity: "high",
|
|
141
|
+
file: skillMdPath,
|
|
142
|
+
message: `description exceeds 1024 char limit (${manifest.description.length} chars)`
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
// SPEC-11: Optional license field validation
|
|
146
|
+
if (manifest.license && typeof manifest.license !== "string") {
|
|
147
|
+
findings.push({
|
|
148
|
+
id: "SPEC-11",
|
|
149
|
+
category: "SPEC",
|
|
150
|
+
asixx: "SPEC",
|
|
151
|
+
severity: "medium",
|
|
152
|
+
file: skillMdPath,
|
|
153
|
+
message: "license must be a string"
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// SPEC-12: Optional compatibility field length
|
|
157
|
+
if (manifest.compatibility && String(manifest.compatibility).length > 500) {
|
|
158
|
+
findings.push({
|
|
159
|
+
id: "SPEC-12",
|
|
160
|
+
category: "SPEC",
|
|
161
|
+
asixx: "SPEC",
|
|
162
|
+
severity: "medium",
|
|
163
|
+
file: skillMdPath,
|
|
164
|
+
message: "compatibility field exceeds 500 char limit"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// SPEC-13: Validate allowed-tools structure (if present)
|
|
168
|
+
if (manifest.allowedTools) {
|
|
169
|
+
findings.push(...validateAllowedTools(manifest.allowedTools, skillMdPath));
|
|
170
|
+
}
|
|
171
|
+
// SPEC-14: SKILL.md length budget check
|
|
172
|
+
const lineCount = skillMdContent.split('\n').length;
|
|
173
|
+
if (lineCount > 500) {
|
|
174
|
+
findings.push({
|
|
175
|
+
id: "SPEC-13",
|
|
176
|
+
category: "SPEC",
|
|
177
|
+
asixx: "SPEC",
|
|
178
|
+
severity: "info",
|
|
179
|
+
file: skillMdPath,
|
|
180
|
+
message: `SKILL.md has ${lineCount} lines - consider progressive disclosure (typical max ~500)`
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// SPEC-15: Directory structure sanity check
|
|
184
|
+
findings.push(...validateDirectoryStructure(skillPath));
|
|
185
|
+
const valid = !findings.some(f => f.severity === "critical");
|
|
186
|
+
return { valid, manifest, findings };
|
|
187
|
+
}
|
|
188
|
+
function validateAllowedTools(allowedTools, filePath) {
|
|
189
|
+
const findings = [];
|
|
190
|
+
if (Array.isArray(allowedTools)) {
|
|
191
|
+
for (const tool of allowedTools) {
|
|
192
|
+
if (typeof tool !== "string" && typeof tool !== "object") {
|
|
193
|
+
findings.push({
|
|
194
|
+
id: "SPEC-14",
|
|
195
|
+
category: "SPEC",
|
|
196
|
+
asixx: "SPEC",
|
|
197
|
+
severity: "medium",
|
|
198
|
+
file: filePath,
|
|
199
|
+
message: `allowed-tools contains non-string/object: ${typeof tool}`
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (allowedTools !== undefined) {
|
|
205
|
+
findings.push({
|
|
206
|
+
id: "SPEC-15",
|
|
207
|
+
category: "SPEC",
|
|
208
|
+
asixx: "SPEC",
|
|
209
|
+
severity: "medium",
|
|
210
|
+
file: filePath,
|
|
211
|
+
message: "allowed-tools should be an array or undefined"
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return findings;
|
|
215
|
+
}
|
|
216
|
+
function validateDirectoryStructure(skillPath) {
|
|
217
|
+
const findings = [];
|
|
218
|
+
// Check for recommended directories
|
|
219
|
+
const recommendedDirs = ["scripts", "references", "assets"];
|
|
220
|
+
const foundDirs = [];
|
|
221
|
+
try {
|
|
222
|
+
const entries = readdirSync(skillPath);
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
const entryPath = join(skillPath, entry);
|
|
225
|
+
try {
|
|
226
|
+
const stat = statSync(entryPath);
|
|
227
|
+
if (stat.isDirectory()) {
|
|
228
|
+
foundDirs.push(entry);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Skip inaccessible entries
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
findings.push({
|
|
238
|
+
id: "SPEC-16",
|
|
239
|
+
category: "SPEC",
|
|
240
|
+
asixx: "SPEC",
|
|
241
|
+
severity: "low",
|
|
242
|
+
file: skillPath,
|
|
243
|
+
message: "Could not read skill directory structure",
|
|
244
|
+
evidence: String(e).slice(0, 100)
|
|
245
|
+
});
|
|
246
|
+
return findings;
|
|
247
|
+
}
|
|
248
|
+
// Warn if no recognized directories (not critical, just informational)
|
|
249
|
+
const hasAnyDir = recommendedDirs.some(d => foundDirs.includes(d));
|
|
250
|
+
if (foundDirs.length > 0 && !hasAnyDir) {
|
|
251
|
+
findings.push({
|
|
252
|
+
id: "SPEC-17",
|
|
253
|
+
category: "SPEC",
|
|
254
|
+
asixx: "SPEC",
|
|
255
|
+
severity: "info",
|
|
256
|
+
file: skillPath,
|
|
257
|
+
message: `Found directories: ${foundDirs.join(', ')} - consider scripts/, references/, assets/ for organization`
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return findings;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Quick spec validation (for use in lint mode - less strict)
|
|
264
|
+
*/
|
|
265
|
+
export function quickValidate(skillPath, dirName) {
|
|
266
|
+
const { valid, findings } = validateSkillSpec(skillPath, dirName);
|
|
267
|
+
const errors = findings
|
|
268
|
+
.filter(f => f.severity === "critical" || f.severity === "high")
|
|
269
|
+
.map(f => `[${f.id}] ${f.message}`);
|
|
270
|
+
return { valid, errors };
|
|
271
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hungpg/skill-audit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Security auditing CLI for AI agent skills",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"skill-audit": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"start": "node dist/index.js",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"postinstall": "node dist/index.js --update-db --quiet 2>/dev/null || echo \"⚠️ Vulnerability DB update skipped (run 'npm run security:update' later)\"",
|
|
14
|
+
"security:update": "node dist/index.js --update-db"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.0.0"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"SKILL.md"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"security",
|
|
26
|
+
"audit",
|
|
27
|
+
"cli",
|
|
28
|
+
"ai",
|
|
29
|
+
"agent",
|
|
30
|
+
"skills",
|
|
31
|
+
"vulnerability",
|
|
32
|
+
"owasp",
|
|
33
|
+
"prompt-injection",
|
|
34
|
+
"llm-security"
|
|
35
|
+
],
|
|
36
|
+
"author": "Hung Pham",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/harrypham2000/skill-audit.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/harrypham2000/skill-audit/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/harrypham2000/skill-audit#readme",
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"commander": "^11.1.0",
|
|
48
|
+
"gray-matter": "^4.0.3",
|
|
49
|
+
"semver": "^7.5.4"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^20.10.0",
|
|
53
|
+
"tsx": "^4.7.0",
|
|
54
|
+
"typescript": "^5.3.0"
|
|
55
|
+
}
|
|
56
|
+
}
|