skill-checker 0.1.4 → 0.1.5

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 CHANGED
@@ -4,7 +4,7 @@ Security checker for Claude Code skills — detect injection, malicious code, an
4
4
 
5
5
  ## Features
6
6
 
7
- - **52 security rules** across 6 categories: structural validity, content quality, injection detection, code safety, supply chain, and resource abuse
7
+ - **53 security rules** across 6 categories: structural validity, content quality, injection detection, code safety, supply chain, and resource abuse
8
8
  - **Scoring system**: Grade A–F with 0–100 score
9
9
  - **Dual entry**: CLI tool + PreToolUse hook for automatic interception
10
10
  - **Configurable policies**: strict / balanced / permissive approval strategies
@@ -14,7 +14,7 @@ Security checker for Claude Code skills — detect injection, malicious code, an
14
14
 
15
15
  ## Security Standard & Benchmark
16
16
 
17
- Skill Checker's 52 rules are aligned with established security frameworks including OWASP Top 10 for LLM Applications (2025), MITRE CWE, and MITRE ATT&CK. The tool ships with a reproducible benchmark dataset of six fixture skills covering all rule categories. This alignment is an internal mapping exercise — Skill Checker does not claim third-party certification or external audit status.
17
+ Skill Checker's 53 rules are aligned with established security frameworks including OWASP Top 10 for LLM Applications (2025), MITRE CWE, and MITRE ATT&CK. The tool ships with a reproducible benchmark dataset of six fixture skills covering all rule categories. This alignment is an internal mapping exercise — Skill Checker does not claim third-party certification or external audit status.
18
18
 
19
19
  See [docs/SECURITY_BENCHMARK.md](docs/SECURITY_BENCHMARK.md) for the full rule mapping matrix, benchmark methodology, scoring model, and known limitations.
20
20
 
