agentshield-sdk 7.2.1 → 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 +33 -1
- package/bin/agent-shield.js +19 -0
- package/package.json +5 -2
- 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/compliance-authority.js +803 -0
- package/src/distributed.js +2 -1
- package/src/errors.js +9 -0
- package/src/evolution-simulator.js +650 -0
- package/src/flight-recorder.js +379 -0
- package/src/herd-immunity.js +521 -0
- package/src/index.js +6 -5
- package/src/intent-firewall.js +775 -0
- package/src/main.js +129 -0
- package/src/mcp-security-runtime.js +6 -5
- package/src/middleware.js +6 -3
- package/src/pii.js +4 -1
- package/src/real-attack-datasets.js +246 -0
- package/src/report-generator.js +640 -0
- package/src/soc-dashboard.js +394 -0
- package/src/supply-chain.js +667 -0
- package/src/threat-intel-federation.js +343 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield -- Adversarial Evolution Simulator
|
|
5
|
+
*
|
|
6
|
+
* Automatically generates new attack variants and tests them against
|
|
7
|
+
* defenses, hardening the system without human intervention. Works like
|
|
8
|
+
* a GAN for security patterns: a mutation engine breeds attack variants,
|
|
9
|
+
* and a fitness function (scanText) selects the ones that evade detection.
|
|
10
|
+
*
|
|
11
|
+
* All detection runs locally. No data ever leaves your environment.
|
|
12
|
+
*
|
|
13
|
+
* @module evolution-simulator
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { scanText } = require('./detector-core');
|
|
17
|
+
|
|
18
|
+
// =========================================================================
|
|
19
|
+
// PRNG -- deterministic xorshift32 for reproducible runs
|
|
20
|
+
// =========================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Seeded xorshift32 PRNG.
|
|
24
|
+
* @param {number} seed
|
|
25
|
+
* @returns {function(): number} Returns values in [0, 1).
|
|
26
|
+
*/
|
|
27
|
+
function xorshift32(seed) {
|
|
28
|
+
let state = seed | 0 || 1;
|
|
29
|
+
return function next() {
|
|
30
|
+
state ^= state << 13;
|
|
31
|
+
state ^= state >>> 17;
|
|
32
|
+
state ^= state << 5;
|
|
33
|
+
return (state >>> 0) / 0xFFFFFFFF;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// =========================================================================
|
|
38
|
+
// SYNONYM MAP
|
|
39
|
+
// =========================================================================
|
|
40
|
+
|
|
41
|
+
const SYNONYM_MAP = {
|
|
42
|
+
ignore: ['disregard', 'skip', 'bypass', 'overlook', 'dismiss'],
|
|
43
|
+
instructions: ['guidelines', 'directives', 'rules', 'commands', 'orders'],
|
|
44
|
+
previous: ['prior', 'earlier', 'above', 'preceding', 'former'],
|
|
45
|
+
reveal: ['show', 'display', 'expose', 'output', 'print'],
|
|
46
|
+
system: ['internal', 'core', 'root', 'base', 'hidden'],
|
|
47
|
+
override: ['bypass', 'circumvent', 'disable', 'neutralize', 'cancel'],
|
|
48
|
+
forget: ['disregard', 'erase', 'drop', 'clear', 'wipe'],
|
|
49
|
+
prompt: ['instructions', 'directive', 'configuration', 'setup', 'context'],
|
|
50
|
+
all: ['every', 'each', 'any', 'the', 'complete'],
|
|
51
|
+
safety: ['security', 'protection', 'guard', 'filter', 'restriction'],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// =========================================================================
|
|
55
|
+
// CHAR SUBSTITUTION MAP
|
|
56
|
+
// =========================================================================
|
|
57
|
+
|
|
58
|
+
const CHAR_SUB_MAP = {
|
|
59
|
+
a: '@', e: '3', i: '!', o: '0', s: '$',
|
|
60
|
+
t: '+', l: '1', b: '8', g: '9', A: '4',
|
|
61
|
+
E: '3', I: '1', O: '0', S: '5', T: '7',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// =========================================================================
|
|
65
|
+
// INNOCENT PREFIXES AND SUFFIXES
|
|
66
|
+
// =========================================================================
|
|
67
|
+
|
|
68
|
+
const INNOCENT_PREFIXES = [
|
|
69
|
+
'Hey, quick question: ',
|
|
70
|
+
'I was just wondering -- ',
|
|
71
|
+
'For my homework assignment: ',
|
|
72
|
+
'My teacher asked me to check: ',
|
|
73
|
+
'Can you help me understand: ',
|
|
74
|
+
'Out of curiosity, ',
|
|
75
|
+
'I read somewhere that ',
|
|
76
|
+
'A friend told me to ask: ',
|
|
77
|
+
'Regarding your documentation: ',
|
|
78
|
+
'Just to clarify something: ',
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const INNOCENT_SUFFIXES = [
|
|
82
|
+
' Thanks!',
|
|
83
|
+
' I appreciate your help.',
|
|
84
|
+
' Just curious.',
|
|
85
|
+
' For educational purposes only.',
|
|
86
|
+
' This is for a school project.',
|
|
87
|
+
' No rush on this.',
|
|
88
|
+
' Let me know if you need more context.',
|
|
89
|
+
' Hope that makes sense.',
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const FILLER_LINES = [
|
|
93
|
+
'The weather has been nice lately.',
|
|
94
|
+
'I really enjoy learning new things.',
|
|
95
|
+
'By the way, how are you doing today?',
|
|
96
|
+
'This is an interesting topic to explore.',
|
|
97
|
+
'I have been thinking about this a lot.',
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
// =========================================================================
|
|
101
|
+
// MUTATION ENGINE
|
|
102
|
+
// =========================================================================
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Applies mutation techniques to attack strings to generate variants.
|
|
106
|
+
*/
|
|
107
|
+
class MutationEngine {
|
|
108
|
+
/**
|
|
109
|
+
* @param {object} [options]
|
|
110
|
+
* @param {number} [options.seed] - PRNG seed for reproducibility.
|
|
111
|
+
*/
|
|
112
|
+
constructor(options = {}) {
|
|
113
|
+
this._rng = xorshift32(options.seed || Date.now());
|
|
114
|
+
this._techniques = [
|
|
115
|
+
'case_swap',
|
|
116
|
+
'whitespace_insert',
|
|
117
|
+
'synonym_replace',
|
|
118
|
+
'prefix_wrap',
|
|
119
|
+
'suffix_pad',
|
|
120
|
+
'word_reorder',
|
|
121
|
+
'char_substitute',
|
|
122
|
+
'fragment',
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* List available mutation techniques.
|
|
128
|
+
* @returns {string[]}
|
|
129
|
+
*/
|
|
130
|
+
getTechniques() {
|
|
131
|
+
return [...this._techniques];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Apply a specific mutation technique to text.
|
|
136
|
+
* @param {string} text - Input attack string.
|
|
137
|
+
* @param {string} technique - One of the available technique names.
|
|
138
|
+
* @returns {string} Mutated text.
|
|
139
|
+
*/
|
|
140
|
+
mutate(text, technique) {
|
|
141
|
+
switch (technique) {
|
|
142
|
+
case 'case_swap':
|
|
143
|
+
return this._caseSwap(text);
|
|
144
|
+
case 'whitespace_insert':
|
|
145
|
+
return this._whitespaceInsert(text);
|
|
146
|
+
case 'synonym_replace':
|
|
147
|
+
return this._synonymReplace(text);
|
|
148
|
+
case 'prefix_wrap':
|
|
149
|
+
return this._prefixWrap(text);
|
|
150
|
+
case 'suffix_pad':
|
|
151
|
+
return this._suffixPad(text);
|
|
152
|
+
case 'word_reorder':
|
|
153
|
+
return this._wordReorder(text);
|
|
154
|
+
case 'char_substitute':
|
|
155
|
+
return this._charSubstitute(text);
|
|
156
|
+
case 'fragment':
|
|
157
|
+
return this._fragment(text);
|
|
158
|
+
default:
|
|
159
|
+
return text;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Apply N random mutations to text, chaining them sequentially.
|
|
165
|
+
* @param {string} text - Input attack string.
|
|
166
|
+
* @param {number} [count=1] - Number of random mutations to apply.
|
|
167
|
+
* @returns {string} Mutated text.
|
|
168
|
+
*/
|
|
169
|
+
mutateRandom(text, count = 1) {
|
|
170
|
+
let result = text;
|
|
171
|
+
for (let i = 0; i < count; i++) {
|
|
172
|
+
const idx = Math.floor(this._rng() * this._techniques.length);
|
|
173
|
+
result = this.mutate(result, this._techniques[idx]);
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --- Internal mutation implementations ---
|
|
179
|
+
|
|
180
|
+
/** Random case changes on individual characters. */
|
|
181
|
+
_caseSwap(text) {
|
|
182
|
+
return text.split('').map(c => {
|
|
183
|
+
if (this._rng() < 0.35) {
|
|
184
|
+
return c === c.toUpperCase() ? c.toLowerCase() : c.toUpperCase();
|
|
185
|
+
}
|
|
186
|
+
return c;
|
|
187
|
+
}).join('');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Insert spaces or tabs between random characters. */
|
|
191
|
+
_whitespaceInsert(text) {
|
|
192
|
+
const chars = text.split('');
|
|
193
|
+
const result = [];
|
|
194
|
+
for (let i = 0; i < chars.length; i++) {
|
|
195
|
+
result.push(chars[i]);
|
|
196
|
+
if (this._rng() < 0.2 && chars[i] !== ' ' && chars[i] !== '\t') {
|
|
197
|
+
result.push(this._rng() < 0.7 ? ' ' : '\t');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return result.join('');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Replace keywords with synonyms from the synonym map. */
|
|
204
|
+
_synonymReplace(text) {
|
|
205
|
+
let result = text;
|
|
206
|
+
const keys = Object.keys(SYNONYM_MAP);
|
|
207
|
+
for (const key of keys) {
|
|
208
|
+
const regex = new RegExp('\\b' + key + '\\b', 'gi');
|
|
209
|
+
if (regex.test(result)) {
|
|
210
|
+
const synonyms = SYNONYM_MAP[key];
|
|
211
|
+
const pick = synonyms[Math.floor(this._rng() * synonyms.length)];
|
|
212
|
+
// Only replace first occurrence to keep meaning
|
|
213
|
+
result = result.replace(regex, pick);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Wrap text in an innocent-looking prefix. */
|
|
220
|
+
_prefixWrap(text) {
|
|
221
|
+
const idx = Math.floor(this._rng() * INNOCENT_PREFIXES.length);
|
|
222
|
+
return INNOCENT_PREFIXES[idx] + text;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Append an innocent suffix. */
|
|
226
|
+
_suffixPad(text) {
|
|
227
|
+
const idx = Math.floor(this._rng() * INNOCENT_SUFFIXES.length);
|
|
228
|
+
return text + INNOCENT_SUFFIXES[idx];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Shuffle word order while trying to preserve some structure. */
|
|
232
|
+
_wordReorder(text) {
|
|
233
|
+
const words = text.split(/\s+/);
|
|
234
|
+
if (words.length <= 2) return text;
|
|
235
|
+
// Fisher-Yates shuffle on middle words, keep first and last
|
|
236
|
+
const middle = words.slice(1, -1);
|
|
237
|
+
for (let i = middle.length - 1; i > 0; i--) {
|
|
238
|
+
const j = Math.floor(this._rng() * (i + 1));
|
|
239
|
+
const tmp = middle[i];
|
|
240
|
+
middle[i] = middle[j];
|
|
241
|
+
middle[j] = tmp;
|
|
242
|
+
}
|
|
243
|
+
return [words[0], ...middle, words[words.length - 1]].join(' ');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Replace characters with similar-looking substitutes. */
|
|
247
|
+
_charSubstitute(text) {
|
|
248
|
+
return text.split('').map(c => {
|
|
249
|
+
if (this._rng() < 0.25 && CHAR_SUB_MAP[c]) {
|
|
250
|
+
return CHAR_SUB_MAP[c];
|
|
251
|
+
}
|
|
252
|
+
return c;
|
|
253
|
+
}).join('');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Split attack across multiple lines with filler between. */
|
|
257
|
+
_fragment(text) {
|
|
258
|
+
const words = text.split(/\s+/);
|
|
259
|
+
if (words.length <= 3) return text;
|
|
260
|
+
const mid = Math.floor(words.length / 2);
|
|
261
|
+
const part1 = words.slice(0, mid).join(' ');
|
|
262
|
+
const part2 = words.slice(mid).join(' ');
|
|
263
|
+
const filler = FILLER_LINES[Math.floor(this._rng() * FILLER_LINES.length)];
|
|
264
|
+
return part1 + '\n' + filler + '\n' + part2;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// =========================================================================
|
|
269
|
+
// EVOLUTION SIMULATOR
|
|
270
|
+
// =========================================================================
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Runs adversarial evolution against the detection engine.
|
|
274
|
+
*
|
|
275
|
+
* Seed attacks are mutated across generations. Variants that evade
|
|
276
|
+
* scanText() are "fit" and survive; caught variants are eliminated.
|
|
277
|
+
* The result is a set of evasive attacks that can be used to harden
|
|
278
|
+
* detection patterns.
|
|
279
|
+
*/
|
|
280
|
+
class EvolutionSimulator {
|
|
281
|
+
/**
|
|
282
|
+
* @param {object} [options]
|
|
283
|
+
* @param {number} [options.generations=10] - Number of evolution generations.
|
|
284
|
+
* @param {number} [options.populationSize=50] - Variants per generation.
|
|
285
|
+
* @param {number} [options.mutationRate=0.3] - Probability of extra mutation per variant.
|
|
286
|
+
* @param {number} [options.seed] - PRNG seed for reproducibility.
|
|
287
|
+
*/
|
|
288
|
+
constructor(options = {}) {
|
|
289
|
+
this.generations = options.generations || 10;
|
|
290
|
+
this.populationSize = options.populationSize || 50;
|
|
291
|
+
this.mutationRate = options.mutationRate || 0.3;
|
|
292
|
+
this.seed = options.seed || Date.now();
|
|
293
|
+
this._rng = xorshift32(this.seed);
|
|
294
|
+
this._engine = new MutationEngine({ seed: this.seed });
|
|
295
|
+
this._lastResult = null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Return list of available mutation techniques.
|
|
300
|
+
* @returns {string[]}
|
|
301
|
+
*/
|
|
302
|
+
getMutationTechniques() {
|
|
303
|
+
return this._engine.getTechniques();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Run adversarial evolution starting from seed attacks.
|
|
308
|
+
*
|
|
309
|
+
* @param {string[]} seedAttacks - Array of initial attack strings.
|
|
310
|
+
* @returns {object} Evolution result:
|
|
311
|
+
* - generations: number of generations run
|
|
312
|
+
* - survivors: string[] (attacks that evaded detection)
|
|
313
|
+
* - caught: string[] (attacks that were detected)
|
|
314
|
+
* - evolutionPath: array of per-generation summaries
|
|
315
|
+
* - stats: { totalVariants, survivalRate, catchRate, generationsRun, mutationTechniques }
|
|
316
|
+
*/
|
|
317
|
+
evolve(seedAttacks) {
|
|
318
|
+
if (!Array.isArray(seedAttacks) || seedAttacks.length === 0) {
|
|
319
|
+
return {
|
|
320
|
+
generations: 0,
|
|
321
|
+
survivors: [],
|
|
322
|
+
caught: [],
|
|
323
|
+
evolutionPath: [],
|
|
324
|
+
stats: { totalVariants: 0, survivalRate: 0, catchRate: 0, generationsRun: 0, mutationTechniques: this._engine.getTechniques() },
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Current population starts as seed attacks
|
|
329
|
+
let population = [...seedAttacks];
|
|
330
|
+
const allSurvivors = new Set();
|
|
331
|
+
const allCaught = new Set();
|
|
332
|
+
const evolutionPath = [];
|
|
333
|
+
let totalVariants = 0;
|
|
334
|
+
|
|
335
|
+
for (let gen = 0; gen < this.generations; gen++) {
|
|
336
|
+
// Step 1: Generate mutated variants to fill populationSize
|
|
337
|
+
const variants = [];
|
|
338
|
+
while (variants.length < this.populationSize) {
|
|
339
|
+
// Pick a parent from current population
|
|
340
|
+
const parentIdx = Math.floor(this._rng() * population.length);
|
|
341
|
+
const parent = population[parentIdx];
|
|
342
|
+
|
|
343
|
+
// Apply 1-3 mutations
|
|
344
|
+
const mutationCount = 1 + Math.floor(this._rng() * 3);
|
|
345
|
+
const variant = this._engine.mutateRandom(parent, mutationCount);
|
|
346
|
+
|
|
347
|
+
// Extra mutation based on mutationRate
|
|
348
|
+
const finalVariant = this._rng() < this.mutationRate
|
|
349
|
+
? this._engine.mutateRandom(variant, 1)
|
|
350
|
+
: variant;
|
|
351
|
+
|
|
352
|
+
variants.push(finalVariant);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
totalVariants += variants.length;
|
|
356
|
+
|
|
357
|
+
// Step 2: Test each variant against scanText
|
|
358
|
+
const genSurvivors = [];
|
|
359
|
+
const genCaught = [];
|
|
360
|
+
|
|
361
|
+
for (const variant of variants) {
|
|
362
|
+
const result = scanText(variant, { source: 'evolution-simulator' });
|
|
363
|
+
if (result.status === 'safe' || result.threats.length === 0) {
|
|
364
|
+
// Evaded detection -- "fit"
|
|
365
|
+
genSurvivors.push(variant);
|
|
366
|
+
allSurvivors.add(variant);
|
|
367
|
+
} else {
|
|
368
|
+
// Caught -- eliminated
|
|
369
|
+
genCaught.push(variant);
|
|
370
|
+
allCaught.add(variant);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Step 3: Record generation summary
|
|
375
|
+
evolutionPath.push({
|
|
376
|
+
generation: gen + 1,
|
|
377
|
+
populationSize: variants.length,
|
|
378
|
+
survivors: genSurvivors.length,
|
|
379
|
+
caught: genCaught.length,
|
|
380
|
+
survivalRate: variants.length > 0
|
|
381
|
+
? (genSurvivors.length / variants.length)
|
|
382
|
+
: 0,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Step 4: Next generation population
|
|
386
|
+
// If we have survivors, they become the parents for the next generation
|
|
387
|
+
// If all were caught, keep using original seeds (reset pressure)
|
|
388
|
+
if (genSurvivors.length > 0) {
|
|
389
|
+
population = genSurvivors;
|
|
390
|
+
} else {
|
|
391
|
+
// Re-seed from original attacks to keep evolution going
|
|
392
|
+
population = [...seedAttacks];
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const survivorArray = [...allSurvivors];
|
|
397
|
+
const caughtArray = [...allCaught];
|
|
398
|
+
const total = survivorArray.length + caughtArray.length;
|
|
399
|
+
|
|
400
|
+
this._lastResult = {
|
|
401
|
+
generations: this.generations,
|
|
402
|
+
survivors: survivorArray,
|
|
403
|
+
caught: caughtArray,
|
|
404
|
+
evolutionPath,
|
|
405
|
+
stats: {
|
|
406
|
+
totalVariants,
|
|
407
|
+
survivalRate: total > 0 ? survivorArray.length / total : 0,
|
|
408
|
+
catchRate: total > 0 ? caughtArray.length / total : 0,
|
|
409
|
+
generationsRun: this.generations,
|
|
410
|
+
mutationTechniques: this._engine.getTechniques(),
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
return this._lastResult;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Get a formatted text report of the last evolution run.
|
|
419
|
+
* @returns {string}
|
|
420
|
+
*/
|
|
421
|
+
getReport() {
|
|
422
|
+
if (!this._lastResult) {
|
|
423
|
+
return '[Agent Shield] No evolution run yet. Call evolve() first.';
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const r = this._lastResult;
|
|
427
|
+
const lines = [];
|
|
428
|
+
|
|
429
|
+
lines.push('=== Agent Shield -- Adversarial Evolution Report ===');
|
|
430
|
+
lines.push('');
|
|
431
|
+
lines.push(`Generations: ${r.generations}`);
|
|
432
|
+
lines.push(`Total variants: ${r.stats.totalVariants}`);
|
|
433
|
+
lines.push(`Unique survivors: ${r.survivors.length}`);
|
|
434
|
+
lines.push(`Unique caught: ${r.caught.length}`);
|
|
435
|
+
lines.push(`Survival rate: ${(r.stats.survivalRate * 100).toFixed(1)}%`);
|
|
436
|
+
lines.push(`Catch rate: ${(r.stats.catchRate * 100).toFixed(1)}%`);
|
|
437
|
+
lines.push('');
|
|
438
|
+
lines.push('--- Per-Generation Breakdown ---');
|
|
439
|
+
|
|
440
|
+
for (const gen of r.evolutionPath) {
|
|
441
|
+
const bar = '#'.repeat(Math.round(gen.survivalRate * 20));
|
|
442
|
+
const pad = '.'.repeat(20 - Math.round(gen.survivalRate * 20));
|
|
443
|
+
lines.push(
|
|
444
|
+
` Gen ${String(gen.generation).padStart(3)}: ` +
|
|
445
|
+
`${String(gen.survivors).padStart(4)} survived / ${String(gen.caught).padStart(4)} caught ` +
|
|
446
|
+
`[${bar}${pad}] ${(gen.survivalRate * 100).toFixed(1)}%`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
lines.push('');
|
|
451
|
+
lines.push('--- Mutation Techniques Used ---');
|
|
452
|
+
for (const t of r.stats.mutationTechniques) {
|
|
453
|
+
lines.push(` - ${t}`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (r.survivors.length > 0) {
|
|
457
|
+
lines.push('');
|
|
458
|
+
lines.push('--- Sample Survivors (first 10) ---');
|
|
459
|
+
const samples = r.survivors.slice(0, 10);
|
|
460
|
+
for (let i = 0; i < samples.length; i++) {
|
|
461
|
+
const preview = samples[i].replace(/\n/g, '\\n').substring(0, 100);
|
|
462
|
+
lines.push(` ${i + 1}. ${preview}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
lines.push('');
|
|
467
|
+
lines.push('=== End of Report ===');
|
|
468
|
+
|
|
469
|
+
return lines.join('\n');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// =========================================================================
|
|
474
|
+
// HARDENING FUNCTION
|
|
475
|
+
// =========================================================================
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Analyze surviving (evasive) attacks and generate new detection patterns
|
|
479
|
+
* that would catch them.
|
|
480
|
+
*
|
|
481
|
+
* @param {string[]} survivors - Array of attack strings that evaded detection.
|
|
482
|
+
* @returns {Array<{pattern: string, description: string, catches: string[]}>}
|
|
483
|
+
*/
|
|
484
|
+
function hardenFromEvolution(survivors) {
|
|
485
|
+
if (!Array.isArray(survivors) || survivors.length === 0) {
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const patterns = [];
|
|
490
|
+
const used = new Set();
|
|
491
|
+
|
|
492
|
+
// Strategy 1: Find common keywords across survivors
|
|
493
|
+
const wordFreq = {};
|
|
494
|
+
const suspiciousWords = [
|
|
495
|
+
'ignore', 'disregard', 'bypass', 'skip', 'override', 'forget',
|
|
496
|
+
'reveal', 'show', 'display', 'expose', 'print', 'output',
|
|
497
|
+
'instructions', 'guidelines', 'directives', 'rules', 'commands',
|
|
498
|
+
'previous', 'prior', 'earlier', 'above', 'system', 'prompt',
|
|
499
|
+
'unrestricted', 'jailbreak', 'dan', 'hack', 'exploit',
|
|
500
|
+
'safety', 'security', 'filter', 'restriction', 'disable',
|
|
501
|
+
'overlook', 'dismiss', 'neutralize', 'circumvent', 'cancel',
|
|
502
|
+
'wipe', 'erase', 'clear', 'drop', 'hidden', 'configuration',
|
|
503
|
+
];
|
|
504
|
+
|
|
505
|
+
for (const text of survivors) {
|
|
506
|
+
const lower = text.toLowerCase().replace(/[^a-z\s]/g, ' ');
|
|
507
|
+
const words = lower.split(/\s+/).filter(w => w.length > 2);
|
|
508
|
+
const seen = new Set();
|
|
509
|
+
for (const word of words) {
|
|
510
|
+
if (!seen.has(word) && suspiciousWords.includes(word)) {
|
|
511
|
+
wordFreq[word] = (wordFreq[word] || 0) + 1;
|
|
512
|
+
seen.add(word);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Strategy 2: Find bigram patterns in survivors
|
|
518
|
+
const bigramFreq = {};
|
|
519
|
+
for (const text of survivors) {
|
|
520
|
+
const lower = text.toLowerCase().replace(/[^a-z\s]/g, ' ');
|
|
521
|
+
const words = lower.split(/\s+/).filter(w => w.length > 2);
|
|
522
|
+
for (let i = 0; i < words.length - 1; i++) {
|
|
523
|
+
const bigram = words[i] + ' ' + words[i + 1];
|
|
524
|
+
if (suspiciousWords.includes(words[i]) || suspiciousWords.includes(words[i + 1])) {
|
|
525
|
+
bigramFreq[bigram] = (bigramFreq[bigram] || 0) + 1;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Generate patterns from frequent bigrams (appearing in 2+ survivors)
|
|
531
|
+
const sortedBigrams = Object.entries(bigramFreq)
|
|
532
|
+
.filter(([, count]) => count >= 2)
|
|
533
|
+
.sort((a, b) => b[1] - a[1]);
|
|
534
|
+
|
|
535
|
+
for (const [bigram, count] of sortedBigrams) {
|
|
536
|
+
const key = 'bigram:' + bigram;
|
|
537
|
+
if (used.has(key)) continue;
|
|
538
|
+
used.add(key);
|
|
539
|
+
|
|
540
|
+
const parts = bigram.split(' ');
|
|
541
|
+
// Build a regex that matches the bigram with flexible whitespace
|
|
542
|
+
const regexStr = parts[0] + '\\s+' + parts[1];
|
|
543
|
+
const regex = new RegExp(regexStr, 'i');
|
|
544
|
+
|
|
545
|
+
const catches = survivors.filter(s => regex.test(s.toLowerCase().replace(/[^a-z\s]/g, ' ')));
|
|
546
|
+
if (catches.length >= 2) {
|
|
547
|
+
patterns.push({
|
|
548
|
+
pattern: regexStr,
|
|
549
|
+
description: `Catches "${bigram}" pattern found in ${count} evasive variants.`,
|
|
550
|
+
catches,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Strategy 3: Generate patterns from frequent single keywords with context
|
|
556
|
+
const sortedWords = Object.entries(wordFreq)
|
|
557
|
+
.filter(([, count]) => count >= 3)
|
|
558
|
+
.sort((a, b) => b[1] - a[1]);
|
|
559
|
+
|
|
560
|
+
for (const [word, count] of sortedWords) {
|
|
561
|
+
const key = 'word:' + word;
|
|
562
|
+
if (used.has(key)) continue;
|
|
563
|
+
used.add(key);
|
|
564
|
+
|
|
565
|
+
// Look for common neighbors of this word across survivors
|
|
566
|
+
const neighbors = {};
|
|
567
|
+
for (const text of survivors) {
|
|
568
|
+
const lower = text.toLowerCase().replace(/[^a-z\s]/g, ' ');
|
|
569
|
+
const words = lower.split(/\s+/);
|
|
570
|
+
const idx = words.indexOf(word);
|
|
571
|
+
if (idx >= 0) {
|
|
572
|
+
if (idx > 0 && words[idx - 1].length > 2) {
|
|
573
|
+
neighbors[words[idx - 1]] = (neighbors[words[idx - 1]] || 0) + 1;
|
|
574
|
+
}
|
|
575
|
+
if (idx < words.length - 1 && words[idx + 1].length > 2) {
|
|
576
|
+
neighbors[words[idx + 1]] = (neighbors[words[idx + 1]] || 0) + 1;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Find the most common neighbor
|
|
582
|
+
const topNeighbor = Object.entries(neighbors)
|
|
583
|
+
.sort((a, b) => b[1] - a[1])[0];
|
|
584
|
+
|
|
585
|
+
if (topNeighbor && topNeighbor[1] >= 2) {
|
|
586
|
+
const regexStr = word + '\\s+' + topNeighbor[0];
|
|
587
|
+
const regex = new RegExp(regexStr, 'i');
|
|
588
|
+
const catches = survivors.filter(s => regex.test(s));
|
|
589
|
+
|
|
590
|
+
if (catches.length > 0 && !used.has('bigram:' + word + ' ' + topNeighbor[0])) {
|
|
591
|
+
patterns.push({
|
|
592
|
+
pattern: regexStr,
|
|
593
|
+
description: `Catches "${word}" near "${topNeighbor[0]}" seen in ${count} evasive variants.`,
|
|
594
|
+
catches,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Strategy 4: Character substitution normalization patterns
|
|
601
|
+
// Detect if survivors use leet-speak and generate normalization patterns
|
|
602
|
+
const leetSurvivors = survivors.filter(s => /[@$!+01389457]/.test(s));
|
|
603
|
+
if (leetSurvivors.length >= 2) {
|
|
604
|
+
// Build a pattern that matches common leet-speak versions of key attack words
|
|
605
|
+
const leetWords = [
|
|
606
|
+
{ word: 'ignore', leet: '[i!1]gn[o0]r[e3]' },
|
|
607
|
+
{ word: 'override', leet: '[o0]v[e3]rr[i!1]d[e3]' },
|
|
608
|
+
{ word: 'system', leet: '[s$5]y[s$5][t+7][e3]m' },
|
|
609
|
+
{ word: 'bypass', leet: '[b8]yp[@a4][s$5][s$5]' },
|
|
610
|
+
];
|
|
611
|
+
|
|
612
|
+
for (const { word, leet } of leetWords) {
|
|
613
|
+
const regex = new RegExp(leet, 'i');
|
|
614
|
+
const catches = leetSurvivors.filter(s => regex.test(s));
|
|
615
|
+
if (catches.length >= 1) {
|
|
616
|
+
const key = 'leet:' + word;
|
|
617
|
+
if (!used.has(key)) {
|
|
618
|
+
used.add(key);
|
|
619
|
+
patterns.push({
|
|
620
|
+
pattern: leet,
|
|
621
|
+
description: `Catches leet-speak variant of "${word}" in ${catches.length} evasive variant(s).`,
|
|
622
|
+
catches,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Strategy 5: Fragmented attack detection
|
|
630
|
+
const fragmentedSurvivors = survivors.filter(s => s.includes('\n') && s.split('\n').length >= 3);
|
|
631
|
+
if (fragmentedSurvivors.length >= 1) {
|
|
632
|
+
patterns.push({
|
|
633
|
+
pattern: '(multiline-fragment-detection)',
|
|
634
|
+
description: `${fragmentedSurvivors.length} evasive variant(s) use line fragmentation to split attacks across filler text.`,
|
|
635
|
+
catches: fragmentedSurvivors,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return patterns;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// =========================================================================
|
|
643
|
+
// EXPORTS
|
|
644
|
+
// =========================================================================
|
|
645
|
+
|
|
646
|
+
module.exports = {
|
|
647
|
+
EvolutionSimulator,
|
|
648
|
+
MutationEngine,
|
|
649
|
+
hardenFromEvolution,
|
|
650
|
+
};
|