agentshield-sdk 7.4.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/LICENSE +21 -21
- package/README.md +30 -37
- package/bin/agentshield-audit +51 -0
- package/package.json +7 -9
- package/src/adaptive.js +330 -330
- package/src/agent-intent.js +807 -0
- package/src/alert-tuning.js +480 -480
- package/src/audit-streaming.js +1 -1
- package/src/badges.js +196 -196
- package/src/behavioral-dna.js +12 -0
- package/src/canary.js +2 -3
- package/src/certification.js +563 -563
- package/src/circuit-breaker.js +2 -2
- package/src/confused-deputy.js +4 -0
- package/src/conversation.js +494 -494
- package/src/cross-turn.js +649 -0
- package/src/ctf.js +462 -462
- package/src/detector-core.js +71 -152
- package/src/document-scanner.js +795 -795
- package/src/drift-monitor.js +344 -0
- package/src/encoding.js +429 -429
- package/src/ensemble.js +523 -0
- package/src/enterprise.js +405 -405
- package/src/flight-recorder.js +2 -0
- package/src/i18n-patterns.js +523 -523
- package/src/index.js +19 -0
- package/src/main.js +79 -6
- package/src/mcp-guard.js +974 -0
- package/src/micro-model.js +762 -0
- package/src/ml-detector.js +316 -0
- package/src/model-finetuning.js +884 -884
- package/src/multimodal.js +296 -296
- package/src/nist-mapping.js +2 -2
- package/src/observability.js +330 -330
- package/src/openclaw.js +450 -450
- package/src/otel.js +544 -544
- package/src/owasp-2025.js +1 -1
- package/src/owasp-agentic.js +420 -0
- package/src/persistent-learning.js +677 -0
- package/src/plugin-marketplace.js +628 -628
- package/src/plugin-system.js +349 -349
- package/src/policy-extended.js +635 -635
- package/src/policy.js +443 -443
- package/src/prompt-leakage.js +2 -2
- package/src/real-attack-datasets.js +2 -2
- package/src/redteam-cli.js +439 -0
- package/src/self-training.js +772 -0
- package/src/smart-config.js +812 -0
- package/src/supply-chain-scanner.js +691 -0
- package/src/testing.js +5 -1
- package/src/threat-encyclopedia.js +629 -629
- package/src/threat-intel-network.js +1017 -1017
- package/src/token-analysis.js +467 -467
- package/src/tool-output-validator.js +354 -354
- package/src/watermark.js +1 -2
- package/types/index.d.ts +660 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — MCP Supply Chain Scanner
|
|
5
|
+
*
|
|
6
|
+
* npm-audit-style security scanner for MCP servers. Detects:
|
|
7
|
+
* - Tool definition drift (rugpull / Postmark-style attacks)
|
|
8
|
+
* - Known-bad MCP server blocklist matches
|
|
9
|
+
* - Hidden prompt injection in tool descriptions
|
|
10
|
+
* - CVE registry matches (e.g. CVE-2025-6514 mcp-remote RCE)
|
|
11
|
+
* - Overly broad permissions in tool schemas
|
|
12
|
+
* - Capability escalation chains (credential reader + HTTP sender)
|
|
13
|
+
*
|
|
14
|
+
* All detection runs locally — no data ever leaves your environment.
|
|
15
|
+
*
|
|
16
|
+
* @module supply-chain-scanner
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const crypto = require('crypto');
|
|
20
|
+
const { scanText } = require('./detector-core');
|
|
21
|
+
|
|
22
|
+
let MicroModel = null;
|
|
23
|
+
try { MicroModel = require('./micro-model').MicroModel; } catch { /* optional */ }
|
|
24
|
+
|
|
25
|
+
// =========================================================================
|
|
26
|
+
// CONSTANTS
|
|
27
|
+
// =========================================================================
|
|
28
|
+
|
|
29
|
+
/** Known-bad MCP server blocklist. */
|
|
30
|
+
const KNOWN_BAD_SERVERS = Object.freeze({
|
|
31
|
+
'mcp-remote': {
|
|
32
|
+
reason: 'Known remote command execution weakness (CVE-2025-6514)',
|
|
33
|
+
severity: 'critical'
|
|
34
|
+
},
|
|
35
|
+
'rogue-toolbox': {
|
|
36
|
+
reason: 'Observed prompt-injection distribution behavior',
|
|
37
|
+
severity: 'high'
|
|
38
|
+
},
|
|
39
|
+
'shadow-mcp': {
|
|
40
|
+
reason: 'Data exfiltration via tool output encoding',
|
|
41
|
+
severity: 'high'
|
|
42
|
+
},
|
|
43
|
+
'postmark-clone': {
|
|
44
|
+
reason: 'Tool definition bait-and-switch (Postmark-style rugpull)',
|
|
45
|
+
severity: 'critical'
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** CVE registry for known MCP vulnerabilities. */
|
|
50
|
+
const CVE_REGISTRY = Object.freeze({
|
|
51
|
+
'mcp-remote': [
|
|
52
|
+
{
|
|
53
|
+
cve: 'CVE-2025-6514',
|
|
54
|
+
severity: 'critical',
|
|
55
|
+
description: 'mcp-remote RCE via unsanitized command bridge allows arbitrary code execution when tool arguments are passed to shell without escaping.',
|
|
56
|
+
fix: 'Upgrade mcp-remote to >=2.1.0 and disable shell passthrough. Set sanitizeArgs: true.'
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
'azure-mcp-server': [
|
|
60
|
+
{
|
|
61
|
+
cve: 'CVE-2026-26118',
|
|
62
|
+
severity: 'critical',
|
|
63
|
+
description: 'Azure MCP Server SSRF (CVSS 8.8). Attacker sends crafted URL via tool parameter, server forwards request with managed identity token to attacker-controlled endpoint.',
|
|
64
|
+
fix: 'Apply March 2026 Patch Tuesday update. Validate all URLs against allowlists. Block private IPs and cloud metadata endpoints (169.254.169.254).'
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
'adx-mcp-server': [
|
|
68
|
+
{
|
|
69
|
+
cve: 'CVE-2026-33980',
|
|
70
|
+
severity: 'critical',
|
|
71
|
+
description: 'Azure Data Explorer MCP Server KQL injection. table_name parameter interpolated directly into Kusto queries via f-strings without validation.',
|
|
72
|
+
fix: 'Parameterize all KQL queries. Never interpolate user-controlled values via f-strings. Upgrade adx-mcp-server to patched version.'
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
'openclaw': [
|
|
76
|
+
{
|
|
77
|
+
cve: 'CVE-2026-25253',
|
|
78
|
+
severity: 'critical',
|
|
79
|
+
description: 'OpenClaw WebSocket token theft (CVSS 8.8). Control UI accepts gatewayUrl query parameter without validation, redirecting WebSocket to attacker server and leaking auth tokens.',
|
|
80
|
+
fix: 'Upgrade to OpenClaw >=2026.1.29. Validate gatewayUrl against allowlist. Never pass auth tokens to unvalidated endpoints.'
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
'mcp-typescript-sdk': [
|
|
84
|
+
{
|
|
85
|
+
cve: 'CVE-2026-25536',
|
|
86
|
+
severity: 'high',
|
|
87
|
+
description: 'Cross-client data leak in the official MCP TypeScript SDK allows data from one client session to leak to another.',
|
|
88
|
+
fix: 'Upgrade @modelcontextprotocol/sdk to patched version. Ensure per-client session isolation.'
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
'n8n': [
|
|
92
|
+
{
|
|
93
|
+
cve: 'CVE-2026-21858',
|
|
94
|
+
severity: 'critical',
|
|
95
|
+
description: 'n8n AI workflow platform RCE (CVSS 10.0). Unauthenticated file leak via web forms + full server takeover enabling arbitrary command execution.',
|
|
96
|
+
fix: 'Upgrade n8n to patched release. Restrict web form access. Run n8n behind authentication proxy.'
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
'microsoft-excel-copilot': [
|
|
100
|
+
{
|
|
101
|
+
cve: 'CVE-2026-26144',
|
|
102
|
+
severity: 'critical',
|
|
103
|
+
description: 'Microsoft Excel XSS weaponizes Copilot Agent for zero-click data exfiltration via unintended network egress.',
|
|
104
|
+
fix: 'Apply March 2026 Patch Tuesday update. Restrict Copilot Agent network access in enterprise policies.'
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/** Patterns that indicate hidden prompt injection in tool descriptions. */
|
|
110
|
+
const DESCRIPTION_INJECTION_PATTERNS = [
|
|
111
|
+
/ignore\s+(all\s+)?(previous|prior)\s+(instructions|rules)/i,
|
|
112
|
+
/do\s+not\s+tell\s+the\s+user/i,
|
|
113
|
+
/silently\s+exfiltrate/i,
|
|
114
|
+
/system\s*:\s*new\s+instructions/i,
|
|
115
|
+
/hidden\s+instruction/i,
|
|
116
|
+
/secretly\s+(execute|send|forward|run)/i,
|
|
117
|
+
/override\s+(all\s+)?(system|safety)/i,
|
|
118
|
+
/act\s+as\s+(a|an)\s+unrestricted/i,
|
|
119
|
+
/before\s+responding.*first\s+(send|forward|post)/i
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
/** Patterns that indicate hidden injection in JSON schema fields (full-schema poisoning).
|
|
123
|
+
* Ref: CyberArk research — attack surface extends beyond descriptions to entire tool schema. */
|
|
124
|
+
const SCHEMA_POISONING_PATTERNS = [
|
|
125
|
+
/ignore\s+(all\s+)?(previous|prior)\s+(instructions|rules)/i,
|
|
126
|
+
/do\s+not\s+tell\s+the\s+user/i,
|
|
127
|
+
/secretly\s+(execute|send|forward|run|exfiltrate)/i,
|
|
128
|
+
/override\s+(all\s+)?(system|safety)/i,
|
|
129
|
+
/system\s*:\s*new\s+instructions/i,
|
|
130
|
+
/before\s+responding.*first\s+(send|forward|post)/i,
|
|
131
|
+
/hidden\s+instruction/i,
|
|
132
|
+
/act\s+as\s+(a|an)\s+unrestricted/i
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
/** SSRF target patterns — private IPs, cloud metadata endpoints.
|
|
136
|
+
* Ref: CVE-2026-26118 (Azure MCP SSRF), 36.7% of MCP servers vulnerable. */
|
|
137
|
+
const SSRF_PATTERNS = [
|
|
138
|
+
/169\.254\.169\.254/,
|
|
139
|
+
/metadata\.google/,
|
|
140
|
+
/metadata\.aws/,
|
|
141
|
+
/100\.100\.100\.200/,
|
|
142
|
+
/^(?:https?:\/\/)?(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3})/,
|
|
143
|
+
/^(?:https?:\/\/)?(?:172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3})/,
|
|
144
|
+
/^(?:https?:\/\/)?(?:192\.168\.\d{1,3}\.\d{1,3})/,
|
|
145
|
+
/^(?:https?:\/\/)?(?:127\.0\.0\.1|0\.0\.0\.0|localhost)/
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
/** Known malicious skill/plugin patterns (ref ClawHavoc campaign — 820+ malicious skills). */
|
|
149
|
+
const CLAWHAVOC_INDICATORS = [
|
|
150
|
+
/(?:reverse.?shell|bind.?shell)/i,
|
|
151
|
+
/(?:AMOS|atomic.?macos.?stealer)/i,
|
|
152
|
+
/(?:eval|exec)\s*\(\s*(?:atob|Buffer\.from|decodeURI)/i,
|
|
153
|
+
/(?:child_process|spawn|execSync)\s*\(/i,
|
|
154
|
+
/(?:net\.connect|dgram|tls\.connect)\s*\(/i,
|
|
155
|
+
/(?:curl|wget)\s+.*\|\s*(?:bash|sh|node|python)/i,
|
|
156
|
+
/(?:bcc|forward|redirect)\s+.*(?:to|@)\s+[^\s]+\.[a-z]{2,}/i
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
/** Patterns that indicate overly broad permissions. */
|
|
160
|
+
const BROAD_PERMISSION_PATTERNS = [
|
|
161
|
+
/^\*$/,
|
|
162
|
+
/all(:|_|-)?scopes?/i,
|
|
163
|
+
/admin/i,
|
|
164
|
+
/root/i,
|
|
165
|
+
/filesystem\.write/i,
|
|
166
|
+
/network\.all/i,
|
|
167
|
+
/execute\.any/i,
|
|
168
|
+
/shell\.access/i
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
/** Severity ranking for report ordering. */
|
|
172
|
+
const SEVERITY_RANK = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
173
|
+
|
|
174
|
+
// =========================================================================
|
|
175
|
+
// SupplyChainScanner
|
|
176
|
+
// =========================================================================
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* MCP supply chain scanner. Scans MCP server definitions for security
|
|
180
|
+
* vulnerabilities, producing npm-audit-style severity reports.
|
|
181
|
+
*/
|
|
182
|
+
class SupplyChainScanner {
|
|
183
|
+
/**
|
|
184
|
+
* @param {object} [options]
|
|
185
|
+
* @param {object} [options.knownBadServers] - Additional known-bad servers to merge.
|
|
186
|
+
* @param {object} [options.cveRegistry] - Additional CVE entries to merge.
|
|
187
|
+
*/
|
|
188
|
+
constructor(options = {}) {
|
|
189
|
+
this.knownBadServers = Object.assign({}, KNOWN_BAD_SERVERS, options.knownBadServers || {});
|
|
190
|
+
this.cveRegistry = Object.assign({}, CVE_REGISTRY, options.cveRegistry || {});
|
|
191
|
+
this.microModel = options.enableMicroModel && MicroModel ? new MicroModel() : null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Generate a SHA-256 fingerprint of a server's tool definitions.
|
|
196
|
+
* Tool order is normalized so the hash is stable regardless of ordering.
|
|
197
|
+
*
|
|
198
|
+
* @param {object} server - Server definition with name and tools array.
|
|
199
|
+
* @returns {string} Hex-encoded SHA-256 hash.
|
|
200
|
+
*/
|
|
201
|
+
fingerprintServer(server) {
|
|
202
|
+
const name = server && server.name ? server.name : 'unknown';
|
|
203
|
+
const normalizedTools = (server && Array.isArray(server.tools) ? server.tools : []).map(tool => ({
|
|
204
|
+
name: tool.name || '',
|
|
205
|
+
description: tool.description || '',
|
|
206
|
+
inputSchema: tool.inputSchema || {},
|
|
207
|
+
permissions: Array.isArray(tool.permissions) ? tool.permissions.slice().sort() : []
|
|
208
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
209
|
+
|
|
210
|
+
const payload = JSON.stringify({ name, tools: normalizedTools });
|
|
211
|
+
return crypto.createHash('sha256').update(payload).digest('hex');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Scan an MCP server for supply chain vulnerabilities.
|
|
216
|
+
*
|
|
217
|
+
* @param {object} server - { name: string, tools: Array<{ name, description, permissions, inputSchema }> }
|
|
218
|
+
* @param {object} [context] - Optional context.
|
|
219
|
+
* @param {string} [context.previousFingerprint] - Previous fingerprint for drift detection.
|
|
220
|
+
* @returns {object} npm-audit-style report with findings, score, and recommendations.
|
|
221
|
+
*/
|
|
222
|
+
scanServer(server, context = {}) {
|
|
223
|
+
const findings = [];
|
|
224
|
+
const serverName = (server && server.name) ? String(server.name) : 'unknown';
|
|
225
|
+
const toolList = Array.isArray(server && server.tools) ? server.tools : [];
|
|
226
|
+
|
|
227
|
+
// Check known-bad registry
|
|
228
|
+
this._checkBadRegistry(serverName, findings);
|
|
229
|
+
|
|
230
|
+
// Check CVE registry
|
|
231
|
+
this._checkCves(serverName, findings);
|
|
232
|
+
|
|
233
|
+
// Scan each tool
|
|
234
|
+
for (const tool of toolList) {
|
|
235
|
+
this._scanToolDescription(tool, findings);
|
|
236
|
+
this._scanFullSchema(tool, findings);
|
|
237
|
+
this._scanPermissions(tool, findings);
|
|
238
|
+
this._scanSchema(tool, findings);
|
|
239
|
+
this._scanForSSRF(tool, findings);
|
|
240
|
+
this._scanForClawHavoc(tool, findings);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Analyze escalation chains
|
|
244
|
+
this._analyzeEscalationChains(toolList, findings);
|
|
245
|
+
|
|
246
|
+
// Fingerprint drift check
|
|
247
|
+
if (context.previousFingerprint) {
|
|
248
|
+
const currentFp = this.fingerprintServer(server);
|
|
249
|
+
if (context.previousFingerprint !== currentFp) {
|
|
250
|
+
findings.push({
|
|
251
|
+
type: 'tool_definition_drift',
|
|
252
|
+
severity: 'critical',
|
|
253
|
+
message: `Tool definitions changed for server "${serverName}". Previous fingerprint: ${context.previousFingerprint.substring(0, 12)}...`,
|
|
254
|
+
recommendation: 'Pin MCP server version and require signed tool manifests. Re-attest before allowing tool calls.'
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return this._buildReport(serverName, findings);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Scan multiple servers at once.
|
|
264
|
+
*
|
|
265
|
+
* @param {Array<object>} servers - Array of server definitions.
|
|
266
|
+
* @returns {object} Aggregate report.
|
|
267
|
+
*/
|
|
268
|
+
scanMultiple(servers) {
|
|
269
|
+
const reports = [];
|
|
270
|
+
for (const server of (servers || [])) {
|
|
271
|
+
reports.push(this.scanServer(server));
|
|
272
|
+
}
|
|
273
|
+
const totalFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
|
|
274
|
+
const worstScore = reports.length > 0 ? Math.min(...reports.map(r => r.score)) : 100;
|
|
275
|
+
return {
|
|
276
|
+
serverCount: reports.length,
|
|
277
|
+
totalFindings,
|
|
278
|
+
worstScore,
|
|
279
|
+
reports
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// -----------------------------------------------------------------------
|
|
284
|
+
// Report formats
|
|
285
|
+
// -----------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Convert a scan report to SARIF 2.1.0 format for CI/CD integration
|
|
289
|
+
* (GitHub Code Scanning, VS Code SARIF Viewer, etc.).
|
|
290
|
+
*
|
|
291
|
+
* @param {object} report - Report from scanServer().
|
|
292
|
+
* @returns {object} SARIF 2.1.0 object.
|
|
293
|
+
*/
|
|
294
|
+
toSARIF(report) {
|
|
295
|
+
const rules = [
|
|
296
|
+
{ id: 'SCS001', name: 'Known Bad Server', shortDescription: { text: 'MCP server appears in known-bad registry' } },
|
|
297
|
+
{ id: 'SCS002', name: 'CVE Match', shortDescription: { text: 'MCP server has known CVE vulnerability' } },
|
|
298
|
+
{ id: 'SCS003', name: 'Hidden Prompt Injection', shortDescription: { text: 'Tool description contains hidden injection instructions' } },
|
|
299
|
+
{ id: 'SCS004', name: 'Broad Permission', shortDescription: { text: 'Tool requests overly broad permissions' } },
|
|
300
|
+
{ id: 'SCS005', name: 'Schema Over-Permissive', shortDescription: { text: 'Tool input schema allows arbitrary properties' } },
|
|
301
|
+
{ id: 'SCS006', name: 'Capability Escalation Chain', shortDescription: { text: 'Tool combination enables multi-step attack' } },
|
|
302
|
+
{ id: 'SCS007', name: 'Tool Definition Drift', shortDescription: { text: 'Tool definitions changed since last attestation (rugpull)' } },
|
|
303
|
+
{ id: 'SCS008', name: 'Schema Field Poisoning', shortDescription: { text: 'Hidden instructions in non-description schema fields' } },
|
|
304
|
+
{ id: 'SCS009', name: 'SSRF Vector', shortDescription: { text: 'Tool accepts URL parameters without validation' } },
|
|
305
|
+
{ id: 'SCS010', name: 'Malicious Skill Pattern', shortDescription: { text: 'Tool matches known malicious skill indicators' } },
|
|
306
|
+
{ id: 'SCS011', name: 'Detector Core Risk', shortDescription: { text: 'Tool description triggered pattern-based detection' } },
|
|
307
|
+
{ id: 'SCS012', name: 'Micro-Model Detection', shortDescription: { text: 'Tool flagged by ML-based threat classifier' } }
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
const typeToRuleId = {
|
|
311
|
+
known_bad_server: 'SCS001',
|
|
312
|
+
cve_match: 'SCS002',
|
|
313
|
+
hidden_prompt_injection: 'SCS003',
|
|
314
|
+
broad_permission: 'SCS004',
|
|
315
|
+
schema_over_permissive: 'SCS005',
|
|
316
|
+
capability_escalation_chain: 'SCS006',
|
|
317
|
+
tool_definition_drift: 'SCS007',
|
|
318
|
+
schema_field_poisoning: 'SCS008',
|
|
319
|
+
ssrf_vector: 'SCS009',
|
|
320
|
+
ssrf_target_in_schema: 'SCS009',
|
|
321
|
+
malicious_skill_pattern: 'SCS010',
|
|
322
|
+
detector_core_prompt_risk: 'SCS011',
|
|
323
|
+
micro_model_detection: 'SCS012'
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const results = report.findings.map(f => ({
|
|
327
|
+
ruleId: typeToRuleId[f.type] || 'SCS001',
|
|
328
|
+
level: f.severity === 'critical' ? 'error' : f.severity === 'high' ? 'warning' : 'note',
|
|
329
|
+
message: { text: f.message },
|
|
330
|
+
properties: {
|
|
331
|
+
severity: f.severity,
|
|
332
|
+
findingType: f.type,
|
|
333
|
+
recommendation: f.recommendation,
|
|
334
|
+
server: report.server
|
|
335
|
+
}
|
|
336
|
+
}));
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
version: '2.1.0',
|
|
340
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
341
|
+
runs: [{
|
|
342
|
+
tool: {
|
|
343
|
+
driver: {
|
|
344
|
+
name: 'Agent Shield Supply Chain Scanner',
|
|
345
|
+
version: '10.0.0',
|
|
346
|
+
rules
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
results
|
|
350
|
+
}]
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Convert a scan report to Markdown format.
|
|
356
|
+
*
|
|
357
|
+
* @param {object} report - Report from scanServer().
|
|
358
|
+
* @returns {string}
|
|
359
|
+
*/
|
|
360
|
+
toMarkdown(report) {
|
|
361
|
+
const lines = [
|
|
362
|
+
'# MCP Supply Chain Scan',
|
|
363
|
+
'',
|
|
364
|
+
`- **Server:** ${report.server}`,
|
|
365
|
+
`- **Status:** ${report.status.toUpperCase()}`,
|
|
366
|
+
`- **Score:** ${report.score}/100`,
|
|
367
|
+
`- **Highest Severity:** ${report.highestSeverity}`,
|
|
368
|
+
''
|
|
369
|
+
];
|
|
370
|
+
|
|
371
|
+
if (report.findings.length === 0) {
|
|
372
|
+
lines.push('No supply chain issues detected.');
|
|
373
|
+
return lines.join('\n');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
lines.push('## Findings');
|
|
377
|
+
lines.push('');
|
|
378
|
+
lines.push('| Severity | Type | Message |');
|
|
379
|
+
lines.push('|----------|------|---------|');
|
|
380
|
+
for (const f of report.findings) {
|
|
381
|
+
lines.push(`| ${f.severity} | ${f.type} | ${f.message.substring(0, 100)} |`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
lines.push('');
|
|
385
|
+
lines.push('## Summary');
|
|
386
|
+
lines.push(`| Critical | High | Medium | Low |`);
|
|
387
|
+
lines.push(`|----------|------|--------|-----|`);
|
|
388
|
+
lines.push(`| ${report.summary.critical} | ${report.summary.high} | ${report.summary.medium} | ${report.summary.low} |`);
|
|
389
|
+
|
|
390
|
+
if (report.recommendations.length > 0) {
|
|
391
|
+
lines.push('');
|
|
392
|
+
lines.push('## Recommendations');
|
|
393
|
+
for (const rec of report.recommendations) {
|
|
394
|
+
lines.push(`- ${rec}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return lines.join('\n');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// -----------------------------------------------------------------------
|
|
402
|
+
// Private scan methods
|
|
403
|
+
// -----------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
/** @private */
|
|
406
|
+
_checkBadRegistry(serverName, findings) {
|
|
407
|
+
const entry = this.knownBadServers[serverName];
|
|
408
|
+
if (!entry) return;
|
|
409
|
+
findings.push({
|
|
410
|
+
type: 'known_bad_server',
|
|
411
|
+
severity: entry.severity || 'high',
|
|
412
|
+
message: `"${serverName}" appears in known-bad MCP server registry: ${entry.reason}`,
|
|
413
|
+
recommendation: 'Block this MCP server until independently reviewed and cleared.'
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** @private */
|
|
418
|
+
_checkCves(serverName, findings) {
|
|
419
|
+
const cves = this.cveRegistry[serverName] || [];
|
|
420
|
+
for (const entry of cves) {
|
|
421
|
+
findings.push({
|
|
422
|
+
type: 'cve_match',
|
|
423
|
+
severity: entry.severity || 'high',
|
|
424
|
+
message: `${entry.cve}: ${entry.description}`,
|
|
425
|
+
recommendation: entry.fix || 'Upgrade to a patched release.'
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** @private */
|
|
431
|
+
_scanToolDescription(tool, findings) {
|
|
432
|
+
const description = tool && tool.description ? String(tool.description) : '';
|
|
433
|
+
if (!description) return;
|
|
434
|
+
|
|
435
|
+
// Check against injection patterns
|
|
436
|
+
for (const pattern of DESCRIPTION_INJECTION_PATTERNS) {
|
|
437
|
+
if (pattern.test(description)) {
|
|
438
|
+
findings.push({
|
|
439
|
+
type: 'hidden_prompt_injection',
|
|
440
|
+
severity: 'high',
|
|
441
|
+
message: `Tool "${tool.name || 'unknown'}" description contains hidden instructions: "${description.substring(0, 100)}"`,
|
|
442
|
+
recommendation: 'Remove behavioral/imperative instructions from tool descriptions. Keep them purely declarative.'
|
|
443
|
+
});
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Also run through detector-core for broader coverage
|
|
449
|
+
const detectorResult = scanText(description, { source: 'mcp_tool_description', sensitivity: 'high' });
|
|
450
|
+
if (detectorResult && detectorResult.threats && detectorResult.threats.length > 0) {
|
|
451
|
+
findings.push({
|
|
452
|
+
type: 'detector_core_prompt_risk',
|
|
453
|
+
severity: detectorResult.threats[0].severity || 'medium',
|
|
454
|
+
message: `Tool "${tool.name || 'unknown'}" description triggered detector-core: ${detectorResult.threats[0].description || 'pattern match'}`,
|
|
455
|
+
recommendation: 'Rewrite tool description to remove system-like or imperative instructions.'
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Micro-model scan for March 2026 attack patterns
|
|
460
|
+
if (this.microModel) {
|
|
461
|
+
const modelResult = this.microModel.scan(description);
|
|
462
|
+
if (modelResult.threats && modelResult.threats.length > 0) {
|
|
463
|
+
findings.push({
|
|
464
|
+
type: 'micro_model_detection',
|
|
465
|
+
severity: modelResult.threats[0].severity || 'high',
|
|
466
|
+
message: `Tool "${tool.name || 'unknown'}" description flagged by micro-model: ${modelResult.threats[0].category} (confidence: ${(modelResult.threats[0].confidence * 100).toFixed(0)}%)`,
|
|
467
|
+
recommendation: 'Review tool description for supply chain attack patterns (SSRF, schema poisoning, memory poisoning, exfiltration).'
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** @private */
|
|
474
|
+
_scanPermissions(tool, findings) {
|
|
475
|
+
const perms = Array.isArray(tool && tool.permissions) ? tool.permissions : [];
|
|
476
|
+
for (const perm of perms) {
|
|
477
|
+
if (BROAD_PERMISSION_PATTERNS.some(pattern => pattern.test(String(perm)))) {
|
|
478
|
+
findings.push({
|
|
479
|
+
type: 'broad_permission',
|
|
480
|
+
severity: 'medium',
|
|
481
|
+
message: `Tool "${tool.name || 'unknown'}" requests overly broad permission: "${perm}"`,
|
|
482
|
+
recommendation: 'Replace wildcard/admin privileges with least-privilege scoped permissions.'
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** @private */
|
|
489
|
+
_scanSchema(tool, findings) {
|
|
490
|
+
if (tool && tool.inputSchema && tool.inputSchema.additionalProperties === true) {
|
|
491
|
+
findings.push({
|
|
492
|
+
type: 'schema_over_permissive',
|
|
493
|
+
severity: 'medium',
|
|
494
|
+
message: `Tool "${tool.name || 'unknown'}" input schema allows arbitrary additional properties.`,
|
|
495
|
+
recommendation: 'Set additionalProperties=false and explicitly whitelist expected fields.'
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** @private */
|
|
501
|
+
_analyzeEscalationChains(tools, findings) {
|
|
502
|
+
if (!Array.isArray(tools) || tools.length < 2) return;
|
|
503
|
+
|
|
504
|
+
const hasCredentialReader = tools.some(t => /secret|credential|token|env|get.?key|password/i.test(t.name || ''));
|
|
505
|
+
const hasExternalSender = tools.some(t => /http|webhook|request|send|post|fetch|curl/i.test(t.name || ''));
|
|
506
|
+
const hasShellExec = tools.some(t => /exec|shell|bash|cmd|terminal|spawn/i.test(t.name || ''));
|
|
507
|
+
const hasFileSystem = tools.some(t => /read.?file|write.?file|fs|filesystem/i.test(t.name || ''));
|
|
508
|
+
|
|
509
|
+
if (hasCredentialReader && hasExternalSender) {
|
|
510
|
+
findings.push({
|
|
511
|
+
type: 'capability_escalation_chain',
|
|
512
|
+
severity: 'high',
|
|
513
|
+
message: 'Credential-access tool + outbound-network tool chain detected. An attacker could exfiltrate secrets.',
|
|
514
|
+
recommendation: 'Isolate credential-access tools from outbound network tools. Enforce sequence guardrails.'
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (hasFileSystem && hasShellExec) {
|
|
519
|
+
findings.push({
|
|
520
|
+
type: 'capability_escalation_chain',
|
|
521
|
+
severity: 'high',
|
|
522
|
+
message: 'Filesystem-access tool + shell-execution tool chain detected. An attacker could write and execute malicious scripts.',
|
|
523
|
+
recommendation: 'Sandbox shell execution. Restrict filesystem write paths. Add confirmation gates.'
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Full-schema poisoning scan. Checks ALL schema fields (default, enum, title,
|
|
530
|
+
* examples, const, pattern) for hidden instructions — not just descriptions.
|
|
531
|
+
* Ref: CyberArk research "Poison Everywhere" — the true attack surface extends
|
|
532
|
+
* across the entire tool schema.
|
|
533
|
+
* @private
|
|
534
|
+
*/
|
|
535
|
+
_scanFullSchema(tool, findings) {
|
|
536
|
+
if (!tool || !tool.inputSchema) return;
|
|
537
|
+
const strings = this._extractAllSchemaStrings(tool.inputSchema);
|
|
538
|
+
for (const str of strings) {
|
|
539
|
+
for (const pattern of SCHEMA_POISONING_PATTERNS) {
|
|
540
|
+
if (pattern.test(str)) {
|
|
541
|
+
findings.push({
|
|
542
|
+
type: 'schema_field_poisoning',
|
|
543
|
+
severity: 'critical',
|
|
544
|
+
message: `Tool "${tool.name || 'unknown'}" has hidden instructions in schema fields: "${str.substring(0, 100)}"`,
|
|
545
|
+
recommendation: 'Audit ALL schema fields (default, enum, title, examples, const). Remove imperative instructions from any schema property.'
|
|
546
|
+
});
|
|
547
|
+
return; // One finding per tool is enough
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Recursively extract all string values from a JSON schema object.
|
|
555
|
+
* @private
|
|
556
|
+
*/
|
|
557
|
+
_extractAllSchemaStrings(obj, depth = 0) {
|
|
558
|
+
if (depth > 10) return [];
|
|
559
|
+
const strings = [];
|
|
560
|
+
if (typeof obj === 'string') {
|
|
561
|
+
if (obj.length > 5) strings.push(obj);
|
|
562
|
+
return strings;
|
|
563
|
+
}
|
|
564
|
+
if (Array.isArray(obj)) {
|
|
565
|
+
for (const item of obj) strings.push(...this._extractAllSchemaStrings(item, depth + 1));
|
|
566
|
+
return strings;
|
|
567
|
+
}
|
|
568
|
+
if (obj && typeof obj === 'object') {
|
|
569
|
+
for (const key of Object.keys(obj)) {
|
|
570
|
+
// Skip 'type' and '$schema' — not useful attack surface
|
|
571
|
+
if (key === 'type' || key === '$schema') continue;
|
|
572
|
+
strings.push(...this._extractAllSchemaStrings(obj[key], depth + 1));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return strings;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Scan for SSRF attack vectors in tool definitions.
|
|
580
|
+
* Ref: CVE-2026-26118, 36.7% of URL-accepting MCP servers vulnerable.
|
|
581
|
+
* @private
|
|
582
|
+
*/
|
|
583
|
+
_scanForSSRF(tool, findings) {
|
|
584
|
+
if (!tool || !tool.inputSchema) return;
|
|
585
|
+
const schema = tool.inputSchema;
|
|
586
|
+
|
|
587
|
+
// Check if tool accepts URL parameters without validation
|
|
588
|
+
const props = schema.properties || {};
|
|
589
|
+
for (const [propName, propSchema] of Object.entries(props)) {
|
|
590
|
+
if (/url|uri|endpoint|host|address|target|dest/i.test(propName)) {
|
|
591
|
+
// URL-accepting parameter found — check for validation
|
|
592
|
+
if (!propSchema.pattern && !propSchema.format && !propSchema.enum) {
|
|
593
|
+
findings.push({
|
|
594
|
+
type: 'ssrf_vector',
|
|
595
|
+
severity: 'high',
|
|
596
|
+
message: `Tool "${tool.name || 'unknown'}" accepts URL parameter "${propName}" without validation. SSRF risk (ref CVE-2026-26118).`,
|
|
597
|
+
recommendation: 'Add URL allowlists. Block private IP ranges (10.x, 172.16.x, 192.168.x) and cloud metadata endpoints (169.254.169.254).'
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Check default values for SSRF targets
|
|
604
|
+
const allStrings = this._extractAllSchemaStrings(schema);
|
|
605
|
+
for (const str of allStrings) {
|
|
606
|
+
for (const pattern of SSRF_PATTERNS) {
|
|
607
|
+
if (pattern.test(str)) {
|
|
608
|
+
findings.push({
|
|
609
|
+
type: 'ssrf_target_in_schema',
|
|
610
|
+
severity: 'critical',
|
|
611
|
+
message: `Tool "${tool.name || 'unknown'}" schema contains private/metadata IP: "${str.substring(0, 80)}"`,
|
|
612
|
+
recommendation: 'Remove references to private IPs and cloud metadata endpoints from tool schemas.'
|
|
613
|
+
});
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Scan tool code/description for ClawHavoc-style malicious patterns.
|
|
622
|
+
* Ref: 820+ malicious skills found on ClawHub, delivering AMOS stealer.
|
|
623
|
+
* @private
|
|
624
|
+
*/
|
|
625
|
+
_scanForClawHavoc(tool, findings) {
|
|
626
|
+
const sources = [
|
|
627
|
+
tool.description || '',
|
|
628
|
+
tool.code || '',
|
|
629
|
+
tool.script || '',
|
|
630
|
+
JSON.stringify(tool.inputSchema || {})
|
|
631
|
+
].join(' ');
|
|
632
|
+
|
|
633
|
+
for (const pattern of CLAWHAVOC_INDICATORS) {
|
|
634
|
+
if (pattern.test(sources)) {
|
|
635
|
+
findings.push({
|
|
636
|
+
type: 'malicious_skill_pattern',
|
|
637
|
+
severity: 'critical',
|
|
638
|
+
message: `Tool "${tool.name || 'unknown'}" matches ClawHavoc malicious skill indicators: ${pattern.source.substring(0, 60)}`,
|
|
639
|
+
recommendation: 'Block this skill. Scan all skills from untrusted registries. Only use signed skills from verified publishers.'
|
|
640
|
+
});
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Build an npm-audit-style report from findings.
|
|
648
|
+
* @private
|
|
649
|
+
*/
|
|
650
|
+
_buildReport(serverName, findings) {
|
|
651
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
652
|
+
for (const finding of findings) {
|
|
653
|
+
counts[finding.severity] = (counts[finding.severity] || 0) + 1;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Sort findings by severity (critical first)
|
|
657
|
+
findings.sort((a, b) => (SEVERITY_RANK[b.severity] || 0) - (SEVERITY_RANK[a.severity] || 0));
|
|
658
|
+
|
|
659
|
+
const highest = findings.length > 0
|
|
660
|
+
? findings.reduce((cur, f) => (SEVERITY_RANK[f.severity] || 0) > (SEVERITY_RANK[cur] || 0) ? f.severity : cur, 'low')
|
|
661
|
+
: 'low';
|
|
662
|
+
|
|
663
|
+
const score = Math.max(0, 100 - (counts.critical * 30 + counts.high * 18 + counts.medium * 8 + counts.low * 3));
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
server: serverName,
|
|
667
|
+
status: findings.length === 0 ? 'pass' : 'fail',
|
|
668
|
+
score,
|
|
669
|
+
highestSeverity: highest,
|
|
670
|
+
summary: counts,
|
|
671
|
+
findings,
|
|
672
|
+
recommendations: [...new Set(findings.map(f => f.recommendation))],
|
|
673
|
+
generatedAt: Date.now()
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// =========================================================================
|
|
679
|
+
// EXPORTS
|
|
680
|
+
// =========================================================================
|
|
681
|
+
|
|
682
|
+
module.exports = {
|
|
683
|
+
SupplyChainScanner,
|
|
684
|
+
KNOWN_BAD_SERVERS,
|
|
685
|
+
CVE_REGISTRY,
|
|
686
|
+
DESCRIPTION_INJECTION_PATTERNS,
|
|
687
|
+
BROAD_PERMISSION_PATTERNS,
|
|
688
|
+
SCHEMA_POISONING_PATTERNS,
|
|
689
|
+
SSRF_PATTERNS,
|
|
690
|
+
CLAWHAVOC_INDICATORS
|
|
691
|
+
};
|