219 lines
9.3 KiB
TypeScript
219 lines
9.3 KiB
TypeScript
// @vitest-environment jsdom
|
||
|
||
import { createElement } from 'react';
|
||
import { renderToStaticMarkup } from 'react-dom/server';
|
||
import { describe, expect, it } from 'vitest';
|
||
import cisCatFixture from '../../fixtures/cis-cat/assessment.synthetic.html?raw';
|
||
import futureControlsFixture from '../../fixtures/cis-cat/assessment.future-controls.synthetic.html?raw';
|
||
import type { FindingRecord } from '../../src/domain/finding';
|
||
import { importFile } from '../../src/importers/importer';
|
||
import { makeSheet } from '../../src/importers/sheet-factory';
|
||
import { toolIdForRecord } from '../../src/importers/status-normalization';
|
||
import { genericProfile } from '../../src/profiles/generic/generic-profile';
|
||
import { FloatingResultsWindow } from '../../src/features/linked-results/FloatingResultsWindow';
|
||
import { resultSectionsFor } from '../../src/features/linked-results/result-message';
|
||
import { linkResultSheets } from '../../src/profiles/purple-knight/result-linker';
|
||
|
||
const options = { profiles: [genericProfile] };
|
||
|
||
/**
|
||
* Vertrag des Results-Fensters fuer CIS-CAT, von der importierten Datei bis zur
|
||
* gerenderten Ausgabe: die Belegdaten des Berichts - Assessment Evidence und
|
||
* die Rahmenwerk-Zuordnungen hinter "More" - stehen im Fenster, mit der
|
||
* Ueberschrift des Berichts. Das Rule Result XML gehoert nicht dorthin,
|
||
* sondern bleibt ein aufklappbares Detail im Finding.
|
||
*/
|
||
async function resultsWindowMarkup(html: string, fileName: string) {
|
||
const imported = await importFile(new File([html], fileName, { type: 'text/html' }), options);
|
||
const findings = imported.sheets.find(sheet => sheet.name === 'assessmentResultTable')!;
|
||
const candidate = imported.extraction!.candidates[0];
|
||
const record: FindingRecord = {
|
||
id: 'cis-cat-record',
|
||
index: 0,
|
||
title: String(candidate.fields.find(field => field.label === 'Benchmark Item')?.decodedValue ?? ''),
|
||
fields: candidate.fields.map((field, index) => ({ index, label: field.label, value: String(field.decodedValue) })),
|
||
filled: [],
|
||
meta: []
|
||
};
|
||
record.fields.push({ index: record.fields.length, label: 'Source: Scan', value: 'CIS-CAT Pro \u00b7 2026-08-02T00:00:00.000Z' });
|
||
return renderToStaticMarkup(createElement(FloatingResultsWindow, {
|
||
id: 'cis-cat-results',
|
||
title: 'Results (1) \u2013 ' + record.title,
|
||
sheets: linkResultSheets(record, imported.sheets, findings.id),
|
||
toolId: toolIdForRecord(record, 'generic'),
|
||
sections: resultSectionsFor(record, 'cis-cat'),
|
||
color: '#7c3aed',
|
||
zIndex: 2100,
|
||
offset: 0,
|
||
search: '',
|
||
onActivate: () => undefined,
|
||
onClose: () => undefined,
|
||
onToggleMinimize: () => undefined
|
||
}));
|
||
}
|
||
|
||
describe('CIS-CAT Results-Fenster', () => {
|
||
it('zeigt versteckte Belegtabellen und Rahmenwerk-Zuordnungen gemeinsam', async () => {
|
||
const markup = await resultsWindowMarkup(cisCatFixture, 'CIS-CAT-SYNTHETIC.html');
|
||
|
||
// Hinter "Show Assessment Evidence" versteckte Tabellen.
|
||
expect(markup.match(/>Assessment<\/h4>/g)).toHaveLength(1);
|
||
// Hinter "More" versteckte Steuerungstabellen, je Rahmenwerk benannt.
|
||
expect(markup).toContain('>CIS Controls V7.0</h4>');
|
||
expect(markup).toContain('>CIS Critical Security Controls V8.0</h4>');
|
||
// Die Zuordnungsliste wird in Tabellenueberschriften ueberfuehrt.
|
||
expect(markup).not.toContain('>Control Mappings</h4>');
|
||
expect(markup).toContain('>Control 1: Synthetic Inventory</h5>');
|
||
expect(markup).toContain('>Control 2: Synthetic Assets</h5>');
|
||
expect(markup).toContain('Control 1: Synthetic Inventory');
|
||
expect(markup).not.toContain('Keine passende Ergebnisdatei automatisch gefunden.');
|
||
});
|
||
|
||
/**
|
||
* Nutzerkorrektur vom 2026-07-31: Das Rule Result XML fuellte das Fenster vor
|
||
* den eigentlichen Belegtabellen. Es bleibt ein aufklappbares Detail im
|
||
* Finding und darf im Fenster an keiner Stelle auftauchen.
|
||
*/
|
||
it('markiert den Assessment-Hinweis als ausgewertete blaue Zeile', () => {
|
||
const noteSheet = makeSheet(
|
||
'Assessment Evidence \u00b7 Hinweis',
|
||
[['No matching system items were found.']],
|
||
'',
|
||
'synthetic-cis-cat.html'
|
||
);
|
||
const markup = renderToStaticMarkup(createElement(FloatingResultsWindow, {
|
||
id: 'cis-cat-note',
|
||
title: 'Results (1) \u2013 Synthetic assessment',
|
||
sheets: [noteSheet],
|
||
toolId: 'cis-cat',
|
||
sections: [],
|
||
color: '#7c3aed',
|
||
zIndex: 2100,
|
||
offset: 0,
|
||
search: '',
|
||
onActivate: () => undefined,
|
||
onClose: () => undefined,
|
||
onToggleMinimize: () => undefined
|
||
}));
|
||
|
||
expect(markup).toContain('<tr class="rich-text-table-row--highlight"><td>No matching system items were found.</td>');
|
||
});
|
||
it('markiert Criterion gruen und die ausgewertete Abweichung rot', () => {
|
||
const evidenceSheet = makeSheet(
|
||
'Assessment Evidence · Registry Item',
|
||
[
|
||
['Field', 'Value'],
|
||
['Criterion:', "Ensure 'Start' is 'Windows: Registry Value' to '4'"],
|
||
['Value', '2']
|
||
],
|
||
'',
|
||
'synthetic-cis-cat.html'
|
||
);
|
||
evidenceSheet.analysis.records[1].highlighted = true;
|
||
const markup = renderToStaticMarkup(createElement(FloatingResultsWindow, {
|
||
id: 'cis-cat-colors',
|
||
title: 'Results (1) – Synthetic registry check',
|
||
sheets: [evidenceSheet],
|
||
toolId: 'cis-cat',
|
||
outcome: 'fail',
|
||
sections: [],
|
||
color: '#7c3aed',
|
||
zIndex: 2100,
|
||
offset: 0,
|
||
search: '',
|
||
onActivate: () => undefined,
|
||
onClose: () => undefined,
|
||
onToggleMinimize: () => undefined
|
||
}));
|
||
expect(markup).toContain('<tr class="rich-text-table-row--criterion"><td>Criterion:</td>');
|
||
expect(markup).toContain('<tr class="rich-text-table-row--violation"><td>Value</td><td>2</td>');
|
||
});
|
||
it('markiert den bestaetigten Ist-Wert eines bestandenen Results gruen statt rot', () => {
|
||
const evidenceSheet = makeSheet(
|
||
'Assessment Evidence · Registry Item',
|
||
[
|
||
['Field', 'Value'],
|
||
['Criterion:', "Ensure 'ForceGuest' is 'Windows: Registry Value' to '0'"],
|
||
['Type', 'reg_dword'],
|
||
['Value', '0']
|
||
],
|
||
'',
|
||
'synthetic-cis-cat.html'
|
||
);
|
||
evidenceSheet.analysis.records[1].highlighted = true;
|
||
evidenceSheet.analysis.records[2].highlighted = true;
|
||
const markup = renderToStaticMarkup(createElement(FloatingResultsWindow, {
|
||
id: 'cis-cat-pass-colors',
|
||
title: 'Results (1) – Passing registry check',
|
||
sheets: [evidenceSheet],
|
||
toolId: 'cis-cat',
|
||
outcome: 'pass',
|
||
sections: [],
|
||
color: '#7c3aed',
|
||
zIndex: 2100,
|
||
offset: 0,
|
||
search: '',
|
||
onActivate: () => undefined,
|
||
onClose: () => undefined,
|
||
onToggleMinimize: () => undefined
|
||
}));
|
||
|
||
expect(markup).toContain('<tr class="rich-text-table-row--match"><td>Type</td><td>reg_dword</td>');
|
||
expect(markup).toContain('<tr class="rich-text-table-row--match"><td>Value</td><td>0</td>');
|
||
expect(markup).not.toContain('rich-text-table-row--violation');
|
||
});
|
||
it('zeigt das Rule Result XML nicht im Fenster', async () => {
|
||
const markup = await resultsWindowMarkup(cisCatFixture, 'CIS-CAT-SYNTHETIC.html');
|
||
|
||
expect(markup).not.toContain('Rule Result XML');
|
||
expect(markup).not.toContain('xccdf:rule-result');
|
||
expect(markup).not.toContain('synthetic-1.1.1');
|
||
});
|
||
|
||
it('zeigt auch ein neues und ein unbekanntes Rahmenwerk vollstaendig an', async () => {
|
||
const markup = await resultsWindowMarkup(futureControlsFixture, 'CIS-CAT-FUTURE.html');
|
||
|
||
expect(markup).toContain('>Assessment</h4>');
|
||
expect(markup).toContain('>CIS Critical Security Controls V8.1</h4>');
|
||
expect(markup).toContain('>Synthetic Framework V1.0</h4>');
|
||
expect(markup).toContain('>Control 8: Synthetic Audit Log Management</h5>');
|
||
expect(markup).toContain('>SF-1: Synthetic Framework Requirement</h5>');
|
||
expect(markup).toContain('Control 8: Synthetic Audit Log Management');
|
||
expect(markup).toContain('SF-1: Synthetic Framework Requirement');
|
||
// Der Rule-Result-XML-Block bleibt vollstaendig aussen vor, samt seiner
|
||
// internen Tabelle.
|
||
expect(markup).not.toContain('XML Evidence');
|
||
expect(markup).not.toContain('synthetic-2.1.1');
|
||
});
|
||
|
||
/**
|
||
* Regression: Die Steuerungstabelle ist eine Schluessel/Wert-Tabelle ohne
|
||
* Kopfzeile. Die Kopfzeilensuche waehlte darin die Zeile mit den zwei
|
||
* laengsten Beschriftungen und verwarf alles darueber - im Fenster blieb von
|
||
* vier Angaben eine einzige Spalte "Label:" mit der Zeile "Description:"
|
||
* uebrig, ohne einen einzigen Wert.
|
||
*/
|
||
it('zeigt jede Angabe der Steuerungstabelle mit ihrem Wert', async () => {
|
||
const markup = await resultsWindowMarkup(cisCatFixture, 'CIS-CAT-SYNTHETIC.html');
|
||
|
||
for (const label of ['Control:', 'Subcontrol:', 'Label:', 'Description:']) {
|
||
expect(markup).toContain(`<td>${label}</td>`);
|
||
}
|
||
expect(markup).toContain('Maintain a synthetic inventory.');
|
||
expect(markup).toContain('Synthetic Inventory');
|
||
expect(markup).toContain('IG-1');
|
||
// Kein Wert darf als Spaltenueberschrift enden.
|
||
expect(markup).not.toContain('<th>Synthetic Inventory</th>');
|
||
});
|
||
|
||
/**
|
||
* "More" ist Bedienelement des Berichts, kein Inhalt - im Fenster gibt es
|
||
* nichts mehr aufzuklappen.
|
||
*/
|
||
it('uebernimmt die Aufklappbeschriftung des Berichts nicht in den Ergebnistext', async () => {
|
||
const markup = await resultsWindowMarkup(cisCatFixture, 'CIS-CAT-SYNTHETIC.html');
|
||
|
||
expect(markup).not.toMatch(/Synthetic Inventory[\s–-]*More/);
|
||
});
|
||
});
|