169 lines
10 KiB
TypeScript
169 lines
10 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import pingCastleXml from '../../fixtures/pingcastle/healthcheck.synthetic.xml?raw';
|
|
import { confirmEntryMappings, previewEntryFromDataset } from '../../src/features/import-workflow/import-workflow';
|
|
import {
|
|
currentPolicyEvaluationsForWork,
|
|
loadCanonicalProjectWorkspace
|
|
} from '../../src/features/project-import/load-canonical-project-workspace';
|
|
import { createCanonicalImportBatch } from '../../src/importers/canonical-import';
|
|
import { importFile } from '../../src/importers/importer';
|
|
import { pingCastleProfile } from '../../src/profiles/pingcastle/pingcastle-profile';
|
|
import { importProfiles } from '../../src/profiles/profile-catalog';
|
|
import type { CanonicalAssessmentModel } from '../../src/domain/canonical-model';
|
|
import { POLICY_EVALUATION_ENGINE, type StoredPolicyEvaluation } from '../../src/domain/policy-evaluation';
|
|
import { selectCurrentPolicyEvaluations } from '../../src/domain/unified-result';
|
|
|
|
describe('canonical project workspace loading', () => {
|
|
it('uses one current evaluation per aggregation even when baseline coverage references more backups', () => {
|
|
const evaluation = (
|
|
id: string,
|
|
version: string,
|
|
baselineCount: number,
|
|
completedAt: string,
|
|
observationId = 'observation-one'
|
|
) => ({
|
|
run: {
|
|
id,
|
|
aggregationRunId: 'aggregation-one',
|
|
completedAt,
|
|
baselineAnalysisIds: Array.from({ length: baselineCount }, (_, index) => `baseline-${index}`),
|
|
evaluator: { ...POLICY_EVALUATION_ENGINE, version }
|
|
},
|
|
results: [{ observationIds: [observationId], backupInstanceIds: ['backup-one'] }]
|
|
}) as unknown as StoredPolicyEvaluation;
|
|
const complete = evaluation('complete', POLICY_EVALUATION_ENGINE.version, 11, '2026-09-06T10:00:00Z');
|
|
complete.results[0].backupInstanceIds = ['backup-one', 'backup-two', 'backup-three'];
|
|
const earlier = evaluation('earlier', POLICY_EVALUATION_ENGINE.version, 11, '2026-09-06T09:00:00Z');
|
|
earlier.results[0].backupInstanceIds = ['backup-one', 'backup-two'];
|
|
const laterPartial = evaluation('later-partial', POLICY_EVALUATION_ENGINE.version, 1, '2026-09-06T11:00:00Z');
|
|
const obsolete = evaluation('obsolete', '1.3.0', 12, '2026-09-06T12:00:00Z');
|
|
const unrelated = evaluation('unrelated', POLICY_EVALUATION_ENGINE.version, 12, '2026-09-06T13:00:00Z', 'other');
|
|
|
|
expect(currentPolicyEvaluationsForWork(
|
|
[laterPartial, obsolete, unrelated, earlier, complete],
|
|
new Set(['observation-one'])
|
|
)).toEqual([complete]);
|
|
const previous = evaluation('previous-version', '1.3.3', 12, '2026-09-06T12:00:00Z');
|
|
expect(currentPolicyEvaluationsForWork([previous], new Set(['observation-one']), new Set([previous.run.id])))
|
|
.toEqual([previous]);
|
|
expect(currentPolicyEvaluationsForWork([previous, complete], new Set(['observation-one']), new Set([previous.run.id])))
|
|
.toEqual([complete]);
|
|
expect(currentPolicyEvaluationsForWork([complete, previous], new Set(['observation-one']), new Set([previous.run.id])))
|
|
.toEqual([complete]);
|
|
expect(currentPolicyEvaluationsForWork([previous], new Set(['observation-one']), new Set())).toEqual([]);
|
|
expect(selectCurrentPolicyEvaluations([previous])).toEqual([]);
|
|
});
|
|
|
|
it('honors authoritative supersession, preserves separate imports and excludes mixed timeslots', () => {
|
|
const evaluation = (id: string, aggregationRunId: string, observationIds: string[]) => ({
|
|
run: { id, aggregationRunId, completedAt: '2026-09-06T10:00:00Z', baselineAnalysisIds: ['baseline'], evaluator: POLICY_EVALUATION_ENGINE },
|
|
results: [{ observationIds, backupInstanceIds: ['same-backup'] }]
|
|
}) as StoredPolicyEvaluation;
|
|
const history = evaluation('history', 'superseded-aggregation', ['one']);
|
|
const current = evaluation('current', 'current-aggregation', ['one']);
|
|
const separateImport = evaluation('separate', 'separate-aggregation', ['two']);
|
|
const otherSlot = evaluation('other-slot', 'other-aggregation', ['elsewhere']);
|
|
const mixed = evaluation('mixed', 'mixed-aggregation', ['one', 'elsewhere']);
|
|
const records = [history, current, separateImport, otherSlot, mixed];
|
|
const before = JSON.stringify(records);
|
|
expect(currentPolicyEvaluationsForWork(records, new Set(['one', 'two']),
|
|
new Set(['current', 'separate', 'other-slot', 'mixed']))).toEqual([current, separateImport]);
|
|
expect(currentPolicyEvaluationsForWork(records, new Set(['one']), new Set())).toEqual([]);
|
|
expect(JSON.stringify(records)).toBe(before);
|
|
});
|
|
|
|
it('opens a GPO-only assessment without loading another timeslot or requiring scanner results', async () => {
|
|
const stamp = '2026-09-06T00:00:00Z';
|
|
const model: CanonicalAssessmentModel = {
|
|
schemaVersion: 1,
|
|
project: { id: 'synthetic-client', name: 'Synthetic', description: '', attributes: {}, createdAt: stamp, updatedAt: stamp },
|
|
assessment: { id: 'gpo-slot', projectId: 'synthetic-client', name: 'Synthetic GPO slot', createdAt: stamp, updatedAt: stamp },
|
|
scanRuns: [], importFiles: [], sourceTables: [], findingDefinitions: [], findingOccurrences: [], evidence: [], reviews: [], diagnostics: []
|
|
};
|
|
const reader = {
|
|
listProjectAssessments: vi.fn().mockResolvedValue([model.assessment, { ...model.assessment, id: 'other-slot' }]),
|
|
loadAssessmentModel: vi.fn().mockResolvedValue(model),
|
|
queryUnifiedResults: vi.fn().mockResolvedValue({ items: [], total: 0, limit: 500, offset: 0 })
|
|
};
|
|
const workspace = await loadCanonicalProjectWorkspace(reader, model.project.id, [], ['gpo-slot']);
|
|
expect(reader.loadAssessmentModel).toHaveBeenCalledTimes(1);
|
|
expect(reader.loadAssessmentModel).toHaveBeenCalledWith('gpo-slot');
|
|
expect(workspace.assessmentIds).toEqual(['gpo-slot']);
|
|
expect(workspace.models).toEqual([model]);
|
|
expect(workspace.dataset).toBeNull();
|
|
expect(reader.queryUnifiedResults).toHaveBeenCalledWith(expect.objectContaining({ assessmentIds: ['gpo-slot'] }));
|
|
});
|
|
|
|
it('retains GPO results from an explicitly selected mixed timeslot when scanner runs live in another assessment', async () => {
|
|
const stamp = '2026-09-06T00:00:00Z';
|
|
const base: CanonicalAssessmentModel = {
|
|
schemaVersion: 1,
|
|
project: { id: 'synthetic-client', name: 'Synthetic', description: '', attributes: {}, createdAt: stamp, updatedAt: stamp },
|
|
assessment: { id: 'scanner-slot', projectId: 'synthetic-client', name: 'Scanner slot', createdAt: stamp, updatedAt: stamp },
|
|
scanRuns: [{ id: 'scanner-run', assessmentId: 'scanner-slot', sequence: 1, label: 'Synthetic scanner',
|
|
tool: { id: 'synthetic', name: 'Synthetic' }, importedAt: stamp }],
|
|
importFiles: [], sourceTables: [], findingDefinitions: [], findingOccurrences: [], evidence: [], reviews: [], diagnostics: []
|
|
};
|
|
const gpoModel = { ...base, assessment: { ...base.assessment, id: 'gpo-slot', name: 'GPO slot' }, scanRuns: [] };
|
|
const observation = {
|
|
sourceKind: 'policy-observation', sourceResultId: 'policy-one', resultProjectionId: 'policy-observation:policy-one',
|
|
projectId: base.project.id, assessmentId: 'gpo-slot', importArtifactId: 'gpo-artifact', title: 'Synthetic policy', importedAt: stamp,
|
|
policyScope: 'computer', policyDeliveryMechanism: 'registry-policy', policyState: 'configured', policyIdentityStatus: 'resolved',
|
|
policyIdentityKey: 'synthetic-policy', policyBackupInstanceId: 'backup-one', policyValues: [{ kind: 'registry-policy',
|
|
key: 'Software\\Policies\\Synthetic', valueName: 'Mode', operation: 'set-value', typeCode: 4,
|
|
data: { kind: 'uint32', value: 1 } }]
|
|
};
|
|
const secondObservation = { ...observation, sourceResultId: 'policy-two',
|
|
resultProjectionId: 'policy-observation:policy-two', policyBackupInstanceId: 'backup-two' };
|
|
const reader = {
|
|
listProjectAssessments: vi.fn().mockResolvedValue([base.assessment, gpoModel.assessment]),
|
|
loadAssessmentModel: vi.fn(async (id: string) => id === 'gpo-slot' ? gpoModel : base),
|
|
queryUnifiedResults: vi.fn().mockResolvedValue({ items: [observation, secondObservation], total: 2, limit: 500, offset: 0 }),
|
|
listPolicyEvaluations: vi.fn().mockResolvedValue({ records: [] })
|
|
};
|
|
|
|
const workspace = await loadCanonicalProjectWorkspace(reader, base.project.id, ['scanner-run'], ['scanner-slot', 'gpo-slot']);
|
|
|
|
expect(reader.queryUnifiedResults).toHaveBeenCalledWith(expect.objectContaining({
|
|
assessmentIds: ['scanner-slot', 'gpo-slot'], sourceKind: ['policy-observation']
|
|
}));
|
|
expect(workspace.assessmentIds).toEqual(['scanner-slot', 'gpo-slot']);
|
|
expect(workspace.dataset?.sheets[0].analysis.records.some(record =>
|
|
record.fields.some(field => field.label === 'Source: Scan' && field.value.startsWith('GPOs')))).toBe(true);
|
|
});
|
|
it('projects persisted PingCastle XML RiskRules instead of generic XML nodes', async () => {
|
|
const imported = await importFile(
|
|
new File([pingCastleXml], 'ad_hc_synthetic.example.xml', { type: 'application/xml' }),
|
|
{ profiles: importProfiles }
|
|
);
|
|
const preview = previewEntryFromDataset(imported, {
|
|
sha256: 'c'.repeat(64),
|
|
fileName: imported.fileName,
|
|
sizeBytes: pingCastleXml.length,
|
|
lastModified: 0
|
|
});
|
|
const entry = confirmEntryMappings(preview, importProfiles);
|
|
const model = createCanonicalImportBatch(
|
|
'pingcastle-workspace',
|
|
[entry],
|
|
'2026-07-22T12:00:00.000Z'
|
|
).model;
|
|
const reader = {
|
|
listProjectAssessments: vi.fn().mockResolvedValue([model.assessment]),
|
|
loadAssessmentModel: vi.fn().mockResolvedValue(model)
|
|
};
|
|
|
|
const workspace = await loadCanonicalProjectWorkspace(reader, model.project.id);
|
|
|
|
expect(reader.listProjectAssessments).toHaveBeenCalledWith(model.project.id);
|
|
expect(reader.loadAssessmentModel).toHaveBeenCalledWith(model.assessment.id);
|
|
expect(workspace.dataset?.profileId).toBe('pingcastle');
|
|
expect(workspace.dataset?.sheets[1].analysis.headers).toEqual(expect.arrayContaining(['Rule ID', 'Title', 'Status']));
|
|
expect(workspace.dataset?.sheets[1].analysis.headers).not.toContain('DomainSid');
|
|
expect(workspace.dataset?.sheets[1].analysis.records[0].title).toBe('Synthetic inactive privileged account');
|
|
expect(pingCastleProfile.defaults(workspace.dataset?.sheets[1]).titleColumn).toBe('Title');
|
|
});
|
|
});
|