@@ -29,6 +29,7 @@ import { scanBloatware, removeBloatware } from '../ipc/debloater.ipc'
2929import { applyServiceChanges } from '../ipc/service-manager.ipc'
3030import { quarantineMalware , deleteMalware } from '../ipc/malware-scanner.ipc'
3131import { scanForLeftovers } from './uninstall-leftovers'
32+ import { getInstalledProgramsFull } from './program-uninstaller'
3233import { PerfMonitorService } from './perf-monitor'
3334import { cloudLog } from './logger'
3435import 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'
4445import { addCloudHistoryEntry } from './cloud-history-store'
4546import { downloadAndUpdateBlacklist , loadBlacklist } from './threat-blacklist-store'
4647import { threatMonitor } from './threat-monitor'
4748import { isLikelyFalsePositive , deduplicateCves } from './cve-filter'
4849
4950const 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
5253const COMMAND_TIMEOUT_MS = 5 * 60 * 1000
5354const 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
0 commit comments