skill-checker 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -14
- package/dist/cli.js +94 -56
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +89 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
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
|
-
|
|
8
1
|
// src/parser.ts
|
|
9
|
-
import { readFileSync, readdirSync,
|
|
2
|
+
import { readFileSync, readdirSync, lstatSync, existsSync, openSync, readSync, closeSync } from "fs";
|
|
10
3
|
import { join, extname, basename, resolve } from "path";
|
|
11
4
|
import { parse as parseYaml } from "yaml";
|
|
12
5
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
@@ -134,52 +127,66 @@ function enumerateFiles(dirPath, warnings) {
|
|
|
134
127
|
}
|
|
135
128
|
for (const entry of entries) {
|
|
136
129
|
const fullPath = join(currentDir, entry.name);
|
|
137
|
-
|
|
130
|
+
const relativePath = fullPath.slice(dirPath.length + 1);
|
|
131
|
+
let lstats;
|
|
132
|
+
try {
|
|
133
|
+
lstats = lstatSync(fullPath);
|
|
134
|
+
} catch {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (lstats.isSymbolicLink()) {
|
|
138
|
+
warnings.push(`Skipped symlink: ${relativePath}`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (lstats.isDirectory()) {
|
|
138
142
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
139
143
|
if (WARN_SKIP_DIRS.has(entry.name)) {
|
|
140
|
-
|
|
141
|
-
warnings.push(`Skipped directory: ${rel}. May contain unscanned files.`);
|
|
144
|
+
warnings.push(`Skipped directory: ${relativePath}. May contain unscanned files.`);
|
|
142
145
|
continue;
|
|
143
146
|
}
|
|
144
147
|
walk(fullPath, depth + 1);
|
|
145
148
|
continue;
|
|
146
149
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
stats = statSync(fullPath);
|
|
151
|
-
} catch {
|
|
150
|
+
if (!lstats.isFile()) {
|
|
151
|
+
warnings.push(`Skipped special file: ${relativePath}`);
|
|
152
152
|
continue;
|
|
153
153
|
}
|
|
154
|
+
const ext = extname(entry.name).toLowerCase();
|
|
154
155
|
const isBinary = BINARY_EXTENSIONS.has(ext);
|
|
155
|
-
const relativePath = fullPath.slice(dirPath.length + 1);
|
|
156
156
|
let content;
|
|
157
157
|
if (!isBinary) {
|
|
158
|
-
if (
|
|
158
|
+
if (lstats.size <= FULL_READ_LIMIT) {
|
|
159
159
|
try {
|
|
160
160
|
content = readFileSync(fullPath, "utf-8");
|
|
161
161
|
} catch {
|
|
162
162
|
}
|
|
163
|
-
} else if (
|
|
163
|
+
} else if (lstats.size <= 5e7) {
|
|
164
|
+
let fd;
|
|
164
165
|
try {
|
|
165
|
-
|
|
166
|
+
fd = openSync(fullPath, "r");
|
|
166
167
|
const buf = Buffer.alloc(PARTIAL_READ_LIMIT);
|
|
167
|
-
const bytesRead =
|
|
168
|
-
__require("fs").closeSync(fd);
|
|
168
|
+
const bytesRead = readSync(fd, buf, 0, PARTIAL_READ_LIMIT, 0);
|
|
169
169
|
content = buf.slice(0, bytesRead).toString("utf-8");
|
|
170
|
-
warnings.push(`Large file partially scanned (first ${PARTIAL_READ_LIMIT} bytes): ${relativePath} (${
|
|
170
|
+
warnings.push(`Large file partially scanned (first ${PARTIAL_READ_LIMIT} bytes): ${relativePath} (${lstats.size} bytes total)`);
|
|
171
171
|
} catch {
|
|
172
|
-
warnings.push(`Large file could not be read: ${relativePath} (${
|
|
172
|
+
warnings.push(`Large file could not be read: ${relativePath} (${lstats.size} bytes)`);
|
|
173
|
+
} finally {
|
|
174
|
+
if (fd !== void 0) {
|
|
175
|
+
try {
|
|
176
|
+
closeSync(fd);
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
}
|
|
173
180
|
}
|
|
174
181
|
} else {
|
|
175
|
-
warnings.push(`File too large to scan: ${relativePath} (${
|
|
182
|
+
warnings.push(`File too large to scan: ${relativePath} (${lstats.size} bytes). Content not checked.`);
|
|
176
183
|
}
|
|
177
184
|
}
|
|
178
185
|
files.push({
|
|
179
186
|
path: relativePath,
|
|
180
187
|
name: basename(entry.name, ext),
|
|
181
188
|
extension: ext,
|
|
182
|
-
sizeBytes:
|
|
189
|
+
sizeBytes: lstats.size,
|
|
183
190
|
isBinary,
|
|
184
191
|
content
|
|
185
192
|
});
|
|
@@ -271,7 +278,8 @@ var structuralChecks = {
|
|
|
271
278
|
category: "STRUCT",
|
|
272
279
|
severity: "HIGH",
|
|
273
280
|
title: "Unexpected binary/executable file",
|
|
274
|
-
message: `Found unexpected file: ${file.path} (${ext})
|
|
281
|
+
message: `Found unexpected file: ${file.path} (${ext})`,
|
|
282
|
+
source: file.path
|
|
275
283
|
});
|
|
276
284
|
}
|
|
277
285
|
}
|
|
@@ -297,12 +305,14 @@ var structuralChecks = {
|
|
|
297
305
|
}
|
|
298
306
|
}
|
|
299
307
|
for (const warning of skill.warnings) {
|
|
308
|
+
const pathMatch = warning.match(/:\s*(.+?)(?:\s*\(|$)/);
|
|
300
309
|
results.push({
|
|
301
310
|
id: "STRUCT-008",
|
|
302
311
|
category: "STRUCT",
|
|
303
312
|
severity: "MEDIUM",
|
|
304
313
|
title: "Scan coverage warning",
|
|
305
|
-
message: warning
|
|
314
|
+
message: warning,
|
|
315
|
+
source: pathMatch?.[1]?.trim()
|
|
306
316
|
});
|
|
307
317
|
}
|
|
308
318
|
return results;
|
|
@@ -1014,7 +1024,11 @@ function getHookAction(policy, severity) {
|
|
|
1014
1024
|
LOW: "report"
|
|
1015
1025
|
}
|
|
1016
1026
|
};
|
|
1017
|
-
|
|
1027
|
+
const row = matrix[policy];
|
|
1028
|
+
if (!row) {
|
|
1029
|
+
return matrix.balanced[severity];
|
|
1030
|
+
}
|
|
1031
|
+
return row[severity];
|
|
1018
1032
|
}
|
|
1019
1033
|
|
|
1020
1034
|
// src/checks/code-safety.ts
|
|
@@ -1106,7 +1120,8 @@ var codeSafetyChecks = {
|
|
|
1106
1120
|
severity: "CRITICAL",
|
|
1107
1121
|
title: "eval/exec/Function constructor",
|
|
1108
1122
|
loc,
|
|
1109
|
-
lineNum
|
|
1123
|
+
lineNum,
|
|
1124
|
+
source
|
|
1110
1125
|
});
|
|
1111
1126
|
if (!SHELL_EXEC_FALSE_POSITIVES.some((p) => p.test(line))) {
|
|
1112
1127
|
checkPatterns(results, line, SHELL_EXEC_PATTERNS, {
|
|
@@ -1114,7 +1129,8 @@ var codeSafetyChecks = {
|
|
|
1114
1129
|
severity: "CRITICAL",
|
|
1115
1130
|
title: "Shell/subprocess execution",
|
|
1116
1131
|
loc,
|
|
1117
|
-
lineNum
|
|
1132
|
+
lineNum,
|
|
1133
|
+
source
|
|
1118
1134
|
});
|
|
1119
1135
|
}
|
|
1120
1136
|
checkPatterns(results, line, DESTRUCTIVE_PATTERNS, {
|
|
@@ -1123,6 +1139,7 @@ var codeSafetyChecks = {
|
|
|
1123
1139
|
title: "Destructive file operation",
|
|
1124
1140
|
loc,
|
|
1125
1141
|
lineNum,
|
|
1142
|
+
source,
|
|
1126
1143
|
codeBlockCtx: cbCtx
|
|
1127
1144
|
});
|
|
1128
1145
|
checkPatterns(results, line, NETWORK_PATTERNS, {
|
|
@@ -1131,6 +1148,7 @@ var codeSafetyChecks = {
|
|
|
1131
1148
|
title: "Hardcoded external URL/network request",
|
|
1132
1149
|
loc,
|
|
1133
1150
|
lineNum,
|
|
1151
|
+
source,
|
|
1134
1152
|
codeBlockCtx: cbCtx
|
|
1135
1153
|
});
|
|
1136
1154
|
checkPatterns(results, line, FILE_WRITE_PATTERNS, {
|
|
@@ -1138,7 +1156,8 @@ var codeSafetyChecks = {
|
|
|
1138
1156
|
severity: "HIGH",
|
|
1139
1157
|
title: "File write outside expected directory",
|
|
1140
1158
|
loc,
|
|
1141
|
-
lineNum
|
|
1159
|
+
lineNum,
|
|
1160
|
+
source
|
|
1142
1161
|
});
|
|
1143
1162
|
checkPatterns(results, line, ENV_ACCESS_PATTERNS, {
|
|
1144
1163
|
id: "CODE-006",
|
|
@@ -1146,6 +1165,7 @@ var codeSafetyChecks = {
|
|
|
1146
1165
|
title: "Environment variable access",
|
|
1147
1166
|
loc,
|
|
1148
1167
|
lineNum,
|
|
1168
|
+
source,
|
|
1149
1169
|
codeBlockCtx: cbCtx
|
|
1150
1170
|
});
|
|
1151
1171
|
checkPatterns(results, line, DYNAMIC_CODE_PATTERNS, {
|
|
@@ -1153,7 +1173,8 @@ var codeSafetyChecks = {
|
|
|
1153
1173
|
severity: "HIGH",
|
|
1154
1174
|
title: "Dynamic code generation pattern",
|
|
1155
1175
|
loc,
|
|
1156
|
-
lineNum
|
|
1176
|
+
lineNum,
|
|
1177
|
+
source
|
|
1157
1178
|
});
|
|
1158
1179
|
{
|
|
1159
1180
|
const isDoc = isInDocumentationContext(lines, i);
|
|
@@ -1163,7 +1184,8 @@ var codeSafetyChecks = {
|
|
|
1163
1184
|
severity: "HIGH",
|
|
1164
1185
|
title: "Permission escalation",
|
|
1165
1186
|
loc,
|
|
1166
|
-
lineNum
|
|
1187
|
+
lineNum,
|
|
1188
|
+
source
|
|
1167
1189
|
});
|
|
1168
1190
|
}
|
|
1169
1191
|
}
|
|
@@ -1194,6 +1216,7 @@ function checkPatterns(results, line, patterns, opts) {
|
|
|
1194
1216
|
message: `At ${opts.loc}: ${line.trim().slice(0, 120)}${msgSuffix}`,
|
|
1195
1217
|
line: opts.lineNum,
|
|
1196
1218
|
snippet: line.trim().slice(0, 120),
|
|
1219
|
+
source: opts.source,
|
|
1197
1220
|
reducedFrom
|
|
1198
1221
|
});
|
|
1199
1222
|
return;
|
|
@@ -1225,7 +1248,8 @@ function scanEncodedStrings(results, text, source) {
|
|
|
1225
1248
|
title: "Long encoded string",
|
|
1226
1249
|
message: `${source}:${lineNum}: Found ${str.length}-char encoded string.`,
|
|
1227
1250
|
line: lineNum,
|
|
1228
|
-
snippet: str.slice(0, 80) + "..."
|
|
1251
|
+
snippet: str.slice(0, 80) + "...",
|
|
1252
|
+
source
|
|
1229
1253
|
});
|
|
1230
1254
|
}
|
|
1231
1255
|
}
|
|
@@ -1240,7 +1264,8 @@ function scanEncodedStrings(results, text, source) {
|
|
|
1240
1264
|
severity: "MEDIUM",
|
|
1241
1265
|
title: "High entropy string",
|
|
1242
1266
|
message: `${source}:${lineNum}: String "${match[0].slice(0, 30)}..." has entropy ${entropy.toFixed(2)} bits/char.`,
|
|
1243
|
-
line: lineNum
|
|
1267
|
+
line: lineNum,
|
|
1268
|
+
source
|
|
1244
1269
|
});
|
|
1245
1270
|
}
|
|
1246
1271
|
}
|
|
@@ -1257,7 +1282,8 @@ function scanEncodedStrings(results, text, source) {
|
|
|
1257
1282
|
category: "CODE",
|
|
1258
1283
|
severity: "CRITICAL",
|
|
1259
1284
|
title: "Multi-layer encoding detected",
|
|
1260
|
-
message: `${source}: Contains nested encoding/decoding operations
|
|
1285
|
+
message: `${source}: Contains nested encoding/decoding operations.`,
|
|
1286
|
+
source
|
|
1261
1287
|
});
|
|
1262
1288
|
break;
|
|
1263
1289
|
}
|
|
@@ -1272,7 +1298,8 @@ function scanObfuscation(results, text, source) {
|
|
|
1272
1298
|
category: "CODE",
|
|
1273
1299
|
severity: "MEDIUM",
|
|
1274
1300
|
title: "Obfuscated variable names",
|
|
1275
|
-
message: `${source}: Found ${obfMatches.length} hex-style variable names (e.g. ${obfMatches[0]}). May indicate obfuscated code
|
|
1301
|
+
message: `${source}: Found ${obfMatches.length} hex-style variable names (e.g. ${obfMatches[0]}). May indicate obfuscated code.`,
|
|
1302
|
+
source
|
|
1276
1303
|
});
|
|
1277
1304
|
}
|
|
1278
1305
|
}
|
|
@@ -1466,6 +1493,7 @@ var supplyChainChecks = {
|
|
|
1466
1493
|
message: `${source}:${lineNum}: References an MCP server. Verify it is from a trusted source.${msgSuffix}`,
|
|
1467
1494
|
line: lineNum,
|
|
1468
1495
|
snippet: line.trim().slice(0, 120),
|
|
1496
|
+
source,
|
|
1469
1497
|
reducedFrom
|
|
1470
1498
|
});
|
|
1471
1499
|
}
|
|
@@ -1477,7 +1505,8 @@ var supplyChainChecks = {
|
|
|
1477
1505
|
title: "npx -y auto-install",
|
|
1478
1506
|
message: `${source}:${lineNum}: Uses npx -y which auto-installs packages without confirmation.`,
|
|
1479
1507
|
line: lineNum,
|
|
1480
|
-
snippet: line.trim().slice(0, 120)
|
|
1508
|
+
snippet: line.trim().slice(0, 120),
|
|
1509
|
+
source
|
|
1481
1510
|
});
|
|
1482
1511
|
}
|
|
1483
1512
|
if (NPM_INSTALL_PATTERN.test(line) || PIP_INSTALL_PATTERN.test(line)) {
|
|
@@ -1507,6 +1536,7 @@ var supplyChainChecks = {
|
|
|
1507
1536
|
message: `${source}:${lineNum}: Installs packages. Verify package names are legitimate.${msgSuffix}`,
|
|
1508
1537
|
line: lineNum,
|
|
1509
1538
|
snippet: line.trim().slice(0, 120),
|
|
1539
|
+
source,
|
|
1510
1540
|
reducedFrom
|
|
1511
1541
|
});
|
|
1512
1542
|
}
|
|
@@ -1519,7 +1549,8 @@ var supplyChainChecks = {
|
|
|
1519
1549
|
title: "git clone command",
|
|
1520
1550
|
message: `${source}:${lineNum}: Clones a git repository. Verify the source.`,
|
|
1521
1551
|
line: lineNum,
|
|
1522
|
-
snippet: line.trim().slice(0, 120)
|
|
1552
|
+
snippet: line.trim().slice(0, 120),
|
|
1553
|
+
source
|
|
1523
1554
|
});
|
|
1524
1555
|
}
|
|
1525
1556
|
const urls = line.match(URL_PATTERN) || [];
|
|
@@ -1548,6 +1579,7 @@ var supplyChainChecks = {
|
|
|
1548
1579
|
message: `${source}:${lineNum}: Uses insecure HTTP: ${url}${msgSuffix}`,
|
|
1549
1580
|
line: lineNum,
|
|
1550
1581
|
snippet: url,
|
|
1582
|
+
source,
|
|
1551
1583
|
reducedFrom
|
|
1552
1584
|
});
|
|
1553
1585
|
}
|
|
@@ -1561,7 +1593,8 @@ var supplyChainChecks = {
|
|
|
1561
1593
|
title: "IP address used instead of domain",
|
|
1562
1594
|
message: `${source}:${lineNum}: Uses raw IP address: ${url}. This may bypass DNS-based security.`,
|
|
1563
1595
|
line: lineNum,
|
|
1564
|
-
snippet: url
|
|
1596
|
+
snippet: url,
|
|
1597
|
+
source
|
|
1565
1598
|
});
|
|
1566
1599
|
}
|
|
1567
1600
|
}
|
|
@@ -1575,7 +1608,8 @@ var supplyChainChecks = {
|
|
|
1575
1608
|
title: "Suspicious domain detected",
|
|
1576
1609
|
message: `${source}:${lineNum}: References suspicious domain "${domain}".`,
|
|
1577
1610
|
line: lineNum,
|
|
1578
|
-
snippet: url
|
|
1611
|
+
snippet: url,
|
|
1612
|
+
source
|
|
1579
1613
|
});
|
|
1580
1614
|
break;
|
|
1581
1615
|
}
|
|
@@ -1775,7 +1809,7 @@ var resourceChecks = {
|
|
|
1775
1809
|
|
|
1776
1810
|
// src/ioc/matcher.ts
|
|
1777
1811
|
import { createHash } from "crypto";
|
|
1778
|
-
import { openSync, readSync, closeSync, statSync
|
|
1812
|
+
import { openSync as openSync2, readSync as readSync2, closeSync as closeSync2, statSync } from "fs";
|
|
1779
1813
|
import { join as join3 } from "path";
|
|
1780
1814
|
|
|
1781
1815
|
// src/utils/levenshtein.ts
|
|
@@ -1824,18 +1858,18 @@ function isPrivateIP(ip) {
|
|
|
1824
1858
|
var HASH_CHUNK_SIZE = 64 * 1024;
|
|
1825
1859
|
function computeFileHash(filePath) {
|
|
1826
1860
|
const hash = createHash("sha256");
|
|
1827
|
-
const fd =
|
|
1861
|
+
const fd = openSync2(filePath, "r");
|
|
1828
1862
|
try {
|
|
1829
1863
|
const buf = Buffer.alloc(HASH_CHUNK_SIZE);
|
|
1830
1864
|
let bytesRead;
|
|
1831
1865
|
do {
|
|
1832
|
-
bytesRead =
|
|
1866
|
+
bytesRead = readSync2(fd, buf, 0, HASH_CHUNK_SIZE, null);
|
|
1833
1867
|
if (bytesRead > 0) {
|
|
1834
1868
|
hash.update(buf.subarray(0, bytesRead));
|
|
1835
1869
|
}
|
|
1836
1870
|
} while (bytesRead > 0);
|
|
1837
1871
|
} finally {
|
|
1838
|
-
|
|
1872
|
+
closeSync2(fd);
|
|
1839
1873
|
}
|
|
1840
1874
|
return hash.digest("hex");
|
|
1841
1875
|
}
|
|
@@ -1846,7 +1880,7 @@ function matchMaliciousHashes(skill, ioc) {
|
|
|
1846
1880
|
for (const file of skill.files) {
|
|
1847
1881
|
const filePath = join3(skill.dirPath, file.path);
|
|
1848
1882
|
try {
|
|
1849
|
-
const stat =
|
|
1883
|
+
const stat = statSync(filePath);
|
|
1850
1884
|
if (stat.size === 0) continue;
|
|
1851
1885
|
const hash = computeFileHash(filePath);
|
|
1852
1886
|
if (hash === EMPTY_FILE_HASH) continue;
|
|
@@ -1937,7 +1971,8 @@ var iocChecks = {
|
|
|
1937
1971
|
severity: "CRITICAL",
|
|
1938
1972
|
title: "Known malicious file hash",
|
|
1939
1973
|
message: `File "${match.file}" matches known malicious hash: ${match.description}`,
|
|
1940
|
-
snippet: match.hash
|
|
1974
|
+
snippet: match.hash,
|
|
1975
|
+
source: match.file
|
|
1941
1976
|
});
|
|
1942
1977
|
}
|
|
1943
1978
|
const ipMatches = matchC2IPs(skill, ioc);
|
|
@@ -1949,7 +1984,8 @@ var iocChecks = {
|
|
|
1949
1984
|
title: "Known C2 IP address",
|
|
1950
1985
|
message: `${match.source}:${match.line}: Contains known C2 server IP: ${match.ip}`,
|
|
1951
1986
|
line: match.line,
|
|
1952
|
-
snippet: match.snippet
|
|
1987
|
+
snippet: match.snippet,
|
|
1988
|
+
source: match.source
|
|
1953
1989
|
});
|
|
1954
1990
|
}
|
|
1955
1991
|
const skillName = skill.frontmatter.name;
|
|
@@ -2046,8 +2082,8 @@ function deduplicateResults(results) {
|
|
|
2046
2082
|
LOW: 1
|
|
2047
2083
|
};
|
|
2048
2084
|
for (const r of results) {
|
|
2049
|
-
const
|
|
2050
|
-
const key = `${r.id}::${
|
|
2085
|
+
const sourceKey = r.source ?? `_no_source_:${r.category}:${r.line ?? ""}`;
|
|
2086
|
+
const key = `${r.id}::${sourceKey}`;
|
|
2051
2087
|
const group = groups.get(key);
|
|
2052
2088
|
if (group) {
|
|
2053
2089
|
group.push(r);
|
|
@@ -2061,16 +2097,13 @@ function deduplicateResults(results) {
|
|
|
2061
2097
|
const best = { ...group[0] };
|
|
2062
2098
|
if (group.length > 1) {
|
|
2063
2099
|
best.occurrences = group.length;
|
|
2064
|
-
best.
|
|
2100
|
+
const suffix = best.source ? ` (${group.length} occurrences in this file)` : ` (${group.length} occurrences)`;
|
|
2101
|
+
best.message += suffix;
|
|
2065
2102
|
}
|
|
2066
2103
|
deduped.push(best);
|
|
2067
2104
|
}
|
|
2068
2105
|
return deduped;
|
|
2069
2106
|
}
|
|
2070
|
-
function extractSourceFile(message) {
|
|
2071
|
-
const m = message.match(/^(?:At\s+)?([^:]+):\d+:/);
|
|
2072
|
-
return m?.[1] ?? "unknown";
|
|
2073
|
-
}
|
|
2074
2107
|
function calculateScore(results) {
|
|
2075
2108
|
let score = 100;
|
|
2076
2109
|
for (const r of results) {
|