Assessor_2.0/tests/features/results-deviation-evidence....

99 lines
7.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @vitest-environment jsdom
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { useApp } from '../../src/app/AppProvider';
import { useFilteredRecords } from '../../src/features/search-filter/use-filtered-records';
import { DatasetViewer } from '../../src/features/dataset-viewer/DatasetViewer';
import { FloatingResultsWindow } from '../../src/features/linked-results/FloatingResultsWindow';
import { makeSheet } from '../../src/importers/sheet-factory';
import { resultSheetRowToneFor } from '../../src/features/linked-results/result-message';
vi.mock('../../src/app/AppProvider', () => { const useApp = vi.fn(); return { useApp, useOptionalApp: useApp }; });
vi.mock('../../src/features/search-filter/use-filtered-records', () => ({ useFilteredRecords: vi.fn() }));
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); Reflect.deleteProperty(HTMLElement.prototype, 'scrollIntoView'); });
function smbFixture() {
const findings = makeSheet('Synthetic Work', [
['Title', 'Status', 'Source Rule ID', 'Verwandte Daten', 'Source: Scan'],
['SMB v1 activated on 1 DC', 'Matched', 'S-SMB-v1', 'synthetic-file::rule-details-S-SMB-v1,synthetic-file::domaincontrollersdetail', 'PingCastle · synthetic']
], '', 'synthetic.html', { toolId: 'pingcastle', isProjectSheet: true });
const finding = findings.analysis.records[0]; finding.importFileId = 'synthetic-file';
const detail = makeSheet('PingCastle · Details', [['Domain controller'], ['DUMMY-DC']], '', 'synthetic.html',
{ id: 'pingcastle-section-synthetic-file::rule-details-S-SMB-v1' });
const inventory = makeSheet('PingCastle · Domain controllers', [['Domain controller', 'SMB v1'], ['DUMMY-OTHER', 'NO']], '', 'synthetic.html',
{ id: 'pingcastle-section-synthetic-file::domaincontrollersdetail' });
return { findings, finding, detail, inventory };
}
describe.each(['dark', 'light'])('Results affected evidence (%s)', theme => {
it('opens SMB Results through Work, marks only the exact affected list and retains proof when Work filters hide the finding', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} });
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', { value: vi.fn(), configurable: true });
document.documentElement.dataset.theme = theme;
const { findings, finding, detail, inventory } = smbFixture();
const before = JSON.stringify([finding, detail, inventory]);
vi.mocked(useApp).mockReturnValue({ dataset: { sheets: [findings, detail, inventory], rawText: '', profileId: 'pingcastle' },
activeSheet: findings, view: 'records', search: '', titleColumn: 'Title', selectedRecordId: finding.id,
reviews: {}, facetFilters: {}, statusFilter: 'all', actionOnly: false,
setView: vi.fn(), setSelectedRecord: vi.fn(), updateReview: vi.fn(), resetFilters: vi.fn()
} as unknown as ReturnType<typeof useApp>);
vi.mocked(useFilteredRecords).mockReturnValue([finding]);
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
try {
await act(async () => root.render(<DatasetViewer/>));
await act(async () => [...host.querySelectorAll<HTMLButtonElement>('button')].find(button => button.textContent === 'Results')!.click());
const red = host.querySelectorAll('.rich-text-table-row--violation');
expect(red).toHaveLength(1); expect(red[0].textContent).toBe('DUMMY-DC');
const other = [...host.querySelectorAll('tbody tr')].find(row => row.textContent?.includes('DUMMY-OTHER'))!;
expect(other.classList.contains('rich-text-table-row--violation')).toBe(false);
vi.mocked(useFilteredRecords).mockReturnValue([]);
await act(async () => root.render(<DatasetViewer/>));
expect(host.querySelector('.rich-text-table-row--violation')!.textContent).toBe('DUMMY-DC');
expect(JSON.stringify([finding, detail, inventory])).toBe(before);
} finally { await act(async () => root.unmount()); host.remove(); }
});
it('colors source-marked Maester inline rows and leaves missing/conflicting status or misleading names neutral', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} });
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
const value = '| Object | Status |\n| --- | --- |\n| DUMMY-BAD | ❌ Failed |\n| DUMMY-GOOD | ✅ Passed |\n| Fail | Unknown |\n| DUMMY-UNKNOWN | Investigate |\n| ✅ DUMMY-CONFLICT | ❌ Failed |';
try {
await act(async () => root.render(<FloatingResultsWindow id="synthetic-inline" title="Results – Synthetic" sheets={[]}
toolId="maester" outcome="fail" sections={[{ title: 'Test result', value }]} color="" zIndex={1} offset={0} search=""
onActivate={vi.fn()} onClose={vi.fn()}/>));
expect(host.querySelectorAll('.rich-text-table-row--violation')).toHaveLength(1);
expect(host.querySelectorAll('.rich-text-table-row--match')).toHaveLength(1);
expect(host.querySelectorAll('tbody tr:not([class])')).toHaveLength(3);
} finally { await act(async () => root.unmount()); host.remove(); }
});
});
it('fails closed on unknown rules, foreign imports, changed layouts, missing scope and non-fail outcomes', () => {
const { finding, detail, inventory } = smbFixture(); const row = detail.analysis.records[0];
const tone = (f = finding, s = detail, outcome: 'fail' | 'pass' | 'warning' = 'fail') => resultSheetRowToneFor('pingcastle', row, outcome, { finding: f, sheet: s });
expect(tone()).toBe('violation'); expect(tone(finding, detail, 'pass')).toBeUndefined(); expect(tone(finding, detail, 'warning')).toBeUndefined();
expect(tone({ ...finding, importFileId: 'foreign' })).toBeUndefined();
expect(tone({ ...finding, importFileId: undefined })).toBeUndefined();
expect(tone({ ...finding, fields: finding.fields.map(field => field.label === 'Source Rule ID' ? { ...field, value: 'S-Unknown' } : field) })).toBeUndefined();
expect(tone(finding, { ...detail, analysis: { ...detail.analysis, headers: ['Unknown'] } })).toBeUndefined();
expect(tone(finding, inventory)).toBeUndefined();
});
it('marks only native-linked, non-ignored Purple Knight affected rows and retains neutral ambiguous/foreign evidence', () => {
const findings = makeSheet('Synthetic', [['Name', 'Status', 'Source Rule ID', 'Result', 'Source: File'],
['Synthetic spooler', 'IOE Found', 'SI000077', 'SI000077', 'synthetic-pk.html']], '', 'synthetic-pk.html');
const finding = findings.analysis.records[0];
const evidence = makeSheet('Purple Knight · SI000077', [['HostName', 'Ignored'], ['DUMMY-DC', 'False'], ['DUMMY-IGNORED', 'True']], '', 'synthetic-pk.html', { sourceTableIndex: 3 });
const tone = (sheet = evidence, row = evidence.analysis.records[0], outcome: 'pass' | 'fail' = 'fail') =>
resultSheetRowToneFor('purple-knight', row, outcome, { finding, sheet });
expect(tone()).toBe('violation');
expect(tone(evidence, evidence.analysis.records[1])).toBeUndefined();
expect(tone(evidence, evidence.analysis.records[0], 'pass')).toBeUndefined();
expect(tone({ ...evidence, sourceFile: 'foreign.html' })).toBeUndefined();
expect(tone({ ...evidence, name: 'Similar spooler title', sourceSheet: 'Other' })).toBeUndefined();
expect(tone({ ...evidence, analysis: { ...evidence.analysis, headers: ['HostName', 'Unknown'] } })).toBeUndefined();
});