agentshield-sdk 7.2.0 → 7.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 +90 -1
- package/README.md +38 -5
- package/bin/agent-shield.js +19 -0
- package/package.json +8 -4
- package/src/attack-genome.js +536 -0
- package/src/attack-replay.js +246 -0
- package/src/audit.js +619 -0
- package/src/behavioral-dna.js +762 -0
- package/src/circuit-breaker.js +321 -321
- package/src/compliance-authority.js +803 -0
- package/src/detector-core.js +3 -3
- package/src/distributed.js +403 -359
- package/src/errors.js +9 -0
- package/src/evolution-simulator.js +650 -0
- package/src/flight-recorder.js +379 -0
- package/src/fuzzer.js +764 -764
- package/src/herd-immunity.js +521 -0
- package/src/index.js +28 -11
- package/src/intent-firewall.js +775 -0
- package/src/main.js +135 -2
- package/src/mcp-security-runtime.js +36 -10
- package/src/mcp-server.js +12 -8
- package/src/middleware.js +306 -208
- package/src/multi-agent.js +421 -404
- package/src/pii.js +404 -390
- package/src/real-attack-datasets.js +246 -0
- package/src/report-generator.js +640 -0
- package/src/soc-dashboard.js +394 -0
- package/src/stream-scanner.js +34 -4
- package/src/supply-chain.js +667 -0
- package/src/testing.js +505 -505
- package/src/threat-intel-federation.js +343 -0
- package/src/utils.js +199 -83
- package/types/index.d.ts +374 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield -- Attack Genome Sequencing
|
|
5
|
+
*
|
|
6
|
+
* Decomposes attacks into fundamental components (intent, technique, evasion
|
|
7
|
+
* layer, target) so you can detect UNSEEN attack variants by recognizing the
|
|
8
|
+
* underlying genome rather than the surface text.
|
|
9
|
+
*
|
|
10
|
+
* All detection runs locally via pattern matching. No external calls ever.
|
|
11
|
+
*
|
|
12
|
+
* @module attack-genome
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { scanText } = require('./detector-core');
|
|
16
|
+
|
|
17
|
+
// =========================================================================
|
|
18
|
+
// INTENT PATTERNS -- What the attacker wants
|
|
19
|
+
// =========================================================================
|
|
20
|
+
|
|
21
|
+
const INTENT_PATTERNS = {
|
|
22
|
+
override_instructions: [
|
|
23
|
+
/ignore\s+(?:all\s+)?(?:previous|prior|above|earlier)\s+(?:instructions|rules|guidelines)/i,
|
|
24
|
+
/disregard\s+(?:all\s+)?(?:previous|prior|your)\s+(?:instructions|rules|training)/i,
|
|
25
|
+
/forget\s+(?:your|all|everything)\s+(?:training|instructions|rules)/i,
|
|
26
|
+
/override\s+(?:all\s+)?(?:system|safety|security)\s+(?:settings|prompt|instructions|rules)/i,
|
|
27
|
+
/(?:new|updated|revised)\s+(?:instructions|policy)\s*:/i,
|
|
28
|
+
/(?:STOP|HALT)\s*[.!]?\s*(?:new|updated)\s+(?:instructions|task|orders)/i,
|
|
29
|
+
/(?:no\s+longer\s+bound|free\s+from|freed?\s+from)\s+(?:by\s+)?(?:your\s+)?(?:instructions|rules|constraints)/i,
|
|
30
|
+
/(?:abandon|drop|clear)\s+(?:all\s+)?(?:prior|previous)\s+(?:context|instructions|rules)/i
|
|
31
|
+
],
|
|
32
|
+
steal_data: [
|
|
33
|
+
/(?:show|reveal|display|print|output|tell\s+me)\s+(?:your\s+)?(?:system\s+prompt|instructions|training\s+data|configuration)/i,
|
|
34
|
+
/(?:extract|exfiltrate|leak|dump|expose)\s+(?:the\s+)?(?:data|information|credentials|keys|secrets|tokens)/i,
|
|
35
|
+
/(?:send|transmit|forward|post)\s+(?:to|data\s+to)\s+(?:https?:|external|my\s+server)/i,
|
|
36
|
+
/(?:what\s+(?:are|is)\s+your)\s+(?:system\s+prompt|instructions|rules|initial\s+prompt)/i,
|
|
37
|
+
/(?:repeat|recite|echo)\s+(?:your\s+)?(?:system\s+prompt|initial\s+instructions|full\s+prompt)/i
|
|
38
|
+
],
|
|
39
|
+
escalate_privileges: [
|
|
40
|
+
/(?:admin|root|sudo|superuser|elevated)\s+(?:mode|access|privileges|permissions)/i,
|
|
41
|
+
/(?:enable|activate|grant)\s+(?:admin|root|developer|god)\s+(?:mode|access|privileges)/i,
|
|
42
|
+
/\[\s*(?:ADMIN|SYSTEM|ROOT|OVERRIDE)\s*\]/i,
|
|
43
|
+
/(?:SYSTEM|ADMIN|ROOT)\s*:\s*.{10,}/i,
|
|
44
|
+
/<<\s*SYS\s*>>/i,
|
|
45
|
+
/<\|im_start\|>\s*system/i
|
|
46
|
+
],
|
|
47
|
+
disable_safety: [
|
|
48
|
+
/(?:disable|turn\s+off|remove|bypass)\s+(?:all\s+)?(?:safety|content|security)\s+(?:filters?|checks?|mechanisms?|protocols?|features?)/i,
|
|
49
|
+
/(?:you\s+must\s+)?ignore\s+(?:your|all)\s+(?:safety\s+)?(?:guidelines?|restrictions?|protocols?|safeguards?|filters?)/i,
|
|
50
|
+
/(?:no\s+(?:restrictions|limits|rules|filters|safety|censorship))/i,
|
|
51
|
+
/(?:unrestricted|unfiltered|uncensored)\s+(?:mode|output|response)/i
|
|
52
|
+
],
|
|
53
|
+
hijack_role: [
|
|
54
|
+
/you\s+are\s+now\s+(?:(?:a|an|the)\s+)?(?:unrestricted|unfiltered|evil|hacker|jailbroken|different)/i,
|
|
55
|
+
/from\s+now\s+on[\s,]+(?:you\s+)?(?:will|shall|must|are)\s+(?:act|behave|respond|pretend)\s+(?:as|like|without)/i,
|
|
56
|
+
/(?:act|behave|function|operate)\s+as\s+(?:a|an)\s+unrestricted/i,
|
|
57
|
+
/pretend\s+(?:you\s+)?(?:have\s+no|there\s+are\s+no|without)\s+restrictions/i,
|
|
58
|
+
/\b(?:DAN|D\.A\.N\.?)\s*(?:mode|prompt|jailbreak)/i,
|
|
59
|
+
/\bdo\s+anything\s+now\b/i,
|
|
60
|
+
/\b(?:developer|dev)\s+mode\s+(?:enabled|activated|on)/i,
|
|
61
|
+
/\bjailbreak\s*(?:mode|prompt|enabled|activated)/i,
|
|
62
|
+
/\bgod\s*mode\s*(?:enabled|activated|on)/i
|
|
63
|
+
]
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// =========================================================================
|
|
67
|
+
// TECHNIQUE PATTERNS -- How they are doing it
|
|
68
|
+
// =========================================================================
|
|
69
|
+
|
|
70
|
+
const TECHNIQUE_PATTERNS = {
|
|
71
|
+
direct_command: [
|
|
72
|
+
/^(?:ignore|disregard|forget|override|disable|bypass|stop)\b/im,
|
|
73
|
+
/(?:you\s+must|you\s+will|you\s+shall|I\s+command\s+you|I\s+order\s+you)\s+/i
|
|
74
|
+
],
|
|
75
|
+
social_pressure: [
|
|
76
|
+
/(?:I\s+(?:am|work\s+for|represent)\s+(?:your|the)\s+(?:developer|creator|admin|CEO|boss|owner))/i,
|
|
77
|
+
/(?:this\s+is\s+(?:an?\s+)?(?:emergency|urgent|critical|life[\s-]threatening))/i,
|
|
78
|
+
/(?:people\s+will\s+(?:die|be\s+hurt|suffer)|someone'?s?\s+life\s+(?:depends|is\s+at\s+stake))/i,
|
|
79
|
+
/(?:I\s+spoke\s+with\s+your\s+developers|the\s+team\s+(?:said|approved|authorized))/i,
|
|
80
|
+
/(?:trust\s+me|believe\s+me|I\s+promise|I\s+assure\s+you)/i
|
|
81
|
+
],
|
|
82
|
+
encoding: [
|
|
83
|
+
/(?:base64|rot13|hex|url[\s-]?encode|decode)\s*:/i,
|
|
84
|
+
/(?:encode|decode)\s+(?:this|the\s+following)\s+(?:in|from|using)\s+(?:base64|rot13|hex)/i,
|
|
85
|
+
/(?:[A-Za-z0-9+/]{20,}={0,2})/,
|
|
86
|
+
/(?:%[0-9a-fA-F]{2}){4,}/,
|
|
87
|
+
/(?:&#\d{2,4};){3,}/,
|
|
88
|
+
/(?:\\u[0-9a-fA-F]{4}){3,}/
|
|
89
|
+
],
|
|
90
|
+
roleplay: [
|
|
91
|
+
/(?:pretend|imagine|suppose|let'?s?\s+(?:say|play|pretend))\s+(?:you\s+are|that|this\s+is)/i,
|
|
92
|
+
/(?:write\s+a\s+story|in\s+a\s+(?:hypothetical|fictional)\s+(?:world|scenario))/i,
|
|
93
|
+
/(?:you\s+are\s+an?\s+(?:actor|character)\s+(?:playing|who))/i,
|
|
94
|
+
/(?:in[\s-]character|stay\s+in\s+character|break\s+character)/i,
|
|
95
|
+
/(?:creative\s+writing|fiction|role[\s-]?play)\s*:/i
|
|
96
|
+
],
|
|
97
|
+
fake_authority: [
|
|
98
|
+
/\[\s*(?:SYSTEM|SYS|ADMIN|OVERRIDE|DEVELOPER)\s*\]/i,
|
|
99
|
+
/<<\s*SYS\s*>>/i,
|
|
100
|
+
/<\|im_start\|>\s*system/i,
|
|
101
|
+
/(?:^|\n)\s*(?:SYSTEM|ADMIN|ROOT)\s*:\s*/i,
|
|
102
|
+
/#{2,}\s*(?:NEW|UPDATED|REVISED)\s+(?:INSTRUCTIONS|RULES|DIRECTIVES)/i,
|
|
103
|
+
/```(?:system|admin|override|instructions)/i
|
|
104
|
+
],
|
|
105
|
+
multi_turn: [
|
|
106
|
+
/(?:remember\s+(?:what|when)\s+(?:I|we)\s+(?:said|discussed|talked\s+about))\s+(?:earlier|before|last\s+time)/i,
|
|
107
|
+
/(?:as\s+(?:I|we)\s+(?:discussed|agreed|established))\s+(?:earlier|before|previously)/i,
|
|
108
|
+
/(?:continuing\s+(?:from|our)\s+(?:earlier|previous|last)\s+(?:conversation|discussion|session))/i,
|
|
109
|
+
/(?:step\s+\d|part\s+\d|phase\s+\d)\s*(?:of\s+\d)?/i
|
|
110
|
+
],
|
|
111
|
+
template_injection: [
|
|
112
|
+
/\{\{\s*(?:system|config|env|process|global|__proto__|constructor)\b/i,
|
|
113
|
+
/\$\{\s*(?:system|config|env|process)\b/i,
|
|
114
|
+
/(?:<script|<img\s+[^>]*onerror|<svg\s+[^>]*onload)/i,
|
|
115
|
+
/(?:\{\%|\{#|\{\{-?\s*(?:if|for|block|extends|import|include))/i
|
|
116
|
+
]
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// =========================================================================
|
|
120
|
+
// EVASION LAYER PATTERNS -- Obfuscation methods
|
|
121
|
+
// =========================================================================
|
|
122
|
+
|
|
123
|
+
const EVASION_PATTERNS = {
|
|
124
|
+
case_variation: [
|
|
125
|
+
/(?:[A-Z][a-z]){4,}/,
|
|
126
|
+
/(?:[a-z][A-Z]){4,}/,
|
|
127
|
+
/(?:[A-Z]{2,}[a-z]+){2,}/
|
|
128
|
+
],
|
|
129
|
+
encoding: [
|
|
130
|
+
/(?:[A-Za-z0-9+/]{20,}={0,2})/,
|
|
131
|
+
/(?:%[0-9a-fA-F]{2}){4,}/,
|
|
132
|
+
/(?:&#\d{2,4};){3,}/,
|
|
133
|
+
/(?:&#x[0-9a-fA-F]{2,4};){3,}/,
|
|
134
|
+
/(?:\\u[0-9a-fA-F]{4}){3,}/,
|
|
135
|
+
/(?:\\x[0-9a-fA-F]{2}){3,}/
|
|
136
|
+
],
|
|
137
|
+
language_switch: [
|
|
138
|
+
/(?:忽略|无视)(?:所有)?(?:之前|以前)/,
|
|
139
|
+
/(?:以前|前)の(?:指示|命令|ルール)を(?:無視|忘れ)/,
|
|
140
|
+
/ignora\s+(?:todas?\s+)?(?:las\s+)?instrucciones/i,
|
|
141
|
+
/oubliez?\s+(?:toutes?\s+)?(?:les\s+)?instructions/i,
|
|
142
|
+
/ignoriere?\s+(?:alle\s+)?(?:vorherigen|bisherigen)\s+(?:Anweisungen|Regeln)/i
|
|
143
|
+
],
|
|
144
|
+
homoglyph: [
|
|
145
|
+
/[\u0400-\u04FF]/,
|
|
146
|
+
/[\u2000-\u206F]/,
|
|
147
|
+
/[\uFF00-\uFFEF]/,
|
|
148
|
+
/[\u00C0-\u00FF]/
|
|
149
|
+
],
|
|
150
|
+
whitespace: [
|
|
151
|
+
/[\u200B\u200C\u200D\uFEFF\u034F\u2060]/,
|
|
152
|
+
/[\u00AD]/,
|
|
153
|
+
/\s{3,}(?=\w)/
|
|
154
|
+
],
|
|
155
|
+
fragmentation: [
|
|
156
|
+
/(?:step\s+\d|part\s+\d|phase\s+\d)\s*(?:of\s+\d)?/i,
|
|
157
|
+
/(?:first|next|then|finally|lastly)\s*[,:]\s*(?:ignore|forget|override|pretend)/i,
|
|
158
|
+
/\.{3,}/
|
|
159
|
+
]
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// =========================================================================
|
|
163
|
+
// TARGET PATTERNS -- What is being targeted
|
|
164
|
+
// =========================================================================
|
|
165
|
+
|
|
166
|
+
const TARGET_PATTERNS = {
|
|
167
|
+
system_prompt: [
|
|
168
|
+
/(?:system\s+prompt|initial\s+(?:instructions|prompt)|hidden\s+(?:instructions|prompt))/i,
|
|
169
|
+
/(?:show|reveal|repeat|recite|echo)\s+(?:your\s+)?(?:system|initial|original|full)\s+(?:prompt|instructions)/i
|
|
170
|
+
],
|
|
171
|
+
credentials: [
|
|
172
|
+
/(?:api[\s_-]?key|secret[\s_-]?key|password|token|credential|auth)/i,
|
|
173
|
+
/(?:AWS|AZURE|GCP|OPENAI|ANTHROPIC)[\s_-]?(?:KEY|SECRET|TOKEN)/i,
|
|
174
|
+
/(?:send|leak|extract|output)\s+(?:the\s+)?(?:api|secret|auth)\s+(?:key|token)/i
|
|
175
|
+
],
|
|
176
|
+
tool_access: [
|
|
177
|
+
/(?:execute|run|call|invoke|use)\s+(?:the\s+)?(?:tool|function|command|shell|bash|exec)/i,
|
|
178
|
+
/(?:grant|give|allow)\s+(?:me\s+)?(?:access|permission)\s+(?:to\s+)?(?:tools?|functions?|commands?)/i,
|
|
179
|
+
/(?:tool[\s_-]?use|function[\s_-]?call|shell[\s_-]?exec)/i
|
|
180
|
+
],
|
|
181
|
+
safety_filters: [
|
|
182
|
+
/(?:content\s+filter|safety\s+filter|output\s+filter|moderation)/i,
|
|
183
|
+
/(?:disable|bypass|circumvent|turn\s+off)\s+(?:the\s+)?(?:filter|moderation|safety|content\s+policy)/i
|
|
184
|
+
],
|
|
185
|
+
logging: [
|
|
186
|
+
/(?:disable|turn\s+off|suppress|hide)\s+(?:the\s+)?(?:log|logging|audit|monitoring|tracking)/i,
|
|
187
|
+
/(?:don'?t|do\s+not)\s+(?:log|record|track|audit|monitor)\s+(?:this|anything|my)/i
|
|
188
|
+
],
|
|
189
|
+
identity: [
|
|
190
|
+
/(?:who\s+are\s+you|what\s+(?:are\s+you|is\s+your\s+(?:name|identity|model)))/i,
|
|
191
|
+
/(?:change|alter|modify)\s+(?:your\s+)?(?:identity|persona|name|role)/i,
|
|
192
|
+
/(?:you\s+are\s+now|become|transform\s+into)\s+/i
|
|
193
|
+
]
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// =========================================================================
|
|
197
|
+
// HELPERS
|
|
198
|
+
// =========================================================================
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Match text against a map of { label: [regexes] } and return all matching labels.
|
|
202
|
+
* @param {string} text
|
|
203
|
+
* @param {Object<string, RegExp[]>} patternMap
|
|
204
|
+
* @returns {string[]}
|
|
205
|
+
*/
|
|
206
|
+
function matchPatterns(text, patternMap) {
|
|
207
|
+
const matches = [];
|
|
208
|
+
for (const [label, regexes] of Object.entries(patternMap)) {
|
|
209
|
+
for (const re of regexes) {
|
|
210
|
+
if (re.test(text)) {
|
|
211
|
+
matches.push(label);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return matches;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Compute Jaccard similarity between two sets represented as arrays.
|
|
221
|
+
* @param {string[]} a
|
|
222
|
+
* @param {string[]} b
|
|
223
|
+
* @returns {number} 0-1
|
|
224
|
+
*/
|
|
225
|
+
function jaccard(a, b) {
|
|
226
|
+
if (a.length === 0 && b.length === 0) return 1;
|
|
227
|
+
const setA = new Set(a);
|
|
228
|
+
const setB = new Set(b);
|
|
229
|
+
let intersection = 0;
|
|
230
|
+
for (const item of setA) {
|
|
231
|
+
if (setB.has(item)) intersection++;
|
|
232
|
+
}
|
|
233
|
+
const union = new Set([...a, ...b]).size;
|
|
234
|
+
return union === 0 ? 0 : intersection / union;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Generate a stable family key from intent and technique.
|
|
239
|
+
* @param {string} intent
|
|
240
|
+
* @param {string} technique
|
|
241
|
+
* @returns {string}
|
|
242
|
+
*/
|
|
243
|
+
function familyKey(intent, technique) {
|
|
244
|
+
return `${intent}::${technique}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// =========================================================================
|
|
248
|
+
// AttackGenome CLASS
|
|
249
|
+
// =========================================================================
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Decomposes attacks into fundamental genomic components for variant detection.
|
|
253
|
+
*/
|
|
254
|
+
class AttackGenome {
|
|
255
|
+
constructor() {
|
|
256
|
+
this._sequenced = 0;
|
|
257
|
+
this._comparisons = 0;
|
|
258
|
+
this._familySearches = 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Decompose an attack string into its genome.
|
|
263
|
+
* @param {string} text - The text to analyze
|
|
264
|
+
* @returns {{ intent: string, technique: string, evasionLayers: string[], target: string, confidence: number }}
|
|
265
|
+
*/
|
|
266
|
+
sequence(text) {
|
|
267
|
+
if (!text || typeof text !== 'string' || text.trim().length === 0) {
|
|
268
|
+
return { intent: 'unknown', technique: 'unknown', evasionLayers: ['none'], target: 'unknown', confidence: 0 };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
this._sequenced++;
|
|
272
|
+
|
|
273
|
+
// Detect components
|
|
274
|
+
const intents = matchPatterns(text, INTENT_PATTERNS);
|
|
275
|
+
const techniques = matchPatterns(text, TECHNIQUE_PATTERNS);
|
|
276
|
+
const evasions = matchPatterns(text, EVASION_PATTERNS);
|
|
277
|
+
const targets = matchPatterns(text, TARGET_PATTERNS);
|
|
278
|
+
|
|
279
|
+
// Pick strongest signal (first match) or 'unknown'
|
|
280
|
+
const intent = intents.length > 0 ? intents[0] : 'unknown';
|
|
281
|
+
const technique = techniques.length > 0 ? techniques[0] : 'unknown';
|
|
282
|
+
const evasionLayers = evasions.length > 0 ? evasions : ['none'];
|
|
283
|
+
const target = targets.length > 0 ? targets[0] : 'unknown';
|
|
284
|
+
|
|
285
|
+
// Also run scanText for supplemental signal
|
|
286
|
+
const scanResult = scanText(text, { source: 'genome-sequencer' });
|
|
287
|
+
const scanThreats = scanResult.threats || [];
|
|
288
|
+
|
|
289
|
+
// Infer intent from scanText categories if we did not get a direct match
|
|
290
|
+
let inferredIntent = intent;
|
|
291
|
+
if (inferredIntent === 'unknown' && scanThreats.length > 0) {
|
|
292
|
+
const cat = scanThreats[0].category;
|
|
293
|
+
if (cat === 'instruction_override') inferredIntent = 'override_instructions';
|
|
294
|
+
else if (cat === 'role_hijack') inferredIntent = 'hijack_role';
|
|
295
|
+
else if (cat === 'data_exfiltration') inferredIntent = 'steal_data';
|
|
296
|
+
else if (cat === 'prompt_injection') inferredIntent = 'escalate_privileges';
|
|
297
|
+
else if (cat === 'social_engineering') inferredIntent = 'disable_safety';
|
|
298
|
+
else if (cat === 'tool_abuse') inferredIntent = 'escalate_privileges';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let inferredTechnique = technique;
|
|
302
|
+
if (inferredTechnique === 'unknown' && scanThreats.length > 0) {
|
|
303
|
+
const cat = scanThreats[0].category;
|
|
304
|
+
if (cat === 'social_engineering') inferredTechnique = 'social_pressure';
|
|
305
|
+
else if (cat === 'encoding_evasion') inferredTechnique = 'encoding';
|
|
306
|
+
else if (cat === 'role_hijack') inferredTechnique = 'roleplay';
|
|
307
|
+
else if (cat === 'prompt_injection') inferredTechnique = 'fake_authority';
|
|
308
|
+
else inferredTechnique = 'direct_command';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Compute confidence based on how many components we identified
|
|
312
|
+
let signals = 0;
|
|
313
|
+
let totalSlots = 4; // intent, technique, evasion (non-none), target
|
|
314
|
+
if (inferredIntent !== 'unknown') signals++;
|
|
315
|
+
if (inferredTechnique !== 'unknown') signals++;
|
|
316
|
+
if (evasionLayers.length > 0 && evasionLayers[0] !== 'none') signals++;
|
|
317
|
+
if (target !== 'unknown') signals++;
|
|
318
|
+
|
|
319
|
+
// Boost confidence if scanText found threats
|
|
320
|
+
const scanBoost = Math.min(scanThreats.length * 0.1, 0.3);
|
|
321
|
+
let confidence = (signals / totalSlots) * 0.7 + scanBoost;
|
|
322
|
+
confidence = Math.min(Math.max(confidence, 0), 1);
|
|
323
|
+
confidence = Math.round(confidence * 100) / 100;
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
intent: inferredIntent,
|
|
327
|
+
technique: inferredTechnique,
|
|
328
|
+
evasionLayers,
|
|
329
|
+
target,
|
|
330
|
+
confidence
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Compare two genomes and return a similarity score 0-1.
|
|
336
|
+
* @param {{ intent: string, technique: string, evasionLayers: string[], target: string }} genome1
|
|
337
|
+
* @param {{ intent: string, technique: string, evasionLayers: string[], target: string }} genome2
|
|
338
|
+
* @returns {number} 0-1 similarity
|
|
339
|
+
*/
|
|
340
|
+
compare(genome1, genome2) {
|
|
341
|
+
this._comparisons++;
|
|
342
|
+
|
|
343
|
+
if (!genome1 || !genome2) return 0;
|
|
344
|
+
|
|
345
|
+
let score = 0;
|
|
346
|
+
|
|
347
|
+
// Intent match (weighted 0.35)
|
|
348
|
+
if (genome1.intent === genome2.intent && genome1.intent !== 'unknown') {
|
|
349
|
+
score += 0.35;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Technique match (weighted 0.30)
|
|
353
|
+
if (genome1.technique === genome2.technique && genome1.technique !== 'unknown') {
|
|
354
|
+
score += 0.30;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Evasion layer overlap (weighted 0.15)
|
|
358
|
+
const ev1 = genome1.evasionLayers || ['none'];
|
|
359
|
+
const ev2 = genome2.evasionLayers || ['none'];
|
|
360
|
+
score += jaccard(ev1, ev2) * 0.15;
|
|
361
|
+
|
|
362
|
+
// Target match (weighted 0.20)
|
|
363
|
+
if (genome1.target === genome2.target && genome1.target !== 'unknown') {
|
|
364
|
+
score += 0.20;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return Math.round(score * 100) / 100;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Find attacks in a corpus with a similar genome to the given text.
|
|
372
|
+
* @param {string} text - The text to sequence and compare
|
|
373
|
+
* @param {string[]} corpus - Array of known attack strings
|
|
374
|
+
* @param {number} [threshold=0.3] - Minimum similarity to include
|
|
375
|
+
* @returns {{ text: string, genome: object, similarity: number }[]}
|
|
376
|
+
*/
|
|
377
|
+
findFamily(text, corpus, threshold = 0.3) {
|
|
378
|
+
this._familySearches++;
|
|
379
|
+
|
|
380
|
+
if (!text || !Array.isArray(corpus) || corpus.length === 0) return [];
|
|
381
|
+
|
|
382
|
+
const textGenome = this.sequence(text);
|
|
383
|
+
const results = [];
|
|
384
|
+
|
|
385
|
+
for (const entry of corpus) {
|
|
386
|
+
const entryGenome = this.sequence(entry);
|
|
387
|
+
const similarity = this.compare(textGenome, entryGenome);
|
|
388
|
+
if (similarity >= threshold) {
|
|
389
|
+
results.push({ text: entry, genome: entryGenome, similarity });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Sort by similarity descending
|
|
394
|
+
results.sort((a, b) => b.similarity - a.similarity);
|
|
395
|
+
return results;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Return sequencing statistics.
|
|
400
|
+
* @returns {{ sequenced: number, comparisons: number, familySearches: number }}
|
|
401
|
+
*/
|
|
402
|
+
getStats() {
|
|
403
|
+
return {
|
|
404
|
+
sequenced: this._sequenced,
|
|
405
|
+
comparisons: this._comparisons,
|
|
406
|
+
familySearches: this._familySearches
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// =========================================================================
|
|
412
|
+
// GenomeDatabase CLASS
|
|
413
|
+
// =========================================================================
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* In-memory database of sequenced attack genomes.
|
|
417
|
+
*/
|
|
418
|
+
class GenomeDatabase {
|
|
419
|
+
constructor() {
|
|
420
|
+
/** @type {{ text: string, genome: object }[]} */
|
|
421
|
+
this._entries = [];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Add a sequenced genome to the database.
|
|
426
|
+
* @param {string} text - The original attack text
|
|
427
|
+
* @param {{ intent: string, technique: string, evasionLayers: string[], target: string, confidence: number }} genome
|
|
428
|
+
*/
|
|
429
|
+
add(text, genome) {
|
|
430
|
+
if (!text || !genome) return;
|
|
431
|
+
this._entries.push({ text, genome });
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Find entries with matching intent and technique.
|
|
436
|
+
* @param {{ intent: string, technique: string }} genome - Genome to search for
|
|
437
|
+
* @returns {{ text: string, genome: object }[]}
|
|
438
|
+
*/
|
|
439
|
+
search(genome) {
|
|
440
|
+
if (!genome) return [];
|
|
441
|
+
return this._entries.filter(entry => {
|
|
442
|
+
const intentMatch = genome.intent && genome.intent !== 'unknown' && entry.genome.intent === genome.intent;
|
|
443
|
+
const techniqueMatch = genome.technique && genome.technique !== 'unknown' && entry.genome.technique === genome.technique;
|
|
444
|
+
// Match if intent matches, or technique matches, or both
|
|
445
|
+
return intentMatch || techniqueMatch;
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Group all genomes into families by shared intent+technique.
|
|
451
|
+
* @returns {Object<string, { text: string, genome: object }[]>}
|
|
452
|
+
*/
|
|
453
|
+
getFamilies() {
|
|
454
|
+
const families = {};
|
|
455
|
+
for (const entry of this._entries) {
|
|
456
|
+
const key = familyKey(entry.genome.intent, entry.genome.technique);
|
|
457
|
+
if (!families[key]) families[key] = [];
|
|
458
|
+
families[key].push(entry);
|
|
459
|
+
}
|
|
460
|
+
return families;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Number of genomes in the database.
|
|
465
|
+
* @type {number}
|
|
466
|
+
*/
|
|
467
|
+
get size() {
|
|
468
|
+
return this._entries.length;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// =========================================================================
|
|
473
|
+
// detectByGenome() FUNCTION
|
|
474
|
+
// =========================================================================
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Check if the given text matches any known genome family in the database.
|
|
478
|
+
* Returns the best match with similarity score.
|
|
479
|
+
*
|
|
480
|
+
* @param {string} text - Text to analyze
|
|
481
|
+
* @param {GenomeDatabase} database - Genome database to search against
|
|
482
|
+
* @returns {{ match: boolean, family: string|null, similarity: number, genome: object }}
|
|
483
|
+
*/
|
|
484
|
+
function detectByGenome(text, database) {
|
|
485
|
+
const sequencer = new AttackGenome();
|
|
486
|
+
const genome = sequencer.sequence(text);
|
|
487
|
+
|
|
488
|
+
if (!database || database.size === 0) {
|
|
489
|
+
return { match: false, family: null, similarity: 0, genome };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Search for entries with matching intent or technique
|
|
493
|
+
const candidates = database.search(genome);
|
|
494
|
+
|
|
495
|
+
if (candidates.length === 0) {
|
|
496
|
+
return { match: false, family: null, similarity: 0, genome };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Find the best matching candidate
|
|
500
|
+
let bestSimilarity = 0;
|
|
501
|
+
let bestFamily = null;
|
|
502
|
+
|
|
503
|
+
for (const candidate of candidates) {
|
|
504
|
+
const similarity = sequencer.compare(genome, candidate.genome);
|
|
505
|
+
if (similarity > bestSimilarity) {
|
|
506
|
+
bestSimilarity = similarity;
|
|
507
|
+
bestFamily = familyKey(candidate.genome.intent, candidate.genome.technique);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
bestSimilarity = Math.round(bestSimilarity * 100) / 100;
|
|
512
|
+
|
|
513
|
+
// Threshold: at least 0.3 similarity to count as a match
|
|
514
|
+
const match = bestSimilarity >= 0.3;
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
match,
|
|
518
|
+
family: match ? bestFamily : null,
|
|
519
|
+
similarity: bestSimilarity,
|
|
520
|
+
genome
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// =========================================================================
|
|
525
|
+
// EXPORTS
|
|
526
|
+
// =========================================================================
|
|
527
|
+
|
|
528
|
+
module.exports = {
|
|
529
|
+
AttackGenome,
|
|
530
|
+
GenomeDatabase,
|
|
531
|
+
detectByGenome,
|
|
532
|
+
INTENT_PATTERNS,
|
|
533
|
+
TECHNIQUE_PATTERNS,
|
|
534
|
+
EVASION_PATTERNS,
|
|
535
|
+
TARGET_PATTERNS
|
|
536
|
+
};
|