Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions src/core/featureFlags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// src/core/featureFlags.js

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Missing autoUpdates flag definition breaks the auto-update check. DEFAULT_FLAGS in src/core/featureFlags.js enumerates 12 flags but omits autoUpdates, which src/main/main.js relies on; since getFlag throws for any key absent from DEFAULT_FLAGS, the consumer call always fails.

  • src/core/featureFlags.js#L6-19: add autoUpdates: true (and confirm whether systemMonitoring, referenced in the PR description, also needs an entry) to DEFAULT_FLAGS.
  • src/main/main.js#L727-727: no change needed here once the flag is defined — this call site will start working correctly against featureFlags.getFlag(db, 'autoUpdates', true).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/featureFlags.js` at line 1, Update DEFAULT_FLAGS in featureFlags.js
to define autoUpdates with a default value of true, matching the key consumed by
featureFlags.getFlag in main.js. Also verify whether systemMonitoring is
referenced as a flag and add it to DEFAULT_FLAGS if required.

// Single source of truth for feature-flag defaults, typed keys, and
// get/set semantics. Falls back to defaults when a key is missing from
// the database, and rejects writes to unknown keys in debug builds.

const DEFAULT_FLAGS = Object.freeze({
realtimeProtection: true,
autoReports: true,
scanHistory: true,
externalLookups: true,
geoLookup: true,
networkPerimeterMap: true,
notificationsEnabled: true,
scanNotifications: true,
launchAtStartup: false,
folderWatch: true,
networkAlerts: true,
networkTrafficHistory: true,
});
Comment on lines +6 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm all featureFlags.getFlag/setFlag call sites use keys present in DEFAULT_FLAGS
rg -nP "featureFlags\.(getFlag|setFlag)\(" -A1
rg -nP "feature\.systemMonitoring"

Repository: chrisriv10/Soterios

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== featureFlags file ==\n'
if [ -f src/core/featureFlags.js ]; then
  wc -l src/core/featureFlags.js
  cat -n src/core/featureFlags.js | sed -n '1,220p'
else
  echo "src/core/featureFlags.js not found"
fi

