packetsnitch 1.5.599

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.
Files changed (54) hide show
  1. package/.eslintrc.json +28 -0
  2. package/.webpack/x64/main/index.js +2 -0
  3. package/.webpack/x64/main/index.js.map +1 -0
  4. package/.webpack/x64/renderer/assets/css/rubikglitch.woff2 +0 -0
  5. package/.webpack/x64/renderer/assets/css/style.css +1916 -0
  6. package/.webpack/x64/renderer/assets/images/loading.gif +0 -0
  7. package/.webpack/x64/renderer/assets/images/logo.webp +0 -0
  8. package/.webpack/x64/renderer/assets/images/packet-snitch-tag.webp +0 -0
  9. package/.webpack/x64/renderer/main_window/index.html +3 -0
  10. package/.webpack/x64/renderer/main_window/index.js +3 -0
  11. package/.webpack/x64/renderer/main_window/index.js.LICENSE.txt +36 -0
  12. package/.webpack/x64/renderer/main_window/index.js.map +1 -0
  13. package/.webpack/x64/renderer/main_window/preload.js +2 -0
  14. package/.webpack/x64/renderer/main_window/preload.js.map +1 -0
  15. package/backend/common/GeoLite2-City.mmdb +0 -0
  16. package/backend/common/mac-vendors-export.csv +56923 -0
  17. package/backend/common/service-names-port-numbers.csv +15368 -0
  18. package/backend/requirements.txt +14 -0
  19. package/backend/snitch.py +3611 -0
  20. package/forge.config.js +80 -0
  21. package/package.json +102 -0
  22. package/ps-icon.ico +0 -0
  23. package/snitch.spec +44 -0
  24. package/src/assets/css/rubikglitch.woff2 +0 -0
  25. package/src/assets/css/style.css +1916 -0
  26. package/src/assets/images/loading.gif +0 -0
  27. package/src/assets/images/logo.webp +0 -0
  28. package/src/assets/images/packet-snitch-tag.webp +0 -0
  29. package/src/back-comm.js +70 -0
  30. package/src/decoders.js +579 -0
  31. package/src/filter.js +461 -0
  32. package/src/front.js +10 -0
  33. package/src/index.html +1036 -0
  34. package/src/logging.js +150 -0
  35. package/src/main.js +571 -0
  36. package/src/preload.js +73 -0
  37. package/src/renderer.js +30 -0
  38. package/src/ui/common-frontend.js +13 -0
  39. package/src/ui/context-menu.js +88 -0
  40. package/src/ui/decoders.js +1 -0
  41. package/src/ui/main-frontend.js +4957 -0
  42. package/src/ui/panels/crypt-panel.js +565 -0
  43. package/src/ui/panels/data-panel.js +151 -0
  44. package/src/ui/panels/data-tools-panel.js +939 -0
  45. package/src/ui/panels/install-screen.js +59 -0
  46. package/src/ui/panels/keystore-panel.js +1248 -0
  47. package/src/ui/panels/list-panel.js +403 -0
  48. package/src/ui/panels/stats-panel.js +351 -0
  49. package/src/ui/panels/summary-panel.js +63 -0
  50. package/webpack.main.config.js +11 -0
  51. package/webpack.plugins.js +13 -0
  52. package/webpack.preload.config.js +7 -0
  53. package/webpack.renderer.config.js +30 -0
  54. package/webpack.rules.js +35 -0
