377 lines
18 KiB
TypeScript
377 lines
18 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
buildPolicyAnalysisOverview,
|
|
currentBaselineAnalyses,
|
|
gpoEvaluationUpdates,
|
|
type BuildPolicyAnalysisOverviewInput
|
|
} from '../../src/domain/policy-analysis-overview';
|
|
|
|
const timestamp = '2026-08-04T15:00:00.000Z';
|
|
|
|
function syntheticInput(): BuildPolicyAnalysisOverviewInput {
|
|
return {
|
|
projectId: 'synthetic-project',
|
|
generatedAt: timestamp,
|
|
profiles: [{
|
|
version: 1, id: 'profile-1', projectId: 'synthetic-project', name: 'Synthetic Windows',
|
|
revision: 1, createdAt: timestamp,
|
|
contexts: [{
|
|
version: 1, id: 'context-1', kind: 'operating-system', productFamily: 'Windows Server',
|
|
productVersion: '2025', role: 'Member Server', scope: 'computer'
|
|
}]
|
|
}],
|
|
identityAnalyses: [{
|
|
run: {
|
|
version: 1, id: 'identity-run-1', projectId: 'synthetic-project', artifactId: 'artifact-1',
|
|
sourceProcessingRunId: 'processing-1', analyzer: { id: 'gpo-policy-identity-analysis', version: '1' },
|
|
identityResolver: { id: 'resolver', version: '1' }, status: 'partial', observationCount: 1,
|
|
resolvedCount: 0, diagnosticCodes: ['gpo-policy-identity-analysis-partial'],
|
|
startedAt: timestamp, completedAt: timestamp
|
|
},
|
|
resolutions: [{
|
|
version: 1, id: 'resolution-1', projectId: 'synthetic-project', analysisRunId: 'identity-run-1',
|
|
sourceProcessingRunId: 'processing-1', backupInstanceId: 'backup-1', observationId: 'observation-1',
|
|
resolution: { version: 1, status: 'unresolved', reasonCode: 'gpo-policy-identity-unsupported-observation' }
|
|
}]
|
|
}],
|
|
aggregations: [{
|
|
version: 1,
|
|
run: {
|
|
version: 1, id: 'aggregation-1', projectId: 'synthetic-project', environmentProfileId: 'profile-1',
|
|
sourceAnalysisRunIds: ['identity-run-1'], snapshotId: 'snapshot-1',
|
|
aggregator: { id: 'gpo-policy-snapshot-aggregation', version: '1.1.0' }, status: 'partial',
|
|
stateCount: 1, conflictCount: 1, unresolvedIdentityCount: 1, reviewHintCount: 1,
|
|
permitsFindingGeneration: false, diagnosticCodes: ['policy-snapshot-incomplete'],
|
|
startedAt: timestamp, completedAt: timestamp
|
|
},
|
|
snapshot: {
|
|
version: 1, id: 'snapshot-1', projectId: 'synthetic-project',
|
|
domain: { version: 1, dnsName: 'synthetic.invalid', evidence: 'backup-metadata' },
|
|
instanceIds: ['backup-1'], status: 'indeterminate', completeness: 'selected-subset',
|
|
permitsSetAbsenceConclusions: false, diagnosticCodes: [], createdAt: timestamp
|
|
},
|
|
states: [{
|
|
version: 1, id: 'state-1', aggregationRunId: 'aggregation-1',
|
|
identity: { version: 1, id: 'identity-1' }, deliveryMechanism: 'registry-policy',
|
|
status: 'conflicted', values: [{ kind: 'registry', type: 'REG_DWORD', value: 1 }],
|
|
observationIds: ['observation-1'], backupInstanceIds: ['backup-1'], applicableContextIds: ['context-1'],
|
|
ordering: 'cross-gpo-conflict', targeting: 'none', completeness: 'complete', effectiveState: 'unknown',
|
|
diagnosticCodes: ['policy-snapshot-customer-conflict']
|
|
}],
|
|
reviewHints: [{
|
|
version: 1, id: 'hint-1', code: 'policy-targeting-unknown', identityId: 'identity-1',
|
|
observationIds: ['observation-1'], backupInstanceIds: ['backup-1']
|
|
}]
|
|
}],
|
|
evaluations: [{
|
|
version: 1,
|
|
run: {
|
|
version: 1, id: 'evaluation-1', projectId: 'synthetic-project', aggregationRunId: 'aggregation-1',
|
|
environmentProfileId: 'profile-1', baselineAnalysisIds: ['baseline-analysis-1'],
|
|
evaluator: { id: 'policy-baseline-evaluation', version: '1.1.0' },
|
|
comparatorCatalog: { id: 'policy-comparator-catalog', version: '1.1.0' },
|
|
dependencyRules: { id: 'policy-dependency-rules', version: '1.1.0' },
|
|
riskRules: { id: 'policy-risk-rules', version: '1.1.0' },
|
|
ruleCatalog: { version: 1, id: 'rules', catalog: { id: 'rules', version: '1' }, status: 'released', rules: [] },
|
|
status: 'partial', resultCount: 2, matchCount: 0, deviationCount: 1, notApplicableCount: 0,
|
|
reviewCount: 1, projectedFindingCount: 0, diagnosticCodes: ['policy-evaluation-partial'],
|
|
startedAt: timestamp, completedAt: timestamp
|
|
},
|
|
results: [{
|
|
version: 1, id: 'evaluation-result-review', evaluationRunId: 'evaluation-1', aggregationStateId: 'state-1',
|
|
identity: { version: 1, id: 'identity-1' }, classification: 'review_required',
|
|
comparator: { version: 1, comparator: { kind: 'equality' }, relation: 'incomparable', diagnosticCodes: [] },
|
|
baselineState: 'catalog-gap', contextIds: ['context-1'], ruleIds: [], dependencyRuleIds: [], riskRuleIds: [],
|
|
observationIds: ['observation-1'], backupInstanceIds: ['backup-1'], baselineAnalysisIds: [], baselineSettingIds: [],
|
|
findingEligibility: 'ineligible', effectiveState: 'unknown', diagnosticCodes: ['policy-evaluation-baseline-missing']
|
|
}],
|
|
evidence: []
|
|
}],
|
|
findingRuns: [{
|
|
version: 1,
|
|
run: {
|
|
version: 1, id: 'finding-run-1', projectId: 'synthetic-project', evaluationRunId: 'evaluation-1',
|
|
factory: { id: 'policy-finding-factory', version: '1.0.0' }, status: 'partial', eligibleResultCount: 2,
|
|
findingCount: 1, rejectedResultCount: 1, startedAt: timestamp, completedAt: timestamp
|
|
},
|
|
definitions: [], occurrences: [{} as never],
|
|
rejections: [{ version: 1, evaluationResultId: 'evaluation-result-rejected', diagnosticCodes: ['policy-finding-evidence-incomplete'] }]
|
|
}]
|
|
} as unknown as BuildPolicyAnalysisOverviewInput;
|
|
}
|
|
|
|
/** All generations share the package-derived timestamp, so only the analyzer version orders them. */
|
|
function baselineAnalyses(records: Array<{ id: string; version: string; baselineId?: string;
|
|
packageSha256?: string; supersedes?: string }>) {
|
|
return records.map(record => ({
|
|
version: 1,
|
|
analysis: {
|
|
id: record.id, baselineId: record.baselineId ?? 'baseline-1', analyzedAt: timestamp,
|
|
...(record.packageSha256 ? { packageSha256: record.packageSha256 } : {}),
|
|
analyzer: { id: 'microsoft-baseline-gpo-settings', version: record.version }
|
|
},
|
|
...(record.supersedes ? { supersedesAnalysisId: record.supersedes } : {})
|
|
})) as unknown as BuildPolicyAnalysisOverviewInput['baselineAnalyses'];
|
|
}
|
|
|
|
describe('policy analysis overview', () => {
|
|
it('exposes old engine results per exact active artifact and clears them after a current evaluation', () => {
|
|
const input = syntheticInput();
|
|
const before = JSON.stringify(input);
|
|
const updates = gpoEvaluationUpdates(input);
|
|
expect(updates).toHaveLength(1);
|
|
expect(updates[0]).toMatchObject({ evaluationRunId: 'evaluation-1', currentVersion: '1.9.0', artifactIds: ['artifact-1'] });
|
|
expect(JSON.stringify(input)).toBe(before);
|
|
input.evaluations.push({ ...input.evaluations[0], run: { ...input.evaluations[0].run,
|
|
id: 'current', evaluator: { id: 'policy-baseline-evaluation', version: '1.9.0' } } });
|
|
expect(gpoEvaluationUpdates(input)).toEqual([]);
|
|
});
|
|
|
|
it('keeps counting an unsupported comparison so the retained parser-gap report stays live', () => {
|
|
const input = syntheticInput();
|
|
input.evaluations[0].results.push({ ...input.evaluations[0].results[0], id: 'evaluation-result-unsupported',
|
|
baselineState: 'unsupported-setting', classification: 'review_required',
|
|
diagnosticCodes: ['policy-evaluation-comparator-unsupported'] } as never);
|
|
|
|
const overview = buildPolicyAnalysisOverview(input);
|
|
expect(overview.summary.parserGaps).toBe(1);
|
|
expect(overview.reviewItems.find(item => item.subjectId === 'evaluation-result-unsupported')?.category)
|
|
.toBe('parser-gap');
|
|
});
|
|
|
|
it('reports a restated unresolved identity once with its authoritative reason', () => {
|
|
const input = syntheticInput();
|
|
const reasonCode = 'identity-resolver-unsupported-group-policy-preference';
|
|
input.identityAnalyses[0].resolutions[0].resolution = { version: 1, status: 'unresolved', reasonCode };
|
|
// The aggregation restates the very same observation as a review hint.
|
|
input.aggregations[0].reviewHints.push({ version: 1, id: 'hint-restated', code: reasonCode,
|
|
observationIds: ['observation-1'], backupInstanceIds: ['backup-1'] } as never);
|
|
|
|
const overview = buildPolicyAnalysisOverview(input);
|
|
const unresolved = overview.reviewItems.filter(item => item.category === 'unresolved-identity');
|
|
expect(unresolved).toHaveLength(1);
|
|
expect(unresolved[0].source).toBe('identity-analysis');
|
|
expect(overview.summary.unresolvedIdentities).toBe(1);
|
|
// The restatement is gone from the queue instead of showing as a second, unnamed review.
|
|
expect(overview.reviewItems.some(item => item.subjectId === 'hint-restated')).toBe(false);
|
|
|
|
// An identity-resolver hint the analyses do not cover stays visible and correctly categorised.
|
|
input.aggregations[0].reviewHints.push({ version: 1, id: 'hint-uncovered', code: reasonCode,
|
|
observationIds: ['observation-elsewhere'], backupInstanceIds: ['backup-1'] } as never);
|
|
const widened = buildPolicyAnalysisOverview(input);
|
|
expect(widened.summary.unresolvedIdentities).toBe(2);
|
|
expect(widened.reviewItems.find(item => item.subjectId === 'hint-uncovered')?.category)
|
|
.toBe('unresolved-identity');
|
|
});
|
|
|
|
it('collapses only generations of the very same baseline package', () => {
|
|
// The automatic pipeline and the readiness check must agree, otherwise a fresh run is
|
|
// reported as outdated the moment it is written.
|
|
const ids = (records: Parameters<typeof baselineAnalyses>[0]) =>
|
|
currentBaselineAnalyses(baselineAnalyses(records)!).map(record => record.analysis.id).sort();
|
|
|
|
expect(ids([
|
|
{ id: 'old-a', version: '1.2.0', baselineId: 'baseline-a', packageSha256: 'package-a' },
|
|
{ id: 'new-a', version: '1.3.0', baselineId: 'baseline-a', packageSha256: 'package-a' },
|
|
{ id: 'only-b', version: '1.2.0', baselineId: 'baseline-b', packageSha256: 'package-b' }
|
|
])).toEqual(['new-a', 'only-b']);
|
|
|
|
// Independently analysed packages stay independent benchmarks, even under one baseline.
|
|
expect(ids([
|
|
{ id: 'package-one', version: '1.3.0', baselineId: 'baseline-a', packageSha256: 'first' },
|
|
{ id: 'package-two', version: '1.3.0', baselineId: 'baseline-a', packageSha256: 'second' }
|
|
])).toEqual(['package-one', 'package-two']);
|
|
|
|
// Without a package checksum nothing is provably one lineage, so nothing collapses.
|
|
expect(ids([
|
|
{ id: 'unknown-one', version: '1.2.0', baselineId: 'baseline-a' },
|
|
{ id: 'unknown-two', version: '1.3.0', baselineId: 'baseline-a' }
|
|
])).toEqual(['unknown-one', 'unknown-two']);
|
|
});
|
|
|
|
it('re-evaluates a current engine run against the current baseline analysis generation', () => {
|
|
const input = syntheticInput();
|
|
input.evaluations[0].run.evaluator = { id: 'policy-baseline-evaluation', version: '1.9.0' };
|
|
input.baselineAnalyses = baselineAnalyses([
|
|
{ id: 'baseline-analysis-1', version: '1.1.0', packageSha256: 'package-1' },
|
|
{ id: 'baseline-analysis-2', version: '1.3.0', packageSha256: 'package-1', supersedes: 'baseline-analysis-1' }
|
|
]);
|
|
|
|
const updates = gpoEvaluationUpdates(input);
|
|
expect(updates).toHaveLength(1);
|
|
// The retained run stays pinned to its own ids; the action must send the newer generation.
|
|
expect(updates[0].baselineAnalysisIds).toEqual(['baseline-analysis-2']);
|
|
expect(updates[0].reasons).toEqual(['baseline-analysis']);
|
|
|
|
input.evaluations.push({ ...input.evaluations[0], run: { ...input.evaluations[0].run,
|
|
id: 'current', baselineAnalysisIds: ['baseline-analysis-2'] } });
|
|
expect(gpoEvaluationUpdates(input)).toEqual([]);
|
|
});
|
|
|
|
it('never widens the evaluated baseline set and keeps a baseline without a newer generation', () => {
|
|
const input = syntheticInput();
|
|
input.evaluations[0].run.evaluator = { id: 'policy-baseline-evaluation', version: '1.9.0' };
|
|
input.baselineAnalyses = baselineAnalyses([
|
|
{ id: 'baseline-analysis-1', version: '1.1.0', packageSha256: 'package-1' },
|
|
{ id: 'unrelated-analysis', version: '1.3.0', baselineId: 'baseline-2', packageSha256: 'package-2' }
|
|
]);
|
|
expect(gpoEvaluationUpdates(input)).toEqual([]);
|
|
|
|
input.evaluations[0].run.evaluator = { id: 'policy-baseline-evaluation', version: '1.8.0' };
|
|
const updates = gpoEvaluationUpdates(input);
|
|
expect(updates[0].baselineAnalysisIds).toEqual(['baseline-analysis-1']);
|
|
expect(updates[0].reasons).toEqual(['engine-version']);
|
|
});
|
|
|
|
it('prefers the newer analyzer generation when a supersession chain is interrupted', () => {
|
|
const input = syntheticInput();
|
|
input.evaluations[0].run.evaluator = { id: 'policy-baseline-evaluation', version: '1.9.0' };
|
|
// Both remain unsuperseded and share the package timestamp; only the analyzer version orders them.
|
|
input.baselineAnalyses = baselineAnalyses([
|
|
{ id: 'baseline-analysis-1', version: '1.1.0', packageSha256: 'package-1' },
|
|
{ id: 'zz-newer-generation', version: '1.3.0', packageSha256: 'package-1' },
|
|
{ id: 'aa-older-generation', version: '1.2.0', packageSha256: 'package-1' }
|
|
]);
|
|
expect(gpoEvaluationUpdates(input)[0].baselineAnalysisIds).toEqual(['zz-newer-generation']);
|
|
});
|
|
|
|
it('does not request re-evaluation for superseded profiles or source analysis chains', () => {
|
|
const input = syntheticInput();
|
|
input.profiles.push({ ...input.profiles[0], id: 'new-profile', supersedesProfileId: input.profiles[0].id });
|
|
expect(gpoEvaluationUpdates(input)).toEqual([]);
|
|
input.profiles.pop();
|
|
input.identityAnalyses.push({ ...input.identityAnalyses[0], run: { ...input.identityAnalyses[0].run,
|
|
id: 'replacement', completedAt: '2026-09-11T00:00:00Z', supersedesAnalysisRunId: input.identityAnalyses[0].run.id } });
|
|
expect(gpoEvaluationUpdates(input)).toEqual([]);
|
|
});
|
|
it('keeps confirmed findings separate from deviations and all review-only evidence', () => {
|
|
const overview = buildPolicyAnalysisOverview(syntheticInput());
|
|
|
|
expect(overview.summary.confirmedDeviations).toBe(1);
|
|
expect(overview.summary.policyFindings).toBe(1);
|
|
expect(overview.summary.conflicts).toBe(1);
|
|
expect(overview.summary.unresolvedIdentities).toBe(1);
|
|
expect(overview.summary.catalogGaps).toBe(1);
|
|
expect(overview.summary.partialRuns).toBe(4);
|
|
expect(overview.current.summary).toEqual(overview.summary);
|
|
expect(overview.current.evaluationRunIds).toEqual(['evaluation-1']);
|
|
expect(overview.current.findingRunIds).toEqual(['finding-run-1']);
|
|
expect(overview.reviewItems.map(item => item.category)).toEqual(expect.arrayContaining([
|
|
'conflict', 'unresolved-identity', 'catalog-gap', 'partial-analysis',
|
|
'targeting-unknown', 'incomplete-evidence'
|
|
]));
|
|
expect(overview.evaluations[0]).toMatchObject({ findingCount: 1, rejectedFindingCount: 1, reviewCount: 1 });
|
|
});
|
|
|
|
it('returns an empty, deterministic project view without inventing findings', () => {
|
|
const overview = buildPolicyAnalysisOverview({
|
|
projectId: 'empty-project', generatedAt: timestamp,
|
|
identityAnalyses: [], profiles: [], aggregations: [], evaluations: [], findingRuns: []
|
|
});
|
|
expect(overview).toMatchObject({
|
|
version: 1, projectId: 'empty-project', generatedAt: timestamp,
|
|
summary: { policyFindings: 0, reviewItems: 0, conflicts: 0, partialRuns: 0 },
|
|
current: {
|
|
version: 1,
|
|
summary: { policyFindings: 0, reviewItems: 0, conflicts: 0, partialRuns: 0 },
|
|
evaluationRunIds: [], findingRunIds: [], reviewItems: []
|
|
},
|
|
evaluations: [], reviewItems: []
|
|
});
|
|
});
|
|
|
|
it('keeps superseded processing chains in history but excludes them from the current snapshot', () => {
|
|
const input = syntheticInput();
|
|
const currentTimestamp = '2026-08-05T15:00:00.000Z';
|
|
input.identityAnalyses.push({
|
|
run: {
|
|
...input.identityAnalyses[0].run,
|
|
id: 'identity-run-2',
|
|
sourceProcessingRunId: 'processing-2',
|
|
supersedesAnalysisRunId: 'identity-run-1',
|
|
status: 'complete',
|
|
resolvedCount: 1,
|
|
diagnosticCodes: [],
|
|
startedAt: currentTimestamp,
|
|
completedAt: currentTimestamp
|
|
},
|
|
resolutions: []
|
|
});
|
|
input.aggregations.push({
|
|
...input.aggregations[0],
|
|
run: {
|
|
...input.aggregations[0].run,
|
|
id: 'aggregation-2',
|
|
sourceAnalysisRunIds: ['identity-run-2'],
|
|
snapshotId: 'snapshot-2',
|
|
status: 'complete',
|
|
conflictCount: 0,
|
|
unresolvedIdentityCount: 0,
|
|
reviewHintCount: 0,
|
|
diagnosticCodes: [],
|
|
startedAt: currentTimestamp,
|
|
completedAt: currentTimestamp
|
|
},
|
|
snapshot: {
|
|
...input.aggregations[0].snapshot,
|
|
id: 'snapshot-2',
|
|
status: 'consistent',
|
|
completeness: 'complete'
|
|
},
|
|
states: [],
|
|
reviewHints: []
|
|
});
|
|
input.evaluations.push({
|
|
...input.evaluations[0],
|
|
run: {
|
|
...input.evaluations[0].run,
|
|
id: 'evaluation-2',
|
|
aggregationRunId: 'aggregation-2',
|
|
status: 'complete',
|
|
matchCount: 3,
|
|
deviationCount: 2,
|
|
reviewCount: 0,
|
|
diagnosticCodes: [],
|
|
startedAt: currentTimestamp,
|
|
completedAt: currentTimestamp
|
|
},
|
|
results: [],
|
|
evidence: []
|
|
});
|
|
input.findingRuns.push({
|
|
...input.findingRuns[0],
|
|
run: {
|
|
...input.findingRuns[0].run,
|
|
id: 'finding-run-2',
|
|
evaluationRunId: 'evaluation-2',
|
|
status: 'complete',
|
|
eligibleResultCount: 2,
|
|
findingCount: 2,
|
|
rejectedResultCount: 0,
|
|
startedAt: currentTimestamp,
|
|
completedAt: currentTimestamp
|
|
},
|
|
occurrences: [{}, {}] as never[],
|
|
rejections: []
|
|
});
|
|
|
|
const overview = buildPolicyAnalysisOverview(input);
|
|
|
|
expect(overview.summary).toMatchObject({
|
|
identityAnalysisRuns: 2, aggregationRuns: 2, evaluationRuns: 2, findingRuns: 2,
|
|
confirmedDeviations: 3, policyFindings: 3
|
|
});
|
|
expect(overview.current.summary).toMatchObject({
|
|
identityAnalysisRuns: 1, aggregationRuns: 1, evaluationRuns: 1, findingRuns: 1,
|
|
configuredMatches: 3, confirmedDeviations: 2, policyFindings: 2,
|
|
reviewItems: 0, conflicts: 0, unresolvedIdentities: 0, partialRuns: 0
|
|
});
|
|
expect(overview.current.evaluationRunIds).toEqual(['evaluation-2']);
|
|
expect(overview.current.findingRunIds).toEqual(['finding-run-2']);
|
|
expect(overview.current.reviewItems).toEqual([]);
|
|
expect(overview.reviewItems.length).toBeGreaterThan(0);
|
|
expect(overview.actions.evaluate.map(action => action.aggregationRunId)).toEqual(['aggregation-2']);
|
|
expect(overview.actions.createFindings.map(action => action.evaluationRunId)).toEqual(['evaluation-2']);
|
|
});
|
|
});
|