Skip to content

Commit 37bc5db

Browse files
dbfxclaudegithub-actions[bot]
authored
feat(cloud): AI safety ratings for Startup Manager and Uninstaller (#73)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 46e3d01 commit 37bc5db

File tree

107 files changed

+2729
-1427
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

107 files changed

+2729
-1427
lines changed

src/main/ipc/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import { registerEmptyFolderCleanerIpc } from './empty-folder-cleaner.ipc'
2929
import { registerFileShredderIpc } from './file-shredder.ipc'
3030
import { registerGameModeIpc } from './game-mode.ipc'
3131
import { registerCveScannerIpc } from './cve-scanner.ipc'
32+
import { registerStartupSafetyIpc } from './startup-safety.ipc'
33+
import { registerProgramSafetyIpc } from './program-safety.ipc'
3234
import { getSettings, setSettings, flushSettings, getOnboardingComplete, setOnboardingComplete } from '../services/settings-store'
3335
import { isAdmin } from '../services/elevation'
3436
import { getHistory, addHistoryEntry, clearHistory } from '../services/history-store'
@@ -65,6 +67,8 @@ export function registerCleanerIpc(getWindow: WindowGetter): void {
6567
registerSoftwareUpdaterIpc(getWindow)
6668
registerCloudAgentIpc()
6769
registerCveScannerIpc()
70+
registerStartupSafetyIpc()
71+
registerProgramSafetyIpc()
6872
registerFileShredderIpc(getWindow)
6973
registerGameModeIpc(getWindow)
7074

@@ -103,9 +107,9 @@ export function registerCleanerIpc(getWindow: WindowGetter): void {
103107

104108
// Onboarding
105109
ipcMain.handle(IPC.ONBOARDING_GET, () => getOnboardingComplete())
106-
ipcMain.handle(IPC.ONBOARDING_SET, (_event, value: boolean) => {
110+
ipcMain.handle(IPC.ONBOARDING_SET, async (_event, value: boolean) => {
107111
if (typeof value !== 'boolean') return
108-
setOnboardingComplete(value)
112+
await setOnboardingComplete(value)
109113
})
110114

111115
// Elevation

src/main/ipc/program-safety.ipc.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ipcMain } from 'electron'
2+
import { IPC } from '../../shared/channels'
3+
import { cloudAgent } from '../services/cloud-agent'
4+
5+
export function registerProgramSafetyIpc(): void {
6+
ipcMain.handle(IPC.PROGRAM_SAFETY_FETCH, async () => {
7+
return cloudAgent.getInstalledProgramSafetyRatings()
8+
})
9+
}

src/main/ipc/startup-manager.ipc.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,11 +397,14 @@ export async function toggleStartupItem(
397397
// This is the authoritative signal Windows checks — even if the Run value
398398
// is re-created by the app, Windows will skip it while the marker is 03.
399399
// The value is a 12-byte REG_BINARY: status byte + 8-byte timestamp + padding.
400+
let approvedOk = false
401+
let deleteOk = false
400402
try {
401403
await execNativeUtf8('reg',[
402404
'add', approvedKey, '/v', name, '/t', 'REG_BINARY',
403405
'/d', '030000000000000000000000', '/f'
404406
], { timeout: 10000 })
407+
approvedOk = true
405408
} catch {
406409
// May fail if key doesn't exist yet — fall through to Run deletion
407410
}
@@ -410,10 +413,14 @@ export async function toggleStartupItem(
410413
await execNativeUtf8('reg',[
411414
'delete', location, '/v', name, '/f'
412415
], { timeout: 10000 })
416+
deleteOk = true
413417
} catch {
414-
// Registry op may fail for permissions — still persist state
418+
// Registry op may fail for permissions
415419
}
416420

421+
// If neither registry operation succeeded, the disable didn't take effect
422+
if (!approvedOk && !deleteOk) return false
423+
417424
await withDisabledFileLock(() => {
418425
const disabled = readDisabledEntries()
419426
if (!disabled.some((e) => e.name === name && e.source === source)) {
@@ -430,12 +437,14 @@ export async function toggleStartupItem(
430437
if (!stored) return false
431438
const safeCommand = stored.command
432439

440+
let addOk = false
433441
try {
434442
await execNativeUtf8('reg',[
435443
'add', location, '/v', name, '/t', 'REG_SZ', '/d', safeCommand, '/f'
436444
], { timeout: 10000 })
445+
addOk = true
437446
} catch {
438-
// Registry op may fail for permissions — still persist state
447+
// Registry op may fail for permissions
439448
}
440449

441450
// Write an "enabled" marker (first byte 02) to StartupApproved
@@ -448,6 +457,9 @@ export async function toggleStartupItem(
448457
// Non-critical — Run key entry is sufficient for most apps
449458
}
450459

460+
// If the critical Run key write failed, the enable didn't take effect
461+
if (!addOk) return false
462+
451463
await withDisabledFileLock(() => {
452464
const current = readDisabledEntries()
453465
writeDisabledEntries(current.filter((e) => !(e.name === name && e.source === source)))

src/main/ipc/startup-safety.ipc.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ipcMain } from 'electron'
2+
import { IPC } from '../../shared/channels'
3+
import { cloudAgent } from '../services/cloud-agent'
4+
5+
export function registerStartupSafetyIpc(): void {
6+
ipcMain.handle(IPC.STARTUP_SAFETY_FETCH, async () => {
7+
return cloudAgent.getStartupSafetyRatings()
8+
})
9+
}

src/main/services/cloud-agent.ts

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { scanBloatware, removeBloatware } from '../ipc/debloater.ipc'
2929
import { applyServiceChanges } from '../ipc/service-manager.ipc'
3030
import { quarantineMalware, deleteMalware } from '../ipc/malware-scanner.ipc'
3131
import { scanForLeftovers } from './uninstall-leftovers'
32+
import { getInstalledProgramsFull } from './program-uninstaller'
3233
import { PerfMonitorService } from './perf-monitor'
3334
import { cloudLog } from './logger'
3435
import type {
@@ -40,14 +41,14 @@ import type {
4041
HealthReport,
4142
AllowedScanType,
4243
} from './cloud-agent-types'
43-
import type { ScanResult, CloudActionEntry } from '../../shared/types'
44+
import type { ScanResult, CloudActionEntry, StartupSafetyResult } from '../../shared/types'
4445
import { addCloudHistoryEntry } from './cloud-history-store'
4546
import { downloadAndUpdateBlacklist, loadBlacklist } from './threat-blacklist-store'
4647
import { threatMonitor } from './threat-monitor'
4748
import { isLikelyFalsePositive, deduplicateCves } from './cve-filter'
4849

4950
const execFileAsync = promisify(execFile)
50-
const DEFAULT_SERVER_URL = app.isPackaged ? 'https://cloud.usekudu.com' : 'http://localhost:8000'
51+
const DEFAULT_SERVER_URL = app.isPackaged ? 'https://cloud.usekudu.com' : 'https://cloud.usekudu.com'
5152

5253
const COMMAND_TIMEOUT_MS = 5 * 60 * 1000
5354
const LONG_COMMAND_TIMEOUT_MS = 30 * 60 * 1000 // for bulk update / install commands
@@ -249,6 +250,18 @@ class CloudAgentService {
249250
}
250251
}
251252

253+
async getStartupSafetyRatings(): Promise<StartupSafetyResult> {
254+
if (this.status !== 'connected') throw new Error('Cloud agent not connected')
255+
this.startupItems = null
256+
return this.submitStartupPrograms()
257+
}
258+
259+
async getInstalledProgramSafetyRatings(): Promise<StartupSafetyResult> {
260+
if (this.status !== 'connected') throw new Error('Cloud agent not connected')
261+
this.cachedInstalledPrograms = null
262+
return this.submitInstalledPrograms()
263+
}
264+
252265
async link(apiKey: string): Promise<{ success: boolean; error?: string }> {
253266
try {
254267
// Stop any existing connection before re-linking
@@ -510,6 +523,8 @@ class CloudAgentService {
510523
this.startTelemetry()
511524
this.startHealthReports()
512525
this.startThreatMonitor()
526+
this.syncStartupSafety().catch(() => {})
527+
this.syncInstalledProgramSafety().catch(() => {})
513528
})
514529

515530
this.channel.bind('pusher:subscription_error', (err: unknown) => {
@@ -1012,6 +1027,8 @@ class CloudAgentService {
10121027
this.healthReportTimer = setInterval(() => {
10131028
if (this.status === 'connected') {
10141029
this.collectAndSendHealthReport()
1030+
this.syncStartupSafety().catch(() => {})
1031+
this.syncInstalledProgramSafety().catch(() => {})
10151032
}
10161033
}, HEALTH_REPORT_INTERVAL_MS)
10171034
}
@@ -2461,6 +2478,142 @@ class CloudAgentService {
24612478
? await toggleStartupItemWin32(name, location, command, source as any, enabled)
24622479
: await getPlatform().startup.toggleItem(name, location, command, source as any, enabled)
24632480
await this.postCommandResult(requestId, success, { name, enabled }, success ? undefined : 'Failed to toggle startup item')
2481+
if (success) this.syncStartupSafety().catch(() => {})
2482+
}
2483+
2484+
// ─── Startup Safety Enrichment ──────────────────────────
2485+
2486+
private startupItems: import('../../shared/types').StartupItem[] | null = null
2487+
2488+
private async submitStartupPrograms(): Promise<StartupSafetyResult> {
2489+
if (!this.startupItems) {
2490+
this.startupItems = process.platform === 'win32'
2491+
? await listStartupItemsWin32()
2492+
: await getPlatform().startup.listItems()
2493+
}
2494+
const raw = (await this.postApi(`/devices/${encodeURIComponent(this.deviceId)}/startup-programs`, {
2495+
items: this.startupItems.map((i) => ({
2496+
name: i.name,
2497+
displayName: i.displayName,
2498+
command: i.command,
2499+
location: i.location,
2500+
source: i.source,
2501+
enabled: i.enabled,
2502+
publisher: i.publisher,
2503+
impact: i.impact,
2504+
})),
2505+
})) as Record<string, unknown> | null
2506+
const rawItems = Array.isArray(raw?.ratings) ? raw!.ratings : []
2507+
const ratings = rawItems
2508+
.filter((item: unknown): item is Record<string, unknown> =>
2509+
item !== null && typeof item === 'object' &&
2510+
typeof (item as Record<string, unknown>).name === 'string' &&
2511+
typeof (item as Record<string, unknown>).safety_score === 'number'
2512+
)
2513+
.map((item) => ({
2514+
name: String(item.name),
2515+
safetyScore: Math.max(1, Math.min(10, Math.round(Number(item.safety_score)))),
2516+
description: typeof item.description === 'string' ? item.description.slice(0, 500) : '',
2517+
analyzedAt: typeof item.analyzed_at === 'string' ? item.analyzed_at : '',
2518+
}))
2519+
const pending = typeof raw?.pending === 'number' ? raw.pending : 0
2520+
return { ratings, pending }
2521+
}
2522+
2523+
private async syncStartupSafety(): Promise<void> {
2524+
try {
2525+
// Clear cached items so we re-list from OS
2526+
this.startupItems = null
2527+
let result = await this.submitStartupPrograms()
2528+
this.pushSafetyToRenderer(result)
2529+
2530+
// Poll while analyses are still pending (max 10 retries, 5s apart)
2531+
let retries = 0
2532+
while (result.pending > 0 && retries < 10) {
2533+
retries++
2534+
await new Promise((r) => setTimeout(r, 5000))
2535+
if (this.status !== 'connected') break
2536+
result = await this.submitStartupPrograms()
2537+
this.pushSafetyToRenderer(result)
2538+
}
2539+
} catch (err) {
2540+
cloudLog('ERROR', `Startup safety sync failed: ${err}`)
2541+
}
2542+
}
2543+
2544+
private pushSafetyToRenderer(result: StartupSafetyResult): void {
2545+
const win = BrowserWindow.getAllWindows()[0]
2546+
if (win && !win.isDestroyed()) {
2547+
win.webContents.send(IPC.STARTUP_SAFETY_UPDATED, result)
2548+
}
2549+
}
2550+
2551+
// ─── Installed Program Safety Enrichment ──────────────────
2552+
2553+
private cachedInstalledPrograms: import('../../shared/types').InstalledProgram[] | null = null
2554+
2555+
private async submitInstalledPrograms(): Promise<StartupSafetyResult> {
2556+
if (!this.cachedInstalledPrograms) {
2557+
this.cachedInstalledPrograms = await getInstalledProgramsFull()
2558+
}
2559+
const raw = (await this.postApi(`/devices/${encodeURIComponent(this.deviceId)}/installed-programs`, {
2560+
items: this.cachedInstalledPrograms.map((p) => ({
2561+
name: p.displayName,
2562+
displayName: p.displayName,
2563+
publisher: p.publisher,
2564+
version: p.displayVersion,
2565+
installDate: p.installDate,
2566+
estimatedSize: p.estimatedSize,
2567+
installLocation: p.installLocation,
2568+
isSystemComponent: p.isSystemComponent,
2569+
})),
2570+
})) as Record<string, unknown> | null
2571+
cloudLog('DEBUG', `installed-programs response: pending=${raw?.pending}, ratings=${Array.isArray(raw?.ratings) ? raw!.ratings.length : 'none'}, keys=${raw ? Object.keys(raw).join(',') : 'null'}`)
2572+
const rawItems = Array.isArray(raw?.ratings) ? raw!.ratings : []
2573+
if (rawItems.length > 0) {
2574+
cloudLog('DEBUG', `installed-programs first rating sample: ${JSON.stringify(rawItems[0]).slice(0, 200)}`)
2575+
}
2576+
const ratings = rawItems
2577+
.filter((item: unknown): item is Record<string, unknown> =>
2578+
item !== null && typeof item === 'object' &&
2579+
typeof (item as Record<string, unknown>).name === 'string' &&
2580+
typeof (item as Record<string, unknown>).safety_score === 'number'
2581+
)
2582+
.map((item) => ({
2583+
name: String(item.name),
2584+
safetyScore: Math.max(1, Math.min(10, Math.round(Number(item.safety_score)))),
2585+
description: typeof item.description === 'string' ? item.description.slice(0, 500) : '',
2586+
analyzedAt: typeof item.analyzed_at === 'string' ? item.analyzed_at : '',
2587+
}))
2588+
const pending = typeof raw?.pending === 'number' ? raw.pending : 0
2589+
cloudLog('DEBUG', `installed-programs parsed: ${ratings.length} ratings, ${pending} pending`)
2590+
return { ratings, pending }
2591+
}
2592+
2593+
private async syncInstalledProgramSafety(): Promise<void> {
2594+
try {
2595+
this.cachedInstalledPrograms = null
2596+
let result = await this.submitInstalledPrograms()
2597+
this.pushProgramSafetyToRenderer(result)
2598+
2599+
let retries = 0
2600+
while (result.pending > 0 && retries < 10) {
2601+
retries++
2602+
await new Promise((r) => setTimeout(r, 5000))
2603+
if (this.status !== 'connected') break
2604+
result = await this.submitInstalledPrograms()
2605+
this.pushProgramSafetyToRenderer(result)
2606+
}
2607+
} catch (err) {
2608+
cloudLog('ERROR', `Installed program safety sync failed: ${err}`)
2609+
}
2610+
}
2611+
2612+
private pushProgramSafetyToRenderer(result: StartupSafetyResult): void {
2613+
const win = BrowserWindow.getAllWindows()[0]
2614+
if (win && !win.isDestroyed()) {
2615+
win.webContents.send(IPC.PROGRAM_SAFETY_UPDATED, result)
2616+
}
24642617
}
24652618

24662619
private perfMonitor: PerfMonitorService | null = null

src/main/services/settings-store.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,11 +260,11 @@ export function getOnboardingComplete(): boolean {
260260
return readStore().onboardingComplete
261261
}
262262

263-
export function setOnboardingComplete(value: boolean): void {
263+
export function setOnboardingComplete(value: boolean): Promise<void> {
264264
const prev = writeLock
265265
let unlock: () => void
266266
writeLock = new Promise<void>((r) => { unlock = r })
267-
prev.then(() => {
267+
return prev.then(() => {
268268
try {
269269
const data = readStore()
270270
data.onboardingComplete = value

src/preload/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import type {
7171
GameModeStatus,
7272
GameModeProgress,
7373
CvePageResult,
74+
StartupSafetyResult,
7475
} from '../shared/types'
7576

7677
const api = {
@@ -143,6 +144,12 @@ const api = {
143144
startupDelete: (name: string, location: string, source: string): Promise<boolean> =>
144145
ipcRenderer.invoke(IPC.STARTUP_DELETE, name, location, source),
145146
startupBootTrace: (): Promise<StartupBootTrace> => ipcRenderer.invoke(IPC.STARTUP_BOOT_TRACE),
147+
startupSafetyFetch: (): Promise<StartupSafetyResult> => ipcRenderer.invoke(IPC.STARTUP_SAFETY_FETCH),
148+
onStartupSafetyUpdated: (callback: (data: StartupSafetyResult) => void) => {
149+
const handler = (_event: Electron.IpcRendererEvent, data: StartupSafetyResult) => callback(data)
150+
ipcRenderer.on(IPC.STARTUP_SAFETY_UPDATED, handler)
151+
return () => { ipcRenderer.removeListener(IPC.STARTUP_SAFETY_UPDATED, handler) }
152+
},
146153

147154
// Network cleanup
148155
networkScan: (): Promise<NetworkItem[]> => ipcRenderer.invoke(IPC.NETWORK_SCAN),
@@ -326,6 +333,12 @@ const api = {
326333
ipcRenderer.on(IPC.UNINSTALLER_PROGRESS, handler)
327334
return () => { ipcRenderer.removeListener(IPC.UNINSTALLER_PROGRESS, handler) }
328335
},
336+
programSafetyFetch: (): Promise<StartupSafetyResult> => ipcRenderer.invoke(IPC.PROGRAM_SAFETY_FETCH),
337+
onProgramSafetyUpdated: (callback: (data: StartupSafetyResult) => void) => {
338+
const handler = (_event: Electron.IpcRendererEvent, data: StartupSafetyResult) => callback(data)
339+
ipcRenderer.on(IPC.PROGRAM_SAFETY_UPDATED, handler)
340+
return () => { ipcRenderer.removeListener(IPC.PROGRAM_SAFETY_UPDATED, handler) }
341+
},
329342

330343
// Software Updater
331344
softwareUpdateCheck: (): Promise<UpdateCheckResult> =>

src/renderer/src/App.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,16 @@ export function App() {
113113
// Hydrate Game Mode status so the sidebar badge works on all pages
114114
useEffect(() => { initGameModeStore() }, [])
115115

116-
if (!onboardingChecked) return null
116+
if (!onboardingChecked) {
117+
return (
118+
<div className="flex h-screen w-screen items-center justify-center" style={{ background: '#09090b' }}>
119+
<div className="flex flex-col items-center gap-4">
120+
<img src="" alt="" className="h-16 w-16 rounded-2xl" style={{ visibility: 'hidden' }} />
121+
<div className="h-5 w-5 animate-spin rounded-full border-2 border-zinc-700 border-t-amber-500" />
122+
</div>
123+
</div>
124+
)
125+
}
117126

118127
return (
119128
<PlatformContext value={platformInfo}>

src/renderer/src/hooks/usePlatform.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { PlatformInfo } from '../../../shared/types'
33

44
const defaultInfo: PlatformInfo = {
55
platform: 'win32',
6-
features: { registry: true, debloater: true, drivers: true, restorePoint: true, bootTrace: true },
6+
features: { registry: true, debloater: true, drivers: true, restorePoint: true, bootTrace: true, gameMode: true },
77
}
88

99
const PlatformContext = createContext<PlatformInfo>(defaultInfo)

0 commit comments

Comments
 (0)