[Server] Dienste-Filterung, globale Ausblendliste und Rollen-Ableitung (roles[] statt role) #216

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

Serverseitige Haelfte zu #215. Der Agent meldet vollstaendig und ungefiltert; hier entstehen die Filterung, die globale Ausblendliste und die Ableitung der Funktion einer VM.

Grundsatz: Filtern beim Anzeigen, nicht beim Erfassen

Der Agent meldet alles. Der Server entscheidet, was sichtbar ist. Das folgt dem Muster, das im Projekt bereits etabliert ist (admin_router.py:221, #30: display-time gating, no data loss), und hat drei konkrete Vorteile: keine Config-Verteilung an N Agents, Einblenden wirkt sofort statt erst beim naechsten Scan, und eine falsch gesetzte Ausblendregel vernichtet keine Daten.

Automatische Filterung — deckt den Grossteil ab

Zwei Regeln reduzieren einen RDS-Host von 250+ Diensten auf etwa 15–25, ohne dass jemand etwas pflegt:

  1. vendor_scope == "thirdparty" (Binary ausserhalb %SystemRoot% bzw. Unit ausserhalb der Distro-Pfade)
  2. state == "Running" — ein gestoppter Dienst ist kein Rollenindikator

Dazu kommen die Windows-Rollen aus Get-WindowsFeature als eigene, ohnehin kleine Quelle.

Die manuelle Ausblendliste ist damit Feinschliff, nicht Grundmechanismus.

Ausblendliste global statt pro VM

Ein Dienst wie "Acme Updater" ist auf allen 79 Guests gleich uninteressant. Pro VM auszublenden hiesse, dieselbe Entscheidung 79-mal zu treffen. Deshalb: globale Regeln nach Dienstname (mit Wildcard), plus optionaler Per-VM-Override fuer Ausnahmen.

Rolle ist mehrwertig

Ein Windows-Server ist real oft gleichzeitig AD-DC, DNS, DHCP und Fileserver. Das in einen einzelnen role-String zu pressen erzwingt eine willkuerliche Entscheidung. Deshalb roles: [] plus primary_role fuer die Anzeige — #209 wird entsprechend angepasst.


Prompt fuer Claude Code

Baue die serverseitige Verarbeitung des Dienste-Inventars aus #215:
Filterung, globale Ausblendliste und Rollen-Ableitung.
SETZT #215 UND #212 (services/guest_view.py) VORAUS.

--- 1. MIGRATION ---
server/migrations/versions/00XX_service_visibility.py
(Nummer zur Umsetzungszeit pruefen: ls server/migrations/versions/ | sort | tail -1)

Tabelle service_visibility_rules:
    id           UUID PK
    pattern      String(255)  NOT NULL   -- Dienstname, '*' als Wildcard erlaubt
    scope        String(16)   NOT NULL   -- 'global' | 'vm'
    node_name    String(255)  NULL       -- nur bei scope='vm'
    vmid         Integer      NULL       -- nur bei scope='vm'
    action       String(16)   NOT NULL   -- 'hide' | 'show'
    note         Text         NULL       -- warum ausgeblendet (Doku fuer spaeter)
    created_by   UUID         NULL
    created_at   DateTime
    Index auf (scope), (node_name, vmid)

'show' existiert, damit eine globale Ausblendung fuer eine einzelne VM
wieder aufgehoben werden kann. Aufloesungsreihenfolge:
    VM-'show' > VM-'hide' > global-'show' > global-'hide' > Automatik

--- 2. FILTERLOGIK ---
server/services/service_filter.py (neu), rein und testbar:

    def classify_services(services: list, rules: list, node_name, vmid) -> dict

Rueckgabe:
    {
      "relevant":  [...],   # angezeigt
      "hidden":    [...],   # per Regel ausgeblendet, mit "hidden_by": pattern
      "system":    [...],   # vendor_scope=system, automatisch aussortiert
      "counts": {"total": n, "relevant": n, "hidden": n, "system": n}
    }

Automatik-Regel: relevant = vendor_scope == "thirdparty" AND state == "Running".
Alles bleibt im Payload — "hidden" heisst ausgeblendet, nicht geloescht. Die
counts muessen ueberall mitgefuehrt werden, damit im UI und in der Doku
sichtbar ist, dass gefiltert wurde ("18 von 247 angezeigt").

Wildcard-Matching mit fnmatch, case-insensitiv. KEINE regulaeren Ausdruecke
— ein Nutzer soll keine ReDoS-Falle bauen koennen.

--- 3. ROLLEN-ABLEITUNG ---
server/services/role_inference.py (neu):

    def infer_roles(view: dict) -> dict
    -> {"roles": ["mail", "database"], "primary_role": "mail",
        "confidence": "high"|"medium"|"low", "evidence": [...]}

Signale, absteigend gewichtet:
  1. Windows-Rollen (roles[] aus Get-WindowsFeature) — hoechste Sicherheit.
     AD-Domain-Services -> "ad-dc"; DNS -> "dns"; DHCP -> "dhcp";
     Web-Server -> "web"; FS-FileServer -> "fileserver";
     RDS-* -> "terminalserver"
  2. Dienstnamen (nur relevant-Liste). Praefix-/Substring-Matching gegen ein
     Mapping-Dict als Modulkonstante:
     "MSExchange*" -> "mail"
     "MSSQL*", "SQLAgent*" -> "database"   (Instanzname mitfuehren!)
     "DATEV*" -> "datev"
     "Veeam*", "BackupExec*", "AcronisAgent*" -> "backup"
     Linux: "postgresql*", "mariadb*", "mysqld" -> "database";
            "nginx","apache2","caddy" -> "web"; "docker" -> "container-host";
            "bind9","named" -> "dns"; "smbd" -> "fileserver"
  3. Docker-Images (bestehende Logik aus #209)
  4. http_services (Titel/erkannter Dienst)
  5. Port-Heuristik als letzter Ausweg

confidence: "high" bei Signal 1 oder 2, "medium" bei 3/4, "low" bei 5.
evidence: Liste der konkreten Treffer ("Dienst MSExchangeIS", "Rolle DNS") —
damit in der Doku nachvollziehbar ist, WARUM eine Rolle gesetzt wurde. Ohne
das ist eine falsche Rolle nicht widerlegbar.

DATEV-Spezifika: Es werden ausschliesslich Server dokumentiert, keine
Clients. Eine DATEV-Rolle wird daher NUR gesetzt, wenn eine SQL-Instanz
(MSSQL$DATEV*) oder ein DATEV-Serverdienst laeuft — nicht schon, wenn
irgendein DATEV-Dienst existiert.

Mehrere Rollen sind der Normalfall. primary_role: die mit dem staerksten
Signal; bei Gleichstand eine feste Prioritaetsliste (Modulkonstante), damit
das Ergebnis deterministisch ist und nicht zwischen Laeufen springt.

--- 4. MANUELLER OVERRIDE ---
models/vm_card_override.py um zwei Spalten erweitern (gleiche Migration):
    roles        JSONB NULL   -- manuell gesetzte Rollenliste
    role_locked  Boolean default false

Ist role_locked gesetzt, gewinnt der manuelle Wert IMMER und die Ableitung
laeuft gar nicht erst. In Doku und UI als "manuell gesetzt" kennzeichnen —
eine Heuristik liegt manchmal daneben, und die Doku muss korrigierbar sein,
ohne dass der naechste Lauf die Korrektur ueberschreibt.

--- 5. GUEST_VIEW ---
services/guest_view.py (#212) liefert zusaetzlich:
    services_relevant[], services_hidden_count, services_total,
    windows_roles[], roles[], primary_role, role_confidence, role_evidence[],
    role_source: "inferred" | "manual"

Damit haben API, Frontend (#<D>) und Docgen (#209) dieselbe Quelle.

--- 6. API ---
    GET    /api/vms/{node}/{vmid}/services      -> klassifiziert, mit counts
    GET    /api/service-rules                   -> alle Regeln
    POST   /api/service-rules                   -> Regel anlegen
    DELETE /api/service-rules/{id}
    PATCH  /api/vms/{node}/{vmid}/roles         -> manueller Override + lock
RBAC wie bei den bestehenden VM-Endpunkten; Regeln aendern ist eine
globale Aktion -> require_superadmin. Jede Regelaenderung ins AuditLog
(action="service_rule.created"/"deleted", details = pattern + scope).

--- 7. DOCGEN (#209) ---
  - Sektion "## Dienste" listet NUR services_relevant, mit Fusszeile
    "18 von 247 Diensten angezeigt (Bordmittel und ausgeblendete
    ausgenommen)". Die Zahl gehoert hin: eine gefilterte Liste ohne
    Hinweis auf die Filterung ist irrefuehrend.
  - Sektion "## Rollen" mit roles, primary_role und der Evidenz.
  - Frontmatter: roles: [...], primary_role: "...",
    role_confidence: "...", role_source: "inferred"|"manual"
  - #209 anpassen: role -> roles (Liste). Tag pro Rolle: role/<slug>.

--- 8. TESTS ---
server/tests/test_service_filter.py, test_role_inference.py:
  - Aufloesungsreihenfolge VM-show > VM-hide > global-show > global-hide
  - Wildcard case-insensitiv; keine Regex-Interpretation
  - Automatik: system+running -> nicht relevant; thirdparty+stopped -> nicht
  - counts stimmen und summieren auf total
  - Exchange-Dienste -> "mail", confidence high, evidence gefuellt
  - AD-DS + DNS + DHCP + FileServer -> vier Rollen, primary deterministisch
  - DATEV-Dienst OHNE SQL-Instanz -> KEINE datev-Rolle
  - role_locked -> Ableitung laeuft nicht, manueller Wert gewinnt
  - Leeres services[] (Linux-Agent alt / kein Agent) -> kein Crash,
    Fallback auf Docker/Ports

Definition of Done

  • Automatik reduziert einen RDS-Host ohne manuelle Pflege auf eine lesbare Liste
  • Globale Regeln wirken auf allen Guests, Per-VM-Override moeglich
  • Ausgeblendete Dienste bleiben erhalten, Einblenden wirkt sofort
  • Gefilterte Anzahl ist ueberall sichtbar
  • Rollen sind mehrwertig, mit Evidenz und Confidence
  • Manueller Override ueberlebt jeden Lauf
  • Exchange, SQL Server, DATEV-Server und AD-DC werden zuverlaessig erkannt
Serverseitige Haelfte zu #215. Der Agent meldet vollstaendig und ungefiltert; hier entstehen die Filterung, die globale Ausblendliste und die Ableitung der Funktion einer VM. ## Grundsatz: Filtern beim Anzeigen, nicht beim Erfassen Der Agent meldet alles. Der Server entscheidet, was sichtbar ist. Das folgt dem Muster, das im Projekt bereits etabliert ist (`admin_router.py:221`, #30: *display-time gating, no data loss*), und hat drei konkrete Vorteile: keine Config-Verteilung an N Agents, Einblenden wirkt sofort statt erst beim naechsten Scan, und eine falsch gesetzte Ausblendregel vernichtet keine Daten. ## Automatische Filterung — deckt den Grossteil ab Zwei Regeln reduzieren einen RDS-Host von 250+ Diensten auf etwa 15–25, ohne dass jemand etwas pflegt: 1. `vendor_scope == "thirdparty"` (Binary ausserhalb `%SystemRoot%` bzw. Unit ausserhalb der Distro-Pfade) 2. `state == "Running"` — ein gestoppter Dienst ist kein Rollenindikator Dazu kommen die Windows-Rollen aus `Get-WindowsFeature` als eigene, ohnehin kleine Quelle. Die manuelle Ausblendliste ist damit **Feinschliff, nicht Grundmechanismus**. ## Ausblendliste global statt pro VM Ein Dienst wie "Acme Updater" ist auf allen 79 Guests gleich uninteressant. Pro VM auszublenden hiesse, dieselbe Entscheidung 79-mal zu treffen. Deshalb: globale Regeln nach Dienstname (mit Wildcard), plus optionaler Per-VM-Override fuer Ausnahmen. ## Rolle ist mehrwertig Ein Windows-Server ist real oft gleichzeitig AD-DC, DNS, DHCP und Fileserver. Das in einen einzelnen `role`-String zu pressen erzwingt eine willkuerliche Entscheidung. Deshalb `roles: []` plus `primary_role` fuer die Anzeige — #209 wird entsprechend angepasst. --- ## Prompt fuer Claude Code ``` Baue die serverseitige Verarbeitung des Dienste-Inventars aus #215: Filterung, globale Ausblendliste und Rollen-Ableitung. SETZT #215 UND #212 (services/guest_view.py) VORAUS. --- 1. MIGRATION --- server/migrations/versions/00XX_service_visibility.py (Nummer zur Umsetzungszeit pruefen: ls server/migrations/versions/ | sort | tail -1) Tabelle service_visibility_rules: id UUID PK pattern String(255) NOT NULL -- Dienstname, '*' als Wildcard erlaubt scope String(16) NOT NULL -- 'global' | 'vm' node_name String(255) NULL -- nur bei scope='vm' vmid Integer NULL -- nur bei scope='vm' action String(16) NOT NULL -- 'hide' | 'show' note Text NULL -- warum ausgeblendet (Doku fuer spaeter) created_by UUID NULL created_at DateTime Index auf (scope), (node_name, vmid) 'show' existiert, damit eine globale Ausblendung fuer eine einzelne VM wieder aufgehoben werden kann. Aufloesungsreihenfolge: VM-'show' > VM-'hide' > global-'show' > global-'hide' > Automatik --- 2. FILTERLOGIK --- server/services/service_filter.py (neu), rein und testbar: def classify_services(services: list, rules: list, node_name, vmid) -> dict Rueckgabe: { "relevant": [...], # angezeigt "hidden": [...], # per Regel ausgeblendet, mit "hidden_by": pattern "system": [...], # vendor_scope=system, automatisch aussortiert "counts": {"total": n, "relevant": n, "hidden": n, "system": n} } Automatik-Regel: relevant = vendor_scope == "thirdparty" AND state == "Running". Alles bleibt im Payload — "hidden" heisst ausgeblendet, nicht geloescht. Die counts muessen ueberall mitgefuehrt werden, damit im UI und in der Doku sichtbar ist, dass gefiltert wurde ("18 von 247 angezeigt"). Wildcard-Matching mit fnmatch, case-insensitiv. KEINE regulaeren Ausdruecke — ein Nutzer soll keine ReDoS-Falle bauen koennen. --- 3. ROLLEN-ABLEITUNG --- server/services/role_inference.py (neu): def infer_roles(view: dict) -> dict -> {"roles": ["mail", "database"], "primary_role": "mail", "confidence": "high"|"medium"|"low", "evidence": [...]} Signale, absteigend gewichtet: 1. Windows-Rollen (roles[] aus Get-WindowsFeature) — hoechste Sicherheit. AD-Domain-Services -> "ad-dc"; DNS -> "dns"; DHCP -> "dhcp"; Web-Server -> "web"; FS-FileServer -> "fileserver"; RDS-* -> "terminalserver" 2. Dienstnamen (nur relevant-Liste). Praefix-/Substring-Matching gegen ein Mapping-Dict als Modulkonstante: "MSExchange*" -> "mail" "MSSQL*", "SQLAgent*" -> "database" (Instanzname mitfuehren!) "DATEV*" -> "datev" "Veeam*", "BackupExec*", "AcronisAgent*" -> "backup" Linux: "postgresql*", "mariadb*", "mysqld" -> "database"; "nginx","apache2","caddy" -> "web"; "docker" -> "container-host"; "bind9","named" -> "dns"; "smbd" -> "fileserver" 3. Docker-Images (bestehende Logik aus #209) 4. http_services (Titel/erkannter Dienst) 5. Port-Heuristik als letzter Ausweg confidence: "high" bei Signal 1 oder 2, "medium" bei 3/4, "low" bei 5. evidence: Liste der konkreten Treffer ("Dienst MSExchangeIS", "Rolle DNS") — damit in der Doku nachvollziehbar ist, WARUM eine Rolle gesetzt wurde. Ohne das ist eine falsche Rolle nicht widerlegbar. DATEV-Spezifika: Es werden ausschliesslich Server dokumentiert, keine Clients. Eine DATEV-Rolle wird daher NUR gesetzt, wenn eine SQL-Instanz (MSSQL$DATEV*) oder ein DATEV-Serverdienst laeuft — nicht schon, wenn irgendein DATEV-Dienst existiert. Mehrere Rollen sind der Normalfall. primary_role: die mit dem staerksten Signal; bei Gleichstand eine feste Prioritaetsliste (Modulkonstante), damit das Ergebnis deterministisch ist und nicht zwischen Laeufen springt. --- 4. MANUELLER OVERRIDE --- models/vm_card_override.py um zwei Spalten erweitern (gleiche Migration): roles JSONB NULL -- manuell gesetzte Rollenliste role_locked Boolean default false Ist role_locked gesetzt, gewinnt der manuelle Wert IMMER und die Ableitung laeuft gar nicht erst. In Doku und UI als "manuell gesetzt" kennzeichnen — eine Heuristik liegt manchmal daneben, und die Doku muss korrigierbar sein, ohne dass der naechste Lauf die Korrektur ueberschreibt. --- 5. GUEST_VIEW --- services/guest_view.py (#212) liefert zusaetzlich: services_relevant[], services_hidden_count, services_total, windows_roles[], roles[], primary_role, role_confidence, role_evidence[], role_source: "inferred" | "manual" Damit haben API, Frontend (#<D>) und Docgen (#209) dieselbe Quelle. --- 6. API --- GET /api/vms/{node}/{vmid}/services -> klassifiziert, mit counts GET /api/service-rules -> alle Regeln POST /api/service-rules -> Regel anlegen DELETE /api/service-rules/{id} PATCH /api/vms/{node}/{vmid}/roles -> manueller Override + lock RBAC wie bei den bestehenden VM-Endpunkten; Regeln aendern ist eine globale Aktion -> require_superadmin. Jede Regelaenderung ins AuditLog (action="service_rule.created"/"deleted", details = pattern + scope). --- 7. DOCGEN (#209) --- - Sektion "## Dienste" listet NUR services_relevant, mit Fusszeile "18 von 247 Diensten angezeigt (Bordmittel und ausgeblendete ausgenommen)". Die Zahl gehoert hin: eine gefilterte Liste ohne Hinweis auf die Filterung ist irrefuehrend. - Sektion "## Rollen" mit roles, primary_role und der Evidenz. - Frontmatter: roles: [...], primary_role: "...", role_confidence: "...", role_source: "inferred"|"manual" - #209 anpassen: role -> roles (Liste). Tag pro Rolle: role/<slug>. --- 8. TESTS --- server/tests/test_service_filter.py, test_role_inference.py: - Aufloesungsreihenfolge VM-show > VM-hide > global-show > global-hide - Wildcard case-insensitiv; keine Regex-Interpretation - Automatik: system+running -> nicht relevant; thirdparty+stopped -> nicht - counts stimmen und summieren auf total - Exchange-Dienste -> "mail", confidence high, evidence gefuellt - AD-DS + DNS + DHCP + FileServer -> vier Rollen, primary deterministisch - DATEV-Dienst OHNE SQL-Instanz -> KEINE datev-Rolle - role_locked -> Ableitung laeuft nicht, manueller Wert gewinnt - Leeres services[] (Linux-Agent alt / kein Agent) -> kein Crash, Fallback auf Docker/Ports ``` ## Definition of Done - [ ] Automatik reduziert einen RDS-Host ohne manuelle Pflege auf eine lesbare Liste - [ ] Globale Regeln wirken auf allen Guests, Per-VM-Override moeglich - [ ] Ausgeblendete Dienste bleiben erhalten, Einblenden wirkt sofort - [ ] Gefilterte Anzahl ist ueberall sichtbar - [ ] Rollen sind mehrwertig, mit Evidenz und Confidence - [ ] Manueller Override ueberlebt jeden Lauf - [ ] Exchange, SQL Server, DATEV-Server und AD-DC werden zuverlaessig erkannt
Author
Owner

Setzt #215 und #212 voraus. Liefert roles[]/primary_role an #209 und die API an #217.

Setzt #215 und #212 voraus. Liefert `roles[]`/`primary_role` an #209 und die API an #217.
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#216
No description provided.