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,1083 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* YARA Condition Parser
|
|
19
|
+
*
|
|
20
|
+
* Parses YARA condition strings into AST format for evaluation.
|
|
21
|
+
* This is a simplified parser that handles common YARA condition patterns.
|
|
22
|
+
*
|
|
23
|
+
* Supported patterns:
|
|
24
|
+
* - String identifiers: $a, $b, $c
|
|
25
|
+
* - Rule identifiers: RuleA, RuleB (dependent rules)
|
|
26
|
+
* - Quantifiers: any of them, all of them, N of them, X% of them
|
|
27
|
+
* - For loops: for all/any of them : (condition), for i in (range) : (condition)
|
|
28
|
+
* - Logical operators: and, or, not
|
|
29
|
+
* - Comparison: ==, !=, <, >, <=, >=
|
|
30
|
+
* - Arithmetic operators: +, -, *, /, %
|
|
31
|
+
* - Bitwise operators: &, |, ^, ~, <<, >>
|
|
32
|
+
* - Proximity operators: $a at offset, $a in (range), $a within N of $b
|
|
33
|
+
* - Identifiers: filesize, entrypoint
|
|
34
|
+
* - Boolean literals: true, false
|
|
35
|
+
* - Numbers and basic arithmetic
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
// Constants
|
|
39
|
+
const PLACEHOLDER_PREFIX = '__LITERAL_';
|
|
40
|
+
const PLACEHOLDER_SUFFIX = '__';
|
|
41
|
+
const STRING_WILDCARD_VAR = '$';
|
|
42
|
+
|
|
43
|
+
const KEYWORD_LITERALS = {
|
|
44
|
+
TRUE: 'true',
|
|
45
|
+
FALSE: 'false',
|
|
46
|
+
FILESIZE: 'filesize',
|
|
47
|
+
ENTRYPOINT: 'entrypoint'
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const QUANTIFIER_KEYWORDS = {
|
|
51
|
+
ALL: 'all',
|
|
52
|
+
ANY: 'any',
|
|
53
|
+
NONE: 'none',
|
|
54
|
+
THEM: 'them'
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Data access type definitions
|
|
58
|
+
const DATA_ACCESS_TYPES = [
|
|
59
|
+
{ name: 'uint8', bits: 8, signed: false, endian: 'little' },
|
|
60
|
+
{ name: 'uint16', bits: 16, signed: false, endian: 'little' },
|
|
61
|
+
{ name: 'uint32', bits: 32, signed: false, endian: 'little' },
|
|
62
|
+
{ name: 'int8', bits: 8, signed: true, endian: 'little' },
|
|
63
|
+
{ name: 'int16', bits: 16, signed: true, endian: 'little' },
|
|
64
|
+
{ name: 'int32', bits: 32, signed: true, endian: 'little' },
|
|
65
|
+
{ name: 'uint16be', bits: 16, signed: false, endian: 'big' },
|
|
66
|
+
{ name: 'uint32be', bits: 32, signed: false, endian: 'big' },
|
|
67
|
+
{ name: 'int16be', bits: 16, signed: true, endian: 'big' },
|
|
68
|
+
{ name: 'int32be', bits: 32, signed: true, endian: 'big' }
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// Operator registry
|
|
72
|
+
const OPERATOR_REGISTRY = {
|
|
73
|
+
string: [
|
|
74
|
+
{ pattern: 'icontains', type: 'icontains' },
|
|
75
|
+
{ pattern: 'contains', type: 'contains' },
|
|
76
|
+
{ pattern: 'istartswith', type: 'istartswith' },
|
|
77
|
+
{ pattern: 'startswith', type: 'startswith' },
|
|
78
|
+
{ pattern: 'iendswith', type: 'iendswith' },
|
|
79
|
+
{ pattern: 'endswith', type: 'endswith' }
|
|
80
|
+
],
|
|
81
|
+
comparison: [
|
|
82
|
+
{ pattern: '==', type: 'equal' },
|
|
83
|
+
{ pattern: '!=', type: 'notEqual' },
|
|
84
|
+
{ pattern: '<=', type: 'lessThanOrEqual' },
|
|
85
|
+
{ pattern: '>=', type: 'greaterThanOrEqual' },
|
|
86
|
+
{ pattern: '<', type: 'lessThan' },
|
|
87
|
+
{ pattern: '>', type: 'greaterThan' }
|
|
88
|
+
],
|
|
89
|
+
arithmetic: [
|
|
90
|
+
{ pattern: '+', type: 'add' },
|
|
91
|
+
{ pattern: '-', type: 'subtract' },
|
|
92
|
+
{ pattern: '*', type: 'multiply' },
|
|
93
|
+
{ pattern: '\\', type: 'divide' },
|
|
94
|
+
{ pattern: '%', type: 'modulo' }
|
|
95
|
+
]
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Utility: Skip whitespace in a string starting from a given index
|
|
100
|
+
* @param {string} str - String to process
|
|
101
|
+
* @param {number} startIndex - Starting position
|
|
102
|
+
* @returns {number} Index after whitespace
|
|
103
|
+
*/
|
|
104
|
+
function skipWhitespace(str, startIndex) {
|
|
105
|
+
let index = startIndex;
|
|
106
|
+
while (index < str.length && /\s/.test(str[index])) index++;
|
|
107
|
+
return index;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Utility: Find matching closing parenthesis
|
|
112
|
+
* @param {string} str - String to search
|
|
113
|
+
* @param {number} startIndex - Index of opening paren
|
|
114
|
+
* @returns {number} Index of closing paren, or -1 if not found
|
|
115
|
+
*/
|
|
116
|
+
function findMatchingParen(str, startIndex) {
|
|
117
|
+
let depth = 1;
|
|
118
|
+
let index = startIndex + 1;
|
|
119
|
+
while (index < str.length && depth > 0) {
|
|
120
|
+
if (str[index] === '(') depth++;
|
|
121
|
+
if (str[index] === ')') depth--;
|
|
122
|
+
index++;
|
|
123
|
+
}
|
|
124
|
+
return depth === 0 ? index : -1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Utility: Check if parentheses wrap the entire expression
|
|
129
|
+
* @param {string} expr - Expression to check
|
|
130
|
+
* @returns {boolean} True if outer parens wrap everything
|
|
131
|
+
*/
|
|
132
|
+
function wrapsEntireExpression(expr) {
|
|
133
|
+
if (!expr.startsWith('(') || !expr.endsWith(')')) return false;
|
|
134
|
+
let depth = 0;
|
|
135
|
+
for (let i = 0; i < expr.length; i++) {
|
|
136
|
+
if (expr[i] === '(') depth++;
|
|
137
|
+
if (expr[i] === ')') depth--;
|
|
138
|
+
if (depth === 0 && i < expr.length - 1) return false;
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if a string has unescaped quotes in the middle
|
|
145
|
+
* @param {string} str - String to check (including outer quotes)
|
|
146
|
+
* @param {string} quoteChar - Quote character to look for (" or ')
|
|
147
|
+
* @returns {boolean} True if there are unescaped quotes in the middle
|
|
148
|
+
*/
|
|
149
|
+
function hasUnescapedQuotesInMiddle(str, quoteChar) {
|
|
150
|
+
let escaped = false;
|
|
151
|
+
|
|
152
|
+
// Check from position 1 to length-1 (excluding outer quotes)
|
|
153
|
+
for (let i = 1; i < str.length - 1; i++) {
|
|
154
|
+
if (escaped) {
|
|
155
|
+
escaped = false;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (str[i] === '\\') {
|
|
159
|
+
escaped = true;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (str[i] === quoteChar) {
|
|
163
|
+
return true; // Found unescaped quote in middle
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Validate if a string is a properly quoted string literal
|
|
172
|
+
* @param {string} str - String to validate (including outer quotes)
|
|
173
|
+
* @param {string} quoteChar - Quote character used (" or ')
|
|
174
|
+
* @returns {boolean} True if valid string literal
|
|
175
|
+
*/
|
|
176
|
+
function isValidStringLiteral(str, quoteChar) {
|
|
177
|
+
// Empty string is valid
|
|
178
|
+
if (str.length === 2) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check for unescaped quotes in the middle
|
|
183
|
+
return !hasUnescapedQuotesInMiddle(str, quoteChar);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Extract string literals from condition and replace with placeholders
|
|
188
|
+
* Phase 1 of parsing: removes string literals to avoid confusion with operators
|
|
189
|
+
*
|
|
190
|
+
* @param {string} condition - Raw condition string
|
|
191
|
+
* @returns {Object} { processed: string with placeholders, literals: array of literal info }
|
|
192
|
+
*/
|
|
193
|
+
function extractStringLiterals(condition) {
|
|
194
|
+
const literals = [];
|
|
195
|
+
let processed = condition;
|
|
196
|
+
let counter = 0;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Helper to extract strings with a specific quote character
|
|
200
|
+
* @param {string} text - Text to process
|
|
201
|
+
* @param {string} quoteChar - Quote character (" or ')
|
|
202
|
+
* @returns {string} Processed text with placeholders
|
|
203
|
+
*/
|
|
204
|
+
const extractQuoted = (text, quoteChar) => {
|
|
205
|
+
const regex = new RegExp(`${quoteChar}(?:[^${quoteChar}\\\\]|\\\\.)*${quoteChar}`, 'g');
|
|
206
|
+
return text.replace(regex, (match) => {
|
|
207
|
+
if (isValidStringLiteral(match, quoteChar)) {
|
|
208
|
+
const placeholder = `${PLACEHOLDER_PREFIX}${counter}${PLACEHOLDER_SUFFIX}`;
|
|
209
|
+
literals.push({
|
|
210
|
+
id: placeholder,
|
|
211
|
+
type: 'string',
|
|
212
|
+
value: match.slice(1, -1) // Remove quotes
|
|
213
|
+
});
|
|
214
|
+
counter++;
|
|
215
|
+
return placeholder;
|
|
216
|
+
}
|
|
217
|
+
return match; // Keep as-is if invalid
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Extract double-quoted strings, then single-quoted strings
|
|
222
|
+
processed = extractQuoted(processed, '"');
|
|
223
|
+
processed = extractQuoted(processed, "'");
|
|
224
|
+
|
|
225
|
+
return { processed, literals };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Restore string literals in AST by replacing placeholder identifiers
|
|
230
|
+
* Phase 3 of parsing: replaces placeholders with actual string literal nodes
|
|
231
|
+
*
|
|
232
|
+
* @param {Object} ast - AST node to process
|
|
233
|
+
* @param {Array} literals - Array of literal info from extraction phase
|
|
234
|
+
* @returns {Object} AST with literals restored
|
|
235
|
+
*/
|
|
236
|
+
function restoreStringLiterals(ast, literals) {
|
|
237
|
+
if (!ast || typeof ast !== 'object') {
|
|
238
|
+
return ast;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check if this is a placeholder identifier
|
|
242
|
+
if (ast.type === 'identifier' && ast.name && ast.name.startsWith(PLACEHOLDER_PREFIX)) {
|
|
243
|
+
const literal = literals.find(lit => lit.id === ast.name);
|
|
244
|
+
if (literal) {
|
|
245
|
+
return { type: 'string', value: literal.value };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Recursively process all properties
|
|
250
|
+
const result = { ...ast };
|
|
251
|
+
for (const key in result) {
|
|
252
|
+
if (Object.prototype.hasOwnProperty.call(result, key)) {
|
|
253
|
+
if (Array.isArray(result[key])) {
|
|
254
|
+
result[key] = result[key].map(item => {
|
|
255
|
+
// Handle plain string items (like in 'items' arrays)
|
|
256
|
+
if (typeof item === 'string' && item.startsWith(PLACEHOLDER_PREFIX)) {
|
|
257
|
+
const literal = literals.find(lit => lit.id === item);
|
|
258
|
+
return literal ? literal.value : item;
|
|
259
|
+
}
|
|
260
|
+
// Handle object items
|
|
261
|
+
return typeof item === 'object' ? restoreStringLiterals(item, literals) : item;
|
|
262
|
+
});
|
|
263
|
+
} else if (typeof result[key] === 'object') {
|
|
264
|
+
result[key] = restoreStringLiterals(result[key], literals);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Parse YARA condition to AST
|
|
274
|
+
* @param {string} condition - Condition string
|
|
275
|
+
* @param {Object} strings - Available string identifiers
|
|
276
|
+
* @returns {Object} AST node
|
|
277
|
+
*/
|
|
278
|
+
export function parseConditionToAST(condition, strings = {}) {
|
|
279
|
+
condition = condition.trim();
|
|
280
|
+
|
|
281
|
+
// Phase 1: Extract string literals to avoid parsing confusion
|
|
282
|
+
const { processed, literals } = extractStringLiterals(condition);
|
|
283
|
+
|
|
284
|
+
// Phase 2: Parse structure with placeholders
|
|
285
|
+
const ast = parseStructure(processed, strings);
|
|
286
|
+
|
|
287
|
+
// Phase 3: Restore string literals in AST
|
|
288
|
+
return restoreStringLiterals(ast, literals);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Parse quantifier expressions (any/all/none of them, N of them, etc.)
|
|
293
|
+
* Consolidates all quantifier parsing patterns into one function
|
|
294
|
+
*
|
|
295
|
+
* @param {string} condition - Condition string
|
|
296
|
+
* @returns {Object|null} AST node for quantifier, or null if not a quantifier
|
|
297
|
+
*/
|
|
298
|
+
function parseQuantifier(condition) {
|
|
299
|
+
// Pattern: <quantifier> of <items>
|
|
300
|
+
// quantifier can be: any, all, none, N (number), X% (percentage), N..M (range)
|
|
301
|
+
// items can be: them, or ($a, $b, $c)
|
|
302
|
+
|
|
303
|
+
const quantifierPattern = /^(any|all|none|\d+|\d+%|\d+\.\.\d+)\s+of\s+(them|\([^)]+\))$/;
|
|
304
|
+
const match = condition.match(quantifierPattern);
|
|
305
|
+
|
|
306
|
+
if (!match) return null;
|
|
307
|
+
|
|
308
|
+
const [, quantPart, itemsPart] = match;
|
|
309
|
+
|
|
310
|
+
// Parse quantifier part
|
|
311
|
+
let quantifier;
|
|
312
|
+
let type;
|
|
313
|
+
|
|
314
|
+
if (quantPart === 'any') {
|
|
315
|
+
type = 'any';
|
|
316
|
+
} else if (quantPart === 'all') {
|
|
317
|
+
type = 'all';
|
|
318
|
+
} else if (quantPart === 'none') {
|
|
319
|
+
type = 'none';
|
|
320
|
+
} else if (quantPart.includes('..')) {
|
|
321
|
+
// Range: N..M
|
|
322
|
+
const [min, max] = quantPart.split('..').map(n => parseInt(n));
|
|
323
|
+
type = 'quantified';
|
|
324
|
+
quantifier = { type: 'range', min, max };
|
|
325
|
+
} else if (quantPart.endsWith('%')) {
|
|
326
|
+
// Percentage: X%
|
|
327
|
+
type = 'quantified';
|
|
328
|
+
quantifier = { type: 'percentage', value: parseInt(quantPart) };
|
|
329
|
+
} else {
|
|
330
|
+
// Number: N
|
|
331
|
+
type = 'quantified';
|
|
332
|
+
quantifier = { type: 'number', value: parseInt(quantPart) };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Parse items part
|
|
336
|
+
let items;
|
|
337
|
+
if (itemsPart === 'them') {
|
|
338
|
+
items = 'them';
|
|
339
|
+
} else {
|
|
340
|
+
// Extract items from parentheses: ($a, $b, $c)
|
|
341
|
+
const itemsStr = itemsPart.slice(1, -1); // Remove outer parens
|
|
342
|
+
items = itemsStr.split(',').map(s => s.trim());
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return { type, ...(quantifier && { quantifier }), items };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Parse condition structure (Phase 2)
|
|
350
|
+
* This is the main parsing logic, now working with string literals replaced by placeholders
|
|
351
|
+
* @param {string} condition - Condition string with placeholders
|
|
352
|
+
* @param {Object} strings - Available string identifiers
|
|
353
|
+
* @returns {Object} AST node
|
|
354
|
+
*/
|
|
355
|
+
function parseStructure(condition, strings = {}) {
|
|
356
|
+
condition = condition.trim();
|
|
357
|
+
|
|
358
|
+
// Remove outer parentheses if they wrap the entire expression
|
|
359
|
+
while (wrapsEntireExpression(condition)) {
|
|
360
|
+
condition = condition.slice(1, -1).trim();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Try parsing as quantifier first
|
|
364
|
+
const quantifierNode = parseQuantifier(condition);
|
|
365
|
+
if (quantifierNode) {
|
|
366
|
+
return quantifierNode;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Helper: Parse binary operator (or, and)
|
|
371
|
+
* @param {string} operator - Operator name ('or', 'and')
|
|
372
|
+
* @param {number} operatorLength - Length of operator string
|
|
373
|
+
* @returns {Object|null} AST node or null
|
|
374
|
+
*/
|
|
375
|
+
const parseBinaryOperator = (operator, operatorLength) => {
|
|
376
|
+
const opIndex = findOperatorOutsideParens(condition, ` ${operator} `);
|
|
377
|
+
if (opIndex === -1) return null;
|
|
378
|
+
|
|
379
|
+
// Find where operator ends (skip whitespace before, operator, and whitespace after)
|
|
380
|
+
let endIndex = skipWhitespace(condition, opIndex);
|
|
381
|
+
endIndex += operatorLength; // skip operator
|
|
382
|
+
endIndex = skipWhitespace(condition, endIndex);
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
type: operator,
|
|
386
|
+
left: parseStructure(condition.substring(0, opIndex), strings),
|
|
387
|
+
right: parseStructure(condition.substring(endIndex), strings)
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// Handle "or" operator (lowest precedence - check first)
|
|
392
|
+
const orNode = parseBinaryOperator('or', 2);
|
|
393
|
+
if (orNode) return orNode;
|
|
394
|
+
|
|
395
|
+
// Handle "and" operator (higher precedence than OR - check after OR)
|
|
396
|
+
const andNode = parseBinaryOperator('and', 3);
|
|
397
|
+
if (andNode) return andNode;
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
// Handle "for" loops (higher precedence than AND/OR - check after them)
|
|
401
|
+
// Patterns:
|
|
402
|
+
// - for all of them : ( condition )
|
|
403
|
+
// - for any of them : ( condition )
|
|
404
|
+
// - for none of them : ( condition )
|
|
405
|
+
// - for 2 of them : ( condition )
|
|
406
|
+
// - for 50% of them : ( condition )
|
|
407
|
+
// - for all of ($a*, $b*) : ( condition )
|
|
408
|
+
// - for all i in (1..10) : ( condition )
|
|
409
|
+
// - for all i in (0..#a) : ( condition )
|
|
410
|
+
|
|
411
|
+
// Use a smarter approach: find "for", then find the matching parentheses for the body
|
|
412
|
+
if (condition.trim().startsWith('for ')) {
|
|
413
|
+
// Find the colon that separates iterator from body
|
|
414
|
+
const colonIndex = condition.indexOf(':');
|
|
415
|
+
if (colonIndex !== -1) {
|
|
416
|
+
// Find the opening paren after the colon
|
|
417
|
+
let openParenIndex = colonIndex + 1;
|
|
418
|
+
while (openParenIndex < condition.length && condition[openParenIndex] !== '(') {
|
|
419
|
+
openParenIndex++;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (openParenIndex < condition.length) {
|
|
423
|
+
// Find the matching closing paren
|
|
424
|
+
let depth = 1;
|
|
425
|
+
let closeParenIndex = openParenIndex + 1;
|
|
426
|
+
while (closeParenIndex < condition.length && depth > 0) {
|
|
427
|
+
if (condition[closeParenIndex] === '(') depth++;
|
|
428
|
+
if (condition[closeParenIndex] === ')') depth--;
|
|
429
|
+
closeParenIndex++;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (depth === 0) {
|
|
433
|
+
// Successfully found matching parens
|
|
434
|
+
const forHeader = condition.substring(0, colonIndex).trim();
|
|
435
|
+
const bodyCondition = condition.substring(openParenIndex + 1, closeParenIndex - 1).trim();
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Helper: Parse for-loop quantifier
|
|
439
|
+
* @param {string} quantifierStr - Quantifier string (all, any, none, N, N%)
|
|
440
|
+
* @returns {string|Object} Parsed quantifier
|
|
441
|
+
*/
|
|
442
|
+
const parseForQuantifier = (quantifierStr) => {
|
|
443
|
+
if (['all', 'any', 'none'].includes(quantifierStr)) return quantifierStr;
|
|
444
|
+
if (quantifierStr.endsWith('%')) {
|
|
445
|
+
return { type: 'percentage', value: parseInt(quantifierStr) };
|
|
446
|
+
}
|
|
447
|
+
return { type: 'number', value: parseInt(quantifierStr) };
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Helper: Parse for-loop iterator
|
|
452
|
+
* @param {string} iteratorPart - Iterator part of for-loop
|
|
453
|
+
* @returns {Object} { variable, set }
|
|
454
|
+
*/
|
|
455
|
+
const parseForIterator = (iteratorPart) => {
|
|
456
|
+
// Check for "i in (range)" pattern
|
|
457
|
+
const iteratorInRangeMatch = iteratorPart.match(/^(\w+)\s+in\s+\((.+?)\.\.(.+?)\)$/);
|
|
458
|
+
if (iteratorInRangeMatch) {
|
|
459
|
+
return {
|
|
460
|
+
variable: iteratorInRangeMatch[1],
|
|
461
|
+
set: {
|
|
462
|
+
type: 'range',
|
|
463
|
+
start: parseExpression(iteratorInRangeMatch[2].trim()),
|
|
464
|
+
end: parseExpression(iteratorInRangeMatch[3].trim())
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Check for "of them" or "of (set)" pattern
|
|
470
|
+
const ofMatch = iteratorPart.match(/^of\s+(.+)$/);
|
|
471
|
+
if (ofMatch) {
|
|
472
|
+
const setStr = ofMatch[1].trim();
|
|
473
|
+
let items;
|
|
474
|
+
|
|
475
|
+
if (setStr === 'them') {
|
|
476
|
+
items = 'them';
|
|
477
|
+
} else if (setStr.startsWith('(') && setStr.endsWith(')')) {
|
|
478
|
+
items = setStr.slice(1, -1).split(',').map(s => s.trim());
|
|
479
|
+
} else {
|
|
480
|
+
items = setStr; // wildcard pattern like $api*
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
variable: '$', // implicit variable for strings
|
|
485
|
+
set: { type: 'stringSet', items }
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return { variable: null, set: null };
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// Parse the "for quantifier iterator" part
|
|
493
|
+
const forMatch = forHeader.match(/^for\s+(all|any|none|\d+|(\d+)%)\s+(.+)$/);
|
|
494
|
+
if (forMatch) {
|
|
495
|
+
const quantifier = parseForQuantifier(forMatch[1]);
|
|
496
|
+
const { variable, set } = parseForIterator(forMatch[3]);
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
type: 'for',
|
|
500
|
+
quantifier,
|
|
501
|
+
variable,
|
|
502
|
+
set,
|
|
503
|
+
condition: parseStructure(bodyCondition, strings)
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Handle "not" operator
|
|
512
|
+
if (condition.startsWith('not ')) {
|
|
513
|
+
return {
|
|
514
|
+
type: 'not',
|
|
515
|
+
operand: parseStructure(condition.substring(4), strings)
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Handle "defined" operator
|
|
520
|
+
if (condition.startsWith('defined ')) {
|
|
521
|
+
return {
|
|
522
|
+
type: 'defined',
|
|
523
|
+
operand: parseStructure(condition.substring(8), strings)
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Handle "$a at offset", "$ at offset" (for loops), or "string" at offset (string literals)
|
|
528
|
+
const atMatch = condition.match(/^(\$\w*|\w+)\s+at\s+(.+)$/);
|
|
529
|
+
if (atMatch) {
|
|
530
|
+
const firstPart = atMatch[1];
|
|
531
|
+
// Check if it's a string identifier ($a) or a placeholder (__LITERAL_N__) or other identifier
|
|
532
|
+
if (firstPart.startsWith('$')) {
|
|
533
|
+
return {
|
|
534
|
+
type: 'at',
|
|
535
|
+
identifier: firstPart,
|
|
536
|
+
offset: parseExpression(atMatch[2])
|
|
537
|
+
};
|
|
538
|
+
} else if (firstPart.startsWith(PLACEHOLDER_PREFIX)) {
|
|
539
|
+
// String literal placeholder - parse it as an expression to get the string node
|
|
540
|
+
return {
|
|
541
|
+
type: 'at',
|
|
542
|
+
identifier: parseExpression(firstPart),
|
|
543
|
+
offset: parseExpression(atMatch[2])
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Handle "$a in (start..end)" or "$ in (start..end)" (for loops)
|
|
549
|
+
const inRangeMatch = condition.match(/^(\$\w*)\s+in\s+\((.+)\.\.(.+)\)$/);
|
|
550
|
+
if (inRangeMatch) {
|
|
551
|
+
return {
|
|
552
|
+
type: 'inRange',
|
|
553
|
+
identifier: inRangeMatch[1],
|
|
554
|
+
range: {
|
|
555
|
+
type: 'range',
|
|
556
|
+
start: parseExpression(inRangeMatch[2]),
|
|
557
|
+
end: parseExpression(inRangeMatch[3])
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Handle "$a within N of $b" or "$a within N bytes of $b"
|
|
563
|
+
// Proximity operator: checks if any occurrence of $a is within N bytes of any occurrence of $b
|
|
564
|
+
const withinMatch = condition.match(/^(\$\w*)\s+within\s+(.+?)(?:\s+bytes)?\s+of\s+(\$\w*)$/);
|
|
565
|
+
if (withinMatch) {
|
|
566
|
+
return {
|
|
567
|
+
type: 'within',
|
|
568
|
+
identifier: withinMatch[1],
|
|
569
|
+
distance: parseExpression(withinMatch[2]),
|
|
570
|
+
reference: withinMatch[3]
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Handle string operators (must check before comparison operators)
|
|
575
|
+
// These are word-based operators that require flexible whitespace matching
|
|
576
|
+
const stringOps = [
|
|
577
|
+
{ pattern: 'icontains', type: 'icontains' },
|
|
578
|
+
{ pattern: 'contains', type: 'contains' },
|
|
579
|
+
{ pattern: 'istartswith', type: 'istartswith' },
|
|
580
|
+
{ pattern: 'startswith', type: 'startswith' },
|
|
581
|
+
{ pattern: 'iendswith', type: 'iendswith' },
|
|
582
|
+
{ pattern: 'endswith', type: 'endswith' }
|
|
583
|
+
];
|
|
584
|
+
|
|
585
|
+
for (const op of stringOps) {
|
|
586
|
+
const opIndex = findOperatorOutsideParens(condition, ` ${op.pattern} `);
|
|
587
|
+
if (opIndex !== -1) {
|
|
588
|
+
// Find where the operator ends (skip whitespace before, operator, and whitespace after)
|
|
589
|
+
let endIndex = opIndex;
|
|
590
|
+
while (endIndex < condition.length && /\s/.test(condition[endIndex])) endIndex++;
|
|
591
|
+
endIndex += op.pattern.length;
|
|
592
|
+
while (endIndex < condition.length && /\s/.test(condition[endIndex])) endIndex++;
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
type: op.type,
|
|
596
|
+
left: parseExpression(condition.substring(0, opIndex)),
|
|
597
|
+
right: parseExpression(condition.substring(endIndex))
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Handle comparison operators
|
|
603
|
+
const comparisonOps = [
|
|
604
|
+
{ pattern: '==', type: 'equal' },
|
|
605
|
+
{ pattern: '!=', type: 'notEqual' },
|
|
606
|
+
{ pattern: '<=', type: 'lessThanOrEqual' },
|
|
607
|
+
{ pattern: '>=', type: 'greaterThanOrEqual' },
|
|
608
|
+
{ pattern: '<', type: 'lessThan' },
|
|
609
|
+
{ pattern: '>', type: 'greaterThan' }
|
|
610
|
+
];
|
|
611
|
+
|
|
612
|
+
for (const op of comparisonOps) {
|
|
613
|
+
const opIndex = findOperatorOutsideParens(condition, op.pattern);
|
|
614
|
+
if (opIndex !== -1) {
|
|
615
|
+
return {
|
|
616
|
+
type: op.type,
|
|
617
|
+
left: parseExpression(condition.substring(0, opIndex)),
|
|
618
|
+
right: parseExpression(condition.substring(opIndex + op.pattern.length))
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Handle single expression
|
|
624
|
+
return parseExpression(condition);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Parse an expression (identifier, number, string identifier, etc.)
|
|
629
|
+
* @param {string} expr - Expression string
|
|
630
|
+
* @returns {Object} AST node
|
|
631
|
+
*/
|
|
632
|
+
function parseExpression(expr) {
|
|
633
|
+
expr = expr.trim();
|
|
634
|
+
|
|
635
|
+
// Boolean literals - must check before identifiers
|
|
636
|
+
if (expr === 'true') {
|
|
637
|
+
return { type: 'boolean', value: true };
|
|
638
|
+
}
|
|
639
|
+
if (expr === 'false') {
|
|
640
|
+
return { type: 'boolean', value: false };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Size units (KB, MB, GB) - must check before plain numbers
|
|
644
|
+
const sizeUnitMatch = expr.match(/^(\d+)(KB|MB|GB)$/i);
|
|
645
|
+
if (sizeUnitMatch) {
|
|
646
|
+
const value = parseInt(sizeUnitMatch[1]);
|
|
647
|
+
const unit = sizeUnitMatch[2].toUpperCase();
|
|
648
|
+
const multipliers = { KB: 1024, MB: 1024 * 1024, GB: 1024 * 1024 * 1024 };
|
|
649
|
+
return { type: 'number', value: value * multipliers[unit] };
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Note: String literals are handled in Phase 1 (extractStringLiterals)
|
|
653
|
+
// and restored in Phase 3 (restoreStringLiterals).
|
|
654
|
+
// By this point, they are placeholders like __LITERAL_0__
|
|
655
|
+
|
|
656
|
+
// Floating point number (must be checked before member access)
|
|
657
|
+
if (/^-?\d+\.\d+$/.test(expr)) {
|
|
658
|
+
return { type: 'number', value: parseFloat(expr) };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Number (integer)
|
|
662
|
+
if (/^-?\d+$/.test(expr)) {
|
|
663
|
+
return { type: 'number', value: parseInt(expr) };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Hex number
|
|
667
|
+
if (/^0x[0-9a-fA-F]+$/.test(expr)) {
|
|
668
|
+
return { type: 'number', value: parseInt(expr, 16) };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// String identifier $a or just $ (for loops)
|
|
672
|
+
if (/^\$\w*$/.test(expr)) {
|
|
673
|
+
return { type: 'stringIdentifier', identifier: expr };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// String count #a or just # (for loops)
|
|
677
|
+
if (/^#\w*$/.test(expr)) {
|
|
678
|
+
return { type: 'stringCount', identifier: expr.replace('#', '$') };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// String offset @a or just @ (for loops)
|
|
682
|
+
if (/^@\w*$/.test(expr)) {
|
|
683
|
+
return { type: 'stringOffset', identifier: expr.replace('@', '$'), index: 0 };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Helper: Parse indexed string operations (@a[n] or !a[n])
|
|
688
|
+
* @param {string} expr - Expression to parse
|
|
689
|
+
* @param {string} prefix - Prefix character (@ or !)
|
|
690
|
+
* @param {string} type - Type name ('stringOffset' or 'stringLength')
|
|
691
|
+
* @returns {Object|null} AST node or null
|
|
692
|
+
*/
|
|
693
|
+
const parseIndexedStringOp = (expr, prefix, type) => {
|
|
694
|
+
const pattern = new RegExp(`^\\${prefix}([\\w$]+)\\[([^\\]]+)\\]$`);
|
|
695
|
+
const match = expr.match(pattern);
|
|
696
|
+
if (!match) return null;
|
|
697
|
+
|
|
698
|
+
const identifierPart = match[1];
|
|
699
|
+
const indexExpr = match[2].trim();
|
|
700
|
+
const identifier = identifierPart.startsWith('$') ? identifierPart : `$${identifierPart}`;
|
|
701
|
+
|
|
702
|
+
// Parse index as number or expression
|
|
703
|
+
const index = /^\d+$/.test(indexExpr) ? parseInt(indexExpr) : parseExpression(indexExpr);
|
|
704
|
+
|
|
705
|
+
return { type, identifier, index };
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
// String offset @a[expr] - supports @a[n], @a[i], @a[i+1], etc.
|
|
709
|
+
const offsetIndexed = parseIndexedStringOp(expr, '@', 'stringOffset');
|
|
710
|
+
if (offsetIndexed) return offsetIndexed;
|
|
711
|
+
|
|
712
|
+
// String length !a or !$
|
|
713
|
+
if (/^![\w$]+$/.test(expr)) {
|
|
714
|
+
const id = expr.slice(1); // Remove '!'
|
|
715
|
+
return { type: 'stringLength', identifier: id.startsWith('$') ? id : `$${id}`, index: 0 };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// String length !a[expr] - supports !a[n], !a[i], !a[i+1], etc.
|
|
719
|
+
const lengthIndexed = parseIndexedStringOp(expr, '!', 'stringLength');
|
|
720
|
+
if (lengthIndexed) return lengthIndexed;
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
// filesize
|
|
724
|
+
if (expr === 'filesize') {
|
|
725
|
+
return { type: 'identifier', name: 'filesize' };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// entrypoint
|
|
729
|
+
if (expr === 'entrypoint') {
|
|
730
|
+
return { type: 'identifier', name: 'entrypoint' };
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Data access functions: uint8, uint16, uint32, int8, int16, int32, uint16be, uint32be, int16be, int32be
|
|
734
|
+
for (const dataAccessType of DATA_ACCESS_TYPES) {
|
|
735
|
+
const pattern = new RegExp(`^${dataAccessType.name}\\((.+)\\)$`);
|
|
736
|
+
const match = expr.match(pattern);
|
|
737
|
+
if (match) {
|
|
738
|
+
return {
|
|
739
|
+
type: 'dataAccess',
|
|
740
|
+
dataType: dataAccessType.name.replace('be', ''), // Remove 'be' suffix for storage
|
|
741
|
+
offset: parseExpression(match[1]),
|
|
742
|
+
endian: dataAccessType.endian
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
// Module function calls (e.g., math.entropy(...), string.to_int(...), time.now())
|
|
749
|
+
const moduleFuncMatch = expr.match(/^(\w+)\.(\w+)\((.*)\)$/);
|
|
750
|
+
if (moduleFuncMatch) {
|
|
751
|
+
const [, moduleName, functionName, argsStr] = moduleFuncMatch;
|
|
752
|
+
|
|
753
|
+
// Parse arguments (simple comma-separated for now)
|
|
754
|
+
let args = [];
|
|
755
|
+
if (argsStr.trim()) {
|
|
756
|
+
// Split by comma, but respect nested parentheses
|
|
757
|
+
let depth = 0;
|
|
758
|
+
let currentArg = '';
|
|
759
|
+
for (let i = 0; i < argsStr.length; i++) {
|
|
760
|
+
const ch = argsStr[i];
|
|
761
|
+
if (ch === '(') depth++;
|
|
762
|
+
if (ch === ')') depth--;
|
|
763
|
+
if (ch === ',' && depth === 0) {
|
|
764
|
+
args.push(parseExpression(currentArg.trim()));
|
|
765
|
+
currentArg = '';
|
|
766
|
+
} else {
|
|
767
|
+
currentArg += ch;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (currentArg.trim()) {
|
|
771
|
+
args.push(parseExpression(currentArg.trim()));
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return {
|
|
776
|
+
type: 'moduleFunction',
|
|
777
|
+
module: moduleName,
|
|
778
|
+
function: functionName,
|
|
779
|
+
args
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Module property with array indexing and optional chained access
|
|
784
|
+
// Examples: pe.sections[0], pe.sections[0].name, elf.sections[i].virtual_address
|
|
785
|
+
const moduleArrayChainMatch = expr.match(/^(\w+)\.(\w+)\[([^\]]+)\]\.?(\w*)$/);
|
|
786
|
+
if (moduleArrayChainMatch) {
|
|
787
|
+
const [, moduleName, propertyName, indexExpr, rest] = moduleArrayChainMatch;
|
|
788
|
+
|
|
789
|
+
// Base array access node
|
|
790
|
+
let node = {
|
|
791
|
+
type: 'arrayAccess',
|
|
792
|
+
object: {
|
|
793
|
+
type: 'memberAccess',
|
|
794
|
+
object: { type: 'identifier', name: moduleName },
|
|
795
|
+
property: propertyName
|
|
796
|
+
},
|
|
797
|
+
index: parseExpression(indexExpr.trim())
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
// If there's chained access (e.g., .name after [0]), parse it
|
|
801
|
+
if (rest) {
|
|
802
|
+
const chainedProperties = rest.split('.');
|
|
803
|
+
for (const prop of chainedProperties) {
|
|
804
|
+
if (prop) {
|
|
805
|
+
node = {
|
|
806
|
+
type: 'memberAccess',
|
|
807
|
+
object: node,
|
|
808
|
+
property: prop
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return node;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Module property access (e.g., pe.entry_point, elf.is_64bit)
|
|
818
|
+
const modulePropMatch = expr.match(/^(\w+)\.(\w+)$/);
|
|
819
|
+
if (modulePropMatch) {
|
|
820
|
+
const [, moduleName, propertyName] = modulePropMatch;
|
|
821
|
+
return {
|
|
822
|
+
type: 'memberAccess',
|
|
823
|
+
object: { type: 'identifier', name: moduleName },
|
|
824
|
+
property: propertyName
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Bitwise NOT operator (unary, highest precedence)
|
|
829
|
+
// Only consume a primary expression (number, identifier, or parenthesized)
|
|
830
|
+
if (expr.startsWith('~')) {
|
|
831
|
+
const rest = expr.substring(1).trim();
|
|
832
|
+
let operandEnd = 0;
|
|
833
|
+
|
|
834
|
+
// If starts with paren, find matching close paren
|
|
835
|
+
if (rest.startsWith('(')) {
|
|
836
|
+
let depth = 1;
|
|
837
|
+
operandEnd = 1;
|
|
838
|
+
while (operandEnd < rest.length && depth > 0) {
|
|
839
|
+
if (rest[operandEnd] === '(') depth++;
|
|
840
|
+
if (rest[operandEnd] === ')') depth--;
|
|
841
|
+
operandEnd++;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
// If starts with another ~, recursively count tildes and then parse primary
|
|
845
|
+
else if (rest.startsWith('~')) {
|
|
846
|
+
// Count consecutive tildes (for potential future multi-NOT support)
|
|
847
|
+
let idx = 0;
|
|
848
|
+
while (idx < rest.length && rest[idx] === '~') {
|
|
849
|
+
idx++;
|
|
850
|
+
}
|
|
851
|
+
const afterTildes = rest.substring(idx).trim();
|
|
852
|
+
|
|
853
|
+
// Now find the primary expression after all the tildes
|
|
854
|
+
let primaryEnd = 0;
|
|
855
|
+
if (afterTildes.startsWith('(')) {
|
|
856
|
+
let depth = 1;
|
|
857
|
+
primaryEnd = 1;
|
|
858
|
+
while (primaryEnd < afterTildes.length && depth > 0) {
|
|
859
|
+
if (afterTildes[primaryEnd] === '(') depth++;
|
|
860
|
+
if (afterTildes[primaryEnd] === ')') depth--;
|
|
861
|
+
primaryEnd++;
|
|
862
|
+
}
|
|
863
|
+
} else {
|
|
864
|
+
// Check if it's a function call like uint8(4), int16be(10), etc.
|
|
865
|
+
const funcMatch = afterTildes.match(/^([a-zA-Z_$]\w*)\s*\(/);
|
|
866
|
+
if (funcMatch) {
|
|
867
|
+
// Find the matching closing parenthesis for the function call
|
|
868
|
+
let depth = 1;
|
|
869
|
+
primaryEnd = funcMatch[0].length; // Start after the opening '('
|
|
870
|
+
while (primaryEnd < afterTildes.length && depth > 0) {
|
|
871
|
+
if (afterTildes[primaryEnd] === '(') depth++;
|
|
872
|
+
if (afterTildes[primaryEnd] === ')') depth--;
|
|
873
|
+
primaryEnd++;
|
|
874
|
+
}
|
|
875
|
+
} else {
|
|
876
|
+
// Simple identifier or number
|
|
877
|
+
const match = afterTildes.match(/^(0x[0-9a-fA-F]+|\d+|[a-zA-Z_$]\w*)/);
|
|
878
|
+
if (match) {
|
|
879
|
+
primaryEnd = match[0].length;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Total operand is all the tildes + the primary
|
|
885
|
+
operandEnd = idx + primaryEnd;
|
|
886
|
+
}
|
|
887
|
+
// Otherwise, consume hex number, decimal number, identifier, or function call
|
|
888
|
+
else {
|
|
889
|
+
// Check if it's a function call like uint8(4), int16be(10), etc.
|
|
890
|
+
const funcMatch = rest.match(/^([a-zA-Z_$]\w*)\s*\(/);
|
|
891
|
+
if (funcMatch) {
|
|
892
|
+
// Find the matching closing parenthesis for the function call
|
|
893
|
+
let depth = 1;
|
|
894
|
+
operandEnd = funcMatch[0].length; // Start after the opening '('
|
|
895
|
+
while (operandEnd < rest.length && depth > 0) {
|
|
896
|
+
if (rest[operandEnd] === '(') depth++;
|
|
897
|
+
if (rest[operandEnd] === ')') depth--;
|
|
898
|
+
operandEnd++;
|
|
899
|
+
}
|
|
900
|
+
} else {
|
|
901
|
+
// Match: hex (0x...), decimal number, or identifier
|
|
902
|
+
const match = rest.match(/^(0x[0-9a-fA-F]+|\d+|[a-zA-Z_$]\w*)/);
|
|
903
|
+
if (match) {
|
|
904
|
+
operandEnd = match[0].length;
|
|
905
|
+
} else {
|
|
906
|
+
// Fallback: parse entire rest as operand (shouldn't happen)
|
|
907
|
+
return {
|
|
908
|
+
type: 'bitwiseNot',
|
|
909
|
+
operand: parseExpression(rest)
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const operand = parseExpression(rest.substring(0, operandEnd));
|
|
916
|
+
const remaining = rest.substring(operandEnd).trim();
|
|
917
|
+
|
|
918
|
+
// If there's more after the operand, we need to continue parsing the full expression
|
|
919
|
+
if (remaining) {
|
|
920
|
+
// The ~ is part of a larger expression, wrap the NOT and remaining in parens to reparse
|
|
921
|
+
const fullExpr = '(' + expr.substring(0, operandEnd + 1) + ')' + remaining;
|
|
922
|
+
return parseExpression(fullExpr);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
return {
|
|
926
|
+
type: 'bitwiseNot',
|
|
927
|
+
operand
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Bitwise OR operator (lowest precedence among bitwise)
|
|
932
|
+
const bitwiseOrIndex = findOperatorOutsideParens(expr, '|');
|
|
933
|
+
if (bitwiseOrIndex !== -1) {
|
|
934
|
+
return {
|
|
935
|
+
type: 'bitwiseOr',
|
|
936
|
+
left: parseExpression(expr.substring(0, bitwiseOrIndex)),
|
|
937
|
+
right: parseExpression(expr.substring(bitwiseOrIndex + 1))
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Bitwise XOR operator
|
|
942
|
+
const bitwiseXorIndex = findOperatorOutsideParens(expr, '^');
|
|
943
|
+
if (bitwiseXorIndex !== -1) {
|
|
944
|
+
return {
|
|
945
|
+
type: 'bitwiseXor',
|
|
946
|
+
left: parseExpression(expr.substring(0, bitwiseXorIndex)),
|
|
947
|
+
right: parseExpression(expr.substring(bitwiseXorIndex + 1))
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Bitwise AND operator
|
|
952
|
+
const bitwiseAndIndex = findOperatorOutsideParens(expr, '&');
|
|
953
|
+
if (bitwiseAndIndex !== -1) {
|
|
954
|
+
return {
|
|
955
|
+
type: 'bitwiseAnd',
|
|
956
|
+
left: parseExpression(expr.substring(0, bitwiseAndIndex)),
|
|
957
|
+
right: parseExpression(expr.substring(bitwiseAndIndex + 1))
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Shift operators
|
|
962
|
+
const shiftLeftIndex = findOperatorOutsideParens(expr, '<<');
|
|
963
|
+
if (shiftLeftIndex !== -1) {
|
|
964
|
+
return {
|
|
965
|
+
type: 'shiftLeft',
|
|
966
|
+
left: parseExpression(expr.substring(0, shiftLeftIndex)),
|
|
967
|
+
right: parseExpression(expr.substring(shiftLeftIndex + 2))
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const shiftRightIndex = findOperatorOutsideParens(expr, '>>');
|
|
972
|
+
if (shiftRightIndex !== -1) {
|
|
973
|
+
return {
|
|
974
|
+
type: 'shiftRight',
|
|
975
|
+
left: parseExpression(expr.substring(0, shiftRightIndex)),
|
|
976
|
+
right: parseExpression(expr.substring(shiftRightIndex + 2))
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Arithmetic operators
|
|
981
|
+
const arithmeticOps = [
|
|
982
|
+
{ pattern: '+', type: 'add' },
|
|
983
|
+
{ pattern: '-', type: 'subtract' },
|
|
984
|
+
{ pattern: '*', type: 'multiply' },
|
|
985
|
+
{ pattern: '\\', type: 'divide' },
|
|
986
|
+
{ pattern: '%', type: 'modulo' }
|
|
987
|
+
];
|
|
988
|
+
|
|
989
|
+
for (const op of arithmeticOps) {
|
|
990
|
+
const opIndex = findOperatorOutsideParens(expr, op.pattern);
|
|
991
|
+
if (opIndex !== -1) {
|
|
992
|
+
return {
|
|
993
|
+
type: op.type,
|
|
994
|
+
left: parseExpression(expr.substring(0, opIndex)),
|
|
995
|
+
right: parseExpression(expr.substring(opIndex + op.pattern.length))
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Parenthesized expression - only remove if parentheses wrap the entire expression
|
|
1001
|
+
if (expr.startsWith('(') && expr.endsWith(')')) {
|
|
1002
|
+
let depth = 0;
|
|
1003
|
+
let wrapsEntireExpression = true;
|
|
1004
|
+
for (let i = 0; i < expr.length; i++) {
|
|
1005
|
+
if (expr[i] === '(') depth++;
|
|
1006
|
+
if (expr[i] === ')') depth--;
|
|
1007
|
+
// If depth hits 0 before the end, the outer parens don't wrap everything
|
|
1008
|
+
if (depth === 0 && i < expr.length - 1) {
|
|
1009
|
+
wrapsEntireExpression = false;
|
|
1010
|
+
break;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (wrapsEntireExpression) {
|
|
1014
|
+
return parseExpression(expr.slice(1, -1));
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Rule identifier (uppercase names like RuleA, MyRule, etc.)
|
|
1019
|
+
// YARA rule names must start with a letter and contain only alphanumeric characters and underscores
|
|
1020
|
+
if (/^[A-Z][a-zA-Z0-9_]*$/.test(expr)) {
|
|
1021
|
+
return { type: 'ruleIdentifier', name: expr };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Default: treat as unknown identifier
|
|
1025
|
+
return { type: 'identifier', name: expr };
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Find operator outside of parentheses and brackets
|
|
1030
|
+
* For word operators (and, or, not), matches with flexible whitespace
|
|
1031
|
+
* @param {string} str - String to search
|
|
1032
|
+
* @param {string} op - Operator to find (e.g., ' and ', ' or ', '>', '==')
|
|
1033
|
+
* @returns {number} Index of operator, or -1 if not found
|
|
1034
|
+
*/
|
|
1035
|
+
function findOperatorOutsideParens(str, op) {
|
|
1036
|
+
const opTrimmed = op.trim();
|
|
1037
|
+
const isWordOp = /^(and|or|not)$/.test(opTrimmed);
|
|
1038
|
+
|
|
1039
|
+
let parenDepth = 0;
|
|
1040
|
+
let bracketDepth = 0;
|
|
1041
|
+
|
|
1042
|
+
for (let i = 0; i < str.length; i++) {
|
|
1043
|
+
if (str[i] === '(') parenDepth++;
|
|
1044
|
+
if (str[i] === ')') parenDepth--;
|
|
1045
|
+
if (str[i] === '[') bracketDepth++;
|
|
1046
|
+
if (str[i] === ']') bracketDepth--;
|
|
1047
|
+
|
|
1048
|
+
// Only look for operators when outside all brackets and parens
|
|
1049
|
+
if (parenDepth === 0 && bracketDepth === 0) {
|
|
1050
|
+
if (isWordOp) {
|
|
1051
|
+
// For word operators, match the word with flexible surrounding whitespace
|
|
1052
|
+
// Must have whitespace before (or be at start)
|
|
1053
|
+
const hasWhitespaceBefore = i === 0 || /\s/.test(str[i - 1]);
|
|
1054
|
+
if (!hasWhitespaceBefore) continue;
|
|
1055
|
+
|
|
1056
|
+
// Skip leading whitespace
|
|
1057
|
+
let j = i;
|
|
1058
|
+
while (j < str.length && /\s/.test(str[j])) j++;
|
|
1059
|
+
|
|
1060
|
+
// Check if operator word matches
|
|
1061
|
+
if (str.substring(j, j + opTrimmed.length) === opTrimmed) {
|
|
1062
|
+
// Check what comes after the operator
|
|
1063
|
+
const afterOp = j + opTrimmed.length;
|
|
1064
|
+
const hasWhitespaceAfter = afterOp >= str.length || /\s/.test(str[afterOp]);
|
|
1065
|
+
|
|
1066
|
+
if (hasWhitespaceAfter) {
|
|
1067
|
+
return i; // Return the position where whitespace before operator starts
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
} else {
|
|
1071
|
+
// For symbol operators, exact match
|
|
1072
|
+
if (str.substring(i, i + op.length) === op) {
|
|
1073
|
+
return i;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return -1;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
export default {
|
|
1082
|
+
parseConditionToAST
|
|
1083
|
+
};
|