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/CHANGELOG.md +15 -0
- package/README.md +260 -1187
- package/package.json +2 -2
- package/src/main.js +22 -0
- package/src/render-differential.js +608 -0
- package/src/side-channel-monitor.js +560 -0
- package/src/sybil-detector.js +529 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentshield-sdk",
|
|
3
|
-
"version": "13.
|
|
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
|
+
};
|