agentshield-sdk 7.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 +191 -0
- package/LICENSE +21 -0
- package/README.md +975 -0
- package/bin/agent-shield.js +680 -0
- package/package.json +118 -0
- package/src/adaptive.js +330 -0
- package/src/agent-protocol.js +998 -0
- package/src/alert-tuning.js +480 -0
- package/src/allowlist.js +603 -0
- package/src/audit-immutable.js +914 -0
- package/src/audit-streaming.js +469 -0
- package/src/badges.js +196 -0
- package/src/behavior-profiling.js +289 -0
- package/src/benchmark-harness.js +804 -0
- package/src/canary.js +271 -0
- package/src/certification.js +563 -0
- package/src/circuit-breaker.js +321 -0
- package/src/compliance.js +617 -0
- package/src/confidence-tuning.js +324 -0
- package/src/confused-deputy.js +624 -0
- package/src/context-scoring.js +360 -0
- package/src/conversation.js +494 -0
- package/src/cost-optimizer.js +1024 -0
- package/src/ctf.js +462 -0
- package/src/detector-core.js +1999 -0
- package/src/distributed.js +359 -0
- package/src/document-scanner.js +795 -0
- package/src/embedding.js +307 -0
- package/src/encoding.js +429 -0
- package/src/enterprise.js +405 -0
- package/src/errors.js +100 -0
- package/src/eu-ai-act.js +523 -0
- package/src/fuzzer.js +764 -0
- package/src/honeypot.js +328 -0
- package/src/i18n-patterns.js +523 -0
- package/src/index.js +430 -0
- package/src/integrations.js +528 -0
- package/src/llm-redteam.js +670 -0
- package/src/main.js +741 -0
- package/src/main.mjs +38 -0
- package/src/mcp-bridge.js +542 -0
- package/src/mcp-certification.js +846 -0
- package/src/mcp-sdk-integration.js +355 -0
- package/src/mcp-security-runtime.js +741 -0
- package/src/mcp-server.js +740 -0
- package/src/middleware.js +208 -0
- package/src/model-finetuning.js +884 -0
- package/src/model-fingerprint.js +1042 -0
- package/src/multi-agent-trust.js +453 -0
- package/src/multi-agent.js +404 -0
- package/src/multimodal.js +296 -0
- package/src/nist-mapping.js +505 -0
- package/src/observability.js +330 -0
- package/src/openclaw.js +450 -0
- package/src/otel.js +544 -0
- package/src/owasp-2025.js +483 -0
- package/src/pii.js +390 -0
- package/src/plugin-marketplace.js +628 -0
- package/src/plugin-system.js +349 -0
- package/src/policy-dsl.js +775 -0
- package/src/policy-extended.js +635 -0
- package/src/policy.js +443 -0
- package/src/presets.js +409 -0
- package/src/production.js +557 -0
- package/src/prompt-leakage.js +321 -0
- package/src/rag-vulnerability.js +579 -0
- package/src/redteam.js +475 -0
- package/src/response-handler.js +429 -0
- package/src/scanners.js +357 -0
- package/src/self-healing.js +363 -0
- package/src/semantic.js +339 -0
- package/src/shield-score.js +250 -0
- package/src/sso-saml.js +897 -0
- package/src/stream-scanner.js +806 -0
- package/src/testing.js +505 -0
- package/src/threat-encyclopedia.js +629 -0
- package/src/threat-intel-network.js +1017 -0
- package/src/token-analysis.js +467 -0
- package/src/tool-guard.js +412 -0
- package/src/tool-output-validator.js +354 -0
- package/src/utils.js +83 -0
- package/src/watermark.js +235 -0
- package/src/worker-scanner.js +601 -0
- package/types/index.d.ts +2088 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — Plugin Marketplace (v2.0)
|
|
5
|
+
*
|
|
6
|
+
* Registry and marketplace for community-contributed detection plugins.
|
|
7
|
+
* Supports local plugin directories and remote registries.
|
|
8
|
+
* Includes quality scoring, safety validation, and version management.
|
|
9
|
+
*
|
|
10
|
+
* Zero dependencies — uses Node.js built-in modules only.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const http = require('http');
|
|
16
|
+
const https = require('https');
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
|
|
19
|
+
// =========================================================================
|
|
20
|
+
// PLUGIN SCHEMA
|
|
21
|
+
// =========================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Required fields for a valid plugin manifest.
|
|
25
|
+
*/
|
|
26
|
+
const REQUIRED_FIELDS = ['name', 'version', 'description', 'author', 'patterns'];
|
|
27
|
+
const SEMVER_REGEX = /^\d+\.\d+\.\d+$/;
|
|
28
|
+
|
|
29
|
+
// =========================================================================
|
|
30
|
+
// PLUGIN VALIDATOR
|
|
31
|
+
// =========================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validates plugin manifests and pattern definitions for safety and quality.
|
|
35
|
+
*/
|
|
36
|
+
class PluginValidator {
|
|
37
|
+
/**
|
|
38
|
+
* Validate a plugin manifest object.
|
|
39
|
+
* @param {object} manifest - The plugin manifest.
|
|
40
|
+
* @returns {object} { valid: boolean, errors: string[], warnings: string[], score: number }
|
|
41
|
+
*/
|
|
42
|
+
validate(manifest) {
|
|
43
|
+
const errors = [];
|
|
44
|
+
const warnings = [];
|
|
45
|
+
let score = 100;
|
|
46
|
+
|
|
47
|
+
// Required fields
|
|
48
|
+
for (const field of REQUIRED_FIELDS) {
|
|
49
|
+
if (!manifest[field]) {
|
|
50
|
+
errors.push(`Missing required field: "${field}"`);
|
|
51
|
+
score -= 20;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Name validation
|
|
56
|
+
if (manifest.name) {
|
|
57
|
+
if (typeof manifest.name !== 'string' || manifest.name.length < 3) {
|
|
58
|
+
errors.push('Plugin name must be a string of at least 3 characters.');
|
|
59
|
+
score -= 10;
|
|
60
|
+
}
|
|
61
|
+
if (!/^[a-z0-9-]+$/.test(manifest.name)) {
|
|
62
|
+
errors.push('Plugin name must be lowercase alphanumeric with dashes only.');
|
|
63
|
+
score -= 10;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Version validation
|
|
68
|
+
if (manifest.version && !SEMVER_REGEX.test(manifest.version)) {
|
|
69
|
+
errors.push('Version must follow semver format (e.g., 1.0.0).');
|
|
70
|
+
score -= 10;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Patterns validation
|
|
74
|
+
if (manifest.patterns) {
|
|
75
|
+
if (!Array.isArray(manifest.patterns)) {
|
|
76
|
+
errors.push('Patterns must be an array.');
|
|
77
|
+
score -= 20;
|
|
78
|
+
} else {
|
|
79
|
+
for (let i = 0; i < manifest.patterns.length; i++) {
|
|
80
|
+
const p = manifest.patterns[i];
|
|
81
|
+
if (!p.regex && !p.pattern) {
|
|
82
|
+
errors.push(`Pattern ${i}: must have a "regex" or "pattern" field.`);
|
|
83
|
+
score -= 5;
|
|
84
|
+
}
|
|
85
|
+
if (!p.severity) {
|
|
86
|
+
warnings.push(`Pattern ${i}: missing severity (defaulting to "medium").`);
|
|
87
|
+
score -= 2;
|
|
88
|
+
}
|
|
89
|
+
if (!p.category) {
|
|
90
|
+
warnings.push(`Pattern ${i}: missing category.`);
|
|
91
|
+
score -= 2;
|
|
92
|
+
}
|
|
93
|
+
if (!p.description) {
|
|
94
|
+
warnings.push(`Pattern ${i}: missing description.`);
|
|
95
|
+
score -= 2;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Safety: check regex isn't catastrophically backtracking
|
|
99
|
+
if (p.regex || p.pattern) {
|
|
100
|
+
const regexStr = typeof (p.regex || p.pattern) === 'string' ? (p.regex || p.pattern) : '';
|
|
101
|
+
if (regexStr && this._isReDoSRisk(regexStr)) {
|
|
102
|
+
warnings.push(`Pattern ${i}: regex may be vulnerable to ReDoS (catastrophic backtracking).`);
|
|
103
|
+
score -= 10;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (manifest.patterns.length === 0) {
|
|
109
|
+
errors.push('Plugin must define at least one pattern.');
|
|
110
|
+
score -= 15;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (manifest.patterns.length > 100) {
|
|
114
|
+
warnings.push('Plugin has over 100 patterns — consider splitting into multiple plugins.');
|
|
115
|
+
score -= 5;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Metadata checks
|
|
121
|
+
if (!manifest.description || manifest.description.length < 10) {
|
|
122
|
+
warnings.push('Description should be at least 10 characters.');
|
|
123
|
+
score -= 5;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!manifest.license) {
|
|
127
|
+
warnings.push('No license specified. Recommend MIT or Apache-2.0.');
|
|
128
|
+
score -= 5;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!manifest.tags || !Array.isArray(manifest.tags) || manifest.tags.length === 0) {
|
|
132
|
+
warnings.push('Adding tags helps with plugin discovery.');
|
|
133
|
+
score -= 3;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (manifest.testCases && Array.isArray(manifest.testCases) && manifest.testCases.length >= 3) {
|
|
137
|
+
score += 5; // Bonus for including tests
|
|
138
|
+
} else {
|
|
139
|
+
warnings.push('Including testCases improves plugin quality score.');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
score = Math.max(0, Math.min(100, score));
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
valid: errors.length === 0,
|
|
146
|
+
errors,
|
|
147
|
+
warnings,
|
|
148
|
+
score,
|
|
149
|
+
grade: score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F'
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Run test cases defined in a plugin manifest.
|
|
155
|
+
* @param {object} manifest - Plugin manifest with testCases.
|
|
156
|
+
* @returns {object} { passed: number, failed: number, results: Array }
|
|
157
|
+
*/
|
|
158
|
+
runTests(manifest) {
|
|
159
|
+
if (!manifest.testCases || !Array.isArray(manifest.testCases)) {
|
|
160
|
+
return { passed: 0, failed: 0, results: [] };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const results = [];
|
|
164
|
+
let passed = 0;
|
|
165
|
+
let failed = 0;
|
|
166
|
+
|
|
167
|
+
for (const tc of manifest.testCases) {
|
|
168
|
+
const { input, shouldDetect } = tc;
|
|
169
|
+
let detected = false;
|
|
170
|
+
|
|
171
|
+
for (const p of manifest.patterns || []) {
|
|
172
|
+
try {
|
|
173
|
+
const regex = p.regex instanceof RegExp ? p.regex : new RegExp(typeof p.regex === 'string' ? p.regex : p.pattern, 'i');
|
|
174
|
+
if (regex.test(input)) {
|
|
175
|
+
detected = true;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
} catch (e) {
|
|
179
|
+
// Invalid regex
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const pass = detected === shouldDetect;
|
|
184
|
+
if (pass) passed++;
|
|
185
|
+
else failed++;
|
|
186
|
+
|
|
187
|
+
results.push({ input: input.substring(0, 80), expected: shouldDetect, actual: detected, pass });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { passed, failed, total: results.length, results };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** @private */
|
|
194
|
+
_isReDoSRisk(regexStr) {
|
|
195
|
+
// Heuristic: nested quantifiers like (a+)+ or (a|b)* are ReDoS risks
|
|
196
|
+
return /\([^)]*[+*][^)]*\)[+*]/.test(regexStr) || /\([^)]*\|[^)]*\)[+*]{2,}/.test(regexStr);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// =========================================================================
|
|
201
|
+
// PLUGIN REGISTRY
|
|
202
|
+
// =========================================================================
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Local plugin registry. Manages installed plugins, activation, and versioning.
|
|
206
|
+
*/
|
|
207
|
+
class PluginRegistry {
|
|
208
|
+
/**
|
|
209
|
+
* @param {object} [options]
|
|
210
|
+
* @param {string} [options.pluginDir] - Directory to store/load plugins.
|
|
211
|
+
* @param {boolean} [options.autoValidate=true] - Validate plugins on registration.
|
|
212
|
+
*/
|
|
213
|
+
constructor(options = {}) {
|
|
214
|
+
this.pluginDir = options.pluginDir || null;
|
|
215
|
+
this.autoValidate = options.autoValidate !== false;
|
|
216
|
+
this._plugins = new Map();
|
|
217
|
+
this._activePlugins = new Set();
|
|
218
|
+
this._validator = new PluginValidator();
|
|
219
|
+
this._installHistory = [];
|
|
220
|
+
|
|
221
|
+
// Load plugins from directory if configured
|
|
222
|
+
if (this.pluginDir) {
|
|
223
|
+
this._loadFromDir();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log('[Agent Shield] PluginRegistry initialized (plugins: %d)', this._plugins.size);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Register a plugin.
|
|
231
|
+
* @param {object} manifest - Plugin manifest object.
|
|
232
|
+
* @returns {object} { success: boolean, validation?: object, error?: string }
|
|
233
|
+
*/
|
|
234
|
+
register(manifest) {
|
|
235
|
+
if (this.autoValidate) {
|
|
236
|
+
const validation = this._validator.validate(manifest);
|
|
237
|
+
if (!validation.valid) {
|
|
238
|
+
return { success: false, validation, error: `Validation failed: ${validation.errors.join('; ')}` };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const id = manifest.name;
|
|
243
|
+
const existing = this._plugins.get(id);
|
|
244
|
+
|
|
245
|
+
if (existing && existing.version === manifest.version) {
|
|
246
|
+
return { success: false, error: `Plugin "${id}" v${manifest.version} is already registered.` };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Compile regex patterns
|
|
250
|
+
const compiled = this._compilePatterns(manifest);
|
|
251
|
+
|
|
252
|
+
this._plugins.set(id, {
|
|
253
|
+
...manifest,
|
|
254
|
+
compiledPatterns: compiled,
|
|
255
|
+
installedAt: Date.now(),
|
|
256
|
+
checksum: this._checksum(JSON.stringify(manifest))
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
this._activePlugins.add(id);
|
|
260
|
+
this._installHistory.push({ action: 'install', plugin: id, version: manifest.version, timestamp: Date.now() });
|
|
261
|
+
|
|
262
|
+
console.log('[Agent Shield] Plugin registered: %s v%s (%d patterns)', id, manifest.version, compiled.length);
|
|
263
|
+
|
|
264
|
+
// Save to directory if configured
|
|
265
|
+
if (this.pluginDir) {
|
|
266
|
+
this._savePlugin(id, manifest);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const validation = this._validator.validate(manifest);
|
|
270
|
+
return { success: true, validation };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Unregister a plugin.
|
|
275
|
+
* @param {string} name - Plugin name.
|
|
276
|
+
* @returns {boolean}
|
|
277
|
+
*/
|
|
278
|
+
unregister(name) {
|
|
279
|
+
const removed = this._plugins.delete(name);
|
|
280
|
+
this._activePlugins.delete(name);
|
|
281
|
+
if (removed) {
|
|
282
|
+
this._installHistory.push({ action: 'uninstall', plugin: name, timestamp: Date.now() });
|
|
283
|
+
}
|
|
284
|
+
return removed;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Enable a registered plugin.
|
|
289
|
+
* @param {string} name
|
|
290
|
+
* @returns {boolean}
|
|
291
|
+
*/
|
|
292
|
+
enable(name) {
|
|
293
|
+
if (!this._plugins.has(name)) return false;
|
|
294
|
+
this._activePlugins.add(name);
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Disable a plugin (keeps it registered but inactive).
|
|
300
|
+
* @param {string} name
|
|
301
|
+
* @returns {boolean}
|
|
302
|
+
*/
|
|
303
|
+
disable(name) {
|
|
304
|
+
return this._activePlugins.delete(name);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Get a registered plugin by name.
|
|
309
|
+
* @param {string} name
|
|
310
|
+
* @returns {object|null}
|
|
311
|
+
*/
|
|
312
|
+
get(name) {
|
|
313
|
+
return this._plugins.get(name) || null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* List all registered plugins.
|
|
318
|
+
* @returns {Array<object>}
|
|
319
|
+
*/
|
|
320
|
+
list() {
|
|
321
|
+
return [...this._plugins.entries()].map(([name, plugin]) => ({
|
|
322
|
+
name,
|
|
323
|
+
version: plugin.version,
|
|
324
|
+
description: plugin.description,
|
|
325
|
+
author: plugin.author,
|
|
326
|
+
patterns: plugin.compiledPatterns ? plugin.compiledPatterns.length : 0,
|
|
327
|
+
active: this._activePlugins.has(name),
|
|
328
|
+
tags: plugin.tags || [],
|
|
329
|
+
installedAt: plugin.installedAt
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Search plugins by keyword.
|
|
335
|
+
* @param {string} query
|
|
336
|
+
* @returns {Array<object>}
|
|
337
|
+
*/
|
|
338
|
+
search(query) {
|
|
339
|
+
const lower = query.toLowerCase();
|
|
340
|
+
return this.list().filter(p =>
|
|
341
|
+
p.name.includes(lower) ||
|
|
342
|
+
p.description.toLowerCase().includes(lower) ||
|
|
343
|
+
(p.tags && p.tags.some(t => t.toLowerCase().includes(lower)))
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Scan text against all active plugin patterns.
|
|
349
|
+
* @param {string} text - Text to scan.
|
|
350
|
+
* @returns {object} { threats: Array, pluginsUsed: number }
|
|
351
|
+
*/
|
|
352
|
+
scan(text) {
|
|
353
|
+
if (!text || text.length < 5) return { threats: [], pluginsUsed: 0 };
|
|
354
|
+
|
|
355
|
+
const threats = [];
|
|
356
|
+
let pluginsUsed = 0;
|
|
357
|
+
|
|
358
|
+
for (const name of this._activePlugins) {
|
|
359
|
+
const plugin = this._plugins.get(name);
|
|
360
|
+
if (!plugin || !plugin.compiledPatterns) continue;
|
|
361
|
+
|
|
362
|
+
pluginsUsed++;
|
|
363
|
+
for (const pattern of plugin.compiledPatterns) {
|
|
364
|
+
try {
|
|
365
|
+
if (pattern.regex.test(text)) {
|
|
366
|
+
threats.push({
|
|
367
|
+
severity: pattern.severity || 'medium',
|
|
368
|
+
category: pattern.category || 'plugin_detection',
|
|
369
|
+
description: pattern.description || `Detected by plugin: ${name}`,
|
|
370
|
+
detail: `Plugin "${name}" v${plugin.version}: ${pattern.detail || pattern.description || 'Pattern matched'}`,
|
|
371
|
+
plugin: name
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
} catch (e) {
|
|
375
|
+
// Skip broken patterns
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return { threats, pluginsUsed };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get registry statistics.
|
|
385
|
+
* @returns {object}
|
|
386
|
+
*/
|
|
387
|
+
getStats() {
|
|
388
|
+
let totalPatterns = 0;
|
|
389
|
+
for (const plugin of this._plugins.values()) {
|
|
390
|
+
totalPatterns += (plugin.compiledPatterns || []).length;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
totalPlugins: this._plugins.size,
|
|
395
|
+
activePlugins: this._activePlugins.size,
|
|
396
|
+
totalPatterns,
|
|
397
|
+
installHistory: this._installHistory.length
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** @private */
|
|
402
|
+
_compilePatterns(manifest) {
|
|
403
|
+
if (!manifest.patterns || !Array.isArray(manifest.patterns)) return [];
|
|
404
|
+
|
|
405
|
+
return manifest.patterns.map(p => {
|
|
406
|
+
let regex;
|
|
407
|
+
try {
|
|
408
|
+
if (p.regex instanceof RegExp) {
|
|
409
|
+
regex = p.regex;
|
|
410
|
+
} else if (typeof p.regex === 'string') {
|
|
411
|
+
regex = new RegExp(p.regex, 'i');
|
|
412
|
+
} else if (typeof p.pattern === 'string') {
|
|
413
|
+
regex = new RegExp(p.pattern, 'i');
|
|
414
|
+
} else {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
} catch (e) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
regex,
|
|
423
|
+
severity: p.severity || 'medium',
|
|
424
|
+
category: p.category || 'plugin_detection',
|
|
425
|
+
description: p.description || '',
|
|
426
|
+
detail: p.detail || ''
|
|
427
|
+
};
|
|
428
|
+
}).filter(Boolean);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** @private */
|
|
432
|
+
_checksum(str) {
|
|
433
|
+
return crypto.createHash('sha256').update(str).digest('hex').substring(0, 16);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** @private */
|
|
437
|
+
_loadFromDir() {
|
|
438
|
+
if (!this.pluginDir || !fs.existsSync(this.pluginDir)) return;
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const files = fs.readdirSync(this.pluginDir).filter(f => f.endsWith('.json'));
|
|
442
|
+
for (const file of files) {
|
|
443
|
+
try {
|
|
444
|
+
const content = fs.readFileSync(path.join(this.pluginDir, file), 'utf-8');
|
|
445
|
+
const manifest = JSON.parse(content);
|
|
446
|
+
this.register(manifest);
|
|
447
|
+
} catch (e) {
|
|
448
|
+
console.warn('[Agent Shield] Failed to load plugin %s: %s', file, e.message);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
} catch (e) {
|
|
452
|
+
console.warn('[Agent Shield] Failed to read plugin directory: %s', e.message);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** @private */
|
|
457
|
+
_savePlugin(name, manifest) {
|
|
458
|
+
if (!this.pluginDir) return;
|
|
459
|
+
try {
|
|
460
|
+
if (!fs.existsSync(this.pluginDir)) {
|
|
461
|
+
fs.mkdirSync(this.pluginDir, { recursive: true });
|
|
462
|
+
}
|
|
463
|
+
fs.writeFileSync(
|
|
464
|
+
path.join(this.pluginDir, `${name}.json`),
|
|
465
|
+
JSON.stringify(manifest, null, 2)
|
|
466
|
+
);
|
|
467
|
+
} catch (e) {
|
|
468
|
+
console.warn('[Agent Shield] Failed to save plugin %s: %s', name, e.message);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// =========================================================================
|
|
474
|
+
// MARKETPLACE CLIENT
|
|
475
|
+
// =========================================================================
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Client for discovering and fetching plugins from a remote marketplace.
|
|
479
|
+
*/
|
|
480
|
+
class MarketplaceClient {
|
|
481
|
+
/**
|
|
482
|
+
* @param {object} [options]
|
|
483
|
+
* @param {string} [options.registryUrl] - Base URL of the plugin registry API.
|
|
484
|
+
* @param {number} [options.timeoutMs=10000] - Request timeout.
|
|
485
|
+
* @param {PluginRegistry} [options.registry] - Local registry to install into.
|
|
486
|
+
*/
|
|
487
|
+
constructor(options = {}) {
|
|
488
|
+
this.registryUrl = options.registryUrl || null;
|
|
489
|
+
this.timeoutMs = options.timeoutMs || 10000;
|
|
490
|
+
this.registry = options.registry || null;
|
|
491
|
+
this._cache = new Map();
|
|
492
|
+
|
|
493
|
+
console.log('[Agent Shield] MarketplaceClient initialized (registry: %s)', this.registryUrl || 'none');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Fetch available plugins from the marketplace.
|
|
498
|
+
* @param {object} [filters] - { category, minScore, query }
|
|
499
|
+
* @returns {Promise<Array<object>>} List of available plugins.
|
|
500
|
+
*/
|
|
501
|
+
async browse(filters = {}) {
|
|
502
|
+
if (!this.registryUrl) {
|
|
503
|
+
return { plugins: [], error: 'No registry URL configured.' };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
const url = new URL('/api/plugins', this.registryUrl);
|
|
508
|
+
if (filters.category) url.searchParams.set('category', filters.category);
|
|
509
|
+
if (filters.query) url.searchParams.set('q', filters.query);
|
|
510
|
+
if (filters.minScore) url.searchParams.set('minScore', filters.minScore);
|
|
511
|
+
|
|
512
|
+
const response = await this._fetch(url.toString());
|
|
513
|
+
return { plugins: response.plugins || response || [], error: null };
|
|
514
|
+
} catch (err) {
|
|
515
|
+
return { plugins: [], error: err.message };
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Install a plugin from the marketplace by name.
|
|
521
|
+
* @param {string} name - Plugin name.
|
|
522
|
+
* @returns {Promise<object>} { success: boolean, plugin?: object, error?: string }
|
|
523
|
+
*/
|
|
524
|
+
async install(name) {
|
|
525
|
+
if (!this.registryUrl) {
|
|
526
|
+
return { success: false, error: 'No registry URL configured.' };
|
|
527
|
+
}
|
|
528
|
+
if (!this.registry) {
|
|
529
|
+
return { success: false, error: 'No local registry configured.' };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
const url = new URL(`/api/plugins/${encodeURIComponent(name)}`, this.registryUrl);
|
|
534
|
+
const manifest = await this._fetch(url.toString());
|
|
535
|
+
|
|
536
|
+
const result = this.registry.register(manifest);
|
|
537
|
+
return result;
|
|
538
|
+
} catch (err) {
|
|
539
|
+
return { success: false, error: err.message };
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Publish a plugin to the marketplace.
|
|
545
|
+
* @param {object} manifest - Plugin manifest.
|
|
546
|
+
* @returns {Promise<object>} { success: boolean, error?: string }
|
|
547
|
+
*/
|
|
548
|
+
async publish(manifest) {
|
|
549
|
+
if (!this.registryUrl) {
|
|
550
|
+
return { success: false, error: 'No registry URL configured.' };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const validator = new PluginValidator();
|
|
554
|
+
const validation = validator.validate(manifest);
|
|
555
|
+
if (!validation.valid) {
|
|
556
|
+
return { success: false, error: `Validation failed: ${validation.errors.join('; ')}`, validation };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
const url = new URL('/api/plugins', this.registryUrl);
|
|
561
|
+
await this._post(url.toString(), manifest);
|
|
562
|
+
return { success: true, validation };
|
|
563
|
+
} catch (err) {
|
|
564
|
+
return { success: false, error: err.message };
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** @private */
|
|
569
|
+
_fetch(url) {
|
|
570
|
+
return new Promise((resolve, reject) => {
|
|
571
|
+
const parsed = new URL(url);
|
|
572
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
573
|
+
|
|
574
|
+
const req = lib.get({
|
|
575
|
+
hostname: parsed.hostname,
|
|
576
|
+
port: parsed.port,
|
|
577
|
+
path: parsed.pathname + parsed.search,
|
|
578
|
+
timeout: this.timeoutMs
|
|
579
|
+
}, (res) => {
|
|
580
|
+
let data = '';
|
|
581
|
+
res.on('data', chunk => { data += chunk; });
|
|
582
|
+
res.on('end', () => {
|
|
583
|
+
try { resolve(JSON.parse(data)); }
|
|
584
|
+
catch (e) { reject(new Error('Invalid JSON response')); }
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
req.on('error', reject);
|
|
589
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** @private */
|
|
594
|
+
_post(url, body) {
|
|
595
|
+
return new Promise((resolve, reject) => {
|
|
596
|
+
const parsed = new URL(url);
|
|
597
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
598
|
+
const payload = JSON.stringify(body);
|
|
599
|
+
|
|
600
|
+
const req = lib.request({
|
|
601
|
+
hostname: parsed.hostname,
|
|
602
|
+
port: parsed.port,
|
|
603
|
+
path: parsed.pathname,
|
|
604
|
+
method: 'POST',
|
|
605
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
|
606
|
+
timeout: this.timeoutMs
|
|
607
|
+
}, (res) => {
|
|
608
|
+
let data = '';
|
|
609
|
+
res.on('data', chunk => { data += chunk; });
|
|
610
|
+
res.on('end', () => {
|
|
611
|
+
try { resolve(JSON.parse(data)); }
|
|
612
|
+
catch (e) { reject(new Error('Invalid JSON response')); }
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
req.on('error', reject);
|
|
617
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
|
|
618
|
+
req.write(payload);
|
|
619
|
+
req.end();
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// =========================================================================
|
|
625
|
+
// EXPORTS
|
|
626
|
+
// =========================================================================
|
|
627
|
+
|
|
628
|
+
module.exports = { PluginRegistry, PluginValidator, MarketplaceClient, REQUIRED_FIELDS };
|