sekant-intercept-js 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +231 -0
- package/NOTICE +15 -0
- package/README.md +327 -0
- package/dist/sekant-intercept.browser.mjs +15 -0
- package/dist/sekant-intercept.node.mjs +15 -0
- package/package.json +69 -0
- package/src/ahocorasickEngine.mjs +229 -0
- package/src/elfModule.mjs +306 -0
- package/src/hashModule.mjs +284 -0
- package/src/index.js +9 -0
- package/src/interceptCustomModules.mjs +230 -0
- package/src/interceptScanner.mjs +865 -0
- package/src/mathModule.mjs +506 -0
- package/src/peModule.mjs +295 -0
- package/src/performanceInstrumentation.mjs +172 -0
- package/src/sharedUtils.mjs +127 -0
- package/src/stringModule.mjs +43 -0
- package/src/timeModule.mjs +19 -0
- package/src/yaraConditionParser.mjs +1083 -0
- package/src/yaraConditionsMatch.mjs +1373 -0
- package/src/yaraRuleCompiler.mjs +448 -0
- package/src/yaraStringMatch.mjs +449 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Rishi Kant (Sekant Security Inc.)
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const MAX_STRING_MATCH_LENGTH = 2048;
|
|
18
|
+
export const MAX_MATCHES = 100;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Compile a YARA-like string definition into a matcher function.
|
|
22
|
+
* Supports: text, wide, ascii, xor, base64/base64wide, regex, hex, fullword, nocase
|
|
23
|
+
*/
|
|
24
|
+
export function compileYaraLike(definition) {
|
|
25
|
+
definition = definition.trim();
|
|
26
|
+
|
|
27
|
+
const isRegex = definition.startsWith("/");
|
|
28
|
+
const isHex = definition.startsWith("{");
|
|
29
|
+
const quoteMatch = definition.match(/^"(.*)"(.*)$/);
|
|
30
|
+
|
|
31
|
+
if (isRegex) {
|
|
32
|
+
return makeRegexMatcher(definition);
|
|
33
|
+
} else if (isHex) {
|
|
34
|
+
const mods = definition.slice(definition.lastIndexOf("}") + 1);
|
|
35
|
+
return makeHexMatcher(definition.slice(1, definition.lastIndexOf("}")).trim(), mods);
|
|
36
|
+
} else if (quoteMatch) {
|
|
37
|
+
const [, text, modsRaw] = quoteMatch;
|
|
38
|
+
const mods = modsRaw.toLowerCase();
|
|
39
|
+
|
|
40
|
+
const opts = {
|
|
41
|
+
wide: /\bwide\b/.test(mods),
|
|
42
|
+
xor: parseXorRange(mods),
|
|
43
|
+
// allowKey0: /\bxor\b/.test(mods) && !/\(0\)/.test(mods),
|
|
44
|
+
allowKey0: true,
|
|
45
|
+
ascii: /\bascii\b/.test(mods) || !/\bwide\b/.test(mods),
|
|
46
|
+
base64: /\bbase64\b/.test(mods),
|
|
47
|
+
base64wide: /\bbase64wide\b/.test(mods),
|
|
48
|
+
fullword: /\bfullword\b/.test(mods),
|
|
49
|
+
nocase: /\bnocase\b/.test(mods),
|
|
50
|
+
private: /\bprivate\b/.test(mods),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return makeTextMatcher(text, opts);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw new Error("Unsupported definition: " + definition);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function addOptionsToMatcher(matcher) {
|
|
60
|
+
return (data, offset = -1, maxLength = MAX_STRING_MATCH_LENGTH) => {
|
|
61
|
+
// If offset is specified, slice the data accordingly and limit length for matching
|
|
62
|
+
const singleRun = offset >= 0;
|
|
63
|
+
// Start 1 byte before to do fullword checks if needed
|
|
64
|
+
offset = Math.max(0, offset - 1);
|
|
65
|
+
data = singleRun ? data.slice(offset, offset + maxLength) : data;
|
|
66
|
+
return matcher(data, singleRun ? offset : -1);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ╭─────────────────────────────╮
|
|
71
|
+
// │ REGEX MATCHER │
|
|
72
|
+
// ╰─────────────────────────────╯
|
|
73
|
+
function makeRegexMatcher(definition) {
|
|
74
|
+
const mods = definition.split("/").pop().toLowerCase() ?? "";
|
|
75
|
+
const regexFlags = mods.split(" ")[0];
|
|
76
|
+
const isPrivate = /\bprivate\b/.test(mods);
|
|
77
|
+
const pattern = definition.slice(1, definition.lastIndexOf("/"));
|
|
78
|
+
const nocase = regexFlags.includes("i");
|
|
79
|
+
const flags = "g" + (regexFlags.includes("s") ? "s" : "") + (nocase ? "i" : "");
|
|
80
|
+
const literalPrefix = getRegexPrefix(pattern, nocase);
|
|
81
|
+
const re = new RegExp(pattern, flags);
|
|
82
|
+
|
|
83
|
+
const matcher = (data, offset = -1) => {
|
|
84
|
+
const encoder = new TextEncoder();
|
|
85
|
+
const text = new TextDecoder().decode(data);
|
|
86
|
+
const results = [];
|
|
87
|
+
const singleRun = offset >= 0;
|
|
88
|
+
offset = offset < 0 ? 0 : offset;
|
|
89
|
+
let m;
|
|
90
|
+
while ((m = re.exec(text)) !== null && results.length < MAX_MATCHES) {
|
|
91
|
+
const byteIndex = encoder.encode(text.slice(0, m.index)).length;
|
|
92
|
+
results.push({ offset: byteIndex + offset, length: encoder.encode(m[0]).length });
|
|
93
|
+
re.lastIndex = m.index + 1; // Move forward to find overlapping matches
|
|
94
|
+
if (singleRun) break;
|
|
95
|
+
}
|
|
96
|
+
return results;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return { matcher: addOptionsToMatcher(matcher), literalPrefix, type: "regex", nocase, private: isPrivate };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getRegexPrefix(pattern, nocase = false) {
|
|
103
|
+
// Remove leading ^ if present
|
|
104
|
+
pattern = pattern.replace(/^\^/, "");
|
|
105
|
+
pattern = nocase ? pattern.toLowerCase() : pattern;
|
|
106
|
+
|
|
107
|
+
// Match literal prefix before first meta pattern
|
|
108
|
+
// const match = pattern.match(/^((?:\\.|[^\\[\]()|*+?{}.])+)/);
|
|
109
|
+
// Match literal prefix: either escaped chars (\. \? etc) or non-special chars
|
|
110
|
+
const match = pattern.match(/^((?:\\[^bBdDwWsSAZzGnrtfv0u]|[^\\[\]()|*+?{}.])+)/);
|
|
111
|
+
if (!match || match.length < 2) return "";
|
|
112
|
+
|
|
113
|
+
// Unescape hex sequences like \x41 → A
|
|
114
|
+
let prefix = match[1].replace(/\\x([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
115
|
+
|
|
116
|
+
// Unescape escaped characters like \. → .
|
|
117
|
+
return prefix.replace(/\\(.)/g, "$1");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ╭─────────────────────────────╮
|
|
121
|
+
// │ TEXT MATCHER GENERATOR │
|
|
122
|
+
// ╰─────────────────────────────╯
|
|
123
|
+
function makeTextMatcher(text, opts) {
|
|
124
|
+
const encoder = new TextEncoder();
|
|
125
|
+
const patterns = [];
|
|
126
|
+
|
|
127
|
+
// Default to ASCII if wide is not specified
|
|
128
|
+
if (!opts.wide) opts.ascii = true;
|
|
129
|
+
|
|
130
|
+
// Skip invalid combinations
|
|
131
|
+
if (opts.base64 || opts.base64wide) {
|
|
132
|
+
opts.nocase = false; // Disable nocase for conflicting base64 types
|
|
133
|
+
opts.fullword = false; // Disable fullword for conflicting base64 types
|
|
134
|
+
opts.xor = null; // Disable xor for conflicting base64 types
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Unescape YARA string escape sequences
|
|
138
|
+
const unescapedText = text
|
|
139
|
+
.replace(/\\n/g, "\n")
|
|
140
|
+
.replace(/\\r/g, "\r")
|
|
141
|
+
.replace(/\\t/g, "\t")
|
|
142
|
+
.replace(/\\x([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
143
|
+
.replace(/\\\\/g, "\\")
|
|
144
|
+
.replace(/\\"/g, '"')
|
|
145
|
+
.replace(/\\'/g, "'");
|
|
146
|
+
|
|
147
|
+
// Handle nocase by forcing to lower case
|
|
148
|
+
const textVariants = opts.nocase ? [unescapedText.toLowerCase()] : [unescapedText];
|
|
149
|
+
|
|
150
|
+
for (const variant of textVariants) {
|
|
151
|
+
if (opts.base64 || opts.base64wide) {
|
|
152
|
+
// When using base64/base64wide, generate patterns from the original text
|
|
153
|
+
// in both ASCII and wide forms as needed
|
|
154
|
+
if (opts.base64) {
|
|
155
|
+
if (opts.ascii) {
|
|
156
|
+
patterns.push(...yaraBase64Variants(variant, false).map((bytes) => ({ bytes, isWide: false })));
|
|
157
|
+
}
|
|
158
|
+
if (opts.wide) {
|
|
159
|
+
patterns.push(...yaraBase64Variants(toWideBytes(variant), false).map((bytes) => ({ bytes, isWide: true })));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (opts.base64wide) {
|
|
163
|
+
if (opts.ascii) {
|
|
164
|
+
// base64wide returns Uint8Arrays when wide=true, so don't encode again
|
|
165
|
+
patterns.push(...yaraBase64Variants(variant, true).map((bytes) => ({ bytes, isWide: true })));
|
|
166
|
+
}
|
|
167
|
+
if (opts.wide) {
|
|
168
|
+
// base64wide returns Uint8Arrays when wide=true, so don't encode again
|
|
169
|
+
patterns.push(...yaraBase64Variants(toWideBytes(variant), true).map((bytes) => ({ bytes, isWide: true })));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// Regular text patterns
|
|
174
|
+
if (opts.ascii) {
|
|
175
|
+
patterns.push({ bytes: encoder.encode(variant), isWide: false });
|
|
176
|
+
}
|
|
177
|
+
if (opts.wide) {
|
|
178
|
+
patterns.push({ bytes: toWideBytes(variant), isWide: true });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const matcher = (data, offset = -1) => {
|
|
184
|
+
let matches = [];
|
|
185
|
+
const singleRun = offset >= 0;
|
|
186
|
+
offset = offset < 0 ? 0 : offset;
|
|
187
|
+
|
|
188
|
+
for (const { bytes, isWide } of patterns) {
|
|
189
|
+
if (opts.xor) {
|
|
190
|
+
const xorMatches = findXorMatches(data, bytes, opts.xor, opts.allowKey0, singleRun);
|
|
191
|
+
// console.log(xorMatches);
|
|
192
|
+
matches.push(...xorMatches.map((m) => ({ ...m, length: bytes.length, isWide })));
|
|
193
|
+
} else {
|
|
194
|
+
const plainMatches = findPlainMatches(data, bytes, opts.nocase, singleRun);
|
|
195
|
+
matches.push(...plainMatches.map((m) => ({ ...m, length: bytes.length, isWide })));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (opts.fullword) {
|
|
200
|
+
matches = matches.filter((m) => isFullWordMatch(data, m.offset, m.length));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Adjust the offsets for the original data if offset was specified
|
|
204
|
+
matches.forEach((m) => {
|
|
205
|
+
m.offset += offset;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return matches;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
return { matcher: addOptionsToMatcher(matcher), patterns, type: "text", xor: opts.xor, nocase: opts.nocase, private: opts.private };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ╭─────────────────────────────╮
|
|
215
|
+
// │ XOR-AWARE MATCHING │
|
|
216
|
+
// ╰─────────────────────────────╯
|
|
217
|
+
function findXorMatches(data, pattern, keyRange, allowKey0 = true, singleRun = false) {
|
|
218
|
+
const [minKey, maxKey] = keyRange;
|
|
219
|
+
const matches = [];
|
|
220
|
+
const pLen = pattern.length;
|
|
221
|
+
const dLen = data.length;
|
|
222
|
+
if (pLen === 0 || pLen > dLen) return matches;
|
|
223
|
+
|
|
224
|
+
for (let candidateKey = minKey; candidateKey <= maxKey; candidateKey++) {
|
|
225
|
+
for (let i = 0; i <= dLen - pLen; i++) {
|
|
226
|
+
let ok = true;
|
|
227
|
+
for (let j = 0; j < pLen; j++) {
|
|
228
|
+
if ((data[i + j] ^ candidateKey) !== pattern[j]) {
|
|
229
|
+
ok = false;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (ok) {
|
|
234
|
+
matches.push({ offset: i, key: candidateKey });
|
|
235
|
+
if (singleRun) break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return matches;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ╭─────────────────────────────╮
|
|
243
|
+
// │ PLAIN MATCHING │
|
|
244
|
+
// ╰─────────────────────────────╯
|
|
245
|
+
function findPlainMatches(data, pattern, nocase = false, singleRun = false) {
|
|
246
|
+
const matches = [];
|
|
247
|
+
const pLen = pattern.length;
|
|
248
|
+
const dLen = data.length;
|
|
249
|
+
outer: for (let i = 0; i <= dLen - pLen; i++) {
|
|
250
|
+
for (let j = 0; j < pLen; j++) {
|
|
251
|
+
if ((nocase ? lowercaseByte(data[i + j]) : data[i + j]) !== pattern[j]) continue outer;
|
|
252
|
+
}
|
|
253
|
+
matches.push({ offset: i });
|
|
254
|
+
if (singleRun) break;
|
|
255
|
+
}
|
|
256
|
+
return matches;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function lowercaseByte(b) {
|
|
260
|
+
if (b >= 0x41 && b <= 0x5a) {
|
|
261
|
+
return b + 0x20;
|
|
262
|
+
}
|
|
263
|
+
return b;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ╭─────────────────────────────╮
|
|
267
|
+
// │ FULLWORD CHECK │
|
|
268
|
+
// ╰─────────────────────────────╯
|
|
269
|
+
function isFullWordMatch(data, offset, length) {
|
|
270
|
+
const isWordChar = (b) =>
|
|
271
|
+
(b >= 0x30 && b <= 0x39) || // 0-9
|
|
272
|
+
(b >= 0x41 && b <= 0x5a) || // A-Z
|
|
273
|
+
(b >= 0x61 && b <= 0x7a); // a-z
|
|
274
|
+
const before = offset === 0 || !isWordChar(data[offset - 1]);
|
|
275
|
+
const after = offset + length >= data.length || !isWordChar(data[offset + length]);
|
|
276
|
+
return before && after;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ╭─────────────────────────────╮
|
|
280
|
+
// │ WIDE BYTES │
|
|
281
|
+
// ╰─────────────────────────────╯
|
|
282
|
+
function toWideBytes(text) {
|
|
283
|
+
const buf = new Uint8Array(text.length * 2);
|
|
284
|
+
for (let i = 0; i < text.length; i++) {
|
|
285
|
+
const code = text.charCodeAt(i);
|
|
286
|
+
buf[i * 2] = code & 0xff;
|
|
287
|
+
buf[i * 2 + 1] = code >> 8;
|
|
288
|
+
}
|
|
289
|
+
return buf;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ╭─────────────────────────────╮
|
|
293
|
+
// │ PARSE XOR RANGE │
|
|
294
|
+
// ╰─────────────────────────────╯
|
|
295
|
+
function parseXorRange(mods) {
|
|
296
|
+
const m = mods.match(/xor(?:\((0x[0-9a-f]+|\d+)(?:-(0x[0-9a-f]+|\d+))?\))?/i);
|
|
297
|
+
if (!m) return null;
|
|
298
|
+
const [, start, end] = m;
|
|
299
|
+
if (!start && !end) return [0, 255]; // YARA starts from 0 if no range specified
|
|
300
|
+
|
|
301
|
+
let startVal = start ? parseInt(start, start.startsWith("0x") ? 16 : 10) : null;
|
|
302
|
+
let endVal = end ? parseInt(end, end.startsWith("0x") ? 16 : 10) : null;
|
|
303
|
+
|
|
304
|
+
if (startVal !== null && endVal === null) return [startVal, startVal];
|
|
305
|
+
if (endVal !== null && startVal === null) return [endVal, endVal];
|
|
306
|
+
|
|
307
|
+
return [startVal, endVal];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ╭─────────────────────────────╮
|
|
311
|
+
// │ HEX MATCHER GENERATOR │
|
|
312
|
+
// ╰─────────────────────────────╯
|
|
313
|
+
|
|
314
|
+
// Global constant for max jump range when not specified
|
|
315
|
+
const MAX_HEX_JUMP = 10000;
|
|
316
|
+
|
|
317
|
+
function makeHexMatcher(hexBody, mods) {
|
|
318
|
+
// Parse hex body into regex pattern
|
|
319
|
+
let pattern = "";
|
|
320
|
+
hexBody = hexBody?.toUpperCase().replace(/[^0-9A-F?~|()[\-\]]/g, "") || ""; // Trim invalid chars
|
|
321
|
+
const parts = hexBody.match(/\[\d*(-\d*)?\]|\(.*\)|~?[0-9A-F?]{2}/g) || [];
|
|
322
|
+
const isPrivate = /\bprivate\b/.test(mods);
|
|
323
|
+
|
|
324
|
+
if (parts.length === 0) {
|
|
325
|
+
// Empty pattern matches nothing
|
|
326
|
+
const matcher = (data) => [];
|
|
327
|
+
return { matcher, type: "hex", private: isPrivate };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for (const token of parts) {
|
|
331
|
+
if (token.startsWith("[")) {
|
|
332
|
+
// Jump: [N] or [N-M] or [N-] or [-M] or [-]
|
|
333
|
+
const dashIndex = token.indexOf("-");
|
|
334
|
+
if (dashIndex === -1) {
|
|
335
|
+
// [N] - exact count
|
|
336
|
+
const count = parseInt(token.slice(1, -1));
|
|
337
|
+
pattern += `(?:[0-9A-F]{2}){${count}}`;
|
|
338
|
+
} else {
|
|
339
|
+
// [N-M] or [N-] or [-M] or [-] - range
|
|
340
|
+
const match = /\[(\d*)-(\d*)\]/.exec(token);
|
|
341
|
+
const min = parseInt(match[1]) || 0;
|
|
342
|
+
const max = parseInt(match[2]) || MAX_HEX_JUMP;
|
|
343
|
+
pattern += `(?:[0-9A-F]{2}){${min},${max}}`;
|
|
344
|
+
}
|
|
345
|
+
} else if (token.startsWith("(")) {
|
|
346
|
+
// Group: (XX | ?X | ?? | ~XX | ~?X)
|
|
347
|
+
pattern += convertNegationToRegex(token).replace(/\?/g, "[0-9A-F]");
|
|
348
|
+
} else if (token.startsWith("~")) {
|
|
349
|
+
// Negated byte: ~XX
|
|
350
|
+
pattern += convertNegationToRegex(token);
|
|
351
|
+
} else {
|
|
352
|
+
// Wildcard: ?? or ?X or X?
|
|
353
|
+
// Literals: XX
|
|
354
|
+
pattern += token.replace(/\?/g, "[0-9A-F]");
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const regex = new RegExp(pattern, "g");
|
|
359
|
+
|
|
360
|
+
const matcher = (data) => {
|
|
361
|
+
// Convert UInt8Array to hex string; if string passed, assumes it is cached hex string
|
|
362
|
+
const hexString = (typeof data === "string") ? data : Array.from(data)
|
|
363
|
+
.map((b) => b.toString(16).toUpperCase().padStart(2, "0"))
|
|
364
|
+
.join("");
|
|
365
|
+
|
|
366
|
+
const matches = [];
|
|
367
|
+
let m;
|
|
368
|
+
while ((m = regex.exec(hexString)) !== null && matches.length < MAX_MATCHES) {
|
|
369
|
+
// Convert hex string offset to byte offset
|
|
370
|
+
if (m.index % 2 !== 0) {
|
|
371
|
+
// Skip to next aligned position to check
|
|
372
|
+
regex.lastIndex = m.index + 1;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
// Ignoring as better to overmatch than undermatch
|
|
376
|
+
// if (m[0].length % 2 !== 0) {
|
|
377
|
+
// // Skip non-aligned length matches
|
|
378
|
+
// continue;
|
|
379
|
+
// }
|
|
380
|
+
matches.push({ offset: m.index / 2 });
|
|
381
|
+
regex.lastIndex = m.index + 2; // Move forward to find overlapping matches
|
|
382
|
+
}
|
|
383
|
+
return matches;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
return { matcher, type: "hex", private: isPrivate };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// function addAllOverlappingMatches(regex, text, textIndex = 0, matches = [], indexStep = 1) {
|
|
390
|
+
// if (!regex || !text || !matches) return;
|
|
391
|
+
// const newRegex = new RegExp(regex.source, regex.flags.replace("g", ""));
|
|
392
|
+
// text = text.slice(indexStep); // Start from next indexStep to check overlaps
|
|
393
|
+
// let additionalOffset = indexStep;
|
|
394
|
+
// let m;
|
|
395
|
+
// while (text?.length > 0 && (m = newRegex.exec(text)) !== null) {
|
|
396
|
+
// if (indexStep > 1 && (m.index % indexStep !== 0 || m[0].length % indexStep !== 0 )) {
|
|
397
|
+
// // Jump to next aligned position
|
|
398
|
+
// const nextAlignedIndex = indexStep * Math.ceil(m.index / indexStep);
|
|
399
|
+
// text = text.slice(nextAlignedIndex);
|
|
400
|
+
// additionalOffset += nextAlignedIndex;
|
|
401
|
+
// continue;
|
|
402
|
+
// } // Skip non-aligned matches
|
|
403
|
+
// matches.push({ offset: (textIndex + m.index + additionalOffset) / indexStep, length: m[0].length / indexStep });
|
|
404
|
+
// text = text.slice(m.index + indexStep);
|
|
405
|
+
// additionalOffset += m.index + indexStep;
|
|
406
|
+
// }
|
|
407
|
+
// return matches;
|
|
408
|
+
// }
|
|
409
|
+
|
|
410
|
+
function convertNegationToRegex(token) {
|
|
411
|
+
if (!token || !token.includes("~")) return token;
|
|
412
|
+
const matches = token.split("~");
|
|
413
|
+
let pattern = matches[0];
|
|
414
|
+
for (let i = 1; i < matches.length; i++) {
|
|
415
|
+
const nibbles = matches[i].slice(0, 2).split("");
|
|
416
|
+
nibbles[0] = nibbles[0] === "?" ? "[0-9A-F]" : `${nibbles[0]}`;
|
|
417
|
+
nibbles[1] = nibbles[1] === "?" ? "[0-9A-F]" : `${nibbles[1]}`;
|
|
418
|
+
pattern += `(?!${nibbles[0]}${nibbles[1]})[0-9A-F]{2}${matches[i].slice(2)}`;
|
|
419
|
+
}
|
|
420
|
+
return pattern;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ╭─────────────────────────────╮
|
|
424
|
+
// │ YARA BASE64 / BASE64WIDE │
|
|
425
|
+
// ╰─────────────────────────────╯
|
|
426
|
+
function yaraBase64Variants(input, wide = false) {
|
|
427
|
+
// Accept either string or Uint8Array (for when 'wide' modifier is applied first)
|
|
428
|
+
const encoder = new TextEncoder();
|
|
429
|
+
let bytes;
|
|
430
|
+
if (typeof input === "string") {
|
|
431
|
+
bytes = encoder.encode(input);
|
|
432
|
+
} else {
|
|
433
|
+
bytes = input; // Already Uint8Array from toWideBytes()
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const variants = [];
|
|
437
|
+
const dropCharsMap = [0, 2, 3];
|
|
438
|
+
for (let offset = 0; offset < 3; offset++) {
|
|
439
|
+
const padded = new Uint8Array(offset + bytes.length);
|
|
440
|
+
padded.set(bytes, offset);
|
|
441
|
+
let b64 = btoa(String.fromCharCode(...padded));
|
|
442
|
+
const dropChars = dropCharsMap[offset];
|
|
443
|
+
const trailing = (/=*$/.exec(b64 ?? [0]))[0].length;
|
|
444
|
+
const b64string = b64.slice(dropChars, b64.length - (trailing ? trailing + 1 : 0));
|
|
445
|
+
variants.push(wide ? toWideBytes(b64string) : encoder.encode(b64string));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return variants.filter((v) => v.length >= 1);
|
|
449
|
+
}
|