81 lines
5.7 KiB
TypeScript
81 lines
5.7 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { UnifiedResultProjection } from '../../src/domain/unified-result';
|
|
import { POLICY_EVALUATION_ENGINE, type PolicyEvaluationResult, type StoredPolicyEvaluation } from '../../src/domain/policy-evaluation';
|
|
import type { CanonicalAssessmentModel } from '../../src/domain/canonical-model';
|
|
import { buildReportProjection } from '../../src/domain/report-projection';
|
|
import { assembleReportProjectionInput } from '../../src/features/reports/report-input-assembler';
|
|
import { loadCanonicalProjectWorkspace } from '../../src/features/project-import/load-canonical-project-workspace';
|
|
import type { AssessorRepository } from '../../src/persistence/assessor-repository';
|
|
|
|
function observation(id: string): UnifiedResultProjection {
|
|
return {
|
|
sourceKind: 'policy-observation', sourceResultId: id, resultProjectionId: 'policy-observation:' + id,
|
|
projectId: 'synthetic', assessmentId: 'slot', importArtifactId: 'import-one', title: 'Mode', importedAt: '2026-09-06T00:00:00Z',
|
|
policyScope: 'computer', policyDeliveryMechanism: 'registry-policy', policyState: 'configured',
|
|
policyIdentityStatus: 'resolved', policyIdentityKey: 'identity-mode', policyBackupInstanceId: 'backup-' + id,
|
|
policyValues: [{ kind: 'registry-policy', key: 'Software\\Policies\\Synthetic', valueName: 'Mode',
|
|
operation: 'set-value', typeCode: 4, data: { kind: 'uint32', value: 1 } }]
|
|
} as unknown as UnifiedResultProjection;
|
|
}
|
|
|
|
function comparison(id: string, ids: string[], classification: PolicyEvaluationResult['classification']): PolicyEvaluationResult {
|
|
return {
|
|
id, observationIds: ids, baselineId: 'ms-baseline', baselineLabel: 'Synthetic Microsoft Baseline',
|
|
baselineState: 'explicit-recommendation', classification, customerState: 'configured',
|
|
comparator: { version: 1, comparator: { kind: 'equality' }, relation: classification === 'configured_match' ? 'match' : 'different',
|
|
actual: { kind: 'integer', value: '1' }, baseline: { kind: 'integer', value: '0' }, diagnosticCodes: [] },
|
|
identity: { id: 'identity-mode', scope: 'computer', target: {
|
|
kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Synthetic', valueName: 'Mode'
|
|
} }, diagnosticCodes: []
|
|
} as unknown as PolicyEvaluationResult;
|
|
}
|
|
|
|
function evaluation(id: string, aggregationRunId: string, completedAt: string, results: PolicyEvaluationResult[]): StoredPolicyEvaluation {
|
|
return { run: { id, aggregationRunId, evaluator: POLICY_EVALUATION_ENGINE, baselineAnalysisIds: ['baseline'], completedAt }, results } as StoredPolicyEvaluation;
|
|
}
|
|
|
|
describe('report GPO counts', () => {
|
|
// Regression 2026-09-28: the report counted every GPO setting once per stored evaluation run
|
|
// (50 Deviation / 50 Passed instead of Work's 25 / 25).
|
|
it('counts each GPO policy setting once, from the same current evaluation run as Work', async () => {
|
|
const observations = [observation('a'), observation('b')];
|
|
const superseded = evaluation('superseded', 'old-aggregation', '2026-09-07T10:00:00Z', [comparison('old', ['a', 'b'], 'confirmed_configured_deviation')]);
|
|
const current = evaluation('current', 'current-aggregation', '2026-09-08T10:00:00Z', [comparison('now', ['a', 'b'], 'confirmed_configured_deviation')]);
|
|
const model = { schemaVersion: 1, project: { id: 'synthetic', name: 'Synthetic' }, assessment: { id: 'slot', projectId: 'synthetic' },
|
|
scanRuns: [], importFiles: [], sourceTables: [], findingDefinitions: [], findingOccurrences: [], evidence: [], reviews: [], diagnostics: [] } as unknown as CanonicalAssessmentModel;
|
|
const repository = {
|
|
getProject: async () => ({ id: 'synthetic', name: 'Synthetic', attributes: {}, createdAt: '', updatedAt: '' }),
|
|
listAssessments: async () => [model.assessment],
|
|
loadAssessmentModel: async () => model,
|
|
queryUnifiedResults: async (query: { sourceKind?: string[] }) => {
|
|
const items = query.sourceKind?.includes('policy-observation') ? observations : [];
|
|
return { items, total: items.length, offset: 0, limit: 500 };
|
|
},
|
|
listPolicyEvaluations: async () => [superseded, current],
|
|
listMicrosoftSecurityBaselineAnalyses: async () => [],
|
|
getEnvironmentSnapshot: async () => ({ observations: [], sections: [] }),
|
|
listReviews: async () => [],
|
|
listProjectProcessingRuns: async () => [],
|
|
listGpoPolicyAnalyses: async () => [],
|
|
listPolicySnapshotAggregations: async () => []
|
|
} as unknown as AssessorRepository;
|
|
const getPolicyAnalysisOverview = async () => ({ current: { evaluationRunIds: ['current'] } as never });
|
|
const scope = { projectId: 'synthetic', assessmentIds: ['slot'], generatedAt: '2026-09-28T10:00:00Z' };
|
|
|
|
const report = buildReportProjection(await assembleReportProjectionInput(repository, scope, { getPolicyAnalysisOverview }));
|
|
const work = await loadCanonicalProjectWorkspace({
|
|
listProjectAssessments: repository.listAssessments, loadAssessmentModel: async () => model,
|
|
queryUnifiedResults: repository.queryUnifiedResults as never,
|
|
listPolicyEvaluations: async () => ({ records: [superseded, current] }), getPolicyAnalysisOverview
|
|
}, 'synthetic', undefined, ['slot']);
|
|
// A GPO-only timeslot has a single GPO sheet and no project sheet; the report must still read it.
|
|
const workRows = work.dataset!.sheets[0].analysis.records;
|
|
|
|
expect(report.policyEntries).toHaveLength(workRows.length);
|
|
expect(report.gpo?.counts.Fail).toBe(1);
|
|
// Without the current-run overview the superseded run is counted as well; this is the regressed path.
|
|
const unscoped = buildReportProjection(await assembleReportProjectionInput(repository, scope));
|
|
expect(unscoped.gpo?.counts.Fail).toBe(2);
|
|
});
|
|
});
|