#Requires -Version 5.1 <# Host Agent Installer for Windows Server Host: {{ $host->name }} Token: {{ $token }} Generated: {{ now() }} #> Set-StrictMode -Off $ErrorActionPreference = 'SilentlyContinue' $AGENT_DIR = "C:\ProgramData\host-agent" $AGENT_PS1 = "$AGENT_DIR\agent.ps1" $REPORT_URL = "{{ $reportUrl }}" $TOKEN = "{{ $token }}" $TASK_NAME = "HostAgent" # ── 建立目錄 ──────────────────────────────────────────────────────── New-Item -ItemType Directory -Force -Path $AGENT_DIR | Out-Null # ── 寫入 agent.ps1 ─────────────────────────────────────────────────── @' #Requires -Version 5.1 Set-StrictMode -Off $ErrorActionPreference = 'SilentlyContinue' $AGENT_DIR = "C:\ProgramData\host-agent" $REPORT_URL = "REPORT_URL_PLACEHOLDER" $TOKEN = "TOKEN_PLACEHOLDER" function esc($s) { if ($null -eq $s) { return "" }; [string]$s -replace '"','\"' -replace '\\','\\' } function jstr($s) { '"' + (esc $s) + '"' } function jbool($v) { if ($v) { 'true' } else { 'false' } } function jnum($v) { if ($null -eq $v -or $v -eq '') { '0' } else { [string][int]$v } } # ── OS 資訊 ───────────────────────────────────────────────────────── $os = Get-WmiObject Win32_OperatingSystem $cs = Get-WmiObject Win32_ComputerSystem $bios = Get-WmiObject Win32_BIOS $OS_NAME = $os.Caption $OS_VERSION = $os.Version $OS_BUILD = $os.BuildNumber $HOSTNAME = $env:COMPUTERNAME $TIMEZONE = (Get-TimeZone).Id $UPTIME_SEC = [int]((Get-Date) - $os.ConvertToDateTime($os.LastBootUpTime)).TotalSeconds $BOOT_TIME = $os.ConvertToDateTime($os.LastBootUpTime).ToString('yyyy-MM-dd HH:mm:ss') # IP $IP = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.InterfaceAlias -notmatch 'Loopback' } | Select-Object -First 1).IPAddress # ── CPU ───────────────────────────────────────────────────────────── $cpu = Get-WmiObject Win32_Processor | Select-Object -First 1 $CPU_MODEL = $cpu.Name $CPU_COUNT = (Get-WmiObject Win32_Processor).Count # ── 記憶體 ────────────────────────────────────────────────────────── $totalMem = [math]::Round($cs.TotalPhysicalMemory / 1GB, 2) $freeMem = [math]::Round($os.FreePhysicalMemory / 1MB / 1024, 2) $usedMem = [math]::Round($totalMem - $freeMem, 2) $memPct = if ($totalMem -gt 0) { [math]::Round($usedMem / $totalMem * 100, 1) } else { 0 } $MEMORY_JSON = "{`"total_gb`":$totalMem,`"used_gb`":$usedMem,`"free_gb`":$freeMem,`"percent`":$memPct}" # ── 磁碟 ──────────────────────────────────────────────────────────── $diskBuf = '' Get-WmiObject Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object { $sz = [math]::Round($_.Size / 1GB, 1) $fr = [math]::Round($_.FreeSpace / 1GB, 1) $us = [math]::Round(($_.Size - $_.FreeSpace) / 1GB, 1) $pct = if ($_.Size -gt 0) { [math]::Round(($_.Size - $_.FreeSpace) / $_.Size * 100, 1) } else { 0 } if ($diskBuf -ne '') { $diskBuf += ',' } $diskBuf += "{`"filesystem`":`"$($_.DeviceID)`",`"mounted_on`":`"$($_.DeviceID)`",`"size`":`"${sz}G`",`"used`":`"${us}G`",`"avail`":`"${fr}G`",`"percent`":`"${pct}%`"}" } $DISK_JSON = "[$diskBuf]" # ── 核心版本 ──────────────────────────────────────────────────────── $KERNEL_VERSION = "Windows NT $OS_VERSION (Build $OS_BUILD)" # ── 軟體版本 ──────────────────────────────────────────────────────── $PHP_VER = (php -v 2>$null | Select-Object -First 1) -replace '^PHP (\S+).*','$1' $MYSQL_VER = (mysql --version 2>$null) -replace '^.*Distrib (\S+),.*','$1' $IIS_VER = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\InetStp' -ErrorAction SilentlyContinue).MajorVersion $OPENSSL_VER = (openssl version 2>$null) -replace '^OpenSSL (\S+).*','$1' $OPENSSH_VER = (ssh -V 2>&1 | Select-String 'OpenSSH').ToString() -replace '^.*OpenSSH_(\S+).*','$1' # ── NTP ───────────────────────────────────────────────────────────── $w32Status = w32tm /query /status 2>$null $NTP_STATUS = if ($w32Status -match 'Source:') { 'synchronized' } else { 'unsynchronized' } $ntpSource = ($w32Status | Select-String 'Source:') -replace '.*Source:\s*','' $NTP_SERVERS_JSON = if ($ntpSource) { "[`"$($ntpSource.Trim())`"]" } else { '[]' } # ── 帳號資訊 ──────────────────────────────────────────────────────── $privAccts = '' Get-LocalGroupMember -Group 'Administrators' -ErrorAction SilentlyContinue | ForEach-Object { if ($privAccts -ne '') { $privAccts += ',' } $privAccts += jstr($_.Name) } $PRIV_ACCOUNTS_JSON = "[$privAccts]" $acctsBuf = '' Get-LocalUser | ForEach-Object { $exp = if ($_.AccountExpires) { $_.AccountExpires.ToString('yyyy-MM-dd') } else { 'never' } if ($acctsBuf -ne '') { $acctsBuf += ',' } $acctsBuf += "{`"user`":$(jstr $_.Name),`"enabled`":$(jbool $_.Enabled),`"expiry`":$(jstr $exp)}" } $ACCOUNTS_DETAIL_JSON = "[$acctsBuf]" # ── 密碼政策 ──────────────────────────────────────────────────────── $netAcc = net accounts 2>$null $passMinLen = ($netAcc | Select-String 'Minimum password length').ToString() -replace '.*:\s*','' $passMaxAge = ($netAcc | Select-String 'Maximum password age').ToString() -replace '.*:\s*','' $passMinAge = ($netAcc | Select-String 'Minimum password age').ToString() -replace '.*:\s*','' $passHistory= ($netAcc | Select-String 'password history').ToString() -replace '.*:\s*','' $lockThresh = ($netAcc | Select-String 'Lockout threshold').ToString() -replace '.*:\s*','' $PASS_POLICY_JSON = "{`"min_length`":$(jstr $passMinLen.Trim()),`"max_age`":$(jstr $passMaxAge.Trim()),`"min_age`":$(jstr $passMinAge.Trim()),`"history`":$(jstr $passHistory.Trim()),`"lockout_threshold`":$(jstr $lockThresh.Trim())}" # ── 防火牆 ────────────────────────────────────────────────────────── $fwProfiles = Get-NetFirewallProfile $fwEnabled = ($fwProfiles | Where-Object { $_.Enabled }).Count -gt 0 $FW_TYPE = 'Windows Firewall' $FW_STATUS = if ($fwEnabled) { 'active' } else { 'inactive' } $fwRulesBuf = '' Get-NetFirewallRule -Direction Inbound -Enabled True -Action Allow -ErrorAction SilentlyContinue | Select-Object -First 30 | ForEach-Object { $ports = (Get-NetFirewallPortFilter -AssociatedNetFirewallRule $_ -ErrorAction SilentlyContinue).LocalPort -join ',' if ($fwRulesBuf -ne '') { $fwRulesBuf += ',' } $fwRulesBuf += "{`"chain`":`"INPUT`",`"proto`":`"tcp`",`"port`":$(jstr $ports),`"action`":`"ACCEPT`",`"comment`":$(jstr $_.DisplayName)}" } $FW_RULES_JSON = "[$fwRulesBuf]" # ── 開放 Port ──────────────────────────────────────────────────────── $portsBuf = '' Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | ForEach-Object { $proc = (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName if ($portsBuf -ne '') { $portsBuf += ',' } $portsBuf += "{`"proto`":`"tcp`",`"local_address`":$(jstr $_.LocalAddress),`"local_port`":$($_.LocalPort),`"process`":$(jstr $proc)}" } $PORTS_JSON = "[$portsBuf]" # ── RDP / SSH 設定 ─────────────────────────────────────────────────── $rdpEnabled = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -ErrorAction SilentlyContinue).fDenyTSConnections -eq 0 $SSH_CONFIG_JSON = "{`"rdp_enabled`":$(jbool $rdpEnabled)}" # ── Windows Defender (防毒) ────────────────────────────────────────── $mpStatus = Get-MpComputerStatus -ErrorAction SilentlyContinue $AV_INSTALLED = jbool($null -ne $mpStatus) $AV_VERSION = if ($mpStatus) { $mpStatus.AMProductVersion } else { '' } $AV_RESULT = if ($mpStatus) { if ($mpStatus.AMRunningMode -eq 'Normal mode') { 'OK' } else { $mpStatus.AMRunningMode } } else { '' } # ── 憑證 (HTTPS) ───────────────────────────────────────────────────── $CERT_EXPIRY = '' Get-ChildItem Cert:\LocalMachine\My -ErrorAction SilentlyContinue | Where-Object { $_.HasPrivateKey } | Sort-Object NotAfter | Select-Object -First 1 | ForEach-Object { $CERT_EXPIRY = $_.NotAfter.ToString('yyyy-MM-dd') } # ── IIS Web 服務 ──────────────────────────────────────────────────── $webSvcJson = 'null' if (Get-Module -ListAvailable -Name WebAdministration -ErrorAction SilentlyContinue) { Import-Module WebAdministration -ErrorAction SilentlyContinue $iisVersion = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\InetStp' -ErrorAction SilentlyContinue).VersionString $sitesBuf = '' Get-WebSite -ErrorAction SilentlyContinue | ForEach-Object { $bindings = (Get-WebBinding -Name $_.Name | ForEach-Object { $_.bindingInformation }) -join ',' if ($sitesBuf -ne '') { $sitesBuf += ',' } $sitesBuf += "{`"name`":$(jstr $_.Name),`"state`":$(jstr $_.State),`"bindings`":$(jstr $bindings)}" } $webSvcJson = "{`"type`":`"IIS`",`"version`":$(jstr $iisVersion),`"sites`":[$sitesBuf]}" } # ── 更新狀態 ──────────────────────────────────────────────────────── $hotfixes = Get-HotFix | Sort-Object InstalledOn -Descending $LAST_UPDATE = if ($hotfixes) { $hotfixes[0].InstalledOn.ToString('yyyy-MM-dd') } else { '' } $PENDING_UPDATES = '' $wuSession = New-Object -ComObject Microsoft.Update.Session -ErrorAction SilentlyContinue if ($wuSession) { $searcher = $wuSession.CreateUpdateSearcher() $result = $searcher.Search("IsInstalled=0 and Type='Software'") $PENDING_UPDATES = [string]$result.Updates.Count } # ── 排程工作(cron 等效)──────────────────────────────────────────── $taskBuf = '' Get-ScheduledTask | Where-Object { $_.State -ne 'Disabled' -and $_.TaskPath -notmatch '^\\Microsoft\\' } | Select-Object -First 50 | ForEach-Object { $actions = ($_.Actions | ForEach-Object { "$($_.Execute) $($_.Arguments)".Trim() }) -join '; ' $trigger = ($_.Triggers | Select-Object -First 1).TriggerType if ($taskBuf -ne '') { $taskBuf += ',' } $taskBuf += "{`"source`":$(jstr $_.TaskPath),`"schedule`":$(jstr $trigger),`"command`":$(jstr $actions)}" } $CRON_JOBS_JSON = "[$taskBuf]" # ── 最近登入 ──────────────────────────────────────────────────────── $loginBuf = '' Get-WinEvent -LogName Security -MaxEvents 100 -FilterXPath "*[System[EventID=4624]]" -ErrorAction SilentlyContinue | Select-Object -First 10 | ForEach-Object { $msg = $_.Message -replace '"', "'" if ($loginBuf -ne '') { $loginBuf += '§' } $loginBuf += "$($_.TimeCreated.ToString('yyyy-MM-dd HH:mm')) $($_.Message -replace '\s+',' ' | Select-Object -First 1)" } $LAST_LOGINS = $loginBuf $FAILED_LOGIN_COUNT = 0 try { $FAILED_LOGIN_COUNT = (Get-WinEvent -LogName Security -MaxEvents 5000 -FilterXPath "*[System[EventID=4625]]" -ErrorAction Stop).Count } catch {} # ── MAC (AppArmor/SELinux 等效:Applocker/WDAC) ────────────────────── $MAC_TYPE = 'Windows Defender Application Control' $wdacPolicy = Get-CIPolicy -ErrorAction SilentlyContinue $MAC_STATUS = if ($wdacPolicy) { 'enabled' } else { 'disabled' } # ── log 完整性(IIS/Windows Event logs)──────────────────────────── $LOG_INTEGRITY_JSON = 'null' # ── SBOM ──────────────────────────────────────────────────────────── $sbomBuf = '' # installed packages via winget $pkgBuf = '' $pkgCount = 0 winget list --accept-source-agreements 2>$null | Select-Object -Skip 2 | ForEach-Object { $parts = $_ -split '\s{2,}' if ($parts.Count -ge 2) { $n = $parts[0].Trim(); $v = $parts[1].Trim() if ($pkgBuf -ne '') { $pkgBuf += ',' } $pkgBuf += "{`"name`":$(jstr $n),`"version`":$(jstr $v)}" $pkgCount++ } } # pip $pipBuf = '' $pipCount = 0 pip list --format=json 2>$null | ConvertFrom-Json -ErrorAction SilentlyContinue | ForEach-Object { if ($pipBuf -ne '') { $pipBuf += ',' } $pipBuf += "{`"name`":$(jstr $_.name),`"version`":$(jstr $_.version)}" $pipCount++ } # npm global $npmBuf = '' $npmCount = 0 npm list -g --depth=0 --json 2>$null | ConvertFrom-Json -ErrorAction SilentlyContinue | ForEach-Object { $_.dependencies.PSObject.Properties } | ForEach-Object { if ($npmBuf -ne '') { $npmBuf += ',' } $npmBuf += "{`"name`":$(jstr $_.Name),`"version`":$(jstr $_.Value.version)}" $npmCount++ } $SBOM_JSON = "{`"collected_at`":`"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')`",`"winget`":[$pkgBuf],`"pip`":[$pipBuf],`"npm`":[$npmBuf]}" # ── 備份檢查 ──────────────────────────────────────────────────────── $backupBuf = '' foreach ($bkType in @('data', 'system')) { foreach ($bkPath in @("C:\backup\$bkType\$bkType.ps1", "C:\backup\$bkType\$bkType.bat")) { $scriptFound = Test-Path $bkPath $bkDir = Split-Path $bkPath -Parent $dirFound = Test-Path $bkDir $lastBk = '' $files24h = 0 $bkSize = '' if ($dirFound) { $recent = Get-ChildItem $bkDir -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -gt (Get-Date).AddHours(-24) } $files24h = $recent.Count $latest = Get-ChildItem $bkDir -Recurse -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($latest) { $lastBk = $latest.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss') } $totalSz = (Get-ChildItem $bkDir -Recurse -File -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum $bkSize = if ($totalSz -gt 1GB) { "$([math]::Round($totalSz/1GB,1))G" } elseif ($totalSz -gt 1MB) { "$([math]::Round($totalSz/1MB,1))M" } else { "${totalSz}B" } } if ($backupBuf -ne '') { $backupBuf += ',' } $inCron = $false Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { ($_.Actions | Where-Object { $_.Execute -like "*$($bkPath)*" -or $_.Arguments -like "*$($bkPath)*" }) } | ForEach-Object { $inCron = $true } $backupBuf += "{`"type`":$(jstr $bkType),`"script_found`":$(jbool $scriptFound),`"script_path`":$(jstr $bkPath),`"in_cron`":$(jbool $inCron),`"dir_found`":$(jbool $dirFound),`"dir_path`":$(jstr $bkDir),`"last_backup`":$(jstr $lastBk),`"files_24h`":$files24h,`"size`":$(jstr $bkSize)}" } } $BACKUP_JSON = "[$backupBuf]" # ── 組合 JSON ──────────────────────────────────────────────────────── $JSON = @" { "hostname": $(jstr $HOSTNAME), "ip": $(jstr $IP), "os_release": {"PRETTY_NAME": $(jstr "$OS_NAME $OS_VERSION"), "platform": "windows"}, "kernel_version": $(jstr $KERNEL_VERSION), "timezone": $(jstr $TIMEZONE), "cpu_model": $(jstr $CPU_MODEL), "cpu_count": $CPU_COUNT, "memory": $MEMORY_JSON, "disk_info": $DISK_JSON, "uptime": $UPTIME_SEC, "boot_time": $(jstr $BOOT_TIME), "php_version": $(jstr $PHP_VER), "mysql_version": $(jstr $MYSQL_VER), "openssl_version": $(jstr $OPENSSL_VER), "openssh_version": $(jstr $OPENSSH_VER), "ntp_status": $(jstr $NTP_STATUS), "ntp_servers": $NTP_SERVERS_JSON, "privileged_accounts": $PRIV_ACCOUNTS_JSON, "accounts_detail": $ACCOUNTS_DETAIL_JSON, "password_policy": $PASS_POLICY_JSON, "firewall_type": $(jstr $FW_TYPE), "firewall_status": $(jstr $FW_STATUS), "firewall_rules": $FW_RULES_JSON, "listening_ports_parsed": $PORTS_JSON, "ssh_config": $SSH_CONFIG_JSON, "antivirus_installed": $AV_INSTALLED, "antivirus_version": $(jstr $AV_VERSION), "antivirus_result": $(jstr $AV_RESULT), "certificate_expiry": $(jstr $CERT_EXPIRY), "web_service": $webSvcJson, "pending_updates": $(jstr $PENDING_UPDATES), "last_update": $(jstr $LAST_UPDATE), "cron_jobs": $CRON_JOBS_JSON, "last_logins": $(jstr $LAST_LOGINS), "failed_login_count": $FAILED_LOGIN_COUNT, "mac_type": $(jstr $MAC_TYPE), "mac_status": $(jstr $MAC_STATUS), "log_integrity": $LOG_INTEGRITY_JSON, "backup": $BACKUP_JSON, "sbom": $SBOM_JSON, "platform": "windows" } "@ # ── 回報 ───────────────────────────────────────────────────────────── $headers = @{ 'X-Agent-Token' = $TOKEN; 'Content-Type' = 'application/json; charset=utf-8' } try { Invoke-RestMethod -Uri $REPORT_URL -Method POST -Headers $headers -Body $JSON -UseBasicParsing | Out-Null Write-Host "[OK] 回報成功 $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" } catch { Write-Host "[ERROR] 回報失敗: $_" } '@ -replace 'REPORT_URL_PLACEHOLDER', $REPORT_URL -replace 'TOKEN_PLACEHOLDER', $TOKEN | Set-Content -Path $AGENT_PS1 -Encoding UTF8 Write-Host "[1/3] agent.ps1 已寫入 $AGENT_PS1" # ── 建立排程工作(每 3 小時) ──────────────────────────────────────── $action = New-ScheduledTaskAction -Execute 'powershell.exe' ` -Argument "-NonInteractive -ExecutionPolicy Bypass -File `"$AGENT_PS1`"" $trigger = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Hours 3) -Once -At (Get-Date) $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 10) -StartWhenAvailable $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -RunLevel Highest Unregister-ScheduledTask -TaskName $TASK_NAME -Confirm:$false -ErrorAction SilentlyContinue Register-ScheduledTask -TaskName $TASK_NAME -Action $action -Trigger $trigger ` -Settings $settings -Principal $principal | Out-Null Write-Host "[2/3] 排程工作 '$TASK_NAME' 已建立(每 3 小時執行)" # ── 立即執行一次 ───────────────────────────────────────────────────── Write-Host "[3/3] 立即執行 Agent..." & powershell.exe -NonInteractive -ExecutionPolicy Bypass -File "$AGENT_PS1" Write-Host "" Write-Host "======================================" Write-Host " Host Agent 安裝完成" Write-Host " Agent 目錄: $AGENT_DIR" Write-Host " 排程: 每 3 小時自動回報" Write-Host "======================================"