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.
@@ -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
+ }