84 lines
4.8 KiB
TypeScript
84 lines
4.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import summaryFixture from '../../fixtures/purple-knight/assessment-summary.synthetic.csv?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';
|
|
|
|
const FILE_NAME = 'Security_Assessment_Report_synthetic.example_(Assessment_summary).csv';
|
|
|
|
async function importSummary(content = summaryFixture, fileName = FILE_NAME) {
|
|
return importFile(new File([content], fileName, { type: 'text/csv' }), { profiles: importProfiles });
|
|
}
|
|
|
|
function observationValue(
|
|
observations: NonNullable<Awaited<ReturnType<typeof importFile>>['extraction']>['environmentObservations'],
|
|
property: string
|
|
) {
|
|
return observations?.find(item => item.property === property);
|
|
}
|
|
|
|
describe('Purple Knight assessment summary source extraction', () => {
|
|
it('turns the summary metrics into typed environment observations without creating findings', async () => {
|
|
const imported = await importSummary();
|
|
|
|
expect(imported.extraction?.extractor.id).toBe('purple-knight-summary-source-extractor');
|
|
expect(imported.extraction?.candidates).toHaveLength(0);
|
|
const observations = imported.extraction?.environmentObservations;
|
|
expect(observationValue(observations, 'purple_knight.ad.security_posture_score')).toMatchObject({
|
|
normalizedValue: 71, valueType: 'number', state: 'observed'
|
|
});
|
|
expect(observationValue(observations, 'purple_knight.ad.ioes_found')?.normalizedValue).toBe(17);
|
|
expect(observationValue(observations, 'purple_knight.ad.evaluated')?.normalizedValue).toBe(117);
|
|
expect(observationValue(observations, 'purple_knight.ad.passed')?.normalizedValue).toBe(100);
|
|
expect(observationValue(observations, 'purple_knight.ad.not_relevant')?.normalizedValue).toBe(2);
|
|
expect(observationValue(observations, 'purple_knight.tool_edition')?.originalValue).toBe('Community');
|
|
expect(observationValue(observations, 'purple_knight.ad.scan_duration')?.originalValue).toBe('00:00:21');
|
|
});
|
|
|
|
it('maps tool-neutral facts to the shared environment properties and keeps category scores separate', async () => {
|
|
const observations = (await importSummary()).extraction?.environmentObservations;
|
|
|
|
expect(observationValue(observations, 'forest.name')).toMatchObject({
|
|
originalValue: 'synthetic.example', subject: { type: 'active-directory-environment' }
|
|
});
|
|
expect(observationValue(observations, 'domain.count')?.normalizedValue).toBe(2);
|
|
expect(observationValue(observations, 'report.generated_at')).toMatchObject({
|
|
originalValue: '2026-01-15 09:30', valueType: 'datetime'
|
|
});
|
|
expect(observationValue(observations, 'purple_knight.ad.category_score.kerberos_security')).toMatchObject({
|
|
label: 'Kerberos Security', normalizedValue: 95, subject: { type: 'security-domain', identity: 'kerberos_security' }
|
|
});
|
|
expect(observations?.filter(item => item.property.startsWith('purple_knight.ad.category_score.'))).toHaveLength(6);
|
|
});
|
|
|
|
it('recognizes the summary as a Purple Knight source and persists it as context without findings', async () => {
|
|
const imported = await importSummary();
|
|
expect(imported.profileId).toBe('purple-knight');
|
|
expect(imported.recognition?.source.selected.toolId).toBe('purple-knight');
|
|
expect(imported.recognition?.source.requiresConfirmation).toBe(false);
|
|
|
|
const preview = previewEntryFromDataset(imported, {
|
|
sha256: FILE_NAME.padEnd(64, '0').slice(0, 64), fileName: FILE_NAME, sizeBytes: summaryFixture.length, lastModified: 0
|
|
});
|
|
const model = createCanonicalImportBatch('summary', [confirmEntryMappings(preview, importProfiles)], '2026-08-25T08:00:00.000Z').model;
|
|
assertCanonicalAssessmentModel(model);
|
|
|
|
expect(model.findingOccurrences).toHaveLength(0);
|
|
expect(model.environmentObservations?.every(item => item.tool.id === 'purple-knight')).toBe(true);
|
|
expect(model.environmentObservations?.find(item => item.property === 'purple_knight.ad.security_posture_score')?.normalizedValue).toBe(71);
|
|
expect(model.reportSections?.map(section => section.sectionKey)).toContain('purple-knight-assessment-summary');
|
|
});
|
|
|
|
it('leaves an unrelated two-column table to the generic extractor', async () => {
|
|
const imported = await importFile(
|
|
new File(['Metric,Value\nUptime,12\nOwner,Team A\n'], 'inventory.csv', { type: 'text/csv' }),
|
|
{ profiles: importProfiles }
|
|
);
|
|
expect(imported.extraction?.extractor.id).not.toBe('purple-knight-summary-source-extractor');
|
|
});
|
|
});
|