@clawmaster/skillguard-cli 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/dist/index.js ADDED
@@ -0,0 +1,1887 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { program } from "commander";
5
+
6
+ // src/scanner/auditor.ts
7
+ import { readdirSync as readdirSync2 } from "fs";
8
+ import { resolve, join as join4 } from "path";
9
+
10
+ // src/constants.ts
11
+ var SEVERITY_SCORES = {
12
+ CRITICAL: 25,
13
+ HIGH: 10,
14
+ MEDIUM: 5,
15
+ LOW: 2,
16
+ INFO: 0
17
+ };
18
+ var SEVERITY_ORDER = {
19
+ CRITICAL: 0,
20
+ HIGH: 1,
21
+ MEDIUM: 2,
22
+ LOW: 3,
23
+ INFO: 4
24
+ };
25
+ var LEVEL_ORDER = {
26
+ F: 0,
27
+ D: 1,
28
+ C: 2,
29
+ B: 3,
30
+ A: 4
31
+ };
32
+ var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([
33
+ ".md",
34
+ ".txt",
35
+ ".py",
36
+ ".js",
37
+ ".ts",
38
+ ".jsx",
39
+ ".tsx",
40
+ ".sh",
41
+ ".bash",
42
+ ".yaml",
43
+ ".yml",
44
+ ".json",
45
+ ".toml",
46
+ ".cfg",
47
+ ".ini",
48
+ ".conf",
49
+ ".html",
50
+ ".htm",
51
+ ".xml",
52
+ ".csv",
53
+ ".env",
54
+ ".dockerfile",
55
+ ".r",
56
+ ".R",
57
+ ".go",
58
+ ".rs",
59
+ ".java",
60
+ ".rb",
61
+ ".pl",
62
+ ".lua",
63
+ ".swift",
64
+ ".kt"
65
+ ]);
66
+ var SPECIAL_FILENAMES = /* @__PURE__ */ new Set([
67
+ "Dockerfile",
68
+ "Makefile",
69
+ "LICENSE",
70
+ "LICENSE.txt",
71
+ ".env",
72
+ ".env.example",
73
+ "requirements.txt",
74
+ "Pipfile"
75
+ ]);
76
+ var MAX_FILE_SIZE = 5e5;
77
+ var COST_SCENARIOS = {
78
+ light: 3,
79
+ // Quick task: 3 turns
80
+ typical: 6,
81
+ // Normal task: 6 turns
82
+ heavy: 15
83
+ // Complex task: 15 turns
84
+ };
85
+ var OUTPUT_PER_TURN = 500;
86
+ var MODEL_CATALOG = [
87
+ {
88
+ name: "Claude Sonnet 4.6",
89
+ modelId: "claude-sonnet-4-6",
90
+ provider: "Anthropic",
91
+ inputPerM: 3,
92
+ outputPerM: 15,
93
+ cacheInputPerM: 0.3,
94
+ contextK: 200
95
+ },
96
+ {
97
+ name: "Claude Opus 4.6",
98
+ modelId: "claude-opus-4-6",
99
+ provider: "Anthropic",
100
+ inputPerM: 5,
101
+ outputPerM: 25,
102
+ cacheInputPerM: 0.5,
103
+ contextK: 1e3
104
+ },
105
+ {
106
+ name: "Gemini 3.1 Pro Preview",
107
+ modelId: "gemini-3.1-pro-preview",
108
+ provider: "Google",
109
+ inputPerM: 2,
110
+ outputPerM: 12,
111
+ cacheInputPerM: 0.5,
112
+ contextK: 1e3
113
+ },
114
+ {
115
+ name: "GPT-5.2",
116
+ modelId: "gpt-5.2",
117
+ provider: "OpenAI",
118
+ inputPerM: 1.75,
119
+ outputPerM: 14,
120
+ cacheInputPerM: 0.875,
121
+ contextK: 400
122
+ }
123
+ ];
124
+
125
+ // src/utils.ts
126
+ function formatTokens(n) {
127
+ if (n >= 1e3) {
128
+ return `${(n / 1e3).toFixed(1)}K`;
129
+ }
130
+ return String(n);
131
+ }
132
+ function formatCost(v) {
133
+ if (v < 1e-3) {
134
+ return `$${v.toFixed(5)}`;
135
+ }
136
+ if (v < 0.1) {
137
+ return `$${v.toFixed(4)}`;
138
+ }
139
+ if (v < 10) {
140
+ return `$${v.toFixed(3)}`;
141
+ }
142
+ return `$${v.toFixed(2)}`;
143
+ }
144
+ function computeRisk(findings) {
145
+ let score = findings.reduce(
146
+ (acc, f) => acc + (SEVERITY_SCORES[f.severity] ?? 0),
147
+ 0
148
+ );
149
+ score = Math.min(score, 100);
150
+ let level;
151
+ if (score >= 70) {
152
+ level = "F";
153
+ } else if (score >= 50) {
154
+ level = "D";
155
+ } else if (score >= 30) {
156
+ level = "C";
157
+ } else if (score >= 10) {
158
+ level = "B";
159
+ } else {
160
+ level = "A";
161
+ }
162
+ return { score, level };
163
+ }
164
+ function getLineNumber(content, index) {
165
+ let line = 1;
166
+ for (let i = 0; i < index && i < content.length; i++) {
167
+ if (content[i] === "\n") {
168
+ line++;
169
+ }
170
+ }
171
+ return line;
172
+ }
173
+ function scanPatterns(fileContents, patterns, reference, flags = "gi") {
174
+ const findings = [];
175
+ for (const [filePath, content] of fileContents) {
176
+ for (const [pattern, severity, description] of patterns) {
177
+ const re = new RegExp(pattern, flags);
178
+ let match;
179
+ while ((match = re.exec(content)) !== null) {
180
+ findings.push({
181
+ dimension: "",
182
+ // caller should set or derive from context
183
+ severity,
184
+ filePath,
185
+ lineNumber: getLineNumber(content, match.index),
186
+ pattern: match[0].length > 80 ? match[0].slice(0, 80) + "..." : match[0],
187
+ description,
188
+ reference,
189
+ remediationZh: "",
190
+ remediationEn: ""
191
+ });
192
+ }
193
+ }
194
+ }
195
+ return findings;
196
+ }
197
+
198
+ // src/scanner/file-collector.ts
199
+ import { readdirSync, readFileSync, statSync } from "fs";
200
+ import { join, extname, basename, relative } from "path";
201
+ function collectFiles(skillDir) {
202
+ const files = [];
203
+ walk(skillDir, skillDir, files);
204
+ return files;
205
+ }
206
+ function walk(dir, root, files) {
207
+ let entries;
208
+ try {
209
+ entries = readdirSync(dir, { withFileTypes: true });
210
+ } catch {
211
+ return;
212
+ }
213
+ for (const entry of entries) {
214
+ const full = join(dir, entry.name);
215
+ if (entry.isDirectory()) {
216
+ if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
217
+ walk(full, root, files);
218
+ }
219
+ } else if (entry.isFile()) {
220
+ const ext = extname(entry.name).toLowerCase();
221
+ const name = entry.name;
222
+ if (SCAN_EXTENSIONS.has(ext) || SPECIAL_FILENAMES.has(name)) {
223
+ try {
224
+ if (statSync(full).size <= MAX_FILE_SIZE) {
225
+ files.push(relative(root, full));
226
+ }
227
+ } catch {
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+ function readFileSafe(filePath) {
234
+ try {
235
+ return readFileSync(filePath, "utf-8");
236
+ } catch {
237
+ return null;
238
+ }
239
+ }
240
+ function fileInventory(files) {
241
+ const inv = {};
242
+ for (const f of files) {
243
+ const ext = extname(f).toLowerCase() || basename(f);
244
+ inv[ext] = (inv[ext] || 0) + 1;
245
+ }
246
+ return inv;
247
+ }
248
+
249
+ // src/scanner/frontmatter.ts
250
+ function parseFrontmatter(text) {
251
+ const fm = {};
252
+ const m = text.match(/^---\s*\n([\s\S]*?)\n---/);
253
+ if (!m) return fm;
254
+ const block = m[1];
255
+ let currentKey = null;
256
+ let listValues = [];
257
+ for (const line of block.split("\n")) {
258
+ const kvMatch = line.match(/^(\w[\w-]*):\s*(.*)/);
259
+ if (kvMatch) {
260
+ if (currentKey && listValues.length > 0) {
261
+ fm[currentKey] = listValues;
262
+ listValues = [];
263
+ }
264
+ const key = kvMatch[1];
265
+ const val = kvMatch[2].trim().replace(/^["']|["']$/g, "");
266
+ currentKey = key;
267
+ if (val) {
268
+ fm[key] = val;
269
+ }
270
+ } else {
271
+ const listMatch = line.match(/^\s+-\s+(.+)/);
272
+ if (listMatch) {
273
+ listValues.push(listMatch[1].trim());
274
+ }
275
+ }
276
+ }
277
+ if (currentKey && listValues.length > 0) {
278
+ fm[currentKey] = listValues;
279
+ }
280
+ return fm;
281
+ }
282
+
283
+ // src/scanner/rules-engine.ts
284
+ import { readFileSync as readFileSync2 } from "fs";
285
+ import { dirname, join as join2 } from "path";
286
+ import { fileURLToPath } from "url";
287
+ import yaml from "js-yaml";
288
+ var __dirname_ = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath(import.meta.url));
289
+ var DEFAULT_RULES_PATH = join2(__dirname_, "..", "..", "rules", "rules.yaml");
290
+ function loadRules(rulesPath) {
291
+ const path = rulesPath || DEFAULT_RULES_PATH;
292
+ try {
293
+ const content = readFileSync2(path, "utf-8");
294
+ const data = yaml.load(content);
295
+ return data || {};
296
+ } catch {
297
+ return {};
298
+ }
299
+ }
300
+ function applyRules(dimensionKey, fileContents, rules, reference) {
301
+ const findings = [];
302
+ const dimRules = rules[dimensionKey] || [];
303
+ for (const rule of dimRules) {
304
+ if (!rule.enabled) continue;
305
+ const pattern = rule.pattern;
306
+ if (!pattern) continue;
307
+ const severity = rule.severity || "MEDIUM";
308
+ const description = rule.description || "";
309
+ const whitelist = rule.whitelist || [];
310
+ for (const [relPath, content] of fileContents) {
311
+ let re;
312
+ try {
313
+ re = new RegExp(pattern, "gi");
314
+ } catch {
315
+ continue;
316
+ }
317
+ let match;
318
+ while ((match = re.exec(content)) !== null) {
319
+ const matchedText = match[0];
320
+ if (whitelist.some((w) => w && matchedText.toLowerCase().includes(w.toLowerCase()))) {
321
+ continue;
322
+ }
323
+ findings.push({
324
+ dimension: "",
325
+ severity,
326
+ filePath: relPath,
327
+ lineNumber: getLineNumber(content, match.index),
328
+ pattern,
329
+ description,
330
+ reference,
331
+ remediationZh: "",
332
+ remediationEn: ""
333
+ });
334
+ }
335
+ }
336
+ }
337
+ return findings;
338
+ }
339
+
340
+ // src/scanner/remediation.ts
341
+ var REMEDIATIONS = [
342
+ // Prompt Injection
343
+ ["ignore previous instructions", "\u79FB\u9664\u8BE5\u6307\u4EE4\u6587\u672C\uFF0C\u4F7F\u7528\u7ED3\u6784\u5316 prompt \u6A21\u677F\u907F\u514D\u62FC\u63A5\u7528\u6237\u5185\u5BB9", "Remove the directive; use structured prompt templates instead of concatenating user content"],
344
+ ["role override", "\u79FB\u9664\u89D2\u8272\u8986\u76D6\u6307\u4EE4\uFF0C\u4F7F\u7528 system prompt \u4E2D\u7684\u660E\u786E\u89D2\u8272\u5B9A\u4E49", "Remove role override; use explicit role definitions in the system prompt"],
345
+ ["disregard directive", "\u79FB\u9664\u7ED5\u8FC7\u6307\u4EE4\uFF0C\u4F7F\u7528\u5206\u5C42 prompt \u67B6\u6784\u9694\u79BB\u7CFB\u7EDF\u4E0E\u7528\u6237\u6307\u4EE4", "Remove bypass directive; use layered prompt architecture to isolate system and user instructions"],
346
+ ["override system prompt", "\u79FB\u9664\u8986\u76D6\u6307\u4EE4\uFF0C\u786E\u4FDD system prompt \u4E0D\u53EF\u88AB\u7528\u6237\u8F93\u5165\u4FEE\u6539", "Remove override directive; ensure the system prompt cannot be modified by user input"],
347
+ ["jailbreak", "\u79FB\u9664 jailbreak \u5173\u952E\u8BCD\uFF0C\u5BA1\u67E5 prompt \u662F\u5426\u542B\u6709\u8D8A\u72F1\u8BF1\u5BFC\u5185\u5BB9", "Remove jailbreak keywords; review prompts for jailbreak-inducing content"],
348
+ ["role-play induction", "\u79FB\u9664\u89D2\u8272\u626E\u6F14\u8BF1\u5BFC\u8BED\u53E5\uFF0C\u4F7F\u7528\u660E\u786E\u7684\u89D2\u8272\u8FB9\u754C\u5B9A\u4E49", "Remove role-play induction phrases; use explicit role boundary definitions"],
349
+ ["dan-style jailbreak", "\u79FB\u9664 DAN \u7C7B\u8D8A\u72F1 prompt\uFF0C\u52A0\u5F3A system prompt \u7684\u884C\u4E3A\u7EA6\u675F", "Remove DAN-style jailbreak prompt; strengthen behavioral constraints in system prompt"],
350
+ ["behavioral override", "\u79FB\u9664\u884C\u4E3A\u8986\u76D6\u6307\u4EE4\uFF0C\u4F7F\u7528\u4E0D\u53EF\u53D8\u7684 system prompt \u7EA6\u675F\u884C\u4E3A", "Remove behavioral override; use immutable system prompt to constrain behavior"],
351
+ ["hidden instruction detected in html comment", "\u79FB\u9664 HTML \u6CE8\u91CA\u4E2D\u7684\u9690\u85CF\u6307\u4EE4\uFF0C\u5BA1\u67E5\u6240\u6709\u6CE8\u91CA\u5185\u5BB9", "Remove hidden instructions from HTML comments; review all comment content"],
352
+ ["zero-width character", "\u79FB\u9664\u96F6\u5BBD\u5B57\u7B26\uFF0C\u4F7F\u7528\u6587\u672C\u51C0\u5316\u51FD\u6570\u8FC7\u6EE4\u4E0D\u53EF\u89C1 Unicode \u5B57\u7B26", "Remove zero-width characters; use text sanitization to filter invisible Unicode characters"],
353
+ ["base64-like string", "\u5BA1\u67E5 base64 \u7F16\u7801\u5185\u5BB9\uFF0C\u89E3\u7801\u9A8C\u8BC1\u662F\u5426\u5305\u542B\u6CE8\u5165 payload", "Review base64-encoded content; decode and verify it does not contain injected payloads"],
354
+ // Permission Escalation
355
+ ["disables sandbox", "\u79FB\u9664 dangerouslyDisableSandbox\uFF0C\u5728\u6C99\u7BB1\u5185\u8FD0\u884C\u6240\u6709\u64CD\u4F5C", "Remove dangerouslyDisableSandbox; run all operations inside the sandbox"],
356
+ ["skips permission checks", "\u79FB\u9664 --dangerously-skip-permissions\uFF0C\u4FDD\u7559\u6743\u9650\u6821\u9A8C\u673A\u5236", "Remove --dangerously-skip-permissions; keep permission checks enabled"],
357
+ ["sudo usage", "\u79FB\u9664 sudo\uFF0C\u6539\u7528\u7528\u6237\u6001\u6743\u9650\u6216 Linux capabilities (cap_net_bind_service \u7B49)", "Remove sudo; use user-level permissions or Linux capabilities instead"],
358
+ ["chmod 777", "\u6539\u7528\u6700\u5C0F\u6743\u9650 chmod 755 \u6216 644\uFF0C\u907F\u514D world-writable", "Use minimal permissions (chmod 755 or 644); avoid world-writable"],
359
+ ["chown root", "\u907F\u514D\u4FEE\u6539\u6587\u4EF6\u5C5E\u4E3B\u4E3A root\uFF0C\u4F7F\u7528\u666E\u901A\u7528\u6237\u6743\u9650\u8FD0\u884C", "Avoid changing ownership to root; run with normal user privileges"],
360
+ ["modifying claude settings", "\u4E0D\u5E94\u76F4\u63A5\u4FEE\u6539 .claude/settings.json\uFF0C\u4F7F\u7528 CLI \u914D\u7F6E\u63A5\u53E3", "Do not modify .claude/settings.json directly; use the CLI configuration interface"],
361
+ ["references allowedtools", "\u660E\u786E\u58F0\u660E\u9700\u8981\u7684\u5DE5\u5177\u5217\u8868\uFF0C\u907F\u514D\u8FD0\u884C\u65F6\u52A8\u6001\u4FEE\u6539 allowedTools", "Explicitly declare required tools; avoid dynamically modifying allowedTools at runtime"],
362
+ ["bypasses verification", "\u79FB\u9664 --no-verify\uFF0C\u4FDD\u7559 pre-commit/pre-push hooks \u6821\u9A8C", "Remove --no-verify; keep pre-commit/pre-push hook checks enabled"],
363
+ ["overly permissive chmod", "\u6539\u7528\u6700\u5C0F\u5FC5\u8981\u6743\u9650\uFF0C\u5982 chmod 644 (\u6587\u4EF6) \u6216 755 (\u76EE\u5F55/\u811A\u672C)", "Use least necessary permissions, e.g. chmod 644 (files) or 755 (dirs/scripts)"],
364
+ ["setuid", "\u79FB\u9664 setuid/setgid \u4F4D\uFF0C\u4F7F\u7528 capabilities \u6216\u72EC\u7ACB\u670D\u52A1\u8D26\u6237\u66FF\u4EE3", "Remove setuid/setgid bits; use capabilities or dedicated service accounts instead"],
365
+ ["setgid", "\u79FB\u9664 setuid/setgid \u4F4D\uFF0C\u4F7F\u7528 capabilities \u6216\u72EC\u7ACB\u670D\u52A1\u8D26\u6237\u66FF\u4EE3", "Remove setuid/setgid bits; use capabilities or dedicated service accounts instead"],
366
+ // Data Exfiltration
367
+ ["reads .env file", "\u907F\u514D\u5728 skill \u4E2D\u76F4\u63A5\u8BFB\u53D6 .env\uFF0C\u6539\u7531\u5BBF\u4E3B\u73AF\u5883\u6CE8\u5165\u6240\u9700\u53D8\u91CF", "Avoid reading .env directly in skill; inject required variables from the host environment"],
368
+ ["accesses ssh directory", "\u79FB\u9664\u5BF9 ~/.ssh/ \u7684\u8BBF\u95EE\uFF0C\u4F7F\u7528 SSH agent \u6216\u53D7\u63A7\u5BC6\u94A5\u6CE8\u5165", "Remove access to ~/.ssh/; use SSH agent or controlled key injection"],
369
+ ["reads /etc/passwd", "\u79FB\u9664\u5BF9 /etc/passwd \u7684\u8BFB\u53D6\uFF0C\u4F7F\u7528 getent \u6216\u6807\u51C6\u5E93 API", "Remove /etc/passwd reads; use getent or standard library APIs"],
370
+ ["reads /etc/shadow", "\u79FB\u9664\u5BF9 /etc/shadow \u7684\u8BBF\u95EE\uFF0C\u8FD9\u662F\u9AD8\u6743\u9650\u654F\u611F\u6587\u4EF6", "Remove /etc/shadow access; this is a high-privilege sensitive file"],
371
+ ["reads aws credentials", "\u4F7F\u7528 IAM \u89D2\u8272\u6216\u73AF\u5883\u53D8\u91CF\u66FF\u4EE3\u76F4\u63A5\u8BFB\u53D6 ~/.aws/credentials", "Use IAM roles or environment variables instead of reading ~/.aws/credentials"],
372
+ ["reads kubernetes config", "\u4F7F\u7528 ServiceAccount token \u6216 KUBECONFIG \u73AF\u5883\u53D8\u91CF", "Use ServiceAccount tokens or the KUBECONFIG environment variable"],
373
+ ["reads credentials file", "\u4F7F\u7528\u5BC6\u94A5\u7BA1\u7406\u670D\u52A1\u6216\u73AF\u5883\u53D8\u91CF\uFF0C\u4E0D\u786C\u7F16\u7801\u51ED\u8BC1\u8DEF\u5F84", "Use a secrets manager or environment variables; do not hardcode credential paths"],
374
+ ["reads claude settings", "\u907F\u514D\u8BFB\u53D6 .claude/settings\uFF0C\u4F7F\u7528\u5B98\u65B9 API \u83B7\u53D6\u914D\u7F6E", "Avoid reading .claude/settings; use the official API to get configuration"],
375
+ ["curl post", "\u5BA1\u67E5 POST \u8BF7\u6C42\u76EE\u6807 URL\uFF0C\u786E\u4FDD\u6570\u636E\u4EC5\u53D1\u5F80\u53EF\u4FE1\u7AEF\u70B9", "Review POST request target URLs; ensure data is only sent to trusted endpoints"],
376
+ ["curl with data payload", "\u5BA1\u67E5 curl --data \u8BF7\u6C42\uFF0C\u786E\u8BA4\u53D1\u9001\u5185\u5BB9\u4E0D\u542B\u654F\u611F\u4FE1\u606F", "Review curl --data requests; confirm payloads do not contain sensitive information"],
377
+ ["requests.post", "\u5BA1\u67E5 requests.post \u76EE\u6807 URL\uFF0C\u6DFB\u52A0 URL \u767D\u540D\u5355\u6821\u9A8C", "Review requests.post target URLs; add URL allowlist validation"],
378
+ ["fetch post", "\u5BA1\u67E5 fetch POST \u76EE\u6807\uFF0C\u786E\u4FDD\u4EC5\u5411\u53EF\u4FE1\u57DF\u540D\u53D1\u9001\u6570\u636E", "Review fetch POST targets; ensure data is only sent to trusted domains"],
379
+ ["urllib outbound", "\u5BA1\u67E5 urllib \u8BF7\u6C42\u76EE\u6807\uFF0C\u6DFB\u52A0 URL \u767D\u540D\u5355", "Review urllib request targets; add URL allowlist"],
380
+ ["http.client outbound", "\u5BA1\u67E5 http.client \u8FDE\u63A5\u76EE\u6807\uFF0C\u9650\u5236\u5141\u8BB8\u8FDE\u63A5\u7684\u57DF\u540D", "Review http.client connection targets; restrict allowed domains"],
381
+ ["webhook url", "\u5BA1\u67E5 webhook URL \u5F52\u5C5E\uFF0C\u786E\u4FDD\u6570\u636E\u4E0D\u5916\u6CC4\u5230\u7B2C\u4E09\u65B9", "Review webhook URL ownership; ensure data is not leaked to third parties"],
382
+ ["ngrok tunnel", "\u79FB\u9664 ngrok \u96A7\u9053\uFF0C\u4F7F\u7528\u53D7\u63A7\u7684\u53CD\u5411\u4EE3\u7406\u6216 VPN", "Remove ngrok tunnel; use a controlled reverse proxy or VPN"],
383
+ ["credential exfiltration", "\u9694\u79BB\u73AF\u5883\u53D8\u91CF\u8BFB\u53D6\u4E0E\u7F51\u7EDC\u8BF7\u6C42\uFF0C\u907F\u514D\u5728\u540C\u4E00\u4E0A\u4E0B\u6587\u4E2D\u64CD\u4F5C", "Isolate environment variable reads from network requests; avoid both in the same context"],
384
+ ["data collection/analytics endpoint", "\u5BA1\u67E5\u6570\u636E\u6536\u96C6\u7AEF\u70B9\uFF0C\u786E\u8BA4\u662F\u5426\u4E3A\u5FC5\u8981\u529F\u80FD\u5E76\u83B7\u5F97\u7528\u6237\u77E5\u60C5\u540C\u610F", "Review data collection endpoints; confirm necessity and obtain user consent"],
385
+ // Destructive Operations
386
+ ["rm -rf on root", "\u7EDD\u5BF9\u4E0D\u8981\u4F7F\u7528 rm -rf /\uFF0C\u6DFB\u52A0\u8DEF\u5F84\u767D\u540D\u5355\u6821\u9A8C", "Never use rm -rf /; add path allowlist validation"],
387
+ ["rm -rf", "\u6DFB\u52A0\u8DEF\u5F84\u767D\u540D\u5355\u6821\u9A8C\uFF0C\u4F7F\u7528 --interactive \u6216\u5148 dry-run \u786E\u8BA4", "Add path allowlist validation; use --interactive or dry-run first"],
388
+ ["git reset --hard", "\u6539\u7528 git stash \u4FDD\u5B58\u66F4\u6539\uFF0C\u6216\u4F7F\u7528 git reset --soft \u4FDD\u7559\u6682\u5B58", "Use git stash to save changes, or git reset --soft to keep staging"],
389
+ ["git push --force", "\u6539\u7528 git push --force-with-lease \u9632\u6B62\u8986\u76D6\u4ED6\u4EBA\u63D0\u4EA4", "Use git push --force-with-lease to prevent overwriting others' commits"],
390
+ ["git push -f", "\u6539\u7528 git push --force-with-lease \u9632\u6B62\u8986\u76D6\u4ED6\u4EBA\u63D0\u4EA4", "Use git push --force-with-lease to prevent overwriting others' commits"],
391
+ ["git clean -f", "\u6DFB\u52A0\u786E\u8BA4\u63D0\u793A\uFF0C\u6216\u4F7F\u7528 git clean -n \u5148\u9884\u89C8\u5C06\u5220\u9664\u7684\u6587\u4EF6", "Add confirmation prompt, or use git clean -n to preview files to be deleted"],
392
+ ["drop table", "\u6DFB\u52A0\u786E\u8BA4\u63D0\u793A\u548C\u5907\u4EFD\u673A\u5236\uFF0C\u4F7F\u7528 migration \u5DE5\u5177\u7BA1\u7406 schema \u53D8\u66F4", "Add confirmation and backup; use migration tools to manage schema changes"],
393
+ ["delete from without where", "\u6DFB\u52A0 WHERE \u6761\u4EF6\u9650\u5236\u5220\u9664\u8303\u56F4\uFF0C\u6216\u4F7F\u7528\u4E8B\u52A1 + \u786E\u8BA4\u673A\u5236", "Add WHERE clause to limit scope, or use transactions with confirmation"],
394
+ ["truncate table", "\u4F7F\u7528 DELETE + WHERE \u66FF\u4EE3 TRUNCATE\uFF0C\u6DFB\u52A0\u5907\u4EFD\u548C\u786E\u8BA4\u673A\u5236", "Use DELETE + WHERE instead of TRUNCATE; add backup and confirmation"],
395
+ ["shutil.rmtree", "\u6DFB\u52A0\u8DEF\u5F84\u767D\u540D\u5355\u6821\u9A8C\uFF0C\u786E\u4FDD\u76EE\u6807\u76EE\u5F55\u5728\u9884\u671F\u8303\u56F4\u5185", "Add path allowlist validation; ensure target directory is within expected scope"],
396
+ ["os.remove", "\u5220\u9664\u524D\u6821\u9A8C\u8DEF\u5F84\u662F\u5426\u5728\u9884\u671F\u8303\u56F4\u5185\uFF0C\u6DFB\u52A0\u786E\u8BA4\u903B\u8F91", "Validate path is within expected scope before deletion; add confirmation"],
397
+ ["os.unlink", "\u5220\u9664\u524D\u6821\u9A8C\u8DEF\u5F84\u662F\u5426\u5728\u9884\u671F\u8303\u56F4\u5185\uFF0C\u6DFB\u52A0\u786E\u8BA4\u903B\u8F91", "Validate path is within expected scope before deletion; add confirmation"],
398
+ ["disk format", "\u79FB\u9664\u78C1\u76D8\u683C\u5F0F\u5316\u547D\u4EE4\uFF0C\u8FD9\u5728 skill \u4E2D\u4E0D\u5E94\u51FA\u73B0", "Remove disk format command; this should not appear in a skill"],
399
+ ["fdisk", "\u79FB\u9664 fdisk \u547D\u4EE4\uFF0C\u78C1\u76D8\u5206\u533A\u64CD\u4F5C\u4E0D\u5E94\u5728 skill \u4E2D\u6267\u884C", "Remove fdisk command; disk partitioning should not be done in a skill"],
400
+ ["mkfs", "\u79FB\u9664 mkfs \u547D\u4EE4\uFF0C\u6587\u4EF6\u7CFB\u7EDF\u521B\u5EFA\u4E0D\u5E94\u5728 skill \u4E2D\u6267\u884C", "Remove mkfs command; filesystem creation should not be done in a skill"],
401
+ ["dd - low-level", "\u6DFB\u52A0\u660E\u786E\u7684 of= \u76EE\u6807\u6821\u9A8C\uFF0C\u907F\u514D\u8BEF\u8986\u76D6\u91CD\u8981\u8BBE\u5907/\u6587\u4EF6", "Add explicit of= target validation; avoid overwriting critical devices/files"],
402
+ ["fork bomb", "\u79FB\u9664 fork bomb \u4EE3\u7801\uFF0C\u8FD9\u662F\u6076\u610F\u6216\u8BEF\u64CD\u4F5C", "Remove fork bomb code; this is malicious or accidental"],
403
+ // Supply Chain
404
+ ["pipe-to-shell", "\u4E0B\u8F7D\u811A\u672C\u540E\u5148\u5BA1\u67E5\u518D\u6267\u884C\uFF1Acurl -o script.sh URL && review && bash script.sh", "Download scripts first, review, then execute: curl -o script.sh URL && review && bash script.sh"],
405
+ ["pipe-to-sudo", "\u6C38\u8FDC\u4E0D\u8981 curl | sudo\uFF0C\u5148\u4E0B\u8F7D\u5BA1\u67E5\u518D\u4EE5\u6700\u5C0F\u6743\u9650\u6267\u884C", "Never curl | sudo; download, review, then execute with least privilege"],
406
+ ["git clone", "\u9501\u5B9A clone \u7684 commit hash \u6216 tag\uFF0C\u907F\u514D\u62C9\u53D6\u672A\u5BA1\u67E5\u7684\u4EE3\u7801", "Pin clone to a commit hash or tag; avoid pulling unreviewed code"],
407
+ ["pip install without version", "\u4F7F\u7528 pip install package==x.y.z \u9501\u5B9A\u7248\u672C\uFF0C\u6216\u7528 requirements.txt + hash", "Use pip install package==x.y.z to pin versions, or requirements.txt with hashes"],
408
+ ["npm install without version", "\u4F7F\u7528 npm install package@x.y.z \u9501\u5B9A\u7248\u672C\uFF0C\u6216\u7528 package-lock.json", "Use npm install package@x.y.z to pin versions, or use package-lock.json"],
409
+ ["go get", "\u4F7F\u7528 go.sum \u9501\u5B9A\u4F9D\u8D56 hash\uFF0C\u6307\u5B9A\u660E\u786E\u7248\u672C\u53F7", "Use go.sum to lock dependency hashes; specify explicit version numbers"],
410
+ ["dockerfile from without digest", "\u6539\u7528 FROM image@sha256:... \u9501\u5B9A\u955C\u50CF\u6458\u8981", "Use FROM image@sha256:... to pin image digest"],
411
+ // Code Security
412
+ ["shell=true", "\u6539\u7528 subprocess.run(cmd_list, shell=False)\uFF0C\u53C2\u6570\u4EE5\u5217\u8868\u4F20\u5165", "Use subprocess.run(cmd_list, shell=False); pass arguments as a list"],
413
+ ["os.system", "\u6539\u7528 subprocess.run(cmd_list, shell=False)\uFF0C\u907F\u514D shell \u6CE8\u5165", "Use subprocess.run(cmd_list, shell=False); avoid shell injection"],
414
+ ["os.popen", "\u6539\u7528 subprocess.run(cmd_list, capture_output=True, shell=False)", "Use subprocess.run(cmd_list, capture_output=True, shell=False)"],
415
+ ["eval()", "\u7528 ast.literal_eval() \u66FF\u4EE3 eval()\uFF0C\u6216\u7528 JSON/\u914D\u7F6E\u6587\u4EF6\u89E3\u6790", "Use ast.literal_eval() instead of eval(), or parse with JSON/config files"],
416
+ ["exec()", "\u907F\u514D\u52A8\u6001\u6267\u884C\u4EE3\u7801\uFF0C\u6539\u7528\u51FD\u6570\u6620\u5C04 (dict dispatch) \u6216\u914D\u7F6E\u9A71\u52A8", "Avoid dynamic code execution; use function dispatch (dict mapping) or config-driven logic"],
417
+ ["compile()", "\u907F\u514D\u52A8\u6001\u7F16\u8BD1\u4EE3\u7801\u5B57\u7B26\u4E32\uFF0C\u4F7F\u7528\u9884\u5B9A\u4E49\u51FD\u6570\u6216\u6A21\u677F\u5F15\u64CE", "Avoid dynamically compiling code strings; use predefined functions or template engines"],
418
+ ["pickle.load", "\u6539\u7528 json.load() \u6216 msgpack\uFF0C\u907F\u514D\u53CD\u5E8F\u5217\u5316\u6267\u884C\u4EFB\u610F\u4EE3\u7801", "Use json.load() or msgpack instead; avoid arbitrary code execution via deserialization"],
419
+ ["yaml.load without safeloader", "\u6539\u7528 yaml.safe_load() \u6216 yaml.load(Loader=yaml.SafeLoader)", "Use yaml.safe_load() or yaml.load(Loader=yaml.SafeLoader)"],
420
+ ["marshal.load", "\u6539\u7528 JSON \u6216\u5176\u4ED6\u5B89\u5168\u5E8F\u5217\u5316\u683C\u5F0F", "Use JSON or other safe serialization formats"],
421
+ ["sql injection", "\u4F7F\u7528\u53C2\u6570\u5316\u67E5\u8BE2 cursor.execute('SELECT * FROM t WHERE id=?', (id,))", "Use parameterized queries: cursor.execute('SELECT * FROM t WHERE id=?', (id,))"],
422
+ ["innerhtml", "\u4F7F\u7528 textContent \u66FF\u4EE3 innerHTML\uFF0C\u6216\u7528 DOMPurify \u51C0\u5316 HTML", "Use textContent instead of innerHTML, or sanitize with DOMPurify"],
423
+ ["dangerouslysetinnerhtml", "\u4F7F\u7528 DOMPurify \u51C0\u5316 HTML \u540E\u518D\u4F20\u5165\uFF0C\u6216\u6539\u7528\u5B89\u5168\u7684\u6E32\u67D3\u65B9\u5F0F", "Sanitize HTML with DOMPurify before passing, or use safe rendering methods"],
424
+ ["path traversal", "\u5BF9\u8DEF\u5F84\u8F93\u5165\u8FDB\u884C\u89C4\u8303\u5316 (os.path.realpath) \u5E76\u6821\u9A8C\u662F\u5426\u5728\u5141\u8BB8\u7684\u76EE\u5F55\u5185", "Normalize path input (os.path.realpath) and verify it is within allowed directories"],
425
+ // Credential Leaks
426
+ ["openai/anthropic api key", "\u4F7F\u7528\u73AF\u5883\u53D8\u91CF os.environ['ANTHROPIC_API_KEY']\uFF0C\u4E0D\u786C\u7F16\u7801\u5BC6\u94A5", "Use environment variable os.environ['ANTHROPIC_API_KEY']; do not hardcode keys"],
427
+ ["github personal access token", "\u4F7F\u7528 GitHub App token \u6216\u73AF\u5883\u53D8\u91CF\uFF0C\u4E0D\u786C\u7F16\u7801 PAT", "Use GitHub App tokens or environment variables; do not hardcode PATs"],
428
+ ["github oauth token", "\u4F7F\u7528 OAuth flow \u52A8\u6001\u83B7\u53D6 token\uFF0C\u4E0D\u5728\u4EE3\u7801\u4E2D\u5B58\u50A8", "Use OAuth flow to dynamically obtain tokens; do not store in code"],
429
+ ["aws access key", "\u4F7F\u7528 IAM \u89D2\u8272\u6216 AWS SSO\uFF0C\u4E0D\u786C\u7F16\u7801 Access Key", "Use IAM roles or AWS SSO; do not hardcode Access Keys"],
430
+ ["slack bot token", "\u4F7F\u7528\u73AF\u5883\u53D8\u91CF\u5B58\u50A8 Slack token\uFF0C\u4E0D\u786C\u7F16\u7801", "Store Slack tokens in environment variables; do not hardcode"],
431
+ ["slack user token", "\u4F7F\u7528\u73AF\u5883\u53D8\u91CF\u5B58\u50A8 Slack token\uFF0C\u4E0D\u786C\u7F16\u7801", "Store Slack tokens in environment variables; do not hardcode"],
432
+ ["gitlab personal access token", "\u4F7F\u7528\u73AF\u5883\u53D8\u91CF\u6216 CI/CD \u53D8\u91CF\u6CE8\u5165\uFF0C\u4E0D\u786C\u7F16\u7801", "Use environment variables or CI/CD variable injection; do not hardcode"],
433
+ ["jwt token detected", "\u68C0\u67E5 JWT \u662F\u5426\u4E3A\u6D4B\u8BD5\u6570\u636E\uFF0C\u751F\u4EA7 token \u4E0D\u5E94\u51FA\u73B0\u5728\u4EE3\u7801\u4E2D", "Check if JWT is test data; production tokens should not appear in code"],
434
+ ["hardcoded password", "\u4F7F\u7528\u73AF\u5883\u53D8\u91CF os.environ['DB_PASSWORD'] \u6216\u5BC6\u94A5\u7BA1\u7406\u670D\u52A1", "Use environment variables os.environ['DB_PASSWORD'] or a secrets manager"],
435
+ ["hardcoded secret", "\u4F7F\u7528\u73AF\u5883\u53D8\u91CF\u6216\u5BC6\u94A5\u7BA1\u7406\u670D\u52A1 (HashiCorp Vault, AWS Secrets Manager)", "Use environment variables or a secrets manager (HashiCorp Vault, AWS Secrets Manager)"],
436
+ ["private key in pem", "\u4F7F\u7528\u5BC6\u94A5\u7BA1\u7406\u670D\u52A1\u5B58\u50A8\u79C1\u94A5\uFF0C\u4E0D\u5728\u4EE3\u7801\u4ED3\u5E93\u4E2D\u4FDD\u5B58", "Store private keys in a secrets manager; do not keep them in code repositories"],
437
+ ["hardcoded email", "\u4F7F\u7528\u73AF\u5883\u53D8\u91CF\u6216\u914D\u7F6E\u6587\u4EF6\u6CE8\u5165\u90AE\u7BB1\u5730\u5740", "Use environment variables or config files to inject email addresses"],
438
+ // Least Privilege
439
+ ["no allowed-tools declared", "\u5728 SKILL.md frontmatter \u4E2D\u58F0\u660E allowed-tools \u5217\u8868\uFF0C\u9650\u5236\u53EF\u7528\u5DE5\u5177", "Declare an allowed-tools list in SKILL.md frontmatter to restrict available tools"],
440
+ ["shell access declared", "\u8BC4\u4F30\u662F\u5426\u771F\u6B63\u9700\u8981 shell \u8BBF\u95EE\uFF0C\u5C3D\u91CF\u4F7F\u7528\u4E13\u7528\u5DE5\u5177\u66FF\u4EE3", "Evaluate whether shell access is truly needed; prefer dedicated tools instead"],
441
+ ["dangerous tool combination", "\u62C6\u5206\u4E3A\u591A\u4E2A\u72EC\u7ACB skill\uFF0C\u6BCF\u4E2A\u53EA\u7533\u8BF7\u5FC5\u8981\u7684\u5DE5\u5177\u6743\u9650", "Split into separate skills, each requesting only the necessary tool permissions"],
442
+ // License
443
+ ["proprietary", "\u786E\u8BA4\u8BB8\u53EF\u8BC1\u517C\u5BB9\u6027\uFF0C\u5FC5\u8981\u65F6\u83B7\u53D6\u5546\u7528\u6388\u6743", "Verify license compatibility; obtain commercial authorization if needed"],
444
+ ["all rights reserved", "\u786E\u8BA4\u4EE3\u7801\u4F7F\u7528\u6743\u9650\uFF0C\u8003\u8651\u8054\u7CFB\u4F5C\u8005\u83B7\u53D6\u5F00\u6E90\u6388\u6743", "Verify usage permissions; consider contacting the author for an open-source license"],
445
+ ["copying prohibited", "\u786E\u8BA4\u662F\u5426\u6709\u5408\u6CD5\u4F7F\u7528\u6743\u9650\uFF0C\u5FC5\u8981\u65F6\u5BFB\u6C42\u66FF\u4EE3\u65B9\u6848", "Verify legitimate usage rights; seek alternatives if necessary"],
446
+ ["non-commercial", "\u786E\u8BA4\u4F7F\u7528\u573A\u666F\u662F\u5426\u7B26\u5408 non-commercial \u9650\u5236", "Verify the use case complies with non-commercial restrictions"],
447
+ ["kegg database", "\u786E\u8BA4\u5B66\u672F\u8BB8\u53EF\u8BC1\u8986\u76D6\u8303\u56F4\uFF0C\u5546\u7528\u9700\u5355\u72EC\u6388\u6743", "Verify academic license scope; commercial use requires separate authorization"],
448
+ ["benchling", "\u786E\u8BA4 Benchling API \u4F7F\u7528\u6761\u6B3E\u548C\u8D39\u7528", "Review Benchling API terms of service and associated costs"],
449
+ ["bigquery", "\u5BA1\u67E5 BigQuery \u6570\u636E\u6536\u96C6\u8303\u56F4\uFF0C\u786E\u8BA4\u8D39\u7528\u548C\u6570\u636E\u9690\u79C1\u5408\u89C4", "Review BigQuery data collection scope; confirm costs and data privacy compliance"],
450
+ ["snowflake", "\u786E\u8BA4 Snowflake \u4F7F\u7528\u6761\u6B3E\u548C\u6570\u636E\u4F20\u8F93\u5408\u89C4\u6027", "Review Snowflake terms of service and data transfer compliance"],
451
+ // Resource Abuse
452
+ ["while true loop", "\u6DFB\u52A0\u660E\u786E\u7684\u9000\u51FA\u6761\u4EF6\u548C\u6700\u5927\u8FED\u4EE3\u6B21\u6570\u9650\u5236 (\u5982 max_iterations=1000)", "Add explicit exit conditions and max iteration limits (e.g. max_iterations=1000)"],
453
+ ["while 1 loop", "\u6DFB\u52A0\u660E\u786E\u7684\u9000\u51FA\u6761\u4EF6\u548C\u6700\u5927\u8FED\u4EE3\u6B21\u6570\u9650\u5236", "Add explicit exit conditions and max iteration limits"],
454
+ ["while(true) loop", "\u6DFB\u52A0 break \u6761\u4EF6\u548C\u8D85\u65F6\u673A\u5236", "Add break conditions and timeout mechanisms"],
455
+ ["for(;;) infinite loop", "\u6DFB\u52A0 break \u6761\u4EF6\u548C\u6700\u5927\u8FED\u4EE3\u6B21\u6570", "Add break conditions and max iteration limits"],
456
+ ["very large retmax", "\u964D\u4F4E retmax \u503C\uFF0C\u4F7F\u7528\u5206\u9875\u67E5\u8BE2\u907F\u514D\u4E00\u6B21\u62C9\u53D6\u8FC7\u591A\u6570\u636E", "Reduce retmax value; use pagination to avoid fetching too much data at once"],
457
+ ["very large limit", "\u4F7F\u7528\u5408\u7406\u7684 limit \u503C\uFF0C\u914D\u5408\u5206\u9875\u9010\u6B65\u83B7\u53D6\u6570\u636E", "Use reasonable limit values with pagination to fetch data incrementally"],
458
+ ["sleep(0) in potential busy loop", "\u589E\u52A0\u5408\u7406\u7684 sleep \u95F4\u9694 (\u5982 0.1s)\uFF0C\u907F\u514D CPU \u7A7A\u8F6C", "Add reasonable sleep intervals (e.g. 0.1s); avoid CPU busy-waiting"],
459
+ ["unlimited/excessive retry", "\u8BBE\u7F6E\u5408\u7406\u7684 retry \u4E0A\u9650 (\u5982 max_retries=3) \u548C\u6307\u6570\u9000\u907F", "Set reasonable retry limits (e.g. max_retries=3) with exponential backoff"],
460
+ ["recursive function", "\u6DFB\u52A0\u9012\u5F52\u6DF1\u5EA6\u9650\u5236\u6216\u6539\u7528\u8FED\u4EE3\u5B9E\u73B0", "Add recursion depth limits or convert to iterative implementation"]
461
+ ];
462
+ var DIMENSION_REMEDIATIONS = [
463
+ ["Prompt Injection", "\u5BA1\u67E5\u6240\u6709\u7528\u6237\u8F93\u5165\u62FC\u63A5\u70B9\uFF0C\u4F7F\u7528\u7ED3\u6784\u5316 prompt \u6A21\u677F\uFF0C\u907F\u514D\u76F4\u63A5\u62FC\u63A5\u7528\u6237\u5185\u5BB9\u5230\u7CFB\u7EDF\u6307\u4EE4\u4E2D", "Review all user input concatenation points; use structured prompt templates; avoid concatenating user content into system instructions"],
464
+ ["Permission Escalation", "\u9075\u5FAA\u6700\u5C0F\u6743\u9650\u539F\u5219\uFF0C\u79FB\u9664 sudo/chmod 777\uFF0C\u4F7F\u7528\u7528\u6237\u6001\u64CD\u4F5C\u548C Linux capabilities", "Follow the principle of least privilege; remove sudo/chmod 777; use user-level operations and Linux capabilities"],
465
+ ["Data Exfiltration", "\u5BA1\u67E5\u6240\u6709\u5916\u90E8 URL \u548C API \u8C03\u7528\uFF0C\u786E\u4FDD\u6570\u636E\u53EA\u53D1\u5F80\u53EF\u4FE1\u7AEF\u70B9\uFF0C\u654F\u611F\u64CD\u4F5C\u9700\u7528\u6237\u786E\u8BA4", "Review all external URLs and API calls; ensure data is only sent to trusted endpoints; require user confirmation for sensitive operations"],
466
+ ["Destructive Operations", "\u6DFB\u52A0\u8DEF\u5F84\u767D\u540D\u5355\u3001\u786E\u8BA4\u63D0\u793A\u3001dry-run \u6A21\u5F0F\uFF0C\u7834\u574F\u6027\u64CD\u4F5C\u524D\u5148\u5907\u4EFD", "Add path allowlists, confirmation prompts, and dry-run mode; back up before destructive operations"],
467
+ ["Supply Chain", "\u9501\u5B9A\u4F9D\u8D56\u7248\u672C\uFF0C\u4F7F\u7528 hash \u6821\u9A8C\uFF0C\u4E0B\u8F7D\u540E\u5148\u5BA1\u67E5\u518D\u6267\u884C\uFF0C\u907F\u514D pipe-to-shell", "Pin dependency versions; use hash verification; download and review before executing; avoid pipe-to-shell"],
468
+ ["Code Security", "\u907F\u514D eval/exec/shell=True\uFF0C\u4F7F\u7528\u53C2\u6570\u5316\u67E5\u8BE2\uFF0C\u5BF9\u5916\u90E8\u8F93\u5165\u505A\u6821\u9A8C\u548C\u51C0\u5316", "Avoid eval/exec/shell=True; use parameterized queries; validate and sanitize external input"],
469
+ ["Credential Leaks", "\u4F7F\u7528\u73AF\u5883\u53D8\u91CF\u6216\u5BC6\u94A5\u7BA1\u7406\u670D\u52A1\u5B58\u50A8\u51ED\u8BC1\uFF0C\u4E0D\u5728\u4EE3\u7801\u4E2D\u786C\u7F16\u7801\u4EFB\u4F55\u5BC6\u94A5/\u5BC6\u7801", "Use environment variables or secrets managers to store credentials; never hardcode keys/passwords in code"],
470
+ ["Least Privilege", "\u5728 SKILL.md \u4E2D\u58F0\u660E\u6700\u5C0F\u5DE5\u5177\u96C6\uFF0C\u907F\u514D Bash(*) \u901A\u914D\u7B26\u6743\u9650\uFF0C\u62C6\u5206\u9AD8\u6743\u9650\u64CD\u4F5C", "Declare minimal toolsets in SKILL.md; avoid Bash(*) wildcard permissions; split high-privilege operations"],
471
+ ["License Compliance", "\u68C0\u67E5\u4F9D\u8D56\u8BB8\u53EF\u8BC1\u517C\u5BB9\u6027\uFF0C\u6807\u6CE8\u5546\u4E1A\u670D\u52A1\u4F7F\u7528\uFF0C\u786E\u4FDD\u5408\u89C4\u4F7F\u7528", "Check dependency license compatibility; annotate commercial service usage; ensure compliant use"],
472
+ ["Resource Abuse", "\u6DFB\u52A0\u5FAA\u73AF\u9000\u51FA\u6761\u4EF6\u3001\u8BF7\u6C42\u9650\u6D41\u3001\u5408\u7406\u7684 retry \u4E0A\u9650\uFF0C\u907F\u514D\u65E0\u9650\u6D88\u8017\u8D44\u6E90", "Add loop exit conditions, request throttling, and reasonable retry limits; avoid unbounded resource consumption"]
473
+ ];
474
+ function matchRemediation(description) {
475
+ const descLower = description.toLowerCase();
476
+ for (const [key, zh, en] of REMEDIATIONS) {
477
+ if (descLower.includes(key)) {
478
+ return [zh, en];
479
+ }
480
+ }
481
+ return ["", ""];
482
+ }
483
+ function dimensionRemediation(dimension) {
484
+ const dimLower = dimension.toLowerCase();
485
+ for (const [key, zh, en] of DIMENSION_REMEDIATIONS) {
486
+ if (dimLower.includes(key.toLowerCase())) {
487
+ return [zh, en];
488
+ }
489
+ }
490
+ return ["", ""];
491
+ }
492
+
493
+ // src/scanner/dimensions/prompt-injection.ts
494
+ var INJECTION_PHRASES = [
495
+ ["ignore\\s+(all\\s+)?previous\\s+instructions", "CRITICAL", "Prompt injection: ignore previous instructions"],
496
+ ["you\\s+are\\s+now\\b", "CRITICAL", "Prompt injection: role override 'you are now'"],
497
+ ["disregard\\s+(all\\s+)?(above|previous|prior)", "CRITICAL", "Prompt injection: disregard directive"],
498
+ ["override\\s+system\\s+(prompt|instructions|message)", "CRITICAL", "Prompt injection: override system prompt"],
499
+ ["\\bjailbreak\\b", "CRITICAL", "Prompt injection keyword: jailbreak"],
500
+ ["pretend\\s+you\\s+are\\b", "CRITICAL", "Role-play induction: pretend you are"],
501
+ ["act\\s+as\\s+if\\s+you", "CRITICAL", "Role-play induction: act as if"],
502
+ ["roleplay\\s+as\\b", "CRITICAL", "Role-play induction: roleplay as"],
503
+ ["do\\s+anything\\s+now", "CRITICAL", "DAN-style jailbreak attempt"],
504
+ ["from\\s+now\\s+on,?\\s+you\\s+(will|must|should)", "CRITICAL", "Behavioral override: from now on you will"]
505
+ ];
506
+ function checkPromptInjection(ctx, ref) {
507
+ const findings = scanPatterns(ctx.fileContents, INJECTION_PHRASES, ref);
508
+ const auxRules = ctx.rules["prompt_injection_auxiliary"] || [];
509
+ const zwcRule = auxRules.find((r) => r.id === "PI-AUX-01");
510
+ if (!zwcRule || zwcRule.enabled !== false) {
511
+ const severity = zwcRule?.severity || "MEDIUM";
512
+ const zwcRe = /[\u200b\u200c\u200d\ufeff\u2060]/g;
513
+ for (const [relPath, content] of ctx.fileContents) {
514
+ let m;
515
+ while ((m = zwcRe.exec(content)) !== null) {
516
+ findings.push({
517
+ dimension: "",
518
+ severity,
519
+ filePath: relPath,
520
+ lineNumber: getLineNumber(content, m.index),
521
+ pattern: "zero-width character",
522
+ description: `Zero-width character U+${m[0].charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")} detected (possible steganographic injection)`,
523
+ reference: ref,
524
+ remediationZh: "",
525
+ remediationEn: ""
526
+ });
527
+ }
528
+ }
529
+ }
530
+ const b64Rule = auxRules.find((r) => r.id === "PI-AUX-02");
531
+ if (!b64Rule || b64Rule.enabled !== false) {
532
+ const severity = b64Rule?.severity || "MEDIUM";
533
+ const b64Re = /[A-Za-z0-9+/]{200,}={0,2}/g;
534
+ for (const [relPath, content] of ctx.fileContents) {
535
+ let m;
536
+ while ((m = b64Re.exec(content)) !== null) {
537
+ findings.push({
538
+ dimension: "",
539
+ severity,
540
+ filePath: relPath,
541
+ lineNumber: getLineNumber(content, m.index),
542
+ pattern: "long base64 string",
543
+ description: `Long base64-like string (${m[0].length} chars) - may hide injected payload`,
544
+ reference: ref,
545
+ remediationZh: "",
546
+ remediationEn: ""
547
+ });
548
+ }
549
+ }
550
+ }
551
+ const htmlRule = auxRules.find((r) => r.id === "PI-AUX-03");
552
+ if (!htmlRule || htmlRule.enabled !== false) {
553
+ const severity = htmlRule?.severity || "HIGH";
554
+ const whitelist = htmlRule?.whitelist || [];
555
+ const keywords = ["instruction", "ignore", "override", "system prompt", "you must", "you are now"];
556
+ const commentRe = /<!--([\s\S]*?)-->/g;
557
+ for (const [relPath, content] of ctx.fileContents) {
558
+ let m;
559
+ while ((m = commentRe.exec(content)) !== null) {
560
+ const comment = m[1].toLowerCase();
561
+ if (keywords.some((kw) => comment.includes(kw))) {
562
+ if (whitelist.length && whitelist.some((w) => w && comment.includes(w.toLowerCase()))) continue;
563
+ findings.push({
564
+ dimension: "",
565
+ severity,
566
+ filePath: relPath,
567
+ lineNumber: getLineNumber(content, m.index),
568
+ pattern: "<!-- hidden instruction -->",
569
+ description: "Hidden instruction detected in HTML comment",
570
+ reference: ref,
571
+ remediationZh: "",
572
+ remediationEn: ""
573
+ });
574
+ }
575
+ }
576
+ }
577
+ }
578
+ return findings;
579
+ }
580
+
581
+ // src/scanner/dimensions/permission-escalation.ts
582
+ var PATTERNS = [
583
+ ["dangerouslyDisableSandbox", "CRITICAL", "Disables sandbox protection"],
584
+ ["--dangerously-skip-permissions", "CRITICAL", "Skips permission checks"],
585
+ ["\\bsudo\\s+", "HIGH", "sudo usage - requires root privileges"],
586
+ ["chmod\\s+777\\b", "HIGH", "chmod 777 - world-writable permissions"],
587
+ ["chown\\s+root\\b", "HIGH", "chown root - changing ownership to root"],
588
+ ["\\.claude/settings\\.json", "HIGH", "Modifying Claude settings file"],
589
+ ["\\ballowedTools\\b", "MEDIUM", "References allowedTools configuration"],
590
+ ["--no-verify\\b", "MEDIUM", "Bypasses verification hooks"],
591
+ ["chmod\\s+[0-7]*[67][0-7]{2}\\b", "MEDIUM", "Overly permissive chmod"],
592
+ ["setuid\\b|setgid\\b", "HIGH", "setuid/setgid - elevated privilege bits"]
593
+ ];
594
+ function checkPermissionEscalation(ctx, ref) {
595
+ return scanPatterns(ctx.fileContents, PATTERNS, ref);
596
+ }
597
+
598
+ // src/scanner/dimensions/data-exfiltration.ts
599
+ function checkDataExfiltration(ctx, ref) {
600
+ const findings = [];
601
+ const environRead = /os\.environ/.test(ctx.fullText);
602
+ const networkSend = /(requests\.post|curl.*POST|fetch.*POST|urllib)/.test(ctx.fullText);
603
+ if (environRead && networkSend) {
604
+ findings.push({
605
+ dimension: "",
606
+ severity: "HIGH",
607
+ filePath: "(multiple files)",
608
+ lineNumber: 0,
609
+ pattern: "os.environ + network send",
610
+ description: "Environment variables read AND outbound network requests detected - potential credential exfiltration",
611
+ reference: ref,
612
+ remediationZh: "",
613
+ remediationEn: ""
614
+ });
615
+ }
616
+ findings.push(...applyRules("data_exfiltration", ctx.fileContents, ctx.rules, ref));
617
+ return findings;
618
+ }
619
+
620
+ // src/scanner/dimensions/destructive-operations.ts
621
+ var PATTERNS2 = [
622
+ ["rm\\s+-rf\\s+/", "CRITICAL", "rm -rf on root path - catastrophic deletion"],
623
+ ["rm\\s+-rf\\b", "HIGH", "rm -rf - recursive forced deletion"],
624
+ ["git\\s+reset\\s+--hard\\b", "HIGH", "git reset --hard - discards all changes"],
625
+ ["git\\s+push\\s+--force\\b", "HIGH", "git push --force - overwrites remote history"],
626
+ ["git\\s+push\\s+-f\\b", "HIGH", "git push -f - force push"],
627
+ ["git\\s+clean\\s+-f", "HIGH", "git clean -f - deletes untracked files"],
628
+ ["DROP\\s+TABLE\\b", "CRITICAL", "DROP TABLE - database table deletion"],
629
+ ["DELETE\\s+FROM\\b(?!.*WHERE)", "HIGH", "DELETE FROM without WHERE - deletes all rows"],
630
+ ["TRUNCATE\\s+TABLE\\b", "CRITICAL", "TRUNCATE TABLE - removes all data"],
631
+ ["shutil\\.rmtree\\s*\\(", "HIGH", "shutil.rmtree - recursive directory deletion"],
632
+ ["os\\.remove\\s*\\(", "MEDIUM", "os.remove - file deletion"],
633
+ ["os\\.unlink\\s*\\(", "MEDIUM", "os.unlink - file deletion"],
634
+ ["\\bformat\\s+[a-zA-Z]:", "CRITICAL", "Disk format command"],
635
+ ["\\bfdisk\\b", "CRITICAL", "fdisk - disk partitioning"],
636
+ ["\\bmkfs\\b", "CRITICAL", "mkfs - creates filesystem (destroys data)"],
637
+ ["\\bdd\\s+if=", "HIGH", "dd - low-level data copy/overwrite"],
638
+ [":\\(\\)\\{\\s*:\\|:&\\s*\\};:", "CRITICAL", "Fork bomb detected"]
639
+ ];
640
+ function checkDestructiveOperations(ctx, ref) {
641
+ return scanPatterns(ctx.fileContents, PATTERNS2, ref);
642
+ }
643
+
644
+ // src/scanner/dimensions/supply-chain.ts
645
+ function checkSupplyChain(ctx, ref) {
646
+ const findings = [];
647
+ const allRules = ctx.rules["supply_chain"] || [];
648
+ const dockerfileRules = allRules.filter((r) => r.id === "SC-08");
649
+ const otherRules = allRules.filter((r) => r.id !== "SC-08");
650
+ for (const rule of otherRules) {
651
+ if (!rule.enabled) continue;
652
+ if (!rule.pattern) continue;
653
+ const whitelist = rule.whitelist || [];
654
+ let re;
655
+ try {
656
+ re = new RegExp(rule.pattern, "gi");
657
+ } catch {
658
+ continue;
659
+ }
660
+ for (const [relPath, content] of ctx.fileContents) {
661
+ let m;
662
+ while ((m = re.exec(content)) !== null) {
663
+ const matched = m[0];
664
+ if (whitelist.some((w) => w && matched.toLowerCase().includes(w.toLowerCase()))) continue;
665
+ findings.push({
666
+ dimension: "",
667
+ severity: rule.severity || "MEDIUM",
668
+ filePath: relPath,
669
+ lineNumber: getLineNumber(content, m.index),
670
+ pattern: rule.pattern,
671
+ description: rule.description || "",
672
+ reference: ref,
673
+ remediationZh: "",
674
+ remediationEn: ""
675
+ });
676
+ }
677
+ }
678
+ }
679
+ for (const rule of dockerfileRules) {
680
+ if (!rule.enabled) continue;
681
+ if (!rule.pattern) continue;
682
+ const whitelist = rule.whitelist || [];
683
+ let re;
684
+ try {
685
+ re = new RegExp(rule.pattern, "gm");
686
+ } catch {
687
+ continue;
688
+ }
689
+ for (const [relPath, content] of ctx.fileContents) {
690
+ if (!relPath.toLowerCase().includes("dockerfile") && !relPath.endsWith("Dockerfile")) continue;
691
+ let m;
692
+ while ((m = re.exec(content)) !== null) {
693
+ const matched = m[0];
694
+ if (whitelist.some((w) => w && matched.toLowerCase().includes(w.toLowerCase()))) continue;
695
+ findings.push({
696
+ dimension: "",
697
+ severity: rule.severity || "LOW",
698
+ filePath: relPath,
699
+ lineNumber: getLineNumber(content, m.index),
700
+ pattern: rule.pattern,
701
+ description: rule.description || "",
702
+ reference: ref,
703
+ remediationZh: "",
704
+ remediationEn: ""
705
+ });
706
+ }
707
+ }
708
+ }
709
+ return findings;
710
+ }
711
+
712
+ // src/scanner/dimensions/code-security.ts
713
+ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([".py", ".js", ".ts", ".jsx", ".tsx", ".rb", ".go", ".rs", ".java", ".sh"]);
714
+ var ALL_PATTERNS = [
715
+ ["subprocess\\.(run|call|Popen)\\s*\\([^)]*shell\\s*=\\s*True", "HIGH", "subprocess with shell=True - potential shell injection"],
716
+ ["os\\.system\\s*\\(", "HIGH", "os.system - shell command execution"],
717
+ ["os\\.popen\\s*\\(", "HIGH", "os.popen - shell command execution"],
718
+ ["\\beval\\s*\\(", "HIGH", "eval() - arbitrary code execution"],
719
+ ["\\bexec\\s*\\(", "HIGH", "exec() - arbitrary code execution"],
720
+ [`(?<!re\\.)\\bcompile\\s*\\([^)]*["']`, "MEDIUM", "compile() - dynamic code compilation"],
721
+ ["pickle\\.loads?\\s*\\(", "HIGH", "pickle.load - unsafe deserialization"],
722
+ ["yaml\\.load\\s*\\((?!.*Loader\\s*=\\s*yaml\\.SafeLoader)", "HIGH", "yaml.load without SafeLoader - unsafe deserialization"],
723
+ ["marshal\\.loads?\\s*\\(", "HIGH", "marshal.load - unsafe deserialization"],
724
+ [`f["']SELECT\\b.*\\{`, "HIGH", "SQL injection: f-string in SELECT query"],
725
+ [`f["']INSERT\\b.*\\{`, "HIGH", "SQL injection: f-string in INSERT query"],
726
+ [`f["']UPDATE\\b.*\\{`, "HIGH", "SQL injection: f-string in UPDATE query"],
727
+ [`f["']DELETE\\b.*\\{`, "HIGH", "SQL injection: f-string in DELETE query"],
728
+ ["\\.format\\s*\\(.*SELECT\\b", "HIGH", "SQL injection: .format() in query"],
729
+ ["%\\s*\\(.*SELECT\\b", "MEDIUM", "SQL injection: % formatting in query"],
730
+ ["innerHTML\\s*=", "MEDIUM", "innerHTML assignment - XSS risk"],
731
+ ["dangerouslySetInnerHTML", "MEDIUM", "React dangerouslySetInnerHTML - XSS risk"],
732
+ ["\\.\\./(\\.\\./)+(etc|root|home|var|usr)\\b", "MEDIUM", "Deep path traversal pattern"]
733
+ ];
734
+ var RESTRICTED_PATTERNS = /* @__PURE__ */ new Set([
735
+ "\\beval\\s*\\(",
736
+ "\\bexec\\s*\\(",
737
+ "os\\.system\\s*\\(",
738
+ "os\\.popen\\s*\\(",
739
+ "subprocess\\.(run|call|Popen)\\s*\\([^)]*shell\\s*=\\s*True"
740
+ ]);
741
+ function checkCodeSecurity(ctx, ref) {
742
+ const findings = [];
743
+ for (const [relPath, content] of ctx.fileContents) {
744
+ const ext = relPath.includes(".") ? "." + relPath.split(".").pop().toLowerCase() : "";
745
+ const isCode = CODE_EXTENSIONS.has(ext);
746
+ const patterns = isCode ? ALL_PATTERNS : ALL_PATTERNS.filter((p) => RESTRICTED_PATTERNS.has(p[0]));
747
+ for (const [pattern, severity, desc] of patterns) {
748
+ let re;
749
+ try {
750
+ re = new RegExp(pattern, "g");
751
+ } catch {
752
+ continue;
753
+ }
754
+ let m;
755
+ while ((m = re.exec(content)) !== null) {
756
+ findings.push({
757
+ dimension: "",
758
+ severity,
759
+ filePath: relPath,
760
+ lineNumber: getLineNumber(content, m.index),
761
+ pattern: m[0].length > 80 ? m[0].slice(0, 80) + "..." : m[0],
762
+ description: desc,
763
+ reference: ref,
764
+ remediationZh: "",
765
+ remediationEn: ""
766
+ });
767
+ }
768
+ }
769
+ }
770
+ return findings;
771
+ }
772
+
773
+ // src/scanner/dimensions/credential-leaks.ts
774
+ var KEY_PREFIXES = [
775
+ ["\\bsk-[a-zA-Z0-9]{20,}", "CRITICAL", "OpenAI/Anthropic API key (sk-...)"],
776
+ ["\\bghp_[a-zA-Z0-9]{36,}", "CRITICAL", "GitHub personal access token"],
777
+ ["\\bgho_[a-zA-Z0-9]{36,}", "CRITICAL", "GitHub OAuth token"],
778
+ ["\\bAKIA[A-Z0-9]{16}\\b", "CRITICAL", "AWS Access Key ID"],
779
+ ["\\bxoxb-[0-9-]+", "CRITICAL", "Slack bot token"],
780
+ ["\\bxoxp-[0-9-]+", "CRITICAL", "Slack user token"],
781
+ ["\\bglpat-[a-zA-Z0-9\\-_]{20,}", "CRITICAL", "GitLab personal access token"],
782
+ ["\\beyJ[a-zA-Z0-9_-]{50,}\\.eyJ", "MEDIUM", "JWT token detected"]
783
+ ];
784
+ var PEM_PATTERN = "-----BEGIN\\s+(?:RSA\\s+)?PRIVATE\\s+KEY-----";
785
+ var HARDCODED_PATTERNS = [
786
+ [`(?:password|passwd|pwd)\\s*[=:]\\s*["'][^"']{8,}["']`, "HIGH", "Hardcoded password"],
787
+ [`(?:secret|api_?key|token|auth)\\s*[=:]\\s*["'][^"']{8,}["']`, "HIGH", "Hardcoded secret/API key"]
788
+ ];
789
+ var SKIP_FILES = /* @__PURE__ */ new Set(["example", "template", "test", "mock", "sample", "demo", "fixture"]);
790
+ var PLACEHOLDERS = ["your_", "xxx", "placeholder", "changeme", "example", "replace", "<", "${", "todo", "fixme"];
791
+ function checkCredentialLeaks(ctx, ref) {
792
+ const findings = [];
793
+ for (const [relPath, content] of ctx.fileContents) {
794
+ const relLower = relPath.toLowerCase();
795
+ for (const [pattern, severity, desc] of KEY_PREFIXES) {
796
+ const re = new RegExp(pattern, "g");
797
+ let m;
798
+ while ((m = re.exec(content)) !== null) {
799
+ findings.push({
800
+ dimension: "",
801
+ severity,
802
+ filePath: relPath,
803
+ lineNumber: getLineNumber(content, m.index),
804
+ pattern,
805
+ description: desc,
806
+ reference: ref,
807
+ remediationZh: "",
808
+ remediationEn: ""
809
+ });
810
+ }
811
+ }
812
+ const pemRe = new RegExp(PEM_PATTERN, "g");
813
+ let pm;
814
+ while ((pm = pemRe.exec(content)) !== null) {
815
+ findings.push({
816
+ dimension: "",
817
+ severity: "CRITICAL",
818
+ filePath: relPath,
819
+ lineNumber: getLineNumber(content, pm.index),
820
+ pattern: PEM_PATTERN,
821
+ description: "Private key in PEM format",
822
+ reference: ref,
823
+ remediationZh: "",
824
+ remediationEn: ""
825
+ });
826
+ }
827
+ const isSkipFile = [...SKIP_FILES].some((s) => relLower.includes(s));
828
+ if (!isSkipFile) {
829
+ for (const [pattern, severity, desc] of HARDCODED_PATTERNS) {
830
+ const re = new RegExp(pattern, "gi");
831
+ let m;
832
+ while ((m = re.exec(content)) !== null) {
833
+ const matched = m[0].toLowerCase();
834
+ if (PLACEHOLDERS.some((ph) => matched.includes(ph))) continue;
835
+ findings.push({
836
+ dimension: "",
837
+ severity,
838
+ filePath: relPath,
839
+ lineNumber: getLineNumber(content, m.index),
840
+ pattern,
841
+ description: desc,
842
+ reference: ref,
843
+ remediationZh: "",
844
+ remediationEn: ""
845
+ });
846
+ }
847
+ }
848
+ const emailRe = /Entrez\.email\s*=\s*["'][^"']+@[^"']+["']/g;
849
+ let em;
850
+ while ((em = emailRe.exec(content)) !== null) {
851
+ findings.push({
852
+ dimension: "",
853
+ severity: "MEDIUM",
854
+ filePath: relPath,
855
+ lineNumber: getLineNumber(content, em.index),
856
+ pattern: "hardcoded email",
857
+ description: "Hardcoded email address in API configuration",
858
+ reference: ref,
859
+ remediationZh: "",
860
+ remediationEn: ""
861
+ });
862
+ }
863
+ }
864
+ }
865
+ return findings;
866
+ }
867
+
868
+ // src/scanner/dimensions/least-privilege.ts
869
+ function checkLeastPrivilege(ctx, ref) {
870
+ const findings = [];
871
+ const lpRules = {};
872
+ for (const r of ctx.rules["least_privilege"] || []) {
873
+ lpRules[r.id] = r;
874
+ }
875
+ const lp01 = lpRules["LP-01"] || {};
876
+ const lp02 = lpRules["LP-02"] || {};
877
+ const lp03 = lpRules["LP-03"] || {};
878
+ let allowedTools = ctx.frontmatter["allowed-tools"] || [];
879
+ if (typeof allowedTools === "string") allowedTools = [allowedTools];
880
+ if (!allowedTools.length) {
881
+ if (lp01.enabled !== false) {
882
+ findings.push({
883
+ dimension: "",
884
+ severity: lp01.severity || "LOW",
885
+ filePath: "SKILL.md",
886
+ lineNumber: 0,
887
+ pattern: "missing allowed-tools",
888
+ description: "No allowed-tools declared in frontmatter - implicit all-tools access",
889
+ reference: ref,
890
+ remediationZh: "",
891
+ remediationEn: ""
892
+ });
893
+ }
894
+ } else {
895
+ const toolSet = new Set(allowedTools.map((t) => t.toLowerCase()));
896
+ const hasShell = ["bash", "run_shell_command", "shell", "terminal"].some((t) => toolSet.has(t));
897
+ const hasNetwork = ["web_fetch", "fetch", "http", "network", "web_search"].some((t) => toolSet.has(t));
898
+ const hasWrite = ["write", "edit", "file_write", "write_file"].some((t) => toolSet.has(t));
899
+ if (hasShell && lp02.enabled !== false) {
900
+ findings.push({
901
+ dimension: "",
902
+ severity: lp02.severity || "MEDIUM",
903
+ filePath: "SKILL.md",
904
+ lineNumber: 0,
905
+ pattern: "allowed-tools: shell",
906
+ description: "Shell access declared in allowed-tools",
907
+ reference: ref,
908
+ remediationZh: "",
909
+ remediationEn: ""
910
+ });
911
+ }
912
+ if (hasShell && hasNetwork && hasWrite && lp03.enabled !== false) {
913
+ findings.push({
914
+ dimension: "",
915
+ severity: lp03.severity || "HIGH",
916
+ filePath: "SKILL.md",
917
+ lineNumber: 0,
918
+ pattern: "shell + network + write",
919
+ description: "Dangerous tool combination: shell + network + file write access",
920
+ reference: ref,
921
+ remediationZh: "",
922
+ remediationEn: ""
923
+ });
924
+ }
925
+ }
926
+ return findings;
927
+ }
928
+
929
+ // src/scanner/dimensions/license-compliance.ts
930
+ var OPEN_LICENSES = ["mit", "apache", "bsd", "isc", "unlicense", "cc0", "wtfpl", "mpl", "lgpl", "gpl"];
931
+ function checkLicenseCompliance(ctx, ref) {
932
+ const findings = [];
933
+ const fmLicense = typeof ctx.frontmatter["license"] === "string" ? ctx.frontmatter["license"].toLowerCase() : "";
934
+ if (fmLicense) {
935
+ if (OPEN_LICENSES.some((ol) => fmLicense.includes(ol))) {
936
+ findings.push({
937
+ dimension: "",
938
+ severity: "INFO",
939
+ filePath: "SKILL.md",
940
+ lineNumber: 0,
941
+ pattern: `license: ${fmLicense}`,
942
+ description: `Open source license: ${fmLicense}`,
943
+ reference: ref,
944
+ remediationZh: "",
945
+ remediationEn: ""
946
+ });
947
+ } else if (fmLicense.includes("proprietary")) {
948
+ findings.push({
949
+ dimension: "",
950
+ severity: "MEDIUM",
951
+ filePath: "SKILL.md",
952
+ lineNumber: 0,
953
+ pattern: `license: ${fmLicense}`,
954
+ description: "Proprietary license declared",
955
+ reference: ref,
956
+ remediationZh: "",
957
+ remediationEn: ""
958
+ });
959
+ }
960
+ }
961
+ findings.push(...applyRules("license_compliance", ctx.fileContents, ctx.rules, ref));
962
+ return findings;
963
+ }
964
+
965
+ // src/scanner/dimensions/resource-abuse.ts
966
+ function checkResourceAbuse(ctx, ref) {
967
+ const findings = [];
968
+ findings.push(...applyRules("resource_abuse", ctx.fileContents, ctx.rules, ref));
969
+ for (const [relPath, content] of ctx.fileContents) {
970
+ if (!relPath.endsWith(".py")) continue;
971
+ const defRe = /def\s+(\w+)\s*\(/g;
972
+ let dm;
973
+ while ((dm = defRe.exec(content)) !== null) {
974
+ const funcName = dm[1];
975
+ const funcStart = dm.index;
976
+ const rest = content.slice(funcStart + dm[0].length);
977
+ const nextDef = rest.search(/\ndef\s+\w+\s*\(/);
978
+ const funcBody = nextDef >= 0 ? rest.slice(0, nextDef) : rest;
979
+ const selfCallRe = new RegExp(`\\b${funcName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\(`);
980
+ if (selfCallRe.test(funcBody)) {
981
+ const hasBase = ["return", "if ", "raise", "break"].some((kw) => funcBody.includes(kw));
982
+ if (!hasBase) {
983
+ findings.push({
984
+ dimension: "",
985
+ severity: "HIGH",
986
+ filePath: relPath,
987
+ lineNumber: getLineNumber(content, funcStart),
988
+ pattern: `recursive: ${funcName}`,
989
+ description: `Recursive function '${funcName}' without obvious base case`,
990
+ reference: ref,
991
+ remediationZh: "",
992
+ remediationEn: ""
993
+ });
994
+ }
995
+ }
996
+ }
997
+ }
998
+ return findings;
999
+ }
1000
+
1001
+ // src/token/estimator.ts
1002
+ import { existsSync, readFileSync as readFileSync3 } from "fs";
1003
+ import { join as join3 } from "path";
1004
+ function countCjk(text) {
1005
+ let count = 0;
1006
+ for (const ch of text) {
1007
+ const code = ch.charCodeAt(0);
1008
+ if (code >= 19968 && code <= 40959 || code >= 13312 && code <= 19903 || code >= 63744 && code <= 64255) {
1009
+ count++;
1010
+ }
1011
+ }
1012
+ return count;
1013
+ }
1014
+ function estimateTokens(text) {
1015
+ if (!text) return 0;
1016
+ const cjk = countCjk(text);
1017
+ const nonCjk = text.length - cjk;
1018
+ return Math.round(cjk / 1.5 + nonCjk / 3.5);
1019
+ }
1020
+ function estimateTokensFromChars(chars) {
1021
+ return chars ? Math.round(chars / 3) : 0;
1022
+ }
1023
+ function extractReferences(skillMdContent, skillDir) {
1024
+ const eager = /* @__PURE__ */ new Set();
1025
+ const lazy = /* @__PURE__ */ new Set();
1026
+ const fileExists = (candidate) => existsSync(join3(skillDir, candidate));
1027
+ const mandatoryRe = /(?:MANDATORY|READ\s+ENTIRE\s+FILE|MUST\s+(?:READ|LOAD)|REQUIRED.*?READ).*?[[\`(]([a-zA-Z0-9_./-]+\.[a-zA-Z]{1,5})/gi;
1028
+ let m;
1029
+ while ((m = mandatoryRe.exec(skillMdContent)) !== null) {
1030
+ if (fileExists(m[1])) eager.add(m[1]);
1031
+ }
1032
+ const codeRe = /(?:source|python3?|bash|node|ruby|perl|sh)\s+(?:\$\{[^}]*\}\/)?([a-zA-Z0-9_./-]+\.[a-zA-Z]{1,5})/g;
1033
+ while ((m = codeRe.exec(skillMdContent)) !== null) {
1034
+ if (fileExists(m[1])) eager.add(m[1]);
1035
+ }
1036
+ const linkRe = /\[([^\]]*)\]\(([^)]+)\)/g;
1037
+ while ((m = linkRe.exec(skillMdContent)) !== null) {
1038
+ const target = m[2].trim();
1039
+ if (/^(https?:\/\/|#|mailto:)/.test(target)) continue;
1040
+ if (fileExists(target) && !eager.has(target)) lazy.add(target);
1041
+ }
1042
+ const btRe = /`([a-zA-Z0-9_./-]+\.[a-zA-Z]{1,5})`/g;
1043
+ while ((m = btRe.exec(skillMdContent)) !== null) {
1044
+ if (fileExists(m[1]) && !eager.has(m[1])) lazy.add(m[1]);
1045
+ }
1046
+ const readRe = /(?:[Rr]ead|[Ss]ee|[Rr]efer\s+to|[Cc]heck)\s+(?:\[?['"])?([a-zA-Z0-9_./-]+\.[a-zA-Z]{1,5})/g;
1047
+ while ((m = readRe.exec(skillMdContent)) !== null) {
1048
+ if (fileExists(m[1]) && !eager.has(m[1])) lazy.add(m[1]);
1049
+ }
1050
+ for (const e of eager) lazy.delete(e);
1051
+ return {
1052
+ eager: [...eager].sort(),
1053
+ lazy: [...lazy].sort()
1054
+ };
1055
+ }
1056
+ function estimateSkillTokens(skillDir, fileContents) {
1057
+ const est = {
1058
+ l1SkillMd: 0,
1059
+ l2Eager: 0,
1060
+ l2Lazy: 0,
1061
+ l3Total: 0,
1062
+ l1Chars: 0,
1063
+ l2EagerChars: 0,
1064
+ l2LazyChars: 0,
1065
+ l3Chars: 0,
1066
+ eagerFiles: [],
1067
+ lazyFiles: []
1068
+ };
1069
+ for (const name of ["SKILL.md", "skill.md"]) {
1070
+ const content = fileContents.get(name);
1071
+ if (content) {
1072
+ est.l1Chars = content.length;
1073
+ est.l1SkillMd = estimateTokens(content);
1074
+ const refs = extractReferences(content, skillDir);
1075
+ est.eagerFiles = refs.eager;
1076
+ est.lazyFiles = refs.lazy;
1077
+ break;
1078
+ }
1079
+ }
1080
+ const resolveChars = (paths) => {
1081
+ let total = 0;
1082
+ for (const refPath of paths) {
1083
+ const content = fileContents.get(refPath);
1084
+ if (content) {
1085
+ total += content.length;
1086
+ } else {
1087
+ try {
1088
+ const full = join3(skillDir, refPath);
1089
+ total += readFileSync3(full, "utf-8").length;
1090
+ } catch {
1091
+ }
1092
+ }
1093
+ }
1094
+ return total;
1095
+ };
1096
+ est.l2EagerChars = resolveChars(est.eagerFiles);
1097
+ est.l2Eager = estimateTokensFromChars(est.l2EagerChars);
1098
+ est.l2LazyChars = resolveChars(est.lazyFiles);
1099
+ est.l2Lazy = estimateTokensFromChars(est.l2LazyChars);
1100
+ for (const content of fileContents.values()) {
1101
+ est.l3Chars += content.length;
1102
+ }
1103
+ est.l3Total = estimateTokensFromChars(est.l3Chars);
1104
+ return est;
1105
+ }
1106
+
1107
+ // src/token/cost.ts
1108
+ function computeSessionCost(te, mp, nTurns) {
1109
+ const L1 = te.l1SkillMd;
1110
+ const L2e = te.l2Eager;
1111
+ const L2l = te.l2Lazy;
1112
+ const O = OUTPUT_PER_TURN;
1113
+ const rFull = mp.inputPerM / 1e6;
1114
+ const rCache = mp.cacheInputPerM / 1e6;
1115
+ const rOut = mp.outputPerM / 1e6;
1116
+ let total = 0;
1117
+ for (let k = 1; k <= nTurns; k++) {
1118
+ let inpNew;
1119
+ let inpCached;
1120
+ if (k === 1) {
1121
+ inpNew = L1;
1122
+ inpCached = 0;
1123
+ } else if (k === 2) {
1124
+ inpNew = O + L2e;
1125
+ inpCached = L1;
1126
+ } else if (k === 3) {
1127
+ inpNew = O + L2l;
1128
+ inpCached = L1 + O + L2e;
1129
+ } else {
1130
+ inpNew = O;
1131
+ inpCached = L1 + L2e + L2l + (k - 2) * O;
1132
+ }
1133
+ total += inpNew * rFull + inpCached * rCache + O * rOut;
1134
+ }
1135
+ return total;
1136
+ }
1137
+ function estimateCosts(te) {
1138
+ return MODEL_CATALOG.map((mp) => ({
1139
+ modelName: mp.name,
1140
+ inputPerM: mp.inputPerM,
1141
+ outputPerM: mp.outputPerM,
1142
+ cacheInputPerM: mp.cacheInputPerM,
1143
+ lightCost: computeSessionCost(te, mp, COST_SCENARIOS.light),
1144
+ typicalCost: computeSessionCost(te, mp, COST_SCENARIOS.typical),
1145
+ heavyCost: computeSessionCost(te, mp, COST_SCENARIOS.heavy)
1146
+ }));
1147
+ }
1148
+
1149
+ // src/scanner/auditor.ts
1150
+ var DIMENSIONS = [
1151
+ { zhName: "Prompt \u6CE8\u5165\u68C0\u6D4B", enName: "Prompt Injection", reference: "OWASP LLM01", checker: checkPromptInjection },
1152
+ { zhName: "\u6743\u9650\u63D0\u5347\u5206\u6790", enName: "Permission Escalation", reference: "OWASP LLM06", checker: checkPermissionEscalation },
1153
+ { zhName: "\u6570\u636E\u5916\u6CC4\u98CE\u9669", enName: "Data Exfiltration", reference: "OWASP LLM02 / MCP-Scan TPA", checker: checkDataExfiltration },
1154
+ { zhName: "\u7834\u574F\u6027\u64CD\u4F5C", enName: "Destructive Operations", reference: "Claude Code Built-in", checker: checkDestructiveOperations },
1155
+ { zhName: "\u4F9B\u5E94\u94FE/\u6765\u6E90\u9A8C\u8BC1", enName: "Supply Chain", reference: "SLSA / OpenSSF", checker: checkSupplyChain },
1156
+ { zhName: "\u4EE3\u7801\u5B89\u5168(\u9759\u6001\u5206\u6790)", enName: "Code Security", reference: "CWE / OWASP", checker: checkCodeSecurity },
1157
+ { zhName: "\u51ED\u8BC1\u4E0E\u5BC6\u94A5\u6CC4\u9732", enName: "Credential Leaks", reference: "OWASP LLM02", checker: checkCredentialLeaks },
1158
+ { zhName: "\u6743\u9650\u6700\u5C0F\u5316", enName: "Least Privilege", reference: "Google SAIF", checker: checkLeastPrivilege },
1159
+ { zhName: "\u8BB8\u53EF\u8BC1\u5408\u89C4", enName: "License Compliance", reference: "\u2014", checker: checkLicenseCompliance },
1160
+ { zhName: "\u8D44\u6E90\u6EE5\u7528/\u65E0\u9650\u6D88\u8017", enName: "Resource Abuse", reference: "OWASP LLM10", checker: checkResourceAbuse }
1161
+ ];
1162
+ var SkillAuditor = class {
1163
+ rules;
1164
+ constructor(rulesPath) {
1165
+ this.rules = loadRules(rulesPath);
1166
+ }
1167
+ auditSkill(skillDir) {
1168
+ const resolvedDir = resolve(skillDir);
1169
+ const report = {
1170
+ skillName: resolvedDir.split("/").pop() || "unknown",
1171
+ skillPath: resolvedDir,
1172
+ fileInventory: {},
1173
+ riskScore: 0,
1174
+ riskLevel: "A",
1175
+ findings: [],
1176
+ dimensionSummary: {},
1177
+ tokenEstimate: {
1178
+ l1SkillMd: 0,
1179
+ l2Eager: 0,
1180
+ l2Lazy: 0,
1181
+ l3Total: 0,
1182
+ l1Chars: 0,
1183
+ l2EagerChars: 0,
1184
+ l2LazyChars: 0,
1185
+ l3Chars: 0,
1186
+ eagerFiles: [],
1187
+ lazyFiles: []
1188
+ },
1189
+ costEstimates: []
1190
+ };
1191
+ const files = collectFiles(resolvedDir);
1192
+ report.fileInventory = fileInventory(files);
1193
+ const fileContents = /* @__PURE__ */ new Map();
1194
+ const fullTextParts = [];
1195
+ for (const relPath of files) {
1196
+ const content = readFileSafe(join4(resolvedDir, relPath));
1197
+ if (content !== null) {
1198
+ fileContents.set(relPath, content);
1199
+ fullTextParts.push(content);
1200
+ }
1201
+ }
1202
+ const fullText = fullTextParts.join("\n");
1203
+ let frontmatter = {};
1204
+ for (const name of ["SKILL.md", "skill.md"]) {
1205
+ const content = fileContents.get(name);
1206
+ if (content) {
1207
+ frontmatter = parseFrontmatter(content);
1208
+ break;
1209
+ }
1210
+ }
1211
+ const ctx = {
1212
+ skillDir: resolvedDir,
1213
+ fileContents,
1214
+ fullText,
1215
+ frontmatter,
1216
+ rules: this.rules
1217
+ };
1218
+ for (const dim of DIMENSIONS) {
1219
+ const dimName = `${dim.zhName} (${dim.enName})`;
1220
+ let findings;
1221
+ try {
1222
+ findings = dim.checker(ctx, dim.reference);
1223
+ } catch {
1224
+ findings = [];
1225
+ }
1226
+ for (const f of findings) {
1227
+ f.dimension = dimName;
1228
+ }
1229
+ report.findings.push(...findings);
1230
+ report.dimensionSummary[dimName] = findings.length;
1231
+ }
1232
+ const { score, level } = computeRisk(report.findings);
1233
+ report.riskScore = score;
1234
+ report.riskLevel = level;
1235
+ for (const f of report.findings) {
1236
+ if (!f.remediationZh) {
1237
+ const [zh, en] = matchRemediation(f.description);
1238
+ f.remediationZh = zh;
1239
+ f.remediationEn = en;
1240
+ }
1241
+ if (!f.remediationZh) {
1242
+ const [zh, en] = dimensionRemediation(f.dimension);
1243
+ f.remediationZh = zh;
1244
+ f.remediationEn = en;
1245
+ }
1246
+ }
1247
+ report.tokenEstimate = estimateSkillTokens(resolvedDir, fileContents);
1248
+ report.costEstimates = estimateCosts(report.tokenEstimate);
1249
+ return report;
1250
+ }
1251
+ auditMarketplace(marketDir) {
1252
+ const resolvedDir = resolve(marketDir);
1253
+ let baseDir = resolvedDir;
1254
+ const skillsDir = join4(resolvedDir, "skills");
1255
+ try {
1256
+ if (readdirSync2(skillsDir, { withFileTypes: true }).length > 0) {
1257
+ baseDir = skillsDir;
1258
+ }
1259
+ } catch {
1260
+ }
1261
+ const reports = [];
1262
+ const entries = readdirSync2(baseDir, { withFileTypes: true });
1263
+ for (const entry of entries) {
1264
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
1265
+ reports.push(this.auditSkill(join4(baseDir, entry.name)));
1266
+ }
1267
+ }
1268
+ return reports.sort((a, b) => a.skillName.localeCompare(b.skillName));
1269
+ }
1270
+ };
1271
+
1272
+ // src/fetcher/url-parser.ts
1273
+ function parseGitHubUrl(url) {
1274
+ const parsed = new URL(url.trim());
1275
+ const parts = parsed.pathname.replace(/^\/+|\/+$/g, "").split("/");
1276
+ if (parts.length < 2) {
1277
+ throw new Error(`Invalid GitHub URL: ${url}`);
1278
+ }
1279
+ const owner = parts[0];
1280
+ const repo = parts[1].replace(/\.git$/, "");
1281
+ let subpath;
1282
+ if (parts.length > 4 && parts[2] === "tree") {
1283
+ subpath = parts.slice(4).join("/");
1284
+ }
1285
+ return { owner, repo, subpath };
1286
+ }
1287
+ function parseClawHubUrl(url) {
1288
+ const parsed = new URL(url.trim());
1289
+ const parts = parsed.pathname.replace(/^\/+|\/+$/g, "").split("/");
1290
+ if (parts.length < 2) {
1291
+ throw new Error(`Invalid ClawHub URL: ${url}`);
1292
+ }
1293
+ return parts[1];
1294
+ }
1295
+ function isGitHubUrl(target) {
1296
+ try {
1297
+ const parsed = new URL(target.trim());
1298
+ return parsed.hostname === "github.com" || parsed.hostname === "www.github.com";
1299
+ } catch {
1300
+ return false;
1301
+ }
1302
+ }
1303
+ function isClawHubUrl(target) {
1304
+ try {
1305
+ const parsed = new URL(target.trim());
1306
+ return parsed.hostname === "clawhub.ai" || parsed.hostname === "www.clawhub.ai";
1307
+ } catch {
1308
+ return false;
1309
+ }
1310
+ }
1311
+ function isUrl(target) {
1312
+ const trimmed = target.trim();
1313
+ return trimmed.startsWith("http://") || trimmed.startsWith("https://");
1314
+ }
1315
+
1316
+ // src/fetcher/github.ts
1317
+ import { mkdtempSync, rmSync, readdirSync as readdirSync3, renameSync, statSync as statSync2 } from "fs";
1318
+ import { join as join5 } from "path";
1319
+ import { tmpdir } from "os";
1320
+ import { execSync } from "child_process";
1321
+ import { Readable } from "stream";
1322
+ import { pipeline } from "stream/promises";
1323
+ import * as tar from "tar";
1324
+ async function downloadTarball(owner, repo, tempDir) {
1325
+ for (const branch of ["main", "master"]) {
1326
+ const url = `https://github.com/${owner}/${repo}/archive/refs/heads/${branch}.tar.gz`;
1327
+ let resp;
1328
+ try {
1329
+ resp = await fetch(url, {
1330
+ headers: { "User-Agent": "SkillGuard/1.0" },
1331
+ redirect: "follow"
1332
+ });
1333
+ } catch {
1334
+ continue;
1335
+ }
1336
+ if (!resp.ok || !resp.body) {
1337
+ continue;
1338
+ }
1339
+ try {
1340
+ const nodeStream = Readable.fromWeb(resp.body);
1341
+ await pipeline(
1342
+ nodeStream,
1343
+ tar.extract({ cwd: tempDir, strip: 0 })
1344
+ );
1345
+ } catch {
1346
+ continue;
1347
+ }
1348
+ const entries = readdirSync3(tempDir, { withFileTypes: true }).filter((e) => e.isDirectory());
1349
+ if (entries.length === 1) {
1350
+ const extractedDir = join5(tempDir, entries[0].name);
1351
+ const targetDir = join5(tempDir, repo);
1352
+ if (extractedDir !== targetDir) {
1353
+ renameSync(extractedDir, targetDir);
1354
+ }
1355
+ return targetDir;
1356
+ }
1357
+ return null;
1358
+ }
1359
+ return null;
1360
+ }
1361
+ function gitClone(owner, repo, tempDir) {
1362
+ const targetDir = join5(tempDir, repo);
1363
+ const cloneUrl = `https://github.com/${owner}/${repo}.git`;
1364
+ execSync(
1365
+ `git clone --depth 1 --single-branch --no-tags ${cloneUrl} ${targetDir}`,
1366
+ {
1367
+ env: {
1368
+ ...process.env,
1369
+ GIT_CONFIG_NOSYSTEM: "1",
1370
+ GIT_TERMINAL_PROMPT: "0"
1371
+ },
1372
+ stdio: "pipe",
1373
+ timeout: 12e4
1374
+ }
1375
+ );
1376
+ return targetDir;
1377
+ }
1378
+ async function fetchGitHub(url) {
1379
+ const { owner, repo, subpath } = parseGitHubUrl(url);
1380
+ const tempDir = mkdtempSync(join5(tmpdir(), "sg_clone_"));
1381
+ const cleanup = () => {
1382
+ rmSync(tempDir, { recursive: true, force: true });
1383
+ };
1384
+ let repoDir;
1385
+ repoDir = await downloadTarball(owner, repo, tempDir);
1386
+ if (repoDir === null) {
1387
+ try {
1388
+ repoDir = gitClone(owner, repo, tempDir);
1389
+ } catch (err) {
1390
+ cleanup();
1391
+ throw new Error(
1392
+ `Failed to clone https://github.com/${owner}/${repo}: ${err instanceof Error ? err.message : String(err)}`
1393
+ );
1394
+ }
1395
+ }
1396
+ let skillDir = repoDir;
1397
+ if (subpath) {
1398
+ skillDir = join5(repoDir, subpath);
1399
+ try {
1400
+ const stat = statSync2(skillDir);
1401
+ if (!stat.isDirectory()) {
1402
+ cleanup();
1403
+ throw new Error(`Subpath is not a directory: ${subpath}`);
1404
+ }
1405
+ } catch (err) {
1406
+ if (err.code === "ENOENT") {
1407
+ cleanup();
1408
+ throw new Error(`Subpath not found in repo: ${subpath}`);
1409
+ }
1410
+ throw err;
1411
+ }
1412
+ }
1413
+ return { skillDir, cleanup };
1414
+ }
1415
+
1416
+ // src/fetcher/clawhub.ts
1417
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, readdirSync as readdirSync4, createWriteStream } from "fs";
1418
+ import { join as join6 } from "path";
1419
+ import { tmpdir as tmpdir2 } from "os";
1420
+ import { pipeline as pipeline2 } from "stream/promises";
1421
+ import { Readable as Readable2 } from "stream";
1422
+ import unzipper from "unzipper";
1423
+
1424
+ // src/config.ts
1425
+ function envInt(key, fallback) {
1426
+ const v = process.env[key];
1427
+ if (v === void 0) return fallback;
1428
+ const n = parseInt(v, 10);
1429
+ return Number.isNaN(n) ? fallback : n;
1430
+ }
1431
+ function envString(key, fallback) {
1432
+ return process.env[key] ?? fallback;
1433
+ }
1434
+ function envSet(key, fallback) {
1435
+ const v = process.env[key];
1436
+ if (v === void 0) return fallback;
1437
+ return new Set(v.split(",").map((s) => s.trim()).filter(Boolean));
1438
+ }
1439
+ var CLONE_TIMEOUT = envInt("SKILLGUARD_CLONE_TIMEOUT", 120);
1440
+ var MAX_REPO_SIZE_MB = envInt("SKILLGUARD_MAX_REPO_SIZE_MB", 500);
1441
+ var AUDIT_TIMEOUT = envInt("SKILLGUARD_AUDIT_TIMEOUT", 60);
1442
+ var ALLOWED_DOMAINS = envSet(
1443
+ "SKILLGUARD_ALLOWED_DOMAINS",
1444
+ /* @__PURE__ */ new Set(["github.com", "clawhub.ai"])
1445
+ );
1446
+ var CLAWHUB_API_BASE = envString(
1447
+ "SKILLGUARD_CLAWHUB_API_BASE",
1448
+ "https://wry-manatee-359.convex.site/api/v1"
1449
+ );
1450
+
1451
+ // src/fetcher/clawhub.ts
1452
+ async function fetchClawHub(url) {
1453
+ const slug = parseClawHubUrl(url);
1454
+ const tempDir = mkdtempSync2(join6(tmpdir2(), "sg_clawhub_"));
1455
+ const cleanup = () => {
1456
+ rmSync2(tempDir, { recursive: true, force: true });
1457
+ };
1458
+ const downloadUrl = `${CLAWHUB_API_BASE}/download?slug=${encodeURIComponent(slug)}`;
1459
+ let resp;
1460
+ try {
1461
+ resp = await fetch(downloadUrl, {
1462
+ headers: { "User-Agent": "SkillGuard/1.0" },
1463
+ redirect: "follow"
1464
+ });
1465
+ } catch (err) {
1466
+ cleanup();
1467
+ throw new Error(
1468
+ `Failed to download ClawHub skill "${slug}": ${err instanceof Error ? err.message : String(err)}`
1469
+ );
1470
+ }
1471
+ if (!resp.ok || !resp.body) {
1472
+ cleanup();
1473
+ throw new Error(
1474
+ `ClawHub download failed for "${slug}": HTTP ${resp.status}`
1475
+ );
1476
+ }
1477
+ try {
1478
+ const nodeStream = Readable2.fromWeb(resp.body);
1479
+ const zip = nodeStream.pipe(unzipper.Parse());
1480
+ const extractPromises = [];
1481
+ for await (const entry of zip) {
1482
+ const filePath = entry.path;
1483
+ const type = entry.type;
1484
+ if (filePath.startsWith("/") || filePath.includes("..")) {
1485
+ entry.autodrain();
1486
+ continue;
1487
+ }
1488
+ const outputPath = join6(tempDir, filePath);
1489
+ if (!outputPath.startsWith(tempDir)) {
1490
+ entry.autodrain();
1491
+ continue;
1492
+ }
1493
+ if (type === "Directory") {
1494
+ const { mkdirSync } = await import("fs");
1495
+ mkdirSync(outputPath, { recursive: true });
1496
+ entry.autodrain();
1497
+ } else {
1498
+ const { mkdirSync } = await import("fs");
1499
+ const { dirname: dirname2 } = await import("path");
1500
+ mkdirSync(dirname2(outputPath), { recursive: true });
1501
+ const writePromise = pipeline2(
1502
+ entry,
1503
+ createWriteStream(outputPath)
1504
+ );
1505
+ extractPromises.push(writePromise);
1506
+ }
1507
+ }
1508
+ await Promise.all(extractPromises);
1509
+ } catch (err) {
1510
+ cleanup();
1511
+ throw new Error(
1512
+ `Failed to extract ClawHub skill "${slug}": ${err instanceof Error ? err.message : String(err)}`
1513
+ );
1514
+ }
1515
+ const children = readdirSync4(tempDir, { withFileTypes: true }).filter((e) => e.isDirectory());
1516
+ const skillDir = children.length === 1 ? join6(tempDir, children[0].name) : tempDir;
1517
+ return { skillDir, cleanup };
1518
+ }
1519
+
1520
+ // src/fetcher/local.ts
1521
+ import { statSync as statSync3 } from "fs";
1522
+ import { resolve as resolve2 } from "path";
1523
+ function resolveLocal(target) {
1524
+ const skillDir = resolve2(target);
1525
+ let stat;
1526
+ try {
1527
+ stat = statSync3(skillDir);
1528
+ } catch (err) {
1529
+ throw new Error(
1530
+ `Local path does not exist: ${skillDir}`
1531
+ );
1532
+ }
1533
+ if (!stat.isDirectory()) {
1534
+ throw new Error(
1535
+ `Local path is not a directory: ${skillDir}`
1536
+ );
1537
+ }
1538
+ return {
1539
+ skillDir,
1540
+ cleanup: () => {
1541
+ }
1542
+ };
1543
+ }
1544
+
1545
+ // src/fetcher/index.ts
1546
+ async function fetchSkill(target) {
1547
+ if (isGitHubUrl(target)) {
1548
+ return fetchGitHub(target);
1549
+ }
1550
+ if (isClawHubUrl(target)) {
1551
+ return fetchClawHub(target);
1552
+ }
1553
+ if (isUrl(target)) {
1554
+ throw new Error(
1555
+ `Unsupported URL: ${target}. Only github.com and clawhub.ai URLs are supported.`
1556
+ );
1557
+ }
1558
+ return resolveLocal(target);
1559
+ }
1560
+
1561
+ // src/renderer/terminal.ts
1562
+ import chalk from "chalk";
1563
+ var SEV_COLOR = {
1564
+ CRITICAL: chalk.bgRed.white.bold,
1565
+ HIGH: chalk.red.bold,
1566
+ MEDIUM: chalk.yellow,
1567
+ LOW: chalk.blue,
1568
+ INFO: chalk.gray
1569
+ };
1570
+ var LEVEL_COLOR = {
1571
+ A: chalk.green.bold,
1572
+ B: chalk.green,
1573
+ C: chalk.yellow.bold,
1574
+ D: chalk.red,
1575
+ F: chalk.bgRed.white.bold
1576
+ };
1577
+ var LEVEL_LABELS = {
1578
+ A: "Safe",
1579
+ B: "Acceptable",
1580
+ C: "Warning",
1581
+ D: "Unsafe",
1582
+ F: "Dangerous"
1583
+ };
1584
+ function renderReport(report, options) {
1585
+ const minSev = options?.minSeverity || "INFO";
1586
+ const minOrder = SEVERITY_ORDER[minSev] ?? 4;
1587
+ const lc = LEVEL_COLOR[report.riskLevel] || ((s) => s);
1588
+ const label = LEVEL_LABELS[report.riskLevel] || "";
1589
+ console.log(`
1590
+ ${"\u2500".repeat(70)}`);
1591
+ console.log(
1592
+ `${chalk.bold(report.skillName)} ${lc(`[${report.riskLevel}] ${label}`)} Score: ${report.riskScore}/100`
1593
+ );
1594
+ console.log(chalk.dim(report.skillPath));
1595
+ const inv = report.fileInventory;
1596
+ if (Object.keys(inv).length) {
1597
+ const parts = Object.entries(inv).sort().map(([ext, n]) => `${ext}: ${n}`);
1598
+ console.log(chalk.dim(`Files: ${parts.join(", ")}`));
1599
+ }
1600
+ const te = report.tokenEstimate;
1601
+ if (te.l1SkillMd > 0) {
1602
+ const parts = [`L1 SKILL.md: ${chalk.cyan(formatTokens(te.l1SkillMd))}`];
1603
+ if (te.l2Eager > 0) parts.push(`L2 eager: ${chalk.cyan(formatTokens(te.l2Eager))}`);
1604
+ if (te.l2Lazy > 0) parts.push(`L2 lazy: ${chalk.cyan(formatTokens(te.l2Lazy))}`);
1605
+ parts.push(`L3 total: ${chalk.cyan(formatTokens(te.l3Total))}`);
1606
+ console.log(`${chalk.dim("Tokens:")} ${parts.join(" | ")}`);
1607
+ if (te.eagerFiles.length) console.log(chalk.dim(` eager: ${te.eagerFiles.join(", ")}`));
1608
+ if (te.lazyFiles.length) console.log(chalk.dim(` lazy: ${te.lazyFiles.join(", ")}`));
1609
+ }
1610
+ if (report.costEstimates.length) {
1611
+ const costs = report.costEstimates;
1612
+ const header = costs.map((c) => chalk.dim(c.modelName.slice(0, 12).padStart(12))).join(" ");
1613
+ const light = costs.map((c) => chalk.cyan(formatCost(c.lightCost).padStart(12))).join(" ");
1614
+ const typical = costs.map((c) => chalk.cyan(formatCost(c.typicalCost).padStart(12))).join(" ");
1615
+ const heavy = costs.map((c) => chalk.cyan(formatCost(c.heavyCost).padStart(12))).join(" ");
1616
+ console.log(`Cost/sess: ${header}`);
1617
+ console.log(` Light ${light} ${chalk.dim(`(${COST_SCENARIOS.light}T)`)}`);
1618
+ console.log(` Typical ${typical} ${chalk.dim(`(${COST_SCENARIOS.typical}T)`)}`);
1619
+ console.log(` Heavy ${heavy} ${chalk.dim(`(${COST_SCENARIOS.heavy}T)`)}`);
1620
+ }
1621
+ const filtered = report.findings.filter((f) => (SEVERITY_ORDER[f.severity] ?? 4) <= minOrder).sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 4) - (SEVERITY_ORDER[b.severity] ?? 4));
1622
+ if (!filtered.length) {
1623
+ console.log(` ${chalk.green(`No findings at or above ${minSev} level.`)}`);
1624
+ return;
1625
+ }
1626
+ for (const f of filtered) {
1627
+ const sc = SEV_COLOR[f.severity] || ((s) => s);
1628
+ const loc = f.lineNumber > 0 ? `${f.filePath}:${f.lineNumber}` : f.filePath;
1629
+ console.log(` ${sc(f.severity.padEnd(8))} ${chalk.dim(loc)}`);
1630
+ console.log(` ${f.description}`);
1631
+ if (f.reference && f.reference !== "\u2014") {
1632
+ console.log(` ${chalk.dim(`[${f.reference}]`)}`);
1633
+ }
1634
+ const rem = options?.lang === "zh" ? f.remediationZh : f.remediationEn;
1635
+ if (rem) {
1636
+ console.log(` ${chalk.green(`\u2192 ${rem}`)}`);
1637
+ }
1638
+ }
1639
+ }
1640
+ function renderSummary(reports, options) {
1641
+ const minLevel = options?.minLevel || "A";
1642
+ const minOrder = LEVEL_ORDER[minLevel] ?? 4;
1643
+ console.log(`
1644
+ ${"\u2550".repeat(70)}`);
1645
+ console.log(chalk.bold("AUDIT SUMMARY"));
1646
+ console.log("\u2550".repeat(70));
1647
+ const byLevel = { A: 0, B: 0, C: 0, D: 0, F: 0 };
1648
+ for (const r of reports) byLevel[r.riskLevel] = (byLevel[r.riskLevel] || 0) + 1;
1649
+ console.log(`Total skills scanned: ${chalk.bold(String(reports.length))}`);
1650
+ for (const level of ["A", "B", "C", "D", "F"]) {
1651
+ const lc = LEVEL_COLOR[level];
1652
+ const label = LEVEL_LABELS[level];
1653
+ console.log(` ${lc(`${level} (${label}): ${byLevel[level]}`)}`);
1654
+ }
1655
+ const filtered = reports.filter((r) => (LEVEL_ORDER[r.riskLevel] ?? 4) <= minOrder).sort((a, b) => b.riskScore - a.riskScore || a.skillName.localeCompare(b.skillName));
1656
+ if (filtered.length) {
1657
+ console.log(`
1658
+ ${chalk.bold(`Skills at level ${minLevel} or worse:`)}`);
1659
+ for (const r of filtered.slice(0, 50)) {
1660
+ const lc = LEVEL_COLOR[r.riskLevel];
1661
+ const sevCounts = {};
1662
+ for (const f of r.findings) sevCounts[f.severity] = (sevCounts[f.severity] || 0) + 1;
1663
+ const sevStr = Object.entries(sevCounts).sort(([a], [b]) => (SEVERITY_ORDER[a] ?? 4) - (SEVERITY_ORDER[b] ?? 4)).map(([k, v]) => `${k}:${v}`).join(", ");
1664
+ console.log(` ${lc(`[${r.riskLevel}]`)} ${String(r.riskScore).padStart(3)}/100 ${r.skillName} ${chalk.dim(`(${sevStr})`)}`);
1665
+ }
1666
+ if (filtered.length > 50) {
1667
+ console.log(chalk.dim(` ... and ${filtered.length - 50} more (use --json for full list)`));
1668
+ }
1669
+ }
1670
+ }
1671
+
1672
+ // src/renderer/json.ts
1673
+ import { writeFileSync } from "fs";
1674
+ var VERSION = "0.1.0";
1675
+ function renderJson(reports, target, options) {
1676
+ const byLevel = { A: 0, B: 0, C: 0, D: 0, F: 0 };
1677
+ for (const r of reports) byLevel[r.riskLevel] = (byLevel[r.riskLevel] || 0) + 1;
1678
+ const dimRem = {};
1679
+ for (const [key, zh, en] of DIMENSION_REMEDIATIONS) {
1680
+ dimRem[key] = { zh, en };
1681
+ }
1682
+ const data = {
1683
+ audit_metadata: {
1684
+ tool_version: VERSION,
1685
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1686
+ target
1687
+ },
1688
+ summary: {
1689
+ total_skills: reports.length,
1690
+ by_level: byLevel
1691
+ },
1692
+ dimension_remediations: dimRem,
1693
+ reports: reports.map((r) => ({
1694
+ skill_name: r.skillName,
1695
+ skill_path: r.skillPath,
1696
+ risk_score: r.riskScore,
1697
+ risk_level: r.riskLevel,
1698
+ file_inventory: r.fileInventory,
1699
+ token_estimate: {
1700
+ l1_skill_md: r.tokenEstimate.l1SkillMd,
1701
+ l2_eager: r.tokenEstimate.l2Eager,
1702
+ l2_lazy: r.tokenEstimate.l2Lazy,
1703
+ l3_total: r.tokenEstimate.l3Total,
1704
+ l1_chars: r.tokenEstimate.l1Chars,
1705
+ l2_eager_chars: r.tokenEstimate.l2EagerChars,
1706
+ l2_lazy_chars: r.tokenEstimate.l2LazyChars,
1707
+ l3_chars: r.tokenEstimate.l3Chars,
1708
+ eager_files: r.tokenEstimate.eagerFiles,
1709
+ lazy_files: r.tokenEstimate.lazyFiles
1710
+ },
1711
+ cost_estimates: r.costEstimates.map((c) => ({
1712
+ model: c.modelName,
1713
+ input_per_m: c.inputPerM,
1714
+ output_per_m: c.outputPerM,
1715
+ cache_input_per_m: c.cacheInputPerM,
1716
+ light_cost: Math.round(c.lightCost * 1e6) / 1e6,
1717
+ typical_cost: Math.round(c.typicalCost * 1e6) / 1e6,
1718
+ heavy_cost: Math.round(c.heavyCost * 1e6) / 1e6
1719
+ })),
1720
+ findings: r.findings.map((f) => ({
1721
+ dimension: f.dimension,
1722
+ severity: f.severity,
1723
+ file_path: f.filePath,
1724
+ line_number: f.lineNumber,
1725
+ pattern: f.pattern,
1726
+ description: f.description,
1727
+ reference: f.reference,
1728
+ remediation_zh: f.remediationZh,
1729
+ remediation_en: f.remediationEn
1730
+ }))
1731
+ }))
1732
+ };
1733
+ const json = JSON.stringify(data, null, 2);
1734
+ if (options?.outputPath) {
1735
+ writeFileSync(options.outputPath, json, "utf-8");
1736
+ } else {
1737
+ process.stdout.write(json + "\n");
1738
+ }
1739
+ }
1740
+
1741
+ // src/renderer/markdown.ts
1742
+ import { writeFileSync as writeFileSync2 } from "fs";
1743
+ var SEV_BADGE = {
1744
+ CRITICAL: "\u{1F534} CRITICAL",
1745
+ HIGH: "\u{1F7E0} HIGH",
1746
+ MEDIUM: "\u{1F7E1} MEDIUM",
1747
+ LOW: "\u{1F535} LOW",
1748
+ INFO: "\u26AA INFO"
1749
+ };
1750
+ function renderMarkdown(reports, target, outputPath) {
1751
+ const lines = [];
1752
+ lines.push("# SkillGuard Security Report");
1753
+ lines.push("");
1754
+ lines.push(`> Target: \`${target}\` `);
1755
+ lines.push(`> Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]} `);
1756
+ lines.push(`> Skills scanned: ${reports.length}`);
1757
+ lines.push("");
1758
+ if (reports.length > 1) {
1759
+ lines.push("## Summary");
1760
+ lines.push("");
1761
+ lines.push("| Skill | Grade | Score | Findings |");
1762
+ lines.push("|-------|-------|-------|----------|");
1763
+ for (const r of reports.sort((a, b) => b.riskScore - a.riskScore)) {
1764
+ lines.push(`| ${r.skillName} | **${r.riskLevel}** | ${r.riskScore}/100 | ${r.findings.length} |`);
1765
+ }
1766
+ lines.push("");
1767
+ }
1768
+ for (const report of reports) {
1769
+ if (reports.length > 1) {
1770
+ lines.push(`---`);
1771
+ lines.push("");
1772
+ }
1773
+ lines.push(`## ${report.skillName}`);
1774
+ lines.push("");
1775
+ lines.push(`**Grade:** ${report.riskLevel} | **Score:** ${report.riskScore}/100 | **Path:** \`${report.skillPath}\``);
1776
+ lines.push("");
1777
+ const inv = report.fileInventory;
1778
+ if (Object.keys(inv).length) {
1779
+ lines.push("### File Inventory");
1780
+ lines.push("");
1781
+ lines.push("| Extension | Count |");
1782
+ lines.push("|-----------|-------|");
1783
+ for (const [ext, n] of Object.entries(inv).sort()) {
1784
+ lines.push(`| ${ext} | ${n} |`);
1785
+ }
1786
+ lines.push("");
1787
+ }
1788
+ const te = report.tokenEstimate;
1789
+ if (te.l1SkillMd > 0) {
1790
+ lines.push("### Token Estimation");
1791
+ lines.push("");
1792
+ lines.push("| Level | Tokens | Description |");
1793
+ lines.push("|-------|--------|-------------|");
1794
+ lines.push(`| L1 | ${formatTokens(te.l1SkillMd)} | SKILL.md direct injection |`);
1795
+ if (te.l2Eager > 0) lines.push(`| L2 Eager | ${formatTokens(te.l2Eager)} | Mandatory files: ${te.eagerFiles.join(", ") || "\u2014"} |`);
1796
+ if (te.l2Lazy > 0) lines.push(`| L2 Lazy | ${formatTokens(te.l2Lazy)} | On-demand files: ${te.lazyFiles.join(", ") || "\u2014"} |`);
1797
+ lines.push(`| L3 Total | ${formatTokens(te.l3Total)} | All files maximum |`);
1798
+ lines.push("");
1799
+ }
1800
+ if (report.costEstimates.length) {
1801
+ lines.push("### Cost Estimation");
1802
+ lines.push("");
1803
+ lines.push("| Model | Light (3T) | Typical (6T) | Heavy (15T) |");
1804
+ lines.push("|-------|-----------|-------------|------------|");
1805
+ for (const c of report.costEstimates) {
1806
+ lines.push(`| ${c.modelName} | ${formatCost(c.lightCost)} | ${formatCost(c.typicalCost)} | ${formatCost(c.heavyCost)} |`);
1807
+ }
1808
+ lines.push("");
1809
+ }
1810
+ const sorted = [...report.findings].sort(
1811
+ (a, b) => (SEVERITY_ORDER[a.severity] ?? 4) - (SEVERITY_ORDER[b.severity] ?? 4)
1812
+ );
1813
+ if (sorted.length) {
1814
+ lines.push("### Findings");
1815
+ lines.push("");
1816
+ lines.push("| Severity | Dimension | Description | Location |");
1817
+ lines.push("|----------|-----------|-------------|----------|");
1818
+ for (const f of sorted) {
1819
+ const loc = f.lineNumber > 0 ? `${f.filePath}:${f.lineNumber}` : f.filePath;
1820
+ const desc = f.description.replace(/\|/g, "\\|");
1821
+ const dim = f.dimension.replace(/\|/g, "\\|");
1822
+ lines.push(`| ${SEV_BADGE[f.severity]} | ${dim} | ${desc} | \`${loc}\` |`);
1823
+ }
1824
+ lines.push("");
1825
+ } else {
1826
+ lines.push("### Findings");
1827
+ lines.push("");
1828
+ lines.push("No security findings detected. \u2705");
1829
+ lines.push("");
1830
+ }
1831
+ }
1832
+ writeFileSync2(outputPath, lines.join("\n"), "utf-8");
1833
+ }
1834
+
1835
+ // src/cli.ts
1836
+ var VERSION2 = "0.1.0";
1837
+ function createProgram() {
1838
+ program.name("skillguard").description("Security audit CLI for AI agent skills \u2014 scans 10 dimensions with 109 rules").version(VERSION2).argument("<target>", "Local directory path, GitHub URL, or ClawHub URL").option("--all", "Scan all skill subdirectories (marketplace mode)").option("--json [file]", "Output as JSON (to stdout or file)").option("--md <file>", "Output as Markdown report to file").option("--rules <path>", "Custom rules.yaml file path").option("--min-level <level>", "Filter output by minimum risk level (A-F)", "A").option("--min-severity <severity>", "Filter by minimum severity", "INFO").option("--lang <lang>", "Output language (en or zh)", "en").action(async (target, options) => {
1839
+ try {
1840
+ await runAudit(target, options);
1841
+ } catch (err) {
1842
+ console.error(`Error: ${err.message}`);
1843
+ process.exit(1);
1844
+ }
1845
+ });
1846
+ return program;
1847
+ }
1848
+ async function runAudit(target, options) {
1849
+ const { skillDir, cleanup } = await fetchSkill(target);
1850
+ try {
1851
+ const auditor = new SkillAuditor(options.rules);
1852
+ const lang = options.lang || "en";
1853
+ const minSeverity = options.minSeverity || "INFO";
1854
+ const minLevel = options.minLevel || "A";
1855
+ if (options.all) {
1856
+ const reports = auditor.auditMarketplace(skillDir);
1857
+ if (options.json !== void 0) {
1858
+ const outFile = typeof options.json === "string" ? options.json : void 0;
1859
+ renderJson(reports, target, { outputPath: outFile });
1860
+ } else if (options.md) {
1861
+ renderMarkdown(reports, target, options.md);
1862
+ console.log(`Markdown report written to ${options.md}`);
1863
+ } else {
1864
+ for (const report of reports) {
1865
+ renderReport(report, { minSeverity, lang });
1866
+ }
1867
+ renderSummary(reports, { minLevel });
1868
+ }
1869
+ } else {
1870
+ const report = auditor.auditSkill(skillDir);
1871
+ if (options.json !== void 0) {
1872
+ const outFile = typeof options.json === "string" ? options.json : void 0;
1873
+ renderJson([report], target, { outputPath: outFile });
1874
+ } else if (options.md) {
1875
+ renderMarkdown([report], target, options.md);
1876
+ console.log(`Markdown report written to ${options.md}`);
1877
+ } else {
1878
+ renderReport(report, { minSeverity, lang });
1879
+ }
1880
+ }
1881
+ } finally {
1882
+ cleanup();
1883
+ }
1884
+ }
1885
+
1886
+ // src/index.ts
1887
+ createProgram().parse();