Anpassung Mitre Catalog Update

This commit is contained in:
Michael 2026-09-28 09:03:24 +02:00
parent add2aceb1d
commit d8f131d6da
10 changed files with 141 additions and 12 deletions

View File

@ -178,6 +178,7 @@
## UI Client Info dashboard ## UI Client Info dashboard
- Explicit user correction 2026-09-26: the proposed Work-style dashboard rearrangement was rejected and fully reverted. Preserve the existing tile layout, Overall Score, domain connections and Results popups. Reduce information only by removing duplicate overview facts within the existing tiles: Domain, Forest, both functional levels and DC OS stay in Domain Information; NetBIOS, schema version, domain creation and Entra identity stay in Directory; aggregate counts stay in Data. Remove repeated counts from Directory, Domain Information and Inventory only when they match the same fact already shown elsewhere; retain differing or non-numeric source values. Equal values for distinct facts (for example Domain and Forest) are not duplicates. Directory and Inventory Results retain their complete facts and original tables. This supersedes only earlier requirements to repeat those facts in several overview tiles.
- Explicit user correction 2026-09-20: the former Client Info `Priority Findings` tile is named `Data`, sits to the left of `Domain Information`, and shows the total `Deviations` value plus the separate additional-result-state row only. Do not show duplicate `Critical` or `High` summary values in that tile. The remaining deviation value occupies the full header width and is a keyboard-accessible button that opens Work with the standard `Deviation` filter. - Explicit user correction 2026-09-20: the former Client Info `Priority Findings` tile is named `Data`, sits to the left of `Domain Information`, and shows the total `Deviations` value plus the separate additional-result-state row only. Do not show duplicate `Critical` or `High` summary values in that tile. The remaining deviation value occupies the full header width and is a keyboard-accessible button that opens Work with the standard `Deviation` filter.
- Explicit user correction 2026-09-18 supersedes the previous Client Info visual exception: `.client-command-center` follows the same Ableton-inspired ASSESSOR language as the approved Data Hub, Work and Pathfinder. Client Info may organize its own domain content, but has no exception for palette, surfaces, tiles, typography, controls, selection, branding, spacing or chart geometry. Existing `--client-info-*` roles are legacy implementation details, not authority for a separate visual theme; reuse the shared roles when bringing this view into conformity. - Explicit user correction 2026-09-18 supersedes the previous Client Info visual exception: `.client-command-center` follows the same Ableton-inspired ASSESSOR language as the approved Data Hub, Work and Pathfinder. Client Info may organize its own domain content, but has no exception for palette, surfaces, tiles, typography, controls, selection, branding, spacing or chart geometry. Existing `--client-info-*` roles are legacy implementation details, not authority for a separate visual theme; reuse the shared roles when bringing this view into conformity.
- Treat Client Info as one connected executive security-posture canvas, not as a collection of unrelated report cards or a spreadsheet. Its stable reading order is overall posture, security domains, Data and Domain Information, then source evidence. - Treat Client Info as one connected executive security-posture canvas, not as a collection of unrelated report cards or a spreadsheet. Its stable reading order is overall posture, security domains, Data and Domain Information, then source evidence.

View File

