// @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'); expect(markup).toContain('>CIS Critical Security Controls V8.0'); // Die Zuordnungsliste wird in Tabellenueberschriften ueberfuehrt. expect(markup).not.toContain('>Control Mappings'); expect(markup).toContain('>Control 1: Synthetic Inventory'); expect(markup).toContain('>Control 2: Synthetic Assets'); 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('No matching system items were found.'); }); 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('Criterion:'); expect(markup).toContain('Value2'); }); 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('Typereg_dword'); expect(markup).toContain('Value0'); 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'); expect(markup).toContain('>CIS Critical Security Controls V8.1'); expect(markup).toContain('>Synthetic Framework V1.0'); expect(markup).toContain('>Control 8: Synthetic Audit Log Management'); expect(markup).toContain('>SF-1: Synthetic Framework Requirement'); 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(`${label}`); } 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('Synthetic Inventory'); }); /** * "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/); }); });