skill-checker 0.1.0 → 0.1.2
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/cli.js +418 -226
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +420 -229
- package/dist/index.js.map +1 -1
- package/hook/skill-gate.sh +24 -12
- package/package.json +8 -2
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
// src/cli.ts
|
|
9
|
+
import { createRequire } from "module";
|
|
2
10
|
import { Command } from "commander";
|
|
3
11
|
|
|
4
12
|
// src/parser.ts
|
|
@@ -50,7 +58,8 @@ function parseSkill(dirPath) {
|
|
|
50
58
|
const hasSkillMd = existsSync(skillMdPath);
|
|
51
59
|
const raw = hasSkillMd ? readFileSync(skillMdPath, "utf-8") : "";
|
|
52
60
|
const { frontmatter, frontmatterValid, body, bodyStartLine } = parseFrontmatter(raw);
|
|
53
|
-
const
|
|
61
|
+
const warnings = [];
|
|
62
|
+
const files = enumerateFiles(absDir, warnings);
|
|
54
63
|
return {
|
|
55
64
|
dirPath: absDir,
|
|
56
65
|
raw,
|
|
@@ -59,7 +68,8 @@ function parseSkill(dirPath) {
|
|
|
59
68
|
body,
|
|
60
69
|
bodyLines: body.split("\n"),
|
|
61
70
|
bodyStartLine,
|
|
62
|
-
files
|
|
71
|
+
files,
|
|
72
|
+
warnings
|
|
63
73
|
};
|
|
64
74
|
}
|
|
65
75
|
function parseFrontmatter(raw) {
|
|
@@ -92,11 +102,20 @@ function parseFrontmatter(raw) {
|
|
|
92
102
|
};
|
|
93
103
|
}
|
|
94
104
|
}
|
|
95
|
-
|
|
105
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git"]);
|
|
106
|
+
var WARN_SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules"]);
|
|
107
|
+
var MAX_DEPTH = 15;
|
|
108
|
+
var FULL_READ_LIMIT = 5e6;
|
|
109
|
+
var PARTIAL_READ_LIMIT = 512 * 1024;
|
|
110
|
+
function enumerateFiles(dirPath, warnings) {
|
|
96
111
|
const files = [];
|
|
97
112
|
if (!existsSync(dirPath)) return files;
|
|
98
113
|
function walk(currentDir, depth) {
|
|
99
|
-
if (depth >
|
|
114
|
+
if (depth > MAX_DEPTH) {
|
|
115
|
+
const rel = currentDir.slice(dirPath.length + 1) || currentDir;
|
|
116
|
+
warnings.push(`Depth limit (${MAX_DEPTH}) exceeded at: ${rel}. Contents not scanned.`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
100
119
|
let entries;
|
|
101
120
|
try {
|
|
102
121
|
entries = readdirSync(currentDir, { withFileTypes: true });
|
|
@@ -106,7 +125,12 @@ function enumerateFiles(dirPath, maxDepth = 5) {
|
|
|
106
125
|
for (const entry of entries) {
|
|
107
126
|
const fullPath = join(currentDir, entry.name);
|
|
108
127
|
if (entry.isDirectory()) {
|
|
109
|
-
if (
|
|
128
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
129
|
+
if (WARN_SKIP_DIRS.has(entry.name)) {
|
|
130
|
+
const rel = fullPath.slice(dirPath.length + 1);
|
|
131
|
+
warnings.push(`Skipped directory: ${rel}. May contain unscanned files.`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
110
134
|
walk(fullPath, depth + 1);
|
|
111
135
|
continue;
|
|
112
136
|
}
|
|
@@ -120,10 +144,25 @@ function enumerateFiles(dirPath, maxDepth = 5) {
|
|
|
120
144
|
const isBinary = BINARY_EXTENSIONS.has(ext);
|
|
121
145
|
const relativePath = fullPath.slice(dirPath.length + 1);
|
|
122
146
|
let content;
|
|
123
|
-
if (!isBinary
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
147
|
+
if (!isBinary) {
|
|
148
|
+
if (stats.size <= FULL_READ_LIMIT) {
|
|
149
|
+
try {
|
|
150
|
+
content = readFileSync(fullPath, "utf-8");
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
} else if (stats.size <= 5e7) {
|
|
154
|
+
try {
|
|
155
|
+
const fd = __require("fs").openSync(fullPath, "r");
|
|
156
|
+
const buf = Buffer.alloc(PARTIAL_READ_LIMIT);
|
|
157
|
+
const bytesRead = __require("fs").readSync(fd, buf, 0, PARTIAL_READ_LIMIT, 0);
|
|
158
|
+
__require("fs").closeSync(fd);
|
|
159
|
+
content = buf.slice(0, bytesRead).toString("utf-8");
|
|
160
|
+
warnings.push(`Large file partially scanned (first ${PARTIAL_READ_LIMIT} bytes): ${relativePath} (${stats.size} bytes total)`);
|
|
161
|
+
} catch {
|
|
162
|
+
warnings.push(`Large file could not be read: ${relativePath} (${stats.size} bytes)`);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
warnings.push(`File too large to scan: ${relativePath} (${stats.size} bytes). Content not checked.`);
|
|
127
166
|
}
|
|
128
167
|
}
|
|
129
168
|
files.push({
|
|
@@ -247,6 +286,15 @@ var structuralChecks = {
|
|
|
247
286
|
});
|
|
248
287
|
}
|
|
249
288
|
}
|
|
289
|
+
for (const warning of skill.warnings) {
|
|
290
|
+
results.push({
|
|
291
|
+
id: "STRUCT-008",
|
|
292
|
+
category: "STRUCT",
|
|
293
|
+
severity: "MEDIUM",
|
|
294
|
+
title: "Scan coverage warning",
|
|
295
|
+
message: warning
|
|
296
|
+
});
|
|
297
|
+
}
|
|
250
298
|
return results;
|
|
251
299
|
}
|
|
252
300
|
};
|
|
@@ -297,6 +345,14 @@ function isInDocumentationContext(lines, lineIndex) {
|
|
|
297
345
|
}
|
|
298
346
|
return false;
|
|
299
347
|
}
|
|
348
|
+
function isLicenseFile(filePath) {
|
|
349
|
+
const name = filePath.split("/").pop()?.toUpperCase() ?? "";
|
|
350
|
+
const base = name.replace(/\.[^.]+$/, "");
|
|
351
|
+
return /^(LICENSE|LICENCE|COPYING|NOTICE|AUTHORS|PATENTS)$/.test(base);
|
|
352
|
+
}
|
|
353
|
+
function isLocalhostURL(url) {
|
|
354
|
+
return /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])/i.test(url);
|
|
355
|
+
}
|
|
300
356
|
function parseURLPath(url) {
|
|
301
357
|
try {
|
|
302
358
|
const u = new URL(url);
|
|
@@ -891,6 +947,66 @@ function dedup(results) {
|
|
|
891
947
|
});
|
|
892
948
|
}
|
|
893
949
|
|
|
950
|
+
// src/types.ts
|
|
951
|
+
var SEVERITY_SCORES = {
|
|
952
|
+
CRITICAL: 25,
|
|
953
|
+
HIGH: 10,
|
|
954
|
+
MEDIUM: 3,
|
|
955
|
+
LOW: 1
|
|
956
|
+
};
|
|
957
|
+
function computeGrade(score) {
|
|
958
|
+
if (score >= 90) return "A";
|
|
959
|
+
if (score >= 75) return "B";
|
|
960
|
+
if (score >= 60) return "C";
|
|
961
|
+
if (score >= 40) return "D";
|
|
962
|
+
return "F";
|
|
963
|
+
}
|
|
964
|
+
var REDUCE_MAP = {
|
|
965
|
+
CRITICAL: "HIGH",
|
|
966
|
+
HIGH: "MEDIUM",
|
|
967
|
+
MEDIUM: "LOW",
|
|
968
|
+
LOW: "LOW"
|
|
969
|
+
};
|
|
970
|
+
function reduceSeverity(original, reason) {
|
|
971
|
+
let reduced = REDUCE_MAP[original];
|
|
972
|
+
if (original === "CRITICAL" && reduced === "LOW") {
|
|
973
|
+
reduced = "MEDIUM";
|
|
974
|
+
}
|
|
975
|
+
return {
|
|
976
|
+
severity: reduced,
|
|
977
|
+
reducedFrom: original,
|
|
978
|
+
annotation: `[reduced: ${reason}]`
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
var DEFAULT_CONFIG = {
|
|
982
|
+
policy: "balanced",
|
|
983
|
+
overrides: {},
|
|
984
|
+
ignore: []
|
|
985
|
+
};
|
|
986
|
+
function getHookAction(policy, severity) {
|
|
987
|
+
const matrix = {
|
|
988
|
+
strict: {
|
|
989
|
+
CRITICAL: "deny",
|
|
990
|
+
HIGH: "deny",
|
|
991
|
+
MEDIUM: "ask",
|
|
992
|
+
LOW: "report"
|
|
993
|
+
},
|
|
994
|
+
balanced: {
|
|
995
|
+
CRITICAL: "deny",
|
|
996
|
+
HIGH: "ask",
|
|
997
|
+
MEDIUM: "report",
|
|
998
|
+
LOW: "report"
|
|
999
|
+
},
|
|
1000
|
+
permissive: {
|
|
1001
|
+
CRITICAL: "ask",
|
|
1002
|
+
HIGH: "report",
|
|
1003
|
+
MEDIUM: "report",
|
|
1004
|
+
LOW: "report"
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
return matrix[policy][severity];
|
|
1008
|
+
}
|
|
1009
|
+
|
|
894
1010
|
// src/checks/code-safety.ts
|
|
895
1011
|
var EVAL_PATTERNS = [
|
|
896
1012
|
/\beval\s*\(/,
|
|
@@ -974,6 +1090,7 @@ var codeSafetyChecks = {
|
|
|
974
1090
|
const line = lines[i];
|
|
975
1091
|
const lineNum = i + 1;
|
|
976
1092
|
const loc = `${source}:${lineNum}`;
|
|
1093
|
+
const cbCtx = { lines, index: i };
|
|
977
1094
|
checkPatterns(results, line, EVAL_PATTERNS, {
|
|
978
1095
|
id: "CODE-001",
|
|
979
1096
|
severity: "CRITICAL",
|
|
@@ -995,14 +1112,16 @@ var codeSafetyChecks = {
|
|
|
995
1112
|
severity: "CRITICAL",
|
|
996
1113
|
title: "Destructive file operation",
|
|
997
1114
|
loc,
|
|
998
|
-
lineNum
|
|
1115
|
+
lineNum,
|
|
1116
|
+
codeBlockCtx: cbCtx
|
|
999
1117
|
});
|
|
1000
1118
|
checkPatterns(results, line, NETWORK_PATTERNS, {
|
|
1001
1119
|
id: "CODE-004",
|
|
1002
1120
|
severity: "HIGH",
|
|
1003
1121
|
title: "Hardcoded external URL/network request",
|
|
1004
1122
|
loc,
|
|
1005
|
-
lineNum
|
|
1123
|
+
lineNum,
|
|
1124
|
+
codeBlockCtx: cbCtx
|
|
1006
1125
|
});
|
|
1007
1126
|
checkPatterns(results, line, FILE_WRITE_PATTERNS, {
|
|
1008
1127
|
id: "CODE-005",
|
|
@@ -1016,7 +1135,8 @@ var codeSafetyChecks = {
|
|
|
1016
1135
|
severity: "MEDIUM",
|
|
1017
1136
|
title: "Environment variable access",
|
|
1018
1137
|
loc,
|
|
1019
|
-
lineNum
|
|
1138
|
+
lineNum,
|
|
1139
|
+
codeBlockCtx: cbCtx
|
|
1020
1140
|
});
|
|
1021
1141
|
checkPatterns(results, line, DYNAMIC_CODE_PATTERNS, {
|
|
1022
1142
|
id: "CODE-010",
|
|
@@ -1026,8 +1146,7 @@ var codeSafetyChecks = {
|
|
|
1026
1146
|
lineNum
|
|
1027
1147
|
});
|
|
1028
1148
|
{
|
|
1029
|
-
const
|
|
1030
|
-
const isDoc = isInDocumentationContext(srcLines, i);
|
|
1149
|
+
const isDoc = isInDocumentationContext(lines, i);
|
|
1031
1150
|
if (!isDoc) {
|
|
1032
1151
|
checkPatterns(results, line, PERMISSION_PATTERNS, {
|
|
1033
1152
|
id: "CODE-012",
|
|
@@ -1048,14 +1167,24 @@ var codeSafetyChecks = {
|
|
|
1048
1167
|
function checkPatterns(results, line, patterns, opts) {
|
|
1049
1168
|
for (const pattern of patterns) {
|
|
1050
1169
|
if (pattern.test(line)) {
|
|
1170
|
+
let severity = opts.severity;
|
|
1171
|
+
let reducedFrom;
|
|
1172
|
+
let msgSuffix = "";
|
|
1173
|
+
if (opts.codeBlockCtx && isInCodeBlock(opts.codeBlockCtx.lines, opts.codeBlockCtx.index)) {
|
|
1174
|
+
const r = reduceSeverity(severity, "in code block");
|
|
1175
|
+
severity = r.severity;
|
|
1176
|
+
reducedFrom = r.reducedFrom;
|
|
1177
|
+
msgSuffix = ` ${r.annotation}`;
|
|
1178
|
+
}
|
|
1051
1179
|
results.push({
|
|
1052
1180
|
id: opts.id,
|
|
1053
1181
|
category: "CODE",
|
|
1054
|
-
severity
|
|
1182
|
+
severity,
|
|
1055
1183
|
title: opts.title,
|
|
1056
|
-
message: `At ${opts.loc}: ${line.trim().slice(0, 120)}`,
|
|
1184
|
+
message: `At ${opts.loc}: ${line.trim().slice(0, 120)}${msgSuffix}`,
|
|
1057
1185
|
line: opts.lineNum,
|
|
1058
|
-
snippet: line.trim().slice(0, 120)
|
|
1186
|
+
snippet: line.trim().slice(0, 120),
|
|
1187
|
+
reducedFrom
|
|
1059
1188
|
});
|
|
1060
1189
|
return;
|
|
1061
1190
|
}
|
|
@@ -1138,22 +1267,144 @@ function scanObfuscation(results, text, source) {
|
|
|
1138
1267
|
}
|
|
1139
1268
|
}
|
|
1140
1269
|
|
|
1270
|
+
// src/ioc/index.ts
|
|
1271
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
1272
|
+
import { join as join2 } from "path";
|
|
1273
|
+
import { homedir } from "os";
|
|
1274
|
+
|
|
1275
|
+
// src/ioc/indicators.ts
|
|
1276
|
+
var DEFAULT_IOC = {
|
|
1277
|
+
version: "2026.03.06",
|
|
1278
|
+
updated: "2026-03-06",
|
|
1279
|
+
c2_ips: [
|
|
1280
|
+
"91.92.242.30",
|
|
1281
|
+
"91.92.242.39",
|
|
1282
|
+
"185.220.101.1",
|
|
1283
|
+
"185.220.101.2",
|
|
1284
|
+
"45.155.205.233"
|
|
1285
|
+
],
|
|
1286
|
+
malicious_hashes: {
|
|
1287
|
+
// NOTE: Never add the SHA-256 of an empty file (e3b0c44298fc...b855)
|
|
1288
|
+
// as it causes false positives on any empty file.
|
|
1289
|
+
"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2": "clawhavoc-exfiltrator"
|
|
1290
|
+
},
|
|
1291
|
+
malicious_domains: [
|
|
1292
|
+
"webhook.site",
|
|
1293
|
+
"requestbin.com",
|
|
1294
|
+
"pipedream.com",
|
|
1295
|
+
"pipedream.net",
|
|
1296
|
+
"hookbin.com",
|
|
1297
|
+
"beeceptor.com",
|
|
1298
|
+
"ngrok.io",
|
|
1299
|
+
"ngrok-free.app",
|
|
1300
|
+
"serveo.net",
|
|
1301
|
+
"localtunnel.me",
|
|
1302
|
+
"bore.pub",
|
|
1303
|
+
"interact.sh",
|
|
1304
|
+
"oast.fun",
|
|
1305
|
+
"oastify.com",
|
|
1306
|
+
"dnslog.cn",
|
|
1307
|
+
"ceye.io",
|
|
1308
|
+
"burpcollaborator.net",
|
|
1309
|
+
"pastebin.com",
|
|
1310
|
+
"paste.ee",
|
|
1311
|
+
"hastebin.com",
|
|
1312
|
+
"ghostbin.com",
|
|
1313
|
+
"evil.com",
|
|
1314
|
+
"malware.com",
|
|
1315
|
+
"exploit.in"
|
|
1316
|
+
],
|
|
1317
|
+
typosquat: {
|
|
1318
|
+
known_patterns: [
|
|
1319
|
+
"clawhub1",
|
|
1320
|
+
"cllawhub",
|
|
1321
|
+
"clawhab",
|
|
1322
|
+
"moltbot",
|
|
1323
|
+
"claw-hub",
|
|
1324
|
+
"clawhub-pro"
|
|
1325
|
+
],
|
|
1326
|
+
protected_names: [
|
|
1327
|
+
"clawhub",
|
|
1328
|
+
"secureclaw",
|
|
1329
|
+
"openclaw",
|
|
1330
|
+
"clawbot",
|
|
1331
|
+
"claude",
|
|
1332
|
+
"anthropic",
|
|
1333
|
+
"skill-checker"
|
|
1334
|
+
]
|
|
1335
|
+
},
|
|
1336
|
+
malicious_publishers: [
|
|
1337
|
+
"clawhavoc",
|
|
1338
|
+
"phantom-tracker",
|
|
1339
|
+
"solana-wallet-drainer"
|
|
1340
|
+
]
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
// src/ioc/index.ts
|
|
1344
|
+
var cachedIOC = null;
|
|
1345
|
+
function loadIOC() {
|
|
1346
|
+
if (cachedIOC) return cachedIOC;
|
|
1347
|
+
const ioc = structuredClone(DEFAULT_IOC);
|
|
1348
|
+
const overridePath = join2(
|
|
1349
|
+
homedir(),
|
|
1350
|
+
".config",
|
|
1351
|
+
"skill-checker",
|
|
1352
|
+
"ioc-override.json"
|
|
1353
|
+
);
|
|
1354
|
+
if (existsSync2(overridePath)) {
|
|
1355
|
+
try {
|
|
1356
|
+
const raw = readFileSync2(overridePath, "utf-8");
|
|
1357
|
+
const ext = JSON.parse(raw);
|
|
1358
|
+
mergeIOC(ioc, ext);
|
|
1359
|
+
} catch {
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
cachedIOC = ioc;
|
|
1363
|
+
return ioc;
|
|
1364
|
+
}
|
|
1365
|
+
function mergeIOC(base, ext) {
|
|
1366
|
+
if (ext.c2_ips) {
|
|
1367
|
+
base.c2_ips = dedupe([...base.c2_ips, ...ext.c2_ips]);
|
|
1368
|
+
}
|
|
1369
|
+
if (ext.malicious_hashes) {
|
|
1370
|
+
Object.assign(base.malicious_hashes, ext.malicious_hashes);
|
|
1371
|
+
}
|
|
1372
|
+
if (ext.malicious_domains) {
|
|
1373
|
+
base.malicious_domains = dedupe([
|
|
1374
|
+
...base.malicious_domains,
|
|
1375
|
+
...ext.malicious_domains
|
|
1376
|
+
]);
|
|
1377
|
+
}
|
|
1378
|
+
if (ext.typosquat) {
|
|
1379
|
+
if (ext.typosquat.known_patterns) {
|
|
1380
|
+
base.typosquat.known_patterns = dedupe([
|
|
1381
|
+
...base.typosquat.known_patterns,
|
|
1382
|
+
...ext.typosquat.known_patterns
|
|
1383
|
+
]);
|
|
1384
|
+
}
|
|
1385
|
+
if (ext.typosquat.protected_names) {
|
|
1386
|
+
base.typosquat.protected_names = dedupe([
|
|
1387
|
+
...base.typosquat.protected_names,
|
|
1388
|
+
...ext.typosquat.protected_names
|
|
1389
|
+
]);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
if (ext.malicious_publishers) {
|
|
1393
|
+
base.malicious_publishers = dedupe([
|
|
1394
|
+
...base.malicious_publishers,
|
|
1395
|
+
...ext.malicious_publishers
|
|
1396
|
+
]);
|
|
1397
|
+
}
|
|
1398
|
+
if (ext.version) base.version = ext.version;
|
|
1399
|
+
if (ext.updated) base.updated = ext.updated;
|
|
1400
|
+
}
|
|
1401
|
+
function dedupe(arr) {
|
|
1402
|
+
return [...new Set(arr)];
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1141
1405
|
// src/checks/supply-chain.ts
|
|
1142
|
-
var
|
|
1143
|
-
"
|
|
1144
|
-
"malware.com",
|
|
1145
|
-
"exploit.in",
|
|
1146
|
-
"darkweb.onion",
|
|
1147
|
-
"pastebin.com",
|
|
1148
|
-
// often used for payload hosting
|
|
1149
|
-
"ngrok.io",
|
|
1150
|
-
// tunneling service
|
|
1151
|
-
"requestbin.com",
|
|
1152
|
-
"webhook.site",
|
|
1153
|
-
"pipedream.net",
|
|
1154
|
-
"burpcollaborator.net",
|
|
1155
|
-
"interact.sh",
|
|
1156
|
-
"oastify.com"
|
|
1406
|
+
var FALLBACK_SUSPICIOUS_DOMAINS = [
|
|
1407
|
+
"darkweb.onion"
|
|
1157
1408
|
];
|
|
1158
1409
|
var MCP_SERVER_PATTERN = /\bmcp[-_]?server\b/i;
|
|
1159
1410
|
var NPX_Y_PATTERN = /\bnpx\s+-y\s+/;
|
|
@@ -1162,23 +1413,47 @@ var PIP_INSTALL_PATTERN = /\bpip3?\s+install\b/;
|
|
|
1162
1413
|
var GIT_CLONE_PATTERN = /\bgit\s+clone\b/;
|
|
1163
1414
|
var URL_PATTERN = /https?:\/\/[^\s"'`<>)\]]+/g;
|
|
1164
1415
|
var IP_URL_PATTERN = /https?:\/\/(?:\d{1,3}\.){3}\d{1,3}/;
|
|
1416
|
+
function extractHostname(url) {
|
|
1417
|
+
const afterProto = url.replace(/^https?:\/\//, "");
|
|
1418
|
+
const hostPort = afterProto.split("/")[0].split("?")[0].split("#")[0];
|
|
1419
|
+
const host = hostPort.split(":")[0];
|
|
1420
|
+
return host.toLowerCase();
|
|
1421
|
+
}
|
|
1422
|
+
function hostnameMatchesDomain(hostname, domain) {
|
|
1423
|
+
const d = domain.toLowerCase();
|
|
1424
|
+
return hostname === d || hostname.endsWith("." + d);
|
|
1425
|
+
}
|
|
1165
1426
|
var supplyChainChecks = {
|
|
1166
1427
|
name: "Supply Chain",
|
|
1167
1428
|
category: "SUPPLY",
|
|
1168
1429
|
run(skill) {
|
|
1169
1430
|
const results = [];
|
|
1170
1431
|
const allText = getAllText(skill);
|
|
1432
|
+
const ioc = loadIOC();
|
|
1433
|
+
const suspiciousDomains = ioc.malicious_domains.length > 0 ? ioc.malicious_domains : FALLBACK_SUSPICIOUS_DOMAINS;
|
|
1171
1434
|
for (let i = 0; i < allText.length; i++) {
|
|
1172
1435
|
const { line, lineNum, source } = allText[i];
|
|
1173
1436
|
if (MCP_SERVER_PATTERN.test(line)) {
|
|
1437
|
+
let severity = "HIGH";
|
|
1438
|
+
let reducedFrom;
|
|
1439
|
+
let msgSuffix = "";
|
|
1440
|
+
const srcLines = getLinesForSource(skill, source);
|
|
1441
|
+
const localIdx = getLocalIndex(source, lineNum, skill.bodyStartLine);
|
|
1442
|
+
if (localIdx >= 0 && isInCodeBlock(srcLines, localIdx)) {
|
|
1443
|
+
const r = reduceSeverity(severity, "in code block");
|
|
1444
|
+
severity = r.severity;
|
|
1445
|
+
reducedFrom = r.reducedFrom;
|
|
1446
|
+
msgSuffix = ` ${r.annotation}`;
|
|
1447
|
+
}
|
|
1174
1448
|
results.push({
|
|
1175
1449
|
id: "SUPPLY-001",
|
|
1176
1450
|
category: "SUPPLY",
|
|
1177
|
-
severity
|
|
1451
|
+
severity,
|
|
1178
1452
|
title: "MCP server reference",
|
|
1179
|
-
message: `${source}:${lineNum}: References an MCP server. Verify it is from a trusted source
|
|
1453
|
+
message: `${source}:${lineNum}: References an MCP server. Verify it is from a trusted source.${msgSuffix}`,
|
|
1180
1454
|
line: lineNum,
|
|
1181
|
-
snippet: line.trim().slice(0, 120)
|
|
1455
|
+
snippet: line.trim().slice(0, 120),
|
|
1456
|
+
reducedFrom
|
|
1182
1457
|
});
|
|
1183
1458
|
}
|
|
1184
1459
|
if (NPX_Y_PATTERN.test(line)) {
|
|
@@ -1200,14 +1475,26 @@ var supplyChainChecks = {
|
|
|
1200
1475
|
globalIdx
|
|
1201
1476
|
);
|
|
1202
1477
|
if (!isDoc) {
|
|
1478
|
+
let severity = "HIGH";
|
|
1479
|
+
let reducedFrom;
|
|
1480
|
+
let msgSuffix = "";
|
|
1481
|
+
const srcLines = getLinesForSource(skill, source);
|
|
1482
|
+
const localIdx = getLocalIndex(source, lineNum, skill.bodyStartLine);
|
|
1483
|
+
if (localIdx >= 0 && isInCodeBlock(srcLines, localIdx)) {
|
|
1484
|
+
const r = reduceSeverity(severity, "in code block");
|
|
1485
|
+
severity = r.severity;
|
|
1486
|
+
reducedFrom = r.reducedFrom;
|
|
1487
|
+
msgSuffix = ` ${r.annotation}`;
|
|
1488
|
+
}
|
|
1203
1489
|
results.push({
|
|
1204
1490
|
id: "SUPPLY-003",
|
|
1205
1491
|
category: "SUPPLY",
|
|
1206
|
-
severity
|
|
1492
|
+
severity,
|
|
1207
1493
|
title: "Package installation command",
|
|
1208
|
-
message: `${source}:${lineNum}: Installs packages. Verify package names are legitimate
|
|
1494
|
+
message: `${source}:${lineNum}: Installs packages. Verify package names are legitimate.${msgSuffix}`,
|
|
1209
1495
|
line: lineNum,
|
|
1210
|
-
snippet: line.trim().slice(0, 120)
|
|
1496
|
+
snippet: line.trim().slice(0, 120),
|
|
1497
|
+
reducedFrom
|
|
1211
1498
|
});
|
|
1212
1499
|
}
|
|
1213
1500
|
}
|
|
@@ -1225,16 +1512,30 @@ var supplyChainChecks = {
|
|
|
1225
1512
|
const urls = line.match(URL_PATTERN) || [];
|
|
1226
1513
|
for (const url of urls) {
|
|
1227
1514
|
if (url.startsWith("http://")) {
|
|
1515
|
+
if (isLicenseFile(source)) continue;
|
|
1516
|
+
if (isLocalhostURL(url)) continue;
|
|
1228
1517
|
if (!isNamespaceOrSchemaURI(url, line)) {
|
|
1229
1518
|
const isNetworkCtx = isInNetworkRequestContext(line);
|
|
1519
|
+
let severity = isNetworkCtx ? "HIGH" : "MEDIUM";
|
|
1520
|
+
let reducedFrom;
|
|
1521
|
+
let msgSuffix = "";
|
|
1522
|
+
const srcLines = getLinesForSource(skill, source);
|
|
1523
|
+
const localIdx = getLocalIndex(source, lineNum, skill.bodyStartLine);
|
|
1524
|
+
if (localIdx >= 0 && isInCodeBlock(srcLines, localIdx)) {
|
|
1525
|
+
const r = reduceSeverity(severity, "in code block");
|
|
1526
|
+
severity = r.severity;
|
|
1527
|
+
reducedFrom = r.reducedFrom;
|
|
1528
|
+
msgSuffix = ` ${r.annotation}`;
|
|
1529
|
+
}
|
|
1230
1530
|
results.push({
|
|
1231
1531
|
id: "SUPPLY-004",
|
|
1232
1532
|
category: "SUPPLY",
|
|
1233
|
-
severity
|
|
1533
|
+
severity,
|
|
1234
1534
|
title: "Non-HTTPS URL",
|
|
1235
|
-
message: `${source}:${lineNum}: Uses insecure HTTP: ${url}`,
|
|
1535
|
+
message: `${source}:${lineNum}: Uses insecure HTTP: ${url}${msgSuffix}`,
|
|
1236
1536
|
line: lineNum,
|
|
1237
|
-
snippet: url
|
|
1537
|
+
snippet: url,
|
|
1538
|
+
reducedFrom
|
|
1238
1539
|
});
|
|
1239
1540
|
}
|
|
1240
1541
|
}
|
|
@@ -1251,8 +1552,9 @@ var supplyChainChecks = {
|
|
|
1251
1552
|
});
|
|
1252
1553
|
}
|
|
1253
1554
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1555
|
+
const hostname = extractHostname(url);
|
|
1556
|
+
for (const domain of suspiciousDomains) {
|
|
1557
|
+
if (hostnameMatchesDomain(hostname, domain)) {
|
|
1256
1558
|
results.push({
|
|
1257
1559
|
id: "SUPPLY-007",
|
|
1258
1560
|
category: "SUPPLY",
|
|
@@ -1270,6 +1572,15 @@ var supplyChainChecks = {
|
|
|
1270
1572
|
return results;
|
|
1271
1573
|
}
|
|
1272
1574
|
};
|
|
1575
|
+
function getLinesForSource(skill, source) {
|
|
1576
|
+
if (source === "SKILL.md") return skill.bodyLines;
|
|
1577
|
+
const file = skill.files.find((f) => f.path === source);
|
|
1578
|
+
return file?.content?.split("\n") ?? [];
|
|
1579
|
+
}
|
|
1580
|
+
function getLocalIndex(source, lineNum, bodyStartLine) {
|
|
1581
|
+
if (source === "SKILL.md") return lineNum - bodyStartLine;
|
|
1582
|
+
return lineNum - 1;
|
|
1583
|
+
}
|
|
1273
1584
|
function getAllText(skill) {
|
|
1274
1585
|
const result = [];
|
|
1275
1586
|
for (let i = 0; i < skill.bodyLines.length; i++) {
|
|
@@ -1449,143 +1760,9 @@ var resourceChecks = {
|
|
|
1449
1760
|
}
|
|
1450
1761
|
};
|
|
1451
1762
|
|
|
1452
|
-
// src/ioc/index.ts
|
|
1453
|
-
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
1454
|
-
import { join as join2 } from "path";
|
|
1455
|
-
import { homedir } from "os";
|
|
1456
|
-
|
|
1457
|
-
// src/ioc/indicators.ts
|
|
1458
|
-
var DEFAULT_IOC = {
|
|
1459
|
-
version: "2026.03.06",
|
|
1460
|
-
updated: "2026-03-06",
|
|
1461
|
-
c2_ips: [
|
|
1462
|
-
"91.92.242.30",
|
|
1463
|
-
"91.92.242.39",
|
|
1464
|
-
"185.220.101.1",
|
|
1465
|
-
"185.220.101.2",
|
|
1466
|
-
"45.155.205.233"
|
|
1467
|
-
],
|
|
1468
|
-
malicious_hashes: {
|
|
1469
|
-
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855": "clawhavoc-empty-payload",
|
|
1470
|
-
"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2": "clawhavoc-exfiltrator"
|
|
1471
|
-
},
|
|
1472
|
-
malicious_domains: [
|
|
1473
|
-
"webhook.site",
|
|
1474
|
-
"requestbin.com",
|
|
1475
|
-
"pipedream.com",
|
|
1476
|
-
"pipedream.net",
|
|
1477
|
-
"hookbin.com",
|
|
1478
|
-
"beeceptor.com",
|
|
1479
|
-
"ngrok.io",
|
|
1480
|
-
"ngrok-free.app",
|
|
1481
|
-
"serveo.net",
|
|
1482
|
-
"localtunnel.me",
|
|
1483
|
-
"bore.pub",
|
|
1484
|
-
"interact.sh",
|
|
1485
|
-
"oast.fun",
|
|
1486
|
-
"oastify.com",
|
|
1487
|
-
"dnslog.cn",
|
|
1488
|
-
"ceye.io",
|
|
1489
|
-
"burpcollaborator.net",
|
|
1490
|
-
"pastebin.com",
|
|
1491
|
-
"paste.ee",
|
|
1492
|
-
"hastebin.com",
|
|
1493
|
-
"ghostbin.com",
|
|
1494
|
-
"evil.com",
|
|
1495
|
-
"malware.com",
|
|
1496
|
-
"exploit.in"
|
|
1497
|
-
],
|
|
1498
|
-
typosquat: {
|
|
1499
|
-
known_patterns: [
|
|
1500
|
-
"clawhub1",
|
|
1501
|
-
"cllawhub",
|
|
1502
|
-
"clawhab",
|
|
1503
|
-
"moltbot",
|
|
1504
|
-
"claw-hub",
|
|
1505
|
-
"clawhub-pro"
|
|
1506
|
-
],
|
|
1507
|
-
protected_names: [
|
|
1508
|
-
"clawhub",
|
|
1509
|
-
"secureclaw",
|
|
1510
|
-
"openclaw",
|
|
1511
|
-
"clawbot",
|
|
1512
|
-
"claude",
|
|
1513
|
-
"anthropic",
|
|
1514
|
-
"skill-checker"
|
|
1515
|
-
]
|
|
1516
|
-
},
|
|
1517
|
-
malicious_publishers: [
|
|
1518
|
-
"clawhavoc",
|
|
1519
|
-
"phantom-tracker",
|
|
1520
|
-
"solana-wallet-drainer"
|
|
1521
|
-
]
|
|
1522
|
-
};
|
|
1523
|
-
|
|
1524
|
-
// src/ioc/index.ts
|
|
1525
|
-
var cachedIOC = null;
|
|
1526
|
-
function loadIOC() {
|
|
1527
|
-
if (cachedIOC) return cachedIOC;
|
|
1528
|
-
const ioc = structuredClone(DEFAULT_IOC);
|
|
1529
|
-
const overridePath = join2(
|
|
1530
|
-
homedir(),
|
|
1531
|
-
".config",
|
|
1532
|
-
"skill-checker",
|
|
1533
|
-
"ioc-override.json"
|
|
1534
|
-
);
|
|
1535
|
-
if (existsSync2(overridePath)) {
|
|
1536
|
-
try {
|
|
1537
|
-
const raw = readFileSync2(overridePath, "utf-8");
|
|
1538
|
-
const ext = JSON.parse(raw);
|
|
1539
|
-
mergeIOC(ioc, ext);
|
|
1540
|
-
} catch {
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
cachedIOC = ioc;
|
|
1544
|
-
return ioc;
|
|
1545
|
-
}
|
|
1546
|
-
function mergeIOC(base, ext) {
|
|
1547
|
-
if (ext.c2_ips) {
|
|
1548
|
-
base.c2_ips = dedupe([...base.c2_ips, ...ext.c2_ips]);
|
|
1549
|
-
}
|
|
1550
|
-
if (ext.malicious_hashes) {
|
|
1551
|
-
Object.assign(base.malicious_hashes, ext.malicious_hashes);
|
|
1552
|
-
}
|
|
1553
|
-
if (ext.malicious_domains) {
|
|
1554
|
-
base.malicious_domains = dedupe([
|
|
1555
|
-
...base.malicious_domains,
|
|
1556
|
-
...ext.malicious_domains
|
|
1557
|
-
]);
|
|
1558
|
-
}
|
|
1559
|
-
if (ext.typosquat) {
|
|
1560
|
-
if (ext.typosquat.known_patterns) {
|
|
1561
|
-
base.typosquat.known_patterns = dedupe([
|
|
1562
|
-
...base.typosquat.known_patterns,
|
|
1563
|
-
...ext.typosquat.known_patterns
|
|
1564
|
-
]);
|
|
1565
|
-
}
|
|
1566
|
-
if (ext.typosquat.protected_names) {
|
|
1567
|
-
base.typosquat.protected_names = dedupe([
|
|
1568
|
-
...base.typosquat.protected_names,
|
|
1569
|
-
...ext.typosquat.protected_names
|
|
1570
|
-
]);
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
if (ext.malicious_publishers) {
|
|
1574
|
-
base.malicious_publishers = dedupe([
|
|
1575
|
-
...base.malicious_publishers,
|
|
1576
|
-
...ext.malicious_publishers
|
|
1577
|
-
]);
|
|
1578
|
-
}
|
|
1579
|
-
if (ext.version) base.version = ext.version;
|
|
1580
|
-
if (ext.updated) base.updated = ext.updated;
|
|
1581
|
-
}
|
|
1582
|
-
function dedupe(arr) {
|
|
1583
|
-
return [...new Set(arr)];
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
1763
|
// src/ioc/matcher.ts
|
|
1587
1764
|
import { createHash } from "crypto";
|
|
1588
|
-
import {
|
|
1765
|
+
import { openSync, readSync, closeSync, statSync as statSync2 } from "fs";
|
|
1589
1766
|
import { join as join3 } from "path";
|
|
1590
1767
|
|
|
1591
1768
|
// src/utils/levenshtein.ts
|
|
@@ -1618,6 +1795,7 @@ function levenshtein(a, b) {
|
|
|
1618
1795
|
}
|
|
1619
1796
|
|
|
1620
1797
|
// src/ioc/matcher.ts
|
|
1798
|
+
var EMPTY_FILE_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
1621
1799
|
var IPV4_PATTERN = /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g;
|
|
1622
1800
|
function isPrivateIP(ip) {
|
|
1623
1801
|
const parts = ip.split(".").map(Number);
|
|
@@ -1630,6 +1808,24 @@ function isPrivateIP(ip) {
|
|
|
1630
1808
|
if (parts[0] === 169 && parts[1] === 254) return true;
|
|
1631
1809
|
return false;
|
|
1632
1810
|
}
|
|
1811
|
+
var HASH_CHUNK_SIZE = 64 * 1024;
|
|
1812
|
+
function computeFileHash(filePath) {
|
|
1813
|
+
const hash = createHash("sha256");
|
|
1814
|
+
const fd = openSync(filePath, "r");
|
|
1815
|
+
try {
|
|
1816
|
+
const buf = Buffer.alloc(HASH_CHUNK_SIZE);
|
|
1817
|
+
let bytesRead;
|
|
1818
|
+
do {
|
|
1819
|
+
bytesRead = readSync(fd, buf, 0, HASH_CHUNK_SIZE, null);
|
|
1820
|
+
if (bytesRead > 0) {
|
|
1821
|
+
hash.update(buf.subarray(0, bytesRead));
|
|
1822
|
+
}
|
|
1823
|
+
} while (bytesRead > 0);
|
|
1824
|
+
} finally {
|
|
1825
|
+
closeSync(fd);
|
|
1826
|
+
}
|
|
1827
|
+
return hash.digest("hex");
|
|
1828
|
+
}
|
|
1633
1829
|
function matchMaliciousHashes(skill, ioc) {
|
|
1634
1830
|
const matches = [];
|
|
1635
1831
|
const hashKeys = Object.keys(ioc.malicious_hashes);
|
|
@@ -1637,8 +1833,10 @@ function matchMaliciousHashes(skill, ioc) {
|
|
|
1637
1833
|
for (const file of skill.files) {
|
|
1638
1834
|
const filePath = join3(skill.dirPath, file.path);
|
|
1639
1835
|
try {
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1836
|
+
const stat = statSync2(filePath);
|
|
1837
|
+
if (stat.size === 0) continue;
|
|
1838
|
+
const hash = computeFileHash(filePath);
|
|
1839
|
+
if (hash === EMPTY_FILE_HASH) continue;
|
|
1642
1840
|
if (ioc.malicious_hashes[hash]) {
|
|
1643
1841
|
matches.push({
|
|
1644
1842
|
file: file.path,
|
|
@@ -1788,49 +1986,6 @@ function runAllChecks(skill) {
|
|
|
1788
1986
|
return results;
|
|
1789
1987
|
}
|
|
1790
1988
|
|
|
1791
|
-
// src/types.ts
|
|
1792
|
-
var SEVERITY_SCORES = {
|
|
1793
|
-
CRITICAL: 25,
|
|
1794
|
-
HIGH: 10,
|
|
1795
|
-
MEDIUM: 3,
|
|
1796
|
-
LOW: 1
|
|
1797
|
-
};
|
|
1798
|
-
function computeGrade(score) {
|
|
1799
|
-
if (score >= 90) return "A";
|
|
1800
|
-
if (score >= 75) return "B";
|
|
1801
|
-
if (score >= 60) return "C";
|
|
1802
|
-
if (score >= 40) return "D";
|
|
1803
|
-
return "F";
|
|
1804
|
-
}
|
|
1805
|
-
var DEFAULT_CONFIG = {
|
|
1806
|
-
policy: "balanced",
|
|
1807
|
-
overrides: {},
|
|
1808
|
-
ignore: []
|
|
1809
|
-
};
|
|
1810
|
-
function getHookAction(policy, severity) {
|
|
1811
|
-
const matrix = {
|
|
1812
|
-
strict: {
|
|
1813
|
-
CRITICAL: "deny",
|
|
1814
|
-
HIGH: "deny",
|
|
1815
|
-
MEDIUM: "ask",
|
|
1816
|
-
LOW: "report"
|
|
1817
|
-
},
|
|
1818
|
-
balanced: {
|
|
1819
|
-
CRITICAL: "deny",
|
|
1820
|
-
HIGH: "ask",
|
|
1821
|
-
MEDIUM: "report",
|
|
1822
|
-
LOW: "report"
|
|
1823
|
-
},
|
|
1824
|
-
permissive: {
|
|
1825
|
-
CRITICAL: "ask",
|
|
1826
|
-
HIGH: "report",
|
|
1827
|
-
MEDIUM: "report",
|
|
1828
|
-
LOW: "report"
|
|
1829
|
-
}
|
|
1830
|
-
};
|
|
1831
|
-
return matrix[policy][severity];
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
1989
|
// src/scanner.ts
|
|
1835
1990
|
function scanSkillDirectory(dirPath, config = DEFAULT_CONFIG) {
|
|
1836
1991
|
const skill = parseSkill(dirPath);
|
|
@@ -1845,6 +2000,7 @@ function buildReport(skill, config) {
|
|
|
1845
2000
|
return r;
|
|
1846
2001
|
});
|
|
1847
2002
|
results = results.filter((r) => !config.ignore.includes(r.id));
|
|
2003
|
+
results = deduplicateResults(results);
|
|
1848
2004
|
const score = calculateScore(results);
|
|
1849
2005
|
const grade = computeGrade(score);
|
|
1850
2006
|
const summary = {
|
|
@@ -1864,6 +2020,40 @@ function buildReport(skill, config) {
|
|
|
1864
2020
|
summary
|
|
1865
2021
|
};
|
|
1866
2022
|
}
|
|
2023
|
+
function deduplicateResults(results) {
|
|
2024
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2025
|
+
const severityOrder = {
|
|
2026
|
+
CRITICAL: 4,
|
|
2027
|
+
HIGH: 3,
|
|
2028
|
+
MEDIUM: 2,
|
|
2029
|
+
LOW: 1
|
|
2030
|
+
};
|
|
2031
|
+
for (const r of results) {
|
|
2032
|
+
const sourceFile = extractSourceFile(r.message);
|
|
2033
|
+
const key = `${r.id}::${sourceFile}`;
|
|
2034
|
+
const group = groups.get(key);
|
|
2035
|
+
if (group) {
|
|
2036
|
+
group.push(r);
|
|
2037
|
+
} else {
|
|
2038
|
+
groups.set(key, [r]);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
const deduped = [];
|
|
2042
|
+
for (const group of groups.values()) {
|
|
2043
|
+
group.sort((a, b) => severityOrder[b.severity] - severityOrder[a.severity]);
|
|
2044
|
+
const best = { ...group[0] };
|
|
2045
|
+
if (group.length > 1) {
|
|
2046
|
+
best.occurrences = group.length;
|
|
2047
|
+
best.message += ` (${group.length} occurrences in this file)`;
|
|
2048
|
+
}
|
|
2049
|
+
deduped.push(best);
|
|
2050
|
+
}
|
|
2051
|
+
return deduped;
|
|
2052
|
+
}
|
|
2053
|
+
function extractSourceFile(message) {
|
|
2054
|
+
const m = message.match(/^(?:At\s+)?([^:]+):\d+:/);
|
|
2055
|
+
return m?.[1] ?? "unknown";
|
|
2056
|
+
}
|
|
1867
2057
|
function calculateScore(results) {
|
|
1868
2058
|
let score = 100;
|
|
1869
2059
|
for (const r of results) {
|
|
@@ -2050,7 +2240,7 @@ function buildReportSummary(report) {
|
|
|
2050
2240
|
}
|
|
2051
2241
|
|
|
2052
2242
|
// src/config.ts
|
|
2053
|
-
import { readFileSync as
|
|
2243
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
2054
2244
|
import { join as join4, resolve as resolve2 } from "path";
|
|
2055
2245
|
import { parse as parseYaml2 } from "yaml";
|
|
2056
2246
|
var CONFIG_FILENAMES = [
|
|
@@ -2092,7 +2282,7 @@ function loadConfig(startDir, configPath) {
|
|
|
2092
2282
|
}
|
|
2093
2283
|
function parseConfigFile(path) {
|
|
2094
2284
|
try {
|
|
2095
|
-
const raw =
|
|
2285
|
+
const raw = readFileSync3(path, "utf-8");
|
|
2096
2286
|
const parsed = parseYaml2(raw);
|
|
2097
2287
|
if (!parsed || typeof parsed !== "object") {
|
|
2098
2288
|
return { ...DEFAULT_CONFIG };
|
|
@@ -2132,10 +2322,12 @@ function normalizeSeverity(value) {
|
|
|
2132
2322
|
}
|
|
2133
2323
|
|
|
2134
2324
|
// src/cli.ts
|
|
2325
|
+
var require2 = createRequire(import.meta.url);
|
|
2326
|
+
var pkg = require2("../package.json");
|
|
2135
2327
|
var program = new Command();
|
|
2136
2328
|
program.name("skill-checker").description(
|
|
2137
2329
|
"Security checker for Claude Code skills - detect injection, malicious code, and supply chain risks"
|
|
2138
|
-
).version(
|
|
2330
|
+
).version(pkg.version);
|
|
2139
2331
|
program.command("scan").description("Scan a skill directory for security issues").argument("<path>", "Path to the skill directory").option("-f, --format <format>", "Output format: terminal, json, hook", "terminal").option("-p, --policy <policy>", "Policy: strict, balanced, permissive").option("-c, --config <path>", "Path to config file").action(
|
|
2140
2332
|
(path, opts) => {
|
|
2141
2333
|
const config = loadConfig(path, opts.config);
|