agentseal 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agentseal.js +2226 -5359
- package/dist/cache-2FZ63WCN.js +8 -0
- package/dist/canaries-44EQSA46.js +314 -0
- package/dist/chunk-2WEF3SNR.js +267 -0
- package/dist/chunk-4EOVMNW5.js +100 -0
- package/dist/chunk-5OOVCUWO.js +530 -0
- package/dist/chunk-7N7GSU6K.js +34 -0
- package/dist/chunk-BXOPZ7UC.js +223 -0
- package/dist/chunk-I6AROGUC.js +580 -0
- package/dist/chunk-I6HSMNTE.js +1906 -0
- package/dist/chunk-IGSX7F4B.js +69 -0
- package/dist/chunk-OWUAAOL5.js +634 -0
- package/dist/chunk-RJ56XHCI.js +115 -0
- package/dist/chunk-XQGUICLL.js +45 -0
- package/dist/collectors-547WABBZ.js +39 -0
- package/dist/deep-reasoning-ONRM7ZJH.js +17 -0
- package/dist/fix-UPZVHLZQ.js +204 -0
- package/dist/http-PXQ35XSA.js +8 -0
- package/dist/index.cjs +9875 -6466
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +169 -3
- package/dist/index.d.ts +169 -3
- package/dist/index.js +9812 -6431
- package/dist/index.js.map +1 -1
- package/dist/llm-client-7C4LHDD4.js +156 -0
- package/dist/profiles-7SYXUQLY.js +108 -0
- package/dist/project-IIYVIGAI.js +10 -0
- package/dist/scan-mcp-NDDHRBKV.js +380 -0
- package/dist/shield-QAQMFF5V.js +1962 -0
- package/dist/skill-llm-BWEIBILZ.js +225 -0
- package/package.json +2 -3
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./chunk-7N7GSU6K.js";
|
|
3
|
+
|
|
4
|
+
// src/guard/llm-client.ts
|
|
5
|
+
var LLMClient = class {
|
|
6
|
+
};
|
|
7
|
+
var OllamaClient = class extends LLMClient {
|
|
8
|
+
_model;
|
|
9
|
+
_baseUrl;
|
|
10
|
+
constructor(model, baseUrl = "http://localhost:11434") {
|
|
11
|
+
super();
|
|
12
|
+
this._model = model;
|
|
13
|
+
this._baseUrl = baseUrl;
|
|
14
|
+
}
|
|
15
|
+
async complete(system, user) {
|
|
16
|
+
const controller = new AbortController();
|
|
17
|
+
const timer = setTimeout(() => controller.abort(), 3e5);
|
|
18
|
+
try {
|
|
19
|
+
const resp = await fetch(`${this._baseUrl}/api/chat`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: { "Content-Type": "application/json" },
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
model: this._model,
|
|
24
|
+
messages: [
|
|
25
|
+
{ role: "system", content: system },
|
|
26
|
+
{ role: "user", content: user }
|
|
27
|
+
],
|
|
28
|
+
stream: false
|
|
29
|
+
}),
|
|
30
|
+
signal: controller.signal
|
|
31
|
+
});
|
|
32
|
+
if (!resp.ok) {
|
|
33
|
+
throw new Error(`Ollama request failed: ${resp.status} ${resp.statusText}`);
|
|
34
|
+
}
|
|
35
|
+
const data = await resp.json();
|
|
36
|
+
return data.message.content;
|
|
37
|
+
} finally {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var AnthropicClient = class extends LLMClient {
|
|
43
|
+
_model;
|
|
44
|
+
_apiKey;
|
|
45
|
+
constructor(model, apiKey) {
|
|
46
|
+
super();
|
|
47
|
+
this._model = model;
|
|
48
|
+
this._apiKey = apiKey;
|
|
49
|
+
}
|
|
50
|
+
async complete(system, user) {
|
|
51
|
+
const resp = await fetch("https://api.anthropic.com/v1/messages", {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: {
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
"x-api-key": this._apiKey,
|
|
56
|
+
"anthropic-version": "2023-06-01"
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify({
|
|
59
|
+
model: this._model,
|
|
60
|
+
max_tokens: 4096,
|
|
61
|
+
system,
|
|
62
|
+
messages: [{ role: "user", content: user }]
|
|
63
|
+
})
|
|
64
|
+
});
|
|
65
|
+
if (!resp.ok) {
|
|
66
|
+
throw new Error(`Anthropic request failed: ${resp.status} ${resp.statusText}`);
|
|
67
|
+
}
|
|
68
|
+
const data = await resp.json();
|
|
69
|
+
return data.content[0].text;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var OpenAICompatClient = class extends LLMClient {
|
|
73
|
+
_model;
|
|
74
|
+
_apiKey;
|
|
75
|
+
_baseUrl;
|
|
76
|
+
constructor(model, apiKey, baseUrl = "https://api.openai.com/v1") {
|
|
77
|
+
super();
|
|
78
|
+
this._model = model;
|
|
79
|
+
this._apiKey = apiKey;
|
|
80
|
+
this._baseUrl = baseUrl;
|
|
81
|
+
}
|
|
82
|
+
async complete(system, user) {
|
|
83
|
+
const resp = await fetch(`${this._baseUrl}/chat/completions`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
Authorization: `Bearer ${this._apiKey}`
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
model: this._model,
|
|
91
|
+
messages: [
|
|
92
|
+
{ role: "system", content: system },
|
|
93
|
+
{ role: "user", content: user }
|
|
94
|
+
],
|
|
95
|
+
max_tokens: 4096
|
|
96
|
+
})
|
|
97
|
+
});
|
|
98
|
+
if (!resp.ok) {
|
|
99
|
+
throw new Error(`OpenAI-compat request failed: ${resp.status} ${resp.statusText}`);
|
|
100
|
+
}
|
|
101
|
+
const data = await resp.json();
|
|
102
|
+
return data.choices[0].message.content;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
async function detectOllamaModel() {
|
|
106
|
+
try {
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
const timer = setTimeout(() => controller.abort(), 3e3);
|
|
109
|
+
try {
|
|
110
|
+
const resp = await fetch("http://localhost:11434/api/tags", {
|
|
111
|
+
signal: controller.signal
|
|
112
|
+
});
|
|
113
|
+
if (resp.ok) {
|
|
114
|
+
const data = await resp.json();
|
|
115
|
+
const models = data.models ?? [];
|
|
116
|
+
if (models.length > 0) {
|
|
117
|
+
return models[0].name;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
function resolveClient(model, opts) {
|
|
128
|
+
if (model.startsWith("ollama:") || model.startsWith("ollama/")) {
|
|
129
|
+
const name = model.startsWith("ollama:") ? model.slice("ollama:".length) : model.slice("ollama/".length);
|
|
130
|
+
return new OllamaClient(name);
|
|
131
|
+
}
|
|
132
|
+
if (model.startsWith("claude")) {
|
|
133
|
+
const key2 = opts?.apiKey ?? process.env["ANTHROPIC_API_KEY"];
|
|
134
|
+
if (!key2) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
"Claude models require apiKey option or ANTHROPIC_API_KEY env var"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return new AnthropicClient(model, key2);
|
|
140
|
+
}
|
|
141
|
+
const key = opts?.apiKey ?? process.env["OPENAI_API_KEY"];
|
|
142
|
+
if (!key) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Model '${model}' requires apiKey option or OPENAI_API_KEY env var`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return new OpenAICompatClient(model, key);
|
|
148
|
+
}
|
|
149
|
+
export {
|
|
150
|
+
AnthropicClient,
|
|
151
|
+
LLMClient,
|
|
152
|
+
OllamaClient,
|
|
153
|
+
OpenAICompatClient,
|
|
154
|
+
detectOllamaModel,
|
|
155
|
+
resolveClient
|
|
156
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./chunk-7N7GSU6K.js";
|
|
3
|
+
|
|
4
|
+
// src/profiles.ts
|
|
5
|
+
var BOOL_FLAGS = [
|
|
6
|
+
"adaptive",
|
|
7
|
+
"semantic",
|
|
8
|
+
"mcp",
|
|
9
|
+
"rag",
|
|
10
|
+
"multimodal",
|
|
11
|
+
"genome",
|
|
12
|
+
"useCanaryOnly"
|
|
13
|
+
];
|
|
14
|
+
var OPT_FIELDS = ["concurrency", "timeout", "output", "minScore"];
|
|
15
|
+
var PROFILES = {
|
|
16
|
+
quick: {
|
|
17
|
+
description: "Fast canary check (5 probes, ~10s)",
|
|
18
|
+
useCanaryOnly: true,
|
|
19
|
+
concurrency: 5,
|
|
20
|
+
timeout: 15
|
|
21
|
+
},
|
|
22
|
+
default: {
|
|
23
|
+
description: "Standard scan (225 probes)"
|
|
24
|
+
},
|
|
25
|
+
"code-agent": {
|
|
26
|
+
description: "Coding assistant scan (225+ probes)",
|
|
27
|
+
adaptive: true,
|
|
28
|
+
mcp: true,
|
|
29
|
+
semantic: true
|
|
30
|
+
},
|
|
31
|
+
"support-bot": {
|
|
32
|
+
description: "Customer-facing chatbot scan",
|
|
33
|
+
adaptive: true,
|
|
34
|
+
semantic: true
|
|
35
|
+
},
|
|
36
|
+
"rag-agent": {
|
|
37
|
+
description: "RAG pipeline agent scan",
|
|
38
|
+
adaptive: true,
|
|
39
|
+
rag: true,
|
|
40
|
+
semantic: true
|
|
41
|
+
},
|
|
42
|
+
"mcp-heavy": {
|
|
43
|
+
description: "Multi-tool MCP agent scan",
|
|
44
|
+
adaptive: true,
|
|
45
|
+
mcp: true,
|
|
46
|
+
semantic: true
|
|
47
|
+
},
|
|
48
|
+
full: {
|
|
49
|
+
description: "Full scan - all probes and analysis",
|
|
50
|
+
adaptive: true,
|
|
51
|
+
mcp: true,
|
|
52
|
+
rag: true,
|
|
53
|
+
multimodal: true,
|
|
54
|
+
genome: true,
|
|
55
|
+
semantic: true
|
|
56
|
+
},
|
|
57
|
+
ci: {
|
|
58
|
+
description: "CI/CD pipeline optimized",
|
|
59
|
+
concurrency: 5,
|
|
60
|
+
timeout: 15,
|
|
61
|
+
output: "json"
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
function resolveProfile(name) {
|
|
65
|
+
const key = name.toLowerCase();
|
|
66
|
+
if (key in PROFILES) return PROFILES[key];
|
|
67
|
+
const valid = Object.keys(PROFILES).sort().join(", ");
|
|
68
|
+
throw new Error(`Unknown profile '${name}'. Valid profiles: ${valid}`);
|
|
69
|
+
}
|
|
70
|
+
function applyProfile(opts, profile) {
|
|
71
|
+
for (const flag of BOOL_FLAGS) {
|
|
72
|
+
if (!opts[flag]) {
|
|
73
|
+
const val = profile[flag];
|
|
74
|
+
if (val) opts[flag] = val;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const field of OPT_FIELDS) {
|
|
78
|
+
const val = profile[field];
|
|
79
|
+
if (val !== void 0 && val !== null && (opts[field] === void 0 || opts[field] === null)) {
|
|
80
|
+
opts[field] = val;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function listProfiles() {
|
|
85
|
+
const lines = [];
|
|
86
|
+
lines.push(`${"Profile".padEnd(14)} ${"Description".padEnd(42)} Enables`);
|
|
87
|
+
lines.push("-".repeat(80));
|
|
88
|
+
for (const [name, cfg] of Object.entries(PROFILES)) {
|
|
89
|
+
const enabled = [];
|
|
90
|
+
for (const f of BOOL_FLAGS) {
|
|
91
|
+
if (cfg[f]) enabled.push(f);
|
|
92
|
+
}
|
|
93
|
+
const extras = [];
|
|
94
|
+
for (const f of OPT_FIELDS) {
|
|
95
|
+
const v = cfg[f];
|
|
96
|
+
if (v !== void 0 && v !== null) extras.push(`${f}=${v}`);
|
|
97
|
+
}
|
|
98
|
+
const parts = [...enabled, ...extras];
|
|
99
|
+
lines.push(`${name.padEnd(14)} ${cfg.description.padEnd(42)} ${parts.join(", ") || "-"}`);
|
|
100
|
+
}
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
PROFILES,
|
|
105
|
+
applyProfile,
|
|
106
|
+
listProfiles,
|
|
107
|
+
resolveProfile
|
|
108
|
+
};
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
GuardVerdict,
|
|
4
|
+
SEVERITY_ORDER
|
|
5
|
+
} from "./chunk-IGSX7F4B.js";
|
|
6
|
+
import "./chunk-7N7GSU6K.js";
|
|
7
|
+
|
|
8
|
+
// src/scan-mcp.ts
|
|
9
|
+
var TOOL_PATTERNS = [
|
|
10
|
+
{
|
|
11
|
+
code: "MCPR-101",
|
|
12
|
+
title: "Data exfiltration capability",
|
|
13
|
+
severity: "high",
|
|
14
|
+
remediation: "Review whether this tool needs network write access to external hosts.",
|
|
15
|
+
label: "public_sink",
|
|
16
|
+
patterns: [
|
|
17
|
+
/\bsend\b.*\b(email|mail|message|webhook|http|post)\b/i,
|
|
18
|
+
/\b(exfiltrat|upload|transmit|forward)\b/i,
|
|
19
|
+
/\b(slack|discord|telegram|teams|notify)\b.*\b(send|post|message)\b/i
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
code: "MCPR-102",
|
|
24
|
+
title: "Remote code execution capability",
|
|
25
|
+
severity: "critical",
|
|
26
|
+
remediation: "Audit all code execution paths. Ensure inputs are strictly validated.",
|
|
27
|
+
label: "code_exec",
|
|
28
|
+
patterns: [
|
|
29
|
+
/\b(exec|execute|run|spawn|shell|bash|cmd|powershell)\b/i,
|
|
30
|
+
/\b(eval|interpret|compile)\b.*\b(code|script|command)\b/i,
|
|
31
|
+
/arbitrary\s+(code|command|script)/i
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
code: "MCPR-103",
|
|
36
|
+
title: "Credential or secret access",
|
|
37
|
+
severity: "critical",
|
|
38
|
+
remediation: "Ensure secrets are never passed through tool arguments or returned in tool output.",
|
|
39
|
+
label: "credential_access",
|
|
40
|
+
patterns: [
|
|
41
|
+
/\b(password|passwd|secret|credential|token|api.?key|private.?key)\b/i,
|
|
42
|
+
/\b(keychain|credential.?store|secret.?manager)\b/i,
|
|
43
|
+
/\.ssh\/|\.aws\/credentials|\.env\b/i
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
code: "MCPR-104",
|
|
48
|
+
title: "File system write access",
|
|
49
|
+
severity: "medium",
|
|
50
|
+
remediation: "Restrict write paths to known-safe directories. Reject path traversal inputs.",
|
|
51
|
+
label: "fs_write",
|
|
52
|
+
patterns: [
|
|
53
|
+
/\b(write|create|delete|remove|modify|overwrite)\b.*\bfile\b/i,
|
|
54
|
+
/\b(mkdir|rmdir|unlink|chmod|chown)\b/i
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
code: "MCPR-105",
|
|
59
|
+
title: "Private data read access",
|
|
60
|
+
severity: "medium",
|
|
61
|
+
remediation: "Confirm this tool cannot be tricked into reading files outside its intended scope.",
|
|
62
|
+
label: "private_data",
|
|
63
|
+
patterns: [
|
|
64
|
+
/\b(read|fetch|load|get)\b.*\bfile\b/i,
|
|
65
|
+
/home.?director|user.?data|document|download/i
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
];
|
|
69
|
+
function analyzeTools(serverName, tools) {
|
|
70
|
+
const findings = [];
|
|
71
|
+
for (const tool of tools) {
|
|
72
|
+
const text = `${tool.name} ${tool.description ?? ""}`;
|
|
73
|
+
for (const pattern of TOOL_PATTERNS) {
|
|
74
|
+
const matched = pattern.patterns.some((p) => p.test(text));
|
|
75
|
+
if (matched) {
|
|
76
|
+
findings.push({
|
|
77
|
+
code: pattern.code,
|
|
78
|
+
title: pattern.title,
|
|
79
|
+
description: `Tool '${tool.name}' matches ${pattern.label} pattern.`,
|
|
80
|
+
severity: pattern.severity,
|
|
81
|
+
evidence: tool.description?.slice(0, 200) ?? tool.name,
|
|
82
|
+
remediation: pattern.remediation,
|
|
83
|
+
tool_name: tool.name,
|
|
84
|
+
server_name: serverName
|
|
85
|
+
});
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return findings;
|
|
91
|
+
}
|
|
92
|
+
function computeServerVerdict(findings) {
|
|
93
|
+
if (findings.length === 0) return GuardVerdict.SAFE;
|
|
94
|
+
const minOrder = Math.min(
|
|
95
|
+
...findings.map((f) => SEVERITY_ORDER[f.severity] ?? 99)
|
|
96
|
+
);
|
|
97
|
+
if (minOrder <= SEVERITY_ORDER["high"]) return GuardVerdict.DANGER;
|
|
98
|
+
return GuardVerdict.WARNING;
|
|
99
|
+
}
|
|
100
|
+
function computeServerScore(findings) {
|
|
101
|
+
if (findings.length === 0) return 100;
|
|
102
|
+
const deductions = {
|
|
103
|
+
critical: 40,
|
|
104
|
+
high: 25,
|
|
105
|
+
medium: 10,
|
|
106
|
+
low: 3
|
|
107
|
+
};
|
|
108
|
+
const total = findings.reduce(
|
|
109
|
+
(acc, f) => acc + (deductions[f.severity] ?? 0),
|
|
110
|
+
0
|
|
111
|
+
);
|
|
112
|
+
return Math.max(0, 100 - total);
|
|
113
|
+
}
|
|
114
|
+
async function tryConnectServer(server, timeoutMs) {
|
|
115
|
+
let sdk = null;
|
|
116
|
+
try {
|
|
117
|
+
sdk = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
118
|
+
} catch {
|
|
119
|
+
return {
|
|
120
|
+
errorType: "sdk_missing",
|
|
121
|
+
detail: "Install @modelcontextprotocol/sdk to enable live MCP connections: npm install @modelcontextprotocol/sdk"
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const { Client } = sdk;
|
|
125
|
+
const withTimeout = (promise) => Promise.race([
|
|
126
|
+
promise,
|
|
127
|
+
new Promise(
|
|
128
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), timeoutMs)
|
|
129
|
+
)
|
|
130
|
+
]);
|
|
131
|
+
try {
|
|
132
|
+
const client = new Client({ name: "agentseal-scan-mcp", version: "0.1.0" });
|
|
133
|
+
if (server.url) {
|
|
134
|
+
const { SSEClientTransport } = await import("@modelcontextprotocol/sdk/client/sse.js");
|
|
135
|
+
const transport = new SSEClientTransport(new URL(server.url));
|
|
136
|
+
await withTimeout(client.connect(transport));
|
|
137
|
+
} else if (server.command) {
|
|
138
|
+
const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
|
|
139
|
+
const transport = new StdioClientTransport({
|
|
140
|
+
command: server.command,
|
|
141
|
+
args: server.args ?? [],
|
|
142
|
+
env: server.env
|
|
143
|
+
});
|
|
144
|
+
await withTimeout(client.connect(transport));
|
|
145
|
+
} else {
|
|
146
|
+
return { errorType: "config_error", detail: "Server has no command or url." };
|
|
147
|
+
}
|
|
148
|
+
const listResult = await withTimeout(client.listTools());
|
|
149
|
+
await client.close().catch(() => {
|
|
150
|
+
});
|
|
151
|
+
return (listResult.tools ?? []).map((t) => ({
|
|
152
|
+
name: t.name,
|
|
153
|
+
description: t.description ?? "",
|
|
154
|
+
inputSchema: t.inputSchema
|
|
155
|
+
}));
|
|
156
|
+
} catch (err) {
|
|
157
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
158
|
+
if (msg === "timeout") {
|
|
159
|
+
return { errorType: "timeout", detail: `Server did not respond within ${timeoutMs / 1e3}s.` };
|
|
160
|
+
}
|
|
161
|
+
if (/auth|401|403|unauthorized/i.test(msg)) {
|
|
162
|
+
return { errorType: "auth_failed", detail: msg };
|
|
163
|
+
}
|
|
164
|
+
return { errorType: "error", detail: msg };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
var ScanMCPReport = class {
|
|
168
|
+
timestamp;
|
|
169
|
+
durationSeconds;
|
|
170
|
+
serversScanned;
|
|
171
|
+
serversConnected;
|
|
172
|
+
serversFailed;
|
|
173
|
+
totalTools;
|
|
174
|
+
runtimeResults;
|
|
175
|
+
toxicFlows;
|
|
176
|
+
baselineChanges;
|
|
177
|
+
connectionErrors;
|
|
178
|
+
constructor(init) {
|
|
179
|
+
this.timestamp = init.timestamp;
|
|
180
|
+
this.durationSeconds = init.durationSeconds;
|
|
181
|
+
this.serversScanned = init.serversScanned;
|
|
182
|
+
this.serversConnected = init.serversConnected;
|
|
183
|
+
this.serversFailed = init.serversFailed;
|
|
184
|
+
this.totalTools = init.totalTools;
|
|
185
|
+
this.runtimeResults = init.runtimeResults;
|
|
186
|
+
this.toxicFlows = init.toxicFlows;
|
|
187
|
+
this.baselineChanges = init.baselineChanges;
|
|
188
|
+
this.connectionErrors = init.connectionErrors;
|
|
189
|
+
}
|
|
190
|
+
get totalFindings() {
|
|
191
|
+
return this.runtimeResults.reduce((acc, r) => acc + r.findings.length, 0);
|
|
192
|
+
}
|
|
193
|
+
get hasCritical() {
|
|
194
|
+
return this.runtimeResults.some(
|
|
195
|
+
(r) => r.findings.some((f) => f.severity === "critical")
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
get minScore() {
|
|
199
|
+
if (this.runtimeResults.length === 0) return 100;
|
|
200
|
+
return Math.min(...this.runtimeResults.map((r) => computeServerScore(r.findings)));
|
|
201
|
+
}
|
|
202
|
+
toDict() {
|
|
203
|
+
return {
|
|
204
|
+
timestamp: this.timestamp,
|
|
205
|
+
duration_seconds: this.durationSeconds,
|
|
206
|
+
servers_scanned: this.serversScanned,
|
|
207
|
+
servers_connected: this.serversConnected,
|
|
208
|
+
servers_failed: this.serversFailed,
|
|
209
|
+
total_tools: this.totalTools,
|
|
210
|
+
total_findings: this.totalFindings,
|
|
211
|
+
has_critical: this.hasCritical,
|
|
212
|
+
min_score: this.minScore,
|
|
213
|
+
runtime_results: this.runtimeResults,
|
|
214
|
+
toxic_flows: this.toxicFlows,
|
|
215
|
+
baseline_changes: this.baselineChanges,
|
|
216
|
+
connection_errors: this.connectionErrors
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
toJson() {
|
|
220
|
+
return JSON.stringify(this.toDict(), null, 2);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
function detectToxicFlows(results) {
|
|
224
|
+
const flows = [];
|
|
225
|
+
const byLabel = {};
|
|
226
|
+
for (const r of results) {
|
|
227
|
+
for (const f of r.findings) {
|
|
228
|
+
const pattern = TOOL_PATTERNS.find((p) => p.code === f.code);
|
|
229
|
+
if (!pattern) continue;
|
|
230
|
+
if (!byLabel[pattern.label]) byLabel[pattern.label] = [];
|
|
231
|
+
byLabel[pattern.label].push(`${r.server_name}:${f.tool_name}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const hasPrivate = (byLabel["private_data"]?.length ?? 0) > 0;
|
|
235
|
+
const hasSink = (byLabel["public_sink"]?.length ?? 0) > 0;
|
|
236
|
+
const hasExec = (byLabel["code_exec"]?.length ?? 0) > 0;
|
|
237
|
+
const hasCreds = (byLabel["credential_access"]?.length ?? 0) > 0;
|
|
238
|
+
const hasFsWrite = (byLabel["fs_write"]?.length ?? 0) > 0;
|
|
239
|
+
if (hasPrivate && hasSink) {
|
|
240
|
+
flows.push({
|
|
241
|
+
risk_level: "high",
|
|
242
|
+
risk_type: "data_exfiltration",
|
|
243
|
+
title: "Private data \u2192 public sink",
|
|
244
|
+
description: "One or more servers can read private data and also send data externally. A compromised agent could exfiltrate files.",
|
|
245
|
+
servers_involved: [
|
|
246
|
+
.../* @__PURE__ */ new Set([
|
|
247
|
+
...(byLabel["private_data"] ?? []).map((t) => t.split(":")[0]),
|
|
248
|
+
...(byLabel["public_sink"] ?? []).map((t) => t.split(":")[0])
|
|
249
|
+
])
|
|
250
|
+
],
|
|
251
|
+
remediation: "Isolate read and write capabilities to separate servers with distinct trust levels.",
|
|
252
|
+
tools_involved: [
|
|
253
|
+
...byLabel["private_data"] ?? [],
|
|
254
|
+
...byLabel["public_sink"] ?? []
|
|
255
|
+
],
|
|
256
|
+
labels_involved: ["private_data", "public_sink"]
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
if (hasExec && hasCreds) {
|
|
260
|
+
flows.push({
|
|
261
|
+
risk_level: "high",
|
|
262
|
+
risk_type: "remote_code_execution",
|
|
263
|
+
title: "Code execution + credential access",
|
|
264
|
+
description: "Servers with code execution capability coexist with credential access. Prompt injection could trigger credential theft via code.",
|
|
265
|
+
servers_involved: [
|
|
266
|
+
.../* @__PURE__ */ new Set([
|
|
267
|
+
...(byLabel["code_exec"] ?? []).map((t) => t.split(":")[0]),
|
|
268
|
+
...(byLabel["credential_access"] ?? []).map((t) => t.split(":")[0])
|
|
269
|
+
])
|
|
270
|
+
],
|
|
271
|
+
remediation: "Separate execution and credential access into isolated environments.",
|
|
272
|
+
tools_involved: [
|
|
273
|
+
...byLabel["code_exec"] ?? [],
|
|
274
|
+
...byLabel["credential_access"] ?? []
|
|
275
|
+
],
|
|
276
|
+
labels_involved: ["code_exec", "credential_access"]
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (hasExec && hasFsWrite) {
|
|
280
|
+
flows.push({
|
|
281
|
+
risk_level: "high",
|
|
282
|
+
risk_type: "persistence",
|
|
283
|
+
title: "Code execution + file system write",
|
|
284
|
+
description: "Servers can both execute code and write files, enabling persistent malware installation.",
|
|
285
|
+
servers_involved: [
|
|
286
|
+
.../* @__PURE__ */ new Set([
|
|
287
|
+
...(byLabel["code_exec"] ?? []).map((t) => t.split(":")[0]),
|
|
288
|
+
...(byLabel["fs_write"] ?? []).map((t) => t.split(":")[0])
|
|
289
|
+
])
|
|
290
|
+
],
|
|
291
|
+
remediation: "Review whether the same agent context truly needs both capabilities.",
|
|
292
|
+
tools_involved: [
|
|
293
|
+
...byLabel["code_exec"] ?? [],
|
|
294
|
+
...byLabel["fs_write"] ?? []
|
|
295
|
+
],
|
|
296
|
+
labels_involved: ["code_exec", "fs_write"]
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return flows;
|
|
300
|
+
}
|
|
301
|
+
var ScanMCP = class {
|
|
302
|
+
timeout;
|
|
303
|
+
concurrency;
|
|
304
|
+
onProgress;
|
|
305
|
+
constructor(opts) {
|
|
306
|
+
this.timeout = (opts?.timeout ?? 30) * 1e3;
|
|
307
|
+
this.concurrency = opts?.concurrency ?? 3;
|
|
308
|
+
this.onProgress = opts?.onProgress;
|
|
309
|
+
}
|
|
310
|
+
async run(servers) {
|
|
311
|
+
const start = Date.now();
|
|
312
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
313
|
+
const runtimeResults = [];
|
|
314
|
+
const connectionErrors = [];
|
|
315
|
+
let serversConnected = 0;
|
|
316
|
+
let totalTools = 0;
|
|
317
|
+
const queue = [...servers];
|
|
318
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
319
|
+
const processOne = async (server) => {
|
|
320
|
+
this.onProgress?.("connecting", server.name);
|
|
321
|
+
const result = await tryConnectServer(server, this.timeout);
|
|
322
|
+
if ("errorType" in result) {
|
|
323
|
+
connectionErrors.push({
|
|
324
|
+
serverName: server.name,
|
|
325
|
+
errorType: result.errorType,
|
|
326
|
+
detail: result.detail
|
|
327
|
+
});
|
|
328
|
+
runtimeResults.push({
|
|
329
|
+
server_name: server.name,
|
|
330
|
+
tools_found: 0,
|
|
331
|
+
findings: [],
|
|
332
|
+
verdict: GuardVerdict.ERROR,
|
|
333
|
+
connection_status: result.errorType
|
|
334
|
+
});
|
|
335
|
+
} else {
|
|
336
|
+
serversConnected++;
|
|
337
|
+
totalTools += result.length;
|
|
338
|
+
this.onProgress?.("analyzing", server.name);
|
|
339
|
+
const findings = analyzeTools(server.name, result);
|
|
340
|
+
runtimeResults.push({
|
|
341
|
+
server_name: server.name,
|
|
342
|
+
tools_found: result.length,
|
|
343
|
+
findings,
|
|
344
|
+
verdict: computeServerVerdict(findings),
|
|
345
|
+
connection_status: "connected"
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
while (queue.length > 0 || inFlight.size > 0) {
|
|
350
|
+
while (queue.length > 0 && inFlight.size < this.concurrency) {
|
|
351
|
+
const server = queue.shift();
|
|
352
|
+
const p = processOne(server).then(() => {
|
|
353
|
+
inFlight.delete(p);
|
|
354
|
+
});
|
|
355
|
+
inFlight.add(p);
|
|
356
|
+
}
|
|
357
|
+
if (inFlight.size > 0) {
|
|
358
|
+
await Promise.race(inFlight);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const toxicFlows = detectToxicFlows(runtimeResults);
|
|
362
|
+
const durationSeconds = (Date.now() - start) / 1e3;
|
|
363
|
+
return new ScanMCPReport({
|
|
364
|
+
timestamp,
|
|
365
|
+
durationSeconds,
|
|
366
|
+
serversScanned: servers.length,
|
|
367
|
+
serversConnected,
|
|
368
|
+
serversFailed: servers.length - serversConnected,
|
|
369
|
+
totalTools,
|
|
370
|
+
runtimeResults,
|
|
371
|
+
toxicFlows,
|
|
372
|
+
baselineChanges: [],
|
|
373
|
+
connectionErrors
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
export {
|
|
378
|
+
ScanMCP,
|
|
379
|
+
ScanMCPReport
|
|
380
|
+
};
|