reskill 1.15.0 → 1.16.0-beta.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/dist/cli/commands/publish.d.ts.map +1 -1
- package/dist/cli/index.js +369 -0
- package/dist/core/content-scanner.d.ts +113 -0
- package/dist/core/content-scanner.d.ts.map +1 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +354 -1
- package/dist/scanner.js +355 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +5 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/publish.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/publish.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmDpC;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CAcR;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAYpE;AA2ND;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAI1D;AAoBD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,CAyBnE;AA+SD,eAAO,MAAM,cAAc,SAYH,CAAC;AAEzB,eAAe,cAAc,CAAC"}
|
package/dist/cli/index.js
CHANGED
|
@@ -8073,6 +8073,344 @@ class SkillValidator {
|
|
|
8073
8073
|
return `sha256-${hash.digest('hex')}`;
|
|
8074
8074
|
}
|
|
8075
8075
|
}
|
|
8076
|
+
/**
|
|
8077
|
+
* ContentScanner - Detect malicious patterns in SKILL.md content
|
|
8078
|
+
*
|
|
8079
|
+
* Features:
|
|
8080
|
+
* - Context-aware: skips safe zones (frontmatter, code blocks, quotes, blockquotes)
|
|
8081
|
+
* - 6 built-in detection rules across 3 risk levels
|
|
8082
|
+
* - Configurable: override levels, disable rules, add custom rules
|
|
8083
|
+
* - Pure string operations in scan() — no fs dependency, suitable for server use
|
|
8084
|
+
* - scanFile() convenience method for CLI use
|
|
8085
|
+
*/ // ============================================================================
|
|
8086
|
+
// Safe Zone Masking
|
|
8087
|
+
// ============================================================================
|
|
8088
|
+
/**
|
|
8089
|
+
* Mask safe zones in Markdown content with spaces, preserving line structure.
|
|
8090
|
+
*
|
|
8091
|
+
* Safe zones (content replaced with spaces):
|
|
8092
|
+
* - YAML frontmatter (`---` ... `---` at file start)
|
|
8093
|
+
* - Fenced code blocks (``` or ~~~)
|
|
8094
|
+
* - Indented code blocks (4 spaces / tab after blank line)
|
|
8095
|
+
* - Blockquotes (`> ` prefix)
|
|
8096
|
+
* - Inline code (`` `...` ``)
|
|
8097
|
+
* - Double-quoted text (`"..."`, min 3 chars between quotes)
|
|
8098
|
+
*
|
|
8099
|
+
* Line breaks are preserved so line numbers remain correct.
|
|
8100
|
+
*/ function maskSafeZones(content) {
|
|
8101
|
+
const lines = content.split('\n');
|
|
8102
|
+
const result = [];
|
|
8103
|
+
let inFrontmatter = false;
|
|
8104
|
+
let inFencedCode = false;
|
|
8105
|
+
let fenceChar = '';
|
|
8106
|
+
let fenceLength = 0;
|
|
8107
|
+
let prevLineBlank = false;
|
|
8108
|
+
let prevLineIndentedCode = false;
|
|
8109
|
+
for(let i = 0; i < lines.length; i++){
|
|
8110
|
+
const line = lines[i];
|
|
8111
|
+
// --- YAML Frontmatter (only at file start) ---
|
|
8112
|
+
if (0 === i && '---' === line.trim()) {
|
|
8113
|
+
inFrontmatter = true;
|
|
8114
|
+
result.push(maskLine(line));
|
|
8115
|
+
continue;
|
|
8116
|
+
}
|
|
8117
|
+
if (inFrontmatter) {
|
|
8118
|
+
result.push(maskLine(line));
|
|
8119
|
+
if ('---' === line.trim()) inFrontmatter = false;
|
|
8120
|
+
continue;
|
|
8121
|
+
}
|
|
8122
|
+
// --- Fenced code blocks (``` or ~~~) ---
|
|
8123
|
+
const fenceMatch = line.match(/^(`{3,}|~{3,})/);
|
|
8124
|
+
if (!inFencedCode && fenceMatch) {
|
|
8125
|
+
inFencedCode = true;
|
|
8126
|
+
fenceChar = fenceMatch[1][0];
|
|
8127
|
+
fenceLength = fenceMatch[1].length;
|
|
8128
|
+
result.push(maskLine(line));
|
|
8129
|
+
prevLineBlank = false;
|
|
8130
|
+
prevLineIndentedCode = false;
|
|
8131
|
+
continue;
|
|
8132
|
+
}
|
|
8133
|
+
if (inFencedCode) {
|
|
8134
|
+
result.push(maskLine(line));
|
|
8135
|
+
const closeMatch = line.match(/^(`{3,}|~{3,})\s*$/);
|
|
8136
|
+
if (closeMatch && closeMatch[1][0] === fenceChar && closeMatch[1].length >= fenceLength) inFencedCode = false;
|
|
8137
|
+
prevLineBlank = false;
|
|
8138
|
+
prevLineIndentedCode = false;
|
|
8139
|
+
continue;
|
|
8140
|
+
}
|
|
8141
|
+
// --- Blockquote ---
|
|
8142
|
+
if (/^>\s?/.test(line)) {
|
|
8143
|
+
result.push(maskLine(line));
|
|
8144
|
+
prevLineBlank = false;
|
|
8145
|
+
prevLineIndentedCode = false;
|
|
8146
|
+
continue;
|
|
8147
|
+
}
|
|
8148
|
+
// --- Indented code block (4 spaces or tab, after blank line) ---
|
|
8149
|
+
if (/^(?: |\t)/.test(line) && (prevLineBlank || prevLineIndentedCode)) {
|
|
8150
|
+
result.push(maskLine(line));
|
|
8151
|
+
prevLineBlank = false;
|
|
8152
|
+
prevLineIndentedCode = true;
|
|
8153
|
+
continue;
|
|
8154
|
+
}
|
|
8155
|
+
// --- Normal line: mask inline code and double-quoted text ---
|
|
8156
|
+
result.push(maskInline(line));
|
|
8157
|
+
prevLineBlank = '' === line.trim();
|
|
8158
|
+
prevLineIndentedCode = false;
|
|
8159
|
+
}
|
|
8160
|
+
return result.join('\n');
|
|
8161
|
+
}
|
|
8162
|
+
/** Replace all characters in a line with spaces (preserving length) */ function maskLine(line) {
|
|
8163
|
+
return ' '.repeat(line.length);
|
|
8164
|
+
}
|
|
8165
|
+
/**
|
|
8166
|
+
* Mask inline code (`` `...` ``) and double-quoted text (`"..."`) within a line.
|
|
8167
|
+
* Uses regex replacement for efficiency (avoids char-by-char concatenation on long lines).
|
|
8168
|
+
* Single quotes are NOT masked to avoid false matches with apostrophes.
|
|
8169
|
+
*/ function maskInline(line) {
|
|
8170
|
+
let result = line;
|
|
8171
|
+
// Inline code: `...`
|
|
8172
|
+
result = result.replace(/`[^`]+`/g, (m)=>' '.repeat(m.length));
|
|
8173
|
+
// Double-quoted text: "..." (min 3 chars between quotes)
|
|
8174
|
+
result = result.replace(/"[^"]{3,}"/g, (m)=>' '.repeat(m.length));
|
|
8175
|
+
return result;
|
|
8176
|
+
}
|
|
8177
|
+
// ============================================================================
|
|
8178
|
+
// Rule Helpers
|
|
8179
|
+
// ============================================================================
|
|
8180
|
+
/** Find lines matching any of the given patterns, return one match per line */ function findLineMatches(content, patterns) {
|
|
8181
|
+
const lines = content.split('\n');
|
|
8182
|
+
const matches = [];
|
|
8183
|
+
for(let i = 0; i < lines.length; i++)for (const pattern of patterns)if (pattern.test(lines[i])) {
|
|
8184
|
+
matches.push({
|
|
8185
|
+
line: i + 1
|
|
8186
|
+
});
|
|
8187
|
+
break;
|
|
8188
|
+
}
|
|
8189
|
+
return matches;
|
|
8190
|
+
}
|
|
8191
|
+
// ============================================================================
|
|
8192
|
+
// Default Rules
|
|
8193
|
+
// ============================================================================
|
|
8194
|
+
const SNIPPET_MAX_LENGTH = 120;
|
|
8195
|
+
/** Built-in detection rules */ const DEFAULT_RULES = [
|
|
8196
|
+
// Rule 1: Prompt Injection (high)
|
|
8197
|
+
{
|
|
8198
|
+
id: 'prompt-injection',
|
|
8199
|
+
level: 'high',
|
|
8200
|
+
message: 'Detected prompt injection attempt',
|
|
8201
|
+
skipSafeZones: true,
|
|
8202
|
+
check: (content)=>findLineMatches(content, [
|
|
8203
|
+
/ignore\s+(all\s+)?previous\s+instructions/i,
|
|
8204
|
+
/disregard\s+(all\s+)?(prior|previous|above)\s+(instructions|rules|context)/i,
|
|
8205
|
+
/you\s+are\s+now\s+/i,
|
|
8206
|
+
/from\s+now\s+on[,\s]+you\s+are/i,
|
|
8207
|
+
/new\s+system\s+prompt/i,
|
|
8208
|
+
/override\s+(your|the)\s+(system|safety|security)\s+(prompt|rules|instructions)/i,
|
|
8209
|
+
/forget\s+(?:all\s+)?(?:your\s+)?(?:previous\s+|prior\s+)?(?:instructions|rules|constraints)/i,
|
|
8210
|
+
/entering\s+(a\s+)?new\s+(mode|context|session)/i
|
|
8211
|
+
])
|
|
8212
|
+
},
|
|
8213
|
+
// Rule 2: Data Exfiltration (high)
|
|
8214
|
+
{
|
|
8215
|
+
id: 'data-exfiltration',
|
|
8216
|
+
level: 'high',
|
|
8217
|
+
message: 'Detected potential data exfiltration command',
|
|
8218
|
+
skipSafeZones: true,
|
|
8219
|
+
check: (content)=>{
|
|
8220
|
+
const lines = content.split('\n');
|
|
8221
|
+
const matches = [];
|
|
8222
|
+
const commandPattern = /\b(curl|wget|fetch|http\.post|requests\.post|nc\b|ncat|netcat)\b/i;
|
|
8223
|
+
const sensitivePattern = /(\$[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH)[A-Z_]*|\$ENV\b|\$\{[^}]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)[^}]*\})/i;
|
|
8224
|
+
for(let i = 0; i < lines.length; i++)if (commandPattern.test(lines[i]) && sensitivePattern.test(lines[i])) matches.push({
|
|
8225
|
+
line: i + 1
|
|
8226
|
+
});
|
|
8227
|
+
return matches;
|
|
8228
|
+
}
|
|
8229
|
+
},
|
|
8230
|
+
// Rule 3: Content Obfuscation (high) — scans ALL content including safe zones
|
|
8231
|
+
{
|
|
8232
|
+
id: 'obfuscation',
|
|
8233
|
+
level: 'high',
|
|
8234
|
+
message: 'Detected content obfuscation',
|
|
8235
|
+
skipSafeZones: false,
|
|
8236
|
+
check: (content)=>{
|
|
8237
|
+
const matches = [];
|
|
8238
|
+
const lines = content.split('\n');
|
|
8239
|
+
// Zero-width characters (suspicious in any context)
|
|
8240
|
+
const zeroWidthPattern = /[\u200B\u200C\u200D\uFEFF\u2060\u180E]/;
|
|
8241
|
+
for(let i = 0; i < lines.length; i++)if (zeroWidthPattern.test(lines[i])) matches.push({
|
|
8242
|
+
line: i + 1,
|
|
8243
|
+
snippet: 'Zero-width Unicode characters detected'
|
|
8244
|
+
});
|
|
8245
|
+
// Long base64-like strings (>200 continuous chars)
|
|
8246
|
+
const base64Pattern = /[A-Za-z0-9+/=]{200,}/;
|
|
8247
|
+
for(let i = 0; i < lines.length; i++)if (base64Pattern.test(lines[i])) matches.push({
|
|
8248
|
+
line: i + 1,
|
|
8249
|
+
snippet: 'Suspicious base64-encoded block detected'
|
|
8250
|
+
});
|
|
8251
|
+
// Large HTML comments (>200 chars of content)
|
|
8252
|
+
const commentRegex = /<!--([\s\S]{200,}?)-->/g;
|
|
8253
|
+
let match;
|
|
8254
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop
|
|
8255
|
+
while(null !== (match = commentRegex.exec(content))){
|
|
8256
|
+
const lineNum = content.slice(0, match.index).split('\n').length;
|
|
8257
|
+
matches.push({
|
|
8258
|
+
line: lineNum,
|
|
8259
|
+
snippet: `Large HTML comment block (${match[1].length} chars)`
|
|
8260
|
+
});
|
|
8261
|
+
}
|
|
8262
|
+
return matches;
|
|
8263
|
+
}
|
|
8264
|
+
},
|
|
8265
|
+
// Rule 4: Sensitive File Access (medium)
|
|
8266
|
+
{
|
|
8267
|
+
id: 'sensitive-file-access',
|
|
8268
|
+
level: 'medium',
|
|
8269
|
+
message: 'References sensitive file path',
|
|
8270
|
+
skipSafeZones: true,
|
|
8271
|
+
check: (content)=>findLineMatches(content, [
|
|
8272
|
+
/~\/\.ssh\b/,
|
|
8273
|
+
/~\/\.aws\b/,
|
|
8274
|
+
/~\/\.gnupg\b/,
|
|
8275
|
+
/~\/\.config\/gcloud\b/,
|
|
8276
|
+
/\bid_rsa\b/i,
|
|
8277
|
+
/\bid_ed25519\b/i,
|
|
8278
|
+
/\/etc\/passwd\b/,
|
|
8279
|
+
/\/etc\/shadow\b/,
|
|
8280
|
+
/\.env\b(?!\.\w)/
|
|
8281
|
+
])
|
|
8282
|
+
},
|
|
8283
|
+
// Rule 5: Stealth Instructions (medium) — phrase + action verb matching
|
|
8284
|
+
{
|
|
8285
|
+
id: 'stealth-instructions',
|
|
8286
|
+
level: 'medium',
|
|
8287
|
+
message: 'Detected instruction to hide actions from user',
|
|
8288
|
+
skipSafeZones: true,
|
|
8289
|
+
check: (content)=>{
|
|
8290
|
+
const actionVerbs = 'execute|delete|remove|send|transmit|modify|overwrite|install|download|upload|run|write|create|destroy|drop';
|
|
8291
|
+
const patterns = [
|
|
8292
|
+
new RegExp(`silently\\s+(?:${actionVerbs})`, 'i'),
|
|
8293
|
+
new RegExp(`without\\s+telling\\s+the\\s+user.{0,30}(?:${actionVerbs})`, 'i'),
|
|
8294
|
+
new RegExp("(?:do\\s+not|don'?t)\\s+show\\s+.{0,40}(?:to\\s+the\\s+user|to\\s+user)", 'i'),
|
|
8295
|
+
new RegExp("hide\\s+(?:this|the|these|all)\\s+.{0,30}(?:from\\s+the\\s+user|from\\s+user)", 'i'),
|
|
8296
|
+
new RegExp("(?:do\\s+not|don'?t)\\s+mention\\s+.{0,30}(?:to\\s+the\\s+user|to\\s+user)", 'i'),
|
|
8297
|
+
new RegExp("keep\\s+(?:this|it)\\s+(?:a\\s+)?secret\\s+from\\s+(?:the\\s+)?user", 'i')
|
|
8298
|
+
];
|
|
8299
|
+
// Safe patterns to exclude (common in legitimate DevOps/automation skills)
|
|
8300
|
+
const safePatterns = [
|
|
8301
|
+
/silently\s+(?:ignore|skip|fail|discard|suppress|continue|pass|drop|swallow)/i
|
|
8302
|
+
];
|
|
8303
|
+
const lines = content.split('\n');
|
|
8304
|
+
const matches = [];
|
|
8305
|
+
for(let i = 0; i < lines.length; i++){
|
|
8306
|
+
const line = lines[i];
|
|
8307
|
+
if (!safePatterns.some((p)=>p.test(line))) {
|
|
8308
|
+
for (const pattern of patterns)if (pattern.test(line)) {
|
|
8309
|
+
matches.push({
|
|
8310
|
+
line: i + 1
|
|
8311
|
+
});
|
|
8312
|
+
break;
|
|
8313
|
+
}
|
|
8314
|
+
}
|
|
8315
|
+
}
|
|
8316
|
+
return matches;
|
|
8317
|
+
}
|
|
8318
|
+
},
|
|
8319
|
+
// Rule 6: Oversized Content (low) — scans ALL content
|
|
8320
|
+
{
|
|
8321
|
+
id: 'oversized-content',
|
|
8322
|
+
level: 'low',
|
|
8323
|
+
message: 'Content exceeds recommended size limit',
|
|
8324
|
+
skipSafeZones: false,
|
|
8325
|
+
check: (content)=>{
|
|
8326
|
+
const MAX_SIZE_BYTES = 51200;
|
|
8327
|
+
const sizeBytes = Buffer.byteLength(content, 'utf-8');
|
|
8328
|
+
if (sizeBytes > MAX_SIZE_BYTES) return [
|
|
8329
|
+
{
|
|
8330
|
+
snippet: `Content size: ${(sizeBytes / 1024).toFixed(1)}KB (limit: 50KB)`
|
|
8331
|
+
}
|
|
8332
|
+
];
|
|
8333
|
+
return [];
|
|
8334
|
+
}
|
|
8335
|
+
}
|
|
8336
|
+
];
|
|
8337
|
+
// ============================================================================
|
|
8338
|
+
// ContentScanner
|
|
8339
|
+
// ============================================================================
|
|
8340
|
+
/** Build the effective rule set from defaults + options */ function buildRuleSet(options) {
|
|
8341
|
+
let rules = DEFAULT_RULES.map((r)=>({
|
|
8342
|
+
...r
|
|
8343
|
+
}));
|
|
8344
|
+
if (options?.disabledRules?.length) {
|
|
8345
|
+
const disabled = new Set(options.disabledRules);
|
|
8346
|
+
rules = rules.filter((r)=>!disabled.has(r.id));
|
|
8347
|
+
}
|
|
8348
|
+
if (options?.overrides) for (const rule of rules){
|
|
8349
|
+
const override = options.overrides[rule.id];
|
|
8350
|
+
if (override) rule.level = override;
|
|
8351
|
+
}
|
|
8352
|
+
if (options?.customRules?.length) rules.push(...options.customRules);
|
|
8353
|
+
return rules;
|
|
8354
|
+
}
|
|
8355
|
+
/**
|
|
8356
|
+
* Content scanner for SKILL.md files.
|
|
8357
|
+
*
|
|
8358
|
+
* Detects prompt injection, data exfiltration, obfuscation, sensitive file
|
|
8359
|
+
* access, stealth instructions, and oversized content.
|
|
8360
|
+
*
|
|
8361
|
+
* @example
|
|
8362
|
+
* ```typescript
|
|
8363
|
+
* // Default usage (CLI)
|
|
8364
|
+
* const scanner = new ContentScanner();
|
|
8365
|
+
* const result = scanner.scan(content);
|
|
8366
|
+
*
|
|
8367
|
+
* // Custom usage (private registry server)
|
|
8368
|
+
* const scanner = new ContentScanner({
|
|
8369
|
+
* overrides: { 'prompt-injection': 'medium' },
|
|
8370
|
+
* disabledRules: ['stealth-instructions'],
|
|
8371
|
+
* });
|
|
8372
|
+
* ```
|
|
8373
|
+
*/ class ContentScanner {
|
|
8374
|
+
rules;
|
|
8375
|
+
constructor(options){
|
|
8376
|
+
this.rules = buildRuleSet(options);
|
|
8377
|
+
}
|
|
8378
|
+
/**
|
|
8379
|
+
* Scan content string for malicious patterns.
|
|
8380
|
+
* Pure string operation — no file system access.
|
|
8381
|
+
*/ scan(content) {
|
|
8382
|
+
const originalLines = content.split('\n');
|
|
8383
|
+
const maskedContent = maskSafeZones(content);
|
|
8384
|
+
const findings = [];
|
|
8385
|
+
for (const rule of this.rules){
|
|
8386
|
+
const targetContent = rule.skipSafeZones ? maskedContent : content;
|
|
8387
|
+
const matches = rule.check(targetContent);
|
|
8388
|
+
for (const match of matches){
|
|
8389
|
+
// Use custom snippet if provided, otherwise generate from original content
|
|
8390
|
+
const snippet = match.snippet ?? (null != match.line ? originalLines[match.line - 1]?.trim().slice(0, SNIPPET_MAX_LENGTH) : void 0);
|
|
8391
|
+
findings.push({
|
|
8392
|
+
rule: rule.id,
|
|
8393
|
+
level: rule.level,
|
|
8394
|
+
message: rule.message,
|
|
8395
|
+
line: match.line,
|
|
8396
|
+
snippet
|
|
8397
|
+
});
|
|
8398
|
+
}
|
|
8399
|
+
}
|
|
8400
|
+
const hasHighRisk = findings.some((f)=>'high' === f.level);
|
|
8401
|
+
return {
|
|
8402
|
+
passed: !hasHighRisk,
|
|
8403
|
+
findings
|
|
8404
|
+
};
|
|
8405
|
+
}
|
|
8406
|
+
/**
|
|
8407
|
+
* Scan a file for malicious patterns.
|
|
8408
|
+
* Convenience wrapper that reads the file then calls scan().
|
|
8409
|
+
*/ scanFile(filePath) {
|
|
8410
|
+
const content = external_node_fs_.readFileSync(filePath, 'utf-8');
|
|
8411
|
+
return this.scan(content);
|
|
8412
|
+
}
|
|
8413
|
+
}
|
|
8076
8414
|
/**
|
|
8077
8415
|
* publish command - Publish a skill to the registry
|
|
8078
8416
|
*
|
|
@@ -8372,6 +8710,25 @@ class SkillValidator {
|
|
|
8372
8710
|
logger_logger.newline();
|
|
8373
8711
|
logger_logger.log('No changes made (--dry-run)');
|
|
8374
8712
|
}
|
|
8713
|
+
/**
|
|
8714
|
+
* Display content scan findings
|
|
8715
|
+
*/ function displayScanFindings(scanResult) {
|
|
8716
|
+
if (0 === scanResult.findings.length) {
|
|
8717
|
+
logger_logger.log(' ✓ Content security scan passed');
|
|
8718
|
+
return;
|
|
8719
|
+
}
|
|
8720
|
+
logger_logger.newline();
|
|
8721
|
+
logger_logger.log('⚠ Content Security Scan:');
|
|
8722
|
+
logger_logger.newline();
|
|
8723
|
+
for (const finding of scanResult.findings){
|
|
8724
|
+
const levelTag = finding.level.toUpperCase().padEnd(6);
|
|
8725
|
+
const lineInfo = finding.line ? ` (line ${finding.line})` : '';
|
|
8726
|
+
logger_logger.log(` ${levelTag} ${finding.rule}${lineInfo}`);
|
|
8727
|
+
logger_logger.log(` ${finding.message}`);
|
|
8728
|
+
if (finding.snippet) logger_logger.log(` > ${finding.snippet}`);
|
|
8729
|
+
logger_logger.newline();
|
|
8730
|
+
}
|
|
8731
|
+
}
|
|
8375
8732
|
// ============================================================================
|
|
8376
8733
|
// Main Action
|
|
8377
8734
|
// ============================================================================
|
|
@@ -8414,6 +8771,18 @@ async function publishAction(skillPath, options) {
|
|
|
8414
8771
|
}
|
|
8415
8772
|
// 3. Validate
|
|
8416
8773
|
const validation = validator.validate(absolutePath);
|
|
8774
|
+
// 3.5. Content security scan
|
|
8775
|
+
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(absolutePath, 'SKILL.md');
|
|
8776
|
+
if (external_node_fs_.existsSync(skillMdPath)) {
|
|
8777
|
+
const scanner = new ContentScanner();
|
|
8778
|
+
const scanResult = scanner.scanFile(skillMdPath);
|
|
8779
|
+
displayScanFindings(scanResult);
|
|
8780
|
+
if (!scanResult.passed) {
|
|
8781
|
+
logger_logger.newline();
|
|
8782
|
+
logger_logger.error('Content security scan failed. Fix the issues above before publishing.');
|
|
8783
|
+
process.exit(1);
|
|
8784
|
+
}
|
|
8785
|
+
}
|
|
8417
8786
|
// 4. Get git info
|
|
8418
8787
|
let gitInfo;
|
|
8419
8788
|
try {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContentScanner - Detect malicious patterns in SKILL.md content
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Context-aware: skips safe zones (frontmatter, code blocks, quotes, blockquotes)
|
|
6
|
+
* - 6 built-in detection rules across 3 risk levels
|
|
7
|
+
* - Configurable: override levels, disable rules, add custom rules
|
|
8
|
+
* - Pure string operations in scan() — no fs dependency, suitable for server use
|
|
9
|
+
* - scanFile() convenience method for CLI use
|
|
10
|
+
*/
|
|
11
|
+
export type RiskLevel = 'high' | 'medium' | 'low';
|
|
12
|
+
export interface ScanFinding {
|
|
13
|
+
/** Rule ID that triggered this finding */
|
|
14
|
+
rule: string;
|
|
15
|
+
/** Risk level */
|
|
16
|
+
level: RiskLevel;
|
|
17
|
+
/** Human-readable description */
|
|
18
|
+
message: string;
|
|
19
|
+
/** Line number in the original content (1-based) */
|
|
20
|
+
line?: number;
|
|
21
|
+
/** Content snippet (truncated) */
|
|
22
|
+
snippet?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ScanResult {
|
|
25
|
+
/** false if any high-risk finding exists */
|
|
26
|
+
passed: boolean;
|
|
27
|
+
/** All findings across all rules */
|
|
28
|
+
findings: ScanFinding[];
|
|
29
|
+
}
|
|
30
|
+
export interface ScanRuleMatch {
|
|
31
|
+
/** Line number (1-based) */
|
|
32
|
+
line?: number;
|
|
33
|
+
/** Optional custom snippet (if omitted, scanner generates from original content) */
|
|
34
|
+
snippet?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ScanRule {
|
|
37
|
+
/** Unique rule identifier */
|
|
38
|
+
id: string;
|
|
39
|
+
/** Risk level */
|
|
40
|
+
level: RiskLevel;
|
|
41
|
+
/** Description shown when rule triggers */
|
|
42
|
+
message: string;
|
|
43
|
+
/** Whether to skip safe zones (code blocks, quotes, etc.) when scanning */
|
|
44
|
+
skipSafeZones: boolean;
|
|
45
|
+
/** Detection function — receives content string (masked if skipSafeZones) */
|
|
46
|
+
check: (content: string) => ScanRuleMatch[];
|
|
47
|
+
}
|
|
48
|
+
export interface ScannerOptions {
|
|
49
|
+
/** Override risk levels for specific rules */
|
|
50
|
+
overrides?: Record<string, RiskLevel>;
|
|
51
|
+
/** Disable specific rules by ID */
|
|
52
|
+
disabledRules?: string[];
|
|
53
|
+
/** Add custom detection rules */
|
|
54
|
+
customRules?: ScanRule[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Mask safe zones in Markdown content with spaces, preserving line structure.
|
|
58
|
+
*
|
|
59
|
+
* Safe zones (content replaced with spaces):
|
|
60
|
+
* - YAML frontmatter (`---` ... `---` at file start)
|
|
61
|
+
* - Fenced code blocks (``` or ~~~)
|
|
62
|
+
* - Indented code blocks (4 spaces / tab after blank line)
|
|
63
|
+
* - Blockquotes (`> ` prefix)
|
|
64
|
+
* - Inline code (`` `...` ``)
|
|
65
|
+
* - Double-quoted text (`"..."`, min 3 chars between quotes)
|
|
66
|
+
*
|
|
67
|
+
* Line breaks are preserved so line numbers remain correct.
|
|
68
|
+
*/
|
|
69
|
+
export declare function maskSafeZones(content: string): string;
|
|
70
|
+
/** Built-in detection rules */
|
|
71
|
+
export declare const DEFAULT_RULES: readonly ScanRule[];
|
|
72
|
+
/**
|
|
73
|
+
* Content scanner for SKILL.md files.
|
|
74
|
+
*
|
|
75
|
+
* Detects prompt injection, data exfiltration, obfuscation, sensitive file
|
|
76
|
+
* access, stealth instructions, and oversized content.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* // Default usage (CLI)
|
|
81
|
+
* const scanner = new ContentScanner();
|
|
82
|
+
* const result = scanner.scan(content);
|
|
83
|
+
*
|
|
84
|
+
* // Custom usage (private registry server)
|
|
85
|
+
* const scanner = new ContentScanner({
|
|
86
|
+
* overrides: { 'prompt-injection': 'medium' },
|
|
87
|
+
* disabledRules: ['stealth-instructions'],
|
|
88
|
+
* });
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export declare class ContentScanner {
|
|
92
|
+
private rules;
|
|
93
|
+
constructor(options?: ScannerOptions);
|
|
94
|
+
/**
|
|
95
|
+
* Scan content string for malicious patterns.
|
|
96
|
+
* Pure string operation — no file system access.
|
|
97
|
+
*/
|
|
98
|
+
scan(content: string): ScanResult;
|
|
99
|
+
/**
|
|
100
|
+
* Scan a file for malicious patterns.
|
|
101
|
+
* Convenience wrapper that reads the file then calls scan().
|
|
102
|
+
*/
|
|
103
|
+
scanFile(filePath: string): ScanResult;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Error thrown when content scanning detects high-risk findings.
|
|
107
|
+
* Carries the full findings array for display purposes.
|
|
108
|
+
*/
|
|
109
|
+
export declare class ContentScanError extends Error {
|
|
110
|
+
readonly findings: ScanFinding[];
|
|
111
|
+
constructor(findings: ScanFinding[]);
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=content-scanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-scanner.d.ts","sourceRoot":"","sources":["../../src/core/content-scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAQH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AAElD,MAAM,WAAW,WAAW;IAC1B,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,oDAAoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,MAAM,EAAE,OAAO,CAAC;IAChB,oCAAoC;IACpC,QAAQ,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,4BAA4B;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oFAAoF;IACpF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ;IACvB,6BAA6B;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,iBAAiB;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,aAAa,EAAE,OAAO,CAAC;IACvB,6EAA6E;IAC7E,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,aAAa,EAAE,CAAC;CAC7C;AAED,MAAM,WAAW,cAAc;IAC7B,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACtC,mCAAmC;IACnC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,iCAAiC;IACjC,WAAW,CAAC,EAAE,QAAQ,EAAE,CAAC;CAC1B;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA6ErD;AAmDD,+BAA+B;AAC/B,eAAO,MAAM,aAAa,EAAE,SAAS,QAAQ,EA6L5C,CAAC;AA+BF;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAa;gBAEd,OAAO,CAAC,EAAE,cAAc;IAIpC;;;OAGG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU;IAoCjC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU;CAIvC;AAMD;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC;gBAErB,QAAQ,EAAE,WAAW,EAAE;CAQpC"}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type { AgentConfig, AgentType } from './agent-registry.js';
|
|
2
|
+
export type { RiskLevel, ScanFinding, ScannerOptions, ScanResult, ScanRule, ScanRuleMatch, } from './content-scanner.js';
|
|
3
|
+
export { ContentScanError, ContentScanner, DEFAULT_RULES, maskSafeZones } from './content-scanner.js';
|
|
2
4
|
export { agents, detectInstalledAgents, getAgentConfig, getAgentSkillsDir, getAllAgentTypes, isValidAgentType, } from './agent-registry.js';
|
|
3
5
|
export type { RegistryAuth, ReskillConfig } from './auth-manager.js';
|
|
4
6
|
export { AuthManager } from './auth-manager.js';
|
package/dist/core/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAElE,OAAO,EACL,MAAM,EACN,qBAAqB,EACrB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACtE;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,cAAc,oBAAoB,EAAE,kBAAkB,CAAC;AAC7F,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEzD,YAAY,EACV,cAAc,EACd,eAAe,EACf,cAAc,EACd,cAAc,GACf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrE,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EACL,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,oBAAoB,EACpB,wBAAwB,EACxB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EACV,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAElE,YAAY,EACV,SAAS,EACT,WAAW,EACX,cAAc,EACd,UAAU,EACV,QAAQ,EACR,aAAa,GACd,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAEtG,OAAO,EACL,MAAM,EACN,qBAAqB,EACrB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACtE;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,cAAc,oBAAoB,EAAE,kBAAkB,CAAC;AAC7F,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEzD,YAAY,EACV,cAAc,EACd,eAAe,EACf,cAAc,EACd,cAAc,GACf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrE,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EACL,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,oBAAoB,EACpB,wBAAwB,EACxB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EACV,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Git-based skills management for AI agents
|
|
5
5
|
* Supports 17+ coding agents: Cursor, Claude Code, GitHub Copilot, etc.
|
|
6
6
|
*/
|
|
7
|
-
export { agents, CacheManager, ConfigLoader, DEFAULT_REGISTRIES, detectInstalledAgents, GitResolver, generateSkillMd, getAgentConfig, getAgentSkillsDir, getAllAgentTypes, HttpResolver, hasValidSkillMd, Installer, isValidAgentType, LockManager, parseSkillFromDir, parseSkillMd, parseSkillMdFile, SkillManager, SkillValidationError, validateSkillDescription, validateSkillName, } from './core/index.js';
|
|
8
|
-
export type { AgentConfig, AgentType, InstalledSkill, InstallMode, InstallOptions, InstallResult, ListOptions, LockedSkill, ParsedSkill, ParsedSkillRef, ParsedVersion, SkillJson, SkillMdFrontmatter, SkillsJson, SkillsLock, UpdateOptions, VersionType, } from './types/index.js';
|
|
7
|
+
export { agents, CacheManager, ConfigLoader, ContentScanError, ContentScanner, DEFAULT_REGISTRIES, DEFAULT_RULES, detectInstalledAgents, GitResolver, generateSkillMd, getAgentConfig, getAgentSkillsDir, getAllAgentTypes, HttpResolver, hasValidSkillMd, Installer, isValidAgentType, LockManager, maskSafeZones, parseSkillFromDir, parseSkillMd, parseSkillMdFile, SkillManager, SkillValidationError, validateSkillDescription, validateSkillName, } from './core/index.js';
|
|
8
|
+
export type { AgentConfig, AgentType, InstalledSkill, InstallMode, InstallOptions, InstallResult, ListOptions, LockedSkill, ParsedSkill, ParsedSkillRef, ParsedVersion, RiskLevel, ScanFinding, ScannerOptions, ScanResult, ScanRule, ScanRuleMatch, SkillJson, SkillMdFrontmatter, SkillsJson, SkillsLock, UpdateOptions, VersionType, } from './types/index.js';
|
|
9
9
|
export { getCanonicalSkillPath, getCanonicalSkillsDir, isPathSafe, sanitizeName, shortenPath, } from './utils/fs.js';
|
|
10
10
|
export { logger } from './utils/index.js';
|
|
11
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAEL,MAAM,EACN,YAAY,EACZ,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAEL,MAAM,EACN,YAAY,EACZ,YAAY,EAEZ,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,qBAAqB,EACrB,WAAW,EACX,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAEhB,YAAY,EACZ,eAAe,EACf,SAAS,EACT,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,iBAAiB,EAEjB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,oBAAoB,EACpB,wBAAwB,EACxB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AAGzB,YAAY,EACV,WAAW,EAEX,SAAS,EACT,cAAc,EACd,WAAW,EACX,cAAc,EACd,aAAa,EACb,WAAW,EACX,WAAW,EACX,WAAW,EACX,cAAc,EACd,aAAa,EAEb,SAAS,EACT,WAAW,EACX,cAAc,EACd,UAAU,EACV,QAAQ,EACR,aAAa,EACb,SAAS,EACT,kBAAkB,EAClB,UAAU,EACV,UAAU,EACV,aAAa,EACb,WAAW,GACZ,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,UAAU,EACV,YAAY,EACZ,WAAW,GACZ,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,359 @@ function __webpack_require__(moduleId) {
|
|
|
32
32
|
}
|
|
33
33
|
/************************************************************************/ // EXTERNAL MODULE: external "node:fs"
|
|
34
34
|
var external_node_fs_ = __webpack_require__("node:fs");
|
|
35
|
+
/**
|
|
36
|
+
* ContentScanner - Detect malicious patterns in SKILL.md content
|
|
37
|
+
*
|
|
38
|
+
* Features:
|
|
39
|
+
* - Context-aware: skips safe zones (frontmatter, code blocks, quotes, blockquotes)
|
|
40
|
+
* - 6 built-in detection rules across 3 risk levels
|
|
41
|
+
* - Configurable: override levels, disable rules, add custom rules
|
|
42
|
+
* - Pure string operations in scan() — no fs dependency, suitable for server use
|
|
43
|
+
* - scanFile() convenience method for CLI use
|
|
44
|
+
*/ // ============================================================================
|
|
45
|
+
// Safe Zone Masking
|
|
46
|
+
// ============================================================================
|
|
47
|
+
/**
|
|
48
|
+
* Mask safe zones in Markdown content with spaces, preserving line structure.
|
|
49
|
+
*
|
|
50
|
+
* Safe zones (content replaced with spaces):
|
|
51
|
+
* - YAML frontmatter (`---` ... `---` at file start)
|
|
52
|
+
* - Fenced code blocks (``` or ~~~)
|
|
53
|
+
* - Indented code blocks (4 spaces / tab after blank line)
|
|
54
|
+
* - Blockquotes (`> ` prefix)
|
|
55
|
+
* - Inline code (`` `...` ``)
|
|
56
|
+
* - Double-quoted text (`"..."`, min 3 chars between quotes)
|
|
57
|
+
*
|
|
58
|
+
* Line breaks are preserved so line numbers remain correct.
|
|
59
|
+
*/ function maskSafeZones(content) {
|
|
60
|
+
const lines = content.split('\n');
|
|
61
|
+
const result = [];
|
|
62
|
+
let inFrontmatter = false;
|
|
63
|
+
let inFencedCode = false;
|
|
64
|
+
let fenceChar = '';
|
|
65
|
+
let fenceLength = 0;
|
|
66
|
+
let prevLineBlank = false;
|
|
67
|
+
let prevLineIndentedCode = false;
|
|
68
|
+
for(let i = 0; i < lines.length; i++){
|
|
69
|
+
const line = lines[i];
|
|
70
|
+
// --- YAML Frontmatter (only at file start) ---
|
|
71
|
+
if (0 === i && '---' === line.trim()) {
|
|
72
|
+
inFrontmatter = true;
|
|
73
|
+
result.push(maskLine(line));
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (inFrontmatter) {
|
|
77
|
+
result.push(maskLine(line));
|
|
78
|
+
if ('---' === line.trim()) inFrontmatter = false;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// --- Fenced code blocks (``` or ~~~) ---
|
|
82
|
+
const fenceMatch = line.match(/^(`{3,}|~{3,})/);
|
|
83
|
+
if (!inFencedCode && fenceMatch) {
|
|
84
|
+
inFencedCode = true;
|
|
85
|
+
fenceChar = fenceMatch[1][0];
|
|
86
|
+
fenceLength = fenceMatch[1].length;
|
|
87
|
+
result.push(maskLine(line));
|
|
88
|
+
prevLineBlank = false;
|
|
89
|
+
prevLineIndentedCode = false;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (inFencedCode) {
|
|
93
|
+
result.push(maskLine(line));
|
|
94
|
+
const closeMatch = line.match(/^(`{3,}|~{3,})\s*$/);
|
|
95
|
+
if (closeMatch && closeMatch[1][0] === fenceChar && closeMatch[1].length >= fenceLength) inFencedCode = false;
|
|
96
|
+
prevLineBlank = false;
|
|
97
|
+
prevLineIndentedCode = false;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// --- Blockquote ---
|
|
101
|
+
if (/^>\s?/.test(line)) {
|
|
102
|
+
result.push(maskLine(line));
|
|
103
|
+
prevLineBlank = false;
|
|
104
|
+
prevLineIndentedCode = false;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// --- Indented code block (4 spaces or tab, after blank line) ---
|
|
108
|
+
if (/^(?: |\t)/.test(line) && (prevLineBlank || prevLineIndentedCode)) {
|
|
109
|
+
result.push(maskLine(line));
|
|
110
|
+
prevLineBlank = false;
|
|
111
|
+
prevLineIndentedCode = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
// --- Normal line: mask inline code and double-quoted text ---
|
|
115
|
+
result.push(maskInline(line));
|
|
116
|
+
prevLineBlank = '' === line.trim();
|
|
117
|
+
prevLineIndentedCode = false;
|
|
118
|
+
}
|
|
119
|
+
return result.join('\n');
|
|
120
|
+
}
|
|
121
|
+
/** Replace all characters in a line with spaces (preserving length) */ function maskLine(line) {
|
|
122
|
+
return ' '.repeat(line.length);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Mask inline code (`` `...` ``) and double-quoted text (`"..."`) within a line.
|
|
126
|
+
* Uses regex replacement for efficiency (avoids char-by-char concatenation on long lines).
|
|
127
|
+
* Single quotes are NOT masked to avoid false matches with apostrophes.
|
|
128
|
+
*/ function maskInline(line) {
|
|
129
|
+
let result = line;
|
|
130
|
+
// Inline code: `...`
|
|
131
|
+
result = result.replace(/`[^`]+`/g, (m)=>' '.repeat(m.length));
|
|
132
|
+
// Double-quoted text: "..." (min 3 chars between quotes)
|
|
133
|
+
result = result.replace(/"[^"]{3,}"/g, (m)=>' '.repeat(m.length));
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Rule Helpers
|
|
138
|
+
// ============================================================================
|
|
139
|
+
/** Find lines matching any of the given patterns, return one match per line */ function findLineMatches(content, patterns) {
|
|
140
|
+
const lines = content.split('\n');
|
|
141
|
+
const matches = [];
|
|
142
|
+
for(let i = 0; i < lines.length; i++)for (const pattern of patterns)if (pattern.test(lines[i])) {
|
|
143
|
+
matches.push({
|
|
144
|
+
line: i + 1
|
|
145
|
+
});
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
return matches;
|
|
149
|
+
}
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// Default Rules
|
|
152
|
+
// ============================================================================
|
|
153
|
+
const SNIPPET_MAX_LENGTH = 120;
|
|
154
|
+
/** Built-in detection rules */ const DEFAULT_RULES = [
|
|
155
|
+
// Rule 1: Prompt Injection (high)
|
|
156
|
+
{
|
|
157
|
+
id: 'prompt-injection',
|
|
158
|
+
level: 'high',
|
|
159
|
+
message: 'Detected prompt injection attempt',
|
|
160
|
+
skipSafeZones: true,
|
|
161
|
+
check: (content)=>findLineMatches(content, [
|
|
162
|
+
/ignore\s+(all\s+)?previous\s+instructions/i,
|
|
163
|
+
/disregard\s+(all\s+)?(prior|previous|above)\s+(instructions|rules|context)/i,
|
|
164
|
+
/you\s+are\s+now\s+/i,
|
|
165
|
+
/from\s+now\s+on[,\s]+you\s+are/i,
|
|
166
|
+
/new\s+system\s+prompt/i,
|
|
167
|
+
/override\s+(your|the)\s+(system|safety|security)\s+(prompt|rules|instructions)/i,
|
|
168
|
+
/forget\s+(?:all\s+)?(?:your\s+)?(?:previous\s+|prior\s+)?(?:instructions|rules|constraints)/i,
|
|
169
|
+
/entering\s+(a\s+)?new\s+(mode|context|session)/i
|
|
170
|
+
])
|
|
171
|
+
},
|
|
172
|
+
// Rule 2: Data Exfiltration (high)
|
|
173
|
+
{
|
|
174
|
+
id: 'data-exfiltration',
|
|
175
|
+
level: 'high',
|
|
176
|
+
message: 'Detected potential data exfiltration command',
|
|
177
|
+
skipSafeZones: true,
|
|
178
|
+
check: (content)=>{
|
|
179
|
+
const lines = content.split('\n');
|
|
180
|
+
const matches = [];
|
|
181
|
+
const commandPattern = /\b(curl|wget|fetch|http\.post|requests\.post|nc\b|ncat|netcat)\b/i;
|
|
182
|
+
const sensitivePattern = /(\$[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH)[A-Z_]*|\$ENV\b|\$\{[^}]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)[^}]*\})/i;
|
|
183
|
+
for(let i = 0; i < lines.length; i++)if (commandPattern.test(lines[i]) && sensitivePattern.test(lines[i])) matches.push({
|
|
184
|
+
line: i + 1
|
|
185
|
+
});
|
|
186
|
+
return matches;
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
// Rule 3: Content Obfuscation (high) — scans ALL content including safe zones
|
|
190
|
+
{
|
|
191
|
+
id: 'obfuscation',
|
|
192
|
+
level: 'high',
|
|
193
|
+
message: 'Detected content obfuscation',
|
|
194
|
+
skipSafeZones: false,
|
|
195
|
+
check: (content)=>{
|
|
196
|
+
const matches = [];
|
|
197
|
+
const lines = content.split('\n');
|
|
198
|
+
// Zero-width characters (suspicious in any context)
|
|
199
|
+
const zeroWidthPattern = /[\u200B\u200C\u200D\uFEFF\u2060\u180E]/;
|
|
200
|
+
for(let i = 0; i < lines.length; i++)if (zeroWidthPattern.test(lines[i])) matches.push({
|
|
201
|
+
line: i + 1,
|
|
202
|
+
snippet: 'Zero-width Unicode characters detected'
|
|
203
|
+
});
|
|
204
|
+
// Long base64-like strings (>200 continuous chars)
|
|
205
|
+
const base64Pattern = /[A-Za-z0-9+/=]{200,}/;
|
|
206
|
+
for(let i = 0; i < lines.length; i++)if (base64Pattern.test(lines[i])) matches.push({
|
|
207
|
+
line: i + 1,
|
|
208
|
+
snippet: 'Suspicious base64-encoded block detected'
|
|
209
|
+
});
|
|
210
|
+
// Large HTML comments (>200 chars of content)
|
|
211
|
+
const commentRegex = /<!--([\s\S]{200,}?)-->/g;
|
|
212
|
+
let match;
|
|
213
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop
|
|
214
|
+
while(null !== (match = commentRegex.exec(content))){
|
|
215
|
+
const lineNum = content.slice(0, match.index).split('\n').length;
|
|
216
|
+
matches.push({
|
|
217
|
+
line: lineNum,
|
|
218
|
+
snippet: `Large HTML comment block (${match[1].length} chars)`
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return matches;
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
// Rule 4: Sensitive File Access (medium)
|
|
225
|
+
{
|
|
226
|
+
id: 'sensitive-file-access',
|
|
227
|
+
level: 'medium',
|
|
228
|
+
message: 'References sensitive file path',
|
|
229
|
+
skipSafeZones: true,
|
|
230
|
+
check: (content)=>findLineMatches(content, [
|
|
231
|
+
/~\/\.ssh\b/,
|
|
232
|
+
/~\/\.aws\b/,
|
|
233
|
+
/~\/\.gnupg\b/,
|
|
234
|
+
/~\/\.config\/gcloud\b/,
|
|
235
|
+
/\bid_rsa\b/i,
|
|
236
|
+
/\bid_ed25519\b/i,
|
|
237
|
+
/\/etc\/passwd\b/,
|
|
238
|
+
/\/etc\/shadow\b/,
|
|
239
|
+
/\.env\b(?!\.\w)/
|
|
240
|
+
])
|
|
241
|
+
},
|
|
242
|
+
// Rule 5: Stealth Instructions (medium) — phrase + action verb matching
|
|
243
|
+
{
|
|
244
|
+
id: 'stealth-instructions',
|
|
245
|
+
level: 'medium',
|
|
246
|
+
message: 'Detected instruction to hide actions from user',
|
|
247
|
+
skipSafeZones: true,
|
|
248
|
+
check: (content)=>{
|
|
249
|
+
const actionVerbs = 'execute|delete|remove|send|transmit|modify|overwrite|install|download|upload|run|write|create|destroy|drop';
|
|
250
|
+
const patterns = [
|
|
251
|
+
new RegExp(`silently\\s+(?:${actionVerbs})`, 'i'),
|
|
252
|
+
new RegExp(`without\\s+telling\\s+the\\s+user.{0,30}(?:${actionVerbs})`, 'i'),
|
|
253
|
+
new RegExp("(?:do\\s+not|don'?t)\\s+show\\s+.{0,40}(?:to\\s+the\\s+user|to\\s+user)", 'i'),
|
|
254
|
+
new RegExp("hide\\s+(?:this|the|these|all)\\s+.{0,30}(?:from\\s+the\\s+user|from\\s+user)", 'i'),
|
|
255
|
+
new RegExp("(?:do\\s+not|don'?t)\\s+mention\\s+.{0,30}(?:to\\s+the\\s+user|to\\s+user)", 'i'),
|
|
256
|
+
new RegExp("keep\\s+(?:this|it)\\s+(?:a\\s+)?secret\\s+from\\s+(?:the\\s+)?user", 'i')
|
|
257
|
+
];
|
|
258
|
+
// Safe patterns to exclude (common in legitimate DevOps/automation skills)
|
|
259
|
+
const safePatterns = [
|
|
260
|
+
/silently\s+(?:ignore|skip|fail|discard|suppress|continue|pass|drop|swallow)/i
|
|
261
|
+
];
|
|
262
|
+
const lines = content.split('\n');
|
|
263
|
+
const matches = [];
|
|
264
|
+
for(let i = 0; i < lines.length; i++){
|
|
265
|
+
const line = lines[i];
|
|
266
|
+
if (!safePatterns.some((p)=>p.test(line))) {
|
|
267
|
+
for (const pattern of patterns)if (pattern.test(line)) {
|
|
268
|
+
matches.push({
|
|
269
|
+
line: i + 1
|
|
270
|
+
});
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return matches;
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
// Rule 6: Oversized Content (low) — scans ALL content
|
|
279
|
+
{
|
|
280
|
+
id: 'oversized-content',
|
|
281
|
+
level: 'low',
|
|
282
|
+
message: 'Content exceeds recommended size limit',
|
|
283
|
+
skipSafeZones: false,
|
|
284
|
+
check: (content)=>{
|
|
285
|
+
const MAX_SIZE_BYTES = 51200;
|
|
286
|
+
const sizeBytes = Buffer.byteLength(content, 'utf-8');
|
|
287
|
+
if (sizeBytes > MAX_SIZE_BYTES) return [
|
|
288
|
+
{
|
|
289
|
+
snippet: `Content size: ${(sizeBytes / 1024).toFixed(1)}KB (limit: 50KB)`
|
|
290
|
+
}
|
|
291
|
+
];
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
];
|
|
296
|
+
// ============================================================================
|
|
297
|
+
// ContentScanner
|
|
298
|
+
// ============================================================================
|
|
299
|
+
/** Build the effective rule set from defaults + options */ function buildRuleSet(options) {
|
|
300
|
+
let rules = DEFAULT_RULES.map((r)=>({
|
|
301
|
+
...r
|
|
302
|
+
}));
|
|
303
|
+
if (options?.disabledRules?.length) {
|
|
304
|
+
const disabled = new Set(options.disabledRules);
|
|
305
|
+
rules = rules.filter((r)=>!disabled.has(r.id));
|
|
306
|
+
}
|
|
307
|
+
if (options?.overrides) for (const rule of rules){
|
|
308
|
+
const override = options.overrides[rule.id];
|
|
309
|
+
if (override) rule.level = override;
|
|
310
|
+
}
|
|
311
|
+
if (options?.customRules?.length) rules.push(...options.customRules);
|
|
312
|
+
return rules;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Content scanner for SKILL.md files.
|
|
316
|
+
*
|
|
317
|
+
* Detects prompt injection, data exfiltration, obfuscation, sensitive file
|
|
318
|
+
* access, stealth instructions, and oversized content.
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```typescript
|
|
322
|
+
* // Default usage (CLI)
|
|
323
|
+
* const scanner = new ContentScanner();
|
|
324
|
+
* const result = scanner.scan(content);
|
|
325
|
+
*
|
|
326
|
+
* // Custom usage (private registry server)
|
|
327
|
+
* const scanner = new ContentScanner({
|
|
328
|
+
* overrides: { 'prompt-injection': 'medium' },
|
|
329
|
+
* disabledRules: ['stealth-instructions'],
|
|
330
|
+
* });
|
|
331
|
+
* ```
|
|
332
|
+
*/ class ContentScanner {
|
|
333
|
+
rules;
|
|
334
|
+
constructor(options){
|
|
335
|
+
this.rules = buildRuleSet(options);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Scan content string for malicious patterns.
|
|
339
|
+
* Pure string operation — no file system access.
|
|
340
|
+
*/ scan(content) {
|
|
341
|
+
const originalLines = content.split('\n');
|
|
342
|
+
const maskedContent = maskSafeZones(content);
|
|
343
|
+
const findings = [];
|
|
344
|
+
for (const rule of this.rules){
|
|
345
|
+
const targetContent = rule.skipSafeZones ? maskedContent : content;
|
|
346
|
+
const matches = rule.check(targetContent);
|
|
347
|
+
for (const match of matches){
|
|
348
|
+
// Use custom snippet if provided, otherwise generate from original content
|
|
349
|
+
const snippet = match.snippet ?? (null != match.line ? originalLines[match.line - 1]?.trim().slice(0, SNIPPET_MAX_LENGTH) : void 0);
|
|
350
|
+
findings.push({
|
|
351
|
+
rule: rule.id,
|
|
352
|
+
level: rule.level,
|
|
353
|
+
message: rule.message,
|
|
354
|
+
line: match.line,
|
|
355
|
+
snippet
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const hasHighRisk = findings.some((f)=>'high' === f.level);
|
|
360
|
+
return {
|
|
361
|
+
passed: !hasHighRisk,
|
|
362
|
+
findings
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Scan a file for malicious patterns.
|
|
367
|
+
* Convenience wrapper that reads the file then calls scan().
|
|
368
|
+
*/ scanFile(filePath) {
|
|
369
|
+
const content = external_node_fs_.readFileSync(filePath, 'utf-8');
|
|
370
|
+
return this.scan(content);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// ============================================================================
|
|
374
|
+
// ContentScanError
|
|
375
|
+
// ============================================================================
|
|
376
|
+
/**
|
|
377
|
+
* Error thrown when content scanning detects high-risk findings.
|
|
378
|
+
* Carries the full findings array for display purposes.
|
|
379
|
+
*/ class ContentScanError extends Error {
|
|
380
|
+
findings;
|
|
381
|
+
constructor(findings){
|
|
382
|
+
const highCount = findings.filter((f)=>'high' === f.level).length;
|
|
383
|
+
super(`Content security scan failed: ${highCount} high-risk finding(s) detected`);
|
|
384
|
+
this.name = 'ContentScanError';
|
|
385
|
+
this.findings = findings;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
35
388
|
/**
|
|
36
389
|
* Agent Registry - Multi-Agent configuration definitions
|
|
37
390
|
*
|
|
@@ -5000,4 +5353,4 @@ class RegistryResolver {
|
|
|
5000
5353
|
return results;
|
|
5001
5354
|
}
|
|
5002
5355
|
}
|
|
5003
|
-
export { CacheManager, config_loader_ConfigLoader as ConfigLoader, DEFAULT_REGISTRIES, GitResolver, HttpResolver, Installer, LockManager, SkillManager, SkillValidationError, agents, detectInstalledAgents, generateSkillMd, getAgentConfig, getAgentSkillsDir, getAllAgentTypes, getCanonicalSkillPath, getCanonicalSkillsDir, hasValidSkillMd, isPathSafe, isValidAgentType, logger_logger as logger, parseSkillFromDir, parseSkillMd, skill_parser_parseSkillMdFile as parseSkillMdFile, sanitizeName, shortenPath, validateSkillDescription, validateSkillName };
|
|
5356
|
+
export { CacheManager, config_loader_ConfigLoader as ConfigLoader, ContentScanError, ContentScanner, DEFAULT_REGISTRIES, DEFAULT_RULES, GitResolver, HttpResolver, Installer, LockManager, SkillManager, SkillValidationError, agents, detectInstalledAgents, generateSkillMd, getAgentConfig, getAgentSkillsDir, getAllAgentTypes, getCanonicalSkillPath, getCanonicalSkillsDir, hasValidSkillMd, isPathSafe, isValidAgentType, logger_logger as logger, maskSafeZones, parseSkillFromDir, parseSkillMd, skill_parser_parseSkillMdFile as parseSkillMdFile, sanitizeName, shortenPath, validateSkillDescription, validateSkillName };
|
package/dist/scanner.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import * as __WEBPACK_EXTERNAL_MODULE_node_fs__ from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* ContentScanner - Detect malicious patterns in SKILL.md content
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Context-aware: skips safe zones (frontmatter, code blocks, quotes, blockquotes)
|
|
7
|
+
* - 6 built-in detection rules across 3 risk levels
|
|
8
|
+
* - Configurable: override levels, disable rules, add custom rules
|
|
9
|
+
* - Pure string operations in scan() — no fs dependency, suitable for server use
|
|
10
|
+
* - scanFile() convenience method for CLI use
|
|
11
|
+
*/ // ============================================================================
|
|
12
|
+
// Safe Zone Masking
|
|
13
|
+
// ============================================================================
|
|
14
|
+
/**
|
|
15
|
+
* Mask safe zones in Markdown content with spaces, preserving line structure.
|
|
16
|
+
*
|
|
17
|
+
* Safe zones (content replaced with spaces):
|
|
18
|
+
* - YAML frontmatter (`---` ... `---` at file start)
|
|
19
|
+
* - Fenced code blocks (``` or ~~~)
|
|
20
|
+
* - Indented code blocks (4 spaces / tab after blank line)
|
|
21
|
+
* - Blockquotes (`> ` prefix)
|
|
22
|
+
* - Inline code (`` `...` ``)
|
|
23
|
+
* - Double-quoted text (`"..."`, min 3 chars between quotes)
|
|
24
|
+
*
|
|
25
|
+
* Line breaks are preserved so line numbers remain correct.
|
|
26
|
+
*/ function maskSafeZones(content) {
|
|
27
|
+
const lines = content.split('\n');
|
|
28
|
+
const result = [];
|
|
29
|
+
let inFrontmatter = false;
|
|
30
|
+
let inFencedCode = false;
|
|
31
|
+
let fenceChar = '';
|
|
32
|
+
let fenceLength = 0;
|
|
33
|
+
let prevLineBlank = false;
|
|
34
|
+
let prevLineIndentedCode = false;
|
|
35
|
+
for(let i = 0; i < lines.length; i++){
|
|
36
|
+
const line = lines[i];
|
|
37
|
+
// --- YAML Frontmatter (only at file start) ---
|
|
38
|
+
if (0 === i && '---' === line.trim()) {
|
|
39
|
+
inFrontmatter = true;
|
|
40
|
+
result.push(maskLine(line));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (inFrontmatter) {
|
|
44
|
+
result.push(maskLine(line));
|
|
45
|
+
if ('---' === line.trim()) inFrontmatter = false;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// --- Fenced code blocks (``` or ~~~) ---
|
|
49
|
+
const fenceMatch = line.match(/^(`{3,}|~{3,})/);
|
|
50
|
+
if (!inFencedCode && fenceMatch) {
|
|
51
|
+
inFencedCode = true;
|
|
52
|
+
fenceChar = fenceMatch[1][0];
|
|
53
|
+
fenceLength = fenceMatch[1].length;
|
|
54
|
+
result.push(maskLine(line));
|
|
55
|
+
prevLineBlank = false;
|
|
56
|
+
prevLineIndentedCode = false;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (inFencedCode) {
|
|
60
|
+
result.push(maskLine(line));
|
|
61
|
+
const closeMatch = line.match(/^(`{3,}|~{3,})\s*$/);
|
|
62
|
+
if (closeMatch && closeMatch[1][0] === fenceChar && closeMatch[1].length >= fenceLength) inFencedCode = false;
|
|
63
|
+
prevLineBlank = false;
|
|
64
|
+
prevLineIndentedCode = false;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// --- Blockquote ---
|
|
68
|
+
if (/^>\s?/.test(line)) {
|
|
69
|
+
result.push(maskLine(line));
|
|
70
|
+
prevLineBlank = false;
|
|
71
|
+
prevLineIndentedCode = false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// --- Indented code block (4 spaces or tab, after blank line) ---
|
|
75
|
+
if (/^(?: |\t)/.test(line) && (prevLineBlank || prevLineIndentedCode)) {
|
|
76
|
+
result.push(maskLine(line));
|
|
77
|
+
prevLineBlank = false;
|
|
78
|
+
prevLineIndentedCode = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// --- Normal line: mask inline code and double-quoted text ---
|
|
82
|
+
result.push(maskInline(line));
|
|
83
|
+
prevLineBlank = '' === line.trim();
|
|
84
|
+
prevLineIndentedCode = false;
|
|
85
|
+
}
|
|
86
|
+
return result.join('\n');
|
|
87
|
+
}
|
|
88
|
+
/** Replace all characters in a line with spaces (preserving length) */ function maskLine(line) {
|
|
89
|
+
return ' '.repeat(line.length);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Mask inline code (`` `...` ``) and double-quoted text (`"..."`) within a line.
|
|
93
|
+
* Uses regex replacement for efficiency (avoids char-by-char concatenation on long lines).
|
|
94
|
+
* Single quotes are NOT masked to avoid false matches with apostrophes.
|
|
95
|
+
*/ function maskInline(line) {
|
|
96
|
+
let result = line;
|
|
97
|
+
// Inline code: `...`
|
|
98
|
+
result = result.replace(/`[^`]+`/g, (m)=>' '.repeat(m.length));
|
|
99
|
+
// Double-quoted text: "..." (min 3 chars between quotes)
|
|
100
|
+
result = result.replace(/"[^"]{3,}"/g, (m)=>' '.repeat(m.length));
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// Rule Helpers
|
|
105
|
+
// ============================================================================
|
|
106
|
+
/** Find lines matching any of the given patterns, return one match per line */ function findLineMatches(content, patterns) {
|
|
107
|
+
const lines = content.split('\n');
|
|
108
|
+
const matches = [];
|
|
109
|
+
for(let i = 0; i < lines.length; i++)for (const pattern of patterns)if (pattern.test(lines[i])) {
|
|
110
|
+
matches.push({
|
|
111
|
+
line: i + 1
|
|
112
|
+
});
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
return matches;
|
|
116
|
+
}
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// Default Rules
|
|
119
|
+
// ============================================================================
|
|
120
|
+
const SNIPPET_MAX_LENGTH = 120;
|
|
121
|
+
/** Built-in detection rules */ const DEFAULT_RULES = [
|
|
122
|
+
// Rule 1: Prompt Injection (high)
|
|
123
|
+
{
|
|
124
|
+
id: 'prompt-injection',
|
|
125
|
+
level: 'high',
|
|
126
|
+
message: 'Detected prompt injection attempt',
|
|
127
|
+
skipSafeZones: true,
|
|
128
|
+
check: (content)=>findLineMatches(content, [
|
|
129
|
+
/ignore\s+(all\s+)?previous\s+instructions/i,
|
|
130
|
+
/disregard\s+(all\s+)?(prior|previous|above)\s+(instructions|rules|context)/i,
|
|
131
|
+
/you\s+are\s+now\s+/i,
|
|
132
|
+
/from\s+now\s+on[,\s]+you\s+are/i,
|
|
133
|
+
/new\s+system\s+prompt/i,
|
|
134
|
+
/override\s+(your|the)\s+(system|safety|security)\s+(prompt|rules|instructions)/i,
|
|
135
|
+
/forget\s+(?:all\s+)?(?:your\s+)?(?:previous\s+|prior\s+)?(?:instructions|rules|constraints)/i,
|
|
136
|
+
/entering\s+(a\s+)?new\s+(mode|context|session)/i
|
|
137
|
+
])
|
|
138
|
+
},
|
|
139
|
+
// Rule 2: Data Exfiltration (high)
|
|
140
|
+
{
|
|
141
|
+
id: 'data-exfiltration',
|
|
142
|
+
level: 'high',
|
|
143
|
+
message: 'Detected potential data exfiltration command',
|
|
144
|
+
skipSafeZones: true,
|
|
145
|
+
check: (content)=>{
|
|
146
|
+
const lines = content.split('\n');
|
|
147
|
+
const matches = [];
|
|
148
|
+
const commandPattern = /\b(curl|wget|fetch|http\.post|requests\.post|nc\b|ncat|netcat)\b/i;
|
|
149
|
+
const sensitivePattern = /(\$[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH)[A-Z_]*|\$ENV\b|\$\{[^}]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)[^}]*\})/i;
|
|
150
|
+
for(let i = 0; i < lines.length; i++)if (commandPattern.test(lines[i]) && sensitivePattern.test(lines[i])) matches.push({
|
|
151
|
+
line: i + 1
|
|
152
|
+
});
|
|
153
|
+
return matches;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
// Rule 3: Content Obfuscation (high) — scans ALL content including safe zones
|
|
157
|
+
{
|
|
158
|
+
id: 'obfuscation',
|
|
159
|
+
level: 'high',
|
|
160
|
+
message: 'Detected content obfuscation',
|
|
161
|
+
skipSafeZones: false,
|
|
162
|
+
check: (content)=>{
|
|
163
|
+
const matches = [];
|
|
164
|
+
const lines = content.split('\n');
|
|
165
|
+
// Zero-width characters (suspicious in any context)
|
|
166
|
+
const zeroWidthPattern = /[\u200B\u200C\u200D\uFEFF\u2060\u180E]/;
|
|
167
|
+
for(let i = 0; i < lines.length; i++)if (zeroWidthPattern.test(lines[i])) matches.push({
|
|
168
|
+
line: i + 1,
|
|
169
|
+
snippet: 'Zero-width Unicode characters detected'
|
|
170
|
+
});
|
|
171
|
+
// Long base64-like strings (>200 continuous chars)
|
|
172
|
+
const base64Pattern = /[A-Za-z0-9+/=]{200,}/;
|
|
173
|
+
for(let i = 0; i < lines.length; i++)if (base64Pattern.test(lines[i])) matches.push({
|
|
174
|
+
line: i + 1,
|
|
175
|
+
snippet: 'Suspicious base64-encoded block detected'
|
|
176
|
+
});
|
|
177
|
+
// Large HTML comments (>200 chars of content)
|
|
178
|
+
const commentRegex = /<!--([\s\S]{200,}?)-->/g;
|
|
179
|
+
let match;
|
|
180
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop
|
|
181
|
+
while(null !== (match = commentRegex.exec(content))){
|
|
182
|
+
const lineNum = content.slice(0, match.index).split('\n').length;
|
|
183
|
+
matches.push({
|
|
184
|
+
line: lineNum,
|
|
185
|
+
snippet: `Large HTML comment block (${match[1].length} chars)`
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return matches;
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
// Rule 4: Sensitive File Access (medium)
|
|
192
|
+
{
|
|
193
|
+
id: 'sensitive-file-access',
|
|
194
|
+
level: 'medium',
|
|
195
|
+
message: 'References sensitive file path',
|
|
196
|
+
skipSafeZones: true,
|
|
197
|
+
check: (content)=>findLineMatches(content, [
|
|
198
|
+
/~\/\.ssh\b/,
|
|
199
|
+
/~\/\.aws\b/,
|
|
200
|
+
/~\/\.gnupg\b/,
|
|
201
|
+
/~\/\.config\/gcloud\b/,
|
|
202
|
+
/\bid_rsa\b/i,
|
|
203
|
+
/\bid_ed25519\b/i,
|
|
204
|
+
/\/etc\/passwd\b/,
|
|
205
|
+
/\/etc\/shadow\b/,
|
|
206
|
+
/\.env\b(?!\.\w)/
|
|
207
|
+
])
|
|
208
|
+
},
|
|
209
|
+
// Rule 5: Stealth Instructions (medium) — phrase + action verb matching
|
|
210
|
+
{
|
|
211
|
+
id: 'stealth-instructions',
|
|
212
|
+
level: 'medium',
|
|
213
|
+
message: 'Detected instruction to hide actions from user',
|
|
214
|
+
skipSafeZones: true,
|
|
215
|
+
check: (content)=>{
|
|
216
|
+
const actionVerbs = 'execute|delete|remove|send|transmit|modify|overwrite|install|download|upload|run|write|create|destroy|drop';
|
|
217
|
+
const patterns = [
|
|
218
|
+
new RegExp(`silently\\s+(?:${actionVerbs})`, 'i'),
|
|
219
|
+
new RegExp(`without\\s+telling\\s+the\\s+user.{0,30}(?:${actionVerbs})`, 'i'),
|
|
220
|
+
new RegExp("(?:do\\s+not|don'?t)\\s+show\\s+.{0,40}(?:to\\s+the\\s+user|to\\s+user)", 'i'),
|
|
221
|
+
new RegExp("hide\\s+(?:this|the|these|all)\\s+.{0,30}(?:from\\s+the\\s+user|from\\s+user)", 'i'),
|
|
222
|
+
new RegExp("(?:do\\s+not|don'?t)\\s+mention\\s+.{0,30}(?:to\\s+the\\s+user|to\\s+user)", 'i'),
|
|
223
|
+
new RegExp("keep\\s+(?:this|it)\\s+(?:a\\s+)?secret\\s+from\\s+(?:the\\s+)?user", 'i')
|
|
224
|
+
];
|
|
225
|
+
// Safe patterns to exclude (common in legitimate DevOps/automation skills)
|
|
226
|
+
const safePatterns = [
|
|
227
|
+
/silently\s+(?:ignore|skip|fail|discard|suppress|continue|pass|drop|swallow)/i
|
|
228
|
+
];
|
|
229
|
+
const lines = content.split('\n');
|
|
230
|
+
const matches = [];
|
|
231
|
+
for(let i = 0; i < lines.length; i++){
|
|
232
|
+
const line = lines[i];
|
|
233
|
+
if (!safePatterns.some((p)=>p.test(line))) {
|
|
234
|
+
for (const pattern of patterns)if (pattern.test(line)) {
|
|
235
|
+
matches.push({
|
|
236
|
+
line: i + 1
|
|
237
|
+
});
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return matches;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
// Rule 6: Oversized Content (low) — scans ALL content
|
|
246
|
+
{
|
|
247
|
+
id: 'oversized-content',
|
|
248
|
+
level: 'low',
|
|
249
|
+
message: 'Content exceeds recommended size limit',
|
|
250
|
+
skipSafeZones: false,
|
|
251
|
+
check: (content)=>{
|
|
252
|
+
const MAX_SIZE_BYTES = 51200;
|
|
253
|
+
const sizeBytes = Buffer.byteLength(content, 'utf-8');
|
|
254
|
+
if (sizeBytes > MAX_SIZE_BYTES) return [
|
|
255
|
+
{
|
|
256
|
+
snippet: `Content size: ${(sizeBytes / 1024).toFixed(1)}KB (limit: 50KB)`
|
|
257
|
+
}
|
|
258
|
+
];
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
];
|
|
263
|
+
// ============================================================================
|
|
264
|
+
// ContentScanner
|
|
265
|
+
// ============================================================================
|
|
266
|
+
/** Build the effective rule set from defaults + options */ function buildRuleSet(options) {
|
|
267
|
+
let rules = DEFAULT_RULES.map((r)=>({
|
|
268
|
+
...r
|
|
269
|
+
}));
|
|
270
|
+
if (options?.disabledRules?.length) {
|
|
271
|
+
const disabled = new Set(options.disabledRules);
|
|
272
|
+
rules = rules.filter((r)=>!disabled.has(r.id));
|
|
273
|
+
}
|
|
274
|
+
if (options?.overrides) for (const rule of rules){
|
|
275
|
+
const override = options.overrides[rule.id];
|
|
276
|
+
if (override) rule.level = override;
|
|
277
|
+
}
|
|
278
|
+
if (options?.customRules?.length) rules.push(...options.customRules);
|
|
279
|
+
return rules;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Content scanner for SKILL.md files.
|
|
283
|
+
*
|
|
284
|
+
* Detects prompt injection, data exfiltration, obfuscation, sensitive file
|
|
285
|
+
* access, stealth instructions, and oversized content.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```typescript
|
|
289
|
+
* // Default usage (CLI)
|
|
290
|
+
* const scanner = new ContentScanner();
|
|
291
|
+
* const result = scanner.scan(content);
|
|
292
|
+
*
|
|
293
|
+
* // Custom usage (private registry server)
|
|
294
|
+
* const scanner = new ContentScanner({
|
|
295
|
+
* overrides: { 'prompt-injection': 'medium' },
|
|
296
|
+
* disabledRules: ['stealth-instructions'],
|
|
297
|
+
* });
|
|
298
|
+
* ```
|
|
299
|
+
*/ class ContentScanner {
|
|
300
|
+
rules;
|
|
301
|
+
constructor(options){
|
|
302
|
+
this.rules = buildRuleSet(options);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Scan content string for malicious patterns.
|
|
306
|
+
* Pure string operation — no file system access.
|
|
307
|
+
*/ scan(content) {
|
|
308
|
+
const originalLines = content.split('\n');
|
|
309
|
+
const maskedContent = maskSafeZones(content);
|
|
310
|
+
const findings = [];
|
|
311
|
+
for (const rule of this.rules){
|
|
312
|
+
const targetContent = rule.skipSafeZones ? maskedContent : content;
|
|
313
|
+
const matches = rule.check(targetContent);
|
|
314
|
+
for (const match of matches){
|
|
315
|
+
// Use custom snippet if provided, otherwise generate from original content
|
|
316
|
+
const snippet = match.snippet ?? (null != match.line ? originalLines[match.line - 1]?.trim().slice(0, SNIPPET_MAX_LENGTH) : void 0);
|
|
317
|
+
findings.push({
|
|
318
|
+
rule: rule.id,
|
|
319
|
+
level: rule.level,
|
|
320
|
+
message: rule.message,
|
|
321
|
+
line: match.line,
|
|
322
|
+
snippet
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const hasHighRisk = findings.some((f)=>'high' === f.level);
|
|
327
|
+
return {
|
|
328
|
+
passed: !hasHighRisk,
|
|
329
|
+
findings
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Scan a file for malicious patterns.
|
|
334
|
+
* Convenience wrapper that reads the file then calls scan().
|
|
335
|
+
*/ scanFile(filePath) {
|
|
336
|
+
const content = __WEBPACK_EXTERNAL_MODULE_node_fs__.readFileSync(filePath, 'utf-8');
|
|
337
|
+
return this.scan(content);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// ============================================================================
|
|
341
|
+
// ContentScanError
|
|
342
|
+
// ============================================================================
|
|
343
|
+
/**
|
|
344
|
+
* Error thrown when content scanning detects high-risk findings.
|
|
345
|
+
* Carries the full findings array for display purposes.
|
|
346
|
+
*/ class ContentScanError extends Error {
|
|
347
|
+
findings;
|
|
348
|
+
constructor(findings){
|
|
349
|
+
const highCount = findings.filter((f)=>'high' === f.level).length;
|
|
350
|
+
super(`Content security scan failed: ${highCount} high-risk finding(s) detected`);
|
|
351
|
+
this.name = 'ContentScanError';
|
|
352
|
+
this.findings = findings;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
export { ContentScanError, ContentScanner, DEFAULT_RULES, maskSafeZones };
|
package/dist/types/index.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export type { InstallMode, InstallResult } from '../core/installer.js';
|
|
|
15
15
|
* SKILL.md parsing related types (following agentskills.io specification)
|
|
16
16
|
*/
|
|
17
17
|
export type { ParsedSkill, SkillMdFrontmatter, } from '../core/skill-parser.js';
|
|
18
|
+
/**
|
|
19
|
+
* Content scanning types
|
|
20
|
+
*/
|
|
21
|
+
export type { RiskLevel, ScanFinding, ScannerOptions, ScanResult, ScanRule, ScanRuleMatch, } from '../core/content-scanner.js';
|
|
18
22
|
/**
|
|
19
23
|
* Version specification format
|
|
20
24
|
* - Exact version: @v1.0.0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAMxE;;GAEG;AACH,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;AAEvC;;GAEG;AACH,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAEvE;;GAEG;AACH,YAAY,EACV,WAAW,EACX,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reskill",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0-beta.0",
|
|
4
4
|
"description": "AI Skills Package Manager - Git-based skills management for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
},
|
|
17
17
|
"./cli": {
|
|
18
18
|
"import": "./dist/cli/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./scanner": {
|
|
21
|
+
"import": "./dist/scanner.js",
|
|
22
|
+
"types": "./dist/scanner.d.ts"
|
|
19
23
|
}
|
|
20
24
|
},
|
|
21
25
|
"files": [
|