@ -4,6 +4,8 @@ Referenzerkennung 2026-09-26 (Phase 13D, erster Umfang umgesetzt und verifiziert
Status: lebendes Projektdokument Status: lebendes Projektdokument
2026-09-26 - Phase 7 / Doppelte Client-Info-Fakten entfernt (2.8.7, umgesetzt und verifiziert): Der zuvor erprobte Work-artige Dashboardumbau wurde auf ausdruecklichen Nutzerwunsch vollstaendig zurueckgenommen. Die anschliessend beauftragte kleine Bereinigung behaelt alle bestehenden Kacheln, Positionen, Donuts, Domainverbindungen, CSS-Regeln und Results-Popups. Domain, Forest, beide Functional Levels und DC OS stehen in Domain Information; NetBIOS, Schema, Erstellungsdatum und Entra-Identitaet in Directory; Bestandszahlen bleiben in Data. Directory und Inventory zeigen identische Uebersichtsfakten nicht nochmals; ihre Results behalten saemtliche Kategorienwerte und Originaltabellen. Verglichen werden nur dieselben benannten Fakten mit gleichem Anzeigewert, nie unterschiedliche Fakten allein wegen gleicher Werte. Abweichende oder nicht numerische Angaben bleiben erhalten; unbekannte Werte bleiben unbekannt. Entscheidung: Patchversion 2.8.7 fuer die rueckwaertskompatible Darstellungsbereinigung, keine Daten-, Import-, Scoring-, API- oder Persistenzaenderung. Verifikation: 52 fokussierte Tests einschliesslich befuellter/leerer Datenlage, nicht numerischer DC-Angabe, tatsaechlicher Directory-/Inventory-Results-Klickwege, Posture-Verbindungen und UI-/API-Versionsvertrag; beide Produktionsbuilds, Design-/Local-only- und Diffpruefung bestanden. Synthetische Light-/Dark-Browserpruefung bestaetigt 14px/16px Rollen, 20px Inset und vollstaendige Results. Die gebaute CSS-Datei ist unveraendert. Grenze: unterschiedliche Darstellungen desselben Quellwertes werden vorsichtshalber nicht als identisch unterdrueckt. Phasenstatus unveraendert.
2026-09-26 - Phase 6/7 / Scanner-Einstieg im Report, CIS-Abschnitte und Work-Zusammenfuehrung (2.8.6, umgesetzt und verifiziert): Purple-Knight-Scanlaeufe mit Umfangsnamen (`for Active Directory`, `for Entra ID`, kombiniert) loesen ueber deklarierte Profil-`runNames` wieder auf das Purple-Knight-Profil auf; zuvor fielen sie in gemischtem Work auf generic zurueck (kein Problem/Next step, generische Status-, Facetten-, Result-Linker- und Graphlogik). Maester ohne Remediation-/HelpUrl-Feld zeigt den nativen Description-Abschnitt `#### Remediation action:` deklarativ (`firstRead.nextStepSection`) als Next step; der Text davor bleibt Hintergrund, jeder Text erscheint einmal. Eingefuehrte PowerShell-Befehle werden auch nach `script ...:` und bei Beginn mit Variablenzuweisung als Befehlsblock abgesetzt; exportbedingt flachgeklopfte Befehle erhalten nur in der Anzeige Zeilen an eindeutigen Grenzen (Anweisung nach schliessender Klammer, Pipeline), ausschliesslich Leerraum aendert sich. Purple-Knight-`Likelihood of compromise` endet vor dem angehaengten nativen `References`-Abschnitt (`firstRead.impactReferenceMarker`); die Referenztitel erscheinen an der Referenzposition, verlorene Absatzumbrueche werden angezeigt. CIS-Abschnittsnamen stammen ausschliesslich aus der gespeicherten Section-Score-Tabelle des eigenen Scanlaufs: `CIS Category` zeigt Nummer und Namen in numerischer Ordnung; Work-Ueberschriften ersetzen nach Nutzerkorrektur die fuehrende Regelnummer durch den Titel des umgebenden Abschnitts, die Nummer bleibt Source Rule ID, Plaketten zeigen Nummer plus Tooltip; widerspruechliche oder fehlende Titel lassen die Nummer stehen. Ursache fuer im Dummy nicht sichtbare Titel und Referenzen: Die Zusammenfuehrung von All Records mit GPO-Ergebnissen kopierte eine manuelle Feldliste. `withRebuiltLayout` uebernimmt jetzt alle Datensatzmetadaten und ersetzt nur Layoutfelder (id, index, title, fields, filled, meta, sourceFile, sourceSheet, highlighted); ein Schutztest mit synthetischem unbekanntem Zusatzfeld sichert kuenftige Metadaten. Dummy ueber den realen Work-Ladeweg (nur Zaehlungen): 411/411 CIS-Datensaetze mit Titeln und Referenzen, 160/160 benannte CIS-Kategorien, 92/92 getrennte Purple-Knight-Referenzabschnitte. Entscheidung: Patchversion 2.8.6 fuer rueckwaertskompatible Korrekturen und Verfeinerungen ueber gespeicherte Fakten; kein Reimport, keine Schema-, Import-, Scoring- oder Review-Aenderung. Verifikation: synthetische Regressionen fuer die tatsaechlichen Pfade (gemischtes Work mit und ohne GPO, Reader, Facetten, Kacheln, Zusammenfuehrung), Versionsvertragstests, beide Produktionsbuilds; Gesamtsuite 1.608 bestanden, 14 uebersprungen, dieselben sechs vorbestehenden fachfremden Fehler in GPO-/Security-Domain-/Result-DB-/Dashboard-Tests. Risiken/Grenzen: Security-Domain-Zuordnung bleibt stichwortbasiert (gesonderte Entscheidung offen); Pathfinder und Graphansichten zeigen CIS-Titel weiterhin mit Nummer; Purple-Knight-Referenztitel ohne Trennzeichen bleiben ein Textblock. Phasenstatus unveraendert. 2026-09-26 - Phase 6/7 / Scanner-Einstieg im Report, CIS-Abschnitte und Work-Zusammenfuehrung (2.8.6, umgesetzt und verifiziert): Purple-Knight-Scanlaeufe mit Umfangsnamen (`for Active Directory`, `for Entra ID`, kombiniert) loesen ueber deklarierte Profil-`runNames` wieder auf das Purple-Knight-Profil auf; zuvor fielen sie in gemischtem Work auf generic zurueck (kein Problem/Next step, generische Status-, Facetten-, Result-Linker- und Graphlogik). Maester ohne Remediation-/HelpUrl-Feld zeigt den nativen Description-Abschnitt `#### Remediation action:` deklarativ (`firstRead.nextStepSection`) als Next step; der Text davor bleibt Hintergrund, jeder Text erscheint einmal. Eingefuehrte PowerShell-Befehle werden auch nach `script ...:` und bei Beginn mit Variablenzuweisung als Befehlsblock abgesetzt; exportbedingt flachgeklopfte Befehle erhalten nur in der Anzeige Zeilen an eindeutigen Grenzen (Anweisung nach schliessender Klammer, Pipeline), ausschliesslich Leerraum aendert sich. Purple-Knight-`Likelihood of compromise` endet vor dem angehaengten nativen `References`-Abschnitt (`firstRead.impactReferenceMarker`); die Referenztitel erscheinen an der Referenzposition, verlorene Absatzumbrueche werden angezeigt. CIS-Abschnittsnamen stammen ausschliesslich aus der gespeicherten Section-Score-Tabelle des eigenen Scanlaufs: `CIS Category` zeigt Nummer und Namen in numerischer Ordnung; Work-Ueberschriften ersetzen nach Nutzerkorrektur die fuehrende Regelnummer durch den Titel des umgebenden Abschnitts, die Nummer bleibt Source Rule ID, Plaketten zeigen Nummer plus Tooltip; widerspruechliche oder fehlende Titel lassen die Nummer stehen. Ursache fuer im Dummy nicht sichtbare Titel und Referenzen: Die Zusammenfuehrung von All Records mit GPO-Ergebnissen kopierte eine manuelle Feldliste. `withRebuiltLayout` uebernimmt jetzt alle Datensatzmetadaten und ersetzt nur Layoutfelder (id, index, title, fields, filled, meta, sourceFile, sourceSheet, highlighted); ein Schutztest mit synthetischem unbekanntem Zusatzfeld sichert kuenftige Metadaten. Dummy ueber den realen Work-Ladeweg (nur Zaehlungen): 411/411 CIS-Datensaetze mit Titeln und Referenzen, 160/160 benannte CIS-Kategorien, 92/92 getrennte Purple-Knight-Referenzabschnitte. Entscheidung: Patchversion 2.8.6 fuer rueckwaertskompatible Korrekturen und Verfeinerungen ueber gespeicherte Fakten; kein Reimport, keine Schema-, Import-, Scoring- oder Review-Aenderung. Verifikation: synthetische Regressionen fuer die tatsaechlichen Pfade (gemischtes Work mit und ohne GPO, Reader, Facetten, Kacheln, Zusammenfuehrung), Versionsvertragstests, beide Produktionsbuilds; Gesamtsuite 1.608 bestanden, 14 uebersprungen, dieselben sechs vorbestehenden fachfremden Fehler in GPO-/Security-Domain-/Result-DB-/Dashboard-Tests. Risiken/Grenzen: Security-Domain-Zuordnung bleibt stichwortbasiert (gesonderte Entscheidung offen); Pathfinder und Graphansichten zeigen CIS-Titel weiterhin mit Nummer; Purple-Knight-Referenztitel ohne Trennzeichen bleiben ein Textblock. Phasenstatus unveraendert.
2026-09-26 - Phase 7 / Finding-Reader-Abstaende (2.8.4, umgesetzt und verifiziert): Die gemeinsame Abschnittsregel umfasst jetzt div und section, damit GPO-Quellen und Baseline-Vergleich denselben Tokenabstand verwenden: space-4 (16 px) vor/nach der Trennlinie, space-2 (8 px) zwischen Feldueberschrift und Text. Redundante Abschlusslinien am letzten Quellblock und letzten Reader-Feld entfernt; Problem-/Next-step-Sonderabstaende bleiben erhalten. Synthetischer permanenter Klicktest prueft den gerenderten GPO-Reader vor, nach und nach erneutem Schliessen des Baseline-Disclosures. 44 gezielte Reader-/UI-/API-Versionsvertragstests, beide Produktionsbuilds, Design-/Local-only- und Diffpruefung bestanden. Synthetische Light-/Dark-Sichtpruefung bestaetigt jeweils rund 16 px Abschnittsabstand. Patchversion fuer die gemeinsame Darstellungskorrektur; keine Daten-, Import-, Bewertungs- oder Persistenzaenderung. Risiko: Die gemeinsame Regel wirkt auch auf weitere native Reader-Abschnitte; fokussierte Reader-Regressionen decken die bestehenden Faelle ab. Phasenstatus unveraendert. 2026-09-26 - Phase 7 / Finding-Reader-Abstaende (2.8.4, umgesetzt und verifiziert): Die gemeinsame Abschnittsregel umfasst jetzt div und section, damit GPO-Quellen und Baseline-Vergleich denselben Tokenabstand verwenden: space-4 (16 px) vor/nach der Trennlinie, space-2 (8 px) zwischen Feldueberschrift und Text. Redundante Abschlusslinien am letzten Quellblock und letzten Reader-Feld entfernt; Problem-/Next-step-Sonderabstaende bleiben erhalten. Synthetischer permanenter Klicktest prueft den gerenderten GPO-Reader vor, nach und nach erneutem Schliessen des Baseline-Disclosures. 44 gezielte Reader-/UI-/API-Versionsvertragstests, beide Produktionsbuilds, Design-/Local-only- und Diffpruefung bestanden. Synthetische Light-/Dark-Sichtpruefung bestaetigt jeweils rund 16 px Abschnittsabstand. Patchversion fuer die gemeinsame Darstellungskorrektur; keine Daten-, Import-, Bewertungs- oder Persistenzaenderung. Risiko: Die gemeinsame Regel wirkt auch auf weitere native Reader-Abschnitte; fokussierte Reader-Regressionen decken die bestehenden Faelle ab. Phasenstatus unveraendert.

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "assessor", "name": "assessor",
"version": "2.8.6", "version": "2.8.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "assessor", "name": "assessor",
"version": "2.8.6", "version": "2.8.7",
"dependencies": { "dependencies": {
"@fontsource-variable/inter": "^5.3.0", "@fontsource-variable/inter": "^5.3.0",
"jsdom": "^29.1.1", "jsdom": "^29.1.1",

View File

@ -1,6 +1,6 @@
{ {
"name": "assessor", "name": "assessor",
"version": "2.8.6", "version": "2.8.7",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@ -756,11 +756,12 @@ function DashboardFactList({ items }: { items: DashboardFact[] }) {
</dl>; </dl>;
} }
function DashboardFactGroup({ title, items, glyph, snapshot, evidenceGroup }: { function DashboardFactGroup({ title, items, overviewItems, glyph, snapshot, evidenceGroup }: {
title: string; items: DashboardFact[]; glyph: DashboardGlyphName; title: string; items: DashboardFact[]; glyph: DashboardGlyphName;
overviewItems?: DashboardFact[];
snapshot?: EnvironmentSnapshot; evidenceGroup: EnvironmentEvidenceGroup; snapshot?: EnvironmentSnapshot; evidenceGroup: EnvironmentEvidenceGroup;
}) { }) {
const tileItems = title === 'Directory' ? items.slice(0, 6) : items; const tileItems = overviewItems ?? (title === 'Directory' ? items.slice(0, 6) : items);
return <section className="client-env-group"> return <section className="client-env-group">
<header className="client-env-group-header"> <header className="client-env-group-header">
<h3 className="client-env-group-title"> <h3 className="client-env-group-title">
@ -772,7 +773,7 @@ function DashboardFactGroup({ title, items, glyph, snapshot, evidenceGroup }: {
<EnvironmentEvidenceTables snapshot={snapshot} group={evidenceGroup}/> <EnvironmentEvidenceTables snapshot={snapshot} group={evidenceGroup}/>
</EnvironmentResultsButton> </EnvironmentResultsButton>
</header> </header>
<DashboardFactList items={tileItems.length ? tileItems : items.slice(0, 8)}/> <DashboardFactList items={tileItems}/>
</section>; </section>;
} }
@ -1426,6 +1427,21 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
const userAccounts = dashboardEnvironmentTableNumber(environment.snapshot, 'sectionUserInformation', /^nb user accounts$/i); const userAccounts = dashboardEnvironmentTableNumber(environment.snapshot, 'sectionUserInformation', /^nb user accounts$/i);
const computerAccounts = dashboardEnvironmentTableNumber(environment.snapshot, 'sectionComputerInformation', /^nb computer accounts$/i); const computerAccounts = dashboardEnvironmentTableNumber(environment.snapshot, 'sectionComputerInformation', /^nb computer accounts$/i);
const domainControllers = dashboardObservationNumber(environment.snapshot, 'domain.controller_count'); 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)],
['User accounts', numberFact(userAccounts)],
['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 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)); const maesterSource = sourceScores.find(source => /maester/i.test(source.tool.label));
const entraTenants = dashboardEntraTenantCount(environment.snapshot) const entraTenants = dashboardEntraTenantCount(environment.snapshot)
?? dashboardObservationNumber(environment.snapshot, 'entra.configuration_detected') ?? dashboardObservationNumber(environment.snapshot, 'entra.configuration_detected')
@ -1558,7 +1574,7 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
</section> </section>
<section className="client-domain-information" aria-labelledby="client-domain-information-heading"> <section className="client-domain-information" aria-labelledby="client-domain-information-heading">
<h2 id="client-domain-information-heading">Domain Information</h2> <h2 id="client-domain-information-heading">Domain Information</h2>
<dl>{domainInformation.map(item => <div key={item.label}><dt>{item.label}</dt><dd>{item.value}</dd></div>)}</dl> <dl>{domainOverview.map(item => <div key={item.label}><dt>{item.label}</dt><dd>{item.value}</dd></div>)}</dl>
</section> </section>
</aside> </aside>
</div> </div>
@ -1566,9 +1582,9 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
{scoringOpen && <ScoringMethodology sources={sourceScores} context={dataset.securityScoring} onClose={() => setScoringOpen(false)} />} {scoringOpen && <ScoringMethodology sources={sourceScores} context={dataset.securityScoring} onClose={() => setScoringOpen(false)} />}
<div className="client-environment-grid" aria-label="Environment overview"> <div className="client-environment-grid" aria-label="Environment overview">
<DashboardFactGroup title="Directory" items={environmentDetail.directory} glyph="domain" snapshot={environment.snapshot} evidenceGroup="directory" /> <DashboardFactGroup title="Directory" items={environmentDetail.directory} overviewItems={directoryOverview} glyph="domain" snapshot={environment.snapshot} evidenceGroup="directory" />
<DashboardFactGroup title="Resilience" items={environmentDetail.resilience} glyph="shield" snapshot={environment.snapshot} evidenceGroup="resilience" /> <DashboardFactGroup title="Resilience" items={environmentDetail.resilience} glyph="shield" snapshot={environment.snapshot} evidenceGroup="resilience" />
<DashboardFactGroup title="Inventory" items={environmentDetail.inventory} glyph="computer" snapshot={environment.snapshot} evidenceGroup="inventory" /> <DashboardFactGroup title="Inventory" items={environmentDetail.inventory} overviewItems={inventoryOverview} glyph="computer" snapshot={environment.snapshot} evidenceGroup="inventory" />
<DashboardFactGroup title="Password policy" items={environmentDetail.passwordPolicy} glyph="lock" snapshot={environment.snapshot} evidenceGroup="password-policy" /> <DashboardFactGroup title="Password policy" items={environmentDetail.passwordPolicy} glyph="lock" snapshot={environment.snapshot} evidenceGroup="password-policy" />
<DashboardFactGroup title="Certificates & PKI" items={environmentDetail.certificates} glyph="policy" snapshot={environment.snapshot} evidenceGroup="certificates" /> <DashboardFactGroup title="Certificates & PKI" items={environmentDetail.certificates} glyph="policy" snapshot={environment.snapshot} evidenceGroup="certificates" />
<OperatingSystemInventory groups={environmentDetail.operatingSystems} icon={<DashboardGlyph name="computer"/>} snapshot={environment.snapshot} /> <OperatingSystemInventory groups={environmentDetail.operatingSystems} icon={<DashboardGlyph name="computer"/>} snapshot={environment.snapshot} />

View File

@ -9,7 +9,7 @@ const components = readFileSync(new URL('../../src/styles/components.css', impor
describe('ASSESSOR version in the brand header', () => { describe('ASSESSOR version in the brand header', () => {
it('shows the centrally maintained product version and opens About from the brand', () => { it('shows the centrally maintained product version and opens About from the brand', () => {
expect(ASSESSOR_PRODUCT_VERSION).toBe('2.8.6'); expect(ASSESSOR_PRODUCT_VERSION).toBe('2.8.7');
expect(app).toContain("import { ASSESSOR_PRODUCT_VERSION } from '../domain/assessor-release';"); expect(app).toContain("import { ASSESSOR_PRODUCT_VERSION } from '../domain/assessor-release';");
expect(app).toContain('aria-label="About ASSESSOR"'); expect(app).toContain('aria-label="About ASSESSOR"');
expect(app).toContain("import assessorLogo from '../assets/branding/assessor-icon-transparent.png';"); expect(app).toContain("import assessorLogo from '../assets/branding/assessor-icon-transparent.png';");

View File

@ -0,0 +1,62 @@
// @vitest-environment jsdom
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, expect, it, vi } from 'vitest';
import { useApp } from '../../src/app/AppProvider';
import { AssessorApiClient } from '../../src/api/assessor-api-client';
import { DashboardPanel } from '../../src/features/dashboard/DashboardPanel';
import { clientFactsSnapshot, clientFactsState } from '../fixtures/client-info-facts';
vi.mock('../../src/app/AppProvider', () => ({ useApp: vi.fn() }));
vi.mock('../../src/features/search-filter/WorkTimeslotControl', () => ({ WorkTimeslotControl: () => <span>Synthetic timeslot</span> }));
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); document.body.replaceChildren(); });
it.each(['populated', 'unavailable', 'non-numeric count'])('shows each overview fact once in the existing tiles and preserves Results: %s', async mode => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const snapshot = structuredClone(clientFactsSnapshot);
if (mode === 'unavailable') { snapshot.observations = []; snapshot.sections = []; }
if (mode === 'non-numeric count') {
const count = snapshot.observations.find(item => item.property === 'domain.controller_count')!;
count.originalValue = count.normalizedValue = 'Two reported controllers'; count.valueType = 'string';
}
vi.spyOn(AssessorApiClient.prototype, 'getEnvironmentSnapshot').mockResolvedValue(snapshot);
vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockResolvedValue({ records: [] } as never);
vi.mocked(useApp).mockReturnValue(clientFactsState() as unknown as ReturnType<typeof useApp>);
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
try {
await act(async () => root.render(<DashboardPanel onOpenWork={vi.fn()}/>));
const domain = host.querySelector('.client-domain-information')!;
const groups = [...host.querySelectorAll('.client-env-group')];
const directory = groups.find(group => group.querySelector('h3')?.textContent === 'Directory')!;
const inventory = groups.find(group => group.querySelector('h3')?.textContent === 'Inventory')!;
const labels = (element: Element) => [...element.querySelectorAll('dt')].map(item => item.textContent);
expect(host.querySelector('.client-posture')).not.toBeNull();
expect(host.querySelector('.client-posture-connectors')).not.toBeNull();
expect(host.querySelector('.client-priority')).not.toBeNull();
expect(host.querySelectorAll('.client-data-fact')).toHaveLength(6);
expect(groups).toHaveLength(5);
expect(labels(directory)).toEqual(['NetBIOS name', 'Schema version', 'Domain created', 'Entra ID']);
expect(labels(domain)).toEqual(mode === 'non-numeric count'
? ['Domain', 'Forest', 'Domain controllers', 'Domain functional level', 'Forest functional level', 'DC OS']
: ['Domain', 'Forest', 'Domain functional level', 'Forest functional level', 'DC OS']);
expect(labels(inventory)).toEqual(['Enabled / disabled', 'Password never expires', 'Admin groups', 'Trusts', 'Assessed host']);
// Equal domain/forest and functional-level values are distinct facts, not duplicates.
if (mode !== 'unavailable') {
expect([...domain.querySelectorAll('dd')].filter(item => item.textContent === 'contoso.example')).toHaveLength(2);
expect([...domain.querySelectorAll('dd')].filter(item => item.textContent === 'Windows Server 2016')).toHaveLength(2);
} else expect([...domain.querySelectorAll('dd')].every(item => item.textContent === '—')).toBe(true);
for (const [title, group, expected] of [
['Directory', directory, ['Domain', 'Forest', 'Domains', 'Domain functional level', 'Schema version']],
['Inventory', inventory, ['User accounts', 'Computer objects', 'Domain controllers', 'Assessed host']]
] as const) {
const trigger = group.querySelector<HTMLButtonElement>('button')!;
expect(trigger.getAttribute('aria-label')).toBe(`Open ${title} results`);
await act(async () => trigger.click());
const popup = document.querySelector('.floating-data-window')!;
for (const label of expected) expect(labels(popup)).toContain(label);
if (mode !== 'unavailable') expect(popup.querySelector('table')).not.toBeNull();
await act(async () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })));
expect(document.querySelector('.floating-data-window')).toBeNull();
}
} finally { await act(async () => root.unmount()); }
});

View File

@ -122,7 +122,7 @@ describe('client info environment detail', () => {
expect(component).not.toContain('className="client-environment-detail"'); expect(component).not.toContain('className="client-environment-detail"');
expect(component).not.toContain('environmentDetailRef'); expect(component).not.toContain('environmentDetailRef');
expect(component).toContain('<DashboardFactList items={items}/>'); expect(component).toContain('<DashboardFactList items={items}/>');
expect(component).toContain('<DashboardFactList items={tileItems.length ? tileItems : items.slice(0, 8)}/>'); expect(component).toContain('<DashboardFactList items={tileItems}/>');
expect(component).toContain('unit: \'/100 ASSESSOR\''); expect(component).toContain('unit: \'/100 ASSESSOR\'');
expect(component).toContain('PingCastle risk ${exactNative}/100'); expect(component).toContain('PingCastle risk ${exactNative}/100');
expect(component).toContain('useGpoInventory'); expect(component).toContain('useGpoInventory');

48
tests/fixtures/client-info-facts.ts vendored Normal file
View File

@ -0,0 +1,48 @@
import type { EnvironmentSnapshot, EnvironmentObservation, ScannerReportSection } from '../../src/domain/environment-context';
import { makeSheet } from '../../src/importers/sheet-factory';
import { genericProfile } from '../../src/profiles/generic/generic-profile';
const tool = { id: 'synthetic', name: 'Synthetic scanner' };
const observation = (property: string, value: string | number): EnvironmentObservation => ({
version: 1, id: property, scanRunId: 'synthetic-run', importFileId: 'synthetic-file', tool,
subject: { type: 'directory' }, property, label: property, originalValue: value, normalizedValue: value,
valueType: typeof value === 'number' ? 'number' : 'string', state: 'observed',
source: { rowIndex: 0 }, mappingVersion: '1', attributes: {}
});
const section = (sectionKey: string, name: string, headers: string[], rows: (string | number)[][]): ScannerReportSection => ({
version: 1, id: sectionKey, scanRunId: 'synthetic-run', importFileId: 'synthetic-file', tool,
sectionKey, label: name, ordinal: 0, state: 'observed', tables: [{ name, headers, rows }],
source: { rowIndex: 0 }, extractorVersion: '1', attributes: {}
});
export const clientFactsSnapshot: EnvironmentSnapshot = {
version: 1, projectId: 'synthetic-project', scanRunIds: ['synthetic-run'], generatedAt: '2026-01-15T00:00:00Z',
coverage: { observed: 11, explicitlyAbsent: 0, empty: 0, restricted: 0, incomplete: 0 },
observations: [
observation('domain.fqdn', 'contoso.example'), observation('forest.name', 'contoso.example'),
observation('domain.netbios_name', 'CONTOSO'), observation('domain.count', 1),
observation('domain.controller_count', 2), observation('domain.functional_level', 'Windows Server 2016'),
observation('forest.functional_level', 'Windows Server 2016'), observation('forest.schema_version', '87'),
observation('domain.created_at', '2020-01-15'), observation('entra.tenant_name', 'Synthetic tenant'),
observation('host.name', 'Synthetic host')
],
sections: [
section('sectionDomainInformation', 'Directory evidence', ['Domain', 'Forest'], [['contoso.example', 'contoso.example']]),
section('sectionUserInformation', 'Account analysis', ['Nb User Accounts', 'Nb Enabled', 'Nb Disabled', 'Nb pwd never Expire'], [[16, 15, 1, 5]]),
section('sectionComputerInformation', 'Computer analysis', ['Nb Computer Accounts'], [[24]])
]
};
export function clientFactsState() {
const sheet = makeSheet('Synthetic layout', [
['Name', 'Status', 'Severity', 'Source: Scan'], ['Synthetic finding', 'Failed', 'High', 'Scanner A']
], '', 'synthetic.csv', { isProjectSheet: true });
return {
dataset: { fileName: 'Synthetic layout', sheets: [sheet], scanTool: 'Generic' },
activeSheet: sheet, activeProfile: genericProfile, reviews: {}, facetFilters: {},
targetProjectId: 'synthetic-project', activeScanRunIds: ['synthetic-run'], activeAssessmentIds: ['synthetic-assessment'],
backendStatus: 'unavailable', projects: [], assessments: [], selectedProject: null,
setStatusFilter() {}, setFacetFilter() {}, setPathFindingIds() {}, resetFilters() {},
setView() {}, setActionOnly() {}, setActiveSheet() {}
};
}

View File

@ -52,7 +52,7 @@ describe('local ASSESSOR HTTP server', () => {
const health = await fetch(`${baseUrl}/api/v1/health`); const health = await fetch(`${baseUrl}/api/v1/health`);
expect(await health.json()).toEqual({ expect(await health.json()).toEqual({
status: 'ok', application: 'ASSESSOR', productVersion: '2.8.6', apiVersion: 1, repositoryRevision: 0, persistence: 'memory' status: 'ok', application: 'ASSESSOR', productVersion: '2.8.7', apiVersion: 1, repositoryRevision: 0, persistence: 'memory'
}); });
expect(health.headers.get('access-control-allow-origin')).toBeNull(); expect(health.headers.get('access-control-allow-origin')).toBeNull();
}); });