Assessor_2.0/tests/features/dashboard-layout.test.ts

295 lines
21 KiB
TypeScript

import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import type { FindingRecord } from '../../src/domain/finding';
import type { EnvironmentSnapshot } from '../../src/domain/environment-context';
import { dashboardCisCatSeverityRemainder, dashboardDomainInformation, dashboardEnvironmentSummary, dashboardExplicitIoeCount, dashboardExplicitIoeRecordIds, dashboardOverallScore, dashboardPingCastleRiskCategories, dashboardRiskTools, dashboardScanSourceLabel } from '../../src/features/dashboard/DashboardPanel';
import { securityDomainCatalog } from '../../src/features/dashboard/security-domains';
const component = readFileSync(new URL('../../src/features/dashboard/DashboardPanel.tsx', import.meta.url), 'utf8');
const overviewComponent = component.slice(component.indexOf('<><ClientInfoTabs tabs=')).split("}, { id: 'security'")[0];
const securityComponent = component.slice(component.indexOf('const securityPanelView ='), component.indexOf('async function exportSecurityReport'));
const styles = readFileSync(new URL('../../src/styles/datahub.css', import.meta.url), 'utf8');
describe('dashboard reference layout', () => {
it('gives Data its own shared-theme tile alongside the score/domain tile', () => {
expect(styles).toMatch(/\.client-overview-summary\s*\{[^}]*border:\s*0;[^}]*background:\s*transparent;/s);
for (const selector of ['client-posture', 'client-priority']) {
const rule = styles.match(new RegExp(`\\.${selector}\\s*\\{([^}]*)\\}`))?.[1];
expect(rule).toContain('border: var(--border-hair) solid var(--tile-border)');
expect(rule).toContain('border-radius: var(--tile-radius)');
expect(rule).toContain('background: var(--tile-surface)');
}
});
it('uses the requested summary-to-action client information hierarchy', () => {
expect(component).toContain('className="client-overview-header"');
expect(component).toContain('<WorkTimeslotControl />');
expect(overviewComponent.match(/<DashboardTopFact/g)).toHaveLength(6);
expect(component).toContain('className="client-overview-main"');
expect(component).toContain('Overall Score');
expect(component).toContain('Security Domains');
expect(component).toContain('<h2 id="client-priority-heading">Data</h2>');
expect(component).toContain('Domain Information');
expect(component).not.toContain('className="client-assessment-gaps"');
expect(component).toContain('<span>Gaps</span>');
expect(component).toContain('<EnvironmentResultsButton title="Overall Score">');
expect(component).toContain('<RiskRegister dataset={dataset} reviews={reviews} onOpenFinding={recordId => jump(() => setPathFindingIds([recordId]))} />');
expect(styles).toMatch(/\.dashboard-page\s*\{[^}]*container-type:\s*inline-size;/s);
expect(component).toContain('className={`client-posture tone-${overallTone}`}');
expect(styles).toMatch(/\.dashboard-page\.client-command-center\s*\{[^}]*--client-info-surface-raised:\s*var\(--tile-surface\);[^}]*background:\s*var\(--surface-app\);/s);
expect(component).not.toContain('className="client-overview-facts"');
expect(component).toContain('className="client-data-facts"');
expect(styles).not.toContain('.client-overview-fact');
expect(styles).toMatch(/\.client-data-facts\s*\{[^}]*grid-template-columns:\s*repeat\(3, minmax\(0, 1fr\)\);/s);
expect(styles).toMatch(/\.client-data-fact\s*\{[^}]*background:\s*transparent;/s);
expect(styles).toMatch(/\.client-overview-main\s*\{[^}]*grid-template-columns:\s*minmax\(0, 3fr\) minmax\(0, 1fr\);[^}]*align-items:\s*stretch;/s);
expect(styles).toMatch(/\.client-priority\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\);[^}]*align-content:\s*stretch;[^}]*align-items:\s*stretch;/s);
expect(styles).not.toContain('.client-assessment-gaps');
expect(styles).toMatch(/@container \(max-width: 760px\)[\s\S]*?\.client-priority\s*\{[^}]*grid-template-columns:\s*1fr;/s);
expect(styles).toMatch(/\.client-posture\s*\{[^}]*grid-template-columns:\s*minmax\(300px, \.82fr\) minmax\(430px, 1\.18fr\);/s);
expect(styles).toMatch(/\.client-risk-register-layout\s*\{[^}]*grid-template-columns:\s*clamp\(420px, 27vw, 520px\) minmax\(0, 1fr\);[^}]*height:\s*calc\(var\(--tile-content-min-height\) \* 2\.5\);/s);
expect(styles).toMatch(/\.client-risk-register-navigation\s*\{[^}]*border-right:\s*var\(--bw-hairline\) solid var\(--border-default\);/s);
expect(styles).toMatch(/\.client-risk-register-information\s*\{[^}]*padding:\s*var\(--space-5\) var\(--space-6\);[^}]*overflow-y:\s*auto;/s);
expect(styles).toMatch(/\.client-risk-register-list button\[aria-pressed="true"\]\s*\{[^}]*box-shadow:\s*inset var\(--marker-width\) 0 0 var\(--marking-color\);/s);
expect(component).toContain('onOpenFinding={recordId => jump(() => setPathFindingIds([recordId]))}');
});
it('keeps source-balanced score, coverage and critical guardrail separate', () => {
expect(dashboardOverallScore([{ score: 90 }, { score: 30 }, { score: undefined }])).toBe(60);
expect(dashboardOverallScore([{ score: undefined }])).toBeUndefined();
expect(component).toContain('<DashboardOverallRing score={overallScore} onClick={() => jump(() => undefined, false)} />');
expect(component).toContain('Assessment Coverage');
expect(component).not.toContain('className="client-overall-status');
expect(component).not.toContain('className="client-overall-critical"');
expect(component).not.toContain('className="client-overall-secondary"');
expect(component).not.toContain('<dt>Lowest domain pass rate</dt>');
expect(component).toContain('dashboardOverallScore(sourceScores)');
expect(component).toContain("name: 'PingCastle'");
expect(component).toContain("name: 'GPO Analysis'");
expect(component).toContain("className=\"client-posture-connectors\"");
expect(styles).toMatch(/\.client-overall-coverage\s*\{[^}]*width:\s*min\(70%, var\(--tile-content-width\)\);/s);
expect(component).not.toContain('Design concept');
expect(component).not.toContain('Illustrative data');
});
it('shows the six structured security domains and all available sources', () => {
// User decision 2026-10-04: six domains; GPO is a delivery path, not a domain.
expect(securityDomainCatalog.map(domain => domain.label)).toEqual([
'Identity & Authentication', 'Privileged Access & Delegation', 'AD Infrastructure & PKI',
'System Hardening', 'Cloud & Collaboration', 'Monitoring & Recovery'
]);
expect(component).toContain('orderedSourceScores.map');
expect(component).toContain('nativePingCastleRisk');
expect(component).toContain('dashboardEnvironmentTableNumber');
expect(component).not.toContain('className="client-source-overview"');
expect(component).toContain('className="client-environment-sources"');
expect(styles).toMatch(/\.client-environment-sources ul\s*\{[^}]*list-style:\s*none;/s);
expect(styles).toMatch(/\.client-environment-sources li > button\s*\{[^}]*cursor:\s*pointer;/s);
expect(styles).not.toContain('.client-source-overview');
expect(component).toContain("dismissResults(); setScoringOpen('overview');");
expect(component).toContain("setScoringOpen('security')");
expect(styles).toMatch(/\.client-overall-ring-track\s*\{\s*stroke:\s*var\(--donut-track\);/);
expect(component).toContain('<span className="client-domain-copy"><strong>{domain.label}</strong></span>');
expect(component).toContain('className="client-domain-amount-bar" aria-hidden="true"');
expect(component).toContain('const domainDistributionTotal = displayedDomains.reduce');
expect(styles).toMatch(/\.client-domains ul\s*\{[^}]*grid-template-columns:\s*42px max-content minmax\(var\(--space-8\), 1fr\) max-content;/s);
expect(styles).toMatch(/\.client-domains li\s*\{[^}]*grid-template-columns:\s*subgrid;/s);
expect(styles).toMatch(/\.client-domain-score\s*\{[^}]*grid-column:\s*1;[^}]*grid-row:\s*1;/s);
expect(styles).toMatch(/\.client-domain-amount-bar\s*\{[^}]*grid-column:\s*3;[^}]*height:\s*var\(--space-6\);/s);
expect(styles).toMatch(/\.client-domain-amount-bar > i\s*\{[^}]*right:\s*0;[^}]*width:\s*var\(--share\);/s);
expect(styles).toMatch(/\.client-domain-amount-bar > i\s*\{[^}]*background:\s*var\(--chart-color, var\(--client-info-neutral\)\);/s);
expect(component).toContain('const overallTone = dashboardScoreTone(overallScore);');
expect(component).toContain('className={`client-posture tone-${overallTone}`}');
expect(styles).not.toContain('--domain-amount-color');
expect(component).not.toContain('client-domain-copy"><strong>{domain.label}</strong><small>');
expect(styles).toMatch(/\.client-domain-copy strong\s*\{[^}]*font-size:\s*var\(--type-h4-size\);/s);
expect(styles).toMatch(/\.client-domains li > div > b\s*\{[^}]*grid-column:\s*4;[^}]*text-align:\s*right;/s);
});
it('uses deviations as the finding base and shows other result states separately', () => {
expect(component).toContain("records: domainRecords.filter(record => states.get(record) === 'deviation')");
// Every check has exactly one of the six domains; there is no Unclassified node any more.
expect(component).not.toContain("label: 'Unclassified'");
expect(component).toContain('const displayedDomains = domains;');
expect(component).not.toContain('total deviations</span>');
expect(component).not.toContain("<small>{reviewOnly ? 'Review required' : 'Deviations'}</small>");
expect(component).toContain("<b title={reviewOnly ? 'Review required' : 'Deviations'}>");
expect(component).toContain('className="client-priority-context"');
expect(component).toContain('className="datahub-reference-knob client-priority-total"');
expect(component).toContain('percentage={assessmentPercentages.deviation}');
expect(component).toContain('percentageBasisLabel="Of assessed checks"');
expect(component.indexOf('className="client-priority-findings"')).toBeLessThan(component.indexOf('className="client-domain-information"'));
expect(component).not.toContain('criticalSeverity');
expect(component).not.toContain('highSeverity');
expect(styles).toMatch(/\.client-priority-findings > header\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\);/s);
expect(styles).toMatch(/\.client-priority-total\s*\{[^}]*--donut-ring-size:\s*var\(--tile-compact-width\);[^}]*width:\s*var\(--tile-compact-width\);[^}]*min-height:\s*0;[^}]*padding:\s*0;[^}]*cursor:\s*pointer;/s);
expect(styles).toMatch(/\.client-priority-total\s*\{[^}]*border-color:\s*var\(--segment-border\);/s);
expect(styles).not.toContain('.client-priority-total .donut-metric-card__plot');
expect(styles).not.toMatch(/\.client-priority-total\s*\{[^}]*(?:background|border-radius|box-shadow):/s);
expect(styles).not.toContain('.client-priority-total .donut-metric-card__title');
expect(component).not.toContain('<ol>{priorityRecords.length');
expect(component).not.toContain('No critical or high deviations');
expect(component).not.toContain('View findings →');
['Passed', 'Not configured', 'Duplicates', 'Conflicts']
.forEach(label => expect(component).toContain(`'${label}'`));
expect(styles).toMatch(/\.client-priority-context\s*\{[^}]*grid-template-columns:\s*repeat\(5, minmax\(0, 1fr\)\);/s);
expect(component.indexOf('className="client-data-facts"')).toBeGreaterThan(component.indexOf('className="client-priority-context"'));
expect((overviewComponent.match(/<DashboardTopFact /g) ?? [])).toHaveLength(6);
expect((securityComponent.match(/<DashboardTopFact /g) ?? [])).toHaveLength(6);
expect(styles).not.toContain('.client-priority-findings ol');
expect(styles).not.toContain('.client-priority-findings { display: grid;');
expect(styles).toMatch(/\.client-domain-information > header h2\s*\{[^}]*font-size:\s*var\(--type-h4-size\);/s);
expect(styles).toMatch(/\.client-domain-information dl > div:first-child\s*\{[^}]*grid-column:\s*1 \/ -1;/s);
expect(styles).toMatch(/\.client-domain-information dt\s*\{[^}]*font-size:\s*var\(--type-table-body-size\);/s);
expect(styles).toMatch(/\.client-env-facts dt\s*\{[^}]*font-weight:\s*var\(--fw-bold\);/s);
});
it('derives card values only from normalized environment snapshot contracts', () => {
const observation = (property: string, label: string, value: string | number | boolean, sectionKey?: string) => ({
version: 1 as const, id: `${property}-${label}`, scanRunId: 'run-1', importFileId: 'file-1',
tool: { id: 'synthetic', name: 'Synthetic', version: '1' }, subject: { type: 'directory' },
property, label, originalValue: value, normalizedValue: value, valueType: typeof value === 'number' ? 'number' as const : typeof value === 'boolean' ? 'boolean' as const : 'string' as const,
state: 'observed' as const, sectionKey, source: { rowIndex: 0 }, mappingVersion: '1', attributes: {}
});
const snapshot = {
version: 1, projectId: 'project-1', scanRunIds: ['run-1'], generatedAt: '2026-08-25T00:00:00.000Z',
coverage: { observed: 4, explicitlyAbsent: 1, empty: 2, restricted: 3, incomplete: 0 },
sections: [{ version: 1, id: 'section-1', scanRunId: 'run-1', importFileId: 'file-1', tool: { id: 'synthetic', name: 'Synthetic', version: '1' }, sectionKey: 'sectionAdminGroups', label: 'Groups', ordinal: 1, state: 'license-restricted', tables: [], source: { rowIndex: 0 }, extractorVersion: '1', attributes: {} }],
observations: [
observation('domain.fqdn', 'Domain', 'example.local'),
observation('risk.anomalies', 'Anomalies score', 42),
observation('admin.name', 'Name', 'Admin A', 'sectionAdminGroups')
]
} satisfies EnvironmentSnapshot;
const summary = dashboardEnvironmentSummary(snapshot);
expect(summary.directoryFacts[0]).toEqual({ label: 'Domain', value: 'example.local' });
expect(summary.directoryFacts.map(item => item.label)).not.toContain('PingCastle');
expect(summary.coverage.map(item => item.value)).toEqual([4, 2, 1, 3, 0]);
expect(summary.identity[0]).toMatchObject({ label: 'Admin groups', value: 1, restricted: true });
expect(summary.risk[1]).toMatchObject({ label: 'Anomalies', value: 42 });
});
it('shows source-backed domain facts beside priority findings without inferred zeroes', () => {
const observation = (property: string, label: string, value: string | number) => ({
version: 1 as const, id: `${property}-${label}`, scanRunId: 'run-1', importFileId: 'file-1',
tool: { id: 'synthetic', name: 'Synthetic', version: '1' }, subject: { type: 'directory' },
property, label, originalValue: value, normalizedValue: value, valueType: typeof value === 'number' ? 'number' as const : 'string' as const,
state: 'observed' as const, source: { rowIndex: 0 }, mappingVersion: '1', attributes: {}
});
const snapshot = {
version: 1, projectId: 'project-1', scanRunIds: ['run-1'], generatedAt: '2026-09-20T00:00:00.000Z',
coverage: { observed: 3, explicitlyAbsent: 0, empty: 0, restricted: 0, incomplete: 0 }, sections: [],
observations: [
observation('domain.fqdn', 'Domain', 'example.local'),
observation('domain.netbios_name', 'Netbios Name', 'EXAMPLE'),
observation('forest.name', 'Forest', 'example.local'),
observation('domain.functional_level', 'Domain Functional Level', 'Windows Server 2016'),
observation('domain.controller_count', 'DC count', 3),
observation('forest.functional_level', 'Forest Functional Level', 'Windows Server 2016'),
observation('forest.schema_version', 'Schema version', 'Windows Server 2019'),
observation('domain.created_at', 'Creation date', '2026-05-27 10:24:53Z'),
observation('entra.configuration_detected', 'Entra ID configuration detected', 'Not detected')
]
} satisfies EnvironmentSnapshot;
expect(dashboardDomainInformation(snapshot)).toEqual([
{ label: 'Domain', value: 'example.local' },
{ label: 'NetBIOS name', value: 'EXAMPLE' },
{ label: 'Forest', value: 'example.local' },
{ label: 'Domains', value: '1' },
{ label: 'Domain controllers', value: '3' },
{ label: 'Domain functional level', value: 'Windows Server 2016' },
{ label: 'Forest functional level', value: 'Windows Server 2016' },
{ label: 'DC OS', value: '—' },
{ label: 'Schema version', value: 'Windows Server 2019' },
{ label: 'Domain created', value: '2026-05-27 10:24:53Z' },
{ label: 'Entra ID', value: 'Not detected' }
]);
});
it('shows DC OS only from observed domain-controller rows and retains distinct DC versions', () => {
const os = (id: string, type: string, value: string, state: 'observed' | 'unknown' = 'observed') => ({
version: 1 as const, id, scanRunId: 'run-1', importFileId: 'file-1',
tool: { id: 'synthetic', name: 'Synthetic', version: '1' }, subject: { type },
property: 'operating_system.name', label: 'Operating System', originalValue: value,
normalizedValue: value, valueType: 'string' as const, state,
source: { rowIndex: 0 }, mappingVersion: '1', attributes: {}
});
const base = { version: 1 as const, projectId: 'project-1', scanRunIds: ['run-1'],
generatedAt: '2026-09-24T00:00:00.000Z',
coverage: { observed: 0, explicitlyAbsent: 0, empty: 0, restricted: 0, incomplete: 0 }, sections: [] };
const general = os('general', 'computer-information.operating-systems', 'Windows Server 2022');
const dc = os('dc', 'computer-information.domain-controllers', 'Windows 2019');
expect(dashboardDomainInformation({ ...base, observations: [general] }).find(item => item.label === 'DC OS')?.value).toBe('—');
expect(dashboardDomainInformation({ ...base, observations: [general, dc, os('dc2', dc.subject.type, 'Windows 2019'),
os('dc3', dc.subject.type, 'Windows Server 2025'), os('unknown', dc.subject.type, 'Windows 2008', 'unknown')] })
.find(item => item.label === 'DC OS')?.value).toBe('Windows 2019, Windows Server 2025');
});
it('removes only a trailing scan timestamp from source labels', () => {
expect(dashboardScanSourceLabel('PurpleKnight · 2026-08-22T08:15:30.000Z')).toBe('PurpleKnight');
expect(dashboardScanSourceLabel('CIS-CAT Pro · 2026-08-22')).toBe('CIS-CAT Pro');
expect(dashboardScanSourceLabel('Scanner · Production')).toBe('Scanner · Production');
});
it('groups scan runs into one selectable risk profile per tool', () => {
expect(dashboardRiskTools([
{ value: 'pingcastle-a', label: 'PingCastle · 2026-08-22T08:15:30.000Z', count: 20 },
{ value: 'pingcastle-b', label: 'PingCastle · 2026-08-23T08:15:30.000Z', count: 24 },
{ value: 'purpleknight-a', label: 'PurpleKnight · 2026-08-23T09:00:00.000Z', count: 30 }
])).toEqual([
{ id: 'pingcastle', label: 'PingCastle', sourceValues: ['pingcastle-a', 'pingcastle-b'], count: 44 },
{ id: 'purpleknight', label: 'PurpleKnight', sourceValues: ['purpleknight-a'], count: 30 }
]);
});
it('adds only the severity-less CIS-CAT share as a neutral distribution segment', () => {
expect(dashboardCisCatSeverityRemainder([
{ value: 'cis-cat-run', label: 'CIS-CAT Pro · 2026-08-17', count: 411 },
{ value: 'maester-run', label: 'Maester · 2026-07-28', count: 318 }
], [])).toEqual({
value: 'dashboard-cis-cat-without-severity',
label: 'CIS-CAT',
count: 411,
sourceValues: ['cis-cat-run']
});
expect(dashboardCisCatSeverityRemainder([
{ value: 'cis-cat-run', label: 'CIS-CAT Pro · 2026-08-17', count: 411 }
], [{ value: 'high', label: 'High', count: 12 }])?.count).toBe(399);
expect(styles).toContain('.severity-dashboard-cis-cat-without-severity { --severity: var(--donut-not-run); }');
});
it('keeps the complete PingCastle risk catalog including zero scores', () => {
expect(dashboardPingCastleRiskCategories([
{ property: 'risk.anomalies', normalizedValue: 20, originalValue: '20' },
{ property: 'risk.stale_objects', normalizedValue: 10, originalValue: '10' },
{ property: 'risk.privileged_accounts', normalizedValue: 7, originalValue: '7' },
{ property: 'risk.trusts', normalizedValue: 0, originalValue: '0' }
], [])).toEqual([
{ value: 'anomalies', label: 'Anomalies', count: 20 },
{ value: 'stale objects', label: 'Stale Objects', count: 10 },
{ value: 'privileged accounts', label: 'Privileged Accounts', count: 7 },
{ value: 'trusts', label: 'Trusts', count: 0 }
]);
});
it('counts only explicit IOE-found source results for the top summary', () => {
const record = (id: string, value: string): FindingRecord => ({
id, index: 0, title: id,
fields: [{ index: 0, label: 'Result', value }],
filled: [], meta: []
});
expect(dashboardExplicitIoeCount([
record('ioe', 'IOE Found'),
record('ioes', 'IOEs found'),
record('deviation', 'Failed'),
record('pass', 'Passed')
])).toBe(2);
expect(dashboardExplicitIoeRecordIds([
record('ioe', 'IOE Found'),
record('ioes', 'IOEs found'),
record('deviation', 'Failed')
])).toEqual(['ioe', 'ioes']);
});
});