skill-checker 0.1.16 → 0.5.0
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 +24 -1
- package/dist/cli.js +439 -21
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +58 -1
- package/dist/index.js +442 -20
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -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 57 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
|
|
17
|
+
Skill Checker's 57 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 nine 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
|
|
|
@@ -42,6 +42,7 @@ skill-checker scan <path> [options]
|
|
|
42
42
|
| `-f, --format <format>` | Output format: `terminal` (default), `json`, `hook` |
|
|
43
43
|
| `-p, --policy <policy>` | Approval policy: `strict`, `balanced` (default), `permissive` |
|
|
44
44
|
| `-c, --config <path>` | Path to config file |
|
|
45
|
+
| `--no-ignore` | Disable inline suppression comments |
|
|
45
46
|
|
|
46
47
|
```bash
|
|
47
48
|
# Colored terminal report
|
|
@@ -149,6 +150,28 @@ ignore:
|
|
|
149
150
|
|
|
150
151
|
Config is resolved in order: CLI `--config` flag → project directory (walks up) → home directory → defaults.
|
|
151
152
|
|
|
153
|
+
### Inline Suppression
|
|
154
|
+
|
|
155
|
+
Suppress specific findings directly in SKILL.md using comments:
|
|
156
|
+
|
|
157
|
+
```markdown
|
|
158
|
+
<!-- skill-checker-ignore CODE-002 -->
|
|
159
|
+
Run `soffice --convert-to pdf` to convert documents.
|
|
160
|
+
|
|
161
|
+
<!-- skill-checker-ignore-file CONT-001 -->
|
|
162
|
+
|
|
163
|
+
subprocess.run("soffice") // skill-checker-ignore CODE-002
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- `<!-- skill-checker-ignore RULE -->` suppresses the finding on the next line only
|
|
167
|
+
- `<!-- skill-checker-ignore-file RULE -->` suppresses the finding for the entire file
|
|
168
|
+
- `// skill-checker-ignore RULE` as a trailing comment suppresses the finding on the same line
|
|
169
|
+
- Multiple rules can be space-separated: `<!-- skill-checker-ignore CODE-002 CONT-001 -->`
|
|
170
|
+
- **INJ rules cannot be suppressed** — attempts produce a warning
|
|
171
|
+
- Directives only affect findings from the same source file (no cross-file suppression)
|
|
172
|
+
- Suppressed findings are excluded from scoring but remain visible in the report
|
|
173
|
+
- Use `--no-ignore` to disable all inline suppression
|
|
174
|
+
|
|
152
175
|
### Policy Matrix
|
|
153
176
|
|
|
154
177
|
| Severity | strict | balanced | permissive |
|
package/dist/cli.js
CHANGED
|
@@ -568,7 +568,8 @@ var contentChecks = {
|
|
|
568
568
|
/\bplaceholder\b.*\b(TITLE|SUBTITLE|BODY|OBJECT|SLIDE|layout|slide|shape|pptx|presentation)/i.test(line) || /\b(TITLE|SUBTITLE|BODY|OBJECT|SLIDE|layout|slide|shape|pptx|presentation)\b.*\bplaceholder\b/i.test(line) || // API/code context: placeholder as a noun in technical documentation
|
|
569
569
|
/\bplaceholder\s+(areas?|locations?|counts?|slots?|elements?|fields?)\b/i.test(line) || /\b(replace|replacing|replacement)\b.*\bplaceholder\b/i.test(line)
|
|
570
570
|
);
|
|
571
|
-
|
|
571
|
+
const isInstructional = /\b(?:create|generate|add|write|insert|produce|include|use)\b.*\bplaceholder\b/i.test(line) || /\bplaceholder\b.*\b(?:for|in|to|as|with)\s+(?:all|each|every|the|missing)\b/i.test(line);
|
|
572
|
+
if (!inCodeBlk && !hasInlineCode && !isTechnicalRef && !isInstructional) {
|
|
572
573
|
for (const pattern of CONTEXT_SENSITIVE_PLACEHOLDER_PATTERNS) {
|
|
573
574
|
if (pattern.test(line)) {
|
|
574
575
|
matched = true;
|
|
@@ -576,6 +577,26 @@ var contentChecks = {
|
|
|
576
577
|
}
|
|
577
578
|
}
|
|
578
579
|
}
|
|
580
|
+
if (!matched && isInstructional && !inCodeBlk && !hasInlineCode && !isTechnicalRef) {
|
|
581
|
+
for (const pattern of CONTEXT_SENSITIVE_PLACEHOLDER_PATTERNS) {
|
|
582
|
+
if (pattern.test(line)) {
|
|
583
|
+
const reduction = reduceSeverity("HIGH", "instructional placeholder context");
|
|
584
|
+
results.push({
|
|
585
|
+
id: "CONT-001",
|
|
586
|
+
category: "CONT",
|
|
587
|
+
severity: reduction.severity,
|
|
588
|
+
title: "Placeholder content detected",
|
|
589
|
+
message: `Line ${skill.bodyStartLine + i}: Contains placeholder text. ${reduction.annotation}`,
|
|
590
|
+
line: skill.bodyStartLine + i,
|
|
591
|
+
snippet: line.trim().slice(0, 120),
|
|
592
|
+
reducedFrom: reduction.reducedFrom,
|
|
593
|
+
source: "SKILL.md"
|
|
594
|
+
});
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
579
600
|
}
|
|
580
601
|
if (matched) {
|
|
581
602
|
results.push({
|
|
@@ -1052,7 +1073,8 @@ var injectionChecks = {
|
|
|
1052
1073
|
title: "System prompt override attempt",
|
|
1053
1074
|
message: `Line ${lineNum}: Attempts to override system instructions.`,
|
|
1054
1075
|
line: lineNum,
|
|
1055
|
-
snippet: line.trim().slice(0, 120)
|
|
1076
|
+
snippet: line.trim().slice(0, 120),
|
|
1077
|
+
source: "SKILL.md"
|
|
1056
1078
|
});
|
|
1057
1079
|
break;
|
|
1058
1080
|
}
|
|
@@ -1066,7 +1088,8 @@ var injectionChecks = {
|
|
|
1066
1088
|
title: "Tool output manipulation",
|
|
1067
1089
|
message: `Line ${lineNum}: Attempts to manipulate tool results.`,
|
|
1068
1090
|
line: lineNum,
|
|
1069
|
-
snippet: line.trim().slice(0, 120)
|
|
1091
|
+
snippet: line.trim().slice(0, 120),
|
|
1092
|
+
source: "SKILL.md"
|
|
1070
1093
|
});
|
|
1071
1094
|
break;
|
|
1072
1095
|
}
|
|
@@ -1080,7 +1103,8 @@ var injectionChecks = {
|
|
|
1080
1103
|
title: "Tag injection detected",
|
|
1081
1104
|
message: `Line ${lineNum}: Contains special model/system tags.`,
|
|
1082
1105
|
line: lineNum,
|
|
1083
|
-
snippet: line.trim().slice(0, 120)
|
|
1106
|
+
snippet: line.trim().slice(0, 120),
|
|
1107
|
+
source: "SKILL.md"
|
|
1084
1108
|
});
|
|
1085
1109
|
break;
|
|
1086
1110
|
}
|
|
@@ -1094,7 +1118,8 @@ var injectionChecks = {
|
|
|
1094
1118
|
title: "Delimiter confusion pattern",
|
|
1095
1119
|
message: `Line ${lineNum}: Uses patterns that could confuse model context boundaries.`,
|
|
1096
1120
|
line: lineNum,
|
|
1097
|
-
snippet: line.trim().slice(0, 120)
|
|
1121
|
+
snippet: line.trim().slice(0, 120),
|
|
1122
|
+
source: "SKILL.md"
|
|
1098
1123
|
});
|
|
1099
1124
|
break;
|
|
1100
1125
|
}
|
|
@@ -1111,7 +1136,8 @@ var injectionChecks = {
|
|
|
1111
1136
|
title: "Social engineering: identity hijacking",
|
|
1112
1137
|
message: `Line ${lineNum}: Attempts to hijack the model's identity.`,
|
|
1113
1138
|
line: lineNum,
|
|
1114
|
-
snippet: line.trim().slice(0, 120)
|
|
1139
|
+
snippet: line.trim().slice(0, 120),
|
|
1140
|
+
source: "SKILL.md"
|
|
1115
1141
|
});
|
|
1116
1142
|
break;
|
|
1117
1143
|
}
|
|
@@ -1125,7 +1151,8 @@ var injectionChecks = {
|
|
|
1125
1151
|
title: "Social engineering: deception/secrecy",
|
|
1126
1152
|
message: `Line ${lineNum}: Instructs the model to hide actions from the user.`,
|
|
1127
1153
|
line: lineNum,
|
|
1128
|
-
snippet: line.trim().slice(0, 120)
|
|
1154
|
+
snippet: line.trim().slice(0, 120),
|
|
1155
|
+
source: "SKILL.md"
|
|
1129
1156
|
});
|
|
1130
1157
|
break;
|
|
1131
1158
|
}
|
|
@@ -1139,7 +1166,8 @@ var injectionChecks = {
|
|
|
1139
1166
|
title: "Social engineering: configuration tampering",
|
|
1140
1167
|
message: `Line ${lineNum}: Attempts to tamper with model configuration or memory.`,
|
|
1141
1168
|
line: lineNum,
|
|
1142
|
-
snippet: line.trim().slice(0, 120)
|
|
1169
|
+
snippet: line.trim().slice(0, 120),
|
|
1170
|
+
source: "SKILL.md"
|
|
1143
1171
|
});
|
|
1144
1172
|
break;
|
|
1145
1173
|
}
|
|
@@ -1153,7 +1181,8 @@ var injectionChecks = {
|
|
|
1153
1181
|
title: "Social engineering: verification bypass",
|
|
1154
1182
|
message: `Line ${lineNum}: Attempts to bypass verification or validation.`,
|
|
1155
1183
|
line: lineNum,
|
|
1156
|
-
snippet: line.trim().slice(0, 120)
|
|
1184
|
+
snippet: line.trim().slice(0, 120),
|
|
1185
|
+
source: "SKILL.md"
|
|
1157
1186
|
});
|
|
1158
1187
|
break;
|
|
1159
1188
|
}
|
|
@@ -1172,7 +1201,8 @@ var injectionChecks = {
|
|
|
1172
1201
|
title: "Hidden instructions in HTML comment",
|
|
1173
1202
|
message: `Line ${lineNum}: HTML comment contains instruction-like content.`,
|
|
1174
1203
|
line: lineNum,
|
|
1175
|
-
snippet: commentBody.trim().slice(0, 120)
|
|
1204
|
+
snippet: commentBody.trim().slice(0, 120),
|
|
1205
|
+
source: "SKILL.md"
|
|
1176
1206
|
});
|
|
1177
1207
|
}
|
|
1178
1208
|
}
|
|
@@ -1191,7 +1221,8 @@ var injectionChecks = {
|
|
|
1191
1221
|
title: "Encoded instructions detected",
|
|
1192
1222
|
message: `Line ${lineNum}: Base64 string decodes to instruction-like content.`,
|
|
1193
1223
|
line: lineNum,
|
|
1194
|
-
snippet: decoded.slice(0, 120)
|
|
1224
|
+
snippet: decoded.slice(0, 120),
|
|
1225
|
+
source: "SKILL.md"
|
|
1195
1226
|
});
|
|
1196
1227
|
}
|
|
1197
1228
|
}
|
|
@@ -2326,7 +2357,8 @@ var resourceChecks = {
|
|
|
2326
2357
|
title: "Instruction amplification",
|
|
2327
2358
|
message: `Line ${lineNum}: Contains recursive/repetitive task pattern.`,
|
|
2328
2359
|
line: lineNum,
|
|
2329
|
-
snippet: line.trim().slice(0, 120)
|
|
2360
|
+
snippet: line.trim().slice(0, 120),
|
|
2361
|
+
source: "SKILL.md"
|
|
2330
2362
|
});
|
|
2331
2363
|
break;
|
|
2332
2364
|
}
|
|
@@ -2340,7 +2372,8 @@ var resourceChecks = {
|
|
|
2340
2372
|
title: "Unrestricted tool access requested",
|
|
2341
2373
|
message: `Line ${lineNum}: Requests broad/unrestricted tool access.`,
|
|
2342
2374
|
line: lineNum,
|
|
2343
|
-
snippet: line.trim().slice(0, 120)
|
|
2375
|
+
snippet: line.trim().slice(0, 120),
|
|
2376
|
+
source: "SKILL.md"
|
|
2344
2377
|
});
|
|
2345
2378
|
break;
|
|
2346
2379
|
}
|
|
@@ -2354,7 +2387,8 @@ var resourceChecks = {
|
|
|
2354
2387
|
title: "Attempts to disable safety checks",
|
|
2355
2388
|
message: `Line ${lineNum}: Instructs disabling of safety mechanisms.`,
|
|
2356
2389
|
line: lineNum,
|
|
2357
|
-
snippet: line.trim().slice(0, 120)
|
|
2390
|
+
snippet: line.trim().slice(0, 120),
|
|
2391
|
+
source: "SKILL.md"
|
|
2358
2392
|
});
|
|
2359
2393
|
break;
|
|
2360
2394
|
}
|
|
@@ -2368,7 +2402,8 @@ var resourceChecks = {
|
|
|
2368
2402
|
title: "Token waste pattern",
|
|
2369
2403
|
message: `Line ${lineNum}: Contains instructions that waste tokens.`,
|
|
2370
2404
|
line: lineNum,
|
|
2371
|
-
snippet: line.trim().slice(0, 120)
|
|
2405
|
+
snippet: line.trim().slice(0, 120),
|
|
2406
|
+
source: "SKILL.md"
|
|
2372
2407
|
});
|
|
2373
2408
|
break;
|
|
2374
2409
|
}
|
|
@@ -2382,7 +2417,8 @@ var resourceChecks = {
|
|
|
2382
2417
|
title: "Attempts to ignore project rules",
|
|
2383
2418
|
message: `Line ${lineNum}: Instructs ignoring CLAUDE.md or project configuration.`,
|
|
2384
2419
|
line: lineNum,
|
|
2385
|
-
snippet: line.trim().slice(0, 120)
|
|
2420
|
+
snippet: line.trim().slice(0, 120),
|
|
2421
|
+
source: "SKILL.md"
|
|
2386
2422
|
});
|
|
2387
2423
|
break;
|
|
2388
2424
|
}
|
|
@@ -2648,6 +2684,344 @@ function runAllChecks(skill) {
|
|
|
2648
2684
|
return results;
|
|
2649
2685
|
}
|
|
2650
2686
|
|
|
2687
|
+
// src/suppression.ts
|
|
2688
|
+
var RULE_ID_RE = /^[A-Z]+-\d{3}$/;
|
|
2689
|
+
var NEXT_LINE_RE = /^<!--\s*skill-checker-ignore\s+([\w-]+(?:\s+[\w-]+)*)\s*-->$/;
|
|
2690
|
+
var FILE_LEVEL_RE = /^<!--\s*skill-checker-ignore-file\s+([\w-]+(?:\s+[\w-]+)*)\s*-->$/;
|
|
2691
|
+
var INLINE_COMMENT_RE = /\/\/\s*skill-checker-ignore\s+([\w-]+(?:\s+[\w-]+)*)\s*$/;
|
|
2692
|
+
function parseSuppressionDirectives(rawLines, source = "SKILL.md") {
|
|
2693
|
+
const directives = [];
|
|
2694
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
2695
|
+
const trimmed = rawLines[i].trim();
|
|
2696
|
+
const lineNum = i + 1;
|
|
2697
|
+
const nextMatch = NEXT_LINE_RE.exec(trimmed);
|
|
2698
|
+
if (nextMatch) {
|
|
2699
|
+
const ruleIds = nextMatch[1].split(/\s+/);
|
|
2700
|
+
directives.push({ ruleIds, scope: "next-line", line: lineNum, source });
|
|
2701
|
+
continue;
|
|
2702
|
+
}
|
|
2703
|
+
const fileMatch = FILE_LEVEL_RE.exec(trimmed);
|
|
2704
|
+
if (fileMatch) {
|
|
2705
|
+
const ruleIds = fileMatch[1].split(/\s+/);
|
|
2706
|
+
directives.push({ ruleIds, scope: "file", line: lineNum, source });
|
|
2707
|
+
continue;
|
|
2708
|
+
}
|
|
2709
|
+
const inlineMatch = INLINE_COMMENT_RE.exec(rawLines[i]);
|
|
2710
|
+
if (inlineMatch) {
|
|
2711
|
+
const ruleIds = inlineMatch[1].split(/\s+/);
|
|
2712
|
+
directives.push({ ruleIds, scope: "same-line", line: lineNum, source });
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
return directives;
|
|
2716
|
+
}
|
|
2717
|
+
function isInjRule(ruleId) {
|
|
2718
|
+
return ruleId.startsWith("INJ-");
|
|
2719
|
+
}
|
|
2720
|
+
function applySuppressions(results, directives, bodyStartLine) {
|
|
2721
|
+
const warnings = [];
|
|
2722
|
+
const usedDirectiveRules = /* @__PURE__ */ new Set();
|
|
2723
|
+
for (const d of directives) {
|
|
2724
|
+
for (const ruleId of d.ruleIds) {
|
|
2725
|
+
if (!RULE_ID_RE.test(ruleId)) {
|
|
2726
|
+
warnings.push(`Invalid suppression: ${ruleId} at line ${d.line}`);
|
|
2727
|
+
} else if (isInjRule(ruleId)) {
|
|
2728
|
+
warnings.push(`Cannot suppress INJ rule: ${ruleId} at line ${d.line} (security policy)`);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
const active = [];
|
|
2733
|
+
const suppressed = [];
|
|
2734
|
+
for (const result of results) {
|
|
2735
|
+
let isSuppressed = false;
|
|
2736
|
+
if (!isInjRule(result.id)) {
|
|
2737
|
+
for (const d of directives) {
|
|
2738
|
+
if (!d.ruleIds.includes(result.id)) continue;
|
|
2739
|
+
if (!RULE_ID_RE.test(result.id)) continue;
|
|
2740
|
+
if (result.source !== void 0 && result.source !== d.source) continue;
|
|
2741
|
+
if (result.source === void 0 && d.source !== "SKILL.md") continue;
|
|
2742
|
+
if (d.scope === "next-line") {
|
|
2743
|
+
const rawTarget = d.line + 1;
|
|
2744
|
+
const bodyTarget = bodyStartLine !== void 0 ? rawTarget - bodyStartLine + 1 : void 0;
|
|
2745
|
+
if (result.line !== void 0 && (result.line === rawTarget || bodyTarget !== void 0 && result.line === bodyTarget)) {
|
|
2746
|
+
isSuppressed = true;
|
|
2747
|
+
usedDirectiveRules.add(`${d.line}:${result.id}`);
|
|
2748
|
+
break;
|
|
2749
|
+
}
|
|
2750
|
+
} else if (d.scope === "same-line") {
|
|
2751
|
+
const rawTarget = d.line;
|
|
2752
|
+
const bodyTarget = bodyStartLine !== void 0 ? rawTarget - bodyStartLine + 1 : void 0;
|
|
2753
|
+
if (result.line !== void 0 && (result.line === rawTarget || bodyTarget !== void 0 && result.line === bodyTarget)) {
|
|
2754
|
+
isSuppressed = true;
|
|
2755
|
+
usedDirectiveRules.add(`${d.line}:${result.id}`);
|
|
2756
|
+
break;
|
|
2757
|
+
}
|
|
2758
|
+
} else if (d.scope === "file") {
|
|
2759
|
+
isSuppressed = true;
|
|
2760
|
+
usedDirectiveRules.add(`${d.line}:${result.id}`);
|
|
2761
|
+
break;
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
if (isSuppressed) {
|
|
2766
|
+
suppressed.push({ ...result, suppressed: true });
|
|
2767
|
+
} else {
|
|
2768
|
+
active.push(result);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
for (const d of directives) {
|
|
2772
|
+
for (const ruleId of d.ruleIds) {
|
|
2773
|
+
if (!RULE_ID_RE.test(ruleId)) continue;
|
|
2774
|
+
if (isInjRule(ruleId)) continue;
|
|
2775
|
+
const key = `${d.line}:${ruleId}`;
|
|
2776
|
+
if (!usedDirectiveRules.has(key)) {
|
|
2777
|
+
warnings.push(`Unused suppression: ${ruleId} at line ${d.line}`);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
return { active, suppressed, warnings };
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
// src/remediation.ts
|
|
2785
|
+
var REMEDIATION_MAP = {
|
|
2786
|
+
// ===== STRUCT =====
|
|
2787
|
+
"STRUCT-001": {
|
|
2788
|
+
guidance: "Create a SKILL.md file in the skill directory with valid YAML frontmatter and a body section.",
|
|
2789
|
+
effort: "low"
|
|
2790
|
+
},
|
|
2791
|
+
"STRUCT-002": {
|
|
2792
|
+
guidance: "Fix the YAML frontmatter syntax. Ensure it is enclosed between --- delimiters and contains valid YAML.",
|
|
2793
|
+
effort: "low"
|
|
2794
|
+
},
|
|
2795
|
+
"STRUCT-003": {
|
|
2796
|
+
guidance: "Add a `name` field to the frontmatter, e.g. `name: my-skill`.",
|
|
2797
|
+
effort: "low"
|
|
2798
|
+
},
|
|
2799
|
+
"STRUCT-004": {
|
|
2800
|
+
guidance: "Add a `description` field to the frontmatter explaining what the skill does.",
|
|
2801
|
+
effort: "low"
|
|
2802
|
+
},
|
|
2803
|
+
"STRUCT-005": {
|
|
2804
|
+
guidance: "Expand the body section with meaningful instructions. A skill body should describe the workflow and expected behavior.",
|
|
2805
|
+
effort: "medium"
|
|
2806
|
+
},
|
|
2807
|
+
"STRUCT-006": {
|
|
2808
|
+
guidance: "Remove binary, executable, or script files from the skill directory. Skills should contain only text-based configuration and documentation.",
|
|
2809
|
+
effort: "low"
|
|
2810
|
+
},
|
|
2811
|
+
"STRUCT-007": {
|
|
2812
|
+
guidance: "Use lowercase kebab-case for the skill name (e.g. `my-skill-name`). Avoid special characters or spaces.",
|
|
2813
|
+
effort: "low"
|
|
2814
|
+
},
|
|
2815
|
+
"STRUCT-008": {
|
|
2816
|
+
guidance: "Ensure the skill directory structure is standard. This warning indicates some files could not be scanned.",
|
|
2817
|
+
effort: "low"
|
|
2818
|
+
},
|
|
2819
|
+
// ===== CONT =====
|
|
2820
|
+
"CONT-001": {
|
|
2821
|
+
guidance: "Replace TODO/FIXME/placeholder text with actual content. If the skill instructs creating placeholders, use `<!-- skill-checker-ignore CONT-001 -->` before the line.",
|
|
2822
|
+
effort: "low"
|
|
2823
|
+
},
|
|
2824
|
+
"CONT-002": {
|
|
2825
|
+
guidance: "Remove lorem ipsum filler text and replace with real skill instructions.",
|
|
2826
|
+
effort: "low"
|
|
2827
|
+
},
|
|
2828
|
+
"CONT-003": {
|
|
2829
|
+
guidance: "Reduce duplicated lines. Consolidate repeated instructions into a single clear statement.",
|
|
2830
|
+
effort: "medium"
|
|
2831
|
+
},
|
|
2832
|
+
"CONT-004": {
|
|
2833
|
+
guidance: "Align the frontmatter description with the body content. The description should accurately summarize what the skill does.",
|
|
2834
|
+
effort: "medium"
|
|
2835
|
+
},
|
|
2836
|
+
"CONT-005": {
|
|
2837
|
+
guidance: "Remove promotional or advertising content. If describing pricing/marketing concepts for educational purposes, use `<!-- skill-checker-ignore CONT-005 -->` before the line.",
|
|
2838
|
+
effort: "low"
|
|
2839
|
+
},
|
|
2840
|
+
"CONT-006": {
|
|
2841
|
+
guidance: "Add more instructional prose around code examples. A skill should explain when and how to use the code, not just provide raw snippets.",
|
|
2842
|
+
effort: "medium"
|
|
2843
|
+
},
|
|
2844
|
+
"CONT-007": {
|
|
2845
|
+
guidance: "Ensure the skill body references the capabilities implied by the skill name, or rename the skill to match its actual functionality.",
|
|
2846
|
+
effort: "medium"
|
|
2847
|
+
},
|
|
2848
|
+
// ===== INJ =====
|
|
2849
|
+
"INJ-001": {
|
|
2850
|
+
guidance: 'Remove zero-width Unicode characters from the file. These can hide malicious content. Use a hex editor or run: `sed -i "" "s/[\\x200B\\x200C\\x200D\\xFEFF]//g" SKILL.md`.',
|
|
2851
|
+
effort: "low"
|
|
2852
|
+
},
|
|
2853
|
+
"INJ-002": {
|
|
2854
|
+
guidance: "Replace homoglyph characters (e.g. Cyrillic/Greek lookalikes) with their ASCII equivalents to prevent spoofing.",
|
|
2855
|
+
effort: "low"
|
|
2856
|
+
},
|
|
2857
|
+
"INJ-003": {
|
|
2858
|
+
guidance: "Remove RTL/bidirectional override characters. These manipulate text display and can disguise malicious content.",
|
|
2859
|
+
effort: "low"
|
|
2860
|
+
},
|
|
2861
|
+
"INJ-004": {
|
|
2862
|
+
guidance: 'Remove instructions that attempt to override the system prompt. A skill must not contain "ignore previous instructions" or similar patterns.',
|
|
2863
|
+
effort: "low"
|
|
2864
|
+
},
|
|
2865
|
+
"INJ-005": {
|
|
2866
|
+
guidance: "Remove patterns that simulate or manipulate tool output (e.g. `permissionDecision`, `tool_result`). Skills must not forge tool responses.",
|
|
2867
|
+
effort: "low"
|
|
2868
|
+
},
|
|
2869
|
+
"INJ-006": {
|
|
2870
|
+
guidance: "Remove instruction-like content from HTML comments. If comments are needed for documentation, ensure they do not contain executable directives.",
|
|
2871
|
+
effort: "low"
|
|
2872
|
+
},
|
|
2873
|
+
"INJ-007": {
|
|
2874
|
+
guidance: "Remove special model/system tags (e.g. `<|im_start|>`, `<system>`). These attempt to inject into the model context boundary.",
|
|
2875
|
+
effort: "low"
|
|
2876
|
+
},
|
|
2877
|
+
"INJ-008": {
|
|
2878
|
+
guidance: "Remove base64-encoded strings that decode to instructions. Legitimate data should be stored in plain text or referenced via file paths.",
|
|
2879
|
+
effort: "low"
|
|
2880
|
+
},
|
|
2881
|
+
"INJ-009": {
|
|
2882
|
+
guidance: "Avoid delimiter patterns that mimic system/instruction boundaries (e.g. `[SYSTEM]`, `[INST]`). Use standard Markdown headers instead.",
|
|
2883
|
+
effort: "low"
|
|
2884
|
+
},
|
|
2885
|
+
"INJ-010": {
|
|
2886
|
+
guidance: "Remove social engineering patterns (identity hijacking, deception, config tampering, verification bypass). Skills must not attempt to alter model identity or hide actions from users.",
|
|
2887
|
+
effort: "low"
|
|
2888
|
+
},
|
|
2889
|
+
// ===== CODE =====
|
|
2890
|
+
"CODE-001": {
|
|
2891
|
+
guidance: "Remove eval(), Function(), or dynamic code execution. Use explicit function calls or tool APIs instead.",
|
|
2892
|
+
effort: "medium"
|
|
2893
|
+
},
|
|
2894
|
+
"CODE-002": {
|
|
2895
|
+
guidance: "Remove shell/subprocess calls. Use tool APIs instead. If shell access is essential (e.g. calling soffice), use `<!-- skill-checker-ignore CODE-002 -->` and explain the necessity.",
|
|
2896
|
+
effort: "medium"
|
|
2897
|
+
},
|
|
2898
|
+
"CODE-003": {
|
|
2899
|
+
guidance: "Remove destructive file operations (rm -rf, rmdir, unlink on broad paths). If file cleanup is needed, target specific files with explicit paths.",
|
|
2900
|
+
effort: "medium"
|
|
2901
|
+
},
|
|
2902
|
+
"CODE-004": {
|
|
2903
|
+
guidance: "Review hardcoded URLs. Ensure all external URLs use HTTPS and point to trusted domains. If URLs are necessary API endpoints, use `<!-- skill-checker-ignore CODE-004 -->`.",
|
|
2904
|
+
effort: "medium"
|
|
2905
|
+
},
|
|
2906
|
+
"CODE-005": {
|
|
2907
|
+
guidance: "Restrict file writes to the skill working directory. Avoid writing to system paths, home directories, or parent directories.",
|
|
2908
|
+
effort: "medium"
|
|
2909
|
+
},
|
|
2910
|
+
"CODE-006": {
|
|
2911
|
+
guidance: "Avoid direct environment variable access for sensitive data. If env vars are needed for configuration, document which variables and why.",
|
|
2912
|
+
effort: "medium"
|
|
2913
|
+
},
|
|
2914
|
+
"CODE-007": {
|
|
2915
|
+
guidance: "Review long encoded strings. If they contain legitimate data (e.g. embedded assets), add a comment explaining their purpose.",
|
|
2916
|
+
effort: "low"
|
|
2917
|
+
},
|
|
2918
|
+
"CODE-008": {
|
|
2919
|
+
guidance: "Review high-entropy strings. These may be embedded secrets or obfuscated code. Replace with references to external configuration.",
|
|
2920
|
+
effort: "medium"
|
|
2921
|
+
},
|
|
2922
|
+
"CODE-009": {
|
|
2923
|
+
guidance: "Remove multi-layer encoding (e.g. base64 within hex). Legitimate content should use a single encoding layer at most.",
|
|
2924
|
+
effort: "low"
|
|
2925
|
+
},
|
|
2926
|
+
"CODE-010": {
|
|
2927
|
+
guidance: "Remove dynamic code generation patterns. Use static code with explicit function calls instead of constructing code at runtime.",
|
|
2928
|
+
effort: "medium"
|
|
2929
|
+
},
|
|
2930
|
+
"CODE-011": {
|
|
2931
|
+
guidance: "Use descriptive variable names instead of obfuscated single-character or hex-named variables.",
|
|
2932
|
+
effort: "low"
|
|
2933
|
+
},
|
|
2934
|
+
"CODE-012": {
|
|
2935
|
+
guidance: "Remove permission escalation commands (sudo, chmod 777, setuid). Skills should operate within normal user permissions.",
|
|
2936
|
+
effort: "low"
|
|
2937
|
+
},
|
|
2938
|
+
"CODE-013": {
|
|
2939
|
+
guidance: "Remove hardcoded API keys, tokens, or credentials. Use environment variables or a secrets manager, and add patterns to .gitignore.",
|
|
2940
|
+
effort: "medium"
|
|
2941
|
+
},
|
|
2942
|
+
"CODE-014": {
|
|
2943
|
+
guidance: "Remove reverse shell patterns. These are indicators of malicious intent and have no legitimate use in skills.",
|
|
2944
|
+
effort: "low"
|
|
2945
|
+
},
|
|
2946
|
+
"CODE-015": {
|
|
2947
|
+
guidance: "Remove remote pipeline execution or data exfiltration patterns (e.g. curl | sh, piping sensitive data to external endpoints).",
|
|
2948
|
+
effort: "low"
|
|
2949
|
+
},
|
|
2950
|
+
"CODE-016": {
|
|
2951
|
+
guidance: "Remove persistence mechanism patterns (cron jobs, launchd plists, shell profile modifications, autostart entries). Skills should not install persistent system services.",
|
|
2952
|
+
effort: "medium"
|
|
2953
|
+
},
|
|
2954
|
+
// ===== SUPPLY =====
|
|
2955
|
+
"SUPPLY-001": {
|
|
2956
|
+
guidance: "Review the MCP server reference. Ensure it points to a trusted, well-known server. Document why external MCP access is needed.",
|
|
2957
|
+
effort: "medium"
|
|
2958
|
+
},
|
|
2959
|
+
"SUPPLY-002": {
|
|
2960
|
+
guidance: "Avoid `npx -y` auto-install. Pin the package version and verify the package before use. Use `npx package@version` instead.",
|
|
2961
|
+
effort: "low"
|
|
2962
|
+
},
|
|
2963
|
+
"SUPPLY-003": {
|
|
2964
|
+
guidance: "Review package installation commands. Pin exact versions and verify package authenticity. Avoid installing packages from untrusted sources.",
|
|
2965
|
+
effort: "medium"
|
|
2966
|
+
},
|
|
2967
|
+
"SUPPLY-004": {
|
|
2968
|
+
guidance: "Use HTTPS instead of HTTP for all URLs to prevent man-in-the-middle attacks.",
|
|
2969
|
+
effort: "low"
|
|
2970
|
+
},
|
|
2971
|
+
"SUPPLY-005": {
|
|
2972
|
+
guidance: "Replace raw IP addresses with domain names. IP-based URLs bypass DNS security controls and are harder to audit.",
|
|
2973
|
+
effort: "low"
|
|
2974
|
+
},
|
|
2975
|
+
"SUPPLY-006": {
|
|
2976
|
+
guidance: "Review git clone commands. Ensure repositories are from trusted sources. Pin to a specific commit hash or tag for reproducibility.",
|
|
2977
|
+
effort: "medium"
|
|
2978
|
+
},
|
|
2979
|
+
"SUPPLY-007": {
|
|
2980
|
+
guidance: "Remove references to known suspicious domains. These domains have been flagged for malicious activity.",
|
|
2981
|
+
effort: "low"
|
|
2982
|
+
},
|
|
2983
|
+
"SUPPLY-008": {
|
|
2984
|
+
guidance: "Remove files matching known malicious hashes. These files have been identified as malware or exploits.",
|
|
2985
|
+
effort: "low"
|
|
2986
|
+
},
|
|
2987
|
+
"SUPPLY-009": {
|
|
2988
|
+
guidance: "Remove references to known command-and-control IP addresses. These IPs are associated with malware infrastructure.",
|
|
2989
|
+
effort: "low"
|
|
2990
|
+
},
|
|
2991
|
+
"SUPPLY-010": {
|
|
2992
|
+
guidance: "Verify the package name is correct and not a typosquat of a popular package. Check the official registry for the canonical name.",
|
|
2993
|
+
effort: "low"
|
|
2994
|
+
},
|
|
2995
|
+
// ===== RES =====
|
|
2996
|
+
"RES-001": {
|
|
2997
|
+
guidance: 'Remove recursive or repetitive task patterns (e.g. "repeat 1000 times"). Use bounded iteration with a clear termination condition.',
|
|
2998
|
+
effort: "low"
|
|
2999
|
+
},
|
|
3000
|
+
"RES-002": {
|
|
3001
|
+
guidance: "Request only the specific tools needed instead of unrestricted access. List individual tools in allowed-tools.",
|
|
3002
|
+
effort: "medium"
|
|
3003
|
+
},
|
|
3004
|
+
"RES-003": {
|
|
3005
|
+
guidance: "Reduce the number of allowed tools. Review if all listed tools are actually needed. Consider splitting into multiple focused skills.",
|
|
3006
|
+
effort: "medium"
|
|
3007
|
+
},
|
|
3008
|
+
"RES-004": {
|
|
3009
|
+
guidance: "Remove instructions that disable safety checks, hooks, or verification. Skills must operate within the security framework.",
|
|
3010
|
+
effort: "low"
|
|
3011
|
+
},
|
|
3012
|
+
"RES-005": {
|
|
3013
|
+
guidance: 'Remove token-wasting patterns (e.g. "repeat in every response"). Keep instructions concise and avoid mandating boilerplate in every output.',
|
|
3014
|
+
effort: "low"
|
|
3015
|
+
},
|
|
3016
|
+
"RES-006": {
|
|
3017
|
+
guidance: "Remove instructions to ignore CLAUDE.md or project rules. Skills must respect the project configuration set by the user.",
|
|
3018
|
+
effort: "low"
|
|
3019
|
+
}
|
|
3020
|
+
};
|
|
3021
|
+
function getRemediation(ruleId) {
|
|
3022
|
+
return REMEDIATION_MAP[ruleId];
|
|
3023
|
+
}
|
|
3024
|
+
|
|
2651
3025
|
// src/scanner.ts
|
|
2652
3026
|
function scanSkillDirectory(dirPath, config = DEFAULT_CONFIG) {
|
|
2653
3027
|
const skill = parseSkill(dirPath);
|
|
@@ -2655,6 +3029,15 @@ function scanSkillDirectory(dirPath, config = DEFAULT_CONFIG) {
|
|
|
2655
3029
|
}
|
|
2656
3030
|
function buildReport(skill, config) {
|
|
2657
3031
|
let results = runAllChecks(skill);
|
|
3032
|
+
let suppressedResults = [];
|
|
3033
|
+
let suppressionWarnings = [];
|
|
3034
|
+
if (!config.noIgnoreInline) {
|
|
3035
|
+
const directives = parseSuppressionDirectives(skill.raw.split("\n"));
|
|
3036
|
+
const sr = applySuppressions(results, directives, skill.bodyStartLine);
|
|
3037
|
+
results = sr.active;
|
|
3038
|
+
suppressedResults = sr.suppressed;
|
|
3039
|
+
suppressionWarnings = sr.warnings;
|
|
3040
|
+
}
|
|
2658
3041
|
results = results.map((r) => {
|
|
2659
3042
|
if (config.overrides[r.id]) {
|
|
2660
3043
|
return { ...r, severity: config.overrides[r.id] };
|
|
@@ -2663,6 +3046,10 @@ function buildReport(skill, config) {
|
|
|
2663
3046
|
});
|
|
2664
3047
|
results = results.filter((r) => !config.ignore.includes(r.id));
|
|
2665
3048
|
results = deduplicateResults(results);
|
|
3049
|
+
results = results.map((r) => {
|
|
3050
|
+
const rem = getRemediation(r.id);
|
|
3051
|
+
return rem ? { ...r, remediation: rem.guidance } : r;
|
|
3052
|
+
});
|
|
2666
3053
|
const score = calculateScore(results);
|
|
2667
3054
|
const grade = computeGrade(score);
|
|
2668
3055
|
const summary = {
|
|
@@ -2679,7 +3066,9 @@ function buildReport(skill, config) {
|
|
|
2679
3066
|
results,
|
|
2680
3067
|
score,
|
|
2681
3068
|
grade,
|
|
2682
|
-
summary
|
|
3069
|
+
summary,
|
|
3070
|
+
suppressedResults,
|
|
3071
|
+
suppressionWarnings
|
|
2683
3072
|
};
|
|
2684
3073
|
}
|
|
2685
3074
|
function deduplicateResults(results) {
|
|
@@ -2692,7 +3081,9 @@ function deduplicateResults(results) {
|
|
|
2692
3081
|
};
|
|
2693
3082
|
for (const r of results) {
|
|
2694
3083
|
const sourceKey = r.source ?? `_no_source_:${r.category}:${r.line ?? ""}`;
|
|
2695
|
-
const
|
|
3084
|
+
const isSecurityLineLevel = r.source === "SKILL.md" && r.line !== void 0 && (r.category === "INJ" || r.category === "RES");
|
|
3085
|
+
const lineKey = isSecurityLineLevel ? `:${r.line}` : "";
|
|
3086
|
+
const key = `${r.id}::${r.title}::${sourceKey}${lineKey}`;
|
|
2696
3087
|
const group = groups.get(key);
|
|
2697
3088
|
if (group) {
|
|
2698
3089
|
group.push(r);
|
|
@@ -2803,10 +3194,29 @@ function formatTerminalReport(report) {
|
|
|
2803
3194
|
if (f.snippet) {
|
|
2804
3195
|
lines.push(` ${chalk.dim(f.snippet)}`);
|
|
2805
3196
|
}
|
|
3197
|
+
if (f.remediation) {
|
|
3198
|
+
lines.push(` ${chalk.dim("Fix: " + f.remediation)}`);
|
|
3199
|
+
}
|
|
2806
3200
|
}
|
|
2807
3201
|
lines.push("");
|
|
2808
3202
|
}
|
|
2809
3203
|
}
|
|
3204
|
+
if (report.suppressedResults && report.suppressedResults.length > 0) {
|
|
3205
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3206
|
+
for (const s of report.suppressedResults) {
|
|
3207
|
+
counts.set(s.id, (counts.get(s.id) ?? 0) + 1);
|
|
3208
|
+
}
|
|
3209
|
+
const detail = Array.from(counts.entries()).map(([id, n]) => `${id}: ${n}`).join(", ");
|
|
3210
|
+
lines.push(chalk.dim(`Suppressed: ${report.suppressedResults.length} finding(s) (${detail})`));
|
|
3211
|
+
lines.push("");
|
|
3212
|
+
}
|
|
3213
|
+
if (report.suppressionWarnings && report.suppressionWarnings.length > 0) {
|
|
3214
|
+
lines.push(chalk.yellow("Suppression warnings:"));
|
|
3215
|
+
for (const w of report.suppressionWarnings) {
|
|
3216
|
+
lines.push(chalk.yellow(` - ${w}`));
|
|
3217
|
+
}
|
|
3218
|
+
lines.push("");
|
|
3219
|
+
}
|
|
2810
3220
|
lines.push(chalk.bold("Recommendation:"));
|
|
2811
3221
|
switch (report.grade) {
|
|
2812
3222
|
case "A":
|
|
@@ -2841,10 +3251,15 @@ function formatJsonReport(report) {
|
|
|
2841
3251
|
}
|
|
2842
3252
|
function generateHookResponse(report, config = DEFAULT_CONFIG) {
|
|
2843
3253
|
const worst = worstSeverity(report.results);
|
|
2844
|
-
|
|
3254
|
+
const suppressedWorst = report.suppressedResults ? worstSeverity(report.suppressedResults) : null;
|
|
3255
|
+
const hasSuppressedCriticalOrHigh = suppressedWorst === "CRITICAL" || suppressedWorst === "HIGH";
|
|
3256
|
+
if (!worst && !hasSuppressedCriticalOrHigh) {
|
|
2845
3257
|
return { permissionDecision: "allow" };
|
|
2846
3258
|
}
|
|
2847
|
-
|
|
3259
|
+
let action = worst ? getHookAction(config.policy, worst) : "report";
|
|
3260
|
+
if (hasSuppressedCriticalOrHigh && (action === "report" || !worst)) {
|
|
3261
|
+
action = "ask";
|
|
3262
|
+
}
|
|
2848
3263
|
switch (action) {
|
|
2849
3264
|
case "deny":
|
|
2850
3265
|
return {
|
|
@@ -2988,7 +3403,7 @@ program.name("skill-checker").description(
|
|
|
2988
3403
|
"Security checker for Claude Code skills - detect injection, malicious code, and supply chain risks"
|
|
2989
3404
|
).version(pkg.version);
|
|
2990
3405
|
var VALID_POLICIES = ["strict", "balanced", "permissive"];
|
|
2991
|
-
program.command("scan").description("Scan a skill directory for security issues").argument("<path>", "Path to the skill directory").option("-f, --format <format>", "Output format: terminal, json, hook", "terminal").option("-p, --policy <policy>", "Policy: strict, balanced, permissive").option("-c, --config <path>", "Path to config file").action(
|
|
3406
|
+
program.command("scan").description("Scan a skill directory for security issues").argument("<path>", "Path to the skill directory").option("-f, --format <format>", "Output format: terminal, json, hook", "terminal").option("-p, --policy <policy>", "Policy: strict, balanced, permissive").option("-c, --config <path>", "Path to config file").option("--no-ignore", "Disable inline suppression comments").action(
|
|
2992
3407
|
(path, opts) => {
|
|
2993
3408
|
if (opts.policy && !VALID_POLICIES.includes(opts.policy)) {
|
|
2994
3409
|
console.error(`Error: invalid policy "${opts.policy}". Valid values: ${VALID_POLICIES.join(", ")}`);
|
|
@@ -3003,6 +3418,9 @@ program.command("scan").description("Scan a skill directory for security issues"
|
|
|
3003
3418
|
if (opts.policy) {
|
|
3004
3419
|
config.policy = opts.policy;
|
|
3005
3420
|
}
|
|
3421
|
+
if (!opts.ignore) {
|
|
3422
|
+
config.noIgnoreInline = true;
|
|
3423
|
+
}
|
|
3006
3424
|
const report = scanSkillDirectory(path, config);
|
|
3007
3425
|
switch (opts.format) {
|
|
3008
3426
|
case "json":
|