From 250c213fc16cb1cb8f3ed5017975a3d0818fb47f Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 4 Oct 2026 19:58:04 +0200 Subject: [PATCH] Business Impact + Management Report --- AGENTS.md | 7 +- docs/CLIENT_INFORMATION.md | 105 +++++ docs/IMPLEMENTATION_PLAN.md | 418 ++++++++++++++++++ docs/REPORT_STRATEGY.md | 18 + .../context-pass-gaps-dummy-2026-10-04.md | 231 ++++++++++ package-lock.json | 26 +- package.json | 2 +- server/assessor-http-server.ts | 49 +- .../postgres/postgres-assessor-repository.ts | 18 +- server/postgres/revision-read-cache.ts | 50 +++ .../reference-recognition-service.ts | 7 +- src/analysis/record-builder.ts | 3 +- src/api/assessor-api-client.ts | 13 +- src/app/AppProvider.tsx | 8 +- src/assessment-context/context-catalog.ts | 47 +- .../context-cluster-severity.ts | 164 +++++++ src/assessment-context/context-engine.ts | 45 +- src/assessment-context/context-heatmap.ts | 81 ++++ .../context-report-publication.ts | 46 ++ src/assessment-context/context-run.ts | 34 +- src/components/ActivityCenter.tsx | 87 +++- .../FilterEmptyState/FilterEmptyState.tsx | 34 ++ src/domain/client-information.ts | 123 ++++++ src/domain/cloud-information.ts | 164 +++++++ src/domain/environment-context.ts | 2 + src/domain/processing-compatibility.ts | 3 +- src/evaluation/security-domains.ts | 183 ++++++++ src/features/context-layer/ContextHeatmap.tsx | 44 ++ .../context-layer/context-cluster-facet.ts | 3 + .../context-layer/context-heatmap.css | 25 ++ .../dashboard/ClientInformationBrowser.tsx | 111 +++++ .../dashboard/CloudInformationTile.tsx | 37 ++ src/features/dashboard/DashboardPanel.tsx | 141 ++++-- .../dashboard/EnvironmentEvidenceTables.tsx | 2 +- .../dashboard/EnvironmentResultsButton.tsx | 7 +- .../dashboard/OperatingSystemInventory.tsx | 6 +- .../dashboard/environment-result-evidence.ts | 21 +- .../dashboard/operating-system-inventory.ts | 18 + src/features/dashboard/security-domains.ts | 91 +--- src/features/dataset-viewer/DatasetViewer.tsx | 19 +- .../db-structure/DataFlowRelationships.tsx | 144 ++++++ .../db-structure/DbStructurePanel.tsx | 22 +- src/features/db-structure/EngineDataFlow.tsx | 13 +- .../db-structure/db-structure-flow.ts | 4 +- .../mitre-graph/ObsidianGraphView.tsx | 4 +- .../ClientInformationChangeDialog.tsx | 36 ++ src/features/project-import/DataHub.tsx | 19 +- .../project-import/DatahubHistoryOverview.tsx | 4 +- .../ReferenceRecognitionPanel.tsx | 4 + .../canonical-model-to-dataset.ts | 39 +- .../datahub-processing-readiness.ts | 6 +- .../policy-observations-to-dataset.ts | 33 +- .../results-browser/ResultsBrowser.tsx | 3 +- .../search-filter/DynamicFilterControls.tsx | 3 +- .../search-filter/business-impact-controls.ts | 36 ++ src/features/search-filter/dynamic-facets.ts | 14 +- src/features/search-filter/filter-records.ts | 8 +- .../search-filter/search-all-findings.ts | 11 + src/features/search-filter/work-facets.ts | 10 +- .../sidebar/SidebarSecurityDomains.tsx | 32 +- src/importers/maester-client-information.ts | 72 +++ src/importers/maester-environment-context.ts | 4 + .../maester-html-source-extractor.ts | 2 +- .../maester-table-context-source-extractor.ts | 45 ++ src/importers/source-extractors.ts | 2 + src/main.tsx | 1 + src/persistence/assessor-repository.ts | 2 + .../client-information-read-port.ts | 60 +++ src/persistence/local-assessor-repository.ts | 5 +- src/platform/activity-store.ts | 32 +- .../cis-cat/cis-cat-security-domains.ts | 51 +++ .../group-policy-security-domains.ts | 82 ++++ src/profiles/maester/maester-definition.ts | 4 +- .../maester/maester-security-domains.ts | 56 +++ .../pingcastle/pingcastle-security-domains.ts | 60 +++ .../purple-knight-security-domains.ts | 25 ++ src/profiles/security-domain-mapping.ts | 49 ++ src/reporting/MANAGEMENT_REPORT_TEMPLATE.md | 160 +++++++ src/reporting/client-report-projection.ts | 30 +- src/reporting/context-management-report.ts | 170 +++++++ src/reporting/management-business-impact.ts | 98 ++++ src/reporting/management-overview-report.ts | 5 +- src/reporting/management-report-texts.ts | 157 +++++++ src/reporting/report-brand.ts | 2 + src/reporting/report-context.ts | 31 ++ src/reporting/report-font.ts | 2 + src/reporting/report-input-assembler.ts | 15 +- src/reporting/report-projection.ts | 13 +- src/reporting/report-read-port.ts | 4 + src/reporting/report-run.ts | 33 +- src/reporting/technical-report.ts | 1 + src/styles/components.css | 37 +- src/styles/datahub-redesign.css | 4 + src/styles/datahub.css | 82 +++- src/styles/filter-empty-state.css | 14 + src/styles/obsidian-graph.css | 2 +- src/styles/ui-tokens.css | 1 + tests/activity-center-preview.html | 27 ++ tests/app/app-version.test.ts | 2 +- .../architecture/core-engine-boundary.test.ts | 6 +- .../context-boundary.test.ts | 21 +- .../assessment-context/context-engine.test.ts | 30 +- .../context-heatmap-fixture.ts | 58 +++ .../context-heatmap-ui.test.tsx | 161 +++++++ .../context-heatmap.test.ts | 108 +++++ .../context-read-reuse.test.ts | 70 +++ .../context-report-publication.test.ts | 28 ++ tests/assessment-context/heatmap-preview.html | 18 + tests/catalog-update-preview.html | 34 ++ tests/client-data-facts-preview.html | 25 ++ tests/client-info-layout-preview.html | 1 + ...ient-information-confirmation-preview.html | 29 ++ tests/client-information-fixture.ts | 21 + tests/client-information-preview.html | 24 + tests/cloud-information-fixture.ts | 20 + tests/cloud-information-preview.html | 28 ++ tests/components/activity-center.test.tsx | 197 ++++++++- tests/domain/cloud-information.test.ts | 67 +++ tests/engine-outline-preview.html | 16 + tests/eol-os-preview.html | 22 + tests/evaluation/security-domains.test.ts | 142 ++++++ .../features/client-business-impact.test.tsx | 122 +++++ .../client-data-fact-results.test.tsx | 101 +++++ .../client-info-action-colors.test.tsx | 49 ++ tests/features/client-info-facts.test.tsx | 9 +- tests/features/client-info-workspace.test.tsx | 103 +++++ .../client-information-browser.test.tsx | 151 +++++++ tests/features/client-report-buttons.test.tsx | 30 +- tests/features/context-cluster-facet.test.tsx | 33 +- .../dashboard-derivation-cost.test.ts | 21 +- .../features/dashboard-domain-filter.test.ts | 28 +- .../dashboard-environment-detail.test.ts | 5 +- tests/features/dashboard-layout.test.ts | 14 +- .../features/datahub-centered-layout.test.ts | 2 +- ...reference-recognition-interaction.test.tsx | 103 ++++- tests/features/db-structure-flow.test.tsx | 166 ++++++- .../domain-information-results.test.tsx | 68 +++ tests/features/dynamic-facets.test.ts | 76 ++-- tests/features/filter-empty-state.test.tsx | 98 ++++ tests/features/finding-context-layer.test.tsx | 2 +- .../gpo-evaluation-visibility.test.tsx | 8 +- tests/features/gpo-filter-context.test.ts | 2 +- tests/features/gpo-source-navigation.test.tsx | 2 +- .../operating-system-inventory.test.tsx | 52 ++- ...reference-recognition-interaction.test.tsx | 36 +- .../features/report-security-domains.test.ts | 52 +-- ...ecurity-domain-filter-interaction.test.tsx | 57 ++- .../sidebar-security-domains.test.tsx | 37 +- .../source-selection-stays-in-view.test.tsx | 15 +- tests/filter-empty-state-preview.html | 22 + .../maester-client-information.test.ts | 77 ++++ tests/importers/maester-coverage.test.ts | 3 +- .../pingcastle-source-extractor.test.ts | 21 +- tests/persistence/client-information.test.ts | 99 +++++ .../postgres/client-information-read.test.ts | 37 ++ tests/postgres/revision-read-cache.test.ts | 94 ++++ .../context-management-report.test.ts | 247 +++++++++++ tests/reporting/context-report-fixture.ts | 21 + tests/reporting/management-preview.html | 78 ++++ tests/reporting/management-report-preview.ts | 29 ++ .../report-engine-invariance.test.ts | 4 +- .../reporting/report-input-assembler.test.ts | 2 +- tests/reporting/report-narrative.test.ts | 2 +- tests/reporting/report-run.test.ts | 32 +- tests/reporting/reporting-boundary.test.ts | 10 +- tests/server/assessor-http-server.test.ts | 23 +- tests/server/reference-recognition.test.ts | 34 +- 167 files changed, 7279 insertions(+), 504 deletions(-) create mode 100644 docs/CLIENT_INFORMATION.md create mode 100644 docs/analysis/context-pass-gaps-dummy-2026-10-04.md create mode 100644 server/postgres/revision-read-cache.ts create mode 100644 src/assessment-context/context-cluster-severity.ts create mode 100644 src/assessment-context/context-heatmap.ts create mode 100644 src/assessment-context/context-report-publication.ts create mode 100644 src/components/FilterEmptyState/FilterEmptyState.tsx create mode 100644 src/domain/client-information.ts create mode 100644 src/domain/cloud-information.ts create mode 100644 src/evaluation/security-domains.ts create mode 100644 src/features/context-layer/ContextHeatmap.tsx create mode 100644 src/features/context-layer/context-heatmap.css create mode 100644 src/features/dashboard/ClientInformationBrowser.tsx create mode 100644 src/features/dashboard/CloudInformationTile.tsx create mode 100644 src/features/db-structure/DataFlowRelationships.tsx create mode 100644 src/features/project-import/ClientInformationChangeDialog.tsx create mode 100644 src/features/search-filter/business-impact-controls.ts create mode 100644 src/features/search-filter/search-all-findings.ts create mode 100644 src/importers/maester-client-information.ts create mode 100644 src/importers/maester-table-context-source-extractor.ts create mode 100644 src/persistence/client-information-read-port.ts create mode 100644 src/profiles/cis-cat/cis-cat-security-domains.ts create mode 100644 src/profiles/group-policy/group-policy-security-domains.ts create mode 100644 src/profiles/maester/maester-security-domains.ts create mode 100644 src/profiles/pingcastle/pingcastle-security-domains.ts create mode 100644 src/profiles/purple-knight/purple-knight-security-domains.ts create mode 100644 src/profiles/security-domain-mapping.ts create mode 100644 src/reporting/MANAGEMENT_REPORT_TEMPLATE.md create mode 100644 src/reporting/context-management-report.ts create mode 100644 src/reporting/management-business-impact.ts create mode 100644 src/reporting/management-report-texts.ts create mode 100644 src/reporting/report-brand.ts create mode 100644 src/reporting/report-context.ts create mode 100644 src/reporting/report-font.ts create mode 100644 src/styles/filter-empty-state.css create mode 100644 tests/activity-center-preview.html create mode 100644 tests/assessment-context/context-heatmap-fixture.ts create mode 100644 tests/assessment-context/context-heatmap-ui.test.tsx create mode 100644 tests/assessment-context/context-heatmap.test.ts create mode 100644 tests/assessment-context/context-read-reuse.test.ts create mode 100644 tests/assessment-context/context-report-publication.test.ts create mode 100644 tests/assessment-context/heatmap-preview.html create mode 100644 tests/catalog-update-preview.html create mode 100644 tests/client-data-facts-preview.html create mode 100644 tests/client-info-layout-preview.html create mode 100644 tests/client-information-confirmation-preview.html create mode 100644 tests/client-information-fixture.ts create mode 100644 tests/client-information-preview.html create mode 100644 tests/cloud-information-fixture.ts create mode 100644 tests/cloud-information-preview.html create mode 100644 tests/domain/cloud-information.test.ts create mode 100644 tests/engine-outline-preview.html create mode 100644 tests/eol-os-preview.html create mode 100644 tests/evaluation/security-domains.test.ts create mode 100644 tests/features/client-business-impact.test.tsx create mode 100644 tests/features/client-data-fact-results.test.tsx create mode 100644 tests/features/client-info-action-colors.test.tsx create mode 100644 tests/features/client-info-workspace.test.tsx create mode 100644 tests/features/client-information-browser.test.tsx create mode 100644 tests/features/domain-information-results.test.tsx create mode 100644 tests/features/filter-empty-state.test.tsx create mode 100644 tests/filter-empty-state-preview.html create mode 100644 tests/importers/maester-client-information.test.ts create mode 100644 tests/persistence/client-information.test.ts create mode 100644 tests/postgres/client-information-read.test.ts create mode 100644 tests/postgres/revision-read-cache.test.ts create mode 100644 tests/reporting/context-management-report.test.ts create mode 100644 tests/reporting/context-report-fixture.ts create mode 100644 tests/reporting/management-preview.html create mode 100644 tests/reporting/management-report-preview.ts diff --git a/AGENTS.md b/AGENTS.md index 567d093..6214630 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -132,8 +132,9 @@ - Explicit user correction 2026-09-25: changing `Source: Scan` inside Work, including back to `All`, starts the newly selected source with the clean Work default. Clear the previous search, status override, other facets, record-ID/path restriction and `My Workspace` restriction, restore the standard `Deviation` status and normal sort, then apply the selected source. Keep the current Work view mode and current client/timeslot. This applies to an in-Work source change, not to an incoming Client Info or Pathfinder drilldown before the user changes Source. - For a GPO-only Work context, whether reached through the dedicated GPO sheet or `Source: Scan = GPOs`, show only `Result Status`, the available GPO facets `Baseline`, `Comparison`, `Conflict`, `Description source`, `Diagnostics`, `Duplicates`, `Imported`, `Policy section`, `Policy type`, and the permanent final `Source: Scan` control. Do not show CIS, severity, category, MITRE, target, result-message, review, or scanner-specific facets. - Explicit user correction 2026-09-17 supersedes only the category exclusion in the preceding GPO-context rule: `Category` is a permanent GPO Work filter in both supported entry paths, including when its only current value is `GPOs`. Keep it directly after `Result Status`. Severity, CIS, MITRE, target, result-message, review and scanner-specific facets remain excluded from the GPO-only context; `Source: Scan` remains last. -- Explicit user extension 2026-09-17: Work also exposes a distinct `Security Domain` facet immediately after `Category`. It must use the exact shared Client Info security-domain catalog and record assignment, never a second filter-only categorization. It is available in mixed Work and both GPO entry paths; a pure GPO scope maps to `GPO & Baseline`. Domain selection and Client Info drilldown must therefore select the same record identities, while ordinary active Work filters continue to apply. `Source: Scan` remains last. -- Explicit user correction 2026-09-18: `Security Domain` always offers all eight Client Info catalog entries in catalog order, including selectable zero-hit domains in mixed Work and both GPO entry paths. `Unclassified` remains an additional option only when present in the loaded sheet. Counts respect the other active filters; a zero-hit selection must not silently remove itself, the active status, or another still-valid facet selection. A Client Info domain-deviation drilldown uses this visible domain facet rather than a fixed record-ID path, so the user can change domains in Work. Explicit review-only and individual-finding paths retain their separate scope semantics. +- [The `GPO & Baseline` mapping is superseded by the 2026-10-04 Security Domain decision below.] Explicit user extension 2026-09-17: Work also exposes a distinct `Security Domain` facet immediately after `Category`. It must use the exact shared Client Info security-domain catalog and record assignment, never a second filter-only categorization. It is available in mixed Work and both GPO entry paths; a pure GPO scope maps to `GPO & Baseline`. Domain selection and Client Info drilldown must therefore select the same record identities, while ordinary active Work filters continue to apply. `Source: Scan` remains last. +- [Eight entries and `Unclassified` superseded by the 2026-10-04 decision below: six entries, no `Unclassified`; the remaining rules stay valid.] Explicit user correction 2026-09-18: `Security Domain` always offers all eight Client Info catalog entries in catalog order, including selectable zero-hit domains in mixed Work and both GPO entry paths. `Unclassified` remains an additional option only when present in the loaded sheet. Counts respect the other active filters; a zero-hit selection must not silently remove itself, the active status, or another still-valid facet selection. A Client Info domain-deviation drilldown uses this visible domain facet rather than a fixed record-ID path, so the user can change domains in Work. Explicit review-only and individual-finding paths retain their separate scope semantics. +- Explicit user decision 2026-10-04 supersedes the eight-domain catalog, the text-pattern assignment, `GPO & Baseline` and `Unclassified`: there are exactly six Security Domains in this order: `Identity & Authentication`, `Privileged Access & Delegation`, `AD Infrastructure & PKI`, `System Hardening`, `Cloud & Collaboration`, `Monitoring & Recovery`. GPO is a delivery path, not a domain; the GPO view stays available through `Source: Scan = GPOs` and the GPO facets. One classifier in Evaluation (`src/evaluation/security-domains.ts`) serves Work, the Work facet, Client Info and the report projection. It reads only the fields Raw Data shows for a record and never the title, description, remediation prose or the outcome, so pass and fail follow the same rules. Each check gets exactly one domain from the first tier with a hit: (1) native category, exact rule ID or native tag, (2) CIS-CAT `Benchmark section` title path from the same scan report (never the number alone), (3) suite or control ID, (4) GPO technical identity (`Policy type`, `Policy section`, `Registry path` by longest key), (5) the source's own fallback area, else a visible global fallback. Mapping tables are versioned per scanner profile (`src/profiles//-security-domains.ts`); a new scanner brings its table or lands deliberately in tier 5. Every assignment carries tier, method and deciding field; per source, the tier-5 share shows where a table needs entries. Entra checks (EIDSCA, CISA MS.AAD, Maester/Entra, Conditional Access, MFA, Identity Protection) are Identity & Authentication; Exchange, Teams, SharePoint, ORCA, Intune and other tenant configuration are Cloud & Collaboration; Purple Knight `Group Policy Security` is System Hardening. The derived Raw Data fields `Benchmark section` (CIS-CAT) and, for security-template settings without a comparison, `Policy section` and `Registry path` are display projections of retained facts; they need no reimport. The assignment never depends on the Context Engine. - Explicit user decision 2026-09-17 supersedes the previous six-value list: the GPO `Result Status` choices are exactly `All`, `Deviation`, `Passed`, `Not configured`, `No baseline rule`, `Not comparable`, `Conflicts`, and `Duplicates`. Keep the seven non-All choices selectable at zero hits; never merge scanner-only status vocabularies into a GPO-only context. `Deviation`, `Passed`, `Not configured`, `No baseline rule` and `Not comparable` match the authoritative rolled-up policy verdict; `Conflicts` and `Duplicates` match the corresponding diagnostic category flags. One policy setting may therefore belong to both `Deviation` and `Conflicts` or `Duplicates`. - Explicit user decision 2026-09-17: `No baseline rule` is the score-neutral verdict for a retained customer setting whose resolved technical identity has no applicable Microsoft baseline recommendation in the compared baseline set. Every aggregated customer state must reach Work under exactly one verdict: an evaluated verdict where a baseline rule exists, otherwise `No baseline rule`. It is distinct from `Not configured`, which is a recommended baseline setting absent from the analyzed backup set, and it must never be presented as a deviation, remediation obligation or score input. A diagnostic `Conflicts`/`Duplicates` flag marks such a setting without replacing its verdict; unresolved identities stay out of this verdict and take `Not comparable` instead. - Explicit user decision 2026-09-17: `Not comparable` is the score-neutral verdict for a retained customer setting whose delivery mechanism has no comparable technical policy identity. Today this is always a Group Policy Preference. Never resolve a preference into a policy identity and never compare it with a Microsoft baseline recommendation: its action, item-level targeting and apply-once semantics differ from a policy setting, so a baseline verdict over it would be invented. Keep the retained CSE, action and item-level targeting readable in the agreed GPO columns; do not add a GPO Work column or facet for them. Identical or differing preference instructions are not proven duplicates or conflicts, so `Not comparable` carries no diagnostic flag. The observation-level unresolved-identity gap reporting stays in place and must report each observation once with its authoritative resolver reason, never restated a second time by snapshot aggregation. @@ -226,7 +227,7 @@ - Explicit user correction 2026-09-20: the Overall Score block does not repeat a textual score status below the ring and does not repeat the critical-finding count already present in Priority Findings. Assessment Coverage uses at most 70% of the Overall zone and never exceeds the shared compact tile-content width; it must end before the curved domain connectors, which may never cross its label, bar or supporting count. - Explicit user correction 2026-09-20: the Overall Score block also omits the secondary `Unassessed` and `Lowest domain pass rate` fields completely, including their divider container. The unassessed count remains visible as `Gaps` in Data; domain pass rates remain visible on their individual Security Domain nodes. - Explicit user decision 2026-09-18: preserve the thin curved fan connections from Overall Score to every displayed Security Domain during the Ableton-style alignment. They are a binding visual feature, not optional decoration. Derive their endpoints from the rendered ring and domain nodes and remeasure on resize/content changes. Preserve the existing stacked narrow-screen fallback; never keep desktop coordinates on a stacked layout. -- Explicit user correction 2026-09-25: Security Domain rows are compact enough for all eight domains to fit beside Data without clipping. Domain names use the shared H3 role; the score node and right-hand deviation or review count remain, without the former metadata line. The Overall Score ring is a keyboard-accessible entry to Work's clean remediation default: clear prior search, source, domain, severity, record-ID and workspace filters, select `Deviation`, and open the standard Work overview. +- [Eight domains superseded by the six-domain decision of 2026-10-04 in `UI Work filter context`; the compact-row rule applies to all six.] Explicit user correction 2026-09-25: Security Domain rows are compact enough for all eight domains to fit beside Data without clipping. Domain names use the shared H3 role; the score node and right-hand deviation or review count remain, without the former metadata line. The Overall Score ring is a keyboard-accessible entry to Work's clean remediation default: clear prior search, source, domain, severity, record-ID and workspace filters, select `Deviation`, and open the standard Work overview. - Explicit user correction 2026-09-20: Every displayed Security Domain row carries a subtle amount bar in its own shared grid column between the longest rendered domain label and the widest rendered amount. Keep the shared gap on both sides so a bar can never grow into text or behind a number. All bars share one right edge and extend left from that edge. The later color correction supersedes the preceding categorical-color decision: every amount bar uses the exact current semantic color of the Overall Score donut and changes with that score's critical, medium, passed, or unknown tone. Do not use selection, focus, per-domain score, or a separate palette. Bar width is that row's displayed deviation amount divided by the sum of the displayed domain amounts; review-only rows use their already-displayed review amount. Keep the exact right-aligned number visible as the authoritative value, make the bar decorative and hidden from assistive technology, and never change counts, scoring, classification, or drilldown semantics to produce the visualization. - Explicit user correction 2026-09-25: Within each Client Info Security Domain row, center the domain name, amount bar and amount number vertically on the center of that row's score circle. They belong to one grid row; do not let the circle span an unused second row that shifts the other elements upward. - Explicit user correction 2026-09-20 supersedes the transparent Client-Info donut treatment above: the total `Deviations` donut in Client Info's `Data` tile uses the exact Data Hub `datahub-reference-knob` visual variant, including its tile surface, dimensions, spacing, title and hover state. Data Hub and Client Info derive its count and percentage denominator from the same selected mixed-timeslot project sheet, including assessment-bound GPO/policy observations as well as scanner records; never calculate the Data Hub value from scanner run IDs alone. The complete donut remains a keyboard-accessible action to Work with the `Deviation` filter. diff --git a/docs/CLIENT_INFORMATION.md b/docs/CLIENT_INFORMATION.md new file mode 100644 index 0000000..b163afa --- /dev/null +++ b/docs/CLIENT_INFORMATION.md @@ -0,0 +1,105 @@ +# Client Information + +Stand: 2026-10-04. Core capability seit ASSESSOR 2.14.0; Cloud-Uebersicht 2.14.2, read contract 1.1.0. + +Client Information bezeichnet die bisher als scannerbezogene Zusatzinformationen behandelten Beobachtungen und Original-Berichtsabschnitte. Der vorhandene Bestand bleibt in `EnvironmentObservation` und `ScannerReportSection` gespeichert. Es gibt keine neue Datenbank, Tabelle, Migration oder zweite Kopie des Bestands. Bestehende Namen auf Persistenzebene bleiben kompatibel. Die technischen Einzelbefundfelder im Finding Reader unter Additional Information und die Stammdaten Name/Consultant sind andere bestehende Funktionen. + +## Gezielter Abruf + +Aktuelle Korrektur 2.14.2: Die unten historisch dokumentierten Verfuegbarkeitshinweise der Cloud-Kachel sind durch konkrete berichtete Werte bzw. `Not reported` ersetzt. Details und Grenzen stehen unter **Konkrete Cloud-Uebersicht**; die permanente Sichtbarkeit und der Results-Scope bleiben verbindlich. + +Client Info besitzt den Button **Client Information**. Er oeffnet das gemeinsame Datenfenster fuer den ausgewaehlten Mandanten und die ausgewaehlten Scanlaeufe. Erst beim Oeffnen beginnt die Abfrage. Bereich, Freitext, exakter Eigenschaftsschluessel und exakte Objektidentitaet schraenken die Ergebnisse ein. Details zeigen den kompletten Originalwert, Erfassungszustand, Erfassungszeit (falls von der Quelle angegeben), Datei, Scanlauf, Quellposition und Mapping-Metadaten. Vorhandene Quellabschnitte behalten ihre Tabellen und Verweise. Apply und Reset wirken nur auf diese Abfrage, nicht auf Work. + +Nutzerentscheidung 2026-10-04, umgesetzt ab 2.14.1: Die zusaetzliche **Cloud Information**-Kachel in der bestehenden Umgebungskachelreihe ist immer sichtbar, auch ohne Cloud-Nachweise. Ihre vier Felder zeigen die Verfuegbarkeit von Quellnachweisen fuer Sign-in/Conditional Access, Rollen/Berechtigungen, Application consent und externen Zugriff/Sharing. `No retained information` kennzeichnet fehlende Nachweise; `Source evidence not assessed` kennzeichnet vorhandene, nicht als beobachtet erfasste Nachweise. Keiner dieser Texte leitet aus einem Teststatus eine konkrete Konfiguration ab. Results oeffnet denselben Client-Information-Browser, eingeschraenkt auf die vier Cloud-Bereiche; Bereichsauswahl und Reset bleiben innerhalb dieses Cloud-Scope. Ohne gespeicherten Mandanten zeigt Results den bestehenden leeren Quellenhinweis, ohne eine API-Abfrage zu erfinden. Die immer sichtbare Kachel ersetzt die zuvor vorgeschlagene Bedingung, sie nur bei vorhandenen Cloud-Daten einzublenden. + +Der Jetzt-Zustand ist der aktuelle **importierte** Bestand im gewaehlten Scope. Er ist kein Liveinventar. Ohne explizite Zeitraumauswahl bedeutet die API alle nicht ersetzten Importlaeufe des Mandanten, nicht automatisch den zeitlich letzten Timeslot. Unterschiedliche Quellen, widerspruechliche Werte und fehlende Zeitangaben werden nicht zu einem vermeintlich eindeutigen aktuellen Wert verschmolzen. + +HTTP: `GET /api/v1/projects/{projectId}/client-information` + +| Parameter | Bedeutung | +| --- | --- | +| `assessmentId` | Optionaler Timeslot, muss zum Mandanten gehoeren | +| `scanRunIds` | Exakte Scanlaeufe; wiederholter Parameter | +| `includeSuperseded` | Standard `false`; `true` erlaubt auch ersetzte historische Importlaeufe | +| `areas` | Fachliche Bereiche, wiederholbar | +| `properties` | Exakte Eigenschaftsschluessel, wiederholbar | +| `subjectTypes`, `subjectIds` | Exakter Objekttyp / Quellidentitaet, wiederholbar | +| `toolIds`, `states` | Exakte Scanner-ID / Erfassungszustaende, wiederholbar | +| `search` | Teiltext in Quellwert, Label, Eigenschaft, Objekt; bei Abschnitten auch Text/Tabellen | +| `offset`, `limit` | Beobachtungsseite; Standard 0/50, maximal 200 pro Seite | +| `sectionOffset`, `sectionLimit` | Seite primaerer Quellabschnitte; Standard 0/10, maximal 50 | + +Selektoren einer Liste werden mit ODER, verschiedene Listen mit UND verknuepft. Fehlende oder leere Listen beschraenken nicht. Erlaubt sind maximal 100 Eintraege pro Liste und 500 Zeichen pro Eintrag oder Suchtext. Unbekannte Parameter/Bereiche/Zustaende, ungueltige Seitengroessen, doppelte skalare HTTP-Parameter und fremde Mandanten-/Timeslot-/Scan-Zuordnungen werden abgewiesen. + +Bereichsschluessel: `directory`, `inventory`, `resilience`, `password-policy`, `certificates`, `cloud-access`, `cloud-roles`, `cloud-apps`, `cloud-sharing`, `source-metadata`, `other`. Die Zuordnung nutzt deklarierte Importmetadaten oder stabile Eigenschafts-/Objekt-/Abschnittsschluessel; keine Bewertung aus Ueberschriften. + +Beispiel: `/api/v1/projects/synthetic-client/client-information?assessmentId=synthetic-slot&properties=domain.fqdn&limit=50`. + +Die Antwort erhaelt den Snapshot mit `contractVersion`, `purpose: internal-analysis`, `repositoryRevision`, `scopeMode`, getrennten Gesamtsummen und Seitengrenzen. `coverage` beschreibt den vollstaendigen gefilterten Bestand, nicht nur die Seite. Beobachtungen sind nach property/id, Abschnitte nach ordinal/id sortiert. Passende Originalabschnitte bleiben vollstaendig; sie koennen weitere Werte enthalten als die ausgewaehlten Beobachtungen. Exakte, eindeutige Linkziele derselben Datei und desselben Laufs werden auch ueber Seitengrenzen mitgeliefert und in `linkedSectionIds` benannt. Sie zaehlen nicht zur primaeren Seitengroesse. Mehrdeutige Ziele werden nicht geraten. + +Bei mehreren Seiten `repositoryRevision` vergleichen. Aendert sie sich, die Seitenauswahl neu beginnen; die API stellt keinen dauerhaft reservierten Snapshot-Token bereit. Jede einzelne Abfrage prueft die committed Revision vor und nach dem Lesen und wiederholt bei Aenderung hoechstens dreimal, danach folgt ein Konfliktfehler. PostgreSQL liest dafuer die Datenbankrevision, nicht nur den lokalen Prozesscache. Historischer Abruf liefert erhaltene Importbeobachtungen, keine zeitpunktbezogene Rekonstruktion geloeschter Daten oder alter Repository-Revisionen. + +## Read-only-Port fuer Engines + +Der veroeffentlichte Vertrag ist `src/persistence/client-information-read-port.ts` (`ClientInformationReadPort`). UI/API und spaetere Engines nutzen `queryClientInformation` mit demselben `ClientInformationQuery` aus `src/domain/client-information.ts`. + +Die Backend-Komposition bindet den Port an eine **read-only** Repository-/Datenbanksitzung und reicht nur den eingefrorenen Port an die Engine weiter: + +```ts +const port = clientInformationReadPortFrom(readOnlyRepository, + () => readOnlyRepository.getCommittedRevision()); +const information = await port.queryClientInformation({ + projectId, assessmentId, scanRunIds, + areas: ['cloud-access'], limit: 100 +}); +``` + +Das ist Kompositionscode, kein direkter Repository-/Tabellenzugriff aus einer Engine. Engines bekommen keine Mutationsmethoden. Die Rueckgabe ist eine unabhaengige Kopie; Veraenderungen durch einen Leser veraendern den Core nicht. Optionalen Engines ausgeschaltet zu lassen hat keinen Einfluss auf diese Core-Funktion. Bestehende Context-/Report-Ports bleiben kompatibel und werden nicht still auf neue Inputs umgestellt. + +Diese Daten dienen interner Analyse. Der Port ist keine Result-DB- oder Exportfreigabe. Was eine Engine anzeigt oder exportiert, muss weiter die bestehenden Disclosure- und Datenschutzregeln durchlaufen. Cloud-Testnachweise werden nicht als neue Result-DB-Zeilen oder Findings veroeffentlicht. Der neue Port fuegt keine Bewertung, Prioritaet oder Scoreberechnung hinzu. + +## Neuer fachlicher Mehrwert und Grenzen + +| Quelle | Bereits vorhandener Bestand | Neu in dieser Umsetzung | +| --- | --- | --- | +| PingCastle | Directory, Inventar, Resilienz, Kennwortrichtlinien, PKI und erhaltene Originaltabellen | Gezielter einheitlicher Zugriff; keine doppelte AD-Extraktion | +| Purple Knight | Bereits erhaltene Assessment-/Reportinformationen | Derselbe Zugriff auf den vorhandenen Bestand | +| CIS-CAT | Bereits erhaltene Host-/Benchmark-/Berichtsinformationen | Derselbe Zugriff auf den vorhandenen Bestand | +| Maester | Tenant, Reportmetadaten und Testzaehler | Erhaltene Detailnachweise zu Anmeldung/Conditional Access, Rollen, App-Zustimmung und externem Zugriff/Sharing separat auffindbar | +| Andere Profile | Nur tatsaechlich bereits erzeugte Beobachtungen/Abschnitte | Gemeinsamer Zugriff; keine behauptete neue Quellabdeckung | + +Maester HTML (`ResultDetail.TestResult`) sowie Excel/CSV mit exakter bekannter Regel-ID und getrennten Status-/Detailspalten werden erweitert. CSV `Result` plus `ResultDetail` und Excel/CSV `Status` plus `Result` werden unterstuetzt. Die generische Tabellenpipeline verarbeitet weiterhin saemtliche Zeilen. JSON/XML und beliebige unbekannte Exportspalten erhalten durch diesen Schritt keine neue Cloud-Semantik. + +Die Eigenschaften haben die Form `cloud...reported_evidence`; etwa `cloud.authentication.eidsca.af01.reported_evidence`. Der vollstaendige Originaltext bleibt unveraendert; enthaltene Markdown-Tabellen werden als Originalbelege mitgefuehrt. Metadaten nennen `informationKind: reported-test-evidence`, `inventoryComplete: false`, Regel-ID, Quellstatus und Mapping-Version. Dies sind die vom jeweiligen Test berichteten Informationen, **kein vollstaendiges Konfigurationsinventar** und keine Ermittlung der effektiv wirksamen Richtlinie. Ein nativer Teststatus allein erzeugt keinen Konfigurationswert. Nicht ausgefuehrte/fehlerhafte Tests mit Detailtext bleiben `not-assessed`; fehlende Nachweise werden nicht zu `false` oder `0`. + +Exakte freigegebene IDs der Version 1.0.0: + +- Authentication: EIDSCA.AF01-06, AG01-03, AM01-04, AM06-07, AM09-10, AP01, AS04, AT01-02, AV01 (jeweils Praefix EIDSCA.). +- Conditional Access: MT.1001, MT.1003-1021, MT.1035, MT.1036, MT.1038. +- Rollen: MT.1025-1032; CISA.MS.AAD.7.1-7.6. +- Zustimmung: EIDSCA.AP08-10, CP01, CP03-04, CR01-04; CISA.MS.AAD.5.1-5.4. +- Externer Zugriff/Sharing: EIDSCA.AP04, AP07, ST08, ST09; CISA.MS.SHAREPOINT.1.1-1.3, 2.1-2.2, 3.1-3.3. + +Die genaue Liste steht deklarativ im Importmodul; Titel-/Aehnlichkeitssuche erweitert sie nicht. Quellpruefung 2026-10-04: offizieller [Maester-Testkatalog](https://maester.dev/docs/tests/), exemplarisch [EIDSCA.AF01](https://maester.dev/docs/tests/EIDSCA.AF01/), [MT.1005](https://maester.dev/docs/tests/MT.1005/) und [MT.1032](https://maester.dev/docs/tests/MT.1032/). Offline-Zusammenfassung der fachlichen Zuordnung: Der Katalog ordnet diese IDs Tests fuer Authentifizierungsmethoden, Conditional Access, privilegierte Rollen, Zustimmungseinstellungen und Gast-/Sharing-Einstellungen zu. Er garantiert keine vollstaendige Konfiguration im Export; allein die tatsaechlich enthaltenen Originalwerte sind massgeblich. Kein Onlineaufruf ist fuer Import oder Abfrage erforderlich. + +Das Maester-Profil wird auf 1.2.0 und der HTML-Extractor auf 1.2.0 angehoben, der Tabellen-Extractor startet mit 1.0.0. Der historische Verarbeitungsmanifest bleibt erhalten, der Vergleichsschluessel unveraendert. Fruehere Maester-Importe empfehlen regulaeren Reimport. Es erfolgt kein automatischer Kunden-Reimport. Supported Maester reports enthalten weiterhin keinen nutzbaren nativen numerischen Overall Score; der deklarierte Grund und die gemeinsame Fallback-Scoringlogik bleiben unveraendert. + +## Verifikation und verbleibende Grenzen + +### Konkrete Cloud-Uebersicht (2026-10-04, 2.14.2) + +Nutzerkorrektur: Die dauerhaft sichtbare Cloud-Kachel zeigt je Bereich einen belegten Istwert statt Verfuegbarkeitshinweisen. Weitere Fakten und Originale bleiben in Results. Fehlende, nicht ausgefuehrte oder nicht eindeutig erkennbare Angaben erscheinen als `Not reported`. Konflikte werden genannt; mehrere Scan-/Tenant-Scopes werden nicht zusammengerechnet. Passed/Failed, Empfehlungen und generische Scores erzeugen keine Konfiguration. + +Die reine Core-Normalisierung `cloudInformationFacts` projiziert bereits gespeicherte Maester-Testdetails. Lesevertrag **1.1.0** ergaenzt `cloudFacts` fuer den gesamten gefilterten Scope vor Pagination: kanonische Property, Original-/Anzeigewert, `reported-setting`/`reported-subset`, Beobachtungs-ID, Scanlauf, Datei, Regel-ID, Quellposition, Quellzeit und Mapping-Version. Engines lesen weiterhin ausschliesslich den veroeffentlichten read-only Port, ohne interne Module zu importieren. Propertyfilter beziehen sich weiter auf die erhaltenen Beobachtungs-Properties; `cloudFacts` enthaelt nur Fakten dieser gefilterten Beobachtungen. Es gibt keine DB-Schreibvorgaenge oder Schemaaenderung. Bereits erhaltene passende Nachweise profitieren ohne Reimport; fehlen die in 2.14.0 eingefuehrten Nachweise, ist weiter ein regulaerer Reimport erforderlich. Profil-/Extractor-Versionen und nativer Scorevertrag bleiben unveraendert. + +Offline-Mapping 1.0.0, Quellpruefung 2026-10-04: + +- EIDSCA.AF01/AM01/AT01/AV01: State von FIDO2, Authenticator, Temporary Access Pass und Voice. AP08/CR01/AP10: Consent-Policy, Admin-Request-State, App-Registrierung. AP04/AP07/ST08/ST09: Gast-Einladungen, Gastrolle, Gruppenbesitz/-Zugriff. Exakte native erste Saetze liefern den berichteten Wert, einschliesslich ausdruecklich nicht konfigurierter Angaben und des kombinierten Ist-/Sollwertsatzes. Alternativ ist nur eine eindeutige zweispaltige `Setting`/`Actual Value`-Tabelle fuer das exakt benannte Feld erlaubt, nie `Expected Value`. +- CISA.MS.AAD.7.4: exakte Tabelle `Role / Principal Type / Display Name / Status` mit `No Expiration` oder expliziter Satz ueber keine entsprechenden Zuweisungen. MT.1025-1028: Anzahl exakt berichteter Listenzeilen fuer den jeweiligen Principal-Typ. Diese Zahlen beschreiben nur die berichtete Pruefteilmenge. MT.1031: expliziter PIM-Management-Satz; derselbe Helper-Satz unter MT.1032 erzeugt keine PIM-Aussage. +- MT.1007: eindeutige berichtete All-User-MFA-Policy-Links oder expliziter Satz ueber keine entsprechende Policy. Keine Gesamtzahl aktiver/Report-only-Policies. CISA.MS.SHAREPOINT.1.1: nur ausdruecklicher `Current setting`-Wert. + +Offizielle Quellen: [EIDSCA-Generator AF01](https://github.com/maester365/maester/blob/main/powershell/internal/eidsca/Test-MtEidscaAF01.ps1) und entsprechende AP08/AP07/CR01-Generatoren im selben Verzeichnis; [permanente Rollen](https://github.com/maester365/maester/blob/main/powershell/public/cisa/entra/Test-MtCisaPermanentRoleAssignment.ps1), [privilegierte Zuweisungen](https://github.com/maester365/maester/blob/main/powershell/public/maester/entra/Test-MtPrivPermanentDirectoryRole.ps1), [PIM-Alerts](https://github.com/maester365/maester/blob/main/powershell/public/maester/entra/Test-MtPimAlertsExists.ps1), [All-User MFA](https://github.com/maester365/maester/blob/main/powershell/public/maester/entra/Test-MtCaMfaForAllUsers.ps1), [Graph-Markdown](https://github.com/maester365/maester/blob/main/powershell/internal/Get-GraphObjectMarkdown.ps1), [SharePoint Sharing](https://github.com/maester365/maester/blob/main/powershell/public/cisa/spo/Test-MtCisaSpoSharing.ps1). Microsoft beschreibt die bekannten [Consent-Policies](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent) und [Gastrollen](https://learn.microsoft.com/en-us/entra/identity/users/users-restrict-guest-permissions). Das Mapping kuerzt deren Anzeige; exakte Originalwerte bleiben im Vertrag und in Results. Keine Online-Abfrage zur Laufzeit. + +Synthetische Tests decken alle drei erweiterten Formate, Originalwerte/Tabellen/Quellpositionen, unbekannte IDs, fehlende/bare Statusnachweise, Error/Skipped, false/0/Leerwerte, Konflikte, Pagination/Links, Mandanten-/Timeslotgrenzen, explizite Historie, konkurrierende Revisionen, read-only PostgreSQL-SQL, HTTP-Validierung und den tatsaechlichen Client-Info-Button ab. UI-Pruefungen umfassen beide Themes, Apply/Reset, Fehler/Retry, spaete Antworten und ausgeschaltete Engines. Keine Kundenberichte oder Kundendaten wurden fuer diese Implementierungspruefung gelesen. + +Filterung erfolgt derzeit ueber den vorhandenen scoped Snapshot im Backend. Es gibt keine SQL-Filterindizes oder serverseitige Stream-Pagination; sehr grosse Scopes koennen deshalb trotz begrenzter Antwortseiten Arbeitsspeicher beanspruchen. Vollstaendige Cloud-Inventare aus weiteren echten Exportstrukturen erfordern eine gesonderte, quellengestuetzte Profilerweiterung und bleiben offen. diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 5f7b0ac..40a77ec 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -1,5 +1,409 @@ # ASSESSOR – Umsetzungsplan +## Management Report text revision (2026-10-04, Phase 7) + +Explicit user request: the report repeated the same generic sentences in every row and theme. Report Engine only (no Core change): renderer 3.3.0, Business Impact mapping 2.1.0 with fuller theme impact text, new versioned `management-report-texts.ts` 1.0.0 with a German name, Sachverhalt, Einordnung and Ansatzpunkt per Context category, written from the catalog conditions of each category. Theme sheets get a data-derived Kernaussage and per-category blocks; the Summary gets data-derived statements; disclaimers appear once in Lesehinweise und Aussagegrenzen; the 38-row observation table becomes one count/date-range sentence; the redundant Findings row is replaced by the deviation rate; Base Severity stays out of theme sheets as before. Permanent test: every category has complete text, no scanner names, the proof disclaimer occurs once. Reviewed with a local Dummy rendering from the stored Context result. Reporting and Context tests pass (126). Shipped within 2.16.4. + +## Business Impact - independent Work filter and clickable bars (2026-10-04, Phase 7) + +Current user correction: Business Impact must have its own selection, never select or replace the Context Cluster filter. The four Client Info columns open Work with the independent `report:business-impact` facet. The same four filters appear underneath Security Domains in the Work sidebar, with counts, a persistent selection marker and a second click to clear that theme. Existing Context selection is preserved on chart navigation and on sidebar selection; normal filter intersections still apply. Context Cluster remains directly before Source: Scan. Unassigned remains omitted. These are authorized Core Presentation extension points; no evaluation, verdict, source data or stored client data changes. + +The facet reads the same published counting keys and theme assignments as the existing report/chart, not every category attachment. This excludes category attachments without published deviation evidence, which direct Dummy verification found could inflate Control & trust by three. The Context index publishes the existing immutable report projection alongside its unchanged cluster facet. Optional engines hide the extension; disabling Report clears only Business Impact, while disabling Context clears both derived selections. Synthetic exact-interaction acceptance in Dark/Light covers clicking a chart, visible separate dropdown selections, retaining a selected cluster, sidebar toggle, exact matching records, input immutability, optional-engine behavior and an attachment without deviation evidence. Patch 2.16.5 is the backward-compatible release for this refinement; final activation and full verification are recorded below. No phase is marked complete. + +Verification completed: 1,909 tests passed, 15 optional skips; local-only and design checks passed. Both production builds passed, with the existing bundle-size advisory. Product package/lockfile and UI/API version contracts agree on 2.16.5. Existing report-export test expectations were synchronized with the already changed heading and count-column position, without changing report text. Direct Dummy interaction in both themes confirms 221 / 63 / 121 / 69 in chart, sidebar and Work, Context stays All on theme selection, and clearing Resilience preserves an explicitly selected Detection Blind Spots cluster. Screenshots saved outside the repository. The canonical backend was restarted from the verified bundle; health confirms 2.16.5, PostgreSQL and unchanged revision 540. No report was generated or stored during these interactions. + +## One finding, one category - Context and Business Impact (2026-10-04, Phase 13E / Phase 7) + +Explicit user decision supersedes multiple cluster membership in counts and the six overlapping Business Impact themes: a deviation is never counted twice. Context Engine 1.11.0 keeps exactly one primary cluster per finding and GPO setting in `clusterIds` (order: catalogued before recognized before estimated, then most supporting evidence, then base severity, then ID); further clusters stay traceable in `secondaryClusterIds`. Catalog 1.10.0 declares `primaryClusters` for all eight conditions several clusters list (ESC8 -> AD CS Escalation; CIS Printers -> Host Credential & Privilege; delegatable admins -> Kerberos Delegation Abuse; privileged SPN and service-account policy -> Kerberoasting; password-never-expires rules -> Password Guessing); catalog validation rejects a shared condition without one. The heatmap counts each deviation only in its primary cluster, so its cells add up to the unique finding total; cluster condition states are unchanged. Report mapping 2.0.0 has four disjoint themes (Kontrolle & Vertrauen, Informationen & Daten, Betriebsfähigkeit, Widerstandsfähigkeit & Wiederanlauf); every category belongs to exactly one, AD Reconnaissance to Informationen & Daten. Financial and compliance consequences are one prose note, never a counted theme. Dummy with the stored context: heatmap 474 = 474 unique (previously 499); themes 221 / 63 / 121 / 69, plus 3 deviations without a Context category = 477. Permanent tests: heatmap sum equals unique count, missing primary cluster is a catalog error, every category in exactly one theme. Client Info (released Presentation, Core Tests, Platform): the Business Impact row has four equal columns in theme order with English labels; its permanent test checks four bars, labels and no Financial/Compliance bar in both themes. Verified in the browser with Dummy: 4 columns, 221 / 63 / 121 / 69, no overflow. Shipped as patch 2.16.4 (the pending, not yet documented working-tree version); full npm test passed with 1,908 tests and 15 optional skips, client and server production builds passed. Open: the 3 deviations without a Context category are not yet explained. No phase is marked complete. + +## Client Info - possible Business Impact columns (2026-10-04, Phase 7) + +User-authorized Presentation/Platform/Core Tests refinement: six vertical columns below Security Domains show the existing deterministic primary Business Impact distribution, with English labels and counts. The user explicitly excludes the unassigned column here. The total and Security Domains remain authoritative and unchanged; financial/compliance possible consequences remain available in the report even when their primary counts are zero. The Report Engine counting-key helper is shared with the live Client Info workspace; current source selection limits the population without changing Context assignments. This is a pinned read-only presentation extension to published Context and Report results, covered by architecture tests. No external service, model, database write, score change or historical report rewrite is introduced. + +Verified with the actual Dummy Client Info, Scan All: 121 / 0 / 57 / 221 / 0 / 69; six columns only, no unassigned label, Core total still 477. Actual Light rendering inspected and saved outside the repository; synthetic interaction tests cover both themes, optional-engine toggles, request reuse, input immutability and unchanged Core presentation. Full npm test: 1,906 passed, 15 optional skips; local-only and design checks passed. Both production builds passed with the existing bundle-size advisory. Backward-compatible presentation refinement released as patch 2.16.4; package/lockfile and UI/API version contracts synchronized. The identified existing backend was restarted using the canonical PostgreSQL configuration; health confirms 2.16.4 and unchanged revision 540. Report renderer remains 3.2.3. Excluding unassigned means the six displayed values sum to 468 rather than changing the 477 total; this is the explicit requested display decision. No phase is marked complete. + +## Management Report - exclusive finding distribution (2026-10-04, Phase 7) + +Explicit user correction supersedes overlapping counts in the Business Impact overview: its table and chart must distribute all 477 Dummy deviations exactly once. Report-only mapping 1.1.0 assigns a category's direct consequence to its main theme; financial/compliance downstream consequences remain in the possible-impact prose. Conflicting or missing primary assignments stay in Nicht eindeutig zugeordnet, with no arbitrary tie-break. The authoritative summary workspace additionally publishes the exact deviation counting keys; only these keys enter named bars. Stored Core data, verdicts, scores and Context associations remain unchanged. Complete Context evidence stays in theme details and the technical report. No phase is marked complete. + +Authorized direct Dummy verification uses all 477 current Work deviations at revision 540 and the corresponding archived Context publication: Betriebsfähigkeit 121, Finanzielle Auswirkungen 0, Informationen & Daten 57, Kontrolle & Vertrauen 221, Compliance & Haftung 0, Widerstandsfähigkeit & Wiederanlauf 69, Nicht eindeutig zugeordnet 9; sum exactly 477. Synthetic acceptance covers overlapping technical evidence, conflicting primary themes, unmapped findings, out-of-scope keys, order independence, unchanged inputs and equal table/chart totals. The corrected Dummy preview is saved outside the repository in the task's visualization directory. Product/backend activation awaits a current Platform/Core Tests release. + +Release completed after current Platform/Core Tests consent: patch 2.16.3 and renderer 3.2.3, package/lockfile and UI/API version contracts synchronized. Full npm test passed with 1,904 tests and 15 optional skips, including the actual export-button interaction checking that chart counts sum to the full unfiltered population. Architecture, Core/Context/Report invariance, local-only and design checks passed. Both production builds passed. The identified backend was restarted from the verified bundle; health confirms 2.16.3, canonical PostgreSQL and unchanged revision 540. The regular report-run API generated Dummy archive 20261004T160231Z-5340b9c2; independent extraction verifies all seven table counts sum exactly to 477 and agree with the diagram. The final API HTML is saved as the reviewable Dummy artifact outside the repository; no historical report or real customer report was rewritten. No phase is marked complete. + +## Context heatmap - total beside heading (2026-10-04, Phase 7 / 13E) + +Explicit user refinement of the compact header: show `Findings: ` directly beside `Context heatmap`, aligned on the text baseline. The existing Context Engine's unique heatmap-finding count is reused; overlapping category membership never increases the total. The scope subtitle and legend remain absent, and the reduced top spacing remains intact. Only Context Engine component, styles and acceptance tests changed; no Core extension point, count contract, report or stored data is modified. Synthetic scope-change, overlap, empty-state and click-path tests pass in both themes: 20 focused tests, including Core/Context invariance. Design-language check passed. Cosmetic display refinement only, no separate product release or backend restart; concurrent work is preserved. + +## Report wording refinements (2026-10-04, Phase 7) + +Counting correction (Phase 7 / 13E): published Context cluster evidence previously exposed multiple technical result IDs per rolled-up GPO setting; the theme union incorrectly counted those IDs as individual findings. The publication now additionally provides immutable counting keys (scanner occurrence and GPO scope/setting identity). Report themes union those keys, preserving all original result IDs and cross-theme membership. Summary reads the shared Core workspace summary with current GPO evaluations through an additional existing read-only repository method, instead of historical GPO evaluation row counts. No verdict, stored data, Core count or scoring changes. Synthetic tests reproduce 482 evidence rows for 477 unique findings, check overlapping themes and the displayed table/chart/summary, and preserve inputs. Read-only verification of Dummy at revision 540 reproduces the exact screenshot: previous theme counts 193/305/482/294/112/208 become 167/279/399/229/112/190. Context currently maps 474 unique findings; the user's 477 total is not substituted by that mapped count in Summary. Initial backend activation and product release awaited current Platform/Core Tests consent. + +Release verification: subsequent current Platform/Core Tests consent authorizes patch 2.16.2 and renderer 3.2.2, including the preceding wording refinements. Full suite: 1,903 passed, 15 optional skipped. The final workspace-summary integration passed 76 focused Report/Context, export-click, architecture and UI/API version tests. Direct canonical read-only Report execution confirms 477 deviations / 759 assessed at unchanged revision 540, and a new Dummy report was archived successfully; historical archives remain unchanged. No real customer report was generated. The first compiled API export check returned a generic internal error; direct source runtime and archive verification succeeded after the final summary integration. Both final production builds and backend API activation are checked before reporting release complete. No phase is marked complete. + +Final activation verified: both production builds passed after the final summary integration, and the identified backend was restarted with that bundle against the canonical PostgreSQL datastore. The regular report-run API successfully generated and archived Dummy run 20261004T153811Z-0ec39826 at unchanged repository revision 540. Its Summary states 477 deviations / 759 assessed, the six theme counts are 167/279/399/229/112/190, and all user-removed notes are absent. This resolves the earlier compiled-export verification failure. Existing HTML exports remain immutable and must be regenerated to display this release. + +Further screenshot correction: remove the technical sentence about findings without a cluster assignment from Summary. Underlying assignments and appendix data remain unchanged; the synthetic renderer test permanently checks that the sentence is absent. + +The subsequent screenshot correction also removes the count/overlap note beneath the theme bar chart. The diagram and its counts remain unchanged; synthetic rendering verifies the note is absent. + +Another explicit screenshot correction removes the automatic-assignment/specialist-review note from each theme sheet. Conditional impact wording and underlying confidence data are preserved; a permanent synthetic renderer assertion checks the sentence is absent. + +Explicit screenshot corrections: remove the technical overlap/absence-of-evidence note beneath the Business Impact overview, use Summary for both first-sheet summary labels, present Handlungsfelder as a heading with bullet points for evidenced themes, and replace Blätter with Angaben in the introductory prose. Counting, absent-evidence handling and stored data remain unchanged. These small Report Engine presentation refinements do not create a separate product release. Fourteen synthetic renderer tests pass, including both Summary labels, the omitted note, the exact evidenced-theme list and the revised introductory wording. The local synthetic preview is regenerated; production backend activation remains separate from source/preview changes. No phase is marked complete. + +## Management Report - ASSESSOR overview and confidentiality (2026-10-04, Phase 7, 2.16.1) + +Explicit user correction: the report presents ASSESSOR only, with no scanner names or source breakdowns. Management appears only in the repeated page header; every header and footer states Streng vertraulich. Evidence columns show plain finding counts, and the theme overview includes a bar chart of those counts. These presentation requirements supersede earlier visible attribution and confidence breakdowns; retained source data and published Context assignments remain unchanged. Possible Business Impact stays conditional, with no impact grades or amounts. Generation remains entirely offline, without Internet or LLM. + +The first sheet now displays the existing ASSESSOR Overall Score as a full-circle SVG donut beside the summary. It reads the shared report projection directly without changing Core scoring: zero is valid, and absent or invalid scores show unavailable rather than an invented zero. Theme counts deduplicate findings within each theme; overlapping themes are never summed into a total. Historical report archives remain unchanged. Renderer 3.2.1 and the binding report template record the refinement. + +Verification: full npm test passed with 1,901 tests and 15 optional skips; architecture, local-only, engine invariance and design checks passed. Permanent tests cover the actual report-export click, score rendering including zero and missing values, scanner-name suppression, header-only Management wording, confidentiality, count-only tables, chart counts and input immutability. Both production builds passed after the final diagram placement, as did the UI/API version contracts. A synthetic production-renderer preview was inspected locally; no customer report was generated or replaced. Current Platform and Core Tests consent authorized patch release 2.16.0 -> 2.16.1 and backend activation. Health confirms 2.16.1, PostgreSQL and unchanged repository revision 540. Concurrent Client Info changes are preserved; no phase is marked complete. + +## Client Info - remove standalone trigger and simplify heatmap header (2026-10-04, Phase 7) + +Explicit user correction with current Presentation and Core Tests consent: remove only the Client Information button from the Dashboard action row, retaining its data, API and standalone browser. Management Report and Technical Report remain blue. The Context heatmap header now contains only its title: remove the scope subtitle, total-findings display and legend. Existing category colors, per-tile findings, scope selection and exact Work navigation remain intact. Reduce the heatmap top margin from the shared space-8 to space-6 token (32px -> 24px); removing the subtitle also moves the grid upwards without changing the shared outer panel inset. This supersedes the preceding three-button and heatmap-header display agreements only for these visible elements. Twenty-eight synthetic tests passed across both themes, including retained standalone browser behavior, scope changes, Work navigation and Core/Context invariance; design-language checks passed. Cosmetic scope only: no separate product release, backend restart or customer-data operation. Concurrent Report Engine work is preserved. + +## Client Info - blue action buttons (2026-10-04, Phase 7) + +Explicit user colour-role exception with current Presentation, Platform and Core Tests consent: Client Information, Management Report and Technical Report use the same theme-aware blue background, text and border tokens as Engines, including hover and active states. A dedicated class limits the shared CSS rule to these three actions; geometry, engine activation and report behavior remain unchanged. Synthetic Dashboard interactions cover both themes and preserve disabled report buttons when Report Engine is off. Four focused Client Info tests and the design-language check passed. This small cosmetic refinement does not create a separate product release or change the shared product version. No backend restart or customer-data operation; concurrent Management Report work is preserved. + +## Management Report - possible business impact (2026-10-04, Phase 7, renderer 3.2.0) + +Explicit user decision: replace the category-first management reading with the six proposed management themes, labelled exclusively **Möglicher Business Impact**, with no impact grades, money amounts or actual client-impact assessment. Report generation must use no Internet or LLM. This supersedes the earlier content sequence and prohibition on business-consequence wording only for clearly conditional possible consequences; the approved export layout, Context evidence and all Core disclosure/verdict boundaries remain binding. + +Implemented within Report Engine: versioned report-only mapping 1.0.0 from published Context category IDs to Betriebsfähigkeit, Finanzielle Auswirkungen, Informationen & Daten, Kontrolle & Vertrauen, Compliance & Haftung and Widerstandsfähigkeit & Wiederanlauf. Fixed German text describes possible consequences and action guidance only when corresponding evidence exists. Each theme contains observation, action, attribution, confidence and limits. Themes without evidence state no derivable impact; unknown or unmapped categories stay visible in the appendix. Counts deduplicate published result IDs within each theme and never add overlapping themes into a total. Summary and management overview precede six theme sheets; complete Context heatmap and method/technical domain appendix remain. Context Base Severity never becomes an impact rating. Core data, Context assignments and historical report archives are unchanged. The revised binding template records this user decision. + +Verification: 44 focused Report Engine/export-button tests passed, including deterministic generation with network access stubbed to fail, overlap counting, empty/unknown categories, confidence, disclosure and input/storage invariance. Full `npm test`: 1,890 passed, 15 optional tests skipped; architecture, local-only, engine invariance and design checks passed. Client and server TypeScript checks passed. Final report-only table-width refinement passed all six affected renderer/export tests. A synthetic production-renderer preview was generated and inspected locally; it contains no customer data or remote assets. No customer report was generated or replaced. No phase is marked complete. + +Remaining release step: Report renderer version advances 3.1.1 -> 3.2.0. Product-version reassessment recommends minor 2.15.2 -> 2.16.0 for this backward-compatible management-report capability. Package/lockfile, Core UI/API version tests, production builds and activation in the running backend await a current **Platform, Core Tests** release, as required by AGENTS.md; those Core files and the running backend were not changed for this request. Product version remains 2.15.2 until that release step is authorized and verified. Concurrent workspace changes are preserved. + +Release completed (2026-10-04): the user's subsequent current **Platform, Core Tests** release authorized the remaining step. Minor product version 2.16.0 is synchronized in package/lockfile and Core UI/API version contracts. All 54 focused version, HTTP, Report Engine and export-button tests passed; both production builds passed, with the existing bundle-size warning only. The identified local backend was restarted with the verified bundle; health confirms 2.16.0, PostgreSQL and unchanged repository revision 540. The backend bundle contains renderer 3.2.0 and the new Business Impact template. The synthetic local preview was regenerated and returns HTTP 200 with version 2.16.0. New Management Reports use the new template; no customer report or historical archive was generated or replaced during release verification. The entire earlier report verification remains recorded above; no phase is marked complete and concurrent Client Info work is preserved. + +## Data Hub - returning catalog versions update correctly (2026-10-04, Phase 7, 2.15.2) + +Verified correction under current Reference Catalogs, Presentation, Platform and Core Tests consent. Reference recognition previously skipped a historical run with the same deterministic identity even when a newer run used different catalogs, leaving the status permanently pending after A -> B -> A. Reprocessing now checks the latest run's input, recognizer version and catalog hashes for unchanged work. Returning to a historical combination creates a new immutable reference run with a timestamp newer than previous runs and a distinct identity. Historical runs, source findings and reviews remain unchanged; no schema or repository contract changes are required. The existing detail panel explicitly reports an incomplete update when the subsequent status still contains pending scans, retains retry and only reports success when pending work is zero. No visual layout or theme change. + +Verification: permanent synthetic service coverage includes repeated A -> B -> A transitions, a backwards-moving clock, unchanged retries and preserved historical runs/findings/reviews. Permanent click-path tests execute the real service through the API client boundary in both themes; incomplete-update feedback remains covered. All 26 focused tests passed. Full `npm test`: 1,888 passed, 15 optional tests skipped; local-only, architecture, engine invariance and design checks passed. Both production builds and UI/API version contracts passed. Patch 2.15.1 -> 2.15.2 records this backward-compatible correction; existing bundle-size warnings remain. The identified local backend was restarted from 2.15.0 to 2.15.2 against the unchanged canonical PostgreSQL datastore. LSW's regular reference-update API processed its one remaining pending scan after restart: 3 indexed scans, 0 pending. A second call returned 0 processed and 3 unchanged. This verification appended reference metadata only, with no reimport or destructive operation. Concurrent edits are preserved and no phase is marked complete. + +## Data Hub - pending catalog references marked red (2026-10-04, Phase 7, 2.15.1) + +Explicit user correction with current Presentation, Platform and Core Tests release: pending Catalog references recognition is important actionable maintenance and marks the client overview dot and Manage button red, including before client selection. This supersedes the earlier yellow overview treatment of pending references. A dedicated `reference-update` presentation state retains the reference-update explanation instead of classifying pending work as a processing failure. It takes precedence over yellow profile/re-evaluation/review notices; real processing errors retain priority and the tooltip preserves both causes. After successful recognition, the remaining processing state determines the color. The existing Catalog references detail panel and its Update references action stay intact; scanner outcomes, scores, imports and stored data are unchanged. No actual customer reference update was executed. + +Verification: permanent synthetic DataHub interactions cover Dark/Light, initially unindexed and previously indexed outdated references, Manage/update/back, successful clearing, failed update and retry, status retry, catalog/import refresh and error priority. An additional check preserves a concurrent yellow profile-update task after reference recognition. Browser verification with synthetic clients confirms red pending-reference dots/Manage in both themes and green/red/yellow remaining states after simulated completion. All 24 focused tests passed; full `npm test`: 1,885 passed, 15 optional tests skipped. Architecture, engine invariance, local-only and design checks passed. Both production builds and UI/API version contracts passed; scoped diff whitespace checks clean. Patch 2.15.0 -> 2.15.1 records this backward-compatible UI correction. Existing bundle-size and unrelated React-key warnings remain. Concurrent edits and historical decisions are preserved; no phase is marked complete. + +## Security Domains - strukturierte Zuordnung (2026-10-04, Phase 7 / Evaluation; umgesetzt, 2.15.0) + +**Umsetzung und Verifikation (2026-10-04).** Die Umsetzung erfolgte mit Core-Freigabe `Evaluation, Ingest, Reference Catalogs, Presentation, Core Tests, Platform, Governance`; die Fortsetzung mit erneuter Freigabe `Evaluation, Ingest, Presentation, Core Tests, Platform`. Der Hook hatte die erste Freigabe nach einer Hintergrund-Benachrichtigung verworfen. + +Umgesetzter Stand: +- **Klassifizierer:** ein einziger in `src/evaluation/security-domains.ts`, Version 2.0.0. Er liest nur Raw-Data-Felder; ein Werkzeug-Hinweis über die `toolId` des Blatts gilt für Einzelblätter ohne `Source: Scan`. +- **Zuordnungstabellen:** versioniert je Profil, in `src/profiles/{cis-cat,group-policy,maester,pingcastle,purple-knight}/*-security-domains.ts`. Maester akzeptiert auch die Feldnamen `Block`, `ID` und `Tag` (JSON-Listen). +- **Abgeleitete Raw-Data-Felder:** + - `Benchmark section` für CIS-CAT (in `canonical-model-to-dataset.ts`). + - Für Security-Template-Einstellungen ohne Vergleich: `Policy section`. Gelesen wird er aus dem ASSESSOR-eigenen Beobachtungstitel ` · `, nur bei Übereinstimmung aller Quellen. + - Für Security-Template `Registry Values`: `Registry path`, aus der Vergleichsidentität bzw. demselben Titel. Dieser Fall ergab sich erst aus der Dummy-Prüfung und betrifft 49 Einstellungen. + - Offen: ein eigenes Projektionsfeld für Abschnitt und Schlüssel wäre sauberer. Es bräuchte Freigabe für Normalization und Disclosure. +- **Titelerkennung:** Sie ignoriert `Benchmark section` als Titelquelle (`record-builder.ts`). +- **Report:** `domainOf` nutzt den Work-Datensatz, über die Occurrence bzw. Scope und Technical Identity. Ohne Work-Datensatz greift der Klassifizierer auf die strukturierten Ergebnisfelder zurück. +- **Bereinigung:** Die `Unclassified`-Zweige in Client Info, Sidebar und Facet sind entfernt. Die Vereinbarungen in AGENTS.md sind ersetzt. +- **GPO-Tabelle:** Der Eintrag `File Security` ist entfallen, weil die Local-only-Prüfung das Wort `File` sperrt; er ergab dieselbe Domain wie die Auffangstufe. + +Verifiziert: +- **Tests:** 13 neue synthetische Klassifizierer-Tests (eine Stufe je Test, keine `Unclassified`-Domain, Summe gleich Gesamtzahl, Unabhängigkeit vom Ergebnis, Werkzeug-Hinweis, Report-Rückfall). Dazu ein neuer Test, dass Report und Work für Scanner- und GPO-Ergebnisse dieselbe Domain liefern. Zehn bestehende Testdateien wurden an die neue Vereinbarung angepasst, darunter der dauerhaft beibehaltene Klickpfad-Test Client Info → Work-Facet. +- **Testsuite und Prüfungen:** `npm test` mit 1.882 bestandenen und 15 übersprungenen Tests; die Local-only- und Designprüfung bestanden. +- **Builds:** Beide Produktionsbuilds bestanden; die bekannte Warnung zur Bundle-Größe bleibt. +- **Version:** UI und API melden 2.15.0. +- **Browserprüfung mit dem freigegebenen Dummy:** + - Client Info zeigt sechs Domains mit den Abweichungen Identity 112, Privileged 54, Infrastructure 19, Hardening 245, Cloud 7, Monitoring 40. + - Der Klick auf System Hardening setzt das Work-Facet auf `hardening` mit 245 Datensätzen und Status `Deviation`. + - Es gibt kein `Unclassified` und kein neues Facet `Benchmark section`. +- **Dummy-Verteilung nachher** (alle 1.833 Datensätze): Identity 302, Privileged 152, Infrastructure 58, Hardening 1.077, Cloud 149, Monitoring 95. +- **Herkunft der Zuordnungen:** + - CIS: 411 über Stufe 2, davon 87 Pass und 323 Fail auf verschiedene Domains verteilt, statt 187 bei Identity. + - Maester: 317 über die Stufen 1/3, 1 über Stufe 5. + - PingCastle 37 und Purple Knight 119 über Stufe 1. + - GPO: 880 über Stufe 4, 68 über Stufe 5. + +Versionsentscheidung: Minor 2.14.3 → 2.15.0, weil sich die sichtbaren Domain-Kategorien und Domain-Pass-Raten ändern. Package, Lockfile und die Versionsverträge für UI und API sind synchronisiert. Historische Reports behalten ihre Domains. Für diese reine Neuzuordnung gespeicherter Felder ist kein Reimport und kein Verarbeitungsmanifest nötig. Die Phase wird nicht als abgeschlossen markiert. + +Ursprünglicher Planungsstand: Bisher war nur die Planung erfolgt, noch kein Code geändert. Der Nutzer hat im Chat zwei Entscheidungen getroffen: **6 Domains** und **zuerst Plan plus Dummy-Vorher-Zahlen**. Die Umsetzung braucht eine eigene Core-Freigabe, die der Nutzer in seiner Nachricht selbst schreibt. Voraussichtlich nötig: `Evaluation, Ingest, Reference Catalogs, Presentation, Core Tests, Platform`. Die Freigabe-Zeile im eingefügten Vorschlag war nur ein Beispiel und gilt nicht als Freigabe. + +**Problem (am Code bestätigt).** `src/features/dashboard/security-domains.ts` ordnet Prüfungen über Textmuster zu. Dabei gewinnt das erste passende Muster. Identity steht ganz oben und erkennt schon `user`, `account` und `password`. Die Beschreibung zählt mit, deshalb landet „Group Policy“ als Umsetzungsweg bei GPO. Die Maester-Suiten stehen in einer Feature-Datei statt im Profil. Außerdem gibt es zwei Klassifizierer: `securityDomainIdForRecord` für Work und Client Info, `securityDomainIdForUnifiedResult` für die Report-Projektion. Sie lesen unterschiedliche Felder. + +**Entscheidung des Nutzers vom 2026-10-04 (wird mit der Umsetzung verbindlich).** Es gibt sechs Domains: +1. Identity & Authentication +2. Privileged Access & Delegation +3. AD Infrastructure & PKI +4. System Hardening +5. Cloud & Collaboration +6. Monitoring & Recovery + +`GPO & Baseline` entfällt als Domain. Die GPO-Sicht bleibt über `Source: Scan = GPOs` und die GPO-Facets vollständig erhalten. Bei der Umsetzung werden diese Vereinbarungen in AGENTS.md abgelöst. Das braucht die Freigabe `Governance`: +- „Security Domain bietet alle acht Katalogeinträge“ +- „reine GPO-Sicht = `GPO & Baseline`“ +- die acht Knoten der Domain-Spine in Client Info +- die Domain-Pass-Raten im Report + +`Unclassified` entfällt. + +**Zielmodell.** Es gibt einen einzigen Klassifizierer in `src/evaluation/` für Work, Client Info, Work-Facet und Report-Projektion. Die Zuordnung hängt nur an der Prüfung, nie am Ergebnis. Pass und Fail folgen also denselben Regeln. Jede Prüfung bekommt genau eine Domain. Die erste Stufe mit einem Treffer entscheidet: +1. Native Kategorie bzw. exakte Regel-ID nach der Profiltabelle. +2. CIS-CAT-Abschnittstitel aus der Score-Tabelle desselben Laufs (`cis-benchmark-section-titles.ts`), nie die Nummer allein. +3. Maester-Suite bzw. Kontroll-ID. +4. GPO `Policy section` bzw. `Policy type`. +5. Auffangdomain je Quelle. + +Jede Zuordnung trägt die Stufe und das Quellfeld mit. Je Quelle wird ausgewiesen, wie viele Prüfungen in Stufe 5 landen. Es gibt keine Textmuster über Titel oder Beschreibung. Die Tabellen sind versioniert und liegen im Scanner-Profil (analog `securityScoring`). Keine Abhängigkeit von der Context Engine. + +**Dummy-Vorher-Stand (heutiger Klassifizierer, Projektblatt, 1.833 Datensätze, nur Zählwerte).** +- CIS-CAT (411): + - Fail: `identity` 120, `gpo` 94, `hardening` 58, `authentication` 23, `infrastructure` 16, `cloud` 5, `privilege` 5, `recovery` 2 + - Pass: `identity` 67, `gpo` 8, `authentication` 4, `hardening` 2, `infrastructure` 2, `unclassified` 2, `privilege` 1, `recovery` 1 + - Die Härtung ist damit deutlich unterrepräsentiert. +- Maester (~ 330): fast alles `identity` (Fail 61, Pass 68, Skipped 86), nur 8 `cloud`. Das gilt auch für ORCA/Exchange. +- PingCastle (37): `identity` 25, `hardening` 4, `infrastructure` 3, `authentication` 2, `recovery` 2, `gpo` 1. +- Purple Knight AD (~ 120): überwiegend `identity`, 2 `unclassified`. +- GPOs (948): alle `gpo` (Fail 25, Pass 25, `Not configured` 879, `No baseline rule` 19). + +**Verfügbarkeit der Quellfakten (Reimport-Prüfung).** +- CIS-CAT: Alle 411 Ergebnisse lösen einen Abschnittstitel desselben Laufs auf. Vorkommende Hauptabschnitte: Account Policies, Local Policies (Security Options, User Rights Assignment), System Services, Windows Defender Firewall, Advanced Audit Policy, Administrative Templates (Computer/User). +- Purple Knight: `Category` ist gespeichert. Werte: Account Security, AD Infrastructure Security, AD Delegation, Kerberos Security, Group Policy Security, Hybrid (AD). +- Maester: `Category`/Block ist gespeichert, ebenso die Regel-ID-Präfixe. Blocks: Maester/Entra, CISA, ORCA, EIDSCA, CIS, Exposure Management, Maester/Exchange, Teams, Intune, AzureConfig u. a. Präfixe: `CISA.MS.AAD`/`EXO`/`SHAREPOINT`, `EIDSCA.*`, `CIS.M365`, `MT.*`, `ORCA`. +- PingCastle: `Category` (Anomalies, Stale Objects, Privileged Accounts, Trusts) wird im HTML- und im XML-Pfad gespeichert, `Model` nur im XML-Pfad. Die Kategorie allein ist für Anomalies und Stale Objects zu grob. Vorschlag: eine exakte Regel-ID-Tabelle im Profil, mit der Kategorie als Rückfall. Dafür ist kein Reimport nötig, weil die Regel-ID immer gespeichert ist. +- GPO: Das Feld `Policy section` ist für Privilege Rights, System Access, Registry Values und Audit-Subkategorien gesetzt. Korrektur derselben Planung: Von den 825 Einstellungen ohne dieses Feld haben 810 einen gespeicherten Registry-Pfad. Das ist das Ziel `key` der technischen Policy-Identität. Die übrigen 15 sind Security-Template-Einstellungen (Kerberos Policy, Privilege Rights). Ihr Abschnitt steht strukturiert in `identity.target.section`. Stufe 4 liest deshalb die technische Identität: Security-Template- bzw. Audit-Abschnitt oder Registry-Schlüssel. Sie liest nicht die Feldbeschriftung oder den Titel. + - Registry-Schlüssel werden über eine versionierte Präfix-Tabelle zugeordnet, der längste Treffer gewinnt. Der Vergleich ignoriert Groß- und Kleinschreibung, die Hive gehört nicht zum Schlüssel. Vorschlag: + - `...\Windows\EventLog`, `...\PowerShell\ScriptBlockLogging`/`Transcription`, Audit → Monitoring & Recovery + - `Control\Lsa`, `Kerberos`, `Biometrics`, `Windows\System` mit Logon-Werten → Identity & Authentication + - `CredentialsDelegation`, `AdmPwd`/`LAPS`, `Services\NTDS` → Privileged Access & Delegation bzw. AD Infrastructure & PKI + - `SystemCertificates` → AD Infrastructure & PKI + - Defender, Firewall, FVE (BitLocker), Office, Internet Explorer, Edge, Tcpip, Netbt, LanmanServer, MrxSmb10, Print und alle übrigen Schlüssel → System Hardening + - Die ADMX-Kategorie (Pfad in den Administrative Templates) ist im ADMX-Katalog nicht gespeichert. Sie würde eine Parser-Änderung und eine Neuauswertung der Baselines erfordern und ist für die Zuordnung nicht nötig. + - Dummy-Verteilung der Schlüssel ohne Abschnitt: Internet Explorer 231, `Policies\Microsoft\Windows` 197, Office 191, Defender 51, Firewall 27 sowie kleinere Gruppen. `Policies\Microsoft\Windows` wird eine Ebene tiefer unterschieden. + +**Nutzervorgabe 2026-10-04: Zuordnung grundsätzlich über Raw Data.** Raw Data entspricht `record.fields` mit Wert (`RawDataView.tsx`). Prüfung am Dummy-Projektblatt: +- PingCastle (37): `Source Rule ID` und `Category` sind zu 100 % vorhanden. `Model` fehlt (HTML-Import). Die Regel-ID-Tabelle reicht. +- Purple Knight (119): `Category` und `Source Rule ID` sind zu 100 % vorhanden. Reicht. +- Maester (318): `Category`/Block, `Source Rule ID` und `Tags` sind zu 100 % vorhanden. Reicht. +- CIS-CAT (411): **Lücke.** Raw Data enthält nur `Source Rule ID` (Nummer), `Benchmark Item` und `Control Mappings`, keinen Abschnittstitel. Der Titel steht nur in der Section-Score-Tabelle des Berichts und in der Record-Eigenschaft `benchmarkSectionTitles`. Vorschlag: ein zusätzliches Raw-Data-Feld `Benchmark section` mit dem Titelpfad aus derselben Lauftabelle, ohne Reimport. Alternativ dienen die nativen `Control Mappings` (CIS Controls) als gröbere Stufe. +- GPO (948): `Policy type` 100 %, `Registry path` 85 %, `Policy section` 13 %. **Lücke bei 15** Security-Template-Einstellungen mit `No baseline rule`: Sie haben weder Abschnitt noch Pfad. Abschnitt und Schlüssel stehen nur im erzeugten Titel, `Technical Identity` ist ein Hash. Vorschlag: `Policy section` auch für diese Zeilen aus `identity.target.section` füllen, statt den Titel zu zerlegen. +- `Source: Scan` (Stufe 5) ist überall zu 100 % vorhanden. + +Ergebnis: Es ist voraussichtlich kein neues Verarbeitungsmanifest und kein Reimport nötig. Die Zuordnung liest nur bereits gespeicherte Felder. + +**Entscheidungen des Nutzers vom 2026-10-04 (zweite Runde).** +- CIS-CAT bekommt das neue Raw-Data-Feld `Benchmark section`. Es enthält den Titelpfad aus der Section-Score-Tabelle desselben Laufs. Bei fehlendem oder mehrdeutigem Titel bleibt das Feld weg, es wird nichts erraten. Stufe 2 liest dieses Feld. +- Entra-Prüfungen (EIDSCA, CISA.MS.AAD, Maester/Entra, Conditional Access, MFA, Identity Protection) gehören zu `Identity & Authentication`. Exchange, Teams, SharePoint, ORCA, Intune und die übrige Tenant-Konfiguration gehören zu `Cloud & Collaboration`. +- Purple Knight `Group Policy Security` gehört zu `System Hardening`. +- GPO: `Policy section` wird auch für Security-Template-Zeilen ohne Baseline-Regel aus `identity.target.section` gefüllt. Das ist ein Vorschlag aus der Raw-Data-Prüfung, den der Nutzer nicht ausdrücklich beantwortet hat. Er ist als Teil der Umsetzung vorgesehen, sofern der Nutzer nicht widerspricht. + +Damit sind alle inhaltlichen Fragen geklärt. Offen ist nur noch die Core-Freigabe, die der Nutzer selbst schreibt. + +**Risiken.** +- `Not configured`-GPO-Einstellungen verteilen sich künftig auf Domains. Zu prüfen ist, dass sie in Domain-Scores weiterhin score-neutral bleiben. +- Gespeicherte lokale Facet-Auswahlen mit `gpo` oder `unclassified` müssen beim Laden still und sichtbar entfallen. +- Die Report-Pass-Raten ändern sich. Historische Reports behalten ihre Werte. + +**Abnahme.** +- Synthetische Tests je Profil, eine Stufe pro Test. +- Kein `Unclassified`. Die Summe der Domains entspricht der Gesamtzahl. +- Gleiche Domain für dieselbe Prüfung in Work, Client Info und Report. +- Der Regressionstest für den Klickpfad Client-Info-Drilldown → Work-Facet bleibt erhalten. +- Dummy-Nachher-Verteilung gegen den Vorher-Stand oben. +- Versionsentscheidung (Minor, weil sich die sichtbaren Domain-Kategorien ändern) nach Abnahme. + +## Operating systems - EOL overview removed (2026-10-04, Phase 7, 2.14.3) + +Explicit user decision supersedes the earlier EOL overview-field implementation: remove `EOL OS` from the Operating systems tile because the lifecycle-reference effort does not provide enough value. Current Core release covers Presentation, Platform and Core Tests. The tile again uses only its existing source-backed inventory facts, with the existing empty Inventory marker. Original report tables and the Results interaction remain unchanged; no lifecycle catalogue, evaluation, reimport, migration or stored-data change is introduced. Historical decisions and source-verification notes below remain intact. + +Verified: permanent synthetic interaction checks in Dark and Light assert that the tile contains no EOL field even when a retained source value exists, preserves inventory facts, opens original Results evidence and leaves the snapshot unchanged. Empty tiles also omit the EOL placeholder in both themes. Browser inspection with synthetic data confirms Dark/Light and the Results interaction. Full `npm test`: 1,869 passed, 15 optional tests skipped; local-only, design, architecture and engine-invariance checks passed. Both production builds, product-version UI/API contracts and scoped diff whitespace checks passed. Existing client bundle-size and unrelated React-key warnings remain. Patch 2.14.2 -> 2.14.3 records this backward-compatible presentation correction; package/lockfile and version contracts are synchronized. No phase is marked complete; concurrent workspace edits are preserved. + +## EOL OS - source verification correction (2026-10-04, Phase 7 / 13C) + +User reports that EOL OS does not work. Read-only audit found that the preceding implementation and test only accept a hypothetical `EOL OS` column: the synthetic import test explicitly injects that table into the fixture. This proves parser handling, not that a normal PingCastle export supplies the value. Public source verification of `netwrix/pingcastle` master found no EOL aggregate in `ReportHealthCheckSingle.cs`'s computer-account/OS table generator and no direct EOL aggregate in the inspected `HealthcheckData.cs`. Therefore the prior statement that EOL is a supported native overview value is not established and must not be repeated as verified scanner coverage. + +PingCastle does publish individual ObsoleteOS rules, for example `S-OS-2012` in `HeatlcheckRuleStaledObsolete2012.cs`; this rule reads OS occurrence counts and respects a paid-support override. Such findings can be displayed as native scanner evidence, but they are not a complete, overlap-free EOL inventory. Missing rule findings do not establish zero EOL systems. Public sources: https://github.com/netwrix/pingcastle/blob/master/PingCastleCommon/Report/ReportHealthCheckSingle.cs and https://github.com/netwrix/pingcastle/blob/master/PingCastleCommon/Healthcheck/Rules/HeatlcheckRuleStaledObsolete2012.cs . No customer report was inspected and no Core file was changed in this audit. The unsupported overview field remains pending explicit current Core consent for its correction; no phase is marked complete. Historical verification notes below are retained and superseded by this source-coverage correction. + +## Operating systems - EOL OS field (2026-10-04, Phase 7) + +Explicit user request and current Ingest, Normalization, Presentation, Platform and Core Tests release: Client Info's Operating systems tile now reserves its second fact for `EOL OS`, including narrow tiles that display only two facts. It reads an explicit retained PingCastle Computer Information total with an EOL OS/OS EOL/end-of-life OS label. Zero is preserved; missing, malformed, unassessed, contradictory or multiple source totals show the existing unknown marker. No lifecycle judgement is inferred from an OS name or current vendor lifecycle dates, and separate reports/rows are never added. Results now includes the original EOL table alongside the OS inventory, with existing native evidence and source references. The generic importer already retains these fields, so no extractor, mapping, native overall score, persistence, migration or reimport changes were required. + +77 focused synthetic import, OS-tile/Results, dashboard, evidence, architecture and UI/API-version tests passed. Permanent interactions cover both themes, the visible second fact, Original Results and unchanged observations; count checks cover zero, invalid, absent and multiple/conflicting sources. Browser inspection confirmed Dark/Light with synthetic data and the native EOL table under Results. Local-only and design checks passed. Patch 2.14.1 -> 2.14.2 records this backward-compatible presentation refinement; package/lock and UI/API version contracts are synchronized. Existing imported reports without an explicit retained EOL total remain unknown; actual customer report coverage was not validated. No phase is marked complete, concurrent changes preserved. + +Final verification for the EOL OS batch: both production builds passed, including TypeScript checking of the synthetic fixtures; the final 12-test PingCastle import rerun passed and scoped `git diff --check` is clean. Existing client bundle-size warning remains. + +## Cloud Information - konkrete Istwerte (2026-10-04, Phase 13C / 7, 2.14.2) + +Nutzerkorrektur mit aktueller Core-Freigabe Ingest, Normalization, Presentation, Platform und Core Tests: Vier identische Verfuegbarkeitshinweise haben keinen ausreichenden Aussagewert. Die dauerhaft sichtbare Kachel zeigt jetzt je Cloud-Bereich einen konkreten, ausdruecklich berichteten Wert, etwa Authentifizierungsmethode, berichtete permanente Rollenzuweisungen, Admin-Consent-Request-State oder Gastrolle. Dies ersetzt die unten historisch dokumentierten Verfuegbarkeitshinweise aus 2.14.1. Fehlende/unassessed/unklare Werte erscheinen als `Not reported`; Konflikte werden sichtbar. Weitere Fakten und komplette Originalnachweise bleiben ueber Results erreichbar. Kompakte Labels und ein Wert pro Feld erhalten die gemeinsame 14px/16px-Typografie und quadratische Kachel ohne Inhalts-Scrollbar in den geprueften synthetischen Beispielen. + +Die pure Normalisierung liest ausschliesslich bereits erhaltene Maester-Testdetails mit exakter nativer Regel-ID und ausdruecklichen Istwert-Templates/Tabellen. Passed/Failed oder Empfehlungen liefern niemals Konfigurationswerte. Zahlen werden als berichtete Testteilmengen gekennzeichnet, nicht als vollstaendige Tenant-Inventare. Mehrere Scan-/Tenant-Scopes werden nicht zusammengerechnet, widerspruechliche Quellen nicht aufgeloest. Offizielle Generatoren und Microsoft-Semantik wurden geprueft; Offline-Mapping und Grenzen stehen in `docs/CLIENT_INFORMATION.md`. Keine Onlineabfrage zur Laufzeit und keine Kundenberichte als Testdaten. + +Der rueckwaertskompatible Client-Information-Lesevertrag 1.1.0 ergaenzt `cloudFacts` fuer den vollstaendig gefilterten Scope vor Pagination, mit kanonischer Property, Wert, Quellen-/Scan-/Dateibezug, Quellzeit, Scope und Mapping-Version. Future Engines nutzen weiterhin ausschliesslich den veroeffentlichten read-only Port. Keine Persistenz-, Schema-, Finding-, Score-, Result-DB- oder Engine-Aenderung. Bereits erhaltene passende Nachweise profitieren ohne Reimport; fehlen sie, bleibt regulaerer Reimport notwendig. Profil-/Extractor-Versionen bleiben unveraendert, da keine neue Extraktion stattfindet; der bestehende native Maester-Overall-Score-Vertrag bleibt erhalten. + +Verifiziert: 23 fokussierte Leseport-/Cloud-/UI-/Importtests sowie 49 Import-/Scoring-/Architekturtests bestanden. Permanenter synthetischer UI-Test prueft vier konkrete Bereiche, Results mit Originaltabelle, Dark/Light, leere dauerhaft sichtbare Kachel und unveraenderte Quellen; die echte Dashboard-Interaktion bleibt abgedeckt. Drei Importformate liefern dieselbe Source-backed Normalisierung ohne veraenderte Finding-Outcomes. Vollstaendiges `npm test`: 1.867 bestanden, 15 optionale Tests uebersprungen; Local-only-/Design-, Architektur-, Disclosure- und Engine-Invarianztests bestanden. Browserpruefung bestaetigt Dark/Light, konkrete vier Felder, Results-Aufruf und leere Kachel. Beide Produktionsbuilds, UI/API-Versionsvertraege und `git diff --check` bestanden. Zwischenzeitliche Typfehler paralleler OS-Workspace-Aenderungen wurden dort korrigiert; anschliessender Build erfolgreich. Bestehende Bundle-Groessenwarnung bleibt; kein Meilenstein neu als abgeschlossen markiert. + +Versionsentscheidung: Patch 2.14.2 als Verfeinerung der bestehenden Client-Information-Faehigkeit; die bereits parallel vorbereitete gemeinsame Package-/Lockfile-/UI-/API-Version 2.14.2 wurde beibehalten statt pro Teilbearbeitung weiter zu inkrementieren. Gleichzeitige lokale Arbeiten und historische Entscheidungen bleiben erhalten. Grenzen: ausschliesslich deklarierte native Ausgabeformen; keine pauschale Aussage ueber komplette Cloud-Konfiguration, eligible/permanent Gesamtzahlen oder Report-only-Policies. + +## Engine outlines - contrast correction (2026-10-04, Phase 7) + +User rejected the first outline as visually indistinguishable from related ordinary tiles and requested explicit Light-mode consideration. The engine identity frame now uses a 2px shared emphasis outline in the theme's neutral secondary-text color, including the left marker: light grey in Dark, dark grey in Light. It no longer shares the faint related-node border color. Blue remains the keyboard-focus role; yellow remains the selected frame and related-arrow role. The generic `engine: true` contract continues to apply to future Engines. This entry corrects the visual acceptance of the preceding implementation without rewriting its history. + +Permanent Dark/Light interaction checks now guard the stronger identity outline as well as deselection and unchanged arrow behavior. 24 focused diagram, architecture and UI/API-version tests passed; browser inspection confirmed all three unselected Engine frames in both themes and yellow selection with exactly three highlighted Context connections. Both production builds and local-only/design checks passed. Version remains 2.14.1: correction of the same current UI batch, no independent capability release. No data, persistence, relationship or phase-completion change; unrelated workspace changes preserved. + +## Engine outlines - user decision and implementation (2026-10-04, Phase 7) + +Explicit user decision applies to every current and future Engine: its Data Flow tile always has a subtle neutral frame. Only selecting the Engine highlights its frame and related arrows in yellow; hovering or deselecting leaves arrows neutral. The shared engine style now supplies the full neutral border, and node descriptors declare `engine: true` instead of relying on a hardcoded list of three IDs. Future Engine descriptors must use this declaration to inherit the same visual contract. Existing initial Core selection, relationships and data flows remain unchanged. + +Verified under the current Presentation, Platform and Core Tests release: 24 focused diagram-interaction, UI/API-version and architecture tests passed. Permanent synthetic interaction tests cover all three Engines, hover, selection and deselection in both themes. Browser inspection of the production component with synthetic data confirmed neutral unselected frames and yellow selected relationships in Dark/Light. Both production builds, local-only/design checks and `git diff --check` passed. Existing bundle-size warning remains; no persistence or engine behavior changed and no phase is marked complete. Product version retained at the already prepared 2.14.1 as part of the current shared patch batch; concurrent workspace changes preserved. + +## Cloud Information - dauerhaft sichtbare Kachel (2026-10-04, Phase 7, 2.14.1) + +Explizite Nutzerkorrektur: Die Cloud-Information-Kachel wird immer angezeigt, auch wenn keine Cloud-Nachweise importiert sind. Dies ersetzt die vorherige bedingte Einblendung als Vorschlag. Nach aktueller Core-Freigabe fuer Presentation, Platform und Core Tests ist die Kachel in der bestehenden Client-Info-Umgebungskachelreihe umgesetzt: vier Felder fuer Sign-in/Conditional Access, Rollen/Berechtigungen, Application consent und externen Zugriff/Sharing. Vorhandene beobachtete Nachweise erscheinen als `Source evidence available`; sonstige vorhandene Nachweise als `Source evidence not assessed`; fehlende als `No retained information`. Test-Outcomes liefern keine erfundenen Konfigurationswerte. Die Kachel verwendet die bestehenden quadratischen Kachelstile, vier Fakten, 14px Labels/16px Werte und den normalen Results-Button. + +Results oeffnet den bestehenden Client-Information-Browser mit den vier Cloud-Bereichen und dem exakten Mandanten-/Timeslot-/Scan-Scope. Die Bereichsauswahl und Reset behalten den Cloud-Scope bei. Ohne gespeicherten Mandanten bleibt die Kachel sichtbar und Results zeigt den vorhandenen leeren Quellenhinweis. Scopewechsel schliessen das bisherige Datenfenster durch den Scope-Key. Der allgemeine Client-Information-Button bleibt unveraendert erreichbar. Keine neue Extraktion, Tabelle, Persistenz, API-/Engine-Beziehung oder Datenfluss; Work-Filter und Core-Werte bleiben unveraendert. + +Verifikation: vollstaendiges `npm test` erfolgreich, 1.854 Tests bestanden, 15 uebersprungen; Local-only-/Design-, Architektur- und Engine-Invarianzpruefungen bestanden. Permanente synthetische Dashboard-Interaktionen pruefen Dark/Light, immer sichtbare vier Cloud-Felder ohne Daten, beobachtete und nicht bewertete Nachweise, den echten Results-Aufruf mit Originaldetails, Cloud-Filter/Reset, unveraenderte Work-Filter und deaktivierte optionale Engines. Bestehende Kachelanzahlpruefungen wurden um genau die neue Kachel erweitert. Browserpruefung mit synthetischen Daten bestaetigt Dark/Light, leere sichtbare Kachel und leeren Results-Aufruf. Beide Produktionsbuilds und UI/API-Versionsvertraege bestanden; `git diff --check` ohne Fehler. Laufender Backend-Healthcheck: 2.14.1, PostgreSQL. + +Versionsentscheidung: Patch 2.14.0 -> 2.14.1 als UI-Ergaenzung des bestehenden Client-Information-Vertrags; Package/Lockfile und Versionsvertraege synchronisiert. Bestehende Bundle-Groessenwarnung und fremde React-Key-Testwarnung bleiben bestehen. Keine Phase als abgeschlossen markiert, keine Kundenberichte gelesen oder reimportiert, gleichzeitige lokale Arbeiten erhalten. Neue Maester-Nachweise erfordern weiterhin regulaeren Reimport; die Kachel behauptet weder vollstaendige Cloud-Konfiguration noch einen Livezustand. + +## Client Information - Umsetzung 2026-10-04 (Phase 13C / 7; Version 2.14.0) + +Nach expliziter Core-Freigabe fuer Ingest, Normalization, Store, Presentation, Platform und Core Tests ist der unten dokumentierte Auftrag umgesetzt und synthetisch verifiziert. Der neue Core-Lesevertrag `ClientInformationReadPort` 1.0.0 und der HTTP-Zugang `/projects/:id/client-information` erschliessen den vorhandenen Beobachtungs-/Berichtsbestand gezielt nach Mandant, Timeslot, Scanlauf, Bereich, Eigenschaft, Objekt, Scanner und Erfassungszustand. Client Info bietet dafuer den Button **Client Information** mit Filterung, Originalwerten/Tabellen, Provenienz, Details, Pagination und Retry. Optional deaktivierte Engines beeintraechtigen ihn nicht. Bestehende Snapshot-, Finding-, Review- und Exportvertraege bleiben erhalten; technische Additional-Information-Felder im Finding Reader wurden nicht pauschal umbenannt. + +Entscheidungen: vorhandene Core-Tabellen weiterverwenden, keine Migration oder Datenkopie; aktuelle nicht ersetzte Importlaeufe als Standard, historische Laeufe nur explizit; keine Zusammenfuehrung widerspruechlicher Quellwerte und keine Livezustandsbehauptung. Jede Abfrage wird gegen die committed Repository-Revision geprueft. Exakte Quellverweise behalten ihre eindeutigen Ziele auch ueber Abschnittsseiten hinweg. Engines erhalten ausschliesslich den veroeffentlichten Leseport, gebunden an read-only Repository-/DB-Zugriff; bisherige Engines wurden nicht auf andere Inputs umgestellt. Die Data-Flow-Beschreibung des vorhandenen kanonischen Bestands nennt Client Information und den Leseport; es gibt keine neuen Tabellen oder Engine-Schreibbeziehungen. + +Mehrwert: Maester 1.2.0 ergaenzt bekannte Cloud-Testdetailwerte aus HTML und Excel/CSV zu Authentifizierung/Conditional Access, Rollen, App-Zustimmung und externem Zugriff/Sharing. Originaltext und enthaltene Tabellen bleiben erhalten. Alle neuen Werte sind als `reported-test-evidence`, `inventoryComplete: false` mit exakter nativer Regel-ID und Quellstatus gekennzeichnet. Ein Teststatus wird niemals als Konfiguration interpretiert. Der native Overall-Score-Vertrag wurde geprueft und bleibt bei der expliziten Nichtverfuegbarkeit eines numerischen Gesamtscores; die gemeinsame Scoringlogik bleibt unveraendert. Historische Verarbeitungsmanifeste bleiben erhalten, fruehere Maester-Importe empfehlen Reimport. Es wurde kein Kunden-Reimport ausgefuehrt. + +Verifikation: vollstaendiges `npm test` erfolgreich, **1.853 Tests bestanden, 15 uebersprungen** (darunter die nicht aktivierten echten PostgreSQL-Integrationstests). Neue synthetische Tests pruefen drei Importformate, unveraenderte Finding-Outcomes und Originalwerte, fehlende/unbekannte Nachweise, read-only PostgreSQL-Abfragen, Scopes/Historie, Revisionen, Pagination/Links, HTTP-Validierung und die echte Dashboard-Interaktion in beiden Themes inklusive Apply/Reset/Retry und veralteter Antworten. Architektur-, Engine-Invarianz-, Scoring-, Disclosure-, Local-only- und Designpruefungen bestanden. Veraltete Client-Info-Tests wurden an die bereits verbindliche Directory-Integration vom 2026-10-02 angepasst, mit vollstaendigen Domain-Information-Result-Pruefungen. Synthetische Browserpruefung bestaetigt Dark/Light, vorhandenes Datenfenster und Eigenschaftsfilter. Beide Produktionsbuilds und UI/API-Versionsvertraege bestanden, `git diff --check` ohne Fehler. Der laufende Backend-Healthcheck meldet 2.14.0 und PostgreSQL. Keine Kundendaten wurden fuer Implementierungstests gelesen. + +Versionsentscheidung: rueckwaertskompatible neue Abfragefaehigkeit, daher Minor 2.13.5 -> **2.14.0**; Package, Lockfile und Versionsvertraege synchronisiert. Historische Report-/Versionsangaben bleiben unveraendert. Bestehende Warnungen fuer Client-Bundlegroesse und einen React-Key in einem fremden RichTextValue-Test bleiben bestehen. Keine Phase als abgeschlossen markiert; gleichzeitige lokale Arbeiten erhalten. + +Offene Grenzen/Risiken: Vollstaendige Cloud-Konfigurationsinventare sind nicht durch Testnachweise ersetzt; unbekannte JSON/XML-/Exportstrukturen benoetigen weitere quellengestuetzte Profilerweiterungen. Die konkrete Abdeckung echter Kundenberichte wurde nicht verifiziert. Neue Extraktionen werden erst bei einem gesonderten regulaeren Reimport sichtbar. Der Port filtert derzeit den vorhandenen scoped Snapshot im Backend, nicht per neuen SQL-Indizes; grosse Scopes koennen entsprechend Arbeitsspeicher benoetigen. Mehrseitige Leser muessen Revisionen vergleichen. Interne Daten bleiben den bestehenden UI-/Export-Disclosure-Regeln unterworfen. Details, genaue ID-/Formatabdeckung und Engine-Nutzung: [CLIENT_INFORMATION.md](CLIENT_INFORMATION.md). + +## Client Information - urspruenglicher Nutzerauftrag 2026-10-04 (Planungsstand vor Freigabe) + +Der Nutzer benennt den bisher als Zusatzinformationen bezeichneten scannerbezogenen Mandanteninformationsbestand **Client Information**. Der Bestand soll alle bisher gewonnenen Informationen erhalten und gezielt sowohl fuer die heutige ASSESSOR-Anwendung als auch fuer spaetere Engines abrufbar sein. Dazu kommen die besprochenen Cloud-Konfigurationsinformationen, soweit die Originalberichte sie tatsaechlich enthalten. Dies ist eine dokumentierte Anforderung, noch keine implementierte oder verifizierte Core-Aenderung. + +Umsetzungsumfang und Abnahmeentwurf: + +- Den vorhandenen Bestand aus `EnvironmentObservation` und `ScannerReportSection` als Grundlage verwenden: Originalwerte, komplette Abschnitte/Tabellen, Erfassungszustaende und Datei-/Scan-Provenienz bleiben erhalten. Die einheitliche fachliche Bezeichnung lautet Client Information. +- Einen versionierten, gezielt abfragbaren Lesevertrag bereitstellen: Mandant, Assessment/Timeslot, Scanlauf, Informationsbereich, Eigenschaft und betroffenes Objekt. Client Info und Engines lesen denselben abgegrenzten Datenstand. Stabile fachliche Schluessel erlauben den Abruf ohne UI-Textsuche oder erneutes Parsen durch eine Engine. +- Der Jetzt-Zustand bezeichnet den aktuellen importierten Stand im gewaehlten Scope, mit Scan-/Erfassungszeit und Repository-Revision; er behauptet keinen Livezustand der Kundenumgebung. Historische Laeufe bleiben adressierbar. Widerspruechliche oder unterschiedlich alte Quellen bleiben zugeordnet sichtbar und werden nicht still zusammengefuehrt. +- Zusaetzliche Cloud-Bereiche anhand der tatsaechlichen Quellfelder erschliessen: Anmeldemethoden und Conditional-Access-Konfiguration samt berichteten Ausnahmen; Cloud-Rollen und berichtete aktive/berechtigte Zuweisungen; App-Consent-, Gastzugriffs- und externe Freigabeeinstellungen. Ein vorhandener Test oder Findingtitel beweist weder das Vorliegen der Konfigurationswerte noch ein vollstaendiges Inventar. Fehlende Angaben bleiben ausdruecklich fehlend. +- Engines konsumieren Client Information ausschliesslich ueber deklarierte Read-only-Ports und Read-only-Datenbankzugriff; der Bestand gehoert zum Core. Bestehende Privacy-/Disclosure-Regeln gelten auch fuer den neuen Lesevertrag. Keine engineeigene Kopie oder Schreibmoeglichkeit in Core-Daten. +- Namensaenderungen nach fachlicher Bedeutung vornehmen: Die bisherige Finding-Reader-Rubrik `Additional Information` enthaelt auch technische Einzelbefundfelder und ist nicht automatisch identisch mit dem Mandanteninformationsbestand. Keine pauschale Text-Ersetzung als Ersatz fuer den gemeinsamen Informationsvertrag. +- Synthetisch pruefen: gezielter Abruf bestehender und neuer Informationen, Scope-/Mandantentrennung, historische Datenstaende, fehlende/widerspruechliche Werte, Herkunftstreue, identische UI-/Engine-Grundmenge sowie unveraenderte Findings/Verdicts/Scores und Engine-Invarianz. Betroffene UI-Interaktionen dauerhaft absichern. + +Bestandspruefung: `getEnvironmentSnapshot` existiert bereits im Repository und in der API; Context- und Report-Read-Ports nutzen ihn. Der jetzige Snapshot-Query grenzt nach Mandant, Assessment und Scanlaeufen ein; fachlich gezielte Abfragen sind darauf aufbauend zu gestalten. Die konkrete Cloud-Feldabdeckung der Kundenberichte ist noch nicht verifiziert. Bestehende Importe erhalten neue extrahierte Werte nur durch einen gesonderten regulaeren Reimport. + +Core-Umsetzung wartet auf die Freigabe der betroffenen Bereiche Ingest, Normalization, Store, Presentation, Platform und Core Tests. Eine Schemaaenderung oder ein Reimport wurde weder vorgenommen noch durch diese Dokumentation freigegeben; falls erforderlich, gelten die bestehenden Backup-/Migrationsregeln. Dokumentationsaenderung ohne Produktversionsanhebung, ohne Laufzeitaenderung und ohne Phasenabschluss. + +2026-10-02 - Phase 7 / approved Management Export assignment wording (Report renderer 3.1.1, verified): User explicitly selects Nach Katalog, Nach Befundmerkmalen and Thematisch eingeordnet. New Management Reports use these labels consistently in the reading key, category assignment tables and count summaries. Descriptions use plain technical/content-clue wording; thematic assignments explicitly remain pending professional confirmation. Report-authored Geschätzt/geschätzt and deterministic/anchor wording are removed from Management HTML. Internal catalogued/recognized/estimated values, counts, technical export, source data and historical archives are unchanged. The permanent Report Engine template records this user decision. + +18 focused management, report-run/narrative, boundary and invariance tests passed, including a new synthetic export test with nonzero counts in all three tiers, exact labels throughout, preserved qualification and unchanged input. Synthetic management preview regenerated through the production renderer; server production build and design/local-only checks passed. Management renderer patch 3.1.0 -> 3.1.1 records the editorial change in new archive metadata. Product version reassessed and retained at 2.13.5: this is a terminology-only Report Engine edit with no Core/product capability, schema, data or layout change. No Core source/test/version files were edited. Existing local backend restarted to activate the renderer, retaining canonical PostgreSQL; frontend API health is ok. No phase completion or new dataflow relationship, no full-suite rerun. + + +2026-10-02 - Phase 7 / Activity reload control (2.13.5 patch, verified): Current Presentation/Core Tests/Platform release adds the requested neutral circular-arrow button immediately after History when loading ends incomplete or cancelled and no task remains active. Accessible name/title Reload application; mouse or native Enter reloads the current application page through window.location.reload. The same action remains available beside a manually minimized incomplete Activity indicator. It does not mark failures as recovered, clear history or alter readiness; running/queued work and recovered Ready suppress it. Existing green Ready dot, manual minimization, five-second automatic minimization and historical failure behavior remain. Shared button geometry, token-sized SVG and both themes retained. + +Permanent synthetic React interactions in Dark/Light verify presence after failure, click callback from expanded/minimized states, unchanged activity records, absence while running and after same-operation recovery despite historical failure. 24 Activity/UI/API version tests passed; both production builds, design/local-only checks and diff check passed. Browser synthetic preview verified native Enter and mouse reload navigation, both themes and visible compact geometry; screenshot saved. Patch 2.13.4 -> 2.13.5 with package/lock/version contracts synchronized. No persistence, API/dataflow or engine relationship change, backend restart or full-suite rerun; existing bundle-size warning remains. + + +2026-10-02 - Phase 7 / approved Management Report template retained: user explicitly approved the implemented report and requested a permanent template inside Report Engine. Added src/reporting/MANAGEMENT_REPORT_TEMPLATE.md with binding visual/content/data contracts and linked it from REPORT_STRATEGY.md. Existing renderer remains the executable template. Documentation-only; no product version change or runtime changes. Dataflow inspection found Context -> Report still marked Planned in EngineDataFlow.tsx despite the implemented publication bridge; diagram update remains a separate Presentation/Core Tests/Platform released request. No phase completion claimed. + +2026-10-02 - Phase 7 / Client Info layout rollback (2.13.4 patch, verified): Explicit user correction "Setz alles wieder zurueck! Wieder wie vorher!" followed by current Presentation/Core Tests/Platform release supersedes the compact-workspace selection documented below. Only the last Client Info layout batch is reverted: Overall Score and Security Domains again sit side by side with the established connectors, Data and Domain Information use their previous arrangement, the compact Context heatmap is full-width underneath, and square environment tiles return to their horizontally scrollable row. Original coverage/ring dimensions, responsive breakpoints and environment-tile first-read disclosure are restored. Accepted Risks and all earlier functionality, including six clickable Data facts, complete Domain Information Results, heatmap totals/cluster drilldowns, engine preferences and report actions, remain intact. No unrelated concurrent changes were reverted; frontend/backend continue running against the canonical datastore. + +The permanent synthetic full-Dashboard regression now checks this restored layout in both themes, preserved source values/Core counts, complete native Domain Information Results and the exact Context-to-Work click. 63 focused layout, popup, disclosure, risk, report, engine-boundary/invariance and UI/API version tests passed; both production builds and design/local-only checks passed. Synthetic browser inspection confirms the restored geometry, both themes and 14px labels/16px values; screenshot saved. Product patch 2.13.3 -> 2.13.4 records the verified rollback without rewriting historical release/report versions. Existing client bundle-size warning remains. No schema/dataflow change, backend restart, full-suite rerun or phase completion claim. + + +2026-10-02 - Phase 7 / management report reference layout (2.13.2 patch, verified): User-provided Huntsman page explicitly becomes the exported Management Report's layout/style reference: white report sheets, ASSESSOR logo in header/footer, orange headings, dark table headers, alternating light rows, two-column explanatory/evidence blocks and factual bar charts. This export-only user decision supersedes earlier app-style guidance for this report's palette/layout; Core application views and tokens stay unchanged. Existing ASSESSOR logo and Inter are embedded offline. Overview and heatmap precede one sheet per populated Context category; each shows catalog description, separate scanner/GPO counts, category Base Severity, confidence and source attribution, followed by scope/method appendix. Footers number logical report sheets, not physical PDF pages. No invented time series, score, impacted endpoint count, ownership or business consequence. Coverage chart uses published assessed counts; category bars use published scanner/GPO counts. 51 distinct focused report/invariance/export/UI/API version tests passed (module-list sort corrected and rechecked), both production builds and design/local-only checks passed. Browser synthetic preview has loaded logo, two-column/table/chart layout and no horizontal overflow at the normal viewport. No PDF pagination or real customer regeneration acceptance claimed. Provided downloaded HTML was inspected read-only and remains unchanged. Product/lock/version contracts synchronized; no backend restart, no schema or engine relationship change; phase remains in progress. + +2026-10-02 - Phase 7 / Client Info, selected compact workspace (2.13.3 patch, implemented and verified): User explicitly selected the attached third proposal, Kompakter Arbeitsbereich, then released Presentation, Core Tests and Platform. The existing Client Info now groups Overall Score and coverage, all Security Domains and the Context heatmap in the left analysis column; Data with its six clickable facts, Domain Information and all five Environment groups occupy the right column. Accepted Risks remains the existing full-width list/detail register below. This chosen layout supersedes the earlier six-square-tile environment row and side-by-side posture arrangement: environment groups are stacked flat reading surfaces, with existing first-read fields and complete Results retained. Decorative ring-to-domain connectors are hidden in the stacked arrangement; scores, source-balanced computation, population semantics, source selection, report actions, engine choices, native evidence, risk records and Work drilldowns are unchanged. No API request, parsing, persistence or engine relationship was added. The heatmap retains its compact 186px tiles with up to four columns, all 22 categories and unique Finding count. Narrow containers stack the columns; the shared root inset remains 20px and fields retain 14px labels/16px values in both themes. + +Verification: permanent synthetic full-Dashboard interaction tests in Dark/Light check left/right placement, all eight domains plus applicable Unclassified, six Data facts, five environment groups, full-width risk register, all 22 heatmap categories, unchanged Core values when enabling Context, opening all eleven Domain Information fields and original evidence, closing Results and selecting the exact Work cluster, with unchanged input data. Existing six-fact popup, progressive disclosure, posture, risk, report-control, Context boundary and Report invariance checks remain. 63 focused tests passed, including UI/API product-version contracts; both production builds, local-only/design checks and diff check passed. Browser inspection used a static rendering of the actual synthetic Dashboard output to verify both themes, responsive stacking, typography and no horizontal overflow; event behavior was verified in the actual React interaction tests, not the static preview. Screenshot saved. Patch 2.13.2 -> 2.13.3, package/lock and both version contracts synchronized. Existing client bundle-size warning remains; no full-suite rerun, real-client data change, backend restart or phase completion claim. Concurrent changes preserved. + + +2026-10-02 - Phase 13B / Dataflow presentation clarification (2.13.1 patch, verified): Result DB materialization tile now states Data protection & disclosure and explicitly describes removal or masking of sensitive identity data, validation before storage and the shared controlled result set for Result DB UI and exports. This documents existing disclosure behavior only; internal-analysis remains distinct. No processing, schema, engine relationship, arrow style or selection change. Current Presentation/Core Tests/Platform consent; 17 focused Dataflow/UI/API version tests, design check and both production builds passed. Package/lock and version contracts synchronized. Existing client bundle size warning remains; no backend restart, no new phase completion, no screenshot acceptance claimed. + +## Context-based Management Report (Phase 7 / published Phase 13E results) + +2026-10-02 - Version 2.13.0, implemented and focused verification complete: current Presentation/Core Tests/Platform release connects the management export to Context Engine's published, frozen result contract. Report Engine does not import Context internals; the already pinned backend extension composes published Context results through the canonical read-only port. The same project, assessment IDs, scan-run IDs and repository revision are checked before rendering/archive; unstable reads retry, mismatched scope or absent producer blocks management generation. Core remains unchanged with engines off. New management generation is blocked in Client Info while Context is off and late UI output is suppressed; technical-only generation still works without computing Context, with no fallback management file. Historical archive renderers and projections are preserved. + +Delivered: Management Summary, all 22 category tiles sorted by actual Context color, unique Finding total, populated problem fields with scanner/GPO counts and catalogued/recognized/estimated confidence, generic review guidance referencing original recommendations in the Technical Report, documented risk acceptance and selected work states, conditional existing delta evidence, environment/source/time coverage and Domain appendix. No invented business loss, probability, ownership, deadlines, effort, threat scenarios or new priority. General category descriptions are labelled as catalog explanation, not a proven shared attack path. Category Base Severity and technical verdicts stay distinct; neutral is not secure, overlapping clusters are not summed, GPO backups are not effective host observations. Missing comparison evidence is stated explicitly. Result IDs stay out of Management HTML and are preserved with matching Context counts in Report B and the confidential frozen archive JSON. Standalone HTML uses embedded existing Inter, no script or external resources. The export reads Context only on demand, reusing a matching cached result; no additional customer-load work. + +Verified: 67 focused Report/Context/publication, actual Client Info export interaction, HTTP generation, archive/replay, on/off invariance, architecture and UI/API version tests passed; both final production builds, design/local-only checks and diff check passed. Synthetic browser preview uses the production Context publisher and renderer with 22 categories, no customer data. Existing client bundle-size warning remains. Capability release 2.13.0 supersedes concurrently established 2.12.6; package/lock and UI/API version contracts synchronized. Report Engine 0.3.0, projection 1.3.0 and management renderer 3.0.0 version the new artifact. No persistence migration, canonical datastore change, backend restart or phase completion claim. Current running backend must load the new build before the live export changes. New implemented data-flow relationship: published Context results -> Report Engine; remind the user to request the Engine diagram update separately. A real customer export and rescan-backed delta were not accessed in this side conversation. + +## Context-Heatmap in Client Info (Phase 13E / 7) + +2026-10-02 - Phase 7 / Data-Kennzahlen als Results-Einstieg (2.12.6, umgesetzt und fokussiert verifiziert): Aktuelle Presentation/Core-Tests/Platform-Freigabe macht AD Domains, Entra Tenant, User Accounts, Computer Objects, Domain Controllers und GPOs zu nativen klickbaren Kennzahl-Kacheln. Die bestehende EnvironmentResultsButton-/FloatingDataWindow-Interaktion zeigt den unveraenderten Uebersichtswert, passende retained Originalbeobachtungen mit Quelle/Scan und die jeweiligen originalen Reportabschnitte samt bestehenden Such-/Detail-/Paging-Funktionen. Eigene Evidenzgruppen begrenzen User-/Computer-/DC-/Domain-/Entra-/GPO-Inhalte. GPO Results zeigt die bereits geladenen verarbeiteten Backup-Dateien, Anzahl, Status und Zeit; der bestehende projektweite Zaehler und seine Datenpopulation bleiben erhalten und werden als All processed client backups benannt. Bestehende GPO-GUID- bzw. Maester-Praesenz-Fallbacks werden als solche erklaert, ohne Detailobjekte zu erfinden. Unbekannte Werte bleiben ein Gedankenstrich; fehlende Quellabschnitte bleiben explizit fehlend. Kachelgeometrie, beide Theme-Tokens und 14px-Daten/16px-Werte bleiben erhalten, Hover und blauer Tastaturfokus zeigen die neue Interaktion. Escape/Close stellt Fokus auf die ausloesende Kennzahl zurueck; Oeffnen aendert weder Work-Filter noch Daten und erzeugt keine neuen Requests. Permanente synthetische Tests klicken alle sechs echten Dashboard-Kennzahlen in beiden Themes, pruefen exakte Werte/passende Evidenz, getrennte User-/Computer-Inhalte, Backup-Herkunft, Escape/Fokus, Dateninvarianz und Requestzahl; separater Test prueft unbekannten Wert ohne erfundene Null/Zeilen. 53 unterschiedliche fokussierte Kennzahl-/Domain-/Evidenz-/Layout-/UI-API-Versionspruefungen bestanden (52 im ersten Lauf, abschliessend drei Kennzahltests einschliesslich neuer Missing-data-Abnahme); Design-/Local-only-Pruefung, diff check und beide Produktionsbuilds bestanden. Synthetische Browserabnahme mit Enter, Escape, Maus und Dark/Light; Screenshot gespeichert. Patch 2.12.5 -> 2.12.6, Package/Lockfile und Versionsvertraege synchron. Kein Vollsuite-Neulauf, Backend-Neustart, Persistence-/API-Vertrags- oder Engine-/Dataflow-Beziehungswechsel; bestehende Bundle-Warnung und concurrent changes bleiben, keine Phasenabschlussbehauptung. + +2026-10-02 - Phase 13E / 7, eindeutige Heatmap-Gesamtzahl (2.12.5, umgesetzt und fokussiert verifiziert): Auf aktuelle Presentation/Core-Tests/Platform-Freigabe steht Findings: N unmittelbar links neben der Severity-Legende. Die Anzahl umfasst die eindeutigen Scanner-Abweichungen und GPO-Settings, die in der Heatmap des ausgewaehlten Timeslots vertreten sind. Die vorhandene Evidenzprojektion wird fuer Kacheln und Gesamtzahl gemeinsam verwendet; mehrere Bedingungen, Evidenzen oder Clusterzuordnungen desselben Findings erhoehen die Gesamtzahl nicht. Scanner und GPO behalten getrennte Identitaeten, GPOs ihren Scope; bestandene Ergebnisse bleiben ausgeschlossen und der bestehende Recognition-Reassignment-Ausschluss bleibt erhalten. Neue Context-Daten aktualisieren den memoisierten Zaehler, leere Daten zeigen 0; Lade-/Fehlerzustaende zeigen keine erfundene Zahl. Keine Quellen-, Verdict-, Katalog-, Persistenz- oder Engine-/Dataflow-Beziehungsaenderung. Permanente synthetische Komponenteninteraktion in beiden Themes prueft Position vor der Legende, 29 GPOs plus zwei Scanner, ueberlappende Cluster/Evidenz, Pass-Ausschluss, unveraenderte Daten, Cluster-Klick und Wechsel zu leerem Scope; Dashboard-Klick prueft zusaetzlich die Gesamtzahl. 30 fokussierte Heatmap-, UI/API-Versions- und Core-Grenztests, Design-/Local-only-Pruefung und beide Produktionsbuilds bestanden. Synthetische Browserabnahme in Dark/Light und schmalem Container ohne Ueberlauf; Screenshot gespeichert. Patch 2.12.4 -> 2.12.5 fuer rueckwaertskompatible UI-Ergaenzung, Package/Lockfile und Versionsvertraege synchron. Kein Vollsuite-Neulauf oder Backend-Neustart; bestehende Bundle-Groessenwarnung bleibt, keine Phasenabschlussbehauptung und concurrent changes erhalten. + +2026-10-02 - Phase 7 / Directory in Domain Information integriert (2.12.4, umgesetzt und fokussiert verifiziert): Nach aktueller Presentation/Core-Tests/Platform-Freigabe entfaellt die separate Directory-Kachel. NetBIOS name, Schema version, Domain created und Entra ID erscheinen zusammen mit den bestehenden Domain-Angaben in Domain Information. Der gemeinsame Results-Button verwendet die bestehende FloatingDataWindow-Interaktion und zeigt alle elf Domain-Information-Felder, einschliesslich der oben bereits dargestellten Domains-/DC-Zaehler und unbekannter Angaben, sowie die bisherigen Directory-Quelltabellen und Beobachtungen. Nur die erste Leselinie vermeidet weiterhin doppelte Zaehler; Results wird nicht durch diese Uebersichtsfilter gekuerzt. Keine neuen Quellen oder fachlichen Ableitungen. Permanenter synthetischer Dashboard-Klicktest in beiden Themes prueft die vier integrierten Werte, das Fehlen der Directory-Kachel, alle elf Result-Felder, Originalevidenz und unveraenderte Work-Filter. 50 fokussierte Dashboard-/Result-/UI-API-Versionspruefungen, Design-/Local-only-Pruefung, diff check und beide Produktionsbuilds bestanden. Patchversion fuer die rueckwaertskompatible UI-Zusammenfuehrung; Package/Lockfile und Versionsvertraege synchron. Abnahmegrenze: synthetische DOM-Interaktionen und bestehende Theme-Tokens, keine neue Browser-Screenshot-Abnahme. Bestehende Bundle-Groessenwarnung bleibt; kein Backend-Neustart, keine Persistenz- oder Engine-/Dataflow-Aenderung, keine Phasenabschlussbehauptung. + +Build-Abnahme zur nachfolgenden Korrektur 2.12.3: Client- und Serverproduktionsbuild erfolgreich; bestehende Client-Bundle-Groessenwarnung bleibt. `git diff --check` ohne Fehler. + +2026-10-02 - Context-Zaehlung und Smartcard-Zuordnung korrigiert (Phase 13E, Patch 2.12.3): Die Heatmap ignoriert den historischen Fallback-Condition-Eintrag eines GPO-Findings nach `assigned-by-recognition` und zaehlt es nur in seinen zugeordneten Clustern. Zuvor zeigte Other solche bereits zugeordneten GPO-Abweichungen weiterhin, waehrend Work sie korrekt ausschloss. Der permanente synthetische Dashboard-Interaktionstest reproduziert 29 neu zugeordnete GPO-Settings plus zwei verbleibende Scanner-Findings und prueft nach Kachel-Klick die Uebereinstimmung von Kachelzahl und Work-Facet-Mitgliedschaft. Katalog 1.9.1 ergaenzt Purple Knight SI000147 anhand des vom Nutzer vorgelegten Berichtstexts: deaktivierte Smartcard-only-Kennwortrotation gehoert zu Lateral Movement Exposure; eine eigene Bedingung und vorsichtige Aussage bewahren die konkrete Bedeutung, ohne eine ATT&CK-Technik oder einen kompromittierten Zustand zu behaupten. Die unspezifische Maester-Regel MT.1024 ohne konkrete Empfehlung bleibt Other; keine Zuordnung aus der generischen Rule-ID. Regressionen pruefen fail/warning, Pass-/Not-run-Ausschluss, unveraenderte Eingabedaten und exakte Regelzuordnung. 100 unterschiedliche fokussierte Context-, Reader-, Facet- und Produktversionspruefungen bestanden, einschliesslich erneuter Reader-Pruefung nach Anpassung des dynamischen Katalogversionsvertrags. Design-/Local-only-Pruefung bestanden. Produkt-Patchversion nach aktueller expliziter Platform/Core-Tests-Freigabe synchronisiert; keine Core-Finding-, Verdict-, Severity-, Datenbank- oder API-Vertragsaenderung. Kein Backend-Neustart oder Kundendatenzugriff; Phase bleibt in Arbeit. Produktionsbuild-Abnahme wird nach Abschluss unten ergaenzt. + +2026-10-02 - Version 2.12.2, kompakte Heatmap und direkter Finding-Klickpfad umgesetzt und fokussiert verifiziert: Die ausdrueckliche Nutzerkorrektur ersetzt die nachfolgend historisch dokumentierte Detailauswahl. Jede Kachel zeigt nur Base Severity, Kategorie und die Gesamtzahl zugeordneter Scanner-/GPO-Findings. Die vorhandene Deduplizierung bleibt bestehen; Bedingungen sind keine Findings. Kacheln verwenden die gemeinsame kompakte Breite von 186px und werden stabil nach ihrer dargestellten Farbe sortiert: Critical, High, Medium, neutral. Klick, Enter und Leertaste oeffnen unmittelbar Work mit dem exakten Context-Cluster-Filter und Abweichungsstatus, ohne Heatmap-Detailansicht. Alte Such-/Source-/Arbeitsfilter werden zurueckgesetzt; aus einem einzelnen Quellblatt wird auf das befuellte Projektblatt gewechselt, damit die gesamte Clusterliste ueber alle Quellen erreichbar ist. Timeslot und Client bleiben erhalten. Keine Aenderung an Quellen, Katalog-Grundschwere, Verdicts, Core-Scores oder Persistenz; keine neue Engine-/Dataflow-Beziehung. Abnahme: 60 fokussierte Tests ueber Context, Architektur, Dashboard, Quellnavigation und UI/API-Versionsvertraege bestanden; Design- und Local-only-Pruefung sowie beide Produktionsbuilds bestanden. Synthetische Browserpruefung in beiden Themes und bei 358px Containerbreite ohne Ueberlauf, mit Maus und Tastatur, bestanden. Permanente Interaktionsregression prueft den echten Dashboard-Klickpfad einschliesslich Quellblattwechsel. Patchversion fuer die rueckwaertskompatible Darstellungs-/Navigationskorrektur, Package/Lockfile und Versionsvertraege synchron. Bestehende Bundle-Groessenwarnung bleibt; kein Backend-Neustart und keine neue Abnahme realer Kundendaten. Phase 13E bleibt in Arbeit. + +2026-10-02 - Version 2.12.0, umgesetzt und fokussiert verifiziert: Auf Nutzerauftrag ist die recherchierte Tabelle mit allen 22 bestehenden Context-Clustern als eigener versionierter Quellenkatalog `src/assessment-context/context-cluster-severity.ts` (1.0.0, Recherche-/Pruefdatum 2026-10-02) hinterlegt. Einstufung: vier Critical, 13 High, vier Medium, ein Undetermined. Jede fachlich eingestufte Kategorie besitzt Begruendung, Einschraenkung und lokal gespeicherte Quellenangaben; die Einstufung ist eine kuratierte ASSESSOR-Grundschwere und kein von den Quellen vergebener Cluster-Score. `Other Deviations` bleibt unbestimmt. Die bisherigen Security Domains und ihre Zuordnung bleiben unveraendert. + +Entscheidung und begrenzte Erweiterung des bisherigen Context-Darstellungsvertrags: Die neue Heatmap darf die bestaetigte statische Kategorie-Grundschwere mit den bestehenden Severity-Farbtokens darstellen, wenn das vorhandene Context-Ergebnis Belege enthaelt. Sie erzeugt keine Finding-Severity, kein CVSS, keine normalisierte Prioritaet, kein Pass/Fail und keine clientbezogene Aussage zur Ausnutzbarkeit. Ohne Belege sowie bei unbestimmter Grundschwere bleibt eine Zelle neutral; neutral bedeutet nicht sicher. Bedingungen, Clusterzustand und Zuordnungssicherheit bleiben getrennt. Die Bedingungen und Cluster-Schwellen werden unveraendert aus Context uebernommen. Scanner-Abweichungen, GPO-Baseline-Abweichungen, deklarierte Techniken sowie erkannte und geschaetzte Zuordnungen werden aus dem vorhandenen Ergebnis gelesen und pro Cluster nach Finding-Occurrence bzw. GPO-Identitaet/Scope dedupliziert; es gibt keine neuen Quellen, Neuinterpretation von Verdicts oder Requests pro Zelle. Reine GPO-Konfigurationsbelege bleiben als Bedingungen erkennbar und werden nicht als beobachtete Host-Findings ausgegeben. Cluster duerfen ueberlappen; ihre Zahlen werden nicht zu einer vermeintlich eindeutigen Gesamtsumme addiert. + +Client Info zeigt die Heatmap fuer den ausgewaehlten Timeslot ueber alle Quellen, ausdruecklich unabhaengig vom Source-Auswahlfeld. Sie nutzt denselben vorbereiteten Context-Abruf wie die bestehenden Leser. Ein Klick bzw. Enter/Leertaste markiert die Zelle gelb und oeffnet deren Grundschwere, Bedingungen, Zuordnungssicherheit, Begruendung und Quellen. Der Fokus wandert zu den Details und beim Schliessen zurueck zur Zelle. Bei Client-/Scope-/Revisionswechsel verfaellt die Auswahl. Context aus entfernt die Heatmap vollstaendig und erzeugt keine neuen Anfragen; spaete Antworten bleiben ausgeblendet. Ladefehler werden nicht als leere oder sichere Bereiche dargestellt. Der neue Core-Erweiterungspunkt in `DashboardPanel.tsx` ist nach aktueller `Core-Freigabe: Presentation, Core Tests, Platform` in der Architekturpruefung eingetragen; fachliche Core-Berechnungen bleiben frei von Context-Abhaengigkeiten. + +Abnahme: 116 fokussierte Context-, Architektur-, Client-Info-, Navigations- und UI/API-Versionsvertragstests bestanden; anschliessender Fokus-/Tastaturtest 4/4 nach letzter Scrollkorrektur bestanden. Der synthetische Test mit dem echten Dashboard vergleicht die vollstaendige Core-Darstellung und Dataset-Werte bei aus-/eingeschalteter Engine. Weitere Tests pruefen Vollstaendigkeit aller 22 Eintraege, fehlende native Scanner-Severity, deduplizierte Scanner-/GPO-Zahlen, Passed-Ausschluss, unbekannte Bereiche, unveraenderte Context-Ergebnisse und spaete Antworten. Synthetische Browser-Sichtpruefung in Hell/Dunkel und bei 358px Containerbreite ohne horizontalen Ueberlauf, einschliesslich Maus, Enter, Leertaste und Fokusrueckkehr, bestanden. Beide Produktionsbuilds, Design- und Local-only-Pruefung bestanden; bestehende Bundle-Groessenwarnung bleibt. Minor-Version 2.12.0 fuer die neue rueckwaertskompatible Faehigkeit; Package, Lockfile und Versionsvertraege synchron. Kein Datenbankeingriff, keine Migration, kein Backend-Neustart und keine Phasenabschlussbehauptung. Abnahmegrenze: synthetische UI-/Datenpruefung, keine neue Pruefung eines realen Kundenbestands. Neue dargestellte Beziehung: Context Engine -> Client-Info-Heatmap; Engine-Diagramm-Aktualisierung bleibt ein gesonderter Nutzerauftrag gemaess Erinnerung unten. + +## Verbindlicher Diagramm-Pfeilstandard + +2026-10-02 - Phase 7 / Ready indicator and explicit minimization (2.12.1 patch, implemented and verified): current Presentation/Core Tests/Platform release adds the requested green system-status dot beside Ready in both expanded Activity and the single compact button. Ready text remains accessible; the decorative dot uses the existing theme-aware state-online token. A native minimize button with an × sits immediately right of History when Ready; clicking or pressing Enter collapses the panel and its disclosure without removing history or changing readiness. The existing five-second automatic minimization and new-work restoration remain unchanged. Permanent both-theme Activity interaction acceptance verifies the actual minimize/reopen path, retained failed history, dot presence only while Ready, action placement and absence while waiting/not ready. Verified: 22 Activity and UI/API version tests passed, security/design checks and both production builds passed. Synthetic production-component browser verification confirms both theme colors, automatic compact state, mouse/Enter minimization and history reopening; screenshot retained. Version decision: backward-compatible presentation refinement, patch from the concurrently established 2.12.0 to 2.12.1, package/lock and both version contracts synchronized. No full-suite rerun for this focused correction; existing client bundle warning remains. Concurrent changes preserved, no phase completion or engine/data-flow change. + +2026-10-02 - Phase 7 / confirmed client identity edits (2.11.10 patch, implemented and verified): explicit user decision requires a mandatory warning before saving changes to the existing Client Information Name or Consultant. Current Presentation/Core Tests/Platform release implements that guard in the shared native form-submit path, including implicit Enter submission. The dialog lists only changed fields with previous and proposed persisted values. Cancel, Escape, close and backdrop dismissal write nothing and retain the draft for review; Confirm changes saves the reviewed form snapshot through the existing API and read-back verification. Re-submitting after cancellation requires confirmation again. Unchanged identity fields, Remarks-only edits and new-client creation retain their existing flow. Initial focus is Cancel, keyboard focus cycles inside the dialog and closure restores the prior focus; Enter on Cancel never confirms. A synchronous save guard prevents duplicate API writes. Existing project ID, creation time and unrelated attributes are retained; no repository/schema/API persistence contract change. Shared dialog, theme surfaces, control geometry and 14px labels/16px values are used; no new palette. Permanent actual DataHub interaction acceptance covers Name, Consultant and both changes, old/new values, no writes before confirmation, Escape/Cancel, native form resubmission, one confirmed save, read-back and preservation of unrelated data; Remarks-only edits remain direct. Verified: 19 Client Info/reference interaction/layout tests plus 15 version/API/Core-boundary tests passed; local-only security/design checks and both final production builds passed. Synthetic shared-dialog browser verification covers dark/light, Cancel and explicit confirmation by Enter, focus wrapping and readable old/new values. Version decision: backward-compatible presentation safety correction, patch 2.11.10. No full-suite rerun for this focused change, no phase completion claimed; existing bundle warning and concurrent changes preserved. No engine/data-flow relationship change or diagram update required. + +2026-10-02 - Phase 7 / minimal Ready button (2.11.9 patch, implemented and verified): current Presentation/Core Tests/Platform consent implements the user's correction that the minimized Activity panel still obstructed work. Collapsed Activity now renders one neutral shared-geometry Ready button, with no panel surface, border, padding or second History control. Its transparent positioning wrapper fits the button instead of reserving the 560px panel width. Clicking or pressing Enter opens the existing panel with History already expanded. New work still opens the full current status and the existing five-second Ready timer is unchanged. Manual collapse of a non-ready state retains its actual status text rather than asserting Ready. Readiness, request identity, historical outcomes and data quality contracts remain unchanged. Permanent both-theme interaction acceptance now verifies a single Ready button, absence of the panel, direct history opening and automatic panel restoration from the tiny state when new work starts. Updated earlier interaction tests select the newly mounted compact control after collapse instead of clicking a detached panel button. Verified: 22 activity/UI/API version tests passed; local-only security and design checks and both production builds passed. Synthetic current-component browser checks confirm dark/light, mouse/Enter history opening and an approximately 66x28px compact control with a transparent, unpadded, borderless wrapper. Version decision: backward-compatible presentation refinement, patch 2.11.9. No full-suite rerun for this focused correction, no phase completion claimed; existing bundle warning and concurrent changes preserved. No engine/data-flow change or diagram update required. + +2026-10-02 - Phase 7 / final Ready and timed minimization (2.11.8 patch, implemented and verified): current Presentation/Core Tests/Platform release implements the user's final-state correction. Activity shows Ready only when no operation is active/queued and the latest attempt for every tracked operation has completed. A successful retry replaces that operation's earlier failure for readiness while keeping the failed invocation in History. API operation identity includes backend, method and exact request path/scope; a success for a different client cannot resolve another client's request failure. Display labels can change without changing operation identity; identifiers/URLs remain internal and are never rendered. Latest operation outcomes are retained separately from pruned/cleared history, so clearing or dismissing rows cannot manufacture readiness. This is conservative: an outstanding unsuccessful operation still prevents Ready until the corresponding operation succeeds. Earlier failures/cancellations now appear only inside the optional History, without a Needs attention decision queue. This explicitly supersedes the 2.11.6 failure-first presentation and immediate all-success collapse. Ready stays expanded for five seconds, then automatically minimizes to the compact Activity/History bar. New work opens the panel, cancels an earlier timer and starts a fresh five-second interval after successful completion; manually reopening History after minimization stays open until another load cycle. No request is retried or changed by this UI. Native buttons, existing neutral styles and readable Ready content retained. Verified: twelve activity acceptance tests include both themes, recovered failures, exact 4,999/5,000ms boundary, queued/new work at an old deadline, reopening History, exact request-scope matching, clearing/dismissing/pruning invariance and concurrent API behavior. Full suite 1,807 passed, 15 skipped; local-only security/design checks and shared UI/API version contracts passed; both production builds passed. Synthetic production-component browser checks confirm Ready, automatic minimization and History reopening in dark/light. Version decision: backward-compatible status correction, patch 2.11.8. No source, assessment, engine execution, persistence or Data Flow relationship change; no diagram update required. Existing bundle warning remains, no phase completion claimed, concurrent work preserved. + +2026-10-02 - Phase 7 / reachable Context Cluster options (2.11.7 patch, implemented and verified): current Presentation/Core Tests/Platform release applies the user's request to the shared Work filter projection. Context Cluster options with zero hits under the existing self-excluding facet counts are no longer offered. Search, status and other active restrictions still determine those counts; selecting a cluster does not hide reachable alternatives from that same facet. All remains available, including an empty scope. A currently selected cluster that becomes empty remains visible as a disabled current option so the UI does not silently drop a restriction; All explicitly clears it. This supersedes the earlier Work-control behavior of offering every catalog cluster at zero hits. The complete engine vocabulary and results, all other declared facets, filter counts and engine activation remain unchanged. Permanent actual SearchFilterBar acceptance covers selecting a cluster, narrowing search to another cluster, disabled zero-hit selection retention, choosing All and a completely empty result scope. Existing badge selection and both-theme checks remain. Verified: 49 filter/Context/GPO tests plus 20 UI/API version and architecture/invariance tests passed; local-only security and design checks and both production builds passed. Version decision: backward-compatible presentation correction, patch 2.11.7. No full-suite rerun for this small focused correction, no phase completion claimed; existing bundle warning and concurrent changes preserved. No data-flow or engine relationship change; no diagram update required. + +2026-10-02 - Phase 7 / activity overview (2.11.6 patch, implemented and verified): current Presentation/Core Tests/Platform consent implements the approved compact activity design. Needs attention appears before running/waiting work, cancellations stay distinct, and the summary includes failures even while other operations run. The operation label is primary and the engine secondary. Successful history starts collapsed; an explicitly opened panel/history stays open through updates. Exact engine/label repeats form expandable groups retaining every recorded invocation. The fixed panel header sits above a scrolling body using the shared 20px inset, 14px informational text and existing controls/theme tokens. All-success state defaults to a compact header with History count. Durations below one second show <1s; active durations update from real timestamps, with no inferred percentage or ETA. Clear completed removes only successes. Failures and cancellations survive success-history pruning and can be acknowledged individually with Dismiss, which never removes active tasks. The existing 40-success history bound remains; acknowledged task records are UI telemetry, not assessment data. Legacy clearFinishedActivities remains for existing non-UI cleanup callers. No invented error detail or retry action: current telemetry contains a state, engine, operation label and timestamps only. No source, result, engine execution, database or Data Flow relationship changes; no diagram update required. Verification: eight activity acceptance tests include real concurrent API operations, exact grouping, opened-history invariance, failure/cancellation retention through pruning and success clearing, individual dismissal and active-task protection. Full suite 1,802 passed, 15 skipped; local-only security and design checks passed; shared UI/API version contracts and both production builds passed. Synthetic current-component browser checks cover light/dark, compact all-success state, keyboard history opening, group expansion and failure/active preservation after Clear completed. Version decision: backward-compatible UI/status refinement, patch 2.11.6. Existing client bundle-size warning remains; no phase completion claimed and concurrent work preserved. + +2026-10-02 - Phase 7 / separate no-hit recovery actions (2.11.5 patch, implemented and verified): current Presentation/Core Tests/Platform release implements the user's correction in Work, Result DB and Pathfinder. The shared companion now has exactly two actions. Reset filters delegates to each view's existing normal reset, including Work/Pathfinder's existing My Workspace behavior and Result DB's default Deviation filter. Search all Findings clears result restrictions and selects All Findings within the current client, timeslot and active sheet; Result DB clears filter values while retaining the exact project/assessment/scan scope. The user's two-action correction explicitly supersedes the earlier separate source-preserving no-hit recovery button: Search all Findings also clears the source facet. Pathfinder retains its binding deviation-only graph scope; its Search all Findings action opens Work with the All filter through the existing navigation callback. No source, verdict, disclosure, persistence or engine behavior changes; no Data Flow diagram update required. Permanent synthetic interaction acceptance compares the no-hit Reset action directly with normal reset and verifies both actions in all three real views, Pathfinder-to-Work navigation, unchanged client/timeslot/sheet, unchanged source data and scoped Result DB queries. Verification: full suite 1,799 passed, 15 skipped; local-only security and design checks passed; UI/API product-version contracts and both production builds passed. The earlier import timeout did not recur in this run. Version decision: backward-compatible correction, patch 2.11.5 shared by UI and API. Existing client bundle-size warning remains; phase status unchanged and concurrent work preserved. + +Visual verification for the 2.11.5 no-hit action correction: the shared production component's explicitly synthetic preview was inspected in light and dark; both native buttons remain readable and use the existing control styles. The preview supplies no customer data and does not replace the actual-view interaction acceptance above. + +2026-10-02 - Phase 7 / process-arrow clearance correction: current Presentation/Core Tests/Platform release covers this local presentation fix. Fresh desktop geometry already cleared the earlier screenshot's intersections, but browser testing exposed direct upload and source-result paths crossing intervening tiles in the responsive one-column layout. Each proposed orthogonal path now checks every measured tile rectangle with stroke clearance; intersecting paths select a clear measured side rail. Source and destination ports stay 4px outside tile boundaries so triangular tips also clear the tiles. The existing engine arrow style, 17 declared deliveries, yellow connected-edge selection and table popup behavior are retained. Permanent synthetic acceptance selects a finding tile, resizes from unequal desktop lanes to stacked tiles, checks every segment against every tile, checks destination clearance and verifies retained selection. Verified: 17 focused diagram/UI/API version tests passed, security/design checks passed, both production builds passed. Live checks in light and dark at desktop width and in the stacked layout found zero arrow or label intersections. Existing bundle-size warning remains. Version decision: included in the related current 2.11.4 refinement batch already present in the shared workspace; no separate version edit. No API, persistence, engine behavior or phase-status change; concurrent work preserved. + +2026-10-02 - Phase 7 / process tile selection and arrow spacing: current Presentation/Core Tests/Platform consent increases processing gaps from 64px to 96px and the shared maximum content width to 1266px, preserving 358px tiles. Clicking a tile surface or its native title button selects/deselects it independently of table detail selection. Selected tiles have the same yellow outline and side marker as engine modules. Every directly connected arrow is highlighted along its complete route, including labels and triangular arrowheads; unrelated arrows remain neutral, matching the engine diagram's connected-edge selection semantics. Consumer routes have more separated label levels. Table chips still open the existing popup and closing it preserves an independently selected tile. Permanent synthetic acceptance covers surface selection, exact connected paths, yellow-marker references, chip navigation, popup closing and deselection. Verified: 10 focused tests, security/design checks and production builds passed; live mouse and Enter-key selection/deselection plus light/dark styling verified. Version decision: same related unreleased 2.11.3 refinement batch, no separate version edit. No data-flow semantics, API, persistence or engine behavior change. Phase status unchanged; existing bundle warning remains. + +Explizite Nutzerentscheidung 2026-10-02: Die bestehende Pfeildarstellung in `Engines & relationships` (`src/features/db-structure/EngineDataFlow.tsx`, `.db-engine-flow__edges` und `.db-engine-flow__edge-label` in `src/styles/datahub.css`) ist die verbindliche visuelle Referenz fuer Beziehungspfeile in Diagrammen. Auch der darunterliegende Data Flow uebernimmt diese Darstellung. Beziehungen werden als durchgehende, gerichtete Linien mit klarer dreieckiger Pfeilspitze und lesbarer Beziehungsbeschriftung dargestellt; rechtwinklige Verzweigungen verwenden denselben Stil. Implementierte Beziehungen sind durchgezogen, geplante Beziehungen gestrichelt und ausdruecklich als geplant beschriftet. Tatsaechlich bidirektionale Beziehungen tragen Pfeilspitzen an beiden Enden. Neutrale Linien verwenden `--line-strong`, ausgewaehlte Beziehungen `--marking-color`; Linienstaerke, Marker-Geometrie und Beschriftungsrollen folgen der genannten Referenz. Keine dekorativen Effekte oder neue Palette. Dieser Standard ersetzt ausschliesslich die Pfeildarstellung, nicht die bestehenden Interaktionen oder fachlichen Beziehungstypen anderer Diagramme. + +Phase-7-Auftrag, zunaechst als Dokumentationsentscheidung gespeichert und am 2026-10-02 nach erneuter Freigabe umgesetzt: Fehlende sichtbare Beziehungen im unteren Data Flow ergaenzen und bestehende Verbinder an diesen Standard angleichen. Vor der Darstellung jede Verbindung gegen den implementierten Datenfluss pruefen; keine Beziehung allein aus der Kachelposition ableiten. Funktionen bleiben unveraendert. Keine Produktversionsaenderung fuer die urspruengliche Dokumentationsentscheidung. + +2026-10-02 - Phase 7 / process relationship arrows: on current Presentation/Core Tests/Platform release, replaced disconnected CSS connector fragments with 17 labelled, directed SVG relationships using the existing engine diagram's 2px lines, 7px triangular markers and 14px labels. Added upload branches, processing chains, baseline input, source-result merge, disclosed-storage-to-Result-DB delivery, direct scanner/policy findings to Work and scanner history to comparison. Other tables remain unconnected because their processing assignment is unknown. Source checks: repository rebuildResultDb source readers, policy/scanner Work projections and compareProjectTimeslotEvidence. No Policy-history arrow: the implemented timeslot comparison consumes scanner runs. DOM geometry and ResizeObserver adapt routes to current tile sizes; merge paths clear taller GPO tiles, side routes bypass consumers, selected table endpoints highlight connected paths. Existing engine overview semantics, popup/table navigation, schema refresh and dictionary retained. Permanent synthetic interaction acceptance now verifies the previously missing relationships, arrow markers, selection/closing invariance and a resize with unequal lane heights. Verified: 10 focused tests passed; security/design checks and both final production builds passed; current-code light/dark browser inspection passed. Existing bundle-size warning remains. Version decision: same related unreleased 2.11.3 refinement batch; no separate version edit. Concurrent backend work untouched, no API/schema/engine behavior change, no phase completion claimed. + +2026-10-02 - Phase 7 / shared filter-empty companion and direct reset (2.11.4 patch, implemented and verified): user chose Work, Result DB and Pathfinder, then renewed Presentation/Core Tests/Platform consent. Added an original local SVG companion with a magnifying glass and gentle searching motion to the completed no-hit states. It uses existing neutral theme roles, shared typography/control geometry and an 88px size token; reduced-motion preferences disable animation. It is decorative and hidden from assistive technology; the visible no-hit heading stays authoritative. Shared native Reset filters buttons support mouse and keyboard. Work and Pathfinder clear search, facets, status restrictions, My Workspace-only and path restrictions while preserving the client, timeslot and active sheet. Work retains its distinct Show all results for this source action, including the documented source-preserving behavior. Result DB uses its existing reset contract (Deviation enabled, remaining filters cleared) and preserves its exact project/assessment/scan scope. Pathfinder's empty overlay permits button clicks without intercepting the graph elsewhere. No engine, importer, evaluation, source, storage or disclosure changes; no Data Flow diagram update required. + +Verification: 15 focused tests passed across new actual Work/Pathfinder/Result DB no-hit/reset interactions, GPO visibility and source recovery, graph text behavior and Result DB scope. Tests prove results return, all intended restrictions clear, source values stay unchanged and client/timeslot/sheet remain selected. Existing GPO source-action acceptance now identifies that action by its text instead of assuming the first button. Final three interaction tests passed again after a test-only optional-query typing correction. UI/API version contracts passed. Full suite: 1,794 passed, 15 skipped, one known default 5-second node-import-engine timeout; separate default reruns also timed out. Diagnostic rerun with a CLI-only 15-second timeout passed all five tests, with the affected format test taking 4,034ms; permanent acceptance deadlines remain unchanged and the default full suite is not claimed green. Security/design checks and both final production builds passed; existing client bundle-size warning remains. Dummy-client live Work inspection passed in light/dark; Pathfinder reset via Enter restored graph nodes. Shared component screenshots saved as filter-empty-light.png and filter-empty-dark.png. Result DB click verified with synthetic API data and the real component; live Result DB inspection was interrupted by the development backend rebuild/page reload, so no completed live Result DB reset claim. During the earlier live client opening, the separate Context request returned HTTP 500 after about five seconds; Core results opened through existing error handling. That runtime Context failure is an unresolved separate observation, not fixed or certified by this UI batch. App/backend health reports 2.11.4 with PostgreSQL revision 535 unchanged. Version decision: backward-compatible UI refinement, patch 2.11.3 -> 2.11.4; package/lock and UI/API version contracts synchronized. Phase status unchanged; no milestone completion claimed. Concurrent Data Flow/backend changes retained. + +Additional visual acceptance fixture for the 2.11.4 empty-state batch: `tests/filter-empty-state-preview.html` renders the shared production component with explicitly synthetic text and no backend reads. Its complete captioned screenshot is `filter-empty-preview.png`; this is a component preview, not a substitute for the real reset-interaction tests described above. + +## Verbindliche Erinnerung: Engine-Diagramm bei Data-Flow-Aenderungen + +2026-10-02 - Phase 7 / Data Flow layout alignment: current Presentation/Core Tests/Platform consent covers a CSS-only follow-up. Summary, schema actions, engine diagram, inspector, process flow and dictionary now share the diagram's 1130px maximum content width and centered alignment. Reference data starts level with the processing lanes; lane tiles remain centered on their connector spines. Result DB and Work form a compact centered pair. All content, relationships, selections and detail-window behavior retained. Verified: 10 focused layout/flow/interaction tests passed after retaining the existing segment-inset contract; security/design checks, client and server production builds passed, with the client rebuilt after final CSS corrections. Live light/dark inspection confirms identical section width and alignment. Existing bundle-size warning remains. Version decision: included in the current unreleased 2.11.3 refinement batch already present in the shared workspace; no separate version edit. Concurrent backend/performance work was left untouched. Phase status unchanged; no architecture or data-flow semantics changed. + +Explizite Nutzerentscheidung 2026-10-02: Bei jeder Umsetzung, die den dargestellten Data Flow betrifft, erinnert der bearbeitende Agent den Nutzer im Abschlussbericht daran, den Auftrag zur Aktualisierung des Engine-Diagramms zu geben. Ausloeser sind neue, entfernte oder geaenderte Engines, Datenquellen, Verarbeitungsschritte, Lese-/Schreibports, Speicherorte, UI-/Exportverbraucher und Beziehungen sowie der Wechsel einer dargestellten Beziehung zwischen geplant und implementiert. Die Erinnerung nennt konkret, welcher Teil der Darstellung betroffen ist. + +Die Erinnerung lautet: "Diese Aenderung betrifft den Data Flow (). Bitte gib mir den Auftrag: Aktualisiere das Engine-Diagramm." + +Wenn die aktuelle Nachricht die Diagrammaktualisierung nicht bereits ausdruecklich beauftragt und die betroffenen Core-Bereiche freigibt, fuegt der Agent die erforderliche Freigabephrase hinzu: "Core-Freigabe: Presentation, Core Tests, Platform". Bestehende ausdrueckliche Auftraege werden innerhalb ihrer aktuellen Freigabe ausgefuehrt; keine doppelte Erinnerung fuer eine bereits erledigte Aktualisierung. Keine automatische Aktualisierung und keine Behauptung, das statische Engine-Diagramm erkenne Architekturveraenderungen selbst. Die bestehende nachrichtenbezogene Core-Freigaberegel bleibt unveraendert. + +Dokumentationsentscheidung: dauerhaft im Implementierungsplan gespeichert; keine Code-, Governance-Datei- oder Produktversionsaenderung. Phasenstatus unveraendert. + +2026-10-02 - Phase 7/13E / quality-preserving client read optimization (2.11.3 patch, implemented and verified): explicit Store/Platform/Presentation/Core Tests release; data quality takes precedence over speed. PostgreSQL internal-analysis pagination reuses complete source projections within the exact query scope (all arguments except offset/limit). Every call checks the live database revision before and after reading, including cache hits and commits from other backend/worker instances. A revision change discards the read and retries up to three times; repeated changes or an unverifiable revision fail explicitly. Concurrent identical reads share their source load; failed loads are evicted. Retention is process-local, bounded to eight entries and 32 MiB per repository; oversized results remain complete but are not retained. Returned pages and categories are independent copies. Result DB disclosure continues through its existing materialized, disclosure-checked path and never consumes the internal cache. Context additionally reuses full policy evaluations, baseline analyses, assessment lists and models only inside a single revision-checked attempt; retries and new Context runs have fresh read ownership. No schema, migration, persisted source, verdict, score, reference catalogue or engine activation change. Client opening still waits for enabled Context before publishing its workspace; original Raw Data and historical-score loading remain intact. + +Verification and limits: the permanent synthetic PostgreSQL paging test compares every field, ordering and count for all 2,501 results across three pages against uncached derivation, with one instead of three full source loads. It covers caller mutation, same-count content changes through a live revision, scope separation and the separate disclosure path. Cache and Context tests cover concurrent reads, bounded retention without truncation, failure/retry and revision-change retry with complete Context equality. Full suite: 1,792 passed, 15 skipped; security and design checks passed. Final focused checks after query-snapshot/read-port tightening: 32 passed, including actual client opening/Context waiting and UI/API version contracts. Both final production builds passed; existing client bundle-size warning remains. App HTTP 200 and backend PostgreSQL health at 2.11.3, repository revision 535. Actual cold-client latency against the reported 80 seconds has not been measured under a controlled identical-client workload; no percentage or time reduction is claimed. Large initial reads and Context computation still run in full, and any repository commit invalidates reuse, including unrelated scopes. Version decision: backward-compatible performance refinement, patch 2.11.2 -> 2.11.3 with package/lock and both version contracts synchronized. Phase status unchanged; no milestone completion claimed. Data Flow follow-up: remind the user to request an Engine diagram update for the revision-checked Core read reuse and request-local Context read step; no automatic diagram edit. + +2026-10-02 - Phase 7 / Data Flow process-tile refinement: with current Presentation/Core Tests/Platform consent, adapted the existing processing tiles to the engine tiles' flat neutral surfaces, square geometry and shared side marker. Limited tiles to the existing 358px content width and centered the three processing lanes within that width; retained typography, all source labels, table counts, connectors, table selection, relationship highlights and floating detail windows. Long table names wrap without truncation. CSS only; no data flow, API, engine or persistence changes. Verified: 20 focused flow/layout/interaction and UI/API version tests passed; final security/design checks and both production builds passed. Live inspection passed in light and dark, all 17 tiles had no horizontal content overflow, and the scan_runs detail window opened and closed normally. Existing bundle-size warning remains. Version decision: part of the same related unreleased 2.11.2 presentation refinement batch; no additional increment. Phase status unchanged. + 2026-10-02 - Phase 7 / interactive Data Flow diagram (follow-up to the preceding overview): on renewed Presentation/Core Tests/Platform release, replaced the always-open engine table with an interactive architecture diagram. Nine selectable modules include all three engines, source inputs, canonical PostgreSQL, read-only port, UI consumers and report outputs. Nine labelled relationships show directional delivery, bidirectional Core database read/write and the dashed, explicitly Planned Context-to-Report target. Selection highlights connected edges and opens an inspector; native buttons support keyboard activation and deselection. The complete relationship table remains in a closed disclosure. Narrow views scroll the diagram without shrinking 14px information text; inspector stacks. Coordinates live in CSS classes, using current tokens and flat tiles, with no engine imports, requests or data writes. Existing schema flow, table popups and dictionary retained. Version decision: same unreleased, related 2.11.2 refinement batch; no additional version increment. Permanent synthetic tests cover module selection, deselection, directed/bidirectional markers, planned output and disclosure; 20 focused tests passed before the final CSS correction, and 14 final diagram/layout/import tests passed afterward. Full suite: 1783 passed, 15 skipped, one unrelated 5-second timeout in node-import-engine; all five tests in that unchanged suite passed in the separate rerun (format test 3504ms). Security/design checks passed. Both production builds passed; final client rebuild after the CSS-class and read/write-marker corrections also passed. Existing bundle-size warning remains. Live mouse/keyboard selection, both themes and horizontal overflow checked. Phase status unchanged; planned report Context integration remains outside this UI change. 2026-10-02 - Phase 7 / Data Flow engine overview (2.11.2 patch): renamed DB Structure to Data Flow in navigation and header. Added Core, Context and Report with nine directed data-delivery relationships, canonical PostgreSQL, read-only Core port, UI consumers and separate report archive. Context-to-Report is explicitly Planned: current report generation does not consume published Context results. Architecture includes disabled engines. Corrected obsolete Result DB wording and assigned result_db_rows/result_db_state to Core-owned disclosure materialization before its consumer. Existing table details preserved. No API, persistence or engine behavior changes. Verified: 1783 tests passed, 15 skipped; UI/API version contracts, architecture and engine invariance passed; security/design checks and both production builds passed, client build repeated after final order correction. Live inspection in both themes; backend healthy at 2.11.2 with PostgreSQL. Existing bundle warning remains. Version decision: backward-compatible refinement, patch 2.11.1 -> 2.11.2, package/lock synchronized. Phase status unchanged. Follow-up request for a more futuristic diagram awaits renewed Core consent under the per-message rule. @@ -2063,6 +2467,17 @@ Abnahmekriterien: - Betrieb, Wiederherstellung und Update sind dokumentiert und getestet. - Es findet keine Übertragung an externe Dienste statt, sofern dies nicht später ausdrücklich beschlossen wird. +#### Spaeteres Arbeitspaket: automatische Security-Baseline-Updates + +Nutzerentscheidung 2026-10-02: fuer spaeter vormerken, jetzt nicht implementieren. Zuordnung: Phase 13B, mit Backend-Betrieb aus Phase 10 und Kataloganzeige aus Phase 7. Status: geplant / zurueckgestellt; keine automatische Pruefung, kein Download und kein Zeitplan aktiviert. + +- Ziel: Das Backend kann regelmaessig nach neuen Security-Baseline-Versionen suchen, sie herunterladen und ueber den bestehenden validierten Importweg als eigene Version aufnehmen. Der manuelle ZIP-Upload bleibt verfuegbar. +- Oberste Praemisse: Datenqualitaet vor Geschwindigkeit. Vor Uebernahme sind Herkunft aus festgelegten offiziellen Quellen, eindeutige Version, unterstuetztes Format und Vollstaendigkeit zu pruefen. Originalartefakt, Herkunft, Pruefsumme und Validierungsergebnis muessen nachvollziehbar erhalten bleiben. +- Fehler, unvollstaendige Pakete, unklare Versionen und nicht erreichbare Quellen duerfen die letzte gueltige Baseline nicht ersetzen. Gleiche Versionen duerfen keine doppelten Katalogeintraege erzeugen; neue Versionen bleiben getrennt von vorhandenen Releases. +- Bestehende Auswertungen und ihre Baseline-Zuordnung werden durch ein Update nicht automatisch ueberschrieben oder neu bewertet. Eine erneute Auswertung bleibt ein gesonderter Vorgang ueber den bestehenden kontrollierten Pfad. +- Vor Umsetzung klaeren: unterstuetzte Hersteller/Produkte und offizielle Downloadquellen, Pruefintervall, Proxy-/Offline-Betrieb sowie sichtbare Update- und Fehlerhinweise. Quellenwechsel und Formatdrift sind Risiken; die Update-Pruefung darf keine Mandantendaten an externe Dienste uebertragen. +- Spaetere Abnahme mit synthetischen Quellen/Paketen: gueltige neue Version, identische Version, falsche Herkunft, unklare Version, unvollstaendiges oder ungueltiges Paket und Netzwerkfehler; jeweils unveraenderte vorhandene Baselines und historische Auswertungen nachweisen. Umsetzung erfordert eine neue Freigabe der tatsaechlich betroffenen Core-Bereiche; diese Planaufnahme ist keine Implementierungsfreigabe. + ### Phase 13D - Referenzerkennung und katalogbasierte Einordnung 2026-09-26 - Phase 13D / gemischte Work-Projektion (2.8.5, umgesetzt und verifiziert): Beim Anhaengen von GPO-Ergebnissen an All Records wird referenceRecognition jetzt vom jeweiligen Scannerbefund uebernommen. Zuvor blieben die Referenzlaeufe gespeichert, gingen aber beim Neuaufbau der Reader-Projektion verloren. Die Uebernahme bewahrt Referenzen, Quellenherkunft, Erkennerversion, Lauf und Katalogversion/-hash einschliesslich bewusst leerer Erkennung; GPOs und nicht indexierte Befunde erhalten keine fremden Referenzen. Synthetische Regression oeffnet Referenzdetails im echten FindingReader mit und ohne GPOs. 30 fokussierte Referenz-/GPO-/CIS-Tests und 9 UI-/API-Versionsvertragstests, beide Produktionsbuilds sowie Design-/Local-only- und Diffpruefung bestanden. Patchversion fuer rueckwaertskompatible Projektionskorrektur; keine Datenmutation und kein Reimport bzw. neuer Erkennungslauf erforderlich. Bereits geladene Workspaces muessen neu geladen werden. Phase 13D bleibt in Arbeit. @@ -2896,6 +3311,9 @@ Festgelegtes Betriebsrisiko vom 2026-09-06: Verzeichnisabhaengige Compose-Projek ## 12. Änderungsprotokoll +2026-10-02 - Nutzerwunsch fuer spaeter dokumentiert: automatische Security-Baseline-Updates als zurueckgestelltes Arbeitspaket in Phase 13B, mit Phase-10-Backend und Phase-7-Anzeige. Datenqualitaet, Herkunft/Version/Format/Vollstaendigkeit, Erhalt der letzten gueltigen Baseline und unveraenderte historische Auswertungen sind Abnahmekriterien; Quellen und Intervall bleiben offen. Ausschliesslich Planaufnahme, keine Implementierung, Automatisierung oder Produktversionsaenderung. + + 2026-10-01 - Phase 13E / Vokabular anhand zweier Result-DB-Exporte des Nutzers erweitert (Engine-Arbeit, per Test verifiziert): Der Nutzer stellte zwei gefilterte Result-DB-Exporte (zusammen 953 Abweichungen) zur Analyse bereit; gelesen wurden Tool, Regel-ID und Regeltext, nichts davon wurde gespeichert oder in Code oder Tests uebernommen. Vorher blieben ueber Katalog und Vokabular 498 offen (GPO 449, Purple Knight 22, PingCastle 15, Maester 12), danach 26 (14, 7, 2, 3; die Simulation beruecksichtigt keine GPO-Katalogregeln, Anker, Aehnlichkeit oder Kategorien, die echte Engine liegt darunter). Klarstellung: `purple-knight-ad`/`-entra` sind nur Bezeichnungen der Result-DB-Projektion; die Engine liest die interne Tool-ID `purple-knight`, die Katalogregeln greifen. Ergaenzt: Kontorichtlinien-Schluessel (Password Guessing), Benutzerrechte `Se...Privilege` (Host Credential), Anmelderechte und DCOM-Gruppe (Lateral Movement), `SeEnableDelegationPrivilege` (Delegation), UAC-Werte, gespeicherte Browser-Passwoerter, Druckertreiber-Installation (Host Credential), LAPS/`AdmPwd`, NTLM-Audit-Werte und Alerts (Detection), Office-Sicherheitspfade, Internet Settings/ZoneMap (Malicious Code), Bildschirmsperre (Physical), AD-Berechtigungsdelegation und Kontrollpfade (AD Privileged), Aenderungsindikatoren (AD Persistence), Seamless SSO, Security Defaults und Modern Authentication (Cloud Sign-in), App-Registrierung (App Consent), Skype-Foederation und Speicheranbieter (External Access), MailTips (E-Mail), Customer Lockbox (Data Sharing), RMAU und SSPR fuer privilegierte Rollen (Cloud Privileged), NTFRS und nicht unterstuetzte Windows-Versionen (Patch Drift); Pfadmuster greifen auch am Schluesselende. Vertrauenswuerdige Standorte nur im Office-Sicherheitspfad, weil Conditional Access den Begriff anders nutzt. Aenderungsindikatoren werden jetzt AD Persistence zugeordnet (vorher bewusst offen). Praezision an allen 332 Katalogtexten 100 %, alle katalogisierten Registry-Ziele widerspruchsfrei. `npm test` gruen (1.774), Typpruefung, beide Produktionsbuilds. Server-Neustart noetig. In 2.10.0 gebuendelt. 2026-10-01 - Phase 13E / Registry-Policy-Bereiche im Vokabular (Engine-Arbeit, per Test verifiziert): Anlass: Eine GPO-Abweichung unter `...\Internet Settings\Zones\2\1201` (Internet-Explorer-Zonenaktion, ActiveX) blieb trotz vollstaendiger Raw Data in `Other Deviations`, weil das Vokabular keine Begriffe fuer diesen Policy-Bereich kannte. Ergaenzt um Registry-Pfadsegmente und Wertnamen der Microsoft-Baselines, die genau einen Angriffsweg benennen: IE-Zonen/ActiveX/URL-Aktionen, Defender-Pfade, Makro-Werte, Attachment Manager, Safer/SrpV2/AppLocker/WDAC (Malicious Code); `WindowsFirewall` (Lateral Movement) und deren `Logging` (Detection); PowerShell-Protokollierung (Detection, ohne ExecutionPolicy); Lanman, Hardened Paths, DNS-Client, Drucker, NetBT (Credential Relay); CredentialsDelegation, DeviceGuard, Installer, LSA-Schutz (Host Credential); DataCollection/CloudContent/Telemetrie (Data Sharing); WindowsUpdate (Patch Drift). Ganze Produktbaeume wie Office oder Edge werden bewusst nicht zugeordnet, weil der Katalog selbst eine Office-Einstellung Host Credential zuordnet. Messung: alle katalogisierten Registry-Ziele ohne Widerspruch (10 von 11 erkannt, die Office-Einstellung bewusst nicht), alle 332 Katalogtexte weiter 100 % praezise, Abdeckung 73,2 %. Dauertests fuer den Zonen-Pfad und den Abgleich mit den katalogisierten Registry-Zielen. `npm test` gruen (1.772), Typpruefung, beide Produktionsbuilds. Server-Neustart noetig. In 2.10.0 gebuendelt. diff --git a/docs/REPORT_STRATEGY.md b/docs/REPORT_STRATEGY.md index e753408..9b6051a 100644 --- a/docs/REPORT_STRATEGY.md +++ b/docs/REPORT_STRATEGY.md @@ -1,5 +1,23 @@ # Assessment output strategy +## Export layout decision 2026-10-02 + +User approval 2026-10-02: this implemented report is the binding template for future Management Reports. Its permanent contract lives inside Report Engine at [MANAGEMENT_REPORT_TEMPLATE.md](../src/reporting/MANAGEMENT_REPORT_TEMPLATE.md). Preserve the approved renderer structure and data boundaries unless explicitly revised by the user; historical HTML exports are not overwritten. + +The user-supplied Huntsman report page is the layout/style reference for the Management Report only: white report sheets, ASSESSOR branding, orange headings, dark table headers, two-column category explanations/evidence, factual bar charts and repeated footers. This explicit export-only decision supersedes previous app-style layout guidance for the Management Report; application views remain unchanged. The report embeds the existing ASSESSOR logo and local Inter with no remote dependency. The opening overview and complete Context heatmap lead into one logical report sheet per populated category, followed by scope and method. Logical sheet numbering does not promise one physical PDF page per sheet. Missing time-series/score/endpoint data is never fabricated to imitate the reference. Original technical evidence and recommendations remain in the separate Technical Report. + +## Management Report with published Context results (2026-10-02) + +Explicit user decision supersedes the earlier Core-only Report A content below: the Management Report requires Context results for the same client, assessments, scan runs and committed repository revision. It is generated offline with fixed German text, no LLM and no Internet access. A missing Context producer or mismatched scope blocks the management run; a changing revision retries the read and ultimately fails rather than combining snapshots. Client Info refuses management generation while Context Engine is off and suppresses late output after it is switched off. Technical generation remains available independently and does not compute Context when invoked alone; its archive contains no management HTML rather than a Core-only fallback. Historical frozen projections retain the historical renderer and are not rewritten. + +Context Engine publishes a versioned result object through `context-report-publication.ts`; Report Engine consumes only its declared `ReportContextPublication` contract. The Core backend composes the two at its existing pinned extension point using the canonical read-only port. Neither engine imports the other. The publication preserves catalog/vocabulary/engine/severity versions, deduplicated finding count, per-cluster scanner/GPO counts, assignment confidence, source provenance and technical Result IDs. It uses the existing Context membership and heatmap semantics, without classifying or scoring again. + +The management file contains: Management Summary; all Context categories in the severity-sorted heatmap with the unique overall Finding count; populated problem fields with general catalog descriptions, evidence counts, confidence and scanner sources; fixed review guidance pointing to original recommendations in Report B; documented risk acceptance and selected work-state counts; comparison results only when supplied by the existing shared projection; environment facts, source coverage and collection times; technical Security Domain counts in the appendix; method and source-version notes. General catalog descriptions are explicitly not evidence of a common attack path. Base Severity is category severity, never a new finding verdict, CVSS, priority, exploitation probability or business loss. Unknown/unexecuted checks are not passed; neutral heatmap cells are not secure; GPO backup configuration is not effective host state. Overlapping cluster counts are never added into a total. Work tracking and risk acceptance do not prove remediation; rescans remain required. + +Management disclosure stays separate: no Result IDs, source Rule IDs, evidence paths, registry keys, technical finding fields or original scanner remediation payloads are hidden in Report A. Report B receives the frozen Context counts and the Result IDs proving them alongside its original detailed evidence and recommendations. Archive projection JSON retains the published results for reproducible replay. Static HTML includes the already bundled local Inter font as embedded data and has no script or external resources. The report export is not part of the client load path. + +Current versions: Report Engine 0.3.0, shared projection 1.3.0, Management renderer 3.0.0; product capability release 2.13.0. No database schema, migration or Core result semantics change. A separate historical Delta Report remains its own artifact; the new management cut does not invent comparison evidence when no delta was supplied. + Decision: 2026-09-18, explicitly approved by the user. The 2026-09-23 user request starts the A/B HTML deliverable through Client Info (Phase 7 UI integration, cross-cutting report projection). Artifact C and D remain separate future work. diff --git a/docs/analysis/context-pass-gaps-dummy-2026-10-04.md b/docs/analysis/context-pass-gaps-dummy-2026-10-04.md new file mode 100644 index 0000000..2dee566 --- /dev/null +++ b/docs/analysis/context-pass-gaps-dummy-2026-10-04.md @@ -0,0 +1,231 @@ +# Context: fehlende Pass-Regeln (Client Dummy) + +Stand: 2026-10-04 · Context Engine 1.10.0 · Katalog 1.9.1 · Scope: alle drei Dummy-Timeslots + +Analyse-Liste, keine Katalogänderung. Aufgeführt ist jede Regel, deren Pass-Ergebnis laut Scan-Bericht in keiner Context-Bedingung vorkommt. Abweichungen sind vollständig katalogisiert; diese Lücke betrifft nur Pass-Belege. PingCastle fehlt, weil der Bericht keine Pass-Ergebnisse enthält. + +Eine Regel gehört erst nach geprüfter Zuordnung zu einem Cluster in den Katalog: exakte Regel-ID mit Polarität `fail-means-met`, bei CIS-CAT der Berichtsabschnitt. Keine Heuristik. + +## Übersicht + +| Quelle | Pass-Regeln gesamt | davon im Context | fehlend | +|---|---|---|---| +| Purple Knight | 96 | 20 | 76 | +| Maester | 74 | 8 | 66 | +| CIS-CAT | 87 | 54 | 33 | +| GPO (Einstellungen) | 25 | 7 | 18 | + +## Purple Knight + +| Regel-ID | Titel | Severity (Quelle) | Kategorie (Quelle) | +|---|---|---|---| +| `SI000013` | Evidence of Mimikatz DCShadow attack | Critical | AD Infrastructure Security | +| `SI000014` | Users and computers with non-default Primary Group IDs | Low | Account Security | +| `SI000015` | Reversible passwords found in GPOs | Critical | Group Policy Security | +| `SI000017` | Kerberos KRBTGT account with old password | Medium | Kerberos Security | +| `SI000018` | Non default value on ms-Mcs-AdmPwd SearchFlags | High | AD Delegation | +| `SI000021` | Non-default principals with DC Sync rights on the domain | Critical | AD Delegation | +| `SI000022` | Risky RODC credential caching | Medium | AD Infrastructure Security | +| `SI000023` | Well-known privileged SIDs in SIDHistory | Critical | AD Infrastructure Security | +| `SI000025` | Privileged objects with unprivileged owners | High | AD Delegation | +| `SI000027` | Users with Kerberos pre-authentication disabled | Medium | Account Security | +| `SI000032` | GPO linking delegation at the domain level | High | Group Policy Security | +| `SI000033` | Recent privileged account creation activity | Low | Account Security | +| `SI000037` | Users with old passwords | Low | Account Security | +| `SI000038` | Enabled admin accounts that are inactive | Medium | Account Security | +| `SI000039` | Privileged users that are disabled | Low | Account Security | +| `SI000040` | GPO linking delegation at the domain controller OU level | High | Group Policy Security | +| `SI000041` | GPO linking delegation at the AD Site level | High | Group Policy Security | +| `SI000042` | Users with SPN defined | Low | Kerberos Security | +| `SI000043` | Changes to privileged group membership in the last 7 days | Medium | Account Security | +| `SI000044` | AD objects created within the last 10 days | Informational | Account Security | +| `SI000046` | Objects in privileged groups without adminCount=1 (SDProp) | Medium | AD Delegation | +| `SI000047` | Changes to default security descriptor schema in the last 90 days | High | AD Delegation | +| `SI000048` | Changes to Default Domain Policy or Default Domain Controllers Policy in the last 7 days | Medium | Group Policy Security | +| `SI000049` | Built-in domain Administrator account with old password (180 days) | Medium | Account Security | +| `SI000050` | Domain Controller owner is not an administrator | High | AD Delegation | +| `SI000051` | Domains with obsolete functional levels | Low | AD Infrastructure Security | +| `SI000053` | Operator groups no longer protected by AdminSDHolder and SDProp | Medium | AD Infrastructure Security | +| `SI000054` | Computers with older OS versions | Medium | AD Infrastructure Security | +| `SI000056` | Enterprise Key Admins with full access to domain | High | AD Delegation | +| `SI000057` | Objects with constrained delegation configured | Medium | Kerberos Security | +| `SI000058` | Kerberos protocol transition delegation configured | High | Kerberos Security | +| `SI000059` | Computers with password last set over 90 days ago | High | AD Infrastructure Security | +| `SI000060` | Recent SIDHistory changes on objects | Medium | Account Security | +| `SI000061` | Computer Accounts in Privileged Groups | High | Account Security | +| `SI000062` | Admins with old passwords | Low | Account Security | +| `SI000063` | Domain Controllers with old passwords | Low | AD Infrastructure Security | +| `SI000064` | Forest contains more than 50 privileged accounts | Medium | Account Security | +| `SI000068` | Trust accounts with old passwords | Low | Account Security | +| `SI000069` | Domain Controllers in inconsistent state | Low | AD Infrastructure Security | +| `SI000072` | gMSA objects with old passwords | High | AD Infrastructure Security | +| `SI000073` | Domain Controllers that have not authenticated to the domain for more than 45 days | Medium | AD Infrastructure Security | +| `SI000074` | User accounts with password not required | High | Account Security | +| `SI000075` | User accounts that use DES encryption | Medium | Account Security | +| `SI000076` | User accounts that store passwords with reversible encryption | Medium | Account Security | +| `SI000078` | Users and computers without readable PGID | Medium | Account Security | +| `SI000081` | Non-standard schema permissions | High | AD Infrastructure Security | +| `SI000082` | Changes to AD Display Specifiers in the past 90 days | Low | AD Delegation | +| `SI000083` | Non-privileged users with access to gMSA passwords | High | AD Delegation | +| `SI000086` | Weak certificate cipher | Critical | AD Infrastructure Security | +| `SI000087` | Domain trust to a third-party domain without quarantine | Medium | AD Infrastructure Security | +| `SI000088` | Outbound forest trust with SID History enabled | High | AD Infrastructure Security | +| `SI000089` | Users with permissions to set Server Trust Account | Critical | AD Delegation | +| `SI000092` | Non-default access to DPAPI key | High | AD Delegation | +| `SI000097` | NTFRS SYSVOL Replication | High | AD Infrastructure Security | +| `SI000098` | Abnormal Password Refresh | Medium | Account Security | +| `SI000100` | Accounts with Constrained Delegation configured to ghost SPN | High | Kerberos Security | +| `SI000101` | SYSVOL Executable Changes | Low | Group Policy Security | +| `SI000102` | Accounts with altSecurityIdentities configured | High | Kerberos Security | +| `SI000103` | FGPP not applied to Global group | Medium | Account Security | +| `SI000106` | Inheritance enabled on AdminSDHolder object | Critical | AD Delegation | +| `SI000108` | Dangerous Trust Attribute Set | High | AD Infrastructure Security | +| `SI000110` | Ephemeral Admins | Low | Account Security | +| `SI000124` | Foreign Security Principals in Privileged Group | Medium | AD Delegation | +| `SI000138` | Privileged user credentials cached on RODC | Medium | AD Infrastructure Security | +| `SI000150` | Operators Groups that are not empty | High | Account Security | +| `SI000185` | Objects with Reanimate-Tombstones extended right | Medium | AD Delegation | +| `SI000188` | GPO Weak LM Hash storage enabled | High | Group Policy Security | +| `SI000189` | Writable shortcuts found in GPO | High | Group Policy Security | +| `SI000193` | OU permissions enabling BadSuccessor dMSA escalation | High | Account Security | +| `SI000217` | Query policies that have the attribute of LDAP deny list set | Medium | AD Infrastructure Security | +| `SI000236` | User accounts using Smart Card authentication with old password | High | Account Security | +| `SI000304` | Dangerous GPO logon script path | High | Group Policy Security | +| `SI000314` | Unexpected accounts in Cert Publishers Group | High | AD Infrastructure Security | +| `SI000316` | GPO with Scheduled Tasks configured | Low | Group Policy Security | +| `SI000339` | Users with the attribute userPassword set | Medium | Kerberos Security | +| `SI000355` | Distributed COM Users group or Performance Log Users group are not empty | High | Account Security | + +## Maester + +| Regel-ID | Titel | Severity (Quelle) | Kategorie (Quelle) | +|---|---|---|---| +| `CIS.M365.1.3.1` | (L1) Ensure the 'Password expiration policy' is set to 'Set passwords to never expire (recommended)' | High | ["CIS.M365.1.3.1","L1","CIS E3 Level 1","CIS E3","CIS","Security","All","CIS M365 v4.0.0"], CIS, CIS.M365.1.3.1, L1, CIS E3 Level 1, CIS E3, CIS, Security, CIS M365 v5.0.0 | +| `CISA.MS.AAD.2.1` | Users detected as high risk SHALL be blocked. | High | ["MS.AAD","MS.AAD.2.1","CISA.MS.AAD.2.1","CISA","Security","All","Entra ID P2"], CISA, MS.AAD, MS.AAD.2.1, CISA.MS.AAD.2.1, CISA, Security, Entra ID P2 | +| `CISA.MS.AAD.2.2` | A notification SHOULD be sent to the administrator when high-risk users are detected. | High | ["MS.AAD","MS.AAD.2.2","CISA.MS.AAD.2.2","CISA","Security","All","Entra ID P2"], CISA, MS.AAD, MS.AAD.2.2, CISA.MS.AAD.2.2, CISA, Security, Entra ID P2 | +| `CISA.MS.AAD.3.2` | If phishing-resistant MFA has not been enforced, an alternative MFA method SHALL be enforced for all users. | High | ["MS.AAD","MS.AAD.3.2","CISA.MS.AAD.3.2","CISA","Security","All","Entra ID P1"], CISA, MS.AAD, MS.AAD.3.2, CISA.MS.AAD.3.2, CISA, Security, Entra ID P1 | +| `CISA.MS.AAD.3.4` | The Authentication Methods Manage Migration feature SHALL be set to Migration Complete. | High | ["MS.AAD","MS.AAD.3.4","CISA.MS.AAD.3.4","CISA","Security","All","Entra ID P1"], CISA, MS.AAD, MS.AAD.3.4, CISA.MS.AAD.3.4, CISA, Security, Entra ID P1 | +| `CISA.MS.AAD.5.1` | Only administrators SHALL be allowed to register applications. | High | ["MS.AAD","MS.AAD.5.1","CISA.MS.AAD.5.1","CISA","Security","All","Entra ID Free"], CISA, MS.AAD, MS.AAD.5.1, CISA.MS.AAD.5.1, CISA, Security, Entra ID Free | +| `CISA.MS.AAD.6.1` | User passwords SHALL NOT expire. | High | ["MS.AAD","MS.AAD.6.1","CISA.MS.AAD.6.1","CISA","Security","All","Entra ID Free"], CISA, MS.AAD, MS.AAD.6.1, CISA.MS.AAD.6.1, CISA, Security, Entra ID Free | +| `CISA.MS.AAD.7.5` | Provisioning users to highly privileged roles SHALL NOT occur outside of a PAM system. | High | ["MS.AAD","MS.AAD.7.5","CISA.MS.AAD.7.5","CISA","Security","All","Entra ID P2"], CISA, MS.AAD, MS.AAD.7.5, CISA.MS.AAD.7.5, CISA, Security, Entra ID P2 | +| `CISA.MS.AAD.7.6` | Activation of the Global Administrator role SHALL require approval. | High | ["MS.AAD","MS.AAD.7.6","CISA.MS.AAD.7.6","CISA","Security","All","Entra ID P2"], CISA, MS.AAD, MS.AAD.7.6, CISA.MS.AAD.7.6, CISA, Security, Entra ID P2 | +| `CISA.MS.AAD.7.8` | User activation of the Global Administrator role SHALL trigger an alert. | High | ["MS.AAD","MS.AAD.7.8","CISA.MS.AAD.7.8","CISA","Security","All","Entra ID P2"], CISA, MS.AAD, MS.AAD.7.8, CISA.MS.AAD.7.8, CISA, Security, Entra ID P2 | +| `CT001` | User device quota should be set to 10 | — | Custom Security Tests, Custom, Security, Device | +| `EIDSCA.AF01` | Authentication Method - FIDO2 security key - State. | High | ["EIDSCA","Security","All","EIDSCA.AF01"], EIDSCA, EIDSCA, Security, EIDSCA.AF01 | +| `EIDSCA.AF02` | Authentication Method - FIDO2 security key - Allow self-service set up. | Medium | ["EIDSCA","Security","All","EIDSCA.AF02"], EIDSCA, EIDSCA, Security, EIDSCA.AF02 | +| `EIDSCA.AG01` | Authentication Method - General Settings - Manage migration. | High | ["EIDSCA","Security","All","EIDSCA.AG01"], EIDSCA, EIDSCA, Security, EIDSCA.AG01 | +| `EIDSCA.AG03` | Authentication Method - General Settings - Report suspicious activity - Included users/groups. | Medium | ["EIDSCA","Security","All","EIDSCA.AG03"], EIDSCA, EIDSCA, Security, EIDSCA.AG03 | +| `EIDSCA.AM01` | Authentication Method - Microsoft Authenticator - State. | High | ["EIDSCA","Security","All","EIDSCA.AM01"], EIDSCA, EIDSCA, Security, EIDSCA.AM01 | +| `EIDSCA.AM03` | Authentication Method - Microsoft Authenticator - Require number matching for push notifications. | Medium | ["EIDSCA","Security","All","EIDSCA.AM03"], EIDSCA, EIDSCA, Security, EIDSCA.AM03 | +| `EIDSCA.AM04` | Authentication Method - Microsoft Authenticator - Included users/groups of number matching for push notifications. | Medium | ["EIDSCA","Security","All","EIDSCA.AM04"], EIDSCA, EIDSCA, Security, EIDSCA.AM04 | +| `EIDSCA.AM06` | Authentication Method - Microsoft Authenticator - Show application name in push and passwordless notifications. | Medium | ["EIDSCA","Security","All","EIDSCA.AM06"], EIDSCA, EIDSCA, Security, EIDSCA.AM06 | +| `EIDSCA.AM07` | Authentication Method - Microsoft Authenticator - Included users/groups to show application name in push and passwordless notifications. | Medium | ["EIDSCA","Security","All","EIDSCA.AM07"], EIDSCA, EIDSCA, Security, EIDSCA.AM07 | +| `EIDSCA.AM09` | Authentication Method - Microsoft Authenticator - Show geographic location in push and passwordless notifications. | Medium | ["EIDSCA","Security","All","EIDSCA.AM09"], EIDSCA, EIDSCA, Security, EIDSCA.AM09 | +| `EIDSCA.AM10` | Authentication Method - Microsoft Authenticator - Included users/groups to show geographic location in push and passwordless notifications. | Medium | ["EIDSCA","Security","All","EIDSCA.AM10"], EIDSCA, EIDSCA, Security, EIDSCA.AM10 | +| `EIDSCA.AP01` | Default Authorization Settings - Enabled Self service password reset for administrators. | High | ["EIDSCA","Security","All","EIDSCA.AP01"], EIDSCA, EIDSCA, Security, EIDSCA.AP01 | +| `EIDSCA.AP08` | Default Authorization Settings - User consent policy assigned for applications. | Medium | ["EIDSCA","Security","All","EIDSCA.AP08"], EIDSCA, EIDSCA, Security, EIDSCA.AP08 | +| `EIDSCA.AP10` | Default Authorization Settings - Default User Role Permissions - Allowed to create Apps. | High | ["EIDSCA","Security","All","EIDSCA.AP10"], EIDSCA, EIDSCA, Security, EIDSCA.AP10 | +| `EIDSCA.AP14` | Default Authorization Settings - Default User Role Permissions - Allowed to read other users. | High | ["EIDSCA","Security","All","EIDSCA.AP14"], EIDSCA, EIDSCA, Security, EIDSCA.AP14 | +| `EIDSCA.AS04` | Authentication Method - SMS - Use for sign-in. | High | ["EIDSCA","Security","All","EIDSCA.AS04"], EIDSCA, EIDSCA, Security, EIDSCA.AS04 | +| `EIDSCA.AT01` | Authentication Method - Temporary Access Pass - State. | High | ["EIDSCA","Security","All","EIDSCA.AT01"], EIDSCA, EIDSCA, Security, EIDSCA.AT01 | +| `EIDSCA.AT02` | Authentication Method - Temporary Access Pass - One-time. | High | ["EIDSCA","Security","All","EIDSCA.AT02"], EIDSCA, EIDSCA, Security, EIDSCA.AT02 | +| `EIDSCA.AV01` | Authentication Method - Voice call - State. | High | ["EIDSCA","Security","All","EIDSCA.AV01"], EIDSCA, EIDSCA, Security, EIDSCA.AV01 | +| `EIDSCA.PR02` | Default Settings - Password Rule Settings - Password Protection - Enable password protection on Windows Server Active Directory. | High | ["EIDSCA","Security","All","EIDSCA.PR02"], EIDSCA, EIDSCA, Security, EIDSCA.PR02 | +| `EIDSCA.ST08` | Default Settings - Classification and M365 Groups - M365 groups - Allow Guests to become Group Owner. | Medium | ["EIDSCA","Security","All","EIDSCA.ST08"], EIDSCA, EIDSCA, Security, EIDSCA.ST08 | +| `EIDSCA.ST09` | Default Settings - Classification and M365 Groups - M365 groups - Allow Guests to have access to groups content. | Medium | ["EIDSCA","Security","All","EIDSCA.ST09"], EIDSCA, EIDSCA, Security, EIDSCA.ST09 | +| `MT.1001` | At least one Conditional Access policy is configured with device compliance. | Medium | ["Maester","CA","Security","All","MT.1001"], Maester/Entra, Maester, CA, Security, MT.1001 | +| `MT.1002` | App management restrictions on applications and service principals is configured and enabled. | High | ["Maester","App","Security","All","MT.1002"], Maester/Entra, Maester, App, Security, MT.1002 | +| `MT.1003` | At least one Conditional Access policy is configured with All Apps. | High | ["Maester","CA","Security","All","MT.1003"], Maester/Entra, Maester, CA, Security, MT.1003 | +| `MT.1004` | At least one Conditional Access policy is configured with All Apps and All Users. | High | ["Maester","CA","Security","All","MT.1004"], Maester/Entra, Maester, CA, Security, MT.1004 | +| `MT.1006` | At least one Conditional Access policy is configured to require MFA for admins. | High | ["Maester","CA","Security","All","MT.1006"], Maester/Entra, Maester, CA, Security, MT.1006 | +| `MT.1007` | At least one Conditional Access policy is configured to require MFA for all users. | High | ["Maester","CA","Security","All","MT.1007"], Maester/Entra, Maester, CA, Security, MT.1007 | +| `MT.1008` | At least one Conditional Access policy is configured to require MFA for Azure management. | High | ["Maester","CA","Security","All","MT.1008"], Maester/Entra, Maester, CA, Security, MT.1008 | +| `MT.1013` | At least one Conditional Access policy is configured to require new password when user risk is high. | High | ["Maester","CA","Security","All","MT.1013"], Maester/Entra, Maester, CA, Security, MT.1013 | +| `MT.1016` | At least one Conditional Access policy is configured to require MFA for guest access. | High | ["Maester","CA","Security","All","MT.1016"], Maester/Entra, Maester, CA, Security, MT.1016 | +| `MT.1022` | All users utilizing a P1 license should be licensed. | Medium | ["Entra","License","MT.1022"], Maester/Entra, Entra, License, MT.1022 | +| `MT.1023` | All users utilizing a P2 license should be licensed. | Medium | ["Entra","License","MT.1023"], Maester/Entra, Entra, License, MT.1023 | +| `MT.1024.oneAdmin` | Designate more than one global admin. | Low | Maester/Entra, Maester, Entra, Security, Recommendation, MT.1024, oneAdmin | +| `MT.1024.pwagePolicyNew` | Do not expire passwords. | High | Maester/Entra, Maester, Entra, Security, Recommendation, MT.1024, pwagePolicyNew | +| `MT.1024.roleOverlap` | Use least privileged administrative roles . | Low | Maester/Entra, Maester, Entra, Security, Recommendation, MT.1024, roleOverlap | +| `MT.1024.selfServicePasswordReset` | Enable self-service password reset. | Low | Maester/Entra, Maester, Entra, Security, Recommendation, MT.1024, selfServicePasswordReset | +| `MT.1024.servicePrincipalKeyExpiry` | Renew expiring service principal credentials. | High | Maester/Entra, Maester, Entra, Security, Recommendation, MT.1024, servicePrincipalKeyExpiry | +| `MT.1026` | No hybrid user with permanent role assignment on Control Plane. | High | ["Maester","Privileged","Security","All","MT.1026"], Maester/Entra, Maester, Privileged, Security, MT.1026 | +| `MT.1029` | Stale accounts are not assigned to privileged roles. | High | ["Privileged","Security","All","PIM","MT.1029"], Maester/Entra, Privileged, Security, PIM, MT.1029 | +| `MT.1031` | Privileged role on Control Plane are managed by PIM only. | High | ["Privileged","Security","All","PIM","MT.1031"], Maester/Entra, Privileged, Security, PIM, MT.1031 | +| `MT.1035` | All security groups assigned to Conditional Access Policies should be protected by RMAU. | High | ["Maester","CA","Security","All","MT.1035"], Maester/Entra, Maester, CA, Security, MT.1035 | +| `MT.1049` | Conditional Access policies for User Risk and Sign-in Risk should be configured separately. | High | ["Maester","CA","Security","All","MT.1049"], Maester/Entra, Maester, CA, Security, MT.1049 | +| `MT.1052` | At least one Conditional Access policy is targeting the Device Code authentication flow. | High | ["Maester","CA","Security","All","MT.1052"], Maester/Entra, Maester, CA, Security, MT.1052 | +| `MT.1055` | Microsoft 365 Group (and Team) creation should be restricted to approved users. | Medium | ["Maester","Group","Security","All","MT.1055"], Maester/Entra, Maester, Group, Security, MT.1055 | +| `MT.1059.d41d8cd98f00b204e9800998ecf8427e` | MDI Health Issues - . | — | Defender for Identity health issues, Maester, Defender, Security, MDI, MT.1059, Severity:, | +| `MT.1061` | Device registration MFA control conflicts with Conditional Access policies. | Medium | Maester/Entra, Maester, CA, Security, MT.1061 | +| `MT.1065` | Ensure all Recovery Services Vaults have soft delete enabled | High | AzureConfig, Backup, Azure, MT.1065 | +| `MT.1067` | Authentication method policies should not reference non-existent groups. | Medium | Maester/Entra, Maester, Authentication, Security, MT.1067 | +| `MT.1068` | Restrict non-admin users from creating tenants. | Medium | Maester/Entra, Governance, Entra, Security, MT.1068 | +| `MT.1069` | Restrict non-admin users from creating security groups. | Low | Maester/Entra, Governance, Entra, Security, MT.1069 | +| `MT.1070` | Restrict device join to selected users/groups or none. | Medium | Maester/Entra, Governance, Entra, Security, MT.1070 | +| `MT.1072` | Conditional access policies should not use the deprecated Approved Client App grant. | High | Maester/Entra, Maester, CA, Security, MT.1072 | +| `MT.1074` | Ensure no more then 100 outbound mails per day are send using the .onmicrosoft.com domain | Medium | Maester/Exchange, Maester, Exchange, MT.1074 | +| `MT.1085` | Pending approvals for Critical Asset Management should not be present. | Medium | Exposure Management, Entra, Graph, Security, XSPM, MT.1085 | + +## CIS-CAT + +Abschnitt = erste zwei Ebenen der Regelnummer. Der Context ordnet CIS-CAT über den Berichtsabschnitt zu; ein ganzer nicht katalogisierter Abschnitt kann daher mit einem Eintrag geschlossen werden. + +| Regel-ID | Abschnitt | Titel | +|---|---|---| +| `1.1.1` | 1.1 | 1.1.1 Ensure 'Enforce password history' is set to '24 or more password(s)' | +| `1.1.2` | 1.1 | 1.1.2 Ensure 'Maximum password age' is set to '365 or fewer days, but not 0' | +| `1.1.3` | 1.1 | 1.1.3 Ensure 'Minimum password age' is set to '1 or more day(s)' | +| `1.1.7` | 1.1 | 1.1.7 Ensure 'Store passwords using reversible encryption' is set to 'Disabled' | +| `2.3.4.1` | 2.3 | 2.3.4.1 Ensure 'Devices: Prevent users from installing printer drivers' is set to 'Enabled' | +| `2.3.6.1` | 2.3 | 2.3.6.1 Ensure 'Domain member: Digitally encrypt or sign secure channel data (always)' is set to 'Enabled' | +| `2.3.6.2` | 2.3 | 2.3.6.2 Ensure 'Domain member: Digitally encrypt secure channel data (when possible)' is set to 'Enabled' | +| `2.3.6.3` | 2.3 | 2.3.6.3 Ensure 'Domain member: Digitally sign secure channel data (when possible)' is set to 'Enabled' | +| `2.3.6.4` | 2.3 | 2.3.6.4 Ensure 'Domain member: Disable machine account password changes' is set to 'Disabled' | +| `2.3.6.5` | 2.3 | 2.3.6.5 Ensure 'Domain member: Maximum machine account password age' is set to '30 or fewer days, but not 0' | +| `2.3.6.6` | 2.3 | 2.3.6.6 Ensure 'Domain member: Require strong (Windows 2000 or later) session key' is set to 'Enabled' | +| `2.3.8.2` | 2.3 | 2.3.8.2 Ensure 'Microsoft network client: Send unencrypted password to third-party SMB servers' is set to 'Disabled' | +| `2.3.9.1` | 2.3 | 2.3.9.1 Ensure 'Microsoft network server: Amount of idle time required before suspending session' is set to '15 or fewer minute(s)' | +| `2.3.9.3` | 2.3 | 2.3.9.3 Ensure 'Microsoft network server: Disconnect clients when logon hours expire' is set to 'Enabled' | +| `2.3.10.1` | 2.3 | 2.3.10.1 Ensure 'Network access: Allow anonymous SID/Name translation' is set to 'Disabled' | +| `2.3.10.5` | 2.3 | 2.3.10.5 Ensure 'Network access: Let Everyone permissions apply to anonymous users' is set to 'Disabled' | +| `2.3.10.8` | 2.3 | 2.3.10.8 Ensure 'Network access: Remotely accessible registry paths' is configured | +| `2.3.10.9` | 2.3 | 2.3.10.9 Ensure 'Network access: Remotely accessible registry paths and sub-paths' is configured | +| `2.3.10.10` | 2.3 | 2.3.10.10 Ensure 'Network access: Restrict anonymous access to Named Pipes and Shares' is set to 'Enabled' | +| `2.3.10.12` | 2.3 | 2.3.10.12 Ensure 'Network access: Shares that can be accessed anonymously' is set to 'None' | +| `2.3.10.13` | 2.3 | 2.3.10.13 Ensure 'Network access: Sharing and security model for local accounts' is set to 'Classic - local users authenticate as themselves' | +| `2.3.13.1` | 2.3 | 2.3.13.1 Ensure 'Shutdown: Allow system to be shut down without having to log on' is set to 'Disabled' | +| `2.3.15.1` | 2.3 | 2.3.15.1 Ensure 'System objects: Require case insensitivity for non-Windows subsystems' is set to 'Enabled' | +| `2.3.15.2` | 2.3 | 2.3.15.2 Ensure 'System objects: Strengthen default permissions of internal system objects (e.g. Symbolic Links)' is set to 'Enabled' | +| `2.3.17.4` | 2.3 | 2.3.17.4 Ensure 'User Account Control: Detect application installations and prompt for elevation' is set to 'Enabled' | +| `2.3.17.5` | 2.3 | 2.3.17.5 Ensure 'User Account Control: Only elevate UIAccess applications that are installed in secure locations' is set to 'Enabled' | +| `2.3.17.6` | 2.3 | 2.3.17.6 Ensure 'User Account Control: Run all administrators in Admin Approval Mode' is set to 'Enabled' | +| `2.3.17.7` | 2.3 | 2.3.17.7 Ensure 'User Account Control: Switch to the secure desktop when prompting for elevation' is set to 'Enabled' | +| `2.3.17.8` | 2.3 | 2.3.17.8 Ensure 'User Account Control: Virtualize file and registry write failures to per-user locations' is set to 'Enabled' | +| `19.7.5.1` | 19.7 | 19.7.5.1 Ensure 'Do not preserve zone information in file attachments' is set to 'Disabled' | +| `19.7.5.2` | 19.7 | 19.7.5.2 Ensure 'Notify antivirus programs when opening attachments' is set to 'Enabled' | +| `19.7.26.1` | 19.7 | 19.7.26.1 Ensure 'Prevent users from sharing files within their profile.' is set to 'Enabled' | +| `19.7.46.2.1` | 19.7 | 19.7.46.2.1 Ensure 'Prevent Codec Download' is set to 'Enabled' | + +## GPO-Einstellungen (Verdict Passed) + +| Einstellung | Scope | Technische Identität | +|---|---|---| +| allowdde | user | `gpo-policy-identity-fe0f949d14c555c06980c7f1be88ecab` | +| enableoleactions | user | `gpo-policy-identity-de40f1ecce2e45b1899716ec95948cb6` | +| EnableScriptBlockInvocationLogging | computer | `gpo-policy-identity-7b0456c1b66a87c2bb97a99315b119d4` | +| fAllowFullControl | computer | `gpo-policy-identity-6d0463b00d14f0674cebe9ed81bfca67` | +| fUseMailto | computer | `gpo-policy-identity-20a81de462534bd60111802c0ecd1ec6` | +| MaxTicketExpiry | computer | `gpo-policy-identity-8590d6843170a869ba261df8ce43b34e` | +| MaxTicketExpiryUnits | computer | `gpo-policy-identity-db2aa9231047c05e8914b11c1bbc1e18` | +| privilege rights · secreatepagefileprivilege | computer | `gpo-policy-identity-b6237595766e0910af2c9b5246f1c6ef` | +| privilege rights · seprofilesingleprocessprivilege | computer | `gpo-policy-identity-b79dd2a5cbf48fadac6a853bfa1588ce` | +| registry values · machine\system\currentcontrolset\control\lsa\nolmhash | computer | `gpo-policy-identity-8750b6186583a650aedbd8cde05b5dce` | +| registry values · machine\system\currentcontrolset\services\lanmanserver\parameters\enablesecuritysignature | computer | `gpo-policy-identity-07391ebdcd8b74f59c174e0dcf465774` | +| registry values · machine\system\currentcontrolset\services\lanmanserver\parameters\requiresecuritysignature | computer | `gpo-policy-identity-40df1cce2a18ab54de584d997a763dce` | +| registry values · machine\system\currentcontrolset\services\netlogon\parameters\requiresignorseal | computer | `gpo-policy-identity-68e6d42ea8fc7b9b8de723f2cba019ef` | +| registry values · machine\system\currentcontrolset\services\ntds\parameters\ldapserverintegrity | computer | `gpo-policy-identity-69964f975c2afa036c53821d833ac388` | +| Software\Policies\Microsoft\Edge\ExtensionInstallBlocklist | computer | `gpo-policy-identity-9c404b72d73c89d991f6bd879de714b2` | +| SOFTWARE\Policies\Microsoft\Windows\DeviceInstall\Restrictions\DenyDeviceClasses | computer | `gpo-policy-identity-9c23ac1bc5d4049c4460cfac648236af` | +| SOFTWARE\Policies\Microsoft\Windows\DeviceInstall\Restrictions\DenyDeviceIDs | computer | `gpo-policy-identity-71d55b57e12919c3eaf1ceb41f73f18d` | +| system access · lsaanonymousnamelookup | computer | `gpo-policy-identity-f9b99fd736dfaef934d63699413dc71f` | diff --git a/package-lock.json b/package-lock.json index 162f2d7..a8ba42a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "assessor", - "version": "2.11.2", + "version": "2.16.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "assessor", - "version": "2.11.2", + "version": "2.16.5", "dependencies": { "@fontsource-variable/inter": "^5.3.0", "jsdom": "^29.1.1", @@ -41,7 +41,7 @@ "@csstools/css-tokenizer": "^4.0.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^20.19.0 || ^22.12.2 || >=24.0.0" } }, "node_modules/@asamuzakjp/dom-selector": { @@ -57,7 +57,7 @@ "is-potential-custom-element-name": "^1.0.1" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^20.19.0 || ^22.12.2 || >=24.0.0" } }, "node_modules/@asamuzakjp/generational-cache": { @@ -66,7 +66,7 @@ "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", "license": "MIT", "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^20.19.0 || ^22.12.2 || >=24.0.0" } }, "node_modules/@asamuzakjp/nwsapi": { @@ -951,7 +951,7 @@ "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", "license": "MIT", "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^20.19.0 || ^22.12.2 || >=24.0.0" }, "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" @@ -1571,7 +1571,7 @@ "react-refresh": "^0.18.0" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^20.19.0 || >=22.12.2" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" @@ -1896,7 +1896,7 @@ "whatwg-url": "^16.0.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^20.19.0 || ^22.12.2 || >=24.0.0" } }, "node_modules/debug": { @@ -2092,7 +2092,7 @@ "@exodus/bytes": "^1.6.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^20.19.0 || ^22.12.2 || >=24.0.0" } }, "node_modules/is-potential-custom-element-name": { @@ -2137,7 +2137,7 @@ "xml-name-validator": "^5.0.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + "node": "^20.19.0 || ^22.13.5 || >=24.0.0" }, "peerDependencies": { "canvas": "^3.0.0" @@ -2839,7 +2839,7 @@ "vite": "bin/vite.js" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^20.19.0 || >=22.12.2" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -2848,7 +2848,7 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", + "@types/node": "^20.19.0 || >=22.12.2", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", @@ -3033,7 +3033,7 @@ "webidl-conversions": "^8.0.1" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^20.19.0 || ^22.12.2 || >=24.0.0" } }, "node_modules/why-is-node-running": { diff --git a/package.json b/package.json index 9ac5704..a8c3bf5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "assessor", - "version": "2.11.2", + "version": "2.16.5", "private": true, "type": "module", "scripts": { diff --git a/server/assessor-http-server.ts b/server/assessor-http-server.ts index b7e9611..2d8c146 100644 --- a/server/assessor-http-server.ts +++ b/server/assessor-http-server.ts @@ -14,6 +14,8 @@ import type { } from '../src/persistence/assessor-repository'; import type { UnifiedResultQuery } from '../src/domain/unified-result'; import type { EnvironmentSnapshotQuery } from '../src/domain/environment-context'; +import type { ClientInformationQuery } from '../src/domain/client-information'; +import { clientInformationReadPortFrom } from '../src/persistence/client-information-read-port'; import type { Assessment, AssessmentProject, FindingReview, ImportFileRecord } from '../src/domain/canonical-model'; import type { LocalComparisonRule } from '../src/domain/comparison'; import { ImportError } from '../src/shared/errors'; @@ -40,6 +42,8 @@ import { assembleConsistentReportInput, createReportRun, ReportStateChangedError import { reportReadPortFrom, type ReportReadPort } from '../src/reporting/report-read-port'; import type { FileSystemReportArchive } from './reporting/report-archive'; import { ContextStateChangedError, createAssessmentContext } from '../src/assessment-context/context-run'; +import { publishContextReport } from '../src/assessment-context/context-report-publication'; +import { ReportContextRequiredError } from '../src/reporting/report-context'; import type { AssessmentContext } from '../src/assessment-context/context-engine'; import { loadCanonicalProjectWorkspace } from '../src/features/project-import/load-canonical-project-workspace'; import { summarizeWorkspaceResults, type DatahubTimeslotResultMetrics } from '../src/features/project-import/datahub-scan-analysis'; @@ -363,6 +367,7 @@ function statusForError(error: unknown): { status: number; code: string; message if (error instanceof ImportError) return { status: 422, code: error.diagnostics[0]?.code ?? 'import_rejected', message: error.message }; if (error instanceof RepositoryNotFoundError) return { status: 404, code: 'repository_not_found', message: error.message }; if (error instanceof ReportStateChangedError) return { status: 409, code: 'report_state_changed', message: error.message }; + if (error instanceof ReportContextRequiredError) return { status: 409, code: 'report_context_required', message: error.message }; if (error instanceof ContextStateChangedError) return { status: 409, code: 'context_state_changed', message: error.message }; if (error instanceof RepositoryConflictError || error instanceof ImportSessionStateError) return { status: 409, code: 'repository_conflict', message: error.message }; if (error instanceof RepositoryValidationError) return { status: 400, code: 'repository_validation', message: error.message }; @@ -395,6 +400,8 @@ async function serveStatic(request: IncomingMessage, response: ServerResponse, s export function createAssessorHttpServer(options: AssessorHttpServerOptions): Server { const repository = options.repository; + const clientInformation = clientInformationReadPortFrom(repository, + () => repository.getCommittedRevision?.() ?? Promise.resolve(repository.revision)); const reportPort = options.reportPort === undefined ? reportReadPortFrom(repository, async () => repository.revision) : options.reportPort; const requireReportPort = async (projectId: string): Promise => { @@ -927,6 +934,29 @@ export function createAssessorHttpServer(options: AssessorHttpServerOptions): Se scanRuns: await repository.listProjectScanRuns(segments[1]) }, requestId); return; } + if (segments[0] === 'projects' && segments[2] === 'client-information' && segments.length === 3 && method === 'GET') { + const params = url.searchParams; + const allowed = new Set(['assessmentId', 'scanRunIds', 'includeSuperseded', 'areas', 'properties', 'subjectTypes', 'subjectIds', 'toolIds', 'states', 'search', 'offset', 'limit', 'sectionOffset', 'sectionLimit']); + if ([...params.keys()].some(key => !allowed.has(key))) throw new RepositoryValidationError('Unknown Client Information query parameter.'); + const scalarKeys = ['assessmentId', 'includeSuperseded', 'search', 'offset', 'limit', 'sectionOffset', 'sectionLimit']; + if (scalarKeys.some(key => params.getAll(key).length > 1)) throw new RepositoryValidationError('Repeated scalar Client Information parameter.'); + const history = params.get('includeSuperseded'); + if (history !== null && history !== 'true' && history !== 'false') throw new RepositoryValidationError('Invalid history selector.'); + const numeric = (key: string) => { + const value = params.get(key); + if (value !== null && !/^\d+$/.test(value)) throw new RepositoryValidationError('Invalid Client Information pagination.'); + return value === null ? undefined : Number(value); + }; + const query: ClientInformationQuery = { + projectId: segments[1], assessmentId: params.get('assessmentId') ?? undefined, + scanRunIds: params.getAll('scanRunIds'), includeSuperseded: history === 'true', + areas: params.getAll('areas') as ClientInformationQuery['areas'], properties: params.getAll('properties'), + subjectTypes: params.getAll('subjectTypes'), subjectIds: params.getAll('subjectIds'), toolIds: params.getAll('toolIds'), + states: params.getAll('states') as ClientInformationQuery['states'], search: params.get('search') ?? undefined, + offset: numeric('offset'), limit: numeric('limit'), sectionOffset: numeric('sectionOffset'), sectionLimit: numeric('sectionLimit') + }; + status = 200; sendJson(response, status, await clientInformation.queryClientInformation(query), requestId); return; + } if (segments[0] === 'projects' && segments[2] === 'environment-snapshot' && segments.length === 3 && method === 'GET') { const snapshotQuery: EnvironmentSnapshotQuery = { projectId: segments[1], @@ -991,10 +1021,25 @@ export function createAssessorHttpServer(options: AssessorHttpServerOptions): Se ? value as string[] : undefined; if (!options.reportArchive) throw new HttpError(503, 'report_archive_unavailable', 'Das Report-Archiv ist nicht konfiguriert.'); const startedAt = now(); - const run = await createReportRun(await requireReportPort(segments[1]), { + const port = await requireReportPort(segments[1]); + const run = await createReportRun(port, { projectId: segments[1], generatedAt: startedAt.toISOString(), assessmentIds: ids(body.assessmentIds), scanRunIds: ids(body.scanRunIds) - }); + }, body.kind === 'management' ? { + requireContext: true, + readContext: async scope => { + const assessmentIds = [...new Set(scope.assessmentIds)].sort(); + const scanRunIds = [...new Set(scope.scanRunIds ?? [])].sort(); + const key = JSON.stringify([await port.currentRevision(), scope.projectId, assessmentIds, scanRunIds]); + let context = assessmentContextCache.get(key); + if (!context) { + context = await createAssessmentContext(port, { ...scope, assessmentIds, scanRunIds }); + assessmentContextCache.set(JSON.stringify([context.repositoryRevision, scope.projectId, assessmentIds, scanRunIds]), context); + while (assessmentContextCache.size > 8) assessmentContextCache.delete(assessmentContextCache.keys().next().value!); + } + return publishContextReport(context); + } + } : { requireContext: false }); const manifest = await options.reportArchive.write(run, startedAt); const result: ReportRunResponse = { runId: manifest.runId, generatedAt: run.projection.generatedAt, repositoryRevision: run.revision, kind: body.kind, diff --git a/server/postgres/postgres-assessor-repository.ts b/server/postgres/postgres-assessor-repository.ts index 3842fa0..1a19ce7 100644 --- a/server/postgres/postgres-assessor-repository.ts +++ b/server/postgres/postgres-assessor-repository.ts @@ -69,6 +69,7 @@ import { processingScopeIssues } from '../../src/domain/processing-compatibility import { compareReimportSourceHashes } from '../../src/domain/reimport-source'; import { planReimportReviewTransfer, type ReimportReviewTransferPlan, type ReimportReviewTransferSummary } from '../../src/domain/reimport-review-transfer'; import { canonicalJson } from '../../src/shared/canonical-json'; +import { RevisionReadCache } from './revision-read-cache'; import { validateLocalComparisonRules } from '../../src/comparison/local-mapping-rules'; import type { AssessorRepository, @@ -391,6 +392,7 @@ function dbStructureLayer(tableName: string): DbStructureLayerId { export class PostgresAssessorRepository implements AssessorRepository { private cachedRevision: number; private readonly activeImportSessions = new Set(); + private readonly unifiedReadCache = new RevisionReadCache(); private constructor(private readonly pool: Pool, initialRevision: number) { this.cachedRevision = initialRevision; @@ -424,6 +426,12 @@ export class PostgresAssessorRepository implements AssessorRepository { return this.cachedRevision; } + async getCommittedRevision(): Promise { + const result = await this.pool.query('SELECT revision FROM assessor_repository_state WHERE singleton = true'); + if (!result.rows[0]) throw new Error('Repository revision is unavailable.'); + return Number(result.rows[0].revision); + } + private async mutate( work: (client: PoolClient) => Promise, expectedRevision?: number, @@ -1251,6 +1259,11 @@ export class PostgresAssessorRepository implements AssessorRepository { private async allUnifiedResults(query: UnifiedResultQuery = {}): Promise { // The Result DB is served only from the stored, disclosure-checked rows (see materializeResultDb). if (query.audience !== 'internal-analysis') return this.storedResultDbRows(query.projectId); + const { offset: _offset, limit: _limit, ...scope } = structuredClone(query); + return this.unifiedReadCache.read(canonicalJson(scope), () => this.databaseRevision(), () => this.loadInternalUnifiedResults(scope)); + } + + private async loadInternalUnifiedResults(query: UnifiedResultQuery): Promise { const [scanner, environment, policyObservations, policyConsistency, policyEvaluations, policy] = await Promise.all([ this.unifiedScannerResults(query), this.unifiedEnvironmentResults(query), @@ -1400,7 +1413,7 @@ export class PostgresAssessorRepository implements AssessorRepository { async queryUnifiedResults(query: UnifiedResultQuery = {}): Promise { const result = queryUnifiedResultItems(await this.allUnifiedResults(query), query); return query.audience === 'internal-analysis' - ? result + ? structuredClone(result) : { ...result, items: result.items.map(removeResultDbScopeIdentifiers) }; } @@ -1417,7 +1430,7 @@ export class PostgresAssessorRepository implements AssessorRepository { async listResultCategories(query: UnifiedResultQuery = {}): Promise { const results = await this.allUnifiedResults(query); - return listUnifiedResultCategories(filterUnifiedResults(results, query)); + return structuredClone(listUnifiedResultCategories(filterUnifiedResults(results, query))); } async adoptScanRunReplacement(projectId: string, replacedScanRunId: string, replacementScanRunId: string, supersededAt: string): Promise { @@ -1493,6 +1506,7 @@ export class PostgresAssessorRepository implements AssessorRepository { if (!context.rows[0]?.assessment_exists) throw new RepositoryValidationError('Environment Snapshot verweist auf ein Assessment außerhalb des Projekts.'); const values: unknown[] = [query.projectId]; const conditions = ['a.project_id = $1']; + if (!query.includeSuperseded) conditions.push("r.data->>'supersededByScanRunId' IS NULL"); if (query.assessmentId) { values.push(query.assessmentId); conditions.push(`a.id = $${values.length}`); diff --git a/server/postgres/revision-read-cache.ts b/server/postgres/revision-read-cache.ts new file mode 100644 index 0000000..b3a451c --- /dev/null +++ b/server/postgres/revision-read-cache.ts @@ -0,0 +1,50 @@ +/** Process-local read reuse. Every access checks the live database revision, including cache hits. */ +export class RevisionReadCache { + private readonly entries = new Map; bytes: number }>(); + private revision: number | undefined; + + constructor(private readonly maxEntries = 8, private readonly maxBytes = 32 * 1024 * 1024) {} + + async read(key: string, currentRevision: () => Promise, load: () => Promise): Promise { + for (let attempt = 0; attempt < 3; attempt += 1) { + const before = await currentRevision(); + if (!Number.isSafeInteger(before) || before < 0) throw new Error('Cannot verify the database revision.'); + if (this.revision !== before) { + this.entries.clear(); + this.revision = before; + } + let entry = this.entries.get(key); + if (!entry) { + entry = { value: Promise.resolve().then(load), bytes: 0 }; + this.entries.set(key, entry); + while (this.entries.size > this.maxEntries) this.entries.delete(this.entries.keys().next().value!); + } + // Refresh insertion order for bounded least-recently-used retention. + if (this.entries.get(key) === entry) { + this.entries.delete(key); + this.entries.set(key, entry); + } + try { + const value = await entry.value; + if (await currentRevision() !== before) { + if (this.entries.get(key) === entry) this.entries.delete(key); + continue; + } + if (this.entries.get(key) === entry && entry.bytes === 0) { + entry.bytes = Buffer.byteLength(JSON.stringify(value), 'utf8'); + let retainedBytes = [...this.entries.values()].reduce((sum, item) => sum + item.bytes, 0); + while (retainedBytes > this.maxBytes && this.entries.size) { + const oldest = this.entries.keys().next().value!; + retainedBytes -= this.entries.get(oldest)!.bytes; + this.entries.delete(oldest); + } + } + return value; + } catch (error) { + if (this.entries.get(key) === entry) this.entries.delete(key); + throw error; + } + } + throw new Error('ASSESSOR data changed repeatedly while results were read. Please retry.'); + } +} diff --git a/server/reference-data/reference-recognition-service.ts b/server/reference-data/reference-recognition-service.ts index 3fd9055..c5e3fb9 100644 --- a/server/reference-data/reference-recognition-service.ts +++ b/server/reference-data/reference-recognition-service.ts @@ -55,9 +55,14 @@ export class ReferenceRecognitionService { for (const scan of model.scanRuns.filter(item => !item.supersededByScanRunId)) { const latestTimestamp = Math.max(0, ...[...existing.values()].filter(run => run.scanRunId === scan.id).map(run => Date.parse(run.createdAt))); const run = deriveReferenceRun(model, scan.id, catalogs, new Date(Math.max(Date.now(), latestTimestamp + 1)).toISOString()); + const latest = latestReferenceRuns([...existing.values()]).find(item => item.scanRunId === scan.id); references += run.results.reduce((sum, item) => sum + item.references.length, 0); - if (existing.has(run.id)) { unchangedScans++; continue; } + if (latest && latest.inputSha256 === run.inputSha256 && latest.recognizerVersion === run.recognizerVersion + && [...latest.catalogHashes].sort().join(':') === run.catalogHashes.join(':')) { unchangedScans++; continue; } + // Returning to a previously used catalog needs a new immutable run to become latest. + if (existing.has(run.id)) run.id = 'references-' + referenceHash([run.id, run.createdAt]); await this.repository.saveReferenceRun(run, catalogs); + existing.set(run.id, run); processedScans++; } } diff --git a/src/analysis/record-builder.ts b/src/analysis/record-builder.ts index df54f59..f656747 100644 --- a/src/analysis/record-builder.ts +++ b/src/analysis/record-builder.ts @@ -40,7 +40,8 @@ export function buildRecord(input: { })); const filled = fields.filter(field => field.value); // ID-Kopfzeilen (z. B. ShortName, Rule ID) duerfen nicht als Titelfeld verwendet werden, da z. B. ShortName "name" als Substring enthaelt. - const isIdHeader = (label: string) => /^(?:short\s*name|shortname|rule\s*id|control\s*id|test\s*id|id|key|code)$/i.test(label.trim()); + // The derived `Benchmark section` path names a report section, never the check itself. + const isIdHeader = (label: string) => /^(?:short\s*name|shortname|rule\s*id|control\s*id|test\s*id|id|key|code|benchmark\s*section)$/i.test(label.trim()); // Provenienz- und Metadaten-Felder (wie "Quelle: Datei") duerfen nie als Fallback-Titel gewählt werden. const nonProvenanceFilled = filled.filter(field => !/^(?:quelle:|source:|datei|file|sheet|blatt|scan)/i.test(field.label.trim())); // Der Titel bleibt bewusst einzeilig: er steht in Kachel, Kartenliste und diff --git a/src/api/assessor-api-client.ts b/src/api/assessor-api-client.ts index 3c2900c..6f9be05 100644 --- a/src/api/assessor-api-client.ts +++ b/src/api/assessor-api-client.ts @@ -12,6 +12,7 @@ import type { import type { ProjectDataExport } from '../persistence/assessor-repository'; import type { DatahubTimeslotResultMetrics } from '../features/project-import/datahub-scan-analysis'; import type { EnvironmentSnapshot, EnvironmentSnapshotQuery } from '../domain/environment-context'; +import type { ClientInformationQuery, ClientInformationResult } from '../domain/client-information'; import type { ImportedDataset } from '../domain/dataset'; import type { MicrosoftSecurityBaselineSelectionContext } from '../domain/microsoft-security-baseline'; import type { ImportBatchPreview, ImportBatchSelection } from '../domain/import-workflow'; @@ -119,7 +120,7 @@ export class AssessorApiClient { private async request(path: string, init: RequestInit = {}): Promise { const descriptor = this.describeActivity(path, init.method); const run = () => this.performRequest(path, init); - return descriptor ? trackActivity(descriptor.area, descriptor.label, run) : run(); + return descriptor ? trackActivity(descriptor.area, descriptor.label, run, `${this.baseUrl} ${init.method ?? 'GET'} ${path}`) : run(); } private async performRequest(path: string, init: RequestInit): Promise { @@ -280,6 +281,16 @@ export class AssessorApiClient { return this.request(`/projects/${encodeURIComponent(query.projectId)}/environment-snapshot${suffix}`); } + queryClientInformation(query: ClientInformationQuery): Promise { + const parameters = new URLSearchParams(); + for (const [key, value] of Object.entries(query)) { + if (key === 'projectId' || value === undefined) continue; + if (Array.isArray(value)) value.forEach(item => parameters.append(key, String(item))); + else parameters.set(key, String(value)); + } + return this.request(`/projects/${encodeURIComponent(query.projectId)}/client-information${parameters.size ? `?${parameters}` : ''}`); + } + deleteAssessment(projectId: string, assessmentId: string): Promise<{ deleted: boolean; repositoryRevision: number }> { return this.request(`/projects/${encodeURIComponent(projectId)}/assessments/${encodeURIComponent(assessmentId)}`, { method: 'DELETE', body: JSON.stringify({ projectId, assessmentId, cascade: true }) diff --git a/src/app/AppProvider.tsx b/src/app/AppProvider.tsx index e153333..4c70289 100644 --- a/src/app/AppProvider.tsx +++ b/src/app/AppProvider.tsx @@ -113,14 +113,14 @@ export function AppProvider({ children }: PropsWithChildren) { const engines = useEnginePreferences(); useEffect(() => { - if (engines.context) return; setState(current => { - if (!current.facetFilters['context:cluster']) return current; const facetFilters = { ...current.facetFilters }; - delete facetFilters['context:cluster']; + if (!engines.context) delete facetFilters['context:cluster']; + if (!engines.context || !engines.report) delete facetFilters['report:business-impact']; + if (Object.keys(facetFilters).length === Object.keys(current.facetFilters).length) return current; return { ...current, facetFilters }; }); - }, [engines.context, state.facetFilters]); + }, [engines.context, engines.report, state.facetFilters]); useEffect(() => { let active = true; diff --git a/src/assessment-context/context-catalog.ts b/src/assessment-context/context-catalog.ts index e361df8..96ab42b 100644 --- a/src/assessment-context/context-catalog.ts +++ b/src/assessment-context/context-catalog.ts @@ -4,7 +4,7 @@ * `not semantically mapped` and is never guessed. Conditions are defined by explicit value sets; * every other value is `unknown`. Nothing here is a compliance verdict or an effective host state. */ -export const ASSESSMENT_CONTEXT_CATALOG = { id: 'assessment-context-catalog', version: '1.9.0' } as const; +export const ASSESSMENT_CONTEXT_CATALOG = { id: 'assessment-context-catalog', version: '1.10.0' } as const; export interface ContextSource { title: string; @@ -123,6 +123,11 @@ export interface AssessmentContextCatalog { gpoVerdictRules: ContextGpoVerdictRule[]; /** Condition for each source's deviations that no rule or section assigns yet; they stay visible. */ uncataloguedDeviationConditions: Record; + /** + * One finding, one cluster (user decision 2026-10-04): a condition several clusters list counts only for the + * cluster whose attack mechanism it names most specifically. Every shared condition must declare it here. + */ + primaryClusters?: Record; } const SECURITY_POLICY_DOCS = 'https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/security-policy-settings/'; @@ -371,6 +376,9 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = { { id: 'ad-rc4-des-on-dcs', label: { de: 'Domain Controller unterstützen RC4 oder DES', en: 'Domain controllers support RC4 or DES' } }, { id: 'ad-privileged-password-never-expires', label: { de: 'Privilegierte Konten haben ein nie ablaufendes Passwort', en: 'Privileged accounts have a password that never expires' } }, { id: 'ad-password-never-expires', label: { de: 'Konten haben das Flag Password Never Expires', en: 'Accounts have the Password Never Expires flag' } }, + { id: 'ad-smartcard-password-rotation-disabled', label: { + de: 'Kennwortrotation fuer Smartcard-only-Konten ist deaktiviert', en: 'Smart card-only account password rotation is disabled' + } }, { id: 'ad-shadow-credentials-privileged', label: { de: 'Privilegierte Objekte tragen Shadow Credentials', en: 'Privileged objects carry shadow credentials' } }, { id: 'mdns-not-disabled', label: { de: 'Multicast DNS (mDNS) ist nicht abgeschaltet', en: 'Multicast DNS (mDNS) is not turned off' } }, { id: 'netbios-not-disabled', label: { de: 'NetBIOS-Namensauflösung ist nicht abgeschaltet', en: 'NetBIOS name resolution is not turned off' } }, @@ -740,6 +748,14 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = { statement: { de: 'Nie ablaufende Passwörter begünstigen, dass geknackte Passwörter dauerhaft gültig bleiben.', en: 'Never-expiring passwords favour cracked passwords staying valid indefinitely.' }, sources: [T1558_003] }, + { + conditionId: 'ad-smartcard-password-rotation-disabled', role: 'prerequisite', techniqueIds: [], + statement: { + de: 'Deaktivierte Kennwortrotation für Smartcard-only-Konten begünstigt die dauerhafte Nutzung übernommener Zugangsdaten und seitliche Bewegung.', + en: 'Disabled password rotation for smart card-only accounts favours lasting use of compromised credentials and lateral movement.' + }, + sources: [PURPLE_KNIGHT] + }, { conditionId: 'ad-shadow-credentials-privileged', role: 'prerequisite', techniqueIds: [], statement: { de: 'Shadow Credentials an privilegierten Objekten begünstigen die Anmeldung als dieses Objekt ohne sein Passwort.', en: 'Shadow credentials on privileged objects favour authenticating as that object without its password.' }, @@ -1654,7 +1670,7 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = { en: 'Several observed findings favour the same path: spreading through the network over remote services with valid credentials. This is not proof that an attack is possible or took place.' }, conditionIds: ['rdp-without-nla', 'winrm-basic-auth', 'winrm-unencrypted', 'winrm-remote-management-allowed', 'smbv1-enabled', 'host-firewall-off', 'host-firewall-inbound-allowed', 'network-logon-rights-broad', - 'laps-not-deployed', 'rdp-sessions-not-timed-out', 'terminal-services-gpo-incomplete', 'rdp-connection-security-weak', + 'laps-not-deployed', 'ad-smartcard-password-rotation-disabled', 'rdp-sessions-not-timed-out', 'terminal-services-gpo-incomplete', 'rdp-connection-security-weak', 'cis-remote-desktop-unmet', 'cis-remote-management-unmet', 'cis-firewall-profile-unmet', 'cis-network-stack-unmet', 'gpo-network-logon-right-deviates', 'gpo-firewall-profile-deviates'], minimumSatisfied: 2, sources: [T1021] @@ -1928,6 +1944,11 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = { ruleText: 'RC4 or DES encryption type are supported by Domain Controllers', sources: [] }, { tool: 'purple-knight', ruleId: 'SI000067', conditionId: 'ad-privileged-password-never-expires', polarity: 'fail-means-met', ruleText: 'Privileged accounts with a password that never expires', sources: [] }, + // Source: user-supplied Purple Knight SI000147 report (2026-10-02), not a tactic-only assignment. + // The report states disabled msDS-ExpirePasswordsOnSmartCardOnlyAccounts permits indefinite credentials + // and can favour persistent access and lateral movement. No effective compromise is asserted. + { tool: 'purple-knight', ruleId: 'SI000147', conditionId: 'ad-smartcard-password-rotation-disabled', polarity: 'fail-means-met', + ruleText: 'Smart card password rotation disabled', sources: [PURPLE_KNIGHT] }, { tool: 'purple-knight', ruleId: 'SI000029', conditionId: 'ad-password-never-expires', polarity: 'fail-means-met', ruleText: 'Users with Password Never Expires flag set', sources: [] }, { tool: 'purple-knight', ruleId: 'SI000351', conditionId: 'ad-shadow-credentials-privileged', polarity: 'fail-means-met', @@ -2532,6 +2553,16 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = { { target: { mechanism: 'security-template', scope: 'computer', section: "System Access", key: "ForceLogoffWhenHourExpire" }, conditionId: 'gpo-logon-hours-enforcement-deviates' }, { target: { mechanism: 'registry-key', scope: 'user', key: "Software\\Policies\\Microsoft\\Office\\16.0\\Common", valueName: "fbaenabledhosts" }, conditionId: 'gpo-office-forms-auth-hosts-deviate' } ], + primaryClusters: { + 'adcs-web-enrollment-esc8': 'adcs-escalation', + 'cis-print-hardening-unmet': 'host-credential-privilege-exposure', + 'ad-admins-delegatable': 'kerberos-delegation-abuse', + 'ad-privileged-spn': 'kerberoasting-exposure', + 'ad-service-accounts-no-strong-policy': 'kerberoasting-exposure', + 'ad-privileged-password-never-expires': 'password-guessing-exposure', + 'ad-password-never-expires': 'password-guessing-exposure', + 'domain-admins-password-never-expires': 'password-guessing-exposure' + }, uncataloguedDeviationConditions: { 'gpo': 'uncatalogued-deviations-gpo', 'pingcastle': 'uncatalogued-deviations-pingcastle', 'purple-knight': 'uncatalogued-deviations-purple-knight', 'cis-cat': 'uncatalogued-deviations-cis-cat', 'maester': 'uncatalogued-deviations-maester' } }; @@ -2605,5 +2636,17 @@ export function assessmentContextCatalogErrors(catalog: AssessmentContextCatalog } sourceOk(cluster.sources, cluster.id); } + // One finding, one cluster (user decision 2026-10-04): every shared condition declares exactly one of its clusters. + for (const conditionId of conditionIds) { + const owners = catalog.clusters.filter(cluster => cluster.conditionIds.includes(conditionId)).map(cluster => cluster.id); + const primary = catalog.primaryClusters?.[conditionId]; + if (owners.length > 1 && !primary) errors.push(`${conditionId}: shared by ${owners.join(', ')} without a primary cluster.`); + if (primary && !owners.includes(primary)) errors.push(`${conditionId}: primary cluster ${primary} does not list the condition.`); + } return errors; } + +/** The single cluster a catalogued condition counts for (user decision 2026-10-04). */ +export function primaryClusterOfCondition(catalog: AssessmentContextCatalog, conditionId: string): string | undefined { + return catalog.primaryClusters?.[conditionId] ?? catalog.clusters.find(cluster => cluster.conditionIds.includes(conditionId))?.id; +} diff --git a/src/assessment-context/context-cluster-severity.ts b/src/assessment-context/context-cluster-severity.ts new file mode 100644 index 0000000..2be99d6 --- /dev/null +++ b/src/assessment-context/context-cluster-severity.ts @@ -0,0 +1,164 @@ +/** + * User-approved category baseline for the Context heatmap, researched 2026-10-02. + * This is ASSESSOR's thematic assessment, not a severity issued by the cited publishers, + * a CVSS score, a finding severity, or a judgement about exploitability in a client. + * Sources and explanations are bundled offline; no network lookup occurs at runtime. + */ +export const CONTEXT_SEVERITY_CATALOG = { + id: 'context-cluster-severity', version: '1.0.0', reviewedAt: '2026-10-02' +} as const; + +export type ContextBaseSeverity = 'critical' | 'high' | 'medium' | 'undetermined'; + +export interface ContextSeverityReference { + clusterId: string; + severity: ContextBaseSeverity; + rationale: string; + qualification: string; + sources: ReadonlyArray<{ title: string; publisher: string; url: string }>; +} + +export const contextSeverityLabels: Record = { + critical: 'Critical', high: 'High', medium: 'Medium', undetermined: 'Undetermined' +}; + +export const contextClusterSeverities: readonly ContextSeverityReference[] = [ + { + clusterId: 'credential-relay-exposure', severity: 'high', + rationale: 'Relayed authentication can enable unauthorized access and privilege escalation.', + qualification: 'Impact depends on the target and account privileges. A missing protection alone does not establish a working relay path.', + sources: [{ publisher: 'Microsoft', title: 'SMB security hardening', url: 'https://learn.microsoft.com/en-us/windows-server/storage/file-server/smb-security-hardening' }] + }, + { + clusterId: 'cloud-privileged-access-exposure', severity: 'critical', + rationale: 'Compromise of cloud administrative identities can expose central identity, authorization and security controls.', + qualification: 'The actual impact depends on the roles and scopes involved; a control gap is not proof of administrative compromise.', + sources: [{ publisher: 'Microsoft', title: 'Privileged access strategy', url: 'https://learn.microsoft.com/security/privileged-access-workstations/privileged-access-strategy' }] + }, + { + clusterId: 'cloud-sign-in-protection-gaps', severity: 'high', + rationale: 'Weak sign-in protections can favour account takeover through stolen credentials or legacy authentication.', + qualification: 'Effective authentication policies, exclusions and the affected identities determine the client-specific impact.', + sources: [{ publisher: 'Microsoft', title: 'Protect identities with Zero Trust', url: 'https://learn.microsoft.com/en-us/entra/fundamentals/zero-trust-protect-identities' }] + }, + { + clusterId: 'ad-privileged-account-exposure', severity: 'critical', + rationale: 'Exposed privileged AD identities can enable control over central directory services and connected systems.', + qualification: 'Account privileges and reachable administrative paths matter; the category does not assert a completed takeover.', + sources: [{ publisher: 'Microsoft', title: 'Securing privileged access', url: 'https://learn.microsoft.com/windows-server/identity/securing-privileged-access/securing-privileged-access' }] + }, + { + clusterId: 'kerberos-delegation-abuse', severity: 'critical', + rationale: 'Abusable delegation can enable impersonation of privileged identities and compromise of domain services.', + qualification: 'Delegation scope and prerequisites determine impact. A narrowly scoped service configuration can have lower impact.', + sources: [{ publisher: 'Microsoft', title: 'Identity security posture assessments for accounts', url: 'https://learn.microsoft.com/en-us/defender-for-identity/security-posture-assessments/accounts' }] + }, + { + clusterId: 'adcs-escalation', severity: 'critical', + rationale: 'Unsafe certificate enrollment configurations can allow impersonation of privileged domain identities.', + qualification: 'Enrollment permissions, template settings and approval requirements must permit the specific escalation path.', + sources: [{ publisher: 'Microsoft', title: 'Insecure AD CS certificate enrollment', url: 'https://learn.microsoft.com/en-au/defender-for-identity/security-assessment-insecure-adcs-certificate-enrollment' }] + }, + { + clusterId: 'kerberoasting-exposure', severity: 'high', + rationale: 'Service tickets can support offline password cracking and compromise of service accounts.', + qualification: 'Password strength and service-account privileges determine whether this exposure leads to significant compromise.', + sources: [{ publisher: 'MITRE', title: 'ATT&CK: Kerberoasting', url: 'https://attack.mitre.org/techniques/T1558/003/' }] + }, + { + clusterId: 'lateral-movement-exposure', severity: 'high', + rationale: 'Weak remote-access controls can favour movement from one compromised system to other systems.', + qualification: 'An available administrative service alone is not a vulnerability; authentication, permissions and reachability matter.', + sources: [{ publisher: 'MITRE', title: 'ATT&CK: Remote Services', url: 'https://attack.mitre.org/techniques/T1021/' }] + }, + { + clusterId: 'host-credential-privilege-exposure', severity: 'high', + rationale: 'Exposed credentials and local privileges can enable credential theft, elevation and further access.', + qualification: 'Impact increases when privileged credentials are available on the affected host.', + sources: [{ publisher: 'Microsoft', title: 'How Credential Guard works', url: 'https://learn.microsoft.com/windows/security/identity-protection/credential-guard/how-it-works/' }] + }, + { + clusterId: 'detection-blind-spots', severity: 'high', + rationale: 'Missing security telemetry can delay detection, containment and investigation of an intrusion.', + qualification: 'This is a defensive visibility gap; it does not itself demonstrate a means of initial access.', + sources: [{ publisher: 'ASD ACSC', title: 'Best practices for event logging and threat detection', url: 'https://www.cyber.gov.au/business-government/detecting-responding-to-threats/event-logging/best-practices-for-event-logging-and-threat-detection' }] + }, + { + clusterId: 'password-guessing-exposure', severity: 'high', + rationale: 'Weak password and authentication protections can favour unauthorized access through password guessing.', + qualification: 'Reachability, MFA, rate limits and account privileges affect the practical impact.', + sources: [{ publisher: 'MITRE', title: 'ATT&CK: Password Guessing', url: 'https://attack.mitre.org/techniques/T1110/001/' }] + }, + { + clusterId: 'malicious-code-execution-exposure', severity: 'high', + rationale: 'Weak execution protections can favour malicious scripts or applications, including ransomware and data theft.', + qualification: 'A missing execution control is not evidence that malicious code has run.', + sources: [{ publisher: 'Microsoft', title: 'Virus and threat protection', url: 'https://learn.microsoft.com/en-us/windows/security/book/operating-system-security-virus-and-threat-protection' }] + }, + { + clusterId: 'ad-persistence-indicators', severity: 'high', + rationale: 'Altered protected-account permissions or related directory conditions can favour persistent privileged access.', + qualification: 'The cluster is heterogeneous. A stale adminCount value alone does not prove persistence or compromise.', + sources: [{ publisher: 'Microsoft', title: 'Reducing the Active Directory attack surface', url: 'https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/plan/security-best-practices/reducing-the-active-directory-attack-surface' }] + }, + { + clusterId: 'ad-reconnaissance-exposure', severity: 'medium', + rationale: 'Directory enumeration can reveal accounts and relationships useful for subsequent attacks.', + qualification: 'Reconnaissance supports follow-on activity but does not by itself grant control of the directory.', + sources: [{ publisher: 'MITRE', title: 'ATT&CK: Domain Account Discovery', url: 'https://attack.mitre.org/techniques/T1087/002/' }] + }, + { + clusterId: 'ad-recovery-gaps', severity: 'high', + rationale: 'Insufficient directory recovery preparations can prolong outages and prevent reliable recovery after compromise.', + qualification: 'Missing usable forest backups can have greater impact; actual recoverability requires recovery evidence.', + sources: [{ publisher: 'Microsoft', title: 'AD forest recovery: determine how to recover', url: 'https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/forest-recovery-guide/ad-forest-recovery-determine-how-to-recover' }] + }, + { + clusterId: 'cloud-app-consent-exposure', severity: 'high', + rationale: 'Malicious application consent can grant persistent access to cloud data and services.', + qualification: 'Granted permissions determine impact. An expiring application secret is not equivalent to a malicious consent grant.', + sources: [{ publisher: 'Microsoft', title: 'Detect and remediate illicit consent grants', url: 'https://learn.microsoft.com/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants' }] + }, + { + clusterId: 'cloud-external-access-exposure', severity: 'medium', + rationale: 'Broad guest and external-sharing permissions can expose information to unintended recipients.', + qualification: 'External collaboration can be legitimate. Sensitive data or excessive permissions can make a specific exposure more severe.', + sources: [{ publisher: 'Microsoft', title: 'External sharing overview', url: 'https://learn.microsoft.com/en-us/sharepoint/external-sharing-overview' }] + }, + { + clusterId: 'data-sharing-exposure', severity: 'medium', + rationale: 'Data sharing, synchronization and diagnostic-data settings can affect confidentiality and privacy.', + qualification: 'These settings do not by themselves establish exfiltration; the data, destinations and approved purpose matter.', + sources: [{ publisher: 'Microsoft', title: 'Configure Windows diagnostic data', url: 'https://learn.microsoft.com/en-us/windows/privacy/configure-windows-diagnostic-data-in-your-organization' }] + }, + { + clusterId: 'physical-local-access-exposure', severity: 'medium', + rationale: 'Weak local-session and physical-access controls can favour access by someone with physical proximity.', + qualification: 'Physical access, unattended sessions and the privileges of the available account determine practical impact.', + sources: [ + { publisher: 'Microsoft', title: 'Interactive logon: machine inactivity limit', url: 'https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/security-policy-settings/interactive-logon-machine-inactivity-limit' }, + { publisher: 'MITRE', title: 'ATT&CK: Hardware Additions', url: 'https://attack.mitre.org/techniques/T1200/' } + ] + }, + { + clusterId: 'patch-configuration-drift', severity: 'high', + rationale: 'Weak patch and configuration maintenance can leave known vulnerabilities and protective configuration gaps unresolved.', + qualification: 'This cluster also covers policy processing and time synchronization. It does not prove that a specific critical patch is missing.', + sources: [{ publisher: 'NIST', title: 'SP 800-40 Rev. 4: Enterprise Patch Management Planning', url: 'https://csrc.nist.gov/pubs/sp/800/40/r4/final' }] + }, + { + clusterId: 'email-spoofing-phishing-exposure', severity: 'high', + rationale: 'Weak email authentication and forwarding controls can favour impersonation, phishing and disclosure of messages.', + qualification: 'A control gap is not proof of a successful phishing attack or unauthorized forwarding.', + sources: [ + { publisher: 'Microsoft', title: 'Email authentication', url: 'https://learn.microsoft.com/en-us/defender-office-365/email-authentication-about' }, + { publisher: 'Microsoft', title: 'Control automatic external email forwarding', url: 'https://learn.microsoft.com/en-us/defender-office-365/outbound-spam-policies-external-email-forwarding' } + ] + }, + { + clusterId: 'other-deviations', severity: 'undetermined', + rationale: 'Uncatalogued deviations have no common established mechanism from which to derive a category severity.', + qualification: 'Undetermined does not mean low severity or absence of a problem. Review the underlying findings.', + sources: [] + } +]; diff --git a/src/assessment-context/context-engine.ts b/src/assessment-context/context-engine.ts index 57f38e2..0435993 100644 --- a/src/assessment-context/context-engine.ts +++ b/src/assessment-context/context-engine.ts @@ -5,11 +5,12 @@ import type { ReferenceRecognitionRun } from '../domain/reference-recognition'; import type { ScannerReportSection } from '../domain/environment-context'; import { cisBenchmarkSectionTitles } from '../profiles/cis-cat/cis-benchmark-section-titles'; import { - assessmentContextCatalogErrors, type AssessmentContextCatalog, type ContextCondition, type ContextRelationship, + assessmentContextCatalogErrors, primaryClusterOfCondition, type AssessmentContextCatalog, type ContextCondition, type ContextRelationship, type ContextGpoVerdictTarget, type ContextScannerRule, type ContextSemanticDefinition, type ContextSettingTarget, type ContextSource, type ContextText } from './context-catalog'; import { CONTEXT_RECOGNITION_TERMS, CONTEXT_RECOGNITION_VOCABULARY, type RecognitionTerm } from './context-recognition'; import { assignRemainingDeviations, type ContextAssignmentMethod } from './context-assignment'; +import { contextClusterSeverities } from './context-cluster-severity'; /** * Context engine (Phase 13E): a derived, switchable second reading of stored ASSESSOR results. @@ -17,7 +18,7 @@ import { assignRemainingDeviations, type ContextAssignmentMethod } from './conte * GPO backups contribute what they declare; scanners contribute what they observed. Both stay * separately attributed evidence of the same catalogued conditions. */ -export const ASSESSMENT_CONTEXT_ENGINE = { id: 'assessment-context-engine', version: '1.10.0' } as const; +export const ASSESSMENT_CONTEXT_ENGINE = { id: 'assessment-context-engine', version: '1.11.0' } as const; export type DeclaredValue = | { state: 'configured'; value: number } @@ -123,7 +124,10 @@ export interface ContextSettingAttachment { factIds: string[]; evaluationIds: string[]; conditionIds: string[]; + /** Exactly one primary cluster per deviation (user decision 2026-10-04); empty when no cluster applies. */ clusterIds: string[]; + /** Further clusters the evidence also names; traceable, but never counted (user decision 2026-10-04). */ + secondaryClusterIds?: string[]; /** Confidence of each cluster that is not catalogued (user decision 2026-10-01); absent means catalogued. */ clusterConfidence?: Record; } @@ -136,7 +140,10 @@ export interface ContextFindingAttachment { sourceRuleId: string; observationIds: string[]; conditionIds: string[]; + /** Exactly one primary cluster per finding (user decision 2026-10-04); empty when no cluster applies. */ clusterIds: string[]; + /** Further clusters the evidence also names; traceable, but never counted (user decision 2026-10-04). */ + secondaryClusterIds?: string[]; /** ATT&CK and D3FEND IDs the scanner itself declares for this finding (user decisions 2026-09-30). */ declaredReferenceIds: string[]; /** CIS benchmark sections containing this rule in its own scan report, top level first. */ @@ -818,6 +825,40 @@ export function buildAssessmentContext(input: AssessmentContextInput): Assessmen entry.clusterConfidence = { ...(entry.clusterConfidence ?? {}), [item.clusterId]: item.confidence }; } + // One finding, one cluster (user decision 2026-10-04): the strongest evidence decides, the rest stays traceable. + const support = new Map>(); + const supports = (key: string, clusterIds: string[]) => { + const counts = support.get(key) ?? new Map(); + for (const clusterId of clusterIds) counts.set(clusterId, (counts.get(clusterId) ?? 0) + 1); + support.set(key, counts); + }; + // A catalogued condition several clusters list supports only its declared primary cluster. + const primaryOf = (conditionId: string) => { const id = primaryClusterOfCondition(input.catalog, conditionId); return id ? [id] : []; }; + for (const item of observations) supports(`finding:${item.occurrenceId}`, primaryOf(item.conditionId)); + for (const item of techniqueAssignments) supports(`finding:${item.occurrenceId}`, [item.clusterId]); + for (const fact of facts) supports(`setting:${fact.scope}\u0000${fact.identityKey}`, semanticById.get(fact.semanticId)!.conditions.flatMap(condition => primaryOf(condition.id))); + for (const item of evaluations.filter(evaluation => evaluation.matchedBy !== 'assigned-by-recognition')) supports(`setting:${item.scope}\u0000${item.identityKey}`, primaryOf(item.conditionId)); + for (const item of recognitions) supports(item.subject === 'scanner-finding' ? `finding:${item.occurrenceId}` : `setting:${item.scope}\u0000${item.identityKey}`, [item.clusterId]); + const confidenceRank = { catalogued: 0, recognized: 1, estimated: 2 } as const; + const severityRank = new Map(contextClusterSeverities.map(item => [item.clusterId, ({ critical: 0, high: 1, medium: 2, undetermined: 3 } as const)[item.severity]])); + const choosePrimary = (key: string, entry: ContextFindingAttachment | ContextSettingAttachment) => { + if (entry.clusterIds.length < 2) return; + const counts = support.get(key); + const ranked = [...entry.clusterIds].sort((left, right) => + confidenceRank[entry.clusterConfidence?.[left] ?? 'catalogued'] - confidenceRank[entry.clusterConfidence?.[right] ?? 'catalogued'] + || (counts?.get(right) ?? 0) - (counts?.get(left) ?? 0) + || (severityRank.get(left) ?? 3) - (severityRank.get(right) ?? 3) + || left.localeCompare(right)); + entry.clusterIds = [ranked[0]]; + entry.secondaryClusterIds = ranked.slice(1); + if (entry.clusterConfidence) { + const confidence = entry.clusterConfidence[ranked[0]]; + if (confidence) entry.clusterConfidence = { [ranked[0]]: confidence }; else delete entry.clusterConfidence; + } + }; + for (const entry of findings.values()) choosePrimary(`finding:${entry.occurrenceId}`, entry); + for (const entry of settings.values()) choosePrimary(`setting:${entry.scope}\u0000${entry.identityKey}`, entry); + // Declared ATT&CK and D3FEND references: latest run per scan run, only findings of this scope, only declared and active IDs. const scopedFindings = new Map((input.scannerFindings ?? []).map(finding => [finding.sourceResultId, finding])); const groups = new Map(); diff --git a/src/assessment-context/context-heatmap.ts b/src/assessment-context/context-heatmap.ts new file mode 100644 index 0000000..e9f6034 --- /dev/null +++ b/src/assessment-context/context-heatmap.ts @@ -0,0 +1,81 @@ +import type { AssessmentContext, ContextClusterResult } from './context-engine'; +import { CONTEXT_SEVERITY_CATALOG, contextClusterSeverities, type ContextSeverityReference } from './context-cluster-severity'; + +export { CONTEXT_SEVERITY_CATALOG, contextSeverityLabels } from './context-cluster-severity'; + +export interface ContextHeatmapCell { + cluster: ContextClusterResult; + reference: ContextSeverityReference; + /** Colour needs supporting Context evidence; automatic assignments count too. */ + hasEvidence: boolean; + scannerDeviations: number; + gpoDeviations: number; + recognized: number; + estimated: number; +} + +type HeatmapContext = Pick + & Partial>; + +function heatmapEvidence(context: HeatmapContext, cluster: ContextClusterResult) { + const conditionIds = new Set(cluster.conditions.map(condition => condition.conditionId)); + const scanners = new Set(context.observations.filter(item => conditionIds.has(item.conditionId) + && (item.outcome === 'fail' || item.outcome === 'warning')).map(item => item.occurrenceId)); + const settings = new Set(context.evaluations.filter(item => item.matchedBy !== 'assigned-by-recognition' + && conditionIds.has(item.conditionId) && item.verdict === 'deviation') + .map(item => `${item.scope}\u0000${item.identityKey}`)); + for (const item of context.techniqueAssignments) { + if (item.clusterId === cluster.clusterId && (item.outcome === 'fail' || item.outcome === 'warning')) scanners.add(item.occurrenceId); + } + const assignments = context.recognitions.filter(item => item.clusterId === cluster.clusterId); + for (const item of assignments) { + if (item.subject === 'scanner-finding' && item.occurrenceId) scanners.add(item.occurrenceId); + if (item.subject === 'gpo-setting' && item.identityKey) settings.add(`${item.scope}\u0000${item.identityKey}`); + } + // One finding, one cluster (user decision 2026-10-04): a deviation counts only in its primary cluster, so the + // cells always add up to the unique finding total. + const findingPrimary = context.findings && new Map(context.findings.map(item => [item.occurrenceId, item.clusterIds[0]])); + const settingPrimary = context.settings && new Map(context.settings.map(item => [`${item.scope}\u0000${item.identityKey}`, item.clusterIds[0]])); + const elsewhere = (primary: Map | undefined, key: string) => Boolean(primary?.has(key) && primary.get(key) !== cluster.clusterId); + for (const id of scanners) if (elsewhere(findingPrimary, id)) scanners.delete(id); + for (const id of settings) if (elsewhere(settingPrimary, id)) settings.delete(id); + return { scanners, settings, assignments: assignments.filter(item => item.subject === 'scanner-finding' + ? !elsewhere(findingPrimary, item.occurrenceId ?? '') : !elsewhere(settingPrimary, `${item.scope}\u0000${item.identityKey}`)) }; +} + +/** Unique findings represented by the heatmap; cross-cluster membership never inflates the total. */ +export function contextHeatmapFindingCount(context: HeatmapContext): number { + const scanners = new Set(); + const settings = new Set(); + for (const cluster of context.clusters) { + const evidence = heatmapEvidence(context, cluster); + for (const id of evidence.scanners) scanners.add(id); + for (const id of evidence.settings) settings.add(id); + } + return scanners.size + settings.size; +} + +/** Presentation projection of existing Context results. Does not classify or evaluate findings. */ +export function contextHeatmap(context: HeatmapContext): ContextHeatmapCell[] { + const references = new Map(contextClusterSeverities.map(reference => [reference.clusterId, reference])); + const cells: ContextHeatmapCell[] = context.clusters.map(cluster => { + const { scanners, settings, assignments } = heatmapEvidence(context, cluster); + return { + cluster, + reference: references.get(cluster.clusterId) ?? { + clusterId: cluster.clusterId, severity: 'undetermined', + rationale: 'No category severity has been reviewed for this cluster.', + qualification: `Severity catalog ${CONTEXT_SEVERITY_CATALOG.version} does not cover this cluster.`, sources: [] + }, + hasEvidence: cluster.satisfied > 0 || scanners.size + settings.size > 0, + scannerDeviations: scanners.size, + gpoDeviations: settings.size, + recognized: assignments.filter(item => item.confidence === 'recognized').length, + estimated: assignments.filter(item => item.confidence === 'estimated').length + }; + }); + // Sort by the colour actually displayed; unpopulated and undetermined cells are grey. + const rank = (cell: ContextHeatmapCell) => !cell.hasEvidence ? 3 + : ({ critical: 0, high: 1, medium: 2, undetermined: 3 } as const)[cell.reference.severity]; + return cells.sort((left, right) => rank(left) - rank(right)); +} diff --git a/src/assessment-context/context-report-publication.ts b/src/assessment-context/context-report-publication.ts new file mode 100644 index 0000000..cfa3fe4 --- /dev/null +++ b/src/assessment-context/context-report-publication.ts @@ -0,0 +1,46 @@ +import type { AssessmentContext } from './context-engine'; +import { contextHeatmap, contextHeatmapFindingCount, CONTEXT_SEVERITY_CATALOG } from './context-heatmap'; + +/** Published, frozen Context results. Consumers never import Context algorithms or catalog internals. */ +export function publishContextReport(context: AssessmentContext) { + const deviations = new Set([ + ...context.observations.filter(item => item.outcome === 'fail' || item.outcome === 'warning').map(item => item.occurrenceId), + ...context.techniqueAssignments.filter(item => item.outcome === 'fail' || item.outcome === 'warning').map(item => item.occurrenceId), + ...context.recognitions.filter(item => item.subject === 'scanner-finding').map(item => item.occurrenceId!) + ]); + const settings = new Set(context.evaluations.filter(item => item.verdict === 'deviation').map(item => `${item.scope}\u0000${item.identityKey}`)); + const clusters = contextHeatmap(context).map(cell => { + const scanner = context.findings.filter(item => deviations.has(item.occurrenceId) && item.clusterIds.includes(cell.cluster.clusterId)); + const gpo = context.settings.filter(item => settings.has(`${item.scope}\u0000${item.identityKey}`) && item.clusterIds.includes(cell.cluster.clusterId)); + const confidence = { catalogued: 0, recognized: 0, estimated: 0 }; + if (cell.cluster.clusterId !== 'other-deviations') { + for (const item of [...scanner, ...gpo]) confidence[item.clusterConfidence?.[cell.cluster.clusterId] ?? 'catalogued']++; + } + return { + id: cell.cluster.clusterId, label: cell.cluster.label.en, description: cell.cluster.description.de, + severity: cell.reference.severity, hasEvidence: cell.hasEvidence, + scanner: scanner.length, gpo: gpo.length, confidence, + tools: [...new Set(scanner.map(item => item.tool))].sort(), + findingKeys: [...new Set([ + ...scanner.map(item => `scanner:${item.occurrenceId}`), + ...gpo.map(item => `gpo:${item.scope}\u0000${item.identityKey}`) + ])].sort(), + resultIds: [...new Set([ + ...scanner.map(item => item.resultProjectionId), + ...gpo.flatMap(item => context.evaluations.filter(evaluation => evaluation.scope === item.scope && evaluation.identityKey === item.identityKey + && evaluation.verdict === 'deviation').flatMap(evaluation => evaluation.resultProjectionIds)) + ])].sort(), + sources: cell.reference.sources.map(source => ({ ...source })) + }; + }); + return { + version: 1 as const, projectId: context.projectId, assessmentIds: [...context.assessmentIds].sort(), + scanRunIds: [...(context.scanRunIds ?? [])].sort(), repositoryRevision: context.repositoryRevision, + generatedAt: context.generatedAt, engineVersion: context.engine.version, catalogVersion: context.catalog.version, + severityCatalogVersion: CONTEXT_SEVERITY_CATALOG.version, vocabularyVersion: context.recognitionVocabulary.version, + findingCount: contextHeatmapFindingCount(context), + clusters, sharedSettingLevers: context.levers.filter(item => item.clusters > 1).map(item => ({ + settings: 1, conditions: item.satisfiedConditions, clusters: item.clusters, backups: item.backupInstanceIds.length + })) + }; +} diff --git a/src/assessment-context/context-run.ts b/src/assessment-context/context-run.ts index d079b4d..629b4f2 100644 --- a/src/assessment-context/context-run.ts +++ b/src/assessment-context/context-run.ts @@ -31,6 +31,37 @@ export class ContextStateChangedError extends Error { } } +/** Reuse large reads only inside one revision-checked attempt; callers receive independent source values. */ +export function reuseContextReads(source: ContextReadPort): ContextReadPort { + function memoize(read: (...args: A) => Promise) { + const reads = new Map>(); + return async (...args: A): Promise => { + const key = JSON.stringify(args); + let pending = reads.get(key); + if (!pending) { + pending = Promise.resolve().then(() => read(...args)); + reads.set(key, pending); + void pending.catch(() => { if (reads.get(key) === pending) reads.delete(key); }); + } + return structuredClone(await pending); + }; + } + return { + // Delegate explicitly: a read port may be a class with prototype methods. + currentRevision: () => source.currentRevision(), + queryUnifiedResults: query => source.queryUnifiedResults(query), + listGpoPolicyAnalyses: projectId => source.listGpoPolicyAnalyses(projectId), + listReferenceRuns: projectId => source.listReferenceRuns(projectId), + getEnvironmentSnapshot: query => source.getEnvironmentSnapshot(query), + listPolicyEvaluations: memoize(projectId => source.listPolicyEvaluations(projectId)), + ...(source.listAssessments ? { listAssessments: memoize(projectId => source.listAssessments!(projectId)) } : {}), + ...(source.loadAssessmentModel ? { loadAssessmentModel: memoize(assessmentId => source.loadAssessmentModel!(assessmentId)) } : {}), + ...(source.listMicrosoftSecurityBaselineAnalyses ? { + listMicrosoftSecurityBaselineAnalyses: memoize(baselineId => source.listMicrosoftSecurityBaselineAnalyses!(baselineId)) + } : {}) + }; +} + const fieldOf = (record: FindingRecord, label: string) => record.fields.find(field => field.label.trim().toLowerCase() === label)?.value?.trim() ?? ''; @@ -68,12 +99,13 @@ async function rawDataRecords(port: ContextReadPort, scope: AssessmentContextSco * state: a revision change during the read repeats it instead of mixing states. Nothing is written. */ export async function createAssessmentContext( - port: ContextReadPort, + source: ContextReadPort, scope: AssessmentContextScope, catalog: AssessmentContextCatalog = RELEASED_ASSESSMENT_CONTEXT_CATALOG, attempts = 3 ): Promise { for (let attempt = 0; attempt < attempts; attempt += 1) { + const port = reuseContextReads(source); const before = await port.currentRevision(); const [observations, scannerFindings, evaluations, analyses, referenceRuns, storedEvaluations] = await Promise.all([ queryAllUnifiedResults(query => port.queryUnifiedResults(query), { diff --git a/src/components/ActivityCenter.tsx b/src/components/ActivityCenter.tsx index af513b7..2520643 100644 --- a/src/components/ActivityCenter.tsx +++ b/src/components/ActivityCenter.tsx @@ -1,25 +1,82 @@ import { useEffect, useState, useSyncExternalStore } from 'react'; -import { activitySnapshot, isActiveActivity, clearFinishedActivities, subscribeActivities } from '../platform/activity-store'; +import { activitySnapshot, activityStatusSnapshot, isActiveActivity, clearCompletedActivities, dismissActivity, subscribeActivities, type Activity } from '../platform/activity-store'; -export function ActivityCenter() { +function duration(item: Activity, now: number) { + const ms = Math.max(0, (item.endedAt ?? now) - item.startedAt); + return ms < 1000 ? '<1s' : `${Math.floor(ms / 1000)}s`; +} +function ActivityRow({ item, now }: { item: Activity; now: number }) { + const state = { queued: 'Waiting', running: 'Running', completed: 'Completed', failed: 'Failed', cancelled: 'Cancelled' }[item.state]; + return
  • +
    {item.label}{item.area}
    +
    {item.state === 'failed' ? '! ' : ''}{state} · {duration(item, now)} + {(item.state === 'failed' || item.state === 'cancelled') && } +
    +
  • ; +} + +export function ActivityCenter({ onReload = () => window.location.reload() }: { onReload?: () => void } = {}) { const activities = useSyncExternalStore(subscribeActivities, activitySnapshot, activitySnapshot); - const running = activities.filter(item => isActiveActivity(item)); - const failed = activities.filter(item => item.state === 'failed').length; + const status = useSyncExternalStore(subscribeActivities, activityStatusSnapshot, activityStatusSnapshot); + const active = activities.filter(isActiveActivity); + const history = activities.filter(item => !isActiveActivity(item)); const [expanded, setExpanded] = useState(true); + const [historyExpanded, setHistoryExpanded] = useState(false); const [now, setNow] = useState(Date.now()); - useEffect(() => { if (!running.length) return; const timer = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(timer); }, [running.length]); - if (!activities.length) return null; - const waiting = running.filter(item => item.state === 'queued').length; - const summary = running.length ? `${running.length - waiting} running${waiting ? ` · ${waiting} waiting` : ''}` : failed ? `${failed} failed` : 'All tasks finished'; + useEffect(() => { if (!active.length) return; const timer = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(timer); }, [active.length]); + useEffect(() => { + if (!status.generation) return; + setExpanded(true); + setHistoryExpanded(false); + if (!status.ready) return; + const timer = setTimeout(() => { setExpanded(false); setHistoryExpanded(false); }, 5000); + return () => clearTimeout(timer); + }, [status.ready, status.generation]); + if (!activities.length && !status.generation) return null; + const waiting = active.filter(item => item.state === 'queued').length; + const summary = active.length + ? [active.length - waiting ? `${active.length - waiting} running` : '', waiting ? `${waiting} waiting` : ''].filter(Boolean).join(' · ') + : status.ready ? 'Ready' : status.failed ? 'Not ready · Loading incomplete' : status.cancelled ? 'Not ready · Loading cancelled' : 'No active tasks'; + const reloadControl = !active.length && (status.failed || status.cancelled) && !status.ready + ? : null; + if (!expanded) return
    + + {reloadControl} +
    ; + const groups = new Map(); + for (const item of [...history].reverse()) { + const key = JSON.stringify([item.area, item.label, item.state]); + groups.set(key, [...(groups.get(key) ?? []), item]); + } return ; } diff --git a/src/components/FilterEmptyState/FilterEmptyState.tsx b/src/components/FilterEmptyState/FilterEmptyState.tsx new file mode 100644 index 0000000..95ebbba --- /dev/null +++ b/src/components/FilterEmptyState/FilterEmptyState.tsx @@ -0,0 +1,34 @@ +import type { ReactNode } from 'react'; + +interface FilterEmptyStateProps { + title: string; + children: ReactNode; + onReset(): void; + onSearchAll(): void; + className?: string; +} + +/** A small, local SVG companion for a completed search with no matches. */ +export function FilterEmptyState({ title, children, onReset, onSearchAll, className = '' }: FilterEmptyStateProps) { + return
    + +

    {title}

    +

    {children}

    +
    + + +
    +
    ; +} diff --git a/src/domain/client-information.ts b/src/domain/client-information.ts new file mode 100644 index 0000000..3499aee --- /dev/null +++ b/src/domain/client-information.ts @@ -0,0 +1,123 @@ +import type { EnvironmentCollectionState, EnvironmentObservation, EnvironmentSnapshot, EnvironmentSnapshotQuery, ScannerReportSection } from './environment-context'; +import { environmentCoverage } from './environment-context'; +import { cloudInformationFacts, type CloudInformationFact } from './cloud-information'; + +/** Published Core contract. Internal analysis data: Result DB/export disclosure still applies. */ +export const CLIENT_INFORMATION_VERSION = '1.1.0'; +export const CLIENT_INFORMATION_AREAS = { + directory: 'Directory', inventory: 'Inventory', resilience: 'Resilience', + 'password-policy': 'Password policy', certificates: 'Certificates & PKI', + 'cloud-access': 'Cloud sign-in configuration', 'cloud-roles': 'Cloud role assignments', + 'cloud-apps': 'Cloud application consent', 'cloud-sharing': 'Cloud external access & sharing', + 'source-metadata': 'Source metadata', other: 'Other retained information' +} as const; +export type ClientInformationArea = keyof typeof CLIENT_INFORMATION_AREAS; + +export interface ClientInformationQuery extends EnvironmentSnapshotQuery { + areas?: ClientInformationArea[]; + properties?: string[]; + subjectTypes?: string[]; + subjectIds?: string[]; + toolIds?: string[]; + states?: EnvironmentCollectionState[]; + search?: string; + offset?: number; + limit?: number; + sectionOffset?: number; + sectionLimit?: number; +} + +export interface ClientInformationResult extends EnvironmentSnapshot { + /** Source-backed facts for the full filtered scope, independent of page size. */ + cloudFacts: CloudInformationFact[]; + contractVersion: typeof CLIENT_INFORMATION_VERSION; + purpose: 'internal-analysis'; + repositoryRevision: number; + scopeMode: 'current-imports' | 'including-history'; + totalObservations: number; + totalSections: number; + offset: number; + limit: number; + sectionOffset: number; + sectionLimit: number; + /** Additional exact source-link targets, included beyond the primary section page. */ + linkedSectionIds: string[]; +} + +export function clientInformationArea(item: EnvironmentObservation | ScannerReportSection): ClientInformationArea { + const declared = item.attributes.clientInformationArea; + if (typeof declared === 'string' && Object.hasOwn(CLIENT_INFORMATION_AREAS, declared)) return declared as ClientInformationArea; + const property = 'property' in item ? item.property : ''; + const subject = 'subject' in item ? item.subject.type : ''; + const section = item.sectionKey?.toLowerCase() ?? ''; + if (/^cloud\.(?:authentication|conditional_access)\./.test(property)) return 'cloud-access'; + if (/^cloud\.roles\./.test(property)) return 'cloud-roles'; + if (/^cloud\.consent\./.test(property)) return 'cloud-apps'; + if (/^cloud\.(?:guests|sharing)\./.test(property)) return 'cloud-sharing'; + if (/^(backup|kerberos\.krbtgt|laps|logging\.wef)\./.test(property) || property === 'domain.recycle_bin_enabled') return 'resilience'; + if (/^(domain|forest|entra)\./.test(property) || /^domain-information(?:\.|$)/.test(subject) || section === 'sectiondomaininformation') return 'directory'; + if (/^(host|operating_system)\./.test(property) || /^(computer-information|user-information|admin-groups|trusts-details)(?:\.|$)/.test(subject) + || ['sectioncomputerinformation', 'sectionuserinformation', 'sectionadmingroups', 'sectiontrustsdetails'].includes(section)) return 'inventory'; + if (/^password-policies(?:\.|$)/.test(subject) || section === 'sectionpasswordpolicies') return 'password-policy'; + if (/^pki(?:\.|$)/.test(subject) || section === 'sectionpki') return 'certificates'; + if (/^(report|risk|purple_knight|benchmark|maester)\./.test(property) + || ['maester-report', 'purple-knight-report', 'purple-knight-assessment', 'benchmark-assessment'].includes(subject) + || /^(?:maester-context-|purple-knight-assessment-summary|cis-cat-)/.test(section)) return 'source-metadata'; + return 'other'; +} + +const includes = (values: readonly string[] | undefined, value: string | undefined) => !values?.length || values.includes(value ?? ''); +const sectionIdentity = (item: { scanRunId: string; importFileId: string; sectionKey?: string }) => + JSON.stringify([item.scanRunId, item.importFileId, item.sectionKey]); +const matchesSearch = (search: string, values: unknown[]) => !search || values.some(value => + (typeof value === 'string' ? value : JSON.stringify(value) ?? '').toLocaleLowerCase('en-US').includes(search)); + +/** Deterministic selection only: never merges observations, infers configuration or resolves conflicts. */ +export function selectClientInformation(snapshot: EnvironmentSnapshot, query: ClientInformationQuery, repositoryRevision: number): ClientInformationResult { + const search = query.search?.trim().toLocaleLowerCase('en-US') ?? ''; + const observations = snapshot.observations.filter(item => + includes(query.areas, clientInformationArea(item)) && includes(query.properties, item.property) + && includes(query.subjectTypes, item.subject.type) && includes(query.subjectIds, item.subject.identity) + && includes(query.toolIds, item.tool.id) && includes(query.states, item.state) + && matchesSearch(search, [item.label, item.property, item.subject, item.originalValue])); + observations.sort((a, b) => a.property.localeCompare(b.property, 'en') || a.id.localeCompare(b.id, 'en')); + const observedSections = new Set(observations.map(sectionIdentity)); + const precise = Boolean(query.properties?.length || query.subjectTypes?.length || query.subjectIds?.length); + // Related sections remain complete source evidence, not a second set of matching observations. + const sections = snapshot.sections.filter(item => observedSections.has(sectionIdentity(item)) || (!precise + && includes(query.areas, clientInformationArea(item)) && includes(query.toolIds, item.tool.id) && includes(query.states, item.state) + && matchesSearch(search, [item.label, item.prose, item.tables]))); + sections.sort((a, b) => a.ordinal - b.ordinal || a.id.localeCompare(b.id, 'en')); + const offset = query.offset ?? 0; + const limit = query.limit ?? 50; + const sectionOffset = query.sectionOffset ?? 0; + const sectionLimit = query.sectionLimit ?? 10; + const page = sections.slice(sectionOffset, sectionOffset + sectionLimit); + const linkedSectionIds: string[] = []; + // Page primary sections first, then retain unambiguous native link targets in the same run/file. + const selected = new Set(page.map(sectionIdentity)); + const byIdentity = new Map(); + for (const item of snapshot.sections) { + const id = sectionIdentity(item); + byIdentity.set(id, [...(byIdentity.get(id) ?? []), item]); + } + for (let index = 0; index < page.length; index++) { + const parent = page[index]; + const targets = [...(parent.detailLinks ?? []).map(link => link.targetId), + ...parent.tables.flatMap(table => (table.cellLinks ?? []).map(link => link.targetId))]; + for (const target of targets) { + const id = sectionIdentity({ ...parent, sectionKey: target }); + const matches = byIdentity.get(id); + if (matches?.length === 1 && !selected.has(id)) { + selected.add(id); page.push(matches[0]); linkedSectionIds.push(matches[0].id); + } + } + } + return structuredClone({ + ...snapshot, contractVersion: CLIENT_INFORMATION_VERSION, purpose: 'internal-analysis', repositoryRevision, + scopeMode: query.includeSuperseded ? 'including-history' : 'current-imports', + totalObservations: observations.length, totalSections: sections.length, offset, limit, sectionOffset, sectionLimit, + observations: observations.slice(offset, offset + limit), sections: page, linkedSectionIds, + coverage: environmentCoverage(sections, observations), cloudFacts: cloudInformationFacts({ observations }) + }); +} diff --git a/src/domain/cloud-information.ts b/src/domain/cloud-information.ts new file mode 100644 index 0000000..d95afeb --- /dev/null +++ b/src/domain/cloud-information.ts @@ -0,0 +1,164 @@ +import type { ClientInformationArea } from './client-information'; +import type { EnvironmentObservation, EnvironmentSnapshot } from './environment-context'; +import { extractMarkdownTables } from '../shared/markdown'; + +/** Read-only semantic projection of retained native result fields, not live tenant state. */ +export const CLOUD_INFORMATION_MAPPING_VERSION = '1.0.0'; +export interface CloudInformationFact { + property: string; + area: ClientInformationArea; + label: string; + value: string | number; + displayValue: string; + scope: 'reported-setting' | 'reported-subset'; + observationId: string; + scanRunId: string; + importFileId: string; + sourceRuleId: string; + subjectIdentity?: string; + observedAt?: string; + source: EnvironmentObservation['source']; + mappingVersion: typeof CLOUD_INFORMATION_MAPPING_VERSION; +} + +// Exact IDs and native output templates verified against official Maester generators. +// Offline semantics and public sources: docs/CLIENT_INFORMATION.md. +const SETTINGS: Record = { + 'EIDSCA.AF01': ['cloud-access', 'cloud.authentication.fido2.state', 'FIDO2'], + 'EIDSCA.AM01': ['cloud-access', 'cloud.authentication.authenticator.state', 'Authenticator'], + 'EIDSCA.AT01': ['cloud-access', 'cloud.authentication.temporary_access_pass.state', 'Temporary Access Pass'], + 'EIDSCA.AV01': ['cloud-access', 'cloud.authentication.voice.state', 'Voice authentication'], + 'EIDSCA.AP08': ['cloud-apps', 'cloud.consent.user_policy', 'User consent'], + 'EIDSCA.CR01': ['cloud-apps', 'cloud.consent.admin_request.enabled', 'Admin consent requests'], + 'EIDSCA.AP10': ['cloud-apps', 'cloud.consent.app_registration.allowed', 'App registration'], + 'EIDSCA.AP04': ['cloud-sharing', 'cloud.sharing.guest_invitation', 'Guest invitations'], + 'EIDSCA.AP07': ['cloud-sharing', 'cloud.sharing.guest_role', 'Guest access'], + 'EIDSCA.ST08': ['cloud-sharing', 'cloud.sharing.guest_group_owner.allowed', 'Guest group owners'], + 'EIDSCA.ST09': ['cloud-sharing', 'cloud.sharing.guest_group_access.allowed', 'Guest group access'] +}; + +function nativeSetting(text: string): string | undefined { + const first = text.trim(); + if (/^Your tenant is \*\*not configured explicitly\*\*\./.test(first)) return 'not configured explicitly'; + const actual = /^Your tenant is configured as \*\*([^*\r\n]+)\*\*\./.exec(first) + ?? /^Well done\. The configuration in your tenant and recommended value is \*\*'([^*\r\n]+)'\*\* for \*\*/.exec(first); + return actual?.[1].trim() || undefined; +} + +function displaySetting(id: string, value: string): string { + const key = value.toLowerCase(); + if (key === 'true') return 'enabled'; + if (key === 'false') return 'disabled'; + if (id === 'EIDSCA.AP08') { + if (key === 'managepermissiongrantsforself.microsoft-user-default-low') return 'low-impact permissions only'; + if (key === 'managepermissiongrantsforself.microsoft-user-default-legacy') return 'all non-admin permissions'; + } + if (id === 'EIDSCA.AP07') { + if (key === '2af84b1e-32c8-42b7-82bc-daa82404023b') return 'Restricted Guest User'; + if (key === '10dae51f-b6af-4016-8d66-8c2a99b929b3') return 'Guest User'; + } + return value; +} + +/** Each fact retains its own source/scope. Never add across tests, runs or tenants. */ +export function cloudInformationFacts(snapshot: Pick | null | undefined): CloudInformationFact[] { + const facts: CloudInformationFact[] = []; + for (const observation of snapshot?.observations ?? []) { + if (observation.state !== 'observed' || observation.tool.id !== 'maester' + || observation.attributes.informationKind !== 'reported-test-evidence' + || typeof observation.originalValue !== 'string') continue; + const id = observation.attributes.sourceRuleId; + if (typeof id !== 'string') continue; + const text = observation.originalValue; + const add = (area: ClientInformationArea, property: string, label: string, value: string | number, + displayValue = String(value), scope: CloudInformationFact['scope'] = 'reported-setting') => { + facts.push({ area, property, label, value, displayValue, scope, observationId: observation.id, + scanRunId: observation.scanRunId, importFileId: observation.importFileId, sourceRuleId: id, + subjectIdentity: observation.subject.identity, observedAt: observation.observedAt, + source: observation.source, mappingVersion: CLOUD_INFORMATION_MAPPING_VERSION }); + }; + const setting = SETTINGS[id]; + if (setting) { + const value = nativeSetting(text); + if (value !== undefined) add(...setting, value, displaySetting(id, value)); + // Only an explicit actual-value column is configuration evidence. Expected values are ignored. + for (const table of extractMarkdownTables(text)) { + if (table.headers.length !== 2 || table.headers[0] !== 'Setting' || table.headers[1] !== 'Actual Value') continue; + if (table.rows.length === 1 && table.rows[0][0] === setting[2] && table.rows[0][1].trim()) { + const raw = table.rows[0][1].trim(); add(...setting, raw, displaySetting(id, raw)); + } + } + } + if (id === 'CISA.MS.AAD.7.4') { + if (text.trim() === 'Well done. Your tenant has no active assignments without expiration to privileged roles.') { + add('cloud-roles', 'cloud.roles.permanent.reported_count', 'Reported permanent assignments', 0, '0', 'reported-subset'); + } else if (text.trim().startsWith('Your tenant has active assignments without expiration to privileged roles.')) { + for (const table of extractMarkdownTables(text)) { + if (table.headers.join('|') !== 'Role|Principal Type|Display Name|Status' || !table.rows.length + || !table.rows.every(row => row.length === 4 && row.slice(0, 3).every(cell => cell.trim()) && row[3].trim() === '❌ No Expiration')) continue; + add('cloud-roles', 'cloud.roles.permanent.reported_count', 'Reported permanent assignments', table.rows.length, String(table.rows.length), 'reported-subset'); + } + } + } + // The helper also uses this sentence for other alerts; only MT.1031 has PIM-management semantics. + if (id === 'MT.1031' && text.trim() === 'All privileged role assignments are managed by PIM. Well done!') { + add('cloud-roles', 'cloud.roles.pim_management', 'Reported privileged roles', 'managed by PIM'); + } + if (id === 'MT.1007' && text.trim() === 'No Conditional Access policy requires multi-factor authentication for all users.') { + add('cloud-access', 'cloud.conditional_access.all_user_mfa.reported_count', 'Reported all-user MFA policies', 0, '0', 'reported-subset'); + } + if (id === 'MT.1007' && text.trim().startsWith('The following Conditional Access policies require multi-factor authentication for all users:')) { + const lines = text.trim().split(/\r?\n/).slice(1).filter(line => line.trim()); + const links = lines.map(line => /^\s*- \[[^\]\r\n]+\]\(https:\/\/[^\s)]+#view\/Microsoft_AAD_ConditionalAccess\/PolicyBlade\/policyId\/([^\s)]+)\)\s*$/.exec(line)); + if (links.length && links.every(link => link !== null)) { + const count = new Set(links.map(link => link![1])).size; + add('cloud-access', 'cloud.conditional_access.all_user_mfa.reported_count', 'Reported all-user MFA policies', count, String(count), 'reported-subset'); + } + } + if (['MT.1025', 'MT.1026', 'MT.1027', 'MT.1028'].includes(id)) { + const types: Record = { 'MT.1025': ['ExternalUser', 'external'], 'MT.1026': ['HybridUser', 'hybrid'], + 'MT.1027': ['ServicePrincipalClientSecret', 'app secret'], 'MT.1028': ['UserMailbox', 'mail-enabled'] }; + const [type, label] = types[id]; + if (text.trim().startsWith(`These directory role assignments for ${type} exists:`)) { + const lines = text.trim().split(/\r?\n/).slice(1).filter(line => line.trim()); + if (lines.length && lines.every(line => /^\s*- \[[^\]\r\n]+\]\(https:\/\/[^\s)]+\) with .+ on scope .+\s*$/.test(line))) { + add('cloud-roles', `cloud.roles.${type.toLowerCase()}.reported_count`, `Reported ${label} assignments`, lines.length, String(lines.length), 'reported-subset'); + } + } + } + if (id === 'CISA.MS.SHAREPOINT.1.1') { + const current = /^\* Current setting: `([^`\r\n]+)`\s*$/m.exec(text); + if (current && text.trim().startsWith('Your tenant does not restrict SharePoint Online sharing.')) { + add('cloud-sharing', 'cloud.sharing.sharepoint.capability', 'SharePoint sharing', current[1]); + } + } + } + return facts.sort((a, b) => a.property.localeCompare(b.property, 'en') || a.observationId.localeCompare(b.observationId, 'en') + || String(a.value).localeCompare(String(b.value), 'en')); +} + +/** One compact overview per area; all facts remain available in the published read result. */ +export function cloudInformationOverview(facts: CloudInformationFact[], area: ClientInformationArea): string[] { + const selected = facts.filter(fact => fact.area === area); + const scopes = new Set(selected.map(fact => JSON.stringify([fact.scanRunId, fact.subjectIdentity ?? fact.importFileId]))); + if (scopes.size > 1) return ['Multiple scan scopes — see Results']; + const properties = [...new Set(selected.map(fact => fact.property))]; + const priority = ['cloud.authentication.fido2.state', 'cloud.consent.admin_request.enabled', 'cloud.sharing.guest_role']; + properties.sort((a, b) => { + const rank = (value: string) => priority.includes(value) ? priority.indexOf(value) : priority.length; + return rank(a) - rank(b) || a.localeCompare(b, 'en'); + }); + // A conflicting value must never disappear behind the compact default selection. + const conflict = properties.find(property => new Set(selected.filter(fact => fact.property === property).map(fact => fact.displayValue)).size > 1); + return (conflict ? [conflict] : properties.slice(0, 1)).map(property => { + const values = selected.filter(fact => fact.property === property); + const variants = [...new Set(values.map(fact => fact.displayValue))]; + const fact = values[0]; + if (variants.length > 1) return `${fact.label}: conflicting source values`; + if (property === 'cloud.roles.permanent.reported_count') return `Permanent: ${fact.value} reported assignments`; + if (property === 'cloud.consent.admin_request.enabled') return `Admin requests: ${fact.displayValue}`; + if (property === 'cloud.sharing.guest_role' && fact.displayValue === 'Restricted Guest User') return 'Guests: restricted role'; + if (property === 'cloud.sharing.guest_role' && fact.displayValue === 'Guest User') return 'Guests: standard guest role'; + return `${fact.label}: ${variants[0]}`; + }); +} diff --git a/src/domain/environment-context.ts b/src/domain/environment-context.ts index 37e8ee5..bb0a19b 100644 --- a/src/domain/environment-context.ts +++ b/src/domain/environment-context.ts @@ -93,6 +93,8 @@ export interface EnvironmentSnapshotQuery { projectId: string; assessmentId?: string; scanRunIds?: string[]; + /** Explicit opt-in to retained replaced imports; ordinary reads use current imports. */ + includeSuperseded?: boolean; } export function environmentCoverage( diff --git a/src/domain/processing-compatibility.ts b/src/domain/processing-compatibility.ts index a19df44..50dbc41 100644 --- a/src/domain/processing-compatibility.ts +++ b/src/domain/processing-compatibility.ts @@ -39,7 +39,8 @@ export const PROCESSING_VERSION_MANIFESTS: readonly ProcessingVersionManifest[] { profileId: 'cis-cat', profileVersion: '1.3.0', mappingVersion: '1.3.0', normalizationVersion: '1.3.0', presentationVersion: '1.3.0', compatibilityKey: 'cis-cat-canonical-v1', changeClass: 'compatible', reimportRequired: false, affectedDimensions: ['evidence', 'presentation'], current: false }, { profileId: 'cis-cat', profileVersion: '1.4.0', mappingVersion: '1.3.0', normalizationVersion: '1.3.0', presentationVersion: '1.3.0', compatibilityKey: 'cis-cat-canonical-v1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context'], current: true }, { profileId: 'maester', profileVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0', compatibilityKey: 'maester-canonical-v1', changeClass: 'compatible', reimportRequired: false, affectedDimensions: [], current: false }, - { profileId: 'maester', profileVersion: '1.1.0', mappingVersion: '1.1.0', normalizationVersion: '1.0.0', presentationVersion: '1.1.0', compatibilityKey: 'maester-canonical-v1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['evidence', 'environment-context', 'presentation'], current: true } + { profileId: 'maester', profileVersion: '1.1.0', mappingVersion: '1.1.0', normalizationVersion: '1.0.0', presentationVersion: '1.1.0', compatibilityKey: 'maester-canonical-v1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['evidence', 'environment-context', 'presentation'], current: false }, + { profileId: 'maester', profileVersion: '1.2.0', mappingVersion: '1.1.0', normalizationVersion: '1.0.0', presentationVersion: '1.1.0', compatibilityKey: 'maester-canonical-v1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context', 'recognition'], current: true } ] as const; const manifestKey = (profileId: string, profileVersion: string) => `${profileId.trim().toLocaleLowerCase('en-US')}@${profileVersion.trim()}`; diff --git a/src/evaluation/security-domains.ts b/src/evaluation/security-domains.ts new file mode 100644 index 0000000..34dd8a4 --- /dev/null +++ b/src/evaluation/security-domains.ts @@ -0,0 +1,183 @@ +import type { FindingRecord } from '../domain/finding'; +import type { UnifiedResultProjection } from '../domain/unified-result'; +import type { + SecurityDomainId, SecurityDomainMapping, SecurityDomainMethod, SecurityDomainRuleSet, SecurityDomainTier +} from '../profiles/security-domain-mapping'; +import { cisCatSecurityDomains } from '../profiles/cis-cat/cis-cat-security-domains'; +import { groupPolicySecurityDomains } from '../profiles/group-policy/group-policy-security-domains'; +import { maesterSecurityDomains } from '../profiles/maester/maester-security-domains'; +import { pingCastleSecurityDomains } from '../profiles/pingcastle/pingcastle-security-domains'; +import { purpleKnightSecurityDomains } from '../profiles/purple-knight/purple-knight-security-domains'; + +export type { SecurityDomainId } from '../profiles/security-domain-mapping'; + +/** + * The single Security Domain classifier for Work, Client Info and reports (user decision + * 2026-10-04). It reads only Raw Data fields of a record, assigns every check exactly one + * of six domains and never reads the outcome, title, description or remediation prose. + */ +export const SECURITY_DOMAIN_CLASSIFIER_VERSION = '2.0.0'; +export const SECURITY_DOMAIN_FACET_ID = 'derived:security-domain'; + +export const securityDomainCatalog = [ + { id: 'identity', label: 'Identity & Authentication', glyph: 'user' }, + { id: 'privilege', label: 'Privileged Access & Delegation', glyph: 'shield' }, + { id: 'infrastructure', label: 'AD Infrastructure & PKI', glyph: 'server' }, + { id: 'hardening', label: 'System Hardening', glyph: 'code' }, + { id: 'cloud', label: 'Cloud & Collaboration', glyph: 'cloud' }, + { id: 'recovery', label: 'Monitoring & Recovery', glyph: 'chart' } +] as const satisfies ReadonlyArray<{ id: SecurityDomainId; label: string; glyph: string }>; + +export const securityDomainMappings: ReadonlyArray = [ + cisCatSecurityDomains, groupPolicySecurityDomains, maesterSecurityDomains, + pingCastleSecurityDomains, purpleKnightSecurityDomains +]; + +/** A source without a declared mapping is still assigned, visibly, by this global fallback. */ +const GLOBAL_FALLBACK: SecurityDomainId = 'hardening'; + +export interface SecurityDomainAssignment { + domainId: SecurityDomainId; + tier: SecurityDomainTier; + method: SecurityDomainMethod | 'source-fallback' | 'global-fallback'; + /** Raw Data field and the exact value that decided; absent for a pure source fallback. */ + field?: string; + value?: string; + profileId?: string; + mappingVersion?: string; +} + +/** Read access to the fields Raw Data shows, by case-insensitive label. */ +export type SecurityDomainFieldReader = (label: string) => string; + +const fold = (value: string) => value.trim().toLocaleLowerCase('en-US'); +const compact = (value: string) => fold(value).replace(/\s+/g, ''); +const HIVE_PREFIX = /^(?:hkey_local_machine|hkey_current_user|hklm|hkcu|machine|user)\\/i; + +function mappingForTool(tool: string): SecurityDomainMapping | undefined { + const name = compact(tool.split(' · ')[0] ?? ''); + if (!name) return undefined; + return securityDomainMappings.find(mapping => mapping.toolNames.some(candidate => name.startsWith(compact(candidate)))); +} + +function matchRuleSet(ruleSet: SecurityDomainRuleSet, raw: string): SecurityDomainId | undefined { + const value = ruleSet.method === 'registry-key' ? raw.trim().replace(HIVE_PREFIX, '') : raw.trim(); + const folded = fold(value); + switch (ruleSet.match) { + case 'exact': + return ruleSet.entries.find(([key]) => fold(key) === folded)?.[1]; + case 'list-item': { + // Plain lists and JSON-style tag arrays (`["ORCA","EXO"]`) alike. + const items = new Set(value.split(/[,;\n]/).map(item => fold(item.replace(/^[\s["']+|[\s\]"']+$/g, ''))).filter(Boolean)); + return ruleSet.entries.find(([key]) => items.has(fold(key)))?.[1]; + } + case 'prefix': { + let best: readonly [string, SecurityDomainId] | undefined; + for (const entry of ruleSet.entries) { + const key = fold(entry[0]); + const hit = ruleSet.boundary + ? folded === key || folded.startsWith(key + ruleSet.boundary.toLocaleLowerCase('en-US')) + : folded.startsWith(key); + if (hit && (!best || key.length > best[0].length)) best = [key, entry[1]]; + } + return best?.[1]; + } + case 'section-path': { + const segments = value.split(' > ').map(fold).filter(Boolean); + let best: { depth: number; domain: SecurityDomainId } | undefined; + for (const [key, domain] of ruleSet.entries) { + const keySegments = key.split(' > ').map(fold); + if (keySegments.length > segments.length) continue; + if (keySegments.every((segment, index) => segments[index].startsWith(segment)) + && (!best || keySegments.length > best.depth)) best = { depth: keySegments.length, domain }; + } + return best?.domain; + } + } +} + +/** + * Assigns one check to exactly one domain from its Raw Data fields; the first tier with a hit + * decides. `toolId` names the source of a single-tool sheet whose rows carry no `Source: Scan`. + */ +export function classifySecurityDomain(read: SecurityDomainFieldReader, toolId?: string): SecurityDomainAssignment { + const mapping = mappingForTool(read('Source: Scan') || toolId || ''); + if (!mapping) return { domainId: GLOBAL_FALLBACK, tier: 5, method: 'global-fallback' }; + const provenance = { profileId: mapping.profileId, mappingVersion: mapping.version }; + for (const ruleSet of mapping.ruleSets) { + if (ruleSet.when && fold(read(ruleSet.when.field)) !== fold(ruleSet.when.equals)) continue; + const labels: ReadonlyArray = typeof ruleSet.field === 'string' ? [ruleSet.field] : ruleSet.field; + for (const label of labels) { + const value = read(label); + const domainId = value ? matchRuleSet(ruleSet, value) : undefined; + if (domainId) return { domainId, tier: ruleSet.tier, method: ruleSet.method, field: label, value, ...provenance }; + } + } + const refinement = mapping.fallback.byField; + const refinementValue = refinement ? read(refinement.field) : ''; + const refined = refinement && refinementValue + ? refinement.entries.find(([key]) => fold(key) === fold(refinementValue))?.[1] : undefined; + return refined + ? { domainId: refined, tier: 5, method: 'source-fallback', field: refinement!.field, value: refinementValue, ...provenance } + : { domainId: mapping.fallback.domain, tier: 5, method: 'source-fallback', ...provenance }; +} + +/** The fields Raw Data shows for a record (`RawDataView`): every field with a value. */ +export function rawDataFieldReader(record: Pick): SecurityDomainFieldReader { + const values = new Map(); + for (const field of record.fields) { + const value = field.value?.trim(); + const label = fold(field.label); + if (value && !values.has(label)) values.set(label, value); + } + return label => values.get(fold(label)) ?? ''; +} + +const recordAssignmentCache = new WeakMap>(); + +export function securityDomainAssignmentForRecord(record: FindingRecord, toolId?: string): SecurityDomainAssignment { + let byTool = recordAssignmentCache.get(record); + if (!byTool) recordAssignmentCache.set(record, byTool = new Map()); + let cached = byTool.get(toolId ?? ''); + if (!cached) byTool.set(toolId ?? '', cached = classifySecurityDomain(rawDataFieldReader(record), toolId)); + return cached; +} + +export function securityDomainIdForRecord(record: FindingRecord, toolId?: string): SecurityDomainId { + return securityDomainAssignmentForRecord(record, toolId).domainId; +} + +/** + * Unified result without a loaded Work record: the same classifier over the structured + * values the result carries. Callers that have the Work record use it instead, so the + * report and Work agree on every record they share. + */ +export function securityDomainIdForUnifiedResult(result: UnifiedResultProjection): SecurityDomainId { + const values = new Map([ + ['source: scan', result.sourceKind.startsWith('policy-') ? 'group-policy' : result.sourceTool ?? ''], + ['source rule id', result.sourceRuleId ?? ''], + ['category', result.categories.length === 1 ? result.categories[0].categoryLabel : ''], + ['policy type', result.policyDeliveryMechanism ?? ''] + ]); + return classifySecurityDomain(label => values.get(fold(label))?.trim() ?? '').domainId; +} + +/** Assign every record once so distributions reconcile to the total. */ +export function securityDomainBuckets(records: FindingRecord[], toolId?: string): Map { + const buckets = new Map(securityDomainCatalog.map(domain => [domain.id, []])); + records.forEach(record => buckets.get(securityDomainIdForRecord(record, toolId))!.push(record)); + return buckets; +} + +/** Per source: how many checks were assigned on each tier; a high tier-5 share means the mapping table needs entries. */ +export function securityDomainTierCoverage(records: FindingRecord[]): Map> { + const coverage = new Map>(); + for (const record of records) { + const assignment = securityDomainAssignmentForRecord(record); + const source = assignment.profileId ?? 'unmapped'; + const tiers = coverage.get(source) ?? new Map(); + tiers.set(assignment.tier, (tiers.get(assignment.tier) ?? 0) + 1); + coverage.set(source, tiers); + } + return coverage; +} diff --git a/src/features/context-layer/ContextHeatmap.tsx b/src/features/context-layer/ContextHeatmap.tsx new file mode 100644 index 0000000..2c18c96 --- /dev/null +++ b/src/features/context-layer/ContextHeatmap.tsx @@ -0,0 +1,44 @@ +import { useId, useMemo } from 'react'; +import { useApp } from '../../app/AppProvider'; +import type { AssessmentContextResponse } from '../../api/assessor-api-contract'; +import { contextHeatmap, contextHeatmapFindingCount, contextSeverityLabels } from '../../assessment-context/context-heatmap'; +import { useAssessmentContext } from './use-assessment-context'; +import './context-heatmap.css'; + +/** Existing scoped Context request is shared with Work and the Finding Reader. */ +export function ClientContextHeatmap({ onOpenCluster }: { onOpenCluster(clusterId: string): void }) { + const app = useApp(); + const state = useAssessmentContext(Boolean(app.dataset), app.targetProjectId, app.activeAssessmentIds, app.activeScanRunIds, app.dataset); + if (state.status === 'off' || state.status === 'unavailable') return null; + if (state.status === 'loading') return
    +

    Context heatmap

    Loading Context…

    +
    ; + if (state.status === 'error') return
    +

    Context heatmap

    Context could not be loaded. Reopen the client to retry.

    +
    ; + return ; +} + +export function ContextHeatmap({ context, onOpenCluster }: { context: AssessmentContextResponse; onOpenCluster(clusterId: string): void }) { + const id = useId(); + const cells = useMemo(() => contextHeatmap(context), [context]); + const findingCount = useMemo(() => contextHeatmapFindingCount(context), [context]); + + return
    +
    +

    Context heatmap

    + Findings: {findingCount.toLocaleString('en-US')} +
    +
    + {cells.map(({ cluster: item, reference, hasEvidence, scannerDeviations, gpoDeviations }) => )} +
    +
    ; +} diff --git a/src/features/context-layer/context-cluster-facet.ts b/src/features/context-layer/context-cluster-facet.ts index e5dd8e8..2d8f069 100644 --- a/src/features/context-layer/context-cluster-facet.ts +++ b/src/features/context-layer/context-cluster-facet.ts @@ -1,6 +1,7 @@ import type { AssessmentContextResponse } from '../../api/assessor-api-contract'; import type { FindingRecord } from '../../domain/finding'; import type { DynamicFacet } from '../search-filter/dynamic-facets'; +import { publishContextReport } from '../../assessment-context/context-report-publication'; /** * Work facet of the context layer (Phase 13E, decision (c)). It exists only while the layer is on and @@ -10,6 +11,7 @@ export const CONTEXT_CLUSTER_FACET_ID = 'context:cluster'; const SOURCE_SCAN_FACET_ID = 'provenance:source-scan'; export interface ContextClusterIndex { + publication: ReturnType; facet: DynamicFacet; /** The loaded clusters with their state, for the Pathfinder details (Phase 13E). */ clusters: AssessmentContextResponse['clusters']; @@ -34,6 +36,7 @@ export function contextClusterIndex(context: AssessmentContextResponse): Context return identity ? bySetting.get(`${field(record, 'Scope')}\u0000${identity}`) ?? [] : []; }; const index: ContextClusterIndex = { + publication: publishContextReport(context), clusters: context.clusters, facet: { id: CONTEXT_CLUSTER_FACET_ID, diff --git a/src/features/context-layer/context-heatmap.css b/src/features/context-layer/context-heatmap.css new file mode 100644 index 0000000..83ab9cd --- /dev/null +++ b/src/features/context-layer/context-heatmap.css @@ -0,0 +1,25 @@ +.context-heatmap { margin-block: var(--space-6) var(--space-8); color: var(--text-primary); min-width: 0; } +.context-heatmap h2 { margin: 0; font-size: var(--type-h3-size); font-weight: var(--fw-semibold); } +.context-heatmap p { margin: var(--space-2) 0; font-size: var(--type-body-size); } +.context-heatmap__header { display: flex; align-items: baseline; justify-content: flex-start; gap: var(--space-4); flex-wrap: wrap; } +.context-heatmap__total { font-size: var(--type-body-size); font-weight: var(--fw-regular); color: var(--text-secondary); font-variant-numeric: tabular-nums; white-space: nowrap; } +.context-heatmap [data-severity="critical"] { --context-heat: var(--sev-critical); } +.context-heatmap [data-severity="high"] { --context-heat: var(--sev-high); } +.context-heatmap [data-severity="medium"] { --context-heat: var(--sev-medium); } +.context-heatmap__grid { + display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--tile-compact-width)), var(--tile-compact-width))); + gap: var(--space-2); margin-top: var(--space-3); grid-auto-rows: 1fr; +} +.context-heatmap__cell { + display: flex; flex-direction: column; align-items: start; gap: var(--space-2); + padding: var(--space-3); min-width: 0; min-height: calc(var(--tile-h) * .75); height: auto; + border: var(--bw-emphasis) solid transparent; border-radius: var(--radius-sm); + background: var(--context-heat, var(--surface-panel-raised)); color: var(--sev-ink); + font-family: inherit; text-align: left; cursor: pointer; overflow-wrap: anywhere; +} +.context-heatmap__cell[data-severity="neutral"], .context-heatmap__cell[data-severity="undetermined"] { color: var(--text-primary); } +.context-heatmap__cell:hover { border-color: currentColor; } +.context-heatmap__cell:focus-visible { outline: var(--bw-emphasis) solid var(--focus-ring); outline-offset: var(--bw-emphasis); } +.context-heatmap__cell strong { font-size: var(--type-body-size); font-weight: var(--fw-semibold); line-height: var(--lh-ui); } +.context-heatmap__category { font-size: var(--type-table-body-size); font-weight: var(--fw-regular); } +.context-heatmap__amount { margin-top: auto; font-size: var(--type-body-size); font-weight: var(--fw-semibold); } diff --git a/src/features/dashboard/ClientInformationBrowser.tsx b/src/features/dashboard/ClientInformationBrowser.tsx new file mode 100644 index 0000000..16fc6a7 --- /dev/null +++ b/src/features/dashboard/ClientInformationBrowser.tsx @@ -0,0 +1,111 @@ +import { useEffect, useState, type FormEvent } from 'react'; +import { AssessorApiClient } from '../../api/assessor-api-client'; +import { CLIENT_INFORMATION_AREAS, type ClientInformationArea, type ClientInformationQuery, type ClientInformationResult } from '../../domain/client-information'; +import type { EnvironmentObservation, EnvironmentSnapshotQuery } from '../../domain/environment-context'; +import { EnvironmentEvidenceTables } from './EnvironmentEvidenceTables'; +import { RichTextValue } from '../../components/RichTextValue/RichTextValue'; + +const api = new AssessorApiClient(); +const textValue = (value: unknown) => typeof value === 'string' ? value : JSON.stringify(value, null, 2) ?? '—'; +const preview = (item: EnvironmentObservation) => { + if (item.originalValue === '') return '""'; + if (item.originalValue !== null && typeof item.originalValue === 'object') return 'Structured source value — open details'; + const value = textValue(item.originalValue); + return value.length > 160 ? `${value.slice(0, 160)}…` : value; +}; + +/** On-demand UI consumer of the same published query contract engines use. */ +export function ClientInformationBrowser({ scope, areas }: { scope: EnvironmentSnapshotQuery; areas?: ClientInformationArea[] }) { + const [area, setArea] = useState(''); + const [search, setSearch] = useState(''); + const [property, setProperty] = useState(''); + const [subject, setSubject] = useState(''); + const [query, setQuery] = useState({ ...scope, areas }); + const [result, setResult] = useState(); + const [error, setError] = useState(false); + const [request, setRequest] = useState(0); + const [selected, setSelected] = useState(); + const scopeKey = JSON.stringify({ ...scope, areas }); + const queryKey = JSON.stringify(query); + useEffect(() => { + let active = true; + setResult(undefined); setError(false); setSelected(undefined); + api.queryClientInformation({ ...query, ...scope, areas: query.areas?.length ? query.areas : areas }).then(value => { + if (active) setResult(value); + }).catch(() => { if (active) setError(true); }); + return () => { active = false; }; + }, [scopeKey, queryKey, request]); + const submit = (event: FormEvent) => { + event.preventDefault(); + setQuery({ ...scope, areas: area ? [area as ClientInformationArea] : undefined, + search: search.trim() || undefined, properties: property.trim() ? [property.trim()] : undefined, + subjectIds: subject.trim() ? [subject.trim()] : undefined }); + setRequest(value => value + 1); + }; + return
    +

    Retained information for the selected client and scan scope. Source dates describe the imported state, not a live inventory.

    +
    + + + + + + +
    + {!result && !error &&

    Loading Client Information…

    } + {error &&

    Client Information could not be loaded.

    } + {result && <> +

    {result.totalObservations.toLocaleString('en-US')} matching observations · {result.totalSections.toLocaleString('en-US')} related source sections

    + {result.totalObservations === 0 &&

    No retained observations match this selection. Missing information does not mean the setting is disabled or the inventory is empty.

    } + {result.observations.length > 0 &&
    + + {result.observations.map(item => + + + )} +
    InformationOriginal valueCollection stateSourceDetails
    {item.label}{preview(item)}{item.state}{item.tool.name}
    } + {result.totalObservations > result.limit && } + {selected &&
    +

    {selected.label}

    + {(result.cloudFacts ?? []).filter(fact => fact.observationId === selected.id).map(fact =>

    + {fact.label}: {fact.displayValue} {fact.scope === 'reported-subset' && '(reported subset)'} +

    )} + {selected.attributes.informationKind === 'reported-test-evidence' &&

    Reported test evidence. It may describe only the objects examined by this test; it is not a complete configuration inventory.

    } +
    +
    Property
    {selected.property}
    +
    Object
    {selected.subject.label ?? selected.subject.identity ?? 'Not stated'}
    +
    Object ID
    {selected.subject.identity ?? 'Not stated'}
    +
    Observed at
    {selected.observedAt ?? 'Not stated by source'}
    +
    Scan run
    {selected.scanRunId}
    +
    Import file
    {selected.importFileId}
    +
    Source location
    {textValue(selected.source)}
    +
    Collection state
    {selected.state}
    +
    +

    Original value

    +
    Source metadata and normalized value
    {textValue({
    +          normalizedValue: selected.normalizedValue, subject: selected.subject, mappingVersion: selected.mappingVersion, attributes: selected.attributes
    +        })}
    +
    } +
    +

    Original source sections

    + + {result.totalSections > result.sectionLimit && } +
    +

    Retrieved {result.generatedAt} · Repository revision {result.repositoryRevision} · {result.scopeMode === 'including-history' ? 'Including historical imports' : 'Current imports'}

    + } +
    ; +} diff --git a/src/features/dashboard/CloudInformationTile.tsx b/src/features/dashboard/CloudInformationTile.tsx new file mode 100644 index 0000000..f5094c6 --- /dev/null +++ b/src/features/dashboard/CloudInformationTile.tsx @@ -0,0 +1,37 @@ +import type { ReactNode } from 'react'; +import { type ClientInformationArea } from '../../domain/client-information'; +import { cloudInformationFacts, cloudInformationOverview } from '../../domain/cloud-information'; +import type { EnvironmentSnapshot, EnvironmentSnapshotQuery } from '../../domain/environment-context'; +import { ClientInformationBrowser } from './ClientInformationBrowser'; +import { EnvironmentResultsButton } from './EnvironmentResultsButton'; +import { EnvironmentEvidenceTables } from './EnvironmentEvidenceTables'; + +const areas: Array<{ id: ClientInformationArea; label: string }> = [ + { id: 'cloud-access', label: 'Authentication & CA' }, + { id: 'cloud-roles', label: 'Role assignments' }, + { id: 'cloud-apps', label: 'App consent' }, + { id: 'cloud-sharing', label: 'External access' } +]; + +/** Concrete reported facts only; never derives configuration from test outcomes. */ +export function CloudInformationTile({ snapshot, scope, icon }: { + snapshot: EnvironmentSnapshot | null | undefined; scope?: EnvironmentSnapshotQuery; icon?: ReactNode; +}) { + const cloudFacts = cloudInformationFacts(snapshot); + const facts = areas.map(area => { + const values = cloudInformationOverview(cloudFacts, area.id); + return { ...area, values: values.length ? values : ['Not reported'] }; + }); + return
    +
    +

    {icon}Cloud Information

    + + {scope ? area.id)} /> + : } + +
    +
    {facts.map(fact =>
    +
    {fact.label}
    {fact.values.map(value =>
    {value}
    )}
    +
    )}
    +
    ; +} diff --git a/src/features/dashboard/DashboardPanel.tsx b/src/features/dashboard/DashboardPanel.tsx index eecb2e7..b5af6cf 100644 --- a/src/features/dashboard/DashboardPanel.tsx +++ b/src/features/dashboard/DashboardPanel.tsx @@ -1,4 +1,10 @@ import { readEnginePreferences, useEnginePreferences } from '../../app/engine-preferences'; +import { ClientContextHeatmap } from '../context-layer/ContextHeatmap'; +import { CONTEXT_CLUSTER_FACET_ID } from '../context-layer/context-cluster-facet'; +import { useAssessmentContext } from '../context-layer/use-assessment-context'; +import { publishContextReport } from '../../assessment-context/context-report-publication'; +import { managementFindingDistribution, managementWorkspaceDeviationKeys } from '../../reporting/management-business-impact'; +import { businessImpactControls, BUSINESS_IMPACT_FACET_ID } from '../search-filter/business-impact-controls'; import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from 'react'; import { AssessorApiClient } from '../../api/assessor-api-client'; import { hasSelectedTimeslotScope } from '../../app/work-scope'; @@ -6,8 +12,10 @@ import { downloadText } from '../../exports/text-download'; import { browserDownloadAdapter } from '../../platform/browser/browser-download-adapter'; import { OperatingSystemInventory } from './OperatingSystemInventory'; import { EnvironmentResultsButton } from './EnvironmentResultsButton'; +import { CloudInformationTile } from './CloudInformationTile'; import { EnvironmentEvidenceTables } from './EnvironmentEvidenceTables'; -import type { EnvironmentEvidenceGroup } from './environment-result-evidence'; +import { environmentEvidenceForGroup, type EnvironmentEvidenceGroup } from './environment-result-evidence'; +import type { GpoBackupStatusRecord } from '../../api/assessor-api-contract'; import { operatingSystemInventory, type OperatingSystemInventoryGroup } from './operating-system-inventory'; import { useApp } from '../../app/AppProvider'; import { EmptyWorkspace } from '../../components/EmptyWorkspace/EmptyWorkspace'; @@ -251,8 +259,8 @@ function EnvironmentOverview({ projectId, snapshot, failed }: { projectId?: stri

    Directory overview

    {snapshot && {snapshot.sections.length.toLocaleString('en-US')} sections} - {!snapshot && !failed &&

    Loading scanner context…

    } - {failed &&

    Scanner context could not be loaded.

    } + {!snapshot && !failed &&

    Loading Client Information…

    } + {failed &&

    Client Information could not be loaded.

    } {snapshot && snapshot.sections.length === 0 &&

    This import predates structured scanner-context capture.

    } {snapshot && (
    @@ -1006,8 +1014,8 @@ function dashboardEnvironmentDomainCount(snapshot: EnvironmentSnapshot | undefin * zaehlte jede sonstige GUID mit. Die Verarbeitungsliste nennt die Zahl * dagegen selbst. */ -function useGpoInventory(projectId?: string): { gpoCount?: number; snapshots?: number } { - const [inventory, setInventory] = useState<{ gpoCount?: number; snapshots?: number }>({}); +function useGpoInventory(projectId?: string): { gpoCount?: number; snapshots?: number; records?: GpoBackupStatusRecord[] } { + const [inventory, setInventory] = useState<{ gpoCount?: number; snapshots?: number; records?: GpoBackupStatusRecord[] }>({}); useEffect(() => { let active = true; @@ -1018,7 +1026,7 @@ function useGpoInventory(projectId?: string): { gpoCount?: number; snapshots?: n if (!active) return; const processed = response.records.filter(record => record.processingStatus); const gpoCount = processed.reduce((sum, record) => sum + record.gpoCount, 0); - setInventory(processed.length ? { gpoCount, snapshots: processed.length } : {}); + setInventory(processed.length ? { gpoCount, snapshots: processed.length, records: processed } : {}); }) .catch(() => { if (active) setInventory({}); }); return () => { active = false; }; @@ -1141,8 +1149,24 @@ export function DashboardOverallRing({ score, onClick }: { score?: number; onCli :
    {content}
    ; } -function DashboardTopFact({ value, label, glyph }: { value?: number | string; label: string; glyph: DashboardGlyphName }) { - return
    {value ?? '—'}{label}
    ; +export function DashboardTopFact({ value, label, glyph, snapshot, evidenceGroup, backupRecords, fallback }: { + value?: number | string; label: string; glyph: DashboardGlyphName; snapshot?: EnvironmentSnapshot; + evidenceGroup: EnvironmentEvidenceGroup; backupRecords?: GpoBackupStatusRecord[]; fallback?: string; +}) { + const evidence = environmentEvidenceForGroup(snapshot, evidenceGroup); + return {value ?? '—'}{label}}> + + {fallback &&

    {fallback}

    } + {!!evidence.observations.length && + {evidence.observations.map(item => )} +
    FieldOriginal valueSourceScan run
    {item.label}{typeof item.originalValue === 'string' ? item.originalValue : JSON.stringify(item.originalValue)}{item.tool.name}{item.scanRunId}
    } + {!!backupRecords?.length && <>

    All processed client backups · the overview count includes every backup listed below.

    + + {backupRecords.map(record => )} +
    Backup fileGPOsProcessing statusUpdated
    {record.fileName}{record.gpoCount}{record.processingStatus}{record.updatedAt}
    } + +
    ; } function dashboardDomainAmount(domain: DashboardDomainScore): number { @@ -1167,6 +1191,28 @@ function DashboardDomainRow({ domain, distributionTotal, onClick }: { domain: Da return
  • {onClick ? :
    {content}
    }
  • ; } +const impactLabels = Object.fromEntries(businessImpactControls.map(theme => [theme.id, theme.label])); + +export function DashboardBusinessImpact({ distribution, loading, onSelect }: { + distribution?: ReturnType; loading: boolean; onSelect?: (themeId: string) => void; +}) { + const titleId = useId(); + const max = Math.max(1, ...(distribution?.themes.map(theme => theme.count) ?? [])); + return
    +

    Possible Business Impact

    + {distribution ?
    + {distribution.themes.map(theme =>
    + +
    )} +
    :

    {loading ? 'Loading Business Impact…' : 'Business Impact is unavailable for this assessment.'}

    } +
    ; +} + export interface DashboardNativeMetrics { pingCastleRisk?: number; purpleKnightPosture?: number; @@ -1313,7 +1359,7 @@ export function dashboardAnalysis( const state = dashboardResultState(record, activeProfile); states.set(record, state); }); - const domainBuckets = dashboardDomainAssignment(records); + const domainBuckets = dashboardDomainAssignment(records, activeSheet?.toolId); const scoreOf = (subset: FindingRecord[]) => dashboardScoreFromStates(subset.map(record => states.get(record) ?? 'unknown'), subset); @@ -1338,22 +1384,13 @@ export function dashboardAnalysis( ...score }; }); - const unclassifiedRecords = domainBuckets.get('unclassified') ?? NO_RECORDS; - const unclassifiedScore = scoreOf(unclassifiedRecords); - const unclassifiedDomain: DashboardDomainScore | undefined = unclassifiedRecords.length ? { - id: 'unclassified', - label: 'Unclassified', - glyph: 'scan', - records: unclassifiedRecords.filter(record => states.get(record) === 'deviation'), - reviewRecords: unclassifiedRecords.filter(record => ['review-required', 'reevaluation-required'].includes(states.get(record) ?? '')), - ...unclassifiedScore - } : undefined; const totalResult = dashboardScoreFromStates(records.map(record => states.get(record) ?? 'unknown'), records); const gapRecordIds = records.filter(record => !['passed', 'deviation', 'not-applicable'].includes(states.get(record) ?? 'unknown')) .map(record => record.id); return { total: records.length, + records, statusFacet, severityFacet, sourceFacet, @@ -1362,7 +1399,6 @@ export function dashboardAnalysis( statusCount: (value: string) => statusFacet?.values.find(item => item.value === value)?.count ?? 0, sourceScores, domains, - unclassifiedDomain, totalResult, gapRecordIds, overallScore: dashboardOverallScore(sourceScores) @@ -1378,6 +1414,12 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) { const sourceFilter = app.facetFilters['provenance:source-scan']; const analysis = useDashboardAnalysis(activeSheet, dataset, activeProfile, reviews, sourceFilter); const engines = useEnginePreferences(); + const impactContext = useAssessmentContext(Boolean(dataset) && engines.report, app.targetProjectId, app.activeAssessmentIds, app.activeScanRunIds, dataset); + const impactDistribution = useMemo(() => { + if (impactContext.status !== 'ready') return undefined; + const keys = managementWorkspaceDeviationKeys(analysis.records, record => dashboardResultState(record, activeProfile) === 'deviation'); + return managementFindingDistribution(publishContextReport(impactContext.context), analysis.totalResult.deviations, keys); + }, [impactContext.status === 'ready' ? impactContext.context : undefined, analysis, activeProfile]); const [reportExporting, setReportExporting] = useState<'management' | 'technical' | null>(null); const [reportError, setReportError] = useState(null); const reportScopeKey = JSON.stringify([app.targetProjectId, app.activeScanRunIds, app.activeAssessmentIds]); @@ -1386,6 +1428,10 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) { const exportReport = async (kind: 'management' | 'technical') => { const projectId = app.targetProjectId; if (!readEnginePreferences().report || !projectId || !dataset || !hasSelectedTimeslotScope(app.activeScanRunIds, app.activeAssessmentIds)) return; + if (kind === 'management' && !readEnginePreferences().context) { + setReportError('Management Report requires Context Engine. Enable it in Engines and try again.'); + return; + } const requestedScope = reportScopeKey; setReportExporting(kind); setReportError(null); @@ -1394,7 +1440,7 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) { const run = await assessorApi.createReportRun(projectId, { kind, scanRunIds: app.activeScanRunIds, assessmentIds: app.activeAssessmentIds }); - if (!readEnginePreferences().report || reportScopeRef.current !== requestedScope) return; + if (!readEnginePreferences().report || (kind === 'management' && !readEnginePreferences().context) || reportScopeRef.current !== requestedScope) return; downloadText(browserDownloadAdapter, run.fileName, run.html, 'text/html;charset=utf-8'); } catch (reason) { if (readEnginePreferences().report) setReportError(reason instanceof Error ? reason.message : 'Report could not be generated.'); @@ -1419,7 +1465,7 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) { const { total, statusFacet, severityFacet, sourceFacet, tools, reports, - sourceScores, domains, unclassifiedDomain, totalResult, gapRecordIds, overallScore + sourceScores, domains, totalResult, gapRecordIds, overallScore } = analysis; const deviations = totalResult.deviations; const assessmentCoverage = totalResult.coverage; @@ -1430,7 +1476,6 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) { const domainControllers = dashboardObservationNumber(environment.snapshot, 'domain.controller_count'); // Give repeated overview facts one home; Results retains the full category. // Compare only the same named fact, never unrelated facts with equal values. - const directoryDetailLabels = ['NetBIOS name', 'Schema version', 'Domain created', 'Entra ID']; const countOverview = new Map([ ['Domains', numberFact(dashboardEnvironmentDomainCount(environment.snapshot))], ['Domain controllers', numberFact(domainControllers)], @@ -1438,9 +1483,7 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) { ['Computer objects', numberFact(computerAccounts)] ]); const alreadyInData = (item: DashboardFact) => countOverview.get(item.label) === item.value; - const domainOverview = domainInformation.filter(item => !directoryDetailLabels.includes(item.label) && !alreadyInData(item)); - const directoryOverview = environmentDetail.directory.filter(item => - !alreadyInData(item) && !domainOverview.some(shown => shown.label === item.label && shown.value === item.value)); + const domainOverview = domainInformation.filter(item => !alreadyInData(item)); const inventoryOverview = environmentDetail.inventory.filter(item => !alreadyInData(item) && !domainOverview.some(shown => shown.label === item.label && shown.value === item.value)); const maesterSource = sourceScores.find(source => /maester/i.test(source.tool.label)); @@ -1468,7 +1511,7 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) { }; return rank(left.tool.label) - rank(right.tool.label) || left.tool.label.localeCompare(right.tool.label); }); - const displayedDomains = unclassifiedDomain?.deviations ? [...domains, unclassifiedDomain] : domains; + const displayedDomains = domains; const domainDistributionTotal = displayedDomains.reduce((sum, domain) => sum + dashboardDomainAmount(domain), 0); const overallTone = dashboardScoreTone(overallScore); @@ -1507,10 +1550,10 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) { setFacetFilter(sourceFacet.id, tool ? { values: tool.sourceValues } : null); }}>{selectedSource === 'selection' && }{tools.map(tool => )}
    - -
    @@ -1547,6 +1590,12 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) { setStatusFilter('all'); setPathFindingIds(domain.reviewRecords.map(record => record.id)); }) : undefined} />)} + {engines.report && engines.context && jump(() => { + const contextFilter = app.facetFilters[CONTEXT_CLUSTER_FACET_ID]; + if (contextFilter) setFacetFilter(CONTEXT_CLUSTER_FACET_ID, contextFilter); + setFacetFilter(BUSINESS_IMPACT_FACET_ID, { values: [themeId] }); + })} />} @@ -1566,30 +1615,48 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) { })}>{totalResult.unassessed.toLocaleString('en-US')}Gaps
    - - - - - - + + + + + +
    -

    Domain Information

    +
    +

    Domain Information

    + + + + +
    {domainOverview.map(item =>
    {item.label}
    {item.value}
    )}
    + jump(() => { + const targetSheet = sourceScanNavigationSheet(dataset, activeSheet); + if (targetSheet) { + app.setActiveSheet(targetSheet.id); + setStatusFilter('deviation'); + } + setFacetFilter(CONTEXT_CLUSTER_FACET_ID, { values: [clusterId] }); + }, false)} /> + {scoringOpen && setScoringOpen(false)} />}
    - } snapshot={environment.snapshot} /> + } + key={`cloud-information:${app.targetProjectId}:${app.activeAssessmentIds?.join(',')}:${app.activeScanRunIds?.join(',')}`} + scope={app.targetProjectId ? { projectId: app.targetProjectId, assessmentId: app.activeAssessmentIds?.length === 1 ? app.activeAssessmentIds[0] : undefined, + scanRunIds: app.activeScanRunIds } : undefined} />
    jump(() => setPathFindingIds([recordId]))} /> diff --git a/src/features/dashboard/EnvironmentEvidenceTables.tsx b/src/features/dashboard/EnvironmentEvidenceTables.tsx index 665824a..60f0702 100644 --- a/src/features/dashboard/EnvironmentEvidenceTables.tsx +++ b/src/features/dashboard/EnvironmentEvidenceTables.tsx @@ -287,7 +287,7 @@ function EvidenceSection({ section, snapshot, linkedSections }: { export function EnvironmentEvidenceTables({ snapshot, group }: { snapshot?: EnvironmentSnapshot; group: EnvironmentEvidenceGroup }) { const [visibleSections, setVisibleSections] = useState(PAGE_SIZE); - if (!snapshot) return

    No retained scanner context available for this selection.

    ; + if (!snapshot) return

    No retained Client Information available for this selection.

    ; const { sections } = environmentEvidenceForGroup(snapshot, group); const linkedSections = new Map(); for (const section of sections) { diff --git a/src/features/dashboard/EnvironmentResultsButton.tsx b/src/features/dashboard/EnvironmentResultsButton.tsx index 9a276c3..aef1a48 100644 --- a/src/features/dashboard/EnvironmentResultsButton.tsx +++ b/src/features/dashboard/EnvironmentResultsButton.tsx @@ -2,8 +2,9 @@ import { useRef, useState, type ReactNode } from 'react'; import { FloatingDataWindow } from '../../components/FloatingDataWindow/FloatingDataWindow'; /** One interaction for every Environment Detail tile, including OS inventory. */ -export function EnvironmentResultsButton({ title, children }: { +export function EnvironmentResultsButton({ title, children, trigger, triggerClassName }: { title: string; children: ReactNode | ((dismiss: () => void) => ReactNode); + trigger?: ReactNode; triggerClassName?: string; }) { const [windowOpen, setWindowOpen] = useState(false); const triggerRef = useRef(null); @@ -12,9 +13,9 @@ export function EnvironmentResultsButton({ title, children }: { requestAnimationFrame(() => triggerRef.current?.focus()); }; return <> - + aria-label={`Open ${title} results`} title="Open results">{trigger ?? 'Results'} {windowOpen && {typeof children === 'function' ? children(() => setWindowOpen(false)) : children} } diff --git a/src/features/dashboard/OperatingSystemInventory.tsx b/src/features/dashboard/OperatingSystemInventory.tsx index 0f38a88..6abeb8c 100644 --- a/src/features/dashboard/OperatingSystemInventory.tsx +++ b/src/features/dashboard/OperatingSystemInventory.tsx @@ -38,6 +38,8 @@ export function OperatingSystemInventory({ groups, icon, snapshot }: { const summary = groups.filter(group => group.total !== undefined).map(group => ({ label: group.label, value: quantity(group.total) })); const systems = groups.flatMap(group => group.entries).slice(0, Math.max(0, 4 - summary.length)) .map(entry => ({ label: entry.name, value: quantity(entry.count) })); + const inventoryFacts = summary.length || systems.length ? [...summary, ...systems] : [{ label: 'Inventory', value: '—' }]; + const facts = inventoryFacts; return

    {icon}Operating systems

    @@ -47,9 +49,9 @@ export function OperatingSystemInventory({ groups, icon, snapshot }: {
    - {summary.length || systems.length ? [...summary, ...systems].slice(0, 4).map((item, index) =>
    + {facts.map((item, index) =>
    {item.label}
    {item.value}
    -
    ) :
    Inventory
    —
    } +
    )}
    ; } diff --git a/src/features/dashboard/environment-result-evidence.ts b/src/features/dashboard/environment-result-evidence.ts index 71c3585..2f322b4 100644 --- a/src/features/dashboard/environment-result-evidence.ts +++ b/src/features/dashboard/environment-result-evidence.ts @@ -1,7 +1,9 @@ import type { EnvironmentObservation, EnvironmentSnapshot, ScannerReportSection, ScannerReportTable } from '../../domain/environment-context'; +import { isOperatingSystemEolLabel, isOperatingSystemEolObservation } from './operating-system-inventory'; export type EnvironmentEvidenceGroup = 'all' | 'directory' | 'resilience' | 'inventory' - | 'password-policy' | 'certificates' | 'source-metrics' | 'operating-systems'; + | 'password-policy' | 'certificates' | 'source-metrics' | 'operating-systems' + | 'ad-domains' | 'entra-tenants' | 'user-accounts' | 'computer-objects' | 'domain-controllers' | 'gpos'; export interface EnvironmentEvidence { sections: ScannerReportSection[]; @@ -124,14 +126,21 @@ export function environmentSectionTitle(snapshot: EnvironmentSnapshot, section: return environmentNativeHeading(section.label) || environmentNativeHeading(section.sectionKey); } -const osTable = (table: ScannerReportTable) => /operating\s+systems?/i.test(table.name); +const osTable = (table: ScannerReportTable) => /operating\s+systems?/i.test(table.name) + || table.headers.some(isOperatingSystemEolLabel); const resilienceProperty = /^(?:backup\.|laps\.|kerberos\.krbtgt\.|logging\.wef\.|domain\.recycle_bin_enabled$)/i; const inventorySection = /^section(?:UserInformation|ComputerInformation|AdminGroups|Trustsdetails|ControlPathsAnalysis|neverexpiresuser|enabledtrusteddelegationcomputer)$/i; function observationInGroup(observation: EnvironmentObservation, group: EnvironmentEvidenceGroup): boolean { const property = observation.property; const section = observation.sectionKey ?? ''; - if (group === 'operating-systems') return /^operating_system\./i.test(property) + if (group === 'ad-domains') return /^(?:domain\.|forest\.)/i.test(property) || /^sectionDomainInformation$/i.test(section); + if (group === 'entra-tenants') return /^entra\./i.test(property); + if (group === 'user-accounts') return /^user\./i.test(property) || /^sectionUserInformation$/i.test(section); + if (group === 'computer-objects') return /^computer\./i.test(property) || /^sectionComputerInformation$/i.test(section); + if (group === 'domain-controllers') return /^domain\.controller/i.test(property) || /^sectionDomainControllers$/i.test(section); + if (group === 'gpos') return /^gpo\./i.test(property) || /^sectionGPO$/i.test(section); + if (group === 'operating-systems') return isOperatingSystemEolObservation(observation) || /^operating_system\./i.test(property) || /operating\s+systems?/i.test(String(observation.attributes.tableName ?? '')); if (group === 'resilience') return resilienceProperty.test(property); if (group === 'password-policy') return /^password_policy\./i.test(property) || /^sectionPasswordPolicies$/i.test(section); @@ -148,6 +157,12 @@ function observationInGroup(observation: EnvironmentObservation, group: Environm function sectionForGroup(section: ScannerReportSection, group: EnvironmentEvidenceGroup): ScannerReportSection | undefined { if (group === 'all') return section; const key = section.sectionKey; + if (group === 'ad-domains' && /^sectionDomainInformation$/i.test(key)) return section; + if (group === 'entra-tenants' && /^(?:entra|azure)[-_ ]|^section(?:Entra|Azure)/i.test(key)) return section; + if (group === 'user-accounts' && /^sectionUserInformation$/i.test(key)) return section; + if (group === 'computer-objects' && /^sectionComputerInformation$/i.test(key)) return section; + if (group === 'domain-controllers' && /^sectionDomainControllers$/i.test(key)) return section; + if (group === 'gpos' && /^sectionGPO$/i.test(key)) return section; if (group === 'directory' && /^section(?:DomainInformation|Infrastructure)$/i.test(key)) return section; if (group === 'inventory' && inventorySection.test(key)) return section; if (group === 'password-policy' && /^sectionPasswordPolicies$/i.test(key)) return section; diff --git a/src/features/dashboard/operating-system-inventory.ts b/src/features/dashboard/operating-system-inventory.ts index c073038..a894b33 100644 --- a/src/features/dashboard/operating-system-inventory.ts +++ b/src/features/dashboard/operating-system-inventory.ts @@ -23,6 +23,24 @@ function count(value: string): number | undefined { return Number.isSafeInteger(number) && number >= 0 ? number : undefined; } +/** Explicit native PingCastle totals only; never derive lifecycle from product names. */ +export const isOperatingSystemEolLabel = (label: string): boolean => + /^(?:(?:nb|number of)\s+)?(?:eol os|os eol|end[- ]of[- ]life (?:os|operating systems?))$/i.test(label.trim()); + +export function isOperatingSystemEolObservation(item: EnvironmentObservation): boolean { + return item.tool.id === 'pingcastle' && /^sectionComputerInformation$/i.test(item.sectionKey ?? '') + && isOperatingSystemEolLabel(item.label); +} + +export function operatingSystemEolCount(snapshot?: EnvironmentSnapshot): number | undefined { + const items = (snapshot?.observations ?? []).filter(isOperatingSystemEolObservation); + // A single retained total is authoritative. Multiple reports/rows are not additive. + const unique = [...new Map(items.map(item => [JSON.stringify([item.scanRunId, item.importFileId, + item.sectionKey, item.attributes.sourceTableIndex, item.source.rowIndex, item.source.columnIndex, item.source.objectPath, + item.originalValue, item.normalizedValue, item.state]), item])).values()]; + return unique.length === 1 && unique[0].state === 'observed' ? count(value(unique[0])) : undefined; +} + function rowCount(row: EnvironmentObservation[], label: RegExp): number | undefined { const values = new Set(row.filter(item => label.test(item.label.trim())).map(item => count(value(item)))); return values.size === 1 ? [...values][0] : undefined; diff --git a/src/features/dashboard/security-domains.ts b/src/features/dashboard/security-domains.ts index 137f37a..3289119 100644 --- a/src/features/dashboard/security-domains.ts +++ b/src/features/dashboard/security-domains.ts @@ -1,91 +1,14 @@ import type { FindingRecord } from '../../domain/finding'; -import type { UnifiedResultProjection } from '../../domain/unified-result'; +import { securityDomainBuckets } from '../../evaluation/security-domains'; -export const SECURITY_DOMAIN_FACET_ID = 'derived:security-domain'; - -export const securityDomainCatalog = [ - { id: 'identity', label: 'Identity & Account Hygiene', glyph: 'user', pattern: /account|identity|user|password|stale|inactive|honey\s*pot/i }, - { id: 'privilege', label: 'Privilege & Attack Paths', glyph: 'shield', pattern: /privileg|admin|delegat|control\s*path|attack\s*path|tier\s*0|pim/i }, - { id: 'authentication', label: 'Authentication & Access', glyph: 'lock', pattern: /authenticat|\bmfa\b|conditional\s*access|kerberos|sign[ -]?in|break[ -]?glass|\bldaps?\b|channel\s*binding|anonymous\s*binding|rootdse|tls\s*1\.[01]/i }, - { id: 'infrastructure', label: 'AD Infrastructure & PKI', glyph: 'server', pattern: /domain\s*controller|forest|\btrust|certificate|\bpki\b|\bdns\b|replication|schema|organizational\s*units?|\bous?\b.*accidental\s*deletion|\bntfrs\b|\bsysvol\b/i }, - { id: 'hardening', label: 'System Hardening', glyph: 'code', pattern: /cis-cat|benchmark|hardening|hardened\s*paths?|firewall|print\s*spooler|\bspooler\b|attack\s*surface|\basr\b|defender|operating\s*system|obsolete\s+os|unsupported\s+(?:os|operating\s*system)|script\s*engines?|\bcve-\d+|lock\s*screen|\bsmb\s*v?1\b|(?:ip\s*)?source\s*routing|icmp\s*redirect|keep\s*alive|netbios\s*name\s*release|router\s*discovery|tcp\s*(?:max\s*data\s*)?retransmissions?|kernel\s*dma\s*protection|nonamereleaseondemand|performrouterdiscovery|tcpmaxdataretransmissions|disableipsourcerouting|enableicmpredirect|keepalivetime/i }, - { id: 'gpo', label: 'GPO & Baseline', glyph: 'policy', pattern: /\bgpo\b|group\s*policy|baseline|policy\s*(?:setting|deviation|conflict)|duplicates?/i }, - { id: 'cloud', label: 'Cloud & Applications', glyph: 'cloud', pattern: /\bcloud\b|\bentra\b|application|\bapps?\b|exchange|teams|sharepoint|guest|intune|purview/i }, - { id: 'recovery', label: 'Monitoring & Recovery', glyph: 'chart', pattern: /audit|monitor|logging|\bwef\b|backup|recover|recycle\s*bin|sensor|event\s*log/i } -] as const; - -export const unclassifiedSecurityDomain = { id: 'unclassified', label: 'Unclassified' } as const; - -const MAESTER_CLOUD_SUITE = /(?:^|[\s,;|\["'])(?:orca(?:\.|\b)|exo(?=$|[\s,;|\]"'])|eidsca(?:\.|\b)|cisa\.ms\.(?:exo|aad)(?:\.|\b)|maester\/(?:exchange|teams|entra)(?=$|[\s,;|\]"']))/i; - -const recordDomainEvidenceCache = new WeakMap(); - -function recordDomainEvidence(record: FindingRecord): { - text: string; - narrativeText: string; - explicitGpo: boolean; - scannerSuiteText: string; -} { - let cached = recordDomainEvidenceCache.get(record); - if (cached === undefined) { - const fields = record.fields; - cached = { - text: [record.title, ...fields - .filter(field => /^(?:title|name|finding|description|category|subcategory|sub-category|categories|control area|policy section|policy type|source rule id)$/i.test(field.label.trim())) - .map(field => field.value)].join(' '), - narrativeText: fields - .filter(field => /^(?:technical explanation|rationale|recommendation|remediation(?: guidance| recommendation)?|advised solution|solution|impact|details|test details)$/i.test(field.label.trim())) - .map(field => field.value).join(' '), - explicitGpo: fields.some(field => /^category$/i.test(field.label.trim()) && /^gpos?$/i.test(field.value.trim())), - // Maester exposes stable suite/control identifiers in BLOCK, ID and TAG. - // They are fallback evidence only: descriptive security-domain matches - // above retain priority and the source values remain unchanged. - scannerSuiteText: [record.title, ...fields - .filter(field => /^(?:block|id|tag|tags|category|categories)$/i.test(field.label.trim())) - .map(field => field.value)].join(' ') - }; - recordDomainEvidenceCache.set(record, cached); - } - return cached; -} - -/** The one shared Client Info / Work classification for a retained record. */ -export function securityDomainIdForRecord(record: FindingRecord): string { - const evidence = recordDomainEvidence(record); - if (evidence.explicitGpo) return 'gpo'; - const descriptiveMatch = securityDomainCatalog.find(domain => domain.pattern.test(evidence.text)); - if (descriptiveMatch) return descriptiveMatch.id; - // Broader narrative fields are useful fallback evidence, but they may mention - // several domains in one remediation. Assign only when the evidence is unique; - // otherwise keeping Unclassified is more honest than inventing a category. - const narrativeMatches = securityDomainCatalog.filter(domain => domain.pattern.test(evidence.narrativeText)); - if (narrativeMatches.length === 1) return narrativeMatches[0].id; - if (MAESTER_CLOUD_SUITE.test(evidence.scannerSuiteText)) return 'cloud'; - return unclassifiedSecurityDomain.id; -} - -/** Unified-result counterpart of the Work/Client Info catalog assignment. */ -export function securityDomainIdForUnifiedResult(result: UnifiedResultProjection): string { - if (result.sourceKind === 'policy-evaluation' || result.sourceKind === 'policy-observation' - || result.categories.some(category => /^gpos?$/i.test(category.categoryLabel.trim()))) return 'gpo'; - const evidence = [result.title, result.description, - ...result.categories.map(category => category.categoryLabel)].filter(Boolean).join(' '); - return securityDomainCatalog.find(domain => domain.pattern.test(evidence))?.id ?? unclassifiedSecurityDomain.id; -} +// The classifier lives in Evaluation (user decision 2026-10-04); views only consume it. +export { + SECURITY_DOMAIN_FACET_ID, securityDomainCatalog, securityDomainIdForRecord +} from '../../evaluation/security-domains'; /** Assign every record once so Client Info distributions reconcile to the total. */ -export function dashboardDomainAssignment(records: FindingRecord[]): Map { - const buckets = new Map([ - ...securityDomainCatalog.map(domain => [domain.id, [] as FindingRecord[]] as const), - [unclassifiedSecurityDomain.id, [] as FindingRecord[]] as const - ]); - records.forEach(record => buckets.get(securityDomainIdForRecord(record))!.push(record)); - return buckets; +export function dashboardDomainAssignment(records: FindingRecord[], toolId?: string): Map { + return securityDomainBuckets(records, toolId); } export function dashboardDomainRecords(records: FindingRecord[], domainId: string): FindingRecord[] { diff --git a/src/features/dataset-viewer/DatasetViewer.tsx b/src/features/dataset-viewer/DatasetViewer.tsx index a28d4a3..2431eb8 100644 --- a/src/features/dataset-viewer/DatasetViewer.tsx +++ b/src/features/dataset-viewer/DatasetViewer.tsx @@ -2,6 +2,8 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, typ import { createPortal } from 'react-dom'; import { previousPolicyReviews } from '../review-workspace/database-review'; import { useApp } from '../../app/AppProvider'; +import { FilterEmptyState } from '../../components/FilterEmptyState/FilterEmptyState'; +import { searchAllFindings } from '../search-filter/search-all-findings'; import { TriangleIcon } from '../../components/TriangleIcon'; import { DataTable } from '../../components/DataTable/DataTable'; import { FindingCard } from '../../components/FindingCard/FindingCard'; @@ -332,19 +334,10 @@ export function DatasetViewer() { <>