// @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
NameSeverity
Weak policyHigh
HostStatus
dc01Open
`; 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 = `
NameEvidence
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 IDTitle
PC-BROWSER-1Bounded 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 = `
Refs
Quelle
Doku
Boese
Daten
Datei
`; 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
ANDCriterion: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
NameValue
HiveHKEY_LOCAL_MACHINE
Typereg_dword
Value5
Windows View64_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
NameValue
HiveHKEY_LOCAL_MACHINE
Value5
'; 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
ANDCriterion: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
SettingA-value
' + '
' + '' + '' + '' + '' + '' + '
Criterion:Synthetic setting is B
Existence Check:At Least One Exists
Item Check:All
Result:Fail
' + '' + '' + '
Registry Item
SettingB-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('