Assessor_2.0/tests/importers/purple-knight-summary-sourc...

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');
});
});