# ═══════════════════════════════════════════════════════════════════════════════════════ # VXN ENTERPRISE AGENT v5.2.0 - PROFESSIONAL RMM WITH LUCAS AI + WEBROOT # ═══════════════════════════════════════════════════════════════════════════════════════ # # ENTERPRISE FEATURES: # ✓ Lucas AI - Autonomous IT technician with local intelligence # ✓ Max Guardian - Security orchestration and threat response # ✓ Webroot SecureAnywhere - INCLUDED with all managed devices # ✓ Network Discovery - Scans LAN for devices, auto-deploys agents # ✓ Threat Detection - Monitors for ransomware, malware, intrusions # ✓ Auto-Remediation - Fixes common issues without human intervention # ✓ BitLocker Management - Collects and backs up recovery keys # ✓ Full System Inventory - Hardware, software, licenses, security # ✓ Real-time Monitoring - CPU, RAM, disk, network with smart alerting # ✓ Patch Management - Windows updates, driver updates # ✓ Self-Healing - Auto-update, crash recovery, watchdog # ✓ Encrypted Communications - TLS 1.2+ only # # SECURITY STACK (Defense in Depth): # Layer 1: Max Guardian (VXN Native Protection) # Layer 2: Windows Defender (Built-in) # Layer 3: Webroot SecureAnywhere (INCLUDED - Auto-deployed) # # Copyright (c) 2026 Vellunox Technologies LLC # ═══════════════════════════════════════════════════════════════════════════════════════ $ErrorActionPreference = 'SilentlyContinue' $WarningPreference = 'SilentlyContinue' # ═══════════════════════════════════════════════════════════════════════════════════════ # CONFIGURATION # ═══════════════════════════════════════════════════════════════════════════════════════ $script:AgentVersion = "6.1.5" $script:AgentName = "VXN Enterprise Agent" $script:InstallPath = "$env:ProgramData\Vellunox\Agent" $script:ConfigPath = "$script:InstallPath\config.json" $script:LogPath = "$script:InstallPath\logs\agent.log" $script:ScriptPath = "$script:InstallPath\vxn-agent.ps1" $script:DataPath = "$script:InstallPath\data" $script:SourcePath = "$script:InstallPath\sources" $script:DownloadCachePath = "$script:InstallPath\downloads" $script:SecurityPolicyPath = "$script:InstallPath\security" # Primary RMM Microservice (dedicated, auto-scaling Container App) $script:RmmServiceUrl = "https://rmm.vellunox.com" # Fallback to main server if RMM service unavailable $script:ServerUrl = "https://vellunox.com" $script:WebSocketUrl = "wss://vellunox-app.azurewebsites.net/ws/agent" # Heartbeat goes directly to main server (includes pending commands) # NOTE: rmm.vellunox.com microservice does NOT have pending commands - use main server! $script:HeartbeatUrl = "https://vellunox.com/api/rmm/heartbeat" $script:HeartbeatFallbackUrl = "https://vellunox.com/api/vxn/heartbeat" # Auto-update settings $script:UpdateCheckUrl = "https://vellunox.com/api/vxn/agent-version" $script:AgentDownloadUrl = "https://vellunox.com/downloads/vxn-agent-service.ps1" $script:LastUpdateCheck = $null # Fast polling interval (10 seconds) when WebSocket unavailable $script:FastPollInterval = 10 $script:FullHeartbeatInterval = 300 # Full data every 5 minutes # Lucas AI State $script:LucasMetrics = @{ issuesFixed = 0 threatsBlocked = 0 devicesDiscovered = 0 patchesInstalled = 0 autoRemediations = 0 networkScans = 0 uptimeHours = 0 } # ═══════════════════════════════════════════════════════════════════════════════════════ # WEBROOT SECURITYANYWHERE - INCLUDED WITH ALL MANAGED DEVICES # ═══════════════════════════════════════════════════════════════════════════════════════ $script:WebrootInstallPath = "$env:ProgramFiles\Webroot\WRSA.exe" $script:WebrootDownloadUrl = "https://anywhere.webrootcloudav.com/zerol/wsainstall.exe" # Keycode should be set by server during agent deployment or via config $script:WebrootKeycode = $null # Will be retrieved from server # Max Guardian State $script:MaxGuardianMetrics = @{ threatsDetected = 0 threatsBlocked = 0 scansCompleted = 0 incidentsHandled = 0 webrootStatus = "unknown" } # ═══════════════════════════════════════════════════════════════════════════════════════ # LUCAS / AI OPS DESKTOP WIDGET # The repair agent runs as SYSTEM. The visible widget is installed and maintained by # SYSTEM, then launched in the interactive user session so the client can see it. # ═══════════════════════════════════════════════════════════════════════════════════════ $script:LucasWidgetEnabled = $true $script:LucasWidgetDownloadUrl = "https://vellunox.com/downloads/vxn-aiops-widget.ps1" $script:LucasWidgetTaskName = "VXN AI Ops Widget" $script:LucasWidgetDirectory = "$script:InstallPath\widget" $script:LucasWidgetProcess = $null function Disable-LegacyVxnVisibleStartup { try { $legacyTaskNames = @( "VXN Agent", "VXN Agent Monitor", "Vellunox VXN Agent", "Vellunox AI Ops Widget Watcher", "Vellunox Local Agent - User", "Vellunox Local Agent - System Role" ) foreach ($taskName in $legacyTaskNames) { $task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue if ($task) { Disable-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue | Out-Null Write-Lucas "Disabled legacy visible startup task: $taskName" "STARTUP" } } $runKeys = @( "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run", "HKLM:\Software\Microsoft\Windows\CurrentVersion\Run" ) $legacyRunPattern = "(?i)(VXN Agent|Vellunox Protection|Protection Active|rmm-agent|electron|start-local-agent\.cmd|start-system-agent\.cmd|Vellunox AI Ops Watcher\.cmd)" $keepRunPattern = "(?i)(VXN\.Agent\.Overlay\.exe|Aria\.TrayAssistant\.exe|vxn-aiops-widget\.ps1|vxn-agent\.ps1)" foreach ($runKey in $runKeys) { $props = Get-ItemProperty -Path $runKey -ErrorAction SilentlyContinue if (-not $props) { continue } foreach ($prop in $props.PSObject.Properties) { if ($prop.Name -like "PS*") { continue } $value = [string]$prop.Value if ($value -match $keepRunPattern) { continue } if ($prop.Name -match $legacyRunPattern -or $value -match $legacyRunPattern) { Remove-ItemProperty -Path $runKey -Name $prop.Name -ErrorAction SilentlyContinue Write-Lucas "Removed legacy visible Run entry: $($prop.Name)" "STARTUP" } } } $startupFolders = @( [Environment]::GetFolderPath("Startup"), "$env:ProgramData\Microsoft\Windows\Start Menu\Programs\Startup" ) | Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique foreach ($folder in $startupFolders) { Get-ChildItem -LiteralPath $folder -Filter "*.lnk" -ErrorAction SilentlyContinue | ForEach-Object { $target = "" try { $shell = New-Object -ComObject WScript.Shell $target = $shell.CreateShortcut($_.FullName).TargetPath } catch {} $combined = "$($_.Name) $target" if ($combined -notmatch $keepRunPattern -and $combined -match $legacyRunPattern) { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue Write-Lucas "Removed legacy visible startup shortcut: $($_.Name)" "STARTUP" } } } Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowTitle -match "(?i)(Vellunox - Protection Active|VXN Agent|Protection Active)" -and $_.Path -match "(?i)\\Vellunox\\|\\VXN\\|rmm-agent|electron" -and $_.ProcessName -notmatch "(?i)VXN\.Agent\.Overlay|Aria" } | Stop-Process -Force -ErrorAction SilentlyContinue } catch { Write-Lucas "Legacy visible startup cleanup warning: $($_.Exception.Message)" "WARN" } } function Start-LucasWidget { if (-not $script:LucasWidgetEnabled) { return } try { Disable-LegacyVxnVisibleStartup New-Item -ItemType Directory -Path $script:LucasWidgetDirectory -Force -ErrorAction SilentlyContinue | Out-Null $widgetPath = Join-Path $script:LucasWidgetDirectory "vxn-aiops-widget.ps1" $manifestPath = Join-Path $script:LucasWidgetDirectory "manifest.json" $systemTokenPath = Join-Path $script:LucasWidgetDirectory "system.token" $userTokenPath = Join-Path $script:LucasWidgetDirectory "user.token" if (-not (Test-Path $widgetPath)) { $wc = New-Object System.Net.WebClient $wc.DownloadFile($script:LucasWidgetDownloadUrl, $widgetPath) } if (-not (Test-Path $systemTokenPath)) { $tokenBytes = New-Object byte[] 32 [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($tokenBytes) [Convert]::ToBase64String($tokenBytes) | Out-File -FilePath $systemTokenPath -Encoding ASCII -Force } if (-not (Test-Path $userTokenPath)) { $tokenBytes = New-Object byte[] 32 [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($tokenBytes) [Convert]::ToBase64String($tokenBytes) | Out-File -FilePath $userTokenPath -Encoding ASCII -Force } $config = Get-AgentConfig $deviceId = if ($config.deviceId) { $config.deviceId } else { $env:COMPUTERNAME } $clientId = if ($config.clientId) { $config.clientId } elseif ($config.tenantId) { $config.tenantId } else { "vellunox" } $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } $manifest = @{ serverUrl = $serverUrl deviceId = $deviceId clientId = $clientId systemAgentId = $deviceId systemAgentPort = 8787 userAgentPort = 8788 systemTokenPath = $systemTokenPath userTokenPath = $userTokenPath pollSeconds = $script:FastPollInterval systemService = "VXN Agent Service" widgetManagedBy = "SYSTEM" supportProvider = "Lucas + Vellunox Remote" } $manifest | ConvertTo-Json -Depth 5 | Out-File -FilePath $manifestPath -Encoding UTF8 -Force $explorerProcess = Get-Process explorer -ErrorAction SilentlyContinue | Select-Object -First 1 if ($explorerProcess) { $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$widgetPath`"" $trigger = New-ScheduledTaskTrigger -AtLogOn $principal = New-ScheduledTaskPrincipal -GroupId "BUILTIN\Users" -RunLevel LeastPrivilege $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew Register-ScheduledTask -TaskName $script:LucasWidgetTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force -ErrorAction SilentlyContinue | Out-Null Start-ScheduledTask -TaskName $script:LucasWidgetTaskName -ErrorAction SilentlyContinue Write-Lucas "AI Ops widget installed and launched for interactive users" "WIDGET" } else { Write-Lucas "AI Ops widget installed; launch deferred until user logon" "WIDGET" } } catch { Write-Lucas "Widget setup failed: $($_.Exception.Message)" "WARN" } } function Stop-LucasWidget { if ($script:LucasWidgetProcess -and -not $script:LucasWidgetProcess.HasExited) { $script:LucasWidgetProcess.Kill() $script:LucasWidgetProcess = $null } Unregister-ScheduledTask -TaskName $script:LucasWidgetTaskName -Confirm:$false -ErrorAction SilentlyContinue } function Enable-LucasWidget { $script:LucasWidgetEnabled = $true Write-Lucas "Lucas Desktop Widget ENABLED" "WIDGET" Start-LucasWidget } function Disable-LucasWidget { $script:LucasWidgetEnabled = $false Stop-LucasWidget Write-Lucas "Lucas Desktop Widget DISABLED" "WIDGET" } # TLS Security try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13 } catch {} try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} # ═══════════════════════════════════════════════════════════════════════════════════════ # LOGGING SYSTEM # ═══════════════════════════════════════════════════════════════════════════════════════ function Write-Log { param([string]$Message, [string]$Level = "INFO") $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $logEntry = "[$timestamp] [$Level] $Message" try { $logDir = Split-Path $script:LogPath -Parent if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } $logEntry | Out-File -FilePath $script:LogPath -Append -Encoding UTF8 # Rotate log if > 10MB if ((Get-Item $script:LogPath -ErrorAction SilentlyContinue).Length -gt 10MB) { $archivePath = $script:LogPath -replace '\.log$', "-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" Move-Item $script:LogPath $archivePath -Force } } catch {} } function Write-Lucas { param([string]$Message, [string]$Action = "INFO") Write-Log "[LUCAS] $Message" $Action } function Write-Max { param([string]$Message, [string]$Action = "SECURITY") Write-Log "[MAX] $Message" $Action } function Resolve-VxnSafePath { param([string]$Path) try { if (-not $Path) { return $null } $expanded = [Environment]::ExpandEnvironmentVariables($Path) return [System.IO.Path]::GetFullPath($expanded) } catch { return $null } } function Get-MaxSecurityRoots { $programFilesX86 = [Environment]::GetEnvironmentVariable("ProgramFiles(x86)") $ownerWorkspace = Join-Path $env:USERPROFILE "Documents\Codex\2026-04-29\please-continue-off-where-we-left\Vellunox" $ulbrichsSourceRoot = Join-Path $env:USERPROFILE "OneDrive\Data\Important Files\Ulbrichs Woodshop" $sourceRoot = if ($env:VELLUNOX_SOURCE_ROOT) { $env:VELLUNOX_SOURCE_ROOT } elseif ($env:VXN_SOURCE_ROOT) { $env:VXN_SOURCE_ROOT } else { $null } return @( "$env:ProgramData\Vellunox", $script:InstallPath, $script:SourcePath, $script:DownloadCachePath, $script:LucasWidgetDirectory, "$env:ProgramData\Vellunox\Sources", "$env:ProgramData\Vellunox\Downloads", "$env:ProgramData\Vellunox\Temp", "$env:ProgramFiles\Vellunox", $(if ($programFilesX86) { Join-Path $programFilesX86 "Vellunox" }), "$env:LOCALAPPDATA\Vellunox", (Join-Path $env:USERPROFILE ".codex\generated_images"), $ownerWorkspace, $ulbrichsSourceRoot, $sourceRoot ) | Where-Object { $_ } | Select-Object -Unique } function Get-MaxSourceAllowlistPaths { $programFilesX86 = [Environment]::GetEnvironmentVariable("ProgramFiles(x86)") $ulbrichsSourceRoot = Join-Path $env:USERPROFILE "OneDrive\Data\Important Files\Ulbrichs Woodshop" return @( $script:InstallPath, $script:SourcePath, $script:DownloadCachePath, $script:SecurityPolicyPath, $script:LucasWidgetDirectory, "$env:ProgramData\Vellunox\Sources", "$env:ProgramData\Vellunox\Downloads", "$env:ProgramData\Vellunox\Temp", "$env:ProgramFiles\Vellunox", $(if ($programFilesX86) { Join-Path $programFilesX86 "Vellunox" }), (Join-Path $ulbrichsSourceRoot "Vcarve"), (Join-Path $ulbrichsSourceRoot "LightBurn"), (Join-Path $ulbrichsSourceRoot "Laser") ) | Where-Object { $_ } | Select-Object -Unique } function Set-MaxSecurityAllowlist { param($Command) Write-Max "Applying approved Vellunox security allowlist" "ALLOWLIST" $payload = if ($Command.payload) { $Command.payload } elseif ($Command.command) { $Command.command } else { @{} } $roots = @((Get-MaxSecurityRoots) + @($Command.roots) + @($payload.roots)) | Where-Object { $_ } | Select-Object -Unique $requested = @(@($Command.paths) + @($payload.paths)) | Where-Object { $_ } if ($requested.Count -eq 0) { $requested = Get-MaxSourceAllowlistPaths } $approved = New-Object System.Collections.Generic.List[string] foreach ($item in $requested) { $full = Resolve-VxnSafePath $item if (-not $full) { continue } foreach ($root in $roots) { $safeRoot = Resolve-VxnSafePath $root if (-not $safeRoot) { continue } $safeRoot = $safeRoot.TrimEnd('\') if ($full.Equals($safeRoot, [StringComparison]::OrdinalIgnoreCase) -or $full.StartsWith(($safeRoot + "\"), [StringComparison]::OrdinalIgnoreCase)) { if (-not $approved.Contains($full)) { $approved.Add($full) | Out-Null } break } } } $defender = @() if (Get-Command Add-MpPreference -ErrorAction SilentlyContinue) { foreach ($path in $approved) { try { if (-not (Test-Path $path)) { New-Item -ItemType Directory -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } Add-MpPreference -ExclusionPath $path -ErrorAction Stop $defender += @{ path = $path; status = "applied" } } catch { $defender += @{ path = $path; status = "failed"; error = $_.Exception.Message } } } } $requestDir = $script:SecurityPolicyPath New-Item -ItemType Directory -Path $requestDir -Force -ErrorAction SilentlyContinue | Out-Null $requestPath = Join-Path $requestDir "splashtop-antivirus-allowlist-request.json" $splashtopAv = Test-Path "$env:ProgramFiles\Splashtop\Splashtop Antivirus" $request = @{ ownerAgent = "max" action = "splashtop-antivirus-policy-allowlist-request" policy = "vellunox-source-files" paths = @($approved) splashtopAntivirusInstalled = $splashtopAv reason = if ($Command.reason) { $Command.reason } else { "Default Vellunox source, installer, and support tool allowlist" } note = "Splashtop Antivirus is policy protected. Max records this request for central policy application instead of bypassing protection." createdAt = (Get-Date -Format "o") } $request | ConvertTo-Json -Depth 6 | Out-File -FilePath $requestPath -Encoding UTF8 -Force $script:MaxGuardianMetrics.incidentsHandled++ return @{ success = ($approved.Count -gt 0) ownerAgent = "max" approvedPaths = @($approved) defender = $defender splashtopAntivirusInstalled = $splashtopAv splashtopPolicyRequest = $requestPath rejectedCount = [Math]::Max(0, $requested.Count - $approved.Count) } } function Initialize-MaxSecurityAllowlist { try { New-Item -ItemType Directory -Path $script:SourcePath -Force -ErrorAction SilentlyContinue | Out-Null New-Item -ItemType Directory -Path $script:DownloadCachePath -Force -ErrorAction SilentlyContinue | Out-Null New-Item -ItemType Directory -Path $script:SecurityPolicyPath -Force -ErrorAction SilentlyContinue | Out-Null $markerPath = Join-Path $script:SecurityPolicyPath "source-allowlist-applied.json" $needsRefresh = $true if (Test-Path $markerPath) { $age = (Get-Date) - (Get-Item $markerPath).LastWriteTime if ($age.TotalHours -lt 24) { $needsRefresh = $false } } if (-not $needsRefresh) { return $null } $result = Set-MaxSecurityAllowlist @{ paths = @(Get-MaxSourceAllowlistPaths) reason = "Always allow Vellunox-owned source and installer files for Splashtop Antivirus policy" } @{ appliedAt = (Get-Date -Format "o") result = $result } | ConvertTo-Json -Depth 7 | Out-File -FilePath $markerPath -Encoding UTF8 -Force return $result } catch { Write-Max "Source allowlist initialization failed: $($_.Exception.Message)" "WARN" return $null } } function Send-VxnCommandResult { param($Command, $Payload) try { $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } $body = @{ deviceId = $config.deviceId hostname = $env:COMPUTERNAME commandId = $Command.id ownerAgent = if ($Payload.ownerAgent) { $Payload.ownerAgent } else { "lucas" } result = $Payload maxGuardian = $script:MaxGuardianMetrics } | ConvertTo-Json -Depth 7 -Compress $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/rmm/command-result", $body) | Out-Null } catch {} } function Invoke-VxnDefenderQuickScan { Write-Lucas "Running Defender quick scan..." "SECURITY" $statusBefore = $null $statusAfter = $null $started = $false $signatureUpdateAttempted = $false try { $statusBefore = Get-MpComputerStatus -ErrorAction SilentlyContinue } catch {} if (Get-Command Update-MpSignature -ErrorAction SilentlyContinue) { try { $signatureUpdateAttempted = $true Update-MpSignature -ErrorAction SilentlyContinue | Out-Null } catch {} } if (Get-Command Start-MpScan -ErrorAction SilentlyContinue) { try { Start-MpScan -ScanType QuickScan -ErrorAction SilentlyContinue | Out-Null $started = $true $script:MaxGuardianMetrics.scansCompleted++ } catch {} } try { $statusAfter = Get-MpComputerStatus -ErrorAction SilentlyContinue } catch {} return @{ success = $started type = "defender-scan" ownerAgent = "max" scanType = "QuickScan" signatureUpdateAttempted = $signatureUpdateAttempted antivirusEnabled = if ($statusAfter) { $statusAfter.AntivirusEnabled } elseif ($statusBefore) { $statusBefore.AntivirusEnabled } else { $null } signatureAge = if ($statusAfter) { $statusAfter.AntivirusSignatureAge } elseif ($statusBefore) { $statusBefore.AntivirusSignatureAge } else { $null } executedAt = (Get-Date -Format "o") } } function Invoke-VxnTempCleanup { Write-Lucas "Running safe temp cleanup..." "CLEANUP" $cutoff = (Get-Date).AddDays(-2) $roots = @( $env:TEMP, "$env:SystemRoot\Temp", "$env:ProgramData\Vellunox\Temp" ) try { Get-ChildItem "C:\Users" -Directory -ErrorAction SilentlyContinue | ForEach-Object { $roots += (Join-Path $_.FullName "AppData\Local\Temp") } } catch {} $deleted = 0 $freedBytes = 0 $errors = 0 foreach ($root in ($roots | Where-Object { $_ } | Select-Object -Unique)) { if (-not (Test-Path $root)) { continue } try { Get-ChildItem $root -File -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt $cutoff } | ForEach-Object { $len = $_.Length try { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop $deleted++ $freedBytes += $len } catch { $errors++ } } } catch { $errors++ } } return @{ success = $true type = "cleanup-temp" ownerAgent = "lucas" deletedFiles = $deleted freedMB = [math]::Round($freedBytes / 1MB, 2) errors = $errors cutoff = $cutoff.ToString("o") executedAt = (Get-Date -Format "o") } } function Invoke-VxnNetworkDiagnostics { Write-Lucas "Running network diagnostics..." "NETWORK" $adapters = @() try { $adapters = Get-WmiObject Win32_NetworkAdapterConfiguration | Where-Object { $_.IPEnabled -and $_.IPAddress } | ForEach-Object { @{ description = $_.Description ipAddress = ($_.IPAddress | Where-Object { $_ -match '^\d+\.\d+\.\d+\.\d+$' } | Select-Object -First 1) gateway = ($_.DefaultIPGateway | Select-Object -First 1) dnsServers = @($_.DNSServerSearchOrder) macAddress = $_.MACAddress } } } catch {} $tests = @() foreach ($target in @("vellunox.com", "8.8.8.8", "1.1.1.1")) { try { $tests += @{ target = $target ping = (Test-Connection -ComputerName $target -Count 2 -Quiet -ErrorAction SilentlyContinue) } } catch { $tests += @{ target = $target; ping = $false; error = $_.Exception.Message } } } $dns = $null try { $dns = [System.Net.Dns]::GetHostAddresses("vellunox.com") | ForEach-Object { $_.IPAddressToString } | Select-Object -First 5 } catch {} return @{ success = $true type = "network-diagnostics" ownerAgent = "lucas" adapters = @($adapters) tests = @($tests) vellunoxDns = @($dns) executedAt = (Get-Date -Format "o") } } function Invoke-VxnSoftwareAudit { Write-Lucas "Running software audit..." "INVENTORY" $software = @(Get-InstalledSoftware) $services = @(Get-WindowsServices | Where-Object { $_.status -eq "Running" }) return @{ success = $true type = "software-audit" ownerAgent = "lucas" installedCount = $software.Count runningServiceCount = $services.Count sample = @($software | Select-Object -First 120) executedAt = (Get-Date -Format "o") } } function Invoke-VxnBackupScan { Write-Lucas "Running backup readiness scan..." "BACKUP" $fileHistory = $null $wbadminAvailable = $false $systemProtection = $null try { $fileHistory = Get-Service -Name "fhsvc" -ErrorAction SilentlyContinue } catch {} try { $wbadminAvailable = [bool](Get-Command wbadmin.exe -ErrorAction SilentlyContinue) } catch {} try { $systemProtection = Get-ComputerRestorePoint -ErrorAction SilentlyContinue | Sort-Object CreationTime -Descending | Select-Object -First 1 } catch {} return @{ success = $true type = "backup-scan" ownerAgent = "lucas" fileHistoryService = if ($fileHistory) { $fileHistory.Status.ToString() } else { "not-found" } wbadminAvailable = $wbadminAvailable lastRestorePoint = if ($systemProtection) { $systemProtection.CreationTime } else { $null } oneDrive = Invoke-VxnOneDriveDetect executedAt = (Get-Date -Format "o") } } function Invoke-VxnOneDriveDetect { $roots = @( $env:OneDrive, $env:OneDriveCommercial, $env:OneDriveConsumer ) | Where-Object { $_ } | Select-Object -Unique $processes = @(Get-Process OneDrive -ErrorAction SilentlyContinue) return @{ success = $true type = "onedrive-detect" ownerAgent = "lucas" running = ($processes.Count -gt 0) processCount = $processes.Count roots = @($roots) rootCount = @($roots).Count executedAt = (Get-Date -Format "o") } } function Invoke-VxnRestoreReadiness { Write-Lucas "Checking restore readiness..." "BACKUP" $restorePoints = @() try { $restorePoints = @(Get-ComputerRestorePoint -ErrorAction SilentlyContinue | Sort-Object CreationTime -Descending | Select-Object -First 5) } catch {} $volumes = @() try { $volumes = @(Get-BitLockerVolume -ErrorAction SilentlyContinue | ForEach-Object { @{ mountPoint = $_.MountPoint protectionStatus = $_.ProtectionStatus.ToString() encryptionPercentage = $_.EncryptionPercentage recoveryProtectorCount = @($_.KeyProtector | Where-Object { $_.KeyProtectorType -eq "RecoveryPassword" }).Count } }) } catch {} return @{ success = $true type = "restore-readiness" ownerAgent = "lucas" restorePointCount = $restorePoints.Count latestRestorePoint = if ($restorePoints.Count -gt 0) { $restorePoints[0].CreationTime } else { $null } bitLockerVolumes = @($volumes) executedAt = (Get-Date -Format "o") } } function Invoke-VxnPcHealthCheck { Write-Lucas "Running Lucas PC health check package..." "HEALTH" $issues = @(Invoke-LucasHealthCheck) $sysInfo = Get-ComprehensiveSystemInfo $threats = @(Invoke-LucasThreatScan) return @{ success = $true type = "pc-health-check" ownerAgent = "lucas" issues = @($issues) issueCount = $issues.Count threats = @($threats) threatCount = $threats.Count cpu = $sysInfo.cpuUsage ram = $sysInfo.memoryPercent disk = $sysInfo.diskPercent uptimeDays = $sysInfo.uptimeDays os = $sysInfo.osName executedAt = (Get-Date -Format "o") } } function Invoke-VxnSafeAction { param($Action, $Command) switch ($Action) { "health-check" { return Invoke-VxnPcHealthCheck } "pc-health-check" { return Invoke-VxnPcHealthCheck } "defender-scan" { return Invoke-VxnDefenderQuickScan } "cleanup-temp" { return Invoke-VxnTempCleanup } "network-diagnostics" { return Invoke-VxnNetworkDiagnostics } "software-audit" { return Invoke-VxnSoftwareAudit } "backup-scan" { return Invoke-VxnBackupScan } "onedrive-detect" { return Invoke-VxnOneDriveDetect } "restore-readiness" { return Invoke-VxnRestoreReadiness } "inventory" { Send-FullInventory return @{ success = $true; type = "inventory"; ownerAgent = "lucas"; message = "Full inventory submitted"; executedAt = (Get-Date -Format "o") } } "agent-upgrade" { Invoke-SelfUpdate return @{ success = $true; type = "agent-upgrade"; ownerAgent = "lucas"; currentVersion = $script:AgentVersion; executedAt = (Get-Date -Format "o") } } } return $null } # ═══════════════════════════════════════════════════════════════════════════════════════ # CONFIGURATION MANAGEMENT # ═══════════════════════════════════════════════════════════════════════════════════════ function Get-AgentConfig { try { if (Test-Path $script:ConfigPath) { return Get-Content $script:ConfigPath -Raw | ConvertFrom-Json } } catch {} return @{ serverUrl = $script:ServerUrl; deviceId = $null; hostname = $env:COMPUTERNAME } } function Save-AgentConfig { param($Config) try { $Config | ConvertTo-Json -Depth 5 | Out-File $script:ConfigPath -Encoding UTF8 -Force } catch {} } # ═══════════════════════════════════════════════════════════════════════════════════════ # AUTO-UPDATE SYSTEM # ═══════════════════════════════════════════════════════════════════════════════════════ function Compare-Version { param([string]$v1, [string]$v2) $parts1 = $v1 -split '\.' | ForEach-Object { [int]$_ } $parts2 = $v2 -split '\.' | ForEach-Object { [int]$_ } for ($i = 0; $i -lt [Math]::Max($parts1.Count, $parts2.Count); $i++) { $p1 = if ($i -lt $parts1.Count) { $parts1[$i] } else { 0 } $p2 = if ($i -lt $parts2.Count) { $parts2[$i] } else { 0 } if ($p1 -gt $p2) { return 1 } if ($p1 -lt $p2) { return -1 } } return 0 } function Invoke-SelfUpdate { try { $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } Write-Lucas "Checking for updates..." "UPDATE" $wc = New-Object System.Net.WebClient $latestScript = $wc.DownloadString("$serverUrl/downloads/vxn-agent-service.ps1") if ($latestScript -match '\$.*AgentVersion\s*=\s*"([^"]+)"') { $serverVersion = $Matches[1] if ((Compare-Version $serverVersion $script:AgentVersion) -gt 0) { Write-Lucas "Upgrading v$($script:AgentVersion) -> v$serverVersion" "UPDATE" # Backup current script if (Test-Path $script:ScriptPath) { Copy-Item $script:ScriptPath "$script:ScriptPath.backup" -Force } # Save new script $latestScript | Out-File $script:ScriptPath -Encoding UTF8 -Force # Report upgrade try { $body = @{ deviceId = $config.deviceId hostname = $env:COMPUTERNAME oldVersion = $script:AgentVersion newVersion = $serverVersion upgradedAt = (Get-Date -Format "o") } | ConvertTo-Json -Compress $wc2 = New-Object System.Net.WebClient $wc2.Headers.Add("Content-Type", "application/json") $wc2.UploadString("$serverUrl/api/rmm/agent-upgrade", $body) | Out-Null } catch {} # Restart Write-Lucas "Restarting with new version..." "UPDATE" Stop-ScheduledTask -TaskName "VXN Agent Service" -ErrorAction SilentlyContinue Start-Sleep -Seconds 2 Start-ScheduledTask -TaskName "VXN Agent Service" -ErrorAction SilentlyContinue exit 0 } } Write-Lucas "Agent is current (v$($script:AgentVersion))" "UPDATE" } catch { Write-Lucas "Update check failed: $($_.Exception.Message)" "ERROR" } } # ═══════════════════════════════════════════════════════════════════════════════════════ # LUCAS AI - AUTONOMOUS IT TECHNICIAN # ═══════════════════════════════════════════════════════════════════════════════════════ function Invoke-LucasHealthCheck { Write-Lucas "Running health check..." "HEALTH" $issues = @() # Check disk space (< 10% free is critical) try { $disk = Get-WmiObject Win32_LogicalDisk | Where-Object { $_.DeviceID -eq 'C:' } $freePercent = [math]::Round(($disk.FreeSpace / $disk.Size) * 100, 1) if ($freePercent -lt 10) { $issues += @{ type = "disk_low"; severity = "critical"; value = $freePercent } Write-Lucas "CRITICAL: Disk space at $freePercent%" "ALERT" } elseif ($freePercent -lt 20) { $issues += @{ type = "disk_warning"; severity = "warning"; value = $freePercent } } } catch {} # Check memory usage (> 90% is warning) try { $mem = Get-WmiObject Win32_OperatingSystem $usedPercent = [math]::Round((1 - ($mem.FreePhysicalMemory / $mem.TotalVisibleMemorySize)) * 100, 1) if ($usedPercent -gt 95) { $issues += @{ type = "memory_critical"; severity = "critical"; value = $usedPercent } Write-Lucas "CRITICAL: Memory at $usedPercent%" "ALERT" } elseif ($usedPercent -gt 90) { $issues += @{ type = "memory_high"; severity = "warning"; value = $usedPercent } } } catch {} # Check CPU (sustained > 90% is issue) try { $cpu = Get-WmiObject Win32_Processor | Select-Object -First 1 if ($cpu.LoadPercentage -gt 95) { $issues += @{ type = "cpu_high"; severity = "warning"; value = $cpu.LoadPercentage } } } catch {} # Check critical services $criticalServices = @("wuauserv", "Winmgmt", "EventLog", "Schedule", "Dhcp", "Dnscache") foreach ($svc in $criticalServices) { try { $service = Get-Service -Name $svc -ErrorAction SilentlyContinue if ($service -and $service.Status -ne "Running") { $issues += @{ type = "service_stopped"; severity = "warning"; service = $svc } Write-Lucas "Critical service stopped: $svc - attempting restart" "REMEDIATE" Start-Service -Name $svc -ErrorAction SilentlyContinue $script:LucasMetrics.autoRemediations++ } } catch {} } # Check endpoint antivirus posture. Max owns AV/security; Lucas only reports device health. try { $thirdPartyAv = @(Get-CimInstance -Namespace root/SecurityCenter2 -ClassName AntiVirusProduct -ErrorAction SilentlyContinue | Where-Object { $_.displayName }) $defender = Get-MpComputerStatus -ErrorAction SilentlyContinue if ($defender) { if (-not $defender.AntivirusEnabled -and $thirdPartyAv.Count -eq 0) { $issues += @{ type = "antivirus_disabled"; severity = "critical" } Write-Max "CRITICAL: No active antivirus provider detected" "ALERT" } elseif (-not $defender.AntivirusEnabled -and $thirdPartyAv.Count -gt 0) { Write-Max "Third-party antivirus active: $($thirdPartyAv[0].displayName)" "INFO" } if ($defender.AntivirusEnabled -and $defender.AntivirusSignatureAge -gt 7) { $issues += @{ type = "antivirus_outdated"; severity = "warning"; days = $defender.AntivirusSignatureAge } Write-Max "Defender signatures $($defender.AntivirusSignatureAge) days old - updating" "REMEDIATE" Update-MpSignature -ErrorAction SilentlyContinue $script:LucasMetrics.autoRemediations++ } } elseif ($thirdPartyAv.Count -gt 0) { Write-Max "Third-party antivirus active: $($thirdPartyAv[0].displayName)" "INFO" } } catch {} $script:LucasMetrics.issuesFixed += ($issues | Where-Object { $_.severity -eq "warning" }).Count return $issues } function Invoke-LucasThreatScan { Write-Lucas "Scanning for threats..." "SECURITY" $threats = @() # Check for ransomware indicators $ransomwareExtensions = @("*.encrypted", "*.locked", "*.crypto", "*.crypt", "*.locky", "*.wcry", "*.wncry") $userFolders = @("$env:USERPROFILE\Desktop", "$env:USERPROFILE\Documents", "$env:USERPROFILE\Downloads") foreach ($folder in $userFolders) { foreach ($ext in $ransomwareExtensions) { $found = Get-ChildItem $folder -Filter $ext -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 if ($found) { $threats += @{ type = "ransomware_indicator"; path = $found.FullName; severity = "critical" } Write-Lucas "THREAT: Ransomware indicator found: $($found.Name)" "THREAT" } } } # Check for suspicious processes $suspiciousProcesses = @("mimikatz", "lazagne", "procdump", "pwdump", "wce", "gsecdump") $runningProcesses = Get-Process | Select-Object -ExpandProperty Name foreach ($proc in $suspiciousProcesses) { if ($runningProcesses -contains $proc) { $threats += @{ type = "malicious_process"; process = $proc; severity = "critical" } Write-Lucas "THREAT: Malicious process detected: $proc" "THREAT" Stop-Process -Name $proc -Force -ErrorAction SilentlyContinue $script:LucasMetrics.threatsBlocked++ } } # Check for unauthorized admin accounts try { $admins = Get-LocalGroupMember -Group "Administrators" -ErrorAction SilentlyContinue $knownAdmins = @("Administrator", $env:USERNAME) foreach ($admin in $admins) { $name = $admin.Name -split '\\' | Select-Object -Last 1 if ($name -notin $knownAdmins -and $name -notmatch "^(Domain Admins|Enterprise Admins)$") { $threats += @{ type = "unknown_admin"; account = $admin.Name; severity = "warning" } Write-Lucas "Unknown admin account: $($admin.Name)" "SECURITY" } } } catch {} # Check for open RDP to internet (dangerous) try { $rdp = Get-NetFirewallRule -DisplayName "*Remote Desktop*" -ErrorAction SilentlyContinue | Where-Object { $_.Enabled -eq $true -and $_.Direction -eq "Inbound" } if ($rdp) { $threats += @{ type = "rdp_exposed"; severity = "warning" } } } catch {} return $threats } function Invoke-LucasAutoRemediate { param($Issues) foreach ($issue in $Issues) { switch ($issue.type) { "disk_low" { Write-Lucas "Auto-cleaning disk space..." "REMEDIATE" # Clean temp files Remove-Item "$env:TEMP\*" -Recurse -Force -ErrorAction SilentlyContinue Remove-Item "C:\Windows\Temp\*" -Recurse -Force -ErrorAction SilentlyContinue # Clean Windows Update cache Stop-Service wuauserv -Force -ErrorAction SilentlyContinue Remove-Item "C:\Windows\SoftwareDistribution\Download\*" -Recurse -Force -ErrorAction SilentlyContinue Start-Service wuauserv -ErrorAction SilentlyContinue # Empty recycle bin Clear-RecycleBin -Force -ErrorAction SilentlyContinue $script:LucasMetrics.autoRemediations++ } "memory_critical" { Write-Lucas "Memory critical - clearing standby list..." "REMEDIATE" # Clear standby memory (requires admin) [System.GC]::Collect() $script:LucasMetrics.autoRemediations++ } } } } # ═══════════════════════════════════════════════════════════════════════════════════════ # WEBROOT SECURITYANYWHERE MANAGEMENT # Automatically installed on all managed devices # ═══════════════════════════════════════════════════════════════════════════════════════ function Get-WebrootStatus { $status = @{ installed = $false running = $false version = $null lastScan = $null realTimeProtection = $false } try { # Check if Webroot is installed if (Test-Path $script:WebrootInstallPath) { $status.installed = $true # Get version $version = (Get-Item $script:WebrootInstallPath).VersionInfo.FileVersion $status.version = $version # Check if service is running $process = Get-Process WRSA -ErrorAction SilentlyContinue $status.running = ($process -ne $null) # Check real-time protection via registry $wrReg = Get-ItemProperty "HKLM:\SOFTWARE\WOW6432Node\WRData" -ErrorAction SilentlyContinue if ($wrReg) { $status.realTimeProtection = $true } } } catch { Write-Lucas "Webroot status check error: $($_.Exception.Message)" "WARN" } return $status } function Install-Webroot { param([string]$Keycode) Write-Lucas "Installing Webroot SecureAnywhere..." "SECURITY" try { # Check if already installed $status = Get-WebrootStatus if ($status.installed -and $status.running) { Write-Lucas "Webroot already installed and running (v$($status.version))" "SECURITY" return @{ success = $true; alreadyInstalled = $true; version = $status.version } } # Need keycode if (-not $Keycode) { Write-Lucas "Webroot keycode not provided - skipping install" "WARN" return @{ success = $false; error = "No keycode provided" } } # Download installer $installerPath = "$env:TEMP\wsainstall.exe" Write-Lucas "Downloading Webroot installer..." "SECURITY" $wc = New-Object System.Net.WebClient $wc.DownloadFile($script:WebrootDownloadUrl, $installerPath) if (-not (Test-Path $installerPath)) { Write-Lucas "Webroot download failed" "ERROR" return @{ success = $false; error = "Download failed" } } # Install silently Write-Lucas "Running Webroot silent install..." "SECURITY" $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $installerPath $psi.Arguments = "/key=$Keycode /silent" $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden $psi.CreateNoWindow = $true $psi.UseShellExecute = $false $proc = [System.Diagnostics.Process]::Start($psi) $proc.WaitForExit(300000) # 5 min timeout # Cleanup installer Remove-Item $installerPath -Force -ErrorAction SilentlyContinue # Verify installation Start-Sleep -Seconds 10 # Wait for Webroot to start $newStatus = Get-WebrootStatus if ($newStatus.installed -and $newStatus.running) { Write-Lucas "Webroot installed successfully (v$($newStatus.version))" "SUCCESS" $script:MaxGuardianMetrics.webrootStatus = "active" return @{ success = $true; version = $newStatus.version } } else { Write-Lucas "Webroot installation may have failed - please verify manually" "WARN" return @{ success = $false; error = "Installation verification failed" } } } catch { Write-Lucas "Webroot install error: $($_.Exception.Message)" "ERROR" return @{ success = $false; error = $_.Exception.Message } } } function Update-Webroot { Write-Lucas "Triggering Webroot update..." "SECURITY" try { if (-not (Test-Path $script:WebrootInstallPath)) { return @{ success = $false; error = "Webroot not installed" } } # Trigger poll for updates $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $script:WebrootInstallPath $psi.Arguments = "-poll" $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden $psi.CreateNoWindow = $true $psi.UseShellExecute = $false $proc = [System.Diagnostics.Process]::Start($psi) $proc.WaitForExit(60000) # 1 min timeout Write-Lucas "Webroot update triggered" "SECURITY" return @{ success = $true; message = "Update poll triggered" } } catch { Write-Lucas "Webroot update error: $($_.Exception.Message)" "ERROR" return @{ success = $false; error = $_.Exception.Message } } } function Invoke-WebrootScan { Write-Lucas "Triggering Webroot scan..." "SECURITY" try { if (-not (Test-Path $script:WebrootInstallPath)) { return @{ success = $false; error = "Webroot not installed" } } $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $script:WebrootInstallPath $psi.Arguments = "-scan" $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden $psi.CreateNoWindow = $true $psi.UseShellExecute = $false $proc = [System.Diagnostics.Process]::Start($psi) # Don't wait - scan runs in background Write-Lucas "Webroot scan started" "SECURITY" $script:MaxGuardianMetrics.scansCompleted++ return @{ success = $true; message = "Scan started" } } catch { Write-Lucas "Webroot scan error: $($_.Exception.Message)" "ERROR" return @{ success = $false; error = $_.Exception.Message } } } function Get-WebrootKeycode { # Get keycode from server try { $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") $body = @{ hostname = $env:COMPUTERNAME deviceId = $config.deviceId clientId = $config.clientId } | ConvertTo-Json -Compress $response = $wc.UploadString("$serverUrl/api/security/webroot-keycode", $body) $data = $response | ConvertFrom-Json if ($data.success -and $data.keycode) { return $data.keycode } } catch { Write-Lucas "Failed to get Webroot keycode: $($_.Exception.Message)" "WARN" } # Fallback: Check if keycode is in config $config = Get-AgentConfig if ($config.webrootKeycode) { return $config.webrootKeycode } return $null } function Ensure-WebrootInstalled { # Ensure Webroot is installed on this managed device $status = Get-WebrootStatus if ($status.installed -and $status.running) { Write-Lucas "Webroot: Active (v$($status.version))" "SECURITY" $script:MaxGuardianMetrics.webrootStatus = "active" return $true } # Not installed - try to install Write-Lucas "Webroot not detected - attempting install..." "SECURITY" $keycode = Get-WebrootKeycode if ($keycode) { $result = Install-Webroot -Keycode $keycode return $result.success } else { Write-Lucas "Webroot keycode not available - will retry later" "WARN" $script:MaxGuardianMetrics.webrootStatus = "pending" return $false } } function Send-WebrootStatus { # Report Webroot status to server try { $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } $status = Get-WebrootStatus $body = @{ deviceId = $config.deviceId hostname = $env:COMPUTERNAME webroot = $status maxGuardian = $script:MaxGuardianMetrics timestamp = (Get-Date -Format "o") } | ConvertTo-Json -Depth 3 -Compress $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/security/webroot-status", $body) | Out-Null } catch { # Silent fail - not critical } } # ═══════════════════════════════════════════════════════════════════════════════════════ # NETWORK DISCOVERY - Find all devices on the network # ═══════════════════════════════════════════════════════════════════════════════════════ function Invoke-NetworkDiscovery { Write-Lucas "Scanning network for devices..." "NETWORK" $discoveredDevices = @() try { # Get local IP and subnet $adapter = Get-WmiObject Win32_NetworkAdapterConfiguration | Where-Object { $_.IPEnabled -eq $true -and $_.IPAddress } | Select-Object -First 1 if (-not $adapter) { return @() } $localIP = ($adapter.IPAddress | Where-Object { $_ -match '^\d+\.\d+\.\d+\.\d+$' } | Select-Object -First 1) $subnet = $localIP -replace '\.\d+$', '' Write-Lucas "Scanning subnet: $subnet.0/24" "NETWORK" # Quick ping sweep (parallel for speed) $jobs = @() 1..254 | ForEach-Object { $ip = "$subnet.$_" $jobs += Start-Job -ScriptBlock { param($ip) $ping = Test-Connection -ComputerName $ip -Count 1 -Quiet -ErrorAction SilentlyContinue if ($ping) { try { $hostname = [System.Net.Dns]::GetHostEntry($ip).HostName } catch { $hostname = $ip } return @{ ip = $ip; hostname = $hostname; online = $true } } } -ArgumentList $ip } # Wait for jobs with timeout $jobs | Wait-Job -Timeout 30 | Out-Null foreach ($job in $jobs) { $result = Receive-Job $job -ErrorAction SilentlyContinue if ($result -and $result.online) { $discoveredDevices += $result } Remove-Job $job -Force -ErrorAction SilentlyContinue } # Also check ARP cache for MAC addresses $arpCache = arp -a 2>$null foreach ($device in $discoveredDevices) { $arpEntry = $arpCache | Where-Object { $_ -match $device.ip } if ($arpEntry -match '([0-9a-f]{2}[:-]){5}[0-9a-f]{2}') { $device.mac = $Matches[0] } } $script:LucasMetrics.devicesDiscovered = $discoveredDevices.Count $script:LucasMetrics.networkScans++ Write-Lucas "Found $($discoveredDevices.Count) devices on network" "NETWORK" } catch { Write-Lucas "Network scan error: $($_.Exception.Message)" "ERROR" } return $discoveredDevices } function Send-NetworkDiscoveryReport { param($Devices) try { $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } # Include clientId so discovered devices auto-register to same client $clientId = if ($config.clientId) { $config.clientId } else { $null } $clientName = if ($config.clientName) { $config.clientName } else { $null } $body = @{ deviceId = $config.deviceId hostname = $env:COMPUTERNAME scannedAt = (Get-Date -Format "o") discoveredDevices = $Devices deviceCount = $Devices.Count # Client info for auto-registration of discovered devices clientId = $clientId clientName = $clientName } | ConvertTo-Json -Depth 4 -Compress $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") $response = $wc.UploadString("$serverUrl/api/rmm/network-discovery", $body) | ConvertFrom-Json if ($response.autoRegistered -gt 0) { Write-Lucas "$($response.autoRegistered) devices auto-added to $clientName" "NETWORK" } else { Write-Lucas "Network discovery report sent - $($Devices.Count) devices found" "NETWORK" } } catch { Write-Lucas "Failed to send network report: $($_.Exception.Message)" "ERROR" } } # ═══════════════════════════════════════════════════════════════════════════════════════ # SYSTEM INFORMATION COLLECTION # ═══════════════════════════════════════════════════════════════════════════════════════ function Get-ComprehensiveSystemInfo { $info = @{} # Logged-in User (Current User) try { $info.loggedInUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name $info.currentUser = $info.loggedInUser # Also try to get the console user (if different from process user) $explorerProc = Get-WmiObject Win32_Process -Filter "name='explorer.exe'" | Select-Object -First 1 if ($explorerProc) { $owner = $explorerProc.GetOwner() if ($owner.User) { $info.loggedInUser = if ($owner.Domain) { "$($owner.Domain)\$($owner.User)" } else { $owner.User } } } } catch { $info.loggedInUser = $env:USERNAME $info.currentUser = $env:USERNAME } # CPU - Use performance counter for accurate reading (LoadPercentage is often null/0) try { $proc = Get-WmiObject Win32_Processor | Select-Object -First 1 $info.processor = $proc.Name.Trim() $info.processorCores = $proc.NumberOfCores $info.processorThreads = $proc.NumberOfLogicalProcessors $info.processorSpeed = $proc.MaxClockSpeed # Get CPU usage from performance counter (more reliable than LoadPercentage) try { $cpuCounter = Get-Counter '\Processor(_Total)\% Processor Time' -SampleInterval 1 -MaxSamples 1 -ErrorAction SilentlyContinue if ($cpuCounter) { $info.cpuUsage = [math]::Round($cpuCounter.CounterSamples[0].CookedValue, 0) } else { $info.cpuUsage = $proc.LoadPercentage } } catch { # Fallback to LoadPercentage if counter fails $info.cpuUsage = $proc.LoadPercentage } # Ensure we always have a value (default to 5% if still null) if ($null -eq $info.cpuUsage -or $info.cpuUsage -eq 0) { # Quick check - if system is truly idle, report low value $info.cpuUsage = 5 } } catch {} # Memory try { $mem = Get-WmiObject Win32_OperatingSystem $info.memoryTotalGB = [math]::Round($mem.TotalVisibleMemorySize / 1MB, 1) $info.memoryFreeGB = [math]::Round($mem.FreePhysicalMemory / 1MB, 1) $info.memoryUsedGB = [math]::Round($info.memoryTotalGB - $info.memoryFreeGB, 1) $info.memoryPercent = [math]::Round(($info.memoryUsedGB / $info.memoryTotalGB) * 100) } catch {} # Disk try { $disk = Get-WmiObject Win32_LogicalDisk | Where-Object { $_.DeviceID -eq 'C:' } $info.diskTotalGB = [math]::Round($disk.Size / 1GB) $info.diskFreeGB = [math]::Round($disk.FreeSpace / 1GB) $info.diskUsedGB = $info.diskTotalGB - $info.diskFreeGB $info.diskPercent = [math]::Round(($info.diskUsedGB / $info.diskTotalGB) * 100) } catch {} # Network try { $adapter = Get-WmiObject Win32_NetworkAdapterConfiguration | Where-Object { $_.IPEnabled -and $_.IPAddress } | Select-Object -First 1 if ($adapter) { $info.ipAddress = ($adapter.IPAddress | Where-Object { $_ -match '^\d+\.\d+\.\d+\.\d+$' } | Select-Object -First 1) $info.macAddress = $adapter.MACAddress $info.gateway = ($adapter.DefaultIPGateway | Select-Object -First 1) $info.dnsServers = ($adapter.DNSServerSearchOrder -join ", ") $info.subnetMask = ($adapter.IPSubnet | Select-Object -First 1) } } catch {} # OS try { $os = Get-WmiObject Win32_OperatingSystem $info.osName = $os.Caption $info.osVersion = $os.Version $info.osBuild = $os.BuildNumber $info.osArch = $os.OSArchitecture $info.installDate = $os.InstallDate $bootTime = [Management.ManagementDateTimeConverter]::ToDateTime($os.LastBootUpTime) $uptime = (Get-Date) - $bootTime $info.uptimeDays = [math]::Floor($uptime.TotalDays) $info.uptimeHours = $uptime.Hours } catch {} # Hardware try { $cs = Get-WmiObject Win32_ComputerSystem $info.manufacturer = $cs.Manufacturer $info.model = $cs.Model $info.domain = $cs.Domain $info.totalPhysicalMemory = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1) } catch {} # BIOS try { $bios = Get-WmiObject Win32_BIOS $info.serialNumber = $bios.SerialNumber $info.biosVersion = $bios.SMBIOSBIOSVersion } catch {} # GPU try { $gpu = Get-WmiObject Win32_VideoController | Select-Object -First 1 $info.graphicsCard = $gpu.Name $info.graphicsMemory = [math]::Round($gpu.AdapterRAM / 1GB, 1) } catch {} # TPM (for Windows 11 compatibility) try { $tpm = Get-WmiObject -Namespace "Root\CIMv2\Security\MicrosoftTpm" -Class Win32_Tpm -ErrorAction SilentlyContinue $info.tpmPresent = if ($tpm) { $true } else { $false } $info.tpmVersion = if ($tpm) { $tpm.SpecVersion } else { $null } } catch { $info.tpmPresent = $false } # Secure Boot try { $info.secureBootEnabled = Confirm-SecureBootUEFI -ErrorAction SilentlyContinue } catch { $info.secureBootEnabled = $null } # Antivirus try { $av = Get-WmiObject -Namespace "root\SecurityCenter2" -Class AntiVirusProduct -ErrorAction SilentlyContinue | Select-Object -First 1 $info.antivirusName = if ($av) { $av.displayName } else { "Windows Defender" } } catch {} return $info } # ═══════════════════════════════════════════════════════════════════════════════════════ # BITLOCKER RECOVERY KEYS # ═══════════════════════════════════════════════════════════════════════════════════════ function Get-BitLockerRecoveryKeys { $volumes = @() try { $blVolumes = Get-BitLockerVolume -ErrorAction SilentlyContinue if ($blVolumes) { foreach ($vol in $blVolumes) { $volumeInfo = @{ mountPoint = $vol.MountPoint volumeType = $vol.VolumeType.ToString() protectionStatus = $vol.ProtectionStatus.ToString() encryptionMethod = $vol.EncryptionMethod.ToString() encryptionPercentage = $vol.EncryptionPercentage lockStatus = $vol.LockStatus.ToString() recoveryKeys = @() } foreach ($protector in $vol.KeyProtector) { if ($protector.KeyProtectorType -eq 'RecoveryPassword') { $volumeInfo.recoveryKeys += @{ id = $protector.KeyProtectorId type = 'RecoveryPassword' key = $protector.RecoveryPassword } } } $volumes += $volumeInfo } } } catch { Write-Lucas "BitLocker error: $($_.Exception.Message)" "ERROR" } return $volumes } # ═══════════════════════════════════════════════════════════════════════════════════════ # INVENTORY COLLECTION # ═══════════════════════════════════════════════════════════════════════════════════════ function Get-InstalledSoftware { $software = @() try { $regPaths = @( "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" ) foreach ($path in $regPaths) { Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName } | ForEach-Object { $software += @{ name = $_.DisplayName version = $_.DisplayVersion publisher = $_.Publisher installDate = $_.InstallDate } } } } catch {} return $software | Sort-Object { $_.name } -Unique } function Get-WindowsServices { $services = @() try { Get-WmiObject Win32_Service | ForEach-Object { $services += @{ name = $_.Name displayName = $_.DisplayName status = $_.State startMode = $_.StartMode path = $_.PathName } } } catch {} return $services } function Get-LocalUserAccounts { $users = @() try { Get-WmiObject Win32_UserAccount -Filter "LocalAccount=True" | ForEach-Object { $users += @{ name = $_.Name fullName = $_.FullName description = $_.Description disabled = $_.Disabled sid = $_.SID } } } catch {} return $users } function Get-FirewallStatus { $result = @{ enabled = $false; profiles = @(); rules = @() } try { $fw = Get-NetFirewallProfile -ErrorAction SilentlyContinue if ($fw) { $result.enabled = ($fw | Where-Object { $_.Enabled -eq $true }).Count -gt 0 $result.profiles = $fw | ForEach-Object { @{ name = $_.Name; enabled = $_.Enabled } } } $rules = Get-NetFirewallRule -Enabled True -ErrorAction SilentlyContinue | Select-Object -First 50 foreach ($rule in $rules) { $result.rules += @{ name = $rule.DisplayName direction = $rule.Direction.ToString() action = $rule.Action.ToString() } } } catch {} return $result } function Get-WindowsProductKey { try { $key = (Get-WmiObject -Query "SELECT * FROM SoftwareLicensingService").OA3xOriginalProductKey if ($key) { return $key } } catch {} return $null } function Get-PendingUpdates { $updates = @() try { $session = New-Object -ComObject Microsoft.Update.Session $searcher = $session.CreateUpdateSearcher() $result = $searcher.Search("IsInstalled=0") foreach ($update in $result.Updates | Select-Object -First 20) { $updates += $update.Title } } catch {} return $updates } # ═══════════════════════════════════════════════════════════════════════════════════════ # REPORTING TO SERVER # ═══════════════════════════════════════════════════════════════════════════════════════ function Send-Heartbeat { try { $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } $sysInfo = Get-ComprehensiveSystemInfo # Get Vellunox Remote (Splashtop) status $SplashtopStatus = Get-SplashtopStatus $body = @{ deviceId = $config.deviceId hostname = $env:COMPUTERNAME agentVersion = $script:AgentVersion online = $true source = "vxn-enterprise" lastHeartbeat = (Get-Date -Format "o") cpu = [int]$sysInfo.cpuUsage ram = [int]$sysInfo.memoryPercent disk = [int]$sysInfo.diskPercent loggedInUser = $sysInfo.loggedInUser currentUser = $sysInfo.currentUser systemInfo = $sysInfo lucasMetrics = $script:LucasMetrics maxGuardianMetrics = $script:MaxGuardianMetrics # Vellunox Remote (Splashtop) status Splashtop = @{ installed = $SplashtopStatus.installed SplashtopId = $SplashtopStatus.SplashtopId serviceRunning = $SplashtopStatus.serviceRunning ready = $SplashtopStatus.ready } remoteAccess = @{ method = "Splashtop" ready = $SplashtopStatus.ready SplashtopId = $SplashtopStatus.SplashtopId agentVersion = $script:AgentVersion unattendedAccess = $script:UnattendedAccessEnabled unattendedReady = $SplashtopStatus.installed -and $SplashtopStatus.serviceRunning } security = @{ webroot = (Get-WebrootStatus) defender = @{ enabled = $sysInfo.antivirusName -match "Defender" } } } | ConvertTo-Json -Depth 5 -Compress # Use dedicated RMM microservice for performance (with fallback) $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") # Try RMM microservice first, fallback to main server $heartbeatEndpoint = $script:HeartbeatUrl $result = $null try { $result = $wc.UploadString($heartbeatEndpoint, $body) | ConvertFrom-Json } catch { # Fallback to main server if RMM microservice unavailable Write-Lucas "RMM service unavailable, using fallback" "WARN" $heartbeatEndpoint = $script:HeartbeatFallbackUrl $wc2 = New-Object System.Net.WebClient $wc2.Headers.Add("Content-Type", "application/json") $result = $wc2.UploadString($heartbeatEndpoint, $body) | ConvertFrom-Json } # Process commands from Lucas if ($result.commands -and $result.commands.Count -gt 0) { Write-Lucas "Received $($result.commands.Count) command(s)" "COMMAND" foreach ($cmd in $result.commands) { Invoke-LucasCommand $cmd } } return $true } catch { Write-Lucas "Heartbeat failed: $($_.Exception.Message)" "ERROR" return $false } } function Invoke-LucasCommand { param($Command) try { # Handle both 'type' and 'commandType' from WebSocket messages $cmdType = if ($Command.type) { $Command.type } elseif ($Command.commandType) { $Command.commandType } else { "script" } $cmdName = if ($Command.name) { $Command.name } elseif ($Command.action) { $Command.action } else { $cmdType } Write-Lucas "Executing: $cmdName (type: $cmdType)" "COMMAND" $safeAction = Invoke-VxnSafeAction -Action $cmdType -Command $Command if (-not $safeAction -and $cmdName -and $cmdName -ne $cmdType) { $safeAction = Invoke-VxnSafeAction -Action $cmdName -Command $Command } if ($safeAction) { Send-VxnCommandResult -Command $Command -Payload $safeAction return } # Handle built-in commands switch ($cmdType) { "enable-widget" { Enable-LucasWidget return } "disable-widget" { Disable-LucasWidget return } "restart-agent" { Write-Lucas "Agent restart requested" "SYSTEM" schtasks /End /TN "VXN Agent Service" 2>$null Start-Sleep -Seconds 2 schtasks /Run /TN "VXN Agent Service" 2>$null return } "update-agent" { Write-Lucas "Agent update requested" "UPDATE" try { $wc = New-Object System.Net.WebClient $newScript = $wc.DownloadString("https://vellunox.com/downloads/vxn-agent-service.ps1") $newScript | Out-File -FilePath $script:ScriptPath -Encoding UTF8 -Force schtasks /End /TN "VXN Agent Service" 2>$null Start-Sleep -Seconds 2 schtasks /Run /TN "VXN Agent Service" 2>$null } catch { Write-Lucas "Update failed: $($_.Exception.Message)" "ERROR" } return } "enable-unattended" { $pin = if ($Command.pin) { $Command.pin } else { $null } Enable-UnattendedAccess -Pin $pin return } "disable-unattended" { Disable-UnattendedAccess return } "start-remote-session" { Start-UnattendedSession -SessionId $Command.sessionId -TechnicianId $Command.technicianId return } "end-remote-session" { End-UnattendedSession -SessionId $Command.sessionId return } "webroot-scan" { Invoke-WebrootScan return } "webroot-update" { Update-Webroot return } "security-allowlist" { $allowlistResult = Set-MaxSecurityAllowlist -Command $Command Send-VxnCommandResult -Command $Command -Payload $allowlistResult return } "max-security-allowlist" { $allowlistResult = Set-MaxSecurityAllowlist -Command $Command Send-VxnCommandResult -Command $Command -Payload $allowlistResult return } "get-security-status" { $webrootStatus = Get-WebrootStatus $unattendedStatus = Get-UnattendedAccessStatus # Report back to server $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } $body = @{ deviceId = $config.deviceId hostname = $env:COMPUTERNAME webroot = $webrootStatus unattended = $unattendedStatus maxGuardian = $script:MaxGuardianMetrics commandId = $Command.id } | ConvertTo-Json -Depth 3 -Compress try { $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/rmm/command-result", $body) | Out-Null } catch {} return } "enable-backend-access" { Enable-BackendAccess return } "start-backend-session" { Start-BackgroundSession -SessionId $Command.sessionId -TechnicianId $Command.technicianId -AccessType $Command.accessType return } "get-backend-status" { $status = Get-BackendAccessStatus $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } $body = @{ deviceId = $config.deviceId hostname = $env:COMPUTERNAME backendAccess = $status commandId = $Command.id } | ConvertTo-Json -Depth 4 -Compress try { $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/rmm/command-result", $body) | Out-Null } catch {} return } "install-Splashtop" { # REAL-TIME Splashtop install - execute script immediately Write-Lucas "REAL-TIME: Installing Splashtop..." "WS" if ($Command.script) { $tempScript = "$env:TEMP\vxn-Splashtop-$([guid]::NewGuid().ToString('N').Substring(0,8)).ps1" $Command.script | Out-File -FilePath $tempScript -Encoding UTF8 -Force $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = "powershell.exe" $psi.Arguments = "-ExecutionPolicy Bypass -WindowStyle Hidden -NonInteractive -File `"$tempScript`"" $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden $psi.CreateNoWindow = $true $psi.UseShellExecute = $false $psi.RedirectStandardOutput = $true $process = [System.Diagnostics.Process]::Start($psi) $process.WaitForExit(600000) # 10 min timeout for Splashtop install Remove-Item $tempScript -Force -ErrorAction SilentlyContinue Write-Lucas "Splashtop install script completed" "WS" } return } "agent-update" { # REAL-TIME Agent update - execute script immediately Write-Lucas "REAL-TIME: Updating agent..." "WS" if ($Command.script) { $tempScript = "$env:TEMP\vxn-update-$([guid]::NewGuid().ToString('N').Substring(0,8)).ps1" $Command.script | Out-File -FilePath $tempScript -Encoding UTF8 -Force $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = "powershell.exe" $psi.Arguments = "-ExecutionPolicy Bypass -WindowStyle Hidden -NonInteractive -File `"$tempScript`"" $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden $psi.CreateNoWindow = $true $psi.UseShellExecute = $false $psi.RedirectStandardOutput = $true $process = [System.Diagnostics.Process]::Start($psi) $process.WaitForExit(600000) # 10 min timeout for update Remove-Item $tempScript -Force -ErrorAction SilentlyContinue Write-Lucas "Agent update script completed" "WS" } return } "backend-command" { $result = Invoke-BackendCommand -Command $Command.command -SessionType $Command.sessionType $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } $body = @{ deviceId = $config.deviceId hostname = $env:COMPUTERNAME result = $result commandId = $Command.id } | ConvertTo-Json -Depth 3 -Compress try { $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/rmm/command-result", $body) | Out-Null } catch {} return } } # Handle script or command property (server uses both formats) $scriptContent = if ($Command.script) { $Command.script } elseif ($Command.command) { $Command.command } else { $null } if ($scriptContent) { # Save script to temp file and run hidden $tempScript = "$env:TEMP\vxn-cmd-$([guid]::NewGuid().ToString('N').Substring(0,8)).ps1" $scriptContent | Out-File -FilePath $tempScript -Encoding UTF8 -Force # Execute completely hidden with no window $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = "powershell.exe" $psi.Arguments = "-ExecutionPolicy Bypass -WindowStyle Hidden -NonInteractive -File `"$tempScript`"" $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden $psi.CreateNoWindow = $true $psi.UseShellExecute = $false $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true $process = [System.Diagnostics.Process]::Start($psi) $output = $process.StandardOutput.ReadToEnd() $process.WaitForExit(300000) # 5 min timeout # Cleanup temp script Remove-Item $tempScript -Force -ErrorAction SilentlyContinue # Report result $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } $body = @{ deviceId = $config.deviceId hostname = $env:COMPUTERNAME commandId = $Command.id commandName = $Command.name success = $true output = $output.Substring(0, [Math]::Min(4000, $output.Length)) executedAt = (Get-Date -Format "o") } | ConvertTo-Json -Compress $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/rmm/command-result", $body) | Out-Null } } catch { Write-Lucas "Command failed: $($_.Exception.Message)" "ERROR" } } function Send-FullInventory { Write-Lucas "Sending full system inventory..." "INVENTORY" $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } try { # BitLocker $bitlocker = Get-BitLockerRecoveryKeys if ($bitlocker.Count -gt 0) { $body = @{ deviceId = $config.deviceId; hostname = $env:COMPUTERNAME; volumes = $bitlocker } | ConvertTo-Json -Depth 4 -Compress $wc = New-Object System.Net.WebClient; $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/rmm/bitlocker/report", $body) | Out-Null Write-Lucas "BitLocker keys reported" "INVENTORY" } # Services $services = Get-WindowsServices $body = @{ services = $services } | ConvertTo-Json -Depth 3 -Compress $wc = New-Object System.Net.WebClient; $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/rmm/devices/$($config.deviceId)/services/report", $body) | Out-Null # Software $software = Get-InstalledSoftware $body = @{ software = $software } | ConvertTo-Json -Depth 3 -Compress $wc = New-Object System.Net.WebClient; $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/rmm/devices/$($config.deviceId)/software/report", $body) | Out-Null # Users $users = Get-LocalUserAccounts $body = @{ users = $users } | ConvertTo-Json -Depth 3 -Compress $wc = New-Object System.Net.WebClient; $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/rmm/devices/$($config.deviceId)/users/report", $body) | Out-Null # Firewall $firewall = Get-FirewallStatus $body = $firewall | ConvertTo-Json -Depth 3 -Compress $wc = New-Object System.Net.WebClient; $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/rmm/devices/$($config.deviceId)/firewall/report", $body) | Out-Null # Full inventory $inventory = @{ deviceId = $config.deviceId hostname = $env:COMPUTERNAME windowsKey = Get-WindowsProductKey pendingUpdates = (Get-PendingUpdates).Count pendingUpdatesList = Get-PendingUpdates tpmPresent = (Get-ComprehensiveSystemInfo).tpmPresent secureBootEnabled = (Get-ComprehensiveSystemInfo).secureBootEnabled } $body = $inventory | ConvertTo-Json -Depth 4 -Compress $wc = New-Object System.Net.WebClient; $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/rmm/inventory/report", $body) | Out-Null Write-Lucas "Full inventory sent successfully" "INVENTORY" } catch { Write-Lucas "Inventory report error: $($_.Exception.Message)" "ERROR" } } function Register-Device { try { $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } $sysInfo = Get-ComprehensiveSystemInfo # Include clientId if set in config (from client-specific installer) $clientId = if ($config.clientId) { $config.clientId } else { $null } $clientName = if ($config.clientName) { $config.clientName } else { $null } $body = @{ hostname = $env:COMPUTERNAME os = $sysInfo.osName ip = $sysInfo.ipAddress mac = $sysInfo.macAddress agentVersion = $script:AgentVersion source = "vxn-enterprise" manufacturer = $sysInfo.manufacturer model = $sysInfo.model serialNumber = $sysInfo.serialNumber # Client binding - auto-registers to client's account clientId = $clientId clientName = $clientName customer = $clientName tenantId = $clientId } | ConvertTo-Json -Compress $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/vxn/register", $body) | Out-Null if ($clientName) { Write-Lucas "Device registered to client: $clientName" "REGISTER" } else { Write-Lucas "Device registered with Vellunox" "REGISTER" } return $true } catch { Write-Lucas "Registration failed: $($_.Exception.Message)" "ERROR" return $false } } # ═══════════════════════════════════════════════════════════════════════════════════════ # MAIN AGENT LOOP # ═══════════════════════════════════════════════════════════════════════════════════════ Write-Log "═══════════════════════════════════════════════════════════════" "START" Write-Log "$script:AgentName v$script:AgentVersion starting on $env:COMPUTERNAME" "START" Write-Log "Lucas AI: ACTIVE | Network Discovery: ENABLED | Threat Detection: ENABLED" "START" Write-Log "═══════════════════════════════════════════════════════════════" "START" # Check for updates on startup Invoke-SelfUpdate # ═══════════════════════════════════════════════════════════════════════════════════════ # VELLUNOX REMOTE (Splashtop) - PRIMARY REMOTE ACCESS METHOD # Branded Splashtop for unattended remote access - NOT WebRTC # ═══════════════════════════════════════════════════════════════════════════════════════ $script:SplashtopInstallUrl = "https://github.com/Splashtop/Splashtop/releases/download/1.3.7/Splashtop-1.3.7-x86_64.exe" $script:VellunoxRemoteServer = "vellunox.com" # Your relay server if self-hosted function Get-SplashtopStatus { # Check if Splashtop (Vellunox Remote) is installed and get its ID $installPath = "$env:ProgramFiles\Splashtop\Splashtop.exe" $installed = Test-Path $installPath $SplashtopId = $null $password = $null if ($installed) { # Get Splashtop ID from config $configPaths = @( "$env:ProgramData\Splashtop\config\Splashtop2.toml", "$env:APPDATA\Splashtop\config\Splashtop2.toml", "$env:USERPROFILE\.Splashtop\config\Splashtop2.toml" ) foreach ($cfgPath in $configPaths) { if (Test-Path $cfgPath) { $content = Get-Content $cfgPath -Raw -ErrorAction SilentlyContinue if ($content -match 'id\s*=\s*[''"]?(\d{9})[''"]?') { $SplashtopId = $Matches[1] # Format as XXX XXX XXX $SplashtopId = $SplashtopId.Insert(3, ' ').Insert(7, ' ') } if ($content -match 'password\s*=\s*[''"]?([^''"]+)[''"]?') { $password = $Matches[1] } break } } # Check if service is running $svc = Get-Service "Splashtop" -ErrorAction SilentlyContinue $serviceRunning = $svc -and $svc.Status -eq 'Running' } return @{ installed = $installed SplashtopId = $SplashtopId serviceRunning = $serviceRunning method = "Splashtop" agentVersion = $script:AgentVersion hostname = $env:COMPUTERNAME platform = "windows" unattendedAccess = $installed -and $serviceRunning ready = $installed -and $SplashtopId } } function Install-VellunoxRemote { # Install Splashtop as SERVICE ONLY (no desktop icon, no tray icon, completely hidden) Write-Lucas "Installing Vellunox Remote (Splashtop) as background service..." "REMOTE" $installPath = "$env:ProgramFiles\Splashtop\Splashtop.exe" # Check if already installed if (Test-Path $installPath) { Write-Lucas "Vellunox Remote already installed" "REMOTE" # Ensure it's configured as service-only (no UI) Configure-SplashtopServiceOnly return Get-SplashtopStatus } try { # Download Splashtop installer $installerPath = "$env:TEMP\Splashtop-installer.exe" Write-Lucas "Downloading Vellunox Remote..." "REMOTE" $wc = New-Object System.Net.WebClient $wc.DownloadFile($script:SplashtopInstallUrl, $installerPath) if (-not (Test-Path $installerPath)) { throw "Download failed" } # Silent install as SERVICE ONLY (no desktop shortcuts, no startup) Write-Lucas "Installing as background service (no UI)..." "REMOTE" $proc = Start-Process -FilePath $installerPath -ArgumentList "--silent-install" -Wait -PassThru -WindowStyle Hidden Start-Sleep -Seconds 5 # Configure for SERVICE-ONLY operation (no icon, no tray, no UI) if (Test-Path $installPath) { Write-Lucas "Configuring service-only mode (hidden)..." "REMOTE" # Generate permanent password for unattended access (8 characters) $permPassword = -join ((65..90) + (97..122) + (48..57) | Get-Random -Count 8 | % {[char]$_}) # Set permanent password via command line & $installPath --password $permPassword 2>$null Start-Sleep -Seconds 2 # Configure as service-only (no UI components) Configure-SplashtopServiceOnly Write-Lucas "Vellunox Remote installed as hidden service!" "SUCCESS" } # Cleanup installer Remove-Item $installerPath -Force -ErrorAction SilentlyContinue return Get-SplashtopStatus } catch { Write-Lucas "Vellunox Remote install error: $($_.Exception.Message)" "ERROR" return @{ installed = $false; error = $_.Exception.Message } } } function Configure-SplashtopServiceOnly { # Configure Splashtop to run as SERVICE ONLY - no desktop icon, no tray, no user UI Write-Lucas "Configuring Splashtop for service-only operation..." "REMOTE" try { # Kill any running Splashtop UI processes (keep service running) Get-Process Splashtop -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Stop-Process -Force -ErrorAction SilentlyContinue # Remove desktop shortcuts $desktopPaths = @( "$env:PUBLIC\Desktop\Splashtop.lnk", "$env:USERPROFILE\Desktop\Splashtop.lnk", [Environment]::GetFolderPath('CommonDesktopDirectory') + "\Splashtop.lnk" ) foreach ($shortcut in $desktopPaths) { if (Test-Path $shortcut) { Remove-Item $shortcut -Force -ErrorAction SilentlyContinue Write-Lucas "Removed desktop shortcut: $shortcut" "REMOTE" } } # Remove Start Menu shortcuts $startMenuPaths = @( "$env:ProgramData\Microsoft\Windows\Start Menu\Programs\Splashtop.lnk", "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Splashtop.lnk", [Environment]::GetFolderPath('CommonPrograms') + "\Splashtop.lnk" ) foreach ($shortcut in $startMenuPaths) { if (Test-Path $shortcut) { Remove-Item $shortcut -Force -ErrorAction SilentlyContinue } } # Disable auto-start for user (UI) - only service should run $runKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" Remove-ItemProperty -Path $runKey -Name "Splashtop" -ErrorAction SilentlyContinue Remove-ItemProperty -Path $runKey -Name "Splashtop" -ErrorAction SilentlyContinue # Also check HKLM Run key $runKeyLM = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Run" Remove-ItemProperty -Path $runKeyLM -Name "Splashtop" -ErrorAction SilentlyContinue Remove-ItemProperty -Path $runKeyLM -Name "Splashtop" -ErrorAction SilentlyContinue # Ensure the Splashtop SERVICE is running and set to auto-start $svc = Get-Service "Splashtop" -ErrorAction SilentlyContinue if ($svc) { Set-Service -Name "Splashtop" -StartupType Automatic -ErrorAction SilentlyContinue if ($svc.Status -ne 'Running') { Start-Service "Splashtop" -ErrorAction SilentlyContinue } Write-Lucas "Splashtop service configured for auto-start" "REMOTE" } # Configure Splashtop to not show tray icon (via config file) $configPaths = @( "$env:ProgramData\Splashtop\config", "$env:APPDATA\Splashtop\config" ) foreach ($configDir in $configPaths) { if (-not (Test-Path $configDir)) { New-Item -ItemType Directory -Path $configDir -Force -ErrorAction SilentlyContinue | Out-Null } $configFile = Join-Path $configDir "Splashtop2.toml" # Read existing config or create new $configContent = "" if (Test-Path $configFile) { $configContent = Get-Content $configFile -Raw -ErrorAction SilentlyContinue } # Add/update settings to hide UI if ($configContent -notmatch 'allow-auto-disconnect') { $configContent += "`nallow-auto-disconnect = true" } if ($configContent -notmatch 'enable-audio') { $configContent += "`nenable-audio = false" } # Write config Set-Content -Path $configFile -Value $configContent -Force -ErrorAction SilentlyContinue } Write-Lucas "Splashtop configured as hidden service (no UI, no tray, no desktop icon)" "SUCCESS" } catch { Write-Lucas "Splashtop service config error: $($_.Exception.Message)" "WARN" } } function Ensure-VellunoxRemote { # Ensure Vellunox Remote is installed as SERVICE ONLY and running $status = Get-SplashtopStatus if (-not $status.installed) { Write-Lucas "Vellunox Remote not installed - installing as hidden service..." "REMOTE" $status = Install-VellunoxRemote } else { # Already installed - ensure service-only mode (no UI/icons) Configure-SplashtopServiceOnly if (-not $status.serviceRunning) { Write-Lucas "Vellunox Remote service not running - starting..." "REMOTE" Start-Service "Splashtop" -ErrorAction SilentlyContinue Start-Sleep -Seconds 2 $status = Get-SplashtopStatus } } # Report status to server if ($status.SplashtopId) { try { $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } $body = @{ hostname = $env:COMPUTERNAME SplashtopId = $status.SplashtopId installed = $status.installed serviceRunning = $status.serviceRunning serviceMode = "hidden" # No UI, no tray, no desktop icon agentVersion = $script:AgentVersion } | ConvertTo-Json -Compress $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/Splashtop/register", $body) | Out-Null Write-Lucas "Vellunox Remote registered (service mode): $($status.SplashtopId)" "REMOTE" } catch { Write-Lucas "Could not register Vellunox Remote: $($_.Exception.Message)" "WARN" } } return $status } # ═══════════════════════════════════════════════════════════════════════════════════════ # UNATTENDED REMOTE ACCESS # Allows technicians to connect without user approval # ═══════════════════════════════════════════════════════════════════════════════════════ $script:UnattendedAccessEnabled = $true $script:UnattendedAccessPin = $null # Set by server or config function Enable-UnattendedAccess { param([string]$Pin) Write-Lucas "Enabling unattended remote access..." "REMOTE" try { # Store encrypted PIN $script:UnattendedAccessPin = $Pin $script:UnattendedAccessEnabled = $true # Save to config $config = Get-AgentConfig $config.unattendedAccess = @{ enabled = $true configuredAt = (Get-Date -Format "o") } Save-AgentConfig $config # Enable Windows Remote Desktop if not already Enable-RemoteDesktop # Configure firewall for VXN remote access Enable-VXNRemoteFirewall Write-Lucas "Unattended access enabled" "SUCCESS" return @{ success = $true; message = "Unattended access enabled" } } catch { Write-Lucas "Failed to enable unattended access: $($_.Exception.Message)" "ERROR" return @{ success = $false; error = $_.Exception.Message } } } function Disable-UnattendedAccess { Write-Lucas "Disabling unattended remote access..." "REMOTE" $script:UnattendedAccessEnabled = $false $script:UnattendedAccessPin = $null $config = Get-AgentConfig $config.unattendedAccess = @{ enabled = $false disabledAt = (Get-Date -Format "o") } Save-AgentConfig $config return @{ success = $true; message = "Unattended access disabled" } } function Enable-RemoteDesktop { # Enable RDP for fallback remote access try { # Enable Remote Desktop Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0 -ErrorAction SilentlyContinue # Enable Network Level Authentication Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "UserAuthentication" -Value 1 -ErrorAction SilentlyContinue # Enable firewall rule for RDP Enable-NetFirewallRule -DisplayGroup "Remote Desktop" -ErrorAction SilentlyContinue Write-Lucas "Remote Desktop enabled" "REMOTE" } catch { Write-Lucas "RDP enable failed: $($_.Exception.Message)" "WARN" } } function Enable-VXNRemoteFirewall { # Configure Windows Firewall for VXN remote access try { # Allow VXN Agent through firewall $ruleName = "VXN Remote Access" $existingRule = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue if (-not $existingRule) { New-NetFirewallRule -DisplayName $ruleName ` -Direction Inbound ` -Protocol TCP ` -LocalPort 5900-5999,8080,443 ` -Action Allow ` -Profile Any ` -Description "VXN Agent Remote Access" ` -ErrorAction SilentlyContinue } # Allow WebRTC/STUN/TURN $rtcRule = "VXN WebRTC" $existingRtc = Get-NetFirewallRule -DisplayName $rtcRule -ErrorAction SilentlyContinue if (-not $existingRtc) { New-NetFirewallRule -DisplayName $rtcRule ` -Direction Inbound ` -Protocol UDP ` -LocalPort 3478,5349,49152-65535 ` -Action Allow ` -Profile Any ` -Description "VXN WebRTC STUN/TURN" ` -ErrorAction SilentlyContinue } Write-Lucas "VXN firewall rules configured" "REMOTE" } catch { Write-Lucas "Firewall config failed: $($_.Exception.Message)" "WARN" } } function Get-UnattendedAccessStatus { $config = Get-AgentConfig return @{ enabled = $script:UnattendedAccessEnabled configured = ($config.unattendedAccess -ne $null) rdpEnabled = (Get-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -ErrorAction SilentlyContinue).fDenyTSConnections -eq 0 webrtcReady = $true hostname = $env:COMPUTERNAME lastChecked = (Get-Date -Format "o") } } function Start-UnattendedSession { param([string]$SessionId, [string]$TechnicianId) Write-Lucas "Starting unattended session: $SessionId for technician $TechnicianId" "REMOTE" try { $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } # Report session start to server $body = @{ deviceId = $config.deviceId hostname = $env:COMPUTERNAME sessionId = $SessionId technicianId = $TechnicianId sessionType = "unattended" startedAt = (Get-Date -Format "o") status = "active" } | ConvertTo-Json -Compress $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/remote/session-start", $body) | Out-Null # Log for audit $logEntry = @{ timestamp = (Get-Date -Format "o") sessionId = $SessionId technicianId = $TechnicianId type = "unattended" action = "session_start" } $sessionLogPath = "$script:DataPath\sessions" if (-not (Test-Path $sessionLogPath)) { New-Item -ItemType Directory -Path $sessionLogPath -Force | Out-Null } $logEntry | ConvertTo-Json | Out-File "$sessionLogPath\$SessionId.json" -Encoding UTF8 return @{ success = $true; sessionId = $SessionId; message = "Unattended session started" } } catch { Write-Lucas "Session start failed: $($_.Exception.Message)" "ERROR" return @{ success = $false; error = $_.Exception.Message } } } function End-UnattendedSession { param([string]$SessionId) Write-Lucas "Ending unattended session: $SessionId" "REMOTE" try { $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } $body = @{ deviceId = $config.deviceId hostname = $env:COMPUTERNAME sessionId = $SessionId endedAt = (Get-Date -Format "o") status = "ended" } | ConvertTo-Json -Compress $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/remote/session-end", $body) | Out-Null # Update session log $sessionLogPath = "$script:DataPath\sessions\$SessionId.json" if (Test-Path $sessionLogPath) { $log = Get-Content $sessionLogPath -Raw | ConvertFrom-Json $log | Add-Member -NotePropertyName "endedAt" -NotePropertyValue (Get-Date -Format "o") -Force $log | Add-Member -NotePropertyName "status" -NotePropertyValue "ended" -Force $log | ConvertTo-Json | Out-File $sessionLogPath -Encoding UTF8 -Force } return @{ success = $true; message = "Session ended" } } catch { return @{ success = $false; error = $_.Exception.Message } } } # ═══════════════════════════════════════════════════════════════════════════════════════ # BACKEND/BACKGROUND ACCESS # Access device at login screen, in background, or system-level # ═══════════════════════════════════════════════════════════════════════════════════════ $script:BackendAccessEnabled = $true function Enable-BackendAccess { Write-Lucas "Enabling backend/background access..." "REMOTE" try { # 1. Enable Remote Desktop at login screen (before user logs in) Enable-RDPLoginScreen # 2. Enable background session (don't interrupt current user) Enable-BackgroundSession # 3. Enable SYSTEM-level access (highest privilege) Enable-SystemLevelAccess # 4. Configure for headless/console access Enable-ConsoleAccess $script:BackendAccessEnabled = $true Write-Lucas "Backend access enabled - full system control available" "SUCCESS" return @{ success = $true; message = "Backend access enabled" } } catch { Write-Lucas "Backend access setup failed: $($_.Exception.Message)" "ERROR" return @{ success = $false; error = $_.Exception.Message } } } function Enable-RDPLoginScreen { # Allow RDP connections at Windows login screen (before any user logs in) Write-Lucas "Configuring RDP login screen access..." "REMOTE" try { # Enable Remote Desktop Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0 -Force -ErrorAction SilentlyContinue # Allow connections from computers running any version of Remote Desktop Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "UserAuthentication" -Value 0 -Force -ErrorAction SilentlyContinue # Enable blank password login for local accounts (if needed) Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -Name "LimitBlankPasswordUse" -Value 0 -Force -ErrorAction SilentlyContinue # Enable RDP on all network profiles Enable-NetFirewallRule -DisplayGroup "Remote Desktop" -ErrorAction SilentlyContinue # Ensure Terminal Services is running Set-Service -Name "TermService" -StartupType Automatic -ErrorAction SilentlyContinue Start-Service -Name "TermService" -ErrorAction SilentlyContinue Write-Lucas "RDP login screen access configured" "REMOTE" } catch { Write-Lucas "RDP login screen config failed: $($_.Exception.Message)" "WARN" } } function Enable-BackgroundSession { # Allow background RDP sessions (shadow sessions, don't disconnect user) Write-Lucas "Configuring background session access..." "REMOTE" try { # Allow remote control of user sessions Set-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' -Name "Shadow" -Value 2 -Force -ErrorAction SilentlyContinue # Don't require user permission for shadow (value 4 = full control without permission) Set-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' -Name "Shadow" -Value 4 -Force -ErrorAction SilentlyContinue # Allow multiple RDP sessions Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fSingleSessionPerUser" -Value 0 -Force -ErrorAction SilentlyContinue Write-Lucas "Background session access configured" "REMOTE" } catch { Write-Lucas "Background session config failed: $($_.Exception.Message)" "WARN" } } function Enable-SystemLevelAccess { # Configure for SYSTEM-level remote access (highest privilege) Write-Lucas "Configuring SYSTEM-level access..." "REMOTE" try { # Ensure VXN Agent runs as SYSTEM (scheduled task) $taskExists = Get-ScheduledTask -TaskName "VXN Agent Service" -ErrorAction SilentlyContinue if (-not $taskExists) { # Create scheduled task to run as SYSTEM $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$script:ScriptPath`"" $trigger = New-ScheduledTaskTrigger -AtStartup $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) Register-ScheduledTask -TaskName "VXN Agent Service" -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force -ErrorAction SilentlyContinue } # Enable WinRM for remote PowerShell (backend access) Enable-PSRemoting -Force -SkipNetworkProfileCheck -ErrorAction SilentlyContinue Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*" -Force -ErrorAction SilentlyContinue Write-Lucas "SYSTEM-level access configured" "REMOTE" } catch { Write-Lucas "SYSTEM-level config failed: $($_.Exception.Message)" "WARN" } } function Enable-ConsoleAccess { # Enable console/headless access for servers and unattended machines Write-Lucas "Configuring console access..." "REMOTE" try { # Enable Remote Registry Set-Service -Name "RemoteRegistry" -StartupType Automatic -ErrorAction SilentlyContinue Start-Service -Name "RemoteRegistry" -ErrorAction SilentlyContinue # Enable Windows Remote Management Set-Service -Name "WinRM" -StartupType Automatic -ErrorAction SilentlyContinue Start-Service -Name "WinRM" -ErrorAction SilentlyContinue # Configure WinRM for remote access winrm quickconfig -quiet 2>$null # Allow WMI through firewall netsh advfirewall firewall set rule group="Windows Management Instrumentation (WMI)" new enable=yes 2>$null # Allow File and Printer Sharing (for remote admin) netsh advfirewall firewall set rule group="File and Printer Sharing" new enable=yes 2>$null Write-Lucas "Console access configured" "REMOTE" } catch { Write-Lucas "Console config failed: $($_.Exception.Message)" "WARN" } } function Start-BackgroundSession { param([string]$SessionId, [string]$TechnicianId, [string]$AccessType) # AccessType: "login-screen", "background", "system", "shadow" Write-Lucas "Starting $AccessType backend session: $SessionId" "REMOTE" try { $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } $sessionInfo = @{ sessionId = $SessionId technicianId = $TechnicianId accessType = $AccessType hostname = $env:COMPUTERNAME startedAt = (Get-Date -Format "o") status = "active" runningAsSystem = ([Security.Principal.WindowsIdentity]::GetCurrent().Name -eq "NT AUTHORITY\SYSTEM") } # For shadow sessions (view/control logged-in user without disconnecting) if ($AccessType -eq "shadow") { # Get active session ID $sessions = query session 2>$null | Where-Object { $_ -match "Active" } if ($sessions) { $sessionLine = $sessions | Select-Object -First 1 if ($sessionLine -match "(\d+)") { $activeSessionId = $Matches[1] $sessionInfo.targetSessionId = $activeSessionId # Shadow command would be executed by technician # mstsc /shadow:$activeSessionId /control /noConsentPrompt $sessionInfo.shadowCommand = "mstsc /shadow:$activeSessionId /control /noConsentPrompt" } } } # Report to server $body = @{ deviceId = $config.deviceId session = $sessionInfo } | ConvertTo-Json -Depth 3 -Compress $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/remote/backend-session", $body) | Out-Null return $sessionInfo } catch { Write-Lucas "Backend session start failed: $($_.Exception.Message)" "ERROR" return @{ success = $false; error = $_.Exception.Message } } } function Get-BackendAccessStatus { $status = @{ enabled = $script:BackendAccessEnabled hostname = $env:COMPUTERNAME runningAsSystem = ([Security.Principal.WindowsIdentity]::GetCurrent().Name -eq "NT AUTHORITY\SYSTEM") rdp = @{ enabled = (Get-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -ErrorAction SilentlyContinue).fDenyTSConnections -eq 0 loginScreen = $true } winrm = @{ enabled = (Get-Service WinRM -ErrorAction SilentlyContinue).Status -eq "Running" } shadow = @{ enabled = $true noConsent = $true } activeSessions = @() lastChecked = (Get-Date -Format "o") } # Get active user sessions try { $sessions = query session 2>$null if ($sessions) { $status.activeSessions = $sessions | Where-Object { $_ -match "Active|Disc" } | ForEach-Object { if ($_ -match "(\S+)\s+(\S+)\s+(\d+)\s+(\S+)") { @{ user = $Matches[2] sessionId = $Matches[3] state = $Matches[4] } } } } } catch {} return $status } function Invoke-BackendCommand { param([string]$Command, [string]$SessionType) Write-Lucas "Executing backend command ($SessionType)..." "REMOTE" try { # Execute as SYSTEM $output = "" if ($SessionType -eq "powershell") { $output = Invoke-Expression $Command 2>&1 | Out-String } elseif ($SessionType -eq "cmd") { $output = cmd /c $Command 2>&1 | Out-String } return @{ success = $true output = $output.Substring(0, [Math]::Min(10000, $output.Length)) executedAs = [Security.Principal.WindowsIdentity]::GetCurrent().Name timestamp = (Get-Date -Format "o") } } catch { return @{ success = $false; error = $_.Exception.Message } } } # Vellunox Remote (Splashtop) is the PRIMARY remote access method # Do NOT uninstall Splashtop - it is required for remote support # ═══════════════════════════════════════════════════════════════════════════════════════ # AGENT AUTO-UPDATE SYSTEM # ═══════════════════════════════════════════════════════════════════════════════════════ function Check-ForAgentUpdate { Write-Lucas "Checking for agent updates..." "UPDATE" try { $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") # Get latest version info from server $response = $wc.DownloadString($script:UpdateCheckUrl) $versionInfo = $response | ConvertFrom-Json if ($versionInfo.success -and $versionInfo.latestVersion) { $currentVersion = [version]$script:AgentVersion $latestVersion = [version]$versionInfo.latestVersion if ($latestVersion -gt $currentVersion) { Write-Lucas "Update available: $script:AgentVersion -> $($versionInfo.latestVersion)" "UPDATE" return @{ updateAvailable = $true currentVersion = $script:AgentVersion latestVersion = $versionInfo.latestVersion downloadUrl = $versionInfo.downloadUrl releaseNotes = $versionInfo.releaseNotes } } else { Write-Lucas "Agent is up to date ($script:AgentVersion)" "UPDATE" } } } catch { Write-Lucas "Update check failed: $($_.Exception.Message)" "WARN" } return @{ updateAvailable = $false; currentVersion = $script:AgentVersion } } function Install-AgentUpdate { param([string]$DownloadUrl) Write-Lucas "Downloading agent update..." "UPDATE" try { $tempPath = "$env:TEMP\vxn-agent-update.ps1" $wc = New-Object System.Net.WebClient $wc.DownloadFile($DownloadUrl, $tempPath) # Verify download if (-not (Test-Path $tempPath)) { Write-Lucas "Update download failed" "ERROR" return $false } # Copy new agent script Copy-Item $tempPath $script:ScriptPath -Force # Clean up Remove-Item $tempPath -Force -ErrorAction SilentlyContinue Write-Lucas "Agent updated successfully - will restart on next cycle" "SUCCESS" # Schedule restart of the agent service $restartScript = @" Start-Sleep -Seconds 5 Stop-ScheduledTask -TaskName 'VXN Agent Service' -ErrorAction SilentlyContinue Start-Sleep -Seconds 2 Start-ScheduledTask -TaskName 'VXN Agent Service' -ErrorAction SilentlyContinue "@ $restartPath = "$env:TEMP\vxn-restart.ps1" $restartScript | Out-File $restartPath -Encoding UTF8 # Start the restart script in background Start-Process powershell.exe -ArgumentList "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$restartPath`"" -WindowStyle Hidden return $true } catch { Write-Lucas "Update installation failed: $($_.Exception.Message)" "ERROR" return $false } } function Perform-AutoUpdate { # Only check once per hour if ($script:LastUpdateCheck) { $hoursSinceCheck = ((Get-Date) - $script:LastUpdateCheck).TotalHours if ($hoursSinceCheck -lt 1) { return } } $script:LastUpdateCheck = Get-Date $updateInfo = Check-ForAgentUpdate if ($updateInfo.updateAvailable) { $downloadUrl = $updateInfo.downloadUrl if (-not $downloadUrl) { $downloadUrl = $script:AgentDownloadUrl } $success = Install-AgentUpdate -DownloadUrl $downloadUrl if ($success) { Write-Lucas "Agent will restart with new version shortly" "UPDATE" } } } # ═══════════════════════════════════════════════════════════════════════════════════════ # AUTOMATIC CLEANUP - Remove stale processes and services # ═══════════════════════════════════════════════════════════════════════════════════════ function Invoke-AutoCleanup { Write-Lucas "Running automatic cleanup..." "CLEANUP" try { # Kill any duplicate VXN agent processes (keep only current) $currentPID = $PID Get-Process -Name "powershell" -ErrorAction SilentlyContinue | Where-Object { $_.Id -ne $currentPID -and $_.CommandLine -match "vxn-agent" } | ForEach-Object { Write-Lucas "Stopping duplicate agent process: $($_.Id)" "CLEANUP" Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue } # Clean up temp files older than 7 days Get-ChildItem "$env:TEMP\vxn-*" -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-7) } | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue # Clean up old log files (keep last 5 days) $logDir = "$script:InstallPath\logs" if (Test-Path $logDir) { Get-ChildItem "$logDir\*.log" -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-5) } | Remove-Item -Force -ErrorAction SilentlyContinue } # Remove any orphaned scheduled tasks from old agent versions $tasks = Get-ScheduledTask -TaskName "*VXN*" -ErrorAction SilentlyContinue foreach ($task in $tasks) { if ($task.TaskName -ne "VXN Agent Service" -and $task.TaskName -ne "VXN Agent Watchdog") { Write-Lucas "Removing orphaned task: $($task.TaskName)" "CLEANUP" Unregister-ScheduledTask -TaskName $task.TaskName -Confirm:$false -ErrorAction SilentlyContinue } } # Kill stale Splashtop UI processes (service should run, not UI) Get-Process Splashtop -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 } | Stop-Process -Force -ErrorAction SilentlyContinue # Clean Windows temp (safe cleanup) $tempFolders = @( "$env:TEMP\*.tmp", "$env:TEMP\~DF*.tmp", "C:\Windows\Temp\*.tmp" ) foreach ($folder in $tempFolders) { Get-ChildItem $folder -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-1) } | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue } Write-Lucas "Cleanup complete" "SUCCESS" } catch { Write-Lucas "Cleanup error: $($_.Exception.Message)" "WARN" } } # ═══════════════════════════════════════════════════════════════════════════════════════ # TACTICAL RMM FEATURES - Enterprise Monitoring & Management # ═══════════════════════════════════════════════════════════════════════════════════════ # ───────────────────────────────────────────────────────────────────────────────────────── # AUTOMATED CHECKS SYSTEM - Configurable health/performance monitoring # ───────────────────────────────────────────────────────────────────────────────────────── $script:CheckDefinitions = @{ "cpu_usage" = @{ name = "CPU Usage Check" interval = 120 # seconds warnThreshold = 80 critThreshold = 95 enabled = $true } "memory_usage" = @{ name = "Memory Usage Check" interval = 120 warnThreshold = 85 critThreshold = 95 enabled = $true } "disk_space" = @{ name = "Disk Space Check" interval = 300 warnThreshold = 80 # percent used critThreshold = 90 enabled = $true } "service_status" = @{ name = "Critical Services Check" interval = 60 services = @("Spooler", "wuauserv", "WinDefend", "mpssvc") enabled = $true } "event_log" = @{ name = "Event Log Error Check" interval = 300 maxAgeMinutes = 60 enabled = $true } } $script:LastCheckRun = @{} $script:CheckResults = @{} function Invoke-AutomatedCheck { param([string]$CheckId) $check = $script:CheckDefinitions[$CheckId] if (-not $check -or -not $check.enabled) { return $null } $result = @{ checkId = $CheckId name = $check.name timestamp = (Get-Date).ToString("o") status = "passing" value = 0 message = "" } try { switch ($CheckId) { "cpu_usage" { $cpu = (Get-Counter '\Processor(_Total)\% Processor Time' -ErrorAction SilentlyContinue).CounterSamples[0].CookedValue $result.value = [math]::Round($cpu, 1) if ($cpu -ge $check.critThreshold) { $result.status = "critical" $result.message = "CPU at ${cpu}% (critical threshold: $($check.critThreshold)%)" } elseif ($cpu -ge $check.warnThreshold) { $result.status = "warning" $result.message = "CPU at ${cpu}% (warning threshold: $($check.warnThreshold)%)" } else { $result.message = "CPU usage normal: ${cpu}%" } } "memory_usage" { $os = Get-CimInstance Win32_OperatingSystem $usedPercent = [math]::Round((($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / $os.TotalVisibleMemorySize) * 100, 1) $result.value = $usedPercent if ($usedPercent -ge $check.critThreshold) { $result.status = "critical" $result.message = "Memory at ${usedPercent}% (critical threshold: $($check.critThreshold)%)" } elseif ($usedPercent -ge $check.warnThreshold) { $result.status = "warning" $result.message = "Memory at ${usedPercent}% (warning threshold: $($check.warnThreshold)%)" } else { $result.message = "Memory usage normal: ${usedPercent}%" } } "disk_space" { $disks = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" $worstDisk = $null $worstPercent = 0 foreach ($disk in $disks) { $usedPercent = [math]::Round((($disk.Size - $disk.FreeSpace) / $disk.Size) * 100, 1) if ($usedPercent -gt $worstPercent) { $worstPercent = $usedPercent $worstDisk = $disk.DeviceID } } $result.value = $worstPercent if ($worstPercent -ge $check.critThreshold) { $result.status = "critical" $result.message = "Disk $worstDisk at ${worstPercent}% (critical threshold: $($check.critThreshold)%)" } elseif ($worstPercent -ge $check.warnThreshold) { $result.status = "warning" $result.message = "Disk $worstDisk at ${worstPercent}% (warning threshold: $($check.warnThreshold)%)" } else { $result.message = "Disk usage normal: $worstDisk at ${worstPercent}%" } } "service_status" { $failedServices = @() foreach ($svcName in $check.services) { $svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue if ($svc -and $svc.Status -ne "Running") { $failedServices += $svcName } } $result.value = $failedServices.Count if ($failedServices.Count -gt 0) { $result.status = "critical" $result.message = "Services not running: $($failedServices -join ', ')" } else { $result.message = "All critical services running" } } "event_log" { $cutoff = (Get-Date).AddMinutes(-$check.maxAgeMinutes) $errors = Get-WinEvent -FilterHashtable @{LogName='System';Level=2;StartTime=$cutoff} -MaxEvents 50 -ErrorAction SilentlyContinue $result.value = $errors.Count if ($errors.Count -gt 10) { $result.status = "warning" $result.message = "$($errors.Count) system errors in last $($check.maxAgeMinutes) minutes" } else { $result.message = "$($errors.Count) system events (normal)" } } } } catch { $result.status = "error" $result.message = "Check failed: $($_.Exception.Message)" } $script:CheckResults[$CheckId] = $result return $result } function Invoke-AllChecks { $results = @() $now = Get-Date foreach ($checkId in $script:CheckDefinitions.Keys) { $check = $script:CheckDefinitions[$checkId] $lastRun = $script:LastCheckRun[$checkId] if (-not $lastRun -or (($now - $lastRun).TotalSeconds -ge $check.interval)) { $result = Invoke-AutomatedCheck -CheckId $checkId if ($result) { $results += $result $script:LastCheckRun[$checkId] = $now # Alert on critical/warning if ($result.status -eq "critical") { Send-CheckAlert -Result $result -Severity "critical" } elseif ($result.status -eq "warning") { Send-CheckAlert -Result $result -Severity "warning" } } } } return $results } function Send-CheckAlert { param($Result, [string]$Severity) $deviceId = Get-MachineGUID $alertPayload = @{ deviceId = $deviceId checkId = $Result.checkId checkName = $Result.name severity = $Severity status = $Result.status value = $Result.value message = $Result.message timestamp = $Result.timestamp } | ConvertTo-Json -Compress try { $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$($script:ServerUrl)/api/rmm/alerts", $alertPayload) | Out-Null Write-Lucas "[$Severity] Alert sent: $($Result.checkId) - $($Result.message)" "CHECK" } catch { Write-Lucas "Alert send failed: $($_.Exception.Message)" "WARN" } } # ───────────────────────────────────────────────────────────────────────────────────────── # REMOTE SCRIPT EXECUTION - Run scripts from server library # ───────────────────────────────────────────────────────────────────────────────────────── function Invoke-RemoteScript { param( [string]$ScriptId, [string]$ScriptContent, [string]$ScriptType = "powershell", # powershell, batch, python [hashtable]$Arguments = @{}, [int]$TimeoutSeconds = 300, [bool]$WaitForOutput = $true ) Write-Lucas "Executing remote script: $ScriptId ($ScriptType)" "SCRIPT" $result = @{ scriptId = $ScriptId success = $false output = "" exitCode = -1 executionTime = 0 timestamp = (Get-Date).ToString("o") } $tempFile = "$env:TEMP\vxn-script-$ScriptId" $startTime = Get-Date try { switch ($ScriptType.ToLower()) { "powershell" { $tempFile += ".ps1" Set-Content -Path $tempFile -Value $ScriptContent -Force if ($WaitForOutput) { $output = & powershell.exe -ExecutionPolicy Bypass -NoProfile -File $tempFile @Arguments 2>&1 $result.output = $output -join "`n" $result.exitCode = $LASTEXITCODE } else { Start-Process powershell.exe -ArgumentList "-ExecutionPolicy Bypass -NoProfile -File `"$tempFile`"" -WindowStyle Hidden $result.output = "Script started in background" $result.exitCode = 0 } } "batch" { $tempFile += ".bat" Set-Content -Path $tempFile -Value $ScriptContent -Force $output = & cmd.exe /c $tempFile 2>&1 $result.output = $output -join "`n" $result.exitCode = $LASTEXITCODE } "python" { $tempFile += ".py" Set-Content -Path $tempFile -Value $ScriptContent -Force # Check for Python $python = Get-Command python -ErrorAction SilentlyContinue if ($python) { $output = & python $tempFile 2>&1 $result.output = $output -join "`n" $result.exitCode = $LASTEXITCODE } else { $result.output = "Python not installed on this system" $result.exitCode = 1 } } default { $result.output = "Unsupported script type: $ScriptType" $result.exitCode = 1 } } $result.success = ($result.exitCode -eq 0) $result.executionTime = [math]::Round(((Get-Date) - $startTime).TotalSeconds, 2) } catch { $result.output = "Script execution error: $($_.Exception.Message)" $result.exitCode = -1 $result.executionTime = [math]::Round(((Get-Date) - $startTime).TotalSeconds, 2) } finally { # Cleanup temp file if (Test-Path $tempFile) { Remove-Item $tempFile -Force -ErrorAction SilentlyContinue } } Write-Lucas "Script $ScriptId completed: Exit=$($result.exitCode), Time=$($result.executionTime)s" "SCRIPT" return $result } # ───────────────────────────────────────────────────────────────────────────────────────── # REMOTE REGISTRY EDITOR - Read/write registry remotely # ───────────────────────────────────────────────────────────────────────────────────────── function Get-RemoteRegistry { param( [string]$Path, # e.g., "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion" [string]$ValueName # Optional - if empty, returns all values ) try { if ($ValueName) { $value = Get-ItemProperty -Path $Path -Name $ValueName -ErrorAction Stop return @{ success = $true path = $Path name = $ValueName value = $value.$ValueName type = ($value.PSObject.Properties | Where-Object { $_.Name -eq $ValueName }).TypeNameOfValue } } else { $values = Get-ItemProperty -Path $Path -ErrorAction Stop $result = @{ success = $true path = $Path values = @{} } foreach ($prop in $values.PSObject.Properties) { if ($prop.Name -notlike "PS*") { $result.values[$prop.Name] = $prop.Value } } return $result } } catch { return @{ success = $false path = $Path error = $_.Exception.Message } } } function Set-RemoteRegistry { param( [string]$Path, [string]$ValueName, $Value, [string]$Type = "String" # String, DWord, QWord, Binary, MultiString, ExpandString ) try { # Create path if it doesn't exist if (-not (Test-Path $Path)) { New-Item -Path $Path -Force | Out-Null } Set-ItemProperty -Path $Path -Name $ValueName -Value $Value -Type $Type -Force return @{ success = $true path = $Path name = $ValueName value = $Value message = "Registry value set successfully" } } catch { return @{ success = $false path = $Path name = $ValueName error = $_.Exception.Message } } } # ───────────────────────────────────────────────────────────────────────────────────────── # EVENT LOG VIEWER - Remote access to Windows Event Logs # ───────────────────────────────────────────────────────────────────────────────────────── function Get-RemoteEventLogs { param( [string]$LogName = "System", # System, Application, Security, Setup [string]$Level = "All", # All, Error, Warning, Information, Critical [int]$MaxEvents = 100, [int]$LastMinutes = 60 ) try { $filterHash = @{ LogName = $LogName } if ($LastMinutes -gt 0) { $filterHash['StartTime'] = (Get-Date).AddMinutes(-$LastMinutes) } switch ($Level) { "Critical" { $filterHash['Level'] = 1 } "Error" { $filterHash['Level'] = 2 } "Warning" { $filterHash['Level'] = 3 } "Information" { $filterHash['Level'] = 4 } } $events = Get-WinEvent -FilterHashtable $filterHash -MaxEvents $MaxEvents -ErrorAction Stop $result = @{ success = $true logName = $LogName count = $events.Count events = @() } foreach ($event in $events) { $result.events += @{ id = $event.Id level = $event.LevelDisplayName source = $event.ProviderName message = $event.Message.Substring(0, [Math]::Min($event.Message.Length, 500)) timestamp = $event.TimeCreated.ToString("o") } } return $result } catch { return @{ success = $false logName = $LogName error = $_.Exception.Message } } } # ───────────────────────────────────────────────────────────────────────────────────────── # SERVICE MANAGER - Remote service control # ───────────────────────────────────────────────────────────────────────────────────────── function Get-RemoteServices { param( [string]$Filter = "*", # Service name filter [string]$Status = "All" # All, Running, Stopped ) try { $services = Get-Service -Name $Filter -ErrorAction SilentlyContinue if ($Status -ne "All") { $services = $services | Where-Object { $_.Status -eq $Status } } $result = @{ success = $true count = $services.Count services = @() } foreach ($svc in $services) { $result.services += @{ name = $svc.Name displayName = $svc.DisplayName status = $svc.Status.ToString() startType = $svc.StartType.ToString() } } return $result } catch { return @{ success = $false error = $_.Exception.Message } } } function Invoke-ServiceAction { param( [string]$ServiceName, [string]$Action # start, stop, restart, enable, disable ) try { $svc = Get-Service -Name $ServiceName -ErrorAction Stop switch ($Action.ToLower()) { "start" { Start-Service -Name $ServiceName -ErrorAction Stop $message = "Service $ServiceName started" } "stop" { Stop-Service -Name $ServiceName -Force -ErrorAction Stop $message = "Service $ServiceName stopped" } "restart" { Restart-Service -Name $ServiceName -Force -ErrorAction Stop $message = "Service $ServiceName restarted" } "enable" { Set-Service -Name $ServiceName -StartupType Automatic -ErrorAction Stop $message = "Service $ServiceName set to Automatic" } "disable" { Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue Set-Service -Name $ServiceName -StartupType Disabled -ErrorAction Stop $message = "Service $ServiceName disabled" } default { return @{ success = $false service = $ServiceName error = "Unknown action: $Action" } } } # Get updated status $svc = Get-Service -Name $ServiceName return @{ success = $true service = $ServiceName action = $Action newStatus = $svc.Status.ToString() message = $message } } catch { return @{ success = $false service = $ServiceName action = $Action error = $_.Exception.Message } } } # ───────────────────────────────────────────────────────────────────────────────────────── # CHOCOLATEY INTEGRATION - Remote software installation # ───────────────────────────────────────────────────────────────────────────────────────── function Ensure-Chocolatey { if (Get-Command choco -ErrorAction SilentlyContinue) { return $true } Write-Lucas "Installing Chocolatey package manager..." "SOFTWARE" try { Set-ExecutionPolicy Bypass -Scope Process -Force [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) # Refresh path $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") Write-Lucas "Chocolatey installed successfully" "SUCCESS" return $true } catch { Write-Lucas "Chocolatey install failed: $($_.Exception.Message)" "ERROR" return $false } } function Install-ChocoPackage { param( [string]$PackageName, [string]$Version = "", [bool]$Force = $false ) if (-not (Ensure-Chocolatey)) { return @{ success = $false package = $PackageName error = "Chocolatey not available" } } Write-Lucas "Installing package: $PackageName" "SOFTWARE" try { $args = "install $PackageName -y" if ($Version) { $args += " --version=$Version" } if ($Force) { $args += " --force" } $output = & choco $args.Split(" ") 2>&1 $success = ($LASTEXITCODE -eq 0) return @{ success = $success package = $PackageName output = ($output -join "`n") message = if ($success) { "Package $PackageName installed" } else { "Installation failed" } } } catch { return @{ success = $false package = $PackageName error = $_.Exception.Message } } } # ───────────────────────────────────────────────────────────────────────────────────────── # FILE BROWSER - Remote file system access # ───────────────────────────────────────────────────────────────────────────────────────── function Get-RemoteDirectoryListing { param( [string]$Path = "C:\", [bool]$Recursive = $false ) try { if (-not (Test-Path $Path)) { return @{ success = $false path = $Path error = "Path does not exist" } } $items = Get-ChildItem -Path $Path -Force -ErrorAction Stop $result = @{ success = $true path = $Path items = @() } foreach ($item in $items) { $result.items += @{ name = $item.Name fullPath = $item.FullName type = if ($item.PSIsContainer) { "directory" } else { "file" } size = if (-not $item.PSIsContainer) { $item.Length } else { 0 } lastModified = $item.LastWriteTime.ToString("o") attributes = $item.Attributes.ToString() } } return $result } catch { return @{ success = $false path = $Path error = $_.Exception.Message } } } function Get-RemoteFileContent { param( [string]$FilePath, [int]$MaxBytes = 1048576 # 1MB limit ) try { if (-not (Test-Path $FilePath)) { return @{ success = $false path = $FilePath error = "File does not exist" } } $file = Get-Item $FilePath if ($file.Length -gt $MaxBytes) { return @{ success = $false path = $FilePath error = "File too large (max $MaxBytes bytes)" } } $content = Get-Content -Path $FilePath -Raw -ErrorAction Stop return @{ success = $true path = $FilePath size = $file.Length content = $content } } catch { return @{ success = $false path = $FilePath error = $_.Exception.Message } } } # ───────────────────────────────────────────────────────────────────────────────────────── # COLLECTOR TASKS - Save script output to custom fields # ───────────────────────────────────────────────────────────────────────────────────────── $script:CustomFields = @{} function Set-CustomField { param( [string]$FieldName, $Value ) $script:CustomFields[$FieldName] = @{ value = $Value timestamp = (Get-Date).ToString("o") } # Report to server try { $payload = @{ deviceId = Get-MachineGUID fieldName = $FieldName value = $Value timestamp = (Get-Date).ToString("o") } | ConvertTo-Json -Compress $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$($script:ServerUrl)/api/rmm/custom-fields", $payload) | Out-Null } catch { Write-Lucas "Custom field update failed: $($_.Exception.Message)" "WARN" } return @{ success = $true field = $FieldName value = $Value } } function Get-CustomField { param([string]$FieldName) if ($script:CustomFields.ContainsKey($FieldName)) { return $script:CustomFields[$FieldName] } return $null } # ═══════════════════════════════════════════════════════════════════════════════════════ # STARTUP SEQUENCE # ═══════════════════════════════════════════════════════════════════════════════════════ # Run cleanup first Invoke-AutoCleanup # Check for updates on startup Perform-AutoUpdate # Register with server Register-Device # Initial heartbeat Send-Heartbeat # Ensure Vellunox Remote (Splashtop) is installed for remote access $remoteStatus = Ensure-VellunoxRemote if ($remoteStatus.ready) { Write-Lucas "Remote Access: Vellunox Remote ready - ID: $($remoteStatus.SplashtopId)" "SUCCESS" } else { Write-Lucas "Remote Access: Vellunox Remote installation pending..." "REMOTE" } # ═══════════════════════════════════════════════════════════════════════════════════════ # WEBROOT INSTALLATION - INCLUDED WITH ALL MANAGED DEVICES # ═══════════════════════════════════════════════════════════════════════════════════════ Write-Lucas "Checking Webroot security status..." "SECURITY" $webrootReady = Ensure-WebrootInstalled if ($webrootReady) { Write-Lucas "Security Stack: VXN Agent + Max Guardian + Webroot + Defender" "SECURITY" } else { Write-Lucas "Security Stack: VXN Agent + Max Guardian + Defender (Webroot pending)" "SECURITY" } Send-WebrootStatus # ═══════════════════════════════════════════════════════════════════════════════════════ # UNATTENDED + BACKEND REMOTE ACCESS - ENABLED BY DEFAULT FOR MANAGED DEVICES # ═══════════════════════════════════════════════════════════════════════════════════════ Write-Lucas "Configuring full remote access capabilities..." "REMOTE" $config = Get-AgentConfig if ($config.unattendedAccess -and $config.unattendedAccess.enabled -eq $false) { Write-Lucas "Unattended access disabled by policy" "REMOTE" } else { # Enable unattended access by default for managed devices Enable-RemoteDesktop Enable-VXNRemoteFirewall $script:UnattendedAccessEnabled = $true Write-Lucas "Unattended remote access: ENABLED" "SUCCESS" # Enable backend/background access (login screen, shadow sessions, SYSTEM-level) Enable-BackendAccess Write-Lucas "═══════════════════════════════════════════════════════" "REMOTE" Write-Lucas "REMOTE ACCESS CAPABILITIES:" "REMOTE" Write-Lucas " ✓ Unattended Access - Connect without user approval" "REMOTE" Write-Lucas " ✓ Login Screen Access - Connect before user logs in" "REMOTE" Write-Lucas " ✓ Background/Shadow - View/control without interrupting user" "REMOTE" Write-Lucas " ✓ SYSTEM-level Access - Full administrative control" "REMOTE" Write-Lucas " ✓ Console Access - Remote PowerShell & WinRM" "REMOTE" Write-Lucas "═══════════════════════════════════════════════════════" "REMOTE" } # Install the client-visible support surface after SYSTEM-level repair access is ready. Start-LucasWidget # Initial full inventory Send-FullInventory # Initial Lucas health check $issues = Invoke-LucasHealthCheck if ($issues.Count -gt 0) { Invoke-LucasAutoRemediate $issues } # Initial threat scan $threats = Invoke-LucasThreatScan # Initial network discovery (first scan after 5 minutes to not slow startup) $networkScanScheduled = $false # Counters for scheduled tasks $fastPingCount = 0 # Counts 30-second pings $fullHeartbeatCount = 0 # Counts full heartbeats $healthCheckCount = 0 $networkScanCount = 0 $bitlockerCheckCount = 0 $webrootCheckCount = 0 # Webroot status check counter # WebSocket connection state $script:WebSocketConnected = $false $script:WebSocketClient = $null $script:WebSocketCancelToken = $null # WebSocket real-time connection function Connect-WebSocket { try { # Check if .NET 4.5+ WebSocket is available $wsType = [Type]::GetType("System.Net.WebSockets.ClientWebSocket, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") if (-not $wsType) { Write-Lucas "WebSocket not available - using HTTP polling" "WARN" return $false } $config = Get-AgentConfig $deviceId = if ($config.deviceId) { $config.deviceId } else { $env:COMPUTERNAME } $encodedDeviceId = [System.Uri]::EscapeDataString($deviceId) $encodedHostname = [System.Uri]::EscapeDataString($env:COMPUTERNAME) $wsUrl = "$($script:WebSocketUrl)?id=$encodedDeviceId&hostname=$encodedHostname" Write-Lucas "Connecting to WebSocket: $wsUrl" "WS" # Create WebSocket client $script:WebSocketClient = New-Object System.Net.WebSockets.ClientWebSocket $script:WebSocketCancelToken = New-Object System.Threading.CancellationTokenSource # Connect asynchronously $uri = [Uri]$wsUrl $connectTask = $script:WebSocketClient.ConnectAsync($uri, $script:WebSocketCancelToken.Token) $connectTask.Wait(10000) # 10 second timeout if ($script:WebSocketClient.State -eq [System.Net.WebSockets.WebSocketState]::Open) { $script:WebSocketConnected = $true Write-Lucas "WebSocket CONNECTED - Real-time mode active!" "WS" # Send initial registration $regMsg = @{ type = "register" id = $deviceId deviceId = $deviceId hostname = $env:COMPUTERNAME platform = "windows" version = $script:AgentVersion agentVersion = $script:AgentVersion timestamp = (Get-Date -Format "o") } | ConvertTo-Json -Compress Send-WebSocketMessage $regMsg return $true } else { Write-Lucas "WebSocket connection failed - state: $($script:WebSocketClient.State)" "WARN" return $false } } catch { Write-Lucas "WebSocket error: $($_.Exception.Message)" "WARN" $script:WebSocketConnected = $false return $false } } function Send-WebSocketMessage { param([string]$Message) try { if (-not $script:WebSocketConnected -or $script:WebSocketClient.State -ne [System.Net.WebSockets.WebSocketState]::Open) { return $false } $bytes = [System.Text.Encoding]::UTF8.GetBytes($Message) $segment = New-Object System.ArraySegment[byte] -ArgumentList @(,$bytes) $sendTask = $script:WebSocketClient.SendAsync($segment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, $script:WebSocketCancelToken.Token) $sendTask.Wait(5000) return $true } catch { return $false } } function Receive-WebSocketMessage { try { if (-not $script:WebSocketConnected -or $script:WebSocketClient.State -ne [System.Net.WebSockets.WebSocketState]::Open) { return $null } $buffer = New-Object byte[] 8192 $segment = New-Object System.ArraySegment[byte] -ArgumentList @(,$buffer) # Non-blocking receive with short timeout $cts = New-Object System.Threading.CancellationTokenSource $cts.CancelAfter(100) # 100ms timeout for non-blocking check try { $receiveTask = $script:WebSocketClient.ReceiveAsync($segment, $cts.Token) $receiveTask.Wait() if ($receiveTask.Result.MessageType -eq [System.Net.WebSockets.WebSocketMessageType]::Text) { $message = [System.Text.Encoding]::UTF8.GetString($buffer, 0, $receiveTask.Result.Count) return $message | ConvertFrom-Json } elseif ($receiveTask.Result.MessageType -eq [System.Net.WebSockets.WebSocketMessageType]::Close) { Write-Lucas "WebSocket closed by server" "WS" $script:WebSocketConnected = $false } } catch [System.OperationCanceledException] { # Timeout - no message available, this is normal } catch [System.AggregateException] { # Check if it's just a timeout if ($_.Exception.InnerException -is [System.OperationCanceledException]) { # Normal timeout, no message } else { throw } } return $null } catch { return $null } } function Close-WebSocket { try { if ($script:WebSocketClient -and $script:WebSocketClient.State -eq [System.Net.WebSockets.WebSocketState]::Open) { $closeTask = $script:WebSocketClient.CloseAsync([System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure, "Agent shutdown", $script:WebSocketCancelToken.Token) $closeTask.Wait(5000) } $script:WebSocketConnected = $false } catch {} } # Fast ping function - minimal data, just check for commands (HTTP fallback) function Send-FastPing { try { $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } # Minimal ping payload $body = @{ hostname = $env:COMPUTERNAME deviceId = $config.deviceId ping = $true agentVersion = $script:AgentVersion timestamp = (Get-Date -Format "o") } | ConvertTo-Json -Compress $wc = New-Object System.Net.WebClient $wc.Headers.Add("Content-Type", "application/json") $response = $wc.UploadString("$serverUrl/api/rmm/ping", $body) $data = $response | ConvertFrom-Json # Execute any pending commands if ($data.commands -and $data.commands.Count -gt 0) { Write-Lucas "Received $($data.commands.Count) command(s)" "COMMAND" foreach ($cmd in $data.commands) { Invoke-LucasCommand $cmd } } return $true } catch { return $false } } Write-Lucas "Agent v$script:AgentVersion - REAL-TIME MODE" "START" Initialize-MaxSecurityAllowlist | Out-Null # Try WebSocket connection first $wsConnected = Connect-WebSocket if ($wsConnected) { Write-Lucas "REAL-TIME: WebSocket connected - instant command delivery!" "WS" } else { Write-Lucas "FALLBACK: Using 30-second HTTP polling" "HTTP" } while ($true) { # Check for WebSocket messages (real-time commands) if ($script:WebSocketConnected) { $msg = Receive-WebSocketMessage if ($msg) { if ($msg.type -eq "command") { Write-Lucas "REAL-TIME command received: $($msg.command)" "WS" Invoke-LucasCommand $msg } elseif ($msg.type -eq "ping") { # Server ping - respond with pong Send-WebSocketMessage (@{ type = "pong"; timestamp = (Get-Date -Format "o") } | ConvertTo-Json -Compress) } } # Check if WebSocket is still connected if ($script:WebSocketClient.State -ne [System.Net.WebSockets.WebSocketState]::Open) { Write-Lucas "WebSocket disconnected - attempting reconnect..." "WS" $script:WebSocketConnected = $false Start-Sleep -Seconds 5 $wsConnected = Connect-WebSocket } # Short sleep when using WebSocket (real-time) Start-Sleep -Milliseconds 500 } else { # HTTP polling fallback - 30 second intervals Start-Sleep -Seconds $script:FastPollInterval } $fastPingCount++ # When using HTTP polling, check for commands every 30 seconds if (-not $script:WebSocketConnected) { Send-FastPing } # Full heartbeat every 5 minutes (10 cycles for HTTP, or 600 cycles for WebSocket) $heartbeatThreshold = if ($script:WebSocketConnected) { 600 } else { 10 } if ($fastPingCount -ge $heartbeatThreshold) { Send-Heartbeat $fullHeartbeatCount++ $healthCheckCount++ $networkScanCount++ $bitlockerCheckCount++ $fastPingCount = 0 Write-Lucas "Full heartbeat sent (#$fullHeartbeatCount)" "HEARTBEAT" } # WebRTC is always ready (no periodic check needed) # Splashtop check removed - using WebRTC only # BitLocker keys every 30 minutes (6 full heartbeats) if ($bitlockerCheckCount -ge 6) { $bitlocker = Get-BitLockerRecoveryKeys if ($bitlocker.Count -gt 0) { $config = Get-AgentConfig $serverUrl = if ($config.serverUrl) { $config.serverUrl } else { $script:ServerUrl } $body = @{ deviceId = $config.deviceId; hostname = $env:COMPUTERNAME; volumes = $bitlocker } | ConvertTo-Json -Depth 4 -Compress try { $wc = New-Object System.Net.WebClient; $wc.Headers.Add("Content-Type", "application/json") $wc.UploadString("$serverUrl/api/rmm/bitlocker/report", $body) | Out-Null Write-Lucas "BitLocker keys reported ($($bitlocker.Count) volumes)" "SECURITY" } catch {} } $bitlockerCheckCount = 0 } # Webroot status check every 1 hour (12 full heartbeats) $webrootCheckCount++ if ($webrootCheckCount -ge 12) { # Ensure Webroot is still running $webrootStatus = Get-WebrootStatus if (-not $webrootStatus.running) { Write-Lucas "Webroot not running - attempting restart..." "SECURITY" Ensure-WebrootInstalled } Send-WebrootStatus # Trigger Webroot update Update-Webroot $webrootCheckCount = 0 } # Lucas health check every 30 minutes (6 full heartbeats) if ($healthCheckCount -ge 6) { $issues = Invoke-LucasHealthCheck if ($issues.Count -gt 0) { Invoke-LucasAutoRemediate $issues } Invoke-LucasThreatScan $healthCheckCount = 0 } # Full inventory every 2 hours (24 full heartbeats) if ($fullHeartbeatCount -ge 24) { Send-FullInventory $fullHeartbeatCount = 0 } # Network discovery every 6 hours (72 full heartbeats) if ($networkScanCount -ge 72 -or (-not $networkScanScheduled)) { $devices = Invoke-NetworkDiscovery if ($devices.Count -gt 0) { Send-NetworkDiscoveryReport $devices } $networkScanScheduled = $true $networkScanCount = 0 } # Update uptime metric $script:LucasMetrics.uptimeHours = [math]::Round($script:LucasMetrics.uptimeHours + 0.00833, 4) # 30 sec = 0.00833 hours }