Assessor_2.0/tests/server/node-import-engine.test.ts

131 lines
7.0 KiB
TypeScript

import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import * as XLSX from 'xlsx';
import { describe, expect, it } from 'vitest';
import { nodeImportRuntimeAdapters } from '../../server/node-import-runtime-adapters';
import { importSource } from '../../src/importers/engine';
import { importProfiles } from '../../src/profiles/profile-catalog';
const fixtureRoot = resolve(import.meta.dirname, '..', '..', 'fixtures');
async function fixture(name: string) {
const bytes = await readFile(resolve(fixtureRoot, name));
return { metadata: { name, sizeBytes: bytes.length, lastModified: 0 }, bytes };
}
describe('Node import runtime', () => {
it('parses CSV, JSON, HTML, XML and Excel through the shared server-side engine', async () => {
const sources = [
await fixture('generic/findings.synthetic.csv'),
await fixture('purple-knight/indicator-results.synthetic.json'),
await fixture('pingcastle/healthcheck.synthetic.html'),
{
metadata: { name: 'assessment.xml', sizeBytes: 0, lastModified: 0 },
bytes: new TextEncoder().encode('<assessment><finding severity="high">Server XML</finding></assessment>')
},
(() => {
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet([
['Rule ID', 'Title', 'Severity'], ['XLS-1', 'Server Excel', 'High']
]), 'Findings');
const bytes = new Uint8Array(XLSX.write(workbook, { type: 'array', bookType: 'xlsx' }));
return { metadata: { name: 'assessment.xlsx', sizeBytes: bytes.length, lastModified: 0 }, bytes };
})()
];
sources[3].metadata.sizeBytes = sources[3].bytes.length;
const datasets = await Promise.all(sources.map(source => importSource(
source,
{ profiles: importProfiles, delimiter: 'auto' },
nodeImportRuntimeAdapters
)));
expect(datasets.map(dataset => dataset.format)).toEqual(['CSV', 'JSON', 'HTML', 'XML', 'Excel']);
expect(datasets.every(dataset => dataset.sheets.some(sheet => sheet.analysis.records.length > 0))).toBe(true);
expect(datasets[2].intermediate?.discardedContent).toBeDefined();
});
it('does not execute imported HTML scripts and rejects malformed XML', async () => {
const html = '<script>globalThis.__assessorExecuted = true</script><table><tr><th>Title</th></tr><tr><td>Safe</td></tr></table>';
const htmlBytes = new TextEncoder().encode(html);
const htmlDataset = await importSource({
metadata: { name: 'safe.html', sizeBytes: htmlBytes.length, lastModified: 0 }, bytes: htmlBytes
}, { profiles: importProfiles }, nodeImportRuntimeAdapters);
expect((globalThis as Record<string, unknown>).__assessorExecuted).toBeUndefined();
expect(htmlDataset.intermediate?.diagnostics).toContainEqual(expect.objectContaining({ code: 'html-active-content-removed' }));
const xmlBytes = new TextEncoder().encode('<assessment><finding></assessment>');
await expect(importSource({
metadata: { name: 'broken.xml', sizeBytes: xmlBytes.length, lastModified: 0 }, bytes: xmlBytes
}, { profiles: importProfiles }, nodeImportRuntimeAdapters)).rejects.toThrow('XML konnte nicht geparst werden');
});
it('rejects external XML entities before DOM parsing and strips active HTML attributes', async () => {
const xml = '<!DOCTYPE report [<!ENTITY secret SYSTEM "file:///etc/passwd">]><report>&secret;</report>';
const xmlBytes = new TextEncoder().encode(xml);
await expect(importSource({
metadata: { name: 'external.xml', sizeBytes: xmlBytes.length, lastModified: 0 }, bytes: xmlBytes
}, { profiles: importProfiles }, nodeImportRuntimeAdapters)).rejects.toMatchObject({
diagnostics: [expect.objectContaining({ code: 'xml-doctype-rejected' })]
});
// Ereignisattribute und aktive Elemente fallen weiterhin heraus; ein
// Verweisziel als Web-Adresse bleibt seit der Freigabe erhalten, ein
// Skriptschema nicht.
const decoded = nodeImportRuntimeAdapters.markup.decodeHtml('<a href="https://example.invalid" onclick="alert(1)">Safe</a><form><input value="x"></form>');
expect(decoded.removedContentCount).toBeGreaterThanOrEqual(2);
expect(decoded.fallbackRows.flat().join(' ')).toContain('Safe');
const linked = nodeImportRuntimeAdapters.markup.decodeHtml('<table><tr><td><a href="https://example.invalid/doc" onclick="alert(1)">Doku</a></td><td><a href="javascript:alert(1)">Boese</a></td></tr></table>');
const cells = linked.tables[0]?.rows.flat().join(' ') ?? '';
expect(cells).toContain('<a href="https://example.invalid/doc">');
expect(cells).not.toContain('onclick');
expect(cells).not.toContain('javascript:');
});
it('parses a 5.9 MB PingCastle-style HTML report without recursive DOM failure', async () => {
const html = '<!doctype html><html><head><title>PingCastle Healthcheck</title></head><body>'
+ '<!--' + 'x'.repeat(5_900_000) + '-->'
+ '<table id="rules"><caption>Rules</caption>'
+ '<tr><th>Rule ID</th><th>Title</th><th>Severity</th></tr>'
+ '<tr><td>PC-LARGE-1</td><td>Large PingCastle report</td><td>High</td></tr>'
+ '</table></body></html>';
const bytes = new TextEncoder().encode(html);
const dataset = await importSource({
metadata: { name: 'ad_hc_example.local.html', sizeBytes: bytes.length, lastModified: 0 }, bytes
}, { profiles: importProfiles }, nodeImportRuntimeAdapters);
expect(dataset.format).toBe('HTML');
expect(dataset.profileId).toBe('pingcastle');
expect(dataset.scanTool).toBe('PingCastle');
expect(dataset.sheets.find(sheet => sheet.name === 'Rules')?.rows[1]).toEqual([
'PC-LARGE-1', 'Large PingCastle report', 'High'
]);
expect(dataset.intermediate?.diagnostics).toContainEqual(expect.objectContaining({
code: 'html-bounded-extraction',
severity: 'info'
}));
}, 30_000);
it('routes a 1.6 MB PingCastle-style HTML report through bounded extraction before the worker time limit', async () => {
const html = '<!doctype html><html><head><title>PingCastle Healthcheck</title></head><body>'
+ '<!--' + 'x'.repeat(1_600_000) + '-->'
+ '<table id="rules"><caption>Rules</caption>'
+ '<tr><th>Rule ID</th><th>Title</th><th>Severity</th></tr>'
+ '<tr><td>PC-BOUNDED-1</td><td>Bounded synthetic report</td><td>High</td></tr>'
+ '</table></body></html>';
const bytes = new TextEncoder().encode(html);
const dataset = await importSource({
metadata: { name: 'ad_hc_bounded.synthetic.html', sizeBytes: bytes.length, lastModified: 0 }, bytes
}, { profiles: importProfiles }, nodeImportRuntimeAdapters);
expect(dataset.format).toBe('HTML');
expect(dataset.profileId).toBe('pingcastle');
expect(dataset.sheets.find(sheet => sheet.name === 'Rules')?.rows[1]).toEqual([
'PC-BOUNDED-1', 'Bounded synthetic report', 'High'
]);
expect(dataset.intermediate?.diagnostics).toContainEqual(expect.objectContaining({
code: 'html-bounded-extraction',
severity: 'info'
}));
}, 30_000);
});