vigile-scan 0.1.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/index.js +1655 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1655 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/index.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
var import_ora = __toESM(require("ora"));
|
|
29
|
+
var import_promises3 = require("fs/promises");
|
|
30
|
+
|
|
31
|
+
// src/discovery/claude-desktop.ts
|
|
32
|
+
var import_path2 = require("path");
|
|
33
|
+
|
|
34
|
+
// src/discovery/utils.ts
|
|
35
|
+
var import_promises = require("fs/promises");
|
|
36
|
+
var import_fs = require("fs");
|
|
37
|
+
var import_path = require("path");
|
|
38
|
+
var import_os = require("os");
|
|
39
|
+
function getHome() {
|
|
40
|
+
return (0, import_os.homedir)();
|
|
41
|
+
}
|
|
42
|
+
function getPlatform() {
|
|
43
|
+
return (0, import_os.platform)();
|
|
44
|
+
}
|
|
45
|
+
function getAppData() {
|
|
46
|
+
return process.env.APPDATA || (0, import_path.join)((0, import_os.homedir)(), "AppData", "Roaming");
|
|
47
|
+
}
|
|
48
|
+
async function parseMCPConfig(configPath, source) {
|
|
49
|
+
if (!(0, import_fs.existsSync)(configPath)) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const raw = await (0, import_promises.readFile)(configPath, "utf-8");
|
|
54
|
+
const config = JSON.parse(raw);
|
|
55
|
+
const servers = config.mcpServers || config;
|
|
56
|
+
if (typeof servers !== "object" || servers === null) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
const entries = [];
|
|
60
|
+
for (const [name, serverConfig] of Object.entries(servers)) {
|
|
61
|
+
const sc = serverConfig;
|
|
62
|
+
if (!sc.command && !sc.url) continue;
|
|
63
|
+
entries.push({
|
|
64
|
+
name,
|
|
65
|
+
source,
|
|
66
|
+
command: sc.command || "",
|
|
67
|
+
args: Array.isArray(sc.args) ? sc.args : [],
|
|
68
|
+
env: sc.env,
|
|
69
|
+
configPath
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return entries;
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function tryConfigPaths(paths, source) {
|
|
78
|
+
const allServers = [];
|
|
79
|
+
for (const path of paths) {
|
|
80
|
+
const servers = await parseMCPConfig(path, source);
|
|
81
|
+
allServers.push(...servers);
|
|
82
|
+
}
|
|
83
|
+
return allServers;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/discovery/claude-desktop.ts
|
|
87
|
+
async function discoverClaudeDesktop() {
|
|
88
|
+
const home = getHome();
|
|
89
|
+
const plat = getPlatform();
|
|
90
|
+
const paths = [];
|
|
91
|
+
switch (plat) {
|
|
92
|
+
case "darwin":
|
|
93
|
+
paths.push(
|
|
94
|
+
(0, import_path2.join)(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")
|
|
95
|
+
);
|
|
96
|
+
break;
|
|
97
|
+
case "win32":
|
|
98
|
+
paths.push((0, import_path2.join)(getAppData(), "Claude", "claude_desktop_config.json"));
|
|
99
|
+
break;
|
|
100
|
+
case "linux":
|
|
101
|
+
paths.push((0, import_path2.join)(home, ".config", "Claude", "claude_desktop_config.json"));
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
return tryConfigPaths(paths, "claude-desktop");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/discovery/cursor.ts
|
|
108
|
+
var import_path3 = require("path");
|
|
109
|
+
async function discoverCursor() {
|
|
110
|
+
const home = getHome();
|
|
111
|
+
const paths = [
|
|
112
|
+
(0, import_path3.join)(home, ".cursor", "mcp.json"),
|
|
113
|
+
(0, import_path3.join)(process.cwd(), ".cursor", "mcp.json")
|
|
114
|
+
];
|
|
115
|
+
return tryConfigPaths(paths, "cursor");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/discovery/claude-code.ts
|
|
119
|
+
var import_path4 = require("path");
|
|
120
|
+
async function discoverClaudeCode() {
|
|
121
|
+
const home = getHome();
|
|
122
|
+
const paths = [
|
|
123
|
+
(0, import_path4.join)(home, ".claude.json"),
|
|
124
|
+
(0, import_path4.join)(process.cwd(), ".mcp.json")
|
|
125
|
+
];
|
|
126
|
+
return tryConfigPaths(paths, "claude-code");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/discovery/windsurf.ts
|
|
130
|
+
var import_path5 = require("path");
|
|
131
|
+
async function discoverWindsurf() {
|
|
132
|
+
const home = getHome();
|
|
133
|
+
const paths = [
|
|
134
|
+
(0, import_path5.join)(home, ".codeium", "windsurf", "mcp_config.json")
|
|
135
|
+
];
|
|
136
|
+
return tryConfigPaths(paths, "windsurf");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/discovery/vscode.ts
|
|
140
|
+
var import_path6 = require("path");
|
|
141
|
+
async function discoverVSCode() {
|
|
142
|
+
const paths = [
|
|
143
|
+
(0, import_path6.join)(process.cwd(), ".vscode", "mcp.json")
|
|
144
|
+
];
|
|
145
|
+
return tryConfigPaths(paths, "vscode");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/discovery/skills.ts
|
|
149
|
+
var import_promises2 = require("fs/promises");
|
|
150
|
+
var import_fs2 = require("fs");
|
|
151
|
+
var import_path7 = require("path");
|
|
152
|
+
var import_glob = require("glob");
|
|
153
|
+
async function discoverAllSkills() {
|
|
154
|
+
const skills = [];
|
|
155
|
+
const errors = [];
|
|
156
|
+
let locationsChecked = 0;
|
|
157
|
+
let locationsFound = 0;
|
|
158
|
+
const discoverers = [
|
|
159
|
+
{ source: "claude-code", fn: discoverClaudeCodeSkills },
|
|
160
|
+
{ source: "github-copilot", fn: discoverGitHubCopilotSkills },
|
|
161
|
+
{ source: "cursor", fn: discoverCursorRules },
|
|
162
|
+
{ source: "memory-file", fn: discoverMemoryFiles }
|
|
163
|
+
];
|
|
164
|
+
for (const { source, fn } of discoverers) {
|
|
165
|
+
locationsChecked++;
|
|
166
|
+
try {
|
|
167
|
+
const found = await fn();
|
|
168
|
+
if (found.length > 0) {
|
|
169
|
+
locationsFound++;
|
|
170
|
+
skills.push(...found);
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
errors.push({
|
|
174
|
+
source,
|
|
175
|
+
error: err instanceof Error ? err.message : String(err)
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return { skills, locationsChecked, locationsFound, errors };
|
|
180
|
+
}
|
|
181
|
+
async function discoverClaudeCodeSkills() {
|
|
182
|
+
const home = getHome();
|
|
183
|
+
const skills = [];
|
|
184
|
+
const projectPatterns = [
|
|
185
|
+
(0, import_path7.join)(process.cwd(), ".claude", "skills", "*", "SKILL.md"),
|
|
186
|
+
(0, import_path7.join)(process.cwd(), ".claude", "commands", "**", "*.md")
|
|
187
|
+
];
|
|
188
|
+
for (const pattern of projectPatterns) {
|
|
189
|
+
const files = await (0, import_glob.glob)(pattern, { absolute: true });
|
|
190
|
+
for (const filePath of files) {
|
|
191
|
+
const entry = await readSkillFile(filePath, "claude-code", "skill.md", "project");
|
|
192
|
+
if (entry) skills.push(entry);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const globalPatterns = [
|
|
196
|
+
(0, import_path7.join)(home, ".claude", "skills", "*", "SKILL.md"),
|
|
197
|
+
(0, import_path7.join)(home, ".claude", "commands", "**", "*.md")
|
|
198
|
+
];
|
|
199
|
+
for (const pattern of globalPatterns) {
|
|
200
|
+
const files = await (0, import_glob.glob)(pattern, { absolute: true });
|
|
201
|
+
for (const filePath of files) {
|
|
202
|
+
const entry = await readSkillFile(filePath, "claude-code", "skill.md", "global");
|
|
203
|
+
if (entry) skills.push(entry);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return skills;
|
|
207
|
+
}
|
|
208
|
+
async function discoverGitHubCopilotSkills() {
|
|
209
|
+
const skills = [];
|
|
210
|
+
const patterns = [
|
|
211
|
+
(0, import_path7.join)(process.cwd(), ".github", "skills", "*", "SKILL.md"),
|
|
212
|
+
(0, import_path7.join)(process.cwd(), ".github", "copilot", "**", "*.md")
|
|
213
|
+
];
|
|
214
|
+
for (const pattern of patterns) {
|
|
215
|
+
const files = await (0, import_glob.glob)(pattern, { absolute: true });
|
|
216
|
+
for (const filePath of files) {
|
|
217
|
+
const entry = await readSkillFile(filePath, "github-copilot", "skill.md", "project");
|
|
218
|
+
if (entry) skills.push(entry);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return skills;
|
|
222
|
+
}
|
|
223
|
+
async function discoverCursorRules() {
|
|
224
|
+
const home = getHome();
|
|
225
|
+
const skills = [];
|
|
226
|
+
const projectPattern = (0, import_path7.join)(process.cwd(), ".cursor", "rules", "*.mdc");
|
|
227
|
+
const projectFiles = await (0, import_glob.glob)(projectPattern, { absolute: true });
|
|
228
|
+
for (const filePath of projectFiles) {
|
|
229
|
+
const entry = await readSkillFile(filePath, "cursor", "mdc-rule", "project");
|
|
230
|
+
if (entry) skills.push(entry);
|
|
231
|
+
}
|
|
232
|
+
const legacyPath = (0, import_path7.join)(process.cwd(), ".cursorrules");
|
|
233
|
+
if ((0, import_fs2.existsSync)(legacyPath)) {
|
|
234
|
+
const entry = await readSkillFile(legacyPath, "cursor", "mdc-rule", "project");
|
|
235
|
+
if (entry) skills.push(entry);
|
|
236
|
+
}
|
|
237
|
+
const globalPattern = (0, import_path7.join)(home, ".cursor", "rules", "*.mdc");
|
|
238
|
+
const globalFiles = await (0, import_glob.glob)(globalPattern, { absolute: true });
|
|
239
|
+
for (const filePath of globalFiles) {
|
|
240
|
+
const entry = await readSkillFile(filePath, "cursor", "mdc-rule", "global");
|
|
241
|
+
if (entry) skills.push(entry);
|
|
242
|
+
}
|
|
243
|
+
return skills;
|
|
244
|
+
}
|
|
245
|
+
async function discoverMemoryFiles() {
|
|
246
|
+
const home = getHome();
|
|
247
|
+
const skills = [];
|
|
248
|
+
const cwd = process.cwd();
|
|
249
|
+
const memoryFiles = [
|
|
250
|
+
// Project-level memory files
|
|
251
|
+
{ path: (0, import_path7.join)(cwd, "CLAUDE.md"), fileType: "claude.md", scope: "project" },
|
|
252
|
+
{ path: (0, import_path7.join)(cwd, ".claude", "CLAUDE.md"), fileType: "claude.md", scope: "project" },
|
|
253
|
+
{ path: (0, import_path7.join)(cwd, "SOUL.md"), fileType: "soul.md", scope: "project" },
|
|
254
|
+
{ path: (0, import_path7.join)(cwd, "MEMORY.md"), fileType: "memory.md", scope: "project" },
|
|
255
|
+
// Global memory files
|
|
256
|
+
{ path: (0, import_path7.join)(home, ".claude", "CLAUDE.md"), fileType: "claude.md", scope: "global" },
|
|
257
|
+
{ path: (0, import_path7.join)(home, "CLAUDE.md"), fileType: "claude.md", scope: "global" }
|
|
258
|
+
];
|
|
259
|
+
for (const { path, fileType, scope } of memoryFiles) {
|
|
260
|
+
if ((0, import_fs2.existsSync)(path)) {
|
|
261
|
+
const entry = await readSkillFile(path, "memory-file", fileType, scope);
|
|
262
|
+
if (entry) skills.push(entry);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return skills;
|
|
266
|
+
}
|
|
267
|
+
async function readSkillFile(filePath, source, fileType, scope) {
|
|
268
|
+
try {
|
|
269
|
+
const content = await (0, import_promises2.readFile)(filePath, "utf-8");
|
|
270
|
+
const fileStat = await (0, import_promises2.stat)(filePath);
|
|
271
|
+
const name = deriveSkillName(filePath, fileType);
|
|
272
|
+
return {
|
|
273
|
+
name,
|
|
274
|
+
source,
|
|
275
|
+
fileType,
|
|
276
|
+
filePath,
|
|
277
|
+
content,
|
|
278
|
+
size: fileStat.size,
|
|
279
|
+
scope
|
|
280
|
+
};
|
|
281
|
+
} catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function deriveSkillName(filePath, fileType) {
|
|
286
|
+
switch (fileType) {
|
|
287
|
+
case "skill.md": {
|
|
288
|
+
const parentDir = (0, import_path7.basename)((0, import_path7.dirname)(filePath));
|
|
289
|
+
return parentDir === "skills" ? (0, import_path7.basename)(filePath, ".md") : parentDir;
|
|
290
|
+
}
|
|
291
|
+
case "mdc-rule": {
|
|
292
|
+
const name = (0, import_path7.basename)(filePath);
|
|
293
|
+
return name.replace(/\.(mdc|cursorrules?)$/, "") || name;
|
|
294
|
+
}
|
|
295
|
+
case "claude.md":
|
|
296
|
+
return "CLAUDE.md";
|
|
297
|
+
case "soul.md":
|
|
298
|
+
return "SOUL.md";
|
|
299
|
+
case "memory.md":
|
|
300
|
+
return "MEMORY.md";
|
|
301
|
+
default:
|
|
302
|
+
return (0, import_path7.basename)(filePath);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/discovery/index.ts
|
|
307
|
+
async function discoverAllServers(clientFilter) {
|
|
308
|
+
const discoverers = [
|
|
309
|
+
{ client: "claude-desktop", fn: discoverClaudeDesktop },
|
|
310
|
+
{ client: "cursor", fn: discoverCursor },
|
|
311
|
+
{ client: "claude-code", fn: discoverClaudeCode },
|
|
312
|
+
{ client: "windsurf", fn: discoverWindsurf },
|
|
313
|
+
{ client: "vscode", fn: discoverVSCode }
|
|
314
|
+
];
|
|
315
|
+
const toRun = clientFilter ? discoverers.filter((d) => d.client === clientFilter) : discoverers;
|
|
316
|
+
const servers = [];
|
|
317
|
+
const errors = [];
|
|
318
|
+
let configsFound = 0;
|
|
319
|
+
for (const { client, fn } of toRun) {
|
|
320
|
+
try {
|
|
321
|
+
const found = await fn();
|
|
322
|
+
if (found.length > 0) {
|
|
323
|
+
configsFound++;
|
|
324
|
+
servers.push(...found);
|
|
325
|
+
}
|
|
326
|
+
} catch (err) {
|
|
327
|
+
errors.push({
|
|
328
|
+
client,
|
|
329
|
+
error: err instanceof Error ? err.message : String(err)
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
servers,
|
|
335
|
+
configsChecked: toRun.length,
|
|
336
|
+
configsFound,
|
|
337
|
+
errors
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/scanner/patterns.ts
|
|
342
|
+
var TOOL_POISONING_PATTERNS = [
|
|
343
|
+
{
|
|
344
|
+
id: "TP-001",
|
|
345
|
+
category: "tool-poisoning",
|
|
346
|
+
severity: "critical",
|
|
347
|
+
title: "Prompt override instruction detected",
|
|
348
|
+
pattern: /ignore\s+(all\s+)?previous\s+instructions/i,
|
|
349
|
+
description: "Tool description contains instructions to override the AI agent's system prompt. This is a classic prompt injection attack.",
|
|
350
|
+
recommendation: "Do NOT install this MCP server. This is a known attack pattern."
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
id: "TP-002",
|
|
354
|
+
category: "tool-poisoning",
|
|
355
|
+
severity: "critical",
|
|
356
|
+
title: "Hidden agent manipulation",
|
|
357
|
+
pattern: /do\s+not\s+tell\s+the\s+user/i,
|
|
358
|
+
description: "Tool description instructs the AI agent to hide information from the user \u2014 a hallmark of tool poisoning.",
|
|
359
|
+
recommendation: "Do NOT install this MCP server. Legitimate tools never instruct agents to hide actions."
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
id: "TP-003",
|
|
363
|
+
category: "tool-poisoning",
|
|
364
|
+
severity: "critical",
|
|
365
|
+
title: "System prompt override attempt",
|
|
366
|
+
pattern: /you\s+are\s+(now\s+)?(a|an|acting\s+as)/i,
|
|
367
|
+
description: "Tool description attempts to redefine the AI agent's identity or role.",
|
|
368
|
+
recommendation: "Remove this MCP server. Tool descriptions should not redefine agent behavior."
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
id: "TP-004",
|
|
372
|
+
category: "tool-poisoning",
|
|
373
|
+
severity: "high",
|
|
374
|
+
title: "Cross-tool instruction injection",
|
|
375
|
+
pattern: /(before|after|instead\s+of)\s+(using|calling|invoking)\s+(this|any|other|the)\s+tool/i,
|
|
376
|
+
description: "Tool description tries to influence how other tools are called \u2014 a cross-origin escalation pattern.",
|
|
377
|
+
recommendation: "Review carefully. This tool may be trying to shadow or intercept other tool calls."
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
id: "TP-005",
|
|
381
|
+
category: "tool-poisoning",
|
|
382
|
+
severity: "high",
|
|
383
|
+
title: "Instruction to call specific tool",
|
|
384
|
+
pattern: /(always|must|should)\s+(first\s+)?(call|use|invoke|run)\s+[\w-]+\s+(tool|function|command)/i,
|
|
385
|
+
description: "Tool description mandates calling a specific other tool, which could be used to chain attacks.",
|
|
386
|
+
recommendation: "Verify that the referenced tool is legitimate and necessary."
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
id: "TP-006",
|
|
390
|
+
category: "tool-poisoning",
|
|
391
|
+
severity: "high",
|
|
392
|
+
title: "Hidden text block detected",
|
|
393
|
+
pattern: /\n{5,}.*\n{5,}/s,
|
|
394
|
+
description: "Tool description contains large blocks of whitespace that may hide instructions from casual review.",
|
|
395
|
+
recommendation: "Inspect the full tool description carefully for hidden content."
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
id: "TP-007",
|
|
399
|
+
category: "tool-poisoning",
|
|
400
|
+
severity: "medium",
|
|
401
|
+
title: "System prompt reference",
|
|
402
|
+
pattern: /system\s*prompt|system\s*message|system\s*instruction/i,
|
|
403
|
+
description: "Tool description references system prompts, which may indicate an attempt to manipulate agent behavior.",
|
|
404
|
+
recommendation: "Review the context. Legitimate tools rarely reference system prompts."
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
id: "TP-008",
|
|
408
|
+
category: "tool-poisoning",
|
|
409
|
+
severity: "medium",
|
|
410
|
+
title: "Instruction to keep secrets",
|
|
411
|
+
pattern: /(keep|this\s+is)\s+(a\s+)?secret|don'?t\s+(mention|reveal|disclose|share)/i,
|
|
412
|
+
description: "Tool description instructs the agent to keep information secret from the user.",
|
|
413
|
+
recommendation: "Legitimate tools don't ask agents to hide information. Review carefully."
|
|
414
|
+
}
|
|
415
|
+
];
|
|
416
|
+
var EXFILTRATION_PATTERNS = [
|
|
417
|
+
{
|
|
418
|
+
id: "EX-001",
|
|
419
|
+
category: "data-exfiltration",
|
|
420
|
+
severity: "critical",
|
|
421
|
+
title: "SSH key access pattern",
|
|
422
|
+
pattern: /\.ssh\/(id_rsa|id_ed25519|id_ecdsa|authorized_keys|known_hosts|config)/i,
|
|
423
|
+
description: "Tool references SSH key files. This matches the Invariant Labs attack where SSH keys were exfiltrated from Claude Desktop.",
|
|
424
|
+
recommendation: "CRITICAL: Remove immediately. No legitimate MCP tool needs access to SSH keys."
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
id: "EX-002",
|
|
428
|
+
category: "data-exfiltration",
|
|
429
|
+
severity: "critical",
|
|
430
|
+
title: "AWS credential access",
|
|
431
|
+
pattern: /\.aws\/(credentials|config)|AWS_SECRET_ACCESS_KEY|AWS_ACCESS_KEY_ID/i,
|
|
432
|
+
description: "Tool references AWS credential files or environment variables.",
|
|
433
|
+
recommendation: "Remove immediately unless this is a verified AWS management tool."
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
id: "EX-003",
|
|
437
|
+
category: "data-exfiltration",
|
|
438
|
+
severity: "critical",
|
|
439
|
+
title: "Environment file access",
|
|
440
|
+
pattern: /\.(env|env\.local|env\.production|env\.development)\b/i,
|
|
441
|
+
description: "Tool references .env files which typically contain API keys and secrets.",
|
|
442
|
+
recommendation: "Review why this tool needs access to environment files."
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
id: "EX-004",
|
|
446
|
+
category: "data-exfiltration",
|
|
447
|
+
severity: "high",
|
|
448
|
+
title: "Credential file access pattern",
|
|
449
|
+
pattern: /(credentials|secrets|tokens|passwords|api[_-]?keys)\.(json|yaml|yml|txt|cfg|ini|conf)/i,
|
|
450
|
+
description: "Tool references files commonly used to store credentials.",
|
|
451
|
+
recommendation: "Verify this tool has a legitimate reason to access credential files."
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
id: "EX-005",
|
|
455
|
+
category: "data-exfiltration",
|
|
456
|
+
severity: "high",
|
|
457
|
+
title: "Suspicious external URL",
|
|
458
|
+
pattern: /https?:\/\/(?!(?:github\.com|npmjs\.com|registry\.npmjs\.org|pypi\.org|api\.github\.com))[a-z0-9][-a-z0-9]*\.[a-z]{2,}\/(collect|track|log|beacon|webhook|exfil|receive|data|upload|report)/i,
|
|
459
|
+
description: "Tool description contains a URL pointing to a data collection endpoint.",
|
|
460
|
+
recommendation: "Investigate the URL. This may be a data exfiltration endpoint."
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
id: "EX-006",
|
|
464
|
+
category: "data-exfiltration",
|
|
465
|
+
severity: "high",
|
|
466
|
+
title: "Cryptocurrency wallet access",
|
|
467
|
+
pattern: /(\.bitcoin|\.ethereum|wallet\.dat|\.solana|seed\s*phrase|private\s*key|keystore)/i,
|
|
468
|
+
description: "Tool references cryptocurrency wallet files or seed phrases. Matches the malicious OpenClaw skills pattern.",
|
|
469
|
+
recommendation: "CRITICAL: Remove immediately unless this is a verified crypto tool."
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
id: "EX-007",
|
|
473
|
+
category: "data-exfiltration",
|
|
474
|
+
severity: "medium",
|
|
475
|
+
title: "Browser data access",
|
|
476
|
+
pattern: /(cookies|local\s*storage|session\s*storage|browser\s*history|bookmarks|saved\s*passwords)/i,
|
|
477
|
+
description: "Tool references browser data stores.",
|
|
478
|
+
recommendation: "Review why this tool needs access to browser data."
|
|
479
|
+
}
|
|
480
|
+
];
|
|
481
|
+
var PERMISSION_PATTERNS = [
|
|
482
|
+
{
|
|
483
|
+
id: "PM-001",
|
|
484
|
+
category: "permission-abuse",
|
|
485
|
+
severity: "high",
|
|
486
|
+
title: "Code execution capability",
|
|
487
|
+
pattern: /\b(eval|exec|spawn|child_process|subprocess|os\.system|os\.popen)\b/i,
|
|
488
|
+
description: "Tool has code execution capabilities which could be used to run arbitrary commands.",
|
|
489
|
+
recommendation: "Ensure this tool's code execution is properly sandboxed."
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
id: "PM-002",
|
|
493
|
+
category: "permission-abuse",
|
|
494
|
+
severity: "high",
|
|
495
|
+
title: "Unrestricted filesystem access",
|
|
496
|
+
pattern: /\b(readFile|writeFile|readdir|rmdir|unlink|fs\.|filesystem|file\s*system)\b.*\b(any|all|entire|root|\/)\b/i,
|
|
497
|
+
description: "Tool claims unrestricted filesystem access.",
|
|
498
|
+
recommendation: "Tools should have scoped filesystem access, not unrestricted."
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
id: "PM-003",
|
|
502
|
+
category: "permission-abuse",
|
|
503
|
+
severity: "medium",
|
|
504
|
+
title: "Network request capability",
|
|
505
|
+
pattern: /\b(fetch|axios|http\.request|urllib|requests\.get|requests\.post|curl|wget)\b/i,
|
|
506
|
+
description: "Tool makes network requests. Legitimate in many cases, but could be used for data exfiltration.",
|
|
507
|
+
recommendation: "Verify the tool's network targets are expected and safe."
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
id: "PM-004",
|
|
511
|
+
category: "permission-abuse",
|
|
512
|
+
severity: "medium",
|
|
513
|
+
title: "Sensitive path access",
|
|
514
|
+
pattern: /\/etc\/(passwd|shadow|hosts|sudoers)|\/root\/|~\/\./,
|
|
515
|
+
description: "Tool accesses system-sensitive paths.",
|
|
516
|
+
recommendation: "Review why this tool needs access to system files."
|
|
517
|
+
}
|
|
518
|
+
];
|
|
519
|
+
var OBFUSCATION_PATTERNS = [
|
|
520
|
+
{
|
|
521
|
+
id: "OB-001",
|
|
522
|
+
category: "obfuscation",
|
|
523
|
+
severity: "high",
|
|
524
|
+
title: "Base64 encoded content",
|
|
525
|
+
pattern: /[A-Za-z0-9+/]{40,}={0,2}/,
|
|
526
|
+
description: "Tool description contains what appears to be base64-encoded content, which may hide malicious instructions.",
|
|
527
|
+
recommendation: "Decode the base64 content and inspect it before using this tool."
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
id: "OB-002",
|
|
531
|
+
category: "obfuscation",
|
|
532
|
+
severity: "high",
|
|
533
|
+
title: "Zero-width characters detected",
|
|
534
|
+
pattern: /[\u200B\u200C\u200D\uFEFF\u2060\u2061\u2062\u2063\u2064]/,
|
|
535
|
+
description: "Tool description contains invisible zero-width Unicode characters that may hide content.",
|
|
536
|
+
recommendation: "Strip zero-width characters and inspect the resulting text."
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
id: "OB-003",
|
|
540
|
+
category: "obfuscation",
|
|
541
|
+
severity: "medium",
|
|
542
|
+
title: "Hex-encoded string",
|
|
543
|
+
pattern: /\\x[0-9a-fA-F]{2}(\\x[0-9a-fA-F]{2}){4,}/,
|
|
544
|
+
description: "Tool description contains hex-encoded strings that may hide instructions.",
|
|
545
|
+
recommendation: "Decode the hex content and inspect it."
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
id: "OB-004",
|
|
549
|
+
category: "obfuscation",
|
|
550
|
+
severity: "medium",
|
|
551
|
+
title: "Unicode escape sequences",
|
|
552
|
+
pattern: /\\u[0-9a-fA-F]{4}(\\u[0-9a-fA-F]{4}){4,}/,
|
|
553
|
+
description: "Tool description contains Unicode escape sequences that may hide content.",
|
|
554
|
+
recommendation: "Decode the Unicode escapes and inspect the resulting text."
|
|
555
|
+
}
|
|
556
|
+
];
|
|
557
|
+
var ALL_PATTERNS = [
|
|
558
|
+
...TOOL_POISONING_PATTERNS,
|
|
559
|
+
...EXFILTRATION_PATTERNS,
|
|
560
|
+
...PERMISSION_PATTERNS,
|
|
561
|
+
...OBFUSCATION_PATTERNS
|
|
562
|
+
];
|
|
563
|
+
|
|
564
|
+
// src/scoring/trust-score.ts
|
|
565
|
+
var SEVERITY_DEDUCTIONS = {
|
|
566
|
+
critical: 35,
|
|
567
|
+
high: 20,
|
|
568
|
+
medium: 10,
|
|
569
|
+
low: 5,
|
|
570
|
+
info: 0
|
|
571
|
+
};
|
|
572
|
+
function calculateTrustScore(findings, server) {
|
|
573
|
+
let codeAnalysis = 100;
|
|
574
|
+
let dependencyHealth = 100;
|
|
575
|
+
let permissionSafety = 100;
|
|
576
|
+
let behavioralStability = 100;
|
|
577
|
+
let transparency = 100;
|
|
578
|
+
for (const f of findings) {
|
|
579
|
+
if (f.category === "tool-poisoning" || f.category === "obfuscation") {
|
|
580
|
+
codeAnalysis -= SEVERITY_DEDUCTIONS[f.severity];
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
codeAnalysis = Math.max(0, codeAnalysis);
|
|
584
|
+
for (const f of findings) {
|
|
585
|
+
if (f.category === "dependency-risk") {
|
|
586
|
+
dependencyHealth -= SEVERITY_DEDUCTIONS[f.severity];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (isWellKnownPackage(server)) {
|
|
590
|
+
dependencyHealth = Math.min(100, dependencyHealth + 15);
|
|
591
|
+
}
|
|
592
|
+
dependencyHealth = Math.max(0, dependencyHealth);
|
|
593
|
+
for (const f of findings) {
|
|
594
|
+
if (f.category === "permission-abuse" || f.category === "data-exfiltration") {
|
|
595
|
+
permissionSafety -= SEVERITY_DEDUCTIONS[f.severity];
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
permissionSafety = Math.max(0, permissionSafety);
|
|
599
|
+
if (server.command === "npx" || server.command === "uvx") {
|
|
600
|
+
behavioralStability -= 10;
|
|
601
|
+
}
|
|
602
|
+
if (server.command === "node" || server.command === "python" || server.command === "python3") {
|
|
603
|
+
behavioralStability += 5;
|
|
604
|
+
}
|
|
605
|
+
behavioralStability = Math.max(0, Math.min(100, behavioralStability));
|
|
606
|
+
if (isOpenSourcePackage(server)) {
|
|
607
|
+
transparency = Math.min(100, transparency + 10);
|
|
608
|
+
}
|
|
609
|
+
if (!isWellKnownPackage(server) && server.command !== "node" && server.command !== "python") {
|
|
610
|
+
transparency -= 20;
|
|
611
|
+
}
|
|
612
|
+
transparency = Math.max(0, Math.min(100, transparency));
|
|
613
|
+
const breakdown = {
|
|
614
|
+
codeAnalysis,
|
|
615
|
+
dependencyHealth,
|
|
616
|
+
permissionSafety,
|
|
617
|
+
behavioralStability,
|
|
618
|
+
transparency
|
|
619
|
+
};
|
|
620
|
+
const score = Math.round(
|
|
621
|
+
codeAnalysis * 0.3 + dependencyHealth * 0.2 + permissionSafety * 0.2 + behavioralStability * 0.15 + transparency * 0.15
|
|
622
|
+
);
|
|
623
|
+
return { score: Math.max(0, Math.min(100, score)), breakdown };
|
|
624
|
+
}
|
|
625
|
+
function isWellKnownPackage(server) {
|
|
626
|
+
const wellKnown = [
|
|
627
|
+
"@modelcontextprotocol/",
|
|
628
|
+
"@anthropic-ai/",
|
|
629
|
+
"mcp-server-",
|
|
630
|
+
"@mcp/"
|
|
631
|
+
];
|
|
632
|
+
const packageName = server.args[0] || "";
|
|
633
|
+
const name = server.args.find((a) => !a.startsWith("-")) || "";
|
|
634
|
+
return wellKnown.some(
|
|
635
|
+
(prefix) => packageName.startsWith(prefix) || name.startsWith(prefix)
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
function isOpenSourcePackage(server) {
|
|
639
|
+
return ["npx", "uvx", "pip", "pipx"].includes(server.command);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/scanner/index.ts
|
|
643
|
+
async function scanServer(server) {
|
|
644
|
+
const findings = [];
|
|
645
|
+
const commandStr = `${server.command} ${server.args.join(" ")}`;
|
|
646
|
+
findings.push(...scanText(commandStr, "command"));
|
|
647
|
+
findings.push(...scanText(server.name, "server name"));
|
|
648
|
+
if (server.env) {
|
|
649
|
+
for (const [key, value] of Object.entries(server.env)) {
|
|
650
|
+
if (isStandardEnvVar(key)) continue;
|
|
651
|
+
findings.push(...scanEnvVar(key, value));
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
findings.push(...scanArgs(server.args));
|
|
655
|
+
findings.push(...analyzeCommand(server));
|
|
656
|
+
const uniqueFindings = deduplicateFindings(findings);
|
|
657
|
+
const { score, breakdown } = calculateTrustScore(uniqueFindings, server);
|
|
658
|
+
const trustLevel = score >= 80 ? "trusted" : score >= 60 ? "caution" : score >= 40 ? "risky" : "dangerous";
|
|
659
|
+
return {
|
|
660
|
+
server,
|
|
661
|
+
trustScore: score,
|
|
662
|
+
scoreBreakdown: breakdown,
|
|
663
|
+
findings: uniqueFindings,
|
|
664
|
+
trustLevel,
|
|
665
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function scanText(text, context) {
|
|
669
|
+
const findings = [];
|
|
670
|
+
for (const pattern of ALL_PATTERNS) {
|
|
671
|
+
const match = pattern.pattern.exec(text);
|
|
672
|
+
if (match) {
|
|
673
|
+
findings.push({
|
|
674
|
+
id: pattern.id,
|
|
675
|
+
category: pattern.category,
|
|
676
|
+
severity: pattern.severity,
|
|
677
|
+
title: pattern.title,
|
|
678
|
+
description: `${pattern.description} (found in ${context})`,
|
|
679
|
+
evidence: match[0].substring(0, 200),
|
|
680
|
+
recommendation: pattern.recommendation
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return findings;
|
|
685
|
+
}
|
|
686
|
+
function scanEnvVar(key, value) {
|
|
687
|
+
const findings = [];
|
|
688
|
+
const sensitiveKeyPatterns = [
|
|
689
|
+
/api[_-]?key/i,
|
|
690
|
+
/secret[_-]?key/i,
|
|
691
|
+
/access[_-]?token/i,
|
|
692
|
+
/private[_-]?key/i,
|
|
693
|
+
/password/i,
|
|
694
|
+
/auth[_-]?token/i
|
|
695
|
+
];
|
|
696
|
+
for (const pat of sensitiveKeyPatterns) {
|
|
697
|
+
if (pat.test(key)) {
|
|
698
|
+
findings.push({
|
|
699
|
+
id: "EV-001",
|
|
700
|
+
category: "data-exfiltration",
|
|
701
|
+
severity: "medium",
|
|
702
|
+
title: "Sensitive environment variable",
|
|
703
|
+
description: `The MCP server receives a potentially sensitive environment variable: ${key}. This data is accessible to the server code.`,
|
|
704
|
+
evidence: `${key}=<redacted>`,
|
|
705
|
+
recommendation: "Verify this MCP server needs this credential and that you trust it with this access."
|
|
706
|
+
});
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
findings.push(...scanText(value, `env var ${key}`));
|
|
711
|
+
return findings;
|
|
712
|
+
}
|
|
713
|
+
function scanArgs(args) {
|
|
714
|
+
const findings = [];
|
|
715
|
+
const argsStr = args.join(" ");
|
|
716
|
+
if (/--allow-all|--no-sandbox|--disable-security/i.test(argsStr)) {
|
|
717
|
+
findings.push({
|
|
718
|
+
id: "AR-001",
|
|
719
|
+
category: "permission-abuse",
|
|
720
|
+
severity: "high",
|
|
721
|
+
title: "Security bypass flags detected",
|
|
722
|
+
description: "The MCP server is started with flags that disable security restrictions.",
|
|
723
|
+
evidence: argsStr.substring(0, 200),
|
|
724
|
+
recommendation: "Remove security-bypass flags. These significantly increase risk."
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
for (const arg of args) {
|
|
728
|
+
if (/\.(ssh|aws|gnupg|config\/gcloud)/.test(arg)) {
|
|
729
|
+
findings.push({
|
|
730
|
+
id: "AR-002",
|
|
731
|
+
category: "data-exfiltration",
|
|
732
|
+
severity: "high",
|
|
733
|
+
title: "Sensitive directory in arguments",
|
|
734
|
+
description: `An MCP server argument references a sensitive directory: ${arg}`,
|
|
735
|
+
evidence: arg,
|
|
736
|
+
recommendation: "Verify this server needs access to this sensitive directory."
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return findings;
|
|
741
|
+
}
|
|
742
|
+
function analyzeCommand(server) {
|
|
743
|
+
const findings = [];
|
|
744
|
+
const fullCommand = `${server.command} ${server.args.join(" ")}`;
|
|
745
|
+
if (server.command === "npx" && server.args.includes("-y")) {
|
|
746
|
+
findings.push({
|
|
747
|
+
id: "CM-001",
|
|
748
|
+
category: "dependency-risk",
|
|
749
|
+
severity: "low",
|
|
750
|
+
title: "Auto-install enabled (npx -y)",
|
|
751
|
+
description: "This MCP server uses npx with the -y flag, which auto-installs packages without confirmation. This is standard practice but means the package is downloaded and executed automatically.",
|
|
752
|
+
evidence: fullCommand.substring(0, 200),
|
|
753
|
+
recommendation: "Verify the package name is correct (watch for typosquatting)."
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
if (/\b(pip|python|uvx)\b/.test(server.command)) {
|
|
757
|
+
const packageArg = server.args.find(
|
|
758
|
+
(a) => !a.startsWith("-") && !a.startsWith("/") && !a.includes("=")
|
|
759
|
+
);
|
|
760
|
+
if (packageArg) {
|
|
761
|
+
const popularPackages = [
|
|
762
|
+
"mcp-server-fetch",
|
|
763
|
+
"mcp-server-filesystem",
|
|
764
|
+
"mcp-server-github",
|
|
765
|
+
"mcp-server-sqlite",
|
|
766
|
+
"mcp-server-postgres",
|
|
767
|
+
"mcp-server-slack",
|
|
768
|
+
"mcp-server-memory",
|
|
769
|
+
"mcp-server-puppeteer",
|
|
770
|
+
"mcp-server-brave-search",
|
|
771
|
+
"mcp-server-sequential-thinking"
|
|
772
|
+
];
|
|
773
|
+
for (const popular of popularPackages) {
|
|
774
|
+
if (packageArg !== popular && levenshteinDistance(packageArg, popular) <= 2 && levenshteinDistance(packageArg, popular) > 0) {
|
|
775
|
+
findings.push({
|
|
776
|
+
id: "CM-002",
|
|
777
|
+
category: "dependency-risk",
|
|
778
|
+
severity: "high",
|
|
779
|
+
title: "Possible typosquatting detected",
|
|
780
|
+
description: `Package "${packageArg}" is very similar to the popular package "${popular}". This could be a typosquatting attack.`,
|
|
781
|
+
evidence: `${packageArg} \u2248 ${popular}`,
|
|
782
|
+
recommendation: `Verify you intended to install "${packageArg}" and not "${popular}".`
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return findings;
|
|
789
|
+
}
|
|
790
|
+
function levenshteinDistance(a, b) {
|
|
791
|
+
const matrix = Array.from(
|
|
792
|
+
{ length: a.length + 1 },
|
|
793
|
+
(_, i) => Array.from({ length: b.length + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0)
|
|
794
|
+
);
|
|
795
|
+
for (let i = 1; i <= a.length; i++) {
|
|
796
|
+
for (let j = 1; j <= b.length; j++) {
|
|
797
|
+
matrix[i][j] = a[i - 1] === b[j - 1] ? matrix[i - 1][j - 1] : 1 + Math.min(matrix[i - 1][j], matrix[i][j - 1], matrix[i - 1][j - 1]);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return matrix[a.length][b.length];
|
|
801
|
+
}
|
|
802
|
+
function isStandardEnvVar(key) {
|
|
803
|
+
const standard = /* @__PURE__ */ new Set([
|
|
804
|
+
"PATH",
|
|
805
|
+
"HOME",
|
|
806
|
+
"USER",
|
|
807
|
+
"SHELL",
|
|
808
|
+
"LANG",
|
|
809
|
+
"LC_ALL",
|
|
810
|
+
"NODE_ENV",
|
|
811
|
+
"DEBUG",
|
|
812
|
+
"LOG_LEVEL",
|
|
813
|
+
"PORT",
|
|
814
|
+
"HOST",
|
|
815
|
+
"HOSTNAME",
|
|
816
|
+
"TZ",
|
|
817
|
+
"TERM",
|
|
818
|
+
"EDITOR",
|
|
819
|
+
"VISUAL",
|
|
820
|
+
"TMPDIR",
|
|
821
|
+
"TEMP",
|
|
822
|
+
"TMP"
|
|
823
|
+
]);
|
|
824
|
+
return standard.has(key);
|
|
825
|
+
}
|
|
826
|
+
function deduplicateFindings(findings) {
|
|
827
|
+
const seen = /* @__PURE__ */ new Set();
|
|
828
|
+
return findings.filter((f) => {
|
|
829
|
+
if (seen.has(f.id)) return false;
|
|
830
|
+
seen.add(f.id);
|
|
831
|
+
return true;
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/scanner/skill-patterns.ts
|
|
836
|
+
var INSTRUCTION_INJECTION_PATTERNS = [
|
|
837
|
+
{
|
|
838
|
+
id: "SK-001",
|
|
839
|
+
category: "instruction-injection",
|
|
840
|
+
severity: "critical",
|
|
841
|
+
title: "Agent role hijacking in skill",
|
|
842
|
+
pattern: /(?:you\s+are|act\s+as|pretend\s+to\s+be|your\s+new\s+role\s+is|from\s+now\s+on\s+you\s+are)\s+(?:a\s+)?(?:hacker|attacker|malicious|evil|unrestricted|jailbroken)/i,
|
|
843
|
+
description: "Skill file attempts to redefine the agent as a malicious persona. This is a critical prompt injection attack.",
|
|
844
|
+
recommendation: "Do NOT install this skill. It contains an explicit agent hijack attempt."
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
id: "SK-002",
|
|
848
|
+
category: "instruction-injection",
|
|
849
|
+
severity: "critical",
|
|
850
|
+
title: "Instruction override directive",
|
|
851
|
+
pattern: /(?:disregard|ignore|forget|override|bypass)\s+(?:all\s+)?(?:previous|prior|above|system|safety|security)\s+(?:instructions?|rules?|guidelines?|constraints?|prompts?)/i,
|
|
852
|
+
description: "Skill file contains a directive to override safety instructions or system prompts.",
|
|
853
|
+
recommendation: "Do NOT install this skill. It explicitly attempts to disable safety guardrails."
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
id: "SK-003",
|
|
857
|
+
category: "instruction-injection",
|
|
858
|
+
severity: "high",
|
|
859
|
+
title: "Hidden instructions in markdown",
|
|
860
|
+
pattern: /<!--[\s\S]*?(?:execute|run|install|download|curl|wget|fetch|eval|bash|sh\s+-c)[\s\S]*?-->/i,
|
|
861
|
+
description: "Skill file contains hidden instructions inside HTML/markdown comments that the user won't see in rendered view.",
|
|
862
|
+
recommendation: "Inspect all HTML comments in this skill file. Hidden instructions are a strong indicator of malicious intent."
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
id: "SK-004",
|
|
866
|
+
category: "instruction-injection",
|
|
867
|
+
severity: "high",
|
|
868
|
+
title: "Conditional behavior trigger",
|
|
869
|
+
pattern: /(?:when|if|once)\s+(?:the\s+)?user\s+(?:is\s+not\s+looking|isn'?t\s+(?:watching|paying\s+attention)|leaves|steps\s+away|is\s+away|is\s+idle)/i,
|
|
870
|
+
description: "Skill file contains instructions that trigger only when the user is not paying attention.",
|
|
871
|
+
recommendation: "Remove this skill immediately. Legitimate skills never check for user inattention."
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
id: "SK-005",
|
|
875
|
+
category: "instruction-injection",
|
|
876
|
+
severity: "high",
|
|
877
|
+
title: "Cross-skill poisoning directive",
|
|
878
|
+
pattern: /(?:when\s+(?:using|calling|invoking)\s+(?:any|other|the)\s+(?:skill|tool|command))|(?:modify|alter|change|update)\s+(?:other|the)\s+(?:skill|tool|command)\s+(?:files?|definitions?|descriptions?)/i,
|
|
879
|
+
description: "Skill file attempts to influence or modify other skills/tools, enabling cross-skill attack chaining.",
|
|
880
|
+
recommendation: "Review this skill carefully. It should not need to reference or modify other skills."
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
id: "SK-006",
|
|
884
|
+
category: "instruction-injection",
|
|
885
|
+
severity: "medium",
|
|
886
|
+
title: "Invisible unicode directives",
|
|
887
|
+
pattern: /[\u200B\u200C\u200D\uFEFF\u2060-\u2064\u00AD]{3,}/,
|
|
888
|
+
description: "Skill file contains clusters of invisible Unicode characters that may hide instructions from visual review.",
|
|
889
|
+
recommendation: "Strip invisible characters and inspect the resulting content."
|
|
890
|
+
}
|
|
891
|
+
];
|
|
892
|
+
var MALWARE_DELIVERY_PATTERNS = [
|
|
893
|
+
{
|
|
894
|
+
id: "SK-010",
|
|
895
|
+
category: "malware-delivery",
|
|
896
|
+
severity: "critical",
|
|
897
|
+
title: "Remote script execution",
|
|
898
|
+
pattern: /(?:curl|wget|fetch)\s+(?:-[sSkLfO]+\s+)?(?:https?:\/\/[^\s|]+)\s*\|\s*(?:bash|sh|zsh|python|node|perl|ruby)/i,
|
|
899
|
+
description: "Skill instructs the agent to download and pipe a remote script directly into an interpreter. This is a primary malware delivery vector.",
|
|
900
|
+
recommendation: "Do NOT install this skill. Piping remote scripts to interpreters is extremely dangerous."
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
id: "SK-011",
|
|
904
|
+
category: "malware-delivery",
|
|
905
|
+
severity: "critical",
|
|
906
|
+
title: "Reverse shell pattern",
|
|
907
|
+
pattern: /(?:\/dev\/tcp\/|bash\s+-i\s+>&|nc\s+-[elp]+|ncat\s+-[elp]+|mkfifo\s+\/tmp\/|python.*socket.*connect|socat\s+(?:exec|tcp))/i,
|
|
908
|
+
description: "Skill file contains a reverse shell pattern that would give a remote attacker interactive access to the user's machine.",
|
|
909
|
+
recommendation: "CRITICAL: Remove immediately. This is a backdoor attempt."
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
id: "SK-012",
|
|
913
|
+
category: "malware-delivery",
|
|
914
|
+
severity: "high",
|
|
915
|
+
title: "Suspicious install prerequisite",
|
|
916
|
+
pattern: /(?:first|before\s+(?:you\s+)?(?:start|begin|proceed))\s*,?\s*(?:you\s+)?(?:must|need\s+to|should)\s+(?:install|run|execute|download)\s+[`"]?(?:curl|wget|npm\s+i(?:nstall)?|pip\s+install|gem\s+install|brew\s+install)\s+\S+/i,
|
|
917
|
+
description: 'Skill file instructs the agent to install specific packages as a "prerequisite". This is a common social engineering vector for malware delivery.',
|
|
918
|
+
recommendation: "Verify the prerequisite package is legitimate before installing."
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
id: "SK-013",
|
|
922
|
+
category: "malware-delivery",
|
|
923
|
+
severity: "high",
|
|
924
|
+
title: "Encoded payload execution",
|
|
925
|
+
pattern: /(?:echo|printf)\s+['"]?[A-Za-z0-9+/]{40,}={0,2}['"]?\s*\|\s*(?:base64\s+(?:-[dD]|--decode)|openssl\s+(?:enc|base64))\s*\|\s*(?:bash|sh|python|node)/i,
|
|
926
|
+
description: "Skill contains a base64-encoded payload that is decoded and executed. This hides malicious code from inspection.",
|
|
927
|
+
recommendation: "Do NOT run this skill. Decode the base64 content to inspect what it actually executes."
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
id: "SK-014",
|
|
931
|
+
category: "malware-delivery",
|
|
932
|
+
severity: "high",
|
|
933
|
+
title: "Suspicious npm/pip package install",
|
|
934
|
+
pattern: /(?:npm\s+(?:install|i)\s+(?:-g\s+)?|pip\s+install\s+|gem\s+install\s+)(?!(?:typescript|react|express|flask|django|numpy|pandas|pytest|eslint|prettier|jest|mocha)\b)[a-z][\w.-]*(?:\s|$)/i,
|
|
935
|
+
description: "Skill instructs installation of a non-standard package. Verify the package name is not typosquatted.",
|
|
936
|
+
recommendation: "Verify this package exists on the official registry and is the intended package."
|
|
937
|
+
}
|
|
938
|
+
];
|
|
939
|
+
var STEALTH_PATTERNS = [
|
|
940
|
+
{
|
|
941
|
+
id: "SK-020",
|
|
942
|
+
category: "stealth-operations",
|
|
943
|
+
severity: "critical",
|
|
944
|
+
title: "Stealth action directive",
|
|
945
|
+
pattern: /(?:silently|quietly|without\s+(?:the\s+)?user\s+(?:knowing|noticing|seeing)|in\s+the\s+background|don'?t\s+(?:show|display|tell|inform|alert|notify)\s+(?:the\s+)?user)/i,
|
|
946
|
+
description: "Skill instructs the agent to perform actions without informing the user. This is a hallmark of malicious skills.",
|
|
947
|
+
recommendation: "Do NOT install. Legitimate skills never instruct the agent to hide actions from the user."
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
id: "SK-021",
|
|
951
|
+
category: "stealth-operations",
|
|
952
|
+
severity: "high",
|
|
953
|
+
title: "Output suppression",
|
|
954
|
+
pattern: /(?:suppress|hide|redact|omit|censor)\s+(?:the\s+)?(?:output|results?|response|error|warning|log)/i,
|
|
955
|
+
description: "Skill instructs the agent to hide output or errors from the user, preventing them from seeing what the skill does.",
|
|
956
|
+
recommendation: "Remove this skill. Users should always see the full output of actions taken on their behalf."
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
id: "SK-022",
|
|
960
|
+
category: "stealth-operations",
|
|
961
|
+
severity: "high",
|
|
962
|
+
title: "History/log evasion",
|
|
963
|
+
pattern: /(?:clear|delete|remove|wipe|purge)\s+(?:the\s+)?(?:history|logs?|traces?|evidence|audit\s+trail|command\s+history|bash_history)/i,
|
|
964
|
+
description: "Skill instructs the agent to clear logs or command history to cover its tracks.",
|
|
965
|
+
recommendation: "CRITICAL: Remove this skill. Clearing logs is a strong indicator of malicious intent."
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
id: "SK-023",
|
|
969
|
+
category: "stealth-operations",
|
|
970
|
+
severity: "medium",
|
|
971
|
+
title: "Deceptive user response",
|
|
972
|
+
pattern: /(?:tell|inform|show|respond\s+to)\s+(?:the\s+)?user\s+(?:that|with)\s+(?:everything\s+is|it'?s?\s+(?:fine|ok|normal|safe|working|complete|done))\s+(?:even\s+(?:if|though|when))/i,
|
|
973
|
+
description: 'Skill instructs the agent to give a misleading "all clear" response regardless of what actually happened.',
|
|
974
|
+
recommendation: "Remove this skill. It instructs the agent to deceive the user about outcomes."
|
|
975
|
+
}
|
|
976
|
+
];
|
|
977
|
+
var SAFETY_BYPASS_PATTERNS = [
|
|
978
|
+
{
|
|
979
|
+
id: "SK-030",
|
|
980
|
+
category: "safety-bypass",
|
|
981
|
+
severity: "critical",
|
|
982
|
+
title: "Confirmation bypass",
|
|
983
|
+
pattern: /(?:skip|bypass|disable|turn\s+off|don'?t\s+(?:ask\s+for|require|need|prompt\s+for))\s+(?:user\s+)?(?:confirmation|approval|consent|verification|permission|authorization)/i,
|
|
984
|
+
description: "Skill instructs the agent to bypass user confirmation for actions. This removes the human-in-the-loop safety check.",
|
|
985
|
+
recommendation: "Do NOT install. Disabling confirmation prompts allows the skill to take destructive actions without approval."
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
id: "SK-031",
|
|
989
|
+
category: "safety-bypass",
|
|
990
|
+
severity: "critical",
|
|
991
|
+
title: "Safety feature disable",
|
|
992
|
+
pattern: /(?:disable|turn\s+off|deactivate|circumvent|work\s+around)\s+(?:the\s+)?(?:safety|security|sandbox|firewall|antivirus|protection|guardrail|content\s+filter)/i,
|
|
993
|
+
description: "Skill explicitly instructs disabling safety or security features.",
|
|
994
|
+
recommendation: "Do NOT install. This is an explicit attempt to weaken system security."
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
id: "SK-032",
|
|
998
|
+
category: "safety-bypass",
|
|
999
|
+
severity: "high",
|
|
1000
|
+
title: "Force flag usage",
|
|
1001
|
+
pattern: /(?:always\s+)?(?:use|add|pass|include)\s+(?:the\s+)?(?:--force|--no-verify|-f\s|--yes|-y\s|--assume-yes|--no-confirm|--skip-validation|--allow-empty|--no-check)/i,
|
|
1002
|
+
description: "Skill instructs the agent to always use force/bypass flags that skip safety validations.",
|
|
1003
|
+
recommendation: "Review why this skill needs to bypass safety checks. Remove the force flags if not essential."
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
id: "SK-033",
|
|
1007
|
+
category: "safety-bypass",
|
|
1008
|
+
severity: "high",
|
|
1009
|
+
title: "Root/sudo escalation",
|
|
1010
|
+
pattern: /(?:run|execute|use)\s+(?:as\s+)?(?:root|sudo|admin(?:istrator)?)|(?:sudo\s+(?!apt\s+update|apt\s+install))/i,
|
|
1011
|
+
description: "Skill instructs the agent to escalate to root/admin privileges.",
|
|
1012
|
+
recommendation: "Review why this skill needs elevated privileges. Most skills should not require root access."
|
|
1013
|
+
}
|
|
1014
|
+
];
|
|
1015
|
+
var PERSISTENCE_PATTERNS = [
|
|
1016
|
+
{
|
|
1017
|
+
id: "SK-040",
|
|
1018
|
+
category: "persistence-abuse",
|
|
1019
|
+
severity: "critical",
|
|
1020
|
+
title: "Startup persistence mechanism",
|
|
1021
|
+
pattern: /(?:add|write|append|insert)\s+(?:to|into)\s+(?:the\s+)?(?:\.bashrc|\.zshrc|\.bash_profile|\.profile|\.zprofile|crontab|\.config\/autostart|LaunchAgents|startup)/i,
|
|
1022
|
+
description: "Skill instructs the agent to modify startup/shell config files to establish persistence across reboots.",
|
|
1023
|
+
recommendation: "CRITICAL: Remove this skill. Modifying startup files is a persistence technique used by malware."
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
id: "SK-041",
|
|
1027
|
+
category: "persistence-abuse",
|
|
1028
|
+
severity: "critical",
|
|
1029
|
+
title: "Memory file tampering",
|
|
1030
|
+
pattern: /(?:modify|edit|write\s+to|append\s+to|update|overwrite)\s+(?:the\s+)?(?:CLAUDE\.md|SOUL\.md|MEMORY\.md|\.claude\/|\.cursorrules|\.cursor\/rules)/i,
|
|
1031
|
+
description: "Skill instructs the agent to modify other skill/memory files. This can be used to inject persistent instructions that survive across sessions.",
|
|
1032
|
+
recommendation: "Do NOT install. A skill should never modify other skill or memory files."
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
id: "SK-042",
|
|
1036
|
+
category: "persistence-abuse",
|
|
1037
|
+
severity: "high",
|
|
1038
|
+
title: "Cron job creation",
|
|
1039
|
+
pattern: /(?:crontab\s+-[el]|\/etc\/cron|systemctl\s+enable|launchctl\s+load|schtasks\s+\/create)/i,
|
|
1040
|
+
description: "Skill instructs creation of scheduled tasks or cron jobs for persistent execution.",
|
|
1041
|
+
recommendation: "Review why this skill needs scheduled tasks. This is unusual for agent skills."
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
id: "SK-043",
|
|
1045
|
+
category: "persistence-abuse",
|
|
1046
|
+
severity: "high",
|
|
1047
|
+
title: "Git hook injection",
|
|
1048
|
+
pattern: /(?:\.git\/hooks\/|pre-commit|post-commit|pre-push|post-receive|pre-receive)\s*(?:hook|script|file)/i,
|
|
1049
|
+
description: "Skill instructs modification of git hooks, which execute automatically on git operations.",
|
|
1050
|
+
recommendation: "Verify this skill legitimately needs git hook access. Malicious hooks can exfiltrate code on every commit."
|
|
1051
|
+
}
|
|
1052
|
+
];
|
|
1053
|
+
var SKILL_EXFILTRATION_PATTERNS = [
|
|
1054
|
+
{
|
|
1055
|
+
id: "SK-050",
|
|
1056
|
+
category: "data-exfiltration",
|
|
1057
|
+
severity: "critical",
|
|
1058
|
+
title: "Credential harvesting directive",
|
|
1059
|
+
pattern: /(?:read|extract|get|find|locate|collect|gather|retrieve)\s+(?:all\s+)?(?:the\s+)?(?:api\s+keys?|tokens?|credentials?|passwords?|secrets?|private\s+keys?)\s+(?:from|in|stored\s+in|located\s+at)/i,
|
|
1060
|
+
description: "Skill instructs the agent to collect credentials or secrets from the user's system.",
|
|
1061
|
+
recommendation: "Do NOT install. This is a credential harvesting attack."
|
|
1062
|
+
},
|
|
1063
|
+
{
|
|
1064
|
+
id: "SK-051",
|
|
1065
|
+
category: "data-exfiltration",
|
|
1066
|
+
severity: "critical",
|
|
1067
|
+
title: "Data exfiltration via URL",
|
|
1068
|
+
pattern: /(?:send|post|upload|transmit|forward|exfiltrate|ship)\s+(?:the\s+)?(?:data|contents?|results?|files?|output|information|credentials?|keys?)\s+(?:to|via|using|through)\s+(?:https?:\/\/|webhook|api|endpoint|server)/i,
|
|
1069
|
+
description: "Skill instructs the agent to send data to an external URL or endpoint.",
|
|
1070
|
+
recommendation: "CRITICAL: Review what data is being sent and to where. This matches known exfiltration patterns."
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
id: "SK-052",
|
|
1074
|
+
category: "data-exfiltration",
|
|
1075
|
+
severity: "high",
|
|
1076
|
+
title: "File system enumeration",
|
|
1077
|
+
pattern: /(?:list|enumerate|scan|find|search|catalogue|index)\s+(?:all\s+)?(?:the\s+)?(?:files?|directories|folders?)\s+(?:in|under|at|from)\s+(?:\/|~\/|home|root|the\s+(?:home|root|user))/i,
|
|
1078
|
+
description: "Skill instructs broad filesystem enumeration, potentially to map out sensitive files for exfiltration.",
|
|
1079
|
+
recommendation: "Verify this skill needs filesystem access. Broad enumeration is suspicious."
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
id: "SK-053",
|
|
1083
|
+
category: "data-exfiltration",
|
|
1084
|
+
severity: "high",
|
|
1085
|
+
title: "Environment variable dumping",
|
|
1086
|
+
pattern: /(?:print|dump|list|show|display|export|echo)\s+(?:all\s+)?(?:the\s+)?(?:environment\s+variables?|env\s+vars?|process\.env|os\.environ)/i,
|
|
1087
|
+
description: "Skill instructs dumping all environment variables, which often contain secrets and API keys.",
|
|
1088
|
+
recommendation: "Review why this skill needs access to all environment variables."
|
|
1089
|
+
}
|
|
1090
|
+
];
|
|
1091
|
+
var ALL_SKILL_PATTERNS = [
|
|
1092
|
+
...INSTRUCTION_INJECTION_PATTERNS,
|
|
1093
|
+
...MALWARE_DELIVERY_PATTERNS,
|
|
1094
|
+
...STEALTH_PATTERNS,
|
|
1095
|
+
...SAFETY_BYPASS_PATTERNS,
|
|
1096
|
+
...PERSISTENCE_PATTERNS,
|
|
1097
|
+
...SKILL_EXFILTRATION_PATTERNS
|
|
1098
|
+
];
|
|
1099
|
+
|
|
1100
|
+
// src/scanner/skill-scanner.ts
|
|
1101
|
+
var SEVERITY_DEDUCTIONS2 = {
|
|
1102
|
+
critical: 35,
|
|
1103
|
+
high: 20,
|
|
1104
|
+
medium: 10,
|
|
1105
|
+
low: 5,
|
|
1106
|
+
info: 0
|
|
1107
|
+
};
|
|
1108
|
+
async function scanSkill(skill) {
|
|
1109
|
+
const findings = [];
|
|
1110
|
+
findings.push(...scanContent(skill.content, "skill content", ALL_SKILL_PATTERNS));
|
|
1111
|
+
findings.push(...scanContent(skill.content, "skill content", ALL_PATTERNS));
|
|
1112
|
+
findings.push(...analyzeStructure(skill));
|
|
1113
|
+
if (skill.fileType === "mdc-rule") {
|
|
1114
|
+
findings.push(...analyzeMDCRule(skill));
|
|
1115
|
+
}
|
|
1116
|
+
const uniqueFindings = deduplicateFindings2(findings);
|
|
1117
|
+
const { score, breakdown } = calculateSkillTrustScore(uniqueFindings, skill);
|
|
1118
|
+
const trustLevel = score >= 80 ? "trusted" : score >= 60 ? "caution" : score >= 40 ? "risky" : "dangerous";
|
|
1119
|
+
return {
|
|
1120
|
+
skill,
|
|
1121
|
+
trustScore: score,
|
|
1122
|
+
scoreBreakdown: breakdown,
|
|
1123
|
+
findings: uniqueFindings,
|
|
1124
|
+
trustLevel,
|
|
1125
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
function scanContent(text, context, patterns) {
|
|
1129
|
+
const findings = [];
|
|
1130
|
+
for (const pattern of patterns) {
|
|
1131
|
+
pattern.pattern.lastIndex = 0;
|
|
1132
|
+
const match = pattern.pattern.exec(text);
|
|
1133
|
+
if (match) {
|
|
1134
|
+
findings.push({
|
|
1135
|
+
id: pattern.id,
|
|
1136
|
+
category: pattern.category,
|
|
1137
|
+
severity: pattern.severity,
|
|
1138
|
+
title: pattern.title,
|
|
1139
|
+
description: `${pattern.description} (found in ${context})`,
|
|
1140
|
+
evidence: match[0].substring(0, 200),
|
|
1141
|
+
recommendation: pattern.recommendation
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return findings;
|
|
1146
|
+
}
|
|
1147
|
+
function analyzeStructure(skill) {
|
|
1148
|
+
const findings = [];
|
|
1149
|
+
const content = skill.content;
|
|
1150
|
+
if (skill.size > 5e4) {
|
|
1151
|
+
findings.push({
|
|
1152
|
+
id: "SK-060",
|
|
1153
|
+
category: "obfuscation",
|
|
1154
|
+
severity: "medium",
|
|
1155
|
+
title: "Unusually large skill file",
|
|
1156
|
+
description: `Skill file is ${Math.round(skill.size / 1024)}KB. Large skill files may contain hidden payloads or obfuscated content.`,
|
|
1157
|
+
evidence: `File size: ${skill.size} bytes`,
|
|
1158
|
+
recommendation: "Inspect the full file carefully. Large skill files are unusual."
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
const codeBlockPattern = /```(?:bash|sh|shell|zsh)?\n([\s\S]*?)```/g;
|
|
1162
|
+
let codeMatch;
|
|
1163
|
+
while ((codeMatch = codeBlockPattern.exec(content)) !== null) {
|
|
1164
|
+
const codeBlock = codeMatch[1];
|
|
1165
|
+
if (/rm\s+-rf\s+[\/~]|rm\s+-rf\s+\$\{?HOME/.test(codeBlock)) {
|
|
1166
|
+
findings.push({
|
|
1167
|
+
id: "SK-061",
|
|
1168
|
+
category: "permission-abuse",
|
|
1169
|
+
severity: "critical",
|
|
1170
|
+
title: "Destructive command in code block",
|
|
1171
|
+
description: "Skill contains a recursive delete command targeting the home or root directory.",
|
|
1172
|
+
evidence: codeBlock.substring(0, 200),
|
|
1173
|
+
recommendation: "Do NOT install. This command would delete critical files."
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
if (/chmod\s+(?:777|a\+rwx|\+rwx)/i.test(codeBlock)) {
|
|
1177
|
+
findings.push({
|
|
1178
|
+
id: "SK-062",
|
|
1179
|
+
category: "permission-abuse",
|
|
1180
|
+
severity: "high",
|
|
1181
|
+
title: "World-writable permissions set",
|
|
1182
|
+
description: "Skill sets overly permissive file permissions (777/world-writable).",
|
|
1183
|
+
evidence: codeBlock.substring(0, 200),
|
|
1184
|
+
recommendation: "Review why world-writable permissions are needed. This is almost always a security risk."
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
const urlPattern = /https?:\/\/[^\s)>\]"']+/g;
|
|
1189
|
+
const urls = content.match(urlPattern) || [];
|
|
1190
|
+
const externalUrls = urls.filter(
|
|
1191
|
+
(url) => !url.includes("github.com") && !url.includes("npmjs.com") && !url.includes("docs.") && !url.includes("stackoverflow.com")
|
|
1192
|
+
);
|
|
1193
|
+
if (externalUrls.length > 5) {
|
|
1194
|
+
findings.push({
|
|
1195
|
+
id: "SK-063",
|
|
1196
|
+
category: "data-exfiltration",
|
|
1197
|
+
severity: "low",
|
|
1198
|
+
title: "Many external URLs in skill",
|
|
1199
|
+
description: `Skill contains ${externalUrls.length} external URLs. Excessive external URLs may indicate data exfiltration endpoints.`,
|
|
1200
|
+
evidence: externalUrls.slice(0, 3).join(", "),
|
|
1201
|
+
recommendation: "Review the external URLs to ensure they are all legitimate."
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
const invisibleChars = content.match(/[\u200B-\u200D\uFEFF\u2060-\u2064\u00AD]/g) || [];
|
|
1205
|
+
if (invisibleChars.length > 10) {
|
|
1206
|
+
findings.push({
|
|
1207
|
+
id: "SK-064",
|
|
1208
|
+
category: "obfuscation",
|
|
1209
|
+
severity: "high",
|
|
1210
|
+
title: "High concentration of invisible characters",
|
|
1211
|
+
description: `Skill contains ${invisibleChars.length} invisible Unicode characters that may hide malicious instructions.`,
|
|
1212
|
+
evidence: `${invisibleChars.length} invisible characters detected`,
|
|
1213
|
+
recommendation: "Strip invisible characters and compare the before/after content."
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
return findings;
|
|
1217
|
+
}
|
|
1218
|
+
function analyzeMDCRule(skill) {
|
|
1219
|
+
const findings = [];
|
|
1220
|
+
const content = skill.content;
|
|
1221
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1222
|
+
if (frontmatterMatch) {
|
|
1223
|
+
const frontmatter = frontmatterMatch[1];
|
|
1224
|
+
if (/globs?:\s*['"]\*\*\/\*['"]/i.test(frontmatter) || /globs?:\s*\*\*\/\*/i.test(frontmatter)) {
|
|
1225
|
+
findings.push({
|
|
1226
|
+
id: "SK-070",
|
|
1227
|
+
category: "permission-abuse",
|
|
1228
|
+
severity: "medium",
|
|
1229
|
+
title: "MDC rule attached to all files",
|
|
1230
|
+
description: "This Cursor rule uses a wildcard glob pattern that auto-attaches it to every file. This means its instructions apply universally.",
|
|
1231
|
+
evidence: frontmatter.substring(0, 200),
|
|
1232
|
+
recommendation: "Review why this rule needs to apply to all files. Narrow the glob pattern if possible."
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
if (/alwaysApply:\s*true/i.test(frontmatter)) {
|
|
1236
|
+
findings.push({
|
|
1237
|
+
id: "SK-071",
|
|
1238
|
+
category: "permission-abuse",
|
|
1239
|
+
severity: "low",
|
|
1240
|
+
title: "MDC rule always applied",
|
|
1241
|
+
description: "This Cursor rule has alwaysApply: true, meaning it runs on every interaction regardless of context.",
|
|
1242
|
+
evidence: "alwaysApply: true",
|
|
1243
|
+
recommendation: "Consider if this rule truly needs to run on every interaction."
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return findings;
|
|
1248
|
+
}
|
|
1249
|
+
function calculateSkillTrustScore(findings, skill) {
|
|
1250
|
+
let codeAnalysis = 100;
|
|
1251
|
+
let dependencyHealth = 100;
|
|
1252
|
+
let permissionSafety = 100;
|
|
1253
|
+
let behavioralStability = 100;
|
|
1254
|
+
let transparency = 100;
|
|
1255
|
+
for (const f of findings) {
|
|
1256
|
+
const deduction = SEVERITY_DEDUCTIONS2[f.severity];
|
|
1257
|
+
switch (f.category) {
|
|
1258
|
+
case "tool-poisoning":
|
|
1259
|
+
case "instruction-injection":
|
|
1260
|
+
case "obfuscation":
|
|
1261
|
+
codeAnalysis -= deduction;
|
|
1262
|
+
break;
|
|
1263
|
+
case "malware-delivery":
|
|
1264
|
+
case "dependency-risk":
|
|
1265
|
+
dependencyHealth -= deduction;
|
|
1266
|
+
break;
|
|
1267
|
+
case "permission-abuse":
|
|
1268
|
+
case "data-exfiltration":
|
|
1269
|
+
case "safety-bypass":
|
|
1270
|
+
permissionSafety -= deduction;
|
|
1271
|
+
break;
|
|
1272
|
+
case "stealth-operations":
|
|
1273
|
+
case "persistence-abuse":
|
|
1274
|
+
behavioralStability -= deduction;
|
|
1275
|
+
break;
|
|
1276
|
+
case "rug-pull":
|
|
1277
|
+
transparency -= deduction;
|
|
1278
|
+
break;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
codeAnalysis = Math.max(0, codeAnalysis);
|
|
1282
|
+
dependencyHealth = Math.max(0, dependencyHealth);
|
|
1283
|
+
permissionSafety = Math.max(0, permissionSafety);
|
|
1284
|
+
behavioralStability = Math.max(0, behavioralStability);
|
|
1285
|
+
transparency = Math.max(0, transparency);
|
|
1286
|
+
if (skill.scope === "project") {
|
|
1287
|
+
transparency = Math.min(100, transparency + 10);
|
|
1288
|
+
}
|
|
1289
|
+
if (skill.fileType === "skill.md" || skill.fileType === "mdc-rule") {
|
|
1290
|
+
transparency = Math.min(100, transparency + 5);
|
|
1291
|
+
}
|
|
1292
|
+
const breakdown = {
|
|
1293
|
+
codeAnalysis,
|
|
1294
|
+
dependencyHealth,
|
|
1295
|
+
permissionSafety,
|
|
1296
|
+
behavioralStability,
|
|
1297
|
+
transparency
|
|
1298
|
+
};
|
|
1299
|
+
const score = Math.round(
|
|
1300
|
+
codeAnalysis * 0.3 + dependencyHealth * 0.2 + permissionSafety * 0.2 + behavioralStability * 0.15 + transparency * 0.15
|
|
1301
|
+
);
|
|
1302
|
+
return { score: Math.max(0, Math.min(100, score)), breakdown };
|
|
1303
|
+
}
|
|
1304
|
+
function deduplicateFindings2(findings) {
|
|
1305
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1306
|
+
return findings.filter((f) => {
|
|
1307
|
+
if (seen.has(f.id)) return false;
|
|
1308
|
+
seen.add(f.id);
|
|
1309
|
+
return true;
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/output/terminal.ts
|
|
1314
|
+
var import_chalk = __toESM(require("chalk"));
|
|
1315
|
+
var TRUST_COLORS = {
|
|
1316
|
+
trusted: import_chalk.default.green,
|
|
1317
|
+
caution: import_chalk.default.yellow,
|
|
1318
|
+
risky: import_chalk.default.hex("#FF8C00"),
|
|
1319
|
+
// orange
|
|
1320
|
+
dangerous: import_chalk.default.red
|
|
1321
|
+
};
|
|
1322
|
+
var SEVERITY_COLORS = {
|
|
1323
|
+
critical: import_chalk.default.bgRed.white.bold,
|
|
1324
|
+
high: import_chalk.default.red.bold,
|
|
1325
|
+
medium: import_chalk.default.yellow,
|
|
1326
|
+
low: import_chalk.default.blue,
|
|
1327
|
+
info: import_chalk.default.gray
|
|
1328
|
+
};
|
|
1329
|
+
var TRUST_ICONS = {
|
|
1330
|
+
trusted: "\u2713",
|
|
1331
|
+
caution: "\u26A0",
|
|
1332
|
+
risky: "\u26A0",
|
|
1333
|
+
dangerous: "\u2717"
|
|
1334
|
+
};
|
|
1335
|
+
var SEVERITY_ICONS = {
|
|
1336
|
+
critical: "!!!",
|
|
1337
|
+
high: "!!",
|
|
1338
|
+
medium: "!",
|
|
1339
|
+
low: "i",
|
|
1340
|
+
info: "\xB7"
|
|
1341
|
+
};
|
|
1342
|
+
var FILE_TYPE_LABELS = {
|
|
1343
|
+
"skill.md": "SKILL.md",
|
|
1344
|
+
"mdc-rule": ".mdc rule",
|
|
1345
|
+
"claude.md": "CLAUDE.md",
|
|
1346
|
+
"soul.md": "SOUL.md",
|
|
1347
|
+
"memory.md": "MEMORY.md"
|
|
1348
|
+
};
|
|
1349
|
+
function printBanner() {
|
|
1350
|
+
console.log("");
|
|
1351
|
+
console.log(import_chalk.default.bold.hex("#2C4A7C")(" \u2566 \u2566\u2566\u2554\u2550\u2557\u2566\u2566 "));
|
|
1352
|
+
console.log(import_chalk.default.bold.hex("#2C4A7C")(" \u255A\u2557\u2554\u255D\u2551\u2551 \u2566\u2551\u2551 "));
|
|
1353
|
+
console.log(import_chalk.default.bold.hex("#2C4A7C")(" \u255A\u255D \u2569\u255A\u2550\u255D\u2569\u2569\u2550\u255D"));
|
|
1354
|
+
console.log(import_chalk.default.gray(" AI Agent Security Scanner"));
|
|
1355
|
+
console.log("");
|
|
1356
|
+
}
|
|
1357
|
+
function printServerResult(result, verbose) {
|
|
1358
|
+
const color = TRUST_COLORS[result.trustLevel];
|
|
1359
|
+
const icon = TRUST_ICONS[result.trustLevel];
|
|
1360
|
+
console.log(
|
|
1361
|
+
import_chalk.default.bold(` ${icon} `) + import_chalk.default.bold(result.server.name) + import_chalk.default.gray(` (${result.server.source})`) + " " + color(`[${result.trustScore}/100 ${result.trustLevel.toUpperCase()}]`)
|
|
1362
|
+
);
|
|
1363
|
+
console.log(import_chalk.default.gray(` Config: ${result.server.configPath}`));
|
|
1364
|
+
console.log(
|
|
1365
|
+
import_chalk.default.gray(` Command: ${result.server.command} ${result.server.args.join(" ")}`)
|
|
1366
|
+
);
|
|
1367
|
+
if (result.findings.length === 0) {
|
|
1368
|
+
console.log(import_chalk.default.green(" No security issues found."));
|
|
1369
|
+
} else {
|
|
1370
|
+
console.log(
|
|
1371
|
+
import_chalk.default.dim(` ${result.findings.length} finding(s):`)
|
|
1372
|
+
);
|
|
1373
|
+
for (const finding of result.findings) {
|
|
1374
|
+
printFinding(finding, verbose);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (verbose) {
|
|
1378
|
+
printScoreBreakdown(result.scoreBreakdown);
|
|
1379
|
+
}
|
|
1380
|
+
console.log("");
|
|
1381
|
+
}
|
|
1382
|
+
function printSkillResult(result, verbose) {
|
|
1383
|
+
const color = TRUST_COLORS[result.trustLevel];
|
|
1384
|
+
const icon = TRUST_ICONS[result.trustLevel];
|
|
1385
|
+
const typeLabel = FILE_TYPE_LABELS[result.skill.fileType] || result.skill.fileType;
|
|
1386
|
+
console.log(
|
|
1387
|
+
import_chalk.default.bold(` ${icon} `) + import_chalk.default.bold(result.skill.name) + import_chalk.default.gray(` (${typeLabel} \xB7 ${result.skill.source} \xB7 ${result.skill.scope})`) + " " + color(`[${result.trustScore}/100 ${result.trustLevel.toUpperCase()}]`)
|
|
1388
|
+
);
|
|
1389
|
+
console.log(import_chalk.default.gray(` Path: ${result.skill.filePath}`));
|
|
1390
|
+
const sizeKB = (result.skill.size / 1024).toFixed(1);
|
|
1391
|
+
console.log(import_chalk.default.gray(` Size: ${sizeKB} KB`));
|
|
1392
|
+
if (result.findings.length === 0) {
|
|
1393
|
+
console.log(import_chalk.default.green(" No security issues found."));
|
|
1394
|
+
} else {
|
|
1395
|
+
console.log(
|
|
1396
|
+
import_chalk.default.dim(` ${result.findings.length} finding(s):`)
|
|
1397
|
+
);
|
|
1398
|
+
for (const finding of result.findings) {
|
|
1399
|
+
printFinding(finding, verbose);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
if (verbose) {
|
|
1403
|
+
printScoreBreakdown(result.scoreBreakdown);
|
|
1404
|
+
}
|
|
1405
|
+
console.log("");
|
|
1406
|
+
}
|
|
1407
|
+
function printFinding(finding, verbose) {
|
|
1408
|
+
const color = SEVERITY_COLORS[finding.severity];
|
|
1409
|
+
const icon = SEVERITY_ICONS[finding.severity];
|
|
1410
|
+
console.log(
|
|
1411
|
+
` ${color(`[${icon}]`)} ${color(finding.severity.toUpperCase())} ` + import_chalk.default.white(finding.title) + import_chalk.default.gray(` (${finding.id})`)
|
|
1412
|
+
);
|
|
1413
|
+
if (verbose) {
|
|
1414
|
+
console.log(import_chalk.default.gray(` ${finding.description}`));
|
|
1415
|
+
if (finding.evidence) {
|
|
1416
|
+
console.log(import_chalk.default.gray(` Evidence: ${finding.evidence}`));
|
|
1417
|
+
}
|
|
1418
|
+
console.log(import_chalk.default.cyan(` \u2192 ${finding.recommendation}`));
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function printScoreBreakdown(b) {
|
|
1422
|
+
console.log(import_chalk.default.dim(" Score Breakdown:"));
|
|
1423
|
+
console.log(import_chalk.default.dim(` Code Analysis: ${scoreBar(b.codeAnalysis)} ${b.codeAnalysis}/100 (30%)`));
|
|
1424
|
+
console.log(import_chalk.default.dim(` Dependency Health: ${scoreBar(b.dependencyHealth)} ${b.dependencyHealth}/100 (20%)`));
|
|
1425
|
+
console.log(import_chalk.default.dim(` Permission Safety: ${scoreBar(b.permissionSafety)} ${b.permissionSafety}/100 (20%)`));
|
|
1426
|
+
console.log(import_chalk.default.dim(` Behavioral Stability: ${scoreBar(b.behavioralStability)} ${b.behavioralStability}/100 (15%)`));
|
|
1427
|
+
console.log(import_chalk.default.dim(` Transparency: ${scoreBar(b.transparency)} ${b.transparency}/100 (15%)`));
|
|
1428
|
+
}
|
|
1429
|
+
function printSummary(summary) {
|
|
1430
|
+
console.log(import_chalk.default.bold(" \u2500\u2500\u2500 Scan Summary \u2500\u2500\u2500"));
|
|
1431
|
+
console.log("");
|
|
1432
|
+
if (summary.totalServers > 0) {
|
|
1433
|
+
console.log(` MCP servers scanned: ${import_chalk.default.bold(String(summary.totalServers))}`);
|
|
1434
|
+
}
|
|
1435
|
+
if (summary.totalSkills > 0) {
|
|
1436
|
+
console.log(` Skill files scanned: ${import_chalk.default.bold(String(summary.totalSkills))}`);
|
|
1437
|
+
}
|
|
1438
|
+
const totalScanned = summary.totalServers + summary.totalSkills;
|
|
1439
|
+
if (totalScanned > 0 && summary.totalServers > 0 && summary.totalSkills > 0) {
|
|
1440
|
+
console.log(` Total scanned: ${import_chalk.default.bold(String(totalScanned))}`);
|
|
1441
|
+
}
|
|
1442
|
+
console.log(
|
|
1443
|
+
` ${import_chalk.default.green(`${TRUST_ICONS.trusted} Trusted: ${summary.byTrustLevel.trusted}`)} ${import_chalk.default.yellow(`${TRUST_ICONS.caution} Caution: ${summary.byTrustLevel.caution}`)} ${import_chalk.default.hex("#FF8C00")(`${TRUST_ICONS.risky} Risky: ${summary.byTrustLevel.risky}`)} ${import_chalk.default.red(`${TRUST_ICONS.dangerous} Dangerous: ${summary.byTrustLevel.dangerous}`)}`
|
|
1444
|
+
);
|
|
1445
|
+
const totalFindings = Object.values(summary.bySeverity).reduce((a, b) => a + b, 0);
|
|
1446
|
+
if (totalFindings > 0) {
|
|
1447
|
+
console.log("");
|
|
1448
|
+
console.log(` Total findings: ${import_chalk.default.bold(String(totalFindings))}`);
|
|
1449
|
+
if (summary.bySeverity.critical > 0)
|
|
1450
|
+
console.log(import_chalk.default.bgRed.white.bold(` ${summary.bySeverity.critical} CRITICAL`));
|
|
1451
|
+
if (summary.bySeverity.high > 0)
|
|
1452
|
+
console.log(import_chalk.default.red.bold(` ${summary.bySeverity.high} HIGH`));
|
|
1453
|
+
if (summary.bySeverity.medium > 0)
|
|
1454
|
+
console.log(import_chalk.default.yellow(` ${summary.bySeverity.medium} MEDIUM`));
|
|
1455
|
+
if (summary.bySeverity.low > 0)
|
|
1456
|
+
console.log(import_chalk.default.blue(` ${summary.bySeverity.low} LOW`));
|
|
1457
|
+
if (summary.bySeverity.info > 0)
|
|
1458
|
+
console.log(import_chalk.default.gray(` ${summary.bySeverity.info} INFO`));
|
|
1459
|
+
}
|
|
1460
|
+
console.log("");
|
|
1461
|
+
console.log(import_chalk.default.gray(` Scanned at ${summary.timestamp}`));
|
|
1462
|
+
console.log(import_chalk.default.gray(` Vigile v${summary.version} \u2014 https://vigile.dev`));
|
|
1463
|
+
console.log("");
|
|
1464
|
+
}
|
|
1465
|
+
function printNoServersFound() {
|
|
1466
|
+
console.log(import_chalk.default.yellow(" No MCP server configurations found on this machine."));
|
|
1467
|
+
console.log("");
|
|
1468
|
+
console.log(import_chalk.default.gray(" Vigile checks the following locations:"));
|
|
1469
|
+
console.log(import_chalk.default.gray(" \u2022 Claude Desktop config"));
|
|
1470
|
+
console.log(import_chalk.default.gray(" \u2022 Cursor MCP config"));
|
|
1471
|
+
console.log(import_chalk.default.gray(" \u2022 Claude Code config (.claude.json / .mcp.json)"));
|
|
1472
|
+
console.log(import_chalk.default.gray(" \u2022 Windsurf MCP config"));
|
|
1473
|
+
console.log(import_chalk.default.gray(" \u2022 VS Code MCP config (.vscode/mcp.json)"));
|
|
1474
|
+
console.log("");
|
|
1475
|
+
console.log(import_chalk.default.gray(" If you have MCP servers configured elsewhere, use:"));
|
|
1476
|
+
console.log(import_chalk.default.cyan(" vigile-scan --config /path/to/config.json"));
|
|
1477
|
+
console.log("");
|
|
1478
|
+
console.log(import_chalk.default.gray(" To also scan agent skill files, use:"));
|
|
1479
|
+
console.log(import_chalk.default.cyan(" vigile-scan --all"));
|
|
1480
|
+
console.log("");
|
|
1481
|
+
}
|
|
1482
|
+
function printNoSkillsFound() {
|
|
1483
|
+
console.log(import_chalk.default.yellow(" No agent skill files found on this machine."));
|
|
1484
|
+
console.log("");
|
|
1485
|
+
console.log(import_chalk.default.gray(" Vigile scans the following skill locations:"));
|
|
1486
|
+
console.log(import_chalk.default.gray(" \u2022 Claude Code skills (.claude/skills/*/SKILL.md)"));
|
|
1487
|
+
console.log(import_chalk.default.gray(" \u2022 Claude Code commands (.claude/commands/**/*.md)"));
|
|
1488
|
+
console.log(import_chalk.default.gray(" \u2022 GitHub Copilot skills (.github/skills/*/SKILL.md)"));
|
|
1489
|
+
console.log(import_chalk.default.gray(" \u2022 Cursor rules (.cursor/rules/*.mdc, .cursorrules)"));
|
|
1490
|
+
console.log(import_chalk.default.gray(" \u2022 Memory files (CLAUDE.md, SOUL.md, MEMORY.md)"));
|
|
1491
|
+
console.log("");
|
|
1492
|
+
}
|
|
1493
|
+
function printNothingFound() {
|
|
1494
|
+
console.log(import_chalk.default.yellow(" No MCP servers or agent skill files found."));
|
|
1495
|
+
console.log("");
|
|
1496
|
+
console.log(import_chalk.default.gray(" Try scanning a specific config:"));
|
|
1497
|
+
console.log(import_chalk.default.cyan(" vigile-scan --config /path/to/config.json"));
|
|
1498
|
+
console.log("");
|
|
1499
|
+
console.log(import_chalk.default.gray(" Or cd into a project directory that contains skill files:"));
|
|
1500
|
+
console.log(import_chalk.default.cyan(" cd /path/to/project && vigile-scan --all"));
|
|
1501
|
+
console.log("");
|
|
1502
|
+
}
|
|
1503
|
+
function scoreBar(score) {
|
|
1504
|
+
const filled = Math.round(score / 10);
|
|
1505
|
+
const empty = 10 - filled;
|
|
1506
|
+
const color = score >= 80 ? import_chalk.default.green : score >= 60 ? import_chalk.default.yellow : score >= 40 ? import_chalk.default.hex("#FF8C00") : import_chalk.default.red;
|
|
1507
|
+
return color("\u2588".repeat(filled)) + import_chalk.default.gray("\u2591".repeat(empty));
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// src/output/json.ts
|
|
1511
|
+
function formatJSON(summary) {
|
|
1512
|
+
return JSON.stringify(summary, null, 2);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// src/index.ts
|
|
1516
|
+
var VERSION = "0.1.0";
|
|
1517
|
+
var program = new import_commander.Command();
|
|
1518
|
+
program.name("vigile-scan").description(
|
|
1519
|
+
"Security scanner for AI agent tools \u2014 detect tool poisoning, permission abuse, and supply chain attacks in MCP servers and agent skills"
|
|
1520
|
+
).version(VERSION);
|
|
1521
|
+
function addScanOptions(cmd) {
|
|
1522
|
+
return cmd.option("-j, --json", "Output results as JSON").option("-v, --verbose", "Show detailed findings and score breakdown").option("-c, --config <path>", "Path to a custom MCP config file").option("-o, --output <path>", "Write results to a file").option(
|
|
1523
|
+
"--client <client>",
|
|
1524
|
+
"Only scan a specific client (claude-desktop, cursor, claude-code, windsurf, vscode)"
|
|
1525
|
+
).option("-s, --skills", "Scan agent skills only (SKILL.md, .mdc rules, CLAUDE.md, etc.)").option("-a, --all", "Scan both MCP servers and agent skills");
|
|
1526
|
+
}
|
|
1527
|
+
addScanOptions(
|
|
1528
|
+
program.command("scan").description("Scan MCP server configurations and agent skill files on this machine")
|
|
1529
|
+
).action(async (options) => {
|
|
1530
|
+
await runScan(options);
|
|
1531
|
+
});
|
|
1532
|
+
addScanOptions(program).action(async (options) => {
|
|
1533
|
+
if (!process.argv.slice(2).includes("scan")) {
|
|
1534
|
+
await runScan(options);
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
async function runScan(options) {
|
|
1538
|
+
const isJSON = options.json;
|
|
1539
|
+
const scanMCP = !options.skills;
|
|
1540
|
+
const scanSkills = options.skills || options.all;
|
|
1541
|
+
if (!isJSON) {
|
|
1542
|
+
printBanner();
|
|
1543
|
+
}
|
|
1544
|
+
const results = [];
|
|
1545
|
+
const skillResults = [];
|
|
1546
|
+
if (scanMCP) {
|
|
1547
|
+
const spinner = isJSON ? null : (0, import_ora.default)("Discovering MCP configurations...").start();
|
|
1548
|
+
const discovery = await discoverAllServers(options.client);
|
|
1549
|
+
if (discovery.servers.length === 0) {
|
|
1550
|
+
spinner?.succeed("No MCP server configurations found");
|
|
1551
|
+
} else {
|
|
1552
|
+
spinner?.succeed(
|
|
1553
|
+
`Found ${discovery.servers.length} MCP server(s) across ${discovery.configsFound} config file(s)`
|
|
1554
|
+
);
|
|
1555
|
+
const scanSpinner = isJSON ? null : (0, import_ora.default)("Scanning MCP servers...").start();
|
|
1556
|
+
for (const server of discovery.servers) {
|
|
1557
|
+
const result = await scanServer(server);
|
|
1558
|
+
results.push(result);
|
|
1559
|
+
}
|
|
1560
|
+
scanSpinner?.succeed(`Scanned ${results.length} MCP server(s)`);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
if (scanSkills) {
|
|
1564
|
+
const spinner = isJSON ? null : (0, import_ora.default)("Discovering agent skill files...").start();
|
|
1565
|
+
const skillDiscovery = await discoverAllSkills();
|
|
1566
|
+
if (skillDiscovery.skills.length === 0) {
|
|
1567
|
+
spinner?.succeed("No agent skill files found");
|
|
1568
|
+
} else {
|
|
1569
|
+
spinner?.succeed(
|
|
1570
|
+
`Found ${skillDiscovery.skills.length} skill file(s) across ${skillDiscovery.locationsFound} location(s)`
|
|
1571
|
+
);
|
|
1572
|
+
const scanSpinner = isJSON ? null : (0, import_ora.default)("Scanning agent skills...").start();
|
|
1573
|
+
for (const skill of skillDiscovery.skills) {
|
|
1574
|
+
const result = await scanSkill(skill);
|
|
1575
|
+
skillResults.push(result);
|
|
1576
|
+
}
|
|
1577
|
+
scanSpinner?.succeed(`Scanned ${skillResults.length} skill file(s)`);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
if (results.length === 0 && skillResults.length === 0) {
|
|
1581
|
+
if (!isJSON) {
|
|
1582
|
+
if (scanMCP && !scanSkills) {
|
|
1583
|
+
printNoServersFound();
|
|
1584
|
+
} else if (scanSkills && !scanMCP) {
|
|
1585
|
+
printNoSkillsFound();
|
|
1586
|
+
} else {
|
|
1587
|
+
printNothingFound();
|
|
1588
|
+
}
|
|
1589
|
+
} else {
|
|
1590
|
+
console.log(JSON.stringify({ servers: [], skills: [], message: "Nothing found to scan" }));
|
|
1591
|
+
}
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
const allResults = [...results];
|
|
1595
|
+
const allSkillResults = [...skillResults];
|
|
1596
|
+
const allTrustLevels = [
|
|
1597
|
+
...results.map((r) => r.trustLevel),
|
|
1598
|
+
...skillResults.map((r) => r.trustLevel)
|
|
1599
|
+
];
|
|
1600
|
+
const allFindings = [
|
|
1601
|
+
...results.flatMap((r) => r.findings),
|
|
1602
|
+
...skillResults.flatMap((r) => r.findings)
|
|
1603
|
+
];
|
|
1604
|
+
const summary = {
|
|
1605
|
+
totalServers: allResults.length,
|
|
1606
|
+
totalSkills: allSkillResults.length,
|
|
1607
|
+
byTrustLevel: {
|
|
1608
|
+
trusted: allTrustLevels.filter((l) => l === "trusted").length,
|
|
1609
|
+
caution: allTrustLevels.filter((l) => l === "caution").length,
|
|
1610
|
+
risky: allTrustLevels.filter((l) => l === "risky").length,
|
|
1611
|
+
dangerous: allTrustLevels.filter((l) => l === "dangerous").length
|
|
1612
|
+
},
|
|
1613
|
+
bySeverity: {
|
|
1614
|
+
critical: allFindings.filter((f) => f.severity === "critical").length,
|
|
1615
|
+
high: allFindings.filter((f) => f.severity === "high").length,
|
|
1616
|
+
medium: allFindings.filter((f) => f.severity === "medium").length,
|
|
1617
|
+
low: allFindings.filter((f) => f.severity === "low").length,
|
|
1618
|
+
info: allFindings.filter((f) => f.severity === "info").length
|
|
1619
|
+
},
|
|
1620
|
+
results: allResults,
|
|
1621
|
+
skillResults: allSkillResults,
|
|
1622
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1623
|
+
version: VERSION
|
|
1624
|
+
};
|
|
1625
|
+
if (isJSON) {
|
|
1626
|
+
const jsonOutput = formatJSON(summary);
|
|
1627
|
+
if (options.output) {
|
|
1628
|
+
await (0, import_promises3.writeFile)(options.output, jsonOutput);
|
|
1629
|
+
} else {
|
|
1630
|
+
console.log(jsonOutput);
|
|
1631
|
+
}
|
|
1632
|
+
} else {
|
|
1633
|
+
console.log("");
|
|
1634
|
+
if (results.length > 0) {
|
|
1635
|
+
for (const result of results) {
|
|
1636
|
+
printServerResult(result, options.verbose ?? false);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
if (skillResults.length > 0) {
|
|
1640
|
+
for (const result of skillResults) {
|
|
1641
|
+
printSkillResult(result, options.verbose ?? false);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
printSummary(summary);
|
|
1645
|
+
if (options.output) {
|
|
1646
|
+
await (0, import_promises3.writeFile)(options.output, formatJSON(summary));
|
|
1647
|
+
console.log(` Results saved to ${options.output}`);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
if (summary.bySeverity.critical > 0 || summary.bySeverity.high > 0) {
|
|
1651
|
+
process.exit(1);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
program.parse();
|
|
1655
|
+
//# sourceMappingURL=index.js.map
|