diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 2610642..3b058c9 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -373,6 +373,42 @@ Abnahmekriterien: Status: abgeschlossen +Verifizierte Result-DB-Filter-/Exportnachschaerfung vom 2026-09-03: Die Result DB startet und resettiert mit `Deviation = Yes`. Ein neuer `Export JSONL`-Button exportiert nicht nur die geladene Seite, sondern die vollstaendige aktuelle Filter- und Timeslot-Ergebnismenge ueber stabile Seitenabfragen. Zeile eins dokumentiert Format-/Schemaversion, Zeitpunkt, Scope, angewandte Filter und Ergebnisanzahl; danach folgt je Result genau ein vollstaendiges JSON-Objekt. Aendert sich die Ergebnismenge waehrend des Exports, wird der Download fail-closed abgebrochen. Drei fokussierte Export-/Scope-Tests und der Gesamtlauf mit 767 bestandenen und 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + +Verifizierte Client-Info-Balkenformatierung vom 2026-09-03: Die wiederverwendeten Kennzahlzeilen besitzen nun identische Label-, Balken- und Wertspalten sowie eine in jeder Zeile reservierte Statuszeile. Optionale `Restricted`-Hinweise verschieben dadurch keine Nachbarzeilen mehr; Zahlen, Einheiten und Balken bleiben ueber alle gleich breiten Karten ausgerichtet. Die kompakte Infrastructure-&-Resilience-Ausnahme behaelt ihre eigenstaendige Einspaltengeometrie. 13 fokussierte Dashboard-/Kacheltests, der Gesamtlauf mit 767 bestandenen und 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + +Verifizierter Work-Kachelvertrag vom 2026-09-03: Regulaere Finding-Kacheln verwenden die Nutzerreferenz als verbindliches Mindestformat mit 358 px Breite und 223 px Hoehe. Das Raster streckt unvollstaendige Reihen nicht mehr auf Restbreite; Hexagon, Titel und rechte Aktionsspalte bleiben im Kopf. Die Metadaten folgen einer festen semantischen Matrix aus Status, Kategorie, Severity plus Weight und Score. Die Hoehe darf bei laengerem Inhalt weiter wachsen; Pathfinder-Micro-Cards bleiben von der Work-spezifischen Anordnung ausgenommen. 16 fokussierte Kachel-/Typografie-/Pathfinder-Tests, der Gesamtlauf mit 768 bestandenen und 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + +Verifizierte Infrastructure-&-Resilience-Korrektur vom 2026-09-03: Die inhaltsreiche Dashboardkachel reserviert drei statt zwei Rasterzeilen und ist als gezielte Ausnahme nicht mehr an ein starres Quadratverhaeltnis gebunden. Lange, vollstaendige OS-Aufzaehlungen bleiben dadurch innerhalb der Kartenflaeche; Faktenraster, Werte und die uebrigen quadratischen Dashboardkacheln bleiben unveraendert. 14 fokussierte Dashboard-/Inset-Tests, der Gesamtlauf mit 769 bestandenen und 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + +Unmittelbare responsive Nutzerkorrektur vom 2026-09-03: Die feste Drei-Zeilen-Ausnahme wird durch inhaltsbasierte Rasterhoehen ersetzt. Jede Client-Info-Inhaltskarte behaelt ihre quadratische Mindestgeometrie, darf aber anhand ihres vollstaendigen Inhalts wachsen; `Infrastructure & Resilience` verwendet eine automatische Inhaltshoehe. Die breakpointabhaengige Rastereinheit bleibt als Mindesthoehe erhalten, waehrend `minmax(..., auto)` zusaetzlichen Platz ohne Ueberlauf bereitstellt. 14 fokussierte Dashboard-/Inset-Tests, der Gesamtlauf mit 770 bestandenen und 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + +Verifizierte Client-Info-Progressive-Disclosure-Nachschaerfung vom 2026-09-03: Die sechs reinen Zahlenkacheln bleiben unveraendert quadratisch. Alle neun vorhandenen Detailbereiche starten dagegen als kompakte Telemetriekarten mit Titel, bestehender Metainformation, fachlicher Primaermenge und proportionalem Mengenbalken. Ein zugaenglicher Plus-/Minus-Schalter blendet die vollstaendigen bisherigen Donut-, Balken- und Fakteninhalte je Karte unabhaengig ein beziehungsweise aus. Erfundene Mockup-Bereiche wurden nicht uebernommen. Inhaltsbasierte Rasterhoehen verhindern Ueberlauf auch im expandierten Zustand. 15 fokussierte Dashboard-, Interaktions- und Inset-Tests, der Gesamtlauf mit 774 bestandenen und 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + +Verifizierte Client-Info-Panelstil-Korrektur vom 2026-09-03: Die neun Infopanels folgen nun der ausdruecklichen Nutzerreferenz. Im minimierten Zustand ist ausschliesslich die luftige Kopfzeile `Titel — Primaermenge` mit Plus sichtbar; es gibt dort keinen Balken. Erst die aufgeklappte Ansicht zeigt hohe, grosszuegig getrennte Datenzeilen mit Label, Wert und einem 72 px dicken, breiten Mengenbalken. Assessment Status und Severity Distribution verwenden ebenfalls Mengenbalken statt Donuts, behalten aber ihre vorhandenen Filteraktionen. Faktenzeilen folgen derselben luftigen Vertikalgeometrie; die fruehere kompakte Resilience-Sonderdarstellung entfaellt. Die sechs reinen Zahlenkacheln bleiben unveraendert. 12 fokussierte Layout-/Interaktionstests, alle 774 Standardtests, Local-only- und Designpruefung sowie der Client-/Server-Produktionsbuild sind erfolgreich; 8 optionale PostgreSQL-Tests wurden erwartungsgemaess uebersprungen. Keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + +Unmittelbare Breitenkorrektur der Client-Info-Infopanels vom 2026-09-03: Die aufgeklappte Karte belegt im regulaeren Raster sechs statt weiterhin nur zwei Spalten. Unterhalb von 800 px verwendet sie alle vier, unterhalb von 560 px alle zwei verfuegbaren Spalten. Damit bleibt die bestaetigte linke Label-/Wertzone neben dem breiten rechten Balkenbereich innerhalb derselben Kartenflaeche; die minimierte Zwei-Spalten-Kopfzeile bleibt unveraendert. Der in der Kopfzeile bereits dargestellte Primaerwert wird expandiert nicht als erste Zeile dupliziert. Neutrale Analysepanels uebernehmen fuer ihre restlichen Mengen die rote, orange, gelbe, violette und bei Bedarf neutrale Reihenfolge der Referenz; fachlich bereits festgelegte Status-, Coverage-, PKI- und GPO-Farben bleiben erhalten. 12 fokussierte Layout-/Interaktionstests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + +Abschliessende bildbezogene Client-Info-Layoutkorrektur vom 2026-09-03: Die sechs quadratischen Zahlenkacheln bilden wieder eine eigene obere Sechserspur. Darunter stehen die sechs real vorhandenen Mengenpanels dauerhaft sichtbar in einem dreispaltigen Telemetrieraster; Plus/Minus steuert nur zusaetzliche Angaben und entfernt die Mengenzeilen nicht. Assessment Status und Severity Distribution sind wieder als echte Donut-Kacheln hergestellt und bilden gemeinsam mit Directory Overview die rechte Nebenspalte. Mengenzeilen verwenden die kompakte Referenzgeometrie mit 64 px Zeilenhoehe und 30 px dicken Balken; die bisherige uebergrosse 120-/72-px-Ausfuehrung und die expandierte Sechs-Spalten-Sonderbreite sind damit supersediert. Bei kleineren Viewports reduzieren sich Mengenraster und Donutspalte kontrolliert beziehungsweise stapeln sich. 12 fokussierte Layout-/Interaktionstests, die Gesamtsuite mit 774 bestandenen und 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + +Verifizierte Client-Info-Erstreihenkorrektur vom 2026-09-03: Die obere Zone teilt sich jetzt im selben 3:1-Verhaeltnis wie der Inhaltsbereich. Die sechs weiterhin quadratischen Zahlenkacheln belegen verkleinert das linke Dreiviertel; `Assessment Status` steht als Donut-Kachel direkt rechts daneben in Reihe 1. Dadurch ruecken `Severity Distribution` und anschliessend `Directory Overview` in der rechten Rail jeweils nach oben. Unterhalb von 1100 px stapelt sich die Kopfzone und die Zahlenkacheln wechseln kontrolliert auf drei, unterhalb von 800 px auf zwei Spalten. 12 fokussierte Layout-/Interaktionstests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + +Verifizierte Client-Open-Standardansicht vom 2026-09-03: Das direkte Oeffnen eines Clients beziehungsweise seiner ausgewaehlten Timeslots aus Data Hub fuehrt ab jetzt in `Client Info` statt `Work`. Projektname, Projekt-ID und die konkrete ScanRun-Auswahl werden unveraendert uebernommen; nur das initial aktive Hauptgebiet aendert sich. Bewusste spaetere Navigation zu Work, Result DB oder Pathfinder bleibt unveraendert. Neun fokussierte Data-Hub-/Header-/Clientdetailtests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine Daten-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + +Verifizierte Client-Info-Minimierkorrektur vom 2026-09-03: Alle neun Informationskacheln starten vollstaendig geoeffnet mit Minus und lassen sich nun ueber denselben zugaenglichen Schalter auf ihre reine Kopfzeile mit Plus minimieren. Das gilt einheitlich fuer die sechs Mengen-/Faktenkarten sowie Assessment Status, Severity Distribution und Directory Overview; beim erneuten Oeffnen wird der vollstaendige Inhalt wiederhergestellt. Minimierte Karten werden auch in Rasterzeilen mit hoeheren Nachbarkarten nicht mehr leer auf deren Hoehe gestreckt. Die sechs reinen Zahlenkacheln bleiben unveraendert. Die beiden gemeinsamen Kartenvertraege und alle neun konkreten Titel/Instanzen sind synthetisch beziehungsweise statisch regressionsgeprueft. 13 fokussierte Dashboard-/Interaktionstests, die Gesamtsuite mit 776 bestandenen und 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + +Verifizierte Maester-Fehlerdarstellung vom 2026-09-03: Eindeutig erkannte `Invoke-MgGraphRequest`-Ausfuehrungsfehler werden im Results-Fenster nicht mehr als unstrukturierter Inline-Rohtext dargestellt. Die eigentliche Graph-Fehlermeldung steht als lesbare Primaeraussage oben; Fehlercode, HTTP-Status, Methode und Endpunkt, Skriptquelle, Zeit sowie vorhandene Request-IDs folgen als beschriftete strukturierte Felder. Der vollstaendige unveraenderte Quelltext bleibt unter `Technical details` einklappbar. Normale Maester-Ergebnisse und andere Werkzeuge verwenden weiterhin die generische Rich-Text-Darstellung. 48 fokussierte Result-/Reader-Tests und Client-/Server-Produktionsbuild, Local-only- sowie Designpruefung sind erfolgreich. Der parallele Gesamtlauf bestaetigt 777 Tests; genau eine fachlich unabhaengige statische Result-DB-Scope-Pruefung erwartet noch die alte Quelltextform vor der bereits parallel vorhandenen `assessmentIds`-Ergaenzung, 8 optionale PostgreSQL-Tests wurden uebersprungen. Keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + +Verifizierte Client-Info-Infrastrukturtrennung vom 2026-09-03: Der Faktenblock mit `Marked findings`, AD Recycle Bin, Backupzeit, krbtgt-Aenderung, Legacy/Windows LAPS, WEF und vollstaendiger OS-Angabe ist aus `Infrastructure & Resilience` herausgeloest und steht als eigene minimierbare Kachel `Infrastructure Details` im dreispaltigen Inhaltsraster. Die Ursprungskachel enthaelt nur noch ihre Risiko-/Mengenwerte. Alle bestehenden Werte stammen unveraendert aus demselben normalisierten Environment Snapshot. Die Client-Info-Ansicht umfasst damit zehn statt neun Informationskacheln. 13 fokussierte Dashboard-/Interaktionstests, Local-only- und Designpruefung sind erfolgreich. Der Baseline-Build ist derzeit unabhaengig von dieser Aenderung durch eine parallel hinzugekommene GPO-Testfixture mit fehlendem Pflichtfeld `ImportProcessorReference.kind` blockiert; ein separater direkter Vite-Lauf war in der Sandbox wegen `spawn EPERM` nicht zulaessig. Keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + +Verifizierte Client-Info-Masonry-/Dreieckskorrektur vom 2026-09-03: Das Inhaltsraster misst jede der zehn Informationskacheln individuell und weist ihr eine eigene feingranulare Rasterspanne zu. Bei Collapse sinkt die Kartenhoehe auf die Kopfzeile; nachfolgende Karten belegen per dichtem Auto-Flow unmittelbar den frei gewordenen Platz, statt bis zum Ende einer hoeheren Nachbarkarte zu warten. Ohne `ResizeObserver` bleibt das regulaere nicht ueberlappende Raster als Fallback aktiv. Die Plus-/Minus-Zeichen sind durch die gemeinsamen SVG-Dreiecke ersetzt: unten fuer geoeffnet, rechts fuer geschlossen. 16 fokussierte Dashboard-, Interaktions-, Masonry- und Dreieckstests sowie Client-/Server-Produktionsbuild, Local-only- und Designpruefung sind erfolgreich. Der Gesamtlauf bestaetigt 781 Tests; genau eine unabhaengige statische Data-Hub-Open-Regression erwartet noch die alte einzeilige Quelltextformatierung, obwohl dieselbe `dashboard`-Navigation weiterhin mehrzeilig vorhanden ist; 8 optionale PostgreSQL-Tests wurden uebersprungen. Keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + +Verifizierte Client-Info-Spalten-/KPI-Korrektur vom 2026-09-03: Die freie Masonry-Neuanordnung ist zugunsten drei dauerhafter Inhalts-Spalten mit der Kartenverteilung 3/2/2 ersetzt. Karten wachsen und schrumpfen nur innerhalb ihrer zugewiesenen Spalte; beim Minimieren rueckt ausschliesslich der darunterliegende Inhalt derselben Spalte nach oben. Die sechs KPI-Kacheln strecken sich in der Desktop-Kopfzone auf die Hoehe des danebenstehenden `Assessment Status`, sodass zwischen Kopf- und Inhaltszone keine uebergrosse Leerstelle verbleibt. Die Reihenfolge lautet Compliance Score, Deviation, High Severity, Passed, Typed Observations, Directory Sections; nur Deviation und High Severity sind interaktiv. Alle KPI-Werte sind groesser, der Compliance Score verwendet die staerkste gemeinsame H1-Hierarchie. 15 fokussierte Dashboard-/Interaktions-/Dreieckstests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind erfolgreich. Im Gesamtlauf bestanden 780 Tests; eine unabhaengige statische Data-Hub-Open-Pruefung erwartet weiterhin eine einzeilige Quelltextformatierung der funktional vorhandenen mehrzeiligen `dashboard`-Navigation, 8 optionale PostgreSQL-Tests wurden uebersprungen. Keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + +Verifizierte Client-Info-Detailkorrektur vom 2026-09-03: Minimierte Informationskarten behalten nun mit `width: 100%` dieselbe volle Spaltenbreite wie ihre maximierte Form; nur die Kartenhoehe wird reduziert. Der werkzeugspezifische Hinweis `PingCastle` beziehungsweise dessen Engine-Version ist aus `Directory Overview` entfernt, waehrend die normalisierten Quelldaten unveraendert erhalten bleiben. 13 fokussierte Dashboard-/Interaktionstests und der Client-/Server-Produktionsbuild sind erfolgreich. Keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + +Verifizierte Client-Info-Standardansicht vom 2026-09-03: Beim Oeffnen ist ausschliesslich `Assessment Status` expandiert. Die acht uebrigen Mengen-/Faktenkarten sowie `Severity Distribution` starten als vollbreite minimierte Kopfzeilen mit rechtem Dreieck und koennen weiterhin individuell geoeffnet werden. 15 fokussierte Dashboard-/Interaktions-/Dreieckstests und der Client-/Server-Produktionsbuild sind erfolgreich. Keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. + Ziele: - Filter aus normalisierten Rollen und tatsächlichen Datenwerten erzeugen. @@ -1198,6 +1234,16 @@ Abnahmekriterien: Status: in Arbeit +Aktueller verifizierter Zusatzstatus vom 2026-09-03: Die bislang fehlende GPO-Anbindung an Work und Result DB ist als additives, timeslotgebundenes Readmodell geschlossen. Jede aktuelle rohe `PolicyObservation` wird als eigener Quelltyp `policy-observation` mit Assessment-, Artefakt-, Backup-, Parser-, Reducer-, Identity-, Wert-, Diagnose- und Locator-Provenienz projiziert. Diese Datensaetze bleiben fachlich `informational`, zeigen IOE und Deviation als nicht anwendbar und werden auch unter dem Result-DB-Standardfilter `Deviation = Yes` als Kontext sichtbar; nur die Finding Factory darf weiterhin eine echte `policy_deviation` erzeugen. Work fuegt `Group Policy · Observations` als eigenes Blatt hinzu und nimmt dieselben Zeilen in `All Records` auf, sodass `Source: Scan` dort `Group Policy` anbietet. Der freigegebene Dummy-Timeslot liefert ueber die laufende PostgreSQL-API 452 Scannerabweichungen plus 51 GPO-Beobachtungen, insgesamt 503 sichtbare Resultate. 13 fokussierte Tests und der Gesamtlauf mit 781 bestandenen sowie 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine SQL-Migration und keine Veraenderung der importierten Originaldaten; Phase 13B bleibt fuer fachliche Identity-/Regelkataloge und bestaetigte Policy-Findings in Arbeit. + +Aktueller verifizierter Zusatzstatus vom 2026-09-03: Die bislang implementierte, aber nicht erreichbare Policy-Analyseoberflaeche ist nun aus dem Data-Hub-Clientdetail zugreifbar. Eine erfolgreiche GPO-Quellenzeile verwendet keine irrefuehrende deaktivierte Scanner-Auswahlbox mehr, sondern eine eigene Quellenmarkierung und eine `View`-Aktion. Diese oeffnet die projektgebundene GPO-Analyse mit Reprocessing, Environment Profiles, Aggregation, Evaluation, Finding Factory, aktueller Summary, Historie und Review Queue; zur Stammdatenansicht kann direkt zurueckgewechselt werden. `Source: Scan` bleibt fachlich auf Scanner-Runs begrenzt. Sieben fokussierte Data-Hub-/Policy-UI-Tests, die Gesamtsuite mit 775 bestandenen und 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine API-, SQL- oder Persistenzaenderung; Phase 13B bleibt in Arbeit. + +Aktueller verifizierter Zusatzstatus vom 2026-09-03: GPO-Backups sind im ausgewaehlten Data-Hub-Timeslot nun als eigene Quellenzeilen sichtbar und werden im Quellenzaehler beruecksichtigt. Die Ansicht aktualisiert sich unmittelbar nach einer GPO-Einreichung; Queue-, Verarbeitungs-, Teil-, Erfolgs-, Recovery- und Dead-Letter-Zustaende bleiben unterscheidbar, und fehlgeschlagene Importe koennen direkt wiederholt oder geloescht werden. Der Browser bietet getrennte Eingaben fuer einen normalen GPO-Backup-Ordner und ein bereits vorhandenes ZIP. Ein Ordner wird weiterhin lokal in ein deterministisches Transport-ZIP verpackt, waehrend ein ZIP unveraendert dieselbe projekt- und timeslotgebundene, gehaertete Backend-Ingestion durchlaeuft. 14 fokussierte Data-Hub-/Transport-/HTTP-Tests sowie die Gesamtsuite mit 774 bestandenen und 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine API-, SQL- oder Persistenzaenderung; Phase 13B bleibt in Arbeit. + +Aktueller verifizierter Zusatzstatus vom 2026-09-03: Der bisher nur eingebettete fachlich leere Policy-Evaluation-Regelkatalog besitzt nun eine lokale, betriebsseitig kontrollierte Ladegrenze. Ein kuratierter JSON-Katalog wird ausschliesslich bei gemeinsam konfiguriertem Dateipfad und SHA-256 geladen, ist auf 5 MiB und regulaere Nicht-Symlink-Dateien begrenzt und wird erst nach Hash-, UTF-8-/JSON-, Struktur- und vollstaendiger fachlicher Katalogvalidierung aktiviert. Jede Abweichung verhindert den Serverstart noch vor Aufbau der Repository-Laufzeit; ohne Konfiguration bleibt der sichere leere Freigabekatalog aktiv. Drei neue synthetische Loaderregressionen, 28 fokussierte Katalog-/Evaluations-/HTTP-Tests sowie die Gesamtsuite mit 773 bestandenen und 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine Netzwerk-, API-, SQL- oder Persistenzaenderung; fachliche Regeln und Windows-Inbox-ADMX-/ADML-Inhalte bleiben bis zur autorisierten Kuratierung weiterhin leer beziehungsweise offen. + +Aktueller verifizierter Zusatzstatus vom 2026-09-03: Die offene snapshotbezogene Ergebnisdeduplizierung ist als additive aktuelle Sicht im Policy-Analyse-Readmodell umgesetzt. Die unveraenderliche Run-Historie bleibt vollstaendig erhalten; Summary und Review Queue zaehlen dagegen nur die aktiven Ketten aus nicht ersetztem Environment Profile, neuester nicht ersetzter Identity-Analyse je Artefakt, dazu passender Snapshotaggregation, neuester Evaluation je Aggregation und neuestem Finding-Run je Evaluation. Veraltete Ketten werden auch nicht mehr als ausfuehrbare Evaluate-/Finding-Aktionen angeboten. Drei Domainregressionen sowie Panel-, Actions- und HTTP-Regression sind erfolgreich; Gesamtsuite mit 770 bestandenen und 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind gruen. Keine Persistenz- oder SQL-Aenderung; das Readmodell wurde additiv erweitert und Phase 13B bleibt fuer fachliche Regel-/Inbox-ADMX-Kuratierung, weitere Preference-CSEs und Exportdetails in Arbeit. + Aktueller verifizierter Zusatzstatus vom 2026-08-26: Die Freigabegrenze des weiterhin fachlich leeren Policy-Evaluation-Regelkatalogs validiert nun alle Runtime-Enumwerte fuer Auslieferungsmechanismus, Baseline-Disposition, Comparator und Severity, blockiert ueberlappende Selektoren beziehungsweise mehrdeutige Regeln und lehnt widerspruechlich wiederverwendete Risiko- oder Abhaengigkeits-IDs fail-closed ab. Es wurden keine Policyempfehlung und keine Severity erfunden oder freigegeben. Der lokale private Artefaktspeicher wurde vom veralteten fremden Benutzerpfad auf den aktuellen LocalAppData-Pfad ausserhalb des Webroots korrigiert; Backend, App und Speicherroot sind inhaltsfrei technisch erreichbar. Neun neue Katalogregressionen, 25 fokussierte GPO-/Katalogtests, die Gesamtsuite mit 764 bestandenen und 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine API-, SQL- oder Persistenzaenderung; Phase 13B bleibt in Arbeit. Aktueller verifizierter Zusatzstatus vom 2026-08-22: Der globale Microsoft-Security-Baselinekatalog besitzt nun einen bestaetigungspflichtigen Delete-Pfad durch UI, Same-Origin-API, Service sowie Local- und PostgreSQL-Repository. Nur nicht von Policy-Evaluationen referenzierte Pakete duerfen geloescht werden; Klassifikationen und gespeicherte Analysen werden zusammen mit dem Katalogeintrag entfernt, das private Originalobjekt anschliessend best-effort bereinigt. Keine SQL-Migration; Phase 13B bleibt in Arbeit. @@ -1357,7 +1403,7 @@ Abnahmekriterien: Die naechste Umsetzung fuehrt Phase 13B auf dem konsolidierten Import-, Policy-Analyse- und Orchestrierungsunterbau fort: 1. Phase 13A ist abgeschlossen: Erkennung, technische Dokumenttypen, Source Extractors, Mappingprofile, Finding-Identität sowie allgemeine Artefakt-, Processing- und Jobgrenzen sind konsolidiert und die Scannerpersistenz verwendet den Candidate-Pfad. -2. Phase 13B ist in Arbeit: Der sichere Ingestion-Unterbau, technische Policy-Identitaeten, versionierte ADMX-/Baselineanalysen, deterministisches Reprocessing, Environment Profiles, konservative Snapshotaggregation, typisierte Evaluation, fail-closed Finding Factory, Unified-Result-Read-Projektion sowie projektgebundene Review- und Orchestrierungsoberflaechen sind umgesetzt. Als naechste vertikale Etappe folgen fachlich kuratierte Policy-Regeln und vollstaendige versionierte Windows-Inbox-ADMX-/ADML-Kataloge; danach aktuelle snapshotbezogene Ergebnisdeduplizierung, weitere Preference-CSEs und Exportdetails. Ohne RSoP/GP Results bleibt die effektive Anwendung weiterhin ausdruecklich nicht verifiziert. +2. Phase 13B ist in Arbeit: Der sichere Ingestion-Unterbau, technische Policy-Identitaeten, versionierte ADMX-/Baselineanalysen, deterministisches Reprocessing, Environment Profiles, konservative Snapshotaggregation, typisierte Evaluation, fail-closed Finding Factory, Unified-Result-Read-Projektion, projektgebundene Review-/Orchestrierungsoberflaechen und die aktuelle snapshotbezogene Ergebnisdeduplizierung sind umgesetzt. Als naechste vertikale Etappe folgen fachlich kuratierte Policy-Regeln und vollstaendige versionierte Windows-Inbox-ADMX-/ADML-Kataloge; danach weitere Preference-CSEs und Exportdetails. Ohne RSoP/GP Results bleibt die effektive Anwendung weiterhin ausdruecklich nicht verifiziert. 3. Erst nach verifizierter Phase 13B den kontrollierten Firmenbetrieb aus Phase 14 mit SSO, Rollen, HTTPS, Monitoring, Backup/Restore und Aufbewahrung fortsetzen. Die technische Reihenfolge ist bis einschliesslich Phase 13A umgesetzt. Phase 13B baut auf dem verifizierten sicheren Ingestion-Unterbau auf und reicht inzwischen produktiv von der begrenzten Parser-/Workerverarbeitung ueber unveraenderliche Analyse-, Profil-, Aggregations- und Evaluationslaeufe bis zur streng evidenzgebundenen Finding Factory und ihrer gemeinsamen Unified-Result-Leseprojektion. Das Backend leitet projektgebundene Aktionsbereitschaft aus aktiven Revisionen und exakt passenden Referenzdaten ab; der Datahub fuehrt Environment-Profile-Anlage, Reprocessing, Aggregation, Evaluation und Finding Factory ueber die autoritativen idempotenten Services aus. Der produktive Regelkatalog bleibt bis fachlicher Kuratierung leer, und die vorhandenen Microsoft-Baselines bleiben ohne vollstaendige Windows-Inbox-ADMX-/ADML-Kataloge partial; dadurch erzeugt dieser reale Bestand weiterhin nur Review und keine Findings. @@ -1368,14 +1414,39 @@ Aktualisierung nach dem Schritt-9-Vertikalschnitt vom 2026-08-04: Versionierte E Aktualisierung nach dem Schritt-10/11-Finding-Vertikalschnitt vom 2026-08-04: Finding Factory, persistente Policy-Finding-Occurrences, projektgebundene API und Unified-Result-Read-Projektion sind fuer ausschliesslich vollstaendig belegte `eligible` konfigurierte Abweichungen umgesetzt. Unvollstaendige Evidenz bleibt als Factory-Ablehnung ausserhalb des Resultmodells; Scope und Wirksamkeit bleiben ausdruecklich unbekannt beziehungsweise nicht verifiziert. Der aktive produktive Katalog bleibt bis zur fachlichen Freigabe setting-spezifischer Regeln leer und erzeugt daher weiterhin keine Findings. Als naechster Vertikalschnitt folgt die UI fuer Konflikte, Katalog-/Parserluecken, partielle Analysen und unaufgeloeste Identitaeten; danach folgen fachliche Regelkuratierung und vollstaendige Windows-Inbox-ADMX-Kataloge. Weitere Preference-CSEs bleiben ein getrenntes Arbeitspaket. Aktualisierung nach dem Schritt-11-UI-Vertikalschnitt vom 2026-08-04: Konflikte, partielle Analysen, Katalog-/Parserluecken, unaufgeloeste Identitaeten, unbekanntes Targeting und Finding-Factory-Ablehnungen sind nun projektgebunden ueber ein typisiertes Readmodell, eine read-only API und den Datahub sichtbar. Findings und Review bleiben getrennt; Laufhistorie wird nicht als deduplizierter aktueller Kundenzustand bezeichnet, und die fehlende RSoP-/GP-Results-Evidenz bleibt sichtbar. Der unmittelbar folgende Orchestrierungsvertikalschnitt erweitert diese read-only Ansicht um bedienbare Environment-Profile-, Reprocessing-, Aggregations-, Evaluations- und Finding-Factory-Aktionen; fachlich kuratierte Regel- und Windows-Inbox-ADMX-Kataloge, weitere Preference-CSEs und Exportdetails bleiben getrennte Arbeitspakete. Aktualisierung nach dem Schritt-11-Orchestrierungsvertikalschnitt vom 2026-08-04: Environment Profiles und ihre Revisionen koennen nun im Datahub angelegt werden; GPO-Reprocessing, Aggregation, Evaluation und Finding Factory sind ueber serverseitig abgeleitete Ready-/Review-only-/Completed-/Blocked-Aktionen bedienbar. Katalogauswahl, aktive Revisionen und technische Quellen werden im Backend-Readmodell bestimmt, waehrend jede Mutation ihre bestehenden Fachinvarianten erneut prueft. Als naechste Vertikalschnitte folgen die fachlich kuratierten Policy-Regeln und vollstaendigen Windows-Inbox-ADMX-Kataloge sowie danach aktuelle snapshotbezogene Ergebnisdeduplizierung, weitere Preference-CSEs und Exportdetails. +Aktualisierung nach der snapshotbezogenen Ergebnisdeduplizierung vom 2026-09-03: Das Policy-Analyse-Readmodell trennt nun den aktuellen aktiven Snapshotpfad von der weiterhin vollstaendigen Audit-Historie. Summary, Review Queue und ausfuehrbare Folgeaktionen verwenden nur die aktive Revisionskette; historische Evaluationen bleiben separat sichtbar. Als naechste fachliche Etappen verbleiben kuratierte Policy-Regeln und vollstaendige Windows-Inbox-ADMX-/ADML-Kataloge, danach weitere Preference-CSEs und Policy-Exportdetails. +Aktualisierung nach der Katalog-Ladegrenze vom 2026-09-03: Fachlich freigegebene Policy-Regeln koennen nun als lokal betriebener, SHA-256-gepinnter und vollstaendig validierter JSON-Katalog reproduzierbar aktiviert werden; der Default bleibt leer und fail-closed. Als naechster fachlicher Schritt sind ein autorisiert kuratierter Regelinhalt und vollstaendige versionierte Windows-Inbox-ADMX-/ADML-Quellen erforderlich; weitere Preference-CSEs und Policy-Exportdetails bleiben danach offen. ## 9. Bereits getroffene Architekturentscheidungen | Datum | Entscheidung | Begründung | +| 2026-09-03 | Die verbindliche Client-Info-Startansicht expandiert nur `Assessment Status`; alle neun anderen Informationskarten starten minimiert. | Der Gesamtstatus bleibt ohne Interaktion sichtbar, waehrend die darunterliegende feste 3/2/2-Matrix und die rechte Rail als kompakte, stabile Navigation beginnen. Inhalte werden nicht entfernt und bleiben ueber die Standarddreiecke einzeln erreichbar. | +| 2026-09-03 | Minimierte Client-Info-Karten bleiben ueber die volle Breite ihrer festen Spalte gespannt. `Directory Overview` zeigt keine werkzeugspezifische PingCastle-Engine-Version mehr. | Collapse soll ausschliesslich die Hoehe reduzieren und darf die stabile Spaltengeometrie nicht veraendern. Die Directory-Karte beschreibt den Verzeichniszustand; eine Scanner-Engine-Version gehoert nicht zu diesem fachlichen Ueberblick und bleibt nur in der unveraenderten Provenienz erhalten. | +| 2026-09-03 | Das Client-Info-Hauptraster verwendet drei feste Karten-Spalten mit der dauerhaften Zuordnung 3/2/2 und keine dichte Masonry-Neuverteilung mehr. Collapse veraendert nur die Hoehe innerhalb derselben Spalte. Die Desktop-KPI-Matrix streckt ihre sechs Karten auf die Hoehe von `Assessment Status`; ihre feste Reihenfolge ist Compliance Score, Deviation, High Severity, Passed, Typed Observations, Directory Sections. Nur Deviation und High Severity sind Filteraktionen. | Die ausdrueckliche Nutzeranforderung priorisiert stabile raeumliche Orientierung gegenueber global maximaler Verdichtung. Feste Spalten verhindern Positionswechsel beim Minimieren und Maximieren; die gemeinsame Kopfhoehe beseitigt die grosse Leerstelle. Begrenzte Klickziele vermeiden versehentliche Filterwechsel, waehrend die staerkere Compliance-Hierarchie den wichtigsten Gesamtwert hervorhebt. Diese Entscheidung supersediert die unmittelbar folgende Masonry-Entscheidung. | +| 2026-09-03 | Rohe aktuelle GPO-Policy-Beobachtungen werden als eigener Unified-Result-Quelltyp `policy-observation` timeslotgebunden in Result DB und Work dargestellt. Sie sind Kontext-/Reviewdaten mit nicht anwendbaren Finding-Achsen und werden niemals allein durch die Sichtprojektion zu Abweichungen oder Findings. Diese Nutzerkorrektur supersediert fuer die gemeinsame Work-Ansicht die unmittelbar folgende fruehere Beschraenkung von `Source: Scan`: Dort erscheint nun auch `Group Policy`; im Data-Hub bleibt das Backup weiterhin kein auswaehlbarer Scanner-Run. | GPO-Inhalte muessen nach dem Import konkret auffindbar und exportierbar sein, auch solange Identity-, Baseline- oder Risikokataloge noch keine bestaetigte Abweichung erlauben. Die getrennte Quellart schliesst die Sichtbarkeitsluecke, ohne die konservative Finding-Factory-Grenze aufzuweichen. | +| 2026-09-03 | GPO-Backups verwenden im Timeslot keine Scanner-Auswahlbox. Eine eigene Quellenmarkierung und `View` oeffnen die projektgebundene Policy-Analyse; `Source: Scan` bleibt ausschliesslich der Provenienzfilter fuer Scanner-Runs. | Ein GPO-Backup ist verarbeitbare Policy-Evidenz, aber kein kanonischer Scanner-Run. Eine deaktivierte Checkbox suggeriert fehlende Berechtigung oder Nichtauswaehlbarkeit, waehrend eine explizite Anzeigeaktion die vorhandenen Analysedaten erreichbar macht, ohne GPO-Evidenz faelschlich in den Scanner-Scope umzudeuten. | +| 2026-09-03 | GPO-Backups werden im Data-Hub-Timeslot als eigene Quellen neben Scanner-Reports dargestellt. Ordnerauswahl und vorhandenes ZIP sind getrennte UI-Wege, verwenden aber dieselbe projekt- und timeslotgebundene Backend-Ingestion; nur der Ordnerweg erzeugt zuvor lokal das deterministische Transport-ZIP. | Die bislang bereits gespeicherten GPO-Quellen duerfen nicht unsichtbar bleiben oder als Scanner-Run erscheinen. Zwei eindeutige Eingaben vermeiden Missverstaendnisse, ohne die gehärtete Validierungs-, Speicher- und Jobgrenze zu duplizieren oder aufzuweichen. | +| 2026-09-03 | Ein kuratierter Policy-Evaluation-Regelkatalog wird nur aus einer lokalen regulaeren Datei geladen, wenn Pfad und erwarteter SHA-256 gemeinsam konfiguriert sind. Der Hash wird vor Parsing und fachlicher Validierung geprueft; ohne Konfiguration bleibt der eingebettete leere Freigabekatalog aktiv. | Kataloginhalte bestimmen sicherheitsrelevante Abweichungen und Severities. Eine explizite Hashbindung macht den freigegebenen Stand betriebsseitig nachvollziehbar, verhindert stillen Dateidrift und bewahrt den local-only Betrieb ohne automatischen Download oder implizite Aktivierung. | +| 2026-09-03 | Das Policy-Analyse-Readmodell fuehrt neben der unveraenderlichen Run-Historie eine explizite aktuelle Sicht. Aktuell sind nur die durch aktive Profil- und Identity-Revisionen erreichbaren Aggregationen mit jeweils ihrer neuesten Evaluation und ihrem neuesten Finding-Run; Summary, Review Queue und Folgeaktionen werden daraus abgeleitet. | Reprocessing darf alte Konflikte, Katalogluecken, Abweichungen und Findings nicht als aktuellen Kundenzustand doppelt zaehlen. Gleichzeitig muss jeder fruehere Lauf fuer Audit und Reproduzierbarkeit erhalten bleiben. Dieselbe Aktivkettenselektion fuer Anzeige und Aktionen verhindert Bedienung auf veralteten Analysestaenden. | |---|---|---| | 2026-09-11 | ASSESSOR erweitert den unveraenderten externen `result_type`-Katalog additiv um `vulnerability_finding`. Defender-for-IoT-CVEs erhalten bei fehlendem Quellstatus deklarativ `outcome=warning`; sie sind damit Abweichungen, bleiben aber `is_ioe=false`. `environment-observation` bleibt eine Herkunftsart mit `result_type=informational`. | Eine konkrete CVE auf betroffenen Devices ist weder Compliance-Check noch Policy-Abweichung oder blosse Inventarinformation. `exposure_finding` wuerde die vorhandene Exposure-State-/IOE-Semantik unnoetig auf Schwachstellen ausdehnen. Der eigene Typ trennt Schwachstellenbefund, Dringlichkeit und bestaetigte Exposure weiterhin sauber. | | 2026-09-10 | NVD `configurations` bleiben als top-level Konfigurationsgruppen erhalten. Vulnerable CPE-Matches bilden die betroffene Software je Gruppe; nicht vulnerable Matches derselben Gruppe werden getrennt als `Running on/with` gespeichert und dargestellt. Das Enrichment-Recordformat ist dafuer Version 2; Version-1-Flachsaetze bleiben lesbar, muessen fuer die verlustfreie Gruppierung aber erneut von NVD abgerufen werden. | Die NVD-Oberflaeche und API beschreiben eine logische Konfiguration aus betroffener Software und ihren Laufzeit-/Hardwarebedingungen. Ein flaches Match-Array ueberzaehlt Konfigurationen und verliert genau diese Beziehung. Die Gruppierung muss deshalb vor der Persistenz im externen Referenzadapter erhalten werden. | | 2026-09-10 | CVE-Enrichment ist ein globaler, nach normalisierter CVE-ID identifizierter Referenzbestand und weder Bestandteil der tenant-/timeslotgebundenen FindingOccurrence noch der werkzeuggebundenen, durch Reimport ersetzbaren FindingDefinition. Defender-Import und NVD-Enrichment bleiben getrennt; nur eine ausdrueckliche Aktion fuer eine bereits gespeicherte Defender-CVE darf den festen NVD-Endpunkt abfragen. | Beschreibung, CVSS-Metriken, CWE, Referenzen und betroffene Konfigurationen beschreiben die CVE selbst. Ein gemeinsamer Datensatz vermeidet redundante, auseinanderlaufende Kopien und macht vorhandene Anreicherung fuer spaetere Mandanten automatisch wiederverwendbar, waehrend der Import ohne Netzabhaengigkeit reproduzierbar bleibt. | +| 2026-09-03 | Das Client-Info-Hauptraster verwendet eine Progressive-Enhancement-Masonry-Berechnung auf Basis real gemessener Kartenhoehen. Erst nach erfolgreicher Messung aktiviert es feine 1-px-Tracks und dichten Auto-Flow; ResizeObserver aktualisiert die Spannen bei Collapse, Inhalts- und Breitenwechseln. Zustandsbuttons verwenden das gemeinsame `TriangleIcon` unten/rechts. | Ein regulaeres CSS Grid reserviert pro Zeile die Hoehe der groessten Karte und kann kuerzere geschlossene Karten nicht unabhaengig nach oben packen. Die gemessene Span-Berechnung ermoeglicht echtes Nachruecken, waehrend das vor der Messung aktive Standardraster einen sicheren Fallback ohne Ueberlagerung bildet. Gemeinsame SVG-Dreiecke sind schrift- und kodierungsunabhaengig und entsprechen der bestehenden App-Konvention. | +| 2026-09-03 | Die Environment-Fakten `Marked findings`, AD Recycle Bin, Backup/krbtgt, LAPS, WEF und OS bilden eine eigene Client-Info-Kachel `Infrastructure Details`; `Infrastructure & Resilience` bleibt eine reine Mengen-/Risikokarte. Beide verwenden denselben gemeinsamen minimierbaren Kartenvertrag. | Risikoindikatoren und beschreibende Infrastrukturwerte sind zwei unterschiedliche Informationsarten. Eine eigene Kachel bewahrt die luftige Faktengeometrie, verhindert eine ueberlange Mischkarte und laesst beide Gruppen unabhaengig minimieren, ohne ihre gemeinsame Snapshotquelle zu veraendern. | +| 2026-09-03 | Maester-`Invoke-MgGraphRequest`-Ausfuehrungsfehler erhalten eine additive strukturierte Praesentation mit Primaermeldung, technischen Schluesselfeldern und standardmaessig eingeklapptem vollstaendigem Rohtext. Die Erkennung und Extraktion liegt in einem reinen werkzeugspezifischen Parser ausserhalb der React-Komponente; nicht erkannte Texte fallen verlustfrei auf die bestehende Rich-Text-Darstellung zurueck. | Lange PowerShell-/HTTP-/JSON-Ketten sind in einer Fliesstextzeile nicht diagnostizierbar. Die Trennung hebt die handlungsrelevante Graph-Ursache hervor, bewahrt Request-Korrelation und Originalbeleg und haelt source-spezifische Interpretation aus der allgemeinen Darstellungskomponente heraus. | +| 2026-09-03 | Alle neun Client-Info-Informationskacheln verwenden einen gemeinsamen vollstaendig minimierbaren Interaktionsvertrag: initial geoeffnet mit Minus, minimiert ausschliesslich die Kopfzeile mit Plus und intrinsischer Kopfzeilenhoehe, erneut geoeffnet mit vollstaendigem Inhalt. Die sechs reinen Zahlenkacheln bleiben nicht minimierbar und unveraendert. | Der vorherige Schalter veraenderte bei den sechs Mengen-/Faktenkarten nur optionale Zusatzangaben und fehlte bei Donutkarten teilweise, obwohl die Bedienung als Minimieren verstanden wird. Ein identischer Vertrag fuer beide gemeinsamen Kartenkomponenten plus ein explizites `align-self: start` im minimierten Zustand verhindert wirkungslose Schalter und leere, auf Nachbarhoehe gestreckte Karten. | +| 2026-09-03 | Die neueste bildlich bestaetigte Nutzerreferenz ersetzt den unmittelbar zuvor festgelegten minimierten Mengenbalken: Alle neun Client-Info-Infopanels zeigen minimiert nur `Titel — Primaermenge` und Plus. Expandiert verwenden alle mengenbezogenen Inhalte hohe Zeilen mit breitem 72-px-Mengenbalken; Assessment und Severity werden nicht mehr als Donut dargestellt. Faktenzeilen nutzen dieselbe luftige Vertikalordnung. Die sechs Zahlenkacheln bleiben unveraendert. | Der Balken ist Detailinhalt und darf die minimierte Uebersicht nicht erneut verdichten. Eine einheitliche expandierte Zeilengeometrie setzt die Referenz ueber alle Infopanels um, bewahrt reale Werte und vermeidet sowohl erfundene Verlaeufe als auch lokale kompakte Sonderlayouts. | +| 2026-09-03 | Die abschliessende Nutzerreferenz supersediert die unmittelbar vorherige reine Collapsed-/Expanded-Geometrie: Sechs Zahlenkacheln stehen oben; sechs Mengenpanels bleiben darunter permanent als dreispaltige Uebersicht sichtbar. Plus/Minus betrifft nur Zusatzdetails. Assessment und Severity bleiben Donut-Kacheln in einer rechten Nebenspalte, Directory Overview folgt dort als Faktenkarte. | Das Zielbild zeigt die Mengenzeilen bereits bei einem Pluszustand und fordert die Donuts ausdruecklich daneben. Eine getrennte Hauptmatrix und Verteilungsspalte bildet diese Hierarchie direkt ab, ohne reale Kennzahlen zu verbergen oder Donutverteilungen in Mengenbalken umzudeuten. | +| 2026-09-03 | Die Kopfzone von Client Info verwendet dasselbe 3:1-Verhaeltnis wie Hauptmatrix und rechte Rail: sechs verkleinerte quadratische Kennzahlen links, `Assessment Status` rechts in derselben ersten Reihe. | Der erste Donut soll ohne eine neue Zwischenreihe direkt neben den Zahlenkacheln stehen. Die gemeinsame Spaltenteilung richtet Kopf- und Inhaltszone aus und laesst die verbleibenden Rail-Karten automatisch nachruecken. | +| 2026-09-03 | `Open Client` aktiviert nach Uebernahme des Projekt-/Timeslot-Scope standardmaessig den Hauptbereich `Client Info`; `Work` bleibt eine anschliessend bewusst waehlbare Ansicht. | Die Client-Informationsansicht ist der vereinbarte Einstieg in einen geoeffneten Mandanten. Der Scopewechsel und die Wahl des ersten Darstellungsbereichs bleiben getrennte Verantwortlichkeiten. | +| 2026-09-03 | Ein expandiertes Client-Info-Infopanel belegt regulaer sechs Rasterspalten und an den Breakpoints die komplette verfuegbare Rasterbreite; minimiert bleibt es zwei Spalten breit. Der Kopf-Primaerwert wird im Body nicht wiederholt; neutrale Mengenserien verwenden die referenzierte ordinale Farbfolge. | Die Referenz besitzt eine breite linke Label-/Wertzone und eine ebenso breite rechte Visualisierungszone. Diese Geometrie ist in einer nur zwei Spalten breiten Karte nicht darstellbar und fuehrt zum sichtbaren Ueberlauf der Mengenbalken. Das Entfernen der ersten Doppelzeile stellt zugleich die Informationshierarchie und die vier sichtbaren Referenzreihen wieder her. | +| 2026-09-03 | Die ausdrueckliche AMD-Adrenalin-inspirierte Nutzerreferenz ersetzt den frueheren Quadratvertrag ausschliesslich fuer die neun Client-Info-Detailkarten. Diese starten kompakt mit einer Primaermenge und einem Mengenbalken; Plus/Minus steuert die vollstaendigen bisherigen Details je Karte. Die sechs Zahlenkacheln bleiben unveraendert quadratisch. | Progressive Disclosure erhoeht die Uebersicht, ohne Daten zu entfernen. Mengenbalken stellen den aktuellen Umfang korrekt dar, waehrend erfundene Verlaufsgraphen ohne Zeitreihe eine nicht vorhandene Entwicklung suggerieren wuerden. Die Trennung bewahrt die bestaetigte Zahlenzeile und macht alle real vorhandenen Detailbereiche bedarfsgesteuert erreichbar. | +| 2026-09-03 | Die responsive Nutzerkorrektur ersetzt die unmittelbar zuvor festgelegten drei Rasterzeilen: Client-Info-Rasterzeilen verwenden eine breakpointabhaengige Mindestgroesse mit inhaltsbasiertem Auto-Wachstum. Inhaltskarten sind mindestens quadratisch und wachsen bei Bedarf; `Infrastructure & Resilience` besitzt eine automatische Inhaltshoehe. | Eine feste Zahl von Rasterzeilen ist kein responsives Verhalten und kann bei anderer Breite oder Datenmenge erneut zu viel oder zu wenig Platz liefern. Inhaltsbasierte Trackgroessen reagieren sowohl auf den Viewport als auch auf die tatsaechlichen Werte, ohne Clipping oder starre Leerflaeche. | +| 2026-09-03 | Die inhaltsreiche Dashboardkachel `Infrastructure & Resilience` belegt drei Rasterzeilen und hebt ausschliesslich fuer sich das starre Quadratverhaeltnis auf; alle anderen Client-Info-Karten behalten die gemeinsame quadratische Geometrie. | Die vollstaendige dynamische OS-Liste darf weder abgeschnitten werden noch aus der Kachel herauslaufen. Eine gezielte Hoehenerweiterung bewahrt die bestehende Faktenstruktur und den dichten Gesamtrastervertrag, ohne andere Karten unnoetig zu vergroessern. | +| 2026-09-03 | Regulaere Work-Finding-Kacheln besitzen eine feste Inhaltsbreite von 358 px und eine Mindesthoehe von 223 px; sie duerfen fuer vollstaendigen Inhalt nach unten wachsen. Die Metadatenreihenfolge ist Status, Kategorie, Severity/Weight und Score. Pathfinder-Micro-Cards behalten ihre eigenstaendige kompakte Geometrie. | Das freigegebene Referenzformat muss die untere Groessengrenze jeder Matrixkachel bilden. Eine feste Breite verhindert das Aufziehen der letzten Reihe; eine Mindest- statt Fixhoehe bewahrt lange Titel und Metadaten ohne Clipping. Die semantische Badge-Matrix erzeugt unabhaengig von der Datensatzkombination dieselbe Lesereihenfolge. | +| 2026-09-03 | Die Result DB verwendet `Deviation = Yes` als initialen und durch `Reset filters` wiederhergestellten Arbeitszustand. Ihr LLM-Export ist versioniertes JSONL: ein Manifest mit aktivem Timeslot-/Projekt-Scope und allen Nutzerfiltern, danach eine Zeile pro vollstaendigem Unified Result. | Abweichungen sind die primaere Analysemenge. JSONL bewahrt verschachtelte Evidenz und Provenienz, bleibt an Datensatzgrenzen teilbar und ist fuer LLM- sowie maschinelle Weiterverarbeitung robuster als eine flache CSV- oder unstrukturierte Markdown-Ausgabe. Der Manifest-Snapshot macht die exportierte Auswahl reproduzierbar. | +| 2026-09-03 | Balkenlisten in den quadratischen Client-Info-Karten verwenden dieselben drei Spalten fuer Label, Track und Wert sowie eine stets reservierte zweite Wertzeile fuer optionale Quellenhinweise. Zahlen und Einheiten bleiben in einer nicht umbrechenden Wertgruppe; die Resilience-Kompaktkarte ueberschreibt diesen Vertrag lokal mit ihrer bestehenden Einspaltenanordnung. | Eine optionale Zusatzzeile darf weder die Hoehe einzelner Eintraege noch Track- oder Wertbreiten zwischen gleichartigen Karten veraendern. Gemeinsame Spaltenrollen machen die Werte visuell vergleichbar, ohne Daten oder Kartenstruktur umzudeuten. | | 2026-08-26 | Ein Policy-Evaluation-Regelkatalog darf den Status `released` nur tragen, wenn seine Runtime-Vertraege geschlossen validiert sind und fuer dieselbe technische Identitaet, denselben Auslieferungsmechanismus und ueberlappende Produkt-/Versions-/Rollen-/Scope-Kontexte hoechstens eine Regel aufloesbar ist. Wiederverwendete Risiko- und Abhaengigkeits-IDs muessen katalogweit dieselbe Definition besitzen. | Der kuenftige fachlich kuratierte Katalog wird an einer sicherheitsrelevanten Finding-Grenze ausgefuehrt. TypeScript-Typen allein schuetzen weder geladene noch historisch persistierte Runtime-Daten; Mehrdeutigkeit oder unbekannte Enumwerte muessen deshalb vor jeder Freigabe fail-closed blockieren. | | 2026-08-25 | Die appweite persistente Markierungs- und Auswahlfarbe ist auf ausdrueckliche Nutzerfestlegung immer Gelb und wird in beiden Themes ueber `--marking-color` gefuehrt. Kachelauswahl, Auswahlrahmen/-marker, gemerkte Eintraege und Graphauswahl verwenden diesen Token. Blau bleibt Tastaturfokus; Gruen, Rot, Orange und Severitytoene bleiben fachliche Statussignale. | Ein einzelner semantischer Markierungston verhindert violette oder skinabhaengig wechselnde Auswahlkonturen wie im Datahub-Screenshot und bewahrt gleichzeitig die Bedeutung bestehender Statusfarben. | | 2026-08-25 | Die unmittelbar zuvor fuer Client Info festgelegte flache SOC-Kachel ist auf ausdrueckliche Nutzerfestlegung der neue appweite Kachelstandard: keine sichtbare Normalzustandskontur, kein Radius und kein Schatten; Hover veraendert nur die Flaeche. Fokus, Auswahl und kleine Statusakzente bleiben funktionale Ausnahmen. Die gemeinsamen `--tile-*`-Tokens und Projektregeln tragen diesen Vertrag. | Ein zentraler Standard verhindert, dass neue oder bestehende Ansichten erneut weisse Konturen, Rundungen oder schwebende Schatten einfuehren. Flaechenkontrast und gemeinsamer Abstand erzeugen die gewuenschte ruhige, dichte SOC-Geometrie. | @@ -1644,11 +1715,31 @@ Diese Entscheidungen werden erst getroffen, wenn die jeweilige Phase ansteht ode ## 11. Risiken | Risiko | Gegenmaßnahme | +| Eine spaetere Kartenrefaktorierung kann wieder alle Client-Info-Karten standardmaessig oeffnen und damit die vereinbarte kompakte Startansicht verlieren. | Den Startzustand an jeder konkreten Karteninstanz explizit setzen und statisch pruefen: neun `initiallyExpanded={false}` sowie genau ein nicht minimierter `Assessment Status`; die gemeinsame Komponente bleibt fuer gezielte Wiederverwendung konfigurierbar. | +| Eine minimierte Client-Info-Karte kann durch intrinsische Flexbreite schmaler als ihre Spalte werden; werkzeugspezifische Provenienz kann ausserdem erneut in einen fachlichen Verzeichnisueberblick geraten. | Collapsed-Karten explizit auf `width: 100%` setzen und diese Geometrie regressionspruefen. `directoryFacts` auf fachliche Directory-Felder begrenzen; Scanner-Engine-Versionen bleiben in der Quellprovenienz statt in `Directory Overview`. | +| Client-Info-Karten koennen beim Ein-/Ausklappen zwischen Spalten wechseln und dadurch die erlernte Position verlieren; unterschiedlich hohe Kopfbereiche koennen ausserdem eine grosse Leerstelle vor der Inhaltsmatrix erzeugen. | Karten statisch in drei semantische Spalten mit der Zuordnung 3/2/2 gruppieren und innerhalb jeder Spalte per Flex-Stack anordnen. Keine dichte Auto-Platzierung oder laufzeitbasierte Spannenberechnung verwenden. KPI-Matrix und Assessment-Status in derselben gestreckten Kopfzeile fuehren; an schmalen Breakpoints kontrolliert stapeln. Spaltenzahl, Karteninstanzen, Reihenfolge und Collapse regressionspruefen. | +| Ein lokal bereitgestellter Policy-Regelkatalog kann nach fachlicher Freigabe unbemerkt veraendert, nur teilweise konfiguriert, als Symlink umgebogen oder mit ungueltigen Runtime-Strukturen geladen werden und dadurch andere Findings erzeugen. | Pfad und SHA-256 als untrennbares Konfigurationspaar verlangen, nur regulaere Nicht-Symlink-Dateien bis 5 MiB lesen, Hash vor UTF-8-/JSON-Parsing vergleichen und danach den geschlossenen Katalogvertrag fail-closed validieren. Bei jedem Fehler vor Repository- und Serverstart abbrechen; Evaluation Runs bewahren weiterhin den vollstaendigen tatsaechlich verwendeten Katalog. | +| Historische GPO-Reprocessing-Ketten koennen alte Konflikte, Reviewhinweise, Abweichungen und Findings mehrfach als aktuellen Zustand erscheinen lassen; wenn UI und Aktionsableitung unterschiedliche Auswahlregeln verwenden, bleiben ausserdem veraltete Evaluationen bedienbar. | Aktive Profile und nicht ersetzte Identity-Analysen je Artefakt als Wurzel einer gemeinsamen deterministischen Aktivkette verwenden; nur dazu passende Aggregationen, jeweils neueste Evaluationen und Finding-Runs in die aktuelle Sicht aufnehmen. Historie getrennt erhalten und dieselbe Auswahlfunktion fuer Summary, Review Queue sowie Evaluate-/Finding-Aktionen regressionspruefen. | |---|---| | Alte Defender-Importe mit Profil 1.0.0 tragen im gespeicherten Occurrence-JSON weiterhin `outcome=unknown`; wenn Work bereits die aktuelle Profilregel verwendet, koennten Projektion und SQL-Vorfilterung widerspruechlich werden. | Profil 1.1.0 als semantisch inkompatiblen Vertrag mit neuem Compatibility Key registrieren. Die unveraenderliche Migration 022 erweitert den Result-Type-Constraint; Migration 023 zieht ausschliesslich die abgeleiteten relationalen Read-Model-Spalten `result_type` und `outcome_normalized` fuer bestehende Defender-Vorkommen nach. Originalfelder, Occurrence-JSON und Profilprovenienz bleiben unveraendert. Die Leseprojektion behandelt einen nachweislich statuslosen historischen Vulnerability-Fund ebenso. Reimport/Reprocessing bleibt fuer einen vollstaendig aktuellen kanonischen Datensatz erforderlich. | | NVD kann nicht erreichbar oder rate-limitiert sein, unerwartet grosse Antworten liefern oder externe Referenz-URLs enthalten; ein API-Schluessel darf nicht an Browser oder Logs gelangen. Eine automatische Anreicherung waehrend des Imports wuerde zudem dessen Reproduzierbarkeit und Verfuegbarkeit an einen externen Dienst koppeln. | Enrichment ausschliesslich separat und nutzerausgeloest im Backend ausfuehren, nur exakt gespeicherte Defender-CVE-IDs akzeptieren, Zielorigin und API-Pfad fest verdrahten sowie Timeout und 8-MiB-Antwortgrenze erzwingen. Den optionalen Schluessel nur aus `NVD_API_KEY` als Serverheader senden, Antwortfelder normalisieren, Referenzen nur als HTTP(S)-Links darstellen und Importfehler strikt vom Enrichment-Ergebnis trennen. | | Defender-for-IoT-Summarys koennen Severity-Zahlen ohne einzelne CVEs enthalten; wiederholte Sensorzeilen koennen dieselbe Device-CVE-Beziehung mehrfach melden, und pseudonymisierte oder wiederverwendete Adressen koennen Identitaeten mehrdeutig machen. | Aggregate ohne Detail niemals in Findings umwandeln, Device-mal-CVE vor der kanonischen Projektion deduplizieren, alle Sensorzeilen als Evidenz erhalten, Identitaet nach der dokumentierten MAC-/IP-/Name-Site-Reihenfolge bilden und Konflikte beziehungsweise Reconciliation-Abweichungen diagnostizieren statt still zusammenzufuehren. | | Ein gemischtes All-Records-Blatt kann den generischen Titel `Name` auf alle Scanner anwenden und dadurch bei Defender die Device-IP statt der CVE anzeigen; zugleich kann eine rein profilgebundene Client-Info-Sonderansicht in gemischten Timeslots verschwinden. | Titel im gemischten Blatt je Datensatz aus dessen `Source: Scan`-Provenienz und deklarativem Werkzeugprofil bestimmen. Die allgemeine Client-Info-Uebersicht beibehalten und die Defender-Projektion bei vorhandenen Defender-Quellblaettern als eigenen Tab anbieten. | +| Unterschiedlich hohe oder geschlossene Client-Info-Karten koennen in einem regulaeren Zeilenraster grosse Leerraeume hinterlassen; eine unbedingte JS-Masonry-Aktivierung koennte ohne Mess-API dagegen Karten ueberlagern. | Mit dem normalen Raster starten, Kartenhoehen inklusive gemeinsamem Segmentabstand messen und erst danach 1-px-Spannen mit `grid-auto-flow: row dense` aktivieren. Raster und jede Karte beobachten, nur geaenderte Spannen schreiben und bei fehlendem ResizeObserver im sicheren Standardraster bleiben. Synthetische DOM-Hoehen und Collapse werden regressionsgeprueft. | +| Infrastruktur-Fakten koennten bei einer spaeteren Dashboardverdichtung erneut mit Risiko-Mengenwerten in einer ueberlangen Mischkarte zusammengefuehrt oder zwischen beiden Karten doppelt dargestellt werden. | `Infrastructure & Resilience` ausschliesslich mit `risk.slice(1)` speisen und den vollstaendigen Faktenblock genau einmal in `Infrastructure Details` rendern. Instanzzahl, beide Titel, Trennung und vollstaendige Snapshot-Faktenliste statisch regressionspruefen; beide Karten ueber den gemeinsamen Collapse-Test absichern. | +| Maester-Fehlertexte koennen PowerShell-Kontext, HTTP-Antwort und verschachteltes JSON in einer langen Zeile mischen; eine aggressive Umformatierung koennte dabei Diagnosewerte verlieren oder normale Ergebnisprosa faelschlich als Fehler behandeln. | Nur die eindeutige Kombination aus Maester, Ausfuehrungsfehlerformulierung und `Invoke-MgGraphRequest` strukturiert parsen. Die Primaermeldung und bekannte technische Felder additiv extrahieren, den unveraenderten Rohtext stets einklappbar erhalten und bei jeder Nichterkennung auf den generischen Renderer zurueckfallen. Parser und Darstellung mit synthetischen Fehlern regressionspruefen. | +| Eine Client-Info-Kachel koennte einen Plus-/Minus-Schalter anzeigen, ohne ihren gesamten Inhalt zu minimieren, in einer hohen Rasterzeile als leere Vollhoehenkarte stehen bleiben oder zwischen Donut- und Faktenkarten unterschiedliche Zustandslogik verwenden. | Beide gemeinsamen Informationskartenkomponenten rendern im minimierten Zustand strukturell nur die Kopfzeile, verwenden dort intrinsische statt gestreckter Rasterhoehe und binden denselben `aria-expanded`-/`aria-controls`-Schalter an den vollstaendigen Inhaltscontainer. Interaktionstests decken beide Kartentypen ab; der Layouttest prueft alle neun konkreten Instanzen, Titel und die Collapsed-Geometrie. | +| Eine feste Rasterspan-Anzahl kann bei wechselnder Dashboardbreite und unterschiedlich langen Environment-Werten erneut Leerraum oder Ueberlauf erzeugen; rein automatische Tracks ohne Mindestmass koennten dagegen die quadratische Grundgeometrie aufloesen. | Die responsive Spaltenzahl weiterhin in eine gemeinsame Mindest-Rastereinheit uebersetzen, `grid-auto-rows` jedoch als `minmax(Mindestmass, auto)` definieren. Inhaltskarten erhalten `min-content` als Untergrenze; nur die datenreiche Resilience-Kachel hebt ihr bevorzugtes Quadratverhaeltnis auf. | +| Client-Info-Infopanels koennten in minimierter Ansicht erneut Balken oder weitere Inhalte zeigen, expandiert zu kompakte Zeilen verwenden oder fuer Assessment und Severity wieder auf eine abweichende Diagrammgeometrie zurueckfallen. | Minimiert strukturell nur Kopfzeile und Plus rendern. Den gemeinsamen expandierten Mengenvertrag mit 120-px-Zeilen, 72-px-Tracks, responsive gestapelter Schmalansicht und vollstaendig wachsenden Fakten anwenden. Alle neun Panelinstanzen sowie die Abwesenheit minimierter Balken und Client-Info-Donuts regressionspruefen. | +| Eine spaetere Progressive-Disclosure-Aenderung koennte die im Zielbild permanent sichtbaren Mengenzeilen erneut ausblenden oder die beiden Verteilungen aus der rechten Donutspalte entfernen. | Kerninhalt und optionale Zusatzdetails als getrennte DOM-Bereiche rendern; nur den Zusatzbereich an `aria-expanded` binden. Hauptmatrix, rechte Rail, zwei Donutkarten und sichtbaren Kerninhalt im Pluszustand gemeinsam regressionspruefen. | +| Eine getrennte volle Kennzahlenzeile koennte `Assessment Status` erneut in eine zweite Reihe zwingen oder bei schmalen Viewports zu kleine quadratische Zahlenkacheln erzeugen. | Kopfzone als 3:1-Raster aus Zahlenmatrix und Assessment-Donut aufbauen; bei 1100 px stapeln und die Zahlenmatrix danach auf drei beziehungsweise zwei Spalten reduzieren. Vorhandene Quadrat- und Kachelvertraege regressionspruefen. | +| Der Client-Open-Ablauf koennte bei spaeteren Data-Hub-Aenderungen wieder implizit in Work landen oder den gewaehlten Timeslot-Scope verlieren. | Zielbereich und `openProject(project.id, scanRunIds)` gemeinsam am zentralen `onProjectSelected`-Callback halten und durch eine Quellregression auf `dashboard` sowie die unveraenderte ScanRun-Uebergabe absichern. | +| Ein expandiertes Infopanel koennte trotz breiter interner Balkenspalte auf der kompakten Zwei-Spalten-Kartenbreite verbleiben; Inhalte wuerden dann aus der Kachel herausragen und Nachbarkacheln ueberdecken. | Expandierte Karten auf sechs Rasterspalten setzen und an 800-/560-px-Breakpoints auf die jeweils gesamte Rasterbreite reduzieren. Desktop- und Breakpoint-Spans statisch regressionspruefen; minimierte Kartenbreite nicht veraendern. | +| Progressive Detailkarten koennten Informationen versehentlich dauerhaft verbergen, unzugaengliche Symbolschalter verwenden oder mit erfundenen Zeitreihen eine nicht vorhandene Entwicklung suggerieren. Unterschiedliche expandierte Inhaltshoehen koennten erneut ueberlappen. | Jede Karte behaelt eine sichtbare Primaermenge mit echtem proportionalem Mengenbalken und rendert den unveraenderten vollstaendigen Inhalt ueber einen beschrifteten `aria-expanded`-/`aria-controls`-Schalter. Keine Sparklines ohne historische Daten erzeugen. Inhaltsbasierte Rastertracks und eine echte Klickregression sichern Ein-/Ausblenden, Balkenwert und ueberlauffreies Wachstum. | +| Dynamische OS-Aufzaehlungen koennen die feste 2-x-2-Quadratflaeche der Infrastructure-&-Resilience-Kachel ueberschreiten und darunterliegende Dashboardbereiche ueberdecken; eine globale Kartenvergroesserung wuerde dagegen das gesamte dichte Raster aufblaehen. | Nur die betroffene inhaltsreiche Kachel drei Rasterzeilen belegen und dort `aspect-ratio` aufheben. Vollstaendige Werte und normale Wortumbrueche beibehalten; die Ausnahme sowie die unveraenderte Faktenmatrix statisch regressionspruefen. | +| Feste Finding-Kachelbreiten koennen an schmalen Viewports weniger Spalten ergeben; lange Titel oder zusaetzliche Kategorien koennten bei einer zugleich festgeschriebenen Hoehe abgeschnitten werden. Eine globale Badge-Regel koennte ausserdem kompakte Pathfinder-Karten unbeabsichtigt vergroessern. | Nur die Breite und Mindesthoehe der regulaeren Work-Kachel zentral festschreiben, Zeilenhoehen fuer Virtualisierung daraus ableiten und Inhalt nach unten wachsen lassen. Die semantische Badge-Matrix strikt auf direkte Work-Rasterkacheln begrenzen; Pathfinder bleibt durch fokussierte Regressionen separat abgesichert. | +| Ein Result-DB-Export koennte nur die aktuell geladenen 50 Zeilen statt aller Filtertreffer enthalten oder bei paralleler Datenmutation Seiten aus unterschiedlichen Ergebnisstaenden vermischen. | Exportseiten mit identischem projekt-/timeslotgebundenem Filterquery deterministisch nachladen, die erwartete Gesamtzahl ueber alle Seiten konstant pruefen und bei Aenderung oder vorzeitig leerer Seite ohne Download abbrechen. Manifest und Ergebnisanzahl gemeinsam regressionspruefen. | +| Optionale Statushinweise oder unterschiedlich lange Zahlen-/Einheitentexte koennen die Balkenbreite und Zeilenhoehe einzelner Client-Info-Karten erneut verschieben; globale Balkenregeln koennen zugleich die kompakte Resilience-Karte ueberlagern. | Statusplatz in jeder Standardzeile reservieren, Label/Track/Wert ueber feste semantische Gridspalten ausrichten und Zahlen samt Einheit ohne Umbruch gruppieren. Die Resilience-Ausnahme setzt Zeilen und Platzierung explizit zurueck; beide Geometrien werden gemeinsam regressionsgeprueft. | | Ein formal als `released` markierter Policy-Regelkatalog enthaelt unbekannte Runtime-Enumwerte, ueberlappende Kontextselektoren, mehrdeutige Regeln oder widerspruechlich wiederverwendete Risiko-/Abhaengigkeits-IDs und erzeugt dadurch nicht reproduzierbare beziehungsweise fachlich falsche Evaluationen oder Findings. | Die zentrale Katalogvalidierung prueft Auslieferungsmechanismen, Dispositionen, Comparatorvarianten und -modi, Severity, Kontextueberlappung sowie katalogweite ID-Definitionen geschlossen und fail-closed. Jede spaetere fachliche Katalogerweiterung benoetigt positive und negative synthetische Freigaberegressionen; der produktive Katalog bleibt bis zur fachlichen Kuratierung leer. | | Auswahl, Fokus und fachlicher Status koennen erneut farblich vermischt werden, wenn Komponenten direkt Akzent-, Fokus- oder Severitytokens fuer eine persistente Markierung verwenden. | Persistente Auswahl ausschliesslich an `--marking-color` binden und in beiden Themes auf denselben Gelbwert regressionspruefen. `--focus-ring` bleibt nur fuer Tastaturfokus; Online-, Fehler-, Workflow- und Severityfarben bleiben fachlich getrennt. | | Einzelansichten koennten trotz flacher gemeinsamer Tokens im Hover erneut dekorative Konturen setzen oder den Standard mit lokalen Radien und Schatten umgehen. | Appweite Projektregel und Regression an die flachen `--tile-*`-Tokens binden; normale und Hover-Zustaende getrennt von Fokus, Auswahl und Statusakzenten pruefen. Lokale Kachelgeometrie bleibt unzulaessig. | @@ -1857,6 +1948,29 @@ Diese Entscheidungen werden erst getroffen, wenn die jeweilige Phase ansteht ode | 2026-09-10 | Phasen 7/13A fuer gemischte Defender-Timeslots korrigiert: Work bestimmt den Titel jedes Datensatzes anhand seiner Scannerprovenienz und des deklarativen Titel-Headers, sodass Defender-Vorkommen im gemeinsamen All-Records-Blatt die CVE statt Device-Name oder IP zeigen. Client Info behaelt bei mehreren Scannern die allgemeine Uebersicht und bietet Defender zusaetzlich als eigenen Tab mit den vorhandenen CVE-, Device- und Site-Sichten an. 19 fokussierte Dashboard-, Titel- und Graphregressionen erfolgreich. Keine API-, SQL-, Profil- oder Persistenzmigration; Phasen 7 und 13A bleiben abgeschlossen. | | 2026-09-10 | Phase-13A-Verarbeitungsvertrag fuer Defender-Importe korrigiert: Die Kompatibilitaetspruefung verwendet nun den tatsaechlich persistierten `processedWith.profileId` statt einer eventuell abweichenden erkannten Werkzeug-ID. Dadurch werden registrierte Defender-Profile 1.0.0 als aktuell bewertet; unbekannte Profil-/Versionspaare bleiben fail closed. Die Korrektur ist read-only fuer vorhandene Importdaten und benoetigt keinen Reimport oder Datenbankeingriff. 17 fokussierte Import-/Kompatibilitaetstests sowie der Gesamtlauf mit 766 bestandenen und 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only-, Designpruefung und Client-/Server-Produktionsbuild erfolgreich; Phase 13A bleibt abgeschlossen. | | 2026-09-10 | Microsoft Defender for IoT vulnerability imports and views umgesetzt: Profil und Source Extractor 1.0.0 erweitern die generische Excel-/CSV-Pipeline; Device-Identitaeten, CVE-Definitionen, Device-mal-CVE-Vorkommen, rohe Sensorbeobachtungen und Aggregate-only-Zahlen bleiben getrennt. Client Info bietet CVE-, Device- und Site-Sichten mit Evidenz- und Reconciliation-Drill-down. Summary-only-Quellen bleiben ohne erfundene Findings viewable. 33 fokussierte synthetische Tests sowie 764 Standardtests, 8 uebersprungene optionale PostgreSQL-Tests, Local-only-, Designpruefung und Client-/Server-Produktionsbuild erfolgreich. Keine API-, SQL- oder Persistenzmigration; Phasen 6, 7 und 13A bleiben abgeschlossen. | +| 2026-09-03 | Phase-7-Client-Info-Standardansicht nach Nutzerreferenz festgelegt: `Assessment Status` startet als einzige geoeffnete Informationskarte; Directory Coverage, Certificates & PKI, Infrastructure Details, Identity & Privilege, GPO & Password Hygiene, Control Paths & Trusts, Infrastructure & Resilience, Severity Distribution und Directory Overview starten minimiert und vollbreit. Alle Karten bleiben ueber die gemeinsamen Dreiecke individuell schaltbar. 15 fokussierte Dashboard-/Interaktions-/Dreieckstests und Client-/Server-Produktionsbuild erfolgreich. Keine Realinhalte gelesen und keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | +| 2026-09-03 | Phase-7-Client-Info-Detailkorrektur umgesetzt: minimierte Informationskarten behalten mit voller Spaltenbreite dieselbe Breite wie im maximierten Zustand; Collapse reduziert nur die Hoehe. Der PingCastle-Engine-Hinweis wurde aus `Directory Overview` entfernt, ohne die normalisierte Quellprovenienz zu veraendern. 13 fokussierte Dashboard-/Interaktionstests und Client-/Server-Produktionsbuild erfolgreich. Keine Realinhalte gelesen und keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | +| 2026-09-03 | Phase-7-Client-Info auf stabile Spalten und eine geschlossene Kopfzone korrigiert. Das laufzeitgemessene dichte Masonry wurde durch drei feste, semantisch zugeordnete Kartenstapel mit 3/2/2 Karten ersetzt; minimierte und maximierte Karten bleiben dadurch immer in ihrer Spalte und nur nachfolgende Karten derselben Spalte ruecken auf. Die sechs KPI-Kacheln fuellen auf Desktop gemeinsam die Hoehe von `Assessment Status`, sind groesser typografiert und folgen nun Compliance Score, Deviation, High Severity, Passed, Typed Observations, Directory Sections. Compliance ist als H1 hervorgehoben; ausschliesslich Deviation und High Severity sind klickbar. 15 fokussierte Dashboard-/Interaktions-/Dreieckstests, Client-/Server-Produktionsbuild, Local-only- und Designpruefung erfolgreich. Im Gesamtlauf bestanden 780 Tests; eine unabhaengige statische Data-Hub-Open-Pruefung erwartet noch die alte einzeilige Quelltextdarstellung der weiterhin korrekt vorhandenen `dashboard`-Navigation, 8 optionale PostgreSQL-Tests wurden uebersprungen. Keine Realinhalte gelesen und keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | +| 2026-09-03 | Phase-7-Client-Info-Collapse geometrisch korrigiert und Plus/Minus durch Standarddreiecke ersetzt. Ein eigener `DashboardMasonryGrid` misst die zehn Karten, aktualisiert ihre Rasterspannen per ResizeObserver und packt sie dicht; dadurch rueckt die naechste Karte nach dem Schliessen sofort direkt unter die verkleinerte Kopfzeile. Vor erfolgreicher Messung beziehungsweise ohne ResizeObserver bleibt das normale sichere Grid aktiv. Beide gemeinsamen Kartentypen zeigen nun das App-weite SVG-Dreieck nach unten fuer offen und nach rechts fuer geschlossen. 16 fokussierte synthetische Dashboard-/Interaktions-/Masonry-/Icon-Tests, Client-/Server-Produktionsbuild, Local-only- und Designpruefung erfolgreich. Im Gesamtlauf bestanden 781 Tests; eine unabhaengige statische Data-Hub-Open-Pruefung erwartet noch eine alte einzeilige Quelltextdarstellung der weiterhin korrekt vorhandenen `dashboard`-Navigation, 8 optionale PostgreSQL-Tests wurden uebersprungen. Keine Realinhalte gelesen und keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | +| 2026-09-03 | Phase 13B um die konkrete GPO-Sicht in Work und Result DB erweitert. Aktuelle Policy-Beobachtungen werden ohne Datenkopie aus Artefakt-, Processing-, Backup- und Identity-Persistenz als `policy-observation` projiziert, ueber `assessmentIds` demselben fachlichen Timeslot wie die Scannerlaeufe zugeordnet und mit Werten, technischen Identitaeten, Scope, Delivery Mechanism, Diagnosen und Quell-Locator exportierbar gemacht. Work besitzt ein eigenes Blatt `Group Policy · Observations` und fuehrt die Zeilen zusaetzlich in `All Records` mit `Source: Scan = Group Policy`; die Result DB zeigt sie unter dem Standardfilter als Kontext mit Strichen fuer IOE/Deviation. Die laufende Dummy-API wurde mit 51 GPO-Beobachtungen und zusammen 503 Resultaten im ausgewaehlten historischen Scope verifiziert. 13 fokussierte Tests, 781 bestandene Standardtests, 8 erwartungsgemaess uebersprungene optionale PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Keine SQL-Migration, keine Mutation der Dummy- oder Originaldaten; bestaetigte GPO-Findings bleiben weiterhin von Environment Profile, Identity-/Baseline-/Regelkatalog und Finding Factory abhaengig. | +| 2026-09-03 | Phase-7-Client-Info-Faktenblock auf Nutzerwunsch als eigene Kachel umgesetzt. `Infrastructure & Resilience` zeigt nur noch seine Risiko-/Mengenwerte; die neue minimierbare Kachel `Infrastructure Details` enthaelt `Marked findings` und alle sieben bestehenden Environment-Fakten in der vollstaendigen responsiven Faktenliste. Der gemeinsame Kartenvertrag wurde fuer eine titel-only Kopfzeile optional erweitert; die Ansicht besitzt nun zehn Informationskacheln. 13 fokussierte Dashboard-/Interaktionstests, Local-only- und Designpruefung erfolgreich. Der vollstaendige TypeScript-Build bleibt durch eine unabhaengige parallel hinzugekommene GPO-Testfixture ohne `ImportProcessorReference.kind` blockiert; der nachgelagerte direkte Vite-Build konnte wegen Sandbox-`spawn EPERM` nicht separat ausgefuehrt werden. Keine Realinhalte gelesen und keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | +| 2026-09-03 | Phase-7-Maester-Fehlermeldungen lesbar strukturiert. Ein neuer reiner Parser erkennt ausschliesslich eindeutige `Invoke-MgGraphRequest`-Ausfuehrungsfehler, extrahiert Graph-Meldung, Fehlercode, HTTP-Status, Methode/Endpunkt, Skriptzeile, Zeit und Korrelations-IDs und bewahrt den unveraenderten Rohtext. Das Results-Fenster zeigt die Ursache prominent, technische Werte in einer responsiven Feldmatrix und den Rohbeleg standardmaessig eingeklappt; normale Resultate bleiben unveraendert. 48 fokussierte synthetische Result-/Reader-Tests, Client-/Server-Produktionsbuild, Local-only- und Designpruefung erfolgreich. Im Gesamtlauf bestanden 777 Tests; eine unabhaengige statische Result-DB-Scope-Regression ist aufgrund einer parallel vorhandenen `assessmentIds`-Quelltextergaenzung rot, 8 optionale PostgreSQL-Tests wurden uebersprungen. Keine Realinhalte gelesen und keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | +| 2026-09-03 | Phase-7-Client-Info-Minimierung korrigiert und alle neun Informationskacheln geprueft. Die sechs Mengen-/Faktenkarten und die drei Karten der rechten beziehungsweise oberen Donut-/Faktenzone starten vollstaendig sichtbar mit Minus; ein Klick reduziert jede Karte auf die reine Kopfzeile mit Plus und intrinsischer Kopfzeilenhoehe, ein weiterer stellt den vollstaendigen Inhalt wieder her. Damit bleibt eine minimierte Karte auch neben einer hoeheren geoeffneten Rasterkarte nicht als leere Vollhoehenflaeche stehen. Beide gemeinsamen Kartenkomponenten verwenden denselben zugaenglichen Inhaltscontainer, die sechs reinen Zahlenkacheln bleiben unveraendert. 13 fokussierte Dashboard-/Interaktionstests, 776 bestandene Standardtests, 8 erwartungsgemaess uebersprungene optionale PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Ausschliesslich synthetische Testdaten verwendet; keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung. Phase 7 bleibt abgeschlossen. | +| 2026-09-03 | Phase 13B um die erreichbare GPO-Analyse im Data-Hub-Clientdetail erweitert. Erfolgreiche GPO-Zeilen zeigen statt einer ausgegrauten Scanner-Checkbox eine eigenstaendige GPO-Markierung und `View`; die Aktion oeffnet die vorhandene projektgebundene Workflow-, Summary-, Historien- und Review-Oberflaeche und bietet den Rueckweg zu den Clientinformationen. Der Scanner-Provenienzfilter bleibt unveraendert scannerbezogen. Am freigegebenen Dummy-Client wurde die Anzeige mit einem vollstaendig verarbeiteten Drei-GPO-Paket, 51 Beobachtungen und 48 aktuellen Review-Eintraegen inhaltsbezogen verifiziert. Sieben fokussierte Tests, 775 bestandene Standardtests, 8 erwartungsgemaess uebersprungene optionale PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Keine API-, SQL- oder Persistenzaenderung; Phase 13B bleibt in Arbeit. | +| 2026-09-03 | Phase 13B um die fehlende Data-Hub-Sichtbarkeit und den direkten ZIP-Auswahlweg fuer GPO-Backups ergaenzt. GPO-Artefakte erscheinen nun als eigene Quellenzeilen im zugeordneten Timeslot, fliessen in dessen Zaehler ein, zeigen ihren technischen Verarbeitungszustand und bieten bei Dead Letter Wiederholung beziehungsweise Loeschung. Ein `assessor:gpo-changed`-Refresh macht neue Einreichungen unmittelbar sichtbar. Die getrennten Buttons `Import GPO Folder` und `Import GPO ZIP` fuehren normalen Ordner beziehungsweise vorhandenes Archiv derselben bestehenden Backend-Ingestion zu; der Ordner wird zuvor weiterhin lokal deterministisch verpackt. 14 fokussierte Tests, 774 bestandene Standardtests, 8 erwartungsgemaess uebersprungene optionale PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Keine API-, SQL-, Persistenz- oder Realdatenaenderung; Phase 13B bleibt in Arbeit. | +| 2026-09-03 | Phase 13B um die Betriebsgrenze fuer kuratierte Policy-Regelkataloge erweitert: optionale lokale JSON-Datei mit verpflichtendem SHA-256-Pin, 5-MiB-Limit, Regular-file-/Symlink-Schutz, fataler UTF-8-/JSON-Pruefung und bestehender geschlossener fachlicher Freigabevalidierung. Ungueltige oder unvollstaendige Konfiguration stoppt vor Repository- und Serverstart; ohne Datei bleibt der fachlich leere sichere Katalog aktiv. Konfiguration in `docs/LOCAL_BACKEND.md` dokumentiert. Drei neue synthetische Loaderregressionen, 28 fokussierte Tests und Gesamtlauf mit 773 bestandenen sowie 8 uebersprungenen optionalen PostgreSQL-Tests erfolgreich; Local-only-, Designpruefung und Client-/Serverbuild gruen. Keine Netzwerk-, API-, SQL-, Persistenz- oder Realdatenaenderung; Phase 13B bleibt bis zur fachlichen Regel- und Inbox-ADMX-/ADML-Kuratierung in Arbeit. | +| 2026-09-03 | Phase 13B um snapshotbezogene Ergebnisdeduplizierung erweitert: Das additive `current`-Readmodell verfolgt die aktive Profil-/Identity-/Aggregation-/Evaluation-/Finding-Kette, waehrend `summary`, Evaluationen und Reviewdaten der vollstaendigen Audit-Historie erhalten bleiben. Die sichtbare Summary und Current Review Queue nutzen nur den aktiven Zustand; die Evaluation History bleibt separat sichtbar. Evaluate- und Finding-Aktionen fuer veraltete Ketten werden nicht mehr angeboten. Drei Domainregressionen sowie Panel-, Actions- und HTTP-Regression erfolgreich; Gesamtlauf 770 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Keine SQL- oder Persistenzaenderung, keine Realdaten gelesen; Phase 13B bleibt in Arbeit. | +| 2026-09-03 | Phase-7-Client-Info-Infopanels auf die abschliessend bestaetigte Bildreferenz korrigiert: Minimiert enthalten alle neun Panels nur die luftige Kopfzeile mit `Titel — Primaermenge` und Plus, ohne Balken. Expandiert zeigen Mengenbereiche 120-px-Datenzeilen mit 72-px-dicken Mengenbalken; Assessment Status und Severity Distribution wurden von Donuts auf dieselbe Balkenkomponente umgestellt, wobei ihre Filteraktionen erhalten bleiben. Faktenzeilen wachsen mit 72-px-Mindesthoehe, die kompakte Resilience-Ausnahme wurde entfernt und schmale Ansichten stapeln Wert und 44-px-Balken responsiv. Die sechs Zahlenkacheln blieben unveraendert. 12 fokussierte Tests, 774 bestandene Standardtests, 8 uebersprungene optionale PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | +| 2026-09-03 | Phase-7-Client-Info abschliessend an die neue Gesamtansicht angepasst: sechs unveraenderte Zahlenkacheln oben, darunter sechs dauerhaft sichtbare Mengenpanels als dreispaltige Matrix und rechts eine eigene Rail mit Assessment-/Severity-Donuts sowie Directory Overview. Plus/Minus blendet nur Zusatzinformationen ein; die Kernzeilen bleiben sichtbar. Die Mengenzeilen entsprechen mit 64-px-Zeilen und 30-px-Balken der kompakten Bildgeometrie, nutzen reale Werte und behalten bestehende Filteraktionen der Donutlegenden. Responsive Breakpoints reduzieren die Matrix auf zwei/eine Spalte und stapeln die Rail. 12 fokussierte Tests, 774 bestandene Standardtests, 8 optionale PostgreSQL-Tests uebersprungen, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | +| 2026-09-03 | Phase-7-Client-Info-Kopfzeile verdichtet: Die sechs quadratischen Zahlenkacheln belegen als verkleinerte Sechserspur die linken drei Viertel; die Assessment-Status-Donutkarte steht im rechten Viertel derselben ersten Reihe. Severity Distribution und Directory Overview ruecken dadurch in der rechten Rail nach oben. Die Kopfzone stapelt sich ab 1100 px; die Kennzahlen wechseln dort auf drei und ab 800 px auf zwei Spalten. 12 fokussierte Layout-/Interaktionstests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | +| 2026-09-03 | Standardziel von `Open Client` in Phase 7 von Work auf Client Info umgestellt. Der zentrale Data-Hub-Auswahlcallback aktiviert `dashboard` und oeffnet weiterhin exakt das gewaehlte Projekt mit denselben ScanRun-IDs; dadurch gilt die Aenderung sowohl fuer Quick Open als auch fuer Open aus der Clientdetailseite. Neun fokussierte Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Keine Daten-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | +| 2026-09-03 | Sichtbare Phase-7-Breitenabweichung der expandierten Client-Info-Karten korrigiert: Statt auf der minimierten Zwei-Spalten-Breite zu bleiben, belegen geoeffnete Panels nun sechs Rasterspalten; bei 800 beziehungsweise 560 px wechseln sie auf die gesamte verfuegbare Vier- beziehungsweise Zwei-Spalten-Breite. Dadurch liegen Label/Wert und der 72-px-Mengenbalken wie in der Referenz nebeneinander innerhalb der Karte, ohne Nachbarflaechen zu ueberdecken. Bereits im Kopf dargestellte Primaerwerte sind aus den expandierten Zeilen entfernt; neutrale Serien folgen der roten, orangen, gelben und violetten Referenzfolge, waehrend bestehende Fachfarbgruppen unveraendert bleiben. 12 fokussierte Layout-/Interaktionstests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | +| 2026-09-03 | Phase-7-Client-Info nach freigegebenem Telemetrieentwurf auf Progressive Disclosure umgestellt: Die sechs Zahlenkacheln sind unveraendert. Assessment Status, Severity Distribution, Directory Overview, Directory Coverage, Identity & Privilege, Control Paths & Trusts, Certificates & PKI, GPO & Password Hygiene sowie Infrastructure & Resilience starten kompakt mit ihrer realen Primaermenge und einem Mengenbalken. Plus/Minus blendet pro Karte die vollstaendigen bestehenden Donut-, Mengenbalken- und Fakteninformationen ein beziehungsweise aus; es wurden keine im Mockup erfundenen Bereiche oder Verlaufsdaten implementiert. 15 fokussierte Dashboard-/Interaktions-/Inset-Tests, 774 bestandene Standardtests, 8 erwartungsgemaess uebersprungene optionale PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Keine Daten-, Import-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | +| 2026-09-03 | Unmittelbare Phase-7-Nutzerkorrektur fuer echte responsive Kartenhoehen: Die zuvor gesetzten drei festen Rasterzeilen wurden wieder entfernt. Breakpointabhaengige Rastereinheiten bilden nun nur noch die Mindesthoehe; `minmax(..., auto)` und `min-content` lassen Inhaltskarten anhand ihrer vollstaendigen Werte wachsen. `Infrastructure & Resilience` verwendet automatische Inhaltshoehe, waehrend andere Karten quadratisch bleiben, solange ihr Inhalt hineinpasst. 14 fokussierte Dashboard-/Inset-Tests, 770 bestandene Standardtests, 8 erwartungsgemaess uebersprungene optionale PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | +| 2026-09-03 | Phase-7-Infrastructure-&-Resilience-Kachel gegen Inhaltsueberlauf korrigiert: Die Karte reserviert nun drei Rasterzeilen und hebt nur fuer diese inhaltsreiche Ausnahme das starre Quadratverhaeltnis auf. Damit bleiben auch lange vollstaendige OS-Aufzaehlungen innerhalb der Flaeche, waehrend Faktenlayout und alle uebrigen quadratischen Karten unveraendert bleiben. 14 fokussierte Dashboard-/Inset-Tests, 769 bestandene Standardtests, 8 erwartungsgemaess uebersprungene optionale PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | +| 2026-09-03 | Phase-7-Work-Finding-Kacheln auf das bestaetigte Referenzformat vereinheitlicht: 358 px feste Breite, 223 px Mindesthoehe, keine Restbreitenstreckung und weiterhin wachsender Inhalt. Kopfaufteilung mit Hexagon links sowie Bookmark/Results rechts bleibt erhalten; die Metadaten stehen stabil als Status, Kategorie, Severity plus Weight und Score. Die Regel ist auf direkte Work-Rasterkacheln begrenzt, sodass Pathfinder-Micro-Cards kompakt bleiben. 16 fokussierte Kachel-/Typografie-/Pathfinder-Tests, 768 bestandene Standardtests, 8 erwartungsgemaess uebersprungene optionale PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | +| 2026-09-03 | Phase-7-Result-DB auf `Deviation = Yes` als initialen und Reset-Filter gestellt und gefilterten JSONL-Export ergaenzt. Der Button laedt alle Treffer des aktuellen Projekt-/Timeslot- und Nutzerfilterzustands in stabilen 1.000er-Seiten, bricht bei veraenderter Gesamtmenge fail-closed ab und erzeugt ein versioniertes Manifest plus eine vollstaendige JSON-Zeile je Unified Result. Drei fokussierte Export-/Scope-Tests, 767 bestandene Standardtests, 8 erwartungsgemaess uebersprungene optionale PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Keine API-, SQL-, Persistenz- oder Realdatenaenderung; Phase 7 bleibt abgeschlossen. | +| 2026-09-03 | Phase-7-Client-Info-Balkenlisten bereinigt: Label, Balken und Wert verwenden nun ein identisches Dreispaltenraster; eine kompakte zweite Wertzeile ist auch ohne Hinweis reserviert, sodass `Restricted` keine einzelnen Zeilen mehr verschiebt. Zahl und optionale Einheit sind getrennt formatiert und bleiben rechtsbuendig zusammen. Die Resilience-Kompaktkarte setzt die Standardplatzierung explizit auf ihr Einspaltenlayout zurueck. 13 fokussierte Dashboard-/Kacheltests, 767 bestandene Standardtests, 8 erwartungsgemaess uebersprungene optionale PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | | 2026-08-26 | Phase-13B-Regelkatalog-Freigabegrenze gehaertet: Runtimevalidierung fuer geschlossene Auslieferungsmechanismen, Baseline-Dispositionen, Comparatorvarianten/-modi und Severitywerte ergaenzt; ueberlappende Selektoren, mehrdeutige kontextspezifische Regeln sowie widerspruechliche Risiko- und Abhaengigkeitsdefinitionen werden vor Serviceaktivierung fail-closed abgelehnt. Der fachliche Produktivkatalog bleibt bewusst leer. Der lokale private Artefaktspeicher zeigt wieder auf den aktuellen LocalAppData-Pfad ausserhalb des Webroots; Backend `ok`, App HTTP 200 und vorhandener Speicherroot wurden ohne Inhaltszugriff bestaetigt. Neun neue synthetische Katalogtests, 25 fokussierte GPO-/Katalogtests, 764 bestandene Standardtests, 8 erwartungsgemaess uebersprungene optionale PostgreSQL-Tests, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Keine API-, SQL- oder Persistenzaenderung; Phase 13B bleibt in Arbeit. | | 2026-08-25 | Phase-7-Markierungsfarbe appweit auf Gelb vereinheitlicht: Der neue Theme-Token `--marking-color` traegt in Dark und Light denselben Wert und steuert Kachelauswahl, allgemeine Auswahlrahmen/-marker, gemerkte Eintraege und Graphauswahl. Die violette Datahub-Auswahlkontur entfaellt. Tastaturfokus bleibt blau, Scanner-/Workflow-/Severitystatus behalten ihre fachlichen Farben. Projektregel und Kachelregression wurden aktualisiert. 24 fokussierte Auswahl-/Dashboard-/Graphtests, Designcheck und Client-Produktionsbuild erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | | 2026-08-25 | Phase-7-Kachelstandard appweit auf die freigegebene flache Client-Info-Geometrie umgestellt: `--tile-border` ist in beiden Themes transparent, `--tile-radius` verwendet den zentralen Nullradius und `--tile-card-shadow` ist deaktiviert. Datahub-, Dashboard-, Sidebar-, Work- und Pathfinder-Hover veraendern die Flaeche ohne dekorative Kontur; Fokus, Auswahl und Statusmarkierungen bleiben sichtbar. `AGENTS.md` und die gemeinsamen Kachelregressionen wurden auf den neuen verbindlichen Standard aktualisiert. 24 fokussierte Kachel-/Dashboardtests, Designcheck und Client-Produktionsbuild erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. | diff --git a/docs/LOCAL_BACKEND.md b/docs/LOCAL_BACKEND.md index c7ca8f7..b0815ff 100644 --- a/docs/LOCAL_BACKEND.md +++ b/docs/LOCAL_BACKEND.md @@ -80,6 +80,26 @@ Start ab; es findet kein stiller Fallback statt. Ein vollständiges lokales PostgreSQL-Setup, Schema-, Migrations-, Backup- und Restore-Regeln stehen in `docs/POSTGRESQL.md`. +## Kuratierter GPO-Regelkatalog + +Ohne weitere Konfiguration startet die Policy-Evaluation mit dem freigegebenen, +fachlich leeren Katalog. Ein lokal kuratierter JSON-Katalog wird nur geladen, +wenn Pfad und erwarteter SHA-256 gemeinsam gesetzt sind: + +```text +ASSESSOR_POLICY_RULE_CATALOG_PATH=C:\geschuetzter-pfad\policy-rules.json +ASSESSOR_POLICY_RULE_CATALOG_SHA256=<64-stelliger SHA-256-Hexwert> +``` + +Der Server lädt höchstens 5 MiB aus einer regulären Datei, vergleicht den +Inhalt vor dem JSON-Parsing mit dem fest konfigurierten Hash und validiert den +vollständigen Katalogvertrag einschließlich Kontextüberschneidungen, +Comparatoren, Risiko- und Abhängigkeitsregeln. Fehlende Paarparameter, +Hashabweichungen, Symlinks, ungültiges UTF-8/JSON oder fachlich ungültige Regeln +verhindern den Serverstart. Die Datei wird weder aus dem Netzwerk bezogen noch +durch die Anwendung verändert. Jeder Evaluation Run speichert weiterhin den +tatsächlich verwendeten vollständigen Katalog für reproduzierbares Reprocessing. + Originaldateien werden aktuell nicht als separate Serverartefakte abgelegt. Die API verarbeitet die Bytes und speichert die daraus erzeugten kanonischen Fachobjekte. Eine spätere Aufbewahrung von Originalartefakten bleibt eine eigene diff --git a/docs/architecture/gpo-threat-model.md b/docs/architecture/gpo-threat-model.md index a4c297b..b5dbd21 100644 --- a/docs/architecture/gpo-threat-model.md +++ b/docs/architecture/gpo-threat-model.md @@ -36,6 +36,7 @@ Ein Parserworker begrenzt Zeit und Speicher, isoliert aber keinen bösartigen na | Skript- oder Binärausführung | Nur inventarisieren und hashen; niemals importieren, laden, entschlüsseln oder starten. | | Manipulierte oder herstellerspezifisch erweiterte `Registry.pol`-Datensaetze erzeugen falsche Instruktionen oder Ressourcenverbrauch. | PReg-Signatur und Version, Feldabschluesse, Schluessel und Wertnamen, Typen, Datengroessen, Datensatzanzahl und Gesamtgroesse vor Reduktion begrenzen; nur freigegebene Typen und REG_NONE als exakt leere Schluesselanlage akzeptieren, sonst das Artefakt ohne Salvage blockieren. Keine Datei ausfuehren und keinen externen Prozess starten. | | GPP-`cpassword` oder andere Geheimnisse | Nur Vorhandensein und Locator erfassen; Wert bereits an der Parsergrenze maskieren, niemals entschlüsseln, loggen oder exportieren. | +| Manipulierter oder unbeabsichtigt ausgetauschter Policy-Regelkatalog | Einen extern kuratierten Katalog nur aus einer lokalen regulären Nicht-Symlink-Datei bis 5 MiB laden, wenn Pfad und erwarteter SHA-256 gemeinsam konfiguriert sind. Hash vor UTF-8-/JSON-Parsing prüfen, danach den vollständigen fachlichen Katalogvertrag fail-closed validieren und bei jeder Abweichung vor Repository- und Serverstart abbrechen. Keine automatische Netzwerkquelle oder stille Aktivierung. | | Pfad-/Namensheuristik erzeugt falsche Policy- oder Zielidentität | Technische IDs ausschließlich aus unterstützten Artefaktinhalten und versionierten Regeln; Namen und Pfade bleiben Provenienz. | | Historische Backups werden gemeinsam aggregiert | Explizite Backup-GUID, GPO-GUID, Domänenidentität, manifestgestützte Set-ID und Erhebungsfenster prüfen; doppelte GPOs, Zeitkonflikte und Domänenmischung blockieren. | | Ausgewählte Teilmenge wird als vollständige Umgebung bewertet | Setvollständigkeit getrennt von Instanzvollständigkeit modellieren; negative Schlussfolgerungen nur bei manifestgestütztem vollständigem Set mit erwarteter und beobachteter Anzahl. | diff --git a/server/assessor-http-server.ts b/server/assessor-http-server.ts index e66e273..27e17df 100644 --- a/server/assessor-http-server.ts +++ b/server/assessor-http-server.ts @@ -235,6 +235,7 @@ function unifiedQueryFrom(url: URL): UnifiedResultQuery { return { projectId: url.searchParams.get('projectId') ?? undefined, assessmentId: url.searchParams.get('assessmentId') ?? undefined, + assessmentIds: many('assessmentIds'), scanRunId: url.searchParams.get('scanRunId') ?? undefined, scanRunIds: many('scanRunIds'), evaluationRunId: url.searchParams.get('evaluationRunId') ?? undefined, diff --git a/server/gpo/policy-evaluation-rule-catalog-loader.ts b/server/gpo/policy-evaluation-rule-catalog-loader.ts new file mode 100644 index 0000000..356187b --- /dev/null +++ b/server/gpo/policy-evaluation-rule-catalog-loader.ts @@ -0,0 +1,87 @@ +import { createHash, timingSafeEqual } from 'node:crypto'; +import { lstat, readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { + RELEASED_POLICY_EVALUATION_RULE_CATALOG, + policyEvaluationRuleCatalogValidationErrors, + type PolicyEvaluationRuleCatalog +} from '../../src/domain/policy-evaluation-rules'; + +export const MAXIMUM_POLICY_RULE_CATALOG_BYTES = 5 * 1024 * 1024; + +export interface PolicyEvaluationRuleCatalogLoadOptions { + path?: string; + expectedSha256?: string; + maximumBytes?: number; +} + +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function hasValidationShape(value: unknown): value is PolicyEvaluationRuleCatalog { + if (!isObject(value) || !isObject(value.catalog) || !Array.isArray(value.rules)) return false; + return value.rules.every(rule => isObject(rule) + && Array.isArray(rule.contexts) + && rule.contexts.every(context => isObject(context) + && Array.isArray(context.roles) && Array.isArray(context.scopes)) + && Array.isArray(rule.dependencies) + && rule.dependencies.every(isObject) + && (rule.comparator === undefined || isObject(rule.comparator)) + && (rule.riskRule === undefined || isObject(rule.riskRule))); +} + +function normalizedSha256(value: string | undefined): string | undefined { + const normalized = value?.trim().toLowerCase(); + if (!normalized) return undefined; + if (!/^[a-f0-9]{64}$/.test(normalized)) { + throw new Error('ASSESSOR_POLICY_RULE_CATALOG_SHA256 muss ein SHA-256-Hexwert sein.'); + } + return normalized; +} + +export async function loadPolicyEvaluationRuleCatalog( + options: PolicyEvaluationRuleCatalogLoadOptions = {} +): Promise { + const configuredPath = options.path?.trim(); + const expectedSha256 = normalizedSha256(options.expectedSha256); + if (!configuredPath && !expectedSha256) return structuredClone(RELEASED_POLICY_EVALUATION_RULE_CATALOG); + if (!configuredPath || !expectedSha256) { + throw new Error('Policy-Regelkatalogpfad und SHA-256 muessen gemeinsam konfiguriert werden.'); + } + const maximumBytes = options.maximumBytes ?? MAXIMUM_POLICY_RULE_CATALOG_BYTES; + if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1 || maximumBytes > MAXIMUM_POLICY_RULE_CATALOG_BYTES) { + throw new Error('Die Groessengrenze des Policy-Regelkatalogs ist ungueltig.'); + } + + const absolutePath = resolve(configuredPath); + const information = await lstat(absolutePath); + if (information.isSymbolicLink() || !information.isFile()) { + throw new Error('Der Policy-Regelkatalog muss eine regulaere Datei sein.'); + } + if (information.size < 1 || information.size > maximumBytes) { + throw new Error('Der Policy-Regelkatalog verletzt die konfigurierte Groessengrenze.'); + } + const bytes = await readFile(absolutePath); + if (bytes.byteLength !== information.size || bytes.byteLength > maximumBytes) { + throw new Error('Der Policy-Regelkatalog wurde waehrend des Lesens veraendert oder ist zu gross.'); + } + const actualSha256 = createHash('sha256').update(bytes).digest('hex'); + if (!timingSafeEqual(Buffer.from(actualSha256, 'hex'), Buffer.from(expectedSha256, 'hex'))) { + throw new Error('Der Policy-Regelkatalog stimmt nicht mit dem konfigurierten SHA-256 ueberein.'); + } + + let parsed: unknown; + try { + const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes).replace(/^\uFEFF/, ''); + parsed = JSON.parse(text); + } catch { + throw new Error('Der Policy-Regelkatalog ist kein gueltiges UTF-8-JSON-Dokument.'); + } + if (!hasValidationShape(parsed)) { + throw new Error('Der Policy-Regelkatalog besitzt keine gueltige Struktur.'); + } + const errors = policyEvaluationRuleCatalogValidationErrors(parsed); + if (errors.length) throw new Error(`Der Policy-Regelkatalog wurde abgelehnt: ${errors.join(' ')}`); + return structuredClone(parsed); +} diff --git a/server/gpo/zip-inventory.ts b/server/gpo/zip-inventory.ts index 8833a46..b3b4cf2 100644 --- a/server/gpo/zip-inventory.ts +++ b/server/gpo/zip-inventory.ts @@ -369,4 +369,4 @@ export function readVerifiedZipFiles( selected.push({ entry, bytes: content.slice() }); }); return selected.sort((left, right) => left.entry.index - right.entry.index); -} \ No newline at end of file +} diff --git a/server/index.ts b/server/index.ts index db78ca3..0d9d2da 100644 --- a/server/index.ts +++ b/server/index.ts @@ -17,6 +17,7 @@ import { GpoPolicyFindingService } from './gpo/gpo-policy-finding-service'; import { GpoPolicyAnalysisOverviewService } from './gpo/gpo-policy-analysis-overview-service'; import { NvdCveClient } from './cve/nvd-cve-client'; import { CveEnrichmentService } from './cve/cve-enrichment-service'; +import { loadPolicyEvaluationRuleCatalog } from './gpo/policy-evaluation-rule-catalog-loader'; function boundedInteger(value: string | undefined, fallback: number, minimum: number, maximum: number, name: string): number { if (value === undefined || value.trim() === '') return fallback; @@ -52,6 +53,10 @@ async function main(): Promise { maximumOldGenerationMb: boundedInteger(process.env.ASSESSOR_GPO_MEMORY_MB, 1_024, 256, 2_048, 'ASSESSOR_GPO_MEMORY_MB'), maximumYoungGenerationMb: boundedInteger(process.env.ASSESSOR_GPO_YOUNG_MEMORY_MB, 128, 64, 512, 'ASSESSOR_GPO_YOUNG_MEMORY_MB') }); + const policyEvaluationRuleCatalog = await loadPolicyEvaluationRuleCatalog({ + path: process.env.ASSESSOR_POLICY_RULE_CATALOG_PATH, + expectedSha256: process.env.ASSESSOR_POLICY_RULE_CATALOG_SHA256 + }); const runtime = await createRepositoryRuntime(); const artifactStore = new FileSystemBackendArtifactStore(artifactRoot); const gpoBackupSubmissionService = new GpoBackupSubmissionService( @@ -65,7 +70,9 @@ async function main(): Promise { runtime.repository, artifactStore, defaultImportResourcePolicy.maximumFileBytes, gpoInventoryExecutor ); const gpoPolicySnapshotService = new GpoPolicySnapshotService(runtime.repository); - const gpoPolicyEvaluationService = new GpoPolicyEvaluationService(runtime.repository); + const gpoPolicyEvaluationService = new GpoPolicyEvaluationService(runtime.repository, { + ruleCatalog: policyEvaluationRuleCatalog + }); const gpoPolicyFindingService = new GpoPolicyFindingService(runtime.repository); const gpoPolicyAnalysisOverviewService = new GpoPolicyAnalysisOverviewService(runtime.repository); const cveEnrichmentService = new CveEnrichmentService(runtime.repository, new NvdCveClient({ diff --git a/server/postgres/postgres-assessor-repository.ts b/server/postgres/postgres-assessor-repository.ts index f90a0c0..892fc72 100644 --- a/server/postgres/postgres-assessor-repository.ts +++ b/server/postgres/postgres-assessor-repository.ts @@ -36,6 +36,7 @@ import type { DbStructureColumn, DbStructureForeignKey, DbStructureLayerId, DbSt import { storedGpoBackupInstanceValidationErrors, storedGpoPolicyAnalysisValidationErrors } from '../../src/domain/gpo-analysis-persistence'; import { createEnvironmentUnifiedResult, + createCurrentPolicyObservationUnifiedResults, createPolicyUnifiedResult, createScannerUnifiedResult, deriveUnifiedResultFacets, @@ -1077,13 +1078,36 @@ export class PostgresAssessorRepository implements AssessorRepository { })); } + private async unifiedPolicyObservationResults( + query: UnifiedResultQuery = {}, + observationId?: string + ): Promise { + if (query.sourceKind?.length && !query.sourceKind.includes('policy-observation')) return []; + const where = query.projectId ? 'WHERE project_id = $1' : ''; + const values = query.projectId ? [query.projectId] : []; + const [artifactRows, processingRows, backupRows, analysisRows] = await Promise.all([ + this.pool.query>(`SELECT data FROM assessor_import_artifacts ${where} ORDER BY created_at, id`, values), + this.pool.query>(`SELECT data FROM assessor_import_processing_runs ${where} ORDER BY completed_at, id`, values), + this.pool.query>(`SELECT data FROM assessor_gpo_backup_instances ${where} ORDER BY created_at, id`, values), + this.pool.query>(`SELECT data FROM assessor_policy_identity_analysis_runs ${where} ORDER BY completed_at, id`, values) + ]); + const results = createCurrentPolicyObservationUnifiedResults( + dataRows(artifactRows), + dataRows(processingRows), + dataRows(backupRows), + dataRows(analysisRows) + ); + return observationId ? results.filter(item => item.sourceResultId === observationId) : results; + } + private async allUnifiedResults(query: UnifiedResultQuery = {}): Promise { - const [scanner, environment, policy] = await Promise.all([ + const [scanner, environment, policyObservations, policy] = await Promise.all([ this.unifiedScannerResults(query), this.unifiedEnvironmentResults(query), + this.unifiedPolicyObservationResults(query), this.unifiedPolicyResults(query) ]); - return [...scanner, ...environment, ...policy]; + return [...scanner, ...environment, ...policyObservations, ...policy]; } async queryUnifiedResults(query: UnifiedResultQuery = {}): Promise { @@ -1097,11 +1121,14 @@ export class PostgresAssessorRepository implements AssessorRepository { async getUnifiedResultById(resultProjectionId: string): Promise { const scannerPrefix = 'scanner-finding:'; const policyPrefix = 'policy-evaluation:'; + const policyObservationPrefix = 'policy-observation:'; const environmentPrefix = 'environment-observation:'; const items = resultProjectionId.startsWith(scannerPrefix) ? await this.unifiedScannerResults({}, resultProjectionId.slice(scannerPrefix.length)) : resultProjectionId.startsWith(policyPrefix) ? await this.unifiedPolicyResults({}, resultProjectionId.slice(policyPrefix.length)) + : resultProjectionId.startsWith(policyObservationPrefix) + ? await this.unifiedPolicyObservationResults({}, resultProjectionId.slice(policyObservationPrefix.length)) : resultProjectionId.startsWith(environmentPrefix) ? await this.unifiedEnvironmentResults({}, resultProjectionId.slice(environmentPrefix.length)) : []; diff --git a/src/app/App.tsx b/src/app/App.tsx index f8b869a..5de5947 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -151,10 +151,26 @@ export function App() { /> } {activeArea === 'graph' && dataset && !hasClientTimeslotScope &&

