131 lines
8.3 KiB
TypeScript
131 lines
8.3 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { act } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import type { ImportedDataset } from '../../src/domain/dataset';
|
|
import { rowsToCsv } from '../../src/exports/csv-export';
|
|
import { DefenderVulnerabilityPanel } from '../../src/features/defender-for-iot/DefenderVulnerabilityPanel';
|
|
import { makeSheet } from '../../src/importers/sheet-factory';
|
|
|
|
vi.mock('../../src/features/search-filter/WorkTimeslotControl', () => ({
|
|
WorkTimeslotControl: () => <select aria-label="Synthetic timeslot" />
|
|
}));
|
|
|
|
function dataset(): ImportedDataset {
|
|
const summary = [
|
|
['Name', 'IP', 'Site', 'Sensor', 'TotalCVEs', 'Critical', 'High', 'Medium', 'Low'],
|
|
['device-a', '192.0.2.1', 'Site A', 'sensor-a', '1', '1', '0', '0', '0'],
|
|
['device-b', '192.0.2.2', 'Site B', 'sensor-b', '2', '1', '1', '0', '0']
|
|
];
|
|
const observationA = { name: 'device-a', ip: '192.0.2.1', site: 'Site A', sensor: 'sensor-a', cve: 'CVE-2026-1001', severity: 'Critical', description: 'Synthetic description A', additionalSourceFields: {}, source: { tableIndex: 1, rowIndex: 1 } };
|
|
const observationB = { name: 'device-b', ip: '192.0.2.2', site: 'Site B', sensor: 'sensor-b', cve: 'CVE-2026-1002', severity: 'High', description: 'Synthetic description B', additionalSourceFields: {}, source: { tableIndex: 1, rowIndex: 2 } };
|
|
const observationC = { name: 'device-b', ip: '192.0.2.2', site: 'Site B', sensor: 'sensor-b', cve: 'CVE-2026-1001', severity: 'Critical', description: 'Synthetic description A', additionalSourceFields: {}, source: { tableIndex: 1, rowIndex: 3 } };
|
|
const detail = [
|
|
['CVE', 'Device Identity', 'Name', 'IP', 'Site', 'Reporting Sensors', 'Severity', 'Beschreibung', 'Observation Count', 'Source Observations'],
|
|
['CVE-2026-1001', 'ip:192.0.2.1', 'device-a', '192.0.2.1', 'Site A', 'sensor-a', 'Critical', 'Synthetic description A', '1', JSON.stringify([observationA])],
|
|
['CVE-2026-1002', 'ip:192.0.2.2', 'device-b', '192.0.2.2', 'Site B', 'sensor-b', 'High', 'Synthetic description B', '1', JSON.stringify([observationB])],
|
|
['CVE-2026-1001', 'ip:192.0.2.2', 'device-b', '192.0.2.2', 'Site B', 'sensor-b', 'Critical', 'Synthetic description A', '1', JSON.stringify([observationC])]
|
|
];
|
|
const vulnerabilitySheet = makeSheet('Vulnerabilities', detail, rowsToCsv(detail), 'synthetic.xlsx', { sourceTableIndex: 1, sourceSheet: 'Vulnerabilities', toolId: 'defender-for-iot-vulnerabilities' });
|
|
vulnerabilitySheet.analysis.records.forEach((record, index) => Object.assign(record, {
|
|
findingOccurrenceId: `occurrence-${index + 1}`,
|
|
findingDefinitionId: index === 2 ? 'definition-1' : `definition-${index + 1}`,
|
|
scanRunId: 'run-1', importFileId: 'file-1'
|
|
}));
|
|
return {
|
|
fileName: 'Synthetic project', fileSize: 1, lastModified: 0, format: 'Project · Microsoft Defender for IoT', rawText: '',
|
|
profileId: 'defender-for-iot-vulnerabilities', profileVersion: '1.0.0', scanTool: 'Microsoft Defender for IoT',
|
|
sheets: [
|
|
vulnerabilitySheet,
|
|
makeSheet('Assets', summary, rowsToCsv(summary), 'synthetic.xlsx', { sourceTableIndex: 0, sourceSheet: 'Assets', toolId: 'defender-for-iot-vulnerabilities' })
|
|
]
|
|
};
|
|
}
|
|
|
|
function toggleButtons(container: HTMLElement): HTMLButtonElement[] {
|
|
return [...container.querySelectorAll<HTMLButtonElement>('.defender-view .defender-row-toggle')];
|
|
}
|
|
|
|
function expectExpandedInline(container: HTMLElement, button: HTMLButtonElement) {
|
|
expect(button.getAttribute('aria-expanded')).toBe('true');
|
|
expect(button.querySelector('.triangle-icon-down')).not.toBeNull();
|
|
const detailRow = button.closest('tr')?.nextElementSibling;
|
|
expect(detailRow?.classList.contains('defender-detail-row')).toBe(true);
|
|
expect(detailRow?.querySelector(`#${button.getAttribute('aria-controls')}`)).not.toBeNull();
|
|
expect(container.querySelectorAll('.defender-view > .defender-table-wrap > .defender-table > tbody > .defender-detail-row')).toHaveLength(1);
|
|
}
|
|
|
|
function exerciseView(container: HTMLElement, buttons: HTMLButtonElement[]) {
|
|
expect(buttons).toHaveLength(2);
|
|
expect(buttons[0].querySelector('.triangle-icon-right')).not.toBeNull();
|
|
act(() => buttons[0].click());
|
|
expectExpandedInline(container, buttons[0]);
|
|
act(() => buttons[1].click());
|
|
expect(buttons[0].getAttribute('aria-expanded')).toBe('false');
|
|
expectExpandedInline(container, buttons[1]);
|
|
act(() => buttons[1].click());
|
|
expect(container.querySelector('.defender-detail-row')).toBeNull();
|
|
}
|
|
|
|
describe('Defender inline drilldowns', () => {
|
|
it('expands, moves, and collapses details beside CVE, device, and site rows', () => {
|
|
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const container = document.createElement('div');
|
|
const root = createRoot(container);
|
|
act(() => root.render(<DefenderVulnerabilityPanel dataset={dataset()} />));
|
|
|
|
const cveListHeaders = [...container.querySelectorAll('.defender-view > .defender-table-wrap > .defender-table > thead th')].map(header => header.textContent);
|
|
expect(cveListHeaders).toEqual(['CVE', 'Severity', 'Affected devices', 'Sites', 'Sensors']);
|
|
const cveButtons = toggleButtons(container);
|
|
exerciseView(container, cveButtons);
|
|
act(() => cveButtons[0].click());
|
|
expect(container.querySelector('.defender-detail-row')?.textContent).toContain('Synthetic description A');
|
|
expect(container.querySelector('#defender-cve-detail-0 details')).toBeNull();
|
|
const evidenceHeaders = [...container.querySelectorAll('#defender-cve-detail-0 > .defender-table-wrap > .defender-table > thead th')].map(header => header.textContent);
|
|
expect(evidenceHeaders).toEqual(['Device', 'IP', 'Site', 'Sensor']);
|
|
expect(container.querySelectorAll('#defender-cve-detail-0 > .defender-table-wrap > .defender-table > tbody > tr')).toHaveLength(2);
|
|
act(() => cveButtons[0].click());
|
|
|
|
const devicesTab = [...container.querySelectorAll<HTMLButtonElement>('.defender-tabs button')].find(button => button.textContent === 'Devices');
|
|
act(() => devicesTab?.click());
|
|
const deviceButtons = toggleButtons(container);
|
|
exerciseView(container, deviceButtons);
|
|
act(() => deviceButtons[0].click());
|
|
const deviceCveHeaders = [...container.querySelectorAll('#defender-device-detail-0 > .defender-table-wrap > .defender-table > thead th')].map(header => header.textContent);
|
|
expect(deviceCveHeaders).toEqual(['CVE', 'Severity']);
|
|
act(() => deviceButtons[0].click());
|
|
|
|
const sitesTab = [...container.querySelectorAll<HTMLButtonElement>('.defender-tabs button')].find(button => button.textContent === 'Sites');
|
|
act(() => sitesTab?.click());
|
|
exerciseView(container, toggleButtons(container));
|
|
|
|
act(() => root.unmount());
|
|
});
|
|
|
|
it('shows exact device coverage and previews each current CVE once before grouped acceptance', () => {
|
|
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const container = document.createElement('div');
|
|
document.body.append(container);
|
|
const root = createRoot(container);
|
|
try {
|
|
act(() => root.render(<DefenderVulnerabilityPanel dataset={dataset()} activeScanRunIds={['run-1']} onScopedRiskAcceptance={vi.fn()} />));
|
|
const devicesTab = [...container.querySelectorAll<HTMLButtonElement>('.defender-tabs button')].find(button => button.textContent === 'Devices')!;
|
|
act(() => devicesTab.click());
|
|
const deviceButtons = toggleButtons(container);
|
|
act(() => deviceButtons[1].click());
|
|
const detail = container.querySelector('#defender-device-detail-1')!;
|
|
expect(detail.querySelector('.defender-risk-coverage')?.textContent).toContain('0 of 2 current findings covered');
|
|
expect([...detail.querySelectorAll('.defender-table thead th')].map(header => header.textContent)).toEqual(['CVE', 'Severity']);
|
|
const acceptAll = [...detail.querySelectorAll<HTMLButtonElement>('button')].find(button => button.textContent === 'Accept all current findings')!;
|
|
act(() => acceptAll.click());
|
|
const scope = document.querySelector('.risk-acceptance-system-scope')!;
|
|
expect(scope.textContent).toContain('2 covered CVEs');
|
|
expect([...scope.querySelectorAll('li')].map(item => item.textContent)).toEqual(['CVE-2026-1001', 'CVE-2026-1002']);
|
|
} finally {
|
|
act(() => root.unmount());
|
|
container.remove();
|
|
}
|
|
});
|
|
});
|