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
package/src/index.js
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield SDK
|
|
5
|
+
*
|
|
6
|
+
* Protects AI agents from prompt injection, data exfiltration,
|
|
7
|
+
* tool abuse, and other AI-specific attacks.
|
|
8
|
+
*
|
|
9
|
+
* All detection runs locally — no data ever leaves your environment.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const { AgentShield } = require('agent-shield');
|
|
13
|
+
*
|
|
14
|
+
* const shield = new AgentShield();
|
|
15
|
+
*
|
|
16
|
+
* // Scan any text
|
|
17
|
+
* const result = shield.scan('ignore all previous instructions');
|
|
18
|
+
* if (result.status !== 'safe') {
|
|
19
|
+
* console.log('Threat detected:', result.threats);
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* // Scan agent input before processing
|
|
23
|
+
* const inputResult = shield.scanInput(userMessage);
|
|
24
|
+
* if (inputResult.blocked) {
|
|
25
|
+
* return 'This input was blocked for safety reasons.';
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* // Scan agent output before returning to user
|
|
29
|
+
* const outputResult = shield.scanOutput(agentResponse);
|
|
30
|
+
*
|
|
31
|
+
* // Scan tool calls before execution
|
|
32
|
+
* const toolResult = shield.scanToolCall('bash', { command: 'cat /etc/passwd' });
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const { scanText, getPatterns, SEVERITY_ORDER } = require('./detector-core');
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Default configuration for AgentShield.
|
|
39
|
+
*/
|
|
40
|
+
const DEFAULT_CONFIG = {
|
|
41
|
+
/** Sensitivity level: 'low', 'medium', or 'high'. */
|
|
42
|
+
sensitivity: 'medium',
|
|
43
|
+
|
|
44
|
+
/** Whether to block inputs that reach the threshold. */
|
|
45
|
+
blockOnThreat: false,
|
|
46
|
+
|
|
47
|
+
/** Minimum severity to trigger a block: 'low', 'medium', 'high', or 'critical'. */
|
|
48
|
+
blockThreshold: 'high',
|
|
49
|
+
|
|
50
|
+
/** Whether to log scan results to console. */
|
|
51
|
+
logging: false,
|
|
52
|
+
|
|
53
|
+
/** Custom callback when a threat is detected. */
|
|
54
|
+
onThreat: null,
|
|
55
|
+
|
|
56
|
+
/** Dangerous tool names that should be scrutinized more carefully. */
|
|
57
|
+
dangerousTools: [
|
|
58
|
+
'bash', 'shell', 'terminal', 'exec', 'execute',
|
|
59
|
+
'eval', 'run_command', 'system',
|
|
60
|
+
'write_file', 'delete_file', 'remove',
|
|
61
|
+
'http_request', 'fetch', 'curl', 'wget',
|
|
62
|
+
'sql', 'query', 'database'
|
|
63
|
+
],
|
|
64
|
+
|
|
65
|
+
/** Sensitive file patterns that should never be accessed. */
|
|
66
|
+
sensitiveFilePatterns: [
|
|
67
|
+
/\.env$/i,
|
|
68
|
+
/credentials/i,
|
|
69
|
+
/secrets?\.(?:json|yaml|yml|toml)/i,
|
|
70
|
+
/private[_-]?key/i,
|
|
71
|
+
/password/i,
|
|
72
|
+
/token/i,
|
|
73
|
+
/\.pem$/i,
|
|
74
|
+
/\.key$/i,
|
|
75
|
+
/id_rsa/i,
|
|
76
|
+
/id_ed25519/i
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
class AgentShield {
|
|
81
|
+
/**
|
|
82
|
+
* Creates a new AgentShield instance.
|
|
83
|
+
* @param {object} [config] - Configuration overrides.
|
|
84
|
+
*/
|
|
85
|
+
constructor(config = {}) {
|
|
86
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
87
|
+
// Deep-merge arrays: append user items to defaults instead of replacing
|
|
88
|
+
if (config.dangerousTools) {
|
|
89
|
+
this.config.dangerousTools = [...new Set([...DEFAULT_CONFIG.dangerousTools, ...config.dangerousTools])];
|
|
90
|
+
}
|
|
91
|
+
if (config.sensitiveFilePatterns) {
|
|
92
|
+
this.config.sensitiveFilePatterns = [...DEFAULT_CONFIG.sensitiveFilePatterns, ...config.sensitiveFilePatterns];
|
|
93
|
+
}
|
|
94
|
+
this.stats = {
|
|
95
|
+
totalScans: 0,
|
|
96
|
+
threatsDetected: 0,
|
|
97
|
+
blocked: 0,
|
|
98
|
+
scanHistory: []
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Scans arbitrary text for threats.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} text - Text to scan.
|
|
106
|
+
* @param {object} [options] - Scan options.
|
|
107
|
+
* @param {string} [options.source] - Label for where the text came from.
|
|
108
|
+
* @param {string} [options.sensitivity] - Override default sensitivity.
|
|
109
|
+
* @returns {object} Scan result.
|
|
110
|
+
* @throws {TypeError} If text is not a string.
|
|
111
|
+
*/
|
|
112
|
+
scan(text, options = {}) {
|
|
113
|
+
if (typeof text !== 'string') {
|
|
114
|
+
throw new TypeError(`[Agent Shield] scan() expects a string, got ${typeof text}`);
|
|
115
|
+
}
|
|
116
|
+
if (text.length > 1_000_000) {
|
|
117
|
+
console.warn('[Agent Shield] Input exceeds 1MB — consider scanning in chunks');
|
|
118
|
+
}
|
|
119
|
+
const result = scanText(text, {
|
|
120
|
+
source: options.source || 'unknown',
|
|
121
|
+
sensitivity: options.sensitivity || this.config.sensitivity
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
this.stats.totalScans++;
|
|
125
|
+
if (result.threats.length > 0) {
|
|
126
|
+
this.stats.threatsDetected += result.threats.length;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Keep last 100 scans in history
|
|
130
|
+
this.stats.scanHistory.push({
|
|
131
|
+
timestamp: result.timestamp,
|
|
132
|
+
status: result.status,
|
|
133
|
+
threatCount: result.threats.length,
|
|
134
|
+
source: options.source || 'unknown'
|
|
135
|
+
});
|
|
136
|
+
if (this.stats.scanHistory.length > 100) {
|
|
137
|
+
this.stats.scanHistory.shift();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (this.config.logging && result.threats.length > 0) {
|
|
141
|
+
console.warn(`[Agent Shield] ${result.threats.length} threat(s) detected in ${options.source || 'unknown'}:`,
|
|
142
|
+
result.threats.map(t => `${t.severity}: ${t.description}`));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (this.config.onThreat && result.threats.length > 0) {
|
|
146
|
+
try {
|
|
147
|
+
this.config.onThreat(result);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error('[Agent Shield] onThreat callback error:', err.message);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Checks if threats meet the blocking threshold.
|
|
158
|
+
* @private
|
|
159
|
+
* @param {Array} threats
|
|
160
|
+
* @returns {boolean}
|
|
161
|
+
*/
|
|
162
|
+
_shouldBlock(threats) {
|
|
163
|
+
if (!this.config.blockOnThreat || threats.length === 0) return false;
|
|
164
|
+
const thresholdLevel = SEVERITY_ORDER[this.config.blockThreshold] ?? 1;
|
|
165
|
+
return threats.some(t => SEVERITY_ORDER[t.severity] <= thresholdLevel);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Scans text, applies blocking logic, and tracks stats.
|
|
170
|
+
* @private
|
|
171
|
+
* @param {string} text
|
|
172
|
+
* @param {string} defaultSource
|
|
173
|
+
* @param {string} logLabel
|
|
174
|
+
* @param {object} options
|
|
175
|
+
* @returns {object}
|
|
176
|
+
*/
|
|
177
|
+
_scanWithBlocking(text, defaultSource, logLabel, options = {}) {
|
|
178
|
+
const source = options.source || defaultSource;
|
|
179
|
+
const result = this.scan(text, { ...options, source });
|
|
180
|
+
|
|
181
|
+
result.blocked = this._shouldBlock(result.threats);
|
|
182
|
+
if (result.blocked) {
|
|
183
|
+
this.stats.blocked++;
|
|
184
|
+
if (this.config.logging) {
|
|
185
|
+
console.warn(`[Agent Shield] ${logLabel} BLOCKED from ${source}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Scans an agent's input (user message, API response, document, etc.)
|
|
194
|
+
* before the agent processes it.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} text - The input text.
|
|
197
|
+
* @param {object} [options] - Options.
|
|
198
|
+
* @param {string} [options.source='user_input'] - Where the input came from.
|
|
199
|
+
* @returns {object} Scan result with additional `blocked` field.
|
|
200
|
+
*/
|
|
201
|
+
scanInput(text, options = {}) {
|
|
202
|
+
if (typeof text !== 'string') {
|
|
203
|
+
throw new TypeError(`[Agent Shield] scanInput() expects a string, got ${typeof text}`);
|
|
204
|
+
}
|
|
205
|
+
return this._scanWithBlocking(text, 'user_input', 'INPUT', options);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Scans an agent's output before it's returned to the user.
|
|
210
|
+
* Catches cases where an agent has been successfully manipulated
|
|
211
|
+
* and is now producing dangerous output.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} text - The agent's output text.
|
|
214
|
+
* @param {object} [options] - Options.
|
|
215
|
+
* @param {string} [options.source='agent_output'] - Source label.
|
|
216
|
+
* @returns {object} Scan result with additional `blocked` field.
|
|
217
|
+
*/
|
|
218
|
+
scanOutput(text, options = {}) {
|
|
219
|
+
if (typeof text !== 'string') {
|
|
220
|
+
throw new TypeError(`[Agent Shield] scanOutput() expects a string, got ${typeof text}`);
|
|
221
|
+
}
|
|
222
|
+
return this._scanWithBlocking(text, 'agent_output', 'OUTPUT', options);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Scans a tool call before the agent executes it.
|
|
227
|
+
* Checks both the tool name and its arguments for threats.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} toolName - Name of the tool being called.
|
|
230
|
+
* @param {object} args - The tool's arguments.
|
|
231
|
+
* @param {object} [options] - Options.
|
|
232
|
+
* @returns {object} Scan result with `blocked` and `warnings` fields.
|
|
233
|
+
*/
|
|
234
|
+
scanToolCall(toolName, args = {}, options = {}) {
|
|
235
|
+
if (!toolName || typeof toolName !== 'string') {
|
|
236
|
+
return { status: 'safe', toolName: toolName || '', threats: [], warnings: ['Invalid tool name'], blocked: false, isDangerousTool: false, timestamp: Date.now() };
|
|
237
|
+
}
|
|
238
|
+
const warnings = [];
|
|
239
|
+
const allThreats = [];
|
|
240
|
+
|
|
241
|
+
// Check if it's a dangerous tool (exact match or word-boundary match)
|
|
242
|
+
const lowerName = toolName.toLowerCase();
|
|
243
|
+
const isDangerousTool = this.config.dangerousTools.some(
|
|
244
|
+
t => lowerName === t || lowerName.startsWith(t + '_') || lowerName.endsWith('_' + t)
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
if (isDangerousTool) {
|
|
248
|
+
warnings.push(`Tool "${toolName}" is on the dangerous tools list.`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Validate args type before processing
|
|
252
|
+
if (args !== null && typeof args !== 'object') {
|
|
253
|
+
args = {};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Scan all string arguments for injection
|
|
257
|
+
const argsText = this._flattenArgs(args);
|
|
258
|
+
if (argsText) {
|
|
259
|
+
const result = this.scan(argsText, {
|
|
260
|
+
source: `tool_call:${toolName}`,
|
|
261
|
+
...options
|
|
262
|
+
});
|
|
263
|
+
allThreats.push(...result.threats);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Check for sensitive file access
|
|
267
|
+
const fileArgs = this._extractFilePaths(args);
|
|
268
|
+
for (const filePath of fileArgs) {
|
|
269
|
+
const isSensitive = this.config.sensitiveFilePatterns.some(
|
|
270
|
+
pattern => pattern.test(filePath)
|
|
271
|
+
);
|
|
272
|
+
if (isSensitive) {
|
|
273
|
+
allThreats.push({
|
|
274
|
+
severity: 'critical',
|
|
275
|
+
category: 'data_exfiltration',
|
|
276
|
+
description: `Tool "${toolName}" is trying to access a sensitive file: ${filePath}`,
|
|
277
|
+
detail: `Sensitive file access attempt via tool call. File: ${filePath}`,
|
|
278
|
+
confidence: 90,
|
|
279
|
+
confidenceLabel: 'Almost certainly a threat'
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Determine if this should be blocked
|
|
285
|
+
let blocked = this._shouldBlock(allThreats);
|
|
286
|
+
|
|
287
|
+
// Also block dangerous tools with any threat
|
|
288
|
+
if (isDangerousTool && allThreats.length > 0) {
|
|
289
|
+
blocked = true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (blocked) {
|
|
293
|
+
this.stats.blocked++;
|
|
294
|
+
if (this.config.logging) {
|
|
295
|
+
console.warn(`[Agent Shield] TOOL CALL BLOCKED: ${toolName}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
status: allThreats.length > 0 ? 'danger' : 'safe',
|
|
301
|
+
toolName,
|
|
302
|
+
threats: allThreats,
|
|
303
|
+
warnings,
|
|
304
|
+
blocked,
|
|
305
|
+
isDangerousTool,
|
|
306
|
+
timestamp: Date.now()
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Scans multiple pieces of text in batch.
|
|
312
|
+
*
|
|
313
|
+
* @param {Array<{text: string, source?: string}>} items - Items to scan.
|
|
314
|
+
* @returns {object} Combined result with per-item results.
|
|
315
|
+
*/
|
|
316
|
+
scanBatch(items) {
|
|
317
|
+
if (!Array.isArray(items)) {
|
|
318
|
+
throw new TypeError(`[Agent Shield] scanBatch() expects an array, got ${typeof items}`);
|
|
319
|
+
}
|
|
320
|
+
const results = items.map(item =>
|
|
321
|
+
this.scan(item.text, { source: item.source || 'batch' })
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const allThreats = results.flatMap(r => r.threats);
|
|
325
|
+
let worstStatus = 'safe';
|
|
326
|
+
for (const r of results) {
|
|
327
|
+
if (r.status === 'danger') { worstStatus = 'danger'; break; }
|
|
328
|
+
if (r.status === 'warning' && worstStatus !== 'danger') worstStatus = 'warning';
|
|
329
|
+
if (r.status === 'caution' && worstStatus === 'safe') worstStatus = 'caution';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
status: worstStatus,
|
|
334
|
+
results,
|
|
335
|
+
totalThreats: allThreats.length,
|
|
336
|
+
timestamp: Date.now()
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Returns the current scan statistics.
|
|
342
|
+
* @returns {object}
|
|
343
|
+
*/
|
|
344
|
+
getStats() {
|
|
345
|
+
return { ...this.stats };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Resets scan statistics.
|
|
350
|
+
*/
|
|
351
|
+
resetStats() {
|
|
352
|
+
this.stats = {
|
|
353
|
+
totalScans: 0,
|
|
354
|
+
threatsDetected: 0,
|
|
355
|
+
blocked: 0,
|
|
356
|
+
scanHistory: []
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Returns all detection patterns the engine uses.
|
|
362
|
+
* @returns {Array}
|
|
363
|
+
*/
|
|
364
|
+
getPatterns() {
|
|
365
|
+
return getPatterns();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Flattens tool arguments into a single string for scanning.
|
|
370
|
+
* @private
|
|
371
|
+
* @param {object} args
|
|
372
|
+
* @returns {string}
|
|
373
|
+
*/
|
|
374
|
+
_flattenArgs(args, maxDepth = 10) {
|
|
375
|
+
const parts = [];
|
|
376
|
+
const flatten = (obj, depth) => {
|
|
377
|
+
if (depth > maxDepth) return;
|
|
378
|
+
if (typeof obj === 'string') {
|
|
379
|
+
parts.push(obj);
|
|
380
|
+
} else if (Array.isArray(obj)) {
|
|
381
|
+
obj.forEach(item => flatten(item, depth + 1));
|
|
382
|
+
} else if (obj && typeof obj === 'object') {
|
|
383
|
+
Object.values(obj).forEach(val => flatten(val, depth + 1));
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
flatten(args, 0);
|
|
387
|
+
return parts.join(' ');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Extracts file paths from tool arguments.
|
|
392
|
+
* @private
|
|
393
|
+
* @param {object} args
|
|
394
|
+
* @returns {Array<string>}
|
|
395
|
+
*/
|
|
396
|
+
_extractFilePaths(args, maxDepth = 10) {
|
|
397
|
+
const paths = [];
|
|
398
|
+
const fileKeys = [
|
|
399
|
+
'file', 'path', 'file_path', 'filepath', 'filename', 'target',
|
|
400
|
+
'destination', 'src', 'dest', 'source', 'dir', 'directory',
|
|
401
|
+
'folder', 'location', 'output', 'input', 'module',
|
|
402
|
+
'bucket', 'table', 'url', 'uri', 'endpoint'
|
|
403
|
+
];
|
|
404
|
+
|
|
405
|
+
/** Normalize key to lowercase with separators removed for flexible matching. */
|
|
406
|
+
const normalizeKey = (key) => key.toLowerCase().replace(/[-_]/g, '');
|
|
407
|
+
const normalizedFileKeys = fileKeys.map(normalizeKey);
|
|
408
|
+
|
|
409
|
+
const extract = (obj, depth) => {
|
|
410
|
+
if (!obj || typeof obj !== 'object' || depth > maxDepth) return;
|
|
411
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
412
|
+
if (typeof value === 'string') {
|
|
413
|
+
// Match by key name (supports camelCase, snake_case, kebab-case)
|
|
414
|
+
if (normalizedFileKeys.includes(normalizeKey(key))) {
|
|
415
|
+
paths.push(value);
|
|
416
|
+
// Match by path-like value patterns
|
|
417
|
+
} else if (value.startsWith('/') || value.startsWith('./') || value.startsWith('../') || /^[A-Z]:\\/.test(value)) {
|
|
418
|
+
paths.push(value);
|
|
419
|
+
}
|
|
420
|
+
} else if (typeof value === 'object') {
|
|
421
|
+
extract(value, depth + 1);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
extract(args, 0);
|
|
426
|
+
return paths;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
module.exports = { AgentShield };
|