[Agent] Dienste- und Rollen-Inventar erfassen (Windows + Linux) — Grundlage für Funktionserkennung #215

Open
opened 2026-07-20 22:55:22 +00:00 by chinux · 1 comment
Owner

Voraussetzung fuer die Funktions-/Rollen-Ableitung in der Doku. Heute erfasst kein Agent ein Dienste- oder Software-Inventar — weder Windows noch Linux. Damit laesst sich auf Windows praktisch nichts ueber den Zweck einer VM aussagen: die Ableitung in #209 stuetzt sich auf Docker-Images, und Docker ist auf Windows-Servern faktisch nicht im Einsatz.

Was heute schon da ist

collect_ports() erfasst pro lauschendem Port bereits den Prozessnamen (Get-Process -Id $_.OwningProcess). Das ist auf Windows ein brauchbares Signal (sqlservr, w3wp, dns, Microsoft.Exchange.*) und wird bisher nur als Beiwerk in der Port-Tabelle angezeigt.

Die Zuordnungstabellen icon_slug() und name_for_port() (collect.rs:724/755) sind rein Linux-/Homelab-orientiert — Grafana, Jellyfin, Pi-hole, Portainer. Kein einziger Eintrag fuer Windows-Serversoftware.

Warum keine Positivliste gepflegter Dienste

Es ist nicht praktikabel, jede Branchensoftware der Welt im Agent zu hinterlegen. Zwei generische Filter erledigen aber fast die ganze Arbeit, ohne dass irgendetwas gepflegt werden muss:

Filter 1 — Get-WindowsFeature. Liefert ausschliesslich die installierten Rollen, typisch 15–25 von rund 250. Das ist die autoritative Antwort auf "was ist dieser Server": AD-DS, DNS, DHCP, Web-Server (IIS), FS-FileServer, RDS. Kein Rauschen.

Filter 2 — Dienste, deren Binary nicht unter C:\Windows\ liegt. Diese eine Regel entfernt praktisch alle Bordmittel (Spooler, WSearch, BITS, …). Uebrig bleibt genau das Interessante:

Software Pfad
Exchange C:\Program Files\Microsoft\Exchange Server\
DATEV C:\Program Files (x86)\DATEV\
SQL Server C:\Program Files\Microsoft SQL Server\
Backup-Agents, ERP, Branchensoftware Program Files

Auf einem RDS-Host mit 250 Diensten bleiben so etwa 15–25 uebrig.

Die beiden sind komplementaer, nicht redundant. Windows-Rollen liegen sehr wohl in System32 (dns.exe, DHCP, w3wp.exe unter inetsrv) — Filter 2 wuerde sie verwerfen. Exchange wiederum taucht in Get-WindowsFeature nicht auf. Beide Quellen sind noetig.

Performance

Get-Service / Get-CimInstance Win32_Service ist ein Aufruf an den Service Control Manager, der alle Dienste auf einmal liefert. Es gibt keine Abfrage pro Dienst; 100 oder 400 Dienste kosten praktisch dasselbe. Der Kostenfaktor ist der powershell.exe-Start (~300–800 ms), und davon laufen in collect_scan() bereits sieben parallel. Ein achtes Skript ist marginal.

Eine agentseitige Ausschlussliste wuerde daher nichts einsparen — man muesste ohnehin erst alles enumerieren und dann filtern.

Filterung gehoert auf den Server, nicht in den Agent

