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/main.js ADDED
@@ -0,0 +1,571 @@
1
+ const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { pathToFileURL } = require('url');
5
+ const { exec } = require('child_process');
6
+ const os = require('os');
7
+ const util = require('util');
8
+ const platform = os.platform();
9
+ const testcaseTempDir = path.join(os.tmpdir(), 'testcases');
10
+ const CONSOLE_INSPECT_DEPTH = 6;
11
+ const CONSOLE_MAX_ARRAY_LENGTH = 50;
12
+ let mainWindow;
13
+ let selectedFilePath;
14
+ let isBackendLoaded = false;
15
+ let versionFilePath;
16
+ let activityLogFilePath;
17
+ let hasLoggedProgramShutdown = false;
18
+ const activityLogEntries = [];
19
+ const pendingActivityLogEntries = [];
20
+ let isFirstRunAfterInstall = false;
21
+ let cachedOllamaInstalled = false;
22
+ if (require('electron-squirrel-startup')) {
23
+ app.quit();
24
+ }
25
+
26
+ ipcMain.handle('file-size', async () => {
27
+ try {
28
+ // Get file stats asynchronously
29
+ const fileStats = await fs.promises.stat(selectedFilePath); // Using promises version of stat
30
+ return fileStats.size; // Send back the file size
31
+ } catch (fileError) {
32
+ console.error('Error getting file stats:', fileError);
33
+ return 0; // Return 0 if there's an error
34
+ }
35
+ });
36
+
37
+ // make sure we have a fresh temp dir
38
+ fs.rmSync(testcaseTempDir, { recursive: true, force: true });
39
+
40
+ function killBackendProcess() {
41
+ console.log('Killing backend proc...');
42
+ if (platform === 'win32') {
43
+ exec('taskkill /IM snitch.exe /T /F', (fileError) => {
44
+ if (fileError) console.error(fileError);
45
+ });
46
+ }
47
+ if (platform === 'linux') {
48
+ exec('pkill -f "testcases"', (fileError) => {
49
+ if (fileError) console.error(fileError);
50
+ });
51
+ }
52
+ }
53
+
54
+ function checkOllama() {
55
+ return new Promise((resolve) => {
56
+ exec('ollama --version', (execError) => {
57
+ if (execError) {
58
+ resolve(false); // not installed or not in PATH
59
+ } else {
60
+ resolve(true);
61
+ }
62
+ });
63
+ });
64
+ }
65
+
66
+ function checkNewInstall() {
67
+ if (!versionFilePath) return false;
68
+ try {
69
+ if (!fs.existsSync(versionFilePath)) {
70
+ return true;
71
+ }
72
+ const storedVersion = fs.readFileSync(versionFilePath, 'utf8').trim();
73
+ return storedVersion !== app.getVersion();
74
+ } catch (err) {
75
+ console.error('Error checking install version:', err);
76
+ return true;
77
+ }
78
+ }
79
+
80
+ function createWindow() {
81
+ mainWindow = new BrowserWindow({
82
+ minWidth: 1450,
83
+ minHeight: 750,
84
+ frame: false,
85
+ webPreferences: {
86
+ preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
87
+ contextIsolation: true,
88
+ nodeIntegration: true,
89
+ },
90
+ });
91
+ mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
92
+ mainWindow.webContents.on('did-finish-load', () => {
93
+ mainWindow.webContents.setZoomFactor(0.8); // makes everything fit snuggly
94
+ });
95
+ mainWindow.once('close', () => {
96
+ appendActivityLogLine(
97
+ timestampLifecycleMessage(
98
+ `Session closed for PacketSnitch v${app.getVersion()}`,
99
+ ),
100
+ { broadcast: false },
101
+ );
102
+ });
103
+ }
104
+
105
+ function formatConsoleArgs(args) {
106
+ return args
107
+ .map((arg) => {
108
+ if (arg instanceof Error) {
109
+ return arg.stack || arg.message;
110
+ }
111
+ if (typeof arg === 'string') {
112
+ return arg;
113
+ }
114
+ return util.inspect(arg, {
115
+ depth: CONSOLE_INSPECT_DEPTH,
116
+ breakLength: Infinity,
117
+ maxArrayLength: CONSOLE_MAX_ARRAY_LENGTH,
118
+ });
119
+ })
120
+ .join(' ');
121
+ }
122
+
123
+ function appendActivityLogToFile(entry) {
124
+ try {
125
+ fs.appendFileSync(activityLogFilePath, entry + os.EOL, 'utf8');
126
+ } catch (error) {
127
+ console.error('Unable to append activity log:', error);
128
+ }
129
+ }
130
+
131
+ function cacheActivityLogEntry(entry) {
132
+ activityLogEntries.unshift(entry);
133
+ }
134
+
135
+ function broadcastActivityLogEntry(entry) {
136
+ if (mainWindow && !mainWindow.isDestroyed()) {
137
+ mainWindow.webContents.send('activity-log-entry', entry);
138
+ }
139
+ }
140
+
141
+ function normalizeActivityLogEntry(entry) {
142
+ if (typeof entry !== 'string' || entry.trim() === '') return null;
143
+ return entry.trim();
144
+ }
145
+
146
+ function timestampLifecycleMessage(message) {
147
+ return `[${new Date().toISOString()}] [Core] ${message}`;
148
+ }
149
+
150
+ function appendActivityLogLine(entry, options = {}) {
151
+ const { broadcast = true } = options;
152
+ const normalizedEntry = normalizeActivityLogEntry(entry);
153
+ if (!normalizedEntry) return;
154
+ cacheActivityLogEntry(normalizedEntry);
155
+ if (activityLogFilePath) {
156
+ appendActivityLogToFile(normalizedEntry);
157
+ } else {
158
+ pendingActivityLogEntries.push(normalizedEntry);
159
+ }
160
+ if (broadcast) {
161
+ broadcastActivityLogEntry(normalizedEntry);
162
+ }
163
+ }
164
+
165
+ function flushPendingActivityLogEntries() {
166
+ if (!activityLogFilePath || pendingActivityLogEntries.length === 0) return;
167
+ pendingActivityLogEntries.forEach((entry) => {
168
+ appendActivityLogToFile(entry);
169
+ });
170
+ pendingActivityLogEntries.splice(0);
171
+ }
172
+
173
+ const originalConsoleLog = console.log.bind(console);
174
+ console.log = (...args) => {
175
+ originalConsoleLog(...args);
176
+ const message = formatConsoleArgs(args);
177
+ if (!message) return;
178
+ appendActivityLogLine(
179
+ `[${new Date().toISOString()}] [Console][Main] ${message}`,
180
+ );
181
+ };
182
+
183
+ global.logBackend = (...args) => {
184
+ const message = formatConsoleArgs(args);
185
+ if (!message) return;
186
+ originalConsoleLog(message);
187
+ const timestamp = new Date().toISOString();
188
+ message.split(/\r?\n/).forEach((line) => {
189
+ if (line.trim() === '') return;
190
+ appendActivityLogLine(`[${timestamp}] [Console][Backend] ${line}`);
191
+ });
192
+ };
193
+
194
+ app.whenReady().then(() => {
195
+ versionFilePath = path.join(app.getPath('userData'), 'installed_version.txt');
196
+ activityLogFilePath = path.join(app.getPath('userData'), 'activity-log.txt');
197
+ flushPendingActivityLogEntries();
198
+ appendActivityLogLine(
199
+ `[${new Date().toISOString()}] [Core] Session started for PacketSnitch v${app.getVersion()}`,
200
+ );
201
+ isFirstRunAfterInstall = checkNewInstall();
202
+ checkOllama().then((isInstalled) => {
203
+ cachedOllamaInstalled = isInstalled;
204
+ if (!isInstalled) {
205
+ console.log(
206
+ 'Ollama is not installed. LLM summarisation will be unavailable.',
207
+ );
208
+ }
209
+ createWindow();
210
+ app.on('activate', function () {
211
+ if (BrowserWindow.getAllWindows().length === 0) createWindow();
212
+ });
213
+ console.log('App ready, waiting for file selection...');
214
+ // start the process that listens for the file selection and runs the backend command
215
+ require('./back-comm');
216
+ ipcMain.handle('select-file', async () => {
217
+ const { canceled, filePaths } = await dialog.showOpenDialog({
218
+ properties: ['openFile'],
219
+ });
220
+ if (canceled) return null;
221
+ console.log('Accepted pcapng.. Checking for json existence...');
222
+ isBackendLoaded = true;
223
+ // Remove stale output directory so snitch always starts with a clean slate
224
+ if (fs.existsSync(testcaseTempDir)) {
225
+ fs.rmSync(testcaseTempDir, { recursive: true, force: true });
226
+ }
227
+ console.log('File selected:', filePaths[0]);
228
+ selectedFilePath = filePaths[0];
229
+ return filePaths[0];
230
+ });
231
+ });
232
+ });
233
+
234
+ ipcMain.handle('check-first-run', async () => {
235
+ const isDev = !app.isPackaged;
236
+ const basePath = isDev
237
+ ? path.join(__dirname, '../..')
238
+ : process.resourcesPath;
239
+ const backendExe = platform === 'win32' ? 'snitch.exe' : 'snitch';
240
+ const filesToCheck = [
241
+ {
242
+ name: 'PacketSnitch Backend (' + backendExe + ')',
243
+ path: path.join(basePath, 'backend', backendExe),
244
+ },
245
+ {
246
+ name: 'GeoIP Database (GeoLite2-City.mmdb)',
247
+ path: path.join(basePath, 'backend', 'common', 'GeoLite2-City.mmdb'),
248
+ },
249
+ {
250
+ name: 'MAC Vendors Database (mac-vendors-export.csv)',
251
+ path: path.join(basePath, 'backend', 'common', 'mac-vendors-export.csv'),
252
+ },
253
+ {
254
+ name: 'Services Database (service-names-port-numbers.csv)',
255
+ path: path.join(
256
+ basePath,
257
+ 'backend',
258
+ 'common',
259
+ 'service-names-port-numbers.csv',
260
+ ),
261
+ },
262
+ ];
263
+ const installedFiles = filesToCheck.map((f) => ({
264
+ name: f.name,
265
+ path: f.path,
266
+ exists: fs.existsSync(f.path),
267
+ }));
268
+ return {
269
+ isFirstRun: isFirstRunAfterInstall,
270
+ version: app.getVersion(),
271
+ ollamaInstalled: cachedOllamaInstalled,
272
+ installedFiles,
273
+ };
274
+ });
275
+
276
+ ipcMain.handle('dismiss-first-run', async () => {
277
+ const currentVersion = app.getVersion();
278
+ try {
279
+ fs.writeFileSync(versionFilePath, currentVersion, 'utf8');
280
+ isFirstRunAfterInstall = false;
281
+ return { success: true };
282
+ } catch (err) {
283
+ console.error('Failed to write version file:', err);
284
+ return { success: false, error: err.message };
285
+ }
286
+ });
287
+
288
+ ipcMain.handle('quit-app', () => {
289
+ app.quit();
290
+ });
291
+
292
+ ipcMain.handle('prompt-save-session-on-exit', async () => {
293
+ const response = await dialog.showMessageBox({
294
+ type: 'question',
295
+ buttons: ['Save Session', "Don't Save", 'Cancel'],
296
+ defaultId: 0,
297
+ cancelId: 2,
298
+ title: 'Save Session',
299
+ message: 'Do you want to save your PacketSnitch session before exiting?',
300
+ });
301
+ if (response.response === 0) return 'save';
302
+ if (response.response === 1) return 'discard';
303
+ return 'cancel';
304
+ });
305
+
306
+ ipcMain.handle('save-json', async (_event, jsonData) => {
307
+ if (typeof jsonData !== 'string' || jsonData.trim() === '') {
308
+ return { success: false, error: 'No JSON data to save' };
309
+ }
310
+
311
+ const { canceled, filePath } = await dialog.showSaveDialog({
312
+ title: 'Save PacketSnitch Session',
313
+ defaultPath: path.join(app.getPath('documents'), 'packetsnitch-session.json'),
314
+ filters: [{ name: 'JSON Files', extensions: ['json'] }],
315
+ });
316
+ if (canceled || !filePath) return { success: false, canceled: true };
317
+
318
+ try {
319
+ await fs.promises.writeFile(filePath, jsonData, 'utf8');
320
+ return { success: true };
321
+ } catch (err) {
322
+ console.error('Save error:', err);
323
+ return { success: false, error: err.message };
324
+ }
325
+ });
326
+
327
+ ipcMain.handle('save-packet', async (_event, packetData) => {
328
+ if (packetData === null || packetData === undefined) {
329
+ return { success: false, error: 'No packet data to save' };
330
+ }
331
+ const packetJson = JSON.stringify(packetData, null, 2);
332
+
333
+ const { canceled, filePath } = await dialog.showSaveDialog({
334
+ title: 'Export Packet',
335
+ defaultPath: path.join(app.getPath('documents'), 'packet.json'),
336
+ filters: [{ name: 'JSON Files', extensions: ['json'] }],
337
+ });
338
+ if (canceled || !filePath) return { success: false, canceled: true };
339
+
340
+ try {
341
+ await fs.promises.writeFile(filePath, packetJson, 'utf8');
342
+ return { success: true };
343
+ } catch (err) {
344
+ console.error('Packet export error:', err);
345
+ return { success: false, error: err.message };
346
+ }
347
+ });
348
+
349
+ ipcMain.handle('save-payload', async (_event, payloadHex) => {
350
+ if (typeof payloadHex !== 'string') {
351
+ return { success: false, error: 'No payload data to save' };
352
+ }
353
+ const normalizedHex = payloadHex.replace(/\s+/g, '');
354
+ if (
355
+ normalizedHex.length === 0 ||
356
+ normalizedHex.length % 2 !== 0 ||
357
+ !/^[\da-fA-F]+$/.test(normalizedHex)
358
+ ) {
359
+ return {
360
+ success: false,
361
+ error: 'Payload must be a non-empty hex string with an even length',
362
+ };
363
+ }
364
+
365
+ const { canceled, filePath } = await dialog.showSaveDialog({
366
+ title: 'Export Packet Payload',
367
+ defaultPath: path.join(app.getPath('documents'), 'packet-payload.bin'),
368
+ filters: [
369
+ { name: 'Binary Files', extensions: ['bin'] },
370
+ { name: 'All Files', extensions: ['*'] },
371
+ ],
372
+ });
373
+ if (canceled || !filePath) return { success: false, canceled: true };
374
+
375
+ try {
376
+ const payloadBuffer = Buffer.from(normalizedHex, 'hex');
377
+ await fs.promises.writeFile(filePath, payloadBuffer);
378
+ return { success: true };
379
+ } catch (err) {
380
+ console.error('Payload export error:', err);
381
+ return { success: false, error: err.message };
382
+ }
383
+ });
384
+
385
+ ipcMain.handle('save-cookie-jar', async (_event, cookieJarText) => {
386
+ if (typeof cookieJarText !== 'string' || cookieJarText.trim() === '') {
387
+ return { success: false, error: 'No cookie jar data to save' };
388
+ }
389
+
390
+ const { canceled, filePath } = await dialog.showSaveDialog({
391
+ title: 'Save Cookie Jar',
392
+ defaultPath: path.join(app.getPath('documents'), 'cookie_jar.txt'),
393
+ filters: [
394
+ { name: 'Text Files', extensions: ['txt'] },
395
+ { name: 'All Files', extensions: ['*'] },
396
+ ],
397
+ });
398
+ if (canceled || !filePath) return { success: false, canceled: true };
399
+
400
+ try {
401
+ await fs.promises.writeFile(filePath, cookieJarText, 'utf8');
402
+ return { success: true };
403
+ } catch (err) {
404
+ console.error('Cookie jar save error:', err);
405
+ return { success: false, error: err.message };
406
+ }
407
+ });
408
+
409
+ ipcMain.handle('save-notes', async (_event, notesText) => {
410
+ if (typeof notesText !== 'string' || notesText.trim() === '') {
411
+ return { success: false, error: 'No notes data to save' };
412
+ }
413
+
414
+ const { canceled, filePath } = await dialog.showSaveDialog({
415
+ title: 'Save Notes',
416
+ defaultPath: path.join(app.getPath('documents'), 'packetsnitch-notes.txt'),
417
+ filters: [
418
+ { name: 'Text Files', extensions: ['txt'] },
419
+ { name: 'All Files', extensions: ['*'] },
420
+ ],
421
+ });
422
+ if (canceled || !filePath) return { success: false, canceled: true };
423
+
424
+ try {
425
+ await fs.promises.writeFile(filePath, notesText, 'utf8');
426
+ return { success: true };
427
+ } catch (err) {
428
+ console.error('Notes save error:', err);
429
+ return { success: false, error: err.message };
430
+ }
431
+ });
432
+
433
+ // Map a Content-Type header value to a file extension for HTTP body exports.
434
+ function extFromContentType(contentType) {
435
+ const base = (contentType || '').split(';')[0].trim().toLowerCase();
436
+ const map = {
437
+ 'text/html': 'html',
438
+ 'text/plain': 'txt',
439
+ 'text/css': 'css',
440
+ 'text/csv': 'csv',
441
+ 'text/xml': 'xml',
442
+ 'application/javascript': 'js',
443
+ 'application/x-javascript': 'js',
444
+ 'text/javascript': 'js',
445
+ 'application/json': 'json',
446
+ 'application/xml': 'xml',
447
+ 'image/jpeg': 'jpg',
448
+ 'image/png': 'png',
449
+ 'image/gif': 'gif',
450
+ 'image/svg+xml': 'svg',
451
+ 'image/webp': 'webp',
452
+ 'image/bmp': 'bmp',
453
+ 'image/x-icon': 'ico',
454
+ 'image/ico': 'ico',
455
+ 'application/pdf': 'pdf',
456
+ 'application/zip': 'zip',
457
+ 'application/x-zip-compressed': 'zip',
458
+ 'application/gzip': 'gz',
459
+ 'application/x-gzip': 'gz',
460
+ 'application/octet-stream': 'bin',
461
+ };
462
+ return map[base] || 'bin';
463
+ }
464
+
465
+ // Validate and decode a hex string into a Buffer; returns null on failure.
466
+ function hexToBuffer(hex) {
467
+ if (typeof hex !== 'string') return null;
468
+ const normalized = hex.replace(/\s+/g, '');
469
+ if (normalized.length === 0 || normalized.length % 2 !== 0) return null;
470
+ if (!/^[\da-fA-F]+$/.test(normalized)) return null;
471
+ return Buffer.from(normalized, 'hex');
472
+ }
473
+
474
+ ipcMain.handle('save-http-body', async (_event, bodyHex, contentType) => {
475
+ const buf = hexToBuffer(bodyHex);
476
+ if (!buf) return { success: false, error: 'Invalid HTTP body data' };
477
+
478
+ const ext = extFromContentType(contentType);
479
+ const { canceled, filePath } = await dialog.showSaveDialog({
480
+ title: 'Save HTTP Body',
481
+ defaultPath: path.join(app.getPath('documents'), `http-body.${ext}`),
482
+ filters: [
483
+ { name: 'HTTP Body', extensions: [ext] },
484
+ { name: 'All Files', extensions: ['*'] },
485
+ ],
486
+ });
487
+ if (canceled || !filePath) return { success: false, canceled: true };
488
+
489
+ try {
490
+ await fs.promises.writeFile(filePath, buf);
491
+ return { success: true };
492
+ } catch (err) {
493
+ console.error('HTTP body save error:', err);
494
+ return { success: false, error: err.message };
495
+ }
496
+ });
497
+
498
+ ipcMain.handle('preview-http-body', async (_event, bodyHex, contentType) => {
499
+ const buf = hexToBuffer(bodyHex);
500
+ if (!buf) return { success: false, error: 'Invalid HTTP body data' };
501
+
502
+ const ext = extFromContentType(contentType);
503
+ try {
504
+ // Use a unique temp directory per preview to avoid races and data leaks.
505
+ const tmpDir = await fs.promises.mkdtemp(
506
+ path.join(os.tmpdir(), 'ps-preview-'),
507
+ );
508
+ const tmpFile = path.join(tmpDir, `http-preview.${ext}`);
509
+ await fs.promises.writeFile(tmpFile, buf);
510
+ const fileUrl = pathToFileURL(tmpFile).href;
511
+ await shell.openExternal(fileUrl);
512
+ // Schedule cleanup after a delay to give the browser time to read the file.
513
+ setTimeout(() => {
514
+ fs.promises.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
515
+ }, 30000);
516
+ return { success: true };
517
+ } catch (err) {
518
+ console.error('HTTP body preview error:', err);
519
+ return { success: false, error: err.message };
520
+ }
521
+ });
522
+
523
+ ipcMain.handle('open-external-url', async (_event, rawUrl) => {
524
+ if (typeof rawUrl !== 'string' || !rawUrl.trim()) {
525
+ return { success: false, error: 'Invalid URL' };
526
+ }
527
+ try {
528
+ const parsed = new URL(rawUrl.trim());
529
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
530
+ return { success: false, error: 'Only HTTP/HTTPS URLs are supported' };
531
+ }
532
+ await shell.openExternal(parsed.href);
533
+ return { success: true };
534
+ } catch (err) {
535
+ return { success: false, error: err?.message || 'Invalid URL' };
536
+ }
537
+ });
538
+
539
+ ipcMain.handle('append-activity-log', async (_event, entry) => {
540
+ const normalizedEntry = normalizeActivityLogEntry(entry);
541
+ if (!normalizedEntry) {
542
+ return { success: false, error: 'Invalid log entry' };
543
+ }
544
+ // Renderer entries are already shown locally, so skip broadcasting them back.
545
+ appendActivityLogLine(normalizedEntry, { broadcast: false });
546
+ return { success: true, path: activityLogFilePath };
547
+ });
548
+
549
+ ipcMain.handle('get-activity-log-path', async () => {
550
+ return activityLogFilePath;
551
+ });
552
+
553
+ ipcMain.handle('get-activity-log-entries', async () => {
554
+ return [...activityLogEntries];
555
+ });
556
+
557
+ app.on('before-quit', () => {
558
+ if (!hasLoggedProgramShutdown) {
559
+ appendActivityLogLine(
560
+ timestampLifecycleMessage(
561
+ `Program shutdown requested for PacketSnitch v${app.getVersion()}`,
562
+ ),
563
+ { broadcast: false },
564
+ );
565
+ hasLoggedProgramShutdown = true;
566
+ }
567
+ // make sure the backend snitch process dies!
568
+ if (isBackendLoaded) {
569
+ killBackendProcess();
570
+ }
571
+ });
package/src/preload.js ADDED
@@ -0,0 +1,73 @@
1
+ const { contextBridge, ipcRenderer } = require('electron');
2
+
3
+ contextBridge.exposeInMainWorld('jsonapi', {
4
+ onJsonData: (callback) => {
5
+ ipcRenderer.on('json-data', (event, hostsJsonData) => {
6
+ callback(hostsJsonData);
7
+ });
8
+ },
9
+ });
10
+
11
+ contextBridge.exposeInMainWorld('snitchapi', {
12
+ runBackendCommand: (filename, useLLM) =>
13
+ ipcRenderer.invoke('run-backend-command', filename, useLLM),
14
+ });
15
+
16
+ contextBridge.exposeInMainWorld('getfileapi', {
17
+ selectFile: () => ipcRenderer.invoke('select-file'),
18
+ });
19
+
20
+ contextBridge.exposeInMainWorld('api', {
21
+ onError: (callback) => {
22
+ ipcRenderer.on('backend-error', (_event, message) => {
23
+ callback(message);
24
+ });
25
+ },
26
+ });
27
+
28
+ contextBridge.exposeInMainWorld('fsize', {
29
+ getFSize: () => ipcRenderer.invoke('file-size'), // Expose this method to renderer
30
+ });
31
+
32
+ contextBridge.exposeInMainWorld('saveapi', {
33
+ saveJson: (jsonData) => ipcRenderer.invoke('save-json', jsonData),
34
+ savePacket: (packetData) => ipcRenderer.invoke('save-packet', packetData),
35
+ savePayload: (payloadHex) => ipcRenderer.invoke('save-payload', payloadHex),
36
+ saveCookieJar: (cookieJarText) =>
37
+ ipcRenderer.invoke('save-cookie-jar', cookieJarText),
38
+ saveHttpBody: (bodyHex, contentType) =>
39
+ ipcRenderer.invoke('save-http-body', bodyHex, contentType),
40
+ saveNotes: (notesText) => ipcRenderer.invoke('save-notes', notesText),
41
+ });
42
+
43
+ contextBridge.exposeInMainWorld('previewapi', {
44
+ previewHttpBody: (bodyHex, contentType) =>
45
+ ipcRenderer.invoke('preview-http-body', bodyHex, contentType),
46
+ });
47
+
48
+ contextBridge.exposeInMainWorld('browserapi', {
49
+ openExternalUrl: (url) => ipcRenderer.invoke('open-external-url', url),
50
+ });
51
+
52
+ contextBridge.exposeInMainWorld('quitapi', {
53
+ quitApp: () => ipcRenderer.invoke('quit-app'),
54
+ promptSaveOnExit: () => ipcRenderer.invoke('prompt-save-session-on-exit'),
55
+ });
56
+
57
+ contextBridge.exposeInMainWorld('installapi', {
58
+ checkFirstRun: () => ipcRenderer.invoke('check-first-run'),
59
+ dismissFirstRun: () => ipcRenderer.invoke('dismiss-first-run'),
60
+ });
61
+
62
+ contextBridge.exposeInMainWorld('logapi', {
63
+ append: (entry) => ipcRenderer.invoke('append-activity-log', entry),
64
+ getPath: () => ipcRenderer.invoke('get-activity-log-path'),
65
+ getEntries: () => ipcRenderer.invoke('get-activity-log-entries'),
66
+ onEntry: (callback) => {
67
+ const listener = (_event, entry) => {
68
+ callback(entry);
69
+ };
70
+ ipcRenderer.on('activity-log-entry', listener);
71
+ return () => ipcRenderer.removeListener('activity-log-entry', listener);
72
+ },
73
+ });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * This file will automatically be loaded by webpack and run in the "renderer" context.
3
+ * To learn more about the differences between the "main" and the "renderer" context in
4
+ * Electron, visit:
5
+ *
6
+ * https://electronjs.org/docs/tutorial/process-model
7
+ *
8
+ * By default, Node.js integration in this file is disabled. When enabling Node.js integration
9
+ * in a renderer process, please be aware of potential security implications. You can read
10
+ * more about security risks here:
11
+ *
12
+ * https://electronjs.org/docs/tutorial/security
13
+ *
14
+ * To enable Node.js integration in this file, open up `main.js` and enable the `nodeIntegration`
15
+ * flag:
16
+ *
17
+ * ```
18
+ * // Create the browser window.
19
+ * mainWindow = new BrowserWindow({
20
+ * width: 800,
21
+ * height: 600,
22
+ * webPreferences: {
23
+ * nodeIntegration: true
24
+ * }
25
+ * });
26
+ * ```
27
+ */
28
+
29
+ import './assets/css/style.css';
30
+ import './front.js';
@@ -0,0 +1,13 @@
1
+ const PANEL_MODULES = [
2
+ "summary-panel",
3
+ "data-panel",
4
+ "stats-panel",
5
+ "list-panel",
6
+ "data-tools-panel",
7
+ "crypt-panel",
8
+ "keystore-panel",
9
+ ];
10
+
11
+ module.exports = {
12
+ PANEL_MODULES,
13
+ };