131 lines
7.9 KiB
TypeScript
131 lines
7.9 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import maesterHtmlFixture from '../../fixtures/maester/test-results.synthetic.html?raw';
|
|
import purpleKnightHtmlFixture from '../../fixtures/purple-knight/security-assessment.synthetic.html?raw';
|
|
import { assertCanonicalAssessmentModel } from '../../src/domain/canonical-validation';
|
|
import { confirmEntryMappings, previewEntryFromDataset } from '../../src/features/import-workflow/import-workflow';
|
|
import { createCanonicalImportBatch } from '../../src/importers/canonical-import';
|
|
import { importFile } from '../../src/importers/importer';
|
|
import { importProfiles } from '../../src/profiles/profile-catalog';
|
|
import { linkResultSheets } from '../../src/profiles/purple-knight/result-linker';
|
|
import { canonicalModelToDataset } from '../../src/features/project-import/canonical-model-to-dataset';
|
|
|
|
function candidateValue(
|
|
candidate: NonNullable<Awaited<ReturnType<typeof importFile>>['extraction']>['candidates'][number],
|
|
label: string
|
|
): string | undefined {
|
|
return candidate.fields.find(field => field.label === label)?.decodedValue;
|
|
}
|
|
|
|
async function canonicalCount(content: string, fileName: string, type: string): Promise<number> {
|
|
const dataset = await importFile(new File([content], fileName, { type }), { profiles: importProfiles });
|
|
const preview = previewEntryFromDataset(dataset, {
|
|
sha256: fileName.padEnd(64, '0').slice(0, 64), fileName, sizeBytes: content.length, lastModified: 0
|
|
});
|
|
const entry = confirmEntryMappings(preview, importProfiles);
|
|
const model = createCanonicalImportBatch(fileName, [entry], '2026-08-25T08:00:00.000Z').model;
|
|
assertCanonicalAssessmentModel(model);
|
|
return model.findingOccurrences.length;
|
|
}
|
|
|
|
describe('script-embedded scanner HTML source extractors', () => {
|
|
it('extracts Maester tests from inert embedded JSON without executing report scripts', async () => {
|
|
delete (window as Window & { __syntheticActiveContentExecuted?: boolean }).__syntheticActiveContentExecuted;
|
|
const imported = await importFile(new File([maesterHtmlFixture], 'Maester-Synthetic-Report.html', { type: 'text/html' }), {
|
|
profiles: importProfiles
|
|
});
|
|
|
|
expect((window as Window & { __syntheticActiveContentExecuted?: boolean }).__syntheticActiveContentExecuted).toBeUndefined();
|
|
expect(imported.profileId).toBe('maester');
|
|
expect(imported.extraction?.extractor.id).toBe('maester-html-source-extractor');
|
|
expect(imported.extraction?.candidates).toHaveLength(2);
|
|
expect(candidateValue(imported.extraction!.candidates[0], 'ID')).toBe('MT.9001');
|
|
expect(candidateValue(imported.extraction!.candidates[0], 'Result')).toBe('Failed');
|
|
expect(candidateValue(imported.extraction!.candidates[0], 'ResultDetail')).toBe('The synthetic policy needs review.');
|
|
expect(imported.extraction!.candidates[0].source.objectPath).toBe('$.Tests[0]');
|
|
expect(await canonicalCount(maesterHtmlFixture, 'Maester-Synthetic-Report.html', 'text/html')).toBe(2);
|
|
});
|
|
|
|
it('extracts PurpleKnight indicators and preserves source-object provenance', async () => {
|
|
delete (window as Window & { __syntheticActiveContentExecuted?: boolean }).__syntheticActiveContentExecuted;
|
|
const imported = await importFile(new File(
|
|
[purpleKnightHtmlFixture], 'Security_Assessment_Report_synthetic.html', { type: 'text/html' }
|
|
), { profiles: importProfiles });
|
|
|
|
expect((window as Window & { __syntheticActiveContentExecuted?: boolean }).__syntheticActiveContentExecuted).toBeUndefined();
|
|
expect(imported.profileId).toBe('purple-knight');
|
|
expect(imported.extraction?.extractor.id).toBe('purple-knight-html-source-extractor');
|
|
expect(imported.extraction?.candidates).toHaveLength(2);
|
|
expect(imported.extraction?.candidates[0].fields.map(field => field.label)).toEqual([
|
|
'ShortName', 'Name', 'SI version', 'Target', 'Status', 'Runtime', 'Score', 'Severity', 'Weight', 'Category',
|
|
'MITRE ATT&CK_1', 'MITRE ATT&CK_2', 'MITRE ATT&CK_3', 'MITRE ATT&CK_4',
|
|
'ANSSI_1', 'ANSSI_2', 'ANSSI_3', 'ANSSI_4', 'MITRE D3FEND_1', 'MITRE D3FEND_2',
|
|
'Description', 'Likelihood of compromise', 'Result message', 'Number of results', 'Result', 'Remediation'
|
|
]);
|
|
expect(candidateValue(imported.extraction!.candidates[0], 'ShortName')).toBe('SI900001');
|
|
expect(candidateValue(imported.extraction!.candidates[0], 'Status')).toBe('IOE Found');
|
|
expect(candidateValue(imported.extraction!.candidates[0], 'Result')).toBe('SI900001');
|
|
expect(candidateValue(imported.extraction!.candidates[0], 'Description')).toBe('A synthetic description.');
|
|
expect(candidateValue(imported.extraction!.candidates[0], 'Category')).toBe('Synthetic Category');
|
|
expect(imported.extraction!.candidates[0].source.objectPath).toContain('$.Category_1');
|
|
expect(imported.extraction?.supportingTables).toEqual([expect.objectContaining({
|
|
name: 'SI900001', headers: ['Object', 'EventTimestamp', 'Ignored'],
|
|
rows: [['Synthetic value', '2026-05-27 12:37', 'False']]
|
|
})]);
|
|
expect(imported.sheets.find(sheet => sheet.sourceSheet === 'SI900001')?.rows)
|
|
.toEqual([['Object', 'EventTimestamp', 'Ignored'], ['Synthetic value', '2026-05-27 12:37', 'False']]);
|
|
expect(await canonicalCount(
|
|
purpleKnightHtmlFixture, 'Security_Assessment_Report_synthetic.html', 'text/html'
|
|
)).toBe(2);
|
|
});
|
|
|
|
it('links embedded PurpleKnight HTML result tables like workbook result sheets', async () => {
|
|
const imported = await importFile(new File(
|
|
[purpleKnightHtmlFixture], 'Security_Assessment_Report_synthetic.html', { type: 'text/html' }
|
|
), { profiles: importProfiles });
|
|
const preview = previewEntryFromDataset(imported, {
|
|
sha256: 'purple-knight-html-result'.padEnd(64, '0').slice(0, 64),
|
|
fileName: imported.fileName, sizeBytes: purpleKnightHtmlFixture.length, lastModified: 0
|
|
});
|
|
const model = createCanonicalImportBatch(
|
|
'purple-knight-html-result', [confirmEntryMappings(preview, importProfiles)], '2026-08-25T08:00:00.000Z'
|
|
).model;
|
|
const dataset = canonicalModelToDataset(model)!;
|
|
const mainSheet = dataset.sheets.find(sheet => sheet.analysis.records.some(record =>
|
|
record.fields.some(field => field.label === 'ShortName' && field.value === 'SI900001')))!;
|
|
const record = mainSheet.analysis.records.find(item =>
|
|
item.fields.some(field => field.label === 'ShortName' && field.value === 'SI900001'))!;
|
|
|
|
const linked = linkResultSheets(record, dataset.sheets, mainSheet.id);
|
|
expect(linked).toHaveLength(1);
|
|
expect(linked[0].sourceSheet).toBe('SI900001');
|
|
expect(linked[0].rows).toEqual([
|
|
['Object', 'EventTimestamp', 'Ignored'], ['Synthetic value', '2026-05-27 12:37', 'False']
|
|
]);
|
|
});
|
|
|
|
it('does not treat an empty Maester report template as a finding export', async () => {
|
|
const template = '<!doctype html><html><body><script>var placeholder={"Tests":[],"EndOfJson":true};</script></body></html>';
|
|
await expect(importFile(new File([template], 'ReportTemplate.html', { type: 'text/html' }), { profiles: importProfiles }))
|
|
.rejects.toThrow(/keine auswertbaren Tabellen/);
|
|
});
|
|
|
|
it('extracts all tenants from a Maester multi-tenant HTML report', async () => {
|
|
const report = `<!doctype html><html><body><div>Maester Security Tests</div><script>
|
|
const data={"Tenants":[
|
|
{"Result":"Passed","Tests":[{"Id":"MT.9101","Title":"Synthetic tenant one","Result":"Passed","Severity":"Low"}],"EndOfJson":true},
|
|
{"Result":"Failed","Tests":[{"Id":"MT.9102","Title":"Synthetic tenant two","Result":"Failed","Severity":"High"}],"EndOfJson":true}
|
|
],"EndOfJson":true};
|
|
</script></body></html>`;
|
|
const imported = await importFile(new File([report], 'Maester-MultiTenant-Synthetic.html', { type: 'text/html' }), {
|
|
profiles: importProfiles
|
|
});
|
|
|
|
expect(imported.profileId).toBe('maester');
|
|
expect(imported.extraction?.candidates.map(candidate => candidate.source.objectPath)).toEqual([
|
|
'$.Tenants[0].Tests[0]', '$.Tenants[1].Tests[0]'
|
|
]);
|
|
});
|
|
});
|