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,448 @@
|
|
|
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
|
+
import { compileYaraLike } from './yaraStringMatch.mjs';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* List of supported YARA modules
|
|
21
|
+
* All modules are automatically available, but imports are validated
|
|
22
|
+
*/
|
|
23
|
+
const SUPPORTED_MODULES = [
|
|
24
|
+
'pe', // Windows PE file format module
|
|
25
|
+
'elf', // Linux/Unix ELF file format module
|
|
26
|
+
'math', // Mathematical and statistical functions
|
|
27
|
+
'hash', // Cryptographic hash functions (MD5, SHA1, SHA256, CRC32)
|
|
28
|
+
'time', // Time-related functions
|
|
29
|
+
'string', // String manipulation functions
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse import statements from YARA rule text
|
|
34
|
+
* Validates that all imported modules are supported
|
|
35
|
+
* @param {string} text - The YARA rule text (before comment stripping)
|
|
36
|
+
* @returns {string[]} Array of imported module names
|
|
37
|
+
* @throws {Error} If an unsupported module is imported
|
|
38
|
+
*/
|
|
39
|
+
function parseImports(text) {
|
|
40
|
+
const imports = [];
|
|
41
|
+
const importRegex = /^\s*import\s+"(\w+)"\s*$/gm;
|
|
42
|
+
let match;
|
|
43
|
+
|
|
44
|
+
while ((match = importRegex.exec(text)) !== null) {
|
|
45
|
+
const moduleName = match[1];
|
|
46
|
+
|
|
47
|
+
// Validate that the module is supported
|
|
48
|
+
if (!SUPPORTED_MODULES.includes(moduleName)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Unsupported module import: "${moduleName}". ` +
|
|
51
|
+
`Supported modules are: ${SUPPORTED_MODULES.join(', ')}. ` +
|
|
52
|
+
`Note: All supported modules are automatically available; ` +
|
|
53
|
+
`the import statement is only for validation.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
imports.push(moduleName);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return imports;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Remove comments from YARA rule text
|
|
65
|
+
* Handles both single-line (//) and multi-line C-style comments
|
|
66
|
+
* Preserves strings to avoid removing comment-like patterns inside them
|
|
67
|
+
* @param {string} text - The text to strip comments from
|
|
68
|
+
* @returns {string} Text with comments removed
|
|
69
|
+
*/
|
|
70
|
+
function stripComments(text) {
|
|
71
|
+
let result = '';
|
|
72
|
+
let i = 0;
|
|
73
|
+
const len = text.length;
|
|
74
|
+
|
|
75
|
+
while (i < len) {
|
|
76
|
+
// Check for string literals (preserve them as-is)
|
|
77
|
+
if (text[i] === '"' || text[i] === "'") {
|
|
78
|
+
const quote = text[i];
|
|
79
|
+
result += text[i++];
|
|
80
|
+
|
|
81
|
+
// Copy the entire string literal, including escaped quotes
|
|
82
|
+
while (i < len) {
|
|
83
|
+
if (text[i] === '\\' && i + 1 < len) {
|
|
84
|
+
result += text[i++]; // backslash
|
|
85
|
+
result += text[i++]; // escaped character
|
|
86
|
+
} else if (text[i] === quote) {
|
|
87
|
+
result += text[i++]; // closing quote
|
|
88
|
+
break;
|
|
89
|
+
} else {
|
|
90
|
+
result += text[i++];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Check for single-line comment
|
|
95
|
+
else if (text[i] === '/' && i + 1 < len && text[i + 1] === '/') {
|
|
96
|
+
// Skip until end of line
|
|
97
|
+
i += 2;
|
|
98
|
+
while (i < len && text[i] !== '\n') {
|
|
99
|
+
i++;
|
|
100
|
+
}
|
|
101
|
+
// Keep the newline for line integrity
|
|
102
|
+
if (i < len && text[i] === '\n') {
|
|
103
|
+
result += text[i++];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Check for multi-line comment
|
|
107
|
+
else if (text[i] === '/' && i + 1 < len && text[i + 1] === '*') {
|
|
108
|
+
// Skip until closing */
|
|
109
|
+
i += 2;
|
|
110
|
+
while (i < len - 1) {
|
|
111
|
+
if (text[i] === '*' && text[i + 1] === '/') {
|
|
112
|
+
i += 2;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
// Preserve newlines to maintain line numbers for error reporting
|
|
116
|
+
if (text[i] === '\n') {
|
|
117
|
+
result += '\n';
|
|
118
|
+
}
|
|
119
|
+
i++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Regular character
|
|
123
|
+
else {
|
|
124
|
+
result += text[i++];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Count braces in a line while ignoring those inside string literals
|
|
133
|
+
* @param {string} line - The line to count braces in
|
|
134
|
+
* @returns {Object} Object with openCount and closeCount
|
|
135
|
+
*/
|
|
136
|
+
function countBracesOutsideStrings(line) {
|
|
137
|
+
let openCount = 0;
|
|
138
|
+
let closeCount = 0;
|
|
139
|
+
let inString = false;
|
|
140
|
+
let stringChar = null;
|
|
141
|
+
let i = 0;
|
|
142
|
+
|
|
143
|
+
while (i < line.length) {
|
|
144
|
+
const char = line[i];
|
|
145
|
+
|
|
146
|
+
// Handle escape sequences in strings
|
|
147
|
+
if (inString && char === '\\' && i + 1 < line.length) {
|
|
148
|
+
i += 2; // Skip the backslash and the next character
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check for string start/end
|
|
153
|
+
if ((char === '"' || char === "'") && !inString) {
|
|
154
|
+
inString = true;
|
|
155
|
+
stringChar = char;
|
|
156
|
+
} else if (char === stringChar && inString) {
|
|
157
|
+
inString = false;
|
|
158
|
+
stringChar = null;
|
|
159
|
+
}
|
|
160
|
+
// Count braces only when not inside a string
|
|
161
|
+
else if (!inString) {
|
|
162
|
+
if (char === '{') {
|
|
163
|
+
openCount++;
|
|
164
|
+
} else if (char === '}') {
|
|
165
|
+
closeCount++;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
i++;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { openCount, closeCount };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function parseYaraRuleGroup(multiRuleText, existingRules = []) {
|
|
176
|
+
// Parse and validate import statements before processing rules
|
|
177
|
+
// This will throw an error if an unsupported module is imported
|
|
178
|
+
parseImports(multiRuleText);
|
|
179
|
+
|
|
180
|
+
// Strip import statements from the text after validation
|
|
181
|
+
// This prevents them from interfering with rule parsing
|
|
182
|
+
multiRuleText = multiRuleText.replace(/^\s*import\s+"[^"]+"\s*$/gm, '');
|
|
183
|
+
|
|
184
|
+
const rules = existingRules ?? [];
|
|
185
|
+
let currentRuleText = '';
|
|
186
|
+
let braceDepth = 0;
|
|
187
|
+
let inRule = false;
|
|
188
|
+
|
|
189
|
+
const lines = multiRuleText.split('\n');
|
|
190
|
+
for (let line of lines) {
|
|
191
|
+
// Check for the start of a rule (with or without private modifier)
|
|
192
|
+
if (!inRule && line.match(/^\s*(?:\s*(?:private|global)\s+)*rule\b/i)) {
|
|
193
|
+
inRule = true;
|
|
194
|
+
currentRuleText = line + '\n';
|
|
195
|
+
// Count braces while ignoring those inside strings
|
|
196
|
+
const braceCounts = countBracesOutsideStrings(line);
|
|
197
|
+
braceDepth = braceCounts.openCount - braceCounts.closeCount;
|
|
198
|
+
const atleastOneBrace = (braceCounts.openCount > 0);
|
|
199
|
+
|
|
200
|
+
// Handle single-line rules where braces balance immediately
|
|
201
|
+
if (braceDepth === 0 && atleastOneBrace) {
|
|
202
|
+
rules.push(parseYaraRule(currentRuleText));
|
|
203
|
+
inRule = false;
|
|
204
|
+
currentRuleText = '';
|
|
205
|
+
}
|
|
206
|
+
} else if (inRule) {
|
|
207
|
+
currentRuleText += line + '\n';
|
|
208
|
+
// Update brace depth while ignoring braces inside strings
|
|
209
|
+
const braceCounts = countBracesOutsideStrings(line);
|
|
210
|
+
braceDepth += braceCounts.openCount - braceCounts.closeCount;
|
|
211
|
+
|
|
212
|
+
// Check for end of rule
|
|
213
|
+
if (braceDepth === 0) {
|
|
214
|
+
// Complete rule found
|
|
215
|
+
rules.push(parseYaraRule(currentRuleText));
|
|
216
|
+
inRule = false;
|
|
217
|
+
currentRuleText = '';
|
|
218
|
+
// console.log("Pushed rule: ", rules[rules.length - 1].name);
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
// Not in a rule, ignore the line
|
|
222
|
+
// console.log('Ignoring line outside of rule:', line);
|
|
223
|
+
}
|
|
224
|
+
// Lines outside of rules are ignored
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Ensure each rule has a unique ID
|
|
228
|
+
rules.forEach((rule, index) => {
|
|
229
|
+
rule.id = index + 1;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return rules;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Parse a YARA rule and return an object representation
|
|
237
|
+
* @param {string} ruleText - The complete YARA rule text
|
|
238
|
+
* @returns {Object} Parsed rule object with metadata, strings, and condition
|
|
239
|
+
*/
|
|
240
|
+
export function parseYaraRule(ruleText) {
|
|
241
|
+
// Strip comments before parsing
|
|
242
|
+
ruleText = stripComments(ruleText).trim();
|
|
243
|
+
|
|
244
|
+
// Check for private modifier
|
|
245
|
+
const isPrivate = /^.*\bprivate\b.*rule\s+/i.test(ruleText);
|
|
246
|
+
|
|
247
|
+
// Check for global modifier
|
|
248
|
+
const isGlobal = /^.*\bglobal\b.*rule\s+/i.test(ruleText);
|
|
249
|
+
|
|
250
|
+
// Extract rule name and tags (with or without private modifier)
|
|
251
|
+
const ruleMatch = ruleText.match(/^(?:.*)?rule\s+(\w+)(?:\s*:\s*([\w\s]+))?\s*\{/);
|
|
252
|
+
if (!ruleMatch) {
|
|
253
|
+
throw new Error('Invalid YARA rule: missing rule declaration');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const ruleName = ruleMatch[1];
|
|
257
|
+
const tags = ruleMatch[2] ? ruleMatch[2].trim().split(/\s+/) : [];
|
|
258
|
+
|
|
259
|
+
// Extract the rule body (everything between the outer braces)
|
|
260
|
+
const bodyMatch = ruleText.match(/\{([\s\S]*)\}/);
|
|
261
|
+
if (!bodyMatch) {
|
|
262
|
+
throw new Error('Invalid YARA rule: missing rule body');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const ruleBody = bodyMatch[1];
|
|
266
|
+
|
|
267
|
+
// Parse metadata section
|
|
268
|
+
const metadata = parseMetadata(ruleBody);
|
|
269
|
+
|
|
270
|
+
// Parse strings section
|
|
271
|
+
const strings = parseStrings(ruleBody);
|
|
272
|
+
|
|
273
|
+
// Parse condition section
|
|
274
|
+
const condition = parseCondition(ruleBody);
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
name: ruleName,
|
|
278
|
+
tags,
|
|
279
|
+
metadata,
|
|
280
|
+
strings,
|
|
281
|
+
condition,
|
|
282
|
+
private: isPrivate,
|
|
283
|
+
global: isGlobal
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Parse the metadata section of a YARA rule
|
|
289
|
+
* @param {string} ruleBody - The rule body text
|
|
290
|
+
* @returns {Object} Metadata key-value pairs
|
|
291
|
+
*/
|
|
292
|
+
function parseMetadata(ruleBody) {
|
|
293
|
+
const metaMatch = ruleBody.match(/meta:\s*([\s\S]*?)(?=strings:|condition:|$)/);
|
|
294
|
+
if (!metaMatch) return {};
|
|
295
|
+
|
|
296
|
+
const metaText = metaMatch[1];
|
|
297
|
+
const metadata = {};
|
|
298
|
+
|
|
299
|
+
// Match key = value pairs (handles strings and numbers)
|
|
300
|
+
const metaLines = metaText.match(/(\w+)\s*=\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|(-)?\d+(\.\d+)?|true|false)/g);
|
|
301
|
+
|
|
302
|
+
if (metaLines) {
|
|
303
|
+
for (const line of metaLines) {
|
|
304
|
+
const [, key, value] = line.match(/(\w+)\s*=\s*(.+)/);
|
|
305
|
+
// Remove quotes from string values
|
|
306
|
+
if (value.startsWith('"') || value.startsWith("'")) {
|
|
307
|
+
metadata[key] = value.slice(1, -1);
|
|
308
|
+
} else if (value === 'true' || value === 'false') {
|
|
309
|
+
metadata[key] = value === 'true';
|
|
310
|
+
} else if (!isNaN(value)) {
|
|
311
|
+
metadata[key] = Number(value);
|
|
312
|
+
} else {
|
|
313
|
+
metadata[key] = value;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return metadata;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Parse the strings section of a YARA rule
|
|
323
|
+
* Supports both named strings ($var = "pattern") and anonymous strings ($ = "pattern")
|
|
324
|
+
* Anonymous strings are automatically assigned internal names like .anon_0, .anon_1, etc.
|
|
325
|
+
* @param {string} ruleBody - The rule body text
|
|
326
|
+
* @returns {Object} String definitions with their compiled matchers
|
|
327
|
+
*/
|
|
328
|
+
function parseStrings(ruleBody) {
|
|
329
|
+
const stringsMatch = ruleBody.match(/strings:\s*([\s\S]*?)(?=condition:|$)/);
|
|
330
|
+
if (!stringsMatch) return {};
|
|
331
|
+
|
|
332
|
+
const stringsText = stringsMatch[1];
|
|
333
|
+
const strings = {};
|
|
334
|
+
|
|
335
|
+
// Match string definitions: $var = "text" modifiers or $ = "text" or $var = /regex/ or $var = { hex }
|
|
336
|
+
const stringLines = stringsText.match(/\$\w*\s*=\s*(.*)?/g);
|
|
337
|
+
|
|
338
|
+
if (stringLines) {
|
|
339
|
+
let anonCounter = 0; // Counter for anonymous strings
|
|
340
|
+
|
|
341
|
+
for (const line of stringLines) {
|
|
342
|
+
const match = line.match(/\$(\w*)\s*=\s*(.+)/);
|
|
343
|
+
if (!match) continue;
|
|
344
|
+
|
|
345
|
+
let [, varName, definition] = match;
|
|
346
|
+
|
|
347
|
+
// Handle anonymous strings ($ = "pattern")
|
|
348
|
+
if (!varName || varName === '') {
|
|
349
|
+
varName = `.anon_${anonCounter++}`;
|
|
350
|
+
} else {
|
|
351
|
+
// Add proper $ prefix to string identifier if not present -- NEED TO VERIFY THIS LOGIC
|
|
352
|
+
if (!varName.startsWith('$')) {
|
|
353
|
+
varName = '$' + varName;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
// Compile the string definition into a matcher function
|
|
359
|
+
const compiledRule = compileYaraLike(definition.trim());
|
|
360
|
+
|
|
361
|
+
strings[varName] = {
|
|
362
|
+
definition: definition.trim(),
|
|
363
|
+
...compiledRule
|
|
364
|
+
};
|
|
365
|
+
} catch (error) {
|
|
366
|
+
console.warn(`Warning: Failed to compile string $${varName}: ${error.message}`);
|
|
367
|
+
console.warn(`Definition: ${definition.trim()}`);
|
|
368
|
+
console.warn(line);
|
|
369
|
+
console.warn(stringsMatch);
|
|
370
|
+
console.warn(ruleBody);
|
|
371
|
+
strings[varName] = {
|
|
372
|
+
definition: definition.trim(),
|
|
373
|
+
type: null,
|
|
374
|
+
matcher: null,
|
|
375
|
+
error: error.message,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return strings;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Parse the condition section of a YARA rule
|
|
386
|
+
* @param {string} ruleBody - The rule body text
|
|
387
|
+
* @returns {string} The condition expression as a string (to be evaluated later)
|
|
388
|
+
*/
|
|
389
|
+
function parseCondition(ruleBody) {
|
|
390
|
+
const conditionMatch = ruleBody.match(/condition:\s*([\s\S]*?)$/);
|
|
391
|
+
if (!conditionMatch) {
|
|
392
|
+
throw new Error('Invalid YARA rule: missing condition');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Return the condition as a trimmed string, removing any trailing braces
|
|
396
|
+
return conditionMatch[1].trim().replace(/\}\s*$/, '').trim();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Compile a complete YARA rule into an executable form
|
|
401
|
+
* @param {string} ruleText - The complete YARA rule text
|
|
402
|
+
* @returns {Object} Compiled rule object ready for evaluation
|
|
403
|
+
*/
|
|
404
|
+
export function compileYaraRule(ruleText) {
|
|
405
|
+
const parsed = parseYaraRule(ruleText);
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
name: parsed.name,
|
|
409
|
+
tags: parsed.tags,
|
|
410
|
+
metadata: parsed.metadata,
|
|
411
|
+
strings: parsed.strings,
|
|
412
|
+
condition: parsed.condition,
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Match this rule against binary data
|
|
416
|
+
* @param {Uint8Array} data - The data to scan
|
|
417
|
+
* @returns {Object} Match results with string matches and overall result
|
|
418
|
+
*/
|
|
419
|
+
match(data) {
|
|
420
|
+
// Execute all string matchers
|
|
421
|
+
const stringMatches = {};
|
|
422
|
+
|
|
423
|
+
for (const [varName, stringDef] of Object.entries(parsed.strings)) {
|
|
424
|
+
if (stringDef.matcher) {
|
|
425
|
+
try {
|
|
426
|
+
const matches = stringDef.matcher(data);
|
|
427
|
+
stringMatches[varName] = matches;
|
|
428
|
+
} catch (error) {
|
|
429
|
+
console.warn(`Warning: Error matching $${varName}: ${error.message}`);
|
|
430
|
+
stringMatches[varName] = [];
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
stringMatches[varName] = [];
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// TODO: Evaluate the condition expression against stringMatches
|
|
438
|
+
// For now, return the matches without evaluating the condition
|
|
439
|
+
return {
|
|
440
|
+
ruleName: parsed.name,
|
|
441
|
+
matched: null, // Will be true/false once condition evaluation is implemented
|
|
442
|
+
stringMatches,
|
|
443
|
+
condition: parsed.condition,
|
|
444
|
+
};
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|