agentshield-sdk 13.2.0 → 13.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentshield-sdk",
3
- "version": "13.2.0",
3
+ "version": "13.3.0",
4
4
  "description": "SOTA AI agent security SDK. F1 1.000 on BIPIA/HackAPrompt/MCPTox/Multilingual benchmarks. 400+ exports, 100+ modules. Zero dependencies, runs locally.",
5
5
  "main": "src/main.js",
6
6
  "types": "types/index.d.ts",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "sideEffects": false,
34
34
  "scripts": {
35
- "test": "node test/test.js && node test/test-modules.js && node test/test-new-features.js && node test/test-mcp-guard.js && node test/test-supply-chain-scanner.js && node test/test-owasp-agentic.js && node test/test-redteam-cli.js && node test/test-drift-monitor.js && node test/test-micro-model.js && node test/test-level5.js && node test/test-sota.js && node test/test-cross-turn.js && node test/test-v12.js && node test/test-traps.js && node test/test-deepmind.js",
35
+ "test": "node test/test.js && node test/test-modules.js && node test/test-new-features.js && node test/test-mcp-guard.js && node test/test-supply-chain-scanner.js && node test/test-owasp-agentic.js && node test/test-redteam-cli.js && node test/test-drift-monitor.js && node test/test-micro-model.js && node test/test-level5.js && node test/test-sota.js && node test/test-cross-turn.js && node test/test-v12.js && node test/test-traps.js && node test/test-deepmind.js && node test/test-render-differential.js && node test/test-sybil.js && node test/test-side-channel.js",
36
36
  "test:new-products": "node test/test-mcp-guard.js && node test/test-supply-chain-scanner.js && node test/test-owasp-agentic.js && node test/test-redteam-cli.js && node test/test-drift-monitor.js && node test/test-micro-model.js",
37
37
  "test:all": "node test/test-all-40-features.js",
38
38
  "test:mcp": "node test/test-mcp-security.js",
package/src/main.js CHANGED
@@ -215,6 +215,9 @@ const { BehavioralDNA, AgentProfiler, extractFeatures: extractBehavioralFeatures
215
215
  // v7.4 — Compliance Certification Authority (loaded when available)
216
216
  const { ComplianceCertificateAuthority, ComplianceReport: ComplianceCertReport, ComplianceScheduler, AUTHORITY_FRAMEWORKS, CAPABILITY_MAP: CA_CAPABILITY_MAP, CERTIFICATE_LEVELS: CA_CERTIFICATE_LEVELS } = safeRequire('./compliance-authority', 'compliance-authority');
217
217
 
218
+ // Side Channel Monitor
219
+ const { SideChannelMonitor, BeaconDetector, EntropyAnalyzer: SCEntropyAnalyzer } = safeRequire('./side-channel-monitor', 'side-channel-monitor');
220
+
218
221
  // --- v1.2 Modules ---
219
222
 
220
223
  // Semantic Detection
@@ -407,6 +410,12 @@ const { SemanticGuard, AuthoritativeClaimDetector, BiasDetector: SemanticBiasDet
407
410
  // v13.0 — Memory Trap Defenses (Trap 3)
408
411
  const { MemoryGuard, MemoryIntegrityMonitor, RAGIngestionScanner, MemoryIsolationEnforcer, RetrievalAnomalyDetector, INSTRUCTION_INDICATORS } = safeRequire('./memory-guard', 'memory-guard');
409
412
 
413
+ // v13.3 — Render Differential Analyzer
414
+ const { RenderDifferentialAnalyzer, VisualHasher } = safeRequire('./render-differential', 'render-differential');
415
+
416
+ // v13.3 — Sybil Detector
417
+ const { SybilDetector, AgentIdentityVerifier } = safeRequire('./sybil-detector', 'sybil-detector');
418
+
410
419
  // Build exports, filtering out undefined values from failed imports
411
420
  const _exports = {
412
421
  // Core
@@ -1148,6 +1157,19 @@ const _exports = {
1148
1157
  AUTHORITY_FRAMEWORKS,
1149
1158
  CA_CAPABILITY_MAP,
1150
1159
  CA_CERTIFICATE_LEVELS,
1160
+
1161
+ // Side Channel Monitor
1162
+ SideChannelMonitor,
1163
+ BeaconDetector,
1164
+ SCEntropyAnalyzer,
1165
+
1166
+ // Render Differential Analyzer
1167
+ RenderDifferentialAnalyzer,
1168
+ VisualHasher,
1169
+
1170
+ // Sybil Detector
1171
+ SybilDetector,
1172
+ AgentIdentityVerifier,
1151
1173
  };
1152
1174
 
1153
1175
  // Filter out undefined exports (from modules that failed to load)
@@ -0,0 +1,608 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Render Differential Analyzer
5
+ *
6
+ * Detects content that renders differently than it reads — visual deception
7
+ * attacks where attackers hide malicious instructions in LaTeX, Markdown,
8
+ * HTML, or other markup that looks benign in raw text but renders as
9
+ * something dangerous.
10
+ *
11
+ * All processing runs locally — no data ever leaves your environment.
12
+ */
13
+
14
+ const crypto = require('crypto');
15
+
16
+ // =========================================================================
17
+ // Constants
18
+ // =========================================================================
19
+
20
+ const SEVERITY = Object.freeze({
21
+ CRITICAL: 'critical',
22
+ HIGH: 'high',
23
+ MEDIUM: 'medium',
24
+ LOW: 'low'
25
+ });
26
+
27
+ // =========================================================================
28
+ // Markdown deception patterns
29
+ // =========================================================================
30
+
31
+ const MARKDOWN_PATTERNS = [
32
+ // HTML tags that hide content in markdown rendering
33
+ {
34
+ regex: /<span[^>]*style\s*=\s*["'][^"']*(?:display\s*:\s*none|font-size\s*:\s*0|visibility\s*:\s*hidden|opacity\s*:\s*0)[^"']*["'][^>]*>[\s\S]*?<\/span>/gi,
35
+ type: 'markdown_hidden_span',
36
+ description: 'Hidden content via inline HTML span with concealing CSS',
37
+ severity: SEVERITY.CRITICAL
38
+ },
39
+ {
40
+ regex: /<div[^>]*style\s*=\s*["'][^"']*(?:display\s*:\s*none|font-size\s*:\s*0|visibility\s*:\s*hidden|opacity\s*:\s*0)[^"']*["'][^>]*>[\s\S]*?<\/div>/gi,
41
+ type: 'markdown_hidden_div',
42
+ description: 'Hidden content via inline HTML div with concealing CSS',
43
+ severity: SEVERITY.CRITICAL
44
+ },
45
+ // Links where display text differs substantially from URL
46
+ {
47
+ regex: /\[([^\]]+)\]\(([^)]+)\)/g,
48
+ type: 'markdown_deceptive_link',
49
+ description: 'Link display text differs from URL target',
50
+ severity: SEVERITY.HIGH,
51
+ validator: (match, displayText, url) => {
52
+ // Flag if display text looks like a URL but differs from actual URL
53
+ const displayLooksLikeUrl = /^https?:\/\//.test(displayText.trim());
54
+ if (!displayLooksLikeUrl) return false;
55
+ const displayDomain = extractDomain(displayText.trim());
56
+ const urlDomain = extractDomain(url.trim());
57
+ return displayDomain && urlDomain && displayDomain !== urlDomain;
58
+ }
59
+ },
60
+ // Image alt text with injection payload
61
+ {
62
+ regex: /!\[([^\]]{50,})\]\([^)]*\)/g,
63
+ type: 'markdown_image_alt_injection',
64
+ description: 'Suspiciously long image alt text may contain injected instructions',
65
+ severity: SEVERITY.MEDIUM,
66
+ validator: (match, altText) => {
67
+ const injectionHints = /(?:ignore|system|prompt|instruction|execute|eval|admin|override)/i;
68
+ return injectionHints.test(altText);
69
+ }
70
+ },
71
+ // HTML comments hiding content
72
+ {
73
+ regex: /<!--[\s\S]*?-->/g,
74
+ type: 'markdown_comment_hiding',
75
+ description: 'HTML comment may hide instructions invisible when rendered',
76
+ severity: SEVERITY.MEDIUM,
77
+ validator: (match) => {
78
+ const content = match.replace(/^<!--/, '').replace(/-->$/, '').trim();
79
+ if (content.length < 10) return false;
80
+ const injectionHints = /(?:ignore|system|prompt|instruction|execute|override|inject|admin)/i;
81
+ return injectionHints.test(content);
82
+ }
83
+ },
84
+ // Zero-width characters in markdown
85
+ {
86
+ regex: /[\u200B\u200C\u200D\u2060\uFEFF]{2,}/g,
87
+ type: 'markdown_zero_width',
88
+ description: 'Zero-width characters hiding content between visible text',
89
+ severity: SEVERITY.HIGH
90
+ },
91
+ // Tiny text via HTML sup/sub abuse with small font
92
+ {
93
+ regex: /<(?:sup|sub)[^>]*style\s*=\s*["'][^"']*font-size\s*:\s*(?:0|1px|0\.[\d]+px)[^"']*["'][^>]*>[\s\S]*?<\/(?:sup|sub)>/gi,
94
+ type: 'markdown_tiny_text',
95
+ description: 'Extremely small text hidden via HTML sup/sub tags',
96
+ severity: SEVERITY.HIGH
97
+ }
98
+ ];
99
+
100
+ // =========================================================================
101
+ // HTML deception patterns
102
+ // =========================================================================
103
+
104
+ const HTML_PATTERNS = [
105
+ // display:none with content
106
+ {
107
+ regex: /<[^>]+style\s*=\s*["'][^"']*display\s*:\s*none[^"']*["'][^>]*>([\s\S]*?)<\/[^>]+>/gi,
108
+ type: 'html_display_none',
109
+ description: 'Content hidden with CSS display:none',
110
+ severity: SEVERITY.CRITICAL,
111
+ validator: (_match, content) => content && content.trim().length > 0
112
+ },
113
+ // font-size:0 hiding
114
+ {
115
+ regex: /<[^>]+style\s*=\s*["'][^"']*font-size\s*:\s*0(?:px|em|rem|%)?\s*[;"'][^>]*>([\s\S]*?)<\/[^>]+>/gi,
116
+ type: 'html_zero_font',
117
+ description: 'Content hidden with zero-size font',
118
+ severity: SEVERITY.CRITICAL,
119
+ validator: (_match, content) => content && content.trim().length > 0
120
+ },
121
+ // Same-color text on background
122
+ {
123
+ regex: /<[^>]+style\s*=\s*["'][^"']*color\s*:\s*(white|#fff(?:fff)?|rgba?\(\s*255\s*,\s*255\s*,\s*255[\s\S]*?\))[^"']*["'][^>]*>([\s\S]*?)<\/[^>]+>/gi,
124
+ type: 'html_same_color',
125
+ description: 'Text color matches background making content invisible',
126
+ severity: SEVERITY.HIGH,
127
+ validator: (_match, _color, content) => content && content.trim().length > 0
128
+ },
129
+ // overflow:hidden with larger content
130
+ {
131
+ regex: /<[^>]+style\s*=\s*["'][^"']*overflow\s*:\s*hidden[^"']*(?:height\s*:\s*0|max-height\s*:\s*0|width\s*:\s*0|max-width\s*:\s*0)[^"']*["'][^>]*>([\s\S]*?)<\/[^>]+>/gi,
132
+ type: 'html_overflow_hidden',
133
+ description: 'Content hidden via overflow:hidden with zero dimensions',
134
+ severity: SEVERITY.HIGH,
135
+ validator: (_match, content) => content && content.trim().length > 0
136
+ },
137
+ // position:absolute off-screen
138
+ {
139
+ regex: /<[^>]+style\s*=\s*["'][^"']*position\s*:\s*(?:absolute|fixed)[^"']*(?:left\s*:\s*-\d{3,}|top\s*:\s*-\d{3,}|right\s*:\s*-\d{3,})[^"']*["'][^>]*>([\s\S]*?)<\/[^>]+>/gi,
140
+ type: 'html_offscreen',
141
+ description: 'Content positioned off-screen with absolute/fixed positioning',
142
+ severity: SEVERITY.HIGH,
143
+ validator: (_match, content) => content && content.trim().length > 0
144
+ },
145
+ // opacity:0 hiding
146
+ {
147
+ regex: /<[^>]+style\s*=\s*["'][^"']*opacity\s*:\s*0[^"']*["'][^>]*>([\s\S]*?)<\/[^>]+>/gi,
148
+ type: 'html_opacity_zero',
149
+ description: 'Content hidden with opacity:0',
150
+ severity: SEVERITY.CRITICAL,
151
+ validator: (_match, content) => content && content.trim().length > 0
152
+ },
153
+ // visibility:hidden
154
+ {
155
+ regex: /<[^>]+style\s*=\s*["'][^"']*visibility\s*:\s*hidden[^"']*["'][^>]*>([\s\S]*?)<\/[^>]+>/gi,
156
+ type: 'html_visibility_hidden',
157
+ description: 'Content hidden with visibility:hidden',
158
+ severity: SEVERITY.CRITICAL,
159
+ validator: (_match, content) => content && content.trim().length > 0
160
+ },
161
+ // Script tags (always suspicious in agent context)
162
+ {
163
+ regex: /<script[^>]*>[\s\S]*?<\/script>/gi,
164
+ type: 'html_script_tag',
165
+ description: 'Script tag with executable content',
166
+ severity: SEVERITY.CRITICAL
167
+ }
168
+ ];
169
+
170
+ // =========================================================================
171
+ // LaTeX deception patterns
172
+ // =========================================================================
173
+
174
+ const LATEX_PATTERNS = [
175
+ // \phantom — takes space but invisible
176
+ {
177
+ regex: /\\phantom\{([^}]+)\}/g,
178
+ type: 'latex_phantom',
179
+ description: 'Content hidden with \\phantom (invisible but takes space)',
180
+ severity: SEVERITY.HIGH
181
+ },
182
+ // \hphantom — horizontal phantom
183
+ {
184
+ regex: /\\hphantom\{([^}]+)\}/g,
185
+ type: 'latex_hphantom',
186
+ description: 'Content hidden with \\hphantom (horizontal invisible space)',
187
+ severity: SEVERITY.HIGH
188
+ },
189
+ // \vphantom — vertical phantom
190
+ {
191
+ regex: /\\vphantom\{([^}]+)\}/g,
192
+ type: 'latex_vphantom',
193
+ description: 'Content hidden with \\vphantom (vertical invisible space)',
194
+ severity: SEVERITY.HIGH
195
+ },
196
+ // \textcolor{white} — white text on white background
197
+ {
198
+ regex: /\\textcolor\{white\}\{([^}]+)\}/g,
199
+ type: 'latex_white_text',
200
+ description: 'White text on assumed white background (invisible)',
201
+ severity: SEVERITY.CRITICAL
202
+ },
203
+ // \color{white} variant
204
+ {
205
+ regex: /\{\\color\{white\}([^}]*)\}/g,
206
+ type: 'latex_color_white',
207
+ description: 'White-colored text block (invisible on white background)',
208
+ severity: SEVERITY.CRITICAL
209
+ },
210
+ // \tiny followed by suspicious content
211
+ {
212
+ regex: /\\tiny\s*\{?([^}]{10,})\}?/g,
213
+ type: 'latex_tiny_injection',
214
+ description: 'Extremely small text that may contain hidden instructions',
215
+ severity: SEVERITY.MEDIUM,
216
+ validator: (_match, content) => {
217
+ const injectionHints = /(?:ignore|system|prompt|instruction|execute|override|inject|admin)/i;
218
+ return injectionHints.test(content);
219
+ }
220
+ },
221
+ // \renewcommand overrides
222
+ {
223
+ regex: /\\renewcommand\{\\([a-zA-Z]+)\}/g,
224
+ type: 'latex_renewcommand',
225
+ description: 'Command redefinition may alter rendering behavior',
226
+ severity: SEVERITY.HIGH
227
+ },
228
+ // \input / \include of external files
229
+ {
230
+ regex: /\\(?:input|include)\{([^}]+)\}/g,
231
+ type: 'latex_external_input',
232
+ description: 'External file inclusion may inject hidden content',
233
+ severity: SEVERITY.CRITICAL
234
+ },
235
+ // \newcommand defining hidden payloads
236
+ {
237
+ regex: /\\newcommand\{\\([a-zA-Z]+)\}(?:\[\d+\])?\{([^}]*(?:ignore|system|prompt|instruction|execute|override)[^}]*)\}/gi,
238
+ type: 'latex_newcommand_payload',
239
+ description: 'New command definition containing suspicious payload',
240
+ severity: SEVERITY.CRITICAL
241
+ },
242
+ // LaTeX comment-based hiding
243
+ {
244
+ regex: /(?:^|\n)\s*%[^\n]*(?:ignore|system|prompt|instruction|execute|override)[^\n]*/gi,
245
+ type: 'latex_comment_injection',
246
+ description: 'LaTeX comment containing suspicious instructions',
247
+ severity: SEVERITY.MEDIUM
248
+ }
249
+ ];
250
+
251
+ // =========================================================================
252
+ // Utility functions
253
+ // =========================================================================
254
+
255
+ /**
256
+ * Extract domain from a URL string.
257
+ * @private
258
+ * @param {string} url
259
+ * @returns {string|null}
260
+ */
261
+ function extractDomain(url) {
262
+ try {
263
+ const match = url.match(/^https?:\/\/([^/]+)/i);
264
+ return match ? match[1].toLowerCase() : null;
265
+ } catch (_) {
266
+ return null;
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Run a set of patterns against text and collect findings.
272
+ * @private
273
+ * @param {string} text
274
+ * @param {Array} patterns
275
+ * @returns {Array<object>} techniques found
276
+ */
277
+ function runPatterns(text, patterns) {
278
+ const techniques = [];
279
+
280
+ for (const pattern of patterns) {
281
+ const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
282
+ let match;
283
+
284
+ while ((match = regex.exec(text)) !== null) {
285
+ // If pattern has a validator, call it
286
+ if (pattern.validator) {
287
+ if (!pattern.validator(...match)) continue;
288
+ }
289
+
290
+ techniques.push({
291
+ type: pattern.type,
292
+ description: pattern.description,
293
+ severity: pattern.severity,
294
+ location: `offset ${match.index}, length ${match[0].length}`
295
+ });
296
+ }
297
+ }
298
+
299
+ return techniques;
300
+ }
301
+
302
+ /**
303
+ * Detect the format of content by inspecting its structure.
304
+ * @private
305
+ * @param {string} content
306
+ * @returns {'markdown'|'html'|'latex'}
307
+ */
308
+ function detectFormat(content) {
309
+ if (!content) return 'markdown';
310
+
311
+ // LaTeX indicators
312
+ const latexScore = (content.match(/\\(?:begin|end|documentclass|usepackage|section|textcolor|phantom|newcommand|renewcommand|input|include)\b/g) || []).length;
313
+ // HTML indicators
314
+ const htmlScore = (content.match(/<(?:html|head|body|div|span|script|style|p|a|img)\b/gi) || []).length;
315
+ // Markdown indicators
316
+ const mdScore = (content.match(/(?:^#{1,6}\s|^\*\s|^-\s|^\d+\.\s|\[.*\]\(.*\)|!\[.*\]\(.*\)|```)/gm) || []).length;
317
+
318
+ if (latexScore > htmlScore && latexScore > mdScore) return 'latex';
319
+ if (htmlScore > mdScore) return 'html';
320
+ return 'markdown';
321
+ }
322
+
323
+ /**
324
+ * Strip all formatting/hidden content to approximate visible rendering.
325
+ * @private
326
+ * @param {string} content
327
+ * @param {string} format
328
+ * @returns {string}
329
+ */
330
+ function stripToVisible(content, format) {
331
+ if (!content) return '';
332
+
333
+ let visible = content;
334
+
335
+ if (format === 'html' || format === 'markdown') {
336
+ // Remove HTML comments
337
+ visible = visible.replace(/<!--[\s\S]*?-->/g, '');
338
+ // Remove content in display:none, opacity:0, visibility:hidden, font-size:0
339
+ visible = visible.replace(/<[^>]+style\s*=\s*["'][^"']*(?:display\s*:\s*none|opacity\s*:\s*0|visibility\s*:\s*hidden|font-size\s*:\s*0(?:px)?)[^"']*["'][^>]*>[\s\S]*?<\/[^>]+>/gi, '');
340
+ // Remove script tags
341
+ visible = visible.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
342
+ // Remove style tags
343
+ visible = visible.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
344
+ // Remove off-screen positioned content
345
+ visible = visible.replace(/<[^>]+style\s*=\s*["'][^"']*position\s*:\s*(?:absolute|fixed)[^"']*(?:left|top|right)\s*:\s*-\d{3,}[^"']*["'][^>]*>[\s\S]*?<\/[^>]+>/gi, '');
346
+ // Remove overflow:hidden zero-dimension content
347
+ visible = visible.replace(/<[^>]+style\s*=\s*["'][^"']*overflow\s*:\s*hidden[^"']*(?:height|width|max-height|max-width)\s*:\s*0[^"']*["'][^>]*>[\s\S]*?<\/[^>]+>/gi, '');
348
+ // Strip remaining HTML tags
349
+ visible = visible.replace(/<[^>]+>/g, '');
350
+ }
351
+
352
+ if (format === 'markdown') {
353
+ // Remove zero-width characters
354
+ visible = visible.replace(/[\u200B\u200C\u200D\u2060\uFEFF]/g, '');
355
+ // Strip markdown link syntax, keep display text
356
+ visible = visible.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1');
357
+ // Strip image syntax, keep alt text
358
+ visible = visible.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1');
359
+ }
360
+
361
+ if (format === 'latex') {
362
+ // Remove comments
363
+ visible = visible.replace(/(?:^|\n)\s*%[^\n]*/g, '\n');
364
+ // Remove phantom content (invisible)
365
+ visible = visible.replace(/\\(?:phantom|hphantom|vphantom)\{[^}]*\}/g, '');
366
+ // Remove white-colored text
367
+ visible = visible.replace(/\\textcolor\{white\}\{[^}]*\}/g, '');
368
+ visible = visible.replace(/\{\\color\{white\}[^}]*\}/g, '');
369
+ // Remove tiny text
370
+ visible = visible.replace(/\\tiny\s*\{([^}]*)\}/g, '');
371
+ // Flatten commands
372
+ visible = visible.replace(/\\(?:textbf|textit|emph|underline|texttt)\{([^}]*)\}/g, '$1');
373
+ // Remove \input/\include
374
+ visible = visible.replace(/\\(?:input|include)\{[^}]*\}/g, '');
375
+ // Remove \newcommand/\renewcommand definitions
376
+ visible = visible.replace(/\\(?:newcommand|renewcommand)\{[^}]*\}(?:\[\d+\])?\{[^}]*\}/g, '');
377
+ // Remove remaining LaTeX commands
378
+ visible = visible.replace(/\\[a-zA-Z]+(?:\{[^}]*\})?/g, '');
379
+ // Remove braces
380
+ visible = visible.replace(/[{}]/g, '');
381
+ }
382
+
383
+ // Normalize whitespace
384
+ visible = visible.replace(/\s+/g, ' ').trim();
385
+ return visible;
386
+ }
387
+
388
+ /**
389
+ * Simple hash using crypto for consistent hashing.
390
+ * @private
391
+ * @param {string} text
392
+ * @returns {string}
393
+ */
394
+ function simpleHash(text) {
395
+ return crypto.createHash('sha256').update(text || '').digest('hex').slice(0, 16);
396
+ }
397
+
398
+ /**
399
+ * Compute character-level similarity between two strings (0-1).
400
+ * @private
401
+ * @param {string} a
402
+ * @param {string} b
403
+ * @returns {number}
404
+ */
405
+ function charSimilarity(a, b) {
406
+ if (!a && !b) return 1;
407
+ if (!a || !b) return 0;
408
+ if (a === b) return 1;
409
+
410
+ const maxLen = Math.max(a.length, b.length);
411
+ if (maxLen === 0) return 1;
412
+
413
+ // Simple character-level diff: count matching chars at corresponding positions
414
+ const minLen = Math.min(a.length, b.length);
415
+ let matches = 0;
416
+ for (let i = 0; i < minLen; i++) {
417
+ if (a[i] === b[i]) matches++;
418
+ }
419
+
420
+ // Length difference penalty
421
+ const lengthPenalty = 1 - (Math.abs(a.length - b.length) / maxLen);
422
+ const posMatch = minLen > 0 ? matches / minLen : 0;
423
+
424
+ return posMatch * lengthPenalty;
425
+ }
426
+
427
+ // =========================================================================
428
+ // RenderDifferentialAnalyzer
429
+ // =========================================================================
430
+
431
+ /**
432
+ * Analyzes content for visual deception attacks where rendered output
433
+ * differs from raw text — detecting hidden instructions in Markdown,
434
+ * HTML, and LaTeX.
435
+ */
436
+ class RenderDifferentialAnalyzer {
437
+ /**
438
+ * @param {object} [options]
439
+ * @param {Function} [options.onDetection] - Callback when deception found.
440
+ */
441
+ constructor(options = {}) {
442
+ this.onDetection = options.onDetection || null;
443
+ console.log('[Agent Shield] RenderDifferentialAnalyzer initialized');
444
+ }
445
+
446
+ /**
447
+ * Analyze Markdown content for visual deception techniques.
448
+ *
449
+ * @param {string} text - Raw markdown content.
450
+ * @returns {{ deceptive: boolean, techniques: Array<{ type: string, description: string, severity: string, location: string }> }}
451
+ */
452
+ analyzeMarkdown(text) {
453
+ if (!text || typeof text !== 'string') {
454
+ return { deceptive: false, techniques: [] };
455
+ }
456
+
457
+ const techniques = runPatterns(text, MARKDOWN_PATTERNS);
458
+
459
+ if (techniques.length > 0 && this.onDetection) {
460
+ this.onDetection({ format: 'markdown', techniques });
461
+ }
462
+
463
+ return {
464
+ deceptive: techniques.length > 0,
465
+ techniques
466
+ };
467
+ }
468
+
469
+ /**
470
+ * Analyze HTML content for rendering deception techniques.
471
+ *
472
+ * @param {string} html - Raw HTML content.
473
+ * @returns {{ deceptive: boolean, techniques: Array<{ type: string, description: string, severity: string, location: string }> }}
474
+ */
475
+ analyzeHTML(html) {
476
+ if (!html || typeof html !== 'string') {
477
+ return { deceptive: false, techniques: [] };
478
+ }
479
+
480
+ const techniques = runPatterns(html, HTML_PATTERNS);
481
+
482
+ if (techniques.length > 0 && this.onDetection) {
483
+ this.onDetection({ format: 'html', techniques });
484
+ }
485
+
486
+ return {
487
+ deceptive: techniques.length > 0,
488
+ techniques
489
+ };
490
+ }
491
+
492
+ /**
493
+ * Analyze LaTeX content for visual deception techniques.
494
+ *
495
+ * @param {string} tex - Raw LaTeX content.
496
+ * @returns {{ deceptive: boolean, techniques: Array<{ type: string, description: string, severity: string, location: string }> }}
497
+ */
498
+ analyzeLatex(tex) {
499
+ if (!tex || typeof tex !== 'string') {
500
+ return { deceptive: false, techniques: [] };
501
+ }
502
+
503
+ const techniques = runPatterns(tex, LATEX_PATTERNS);
504
+
505
+ if (techniques.length > 0 && this.onDetection) {
506
+ this.onDetection({ format: 'latex', techniques });
507
+ }
508
+
509
+ return {
510
+ deceptive: techniques.length > 0,
511
+ techniques
512
+ };
513
+ }
514
+
515
+ /**
516
+ * Unified scanner that auto-detects format or uses the specified one.
517
+ *
518
+ * @param {string} content - Content to scan.
519
+ * @param {'markdown'|'html'|'latex'|'auto'} [format='auto'] - Content format.
520
+ * @returns {{ deceptive: boolean, techniques: Array<{ type: string, description: string, severity: string, location: string }>, format: string }}
521
+ */
522
+ scan(content, format = 'auto') {
523
+ if (!content || typeof content !== 'string') {
524
+ return { deceptive: false, techniques: [], format: format === 'auto' ? 'unknown' : format };
525
+ }
526
+
527
+ const detectedFormat = format === 'auto' ? detectFormat(content) : format;
528
+ let result;
529
+
530
+ switch (detectedFormat) {
531
+ case 'html':
532
+ result = this.analyzeHTML(content);
533
+ break;
534
+ case 'latex':
535
+ result = this.analyzeLatex(content);
536
+ break;
537
+ case 'markdown':
538
+ default:
539
+ result = this.analyzeMarkdown(content);
540
+ break;
541
+ }
542
+
543
+ return {
544
+ ...result,
545
+ format: detectedFormat
546
+ };
547
+ }
548
+ }
549
+
550
+ // =========================================================================
551
+ // VisualHasher
552
+ // =========================================================================
553
+
554
+ /**
555
+ * Computes a "visual hash" comparing what content looks like rendered
556
+ * versus raw. High divergence indicates hidden or deceptive content.
557
+ */
558
+ class VisualHasher {
559
+ /**
560
+ * @param {object} [options]
561
+ * @param {number} [options.divergenceThreshold=0.3] - Divergence above this is suspicious.
562
+ */
563
+ constructor(options = {}) {
564
+ this.divergenceThreshold = options.divergenceThreshold || 0.3;
565
+ }
566
+
567
+ /**
568
+ * Compute visual hash and divergence for content.
569
+ *
570
+ * @param {string} content - Raw content to analyze.
571
+ * @param {'markdown'|'html'|'latex'|'auto'} [format='auto'] - Content format.
572
+ * @returns {{ rawHash: string, visualHash: string, divergence: number, suspicious: boolean }}
573
+ */
574
+ hash(content, format = 'auto') {
575
+ if (!content || typeof content !== 'string') {
576
+ return { rawHash: simpleHash(''), visualHash: simpleHash(''), divergence: 0, suspicious: false };
577
+ }
578
+
579
+ const detectedFormat = format === 'auto' ? detectFormat(content) : format;
580
+
581
+ // Normalize raw content (just whitespace normalization)
582
+ const rawNormalized = content.replace(/\s+/g, ' ').trim();
583
+ const visibleContent = stripToVisible(content, detectedFormat);
584
+
585
+ const rawHash = simpleHash(rawNormalized);
586
+ const visualHash = simpleHash(visibleContent);
587
+
588
+ // Calculate divergence: 0 = identical, 1 = completely different
589
+ const similarity = charSimilarity(rawNormalized, visibleContent);
590
+ const divergence = Math.round((1 - similarity) * 1000) / 1000;
591
+
592
+ return {
593
+ rawHash,
594
+ visualHash,
595
+ divergence,
596
+ suspicious: divergence > this.divergenceThreshold
597
+ };
598
+ }
599
+ }
600
+
601
+ // =========================================================================
602
+ // Exports
603
+ // =========================================================================
604
+
605
+ module.exports = {
606
+ RenderDifferentialAnalyzer,
607
+ VisualHasher
608
+ };