feat: Windows Agent v1.0.0
- 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
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user