- Windows-Dienst (golang.org/x/sys/windows/svc) - Collector: CPU (GetSystemTimes), RAM (GlobalMemoryStatusEx), Disks (GetDiskFreeSpaceEx) - WS-Client: gleiche Verbindungslogik wie Linux/BSD Agent - Commands: exec, winget list/install/upgrade/upgrade-all, Windows Updates check/install, reboot - install.ps1: automatische Service-Installation mit Parametern - make agent-windows VERSION=x.x.x
355 lines
8.5 KiB
Go
355 lines
8.5 KiB
Go
package collector
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
"golang.org/x/sys/windows/registry"
|
|
)
|
|
|
|
// SystemData ist das Haupt-Daten-Struct das an das Backend gesendet wird
|
|
type SystemData struct {
|
|
Windows *WindowsData `json:"windows,omitempty"`
|
|
}
|
|
|
|
type WindowsData struct {
|
|
Hostname string `json:"hostname"`
|
|
OSVersion string `json:"os_version"`
|
|
OSBuild string `json:"os_build"`
|
|
UptimeSecs int64 `json:"uptime_seconds"`
|
|
CPU CPUInfo `json:"cpu"`
|
|
Memory MemoryInfo `json:"memory"`
|
|
Disks []DiskInfo `json:"disks"`
|
|
Services []SvcInfo `json:"services,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
LastUpdate time.Time `json:"last_update"`
|
|
}
|
|
|
|
type CPUInfo struct {
|
|
Model string `json:"model"`
|
|
Cores int `json:"cores"`
|
|
LogicalCores int `json:"logical_cores"`
|
|
LoadPercent float64 `json:"load_percent"`
|
|
}
|
|
|
|
type MemoryInfo struct {
|
|
TotalBytes uint64 `json:"total_bytes"`
|
|
AvailableBytes uint64 `json:"available_bytes"`
|
|
UsedPercent float64 `json:"used_percent"`
|
|
}
|
|
|
|
type DiskInfo struct {
|
|
Drive string `json:"drive"`
|
|
Label string `json:"label,omitempty"`
|
|
TotalBytes uint64 `json:"total_bytes"`
|
|
FreeBytes uint64 `json:"free_bytes"`
|
|
UsedPercent float64 `json:"used_percent"`
|
|
Filesystem string `json:"filesystem,omitempty"`
|
|
}
|
|
|
|
type SvcInfo struct {
|
|
Name string `json:"name"`
|
|
DisplayName string `json:"display_name"`
|
|
Status string `json:"status"`
|
|
StartType string `json:"start_type"`
|
|
}
|
|
|
|
// MEMORYSTATUSEX fuer GlobalMemoryStatusEx
|
|
type memoryStatusEx struct {
|
|
dwLength uint32
|
|
dwMemoryLoad uint32
|
|
ullTotalPhys uint64
|
|
ullAvailPhys uint64
|
|
ullTotalPageFile uint64
|
|
ullAvailPageFile uint64
|
|
ullTotalVirtual uint64
|
|
ullAvailVirtual uint64
|
|
ullAvailExtendedVirtual uint64
|
|
}
|
|
|
|
var (
|
|
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
|
procGlobalMemoryStatusEx = kernel32.NewProc("GlobalMemoryStatusEx")
|
|
procGetTickCount64 = kernel32.NewProc("GetTickCount64")
|
|
procGetSystemTimes = kernel32.NewProc("GetSystemTimes")
|
|
)
|
|
|
|
// Collect sammelt alle Systemdaten
|
|
func Collect() (*SystemData, error) {
|
|
wd := &WindowsData{
|
|
LastUpdate: time.Now(),
|
|
}
|
|
|
|
// Hostname
|
|
if h, err := os.Hostname(); err == nil {
|
|
wd.Hostname = h
|
|
}
|
|
|
|
// OS-Version
|
|
wd.OSVersion, wd.OSBuild = getOSVersion()
|
|
|
|
// Uptime
|
|
wd.UptimeSecs = getUptime()
|
|
|
|
// CPU
|
|
wd.CPU = getCPUInfo()
|
|
|
|
// Memory
|
|
wd.Memory = getMemoryInfo()
|
|
|
|
// Disks
|
|
wd.Disks = getDiskInfo()
|
|
|
|
// Domain
|
|
wd.Domain = getDomain()
|
|
|
|
return &SystemData{Windows: wd}, nil
|
|
}
|
|
|
|
func getOSVersion() (version, build string) {
|
|
// Registry: HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion
|
|
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
|
|
`SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
|
|
if err != nil {
|
|
return "Windows (unbekannt)", ""
|
|
}
|
|
defer k.Close()
|
|
|
|
productName, _, _ := k.GetStringValue("ProductName")
|
|
displayVersion, _, _ := k.GetStringValue("DisplayVersion")
|
|
currentBuild, _, _ := k.GetStringValue("CurrentBuild")
|
|
ubr, _, _ := k.GetIntegerValue("UBR")
|
|
|
|
if displayVersion != "" {
|
|
version = fmt.Sprintf("%s %s", productName, displayVersion)
|
|
} else {
|
|
version = productName
|
|
}
|
|
if ubr > 0 {
|
|
build = fmt.Sprintf("%s.%d", currentBuild, ubr)
|
|
} else {
|
|
build = currentBuild
|
|
}
|
|
return
|
|
}
|
|
|
|
func getUptime() int64 {
|
|
ms, _, _ := procGetTickCount64.Call()
|
|
return int64(ms) / 1000
|
|
}
|
|
|
|
func getCPUInfo() CPUInfo {
|
|
info := CPUInfo{}
|
|
|
|
// Modell aus Registry
|
|
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
|
|
`HARDWARE\DESCRIPTION\System\CentralProcessor\0`, registry.QUERY_VALUE)
|
|
if err == nil {
|
|
defer k.Close()
|
|
info.Model, _, _ = k.GetStringValue("ProcessorNameString")
|
|
info.Model = strings.TrimSpace(info.Model)
|
|
}
|
|
|
|
// Kern-Anzahl
|
|
info.LogicalCores = getLogicalCoreCount()
|
|
info.Cores = getPhysicalCoreCount()
|
|
if info.Cores == 0 {
|
|
info.Cores = info.LogicalCores
|
|
}
|
|
|
|
// CPU-Last via PowerShell (einmalig, nicht ideal aber zuverlässig)
|
|
info.LoadPercent = getCPULoad()
|
|
|
|
return info
|
|
}
|
|
|
|
type systemInfo struct {
|
|
wProcessorArchitecture uint16
|
|
wReserved uint16
|
|
dwPageSize uint32
|
|
lpMinimumApplicationAddress uintptr
|
|
lpMaximumApplicationAddress uintptr
|
|
dwActiveProcessorMask uintptr
|
|
dwNumberOfProcessors uint32
|
|
dwProcessorType uint32
|
|
dwAllocationGranularity uint32
|
|
wProcessorLevel uint16
|
|
wProcessorRevision uint16
|
|
}
|
|
|
|
func getLogicalCoreCount() int {
|
|
var si systemInfo
|
|
kernel32.NewProc("GetSystemInfo").Call(uintptr(unsafe.Pointer(&si)))
|
|
return int(si.dwNumberOfProcessors)
|
|
}
|
|
|
|
func getPhysicalCoreCount() int {
|
|
out, err := runPS("(Get-CimInstance Win32_Processor | Measure-Object -Property NumberOfCores -Sum).Sum")
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
n, _ := strconv.Atoi(strings.TrimSpace(out))
|
|
return n
|
|
}
|
|
|
|
func getCPULoad() float64 {
|
|
// GetSystemTimes: idle, kernel (includes idle), user
|
|
type filetime struct{ lo, hi uint32 }
|
|
var idle, kernel, user filetime
|
|
|
|
r, _, _ := procGetSystemTimes.Call(
|
|
uintptr(unsafe.Pointer(&idle)),
|
|
uintptr(unsafe.Pointer(&kernel)),
|
|
uintptr(unsafe.Pointer(&user)),
|
|
)
|
|
if r == 0 {
|
|
return 0
|
|
}
|
|
|
|
toUint64 := func(f filetime) uint64 { return uint64(f.hi)<<32 | uint64(f.lo) }
|
|
|
|
t1idle := toUint64(idle)
|
|
t1kernel := toUint64(kernel)
|
|
t1user := toUint64(user)
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
r, _, _ = procGetSystemTimes.Call(
|
|
uintptr(unsafe.Pointer(&idle)),
|
|
uintptr(unsafe.Pointer(&kernel)),
|
|
uintptr(unsafe.Pointer(&user)),
|
|
)
|
|
if r == 0 {
|
|
return 0
|
|
}
|
|
|
|
t2idle := toUint64(idle)
|
|
t2kernel := toUint64(kernel)
|
|
t2user := toUint64(user)
|
|
|
|
idleDelta := t2idle - t1idle
|
|
kernelDelta := t2kernel - t1kernel
|
|
userDelta := t2user - t1user
|
|
|
|
total := kernelDelta + userDelta
|
|
if total == 0 {
|
|
return 0
|
|
}
|
|
busy := total - idleDelta
|
|
return float64(busy) / float64(total) * 100.0
|
|
}
|
|
|
|
func getMemoryInfo() MemoryInfo {
|
|
var ms memoryStatusEx
|
|
ms.dwLength = uint32(unsafe.Sizeof(ms))
|
|
procGlobalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&ms)))
|
|
|
|
used := ms.ullTotalPhys - ms.ullAvailPhys
|
|
usedPct := 0.0
|
|
if ms.ullTotalPhys > 0 {
|
|
usedPct = float64(used) / float64(ms.ullTotalPhys) * 100.0
|
|
}
|
|
return MemoryInfo{
|
|
TotalBytes: ms.ullTotalPhys,
|
|
AvailableBytes: ms.ullAvailPhys,
|
|
UsedPercent: usedPct,
|
|
}
|
|
}
|
|
|
|
func getDiskInfo() []DiskInfo {
|
|
var disks []DiskInfo
|
|
|
|
// Alle logischen Laufwerke ermitteln
|
|
buf := make([]uint16, 256)
|
|
kernel32.NewProc("GetLogicalDriveStringsW").Call(
|
|
uintptr(len(buf)),
|
|
uintptr(unsafe.Pointer(&buf[0])),
|
|
)
|
|
|
|
// buf enthält null-separierte Strings ("C:\\\0D:\\\0\0")
|
|
drives := windows.UTF16ToString(buf)
|
|
for _, drive := range strings.Split(drives, "\x00") {
|
|
drive = strings.TrimSpace(drive)
|
|
if len(drive) < 2 {
|
|
continue
|
|
}
|
|
|
|
// Nur lokale Festplatten (DRIVE_FIXED = 3)
|
|
driveType := kernel32.NewProc("GetDriveTypeW")
|
|
drivePath := windows.StringToUTF16Ptr(drive)
|
|
t, _, _ := driveType.Call(uintptr(unsafe.Pointer(drivePath)))
|
|
if t != 3 { // DRIVE_FIXED
|
|
continue
|
|
}
|
|
|
|
// Speicherplatz
|
|
var freeBytesToCaller, totalBytes, freeBytes uint64
|
|
kernel32.NewProc("GetDiskFreeSpaceExW").Call(
|
|
uintptr(unsafe.Pointer(drivePath)),
|
|
uintptr(unsafe.Pointer(&freeBytesToCaller)),
|
|
uintptr(unsafe.Pointer(&totalBytes)),
|
|
uintptr(unsafe.Pointer(&freeBytes)),
|
|
)
|
|
|
|
if totalBytes == 0 {
|
|
continue
|
|
}
|
|
|
|
usedPct := float64(totalBytes-freeBytes) / float64(totalBytes) * 100.0
|
|
|
|
// Laufwerksbuchstabe ohne Backslash
|
|
letter := strings.TrimSuffix(strings.TrimSuffix(drive, `\`), "/")
|
|
|
|
disks = append(disks, DiskInfo{
|
|
Drive: letter,
|
|
TotalBytes: totalBytes,
|
|
FreeBytes: freeBytes,
|
|
UsedPercent: usedPct,
|
|
})
|
|
}
|
|
return disks
|
|
}
|
|
|
|
func getDomain() string {
|
|
out, err := runPS("(Get-WmiObject Win32_ComputerSystem).Domain")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
d := strings.TrimSpace(out)
|
|
if d == "WORKGROUP" {
|
|
return ""
|
|
}
|
|
return d
|
|
}
|
|
|
|
// ToJSON serialisiert SystemData für den Heartbeat
|
|
func (s *SystemData) ToJSON() (json.RawMessage, error) {
|
|
b, err := json.Marshal(s)
|
|
return json.RawMessage(b), err
|
|
}
|
|
|
|
// runPS führt einen PowerShell-Befehl aus und gibt stdout zurück
|
|
func runPS(cmd string) (string, error) {
|
|
out, err := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", cmd).Output()
|
|
if err != nil {
|
|
log.Printf("PowerShell-Fehler (%s): %v", cmd[:min(len(cmd), 40)], err)
|
|
return "", err
|
|
}
|
|
return strings.TrimSpace(string(out)), nil
|
|
}
|
|
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|