Assessor_2.0/tests/features/report-gpo-current-evaluati...

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