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,506 @@
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 Math Module Implementation
19
+ *
20
+ * Provides mathematical and statistical functions for YARA rules.
21
+ * This implementation matches the official YARA math module API.
22
+ *
23
+ * Supported functions:
24
+ * - entropy(offset, size) : Calculate Shannon entropy
25
+ * - entropy(string) : Calculate Shannon entropy of string
26
+ * - monte_carlo_pi(offset, size) : Monte Carlo estimation of Pi
27
+ * - serial_correlation(offset, size) : Serial correlation coefficient
28
+ * - mean(offset, size) : Arithmetic mean of bytes
29
+ * - deviation(offset, size) : Standard deviation of bytes
30
+ * - min(offset, size) : Minimum byte value
31
+ * - max(offset, size) : Maximum byte value
32
+ * - abs(value) : Absolute value
33
+ * - count(byte, offset, size) : Count occurrences of byte
34
+ * - percentage(byte, offset, size) : Percentage of byte occurrences
35
+ * - mode(offset, size) : Most common byte value
36
+ * - to_number(bool) : Convert boolean to number
37
+ * - in_range(value, lower, upper): Check if value is in range
38
+ *
39
+ * All statistical functions operate on byte values (0-255).
40
+ *
41
+ * @see https://yara.readthedocs.io/en/stable/modules/math.html
42
+ */
43
+
44
+ /**
45
+ * Convert string to Uint8Array
46
+ * @param {string} str
47
+ * @returns {Uint8Array}
48
+ */
49
+ function stringToBytes(str) {
50
+ const encoder = new TextEncoder();
51
+ return encoder.encode(str);
52
+ }
53
+
54
+ /**
55
+ * Calculate Shannon entropy of data
56
+ * Entropy measures the randomness/unpredictability of data
57
+ * @param {Uint8Array} data
58
+ * @returns {number} Entropy value (0 to 8 bits per byte)
59
+ */
60
+ function calculateEntropy(data) {
61
+ if (data.length === 0) return 0.0;
62
+
63
+ // Count frequency of each byte value (0-255)
64
+ const frequency = new Uint32Array(256);
65
+ for (let i = 0; i < data.length; i++) {
66
+ frequency[data[i]]++;
67
+ }
68
+
69
+ // Calculate entropy
70
+ let entropy = 0.0;
71
+ const length = data.length;
72
+
73
+ for (let i = 0; i < 256; i++) {
74
+ if (frequency[i] > 0) {
75
+ const probability = frequency[i] / length;
76
+ entropy -= probability * Math.log2(probability);
77
+ }
78
+ }
79
+
80
+ return entropy;
81
+ }
82
+
83
+ /**
84
+ * Calculate arithmetic mean of byte values
85
+ * @param {Uint8Array} data
86
+ * @returns {number} Mean value (0 to 255)
87
+ */
88
+ function calculateMean(data) {
89
+ if (data.length === 0) return 0.0;
90
+
91
+ let sum = 0;
92
+ for (let i = 0; i < data.length; i++) {
93
+ sum += data[i];
94
+ }
95
+
96
+ return sum / data.length;
97
+ }
98
+
99
+ /**
100
+ * Calculate standard deviation of byte values
101
+ * @param {Uint8Array} data
102
+ * @returns {number} Standard deviation
103
+ */
104
+ function calculateDeviation(data, mean) {
105
+ if (data.length === 0) return 0.0;
106
+
107
+ let sumSquaredDiff = 0;
108
+
109
+ for (let i = 0; i < data.length; i++) {
110
+ const diff = data[i] - mean;
111
+ sumSquaredDiff += diff * diff;
112
+ }
113
+
114
+ return Math.sqrt(sumSquaredDiff / data.length);
115
+ }
116
+
117
+ /**
118
+ * Monte Carlo estimation of Pi using byte pairs as coordinates
119
+ * @param {Uint8Array} data
120
+ * @returns {number} Estimated value of Pi (typically close to 3.14159...)
121
+ */
122
+ function calculateMonteCarloPI(data) {
123
+ if (data.length < 2) return 0.0;
124
+
125
+ let insideCircle = 0;
126
+ let totalPoints = 0;
127
+
128
+ // Use consecutive byte pairs as (x, y) coordinates
129
+ for (let i = 0; i < data.length - 1; i += 2) {
130
+ // Normalize coordinates to [-1, 1] range
131
+ const x = (data[i] / 255.0) * 2.0 - 1.0;
132
+ const y = (data[i + 1] / 255.0) * 2.0 - 1.0;
133
+
134
+ // Check if point is inside unit circle
135
+ if (x * x + y * y <= 1.0) {
136
+ insideCircle++;
137
+ }
138
+ totalPoints++;
139
+ }
140
+
141
+ if (totalPoints === 0) return 0.0;
142
+
143
+ // Pi ≈ 4 * (points inside circle / total points)
144
+ return (4.0 * insideCircle) / totalPoints;
145
+ }
146
+
147
+ /**
148
+ * Calculate serial correlation coefficient
149
+ * Measures correlation between consecutive bytes
150
+ * @param {Uint8Array} data
151
+ * @returns {number} Correlation coefficient (-1 to 1)
152
+ */
153
+ function calculateSerialCorrelation(data) {
154
+ if (data.length < 2) return 0.0;
155
+
156
+ const mean = calculateMean(data);
157
+
158
+ let numerator = 0;
159
+ let denominator = 0;
160
+
161
+ for (let i = 0; i < data.length - 1; i++) {
162
+ numerator += (data[i] - mean) * (data[i + 1] - mean);
163
+ }
164
+
165
+ for (let i = 0; i < data.length; i++) {
166
+ const diff = data[i] - mean;
167
+ denominator += diff * diff;
168
+ }
169
+
170
+ if (denominator === 0) return 0.0;
171
+
172
+ return numerator / denominator;
173
+ }
174
+
175
+ /**
176
+ * Find minimum byte value
177
+ * @param {Array<number>} data
178
+ * @returns {number} Minimum value
179
+ */
180
+ function findMin(...data) {
181
+ if (data.length === 0) throw new Error("No data provided");
182
+
183
+ return Math.min(...data);
184
+ }
185
+
186
+ /**
187
+ * Find maximum byte value
188
+ * @param {Array<number>} data
189
+ * @returns {number} Maximum value
190
+ */
191
+ function findMax(...data) {
192
+ if (data.length === 0) throw new Error("No data provided");
193
+
194
+ return Math.max(...data);
195
+ }
196
+
197
+ /**
198
+ * Count occurrences of a specific byte value
199
+ * @param {number} byte - Byte value to count (0-255)
200
+ * @param {Uint8Array} data
201
+ * @returns {number} Count of occurrences
202
+ */
203
+ function countByte(byte, data) {
204
+ let count = 0;
205
+ for (let i = 0; i < data.length; i++) {
206
+ if (data[i] === byte) {
207
+ count++;
208
+ }
209
+ }
210
+ return count;
211
+ }
212
+
213
+ /**
214
+ * Calculate percentage of a specific byte value
215
+ * @param {number} byte - Byte value (0-255)
216
+ * @param {Uint8Array} data
217
+ * @returns {number} Percentage (0.0 to 1.0)
218
+ */
219
+ function percentageByte(byte, data) {
220
+ if (data.length === 0) return 0.0;
221
+ return countByte(byte, data) / data.length;
222
+ }
223
+
224
+ /**
225
+ * Find the mode (most common byte value)
226
+ * @param {Uint8Array} data
227
+ * @returns {number} Most common byte value (0-255)
228
+ */
229
+ function findMode(data) {
230
+ if (data.length === 0) return 0;
231
+
232
+ const frequency = new Uint32Array(256);
233
+ for (let i = 0; i < data.length; i++) {
234
+ frequency[data[i]]++;
235
+ }
236
+
237
+ let maxFreq = 0;
238
+ let mode = 0;
239
+
240
+ for (let i = 0; i < 256; i++) {
241
+ if (frequency[i] > maxFreq) {
242
+ maxFreq = frequency[i];
243
+ mode = i;
244
+ }
245
+ }
246
+
247
+ return mode;
248
+ }
249
+
250
+ /**
251
+ * Create a math module instance for a specific data buffer
252
+ * @param {Uint8Array} data - The data to perform mathematical operations on
253
+ * @returns {Object} Math module with all YARA math functions
254
+ */
255
+ export function createMathModule(data) {
256
+ if (!data || !(data instanceof Uint8Array)) {
257
+ throw new Error("Math module requires a Uint8Array data buffer");
258
+ }
259
+
260
+ return {
261
+ /**
262
+ * Calculate Shannon entropy
263
+ * Can be called with (offset, size) or (string)
264
+ */
265
+ entropy: function (...args) {
266
+ if (args.length === 1 && typeof args[0] === "string") {
267
+ // entropy(string)
268
+ const bytes = stringToBytes(args[0]);
269
+ return calculateEntropy(bytes);
270
+ } else if (args.length === 2) {
271
+ // entropy(offset, size)
272
+ const offset = args[0];
273
+ const size = args[1];
274
+ if (offset < 0 || size < 0) {
275
+ throw new Error(`Invalid range: offset=${offset}, size=${size}, data.length=${data.length}`);
276
+ }
277
+ if (offset >= data.length || offset + size > data.length) return 0;
278
+ const slice = data.slice(offset, offset + size);
279
+ if (slice.length === 0) return 0;
280
+ return calculateEntropy(slice);
281
+ } else {
282
+ throw new Error("entropy() requires either (offset, size) or (string)");
283
+ }
284
+ },
285
+
286
+ /**
287
+ * Monte Carlo estimation of Pi
288
+ * Called with (offset, size) or (string)
289
+ */
290
+ monte_carlo_pi: function (...args) {
291
+ if (args.length === 1 && typeof args[0] === "string") {
292
+ // monte_carlo_pi(string)
293
+ const bytes = stringToBytes(args[0]);
294
+ return calculateMonteCarloPI(bytes);
295
+ } else if (args.length === 2) {
296
+ const [offset, size] = args;
297
+ if (offset < 0 || size < 0) {
298
+ throw new Error(`Invalid range: offset=${offset}, size=${size}, data.length=${data.length}`);
299
+ }
300
+ if (offset >= data.length || offset + size > data.length) return 0;
301
+ const slice = data.slice(offset, offset + size);
302
+ if (slice.length === 0) return 0;
303
+ return calculateMonteCarloPI(slice);
304
+ } else {
305
+ throw new Error("monte_carlo_pi() requires (offset, size)");
306
+ }
307
+ },
308
+
309
+ /**
310
+ * Serial correlation coefficient
311
+ * Called with (offset, size) or (string)
312
+ */
313
+ serial_correlation: function (...args) {
314
+ if (args.length === 1 && typeof args[0] === "string") {
315
+ // monte_carlo_pi(string)
316
+ const bytes = stringToBytes(args[0]);
317
+ return calculateSerialCorrelation(bytes);
318
+ } else if (args.length === 2) {
319
+ const [offset, size] = args;
320
+ if (offset < 0 || size < 0) {
321
+ throw new Error(`Invalid range: offset=${offset}, size=${size}, data.length=${data.length}`);
322
+ }
323
+ if (offset >= data.length || offset + size > data.length) return 0;
324
+ const slice = data.slice(offset, offset + size);
325
+ if (slice.length === 0) return 0;
326
+ return calculateSerialCorrelation(slice);
327
+ } else {
328
+ throw new Error("serial_correlation() requires (offset, size)");
329
+ }
330
+ },
331
+
332
+ /**
333
+ * Arithmetic mean of bytes
334
+ * Called with (offset, size) or (string)
335
+ */
336
+ mean: function (...args) {
337
+ if (args.length === 1 && typeof args[0] === "string") {
338
+ // monte_carlo_pi(string)
339
+ const bytes = stringToBytes(args[0]);
340
+ return calculateMean(bytes);
341
+ } else if (args.length === 2) {
342
+ const [offset, size] = args;
343
+ if (offset < 0 || size < 0) {
344
+ throw new Error(`Invalid range: offset=${offset}, size=${size}, data.length=${data.length}`);
345
+ }
346
+ if (offset >= data.length || offset + size > data.length) return 0;
347
+ const slice = data.slice(offset, offset + size);
348
+ if (slice.length === 0) return 0;
349
+ return calculateMean(slice);
350
+ } else {
351
+ throw new Error("mean() requires (offset, size)");
352
+ }
353
+ },
354
+
355
+ /**
356
+ * Standard deviation of bytes
357
+ * Called with (offset, size, mean) or (string)
358
+ */
359
+ deviation: function (...args) {
360
+ if (args.length === 2 && typeof args[0] === "string") {
361
+ // deviation(string)
362
+ const [str, mean] = args;
363
+ const bytes = stringToBytes(str);
364
+ return calculateDeviation(bytes, mean);
365
+ } else if (args.length === 3) {
366
+ const [offset, size, mean] = args;
367
+ if (offset < 0 || size < 0) {
368
+ throw new Error(`Invalid range: offset=${offset}, size=${size}, data.length=${data.length}`);
369
+ }
370
+ if (offset >= data.length || offset + size > data.length) return 0;
371
+ const slice = data.slice(offset, offset + size);
372
+ if (slice.length === 0) return 0;
373
+ return calculateDeviation(slice, mean);
374
+ } else {
375
+ throw new Error("deviation() requires (offset, size, mean)");
376
+ }
377
+ },
378
+
379
+ /**
380
+ * Minimum byte value
381
+ * Called with (offset, size)
382
+ */
383
+ min: function (a, b) {
384
+ return findMin(a, b);
385
+ },
386
+
387
+ /**
388
+ * Maximum byte value
389
+ * Called with (offset, size)
390
+ */
391
+ max: function (a, b) {
392
+ return findMax(a, b);
393
+ },
394
+
395
+ /**
396
+ * Count occurrences of a byte
397
+ * Called with (byte, offset, size)
398
+ */
399
+ count: function (byte, offset, size) {
400
+ if (byte < 0 || byte > 255) {
401
+ throw new Error(`Invalid byte value: ${byte} (must be 0-255)`);
402
+ }
403
+ if (offset < 0 || size < 0) {
404
+ throw new Error(`Invalid range: offset=${offset}, size=${size}, data.length=${data.length}`);
405
+ }
406
+ const slice = data.slice(offset, offset + size);
407
+ if (slice.length === 0) return 0;
408
+ return countByte(byte, slice);
409
+ },
410
+
411
+ /**
412
+ * Percentage of a byte value
413
+ * Called with (byte, offset, size)
414
+ */
415
+ percentage: function (byte, offset, size) {
416
+ return this.count(byte, offset, size) / size;
417
+ },
418
+
419
+ /**
420
+ * Most common byte value (mode)
421
+ * Called with (offset, size)
422
+ */
423
+ mode: function (offset, size) {
424
+ if (offset < 0 || size < 0) {
425
+ throw new Error(`Invalid range: offset=${offset}, size=${size}, data.length=${data.length}`);
426
+ }
427
+ const slice = data.slice(offset, offset + size);
428
+ if (slice.length === 0) return 0;
429
+ return findMode(slice);
430
+ },
431
+
432
+ /**
433
+ * Absolute value
434
+ * Called with (value)
435
+ */
436
+ abs: function (value) {
437
+ return Math.abs(value);
438
+ },
439
+
440
+ /**
441
+ * Convert boolean to number
442
+ * Called with (bool)
443
+ */
444
+ to_number: function (bool) {
445
+ return bool ? 1 : 0;
446
+ },
447
+
448
+ /**
449
+ * Check if value is in range
450
+ * Called with (value, lower, upper)
451
+ */
452
+ in_range: function (value, lower, upper) {
453
+ return value >= lower && value <= upper;
454
+ },
455
+ };
456
+ }
457
+
458
+ /**
459
+ * Standalone math functions that don't require a data buffer
460
+ */
461
+ export const math = {
462
+ /**
463
+ * Calculate entropy of a string
464
+ * @param {string} str
465
+ * @returns {number} Entropy value
466
+ */
467
+ entropy: function (str) {
468
+ const bytes = stringToBytes(str);
469
+ return calculateEntropy(bytes);
470
+ },
471
+
472
+ /**
473
+ * Absolute value
474
+ * @param {number} value
475
+ * @returns {number}
476
+ */
477
+ abs: function (value) {
478
+ return Math.abs(value);
479
+ },
480
+
481
+ /**
482
+ * Convert boolean to number
483
+ * @param {boolean} bool
484
+ * @returns {number} 1 for true, 0 for false
485
+ */
486
+ to_number: function (bool) {
487
+ return bool ? 1 : 0;
488
+ },
489
+
490
+ /**
491
+ * Check if value is in range [lower, upper]
492
+ * @param {number} value
493
+ * @param {number} lower
494
+ * @param {number} upper
495
+ * @returns {boolean}
496
+ */
497
+ in_range: function (value, lower, upper) {
498
+ return value >= lower && value <= upper;
499
+ },
500
+ };
501
+
502
+ // Export for convenience
503
+ export default {
504
+ createMathModule,
505
+ math,
506
+ };