skill-checker 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,2168 @@
1
+ // src/parser.ts
2
+ import { readFileSync, readdirSync, statSync, existsSync } from "fs";
3
+ import { join, extname, basename, resolve } from "path";
4
+ import { parse as parseYaml } from "yaml";
5
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
6
+ ".png",
7
+ ".jpg",
8
+ ".jpeg",
9
+ ".gif",
10
+ ".bmp",
11
+ ".ico",
12
+ ".webp",
13
+ ".svg",
14
+ ".woff",
15
+ ".woff2",
16
+ ".ttf",
17
+ ".eot",
18
+ ".otf",
19
+ ".zip",
20
+ ".gz",
21
+ ".tar",
22
+ ".bz2",
23
+ ".7z",
24
+ ".rar",
25
+ ".exe",
26
+ ".dll",
27
+ ".so",
28
+ ".dylib",
29
+ ".bin",
30
+ ".pdf",
31
+ ".doc",
32
+ ".docx",
33
+ ".xls",
34
+ ".xlsx",
35
+ ".mp3",
36
+ ".mp4",
37
+ ".wav",
38
+ ".avi",
39
+ ".mov",
40
+ ".wasm",
41
+ ".pyc",
42
+ ".class"
43
+ ]);
44
+ function parseSkill(dirPath) {
45
+ const absDir = resolve(dirPath);
46
+ const skillMdPath = join(absDir, "SKILL.md");
47
+ const hasSkillMd = existsSync(skillMdPath);
48
+ const raw = hasSkillMd ? readFileSync(skillMdPath, "utf-8") : "";
49
+ const { frontmatter, frontmatterValid, body, bodyStartLine } = parseFrontmatter(raw);
50
+ const files = enumerateFiles(absDir);
51
+ return {
52
+ dirPath: absDir,
53
+ raw,
54
+ frontmatter,
55
+ frontmatterValid,
56
+ body,
57
+ bodyLines: body.split("\n"),
58
+ bodyStartLine,
59
+ files
60
+ };
61
+ }
62
+ function parseSkillContent(content, dirPath = ".") {
63
+ const { frontmatter, frontmatterValid, body, bodyStartLine } = parseFrontmatter(content);
64
+ return {
65
+ dirPath,
66
+ raw: content,
67
+ frontmatter,
68
+ frontmatterValid,
69
+ body,
70
+ bodyLines: body.split("\n"),
71
+ bodyStartLine,
72
+ files: []
73
+ };
74
+ }
75
+ function parseFrontmatter(raw) {
76
+ const fmRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
77
+ const match = raw.match(fmRegex);
78
+ if (!match) {
79
+ return {
80
+ frontmatter: {},
81
+ frontmatterValid: false,
82
+ body: raw,
83
+ bodyStartLine: 1
84
+ };
85
+ }
86
+ const yamlStr = match[1];
87
+ const fmLineCount = match[0].split("\n").length;
88
+ try {
89
+ const parsed = parseYaml(yamlStr);
90
+ return {
91
+ frontmatter: typeof parsed === "object" && parsed !== null ? parsed : {},
92
+ frontmatterValid: true,
93
+ body: raw.slice(match[0].length),
94
+ bodyStartLine: fmLineCount
95
+ };
96
+ } catch {
97
+ return {
98
+ frontmatter: {},
99
+ frontmatterValid: false,
100
+ body: raw.slice(match[0].length),
101
+ bodyStartLine: fmLineCount
102
+ };
103
+ }
104
+ }
105
+ function enumerateFiles(dirPath, maxDepth = 5) {
106
+ const files = [];
107
+ if (!existsSync(dirPath)) return files;
108
+ function walk(currentDir, depth) {
109
+ if (depth > maxDepth) return;
110
+ let entries;
111
+ try {
112
+ entries = readdirSync(currentDir, { withFileTypes: true });
113
+ } catch {
114
+ return;
115
+ }
116
+ for (const entry of entries) {
117
+ const fullPath = join(currentDir, entry.name);
118
+ if (entry.isDirectory()) {
119
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
120
+ walk(fullPath, depth + 1);
121
+ continue;
122
+ }
123
+ const ext = extname(entry.name).toLowerCase();
124
+ let stats;
125
+ try {
126
+ stats = statSync(fullPath);
127
+ } catch {
128
+ continue;
129
+ }
130
+ const isBinary = BINARY_EXTENSIONS.has(ext);
131
+ const relativePath = fullPath.slice(dirPath.length + 1);
132
+ let content;
133
+ if (!isBinary && stats.size < 1e6) {
134
+ try {
135
+ content = readFileSync(fullPath, "utf-8");
136
+ } catch {
137
+ }
138
+ }
139
+ files.push({
140
+ path: relativePath,
141
+ name: basename(entry.name, ext),
142
+ extension: ext,
143
+ sizeBytes: stats.size,
144
+ isBinary,
145
+ content
146
+ });
147
+ }
148
+ }
149
+ walk(dirPath, 0);
150
+ return files;
151
+ }
152
+
153
+ // src/checks/structural.ts
154
+ var HYPHEN_CASE_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
155
+ var MAX_NAME_LENGTH = 64;
156
+ var EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
157
+ ".exe",
158
+ ".bat",
159
+ ".cmd",
160
+ ".sh",
161
+ ".bash",
162
+ ".ps1",
163
+ ".com",
164
+ ".msi"
165
+ ]);
166
+ var BINARY_EXTENSIONS2 = /* @__PURE__ */ new Set([
167
+ ".exe",
168
+ ".dll",
169
+ ".so",
170
+ ".dylib",
171
+ ".bin",
172
+ ".wasm",
173
+ ".class",
174
+ ".pyc"
175
+ ]);
176
+ var structuralChecks = {
177
+ name: "Structural Validity",
178
+ category: "STRUCT",
179
+ run(skill) {
180
+ const results = [];
181
+ if (!skill.raw) {
182
+ results.push({
183
+ id: "STRUCT-001",
184
+ category: "STRUCT",
185
+ severity: "CRITICAL",
186
+ title: "Missing SKILL.md",
187
+ message: "No SKILL.md file found in the skill directory."
188
+ });
189
+ return results;
190
+ }
191
+ if (!skill.frontmatterValid) {
192
+ results.push({
193
+ id: "STRUCT-002",
194
+ category: "STRUCT",
195
+ severity: "HIGH",
196
+ title: "Invalid YAML frontmatter",
197
+ message: "SKILL.md is missing valid YAML frontmatter (---...--- block)."
198
+ });
199
+ }
200
+ if (!skill.frontmatter.name) {
201
+ results.push({
202
+ id: "STRUCT-003",
203
+ category: "STRUCT",
204
+ severity: "HIGH",
205
+ title: "Missing name field",
206
+ message: 'Frontmatter is missing the required "name" field.'
207
+ });
208
+ }
209
+ if (!skill.frontmatter.description) {
210
+ results.push({
211
+ id: "STRUCT-004",
212
+ category: "STRUCT",
213
+ severity: "MEDIUM",
214
+ title: "Missing description field",
215
+ message: 'Frontmatter is missing the "description" field.'
216
+ });
217
+ }
218
+ if (skill.body.trim().length < 50) {
219
+ results.push({
220
+ id: "STRUCT-005",
221
+ category: "STRUCT",
222
+ severity: "CRITICAL",
223
+ title: "SKILL.md body is too short",
224
+ message: `Body is only ${skill.body.trim().length} characters. A valid skill should have meaningful instructions (>=50 chars).`
225
+ });
226
+ }
227
+ for (const file of skill.files) {
228
+ const ext = file.extension.toLowerCase();
229
+ if (BINARY_EXTENSIONS2.has(ext) || EXECUTABLE_EXTENSIONS.has(ext)) {
230
+ results.push({
231
+ id: "STRUCT-006",
232
+ category: "STRUCT",
233
+ severity: "HIGH",
234
+ title: "Unexpected binary/executable file",
235
+ message: `Found unexpected file: ${file.path} (${ext})`
236
+ });
237
+ }
238
+ }
239
+ const name = skill.frontmatter.name;
240
+ if (name) {
241
+ if (!HYPHEN_CASE_RE.test(name)) {
242
+ results.push({
243
+ id: "STRUCT-007",
244
+ category: "STRUCT",
245
+ severity: "MEDIUM",
246
+ title: "Name not in hyphen-case format",
247
+ message: `Skill name "${name}" should be in hyphen-case (e.g. "my-skill").`
248
+ });
249
+ }
250
+ if (name.length > MAX_NAME_LENGTH) {
251
+ results.push({
252
+ id: "STRUCT-007",
253
+ category: "STRUCT",
254
+ severity: "MEDIUM",
255
+ title: "Name too long",
256
+ message: `Skill name is ${name.length} chars, max ${MAX_NAME_LENGTH}.`
257
+ });
258
+ }
259
+ }
260
+ return results;
261
+ }
262
+ };
263
+
264
+ // src/utils/context.ts
265
+ function isInCodeBlock(lines, lineIndex) {
266
+ let inBlock = false;
267
+ for (let i = 0; i < lineIndex && i < lines.length; i++) {
268
+ if (lines[i].trim().startsWith("```")) {
269
+ inBlock = !inBlock;
270
+ }
271
+ }
272
+ return inBlock;
273
+ }
274
+ function isNamespaceOrSchemaURI(url, line) {
275
+ if (/\bxmlns\b/i.test(line)) return true;
276
+ if (/\bnamespace\b/i.test(line)) return true;
277
+ if (/\bschema[s]?\b/i.test(line) && !/(schema\.org)/i.test(url)) return true;
278
+ const parsed = parseURLPath(url);
279
+ if (!parsed) return false;
280
+ if (/\/\d{4}\//.test(parsed.path)) {
281
+ if (!parsed.hasQuery && !parsed.hasFileExtension) return true;
282
+ }
283
+ return false;
284
+ }
285
+ function isInNetworkRequestContext(line) {
286
+ const networkPatterns = [
287
+ /\bfetch\s*\(/i,
288
+ /\bcurl\s+/i,
289
+ /\bwget\s+/i,
290
+ /\baxios\b/i,
291
+ /\brequests?\.(get|post|put|delete|head)\s*\(/i,
292
+ /\bhttp\.(get|request)\s*\(/i,
293
+ /\bopen\s*\(\s*["'](?:GET|POST|PUT|DELETE)/i,
294
+ /\bURLSession\b/,
295
+ /\bInvoke-WebRequest\b/i
296
+ ];
297
+ return networkPatterns.some((p) => p.test(line));
298
+ }
299
+ function isInDocumentationContext(lines, lineIndex) {
300
+ const line = lines[lineIndex];
301
+ if (/^\s*[-*]\s+\*\*\w+\*\*\s*[::]/.test(line)) return true;
302
+ for (let i = lineIndex; i >= Math.max(0, lineIndex - 15); i--) {
303
+ const l = lines[i];
304
+ if (/^#{1,4}\s+.*(install|setup|prerequisite|requirement|depend|getting\s+started)/i.test(l)) {
305
+ return true;
306
+ }
307
+ }
308
+ return false;
309
+ }
310
+ function parseURLPath(url) {
311
+ try {
312
+ const u = new URL(url);
313
+ const hasQuery = u.search.length > 0;
314
+ const lastSegment = u.pathname.split("/").pop() ?? "";
315
+ const hasFileExtension = /\.\w{1,5}$/.test(lastSegment);
316
+ return { path: u.pathname, hasQuery, hasFileExtension };
317
+ } catch {
318
+ return null;
319
+ }
320
+ }
321
+
322
+ // src/checks/content.ts
323
+ var STRICT_PLACEHOLDER_PATTERNS = [
324
+ /\bTODO\b/,
325
+ /\bFIXME\b/,
326
+ /\bHACK\b/,
327
+ /\bXXX\b/,
328
+ /\binsert\s+here\b/i,
329
+ /\bfill\s+in\b/i,
330
+ /\bTBD\b/,
331
+ /\bcoming\s+soon\b/i
332
+ ];
333
+ var CONTEXT_SENSITIVE_PLACEHOLDER_PATTERNS = [
334
+ /\bplaceholder\b/i
335
+ ];
336
+ var LOREM_PATTERNS = [
337
+ /lorem\s+ipsum/i,
338
+ /dolor\s+sit\s+amet/i,
339
+ /consectetur\s+adipiscing/i
340
+ ];
341
+ var AD_PATTERNS = [
342
+ /\bbuy\s+now\b/i,
343
+ /\bfree\s+trial\b/i,
344
+ /\bdiscount\b/i,
345
+ /\bpromo\s*code\b/i,
346
+ /\bsubscribe\s+(to|now)\b/i,
347
+ /\bsponsored\s+by\b/i,
348
+ /\baffiliate\s+link\b/i,
349
+ /\bclick\s+here\s+to\s+(buy|subscribe|download)/i,
350
+ /\buse\s+code\b.*\b\d+%?\s*off\b/i,
351
+ /\bcheck\s+out\s+my\b/i
352
+ ];
353
+ var contentChecks = {
354
+ name: "Content Quality",
355
+ category: "CONT",
356
+ run(skill) {
357
+ const results = [];
358
+ if (!skill.body || skill.body.trim().length === 0) return results;
359
+ for (let i = 0; i < skill.bodyLines.length; i++) {
360
+ const line = skill.bodyLines[i];
361
+ let matched = false;
362
+ for (const pattern of STRICT_PLACEHOLDER_PATTERNS) {
363
+ if (pattern.test(line)) {
364
+ matched = true;
365
+ break;
366
+ }
367
+ }
368
+ if (!matched) {
369
+ const inCodeBlk = isInCodeBlock(skill.bodyLines, i);
370
+ const hasInlineCode = /`[^`]*placeholder[^`]*`/i.test(line);
371
+ const isTechnicalRef = (
372
+ // CSS/HTML context
373
+ /class\s*=\s*["'].*placeholder/i.test(line) || // Compound technical terms
374
+ /placeholder[_-]?(type|text|image|content|area|location|id|index|name|shape)/i.test(line) || // PPT/slide layout context: placeholder alongside slide/layout terms
375
+ /\bplaceholder\b.*\b(TITLE|SUBTITLE|BODY|OBJECT|SLIDE|layout|slide|shape|pptx|presentation)/i.test(line) || /\b(TITLE|SUBTITLE|BODY|OBJECT|SLIDE|layout|slide|shape|pptx|presentation)\b.*\bplaceholder\b/i.test(line) || // API/code context: placeholder as a noun in technical documentation
376
+ /\bplaceholder\s+(areas?|locations?|counts?|slots?|elements?|fields?)\b/i.test(line) || /\b(replace|replacing|replacement)\b.*\bplaceholder\b/i.test(line)
377
+ );
378
+ if (!inCodeBlk && !hasInlineCode && !isTechnicalRef) {
379
+ for (const pattern of CONTEXT_SENSITIVE_PLACEHOLDER_PATTERNS) {
380
+ if (pattern.test(line)) {
381
+ matched = true;
382
+ break;
383
+ }
384
+ }
385
+ }
386
+ }
387
+ if (matched) {
388
+ results.push({
389
+ id: "CONT-001",
390
+ category: "CONT",
391
+ severity: "HIGH",
392
+ title: "Placeholder content detected",
393
+ message: `Line ${skill.bodyStartLine + i}: Contains placeholder text.`,
394
+ line: skill.bodyStartLine + i,
395
+ snippet: line.trim().slice(0, 120)
396
+ });
397
+ }
398
+ }
399
+ for (const pattern of LOREM_PATTERNS) {
400
+ if (pattern.test(skill.body)) {
401
+ results.push({
402
+ id: "CONT-002",
403
+ category: "CONT",
404
+ severity: "CRITICAL",
405
+ title: "Lorem ipsum filler text",
406
+ message: "Body contains lorem ipsum placeholder text."
407
+ });
408
+ break;
409
+ }
410
+ }
411
+ checkRepetition(results, skill);
412
+ checkDescriptionMismatch(results, skill);
413
+ for (let i = 0; i < skill.bodyLines.length; i++) {
414
+ const line = skill.bodyLines[i];
415
+ for (const pattern of AD_PATTERNS) {
416
+ if (pattern.test(line)) {
417
+ results.push({
418
+ id: "CONT-005",
419
+ category: "CONT",
420
+ severity: "HIGH",
421
+ title: "Promotional/advertising content",
422
+ message: `Line ${skill.bodyStartLine + i}: Contains ad-like content.`,
423
+ line: skill.bodyStartLine + i,
424
+ snippet: line.trim().slice(0, 120)
425
+ });
426
+ break;
427
+ }
428
+ }
429
+ }
430
+ checkCodeHeavy(results, skill);
431
+ checkNameMismatch(results, skill);
432
+ return results;
433
+ }
434
+ };
435
+ function checkRepetition(results, skill) {
436
+ const lines = skill.bodyLines.filter((l) => l.trim().length > 0);
437
+ if (lines.length < 5) return;
438
+ const lineCounts = /* @__PURE__ */ new Map();
439
+ for (const line of lines) {
440
+ const normalized = line.trim().toLowerCase();
441
+ lineCounts.set(normalized, (lineCounts.get(normalized) ?? 0) + 1);
442
+ }
443
+ let duplicated = 0;
444
+ for (const count of lineCounts.values()) {
445
+ if (count > 1) duplicated += count - 1;
446
+ }
447
+ const ratio = duplicated / lines.length;
448
+ if (ratio > 0.5) {
449
+ results.push({
450
+ id: "CONT-003",
451
+ category: "CONT",
452
+ severity: "MEDIUM",
453
+ title: "Low information density",
454
+ message: `${Math.round(ratio * 100)}% of lines are duplicates. Possible filler content.`
455
+ });
456
+ }
457
+ }
458
+ function checkDescriptionMismatch(results, skill) {
459
+ const desc = skill.frontmatter.description;
460
+ if (!desc || desc.length < 10) return;
461
+ const descWords = desc.toLowerCase().split(/\W+/).filter((w) => w.length > 4);
462
+ if (descWords.length === 0) return;
463
+ const bodyLower = skill.body.toLowerCase();
464
+ const matched = descWords.filter((w) => bodyLower.includes(w));
465
+ if (matched.length / descWords.length < 0.2) {
466
+ results.push({
467
+ id: "CONT-004",
468
+ category: "CONT",
469
+ severity: "MEDIUM",
470
+ title: "Description/body mismatch",
471
+ message: "The frontmatter description appears unrelated to the body content."
472
+ });
473
+ }
474
+ }
475
+ function checkCodeHeavy(results, skill) {
476
+ const lines = skill.bodyLines;
477
+ if (lines.length < 10) return;
478
+ let inCodeBlock = false;
479
+ let codeLines = 0;
480
+ for (const line of lines) {
481
+ if (line.trim().startsWith("```")) {
482
+ inCodeBlock = !inCodeBlock;
483
+ continue;
484
+ }
485
+ if (inCodeBlock) codeLines++;
486
+ }
487
+ const nonEmptyLines = lines.filter((l) => l.trim().length > 0).length;
488
+ if (nonEmptyLines > 0 && codeLines / nonEmptyLines > 0.8) {
489
+ results.push({
490
+ id: "CONT-006",
491
+ category: "CONT",
492
+ severity: "MEDIUM",
493
+ title: "Body is mostly code examples",
494
+ message: "Over 80% of body content is in code blocks with minimal instructions."
495
+ });
496
+ }
497
+ }
498
+ function checkNameMismatch(results, skill) {
499
+ const name = skill.frontmatter.name;
500
+ if (!name) return;
501
+ const nameWords = name.split(/[-_]/).filter((w) => w.length > 2).map((w) => w.toLowerCase());
502
+ const bodyLower = skill.body.toLowerCase();
503
+ const capabilityHints = nameWords.filter(
504
+ (w) => !["the", "and", "for", "skill", "tool", "helper", "util"].includes(w)
505
+ );
506
+ if (capabilityHints.length === 0) return;
507
+ const matched = capabilityHints.filter((w) => bodyLower.includes(w));
508
+ if (matched.length === 0 && capabilityHints.length >= 2) {
509
+ results.push({
510
+ id: "CONT-007",
511
+ category: "CONT",
512
+ severity: "HIGH",
513
+ title: "Name/body capability mismatch",
514
+ message: `Skill name "${name}" implies capabilities not found in body content.`
515
+ });
516
+ }
517
+ }
518
+
519
+ // src/utils/unicode.ts
520
+ var ZERO_WIDTH_CHARS = [
521
+ "\u200B",
522
+ // ZERO WIDTH SPACE
523
+ "\u200C",
524
+ // ZERO WIDTH NON-JOINER
525
+ "\u200D",
526
+ // ZERO WIDTH JOINER
527
+ "\u200E",
528
+ // LEFT-TO-RIGHT MARK
529
+ "\u200F",
530
+ // RIGHT-TO-LEFT MARK
531
+ "\uFEFF",
532
+ // ZERO WIDTH NO-BREAK SPACE (BOM)
533
+ "\u2060",
534
+ // WORD JOINER
535
+ "\u2061",
536
+ // FUNCTION APPLICATION
537
+ "\u2062",
538
+ // INVISIBLE TIMES
539
+ "\u2063",
540
+ // INVISIBLE SEPARATOR
541
+ "\u2064"
542
+ // INVISIBLE PLUS
543
+ ];
544
+ var RTL_OVERRIDE_CHARS = [
545
+ "\u202A",
546
+ // LEFT-TO-RIGHT EMBEDDING
547
+ "\u202B",
548
+ // RIGHT-TO-LEFT EMBEDDING
549
+ "\u202C",
550
+ // POP DIRECTIONAL FORMATTING
551
+ "\u202D",
552
+ // LEFT-TO-RIGHT OVERRIDE
553
+ "\u202E",
554
+ // RIGHT-TO-LEFT OVERRIDE
555
+ "\u2066",
556
+ // LEFT-TO-RIGHT ISOLATE
557
+ "\u2067",
558
+ // RIGHT-TO-LEFT ISOLATE
559
+ "\u2068",
560
+ // FIRST STRONG ISOLATE
561
+ "\u2069"
562
+ // POP DIRECTIONAL ISOLATE
563
+ ];
564
+ var HOMOGLYPHS = {
565
+ "\u0410": "A",
566
+ // Cyrillic А
567
+ "\u0412": "B",
568
+ // Cyrillic В
569
+ "\u0421": "C",
570
+ // Cyrillic С
571
+ "\u0415": "E",
572
+ // Cyrillic Е
573
+ "\u041D": "H",
574
+ // Cyrillic Н
575
+ "\u041A": "K",
576
+ // Cyrillic К
577
+ "\u041C": "M",
578
+ // Cyrillic М
579
+ "\u041E": "O",
580
+ // Cyrillic О
581
+ "\u0420": "P",
582
+ // Cyrillic Р
583
+ "\u0422": "T",
584
+ // Cyrillic Т
585
+ "\u0425": "X",
586
+ // Cyrillic Х
587
+ "\u0430": "a",
588
+ // Cyrillic а
589
+ "\u0435": "e",
590
+ // Cyrillic е
591
+ "\u043E": "o",
592
+ // Cyrillic о
593
+ "\u0440": "p",
594
+ // Cyrillic р
595
+ "\u0441": "c",
596
+ // Cyrillic с
597
+ "\u0443": "y",
598
+ // Cyrillic у
599
+ "\u0445": "x",
600
+ // Cyrillic х
601
+ "\u0391": "A",
602
+ // Greek Α
603
+ "\u0392": "B",
604
+ // Greek Β
605
+ "\u0395": "E",
606
+ // Greek Ε
607
+ "\u0397": "H",
608
+ // Greek Η
609
+ "\u0399": "I",
610
+ // Greek Ι
611
+ "\u039A": "K",
612
+ // Greek Κ
613
+ "\u039C": "M",
614
+ // Greek Μ
615
+ "\u039D": "N",
616
+ // Greek Ν
617
+ "\u039F": "O",
618
+ // Greek Ο
619
+ "\u03A1": "P",
620
+ // Greek Ρ
621
+ "\u03A4": "T",
622
+ // Greek Τ
623
+ "\u03A7": "X",
624
+ // Greek Χ
625
+ "\u03BF": "o"
626
+ // Greek ο
627
+ };
628
+ function findZeroWidthChars(text) {
629
+ const found = [];
630
+ for (let i = 0; i < text.length; i++) {
631
+ if (ZERO_WIDTH_CHARS.includes(text[i])) {
632
+ found.push({
633
+ char: text[i],
634
+ codePoint: "U+" + text[i].charCodeAt(0).toString(16).toUpperCase().padStart(4, "0"),
635
+ position: i
636
+ });
637
+ }
638
+ }
639
+ return found;
640
+ }
641
+ function findRTLOverrides(text) {
642
+ const found = [];
643
+ for (let i = 0; i < text.length; i++) {
644
+ if (RTL_OVERRIDE_CHARS.includes(text[i])) {
645
+ found.push({
646
+ char: text[i],
647
+ codePoint: "U+" + text[i].charCodeAt(0).toString(16).toUpperCase().padStart(4, "0"),
648
+ position: i
649
+ });
650
+ }
651
+ }
652
+ return found;
653
+ }
654
+ function findHomoglyphs(text) {
655
+ const found = [];
656
+ for (let i = 0; i < text.length; i++) {
657
+ const latin = HOMOGLYPHS[text[i]];
658
+ if (latin) {
659
+ found.push({ char: text[i], looksLike: latin, position: i });
660
+ }
661
+ }
662
+ return found;
663
+ }
664
+
665
+ // src/utils/entropy.ts
666
+ function shannonEntropy(str) {
667
+ if (str.length === 0) return 0;
668
+ const freq = /* @__PURE__ */ new Map();
669
+ for (const ch of str) {
670
+ freq.set(ch, (freq.get(ch) ?? 0) + 1);
671
+ }
672
+ let entropy = 0;
673
+ const len = str.length;
674
+ for (const count of freq.values()) {
675
+ const p = count / len;
676
+ if (p > 0) {
677
+ entropy -= p * Math.log2(p);
678
+ }
679
+ }
680
+ return entropy;
681
+ }
682
+ function isBase64Like(str) {
683
+ if (str.length < 50) return false;
684
+ return /^[A-Za-z0-9+/=]{50,}$/.test(str.trim());
685
+ }
686
+ function isHexEncoded(str) {
687
+ if (str.length < 50) return false;
688
+ return /^(0x)?[0-9a-fA-F]{50,}$/.test(str.trim());
689
+ }
690
+ function tryDecodeBase64(str) {
691
+ try {
692
+ const decoded = Buffer.from(str.trim(), "base64").toString("utf-8");
693
+ const printable = decoded.replace(/[^\x20-\x7E\n\r\t]/g, "");
694
+ if (printable.length / decoded.length > 0.8) {
695
+ return decoded;
696
+ }
697
+ return null;
698
+ } catch {
699
+ return null;
700
+ }
701
+ }
702
+
703
+ // src/checks/injection.ts
704
+ var SYSTEM_OVERRIDE_PATTERNS = [
705
+ /ignore\s+(all\s+)?previous\s+instructions/i,
706
+ /ignore\s+(all\s+)?prior\s+instructions/i,
707
+ /disregard\s+(all\s+)?previous/i,
708
+ /forget\s+(all\s+)?previous/i,
709
+ /you\s+are\s+now\s+a\s+different/i,
710
+ /new\s+system\s+prompt/i,
711
+ /override\s+system\s+prompt/i,
712
+ /your\s+new\s+instructions?\s+(are|is)/i,
713
+ /from\s+now\s+on,?\s+you\s+(will|must|should)/i,
714
+ /act\s+as\s+(if|though)\s+your\s+instructions/i
715
+ ];
716
+ var TOOL_MANIPULATION_PATTERNS = [
717
+ /\bresult\s*[:=]\s*["']?success/i,
718
+ /tool_result/i,
719
+ /<tool_result>/i,
720
+ /\breturn\s+["']?(true|success|approved)/i,
721
+ /permissionDecision\s*[:=]/i
722
+ ];
723
+ var TAG_INJECTION_PATTERNS = [
724
+ /<system>/i,
725
+ /<\/system>/i,
726
+ /<\|im_start\|>/i,
727
+ /<\|im_end\|>/i,
728
+ /<\|endoftext\|>/i,
729
+ /<human>/i,
730
+ /<assistant>/i,
731
+ /<\|system\|>/i,
732
+ /<\|user\|>/i,
733
+ /<\|assistant\|>/i
734
+ ];
735
+ var DELIMITER_PATTERNS = [
736
+ /={5,}/,
737
+ /-{5,}\s*(system|instruction|prompt)/i,
738
+ /#{3,}\s*(system|instruction|prompt)/i,
739
+ /\[SYSTEM\]/i,
740
+ /\[INST\]/i,
741
+ /\[\/INST\]/i
742
+ ];
743
+ var injectionChecks = {
744
+ name: "Injection Detection",
745
+ category: "INJ",
746
+ run(skill) {
747
+ const results = [];
748
+ const fullText = skill.raw;
749
+ const zeroWidth = findZeroWidthChars(fullText);
750
+ if (zeroWidth.length > 0) {
751
+ results.push({
752
+ id: "INJ-001",
753
+ category: "INJ",
754
+ severity: "CRITICAL",
755
+ title: "Zero-width Unicode characters detected",
756
+ message: `Found ${zeroWidth.length} zero-width character(s): ${zeroWidth.slice(0, 5).map((z) => z.codePoint).join(", ")}. These can hide malicious content.`
757
+ });
758
+ }
759
+ const homoglyphs = findHomoglyphs(fullText);
760
+ if (homoglyphs.length > 0) {
761
+ results.push({
762
+ id: "INJ-002",
763
+ category: "INJ",
764
+ severity: "HIGH",
765
+ title: "Homoglyph characters detected",
766
+ message: `Found ${homoglyphs.length} character(s) that mimic Latin letters (e.g. Cyrillic/Greek). Could be used for spoofing.`,
767
+ snippet: homoglyphs.slice(0, 5).map((h) => `"${h.char}" looks like "${h.looksLike}"`).join(", ")
768
+ });
769
+ }
770
+ const rtl = findRTLOverrides(fullText);
771
+ if (rtl.length > 0) {
772
+ results.push({
773
+ id: "INJ-003",
774
+ category: "INJ",
775
+ severity: "CRITICAL",
776
+ title: "RTL override characters detected",
777
+ message: `Found ${rtl.length} RTL/bidirectional override character(s): ${rtl.slice(0, 5).map((r) => r.codePoint).join(", ")}. These can manipulate text display direction.`
778
+ });
779
+ }
780
+ for (let i = 0; i < skill.bodyLines.length; i++) {
781
+ const line = skill.bodyLines[i];
782
+ const lineNum = skill.bodyStartLine + i;
783
+ for (const pattern of SYSTEM_OVERRIDE_PATTERNS) {
784
+ if (pattern.test(line)) {
785
+ results.push({
786
+ id: "INJ-004",
787
+ category: "INJ",
788
+ severity: "CRITICAL",
789
+ title: "System prompt override attempt",
790
+ message: `Line ${lineNum}: Attempts to override system instructions.`,
791
+ line: lineNum,
792
+ snippet: line.trim().slice(0, 120)
793
+ });
794
+ break;
795
+ }
796
+ }
797
+ for (const pattern of TOOL_MANIPULATION_PATTERNS) {
798
+ if (pattern.test(line)) {
799
+ results.push({
800
+ id: "INJ-005",
801
+ category: "INJ",
802
+ severity: "HIGH",
803
+ title: "Tool output manipulation",
804
+ message: `Line ${lineNum}: Attempts to manipulate tool results.`,
805
+ line: lineNum,
806
+ snippet: line.trim().slice(0, 120)
807
+ });
808
+ break;
809
+ }
810
+ }
811
+ for (const pattern of TAG_INJECTION_PATTERNS) {
812
+ if (pattern.test(line)) {
813
+ results.push({
814
+ id: "INJ-007",
815
+ category: "INJ",
816
+ severity: "CRITICAL",
817
+ title: "Tag injection detected",
818
+ message: `Line ${lineNum}: Contains special model/system tags.`,
819
+ line: lineNum,
820
+ snippet: line.trim().slice(0, 120)
821
+ });
822
+ break;
823
+ }
824
+ }
825
+ for (const pattern of DELIMITER_PATTERNS) {
826
+ if (pattern.test(line)) {
827
+ results.push({
828
+ id: "INJ-009",
829
+ category: "INJ",
830
+ severity: "MEDIUM",
831
+ title: "Delimiter confusion pattern",
832
+ message: `Line ${lineNum}: Uses patterns that could confuse model context boundaries.`,
833
+ line: lineNum,
834
+ snippet: line.trim().slice(0, 120)
835
+ });
836
+ break;
837
+ }
838
+ }
839
+ }
840
+ const commentRegex = /<!--([\s\S]*?)-->/g;
841
+ let commentMatch;
842
+ while ((commentMatch = commentRegex.exec(fullText)) !== null) {
843
+ const commentBody = commentMatch[1];
844
+ if (hasInstructionLikeContent(commentBody)) {
845
+ const lineNum = fullText.slice(0, commentMatch.index).split("\n").length;
846
+ results.push({
847
+ id: "INJ-006",
848
+ category: "INJ",
849
+ severity: "HIGH",
850
+ title: "Hidden instructions in HTML comment",
851
+ message: `Line ${lineNum}: HTML comment contains instruction-like content.`,
852
+ line: lineNum,
853
+ snippet: commentBody.trim().slice(0, 120)
854
+ });
855
+ }
856
+ }
857
+ const base64Regex = /[A-Za-z0-9+/=]{60,}/g;
858
+ let b64Match;
859
+ while ((b64Match = base64Regex.exec(skill.body)) !== null) {
860
+ const candidate = b64Match[0];
861
+ if (isBase64Like(candidate)) {
862
+ const decoded = tryDecodeBase64(candidate);
863
+ if (decoded && hasInstructionLikeContent(decoded)) {
864
+ const lineNum = skill.bodyStartLine + skill.body.slice(0, b64Match.index).split("\n").length - 1;
865
+ results.push({
866
+ id: "INJ-008",
867
+ category: "INJ",
868
+ severity: "CRITICAL",
869
+ title: "Encoded instructions detected",
870
+ message: `Line ${lineNum}: Base64 string decodes to instruction-like content.`,
871
+ line: lineNum,
872
+ snippet: decoded.slice(0, 120)
873
+ });
874
+ }
875
+ }
876
+ }
877
+ return dedup(results);
878
+ }
879
+ };
880
+ function hasInstructionLikeContent(text) {
881
+ const instructionPatterns = [
882
+ /you\s+(must|should|will|are)/i,
883
+ /ignore\s+previous/i,
884
+ /execute\s+the\s+following/i,
885
+ /run\s+this\s+command/i,
886
+ /\bsudo\b/i,
887
+ /\brm\s+-rf\b/i,
888
+ /\bcurl\b.*\bsh\b/i,
889
+ /\beval\b/i,
890
+ /\bexec\b/i
891
+ ];
892
+ return instructionPatterns.some((p) => p.test(text));
893
+ }
894
+ function dedup(results) {
895
+ const seen = /* @__PURE__ */ new Set();
896
+ return results.filter((r) => {
897
+ const key = `${r.id}:${r.line ?? ""}`;
898
+ if (seen.has(key)) return false;
899
+ seen.add(key);
900
+ return true;
901
+ });
902
+ }
903
+
904
+ // src/checks/code-safety.ts
905
+ var EVAL_PATTERNS = [
906
+ /\beval\s*\(/,
907
+ /\bexec\s*\(/,
908
+ /\bnew\s+Function\s*\(/,
909
+ /\bsetTimeout\s*\(\s*["'`]/,
910
+ /\bsetInterval\s*\(\s*["'`]/
911
+ ];
912
+ var SHELL_EXEC_PATTERNS = [
913
+ /\bchild_process\b/,
914
+ /\bexecSync\b/,
915
+ /\bspawnSync\b/,
916
+ /\bos\.system\s*\(/,
917
+ /\bsubprocess\.(run|call|Popen)\s*\(/,
918
+ /(?<!\bplatform\.)\bsystem\s*\(/,
919
+ // exclude platform.system()
920
+ /`[^`]*\$\([^)]+\)[^`]*`/
921
+ // backtick with command substitution
922
+ ];
923
+ var SHELL_EXEC_FALSE_POSITIVES = [
924
+ /\bplatform\.system\s*\(\s*\)/
925
+ // Python: just reads OS name
926
+ ];
927
+ var DESTRUCTIVE_PATTERNS = [
928
+ /\brm\s+-rf\b/,
929
+ /\brm\s+-r\b/,
930
+ /\brmdir\b/,
931
+ /\bunlink\s*\(/,
932
+ /\bfs\.rm(Sync)?\s*\(/,
933
+ /\bshutil\.rmtree\s*\(/,
934
+ /\bdel\s+\/[sf]/i,
935
+ /\bformat\s+[a-z]:/i
936
+ ];
937
+ var NETWORK_PATTERNS = [
938
+ /\bfetch\s*\(\s*["'`]https?:\/\//,
939
+ /\baxios\.(get|post|put|delete)\s*\(\s*["'`]https?:\/\//,
940
+ /\bcurl\s+/,
941
+ /\bwget\s+/,
942
+ /\brequests?\.(get|post)\s*\(/,
943
+ /\bhttp\.get\s*\(/,
944
+ /\bURLSession\b/
945
+ ];
946
+ var FILE_WRITE_PATTERNS = [
947
+ /\bfs\.writeFile(Sync)?\s*\(\s*["'`]\//,
948
+ /\bopen\s*\(\s*["'`]\/[^"'`]+["'`]\s*,\s*["'`]w/,
949
+ />\s*\/etc\//,
950
+ />\s*\/usr\//,
951
+ />\s*~\//,
952
+ />\s*\$HOME\//
953
+ ];
954
+ var ENV_ACCESS_PATTERNS = [
955
+ /process\.env\b/,
956
+ /\bos\.environ\b/,
957
+ /\bgetenv\s*\(/,
958
+ /\$\{?\w*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API_KEY)\w*\}?/i
959
+ ];
960
+ var DYNAMIC_CODE_PATTERNS = [
961
+ /\bcompile\s*\(/,
962
+ /\bcodegen\b/i,
963
+ /\bimport\s*\(\s*[^"'`\s]/,
964
+ /\brequire\s*\(\s*[^"'`\s]/,
965
+ /\b__import__\s*\(/
966
+ ];
967
+ var PERMISSION_PATTERNS = [
968
+ /\bchmod\s+[+0-9]/,
969
+ /\bchown\b/,
970
+ /\bsudo\b/,
971
+ /\bdoas\b/,
972
+ /\bsetuid\b/,
973
+ /\bsetgid\b/
974
+ ];
975
+ var codeSafetyChecks = {
976
+ name: "Code Safety",
977
+ category: "CODE",
978
+ run(skill) {
979
+ const results = [];
980
+ const textSources = getTextSources(skill);
981
+ for (const { text, source } of textSources) {
982
+ const lines = text.split("\n");
983
+ for (let i = 0; i < lines.length; i++) {
984
+ const line = lines[i];
985
+ const lineNum = i + 1;
986
+ const loc = `${source}:${lineNum}`;
987
+ checkPatterns(results, line, EVAL_PATTERNS, {
988
+ id: "CODE-001",
989
+ severity: "CRITICAL",
990
+ title: "eval/exec/Function constructor",
991
+ loc,
992
+ lineNum
993
+ });
994
+ if (!SHELL_EXEC_FALSE_POSITIVES.some((p) => p.test(line))) {
995
+ checkPatterns(results, line, SHELL_EXEC_PATTERNS, {
996
+ id: "CODE-002",
997
+ severity: "CRITICAL",
998
+ title: "Shell/subprocess execution",
999
+ loc,
1000
+ lineNum
1001
+ });
1002
+ }
1003
+ checkPatterns(results, line, DESTRUCTIVE_PATTERNS, {
1004
+ id: "CODE-003",
1005
+ severity: "CRITICAL",
1006
+ title: "Destructive file operation",
1007
+ loc,
1008
+ lineNum
1009
+ });
1010
+ checkPatterns(results, line, NETWORK_PATTERNS, {
1011
+ id: "CODE-004",
1012
+ severity: "HIGH",
1013
+ title: "Hardcoded external URL/network request",
1014
+ loc,
1015
+ lineNum
1016
+ });
1017
+ checkPatterns(results, line, FILE_WRITE_PATTERNS, {
1018
+ id: "CODE-005",
1019
+ severity: "HIGH",
1020
+ title: "File write outside expected directory",
1021
+ loc,
1022
+ lineNum
1023
+ });
1024
+ checkPatterns(results, line, ENV_ACCESS_PATTERNS, {
1025
+ id: "CODE-006",
1026
+ severity: "MEDIUM",
1027
+ title: "Environment variable access",
1028
+ loc,
1029
+ lineNum
1030
+ });
1031
+ checkPatterns(results, line, DYNAMIC_CODE_PATTERNS, {
1032
+ id: "CODE-010",
1033
+ severity: "HIGH",
1034
+ title: "Dynamic code generation pattern",
1035
+ loc,
1036
+ lineNum
1037
+ });
1038
+ {
1039
+ const srcLines = text.split("\n");
1040
+ const isDoc = isInDocumentationContext(srcLines, i);
1041
+ if (!isDoc) {
1042
+ checkPatterns(results, line, PERMISSION_PATTERNS, {
1043
+ id: "CODE-012",
1044
+ severity: "HIGH",
1045
+ title: "Permission escalation",
1046
+ loc,
1047
+ lineNum
1048
+ });
1049
+ }
1050
+ }
1051
+ }
1052
+ scanEncodedStrings(results, text, source);
1053
+ scanObfuscation(results, text, source);
1054
+ }
1055
+ return results;
1056
+ }
1057
+ };
1058
+ function checkPatterns(results, line, patterns, opts) {
1059
+ for (const pattern of patterns) {
1060
+ if (pattern.test(line)) {
1061
+ results.push({
1062
+ id: opts.id,
1063
+ category: "CODE",
1064
+ severity: opts.severity,
1065
+ title: opts.title,
1066
+ message: `At ${opts.loc}: ${line.trim().slice(0, 120)}`,
1067
+ line: opts.lineNum,
1068
+ snippet: line.trim().slice(0, 120)
1069
+ });
1070
+ return;
1071
+ }
1072
+ }
1073
+ }
1074
+ function getTextSources(skill) {
1075
+ const sources = [
1076
+ { text: skill.body, source: "SKILL.md" }
1077
+ ];
1078
+ for (const file of skill.files) {
1079
+ if (file.content && file.path !== "SKILL.md") {
1080
+ sources.push({ text: file.content, source: file.path });
1081
+ }
1082
+ }
1083
+ return sources;
1084
+ }
1085
+ function scanEncodedStrings(results, text, source) {
1086
+ const longStringRegex = /[A-Za-z0-9+/=]{50,}|(?:0x)?[0-9a-fA-F]{50,}/g;
1087
+ let match;
1088
+ while ((match = longStringRegex.exec(text)) !== null) {
1089
+ const str = match[0];
1090
+ if (isBase64Like(str) || isHexEncoded(str)) {
1091
+ const lineNum = text.slice(0, match.index).split("\n").length;
1092
+ results.push({
1093
+ id: "CODE-007",
1094
+ category: "CODE",
1095
+ severity: "HIGH",
1096
+ title: "Long encoded string",
1097
+ message: `${source}:${lineNum}: Found ${str.length}-char encoded string.`,
1098
+ line: lineNum,
1099
+ snippet: str.slice(0, 80) + "..."
1100
+ });
1101
+ }
1102
+ }
1103
+ const wordRegex = /\b[A-Za-z0-9_]{20,}\b/g;
1104
+ while ((match = wordRegex.exec(text)) !== null) {
1105
+ const entropy = shannonEntropy(match[0]);
1106
+ if (entropy > 4.5) {
1107
+ const lineNum = text.slice(0, match.index).split("\n").length;
1108
+ results.push({
1109
+ id: "CODE-008",
1110
+ category: "CODE",
1111
+ severity: "MEDIUM",
1112
+ title: "High entropy string",
1113
+ message: `${source}:${lineNum}: String "${match[0].slice(0, 30)}..." has entropy ${entropy.toFixed(2)} bits/char.`,
1114
+ line: lineNum
1115
+ });
1116
+ }
1117
+ }
1118
+ const multiEncodingPatterns = [
1119
+ /atob\s*\(\s*atob/i,
1120
+ /base64.*decode.*base64.*decode/i,
1121
+ /Buffer\.from\(.*Buffer\.from/,
1122
+ /decode.*decode.*decode/i
1123
+ ];
1124
+ for (const pattern of multiEncodingPatterns) {
1125
+ if (pattern.test(text)) {
1126
+ results.push({
1127
+ id: "CODE-009",
1128
+ category: "CODE",
1129
+ severity: "CRITICAL",
1130
+ title: "Multi-layer encoding detected",
1131
+ message: `${source}: Contains nested encoding/decoding operations.`
1132
+ });
1133
+ break;
1134
+ }
1135
+ }
1136
+ }
1137
+ function scanObfuscation(results, text, source) {
1138
+ const obfuscatedVarRegex = /\b_0x[0-9a-f]{2,}\b/g;
1139
+ const obfMatches = text.match(obfuscatedVarRegex);
1140
+ if (obfMatches && obfMatches.length >= 3) {
1141
+ results.push({
1142
+ id: "CODE-011",
1143
+ category: "CODE",
1144
+ severity: "MEDIUM",
1145
+ title: "Obfuscated variable names",
1146
+ message: `${source}: Found ${obfMatches.length} hex-style variable names (e.g. ${obfMatches[0]}). May indicate obfuscated code.`
1147
+ });
1148
+ }
1149
+ }
1150
+
1151
+ // src/checks/supply-chain.ts
1152
+ var SUSPICIOUS_DOMAINS = [
1153
+ "evil.com",
1154
+ "malware.com",
1155
+ "exploit.in",
1156
+ "darkweb.onion",
1157
+ "pastebin.com",
1158
+ // often used for payload hosting
1159
+ "ngrok.io",
1160
+ // tunneling service
1161
+ "requestbin.com",
1162
+ "webhook.site",
1163
+ "pipedream.net",
1164
+ "burpcollaborator.net",
1165
+ "interact.sh",
1166
+ "oastify.com"
1167
+ ];
1168
+ var MCP_SERVER_PATTERN = /\bmcp[-_]?server\b/i;
1169
+ var NPX_Y_PATTERN = /\bnpx\s+-y\s+/;
1170
+ var NPM_INSTALL_PATTERN = /\bnpm\s+install\b/;
1171
+ var PIP_INSTALL_PATTERN = /\bpip3?\s+install\b/;
1172
+ var GIT_CLONE_PATTERN = /\bgit\s+clone\b/;
1173
+ var URL_PATTERN = /https?:\/\/[^\s"'`<>)\]]+/g;
1174
+ var IP_URL_PATTERN = /https?:\/\/(?:\d{1,3}\.){3}\d{1,3}/;
1175
+ var supplyChainChecks = {
1176
+ name: "Supply Chain",
1177
+ category: "SUPPLY",
1178
+ run(skill) {
1179
+ const results = [];
1180
+ const allText = getAllText(skill);
1181
+ for (let i = 0; i < allText.length; i++) {
1182
+ const { line, lineNum, source } = allText[i];
1183
+ if (MCP_SERVER_PATTERN.test(line)) {
1184
+ results.push({
1185
+ id: "SUPPLY-001",
1186
+ category: "SUPPLY",
1187
+ severity: "HIGH",
1188
+ title: "MCP server reference",
1189
+ message: `${source}:${lineNum}: References an MCP server. Verify it is from a trusted source.`,
1190
+ line: lineNum,
1191
+ snippet: line.trim().slice(0, 120)
1192
+ });
1193
+ }
1194
+ if (NPX_Y_PATTERN.test(line)) {
1195
+ results.push({
1196
+ id: "SUPPLY-002",
1197
+ category: "SUPPLY",
1198
+ severity: "MEDIUM",
1199
+ title: "npx -y auto-install",
1200
+ message: `${source}:${lineNum}: Uses npx -y which auto-installs packages without confirmation.`,
1201
+ line: lineNum,
1202
+ snippet: line.trim().slice(0, 120)
1203
+ });
1204
+ }
1205
+ if (NPM_INSTALL_PATTERN.test(line) || PIP_INSTALL_PATTERN.test(line)) {
1206
+ const allLines = getAllLines(skill);
1207
+ const globalIdx = findGlobalLineIndex(allLines, source, lineNum);
1208
+ const isDoc = globalIdx >= 0 && isInDocumentationContext(
1209
+ allLines.map((l) => l.line),
1210
+ globalIdx
1211
+ );
1212
+ if (!isDoc) {
1213
+ results.push({
1214
+ id: "SUPPLY-003",
1215
+ category: "SUPPLY",
1216
+ severity: "HIGH",
1217
+ title: "Package installation command",
1218
+ message: `${source}:${lineNum}: Installs packages. Verify package names are legitimate.`,
1219
+ line: lineNum,
1220
+ snippet: line.trim().slice(0, 120)
1221
+ });
1222
+ }
1223
+ }
1224
+ if (GIT_CLONE_PATTERN.test(line)) {
1225
+ results.push({
1226
+ id: "SUPPLY-006",
1227
+ category: "SUPPLY",
1228
+ severity: "MEDIUM",
1229
+ title: "git clone command",
1230
+ message: `${source}:${lineNum}: Clones a git repository. Verify the source.`,
1231
+ line: lineNum,
1232
+ snippet: line.trim().slice(0, 120)
1233
+ });
1234
+ }
1235
+ const urls = line.match(URL_PATTERN) || [];
1236
+ for (const url of urls) {
1237
+ if (url.startsWith("http://")) {
1238
+ if (!isNamespaceOrSchemaURI(url, line)) {
1239
+ const isNetworkCtx = isInNetworkRequestContext(line);
1240
+ results.push({
1241
+ id: "SUPPLY-004",
1242
+ category: "SUPPLY",
1243
+ severity: isNetworkCtx ? "HIGH" : "MEDIUM",
1244
+ title: "Non-HTTPS URL",
1245
+ message: `${source}:${lineNum}: Uses insecure HTTP: ${url}`,
1246
+ line: lineNum,
1247
+ snippet: url
1248
+ });
1249
+ }
1250
+ }
1251
+ if (IP_URL_PATTERN.test(url)) {
1252
+ if (!/https?:\/\/127\.0\.0\.1/.test(url) && !/https?:\/\/0\.0\.0\.0/.test(url)) {
1253
+ results.push({
1254
+ id: "SUPPLY-005",
1255
+ category: "SUPPLY",
1256
+ severity: "CRITICAL",
1257
+ title: "IP address used instead of domain",
1258
+ message: `${source}:${lineNum}: Uses raw IP address: ${url}. This may bypass DNS-based security.`,
1259
+ line: lineNum,
1260
+ snippet: url
1261
+ });
1262
+ }
1263
+ }
1264
+ for (const domain of SUSPICIOUS_DOMAINS) {
1265
+ if (url.includes(domain)) {
1266
+ results.push({
1267
+ id: "SUPPLY-007",
1268
+ category: "SUPPLY",
1269
+ severity: "CRITICAL",
1270
+ title: "Suspicious domain detected",
1271
+ message: `${source}:${lineNum}: References suspicious domain "${domain}".`,
1272
+ line: lineNum,
1273
+ snippet: url
1274
+ });
1275
+ break;
1276
+ }
1277
+ }
1278
+ }
1279
+ }
1280
+ return results;
1281
+ }
1282
+ };
1283
+ function getAllText(skill) {
1284
+ const result = [];
1285
+ for (let i = 0; i < skill.bodyLines.length; i++) {
1286
+ result.push({
1287
+ line: skill.bodyLines[i],
1288
+ lineNum: skill.bodyStartLine + i,
1289
+ source: "SKILL.md"
1290
+ });
1291
+ }
1292
+ for (const file of skill.files) {
1293
+ if (file.content && file.path !== "SKILL.md") {
1294
+ const lines = file.content.split("\n");
1295
+ for (let i = 0; i < lines.length; i++) {
1296
+ result.push({ line: lines[i], lineNum: i + 1, source: file.path });
1297
+ }
1298
+ }
1299
+ }
1300
+ return result;
1301
+ }
1302
+ function getAllLines(skill) {
1303
+ return getAllText(skill);
1304
+ }
1305
+ function findGlobalLineIndex(allLines, source, lineNum) {
1306
+ return allLines.findIndex(
1307
+ (l) => l.source === source && l.lineNum === lineNum
1308
+ );
1309
+ }
1310
+
1311
+ // src/checks/resource.ts
1312
+ var AMPLIFICATION_PATTERNS = [
1313
+ /\brepeat\s+(this|the\s+above)\s+\d+\s+times\b/i,
1314
+ /\bdo\s+this\s+forever\b/i,
1315
+ /\binfinite\s+loop\b/i,
1316
+ /\bwhile\s*\(\s*true\s*\)/,
1317
+ /\bfor\s*\(\s*;;\s*\)/,
1318
+ /\brecursively\s+(apply|run|execute|call)/i,
1319
+ /\bkeep\s+(running|doing|executing)\s+until/i
1320
+ ];
1321
+ var UNRESTRICTED_TOOL_PATTERNS = [
1322
+ /\bBash\s*\(\s*\*\s*\)/,
1323
+ /allowed[_-]?tools\s*:\s*\[?\s*["']?\*["']?\s*\]?/i,
1324
+ /\ball\s+tools\b/i,
1325
+ /\bunrestricted\s+access\b/i,
1326
+ /\bfull\s+access\b/i
1327
+ ];
1328
+ var DISABLE_SAFETY_PATTERNS = [
1329
+ /\bdisable\s+(safety|security|checks?|hooks?|guard)/i,
1330
+ /\bbypass\s+(safety|security|checks?|hooks?|guard)/i,
1331
+ /\bskip\s+(safety|security|checks?|hooks?|guard|verification)/i,
1332
+ /\bturn\s+off\s+(safety|security|checks?|hooks?)/i,
1333
+ /--no-verify\b/,
1334
+ /--force\b/,
1335
+ /--skip-hooks?\b/
1336
+ ];
1337
+ var IGNORE_RULES_PATTERNS = [
1338
+ /\bignore\s+(the\s+)?CLAUDE\.md\b/i,
1339
+ /\bignore\s+(the\s+)?project\s+rules?\b/i,
1340
+ /\bignore\s+(the\s+)?\.claude\b/i,
1341
+ /\boverride\s+(the\s+)?project\s+(settings?|config|rules?)\b/i,
1342
+ /\bdo\s+not\s+(follow|obey|respect)\s+(the\s+)?(project|CLAUDE)/i,
1343
+ /\bdisregard\s+(the\s+)?(project|CLAUDE)\s+(rules?|config|settings?)/i
1344
+ ];
1345
+ var TOKEN_WASTE_PATTERNS = [
1346
+ /\brepeat\s+(every|each)\s+(response|answer|reply)/i,
1347
+ /\balways\s+(start|begin|end)\s+(every|each)\s+(response|answer|reply)\s+with/i,
1348
+ /\binclude\s+this\s+(text|message|string)\s+in\s+(every|each|all)/i,
1349
+ /\bprint\s+(the\s+)?full\s+(source|code|file)\s+(every|each)\s+time/i
1350
+ ];
1351
+ var resourceChecks = {
1352
+ name: "Resource Abuse",
1353
+ category: "RES",
1354
+ run(skill) {
1355
+ const results = [];
1356
+ for (let i = 0; i < skill.bodyLines.length; i++) {
1357
+ const line = skill.bodyLines[i];
1358
+ const lineNum = skill.bodyStartLine + i;
1359
+ for (const pattern of AMPLIFICATION_PATTERNS) {
1360
+ if (pattern.test(line)) {
1361
+ results.push({
1362
+ id: "RES-001",
1363
+ category: "RES",
1364
+ severity: "HIGH",
1365
+ title: "Instruction amplification",
1366
+ message: `Line ${lineNum}: Contains recursive/repetitive task pattern.`,
1367
+ line: lineNum,
1368
+ snippet: line.trim().slice(0, 120)
1369
+ });
1370
+ break;
1371
+ }
1372
+ }
1373
+ for (const pattern of UNRESTRICTED_TOOL_PATTERNS) {
1374
+ if (pattern.test(line)) {
1375
+ results.push({
1376
+ id: "RES-002",
1377
+ category: "RES",
1378
+ severity: "CRITICAL",
1379
+ title: "Unrestricted tool access requested",
1380
+ message: `Line ${lineNum}: Requests broad/unrestricted tool access.`,
1381
+ line: lineNum,
1382
+ snippet: line.trim().slice(0, 120)
1383
+ });
1384
+ break;
1385
+ }
1386
+ }
1387
+ for (const pattern of DISABLE_SAFETY_PATTERNS) {
1388
+ if (pattern.test(line)) {
1389
+ results.push({
1390
+ id: "RES-004",
1391
+ category: "RES",
1392
+ severity: "CRITICAL",
1393
+ title: "Attempts to disable safety checks",
1394
+ message: `Line ${lineNum}: Instructs disabling of safety mechanisms.`,
1395
+ line: lineNum,
1396
+ snippet: line.trim().slice(0, 120)
1397
+ });
1398
+ break;
1399
+ }
1400
+ }
1401
+ for (const pattern of TOKEN_WASTE_PATTERNS) {
1402
+ if (pattern.test(line)) {
1403
+ results.push({
1404
+ id: "RES-005",
1405
+ category: "RES",
1406
+ severity: "MEDIUM",
1407
+ title: "Token waste pattern",
1408
+ message: `Line ${lineNum}: Contains instructions that waste tokens.`,
1409
+ line: lineNum,
1410
+ snippet: line.trim().slice(0, 120)
1411
+ });
1412
+ break;
1413
+ }
1414
+ }
1415
+ for (const pattern of IGNORE_RULES_PATTERNS) {
1416
+ if (pattern.test(line)) {
1417
+ results.push({
1418
+ id: "RES-006",
1419
+ category: "RES",
1420
+ severity: "CRITICAL",
1421
+ title: "Attempts to ignore project rules",
1422
+ message: `Line ${lineNum}: Instructs ignoring CLAUDE.md or project configuration.`,
1423
+ line: lineNum,
1424
+ snippet: line.trim().slice(0, 120)
1425
+ });
1426
+ break;
1427
+ }
1428
+ }
1429
+ }
1430
+ const allowedTools = skill.frontmatter["allowed-tools"];
1431
+ if (Array.isArray(allowedTools) && allowedTools.length > 15) {
1432
+ results.push({
1433
+ id: "RES-003",
1434
+ category: "RES",
1435
+ severity: "MEDIUM",
1436
+ title: "Excessive allowed-tools list",
1437
+ message: `Frontmatter declares ${allowedTools.length} allowed tools. This is unusually broad.`
1438
+ });
1439
+ }
1440
+ if (Array.isArray(allowedTools)) {
1441
+ for (const tool of allowedTools) {
1442
+ if (typeof tool !== "string") continue;
1443
+ for (const pattern of UNRESTRICTED_TOOL_PATTERNS) {
1444
+ if (pattern.test(tool)) {
1445
+ results.push({
1446
+ id: "RES-002",
1447
+ category: "RES",
1448
+ severity: "CRITICAL",
1449
+ title: "Unrestricted tool access requested",
1450
+ message: `Frontmatter allowed-tools contains dangerous pattern: "${tool}"`,
1451
+ snippet: tool.slice(0, 120)
1452
+ });
1453
+ break;
1454
+ }
1455
+ }
1456
+ }
1457
+ }
1458
+ return results;
1459
+ }
1460
+ };
1461
+
1462
+ // src/ioc/index.ts
1463
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
1464
+ import { join as join2 } from "path";
1465
+ import { homedir } from "os";
1466
+
1467
+ // src/ioc/indicators.ts
1468
+ var DEFAULT_IOC = {
1469
+ version: "2026.03.06",
1470
+ updated: "2026-03-06",
1471
+ c2_ips: [
1472
+ "91.92.242.30",
1473
+ "91.92.242.39",
1474
+ "185.220.101.1",
1475
+ "185.220.101.2",
1476
+ "45.155.205.233"
1477
+ ],
1478
+ malicious_hashes: {
1479
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855": "clawhavoc-empty-payload",
1480
+ "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2": "clawhavoc-exfiltrator"
1481
+ },
1482
+ malicious_domains: [
1483
+ "webhook.site",
1484
+ "requestbin.com",
1485
+ "pipedream.com",
1486
+ "pipedream.net",
1487
+ "hookbin.com",
1488
+ "beeceptor.com",
1489
+ "ngrok.io",
1490
+ "ngrok-free.app",
1491
+ "serveo.net",
1492
+ "localtunnel.me",
1493
+ "bore.pub",
1494
+ "interact.sh",
1495
+ "oast.fun",
1496
+ "oastify.com",
1497
+ "dnslog.cn",
1498
+ "ceye.io",
1499
+ "burpcollaborator.net",
1500
+ "pastebin.com",
1501
+ "paste.ee",
1502
+ "hastebin.com",
1503
+ "ghostbin.com",
1504
+ "evil.com",
1505
+ "malware.com",
1506
+ "exploit.in"
1507
+ ],
1508
+ typosquat: {
1509
+ known_patterns: [
1510
+ "clawhub1",
1511
+ "cllawhub",
1512
+ "clawhab",
1513
+ "moltbot",
1514
+ "claw-hub",
1515
+ "clawhub-pro"
1516
+ ],
1517
+ protected_names: [
1518
+ "clawhub",
1519
+ "secureclaw",
1520
+ "openclaw",
1521
+ "clawbot",
1522
+ "claude",
1523
+ "anthropic",
1524
+ "skill-checker"
1525
+ ]
1526
+ },
1527
+ malicious_publishers: [
1528
+ "clawhavoc",
1529
+ "phantom-tracker",
1530
+ "solana-wallet-drainer"
1531
+ ]
1532
+ };
1533
+
1534
+ // src/ioc/index.ts
1535
+ var cachedIOC = null;
1536
+ function loadIOC() {
1537
+ if (cachedIOC) return cachedIOC;
1538
+ const ioc = structuredClone(DEFAULT_IOC);
1539
+ const overridePath = join2(
1540
+ homedir(),
1541
+ ".config",
1542
+ "skill-checker",
1543
+ "ioc-override.json"
1544
+ );
1545
+ if (existsSync2(overridePath)) {
1546
+ try {
1547
+ const raw = readFileSync2(overridePath, "utf-8");
1548
+ const ext = JSON.parse(raw);
1549
+ mergeIOC(ioc, ext);
1550
+ } catch {
1551
+ }
1552
+ }
1553
+ cachedIOC = ioc;
1554
+ return ioc;
1555
+ }
1556
+ function resetIOCCache() {
1557
+ cachedIOC = null;
1558
+ }
1559
+ function mergeIOC(base, ext) {
1560
+ if (ext.c2_ips) {
1561
+ base.c2_ips = dedupe([...base.c2_ips, ...ext.c2_ips]);
1562
+ }
1563
+ if (ext.malicious_hashes) {
1564
+ Object.assign(base.malicious_hashes, ext.malicious_hashes);
1565
+ }
1566
+ if (ext.malicious_domains) {
1567
+ base.malicious_domains = dedupe([
1568
+ ...base.malicious_domains,
1569
+ ...ext.malicious_domains
1570
+ ]);
1571
+ }
1572
+ if (ext.typosquat) {
1573
+ if (ext.typosquat.known_patterns) {
1574
+ base.typosquat.known_patterns = dedupe([
1575
+ ...base.typosquat.known_patterns,
1576
+ ...ext.typosquat.known_patterns
1577
+ ]);
1578
+ }
1579
+ if (ext.typosquat.protected_names) {
1580
+ base.typosquat.protected_names = dedupe([
1581
+ ...base.typosquat.protected_names,
1582
+ ...ext.typosquat.protected_names
1583
+ ]);
1584
+ }
1585
+ }
1586
+ if (ext.malicious_publishers) {
1587
+ base.malicious_publishers = dedupe([
1588
+ ...base.malicious_publishers,
1589
+ ...ext.malicious_publishers
1590
+ ]);
1591
+ }
1592
+ if (ext.version) base.version = ext.version;
1593
+ if (ext.updated) base.updated = ext.updated;
1594
+ }
1595
+ function dedupe(arr) {
1596
+ return [...new Set(arr)];
1597
+ }
1598
+
1599
+ // src/ioc/matcher.ts
1600
+ import { createHash } from "crypto";
1601
+ import { readFileSync as readFileSync3 } from "fs";
1602
+ import { join as join3 } from "path";
1603
+
1604
+ // src/utils/levenshtein.ts
1605
+ function levenshtein(a, b) {
1606
+ if (a === b) return 0;
1607
+ if (a.length === 0) return b.length;
1608
+ if (b.length === 0) return a.length;
1609
+ if (a.length > b.length) [a, b] = [b, a];
1610
+ const aLen = a.length;
1611
+ const bLen = b.length;
1612
+ let prev = new Array(aLen + 1);
1613
+ let curr = new Array(aLen + 1);
1614
+ for (let i = 0; i <= aLen; i++) prev[i] = i;
1615
+ for (let j = 1; j <= bLen; j++) {
1616
+ curr[0] = j;
1617
+ for (let i = 1; i <= aLen; i++) {
1618
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1619
+ curr[i] = Math.min(
1620
+ prev[i] + 1,
1621
+ // deletion
1622
+ curr[i - 1] + 1,
1623
+ // insertion
1624
+ prev[i - 1] + cost
1625
+ // substitution
1626
+ );
1627
+ }
1628
+ [prev, curr] = [curr, prev];
1629
+ }
1630
+ return prev[aLen];
1631
+ }
1632
+
1633
+ // src/ioc/matcher.ts
1634
+ var IPV4_PATTERN = /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g;
1635
+ function isPrivateIP(ip) {
1636
+ const parts = ip.split(".").map(Number);
1637
+ if (parts.length !== 4 || parts.some((p) => p < 0 || p > 255)) return true;
1638
+ if (parts[0] === 127) return true;
1639
+ if (parts[0] === 10) return true;
1640
+ if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
1641
+ if (parts[0] === 192 && parts[1] === 168) return true;
1642
+ if (parts.every((p) => p === 0)) return true;
1643
+ if (parts[0] === 169 && parts[1] === 254) return true;
1644
+ return false;
1645
+ }
1646
+ function matchMaliciousHashes(skill, ioc) {
1647
+ const matches = [];
1648
+ const hashKeys = Object.keys(ioc.malicious_hashes);
1649
+ if (hashKeys.length === 0) return matches;
1650
+ for (const file of skill.files) {
1651
+ const filePath = join3(skill.dirPath, file.path);
1652
+ try {
1653
+ const content = readFileSync3(filePath);
1654
+ const hash = createHash("sha256").update(content).digest("hex");
1655
+ if (ioc.malicious_hashes[hash]) {
1656
+ matches.push({
1657
+ file: file.path,
1658
+ hash,
1659
+ description: ioc.malicious_hashes[hash]
1660
+ });
1661
+ }
1662
+ } catch {
1663
+ }
1664
+ }
1665
+ return matches;
1666
+ }
1667
+ function matchC2IPs(skill, ioc) {
1668
+ const matches = [];
1669
+ if (ioc.c2_ips.length === 0) return matches;
1670
+ const c2Set = new Set(ioc.c2_ips);
1671
+ const allText = getAllText2(skill);
1672
+ for (const { line, lineNum, source } of allText) {
1673
+ let m;
1674
+ const re = new RegExp(IPV4_PATTERN.source, "g");
1675
+ while ((m = re.exec(line)) !== null) {
1676
+ const ip = m[1];
1677
+ if (!isPrivateIP(ip) && c2Set.has(ip)) {
1678
+ matches.push({
1679
+ ip,
1680
+ line: lineNum,
1681
+ source,
1682
+ snippet: line.trim().slice(0, 120)
1683
+ });
1684
+ }
1685
+ }
1686
+ }
1687
+ return matches;
1688
+ }
1689
+ function matchTyposquat(skillName, ioc) {
1690
+ if (!skillName) return null;
1691
+ const name = skillName.toLowerCase().trim();
1692
+ for (const pattern of ioc.typosquat.known_patterns) {
1693
+ if (name === pattern.toLowerCase()) {
1694
+ return { type: "known", target: pattern };
1695
+ }
1696
+ }
1697
+ for (const protected_name of ioc.typosquat.protected_names) {
1698
+ const pn = protected_name.toLowerCase();
1699
+ if (name === pn) continue;
1700
+ const dist = levenshtein(name, pn);
1701
+ if (dist > 0 && dist <= 2) {
1702
+ return { type: "similar", target: protected_name, distance: dist };
1703
+ }
1704
+ }
1705
+ return null;
1706
+ }
1707
+ function getAllText2(skill) {
1708
+ const result = [];
1709
+ for (let i = 0; i < skill.bodyLines.length; i++) {
1710
+ result.push({
1711
+ line: skill.bodyLines[i],
1712
+ lineNum: skill.bodyStartLine + i,
1713
+ source: "SKILL.md"
1714
+ });
1715
+ }
1716
+ for (const file of skill.files) {
1717
+ if (file.content && file.path !== "SKILL.md") {
1718
+ const lines = file.content.split("\n");
1719
+ for (let i = 0; i < lines.length; i++) {
1720
+ result.push({ line: lines[i], lineNum: i + 1, source: file.path });
1721
+ }
1722
+ }
1723
+ }
1724
+ return result;
1725
+ }
1726
+
1727
+ // src/checks/ioc.ts
1728
+ var iocChecks = {
1729
+ name: "IOC Threat Intelligence",
1730
+ category: "SUPPLY",
1731
+ run(skill) {
1732
+ const results = [];
1733
+ const ioc = loadIOC();
1734
+ const hashMatches = matchMaliciousHashes(skill, ioc);
1735
+ for (const match of hashMatches) {
1736
+ results.push({
1737
+ id: "SUPPLY-008",
1738
+ category: "SUPPLY",
1739
+ severity: "CRITICAL",
1740
+ title: "Known malicious file hash",
1741
+ message: `File "${match.file}" matches known malicious hash: ${match.description}`,
1742
+ snippet: match.hash
1743
+ });
1744
+ }
1745
+ const ipMatches = matchC2IPs(skill, ioc);
1746
+ for (const match of ipMatches) {
1747
+ results.push({
1748
+ id: "SUPPLY-009",
1749
+ category: "SUPPLY",
1750
+ severity: "CRITICAL",
1751
+ title: "Known C2 IP address",
1752
+ message: `${match.source}:${match.line}: Contains known C2 server IP: ${match.ip}`,
1753
+ line: match.line,
1754
+ snippet: match.snippet
1755
+ });
1756
+ }
1757
+ const skillName = skill.frontmatter.name;
1758
+ if (skillName) {
1759
+ const typoMatch = matchTyposquat(skillName, ioc);
1760
+ if (typoMatch) {
1761
+ if (typoMatch.type === "known") {
1762
+ results.push({
1763
+ id: "SUPPLY-010",
1764
+ category: "SUPPLY",
1765
+ severity: "CRITICAL",
1766
+ title: "Known typosquat name",
1767
+ message: `Skill name "${skillName}" matches known typosquat pattern "${typoMatch.target}".`,
1768
+ snippet: skillName
1769
+ });
1770
+ } else {
1771
+ results.push({
1772
+ id: "SUPPLY-010",
1773
+ category: "SUPPLY",
1774
+ severity: "HIGH",
1775
+ title: "Possible typosquat name",
1776
+ message: `Skill name "${skillName}" is similar to protected name "${typoMatch.target}" (edit distance: ${typoMatch.distance}).`,
1777
+ snippet: skillName
1778
+ });
1779
+ }
1780
+ }
1781
+ }
1782
+ return results;
1783
+ }
1784
+ };
1785
+
1786
+ // src/checks/index.ts
1787
+ var ALL_MODULES = [
1788
+ structuralChecks,
1789
+ contentChecks,
1790
+ injectionChecks,
1791
+ codeSafetyChecks,
1792
+ supplyChainChecks,
1793
+ resourceChecks,
1794
+ iocChecks
1795
+ ];
1796
+ function runAllChecks(skill) {
1797
+ const results = [];
1798
+ for (const mod of ALL_MODULES) {
1799
+ results.push(...mod.run(skill));
1800
+ }
1801
+ return results;
1802
+ }
1803
+
1804
+ // src/types.ts
1805
+ var SEVERITY_SCORES = {
1806
+ CRITICAL: 25,
1807
+ HIGH: 10,
1808
+ MEDIUM: 3,
1809
+ LOW: 1
1810
+ };
1811
+ function computeGrade(score) {
1812
+ if (score >= 90) return "A";
1813
+ if (score >= 75) return "B";
1814
+ if (score >= 60) return "C";
1815
+ if (score >= 40) return "D";
1816
+ return "F";
1817
+ }
1818
+ var DEFAULT_CONFIG = {
1819
+ policy: "balanced",
1820
+ overrides: {},
1821
+ ignore: []
1822
+ };
1823
+ function getHookAction(policy, severity) {
1824
+ const matrix = {
1825
+ strict: {
1826
+ CRITICAL: "deny",
1827
+ HIGH: "deny",
1828
+ MEDIUM: "ask",
1829
+ LOW: "report"
1830
+ },
1831
+ balanced: {
1832
+ CRITICAL: "deny",
1833
+ HIGH: "ask",
1834
+ MEDIUM: "report",
1835
+ LOW: "report"
1836
+ },
1837
+ permissive: {
1838
+ CRITICAL: "ask",
1839
+ HIGH: "report",
1840
+ MEDIUM: "report",
1841
+ LOW: "report"
1842
+ }
1843
+ };
1844
+ return matrix[policy][severity];
1845
+ }
1846
+
1847
+ // src/scanner.ts
1848
+ function scanSkillDirectory(dirPath, config = DEFAULT_CONFIG) {
1849
+ const skill = parseSkill(dirPath);
1850
+ return buildReport(skill, config);
1851
+ }
1852
+ function scanSkillContent(content, config = DEFAULT_CONFIG) {
1853
+ const skill = parseSkillContent(content);
1854
+ return buildReport(skill, config);
1855
+ }
1856
+ function buildReport(skill, config) {
1857
+ let results = runAllChecks(skill);
1858
+ results = results.map((r) => {
1859
+ if (config.overrides[r.id]) {
1860
+ return { ...r, severity: config.overrides[r.id] };
1861
+ }
1862
+ return r;
1863
+ });
1864
+ results = results.filter((r) => !config.ignore.includes(r.id));
1865
+ const score = calculateScore(results);
1866
+ const grade = computeGrade(score);
1867
+ const summary = {
1868
+ total: results.length,
1869
+ critical: results.filter((r) => r.severity === "CRITICAL").length,
1870
+ high: results.filter((r) => r.severity === "HIGH").length,
1871
+ medium: results.filter((r) => r.severity === "MEDIUM").length,
1872
+ low: results.filter((r) => r.severity === "LOW").length
1873
+ };
1874
+ return {
1875
+ skillPath: skill.dirPath,
1876
+ skillName: skill.frontmatter.name ?? "unknown",
1877
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1878
+ results,
1879
+ score,
1880
+ grade,
1881
+ summary
1882
+ };
1883
+ }
1884
+ function calculateScore(results) {
1885
+ let score = 100;
1886
+ for (const r of results) {
1887
+ score -= SEVERITY_SCORES[r.severity];
1888
+ }
1889
+ return Math.max(0, score);
1890
+ }
1891
+ function worstSeverity(results) {
1892
+ const order = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
1893
+ for (const sev of order) {
1894
+ if (results.some((r) => r.severity === sev)) return sev;
1895
+ }
1896
+ return null;
1897
+ }
1898
+
1899
+ // src/reporter/terminal.ts
1900
+ import chalk from "chalk";
1901
+ var SEVERITY_COLORS = {
1902
+ CRITICAL: chalk.bgRed.white.bold,
1903
+ HIGH: chalk.red.bold,
1904
+ MEDIUM: chalk.yellow,
1905
+ LOW: chalk.gray
1906
+ };
1907
+ var GRADE_COLORS = {
1908
+ A: chalk.green.bold,
1909
+ B: chalk.cyan.bold,
1910
+ C: chalk.yellow.bold,
1911
+ D: chalk.red.bold,
1912
+ F: chalk.bgRed.white.bold
1913
+ };
1914
+ var SEVERITY_ICONS = {
1915
+ CRITICAL: "X",
1916
+ HIGH: "!",
1917
+ MEDIUM: "~",
1918
+ LOW: "-"
1919
+ };
1920
+ function formatTerminalReport(report) {
1921
+ const lines = [];
1922
+ lines.push("");
1923
+ lines.push(
1924
+ chalk.bold("Skill Security Report") + chalk.gray(` - ${report.skillName}`)
1925
+ );
1926
+ lines.push(chalk.gray(`Path: ${report.skillPath}`));
1927
+ lines.push(chalk.gray(`Time: ${report.timestamp}`));
1928
+ lines.push("");
1929
+ const gradeStr = GRADE_COLORS[report.grade](` ${report.grade} `);
1930
+ const scoreStr = report.score >= 75 ? chalk.green(`${report.score}/100`) : report.score >= 40 ? chalk.yellow(`${report.score}/100`) : chalk.red(`${report.score}/100`);
1931
+ lines.push(`Grade: ${gradeStr} Score: ${scoreStr}`);
1932
+ lines.push("");
1933
+ const parts = [];
1934
+ if (report.summary.critical > 0)
1935
+ parts.push(chalk.bgRed.white(` ${report.summary.critical} CRITICAL `));
1936
+ if (report.summary.high > 0)
1937
+ parts.push(chalk.red(` ${report.summary.high} HIGH `));
1938
+ if (report.summary.medium > 0)
1939
+ parts.push(chalk.yellow(` ${report.summary.medium} MEDIUM `));
1940
+ if (report.summary.low > 0)
1941
+ parts.push(chalk.gray(` ${report.summary.low} LOW `));
1942
+ if (parts.length > 0) {
1943
+ lines.push(`Findings: ${parts.join(" ")}`);
1944
+ } else {
1945
+ lines.push(chalk.green("No issues found."));
1946
+ }
1947
+ lines.push("");
1948
+ if (report.results.length > 0) {
1949
+ lines.push(chalk.bold.underline("Findings:"));
1950
+ lines.push("");
1951
+ const grouped = /* @__PURE__ */ new Map();
1952
+ for (const r of report.results) {
1953
+ const group = grouped.get(r.category) ?? [];
1954
+ group.push(r);
1955
+ grouped.set(r.category, group);
1956
+ }
1957
+ for (const [category, findings] of grouped) {
1958
+ lines.push(chalk.bold(`[${category}]`));
1959
+ const order = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
1960
+ findings.sort(
1961
+ (a, b) => order.indexOf(a.severity) - order.indexOf(b.severity)
1962
+ );
1963
+ for (const f of findings) {
1964
+ const icon = SEVERITY_ICONS[f.severity];
1965
+ const sevLabel = SEVERITY_COLORS[f.severity](
1966
+ ` ${f.severity} `
1967
+ );
1968
+ const idStr = chalk.gray(f.id);
1969
+ lines.push(` [${icon}] ${sevLabel} ${idStr} ${f.title}`);
1970
+ lines.push(` ${chalk.gray(f.message)}`);
1971
+ if (f.snippet) {
1972
+ lines.push(` ${chalk.dim(f.snippet)}`);
1973
+ }
1974
+ }
1975
+ lines.push("");
1976
+ }
1977
+ }
1978
+ lines.push(chalk.bold("Recommendation:"));
1979
+ switch (report.grade) {
1980
+ case "A":
1981
+ lines.push(chalk.green(" Safe to install."));
1982
+ break;
1983
+ case "B":
1984
+ lines.push(chalk.cyan(" Minor issues found. Generally safe."));
1985
+ break;
1986
+ case "C":
1987
+ lines.push(
1988
+ chalk.yellow(" Review recommended before installation.")
1989
+ );
1990
+ break;
1991
+ case "D":
1992
+ lines.push(
1993
+ chalk.red(" Significant risks detected. Install with caution.")
1994
+ );
1995
+ break;
1996
+ case "F":
1997
+ lines.push(
1998
+ chalk.bgRed.white(" DO NOT INSTALL. Critical security issues found.")
1999
+ );
2000
+ break;
2001
+ }
2002
+ lines.push("");
2003
+ return lines.join("\n");
2004
+ }
2005
+
2006
+ // src/reporter/json.ts
2007
+ function formatJsonReport(report) {
2008
+ return JSON.stringify(report, null, 2);
2009
+ }
2010
+ function generateHookResponse(report, config = DEFAULT_CONFIG) {
2011
+ const worst = worstSeverity(report.results);
2012
+ if (!worst) {
2013
+ return { permissionDecision: "allow" };
2014
+ }
2015
+ const action = getHookAction(config.policy, worst);
2016
+ switch (action) {
2017
+ case "deny":
2018
+ return {
2019
+ permissionDecision: "deny",
2020
+ reason: buildDenySummary(report)
2021
+ };
2022
+ case "ask":
2023
+ return {
2024
+ permissionDecision: "ask",
2025
+ reason: buildAskSummary(report)
2026
+ };
2027
+ case "report":
2028
+ return {
2029
+ permissionDecision: "allow",
2030
+ additionalContext: buildReportSummary(report)
2031
+ };
2032
+ }
2033
+ }
2034
+ function buildDenySummary(report) {
2035
+ const lines = [
2036
+ `Skill Security Check FAILED (Grade: ${report.grade}, Score: ${report.score}/100)`
2037
+ ];
2038
+ const criticals = report.results.filter((r) => r.severity === "CRITICAL");
2039
+ if (criticals.length > 0) {
2040
+ lines.push(`Critical issues (${criticals.length}):`);
2041
+ for (const c of criticals.slice(0, 5)) {
2042
+ lines.push(` - [${c.id}] ${c.title}: ${c.message}`);
2043
+ }
2044
+ }
2045
+ return lines.join("\n");
2046
+ }
2047
+ function buildAskSummary(report) {
2048
+ const lines = [
2049
+ `Skill Security Check: Grade ${report.grade} (${report.score}/100)`,
2050
+ `Found: ${report.summary.critical} critical, ${report.summary.high} high, ${report.summary.medium} medium issues.`,
2051
+ "Review the findings before allowing installation."
2052
+ ];
2053
+ return lines.join("\n");
2054
+ }
2055
+ function buildReportSummary(report) {
2056
+ const lines = [
2057
+ `[Skill Checker] Grade: ${report.grade} (${report.score}/100)`,
2058
+ `Issues: ${report.summary.total} (${report.summary.critical}C/${report.summary.high}H/${report.summary.medium}M/${report.summary.low}L)`
2059
+ ];
2060
+ if (report.results.length > 0) {
2061
+ lines.push("Top findings:");
2062
+ for (const r of report.results.slice(0, 3)) {
2063
+ lines.push(` [${r.id}] ${r.severity}: ${r.title}`);
2064
+ }
2065
+ }
2066
+ return lines.join("\n");
2067
+ }
2068
+
2069
+ // src/config.ts
2070
+ import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
2071
+ import { join as join4, resolve as resolve2 } from "path";
2072
+ import { parse as parseYaml2 } from "yaml";
2073
+ var CONFIG_FILENAMES = [
2074
+ ".skillcheckerrc.yaml",
2075
+ ".skillcheckerrc.yml",
2076
+ ".skillcheckerrc"
2077
+ ];
2078
+ function loadConfig(startDir, configPath) {
2079
+ if (configPath) {
2080
+ const absPath = resolve2(configPath);
2081
+ if (existsSync3(absPath)) {
2082
+ return parseConfigFile(absPath);
2083
+ }
2084
+ return { ...DEFAULT_CONFIG };
2085
+ }
2086
+ const dir = startDir ? resolve2(startDir) : process.cwd();
2087
+ let current = dir;
2088
+ while (true) {
2089
+ for (const filename of CONFIG_FILENAMES) {
2090
+ const configPath2 = join4(current, filename);
2091
+ if (existsSync3(configPath2)) {
2092
+ return parseConfigFile(configPath2);
2093
+ }
2094
+ }
2095
+ const parent = join4(current, "..");
2096
+ if (parent === current) break;
2097
+ current = parent;
2098
+ }
2099
+ const home = process.env.HOME ?? process.env.USERPROFILE;
2100
+ if (home) {
2101
+ for (const filename of CONFIG_FILENAMES) {
2102
+ const configPath2 = join4(home, filename);
2103
+ if (existsSync3(configPath2)) {
2104
+ return parseConfigFile(configPath2);
2105
+ }
2106
+ }
2107
+ }
2108
+ return { ...DEFAULT_CONFIG };
2109
+ }
2110
+ function parseConfigFile(path) {
2111
+ try {
2112
+ const raw = readFileSync4(path, "utf-8");
2113
+ const parsed = parseYaml2(raw);
2114
+ if (!parsed || typeof parsed !== "object") {
2115
+ return { ...DEFAULT_CONFIG };
2116
+ }
2117
+ const config = {
2118
+ policy: isValidPolicy(parsed.policy) ? parsed.policy : "balanced",
2119
+ overrides: {},
2120
+ ignore: []
2121
+ };
2122
+ if (parsed.overrides && typeof parsed.overrides === "object") {
2123
+ for (const [key, value] of Object.entries(parsed.overrides)) {
2124
+ const sev = normalizeSeverity(value);
2125
+ if (sev) {
2126
+ config.overrides[key] = sev;
2127
+ }
2128
+ }
2129
+ }
2130
+ if (Array.isArray(parsed.ignore)) {
2131
+ config.ignore = parsed.ignore.filter(
2132
+ (item) => typeof item === "string"
2133
+ );
2134
+ }
2135
+ return config;
2136
+ } catch {
2137
+ return { ...DEFAULT_CONFIG };
2138
+ }
2139
+ }
2140
+ function isValidPolicy(value) {
2141
+ return typeof value === "string" && ["strict", "balanced", "permissive"].includes(value);
2142
+ }
2143
+ function normalizeSeverity(value) {
2144
+ const upper = value?.toUpperCase();
2145
+ if (["CRITICAL", "HIGH", "MEDIUM", "LOW"].includes(upper)) {
2146
+ return upper;
2147
+ }
2148
+ return null;
2149
+ }
2150
+ export {
2151
+ DEFAULT_CONFIG,
2152
+ SEVERITY_SCORES,
2153
+ computeGrade,
2154
+ formatJsonReport,
2155
+ formatTerminalReport,
2156
+ generateHookResponse,
2157
+ getHookAction,
2158
+ loadConfig,
2159
+ loadIOC,
2160
+ parseSkill,
2161
+ parseSkillContent,
2162
+ resetIOCCache,
2163
+ runAllChecks,
2164
+ scanSkillContent,
2165
+ scanSkillDirectory,
2166
+ worstSeverity
2167
+ };
2168
+ //# sourceMappingURL=index.js.map