197 lines
9.4 KiB
TypeScript
197 lines
9.4 KiB
TypeScript
// @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 = '<!doctype html><html><head><title>CIS-CAT</title></head><body>'
|
|
+ '<!--' + 'x'.repeat(4_200_000) + '-->'
|
|
+ '<table id="assessmentResultTable" class="result">'
|
|
+ '<thead><tr><th>Benchmark Item</th><th>Result</th></tr></thead>'
|
|
+ '<tbody><tr><td><a href="#detail-syn-201">2.1 Ensure the synthetic OR setting is enabled</a></td>'
|
|
+ '<td><a href="#detail-syn-201">Fail</a></td></tr></tbody>'
|
|
+ '</table>'
|
|
+ '<div id="detail-syn-201" class="Rule visible">'
|
|
+ '<div class="check" id="detail-syn-201">'
|
|
+ '<div><span class="action">Show</span><span class="caption">Assessment Evidence</span></div>'
|
|
+ '<div class="xml" id="syn-201_evidence">'
|
|
+ '<table class="evidence-sep" width="100%"><caption class="bold">Complex Check</caption>'
|
|
+ '<tr><td class="bold" width="5%">OR</td><td width="95%">'
|
|
+ '<div class="definition"><div class="criteria"><div class="criterion" check="all" check_existence="at_least_one_exists">'
|
|
+ '<table class="evidence-sep" width="100%"><tbody class="tbe">'
|
|
+ '<tr><td class="bold">Criterion:</td><td>Synthetic setting is A</td></tr>'
|
|
+ '<tr><td class="bold">Existence Check:</td><td>At Least One Exists</td></tr>'
|
|
+ '<tr><td class="bold">Item Check:</td><td>All</td></tr>'
|
|
+ '<tr><td class="bold">Result:</td><td class="fail">Fail</td></tr>'
|
|
+ '</tbody></table>'
|
|
+ '<table class="evidence" width="100%"><caption>Registry Item</caption>'
|
|
+ '<thead><tr><th>Name</th><th>Value</th></tr></thead>'
|
|
+ '<tbody class="tbe"><tr><td>Setting</td><td>A-value</td></tr></tbody>'
|
|
+ '</table>'
|
|
+ '</div></div></div>'
|
|
+ '<div class="definition"><div class="criteria"><div class="criterion" check="all" check_existence="at_least_one_exists">'
|
|
+ '<table class="evidence-sep" width="100%"><tbody class="tbe">'
|
|
+ '<tr><td class="bold">Criterion:</td><td>Synthetic setting is B</td></tr>'
|
|
+ '<tr><td class="bold">Existence Check:</td><td>At Least One Exists</td></tr>'
|
|
+ '<tr><td class="bold">Item Check:</td><td>All</td></tr>'
|
|
+ '<tr><td class="bold">Result:</td><td class="fail">Fail</td></tr>'
|
|
+ '</tbody></table>'
|
|
+ '<table class="evidence" width="100%"><caption>Registry Item</caption>'
|
|
+ '<thead><tr><th>Name</th><th>Value</th></tr></thead>'
|
|
+ '<tbody class="tbe"><tr><td>Setting</td><td>B-value</td></tr></tbody>'
|
|
+ '</table>'
|
|
+ '</div></div></div>'
|
|
+ '</td></tr>'
|
|
+ '</table>'
|
|
+ '</div>'
|
|
+ '</div>'
|
|
+ '</div>'
|
|
+ '</body></html>';
|
|
|
|
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']
|
|
]);
|
|
});
|
|
});
|