// @vitest-environment jsdom 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 { importFile } from '../../src/importers/importer'; import { genericProfile } from '../../src/profiles/generic/generic-profile'; const options = { profiles: [genericProfile] }; async function candidateValues(html: string, fileName: string, index = 0) { const imported = await importFile(new File([html], fileName, { type: 'text/html' }), options); const candidate = imported.extraction?.candidates[index]; return { imported, value: (label: string) => candidate?.fields.find(field => field.label === label)?.decodedValue ?? '' }; } async function firstCandidateValues(html: string, fileName: string) { return candidateValues(html, fileName, 0); } /** * Der Bericht versteckt seine Belegdaten hinter Aufklappverweisen: "Show * Assessment Evidence", "Show Rule Result XML" und je Rahmenwerk ein "More". * Alles davon muss im Results-Fenster ankommen - und zwar auch dann, wenn ein * spaeterer Bericht ein Rahmenwerk fuehrt, das es heute noch nicht gibt. */ describe('CIS-CAT source extractor: verlinkte und aufklappbare Abschnitte', () => { it('verknuepft jede versteckte Tabelle mit ihrer woertlichen Abschnittsueberschrift', async () => { const { value } = await firstCandidateValues(cisCatFixture, 'CIS-CAT-SYNTHETIC.html'); expect(value('Results').split('\n')).toEqual([ 'source-table:5 "Assessment Evidence · Complex Check"', 'source-table:6 "Assessment Evidence · Synthetic Item"', 'source-table:8 "CIS Controls V7.0 · CIS Control Information"', 'source-table:9 "CIS Critical Security Controls V8.0 · CIS Control Information"' ]); }); it('gibt den Aufzaehlungstext der Rahmenwerk-Zuordnungen als eigenen Abschnitt aus', async () => { const { value } = await firstCandidateValues(cisCatFixture, 'CIS-CAT-SYNTHETIC.html'); expect(value('Control Mappings')).toBe([ '### CIS Controls V7.0', '', '- Control 1: Synthetic Inventory', '', '### CIS Critical Security Controls V8.0', '', '- Control 2: Synthetic Assets' ].join('\n')); }); /** * Regression: Der Bericht gibt seinen References-Block mit derselben * Listenklasse aus wie die Rahmenwerk-Zuordnungen. Die Belegstellen standen * dadurch ein zweites Mal in den Control Mappings, obwohl sie ihr eigenes * Feld haben. */ it('nimmt den References-Block nicht in die Rahmenwerk-Zuordnungen auf', async () => { const { value } = await firstCandidateValues(cisCatFixture, 'CIS-CAT-SYNTHETIC.html'); expect(value('References')).toContain('SYNTHETIC-CONTROL-1'); expect(value('Control Mappings')).not.toContain('SYNTHETIC-CONTROL-1'); expect(value('Control Mappings')).not.toContain('References'); }); /** * Regression: Die Abschnittszuordnung kannte "CIS Controls V7" und "CIS * Critical Security Controls V8" als feste Zeichenketten. Eine neue Fassung * fiel auf die vorherige Marke zurueck - das war der Rule-Result-XML-Block, * dessen Tabellen bewusst aussortiert werden. Die Steuerungstabelle * verschwand dadurch vollstaendig aus dem Results-Fenster. */ it('erkennt eine neue Fassung und ein unbekanntes Rahmenwerk gleichermassen', async () => { const { value } = await firstCandidateValues(futureControlsFixture, 'CIS-CAT-FUTURE.html'); expect(value('Results').split('\n')).toEqual([ 'source-table:1 "Assessment Evidence · Synthetic Future Item"', 'source-table:3 "CIS Critical Security Controls V8.1 · CIS Control Information"', 'source-table:4 "Synthetic Framework V1.0 · Synthetic Framework Information"' ]); expect(value('Control Mappings')).toBe([ '### CIS Critical Security Controls V8.1', '', '- Control 8: Synthetic Audit Log Management', '', '### Synthetic Framework V1.0', '', '- SF-1: Synthetic Framework Requirement' ].join('\n')); }); it('verlinkt die Tabelle im Rule-Result-XML-Block nicht ein zweites Mal', async () => { const { value } = await firstCandidateValues(futureControlsFixture, 'CIS-CAT-FUTURE.html'); expect(value('Results')).not.toContain('XML Evidence'); expect(value('Rule Result XML')).toContain('synthetic-2.1.1'); }); /** * Regression: CIS-CAT bettet die Belegtabelle eines Complex Checks im * Bericht mitunter wortgleich ein zweites Mal in denselben Regelblock ein - * gleicher Name, identischer Zeileninhalt. Der Originalbericht zeigt sie * trotzdem nur einmal; das Results-Fenster darf sie deshalb nicht als * "Item 1" / "Item 2" auffuehren, wenn beide Vorkommen inhaltsgleich sind. */ it('fuehrt eine wortgleich wiederholte Belegtabelle nur einmal auf', async () => { const { value } = await candidateValues(cisCatFixture, 'CIS-CAT-SYNTHETIC.html', 1); const results = value('Results').split('\n'); expect(results).toEqual([ 'source-table:10 "Assessment Evidence · Complex Check"', 'source-table:11 "Assessment Evidence · Synthetic Audit Item"' ]); expect(results.some(line => /Synthetic Audit Item \d/.test(line))).toBe(false); }); /** * Regression: Ein OR/AND-verknuepfter Complex Check bettet die * Belegtabelle jeder Verknuepfung in eine Zelle der aeusseren * "Complex Check"-Tabelle ein. In grossen Berichten (> 4 MB) griff dafuer * die stack-sichere Extraktion, die verschachtelte Tabellen bisher nicht * kannte - die erste Verknuepfung verlor dadurch ihre "Criterion:"-Zeile. */ it('verknuepft alle Belegtabellen eines OR-Complex-Checks auch in grossen Berichten korrekt', async () => { const html = 'CIS-CAT' + '' + '' + '' + '' + '' + '
Benchmark ItemResult
2.1 Ensure the synthetic OR setting is enabledFail
' + '
' + '
' + '
ShowAssessment Evidence
' + '
' + '' + '' + '
Complex Check
OR' + '
' + '' + '' + '' + '' + '' + '
Criterion:Synthetic setting is A
Existence Check:At Least One Exists
Item Check:All
Result:Fail
' + '' + '' + '' + '
Registry Item
NameValue
SettingA-value
' + '
' + '
' + '' + '' + '' + '' + '' + '
Criterion:Synthetic setting is B
Existence Check:At Least One Exists
Item Check:All
Result:Fail
' + '' + '' + '' + '
Registry Item
NameValue
SettingB-value
' + '
' + '
' + '
' + '
' + '
' + ''; const { imported, value } = await candidateValues(html, 'CIS-CAT-OR-LARGE.html'); expect(imported.intermediate?.diagnostics).toContainEqual(expect.objectContaining({ code: 'html-bounded-extraction' })); const results = value('Results').split('\n'); expect(results).toEqual([ 'source-table:1 "Assessment Evidence · Complex Check 1"', 'source-table:2 "Assessment Evidence · Complex Check 2"', 'source-table:3 "Assessment Evidence · Registry Item 1"', 'source-table:4 "Assessment Evidence · Complex Check 3"', 'source-table:5 "Assessment Evidence · Registry Item 2"' ]); // Beide Kriterien behalten ihre "Criterion:"-Zeile - die urspruengliche // Regression liess genau diese Zeile beim ERSTEN Kriterium verschwinden, // waehrend das zweite (nicht in die aeussere Tabelle verschachtelte) // zufaellig unversehrt blieb. const firstCriterionRows = imported.sheets[2].rows; expect(firstCriterionRows).toEqual([ ['Criterion:', 'Synthetic setting is A'], ['Existence Check:', 'At Least One Exists'], ['Item Check:', 'All'], ['Result:', 'Fail'] ]); }); });