diff --git a/GeoNetAgent/.cursor/api-contracts.md b/GeoNetAgent/.cursor/api-contracts.md index 2f979b8..70b63d0 100644 --- a/GeoNetAgent/.cursor/api-contracts.md +++ b/GeoNetAgent/.cursor/api-contracts.md @@ -51,16 +51,27 @@ Content-Type: application/json "os_build": "10.0.26200 (26200)", "cpu_model": "Intel(R) Core(TM) i7-4710HQ CPU @ 2.50GHz", "ram_gb": 15.9, - "gpu_model": "Intel(R) HD Graphics 4600", + "gpu_model": "NVIDIA GeForce GTX 850M", "last_boot_at": "2026-06-15T08:42:51+07:00" }, + "gpus": [ + { + "name": "NVIDIA GeForce GTX 850M", + "dedicated_memory_mb": 4054 + }, + { + "name": "Intel(R) HD Graphics 4600", + "dedicated_memory_mb": 128 + } + ], "disks": [ { "drive_letter": "C:", "total_gb": 188.7, "free_gb": 3.1, "pct_used": 98.3, - "file_system": "NTFS" + "file_system": "NTFS", + "model": "ADATA SU650" } ], "memory_modules": [ diff --git a/GeoNetAgent/.cursor/db-schema.md b/GeoNetAgent/.cursor/db-schema.md index a64e023..2f08c92 100644 --- a/GeoNetAgent/.cursor/db-schema.md +++ b/GeoNetAgent/.cursor/db-schema.md @@ -187,9 +187,14 @@ Normalized dari snapshot — memudahkan query "disk > 85%". | free_gb | numeric(10,2) | NO | | | pct_used | numeric(5,2) | NO | generated or computed | | file_system | varchar(20) | YES | NTFS | +| model | varchar(100) | YES | physical drive model (WMI), e.g. ADATA SU650 | **Index:** `idx_disks_snapshot` +**Migration:** `002_disk_model.sql` menambah kolom `model`. + +**Catatan GPU:** array `gpus[]` disimpan di `asset_snapshots.payload` (belum dinormalisasi ke tabel terpisah). + --- ### `asset_network_adapters` diff --git a/GeoNetAgent/.cursor/progress.md b/GeoNetAgent/.cursor/progress.md index b03ee13..d59d698 100644 --- a/GeoNetAgent/.cursor/progress.md +++ b/GeoNetAgent/.cursor/progress.md @@ -1,120 +1,239 @@ -# GeoNetAgent — Progress & Monitoring Fase - -> **Update file ini** setiap milestone selesai. Ini sumber tunggal untuk cek *kita ada di fase mana*. - -**Terakhir diupdate:** 2026-06-17 (web install agent · domain gisportal.id · reverse proxy) - ---- - -## Snapshot (baca cepat) - -| Item | Status | -|------|--------| -| **Fase aktif** | **Fase 1 — Foundation** 🔄 | -| Fase berikutnya | Laravel asset index · auth IT | -| Agent POC | `v0.1.6-phase0` — collect + POST + web install | -| Collector | `https://collector.gisportal.id` UI · `agent.gisportal.id` POST · `:8000` HTTP LAN | -| Monorepo | Partial — `agent/` + `collector/` + `samples/` + `infra/` ✅ · `portal/` ❌ | -| Blocker utama | Laravel portal belum scaffold | - -### Progress ringkas - -| Fase | Status | Progress | -|------|--------|----------| -| 0 — Blueprint | ✅ Selesai | 8 / 8 | -| 1 — Foundation | 🔄 Aktif | 7 / 9 | -| 2 — Integrasi monitoring | ⏳ | 0 / 2 | -| 3 — Infra sync & CMDB | ⏳ | 0 / 3 | -| 4 — Scale & intelligence | ⏳ | 1 / 3 | - ---- - -## Fase 0 — Blueprint ✅ **(SELESAI)** - -**DoD:** Dokumentasi lengkap, blueprint disetujui user, struktur monorepo ter-scaffold. - -- [x] Dokumentasi `.cursor/` lengkap (blueprint, db, API, repo, monitoring, playbook) -- [x] `server.md` + SSH key-only diverifikasi -- [x] Rule `project-context.mdc` (server, Dockge, QNAP Object Storage) -- [x] Scaffold partial — folder `agent/` + `samples/agent-reports/` -- [x] Uji agent collect-only di laptop dev (`DESKTOP-RRJ9G01`) -- [ ] Review & approval user terhadap blueprint -- [ ] Scaffold monorepo penuh (`portal/`) -- [x] Pindahkan sample DXDiag ke `samples/dxdiag/` - -### Sub-task: Agent POC - -- [x] `Collect-GeoNetReport.ps1` + modul `lib/` -- [x] Payload: system, disk, memory, network, health, peripherals, monitor -- [x] `Send-GeoNetReport.ps1` — E2E POST ke collector -- [x] Web install `https://agent.gisportal.id/install/` — pilot `DESKTOP-RRJ9G01` -- [ ] Uji collect di laptop kedua (`BKK-MINIPC`) - ---- - -## Fase 1 — Foundation 🔄 **(AKTIF)** - -**DoD:** Agent di 1 laptop test POST → data di DB → Laravel tampilkan halaman asset list. - -- [x] PostgreSQL `geonetagent_dev` + SQL migration `001_initial_schema` di main-db -- [x] FastAPI `/api/v1/agent/report` + `/health` + read API assets -- [x] PowerShell agent v0.1.6 + kirim ke collector (E2E verified) -- [x] UI uji read-only — `https://collector.gisportal.id/ui/` · `http://10.100.1.24:8000/ui/` -- [x] Nginx reverse proxy — POST agent `agent.gisportal.id` · HTTPS UI `collector.gisportal.id` -- [x] DNS publik + split-DNS LAN `agent.gisportal.id` / `collector.gisportal.id` -- [x] Firewalld port `8000` untuk subnet internal di `.24` -- [ ] Laravel asset index (read-only) -- [ ] Auth IT user lokal (M9) - ---- - -## Fase 2 — Integrasi monitoring ⏳ - -**DoD:** Alert Zabbix di portal; status host dari Zabbix API. - -- [ ] Zabbix webhook → Laravel -- [ ] Zabbix host status cache di dashboard - ---- - -## Fase 3 — Infra sync & CMDB ⏳ - -**DoD:** VM Proxmox otomatis di CMDB; license manual bisa di-assign. - -- [ ] Proxmox sync job (FastAPI) -- [ ] License module (Laravel) -- [ ] User ↔ device assignment UI - ---- - -## Fase 4 — Scale & intelligence ⏳ - -- [ ] GPO deploy agent ke semua laptop (web install siap; GPO opsional) -- [x] Web install `https://agent.gisportal.id/install/` + `Install-GeoNetAgent.ps1` -- [ ] Trend & compliance report -- [ ] Ollama assistant (opsional) - ---- - -## Cara update - -1. Centang `[x]` item yang selesai -2. Update **Terakhir diupdate** dan tabel **Snapshot** -3. Hitung ulang kolom **Progress** (item selesai / total item fase) -4. Jika pindah fase: ubah **Fase aktif** di Snapshot + banner `(AKTIF)` pada section fase -5. Sinkronkan baris status singkat di `blueprint.md`, `me.md`, `monitoring-architecture.md`, `project-context.mdc`, `perintah.md` - ---- - -## Riwayat milestone - -| Tanggal | Milestone | -|---------|-----------| -| 2026-06-17 | Dokumentasi `.cursor/` + SSH server verified | -| 2026-06-17 | Agent POC `Collect-GeoNetReport.ps1` v0.1.2 — collect JSON lokal | -| 2026-06-17 | File `progress.md` dibuat — tracking fase terpusat | -| 2026-06-17 | Agent v0.1.4 — `peripherals[]` USB/HID/BT/audio jack | -| 2026-06-17 | Collector redeploy `.24` + UI peripherals di `/ui/` | -| 2026-06-17 | Nginx reverse proxy — domain `gisportal.id` | -| 2026-06-17 | Web install agent + pilot deploy `DESKTOP-RRJ9G01` | - \ No newline at end of file +# GeoNetAgent — Progress & Monitoring Fase + + + +> **Update file ini** setiap milestone selesai. Ini sumber tunggal untuk cek *kita ada di fase mana*. + + + +**Terakhir diupdate:** 2026-06-17 (web install agent · domain gisportal.id · reverse proxy) + + + +--- + + + +## Snapshot (baca cepat) + + + +| Item | Status | + +|------|--------| + +| **Fase aktif** | **Fase 1 — Foundation** 🔄 | + +| Fase berikutnya | Laravel asset index · auth IT | + +| Agent POC | `v0.1.6-phase0` — collect + POST + web install | + +| Collector | `https://collector.gisportal.id` UI · `agent.gisportal.id` POST · `:8000` HTTP LAN | + +| Monorepo | Partial — `agent/` + `collector/` + `samples/` + `infra/` ✅ · `portal/` ❌ | + +| Blocker utama | Laravel portal belum scaffold | + + + +### Progress ringkas + + + +| Fase | Status | Progress | + +|------|--------|----------| + +| 0 — Blueprint | ✅ Selesai | 8 / 8 | + +| 1 — Foundation | 🔄 Aktif | 7 / 9 | + +| 2 — Integrasi monitoring | ⏳ | 0 / 2 | + +| 3 — Infra sync & CMDB | ⏳ | 0 / 3 | + +| 4 — Scale & intelligence | ⏳ | 1 / 3 | + + + +--- + + + +## Fase 0 — Blueprint ✅ **(SELESAI)** + + + +**DoD:** Dokumentasi lengkap, blueprint disetujui user, struktur monorepo ter-scaffold. + + + +- [x] Dokumentasi `.cursor/` lengkap (blueprint, db, API, repo, monitoring, playbook) + +- [x] `server.md` + SSH key-only diverifikasi + +- [x] Rule `project-context.mdc` (server, Dockge, QNAP Object Storage) + +- [x] Scaffold partial — folder `agent/` + `samples/agent-reports/` + +- [x] Uji agent collect-only di laptop dev (`DESKTOP-RRJ9G01`) + +- [ ] Review & approval user terhadap blueprint + +- [ ] Scaffold monorepo penuh (`portal/`) + +- [x] Pindahkan sample DXDiag ke `samples/dxdiag/` + + + +### Sub-task: Agent POC + + + +- [x] `Collect-GeoNetReport.ps1` + modul `lib/` + +- [x] Payload: system, disk, memory, network, health, peripherals, monitor + +- [x] `Send-GeoNetReport.ps1` — E2E POST ke collector + +- [x] Web install `https://agent.gisportal.id/install/` — pilot `DESKTOP-RRJ9G01` + +- [ ] Uji collect di laptop kedua (`BKK-MINIPC`) + + + +--- + + + +## Fase 1 — Foundation 🔄 **(AKTIF)** + + + +**DoD:** Agent di 1 laptop test POST → data di DB → Laravel tampilkan halaman asset list. + + + +- [x] PostgreSQL `geonetagent_dev` + SQL migration `001_initial_schema` di main-db + +- [x] FastAPI `/api/v1/agent/report` + `/health` + read API assets + +- [x] PowerShell agent v0.1.6 + kirim ke collector (E2E verified) + +- [x] UI uji read-only — `https://collector.gisportal.id/ui/` · `http://10.100.1.24:8000/ui/` + +- [x] Nginx reverse proxy — POST agent `agent.gisportal.id` · HTTPS UI `collector.gisportal.id` + +- [x] DNS publik + split-DNS LAN `agent.gisportal.id` / `collector.gisportal.id` + +- [x] Firewalld port `8000` untuk subnet internal di `.24` + +- [ ] Laravel asset index (read-only) + +- [ ] Auth IT user lokal (M9) + + + +--- + + + +## Fase 2 — Integrasi monitoring ⏳ + + + +**DoD:** Alert Zabbix di portal; status host dari Zabbix API. + + + +- [ ] Zabbix webhook → Laravel + +- [ ] Zabbix host status cache di dashboard + + + +--- + + + +## Fase 3 — Infra sync & CMDB ⏳ + + + +**DoD:** VM Proxmox otomatis di CMDB; license manual bisa di-assign. + + + +- [ ] Proxmox sync job (FastAPI) + +- [ ] License module (Laravel) + +- [ ] User ↔ device assignment UI + + + +--- + + + +## Fase 4 — Scale & intelligence ⏳ + + + +- [ ] GPO deploy agent ke semua laptop (web install siap; GPO opsional) + +- [x] Web install `https://agent.gisportal.id/install/` + `Install-GeoNetAgent.ps1` + +- [ ] Trend & compliance report + +- [ ] Ollama assistant (opsional) + + + +--- + + + +## Cara update + + + +1. Centang `[x]` item yang selesai + +2. Update **Terakhir diupdate** dan tabel **Snapshot** + +3. Hitung ulang kolom **Progress** (item selesai / total item fase) + +4. Jika pindah fase: ubah **Fase aktif** di Snapshot + banner `(AKTIF)` pada section fase + +5. Sinkronkan baris status singkat di `blueprint.md`, `me.md`, `monitoring-architecture.md`, `project-context.mdc`, `perintah.md` + + + +--- + + + +## Riwayat milestone + + + +| Tanggal | Milestone | + +|---------|-----------| + +| 2026-06-17 | Dokumentasi `.cursor/` + SSH server verified | + +| 2026-06-17 | Agent POC `Collect-GeoNetReport.ps1` v0.1.2 — collect JSON lokal | + +| 2026-06-17 | File `progress.md` dibuat — tracking fase terpusat | + +| 2026-06-17 | Agent v0.1.4 — `peripherals[]` USB/HID/BT/audio jack | + +| 2026-06-17 | Collector redeploy `.24` + UI peripherals di `/ui/` | + +| 2026-06-17 | Nginx reverse proxy — domain `gisportal.id` | + +| 2026-06-17 | Web install agent + pilot deploy `DESKTOP-RRJ9G01` | + + diff --git a/GeoNetAgent/agent/Collect-GeoNetReport.ps1 b/GeoNetAgent/agent/Collect-GeoNetReport.ps1 index a9582eb..cd25fb3 100644 --- a/GeoNetAgent/agent/Collect-GeoNetReport.ps1 +++ b/GeoNetAgent/agent/Collect-GeoNetReport.ps1 @@ -24,7 +24,7 @@ param( ) $ErrorActionPreference = 'Stop' -$AgentVersion = '0.1.6-phase0' +$AgentVersion = '0.1.8-phase0' $SchemaVersion = '1.0' $scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path @@ -33,6 +33,7 @@ $libPath = Join-Path $scriptRoot 'lib' . (Join-Path $libPath 'Format-IsoDateTime.ps1') . (Join-Path $libPath 'Format-Text.ps1') . (Join-Path $libPath 'Get-SystemInfo.ps1') +. (Join-Path $libPath 'Get-GpuInfo.ps1') . (Join-Path $libPath 'Get-DiskInfo.ps1') . (Join-Path $libPath 'Get-MemoryInfo.ps1') . (Join-Path $libPath 'Get-NetworkInfo.ps1') @@ -99,7 +100,22 @@ $payload = [ordered]@{ gpu_model = (Convert-GeoNetOptionalString -Value $sys.GpuModel) last_boot_at = $sys.LastBootAt } - disks = $disks + gpus = @($sys.Gpus | ForEach-Object { + [ordered]@{ + name = $_.name + dedicated_memory_mb = $_.dedicated_memory_mb + } + }) + disks = @($disks | ForEach-Object { + [ordered]@{ + drive_letter = $_.drive_letter + total_gb = $_.total_gb + free_gb = $_.free_gb + pct_used = $_.pct_used + file_system = $_.file_system + model = $_.model + } + }) memory_modules = @($memory.Modules | ForEach-Object { [ordered]@{ slot = $_.slot diff --git a/GeoNetAgent/agent/Install-GeoNetAgent.ps1 b/GeoNetAgent/agent/Install-GeoNetAgent.ps1 index c52ace5..eb0c5cf 100644 --- a/GeoNetAgent/agent/Install-GeoNetAgent.ps1 +++ b/GeoNetAgent/agent/Install-GeoNetAgent.ps1 @@ -8,177 +8,18 @@ param( [string]$SourcePath, [switch]$SkipScheduledTask, [switch]$SkipInitialRun, - [int]$ScheduleHours = 6 + [int]$ScheduleHours = 3 ) $ErrorActionPreference = 'Stop' - -function Test-GeoNetAdmin { - $id = [Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object Security.Principal.WindowsPrincipal($id) - return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -} - -function Get-GeoNetInstallBaseUrl { - param([string]$Url) - return $Url.TrimEnd('/') -} - -function Get-GeoNetRemoteJson { - param([string]$Url) - return (Invoke-WebRequest -Uri $Url -UseBasicParsing).Content | ConvertFrom-Json -} - -function Get-GeoNetRemoteFile { - param([string]$Url, [string]$Destination) - $parent = Split-Path -Parent $Destination - if (-not (Test-Path $parent)) { - New-Item -ItemType Directory -Path $parent -Force | Out-Null - } - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - $lastErr = $null - for ($try = 1; $try -le 3; $try++) { - try { - Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing -TimeoutSec 90 - if ((Get-Item $Destination).Length -gt 0) { return } - } - catch { - $lastErr = $_ - Start-Sleep -Seconds 2 - } - } - if (Get-Command curl.exe -ErrorAction SilentlyContinue) { - & curl.exe -fsSL --tlsv1.2 $Url -o $Destination - if ($LASTEXITCODE -eq 0 -and (Test-Path $Destination)) { return } - } - throw $lastErr +$core = Join-Path $PSScriptRoot 'lib\Agent-InstallCore.ps1' +if (-not (Test-Path $core)) { + $core = Join-Path $InstallRoot 'lib\Agent-InstallCore.ps1' } - -function Copy-GeoNetAgentPayload { - param( - [string]$SourceRoot, - [string]$DestinationRoot, - [object]$Manifest - ) - foreach ($rel in $Manifest.files) { - $src = Join-Path $SourceRoot ($rel -replace '/', [IO.Path]::DirectorySeparatorChar) - $dst = Join-Path $DestinationRoot ($rel -replace '/', [IO.Path]::DirectorySeparatorChar) - if (-not (Test-Path $src)) { - throw "File sumber tidak ada: $src" - } - $dir = Split-Path -Parent $dst - if (-not (Test-Path $dir)) { - New-Item -ItemType Directory -Path $dir -Force | Out-Null - } - Copy-Item -Path $src -Destination $dst -Force - } -} - -function Install-GeoNetAgentPayload { - param( - [string]$Base, - [string]$DestinationRoot, - [object]$Manifest - ) - foreach ($rel in $Manifest.files) { - $url = "$Base/$($rel -replace '\\', '/')" - $dst = Join-Path $DestinationRoot ($rel -replace '/', [IO.Path]::DirectorySeparatorChar) - Write-Host " -> $rel" - Get-GeoNetRemoteFile -Url $url -Destination $dst - } -} - -function Set-GeoNetAgentConfig { - param( - [string]$InstallRoot, - [string]$Base, - [string]$Token, - [bool]$UseRemoteConfig - ) - $configPath = Join-Path $InstallRoot 'config.json' - - if ($Token) { - $cfg = [ordered]@{ - collector_url = 'https://agent.gisportal.id/api/v1/agent/report' - agent_token = $Token - include_software = $true - max_software = 500 - timeout_seconds = 120 - } - if (Test-Path $configPath) { - try { - $existing = Get-Content $configPath -Raw | ConvertFrom-Json - if ($existing.collector_url) { $cfg.collector_url = $existing.collector_url } - if ($null -ne $existing.include_software) { $cfg.include_software = [bool]$existing.include_software } - if ($existing.max_software) { $cfg.max_software = [int]$existing.max_software } - if ($existing.timeout_seconds) { $cfg.timeout_seconds = [int]$existing.timeout_seconds } - } - catch { } - } - $cfg | ConvertTo-Json | Set-Content -Path $configPath -Encoding UTF8 - return - } - - if ((Test-Path $configPath) -and -not $UseRemoteConfig) { - Write-Host 'config.json lokal dipertahankan.' -ForegroundColor DarkGray - return - } - - if ($UseRemoteConfig) { - Get-GeoNetRemoteFile -Url "$Base/config.json" -Destination $configPath - return - } - - throw 'config.json tidak ada. Sediakan -AgentToken atau config.json di URL install.' -} - -function Invoke-GeoNetSchtasks { - param( - [Parameter(Mandatory)][string[]]$ArgumentList, - [switch]$IgnoreError - ) - $prev = $ErrorActionPreference - $ErrorActionPreference = 'SilentlyContinue' - $output = & schtasks.exe @ArgumentList 2>&1 - $code = $LASTEXITCODE - $ErrorActionPreference = $prev - if ($code -ne 0 -and -not $IgnoreError) { - $msg = ($output | Out-String).Trim() - if ($msg) { throw $msg } - throw "schtasks gagal (exit $code): schtasks.exe $($ArgumentList -join ' ')" - } - return $code -} - -function Register-GeoNetAgentTask { - param( - [string]$InstallRoot, - [int]$Hours - ) - $userScript = Join-Path $InstallRoot 'UserAgent.ps1' - if (-not (Test-Path $userScript)) { - throw "UserAgent.ps1 tidak ditemukan di $InstallRoot" - } - - $taskArgs = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$userScript`"" - $taskCmd = "powershell.exe $taskArgs" - $taskNames = @('GeoNetAgent-Report', 'GeoNetAgent-Report-Startup') - - foreach ($name in $taskNames) { - Unregister-ScheduledTask -TaskName $name -Confirm:$false -ErrorAction SilentlyContinue - Invoke-GeoNetSchtasks -ArgumentList @('/Delete', '/TN', $name, '/F') -IgnoreError | Out-Null - } - - Invoke-GeoNetSchtasks -ArgumentList @( - '/Create', '/TN', 'GeoNetAgent-Report-Startup', '/TR', $taskCmd, - '/SC', 'ONSTART', '/RU', 'SYSTEM', '/RL', 'HIGHEST', '/F' - ) | Out-Null - - Invoke-GeoNetSchtasks -ArgumentList @( - '/Create', '/TN', 'GeoNetAgent-Report', '/TR', $taskCmd, - '/SC', 'HOURLY', '/MO', "$Hours", '/RU', 'SYSTEM', '/RL', 'HIGHEST', '/F' - ) | Out-Null +if (-not (Test-Path $core)) { + throw "Agent-InstallCore.ps1 tidak ditemukan (cari di $PSScriptRoot\lib dan $InstallRoot\lib)." } +. $core $base = Get-GeoNetInstallBaseUrl -Url $BaseUrl Write-Host "GeoNetAgent install/update" -ForegroundColor Cyan @@ -216,7 +57,7 @@ else { Set-GeoNetAgentConfig -InstallRoot $InstallRoot -Base $base -Token $AgentToken -UseRemoteConfig:$remoteConfig } -$manifest.version | Set-Content -Path (Join-Path $InstallRoot 'VERSION') -Encoding ASCII -NoNewline +Set-GeoNetLocalAgentVersion -InstallRoot $InstallRoot -Version $manifest.version if (-not $SkipScheduledTask) { if (-not (Test-GeoNetAdmin)) { diff --git a/GeoNetAgent/agent/README.md b/GeoNetAgent/agent/README.md index e94421f..9631bbd 100644 --- a/GeoNetAgent/agent/README.md +++ b/GeoNetAgent/agent/README.md @@ -30,6 +30,12 @@ Atau hanya pasang/update + task (tanpa laporan): Perintah yang sama untuk **update** agent. `config.json` lokal di `%ProgramData%\GeoNetAgent` dipertahankan. +Setelah terpasang, **auto-update**: Scheduled Task (SYSTEM) membandingkan `manifest.json` server vs file `VERSION` lokal sebelum setiap laporan. Jika versi server lebih baru, file agent diunduh ulang tanpa interaksi user. + +## Rollout massal (GPO) + +Skrip startup siap pakai: **`agent/gpo/`** — lihat [gpo/README.md](./gpo/README.md). + ## IT — publish paket ke server ```bash diff --git a/GeoNetAgent/agent/Send-GeoNetReport.ps1 b/GeoNetAgent/agent/Send-GeoNetReport.ps1 index 12c7616..6e4c65c 100644 --- a/GeoNetAgent/agent/Send-GeoNetReport.ps1 +++ b/GeoNetAgent/agent/Send-GeoNetReport.ps1 @@ -22,6 +22,7 @@ $libPath = Join-Path $scriptRoot 'lib' . (Join-Path $libPath 'Format-IsoDateTime.ps1') . (Join-Path $libPath 'Format-Text.ps1') . (Join-Path $libPath 'Get-SystemInfo.ps1') +. (Join-Path $libPath 'Get-GpuInfo.ps1') . (Join-Path $libPath 'Get-DiskInfo.ps1') . (Join-Path $libPath 'Get-MemoryInfo.ps1') . (Join-Path $libPath 'Get-NetworkInfo.ps1') @@ -54,7 +55,7 @@ if (-not $CollectorUrl -or -not $AgentToken) { } # Build payload (same as Collect-GeoNetReport) -$AgentVersion = '0.1.6-phase0' +$AgentVersion = '0.1.8-phase0' $SchemaVersion = '1.0' $sys = Get-GeoNetSystemInfo @@ -102,6 +103,12 @@ $payload = [ordered]@{ gpu_model = (Convert-GeoNetOptionalString -Value $sys.GpuModel) last_boot_at = $sys.LastBootAt } + gpus = @($sys.Gpus | ForEach-Object { + [ordered]@{ + name = $_.name + dedicated_memory_mb = $_.dedicated_memory_mb + } + }) disks = $disks memory_modules = @($memory.Modules) memory_summary = $memory.Summary diff --git a/GeoNetAgent/agent/UserAgent.ps1 b/GeoNetAgent/agent/UserAgent.ps1 index 43371dc..5466f3a 100644 --- a/GeoNetAgent/agent/UserAgent.ps1 +++ b/GeoNetAgent/agent/UserAgent.ps1 @@ -1,41 +1,50 @@ <# .SYNOPSIS - Entry point agent GeoNetAgent untuk user - sekali run / double-click. + Entry point agent GeoNetAgent — auto-update (opsional) + kirim laporan. .DESCRIPTION - Jalankan dari share IT, mis.: - \\10.100.1.10\software installer\GeonetAgent\agent\UserAgent.ps1 - - Membaca config global config.json di folder yang sama (collector URL + token). - Tidak perlu config per laptop - hostname dan identitas diambil otomatis dari WMI. + Dipanggil Scheduled Task (SYSTEM) atau manual. + Sebelum kirim laporan: cek manifest.json vs VERSION lokal; update silent jika beda. + config.json lokal dipertahankan saat auto-update. .EXAMPLE - powershell -NoProfile -ExecutionPolicy Bypass -File "\\10.100.1.10\software installer\GeonetAgent\agent\UserAgent.ps1" + powershell -NoProfile -ExecutionPolicy Bypass -File "$env:ProgramData\GeoNetAgent\UserAgent.ps1" #> [CmdletBinding()] param( - [switch]$IncludeSoftware + [switch]$IncludeSoftware, + [switch]$SkipAutoUpdate ) $ErrorActionPreference = 'Stop' $agentRoot = Split-Path -Parent $MyInvocation.MyCommand.Path -$sendScript = Join-Path $agentRoot 'Send-GeoNetReport.ps1' $configPath = Join-Path $agentRoot 'config.json' +$updateScript = Join-Path $agentRoot 'lib\Update-GeoNetAgent.ps1' +$sendScript = Join-Path $agentRoot 'Send-GeoNetReport.ps1' if (-not (Test-Path $configPath)) { $shareTemplate = Join-Path $agentRoot 'config.share.json' if (Test-Path $shareTemplate) { - Write-Warning 'config.json tidak ada - memakai config.share.json. Di share QNAP rename ke config.json.' + Write-Warning 'config.json tidak ada - memakai config.share.json.' $configPath = $shareTemplate } } +if (-not $SkipAutoUpdate -and (Test-Path $updateScript)) { + . $updateScript + $null = Update-GeoNetAgentIfNeeded ` + -InstallRoot $agentRoot ` + -ConfigPath $configPath ` + -ScheduleHours 3 ` + -Quiet +} + if (-not (Test-Path $sendScript)) { throw "Send-GeoNetReport.ps1 tidak ditemukan di: $agentRoot" } if (-not (Test-Path $configPath)) { - throw "config.json tidak ditemukan di: $agentRoot. Copy config.share.json ke config.json di share IT." + throw "config.json tidak ditemukan di: $agentRoot" } $params = @{ diff --git a/GeoNetAgent/agent/VERSION b/GeoNetAgent/agent/VERSION index 6f1f9c0..cd512a6 100644 --- a/GeoNetAgent/agent/VERSION +++ b/GeoNetAgent/agent/VERSION @@ -1 +1 @@ -0.1.6-phase0 +0.1.8-phase0 diff --git a/GeoNetAgent/agent/gpo/GeoNetAgent-GPO-Startup.cmd b/GeoNetAgent/agent/gpo/GeoNetAgent-GPO-Startup.cmd new file mode 100644 index 0000000..292ab72 --- /dev/null +++ b/GeoNetAgent/agent/gpo/GeoNetAgent-GPO-Startup.cmd @@ -0,0 +1,6 @@ +@echo off +REM GeoNetAgent — GPO Computer Startup Script (wrapper) +REM Deploy folder ini ke SYSVOL / NETLOGON, lalu tautkan .cmd ini di GPO Startup. + +powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "%~dp0GeoNetAgent-GPO-Startup.ps1" +exit /b 0 diff --git a/GeoNetAgent/agent/gpo/GeoNetAgent-GPO-Startup.ps1 b/GeoNetAgent/agent/gpo/GeoNetAgent-GPO-Startup.ps1 new file mode 100644 index 0000000..aefa6b3 --- /dev/null +++ b/GeoNetAgent/agent/gpo/GeoNetAgent-GPO-Startup.ps1 @@ -0,0 +1,185 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + GPO Startup Script — pasang atau update GeoNetAgent (silent, SYSTEM). + +.DESCRIPTION + Dipanggil dari Group Policy (Computer Configuration → Startup Scripts). + - Belum terpasang → install dari https://agent.gisportal.id/install/ + - Sudah terpasang → cek manifest vs VERSION, update jika perlu + - Memastikan Scheduled Task ada (startup + tiap 3 jam) + - Tidak kirim laporan langsung (-SkipInitialRun); task SYSTEM yang kirim + +.PARAMETER BaseUrl + URL paket install. Default: https://agent.gisportal.id/install + +.PARAMETER ScheduleHours + Interval laporan terjadwal. Default: 3 + +.PARAMETER MaxNetworkWaitSeconds + Tunggu jaringan siap sebelum unduh (boot awal). Default: 120 + +.NOTES + Log: %ProgramData%\GeoNetAgent\logs\gpo-startup.log + Exit selalu 0 agar GPO tidak mengulang agresif; cek log jika perlu. +#> +[CmdletBinding()] +param( + [string]$BaseUrl = 'https://agent.gisportal.id/install', + [int]$ScheduleHours = 3, + [int]$MaxNetworkWaitSeconds = 120 +) + +$ErrorActionPreference = 'Continue' +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +$InstallRoot = Join-Path $env:ProgramData 'GeoNetAgent' +$LogDir = Join-Path $InstallRoot 'logs' +$LogFile = Join-Path $LogDir 'gpo-startup.log' + +function Write-GeoNetGpoLog { + param([string]$Message) + $line = '{0} {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message + try { + if (-not (Test-Path $LogDir)) { + New-Item -ItemType Directory -Path $LogDir -Force | Out-Null + } + Add-Content -Path $LogFile -Value $line -Encoding UTF8 + } + catch { } +} + +function Wait-GeoNetNetwork { + param( + [string]$Url, + [int]$MaxSeconds + ) + $base = $Url.TrimEnd('/') + $probe = "$base/manifest.json" + $deadline = (Get-Date).AddSeconds($MaxSeconds) + while ((Get-Date) -lt $deadline) { + try { + $null = Invoke-WebRequest -Uri $probe -Method Head -UseBasicParsing -TimeoutSec 8 + return $true + } + catch { + Start-Sleep -Seconds 5 + } + } + return $false +} + +function Save-GeoNetRemoteFile { + param([string]$Url, [string]$Destination) + $parent = Split-Path -Parent $Destination + if (-not (Test-Path $parent)) { + New-Item -ItemType Directory -Path $parent -Force | Out-Null + } + try { + Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing -TimeoutSec 90 + if ((Get-Item $Destination).Length -gt 0) { return } + } + catch { + if (Get-Command curl.exe -ErrorAction SilentlyContinue) { + & curl.exe -fsSL --tlsv1.2 $Url -o $Destination + if ($LASTEXITCODE -eq 0 -and (Test-Path $Destination)) { return } + } + throw + } +} + +function Invoke-GeoNetGpoWebInstall { + param([string]$Url) + $base = $Url.TrimEnd('/') + $stage = Join-Path $env:TEMP 'GeoNetAgent-gpo-install' + $installer = Join-Path $stage 'Install-GeoNetAgent.ps1' + $coreLib = Join-Path $stage 'lib\Agent-InstallCore.ps1' + + Write-GeoNetGpoLog "Unduh installer dari $base" + Save-GeoNetRemoteFile -Url "$base/Install-GeoNetAgent.ps1" -Destination $installer + Save-GeoNetRemoteFile -Url "$base/lib/Agent-InstallCore.ps1" -Destination $coreLib + + Write-GeoNetGpoLog 'Jalankan Install-GeoNetAgent.ps1 -SkipInitialRun' + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installer ` + -BaseUrl $base -InstallRoot $InstallRoot -SkipInitialRun -ScheduleHours $ScheduleHours + if ($LASTEXITCODE -ne 0) { + throw "Install-GeoNetAgent exit code $LASTEXITCODE" + } +} + +function Test-GeoNetScheduledTasks { + $names = @('GeoNetAgent-Report', 'GeoNetAgent-Report-Startup') + foreach ($name in $names) { + $null = & schtasks.exe /Query /TN $name /FO LIST 2>$null + if ($LASTEXITCODE -ne 0) { return $false } + } + return $true +} + +function Repair-GeoNetScheduledTasks { + param([string]$Root, [int]$Hours) + $core = Join-Path $Root 'lib\Agent-InstallCore.ps1' + if (-not (Test-Path $core)) { + throw "Agent-InstallCore.ps1 tidak ada di $Root" + } + . $core + Register-GeoNetAgentTask -InstallRoot $Root -Hours $Hours + Write-GeoNetGpoLog "Scheduled Task diperbaiki (interval ${Hours} jam)" +} + +Write-GeoNetGpoLog '=== GPO startup begin ===' + +try { + if (-not (Wait-GeoNetNetwork -Url $BaseUrl -MaxSeconds $MaxNetworkWaitSeconds)) { + throw "Jaringan/install URL tidak reachable dalam ${MaxNetworkWaitSeconds}s" + } + + $userAgent = Join-Path $InstallRoot 'UserAgent.ps1' + $updateScript = Join-Path $InstallRoot 'lib\Update-GeoNetAgent.ps1' + + if (-not (Test-Path $userAgent)) { + Write-GeoNetGpoLog 'Agent belum terpasang — install baru' + Invoke-GeoNetGpoWebInstall -Url $BaseUrl + } + elseif (-not (Test-Path $updateScript)) { + Write-GeoNetGpoLog 'Agent lama (tanpa auto-update) — reinstall' + Invoke-GeoNetGpoWebInstall -Url $BaseUrl + } + else { + Write-GeoNetGpoLog 'Agent ada — cek update' + . $updateScript + $result = Update-GeoNetAgentIfNeeded ` + -InstallRoot $InstallRoot ` + -BaseUrl $BaseUrl ` + -ConfigPath (Join-Path $InstallRoot 'config.json') ` + -ScheduleHours $ScheduleHours ` + -Quiet + if ($result.Updated) { + Write-GeoNetGpoLog "Updated: $($result.From) -> $($result.To)" + } + else { + Write-GeoNetGpoLog "Versi OK: $($result.From)" + if ($result.Error) { + Write-GeoNetGpoLog "Update check warning: $($result.Error)" + } + } + } + + if (-not (Test-GeoNetScheduledTasks)) { + Write-GeoNetGpoLog 'Scheduled Task hilang — perbaiki' + Repair-GeoNetScheduledTasks -Root $InstallRoot -Hours $ScheduleHours + } + + $version = $null + $versionPath = Join-Path $InstallRoot 'VERSION' + if (Test-Path $versionPath) { + $version = (Get-Content $versionPath -Raw).Trim() + } + Write-GeoNetGpoLog "Selesai OK. VERSION=$version" +} +catch { + Write-GeoNetGpoLog "ERROR: $($_.Exception.Message)" +} + +Write-GeoNetGpoLog '=== GPO startup end ===' +exit 0 diff --git a/GeoNetAgent/agent/gpo/README.md b/GeoNetAgent/agent/gpo/README.md new file mode 100644 index 0000000..12649f9 --- /dev/null +++ b/GeoNetAgent/agent/gpo/README.md @@ -0,0 +1,146 @@ +# GeoNetAgent — Rollout massal via GPO + +Skrip startup untuk **Computer Configuration** Active Directory. Jalan sebagai **SYSTEM** saat boot — tidak perlu user Admin login. + +## Isi folder + +| File | Fungsi | +|------|--------| +| `GeoNetAgent-GPO-Startup.cmd` | Entry point untuk GPO (tambahkan file ini di policy) | +| `GeoNetAgent-GPO-Startup.ps1` | Logika install / update / perbaiki task | +| `README.md` | Panduan ini | + +## Prasyarat server + +1. Agent v0.1.8+ sudah di-publish: `bash infra/collector/publish_agent_install.sh` +2. `https://agent.gisportal.id/install/manifest.json` bisa diakses dari jaringan user +3. Migration DB `002_disk_model.sql` sudah dijalankan (jika pakai fitur model disk) + +## Langkah 1 — Salin ke SYSVOL + +Salin seluruh folder `agent/gpo/` ke share domain, contoh: + +``` +\\gisportal.id\SYSVOL\gisportal.id\scripts\GeoNetAgent\ +``` + +Atau (klassik): + +``` +\\\NETLOGON\GeoNetAgent\ +``` + +Pastikan **Computer Account** domain bisa **Read** folder tersebut. + +## Langkah 2 — Buat / edit GPO + +1. Buka **Group Policy Management** di DC (`AD 2012 R2` OK) +2. Buat GPO baru, mis. `GeoNetAgent — Deploy Endpoint` +3. Edit GPO → **Computer Configuration** → **Policies** → **Windows Settings** → **Scripts (Startup/Shutdown)** +4. Tab **Startup** → **Add** → pilih: + + ``` + \\gisportal.id\SYSVOL\gisportal.id\scripts\GeoNetAgent\GeoNetAgent-GPO-Startup.cmd + ``` + +5. **OK** → tutup editor GPO + +## Langkah 3 — Link ke OU + +- Link GPO ke OU yang berisi **laptop/PC Windows** user +- **Jangan** link ke OU server jika server tidak perlu agent +- Urutan: taruh GPO di level OU yang tepat; hindari konflik dengan GPO lain yang memblokir script + +## Langkah 4 — Uji (1–2 mesin dulu) + +Pada laptop uji (sudah join domain): + +```cmd +gpupdate /force +shutdown /r /t 0 +``` + +Setelah boot, cek: + +```powershell +Get-Content "$env:ProgramData\GeoNetAgent\VERSION" +Get-Content "$env:ProgramData\GeoNetAgent\logs\gpo-startup.log" -Tail 20 +schtasks /Query /TN GeoNetAgent-Report /V /FO LIST +Test-Path "$env:ProgramData\GeoNetAgent\UserAgent.ps1" +``` + +Harapan: + +- `VERSION` = versi terbaru di server (mis. `0.1.8-phase0`) +- Log berisi `Selesai OK` +- Task `GeoNetAgent-Report` — HOURLY, interval **3** jam, run as **SYSTEM** + +## Perilaku skrip + +| Kondisi | Aksi | +|---------|------| +| Agent belum ada | Install penuh dari web (silent, `-SkipInitialRun`) | +| Agent lama tanpa modul auto-update | Reinstall | +| Agent ada | Cek `manifest.json` vs `VERSION`, update jika beda | +| Scheduled Task hilang | Daftar ulang (startup + 3 jam) | +| Jaringan belum siap saat boot | Tunggu hingga 120 detik | +| Error | Catat di log, **exit 0** (boot tidak terganggu) | + +Laporan inventaris dikirim oleh Scheduled Task (`UserAgent.ps1`), bukan langsung dari skrip GPO. + +## Log + +``` +%ProgramData%\GeoNetAgent\logs\gpo-startup.log +``` + +Contoh: + +``` +2026-06-18 08:15:02 === GPO startup begin === +2026-06-18 08:15:08 Agent belum terpasang — install baru +2026-06-18 08:15:45 Selesai OK. VERSION=0.1.8-phase0 +2026-06-18 08:15:45 === GPO startup end === +``` + +## Firewall / proxy + +Laptop harus bisa **HTTPS outbound** ke: + +- `https://agent.gisportal.id` (install + POST laporan) + +Split-DNS MikroTik sudah mengarahkan domain ini ke `10.100.1.24` di LAN. + +## Opsi parameter (advanced) + +Edit baris di `.cmd` jika perlu: + +```batch +powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "%~dp0GeoNetAgent-GPO-Startup.ps1" -ScheduleHours 3 -MaxNetworkWaitSeconds 180 +``` + +## Rollback / uninstall + +1. Unlink GPO dari OU +2. Hapus task: + + ```cmd + schtasks /Delete /TN GeoNetAgent-Report /F + schtasks /Delete /TN GeoNetAgent-Report-Startup /F + ``` + +3. Hapus folder: `%ProgramData%\GeoNetAgent` + +## Troubleshooting + +| Gejala | Cek | +|--------|-----| +| Log tidak ada | GPO belum jalan / path script salah / OU salah | +| `Jaringan tidak reachable` | DNS, firewall, proxy; boot terlalu cepat — naikkan `MaxNetworkWaitSeconds` | +| VERSION kosong | Install gagal — buka log, coba web install manual Admin | +| Task tidak ada | Jalankan manual: `Repair` via install Admin sekali | +| GPO tidak apply | `gpresult /H report.html` pada laptop | + +## Alternatif tanpa GPO + +Web install manual (2 baris PS Admin) — lihat `agent/install/index.html` atau `agent/README.md`. diff --git a/GeoNetAgent/agent/install/GeoNetAgent-Install.ps1 b/GeoNetAgent/agent/install/GeoNetAgent-Install.ps1 index aef1525..44d62a1 100644 --- a/GeoNetAgent/agent/install/GeoNetAgent-Install.ps1 +++ b/GeoNetAgent/agent/install/GeoNetAgent-Install.ps1 @@ -6,17 +6,30 @@ param( $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $base = 'https://agent.gisportal.id/install' -$installer = Join-Path $env:TEMP 'Install-GeoNetAgent.ps1' -Write-Host "GeoNetAgent install - $base" -ForegroundColor Cyan -try { - Invoke-WebRequest -Uri "$base/Install-GeoNetAgent.ps1" -OutFile $installer -UseBasicParsing -TimeoutSec 90 -} -catch { - & curl.exe -fsSL --tlsv1.2 "$base/Install-GeoNetAgent.ps1" -o $installer - if ($LASTEXITCODE -ne 0) { throw $_ } +function Save-GeoNetInstallFile { + param([string]$Url, [string]$Destination) + $parent = Split-Path -Parent $Destination + if (-not (Test-Path $parent)) { + New-Item -ItemType Directory -Path $parent -Force | Out-Null + } + try { + Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing -TimeoutSec 90 + } + catch { + & curl.exe -fsSL --tlsv1.2 $Url -o $Destination + if ($LASTEXITCODE -ne 0) { throw $_ } + } } +$stage = Join-Path $env:TEMP 'GeoNetAgent-install' +$installer = Join-Path $stage 'Install-GeoNetAgent.ps1' +$coreLib = Join-Path $stage 'lib\Agent-InstallCore.ps1' + +Write-Host "GeoNetAgent install - $base" -ForegroundColor Cyan +Save-GeoNetInstallFile -Url "$base/Install-GeoNetAgent.ps1" -Destination $installer +Save-GeoNetInstallFile -Url "$base/lib/Agent-InstallCore.ps1" -Destination $coreLib + $params = @{ BaseUrl = $base } if ($SkipInitialRun) { $params['SkipInitialRun'] = $true } & $installer @params diff --git a/GeoNetAgent/agent/install/bootstrap.ps1 b/GeoNetAgent/agent/install/bootstrap.ps1 index 5bca837..e2e8551 100644 --- a/GeoNetAgent/agent/install/bootstrap.ps1 +++ b/GeoNetAgent/agent/install/bootstrap.ps1 @@ -9,17 +9,30 @@ param( $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $base = $BaseUrl.TrimEnd('/') -$installer = Join-Path $env:TEMP 'Install-GeoNetAgent.ps1' -Write-Host "GeoNetAgent bootstrap - $base" -ForegroundColor Cyan -try { - Invoke-WebRequest -Uri "$base/Install-GeoNetAgent.ps1" -OutFile $installer -UseBasicParsing -TimeoutSec 90 -} -catch { - & curl.exe -fsSL --tlsv1.2 "$base/Install-GeoNetAgent.ps1" -o $installer - if ($LASTEXITCODE -ne 0) { throw $_ } +function Save-GeoNetInstallFile { + param([string]$Url, [string]$Destination) + $parent = Split-Path -Parent $Destination + if (-not (Test-Path $parent)) { + New-Item -ItemType Directory -Path $parent -Force | Out-Null + } + try { + Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing -TimeoutSec 90 + } + catch { + & curl.exe -fsSL --tlsv1.2 $Url -o $Destination + if ($LASTEXITCODE -ne 0) { throw $_ } + } } +$stage = Join-Path $env:TEMP 'GeoNetAgent-install' +$installer = Join-Path $stage 'Install-GeoNetAgent.ps1' +$coreLib = Join-Path $stage 'lib\Agent-InstallCore.ps1' + +Write-Host "GeoNetAgent bootstrap - $base" -ForegroundColor Cyan +Save-GeoNetInstallFile -Url "$base/Install-GeoNetAgent.ps1" -Destination $installer +Save-GeoNetInstallFile -Url "$base/lib/Agent-InstallCore.ps1" -Destination $coreLib + $params = @{ BaseUrl = $base } if ($AgentToken) { $params['AgentToken'] = $AgentToken } if ($SkipInitialRun) { $params['SkipInitialRun'] = $true } diff --git a/GeoNetAgent/agent/install/index.html b/GeoNetAgent/agent/install/index.html index e5988ce..2dfddd6 100644 --- a/GeoNetAgent/agent/install/index.html +++ b/GeoNetAgent/agent/install/index.html @@ -36,6 +36,9 @@

Baris 2a Install + kirim laporan pertama:

& "$env:TEMP\GeoNetAgent-Install.ps1"
+

Baris 2a Install + kirim laporan pertama (Optional jika error):

+
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$env:TEMP\GeoNetAgent-Install.ps1"
+

Baris 2b Atau hanya pasang/update file + task (tanpa laporan sekarang):

& "$env:TEMP\GeoNetAgent-Install.ps1" -SkipInitialRun
@@ -56,7 +59,7 @@ schtasks /Query /TN GeoNetAgent-Report-Startup
diff --git a/GeoNetAgent/agent/install/manifest.json b/GeoNetAgent/agent/install/manifest.json index 168dc65..166f85a 100644 --- a/GeoNetAgent/agent/install/manifest.json +++ b/GeoNetAgent/agent/install/manifest.json @@ -1,13 +1,15 @@ { - "version": "0.1.6-phase0", + "version": "0.1.8-phase0", "files": [ - "UserAgent.ps1", "Send-GeoNetReport.ps1", "Collect-GeoNetReport.ps1", + "lib/Agent-InstallCore.ps1", + "lib/Update-GeoNetAgent.ps1", "lib/Format-IsoDateTime.ps1", "lib/Format-Text.ps1", "lib/Get-AssetIdentity.ps1", "lib/Get-DiskInfo.ps1", + "lib/Get-GpuInfo.ps1", "lib/Get-DisplaySettings.ps1", "lib/Get-HealthInfo.ps1", "lib/Get-MemoryInfo.ps1", @@ -15,6 +17,7 @@ "lib/Get-PeripheralsInfo.ps1", "lib/Get-SoftwareInfo.ps1", "lib/Get-SystemInfo.ps1", - "lib/Send-CollectorReport.ps1" + "lib/Send-CollectorReport.ps1", + "UserAgent.ps1" ] } diff --git a/GeoNetAgent/agent/lib/Agent-InstallCore.ps1 b/GeoNetAgent/agent/lib/Agent-InstallCore.ps1 new file mode 100644 index 0000000..ae5a540 --- /dev/null +++ b/GeoNetAgent/agent/lib/Agent-InstallCore.ps1 @@ -0,0 +1,209 @@ +function Get-GeoNetInstallBaseUrl { + param([string]$Url) + return $Url.TrimEnd('/') +} + +function Get-GeoNetRemoteJson { + param([string]$Url) + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + return (Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 60).Content | ConvertFrom-Json +} + +function Get-GeoNetRemoteFile { + param([string]$Url, [string]$Destination) + $parent = Split-Path -Parent $Destination + if (-not (Test-Path $parent)) { + New-Item -ItemType Directory -Path $parent -Force | Out-Null + } + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $lastErr = $null + for ($try = 1; $try -le 3; $try++) { + try { + Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing -TimeoutSec 90 + if ((Get-Item $Destination).Length -gt 0) { return } + } + catch { + $lastErr = $_ + Start-Sleep -Seconds 2 + } + } + if (Get-Command curl.exe -ErrorAction SilentlyContinue) { + & curl.exe -fsSL --tlsv1.2 $Url -o $Destination + if ($LASTEXITCODE -eq 0 -and (Test-Path $Destination)) { return } + } + throw $lastErr +} + +function Copy-GeoNetAgentPayload { + param( + [string]$SourceRoot, + [string]$DestinationRoot, + [object]$Manifest + ) + foreach ($rel in @($Manifest.files)) { + $src = Join-Path $SourceRoot ($rel -replace '/', [IO.Path]::DirectorySeparatorChar) + $dst = Join-Path $DestinationRoot ($rel -replace '/', [IO.Path]::DirectorySeparatorChar) + if (-not (Test-Path $src)) { + throw "File sumber tidak ada: $src" + } + $dir = Split-Path -Parent $dst + if (-not (Test-Path $dir)) { + New-Item -ItemType Directory -Path $dir -Force | Out-Null + } + Copy-Item -Path $src -Destination $dst -Force + } +} + +function Install-GeoNetAgentPayload { + param( + [string]$Base, + [string]$DestinationRoot, + [object]$Manifest, + [switch]$Quiet + ) + foreach ($rel in @(Get-GeoNetManifestFileOrder -Files $Manifest.files)) { + $url = "$Base/$($rel -replace '\\', '/')" + $dst = Join-Path $DestinationRoot ($rel -replace '/', [IO.Path]::DirectorySeparatorChar) + if (-not $Quiet) { Write-Host " -> $rel" } + Get-GeoNetRemoteFile -Url $url -Destination $dst + } +} + +function Get-GeoNetManifestFileOrder { + param([object[]]$Files) + @($Files | Sort-Object { + if ($_ -eq 'UserAgent.ps1') { 'zzz' } else { $_ } + }) +} + +function Get-GeoNetLocalAgentVersion { + param([string]$InstallRoot) + $versionPath = Join-Path $InstallRoot 'VERSION' + if (-not (Test-Path $versionPath)) { return $null } + $text = (Get-Content $versionPath -Raw -ErrorAction SilentlyContinue) + if ([string]::IsNullOrWhiteSpace($text)) { return $null } + return $text.Trim() +} + +function Set-GeoNetLocalAgentVersion { + param( + [string]$InstallRoot, + [string]$Version + ) + $Version.Trim() | Set-Content -Path (Join-Path $InstallRoot 'VERSION') -Encoding ASCII -NoNewline +} + +function Invoke-GeoNetSchtasks { + param( + [Parameter(Mandatory)][string[]]$ArgumentList, + [switch]$IgnoreError + ) + $prev = $ErrorActionPreference + $ErrorActionPreference = 'SilentlyContinue' + $output = & schtasks.exe @ArgumentList 2>&1 + $code = $LASTEXITCODE + $ErrorActionPreference = $prev + if ($code -ne 0 -and -not $IgnoreError) { + $msg = ($output | Out-String).Trim() + if ($msg) { throw $msg } + throw "schtasks gagal (exit $code): schtasks.exe $($ArgumentList -join ' ')" + } + return $code +} + +function Register-GeoNetAgentTask { + param( + [string]$InstallRoot, + [int]$Hours = 3 + ) + $userScript = Join-Path $InstallRoot 'UserAgent.ps1' + if (-not (Test-Path $userScript)) { + throw "UserAgent.ps1 tidak ditemukan di $InstallRoot" + } + + $taskArgs = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$userScript`"" + $taskCmd = "powershell.exe $taskArgs" + $taskNames = @('GeoNetAgent-Report', 'GeoNetAgent-Report-Startup') + + foreach ($name in $taskNames) { + Unregister-ScheduledTask -TaskName $name -Confirm:$false -ErrorAction SilentlyContinue + Invoke-GeoNetSchtasks -ArgumentList @('/Delete', '/TN', $name, '/F') -IgnoreError | Out-Null + } + + Invoke-GeoNetSchtasks -ArgumentList @( + '/Create', '/TN', 'GeoNetAgent-Report-Startup', '/TR', $taskCmd, + '/SC', 'ONSTART', '/RU', 'SYSTEM', '/RL', 'HIGHEST', '/F' + ) | Out-Null + + Invoke-GeoNetSchtasks -ArgumentList @( + '/Create', '/TN', 'GeoNetAgent-Report', '/TR', $taskCmd, + '/SC', 'HOURLY', '/MO', "$Hours", '/RU', 'SYSTEM', '/RL', 'HIGHEST', '/F' + ) | Out-Null +} + +function Set-GeoNetAgentConfig { + param( + [string]$InstallRoot, + [string]$Base, + [string]$Token, + [bool]$UseRemoteConfig + ) + $configPath = Join-Path $InstallRoot 'config.json' + + if ($Token) { + $cfg = [ordered]@{ + collector_url = 'https://agent.gisportal.id/api/v1/agent/report' + agent_token = $Token + include_software = $true + max_software = 500 + timeout_seconds = 120 + } + if (Test-Path $configPath) { + try { + $existing = Get-Content $configPath -Raw | ConvertFrom-Json + if ($existing.collector_url) { $cfg.collector_url = $existing.collector_url } + if ($null -ne $existing.include_software) { $cfg.include_software = [bool]$existing.include_software } + if ($existing.max_software) { $cfg.max_software = [int]$existing.max_software } + if ($existing.timeout_seconds) { $cfg.timeout_seconds = [int]$existing.timeout_seconds } + } + catch { } + } + $cfg | ConvertTo-Json | Set-Content -Path $configPath -Encoding UTF8 + return + } + + if ((Test-Path $configPath) -and -not $UseRemoteConfig) { + return + } + + if ($UseRemoteConfig) { + Get-GeoNetRemoteFile -Url "$Base/config.json" -Destination $configPath + return + } + + throw 'config.json tidak ada. Sediakan -AgentToken atau config.json di URL install.' +} + +function Test-GeoNetAdmin { + $id = [Security.Principal.WindowsIdentity]::GetCurrent() + $p = New-Object Security.Principal.WindowsPrincipal($id) + return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +function Get-GeoNetInstallBaseUrlFromConfig { + param([string]$ConfigPath) + + $default = 'https://agent.gisportal.id/install' + if (-not $ConfigPath -or -not (Test-Path $ConfigPath)) { return $default } + + try { + $cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Json + if ($cfg.install_url) { return Get-GeoNetInstallBaseUrl -Url $cfg.install_url } + if ($cfg.collector_url -match '^(https?://[^/]+)') { + return Get-GeoNetInstallBaseUrl -Url "$($Matches[1])/install" + } + } + catch { } + + return $default +} diff --git a/GeoNetAgent/agent/lib/Get-DiskInfo.ps1 b/GeoNetAgent/agent/lib/Get-DiskInfo.ps1 index a84f31d..a4c3009 100644 --- a/GeoNetAgent/agent/lib/Get-DiskInfo.ps1 +++ b/GeoNetAgent/agent/lib/Get-DiskInfo.ps1 @@ -1,3 +1,29 @@ +$formatText = Join-Path $PSScriptRoot 'Format-Text.ps1' +if (Test-Path $formatText) { . $formatText } + +function Get-GeoNetPhysicalDiskModel { + param([string]$DriveLetter) + + if (-not $DriveLetter) { return $null } + + $model = Invoke-GeoNetSafe { + $logical = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='$DriveLetter'" -ErrorAction Stop + if (-not $logical) { return $null } + + $partitions = @(Get-CimAssociatedInstance -InputObject $logical -Association Win32_LogicalDiskToPartition) + foreach ($partition in $partitions) { + $drives = @(Get-CimAssociatedInstance -InputObject $partition -Association Win32_DiskDriveToDiskPartition) + foreach ($drive in $drives) { + $candidate = Convert-GeoNetOptionalString -Value $drive.Model + if ($candidate) { return $candidate } + } + } + return $null + } + + return $model +} + function Get-GeoNetDiskInfo { Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object { @@ -13,6 +39,7 @@ function Get-GeoNetDiskInfo { free_gb = $freeGb pct_used = $usedPct file_system = $_.FileSystem + model = Get-GeoNetPhysicalDiskModel -DriveLetter $_.DeviceID } } } diff --git a/GeoNetAgent/agent/lib/Get-GpuInfo.ps1 b/GeoNetAgent/agent/lib/Get-GpuInfo.ps1 new file mode 100644 index 0000000..f364ea7 --- /dev/null +++ b/GeoNetAgent/agent/lib/Get-GpuInfo.ps1 @@ -0,0 +1,39 @@ +$formatText = Join-Path $PSScriptRoot 'Format-Text.ps1' +if (Test-Path $formatText) { . $formatText } + +function Convert-GeoNetAdapterRamMb { + param([AllowNull()]$AdapterRam) + + if ($null -eq $AdapterRam -or $AdapterRam -le 0) { return $null } + + try { + $mb = [math]::Round([decimal]$AdapterRam / 1MB, 0) + if ($mb -le 0 -or $mb -gt 131072) { return $null } + return [int]$mb + } catch { + return $null + } +} + +function Get-GeoNetGpuInfo { + $controllers = Invoke-GeoNetSafe { + Get-CimInstance Win32_VideoController -ErrorAction Stop | + Where-Object { + $_.Name -and $_.Name -notmatch 'Microsoft Basic|Remote|Sage Display|Display Manager' + } + } -Default @() + + $gpus = @($controllers | ForEach-Object { + $name = Convert-GeoNetOptionalString -Value $_.Name + if (-not $name) { return } + + [pscustomobject]@{ + name = $name + dedicated_memory_mb = Convert-GeoNetAdapterRamMb -AdapterRam $_.AdapterRAM + } + }) + + @($gpus | Sort-Object { + if ($null -ne $_.dedicated_memory_mb) { $_.dedicated_memory_mb } else { -1 } + } -Descending) +} diff --git a/GeoNetAgent/agent/lib/Get-SystemInfo.ps1 b/GeoNetAgent/agent/lib/Get-SystemInfo.ps1 index c81c085..3d82145 100644 --- a/GeoNetAgent/agent/lib/Get-SystemInfo.ps1 +++ b/GeoNetAgent/agent/lib/Get-SystemInfo.ps1 @@ -1,6 +1,9 @@ $formatText = Join-Path $PSScriptRoot 'Format-Text.ps1' if (Test-Path $formatText) { . $formatText } +$gpuInfo = Join-Path $PSScriptRoot 'Get-GpuInfo.ps1' +if (Test-Path $gpuInfo) { . $gpuInfo } + function Test-GeoNetValidUuid { param($Uuid) @@ -53,11 +56,8 @@ function Get-GeoNetSystemInfo { Convert-GeoNetOptionalString -Value $guid } - $gpu = Invoke-GeoNetSafe { - Get-CimInstance Win32_VideoController -ErrorAction Stop | - Where-Object { $_.Name -and $_.Name -notmatch 'Microsoft Basic|Remote' } | - Select-Object -First 1 - } + $gpus = @(Invoke-GeoNetSafe { @(Get-GeoNetGpuInfo) } -Default @()) + $gpu = $gpus | Select-Object -First 1 $ramGb = $null if ($cs -and $cs.TotalPhysicalMemory) { @@ -97,7 +97,8 @@ function Get-GeoNetSystemInfo { OsBuild = $osBuild CpuModel = Convert-GeoNetOptionalString -Value $cpu.Name RamGb = $ramGb - GpuModel = Convert-GeoNetOptionalString -Value $gpu.Name + GpuModel = Convert-GeoNetOptionalString -Value $gpu.name + Gpus = $gpus LastBootAt = $lastBootAt } } diff --git a/GeoNetAgent/agent/lib/Update-GeoNetAgent.ps1 b/GeoNetAgent/agent/lib/Update-GeoNetAgent.ps1 new file mode 100644 index 0000000..edc6268 --- /dev/null +++ b/GeoNetAgent/agent/lib/Update-GeoNetAgent.ps1 @@ -0,0 +1,74 @@ +$installCore = Join-Path $PSScriptRoot 'Agent-InstallCore.ps1' +if (Test-Path $installCore) { . $installCore } + +function Update-GeoNetAgentIfNeeded { + <# + .SYNOPSIS + Cek manifest.json di server vs VERSION lokal; unduh paket jika beda. + Gagal update tidak menghentikan pemanggil (mis. kirim laporan tetap jalan). + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$InstallRoot, + [string]$BaseUrl, + [string]$ConfigPath, + [int]$ScheduleHours = 3, + [switch]$Quiet + ) + + $result = [ordered]@{ + Updated = $false + From = $null + To = $null + Error = $null + } + + try { + if (-not $BaseUrl) { + $BaseUrl = Get-GeoNetInstallBaseUrlFromConfig -ConfigPath $ConfigPath + } + $base = Get-GeoNetInstallBaseUrl -Url $BaseUrl + + $manifest = Get-GeoNetRemoteJson -Url "$base/manifest.json" + $remoteVersion = [string]$manifest.version + if ([string]::IsNullOrWhiteSpace($remoteVersion)) { + throw 'manifest.json server tidak memuat version.' + } + $remoteVersion = $remoteVersion.Trim() + + $localVersion = Get-GeoNetLocalAgentVersion -InstallRoot $InstallRoot + $result.From = $localVersion + + if ($localVersion -eq $remoteVersion) { + return [pscustomobject]$result + } + + if (-not $Quiet) { + Write-Host "GeoNetAgent auto-update: $localVersion -> $remoteVersion" -ForegroundColor Cyan + } + + Install-GeoNetAgentPayload -Base $base -DestinationRoot $InstallRoot -Manifest $manifest -Quiet:$Quiet + Set-GeoNetLocalAgentVersion -InstallRoot $InstallRoot -Version $remoteVersion + + try { + Register-GeoNetAgentTask -InstallRoot $InstallRoot -Hours $ScheduleHours + } + catch { + if (-not $Quiet) { + Write-Warning "Auto-update: file terbaru, tetapi refresh Scheduled Task gagal: $_" + } + } + + $result.Updated = $true + $result.To = $remoteVersion + } + catch { + $result.Error = $_.Exception.Message + if (-not $Quiet) { + Write-Warning "GeoNetAgent auto-update dilewati: $($result.Error)" + } + } + + return [pscustomobject]$result +} diff --git a/GeoNetAgent/collector/app/db/models.py b/GeoNetAgent/collector/app/db/models.py index 48c3a56..395ccc6 100644 --- a/GeoNetAgent/collector/app/db/models.py +++ b/GeoNetAgent/collector/app/db/models.py @@ -59,6 +59,7 @@ class AssetDisk(Base): free_gb: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False) pct_used: Mapped[float] = mapped_column(Numeric(5, 2), nullable=False) file_system: Mapped[str | None] = mapped_column(String(20)) + model: Mapped[str | None] = mapped_column(String(100)) class AssetNetworkAdapter(Base): diff --git a/GeoNetAgent/collector/app/schemas/agent_report.py b/GeoNetAgent/collector/app/schemas/agent_report.py index 4274db9..8e7d0b2 100644 --- a/GeoNetAgent/collector/app/schemas/agent_report.py +++ b/GeoNetAgent/collector/app/schemas/agent_report.py @@ -4,6 +4,11 @@ from typing import Any, Literal from pydantic import BaseModel, Field, field_validator +class GpuInfo(BaseModel): + name: str + dedicated_memory_mb: int | None = None + + class SystemInfo(BaseModel): manufacturer: str | None = None model: str | None = None @@ -23,6 +28,7 @@ class DiskInfo(BaseModel): free_gb: float pct_used: float file_system: str | None = None + model: str | None = None class MemoryModule(BaseModel): @@ -116,6 +122,7 @@ class AgentReportRequest(BaseModel): asset_type: str = "endpoint" logged_in_user: str | None = None system: SystemInfo + gpus: list[GpuInfo] = Field(default_factory=list) disks: list[DiskInfo] = Field(default_factory=list) memory_modules: list[MemoryModule] = Field(default_factory=list) memory_summary: MemorySummary | None = None diff --git a/GeoNetAgent/collector/app/schemas/assets.py b/GeoNetAgent/collector/app/schemas/assets.py index cec4c81..30843a9 100644 --- a/GeoNetAgent/collector/app/schemas/assets.py +++ b/GeoNetAgent/collector/app/schemas/assets.py @@ -34,6 +34,12 @@ class DiskOut(BaseModel): free_gb: float pct_used: float file_system: str | None = None + model: str | None = None + + +class GpuOut(BaseModel): + name: str + dedicated_memory_mb: int | None = None class NetworkAdapterOut(BaseModel): @@ -78,6 +84,7 @@ class SnapshotSummary(BaseModel): software_count: int | None = None collected_at: datetime created_at: datetime + gpus: list[GpuOut] = Field(default_factory=list) disks: list[DiskOut] = Field(default_factory=list) network: list[NetworkAdapterOut] = Field(default_factory=list) peripherals: list[PeripheralOut] = Field(default_factory=list) diff --git a/GeoNetAgent/collector/app/services/asset_read_service.py b/GeoNetAgent/collector/app/services/asset_read_service.py index 7a23c43..4039c7a 100644 --- a/GeoNetAgent/collector/app/services/asset_read_service.py +++ b/GeoNetAgent/collector/app/services/asset_read_service.py @@ -10,6 +10,7 @@ from app.schemas.assets import ( AssetDetail, AssetListItem, DiskOut, + GpuOut, NetworkAdapterOut, PaginatedAssets, PaginatedSnapshots, @@ -183,6 +184,7 @@ def _load_snapshot_children(db: Session, snapshot: AssetSnapshot) -> tuple[list[ free_gb=float(d.free_gb), pct_used=float(d.pct_used), file_system=d.file_system, + model=d.model, ) for d in disks ], @@ -238,9 +240,38 @@ def _peripherals_from_payload(payload: dict | None) -> list[PeripheralOut]: return items +def _gpus_from_payload(payload: dict | None) -> list[GpuOut]: + if not payload: + return [] + + raw = payload.get("gpus") + if not isinstance(raw, list): + gpu_model = payload.get("system", {}).get("gpu_model") if isinstance(payload.get("system"), dict) else None + if gpu_model: + return [GpuOut(name=str(gpu_model))] + return [] + + items: list[GpuOut] = [] + for entry in raw: + if not isinstance(entry, dict): + continue + name = entry.get("name") + if not name: + continue + dedicated = entry.get("dedicated_memory_mb") + items.append( + GpuOut( + name=str(name), + dedicated_memory_mb=int(dedicated) if dedicated is not None else None, + ) + ) + return items + + def _snapshot_to_summary(db: Session, snapshot: AssetSnapshot) -> SnapshotSummary: disks, network = _load_snapshot_children(db, snapshot) peripherals = _peripherals_from_payload(snapshot.payload) + gpus = _gpus_from_payload(snapshot.payload) return SnapshotSummary( id=str(snapshot.id), agent_version=snapshot.agent_version, @@ -255,6 +286,7 @@ def _snapshot_to_summary(db: Session, snapshot: AssetSnapshot) -> SnapshotSummar software_count=snapshot.software_count, collected_at=snapshot.collected_at, created_at=snapshot.created_at, + gpus=gpus, disks=disks, network=network, peripherals=peripherals, diff --git a/GeoNetAgent/collector/app/services/asset_service.py b/GeoNetAgent/collector/app/services/asset_service.py index 983ccf8..7d84262 100644 --- a/GeoNetAgent/collector/app/services/asset_service.py +++ b/GeoNetAgent/collector/app/services/asset_service.py @@ -164,7 +164,7 @@ def ingest_agent_report(db: Session, report: AgentReportRequest) -> AgentReportR os_version=report.system.os_version or report.system.os_build, cpu_model=report.system.cpu_model, ram_gb=report.system.ram_gb, - gpu_model=report.system.gpu_model, + gpu_model=report.system.gpu_model or (report.gpus[0].name if report.gpus else None), bios_version=report.system.bios_version, last_boot_at=report.system.last_boot_at, bsod_count_30d=report.health.bsod_count_30d, @@ -184,6 +184,7 @@ def ingest_agent_report(db: Session, report: AgentReportRequest) -> AgentReportR free_gb=disk.free_gb, pct_used=disk.pct_used, file_system=disk.file_system, + model=_sanitize_text(disk.model, 100), ) ) diff --git a/GeoNetAgent/collector/static/app.js b/GeoNetAgent/collector/static/app.js index 4580ff8..2cf78fa 100644 --- a/GeoNetAgent/collector/static/app.js +++ b/GeoNetAgent/collector/static/app.js @@ -220,15 +220,28 @@ function renderDetail(asset, software) {
OS
${escapeHtml(snap.os_name || "—")} ${escapeHtml(snap.os_version || "")}
CPU
${escapeHtml(snap.cpu_model || "—")}
RAM
${escapeHtml(snap.ram_gb != null ? `${snap.ram_gb} GB` : "—")}
+
GPU
${escapeHtml(snap.gpu_model || "—")}
Agent
${escapeHtml(snap.agent_version)}
`; + if (snap.gpus?.length) { + html += `
GPU
`; + html += snap.gpus + .map((g) => { + const mem = + g.dedicated_memory_mb != null ? `${g.dedicated_memory_mb} MB` : "—"; + return ``; + }) + .join(""); + html += `
CardDedicated memory
${escapeHtml(g.name)}${escapeHtml(mem)}
`; + } + if (snap.disks?.length) { - html += `
Disk
`; + html += `
Disk
DriveUsedFree
`; html += snap.disks .map((d) => { const warn = d.pct_used > 85 ? "style='color:var(--warn)'" : ""; - return ``; + return ``; }) .join(""); html += `
DriveModelUsedFree
${escapeHtml(d.drive_letter)}${escapeHtml(d.pct_used)}%${escapeHtml(d.free_gb)} GB
${escapeHtml(d.drive_letter)}${escapeHtml(d.model || "—")}${escapeHtml(d.pct_used)}%${escapeHtml(d.free_gb)} GB
`; diff --git a/GeoNetAgent/infra/collector/publish_agent_install.sh b/GeoNetAgent/infra/collector/publish_agent_install.sh index 9b5ff4a..d3527f6 100644 --- a/GeoNetAgent/infra/collector/publish_agent_install.sh +++ b/GeoNetAgent/infra/collector/publish_agent_install.sh @@ -27,6 +27,8 @@ done echo "==> Upload installer + manifest" scp -q "$AGENT/Install-GeoNetAgent.ps1" "$REMOTE:$INSTALL_ROOT/" +ssh "$REMOTE" "mkdir -p $INSTALL_ROOT/lib" +scp -q "$AGENT/lib/Agent-InstallCore.ps1" "$REMOTE:$INSTALL_ROOT/lib/" scp -q "$AGENT/install/bootstrap.ps1" "$REMOTE:$INSTALL_ROOT/" scp -q "$AGENT/install/manifest.json" "$REMOTE:$INSTALL_ROOT/" scp -q "$AGENT/install/GeoNetAgent-Install.ps1" "$REMOTE:$INSTALL_ROOT/" diff --git a/GeoNetAgent/infra/db/migrations/002_disk_model.sql b/GeoNetAgent/infra/db/migrations/002_disk_model.sql new file mode 100644 index 0000000..489c2df --- /dev/null +++ b/GeoNetAgent/infra/db/migrations/002_disk_model.sql @@ -0,0 +1,3 @@ +-- Migration 002: physical disk model on asset_disks +ALTER TABLE asset_disks + ADD COLUMN IF NOT EXISTS model varchar(100); diff --git a/GeoNetAgent/samples/agent-reports/DESKTOP-RRJ9G01_20260618_112523.json b/GeoNetAgent/samples/agent-reports/DESKTOP-RRJ9G01_20260618_112523.json new file mode 100644 index 0000000..888393e --- /dev/null +++ b/GeoNetAgent/samples/agent-reports/DESKTOP-RRJ9G01_20260618_112523.json @@ -0,0 +1,320 @@ +{ + "schema_version": "1.0", + "agent_version": "0.1.7-phase0", + "collected_at": "2026-06-18T11:25:23+07:00", + "hostname": "DESKTOP-RRJ9G01", + "machine_id": "3709baa5-5265-4e4d-9a81-fb8c8927bb3b", + "smbios_uuid": "03D502E0-045E-0550-8E06-BA0700080009", + "serial_number": null, + "asset_type": "endpoint", + "logged_in_user": "DESKTOP-RRJ9G01\\admin", + "system": { + "manufacturer": "Gigabyte Technology Co., Ltd.", + "model": "H81M-DS2", + "bios_version": "F2", + "os_name": "Microsoft Windows 11 Pro", + "os_version": "10.0", + "os_build": "10.0.26200 (26200)", + "cpu_model": "Intel(R) Core(TM) i5-4460 CPU @ 3.20GHz", + "ram_gb": 15.9, + "gpu_model": "NVIDIA GeForce GT 730", + "last_boot_at": "2026-06-18T10:41:02+07:00" + }, + "gpus": [ + { + "name": "NVIDIA GeForce GT 730", + "dedicated_memory_mb": 2048 + }, + { + "name": "Intel(R) HD Graphics 4600", + "dedicated_memory_mb": 1024 + } + ], + "disks": [ + { + "drive_letter": "C:", + "total_gb": 188.7, + "free_gb": 4.6, + "pct_used": 97.6, + "file_system": "NTFS", + "model": "ADATA SU650" + }, + { + "drive_letter": "D:", + "total_gb": 256.5, + "free_gb": 68.5, + "pct_used": 73.3, + "file_system": "NTFS", + "model": "ADATA SU650" + } + ], + "memory_modules": [ + { + "slot": "ChannelA-DIMM0", + "bank_label": "BANK 0", + "capacity_gb": 8, + "speed_mhz": 1600, + "manufacturer": "0000", + "part_number": "", + "form_factor": "tsop", + "memory_type": 24 + }, + { + "slot": "ChannelB-DIMM0", + "bank_label": "BANK 2", + "capacity_gb": 8, + "speed_mhz": 1600, + "manufacturer": "0000", + "part_number": "", + "form_factor": "tsop", + "memory_type": 24 + } + ], + "memory_summary": { + "total_gb": 16, + "module_count": 2, + "slot_count": 2, + "empty_slots": 0, + "layout": "2x8GB", + "configuration": "2x8GB tsop", + "upgradeable": false + }, + "network": [ + { + "adapter_name": "Realtek(R) PCI(e) Ethernet Controller", + "mac_address": "E0:D5:5E:50:8E:BA", + "ip_address": "192.168.0.21", + "adapter_kind": "physical", + "status": "Up", + "is_primary": true, + "is_built_in": true + }, + { + "adapter_name": "Fortinet SSL VPN Virtual Ethernet Adapter", + "mac_address": "00:09:0F:AA:00:01", + "ip_address": null, + "adapter_kind": "virtual", + "status": "Not Present", + "is_primary": false, + "is_built_in": null + }, + { + "adapter_name": "Fortinet Virtual Ethernet Adapter (NDIS 6.30)", + "mac_address": "00:09:0F:FE:00:01", + "ip_address": null, + "adapter_kind": "virtual", + "status": "Disconnected", + "is_primary": false, + "is_built_in": null + }, + { + "adapter_name": "Hyper-V Virtual Ethernet Adapter", + "mac_address": "00:15:5D:F1:DB:09", + "ip_address": "172.27.176.1", + "adapter_kind": "virtual", + "status": "Up", + "is_primary": false, + "is_built_in": null + }, + { + "adapter_name": "OpenVPN Data Channel Offload", + "mac_address": null, + "ip_address": null, + "adapter_kind": "virtual", + "status": "Disconnected", + "is_primary": false, + "is_built_in": null + }, + { + "adapter_name": "PANGP Virtual Ethernet Adapter Secure #2", + "mac_address": "02:50:41:00:00:01", + "ip_address": null, + "adapter_kind": "virtual", + "status": "Disabled", + "is_primary": false, + "is_built_in": null + }, + { + "adapter_name": "TAP-Windows Adapter V9 for OpenVPN Connect", + "mac_address": "00:FF:E0:E1:4D:4D", + "ip_address": null, + "adapter_kind": "virtual", + "status": "Disconnected", + "is_primary": false, + "is_built_in": null + } + ], + "asset_identity": { + "primary_physical_mac": "E0:D5:5E:50:8E:BA", + "primary_physical_adapter": "Realtek(R) PCI(e) Ethernet Controller", + "primary_mac_built_in": true, + "primary_mac_source": "primary_route_built_in_physical", + "merge_key": "mac:E0:D5:5E:50:8E:BA", + "recommended_match_order": [ + "serial_number", + "smbios_uuid", + "primary_physical_mac", + "machine_id", + "hostname" + ] + }, + "peripherals": [ + { + "name": "E1942", + "device_class": "Monitor", + "manufacturer": "GSM", + "connection": "display", + "device_role": "display", + "vid": null, + "pid": null, + "pnp_device_id": "DISPLAY\\GSM4C09\\5\u0026316DE597\u00260\u0026UID4865", + "device_serial": "211INNG5R639", + "edid_product": "GSM4C09", + "resolution": "1366x768", + "resolution_width": 1366, + "resolution_height": 768, + "refresh_hz": 60, + "status": "OK", + "is_external": true + }, + { + "name": "USB Input Device", + "device_class": "HIDClass", + "manufacturer": "(Standard system devices)", + "connection": "usb", + "device_role": "peripheral", + "vid": "3151", + "pid": "3022", + "pnp_device_id": "USB\\VID_3151\u0026PID_3022\u0026MI_01\\8\u0026104DB858\u00260\u00260001", + "device_serial": null, + "edid_product": null, + "resolution": null, + "resolution_width": null, + "resolution_height": null, + "refresh_hz": null, + "status": "OK", + "is_external": true + }, + { + "name": "USB Input Device", + "device_class": "HIDClass", + "manufacturer": "(Standard system devices)", + "connection": "usb", + "device_role": "peripheral", + "vid": "3151", + "pid": "3022", + "pnp_device_id": "USB\\VID_3151\u0026PID_3022\u0026MI_00\\8\u0026104DB858\u00260\u00260000", + "device_serial": null, + "edid_product": null, + "resolution": null, + "resolution_width": null, + "resolution_height": null, + "refresh_hz": null, + "status": "OK", + "is_external": true + }, + { + "name": "HID-compliant consumer control device", + "device_class": "HIDClass", + "manufacturer": "Microsoft", + "connection": "usb_hid", + "device_role": "input", + "vid": "3151", + "pid": "3022", + "pnp_device_id": "HID\\VID_3151\u0026PID_3022\u0026MI_01\u0026COL03\\9\u00262CF1CDFF\u00260\u00260002", + "device_serial": null, + "edid_product": null, + "resolution": null, + "resolution_width": null, + "resolution_height": null, + "refresh_hz": null, + "status": "OK", + "is_external": true + }, + { + "name": "HID-compliant system controller", + "device_class": "HIDClass", + "manufacturer": "(Standard system devices)", + "connection": "usb_hid", + "device_role": "input", + "vid": "3151", + "pid": "3022", + "pnp_device_id": "HID\\VID_3151\u0026PID_3022\u0026MI_01\u0026COL02\\9\u00262CF1CDFF\u00260\u00260001", + "device_serial": null, + "edid_product": null, + "resolution": null, + "resolution_width": null, + "resolution_height": null, + "refresh_hz": null, + "status": "OK", + "is_external": true + }, + { + "name": "HID-compliant vendor-defined device", + "device_class": "HIDClass", + "manufacturer": "(Standard system devices)", + "connection": "usb_hid", + "device_role": "input", + "vid": "3151", + "pid": "3022", + "pnp_device_id": "HID\\VID_3151\u0026PID_3022\u0026MI_01\u0026COL04\\9\u00262CF1CDFF\u00260\u00260003", + "device_serial": null, + "edid_product": null, + "resolution": null, + "resolution_width": null, + "resolution_height": null, + "refresh_hz": null, + "status": "OK", + "is_external": true + }, + { + "name": "HID Keyboard Device", + "device_class": "Keyboard", + "manufacturer": "(Standard keyboards)", + "connection": "usb_hid", + "device_role": "input", + "vid": "3151", + "pid": "3022", + "pnp_device_id": "HID\\VID_3151\u0026PID_3022\u0026MI_00\\9\u002638016031\u00260\u00260000", + "device_serial": null, + "edid_product": null, + "resolution": null, + "resolution_width": null, + "resolution_height": null, + "refresh_hz": null, + "status": "OK", + "is_external": true + }, + { + "name": "HID-compliant mouse", + "device_class": "Mouse", + "manufacturer": "Microsoft", + "connection": "usb_hid", + "device_role": "input", + "vid": "3151", + "pid": "3022", + "pnp_device_id": "HID\\VID_3151\u0026PID_3022\u0026MI_01\u0026COL01\\9\u00262CF1CDFF\u00260\u00260000", + "device_serial": null, + "edid_product": null, + "resolution": null, + "resolution_width": null, + "resolution_height": null, + "refresh_hz": null, + "status": "OK", + "is_external": true + } + ], + "health": { + "bsod_count_30d": 6, + "pending_reboot": false, + "last_patch_installed": "2026-02-27" + }, + "software": [ + + ], + "metadata": { + "collect_mode": "phase0-local", + "software_included": false, + "software_count": 0 + } +} \ No newline at end of file