feat: host inventory to SAC every 12h (GetInventory)

Collect CPU/RAM/disks/GPU/OS via agent.inventory; setting $GetInventory (default true); deploy adds missing setting.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-05 09:53:03 +10:00
parent bd83dc84dc
commit 82c63fb9a8
4 changed files with 210 additions and 2 deletions
+44
View File
@@ -635,6 +635,7 @@ function Invoke-RdpMonitorSettingsPostPatches {
if (Repair-RdpMonitorSettingsWinRmLinesIfInvalid -LocalSettings $LocalSettings) { $changed = $true }
if (Sync-RdpMonitorSettingsWinRmInboundBlock -LocalSettings $LocalSettings) { $changed = $true }
if (Sync-RdpMonitorSettingsMaxBackupDaysIfMissing -LocalSettings $LocalSettings) { $changed = $true }
if (Sync-RdpMonitorSettingsGetInventoryIfMissing -LocalSettings $LocalSettings) { $changed = $true }
return $changed
}
@@ -683,6 +684,49 @@ function Sync-RdpMonitorSettingsMaxBackupDaysIfMissing {
return $true
}
function Test-RdpMonitorSettingsNeedsGetInventory {
param([string]$SettingsPath)
if (-not (Test-Path -LiteralPath $SettingsPath)) { return $false }
$c = Get-RdpMonitorSettingsRaw -Path $SettingsPath
if ([string]::IsNullOrWhiteSpace($c)) { return $true }
return ($c -notmatch '(?m)^\s*\$GetInventory\s*=')
}
function Sync-RdpMonitorSettingsGetInventoryIfMissing {
param([string]$LocalSettings)
if (-not (Test-RdpMonitorSettingsNeedsGetInventory -SettingsPath $LocalSettings)) {
return $false
}
$c = Get-RdpMonitorSettingsRaw -Path $LocalSettings
$block = @(
'# --- Инвентаризация железа/ПО для SAC (agent.inventory, раз в 12 ч) ---'
'$GetInventory = $true'
) -join "`r`n"
$bak = "$LocalSettings.bak.$(Get-Date -Format 'yyyyMMdd_HHmmss')"
if (Test-Path -LiteralPath $LocalSettings) {
Copy-Item -LiteralPath $LocalSettings -Destination $bak -Force
Write-DeployLog "Добавление `$GetInventory в login_monitor.settings.ps1; резервная копия: $bak"
} else {
Write-DeployLog "Создание login_monitor.settings.ps1 с `$GetInventory = `$true"
}
$insertAfterDaily = '(?m)^(\s*\$DailyReportEnabled\s*=.*)$'
if (-not [string]::IsNullOrWhiteSpace($c) -and $c -match $insertAfterDaily) {
$newContent = [regex]::Replace($c, $insertAfterDaily, ('$1' + "`r`n`r`n" + $block), 1)
} elseif (-not [string]::IsNullOrWhiteSpace($c)) {
$newContent = ($c.TrimEnd() + "`r`n`r`n" + $block + "`r`n")
} else {
$newContent = ($block + "`r`n")
}
[System.IO.File]::WriteAllText($LocalSettings, $newContent.TrimEnd() + "`r`n", $Utf8Bom)
Write-DeployLog 'login_monitor.settings.ps1: добавлен $GetInventory = $true'
return $true
}
function Update-RdpMonitorSettingsServerDisplayNameHintIfMissing {
param([string]$LocalSettings)
if (-not (Test-Path -LiteralPath $LocalSettings)) { return $false }
+162 -1
View File
@@ -89,7 +89,7 @@ $script:SkipLogDetailLimit = 15
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
$ScriptVersion = "2.0.20-SAC"
$ScriptVersion = "2.0.21-SAC"
# Логи (все под InstallRoot)
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
@@ -102,6 +102,9 @@ $MaxBackupDays = 31
# Heartbeat (файл; при отсутствии обновления > HeartbeatStaleAlertMultiplier × интервал — оповещение)
$HeartbeatInterval = 3600
# Инвентаризация железа/ПО для SAC (agent.inventory); интервал опроса, сек (12 ч).
$InventoryIntervalSec = 43200
$GetInventory = 1
$HeartbeatStaleAlertMultiplier = 2
# Один RDP-вход часто даёт 2+ события 4624 с одним временем — не слать дубли в Telegram/SAC.
$LoginSuccessNotifyDedupSeconds = 90
@@ -2077,6 +2080,148 @@ function Build-DailyReportPlainBodyWindows {
return $sb.ToString().TrimEnd()
}
function Get-HostInventorySnapshot {
$inv = [ordered]@{
schema_version = 1
collected_at = (Get-Date).ToUniversalTime().ToString('o')
computer_name = $env:COMPUTERNAME
processor = @()
motherboard = $null
memory_gb = $null
disks = @()
video = @()
windows = $null
ipv4 = @()
}
try {
Get-CimInstance -ClassName Win32_Processor -ErrorAction Stop | ForEach-Object {
$inv.processor += @{
name = [string]$_.Name
cores = [int]$_.NumberOfCores
logical_processors = [int]$_.NumberOfLogicalProcessors
}
}
} catch {
Write-Log "Inventory: Win32_Processor — $($_.Exception.Message)"
}
try {
$bb = Get-CimInstance -ClassName Win32_BaseBoard -ErrorAction Stop | Select-Object -First 1
if ($null -ne $bb) {
$inv.motherboard = @{
product = [string]$bb.Product
manufacturer = [string]$bb.Manufacturer
}
}
} catch {
Write-Log "Inventory: Win32_BaseBoard — $($_.Exception.Message)"
}
try {
$mem = Get-CimInstance -ClassName Win32_PhysicalMemory -ErrorAction Stop
if ($mem) {
$sum = ($mem | Measure-Object -Property Capacity -Sum).Sum
if ($sum) { $inv.memory_gb = [math]::Round([double]$sum / 1GB, 2) }
}
} catch {
Write-Log "Inventory: Win32_PhysicalMemory — $($_.Exception.Message)"
}
try {
Get-PhysicalDisk -ErrorAction Stop | ForEach-Object {
$inv.disks += @{
friendly_name = [string]$_.FriendlyName
media_type = [string]$_.MediaType
size_gb = [math]::Round([double]$_.Size / 1GB, 2)
}
}
} catch {
try {
Get-CimInstance -ClassName Win32_DiskDrive -ErrorAction Stop | ForEach-Object {
$sizeGb = if ($_.Size) { [math]::Round([double]$_.Size / 1GB, 2) } else { $null }
$inv.disks += @{
friendly_name = [string]$_.Model
media_type = [string]$_.MediaType
size_gb = $sizeGb
}
}
} catch {
Write-Log "Inventory: disks — $($_.Exception.Message)"
}
}
try {
Get-CimInstance -ClassName Win32_VideoController -ErrorAction Stop |
Where-Object { -not [string]::IsNullOrWhiteSpace($_.Name) } |
ForEach-Object {
$inv.video += @{ name = [string]$_.Name }
}
} catch {
Write-Log "Inventory: Win32_VideoController — $($_.Exception.Message)"
}
try {
$ci = Get-ComputerInfo -ErrorAction Stop
$inv.windows = @{
product_name = [string]$ci.WindowsProductName
version = [string]$ci.WindowsVersion
os_hardware_abstraction_layer = [string]$ci.OsHardwareAbstractionLayer
}
} catch {
try {
$os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop | Select-Object -First 1
if ($null -ne $os) {
$inv.windows = @{
product_name = [string]$os.Caption
version = [string]$os.Version
os_hardware_abstraction_layer = [string]$os.OSArchitecture
}
}
} catch {
Write-Log "Inventory: Windows version — $($_.Exception.Message)"
}
}
try {
$ips = @(
Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop |
Where-Object {
$_.IPAddress -and
$_.IPAddress -notlike '169.254*' -and
$_.IPAddress -ne '127.0.0.1'
} |
ForEach-Object { [string]$_.IPAddress }
) | Sort-Object -Unique
$inv.ipv4 = @($ips)
} catch {
Write-Log "Inventory: IPv4 — $($_.Exception.Message)"
}
return $inv
}
function Send-HostInventoryToSac {
if (-not (Test-MonitorFeatureEnabled -Value $GetInventory)) { return $false }
if (-not $script:SacClientLoaded) { return $false }
if (-not (Get-Command Send-SacEvent -ErrorAction SilentlyContinue)) { return $false }
if ((Get-SacNormalizedMode) -eq 'off') { return $false }
if (-not (Test-SacConfigured)) { return $false }
$inv = Get-HostInventorySnapshot
$label = Get-MonitorServerLabelWithIp
$summary = "Inventory snapshot for $label"
$title = "Host inventory: $label"
$ok = Send-SacEvent -EventType 'agent.inventory' -Severity 'info' -Title $title -Summary $summary `
-Details @{ inventory = $inv }
if ($ok) {
Write-Log "SAC: agent.inventory отправлен (RAM=$($inv.memory_gb) GB, CPU=$($inv.processor.Count), disks=$($inv.disks.Count))."
} else {
Write-Log "SAC: не удалось отправить agent.inventory."
}
return [bool]$ok
}
function Convert-DailyReportPlainToTelegramHtml {
param([string]$PlainBody)
$lines = $PlainBody -split "`r?`n"
@@ -3796,6 +3941,12 @@ function Start-LoginMonitor {
if (Test-MonitorFeatureEnabled -Value $EnableAdminShareMonitoring) {
Write-Log "Admin share: Security 5140 для $($AdminShareMonitorSuffixes -join ', ') (audit File Share; workstation + server)."
}
if (Test-MonitorFeatureEnabled -Value $GetInventory) {
$invHours = [math]::Round($InventoryIntervalSec / 3600.0, 1)
Write-Log "Inventory: SAC agent.inventory включён, интервал ${invHours} ч ($InventoryIntervalSec с)."
} else {
Write-Log "Inventory: опрос железа/ПО отключён (`$GetInventory)."
}
Write-Log "========================================"
Write-Log "Каналы уведомлений: $(Get-NotifyChainHuman)"
Write-Log "Фильтр Exchange 4624 LT3 EmptyIP: ${Ignore4624-LT3-EmptyIP-Event}"
@@ -3840,6 +3991,9 @@ function Start-LoginMonitor {
Cleanup-OldLogs
Send-Heartbeat -IsStartup
Enable-SecurityAudit
if (Test-MonitorFeatureEnabled -Value $GetInventory) {
Send-HostInventoryToSac | Out-Null
}
$script:MonitorLoopInitialized = $true
}
@@ -3856,6 +4010,7 @@ function Start-LoginMonitor {
$adminShareMonitoringEnabled = Test-MonitorFeatureEnabled -Value $EnableAdminShareMonitoring
$nextHeartbeatTime = (Get-Date).AddSeconds($HeartbeatInterval)
$nextInventoryTime = (Get-Date).AddSeconds($InventoryIntervalSec)
$nextRotationCheck = Check-AndRotateLog
$nextReportCheck = Check-AndSendDailyReport
$lastCheckTime = (Get-Date).AddSeconds(-10)
@@ -4356,6 +4511,12 @@ function Start-LoginMonitor {
Send-Heartbeat
$nextHeartbeatTime = $nextHeartbeatTime.AddSeconds($HeartbeatInterval)
}
if (Test-MonitorFeatureEnabled -Value $GetInventory) {
if ($now -ge $nextInventoryTime) {
Send-HostInventoryToSac | Out-Null
$nextInventoryTime = $nextInventoryTime.AddSeconds($InventoryIntervalSec)
}
}
if ($now -ge $nextRotationCheck) {
$nextRotationCheck = Check-AndRotateLog
}
+3
View File
@@ -45,6 +45,9 @@ $SacApiKey = 'sac_UkOsAT3UWiQS54KK5OJPBDCSucysQDrKFju28wmYiz8'
# В settings.ps1 используйте 1/0 или $true/$false — не пишите голое false без $
$DailyReportEnabled = 1
# --- Инвентаризация железа/ПО для SAC (agent.inventory, раз в 12 ч) ---
$GetInventory = $true
# --- RDS Shadow Control + WinRM inbound (Enter-PSSession), severity warning ---
# $EnableRcmShadowControlMonitoring = 1 # RCM Operational 20506/20507/20510
# $EnableWinRmInboundMonitoring = 1 # WinRM Operational 91 (+ correlate Security 4624)
+1 -1
View File
@@ -1 +1 @@
2.0.20-SAC
2.0.21-SAC