<# : @echo off setlocal title IT Asset Tracker - Enterprise Installer :: 1. Administrative Elevation net session >nul 2>&1 || (powershell -Command "Start-Process -FilePath '%~f0' -Verb RunAs" & exit /b) :: 2. Copy to temp .ps1 to bypass PowerShell 5.1 extension policy copy /y "%~f0" "%temp%\%~n0.ps1" >nul powershell -NoProfile -ExecutionPolicy Bypass -File "%temp%\%~n0.ps1" del "%temp%\%~n0.ps1" echo. if %errorlevel% neq 0 ( echo [PROCESS TERMINATED] An error occurred during installation. ) pause exit /b #> # --- ENTERPRISE ASSET TRACKER ENGINE v0.5.0 --- # AUTO-GENERATED BY SERVER — DO NOT EDIT MANUALLY $ErrorActionPreference = "Stop" $VERSION = "0.5.0" $INSTALL_DIR = "C:\assetTracker" $SCRIPT_NAME = "assetTracker.ps1" $TASK_NAME = "assetTracker" $API_URL = "http://it-asset-inventory.test/api/assets/sync" $API_KEY = "asset_inventory_secret_2026_token" $TOKEN_FILE = "$INSTALL_DIR\token.txt" $CACHE_FILE = "$INSTALL_DIR\last_scan.json" $QUEUE_FILE = "$INSTALL_DIR\offline_payload.json" $VERSION_FILE = "$INSTALL_DIR\version.txt" function Write-TUI($status, $msg, $color="96") { Write-Host " [$([char]27)[$($color)m$status$([char]27)[0m] $msg" } # Ensure directory exists if (!(Test-Path $INSTALL_DIR)) { New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null } # Load Token $headers = @{} if (Test-Path $TOKEN_FILE) { $token = (Get-Content $TOKEN_FILE).Trim() if ($token) { $headers["X-Machine-Token"] = $token } } if (!$headers.ContainsKey("X-Machine-Token")) { $headers["X-Asset-Key"] = $API_KEY } # Function to send payload with retries function Send-PayloadWithRetry($payload, $isHeartbeat=$false) { $maxRetries = 3 $retryDelay = 30 $json = $payload | ConvertTo-Json -Depth 10 -Compress for ($i = 1; $i -le $maxRetries; $i++) { try { $uri = $API_URL if ($isHeartbeat) { $uri = "${API_URL}?heartbeat=true" } $response = Invoke-RestMethod -Uri $uri -Method Post -Body $json -ContentType "application/json" -Headers $headers return $response } catch { Write-TUI "WARN" "Send attempt $i failed: $($_.Exception.Message)" "93" if ($_.Exception.Response -and ([int]$_.Exception.Response.StatusCode -eq 404 -or [int]$_.Exception.Response.StatusCode -eq 401)) { Write-TUI "WARN" "Server returned 401/404 (Unauthorized/Not Found). Clearing local cache to force full sync." "93" if (Test-Path $CACHE_FILE) { Remove-Item $CACHE_FILE -Force | Out-Null } if (Test-Path $TOKEN_FILE) { Remove-Item $TOKEN_FILE -Force | Out-Null } if (Test-Path $QUEUE_FILE) { Remove-Item $QUEUE_FILE -Force | Out-Null } $script:headers = @{"X-Asset-Key" = $script:API_KEY} return $null } if ($i -lt $maxRetries) { Start-Sleep -Seconds $retryDelay } } } return $null } # --- Self-Update Check --- function Invoke-SelfUpdate { try { $versionUrl = ($API_URL -replace '/assets/sync$', '/installer/version') $installerUrl = ($API_URL -replace '/assets/sync$', '/installer') $latestInfo = Invoke-RestMethod -Uri $versionUrl -Method Get -TimeoutSec 10 $latestVersion = $latestInfo.version if ($latestVersion -and $latestVersion -ne $VERSION) { Write-TUI "BUSY" "Update available: $VERSION -> $latestVersion" "93" $newScript = Invoke-RestMethod -Uri "${installerUrl}?key=$API_KEY" -Method Get -TimeoutSec 30 if ($newScript -and $newScript.Length -gt 100) { $newScript | Out-File -FilePath "$INSTALL_DIR\$SCRIPT_NAME" -Encoding UTF8 -Force $latestVersion | Out-File -FilePath $VERSION_FILE -Encoding ASCII -Force Write-TUI "DONE" "Agent updated to v$latestVersion. Restarting..." "92" # Re-execute the updated script & "$INSTALL_DIR\$SCRIPT_NAME" exit } else { Write-TUI "WARN" "Downloaded script appears invalid. Skipping update." "93" } } else { Write-TUI "DONE" "Agent is up to date (v$VERSION)." "92" } } catch { Write-TUI "WARN" "Self-update check failed: $($_.Exception.Message). Continuing with current version." "93" } } cls Write-Host @" _____ __ _________ __ / _ \ ______ ______ ___/ |\__ __/______ _____ ____ | | __ ____ ______ / /_\ \ / ___// ___// __ \ | | \_ __ \\__ \ / ___\| |/ /_/ __ \\_ __ \ / | \\___ \ \___ \/ /_/ \ | | | | \/ / __ \/ /_/ > < \ ___/ | | \/ \____|__ /____ >____ >\____ \ |__| |__| (____ /\___ /|__|_ \\\___ >|__| \/ \/ \/ \/ \//_____/ \/ \/ INTERNAL IT INFRASTRUCTURE - UNIFIED DEPLOYMENT v$VERSION ====================================================== "@ try { # 0. Detect execution context $scriptPath = $MyInvocation.MyCommand.Path $isInstalled = $scriptPath -and ($scriptPath -like "$INSTALL_DIR*") $isFromFile = [bool]$scriptPath # Self-Update (only when running from installed location) if ($isInstalled) { Invoke-SelfUpdate } # 1. Handle offline queue first if exists if (Test-Path $QUEUE_FILE) { Write-TUI "BUSY" "Found offline queue data. Attempting to sync..." "93" $queuedData = Get-Content $QUEUE_FILE -Raw | ConvertFrom-Json $isHB = $queuedData.PSObject.Properties['heartbeat'] -ne $null -and $queuedData.heartbeat -eq $true $response = Send-PayloadWithRetry $queuedData $isHB if ($response) { Write-TUI "DONE" "Offline queue synced successfully." "92" Remove-Item $QUEUE_FILE -Force | Out-Null if ($response.PSObject.Properties['machine_token'] -and $response.machine_token) { $response.machine_token | Out-File -FilePath $TOKEN_FILE -Encoding ASCII -Force $headers["X-Machine-Token"] = $response.machine_token if ($headers.ContainsKey("X-Asset-Key")) { $headers.Remove("X-Asset-Key") } } } else { Write-TUI "FAIL" "Could not sync offline queue. Proceeding in offline mode..." "91" } } # 2. Deployment (first run only) Write-TUI "BUSY" "Preparing system directory..." "93" if (!$isInstalled) { Write-TUI "BUSY" "Deploying engine to $INSTALL_DIR..." "93" if ($isFromFile) { # Running from a local file (e.g. .bat or .ps1 on disk) $Content = Get-Content -LiteralPath $scriptPath $Content | Out-File -FilePath "$INSTALL_DIR\$SCRIPT_NAME" -Encoding UTF8 -Force } else { # Running via irm | iex (in-memory, no file on disk) $installerUrl = ($API_URL -replace '/assets/sync$', '/installer') Write-TUI "BUSY" "Downloading agent from server..." "93" $downloadedScript = Invoke-RestMethod -Uri "${installerUrl}?key=$API_KEY" -Method Get -TimeoutSec 30 $downloadedScript | Out-File -FilePath "$INSTALL_DIR\$SCRIPT_NAME" -Encoding UTF8 -Force } $VERSION | Out-File -FilePath $VERSION_FILE -Encoding ASCII -Force Write-TUI "DONE" "Engine deployed successfully." "92" } # 3. Persistence (Startup + 30-minute) Write-TUI "BUSY" "Configuring Startup & 30-minute Persistence..." "93" $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File $INSTALL_DIR\$SCRIPT_NAME" $trigger1 = New-ScheduledTaskTrigger -AtStartup $trigger2 = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 30) $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries Register-ScheduledTask -TaskName $TASK_NAME -Action $action -Trigger @($trigger1, $trigger2) -Principal $principal -Settings $settings -Force | Out-Null Write-TUI "DONE" "Persistence established." "92" # 4. Discovery Write-TUI "BUSY" "Performing system audit..." "93" $cs = Get-CimInstance Win32_ComputerSystem $os = Get-CimInstance Win32_OperatingSystem $cpu = Get-CimInstance Win32_Processor $diskC = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'" $bios = Get-CimInstance Win32_Bios $mb = Get-CimInstance Win32_BaseBoard # Hardware Collections $ramDetails = Get-CimInstance Win32_PhysicalMemory | ForEach-Object { [PSCustomObject]@{ manufacturer = "$($_.Manufacturer)".Trim(); part_number = "$($_.PartNumber)".Trim(); capacity_gb = [int][Math]::Round($_.Capacity / 1GB); speed_mhz = [int]$_.ConfiguredClockSpeed } } $physicalDisks = Get-CimInstance Win32_DiskDrive | ForEach-Object { $drive = $_; $pDisk = Get-PhysicalDisk | Where-Object { $_.DeviceID -eq $drive.Index } if ($pDisk.BusType -ne 'USB') { [PSCustomObject]@{ model = "$($drive.Model)".Trim(); size_gb = [int][Math]::Round($drive.Size / 1GB); media_type = "$($pDisk.MediaType)"; interface = "$($pDisk.BusType)" } } } | Where-Object { $_ } $gpus = Get-CimInstance Win32_VideoController | ForEach-Object { [PSCustomObject]@{ name = "$($_.Name)"; vram_mb = [int][Math]::Round($_.AdapterRAM / 1MB); driver_version = "$($_.DriverVersion)" } } $monitors = Get-CimInstance WmiMonitorID -Namespace root\wmi -ErrorAction SilentlyContinue | ForEach-Object { $mBytes = $_.ManufacturerName | Where-Object { $_ -ne 0 }; $manuf = if ($mBytes) { [System.Text.Encoding]::ASCII.GetString($mBytes) } else { "" } $pBytes = $_.UserFriendlyName | Where-Object { $_ -ne 0 }; $model = if ($pBytes) { [System.Text.Encoding]::ASCII.GetString($pBytes) } else { "" } $sBytes = $_.SerialNumberID | Where-Object { $_ -ne 0 }; $sn = if ($sBytes) { [System.Text.Encoding]::ASCII.GetString($sBytes) } else { "" } [PSCustomObject]@{ manufacturer = "$manuf".Trim(); model = "$model".Trim(); serial_number = "$sn".Trim() } } $totalDiskSize = 0 foreach($d in $physicalDisks) { $totalDiskSize += $d.size_gb } # Network Topology $nics = Get-NetIPConfiguration | Where-Object { $_.IPv4Address } | Select-Object -First 1 $adapter = Get-NetAdapter | Where-Object InterfaceAlias -eq $nics.InterfaceAlias $dnsArray = $nics.DNSServer.ServerAddresses | Where-Object { $_ -match '^\d+\.\d+\.\d+\.\d+$' } | Sort-Object $dnsString = $dnsArray -join ', ' $ssid = $null if ($adapter.InterfaceAlias -like "*Wi-Fi*") { $wlanInfo = netsh wlan show interfaces if ($wlanInfo -match "\s+SSID\s+:\s+(.*)") { $ssid = $matches[1].Trim() } } $payload = @{ serial_number = "$($bios.SerialNumber)" hostname = "$($env:COMPUTERNAME)" agent_version = $VERSION hardware = @{ cpu = "$($cpu.Name)" motherboard = @{ manufacturer = "$($mb.Manufacturer)"; product = "$($mb.Product)"; version = "$($mb.Version)" } ram_gb = [int][Math]::Round($cs.TotalPhysicalMemory / 1GB) ram_details = @($ramDetails) disk_total_gb = [int]$totalDiskSize disk_free_gb = [int][Math]::Round($diskC.FreeSpace / 1GB) disk_details = @($physicalDisks) gpu_details = @($gpus) monitors = @($monitors) } os = @{ name = "$($os.Caption)" version = "$($os.Version)" last_boot = $os.LastBootUpTime.ToString("yyyy-MM-dd HH:mm:ss") battery_status = if ($cs.PCSystemType -eq 2) { "Battery" } else { "A/C Power" } } network = @{ ip = "$($nics.IPv4Address.IPAddress)" gateway = if ($nics.IPv4DefaultGateway) { "$($nics.IPv4DefaultGateway.NextHop)" } else { $null } subnet = "$($nics.IPv4Address.PrefixLength)" dns = $dnsString mac = "$($adapter.MacAddress)" type = "$($nics.InterfaceAlias)" speed = "$($adapter.LinkSpeed)" ssid = $ssid dhcp = $true } user = "$($env:USERNAME)" } # 5. Delta Sync check $isDelta = $false $currentFullJson = $payload | ConvertTo-Json -Depth 10 -Compress if (Test-Path $CACHE_FILE) { $cachedFullJson = Get-Content $CACHE_FILE -Raw try { $cachedPayload = $cachedFullJson | ConvertFrom-Json $cpuChange = $payload.hardware.cpu -ne $cachedPayload.hardware.cpu $ramChange = $payload.hardware.ram_gb -ne $cachedPayload.hardware.ram_gb $diskChange = $payload.hardware.disk_total_gb -ne $cachedPayload.hardware.disk_total_gb $osChange = $payload.os.name -ne $cachedPayload.os.name if (!$cpuChange -and !$ramChange -and !$diskChange -and !$osChange) { $isDelta = $true } } catch { $isDelta = $false } } if ($isDelta) { Write-TUI "BUSY" "No hardware changes detected. Sending heartbeat..." "93" $hbPayload = @{ serial_number = $payload.serial_number hostname = $payload.hostname user = $payload.user agent_version = $VERSION heartbeat = $true } $response = Send-PayloadWithRetry $hbPayload $true if ($response) { Write-TUI "DONE" "Heartbeat registered." "92" } else { Write-TUI "FAIL" "Failed to send heartbeat. Saving to offline queue." "91" $hbPayload | ConvertTo-Json -Depth 10 -Compress | Out-File -FilePath $QUEUE_FILE -Force } } else { Write-TUI "BUSY" "Hardware changes detected or first sync. Sending full profile..." "93" $response = Send-PayloadWithRetry $payload $false if ($response) { Write-TUI "DONE" "Full profile synchronized. Asset Registered: $($response.hostname)" "92" $currentFullJson | Out-File -FilePath $CACHE_FILE -Force if ($response.PSObject.Properties['machine_token'] -and $response.machine_token) { $response.machine_token | Out-File -FilePath $TOKEN_FILE -Encoding ASCII -Force } } else { Write-TUI "FAIL" "Failed to send profile. Saving to offline queue." "91" $currentFullJson | Out-File -FilePath $QUEUE_FILE -Force } } Write-Host "`n ======================================================" -ForegroundColor Green Write-Host " DEPLOYMENT SUCCESSFUL!" -ForegroundColor White Write-Host " The agent is now active and will sync periodically." -ForegroundColor White Write-Host " ======================================================`n" -ForegroundColor Green } catch { Write-TUI "FAIL" "Critical Error: $($_.Exception.Message)" -color "91" if ($_.Exception.Response) { $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) $details = $reader.ReadToEnd() Write-Host " [DEBUG] Server Response: $details" -ForegroundColor Gray } } if ([Environment]::UserInteractive) { Write-Host " Press any key to exit..." [void][System.Console]::ReadKey($true) }