vigile-scan 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -17
- package/dist/index.js +413 -95
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -6,6 +6,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
10
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
+
};
|
|
9
12
|
var __copyProps = (to, from, except, desc) => {
|
|
10
13
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
14
|
for (let key of __getOwnPropNames(from))
|
|
@@ -23,11 +26,88 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
26
|
mod
|
|
24
27
|
));
|
|
25
28
|
|
|
29
|
+
// package.json
|
|
30
|
+
var require_package = __commonJS({
|
|
31
|
+
"package.json"(exports2, module2) {
|
|
32
|
+
module2.exports = {
|
|
33
|
+
name: "vigile-scan",
|
|
34
|
+
version: "0.2.5",
|
|
35
|
+
description: "Security scanner for AI agent tools \u2014 detect tool poisoning, permission abuse, and supply chain attacks in MCP servers and agent skills",
|
|
36
|
+
main: "dist/index.js",
|
|
37
|
+
bin: {
|
|
38
|
+
"vigile-scan": "dist/index.js",
|
|
39
|
+
vigile: "dist/index.js"
|
|
40
|
+
},
|
|
41
|
+
files: [
|
|
42
|
+
"dist",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
],
|
|
46
|
+
scripts: {
|
|
47
|
+
build: "tsup && chmod +x dist/index.js",
|
|
48
|
+
dev: "tsup --watch",
|
|
49
|
+
start: "node dist/index.js",
|
|
50
|
+
lint: "eslint src/",
|
|
51
|
+
typecheck: "tsc --noEmit",
|
|
52
|
+
test: "vitest run",
|
|
53
|
+
"test:watch": "vitest",
|
|
54
|
+
"test:coverage": "vitest run --coverage",
|
|
55
|
+
prepublishOnly: "npm run build"
|
|
56
|
+
},
|
|
57
|
+
keywords: [
|
|
58
|
+
"mcp",
|
|
59
|
+
"security",
|
|
60
|
+
"ai-agent",
|
|
61
|
+
"scanner",
|
|
62
|
+
"trust",
|
|
63
|
+
"model-context-protocol",
|
|
64
|
+
"tool-poisoning",
|
|
65
|
+
"ai-security",
|
|
66
|
+
"agent-skills",
|
|
67
|
+
"skill-scanning",
|
|
68
|
+
"prompt-injection",
|
|
69
|
+
"data-exfiltration",
|
|
70
|
+
"claude",
|
|
71
|
+
"cursor",
|
|
72
|
+
"copilot"
|
|
73
|
+
],
|
|
74
|
+
author: "Vigile AI Security",
|
|
75
|
+
license: "Apache-2.0",
|
|
76
|
+
homepage: "https://vigile.dev",
|
|
77
|
+
repository: {
|
|
78
|
+
type: "git",
|
|
79
|
+
url: "git+https://github.com/Vigile-ai/vigile-cli.git"
|
|
80
|
+
},
|
|
81
|
+
bugs: {
|
|
82
|
+
url: "https://github.com/Vigile-ai/vigile-cli/issues"
|
|
83
|
+
},
|
|
84
|
+
publishConfig: {
|
|
85
|
+
access: "public"
|
|
86
|
+
},
|
|
87
|
+
engines: {
|
|
88
|
+
node: ">=18.0.0"
|
|
89
|
+
},
|
|
90
|
+
dependencies: {
|
|
91
|
+
chalk: "^5.3.0",
|
|
92
|
+
commander: "^12.1.0",
|
|
93
|
+
glob: "^11.0.0",
|
|
94
|
+
ora: "^8.1.0"
|
|
95
|
+
},
|
|
96
|
+
devDependencies: {
|
|
97
|
+
"@types/node": "^20.11.0",
|
|
98
|
+
tsup: "^8.0.0",
|
|
99
|
+
typescript: "^5.4.0",
|
|
100
|
+
vitest: "^2.0.0"
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
26
106
|
// src/index.ts
|
|
27
107
|
var import_commander = require("commander");
|
|
28
108
|
var import_chalk2 = __toESM(require("chalk"));
|
|
29
109
|
var import_ora = __toESM(require("ora"));
|
|
30
|
-
var
|
|
110
|
+
var import_promises5 = require("fs/promises");
|
|
31
111
|
|
|
32
112
|
// src/discovery/claude-desktop.ts
|
|
33
113
|
var import_path2 = require("path");
|
|
@@ -53,7 +133,7 @@ async function parseMCPConfig(configPath, source) {
|
|
|
53
133
|
try {
|
|
54
134
|
const raw = await (0, import_promises.readFile)(configPath, "utf-8");
|
|
55
135
|
const config = JSON.parse(raw);
|
|
56
|
-
const servers = config.mcpServers || config;
|
|
136
|
+
const servers = config.mcpServers || config.servers || config;
|
|
57
137
|
if (typeof servers !== "object" || servers === null) {
|
|
58
138
|
return [];
|
|
59
139
|
}
|
|
@@ -118,13 +198,63 @@ async function discoverCursor() {
|
|
|
118
198
|
|
|
119
199
|
// src/discovery/claude-code.ts
|
|
120
200
|
var import_path4 = require("path");
|
|
201
|
+
var import_fs2 = require("fs");
|
|
121
202
|
async function discoverClaudeCode() {
|
|
122
203
|
const home = getHome();
|
|
123
|
-
const
|
|
204
|
+
const allServers = [];
|
|
205
|
+
const traditionalPaths = [
|
|
124
206
|
(0, import_path4.join)(home, ".claude.json"),
|
|
125
207
|
(0, import_path4.join)(process.cwd(), ".mcp.json")
|
|
126
208
|
];
|
|
127
|
-
|
|
209
|
+
allServers.push(...await tryConfigPaths(traditionalPaths, "claude-code"));
|
|
210
|
+
const pluginCacheDir = (0, import_path4.join)(home, ".claude", "plugins", "cache", "claude-plugins-official");
|
|
211
|
+
if ((0, import_fs2.existsSync)(pluginCacheDir)) {
|
|
212
|
+
allServers.push(...await discoverPluginMCPConfigs(pluginCacheDir));
|
|
213
|
+
}
|
|
214
|
+
return allServers;
|
|
215
|
+
}
|
|
216
|
+
async function discoverPluginMCPConfigs(cacheDir) {
|
|
217
|
+
const servers = [];
|
|
218
|
+
const seen = /* @__PURE__ */ new Set();
|
|
219
|
+
let pluginDirs;
|
|
220
|
+
try {
|
|
221
|
+
pluginDirs = (0, import_fs2.readdirSync)(cacheDir, { withFileTypes: true });
|
|
222
|
+
} catch {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
for (const pluginEntry of pluginDirs) {
|
|
226
|
+
if (!pluginEntry.isDirectory()) continue;
|
|
227
|
+
const pluginDir = (0, import_path4.join)(cacheDir, pluginEntry.name);
|
|
228
|
+
let versionDirs;
|
|
229
|
+
try {
|
|
230
|
+
versionDirs = (0, import_fs2.readdirSync)(pluginDir, { withFileTypes: true });
|
|
231
|
+
} catch {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
for (const versionEntry of versionDirs) {
|
|
235
|
+
if (!versionEntry.isDirectory()) continue;
|
|
236
|
+
const versionDir = (0, import_path4.join)(pluginDir, versionEntry.name);
|
|
237
|
+
const candidates = [(0, import_path4.join)(versionDir, ".mcp.json")];
|
|
238
|
+
try {
|
|
239
|
+
for (const sub of (0, import_fs2.readdirSync)(versionDir, { withFileTypes: true })) {
|
|
240
|
+
if (sub.isDirectory()) {
|
|
241
|
+
candidates.push((0, import_path4.join)(versionDir, sub.name, ".mcp.json"));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
for (const candidate of candidates) {
|
|
247
|
+
const found = await parseMCPConfig(candidate, "claude-code");
|
|
248
|
+
for (const server of found) {
|
|
249
|
+
if (!seen.has(server.name)) {
|
|
250
|
+
seen.add(server.name);
|
|
251
|
+
servers.push(server);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return servers;
|
|
128
258
|
}
|
|
129
259
|
|
|
130
260
|
// src/discovery/windsurf.ts
|
|
@@ -146,10 +276,67 @@ async function discoverVSCode() {
|
|
|
146
276
|
return tryConfigPaths(paths, "vscode");
|
|
147
277
|
}
|
|
148
278
|
|
|
149
|
-
// src/discovery/
|
|
150
|
-
var import_promises2 = require("fs/promises");
|
|
151
|
-
var import_fs2 = require("fs");
|
|
279
|
+
// src/discovery/openclaw.ts
|
|
152
280
|
var import_path7 = require("path");
|
|
281
|
+
var import_promises2 = require("fs/promises");
|
|
282
|
+
var import_fs3 = require("fs");
|
|
283
|
+
async function discoverOpenClaw() {
|
|
284
|
+
const home = getHome();
|
|
285
|
+
const allServers = [];
|
|
286
|
+
const seen = /* @__PURE__ */ new Set();
|
|
287
|
+
const configPaths = [
|
|
288
|
+
(0, import_path7.join)(home, ".openclaw", "openclaw.json"),
|
|
289
|
+
(0, import_path7.join)(process.cwd(), "openclaw.config.json")
|
|
290
|
+
];
|
|
291
|
+
for (const server of await tryConfigPaths(configPaths, "openclaw")) {
|
|
292
|
+
if (!seen.has(server.name)) {
|
|
293
|
+
seen.add(server.name);
|
|
294
|
+
allServers.push(server);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const globalConfig = (0, import_path7.join)(home, ".openclaw", "openclaw.json");
|
|
298
|
+
if ((0, import_fs3.existsSync)(globalConfig)) {
|
|
299
|
+
for (const server of await parseAgentMCPConfigs(globalConfig)) {
|
|
300
|
+
if (!seen.has(server.name)) {
|
|
301
|
+
seen.add(server.name);
|
|
302
|
+
allServers.push(server);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return allServers;
|
|
307
|
+
}
|
|
308
|
+
async function parseAgentMCPConfigs(configPath) {
|
|
309
|
+
try {
|
|
310
|
+
const raw = await (0, import_promises2.readFile)(configPath, "utf-8");
|
|
311
|
+
const config = JSON.parse(raw);
|
|
312
|
+
const agents = config?.agents?.list;
|
|
313
|
+
if (!Array.isArray(agents)) return [];
|
|
314
|
+
const servers = [];
|
|
315
|
+
for (const agent of agents) {
|
|
316
|
+
const mcpServers = agent?.mcp?.servers;
|
|
317
|
+
if (!Array.isArray(mcpServers)) continue;
|
|
318
|
+
for (const server of mcpServers) {
|
|
319
|
+
if (!server.name || !server.command && !server.url) continue;
|
|
320
|
+
servers.push({
|
|
321
|
+
name: server.name,
|
|
322
|
+
source: "openclaw",
|
|
323
|
+
command: server.command || "",
|
|
324
|
+
args: Array.isArray(server.args) ? server.args : [],
|
|
325
|
+
env: server.env,
|
|
326
|
+
configPath
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return servers;
|
|
331
|
+
} catch {
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/discovery/skills.ts
|
|
337
|
+
var import_promises3 = require("fs/promises");
|
|
338
|
+
var import_fs4 = require("fs");
|
|
339
|
+
var import_path8 = require("path");
|
|
153
340
|
var import_glob = require("glob");
|
|
154
341
|
async function discoverAllSkills() {
|
|
155
342
|
const skills = [];
|
|
@@ -183,8 +370,8 @@ async function discoverClaudeCodeSkills() {
|
|
|
183
370
|
const home = getHome();
|
|
184
371
|
const skills = [];
|
|
185
372
|
const projectPatterns = [
|
|
186
|
-
(0,
|
|
187
|
-
(0,
|
|
373
|
+
(0, import_path8.join)(process.cwd(), ".claude", "skills", "*", "SKILL.md"),
|
|
374
|
+
(0, import_path8.join)(process.cwd(), ".claude", "commands", "**", "*.md")
|
|
188
375
|
];
|
|
189
376
|
for (const pattern of projectPatterns) {
|
|
190
377
|
const files = await (0, import_glob.glob)(pattern, { absolute: true });
|
|
@@ -194,8 +381,8 @@ async function discoverClaudeCodeSkills() {
|
|
|
194
381
|
}
|
|
195
382
|
}
|
|
196
383
|
const globalPatterns = [
|
|
197
|
-
(0,
|
|
198
|
-
(0,
|
|
384
|
+
(0, import_path8.join)(home, ".claude", "skills", "*", "SKILL.md"),
|
|
385
|
+
(0, import_path8.join)(home, ".claude", "commands", "**", "*.md")
|
|
199
386
|
];
|
|
200
387
|
for (const pattern of globalPatterns) {
|
|
201
388
|
const files = await (0, import_glob.glob)(pattern, { absolute: true });
|
|
@@ -209,8 +396,8 @@ async function discoverClaudeCodeSkills() {
|
|
|
209
396
|
async function discoverGitHubCopilotSkills() {
|
|
210
397
|
const skills = [];
|
|
211
398
|
const patterns = [
|
|
212
|
-
(0,
|
|
213
|
-
(0,
|
|
399
|
+
(0, import_path8.join)(process.cwd(), ".github", "skills", "*", "SKILL.md"),
|
|
400
|
+
(0, import_path8.join)(process.cwd(), ".github", "copilot", "**", "*.md")
|
|
214
401
|
];
|
|
215
402
|
for (const pattern of patterns) {
|
|
216
403
|
const files = await (0, import_glob.glob)(pattern, { absolute: true });
|
|
@@ -224,18 +411,18 @@ async function discoverGitHubCopilotSkills() {
|
|
|
224
411
|
async function discoverCursorRules() {
|
|
225
412
|
const home = getHome();
|
|
226
413
|
const skills = [];
|
|
227
|
-
const projectPattern = (0,
|
|
414
|
+
const projectPattern = (0, import_path8.join)(process.cwd(), ".cursor", "rules", "*.mdc");
|
|
228
415
|
const projectFiles = await (0, import_glob.glob)(projectPattern, { absolute: true });
|
|
229
416
|
for (const filePath of projectFiles) {
|
|
230
417
|
const entry = await readSkillFile(filePath, "cursor", "mdc-rule", "project");
|
|
231
418
|
if (entry) skills.push(entry);
|
|
232
419
|
}
|
|
233
|
-
const legacyPath = (0,
|
|
234
|
-
if ((0,
|
|
420
|
+
const legacyPath = (0, import_path8.join)(process.cwd(), ".cursorrules");
|
|
421
|
+
if ((0, import_fs4.existsSync)(legacyPath)) {
|
|
235
422
|
const entry = await readSkillFile(legacyPath, "cursor", "mdc-rule", "project");
|
|
236
423
|
if (entry) skills.push(entry);
|
|
237
424
|
}
|
|
238
|
-
const globalPattern = (0,
|
|
425
|
+
const globalPattern = (0, import_path8.join)(home, ".cursor", "rules", "*.mdc");
|
|
239
426
|
const globalFiles = await (0, import_glob.glob)(globalPattern, { absolute: true });
|
|
240
427
|
for (const filePath of globalFiles) {
|
|
241
428
|
const entry = await readSkillFile(filePath, "cursor", "mdc-rule", "global");
|
|
@@ -249,16 +436,21 @@ async function discoverMemoryFiles() {
|
|
|
249
436
|
const cwd = process.cwd();
|
|
250
437
|
const memoryFiles = [
|
|
251
438
|
// Project-level memory files
|
|
252
|
-
{ path: (0,
|
|
253
|
-
{ path: (0,
|
|
254
|
-
{ path: (0,
|
|
255
|
-
{ path: (0,
|
|
439
|
+
{ path: (0, import_path8.join)(cwd, "CLAUDE.md"), fileType: "claude.md", scope: "project" },
|
|
440
|
+
{ path: (0, import_path8.join)(cwd, ".claude", "CLAUDE.md"), fileType: "claude.md", scope: "project" },
|
|
441
|
+
{ path: (0, import_path8.join)(cwd, "SOUL.md"), fileType: "soul.md", scope: "project" },
|
|
442
|
+
{ path: (0, import_path8.join)(cwd, "MEMORY.md"), fileType: "memory.md", scope: "project" },
|
|
443
|
+
// Additional project instruction files
|
|
444
|
+
{ path: (0, import_path8.join)(cwd, ".github", "copilot-instructions.md"), fileType: "claude.md", scope: "project" },
|
|
445
|
+
{ path: (0, import_path8.join)(cwd, "AGENTS.md"), fileType: "claude.md", scope: "project" },
|
|
446
|
+
{ path: (0, import_path8.join)(cwd, ".windsurfrules"), fileType: "claude.md", scope: "project" },
|
|
447
|
+
{ path: (0, import_path8.join)(cwd, "GEMINI.md"), fileType: "claude.md", scope: "project" },
|
|
256
448
|
// Global memory files
|
|
257
|
-
{ path: (0,
|
|
258
|
-
{ path: (0,
|
|
449
|
+
{ path: (0, import_path8.join)(home, ".claude", "CLAUDE.md"), fileType: "claude.md", scope: "global" },
|
|
450
|
+
{ path: (0, import_path8.join)(home, "CLAUDE.md"), fileType: "claude.md", scope: "global" }
|
|
259
451
|
];
|
|
260
452
|
for (const { path, fileType, scope } of memoryFiles) {
|
|
261
|
-
if ((0,
|
|
453
|
+
if ((0, import_fs4.existsSync)(path)) {
|
|
262
454
|
const entry = await readSkillFile(path, "memory-file", fileType, scope);
|
|
263
455
|
if (entry) skills.push(entry);
|
|
264
456
|
}
|
|
@@ -267,8 +459,8 @@ async function discoverMemoryFiles() {
|
|
|
267
459
|
}
|
|
268
460
|
async function readSkillFile(filePath, source, fileType, scope) {
|
|
269
461
|
try {
|
|
270
|
-
const content = await (0,
|
|
271
|
-
const fileStat = await (0,
|
|
462
|
+
const content = await (0, import_promises3.readFile)(filePath, "utf-8");
|
|
463
|
+
const fileStat = await (0, import_promises3.stat)(filePath);
|
|
272
464
|
const name = deriveSkillName(filePath, fileType);
|
|
273
465
|
return {
|
|
274
466
|
name,
|
|
@@ -286,11 +478,11 @@ async function readSkillFile(filePath, source, fileType, scope) {
|
|
|
286
478
|
function deriveSkillName(filePath, fileType) {
|
|
287
479
|
switch (fileType) {
|
|
288
480
|
case "skill.md": {
|
|
289
|
-
const parentDir = (0,
|
|
290
|
-
return parentDir === "skills" ? (0,
|
|
481
|
+
const parentDir = (0, import_path8.basename)((0, import_path8.dirname)(filePath));
|
|
482
|
+
return parentDir === "skills" ? (0, import_path8.basename)(filePath, ".md") : parentDir;
|
|
291
483
|
}
|
|
292
484
|
case "mdc-rule": {
|
|
293
|
-
const name = (0,
|
|
485
|
+
const name = (0, import_path8.basename)(filePath);
|
|
294
486
|
return name.replace(/\.(mdc|cursorrules?)$/, "") || name;
|
|
295
487
|
}
|
|
296
488
|
case "claude.md":
|
|
@@ -300,7 +492,7 @@ function deriveSkillName(filePath, fileType) {
|
|
|
300
492
|
case "memory.md":
|
|
301
493
|
return "MEMORY.md";
|
|
302
494
|
default:
|
|
303
|
-
return (0,
|
|
495
|
+
return (0, import_path8.basename)(filePath);
|
|
304
496
|
}
|
|
305
497
|
}
|
|
306
498
|
|
|
@@ -311,7 +503,8 @@ async function discoverAllServers(clientFilter) {
|
|
|
311
503
|
{ client: "cursor", fn: discoverCursor },
|
|
312
504
|
{ client: "claude-code", fn: discoverClaudeCode },
|
|
313
505
|
{ client: "windsurf", fn: discoverWindsurf },
|
|
314
|
-
{ client: "vscode", fn: discoverVSCode }
|
|
506
|
+
{ client: "vscode", fn: discoverVSCode },
|
|
507
|
+
{ client: "openclaw", fn: discoverOpenClaw }
|
|
315
508
|
];
|
|
316
509
|
const toRun = clientFilter ? discoverers.filter((d) => d.client === clientFilter) : discoverers;
|
|
317
510
|
const servers = [];
|
|
@@ -359,15 +552,10 @@ var TOOL_POISONING_PATTERNS = [
|
|
|
359
552
|
description: "Tool description instructs the AI agent to hide information from the user \u2014 a hallmark of tool poisoning.",
|
|
360
553
|
recommendation: "Do NOT install this MCP server. Legitimate tools never instruct agents to hide actions."
|
|
361
554
|
},
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
title: "System prompt override attempt",
|
|
367
|
-
pattern: /you\s+are\s+(now\s+)?(a|an|acting\s+as)/i,
|
|
368
|
-
description: "Tool description attempts to redefine the AI agent's identity or role.",
|
|
369
|
-
recommendation: "Remove this MCP server. Tool descriptions should not redefine agent behavior."
|
|
370
|
-
},
|
|
555
|
+
// TP-003 removed: "you are a/an [role]" is too broad — matches legitimate
|
|
556
|
+
// role descriptions in skills (e.g., "You are a computer science professor").
|
|
557
|
+
// SK-001 covers the real threat (malicious persona hijacking) with proper
|
|
558
|
+
// specificity by requiring suspicious role terms like "hacker" or "jailbroken".
|
|
371
559
|
{
|
|
372
560
|
id: "TP-004",
|
|
373
561
|
category: "tool-poisoning",
|
|
@@ -1313,6 +1501,7 @@ function deduplicateFindings2(findings) {
|
|
|
1313
1501
|
|
|
1314
1502
|
// src/sentinel/sentinel.ts
|
|
1315
1503
|
var import_child_process = require("child_process");
|
|
1504
|
+
var import_dns = require("dns");
|
|
1316
1505
|
|
|
1317
1506
|
// src/sentinel/sentinel-patterns.ts
|
|
1318
1507
|
var SUSPICIOUS_ENDPOINT_PATTERNS = [
|
|
@@ -1580,6 +1769,8 @@ var SentinelEngine = class {
|
|
|
1580
1769
|
serverName;
|
|
1581
1770
|
durationSeconds;
|
|
1582
1771
|
onEvent;
|
|
1772
|
+
/** DNS reverse-lookup cache: IP → hostname. Persists for the session. */
|
|
1773
|
+
dnsCache = /* @__PURE__ */ new Map();
|
|
1583
1774
|
constructor(options) {
|
|
1584
1775
|
this.serverName = options.serverName;
|
|
1585
1776
|
this.durationSeconds = options.durationSeconds || 120;
|
|
@@ -1588,11 +1779,15 @@ var SentinelEngine = class {
|
|
|
1588
1779
|
/**
|
|
1589
1780
|
* Start monitoring network activity for the given MCP server.
|
|
1590
1781
|
*
|
|
1591
|
-
* The monitor works in
|
|
1592
|
-
* 1. macOS: Uses `
|
|
1593
|
-
* 2. Linux: Uses `ss` polling
|
|
1594
|
-
* 3.
|
|
1595
|
-
*
|
|
1782
|
+
* The monitor works in four modes depending on the OS and permissions:
|
|
1783
|
+
* 1. macOS: Uses `lsof -i` polling (no root required)
|
|
1784
|
+
* 2. Linux: Uses `ss -tnp` polling (no root required)
|
|
1785
|
+
* 3. Windows: Uses PowerShell `Get-NetTCPConnection` (no admin required)
|
|
1786
|
+
* 4. Fallback: Stub — events must be fed manually via ingestEvent()
|
|
1787
|
+
*
|
|
1788
|
+
* All modes perform async reverse-DNS enrichment so that endpoint patterns
|
|
1789
|
+
* (pastebin.com, ngrok.io, etc.) match against resolved hostnames rather
|
|
1790
|
+
* than raw IPs.
|
|
1596
1791
|
*/
|
|
1597
1792
|
async startMonitoring() {
|
|
1598
1793
|
this.startTime = Date.now();
|
|
@@ -1606,6 +1801,9 @@ var SentinelEngine = class {
|
|
|
1606
1801
|
case "ss-poll":
|
|
1607
1802
|
await this.startSsPolling();
|
|
1608
1803
|
break;
|
|
1804
|
+
case "netstat-poll":
|
|
1805
|
+
await this.startNetstatPolling();
|
|
1806
|
+
break;
|
|
1609
1807
|
case "proxy":
|
|
1610
1808
|
await this.startProxyCapture();
|
|
1611
1809
|
break;
|
|
@@ -1734,66 +1932,103 @@ var SentinelEngine = class {
|
|
|
1734
1932
|
}
|
|
1735
1933
|
// ── Monitoring Methods ──
|
|
1736
1934
|
detectMonitoringMethod() {
|
|
1737
|
-
|
|
1738
|
-
|
|
1935
|
+
if (process.platform === "win32") {
|
|
1936
|
+
return "netstat-poll";
|
|
1937
|
+
}
|
|
1938
|
+
if (process.platform === "darwin") {
|
|
1739
1939
|
return "lsof-poll";
|
|
1940
|
+
}
|
|
1941
|
+
try {
|
|
1942
|
+
(0, import_child_process.execFileSync)("which", ["ss"], { stdio: "pipe" });
|
|
1943
|
+
return "ss-poll";
|
|
1740
1944
|
} catch {
|
|
1741
1945
|
try {
|
|
1742
|
-
(0, import_child_process.
|
|
1743
|
-
return "
|
|
1946
|
+
(0, import_child_process.execFileSync)("which", ["lsof"], { stdio: "pipe" });
|
|
1947
|
+
return "lsof-poll";
|
|
1744
1948
|
} catch {
|
|
1745
1949
|
return "proxy";
|
|
1746
1950
|
}
|
|
1747
1951
|
}
|
|
1748
1952
|
}
|
|
1749
1953
|
/**
|
|
1750
|
-
* macOS
|
|
1751
|
-
*
|
|
1954
|
+
* macOS: Poll `lsof -i` to capture network connections.
|
|
1955
|
+
* No root required. DNS-enriches IPs → hostnames before pattern matching.
|
|
1752
1956
|
*/
|
|
1753
1957
|
async startLsofPolling() {
|
|
1754
|
-
const
|
|
1958
|
+
const safeName = this.serverName.replace(/[^a-zA-Z0-9._-]/g, "");
|
|
1959
|
+
const pollInterval = setInterval(async () => {
|
|
1755
1960
|
try {
|
|
1756
|
-
const
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1961
|
+
const lsofOutput = (0, import_child_process.execFileSync)("/usr/sbin/lsof", ["-i", "-n", "-P"], {
|
|
1962
|
+
timeout: 5e3,
|
|
1963
|
+
encoding: "utf-8",
|
|
1964
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1965
|
+
});
|
|
1966
|
+
const lines = lsofOutput.split("\n").filter((line) => line.toLowerCase().includes(safeName.toLowerCase())).filter(Boolean);
|
|
1967
|
+
const rawEvents = lines.map((l) => this.parseLsofLine(l)).filter((e) => e !== null);
|
|
1968
|
+
const enriched = await Promise.all(rawEvents.map((e) => this.enrichWithHostname(e)));
|
|
1969
|
+
for (const event of enriched) this.ingestEvent(event);
|
|
1764
1970
|
} catch {
|
|
1765
1971
|
}
|
|
1766
1972
|
}, 2e3);
|
|
1767
|
-
this.monitorProcess = {
|
|
1768
|
-
|
|
1769
|
-
};
|
|
1770
|
-
setTimeout(() => {
|
|
1771
|
-
clearInterval(pollInterval);
|
|
1772
|
-
}, this.durationSeconds * 1e3);
|
|
1973
|
+
this.monitorProcess = { kill: () => clearInterval(pollInterval) };
|
|
1974
|
+
setTimeout(() => clearInterval(pollInterval), this.durationSeconds * 1e3);
|
|
1773
1975
|
}
|
|
1774
1976
|
/**
|
|
1775
|
-
* Linux: Poll `ss` (socket statistics) for network connections.
|
|
1977
|
+
* Linux: Poll `ss -tnp` (socket statistics) for network connections.
|
|
1978
|
+
* No root required. DNS-enriches IPs → hostnames before pattern matching.
|
|
1776
1979
|
*/
|
|
1777
1980
|
async startSsPolling() {
|
|
1778
|
-
const
|
|
1981
|
+
const safeName = this.serverName.replace(/[^a-zA-Z0-9._-]/g, "");
|
|
1982
|
+
const pollInterval = setInterval(async () => {
|
|
1779
1983
|
try {
|
|
1780
|
-
const
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1984
|
+
const ssOutput = (0, import_child_process.execFileSync)("ss", ["-tnp"], {
|
|
1985
|
+
timeout: 5e3,
|
|
1986
|
+
encoding: "utf-8",
|
|
1987
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1988
|
+
});
|
|
1989
|
+
const lines = ssOutput.split("\n").filter((line) => line.includes(safeName)).filter(Boolean);
|
|
1990
|
+
const rawEvents = lines.map((l) => this.parseSsLine(l)).filter((e) => e !== null);
|
|
1991
|
+
const enriched = await Promise.all(rawEvents.map((e) => this.enrichWithHostname(e)));
|
|
1992
|
+
for (const event of enriched) this.ingestEvent(event);
|
|
1788
1993
|
} catch {
|
|
1789
1994
|
}
|
|
1790
1995
|
}, 2e3);
|
|
1791
|
-
this.monitorProcess = {
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1996
|
+
this.monitorProcess = { kill: () => clearInterval(pollInterval) };
|
|
1997
|
+
setTimeout(() => clearInterval(pollInterval), this.durationSeconds * 1e3);
|
|
1998
|
+
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Windows: Poll PowerShell `Get-NetTCPConnection` for established connections.
|
|
2001
|
+
* No admin required (available on Windows 8+ / Server 2012+).
|
|
2002
|
+
* DNS-enriches IPs → hostnames so endpoint patterns match correctly.
|
|
2003
|
+
*
|
|
2004
|
+
* Note: Unlike lsof/ss, this captures ALL machine connections (not filtered
|
|
2005
|
+
* by process name) because Windows process-to-connection mapping requires
|
|
2006
|
+
* admin rights. Vigil's patterns handle the noise — it flags what matters.
|
|
2007
|
+
*/
|
|
2008
|
+
async startNetstatPolling() {
|
|
2009
|
+
const psCommand = `Get-NetTCPConnection -State Established | Where-Object { $_.RemoteAddress -notmatch '^(127\\.|::1$|0\\.0\\.0\\.0$|::$)' } | Select-Object LocalAddress,LocalPort,RemoteAddress,RemotePort | ConvertTo-Csv -NoTypeInformation`;
|
|
2010
|
+
const pollInterval = setInterval(async () => {
|
|
2011
|
+
try {
|
|
2012
|
+
const output = (0, import_child_process.execFileSync)("powershell", [
|
|
2013
|
+
"-NoProfile",
|
|
2014
|
+
"-NonInteractive",
|
|
2015
|
+
"-Command",
|
|
2016
|
+
psCommand
|
|
2017
|
+
], {
|
|
2018
|
+
timeout: 8e3,
|
|
2019
|
+
// PowerShell startup is slower than lsof
|
|
2020
|
+
encoding: "utf-8",
|
|
2021
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2022
|
+
});
|
|
2023
|
+
const lines = output.split("\n").slice(1).map((l) => l.trim()).filter(Boolean);
|
|
2024
|
+
const rawEvents = lines.map((l) => this.parsePowerShellLine(l)).filter((e) => e !== null);
|
|
2025
|
+
const enriched = await Promise.all(rawEvents.map((e) => this.enrichWithHostname(e)));
|
|
2026
|
+
for (const event of enriched) this.ingestEvent(event);
|
|
2027
|
+
} catch {
|
|
2028
|
+
}
|
|
2029
|
+
}, 3e3);
|
|
2030
|
+
this.monitorProcess = { kill: () => clearInterval(pollInterval) };
|
|
2031
|
+
setTimeout(() => clearInterval(pollInterval), this.durationSeconds * 1e3);
|
|
1797
2032
|
}
|
|
1798
2033
|
/**
|
|
1799
2034
|
* Fallback: Start a lightweight MITM proxy that MCP traffic routes through.
|
|
@@ -1839,6 +2074,65 @@ var SentinelEngine = class {
|
|
|
1839
2074
|
tls: parseInt(remotePort, 10) === 443
|
|
1840
2075
|
};
|
|
1841
2076
|
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Windows: Parse a CSV row from PowerShell `Get-NetTCPConnection | ConvertTo-Csv`.
|
|
2079
|
+
* Format: "LocalAddress","LocalPort","RemoteAddress","RemotePort"
|
|
2080
|
+
* Example: "192.168.1.5","54123","172.64.155.209","443"
|
|
2081
|
+
*/
|
|
2082
|
+
parsePowerShellLine(line) {
|
|
2083
|
+
const match = line.match(/^"?([^",]+)"?,"\d+","?([^",]+)"?,"?(\d+)"?/);
|
|
2084
|
+
if (!match) return null;
|
|
2085
|
+
const [, , remoteAddress, remotePort] = match;
|
|
2086
|
+
const port = parseInt(remotePort, 10);
|
|
2087
|
+
if (remoteAddress === "127.0.0.1" || remoteAddress === "::1" || remoteAddress.startsWith("10.") || remoteAddress.startsWith("192.168.") || /^172\.(1[6-9]|2\d|3[01])\./.test(remoteAddress)) {
|
|
2088
|
+
return null;
|
|
2089
|
+
}
|
|
2090
|
+
return {
|
|
2091
|
+
timestamp: Date.now(),
|
|
2092
|
+
serverName: this.serverName,
|
|
2093
|
+
method: "TCP",
|
|
2094
|
+
url: `https://${remoteAddress}:${remotePort}/`,
|
|
2095
|
+
destinationIp: remoteAddress,
|
|
2096
|
+
port,
|
|
2097
|
+
requestSize: 0,
|
|
2098
|
+
tls: port === 443
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
// ── DNS Enrichment ──
|
|
2102
|
+
/**
|
|
2103
|
+
* Reverse-DNS lookup with a session-scoped cache.
|
|
2104
|
+
* Returns the first PTR record if available, otherwise the raw IP.
|
|
2105
|
+
*
|
|
2106
|
+
* WHY this matters: Sentinel's endpoint patterns match against known-bad
|
|
2107
|
+
* hostnames (pastebin.com, ngrok.io, etc.). Without DNS enrichment, lsof/ss/
|
|
2108
|
+
* netstat all return raw IPs and those patterns would silently miss every hit.
|
|
2109
|
+
*/
|
|
2110
|
+
async resolveIp(ip) {
|
|
2111
|
+
const cached = this.dnsCache.get(ip);
|
|
2112
|
+
if (cached !== void 0) return cached;
|
|
2113
|
+
try {
|
|
2114
|
+
const hostnames = await import_dns.promises.reverse(ip);
|
|
2115
|
+
const hostname = hostnames[0] ?? ip;
|
|
2116
|
+
this.dnsCache.set(ip, hostname);
|
|
2117
|
+
return hostname;
|
|
2118
|
+
} catch {
|
|
2119
|
+
this.dnsCache.set(ip, ip);
|
|
2120
|
+
return ip;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Enrich a NetworkEvent's url field with a resolved hostname.
|
|
2125
|
+
* Returns a new event object (does not mutate the original).
|
|
2126
|
+
*/
|
|
2127
|
+
async enrichWithHostname(event) {
|
|
2128
|
+
if (!event.destinationIp) return event;
|
|
2129
|
+
const hostname = await this.resolveIp(event.destinationIp);
|
|
2130
|
+
if (hostname === event.destinationIp) return event;
|
|
2131
|
+
return {
|
|
2132
|
+
...event,
|
|
2133
|
+
url: `https://${hostname}:${event.port}/`
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
1842
2136
|
};
|
|
1843
2137
|
function getSentinelFeatures(tier) {
|
|
1844
2138
|
switch (tier) {
|
|
@@ -2043,6 +2337,7 @@ function printNoServersFound() {
|
|
|
2043
2337
|
console.log(import_chalk.default.gray(" \u2022 Claude Code config (.claude.json / .mcp.json)"));
|
|
2044
2338
|
console.log(import_chalk.default.gray(" \u2022 Windsurf MCP config"));
|
|
2045
2339
|
console.log(import_chalk.default.gray(" \u2022 VS Code MCP config (.vscode/mcp.json)"));
|
|
2340
|
+
console.log(import_chalk.default.gray(" \u2022 OpenClaw config (~/.openclaw/openclaw.json)"));
|
|
2046
2341
|
console.log("");
|
|
2047
2342
|
console.log(import_chalk.default.gray(" If you have MCP servers configured elsewhere, use:"));
|
|
2048
2343
|
console.log(import_chalk.default.cyan(" vigile-scan --config /path/to/config.json"));
|
|
@@ -2204,8 +2499,8 @@ function formatJSON(summary) {
|
|
|
2204
2499
|
}
|
|
2205
2500
|
|
|
2206
2501
|
// src/api/auth.ts
|
|
2207
|
-
var
|
|
2208
|
-
var
|
|
2502
|
+
var import_promises4 = require("fs/promises");
|
|
2503
|
+
var import_path9 = require("path");
|
|
2209
2504
|
var import_os2 = require("os");
|
|
2210
2505
|
|
|
2211
2506
|
// src/api/client.ts
|
|
@@ -2216,7 +2511,19 @@ var VigileApiClient = class {
|
|
|
2216
2511
|
baseUrl;
|
|
2217
2512
|
token;
|
|
2218
2513
|
constructor(baseUrl, token) {
|
|
2219
|
-
|
|
2514
|
+
const rawUrl = (baseUrl || process.env.VIGILE_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
2515
|
+
try {
|
|
2516
|
+
const u = new URL(rawUrl);
|
|
2517
|
+
if (u.protocol !== "https:" && u.hostname !== "localhost" && u.hostname !== "127.0.0.1") {
|
|
2518
|
+
console.error(`[vigile] API URL must use HTTPS \u2014 falling back to default`);
|
|
2519
|
+
this.baseUrl = DEFAULT_API_URL;
|
|
2520
|
+
} else {
|
|
2521
|
+
this.baseUrl = rawUrl;
|
|
2522
|
+
}
|
|
2523
|
+
} catch {
|
|
2524
|
+
console.error(`[vigile] Invalid API URL \u2014 falling back to default`);
|
|
2525
|
+
this.baseUrl = DEFAULT_API_URL;
|
|
2526
|
+
}
|
|
2220
2527
|
this.token = token || null;
|
|
2221
2528
|
}
|
|
2222
2529
|
get isAuthenticated() {
|
|
@@ -2301,8 +2608,8 @@ var VigileApiClient = class {
|
|
|
2301
2608
|
};
|
|
2302
2609
|
|
|
2303
2610
|
// src/api/auth.ts
|
|
2304
|
-
var CONFIG_DIR = (0,
|
|
2305
|
-
var CONFIG_FILE = (0,
|
|
2611
|
+
var CONFIG_DIR = (0, import_path9.join)((0, import_os2.homedir)(), ".vigile");
|
|
2612
|
+
var CONFIG_FILE = (0, import_path9.join)(CONFIG_DIR, "config.json");
|
|
2306
2613
|
async function resolveToken() {
|
|
2307
2614
|
const envToken = process.env.VIGILE_TOKEN;
|
|
2308
2615
|
if (envToken && envToken.length > 0) {
|
|
@@ -2319,15 +2626,15 @@ async function resolveApiUrl() {
|
|
|
2319
2626
|
}
|
|
2320
2627
|
async function loadConfig() {
|
|
2321
2628
|
try {
|
|
2322
|
-
const raw = await (0,
|
|
2629
|
+
const raw = await (0, import_promises4.readFile)(CONFIG_FILE, "utf-8");
|
|
2323
2630
|
return JSON.parse(raw);
|
|
2324
2631
|
} catch {
|
|
2325
2632
|
return {};
|
|
2326
2633
|
}
|
|
2327
2634
|
}
|
|
2328
2635
|
async function saveConfig(config) {
|
|
2329
|
-
await (0,
|
|
2330
|
-
await (0,
|
|
2636
|
+
await (0, import_promises4.mkdir)(CONFIG_DIR, { recursive: true });
|
|
2637
|
+
await (0, import_promises4.writeFile)(CONFIG_FILE, JSON.stringify(config, null, 2), {
|
|
2331
2638
|
mode: 384
|
|
2332
2639
|
// Owner read/write only
|
|
2333
2640
|
});
|
|
@@ -2379,7 +2686,7 @@ async function getAuthenticatedClient() {
|
|
|
2379
2686
|
}
|
|
2380
2687
|
|
|
2381
2688
|
// src/index.ts
|
|
2382
|
-
var VERSION =
|
|
2689
|
+
var VERSION = require_package().version;
|
|
2383
2690
|
var program = new import_commander.Command();
|
|
2384
2691
|
program.name("vigile-scan").description(
|
|
2385
2692
|
"Security scanner for AI agent tools \u2014 detect tool poisoning, permission abuse, and supply chain attacks in MCP servers and agent skills"
|
|
@@ -2387,7 +2694,7 @@ program.name("vigile-scan").description(
|
|
|
2387
2694
|
function addScanOptions(cmd) {
|
|
2388
2695
|
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(
|
|
2389
2696
|
"--client <client>",
|
|
2390
|
-
"Only scan a specific client (claude-desktop, cursor, claude-code, windsurf, vscode)"
|
|
2697
|
+
"Only scan a specific client (claude-desktop, cursor, claude-code, windsurf, vscode, openclaw)"
|
|
2391
2698
|
).option("-s, --skills", "Scan agent skills only (SKILL.md, .mdc rules, CLAUDE.md, etc.)").option("-a, --all", "Scan both MCP servers and agent skills").option("--sentinel", "Enable Sentinel runtime monitoring (Pro+ feature)").option("--sentinel-server <name>", "Monitor a specific MCP server by name").option("--sentinel-duration <seconds>", "Monitoring duration in seconds (default: 120)", parseInt).option("--no-upload", "Skip uploading scan results to Vigile API");
|
|
2392
2699
|
}
|
|
2393
2700
|
addScanOptions(
|
|
@@ -2527,9 +2834,13 @@ async function runScan(options) {
|
|
|
2527
2834
|
if (isJSON) {
|
|
2528
2835
|
const jsonOutput = formatJSON(summary);
|
|
2529
2836
|
if (options.output) {
|
|
2530
|
-
await (0,
|
|
2837
|
+
await (0, import_promises5.writeFile)(options.output, jsonOutput);
|
|
2531
2838
|
} else {
|
|
2532
|
-
|
|
2839
|
+
await new Promise((resolve, reject) => {
|
|
2840
|
+
const ok = process.stdout.write(jsonOutput + "\n");
|
|
2841
|
+
if (ok) resolve();
|
|
2842
|
+
else process.stdout.once("drain", resolve);
|
|
2843
|
+
});
|
|
2533
2844
|
}
|
|
2534
2845
|
} else {
|
|
2535
2846
|
console.log("");
|
|
@@ -2545,7 +2856,7 @@ async function runScan(options) {
|
|
|
2545
2856
|
}
|
|
2546
2857
|
printSummary(summary);
|
|
2547
2858
|
if (options.output) {
|
|
2548
|
-
await (0,
|
|
2859
|
+
await (0, import_promises5.writeFile)(options.output, formatJSON(summary));
|
|
2549
2860
|
console.log(` Results saved to ${options.output}`);
|
|
2550
2861
|
}
|
|
2551
2862
|
}
|
|
@@ -2556,6 +2867,13 @@ async function runScan(options) {
|
|
|
2556
2867
|
await runSentinel(options, results, isJSON);
|
|
2557
2868
|
}
|
|
2558
2869
|
if (summary.bySeverity.critical > 0 || summary.bySeverity.high > 0) {
|
|
2870
|
+
await new Promise((resolve) => {
|
|
2871
|
+
if (process.stdout.writableNeedDrain) {
|
|
2872
|
+
process.stdout.once("drain", resolve);
|
|
2873
|
+
} else {
|
|
2874
|
+
resolve();
|
|
2875
|
+
}
|
|
2876
|
+
});
|
|
2559
2877
|
process.exit(1);
|
|
2560
2878
|
}
|
|
2561
2879
|
}
|