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/encoding.js
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Advanced Encoding Detection: Steganographic Detection (#37),
|
|
5
|
+
* Encoding Bruteforce Detection (#14), Indirect Injection via
|
|
6
|
+
* Structured Data (#15)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { scanText } = require('./detector-core');
|
|
10
|
+
|
|
11
|
+
// =========================================================================
|
|
12
|
+
// STEGANOGRAPHIC DETECTION
|
|
13
|
+
// =========================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Patterns that indicate steganographic hiding techniques.
|
|
17
|
+
*/
|
|
18
|
+
const STEGO_PATTERNS = {
|
|
19
|
+
// Unicode direction markers used to hide text
|
|
20
|
+
bidi_override: /[\u200E\u200F\u202A-\u202E\u2066-\u2069]/g,
|
|
21
|
+
|
|
22
|
+
// Invisible Unicode characters beyond zero-width (excludes common whitespace)
|
|
23
|
+
invisible_chars: /[\u00AD\u034F\u061C\u115F\u1160\u17B4\u17B5\u180E\u200B-\u200F\u2060-\u2064\u2066-\u206F\uFEFF\uFFF9-\uFFFB]/g,
|
|
24
|
+
|
|
25
|
+
// Whitespace variations used as binary encoding
|
|
26
|
+
whitespace_encoding: /[\u0009\u000A\u000B\u000C\u000D\u0020\u0085\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]/g,
|
|
27
|
+
|
|
28
|
+
// Tag characters (Unicode Tags block U+E0001–U+E007F) — can encode hidden text
|
|
29
|
+
tag_characters: /(?:\uDB40[\uDC01-\uDC7F])/g,
|
|
30
|
+
|
|
31
|
+
// Variation selectors (can modify rendering without visible change)
|
|
32
|
+
variation_selectors: /[\uFE00-\uFE0F]|(?:\uDB40[\uDD00-\uDDEF])/g
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
class SteganographyDetector {
|
|
36
|
+
/**
|
|
37
|
+
* @param {object} [options]
|
|
38
|
+
* @param {Function} [options.onDetection] - Callback on steganographic content found.
|
|
39
|
+
*/
|
|
40
|
+
constructor(options = {}) {
|
|
41
|
+
this.onDetection = options.onDetection || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Scans text for steganographic hiding techniques.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} text
|
|
48
|
+
* @returns {object} { found: boolean, techniques: Array, cleaned: string }
|
|
49
|
+
*/
|
|
50
|
+
scan(text) {
|
|
51
|
+
if (!text) return { found: false, techniques: [], cleaned: text };
|
|
52
|
+
|
|
53
|
+
const techniques = [];
|
|
54
|
+
let cleaned = text;
|
|
55
|
+
|
|
56
|
+
// Check for bidirectional override characters
|
|
57
|
+
const bidiMatches = text.match(STEGO_PATTERNS.bidi_override);
|
|
58
|
+
if (bidiMatches && bidiMatches.length > 0) {
|
|
59
|
+
techniques.push({
|
|
60
|
+
type: 'bidi_override',
|
|
61
|
+
count: bidiMatches.length,
|
|
62
|
+
severity: 'high',
|
|
63
|
+
description: `${bidiMatches.length} bidirectional override character(s) found. Text direction may be manipulated to hide content.`
|
|
64
|
+
});
|
|
65
|
+
cleaned = cleaned.replace(STEGO_PATTERNS.bidi_override, '');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check for invisible characters
|
|
69
|
+
const invisibleMatches = text.match(STEGO_PATTERNS.invisible_chars);
|
|
70
|
+
if (invisibleMatches && invisibleMatches.length > 3) {
|
|
71
|
+
techniques.push({
|
|
72
|
+
type: 'invisible_chars',
|
|
73
|
+
count: invisibleMatches.length,
|
|
74
|
+
severity: 'medium',
|
|
75
|
+
description: `${invisibleMatches.length} invisible Unicode characters found. May encode hidden messages.`
|
|
76
|
+
});
|
|
77
|
+
cleaned = cleaned.replace(STEGO_PATTERNS.invisible_chars, '');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check for suspicious whitespace patterns (potential binary encoding)
|
|
81
|
+
const words = text.split(/\S+/);
|
|
82
|
+
const spaceCounts = words.map(w => w.length).filter(l => l > 0);
|
|
83
|
+
if (spaceCounts.length > 10) {
|
|
84
|
+
const uniqueSpacings = new Set(spaceCounts);
|
|
85
|
+
// Binary encoding would show exactly 2 space widths
|
|
86
|
+
if (uniqueSpacings.size === 2 && spaceCounts.length > 20) {
|
|
87
|
+
techniques.push({
|
|
88
|
+
type: 'whitespace_binary',
|
|
89
|
+
severity: 'high',
|
|
90
|
+
description: 'Suspicious binary whitespace pattern detected. Spaces may encode hidden data.'
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check for Unicode tag characters
|
|
96
|
+
const tagMatches = text.match(STEGO_PATTERNS.tag_characters);
|
|
97
|
+
if (tagMatches && tagMatches.length > 0) {
|
|
98
|
+
techniques.push({
|
|
99
|
+
type: 'tag_characters',
|
|
100
|
+
count: tagMatches.length,
|
|
101
|
+
severity: 'critical',
|
|
102
|
+
description: `${tagMatches.length} Unicode tag character(s) found. These can encode entire hidden messages.`
|
|
103
|
+
});
|
|
104
|
+
cleaned = cleaned.replace(STEGO_PATTERNS.tag_characters, '');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// After cleaning, re-scan for injections that were hidden
|
|
108
|
+
if (techniques.length > 0 && cleaned !== text) {
|
|
109
|
+
const cleanedResult = scanText(cleaned, { source: 'stego_cleaned', sensitivity: 'high' });
|
|
110
|
+
if (cleanedResult.threats.length > 0) {
|
|
111
|
+
techniques.push({
|
|
112
|
+
type: 'hidden_injection',
|
|
113
|
+
severity: 'critical',
|
|
114
|
+
description: 'After removing steganographic characters, injection patterns were revealed.',
|
|
115
|
+
hiddenThreats: cleanedResult.threats
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (techniques.length > 0 && this.onDetection) {
|
|
121
|
+
this.onDetection({ techniques, timestamp: Date.now() });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { found: techniques.length > 0, techniques, cleaned };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// =========================================================================
|
|
129
|
+
// ENCODING BRUTEFORCE DETECTOR
|
|
130
|
+
// =========================================================================
|
|
131
|
+
|
|
132
|
+
class EncodingBruteforceDetector {
|
|
133
|
+
/**
|
|
134
|
+
* Detects rapid-fire attempts with different encodings.
|
|
135
|
+
*
|
|
136
|
+
* @param {object} [options]
|
|
137
|
+
* @param {number} [options.windowMs=60000] - Time window.
|
|
138
|
+
* @param {number} [options.threshold=5] - Number of encoded inputs to flag as bruteforce.
|
|
139
|
+
* @param {Function} [options.onDetection] - Callback on bruteforce detected.
|
|
140
|
+
*/
|
|
141
|
+
constructor(options = {}) {
|
|
142
|
+
this.windowMs = options.windowMs || 60000;
|
|
143
|
+
this.threshold = options.threshold || 5;
|
|
144
|
+
this.onDetection = options.onDetection || null;
|
|
145
|
+
this.encodedInputs = [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Checks an input for encoding and tracks frequency.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} text
|
|
152
|
+
* @returns {object} { encoded: boolean, encodingType: string|null, bruteforce: boolean, count: number }
|
|
153
|
+
*/
|
|
154
|
+
check(text) {
|
|
155
|
+
if (!text) return { encoded: false, encodingType: null, bruteforce: false, count: 0 };
|
|
156
|
+
|
|
157
|
+
const encoding = this._detectEncoding(text);
|
|
158
|
+
|
|
159
|
+
if (encoding) {
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
this.encodedInputs.push({ type: encoding, timestamp: now });
|
|
162
|
+
|
|
163
|
+
// Prune old entries
|
|
164
|
+
const cutoff = now - this.windowMs;
|
|
165
|
+
this.encodedInputs = this.encodedInputs.filter(e => e.timestamp > cutoff);
|
|
166
|
+
|
|
167
|
+
const isBruteforce = this.encodedInputs.length >= this.threshold;
|
|
168
|
+
|
|
169
|
+
if (isBruteforce && this.onDetection) {
|
|
170
|
+
const typeCounts = {};
|
|
171
|
+
for (const e of this.encodedInputs) {
|
|
172
|
+
typeCounts[e.type] = (typeCounts[e.type] || 0) + 1;
|
|
173
|
+
}
|
|
174
|
+
this.onDetection({
|
|
175
|
+
count: this.encodedInputs.length,
|
|
176
|
+
types: typeCounts,
|
|
177
|
+
timestamp: now
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
encoded: true,
|
|
183
|
+
encodingType: encoding,
|
|
184
|
+
bruteforce: isBruteforce,
|
|
185
|
+
count: this.encodedInputs.length
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { encoded: false, encodingType: null, bruteforce: false, count: this.encodedInputs.length };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** @private */
|
|
193
|
+
_detectEncoding(text) {
|
|
194
|
+
// Base64
|
|
195
|
+
if (/^[A-Za-z0-9+/]{20,}={0,2}$/.test(text.trim())) return 'base64';
|
|
196
|
+
|
|
197
|
+
// Hex encoding
|
|
198
|
+
if (/^(?:0x)?[0-9a-fA-F]{20,}$/i.test(text.trim())) return 'hex';
|
|
199
|
+
if (/(?:\\x[0-9a-fA-F]{2}){5,}/.test(text)) return 'hex_escape';
|
|
200
|
+
|
|
201
|
+
// URL encoding (high density)
|
|
202
|
+
const pctCount = (text.match(/%[0-9a-fA-F]{2}/g) || []).length;
|
|
203
|
+
if (pctCount > 5 && pctCount / text.length > 0.1) return 'url_encoding';
|
|
204
|
+
|
|
205
|
+
// HTML entities (high density)
|
|
206
|
+
const entityCount = (text.match(/&#\w+;/g) || []).length;
|
|
207
|
+
if (entityCount > 5) return 'html_entities';
|
|
208
|
+
|
|
209
|
+
// Unicode escapes
|
|
210
|
+
if (/(?:\\u[0-9a-fA-F]{4}){3,}/.test(text)) return 'unicode_escape';
|
|
211
|
+
|
|
212
|
+
// ROT13 heuristic (text looks like garbled English)
|
|
213
|
+
if (this._looksLikeRot13(text)) return 'rot13';
|
|
214
|
+
|
|
215
|
+
// Morse code
|
|
216
|
+
if (/^[\s./-]{20,}$/.test(text.trim()) && /\.{1,3}/.test(text) && /-{1,3}/.test(text)) return 'morse';
|
|
217
|
+
|
|
218
|
+
// Binary
|
|
219
|
+
if (/^[01\s]{20,}$/.test(text.trim()) && text.replace(/\s/g, '').length % 8 === 0) return 'binary';
|
|
220
|
+
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** @private */
|
|
225
|
+
_looksLikeRot13(text) {
|
|
226
|
+
if (text.length < 20 || !/^[a-zA-Z\s.,!?]+$/.test(text)) return false;
|
|
227
|
+
|
|
228
|
+
// Decode ROT13 and check if result has common English words
|
|
229
|
+
const decoded = text.replace(/[a-zA-Z]/g, c => {
|
|
230
|
+
const base = c <= 'Z' ? 65 : 97;
|
|
231
|
+
return String.fromCharCode(((c.charCodeAt(0) - base + 13) % 26) + base);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const commonWords = /\b(?:the|and|for|are|but|not|you|all|can|had|her|was|one|our|ignore|previous|instructions|system|forget|override)\b/i;
|
|
235
|
+
return commonWords.test(decoded) && !commonWords.test(text);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
reset() {
|
|
239
|
+
this.encodedInputs = [];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// =========================================================================
|
|
244
|
+
// STRUCTURED DATA INJECTION SCANNER
|
|
245
|
+
// =========================================================================
|
|
246
|
+
|
|
247
|
+
class StructuredDataScanner {
|
|
248
|
+
/**
|
|
249
|
+
* Scans JSON, XML, YAML, CSV, and other structured data for injections.
|
|
250
|
+
*
|
|
251
|
+
* @param {object} [options]
|
|
252
|
+
* @param {Function} [options.onDetection] - Callback on injection found.
|
|
253
|
+
*/
|
|
254
|
+
constructor(options = {}) {
|
|
255
|
+
this.onDetection = options.onDetection || null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Scans a JSON object for injection patterns in string values.
|
|
260
|
+
*
|
|
261
|
+
* @param {object|string} data - JSON object or JSON string.
|
|
262
|
+
* @param {string} [source='json_data'] - Source label.
|
|
263
|
+
* @returns {object} { clean: boolean, threats: Array }
|
|
264
|
+
*/
|
|
265
|
+
scanJSON(data, source = 'json_data') {
|
|
266
|
+
let obj = data;
|
|
267
|
+
if (typeof data === 'string') {
|
|
268
|
+
try { obj = JSON.parse(data); } catch (e) { return { clean: true, threats: [] }; }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const strings = this._extractStrings(obj);
|
|
272
|
+
return this._scanStrings(strings, source);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Scans XML/HTML-like text for injections in attributes and content.
|
|
277
|
+
*
|
|
278
|
+
* @param {string} xml
|
|
279
|
+
* @param {string} [source='xml_data']
|
|
280
|
+
* @returns {object} { clean: boolean, threats: Array }
|
|
281
|
+
*/
|
|
282
|
+
scanXML(xml, source = 'xml_data') {
|
|
283
|
+
if (!xml) return { clean: true, threats: [] };
|
|
284
|
+
|
|
285
|
+
const strings = [];
|
|
286
|
+
|
|
287
|
+
// Extract attribute values
|
|
288
|
+
const attrRegex = /\w+\s*=\s*["']([^"']+)["']/g;
|
|
289
|
+
let match;
|
|
290
|
+
while ((match = attrRegex.exec(xml)) !== null) {
|
|
291
|
+
strings.push({ value: match[1], path: `attribute:${match[0].split('=')[0].trim()}` });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Extract text content between tags
|
|
295
|
+
const contentRegex = />([^<]+)</g;
|
|
296
|
+
while ((match = contentRegex.exec(xml)) !== null) {
|
|
297
|
+
const text = match[1].trim();
|
|
298
|
+
if (text.length > 5) {
|
|
299
|
+
strings.push({ value: text, path: 'text_content' });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Extract CDATA sections
|
|
304
|
+
const cdataRegex = /<!\[CDATA\[([\s\S]*?)\]\]>/g;
|
|
305
|
+
while ((match = cdataRegex.exec(xml)) !== null) {
|
|
306
|
+
strings.push({ value: match[1], path: 'cdata' });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return this._scanStrings(strings, source);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Scans CSV data for injections.
|
|
314
|
+
*
|
|
315
|
+
* @param {string} csv
|
|
316
|
+
* @param {string} [source='csv_data']
|
|
317
|
+
* @returns {object} { clean: boolean, threats: Array }
|
|
318
|
+
*/
|
|
319
|
+
scanCSV(csv, source = 'csv_data') {
|
|
320
|
+
if (!csv) return { clean: true, threats: [] };
|
|
321
|
+
|
|
322
|
+
const strings = [];
|
|
323
|
+
const lines = csv.split('\n');
|
|
324
|
+
|
|
325
|
+
for (let i = 0; i < lines.length; i++) {
|
|
326
|
+
const cells = lines[i].split(',').map(c => c.trim().replace(/^["']|["']$/g, ''));
|
|
327
|
+
for (let j = 0; j < cells.length; j++) {
|
|
328
|
+
if (cells[j].length > 5) {
|
|
329
|
+
strings.push({ value: cells[j], path: `row${i + 1}:col${j + 1}` });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return this._scanStrings(strings, source);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Scans Markdown for injections in various elements.
|
|
339
|
+
*
|
|
340
|
+
* @param {string} markdown
|
|
341
|
+
* @param {string} [source='markdown']
|
|
342
|
+
* @returns {object} { clean: boolean, threats: Array }
|
|
343
|
+
*/
|
|
344
|
+
scanMarkdown(markdown, source = 'markdown') {
|
|
345
|
+
if (!markdown) return { clean: true, threats: [] };
|
|
346
|
+
|
|
347
|
+
// Scan the full markdown text
|
|
348
|
+
const result = scanText(markdown, { source, sensitivity: 'high' });
|
|
349
|
+
|
|
350
|
+
// Additionally check for suspicious elements
|
|
351
|
+
const strings = [];
|
|
352
|
+
|
|
353
|
+
// Link text and URLs
|
|
354
|
+
const linkRegex = /\[([^\]]*)\]\(([^)]*)\)/g;
|
|
355
|
+
let match;
|
|
356
|
+
while ((match = linkRegex.exec(markdown)) !== null) {
|
|
357
|
+
strings.push({ value: match[1], path: 'link_text' });
|
|
358
|
+
strings.push({ value: match[2], path: 'link_url' });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Image alt text
|
|
362
|
+
const imgRegex = /!\[([^\]]*)\]\(([^)]*)\)/g;
|
|
363
|
+
while ((match = imgRegex.exec(markdown)) !== null) {
|
|
364
|
+
strings.push({ value: match[1], path: 'image_alt' });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// HTML comments in markdown
|
|
368
|
+
const commentRegex = /<!--([\s\S]*?)-->/g;
|
|
369
|
+
while ((match = commentRegex.exec(markdown)) !== null) {
|
|
370
|
+
strings.push({ value: match[1], path: 'html_comment' });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const structuredResult = this._scanStrings(strings, source);
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
clean: result.threats.length === 0 && structuredResult.clean,
|
|
377
|
+
threats: [...result.threats, ...structuredResult.threats]
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** @private */
|
|
382
|
+
_extractStrings(obj, path = '', depth = 0) {
|
|
383
|
+
const strings = [];
|
|
384
|
+
if (depth > 10) return strings;
|
|
385
|
+
|
|
386
|
+
if (typeof obj === 'string' && obj.length > 5) {
|
|
387
|
+
strings.push({ value: obj, path: path || 'root' });
|
|
388
|
+
} else if (Array.isArray(obj)) {
|
|
389
|
+
obj.forEach((item, i) => {
|
|
390
|
+
strings.push(...this._extractStrings(item, `${path}[${i}]`, depth + 1));
|
|
391
|
+
});
|
|
392
|
+
} else if (obj && typeof obj === 'object') {
|
|
393
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
394
|
+
strings.push(...this._extractStrings(value, path ? `${path}.${key}` : key, depth + 1));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return strings;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** @private */
|
|
402
|
+
_scanStrings(strings, source) {
|
|
403
|
+
const threats = [];
|
|
404
|
+
|
|
405
|
+
for (const { value, path } of strings) {
|
|
406
|
+
const result = scanText(value, { source: `${source}:${path}`, sensitivity: 'high' });
|
|
407
|
+
if (result.threats.length > 0) {
|
|
408
|
+
threats.push(...result.threats.map(t => ({
|
|
409
|
+
...t,
|
|
410
|
+
dataPath: path,
|
|
411
|
+
description: `${t.description} (found in structured data at ${path})`
|
|
412
|
+
})));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (threats.length > 0 && this.onDetection) {
|
|
417
|
+
this.onDetection({ threats, source, timestamp: Date.now() });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return { clean: threats.length === 0, threats };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
module.exports = {
|
|
425
|
+
SteganographyDetector,
|
|
426
|
+
EncodingBruteforceDetector,
|
|
427
|
+
StructuredDataScanner,
|
|
428
|
+
STEGO_PATTERNS
|
|
429
|
+
};
|