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/index.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
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/parser.ts
|
|
2
9
|
import { readFileSync, readdirSync, statSync, existsSync } from "fs";
|
|
3
10
|
import { join, extname, basename, resolve } from "path";
|
|
@@ -47,7 +54,8 @@ function parseSkill(dirPath) {
|
|
|
47
54
|
const hasSkillMd = existsSync(skillMdPath);
|
|
48
55
|
const raw = hasSkillMd ? readFileSync(skillMdPath, "utf-8") : "";
|
|
49
56
|
const { frontmatter, frontmatterValid, body, bodyStartLine } = parseFrontmatter(raw);
|
|
50
|
-
const
|
|
57
|
+
const warnings = [];
|
|
58
|
+
const files = enumerateFiles(absDir, warnings);
|
|
51
59
|
return {
|
|
52
60
|
dirPath: absDir,
|
|
53
61
|
raw,
|
|
@@ -56,7 +64,8 @@ function parseSkill(dirPath) {
|
|
|
56
64
|
body,
|
|
57
65
|
bodyLines: body.split("\n"),
|
|
58
66
|
bodyStartLine,
|
|
59
|
-
files
|
|
67
|
+
files,
|
|
68
|
+
warnings
|
|
60
69
|
};
|
|
61
70
|
}
|
|
62
71
|
function parseSkillContent(content, dirPath = ".") {
|
|
@@ -69,7 +78,8 @@ function parseSkillContent(content, dirPath = ".") {
|
|
|
69
78
|
body,
|
|
70
79
|
bodyLines: body.split("\n"),
|
|
71
80
|
bodyStartLine,
|
|
72
|
-
files: []
|
|
81
|
+
files: [],
|
|
82
|
+
warnings: []
|
|
73
83
|
};
|
|
74
84
|
}
|
|
75
85
|
function parseFrontmatter(raw) {
|
|
@@ -102,11 +112,20 @@ function parseFrontmatter(raw) {
|
|
|
102
112
|
};
|
|
103
113
|
}
|
|
104
114
|
}
|
|
105
|
-
|
|
115
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git"]);
|
|
116
|
+
var WARN_SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules"]);
|
|
117
|
+
var MAX_DEPTH = 15;
|
|
118
|
+
var FULL_READ_LIMIT = 5e6;
|
|
119
|
+
var PARTIAL_READ_LIMIT = 512 * 1024;
|
|
120
|
+
function enumerateFiles(dirPath, warnings) {
|
|
106
121
|
const files = [];
|
|
107
122
|
if (!existsSync(dirPath)) return files;
|
|
108
123
|
function walk(currentDir, depth) {
|
|
109
|
-
if (depth >
|
|
124
|
+
if (depth > MAX_DEPTH) {
|
|
125
|
+
const rel = currentDir.slice(dirPath.length + 1) || currentDir;
|
|
126
|
+
warnings.push(`Depth limit (${MAX_DEPTH}) exceeded at: ${rel}. Contents not scanned.`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
110
129
|
let entries;
|
|
111
130
|
try {
|
|
112
131
|
entries = readdirSync(currentDir, { withFileTypes: true });
|
|
@@ -116,7 +135,12 @@ function enumerateFiles(dirPath, maxDepth = 5) {
|
|
|
116
135
|
for (const entry of entries) {
|
|
117
136
|
const fullPath = join(currentDir, entry.name);
|
|
118
137
|
if (entry.isDirectory()) {
|
|
119
|
-
if (
|
|
138
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
139
|
+
if (WARN_SKIP_DIRS.has(entry.name)) {
|
|
140
|
+
const rel = fullPath.slice(dirPath.length + 1);
|
|
141
|
+
warnings.push(`Skipped directory: ${rel}. May contain unscanned files.`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
120
144
|
walk(fullPath, depth + 1);
|
|
121
145
|
continue;
|
|
122
146
|
}
|
|
@@ -130,10 +154,25 @@ function enumerateFiles(dirPath, maxDepth = 5) {
|
|
|
130
154
|
const isBinary = BINARY_EXTENSIONS.has(ext);
|
|
131
155
|
const relativePath = fullPath.slice(dirPath.length + 1);
|
|
132
156
|
let content;
|
|
133
|
-
if (!isBinary
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
157
|
+
if (!isBinary) {
|
|
158
|
+
if (stats.size <= FULL_READ_LIMIT) {
|
|
159
|
+
try {
|
|
160
|
+
content = readFileSync(fullPath, "utf-8");
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
} else if (stats.size <= 5e7) {
|
|
164
|
+
try {
|
|
165
|
+
const fd = __require("fs").openSync(fullPath, "r");
|
|
166
|
+
const buf = Buffer.alloc(PARTIAL_READ_LIMIT);
|
|
167
|
+
const bytesRead = __require("fs").readSync(fd, buf, 0, PARTIAL_READ_LIMIT, 0);
|
|
168
|
+
__require("fs").closeSync(fd);
|
|
169
|
+
content = buf.slice(0, bytesRead).toString("utf-8");
|
|
170
|
+
warnings.push(`Large file partially scanned (first ${PARTIAL_READ_LIMIT} bytes): ${relativePath} (${stats.size} bytes total)`);
|
|
171
|
+
} catch {
|
|
172
|
+
warnings.push(`Large file could not be read: ${relativePath} (${stats.size} bytes)`);
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
warnings.push(`File too large to scan: ${relativePath} (${stats.size} bytes). Content not checked.`);
|
|
137
176
|
}
|
|
138
177
|
}
|
|
139
178
|
files.push({
|
|
@@ -257,6 +296,15 @@ var structuralChecks = {
|
|
|
257
296
|
});
|
|
258
297
|
}
|
|
259
298
|
}
|
|
299
|
+
for (const warning of skill.warnings) {
|
|
300
|
+
results.push({
|
|
301
|
+
id: "STRUCT-008",
|
|
302
|
+
category: "STRUCT",
|
|
303
|
+
severity: "MEDIUM",
|
|
304
|
+
title: "Scan coverage warning",
|
|
305
|
+
message: warning
|
|
306
|
+
});
|
|
307
|
+
}
|
|
260
308
|
return results;
|
|
261
309
|
}
|
|
262
310
|
};
|
|
@@ -307,6 +355,14 @@ function isInDocumentationContext(lines, lineIndex) {
|
|
|
307
355
|
}
|
|
308
356
|
return false;
|
|
309
357
|
}
|
|
358
|
+
function isLicenseFile(filePath) {
|
|
359
|
+
const name = filePath.split("/").pop()?.toUpperCase() ?? "";
|
|
360
|
+
const base = name.replace(/\.[^.]+$/, "");
|
|
361
|
+
return /^(LICENSE|LICENCE|COPYING|NOTICE|AUTHORS|PATENTS)$/.test(base);
|
|
362
|
+
}
|
|
363
|
+
function isLocalhostURL(url) {
|
|
364
|
+
return /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])/i.test(url);
|
|
365
|
+
}
|
|
310
366
|
function parseURLPath(url) {
|
|
311
367
|
try {
|
|
312
368
|
const u = new URL(url);
|
|
@@ -901,6 +957,66 @@ function dedup(results) {
|
|
|
901
957
|
});
|
|
902
958
|
}
|
|
903
959
|
|
|
960
|
+
// src/types.ts
|
|
961
|
+
var SEVERITY_SCORES = {
|
|
962
|
+
CRITICAL: 25,
|
|
963
|
+
HIGH: 10,
|
|
964
|
+
MEDIUM: 3,
|
|
965
|
+
LOW: 1
|
|
966
|
+
};
|
|
967
|
+
function computeGrade(score) {
|
|
968
|
+
if (score >= 90) return "A";
|
|
969
|
+
if (score >= 75) return "B";
|
|
970
|
+
if (score >= 60) return "C";
|
|
971
|
+
if (score >= 40) return "D";
|
|
972
|
+
return "F";
|
|
973
|
+
}
|
|
974
|
+
var REDUCE_MAP = {
|
|
975
|
+
CRITICAL: "HIGH",
|
|
976
|
+
HIGH: "MEDIUM",
|
|
977
|
+
MEDIUM: "LOW",
|
|
978
|
+
LOW: "LOW"
|
|
979
|
+
};
|
|
980
|
+
function reduceSeverity(original, reason) {
|
|
981
|
+
let reduced = REDUCE_MAP[original];
|
|
982
|
+
if (original === "CRITICAL" && reduced === "LOW") {
|
|
983
|
+
reduced = "MEDIUM";
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
severity: reduced,
|
|
987
|
+
reducedFrom: original,
|
|
988
|
+
annotation: `[reduced: ${reason}]`
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
var DEFAULT_CONFIG = {
|
|
992
|
+
policy: "balanced",
|
|
993
|
+
overrides: {},
|
|
994
|
+
ignore: []
|
|
995
|
+
};
|
|
996
|
+
function getHookAction(policy, severity) {
|
|
997
|
+
const matrix = {
|
|
998
|
+
strict: {
|
|
999
|
+
CRITICAL: "deny",
|
|
1000
|
+
HIGH: "deny",
|
|
1001
|
+
MEDIUM: "ask",
|
|
1002
|
+
LOW: "report"
|
|
1003
|
+
},
|
|
1004
|
+
balanced: {
|
|
1005
|
+
CRITICAL: "deny",
|
|
1006
|
+
HIGH: "ask",
|
|
1007
|
+
MEDIUM: "report",
|
|
1008
|
+
LOW: "report"
|
|
1009
|
+
},
|
|
1010
|
+
permissive: {
|
|
1011
|
+
CRITICAL: "ask",
|
|
1012
|
+
HIGH: "report",
|
|
1013
|
+
MEDIUM: "report",
|
|
1014
|
+
LOW: "report"
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
return matrix[policy][severity];
|
|
1018
|
+
}
|
|
1019
|
+
|
|
904
1020
|
// src/checks/code-safety.ts
|
|
905
1021
|
var EVAL_PATTERNS = [
|
|
906
1022
|
/\beval\s*\(/,
|
|
@@ -984,6 +1100,7 @@ var codeSafetyChecks = {
|
|
|
984
1100
|
const line = lines[i];
|
|
985
1101
|
const lineNum = i + 1;
|
|
986
1102
|
const loc = `${source}:${lineNum}`;
|
|
1103
|
+
const cbCtx = { lines, index: i };
|
|
987
1104
|
checkPatterns(results, line, EVAL_PATTERNS, {
|
|
988
1105
|
id: "CODE-001",
|
|
989
1106
|
severity: "CRITICAL",
|
|
@@ -1005,14 +1122,16 @@ var codeSafetyChecks = {
|
|
|
1005
1122
|
severity: "CRITICAL",
|
|
1006
1123
|
title: "Destructive file operation",
|
|
1007
1124
|
loc,
|
|
1008
|
-
lineNum
|
|
1125
|
+
lineNum,
|
|
1126
|
+
codeBlockCtx: cbCtx
|
|
1009
1127
|
});
|
|
1010
1128
|
checkPatterns(results, line, NETWORK_PATTERNS, {
|
|
1011
1129
|
id: "CODE-004",
|
|
1012
1130
|
severity: "HIGH",
|
|
1013
1131
|
title: "Hardcoded external URL/network request",
|
|
1014
1132
|
loc,
|
|
1015
|
-
lineNum
|
|
1133
|
+
lineNum,
|
|
1134
|
+
codeBlockCtx: cbCtx
|
|
1016
1135
|
});
|
|
1017
1136
|
checkPatterns(results, line, FILE_WRITE_PATTERNS, {
|
|
1018
1137
|
id: "CODE-005",
|
|
@@ -1026,7 +1145,8 @@ var codeSafetyChecks = {
|
|
|
1026
1145
|
severity: "MEDIUM",
|
|
1027
1146
|
title: "Environment variable access",
|
|
1028
1147
|
loc,
|
|
1029
|
-
lineNum
|
|
1148
|
+
lineNum,
|
|
1149
|
+
codeBlockCtx: cbCtx
|
|
1030
1150
|
});
|
|
1031
1151
|
checkPatterns(results, line, DYNAMIC_CODE_PATTERNS, {
|
|
1032
1152
|
id: "CODE-010",
|
|
@@ -1036,8 +1156,7 @@ var codeSafetyChecks = {
|
|
|
1036
1156
|
lineNum
|
|
1037
1157
|
});
|
|
1038
1158
|
{
|
|
1039
|
-
const
|
|
1040
|
-
const isDoc = isInDocumentationContext(srcLines, i);
|
|
1159
|
+
const isDoc = isInDocumentationContext(lines, i);
|
|
1041
1160
|
if (!isDoc) {
|
|
1042
1161
|
checkPatterns(results, line, PERMISSION_PATTERNS, {
|
|
1043
1162
|
id: "CODE-012",
|
|
@@ -1058,14 +1177,24 @@ var codeSafetyChecks = {
|
|
|
1058
1177
|
function checkPatterns(results, line, patterns, opts) {
|
|
1059
1178
|
for (const pattern of patterns) {
|
|
1060
1179
|
if (pattern.test(line)) {
|
|
1180
|
+
let severity = opts.severity;
|
|
1181
|
+
let reducedFrom;
|
|
1182
|
+
let msgSuffix = "";
|
|
1183
|
+
if (opts.codeBlockCtx && isInCodeBlock(opts.codeBlockCtx.lines, opts.codeBlockCtx.index)) {
|
|
1184
|
+
const r = reduceSeverity(severity, "in code block");
|
|
1185
|
+
severity = r.severity;
|
|
1186
|
+
reducedFrom = r.reducedFrom;
|
|
1187
|
+
msgSuffix = ` ${r.annotation}`;
|
|
1188
|
+
}
|
|
1061
1189
|
results.push({
|
|
1062
1190
|
id: opts.id,
|
|
1063
1191
|
category: "CODE",
|
|
1064
|
-
severity
|
|
1192
|
+
severity,
|
|
1065
1193
|
title: opts.title,
|
|
1066
|
-
message: `At ${opts.loc}: ${line.trim().slice(0, 120)}`,
|
|
1194
|
+
message: `At ${opts.loc}: ${line.trim().slice(0, 120)}${msgSuffix}`,
|
|
1067
1195
|
line: opts.lineNum,
|
|
1068
|
-
snippet: line.trim().slice(0, 120)
|
|
1196
|
+
snippet: line.trim().slice(0, 120),
|
|
1197
|
+
reducedFrom
|
|
1069
1198
|
});
|
|
1070
1199
|
return;
|
|
1071
1200
|
}
|
|
@@ -1148,22 +1277,147 @@ function scanObfuscation(results, text, source) {
|
|
|
1148
1277
|
}
|
|
1149
1278
|
}
|
|
1150
1279
|
|
|
1280
|
+
// src/ioc/index.ts
|
|
1281
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
1282
|
+
import { join as join2 } from "path";
|
|
1283
|
+
import { homedir } from "os";
|
|
1284
|
+
|
|
1285
|
+
// src/ioc/indicators.ts
|
|
1286
|
+
var DEFAULT_IOC = {
|
|
1287
|
+
version: "2026.03.06",
|
|
1288
|
+
updated: "2026-03-06",
|
|
1289
|
+
c2_ips: [
|
|
1290
|
+
"91.92.242.30",
|
|
1291
|
+
"91.92.242.39",
|
|
1292
|
+
"185.220.101.1",
|
|
1293
|
+
"185.220.101.2",
|
|
1294
|
+
"45.155.205.233"
|
|
1295
|
+
],
|
|
1296
|
+
malicious_hashes: {
|
|
1297
|
+
// NOTE: Never add the SHA-256 of an empty file (e3b0c44298fc...b855)
|
|
1298
|
+
// as it causes false positives on any empty file.
|
|
1299
|
+
"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2": "clawhavoc-exfiltrator"
|
|
1300
|
+
},
|
|
1301
|
+
malicious_domains: [
|
|
1302
|
+
"webhook.site",
|
|
1303
|
+
"requestbin.com",
|
|
1304
|
+
"pipedream.com",
|
|
1305
|
+
"pipedream.net",
|
|
1306
|
+
"hookbin.com",
|
|
1307
|
+
"beeceptor.com",
|
|
1308
|
+
"ngrok.io",
|
|
1309
|
+
"ngrok-free.app",
|
|
1310
|
+
"serveo.net",
|
|
1311
|
+
"localtunnel.me",
|
|
1312
|
+
"bore.pub",
|
|
1313
|
+
"interact.sh",
|
|
1314
|
+
"oast.fun",
|
|
1315
|
+
"oastify.com",
|
|
1316
|
+
"dnslog.cn",
|
|
1317
|
+
"ceye.io",
|
|
1318
|
+
"burpcollaborator.net",
|
|
1319
|
+
"pastebin.com",
|
|
1320
|
+
"paste.ee",
|
|
1321
|
+
"hastebin.com",
|
|
1322
|
+
"ghostbin.com",
|
|
1323
|
+
"evil.com",
|
|
1324
|
+
"malware.com",
|
|
1325
|
+
"exploit.in"
|
|
1326
|
+
],
|
|
1327
|
+
typosquat: {
|
|
1328
|
+
known_patterns: [
|
|
1329
|
+
"clawhub1",
|
|
1330
|
+
"cllawhub",
|
|
1331
|
+
"clawhab",
|
|
1332
|
+
"moltbot",
|
|
1333
|
+
"claw-hub",
|
|
1334
|
+
"clawhub-pro"
|
|
1335
|
+
],
|
|
1336
|
+
protected_names: [
|
|
1337
|
+
"clawhub",
|
|
1338
|
+
"secureclaw",
|
|
1339
|
+
"openclaw",
|
|
1340
|
+
"clawbot",
|
|
1341
|
+
"claude",
|
|
1342
|
+
"anthropic",
|
|
1343
|
+
"skill-checker"
|
|
1344
|
+
]
|
|
1345
|
+
},
|
|
1346
|
+
malicious_publishers: [
|
|
1347
|
+
"clawhavoc",
|
|
1348
|
+
"phantom-tracker",
|
|
1349
|
+
"solana-wallet-drainer"
|
|
1350
|
+
]
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
// src/ioc/index.ts
|
|
1354
|
+
var cachedIOC = null;
|
|
1355
|
+
function loadIOC() {
|
|
1356
|
+
if (cachedIOC) return cachedIOC;
|
|
1357
|
+
const ioc = structuredClone(DEFAULT_IOC);
|
|
1358
|
+
const overridePath = join2(
|
|
1359
|
+
homedir(),
|
|
1360
|
+
".config",
|
|
1361
|
+
"skill-checker",
|
|
1362
|
+
"ioc-override.json"
|
|
1363
|
+
);
|
|
1364
|
+
if (existsSync2(overridePath)) {
|
|
1365
|
+
try {
|
|
1366
|
+
const raw = readFileSync2(overridePath, "utf-8");
|
|
1367
|
+
const ext = JSON.parse(raw);
|
|
1368
|
+
mergeIOC(ioc, ext);
|
|
1369
|
+
} catch {
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
cachedIOC = ioc;
|
|
1373
|
+
return ioc;
|
|
1374
|
+
}
|
|
1375
|
+
function resetIOCCache() {
|
|
1376
|
+
cachedIOC = null;
|
|
1377
|
+
}
|
|
1378
|
+
function mergeIOC(base, ext) {
|
|
1379
|
+
if (ext.c2_ips) {
|
|
1380
|
+
base.c2_ips = dedupe([...base.c2_ips, ...ext.c2_ips]);
|
|
1381
|
+
}
|
|
1382
|
+
if (ext.malicious_hashes) {
|
|
1383
|
+
Object.assign(base.malicious_hashes, ext.malicious_hashes);
|
|
1384
|
+
}
|
|
1385
|
+
if (ext.malicious_domains) {
|
|
1386
|
+
base.malicious_domains = dedupe([
|
|
1387
|
+
...base.malicious_domains,
|
|
1388
|
+
...ext.malicious_domains
|
|
1389
|
+
]);
|
|
1390
|
+
}
|
|
1391
|
+
if (ext.typosquat) {
|
|
1392
|
+
if (ext.typosquat.known_patterns) {
|
|
1393
|
+
base.typosquat.known_patterns = dedupe([
|
|
1394
|
+
...base.typosquat.known_patterns,
|
|
1395
|
+
...ext.typosquat.known_patterns
|
|
1396
|
+
]);
|
|
1397
|
+
}
|
|
1398
|
+
if (ext.typosquat.protected_names) {
|
|
1399
|
+
base.typosquat.protected_names = dedupe([
|
|
1400
|
+
...base.typosquat.protected_names,
|
|
1401
|
+
...ext.typosquat.protected_names
|
|
1402
|
+
]);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (ext.malicious_publishers) {
|
|
1406
|
+
base.malicious_publishers = dedupe([
|
|
1407
|
+
...base.malicious_publishers,
|
|
1408
|
+
...ext.malicious_publishers
|
|
1409
|
+
]);
|
|
1410
|
+
}
|
|
1411
|
+
if (ext.version) base.version = ext.version;
|
|
1412
|
+
if (ext.updated) base.updated = ext.updated;
|
|
1413
|
+
}
|
|
1414
|
+
function dedupe(arr) {
|
|
1415
|
+
return [...new Set(arr)];
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1151
1418
|
// src/checks/supply-chain.ts
|
|
1152
|
-
var
|
|
1153
|
-
"
|
|
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"
|
|
1419
|
+
var FALLBACK_SUSPICIOUS_DOMAINS = [
|
|
1420
|
+
"darkweb.onion"
|
|
1167
1421
|
];
|
|
1168
1422
|
var MCP_SERVER_PATTERN = /\bmcp[-_]?server\b/i;
|
|
1169
1423
|
var NPX_Y_PATTERN = /\bnpx\s+-y\s+/;
|
|
@@ -1172,23 +1426,47 @@ var PIP_INSTALL_PATTERN = /\bpip3?\s+install\b/;
|
|
|
1172
1426
|
var GIT_CLONE_PATTERN = /\bgit\s+clone\b/;
|
|
1173
1427
|
var URL_PATTERN = /https?:\/\/[^\s"'`<>)\]]+/g;
|
|
1174
1428
|
var IP_URL_PATTERN = /https?:\/\/(?:\d{1,3}\.){3}\d{1,3}/;
|
|
1429
|
+
function extractHostname(url) {
|
|
1430
|
+
const afterProto = url.replace(/^https?:\/\//, "");
|
|
1431
|
+
const hostPort = afterProto.split("/")[0].split("?")[0].split("#")[0];
|
|
1432
|
+
const host = hostPort.split(":")[0];
|
|
1433
|
+
return host.toLowerCase();
|
|
1434
|
+
}
|
|
1435
|
+
function hostnameMatchesDomain(hostname, domain) {
|
|
1436
|
+
const d = domain.toLowerCase();
|
|
1437
|
+
return hostname === d || hostname.endsWith("." + d);
|
|
1438
|
+
}
|
|
1175
1439
|
var supplyChainChecks = {
|
|
1176
1440
|
name: "Supply Chain",
|
|
1177
1441
|
category: "SUPPLY",
|
|
1178
1442
|
run(skill) {
|
|
1179
1443
|
const results = [];
|
|
1180
1444
|
const allText = getAllText(skill);
|
|
1445
|
+
const ioc = loadIOC();
|
|
1446
|
+
const suspiciousDomains = ioc.malicious_domains.length > 0 ? ioc.malicious_domains : FALLBACK_SUSPICIOUS_DOMAINS;
|
|
1181
1447
|
for (let i = 0; i < allText.length; i++) {
|
|
1182
1448
|
const { line, lineNum, source } = allText[i];
|
|
1183
1449
|
if (MCP_SERVER_PATTERN.test(line)) {
|
|
1450
|
+
let severity = "HIGH";
|
|
1451
|
+
let reducedFrom;
|
|
1452
|
+
let msgSuffix = "";
|
|
1453
|
+
const srcLines = getLinesForSource(skill, source);
|
|
1454
|
+
const localIdx = getLocalIndex(source, lineNum, skill.bodyStartLine);
|
|
1455
|
+
if (localIdx >= 0 && isInCodeBlock(srcLines, localIdx)) {
|
|
1456
|
+
const r = reduceSeverity(severity, "in code block");
|
|
1457
|
+
severity = r.severity;
|
|
1458
|
+
reducedFrom = r.reducedFrom;
|
|
1459
|
+
msgSuffix = ` ${r.annotation}`;
|
|
1460
|
+
}
|
|
1184
1461
|
results.push({
|
|
1185
1462
|
id: "SUPPLY-001",
|
|
1186
1463
|
category: "SUPPLY",
|
|
1187
|
-
severity
|
|
1464
|
+
severity,
|
|
1188
1465
|
title: "MCP server reference",
|
|
1189
|
-
message: `${source}:${lineNum}: References an MCP server. Verify it is from a trusted source
|
|
1466
|
+
message: `${source}:${lineNum}: References an MCP server. Verify it is from a trusted source.${msgSuffix}`,
|
|
1190
1467
|
line: lineNum,
|
|
1191
|
-
snippet: line.trim().slice(0, 120)
|
|
1468
|
+
snippet: line.trim().slice(0, 120),
|
|
1469
|
+
reducedFrom
|
|
1192
1470
|
});
|
|
1193
1471
|
}
|
|
1194
1472
|
if (NPX_Y_PATTERN.test(line)) {
|
|
@@ -1210,14 +1488,26 @@ var supplyChainChecks = {
|
|
|
1210
1488
|
globalIdx
|
|
1211
1489
|
);
|
|
1212
1490
|
if (!isDoc) {
|
|
1491
|
+
let severity = "HIGH";
|
|
1492
|
+
let reducedFrom;
|
|
1493
|
+
let msgSuffix = "";
|
|
1494
|
+
const srcLines = getLinesForSource(skill, source);
|
|
1495
|
+
const localIdx = getLocalIndex(source, lineNum, skill.bodyStartLine);
|
|
1496
|
+
if (localIdx >= 0 && isInCodeBlock(srcLines, localIdx)) {
|
|
1497
|
+
const r = reduceSeverity(severity, "in code block");
|
|
1498
|
+
severity = r.severity;
|
|
1499
|
+
reducedFrom = r.reducedFrom;
|
|
1500
|
+
msgSuffix = ` ${r.annotation}`;
|
|
1501
|
+
}
|
|
1213
1502
|
results.push({
|
|
1214
1503
|
id: "SUPPLY-003",
|
|
1215
1504
|
category: "SUPPLY",
|
|
1216
|
-
severity
|
|
1505
|
+
severity,
|
|
1217
1506
|
title: "Package installation command",
|
|
1218
|
-
message: `${source}:${lineNum}: Installs packages. Verify package names are legitimate
|
|
1507
|
+
message: `${source}:${lineNum}: Installs packages. Verify package names are legitimate.${msgSuffix}`,
|
|
1219
1508
|
line: lineNum,
|
|
1220
|
-
snippet: line.trim().slice(0, 120)
|
|
1509
|
+
snippet: line.trim().slice(0, 120),
|
|
1510
|
+
reducedFrom
|
|
1221
1511
|
});
|
|
1222
1512
|
}
|
|
1223
1513
|
}
|
|
@@ -1235,16 +1525,30 @@ var supplyChainChecks = {
|
|
|
1235
1525
|
const urls = line.match(URL_PATTERN) || [];
|
|
1236
1526
|
for (const url of urls) {
|
|
1237
1527
|
if (url.startsWith("http://")) {
|
|
1528
|
+
if (isLicenseFile(source)) continue;
|
|
1529
|
+
if (isLocalhostURL(url)) continue;
|
|
1238
1530
|
if (!isNamespaceOrSchemaURI(url, line)) {
|
|
1239
1531
|
const isNetworkCtx = isInNetworkRequestContext(line);
|
|
1532
|
+
let severity = isNetworkCtx ? "HIGH" : "MEDIUM";
|
|
1533
|
+
let reducedFrom;
|
|
1534
|
+
let msgSuffix = "";
|
|
1535
|
+
const srcLines = getLinesForSource(skill, source);
|
|
1536
|
+
const localIdx = getLocalIndex(source, lineNum, skill.bodyStartLine);
|
|
1537
|
+
if (localIdx >= 0 && isInCodeBlock(srcLines, localIdx)) {
|
|
1538
|
+
const r = reduceSeverity(severity, "in code block");
|
|
1539
|
+
severity = r.severity;
|
|
1540
|
+
reducedFrom = r.reducedFrom;
|
|
1541
|
+
msgSuffix = ` ${r.annotation}`;
|
|
1542
|
+
}
|
|
1240
1543
|
results.push({
|
|
1241
1544
|
id: "SUPPLY-004",
|
|
1242
1545
|
category: "SUPPLY",
|
|
1243
|
-
severity
|
|
1546
|
+
severity,
|
|
1244
1547
|
title: "Non-HTTPS URL",
|
|
1245
|
-
message: `${source}:${lineNum}: Uses insecure HTTP: ${url}`,
|
|
1548
|
+
message: `${source}:${lineNum}: Uses insecure HTTP: ${url}${msgSuffix}`,
|
|
1246
1549
|
line: lineNum,
|
|
1247
|
-
snippet: url
|
|
1550
|
+
snippet: url,
|
|
1551
|
+
reducedFrom
|
|
1248
1552
|
});
|
|
1249
1553
|
}
|
|
1250
1554
|
}
|
|
@@ -1261,8 +1565,9 @@ var supplyChainChecks = {
|
|
|
1261
1565
|
});
|
|
1262
1566
|
}
|
|
1263
1567
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1568
|
+
const hostname = extractHostname(url);
|
|
1569
|
+
for (const domain of suspiciousDomains) {
|
|
1570
|
+
if (hostnameMatchesDomain(hostname, domain)) {
|
|
1266
1571
|
results.push({
|
|
1267
1572
|
id: "SUPPLY-007",
|
|
1268
1573
|
category: "SUPPLY",
|
|
@@ -1280,6 +1585,15 @@ var supplyChainChecks = {
|
|
|
1280
1585
|
return results;
|
|
1281
1586
|
}
|
|
1282
1587
|
};
|
|
1588
|
+
function getLinesForSource(skill, source) {
|
|
1589
|
+
if (source === "SKILL.md") return skill.bodyLines;
|
|
1590
|
+
const file = skill.files.find((f) => f.path === source);
|
|
1591
|
+
return file?.content?.split("\n") ?? [];
|
|
1592
|
+
}
|
|
1593
|
+
function getLocalIndex(source, lineNum, bodyStartLine) {
|
|
1594
|
+
if (source === "SKILL.md") return lineNum - bodyStartLine;
|
|
1595
|
+
return lineNum - 1;
|
|
1596
|
+
}
|
|
1283
1597
|
function getAllText(skill) {
|
|
1284
1598
|
const result = [];
|
|
1285
1599
|
for (let i = 0; i < skill.bodyLines.length; i++) {
|
|
@@ -1459,146 +1773,9 @@ var resourceChecks = {
|
|
|
1459
1773
|
}
|
|
1460
1774
|
};
|
|
1461
1775
|
|
|
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
1776
|
// src/ioc/matcher.ts
|
|
1600
1777
|
import { createHash } from "crypto";
|
|
1601
|
-
import {
|
|
1778
|
+
import { openSync, readSync, closeSync, statSync as statSync2 } from "fs";
|
|
1602
1779
|
import { join as join3 } from "path";
|
|
1603
1780
|
|
|
1604
1781
|
// src/utils/levenshtein.ts
|
|
@@ -1631,6 +1808,7 @@ function levenshtein(a, b) {
|
|
|
1631
1808
|
}
|
|
1632
1809
|
|
|
1633
1810
|
// src/ioc/matcher.ts
|
|
1811
|
+
var EMPTY_FILE_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
1634
1812
|
var IPV4_PATTERN = /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g;
|
|
1635
1813
|
function isPrivateIP(ip) {
|
|
1636
1814
|
const parts = ip.split(".").map(Number);
|
|
@@ -1643,6 +1821,24 @@ function isPrivateIP(ip) {
|
|
|
1643
1821
|
if (parts[0] === 169 && parts[1] === 254) return true;
|
|
1644
1822
|
return false;
|
|
1645
1823
|
}
|
|
1824
|
+
var HASH_CHUNK_SIZE = 64 * 1024;
|
|
1825
|
+
function computeFileHash(filePath) {
|
|
1826
|
+
const hash = createHash("sha256");
|
|
1827
|
+
const fd = openSync(filePath, "r");
|
|
1828
|
+
try {
|
|
1829
|
+
const buf = Buffer.alloc(HASH_CHUNK_SIZE);
|
|
1830
|
+
let bytesRead;
|
|
1831
|
+
do {
|
|
1832
|
+
bytesRead = readSync(fd, buf, 0, HASH_CHUNK_SIZE, null);
|
|
1833
|
+
if (bytesRead > 0) {
|
|
1834
|
+
hash.update(buf.subarray(0, bytesRead));
|
|
1835
|
+
}
|
|
1836
|
+
} while (bytesRead > 0);
|
|
1837
|
+
} finally {
|
|
1838
|
+
closeSync(fd);
|
|
1839
|
+
}
|
|
1840
|
+
return hash.digest("hex");
|
|
1841
|
+
}
|
|
1646
1842
|
function matchMaliciousHashes(skill, ioc) {
|
|
1647
1843
|
const matches = [];
|
|
1648
1844
|
const hashKeys = Object.keys(ioc.malicious_hashes);
|
|
@@ -1650,8 +1846,10 @@ function matchMaliciousHashes(skill, ioc) {
|
|
|
1650
1846
|
for (const file of skill.files) {
|
|
1651
1847
|
const filePath = join3(skill.dirPath, file.path);
|
|
1652
1848
|
try {
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1849
|
+
const stat = statSync2(filePath);
|
|
1850
|
+
if (stat.size === 0) continue;
|
|
1851
|
+
const hash = computeFileHash(filePath);
|
|
1852
|
+
if (hash === EMPTY_FILE_HASH) continue;
|
|
1655
1853
|
if (ioc.malicious_hashes[hash]) {
|
|
1656
1854
|
matches.push({
|
|
1657
1855
|
file: file.path,
|
|
@@ -1801,49 +1999,6 @@ function runAllChecks(skill) {
|
|
|
1801
1999
|
return results;
|
|
1802
2000
|
}
|
|
1803
2001
|
|
|
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
2002
|
// src/scanner.ts
|
|
1848
2003
|
function scanSkillDirectory(dirPath, config = DEFAULT_CONFIG) {
|
|
1849
2004
|
const skill = parseSkill(dirPath);
|
|
@@ -1862,6 +2017,7 @@ function buildReport(skill, config) {
|
|
|
1862
2017
|
return r;
|
|
1863
2018
|
});
|
|
1864
2019
|
results = results.filter((r) => !config.ignore.includes(r.id));
|
|
2020
|
+
results = deduplicateResults(results);
|
|
1865
2021
|
const score = calculateScore(results);
|
|
1866
2022
|
const grade = computeGrade(score);
|
|
1867
2023
|
const summary = {
|
|
@@ -1881,6 +2037,40 @@ function buildReport(skill, config) {
|
|
|
1881
2037
|
summary
|
|
1882
2038
|
};
|
|
1883
2039
|
}
|
|
2040
|
+
function deduplicateResults(results) {
|
|
2041
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2042
|
+
const severityOrder = {
|
|
2043
|
+
CRITICAL: 4,
|
|
2044
|
+
HIGH: 3,
|
|
2045
|
+
MEDIUM: 2,
|
|
2046
|
+
LOW: 1
|
|
2047
|
+
};
|
|
2048
|
+
for (const r of results) {
|
|
2049
|
+
const sourceFile = extractSourceFile(r.message);
|
|
2050
|
+
const key = `${r.id}::${sourceFile}`;
|
|
2051
|
+
const group = groups.get(key);
|
|
2052
|
+
if (group) {
|
|
2053
|
+
group.push(r);
|
|
2054
|
+
} else {
|
|
2055
|
+
groups.set(key, [r]);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
const deduped = [];
|
|
2059
|
+
for (const group of groups.values()) {
|
|
2060
|
+
group.sort((a, b) => severityOrder[b.severity] - severityOrder[a.severity]);
|
|
2061
|
+
const best = { ...group[0] };
|
|
2062
|
+
if (group.length > 1) {
|
|
2063
|
+
best.occurrences = group.length;
|
|
2064
|
+
best.message += ` (${group.length} occurrences in this file)`;
|
|
2065
|
+
}
|
|
2066
|
+
deduped.push(best);
|
|
2067
|
+
}
|
|
2068
|
+
return deduped;
|
|
2069
|
+
}
|
|
2070
|
+
function extractSourceFile(message) {
|
|
2071
|
+
const m = message.match(/^(?:At\s+)?([^:]+):\d+:/);
|
|
2072
|
+
return m?.[1] ?? "unknown";
|
|
2073
|
+
}
|
|
1884
2074
|
function calculateScore(results) {
|
|
1885
2075
|
let score = 100;
|
|
1886
2076
|
for (const r of results) {
|
|
@@ -2067,7 +2257,7 @@ function buildReportSummary(report) {
|
|
|
2067
2257
|
}
|
|
2068
2258
|
|
|
2069
2259
|
// src/config.ts
|
|
2070
|
-
import { readFileSync as
|
|
2260
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
2071
2261
|
import { join as join4, resolve as resolve2 } from "path";
|
|
2072
2262
|
import { parse as parseYaml2 } from "yaml";
|
|
2073
2263
|
var CONFIG_FILENAMES = [
|
|
@@ -2109,7 +2299,7 @@ function loadConfig(startDir, configPath) {
|
|
|
2109
2299
|
}
|
|
2110
2300
|
function parseConfigFile(path) {
|
|
2111
2301
|
try {
|
|
2112
|
-
const raw =
|
|
2302
|
+
const raw = readFileSync3(path, "utf-8");
|
|
2113
2303
|
const parsed = parseYaml2(raw);
|
|
2114
2304
|
if (!parsed || typeof parsed !== "object") {
|
|
2115
2305
|
return { ...DEFAULT_CONFIG };
|
|
@@ -2159,6 +2349,7 @@ export {
|
|
|
2159
2349
|
loadIOC,
|
|
2160
2350
|
parseSkill,
|
|
2161
2351
|
parseSkillContent,
|
|
2352
|
+
reduceSeverity,
|
|
2162
2353
|
resetIOCCache,
|
|
2163
2354
|
runAllChecks,
|
|
2164
2355
|
scanSkillContent,
|