Assessor_2.0/tests/features/scanner-first-read.test.tsx

99 lines
7.5 KiB
TypeScript

// @vitest-environment jsdom
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, expect, it } from 'vitest';
import type { DatasetSheet } from '../../src/domain/dataset';
import type { FindingRecord } from '../../src/domain/finding';
import { FindingReader } from '../../src/features/finding-reader/FindingReader';
import { makeSheet } from '../../src/importers/sheet-factory';
/** Synthetic scanner finding with the given retained fields. */
function record(fields: Array<[string, string]>): FindingRecord {
const values = fields.map(([label, value], index) => ({ label, value, index }));
return { id: 'synthetic', index: 0, title: 'Synthetic finding', fields: values, filled: values, meta: [] };
}
function sheet(rows: string[][]): DatasetSheet {
return makeSheet('Synthetic results', rows, '', 'synthetic.html');
}
function render(item: FindingRecord, toolId: string, linkedSheets: DatasetSheet[] = []) {
const container = document.createElement('div');
container.innerHTML = renderToStaticMarkup(<FindingReader record={item} title={item.title} linkedSheets={linkedSheets} resultReferences={[]}
search="" position={0} recordCount={1} toolId={toolId} onPrevious={() => {}} onNext={() => {}} onReview={() => {}}/>);
const values = (selector: string) => Object.fromEntries([...container.querySelectorAll(`${selector} .policy-problem__values > div`)]
.map(row => [row.querySelector('dt')!.textContent, row.querySelector('dd')!.textContent]));
return { container, values };
}
const headings = (container: HTMLElement) => [...container.querySelectorAll('.reader-summary > div > h4')].map(item => item.textContent);
describe('scanner first read', () => {
it('PingCastle: evidence and result rows as the problem, the advised solution as the next step, background below', () => {
const item = record([['Rule ID', 'S-Synthetic'], ['Title', 'Synthetic finding'], ['Status', 'Matched'], ['Evidence', 'Synthetic accounts found: 3'],
['Description', 'Synthetic description.'], ['Technical explanation', 'Synthetic explanation.'],
['Advised solution', 'Change the synthetic setting.\n\n$Synthetic = Get-Synthetic'], ['Documentation', 'Synthetic documentation']]);
const { container, values } = render(item, 'pingcastle', [sheet([['Account'], ['synthetic-a'], ['synthetic-b'], ['synthetic-c']])]);
const problem = container.querySelector('.finding-first-read.policy-problem')!;
expect(problem.getAttribute('data-outcome')).toBe(container.querySelector('.outcome-mark')?.getAttribute('data-outcome'));
expect(values('.finding-first-read')).toEqual({ Result: 'Synthetic accounts found: 3', Results: '3 rows in 1 table' });
const next = problem.nextElementSibling!;
expect(next.matches('section.policy-next-step')).toBe(true);
expect(next.textContent).toContain('Change the synthetic setting.');
// The remaining remediation text is complete, only behind a disclosure.
expect(next.querySelector('details summary')?.textContent).toBe('Full remediation');
expect(next.querySelector('details')?.textContent).toContain('$Synthetic = Get-Synthetic');
expect(headings(container)).toEqual(['Description', 'Technical explanation', 'Documentation']);
});
it('keeps a PingCastle PowerShell command complete and readable outside the prose', () => {
const command = 'Get-ADComputer -Filter {(Enabled -eq $true) -and (OperatingSystem -Like "*Windows 7*")} -Property Name,OperatingSystem | format-table Name,OperatingSystem -Wrap -Auto';
const advice = `Upgrade the workstations. You can check the OS with the following PowerShell command: ${command}`;
const item = record([['Advised solution', `${advice}\n\nConfirm the resulting inventory.`]]);
const { container } = render(item, 'pingcastle');
const step = container.querySelector('.finding-first-read.policy-next-step')!;
expect(step.querySelector('p')?.textContent).toBe('Upgrade the workstations. You can check the OS with the following PowerShell command:');
expect(step.querySelector('pre.finding-first-read__command code')?.textContent).toBe(command);
expect(step.querySelector('details')?.textContent).toContain('Confirm the resulting inventory.');
expect(item.fields[0].value).toBe(`${advice}\n\nConfirm the resulting inventory.`);
});
it('Purple Knight: result message, affected count and likelihood as the problem, remediation as the next step', () => {
const item = record([['Name', 'Synthetic finding'], ['Status', 'IOE Found'], ['Description', 'Synthetic description.'],
['Likelihood of compromise', 'Synthetic likelihood.'], ['Result message', 'Found 2 synthetic objects.'], ['Number of results', '2'],
['Remediation', 'Remove the synthetic permission.']]);
const { container, values } = render(item, 'purple-knight');
expect(values('.finding-first-read')).toEqual({ Result: 'Found 2 synthetic objects.', Affected: '2', Impact: 'Synthetic likelihood.' });
expect(container.querySelector('.policy-next-step')?.textContent).toContain('Remove the synthetic permission.');
expect(container.querySelector('.policy-next-step details')).toBeNull();
expect(headings(container)).toEqual(['Description']);
});
it('CIS-CAT: the report criterion and evaluated row as expected and found, colored by the finding outcome', () => {
const item = record([['Benchmark Item', 'Synthetic finding'], ['Result', 'Fail'], ['Description', 'Synthetic description.'],
['Rationale', 'Synthetic rationale.'], ['Impact', 'Synthetic impact.'], ['Remediation', 'Set the synthetic value to 1.']]);
const evidence = sheet([['Item', 'Value'], ['Criterion: synthetic value', 'equals 1'], ['Synthetic value', '0']]);
evidence.analysis.records[1].highlighted = true;
const { container, values } = render(item, 'cis-cat', [evidence]);
expect(values('.finding-first-read')).toEqual({ Expected: 'Criterion: synthetic value · equals 1', Found: 'Synthetic value · 0', Impact: 'Synthetic impact.' });
expect(container.querySelector('.finding-first-read [data-state="deviating"] .policy-value-mark.is-deviating')?.textContent).toBe('Synthetic value · 0');
expect(headings(container)).toEqual(['Description', 'Rationale']);
});
it('Maester: counts only rows the source marks, keeps the test result out of the reader and links https documentation', () => {
const item = record([['ID', 'MT.9001'], ['Title', 'Synthetic finding'], ['Result', 'Failed'], ['ResultDetail', 'Synthetic native test result'],
['Description', 'Synthetic description.'], ['HelpUrl', 'https://maester.dev/docs/tests/MT.9001']]);
const results = sheet([['Policy', 'Status'], ['Synthetic A', '❌ Disabled'], ['Synthetic B', '✅ Enabled'], ['Synthetic C', 'Report-only']]);
const { container, values } = render(item, 'maester', [results]);
expect(values('.finding-first-read')).toEqual({ Results: '1 of 3 rows marked failed · 1 marked passed' });
expect(container.querySelector('.reader-summary')?.textContent).not.toContain('Synthetic native test result');
const link = container.querySelector<HTMLAnchorElement>('.policy-next-step a')!;
expect(link.getAttribute('href')).toBe('https://maester.dev/docs/tests/MT.9001');
expect(link.getAttribute('rel')).toBe('noopener noreferrer');
});
it('never offers a non-https documentation link from imported content', () => {
const item = record([['ID', 'MT.9002'], ['Title', 'Synthetic finding'], ['Result', 'Failed'], ['HelpUrl', 'javascript:alert(1)']]);
expect(render(item, 'maester').container.querySelector('.policy-next-step')).toBeNull();
});
});