Der Agent meldet Fakten, das Filtern ist eine Anzeigeentscheidung (Details in #216). Gruende gegen agentseitiges Filtern:

  • Die Liste muesste an jeden Agent verteilt, versioniert und synchron gehalten werden.
  • Einblenden ginge nur in die Zukunft — versteckte Daten waeren bis zum naechsten Scan nicht rekonstruierbar.
  • Das Projekt macht es an anderer Stelle bereits richtig (admin_router.py:221, #30: display-time gating, no data loss).

Prompt fuer Claude Code

Erweitere BEIDE VM-Guest-Agents um ein Dienste- und Rollen-Inventar. Der
Agent meldet vollstaendig und ungefiltert; die Filterung passiert
serverseitig (#216). SETZT #214 VORAUS (run_ps ohne Timeout).

=== TEIL 1: WINDOWS-AGENT ===
Windows-VM-Guest-Agent/src/collect.rs — zwei neue Collectors.

--- collect_services() ---
Ein PowerShell-Aufruf, Win32_Service (nicht Get-Service — nur CIM liefert
PathName und StartName):

    Get-CimInstance Win32_Service | Select-Object Name, DisplayName, State,
      StartMode, PathName, StartName | ConvertTo-Json -Compress

Pro Dienst ausgeben:
    { "name", "display_name", "state", "start_mode",
      "path", "account", "vendor_scope": "system"|"thirdparty" }

vendor_scope wird AGENTSEITIG bestimmt (billig, und der Server braucht den
Pfad sonst nicht zu parsen): "system" wenn die aufgeloeste Binary unter
%SystemRoot% liegt, sonst "thirdparty". PathName enthaelt Argumente und
teils Anfuehrungszeichen — sauber auf den Binary-Pfad reduzieren, bevor
verglichen wird. Case-insensitiv, und %SystemRoot% aus der Umgebung lesen
statt "C:\Windows" hart zu kodieren.

WICHTIG: "account" (StartName) NICHT weglassen — ein Dienst, der unter
einem Domaenenkonto laeuft, ist ein starkes Signal fuer Serverdienste. Aber
NIE Passwoerter oder Credentials mitschicken; StartName ist nur der
Kontoname.

--- collect_roles() ---
Nur auf Server-OS sinnvoll. Erst pruefen, ob das Cmdlet existiert
(ServerManager-Modul), sonst leeres Array zurueckgeben — auf Client-Windows
gibt es Get-WindowsFeature nicht und der Aufruf wuerde nur Fehler erzeugen:

    Get-WindowsFeature | Where-Object Installed | Select-Object Name,
      DisplayName, FeatureType | ConvertTo-Json -Compress

Ausgabe: [{ "name", "display_name", "type" }]

--- Payload ---
collect_scan() um beide Felder erweitern:
    "services": [...],
    "roles":    [...]
Beide ins tokio::join! aufnehmen, Budget PS_TIMEOUT_INVENTORY aus #214.

Kein "mailcow"-artiger Stub noetig — leeres Array ist eindeutig.

=== TEIL 2: LINUX-AGENT ===
VM-Guest-Agent/src/collect/ — Aequivalent, damit beide Plattformen
dieselbe Ableitungsqualitaet liefern. Sonst hat die Doku zwei Klassen.

--- collect_services() ---
    systemctl list-units --type=service --all --no-pager --plain \
      --no-legend --output=json
(Faellt auf das Textformat zurueck, wenn --output=json nicht unterstuetzt
wird — aeltere systemd-Versionen. Parser fuer beide Faelle.)

vendor_scope-Aequivalent: Unit-Datei-Pfad ermitteln
(systemctl show -p FragmentPath). Unter /lib/systemd/ oder
/usr/lib/systemd/ -> "system"; unter /etc/systemd/ oder /usr/local/ ->
"thirdparty". Das trifft die Absicht besser als eine Paketabfrage und
kostet einen statt N Aufrufe.

Gleiche Ausgabestruktur wie Windows (name, display_name, state,
start_mode, path, account, vendor_scope), damit der Server EINEN Codepfad
hat. Feldbelegung dokumentieren, wo die Semantik abweicht.

--- roles ---
Auf Linux gibt es kein Rollenkonzept. Leeres Array. NICHT versuchen, aus
Paketen Rollen zu basteln — das ist Aufgabe der Ableitung auf dem Server.

=== TEIL 3: NUTZLASTGROESSE ===
Ein RDS-Host kann 250+ Dienste haben. Bei 79 Guests waechst last_data
spuerbar. Deshalb:
  - NUR die oben genannten Felder senden, keine Description, kein
    vollstaendiges PathName mit Argumenten (nur der Binary-Pfad).
  - Dienste mit state="Stopped" UND start_mode="Disabled" weglassen — die
    sagen ueber den Zweck der Maschine nichts aus und machen auf
    Windows-Clients einen erheblichen Teil der Liste aus.
  - Sortiert und dedupliziert senden (nach name), damit der JSONB-Diff im
    Backend stabil bleibt und nicht bei jedem Scan rauscht.

=== TEIL 4: SICHERHEIT ===
Ausschliesslich lesende Aufrufe. Kein Start/Stop, kein sc.exe config, kein
systemctl enable/disable. Der Agent darf Dienste NICHT beeinflussen. Im
Modulkopf als Kommentar festhalten.

=== TEIL 5: TESTS ===
  - PathName-Parsing: mit Anfuehrungszeichen, mit Argumenten, mit
    Netzwerkpfad, leer -> vendor_scope korrekt bzw. "unknown"
  - %SystemRoot% abweichend von C:\Windows -> Erkennung greift trotzdem
  - Get-WindowsFeature nicht vorhanden -> leeres Array, kein Fehler
  - systemd ohne --output=json -> Textparser greift
  - Disabled+Stopped werden gefiltert
  - Ausgabe ist sortiert und dedupliziert

Definition of Done

  • Windows-Agent meldet services und roles
  • Linux-Agent meldet services in identischer Struktur
  • vendor_scope trennt Bordmittel von Fremdsoftware
  • Ausschliesslich lesende Aufrufe
  • Payload sortiert, dedupliziert, ohne Ballastfelder
  • Kein Absturz auf Client-Windows oder altem systemd
Voraussetzung fuer die Funktions-/Rollen-Ableitung in der Doku. Heute erfasst **kein** Agent ein Dienste- oder Software-Inventar — weder Windows noch Linux. Damit laesst sich auf Windows praktisch nichts ueber den Zweck einer VM aussagen: die Ableitung in #209 stuetzt sich auf Docker-Images, und Docker ist auf Windows-Servern faktisch nicht im Einsatz. ## Was heute schon da ist `collect_ports()` erfasst pro lauschendem Port bereits den **Prozessnamen** (`Get-Process -Id $_.OwningProcess`). Das ist auf Windows ein brauchbares Signal (`sqlservr`, `w3wp`, `dns`, `Microsoft.Exchange.*`) und wird bisher nur als Beiwerk in der Port-Tabelle angezeigt. Die Zuordnungstabellen `icon_slug()` und `name_for_port()` (`collect.rs:724/755`) sind rein Linux-/Homelab-orientiert — Grafana, Jellyfin, Pi-hole, Portainer. Kein einziger Eintrag fuer Windows-Serversoftware. ## Warum keine Positivliste gepflegter Dienste Es ist nicht praktikabel, jede Branchensoftware der Welt im Agent zu hinterlegen. Zwei generische Filter erledigen aber fast die ganze Arbeit, ohne dass irgendetwas gepflegt werden muss: **Filter 1 — `Get-WindowsFeature`.** Liefert ausschliesslich die *installierten* Rollen, typisch 15–25 von rund 250. Das ist die autoritative Antwort auf "was ist dieser Server": AD-DS, DNS, DHCP, Web-Server (IIS), FS-FileServer, RDS. Kein Rauschen. **Filter 2 — Dienste, deren Binary nicht unter `C:\Windows\` liegt.** Diese eine Regel entfernt praktisch alle Bordmittel (Spooler, WSearch, BITS, …). Uebrig bleibt genau das Interessante: | Software | Pfad | |---|---| | Exchange | `C:\Program Files\Microsoft\Exchange Server\` | | DATEV | `C:\Program Files (x86)\DATEV\` | | SQL Server | `C:\Program Files\Microsoft SQL Server\` | | Backup-Agents, ERP, Branchensoftware | Program Files | Auf einem RDS-Host mit 250 Diensten bleiben so etwa 15–25 uebrig. **Die beiden sind komplementaer, nicht redundant.** Windows-Rollen liegen sehr wohl in System32 (`dns.exe`, DHCP, `w3wp.exe` unter `inetsrv`) — Filter 2 wuerde sie verwerfen. Exchange wiederum taucht in `Get-WindowsFeature` nicht auf. Beide Quellen sind noetig. ## Performance `Get-Service` / `Get-CimInstance Win32_Service` ist **ein** Aufruf an den Service Control Manager, der alle Dienste auf einmal liefert. Es gibt keine Abfrage pro Dienst; 100 oder 400 Dienste kosten praktisch dasselbe. Der Kostenfaktor ist der `powershell.exe`-Start (~300–800 ms), und davon laufen in `collect_scan()` bereits sieben parallel. Ein achtes Skript ist marginal. Eine agentseitige Ausschlussliste wuerde daher **nichts** einsparen — man muesste ohnehin erst alles enumerieren und dann filtern. ## Filterung gehoert auf den Server, nicht in den Agent Der Agent meldet Fakten, das Filtern ist eine Anzeigeentscheidung (Details in #216). Gruende gegen agentseitiges Filtern: - Die Liste muesste an jeden Agent verteilt, versioniert und synchron gehalten werden. - Einblenden ginge nur in die Zukunft — versteckte Daten waeren bis zum naechsten Scan nicht rekonstruierbar. - Das Projekt macht es an anderer Stelle bereits richtig (`admin_router.py:221`, #30: *display-time gating, no data loss*). --- ## Prompt fuer Claude Code ``` Erweitere BEIDE VM-Guest-Agents um ein Dienste- und Rollen-Inventar. Der Agent meldet vollstaendig und ungefiltert; die Filterung passiert serverseitig (#216). SETZT #214 VORAUS (run_ps ohne Timeout). === TEIL 1: WINDOWS-AGENT === Windows-VM-Guest-Agent/src/collect.rs — zwei neue Collectors. --- collect_services() --- Ein PowerShell-Aufruf, Win32_Service (nicht Get-Service — nur CIM liefert PathName und StartName): Get-CimInstance Win32_Service | Select-Object Name, DisplayName, State, StartMode, PathName, StartName | ConvertTo-Json -Compress Pro Dienst ausgeben: { "name", "display_name", "state", "start_mode", "path", "account", "vendor_scope": "system"|"thirdparty" } vendor_scope wird AGENTSEITIG bestimmt (billig, und der Server braucht den Pfad sonst nicht zu parsen): "system" wenn die aufgeloeste Binary unter %SystemRoot% liegt, sonst "thirdparty". PathName enthaelt Argumente und teils Anfuehrungszeichen — sauber auf den Binary-Pfad reduzieren, bevor verglichen wird. Case-insensitiv, und %SystemRoot% aus der Umgebung lesen statt "C:\Windows" hart zu kodieren. WICHTIG: "account" (StartName) NICHT weglassen — ein Dienst, der unter einem Domaenenkonto laeuft, ist ein starkes Signal fuer Serverdienste. Aber NIE Passwoerter oder Credentials mitschicken; StartName ist nur der Kontoname. --- collect_roles() --- Nur auf Server-OS sinnvoll. Erst pruefen, ob das Cmdlet existiert (ServerManager-Modul), sonst leeres Array zurueckgeben — auf Client-Windows gibt es Get-WindowsFeature nicht und der Aufruf wuerde nur Fehler erzeugen: Get-WindowsFeature | Where-Object Installed | Select-Object Name, DisplayName, FeatureType | ConvertTo-Json -Compress Ausgabe: [{ "name", "display_name", "type" }] --- Payload --- collect_scan() um beide Felder erweitern: "services": [...], "roles": [...] Beide ins tokio::join! aufnehmen, Budget PS_TIMEOUT_INVENTORY aus #214. Kein "mailcow"-artiger Stub noetig — leeres Array ist eindeutig. === TEIL 2: LINUX-AGENT === VM-Guest-Agent/src/collect/ — Aequivalent, damit beide Plattformen dieselbe Ableitungsqualitaet liefern. Sonst hat die Doku zwei Klassen. --- collect_services() --- systemctl list-units --type=service --all --no-pager --plain \ --no-legend --output=json (Faellt auf das Textformat zurueck, wenn --output=json nicht unterstuetzt wird — aeltere systemd-Versionen. Parser fuer beide Faelle.) vendor_scope-Aequivalent: Unit-Datei-Pfad ermitteln (systemctl show -p FragmentPath). Unter /lib/systemd/ oder /usr/lib/systemd/ -> "system"; unter /etc/systemd/ oder /usr/local/ -> "thirdparty". Das trifft die Absicht besser als eine Paketabfrage und kostet einen statt N Aufrufe. Gleiche Ausgabestruktur wie Windows (name, display_name, state, start_mode, path, account, vendor_scope), damit der Server EINEN Codepfad hat. Feldbelegung dokumentieren, wo die Semantik abweicht. --- roles --- Auf Linux gibt es kein Rollenkonzept. Leeres Array. NICHT versuchen, aus Paketen Rollen zu basteln — das ist Aufgabe der Ableitung auf dem Server. === TEIL 3: NUTZLASTGROESSE === Ein RDS-Host kann 250+ Dienste haben. Bei 79 Guests waechst last_data spuerbar. Deshalb: - NUR die oben genannten Felder senden, keine Description, kein vollstaendiges PathName mit Argumenten (nur der Binary-Pfad). - Dienste mit state="Stopped" UND start_mode="Disabled" weglassen — die sagen ueber den Zweck der Maschine nichts aus und machen auf Windows-Clients einen erheblichen Teil der Liste aus. - Sortiert und dedupliziert senden (nach name), damit der JSONB-Diff im Backend stabil bleibt und nicht bei jedem Scan rauscht. === TEIL 4: SICHERHEIT === Ausschliesslich lesende Aufrufe. Kein Start/Stop, kein sc.exe config, kein systemctl enable/disable. Der Agent darf Dienste NICHT beeinflussen. Im Modulkopf als Kommentar festhalten. === TEIL 5: TESTS === - PathName-Parsing: mit Anfuehrungszeichen, mit Argumenten, mit Netzwerkpfad, leer -> vendor_scope korrekt bzw. "unknown" - %SystemRoot% abweichend von C:\Windows -> Erkennung greift trotzdem - Get-WindowsFeature nicht vorhanden -> leeres Array, kein Fehler - systemd ohne --output=json -> Textparser greift - Disabled+Stopped werden gefiltert - Ausgabe ist sortiert und dedupliziert ``` ## Definition of Done - [ ] Windows-Agent meldet `services` und `roles` - [ ] Linux-Agent meldet `services` in identischer Struktur - [ ] `vendor_scope` trennt Bordmittel von Fremdsoftware - [ ] Ausschliesslich lesende Aufrufe - [ ] Payload sortiert, dedupliziert, ohne Ballastfelder - [ ] Kein Absturz auf Client-Windows oder altem systemd
Author
Owner

Kette: #214 (Timeout) → #215 (Erfassung) → #216 (Filterung/Ableitung) → #217 (UI) → #209 (Doku).

Kette: #214 (Timeout) → #215 (Erfassung) → #216 (Filterung/Ableitung) → #217 (UI) → #209 (Doku).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
chinux/theProx#215
No description provided.