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
package/src/peModule.mjs
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
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
|
+
// Browser-compatible PE parser that mirrors YARA's PE module fields.
|
|
18
|
+
// Includes lightweight detection of digital signature (no PKIjs).
|
|
19
|
+
|
|
20
|
+
import * as PE from "pe-library";
|
|
21
|
+
import { computeEntropy, digestHex, calculateMD5, readUtf16leString, findPattern } from "./sharedUtils.mjs";
|
|
22
|
+
|
|
23
|
+
/* -------------------------
|
|
24
|
+
Section processing
|
|
25
|
+
------------------------- */
|
|
26
|
+
async function parsePESections(exe, inputBytes) {
|
|
27
|
+
const sections = [];
|
|
28
|
+
const exeSections = exe.getAllSections?.() || exe.sections || [];
|
|
29
|
+
for (const s of exeSections) {
|
|
30
|
+
// Handle different PE library structures
|
|
31
|
+
const sectionInfo = s.info || s;
|
|
32
|
+
const data = s.data ? new Uint8Array(s.data) : (s.data ?? new Uint8Array());
|
|
33
|
+
|
|
34
|
+
// Compute section hashes using shared utilities (browser-compatible)
|
|
35
|
+
const md5Hash = calculateMD5(data);
|
|
36
|
+
const sha1Hash = await digestHex("SHA-1", data);
|
|
37
|
+
const sha256Hash = await digestHex("SHA-256", data);
|
|
38
|
+
|
|
39
|
+
const sectionHashes = {
|
|
40
|
+
md5: md5Hash,
|
|
41
|
+
sha1: sha1Hash,
|
|
42
|
+
sha256: sha256Hash,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
sections.push({
|
|
46
|
+
name: (sectionInfo.name || s.name || "")?.replace(/\0.*$/, ""),
|
|
47
|
+
raw_data: data,
|
|
48
|
+
entropy: computeEntropy(data),
|
|
49
|
+
virtual_address: sectionInfo.virtualAddress ?? sectionInfo.VirtualAddress ?? s.VirtualAddress ?? 0,
|
|
50
|
+
virtual_size: sectionInfo.virtualSize ?? sectionInfo.VirtualSize ?? s.VirtualSize ?? 0,
|
|
51
|
+
raw_data_offset: sectionInfo.pointerToRawData ?? sectionInfo.PointerToRawData ?? s.PointerToRawData ?? 0,
|
|
52
|
+
raw_data_size: sectionInfo.sizeOfRawData ?? sectionInfo.SizeOfRawData ?? s.SizeOfRawData ?? 0,
|
|
53
|
+
characteristics: (sectionInfo.characteristics ?? sectionInfo.Characteristics ?? s.Characteristics ?? 0) >>> 0, // Convert to unsigned 32-bit
|
|
54
|
+
...sectionHashes,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return sections;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* -------------------------
|
|
61
|
+
Imports / Exports
|
|
62
|
+
------------------------- */
|
|
63
|
+
function parseImports(exe) {
|
|
64
|
+
return (exe.imports || []).map(imp => ({
|
|
65
|
+
dll: imp.name || "",
|
|
66
|
+
functions: (imp.functions || []).map(f => ({ name: f.name || null, ordinal: f.ordinal || null })),
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseExports(exe) {
|
|
71
|
+
return (exe.exports || []).map(e => e.name || e).filter(Boolean);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* -------------------------
|
|
75
|
+
ImpHash
|
|
76
|
+
------------------------- */
|
|
77
|
+
function computeImpHash(importsArray) {
|
|
78
|
+
const parts = [];
|
|
79
|
+
for (const imp of importsArray) {
|
|
80
|
+
const dll = (imp.dll || "").toLowerCase();
|
|
81
|
+
if (Array.isArray(imp.functions)) {
|
|
82
|
+
for (const fn of imp.functions) {
|
|
83
|
+
const name = (fn.name || `ordinal${fn.ordinal || ""}`).toLowerCase();
|
|
84
|
+
parts.push(`${dll}.${name}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Use calculateMD5 with TextEncoder for string hashing
|
|
89
|
+
const encoder = new TextEncoder();
|
|
90
|
+
return calculateMD5(encoder.encode(parts.join(",")));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* -------------------------
|
|
94
|
+
Digital Signature
|
|
95
|
+
------------------------- */
|
|
96
|
+
function detectAuthenticode(exe) {
|
|
97
|
+
const dirEntry = exe.optionalHeader?.dataDirectory?.[PE.Format.ImageDirectoryEntry.Security];
|
|
98
|
+
if (!dirEntry || !dirEntry.virtualAddress || !dirEntry.size) return { has_signature: false };
|
|
99
|
+
return { has_signature: true, certificate_table: { offset: dirEntry.virtualAddress, size: dirEntry.size } };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* -------------------------
|
|
103
|
+
Rich Header detection
|
|
104
|
+
------------------------- */
|
|
105
|
+
function detectRichHeader(buf) {
|
|
106
|
+
const pattern = new Uint8Array([0x52, 0x69, 0x63, 0x68]); // "Rich"
|
|
107
|
+
const offset = findPattern(buf, pattern);
|
|
108
|
+
return offset !== -1 ? { offset } : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* -------------------------
|
|
112
|
+
Optional Header Fields (flattened for YARA)
|
|
113
|
+
------------------------- */
|
|
114
|
+
function parseOptionalHeaderFlat(exe) {
|
|
115
|
+
const opt = exe.newHeader?.optionalHeader || exe.optionalHeader;
|
|
116
|
+
if (!opt) return {};
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
size_of_code: opt.sizeOfCode ?? 0,
|
|
120
|
+
size_of_initialized_data: opt.sizeOfInitializedData ?? 0,
|
|
121
|
+
size_of_uninitialized_data: opt.sizeOfUninitializedData ?? 0,
|
|
122
|
+
address_of_entry_point: opt.addressOfEntryPoint ?? 0,
|
|
123
|
+
base_of_code: opt.baseOfCode ?? 0,
|
|
124
|
+
base_of_data: opt.baseOfData ?? 0,
|
|
125
|
+
image_base: opt.imageBase ?? 0,
|
|
126
|
+
section_alignment: opt.sectionAlignment ?? 0,
|
|
127
|
+
file_alignment: opt.fileAlignment ?? 0,
|
|
128
|
+
subsystem: opt.subsystem ?? 0,
|
|
129
|
+
dll_characteristics: opt.dllCharacteristics ?? 0,
|
|
130
|
+
size_of_stack_reserve: opt.sizeOfStackReserve ?? 0,
|
|
131
|
+
size_of_stack_commit: opt.sizeOfStackCommit ?? 0,
|
|
132
|
+
size_of_heap_reserve: opt.sizeOfHeapReserve ?? 0,
|
|
133
|
+
size_of_heap_commit: opt.sizeOfHeapCommit ?? 0,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* -------------------------
|
|
138
|
+
Main YARA-Compatible Parser
|
|
139
|
+
------------------------- */
|
|
140
|
+
export async function parsePEYara(inputBytes) {
|
|
141
|
+
if (inputBytes instanceof ArrayBuffer) inputBytes = new Uint8Array(inputBytes);
|
|
142
|
+
|
|
143
|
+
let exe;
|
|
144
|
+
try { exe = PE.NtExecutable.from(inputBytes); }
|
|
145
|
+
catch(e) { return { error: "Failed to parse PE", message: e.message }; }
|
|
146
|
+
|
|
147
|
+
const sections = await parsePESections(exe, inputBytes);
|
|
148
|
+
const imports = parseImports(exe);
|
|
149
|
+
const exportsArr = parseExports(exe);
|
|
150
|
+
const imphash = computeImpHash(imports);
|
|
151
|
+
const digital_signature = detectAuthenticode(exe);
|
|
152
|
+
const rich_header = detectRichHeader(inputBytes);
|
|
153
|
+
const optional_header = parseOptionalHeaderFlat(exe);
|
|
154
|
+
|
|
155
|
+
// Get optional header reference (supports both old and new pe-library versions)
|
|
156
|
+
const optHeader = exe.newHeader?.optionalHeader || exe.optionalHeader;
|
|
157
|
+
const coffHeader = exe.newHeader?.fileHeader || exe.coffHeader;
|
|
158
|
+
|
|
159
|
+
// Convert entry_point from RVA to file offset (like we do for ELF)
|
|
160
|
+
const entryPointRVA = optHeader?.addressOfEntryPoint ?? 0;
|
|
161
|
+
let entryPointOffset = entryPointRVA;
|
|
162
|
+
|
|
163
|
+
if (entryPointRVA > 0 && sections.length > 0) {
|
|
164
|
+
// Find the section containing the entry point RVA
|
|
165
|
+
for (const section of sections) {
|
|
166
|
+
const sectionStart = section.virtual_address;
|
|
167
|
+
const sectionEnd = sectionStart + section.virtual_size;
|
|
168
|
+
|
|
169
|
+
if (entryPointRVA >= sectionStart && entryPointRVA < sectionEnd) {
|
|
170
|
+
// Convert RVA to file offset: file_offset = RVA - virtual_address + raw_data_offset
|
|
171
|
+
entryPointOffset = entryPointRVA - section.virtual_address + section.raw_data_offset;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const peObj = {
|
|
178
|
+
machine: coffHeader?.machine ?? 0,
|
|
179
|
+
entry_point: entryPointOffset,
|
|
180
|
+
image_base: optHeader?.imageBase ?? 0,
|
|
181
|
+
number_of_sections: sections.length,
|
|
182
|
+
sections,
|
|
183
|
+
imports,
|
|
184
|
+
exports: exportsArr,
|
|
185
|
+
imphash,
|
|
186
|
+
digital_signature,
|
|
187
|
+
rich_signature: rich_header, // matches YARA field name
|
|
188
|
+
// Flatten optional header fields at top-level for YARA
|
|
189
|
+
...optional_header,
|
|
190
|
+
file_size: inputBytes.length,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Add YARA-compatible helper methods
|
|
194
|
+
peObj.is_dll = () => {
|
|
195
|
+
const characteristics = coffHeader?.characteristics ?? 0;
|
|
196
|
+
return (characteristics & 0x2000) !== 0; // IMAGE_FILE_DLL
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
peObj.is_32bit = () => {
|
|
200
|
+
const magic = optHeader?.magic ?? 0;
|
|
201
|
+
return magic === 0x010b; // PE32
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
peObj.is_64bit = () => {
|
|
205
|
+
const magic = optHeader?.magic ?? 0;
|
|
206
|
+
return magic === 0x020b; // PE32+
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return peObj;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* -------------------------
|
|
213
|
+
PE Module for YARA Conditions
|
|
214
|
+
This wraps the parsed PE with YARA-compatible methods
|
|
215
|
+
------------------------- */
|
|
216
|
+
export function createPEModule(parsedPE) {
|
|
217
|
+
if (!parsedPE || parsedPE.error) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
// Direct properties
|
|
223
|
+
machine: parsedPE.machine,
|
|
224
|
+
entry_point: parsedPE.entry_point,
|
|
225
|
+
image_base: parsedPE.image_base,
|
|
226
|
+
number_of_sections: parsedPE.number_of_sections,
|
|
227
|
+
sections: parsedPE.sections,
|
|
228
|
+
imports: parsedPE.imports,
|
|
229
|
+
exports: parsedPE.exports,
|
|
230
|
+
imphash: parsedPE.imphash,
|
|
231
|
+
rich_signature: parsedPE.rich_signature,
|
|
232
|
+
digital_signature: parsedPE.digital_signature,
|
|
233
|
+
|
|
234
|
+
// Optional header fields
|
|
235
|
+
size_of_code: parsedPE.size_of_code,
|
|
236
|
+
size_of_initialized_data: parsedPE.size_of_initialized_data,
|
|
237
|
+
size_of_uninitialized_data: parsedPE.size_of_uninitialized_data,
|
|
238
|
+
address_of_entry_point: parsedPE.address_of_entry_point,
|
|
239
|
+
base_of_code: parsedPE.base_of_code,
|
|
240
|
+
base_of_data: parsedPE.base_of_data,
|
|
241
|
+
section_alignment: parsedPE.section_alignment,
|
|
242
|
+
file_alignment: parsedPE.file_alignment,
|
|
243
|
+
subsystem: parsedPE.subsystem,
|
|
244
|
+
dll_characteristics: parsedPE.dll_characteristics,
|
|
245
|
+
|
|
246
|
+
// YARA-compatible methods
|
|
247
|
+
is_dll: parsedPE.is_dll,
|
|
248
|
+
is_32bit: parsedPE.is_32bit,
|
|
249
|
+
is_64bit: parsedPE.is_64bit,
|
|
250
|
+
|
|
251
|
+
// Helper to check if a DLL is imported
|
|
252
|
+
imports_dll: (dllName) => {
|
|
253
|
+
const lower = dllName.toLowerCase();
|
|
254
|
+
return parsedPE.imports.some(imp => imp.dll.toLowerCase() === lower);
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
// Helper to check if a specific function is imported from a DLL
|
|
258
|
+
// Usage: pe.imports("kernel32.dll", "CreateProcess") or pe.imports("kernel32.dll")
|
|
259
|
+
importsFunction: (dllName, functionName) => {
|
|
260
|
+
const lower = dllName.toLowerCase();
|
|
261
|
+
const imp = parsedPE.imports.find(imp => imp.dll.toLowerCase() === lower);
|
|
262
|
+
if (!imp) return false;
|
|
263
|
+
|
|
264
|
+
if (!functionName) return true; // Just check if DLL is imported
|
|
265
|
+
|
|
266
|
+
const lowerFunc = functionName.toLowerCase();
|
|
267
|
+
return imp.functions.some(fn =>
|
|
268
|
+
fn.name && fn.name.toLowerCase() === lowerFunc
|
|
269
|
+
);
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
// Helper to check if a function is exported
|
|
273
|
+
exports_function: (functionName) => {
|
|
274
|
+
const lower = functionName.toLowerCase();
|
|
275
|
+
return parsedPE.exports.some(exp => exp.toLowerCase() === lower);
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
// PE constants (common values used in YARA rules)
|
|
279
|
+
MACHINE_I386: 0x014c,
|
|
280
|
+
MACHINE_AMD64: 0x8664,
|
|
281
|
+
MACHINE_ARM: 0x01c0,
|
|
282
|
+
MACHINE_ARM64: 0xaa64,
|
|
283
|
+
|
|
284
|
+
SUBSYSTEM_NATIVE: 1,
|
|
285
|
+
SUBSYSTEM_WINDOWS_GUI: 2,
|
|
286
|
+
SUBSYSTEM_WINDOWS_CUI: 3,
|
|
287
|
+
|
|
288
|
+
SECTION_CNT_CODE: 0x00000020,
|
|
289
|
+
SECTION_CNT_INITIALIZED_DATA: 0x00000040,
|
|
290
|
+
SECTION_CNT_UNINITIALIZED_DATA: 0x00000080,
|
|
291
|
+
SECTION_MEM_EXECUTE: 0x20000000,
|
|
292
|
+
SECTION_MEM_READ: 0x40000000,
|
|
293
|
+
SECTION_MEM_WRITE: 0x80000000,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
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 defaultLogger = typeof console !== "undefined" && typeof console.log === "function" ? (...args) => console.log(...args) : () => {};
|
|
18
|
+
|
|
19
|
+
const hasPerformance = typeof globalThis !== "undefined" && globalThis.performance && typeof globalThis.performance.now === "function";
|
|
20
|
+
|
|
21
|
+
function now() {
|
|
22
|
+
return hasPerformance ? globalThis.performance.now() : Date.now();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function cloneDeep(value) {
|
|
26
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class PerformanceTracker {
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
const enabled = options.enabled ?? options.enableTiming ?? false;
|
|
32
|
+
const logger = options.logger ?? defaultLogger;
|
|
33
|
+
const autoPrint = options.autoPrint ?? false;
|
|
34
|
+
|
|
35
|
+
this.options = {
|
|
36
|
+
enabled: Boolean(enabled),
|
|
37
|
+
logger,
|
|
38
|
+
autoPrint: Boolean(autoPrint),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
this.lastTiming = {
|
|
42
|
+
compile: null,
|
|
43
|
+
scan: null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
isEnabled() {
|
|
48
|
+
return Boolean(this.options.enabled);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
now() {
|
|
52
|
+
return now();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
updateOptions(options = {}) {
|
|
56
|
+
if (Object.prototype.hasOwnProperty.call(options, "enabled") || Object.prototype.hasOwnProperty.call(options, "enableTiming")) {
|
|
57
|
+
this.options.enabled = Boolean(options.enabled ?? options.enableTiming);
|
|
58
|
+
}
|
|
59
|
+
if (Object.prototype.hasOwnProperty.call(options, "logger")) {
|
|
60
|
+
this.options.logger = options.logger ?? defaultLogger;
|
|
61
|
+
}
|
|
62
|
+
if (Object.prototype.hasOwnProperty.call(options, "autoPrint")) {
|
|
63
|
+
this.options.autoPrint = Boolean(options.autoPrint);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
enable(enable = true, logger) {
|
|
68
|
+
this.updateOptions({ enabled: enable, logger });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
clearCompile() {
|
|
72
|
+
this.lastTiming.compile = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
clearScan() {
|
|
76
|
+
this.lastTiming.scan = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
recordCompile(duration) {
|
|
80
|
+
const entry = { total: duration };
|
|
81
|
+
this.lastTiming.compile = entry;
|
|
82
|
+
this.printTiming("compile", entry);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
createScanTiming() {
|
|
86
|
+
return {
|
|
87
|
+
total: 0,
|
|
88
|
+
steps: {
|
|
89
|
+
buildAutomaton: 0,
|
|
90
|
+
acSearch: 0,
|
|
91
|
+
deduplicate: 0,
|
|
92
|
+
verifyCandidates: 0,
|
|
93
|
+
buildMatches: 0,
|
|
94
|
+
evaluateConditions: 0,
|
|
95
|
+
filterStrings: 0,
|
|
96
|
+
},
|
|
97
|
+
modules: {
|
|
98
|
+
total: 0,
|
|
99
|
+
pe: 0,
|
|
100
|
+
elf: 0,
|
|
101
|
+
math: 0,
|
|
102
|
+
hash: 0,
|
|
103
|
+
},
|
|
104
|
+
conditionParsing: {
|
|
105
|
+
total: 0,
|
|
106
|
+
byRule: {},
|
|
107
|
+
},
|
|
108
|
+
matchCount: 0,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
finalizeScan(scanTiming) {
|
|
113
|
+
this.lastTiming.scan = scanTiming;
|
|
114
|
+
this.printTiming("scan", scanTiming);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
accumulateStep(steps, key, duration) {
|
|
118
|
+
if (!steps || !Number.isFinite(duration)) return;
|
|
119
|
+
steps[key] = (steps[key] || 0) + duration;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
accumulateModule(modules, key, duration) {
|
|
123
|
+
if (!modules || !Number.isFinite(duration)) return;
|
|
124
|
+
modules.total = (modules.total || 0) + duration;
|
|
125
|
+
modules[key] = (modules[key] || 0) + duration;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
accumulateCondition(conditionTiming, ruleName, duration) {
|
|
129
|
+
if (!conditionTiming || !Number.isFinite(duration)) return;
|
|
130
|
+
conditionTiming.total = (conditionTiming.total || 0) + duration;
|
|
131
|
+
if (!conditionTiming.byRule) {
|
|
132
|
+
conditionTiming.byRule = {};
|
|
133
|
+
}
|
|
134
|
+
conditionTiming.byRule[ruleName] = (conditionTiming.byRule[ruleName] || 0) + duration;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getSnapshot() {
|
|
138
|
+
return cloneDeep(this.lastTiming);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
printTiming(phase, data) {
|
|
142
|
+
if (!this.options.enabled || !this.options.autoPrint || !this.options.logger || !data) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const logger = this.options.logger;
|
|
146
|
+
if (phase === "compile") {
|
|
147
|
+
logger(`[InterceptScanner] Compile completed in ${data.total.toFixed(3)}ms`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (phase === "scan") {
|
|
152
|
+
const matchCount = data.matchCount ?? "n/a";
|
|
153
|
+
logger(`[InterceptScanner] Scan completed in ${data.total.toFixed(3)}ms (matches: ${matchCount})`);
|
|
154
|
+
if (data.steps) {
|
|
155
|
+
Object.entries(data.steps).forEach(([step, value]) => {
|
|
156
|
+
if (value == null) return;
|
|
157
|
+
logger(` • ${step}: ${value.toFixed(3)}ms`);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (data.modules && data.modules.total) {
|
|
161
|
+
logger(` • moduleCreation.total: ${data.modules.total.toFixed(3)}ms`);
|
|
162
|
+
}
|
|
163
|
+
if (data.conditionParsing && data.conditionParsing.total) {
|
|
164
|
+
logger(` • conditionParsing.total: ${data.conditionParsing.total.toFixed(3)}ms`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function createPerformanceTracker(options = {}) {
|
|
171
|
+
return new PerformanceTracker(options);
|
|
172
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
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 SparkMD5 from 'spark-md5';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Detect runtime environment
|
|
21
|
+
*/
|
|
22
|
+
const isBrowser = typeof crypto !== 'undefined';
|
|
23
|
+
const isNode = typeof globalThis.process !== 'undefined' && globalThis.process.versions && globalThis.process.versions.node;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get crypto object based on environment
|
|
27
|
+
* In browser: window.crypto or self.crypto
|
|
28
|
+
* In Node.js: assumes crypto is available in global namespace
|
|
29
|
+
*/
|
|
30
|
+
function getCrypto() {
|
|
31
|
+
if (isBrowser) {
|
|
32
|
+
return crypto;
|
|
33
|
+
} else if (isNode) {
|
|
34
|
+
// In Node.js, assume crypto has been imported and is available globally
|
|
35
|
+
return globalThis.crypto;
|
|
36
|
+
}
|
|
37
|
+
throw new Error('Crypto API not available in this environment');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function computeEntropy(u8) {
|
|
41
|
+
const counts = new Uint32Array(256);
|
|
42
|
+
for (let i = 0; i < u8.length; i++) counts[u8[i]]++;
|
|
43
|
+
let entropy = 0;
|
|
44
|
+
const len = u8.length;
|
|
45
|
+
if (len === 0) return 0;
|
|
46
|
+
for (let i = 0; i < 256; i++) {
|
|
47
|
+
if (counts[i] === 0) continue;
|
|
48
|
+
const p = counts[i] / len;
|
|
49
|
+
entropy -= p * Math.log2(p);
|
|
50
|
+
}
|
|
51
|
+
return entropy;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function toHex(u8) {
|
|
55
|
+
return Array.from(u8).map(b => b.toString(16).padStart(2, "0")).join("");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Calculate hash digest and return as hex string
|
|
60
|
+
* @param {string} algo - Algorithm name (e.g., 'SHA-1', 'SHA-256')
|
|
61
|
+
* @param {Uint8Array} buf - Data to hash
|
|
62
|
+
* @returns {Promise<string>} Hex string
|
|
63
|
+
*/
|
|
64
|
+
export async function digestHex(algo, buf) {
|
|
65
|
+
const crypto = getCrypto();
|
|
66
|
+
const hash = await crypto.subtle.digest(algo, buf);
|
|
67
|
+
return toHex(new Uint8Array(hash));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Calculate MD5 hash using SparkMD5
|
|
72
|
+
* @param {Uint8Array} data - Data to hash
|
|
73
|
+
* @returns {string} Hex string
|
|
74
|
+
*/
|
|
75
|
+
export function calculateMD5(data) {
|
|
76
|
+
const spark = new SparkMD5.ArrayBuffer();
|
|
77
|
+
spark.append(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
|
|
78
|
+
return spark.end();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Endianness-aware readers */
|
|
82
|
+
export function readUint16(view, offset, littleEndian) {
|
|
83
|
+
return view.getUint16(offset, littleEndian);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function readUint32(view, offset, littleEndian) {
|
|
87
|
+
return view.getUint32(offset, littleEndian);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function readUint64(view, offset, littleEndian) {
|
|
91
|
+
const lo = view.getUint32(offset + (littleEndian ? 0 : 4), littleEndian);
|
|
92
|
+
const hi = view.getUint32(offset + (littleEndian ? 4 : 0), littleEndian);
|
|
93
|
+
return hi * 2 ** 32 + lo;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Null-terminated UTF-8 string */
|
|
97
|
+
export function readString(buf, offset, maxLength = 1024) {
|
|
98
|
+
const end = Math.min(buf.length, offset + maxLength);
|
|
99
|
+
let str = "";
|
|
100
|
+
for (let i = offset; i < end; i++) {
|
|
101
|
+
if (buf[i] === 0) break;
|
|
102
|
+
str += String.fromCharCode(buf[i]);
|
|
103
|
+
}
|
|
104
|
+
return str;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Null-terminated UTF-16 LE string */
|
|
108
|
+
export function readUtf16leString(view, offset, maxBytes = 2048) {
|
|
109
|
+
let str = "";
|
|
110
|
+
for (let i = 0; i + 1 < maxBytes; i += 2) {
|
|
111
|
+
const code = view.getUint16(offset + i, true);
|
|
112
|
+
if (code === 0) break;
|
|
113
|
+
str += String.fromCharCode(code);
|
|
114
|
+
}
|
|
115
|
+
return str;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Find a pattern in Uint8Array */
|
|
119
|
+
export function findPattern(buf, pattern) {
|
|
120
|
+
const len = buf.length, plen = pattern.length;
|
|
121
|
+
for (let i = 0; i <= len - plen; i++) {
|
|
122
|
+
let ok = true;
|
|
123
|
+
for (let j = 0; j < plen; j++) if (buf[i + j] !== pattern[j]) { ok = false; break; }
|
|
124
|
+
if (ok) return i;
|
|
125
|
+
}
|
|
126
|
+
return -1;
|
|
127
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
export const string = {
|
|
18
|
+
to_int: (s, base = 10) => {
|
|
19
|
+
if (typeof s !== "string" && !(s instanceof String)) {
|
|
20
|
+
throw new Error("to_int() first argument must be a string");
|
|
21
|
+
// return 0; // Match YARA behavior
|
|
22
|
+
}
|
|
23
|
+
if (s.startsWith("0x") || s.startsWith("0X")) {
|
|
24
|
+
base = 16;
|
|
25
|
+
s = s.slice(2);
|
|
26
|
+
} else if (s.startsWith("O") || s.startsWith("o")) {
|
|
27
|
+
base = 8;
|
|
28
|
+
s = s.slice(1);
|
|
29
|
+
}
|
|
30
|
+
const result = parseInt(s, base);
|
|
31
|
+
if (isNaN(result)) {
|
|
32
|
+
throw new Error(`to_int() could not convert string '${s}' to integer`);
|
|
33
|
+
// return 0; // Match YARA behavior
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
},
|
|
37
|
+
length: (s) => {
|
|
38
|
+
if (typeof s === "string" || s instanceof String) {
|
|
39
|
+
return s.length;
|
|
40
|
+
}
|
|
41
|
+
return 0;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
export const time = {
|
|
18
|
+
now: () => Date.now()
|
|
19
|
+
}
|