151 lines
12 KiB
TypeScript
151 lines
12 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act } from 'react';
|
|
import { readFileSync } from 'node:fs';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { useApp } from '../../src/app/AppProvider';
|
|
import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences';
|
|
import { AssessorApiClient } from '../../src/api/assessor-api-client';
|
|
import { DashboardPanel } from '../../src/features/dashboard/DashboardPanel';
|
|
import { ClientSecurityPanel } from '../../src/features/dashboard/ClientSecurityPanel';
|
|
import { makeSheet } from '../../src/importers/sheet-factory';
|
|
import { genericProfile } from '../../src/profiles/generic/generic-profile';
|
|
import { clientFactsSnapshot, clientFactsState } from '../fixtures/client-info-facts';
|
|
import { emptyHeatmapContext } from '../assessment-context/context-heatmap-fixture';
|
|
|
|
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(); localStorage.clear(); resetEnginePreferencesForTests(); });
|
|
|
|
describe.each(['light', 'dark'])('Security findings tab (%s)', theme => {
|
|
it('opens a bold finding without scanner metadata and places Description directly after Affected', async () => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
document.documentElement.dataset.theme = theme;
|
|
const sheet = makeSheet('Synthetic', [['Name', 'Status', 'Severity', 'Result Message', 'Number of Results', 'Description', 'Impact', 'Remediation', 'Source Rule ID'],
|
|
['DC Sync rights', 'Failed', 'Critical', 'Retained result.', '3', 'Retained result.', 'Retained impact.', 'Retained fix.', 'SYNTHETIC-001']
|
|
], '', 'synthetic.csv', { isProjectSheet: true });
|
|
const record = sheet.analysis.records[0];
|
|
const dataset = { fileName: 'Synthetic', fileSize: 0, lastModified: 0, format: 'csv', rawText: '', profileId: 'purple-knight', scanTool: 'Purple Knight', sheets: [sheet] };
|
|
const original = JSON.stringify(record);
|
|
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
|
|
const style = document.createElement('style');
|
|
style.textContent = readFileSync('src/styles/ui-tokens.css', 'utf8') + readFileSync('src/styles/client-security.css', 'utf8');
|
|
document.head.append(style);
|
|
try {
|
|
await act(async () => root.render(<ClientSecurityPanel dataset={dataset} sheetId={sheet.id} toolId="purple-knight" contextStatus="off" total={1}
|
|
domains={[]} groups={[{ id: 'all', label: 'All findings', severity: 'critical', records: [record], domains: [] }]}/>));
|
|
await act(async () => host.querySelector<HTMLButtonElement>('.client-security-group-toggle')!.click());
|
|
const toggle = host.querySelector<HTMLButtonElement>('.client-security-finding-toggle')!;
|
|
expect(toggle.textContent).toBe('DC Sync rights');
|
|
expect(toggle.querySelector('small')).toBeNull();
|
|
expect(toggle.querySelector('strong')!.textContent).toBe('DC Sync rights');
|
|
expect(getComputedStyle(toggle.querySelector('strong')!).fontWeight).toBe('var(--fw-bold)');
|
|
await act(async () => toggle.click());
|
|
const labels = [...host.querySelectorAll('.client-security-finding-detail > div:first-child .client-security-source > span')].map(item => item.textContent);
|
|
expect(labels).toContain('Affected');
|
|
expect(labels[labels.indexOf('Affected') + 1]).toBe('Description');
|
|
expect(labels.indexOf('Description')).toBeLessThan(labels.indexOf('Impact'));
|
|
expect(host.textContent).toContain('Retained fix.');
|
|
expect(JSON.stringify(record)).toBe(original);
|
|
} finally { await act(async () => root.unmount()); style.remove(); }
|
|
});
|
|
|
|
it('keeps all 105 deviations in heatmap order, reconciles domains and opens the original explanation and remediation without changing Work', async () => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} });
|
|
document.documentElement.dataset.theme = theme;
|
|
resetEnginePreferencesForTests();
|
|
const state = clientFactsState();
|
|
const sheet = makeSheet('Synthetic', [['Name', 'Status', 'Severity', 'Description', 'Remediation'],
|
|
...Array.from({ length: 105 }, (_, index) => [`Control ${String(index).padStart(3, '0')}`, 'Failed', 'High', '<p>Synthetic retained explanation.</p>', 'Synthetic exact remediation.']),
|
|
['Passed control', 'Passed', 'Critical', 'Not a deviation', ''], ['Unassessed control', 'Not configured', '', '', '']
|
|
], '', 'synthetic.csv', { isProjectSheet: true });
|
|
const context = emptyHeatmapContext();
|
|
sheet.analysis.records.slice(0, 105).forEach((record, index) => {
|
|
const occurrenceId = `synthetic-occurrence-${index}`; record.findingOccurrenceId = occurrenceId;
|
|
const clusterId = index < 104 ? 'cloud-privileged-access-exposure' : 'credential-relay-exposure';
|
|
const conditionId = context.clusters.find(cluster => cluster.clusterId === clusterId)!.conditions[0].conditionId;
|
|
context.observations.push({ id: occurrenceId, conditionId, occurrenceId, resultProjectionId: occurrenceId,
|
|
tool: 'purple-knight', sourceRuleId: 'SYNTHETIC', ruleText: record.title, matchedBy: 'rule-id', outcome: 'fail', assertion: 'observed-by-scanner' });
|
|
context.findings.push({ occurrenceId, resultProjectionId: occurrenceId, tool: 'purple-knight', sourceRuleId: 'SYNTHETIC',
|
|
observationIds: [occurrenceId], conditionIds: [conditionId], clusterIds: [clusterId], declaredReferenceIds: [], cisControlAreaIds: [] });
|
|
});
|
|
const openWork = vi.fn(), setFilter = vi.fn();
|
|
vi.mocked(useApp).mockReturnValue({ ...state, dataset: { ...state.dataset, sheets: [sheet] }, activeSheet: sheet,
|
|
activeProfile: genericProfile, setFacetFilter: setFilter } as unknown as ReturnType<typeof useApp>);
|
|
const original = JSON.stringify(useApp().dataset);
|
|
vi.spyOn(AssessorApiClient.prototype, 'getEnvironmentSnapshot').mockResolvedValue(clientFactsSnapshot);
|
|
vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockResolvedValue({ records: [] } as never);
|
|
vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockResolvedValue(context);
|
|
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
|
|
const style = document.createElement('style');
|
|
style.textContent = readFileSync('src/styles/base.css', 'utf8') + readFileSync('src/styles/client-security.css', 'utf8');
|
|
document.head.append(style);
|
|
try {
|
|
await act(async () => root.render(<DashboardPanel onOpenWork={openWork}/>));
|
|
const tabs = host.querySelectorAll<HTMLButtonElement>('[role="tab"]');
|
|
await act(async () => tabs[1].click());
|
|
const panel = host.querySelector<HTMLElement>('[role="tabpanel"]:not([hidden])')!;
|
|
expect(panel.textContent).toContain('105 deviations');
|
|
const groups = [...panel.querySelectorAll<HTMLElement>('.client-security-group')];
|
|
expect(groups[0].dataset.cluster).toBe('cloud-privileged-access-exposure');
|
|
expect(groups.map(group => group.dataset.cluster)).toEqual([...host.querySelectorAll<HTMLElement>('.context-heatmap__cell')].map(cell => cell.dataset.cluster));
|
|
expect(groups.map(group => Number(group.querySelector('.client-security-group-toggle > strong')!.textContent)).reduce((sum, count) => sum + count, 0)).toBe(105);
|
|
expect(panel.querySelector('[data-cluster="unassigned"]')).toBeNull();
|
|
const map = panel.querySelector<HTMLDetailsElement>('.client-security-map')!;
|
|
expect(map.open).toBe(false);
|
|
map.open = true;
|
|
expect(getComputedStyle(map.querySelector('.client-security-map-scroll > svg')!).height).toBe('auto');
|
|
expect(getComputedStyle(map.querySelector('summary > svg')!).width).not.toBe('1040px');
|
|
expect(panel.querySelectorAll('.client-security-flow')).not.toHaveLength(0);
|
|
expect(panel.querySelector('svg[role="img"]')!.textContent).toContain('104');
|
|
const category = groups[0].querySelector<HTMLButtonElement>('.client-security-group-toggle')!;
|
|
expect(category.getAttribute('aria-expanded')).toBe('false');
|
|
await act(async () => category.click());
|
|
expect(groups[0].textContent).toContain('Technical approach');
|
|
expect(groups[0].querySelectorAll('.client-security-finding')).toHaveLength(104);
|
|
expect(groups[0].textContent).not.toContain('Passed control');
|
|
const finding = groups[0].querySelector<HTMLButtonElement>('.client-security-finding-toggle')!;
|
|
await act(async () => finding.click());
|
|
expect(finding.getAttribute('aria-expanded')).toBe('true');
|
|
expect(groups[0].textContent).toContain('Synthetic retained explanation.');
|
|
expect(groups[0].textContent).toContain('Synthetic exact remediation.');
|
|
await act(async () => tabs[0].click()); await act(async () => tabs[1].click());
|
|
expect(category.getAttribute('aria-expanded')).toBe('true');
|
|
expect(finding.getAttribute('aria-expanded')).toBe('true');
|
|
await act(async () => finding.click());
|
|
expect(groups[0].querySelector('.client-security-finding-detail')).toBeNull();
|
|
expect(setFilter).not.toHaveBeenCalled(); expect(openWork).not.toHaveBeenCalled();
|
|
expect(JSON.stringify(useApp().dataset)).toBe(original);
|
|
await act(async () => setEngineEnabled('report', false));
|
|
expect(panel.textContent).not.toContain('Technical approach');
|
|
expect(panel.textContent).toContain('105 deviations');
|
|
await act(async () => setEngineEnabled('context', false));
|
|
expect(panel.querySelector('.client-security-map')).toBeNull();
|
|
expect(panel.textContent).toContain('105 deviations');
|
|
expect(panel.textContent).toContain('All findings');
|
|
} finally { await act(async () => root.unmount()); style.remove(); }
|
|
});
|
|
|
|
it('counts a rolled-up GPO once and keeps distinct stored baseline fixes separate', async () => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
document.documentElement.dataset.theme = theme;
|
|
const sheet = makeSheet('Synthetic GPO', [['Name', 'Status', 'Category'], ['Stored setting', 'Fail', 'GPOs']], '', 'synthetic.csv', { isProjectSheet: true });
|
|
const record = sheet.analysis.records[0];
|
|
record.policyBaselineComparisons = ['A', 'B'].map(name => ({ baseline: `Synthetic baseline ${name}`, status: 'Fail', summary: 'Stored comparison', context: '', scopeNotice: '', explanation: '',
|
|
remediation: `Retained fix ${name}`, recommendations: [{ template: `Template ${name}`, settingId: 'Synthetic exact setting', comparison: 'Fail', expected: name, additional: [], missing: [] }] }));
|
|
const dataset = { fileName: 'Synthetic', fileSize: 0, lastModified: 0, format: 'csv', rawText: '', profileId: 'generic', scanTool: 'Generic', sheets: [sheet] };
|
|
const original = JSON.stringify(record);
|
|
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
|
|
try {
|
|
await act(async () => root.render(<ClientSecurityPanel dataset={dataset} sheetId={sheet.id} toolId="group-policy" contextStatus="off" total={1}
|
|
domains={[]} groups={[{ id: 'all', label: 'All findings', severity: 'unknown', records: [record], domains: [] }]}/>));
|
|
await act(async () => host.querySelector<HTMLButtonElement>('.client-security-group-toggle')!.click());
|
|
await act(async () => host.querySelector<HTMLButtonElement>('.client-security-finding-toggle')!.click());
|
|
expect(host.querySelectorAll('.client-security-finding')).toHaveLength(1);
|
|
expect(host.textContent).toContain('Retained fix A'); expect(host.textContent).toContain('Retained fix B');
|
|
expect(JSON.stringify(record)).toBe(original);
|
|
} finally { await act(async () => root.unmount()); }
|
|
});
|
|
});
|