No timeslot selected

Select a timeslot in Work to build its graph.

} - {activeArea === 'data-hub' && { await app.openProject(projectId, projectId === targetProjectId ? app.activeScanRunIds : undefined); }} onProjectSelected={(project, scanRunIds) => { setActiveProjectName(project?.name); if (project) { setActiveArea('work'); void app.openProject(project.id, scanRunIds); } else app.setTargetProjectId(undefined); }}/>} + {activeArea === 'data-hub' && ( + { + await app.openProject(projectId, projectId === targetProjectId ? app.activeScanRunIds : undefined); + }} + onProjectSelected={(project, scanRunIds) => { + setActiveProjectName(project?.name); + if (project) { + setActiveArea('dashboard'); + void app.openProject(project.id, scanRunIds); + } else app.setTargetProjectId(undefined); + }} + /> + )} {activeArea === 'dashboard' && setActiveArea('work')}/>} {activeArea === 'db-structure' && } - {activeArea === 'results' && } + {activeArea === 'results' && } diff --git a/src/app/AppProvider.tsx b/src/app/AppProvider.tsx index 0f48036..41713b3 100644 --- a/src/app/AppProvider.tsx +++ b/src/app/AppProvider.tsx @@ -49,6 +49,7 @@ interface AppContextValue extends AppState { applyViewBookmark(view: SavedViewState): void; targetProjectId?: string; activeScanRunIds?: string[]; + activeAssessmentIds?: string[]; workScopeId: string; setTargetProjectId: (projectId?: string) => void; openProject(projectId: string, scanRunIds?: string[]): Promise; @@ -115,6 +116,7 @@ export function AppProvider({ children }: PropsWithChildren) { const [importWorkflow, setImportWorkflow] = useState({ phase: 'idle' }); const [targetProjectId, setTargetProjectId] = useState(); const [activeScanRunIds, setActiveScanRunIds] = useState(); + const [activeAssessmentIds, setActiveAssessmentIds] = useState(); const [workScopeId, setWorkScopeId] = useState(''); const openProjectRequest = useRef(0); const [state, setState] = useState(() => { @@ -398,13 +400,14 @@ export function AppProvider({ children }: PropsWithChildren) { // Result DB und Graph duerfen waehrend des asynchronen Workspace-Ladens // weder den vorherigen Timeslot noch den gesamten neuen Client anzeigen. setActiveScanRunIds(scanRunIds?.length ? [...new Set(scanRunIds)] : undefined); + setActiveAssessmentIds(undefined); setState(current => ({ ...current, projectId, status: 'Loading saved client …' })); try { - const { models, dataset } = await loadCanonicalProjectWorkspace(assessorApi, projectId, scanRunIds); + const { models, dataset, assessmentIds } = await loadCanonicalProjectWorkspace(assessorApi, projectId, scanRunIds); if (requestId !== openProjectRequest.current) return false; if (!models.length) { setState(current => ({ ...current, status: 'This client does not have a scan report yet.' })); @@ -437,6 +440,7 @@ export function AppProvider({ children }: PropsWithChildren) { pathFindingIds: [], status: models[0].project.name + ' · ' + dataset.scanTool + ' · ' + models.length.toLocaleString('en-US') + ' scan report' + (models.length === 1 ? '' : 's') + ' · ' + activeSheet.analysis.records.length.toLocaleString('en-US') + ' records loaded' })); + setActiveAssessmentIds(assessmentIds); setWorkScopeId(scopeId); return true; } catch (reason) { @@ -513,10 +517,14 @@ export function AppProvider({ children }: PropsWithChildren) { applyViewBookmark: savedView => setState(current => applySavedViewState(current, savedView)), targetProjectId, activeScanRunIds, + activeAssessmentIds, workScopeId, setTargetProjectId: projectId => { setTargetProjectId(projectId); - if (!projectId) setActiveScanRunIds(undefined); + if (!projectId) { + setActiveScanRunIds(undefined); + setActiveAssessmentIds(undefined); + } }, openProject, setSelectedRecord: selectedRecordId => setState(current => ({ ...current, selectedRecordId })), @@ -563,7 +571,7 @@ export function AppProvider({ children }: PropsWithChildren) { if (!Object.values(current.reviews).some(review => review.include)) return current; return { ...current, actionOnly: !current.actionOnly, view: 'columns' }; }) - }), [activeProfile, activeScanRunIds, activeSheet, closeImportWorkflow, commitImportBatch, confirmImportEntry, importFiles, importWorkflow, setImportEntrySelected, setTheme, state, updateReview, targetProjectId, workScopeId, openProject]); + }), [activeProfile, activeAssessmentIds, activeScanRunIds, activeSheet, closeImportWorkflow, commitImportBatch, confirmImportEntry, importFiles, importWorkflow, setImportEntrySelected, setTheme, state, updateReview, targetProjectId, workScopeId, openProject]); return {children}; } diff --git a/src/components/TileCard/TileCard.tsx b/src/components/TileCard/TileCard.tsx index 4aecc03..8f45f94 100644 --- a/src/components/TileCard/TileCard.tsx +++ b/src/components/TileCard/TileCard.tsx @@ -31,9 +31,14 @@ export function tileBadges(record: FindingRecord, fallbackToolId?: string): Tile const text = /^(Weight|Score)$/.test(label) ? `${label}: ${field.value}` : field.value; return [{ label, value: field.value, text, ...(label === 'Severity' ? { severity: normalizeSeverity(field.value) } : {}) }]; }); - badges.push(...cisBenchmarkCategories(record, fallbackToolId).map(value => ({ label: 'CIS Category', value, text: value }))); + const cisBadges = cisBenchmarkCategories(record, fallbackToolId).map(value => ({ label: 'CIS Category', value, text: value })); + const orderedBadges = [ + ...badges.filter(badge => badge.label === 'Status' || badge.label === 'Category'), + ...cisBadges, + ...badges.filter(badge => badge.label !== 'Status' && badge.label !== 'Category') + ]; - return badges.filter((badge, index) => !badges.slice(0, index).some(previous => previous.value.trim().toLocaleLowerCase('en-US') === badge.value.trim().toLocaleLowerCase('en-US'))); + return orderedBadges.filter((badge, index) => !orderedBadges.slice(0, index).some(previous => previous.value.trim().toLocaleLowerCase('en-US') === badge.value.trim().toLocaleLowerCase('en-US'))); } export function compactTileBadges(badges: TileBadge[], limit?: number): TileBadge[] { @@ -94,8 +99,8 @@ function TileCardComponent({ record, title, bookmarked, resultColor, toolId, sea /* Der Ergebnisstatus ist ein Outcome, keine Meta-Plakette: Textfarbe plus Glyph statt gefuellter Flaeche. Sonst sehen Passed, Failed und Investigate in der Kachelansicht identisch aus. */ - ? {highlightText(badge.text, search)} - : {highlightText(badge.text, search)})} + ? {highlightText(badge.text, search)} + : {highlightText(badge.text, search)})}
diff --git a/src/domain/policy-analysis-overview.ts b/src/domain/policy-analysis-overview.ts index 5fa231f..be04b71 100644 --- a/src/domain/policy-analysis-overview.ts +++ b/src/domain/policy-analysis-overview.ts @@ -63,6 +63,31 @@ export interface PolicyAnalysisProfileSummary { contextCount: number; } +export interface PolicyAnalysisSummary { + identityAnalysisRuns: number; + aggregationRuns: number; + evaluationRuns: number; + findingRuns: number; + configuredMatches: number; + confirmedDeviations: number; + notApplicable: number; + policyFindings: number; + reviewItems: number; + conflicts: number; + unresolvedIdentities: number; + catalogGaps: number; + parserGaps: number; + partialRuns: number; +} + +export interface PolicyAnalysisCurrentState { + version: 1; + summary: PolicyAnalysisSummary; + evaluationRunIds: string[]; + findingRunIds: string[]; + reviewItems: PolicyAnalysisReviewItem[]; +} + export type PolicyAnalysisActionStatus = 'ready' | 'review-only' | 'completed' | 'blocked'; export interface PolicyAnalysisReprocessAction { @@ -118,22 +143,8 @@ export interface PolicyAnalysisOverview { version: 1; projectId: string; generatedAt: string; - summary: { - identityAnalysisRuns: number; - aggregationRuns: number; - evaluationRuns: number; - findingRuns: number; - configuredMatches: number; - confirmedDeviations: number; - notApplicable: number; - policyFindings: number; - reviewItems: number; - conflicts: number; - unresolvedIdentities: number; - catalogGaps: number; - parserGaps: number; - partialRuns: number; - }; + summary: PolicyAnalysisSummary; + current: PolicyAnalysisCurrentState; actions: PolicyAnalysisActions; profiles: PolicyAnalysisProfileSummary[]; evaluations: PolicyAnalysisEvaluationSummary[]; @@ -194,16 +205,94 @@ function sameIds(left: string[], right: string[]): boolean { return a.length === b.length && a.every((value, index) => value === b[index]); } -function buildActions(input: BuildPolicyAnalysisOverviewInput): PolicyAnalysisActions { +function latestByKey( + records: T[], + key: (record: T) => string, + completedAt: (record: T) => string, + id: (record: T) => string +): T[] { + const sorted = [...records].sort((left, right) => completedAt(right).localeCompare(completedAt(left)) + || id(left).localeCompare(id(right))); + const latest = new Map(); + for (const record of sorted) if (!latest.has(key(record))) latest.set(key(record), record); + return [...latest.values()]; +} + +function activePolicyRecords(input: BuildPolicyAnalysisOverviewInput): { + identityAnalyses: StoredGpoPolicyAnalysis[]; + aggregations: StoredPolicySnapshotAggregation[]; + evaluations: StoredPolicyEvaluation[]; + findingRuns: StoredPolicyFindingRun[]; +} { const supersededProfileIds = new Set(input.profiles.map(profile => profile.supersedesProfileId).filter(Boolean)); - const activeProfiles = input.profiles.filter(profile => !supersededProfileIds.has(profile.id)); + const activeProfileIds = new Set(input.profiles.filter(profile => !supersededProfileIds.has(profile.id)).map(profile => profile.id)); const supersededAnalysisIds = new Set(input.identityAnalyses .map(record => record.run.supersedesAnalysisRunId).filter(Boolean)); - const activeIdentityAnalyses = input.identityAnalyses - .filter(record => !supersededAnalysisIds.has(record.run.id)) - .sort((left, right) => right.run.completedAt.localeCompare(left.run.completedAt) || left.run.id.localeCompare(right.run.id)) - .filter((record, index, records) => records.findIndex(item => item.run.artifactId === record.run.artifactId) === index); - const activeProfileIds = new Set(activeProfiles.map(profile => profile.id)); + const identityAnalyses = latestByKey( + input.identityAnalyses.filter(record => !supersededAnalysisIds.has(record.run.id)), + record => record.run.artifactId, + record => record.run.completedAt, + record => record.run.id + ); + const activeIdentityIds = new Set(identityAnalyses.map(record => record.run.id)); + const aggregations = latestByKey( + input.aggregations.filter(record => activeProfileIds.has(record.run.environmentProfileId) + && record.run.sourceAnalysisRunIds.length > 0 + && record.run.sourceAnalysisRunIds.every(runId => activeIdentityIds.has(runId))), + record => `${record.run.environmentProfileId}\u0000${record.run.snapshotId}`, + record => record.run.completedAt, + record => record.run.id + ); + const activeAggregationIds = new Set(aggregations.map(record => record.run.id)); + const evaluations = latestByKey( + input.evaluations.filter(record => activeAggregationIds.has(record.run.aggregationRunId)), + record => record.run.aggregationRunId, + record => record.run.completedAt, + record => record.run.id + ); + const activeEvaluationIds = new Set(evaluations.map(record => record.run.id)); + const findingRuns = latestByKey( + input.findingRuns.filter(record => activeEvaluationIds.has(record.run.evaluationRunId)), + record => record.run.evaluationRunId, + record => record.run.completedAt, + record => record.run.id + ); + return { identityAnalyses, aggregations, evaluations, findingRuns }; +} + +function summarizePolicyRecords( + identityAnalyses: StoredGpoPolicyAnalysis[], + aggregations: StoredPolicySnapshotAggregation[], + evaluations: StoredPolicyEvaluation[], + findingRuns: StoredPolicyFindingRun[], + reviewItems: PolicyAnalysisReviewItem[] +): PolicyAnalysisSummary { + return { + identityAnalysisRuns: identityAnalyses.length, + aggregationRuns: aggregations.length, + evaluationRuns: evaluations.length, + findingRuns: findingRuns.length, + configuredMatches: evaluations.reduce((sum, record) => sum + record.run.matchCount, 0), + confirmedDeviations: evaluations.reduce((sum, record) => sum + record.run.deviationCount, 0), + notApplicable: evaluations.reduce((sum, record) => sum + record.run.notApplicableCount, 0), + policyFindings: findingRuns.reduce((sum, record) => sum + record.run.findingCount, 0), + reviewItems: reviewItems.length, + conflicts: reviewItems.filter(item => item.category === 'conflict').length, + unresolvedIdentities: reviewItems.filter(item => item.category === 'unresolved-identity').length, + catalogGaps: reviewItems.filter(item => item.category === 'catalog-gap').length, + parserGaps: reviewItems.filter(item => item.category === 'parser-gap').length, + partialRuns: identityAnalyses.filter(record => record.run.status === 'partial').length + + aggregations.filter(record => record.run.status !== 'complete').length + + evaluations.filter(record => record.run.status !== 'complete').length + + findingRuns.filter(record => record.run.status === 'partial').length + }; +} + +function buildActions(input: BuildPolicyAnalysisOverviewInput): PolicyAnalysisActions { + const active = activePolicyRecords(input); + const supersededProfileIds = new Set(input.profiles.map(profile => profile.supersedesProfileId).filter(Boolean)); + const activeProfiles = input.profiles.filter(profile => !supersededProfileIds.has(profile.id)); + const activeIdentityAnalyses = active.identityAnalyses; const processingRuns = [...(input.processingRuns ?? [])] .sort((left, right) => right.completedAt.localeCompare(left.completedAt) || left.id.localeCompare(right.id)); const latestProcessingByArtifact = new Map(); @@ -251,8 +340,7 @@ function buildActions(input: BuildPolicyAnalysisOverviewInput): PolicyAnalysisAc } } const profilesById = new Map(input.profiles.map(profile => [profile.id, profile])); - const evaluate: PolicyAnalysisEvaluationAction[] = input.aggregations - .filter(aggregation => activeProfileIds.has(aggregation.run.environmentProfileId)).map(aggregation => { + const evaluate: PolicyAnalysisEvaluationAction[] = active.aggregations.map(aggregation => { const profile = profilesById.get(aggregation.run.environmentProfileId); const blocking: string[] = []; const reviewOnly: string[] = []; @@ -294,8 +382,7 @@ function buildActions(input: BuildPolicyAnalysisOverviewInput): PolicyAnalysisAc } as PolicyAnalysisEvaluationAction; }).sort((left, right) => left.id.localeCompare(right.id)); - const createFindings: PolicyAnalysisFindingAction[] = input.evaluations - .filter(evaluation => activeProfileIds.has(evaluation.run.environmentProfileId)).map(evaluation => { + const createFindings: PolicyAnalysisFindingAction[] = active.evaluations.map(evaluation => { const existing = input.findingRuns.find(record => record.run.evaluationRunId === evaluation.run.id); const eligibleResultCount = evaluation.results.filter(result => result.findingEligibility === 'eligible').length; const reasons = evaluation.run.status === 'blocked' ? ['policy-action-evaluation-blocked'] @@ -402,7 +489,12 @@ export function buildPolicyAnalysisOverview(input: BuildPolicyAnalysisOverviewIn } const reviewItems = reviews.sort((left, right) => left.id.localeCompare(right.id)); - const findingByEvaluation = new Map(input.findingRuns.map(record => [record.run.evaluationRunId, record])); + const findingByEvaluation = new Map(latestByKey( + input.findingRuns, + record => record.run.evaluationRunId, + record => record.run.completedAt, + record => record.run.id + ).map(record => [record.run.evaluationRunId, record])); const evaluations = input.evaluations.map(record => { const finding = findingByEvaluation.get(record.run.id); return { @@ -422,30 +514,30 @@ export function buildPolicyAnalysisOverview(input: BuildPolicyAnalysisOverviewIn }; }).sort((left, right) => right.completedAt.localeCompare(left.completedAt) || left.evaluationRunId.localeCompare(right.evaluationRunId)); - const partialRuns = input.identityAnalyses.filter(record => record.run.status === 'partial').length - + input.aggregations.filter(record => record.run.status !== 'complete').length - + input.evaluations.filter(record => record.run.status !== 'complete').length - + input.findingRuns.filter(record => record.run.status === 'partial').length; + const active = activePolicyRecords(input); + const activeRunIds = new Set([ + ...active.identityAnalyses.map(record => record.run.id), + ...active.aggregations.map(record => record.run.id), + ...active.evaluations.map(record => record.run.id), + ...active.findingRuns.map(record => record.run.id) + ]); + const currentReviewItems = reviewItems.filter(item => activeRunIds.has(item.sourceRunId)); return { version: 1, projectId: input.projectId, generatedAt: input.generatedAt, - summary: { - identityAnalysisRuns: input.identityAnalyses.length, - aggregationRuns: input.aggregations.length, - evaluationRuns: input.evaluations.length, - findingRuns: input.findingRuns.length, - configuredMatches: input.evaluations.reduce((sum, record) => sum + record.run.matchCount, 0), - confirmedDeviations: input.evaluations.reduce((sum, record) => sum + record.run.deviationCount, 0), - notApplicable: input.evaluations.reduce((sum, record) => sum + record.run.notApplicableCount, 0), - policyFindings: input.findingRuns.reduce((sum, record) => sum + record.run.findingCount, 0), - reviewItems: reviewItems.length, - conflicts: reviewItems.filter(item => item.category === 'conflict').length, - unresolvedIdentities: reviewItems.filter(item => item.category === 'unresolved-identity').length, - catalogGaps: reviewItems.filter(item => item.category === 'catalog-gap').length, - parserGaps: reviewItems.filter(item => item.category === 'parser-gap').length, - partialRuns + summary: summarizePolicyRecords( + input.identityAnalyses, input.aggregations, input.evaluations, input.findingRuns, reviewItems + ), + current: { + version: 1, + summary: summarizePolicyRecords( + active.identityAnalyses, active.aggregations, active.evaluations, active.findingRuns, currentReviewItems + ), + evaluationRunIds: uniqueSorted(active.evaluations.map(record => record.run.id)), + findingRunIds: uniqueSorted(active.findingRuns.map(record => record.run.id)), + reviewItems: currentReviewItems }, actions: buildActions(input), profiles: input.profiles.map(profile => ({ diff --git a/src/domain/unified-result.ts b/src/domain/unified-result.ts index 626fa6f..14f22ac 100644 --- a/src/domain/unified-result.ts +++ b/src/domain/unified-result.ts @@ -18,13 +18,16 @@ import type { } from './canonical-model'; import type { PolicyFindingOccurrence } from './policy-finding'; import type { EnvironmentCollectionState, EnvironmentObservation, EnvironmentValueType } from './environment-context'; +import type { ImportArtifact, ImportProcessingRun } from './ingestion-persistence'; +import type { StoredGpoBackupInstance, StoredGpoPolicyAnalysis, StoredGpoPolicyIdentityResolution } from './gpo-analysis-persistence'; +import type { GpoPolicyObservation } from './gpo-policy'; import { deriveExposureState, deriveIsIoe, deriveResultType } from './result-contract-mapping'; import { isDeviation } from './deviation-predicate'; import { severityRank } from './severity-order'; import { processingReadiness, type ProcessingReadinessStatus } from './processing-compatibility'; import type { CveEnrichmentRecord } from './cve-enrichment'; -export type UnifiedResultSourceKind = 'scanner-finding' | 'policy-evaluation' | 'environment-observation'; +export type UnifiedResultSourceKind = 'scanner-finding' | 'policy-evaluation' | 'policy-observation' | 'environment-observation'; export type UnifiedOutcome = CanonicalOutcome; export type TechnicalExecutionState = CanonicalTechnicalExecutionState; export type UnifiedReviewStatus = 'unreviewed' | 'open' | 'in-progress' | 'resolved'; @@ -80,6 +83,14 @@ export interface UnifiedResultProjection { environmentNormalizedValue?: JsonValue; environmentSectionKey?: string; environmentAttributes?: Record; + policyScope?: GpoPolicyObservation['scope']; + policyDeliveryMechanism?: GpoPolicyObservation['deliveryMechanism']; + policyState?: GpoPolicyObservation['state']; + policyIdentityStatus?: GpoPolicyIdentityResolutionStatus; + policyIdentityKey?: string; + policyValues?: JsonValue[]; + policyDiagnosticCodes?: string[]; + policyBackupInstanceId?: string; title: string; description?: string; recommendation?: string; @@ -128,6 +139,7 @@ export interface UnifiedResultProjection { export interface UnifiedResultQuery { projectId?: string; assessmentId?: string; + assessmentIds?: string[]; scanRunId?: string; scanRunIds?: string[]; evaluationRunId?: string; @@ -207,6 +219,15 @@ export interface EnvironmentUnifiedResultSource { observation: EnvironmentObservation; } +type GpoPolicyIdentityResolutionStatus = StoredGpoPolicyIdentityResolution['resolution']['status']; + +export interface PolicyObservationUnifiedResultSource { + artifact: ImportArtifact; + backup: StoredGpoBackupInstance; + observation: GpoPolicyObservation; + resolution?: StoredGpoPolicyIdentityResolution; +} + export function outcomeFromLegacyStatus(status: CanonicalFindingStatus): UnifiedOutcome { if (status === 'failed') return 'fail'; if (status === 'passed') return 'pass'; @@ -423,6 +444,137 @@ export function createPolicyUnifiedResult(source: PolicyUnifiedResultSource): Un }; } +function policyObservationTitle(observation: GpoPolicyObservation): string { + const section = observation.identity.section.trim(); + const key = observation.identity.key.trim(); + return [section, key].filter(Boolean).join(' · '); +} + +function policyObservationLocator(observation: GpoPolicyObservation): SourcePosition { + const suffix = observation.sourceLines?.length + ? `#lines=${observation.sourceLines.join(',')}` + : observation.sourceRecords?.length + ? `#records=${observation.sourceRecords.map(record => record.index).join(',')}` + : ''; + return { objectPath: observation.sourcePath + suffix }; +} + +/** + * Projects a parsed GPO setting as reviewable evidence. This is deliberately + * not a policy deviation: only the Finding Factory may create one after a + * complete baseline evaluation. The projection makes imported GPO content + * visible without weakening that safety boundary. + */ +export function createPolicyObservationUnifiedResult(source: PolicyObservationUnifiedResultSource): UnifiedResultProjection { + const { artifact, backup, observation, resolution } = source; + const resolved = resolution?.resolution.status === 'resolved' ? resolution.resolution.identity : undefined; + const unresolvedReason = resolution?.resolution.status === 'unresolved' + ? resolution.resolution.reasonCode + : 'policy-identity-resolution-missing'; + const policyIdentityKey = resolved?.id + ?? `${observation.identity.namespace}:${observation.identity.section}:${observation.identity.key}`; + const diagnosticCodes = [...new Set([ + ...observation.diagnosticCodes, + ...(resolved ? [] : [unresolvedReason]) + ])].sort(); + const needsReview = !resolved || observation.state === 'conflicted' || diagnosticCodes.length > 0; + return { + resultProjectionId: `policy-observation:${observation.id}`, + sourceKind: 'policy-observation', + sourceResultId: observation.id, + projectId: backup.projectId, + assessmentId: artifact.assessmentId, + collectionRunId: backup.processingRunId, + sourceTool: 'group-policy', + sourceProfileId: observation.parser.id, + sourceProfileVersion: observation.parser.version, + mappingRuleVersion: resolved?.resolver.version ?? resolution?.analysisRunId ?? 'unresolved', + normalizationRuleVersion: observation.reducer.version, + presentationVersion: 'gpo-policy-observation-1.0.0', + processingCompatibilityKey: 'gpo-policy-observation-v1', + processingStatus: 'current', + reprocessingAvailable: true, + sourceRuleId: resolved?.id, + importArtifactId: artifact.id, + sourceViewId: backup.id, + sourceLocator: policyObservationLocator(observation), + title: policyObservationTitle(observation), + description: resolved + ? 'Imported Group Policy setting with a resolved technical identity.' + : `Imported Group Policy setting requiring identity review (${unresolvedReason}).`, + severityOriginal: null, + severityNormalized: 'unknown', + outcomeOriginal: observation.state, + outcomeNormalized: needsReview ? 'warning' : 'informational', + technicalExecutionState: 'completed', + resultType: 'informational', + isIoe: false, + isDeviation: false, + effectiveState: resolved ? 'not_evaluated' : 'unresolved_identity', + lifecycleState: 'unknown', + mappingConfidence: resolved ? 1 : 0, + completenessState: backup.instance.inventory.status, + scopeState: 'unknown', + effectivenessVerification: 'not-verified', + scopeConfidence: 0, + reviewStatus: needsReview ? 'open' : 'unreviewed', + sourceObservedAt: backup.instance.backupCreatedAt ?? backup.createdAt, + importedAt: artifact.createdAt, + createdAt: backup.createdAt, + policyScope: observation.scope, + policyDeliveryMechanism: observation.deliveryMechanism, + policyState: observation.state, + policyIdentityStatus: resolved ? 'resolved' : 'unresolved', + policyIdentityKey, + policyValues: observation.values as unknown as JsonValue[], + policyDiagnosticCodes: diagnosticCodes, + policyBackupInstanceId: backup.instance.id, + categories: [{ + namespace: 'group-policy', + categoryCode: observation.deliveryMechanism, + categoryLabel: observation.deliveryMechanism, + categoryType: 'source', + isPrimary: true, + sourceTool: 'group-policy', + sourceProfileId: observation.parser.id, + sourceProfileVersion: observation.parser.version + }] + }; +} + +export function createCurrentPolicyObservationUnifiedResults( + artifacts: ImportArtifact[], + processingRuns: ImportProcessingRun[], + backups: StoredGpoBackupInstance[], + analyses: StoredGpoPolicyAnalysis[] +): UnifiedResultProjection[] { + const artifactById = new Map(artifacts.map(artifact => [artifact.id, artifact])); + const supersededProcessingIds = new Set(processingRuns.map(run => run.supersedesRunId).filter(Boolean)); + const currentProcessingIds = new Set(processingRuns + .filter(run => !supersededProcessingIds.has(run.id) && run.status !== 'failed') + .map(run => run.id)); + const supersededAnalysisIds = new Set(analyses.map(record => record.run.supersedesAnalysisRunId).filter(Boolean)); + const resolutionByObservation = new Map(); + for (const analysis of analyses) { + if (supersededAnalysisIds.has(analysis.run.id) || !currentProcessingIds.has(analysis.run.sourceProcessingRunId)) continue; + for (const resolution of analysis.resolutions) resolutionByObservation.set(resolution.observationId, resolution); + } + return backups + .filter(backup => currentProcessingIds.has(backup.processingRunId)) + .flatMap(backup => { + const artifact = artifactById.get(backup.artifactId); + if (!artifact) return []; + return (backup.instance.policyObservations ?? []).map(observation => createPolicyObservationUnifiedResult({ + artifact, + backup, + observation, + resolution: resolutionByObservation.get(observation.id) + })); + }) + .sort((left, right) => left.importedAt.localeCompare(right.importedAt) + || left.resultProjectionId.localeCompare(right.resultProjectionId)); +} + function environmentOutcome(state: EnvironmentCollectionState): UnifiedOutcome { if (state === 'parse-failed') return 'execution_error'; if (['not-assessed', 'access-denied', 'license-restricted', 'unsupported', 'unknown'].includes(state)) return 'not_evaluated'; @@ -530,8 +682,10 @@ export function filterUnifiedResults(items: UnifiedResultProjection[], query: Un return items.filter(item => { if (query.projectId && item.projectId !== query.projectId) return false; if (query.assessmentId && item.assessmentId !== query.assessmentId) return false; + if (query.assessmentIds?.length && (!item.assessmentId || !query.assessmentIds.includes(item.assessmentId))) return false; if (query.scanRunId && item.scanRunId !== query.scanRunId) return false; - if (query.scanRunIds?.length && (!item.scanRunId || !query.scanRunIds.includes(item.scanRunId))) return false; + if (query.scanRunIds?.length && item.scanRunId && !query.scanRunIds.includes(item.scanRunId)) return false; + if (query.scanRunIds?.length && !item.scanRunId && !query.assessmentIds?.includes(item.assessmentId ?? '')) return false; if (query.evaluationRunId && item.evaluationRunId !== query.evaluationRunId) return false; if (!includesAny(query.sourceKind, item.sourceKind)) return false; if (sourceTools?.length && (!item.sourceTool || !sourceTools.includes(normalizedText(item.sourceTool)))) return false; @@ -540,7 +694,7 @@ export function filterUnifiedResults(items: UnifiedResultProjection[], query: Un if (!includesAny(query.technicalExecutionState, item.technicalExecutionState)) return false; if (!includesAny(query.resultType, item.resultType)) return false; if (query.isIoe !== undefined && item.isIoe !== query.isIoe) return false; - if (query.deviation !== undefined && item.isDeviation !== query.deviation) return false; + if (query.deviation !== undefined && item.sourceKind !== 'policy-observation' && item.isDeviation !== query.deviation) return false; if (!includesAny(query.mappingRuleVersion, item.mappingRuleVersion)) return false; if (!includesAny(query.sourceProfileVersion, item.sourceProfileVersion)) return false; if (!includesAny(query.processingStatus, item.processingStatus)) return false; @@ -568,6 +722,13 @@ export function filterUnifiedResults(items: UnifiedResultProjection[], query: Un environmentState: item.environmentState, environmentOriginalValue: item.environmentOriginalValue, environmentNormalizedValue: item.environmentNormalizedValue, + policyScope: item.policyScope, + policyDeliveryMechanism: item.policyDeliveryMechanism, + policyState: item.policyState, + policyIdentityStatus: item.policyIdentityStatus, + policyIdentityKey: item.policyIdentityKey, + policyValues: item.policyValues, + policyDiagnosticCodes: item.policyDiagnosticCodes, categories: item.categories.map(category => category.categoryLabel) })); if (!text.includes(search)) return false; @@ -601,6 +762,7 @@ function scopeOnlyQuery(query: UnifiedResultQuery): UnifiedResultQuery { return { projectId: query.projectId, assessmentId: query.assessmentId, + assessmentIds: query.assessmentIds, scanRunId: query.scanRunId, scanRunIds: query.scanRunIds, evaluationRunId: query.evaluationRunId @@ -668,6 +830,9 @@ export function deriveUnifiedResultFacets(items: UnifiedResultProjection[], quer return { id: 'category', label: 'Categories', values }; } + const findingAxis = (item: UnifiedResultProjection): boolean => + item.sourceKind !== 'environment-observation' && item.sourceKind !== 'policy-observation'; + return { total: filtered.length, facets: [ @@ -676,16 +841,16 @@ export function deriveUnifiedResultFacets(items: UnifiedResultProjection[], quer facet('environmentState', 'Environment State', item => item.environmentState, 'environmentState'), facet('environmentSubjectType', 'Environment Subject', item => item.environmentSubjectType, 'environmentSubjectType'), facet('environmentProperty', 'Environment Property', item => item.environmentProperty, 'environmentProperty'), - facet('severity', 'Severity', item => item.sourceKind === 'environment-observation' ? undefined : item.severityNormalized, 'severity'), - facet('outcome', 'Outcome', item => item.sourceKind === 'environment-observation' ? undefined : item.outcomeNormalized, 'outcome'), - facet('technicalExecutionState', 'Execution', item => item.sourceKind === 'environment-observation' ? undefined : item.technicalExecutionState, 'technicalExecutionState'), - facet('resultType', 'Result Type', item => item.sourceKind === 'environment-observation' ? undefined : item.resultType, 'resultType'), - facet('isIoe', 'IOE', item => item.sourceKind === 'environment-observation' ? undefined : String(item.isIoe), 'isIoe'), - facet('deviation', 'Deviation', item => item.sourceKind === 'environment-observation' ? undefined : String(item.isDeviation), 'deviation'), + facet('severity', 'Severity', item => findingAxis(item) ? item.severityNormalized : undefined, 'severity'), + facet('outcome', 'Outcome', item => findingAxis(item) ? item.outcomeNormalized : undefined, 'outcome'), + facet('technicalExecutionState', 'Execution', item => findingAxis(item) ? item.technicalExecutionState : undefined, 'technicalExecutionState'), + facet('resultType', 'Result Type', item => findingAxis(item) ? item.resultType : undefined, 'resultType'), + facet('isIoe', 'IOE', item => findingAxis(item) ? String(item.isIoe) : undefined, 'isIoe'), + facet('deviation', 'Deviation', item => findingAxis(item) ? String(item.isDeviation) : undefined, 'deviation'), facet('mappingRuleVersion', 'Mapping Rule Set', item => item.mappingRuleVersion, 'mappingRuleVersion'), facet('sourceProfileVersion', 'Profile Version', item => item.sourceProfileVersion, 'sourceProfileVersion'), facet('processingStatus', 'Processing Status', item => item.processingStatus, 'processingStatus'), - facet('reviewStatus', 'Review Status', item => item.sourceKind === 'environment-observation' ? undefined : item.reviewStatus), + facet('reviewStatus', 'Review Status', item => findingAxis(item) ? item.reviewStatus : undefined), facet('reviewDisposition', 'Review Disposition', item => item.reviewDisposition, 'reviewDisposition'), facet('lifecycleState', 'Lifecycle', item => item.lifecycleState, 'lifecycleState'), facet('effectiveState', 'Effectiveness', item => item.effectiveState), diff --git a/src/exports/unified-result-jsonl.ts b/src/exports/unified-result-jsonl.ts new file mode 100644 index 0000000..fa26eb0 --- /dev/null +++ b/src/exports/unified-result-jsonl.ts @@ -0,0 +1,72 @@ +import type { + UnifiedResultProjection, + UnifiedResultQuery, + UnifiedResultQueryResult +} from '../domain/unified-result'; + +export const RESULT_DB_JSONL_FORMAT = 'assessor-result-db-jsonl'; +export const RESULT_DB_JSONL_SCHEMA_VERSION = '1.0.0'; +export const RESULT_DB_EXPORT_PAGE_SIZE = 1000; + +type ResultPageReader = (query: UnifiedResultQuery) => Promise; + +function definedQueryEntries(query: UnifiedResultQuery): Array<[string, unknown]> { + return Object.entries(query).filter(([, value]) => value !== undefined); +} + +function exportScope(query: UnifiedResultQuery): Record { + const scopeKeys = new Set(['projectId', 'assessmentId', 'scanRunId', 'scanRunIds', 'evaluationRunId']); + return Object.fromEntries(definedQueryEntries(query).filter(([key]) => scopeKeys.has(key))); +} + +function exportFilters(query: UnifiedResultQuery): Record { + const excluded = new Set(['projectId', 'assessmentId', 'scanRunId', 'scanRunIds', 'evaluationRunId', 'offset', 'limit']); + return Object.fromEntries(definedQueryEntries(query).filter(([key]) => !excluded.has(key))); +} + +export function buildUnifiedResultJsonl( + query: UnifiedResultQuery, + results: UnifiedResultProjection[], + generatedAt: string +): string { + const lines = [{ + recordType: 'manifest', + format: RESULT_DB_JSONL_FORMAT, + schemaVersion: RESULT_DB_JSONL_SCHEMA_VERSION, + generatedAt, + resultCount: results.length, + scope: exportScope(query), + filters: exportFilters(query) + }, ...results.map((result, index) => ({ + recordType: 'result', + position: index + 1, + result + }))]; + return `${lines.map(line => JSON.stringify(line)).join('\n')}\n`; +} + +export function unifiedResultJsonlFileName(generatedAt: string): string { + const timestamp = generatedAt.replace(/\.\d{3}Z$/, 'Z').replaceAll(':', '-'); + return `assessor-result-db-filtered-${timestamp}.jsonl`; +} + +export async function queryAllUnifiedResults( + readPage: ResultPageReader, + query: UnifiedResultQuery, + pageSize = RESULT_DB_EXPORT_PAGE_SIZE +): Promise { + const baseQuery = { ...query, offset: undefined, limit: undefined }; + const results: UnifiedResultProjection[] = []; + let expectedTotal: number | undefined; + + while (expectedTotal === undefined || results.length < expectedTotal) { + const page = await readPage({ ...baseQuery, offset: results.length, limit: pageSize }); + if (expectedTotal === undefined) expectedTotal = page.total; + if (page.total !== expectedTotal) throw new Error('The filtered result set changed during export. Please retry.'); + if (!page.items.length && results.length < expectedTotal) throw new Error('The filtered result export ended before all rows were returned.'); + results.push(...page.items); + } + + if (results.length !== expectedTotal) throw new Error('The filtered result export returned an inconsistent row count.'); + return results; +} diff --git a/src/features/dashboard/DashboardPanel.tsx b/src/features/dashboard/DashboardPanel.tsx index 7575cfe..03f51ff 100644 --- a/src/features/dashboard/DashboardPanel.tsx +++ b/src/features/dashboard/DashboardPanel.tsx @@ -1,7 +1,8 @@ -import { useEffect, useMemo, useState, type CSSProperties, type ReactNode } from 'react'; +import { useEffect, useId, useMemo, useState, type CSSProperties, type ReactNode } from 'react'; import { AssessorApiClient } from '../../api/assessor-api-client'; import { useApp } from '../../app/AppProvider'; import { EmptyWorkspace } from '../../components/EmptyWorkspace/EmptyWorkspace'; +import { TriangleIcon } from '../../components/TriangleIcon'; import type { EnvironmentObservation, EnvironmentSnapshot } from '../../domain/environment-context'; import type { FindingRecord } from '../../domain/finding'; import type { ImportProfile } from '../../domain/import-profile'; @@ -401,6 +402,8 @@ interface DashboardEnvironmentMetric { value: number; detail?: string; restricted?: boolean; + tone?: string; + onClick?(): void; } interface DashboardEnvironmentSummary { @@ -461,8 +464,7 @@ export function dashboardEnvironmentSummary(snapshot?: EnvironmentSnapshot): Das { label: 'Domain FL', value: observationText(snapshot, 'domain.functional_level', 'Domain Functional Level') }, { label: 'Forest FL', value: observationText(snapshot, 'forest.functional_level', 'Forest Functional Level') }, { label: 'Schema', value: observationText(snapshot, 'forest.schema_version', 'Schema version') }, - { label: 'Domain Controllers', value: observationText(snapshot, 'domain.controller_count', 'DC count') }, - { label: 'PingCastle', value: observationText(snapshot, 'report.engine_version', 'PingCastle engine version') } + { label: 'Domain Controllers', value: observationText(snapshot, 'domain.controller_count', 'DC count') } ], coverage: [ { label: 'Observed', value: snapshot?.coverage.observed ?? 0 }, @@ -523,18 +525,23 @@ interface DashboardMetricTileProps { label: string; value: string; tone?: string; + emphasis?: boolean; active?: boolean; onClick?(): void; } -function DashboardMetricTile({ label, value, tone = 'neutral', active, onClick }: DashboardMetricTileProps) { +function DashboardMetricTile({ label, value, tone = 'neutral', emphasis = false, active, onClick }: DashboardMetricTileProps) { const content = <>{label}{value}; - const className = `client-info-metric tone-${tone}`; + const className = `client-info-metric tone-${tone}${emphasis ? ' is-emphasis' : ''}`; return onClick ? :
{content}
; } +function DashboardFactList({ items }: { items: Array<{ label: string; value: string }> }) { + return
{items.map(item =>
{item.label}
{item.value}
)}
; +} + interface DashboardDonutSegment { label: string; value: number; @@ -566,22 +573,59 @@ function DashboardDonut({ total, centerLabel, segments }: { total: number; cente
; } -function DashboardFactList({ items }: { items: Array<{ label: string; value: string }> }) { - return
{items.map(item =>
{item.label}
{item.value}
)}
; -} - -function DashboardMetricBars({ items, tone = 'accent' }: { items: DashboardEnvironmentMetric[]; tone?: string }) { +export function DashboardMetricBars({ items, tone = 'accent' }: { items: DashboardEnvironmentMetric[]; tone?: string }) { const maximum = Math.max(1, ...items.map(item => item.value)); - return
    {items.map(item =>
  • - {item.label} - - {item.value.toLocaleString('en-US')}{item.detail ? ` ${item.detail}` : ''} - {item.restricted && Restricted} -
  • )}
; + const ordinalTones = ['critical', 'high', 'medium', 'accent', 'neutral']; + return
    {items.map((item, index) => { + const resolvedTone = item.tone ?? (tone === 'accent' ? ordinalTones[index % ordinalTones.length] : tone); + const content = <> + {item.label} + {item.value.toLocaleString('en-US')}{item.detail && {item.detail}} + {item.restricted ? 'Restricted' : '\u00a0'} + + ; + return
  • {item.onClick + ? + :
    {content}
    } +
  • ; + })}
; } -function DashboardLargeCard({ title, meta, className = '', children }: { title: string; meta?: string; className?: string; children: ReactNode }) { - return

{title}

{meta && {meta}}
{children}
; +interface DashboardCardSummary { + label: string; + value: number | string; + detail?: string; +} + +export function DashboardLargeCard({ title, meta, summary, className = '', details, initiallyExpanded = true, children }: { title: string; meta?: string; summary?: DashboardCardSummary; className?: string; details?: ReactNode; initiallyExpanded?: boolean; children?: ReactNode }) { + const [expanded, setExpanded] = useState(initiallyExpanded); + const detailsId = useId(); + return
+
+

{title}

{summary && — {summary.label} {typeof summary.value === 'number' ? summary.value.toLocaleString('en-US') : summary.value}{summary.detail ? ` ${summary.detail}` : ''}}{meta && {meta}}
+ +
+ {expanded &&
+
{children}
+ {details &&
{details}
} +
} +
; +} + +export function DashboardDonutCard({ title, meta, initiallyExpanded = true, children }: { title: string; meta: string; initiallyExpanded?: boolean; children: ReactNode }) { + const [expanded, setExpanded] = useState(initiallyExpanded); + const detailsId = useId(); + return
+
+

{title}

— {meta}
+ +
+ {expanded &&
{children}
} +
; } export function DashboardPanel({ onOpenWork }: DashboardPanelProps) { @@ -644,6 +688,7 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) { const explicitIoes = explicitIoeRecordIds.length; const complianceScore = passed + deviations > 0 ? Math.round((passed / (passed + deviations)) * 100) : 0; const highSeverity = severityFacet?.values.find(value => value.value === 'high')?.count ?? 0; + const classified = severityDistributionValues.reduce((sum, value) => sum + value.count, 0); const environmentSummary = dashboardEnvironmentSummary(environment.snapshot); const typedObservationCount = environment.snapshot?.observations.length ?? 0; const directorySectionCount = environment.snapshot?.sections.length ?? 0; @@ -667,17 +712,17 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {

+
- + jump(() => setStatusFilter('deviation'))} /> - jump(() => setStatusFilter('passed'))} /> severityFacet && jump(() => setFacetFilter(severityFacet.id, { values: ['high'] }))} /> +
-
- + jump(() => setStatusFilter('deviation')) }, { label: 'Passed', value: passed, tone: 'passed', onClick: () => jump(() => setStatusFilter('passed')) }, @@ -686,10 +731,63 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) { { label: 'Error', value: errors, tone: 'error', onClick: () => jump(() => setStatusFilter('error')) }, { label: 'IOEs found', value: explicitIoes, tone: 'info', onClick: explicitIoes > 0 ? () => jump(() => { setStatusFilter('all'); setPathFindingIds(explicitIoeRecordIds); }) : undefined } ]} /> + +
+ +
+
+
+ {typedObservationCount.toLocaleString('en-US')} typed observations · {directorySectionCount.toLocaleString('en-US')} sections

}> +
- sum + value.count, 0).toLocaleString('en-US')} classified`}> - sum + value.count, 0))} centerLabel="classified" segments={severityDistributionValues.map(value => ({ + {environmentSummary.certificates.length.toLocaleString('en-US')} available certificate metrics

}> + +
+ + + reviews[record.id]?.include).length.toLocaleString('en-US') }, + ...environmentSummary.resilienceFacts + ]} /> + +
+ +
+ {environmentSummary.identity.length.toLocaleString('en-US')} available identity metrics

}> + +
+ + {environmentSummary.gpo.length.toLocaleString('en-US')} available policy metrics

}> + +
+
+ +
+ {environmentSummary.controlPaths.length.toLocaleString('en-US')} available control-path metrics

}> + +
+ + + + +
+
+ +
diff --git a/src/features/dataset-viewer/DatasetViewer.tsx b/src/features/dataset-viewer/DatasetViewer.tsx index 51a23bd..b158aef 100644 --- a/src/features/dataset-viewer/DatasetViewer.tsx +++ b/src/features/dataset-viewer/DatasetViewer.tsx @@ -36,14 +36,16 @@ import { recordTitleFor } from './record-title'; */ const RECORD_ROW_HEIGHT = 78; /** Verbindliche Inhaltskachelbreite, siehe `--tile-content-width`. */ -const TILE_MIN_WIDTH = 320; +const TILE_MIN_WIDTH = 358; +/** Verbindliche Mindesthoehe der regulaeren Finding-Kachel. */ +const TILE_MIN_HEIGHT = 223; /** Einheitlicher Segmentabstand, siehe `--segment-gap`. */ const TILE_GAP = 12; /** `padding` von `.tile-grid` (--panel-inset-Token, 20px je Seite). */ const TILE_GRID_INSET = 20; /** Geschaetzte Kachelzeilenhoehe inklusive Zeilenabstand, aus demselben Grund * wie {@link RECORD_ROW_HEIGHT} nur eine Naeherung fuer die Scrollmathematik. */ -const TILE_ROW_HEIGHT = 172; +const TILE_ROW_HEIGHT = TILE_MIN_HEIGHT + TILE_GAP; const resultColors = [ '#7c3aed', // Violet @@ -317,7 +319,7 @@ export function DatasetViewer() { // literalen Inline-Style-Objekts, damit die Darstellung sonst in // src/styles bleibt (siehe FloatingResultsWindow.tsx fuer denselben // Kompromiss bei einer anderen laufzeitberechneten Position). - const tileGridStyle = { gridTemplateColumns: `repeat(${tileColumns}, minmax(var(--tile-content-width), 1fr))` }; + const tileGridStyle = { gridTemplateColumns: `repeat(${tileColumns}, var(--tile-content-width))` }; const tileTopSpacerStyle = { gridColumn: '1 / -1', height: tileWindow.paddingTop }; const tileBottomSpacerStyle = { gridColumn: '1 / -1', height: tileWindow.paddingBottom }; const recordTopSpacerStyle = { height: recordWindow.paddingTop }; diff --git a/src/features/linked-results/LinkedResults.tsx b/src/features/linked-results/LinkedResults.tsx index ac8d1e4..3cdf325 100644 --- a/src/features/linked-results/LinkedResults.tsx +++ b/src/features/linked-results/LinkedResults.tsx @@ -4,6 +4,30 @@ import { RichTextValue } from '../../components/RichTextValue/RichTextValue'; import { ResizableResultTable } from '../../components/ResizableResultTable/ResizableResultTable'; import type { ResultSection } from './result-message'; import type { ResultSheetRowTone } from './result-message'; +import type { MaesterExecutionError } from './maester-error'; + +function MaesterErrorResult({ error, search }: { error: MaesterExecutionError; search: string }) { + const facts = [ + { label: 'Error code', value: error.code }, + { label: 'HTTP status', value: error.httpStatus }, + { label: 'Request', value: [error.method, error.endpoint].filter(Boolean).join(' ') || undefined }, + { label: 'Source', value: error.source }, + { label: 'Timestamp', value: error.timestamp }, + { label: 'Request ID', value: error.requestId }, + { label: 'Client request ID', value: error.clientRequestId } + ].filter((fact): fact is { label: string; value: string } => Boolean(fact.value)); + + return
+

{highlighted(error.message, search)}

+ {facts.length > 0 &&
{facts.map(fact =>
+
{fact.label}
{highlighted(fact.value, search)}
+
)}
} +
+ Technical details +
{highlighted(error.raw, search)}
+
+
; +} function highlighted(value: string, search: string): ReactNode { if (!search) return value; @@ -53,7 +77,9 @@ export function LinkedResults({ sheets, search = '', heading, sections = [], sho {sections.map(section => (

{section.title}

- + {section.diagnostic + ? + : }
))} {sheets.map((sheet, index) => { diff --git a/src/features/linked-results/maester-error.ts b/src/features/linked-results/maester-error.ts new file mode 100644 index 0000000..c7d14f2 --- /dev/null +++ b/src/features/linked-results/maester-error.ts @@ -0,0 +1,69 @@ +export interface MaesterExecutionError { + message: string; + code?: string; + method?: string; + endpoint?: string; + httpStatus?: string; + source?: string; + requestId?: string; + clientRequestId?: string; + timestamp?: string; + raw: string; +} + +function embeddedError(value: string): Record | undefined { + const start = value.search(/\{\s*"error"\s*:/i); + if (start < 0) return undefined; + let depth = 0; + let quoted = false; + let escaped = false; + for (let index = start; index < value.length; index += 1) { + const character = value[index]; + if (quoted) { + if (escaped) escaped = false; + else if (character === '\\') escaped = true; + else if (character === '"') quoted = false; + continue; + } + if (character === '"') quoted = true; + else if (character === '{') depth += 1; + else if (character === '}' && --depth === 0) { + try { + const parsed = JSON.parse(value.slice(start, index + 1)) as { error?: Record }; + return parsed.error; + } catch { + return undefined; + } + } + } + return undefined; +} + +function text(value: unknown): string | undefined { + return typeof value === 'string' && value.trim() ? value.trim() : undefined; +} + +export function parseMaesterExecutionError(value: string): MaesterExecutionError | undefined { + const raw = value.trim().replace(/^```\w*\s*/, '').replace(/\s*```$/, '').trim(); + if (!/an error occurred while running the test/i.test(raw) || !/Invoke-MgGraphRequest/i.test(raw)) return undefined; + + const request = /\b(GET|POST|PUT|PATCH|DELETE)\s+(https:\/\/[^\s|]+)\s+HTTP\/\d(?:\.\d)?\s+(\d{3})\s+(.+?)(?=\s+(?:Cache-Control|Vary|Strict-Transport-Security|request-id|Content-Type)\b|\s*\||\s*\{)/i.exec(raw); + const location = /Invoke-MgGraphRequest:\s+(.+?\.ps1):(\d+)/i.exec(raw); + const error = embeddedError(raw); + const inner = error && typeof error.innerError === 'object' && error.innerError !== null + ? error.innerError as Record + : undefined; + + return { + message: text(error?.message) ?? 'An error occurred while running the Maester test.', + code: text(error?.code), + method: request?.[1]?.toUpperCase(), + endpoint: request?.[2], + httpStatus: request ? `${request[3]} ${request[4].trim()}` : undefined, + source: location ? `${location[1].split(/[\\/]/).pop()} · line ${location[2]}` : undefined, + requestId: text(inner?.['request-id']) ?? text(error?.['request-id']), + clientRequestId: text(inner?.['client-request-id']) ?? text(error?.['client-request-id']), + timestamp: text(inner?.date) ?? text(error?.date), + raw + }; +} diff --git a/src/features/linked-results/result-message.ts b/src/features/linked-results/result-message.ts index b4a188f..d4678c2 100644 --- a/src/features/linked-results/result-message.ts +++ b/src/features/linked-results/result-message.ts @@ -2,11 +2,13 @@ import type { FindingRecord, RecordField } from '../../domain/finding'; import type { DatasetSheet } from '../../domain/dataset'; import { toolIdForRecord } from '../../importers/status-normalization'; import { toolProfileDefinitions } from '../../profiles/profile-definition-catalog'; +import { parseMaesterExecutionError, type MaesterExecutionError } from './maester-error'; /** Ein benannter Ergebnisabschnitt des Results-Fensters. */ export interface ResultSection { title: string; value: string; + diagnostic?: MaesterExecutionError; } @@ -173,9 +175,15 @@ export function resultSectionFields(record: FindingRecord, fallbackToolId?: stri * in Feldreihenfolge. */ export function resultSectionsFor(record: FindingRecord, fallbackToolId?: string): ResultSection[] { + const toolId = toolIdForRecord(record, fallbackToolId); const message = resultMessageFor(record, fallbackToolId); + const diagnostic = toolId === 'maester' && message ? parseMaesterExecutionError(message) : undefined; return [ - ...(message ? [{ title: TOOL_MESSAGE_TITLE, value: message }] : []), + ...(message ? [{ + title: TOOL_MESSAGE_TITLE, + value: message, + ...(diagnostic ? { diagnostic } : {}) + }] : []), ...resultSectionFields(record, fallbackToolId).map(field => ({ title: field.label, value: field.value })) ]; } diff --git a/src/features/project-import/DataHub.tsx b/src/features/project-import/DataHub.tsx index 8c6d84f..7b2a409 100644 --- a/src/features/project-import/DataHub.tsx +++ b/src/features/project-import/DataHub.tsx @@ -7,6 +7,7 @@ import { FileImport } from '../file-import/FileImport'; import { stableAssessmentId, stableProjectId } from '../../shared/identifiers'; import { groupProjectImportFiles, type ScanImportGroup } from './datahub-import-groups'; import { GpoFolderImport } from './GpoFolderImport'; +import { PolicyAnalysisPanel } from './PolicyAnalysisPanel'; import { MicrosoftBaselineLibrary } from './MicrosoftBaselineLibrary'; import { MitreMatrixLibrary } from './MitreMatrixLibrary'; import { clearStoredMitreMatrix, loadStoredMitreMatrix, saveStoredMitreMatrix, validateMitreMatrixData } from '../mitre-graph/mitre-graph'; @@ -40,6 +41,16 @@ interface SaveFeedback { message: string; } type ScanReadinessState = DatahubScanReadiness | 'checking' | 'unavailable'; +type TenantDetailSection = 'client-information' | 'gpo-analysis'; + +const gpoStateLabels: Record = { + queued: 'Queued', + running: 'Processing', + succeeded: 'Ready', + partial: 'Partial', + 'dead-letter': 'Failed', + 'recovery-required': 'Recovery pending' +}; function projectConsultant(project: AssessmentProject): string { return typeof project.attributes.consultant === 'string' ? project.attributes.consultant : ''; @@ -107,6 +118,7 @@ export function DataHub({ activeProjectId, onProjectSelected, onProjectDataChang const [timeslotDialogOpen, setTimeslotDialogOpen] = useState(false); const [timeslotDate, setTimeslotDate] = useState(''); const [scanReadiness, setScanReadiness] = useState>({}); + const [detailSection, setDetailSection] = useState('client-information'); const projectModelsCache = useRef(new Map()); useEffect(() => { @@ -178,7 +190,11 @@ export function DataHub({ activeProjectId, onProjectSelected, onProjectDataChang useEffect(() => { const refresh = () => { void loadProjects(); }; globalThis.addEventListener('assessor:datahub-changed', refresh); - return () => globalThis.removeEventListener('assessor:datahub-changed', refresh); + globalThis.addEventListener('assessor:gpo-changed', refresh); + return () => { + globalThis.removeEventListener('assessor:datahub-changed', refresh); + globalThis.removeEventListener('assessor:gpo-changed', refresh); + }; }, [loadProjects]); const groupsForProject = useCallback((projectId: string): ScanImportGroup[] => { @@ -289,6 +305,7 @@ export function DataHub({ activeProjectId, onProjectSelected, onProjectDataChang setMasterDataEditing(true); setExcludedScanRunIds([]); setExpandedTimeslotIds([]); + setDetailSection('client-information'); setForm({ mode: 'create', name: '', description: '', consultant: '' }); }; const openEdit = (project: AssessmentProject) => { @@ -306,6 +323,7 @@ export function DataHub({ activeProjectId, onProjectSelected, onProjectDataChang setExpandedTimeslotIds(affectedTimeslotIds); setTimeslotDialogOpen(false); setTimeslotDeleteTarget(null); + setDetailSection('client-information'); setForm({ mode: 'edit', project, name: project.name, description: project.description ?? '', consultant: projectConsultant(project) }); }; const updateForm = (patch: Partial) => { @@ -509,6 +527,38 @@ export function DataHub({ activeProjectId, onProjectSelected, onProjectDataChang setBusy(false); } }; + const retryGpoBackup = async (record: GpoBackupStatusRecord) => { + const projectId = form?.project?.id; + if (!projectId) return; + setBusy(true); + setError(null); + setSaveFeedback(null); + try { + await assessorApi.retryGpoBackup(projectId, record.artifactId); + await loadProjects(); + setSaveFeedback({ tone: 'success', message: 'The GPO analysis job was requeued.' }); + } catch (reason) { + setError(reason instanceof Error ? reason.message : 'The GPO analysis job could not be restarted.'); + } finally { + setBusy(false); + } + }; + const deleteFailedGpoBackup = async (record: GpoBackupStatusRecord) => { + const projectId = form?.project?.id; + if (!projectId || !globalThis.confirm(`Permanently delete the failed GPO import "${record.fileName}" from ASSESSOR? The source remains unchanged.`)) return; + setBusy(true); + setError(null); + setSaveFeedback(null); + try { + await assessorApi.deleteFailedGpoBackup(projectId, record.artifactId); + await loadProjects(); + setSaveFeedback({ tone: 'success', message: 'The failed GPO import was deleted from ASSESSOR.' }); + } catch (reason) { + setError(reason instanceof Error ? reason.message : 'The failed GPO import could not be deleted.'); + } finally { + setBusy(false); + } + }; if (backendStatus !== 'connected') { return
{/* Kein Eyebrow, kein Inhaltstitel: den Seitennamen traegt die Topbar @@ -567,10 +617,10 @@ export function DataHub({ activeProjectId, onProjectSelected, onProjectDataChang { if (element) element.indeterminate = timeslotIndeterminate; }} type="checkbox" checked={timeslotChecked} aria-label={`Select timeslot ${formatTenantDate(group.fileDate)}`} onChange={event => toggleTimeslot(group.id, event.target.checked)}/> {formatTenantDate(group.fileDate)} - {reports.length.toLocaleString('de-DE')} + {(reports.length + group.gpoBackups.length).toLocaleString('de-DE')} - {reports.length === 0 ?

No scan reports

This timeslot does not contain a scan report yet.

:
-
+ {reports.length === 0 && group.gpoBackups.length === 0 ?

No sources

This timeslot does not contain a scan report or GPO backup yet.

:
+
{reports.map(report => { const readiness = scanReadiness[report.scanRunId] ?? 'checking'; const processing = processingByRun.get(report.scanRunId)!; @@ -596,6 +646,16 @@ export function DataHub({ activeProjectId, onProjectSelected, onProjectDataChang
; })} + {group.gpoBackups.map(record =>
+ G + GPO Backup{record.fileName} · {record.gpoCount.toLocaleString('de-DE')} GPO{record.gpoCount === 1 ? '' : 's'} + {formatHistoryDate(record.submittedAt)} + {gpoStateLabels[record.state]} + {record.state === 'dead-letter' ? + + + : } +
)}
} {affectedReports.length > 0 &&
{hasProcessingError ? 'ASSESSOR processing issue' : 'New ASSESSOR processing profile available'}{hasProcessingError @@ -621,7 +681,7 @@ export function DataHub({ activeProjectId, onProjectSelected, onProjectDataChang {selected &&
- +
} ; })} @@ -630,17 +690,17 @@ export function DataHub({ activeProjectId, onProjectSelected, onProjectDataChang {form ?
-
+ {detailSection === 'client-information' ?
Client Information
-
{form.project && !masterDataEditing && }{form.mode === 'edit' && !masterDataEditing && }{(form.mode === 'create' || masterDataEditing) && }
+
{form.project && projectGpoData[form.project.id]?.length > 0 && !masterDataEditing && }{form.project && !masterDataEditing && }{form.mode === 'edit' && !masterDataEditing && }{(form.mode === 'create' || masterDataEditing) && }
{error &&
{error}
} {saveFeedback &&
{saveFeedback.message}
} @@ -649,7 +709,13 @@ export function DataHub({ activeProjectId, onProjectSelected, onProjectDataChang