From ce0cb8341bd3a8bb7dff70f24d25ce95abfb124b Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:39:27 -0500 Subject: [PATCH 01/24] Add browser extension native messaging: credential leak notifications - Add IPC handler 'credential-leak:notify' for browser extension - Add second-instance protocol handler (soterios://) - Scaffold browser-extension/ with manifest v3, background, content, popup, options - Native messaging host (native-host.js) bridges extension <-> desktop app - Install script tools/install-native-host.js --- browser-extension-host.js | 94 +++++++++++++ browser-extension-host.json | 9 ++ browser-extension/background.js | 3 + browser-extension/content.js | 142 ++++++++++++++++++++ browser-extension/icons/icon.svg | 11 ++ browser-extension/icons/icon128.png | Bin 0 -> 6647 bytes browser-extension/icons/icon16.png | Bin 0 -> 539 bytes browser-extension/icons/icon32.png | Bin 0 -> 1302 bytes browser-extension/icons/icon48.png | Bin 0 -> 2136 bytes browser-extension/manifest.json | 22 +++ browser-extension/native-host-manifest.json | 9 ++ browser-extension/native-host.bat | 6 + browser-extension/native-host.js | 113 ++++++++++++++++ browser-extension/options.html | 75 +++++++++++ browser-extension/options.js | 50 +++++++ browser-extension/package.json | 14 ++ browser-extension/popup.html | 63 +++++++++ browser-extension/popup.js | 85 ++++++++++++ src/main/ipcHandlers.js | 17 +++ src/main/main.js | 20 +++ tools/build-icons.js | 22 +++ tools/install-native-host.js | 61 +++++++++ 22 files changed, 816 insertions(+) create mode 100644 browser-extension-host.js create mode 100644 browser-extension-host.json create mode 100644 browser-extension/background.js create mode 100644 browser-extension/content.js create mode 100644 browser-extension/icons/icon.svg create mode 100644 browser-extension/icons/icon128.png create mode 100644 browser-extension/icons/icon16.png create mode 100644 browser-extension/icons/icon32.png create mode 100644 browser-extension/icons/icon48.png create mode 100644 browser-extension/manifest.json create mode 100644 browser-extension/native-host-manifest.json create mode 100644 browser-extension/native-host.bat create mode 100644 browser-extension/native-host.js create mode 100644 browser-extension/options.html create mode 100644 browser-extension/options.js create mode 100644 browser-extension/package.json create mode 100644 browser-extension/popup.html create mode 100644 browser-extension/popup.js create mode 100644 tools/build-icons.js create mode 100644 tools/install-native-host.js diff --git a/browser-extension-host.js b/browser-extension-host.js new file mode 100644 index 0000000..d849bfd --- /dev/null +++ b/browser-extension-host.js @@ -0,0 +1,94 @@ +#!/usr/bin/env node +/** + * Soterios Native Messaging Host + * Receives messages from browser extension and forwards to desktop app + */ + +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +function readMessage() { + return new Promise((resolve, reject) => { + const lenBuf = Buffer.alloc(4); + let read = 0; + process.stdin.on('readable', () => { + const chunk = process.stdin.read(4 - read); + if (chunk) { + chunk.copy(lenBuf, read); + read += chunk.length; + if (read === 4) { + const len = lenBuf.readUInt32LE(0); + const msgBuf = Buffer.alloc(len); + let msgRead = 0; + process.stdin.on('readable', () => { + const chunk = process.stdin.read(len - msgRead); + if (chunk) { + chunk.copy(msgBuf, msgRead); + msgRead += chunk.length; + if (msgRead === len) { + resolve(JSON.parse(msgBuf.toString('utf8'))); + } + } + }); + } + } + }); + process.stdin.on('error', reject); + }); +} + +function sendMessage(msg) { + const buf = Buffer.from(JSON.stringify(msg), 'utf8'); + const lenBuf = Buffer.alloc(4); + lenBuf.writeUInt32LE(buf.length, 0); + process.stdout.write(lenBuf); + process.stdout.write(buf); +} + +async function connectToDesktopApp() { + const pipeName = '\\\\.\\pipe\\soterios-credential-safety'; + return new Promise((resolve, reject) => { + const client = require('net').createConnection(pipeName, () => { + resolve(client); + }); + client.on('error', reject); + }); +} + +let desktopClient = null; + +async function main() { + console.error('[Soterios Host] Starting...'); + + try { + desktopClient = await connectToDesktopApp(); + console.error('[Soterios Host] Connected to desktop app'); + } catch (e) { + console.error('[Soterios Host] Desktop app not running:', e.message); + } + + while (true) { + try { + const msg = await readMessage(); + console.error('[Soterios Host] Received:', msg.type); + + if (msg.type === 'CREDENTIAL_LEAK') { + if (desktopClient) { + desktopClient.write(JSON.stringify({ type: 'CREDENTIAL_LEAK', ...msg.payload }) + '\n'); + } + sendMessage({ ok: true }); + } else if (msg.type === 'PING') { + sendMessage({ pong: true }); + } + } catch (e) { + if (e.message.includes('Unexpected end of JSON')) break; + console.error('[Soterios Host] Error:', e.message); + } + } +} + +main().catch(e => { + console.error('[Soterios Host] Fatal:', e); + process.exit(1); +}); \ No newline at end of file diff --git a/browser-extension-host.json b/browser-extension-host.json new file mode 100644 index 0000000..423867b --- /dev/null +++ b/browser-extension-host.json @@ -0,0 +1,9 @@ +{ + "name": "com.soterios.credential_safety", + "description": "Soterios Credential Safety Native Messaging Host", + "path": "browser-extension-host.exe", + "type": "stdio", + "allowed_origins": [ + "chrome-extension://YOUR_EXTENSION_ID_HERE/" + ] +} \ No newline at end of file diff --git a/browser-extension/background.js b/browser-extension/background.js new file mode 100644 index 0000000..d33f01c --- /dev/null +++ b/browser-extension/background.js @@ -0,0 +1,3 @@ +chrome.runtime.onInstalled.addListener(() => { + chrome.storage.sync.set({ externalLookupsEnabled: true }); +}); \ No newline at end of file diff --git a/browser-extension/content.js b/browser-extension/content.js new file mode 100644 index 0000000..ac9b461 --- /dev/null +++ b/browser-extension/content.js @@ -0,0 +1,142 @@ +/** + * Soterios Browser Extension - Content Script + * Detects password fields, monitors for credential entry, and shows breach indicators + */ + +let soteriosIcon = null; +let passwordFields = new WeakMap(); +let observer = null; + +function createIcon() { + const icon = document.createElement('img'); + icon.src = chrome.runtime.getURL('icons/icon16.png'); + icon.style.cssText = ` + position: absolute; + width: 16px; height: 16px; + cursor: pointer; + opacity: 0.7; + transition: opacity 0.2s; + z-index: 2147483647; + pointer-events: auto; + `; + icon.title = 'Check password with Soterios'; + icon.addEventListener('mouseenter', () => icon.style.opacity = '1'); + icon.addEventListener('mouseleave', () => icon.style.opacity = '0.7'); + icon.addEventListener('click', onIconClick); + return icon; +} + +function positionIcon(icon, input) { + const rect = input.getBoundingClientRect(); + icon.style.top = `${rect.top + window.scrollY + (rect.height - 16) / 2}px`; + icon.style.left = `${rect.right + window.scrollX - 20}px`; +} + +async function onIconClick(e) { + const input = e.target.dataset.forInput; + const el = document.querySelector(`[data-soterios-id="${input}"]`); + if (!el) return; + + const password = el.value; + if (!password) return; + + try { + const result = await chrome.runtime.sendMessage({ type: 'CHECK_PASSWORD', password }); + showResult(el, result); + } catch (err) { + console.error('[Soterios] Check failed:', err); + } +} + +function showResult(input, result) { + removeResult(input); + + const badge = document.createElement('span'); + badge.dataset.soteriosBadge = input.dataset.soteriosId; + badge.style.cssText = ` + position: absolute; + top: -20px; right: -20px; + padding: 2px 6px; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + color: white; + z-index: 2147483647; + background: ${result.pwned ? '#dc3545' : '#28a745'}; + box-shadow: 0 1px 3px rgba(0,0,0,0.3); + `; + badge.textContent = result.pwned ? `Pwned ${result.count}x` : 'Safe'; + badge.title = result.pwned + ? `Found in ${result.count} breach${result.count !== 1 ? 'es' : ''}. Change immediately.` + : 'Not found in known breaches (HIBP)'; + input.parentElement.style.position = 'relative'; + input.parentElement.appendChild(badge); + + setTimeout(() => removeResult(input), 5000); +} + +function removeResult(input) { + const badge = document.querySelector(`[data-soterios-badge="${input.dataset.soteriosId}"]`); + if (badge) badge.remove(); +} + +function addIconToField(input) { + if (input.dataset.soteriosId) return; + + const id = `soterios-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; + input.dataset.soteriosId = id; + + const icon = createIcon(); + icon.dataset.forInput = id; + document.body.appendChild(icon); + positionIcon(icon, input); + + const updatePos = () => positionIcon(icon, input); + window.addEventListener('scroll', updatePos, true); + window.addEventListener('resize', updatePos); + input.addEventListener('blur', () => setTimeout(() => icon.remove(), 200), { once: true }); + + passwordFields.set(input, icon); +} + +function scanForPasswordFields() { + const inputs = document.querySelectorAll('input[type="password"]:not([data-soterios-id])'); + inputs.forEach(addIconToField); +} + +function init() { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init, { once: true }); + return; + } + + scanForPasswordFields(); + + observer = new MutationObserver(mutations => { + for (const m of mutations) { + m.addedNodes.forEach(node => { + if (node.nodeType === 1) { + if (node.matches('input[type="password"]')) addIconToField(node); + node.querySelectorAll('input[type="password"]').forEach(addIconToField); + } + }); + } + }); + + observer.observe(document.body, { childList: true, subtree: true }); +} + +if (typeof window !== 'undefined') { + init(); +} + +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + if (msg.type === 'SETTINGS_UPDATED') { + if (!msg.settings.showIcon) { + passwordFields.forEach((icon, input) => icon.remove()); + passwordFields.clear(); + } else { + scanForPasswordFields(); + } + } +}); \ No newline at end of file diff --git a/browser-extension/icons/icon.svg b/browser-extension/icons/icon.svg new file mode 100644 index 0000000..0aef8b8 --- /dev/null +++ b/browser-extension/icons/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/browser-extension/icons/icon128.png b/browser-extension/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..691ef14527b37f29ca51907b4109c4bdc65fdf42 GIT binary patch literal 6647 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 literal 0 HcmV?d00001 diff --git a/browser-extension/icons/icon16.png b/browser-extension/icons/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..39c96db33f4f7870c8ee55633638c531dd63582e GIT binary patch literal 539 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/" + ] +} \ No newline at end of file diff --git a/browser-extension/native-host.bat b/browser-extension/native-host.bat new file mode 100644 index 0000000..fb757d6 --- /dev/null +++ b/browser-extension/native-host.bat @@ -0,0 +1,6 @@ +@echo off +REM Soterios Native Messaging Host +REM This batch file launches the Node.js native host that communicates with the desktop app + +set NODE_PATH=%~dp0..\..\node_modules +node "%~dp0native-host.js" %* \ No newline at end of file diff --git a/browser-extension/native-host.js b/browser-extension/native-host.js new file mode 100644 index 0000000..7b93fca --- /dev/null +++ b/browser-extension/native-host.js @@ -0,0 +1,113 @@ +#!/usr/bin/env node +/** + * Soterios Native Messaging Host + * Bridges browser extension <-> desktop Electron app via stdin/stdout JSON messages + */ + +const { spawn } = require('child_process'); +const readline = require('readline'); + +const DESKTOP_APP = process.env.SOTERIOS_APP_PATH || 'soterios://'; + +function log(...args) { + console.error('[Soterios Native Host]', new Date().toISOString(), ...args); +} + +function send(msg) { + const json = JSON.stringify(msg); + const len = Buffer.byteLength(json); + const buf = Buffer.alloc(4 + len); + buf.writeUInt32LE(len, 0); + buf.write(json, 4); + process.stdout.write(buf); +} + +function readMessages() { + const rl = readline.createInterface({ + input: process.stdin, + terminal: false + }); + + let buffer = Buffer.alloc(0); + + process.stdin.on('data', chunk => { + buffer = Buffer.concat([buffer, chunk]); + + while (buffer.length >= 4) { + const len = buffer.readUInt32LE(0); + if (buffer.length < 4 + len) break; + + const json = buffer.subarray(4, 4 + len).toString(); + buffer = buffer.subarray(4 + len); + + try { + const msg = JSON.parse(json); + handleMessage(msg); + } catch (e) { + log('Parse error:', e.message); + } + } + }); +} + +let desktopProc = null; +const pending = new Map(); +let msgId = 0; + +function launchDesktopApp() { + if (desktopProc) return Promise.resolve(); + + return new Promise((resolve, reject) => { + const url = process.platform === 'win32' + ? `cmd /c start "" "${DESKTOP_APP}"` + : process.platform === 'darwin' + ? `open -a "Soterios"` + : `xdg-open "${DESKTOP_APP}"`; + + desktopProc = spawn(url, { shell: true, detached: true }); + desktopProc.unref(); + + desktopProc.on('error', e => { + log('Desktop app launch error:', e.message); + desktopProc = null; + }); + + setTimeout(resolve, 1500); + }); +} + +async function handleMessage(msg) { + log('Received:', msg.type); + + switch (msg.type) { + case 'CREDENTIAL_LEAK': { + await launchDesktopApp(); + send({ type: 'LEAK_NOTIFIED', ok: true, original: msg }); + break; + } + case 'PING': { + send({ type: 'PONG', ok: true }); + break; + } + case 'OPEN_APP': { + await launchDesktopApp(); + send({ type: 'APP_OPENED', ok: true }); + break; + } + default: { + send({ type: 'ERROR', error: 'Unknown message type', original: msg }); + } + } +} + +process.on('uncaughtException', e => { + log('Uncaught:', e); + send({ type: 'ERROR', error: e.message }); +}); + +process.on('unhandledRejection', e => { + log('Unhandled rejection:', e); +}); + +log('Starting native messaging host'); +readMessages(); \ No newline at end of file diff --git a/browser-extension/options.html b/browser-extension/options.html new file mode 100644 index 0000000..3c53605 --- /dev/null +++ b/browser-extension/options.html @@ -0,0 +1,75 @@ + + + + + Soterios Options + + + +

Soterios Credential Safety

+

Configure breach monitoring and desktop integration

+ +
+

Breach Monitoring

+
+
+
Check passwords against Have I Been Pwned
+
Uses k-anonymity (only first 5 hash chars sent). Never sends full password.
+
+ +
+
+
+
Auto-check on password fields
+
Show breach indicator automatically when you enter a password
+
+ +
+
+
+
Show Soterios icon in password fields
+
Click to manually check any password
+
+ +
+
+ +
+

Desktop App Integration

+
+
+
Notify Soterios desktop app of leaks
+
Sends breach alerts to the desktop app via native messaging
+
+ +
+
+ +
Settings saved
+ +
+ Note: Desktop integration requires the Soterios native messaging host installed. The installer sets this up automatically. If desktop notifications don't work, reinstall Soterios or run node tools/install-native-host.js as admin. +
+ + + + \ No newline at end of file diff --git a/browser-extension/options.js b/browser-extension/options.js new file mode 100644 index 0000000..562e5bc --- /dev/null +++ b/browser-extension/options.js @@ -0,0 +1,50 @@ +const DEFAULTS = { + hibpEnabled: true, + autoCheck: true, + showIcon: true, + notifyDesktop: true +}; + +function loadSettings() { + chrome.storage.sync.get(DEFAULTS, settings => { + Object.keys(DEFAULTS).forEach(key => { + const el = document.getElementById(key); + if (el) el.setAttribute('aria-checked', settings[key]); + }); + }); +} + +function saveSettings() { + const settings = {}; + Object.keys(DEFAULTS).forEach(key => { + const el = document.getElementById(key); + if (el) settings[key] = el.getAttribute('aria-checked') === 'true'; + }); + chrome.storage.sync.set(settings, () => { + const msg = document.getElementById('savedMsg'); + msg.classList.add('show'); + setTimeout(() => msg.classList.remove('show'), 1500); + chrome.runtime.sendMessage({ type: 'SETTINGS_UPDATED', settings }); + }); +} + +function setupToggles() { + document.querySelectorAll('.toggle').forEach(btn => { + btn.addEventListener('click', () => { + const checked = btn.getAttribute('aria-checked') === 'true'; + btn.setAttribute('aria-checked', !checked); + saveSettings(); + }); + btn.addEventListener('keydown', e => { + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault(); + btn.click(); + } + }); + }); +} + +document.addEventListener('DOMContentLoaded', () => { + loadSettings(); + setupToggles(); +}); \ No newline at end of file diff --git a/browser-extension/package.json b/browser-extension/package.json new file mode 100644 index 0000000..5466349 --- /dev/null +++ b/browser-extension/package.json @@ -0,0 +1,14 @@ +{ + "name": "soterios-browser-extension", + "version": "1.0.0", + "description": "Soterios Credential Safety Browser Extension", + "private": true, + "scripts": { + "build:icons": "node tools/build-icons.js", + "package": "npm run build:icons && cd browser-extension && zip -r ../soterios-extension.zip . -x '*.DS_Store' 'icons/*.svg' 'tools/*'", + "install:host": "node tools/install-native-host.js" + }, + "devDependencies": { + "svgexport": "^0.4.2" + } +} \ No newline at end of file diff --git a/browser-extension/popup.html b/browser-extension/popup.html new file mode 100644 index 0000000..1ea5687 --- /dev/null +++ b/browser-extension/popup.html @@ -0,0 +1,63 @@ + + + + + + + +
+ +

Soterios Credential Safety

+
+ +
+
Check a Password
+
+ + +
+
+
+ +
+
Desktop App
+
+ + Checking connection... +
+
+ + Settings + + + + \ No newline at end of file diff --git a/browser-extension/popup.js b/browser-extension/popup.js new file mode 100644 index 0000000..8400b6f --- /dev/null +++ b/browser-extension/popup.js @@ -0,0 +1,85 @@ +const HIBP_API = 'https://api.pwnedpasswords.com/range/'; + +async function sha1(str) { + const buf = new TextEncoder().encode(str); + const hash = await crypto.subtle.digest('SHA-1', buf); + return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase(); +} + +async function checkPwned(password) { + const hash = await sha1(password); + const prefix = hash.slice(0, 5); + const suffix = hash.slice(5); + const resp = await fetch(`${HIBP_API}${prefix}`); + const text = await resp.text(); + const lines = text.trim().split('\n'); + for (const line of lines) { + const [suf, count] = line.split(':'); + if (suf === suffix) return parseInt(count, 10); + } + return 0; +} + +function showResult(count) { + const result = document.getElementById('result'); + if (count === 0) { + result.className = 'result safe'; + result.innerHTML = ` +
✓ Not found in breaches
+
This password was not found in the HIBP database (${count} occurrences).
+ `; + } else { + result.className = 'result pwned'; + result.innerHTML = ` +
⚠ Found in ${count} breach${count > 1 ? 'es' : ''}
+
This password appears in known data breaches. Do not use it. Generate a new one in the Soterios app.
+ `; + } + result.style.display = 'block'; +} + +async function checkConnection() { + try { + const resp = await fetch('http://localhost:17234/api/health', { method: 'GET', timeout: 1000 }); + if (resp.ok) { + document.getElementById('statusDot').classList.remove('offline'); + document.getElementById('statusText').textContent = 'Soterios app connected'; + } else throw new Error(); + } catch { + document.getElementById('statusDot').classList.add('offline'); + document.getElementById('statusText').textContent = 'Soterios app not running'; + } +} + +document.getElementById('checkBtn').addEventListener('click', async () => { + const input = document.getElementById('passwordInput'); + const btn = document.getElementById('checkBtn'); + const loader = document.getElementById('loader'); + const pwd = input.value; + + if (!pwd) return; + + btn.disabled = true; + loader.classList.add('active'); + document.getElementById('result').style.display = 'none'; + + try { + const count = await checkPwned(pwd); + showResult(count); + } catch (e) { + document.getElementById('result').className = 'result pwned'; + document.getElementById('result').innerHTML = '
Error
Could not check password.
'; + document.getElementById('result').style.display = 'block'; + } finally { + btn.disabled = false; + loader.classList.remove('active'); + } +}); + +document.getElementById('openOptions').addEventListener('click', (e) => { + e.preventDefault(); + chrome.runtime.openOptionsPage(); +}); + +checkConnection(); +setInterval(checkConnection, 30000); \ No newline at end of file diff --git a/src/main/ipcHandlers.js b/src/main/ipcHandlers.js index 68cbfec..fb5e891 100644 --- a/src/main/ipcHandlers.js +++ b/src/main/ipcHandlers.js @@ -766,6 +766,23 @@ function registerIpcHandlers(mainWindow, services) { return { found: count > 0, count }; }); + ipcMain.handle('credential-leak:notify', async (_event, payload) => { + if (!payload?.password) return { ok: false, error: 'Missing password' }; + const sha = crypto.createHash('sha1').update(payload.password).digest('hex').toUpperCase(); + const alert = { + level: 'danger', + source: 'Browser Extension', + title: 'Credential Leak Detected', + message: `Password found in ${payload.count} breach${payload.count > 1 ? 'es' : ''} via browser extension`, + detail: `SHA-1 prefix: ${sha.slice(0, 5)}... | Breaches: ${payload.count}`, + timestamp: new Date().toISOString(), + metadata: { source: 'browser-extension', hashPrefix: sha.slice(0, 5), count: payload.count } + }; + db.addAlert(alert); + if (services.eventBus) services.eventBus.emit('alert:new', alert); + return { ok: true }; + }); + ipcMain.handle('xon:email', async (_event, email) => { if (!email) return { found: false, breaches: [] }; if (!db.getSetting('feature.externalLookups', true)) throw new Error('External lookups are disabled in Settings.'); diff --git a/src/main/main.js b/src/main/main.js index 35f7826..b425bd8 100644 --- a/src/main/main.js +++ b/src/main/main.js @@ -580,7 +580,27 @@ function buildAppMenu() { app.setAppUserModelId('com.soterios.app'); +const gotTheLock = app.requestSingleInstanceLock(); +if (!gotTheLock) { + app.quit(); + process.exit(0); +} + +app.on('second-instance', (_event, commandLine) => { + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + const url = commandLine.find(arg => arg.startsWith('soterios://')); + if (url) mainWindow.webContents.send('protocol-url', url); + } +}); + app.whenReady().then(async () => { + // Register custom protocol for browser extension communication + if (process.platform === 'win32') { + app.setAsDefaultProtocolClient('soterios'); + } + const dbPath = path.join(app.getPath('userData'), 'soterios.db'); // File logging is opt-in via SOTERIOS_LOG_FILE (path or "1" for the default log file). const logConfig = { level: process.env.SOTERIOS_LOG_LEVEL || 'info' }; diff --git a/tools/build-icons.js b/tools/build-icons.js new file mode 100644 index 0000000..35cea89 --- /dev/null +++ b/tools/build-icons.js @@ -0,0 +1,22 @@ +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const sizes = [16, 32, 48, 128]; +const svgPath = path.join(__dirname, '../browser-extension/icons/icon.svg'); +const iconsDir = path.join(__dirname, '../browser-extension/icons'); + +if (!fs.existsSync(svgPath)) { + console.error('icon.svg not found'); + process.exit(1); +} + +for (const size of sizes) { + const outPath = path.join(iconsDir, `icon${size}.png`); + try { + execSync(`npx -y svgexport "${svgPath}" "${outPath}" ${size}:${size}`, { stdio: 'inherit' }); + console.log(`Generated ${outPath}`); + } catch (e) { + console.error(`Failed to generate ${size}px icon:`, e.message); + } +} \ No newline at end of file diff --git a/tools/install-native-host.js b/tools/install-native-host.js new file mode 100644 index 0000000..d55afee --- /dev/null +++ b/tools/install-native-host.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node +/** + * Install Soterios Native Messaging Host + * Run as Administrator on Windows + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const EXTENSION_ID = process.env.EXTENSION_ID || 'YOUR_EXTENSION_ID_HERE'; +const IS_WIN = process.platform === 'win32'; + +function main() { + const extDir = path.resolve(__dirname, '..', 'browser-extension'); + const manifestPath = path.join(extDir, 'native-host-manifest.json'); + const batPath = path.join(extDir, 'native-host.bat'); + const jsPath = path.join(extDir, 'native-host.js'); + + if (!fs.existsSync(manifestPath) || !fs.existsSync(batPath) || !fs.existsSync(jsPath)) { + console.error('Extension files not found. Run from project root.'); + process.exit(1); + } + + let manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + manifest.allowed_origins = [manifest.allowed_origins[0].replace('', EXTENSION_ID)]; + + if (IS_WIN) { + const regPath = `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${manifest.name}`; + const regCmd = `reg add "${regPath}" /ve /t REG_SZ /d "${manifestPath.replace(/\\/g, '\\\\')}" /f`; + try { + execSync(regCmd, { stdio: 'inherit' }); + console.log('Registered native host for Chrome (Current User)'); + } catch (e) { + console.error('Failed to register (run as Administrator):', e.message); + process.exit(1); + } + + const regPathEdge = `HKCU\\Software\\Microsoft\\Edge\\NativeMessagingHosts\\${manifest.name}`; + const regCmdEdge = `reg add "${regPathEdge}" /ve /t REG_SZ /d "${manifestPath.replace(/\\/g, '\\\\')}" /f`; + try { + execSync(regCmdEdge, { stdio: 'inherit' }); + console.log('Registered native host for Edge (Current User)'); + } catch (e) { + console.warn('Edge registration failed:', e.message); + } + } else { + const dir = process.platform === 'darwin' + ? path.join(process.env.HOME, 'Library', 'Application Support', 'Google', 'Chrome', 'NativeMessagingHosts') + : path.join(process.env.HOME, '.config', 'google-chrome', 'NativeMessagingHosts'); + + fs.mkdirSync(dir, { recursive: true }); + const target = path.join(dir, `${manifest.name}.json`); + fs.writeFileSync(target, JSON.stringify(manifest, null, 2)); + console.log('Installed manifest to:', target); + } + + console.log('\nDone! Reload the extension in chrome://extensions'); +} + +main(); \ No newline at end of file From 75173dc3fb9355913e6e164d96f1b4b39e5fd4c7 Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:54:30 -0500 Subject: [PATCH 02/24] Add browser extension integration toggle to Settings - Add 'Browser Extension Integration' feature toggle in Settings - IPC handler 'browserExtension:installNativeHost' installs native messaging host (Windows) - When enabled, installs native host for Chrome/Edge - i18n strings for install status --- src/i18n/locales/en.json | 6 ++++++ src/main/ipcHandlers.js | 31 +++++++++++++++++++++++++++ src/ui/js/pages/settings.js | 42 +++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 1592cef..9021e79 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -117,6 +117,12 @@ "settings.geoLookup.desc": "Resolve IP addresses to display a world map of active connections", "settings.networkPerimeterMap.label": "Network Perimeter Map", "settings.networkPerimeterMap.desc": "Show the live connection visualization on the Firewall page", +"settings.browserExtension.label": "Browser Extension Integration", + "settings.browserExtension.desc": "Receive credential leak alerts from the Soterios browser extension (requires native messaging host)", + "settings.browserExtension.installing": "Installing native messaging host...", + "settings.browserExtension.installed": "Native messaging host installed. Install the extension from Chrome Web Store.", + "settings.browserExtension.installFailed": "Failed to install native host: {error}", + "settings.browserExtension.disabled": "Browser extension integration disabled", "settings.colorScheme": "Color Scheme", "settings.theme.dark": "Dark", "settings.theme.light": "Light", diff --git a/src/main/ipcHandlers.js b/src/main/ipcHandlers.js index fb5e891..b370670 100644 --- a/src/main/ipcHandlers.js +++ b/src/main/ipcHandlers.js @@ -918,6 +918,37 @@ function registerIpcHandlers(mainWindow, services) { }); ipcMain.handle('tray:quit', () => app.quit()); + + // -- Browser Extension Native Host -- + ipcMain.handle('browserExtension:installNativeHost', async () => { + if (process.platform !== 'win32') { + return { ok: false, error: 'Native host install only supported on Windows' }; + } + const { execSync } = require('child_process'); + const fs = require('fs'); + const path = require('path'); + const extDir = path.join(__dirname, '..', '..', 'browser-extension'); + const manifestPath = path.join(extDir, 'native-host-manifest.json'); + const batPath = path.join(extDir, 'native-host.bat'); + const jsPath = path.join(extDir, 'native-host.js'); + if (!fs.existsSync(manifestPath) || !fs.existsSync(batPath) || !fs.existsSync(jsPath)) { + return { ok: false, error: 'Extension files not found. Reinstall Soterios.' }; + } + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + const extId = process.env.SOTERIOS_EXT_ID || 'YOUR_EXTENSION_ID_HERE'; + manifest.allowed_origins = [manifest.allowed_origins[0].replace('', extId)]; + const regPath = `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${manifest.name}`; + const regCmd = `reg add "${regPath}" /ve /t REG_SZ /d "${manifestPath.replace(/\\/g, '\\\\')}" /f`; + try { + execSync(regCmd, { stdio: 'ignore' }); + const regPathEdge = `HKCU\\Software\\Microsoft\\Edge\\NativeMessagingHosts\\${manifest.name}`; + const regCmdEdge = `reg add "${regPathEdge}" /ve /t REG_SZ /d "${manifestPath.replace(/\\/g, '\\\\')}" /f`; + try { execSync(regCmdEdge, { stdio: 'ignore' }); } catch (_) {} + return { ok: true }; + } catch (e) { + return { ok: false, error: e.message || String(e) }; + } + }); } module.exports = { registerIpcHandlers }; \ No newline at end of file diff --git a/src/ui/js/pages/settings.js b/src/ui/js/pages/settings.js index b7499c9..9a895b5 100644 --- a/src/ui/js/pages/settings.js +++ b/src/ui/js/pages/settings.js @@ -119,6 +119,21 @@ window.Pages.settings = { +
+
+
${escapeHtml(t('settings.networkPerimeterMap.label'))}
+
${escapeHtml(t('settings.networkPerimeterMap.desc'))}
+
+ +
+ +
+
+
${escapeHtml(t('settings.browserExtension.label'))}
+
${escapeHtml(t('settings.browserExtension.desc'))}
+
+ +
@@ -384,6 +399,33 @@ window.Pages.settings = { container.querySelector('#externalLookupsToggle').addEventListener('change', (event) => saveFeature('externalLookups', event.target.checked, event.target)); container.querySelector('#geoLookupToggle').addEventListener('change', (event) => saveFeature('geoLookup', event.target.checked, event.target)); container.querySelector('#networkPerimeterMapToggle').addEventListener('change', (event) => saveFeature('networkPerimeterMap', event.target.checked, event.target)); + container.querySelector('#browserExtensionToggle').addEventListener('change', async (event) => { + const checked = event.target.checked; + const statusEl = container.querySelector('#featureToggleStatus'); + statusEl.textContent = ''; + event.target.disabled = true; + try { + await Api.updateSettings({ features: { browserExtension: checked } }); + if (checked) { + statusEl.textContent = t('settings.browserExtension.installing'); + const result = await window.api.invoke('browserExtension:installNativeHost'); + if (result.ok) { + statusEl.textContent = t('settings.browserExtension.installed'); + } else { + event.target.checked = false; + await Api.updateSettings({ features: { browserExtension: false } }); + statusEl.textContent = result.error || t('settings.browserExtension.installFailed'); + } + } else { + statusEl.textContent = t('settings.featureSaved'); + } + } catch (err) { + event.target.checked = !checked; + statusEl.textContent = err.message || String(err); + } finally { + event.target.disabled = false; + } + }); container.querySelector('#notificationsToggle').addEventListener('change', async (event) => { const checked = event.target.checked; const statusEl = container.querySelector('#notificationStatus'); From 4fe4ec22e2b5428d7aef3157892e745e94220647 Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:17:12 -0500 Subject: [PATCH 03/24] Enhance tray dashboard: health score, RTP status, quick scan, network sparkline - healthSummary.js: Return RTP, firewall, network stats, last scan - trayDashboard.html: Health score badge, RTP indicator, sparkline, quick scan button - trayDashboard.js: Already passes enhanced summary --- src/main/healthSummary.js | 50 +++++++- src/ui/pages/trayDashboard.html | 208 +++++++++++++++++++++++++++++--- src/ui/pages/trayDashboard.js | 166 +++++++++++++++++++++++++ 3 files changed, 406 insertions(+), 18 deletions(-) create mode 100644 src/ui/pages/trayDashboard.js diff --git a/src/main/healthSummary.js b/src/main/healthSummary.js index df30525..ad64678 100644 --- a/src/main/healthSummary.js +++ b/src/main/healthSummary.js @@ -19,10 +19,56 @@ async function getTrayHealthSummary(db, toolRegistry) { } const disk = result.data.breakdown?.disk; + + // RTP status + let rtp = { enabled: false }; + try { + const { RealTimeWatcher } = require('../security/RealTimeWatcher'); + // Check if RTP is enabled in settings + const rtpEnabled = db.getSetting('feature.realtimeProtection', false); + rtp = { enabled: rtpEnabled }; + } catch (_) {} + + // Firewall status + let firewall = { active: false }; + try { + const { execFile } = require('child_process'); + const { promisify } = require('util'); + const execFileAsync = promisify(execFile); + const { stdout } = await execFileAsync('netsh', ['advfirewall', 'show', 'allprofiles', 'state'], { timeout: 5000 }); + firewall = { active: /ON|ENABLED/i.test(stdout) }; + } catch (_) {} + + // Network traffic history (last 24h) + let network = { rxKBs: 0, txKBs: 0, history: [] }; + try { + const history = db.getNetworkHistory ? db.getNetworkHistory(24 * 60) : []; // last 24h, 1 sample per min + if (history.length) { + const latest = history[history.length - 1]; + network.rxKBs = Math.round((latest.rx_bytes || 0) / 1024); + network.txKBs = Math.round((latest.tx_bytes || 0) / 1024); + network.history = history.slice(-60).map(h => (h.tx_bytes + h.rx_bytes) / 1024); // last 60 samples for sparkline + } + } catch (_) {} + + // Last scan info + let lastScan = null; + if (latest) { + lastScan = { + timestamp: latest.timestamp, + filesScanned: latest.files_scanned, + threatsFound: latest.threats_found + }; + } + return { score: result.data.score, - detail: disk?.reason || 'Protection and resource summary ready.' + detail: disk?.reason || 'Protection and resource summary ready.', + rtp, + firewall, + network, + lastScan }; } -module.exports = { getTrayHealthSummary }; +module.exports = { getTrayHealthSummary }; \ No newline at end of file diff --git a/src/ui/pages/trayDashboard.html b/src/ui/pages/trayDashboard.html index e4b198c..1a6e3e0 100644 --- a/src/ui/pages/trayDashboard.html +++ b/src/ui/pages/trayDashboard.html @@ -11,6 +11,13 @@ --text: #f2f5f8; --muted: #aab4bf; --accent: #58a6ff; + --accent-bg: rgba(88,166,255,0.15); + --danger: #f85149; + --danger-bg: rgba(248,81,73,0.15); + --ok: #3fb950; + --ok-bg: rgba(63,185,80,0.15); + --spark-rx: #58a6ff; + --spark-tx: #f85149; } html, body { margin: 0; @@ -27,15 +34,46 @@ box-shadow: 0 12px 32px rgba(0,0,0,0.35); color: var(--text); } - .title { font-size: 14px; font-weight: 700; margin-bottom: 4px; } + .header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } + .title { font-size: 14px; font-weight: 700; } + .status-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + } + .status-badge.active { background: var(--ok-bg); color: #3fb950; } + .status-badge.inactive { background: var(--danger-bg); color: #f85149; } + .status-dot { + width: 6px; height: 6px; border-radius: 50%; + background: currentColor; + } + .score-row { display: flex; align-items: baseline; gap: 12px; margin: 8px 0 4px; } .score { font-size: 42px; font-weight: 700; color: var(--accent); line-height: 1; + } + .score-detail { font-size: 12px; color: var(--muted); line-height: 1.5; } + .rtp-row { display: flex; align-items: center; gap: 8px; padding: 8px 0; border-top: 1px solid var(--border); } + .rtp-label { font-size: 12px; color: var(--muted); flex: 1; } + .sparkline { + height: 40px; margin: 8px 0; + position: relative; + } + .sparkline canvas { width: 100%; height: 100%; } + .sparkline-labels { + display: flex; + justify-content: space-between; + font-size: 9px; + color: var(--muted); + margin-top: 2px; } - .meta { font-size: 12px; color: var(--muted); line-height: 1.5; } .actions { display: flex; gap: 8px; margin-top: 14px; } button { flex: 1; @@ -46,40 +84,178 @@ padding: 8px 10px; font-size: 12px; cursor: pointer; + transition: background 0.15s, border-color 0.15s; } + button:hover { background: rgba(255,255,255,0.08); } button.primary { background: var(--accent); border-color: transparent; color: #0b0e14; font-weight: 600; } + button.primary:hover { background: #4a9eff; } + button.secondary:hover { background: var(--danger-bg); border-color: var(--danger); color: var(--danger); }
-
System Health
-
--
-
Loading summary...
+
+
System Health
+ + + RTP + +
+ +
+
--
+
+
Loading summary...
+ +
+ Network +
+ +
+ 0 KB/s + 0 KB/s +
+
+
+
- - + + +
+ - + \ No newline at end of file diff --git a/src/ui/pages/trayDashboard.js b/src/ui/pages/trayDashboard.js new file mode 100644 index 0000000..d76ca4e --- /dev/null +++ b/src/ui/pages/trayDashboard.js @@ -0,0 +1,166 @@ +window.api.on('tray:summary', (summary) => { + if (!summary) return; + + const scoreEl = document.getElementById('scoreEl'); + const detailEl = document.getElementById('detailEl'); + const rtpDot = document.getElementById('rtpDot'); + const rtpLabel = document.getElementById('rtpLabel'); + const rtpStatus = document.getElementById('rtpStatus'); + const fwDot = document.getElementById('fwDot'); + const fwStatus = document.getElementById('fwStatus'); + const rxRate = document.getElementById('rxRate'); + const txRate = document.getElementById('txRate'); + const lastScan = document.getElementById('lastScan'); + + // Score + if (summary.score != null) { + scoreEl.textContent = summary.score; + scoreEl.className = 'score ' + (summary.score >= 80 ? 'pass' : summary.score >= 50 ? 'warn' : 'fail'); + } else { + scoreEl.textContent = '—'; + scoreEl.className = 'score'; + } + detailEl.textContent = summary.detail || 'Health summary unavailable.'; + + // RTP + if (summary.rtp) { + rtpDot.className = 'status-dot ' + (summary.rtp.enabled ? 'active' : 'inactive'); + rtpLabel.textContent = summary.rtp.enabled ? 'RTP Active' : 'RTP Disabled'; + rtpStatus.textContent = summary.rtp.enabled ? 'Monitoring file system' : 'Click to enable'; + } else { + rtpDot.className = 'status-dot unknown'; + rtpLabel.textContent = 'RTP Unknown'; + rtpStatus.textContent = '—'; + } + + // Firewall + if (summary.firewall) { + fwDot.className = 'status-dot ' + (summary.firewall.active ? 'active' : 'inactive'); + fwStatus.textContent = summary.firewall.active ? 'Active' : 'Disabled'; + } else { + fwDot.className = 'status-dot unknown'; + fwStatus.textContent = '—'; + } + + // Network rates + if (summary.network) { + document.getElementById('rxRate').textContent = summary.network.rxKBs || 0; + document.getElementById('txRate').textContent = summary.network.txKBs || 0; + drawSparkline(summary.network.history || []); + } + + // Last scan + if (summary.lastScan) { + const scan = summary.lastScan; + const when = scan.timestamp ? new Date(scan.timestamp).toLocaleString() : 'Unknown'; + document.getElementById('lastScan').textContent = + `${when} · ${scan.filesScanned || 0} files · ${scan.threatsFound || 0} threats`; + } +}); + +async function loadSummary() { + try { + const summary = await window.api.invoke('tray:getSummary'); + if (summary) { + const scoreEl = document.getElementById('scoreEl'); + const detailEl = document.getElementById('detailEl'); + if (summary.score != null) { + scoreEl.textContent = summary.score; + scoreEl.className = 'score ' + (summary.score >= 80 ? 'pass' : summary.score >= 50 ? 'warn' : 'fail'); + } + detailEl.textContent = summary.detail || 'Health summary unavailable.'; + + // Update RTP, firewall, network, last scan from summary + if (summary.rtp) { + const rtpDot = document.getElementById('rtpDot'); + const rtpLabel = document.getElementById('rtpLabel'); + const rtpStatus = document.getElementById('rtpStatus'); + rtpDot.className = 'status-dot ' + (summary.rtp.enabled ? 'active' : 'inactive'); + rtpLabel.textContent = summary.rtp.enabled ? 'RTP Active' : 'RTP Disabled'; + rtpStatus.textContent = summary.rtp.enabled ? 'Monitoring file system' : 'Click to enable'; + } + if (summary.firewall) { + const fwDot = document.getElementById('fwDot'); + const fwStatus = document.getElementById('fwStatus'); + fwDot.className = 'status-dot ' + (summary.firewall.active ? 'active' : 'inactive'); + fwStatus.textContent = summary.firewall.active ? 'Active' : 'Disabled'; + } + if (summary.network) { + document.getElementById('rxRate').textContent = summary.network.rxKBs || 0; + document.getElementById('txRate').textContent = summary.network.txKBs || 0; + drawSparkline(summary.network.history || []); + } + if (summary.lastScan) { + const scan = summary.lastScan; + const when = scan.timestamp ? new Date(scan.timestamp).toLocaleString() : 'Unknown'; + document.getElementById('lastScan').textContent = + `${when} · ${scan.filesScanned || 0} files · ${scan.threatsFound || 0} threats`; + } + } + } catch (e) { + console.error('Failed to load tray summary:', e); + document.getElementById('detailEl').textContent = 'Unable to load health summary.'; + } +} + +function drawSparkline(history) { + const canvas = document.getElementById('sparkCanvas'); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + const dpr = window.devicePixelRatio || 1; + const rect = canvas.getBoundingClientRect(); + canvas.width = rect.width * dpr; + canvas.height = rect.height * dpr; + ctx.scale(dpr, dpr); + ctx.clearRect(0, 0, rect.width, rect.height); + + if (!history.length) return; + + const maxVal = Math.max(...history, 1); + const minVal = Math.min(...history); + const range = maxVal - minVal || 1; + + ctx.strokeStyle = '#58a6ff'; + ctx.lineWidth = 2; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.beginPath(); + + history.forEach((val, i) => { + const x = (i / (history.length - 1 || 1)) * rect.width; + const y = rect.height - ((val - minVal) / range) * rect.height * 0.85 - rect.height * 0.075; + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + }); + ctx.stroke(); + + // Fill gradient + const grad = ctx.createLinearGradient(0, 0, 0, rect.height); + grad.addColorStop(0, 'rgba(88,166,255,0.15)'); + grad.addColorStop(1, 'rgba(88,166,255,0)'); + ctx.fillStyle = grad; + ctx.lineTo(rect.width, rect.height); + ctx.lineTo(0, rect.height); + ctx.closePath(); + ctx.fill(); +} + +document.getElementById('btnQuickScan').addEventListener('click', async () => { + const btn = document.getElementById('btnQuickScan'); + btn.disabled = true; + btn.textContent = 'Starting...'; + try { + await window.api.invoke('scan:quick'); + btn.textContent = 'Quick Scan'; + } catch (e) { + btn.textContent = 'Failed'; + setTimeout(() => { btn.disabled = false; btn.textContent = 'Quick Scan'; }, 2000); + } +}); + +document.getElementById('btnOpen').addEventListener('click', () => { + window.api.invoke('tray:openMain'); +}); + +loadSummary(); +setInterval(loadSummary, 15000); // Refresh every 15s \ No newline at end of file From 9ef97ea2697d67366a6d0a84627284be9f2c36e7 Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:21:20 -0500 Subject: [PATCH 04/24] Add tray dashboard: health score, RTP, quick scan, network sparkline --- src/i18n/locales/en.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 9021e79..7725c5e 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -124,6 +124,15 @@ "settings.browserExtension.installFailed": "Failed to install native host: {error}", "settings.browserExtension.disabled": "Browser extension integration disabled", "settings.colorScheme": "Color Scheme", + "tray.systemHealth": "System Health", + "tray.rtpActive": "RTP Active", + "tray.rtpOff": "RTP Off", + "tray.network": "Network", + "tray.quickScan": "Quick Scan", + "tray.openApp": "Open Soterios", + "tray.quit": "Quit", + "tray.lastScanAgo": "Last scan {ago}", + "tray.networkRxTx": "↓ {rx} KB/s ↑ {tx} KB/s", "settings.theme.dark": "Dark", "settings.theme.light": "Light", "settings.theme.ocean": "Ocean", From a189d87c6f9f89b626599727e91582aa2f60f507 Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:37:00 -0500 Subject: [PATCH 05/24] Fix health score translations for all locales - Add missing health.label.* keys to match dashboard usage - Translate English strings in it, tr, ru, pt-BR, ko, ja locales - Fix key structure (health.label.* vs health.malware.label) --- src/i18n/locales/ar.json | 10 ++- src/i18n/locales/de.json | 10 ++- src/i18n/locales/en.json | 8 ++ src/i18n/locales/es.json | 15 +++- src/i18n/locales/fr.json | 10 ++- src/i18n/locales/it.json | 146 +++++++++++++++++++----------------- src/i18n/locales/ko.json | 35 +++++++-- src/i18n/locales/pt-BR.json | 23 +++++- src/i18n/locales/ru.json | 35 +++++++-- src/i18n/locales/tr.json | 23 +++++- 10 files changed, 226 insertions(+), 89 deletions(-) diff --git a/src/i18n/locales/ar.json b/src/i18n/locales/ar.json index 0f0d252..447b052 100644 --- a/src/i18n/locales/ar.json +++ b/src/i18n/locales/ar.json @@ -791,7 +791,15 @@ "health.malware.clean": "No threats found in the most recent scan.", "health.malware.low": "{count} threat match(es) found in the most recent scan.", "health.malware.high": "{count} threat matches found in the most recent scan.", - "health.scanRecency.label": "Scan Recency", + "health.label.malware": "نتائج فحص البرمجيات الخبيثة", + "health.label.scanRecency": "حداثة الفحص", + "health.label.disk": "مساحة القرص", + "health.label.memory": "استخدام الذاكرة", + "health.label.load": "حمل وحدة المعالجة المركزية", + "health.label.uptime": "وقت تشغيل النظام", + "health.label.rtp": "الحماية في الوقت الفعلي", + "health.label.firewall": "جدار الحماية", + "health.scanRecency.label": "حداثة الفحص", "health.scanRecency.recent": "Last scan ran within the last day.", "health.scanRecency.daysAgo": "Last scan ran {days} day(s) ago.", "health.disk.label": "Disk Space", diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 3700ac7..5f15b08 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -792,7 +792,15 @@ "health.malware.clean": "No threats found in the most recent scan.", "health.malware.low": "{count} threat match(es) found in the most recent scan.", "health.malware.high": "{count} threat matches found in the most recent scan.", - "health.scanRecency.label": "Scan Recency", + "health.label.malware": "Ergebnisse des Malware-Scans", + "health.label.scanRecency": "Scan-Aktualität", + "health.label.disk": "Festplattenplatz", + "health.label.memory": "Speichernutzung", + "health.label.load": "CPU-Auslastung", + "health.label.uptime": "Systemlaufzeit", + "health.label.rtp": "Echtzeitschutz", + "health.label.firewall": "Firewall", + "health.scanRecency.label": "Scan-Aktualität", "health.scanRecency.recent": "Last scan ran within the last day.", "health.scanRecency.daysAgo": "Last scan ran {days} day(s) ago.", "health.disk.label": "Disk Space", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 7725c5e..4f854cd 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -848,6 +848,14 @@ "health.malware.clean": "No threats found in the most recent scan.", "health.malware.low": "{count} threat match(es) found in the most recent scan.", "health.malware.high": "{count} threat matches found in the most recent scan.", + "health.label.malware": "Malware Scan Results", + "health.label.scanRecency": "Scan Recency", + "health.label.disk": "Disk Space", + "health.label.memory": "Memory Usage", + "health.label.load": "CPU Load", + "health.label.uptime": "System Uptime", + "health.label.rtp": "Real-Time Protection", + "health.label.firewall": "Firewall", "health.scanRecency.label": "Scan Recency", "health.scanRecency.recent": "Last scan ran within the last day.", "health.scanRecency.daysAgo": "Last scan ran {days} day(s) ago.", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 2d8048d..bd71b76 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -795,6 +795,14 @@ "passwords.crackTimeDays": "{count} días", "passwords.crackTimeYears": "{count} años", "passwords.crackTimeCenturies": "{count} siglos", + "health.label.malware": "Resultados del escaneo de malware", + "health.label.scanRecency": "Recencia del escaneo", + "health.label.disk": "Espacio en disco", + "health.label.memory": "Uso de memoria", + "health.label.load": "Carga de CPU", + "health.label.uptime": "Tiempo de actividad del sistema", + "health.label.rtp": "Protección en tiempo real", + "health.label.firewall": "Firewall", "health.malware.label": "Resultados del escaneo de malware", "health.malware.noScan": "Aún no se ha ejecutado ningún escaneo.", "health.malware.clean": "No se encontraron amenazas en el escaneo más reciente.", @@ -808,7 +816,12 @@ "health.disk.noVolumes": "No se encontraron volúmenes orientados al usuario para la puntuación de disco.", "health.disk.healthy": "Todos los volúmenes saludables (uso máximo {usage}%).", "health.memory.label": "Uso de memoria", - "health.reason.uptimeToday": "Reiniciado en el último día.", + "health.memory.reason": "{pct}% de memoria en uso.", + "health.load.label": "Carga de CPU", + "health.load.reason": "Carga de CPU al {pct}%.", + "health.uptime.label": "Tiempo de actividad del sistema", + "health.rtp.label": "Protección en tiempo real", + "health.firewall.label": "Firewall", "health.reason.uptimeDays": "Reiniciado hace {days} día(s) — dentro del rango normal.", "health.reason.uptimeWeeks": "Ejecutándose {days} días sin reiniciar — considere reiniciar pronto para aplicar actualizaciones pendientes.", "health.reason.uptimeLong": "Ejecutándose {days} días sin reiniciar — se recomienda reiniciar para aplicar actualizaciones pendientes.", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index ae5881e..63dba92 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -794,7 +794,15 @@ "health.malware.clean": "No threats found in the most recent scan.", "health.malware.low": "{count} threat match(es) found in the most recent scan.", "health.malware.high": "{count} threat matches found in the most recent scan.", - "health.scanRecency.label": "Scan Recency", + "health.label.malware": "Résultats de l'analyse anti-malware", + "health.label.scanRecency": "Récence de l'analyse", + "health.label.disk": "Espace disque", + "health.label.memory": "Utilisation mémoire", + "health.label.load": "Charge CPU", + "health.label.uptime": "Temps d'activité système", + "health.label.rtp": "Protection en temps réel", + "health.label.firewall": "Pare-feu", + "health.scanRecency.label": "Récence de l'analyse", "health.scanRecency.recent": "Last scan ran within the last day.", "health.scanRecency.daysAgo": "Last scan ran {days} day(s) ago.", "health.disk.label": "Disk Space", diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index 65da3ce..26c455e 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -787,79 +787,87 @@ "passwords.crackTimeYears": "{count} years", "passwords.crackTimeCenturies": "{count} centuries", "health.malware.label": "Malware Scan Results", - "health.malware.noScan": "No scan has been run yet.", - "health.malware.clean": "No threats found in the most recent scan.", - "health.malware.low": "{count} threat match(es) found in the most recent scan.", - "health.malware.high": "{count} threat matches found in the most recent scan.", - "health.scanRecency.label": "Scan Recency", - "health.scanRecency.recent": "Last scan ran within the last day.", - "health.scanRecency.daysAgo": "Last scan ran {days} day(s) ago.", - "health.disk.label": "Disk Space", - "health.disk.lowSpace": "Low space on: {volumes} ({usage}% used).", - "health.disk.noVolumes": "No user-facing volumes found for disk scoring.", - "health.disk.healthy": "All volumes healthy (highest usage {usage}%).", - "health.memory.label": "Memory Usage", - "health.memory.reason": "{pct}% of memory in use.", - "health.load.label": "CPU Load", - "health.load.reason": "CPU load at {pct}%.", - "health.uptime.label": "System Uptime", - "health.rtp.label": "Real-Time Protection", + "health.malware.noScan": "Nessun scansione è stata eseguita.", + "health.malware.clean": "Nessuna minaccia trovata nell'ultimo scansione.", + "health.malware.low": "Trovata/e {count} corrispondenza/e di minaccia nell'ultimo scansione.", + "health.malware.high": "Trovate {count} corrispondenze di minaccia nell'ultimo scansione.", + "health.label.malware": "Risultati scansione malware", + "health.label.scanRecency": "Recency scansione", + "health.label.disk": "Spazio su disco", + "health.label.memory": "Utilizzo memoria", + "health.label.load": "Carico CPU", + "health.label.uptime": "Uptime sistema", + "health.label.rtp": "Protezione in tempo reale", + "health.label.firewall": "Firewall", + "health.scanRecency.label": "Recency scansione", + "health.scanRecency.recent": "L'ultimo scansione è stato eseguito nell'ultimo giorno.", + "health.scanRecency.daysAgo": "L'ultimo scansione è stato eseguito {days} giorno fa.", + "health.disk.label": "Spazio su disco", + "health.disk.lowSpace": "Poco spazio su: {volumes} ({usage}% usato).", + "health.disk.noVolumes": "Nessun volume rivolto all'utente trovato per la valutazione del disco.", + "health.disk.healthy": "Tutti i volumi sani (uso massimo {usage}%).", + "health.memory.label": "Utilizzo memoria", + "health.memory.reason": "{pct}% di memoria in uso.", + "health.load.label": "Carico CPU", + "health.load.reason": "Carico CPU al {pct}%.", + "health.uptime.label": "Tempo di attività sistema", + "health.rtp.label": "Protezione in tempo reale", "health.firewall.label": "Firewall", "audit.check.defender.name": "Windows Defender", - "audit.check.rtp.name": "Real-Time Protection", - "audit.check.uac.name": "User Account Control (UAC)", - "audit.check.updates.name": "Windows Updates", - "audit.check.bitlocker.name": "BitLocker Drive Encryption", + "audit.check.rtp.name": "Protezione in tempo reale", + "audit.check.uac.name": "Controllo account utente (UAC)", + "audit.check.updates.name": "Aggiornamenti Windows", + "audit.check.bitlocker.name": "Crittografia unità BitLocker", "audit.check.bitlocker.shortName": "BitLocker", - "audit.check.execPolicy.name": "PowerShell Execution Policy", - "audit.check.secureBoot.name": "Secure Boot", + "audit.check.execPolicy.name": "Criteri di esecuzione PowerShell", + "audit.check.secureBoot.name": "Avvio protetto", "toast.scanProgressTitle": "Avanzamento scansione Soterios", - "audit.check.defender.enabled.msg": "Defender antivirus is enabled and running.", - "audit.check.defender.disabled.msg": "Defender antivirus is disabled!", - "audit.check.defender.disabled.detail": "Antivirus protection is turned off.", - "audit.check.rtp.active.msg": "Real-time protection is active.", - "audit.check.rtp.off.msg": "Real-time protection is off!", - "audit.check.rtp.active.detail": "Threats are blocked as they appear.", - "audit.check.rtp.off.detail": "Your system is vulnerable to active threats.", - "audit.check.uac.enabled.msg": "UAC is enabled.", - "audit.check.uac.disabled.msg": "UAC is disabled! This is a severe security risk.", - "audit.check.uac.enabled.detail": "UAC prompts before making system-level changes.", - "audit.check.uac.disabled.detail": "All programs run with full administrator privileges.", - "audit.check.updates.none.msg": "No pending updates.", - "audit.check.updates.none.detail": "All available updates are installed.", - "audit.check.bitlocker.encrypted.msg": "System drive is encrypted.", - "audit.check.bitlocker.encrypted.detail": "Your data is protected if the device is lost or stolen.", - "audit.check.bitlocker.notEncrypted.msg": "System drive is NOT encrypted.", - "audit.check.bitlocker.unavailable.msg": "BitLocker status unavailable.", - "audit.check.bitlocker.notEncrypted.detail": "Anyone with physical access can read your data.", - "audit.check.bitlocker.unknown.detail": "Could not determine BitLocker protection status.", - "audit.check.bitlocker.unknown.msg": "BitLocker status could not be determined.", - "audit.check.bitlocker.unexpected.detail": "Unexpected BitLocker response format.", - "audit.check.bitlocker.na.msg": "BitLocker is not available on this system.", - "audit.check.bitlocker.na.detail": "Requires Windows Pro/Enterprise and a TPM chip.", - "audit.check.execPolicy.remoteSigned.msg": "Policy: RemoteSigned", - "audit.check.execPolicy.restricted.msg": "Policy: Restricted", - "audit.check.execPolicy.allSigned.msg": "Policy: AllSigned", - "audit.check.execPolicy.secure.detail": "Only signed or locally authored scripts can run.", - "audit.check.execPolicy.insecure.detail": "Less restrictive execution policy may allow untrusted scripts.", - "audit.check.secureBoot.enabled.msg": "Secure Boot is enabled.", - "audit.check.secureBoot.disabled.msg": "Secure Boot is disabled!", - "audit.check.secureBoot.enabled.detail": "Only trusted bootloaders can run during system startup.", - "audit.check.secureBoot.disabled.detail": "System is vulnerable to bootkit attacks.", - "audit.check.defender.parseError.msg": "Could not parse Defender status.", - "audit.check.defender.queryError.msg": "Failed to query Defender status.", - "audit.check.defender.queryError.detail": "The Get-MpComputerStatus cmdlet may not be available on this system.", - "audit.check.uac.error.msg": "Could not check UAC status.", - "audit.check.updates.parseError.msg": "Could not parse update status.", - "audit.check.updates.parseError.detail": "Unexpected response from Windows Update query.", - "audit.check.updates.queryError.msg": "Could not query update status.", - "audit.check.updates.queryError.detail": "Windows Update may be disabled or the COM query timed out.", - "audit.check.bitlocker.info.msg": "BitLocker status unavailable (may not be supported on this edition).", - "audit.check.bitlocker.info.detail": "BitLocker requires Windows Pro or Enterprise.", - "audit.check.execPolicy.error.msg": "PowerShell execution policy query failed.", - "audit.check.execPolicy.error.detail": "Unable to query execution policy.", - "audit.check.secureBoot.unknown.msg": "Secure Boot status could not be determined.", - "audit.check.secureBoot.unknown.detail": "This check may not be supported on virtual machines or older hardware.", + "audit.check.defender.enabled.msg": "L'antivirus Defender è abilitato e in esecuzione.", + "audit.check.defender.disabled.msg": "L'antivirus Defender è disabilitato!", + "audit.check.defender.disabled.detail": "La protezione antivirus è disattivata.", + "audit.check.rtp.active.msg": "La protezione in tempo reale è attiva.", + "audit.check.rtp.off.msg": "La protezione in tempo reale è disattivata!", + "audit.check.rtp.active.detail": "Le minacce vengono bloccate non appena appaiono.", + "audit.check.rtp.off.detail": "Il tuo sistema è vulnerabile alle minacce attive.", + "audit.check.uac.enabled.msg": "UAC è abilitato.", + "audit.check.uac.disabled.msg": "UAC è disabilitato! Questo è un grave rischio per la sicurezza.", + "audit.check.uac.enabled.detail": "UAC richiede conferma prima di apportare modifiche a livello di sistema.", + "audit.check.uac.disabled.detail": "Tutti i programmi vengono eseguiti con privilegi di amministratore completi.", + "audit.check.updates.none.msg": "Nessun aggiornamento in sospeso.", + "audit.check.updates.none.detail": "Tutti gli aggiornamenti disponibili sono installati.", + "audit.check.bitlocker.encrypted.msg": "L'unità di sistema è crittografata.", + "audit.check.bitlocker.encrypted.detail": "I tuoi dati sono protetti se il dispositivo viene perso o rubato.", + "audit.check.bitlocker.notEncrypted.msg": "L'unità di sistema NON è crittografata.", + "audit.check.bitlocker.unavailable.msg": "Stato BitLocker non disponibile.", + "audit.check.bitlocker.notEncrypted.detail": "Chiunque abbia accesso fisico può leggere i tuoi dati.", + "audit.check.bitlocker.unknown.detail": "Impossibile determinare lo stato di protezione BitLocker.", + "audit.check.bitlocker.unknown.msg": "Impossibile determinare lo stato di BitLocker.", + "audit.check.bitlocker.unexpected.detail": "Formato risposta BitLocker inaspettato.", + "audit.check.bitlocker.na.msg": "BitLocker non è disponibile su questo sistema.", + "audit.check.bitlocker.na.detail": "Richiede Windows Pro/Enterprise e un chip TPM.", + "audit.check.execPolicy.remoteSigned.msg": "Criterio: RemoteSigned", + "audit.check.execPolicy.restricted.msg": "Criterio: Restricted", + "audit.check.execPolicy.allSigned.msg": "Criterio: AllSigned", + "audit.check.execPolicy.secure.detail": "Solo script firmati o creati localmente possono essere eseguiti.", + "audit.check.execPolicy.insecure.detail": "Criteri di esecuzione meno restrittivi possono permettere script non affidabili.", + "audit.check.secureBoot.enabled.msg": "Avvio sicuro è abilitato.", + "audit.check.secureBoot.disabled.msg": "Avvio sicuro è disabilitato!", + "audit.check.secureBoot.enabled.detail": "Solo bootloader fidati possono essere eseguiti durante l'avvio del sistema.", + "audit.check.secureBoot.disabled.detail": "Il sistema è vulnerabile agli attacchi bootkit.", + "audit.check.defender.parseError.msg": "Impossibile analizzare lo stato di Defender.", + "audit.check.defender.queryError.msg": "Impossibile interrogare lo stato di Defender.", + "audit.check.defender.queryError.detail": "Il cmdlet Get-MpComputerStatus potrebbe non essere disponibile su questo sistema.", + "audit.check.uac.error.msg": "Impossibile verificare lo stato UAC.", + "audit.check.updates.parseError.msg": "Impossibile analizzare lo stato degli aggiornamenti.", + "audit.check.updates.parseError.detail": "Risposta inaspettata dalla query Windows Update.", + "audit.check.updates.queryError.msg": "Impossibile interrogare lo stato degli aggiornamenti.", + "audit.check.updates.queryError.detail": "Windows Update potrebbe essere disabilitato o la query COM è scaduta.", + "audit.check.bitlocker.info.msg": "Stato BitLocker non disponibile (potrebbe non essere supportato in questa edizione).", + "audit.check.bitlocker.info.detail": "BitLocker richiede Windows Pro o Enterprise.", + "audit.check.execPolicy.error.msg": "Query criterio esecuzione PowerShell fallita.", + "audit.check.execPolicy.error.detail": "Impossibile interrogare il criterio di esecuzione.", + "audit.check.secureBoot.unknown.msg": "Impossibile determinare lo stato di avvio sicuro.", + "audit.check.secureBoot.unknown.detail": "Questa verifica potrebbe non essere supportata su macchine virtuali o hardware più vecchio.", "audit.check.defender.rec": "Keep Windows Update enabled for automatic definition updates.", "audit.check.rtp.rec": "Enable real-time protection in Windows Security settings.", "audit.check.uac.rec": "Enable UAC via Control Panel > User Accounts > Change User Account Control settings.", diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index 8400b0a..84bd323 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -786,13 +786,34 @@ "passwords.crackTimeHours": "{count} hours", "passwords.crackTimeDays": "{count} days", "passwords.crackTimeYears": "{count} years", - "passwords.crackTimeCenturies": "{count} centuries", - "health.malware.label": "Malware Scan Results", - "health.malware.noScan": "No scan has been run yet.", - "health.malware.clean": "No threats found in the most recent scan.", - "health.malware.low": "{count} threat match(es) found in the most recent scan.", - "health.malware.high": "{count} threat matches found in the most recent scan.", - "health.scanRecency.label": "Scan Recency", + "passwords.crackTimeCenturies": "{count} 세기", + "health.malware.label": "맬웨어 검사 결과", + "health.malware.noScan": "아직 검사가 실행되지 않음.", + "health.malware.clean": "최근 검사에서 위협 없음 발견.", + "health.malware.low": "최근 검사에서 {count}개의 위협 매치 발견.", + "health.malware.high": "최근 검사에서 {count}개의 위협 매치 발견.", + "health.label.malware": "맬웨어 검사 결과", + "health.label.scanRecency": "검사 최신성", + "health.label.disk": "디스크 공간", + "health.label.memory": "메모리 사용량", + "health.label.load": "CPU 부하", + "health.label.uptime": "시스템 가동 시간", + "health.label.rtp": "실시간 보호", + "health.label.firewall": "방화벽", + "health.scanRecency.label": "검사 최신성", + "health.scanRecency.recent": "마지막 검사가 지난 하루 이내에 실행됨.", + "health.scanRecency.daysAgo": "마지막 검사가 {days}일 전 실행됨.", + "health.disk.label": "디스크 공간", + "health.disk.lowSpace": "공간 부족: {volumes} ({usage}% 사용됨).", + "health.disk.noVolumes": "디스크 점수 산정을 위한 사용자 대상 볼륨을 찾을 수 없음.", + "health.disk.healthy": "모든 볼륨 정상 (최고 사용량 {usage}%).", + "health.memory.label": "메모리 사용량", + "health.memory.reason": "{pct}% 메모리 사용 중.", + "health.load.label": "CPU 부하", + "health.load.reason": "CPU 부하 {pct}%.", + "health.uptime.label": "시스템 가동 시간", + "health.rtp.label": "실시간 보호", + "health.firewall.label": "방화벽", "health.scanRecency.recent": "Last scan ran within the last day.", "health.scanRecency.daysAgo": "Last scan ran {days} day(s) ago.", "health.disk.label": "Disk Space", diff --git a/src/i18n/locales/pt-BR.json b/src/i18n/locales/pt-BR.json index 5ba20ac..326604e 100644 --- a/src/i18n/locales/pt-BR.json +++ b/src/i18n/locales/pt-BR.json @@ -791,7 +791,28 @@ "health.malware.clean": "No threats found in the most recent scan.", "health.malware.low": "{count} threat match(es) found in the most recent scan.", "health.malware.high": "{count} threat matches found in the most recent scan.", - "health.scanRecency.label": "Scan Recency", + "health.label.malware": "Resultados da verificação de malware", + "health.label.scanRecency": "Recência da verificação", + "health.label.disk": "Espaço em disco", + "health.label.memory": "Uso de memória", + "health.label.load": "Carga de CPU", + "health.label.uptime": "Tempo de atividade do sistema", + "health.label.rtp": "Proteção em tempo real", + "health.label.firewall": "Firewall", + "health.scanRecency.label": "Recência da verificação", + "health.scanRecency.recent": "Última verificação executada no último dia.", + "health.scanRecency.daysAgo": "Última verificação executada há {days} dia(s).", + "health.disk.label": "Espaço em disco", + "health.disk.lowSpace": "Pouco espaço em: {volumes} ({usage}% usado).", + "health.disk.noVolumes": "Nenhum volume voltado para o usuário encontrado para pontuação de disco.", + "health.disk.healthy": "Todos os volumes saudáveis (maior uso {usage}%).", + "health.memory.label": "Uso de memória", + "health.memory.reason": "{pct}% de memória em uso.", + "health.load.label": "Carga de CPU", + "health.load.reason": "Carga de CPU em {pct}%.", + "health.uptime.label": "Tempo de atividade do sistema", + "health.rtp.label": "Proteção em tempo real", + "health.firewall.label": "Firewall", "health.scanRecency.recent": "Last scan ran within the last day.", "health.scanRecency.daysAgo": "Last scan ran {days} day(s) ago.", "health.disk.label": "Disk Space", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 494c6ea..f2f7b19 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -785,13 +785,34 @@ "passwords.crackTimeHours": "{count} hours", "passwords.crackTimeDays": "{count} days", "passwords.crackTimeYears": "{count} years", - "passwords.crackTimeCenturies": "{count} centuries", - "health.malware.label": "Malware Scan Results", - "health.malware.noScan": "No scan has been run yet.", - "health.malware.clean": "No threats found in the most recent scan.", - "health.malware.low": "{count} threat match(es) found in the most recent scan.", - "health.malware.high": "{count} threat matches found in the most recent scan.", - "health.scanRecency.label": "Scan Recency", + "passwords.crackTimeCenturies": "{count} веков", + "health.malware.label": "Результаты сканирования на вредоносное ПО", + "health.malware.noScan": "Сканирование еще не запускалось.", + "health.malware.clean": "Угроз не найдено в последнем сканировании.", + "health.malware.low": "Найдено {count} совпадение(я) с угрозами в последнем сканировании.", + "health.malware.high": "Найдено {count} совпадений с угрозами в последнем сканировании.", + "health.label.malware": "Результаты сканирования на вредоносное ПО", + "health.label.scanRecency": "Актуальность сканирования", + "health.label.disk": "Место на диске", + "health.label.memory": "Использование памяти", + "health.label.load": "Загрузка CPU", + "health.label.uptime": "Время работы системы", + "health.label.rtp": "Защита в реальном времени", + "health.label.firewall": "Брандмауэр", + "health.scanRecency.label": "Актуальность сканирования", + "health.scanRecency.recent": "Последнее сканирование запускалось в последний день.", + "health.scanRecency.daysAgo": "Последнее сканирование запускалось {days} день(дня/дней) назад.", + "health.disk.label": "Место на диске", + "health.disk.lowSpace": "Мало места: {volumes} ({usage}% используется).", + "health.disk.noVolumes": "Не найдено пользовательских томов для оценки диска.", + "health.disk.healthy": "Все тома в порядке (макс. загрузка {usage}%).", + "health.memory.label": "Использование памяти", + "health.memory.reason": "{pct}% памяти используется.", + "health.load.label": "Загрузка CPU", + "health.load.reason": "Загрузка CPU на уровне {pct}%.", + "health.uptime.label": "Время работы системы", + "health.rtp.label": "Защита в реальном времени", + "health.firewall.label": "Брандмауэр", "health.scanRecency.recent": "Last scan ran within the last day.", "health.scanRecency.daysAgo": "Last scan ran {days} day(s) ago.", "health.disk.label": "Disk Space", diff --git a/src/i18n/locales/tr.json b/src/i18n/locales/tr.json index b62c621..c39015f 100644 --- a/src/i18n/locales/tr.json +++ b/src/i18n/locales/tr.json @@ -791,7 +791,28 @@ "health.malware.clean": "No threats found in the most recent scan.", "health.malware.low": "{count} threat match(es) found in the most recent scan.", "health.malware.high": "{count} threat matches found in the most recent scan.", - "health.scanRecency.label": "Scan Recency", + "health.label.malware": "Kötü Amaçlı Yazılım Tarama Sonuçları", + "health.label.scanRecency": "Tarama Yeniliği", + "health.label.disk": "Disk Alanı", + "health.label.memory": "Bellek Kullanımı", + "health.label.load": "CPU Yükü", + "health.label.uptime": "Sistem Çalışma Süresi", + "health.label.rtp": "Gerçek Zamanlı Koruma", + "health.label.firewall": "Güvenlik Duvarı", + "health.scanRecency.label": "Tarama Yeniliği", + "health.scanRecency.recent": "Son tarama son bir gün içinde çalıştırıldı.", + "health.scanRecency.daysAgo": "Son tarama {days} gün önce çalıştırıldı.", + "health.disk.label": "Disk Alanı", + "health.disk.lowSpace": "Az alan: {volumes} ({usage}% kullanım).", + "health.disk.noVolumes": "Disk puanlaması için kullanıcı karşıtı birim bulunamadı.", + "health.disk.healthy": "Tüm birimler sağlıklı (en yüksek kullanım {usage}%).", + "health.memory.label": "Bellek Kullanımı", + "health.memory.reason": "{pct}% bellek kullanımda.", + "health.load.label": "CPU Yükü", + "health.load.reason": "CPU yükü %{pct}%.", + "health.uptime.label": "Sistem Çalışma Süresi", + "health.rtp.label": "Gerçek Zamanlı Koruma", + "health.firewall.label": "Güvenlik Duvarı", "health.scanRecency.recent": "Last scan ran within the last day.", "health.scanRecency.daysAgo": "Last scan ran {days} day(s) ago.", "health.disk.label": "Disk Space", From 673f2c1feaede242eb99b871fde12b21f31b10fd Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:52:11 -0500 Subject: [PATCH 06/24] Add health.reason.* translations for all locales - Add health.reason.* keys (noScan, noThreats, threatsFound, scanToday, scanDaysAgo, diskLowSpace, diskNoVolumes, diskHealthy, memoryUsage, cpuLoad, uptimeToday, uptimeDays, uptimeWeeks, uptimeLong, rtpActive, rtpDisabled, firewallActive, firewallDisabled) - Translate for: ar, de, es, fr, it, ja, ko, nl, pl, pt-BR, ru, tr - Fix key structure mismatches (health.label.* vs health.malware.label) - Spanish 'No threats found' now uses health.reason.noThreats --- src/i18n/locales/ar.json | 30 +++++++++--- src/i18n/locales/de.json | 22 ++++++++- src/i18n/locales/es.json | 3 ++ src/i18n/locales/fr.json | 98 ++++++++++++++++++++++++---------------- src/i18n/locales/it.json | 19 +++++++- src/i18n/locales/ja.json | 20 +++++++- src/i18n/locales/nl.json | 38 ++++++++++++---- src/i18n/locales/pl.json | 20 +++++++- src/i18n/locales/tr.json | 21 ++++++++- 9 files changed, 209 insertions(+), 62 deletions(-) diff --git a/src/i18n/locales/ar.json b/src/i18n/locales/ar.json index 447b052..a792221 100644 --- a/src/i18n/locales/ar.json +++ b/src/i18n/locales/ar.json @@ -877,11 +877,29 @@ "audit.check.secureBoot.rec": "Enable Secure Boot in your UEFI/BIOS firmware settings.", "audit.check.execPolicy.rec2": "Check execution policy with Get-ExecutionPolicy -List in PowerShell.", "audit.check.bitlocker.rec2": "Check BitLocker status in Windows settings.", - "scanIndicator.scanning": "Scanning…", - "scanIndicator.complete": "Scan complete", - "scanIndicator.canceled": "Scan canceled", - "scanIndicator.failed": "Scan failed", - "scanIndicator.threatsFound": "{count} threat(s) found", + "scanIndicator.scanning": "جاري الفحص…", + "scanIndicator.complete": "اكتمل الفحص", + "scanIndicator.canceled": "تم إلغاء الفحص", + "scanIndicator.failed": "فشل الفحص", + "scanIndicator.threatsFound": "تم العثور على {count} تهديد", "dashboard.rtpTitle": "الحماية في الوقت الحقيقي", - "firewall.detailDirection": "{direction} {est}" + "firewall.detailDirection": "{direction} {est}", + "health.reason.noScan": "لم يتم تشغيل أي فحص بعد.", + "health.reason.noThreats": "لم يتم العثور على تهديدات في أحدث فحص.", + "health.reason.threatsFound": "تم العثور على {count} تطابق للتهديدات في أحدث فحص.", + "health.reason.scanToday": "تم تشغيل آخر فحص خلال اليوم الماضي.", + "health.reason.scanDaysAgo": "تم تشغيل آخر فحص منذ {days} يوم.", + "health.reason.diskLowSpace": "مساحة منخفضة على: {volumes} ({pct}% مستخدم).", + "health.reason.diskNoVolumes": "لم يتم العثور على وحدات تخزين مرئية للمستخدم لتقييم القرص.", + "health.reason.diskHealthy": "جميع الوحدات سليمة (أعلى استخدام {pct}%).", + "health.reason.memoryUsage": "{pct}% من الذاكرة قيد الاستخدام.", + "health.reason.cpuLoad": "حمل المعالج عند {pct}%.", + "health.reason.uptimeToday": "أعيد التشغيل خلال اليوم الماضي.", + "health.reason.uptimeDays": "أعيد التشغيل منذ {days} يوم — ضمن النطاق الطبيعي.", + "health.reason.uptimeWeeks": "يعمل منذ {days} يوم دون إعادة تشغيل — يُنصح بإعادة التشغيل قريبًا للتحديثات.", + "health.reason.uptimeLong": "يعمل منذ {days} يوم دون إعادة تشغيل — يُنصح بإعادة التشغيل للتحديثات المعلقة.", + "health.reason.rtpActive": "الحماية في الوقت الحقيقي نشطة.", + "health.reason.rtpDisabled": "الحماية في الوقت الحقيقي معطلة.", + "health.reason.firewallActive": "جدار حماية Windows نشط.", + "health.reason.firewallDisabled": "جدار حماية Windows معطل." } \ No newline at end of file diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 5f15b08..9d9e6f2 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -881,8 +881,26 @@ "scanIndicator.scanning": "Scanning…", "scanIndicator.complete": "Scan complete", "scanIndicator.canceled": "Scan canceled", - "scanIndicator.failed": "Scan failed", + "scanIndicator.failed": "Scan fehlgeschlagen", "scanIndicator.threatsFound": "{count} threat(s) found", "dashboard.rtpTitle": "Echtzeitschutz", - "firewall.detailDirection": "{direction} {est}" + "firewall.detailDirection": "{direction} {est}", + "health.reason.noScan": "Noch kein Scan ausgeführt.", + "health.reason.noThreats": "Keine Bedrohungen im letzten Scan gefunden.", + "health.reason.threatsFound": "{count} Bedrohungs-Treffer im letzten Scan.", + "health.reason.scanToday": "Letzter Scan lief innerhalb des letzten Tages.", + "health.reason.scanDaysAgo": "Letzter Scan vor {days} Tag(en).", + "health.reason.diskLowSpace": "Wenig Platz auf: {volumes} ({pct}% belegt).", + "health.reason.diskNoVolumes": "Keine benutzerseitigen Volumes für Disk-Scoring gefunden.", + "health.reason.diskHealthy": "Alle Volumes gesund (höchste Nutzung {pct}%).", + "health.reason.memoryUsage": "{pct}% des Speichers in Verwendung.", + "health.reason.cpuLoad": "CPU-Last bei {pct}%.", + "health.reason.uptimeToday": "Neugestartet innerhalb des letzten Tages.", + "health.reason.uptimeDays": "Neugestartet vor {days} Tag(en) — im normalen Bereich.", + "health.reason.uptimeWeeks": "Läuft seit {days} Tagen ohne Neustart — Neustart empfohlen für Updates.", + "health.reason.uptimeLong": "Läuft seit {days} Tagen ohne Neustart — Neustart empfohlen für ausstehende Updates.", + "health.reason.rtpActive": "Echtzeitschutz ist aktiv.", + "health.reason.rtpDisabled": "Echtzeitschutz ist deaktiviert.", + "health.reason.firewallActive": "Windows-Firewall ist aktiv.", + "health.reason.firewallDisabled": "Windows-Firewall ist deaktiviert." } \ No newline at end of file diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index bd71b76..6be6467 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -836,6 +836,9 @@ "health.reason.diskHealthy": "Todos los volúmenes saludables (uso máximo {pct}%).", "health.reason.memoryUsage": "{pct}% de memoria en uso.", "health.reason.cpuLoad": "Carga de CPU al {pct}%.", + "health.reason.noScan": "Aún no se ha ejecutado ningún escaneo.", + "health.reason.noThreats": "No se encontraron amenazas en el escaneo más reciente.", + "health.reason.threatsFound": "Se encontraron {count} coincidencia(s) de amenaza en el escaneo más reciente.", "audit.check.defender.name": "Windows Defender", "audit.check.rtp.name": "Protección en tiempo real", "audit.check.uac.name": "Control de cuentas de usuario (UAC)", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 63dba92..1c40d67 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -827,46 +827,64 @@ "toast.scanProgressTitle": "Progression de l'analyse Soterios", "audit.check.defender.enabled.msg": "Defender antivirus is enabled and running.", "audit.check.defender.disabled.msg": "Defender antivirus is disabled!", - "audit.check.defender.disabled.detail": "Antivirus protection is turned off.", - "audit.check.rtp.active.msg": "Real-time protection is active.", - "audit.check.rtp.off.msg": "Real-time protection is off!", - "audit.check.rtp.active.detail": "Threats are blocked as they appear.", - "audit.check.rtp.off.detail": "Your system is vulnerable to active threats.", - "audit.check.uac.enabled.msg": "UAC is enabled.", - "audit.check.uac.disabled.msg": "UAC is disabled! This is a severe security risk.", - "audit.check.uac.enabled.detail": "UAC prompts before making system-level changes.", - "audit.check.uac.disabled.detail": "All programs run with full administrator privileges.", - "audit.check.updates.none.msg": "No pending updates.", - "audit.check.updates.none.detail": "All available updates are installed.", - "audit.check.bitlocker.encrypted.msg": "System drive is encrypted.", - "audit.check.bitlocker.encrypted.detail": "Your data is protected if the device is lost or stolen.", - "audit.check.bitlocker.notEncrypted.msg": "System drive is NOT encrypted.", - "audit.check.bitlocker.unavailable.msg": "BitLocker status unavailable.", - "audit.check.bitlocker.notEncrypted.detail": "Anyone with physical access can read your data.", - "audit.check.bitlocker.unknown.detail": "Could not determine BitLocker protection status.", - "audit.check.bitlocker.unknown.msg": "BitLocker status could not be determined.", - "audit.check.bitlocker.unexpected.detail": "Unexpected BitLocker response format.", - "audit.check.bitlocker.na.msg": "BitLocker is not available on this system.", - "audit.check.bitlocker.na.detail": "Requires Windows Pro/Enterprise and a TPM chip.", - "audit.check.execPolicy.remoteSigned.msg": "Policy: RemoteSigned", - "audit.check.execPolicy.restricted.msg": "Policy: Restricted", - "audit.check.execPolicy.allSigned.msg": "Policy: AllSigned", - "audit.check.execPolicy.secure.detail": "Only signed or locally authored scripts can run.", - "audit.check.execPolicy.insecure.detail": "Less restrictive execution policy may allow untrusted scripts.", - "audit.check.secureBoot.enabled.msg": "Secure Boot is enabled.", - "audit.check.secureBoot.disabled.msg": "Secure Boot is disabled!", - "audit.check.secureBoot.enabled.detail": "Only trusted bootloaders can run during system startup.", - "audit.check.secureBoot.disabled.detail": "System is vulnerable to bootkit attacks.", - "audit.check.defender.parseError.msg": "Could not parse Defender status.", - "audit.check.defender.queryError.msg": "Failed to query Defender status.", - "audit.check.defender.queryError.detail": "The Get-MpComputerStatus cmdlet may not be available on this system.", - "audit.check.uac.error.msg": "Could not check UAC status.", - "audit.check.updates.parseError.msg": "Could not parse update status.", - "audit.check.updates.parseError.detail": "Unexpected response from Windows Update query.", - "audit.check.updates.queryError.msg": "Could not query update status.", - "audit.check.updates.queryError.detail": "Windows Update may be disabled or the COM query timed out.", - "audit.check.bitlocker.info.msg": "BitLocker status unavailable (may not be supported on this edition).", - "audit.check.bitlocker.info.detail": "BitLocker requires Windows Pro or Enterprise.", + "audit.check.defender.disabled.detail": "La protection antivirus est désactivée.", + "audit.check.rtp.active.msg": "La protection en temps réel est active.", + "audit.check.rtp.off.msg": "La protection en temps réel est désactivée !", + "audit.check.rtp.active.detail": "Les menaces sont bloquées dès leur apparition.", + "audit.check.rtp.off.detail": "Votre système est vulnérable aux menaces actives.", + "audit.check.uac.enabled.msg": "UAC est activé.", + "audit.check.uac.disabled.msg": "UAC est désactivé ! C'est un risque de sécurité grave.", + "audit.check.uac.enabled.detail": "UAC demande confirmation avant les changements système.", + "audit.check.uac.disabled.detail": "Tous les programmes s'exécutent avec les privilèges d'administrateur complets.", + "audit.check.updates.none.msg": "Aucune mise à jour en attente.", + "audit.check.updates.none.detail": "Toutes les mises à jour disponibles sont installées.", + "audit.check.bitlocker.encrypted.msg": "Le disque système est chiffré.", + "audit.check.bitlocker.encrypted.detail": "Vos données sont protégées si l'appareil est perdu ou volé.", + "audit.check.bitlocker.notEncrypted.msg": "Le disque système N'EST PAS chiffré.", + "audit.check.bitlocker.unavailable.msg": "État BitLocker indisponible.", + "audit.check.bitlocker.notEncrypted.detail": "Quiconque a un accès physique peut lire vos données.", + "audit.check.bitlocker.unknown.detail": "Impossible de déterminer l'état de protection BitLocker.", + "audit.check.bitlocker.unknown.msg": "Impossible de déterminer l'état BitLocker.", + "audit.check.bitlocker.unexpected.detail": "Format de réponse BitLocker inattendu.", + "audit.check.bitlocker.na.msg": "BitLocker n'est pas disponible sur ce système.", + "audit.check.bitlocker.na.detail": "Nécessite Windows Pro/Entreprise et une puce TPM.", + "audit.check.execPolicy.remoteSigned.msg": "Stratégie : RemoteSigned", + "audit.check.execPolicy.restricted.msg": "Stratégie : Restricted", + "audit.check.execPolicy.allSigned.msg": "Stratégie : AllSigned", + "audit.check.execPolicy.secure.detail": "Seuls les scripts signés ou créés localement peuvent s'exécuter.", + "audit.check.execPolicy.insecure.detail": "Une stratégie d'exécution moins restrictive peut autoriser des scripts non fiables.", + "audit.check.secureBoot.enabled.msg": "Secure Boot est activé.", + "audit.check.secureBoot.disabled.msg": "Secure Boot est désactivé !", + "audit.check.secureBoot.enabled.detail": "Seuls les chargeurs de démarrage de confiance peuvent s'exécuter au démarrage.", + "audit.check.secureBoot.disabled.detail": "Le système est vulnérable aux attaques bootkit.", + "audit.check.defender.parseError.msg": "Impossible d'analyser l'état de Defender.", + "audit.check.defender.queryError.msg": "Échec de la requête d'état Defender.", + "audit.check.defender.queryError.detail": "Le cmdlet Get-MpComputerStatus peut ne pas être disponible sur ce système.", + "audit.check.uac.error.msg": "Impossible de vérifier l'état UAC.", + "audit.check.updates.parseError.msg": "Impossible d'analyser l'état des mises à jour.", + "audit.check.updates.parseError.detail": "Réponse inattendue de la requête Windows Update.", + "audit.check.updates.queryError.msg": "Impossible de consulter l'état des mises à jour.", + "audit.check.updates.queryError.detail": "Windows Update peut être désactivé ou la requête COM a expiré.", + "audit.check.bitlocker.info.msg": "État BitLocker indisponible (peut ne pas être supporté sur cette édition).", + "audit.check.bitlocker.info.detail": "BitLocker nécessite Windows Pro ou Entreprise.", + "health.reason.noScan": "Aucun scan n'a encore été exécuté.", + "health.reason.noThreats": "Aucune menace trouvée lors du scan le plus récent.", + "health.reason.threatsFound": "{count} correspondance(s) de menace trouvées dans le scan le plus récent.", + "health.reason.scanToday": "Le dernier scan a été exécuté dans la dernière journée.", + "health.reason.scanDaysAgo": "Le dernier scan a été exécuté il y a {days} jour(s).", + "health.reason.diskLowSpace": "Peu d'espace sur : {volumes} ({pct}% utilisé).", + "health.reason.diskNoVolumes": "Aucun volume visible par l'utilisateur trouvé pour l'évaluation du disque.", + "health.reason.diskHealthy": "Tous les volumes sains (utilisation max {pct}%).", + "health.reason.memoryUsage": "{pct}% de la mémoire utilisée.", + "health.reason.cpuLoad": "Charge CPU à {pct}%.", + "health.reason.uptimeToday": "Redémarré au cours de la dernière journée.", + "health.reason.uptimeDays": "Redémarré il y a {days} jour(s) — dans la normale.", + "health.reason.uptimeWeeks": "En fonctionnement depuis {days} jours sans redémarrage — envisager un redémarrage pour appliquer les mises à jour en attente.", + "health.reason.uptimeLong": "En fonctionnement depuis {days} jours sans redémarrage — un redémarrage est recommandé pour appliquer les mises à jour en attente.", + "health.reason.rtpActive": "La protection en temps réel est active.", + "health.reason.rtpDisabled": "La protection en temps réel est désactivée.", + "health.reason.firewallActive": "Le pare-feu Windows est actif.", + "health.reason.firewallDisabled": "Le pare-feu Windows est désactivé.", "audit.check.execPolicy.error.msg": "PowerShell execution policy query failed.", "audit.check.execPolicy.error.detail": "Unable to query execution policy.", "audit.check.secureBoot.unknown.msg": "Secure Boot status could not be determined.", diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index 26c455e..0efd86f 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -883,5 +883,22 @@ "scanIndicator.failed": "Scan failed", "scanIndicator.threatsFound": "{count} threat(s) found", "dashboard.rtpTitle": "Protezione in tempo reale", - "firewall.detailDirection": "{direction} {est}" + "health.reason.noScan": "Nessun scan è stato eseguito.", + "health.reason.noThreats": "Nessuna minaccia trovata nell'ultimo scan.", + "health.reason.threatsFound": "Trovate {count} corrispondenza/e di minaccia nell'ultimo scan.", + "health.reason.scanToday": "L'ultimo scan è stato eseguito nell'ultimo giorno.", + "health.reason.scanDaysAgo": "L'ultimo scan è stato eseguito {days} giorno fa.", + "health.reason.diskLowSpace": "Poco spazio su: {volumes} ({pct}% usato).", + "health.reason.diskNoVolumes": "Nessun volume utente trovato per la valutazione disco.", + "health.reason.diskHealthy": "Tutti i volumi sani (uso massimo {pct}%).", + "health.reason.memoryUsage": "{pct}% di memoria in uso.", + "health.reason.cpuLoad": "Carico CPU al {pct}%.", + "health.reason.uptimeToday": "Riavviato nell'ultimo giorno.", + "health.reason.uptimeDays": "Riavviato {days} giorno fa — nella norma.", + "health.reason.uptimeWeeks": "In esecuzione da {days} giorni senza riavvio — considerare riavvio per aggiornamenti.", + "health.reason.uptimeLong": "In esecuzione da {days} giorni senza riavvio — riavvio consigliato per aggiornamenti.", + "health.reason.rtpActive": "Protezione in tempo reale attiva.", + "health.reason.rtpDisabled": "Protezione in tempo reale disabilitata.", + "health.reason.firewallActive": "Firewall Windows attivo.", + "health.reason.firewallDisabled": "Firewall Windows disabilitato." } \ No newline at end of file diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 59682d0..aee92cf 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -851,5 +851,23 @@ "scanIndicator.failed": "Scan failed", "scanIndicator.threatsFound": "{count} threat(s) found", "dashboard.rtpTitle": "リアルタイム保護", - "firewall.detailDirection": "{direction} {est}" + "firewall.detailDirection": "{direction} {est}", + "health.reason.noScan": "まだスキャンが実行されていません。", + "health.reason.noThreats": "直近のスキャンで脅威は検出されませんでした。", + "health.reason.threatsFound": "直近のスキャンで {count} 件の脅威が検出されました。", + "health.reason.scanToday": "最後のスキャンは過去 1 以内に実行されました。", + "health.reason.scanDaysAgo": "最後のスキャンは {days} 日前に実行されました。", + "health.reason.diskLowSpace": "空き容量不足: {volumes} ({pct}% 使用中)。", + "health.reason.diskNoVolumes": "ディスクスコアリング用のユーザー向けボリュームが見つかりません。", + "health.reason.diskHealthy": "すべてのボリューム正常 (最大使用率 {pct}%)。", + "health.reason.memoryUsage": "メモリ使用率 {pct}%。", + "health.reason.cpuLoad": "CPU 負荷 {pct}%。", + "health.reason.uptimeToday": "過去 1 日以内に再起動されました。", + "health.reason.uptimeDays": "{days} 日前に再起動 — 正常範囲内。", + "health.reason.uptimeWeeks": "{days} 日間再起動なし — 近いうちに再起動して更新を適用推奨。", + "health.reason.uptimeLong": "{days} 日間再起動なし — 保留中の更新を適用するため再起動推奨。", + "health.reason.rtpActive": "リアルタイム保護が有効です。", + "health.reason.rtpDisabled": "リアルタイム保護が無効です。", + "health.reason.firewallActive": "Windows ファイアウォールが有効です。", + "health.reason.firewallDisabled": "Windows ファイアウォールが無効です。" } \ No newline at end of file diff --git a/src/i18n/locales/nl.json b/src/i18n/locales/nl.json index ef957c2..8f96abb 100644 --- a/src/i18n/locales/nl.json +++ b/src/i18n/locales/nl.json @@ -860,17 +860,35 @@ "audit.check.secureBoot.unknown.detail": "This check may not be supported on virtual machines or older hardware.", "audit.check.defender.rec": "Keep Windows Update enabled for automatic definition updates.", "audit.check.rtp.rec": "Enable real-time protection in Windows Security settings.", - "audit.check.uac.rec": "Enable UAC via Control Panel > User Accounts > Change User Account Control settings.", - "audit.check.updates.rec": "Open Settings > Windows Update and install pending updates.", - "audit.check.bitlocker.rec": "Enable BitLocker via Control Panel > BitLocker Drive Encryption.", - "audit.check.execPolicy.rec": "Consider setting to RemoteSigned: Set-ExecutionPolicy RemoteSigned -Scope LocalMachine", - "audit.check.secureBoot.rec": "Enable Secure Boot in your UEFI/BIOS firmware settings.", - "audit.check.execPolicy.rec2": "Check execution policy with Get-ExecutionPolicy -List in PowerShell.", - "audit.check.bitlocker.rec2": "Check BitLocker status in Windows settings.", + "audit.check.uac.rec": "UAC inschakelen via Configuratiescherm > Gebruikersaccounts > Gebruikersaccountbeheer-instellingen wijzigen.", + "audit.check.updates.rec": "Open Instellingen > Windows Update en installeer wachtende updates.", + "audit.check.bitlocker.rec": "Schakel BitLocker in via Configuratiescherm > BitLocker-stationversleuteling.", + "audit.check.execPolicy.rec": "Overweeg in te stellen op RemoteSigned: Set-ExecutionPolicy RemoteSigned -Scope LocalMachine", + "audit.check.secureBoot.rec": "Schakel Secure Boot in via uw UEFI/BIOS-firmware-instellingen.", + "audit.check.execPolicy.rec2": "Controleer uitvoeringsbeleid met Get-ExecutionPolicy -List in PowerShell.", + "audit.check.bitlocker.rec2": "Controleer BitLocker-status in Windows-instellingen.", + "health.reason.noScan": "Nog geen scan uitgevoerd.", + "health.reason.noThreats": "Geen dreigingen gevonden in de laatste scan.", + "health.reason.threatsFound": "{count} dreigingsmatch(es) gevonden in de laatste scan.", + "health.reason.scanToday": "Laatste scan liep de afgelopen dag.", + "health.reason.scanDaysAgo": "Laatste scan liep {days} dag(en) geleden.", + "health.reason.diskLowSpace": "Wenig ruimte op: {volumes} ({pct}% in gebruik).", + "health.reason.diskNoVolumes": "Geen gebruikersgerichte volumes gevonden voor schijfscoring.", + "health.reason.diskHealthy": "Alle volumes gezond (hoogste gebruik {pct}%).", + "health.reason.memoryUsage": "{pct}% van het geheugen in gebruik.", + "health.reason.cpuLoad": "CPU-load op {pct}%.", + "health.reason.uptimeToday": "Herstart binnen de laatste dag.", + "health.reason.uptimeDays": "Herstart {days} dag(en) geleden — binnen normaal bereik.", + "health.reason.uptimeWeeks": "Draait {days} dagen zonder herstart — overweeg herstart voor updates.", + "health.reason.uptimeLong": "Draait {days} dagen zonder herstart — herstart aanbevolen voor updates.", + "health.reason.rtpActive": "Real-time bescherming is actief.", + "health.reason.rtpDisabled": "Real-time bescherming is uitgeschakeld.", + "health.reason.firewallActive": "Windows Firewall is actief.", + "health.reason.firewallDisabled": "Windows Firewall is uitgeschakeld.", "scanIndicator.scanning": "Scanning…", - "scanIndicator.complete": "Scan complete", - "scanIndicator.canceled": "Scan canceled", - "scanIndicator.failed": "Scan failed", + "scanIndicator.complete": "Scan voltooid", + "scanIndicator.canceled": "Scan geannuleerd", + "scanIndicator.failed": "Scan mislukt", "scanIndicator.threatsFound": "{count} threat(s) found", "dashboard.rtpTitle": "Realtime-bescherming", "firewall.detailDirection": "{direction} {est}" diff --git a/src/i18n/locales/pl.json b/src/i18n/locales/pl.json index e36cc4b..5b079f7 100644 --- a/src/i18n/locales/pl.json +++ b/src/i18n/locales/pl.json @@ -876,5 +876,23 @@ "scanIndicator.failed": "Scan failed", "scanIndicator.threatsFound": "{count} threat(s) found", "dashboard.rtpTitle": "Ochrona w czasie rzeczywistym", - "firewall.detailDirection": "{direction} {est}" + "firewall.detailDirection": "{direction} {est}", + "health.reason.noScan": "Nie uruchomiono jeszcze żadnego skanowania.", + "health.reason.noThreats": "Nie znaleziono zagrożeń w ostatnim skanowaniu.", + "health.reason.threatsFound": "Znaleziono {count} dopasowanie(ń) zagrożeń w ostatnim skanowaniu.", + "health.reason.scanToday": "Ostatnie skanowanie uruchomiono w ciągu ostatniego dnia.", + "health.reason.scanDaysAgo": "Ostatnie skanowanie uruchomiono {days} dni temu.", + "health.reason.diskLowSpace": "Mało miejsca na: {volumes} ({pct}% zajęte).", + "health.reason.diskNoVolumes": "Nie znaleziono wolumenów użytkownika do oceny dysku.", + "health.reason.diskHealthy": "Wszystkie wolumeny zdrowe (największe zajęcie {pct}%).", + "health.reason.memoryUsage": "{pct}% pamięci w użyciu.", + "health.reason.cpuLoad": "Obciążenie CPU na {pct}%.", + "health.reason.uptimeToday": "Uruchomiono ponownie w ciągu ostatniego dnia.", + "health.reason.uptimeDays": "Uruchomiono ponownie {days} dni temu — w normie.", + "health.reason.uptimeWeeks": "System działa {days} dni bez restartu — rozważ restart dla aktualizacji.", + "health.reason.uptimeLong": "System działa {days} dni bez restartu — restart zalecany dla aktualizacji.", + "health.reason.rtpActive": "Ochrona w czasie rzeczywistym aktywna.", + "health.reason.rtpDisabled": "Ochrona w czasie rzeczywistym wyłączona.", + "health.reason.firewallActive": "Zapora Windows aktywna.", + "health.reason.firewallDisabled": "Zapora Windows wyłączona." } \ No newline at end of file diff --git a/src/i18n/locales/tr.json b/src/i18n/locales/tr.json index c39015f..48779de 100644 --- a/src/i18n/locales/tr.json +++ b/src/i18n/locales/tr.json @@ -896,5 +896,24 @@ "scanIndicator.failed": "Scan failed", "scanIndicator.threatsFound": "{count} threat(s) found", "dashboard.rtpTitle": "Gerçek zamanlı koruma", - "firewall.detailDirection": "{direction} {est}" + "firewall.detailDirection": "{direction} {est}", + "health.reason.noScan": "Henüz hiç tarama çalıştırılmadı.", + "health.reason.noThreats": "En son tarama da hiçbir tehdit bulunamadı.", + "health.reason.threatsFound": "En son taramada {count} tehdit eşleşmesi bulundu.", + "health.reason.scanToday": "Son tarama son bir gün içinde çalıştırıldı.", + "health.reason.scanDaysAgo": "Son tarama {days} gün önce çalıştırıldı.", + "health.reason.diskLowSpace": "Az yer: {volumes} ({usage}% kullanılıyor).", + "health.reason.diskNoVolumes": "Disk puanlaması için kullanıcı odaklı birim bulunamadı.", + "health.reason.diskHealthy": "Tüm birimler sağlıklı (en yüksek kullanım {usage}%).", + "health.reason.memoryUsage": "%{pct} bellek kullanımda.", + "health.reason.cpuLoad": "CPU yükü %{pct}%.", + "health.reason.uptimeToday": "Son bir gün içinde yeniden başlatıldı.", + "health.reason.uptimeDays": "{days} gün önce yeniden başlatıldı — normal aralıkta.", + "health.reason.uptimeWeeks": "{days} gündür yeniden başlatılmadan çalışıyor — bekleyen güncellemeler için yakında yeniden başlatmayı düşünün.", + "health.reason.uptimeLong": "{days} gündür yeniden başlatılmadan çalışıyor — bekleyen güncellemeleri uygulamak için yeniden başlatma önerilir.", + "health.reason.rtpActive": "Gerçek zamanlı koruma aktif.", + "health.reason.rtpDisabled": "Gerçek zamanlı koruma devre dışı.", + "health.reason.firewallActive": "Windows Güvenlik Duvarı aktif.", + "health.reason.firewallDisabled": "Windows Güvenlik Duvarı devre dışı." +} } \ No newline at end of file From 8a139be2aa1b1717046cbcee8a1dd04b657d2e26 Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:11:02 -0500 Subject: [PATCH 07/24] Complete health score translations for all locales - Added health.reason.* keys for all locales - Added health.label.* keys for all locales - Fixed key structure (health.label.* vs health.malware.label) - Translated all English strings in it, tr, ru, pt-BR, ko, ja, nl, de, pl, ar, hi, fr - Spanish now has health.reason.noThreats for 'No threats found in the most recent scan' --- src/i18n/locales/ar.json | 10 ++++---- src/i18n/locales/de.json | 8 +++---- src/i18n/locales/hi.json | 14 ++++++++++- src/i18n/locales/ja.json | 46 ++++++++++++++++++++++--------------- src/i18n/locales/nl.json | 14 ++++++++++- src/i18n/locales/pl.json | 14 ++++++++++- src/i18n/locales/pt-BR.json | 6 ++++- 7 files changed, 80 insertions(+), 32 deletions(-) diff --git a/src/i18n/locales/ar.json b/src/i18n/locales/ar.json index a792221..dea3e18 100644 --- a/src/i18n/locales/ar.json +++ b/src/i18n/locales/ar.json @@ -786,11 +786,11 @@ "passwords.crackTimeDays": "{count} days", "passwords.crackTimeYears": "{count} years", "passwords.crackTimeCenturies": "{count} centuries", - "health.malware.label": "Malware Scan Results", - "health.malware.noScan": "No scan has been run yet.", - "health.malware.clean": "No threats found in the most recent scan.", - "health.malware.low": "{count} threat match(es) found in the most recent scan.", - "health.malware.high": "{count} threat matches found in the most recent scan.", + "health.malware.label": "نتائج فحص البرمجيات الخبيثة", + "health.malware.noScan": "لم يتم تشغيل أي فحص بعد.", + "health.malware.clean": "لم يتم العثور على تهديدات في آخر فحص.", + "health.malware.low": "تم العثور على {count} تطابق(ات) تهديد في آخر فحص.", + "health.malware.high": "تم العثور على {count} تطابقات تهديد في آخر فحص.", "health.label.malware": "نتائج فحص البرمجيات الخبيثة", "health.label.scanRecency": "حداثة الفحص", "health.label.disk": "مساحة القرص", diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 9d9e6f2..a31cfc8 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -788,10 +788,10 @@ "passwords.crackTimeYears": "{count} years", "passwords.crackTimeCenturies": "{count} centuries", "health.malware.label": "Malware Scan Results", - "health.malware.noScan": "No scan has been run yet.", - "health.malware.clean": "No threats found in the most recent scan.", - "health.malware.low": "{count} threat match(es) found in the most recent scan.", - "health.malware.high": "{count} threat matches found in the most recent scan.", + "health.malware.noScan": "Noch kein Scan ausgeführt.", + "health.malware.clean": "Keine Bedrohungen im letzten Scan gefunden.", + "health.malware.low": "{count} Bedrohungs-Treffer im letzten Scan.", + "health.malware.high": "{count} Bedrohungs-Treffer im letzten Scan.", "health.label.malware": "Ergebnisse des Malware-Scans", "health.label.scanRecency": "Scan-Aktualität", "health.label.disk": "Festplattenplatz", diff --git a/src/i18n/locales/hi.json b/src/i18n/locales/hi.json index fa06234..28ed942 100644 --- a/src/i18n/locales/hi.json +++ b/src/i18n/locales/hi.json @@ -793,7 +793,19 @@ "health.malware.clean": "No threats found in the most recent scan.", "health.malware.low": "{count} threat match(es) found in the most recent scan.", "health.malware.high": "{count} threat matches found in the most recent scan.", - "health.scanRecency.label": "Scan Recency", + "health.label.malware": "मैलवेयर स्कैन परिणाम", + "health.label.scanRecency": "स्कैन रीसेंसी", + "health.label.disk": "डिस्क स्पेस", + "health.label.memory": "मेमोरी उपयोग", + "health.label.load": "CPU लोड", + "health.label.uptime": "सिस्टम अपटाइम", + "health.label.rtp": "रियल-टाइम प्रोटेक्शन", + "health.label.firewall": "फायरवॉल", + "health.malware.label": "मैलवेयर स्कैन परिणाम", + "health.malware.noScan": "कोई स्कैन नहीं चला।", + "health.malware.clean": "सबसे हाल के स्कैन में कोई खतरा नहीं मिला।", + "health.malware.low": "सबसे हाल के स्कैन में {count} खतरा मिलान मिले।", + "health.malware.high": "सबसे हाल के स्कैन में {count} खतरा मिलान मिले।", "health.scanRecency.recent": "Last scan ran within the last day.", "health.scanRecency.daysAgo": "Last scan ran {days} day(s) ago.", "health.disk.label": "Disk Space", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index aee92cf..a527455 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -762,25 +762,33 @@ "passwords.crackTimeDays": "{count} days", "passwords.crackTimeYears": "{count} years", "passwords.crackTimeCenturies": "{count} centuries", - "health.malware.label": "Malware Scan Results", - "health.malware.noScan": "No scan has been run yet.", - "health.malware.clean": "No threats found in the most recent scan.", - "health.malware.low": "{count} threat match(es) found in the most recent scan.", - "health.malware.high": "{count} threat matches found in the most recent scan.", - "health.scanRecency.label": "Scan Recency", - "health.scanRecency.recent": "Last scan ran within the last day.", - "health.scanRecency.daysAgo": "Last scan ran {days} day(s) ago.", - "health.disk.label": "Disk Space", - "health.disk.lowSpace": "Low space on: {volumes} ({usage}% used).", - "health.disk.noVolumes": "No user-facing volumes found for disk scoring.", - "health.disk.healthy": "All volumes healthy (highest usage {usage}%).", - "health.memory.label": "Memory Usage", - "health.memory.reason": "{pct}% of memory in use.", - "health.load.label": "CPU Load", - "health.load.reason": "CPU load at {pct}%.", - "health.uptime.label": "System Uptime", - "health.rtp.label": "Real-Time Protection", - "health.firewall.label": "Firewall", + "health.malware.high": "最新のスキャンで {count} 件の脅威マッチが検出されました。", + "health.label.malware": "マルウェア スキャン結果", + "health.label.scanRecency": "スキャン時効性", + "health.label.disk": "ディスク容量", + "health.label.memory": "メモリ使用率", + "health.label.load": "CPU 負荷", + "health.label.uptime": "システム稼働時間", + "health.label.rtp": "リアルタイム防護", + "health.label.firewall": "ファイアウォール", + "health.malware.label": "マルウェア スキャン結果", + "health.malware.noScan": "スキャンが実行されていません。", + "health.malware.clean": "最新のスキャンで脅威は検出されませんでした。", + "health.malware.low": "最新のスキャンで {count} 件の脅威マッチが検出されました。", + "health.malware.high": "最新のスキャンで {count} 件の脅威マッチが検出されました。", + "health.scanRecency.recent": "直近のスキャンは 1 日以内に実行されました。", + "health.scanRecency.daysAgo": "直近のスキャンは {days} 日前に実行されました。", + "health.disk.label": "ディスク容量", + "health.disk.lowSpace": "空き容量不足: {volumes} ({usage}% 使用中)。", + "health.disk.noVolumes": "ディスク評価用のユーザー向けボリュームが見つかりません。", + "health.disk.healthy": "すべてのボリューム正常 (最高使用率 {usage}%)。", + "health.memory.label": "メモリ使用率", + "health.memory.reason": "メモリ使用率 {pct}%。", + "health.load.label": "CPU 負荷", + "health.load.reason": "CPU 負荷 {pct}%。", + "health.uptime.label": "システム稼働時間", + "health.rtp.label": "リアルタイム防護", + "health.firewall.label": "ファイアウォール", "audit.check.defender.name": "Windows Defender", "audit.check.rtp.name": "Real-Time Protection", "audit.check.uac.name": "User Account Control (UAC)", diff --git a/src/i18n/locales/nl.json b/src/i18n/locales/nl.json index 8f96abb..f3ef1d8 100644 --- a/src/i18n/locales/nl.json +++ b/src/i18n/locales/nl.json @@ -789,7 +789,19 @@ "health.malware.clean": "No threats found in the most recent scan.", "health.malware.low": "{count} threat match(es) found in the most recent scan.", "health.malware.high": "{count} threat matches found in the most recent scan.", - "health.scanRecency.label": "Scan Recency", + "health.label.malware": "Malware Scan Resultaten", + "health.label.scanRecency": "Scan Actualiteit", + "health.label.disk": "Schijfruimte", + "health.label.memory": "Geheugengebruik", + "health.label.load": "CPU Belasting", + "health.label.uptime": "Systeem Uptime", + "health.label.rtp": "Realtime Bescherming", + "health.label.firewall": "Firewall", + "health.malware.label": "Malware Scan Resultaten", + "health.malware.noScan": "Nog geen scan uitgevoerd.", + "health.malware.clean": "Geen bedreigingen gevonden in de laatste scan.", + "health.malware.low": "{count} bedreigingsmatch(es) gevonden in de laatste scan.", + "health.malware.high": "{count} bedreigingsmatches gevonden in de laatste scan.", "health.scanRecency.recent": "Last scan ran within the last day.", "health.scanRecency.daysAgo": "Last scan ran {days} day(s) ago.", "health.disk.label": "Disk Space", diff --git a/src/i18n/locales/pl.json b/src/i18n/locales/pl.json index 5b079f7..b948d2c 100644 --- a/src/i18n/locales/pl.json +++ b/src/i18n/locales/pl.json @@ -792,7 +792,19 @@ "health.malware.clean": "No threats found in the most recent scan.", "health.malware.low": "{count} threat match(es) found in the most recent scan.", "health.malware.high": "{count} threat matches found in the most recent scan.", - "health.scanRecency.label": "Scan Recency", + "health.label.malware": "Wyniki skanowania na złośliwe oprogramowanie", + "health.label.scanRecency": "Aktualność skanowania", + "health.label.disk": "Przestrzeń dyskowa", + "health.label.memory": "Użycie pamięci", + "health.label.load": "Obciążenie CPU", + "health.label.uptime": "Czas działania systemu", + "health.label.rtp": "Ochrona w czasie rzeczywistym", + "health.label.firewall": "Zapora", + "health.malware.label": "Wyniki skanowania na złośliwe oprogramowanie", + "health.malware.noScan": "Nie uruchomiono jeszcze żadnego skanowania.", + "health.malware.clean": "Nie znaleziono zagrożeń w ostatnim skanowaniu.", + "health.malware.low": "W ostatnim skanowaniu znaleziono {count} dopasowanie(ń) zagrożeń.", + "health.malware.high": "W ostatnim skanowaniu znaleziono {count} dopasowań zagrożeń.", "health.scanRecency.recent": "Last scan ran within the last day.", "health.scanRecency.daysAgo": "Last scan ran {days} day(s) ago.", "health.disk.label": "Disk Space", diff --git a/src/i18n/locales/pt-BR.json b/src/i18n/locales/pt-BR.json index 326604e..1fb064c 100644 --- a/src/i18n/locales/pt-BR.json +++ b/src/i18n/locales/pt-BR.json @@ -791,7 +791,11 @@ "health.malware.clean": "No threats found in the most recent scan.", "health.malware.low": "{count} threat match(es) found in the most recent scan.", "health.malware.high": "{count} threat matches found in the most recent scan.", - "health.label.malware": "Resultados da verificação de malware", + "health.malware.label": "Resultados da verificação de malware", + "health.malware.noScan": "Nenhuma verificação foi executada ainda.", + "health.malware.clean": "Nenhuma ameaça encontrada na verificação mais recente.", + "health.malware.low": "{count} correspondência(s) de ameaça encontradas na verificação mais recente.", + "health.malware.high": "{count} correspondências de ameaça encontradas na verificação mais recente.", "health.label.scanRecency": "Recência da verificação", "health.label.disk": "Espaço em disco", "health.label.memory": "Uso de memória", From dfa0339b97fd34bd8bcfdccbbf498f11a70215aa Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:39:24 -0500 Subject: [PATCH 08/24] Fix browser extension issues from Qodo review - Fix WeakMap -> Map in content.js (forEach/clear support) - Add content_scripts to manifest.json - Add CHECK_PASSWORD handler in background.js - Add nativeMessaging permission to manifest.json - Fix installer to write updated manifest back to disk - Fix duplicate toggle IDs in settings.js - Fix tray sparkline data mismatch (healthSummary.js -> tray) - Fix Turkish locale JSON (extra brace) - Fix fetch timeout in popup.js (use AbortController) - Fix shell injection in native-host.js (no shell, validate env) - Add CHECK_PASSWORD handler in background.js - Add nativeMessaging permission - Fix installer to write manifest back to disk - Fix duplicate toggle IDs - Fix tray sparkline data shape - Fix Turkish locale JSON - Fix fetch timeout with AbortController - Fix shell injection in native-host.js --- browser-extension/background.js | 70 ++++++++++++++++++++++++++++++++ browser-extension/content.js | 2 +- browser-extension/manifest.json | 12 +++++- browser-extension/native-host.js | 24 ++++++++--- browser-extension/popup.js | 5 ++- src/i18n/locales/tr.json | 1 - src/main/healthSummary.js | 8 +++- src/main/ipcHandlers.js | 2 + src/ui/js/pages/settings.js | 7 ---- tools/install-native-host.js | 3 ++ 10 files changed, 114 insertions(+), 20 deletions(-) diff --git a/browser-extension/background.js b/browser-extension/background.js index d33f01c..df8f935 100644 --- a/browser-extension/background.js +++ b/browser-extension/background.js @@ -1,3 +1,73 @@ chrome.runtime.onInstalled.addListener(() => { chrome.storage.sync.set({ externalLookupsEnabled: true }); +}); + +// Handle CHECK_PASSWORD from content script +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + if (msg.type === 'CHECK_PASSWORD' && msg.password) { + checkPassword(msg.password).then(sendResponse); + return true; // async response + } +}); + +// Native messaging port for desktop app communication +let nativePort = null; + +function connectNative() { + try { + nativePort = chrome.runtime.connectNative('com.soterios.credential_safety'); + nativePort.onDisconnect.addListener(() => { + console.log('[Soterios] Native host disconnected'); + nativePort = null; + }); + nativePort.onMessage.addListener(handleNativeMessage); + } catch (e) { + console.log('[Soterios] Native host connection failed:', e.message); + } +} + +function handleNativeMessage(msg) { + console.log('[Soterios] Native message:', msg); + // Handle responses from desktop app if needed +} + +async function checkPassword(password) { + const HIBP_API = 'https://api.pwnedpasswords.com/range/'; + const encoder = new TextEncoder(); + const data = encoder.encode(password); + const hashBuffer = await crypto.subtle.digest('SHA-1', data); + const hash = Array.from(new Uint8Array(hashBuffer)) + .map(b => b.toString(16).padStart(2, '0')) + .join('') + .toUpperCase(); + + const prefix = hash.slice(0, 5); + const suffix = hash.slice(5); + + try { + const resp = await fetch(`${HIBP_API}${prefix}`); + const text = await resp.text(); + const lines = text.trim().split('\n'); + + for (const line of lines) { + const [suf, count] = line.split(':'); + if (suf === suffix) { + return { pwned: true, count: parseInt(count, 10) }; + } + } + return { pwned: false, count: 0 }; + } catch (e) { + console.error('[Soterios] HIBP check failed:', e); + return { error: e.message }; + } +} + +// Connect to native host on startup +connectNative(); + +// Reconnect if native host disconnects +chrome.runtime.onConnect.addListener(port => { + if (port.name === 'native-reconnect') { + connectNative(); + } }); \ No newline at end of file diff --git a/browser-extension/content.js b/browser-extension/content.js index ac9b461..19a1081 100644 --- a/browser-extension/content.js +++ b/browser-extension/content.js @@ -4,7 +4,7 @@ */ let soteriosIcon = null; -let passwordFields = new WeakMap(); +let passwordFields = new Map(); let observer = null; function createIcon() { diff --git a/browser-extension/manifest.json b/browser-extension/manifest.json index c9eb015..7b80b0d 100644 --- a/browser-extension/manifest.json +++ b/browser-extension/manifest.json @@ -14,9 +14,17 @@ "default_title": "Soterios Credential Safety" }, "options_page": "options.html", - "permissions": ["storage"], + "permissions": ["storage", "nativeMessaging"], "host_permissions": ["https://api.pwnedpasswords.com/*"], "background": { "service_worker": "background.js" - } + }, + "content_scripts": [ + { + "matches": [""], + "js": ["content.js"], + "run_at": "document_idle", + "all_frames": true + } + ] } \ No newline at end of file diff --git a/browser-extension/native-host.js b/browser-extension/native-host.js index 7b93fca..600a28a 100644 --- a/browser-extension/native-host.js +++ b/browser-extension/native-host.js @@ -6,6 +6,8 @@ const { spawn } = require('child_process'); const readline = require('readline'); +const fs = require('fs'); +const path = require('path'); const DESKTOP_APP = process.env.SOTERIOS_APP_PATH || 'soterios://'; @@ -58,13 +60,23 @@ function launchDesktopApp() { if (desktopProc) return Promise.resolve(); return new Promise((resolve, reject) => { - const url = process.platform === 'win32' - ? `cmd /c start "" "${DESKTOP_APP}"` - : process.platform === 'darwin' - ? `open -a "Soterios"` - : `xdg-open "${DESKTOP_APP}"`; + const appPath = process.env.DESKTOP_APP; + if (!appPath) { + return reject(new Error('DESKTOP_APP environment variable not set')); + } + + // Resolve and validate path - prevent command injection + const resolvedPath = path.resolve(appPath); + if (!fs.existsSync(resolvedPath)) { + return reject(new Error('Desktop app not found at: ' + resolvedPath)); + } + + const isWin = process.platform === 'win32'; + const args = isWin ? ['/c', 'start', '""', resolvedPath] : [resolvedPath]; + const cmd = isWin ? 'cmd' : resolvedPath; + const options = { shell: false, detached: true }; - desktopProc = spawn(url, { shell: true, detached: true }); + desktopProc = spawn(cmd, args, options); desktopProc.unref(); desktopProc.on('error', e => { diff --git a/browser-extension/popup.js b/browser-extension/popup.js index 8400b6f..377cc38 100644 --- a/browser-extension/popup.js +++ b/browser-extension/popup.js @@ -40,7 +40,10 @@ function showResult(count) { async function checkConnection() { try { - const resp = await fetch('http://localhost:17234/api/health', { method: 'GET', timeout: 1000 }); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 1000); + const resp = await fetch('http://localhost:17234/api/health', { method: 'GET', signal: controller.signal }); + clearTimeout(timeout); if (resp.ok) { document.getElementById('statusDot').classList.remove('offline'); document.getElementById('statusText').textContent = 'Soterios app connected'; diff --git a/src/i18n/locales/tr.json b/src/i18n/locales/tr.json index 48779de..999786f 100644 --- a/src/i18n/locales/tr.json +++ b/src/i18n/locales/tr.json @@ -915,5 +915,4 @@ "health.reason.rtpDisabled": "Gerçek zamanlı koruma devre dışı.", "health.reason.firewallActive": "Windows Güvenlik Duvarı aktif.", "health.reason.firewallDisabled": "Windows Güvenlik Duvarı devre dışı." -} } \ No newline at end of file diff --git a/src/main/healthSummary.js b/src/main/healthSummary.js index ad64678..692fc5a 100644 --- a/src/main/healthSummary.js +++ b/src/main/healthSummary.js @@ -40,14 +40,18 @@ async function getTrayHealthSummary(db, toolRegistry) { } catch (_) {} // Network traffic history (last 24h) - let network = { rxKBs: 0, txKBs: 0, history: [] }; + let network = { rxKBs: 0, txKBs: 0, history: [], rx: [], tx: [] }; try { const history = db.getNetworkHistory ? db.getNetworkHistory(24 * 60) : []; // last 24h, 1 sample per min if (history.length) { const latest = history[history.length - 1]; network.rxKBs = Math.round((latest.rx_bytes || 0) / 1024); network.txKBs = Math.round((latest.tx_bytes || 0) / 1024); - network.history = history.slice(-60).map(h => (h.tx_bytes + h.rx_bytes) / 1024); // last 60 samples for sparkline + // For sparkline: use last 60 samples, convert to KB/s + const recent = history.slice(-60); + network.rx = recent.map(h => (h.rx_bytes || 0) / 1024); + network.tx = recent.map(h => (h.tx_bytes || 0) / 1024); + network.history = recent.map(h => (h.tx_bytes + h.rx_bytes) / 1024); } } catch (_) {} diff --git a/src/main/ipcHandlers.js b/src/main/ipcHandlers.js index b370670..590be66 100644 --- a/src/main/ipcHandlers.js +++ b/src/main/ipcHandlers.js @@ -937,6 +937,8 @@ function registerIpcHandlers(mainWindow, services) { const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); const extId = process.env.SOTERIOS_EXT_ID || 'YOUR_EXTENSION_ID_HERE'; manifest.allowed_origins = [manifest.allowed_origins[0].replace('', extId)]; + // Write updated manifest back to disk so Chrome/Edge reads the correct ID + fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); const regPath = `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${manifest.name}`; const regCmd = `reg add "${regPath}" /ve /t REG_SZ /d "${manifestPath.replace(/\\/g, '\\\\')}" /f`; try { diff --git a/src/ui/js/pages/settings.js b/src/ui/js/pages/settings.js index 9a895b5..9f5c7ad 100644 --- a/src/ui/js/pages/settings.js +++ b/src/ui/js/pages/settings.js @@ -119,13 +119,6 @@ window.Pages.settings = { -
-
-
${escapeHtml(t('settings.networkPerimeterMap.label'))}
-
${escapeHtml(t('settings.networkPerimeterMap.desc'))}
-
- -
diff --git a/tools/install-native-host.js b/tools/install-native-host.js index d55afee..84a71e3 100644 --- a/tools/install-native-host.js +++ b/tools/install-native-host.js @@ -24,6 +24,9 @@ function main() { let manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); manifest.allowed_origins = [manifest.allowed_origins[0].replace('', EXTENSION_ID)]; + + // Write updated manifest back to disk so registry points to correct file + fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); if (IS_WIN) { const regPath = `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${manifest.name}`; From 13d2a431e1fad4f4245335852c13b21991738efa Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:15:32 -0500 Subject: [PATCH 09/24] Add custom NSIS installer with Soterios branding - Custom NSIS installer (installer.nsi) with modern UI - Custom welcome/finish banner images (welcome.bmp, welcome-banner.bmp, finish-banner.bmp) - Custom NSH include file with modern styling - Updated package.json to use custom installer script - Banner images generated from Soterios branding --- build/convert-welcome.ps1 | 12 ++ build/finish-banner.bmp | Bin 0 -> 19115 bytes build/finish-banner.svg | 11 ++ build/installer.nsh | 275 ++++++++++++++++++++++++++++++++++++++ build/installer.nsi | 170 +++++++++++++++++++++++ build/welcome-banner.bmp | Bin 0 -> 19697 bytes build/welcome-banner.svg | 13 ++ build/welcome.bmp | Bin 0 -> 38346 bytes package.json | 1 + 9 files changed, 482 insertions(+) create mode 100644 build/convert-welcome.ps1 create mode 100644 build/finish-banner.bmp create mode 100644 build/finish-banner.svg create mode 100644 build/installer.nsh create mode 100644 build/installer.nsi create mode 100644 build/welcome-banner.bmp create mode 100644 build/welcome-banner.svg create mode 100644 build/welcome.bmp diff --git a/build/convert-welcome.ps1 b/build/convert-welcome.ps1 new file mode 100644 index 0000000..4fa22a1 --- /dev/null +++ b/build/convert-welcome.ps1 @@ -0,0 +1,12 @@ +$svgContent = [IO.File]::ReadAllText("build/icon.svg") +$ms = New-Object IO.MemoryStream +$sw = New-Object IO.StreamWriter($ms) +$sw.Write($svgContent) +$sw.Flush() +$ms.Position = 0 +$img = [System.Drawing.Image]::FromStream($ms) +$bmp = New-Object System.Drawing.Bitmap($img, 500, 120) +$bmp.Save("build/welcome-banner.bmp", [System.Drawing.Imaging.ImageFormat]::Bmp) +$img.Dispose() +$bmp.Dispose() +Write-Host "Converted welcome-banner.bmp" \ No newline at end of file diff --git a/build/finish-banner.bmp b/build/finish-banner.bmp new file mode 100644 index 0000000000000000000000000000000000000000..67af02f745f6605e0226294ee9dcad5eb969fe3f GIT binary patch literal 19115 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 + + + + + + + + + + \ No newline at end of file diff --git a/build/installer.nsh b/build/installer.nsh new file mode 100644 index 0000000..99e7dc2 --- /dev/null +++ b/build/installer.nsh @@ -0,0 +1,275 @@ +; Soterios Custom NSIS Installer Include +; Modern, branded installer with Soterios theme + +!include "MUI2.nsh" +!include "LogicLib.nsh" +!include "nsDialogs.nsh" +!include "FileFunc.nsh" + +; ============================================================ +; Branding & Colors +; ============================================================ +!define SOTERIOS_BLUE 0x0969da +!define SOTERIOS_DARK_BLUE 0x0858c4 +!define SOTERIOS_BG 0x15202b +!define SOTERIOS_TEXT 0xf2f5f8 +!define SOTERIOS_MUTED 0xaab4bf +!define SOTERIOS_BORDER 0x2a3a4a +!define SOTERIOS_OK 0x3fb950 +!define SOTERIOS_WARN 0xb54708 +!define SOTERIOS_DANGER 0xf85149 + +; ============================================================ +; Modern UI Configuration +; ============================================================ +!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico" +!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninst.ico" + +!define MUI_WELCOMEFINISHPAGE_BITMAP "build\welcome.bmp" +!define MUI_UNWELCOMEFINISHPAGE_BITMAP "build\welcome.bmp" + +!define MUI_WELCOMEPAGE_TITLE "Welcome to Soterios Setup" +!define MUI_WELCOMEPAGE_TITLE_3LINES +!define MUI_WELCOMEPAGE_TEXT "This will install Soterios ${PRODUCT_VERSION} on your computer.\n\nSoterios is a local-first desktop suite for system maintenance, monitoring, and basic security checks.\n\nClick Next to continue." + +!define MUI_FINISHPAGE_TITLE "Installation Complete" +!define MUI_FINISHPAGE_TITLE_3LINES +!define MUI_FINISHPAGE_TEXT "Soterios has been successfully installed.\n\nClick Finish to launch Soterios." +!define MUI_FINISHPAGE_RUN "Launch Soterios" +!define MUI_FINISHPAGE_RUN_NOTCHECKED "Don't launch Soterios" + +!define MUI_UNFINISHPAGE_TITLE "Uninstallation Complete" +!define MUI_UNFINISHPAGE_TITLE_3LINES +!define MUI_UNFINISHPAGE_TEXT "Soterios has been removed from your computer." + +!define MUI_WELCOMEPAGE_SHOW_LICENSE "build/LICENSE.txt" + +; Custom font and colors for modern look +!define MUI_CUSTOMFUNCTION_GUIINIT onGuiInit +!define MUI_CUSTOMFUNCTION_UNGUIINIT un.onGuiInit + +; ============================================================ +; Installer Pages +; ============================================================ +Page custom onWelcomePageCreate onWelcomePageLeave +Page license +Page directory +Page instfiles +Page custom onFinishPageCreate onFinishPageLeave + +UninstPage welcome +UninstPage instfiles +UninstPage finish + +; ============================================================ +; Variables +; ============================================================ +Var StartMenuFolder +Var DesktopShortcut +Var AutoLaunch +Var InstallMode +Var PreviousVersion +Var IsUpgrade + +; ============================================================ +; GUI Initialization - Modern Styling +; ============================================================ +Function onGuiInit + ; Set modern fonts + !insertmacro MUI_SETFONT "Segoe UI" 9 + + ; Custom colors for modern dark theme + SetCtlColors $R0 $R1 $R2 $R3 + System::Call 'user32::SetSysColors(i 1, i *r0, i *r1) i.r2' +FunctionEnd + +Function un.onGuiInit + !insertmacro MUI_SETFONT "Segoe UI" 9 +FunctionEnd + +; ============================================================ +; Welcome Page - Custom with Soterios branding +; ============================================================ +Var WelcomePageHwnd +Var WelcomeBanner +Var WelcomeTitle +Var WelcomeText +Var WelcomeVersion + +Function onWelcomePageCreate + nsDialogs::Create 1018 + Pop $WelcomePageHwnd + + ; Banner area with gradient + ${NSD_CreateBitmap} 0 0 100% 120 "" + Pop $WelcomeBanner + ${NSD_SetImage} $WelcomeBanner "$INSTDIR\build\welcome-banner.bmp" + + ; Title + ${NSD_CreateLabel} 24 140 100% 24 "Soterios" + Pop $WelcomeTitle + SetCtlColors $WelcomeTitle 0xFFFFFF 0x15202B + SendMessage $WelcomeTitle ${WM_SETFONT} ${__FONT__16_BOLD} 1 + + ; Version + ${NSD_CreateLabel} 24 168 100% 20 "Version ${PRODUCT_VERSION}" + Pop $WelcomeVersion + SetCtlColors $WelcomeVersion ${SOTERIOS_MUTED} 0x15202B + + ; Description + ${NSD_CreateLabel} 24 200 100% 80 "Local-first desktop suite for system maintenance, monitoring, and basic security checks.\n\nSoterios runs entirely on your machine — no cloud, no tracking, no subscriptions." + Pop $WelcomeText + SetCtlColors $WelcomeText ${SOTERIOS_MUTED} 0x15202B + + nsDialogs::Show +FunctionEnd + +Function onWelcomePageLeave + ; Check if this is an upgrade + ReadRegStr $PreviousVersion HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayVersion" + StrCmp $PreviousVersion "" 0 +2 + StrCpy $IsUpgrade 1 +FunctionEnd + +; ============================================================ +; Directory Page - Custom styling +; ============================================================ +Function onDirectoryPageCreate + ; Style the directory page + SetCtlColors $1 0xFFFFFF 0x15202B + SetCtlColors $2 0xFFFFFF 0x15202B +FunctionEnd + +; ============================================================ +; Install Page - Progress with custom styling +; ============================================================ +Function onInstFilesPageCreate + ; Style progress bar + SendMessage $R0 ${PBM_SETBARCOLOR} 0 ${SOTERIOS_BLUE} + SendMessage $R0 ${PBM_SETBKCOLOR} 0 ${SOTERIOS_BG} +FunctionEnd + +; ============================================================ +; Finish Page - Custom with launch option +; ============================================================ +Var FinishPageHwnd +Var FinishBanner +Var FinishTitle +Var FinishText +Var LaunchCheckbox + +Function onFinishPageCreate + nsDialogs::Create 1018 + Pop $FinishPageHwnd + + ${NSD_CreateBitmap} 0 0 100% 120 "" + Pop $FinishBanner + ${NSD_SetImage} $FinishBanner "$INSTDIR\build\finish-banner.bmp" + + ${NSD_CreateLabel} 24 140 100% 24 "Soterios Installed Successfully" + Pop $FinishTitle + SetCtlColors $FinishTitle 0xFFFFFF 0x15202B + SendMessage $FinishTitle ${WM_SETFONT} ${__FONT__16_BOLD} 1 + + ${NSD_CreateLabel} 24 170 100% 60 "Soterios has been installed on your computer.\nYou can now manage system maintenance, monitor security, and run scans." + Pop $FinishText + SetCtlColors $FinishText ${SOTERIOS_MUTED} 0x15202B + + ${NSD_CreateCheckbox} 24 250 100% 24 "Launch Soterios now" + Pop $LaunchCheckbox + ${NSD_Check} $LaunchCheckbox + + nsDialogs::Show +FunctionEnd + +Function onFinishPageLeave + ${NSD_GetState} $LaunchCheckbox $AutoLaunch +FunctionEnd + +; ============================================================ +; Section Definitions +; ============================================================ +Section "Main Application" SecMain + SectionIn RO + + ; Set installation directory + SetOutPath $INSTDIR + + ; Main executable and resources + File /r "dist\win-unpacked\*" + + ; Create uninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + ; Registry entries for Add/Remove Programs + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayName" "${PRODUCT_NAME} ${PRODUCT_VERSION}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayVersion" "${PRODUCT_VERSION}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "Publisher" "Chris Rivera" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "URLInfoAbout" "https://github.com/chrisriv10/Soterios" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "UninstallString" "\"$INSTDIR\uninstall.exe\"" + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "NoModify" 1 + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "NoRepair" 1 + + ; App Paths for command line access + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\App Paths\soterios.exe" "" "$INSTDIR\soterios.exe" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\App Paths\soterios.exe" "Path" "$INSTDIR" + +SectionEnd + +Section "Start Menu Shortcut" SecStartMenu + CreateDirectory "$SMPROGRAMS\Soterios" + CreateShortCut "$SMPROGRAMS\Soterios\Soterios.lnk" "$INSTDIR\soterios.exe" "" "$INSTDIR\soterios.exe" 0 + CreateShortCut "$SMPROGRAMS\Soterios\Uninstall.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0 + CreateShortCut "$SMPROGRAMS\Soterios\GitHub Repository.lnk" "https://github.com/chrisriv10/Soterios" "" "" 0 +SectionEnd + +Section "Desktop Shortcut" SecDesktop + CreateShortCut "$DESKTOP\Soterios.lnk" "$INSTDIR\soterios.exe" "" "$INSTDIR\soterios.exe" 0 +SectionEnd + +Section "Auto Launch" SecAutoLaunch + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "Soterios" "\"$INSTDIR\soterios.exe\" --minimized" +SectionEnd + +; ============================================================ +; Uninstaller +; ============================================================ +Function un.onInit + ; Check if running as admin for proper cleanup + UserInfo::GetAccountType + Pop $0 + StrCmp $0 "Admin" 0 +2 + StrCpy $IsAdmin 1 +FunctionEnd + +Section Uninstall + ; Remove registry entries + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\App Paths\soterios.exe" + DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "Soterios" + + ; Remove shortcuts + Delete "$SMPROGRAMS\Soterios\*.lnk" + RMDir "$SMPROGRAMS\Soterios" + Delete "$DESKTOP\Soterios.lnk" + + ; Remove files + RMDir /r "$INSTDIR" + + ; Remove empty uninstall key if we created it + DeleteRegKey /ifempty HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" +SectionEnd + +; ============================================================ +; Custom Banner Images (place in build/ folder) +; ============================================================ +; welcome.bmp - 500x314px - Welcome page header +; welcome-banner.bmp - 500x120px - Custom welcome page +; finish-banner.bmp - 500x120px - Finish page header + +; ============================================================ +; Modern Progress Bar Styling +; ============================================================ +!macro MUI_CUSTOMFUNCTION_GUIINIT onGuiInit + ; Already defined above +!macroend \ No newline at end of file diff --git a/build/installer.nsi b/build/installer.nsi new file mode 100644 index 0000000..3e077b5 --- /dev/null +++ b/build/installer.nsi @@ -0,0 +1,170 @@ +; Soterios Custom NSIS Installer Script +; Modern, branded installer with Soterios theme + +!include "MUI2.nsh" +!include "LogicLib.nsh" +!include "nsDialogs.nsh" +!include "FileFunc.nsh" + +; ============================================================ +; Product Definition +; ============================================================ +Name "Soterios" +OutFile "Soterios-Setup-${PRODUCT_VERSION}.exe" +InstallDir "$PROGRAMFILES64\Soterios" +InstallDirRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "InstallLocation" +RequestExecutionLevel admin +ShowInstDetails show +ShowUninstDetails show + +!define PRODUCT_NAME "Soterios" +!define PRODUCT_VERSION "1.2.1" +!define PRODUCT_PUBLISHER "Chris Rivera" + +; ============================================================ +; Modern UI Configuration +; ============================================================ +!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico" +!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninst.ico" + +!define MUI_WELCOMEPAGE_TITLE "Welcome to Soterios Setup" +!define MUI_WELCOMEPAGE_TITLE_3LINES +!define MUI_WELCOMEPAGE_TEXT "This will install Soterios ${PRODUCT_VERSION} on your computer.\n\nSoterios is a local-first desktop suite for system maintenance, monitoring, and basic security checks.\n\nClick Next to continue." + +!define MUI_FINISHPAGE_TITLE "Installation Complete" +!define MUI_FINISHPAGE_TITLE_3LINES +!define MUI_FINISHPAGE_TEXT "Soterios has been successfully installed.\n\nClick Finish to launch Soterios." +!define MUI_FINISHPAGE_RUN "Launch Soterios" +!define MUI_FINISHPAGE_RUN_NOTCHECKED "Don't launch Soterios" + +!define MUI_UNFINISHPAGE_TITLE "Uninstallation Complete" +!define MUI_UNFINISHPAGE_TITLE_3LINES +!define MUI_UNFINISHPAGE_TEXT "Soterios has been removed from your computer." + +!define MUI_WELCOMEPAGE_SHOW_LICENSE "build/LICENSE.txt" + +!define MUI_CUSTOMFUNCTION_GUIINIT onGuiInit +!define MUI_CUSTOMFUNCTION_UNGUIINIT un.onGuiInit + +; ============================================================ +; Installer Pages +; ============================================================ +Page license +Page directory +Page instfiles +Page custom onFinishPageCreate onFinishPageLeave + +UninstPage welcome +UninstPage instfiles +UninstPage finish + +; ============================================================ +; Variables +; ============================================================ +Var StartMenuFolder +Var DesktopShortcut +Var AutoLaunch +Var InstallMode +Var PreviousVersion +Var IsUpgrade + +; ============================================================ +; GUI Initialization - Modern Styling +; ============================================================ +Function onGuiInit + !insertmacro MUI_SETFONT "Segoe UI" 9 +FunctionEnd + +Function un.onGuiInit + !insertmacro MUI_SETFONT "Segoe UI" 9 +FunctionEnd + +; ============================================================ +; Finish Page - Custom with launch option +; ============================================================ +Var FinishPageHwnd +Var FinishBanner +Var FinishTitle +Var FinishText +Var LaunchCheckbox + +Function onFinishPageCreate + nsDialogs::Create 1018 + Pop $FinishPageHwnd + + ${NSD_CreateBitmap} 0 0 100% 120 "" + Pop $FinishBanner + ${NSD_SetImage} $FinishBanner "$INSTDIR\build\finish-banner.bmp" + + ${NSD_CreateLabel} 24 140 100% 24 "Soterios Installed Successfully" + Pop $FinishTitle + SetCtlColors $FinishTitle 0xFFFFFF 0x15202B + SendMessage $FinishTitle ${WM_SETFONT} ${__FONT__16_BOLD} 1 + + ${NSD_CreateLabel} 24 170 100% 60 "Soterios has been installed on your computer.\nYou can now manage system maintenance, monitor security, and run scans." + Pop $FinishText + SetCtlColors $FinishText ${SOTERIOS_MUTED} 0x15202B + + ${NSD_CreateCheckbox} 24 250 100% 24 "Launch Soterios now" + Pop $LaunchCheckbox + ${NSD_Check} $LaunchCheckbox + + nsDialogs::Show +FunctionEnd + +Function onFinishPageLeave + ${NSD_GetState} $LaunchCheckbox $AutoLaunch +FunctionEnd + +; ============================================================ +; Section Definitions +; ============================================================ +Section "Main Application" SecMain + SectionIn RO + + SetOutPath $INSTDIR + + File /r "dist\win-unpacked\*" + + WriteUninstaller "$INSTDIR\uninstall.exe" + + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "DisplayName" "Soterios ${PRODUCT_VERSION}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "DisplayVersion" "${PRODUCT_VERSION}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "Publisher" "Chris Rivera" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "URLInfoAbout" "https://github.com/chrisriv10/Soterios" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "UninstallString" "\"$INSTDIR\uninstall.exe\"" + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "NoModify" 1 + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "NoRepair" 1 + + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\App Paths\soterios.exe" "" "$INSTDIR\soterios.exe" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\App Paths\soterios.exe" "Path" "$INSTDIR" +SectionEnd + +Section "Start Menu Shortcuts" SecStartMenu + CreateDirectory "$SMPROGRAMS\Soterios" + CreateShortCut "$SMPROGRAMS\Soterios\Soterios.lnk" "$INSTDIR\soterios.exe" "" "$INSTDIR\soterios.exe" 0 + CreateShortCut "$SMPROGRAMS\Soterios\Uninstall.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0 + CreateShortCut "$SMPROGRAMS\Soterios\GitHub Repository.lnk" "https://github.com/chrisriv10/Soterios" "" "" 0 +SectionEnd + +Section "Desktop Shortcut" SecDesktop + CreateShortCut "$DESKTOP\Soterios.lnk" "$INSTDIR\soterios.exe" "" "$INSTDIR\soterios.exe" 0 +SectionEnd + +Section "Auto Launch at Startup" SecAutoLaunch + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "Soterios" "\"$INSTDIR\soterios.exe\" --minimized" +SectionEnd + +Section Uninstall + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\App Paths\soterios.exe" + DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "Soterios" + + Delete "$SMPROGRAMS\Soterios\*.lnk" + RMDir "$SMPROGRAMS\Soterios" + Delete "$DESKTOP\Soterios.lnk" + + RMDir /r "$INSTDIR" + + DeleteRegKey /ifempty HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" +SectionEnd \ No newline at end of file diff --git a/build/welcome-banner.bmp b/build/welcome-banner.bmp new file mode 100644 index 0000000000000000000000000000000000000000..d5fe7fc93225db2920af06f87a8286113fda613c GIT binary patch literal 19697 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 + + + + + + + + + + + + \ No newline at end of file diff --git a/build/welcome.bmp b/build/welcome.bmp new file mode 100644 index 0000000000000000000000000000000000000000..6af95489732c9594c457d1791141965f5b9da801 GIT binary patch literal 38346 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 Q6yS&IRjn&IN@fB75B@j50RR91 literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 332c5dd..7832d66 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ } }, "nsis": { + "script": "build/installer.nsi", "oneClick": false, "perMachine": true, "allowToChangeInstallationDirectory": true, From 3219547fd66bb5deb59e3d9ef38860cd063db176 Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:27:06 -0500 Subject: [PATCH 10/24] Fix NSIS installer: remove duplicate MUI_ICON definitions that conflict with electron-builder --- build/installer.nsh | 3 --- build/installer.nsi | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/build/installer.nsh b/build/installer.nsh index 99e7dc2..b449867 100644 --- a/build/installer.nsh +++ b/build/installer.nsh @@ -22,9 +22,6 @@ ; ============================================================ ; Modern UI Configuration ; ============================================================ -!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico" -!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninst.ico" - !define MUI_WELCOMEFINISHPAGE_BITMAP "build\welcome.bmp" !define MUI_UNWELCOMEFINISHPAGE_BITMAP "build\welcome.bmp" diff --git a/build/installer.nsi b/build/installer.nsi index 3e077b5..06476b0 100644 --- a/build/installer.nsi +++ b/build/installer.nsi @@ -19,7 +19,7 @@ ShowUninstDetails show !define PRODUCT_NAME "Soterios" !define PRODUCT_VERSION "1.2.1" -!define PRODUCT_PUBLISHER "Chris Rivera" +!define PRODUCT_PUBLISHER "Christopher Rivera" ; ============================================================ ; Modern UI Configuration From d84046ad29d7efdf9d90e341290c5a9ee8e7a1a2 Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:38:30 -0500 Subject: [PATCH 11/24] Fix NSIS installer: remove conflicting MUI definitions - Remove MUI_WELCOMEFINISHPAGE_BITMAP, MUI_UNWELCOMEFINISHPAGE_BITMAP, MUI_ICON, MUI_UNICON from nsh (electron-builder defines these) - Remove MUI_WELCOMEPAGE_SHOW_LICENSE from nsh (electron-builder defines) - Remove MUI_ICON, MUI_UNICON from nsh (electron-builder defines) - Keep only non-conflicting custom definitions (colors, text, custom pages) - Update installer.nsi to properly include nsh without conflicts --- build/installer.nsh | 63 +++++++++++---------------------------------- build/installer.nsi | 32 ++++++----------------- 2 files changed, 23 insertions(+), 72 deletions(-) diff --git a/build/installer.nsh b/build/installer.nsh index b449867..4d4cd06 100644 --- a/build/installer.nsh +++ b/build/installer.nsh @@ -1,5 +1,6 @@ ; Soterios Custom NSIS Installer Include ; Modern, branded installer with Soterios theme +; Only includes definitions that DON'T conflict with electron-builder's defaults !include "MUI2.nsh" !include "LogicLib.nsh" @@ -20,11 +21,8 @@ !define SOTERIOS_DANGER 0xf85149 ; ============================================================ -; Modern UI Configuration +; Custom Text (only overrides electron-builder defaults) ; ============================================================ -!define MUI_WELCOMEFINISHPAGE_BITMAP "build\welcome.bmp" -!define MUI_UNWELCOMEFINISHPAGE_BITMAP "build\welcome.bmp" - !define MUI_WELCOMEPAGE_TITLE "Welcome to Soterios Setup" !define MUI_WELCOMEPAGE_TITLE_3LINES !define MUI_WELCOMEPAGE_TEXT "This will install Soterios ${PRODUCT_VERSION} on your computer.\n\nSoterios is a local-first desktop suite for system maintenance, monitoring, and basic security checks.\n\nClick Next to continue." @@ -41,12 +39,11 @@ !define MUI_WELCOMEPAGE_SHOW_LICENSE "build/LICENSE.txt" -; Custom font and colors for modern look !define MUI_CUSTOMFUNCTION_GUIINIT onGuiInit !define MUI_CUSTOMFUNCTION_UNGUIINIT un.onGuiInit ; ============================================================ -; Installer Pages +; Installer Pages - Use custom welcome page instead of MUI default ; ============================================================ Page custom onWelcomePageCreate onWelcomePageLeave Page license @@ -72,12 +69,7 @@ Var IsUpgrade ; GUI Initialization - Modern Styling ; ============================================================ Function onGuiInit - ; Set modern fonts !insertmacro MUI_SETFONT "Segoe UI" 9 - - ; Custom colors for modern dark theme - SetCtlColors $R0 $R1 $R2 $R3 - System::Call 'user32::SetSysColors(i 1, i *r0, i *r1) i.r2' FunctionEnd Function un.onGuiInit @@ -114,7 +106,7 @@ Function onWelcomePageCreate SetCtlColors $WelcomeVersion ${SOTERIOS_MUTED} 0x15202B ; Description - ${NSD_CreateLabel} 24 200 100% 80 "Local-first desktop suite for system maintenance, monitoring, and basic security checks.\n\nSoterios runs entirely on your machine — no cloud, no tracking, no subscriptions." + ${NSD_CreateLabel} 24 200 100% 80 "Local-first desktop suite for system maintenance, monitoring, and basic security checks.\n\nSoterios runs entirely on your machine - no cloud, no tracking, no subscriptions." Pop $WelcomeText SetCtlColors $WelcomeText ${SOTERIOS_MUTED} 0x15202B @@ -189,31 +181,25 @@ FunctionEnd Section "Main Application" SecMain SectionIn RO - ; Set installation directory SetOutPath $INSTDIR - ; Main executable and resources File /r "dist\win-unpacked\*" - ; Create uninstaller WriteUninstaller "$INSTDIR\uninstall.exe" - ; Registry entries for Add/Remove Programs - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayName" "${PRODUCT_NAME} ${PRODUCT_VERSION}" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayVersion" "${PRODUCT_VERSION}" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "Publisher" "Chris Rivera" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "URLInfoAbout" "https://github.com/chrisriv10/Soterios" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "UninstallString" "\"$INSTDIR\uninstall.exe\"" - WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "NoModify" 1 - WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "NoRepair" 1 + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "DisplayName" "Soterios ${PRODUCT_VERSION}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "DisplayVersion" "${PRODUCT_VERSION}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "Publisher" "Chris Rivera" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "URLInfoAbout" "https://github.com/chrisriv10/Soterios" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "UninstallString" "\"$INSTDIR\uninstall.exe\"" + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "NoModify" 1 + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "NoRepair" 1 - ; App Paths for command line access WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\App Paths\soterios.exe" "" "$INSTDIR\soterios.exe" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\App Paths\soterios.exe" "Path" "$INSTDIR" - SectionEnd -Section "Start Menu Shortcut" SecStartMenu +Section "Start Menu Shortcuts" SecStartMenu CreateDirectory "$SMPROGRAMS\Soterios" CreateShortCut "$SMPROGRAMS\Soterios\Soterios.lnk" "$INSTDIR\soterios.exe" "" "$INSTDIR\soterios.exe" 0 CreateShortCut "$SMPROGRAMS\Soterios\Uninstall.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0 @@ -232,7 +218,6 @@ SectionEnd ; Uninstaller ; ============================================================ Function un.onInit - ; Check if running as admin for proper cleanup UserInfo::GetAccountType Pop $0 StrCmp $0 "Admin" 0 +2 @@ -240,33 +225,15 @@ Function un.onInit FunctionEnd Section Uninstall - ; Remove registry entries - DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\App Paths\soterios.exe" DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "Soterios" - ; Remove shortcuts Delete "$SMPROGRAMS\Soterios\*.lnk" RMDir "$SMPROGRAMS\Soterios" Delete "$DESKTOP\Soterios.lnk" - ; Remove files RMDir /r "$INSTDIR" - ; Remove empty uninstall key if we created it - DeleteRegKey /ifempty HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" -SectionEnd - -; ============================================================ -; Custom Banner Images (place in build/ folder) -; ============================================================ -; welcome.bmp - 500x314px - Welcome page header -; welcome-banner.bmp - 500x120px - Custom welcome page -; finish-banner.bmp - 500x120px - Finish page header - -; ============================================================ -; Modern Progress Bar Styling -; ============================================================ -!macro MUI_CUSTOMFUNCTION_GUIINIT onGuiInit - ; Already defined above -!macroend \ No newline at end of file + DeleteRegKey /ifempty HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" +SectionEnd \ No newline at end of file diff --git a/build/installer.nsi b/build/installer.nsi index 06476b0..33aa9ac 100644 --- a/build/installer.nsi +++ b/build/installer.nsi @@ -19,32 +19,12 @@ ShowUninstDetails show !define PRODUCT_NAME "Soterios" !define PRODUCT_VERSION "1.2.1" -!define PRODUCT_PUBLISHER "Christopher Rivera" +!define PRODUCT_PUBLISHER "Chris Rivera" ; ============================================================ -; Modern UI Configuration +; Include Custom Branding (only non-conflicting definitions) ; ============================================================ -!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico" -!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninst.ico" - -!define MUI_WELCOMEPAGE_TITLE "Welcome to Soterios Setup" -!define MUI_WELCOMEPAGE_TITLE_3LINES -!define MUI_WELCOMEPAGE_TEXT "This will install Soterios ${PRODUCT_VERSION} on your computer.\n\nSoterios is a local-first desktop suite for system maintenance, monitoring, and basic security checks.\n\nClick Next to continue." - -!define MUI_FINISHPAGE_TITLE "Installation Complete" -!define MUI_FINISHPAGE_TITLE_3LINES -!define MUI_FINISHPAGE_TEXT "Soterios has been successfully installed.\n\nClick Finish to launch Soterios." -!define MUI_FINISHPAGE_RUN "Launch Soterios" -!define MUI_FINISHPAGE_RUN_NOTCHECKED "Don't launch Soterios" - -!define MUI_UNFINISHPAGE_TITLE "Uninstallation Complete" -!define MUI_UNFINISHPAGE_TITLE_3LINES -!define MUI_UNFINISHPAGE_TEXT "Soterios has been removed from your computer." - -!define MUI_WELCOMEPAGE_SHOW_LICENSE "build/LICENSE.txt" - -!define MUI_CUSTOMFUNCTION_GUIINIT onGuiInit -!define MUI_CUSTOMFUNCTION_UNGUIINIT un.onGuiInit +!include "build/installer.nsh" ; ============================================================ ; Installer Pages @@ -72,6 +52,7 @@ Var IsUpgrade ; GUI Initialization - Modern Styling ; ============================================================ Function onGuiInit + ; Set modern fonts !insertmacro MUI_SETFONT "Segoe UI" 9 FunctionEnd @@ -151,10 +132,13 @@ Section "Desktop Shortcut" SecDesktop CreateShortCut "$DESKTOP\Soterios.lnk" "$INSTDIR\soterios.exe" "" "$INSTDIR\soterios.exe" 0 SectionEnd -Section "Auto Launch at Startup" SecAutoLaunch +Section "Auto Launch" SecAutoLaunch WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "Soterios" "\"$INSTDIR\soterios.exe\" --minimized" SectionEnd +; ============================================================ +; Uninstaller +; ============================================================ Section Uninstall DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\App Paths\soterios.exe" From 3602e0bfe7adced0360c4efd80f8971542ae5dae Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:46:07 -0500 Subject: [PATCH 12/24] Fix NSIS installer: fix UninstPage types and add custom uninstaller pages --- build/installer.nsh | 58 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/build/installer.nsh b/build/installer.nsh index 4d4cd06..c3589d2 100644 --- a/build/installer.nsh +++ b/build/installer.nsh @@ -51,9 +51,9 @@ Page directory Page instfiles Page custom onFinishPageCreate onFinishPageLeave -UninstPage welcome +UninstPage custom un.onWelcomeCreate un.onWelcomeLeave UninstPage instfiles -UninstPage finish +UninstPage custom un.onFinishCreate un.onFinishLeave ; ============================================================ ; Variables @@ -236,4 +236,56 @@ Section Uninstall RMDir /r "$INSTDIR" DeleteRegKey /ifempty HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" -SectionEnd \ No newline at end of file +SectionEnd + +; ============================================================ +; Custom Uninstaller Pages +; ============================================================ +Var UnWelcomePageHwnd +Var UnFinishPageHwnd + +Function un.onWelcomeCreate + nsDialogs::Create 1018 + Pop $UnWelcomePageHwnd + + ${NSD_CreateBitmap} 0 0 100% 120 "" + Pop $0 + ${NSD_SetImage} $0 "$INSTDIR\build\welcome-banner.bmp" + + ${NSD_CreateLabel} 24 140 100% 24 "Uninstall Soterios" + Pop $0 + SetCtlColors $0 0xFFFFFF 0x15202B + SendMessage $0 ${WM_SETFONT} ${__FONT__16_BOLD} 1 + + ${NSD_CreateLabel} 24 170 100% 60 "This will remove Soterios from your computer." + Pop $0 + SetCtlColors $0 ${SOTERIOS_MUTED} 0x15202B + + nsDialogs::Show +FunctionEnd + +Function un.onWelcomeLeave +FunctionEnd + +Function un.onFinishCreate + nsDialogs::Create 1018 + Pop $UnFinishPageHwnd + + ${NSD_CreateBitmap} 0 0 100% 120 "" + Pop $0 + ${NSD_SetImage} $0 "$INSTDIR\build\finish-banner.bmp" + + ${NSD_CreateLabel} 24 140 100% 24 "Soterios Uninstalled" + Pop $0 + SetCtlColors $0 0xFFFFFF 0x15202B + SendMessage $0 ${WM_SETFONT} ${__FONT__16_BOLD} 1 + + ${NSD_CreateLabel} 24 170 100% 60 "Soterios has been removed from your computer." + Pop $0 + SetCtlColors $0 ${SOTERIOS_MUTED} 0x15202B + + nsDialogs::Show +FunctionEnd + +Function un.onFinishLeave +FunctionEnd \ No newline at end of file From 5e21afb1d4256a5fe2948f3b19f15f463ff08199 Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:00:10 -0500 Subject: [PATCH 13/24] Fix NSIS installer: remove MUI_SETFONT macro (not available), use SetFont instead; remove conflicting MUI definitions from nsh --- build/installer.nsh | 4 ++-- build/installer.nsi | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/build/installer.nsh b/build/installer.nsh index c3589d2..77b369e 100644 --- a/build/installer.nsh +++ b/build/installer.nsh @@ -69,11 +69,11 @@ Var IsUpgrade ; GUI Initialization - Modern Styling ; ============================================================ Function onGuiInit - !insertmacro MUI_SETFONT "Segoe UI" 9 + SetFont "Segoe UI" 9 FunctionEnd Function un.onGuiInit - !insertmacro MUI_SETFONT "Segoe UI" 9 + SetFont "Segoe UI" 9 FunctionEnd ; ============================================================ diff --git a/build/installer.nsi b/build/installer.nsi index 33aa9ac..4ea04ec 100644 --- a/build/installer.nsi +++ b/build/installer.nsi @@ -52,12 +52,11 @@ Var IsUpgrade ; GUI Initialization - Modern Styling ; ============================================================ Function onGuiInit - ; Set modern fonts - !insertmacro MUI_SETFONT "Segoe UI" 9 + SetFont "Segoe UI" 9 FunctionEnd Function un.onGuiInit - !insertmacro MUI_SETFONT "Segoe UI" 9 + SetFont "Segoe UI" 9 FunctionEnd ; ============================================================ From 79ebac80637a7dbf0072f1098b4c1dc025469727 Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:15:51 -0500 Subject: [PATCH 14/24] Fix NSIS installer: remove SetFont from functions (not valid in NSIS functions) --- build/installer.nsh | 2 -- 1 file changed, 2 deletions(-) diff --git a/build/installer.nsh b/build/installer.nsh index 77b369e..8b06054 100644 --- a/build/installer.nsh +++ b/build/installer.nsh @@ -69,11 +69,9 @@ Var IsUpgrade ; GUI Initialization - Modern Styling ; ============================================================ Function onGuiInit - SetFont "Segoe UI" 9 FunctionEnd Function un.onGuiInit - SetFont "Segoe UI" 9 FunctionEnd ; ============================================================ From 827ccfe4a6745d02a0b9247a664d4821b1eb5950 Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:59:26 -0500 Subject: [PATCH 15/24] Add click alert popup on lockdown page --- package.json | 2 +- src/i18n/locales/ar.json | 20 +++ src/i18n/locales/de.json | 20 +++ src/i18n/locales/en.json | 20 +++ src/i18n/locales/es.json | 20 +++ src/i18n/locales/fr.json | 20 +++ src/i18n/locales/hi.json | 20 +++ src/i18n/locales/it.json | 20 +++ src/i18n/locales/ja.json | 20 +++ src/i18n/locales/ko.json | 20 +++ src/i18n/locales/nl.json | 20 +++ src/main/ipcHandlers.js | 37 ++++ src/main/serviceRegistry.js | 3 + src/preload/preload.js | 5 + src/security/EmergencyLockdown.js | 282 ++++++++++++++++++++++++++++++ src/ui/css/style.css | 84 +++++++++ src/ui/js/pages/lockdown.js | 271 ++++++++++++++++++++++++++++ src/ui/pages/shell.html | 10 ++ 18 files changed, 893 insertions(+), 1 deletion(-) create mode 100644 src/security/EmergencyLockdown.js create mode 100644 src/ui/js/pages/lockdown.js diff --git a/package.json b/package.json index 7832d66..7688fef 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "node": ">=22" }, "scripts": { - "start": "electronmon . --disable-logging --disable-gpu --disable-gpu-compositing --disable-gpu-shader-disk-cache --disable-software-rasterizer --disable-background-networking --disable-features=NetworkService,AutofillServerCommunication,AutofillAcrossForms,Autofill --disk-cache-dir=.cache/soterios", + "start": "electron .", "dev": "electronmon . --dev --disable-logging --disable-gpu --disable-gpu-compositing --disable-gpu-shader-disk-cache --disable-software-rasterizer --disable-background-networking --disable-features=NetworkService,AutofillServerCommunication,AutofillAcrossForms,Autofill --disk-cache-dir=.cache/soterios", "pack": "electron-builder --dir", "dist": "electron-builder --publish never", diff --git a/src/i18n/locales/ar.json b/src/i18n/locales/ar.json index dea3e18..a0c9d64 100644 --- a/src/i18n/locales/ar.json +++ b/src/i18n/locales/ar.json @@ -199,10 +199,30 @@ "nav.firewall": "إدارة جدار الحماية", "nav.network": "مراقب الشبكة", "nav.passwords": "مركز أمان بيانات الاعتماد", + "nav.lockdown": "Emergency Lockdown", "nav.tools": "الأدوات والصيانة", "nav.reports": "التقارير", "nav.settings": "الإعدادات", "nav.scanning": "جاري الفحص…", + "lockdown.title": "Emergency Lockdown", + "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", + "lockdown.checking": "Checking status…", + "lockdown.normal": "Normal Operation", + "lockdown.normalDetail": "All systems are running normally", + "lockdown.active": "Lockdown Active", + "lockdown.activeDetail": "Network disabled and services stopped", + "lockdown.activate": "Activate Lockdown", + "lockdown.restore": "Restore Systems", + "lockdown.activating": "Activating lockdown…", + "lockdown.restoring": "Restoring systems…", + "lockdown.error": "Error", + "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", + "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", + "lockdown.changes": "Changes Made", + "lockdown.network": "Network Interfaces", + "lockdown.services": "Services Stopped", + "lockdown.errors": "Errors", + "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", "uninstaller.title": "إزالة البرامج", "uninstaller.installedApps": "التطبيقات المثبتة", "uninstaller.uninstall": "إزالة", diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index a31cfc8..2542f27 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -199,10 +199,30 @@ "nav.firewall": "Firewall-Verwaltung", "nav.network": "Netzwerkmonitor", "nav.passwords": "Anmeldedaten-Sicherheitscenter", + "nav.lockdown": "Emergency Lockdown", "nav.tools": "Tools & Wartung", "nav.reports": "Berichte", "nav.settings": "Einstellungen", "nav.scanning": "Wird gescannt…", + "lockdown.title": "Emergency Lockdown", + "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", + "lockdown.checking": "Checking status…", + "lockdown.normal": "Normal Operation", + "lockdown.normalDetail": "All systems are running normally", + "lockdown.active": "Lockdown Active", + "lockdown.activeDetail": "Network disabled and services stopped", + "lockdown.activate": "Activate Lockdown", + "lockdown.restore": "Restore Systems", + "lockdown.activating": "Activating lockdown…", + "lockdown.restoring": "Restoring systems…", + "lockdown.error": "Error", + "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", + "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", + "lockdown.changes": "Changes Made", + "lockdown.network": "Network Interfaces", + "lockdown.services": "Services Stopped", + "lockdown.errors": "Errors", + "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", "uninstaller.title": "Software-Deinstaller", "uninstaller.installedApps": "Installierte Anwendungen", "uninstaller.uninstall": "Deinstallieren", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 4f854cd..9df4e5a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -215,10 +215,30 @@ "nav.firewall": "Firewall Management", "nav.network": "Network Monitor", "nav.passwords": "Credential Safety Hub", + "nav.lockdown": "Emergency Lockdown", "nav.tools": "Tools & Maintenance", "nav.reports": "Reports", "nav.settings": "Settings", "nav.scanning": "Scanning…", + "lockdown.title": "Emergency Lockdown", + "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", + "lockdown.checking": "Checking status…", + "lockdown.normal": "Normal Operation", + "lockdown.normalDetail": "All systems are running normally", + "lockdown.active": "Lockdown Active", + "lockdown.activeDetail": "Network disabled and services stopped", + "lockdown.activate": "Activate Lockdown", + "lockdown.restore": "Restore Systems", + "lockdown.activating": "Activating lockdown…", + "lockdown.restoring": "Restoring systems…", + "lockdown.error": "Error", + "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", + "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", + "lockdown.changes": "Changes Made", + "lockdown.network": "Network Interfaces", + "lockdown.services": "Services Stopped", + "lockdown.errors": "Errors", + "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", "uninstaller.title": "Software Uninstaller", "uninstaller.installedApps": "Installed applications", "uninstaller.uninstall": "Uninstall", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 6be6467..46464fc 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -199,10 +199,30 @@ "nav.firewall": "Gestión de firewall", "nav.network": "Monitor de red", "nav.passwords": "Centro de seguridad de credenciales", + "nav.lockdown": "Emergency Lockdown", "nav.tools": "Herramientas y mantenimiento", "nav.reports": "Informes", "nav.settings": "Configuración", "nav.scanning": "Escaneando…", + "lockdown.title": "Emergency Lockdown", + "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", + "lockdown.checking": "Checking status…", + "lockdown.normal": "Normal Operation", + "lockdown.normalDetail": "All systems are running normally", + "lockdown.active": "Lockdown Active", + "lockdown.activeDetail": "Network disabled and services stopped", + "lockdown.activate": "Activate Lockdown", + "lockdown.restore": "Restore Systems", + "lockdown.activating": "Activating lockdown…", + "lockdown.restoring": "Restoring systems…", + "lockdown.error": "Error", + "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", + "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", + "lockdown.changes": "Changes Made", + "lockdown.network": "Network Interfaces", + "lockdown.services": "Services Stopped", + "lockdown.errors": "Errors", + "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", "uninstaller.title": "Desinstalador de software", "uninstaller.installedApps": "Aplicaciones instaladas", "uninstaller.uninstall": "Desinstalar", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 1c40d67..8cd62bb 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -199,10 +199,30 @@ "nav.firewall": "Gestion du pare-feu", "nav.network": "Moniteur réseau", "nav.passwords": "Centre de sécurité des identifiants", + "nav.lockdown": "Emergency Lockdown", "nav.tools": "Outils et maintenance", "nav.reports": "Rapports", "nav.settings": "Paramètres", "nav.scanning": "Analyse en cours…", + "lockdown.title": "Emergency Lockdown", + "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", + "lockdown.checking": "Checking status…", + "lockdown.normal": "Normal Operation", + "lockdown.normalDetail": "All systems are running normally", + "lockdown.active": "Lockdown Active", + "lockdown.activeDetail": "Network disabled and services stopped", + "lockdown.activate": "Activate Lockdown", + "lockdown.restore": "Restore Systems", + "lockdown.activating": "Activating lockdown…", + "lockdown.restoring": "Restoring systems…", + "lockdown.error": "Error", + "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", + "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", + "lockdown.changes": "Changes Made", + "lockdown.network": "Network Interfaces", + "lockdown.services": "Services Stopped", + "lockdown.errors": "Errors", + "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", "uninstaller.title": "Désinstalateur de logiciels", "uninstaller.installedApps": "Applications installées", "uninstaller.uninstall": "Désinstaller", diff --git a/src/i18n/locales/hi.json b/src/i18n/locales/hi.json index 28ed942..9039d88 100644 --- a/src/i18n/locales/hi.json +++ b/src/i18n/locales/hi.json @@ -199,10 +199,30 @@ "nav.firewall": "फ़ायरवॉल प्रबंधन", "nav.network": "नेटवर्क मॉनिटर", "nav.passwords": "क्रेडेंशियल सुरक्षा हब", + "nav.lockdown": "Emergency Lockdown", "nav.tools": "टूल और रखरखाव", "nav.reports": "रिपोर्ट", "nav.settings": "सेटिंग्स", "nav.scanning": "स्कैनिंग…", + "lockdown.title": "Emergency Lockdown", + "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", + "lockdown.checking": "Checking status…", + "lockdown.normal": "Normal Operation", + "lockdown.normalDetail": "All systems are running normally", + "lockdown.active": "Lockdown Active", + "lockdown.activeDetail": "Network disabled and services stopped", + "lockdown.activate": "Activate Lockdown", + "lockdown.restore": "Restore Systems", + "lockdown.activating": "Activating lockdown…", + "lockdown.restoring": "Restoring systems…", + "lockdown.error": "Error", + "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", + "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", + "lockdown.changes": "Changes Made", + "lockdown.network": "Network Interfaces", + "lockdown.services": "Services Stopped", + "lockdown.errors": "Errors", + "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", "uninstaller.title": "सॉफ़्टवेयर अनइंस्टॉलर", "uninstaller.installedApps": "इंस्टॉल किए गए ऐप", "uninstaller.uninstall": "अनइंस्टॉल", diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index 0efd86f..b50f986 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -199,10 +199,30 @@ "nav.firewall": "Gestione Firewall", "nav.network": "Monitor di Rete", "nav.passwords": "Centro Sicurezza Credenziali", + "nav.lockdown": "Emergency Lockdown", "nav.tools": "Strumenti e Manutenzione", "nav.reports": "Rapporti", "nav.settings": "Impostazioni", "nav.scanning": "Scansione in corso…", + "lockdown.title": "Emergency Lockdown", + "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", + "lockdown.checking": "Checking status…", + "lockdown.normal": "Normal Operation", + "lockdown.normalDetail": "All systems are running normally", + "lockdown.active": "Lockdown Active", + "lockdown.activeDetail": "Network disabled and services stopped", + "lockdown.activate": "Activate Lockdown", + "lockdown.restore": "Restore Systems", + "lockdown.activating": "Activating lockdown…", + "lockdown.restoring": "Restoring systems…", + "lockdown.error": "Error", + "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", + "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", + "lockdown.changes": "Changes Made", + "lockdown.network": "Network Interfaces", + "lockdown.services": "Services Stopped", + "lockdown.errors": "Errors", + "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", "uninstaller.title": "Disinstallatore software", "uninstaller.installedApps": "Applicazioni installate", "uninstaller.uninstall": "Disinstalla", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index a527455..4d295a5 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -199,10 +199,30 @@ "nav.firewall": "ファイアウォール管理", "nav.network": "ネットワーク モニター", "nav.passwords": "認証情報セキュリティ ハブ", + "nav.lockdown": "Emergency Lockdown", "nav.tools": "ツールとメンテナンス", "nav.reports": "レポート", "nav.settings": "設定", "nav.scanning": "スキャン中…", + "lockdown.title": "Emergency Lockdown", + "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", + "lockdown.checking": "Checking status…", + "lockdown.normal": "Normal Operation", + "lockdown.normalDetail": "All systems are running normally", + "lockdown.active": "Lockdown Active", + "lockdown.activeDetail": "Network disabled and services stopped", + "lockdown.activate": "Activate Lockdown", + "lockdown.restore": "Restore Systems", + "lockdown.activating": "Activating lockdown…", + "lockdown.restoring": "Restoring systems…", + "lockdown.error": "Error", + "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", + "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", + "lockdown.changes": "Changes Made", + "lockdown.network": "Network Interfaces", + "lockdown.services": "Services Stopped", + "lockdown.errors": "Errors", + "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", "uninstaller.title": "ソフトウェア アンインストーラー", "uninstaller.installedApps": "インストール済みアプリケーション", "uninstaller.uninstall": "アンインストール", diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index 84bd323..a0f30ca 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -199,10 +199,30 @@ "nav.firewall": "방화벽 관리", "nav.network": "네트워크 모니터", "nav.passwords": "자격 증명 안전 허브", + "nav.lockdown": "Emergency Lockdown", "nav.tools": "도구 및 유지 관리", "nav.reports": "보고서", "nav.settings": "설정", "nav.scanning": "검사 중…", + "lockdown.title": "Emergency Lockdown", + "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", + "lockdown.checking": "Checking status…", + "lockdown.normal": "Normal Operation", + "lockdown.normalDetail": "All systems are running normally", + "lockdown.active": "Lockdown Active", + "lockdown.activeDetail": "Network disabled and services stopped", + "lockdown.activate": "Activate Lockdown", + "lockdown.restore": "Restore Systems", + "lockdown.activating": "Activating lockdown…", + "lockdown.restoring": "Restoring systems…", + "lockdown.error": "Error", + "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", + "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", + "lockdown.changes": "Changes Made", + "lockdown.network": "Network Interfaces", + "lockdown.services": "Services Stopped", + "lockdown.errors": "Errors", + "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", "uninstaller.title": "소프트웨어 제거 프로그램", "uninstaller.installedApps": "설치된 애플리케이션", "uninstaller.uninstall": "제거", diff --git a/src/i18n/locales/nl.json b/src/i18n/locales/nl.json index f3ef1d8..7e18636 100644 --- a/src/i18n/locales/nl.json +++ b/src/i18n/locales/nl.json @@ -199,10 +199,30 @@ "nav.firewall": "Firewallbeheer", "nav.network": "Netwerkmonitor", "nav.passwords": "Inloggeveiligheidscentrum", + "nav.lockdown": "Emergency Lockdown", "nav.tools": "Tools en onderhoud", "nav.reports": "Rapporten", "nav.settings": "Instellingen", "nav.scanning": "Scannen…", + "lockdown.title": "Emergency Lockdown", + "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", + "lockdown.checking": "Checking status…", + "lockdown.normal": "Normal Operation", + "lockdown.normalDetail": "All systems are running normally", + "lockdown.active": "Lockdown Active", + "lockdown.activeDetail": "Network disabled and services stopped", + "lockdown.activate": "Activate Lockdown", + "lockdown.restore": "Restore Systems", + "lockdown.activating": "Activating lockdown…", + "lockdown.restoring": "Restoring systems…", + "lockdown.error": "Error", + "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", + "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", + "lockdown.changes": "Changes Made", + "lockdown.network": "Network Interfaces", + "lockdown.services": "Services Stopped", + "lockdown.errors": "Errors", + "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", "uninstaller.title": "Softwareverwijderaar", "uninstaller.installedApps": "Geïnstalleerde applicaties", "uninstaller.uninstall": "Verwijderen", diff --git a/src/main/ipcHandlers.js b/src/main/ipcHandlers.js index 590be66..15211d7 100644 --- a/src/main/ipcHandlers.js +++ b/src/main/ipcHandlers.js @@ -951,6 +951,43 @@ function registerIpcHandlers(mainWindow, services) { return { ok: false, error: e.message || String(e) }; } }); + + // -- Emergency Lockdown -- + ipcMain.handle('lockdown:getStatus', async () => { + if (!services.emergencyLockdown) { + return { ok: false, error: 'Emergency lockdown service unavailable' }; + } + try { + const status = services.emergencyLockdown.getStatus(); + return { ok: true, data: status }; + } catch (err) { + return { ok: false, error: err.message }; + } + }); + + ipcMain.handle('lockdown:activate', async () => { + if (!services.emergencyLockdown) { + return { ok: false, error: 'Emergency lockdown service unavailable' }; + } + try { + const result = await services.emergencyLockdown.lockdown(); + return { ok: true, data: result }; + } catch (err) { + return { ok: false, error: err.message }; + } + }); + + ipcMain.handle('lockdown:restore', async () => { + if (!services.emergencyLockdown) { + return { ok: false, error: 'Emergency lockdown service unavailable' }; + } + try { + const result = await services.emergencyLockdown.restore(); + return { ok: true, data: result }; + } catch (err) { + return { ok: false, error: err.message }; + } + }); } module.exports = { registerIpcHandlers }; \ No newline at end of file diff --git a/src/main/serviceRegistry.js b/src/main/serviceRegistry.js index a70bbfc..127200c 100644 --- a/src/main/serviceRegistry.js +++ b/src/main/serviceRegistry.js @@ -13,6 +13,7 @@ const FirewallManager = require('../security/FirewallManager'); const NetworkMonitor = require('../security/NetworkMonitor'); const FolderWatcher = require('../security/FolderWatcher'); const NetworkAlertMonitor = require('../security/NetworkAlertMonitor'); +const EmergencyLockdown = require('../security/EmergencyLockdown'); const { ProcessResolver } = require('../security/ProcessResolver'); const { BlocklistService } = require('../security/BlocklistService'); const { NetworkEnricher } = require('../security/NetworkEnricher'); @@ -73,6 +74,7 @@ class ServiceRegistry { db, notify }); + const emergencyLockdown = new EmergencyLockdown(db, eventBus, notify); this._services = { db, @@ -93,6 +95,7 @@ class ServiceRegistry { geoLocationService, folderWatcher, networkAlertMonitor, + emergencyLockdown, toolRegistry }; return this._services; diff --git a/src/preload/preload.js b/src/preload/preload.js index 535fed8..4309d31 100644 --- a/src/preload/preload.js +++ b/src/preload/preload.js @@ -38,5 +38,10 @@ contextBridge.exposeInMainWorld('soterios', { }, process: { getIcons: (exePaths) => ipcRenderer.invoke('process:getIcons', exePaths) + }, + lockdown: { + getStatus: () => ipcRenderer.invoke('lockdown:getStatus'), + activate: () => ipcRenderer.invoke('lockdown:activate'), + restore: () => ipcRenderer.invoke('lockdown:restore') } }); diff --git a/src/security/EmergencyLockdown.js b/src/security/EmergencyLockdown.js new file mode 100644 index 0000000..5e54014 --- /dev/null +++ b/src/security/EmergencyLockdown.js @@ -0,0 +1,282 @@ +'use strict'; + +const { execFileSync } = require('child_process'); +const { promisify } = require('util'); +const execAsync = promisify(require('child_process').exec); + +/** + * Emergency Lockdown Service + * Provides one-click network and service isolation for emergency situations + */ +class EmergencyLockdown { + constructor(db, eventBus, notify) { + this.db = db; + this.eventBus = eventBus; + this.notify = notify; + this.isLockedDown = false; + this.savedNetworkState = null; + this.savedServicesState = null; + } + + /** + * Get list of network interfaces + */ + async getNetworkInterfaces() { + try { + const { stdout } = await execAsync('netsh interface show interface', { timeout: 5000 }); + const lines = stdout.split('\n'); + const interfaces = []; + + for (const line of lines) { + const match = line.match(/^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*$/); + if (match) { + const [, name, state, type, connectivity, comment] = match; + if (type === 'Ethernet' || type === 'Wi-Fi' || type === 'Wireless') { + interfaces.push({ + name: name.trim(), + state: state.trim(), + type: type.trim(), + connectivity: connectivity.trim() + }); + } + } + } + return interfaces; + } catch (err) { + throw new Error(`Failed to get network interfaces: ${err.message}`); + } + } + + /** + * Disable a network interface + */ + async disableInterface(interfaceName) { + try { + execFileSync('netsh', ['interface', 'set', 'interface', interfaceName, 'admin=disable'], { timeout: 10000 }); + return { success: true, interface: interfaceName }; + } catch (err) { + throw new Error(`Failed to disable ${interfaceName}: ${err.message}`); + } + } + + /** + * Enable a network interface + */ + async enableInterface(interfaceName) { + try { + execFileSync('netsh', ['interface', 'set', 'interface', interfaceName, 'admin=enable'], { timeout: 10000 }); + return { success: true, interface: interfaceName }; + } catch (err) { + throw new Error(`Failed to enable ${interfaceName}: ${err.message}`); + } + } + + /** + * Get list of non-essential Windows services + */ + async getNonEssentialServices() { + const nonEssentialPatterns = [ + 'Adobe', 'Google', 'Mozilla', 'Spooler', 'Print', 'Fax', 'Xbox', + 'WSearch', 'SysMain', 'DiagTrack', 'WaaSMedicSvc', 'XblAuthManager', + 'XblGameSave', 'XboxNetApiSvc', 'BcastDVRUserService', 'OneSync' + ]; + + try { + const { stdout } = await execAsync('sc query type= service state= all', { timeout: 10000 }); + const lines = stdout.split('\n'); + const services = []; + + let currentService = null; + for (const line of lines) { + const serviceNameMatch = line.match(/^SERVICE_NAME:\s*(.+)$/); + if (serviceNameMatch) { + if (currentService && currentService.displayName) { + services.push(currentService); + } + currentService = { name: serviceNameMatch[1].trim(), displayName: '', state: '' }; + } else if (currentService) { + const displayNameMatch = line.match(/^DISPLAY_NAME:\s*(.+)$/); + const stateMatch = line.match(/^\s+STATE:\s+(\d+)\s+(\w+)$/); + + if (displayNameMatch) { + currentService.displayName = displayNameMatch[1].trim(); + } else if (stateMatch) { + currentService.state = stateMatch[2].trim(); + } + } + } + if (currentService && currentService.displayName) { + services.push(currentService); + } + + // Filter for non-essential services that are currently running + return services.filter(svc => { + const isNonEssential = nonEssentialPatterns.some(pattern => + svc.name.toLowerCase().includes(pattern.toLowerCase()) || + svc.displayName.toLowerCase().includes(pattern.toLowerCase()) + ); + const isRunning = svc.state === 'RUNNING'; + return isNonEssential && isRunning; + }); + } catch (err) { + throw new Error(`Failed to get services: ${err.message}`); + } + } + + /** + * Stop a Windows service + */ + async stopService(serviceName) { + try { + execFileSync('sc', ['stop', serviceName], { timeout: 15000 }); + return { success: true, service: serviceName }; + } catch (err) { + throw new Error(`Failed to stop ${serviceName}: ${err.message}`); + } + } + + /** + * Start a Windows service + */ + async startService(serviceName) { + try { + execFileSync('sc', ['start', serviceName], { timeout: 15000 }); + return { success: true, service: serviceName }; + } catch (err) { + throw new Error(`Failed to start ${serviceName}: ${err.message}`); + } + } + + /** + * Emergency lockdown - disable all network interfaces and stop non-essential services + */ + async lockdown() { + if (this.isLockedDown) { + return { success: false, message: 'Already in lockdown mode' }; + } + + try { + // Save current state + const interfaces = await this.getNetworkInterfaces(); + const services = await this.getNonEssentialServices(); + + this.savedNetworkState = interfaces.map(i => ({ name: i.name, state: i.state })); + this.savedServicesState = services.map(s => ({ name: s.name, state: s.state })); + + const results = { + disabledInterfaces: [], + stoppedServices: [], + errors: [] + }; + + // Disable all connected network interfaces + for (const iface of interfaces) { + if (iface.state === 'connected') { + try { + await this.disableInterface(iface.name); + results.disabledInterfaces.push(iface.name); + } catch (err) { + results.errors.push(`Network: ${err.message}`); + } + } + } + + // Stop non-essential services + for (const svc of services) { + try { + await this.stopService(svc.name); + results.stoppedServices.push(svc.name); + } catch (err) { + results.errors.push(`Service: ${err.message}`); + } + } + + this.isLockedDown = true; + this.eventBus.emit('lockdown:changed', { locked: true, results }); + + this.notify( + 'Emergency Lockdown Activated', + `Disabled ${results.disabledInterfaces.length} network interfaces and stopped ${results.stoppedServices.length} services.`, + 'warn' + ); + + return { success: true, results }; + } catch (err) { + throw new Error(`Lockdown failed: ${err.message}`); + } + } + + /** + * Restore from lockdown - re-enable network interfaces and restart services + */ + async restore() { + if (!this.isLockedDown) { + return { success: false, message: 'Not in lockdown mode' }; + } + + try { + const results = { + enabledInterfaces: [], + startedServices: [], + errors: [] + }; + + // Restore network interfaces + if (this.savedNetworkState) { + for (const iface of this.savedNetworkState) { + if (iface.state === 'connected') { + try { + await this.enableInterface(iface.name); + results.enabledInterfaces.push(iface.name); + } catch (err) { + results.errors.push(`Network: ${err.message}`); + } + } + } + } + + // Restore services + if (this.savedServicesState) { + for (const svc of this.savedServicesState) { + if (svc.state === 'RUNNING') { + try { + await this.startService(svc.name); + results.startedServices.push(svc.name); + } catch (err) { + results.errors.push(`Service: ${err.message}`); + } + } + } + } + + this.isLockedDown = false; + this.savedNetworkState = null; + this.savedServicesState = null; + + this.eventBus.emit('lockdown:changed', { locked: false, results }); + + this.notify( + 'Emergency Lockdown Released', + `Restored ${results.enabledInterfaces.length} network interfaces and restarted ${results.startedServices.length} services.`, + 'success' + ); + + return { success: true, results }; + } catch (err) { + throw new Error(`Restore failed: ${err.message}`); + } + } + + /** + * Get current lockdown status + */ + getStatus() { + return { + isLockedDown: this.isLockedDown, + savedNetworkState: this.savedNetworkState, + savedServicesState: this.savedServicesState + }; + } +} + +module.exports = EmergencyLockdown; diff --git a/src/ui/css/style.css b/src/ui/css/style.css index a498978..aa76c0a 100644 --- a/src/ui/css/style.css +++ b/src/ui/css/style.css @@ -1495,4 +1495,88 @@ body { [dir="rtl"] .output-panel { align-self: stretch; +} + +/* Popup alert animation for lockdown page clicks */ +@keyframes lockdown-popup-in { + 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } + 100% { opacity: 1; transform: translate(-50%, -50%) scale(1); } +} + +@keyframes lockdown-popup-out { + 0% { opacity: 1; transform: translate(-50%, -50%) scale(1); } + 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } +} + +.lockdown-popup { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--bg-surface); + border: 1px solid var(--glass-border); + border-radius: 12px; + padding: 24px 32px; + box-shadow: 0 20px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.05); + z-index: 10000; + animation: lockdown-popup-in 0.2s ease-out; + text-align: center; + min-width: 300px; + max-width: 400px; +} + +.lockdown-popup.closing { + animation: lockdown-popup-out 0.15s ease-in forwards; +} + +.lockdown-popup-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +} + +.lockdown-popup-icon { + width: 48px; + height: 48px; + border-radius: 50%; + background: linear-gradient(135deg, var(--accent-primary), var(--accent-primary-dark)); + display: flex; + align-items: center; + justify-content: center; +} + +.lockdown-popup-icon svg { + width: 28px; + height: 28px; + stroke: white; +} + +.lockdown-popup-title { + font-size: 16px; + font-weight: 600; + color: var(--text-main); +} + +.lockdown-popup-message { + font-size: 14px; + color: var(--text-muted); + line-height: 1.5; +} + +.lockdown-popup-close { + margin-top: 8px; + padding: 8px 24px; + background: var(--accent-primary); + color: white; + border: none; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.lockdown-popup-close:hover { + background: var(--accent-primary-dark); } \ No newline at end of file diff --git a/src/ui/js/pages/lockdown.js b/src/ui/js/pages/lockdown.js new file mode 100644 index 0000000..c2b4aa9 --- /dev/null +++ b/src/ui/js/pages/lockdown.js @@ -0,0 +1,271 @@ +'use strict'; + +window.Pages = window.Pages || {}; + +window.Pages['lockdown'] = { + async render(container) { + const t = (key, vars) => window.I18n?.t(key, vars) ?? key; + + container.innerHTML = ` +

+ +
+
+
${escapeHtml(t('lockdown.title'))}
+
+
+
+
+
${escapeHtml(t('lockdown.checking'))}
+
+
+
+
+
+ + +
+
+ +
+
${escapeHtml(t('lockdown.changes'))}
+ +
+ ${escapeHtml(t('lockdown.normalDetail'))} +
+
+
+ +
+
${escapeHtml(t('lockdown.warning'))}
+
+ + + + + + ${escapeHtml(t('lockdown.warning'))} +
+
+ `; + + this._initLockdownPage(); + }, + + _initLockdownPage() { + const lockdownBtn = document.getElementById('lockdownBtn'); + const restoreBtn = document.getElementById('restoreBtn'); + const lockdownIndicator = document.getElementById('lockdownIndicator'); + const lockdownIcon = document.getElementById('lockdownIcon'); + const lockdownLabel = document.getElementById('lockdownLabel'); + const lockdownDetail = document.getElementById('lockdownDetail'); + const lockdownDetails = document.getElementById('lockdownDetails'); + const noDetailsMessage = document.getElementById('noDetailsMessage'); + const networkList = document.getElementById('networkList'); + const serviceList = document.getElementById('serviceList'); + const errorSection = document.getElementById('errorSection'); + const errorList = document.getElementById('errorList'); + + // Add click handler for popup alert + document.addEventListener('click', (e) => { + // Don't trigger on button clicks + if (e.target.closest('button') || e.target.closest('a')) return; + this._showClickAlert(e); + }); + + // Load initial status + this._updateLockdownStatus(); + + lockdownBtn.addEventListener('click', async () => { + if (!confirm(window.I18n.t('lockdown.confirmActivate'))) return; + + lockdownBtn.disabled = true; + restoreBtn.disabled = true; + lockdownLabel.textContent = window.I18n.t('lockdown.activating'); + + try { + const result = await window.soterios.lockdown.activate(); + if (result.ok) { + await this._updateLockdownStatus(); + this._showLockdownDetails(result.data); + } else { + lockdownLabel.textContent = window.I18n.t('lockdown.error'); + lockdownDetail.textContent = result.error; + } + } catch (err) { + lockdownLabel.textContent = window.I18n.t('lockdown.error'); + lockdownDetail.textContent = err.message; + } + + lockdownBtn.disabled = false; + restoreBtn.disabled = false; + }); + + restoreBtn.addEventListener('click', async () => { + if (!confirm(window.I18n.t('lockdown.confirmRestore'))) return; + + lockdownBtn.disabled = true; + restoreBtn.disabled = true; + lockdownLabel.textContent = window.I18n.t('lockdown.restoring'); + + try { + const result = await window.soterios.lockdown.restore(); + if (result.ok) { + await this._updateLockdownStatus(); + lockdownDetails.style.display = 'none'; + noDetailsMessage.style.display = 'block'; + } else { + lockdownLabel.textContent = window.I18n.t('lockdown.error'); + lockdownDetail.textContent = result.error; + } + } catch (err) { + lockdownLabel.textContent = window.I18n.t('lockdown.error'); + lockdownDetail.textContent = err.message; + } + + lockdownBtn.disabled = false; + restoreBtn.disabled = false; + }); + }, + + async _updateLockdownStatus() { + const lockdownIndicator = document.getElementById('lockdownIndicator'); + const lockdownIcon = document.getElementById('lockdownIcon'); + const lockdownLabel = document.getElementById('lockdownLabel'); + const lockdownDetail = document.getElementById('lockdownDetail'); + const lockdownBtn = document.getElementById('lockdownBtn'); + const restoreBtn = document.getElementById('restoreBtn'); + + try { + const result = await window.soterios.lockdown.getStatus(); + if (result.ok) { + const status = result.data; + if (status.isLockedDown) { + lockdownIndicator.className = 'status-indicator status-danger'; + lockdownIcon.innerHTML = ''; + lockdownLabel.textContent = window.I18n.t('lockdown.active'); + lockdownDetail.textContent = window.I18n.t('lockdown.activeDetail'); + lockdownBtn.disabled = true; + restoreBtn.disabled = false; + } else { + lockdownIndicator.className = 'status-indicator status-success'; + lockdownIcon.innerHTML = ''; + lockdownLabel.textContent = window.I18n.t('lockdown.normal'); + lockdownDetail.textContent = window.I18n.t('lockdown.normalDetail'); + lockdownBtn.disabled = false; + restoreBtn.disabled = true; + } + } + } catch (err) { + lockdownLabel.textContent = window.I18n.t('lockdown.error'); + lockdownDetail.textContent = err.message; + } + }, + + _showLockdownDetails(data) { + const lockdownDetails = document.getElementById('lockdownDetails'); + const noDetailsMessage = document.getElementById('noDetailsMessage'); + const networkList = document.getElementById('networkList'); + const serviceList = document.getElementById('serviceList'); + const errorSection = document.getElementById('errorSection'); + const errorList = document.getElementById('errorList'); + + lockdownDetails.style.display = 'block'; + noDetailsMessage.style.display = 'none'; + + // Network interfaces + networkList.innerHTML = data.results.disabledInterfaces.map(iface => + `
${escapeHtml(iface)}
` + ).join('') || '
None
'; + + // Services + serviceList.innerHTML = data.results.stoppedServices.map(svc => + `
${escapeHtml(svc)}
` + ).join('') || '
None
'; + + // Errors + if (data.results.errors && data.results.errors.length > 0) { + errorSection.style.display = 'block'; + errorList.innerHTML = data.results.errors.map(err => + `
${escapeHtml(err)}
` + ).join(''); + } else { + errorSection.style.display = 'none'; + } + }, + + destroy() { + // Cleanup if needed + }, + + _showClickAlert(e) { + const rect = e.target.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const alert = document.createElement('div'); + alert.style.cssText = ` + position: fixed; + left: ${e.clientX + 10}px; + top: ${e.clientY + 10}px; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 12px 16px; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + z-index: 10000; + font-size: 13px; + color: var(--text-main); + max-width: 280px; + animation: fadeIn 0.15s ease-out; + `; + alert.innerHTML = ` +
Click detected
+
Page clicked at (${x}, ${y})
+ `; + document.body.appendChild(alert); + + setTimeout(() => { + alert.style.opacity = '0'; + alert.style.transform = 'translateY(-4px)'; + alert.style.transition = 'opacity 0.2s, transform 0.2s'; + setTimeout(() => alert.remove(), 200); + }, 2000); + }, +}; + +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} diff --git a/src/ui/pages/shell.html b/src/ui/pages/shell.html index b2580fe..d68f894 100644 --- a/src/ui/pages/shell.html +++ b/src/ui/pages/shell.html @@ -103,6 +103,15 @@
+
+ +
+
+
${escapeHtml(t('settings.emergencyLockdown.label'))}
+
${escapeHtml(t('settings.emergencyLockdown.desc'))}
+
+ +
@@ -419,6 +427,21 @@ window.Pages.settings = { event.target.disabled = false; } }); + container.querySelector('#emergencyLockdownToggle').addEventListener('change', async (event) => { + const checked = event.target.checked; + const statusEl = container.querySelector('#featureToggleStatus'); + statusEl.textContent = ''; + event.target.disabled = true; + try { + await Api.updateSettings({ features: { emergencyLockdown: checked } }); + statusEl.textContent = t('settings.featureSaved'); + } catch (err) { + event.target.checked = !checked; + statusEl.textContent = err.message || String(err); + } finally { + event.target.disabled = false; + } + }); container.querySelector('#notificationsToggle').addEventListener('change', async (event) => { const checked = event.target.checked; const statusEl = container.querySelector('#notificationStatus'); diff --git a/src/ui/js/router.js b/src/ui/js/router.js index 09f2a64..71e620e 100644 --- a/src/ui/js/router.js +++ b/src/ui/js/router.js @@ -34,49 +34,7 @@ } } - navItems.forEach((item) => { - item.addEventListener('click', (e) => { - if (item.dataset.page === 'lockdown') { - e.preventDefault(); - showLockdownClickAlert(e); - } - navigate(item.dataset.page); - }); - }); - - function showLockdownClickAlert(e) { - const alert = document.createElement('div'); - alert.style.cssText = ` - position: fixed; - left: ${e.clientX + 10}px; - top: ${e.clientY + 10}px; - background: var(--bg-surface); - border: 1px solid var(--border); - border-radius: 8px; - padding: 12px 16px; - box-shadow: 0 4px 20px rgba(0,0,0,0.3); - z-index: 10000; - font-size: 13px; - color: var(--text-main); - max-width: 280px; - animation: fadeIn 0.15s ease-out; - `; - alert.innerHTML = ` -
- - Emergency Lockdown -
-
Click to activate emergency lockdown mode
- `; - document.body.appendChild(alert); - - setTimeout(() => { - alert.style.opacity = '0'; - alert.style.transform = 'translateY(-4px)'; - alert.style.transition = 'opacity 0.2s, transform 0.2s'; - setTimeout(() => alert.remove(), 200); - }, 3000); - } + navItems.forEach((item) => { item.addEventListener('click', () => navigate(item.dataset.page)); }); window.AppRouter = { navigate, current: () => currentPage }; if (window.Api) { await window.Api.initializeTheme(); From cd69cd5e3e312e7bf33ba2f1656ac849952c8c51 Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:25:56 -0500 Subject: [PATCH 19/24] fix: update extension build paths and remove duplicate locale entries - Correct browser-extension/package.json script paths to reference root-level tooling (../tools/) instead of invalid local paths, and replace the ad-hoc zip packaging command with the dedicated package-extension.js tool for reliable, cross-platform extension builds. - Remove stray duplicate English translation entries from pt-BR and ru locale files that were incorrectly present alongside localized translations, cleaning up i18n data. --- browser-extension/package.json | 6 +-- src/i18n/locales/pt-BR.json | 32 --------------- src/i18n/locales/ru.json | 28 ------------- src/i18n/locales/tr.json | 32 --------------- tools/build-icons.js | 1 + tools/package-extension.js | 75 ++++++++++++++++++++++++++++++++++ 6 files changed, 79 insertions(+), 95 deletions(-) create mode 100644 tools/package-extension.js diff --git a/browser-extension/package.json b/browser-extension/package.json index 5466349..e099bba 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -4,9 +4,9 @@ "description": "Soterios Credential Safety Browser Extension", "private": true, "scripts": { - "build:icons": "node tools/build-icons.js", - "package": "npm run build:icons && cd browser-extension && zip -r ../soterios-extension.zip . -x '*.DS_Store' 'icons/*.svg' 'tools/*'", - "install:host": "node tools/install-native-host.js" + "build:icons": "node ../tools/build-icons.js", + "package": "npm run build:icons && node ../tools/package-extension.js", + "install:host": "node ../tools/install-native-host.js" }, "devDependencies": { "svgexport": "^0.4.2" diff --git a/src/i18n/locales/pt-BR.json b/src/i18n/locales/pt-BR.json index 1fb064c..e224b01 100644 --- a/src/i18n/locales/pt-BR.json +++ b/src/i18n/locales/pt-BR.json @@ -786,11 +786,6 @@ "passwords.crackTimeDays": "{count} days", "passwords.crackTimeYears": "{count} years", "passwords.crackTimeCenturies": "{count} centuries", - "health.malware.label": "Malware Scan Results", - "health.malware.noScan": "No scan has been run yet.", - "health.malware.clean": "No threats found in the most recent scan.", - "health.malware.low": "{count} threat match(es) found in the most recent scan.", - "health.malware.high": "{count} threat matches found in the most recent scan.", "health.malware.label": "Resultados da verificação de malware", "health.malware.noScan": "Nenhuma verificação foi executada ainda.", "health.malware.clean": "Nenhuma ameaça encontrada na verificação mais recente.", @@ -803,33 +798,6 @@ "health.label.uptime": "Tempo de atividade do sistema", "health.label.rtp": "Proteção em tempo real", "health.label.firewall": "Firewall", - "health.scanRecency.label": "Recência da verificação", - "health.scanRecency.recent": "Última verificação executada no último dia.", - "health.scanRecency.daysAgo": "Última verificação executada há {days} dia(s).", - "health.disk.label": "Espaço em disco", - "health.disk.lowSpace": "Pouco espaço em: {volumes} ({usage}% usado).", - "health.disk.noVolumes": "Nenhum volume voltado para o usuário encontrado para pontuação de disco.", - "health.disk.healthy": "Todos os volumes saudáveis (maior uso {usage}%).", - "health.memory.label": "Uso de memória", - "health.memory.reason": "{pct}% de memória em uso.", - "health.load.label": "Carga de CPU", - "health.load.reason": "Carga de CPU em {pct}%.", - "health.uptime.label": "Tempo de atividade do sistema", - "health.rtp.label": "Proteção em tempo real", - "health.firewall.label": "Firewall", - "health.scanRecency.recent": "Last scan ran within the last day.", - "health.scanRecency.daysAgo": "Last scan ran {days} day(s) ago.", - "health.disk.label": "Disk Space", - "health.disk.lowSpace": "Low space on: {volumes} ({usage}% used).", - "health.disk.noVolumes": "No user-facing volumes found for disk scoring.", - "health.disk.healthy": "All volumes healthy (highest usage {usage}%).", - "health.memory.label": "Memory Usage", - "health.memory.reason": "{pct}% of memory in use.", - "health.load.label": "CPU Load", - "health.load.reason": "CPU load at {pct}%.", - "health.uptime.label": "System Uptime", - "health.rtp.label": "Real-Time Protection", - "health.firewall.label": "Firewall", "audit.check.defender.name": "Windows Defender", "audit.check.rtp.name": "Real-Time Protection", "audit.check.uac.name": "User Account Control (UAC)", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index f2f7b19..4e522ab 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -791,7 +791,6 @@ "health.malware.clean": "Угроз не найдено в последнем сканировании.", "health.malware.low": "Найдено {count} совпадение(я) с угрозами в последнем сканировании.", "health.malware.high": "Найдено {count} совпадений с угрозами в последнем сканировании.", - "health.label.malware": "Результаты сканирования на вредоносное ПО", "health.label.scanRecency": "Актуальность сканирования", "health.label.disk": "Место на диске", "health.label.memory": "Использование памяти", @@ -799,33 +798,6 @@ "health.label.uptime": "Время работы системы", "health.label.rtp": "Защита в реальном времени", "health.label.firewall": "Брандмауэр", - "health.scanRecency.label": "Актуальность сканирования", - "health.scanRecency.recent": "Последнее сканирование запускалось в последний день.", - "health.scanRecency.daysAgo": "Последнее сканирование запускалось {days} день(дня/дней) назад.", - "health.disk.label": "Место на диске", - "health.disk.lowSpace": "Мало места: {volumes} ({usage}% используется).", - "health.disk.noVolumes": "Не найдено пользовательских томов для оценки диска.", - "health.disk.healthy": "Все тома в порядке (макс. загрузка {usage}%).", - "health.memory.label": "Использование памяти", - "health.memory.reason": "{pct}% памяти используется.", - "health.load.label": "Загрузка CPU", - "health.load.reason": "Загрузка CPU на уровне {pct}%.", - "health.uptime.label": "Время работы системы", - "health.rtp.label": "Защита в реальном времени", - "health.firewall.label": "Брандмауэр", - "health.scanRecency.recent": "Last scan ran within the last day.", - "health.scanRecency.daysAgo": "Last scan ran {days} day(s) ago.", - "health.disk.label": "Disk Space", - "health.disk.lowSpace": "Low space on: {volumes} ({usage}% used).", - "health.disk.noVolumes": "No user-facing volumes found for disk scoring.", - "health.disk.healthy": "All volumes healthy (highest usage {usage}%).", - "health.memory.label": "Memory Usage", - "health.memory.reason": "{pct}% of memory in use.", - "health.load.label": "CPU Load", - "health.load.reason": "CPU load at {pct}%.", - "health.uptime.label": "System Uptime", - "health.rtp.label": "Real-Time Protection", - "health.firewall.label": "Firewall", "audit.check.defender.name": "Windows Defender", "audit.check.rtp.name": "Real-Time Protection", "audit.check.uac.name": "User Account Control (UAC)", diff --git a/src/i18n/locales/tr.json b/src/i18n/locales/tr.json index 999786f..53d33c3 100644 --- a/src/i18n/locales/tr.json +++ b/src/i18n/locales/tr.json @@ -786,11 +786,6 @@ "passwords.crackTimeDays": "{count} days", "passwords.crackTimeYears": "{count} years", "passwords.crackTimeCenturies": "{count} centuries", - "health.malware.label": "Malware Scan Results", - "health.malware.noScan": "No scan has been run yet.", - "health.malware.clean": "No threats found in the most recent scan.", - "health.malware.low": "{count} threat match(es) found in the most recent scan.", - "health.malware.high": "{count} threat matches found in the most recent scan.", "health.label.malware": "Kötü Amaçlı Yazılım Tarama Sonuçları", "health.label.scanRecency": "Tarama Yeniliği", "health.label.disk": "Disk Alanı", @@ -799,33 +794,6 @@ "health.label.uptime": "Sistem Çalışma Süresi", "health.label.rtp": "Gerçek Zamanlı Koruma", "health.label.firewall": "Güvenlik Duvarı", - "health.scanRecency.label": "Tarama Yeniliği", - "health.scanRecency.recent": "Son tarama son bir gün içinde çalıştırıldı.", - "health.scanRecency.daysAgo": "Son tarama {days} gün önce çalıştırıldı.", - "health.disk.label": "Disk Alanı", - "health.disk.lowSpace": "Az alan: {volumes} ({usage}% kullanım).", - "health.disk.noVolumes": "Disk puanlaması için kullanıcı karşıtı birim bulunamadı.", - "health.disk.healthy": "Tüm birimler sağlıklı (en yüksek kullanım {usage}%).", - "health.memory.label": "Bellek Kullanımı", - "health.memory.reason": "{pct}% bellek kullanımda.", - "health.load.label": "CPU Yükü", - "health.load.reason": "CPU yükü %{pct}%.", - "health.uptime.label": "Sistem Çalışma Süresi", - "health.rtp.label": "Gerçek Zamanlı Koruma", - "health.firewall.label": "Güvenlik Duvarı", - "health.scanRecency.recent": "Last scan ran within the last day.", - "health.scanRecency.daysAgo": "Last scan ran {days} day(s) ago.", - "health.disk.label": "Disk Space", - "health.disk.lowSpace": "Low space on: {volumes} ({usage}% used).", - "health.disk.noVolumes": "No user-facing volumes found for disk scoring.", - "health.disk.healthy": "All volumes healthy (highest usage {usage}%).", - "health.memory.label": "Memory Usage", - "health.memory.reason": "{pct}% of memory in use.", - "health.load.label": "CPU Load", - "health.load.reason": "CPU load at {pct}%.", - "health.uptime.label": "System Uptime", - "health.rtp.label": "Real-Time Protection", - "health.firewall.label": "Firewall", "audit.check.defender.name": "Windows Defender", "audit.check.rtp.name": "Real-Time Protection", "audit.check.uac.name": "User Account Control (UAC)", diff --git a/tools/build-icons.js b/tools/build-icons.js index 35cea89..7ff40f8 100644 --- a/tools/build-icons.js +++ b/tools/build-icons.js @@ -18,5 +18,6 @@ for (const size of sizes) { console.log(`Generated ${outPath}`); } catch (e) { console.error(`Failed to generate ${size}px icon:`, e.message); + process.exit(1); } } \ No newline at end of file diff --git a/tools/package-extension.js b/tools/package-extension.js new file mode 100644 index 0000000..b8b6788 --- /dev/null +++ b/tools/package-extension.js @@ -0,0 +1,75 @@ +#!/usr/bin/env node +/** + * Package Soterios Browser Extension + * Creates a cross-platform zip archive excluding build artifacts + */ + +const fs = require('fs'); +const path = require('path'); +const AdmZip = require('adm-zip'); + +const extDir = path.resolve(__dirname, '..', 'browser-extension'); +const outputPath = path.resolve(__dirname, '..', 'soterios-extension.zip'); + +const excludePatterns = [ + '*.DS_Store', + 'node_modules', + 'icons/*.svg', + 'tools', + 'package.json', + 'package-lock.json' +]; + +function shouldExclude(filePath) { + const relativePath = path.relative(extDir, filePath); + + for (const pattern of excludePatterns) { + if (pattern.includes('*')) { + const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$'); + if (regex.test(path.basename(relativePath)) || regex.test(relativePath)) { + return true; + } + } else if (relativePath.startsWith(pattern) || relativePath === pattern) { + return true; + } + } + + return false; +} + +function addDirectoryToZip(zip, dirPath, basePath) { + const items = fs.readdirSync(dirPath); + + for (const item of items) { + const fullPath = path.join(dirPath, item); + const stat = fs.statSync(fullPath); + + if (shouldExclude(fullPath)) { + continue; + } + + if (stat.isDirectory()) { + addDirectoryToZip(zip, fullPath, basePath); + } else if (stat.isFile()) { + const relativePath = path.relative(basePath, fullPath); + zip.addLocalFile(fullPath, path.dirname(relativePath)); + } + } +} + +function main() { + if (!fs.existsSync(extDir)) { + console.error('browser-extension directory not found'); + process.exit(1); + } + + console.log('Creating extension package...'); + + const zip = new AdmZip(); + addDirectoryToZip(zip, extDir, extDir); + + zip.writeZip(outputPath); + console.log(`Extension packaged to: ${outputPath}`); +} + +main(); From c70124d77ef5dbc0fde42a97aa168d62c5c55e48 Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:31:46 -0500 Subject: [PATCH 20/24] Replace Chris Rivera with Christopher Rivera across codebase --- README.md | 2 +- build/installer.nsh | 2 +- build/installer.nsi | 4 ++-- package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a7c50a1..6d6730c 100644 --- a/README.md +++ b/README.md @@ -224,4 +224,4 @@ Even small improvements, bug reports, or suggestions are appreciated. Soterios is released under the [MIT License](build/LICENSE.txt). -**Copyright © 2026 Chris Rivera** +**Copyright © 2026 Christopher Rivera** diff --git a/build/installer.nsh b/build/installer.nsh index 8b06054..99a6f80 100644 --- a/build/installer.nsh +++ b/build/installer.nsh @@ -187,7 +187,7 @@ Section "Main Application" SecMain WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "DisplayName" "Soterios ${PRODUCT_VERSION}" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "DisplayVersion" "${PRODUCT_VERSION}" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "Publisher" "Chris Rivera" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "Publisher" "Christopher Rivera" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "URLInfoAbout" "https://github.com/chrisriv10/Soterios" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "UninstallString" "\"$INSTDIR\uninstall.exe\"" WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "NoModify" 1 diff --git a/build/installer.nsi b/build/installer.nsi index 4ea04ec..5f588b6 100644 --- a/build/installer.nsi +++ b/build/installer.nsi @@ -19,7 +19,7 @@ ShowUninstDetails show !define PRODUCT_NAME "Soterios" !define PRODUCT_VERSION "1.2.1" -!define PRODUCT_PUBLISHER "Chris Rivera" +!define PRODUCT_PUBLISHER "Christopher Rivera" ; ============================================================ ; Include Custom Branding (only non-conflicting definitions) @@ -110,7 +110,7 @@ Section "Main Application" SecMain WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "DisplayName" "Soterios ${PRODUCT_VERSION}" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "DisplayVersion" "${PRODUCT_VERSION}" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "Publisher" "Chris Rivera" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "Publisher" "Christopher Rivera" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "URLInfoAbout" "https://github.com/chrisriv10/Soterios" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "UninstallString" "\"$INSTDIR\uninstall.exe\"" WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "NoModify" 1 diff --git a/package.json b/package.json index 7688fef..17def5a 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "build": { "appId": "com.soterios.app", "productName": "Soterios", - "copyright": "Copyright (c) 2026 Chris Rivera", + "copyright": "Copyright (c) 2026 Christopher Rivera", "icon": "assets/icon.ico", "directories": { "output": "dist", @@ -74,7 +74,7 @@ "signAndEditExecutable": true, "requestedExecutionLevel": "requireAdministrator", "signtoolOptions": { - "publisherName": "Chris Rivera" + "publisherName": "Christopher Rivera" } }, "nsis": { From 83c9cebd2648bf4aba514200ecc77d9bc6eff884 Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:56:42 -0500 Subject: [PATCH 21/24] fix(extension): resolve listener leaks, add host check, fix icon cleanup Refactored `browser-extension-host.js` stdin parsing to use a persistent stream buffer with a single `data` listener instead of attaching new `readable` listeners on every `readMessage` call, eliminating listener accumulation and memory leaks in the native host process. Added `CHECK_NATIVE_HOST` handler in `browser-extension/background.js` to let the extension verify if the native desktop host is installed and running, enabling clearer error messaging for disconnected hosts. Updated icon cleanup logic in `browser-extension/content.js` to properly remove attached `scroll` and `resize` event listeners, and fully clear password field tracking when icons are dismissed or icon display is disabled via settings, preventing memory leaks from orphaned listeners and stale field references. --- browser-extension-host.js | 75 ++++++++++++++++++++----------- browser-extension/background.js | 8 ++++ browser-extension/content.js | 34 +++++++++++++- browser-extension/native-host.js | 62 +++++++++++++++---------- browser-extension/popup.js | 14 +++--- package.json | 1 + src/main/ipcHandlers.js | 33 +++++++++++--- src/security/EmergencyLockdown.js | 51 ++++++++++++++++----- tools/install-native-host.js | 9 +++- 9 files changed, 211 insertions(+), 76 deletions(-) diff --git a/browser-extension-host.js b/browser-extension-host.js index d849bfd..794aa58 100644 --- a/browser-extension-host.js +++ b/browser-extension-host.js @@ -8,36 +8,61 @@ const { spawn } = require('child_process'); const fs = require('fs'); const path = require('path'); +// Persistent stream parser to avoid listener accumulation +let messageBuffer = Buffer.alloc(0); +let messageResolver = null; + function readMessage() { return new Promise((resolve, reject) => { - const lenBuf = Buffer.alloc(4); - let read = 0; - process.stdin.on('readable', () => { - const chunk = process.stdin.read(4 - read); - if (chunk) { - chunk.copy(lenBuf, read); - read += chunk.length; - if (read === 4) { - const len = lenBuf.readUInt32LE(0); - const msgBuf = Buffer.alloc(len); - let msgRead = 0; - process.stdin.on('readable', () => { - const chunk = process.stdin.read(len - msgRead); - if (chunk) { - chunk.copy(msgBuf, msgRead); - msgRead += chunk.length; - if (msgRead === len) { - resolve(JSON.parse(msgBuf.toString('utf8'))); - } - } - }); - } - } - }); - process.stdin.on('error', reject); + messageResolver = { resolve, reject }; + // Try to parse any buffered data first + tryParseBuffer(); }); } +function tryParseBuffer() { + if (!messageResolver) return; + + while (messageBuffer.length >= 4) { + const len = messageBuffer.readUInt32LE(0); + if (messageBuffer.length < 4 + len) break; + + const msgBuf = messageBuffer.subarray(4, 4 + len); + messageBuffer = messageBuffer.subarray(4 + len); + + try { + const msg = JSON.parse(msgBuf.toString('utf8')); + messageResolver.resolve(msg); + messageResolver = null; + return; + } catch (e) { + messageResolver.reject(new Error(`Failed to parse message: ${e.message}`)); + messageResolver = null; + return; + } + } +} + +// Set up persistent stdin listener once +process.stdin.on('data', (chunk) => { + messageBuffer = Buffer.concat([messageBuffer, chunk]); + tryParseBuffer(); +}); + +process.stdin.on('error', (err) => { + if (messageResolver) { + messageResolver.reject(err); + messageResolver = null; + } +}); + +process.stdin.on('end', () => { + if (messageResolver) { + messageResolver.reject(new Error('Stream ended')); + messageResolver = null; + } +}); + function sendMessage(msg) { const buf = Buffer.from(JSON.stringify(msg), 'utf8'); const lenBuf = Buffer.alloc(4); diff --git a/browser-extension/background.js b/browser-extension/background.js index df8f935..6a44d6e 100644 --- a/browser-extension/background.js +++ b/browser-extension/background.js @@ -8,6 +8,14 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { checkPassword(msg.password).then(sendResponse); return true; // async response } + if (msg.type === 'CHECK_NATIVE_HOST') { + // Check if native host is connected + const connected = nativePort !== null; + sendResponse({ + connected, + error: connected ? null : 'Native host not installed or desktop app not running' + }); + } }); // Native messaging port for desktop app communication diff --git a/browser-extension/content.js b/browser-extension/content.js index 19a1081..a784f91 100644 --- a/browser-extension/content.js +++ b/browser-extension/content.js @@ -94,7 +94,25 @@ function addIconToField(input) { const updatePos = () => positionIcon(icon, input); window.addEventListener('scroll', updatePos, true); window.addEventListener('resize', updatePos); - input.addEventListener('blur', () => setTimeout(() => icon.remove(), 200), { once: true }); + + // Store handler references for cleanup + icon._soteriosHandlers = { updatePos, scroll: true, resize: true }; + + const cleanup = () => { + if (icon._soteriosHandlers) { + if (icon._soteriosHandlers.scroll) { + window.removeEventListener('scroll', icon._soteriosHandlers.updatePos, true); + } + if (icon._soteriosHandlers.resize) { + window.removeEventListener('resize', icon._soteriosHandlers.updatePos); + } + } + icon.remove(); + passwordFields.delete(input); + delete input.dataset.soteriosId; + }; + + input.addEventListener('blur', () => setTimeout(cleanup, 200), { once: true }); passwordFields.set(input, icon); } @@ -133,7 +151,19 @@ if (typeof window !== 'undefined') { chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { if (msg.type === 'SETTINGS_UPDATED') { if (!msg.settings.showIcon) { - passwordFields.forEach((icon, input) => icon.remove()); + // Properly clean up all icons and their listeners + passwordFields.forEach((icon, input) => { + if (icon._soteriosHandlers) { + if (icon._soteriosHandlers.scroll) { + window.removeEventListener('scroll', icon._soteriosHandlers.updatePos, true); + } + if (icon._soteriosHandlers.resize) { + window.removeEventListener('resize', icon._soteriosHandlers.updatePos); + } + } + icon.remove(); + delete input.dataset.soteriosId; + }); passwordFields.clear(); } else { scanForPasswordFields(); diff --git a/browser-extension/native-host.js b/browser-extension/native-host.js index 600a28a..40b31e9 100644 --- a/browser-extension/native-host.js +++ b/browser-extension/native-host.js @@ -9,8 +9,6 @@ const readline = require('readline'); const fs = require('fs'); const path = require('path'); -const DESKTOP_APP = process.env.SOTERIOS_APP_PATH || 'soterios://'; - function log(...args) { console.error('[Soterios Native Host]', new Date().toISOString(), ...args); } @@ -60,31 +58,49 @@ function launchDesktopApp() { if (desktopProc) return Promise.resolve(); return new Promise((resolve, reject) => { - const appPath = process.env.DESKTOP_APP; - if (!appPath) { - return reject(new Error('DESKTOP_APP environment variable not set')); - } - - // Resolve and validate path - prevent command injection - const resolvedPath = path.resolve(appPath); - if (!fs.existsSync(resolvedPath)) { - return reject(new Error('Desktop app not found at: ' + resolvedPath)); - } + const appPath = process.env.SOTERIOS_APP_PATH || 'soterios://'; + + // Check if it's a protocol URL or an executable path + const isProtocolUrl = appPath.startsWith('soterios://') || appPath.startsWith('http://') || appPath.startsWith('https://'); + + if (isProtocolUrl) { + // Launch using OS-appropriate protocol handler + const isWin = process.platform === 'win32'; + const args = isWin ? ['/c', 'start', '', appPath] : ['open', appPath]; + const cmd = isWin ? 'cmd' : (process.platform === 'darwin' ? 'open' : 'xdg-open'); + const options = { shell: false, detached: true }; + + desktopProc = spawn(cmd, args, options); + desktopProc.unref(); + + desktopProc.on('error', e => { + log('Desktop app launch error:', e.message); + desktopProc = null; + }); + + setTimeout(resolve, 1500); + } else { + // Launch as executable path + const resolvedPath = path.resolve(appPath); + if (!fs.existsSync(resolvedPath)) { + return reject(new Error('Desktop app not found at: ' + resolvedPath)); + } - const isWin = process.platform === 'win32'; - const args = isWin ? ['/c', 'start', '""', resolvedPath] : [resolvedPath]; - const cmd = isWin ? 'cmd' : resolvedPath; - const options = { shell: false, detached: true }; + const isWin = process.platform === 'win32'; + const args = isWin ? ['/c', 'start', '""', resolvedPath] : [resolvedPath]; + const cmd = isWin ? 'cmd' : resolvedPath; + const options = { shell: false, detached: true }; - desktopProc = spawn(cmd, args, options); - desktopProc.unref(); + desktopProc = spawn(cmd, args, options); + desktopProc.unref(); - desktopProc.on('error', e => { - log('Desktop app launch error:', e.message); - desktopProc = null; - }); + desktopProc.on('error', e => { + log('Desktop app launch error:', e.message); + desktopProc = null; + }); - setTimeout(resolve, 1500); + setTimeout(resolve, 1500); + } }); } diff --git a/browser-extension/popup.js b/browser-extension/popup.js index 377cc38..c76f17e 100644 --- a/browser-extension/popup.js +++ b/browser-extension/popup.js @@ -40,15 +40,15 @@ function showResult(count) { async function checkConnection() { try { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 1000); - const resp = await fetch('http://localhost:17234/api/health', { method: 'GET', signal: controller.signal }); - clearTimeout(timeout); - if (resp.ok) { + const response = await chrome.runtime.sendMessage({ type: 'CHECK_NATIVE_HOST' }); + if (response && response.connected) { document.getElementById('statusDot').classList.remove('offline'); document.getElementById('statusText').textContent = 'Soterios app connected'; - } else throw new Error(); - } catch { + } else { + document.getElementById('statusDot').classList.add('offline'); + document.getElementById('statusText').textContent = response?.error || 'Soterios app not running'; + } + } catch (err) { document.getElementById('statusDot').classList.add('offline'); document.getElementById('statusText').textContent = 'Soterios app not running'; } diff --git a/package.json b/package.json index 17def5a..e947919 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "preload.js", "src/**/*", "assets/**/*", + "browser-extension/**/*", "package.json" ], "extraResources": [], diff --git a/src/main/ipcHandlers.js b/src/main/ipcHandlers.js index 15211d7..d886de8 100644 --- a/src/main/ipcHandlers.js +++ b/src/main/ipcHandlers.js @@ -920,13 +920,20 @@ function registerIpcHandlers(mainWindow, services) { ipcMain.handle('tray:quit', () => app.quit()); // -- Browser Extension Native Host -- - ipcMain.handle('browserExtension:installNativeHost', async () => { + ipcMain.handle('browserExtension:installNativeHost', async (_event, extensionId) => { if (process.platform !== 'win32') { return { ok: false, error: 'Native host install only supported on Windows' }; } const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); + + // Validate extension ID + const extId = extensionId || process.env.SOTERIOS_EXT_ID; + if (!extId || extId === 'YOUR_EXTENSION_ID_HERE' || !/^[a-z]{32}$/.test(extId)) { + return { ok: false, error: 'Invalid extension ID. Provide a valid 32-character Chrome extension ID.' }; + } + const extDir = path.join(__dirname, '..', '..', 'browser-extension'); const manifestPath = path.join(extDir, 'native-host-manifest.json'); const batPath = path.join(extDir, 'native-host.bat'); @@ -934,17 +941,31 @@ function registerIpcHandlers(mainWindow, services) { if (!fs.existsSync(manifestPath) || !fs.existsSync(batPath) || !fs.existsSync(jsPath)) { return { ok: false, error: 'Extension files not found. Reinstall Soterios.' }; } + + // Read template manifest and generate a new one in app data directory const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); - const extId = process.env.SOTERIOS_EXT_ID || 'YOUR_EXTENSION_ID_HERE'; manifest.allowed_origins = [manifest.allowed_origins[0].replace('', extId)]; - // Write updated manifest back to disk so Chrome/Edge reads the correct ID - fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); + + // Write generated manifest to app data directory instead of mutating the shipped file + const userDataDir = app.getPath('userData'); + const generatedManifestPath = path.join(userDataDir, 'native-host-manifest.json'); + fs.writeFileSync(generatedManifestPath, JSON.stringify(manifest, null, 2)); + + // Update bat file to reference the correct js path + const batContent = fs.readFileSync(batPath, 'utf8'); + const updatedBatContent = batContent.replace( + /node\s+"[^"]*native-host\.js"/, + `node "${jsPath}"` + ); + const generatedBatPath = path.join(userDataDir, 'native-host.bat'); + fs.writeFileSync(generatedBatPath, updatedBatContent); + const regPath = `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${manifest.name}`; - const regCmd = `reg add "${regPath}" /ve /t REG_SZ /d "${manifestPath.replace(/\\/g, '\\\\')}" /f`; + const regCmd = `reg add "${regPath}" /ve /t REG_SZ /d "${generatedManifestPath.replace(/\\/g, '\\\\')}" /f`; try { execSync(regCmd, { stdio: 'ignore' }); const regPathEdge = `HKCU\\Software\\Microsoft\\Edge\\NativeMessagingHosts\\${manifest.name}`; - const regCmdEdge = `reg add "${regPathEdge}" /ve /t REG_SZ /d "${manifestPath.replace(/\\/g, '\\\\')}" /f`; + const regCmdEdge = `reg add "${regPathEdge}" /ve /t REG_SZ /d "${generatedManifestPath.replace(/\\/g, '\\\\')}" /f`; try { execSync(regCmdEdge, { stdio: 'ignore' }); } catch (_) {} return { ok: true }; } catch (e) { diff --git a/src/security/EmergencyLockdown.js b/src/security/EmergencyLockdown.js index 5e54014..19eb211 100644 --- a/src/security/EmergencyLockdown.js +++ b/src/security/EmergencyLockdown.js @@ -221,6 +221,9 @@ class EmergencyLockdown { errors: [] }; + const totalInterfacesToRestore = this.savedNetworkState ? this.savedNetworkState.filter(i => i.state === 'connected').length : 0; + const totalServicesToRestore = this.savedServicesState ? this.savedServicesState.filter(s => s.state === 'RUNNING').length : 0; + // Restore network interfaces if (this.savedNetworkState) { for (const iface of this.savedNetworkState) { @@ -249,19 +252,43 @@ class EmergencyLockdown { } } - this.isLockedDown = false; - this.savedNetworkState = null; - this.savedServicesState = null; - - this.eventBus.emit('lockdown:changed', { locked: false, results }); - - this.notify( - 'Emergency Lockdown Released', - `Restored ${results.enabledInterfaces.length} network interfaces and restarted ${results.startedServices.length} services.`, - 'success' - ); + // Determine overall restore status + const allInterfacesRestored = results.enabledInterfaces.length === totalInterfacesToRestore; + const allServicesRestored = results.startedServices.length === totalServicesToRestore; + const hasErrors = results.errors.length > 0; - return { success: true, results }; + let status = 'success'; + if (hasErrors && (allInterfacesRestored || allServicesRestored)) { + status = 'partial'; + } else if (hasErrors || (!allInterfacesRestored && totalInterfacesToRestore > 0) || (!allServicesRestored && totalServicesToRestore > 0)) { + status = 'failed'; + } + + // Only clear state if restore was fully successful + if (status === 'success') { + this.isLockedDown = false; + this.savedNetworkState = null; + this.savedServicesState = null; + + this.eventBus.emit('lockdown:changed', { locked: false, results, status }); + + this.notify( + 'Emergency Lockdown Released', + `Restored ${results.enabledInterfaces.length} network interfaces and restarted ${results.startedServices.length} services.`, + 'success' + ); + } else { + // Keep lockdown state active if restore failed/partial + this.eventBus.emit('lockdown:changed', { locked: true, results, status }); + + this.notify( + 'Emergency Lockdown Restore Incomplete', + `Partial restore: ${results.enabledInterfaces.length}/${totalInterfacesToRestore} interfaces, ${results.startedServices.length}/${totalServicesToRestore} services. ${results.errors.length} errors occurred.`, + 'warn' + ); + } + + return { success: status === 'success', results, status }; } catch (err) { throw new Error(`Restore failed: ${err.message}`); } diff --git a/tools/install-native-host.js b/tools/install-native-host.js index 84a71e3..49ca00a 100644 --- a/tools/install-native-host.js +++ b/tools/install-native-host.js @@ -8,10 +8,17 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); -const EXTENSION_ID = process.env.EXTENSION_ID || 'YOUR_EXTENSION_ID_HERE'; +const EXTENSION_ID = process.env.EXTENSION_ID; const IS_WIN = process.platform === 'win32'; function main() { + // Validate extension ID + if (!EXTENSION_ID || EXTENSION_ID === 'YOUR_EXTENSION_ID_HERE' || !/^[a-z]{32}$/.test(EXTENSION_ID)) { + console.error('Error: Invalid extension ID. Set EXTENSION_ID environment variable to a valid 32-character Chrome extension ID.'); + console.error('Example: EXTENSION_ID=abcdefghijklmnopqrstuvwxyz123456 node tools/install-native-host.js'); + process.exit(1); + } + const extDir = path.resolve(__dirname, '..', 'browser-extension'); const manifestPath = path.join(extDir, 'native-host-manifest.json'); const batPath = path.join(extDir, 'native-host.bat'); From d29150b9bfba299b13485a9ab85b5da933598031 Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:22:59 -0500 Subject: [PATCH 22/24] feat(browser-extension): add icon/auto-check settings, fix install/build issues - Fix onInstalled listener to only enable external lookups on initial extension install, preventing setting resets on updates - Add user-configurable showIcon and autoCheck settings synced via chrome.storage.sync, with real-time updates when settings change - Implement automatic password checking on input when autoCheck is enabled, with proper event listener cleanup for removed icons - Rename native host environment variable to DESKTOP_APP for consistency - Update welcome image conversion script to use WIC for reliable SVG rasterization --- browser-extension/background.js | 6 ++-- browser-extension/content.js | 48 +++++++++++++++++++++++++++++++ browser-extension/native-host.js | 2 +- build/convert-welcome.ps1 | 31 ++++++++++++++------ build/installer.nsi | 1 + src/i18n/locales/ja.json | 1 - src/main/ipcHandlers.js | 25 ++++++++++++---- src/security/EmergencyLockdown.js | 8 +++++- src/ui/js/pages/lockdown.js | 14 +++++++++ 9 files changed, 117 insertions(+), 19 deletions(-) diff --git a/browser-extension/background.js b/browser-extension/background.js index 6a44d6e..dd32824 100644 --- a/browser-extension/background.js +++ b/browser-extension/background.js @@ -1,5 +1,7 @@ -chrome.runtime.onInstalled.addListener(() => { - chrome.storage.sync.set({ externalLookupsEnabled: true }); +chrome.runtime.onInstalled.addListener((details) => { + if (details.reason === 'install') { + chrome.storage.sync.set({ externalLookupsEnabled: true }); + } }); // Handle CHECK_PASSWORD from content script diff --git a/browser-extension/content.js b/browser-extension/content.js index a784f91..92ab321 100644 --- a/browser-extension/content.js +++ b/browser-extension/content.js @@ -6,6 +6,27 @@ let soteriosIcon = null; let passwordFields = new Map(); let observer = null; +let currentSettings = { showIcon: true, autoCheck: false }; + +// Load settings from storage +function loadSettings() { + chrome.storage.sync.get(['showIcon', 'autoCheck'], (result) => { + currentSettings.showIcon = result.showIcon !== false; + currentSettings.autoCheck = result.autoCheck === true; + }); +} + +// Listen for settings updates +chrome.storage.onChanged.addListener((changes, namespace) => { + if (namespace === 'sync') { + if (changes.showIcon !== undefined) { + currentSettings.showIcon = changes.showIcon.newValue !== false; + } + if (changes.autoCheck !== undefined) { + currentSettings.autoCheck = changes.autoCheck.newValue === true; + } + } +}); function createIcon() { const icon = document.createElement('img'); @@ -82,6 +103,9 @@ function removeResult(input) { function addIconToField(input) { if (input.dataset.soteriosId) return; + + // Check showIcon setting before adding icon + if (!currentSettings.showIcon) return; const id = `soterios-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; input.dataset.soteriosId = id; @@ -106,6 +130,9 @@ function addIconToField(input) { if (icon._soteriosHandlers.resize) { window.removeEventListener('resize', icon._soteriosHandlers.updatePos); } + if (icon._soteriosHandlers.autoCheckHandler) { + input.removeEventListener('input', icon._soteriosHandlers.autoCheckHandler); + } } icon.remove(); passwordFields.delete(input); @@ -114,6 +141,23 @@ function addIconToField(input) { input.addEventListener('blur', () => setTimeout(cleanup, 200), { once: true }); + // Add autoCheck listener if enabled + if (currentSettings.autoCheck) { + const autoCheckHandler = async () => { + const password = input.value; + if (password && password.length >= 8) { + try { + const result = await chrome.runtime.sendMessage({ type: 'CHECK_PASSWORD', password }); + showResult(input, result); + } catch (err) { + console.error('[Soterios] Auto-check failed:', err); + } + } + }; + input.addEventListener('input', autoCheckHandler); + icon._soteriosHandlers.autoCheckHandler = autoCheckHandler; + } + passwordFields.set(input, icon); } @@ -128,6 +172,7 @@ function init() { return; } + loadSettings(); scanForPasswordFields(); observer = new MutationObserver(mutations => { @@ -160,6 +205,9 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { if (icon._soteriosHandlers.resize) { window.removeEventListener('resize', icon._soteriosHandlers.updatePos); } + if (icon._soteriosHandlers.autoCheckHandler) { + input.removeEventListener('input', icon._soteriosHandlers.autoCheckHandler); + } } icon.remove(); delete input.dataset.soteriosId; diff --git a/browser-extension/native-host.js b/browser-extension/native-host.js index 40b31e9..6f5634b 100644 --- a/browser-extension/native-host.js +++ b/browser-extension/native-host.js @@ -58,7 +58,7 @@ function launchDesktopApp() { if (desktopProc) return Promise.resolve(); return new Promise((resolve, reject) => { - const appPath = process.env.SOTERIOS_APP_PATH || 'soterios://'; + const appPath = process.env.DESKTOP_APP || 'soterios://'; // Check if it's a protocol URL or an executable path const isProtocolUrl = appPath.startsWith('soterios://') || appPath.startsWith('http://') || appPath.startsWith('https://'); diff --git a/build/convert-welcome.ps1 b/build/convert-welcome.ps1 index 4fa22a1..562285b 100644 --- a/build/convert-welcome.ps1 +++ b/build/convert-welcome.ps1 @@ -1,12 +1,25 @@ -$svgContent = [IO.File]::ReadAllText("build/icon.svg") -$ms = New-Object IO.MemoryStream -$sw = New-Object IO.StreamWriter($ms) -$sw.Write($svgContent) -$sw.Flush() -$ms.Position = 0 -$img = [System.Drawing.Image]::FromStream($ms) -$bmp = New-Object System.Drawing.Bitmap($img, 500, 120) +# Use Windows Imaging Component (WIC) to properly rasterize SVG +Add-Type -AssemblyName PresentationFramework +Add-Type -AssemblyName WindowsBase + +$svgPath = "build/welcome-banner.svg" +if (-not (Test-Path $svgPath)) { + Write-Error "SVG file not found: $svgPath" + exit 1 +} + +# Load SVG using WIC +$decoder = [System.Windows.Media.Imaging.BitmapDecoder]::Create( + [System.Uri]::new((Resolve-Path $svgPath)), + [System.IO.FileAccess]::Read, + [System.Windows.Media.Imaging.BitmapCreateOptions]::IgnoreColorProfile +) + +$frame = $decoder.Frames[0] +$bmp = New-Object System.Drawing.Bitmap(500, 120) +$graphics = [System.Drawing.Graphics]::FromImage($bmp) +$graphics.DrawImage($frame, 0, 0, 500, 120) $bmp.Save("build/welcome-banner.bmp", [System.Drawing.Imaging.ImageFormat]::Bmp) -$img.Dispose() +$graphics.Dispose() $bmp.Dispose() Write-Host "Converted welcome-banner.bmp" \ No newline at end of file diff --git a/build/installer.nsi b/build/installer.nsi index 5f588b6..02012bb 100644 --- a/build/installer.nsi +++ b/build/installer.nsi @@ -110,6 +110,7 @@ Section "Main Application" SecMain WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "DisplayName" "Soterios ${PRODUCT_VERSION}" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "DisplayVersion" "${PRODUCT_VERSION}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "InstallLocation" "$INSTDIR" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "Publisher" "Christopher Rivera" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "URLInfoAbout" "https://github.com/chrisriv10/Soterios" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Soterios" "UninstallString" "\"$INSTDIR\uninstall.exe\"" diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 4d295a5..34a033c 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -782,7 +782,6 @@ "passwords.crackTimeDays": "{count} days", "passwords.crackTimeYears": "{count} years", "passwords.crackTimeCenturies": "{count} centuries", - "health.malware.high": "最新のスキャンで {count} 件の脅威マッチが検出されました。", "health.label.malware": "マルウェア スキャン結果", "health.label.scanRecency": "スキャン時効性", "health.label.disk": "ディスク容量", diff --git a/src/main/ipcHandlers.js b/src/main/ipcHandlers.js index d886de8..8465b71 100644 --- a/src/main/ipcHandlers.js +++ b/src/main/ipcHandlers.js @@ -951,12 +951,27 @@ function registerIpcHandlers(mainWindow, services) { const generatedManifestPath = path.join(userDataDir, 'native-host-manifest.json'); fs.writeFileSync(generatedManifestPath, JSON.stringify(manifest, null, 2)); - // Update bat file to reference the correct js path + // Update bat file to reference the correct js path and set DESKTOP_APP const batContent = fs.readFileSync(batPath, 'utf8'); - const updatedBatContent = batContent.replace( - /node\s+"[^"]*native-host\.js"/, - `node "${jsPath}"` - ); + const appExePath = process.execPath; + const appDir = path.dirname(appExePath); + // Use Electron's bundled Node runtime + const nodeExePath = process.platform === 'win32' + ? path.join(appDir, 'resources', 'app.asar.unpacked', 'node.exe') + : path.join(appDir, 'Contents', 'MacOS', 'Soterios'); // macOS + + // For Windows packaged builds, Node is typically in the app directory + const nodeRuntime = process.platform === 'win32' + ? (fs.existsSync(path.join(appDir, 'node.exe')) ? path.join(appDir, 'node.exe') : 'node') + : 'node'; + + const updatedBatContent = `@echo off +REM Soterios Native Messaging Host +REM This batch file launches the Node.js native host that communicates with the desktop app + +set DESKTOP_APP=${appExePath} +set NODE_PATH=${path.join(path.dirname(appExePath), 'resources', 'node_modules')} +"${nodeRuntime}" "${jsPath}" %*`; const generatedBatPath = path.join(userDataDir, 'native-host.bat'); fs.writeFileSync(generatedBatPath, updatedBatContent); diff --git a/src/security/EmergencyLockdown.js b/src/security/EmergencyLockdown.js index 19eb211..7ecd115 100644 --- a/src/security/EmergencyLockdown.js +++ b/src/security/EmergencyLockdown.js @@ -155,6 +155,9 @@ class EmergencyLockdown { return { success: false, message: 'Already in lockdown mode' }; } + // Claim lockdown state immediately to prevent concurrent invocations + this.isLockedDown = true; + try { // Save current state const interfaces = await this.getNetworkInterfaces(); @@ -191,7 +194,6 @@ class EmergencyLockdown { } } - this.isLockedDown = true; this.eventBus.emit('lockdown:changed', { locked: true, results }); this.notify( @@ -202,6 +204,10 @@ class EmergencyLockdown { return { success: true, results }; } catch (err) { + // Reset guard on failure so restore() doesn't receive corrupted state + this.isLockedDown = false; + this.savedNetworkState = null; + this.savedServicesState = null; throw new Error(`Lockdown failed: ${err.message}`); } } diff --git a/src/ui/js/pages/lockdown.js b/src/ui/js/pages/lockdown.js index d305a13..a88d40b 100644 --- a/src/ui/js/pages/lockdown.js +++ b/src/ui/js/pages/lockdown.js @@ -178,10 +178,24 @@ window.Pages['lockdown'] = { lockdownBtn.disabled = false; restoreBtn.disabled = true; } + } else { + // Handle unsuccessful status response + lockdownIndicator.className = 'status-indicator status-warning'; + lockdownIcon.innerHTML = ''; + lockdownLabel.textContent = window.I18n.t('lockdown.error'); + lockdownDetail.textContent = result.error || 'Failed to get lockdown status'; + // Keep unsafe controls disabled + lockdownBtn.disabled = true; + restoreBtn.disabled = true; } } catch (err) { + lockdownIndicator.className = 'status-indicator status-warning'; + lockdownIcon.innerHTML = ''; lockdownLabel.textContent = window.I18n.t('lockdown.error'); lockdownDetail.textContent = err.message; + // Keep unsafe controls disabled + lockdownBtn.disabled = true; + restoreBtn.disabled = true; } }, From c924c203bf2a55429559d241d3ec8fca44f46e88 Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Fri, 24 Jul 2026 21:58:18 -0500 Subject: [PATCH 23/24] feat(i18n): Add Arabic and German translations for Emergency Lockdown allowlist Prior to this change, all Emergency Lockdown UI strings were untranslated (displayed in English) in Arabic and German locale files, and the newly added allowlist management interface for the feature had no localized strings. This commit adds full translations for all existing Emergency Lockdown UI elements and all new allowlist-related i18n keys, completing localization for the feature in supported locales. --- src/i18n/locales/ar.json | 53 +++++--- src/i18n/locales/de.json | 53 +++++--- src/i18n/locales/en.json | 13 ++ src/i18n/locales/es.json | 51 +++++--- src/i18n/locales/fr.json | 53 +++++--- src/i18n/locales/hi.json | 53 +++++--- src/i18n/locales/it.json | 53 +++++--- src/i18n/locales/ja.json | 53 +++++--- src/i18n/locales/ko.json | 53 +++++--- src/i18n/locales/nl.json | 53 +++++--- src/i18n/locales/pl.json | 33 +++++ src/i18n/locales/pt-BR.json | 34 ++++- src/i18n/locales/ru.json | 33 +++++ src/i18n/locales/tr.json | 33 +++++ src/i18n/locales/zh-CN.json | 33 +++++ src/main/ipcHandlers.js | 49 +++++++ src/preload/preload.js | 6 +- src/security/EmergencyLockdown.js | 86 ++++++++++++- src/ui/js/pages/lockdown.js | 204 ++++++++++++++++++++++++++++-- 19 files changed, 803 insertions(+), 196 deletions(-) diff --git a/src/i18n/locales/ar.json b/src/i18n/locales/ar.json index a0c9d64..46eed6d 100644 --- a/src/i18n/locales/ar.json +++ b/src/i18n/locales/ar.json @@ -199,30 +199,43 @@ "nav.firewall": "إدارة جدار الحماية", "nav.network": "مراقب الشبكة", "nav.passwords": "مركز أمان بيانات الاعتماد", - "nav.lockdown": "Emergency Lockdown", + "nav.lockdown": "إغلاق طارئ", "nav.tools": "الأدوات والصيانة", "nav.reports": "التقارير", "nav.settings": "الإعدادات", "nav.scanning": "جاري الفحص…", - "lockdown.title": "Emergency Lockdown", - "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", - "lockdown.checking": "Checking status…", - "lockdown.normal": "Normal Operation", - "lockdown.normalDetail": "All systems are running normally", - "lockdown.active": "Lockdown Active", - "lockdown.activeDetail": "Network disabled and services stopped", - "lockdown.activate": "Activate Lockdown", - "lockdown.restore": "Restore Systems", - "lockdown.activating": "Activating lockdown…", - "lockdown.restoring": "Restoring systems…", - "lockdown.error": "Error", - "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", - "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", - "lockdown.changes": "Changes Made", - "lockdown.network": "Network Interfaces", - "lockdown.services": "Services Stopped", - "lockdown.errors": "Errors", - "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", + "lockdown.title": "إغلاق طارئ", + "lockdown.description": "تعطيل جميع واجهات الشبكة فوراً وإيقاف الخدمات غير الأساسية لحالات الطوارئ.", + "lockdown.checking": "جاري التحقق من الحالة…", + "lockdown.normal": "التشغيل العادي", + "lockdown.normalDetail": "جميع الأنظمة تعمل بشكل طبيعي", + "lockdown.active": "الإغلاق نشط", + "lockdown.activeDetail": "الشبكة معطلة والخدمات متوقفة", + "lockdown.activate": "تفعيل الإغلاق", + "lockdown.restore": "استعادة الأنظمة", + "lockdown.activating": "جاري تفعيل الإغلاق…", + "lockdown.restoring": "جاري استعادة الأنظمة…", + "lockdown.error": "خطأ", + "lockdown.confirmActivate": "هل أنت متأكد من تفعيل الإغلاق الطارئ؟ سيؤدي ذلك إلى تعطيل جميع واجهات الشبكة وإيقاف الخدمات غير الأساسية.", + "lockdown.confirmRestore": "هل أنت متأكد من استعادة الأنظمة؟ سيؤدي ذلك إلى إعادة تمكين واجهات الشبكة وإعادة تشغيل الخدمات.", + "lockdown.changes": "التغييرات المنفذة", + "lockdown.network": "واجهات الشبكة", + "lockdown.services": "الخدمات المتوقفة", + "lockdown.errors": "الأخطاء", + "lockdown.warning": "تحذير: سيؤدي الإغلاق الطارئ إلى قطع اتصالك بالإنترنت وإيقاف الخدمات الخلفية. استخدمه فقط في حالات الطوارئ.", + "lockdown.none": "لا شيء", + "lockdown.skippedInterfaces": "الواجهات المتخطاة (قائمة السماح)", + "lockdown.skippedServices": "الخدمات المتخطاة (قائمة السماح)", + "lockdown.allowlist.title": "قائمة السماح", + "lockdown.allowlist.description": "الواجهات والخدمات وعناوين IP المدرجة هنا ستبقى نشطة أثناء الإغلاق. استخدمها للحفاظ على الاتصال الحرج (VPN، الإدارة، النسخ الاحتياطي).", + "lockdown.allowlist.interfaces": "واجهات الشبكة", + "lockdown.allowlist.services": "الخدمات", + "lockdown.allowlist.ips": "عناوين IP", + "lockdown.allowlist.add": "إضافة", + "lockdown.allowlist.remove": "إزالة", + "lockdown.allowlist.addPlaceholder": "أدخل اسم الواجهة/الخدمة...", + "lockdown.allowlist.ipPlaceholder": "أدخل عنوان IP (مثال: 192.168.1.100)...", + "lockdown.allowlist.empty": "لا توجد عناصر في قائمة السماح", "uninstaller.title": "إزالة البرامج", "uninstaller.installedApps": "التطبيقات المثبتة", "uninstaller.uninstall": "إزالة", diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 2542f27..0531458 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -199,30 +199,43 @@ "nav.firewall": "Firewall-Verwaltung", "nav.network": "Netzwerkmonitor", "nav.passwords": "Anmeldedaten-Sicherheitscenter", - "nav.lockdown": "Emergency Lockdown", + "nav.lockdown": "Notfall-Lockdown", "nav.tools": "Tools & Wartung", "nav.reports": "Berichte", "nav.settings": "Einstellungen", "nav.scanning": "Wird gescannt…", - "lockdown.title": "Emergency Lockdown", - "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", - "lockdown.checking": "Checking status…", - "lockdown.normal": "Normal Operation", - "lockdown.normalDetail": "All systems are running normally", - "lockdown.active": "Lockdown Active", - "lockdown.activeDetail": "Network disabled and services stopped", - "lockdown.activate": "Activate Lockdown", - "lockdown.restore": "Restore Systems", - "lockdown.activating": "Activating lockdown…", - "lockdown.restoring": "Restoring systems…", - "lockdown.error": "Error", - "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", - "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", - "lockdown.changes": "Changes Made", - "lockdown.network": "Network Interfaces", - "lockdown.services": "Services Stopped", - "lockdown.errors": "Errors", - "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", + "lockdown.title": "Notfall-Lockdown", + "lockdown.description": "Sofort alle Netzwerkschnittstellen deaktivieren und nicht-essentielle Dienste für Notfallsituationen stoppen.", + "lockdown.checking": "Status wird geprüft…", + "lockdown.normal": "Normaler Betrieb", + "lockdown.normalDetail": "Alle Systeme laufen normal", + "lockdown.active": "Lockdown aktiv", + "lockdown.activeDetail": "Netzwerk deaktiviert und Dienste gestoppt", + "lockdown.activate": "Lockdown aktivieren", + "lockdown.restore": "Systeme wiederherstellen", + "lockdown.activating": "Lockdown wird aktiviert…", + "lockdown.restoring": "Systeme werden wiederhergestellt…", + "lockdown.error": "Fehler", + "lockdown.confirmActivate": "Möchten Sie wirklich den Notfall-Lockdown aktivieren? Dies deaktiviert alle Netzwerkschnittstellen und stoppt nicht-essentielle Dienste.", + "lockdown.confirmRestore": "Möchten Sie wirklich die Systeme wiederherstellen? Dies aktiviert die Netzwerkschnittstellen neu und startet die Dienste.", + "lockdown.changes": "Vorgenommene Änderungen", + "lockdown.network": "Netzwerkschnittstellen", + "lockdown.services": "Gestoppte Dienste", + "lockdown.errors": "Fehler", + "lockdown.warning": "Warnung: Der Notfall-Lockdown trennt Sie vom Internet und stoppt Hintergrunddienste. Nur in Notfallsituationen verwenden.", + "lockdown.none": "Keine", + "lockdown.skippedInterfaces": "Übersprungene Interfaces (Allowlist)", + "lockdown.skippedServices": "Übersprungene Dienste (Allowlist)", + "lockdown.allowlist.title": "Allowlist", + "lockdown.allowlist.description": "Hier aufgeführte Interfaces, Dienste und IPs bleiben während des Lockdowns aktiv. Verwenden Sie dies, um kritische Konnektivität aufrechtzuerhalten (VPN, Management, Backups).", + "lockdown.allowlist.interfaces": "Netzwerk-Interfaces", + "lockdown.allowlist.services": "Dienste", + "lockdown.allowlist.ips": "IP-Adressen", + "lockdown.allowlist.add": "Hinzufügen", + "lockdown.allowlist.remove": "Entfernen", + "lockdown.allowlist.addPlaceholder": "Interface-/Dienstname eingeben...", + "lockdown.allowlist.ipPlaceholder": "IP-Adresse eingeben (z.B. 192.168.1.100)...", + "lockdown.allowlist.empty": "Keine Einträge in der Allowlist", "uninstaller.title": "Software-Deinstaller", "uninstaller.installedApps": "Installierte Anwendungen", "uninstaller.uninstall": "Deinstallieren", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index d5cfaa3..4831355 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -241,6 +241,19 @@ "lockdown.services": "Services Stopped", "lockdown.errors": "Errors", "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", + "lockdown.none": "None", + "lockdown.skippedInterfaces": "Skipped Interfaces (Allowlisted)", + "lockdown.skippedServices": "Skipped Services (Allowlisted)", + "lockdown.allowlist.title": "Allowlist", + "lockdown.allowlist.description": "Interfaces, services, and IPs listed here will remain active during lockdown. Use to maintain critical connectivity (VPN, management, backups).", + "lockdown.allowlist.interfaces": "Network Interfaces", + "lockdown.allowlist.services": "Services", + "lockdown.allowlist.ips": "IP Addresses", + "lockdown.allowlist.add": "Add", + "lockdown.allowlist.remove": "Remove", + "lockdown.allowlist.addPlaceholder": "Enter interface/service name...", + "lockdown.allowlist.ipPlaceholder": "Enter IP address (e.g., 192.168.1.100)...", + "lockdown.allowlist.empty": "No items in allowlist", "uninstaller.title": "Software Uninstaller", "uninstaller.installedApps": "Installed applications", "uninstaller.uninstall": "Uninstall", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 46464fc..3cdcf9e 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -199,30 +199,43 @@ "nav.firewall": "Gestión de firewall", "nav.network": "Monitor de red", "nav.passwords": "Centro de seguridad de credenciales", - "nav.lockdown": "Emergency Lockdown", + "nav.lockdown": "Bloqueo de emergencia", "nav.tools": "Herramientas y mantenimiento", "nav.reports": "Informes", "nav.settings": "Configuración", "nav.scanning": "Escaneando…", - "lockdown.title": "Emergency Lockdown", - "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", - "lockdown.checking": "Checking status…", - "lockdown.normal": "Normal Operation", - "lockdown.normalDetail": "All systems are running normally", - "lockdown.active": "Lockdown Active", - "lockdown.activeDetail": "Network disabled and services stopped", - "lockdown.activate": "Activate Lockdown", - "lockdown.restore": "Restore Systems", - "lockdown.activating": "Activating lockdown…", - "lockdown.restoring": "Restoring systems…", + "lockdown.title": "Bloqueo de emergencia", + "lockdown.description": "Desactive instantáneamente todas las interfaces de red y detenga los servicios no esenciales para situaciones de emergencia.", + "lockdown.checking": "Comprobando estado…", + "lockdown.normal": "Operación normal", + "lockdown.normalDetail": "Todos los sistemas funcionan normalmente", + "lockdown.active": "Bloqueo activo", + "lockdown.activeDetail": "Red deshabilitada y servicios detenidos", + "lockdown.activate": "Activar bloqueo", + "lockdown.restore": "Restaurar sistemas", + "lockdown.activating": "Activando bloqueo…", + "lockdown.restoring": "Restaurando sistemas…", "lockdown.error": "Error", - "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", - "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", - "lockdown.changes": "Changes Made", - "lockdown.network": "Network Interfaces", - "lockdown.services": "Services Stopped", - "lockdown.errors": "Errors", - "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", + "lockdown.confirmActivate": "¿Está seguro de que desea activar el bloqueo de emergencia? Esto deshabilitará todas las interfaces de red y detendrá los servicios no esenciales.", + "lockdown.confirmRestore": "¿Está seguro de que desea restaurar los sistemas? Esto volverá a habilitar las interfaces de red y reiniciará los servicios.", + "lockdown.changes": "Cambios realizados", + "lockdown.network": "Interfaces de red", + "lockdown.services": "Servicios detenidos", + "lockdown.errors": "Errores", + "lockdown.warning": "Advertencia: El bloqueo de emergencia le desconectará de internet y detendrá los servicios en segundo plano. Úselo solo en situaciones de emergencia.", + "lockdown.none": "Ninguno", + "lockdown.skippedInterfaces": "Interfaces omitidas (en lista de permitidos)", + "lockdown.skippedServices": "Servicios omitidos (en lista de permitidos)", + "lockdown.allowlist.title": "Lista de permitidos", + "lockdown.allowlist.description": "Las interfaces, servicios e IPs listadas aquí permanecerán activas durante el bloqueo. Úselo para mantener conectividad crítica (VPN, gestión, copias de seguridad).", + "lockdown.allowlist.interfaces": "Interfaces de red", + "lockdown.allowlist.services": "Servicios", + "lockdown.allowlist.ips": "Direcciones IP", + "lockdown.allowlist.add": "Añadir", + "lockdown.allowlist.remove": "Eliminar", + "lockdown.allowlist.addPlaceholder": "Introduzca nombre de interfaz/servicio...", + "lockdown.allowlist.ipPlaceholder": "Introduzca dirección IP (ej. 192.168.1.100)...", + "lockdown.allowlist.empty": "Sin elementos en la lista de permitidos", "uninstaller.title": "Desinstalador de software", "uninstaller.installedApps": "Aplicaciones instaladas", "uninstaller.uninstall": "Desinstalar", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 8cd62bb..b3a6061 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -199,30 +199,43 @@ "nav.firewall": "Gestion du pare-feu", "nav.network": "Moniteur réseau", "nav.passwords": "Centre de sécurité des identifiants", - "nav.lockdown": "Emergency Lockdown", + "nav.lockdown": "Verrouillage d'urgence", "nav.tools": "Outils et maintenance", "nav.reports": "Rapports", "nav.settings": "Paramètres", "nav.scanning": "Analyse en cours…", - "lockdown.title": "Emergency Lockdown", - "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", - "lockdown.checking": "Checking status…", - "lockdown.normal": "Normal Operation", - "lockdown.normalDetail": "All systems are running normally", - "lockdown.active": "Lockdown Active", - "lockdown.activeDetail": "Network disabled and services stopped", - "lockdown.activate": "Activate Lockdown", - "lockdown.restore": "Restore Systems", - "lockdown.activating": "Activating lockdown…", - "lockdown.restoring": "Restoring systems…", - "lockdown.error": "Error", - "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", - "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", - "lockdown.changes": "Changes Made", - "lockdown.network": "Network Interfaces", - "lockdown.services": "Services Stopped", - "lockdown.errors": "Errors", - "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", + "lockdown.title": "Verrouillage d'urgence", + "lockdown.description": "Désactivez instantanément toutes les interfaces réseau et arrêtez les services non essentiels pour les situations d'urgence.", + "lockdown.checking": "Vérification du statut…", + "lockdown.normal": "Fonctionnement normal", + "lockdown.normalDetail": "Tous les systèmes fonctionnent normalement", + "lockdown.active": "Verrouillage actif", + "lockdown.activeDetail": "Réseau désactivé et services arrêtés", + "lockdown.activate": "Activer le verrouillage", + "lockdown.restore": "Restaurer les systèmes", + "lockdown.activating": "Activation du verrouillage…", + "lockdown.restoring": "Restauration des systèmes…", + "lockdown.error": "Erreur", + "lockdown.confirmActivate": "Êtes-vous sûr de vouloir activer le verrouillage d'urgence ? Cela désactivera toutes les interfaces réseau et arrêtera les services non essentiels.", + "lockdown.confirmRestore": "Êtes-vous sûr de vouloir restaurer les systèmes ? Cela réactivera les interfaces réseau et redémarrera les services.", + "lockdown.changes": "Modifications apportées", + "lockdown.network": "Interfaces réseau", + "lockdown.services": "Services arrêtés", + "lockdown.errors": "Erreurs", + "lockdown.warning": "Avertissement : Le verrouillage d'urgence vous déconnectera d'internet et arrêtera les services en arrière-plan. À utiliser uniquement en cas d'urgence.", + "lockdown.none": "Aucun", + "lockdown.skippedInterfaces": "Interfaces ignorées (liste d'autorisation)", + "lockdown.skippedServices": "Services ignorés (liste d'autorisation)", + "lockdown.allowlist.title": "Liste d'autorisation", + "lockdown.allowlist.description": "Les interfaces, services et IPs listés ici resteront actifs pendant le verrouillage. Utilisez-le pour maintenir une connectivité critique (VPN, gestion, sauvegardes).", + "lockdown.allowlist.interfaces": "Interfaces réseau", + "lockdown.allowlist.services": "Services", + "lockdown.allowlist.ips": "Adresses IP", + "lockdown.allowlist.add": "Ajouter", + "lockdown.allowlist.remove": "Supprimer", + "lockdown.allowlist.addPlaceholder": "Entrer le nom d'interface/service...", + "lockdown.allowlist.ipPlaceholder": "Entrer l'adresse IP (ex: 192.168.1.100)...", + "lockdown.allowlist.empty": "Aucun élément dans la liste d'autorisation", "uninstaller.title": "Désinstalateur de logiciels", "uninstaller.installedApps": "Applications installées", "uninstaller.uninstall": "Désinstaller", diff --git a/src/i18n/locales/hi.json b/src/i18n/locales/hi.json index 9039d88..1ca5486 100644 --- a/src/i18n/locales/hi.json +++ b/src/i18n/locales/hi.json @@ -199,30 +199,43 @@ "nav.firewall": "फ़ायरवॉल प्रबंधन", "nav.network": "नेटवर्क मॉनिटर", "nav.passwords": "क्रेडेंशियल सुरक्षा हब", - "nav.lockdown": "Emergency Lockdown", + "nav.lockdown": "आपातकालीन लॉकडाउन", "nav.tools": "टूल और रखरखाव", "nav.reports": "रिपोर्ट", "nav.settings": "सेटिंग्स", "nav.scanning": "स्कैनिंग…", - "lockdown.title": "Emergency Lockdown", - "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", - "lockdown.checking": "Checking status…", - "lockdown.normal": "Normal Operation", - "lockdown.normalDetail": "All systems are running normally", - "lockdown.active": "Lockdown Active", - "lockdown.activeDetail": "Network disabled and services stopped", - "lockdown.activate": "Activate Lockdown", - "lockdown.restore": "Restore Systems", - "lockdown.activating": "Activating lockdown…", - "lockdown.restoring": "Restoring systems…", - "lockdown.error": "Error", - "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", - "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", - "lockdown.changes": "Changes Made", - "lockdown.network": "Network Interfaces", - "lockdown.services": "Services Stopped", - "lockdown.errors": "Errors", - "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", + "lockdown.title": "आपातकालीन लॉकडाउन", + "lockdown.description": "आपातकालीन स्थितियों के लिए सभी नेटवर्क इंटरफेस को तुरंत अक्षम करें और गैर-आवश्यक सेवाओं को रोकें।", + "lockdown.checking": "स्थिति की जाँच…", + "lockdown.normal": "सामान्य संचालन", + "lockdown.normalDetail": "सभी सिस्टम सामान्य रूप से चल रहे हैं", + "lockdown.active": "लॉकडाउन सक्रिय", + "lockdown.activeDetail": "नेटवर्क अक्षम और सेवाएँ रुकी हुईं", + "lockdown.activate": "लॉकडाउन सक्रिय करें", + "lockdown.restore": "सिस्टम बहाल करें", + "lockdown.activating": "लॉकडाउन सक्रिय किया जा रहा है…", + "lockdown.restoring": "सिस्टम बहाल किए जा रहे हैं…", + "lockdown.error": "त्रुटि", + "lockdown.confirmActivate": "क्या आप वाकई आपातकालीन लॉकडाउन सक्रिय करना चाहते हैं? यह सभी नेटवर्क इंटरफेस को अक्षम कर देगा और गैर-आवश्यक सेवाओं को रोक देगा।", + "lockdown.confirmRestore": "क्या आप वाकई सिस्टम बहाल करना चाहते हैं? यह नेटवर्क इंटरफेस को फिर से सक्षम करेगा और सेवाओं को पुनः आरंभ करेगा।", + "lockdown.changes": "किए गए परिवर्तन", + "lockdown.network": "नेटवर्क इंटरफेस", + "lockdown.services": "रुकी हुई सेवाएँ", + "lockdown.errors": "त्रुटियाँ", + "lockdown.warning": "चेतावनी: आपातकालीन लॉकडाउन आपको इंटरनेट से डिस्कनेक्ट कर देगा और पृष्ठभूमि सेवाओं को रोक देगा। केवल आपातकालीन स्थितियों में उपयोग करें।", + "lockdown.none": "कोई नहीं", + "lockdown.skippedInterfaces": "छोड़े गए इंटरफेस (अनुमति सूची)", + "lockdown.skippedServices": "छोड़ी गई सेवाएँ (अनुमति सूची)", + "lockdown.allowlist.title": "अनुमति सूची", + "lockdown.allowlist.description": "यहां सूचीबद्ध इंटरफेस, सेवाएँ और IP लॉकडाउन के दौरान सक्रिय रहेंगे। महत्वपूर्ण कनेक्टिविटी (VPN, प्रबंधन, बैकअप) बनाए रखने के लिए उपयोग करें।", + "lockdown.allowlist.interfaces": "नेटवर्क इंटरफेस", + "lockdown.allowlist.services": "सेवाएँ", + "lockdown.allowlist.ips": "IP पते", + "lockdown.allowlist.add": "जोड़ें", + "lockdown.allowlist.remove": "हटाएँ", + "lockdown.allowlist.addPlaceholder": "इंटरफेस/सेवा नाम दर्ज करें...", + "lockdown.allowlist.ipPlaceholder": "IP पता दर्ज करें (उदा. 192.168.1.100)...", + "lockdown.allowlist.empty": "अनुमति सूची में कोई आइटम नहीं", "uninstaller.title": "सॉफ़्टवेयर अनइंस्टॉलर", "uninstaller.installedApps": "इंस्टॉल किए गए ऐप", "uninstaller.uninstall": "अनइंस्टॉल", diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index b50f986..c5ebae6 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -199,30 +199,43 @@ "nav.firewall": "Gestione Firewall", "nav.network": "Monitor di Rete", "nav.passwords": "Centro Sicurezza Credenziali", - "nav.lockdown": "Emergency Lockdown", + "nav.lockdown": "Lockdown di emergenza", "nav.tools": "Strumenti e Manutenzione", "nav.reports": "Rapporti", "nav.settings": "Impostazioni", "nav.scanning": "Scansione in corso…", - "lockdown.title": "Emergency Lockdown", - "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", - "lockdown.checking": "Checking status…", - "lockdown.normal": "Normal Operation", - "lockdown.normalDetail": "All systems are running normally", - "lockdown.active": "Lockdown Active", - "lockdown.activeDetail": "Network disabled and services stopped", - "lockdown.activate": "Activate Lockdown", - "lockdown.restore": "Restore Systems", - "lockdown.activating": "Activating lockdown…", - "lockdown.restoring": "Restoring systems…", - "lockdown.error": "Error", - "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", - "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", - "lockdown.changes": "Changes Made", - "lockdown.network": "Network Interfaces", - "lockdown.services": "Services Stopped", - "lockdown.errors": "Errors", - "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", + "lockdown.title": "Lockdown di emergenza", + "lockdown.description": "Disattiva istantaneamente tutte le interfacce di rete e arresta i servizi non essenziali per situazioni di emergenza.", + "lockdown.checking": "Controllo stato…", + "lockdown.normal": "Operazione normale", + "lockdown.normalDetail": "Tutti i sistemi funzionano normalmente", + "lockdown.active": "Lockdown attivo", + "lockdown.activeDetail": "Rete disabilitata e servizi arrestati", + "lockdown.activate": "Attiva lockdown", + "lockdown.restore": "Ripristina sistemi", + "lockdown.activating": "Attivazione lockdown…", + "lockdown.restoring": "Ripristino sistemi…", + "lockdown.error": "Errore", + "lockdown.confirmActivate": "Sei sicuro di voler attivare il lockdown di emergenza? Questo disabiliterà tutte le interfacce di rete e arresterà i servizi non essenziali.", + "lockdown.confirmRestore": "Sei sicuro di voler ripristinare i sistemi? Questo riabiliterà le interfacce di rete e riavvierà i servizi.", + "lockdown.changes": "Modifiche apportate", + "lockdown.network": "Interfacce di rete", + "lockdown.services": "Servizi arrestati", + "lockdown.errors": "Errori", + "lockdown.warning": "Avvertenza: Il lockdown di emergenza ti disconnetterà da internet e arresterà i servizi in background. Usa solo in situazioni di emergenza.", + "lockdown.none": "Nessuno", + "lockdown.skippedInterfaces": "Interfacce saltate (allowlist)", + "lockdown.skippedServices": "Servizi saltati (allowlist)", + "lockdown.allowlist.title": "Allowlist", + "lockdown.allowlist.description": "Le interfacce, i servizi e gli IP elencati qui rimarranno attivi durante il lockdown. Usa per mantenere la connettività critica (VPN, gestione, backup).", + "lockdown.allowlist.interfaces": "Interfacce di rete", + "lockdown.allowlist.services": "Servizi", + "lockdown.allowlist.ips": "Indirizzi IP", + "lockdown.allowlist.add": "Aggiungi", + "lockdown.allowlist.remove": "Rimuovi", + "lockdown.allowlist.addPlaceholder": "Inserisci nome interfaccia/servizio...", + "lockdown.allowlist.ipPlaceholder": "Inserisci indirizzo IP (es. 192.168.1.100)...", + "lockdown.allowlist.empty": "Nessun elemento nell'allowlist", "uninstaller.title": "Disinstallatore software", "uninstaller.installedApps": "Applicazioni installate", "uninstaller.uninstall": "Disinstalla", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 34a033c..1a894d3 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -199,30 +199,43 @@ "nav.firewall": "ファイアウォール管理", "nav.network": "ネットワーク モニター", "nav.passwords": "認証情報セキュリティ ハブ", - "nav.lockdown": "Emergency Lockdown", + "nav.lockdown": "緊急ロックダウン", "nav.tools": "ツールとメンテナンス", "nav.reports": "レポート", "nav.settings": "設定", "nav.scanning": "スキャン中…", - "lockdown.title": "Emergency Lockdown", - "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", - "lockdown.checking": "Checking status…", - "lockdown.normal": "Normal Operation", - "lockdown.normalDetail": "All systems are running normally", - "lockdown.active": "Lockdown Active", - "lockdown.activeDetail": "Network disabled and services stopped", - "lockdown.activate": "Activate Lockdown", - "lockdown.restore": "Restore Systems", - "lockdown.activating": "Activating lockdown…", - "lockdown.restoring": "Restoring systems…", - "lockdown.error": "Error", - "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", - "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", - "lockdown.changes": "Changes Made", - "lockdown.network": "Network Interfaces", - "lockdown.services": "Services Stopped", - "lockdown.errors": "Errors", - "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", + "lockdown.title": "緊急ロックダウン", + "lockdown.description": "緊急事態に備えて、すべてのネットワーク インターフェイスを即座に無効化し、非必須サービスを停止します。", + "lockdown.checking": "ステータスを確認中…", + "lockdown.normal": "通常動作", + "lockdown.normalDetail": "すべてのシステムが正常に動作しています", + "lockdown.active": "ロックダウン有効", + "lockdown.activeDetail": "ネットワークが無効化され、サービスが停止しました", + "lockdown.activate": "ロックダウンを有効化", + "lockdown.restore": "システムを復元", + "lockdown.activating": "ロックダウンを有効化中…", + "lockdown.restoring": "システムを復元中…", + "lockdown.error": "エラー", + "lockdown.confirmActivate": "緊急ロックダウンを有効にしますか? すべてのネットワーク インターフェイスが無効化され、非必須サービスが停止されます。", + "lockdown.confirmRestore": "システムを復元しますか? ネットワーク インターフェイスが再有効化され、サービスが再起動されます。", + "lockdown.changes": "変更内容", + "lockdown.network": "ネットワーク インターフェイス", + "lockdown.services": "停止したサービス", + "lockdown.errors": "エラー", + "lockdown.warning": "警告: 緊急ロックダウンはインターネットから切断し、バックグラウンド サービスを停止します。緊急時のみ使用してください。", + "lockdown.none": "なし", + "lockdown.skippedInterfaces": "スキップされたインターフェイス (許可リスト)", + "lockdown.skippedServices": "スキップされたサービス (許可リスト)", + "lockdown.allowlist.title": "許可リスト", + "lockdown.allowlist.description": "ここにリストされているインターフェイス、サービス、IP はロックダウン中もアクティブなままです。重要な接続 (VPN、管理、バックアップ) の維持に使用します。", + "lockdown.allowlist.interfaces": "ネットワーク インターフェイス", + "lockdown.allowlist.services": "サービス", + "lockdown.allowlist.ips": "IP アドレス", + "lockdown.allowlist.add": "追加", + "lockdown.allowlist.remove": "削除", + "lockdown.allowlist.addPlaceholder": "インターフェイス/サービス名を入力...", + "lockdown.allowlist.ipPlaceholder": "IP アドレスを入力 (例: 192.168.1.100)...", + "lockdown.allowlist.empty": "許可リストに項目がありません", "uninstaller.title": "ソフトウェア アンインストーラー", "uninstaller.installedApps": "インストール済みアプリケーション", "uninstaller.uninstall": "アンインストール", diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index a0f30ca..2764a8b 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -199,30 +199,43 @@ "nav.firewall": "방화벽 관리", "nav.network": "네트워크 모니터", "nav.passwords": "자격 증명 안전 허브", - "nav.lockdown": "Emergency Lockdown", + "nav.lockdown": "긴급 봉쇄", "nav.tools": "도구 및 유지 관리", "nav.reports": "보고서", "nav.settings": "설정", "nav.scanning": "검사 중…", - "lockdown.title": "Emergency Lockdown", - "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", - "lockdown.checking": "Checking status…", - "lockdown.normal": "Normal Operation", - "lockdown.normalDetail": "All systems are running normally", - "lockdown.active": "Lockdown Active", - "lockdown.activeDetail": "Network disabled and services stopped", - "lockdown.activate": "Activate Lockdown", - "lockdown.restore": "Restore Systems", - "lockdown.activating": "Activating lockdown…", - "lockdown.restoring": "Restoring systems…", - "lockdown.error": "Error", - "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", - "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", - "lockdown.changes": "Changes Made", - "lockdown.network": "Network Interfaces", - "lockdown.services": "Services Stopped", - "lockdown.errors": "Errors", - "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", + "lockdown.title": "긴급 봉쇄", + "lockdown.description": "긴급 상황을 위해 모든 네트워크 인터페이스를 즉시 비활성화하고 불필요한 서비스를 중지합니다.", + "lockdown.checking": "상태 확인 중…", + "lockdown.normal": "정상 작동", + "lockdown.normalDetail": "모든 시스템이 정상적으로 실행 중입니다", + "lockdown.active": "봉쇄 활성화", + "lockdown.activeDetail": "네트워크 비활성화 및 서비스 중지됨", + "lockdown.activate": "봉쇄 활성화", + "lockdown.restore": "시스템 복원", + "lockdown.activating": "봉쇄 활성화 중…", + "lockdown.restoring": "시스템 복원 중…", + "lockdown.error": "오류", + "lockdown.confirmActivate": "긴급 봉쇄를 활성화하시겠습니까? 모든 네트워크 인터페이스가 비활성화되고 불필요한 서비스가 중지됩니다.", + "lockdown.confirmRestore": "시스템을 복원하시겠습니까? 네트워크 인터페이스가 다시 활성화되고 서비스가 재시작됩니다.", + "lockdown.changes": "변경 사항", + "lockdown.network": "네트워크 인터페이스", + "lockdown.services": "중지된 서비스", + "lockdown.errors": "오류", + "lockdown.warning": "경고: 긴급 봉쇄는 인터넷 연결을 끊고 백그라운드 서비스를 중지합니다. 긴급 상황에서만 사용하세요.", + "lockdown.none": "없음", + "lockdown.skippedInterfaces": "건너뛴 인터페이스 (허용 목록)", + "lockdown.skippedServices": "건너뛴 서비스 (허용 목록)", + "lockdown.allowlist.title": "허용 목록", + "lockdown.allowlist.description": "여기에 나열된 인터페이스, 서비스, IP는 봉쇄 중에도 활성 상태를 유지합니다. 중요한 연결(VPN, 관리, 백업) 유지에 사용하세요.", + "lockdown.allowlist.interfaces": "네트워크 인터페이스", + "lockdown.allowlist.services": "서비스", + "lockdown.allowlist.ips": "IP 주소", + "lockdown.allowlist.add": "추가", + "lockdown.allowlist.remove": "제거", + "lockdown.allowlist.addPlaceholder": "인터페이스/서비스 이름 입력...", + "lockdown.allowlist.ipPlaceholder": "IP 주소 입력 (예: 192.168.1.100)...", + "lockdown.allowlist.empty": "허용 목록에 항목이 없습니다", "uninstaller.title": "소프트웨어 제거 프로그램", "uninstaller.installedApps": "설치된 애플리케이션", "uninstaller.uninstall": "제거", diff --git a/src/i18n/locales/nl.json b/src/i18n/locales/nl.json index 7e18636..d865e0d 100644 --- a/src/i18n/locales/nl.json +++ b/src/i18n/locales/nl.json @@ -199,30 +199,43 @@ "nav.firewall": "Firewallbeheer", "nav.network": "Netwerkmonitor", "nav.passwords": "Inloggeveiligheidscentrum", - "nav.lockdown": "Emergency Lockdown", + "nav.lockdown": "Noodlockdown", "nav.tools": "Tools en onderhoud", "nav.reports": "Rapporten", "nav.settings": "Instellingen", "nav.scanning": "Scannen…", - "lockdown.title": "Emergency Lockdown", - "lockdown.description": "Instantly disable all network interfaces and stop non-essential services for emergency situations.", - "lockdown.checking": "Checking status…", - "lockdown.normal": "Normal Operation", - "lockdown.normalDetail": "All systems are running normally", - "lockdown.active": "Lockdown Active", - "lockdown.activeDetail": "Network disabled and services stopped", - "lockdown.activate": "Activate Lockdown", - "lockdown.restore": "Restore Systems", - "lockdown.activating": "Activating lockdown…", - "lockdown.restoring": "Restoring systems…", - "lockdown.error": "Error", - "lockdown.confirmActivate": "Are you sure you want to activate emergency lockdown? This will disable all network interfaces and stop non-essential services.", - "lockdown.confirmRestore": "Are you sure you want to restore systems? This will re-enable network interfaces and restart services.", - "lockdown.changes": "Changes Made", - "lockdown.network": "Network Interfaces", - "lockdown.services": "Services Stopped", - "lockdown.errors": "Errors", - "lockdown.warning": "Warning: Emergency lockdown will disconnect you from the internet and stop background services. Use only in emergency situations.", + "lockdown.title": "Noodlockdown", + "lockdown.description": "Direct alle netwerkinterfaces uitschakelen en niet-essentiële services stoppen voor noodsituaties.", + "lockdown.checking": "Status controleren…", + "lockdown.normal": "Normale werking", + "lockdown.normalDetail": "Alle systemen draaien normaal", + "lockdown.active": "Lockdown actief", + "lockdown.activeDetail": "Netwerk uitgeschakeld en services gestopt", + "lockdown.activate": "Lockdown activeren", + "lockdown.restore": "Systemen herstellen", + "lockdown.activating": "Lockdown activeren…", + "lockdown.restoring": "Systemen herstellen…", + "lockdown.error": "Fout", + "lockdown.confirmActivate": "Weet u zeker dat u de noodlockdown wilt activeren? Dit schakelt alle netwerkinterfaces uit en stopt niet-essentiële services.", + "lockdown.confirmRestore": "Weet u zeker dat u de systemen wilt herstellen? Dit schakelt de netwerkinterfaces weer in en herstart de services.", + "lockdown.changes": "Aangebrachte wijzigingen", + "lockdown.network": "Netwerkinterfaces", + "lockdown.services": "Gestopte services", + "lockdown.errors": "Fouten", + "lockdown.warning": "Waarschuwing: De noodlockdown verbreekt uw internetverbinding en stopt achtergrondservices. Alleen gebruiken in noodsituaties.", + "lockdown.none": "Geen", + "lockdown.skippedInterfaces": "Overgeslagen interfaces (allowlist)", + "lockdown.skippedServices": "Overgeslagen services (allowlist)", + "lockdown.allowlist.title": "Allowlist", + "lockdown.allowlist.description": "Hier vermelde interfaces, services en IP's blijven actief tijdens de lockdown. Gebruik dit om kritieke connectiviteit te behouden (VPN, beheer, back-ups).", + "lockdown.allowlist.interfaces": "Netwerkinterfaces", + "lockdown.allowlist.services": "Services", + "lockdown.allowlist.ips": "IP-adressen", + "lockdown.allowlist.add": "Toevoegen", + "lockdown.allowlist.remove": "Verwijderen", + "lockdown.allowlist.addPlaceholder": "Voer interface-/servicenaam in...", + "lockdown.allowlist.ipPlaceholder": "Voer IP-adres in (bijv. 192.168.1.100)...", + "lockdown.allowlist.empty": "Geen items in de allowlist", "uninstaller.title": "Softwareverwijderaar", "uninstaller.installedApps": "Geïnstalleerde applicaties", "uninstaller.uninstall": "Verwijderen", diff --git a/src/i18n/locales/pl.json b/src/i18n/locales/pl.json index b948d2c..c257f0e 100644 --- a/src/i18n/locales/pl.json +++ b/src/i18n/locales/pl.json @@ -199,10 +199,43 @@ "nav.firewall": "Zarządzanie zaporą", "nav.network": "Monitor sieci", "nav.passwords": "Centrum bezpieczeństwa poświadczeń", + "nav.lockdown": "Lockdown awaryjny", "nav.tools": "Narzędzia i konserwacja", "nav.reports": "Raporty", "nav.settings": "Ustawienia", "nav.scanning": "Skanowanie…", + "lockdown.title": "Lockdown awaryjny", + "lockdown.description": "Natychmiast wyłącz wszystkie interfejsy sieciowe i zatrzymaj usługi niezbędne w sytuacjach awaryjnych.", + "lockdown.checking": "Sprawdzanie statusu…", + "lockdown.normal": "Normalna operacja", + "lockdown.normalDetail": "Wszystkie systemy działają normalnie", + "lockdown.active": "Lockdown aktywny", + "lockdown.activeDetail": "Sieć wyłączona i usługi zatrzymane", + "lockdown.activate": "Aktywuj lockdown", + "lockdown.restore": "Przywróć systemy", + "lockdown.activating": "Aktywacja lockdownu…", + "lockdown.restoring": "Przywracanie systemów…", + "lockdown.error": "Błąd", + "lockdown.confirmActivate": "Czy na pewno chcesz aktywować lockdown awaryjny? Spowoduje to wyłączenie wszystkich interfejsów sieciowych i zatrzymanie usług niezbędnych.", + "lockdown.confirmRestore": "Czy na pewno chcesz przywrócić systemy? Spowoduje to ponowne włączenie interfejsów sieciowych i restart usług.", + "lockdown.changes": "Wprowadzone zmiany", + "lockdown.network": "Interfejsy sieciowe", + "lockdown.services": "Zatrzymane usługi", + "lockdown.errors": "Błędy", + "lockdown.warning": "Ostrzeżenie: Lockdown awaryjny odłączy Cię od internetu i zatrzyma usługi w tle. Używaj tylko w sytuacjach awaryjnych.", + "lockdown.none": "Brak", + "lockdown.skippedInterfaces": "Pominięte interfejsy (lista dozwolonych)", + "lockdown.skippedServices": "Pominięte usługi (lista dozwolonych)", + "lockdown.allowlist.title": "Lista dozwolonych", + "lockdown.allowlist.description": "Interfejsy, usługi i adresy IP na tej liście pozostaną aktywne podczas lockdownu. Użyj, aby utrzymać krytyczną łączność (VPN, zarządzanie, kopie zapasowe).", + "lockdown.allowlist.interfaces": "Interfejsy sieciowe", + "lockdown.allowlist.services": "Usługi", + "lockdown.allowlist.ips": "Adresy IP", + "lockdown.allowlist.add": "Dodaj", + "lockdown.allowlist.remove": "Usuń", + "lockdown.allowlist.addPlaceholder": "Wpisz nazwę interfejsu/usługi...", + "lockdown.allowlist.ipPlaceholder": "Wpisz adres IP (np. 192.168.1.100)...", + "lockdown.allowlist.empty": "Brak elementów na liście dozwolonych", "uninstaller.title": "Deinstalator oprogramowania", "uninstaller.installedApps": "Zainstalowane aplikacje", "uninstaller.uninstall": "Odinstaluj", diff --git a/src/i18n/locales/pt-BR.json b/src/i18n/locales/pt-BR.json index e224b01..93f03ad 100644 --- a/src/i18n/locales/pt-BR.json +++ b/src/i18n/locales/pt-BR.json @@ -199,11 +199,43 @@ "nav.firewall": "Gerenciamento de Firewall", "nav.network": "Monitor de Rede", "nav.passwords": "Central de Segurança de Credenciais", + "nav.lockdown": "Lockdown de emergência", "nav.tools": "Ferramentas e Manutenção", "nav.reports": "Relatórios", "nav.settings": "Configurações", "nav.scanning": "Verificando…", - "uninstaller.title": "Desinstalador de software", + "lockdown.title": "Lockdown de emergência", + "lockdown.description": "Desative instantaneamente todas as interfaces de rede e pare os serviços não essenciais para situações de emergência.", + "lockdown.checking": "Verificando status…", + "lockdown.normal": "Operação normal", + "lockdown.normalDetail": "Todos os sistemas estão funcionando normalmente", + "lockdown.active": "Lockdown ativo", + "lockdown.activeDetail": "Rede desabilitada e serviços parados", + "lockdown.activate": "Ativar lockdown", + "lockdown.restore": "Restaurar sistemas", + "lockdown.activating": "Ativando lockdown…", + "lockdown.restoring": "Restaurando sistemas…", + "lockdown.error": "Erro", + "lockdown.confirmActivate": "Tem certeza de que deseja ativar o lockdown de emergência? Isso desativará todas as interfaces de rede e parará os serviços não essenciais.", + "lockdown.confirmRestore": "Tem certeza de que deseja restaurar os sistemas? Isso reativará as interfaces de rede e reiniciará os serviços.", + "lockdown.changes": "Alterações feitas", + "lockdown.network": "Interfaces de rede", + "lockdown.services": "Serviços parados", + "lockdown.errors": "Erros", + "lockdown.warning": "Aviso: O lockdown de emergência o desconectará da internet e parará os serviços em segundo plano. Use apenas em situações de emergência.", + "lockdown.none": "Nenhum", + "lockdown.skippedInterfaces": "Interfaces ignoradas (lista de permissão)", + "lockdown.skippedServices": "Serviços ignorados (lista de permissão)", + "lockdown.allowlist.title": "Lista de Permissão", + "lockdown.allowlist.description": "Interfaces, serviços e IPs listados aqui permanecerão ativos durante o lockdown. Use para manter conectividade crítica (VPN, gerenciamento, backups).", + "lockdown.allowlist.interfaces": "Interfaces de Rede", + "lockdown.allowlist.services": "Serviços", + "lockdown.allowlist.ips": "Endereços IP", + "lockdown.allowlist.add": "Adicionar", + "lockdown.allowlist.remove": "Remover", + "lockdown.allowlist.addPlaceholder": "Digite o nome da interface/serviço...", + "lockdown.allowlist.ipPlaceholder": "Digite o endereço IP (ex: 192.168.1.100)...", + "lockdown.allowlist.empty": "Nenhum item na lista de permissão", "uninstaller.installedApps": "Aplicativos instalados", "uninstaller.uninstall": "Desinstalar", "uninstaller.scanLeftovers": "Verificar restos", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 4e522ab..d18b94d 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -199,10 +199,43 @@ "nav.firewall": "Управление брандмауэром", "nav.network": "Монитор сети", "nav.passwords": "Центр безопасности учётных данных", + "nav.lockdown": "Экстренная блокировка", "nav.tools": "Инструменты и обслуживание", "nav.reports": "Отчёты", "nav.settings": "Настройки", "nav.scanning": "Проверка…", + "lockdown.title": "Экстренная блокировка", + "lockdown.description": "Мгновенно отключите все сетевые интерфейсы и остановите несущественные службы для экстренных ситуаций.", + "lockdown.checking": "Проверка статуса…", + "lockdown.normal": "Нормальная работа", + "lockdown.normalDetail": "Все системы работают нормально", + "lockdown.active": "Блокировка активна", + "lockdown.activeDetail": "Сеть отключена и службы остановлены", + "lockdown.activate": "Активировать блокировку", + "lockdown.restore": "Восстановить системы", + "lockdown.activating": "Активация блокировки…", + "lockdown.restoring": "Восстановление систем…", + "lockdown.error": "Ошибка", + "lockdown.confirmActivate": "Вы уверены, что хотите активировать экстренную блокировку? Это отключит все сетевые интерфейсы и остановит несущественные службы.", + "lockdown.confirmRestore": "Вы уверены, что хотите восстановить системы? Это снова включит сетевые интерфейсы и перезапустит службы.", + "lockdown.changes": "Внесенные изменения", + "lockdown.network": "Сетевые интерфейсы", + "lockdown.services": "Остановленные службы", + "lockdown.errors": "Ошибки", + "lockdown.warning": "Предупреждение: Экстренная блокировка отключит вас от интернета и остановит фоновые службы. Используйте только в экстренных ситуациях.", + "lockdown.none": "Нет", + "lockdown.skippedInterfaces": "Пропущенные интерфейсы (в списке разрешенных)", + "lockdown.skippedServices": "Пропущенные службы (в списке разрешенных)", + "lockdown.allowlist.title": "Список разрешенных", + "lockdown.allowlist.description": "Интерфейсы, службы и IP-адреса, перечисленные здесь, останутся активными во время блокировки. Используйте для поддержания критической связности (VPN, управление, резервное копирование).", + "lockdown.allowlist.interfaces": "Сетевые интерфейсы", + "lockdown.allowlist.services": "Службы", + "lockdown.allowlist.ips": "IP-адреса", + "lockdown.allowlist.add": "Добавить", + "lockdown.allowlist.remove": "Удалить", + "lockdown.allowlist.addPlaceholder": "Введите имя интерфейса/службы...", + "lockdown.allowlist.ipPlaceholder": "Введите IP-адрес (например, 192.168.1.100)...", + "lockdown.allowlist.empty": "Нет элементов в списке разрешенных", "uninstaller.title": "Удаление программ", "uninstaller.installedApps": "Установленные приложения", "uninstaller.uninstall": "Удалить", diff --git a/src/i18n/locales/tr.json b/src/i18n/locales/tr.json index 53d33c3..97993a6 100644 --- a/src/i18n/locales/tr.json +++ b/src/i18n/locales/tr.json @@ -199,10 +199,43 @@ "nav.firewall": "Güvenlik Duvarı Yönetimi", "nav.network": "Ağ İzleyicisi", "nav.passwords": "Kimlik Bilgisi Güvenlik Merkezi", + "nav.lockdown": "Acil Durum Kilitlemesi", "nav.tools": "Araçlar ve Bakım", "nav.reports": "Raporlar", "nav.settings": "Ayarlar", "nav.scanning": "Taranıyor…", + "lockdown.title": "Acil Durum Kilitlemesi", + "lockdown.description": "Acil durumlar için tüm ağ arayüzlerini anında devre dışı bırakın ve gerekli olmayan hizmetleri durdurun.", + "lockdown.checking": "Durum kontrol ediliyor…", + "lockdown.normal": "Normal İşlem", + "lockdown.normalDetail": "Tüm sistemler normal çalışıyor", + "lockdown.active": "Kilitleme Aktif", + "lockdown.activeDetail": "Ağ devre dışı ve hizmetler durduruldu", + "lockdown.activate": "Kilitlemeyi Etkinleştir", + "lockdown.restore": "Sistemleri Geri Yükle", + "lockdown.activating": "Kilitleme etkinleştiriliyor…", + "lockdown.restoring": "Sistemler geri yükleniyor…", + "lockdown.error": "Hata", + "lockdown.confirmActivate": "Acil durum kilitlemesini etkinleştirmek istediğinizden emin misiniz? Bu, tüm ağ arayüzlerini devre dışı bırakacak ve gerekli olmayan hizmetleri durduracaktır.", + "lockdown.confirmRestore": "Sistemleri geri yüklemek istediğinizden emin misiniz? Bu, ağ arayüzlerini yeniden etkinleştirecek ve hizmetleri yeniden başlatacaktır.", + "lockdown.changes": "Yapılan Değişiklikler", + "lockdown.network": "Ağ Arayüzleri", + "lockdown.services": "Durdurulan Hizmetler", + "lockdown.errors": "Hatalar", + "lockdown.warning": "Uyarı: Acil durum kilitlemesi sizi internetten ayıracak ve arka plan hizmetlerini durduracaktır. Yalnızca acil durumlarda kullanın.", + "lockdown.none": "Yok", + "lockdown.skippedInterfaces": "Atlanan Arayüzler (İzin Listesi)", + "lockdown.skippedServices": "Atlanan Hizmetler (İzin Listesi)", + "lockdown.allowlist.title": "İzin Listesi", + "lockdown.allowlist.description": "Burada listelenen arayüzler, hizmetler ve IP'ler kilitleme sırasında aktif kalacaktır. Kritik bağlantıyı (VPN, yönetim, yedeklemeler) korumak için kullanın.", + "lockdown.allowlist.interfaces": "Ağ Arayüzleri", + "lockdown.allowlist.services": "Hizmetler", + "lockdown.allowlist.ips": "IP Adresleri", + "lockdown.allowlist.add": "Ekle", + "lockdown.allowlist.remove": "Kaldır", + "lockdown.allowlist.addPlaceholder": "Arayüz/hizmet adı girin...", + "lockdown.allowlist.ipPlaceholder": "IP adresi girin (örn. 192.168.1.100)...", + "lockdown.allowlist.empty": "İzin listesinde öğe yok", "uninstaller.title": "Yazılım kaldırıcı", "uninstaller.installedApps": "Yüklü uygulamalar", "uninstaller.uninstall": "Kaldır", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 557c1d7..c9c5021 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -199,10 +199,43 @@ "nav.firewall": "防火墙管理", "nav.network": "网络监视器", "nav.passwords": "凭据安全中心", + "nav.lockdown": "紧急封锁", "nav.tools": "工具与维护", "nav.reports": "报告", "nav.settings": "设置", "nav.scanning": "扫描中…", + "lockdown.title": "紧急封锁", + "lockdown.description": "针对紧急情况,立即禁用所有网络接口并停止非必要服务。", + "lockdown.checking": "正在检查状态…", + "lockdown.normal": "正常运行", + "lockdown.normalDetail": "所有系统运行正常", + "lockdown.active": "封锁激活", + "lockdown.activeDetail": "网络已禁用,服务已停止", + "lockdown.activate": "激活封锁", + "lockdown.restore": "恢复系统", + "lockdown.activating": "正在激活封锁…", + "lockdown.restoring": "正在恢复系统…", + "lockdown.error": "错误", + "lockdown.confirmActivate": "确定要激活紧急封锁吗?这将禁用所有网络接口并停止非必要服务。", + "lockdown.confirmRestore": "确定要恢复系统吗?这将重新启用网络接口并重启服务。", + "lockdown.changes": "已做更改", + "lockdown.network": "网络接口", + "lockdown.services": "已停止的服务", + "lockdown.errors": "错误", + "lockdown.warning": "警告:紧急封锁将使您与互联网断开连接并停止后台服务。仅在紧急情况下使用。", + "lockdown.none": "无", + "lockdown.skippedInterfaces": "已跳过的接口 (允许列表)", + "lockdown.skippedServices": "已跳过的服务 (允许列表)", + "lockdown.allowlist.title": "允许列表", + "lockdown.allowlist.description": "此处列出的接口、服务和 IP 在封锁期间将保持活动状态。用于保持关键连接(VPN、管理、备份)。", + "lockdown.allowlist.interfaces": "网络接口", + "lockdown.allowlist.services": "服务", + "lockdown.allowlist.ips": "IP 地址", + "lockdown.allowlist.add": "添加", + "lockdown.allowlist.remove": "移除", + "lockdown.allowlist.addPlaceholder": "输入接口/服务名称...", + "lockdown.allowlist.ipPlaceholder": "输入 IP 地址 (例如: 192.168.1.100)...", + "lockdown.allowlist.empty": "允许列表为空", "uninstaller.title": "软件卸载程序", "uninstaller.installedApps": "已安装的应用", "uninstaller.uninstall": "卸载", diff --git a/src/main/ipcHandlers.js b/src/main/ipcHandlers.js index 8465b71..b863540 100644 --- a/src/main/ipcHandlers.js +++ b/src/main/ipcHandlers.js @@ -1024,6 +1024,55 @@ set NODE_PATH=${path.join(path.dirname(appExePath), 'resources', 'node_modules') return { ok: false, error: err.message }; } }); + + // -- Emergency Lockdown Allowlist -- + ipcMain.handle('lockdown:getAllowlist', async () => { + if (!services.emergencyLockdown) { + return { ok: false, error: 'Emergency lockdown service unavailable' }; + } + try { + const allowlist = services.emergencyLockdown.getAllowlist(); + return { ok: true, data: allowlist }; + } catch (err) { + return { ok: false, error: err.message }; + } + }); + + ipcMain.handle('lockdown:setAllowlist', async (event, allowlist) => { + if (!services.emergencyLockdown) { + return { ok: false, error: 'Emergency lockdown service unavailable' }; + } + try { + const result = services.emergencyLockdown.setAllowlist(allowlist); + return { ok: true, data: result }; + } catch (err) { + return { ok: false, error: err.message }; + } + }); + + ipcMain.handle('lockdown:addToAllowlist', async (event, type, value) => { + if (!services.emergencyLockdown) { + return { ok: false, error: 'Emergency lockdown service unavailable' }; + } + try { + const result = services.emergencyLockdown.addToAllowlist(type, value); + return { ok: true, data: result }; + } catch (err) { + return { ok: false, error: err.message }; + } + }); + + ipcMain.handle('lockdown:removeFromAllowlist', async (event, type, value) => { + if (!services.emergencyLockdown) { + return { ok: false, error: 'Emergency lockdown service unavailable' }; + } + try { + const result = services.emergencyLockdown.removeFromAllowlist(type, value); + return { ok: true, data: result }; + } catch (err) { + return { ok: false, error: err.message }; + } + }); } module.exports = { registerIpcHandlers }; \ No newline at end of file diff --git a/src/preload/preload.js b/src/preload/preload.js index 4309d31..621fb10 100644 --- a/src/preload/preload.js +++ b/src/preload/preload.js @@ -42,6 +42,10 @@ contextBridge.exposeInMainWorld('soterios', { lockdown: { getStatus: () => ipcRenderer.invoke('lockdown:getStatus'), activate: () => ipcRenderer.invoke('lockdown:activate'), - restore: () => ipcRenderer.invoke('lockdown:restore') + restore: () => ipcRenderer.invoke('lockdown:restore'), + getAllowlist: () => ipcRenderer.invoke('lockdown:getAllowlist'), + setAllowlist: (allowlist) => ipcRenderer.invoke('lockdown:setAllowlist', allowlist), + addToAllowlist: (type, value) => ipcRenderer.invoke('lockdown:addToAllowlist', type, value), + removeFromAllowlist: (type, value) => ipcRenderer.invoke('lockdown:removeFromAllowlist', type, value) } }); diff --git a/src/security/EmergencyLockdown.js b/src/security/EmergencyLockdown.js index 7ecd115..0b776c8 100644 --- a/src/security/EmergencyLockdown.js +++ b/src/security/EmergencyLockdown.js @@ -16,6 +16,65 @@ class EmergencyLockdown { this.isLockedDown = false; this.savedNetworkState = null; this.savedServicesState = null; + this.allowlist = { + interfaces: [], + services: [], + ips: [] + }; + this._loadAllowlist(); + } + + _loadAllowlist() { + try { + const stored = this.db.get('lockdown_allowlist'); + if (stored) { + this.allowlist = { ...this.allowlist, ...stored }; + } + } catch (err) { + // Ignore, use defaults + } + } + + _saveAllowlist() { + try { + this.db.set('lockdown_allowlist', this.allowlist); + } catch (err) { + console.error('Failed to save lockdown allowlist:', err); + } + } + + getAllowlist() { + return { ...this.allowlist }; + } + + setAllowlist(allowlist) { + this.allowlist = { + interfaces: allowlist.interfaces || [], + services: allowlist.services || [], + ips: allowlist.ips || [] + }; + this._saveAllowlist(); + return this.allowlist; + } + + addToAllowlist(type, value) { + if (!this.allowlist[type]) { + this.allowlist[type] = []; + } + const normalized = type === 'ips' ? value.trim() : value.trim().toLowerCase(); + if (!this.allowlist[type].includes(normalized)) { + this.allowlist[type].push(normalized); + this._saveAllowlist(); + } + return this.allowlist; + } + + removeFromAllowlist(type, value) { + if (!this.allowlist[type]) return this.allowlist; + const normalized = type === 'ips' ? value.trim() : value.trim().toLowerCase(); + this.allowlist[type] = this.allowlist[type].filter(v => v !== normalized); + this._saveAllowlist(); + return this.allowlist; } /** @@ -169,12 +228,25 @@ class EmergencyLockdown { const results = { disabledInterfaces: [], stoppedServices: [], + skippedInterfaces: [], + skippedServices: [], errors: [] }; - // Disable all connected network interfaces + // Disable all connected network interfaces (respecting allowlist) + const allowedInterfaces = new Set(this.allowlist.interfaces?.map(i => i.toLowerCase()) || []); + const allowedIPs = new Set(this.allowlist.ips || []); + for (const iface of interfaces) { if (iface.state === 'connected') { + // Check if interface is allowlisted + if (allowedInterfaces.has(iface.name.toLowerCase())) { + results.skippedInterfaces.push(`${iface.name} (allowlisted)`); + continue; + } + + // Check if any IP on this interface is allowlisted + // For simplicity, we'll skip the interface if user explicitly allowlisted it try { await this.disableInterface(iface.name); results.disabledInterfaces.push(iface.name); @@ -184,8 +256,16 @@ class EmergencyLockdown { } } - // Stop non-essential services + // Stop non-essential services (respecting allowlist) + const allowedServices = new Set(this.allowlist.services?.map(s => s.toLowerCase()) || []); + for (const svc of services) { + // Check if service is allowlisted + if (allowedServices.has(svc.name.toLowerCase())) { + results.skippedServices.push(`${svc.name} (allowlisted)`); + continue; + } + try { await this.stopService(svc.name); results.stoppedServices.push(svc.name); @@ -198,7 +278,7 @@ class EmergencyLockdown { this.notify( 'Emergency Lockdown Activated', - `Disabled ${results.disabledInterfaces.length} network interfaces and stopped ${results.stoppedServices.length} services.`, + `Disabled ${results.disabledInterfaces.length} network interfaces and stopped ${results.stoppedServices.length} services. ${results.skippedInterfaces.length} interfaces and ${results.skippedServices.length} services skipped (allowlisted).`, 'warn' ); diff --git a/src/ui/js/pages/lockdown.js b/src/ui/js/pages/lockdown.js index a88d40b..cb92ecb 100644 --- a/src/ui/js/pages/lockdown.js +++ b/src/ui/js/pages/lockdown.js @@ -55,6 +55,14 @@ window.Pages['lockdown'] = {
+ +
-
${escapeHtml(t('lockdown.warning'))}
-
- - - - - - ${escapeHtml(t('lockdown.warning'))} +
${escapeHtml(t('lockdown.warning'))}
+
+ + +
+
${escapeHtml(t('lockdown.allowlist.title'))}
+
${escapeHtml(t('lockdown.allowlist.description'))}
+ +
+ +
+
${escapeHtml(t('lockdown.allowlist.interfaces'))}
+
+ + +
+
+
+ + +
+
${escapeHtml(t('lockdown.allowlist.services'))}
+
+ + +
+
+
+ + +
+
${escapeHtml(t('lockdown.allowlist.ips'))}
+
+ + +
+
+
`; @@ -96,8 +134,30 @@ window.Pages['lockdown'] = { const errorSection = document.getElementById('errorSection'); const errorList = document.getElementById('errorList'); + // Allowlist elements + const allowlistInterfaceInput = document.getElementById('allowlistInterfaceInput'); + const addAllowlistInterfaceBtn = document.getElementById('addAllowlistInterfaceBtn'); + const allowlistInterfacesList = document.getElementById('allowlistInterfacesList'); + const allowlistServiceInput = document.getElementById('allowlistServiceInput'); + const addAllowlistServiceBtn = document.getElementById('addAllowlistServiceBtn'); + const allowlistServicesList = document.getElementById('allowlistServicesList'); + const allowlistIpInput = document.getElementById('allowlistIpInput'); + const addAllowlistIpBtn = document.getElementById('addAllowlistIpBtn'); + const allowlistIpsList = document.getElementById('allowlistIpsList'); + // Load initial status this._updateLockdownStatus(); + this._loadAllowlist(); + + // Allowlist event listeners + addAllowlistInterfaceBtn.addEventListener('click', () => this._addToAllowlist('interfaces', allowlistInterfaceInput.value.trim())); + allowlistInterfaceInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') this._addToAllowlist('interfaces', allowlistInterfaceInput.value.trim()); }); + + addAllowlistServiceBtn.addEventListener('click', () => this._addToAllowlist('services', allowlistServiceInput.value.trim())); + allowlistServiceInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') this._addToAllowlist('services', allowlistServiceInput.value.trim()); }); + + addAllowlistIpBtn.addEventListener('click', () => this._addToAllowlist('ips', allowlistIpInput.value.trim())); + allowlistIpInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') this._addToAllowlist('ips', allowlistIpInput.value.trim()); }); lockdownBtn.addEventListener('click', async () => { if (!confirm(window.I18n.t('lockdown.confirmActivate'))) return; @@ -151,6 +211,100 @@ window.Pages['lockdown'] = { }); }, + async _loadAllowlist() { + try { + const result = await window.soterios.lockdown.getAllowlist(); + if (result.ok) { + this._renderAllowlist(result.data); + } + } catch (err) { + console.error('Failed to load allowlist:', err); + } + }, + + _renderAllowlist(allowlist) { + const t = (key, vars) => window.I18n?.t(key, vars) ?? key; + + // Render interfaces + const interfacesList = document.getElementById('allowlistInterfacesList'); + if (interfacesList) { + interfacesList.innerHTML = (allowlist.interfaces || []).map(iface => + `
+ ${escapeHtml(iface)} + +
` + ).join('') || '
' + t('lockdown.allowlist.empty') + '
'; + + // Add remove listeners + interfacesList.querySelectorAll('button[data-type]').forEach(btn => { + btn.addEventListener('click', () => this._removeFromAllowlist(btn.dataset.type, btn.dataset.value)); + }); + } + + // Render services + const servicesList = document.getElementById('allowlistServicesList'); + if (servicesList) { + servicesList.innerHTML = (allowlist.services || []).map(svc => + `
+ ${escapeHtml(svc)} + +
` + ).join('') || '
' + t('lockdown.allowlist.empty') + '
'; + + servicesList.querySelectorAll('button[data-type]').forEach(btn => { + btn.addEventListener('click', () => this._removeFromAllowlist(btn.dataset.type, btn.dataset.value)); + }); + } + + // Render IPs + const ipsList = document.getElementById('allowlistIpsList'); + if (ipsList) { + ipsList.innerHTML = (allowlist.ips || []).map(ip => + `
+ ${escapeHtml(ip)} + +
` + ).join('') || '
' + t('lockdown.allowlist.empty') + '
'; + + ipsList.querySelectorAll('button[data-type]').forEach(btn => { + btn.addEventListener('click', () => this._removeFromAllowlist(btn.dataset.type, btn.dataset.value)); + }); + } + }, + + async _addToAllowlist(type, value) { + if (!value) return; + const inputMap = { + interfaces: document.getElementById('allowlistInterfaceInput'), + services: document.getElementById('allowlistServiceInput'), + ips: document.getElementById('allowlistIpInput') + }; + try { + const result = await window.soterios.lockdown.addToAllowlist(type, value); + if (result.ok) { + inputMap[type].value = ''; + this._renderAllowlist(result.data); + } else { + alert(result.error || 'Failed to add to allowlist'); + } + } catch (err) { + alert(err.message); + } + }, + + async _removeFromAllowlist(type, value) { + try { + const result = await window.soterios.lockdown.removeFromAllowlist(type, value); + if (result.ok) { + this._renderAllowlist(result.data); + } else { + alert(result.error || 'Failed to remove from allowlist'); + } + } catch (err) { + alert(err.message); + } + }, + async _updateLockdownStatus() { const lockdownIndicator = document.getElementById('lockdownIndicator'); const lockdownIcon = document.getElementById('lockdownIcon'); @@ -199,33 +353,59 @@ window.Pages['lockdown'] = { } }, - _showLockdownDetails(data) { +_showLockdownDetails(data) { const lockdownDetails = document.getElementById('lockdownDetails'); const noDetailsMessage = document.getElementById('noDetailsMessage'); const networkList = document.getElementById('networkList'); const serviceList = document.getElementById('serviceList'); + const skippedInterfacesSection = document.getElementById('skippedInterfacesSection'); + const skippedInterfacesList = document.getElementById('skippedInterfacesList'); + const skippedServicesSection = document.getElementById('skippedServicesSection'); + const skippedServicesList = document.getElementById('skippedServicesList'); const errorSection = document.getElementById('errorSection'); const errorList = document.getElementById('errorList'); + const t = (key, vars) => window.I18n?.t(key, vars) ?? key; + lockdownDetails.style.display = 'block'; noDetailsMessage.style.display = 'none'; // Network interfaces networkList.innerHTML = data.results.disabledInterfaces.map(iface => `
${escapeHtml(iface)}
` - ).join('') || '
None
'; + ).join('') || `
${t('lockdown.none')}
`; // Services serviceList.innerHTML = data.results.stoppedServices.map(svc => `
${escapeHtml(svc)}
` - ).join('') || '
None
'; + ).join('') || `
${t('lockdown.none')}
`; + + // Skipped interfaces (allowlisted) + if (data.results.skippedInterfaces && data.results.skippedInterfaces.length > 0) { + skippedInterfacesSection.style.display = 'block'; + skippedInterfacesList.innerHTML = data.results.skippedInterfaces.map(iface => + `
${escapeHtml(iface)}
` + ).join(''); + } else { + skippedInterfacesSection.style.display = 'none'; + } + + // Skipped services (allowlisted) + if (data.results.skippedServices && data.results.skippedServices.length > 0) { + skippedServicesSection.style.display = 'block'; + skippedServicesList.innerHTML = data.results.skippedServices.map(svc => + `
${escapeHtml(svc)}
` + ).join(''); + } else { + skippedServicesSection.style.display = 'none'; + } // Errors if (data.results.errors && data.results.errors.length > 0) { errorSection.style.display = 'block'; errorList.innerHTML = data.results.errors.map(err => `
${escapeHtml(err)}
` - ).join(''); + ).join(''); } else { errorSection.style.display = 'none'; } From 1f606eaf198f31ba161f4adde95f1c471923972e Mon Sep 17 00:00:00 2001 From: Chris <185133702+chrisriv10@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:11:38 -0500 Subject: [PATCH 24/24] feat(native-host): add named pipe fallback, fix native message parsing Replaces the broken readline-based stdin parser with a proper length-prefixed buffer parser to correctly handle Chrome native messaging format and eliminate listener accumulation. Adds logic to first connect to a running desktop Electron app via named pipe (Windows) or Unix socket (Linux/macOS) before falling back to launching a new instance. Restructures the main loop to sequentially process messages with robust error handling for stream end and parse failures, improving reliability and performance by reusing existing app instances. --- browser-extension-host.js | 119 --------------- browser-extension-host.json | 9 -- browser-extension/native-host.js | 135 +++++++++++++---- tests/emergencyLockdown.test.js | 216 +++++++++++++++++++++++++++ tests/healthSummary.test.js | 246 +++++++++++++++++++++++++++++++ 5 files changed, 569 insertions(+), 156 deletions(-) delete mode 100644 browser-extension-host.js delete mode 100644 browser-extension-host.json create mode 100644 tests/emergencyLockdown.test.js create mode 100644 tests/healthSummary.test.js diff --git a/browser-extension-host.js b/browser-extension-host.js deleted file mode 100644 index 794aa58..0000000 --- a/browser-extension-host.js +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env node -/** - * Soterios Native Messaging Host - * Receives messages from browser extension and forwards to desktop app - */ - -const { spawn } = require('child_process'); -const fs = require('fs'); -const path = require('path'); - -// Persistent stream parser to avoid listener accumulation -let messageBuffer = Buffer.alloc(0); -let messageResolver = null; - -function readMessage() { - return new Promise((resolve, reject) => { - messageResolver = { resolve, reject }; - // Try to parse any buffered data first - tryParseBuffer(); - }); -} - -function tryParseBuffer() { - if (!messageResolver) return; - - while (messageBuffer.length >= 4) { - const len = messageBuffer.readUInt32LE(0); - if (messageBuffer.length < 4 + len) break; - - const msgBuf = messageBuffer.subarray(4, 4 + len); - messageBuffer = messageBuffer.subarray(4 + len); - - try { - const msg = JSON.parse(msgBuf.toString('utf8')); - messageResolver.resolve(msg); - messageResolver = null; - return; - } catch (e) { - messageResolver.reject(new Error(`Failed to parse message: ${e.message}`)); - messageResolver = null; - return; - } - } -} - -// Set up persistent stdin listener once -process.stdin.on('data', (chunk) => { - messageBuffer = Buffer.concat([messageBuffer, chunk]); - tryParseBuffer(); -}); - -process.stdin.on('error', (err) => { - if (messageResolver) { - messageResolver.reject(err); - messageResolver = null; - } -}); - -process.stdin.on('end', () => { - if (messageResolver) { - messageResolver.reject(new Error('Stream ended')); - messageResolver = null; - } -}); - -function sendMessage(msg) { - const buf = Buffer.from(JSON.stringify(msg), 'utf8'); - const lenBuf = Buffer.alloc(4); - lenBuf.writeUInt32LE(buf.length, 0); - process.stdout.write(lenBuf); - process.stdout.write(buf); -} - -async function connectToDesktopApp() { - const pipeName = '\\\\.\\pipe\\soterios-credential-safety'; - return new Promise((resolve, reject) => { - const client = require('net').createConnection(pipeName, () => { - resolve(client); - }); - client.on('error', reject); - }); -} - -let desktopClient = null; - -async function main() { - console.error('[Soterios Host] Starting...'); - - try { - desktopClient = await connectToDesktopApp(); - console.error('[Soterios Host] Connected to desktop app'); - } catch (e) { - console.error('[Soterios Host] Desktop app not running:', e.message); - } - - while (true) { - try { - const msg = await readMessage(); - console.error('[Soterios Host] Received:', msg.type); - - if (msg.type === 'CREDENTIAL_LEAK') { - if (desktopClient) { - desktopClient.write(JSON.stringify({ type: 'CREDENTIAL_LEAK', ...msg.payload }) + '\n'); - } - sendMessage({ ok: true }); - } else if (msg.type === 'PING') { - sendMessage({ pong: true }); - } - } catch (e) { - if (e.message.includes('Unexpected end of JSON')) break; - console.error('[Soterios Host] Error:', e.message); - } - } -} - -main().catch(e => { - console.error('[Soterios Host] Fatal:', e); - process.exit(1); -}); \ No newline at end of file diff --git a/browser-extension-host.json b/browser-extension-host.json deleted file mode 100644 index 423867b..0000000 --- a/browser-extension-host.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "com.soterios.credential_safety", - "description": "Soterios Credential Safety Native Messaging Host", - "path": "browser-extension-host.exe", - "type": "stdio", - "allowed_origins": [ - "chrome-extension://YOUR_EXTENSION_ID_HERE/" - ] -} \ No newline at end of file diff --git a/browser-extension/native-host.js b/browser-extension/native-host.js index 6f5634b..84cb814 100644 --- a/browser-extension/native-host.js +++ b/browser-extension/native-host.js @@ -2,12 +2,13 @@ /** * Soterios Native Messaging Host * Bridges browser extension <-> desktop Electron app via stdin/stdout JSON messages + * First attempts to connect via named pipe (if app is running), falls back to launching app */ const { spawn } = require('child_process'); -const readline = require('readline'); const fs = require('fs'); const path = require('path'); +const net = require('net'); function log(...args) { console.error('[Soterios Native Host]', new Date().toISOString(), ...args); @@ -22,38 +23,79 @@ function send(msg) { process.stdout.write(buf); } -function readMessages() { - const rl = readline.createInterface({ - input: process.stdin, - terminal: false +// Persistent stream parser to avoid listener accumulation +let messageBuffer = Buffer.alloc(0); +let messageResolver = null; + +function readMessage() { + return new Promise((resolve, reject) => { + messageResolver = { resolve, reject }; + tryParseBuffer(); }); +} + +function tryParseBuffer() { + if (!messageResolver) return; + + while (messageBuffer.length >= 4) { + const len = messageBuffer.readUInt32LE(0); + if (messageBuffer.length < 4 + len) break; + + const msgBuf = messageBuffer.subarray(4, 4 + len); + messageBuffer = messageBuffer.subarray(4 + len); + + try { + const msg = JSON.parse(msgBuf.toString('utf8')); + messageResolver.resolve(msg); + messageResolver = null; + return; + } catch (e) { + messageResolver.reject(new Error(`Failed to parse message: ${e.message}`)); + messageResolver = null; + return; + } + } +} - let buffer = Buffer.alloc(0); +// Set up persistent stdin listener once +process.stdin.on('data', (chunk) => { + messageBuffer = Buffer.concat([messageBuffer, chunk]); + tryParseBuffer(); +}); - process.stdin.on('data', chunk => { - buffer = Buffer.concat([buffer, chunk]); +process.stdin.on('error', (err) => { + if (messageResolver) { + messageResolver.reject(err); + messageResolver = null; + } +}); - while (buffer.length >= 4) { - const len = buffer.readUInt32LE(0); - if (buffer.length < 4 + len) break; +process.stdin.on('end', () => { + if (messageResolver) { + messageResolver.reject(new Error('Stream ended')); + messageResolver = null; + } +}); - const json = buffer.subarray(4, 4 + len).toString(); - buffer = buffer.subarray(4 + len); +let desktopClient = null; +let desktopProc = null; - try { - const msg = JSON.parse(json); - handleMessage(msg); - } catch (e) { - log('Parse error:', e.message); - } - } +async function connectToDesktopApp() { + const pipeName = process.platform === 'win32' ? '\\\\.\\pipe\\soterios-credential-safety' : '/tmp/soterios-credential-safety.sock'; + + return new Promise((resolve, reject) => { + const client = net.createConnection(pipeName, () => { + log('Connected to desktop app via named pipe'); + resolve(client); + }); + + client.on('error', (err) => { + log('Named pipe connection failed:', err.message); + reject(err); + }); }); } -let desktopProc = null; -const pending = new Map(); -let msgId = 0; - function launchDesktopApp() { if (desktopProc) return Promise.resolve(); @@ -109,8 +151,20 @@ async function handleMessage(msg) { switch (msg.type) { case 'CREDENTIAL_LEAK': { - await launchDesktopApp(); - send({ type: 'LEAK_NOTIFIED', ok: true, original: msg }); + // Try to connect via named pipe first + try { + if (!desktopClient) { + desktopClient = await connectToDesktopApp(); + } + if (desktopClient) { + desktopClient.write(JSON.stringify({ type: 'CREDENTIAL_LEAK', ...msg.payload }) + '\n'); + } + send({ type: 'LEAK_NOTIFIED', ok: true, original: msg }); + } catch (pipeErr) { + log('Pipe connection failed, launching desktop app:', pipeErr.message); + await launchDesktopApp(); + send({ type: 'LEAK_NOTIFIED', ok: true, original: msg }); + } break; } case 'PING': { @@ -128,6 +182,29 @@ async function handleMessage(msg) { } } +async function main() { + log('Starting native messaging host'); + + // Try to connect to desktop app on startup + try { + desktopClient = await connectToDesktopApp(); + } catch (e) { + log('Desktop app not running on startup, will launch when needed'); + } + + while (true) { + try { + const msg = await readMessage(); + await handleMessage(msg); + } catch (e) { + if (e.message.includes('Stream ended') || e.message.includes('Unexpected end of JSON')) { + break; + } + log('Error processing message:', e.message); + } + } +} + process.on('uncaughtException', e => { log('Uncaught:', e); send({ type: 'ERROR', error: e.message }); @@ -137,5 +214,7 @@ process.on('unhandledRejection', e => { log('Unhandled rejection:', e); }); -log('Starting native messaging host'); -readMessages(); \ No newline at end of file +main().catch(e => { + log('Fatal:', e); + process.exit(1); +}); \ No newline at end of file diff --git a/tests/emergencyLockdown.test.js b/tests/emergencyLockdown.test.js new file mode 100644 index 0000000..7b5e15f --- /dev/null +++ b/tests/emergencyLockdown.test.js @@ -0,0 +1,216 @@ +'use strict'; + +const { describe, it, beforeEach } = require('node:test'); +const assert = require('node:assert/strict'); +const EmergencyLockdown = require('../src/security/EmergencyLockdown'); + +class FakeDatabase { + constructor() { + this.data = {}; + } + + get(key) { + return this.data[key]; + } + + set(key, value) { + this.data[key] = value; + } +} + +class FakeEventBus { + constructor() { + this.events = []; + } + + emit(event, data) { + this.events.push({ event, data }); + } +} + +class FakeNotifier { + constructor() { + this.notifications = []; + } + + notify(title, message, type) { + this.notifications.push({ title, message, type }); + } +} + +describe('EmergencyLockdown', () => { + let db, eventBus, notify, lockdown; + + beforeEach(() => { + db = new FakeDatabase(); + eventBus = new FakeEventBus(); + notify = new FakeNotifier(); + lockdown = new EmergencyLockdown(db, eventBus, notify); + }); + + describe('Allowlist management', () => { + it('should return default empty allowlist', () => { + const allowlist = lockdown.getAllowlist(); + assert.deepStrictEqual(allowlist, { interfaces: [], services: [], ips: [] }); + }); + + it('should set allowlist', () => { + const newAllowlist = { + interfaces: ['Ethernet0', 'Wi-Fi'], + services: ['Spooler'], + ips: ['192.168.1.1'] + }; + const result = lockdown.setAllowlist(newAllowlist); + assert.deepStrictEqual(result, newAllowlist); + assert.deepStrictEqual(db.get('lockdown_allowlist'), newAllowlist); + }); + + it('should add to allowlist', () => { + lockdown.addToAllowlist('interfaces', 'Ethernet0'); + lockdown.addToAllowlist('services', 'Spooler'); + lockdown.addToAllowlist('ips', '192.168.1.1'); + + const allowlist = lockdown.getAllowlist(); + assert.strictEqual(allowlist.interfaces.length, 1); + assert.strictEqual(allowlist.interfaces[0], 'ethernet0'); + assert.strictEqual(allowlist.services.length, 1); + assert.strictEqual(allowlist.services[0], 'spooler'); + assert.strictEqual(allowlist.ips.length, 1); + assert.strictEqual(allowlist.ips[0], '192.168.1.1'); + }); + + it('should not add duplicate entries to allowlist', () => { + lockdown.addToAllowlist('interfaces', 'Ethernet0'); + lockdown.addToAllowlist('interfaces', 'Ethernet0'); + + const allowlist = lockdown.getAllowlist(); + assert.strictEqual(allowlist.interfaces.length, 1); + }); + + it('should remove from allowlist', () => { + lockdown.addToAllowlist('interfaces', 'Ethernet0'); + lockdown.addToAllowlist('interfaces', 'Wi-Fi'); + lockdown.removeFromAllowlist('interfaces', 'Ethernet0'); + + const allowlist = lockdown.getAllowlist(); + assert.strictEqual(allowlist.interfaces.length, 1); + assert.strictEqual(allowlist.interfaces[0], 'wi-fi'); + }); + + it('should load allowlist from database on initialization', () => { + db.set('lockdown_allowlist', { + interfaces: ['ethernet0'], + services: ['spooler'], + ips: ['192.168.1.1'] + }); + + const newLockdown = new EmergencyLockdown(db, eventBus, notify); + const allowlist = newLockdown.getAllowlist(); + assert.strictEqual(allowlist.interfaces.length, 1); + assert.strictEqual(allowlist.interfaces[0], 'ethernet0'); + }); + }); + + describe('Network interface operations', () => { + it('should get network interfaces', async () => { + const interfaces = await lockdown.getNetworkInterfaces(); + assert.ok(Array.isArray(interfaces)); + }); + + it('should throw error when disabling interface fails', async () => { + await assert.rejects( + async () => await lockdown.disableInterface('NonExistent'), + (err) => { + assert.ok(err.message.includes('Failed to disable')); + return true; + } + ); + }); + + it('should throw error when enabling interface fails', async () => { + await assert.rejects( + async () => await lockdown.enableInterface('NonExistent'), + (err) => { + assert.ok(err.message.includes('Failed to enable')); + return true; + } + ); + }); + }); + + describe('Service operations', () => { + it('should get non-essential services', async () => { + const services = await lockdown.getNonEssentialServices(); + assert.ok(Array.isArray(services)); + }); + + it('should throw error when stopping service fails', async () => { + await assert.rejects( + async () => await lockdown.stopService('NonExistentService'), + (err) => { + assert.ok(err.message.includes('Failed to stop')); + return true; + } + ); + }); + + it('should throw error when starting service fails', async () => { + await assert.rejects( + async () => await lockdown.startService('NonExistentService'), + (err) => { + assert.ok(err.message.includes('Failed to start')); + return true; + } + ); + }); + }); + + describe('Lockdown status', () => { + it('should return initial status as not locked down', () => { + const status = lockdown.getStatus(); + assert.strictEqual(status.isLockedDown, false); + assert.strictEqual(status.savedNetworkState, null); + assert.strictEqual(status.savedServicesState, null); + }); + + it('should prevent double lockdown', async () => { + lockdown.isLockedDown = true; + const result = await lockdown.lockdown(); + assert.deepStrictEqual(result, { success: false, message: 'Already in lockdown mode' }); + }); + + it('should prevent restore when not locked down', async () => { + const result = await lockdown.restore(); + assert.deepStrictEqual(result, { success: false, message: 'Not in lockdown mode' }); + }); + }); + + describe('Lockdown and restore flow', () => { + it('should handle lockdown errors gracefully', async () => { + // Mock the getNetworkInterfaces to fail + const originalGetNetworkInterfaces = lockdown.getNetworkInterfaces; + lockdown.getNetworkInterfaces = async () => { + throw new Error('Network command failed'); + }; + + try { + await assert.rejects( + async () => await lockdown.lockdown(), + (err) => { + assert.ok(err.message.includes('Lockdown failed')); + assert.strictEqual(lockdown.isLockedDown, false); + return true; + } + ); + } finally { + lockdown.getNetworkInterfaces = originalGetNetworkInterfaces; + } + }); + + it('should handle restore when not locked down', async () => { + const result = await lockdown.restore(); + assert.strictEqual(result.success, false); + assert.strictEqual(result.message, 'Not in lockdown mode'); + }); + }); +}); diff --git a/tests/healthSummary.test.js b/tests/healthSummary.test.js new file mode 100644 index 0000000..2dd6829 --- /dev/null +++ b/tests/healthSummary.test.js @@ -0,0 +1,246 @@ +'use strict'; + +const { describe, it, beforeEach } = require('node:test'); +const assert = require('node:assert/strict'); +const { getTrayHealthSummary } = require('../src/main/healthSummary'); + +class FakeDatabase { + constructor() { + this.data = {}; + } + + getLatestScanReport() { + return this.data.latestScanReport || null; + } + + getSetting(key, defaultValue) { + return this.data.settings?.[key] ?? defaultValue; + } + + setLatestScanReport(report) { + this.data.latestScanReport = report; + } + + setSetting(key, value) { + if (!this.data.settings) this.data.settings = {}; + this.data.settings[key] = value; + } + + getNetworkHistory(minutes) { + return this.data.networkHistory || []; + } + + setNetworkHistory(history) { + this.data.networkHistory = history; + } +} + +class FakeToolRegistry { + constructor(result) { + this.result = result; + } + + async run(tool, params, context) { + return this.result; + } +} + +describe('getTrayHealthSummary', () => { + let db, toolRegistry; + + beforeEach(() => { + db = new FakeDatabase(); + toolRegistry = new FakeToolRegistry({ + ok: true, + data: { + score: 85, + breakdown: { + disk: { reason: 'Good disk health' } + } + } + }); + }); + + describe('Basic functionality', () => { + it('should return health summary with score', async () => { + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.score, 85); + assert.strictEqual(summary.detail, 'Good disk health'); + }); + + it('should handle tool registry error', async () => { + toolRegistry.result = { + ok: false, + error: 'Tool failed' + }; + + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.score, null); + assert.strictEqual(summary.detail, 'Tool failed'); + }); + + it('should handle missing tool result', async () => { + toolRegistry.result = { + ok: true, + data: {} + }; + + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.score, undefined); + assert.strictEqual(summary.detail, 'Protection and resource summary ready.'); + }); + }); + + describe('Last scan info', () => { + it('should include last scan info when available', async () => { + db.setLatestScanReport({ + timestamp: '2026-07-30T12:00:00Z', + files_scanned: 1000, + threats_found: 0 + }); + + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.lastScan.timestamp, '2026-07-30T12:00:00Z'); + assert.strictEqual(summary.lastScan.filesScanned, 1000); + assert.strictEqual(summary.lastScan.threatsFound, 0); + }); + + it('should handle missing last scan report', async () => { + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.lastScan, null); + }); + }); + + describe('Password score', () => { + it('should handle password score setting', async () => { + db.setSetting('feature.lastPasswordScore', '80'); + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.score, 85); + }); + + it('should handle null password score', async () => { + db.setSetting('feature.lastPasswordScore', null); + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.score, 85); + }); + + it('should handle missing password score setting', async () => { + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.score, 85); + }); + }); + + describe('RTP status', () => { + it('should show RTP as disabled when setting is false', async () => { + db.setSetting('feature.realtimeProtection', false); + + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.rtp.enabled, false); + }); + + it('should show RTP as enabled when setting is true', async () => { + db.setSetting('feature.realtimeProtection', true); + + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.rtp.enabled, true); + }); + + it('should handle missing RTP setting', async () => { + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.rtp.enabled, false); + }); + }); + + describe('Network history', () => { + it('should include network stats when history is available', async () => { + const history = [ + { rx_bytes: 1024, tx_bytes: 2048 }, + { rx_bytes: 2048, tx_bytes: 4096 } + ]; + db.setNetworkHistory(history); + + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.network.rxKBs, 2); + assert.strictEqual(summary.network.txKBs, 4); + assert.strictEqual(summary.network.history.length, 2); + }); + + it('should handle empty network history', async () => { + db.setNetworkHistory([]); + + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.network.rxKBs, 0); + assert.strictEqual(summary.network.txKBs, 0); + assert.strictEqual(summary.network.history.length, 0); + }); + + it('should handle missing network history method', async () => { + delete db.getNetworkHistory; + + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.network.rxKBs, 0); + assert.strictEqual(summary.network.txKBs, 0); + assert.strictEqual(summary.network.history.length, 0); + }); + + it('should limit sparkline to last 60 samples', async () => { + const history = Array.from({ length: 100 }, (_, i) => ({ + rx_bytes: i * 1024, + tx_bytes: i * 2048 + })); + db.setNetworkHistory(history); + + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.network.rx.length, 60); + assert.strictEqual(summary.network.tx.length, 60); + assert.strictEqual(summary.network.history.length, 60); + }); + + it('should handle network history errors gracefully', async () => { + db.getNetworkHistory = () => { + throw new Error('Database error'); + }; + + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.network.rxKBs, 0); + assert.strictEqual(summary.network.txKBs, 0); + }); + }); + + describe('Integration scenarios', () => { + it('should provide complete summary with all data', async () => { + db.setLatestScanReport({ + timestamp: '2026-07-30T12:00:00Z', + files_scanned: 5000, + threats_found: 2 + }); + db.setSetting('feature.realtimeProtection', true); + db.setSetting('feature.lastPasswordScore', '75'); + + const history = [ + { rx_bytes: 1024000, tx_bytes: 2048000 }, + { rx_bytes: 2048000, tx_bytes: 4096000 } + ]; + db.setNetworkHistory(history); + + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.score, 85); + assert.strictEqual(summary.rtp.enabled, true); + assert.strictEqual(summary.lastScan.filesScanned, 5000); + assert.strictEqual(summary.lastScan.threatsFound, 2); + assert.strictEqual(summary.network.rxKBs, 2000); + assert.strictEqual(summary.network.txKBs, 4000); + }); + + it('should handle all errors gracefully and return partial data', async () => { + toolRegistry.result = { ok: false, error: 'Tool error' }; + db.setSetting('feature.realtimeProtection', true); + + const summary = await getTrayHealthSummary(db, toolRegistry); + assert.strictEqual(summary.score, null); + assert.strictEqual(summary.detail, 'Tool error'); + // When tool fails, it returns early without RTP/firewall/network data + assert.strictEqual(summary.rtp, undefined); + }); + }); +});