feat: WAU-Integration + Windows Software-Inventar (v1.1.0)

Windows Agent (v1.1.0):
- Software-Inventar aus Windows-Registry (64-bit + 32-bit)
  Sammelt Name, Version, Hersteller, InstallDate aller installierten Apps
  Filtert SystemComponent-Einträge heraus, sortiert alphabetisch
  Funktioniert unter SYSTEM-Account (kein winget nötig)
- Version auf 1.1.0 gehoben

Backend:
- customer_wau_rules Tabelle (Migration automatisch)
- WAU-Regeln pro Kunde (allow/block, winget-ID + Anzeigename)
- GET/POST/DELETE /api/v1/customers/{id}/wau/rules
- GET /api/v1/customers/{id}/wau/inventory  (SQL-Aggregation aus Agenten-Daten)
- GET /api/v1/customers/{id}/wau/included   (Plaintext für WAU)
- GET /api/v1/customers/{id}/wau/excluded   (Plaintext für WAU)

Frontend:
- Kunden-Seite: Tab-Navigation (Agents | API-Keys | WAU)
- WAU-Tab pro Kunde: Allowlist + Blocklist mit Inline-Assign aus Inventar
  Allow/Block-Buttons direkt in Inventar-Zeile, winget-ID vorgeschlagen
  Installationsbefehl mit externer Backend-URL aus Systemeinstellungen
- Windows-Panel Software-Tab: Installiert-Liste aus Registry-Daten
- Windows-Panel WAU-Tab: Status (installiert/Task/letzter Lauf)
  WAU installieren via MSI-Download + msiexec (synchron)
  Jetzt ausführen, Log anzeigen
  Gewünschten Zustand herstellen (Allowlist auf Client installieren)
This commit is contained in:
cynfo3000
2026-03-10 08:46:00 +01:00
parent 7a5f4c0564
commit bf51ed8569
10 changed files with 1242 additions and 131 deletions
+92 -10
View File
@@ -6,6 +6,7 @@ import (
"log"
"os"
"os/exec"
"sort"
"strconv"
"strings"
"time"
@@ -21,16 +22,17 @@ type SystemData struct {
}
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"`
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"`
InstalledSoftware []SoftwareInfo `json:"installed_software,omitempty"`
Domain string `json:"domain,omitempty"`
LastUpdate time.Time `json:"last_update"`
}
type CPUInfo struct {
@@ -62,6 +64,13 @@ type SvcInfo struct {
StartType string `json:"start_type"`
}
type SoftwareInfo struct {
Name string `json:"name"`
Version string `json:"version,omitempty"`
Publisher string `json:"publisher,omitempty"`
InstallDate string `json:"install_date,omitempty"`
}
// MEMORYSTATUSEX fuer GlobalMemoryStatusEx
type memoryStatusEx struct {
dwLength uint32
@@ -111,6 +120,9 @@ func Collect() (*SystemData, error) {
// Domain
wd.Domain = getDomain()
// Installierte Software (Registry)
wd.InstalledSoftware = getInstalledSoftware()
return &SystemData{Windows: wd}, nil
}
@@ -359,3 +371,73 @@ func min(a, b int) int {
}
return b
}
// getInstalledSoftware liest installierte Software aus der Windows-Registry.
// Funktioniert auch unter SYSTEM-Account (kein winget noetig).
func getInstalledSoftware() []SoftwareInfo {
var result []SoftwareInfo
seen := make(map[string]bool)
regPaths := []struct {
root registry.Key
path string
}{
{registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`},
{registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall`},
}
for _, rp := range regPaths {
k, err := registry.OpenKey(rp.root, rp.path, registry.READ|registry.ENUMERATE_SUB_KEYS)
if err != nil {
continue
}
subkeys, err := k.ReadSubKeyNames(-1)
k.Close()
if err != nil {
continue
}
for _, subkey := range subkeys {
sk, err := registry.OpenKey(rp.root, rp.path+`\`+subkey, registry.READ)
if err != nil {
continue
}
name, _, _ := sk.GetStringValue("DisplayName")
sk.Close()
if name == "" || seen[name] {
continue
}
seen[name] = true
// Nochmal öffnen für weitere Felder (getrennt um Close sauber zu halten)
sk2, err := registry.OpenKey(rp.root, rp.path+`\`+subkey, registry.READ)
if err != nil {
result = append(result, SoftwareInfo{Name: name})
continue
}
version, _, _ := sk2.GetStringValue("DisplayVersion")
publisher, _, _ := sk2.GetStringValue("Publisher")
installDate, _, _ := sk2.GetStringValue("InstallDate")
// SystemComponent überspringen (Windows-interne Komponenten)
sysComp, _, _ := sk2.GetIntegerValue("SystemComponent")
sk2.Close()
if sysComp == 1 {
continue
}
result = append(result, SoftwareInfo{
Name: name,
Version: version,
Publisher: publisher,
InstallDate: installDate,
})
}
}
sort.Slice(result, func(i, j int) bool {
return strings.ToLower(result[i].Name) < strings.ToLower(result[j].Name)
})
return result
}