package/src/filter.js ADDED
@@ -0,0 +1,461 @@
1
+ // Pre-compiled regex patterns for getDataType (avoid recompilation on every call)
2
+ const REGEX_IPV4 = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
3
+ const REGEX_HEX = /^0x[0-9a-fA-F]+$/;
4
+ const REGEX_MAC = /^([0-9A-Fa-f]{2}([-:])){5}[0-9A-Fa-f]{2}$/;
5
+ const REGEX_ASCII = /^[\x00-\x7F]*$/;
6
+
7
+ // Memoization cache for getLeafKeys to avoid recomputing for same packet structures
8
+ const leafKeysCache = new Map();
9
+ // Cache for normalized/original key maps per host
10
+ const keyMapCache = new Map();
11
+
12
+ const operators = {
13
+ '==': (a, b) => a == b,
14
+ '!=': (a, b) => a != b,
15
+ '>=': (a, b) => a >= b,
16
+ '>': (a, b) => a > b,
17
+ '<=': (a, b) => a <= b,
18
+ '<': (a, b) => a < b,
19
+ };
20
+
21
+ const compare = (a, b, op) => (operators[op] || operators['=='])(a, b);
22
+
23
+ const getPacketKey = (p) => {
24
+ const hostKey = Object.keys(p.Host)[0];
25
+ const packetItem = p.Host[hostKey][0];
26
+ return `${hostKey}-${packetItem['Packet Info']['Packet Processed']}`;
27
+ };
28
+
29
+ const unionBy = (arr, keyFn) => {
30
+ const map = new Map();
31
+ for (const item of arr) map.set(keyFn(item), item);
32
+ return [...map.values()];
33
+ };
34
+
35
+ const intersectBy = (a, b, keyFn) => {
36
+ const setB = new Set(b.map(keyFn));
37
+ return a.filter((item) => setB.has(keyFn(item)));
38
+ };
39
+
40
+ const subtractBy = (source, excluded, keyFn) => {
41
+ const excludedSet = new Set(excluded.map(keyFn));
42
+ return source.filter((item) => !excludedSet.has(keyFn(item)));
43
+ };
44
+
45
+ function getAllPackets(data) {
46
+ const parsedHosts = typeof data === 'string' ? JSON.parse(data) : data;
47
+ const allPackets = [];
48
+ if (!parsedHosts?.Host) return allPackets;
49
+
50
+ for (const host in parsedHosts.Host) {
51
+ const hostPackets = parsedHosts.Host[host];
52
+ if (!Array.isArray(hostPackets)) continue;
53
+ for (const packetItem of hostPackets) {
54
+ allPackets.push({ Host: { [host]: [packetItem] } });
55
+ }
56
+ }
57
+
58
+ return allPackets;
59
+ }
60
+
61
+ function getDataType(data) {
62
+ if (REGEX_IPV4.test(data)) return 'IP';
63
+ if (REGEX_HEX.test(data)) return 'HEX';
64
+ if (REGEX_MAC.test(data)) return 'MAC';
65
+ if (Number.isInteger(data)) return 'INT';
66
+ if (typeof data === 'number') return 'FLOAT';
67
+ if (typeof data === 'string' && REGEX_ASCII.test(data)) return 'ASCII';
68
+ return 'BIN';
69
+ }
70
+
71
+ function searchFullKey(obj, targetKey) {
72
+ for (const objKey in obj) {
73
+ if (objKey === targetKey) return obj[objKey];
74
+ const val = obj[objKey];
75
+ if (val && typeof val === 'object') {
76
+ const res = searchFullKey(val, targetKey);
77
+ if (res !== undefined) return res;
78
+ }
79
+ }
80
+ }
81
+
82
+ function getLeafKeys(obj) {
83
+ const result = [];
84
+ const walk = (o) => {
85
+ for (const objKey in o) {
86
+ const val = o[objKey];
87
+
88
+ if (val && typeof val === 'object' && !Array.isArray(val)) {
89
+ walk(val);
90
+ } else {
91
+ result.push({
92
+ [objKey]: objKey.toLowerCase().replace(/ /g, '-'),
93
+ type: getDataType(val),
94
+ });
95
+ }
96
+ }
97
+ };
98
+
99
+ walk(obj);
100
+ return result;
101
+ }
102
+
103
+ function normalizeFilterKey(key) {
104
+ return key.toLowerCase().replace(/[._\s-]+/g, '-');
105
+ }
106
+
107
+ function getAliasedFieldValue(packetItem, normalizedKey) {
108
+ switch (normalizedKey) {
109
+ case 'wire-proto': {
110
+ const packetInfo = packetItem?.['Packet Info'] || {};
111
+ const explicitProtocol = packetInfo['Protocol'];
112
+ if (typeof explicitProtocol === 'string') {
113
+ return explicitProtocol.toLowerCase();
114
+ }
115
+ if (packetInfo['TCP']) return 'tcp';
116
+ if (packetInfo['UDP']) return 'udp';
117
+ if (packetInfo['ICMP']) return 'icmp';
118
+ return undefined;
119
+ }
120
+ case 'eth-src-vendor':
121
+ return (
122
+ packetItem?.['Packet Info']?.['Ethernet Frame']?.['MAC Source Vendor'] ??
123
+ packetItem?.['Packet Info']?.['Ethernet Frame']?.['ether.src.mac.vendor']
124
+ );
125
+ case 'mime-type':
126
+ return (
127
+ packetItem?.['Extra Info']?.['MIME Type'] ??
128
+ packetItem?.['Extra Info']?.['payload.mime']
129
+ );
130
+ case 'dns-qname': {
131
+ const aliasHostnames =
132
+ packetItem?.['Extra Info']?.['Traits']?.['Network Data']?.['Hostnames']?.['Hostnames'];
133
+ const dnsQname = searchFullKey(packetItem, 'dns.qname');
134
+ const dnsQnames = searchFullKey(packetItem, 'dns.qnames');
135
+ return [aliasHostnames, dnsQname, dnsQnames]
136
+ .flat()
137
+ .filter((value) => typeof value === 'string');
138
+ }
139
+ default:
140
+ return undefined;
141
+ }
142
+ }
143
+
144
+ function filterChunk(data, filter) {
145
+ const parsedHosts = typeof data === 'string' ? JSON.parse(data) : data;
146
+ const matchedPackets = [];
147
+ const comparisonOps = ['>=', '<=', '>', '<', '==', '!='];
148
+
149
+ // Pre-parse filter once outside the loop
150
+ if (!filter || !filter.includes(':')) return matchedPackets;
151
+ const [filterKey, filterValRaw] = filter.split(':').map((s) => s.trim());
152
+ const normalizedFilterKey = normalizeFilterKey(filterKey);
153
+ const filterModifier = comparisonOps.find((m) => filterValRaw.includes(m));
154
+ const filterValue = filterValRaw.replace(filterModifier, '').trim();
155
+ const filterValueLower = filterValue.toLowerCase();
156
+ const isStringFilter = ['ASCII', 'HEX', 'IP', 'MAC'].includes(getDataType(filterValue));
157
+
158
+ for (const host in parsedHosts.Host) {
159
+ const hostPackets = parsedHosts.Host[host];
160
+ const firstPacket = hostPackets[0];
161
+
162
+ // Use cached key maps per host to avoid recomputing
163
+ let keyMap = keyMapCache.get(host);
164
+ if (!keyMap) {
165
+ const leafKeyList = getLeafKeys(firstPacket);
166
+ keyMap = {
167
+ normalized: leafKeyList.map((k) => Object.values(k)[0]),
168
+ original: leafKeyList.map((k) => Object.keys(k)[0]),
169
+ };
170
+ keyMapCache.set(host, keyMap);
171
+ }
172
+
173
+ const targetIdx = keyMap.normalized.findIndex(
174
+ (candidateKey) => normalizeFilterKey(candidateKey) === normalizedFilterKey,
175
+ );
176
+ const originalKey = targetIdx === -1 ? null : keyMap.original[targetIdx];
177
+
178
+ for (const packetItem of hostPackets) {
179
+ let fieldValue = getAliasedFieldValue(packetItem, normalizedFilterKey);
180
+ if (fieldValue === undefined && originalKey) {
181
+ fieldValue = searchFullKey(packetItem, originalKey);
182
+ }
183
+ if (fieldValue === undefined) continue;
184
+
185
+ let matched = false;
186
+
187
+ if (
188
+ !filterModifier &&
189
+ ['dns-qname', 'eth-src-vendor', 'mime-type'].includes(normalizedFilterKey)
190
+ ) {
191
+ const textValues = Array.isArray(fieldValue) ? fieldValue : [fieldValue];
192
+ matched = textValues.some(
193
+ (value) =>
194
+ typeof value === 'string' &&
195
+ value.toLowerCase().includes(filterValueLower),
196
+ );
197
+ } else {
198
+ if (filterModifier) {
199
+ matched = compare(fieldValue, filterValue, filterModifier);
200
+ } else {
201
+ matched = compare(fieldValue, filterValue, '==');
202
+ }
203
+ }
204
+
205
+ if (!matched && isStringFilter) {
206
+ const type = getDataType(fieldValue);
207
+ if (['ASCII', 'HEX', 'IP', 'MAC'].includes(type)) {
208
+ matched = String(fieldValue).toLowerCase() === filterValueLower;
209
+ }
210
+ }
211
+
212
+ if (matched) {
213
+ matchedPackets.push({ Host: { [host]: [packetItem] } });
214
+ }
215
+ }
216
+ }
217
+ return matchedPackets;
218
+ }
219
+
220
+ function tokenizeQuery(query) {
221
+ const tokenList = [];
222
+ let i = 0;
223
+ while (i < query.length) {
224
+ if (/\s/.test(query[i])) {
225
+ i++;
226
+ continue;
227
+ }
228
+ if (query[i] === '(') {
229
+ tokenList.push({ type: 'LPAREN' });
230
+ i++;
231
+ continue;
232
+ }
233
+ if (query[i] === ')') {
234
+ tokenList.push({ type: 'RPAREN' });
235
+ i++;
236
+ continue;
237
+ }
238
+ if (query.startsWith('||', i)) {
239
+ tokenList.push({ type: 'OR' });
240
+ i += 2;
241
+ continue;
242
+ }
243
+ if (query.startsWith('&&', i)) {
244
+ tokenList.push({ type: 'AND' });
245
+ i += 2;
246
+ continue;
247
+ }
248
+ if (query[i] === '!' && query[i + 1] !== '=') {
249
+ tokenList.push({ type: 'NOT' });
250
+ i++;
251
+ continue;
252
+ }
253
+ let exprEnd = i;
254
+ while (
255
+ exprEnd < query.length &&
256
+ !query.startsWith('||', exprEnd) &&
257
+ !query.startsWith('&&', exprEnd) &&
258
+ !(query[exprEnd] === '!' && query[exprEnd + 1] !== '=') &&
259
+ query[exprEnd] !== '(' &&
260
+ query[exprEnd] !== ')'
261
+ ) {
262
+ exprEnd++;
263
+ }
264
+ const tokenExpr = query.slice(i, exprEnd).trim();
265
+ if (tokenExpr) tokenList.push({ type: 'EXPR', value: tokenExpr });
266
+ i = exprEnd;
267
+ }
268
+ return tokenList;
269
+ }
270
+
271
+ function validateFilterExpression(expression) {
272
+ if (typeof expression !== 'string') {
273
+ throw new Error('Filter expression must be text');
274
+ }
275
+
276
+ const separatorIndex = expression.indexOf(':');
277
+ if (separatorIndex === -1) {
278
+ throw new Error(`Missing ":" in expression "${expression.trim()}"`);
279
+ }
280
+
281
+ const filterKey = expression.slice(0, separatorIndex).trim();
282
+ const filterValue = expression.slice(separatorIndex + 1).trim();
283
+
284
+ if (!filterKey) {
285
+ throw new Error(`Missing filter field before ":" in "${expression.trim()}"`);
286
+ }
287
+ if (!filterValue) {
288
+ throw new Error(`Missing filter value after ":" in "${expression.trim()}"`);
289
+ }
290
+ }
291
+
292
+ function validateFilterSyntax(query) {
293
+ const normalizedQuery = typeof query === 'string' ? query.trim() : '';
294
+ if (!normalizedQuery) return true;
295
+
296
+ const tokenList = tokenizeQuery(normalizedQuery);
297
+ let pos = 0;
298
+
299
+ function peek() {
300
+ return tokenList[pos];
301
+ }
302
+
303
+ function consume(type) {
304
+ const currentToken = tokenList[pos];
305
+ if (type && (!currentToken || currentToken.type !== type)) {
306
+ throw new Error(
307
+ `Expected ${type} but got ${currentToken ? currentToken.type : 'EOF'}`,
308
+ );
309
+ }
310
+ pos++;
311
+ return currentToken;
312
+ }
313
+
314
+ function parseOr() {
315
+ parseAnd();
316
+ while (peek() && peek().type === 'OR') {
317
+ consume('OR');
318
+ parseAnd();
319
+ }
320
+ }
321
+
322
+ function parseAnd() {
323
+ parseTerm();
324
+ while (peek() && peek().type === 'AND') {
325
+ consume('AND');
326
+ parseTerm();
327
+ }
328
+ }
329
+
330
+ function parseTerm() {
331
+ const currentToken = peek();
332
+ if (!currentToken) {
333
+ throw new Error('Unexpected end of query');
334
+ }
335
+ if (currentToken.type === 'NOT') {
336
+ consume('NOT');
337
+ if (!peek()) {
338
+ throw new Error('Expected expression or group after !');
339
+ }
340
+ parseTerm();
341
+ return;
342
+ }
343
+ if (currentToken.type === 'LPAREN') {
344
+ consume('LPAREN');
345
+ if (peek()?.type === 'RPAREN') {
346
+ throw new Error('Empty parentheses are not allowed');
347
+ }
348
+ parseOr();
349
+ if (!peek() || peek().type !== 'RPAREN') {
350
+ throw new Error('Missing closing parenthesis');
351
+ }
352
+ consume('RPAREN');
353
+ return;
354
+ }
355
+ if (currentToken.type === 'EXPR') {
356
+ consume('EXPR');
357
+ validateFilterExpression(currentToken.value);
358
+ return;
359
+ }
360
+ if (currentToken.type === 'RPAREN') {
361
+ throw new Error('Unexpected closing parenthesis');
362
+ }
363
+ if (currentToken.type === 'AND' || currentToken.type === 'OR') {
364
+ throw new Error(`Unexpected operator ${currentToken.type}`);
365
+ }
366
+ throw new Error(`Unexpected token ${currentToken.type}`);
367
+ }
368
+
369
+ parseOr();
370
+ if (pos < tokenList.length) {
371
+ const remainingToken = tokenList[pos];
372
+ if (remainingToken.type === 'RPAREN') {
373
+ throw new Error('Unexpected closing parenthesis');
374
+ }
375
+ throw new Error(`Unexpected token ${remainingToken.type}`);
376
+ }
377
+ return true;
378
+ }
379
+
380
+ function runQuery(data, query) {
381
+ const tokenList = tokenizeQuery(query);
382
+ const allPackets = getAllPackets(data);
383
+ let pos = 0;
384
+
385
+ function peek() {
386
+ return tokenList[pos];
387
+ }
388
+ function consume(type) {
389
+ const currentToken = tokenList[pos];
390
+ if (type && (!currentToken || currentToken.type !== type)) {
391
+ throw new Error(
392
+ `Expected ${type} but got ${currentToken ? currentToken.type : 'EOF'}`,
393
+ );
394
+ }
395
+ pos++;
396
+ return currentToken;
397
+ }
398
+
399
+ function parseOr() {
400
+ let result = parseAnd();
401
+ while (peek() && peek().type === 'OR') {
402
+ consume('OR');
403
+ const rightResult = parseAnd();
404
+ result = unionBy([...result, ...rightResult], getPacketKey);
405
+ }
406
+ return result;
407
+ }
408
+
409
+ function parseAnd() {
410
+ let result = parseTerm();
411
+ while (peek() && peek().type === 'AND') {
412
+ consume('AND');
413
+ const rightResult = parseTerm();
414
+ result = intersectBy(result, rightResult, getPacketKey);
415
+ }
416
+ return result;
417
+ }
418
+
419
+ function parseTerm() {
420
+ const currentToken = peek();
421
+ if (currentToken && currentToken.type === 'NOT') {
422
+ consume('NOT');
423
+ if (!peek()) {
424
+ throw new Error('Expected expression or group after !');
425
+ }
426
+ const negatedResult = parseTerm();
427
+ return subtractBy(allPackets, negatedResult, getPacketKey);
428
+ }
429
+ if (currentToken && currentToken.type === 'LPAREN') {
430
+ consume('LPAREN');
431
+ const result = parseOr();
432
+ consume('RPAREN');
433
+ return result;
434
+ }
435
+ if (currentToken && currentToken.type === 'EXPR') {
436
+ consume('EXPR');
437
+ return filterChunk(data, currentToken.value);
438
+ }
439
+ return [];
440
+ }
441
+
442
+ return parseOr();
443
+ }
444
+
445
+ function filterPackets(data, query) {
446
+ let matchedPackets;
447
+ if (query.trim() === '') {
448
+ // dummy function so we can return all packets in the right format
449
+ matchedPackets = runQuery(data, 'wire-length:>=0'); // dummy filter that matches all packets
450
+ } else {
451
+ validateFilterSyntax(query);
452
+ matchedPackets = runQuery(data, query);
453
+ }
454
+
455
+ return matchedPackets.map((p) => {
456
+ const hostKey = Object.keys(p.Host)[0];
457
+ return p.Host[hostKey][0];
458
+ });
459
+ }
460
+
461
+ module.exports = { filterPackets, getDataType, validateFilterSyntax };
package/src/front.js ADDED
@@ -0,0 +1,10 @@
1
+ import "./ui/common-frontend";
2
+ import "./ui/decoders";
3
+ import "./ui/panels/summary-panel";
4
+ import "./ui/panels/data-panel";
5
+ import "./ui/panels/stats-panel";
6
+ import "./ui/panels/list-panel";
7
+ import "./ui/panels/data-tools-panel";
8
+ import "./ui/panels/crypt-panel";
9
+ import "./ui/panels/keystore-panel";
10
+ import "./ui/main-frontend";