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 }