// @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:3 "Assessment Evidence · Complex Check"',
'source-table:4 "Assessment Evidence · Synthetic Item"',
'source-table:6 "CIS Controls V7.0 · CIS Control Information"',
'source-table:7 "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:8 "Assessment Evidence · Complex Check"',
'source-table:9 "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'
+ ''
+ ''
+ ''
+ '
'
+ '
ShowAssessment Evidence
'
+ '
'
+ '
Complex Check'
+ '| OR | '
+ ''
+ ' '
+ '| Criterion: | Synthetic setting is A | '
+ '| Existence Check: | At Least One Exists | '
+ '| Item Check: | All | '
+ '| Result: | Fail | '
+ ' '
+ ' Registry Item'
+ '| Name | Value | '
+ '| Setting | A-value | '
+ ' '
+ ' '
+ ''
+ ' '
+ '| Criterion: | Synthetic setting is B | '
+ '| Existence Check: | At Least One Exists | '
+ '| Item Check: | All | '
+ '| Result: | Fail | '
+ ' '
+ ' Registry Item'
+ '| Name | Value | '
+ '| Setting | B-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']
]);
});
});