// @vitest-environment jsdom
import { describe, expect, it } from 'vitest';
import { importHtml } from '../../src/importers/html-importer';
import { browserMarkupDocumentAdapter } from '../../src/platform/browser/import-runtime-adapters';
import { sourceMetadata } from './import-source-fixtures';
describe('HTML importer', () => {
it('extracts multiple named tables and their source metadata', () => {
const text = `
Findings
| Name | Severity |
| Weak policy | High |
`;
const source = sourceMetadata('report.html', text.length, 'text/html');
const imported = importHtml(source, text, browserMarkupDocumentAdapter);
expect(imported.format).toBe('HTML');
expect(imported.sheets.map(sheet => sheet.name)).toEqual(['Findings', 'systems']);
expect(imported.sheets[0].rows).toEqual([
['Name', 'Severity'],
['Weak policy', 'High']
]);
expect(imported.sheets[0].sourceFile).toBe('report.html');
expect(imported.sheets[0].analysis.records[0].title).toBe('Weak policy');
});
it('removes active content before extracting table cells', () => {
const text = `
| Name | Evidence |
| Unsafe markup |
Safe evidence |
`;
const source = sourceMetadata('untrusted.html', text.length, 'text/html');
const imported = importHtml(source, text, browserMarkupDocumentAdapter);
expect(imported.sheets[0].name).toBe('Security findings');
expect(imported.sheets[0].rows[1]).toEqual(['Unsafe markup', 'Safe evidence']);
expect(imported.sheets[0].raw).not.toContain('window.evil');
expect(imported.sheets[0].raw).not.toContain('example.invalid');
});
it('falls back to inert body text when no table exists', () => {
const text = 'Summary
No findings exported.
';
const source = sourceMetadata('summary.html', text.length, 'text/html');
const imported = importHtml(source, text, browserMarkupDocumentAdapter);
expect(imported.sheets).toHaveLength(1);
expect(imported.sheets[0].name).toBe('HTML');
expect(imported.sheets[0].rows.flat().join(' ')).toContain('Summary');
expect(imported.sheets[0].rows.flat().join(' ')).not.toContain('alert(1)');
});
it('uses bounded extraction for multi-megabyte browser-side HTML reports', () => {
const text = 'PingCastle'
+ ''
+ '| Rule ID | Title |
'
+ '| PC-BROWSER-1 | Bounded browser extraction |
'
+ '';
const source = sourceMetadata('ad_hc_browser.local.html', text.length, 'text/html');
const imported = importHtml(source, text, browserMarkupDocumentAdapter);
expect(imported.sheets[0].rows[1]).toEqual(['PC-BROWSER-1', 'Bounded browser extraction']);
expect(imported.intermediate?.diagnostics).toContainEqual(expect.objectContaining({
code: 'html-bounded-extraction',
severity: 'info'
}));
});
// Verweisziele sind seit der Freigabe erwuenscht - aber ausschliesslich als
// Web-Adresse. Jedes andere Schema muss weiterhin verschwinden, sonst wuerde
// ein Bericht ausfuehrbare Inhalte in die Oberflaeche tragen.
it('keeps web links from reports and discards every other scheme', () => {
const text = `
`;
const source = sourceMetadata('refs.html', text.length, 'text/html');
const rows = importHtml(source, text, browserMarkupDocumentAdapter).sheets[0].rows;
expect(rows[1][0]).toContain('');
expect(rows[1][0]).toContain('Doku');
expect(rows[2][0]).toContain('Boese');
expect(rows[3][0]).toContain('Daten');
expect(rows[4][0]).toContain('Datei');
rows.slice(2).forEach(row => {
expect(row[0]).not.toContain('href');
expect(row[0]).not.toContain(' {
const text = `
Complex Check
| AND | Criterion: | Ensure setting is enabled |
| Existence Check: | At Least One Exists |
| Item Check: | All |
| Result: | Fail |
`;
const source = sourceMetadata('rowspan.html', text.length, 'text/html');
const rows = importHtml(source, text, browserMarkupDocumentAdapter).sheets[0].rows;
expect(rows).toEqual([
['AND', 'Criterion:', 'Ensure setting is enabled'],
['AND', 'Existence Check:', 'At Least One Exists'],
['AND', 'Item Check:', 'All'],
['AND', 'Result:', 'Fail']
]);
});
/**
* CIS-CAT markiert in einer Belegtabelle (z. B. "Registry Item",
* "Auditeventpolicysubcategories Item") genau die Zeile(n) mit
* ``, gegen die ein Criterion tatsaechlich prueft -
* im Originalbericht als Hintergrundfarbe sichtbar. Diese Markierung muss
* bis zum auswertbaren Datensatz erhalten bleiben, sonst verschwindet im
* Ergebnisfenster, welche Zeile einer oft langen Tabelle zum Ergebnis
* gefuehrt hat.
*/
it('uebernimmt eine vom Bericht markierte Zeile bis zum auswertbaren Datensatz', () => {
const text = `
Registry Item
| Name | Value |
| Hive | HKEY_LOCAL_MACHINE |
| Type | reg_dword |
| Value | 5 |
| Windows View | 64_bit |
`;
const source = sourceMetadata('evaluated-row.html', text.length, 'text/html');
const records = importHtml(source, text, browserMarkupDocumentAdapter).sheets[0].analysis.records;
expect(records.map(record => ({ name: record.fields[0].value, highlighted: record.highlighted ?? false }))).toEqual([
{ name: 'Hive', highlighted: false },
{ name: 'Type', highlighted: true },
{ name: 'Value', highlighted: true },
{ name: 'Windows View', highlighted: false }
]);
});
it('uebernimmt eine vom Bericht markierte Zeile auch in der stack-sicheren Extraktion grosser Berichte', () => {
const text = 'CIS-CAT'
+ ''
+ 'Registry Item'
+ '| Name | Value |
'
+ '| Hive | HKEY_LOCAL_MACHINE |
'
+ '| Value | 5 |
'
+ '
';
const source = sourceMetadata('evaluated-row-bounded.html', text.length, 'text/html');
const imported = importHtml(source, text, browserMarkupDocumentAdapter);
expect(imported.intermediate?.diagnostics).toContainEqual(expect.objectContaining({ code: 'html-bounded-extraction' }));
const records = imported.sheets[0].analysis.records;
expect(records.map(record => ({ name: record.fields[0].value, highlighted: record.highlighted ?? false }))).toEqual([
{ name: 'Hive', highlighted: false },
{ name: 'Value', highlighted: true }
]);
});
it('loest rowspan auch in der stack-sicheren Extraktion grosser Berichte auf', () => {
const text = 'CIS-CAT'
+ ''
+ 'Complex Check'
+ '| AND | Criterion: | Ensure setting is enabled |
'
+ '| Existence Check: | At Least One Exists |
'
+ '| Item Check: | All |
'
+ '| Result: | Fail |
'
+ '
';
const source = sourceMetadata('rowspan-bounded.html', text.length, 'text/html');
const imported = importHtml(source, text, browserMarkupDocumentAdapter);
expect(imported.intermediate?.diagnostics).toContainEqual(expect.objectContaining({ code: 'html-bounded-extraction' }));
expect(imported.sheets[0].rows).toEqual([
['AND', 'Criterion:', 'Ensure setting is enabled'],
['AND', 'Existence Check:', 'At Least One Exists'],
['AND', 'Item Check:', 'All'],
['AND', 'Result:', 'Fail']
]);
});
/**
* Regression: CIS-CAT bettet die Belegtabelle eines OR/AND-verknuepften
* Complex Checks in eine Zelle der aeusseren "Complex Check"-Tabelle ein.
* Die stack-sichere Extraktion grosser Berichte suchte bisher naiv nach
* dem naechsten `` und schloss dadurch die AEUSSERE Tabelle schon
* am Ende der ERSTEN verschachtelten Tabelle - "Criterion:" der ersten
* Verknuepfung verschwand als vermeintliche Kopfzeile.
*/
it('loest eine in eine Tabellenzelle verschachtelte Tabelle in der stack-sicheren Extraktion korrekt auf', () => {
const text = 'CIS-CAT'
+ ''
+ 'Complex Check'
+ '| OR | '
+ ''
+ '| Criterion: | Synthetic setting is A | '
+ '| Existence Check: | At Least One Exists | '
+ '| Item Check: | All | '
+ '| Result: | Fail | '
+ ' '
+ 'Registry Item'
+ '| Setting | A-value | '
+ ' '
+ ' |
'
+ ''
+ '| Criterion: | Synthetic setting is B |
'
+ '| Existence Check: | At Least One Exists |
'
+ '| Item Check: | All |
'
+ '| Result: | Fail |
'
+ '
'
+ 'Registry Item'
+ '| Setting | B-value |
'
+ '
'
+ '';
const source = sourceMetadata('or-complex-check.html', text.length, 'text/html');
const imported = importHtml(source, text, browserMarkupDocumentAdapter);
expect(imported.intermediate?.diagnostics).toContainEqual(expect.objectContaining({ code: 'html-bounded-extraction' }));
expect(imported.sheets.map(sheet => ({ name: sheet.name, rows: sheet.rows }))).toEqual([
{ name: 'Complex Check', rows: [['OR', '']] },
{ name: 'Tabelle 2', rows: [
['Criterion:', 'Synthetic setting is A'],
['Existence Check:', 'At Least One Exists'],
['Item Check:', 'All'],
['Result:', 'Fail']
] },
{ name: 'Registry Item', rows: [['Setting', 'A-value']] },
{ name: 'Tabelle 4', rows: [
['Criterion:', 'Synthetic setting is B'],
['Existence Check:', 'At Least One Exists'],
['Item Check:', 'All'],
['Result:', 'Fail']
] },
{ name: 'Registry Item', rows: [['Setting', 'B-value']] }
]);
});
it('vervielfaeltigt eine per colspan gespannte Kopfzelle in jede ueberspannte Spalte', () => {
const text = `
| CIS Control Information |
| Control: | Maintain an inventory. |
`;
const source = sourceMetadata('colspan.html', text.length, 'text/html');
const rows = importHtml(source, text, browserMarkupDocumentAdapter).sheets[0].rows;
expect(rows[0]).toEqual(['CIS Control Information', 'CIS Control Information']);
expect(rows[1]).toEqual(['Control:', 'Maintain an inventory.']);
});
it('escapes source text so a report cannot inject its own markup', () => {
const text = `
Injection
| Wert |
| <a href="javascript:alert(1)">klick</a>ok |
`;
const source = sourceMetadata('inject.html', text.length, 'text/html');
const value = importHtml(source, text, browserMarkupDocumentAdapter).sheets[0].rows[1][0];
expect(value).toContain('<a href="javascript:alert(1)">');
expect(value).toContain('');
expect(value).not.toContain('