@@ -153,7 +153,7 @@ Config is resolved in order: CLI `--config` flag → project directory (walks up
153
153
  | Structural (STRUCT) | 8 | Missing SKILL.md, invalid frontmatter, binary files |
154
154
  | Content (CONT) | 7 | Placeholder text, lorem ipsum, promotional content |
155
155
  | Injection (INJ) | 9 | Zero-width chars, prompt override, tag injection, encoded payloads |
156
- | Code Safety (CODE) | 12 | eval/exec, shell execution, rm -rf, obfuscation |
156
+ | Code Safety (CODE) | 13 | eval/exec, shell execution, API key leakage, rm -rf, obfuscation |
157
157
  | Supply Chain (SUPPLY) | 10 | Unknown MCP servers, suspicious domains, malicious hashes, typosquat |
158
158
  | Resource Abuse (RES) | 6 | Unrestricted tool access, disable safety checks, ignore project rules |
159
159
 
File without changes
package/dist/cli.js CHANGED
@@ -1084,6 +1084,55 @@ var DYNAMIC_CODE_PATTERNS = [
1084
1084
  /\brequire\s*\(\s*[^"'`\s]/,
1085
1085
  /\b__import__\s*\(/
1086
1086
  ];
1087
+ var PROVIDER_CREDENTIAL_PATTERNS = [
1088
+ {
1089
+ pattern: /\bsk-ant-api03-[A-Za-z0-9_-]{20,}\b/,
1090
+ title: "Anthropic API key exposure"
1091
+ },
1092
+ {
1093
+ pattern: /\bsk-proj-[A-Za-z0-9_-]{20,}\b/,
1094
+ title: "OpenAI project key exposure"
1095
+ },
1096
+ {
1097
+ pattern: /\bxox[bps]-[A-Za-z0-9-]{20,}\b/,
1098
+ title: "Slack token exposure"
1099
+ },
1100
+ {
1101
+ pattern: /\bAKIA[0-9A-Z]{16}\b/,
1102
+ title: "AWS access key exposure"
1103
+ },
1104
+ {
1105
+ pattern: /\bgh[op]_[A-Za-z0-9]{20,}\b/,
1106
+ title: "GitHub token exposure"
1107
+ },
1108
+ {
1109
+ pattern: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/,
1110
+ title: "GitHub fine-grained token exposure"
1111
+ }
1112
+ ];
1113
+ var OPENAI_SK_FALLBACK_PATTERN = /\bsk-[A-Za-z0-9_-]{20,}\b/;
1114
+ var CREDENTIAL_NAME_TOKENS = /* @__PURE__ */ new Set([
1115
+ "api",
1116
+ "key",
1117
+ "token",
1118
+ "secret",
1119
+ "password",
1120
+ "credential"
1121
+ ]);
1122
+ var CREDENTIAL_COMPOUND_NAMES = /* @__PURE__ */ new Set([
1123
+ "apikey",
1124
+ "apitoken",
1125
+ "accesskey",
1126
+ "accesstoken",
1127
+ "secretkey",
1128
+ "secrettoken"
1129
+ ]);
1130
+ var CREDENTIAL_EQUALS_PATTERN = /\b([A-Za-z0-9_-]+)\b\s*=\s*["'`]?([A-Za-z0-9._~+/=\-]{20,})/i;
1131
+ var CREDENTIAL_KEY_VALUE_PATTERN = /^\s*["'`]?([A-Za-z0-9_-]+)["'`]?\s*:\s*["'`]?([A-Za-z0-9._~+/=\-]{20,})/i;
1132
+ var AUTHORIZATION_BEARER_PATTERN = /\bAuthorization\b\s*:\s*Bearer\s+([A-Za-z0-9._~+/=\-]{20,})/i;
1133
+ var X_API_KEY_PATTERN = /\bx-api-key\b\s*:\s*([A-Za-z0-9._~+/=\-]{20,})/i;
1134
+ var CREDENTIAL_MIN_LENGTH = 20;
1135
+ var CREDENTIAL_MIN_ENTROPY = 4.5;
1087
1136
  var PERMISSION_PATTERNS = [
1088
1137
  /\bchmod\s+[+0-9]/,
1089
1138
  /\bchown\b/,
@@ -1158,6 +1207,19 @@ var codeSafetyChecks = {
1158
1207
  source,
1159
1208
  codeBlockCtx: cbCtx
1160
1209
  });
1210
+ const credentialLeak = detectCredentialLeak(line);
1211
+ if (credentialLeak) {
1212
+ results.push({
1213
+ id: "CODE-013",
1214
+ category: "CODE",
1215
+ severity: credentialLeak.severity,
1216
+ title: credentialLeak.title,
1217
+ message: `At ${loc}: ${line.trim().slice(0, 120)}`,
1218
+ line: lineNum,
1219
+ snippet: line.trim().slice(0, 120),
1220
+ source
1221
+ });
1222
+ }
1161
1223
  checkPatterns(results, line, DYNAMIC_CODE_PATTERNS, {
1162
1224
  id: "CODE-010",
1163
1225
  severity: "HIGH",
@@ -1213,6 +1275,82 @@ function checkPatterns(results, line, patterns, opts) {
1213
1275
  }
1214
1276
  }
1215
1277
  }
1278
+ function detectCredentialLeak(line) {
1279
+ for (const provider of PROVIDER_CREDENTIAL_PATTERNS) {
1280
+ if (provider.pattern.test(line)) {
1281
+ return {
1282
+ severity: "CRITICAL",
1283
+ title: provider.title
1284
+ };
1285
+ }
1286
+ }
1287
+ if (OPENAI_SK_FALLBACK_PATTERN.test(line) && isCredentialAssignmentContext(line)) {
1288
+ return {
1289
+ severity: "CRITICAL",
1290
+ title: "OpenAI-style API key exposure"
1291
+ };
1292
+ }
1293
+ const assignmentMatch = line.match(CREDENTIAL_EQUALS_PATTERN);
1294
+ if (assignmentMatch?.[1] && assignmentMatch[2] && hasCredentialNameToken(assignmentMatch[1]) && isHighEntropyCredential(assignmentMatch[2])) {
1295
+ return {
1296
+ severity: "HIGH",
1297
+ title: "High-entropy credential assignment"
1298
+ };
1299
+ }
1300
+ const keyValueMatch = line.match(CREDENTIAL_KEY_VALUE_PATTERN);
1301
+ if (keyValueMatch?.[1] && keyValueMatch[2] && hasCredentialNameToken(keyValueMatch[1]) && isHighEntropyCredential(keyValueMatch[2])) {
1302
+ return {
1303
+ severity: "HIGH",
1304
+ title: "High-entropy credential assignment"
1305
+ };
1306
+ }
1307
+ const bearerMatch = line.match(AUTHORIZATION_BEARER_PATTERN);
1308
+ if (bearerMatch?.[1] && isHighEntropyCredential(bearerMatch[1])) {
1309
+ return {
1310
+ severity: "HIGH",
1311
+ title: "Authorization bearer credential exposure"
1312
+ };
1313
+ }
1314
+ const xApiKeyMatch = line.match(X_API_KEY_PATTERN);
1315
+ if (xApiKeyMatch?.[1] && isHighEntropyCredential(xApiKeyMatch[1])) {
1316
+ return {
1317
+ severity: "HIGH",
1318
+ title: "X-API-Key credential exposure"
1319
+ };
1320
+ }
1321
+ return null;
1322
+ }
1323
+ function isCredentialAssignmentContext(line) {
1324
+ if (/\bAuthorization\b\s*:\s*Bearer\s+sk-/i.test(line)) {
1325
+ return true;
1326
+ }
1327
+ if (/\bx-api-key\b\s*:\s*sk-/i.test(line)) {
1328
+ return true;
1329
+ }
1330
+ const equalsMatch = line.match(CREDENTIAL_EQUALS_PATTERN);
1331
+ if (equalsMatch?.[1] && equalsMatch[2]) {
1332
+ return hasCredentialNameToken(equalsMatch[1]) && equalsMatch[2].startsWith("sk-");
1333
+ }
1334
+ const keyValueMatch = line.match(CREDENTIAL_KEY_VALUE_PATTERN);
1335
+ if (keyValueMatch?.[1] && keyValueMatch[2]) {
1336
+ return hasCredentialNameToken(keyValueMatch[1]) && keyValueMatch[2].startsWith("sk-");
1337
+ }
1338
+ return false;
1339
+ }
1340
+ function hasCredentialNameToken(name) {
1341
+ const normalized = name.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/([A-Za-z])([0-9])/g, "$1_$2").replace(/([0-9])([A-Za-z])/g, "$1_$2").toLowerCase();
1342
+ const tokens = normalized.split(/[_-]+/).map((token) => token.trim()).filter(Boolean);
1343
+ if (tokens.some((token) => CREDENTIAL_NAME_TOKENS.has(token))) {
1344
+ return true;
1345
+ }
1346
+ return tokens.length === 1 && CREDENTIAL_COMPOUND_NAMES.has(tokens[0]);
1347
+ }
1348
+ function isHighEntropyCredential(value) {
1349
+ if (value.length < CREDENTIAL_MIN_LENGTH) {
1350
+ return false;
1351
+ }
1352
+ return shannonEntropy(value) > CREDENTIAL_MIN_ENTROPY;
1353
+ }
1216
1354
  function getTextSources(skill) {
1217
1355
  const sources = [
1218
1356
  { text: skill.body, source: "SKILL.md" }