109 lines
7.7 KiB
TypeScript
109 lines
7.7 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { RELEASED_ASSESSMENT_CONTEXT_CATALOG } from '../../src/assessment-context/context-catalog';
|
|
import { contextClusterSeverities } from '../../src/assessment-context/context-cluster-severity';
|
|
import { contextHeatmap } from '../../src/assessment-context/context-heatmap';
|
|
import { buildAssessmentContext } from '../../src/assessment-context/context-engine';
|
|
import type { UnifiedResultProjection } from '../../src/domain/unified-result';
|
|
import { emptyHeatmapContext, syntheticHeatmapContext, reassignedHeatmapContext } from './context-heatmap-fixture';
|
|
|
|
describe('Context category severity and heatmap', () => {
|
|
it('excludes GPO settings from Other after recognition assigns their real cluster', () => {
|
|
const context = reassignedHeatmapContext();
|
|
const before = JSON.stringify(context);
|
|
const cells = contextHeatmap(context);
|
|
const other = cells.find(item => item.cluster.clusterId === 'other-deviations')!;
|
|
expect(other.scannerDeviations + other.gpoDeviations).toBe(2);
|
|
expect(cells.find(item => item.cluster.clusterId === 'lateral-movement-exposure')!.gpoDeviations).toBe(29);
|
|
expect(JSON.stringify(context)).toBe(before);
|
|
});
|
|
|
|
it('catalogues the reported smartcard rule but never guesses the unspecified Entra recommendation', () => {
|
|
const scannerFindings = ['fail', 'warning', 'pass', 'not_run'].map((outcome, index) => ({
|
|
sourceKind: 'scanner-finding', resultProjectionId: `smartcard-${index}`, sourceResultId: `smartcard-${index}`,
|
|
sourceTool: 'purple-knight', sourceRuleId: 'SI000147', outcomeNormalized: outcome,
|
|
title: 'Smart card password rotation disabled', categories: []
|
|
} as unknown as UnifiedResultProjection));
|
|
scannerFindings.push({ sourceKind: 'scanner-finding', resultProjectionId: 'entra', sourceResultId: 'entra',
|
|
sourceTool: 'maester', sourceRuleId: 'MT.1024', outcomeNormalized: 'fail', title: 'Entra Recommendation - .', categories: []
|
|
} as unknown as UnifiedResultProjection);
|
|
const before = JSON.stringify(scannerFindings);
|
|
const context = buildAssessmentContext({ projectId: 'synthetic', assessmentIds: ['slot'], repositoryRevision: 1,
|
|
generatedAt: '2026-10-02T12:00:00Z', observations: [], analyses: [], scannerFindings, catalog: RELEASED_ASSESSMENT_CONTEXT_CATALOG });
|
|
expect(context.findings.find(item => item.occurrenceId === 'smartcard-0')!.clusterIds).toEqual(['lateral-movement-exposure']);
|
|
expect(context.observations.find(item => item.occurrenceId === 'smartcard-0')!.matchedBy).toBe('rule-id');
|
|
expect(context.findings.find(item => item.occurrenceId === 'entra')!.clusterIds).toEqual(['other-deviations']);
|
|
const cells = contextHeatmap(context);
|
|
expect(cells.find(item => item.cluster.clusterId === 'lateral-movement-exposure')!.scannerDeviations).toBe(2);
|
|
expect(cells.find(item => item.cluster.clusterId === 'other-deviations')!.scannerDeviations).toBe(1);
|
|
expect(JSON.stringify(scannerFindings)).toBe(before);
|
|
});
|
|
|
|
it('covers every released cluster exactly once with the approved category assessment and local sources', () => {
|
|
expect(contextClusterSeverities.map(item => item.clusterId).sort()).toEqual(RELEASED_ASSESSMENT_CONTEXT_CATALOG.clusters.map(item => item.id).sort());
|
|
expect(contextClusterSeverities).toHaveLength(22);
|
|
expect(contextClusterSeverities.filter(item => item.severity === 'critical').map(item => item.clusterId)).toEqual([
|
|
'cloud-privileged-access-exposure', 'ad-privileged-account-exposure', 'kerberos-delegation-abuse', 'adcs-escalation'
|
|
]);
|
|
expect(contextClusterSeverities.filter(item => item.severity === 'high')).toHaveLength(13);
|
|
expect(contextClusterSeverities.filter(item => item.severity === 'medium')).toHaveLength(4);
|
|
for (const item of contextClusterSeverities) {
|
|
expect(item.rationale.length).toBeGreaterThan(30);
|
|
expect(item.qualification.length).toBeGreaterThan(30);
|
|
if (item.severity === 'undetermined') expect(item.clusterId).toBe('other-deviations');
|
|
else expect(item.sources.length).toBeGreaterThan(0);
|
|
for (const source of item.sources) expect(new URL(source.url).protocol).toBe('https:');
|
|
}
|
|
});
|
|
|
|
it('retains unknown and passed-only areas as neutral and never treats missing coverage as low severity', () => {
|
|
const context = emptyHeatmapContext();
|
|
expect(contextHeatmap(context).every(cell => !cell.hasEvidence)).toBe(true);
|
|
const cluster = context.clusters[0];
|
|
cluster.unknown = 0; cluster.notSatisfied = cluster.conditions.length; cluster.state = 'not-present';
|
|
expect(contextHeatmap(context)[0].hasEvidence).toBe(false);
|
|
expect(contextHeatmap(context).find(cell => cell.cluster.clusterId === 'other-deviations')?.reference.severity).toBe('undetermined');
|
|
context.clusters.push({ ...cluster, clusterId: 'future-cluster' });
|
|
expect(contextHeatmap(context).at(-1)?.reference.severity).toBe('undetermined');
|
|
});
|
|
|
|
it('retains the Context threshold and all conditions without re-evaluating or mutating them', () => {
|
|
const context = syntheticHeatmapContext();
|
|
const before = JSON.stringify(context);
|
|
const cells = contextHeatmap(context);
|
|
expect(cells.some(cell => cell.hasEvidence && cell.cluster.state === 'undetermined')).toBe(true);
|
|
for (const cell of cells) {
|
|
expect(cell.hasEvidence).toBe(cell.cluster.satisfied > 0);
|
|
expect(cell.cluster).toBe(context.clusters.find(cluster => cluster.clusterId === cell.cluster.clusterId));
|
|
}
|
|
expect(JSON.stringify(context)).toBe(before);
|
|
});
|
|
|
|
it('includes scanner deviations without source severity and retains recognized confidence', () => {
|
|
const source = { sourceKind: 'scanner-finding', resultProjectionId: 'scanner-finding:one', sourceResultId: 'one',
|
|
sourceTool: 'purple-knight', sourceRuleId: 'SYNTHETIC', outcomeNormalized: 'fail', title: 'Users with SPN defined', categories: [] } as unknown as UnifiedResultProjection;
|
|
const context = buildAssessmentContext({ projectId: 'synthetic', assessmentIds: ['slot'], repositoryRevision: 1,
|
|
generatedAt: '2026-10-02T12:00:00Z', observations: [], analyses: [], scannerFindings: [source], catalog: RELEASED_ASSESSMENT_CONTEXT_CATALOG });
|
|
const cell = contextHeatmap(context).find(item => item.cluster.clusterId === 'kerberoasting-exposure')!;
|
|
expect(cell.hasEvidence).toBe(true);
|
|
expect(cell.scannerDeviations).toBe(1);
|
|
expect(cell.recognized).toBe(1);
|
|
expect(cell.reference.severity).toBe('high');
|
|
expect(context.recognitions.some(item => item.confidence === 'recognized')).toBe(true);
|
|
expect(source.severityNormalized).toBeUndefined();
|
|
});
|
|
|
|
it('deduplicates scanner and GPO deviations per cluster and excludes passed evaluations', () => {
|
|
const context = syntheticHeatmapContext();
|
|
const cluster = context.clusters.find(item => item.clusterId === 'adcs-escalation')!;
|
|
const conditionId = cluster.conditions[0].conditionId;
|
|
const before = contextHeatmap(context).find(item => item.cluster.clusterId === cluster.clusterId)!.scannerDeviations;
|
|
context.observations.push({ ...context.observations.find(item => item.conditionId === conditionId)!, id: 'duplicate-evidence' });
|
|
const evaluation = { id: 'gpo-a', conditionId, identityKey: 'setting-one', scope: 'computer' as const, settingName: 'Synthetic setting',
|
|
verdict: 'deviation' as const, matchedBy: 'setting' as const, resultProjectionIds: ['gpo-a'], assertion: 'evaluated-by-assessor' as const };
|
|
context.evaluations.push(evaluation, { ...evaluation, id: 'gpo-b' }, { ...evaluation, id: 'passed', identityKey: 'passed-setting', verdict: 'passed' });
|
|
const cell = contextHeatmap(context).find(item => item.cluster.clusterId === cluster.clusterId)!;
|
|
expect(cell.scannerDeviations).toBe(before);
|
|
expect(cell.gpoDeviations).toBe(1);
|
|
});
|
|
});
|