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/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ interface CheckResult {
11
11
  message: string;
12
12
  line?: number;
13
13
  snippet?: string;
14
+ source?: string;
14
15
  reducedFrom?: Severity;
15
16
  occurrences?: number;
16
17
  }
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, statSync, existsSync } from "fs";
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
- if (entry.isDirectory()) {
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
- const rel = fullPath.slice(dirPath.length + 1);
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
- const ext = extname(entry.name).toLowerCase();
148
- let stats;
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 (stats.size <= FULL_READ_LIMIT) {
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 (stats.size <= 5e7) {
163
+ } else if (lstats.size <= 5e7) {
164
+ let fd;
164
165
  try {
165
- const fd = __require("fs").openSync(fullPath, "r");
166
+ fd = openSync(fullPath, "r");
166
167
  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);
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} (${stats.size} bytes total)`);
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} (${stats.size} bytes)`);
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} (${stats.size} bytes). Content not checked.`);
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: stats.size,
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
- return matrix[policy][severity];
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 as statSync2 } from "fs";
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 = openSync(filePath, "r");
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 = readSync(fd, buf, 0, HASH_CHUNK_SIZE, null);
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
- closeSync(fd);
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 = statSync2(filePath);
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 sourceFile = extractSourceFile(r.message);
2050
- const key = `${r.id}::${sourceFile}`;
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.message += ` (${group.length} occurrences in this file)`;
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) {