printf '\n== search autoUpdates/systemMonitoring/featureFlags call sites ==\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  -e 'autoUpdates' -e 'systemMonitoring' -e 'featureFlags\.(getFlag|setFlag)\(' -e 'getFlag\(db, '\''autoUpdates'\''' -e 'feature\.autoUpdates' -e 'feature\.systemMonitoring' .

Repository: chrisriv10/Soterios

Length of output: 7563


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== relevant files ==\n'
git ls-files 'src/**' | rg 'featureFlags|main\.js|systemMonitoring|autoUpdates'

printf '\n== feature flag references ==\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  -e 'autoUpdates' -e 'systemMonitoring' -e 'featureFlags' src

Repository: chrisriv10/Soterios

Length of output: 5958


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '700,740p' src/main/main.js | nl -ba

Repository: chrisriv10/Soterios

Length of output: 197


Add autoUpdates to DEFAULT_FLAGSsrc/main/main.js:727 calls featureFlags.getFlag(db, 'autoUpdates', true), and getFlag() throws on unknown keys.

Suggested change
 const DEFAULT_FLAGS = Object.freeze({
   realtimeProtection: true,
   autoReports: true,
   scanHistory: true,
   externalLookups: true,
   geoLookup: true,
   networkPerimeterMap: true,
   notificationsEnabled: true,
   scanNotifications: true,
   launchAtStartup: false,
   folderWatch: true,
   networkAlerts: true,
   networkTrafficHistory: true,
+  autoUpdates: true,
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const DEFAULT_FLAGS = Object.freeze({
realtimeProtection: true,
autoReports: true,
scanHistory: true,
externalLookups: true,
geoLookup: true,
networkPerimeterMap: true,
notificationsEnabled: true,
scanNotifications: true,
launchAtStartup: false,
folderWatch: true,
networkAlerts: true,
networkTrafficHistory: true,
});
const DEFAULT_FLAGS = Object.freeze({
realtimeProtection: true,
autoReports: true,
scanHistory: true,
externalLookups: true,
geoLookup: true,
networkPerimeterMap: true,
notificationsEnabled: true,
scanNotifications: true,
launchAtStartup: false,
folderWatch: true,
networkAlerts: true,
networkTrafficHistory: true,
autoUpdates: true,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/featureFlags.js` around lines 6 - 19, Add the missing autoUpdates
key to the DEFAULT_FLAGS object in featureFlags.js, using the existing default
expected by main.js’s getFlag(db, 'autoUpdates', true) call so the key is
recognized without changing other feature flag defaults.


const FLAG_KEYS = Object.freeze(Object.keys(DEFAULT_FLAGS));

function isKnownFlag(key) {
return Object.prototype.hasOwnProperty.call(DEFAULT_FLAGS, key);
}

function getFlag(db, key, fallback) {
if (!isKnownFlag(key)) {
throw new Error(`Unknown feature flag: ${key}`);
}
const raw = db.getSetting(key, undefined);
if (raw === undefined || raw === null) {
return typeof fallback === 'undefined' ? DEFAULT_FLAGS[key] : fallback;
}
return Boolean(raw);
}

function setFlag(db, key, value) {
if (!isKnownFlag(key)) {
throw new Error(`Unknown feature flag: ${key}`);
}
const boolValue = Boolean(value);
db.setSetting(key, boolValue);
return boolValue;
}

function getDefaults() {
return { ...DEFAULT_FLAGS };
}

module.exports = {
DEFAULT_FLAGS,
FLAG_KEYS,
isKnownFlag,
getFlag,
setFlag,
getDefaults,
};
14 changes: 14 additions & 0 deletions src/core/scanProgress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// src/core/scanProgress.js
// Centralised progress normalisation used by the scan engine and IPC
// handlers. Guarantees finite integers in the 0-100 range so every
// caller does not have to re-implement the same guards.

function clampProgress(value) {
const n = Number(value);
if (!Number.isFinite(n)) return 0;
return Math.max(0, Math.min(100, Math.round(n)));
Comment on lines +6 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep progress normalization non-throwing.

Number(value) can throw for Symbol values and non-coercible objects, so malformed progress input can escape instead of normalizing to 0.

Proposed fix
 function clampProgress(value) {
-  const n = Number(value);
+  let n;
+  try {
+    n = Number(value);
+  } catch (_) {
+    return 0;
+  }
   if (!Number.isFinite(n)) return 0;
   return Math.max(0, Math.min(100, Math.round(n)));
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function clampProgress(value) {
const n = Number(value);
if (!Number.isFinite(n)) return 0;
return Math.max(0, Math.min(100, Math.round(n)));
function clampProgress(value) {
let n;
try {
n = Number(value);
} catch (_) {
return 0;
}
if (!Number.isFinite(n)) return 0;
return Math.max(0, Math.min(100, Math.round(n)));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/scanProgress.js` around lines 6 - 9, Update clampProgress so
conversion of value to a number is guarded against coercion errors, including
Symbol and non-coercible object inputs. When conversion throws, return 0;
preserve the existing finite check and 0–100 rounded clamping for successfully
converted values.

}

module.exports = {
clampProgress,
};
23 changes: 23 additions & 0 deletions src/main/ipc/_shared.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const https = require('https');

function requestText(url, options = {}) {
return new Promise((resolve, reject) => {
const req = https.request(url, {
method: 'GET',
headers: {
'User-Agent': 'Soterios',
...options.headers,
},
}, (res) => {
let body = '';
res.setEncoding('utf8');
res.on('data', chunk => { body += chunk; });
res.on('end', () => resolve({ statusCode: res.statusCode, body }));
});
req.on('error', reject);
req.setTimeout(15000, () => req.destroy(new Error('Request timed out')));
req.end();
});
}

module.exports = { requestText };
130 changes: 130 additions & 0 deletions src/main/ipc/firewall.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
const { ipcMain, dialog, BrowserWindow } = require('electron');
const path = require('path');
const fs = require('fs');
const { requestText } = require('../ipc/_shared');
const {
isPathInScanReportsDir,
} = require('../../security/reportExport');

const VALID_FIREWALL_PROFILES = ['Domain', 'Private', 'Public'];

function isValidFirewallProfile(name) {
return typeof name === 'string' && VALID_FIREWALL_PROFILES.includes(name);
}

function isValidIp(ip) {
const v4 = /^(\d{1,3}\.){3}\d{1,3}$/;
const v6 = /^[0-9a-fA-F:]+$/;
return v4.test(ip) || (v6.test(ip) && ip.includes(':'));
}

function register(mainWindow, { db, firewallManager }) {
ipcMain.handle('firewall:status', async () => {
return firewallManager.getStatus();
});

ipcMain.handle('firewall:rules', async () => {
return firewallManager.getRules();
});

ipcMain.handle('firewall:listRules', async () => {
return firewallManager.listRules();
});

ipcMain.handle('firewall:createRule', async (_event, spec) => {
return firewallManager.createRule(spec);
});

ipcMain.handle('firewall:deleteRule', async (_event, name) => {
return firewallManager.deleteRule(name);
});

ipcMain.handle('firewall:setRuleEnabled', async (_event, { name, enabled }) => {
return firewallManager.setRuleEnabled(name, enabled);
});
Comment on lines +34 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'FirewallManager' --exec ast-grep outline {} --items all
rg -nP '\b(createRule|deleteRule|setRuleEnabled)\s*\(' -C6 --iglob '*firewall*'

Repository: chrisriv10/Soterios

Length of output: 715


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- src/main/ipc/firewall.js ---'
cat -n src/main/ipc/firewall.js | sed -n '1,120p'

echo
echo '--- src/security/FirewallManager.js (outline) ---'
ast-grep outline src/security/FirewallManager.js --items all

echo
echo '--- src/security/FirewallManager.js (relevant methods) ---'
rg -n "class FirewallManager|createRule|deleteRule|setRuleEnabled|setProfileEnabled|friendlyFirewallError|_validateImportRule|isValidIp|psEscape" src/security/FirewallManager.js -n -C 6

Repository: chrisriv10/Soterios

Length of output: 14930


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,260p' src/security/FirewallManager.js | cat -n

Repository: chrisriv10/Soterios

Length of output: 13046


Guard firewall:setRuleEnabled before destructuring. null/undefined from the renderer will throw a raw TypeError here; use a payload fallback and let FirewallManager handle the rule-name check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/ipc/firewall.js` around lines 34 - 44, Update the
firewall:setRuleEnabled IPC handler to use a safe fallback before destructuring
its payload, so null or undefined input does not cause a TypeError. Preserve
passing the resulting name and enabled values to FirewallManager.setRuleEnabled,
allowing it to perform rule-name validation.

Source: Linters/SAST tools


ipcMain.handle('firewall:setProfileEnabled', async (_event, { profile, enabled }) => {
if (!isValidFirewallProfile(profile)) throw new Error(`Invalid firewall profile: ${profile}`);
return firewallManager.setProfileEnabled(profile, !!enabled);
});

ipcMain.handle('firewall:exportRules', async () => {
const data = await firewallManager.exportRules();
const result = await dialog.showSaveDialog(mainWindow || BrowserWindow.getFocusedWindow(), {
title: 'Export Soterios firewall rules',
defaultPath: 'soterios-firewall-rules.json',
filters: [{ name: 'JSON', extensions: ['json'] }],
});
if (result.canceled || !result.filePath) return { canceled: true };
await fs.promises.writeFile(result.filePath, JSON.stringify(data, null, 2), 'utf8');
return { success: true, path: result.filePath, count: data.rules.length };
});

ipcMain.handle('firewall:importRules', async (_event, options = {}) => {
const onConflict = ['skip', 'overwrite', 'rename'].includes(options && options.onConflict)
? options.onConflict
: 'skip';
const result = await dialog.showOpenDialog(mainWindow || BrowserWindow.getFocusedWindow(), {
title: 'Import Soterios firewall rules',
properties: ['openFile'],
filters: [{ name: 'JSON', extensions: ['json'] }],
});
if (result.canceled || !result.filePaths.length) return { canceled: true };
const filePath = result.filePaths[0];
const stat = await fs.promises.stat(filePath);
const MAX_IMPORT_BYTES = 2 * 1024 * 1024;
if (stat.size > MAX_IMPORT_BYTES) {
throw new Error('Import file is too large (limit 2 MB).');
}
let payload;
try {
const raw = await fs.promises.readFile(filePath, 'utf8');
payload = JSON.parse(raw);
} catch (e) {
throw new Error('Could not parse import file as JSON.');
}
const summary = await firewallManager.importRules(payload, { onConflict });
return { ...summary, path: filePath };
});

const TRUSTED_IPS_KEY = 'firewall.trustedIps';

ipcMain.handle('firewall:getTrusted', () => {
return db.getSetting(TRUSTED_IPS_KEY, []);
});

ipcMain.handle('firewall:trustConnection', (_event, ip) => {
if (!ip || !isValidIp(ip)) throw new Error('Invalid address.');
const current = db.getSetting(TRUSTED_IPS_KEY, []);
if (!current.includes(ip)) current.push(ip);
db.setSetting(TRUSTED_IPS_KEY, current);
return current;
});
Comment on lines +96 to +102

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Normalize the stored value to an array.

untrustConnection (Line 105) defends with || [], but trustConnection calls .includes/.push directly on whatever getSetting returns; a corrupted/non-array persisted value throws here. Also worth deduplicating and normalizing case for IPv6.

🛡️ Proposed fix
-    const current = db.getSetting(TRUSTED_IPS_KEY, []);
+    const stored = db.getSetting(TRUSTED_IPS_KEY, []);
+    const current = Array.isArray(stored) ? [...stored] : [];
     if (!current.includes(ip)) current.push(ip);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ipcMain.handle('firewall:trustConnection', (_event, ip) => {
if (!ip || !isValidIp(ip)) throw new Error('Invalid address.');
const current = db.getSetting(TRUSTED_IPS_KEY, []);
if (!current.includes(ip)) current.push(ip);
db.setSetting(TRUSTED_IPS_KEY, current);
return current;
});
ipcMain.handle('firewall:trustConnection', (_event, ip) => {
if (!ip || !isValidIp(ip)) throw new Error('Invalid address.');
const stored = db.getSetting(TRUSTED_IPS_KEY, []);
const current = Array.isArray(stored) ? [...stored] : [];
if (!current.includes(ip)) current.push(ip);
db.setSetting(TRUSTED_IPS_KEY, current);
return current;
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/ipc/firewall.js` around lines 96 - 102, Update the trustConnection
handler to normalize the value returned by db.getSetting(TRUSTED_IPS_KEY, []) to
an array before calling includes or push, falling back to an empty array for
corrupted persisted values. Deduplicate entries using normalized IP comparison,
including case-insensitive handling for IPv6, while preserving the returned and
stored trusted-address list behavior.


ipcMain.handle('firewall:untrustConnection', (_event, ip) => {
const current = (db.getSetting(TRUSTED_IPS_KEY, []) || []).filter((x) => x !== ip);
db.setSetting(TRUSTED_IPS_KEY, current);
return current;
});

// -- WHOIS lookup (no API key required) --
ipcMain.handle('network:whois', async (_event, ip) => {
if (!ip || !isValidIp(ip)) throw new Error('Invalid address.');
const res = await requestText(`https://ipwho.is/${encodeURIComponent(ip)}`);
if (res.statusCode !== 200) throw new Error(`WHOIS lookup failed (${res.statusCode}).`);
const data = JSON.parse(res.body || '{}');
if (data.success === false) return { found: false };
Comment on lines +111 to +116

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle non-JSON responses from ipwho.is.

A 200 response with an HTML error/captcha body makes JSON.parse throw a raw SyntaxError across the IPC boundary. Wrap the parse and return a clean failure.

🛡️ Proposed fix
-    const data = JSON.parse(res.body || '{}');
+    let data;
+    try {
+      data = JSON.parse(res.body || '{}');
+    } catch {
+      throw new Error('WHOIS lookup returned an unreadable response.');
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ipcMain.handle('network:whois', async (_event, ip) => {
if (!ip || !isValidIp(ip)) throw new Error('Invalid address.');
const res = await requestText(`https://ipwho.is/${encodeURIComponent(ip)}`);
if (res.statusCode !== 200) throw new Error(`WHOIS lookup failed (${res.statusCode}).`);
const data = JSON.parse(res.body || '{}');
if (data.success === false) return { found: false };
ipcMain.handle('network:whois', async (_event, ip) => {
if (!ip || !isValidIp(ip)) throw new Error('Invalid address.');
const res = await requestText(`https://ipwho.is/${encodeURIComponent(ip)}`);
if (res.statusCode !== 200) throw new Error(`WHOIS lookup failed (${res.statusCode}).`);
let data;
try {
data = JSON.parse(res.body || '{}');
} catch {
throw new Error('WHOIS lookup returned an unreadable response.');
}
if (data.success === false) return { found: false };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/ipc/firewall.js` around lines 111 - 116, Update the network:whois
handler to safely parse the response body from ipwho.is: wrap JSON.parse in a
try/catch and return the existing clean failure result when parsing fails,
rather than allowing a SyntaxError to cross the IPC boundary. Preserve the
current handling for valid JSON and non-200 responses.

return {
found: true,
ip: data.ip,
country: data.country,
region: data.region,
city: data.city,
org: (data.connection && data.connection.org) || data.org || null,
isp: (data.connection && data.connection.isp) || null,
asn: (data.connection && data.connection.asn) || null,
};
});
}

module.exports = { register };
Loading
Loading