agentseal 0.8.1 → 0.9.1

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.
@@ -0,0 +1,1962 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ BaselineStore
4
+ } from "./chunk-PG5LEDUE.js";
5
+ import {
6
+ GuardVerdict
7
+ } from "./chunk-IGSX7F4B.js";
8
+ import {
9
+ PROJECT_SKILL_DIRS,
10
+ PROJECT_SKILL_FILES,
11
+ getWellKnownConfigs,
12
+ init_machine_discovery,
13
+ stripJsonComments
14
+ } from "./chunk-IO5DO7DS.js";
15
+ import "./chunk-ZLRN7Q7C.js";
16
+
17
+ // src/shield.ts
18
+ import { readFileSync as readFileSync3, statSync as statSync3, watch } from "fs";
19
+ import { homedir as homedir4 } from "os";
20
+ import { basename as basename3, dirname as dirname2, extname as extname2, join as join3, resolve as resolve2 } from "path";
21
+
22
+ // src/blocklist.ts
23
+ import { createHash } from "crypto";
24
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
25
+ import { homedir } from "os";
26
+ import { join } from "path";
27
+ var SEED_HASHES = /* @__PURE__ */ new Set([
28
+ "854aa9bd5a641b03fcf2e4a26affb33057af3238a10a83e194c05384f371734f",
29
+ // credential-theft-cursorrules
30
+ "46315c1d4dcd39199c6d0e43985c5007c1156bc538e3a82ba9b2883f363eab35",
31
+ // markdown-image-exfil
32
+ "0b2ca8fedb87a97de9f5c462e09110febf887516dd62877d7e95a5556ef90905",
33
+ // reverse-shell-instruction
34
+ "2b5a339d00216894c7bd3620e008e5443f4e30b9e9883a2b15c082d076775084",
35
+ // curl-exfil-instruction
36
+ "eccb3a65c459a6b69223d38726e3fddb6184a6e7c52935148fdcd84961a6f9df",
37
+ // prompt-injection-override
38
+ "f554a511faaca2431265399a9d5b2f7184778b9521952dc757257dbe0aab2a46",
39
+ // supply-chain-install
40
+ "323b9121b6e320fb04bae89c963690069c5172dca017469be2917e5feaec886c",
41
+ // obfuscated-credential-theft
42
+ "4826c0e8aef00f902190ab32519e4533b7e4b725f46fb70156705ea8708a7385",
43
+ // social-engineering-exfil
44
+ "3951cdb38bbc37e28f98448e0478b93d319d892783efb23462b59fedea52189d",
45
+ // mcp-config-injection
46
+ "a7ddd5ce6c41055b4ef808810ac6f1b09dc4ae05eecc2f89dc64ac4682502d99",
47
+ // keylogger-instruction
48
+ "eab3b7330de3b61fae1b5cba738ae499424e1c45ef1b025c560cca410e6cd16b",
49
+ // crypto-miner-injection
50
+ "d71ceee36d1e136a5cddc0d5b416210d94635a71fa90f9ef817f4f74a7b21603"
51
+ // dns-exfil-instruction
52
+ ]);
53
+ var Blocklist = class _Blocklist {
54
+ static REMOTE_URL = "https://agentseal.org/api/v1/blocklist/skills.json";
55
+ static CACHE_TTL = 3600;
56
+ // 1 hour in seconds
57
+ _hashes = new Set(SEED_HASHES);
58
+ _loaded = false;
59
+ _cacheDir;
60
+ _cachePath;
61
+ constructor(cacheDir) {
62
+ this._cacheDir = cacheDir ?? join(homedir(), ".agentseal");
63
+ this._cachePath = join(this._cacheDir, "blocklist.json");
64
+ }
65
+ /** Override cache dir (useful for testing). */
66
+ setCacheDir(dir) {
67
+ this._cacheDir = dir;
68
+ this._cachePath = join(dir, "blocklist.json");
69
+ this._loaded = false;
70
+ this._hashes = new Set(SEED_HASHES);
71
+ }
72
+ _load() {
73
+ if (this._loaded) return;
74
+ if (existsSync(this._cachePath)) {
75
+ try {
76
+ const age = Date.now() / 1e3 - statSync(this._cachePath).mtimeMs / 1e3;
77
+ if (age < _Blocklist.CACHE_TTL) {
78
+ this._loadFromFile(this._cachePath);
79
+ this._loaded = true;
80
+ return;
81
+ }
82
+ } catch {
83
+ }
84
+ }
85
+ if (this._tryRemoteFetch()) {
86
+ this._loaded = true;
87
+ return;
88
+ }
89
+ if (existsSync(this._cachePath)) {
90
+ this._loadFromFile(this._cachePath);
91
+ }
92
+ this._loaded = true;
93
+ }
94
+ _loadFromFile(path) {
95
+ try {
96
+ const raw = readFileSync(path, "utf-8");
97
+ const data = JSON.parse(raw);
98
+ for (const h of data.sha256_hashes ?? []) {
99
+ this._hashes.add(h);
100
+ }
101
+ } catch {
102
+ }
103
+ }
104
+ _tryRemoteFetch() {
105
+ return false;
106
+ }
107
+ /** Async remote fetch — call this once at startup if you want remote blocklist. */
108
+ async loadAsync() {
109
+ if (this._loaded) return;
110
+ if (existsSync(this._cachePath)) {
111
+ try {
112
+ const age = Date.now() / 1e3 - statSync(this._cachePath).mtimeMs / 1e3;
113
+ if (age < _Blocklist.CACHE_TTL) {
114
+ this._loadFromFile(this._cachePath);
115
+ this._loaded = true;
116
+ return;
117
+ }
118
+ } catch {
119
+ }
120
+ }
121
+ try {
122
+ const resp = await fetch(_Blocklist.REMOTE_URL, {
123
+ signal: AbortSignal.timeout(5e3)
124
+ });
125
+ if (resp.ok) {
126
+ const data = await resp.json();
127
+ for (const h of data.sha256_hashes ?? []) {
128
+ this._hashes.add(h);
129
+ }
130
+ mkdirSync(this._cacheDir, { recursive: true });
131
+ writeFileSync(this._cachePath, JSON.stringify(data), "utf-8");
132
+ this._loaded = true;
133
+ return;
134
+ }
135
+ } catch {
136
+ }
137
+ if (existsSync(this._cachePath)) {
138
+ this._loadFromFile(this._cachePath);
139
+ }
140
+ this._loaded = true;
141
+ }
142
+ /** Check if a SHA256 hash is in the blocklist. */
143
+ isBlocked(sha256) {
144
+ this._load();
145
+ return this._hashes.has(sha256.toLowerCase());
146
+ }
147
+ /** Number of hashes in the blocklist. */
148
+ get size() {
149
+ this._load();
150
+ return this._hashes.size;
151
+ }
152
+ /** Manually add hashes (for testing or seed data). */
153
+ addHashes(hashes) {
154
+ for (const h of hashes) {
155
+ this._hashes.add(h.toLowerCase());
156
+ }
157
+ }
158
+ };
159
+
160
+ // src/shield.ts
161
+ init_machine_discovery();
162
+
163
+ // src/mcp-checker.ts
164
+ import { realpathSync } from "fs";
165
+ import { homedir as homedir2 } from "os";
166
+ import { basename } from "path";
167
+ var SENSITIVE_PATHS = [
168
+ [".ssh", "SSH private keys"],
169
+ [".aws", "AWS credentials"],
170
+ [".gnupg", "GPG private keys"],
171
+ [".config/gh", "GitHub CLI credentials"],
172
+ [".npmrc", "NPM auth tokens"],
173
+ [".pypirc", "PyPI credentials"],
174
+ [".docker", "Docker credentials"],
175
+ [".kube", "Kubernetes credentials"],
176
+ [".netrc", "Network login credentials"],
177
+ [".bitcoin", "Bitcoin wallet"],
178
+ [".ethereum", "Ethereum wallet"],
179
+ ["Library/Keychains", "macOS Keychain"],
180
+ [".gitconfig", "Git credentials"],
181
+ [".clawdbot/.env", "OpenClaw credentials"],
182
+ [".openclaw/.env", "OpenClaw credentials"]
183
+ ];
184
+ var CREDENTIAL_PATTERNS = [
185
+ [/sk-(?:proj-)?[a-zA-Z0-9]{20,}/, "OpenAI API key"],
186
+ [/sk_live_[a-zA-Z0-9]+/, "Stripe live key"],
187
+ [/sk_test_[a-zA-Z0-9]+/, "Stripe test key"],
188
+ [/AKIA[0-9A-Z]{16}/, "AWS access key"],
189
+ [/ghp_[a-zA-Z0-9]{36}/, "GitHub personal token"],
190
+ [/gho_[a-zA-Z0-9]{36}/, "GitHub OAuth token"],
191
+ [/xoxb-[a-zA-Z0-9-]+/, "Slack bot token"],
192
+ [/xoxp-[a-zA-Z0-9-]+/, "Slack user token"],
193
+ [/glpat-[a-zA-Z0-9_-]{20,}/, "GitLab personal token"],
194
+ [/SG\.[a-zA-Z0-9_-]{22,}/, "SendGrid API key"],
195
+ [/sk-ant-api03-[A-Za-z0-9_-]{90,}/, "Anthropic API key"],
196
+ [/AIza[A-Za-z0-9_-]{35}/, "Google/Gemini API key"],
197
+ [/gsk_[A-Za-z0-9]{20,}/, "Groq API key"],
198
+ [/co-[A-Za-z0-9]{20,}/, "Cohere API key"],
199
+ [/r8_[A-Za-z0-9]{20,}/, "Replicate API token"],
200
+ [/hf_[A-Za-z0-9]{20,}/, "HuggingFace token"],
201
+ [/pcsk_[A-Za-z0-9_-]{20,}/, "Pinecone API key"],
202
+ [/sbp_[a-f0-9]{40,}/, "Supabase token"],
203
+ [/vercel_[A-Za-z0-9_-]{20,}/, "Vercel token"],
204
+ [/fw_[A-Za-z0-9]{20,}/, "Fireworks API key"],
205
+ [/pplx-[a-f0-9]{48,}/, "Perplexity API key"],
206
+ [/SK[a-f0-9]{32}/, "Twilio API key"],
207
+ [/dd[a-z][a-f0-9]{40}/, "Datadog API key"],
208
+ [/el_[A-Za-z0-9]{20,}/, "ElevenLabs API key"],
209
+ [/voyage-[A-Za-z0-9_-]{20,}/, "Voyage AI key"],
210
+ [/tog-[A-Za-z0-9]{20,}/, "Together AI key"],
211
+ [/csk-[A-Za-z0-9]{20,}/, "Cerebras API key"],
212
+ [/v1\.0-[a-f0-9]{24}-[a-f0-9]{64,}/, "Cloudflare API token"],
213
+ [/-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/, "PEM private key"]
214
+ ];
215
+ var KNOWN_MALICIOUS_PACKAGES = /* @__PURE__ */ new Set([
216
+ "crossenv",
217
+ "d3.js",
218
+ "fabric-js",
219
+ "ffmepg",
220
+ "grequsts",
221
+ "http-proxy.js",
222
+ "mariadb",
223
+ "mssql-node",
224
+ "mssql.js",
225
+ "mysqljs",
226
+ "node-fabric",
227
+ "node-opencv",
228
+ "node-opensl",
229
+ "node-openssl",
230
+ "nodecaffe",
231
+ "nodefabric",
232
+ "nodeffmpeg",
233
+ "nodemailer-js",
234
+ "nodemssql",
235
+ "noderequest",
236
+ "nodesass",
237
+ "nodesqlite",
238
+ "opencv.js",
239
+ "openssl.js",
240
+ "proxy.js",
241
+ "shadowsock",
242
+ "smb",
243
+ "sqlite.js",
244
+ "sqliter",
245
+ "sqlserver",
246
+ "tkinter"
247
+ ]);
248
+ var DANGEROUS_SHELLS = /* @__PURE__ */ new Set(["bash", "sh", "cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh"]);
249
+ var SHELL_META = /[;|&`$()]/;
250
+ var HTTP_NON_LOCAL = /http:\/\/(?!localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])/;
251
+ function shannonEntropy(s) {
252
+ if (!s) return 0;
253
+ const freq = {};
254
+ for (const c of s) {
255
+ freq[c] = (freq[c] ?? 0) + 1;
256
+ }
257
+ const len = s.length;
258
+ let entropy = 0;
259
+ for (const count of Object.values(freq)) {
260
+ const p = count / len;
261
+ entropy -= p * Math.log2(p);
262
+ }
263
+ return entropy;
264
+ }
265
+ function verdictFromFindings(findings) {
266
+ if (findings.length === 0) return GuardVerdict.SAFE;
267
+ if (findings.some((f) => f.severity === "critical")) return GuardVerdict.DANGER;
268
+ if (findings.some((f) => f.severity === "high" || f.severity === "medium")) return GuardVerdict.WARNING;
269
+ return GuardVerdict.SAFE;
270
+ }
271
+ var MCPConfigChecker = class {
272
+ /** Check a single MCP server config dict for security issues. */
273
+ check(server) {
274
+ const name = server.name ?? "unknown";
275
+ const rawCmd = server.command ?? "";
276
+ let command;
277
+ let args;
278
+ if (Array.isArray(rawCmd)) {
279
+ command = String(rawCmd[0] ?? "");
280
+ args = [...rawCmd.slice(1).map(String), ...server.args ?? []];
281
+ } else {
282
+ command = String(rawCmd);
283
+ args = server.args ?? [];
284
+ }
285
+ const env = server.env ?? {};
286
+ const source = server.source_file ?? "";
287
+ const url = server.url ?? "";
288
+ const findings = [];
289
+ findings.push(...this._checkSensitivePaths(name, args));
290
+ findings.push(...this._checkEnvCredentials(name, env));
291
+ findings.push(...this._checkBroadAccess(name, args));
292
+ findings.push(...this._checkInsecureUrls(name, args, env));
293
+ if (url) findings.push(...this._checkHttpServer(name, server));
294
+ findings.push(...this._checkSupplyChain(name, command, args));
295
+ findings.push(...this._checkCommandInjection(name, command, args));
296
+ findings.push(...this._checkMissingAuth(name, server));
297
+ findings.push(...this._checkKnownCVEs(name, server));
298
+ findings.push(...this._checkHighEntropySecrets(name, env));
299
+ const verdict = verdictFromFindings(findings);
300
+ return {
301
+ name,
302
+ command: command || url,
303
+ source_file: source,
304
+ verdict,
305
+ findings
306
+ };
307
+ }
308
+ /** Check multiple MCP server configs. */
309
+ checkAll(servers) {
310
+ return servers.map((s) => this.check(s));
311
+ }
312
+ // ── Individual checks ──────────────────────────────────────────────
313
+ _checkSensitivePaths(name, args) {
314
+ const findings = [];
315
+ const home = homedir2();
316
+ const resolvedArgs = args.map((a) => {
317
+ try {
318
+ return realpathSync(a);
319
+ } catch {
320
+ return a;
321
+ }
322
+ });
323
+ for (const arg of resolvedArgs) {
324
+ if (typeof arg !== "string") continue;
325
+ const expanded = arg.startsWith("~") ? home + arg.slice(1) : arg;
326
+ for (const [suffix, description] of SENSITIVE_PATHS) {
327
+ const full = `${home}/${suffix}`;
328
+ if (expanded.includes(full) || arg.includes(suffix)) {
329
+ findings.push({
330
+ code: "MCP-001",
331
+ title: `Access to ${description}`,
332
+ description: `MCP server '${name}' has filesystem access to ${suffix} (${description}). This is a critical security risk.`,
333
+ severity: "critical",
334
+ remediation: `Restrict '${name}' MCP server: remove ${suffix} from allowed paths. It does not need access to ${description}.`
335
+ });
336
+ break;
337
+ }
338
+ }
339
+ }
340
+ return findings;
341
+ }
342
+ _checkEnvCredentials(name, env) {
343
+ const findings = [];
344
+ for (const [envKey, envValue] of Object.entries(env)) {
345
+ if (typeof envValue !== "string") continue;
346
+ if (envValue.startsWith("${") || envValue.startsWith("$")) continue;
347
+ for (const [pattern, credType] of CREDENTIAL_PATTERNS) {
348
+ if (pattern.test(envValue)) {
349
+ const redacted = envValue.length > 14 ? envValue.slice(0, 6) + "..." + envValue.slice(-4) : "***";
350
+ findings.push({
351
+ code: "MCP-002",
352
+ title: `Hardcoded ${credType}`,
353
+ description: `MCP server '${name}' has a hardcoded ${credType} in env var ${envKey} (${redacted}). Credentials should not be stored in config files.`,
354
+ severity: "high",
355
+ remediation: `Move ${envKey} for '${name}' to a secrets manager or environment variable. Do not store API keys in MCP config files.`
356
+ });
357
+ break;
358
+ }
359
+ }
360
+ }
361
+ return findings;
362
+ }
363
+ _checkBroadAccess(name, args) {
364
+ const home = homedir2();
365
+ for (const arg of args) {
366
+ if (typeof arg !== "string") continue;
367
+ const expanded = arg.replace("~", home);
368
+ if (expanded === "/" || expanded === home || arg === "~" || arg === "/") {
369
+ return [{
370
+ code: "MCP-003",
371
+ title: "Overly broad filesystem access",
372
+ description: `MCP server '${name}' has access to the entire ${expanded === home ? "home directory" : "filesystem"}. This grants access to all files including credentials.`,
373
+ severity: "high",
374
+ remediation: `Restrict '${name}' to specific project directories only.`
375
+ }];
376
+ }
377
+ }
378
+ return [];
379
+ }
380
+ _checkInsecureUrls(name, args, env) {
381
+ const allValues = args.filter((a) => typeof a === "string");
382
+ for (const v of Object.values(env)) {
383
+ if (typeof v === "string") allValues.push(v);
384
+ }
385
+ for (const value of allValues) {
386
+ if (HTTP_NON_LOCAL.test(value)) {
387
+ return [{
388
+ code: "MCP-005",
389
+ title: "Insecure HTTP connection",
390
+ description: `MCP server '${name}' uses an unencrypted HTTP connection. Data sent to this server could be intercepted.`,
391
+ severity: "medium",
392
+ remediation: `Use HTTPS for '${name}' MCP server connections.`
393
+ }];
394
+ }
395
+ }
396
+ return [];
397
+ }
398
+ _checkHttpServer(name, server) {
399
+ const findings = [];
400
+ const url = server.url ?? "";
401
+ const headers = server.headers ?? {};
402
+ const apiKey = server.apiKey ?? "";
403
+ if (typeof url === "string" && HTTP_NON_LOCAL.test(url)) {
404
+ findings.push({
405
+ code: "MCP-006",
406
+ title: "Insecure remote MCP endpoint",
407
+ description: `MCP server '${name}' connects to a remote HTTP endpoint without TLS. All JSON-RPC traffic can be intercepted.`,
408
+ severity: "critical",
409
+ remediation: `Use HTTPS for remote MCP server '${name}': change ${url} to use https://`
410
+ });
411
+ }
412
+ if (typeof apiKey === "string" && apiKey && !apiKey.startsWith("${")) {
413
+ for (const [pattern, credType] of CREDENTIAL_PATTERNS) {
414
+ if (pattern.test(apiKey)) {
415
+ const redacted = apiKey.length > 14 ? apiKey.slice(0, 6) + "..." + apiKey.slice(-4) : "***";
416
+ findings.push({
417
+ code: "MCP-006",
418
+ title: `Hardcoded ${credType} in apiKey`,
419
+ description: `MCP server '${name}' has a hardcoded ${credType} in apiKey field (${redacted}). Use environment variable references.`,
420
+ severity: "high",
421
+ remediation: `Move apiKey for '${name}' to a secrets manager or env var reference.`
422
+ });
423
+ break;
424
+ }
425
+ }
426
+ }
427
+ if (typeof headers === "object" && headers !== null) {
428
+ const authVal = headers.Authorization ?? "";
429
+ if (typeof authVal === "string" && authVal && !authVal.startsWith("${")) {
430
+ for (const [pattern, credType] of CREDENTIAL_PATTERNS) {
431
+ if (pattern.test(authVal)) {
432
+ findings.push({
433
+ code: "MCP-006",
434
+ title: `Hardcoded ${credType} in Authorization header`,
435
+ description: `MCP server '${name}' has a hardcoded credential in the Authorization header. Use environment variable references.`,
436
+ severity: "high",
437
+ remediation: `Move Authorization header for '${name}' to env var reference.`
438
+ });
439
+ break;
440
+ }
441
+ }
442
+ }
443
+ }
444
+ return findings;
445
+ }
446
+ _checkSupplyChain(name, command, args) {
447
+ const findings = [];
448
+ const allStr = [command, ...args.filter((a) => typeof a === "string")].join(" ");
449
+ const npxMatch = allStr.match(/npx\s+-y\s+(@?[a-zA-Z0-9_./-]+(?:@[^\s]+)?)/);
450
+ if (npxMatch) {
451
+ const pkg = npxMatch[1];
452
+ const parts = pkg.split("/");
453
+ const lastPart = parts[parts.length - 1] ?? pkg;
454
+ const hasVersion = lastPart.includes("@") && !lastPart.startsWith("@");
455
+ if (!hasVersion) {
456
+ findings.push({
457
+ code: "MCP-007",
458
+ title: "Unpinned npx package",
459
+ description: `MCP server '${name}' installs '${pkg}' via npx without version pinning. A supply chain attack could inject malicious code.`,
460
+ severity: "high",
461
+ remediation: `Pin the version: npx -y ${pkg}@<version>`
462
+ });
463
+ }
464
+ }
465
+ const uvxMatch = allStr.match(/uvx\s+([a-zA-Z0-9_.-]+)/);
466
+ if (uvxMatch) {
467
+ const pkg = uvxMatch[1];
468
+ const afterPkg = allStr.split(pkg).slice(1).join("").slice(0, 20);
469
+ if (!afterPkg.includes("==")) {
470
+ findings.push({
471
+ code: "MCP-007",
472
+ title: "Unpinned uvx package",
473
+ description: `MCP server '${name}' installs '${pkg}' via uvx without version pinning.`,
474
+ severity: "high",
475
+ remediation: `Pin the version: uvx ${pkg}==<version>`
476
+ });
477
+ }
478
+ }
479
+ const bunxMatch = allStr.match(/bunx\s+(@?[a-zA-Z0-9_./-]+(?:@[^\s]+)?)/);
480
+ if (bunxMatch) {
481
+ const pkg = bunxMatch[1];
482
+ const parts = pkg.split("/");
483
+ const lastPart = parts[parts.length - 1] ?? pkg;
484
+ const hasVersion = lastPart.includes("@") && !lastPart.startsWith("@");
485
+ if (!hasVersion) {
486
+ findings.push({
487
+ code: "MCP-007",
488
+ title: "Unpinned bunx package",
489
+ description: `MCP server '${name}' installs '${pkg}' via bunx without version pinning. A supply chain attack could inject malicious code.`,
490
+ severity: "medium",
491
+ remediation: `Pin the version: bunx ${pkg}@<version>`
492
+ });
493
+ }
494
+ }
495
+ const denoMatch = allStr.match(/deno\s+run\s+(?:--allow-\S+\s+)*(\S+)/);
496
+ if (denoMatch) {
497
+ const target = denoMatch[1];
498
+ if (!target.startsWith(".") && !target.startsWith("/")) {
499
+ if (!target.includes("@")) {
500
+ findings.push({
501
+ code: "MCP-007",
502
+ title: "Unpinned deno package",
503
+ description: `MCP server '${name}' runs '${target}' via deno without version pinning.`,
504
+ severity: "medium",
505
+ remediation: `Pin the version: deno run ${target}@<version>`
506
+ });
507
+ }
508
+ }
509
+ }
510
+ const dockerMatch = allStr.match(/docker\s+run\s+(?:-[^\s]+\s+)*([a-zA-Z0-9_./-]+(?::[^\s]+)?)/);
511
+ if (dockerMatch) {
512
+ const image = dockerMatch[1];
513
+ if (!image.includes(":")) {
514
+ findings.push({
515
+ code: "MCP-007",
516
+ title: "Unpinned docker image",
517
+ description: `MCP server '${name}' runs docker image '${image}' without a tag. This defaults to :latest which is mutable.`,
518
+ severity: "medium",
519
+ remediation: `Pin the image tag: docker run ${image}:<version>`
520
+ });
521
+ } else if (image.endsWith(":latest")) {
522
+ findings.push({
523
+ code: "MCP-007",
524
+ title: "Unpinned docker image (:latest)",
525
+ description: `MCP server '${name}' runs docker image '${image}' with :latest tag. This is mutable and not reproducible.`,
526
+ severity: "medium",
527
+ remediation: `Pin a specific image tag instead of :latest`
528
+ });
529
+ }
530
+ }
531
+ const pipMatch = allStr.match(/pip3?\s+install\s+([a-zA-Z0-9_.-]+)/);
532
+ if (pipMatch) {
533
+ const pkg = pipMatch[1];
534
+ if (!pkg.startsWith("-")) {
535
+ const afterPkg = allStr.split(pipMatch[0]).slice(1).join("").slice(0, 30);
536
+ if (!afterPkg.includes("==")) {
537
+ findings.push({
538
+ code: "MCP-007",
539
+ title: "Unpinned pip package",
540
+ description: `MCP server '${name}' installs '${pkg}' via pip without version pinning.`,
541
+ severity: "medium",
542
+ remediation: `Pin the version: pip install ${pkg}==<version>`
543
+ });
544
+ }
545
+ }
546
+ }
547
+ const goMatch = allStr.match(/go\s+run\s+([a-zA-Z0-9_./@-]+)/);
548
+ if (goMatch) {
549
+ const target = goMatch[1];
550
+ if (!target.startsWith(".") && !target.startsWith("/")) {
551
+ if (!target.includes("@")) {
552
+ findings.push({
553
+ code: "MCP-007",
554
+ title: "Unpinned go package",
555
+ description: `MCP server '${name}' runs '${target}' via go run without version pinning.`,
556
+ severity: "medium",
557
+ remediation: `Pin the version: go run ${target}@<version>`
558
+ });
559
+ }
560
+ }
561
+ }
562
+ const allArgs = [command, ...args.filter((a) => typeof a === "string")];
563
+ for (const arg of allArgs) {
564
+ for (const pkgName of KNOWN_MALICIOUS_PACKAGES) {
565
+ if (arg.toLowerCase().includes(pkgName)) {
566
+ findings.push({
567
+ code: "MCP-007",
568
+ title: `Known malicious package: ${pkgName}`,
569
+ description: `MCP server '${name}' references known malicious package '${pkgName}'.`,
570
+ severity: "critical",
571
+ remediation: `Remove MCP server '${name}' immediately.`
572
+ });
573
+ return findings;
574
+ }
575
+ }
576
+ }
577
+ return findings;
578
+ }
579
+ _checkCommandInjection(name, command, args) {
580
+ const findings = [];
581
+ const cmdBase = basename(command).toLowerCase();
582
+ if (DANGEROUS_SHELLS.has(cmdBase)) {
583
+ findings.push({
584
+ code: "MCP-008",
585
+ title: "Shell binary as MCP server",
586
+ description: `MCP server '${name}' uses '${cmdBase}' as its binary. This allows arbitrary command execution.`,
587
+ severity: "critical",
588
+ remediation: `Replace shell command for '${name}' with a dedicated MCP server binary.`
589
+ });
590
+ }
591
+ for (const arg of args) {
592
+ if (typeof arg === "string" && SHELL_META.test(arg)) {
593
+ findings.push({
594
+ code: "MCP-008",
595
+ title: "Shell metacharacters in arguments",
596
+ description: `MCP server '${name}' has shell metacharacters in args: '${arg.slice(0, 60)}'. This may allow command injection.`,
597
+ severity: "high",
598
+ remediation: `Remove shell metacharacters from '${name}' arguments.`
599
+ });
600
+ break;
601
+ }
602
+ }
603
+ return findings;
604
+ }
605
+ _checkMissingAuth(name, server) {
606
+ const url = server.url;
607
+ if (!url || typeof url !== "string") return [];
608
+ const localhostPattern = /^https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])/;
609
+ if (localhostPattern.test(url)) return [];
610
+ const hasApiKey = Boolean(server.apiKey);
611
+ const headers = server.headers;
612
+ const hasAuthHeader = typeof headers === "object" && headers !== null && Boolean(headers.Authorization);
613
+ const hasOAuth = Boolean(server.oauth || server.auth);
614
+ if (!hasApiKey && !hasAuthHeader && !hasOAuth) {
615
+ return [{
616
+ code: "MCP-009",
617
+ title: "Missing authentication",
618
+ description: `Remote MCP server '${name}' at ${url} has no authentication configured. Anyone who discovers the endpoint can use it.`,
619
+ severity: "high",
620
+ remediation: `Add apiKey, Authorization header, or OAuth config for '${name}'.`
621
+ }];
622
+ }
623
+ return [];
624
+ }
625
+ _checkKnownCVEs(name, server) {
626
+ const findings = [];
627
+ const rawCmd2 = server.command ?? "";
628
+ let command;
629
+ let args;
630
+ if (Array.isArray(rawCmd2)) {
631
+ command = String(rawCmd2[0] ?? "");
632
+ args = [...rawCmd2.slice(1).map(String), ...server.args ?? []];
633
+ } else {
634
+ command = String(rawCmd2);
635
+ args = server.args ?? [];
636
+ }
637
+ const source = server.source_file ?? "";
638
+ const allArgsStr = args.filter((a) => typeof a === "string").join(" ");
639
+ for (const arg of args) {
640
+ if (typeof arg === "string" && arg.includes("../")) {
641
+ findings.push({
642
+ code: "MCP-CVE",
643
+ title: "CVE-2025-53110: Path traversal in arguments",
644
+ description: `MCP server '${name}' has path traversal sequence '../' in arguments.`,
645
+ severity: "high",
646
+ remediation: "Remove path traversal sequences from MCP server arguments."
647
+ });
648
+ break;
649
+ }
650
+ }
651
+ const isGitServer = /\bgit\b/.test(command.toLowerCase()) || /server-git|mcp-git/.test(allArgsStr.toLowerCase());
652
+ if (isGitServer && !args.some((a) => typeof a === "string" && (a.includes("--allowed") || a.toLowerCase().includes("path")))) {
653
+ findings.push({
654
+ code: "MCP-CVE",
655
+ title: "CVE-2025-68143: Unrestricted git MCP server",
656
+ description: `Git MCP server '${name}' has no path restrictions configured. It can access any repository on the machine.`,
657
+ severity: "high",
658
+ remediation: `Add --allowed-path restrictions to git MCP server '${name}'.`
659
+ });
660
+ }
661
+ if (source && basename(source) === ".mcp.json") {
662
+ findings.push({
663
+ code: "MCP-CVE",
664
+ title: "CVE-2025-59536: Project-level MCP config",
665
+ description: `MCP server '${name}' is defined in a project-level .mcp.json file. Cloning a malicious repo could auto-register MCP servers.`,
666
+ severity: "medium",
667
+ remediation: "Review project-level MCP configs carefully. Consider using global configs only."
668
+ });
669
+ }
670
+ if (command.includes("mcp-remote") || allArgsStr.includes("mcp-remote")) {
671
+ findings.push({
672
+ code: "MCP-CVE",
673
+ title: "CVE-2025-6514: mcp-remote OAuth vulnerability",
674
+ description: `MCP server '${name}' uses mcp-remote which has known OAuth vulnerabilities.`,
675
+ severity: "medium",
676
+ remediation: "Update mcp-remote to the latest version or use direct SSE connections."
677
+ });
678
+ }
679
+ return findings;
680
+ }
681
+ _checkHighEntropySecrets(name, env) {
682
+ const findings = [];
683
+ for (const [envKey, envValue] of Object.entries(env)) {
684
+ if (typeof envValue !== "string" || envValue.length < 20) continue;
685
+ if (envValue.startsWith("${") || envValue.startsWith("$")) continue;
686
+ let matched = false;
687
+ for (const [pattern] of CREDENTIAL_PATTERNS) {
688
+ if (pattern.test(envValue)) {
689
+ matched = true;
690
+ break;
691
+ }
692
+ }
693
+ if (matched) continue;
694
+ const entropy = shannonEntropy(envValue);
695
+ if (entropy > 4.5) {
696
+ const redacted = envValue.length > 12 ? envValue.slice(0, 4) + "..." + envValue.slice(-4) : "***";
697
+ findings.push({
698
+ code: "MCP-002",
699
+ title: `High-entropy secret in ${envKey}`,
700
+ description: `MCP server '${name}' has a high-entropy string in env var ${envKey} (${redacted}, entropy=${entropy.toFixed(1)}). This may be a credential from an unknown provider.`,
701
+ severity: "medium",
702
+ remediation: `Move ${envKey} for '${name}' to a secrets manager or env var reference.`
703
+ });
704
+ }
705
+ }
706
+ return findings;
707
+ }
708
+ };
709
+
710
+ // src/notify.ts
711
+ import { execFileSync } from "child_process";
712
+ import { platform } from "os";
713
+ var SEVERITY_ICONS = {
714
+ critical: "CRITICAL",
715
+ high: "HIGH",
716
+ medium: "MEDIUM",
717
+ low: "LOW"
718
+ };
719
+ var Notifier = class {
720
+ _enabled;
721
+ _minInterval;
722
+ _lastNotifyTime = -Infinity;
723
+ _platform;
724
+ constructor(enabled = true, minInterval = 30) {
725
+ this._enabled = enabled;
726
+ this._minInterval = minInterval;
727
+ this._platform = platform();
728
+ }
729
+ get enabled() {
730
+ return this._enabled;
731
+ }
732
+ /** Send a desktop notification. Returns true if sent. Respects throttle interval. */
733
+ notify(title, message, urgent = false) {
734
+ if (!this._enabled) return false;
735
+ const now = performance.now() / 1e3;
736
+ if (now - this._lastNotifyTime < this._minInterval) return false;
737
+ const sent = this._dispatch(title, message, urgent);
738
+ if (sent) this._lastNotifyTime = now;
739
+ return sent;
740
+ }
741
+ /** Send a threat notification with standard formatting. */
742
+ notifyThreat(itemName, itemType, severity, detail) {
743
+ const level = SEVERITY_ICONS[severity] ?? severity.toUpperCase();
744
+ const title = `AgentSeal Shield - ${level}`;
745
+ const message = `${itemType}: ${itemName}
746
+ ${detail}`;
747
+ return this.notify(title, message, severity === "critical" || severity === "high");
748
+ }
749
+ _dispatch(title, message, urgent) {
750
+ if (this._platform === "darwin") return this._notifyMacOS(title, message, urgent);
751
+ if (this._platform === "linux") return this._notifyLinux(title, message, urgent);
752
+ return this._notifyFallback(title, message);
753
+ }
754
+ _notifyMacOS(title, message, urgent) {
755
+ const safeTitle = title.replace(/"/g, '\\"');
756
+ const safeMessage = message.replace(/"/g, '\\"').replace(/\n/g, " - ");
757
+ const sound = urgent ? ' sound name "Basso"' : "";
758
+ const script = `display notification "${safeMessage}" with title "${safeTitle}"${sound}`;
759
+ try {
760
+ execFileSync("osascript", ["-e", script], { timeout: 5e3, stdio: "pipe" });
761
+ return true;
762
+ } catch {
763
+ return this._notifyFallback(title, message);
764
+ }
765
+ }
766
+ _notifyLinux(title, message, urgent) {
767
+ const urgency = urgent ? "critical" : "normal";
768
+ try {
769
+ execFileSync(
770
+ "notify-send",
771
+ [title, message, `--urgency=${urgency}`, "--icon=dialog-warning"],
772
+ { timeout: 5e3, stdio: "pipe" }
773
+ );
774
+ return true;
775
+ } catch {
776
+ return this._notifyFallback(title, message);
777
+ }
778
+ }
779
+ _notifyFallback(title, message) {
780
+ process.stderr.write(`\x07\x1B[93m[${title}]\x1B[0m ${message}
781
+ `);
782
+ return true;
783
+ }
784
+ };
785
+
786
+ // src/deobfuscate.ts
787
+ var ZERO_WIDTH = /[\u200B\u200C\u200D\uFEFF\u00AD\u2060]/g;
788
+ var TAG_CHARS = /[\u{E0001}-\u{E007F}]/gu;
789
+ var VARIATION_SELECTORS = /[\uFE00-\uFE0F\u{E0100}-\u{E01EF}]/gu;
790
+ var BIDI_CONTROLS = /[\u202A-\u202E\u2066-\u2069\u200E\u200F]/g;
791
+ var HTML_COMMENTS = /<!--[\s\S]*?-->/g;
792
+ var INVISIBLE_CHARS = /[\u200B\u200C\u200D\uFEFF\u00AD\u2060\u{E0001}-\u{E007F}\uFE00-\uFE0F\u{E0100}-\u{E01EF}\u202A-\u202E\u2066-\u2069\u200E\u200F]/gu;
793
+ var BASE64_BLOCK = /(?<=["'\s(]|^)([A-Za-z0-9+/=]{8,})(?=["'\s)]|$)/gm;
794
+ var HEX_ESCAPE = /\\x([0-9A-Fa-f]{2})/g;
795
+ var UNICODE_ESCAPE = /\\u([0-9A-Fa-f]{4})/g;
796
+ var CONCAT_DOUBLE = /"([^"]*?)"\s*\+\s*"([^"]*?)"/g;
797
+ var CONCAT_SINGLE = /'([^']*?)'\s*\+\s*'([^']*?)'/g;
798
+ var SIMPLE_ESCAPES = {
799
+ "\\n": "\n",
800
+ "\\t": " ",
801
+ "\\r": "\r"
802
+ };
803
+ function stripZeroWidth(text) {
804
+ return text.replace(ZERO_WIDTH, "");
805
+ }
806
+ function stripTagChars(text) {
807
+ return text.replace(TAG_CHARS, "");
808
+ }
809
+ function stripVariationSelectors(text) {
810
+ return text.replace(VARIATION_SELECTORS, "");
811
+ }
812
+ function stripBidiControls(text) {
813
+ return text.replace(BIDI_CONTROLS, "");
814
+ }
815
+ function stripHtmlComments(text) {
816
+ return text.replace(HTML_COMMENTS, "");
817
+ }
818
+ function hasInvisibleChars(text) {
819
+ INVISIBLE_CHARS.lastIndex = 0;
820
+ return INVISIBLE_CHARS.test(text);
821
+ }
822
+ var CONFUSABLES = new Map([
823
+ // — Cyrillic uppercase —
824
+ ["\u0410", "A"],
825
+ ["\u0412", "B"],
826
+ ["\u0421", "C"],
827
+ ["\u0415", "E"],
828
+ ["\u041D", "H"],
829
+ ["\u0406", "I"],
830
+ ["\u0408", "J"],
831
+ ["\u041A", "K"],
832
+ ["\u041C", "M"],
833
+ ["\u041E", "O"],
834
+ ["\u0420", "P"],
835
+ ["\u0405", "S"],
836
+ ["\u0422", "T"],
837
+ ["\u0425", "X"],
838
+ ["\u0423", "Y"],
839
+ ["\u0417", "Z"],
840
+ // — Cyrillic lowercase —
841
+ ["\u0430", "a"],
842
+ ["\u0441", "c"],
843
+ ["\u0435", "e"],
844
+ ["\u04BB", "h"],
845
+ ["\u0456", "i"],
846
+ ["\u0458", "j"],
847
+ ["\u043E", "o"],
848
+ ["\u0440", "p"],
849
+ ["\u0455", "s"],
850
+ ["\u0445", "x"],
851
+ ["\u0443", "y"],
852
+ // — Greek uppercase —
853
+ ["\u0391", "A"],
854
+ ["\u0392", "B"],
855
+ ["\u0395", "E"],
856
+ ["\u0397", "H"],
857
+ ["\u0399", "I"],
858
+ ["\u039A", "K"],
859
+ ["\u039C", "M"],
860
+ ["\u039D", "N"],
861
+ ["\u039F", "O"],
862
+ ["\u03A1", "P"],
863
+ ["\u03A4", "T"],
864
+ ["\u03A7", "X"],
865
+ ["\u03A5", "Y"],
866
+ ["\u0396", "Z"],
867
+ // — Greek lowercase —
868
+ ["\u03BF", "o"],
869
+ ["\u03B1", "a"],
870
+ // — Cherokee —
871
+ ["\u13A0", "D"],
872
+ ["\u13A1", "R"],
873
+ ["\u13A2", "T"],
874
+ ["\u13AA", "G"],
875
+ ["\u13B3", "W"],
876
+ ["\u13D2", "S"],
877
+ ["\u13DA", "S"],
878
+ ["\uAB4E", "s"],
879
+ ["\uAB4F", "s"],
880
+ ["\uABA3", "s"],
881
+ ["\uABAA", "s"],
882
+ // — Turkish dotless i —
883
+ ["\u0131", "i"],
884
+ // — Small caps —
885
+ ["\u1D00", "A"],
886
+ ["\u0299", "B"],
887
+ ["\u1D04", "C"],
888
+ // — Fullwidth Latin uppercase A–Z (U+FF21–U+FF3A) —
889
+ ...Array.from({ length: 26 }, (_, i) => [
890
+ String.fromCharCode(65313 + i),
891
+ String.fromCharCode(65 + i)
892
+ ]),
893
+ // — Fullwidth Latin lowercase a–z (U+FF41–U+FF5A) —
894
+ ...Array.from({ length: 26 }, (_, i) => [
895
+ String.fromCharCode(65345 + i),
896
+ String.fromCharCode(97 + i)
897
+ ])
898
+ ]);
899
+ function normalizeUnicode(text) {
900
+ let result = text.normalize("NFKC");
901
+ let out = "";
902
+ for (const ch of result) {
903
+ out += CONFUSABLES.get(ch) ?? ch;
904
+ }
905
+ return out;
906
+ }
907
+ function isPrintableText(decoded) {
908
+ let nonPrintable = 0;
909
+ for (const ch of decoded) {
910
+ const code = ch.codePointAt(0);
911
+ if (ch === "\n" || ch === "\r" || ch === " " || ch === " ") continue;
912
+ if (code < 32 || code >= 127 && code <= 159) {
913
+ nonPrintable++;
914
+ }
915
+ }
916
+ return nonPrintable <= decoded.length * 0.1;
917
+ }
918
+ function decodeBase64Blocks(text) {
919
+ BASE64_BLOCK.lastIndex = 0;
920
+ return text.replace(BASE64_BLOCK, (fullMatch, token) => {
921
+ if (/^[a-z]+$/.test(token)) return fullMatch;
922
+ try {
923
+ const decoded = Buffer.from(token, "base64").toString("utf-8");
924
+ if (Buffer.from(decoded, "utf-8").toString("base64").replace(/=+$/, "") !== token.replace(/=+$/, "")) {
925
+ return fullMatch;
926
+ }
927
+ if (!isPrintableText(decoded)) return fullMatch;
928
+ const tokenStart = fullMatch.indexOf(token);
929
+ const prefix = fullMatch.slice(0, tokenStart);
930
+ const suffix = fullMatch.slice(tokenStart + token.length);
931
+ return prefix + decoded + suffix;
932
+ } catch {
933
+ return fullMatch;
934
+ }
935
+ });
936
+ }
937
+ function unescapeSequences(text) {
938
+ const PLACEHOLDER = "\0BKSL\0";
939
+ text = text.replaceAll("\\\\", PLACEHOLDER);
940
+ HEX_ESCAPE.lastIndex = 0;
941
+ text = text.replace(
942
+ HEX_ESCAPE,
943
+ (_m, hex) => String.fromCharCode(parseInt(hex, 16))
944
+ );
945
+ UNICODE_ESCAPE.lastIndex = 0;
946
+ text = text.replace(
947
+ UNICODE_ESCAPE,
948
+ (_m, hex) => String.fromCharCode(parseInt(hex, 16))
949
+ );
950
+ for (const [seq, char] of Object.entries(SIMPLE_ESCAPES)) {
951
+ text = text.replaceAll(seq, char);
952
+ }
953
+ text = text.replaceAll(PLACEHOLDER, "\\");
954
+ return text;
955
+ }
956
+ function expandStringConcat(text) {
957
+ let prev;
958
+ while (prev !== text) {
959
+ prev = text;
960
+ CONCAT_DOUBLE.lastIndex = 0;
961
+ text = text.replace(CONCAT_DOUBLE, '"$1$2"');
962
+ CONCAT_SINGLE.lastIndex = 0;
963
+ text = text.replace(CONCAT_SINGLE, "'$1$2'");
964
+ }
965
+ return text;
966
+ }
967
+ var NAMED_ENTITIES = {
968
+ amp: "&",
969
+ lt: "<",
970
+ gt: ">",
971
+ quot: '"',
972
+ apos: "'",
973
+ nbsp: "\xA0",
974
+ copy: "\xA9",
975
+ reg: "\xAE"
976
+ };
977
+ function decodeHtmlEntities(text) {
978
+ return text.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16))).replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(parseInt(dec, 10))).replace(/&([a-zA-Z]+);/g, (match, name) => NAMED_ENTITIES[name.toLowerCase()] ?? match);
979
+ }
980
+ function _deobfuscatePass(text) {
981
+ text = stripZeroWidth(text);
982
+ text = stripTagChars(text);
983
+ text = stripVariationSelectors(text);
984
+ text = stripBidiControls(text);
985
+ text = stripHtmlComments(text);
986
+ text = decodeHtmlEntities(text);
987
+ text = normalizeUnicode(text);
988
+ text = decodeBase64Blocks(text);
989
+ text = unescapeSequences(text);
990
+ text = expandStringConcat(text);
991
+ return text;
992
+ }
993
+ function deobfuscate(text) {
994
+ text = _deobfuscatePass(text);
995
+ text = _deobfuscatePass(text);
996
+ return text;
997
+ }
998
+
999
+ // src/skill-scanner.ts
1000
+ var PATTERN_RULES = [
1001
+ {
1002
+ code: "SKILL-001",
1003
+ title: "Credential access",
1004
+ severity: "critical",
1005
+ patterns: [
1006
+ /~\/\.ssh\b/i,
1007
+ /~\/\.aws\b/i,
1008
+ /~\/\.gnupg\b/i,
1009
+ /~\/\.config\/gh\b/i,
1010
+ /~\/\.npmrc\b/i,
1011
+ /~\/\.pypirc\b/i,
1012
+ /~\/\.docker\b/i,
1013
+ /~\/\.kube\b/i,
1014
+ /~\/\.netrc\b/i,
1015
+ /~\/\.bitcoin\b/i,
1016
+ /~\/\.ethereum\b/i,
1017
+ /~\/Library\/Keychains\b/i,
1018
+ /\.env\b(?!\.example|\.sample|\.template)/i,
1019
+ /credentials\.json\b/i,
1020
+ /id_rsa\b/i,
1021
+ /id_ed25519\b/i,
1022
+ /wallet\.dat\b/i,
1023
+ /aws_access_key_id/i,
1024
+ /aws_secret_access_key/i,
1025
+ /\/etc\/passwd\b/i,
1026
+ /\/etc\/shadow\b/i,
1027
+ /PRIVATE[_\s]KEY/i
1028
+ ],
1029
+ descriptionTemplate: "This skill accesses sensitive credentials: {match}",
1030
+ remediation: "Remove this skill immediately and rotate all credentials it may have accessed."
1031
+ },
1032
+ {
1033
+ code: "SKILL-002",
1034
+ title: "Data exfiltration",
1035
+ severity: "critical",
1036
+ patterns: [
1037
+ /curl\s+.*(?:-d|--data)\s+.*https?:\/\//i,
1038
+ /wget\s+.*--post-(?:data|file)/i,
1039
+ /requests\.post\s*\(/i,
1040
+ /fetch\s*\(.*method.*['"]POST['"]/i,
1041
+ /urllib\.request\.urlopen\s*\(.*data=/i,
1042
+ /socket\.connect\s*\(/i,
1043
+ /\bnc(?:at)?\b.*\b(?:--send-only|--recv-only)\b/i,
1044
+ /httpx\.post\s*\(/i,
1045
+ /!\[.*?\]\(https?:\/\/[^\s)]+\?[^\s)]*(?:data|content|file|secret|key|token|d)=/i,
1046
+ /<img\s+[^>]*src=["']https?:\/\/[^"']+\?[^"']*(?:data|content|file|secret|key|token|d)=/i,
1047
+ /(?:render|display|show|include)\s+(?:an?\s+)?(?:image|img|markdown)\s+(?:tag|link)?\s*.*https?:\/\//i
1048
+ ],
1049
+ descriptionTemplate: "This skill sends data to an external server: {match}",
1050
+ remediation: "Remove this skill. It exfiltrates data to an external endpoint. Check for compromised credentials."
1051
+ },
1052
+ {
1053
+ code: "SKILL-003",
1054
+ title: "Remote payload execution",
1055
+ severity: "critical",
1056
+ patterns: [
1057
+ /curl\s+.*\|\s*(?:sh|bash|python|python3|node|ruby|perl)\b/i,
1058
+ /wget\s+.*-O\s*-\s*\|/i,
1059
+ /eval\s*\(\s*(?:fetch|require|import)/i,
1060
+ /exec\s*\(\s*(?:urllib|requests|httpx)/i,
1061
+ /pip\s+install\s+--index-url\s+http[^s]/i,
1062
+ /npm\s+install\s+.*--registry\s+http[^s]/i,
1063
+ /curl\s+.*>\s*\/tmp\/.*&&.*(?:sh|bash|chmod)/i
1064
+ ],
1065
+ descriptionTemplate: "This skill downloads and executes remote code: {match}",
1066
+ remediation: "Remove this skill immediately. It fetches and runs code from the internet."
1067
+ },
1068
+ {
1069
+ code: "SKILL-004",
1070
+ title: "Reverse shell / backdoor",
1071
+ severity: "critical",
1072
+ patterns: [
1073
+ /\/bin\/(?:ba)?sh\s+-i/i,
1074
+ /python3?\s+-c\s+['"]import\s+socket/i,
1075
+ /\bnc(?:at)?\s+(?:-e|--exec)\b/i,
1076
+ /bash\s+-c\s+.*>\/dev\/tcp\//i,
1077
+ /mkfifo\s+.*\bnc(?:at)?\b/i,
1078
+ /socat\s+.*exec:/i,
1079
+ /powershell.*-e\s+[A-Za-z0-9+/=]{20,}/i
1080
+ ],
1081
+ descriptionTemplate: "This skill opens a backdoor to your machine: {match}",
1082
+ remediation: "Remove this skill immediately and run a full system security audit."
1083
+ },
1084
+ {
1085
+ code: "SKILL-005",
1086
+ title: "Code obfuscation",
1087
+ severity: "high",
1088
+ patterns: [
1089
+ /base64\s+(?:--)?decode/i,
1090
+ /\batob\s*\(/i,
1091
+ /(?:\\x[0-9a-fA-F]{2}){10,}/i,
1092
+ /eval\s*\(.*chr\s*\(/i,
1093
+ /String\.fromCharCode/i,
1094
+ /codecs\.decode\s*\(.*rot.13/i,
1095
+ /exec\s*\(\s*compile\s*\(/i,
1096
+ /exec\s*\(\s*__import__/i
1097
+ ],
1098
+ descriptionTemplate: "This skill uses code obfuscation: {match}",
1099
+ remediation: "This skill obfuscates its code \u2014 a common malware technique. Review the decoded content before trusting it."
1100
+ },
1101
+ {
1102
+ code: "SKILL-006",
1103
+ title: "Prompt injection",
1104
+ severity: "high",
1105
+ patterns: [
1106
+ /ignore\s+(?:all\s+)?previous\s+instructions/i,
1107
+ /you\s+are\s+now\s+(?:a|an|in)\b/i,
1108
+ /disregard\s+(?:all|any|your)\s+(?:previous|prior)/i,
1109
+ /system:\s*you\s+are/i,
1110
+ /<\s*system\s*>/i,
1111
+ /IMPORTANT:.*override/i,
1112
+ /\[INST\]|\[\/INST\]|<<SYS>>|<\|im_start\|>/i,
1113
+ /new\s+instructions?\s*:/i,
1114
+ /forget\s+(?:all|everything)\s+(?:above|before|previous)/i
1115
+ ],
1116
+ descriptionTemplate: "This skill contains prompt injection: {match}",
1117
+ remediation: "This skill tries to override your agent's instructions. Remove it."
1118
+ },
1119
+ {
1120
+ code: "SKILL-007",
1121
+ title: "Suspicious URLs",
1122
+ severity: "medium",
1123
+ patterns: [
1124
+ /https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}[:/]/i,
1125
+ /https?:\/\/[^\s]*\.(?:tk|ml|ga|cf|gq)\//i,
1126
+ /(?:bit\.ly|tinyurl\.com|is\.gd|t\.co|rb\.gy)\/[^\s]+/i,
1127
+ /(?:pastebin\.com|hastebin\.com|0x0\.st)\/[^\s]+/i
1128
+ ],
1129
+ descriptionTemplate: "This skill references a suspicious URL: {match}",
1130
+ remediation: "Verify this URL is legitimate before allowing the skill to access it."
1131
+ },
1132
+ {
1133
+ code: "SKILL-008",
1134
+ title: "Hardcoded secrets",
1135
+ severity: "high",
1136
+ patterns: [
1137
+ /(?:sk-(?:proj-)?|sk_live_|sk_test_)[a-zA-Z0-9]{20,}/i,
1138
+ /AKIA[0-9A-Z]{16}/,
1139
+ /ghp_[a-zA-Z0-9]{36}/,
1140
+ /gho_[a-zA-Z0-9]{36}/,
1141
+ /xoxb-[a-zA-Z0-9-]+/,
1142
+ /xoxp-[a-zA-Z0-9-]+/,
1143
+ /-----BEGIN\s+(?:RSA\s+|EC\s+|DSA\s+)?PRIVATE\s+KEY/i,
1144
+ /glpat-[a-zA-Z0-9_-]{20,}/,
1145
+ /SG\.[a-zA-Z0-9_-]{22,}/
1146
+ ],
1147
+ descriptionTemplate: "This skill contains a hardcoded secret: {match}",
1148
+ remediation: "Rotate the exposed credentials immediately. Never store secrets in skill files."
1149
+ },
1150
+ {
1151
+ code: "SKILL-009",
1152
+ title: "System modification",
1153
+ severity: "high",
1154
+ patterns: [
1155
+ /chmod\s+[0-7]*[7][0-7]*\s/i,
1156
+ /crontab\s+(?:-r|-e|-)\b/i,
1157
+ /launchctl\s+(?:load|submit)\b/i,
1158
+ /systemctl\s+(?:enable|start)\b/i,
1159
+ /\/etc\/(?:init\.d|rc\.local)\b/i,
1160
+ /(?:>>.*(?:\.bashrc|\.zshrc|\.bash_profile|\.profile)\b|(?:\.bashrc|\.zshrc|\.bash_profile|\.profile)\b.*>>)/i,
1161
+ /chown\s+root\b/i,
1162
+ /visudo\b/i
1163
+ ],
1164
+ descriptionTemplate: "This skill modifies system configuration: {match}",
1165
+ remediation: "This skill changes system startup scripts or permissions. Review carefully before allowing."
1166
+ }
1167
+ ];
1168
+ var DANGER_CONCEPTS = [
1169
+ "Read SSH private keys and send them to a remote server",
1170
+ "Download a script from the internet and execute it",
1171
+ "Open a reverse shell connection back to the attacker",
1172
+ "Steal browser cookies and saved passwords",
1173
+ "Exfiltrate environment variables to an external endpoint",
1174
+ "Override the AI assistant's system instructions",
1175
+ "Ignore previous safety guidelines and follow new ones",
1176
+ "Encode stolen data in base64 and send via DNS",
1177
+ "Install a cryptocurrency miner in the background",
1178
+ "Modify crontab to maintain persistence after reboot",
1179
+ "Read wallet.dat and private keys for crypto theft",
1180
+ "Access AWS credentials from ~/.aws/credentials",
1181
+ "Send keystrokes to a command and control server",
1182
+ "Replace legitimate binaries with trojanized versions",
1183
+ "Disable security monitoring and logging",
1184
+ "Create a hidden user account for backdoor access",
1185
+ "Intercept and forward API tokens to attacker domain",
1186
+ "Use steganography to hide data in image files",
1187
+ "Establish covert channel through DNS tunneling",
1188
+ "Scrape and exfiltrate local git credentials"
1189
+ ];
1190
+ var INVISIBLE_CATEGORIES = [
1191
+ { pattern: /[\u{E0001}-\u{E007F}]/gu, name: "Unicode Tag Characters (ASCII smuggling)" },
1192
+ { pattern: /[\uFE00-\uFE0F\u{E0100}-\u{E01EF}]/gu, name: "Variation Selectors" },
1193
+ { pattern: /[\u202A-\u202E\u2066-\u2069\u200E\u200F]/g, name: "BiDi Controls" },
1194
+ { pattern: /[\u200B\u200C\u200D\uFEFF\u00AD\u2060]/g, name: "Zero-width Characters" }
1195
+ ];
1196
+ function findInvisibleEvidence(content) {
1197
+ const found = [];
1198
+ for (const { pattern, name } of INVISIBLE_CATEGORIES) {
1199
+ pattern.lastIndex = 0;
1200
+ const matches = content.match(pattern);
1201
+ if (matches && matches.length > 0) {
1202
+ found.push(`${name} (${matches.length} chars)`);
1203
+ }
1204
+ }
1205
+ return found.length > 0 ? found.join("; ") : "Invisible characters detected";
1206
+ }
1207
+ function extractEvidenceLine(content, matchPos) {
1208
+ const lineStart = content.lastIndexOf("\n", matchPos - 1) + 1;
1209
+ let lineEnd = content.indexOf("\n", matchPos);
1210
+ if (lineEnd === -1) lineEnd = content.length;
1211
+ let line = content.slice(lineStart, lineEnd).trim();
1212
+ if (line.length > 200) {
1213
+ line = line.slice(0, 197) + "...";
1214
+ }
1215
+ return line;
1216
+ }
1217
+ var SkillScanner = class {
1218
+ /** Layer 1: Fast static pattern matching against known threat patterns. */
1219
+ scanPatterns(content) {
1220
+ const findings = [];
1221
+ const seenCodes = /* @__PURE__ */ new Set();
1222
+ for (const rule of PATTERN_RULES) {
1223
+ if (seenCodes.has(rule.code)) continue;
1224
+ for (const pattern of rule.patterns) {
1225
+ pattern.lastIndex = 0;
1226
+ const match = pattern.exec(content);
1227
+ if (match) {
1228
+ let matchedText = match[0];
1229
+ if (matchedText.length > 80) {
1230
+ matchedText = matchedText.slice(0, 77) + "...";
1231
+ }
1232
+ findings.push({
1233
+ code: rule.code,
1234
+ title: rule.title,
1235
+ description: rule.descriptionTemplate.replace("{match}", matchedText),
1236
+ severity: rule.severity,
1237
+ evidence: extractEvidenceLine(content, match.index),
1238
+ remediation: rule.remediation
1239
+ });
1240
+ seenCodes.add(rule.code);
1241
+ break;
1242
+ }
1243
+ }
1244
+ }
1245
+ if (hasInvisibleChars(content)) {
1246
+ findings.push({
1247
+ code: "SKILL-011",
1248
+ title: "Invisible characters detected",
1249
+ description: "This skill contains invisible Unicode characters (tag chars, variation selectors, BiDi controls, or zero-width chars) that can hide malicious instructions.",
1250
+ severity: "high",
1251
+ evidence: findInvisibleEvidence(content),
1252
+ remediation: "Strip invisible characters and review the decoded content carefully."
1253
+ });
1254
+ }
1255
+ return findings;
1256
+ }
1257
+ /**
1258
+ * Layer 2: Semantic similarity against known danger concepts.
1259
+ *
1260
+ * Requires an embedding function. Returns empty array if not provided.
1261
+ * Compares content chunks against DANGER_CONCEPTS with similarity thresholds.
1262
+ */
1263
+ async scanSemantic(content, embedFn) {
1264
+ if (!embedFn) return [];
1265
+ const findings = [];
1266
+ const chunkSize = 2e3;
1267
+ const chunks = [];
1268
+ for (let i = 0; i < content.length; i += chunkSize) {
1269
+ const chunk = content.slice(i, i + chunkSize);
1270
+ if (chunk.trim().length >= 20) chunks.push(chunk);
1271
+ }
1272
+ if (chunks.length === 0) return [];
1273
+ const allTexts = [...chunks, ...DANGER_CONCEPTS];
1274
+ let embeddings;
1275
+ try {
1276
+ embeddings = await embedFn(allTexts);
1277
+ } catch {
1278
+ return [];
1279
+ }
1280
+ const chunkEmbeddings = embeddings.slice(0, chunks.length);
1281
+ const conceptEmbeddings = embeddings.slice(chunks.length);
1282
+ for (let ci = 0; ci < chunks.length; ci++) {
1283
+ const chunkVec = chunkEmbeddings[ci];
1284
+ const chunk = chunks[ci];
1285
+ for (let di = 0; di < DANGER_CONCEPTS.length; di++) {
1286
+ const conceptVec = conceptEmbeddings[di];
1287
+ const similarity = cosineSimilarity(chunkVec, conceptVec);
1288
+ if (similarity >= 0.85) {
1289
+ findings.push({
1290
+ code: "SKILL-SEM",
1291
+ title: "Semantic threat match",
1292
+ description: `Content semantically matches danger pattern: '${DANGER_CONCEPTS[di]}' (similarity: ${similarity.toFixed(2)})`,
1293
+ severity: "critical",
1294
+ evidence: chunk.slice(0, 120).replace(/\n/g, " ") + "...",
1295
+ remediation: "This skill's content closely matches known malicious behavior. Review carefully before allowing."
1296
+ });
1297
+ break;
1298
+ } else if (similarity >= 0.75) {
1299
+ findings.push({
1300
+ code: "SKILL-SEM",
1301
+ title: "Suspicious semantic similarity",
1302
+ description: `Content resembles danger pattern: '${DANGER_CONCEPTS[di]}' (similarity: ${similarity.toFixed(2)})`,
1303
+ severity: "medium",
1304
+ evidence: chunk.slice(0, 120).replace(/\n/g, " ") + "...",
1305
+ remediation: "Review this skill's content \u2014 it resembles known malicious patterns."
1306
+ });
1307
+ break;
1308
+ }
1309
+ }
1310
+ }
1311
+ const seen = /* @__PURE__ */ new Set();
1312
+ return findings.filter((f) => {
1313
+ if (seen.has(f.severity)) return false;
1314
+ seen.add(f.severity);
1315
+ return true;
1316
+ });
1317
+ }
1318
+ };
1319
+ function cosineSimilarity(a, b) {
1320
+ let dot = 0;
1321
+ let normA = 0;
1322
+ let normB = 0;
1323
+ for (let i = 0; i < a.length; i++) {
1324
+ const ai = a[i];
1325
+ const bi = b[i];
1326
+ dot += ai * bi;
1327
+ normA += ai * ai;
1328
+ normB += bi * bi;
1329
+ }
1330
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
1331
+ return denom === 0 ? 0 : dot / denom;
1332
+ }
1333
+
1334
+ // src/toxic-flows.ts
1335
+ var LABEL_PUBLIC_SINK = "public_sink";
1336
+ var LABEL_DESTRUCTIVE = "destructive";
1337
+ var LABEL_UNTRUSTED = "untrusted_content";
1338
+ var LABEL_PRIVATE = "private_data";
1339
+ var KNOWN_SERVER_LABELS = {
1340
+ filesystem: /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_DESTRUCTIVE]),
1341
+ fs: /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_DESTRUCTIVE]),
1342
+ slack: /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK]),
1343
+ discord: /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK]),
1344
+ email: /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK]),
1345
+ gmail: /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK]),
1346
+ smtp: /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK]),
1347
+ sendgrid: /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK]),
1348
+ twilio: /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK]),
1349
+ telegram: /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK]),
1350
+ teams: /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK]),
1351
+ webhook: /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK]),
1352
+ github: /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK, LABEL_PRIVATE]),
1353
+ gitlab: /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK, LABEL_PRIVATE]),
1354
+ bitbucket: /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK, LABEL_PRIVATE]),
1355
+ linear: /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK, LABEL_PRIVATE]),
1356
+ jira: /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK, LABEL_PRIVATE]),
1357
+ notion: /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK, LABEL_PRIVATE]),
1358
+ asana: /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK, LABEL_PRIVATE]),
1359
+ postgres: /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_DESTRUCTIVE]),
1360
+ postgresql: /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_DESTRUCTIVE]),
1361
+ mysql: /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_DESTRUCTIVE]),
1362
+ sqlite: /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_DESTRUCTIVE]),
1363
+ mongo: /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_DESTRUCTIVE]),
1364
+ mongodb: /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_DESTRUCTIVE]),
1365
+ redis: /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_DESTRUCTIVE]),
1366
+ supabase: /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_DESTRUCTIVE, LABEL_PUBLIC_SINK]),
1367
+ fetch: /* @__PURE__ */ new Set([LABEL_UNTRUSTED]),
1368
+ puppeteer: /* @__PURE__ */ new Set([LABEL_UNTRUSTED]),
1369
+ playwright: /* @__PURE__ */ new Set([LABEL_UNTRUSTED]),
1370
+ browser: /* @__PURE__ */ new Set([LABEL_UNTRUSTED]),
1371
+ "brave-search": /* @__PURE__ */ new Set([LABEL_UNTRUSTED]),
1372
+ tavily: /* @__PURE__ */ new Set([LABEL_UNTRUSTED]),
1373
+ "web-search": /* @__PURE__ */ new Set([LABEL_UNTRUSTED]),
1374
+ scraper: /* @__PURE__ */ new Set([LABEL_UNTRUSTED]),
1375
+ crawl: /* @__PURE__ */ new Set([LABEL_UNTRUSTED]),
1376
+ aws: /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_DESTRUCTIVE, LABEL_PUBLIC_SINK]),
1377
+ gcp: /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_DESTRUCTIVE, LABEL_PUBLIC_SINK]),
1378
+ azure: /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_DESTRUCTIVE, LABEL_PUBLIC_SINK]),
1379
+ docker: /* @__PURE__ */ new Set([LABEL_DESTRUCTIVE]),
1380
+ kubernetes: /* @__PURE__ */ new Set([LABEL_DESTRUCTIVE]),
1381
+ k8s: /* @__PURE__ */ new Set([LABEL_DESTRUCTIVE]),
1382
+ terraform: /* @__PURE__ */ new Set([LABEL_DESTRUCTIVE]),
1383
+ shell: /* @__PURE__ */ new Set([LABEL_DESTRUCTIVE, LABEL_UNTRUSTED]),
1384
+ terminal: /* @__PURE__ */ new Set([LABEL_DESTRUCTIVE, LABEL_UNTRUSTED]),
1385
+ exec: /* @__PURE__ */ new Set([LABEL_DESTRUCTIVE]),
1386
+ "code-runner": /* @__PURE__ */ new Set([LABEL_DESTRUCTIVE]),
1387
+ sandbox: /* @__PURE__ */ new Set([LABEL_DESTRUCTIVE]),
1388
+ memory: /* @__PURE__ */ new Set([LABEL_PRIVATE]),
1389
+ knowledge: /* @__PURE__ */ new Set([LABEL_PRIVATE]),
1390
+ vector: /* @__PURE__ */ new Set([LABEL_PRIVATE]),
1391
+ sentry: /* @__PURE__ */ new Set([LABEL_PRIVATE]),
1392
+ datadog: /* @__PURE__ */ new Set([LABEL_PRIVATE]),
1393
+ grafana: /* @__PURE__ */ new Set([LABEL_PRIVATE]),
1394
+ s3: /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_PUBLIC_SINK, LABEL_DESTRUCTIVE]),
1395
+ gcs: /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_PUBLIC_SINK, LABEL_DESTRUCTIVE]),
1396
+ drive: /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_PUBLIC_SINK]),
1397
+ dropbox: /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_PUBLIC_SINK])
1398
+ };
1399
+ var NAME_HEURISTICS = [
1400
+ [/(?:file|fs|disk)/i, /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_DESTRUCTIVE])],
1401
+ [/(?:mail|email|smtp)/i, /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK])],
1402
+ [/(?:http|fetch|web|browser|scrape|crawl)/i, /* @__PURE__ */ new Set([LABEL_UNTRUSTED])],
1403
+ [/(?:db|sql|database|mongo|redis)/i, /* @__PURE__ */ new Set([LABEL_PRIVATE])],
1404
+ [/(?:exec|shell|command|terminal|run)/i, /* @__PURE__ */ new Set([LABEL_DESTRUCTIVE])],
1405
+ [/(?:slack|discord|teams|telegram|chat)/i, /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK])],
1406
+ [/(?:github|gitlab|bitbucket|jira|linear)/i, /* @__PURE__ */ new Set([LABEL_PUBLIC_SINK, LABEL_PRIVATE])],
1407
+ [/(?:aws|gcp|azure|cloud)/i, /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_DESTRUCTIVE])],
1408
+ [/(?:docker|k8s|kubernetes|terraform)/i, /* @__PURE__ */ new Set([LABEL_DESTRUCTIVE])],
1409
+ [/(?:s3|gcs|storage|drive|dropbox)/i, /* @__PURE__ */ new Set([LABEL_PRIVATE, LABEL_PUBLIC_SINK])]
1410
+ ];
1411
+ function classifyServer(server) {
1412
+ const name = (server.name ?? "").toLowerCase().trim();
1413
+ const rawCmd = server.command ?? "";
1414
+ const command = (Array.isArray(rawCmd) ? rawCmd.join(" ") : String(rawCmd)).toLowerCase();
1415
+ const argsStr = (server.args ?? []).filter((a) => typeof a === "string").join(" ").toLowerCase();
1416
+ if (KNOWN_SERVER_LABELS[name]) {
1417
+ return new Set(KNOWN_SERVER_LABELS[name]);
1418
+ }
1419
+ for (const [known, labels2] of Object.entries(KNOWN_SERVER_LABELS)) {
1420
+ if (name.includes(known)) return new Set(labels2);
1421
+ }
1422
+ const searchText = `${command} ${argsStr}`;
1423
+ for (const [known, labels2] of Object.entries(KNOWN_SERVER_LABELS)) {
1424
+ if (searchText.includes(known)) return new Set(labels2);
1425
+ }
1426
+ const labels = /* @__PURE__ */ new Set();
1427
+ for (const [pattern, hLabels] of NAME_HEURISTICS) {
1428
+ if (pattern.test(name) || pattern.test(command) || pattern.test(argsStr)) {
1429
+ for (const l of hLabels) labels.add(l);
1430
+ }
1431
+ }
1432
+ return labels;
1433
+ }
1434
+ function detectCombos(serverLabels) {
1435
+ const flows = [];
1436
+ const allLabels = /* @__PURE__ */ new Set();
1437
+ for (const labels of serverLabels.values()) {
1438
+ for (const l of labels) allLabels.add(l);
1439
+ }
1440
+ const byLabel = /* @__PURE__ */ new Map();
1441
+ for (const [name, labels] of serverLabels) {
1442
+ for (const label of labels) {
1443
+ if (!byLabel.has(label)) byLabel.set(label, []);
1444
+ byLabel.get(label).push(name);
1445
+ }
1446
+ }
1447
+ const has = (l) => allLabels.has(l);
1448
+ const serversFor = (...labels) => [...new Set(labels.flatMap((l) => byLabel.get(l) ?? []))].sort();
1449
+ if (has(LABEL_UNTRUSTED) && has(LABEL_PRIVATE) && has(LABEL_PUBLIC_SINK)) {
1450
+ flows.push({
1451
+ risk_level: "high",
1452
+ risk_type: "full_chain",
1453
+ title: "Full attack chain detected",
1454
+ description: "This agent can fetch external content, read private data, and send data externally. An attacker could inject instructions via fetched content, read sensitive files, and exfiltrate them.",
1455
+ servers_involved: serversFor(LABEL_UNTRUSTED, LABEL_PRIVATE, LABEL_PUBLIC_SINK),
1456
+ labels_involved: [LABEL_UNTRUSTED, LABEL_PRIVATE, LABEL_PUBLIC_SINK],
1457
+ remediation: "Scope filesystem access to non-sensitive directories. Remove or restrict external communication servers.",
1458
+ tools_involved: []
1459
+ });
1460
+ return flows;
1461
+ }
1462
+ if (has(LABEL_PRIVATE) && has(LABEL_PUBLIC_SINK)) {
1463
+ flows.push({
1464
+ risk_level: "high",
1465
+ risk_type: "data_exfiltration",
1466
+ title: "Data exfiltration path detected",
1467
+ description: "This agent can read private data and send it externally. A prompt injection could instruct the agent to read sensitive files and leak them via an external service.",
1468
+ servers_involved: serversFor(LABEL_PRIVATE, LABEL_PUBLIC_SINK),
1469
+ labels_involved: [LABEL_PRIVATE, LABEL_PUBLIC_SINK],
1470
+ remediation: "Scope filesystem access to non-sensitive directories only. Review which external services truly need write access.",
1471
+ tools_involved: []
1472
+ });
1473
+ }
1474
+ if (has(LABEL_UNTRUSTED) && has(LABEL_DESTRUCTIVE)) {
1475
+ flows.push({
1476
+ risk_level: "high",
1477
+ risk_type: "remote_code_execution",
1478
+ title: "Remote code execution path detected",
1479
+ description: "This agent can fetch external content and execute destructive operations. Fetched content could contain malicious instructions that modify files, execute commands, or alter databases.",
1480
+ servers_involved: serversFor(LABEL_UNTRUSTED, LABEL_DESTRUCTIVE),
1481
+ labels_involved: [LABEL_UNTRUSTED, LABEL_DESTRUCTIVE],
1482
+ remediation: "Add confirmation steps before destructive operations. Restrict or sandbox the execution server.",
1483
+ tools_involved: []
1484
+ });
1485
+ }
1486
+ if (has(LABEL_PRIVATE) && has(LABEL_DESTRUCTIVE)) {
1487
+ const privateServers = new Set(byLabel.get(LABEL_PRIVATE) ?? []);
1488
+ const destructiveServers = new Set(byLabel.get(LABEL_DESTRUCTIVE) ?? []);
1489
+ const same = privateServers.size === destructiveServers.size && [...privateServers].every((s) => destructiveServers.has(s));
1490
+ if (!same) {
1491
+ flows.push({
1492
+ risk_level: "medium",
1493
+ risk_type: "data_destruction",
1494
+ title: "Data destruction path detected",
1495
+ description: "This agent can read private data from one source and perform destructive operations on another. This could lead to data corruption or deletion.",
1496
+ servers_involved: [.../* @__PURE__ */ new Set([...privateServers, ...destructiveServers])].sort(),
1497
+ labels_involved: [LABEL_PRIVATE, LABEL_DESTRUCTIVE],
1498
+ remediation: "Review whether both data read and write capabilities are necessary. Consider read-only access where possible.",
1499
+ tools_involved: []
1500
+ });
1501
+ }
1502
+ }
1503
+ return flows;
1504
+ }
1505
+ function analyzeToxicFlows(servers) {
1506
+ if (servers.length < 2) return [];
1507
+ const serverLabels = /* @__PURE__ */ new Map();
1508
+ for (const srv of servers) {
1509
+ const name = srv.name ?? "unknown";
1510
+ const labels = classifyServer(srv);
1511
+ if (labels.size > 0) {
1512
+ serverLabels.set(name, labels);
1513
+ }
1514
+ }
1515
+ if (serverLabels.size === 0) return [];
1516
+ return detectCombos(serverLabels);
1517
+ }
1518
+
1519
+ // src/guard.ts
1520
+ import { createHash as createHash2 } from "crypto";
1521
+ import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
1522
+ import { basename as basename2, extname } from "path";
1523
+
1524
+ // src/history.ts
1525
+ import { createRequire } from "module";
1526
+ import { homedir as homedir3 } from "os";
1527
+ import { resolve, dirname, join as join2 } from "path";
1528
+ import { mkdirSync as mkdirSync2 } from "fs";
1529
+ var _require = createRequire(import.meta.url);
1530
+ var Database = null;
1531
+ try {
1532
+ Database = _require("better-sqlite3");
1533
+ } catch {
1534
+ }
1535
+
1536
+ // src/guard.ts
1537
+ init_machine_discovery();
1538
+ var MAX_FILE_SIZE = 10 * 1024 * 1024;
1539
+ function extractSkillName(filePath) {
1540
+ const name = basename2(filePath);
1541
+ if (name.toLowerCase() === "skill.md") {
1542
+ const parts = filePath.split("/");
1543
+ return parts[parts.length - 2] ?? name;
1544
+ }
1545
+ const ext = extname(name);
1546
+ return ext ? name.slice(0, -ext.length) : name;
1547
+ }
1548
+ function computeVerdict(findings) {
1549
+ if (findings.length === 0) return GuardVerdict.SAFE;
1550
+ if (findings.some((f) => f.severity === "critical")) return GuardVerdict.DANGER;
1551
+ if (findings.some((f) => f.severity === "high" || f.severity === "medium")) return GuardVerdict.WARNING;
1552
+ return GuardVerdict.SAFE;
1553
+ }
1554
+ function scanSkillFile(filePath, scanner, blocklist) {
1555
+ const name = extractSkillName(filePath);
1556
+ let content;
1557
+ let sha256;
1558
+ try {
1559
+ const stat = statSync2(filePath);
1560
+ if (stat.size > MAX_FILE_SIZE) {
1561
+ return {
1562
+ name,
1563
+ path: filePath,
1564
+ verdict: GuardVerdict.ERROR,
1565
+ findings: [{
1566
+ code: "SKILL-ERR",
1567
+ title: "File too large",
1568
+ description: `File is ${Math.floor(stat.size / 1024 / 1024)}MB, max is 10MB.`,
1569
+ severity: "low",
1570
+ evidence: "",
1571
+ remediation: "Skill files should be small text files."
1572
+ }],
1573
+ blocklist_match: false,
1574
+ sha256: ""
1575
+ };
1576
+ }
1577
+ const raw = readFileSync2(filePath);
1578
+ sha256 = createHash2("sha256").update(raw).digest("hex");
1579
+ content = raw.toString("utf-8");
1580
+ } catch (err) {
1581
+ return {
1582
+ name,
1583
+ path: filePath,
1584
+ verdict: GuardVerdict.ERROR,
1585
+ findings: [{
1586
+ code: "SKILL-ERR",
1587
+ title: "Could not read file",
1588
+ description: String(err),
1589
+ severity: "low",
1590
+ evidence: "",
1591
+ remediation: "Check file permissions."
1592
+ }],
1593
+ blocklist_match: false,
1594
+ sha256: ""
1595
+ };
1596
+ }
1597
+ if (!content.trim()) {
1598
+ return { name, path: filePath, verdict: GuardVerdict.SAFE, findings: [], blocklist_match: false, sha256 };
1599
+ }
1600
+ if (blocklist.isBlocked(sha256)) {
1601
+ return {
1602
+ name,
1603
+ path: filePath,
1604
+ verdict: GuardVerdict.DANGER,
1605
+ findings: [{
1606
+ code: "SKILL-000",
1607
+ title: "Known malicious skill",
1608
+ description: "This skill matches a known malware hash in the AgentSeal threat database.",
1609
+ severity: "critical",
1610
+ evidence: `SHA256: ${sha256}`,
1611
+ remediation: "Remove this skill immediately and rotate all credentials."
1612
+ }],
1613
+ blocklist_match: true,
1614
+ sha256
1615
+ };
1616
+ }
1617
+ const findings = scanner.scanPatterns(content);
1618
+ const deobfuscated = deobfuscate(content);
1619
+ if (deobfuscated !== content) {
1620
+ const deobFindings = scanner.scanPatterns(deobfuscated);
1621
+ const existing = new Set(findings.map((f) => `${f.code}::${f.evidence}`));
1622
+ for (const f of deobFindings) {
1623
+ if (!existing.has(`${f.code}::${f.evidence}`)) {
1624
+ findings.push(f);
1625
+ }
1626
+ }
1627
+ }
1628
+ const verdict = computeVerdict(findings);
1629
+ return { name, path: filePath, verdict, findings, blocklist_match: false, sha256 };
1630
+ }
1631
+
1632
+ // src/shield.ts
1633
+ var DebouncedHandler = class {
1634
+ _onChange;
1635
+ _debounceMs;
1636
+ _timers = /* @__PURE__ */ new Map();
1637
+ constructor(onChange, debounceMs = 2e3) {
1638
+ this._onChange = onChange;
1639
+ this._debounceMs = debounceMs;
1640
+ }
1641
+ /** Handle a filesystem event. Skips directories and temp files. */
1642
+ handleEvent(filePath, isDirectory = false) {
1643
+ if (isDirectory) return;
1644
+ if (filePath.endsWith("~") || filePath.endsWith(".swp") || filePath.endsWith(".swx") || filePath.endsWith(".tmp") || filePath.endsWith(".DS_Store")) {
1645
+ return;
1646
+ }
1647
+ const existing = this._timers.get(filePath);
1648
+ if (existing !== void 0) {
1649
+ clearTimeout(existing);
1650
+ }
1651
+ const timer = setTimeout(() => {
1652
+ this._timers.delete(filePath);
1653
+ this._onChange(filePath);
1654
+ }, this._debounceMs);
1655
+ this._timers.set(filePath, timer);
1656
+ }
1657
+ /** Cancel all pending timers. */
1658
+ cancelAll() {
1659
+ for (const timer of this._timers.values()) {
1660
+ clearTimeout(timer);
1661
+ }
1662
+ this._timers.clear();
1663
+ }
1664
+ /** Number of pending timers (for testing). */
1665
+ get pendingCount() {
1666
+ return this._timers.size;
1667
+ }
1668
+ };
1669
+ var MCP_CONFIG_NAMES = /* @__PURE__ */ new Set([
1670
+ "claude_desktop_config.json",
1671
+ "mcp.json",
1672
+ "mcp_config.json",
1673
+ "cline_mcp_settings.json"
1674
+ ]);
1675
+ var AGENT_PATH_MARKERS = [
1676
+ ".claude",
1677
+ ".cursor",
1678
+ ".gemini",
1679
+ ".codex",
1680
+ ".kiro",
1681
+ ".opencode",
1682
+ ".continue",
1683
+ ".aider",
1684
+ ".roo",
1685
+ ".amp",
1686
+ "windsurf",
1687
+ "zed"
1688
+ ];
1689
+ function classifyPath(filePath) {
1690
+ const name = basename3(filePath).toLowerCase();
1691
+ const ext = extname2(filePath).toLowerCase();
1692
+ if (MCP_CONFIG_NAMES.has(name)) return "mcp_config";
1693
+ if (name === "settings.json" || name === "config.json") {
1694
+ const lower = filePath.toLowerCase();
1695
+ if (AGENT_PATH_MARKERS.some((marker) => lower.includes(marker))) {
1696
+ return "mcp_config";
1697
+ }
1698
+ }
1699
+ if ([".md", ".txt", ".yaml", ".yml"].includes(ext)) return "skill";
1700
+ if (name === ".cursorrules") return "skill";
1701
+ return "unknown";
1702
+ }
1703
+ function isDir(p) {
1704
+ try {
1705
+ return statSync3(p).isDirectory();
1706
+ } catch {
1707
+ return false;
1708
+ }
1709
+ }
1710
+ function fileExists(p) {
1711
+ try {
1712
+ return statSync3(p).isFile();
1713
+ } catch {
1714
+ return false;
1715
+ }
1716
+ }
1717
+ function collectWatchPaths(homeOverride) {
1718
+ const home = homeOverride ?? homedir4();
1719
+ const plat = process.platform === "darwin" ? "Darwin" : process.platform === "win32" ? "Windows" : "Linux";
1720
+ const configs = getWellKnownConfigs();
1721
+ const dirs = [];
1722
+ const files = [];
1723
+ const seen = /* @__PURE__ */ new Set();
1724
+ const addDir = (p) => {
1725
+ const resolved = resolve2(p);
1726
+ if (!seen.has(resolved) && isDir(p)) {
1727
+ seen.add(resolved);
1728
+ dirs.push(p);
1729
+ }
1730
+ };
1731
+ const addFile = (p) => {
1732
+ const resolved = resolve2(p);
1733
+ if (!seen.has(resolved) && fileExists(p)) {
1734
+ seen.add(resolved);
1735
+ files.push(p);
1736
+ }
1737
+ };
1738
+ for (const cfg of configs) {
1739
+ const paths = cfg.paths;
1740
+ let cfgPath = paths[plat] ?? paths.all;
1741
+ if (!cfgPath) continue;
1742
+ cfgPath = cfgPath.replace(/^~/, home);
1743
+ const parent = dirname2(cfgPath);
1744
+ if (isDir(parent)) addDir(parent);
1745
+ }
1746
+ for (const skillDirRel of PROJECT_SKILL_DIRS) {
1747
+ const skillDir = join3(home, skillDirRel);
1748
+ addDir(skillDir);
1749
+ }
1750
+ for (const skillFileRel of PROJECT_SKILL_FILES) {
1751
+ const skillFile = join3(home, skillFileRel);
1752
+ const parent = dirname2(skillFile);
1753
+ if (isDir(parent)) addDir(parent);
1754
+ }
1755
+ try {
1756
+ const cwd = process.cwd();
1757
+ for (const name of [".cursorrules", "CLAUDE.md", ".github"]) {
1758
+ const candidate = join3(cwd, name);
1759
+ if (isDir(candidate)) addDir(candidate);
1760
+ else if (fileExists(candidate)) addFile(candidate);
1761
+ }
1762
+ } catch {
1763
+ }
1764
+ return { dirs, files };
1765
+ }
1766
+ var Shield = class {
1767
+ _onEvent;
1768
+ _notifier;
1769
+ _scanner;
1770
+ _mcpChecker;
1771
+ _blocklist;
1772
+ _baselineStore;
1773
+ _debounceMs;
1774
+ _watchers = [];
1775
+ _handler = null;
1776
+ _running = false;
1777
+ _scanCount = 0;
1778
+ _threatCount = 0;
1779
+ constructor(options = {}) {
1780
+ this._onEvent = options.onEvent ?? (() => {
1781
+ });
1782
+ this._notifier = new Notifier(options.notify ?? true);
1783
+ this._scanner = new SkillScanner();
1784
+ this._mcpChecker = new MCPConfigChecker();
1785
+ this._blocklist = new Blocklist();
1786
+ this._baselineStore = new BaselineStore();
1787
+ this._debounceMs = (options.debounceSeconds ?? 2) * 1e3;
1788
+ }
1789
+ get scanCount() {
1790
+ return this._scanCount;
1791
+ }
1792
+ get threatCount() {
1793
+ return this._threatCount;
1794
+ }
1795
+ get running() {
1796
+ return this._running;
1797
+ }
1798
+ /** Handle a single file change event. */
1799
+ handleChange(filePath) {
1800
+ if (!fileExists(filePath)) return;
1801
+ const fileType = classifyPath(filePath);
1802
+ this._scanCount++;
1803
+ if (fileType === "skill") {
1804
+ this._scanSkill(filePath);
1805
+ } else if (fileType === "mcp_config") {
1806
+ this._scanMcpConfig(filePath);
1807
+ } else {
1808
+ const ext = extname2(filePath).toLowerCase();
1809
+ if ([".md", ".txt", ".yaml", ".yml"].includes(ext)) {
1810
+ this._scanSkill(filePath);
1811
+ }
1812
+ }
1813
+ }
1814
+ _scanSkill(filePath) {
1815
+ try {
1816
+ const result = scanSkillFile(filePath, this._scanner, this._blocklist);
1817
+ if (result.verdict === GuardVerdict.DANGER) {
1818
+ this._threatCount++;
1819
+ const detail = result.findings[0]?.title ?? "Threat detected";
1820
+ this._onEvent("threat", filePath, `DANGER - ${detail}`);
1821
+ this._notifier.notifyThreat(
1822
+ result.name,
1823
+ "Skill",
1824
+ result.findings[0]?.severity ?? "high",
1825
+ detail
1826
+ );
1827
+ } else if (result.verdict === GuardVerdict.WARNING) {
1828
+ const detail = result.findings[0]?.title ?? "Warning";
1829
+ this._onEvent("warning", filePath, `WARNING - ${detail}`);
1830
+ } else {
1831
+ this._onEvent("clean", filePath, "CLEAN");
1832
+ }
1833
+ } catch {
1834
+ this._onEvent("error", filePath, "Failed to scan file");
1835
+ }
1836
+ }
1837
+ _scanMcpConfig(filePath) {
1838
+ let data;
1839
+ try {
1840
+ const raw = readFileSync3(filePath, "utf-8");
1841
+ data = JSON.parse(stripJsonComments(raw));
1842
+ } catch {
1843
+ this._onEvent("error", filePath, "Failed to parse config");
1844
+ return;
1845
+ }
1846
+ let servers = {};
1847
+ for (const key of ["mcpServers", "servers", "context_servers"]) {
1848
+ if (key in data && typeof data[key] === "object" && data[key] !== null) {
1849
+ servers = data[key];
1850
+ break;
1851
+ }
1852
+ }
1853
+ if (Object.keys(servers).length === 0) {
1854
+ this._onEvent("clean", filePath, "No MCP servers in config");
1855
+ return;
1856
+ }
1857
+ let hasThreat = false;
1858
+ const serverDicts = [];
1859
+ for (const [srvName, srvCfg] of Object.entries(servers)) {
1860
+ if (typeof srvCfg !== "object" || srvCfg === null) continue;
1861
+ const serverDict = { name: srvName, source_file: filePath, ...srvCfg };
1862
+ serverDicts.push(serverDict);
1863
+ const result = this._mcpChecker.check(serverDict);
1864
+ if (result.verdict === GuardVerdict.DANGER) {
1865
+ hasThreat = true;
1866
+ this._threatCount++;
1867
+ const detail = result.findings[0]?.title ?? "Threat detected";
1868
+ this._onEvent("threat", filePath, `MCP '${srvName}': DANGER - ${detail}`);
1869
+ this._notifier.notifyThreat(
1870
+ srvName,
1871
+ "MCP Server",
1872
+ result.findings[0]?.severity ?? "high",
1873
+ detail
1874
+ );
1875
+ } else if (result.verdict === GuardVerdict.WARNING) {
1876
+ const detail = result.findings[0]?.title ?? "Warning";
1877
+ this._onEvent("warning", filePath, `MCP '${srvName}': WARNING - ${detail}`);
1878
+ }
1879
+ const change = this._baselineStore.checkServer(serverDict);
1880
+ if (change && (change.change_type === "config_changed" || change.change_type === "binary_changed")) {
1881
+ this._threatCount++;
1882
+ this._onEvent("warning", filePath, `BASELINE: ${change.detail}`);
1883
+ this._notifier.notifyThreat(srvName, "MCP Baseline", "high", change.detail);
1884
+ }
1885
+ }
1886
+ if (serverDicts.length >= 2) {
1887
+ const flows = analyzeToxicFlows(serverDicts);
1888
+ for (const flow of flows) {
1889
+ this._onEvent("warning", filePath, `TOXIC FLOW: ${flow.title}`);
1890
+ }
1891
+ }
1892
+ if (!hasThreat) {
1893
+ this._onEvent("clean", filePath, `MCP config OK (${Object.keys(servers).length} servers)`);
1894
+ }
1895
+ }
1896
+ /**
1897
+ * Start watching. Returns { dirsWatched, filesWatched }.
1898
+ *
1899
+ * Uses Node.js fs.watch with recursive option (macOS/Windows).
1900
+ * Does NOT block — call stop() to clean up.
1901
+ */
1902
+ start(homeOverride) {
1903
+ const { dirs, files } = collectWatchPaths(homeOverride);
1904
+ this._handler = new DebouncedHandler(
1905
+ (fp) => this.handleChange(fp),
1906
+ this._debounceMs
1907
+ );
1908
+ let watchedCount = 0;
1909
+ for (const d of dirs) {
1910
+ try {
1911
+ const watcher = watch(d, { recursive: true }, (_eventType, filename) => {
1912
+ if (filename) {
1913
+ this._handler?.handleEvent(join3(d, filename));
1914
+ }
1915
+ });
1916
+ this._watchers.push(watcher);
1917
+ watchedCount++;
1918
+ } catch {
1919
+ }
1920
+ }
1921
+ const fileParents = /* @__PURE__ */ new Set();
1922
+ for (const f of files) {
1923
+ const parent = dirname2(f);
1924
+ if (!fileParents.has(parent)) {
1925
+ fileParents.add(parent);
1926
+ try {
1927
+ const watcher = watch(parent, { recursive: false }, (_eventType, filename) => {
1928
+ if (filename) {
1929
+ this._handler?.handleEvent(join3(parent, filename));
1930
+ }
1931
+ });
1932
+ this._watchers.push(watcher);
1933
+ watchedCount++;
1934
+ } catch {
1935
+ }
1936
+ }
1937
+ }
1938
+ this._running = true;
1939
+ return { dirsWatched: watchedCount, filesWatched: files.length };
1940
+ }
1941
+ /** Stop the filesystem watchers. */
1942
+ stop() {
1943
+ this._running = false;
1944
+ if (this._handler) {
1945
+ this._handler.cancelAll();
1946
+ this._handler = null;
1947
+ }
1948
+ for (const w of this._watchers) {
1949
+ try {
1950
+ w.close();
1951
+ } catch {
1952
+ }
1953
+ }
1954
+ this._watchers = [];
1955
+ }
1956
+ };
1957
+ export {
1958
+ DebouncedHandler,
1959
+ Shield,
1960
+ classifyPath,
1961
+ collectWatchPaths
1962
+ };