99 lines
7.5 KiB
TypeScript
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();
|
|
});
|
|
});
|