Assessor_2.0/tests/assessment-context/context-engine.test.ts

693 lines
54 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { JsonValue } from '../../src/domain/canonical-model';
import type { StoredGpoPolicyAnalysis } from '../../src/domain/gpo-analysis-persistence';
import type { UnifiedResultProjection } from '../../src/domain/unified-result';
import {
assessmentContextCatalogErrors, RELEASED_ASSESSMENT_CONTEXT_CATALOG, type AssessmentContextCatalog
} from '../../src/assessment-context/context-catalog';
import { buildAssessmentContext } from '../../src/assessment-context/context-engine';
import { contextHeatmap, contextHeatmapFindingCount } from '../../src/assessment-context/context-heatmap';
import { ContextStateChangedError, createAssessmentContext, type ContextReadPort } from '../../src/assessment-context/context-run';
type Target =
| { kind: 'security-template'; section: string; key: string }
| { kind: 'registry-value'; hive: 'HKEY_LOCAL_MACHINE'; key: string; valueName: string };
interface Declared { id: string; backup: string; link?: 'linked' | 'not-linked'; target: Target; values: JsonValue[]; state?: string; resolved?: boolean }
const template = (key: string): Target => ({ kind: 'security-template', section: 'Registry Values', key });
const dword = (raw: string): JsonValue => ({ kind: 'registry', typeCode: 4, rawData: raw });
const LSA = 'MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\LmCompatibilityLevel';
const SMB_SERVER = 'MACHINE\\System\\CurrentControlSet\\Services\\LanManServer\\Parameters\\RequireSecuritySignature';
const LDAP_SERVER = 'MACHINE\\System\\CurrentControlSet\\Services\\NTDS\\Parameters\\LDAPServerIntegrity';
const LDAP_CLIENT = 'MACHINE\\System\\CurrentControlSet\\Services\\LDAP\\LDAPClientIntegrity';
const llmnr: Target = { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Microsoft\\Windows NT\\DNSClient', valueName: 'EnableMulticast' };
function fixture(declared: Declared[]): { observations: UnifiedResultProjection[]; analyses: StoredGpoPolicyAnalysis[] } {
const observations = declared.map(item => ({
resultProjectionId: `policy-observation:${item.id}`, sourceKind: 'policy-observation', sourceResultId: item.id,
projectId: 'dummy-project', policyScope: 'computer', policyState: item.state ?? 'configured',
policyIdentityStatus: item.resolved === false ? 'unresolved' : 'resolved',
policyIdentityKey: `identity-${JSON.stringify(item.target).length}-${'key' in item.target ? item.target.key.split('\\').at(-1) : ''}-${'valueName' in item.target ? item.target.valueName : ''}`,
policyValues: item.values, policyBackupInstanceId: item.backup, policyGpoId: `{${item.backup}}`,
...(item.link ? { policyBackupLinkStatus: item.link } : {})
})) as unknown as UnifiedResultProjection[];
const analyses = [{
run: { id: 'dummy-analysis' },
resolutions: declared.filter(item => item.resolved !== false).map((item, index) => ({
observationId: item.id,
resolution: { version: 1, status: 'resolved', identity: { id: observations[declared.indexOf(item)].policyIdentityKey, target: item.target } },
index
}))
}] as unknown as StoredGpoPolicyAnalysis[];
return { observations, analyses };
}
function context(declared: Declared[], catalog: AssessmentContextCatalog = RELEASED_ASSESSMENT_CONTEXT_CATALOG) {
const { observations, analyses } = fixture(declared);
return buildAssessmentContext({
projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: '2026-09-30T08:00:00Z',
repositoryRevision: 4, observations, analyses, catalog
});
}
const condition = (result: ReturnType<typeof context>, id: string) =>
result.clusters[0].conditions.find(item => item.conditionId === id)!;
describe('context catalog', () => {
it('is valid, sourced and phrased as favouring only', () => {
expect(assessmentContextCatalogErrors(RELEASED_ASSESSMENT_CONTEXT_CATALOG)).toEqual([]);
expect(RELEASED_ASSESSMENT_CONTEXT_CATALOG.semantics).toHaveLength(10);
for (const relationship of RELEASED_ASSESSMENT_CONTEXT_CATALOG.relationships) {
expect(relationship.statement.de).toMatch(/begünstig(?:t|en)/);
expect(relationship.statement.de).not.toMatch(/ausnutzbar|bestätigt|kompromittiert/i);
}
});
it('rejects overlapping value sets, missing sources and confirmed wording', () => {
const broken: AssessmentContextCatalog = structuredClone(RELEASED_ASSESSMENT_CONTEXT_CATALOG);
broken.semantics[0].conditions[0].notSatisfiedValues.push(0);
broken.semantics[1].sources = [];
broken.relationships[0].statement = { de: 'Der Angriff ist bestätigt.', en: 'The attack is confirmed.' };
broken.clusters[0].minimumSatisfied = 1;
const errors = assessmentContextCatalogErrors(broken).join(' ');
expect(errors).toMatch(/overlap/);
expect(errors).toMatch(/source is required/);
expect(errors).toMatch(/favouring/);
expect(errors).toMatch(/several conditions/);
expect(() => context([], broken)).toThrow(/catalog is invalid/);
});
});
/** A value that refutes every condition of one setting. */
const hardenedValue = (semantic: AssessmentContextCatalog['semantics'][number]) =>
semantic.conditions[0].notSatisfiedValues.find(value => semantic.conditions.every(item => item.notSatisfiedValues.includes(value)))!;
describe('context engine', () => {
const declared: Declared[] = [
{ id: 'obs-lm', backup: 'backup-a', link: 'linked', target: template(LSA), values: [dword('2')] },
{ id: 'obs-smb-a', backup: 'backup-a', link: 'linked', target: template(SMB_SERVER), values: [dword('0')] },
{ id: 'obs-smb-b', backup: 'backup-b', link: 'linked', target: template(SMB_SERVER.toUpperCase()), values: [dword('1')] },
{ id: 'obs-llmnr', backup: 'backup-c', link: 'not-linked', target: llmnr,
values: [{ kind: 'registry-policy', operation: 'set-value', key: llmnr.kind === 'registry-value' ? llmnr.key : '', valueName: 'EnableMulticast', typeCode: 4, data: { kind: 'uint32', value: 1 } }] },
{ id: 'obs-ldap-server', backup: 'backup-a', target: template(LDAP_SERVER), values: [dword('2')] },
{ id: 'obs-ldap-client', backup: 'backup-a', target: template(LDAP_CLIENT), values: [dword('7')] },
{ id: 'obs-unrelated', backup: 'backup-a', target: template('MACHINE\\Software\\Dummy\\Unrelated'), values: [dword('1')] },
{ id: 'obs-unresolved', backup: 'backup-a', target: template(LSA), values: [dword('0')], resolved: false }
];
it('derives declared facts per GPO backup and aggregates conditions without assuming precedence', () => {
const result = context(declared);
expect(result.engine.version).toBe('1.13.0');
expect(result.coverage).toEqual({ observations: 8, semanticallyMapped: 6, notSemanticallyMapped: 1, unresolvedIdentity: 1,
scannerFindings: 0, scannerMapped: 0, scannerNotInCatalog: 0, scannerGuardRejected: 0, gpoEvaluatedSettings: 0, gpoMappedByVerdict: 0, gpoUncatalogued: 0, scannerMappedBySection: 0, scannerUncatalogued: 0, scannerWithDeclaredReferences: 0, scannerWithCisControlArea: 0,
scannerMappedByDeclaredTechnique: 0, scannerRecognized: 0, scannerEstimated: 0, gpoRecognized: 0, gpoEstimated: 0, uncataloguedWithoutRecognition: 0 });
expect(result.facts.every(fact => fact.assertion === 'declared-in-gpo-backup')).toBe(true);
expect(condition(result, 'ntlm-legacy-responses-sent')).toMatchObject({ state: 'satisfied', conflictingDeclarations: false });
expect(condition(result, 'smb-server-signing-not-required')).toMatchObject({ state: 'satisfied', conflictingDeclarations: true });
expect(condition(result, 'smb-server-signing-not-required').notSatisfiedBy).toHaveLength(1);
expect(condition(result, 'llmnr-not-turned-off')).toMatchObject({ state: 'satisfied', onlyUnlinkedSources: true, declaredVersusObserved: false });
expect(condition(result, 'ldap-server-signing-not-required').state).toBe('not-satisfied');
expect(condition(result, 'ldap-client-signing-none')).toMatchObject({ state: 'unknown' });
expect(condition(result, 'ldap-client-signing-none').unknownBy).toHaveLength(1);
expect(condition(result, 'ldap-channel-binding-not-always')).toMatchObject({ state: 'unknown', satisfiedBy: [], unknownBy: [] });
expect(result.clusters[0]).toMatchObject({ clusterId: 'credential-relay-exposure', state: 'present', satisfied: 4, notSatisfied: 1, unknown: 26 });
expect(JSON.stringify(result)).not.toMatch(/"(?:verdict|pass|fail|severity|priority|rank)"/i);
});
it('offers attach points by Work identity and scope and plain-count levers', () => {
const result = context(declared);
const smb = result.settings.filter(item => item.semanticId === 'windows.smb.server.signing_required');
expect(smb.every(item => item.scope === 'computer' && item.clusterIds.includes('credential-relay-exposure'))).toBe(true);
const lever = result.levers.find(item => item.semanticId === 'windows.smb.server.signing_required')!;
expect(lever).toEqual({ semanticId: 'windows.smb.server.signing_required', settingName: 'Microsoft network server: Digitally sign communications (always)',
satisfiedConditions: 1, clusters: 1, backupInstanceIds: ['backup-a'] });
expect(result.levers.some(item => item.semanticId === 'windows.ldap.server.signing_requirement')).toBe(false);
});
it('keeps deleted, conflicted and unsupported values unknown and distinguishes not-present from undetermined', () => {
const deleted = context([
{ id: 'obs-llmnr-deleted', backup: 'backup-a', target: llmnr,
values: [{ kind: 'registry-policy', operation: 'delete-value', key: 'Software\\Policies\\Microsoft\\Windows NT\\DNSClient', valueName: 'EnableMulticast' }] },
{ id: 'obs-lm-conflicted', backup: 'backup-a', target: template(LSA), values: [dword('0')], state: 'conflicted' }
]);
expect(deleted.facts.map(fact => fact.value.state).sort()).toEqual(['deleted', 'unknown']);
expect(deleted.clusters[0].state).toBe('undetermined');
const hardened = context(RELEASED_ASSESSMENT_CONTEXT_CATALOG.semantics.flatMap(semantic => semantic.targets.map((target, index) => ({
id: `obs-${semantic.id}-${index}`, backup: 'backup-a',
target: target.mechanism === 'security-template' ? template(target.key)
: { kind: 'registry-value' as const, hive: 'HKEY_LOCAL_MACHINE' as const, key: target.key, valueName: target.valueName },
values: target.mechanism === 'security-template' ? [dword(String(hardenedValue(semantic)))]
: [{ kind: 'registry-policy', operation: 'set-value', key: target.key, valueName: target.valueName, typeCode: 4,
data: { kind: 'uint32', value: hardenedValue(semantic) } }]
}))));
// Conditions without a GPO setting (spooler coercion, ESC8, name resolution, hardened paths, SMB, DNS, TLS, report sections) stay unknown.
expect(hardened.clusters[0]).toMatchObject({ state: 'undetermined', satisfied: 0, notSatisfied: 8, unknown: 23 });
expect(hardened.levers).toEqual([]);
const { observations, analyses } = fixture(RELEASED_ASSESSMENT_CONTEXT_CATALOG.semantics.flatMap(semantic => semantic.targets.map((target, index) => ({
id: `obs-h-${semantic.id}-${index}`, backup: 'backup-a',
target: target.mechanism === 'security-template' ? template(target.key)
: { kind: 'registry-value' as const, hive: 'HKEY_LOCAL_MACHINE' as const, key: target.key, valueName: target.valueName },
values: target.mechanism === 'security-template' ? [dword(String(hardenedValue(semantic)))]
: [{ kind: 'registry-policy', operation: 'set-value', key: target.key, valueName: target.valueName, typeCode: 4,
data: { kind: 'uint32', value: hardenedValue(semantic) } }]
}))));
const passing = (tool: string, ruleId: string, title = ruleId) => ({ resultProjectionId: `scanner-finding:${ruleId}`, sourceKind: 'scanner-finding',
sourceResultId: ruleId, sourceTool: tool, sourceRuleId: ruleId, outcomeNormalized: 'pass', title }) as unknown as UnifiedResultProjection;
const fullyHardened = buildAssessmentContext({
projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: '2026-09-30T08:00:00Z', repositoryRevision: 4,
observations, analyses, catalog: RELEASED_ASSESSMENT_CONTEXT_CATALOG,
scannerFindings: [passing('purple-knight', 'SI000077'), passing('purple-knight', 'SI000095'),
passing('cis-cat', '18.6.4.1', "18.6.4.1 Ensure 'Configure multicast DNS (mDNS) protocol' is set to 'Disabled'"),
passing('cis-cat', '18.6.4.2', "18.6.4.2 Ensure 'Configure NetBIOS settings' is set to 'Enabled'"),
passing('cis-cat', '18.11.1', "18.11.1 Ensure 'Disable HTTP proxy features: Disable WPAD' is set to 'Enabled: Checked'"),
passing('pingcastle', 'A-HardenedPaths'), passing('purple-knight', 'SI000085'),
passing('cis-cat', '18.6.14.1', "18.6.14.1 Ensure 'Hardened UNC Paths' is set to 'Enabled'"),
passing('cis-cat', '18.6.8.4', "18.6.8.4 Ensure 'Enable insecure guest logons' is set to 'Disabled'")]
});
// Refuted conditions no longer count; report-section and newly catalogued conditions without evidence stay unknown.
expect(fullyHardened.clusters[0]).toMatchObject({ state: 'undetermined', satisfied: 0, notSatisfied: 17, unknown: 14 });
});
it('is deterministic and never changes its inputs', () => {
const { observations, analyses } = fixture(declared);
const input = {
projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: '2026-09-30T08:00:00Z',
repositoryRevision: 4, observations, analyses, catalog: RELEASED_ASSESSMENT_CONTEXT_CATALOG
};
const before = structuredClone({ observations, analyses });
const deepFreeze = (value: unknown): void => {
if (value && typeof value === 'object' && !Object.isFrozen(value)) { Object.freeze(value); Object.values(value).forEach(deepFreeze); }
};
deepFreeze(observations); deepFreeze(analyses);
expect(buildAssessmentContext(input)).toEqual(buildAssessmentContext(input));
expect({ observations, analyses }).toEqual(before);
});
});
describe('context run', () => {
const port = (revisions: number[], data = fixture([{ id: 'obs-lm', backup: 'backup-a', target: template(LSA), values: [dword('1')] }])): ContextReadPort => ({
currentRevision: async () => revisions.length > 1 ? revisions.shift()! : revisions[0],
queryUnifiedResults: async query => {
const items = query?.sourceKind?.includes('policy-observation') ? data.observations : [];
return { items, total: items.length, offset: 0, limit: 500 };
},
listGpoPolicyAnalyses: async () => data.analyses,
listReferenceRuns: async () => [],
listPolicyEvaluations: async () => [],
getEnvironmentSnapshot: async () => { throw new Error('no CIS findings in scope'); }
});
it('repeats the read after a revision change and records the committed revision', async () => {
const result = await createAssessmentContext(port([6, 7, 7, 7]), { projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: '2026-09-30T08:00:00Z' });
expect(result.repositoryRevision).toBe(7);
expect(result.facts).toHaveLength(1);
});
it('refuses instead of mixing states when the data keeps changing', async () => {
let revision = 0;
const changing = { ...port([0]), currentRevision: async () => revision++ };
await expect(createAssessmentContext(changing, { projectId: 'dummy-project', assessmentIds: ['a'], generatedAt: '2026-09-30T08:00:00Z' }))
.rejects.toBeInstanceOf(ContextStateChangedError);
});
});
describe('context engine scanner observations', () => {
const finding = (tool: string, ruleId: string, outcome: string, occurrence: string, title: string) => ({
resultProjectionId: `scanner-finding:${occurrence}`, sourceKind: 'scanner-finding', sourceResultId: occurrence,
sourceTool: tool, sourceRuleId: ruleId, outcomeNormalized: outcome, title, sourceObservedAt: '2026-09-01T00:00:00Z'
}) as unknown as UnifiedResultProjection;
const scannerFindings = [
finding('purple-knight', 'SI000154', 'fail', 'occ-pk', 'SMB Signing is not required on Domain Controllers'),
finding('cis-cat', '2.3.9.2', 'pass', 'occ-cis-server', "2.3.9.2 Ensure 'Microsoft network server: Digitally sign communications (always)' is set to 'Enabled'"),
finding('cis-cat', '2.3.8.1', 'fail', 'occ-cis-renumbered', "2.3.8.1 Ensure 'Some other setting' is set to 'Enabled'"),
finding('cis-cat', '9.9.9', 'fail', 'occ-cis-lookalike', "9.9.9 Ensure 'Microsoft network client: Digitally sign communications (always)' is set to 'Enabled'"),
finding('pingcastle', 'S-OldNtlm', 'fail', 'occ-pc', 'The LAN Manager Authentication Level allows the use of NTLMv1 or LM.'),
finding('cis-cat', '18.6.4.4', 'not_run', 'occ-cis-llmnr', "18.6.4.4 Ensure 'Turn off multicast name resolution' is set to 'Enabled'"),
finding('maester', 'MT.0001', 'fail', 'occ-maester', 'Unrelated cloud check')
];
function withScanners(declared: Declared[]) {
const { observations, analyses } = fixture(declared);
return buildAssessmentContext({
projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: '2026-09-30T08:00:00Z',
repositoryRevision: 4, observations, analyses, scannerFindings, catalog: RELEASED_ASSESSMENT_CONTEXT_CATALOG
});
}
it('maps exact rule IDs, guards renumbered benchmarks, keeps every deviation and leaves not-run checks unknown', () => {
const result = withScanners([]);
// User decision 2026-09-30: every fail is a finding. A renumbered or uncatalogued deviation is never dropped and
// never linked to the wrong setting's condition. User decision 2026-10-01: it receives a recognized or estimated
// cluster when its text carries a usable signal; without one it stays an uncatalogued deviation.
expect(result.coverage).toMatchObject({ scannerFindings: 7, scannerMapped: 6, scannerNotInCatalog: 0, scannerGuardRejected: 1, scannerUncatalogued: 2, scannerRecognized: 1 });
expect(result.observations.filter(item => item.matchedBy === 'rule-id').map(item => item.occurrenceId).sort()).toEqual(['occ-cis-llmnr', 'occ-cis-server', 'occ-pc', 'occ-pk']);
expect(result.observations.filter(item => item.matchedBy === 'uncatalogued-deviation').map(item => `${item.occurrenceId}:${item.conditionId}`).sort()).toEqual([
'occ-cis-renumbered:uncatalogued-deviations-cis-cat', 'occ-maester:uncatalogued-deviations-maester']);
// The look-alike reaches the cluster of its terms, not the condition of the setting it only resembles.
expect(result.recognitions.map(item => [item.occurrenceId, item.clusterId, item.confidence, item.method])).toEqual([
['occ-cis-lookalike', 'credential-relay-exposure', 'recognized', 'title-term']]);
expect(result.findings.find(item => item.occurrenceId === 'occ-cis-lookalike')).toMatchObject({
conditionIds: [], clusterIds: ['credential-relay-exposure'], clusterConfidence: { 'credential-relay-exposure': 'recognized' } });
expect(result.observations.every(item => item.assertion === 'observed-by-scanner')).toBe(true);
expect(condition(result, 'llmnr-not-turned-off')).toMatchObject({ state: 'unknown', satisfiedBy: [], notSatisfiedBy: [] });
expect(condition(result, 'ntlm-legacy-responses-sent').state).toBe('satisfied');
expect(condition(result, 'smb-client-signing-not-required').state).toBe('unknown');
});
it('combines declared and observed evidence and flags when they disagree', () => {
const result = withScanners([{ id: 'obs-smb', backup: 'backup-a', link: 'linked', target: template(SMB_SERVER), values: [dword('1')] }]);
const smb = condition(result, 'smb-server-signing-not-required');
expect(smb).toMatchObject({ state: 'satisfied', declaredVersusObserved: true, conflictingDeclarations: false, onlyUnlinkedSources: false });
expect(smb.satisfiedBy).toEqual(['context-observation:scanner-finding:occ-pk:smb-server-signing-not-required']);
expect(smb.notSatisfiedBy).toHaveLength(2);
expect(result.clusters[0]).toMatchObject({ state: 'present', satisfied: 2 });
const attachment = result.findings.find(item => item.occurrenceId === 'occ-pk')!;
expect(attachment).toMatchObject({ tool: 'purple-knight', sourceRuleId: 'SI000154', conditionIds: ['smb-server-signing-not-required'], clusterIds: ['credential-relay-exposure'] });
expect(result.levers.find(item => item.semanticId === 'windows.smb.server.signing_required')?.backupInstanceIds).toEqual([]);
});
it('restricts only scanner findings to the timeslot scan runs', async () => {
const queries: Array<{ sourceKind?: string[]; scanRunIds?: string[] }> = [];
const port: ContextReadPort = {
currentRevision: async () => 1,
queryUnifiedResults: async query => { queries.push(query ?? {}); return { items: [], total: 0, offset: 0, limit: 500 }; },
listGpoPolicyAnalyses: async () => [],
listReferenceRuns: async () => [],
listPolicyEvaluations: async () => [],
getEnvironmentSnapshot: async () => { throw new Error('no CIS findings in scope'); }
};
const result = await createAssessmentContext(port, { projectId: 'dummy-project', assessmentIds: ['a'], scanRunIds: ['run-1'], generatedAt: '2026-09-30T08:00:00Z' });
expect(queries.find(query => query.sourceKind?.includes('policy-observation'))?.scanRunIds).toBeUndefined();
expect(queries.find(query => query.sourceKind?.includes('scanner-finding'))?.scanRunIds).toEqual(['run-1']);
expect(result.scanRunIds).toEqual(['run-1']);
});
it('rejects scanner rules without condition or text and with an invalid optional link', () => {
const broken: AssessmentContextCatalog = structuredClone(RELEASED_ASSESSMENT_CONTEXT_CATALOG);
broken.scannerRules.push({ ...broken.scannerRules[0] });
broken.scannerRules[1] = { ...broken.scannerRules[1], conditionId: 'missing', ruleText: ' ', sources: [{ title: 'Broken', url: 'not-a-link', publisher: 'CIS' }] };
const errors = assessmentContextCatalogErrors(broken).join(' ');
expect(errors).toMatch(/duplicate scanner rule/);
expect(errors).toMatch(/unknown condition/);
expect(errors).toMatch(/native rule text/);
});
});
describe('context catalog clusters', () => {
it('gives every cluster condition at least one evidence source and accepts scanner rules from the report alone', () => {
const catalog = RELEASED_ASSESSMENT_CONTEXT_CATALOG;
expect(catalog.clusters.map(cluster => cluster.id)).toEqual([
'credential-relay-exposure', 'cloud-privileged-access-exposure', 'cloud-sign-in-protection-gaps', 'ad-privileged-account-exposure',
'kerberos-delegation-abuse', 'adcs-escalation', 'kerberoasting-exposure',
'lateral-movement-exposure', 'host-credential-privilege-exposure', 'detection-blind-spots', 'password-guessing-exposure',
'malicious-code-execution-exposure', 'ad-persistence-indicators', 'ad-reconnaissance-exposure', 'ad-recovery-gaps',
'cloud-app-consent-exposure', 'cloud-external-access-exposure', 'data-sharing-exposure', 'physical-local-access-exposure',
'patch-configuration-drift', 'email-spoofing-phishing-exposure', 'other-deviations'
]);
const semanticConditions = new Set(catalog.semantics.flatMap(semantic => semantic.conditions.map(condition => condition.id)));
const scannerConditions = new Set([...catalog.scannerRules.map(rule => rule.conditionId), ...catalog.cisSectionRules.map(rule => rule.conditionId),
...catalog.gpoVerdictRules.map(rule => rule.conditionId), ...Object.values(catalog.uncataloguedDeviationConditions)]);
// Catalogue 1.9.0: the email conditions are reached through automatic recognition until a scan report supplies the
// exact rule texts of matching scanner rules (the report is authoritative; texts are never written from memory).
const recognitionOnly = new Set(['email-sender-authentication-incomplete', 'email-threat-protection-weak', 'email-external-forwarding-allowed']);
const orphaned = catalog.clusters.flatMap(cluster => cluster.conditionIds)
.filter(id => !semanticConditions.has(id) && !scannerConditions.has(id) && !recognitionOnly.has(id));
expect(orphaned).toEqual([]);
// ATT&CK IDs only where MITRE documents the technique; delegation abuse has none and is backed by Microsoft documentation.
expect(catalog.relationships.flatMap(item => item.techniqueIds).every(id => /^T\d{4}(?:\.\d{3})?$/.test(id))).toBe(true);
// User decision 2026-09-30: the scan report is authoritative; a rule needs no documentation source.
const reportOnly: AssessmentContextCatalog = structuredClone(catalog);
reportOnly.scannerRules[0].sources = [];
expect(assessmentContextCatalogErrors(reportOnly)).toEqual([]);
});
it('evaluates scanner-only clusters from exact rule IDs without a GPO setting behind them', () => {
const finding = (tool: string, ruleId: string, outcome: string, occurrence: string) => ({
resultProjectionId: `scanner-finding:${occurrence}`, sourceKind: 'scanner-finding', sourceResultId: occurrence,
sourceTool: tool, sourceRuleId: ruleId, outcomeNormalized: outcome, title: `Synthetic ${ruleId}`
}) as unknown as UnifiedResultProjection;
const result = buildAssessmentContext({
projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: '2026-09-30T08:00:00Z', repositoryRevision: 1,
observations: [], analyses: [], catalog: RELEASED_ASSESSMENT_CONTEXT_CATALOG,
scannerFindings: [
finding('maester', 'CISA.MS.AAD.7.4', 'fail', 'occ-74'),
finding('maester', 'MT.1025', 'fail', 'occ-1025'),
finding('maester', 'CIS.M365.1.1.1', 'pass', 'occ-cloud-only'),
finding('maester', 'MT.1032', 'fail', 'occ-ga-count'),
finding('pingcastle', 'P-Delegated', 'fail', 'occ-delegated'),
finding('purple-knight', 'SI000019', 'pass', 'occ-spn')
]
});
const cluster = (id: string) => result.clusters.find(item => item.clusterId === id)!;
expect(cluster('cloud-privileged-access-exposure')).toMatchObject({ state: 'present', satisfied: 3, notSatisfied: 1 });
expect(cluster('ad-privileged-account-exposure')).toMatchObject({ state: 'undetermined', satisfied: 1, notSatisfied: 1, unknown: 11 });
expect(cluster('cloud-sign-in-protection-gaps')).toMatchObject({ state: 'undetermined', satisfied: 0, unknown: 19 });
const permanent = cluster('cloud-privileged-access-exposure').conditions.find(item => item.conditionId === 'entra-permanent-privileged-assignments')!;
expect(permanent.semanticId).toBeUndefined();
expect(permanent.relationships[0].techniqueIds).toEqual(['T1078.004']);
// User decision 2026-09-30: every fail is a finding. The Global Administrator count is worded as stated: outside the recommendation.
expect(result.findings.find(item => item.occurrenceId === 'occ-ga-count')).toMatchObject({
conditionIds: ['entra-global-admin-count-outside-range'], clusterIds: ['cloud-privileged-access-exposure'] });
// The catalogue lists delegatable admins in two clusters; the finding still counts only once.
const delegated = result.findings.find(item => item.occurrenceId === 'occ-delegated');
// One finding, one cluster (user decision 2026-10-04): the declared primary cluster counts, the other stays traceable.
expect(delegated?.clusterIds).toEqual(['kerberos-delegation-abuse']);
expect(delegated?.secondaryClusterIds).toEqual(['ad-privileged-account-exposure']);
// Permanent acceptance (user decision 2026-10-04): heatmap cells never count a finding twice.
const cells = contextHeatmap(result);
expect(cells.reduce((sum, cell) => sum + cell.scannerDeviations + cell.gpoDeviations, 0)).toBe(contextHeatmapFindingCount(result));
expect(cells.find(cell => cell.cluster.clusterId === 'ad-privileged-account-exposure')?.scannerDeviations).toBe(0);
expect(cells.find(cell => cell.cluster.clusterId === 'kerberos-delegation-abuse')?.scannerDeviations).toBe(1);
});
it('rejects a condition shared by several clusters without a declared primary cluster', () => {
const shared: AssessmentContextCatalog = structuredClone(RELEASED_ASSESSMENT_CONTEXT_CATALOG);
delete shared.primaryClusters!['ad-admins-delegatable'];
expect(assessmentContextCatalogErrors(shared)).toEqual([
'ad-admins-delegatable: shared by ad-privileged-account-exposure, kerberos-delegation-abuse without a primary cluster.']);
shared.primaryClusters!['ad-admins-delegatable'] = 'adcs-escalation';
expect(assessmentContextCatalogErrors(shared)).toEqual(['ad-admins-delegatable: primary cluster adcs-escalation does not list the condition.']);
});
});
describe('context engine declared ATT&CK references', () => {
const finding = (occurrence: string, tool: string, outcome: string) => ({
resultProjectionId: `scanner-finding:${occurrence}`, sourceKind: 'scanner-finding', sourceResultId: occurrence,
sourceTool: tool, sourceRuleId: `RULE-${occurrence}`, outcomeNormalized: outcome, title: occurrence
}) as unknown as UnifiedResultProjection;
const reference = (id: string, evidence: 'declared' | 'mentioned', status = 'active', framework = 'mitre-attack') => ({
framework, id, title: `Title ${id}`, kind: id.startsWith('TA') ? 'tactic' : 'technique', tactics: [], status, evidence,
matchedBy: 'id', catalogVersion: '19.2', catalogSha256: 'x', source: {}
});
const run = (id: string, scanRunId: string, createdAt: string, results: Array<[string, ReturnType<typeof reference>[]]>) => ({
schemaVersion: 1, id, projectId: 'dummy-project', assessmentId: 'dummy-assessment', scanRunId, recognizerVersion: '1.0.0',
inputSha256: 'x', catalogHashes: [], createdAt,
results: results.map(([occurrenceId, references]) => ({ occurrenceId, importFileId: 'f', sourcePosition: {}, references }))
});
it('groups only references the scanner declares, from the latest run of each scan, within the timeslot', () => {
const result = buildAssessmentContext({
projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: '2026-09-30T08:00:00Z', repositoryRevision: 1,
observations: [], analyses: [], catalog: RELEASED_ASSESSMENT_CONTEXT_CATALOG,
scannerFindings: [finding('occ-a', 'purple-knight', 'fail'), finding('occ-b', 'pingcastle', 'fail'), finding('occ-c', 'maester', 'pass')],
referenceRuns: [
run('old', 'run-pk', '2026-09-01T00:00:00Z', [['occ-a', [reference('TA0008', 'declared')]]]),
run('new', 'run-pk', '2026-09-20T00:00:00Z', [['occ-a', [reference('TA0006', 'declared'), reference('TA0006', 'declared'), reference('T1003', 'mentioned')]]]),
run('pc', 'run-pc', '2026-09-20T00:00:00Z', [['occ-b', [reference('TA0006', 'declared'), reference('T9999', 'declared', 'not-in-catalog'), reference('D3-AL', 'declared', 'active', 'mitre-d3fend')]]]),
run('ma', 'run-ma', '2026-09-20T00:00:00Z', [['occ-c', [reference('TA0006', 'declared')]], ['occ-outside', [reference('TA0006', 'declared')]]])
] as never
}) as ReturnType<typeof buildAssessmentContext>;
// D3FEND countermeasures the scanner declares group separately from ATT&CK (user decision 2026-09-30).
expect(result.declaredReferences.map(group => `${group.framework}:${group.id}`)).toEqual(['mitre-attack:TA0006', 'mitre-d3fend:D3-AL']);
expect(result.declaredReferences[0].findings.map(item => [item.occurrenceId, item.tool, item.outcome]))
.toEqual([['occ-a', 'purple-knight', 'fail'], ['occ-b', 'pingcastle', 'fail'], ['occ-c', 'maester', 'pass']]);
expect(result.findings.find(item => item.occurrenceId === 'occ-a')).toMatchObject({ declaredReferenceIds: ['TA0006'], clusterIds: ['other-deviations'] });
expect(result.coverage.scannerWithDeclaredReferences).toBe(3);
});
});
describe('context engine CIS control areas', () => {
const cis = (occurrence: string, ruleId: string, outcome: string, scanRunId = 'run-server', tool = 'cis-cat') => ({
resultProjectionId: `scanner-finding:${occurrence}`, sourceKind: 'scanner-finding', sourceResultId: occurrence, scanRunId,
sourceTool: tool, sourceRuleId: ruleId, outcomeNormalized: outcome, title: `${ruleId} Ensure synthetic setting`
}) as unknown as UnifiedResultProjection;
const scores = (scanRunId: string, rows: string[][]) => ({
scanRunId, sectionKey: 'cis-cat-section-scores',
tables: [{ name: 'Section scores', headers: ['Description', 'Pass', 'Fail', 'Error', 'Score'], rows }]
});
const build = (scannerFindings: UnifiedResultProjection[], cisReportSections: ReturnType<typeof scores>[]) => buildAssessmentContext({
projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: '2026-09-30T08:00:00Z', repositoryRevision: 1,
observations: [], analyses: [], catalog: RELEASED_ASSESSMENT_CONTEXT_CATALOG, scannerFindings, cisReportSections
});
it('groups CIS findings by the exact number hierarchy of their own scan report, with its titles and stated counts', () => {
const result = build([
cis('occ-1', '9.1.1', 'fail'), cis('occ-2', '9.1.2', 'fail'), cis('occ-3', '9.2.1', 'pass'), cis('occ-4', '9.1.3', 'not_run'),
cis('occ-5', '9.1.1', 'pass', 'run-client'), cis('occ-6', 'SI000001', 'fail', 'run-server', 'purple-knight'), cis('occ-7', 'no-number', 'fail')
], [
scores('run-server', [['9 Windows Firewall', '1', '2', '0', '33%'], ['9.1 Domain Profile', '0', '7', '0', '0%'],
['9.2 Private Profile', 'x', '1', '0', '?'], ['9.1.1 Ensure synthetic setting', '0', '1', '0', '0%']]),
scores('run-client', [['9.1 Firewall: Domain', '3', '0', '0', '100%']])
]);
expect(result.engine.version).toBe('1.13.0');
const area = (id: string) => result.cisControlAreas.find(item => item.id === id);
expect(area('cis:run-server:9.1')).toEqual({ id: 'cis:run-server:9.1', scanRunId: 'run-server', sectionId: '9.1', parentSectionId: '9',
title: 'Domain Profile', findings: 3, fail: 2, pass: 0, other: 1, reported: { pass: 0, fail: 7 } });
expect(area('cis:run-server:9')).toMatchObject({ title: 'Windows Firewall', findings: 4, fail: 2, pass: 1, other: 1, reported: { pass: 1, fail: 2 } });
// A row whose stated counts are not whole numbers keeps its title but gets no counts.
expect(area('cis:run-server:9.2')).toMatchObject({ title: 'Private Profile', findings: 1 });
expect(area('cis:run-server:9.2')?.reported).toBeUndefined();
// Another benchmark reuses the numbering: never merged across scan runs.
expect(area('cis:run-client:9.1')).toMatchObject({ title: 'Firewall: Domain', findings: 1, pass: 1 });
expect(area('cis:run-client:9')?.title).toBeUndefined();
expect(result.findings.find(item => item.occurrenceId === 'occ-1')?.cisControlAreaIds).toEqual(['cis:run-server:9', 'cis:run-server:9.1']);
// Findings without a CIS number get no control area; as deviations they still reach the uncatalogued cluster.
expect(result.findings.filter(item => item.occurrenceId === 'occ-6' || item.occurrenceId === 'occ-7').map(item => item.cisControlAreaIds)).toEqual([[], []]);
expect(result.coverage.scannerWithCisControlArea).toBe(5);
});
it('assigns every CIS deviation through the most specific section title of its own report, else keeps it visible', () => {
const result = build([
cis('occ-print', '18.7.3', 'fail'), cis('occ-print-pass', '18.7.4', 'pass'), cis('occ-audit', '17.6.2', 'fail'),
cis('occ-autoplay', '18.10.8.1', 'fail'), cis('occ-unknown-section', '42.1.1', 'fail'), cis('occ-unknown-pass', '42.1.2', 'pass'),
{ ...cis('occ-exact', '1.1.4', 'fail'), title: "1.1.4 Ensure 'Minimum password length' is set to '14 or more character(s)'" } as UnifiedResultProjection
], [scores('run-server', [['18 Administrative Templates (Computer)', '0', '1', '0', '0%'], ['18.7 Printers', '1', '1', '0', '50%'],
['17 Advanced Audit Policy Configuration', '0', '1', '0', '0%'], ['17.6 Object Access', '0', '1', '0', '0%'],
['18.10 Windows Components', '0', '1', '0', '0%'], ['18.10.8 AutoPlay Policies', '0', '1', '0', '0%'], ['42 Unknown Area', '0', '1', '0', '0%']])]);
const observed = (occurrence: string) => result.observations.filter(item => item.occurrenceId === occurrence)
.map(item => `${item.matchedBy}:${item.conditionId}${item.sectionTitle ? `@${item.sectionTitle}` : ''}`);
expect(observed('occ-print')).toEqual(['report-section:cis-print-hardening-unmet@Printers']);
expect(observed('occ-print-pass')).toEqual(['report-section:cis-print-hardening-unmet@Printers']);
// The parent 'Advanced Audit Policy Configuration' decides when 'Object Access' has no own rule.
expect(observed('occ-audit')).toEqual(['report-section:cis-audit-policy-unmet@Advanced Audit Policy Configuration']);
expect(observed('occ-autoplay')).toEqual(['report-section:autorun-enabled@AutoPlay Policies']);
expect(observed('occ-unknown-section')).toEqual(['uncatalogued-deviation:uncatalogued-deviations-cis-cat']);
expect(observed('occ-unknown-pass')).toEqual([]);
// An exact rule ID keeps precedence over its section (the rule text in the finding title is the guard).
expect(result.observations.find(item => item.occurrenceId === 'occ-exact')?.matchedBy).toBe('rule-id');
const print = result.clusters.flatMap(item => item.conditions).find(item => item.conditionId === 'cis-print-hardening-unmet')!;
expect(print).toMatchObject({ state: 'satisfied', satisfiedBy: ['context-observation:scanner-finding:occ-print:cis-print-hardening-unmet'] });
const printFinding = result.findings.find(item => item.occurrenceId === 'occ-print');
expect(printFinding?.clusterIds).toEqual(['host-credential-privilege-exposure']);
expect(printFinding?.secondaryClusterIds).toEqual(['credential-relay-exposure']);
expect(result.coverage).toMatchObject({ scannerMappedBySection: 4, scannerUncatalogued: 1 });
});
it('leaves out a title or count the scan report states ambiguously', () => {
const result = build([cis('occ-1', '2.3.1', 'fail')], [
scores('run-server', [['2.3 Security Options', '1', '1', '0', '50%'], ['2.3 Other Title', '2', '1', '0', '66%']])
]);
const area = result.cisControlAreas.find(item => item.id === 'cis:run-server:2.3');
expect(area?.title).toBeUndefined();
expect(area?.reported).toBeUndefined();
expect(area?.findings).toBe(1);
});
it('reads the section scores only for the CIS scan runs in scope', async () => {
const snapshots: unknown[] = [];
const port: ContextReadPort = {
currentRevision: async () => 1,
queryUnifiedResults: async query => {
const items = query?.sourceKind?.includes('scanner-finding') ? [cis('occ-1', '9.1.1', 'fail'), cis('occ-2', 'SI1', 'fail', 'run-pk', 'purple-knight')] : [];
return { items, total: items.length, offset: 0, limit: 500 };
},
listGpoPolicyAnalyses: async () => [],
listReferenceRuns: async () => [],
listPolicyEvaluations: async () => [],
getEnvironmentSnapshot: async query => {
snapshots.push(query);
return { version: 1, projectId: query.projectId, scanRunIds: query.scanRunIds ?? [], generatedAt: '2026-09-30T08:00:00Z',
sections: [{ ...scores('run-server', [['9.1 Domain Profile', '0', '1', '0', '0%']]) }, { scanRunId: 'run-server', sectionKey: 'other', tables: [] }],
observations: [] } as never;
}
};
const result = await createAssessmentContext(port, { projectId: 'dummy-project', assessmentIds: ['a'], generatedAt: '2026-09-30T08:00:00Z' });
expect(snapshots).toEqual([{ projectId: 'dummy-project', scanRunIds: ['run-server'] }]);
expect(result.cisControlAreas.find(item => item.sectionId === '9.1')?.title).toBe('Domain Profile');
});
});
describe('context password guessing exposure', () => {
const account = (key: string): Target => ({ kind: 'security-template', section: 'System Access', key });
const scanner = (tool: string, ruleId: string, outcome: string, title = ruleId) => ({
resultProjectionId: `scanner-finding:${tool}-${ruleId}`, sourceKind: 'scanner-finding', sourceResultId: `${tool}-${ruleId}`,
sourceTool: tool, sourceRuleId: ruleId, outcomeNormalized: outcome, title
}) as unknown as UnifiedResultProjection;
it('counts the ASSESSOR deviation of a value-semantic setting in the heatmap, as Work and Client Info count it', () => {
// Synthetic regression (Dummy 2026-10-05): MinimumPasswordLength had a declared GPO value and a Fail verdict,
// was assigned to its cluster, but the heatmap total omitted it (474 instead of 477).
const { observations, analyses } = fixture([
{ id: 'obs-len', backup: 'backup-default-domain', target: account('MinimumPasswordLength'), values: [{ kind: 'scalar', raw: '7' }] }
]);
const identity = observations[0].policyIdentityKey!;
const verdict = (outcome: string, isDeviation: boolean) => ({
resultProjectionId: `policy-evaluation:${outcome}`, sourceKind: 'policy-evaluation', sourceResultId: `evaluation-${outcome}`,
policyScope: 'computer', policyIdentityKey: identity, outcomeNormalized: outcome, isDeviation, title: 'MinimumPasswordLength'
}) as unknown as UnifiedResultProjection;
const build = (evaluations: UnifiedResultProjection[]) => buildAssessmentContext({
projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: '2026-09-30T08:00:00Z', repositoryRevision: 1,
observations, analyses, evaluations, catalog: RELEASED_ASSESSMENT_CONTEXT_CATALOG
});
const failed = build([verdict('fail', true)]);
expect(failed.declaredDeviations).toEqual([{ scope: 'computer', identityKey: identity, resultProjectionIds: ['policy-evaluation:fail'] }]);
expect(contextHeatmapFindingCount(failed)).toBe(1);
const cell = contextHeatmap(failed).find(item => item.cluster.clusterId === 'password-guessing-exposure')!;
expect(cell.gpoDeviations).toBe(1);
expect(contextHeatmap(failed).reduce((sum, item) => sum + item.gpoDeviations, 0)).toBe(1);
// A passed verdict is no deviation.
const passed = build([verdict('pass', false)]);
expect(passed.declaredDeviations).toEqual([]);
expect(contextHeatmapFindingCount(passed)).toBe(0);
});
it('reads exact account policy integers from GPO backups and links the password findings of every scanner', () => {
const { observations, analyses } = fixture([
{ id: 'obs-len', backup: 'backup-default-domain', target: account('MinimumPasswordLength'), values: [{ kind: 'scalar', raw: '7' }] },
{ id: 'obs-lockout', backup: 'backup-default-domain', target: account('LockoutBadCount'), values: [{ kind: 'scalar', raw: '0' }] },
{ id: 'obs-complexity', backup: 'backup-default-domain', target: account('PasswordComplexity'), values: [{ kind: 'scalar', raw: 'enabled' }] }
]);
const result = buildAssessmentContext({
projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: '2026-09-30T08:00:00Z', repositoryRevision: 1,
observations, analyses, catalog: RELEASED_ASSESSMENT_CONTEXT_CATALOG,
scannerFindings: [
scanner('purple-knight', 'SI000096', 'fail'), scanner('pingcastle', 'A-MinPwdLen', 'fail'),
scanner('cis-cat', '1.1.4', 'pass', "1.1.4 Ensure 'Minimum password length' is set to '14 or more character(s)'"),
scanner('cis-cat', '1.2.1', 'not_run', "1.2.1 Ensure 'Account lockout duration' is set to '15 or more minute(s)'"),
scanner('maester', 'EIDSCA.PR06', 'fail')
]
});
const cluster = result.clusters.find(item => item.clusterId === 'password-guessing-exposure')!;
const state = (id: string) => cluster.conditions.find(item => item.conditionId === id)!;
expect(cluster.state).toBe('present');
// A declared length of 7 meets both length conditions; CIS-CAT passing 14+ disagrees with the declaration.
expect(state('password-length-below-8')).toMatchObject({ state: 'satisfied', declaredVersusObserved: false });
expect(state('password-length-below-14')).toMatchObject({ state: 'satisfied', declaredVersusObserved: true });
expect(state('account-lockout-threshold-weak').state).toBe('satisfied');
// A non-integer instruction and a not-run check stay unknown.
expect(state('password-complexity-off').state).toBe('unknown');
expect(state('account-lockout-duration-short').state).toBe('unknown');
expect(state('ad-privileged-weak-password-policy').state).toBe('satisfied');
expect(result.facts.find(fact => fact.observationIds.includes('obs-complexity'))?.value).toEqual({ state: 'unknown', reason: 'unsupported-value' });
// The Entra smart lockout belongs to the cloud sign-in path, not to the AD password path.
expect(result.findings.find(item => item.occurrenceId === 'maester-EIDSCA.PR06')?.clusterIds).toEqual(['cloud-sign-in-protection-gaps']);
expect(result.settings.map(item => item.settingName).sort()).toEqual(['Account lockout threshold', 'Minimum password length', 'Password must meet complexity requirements']);
});
});
describe('context GPO verdicts', () => {
const evaluation = (id: string, identity: string, outcome: string, isDeviation: boolean, title: string) => ({
resultProjectionId: `policy-evaluation:${id}`, sourceKind: 'policy-evaluation', sourceResultId: id, policyScope: 'computer',
policyIdentityKey: identity, outcomeNormalized: outcome, isDeviation, title
}) as unknown as UnifiedResultProjection;
const observation = (id: string, identity: string) => ({ resultProjectionId: `policy-observation:${id}`, sourceKind: 'policy-observation', sourceResultId: id,
policyScope: 'computer', policyState: 'configured', policyIdentityStatus: 'resolved', policyIdentityKey: identity, policyValues: [], policyBackupInstanceId: 'backup-a' }) as unknown as UnifiedResultProjection;
const resolution = (id: string, identity: string, target: Record<string, string>) => ({ observationId: id, resolution: { status: 'resolved', identity: { id: identity, target } } });
it('uses the authoritative rolled-up GPO verdict, assigns the most specific rule and keeps every other deviation visible', () => {
const targets: Array<[string, Record<string, string>]> = [
['id-backup', { kind: 'security-template', section: 'Privilege Rights', key: 'SeBackupPrivilege' }],
['id-fw-log', { kind: 'registry-key', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Microsoft\\WindowsFirewall\\DomainProfile\\Logging' }],
// A subkey of SrpV2 matches the SrpV2 rule; the firewall Logging subkey wins over its profile key.
['id-srp', { kind: 'registry-key', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Microsoft\\Windows\\SrpV2\\Script\\rule-1' }],
['id-other', { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Synthetic', valueName: 'Value' }],
['id-passed', { kind: 'security-template', section: 'System Access', key: 'LockoutDuration' }]
];
const result = buildAssessmentContext({
projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: '2026-09-30T08:00:00Z', repositoryRevision: 1,
observations: targets.map(([identity]) => observation(`obs-${identity}`, identity)),
analyses: [{ run: { id: 'a' }, resolutions: targets.map(([identity, target]) => resolution(`obs-${identity}`, identity, target)) }] as never,
evaluations: [
// One failing member makes the whole setting a deviation, as ASSESSOR rolls it up.
evaluation('e1', 'id-backup', 'pass', false, 'Privilege Rights · SeBackupPrivilege'), evaluation('e2', 'id-backup', 'fail', true, 'Privilege Rights · SeBackupPrivilege'),
evaluation('e3', 'id-fw-log', 'fail', true, 'DomainProfile Logging'), evaluation('e4', 'id-srp', 'fail', true, 'SrpV2 Script'),
evaluation('e5', 'id-other', 'fail', true, 'Synthetic value'), evaluation('e6', 'id-passed', 'pass', false, 'System Access · LockoutDuration'),
evaluation('e7', 'id-not-configured', 'not_evaluated', false, 'Not configured')
],
catalog: RELEASED_ASSESSMENT_CONTEXT_CATALOG
});
expect(result.evaluations.map(item => `${item.identityKey}:${item.verdict}:${item.conditionId}`)).toEqual([
'id-backup:deviation:gpo-privileged-rights-deviate',
'id-fw-log:deviation:gpo-firewall-logging-deviates',
'id-other:deviation:uncatalogued-deviations-gpo',
'id-passed:passed:gpo-lockout-timing-deviates',
'id-srp:deviation:gpo-application-control-deviates'
]);
expect(result.coverage).toMatchObject({ gpoEvaluatedSettings: 6, gpoMappedByVerdict: 4, gpoUncatalogued: 1 });
const setting = (identity: string) => result.settings.find(item => item.identityKey === identity);
expect(setting('id-backup')).toMatchObject({ clusterIds: ['host-credential-privilege-exposure'], evaluationIds: ['context-evaluation:computer:id-backup'] });
expect(setting('id-other')?.clusterIds).toEqual(['other-deviations']);
expect(setting('id-backup')?.semanticId).toBeUndefined();
const conditions = result.clusters.flatMap(item => item.conditions);
expect(conditions.find(item => item.conditionId === 'gpo-lockout-timing-deviates')?.state).toBe('not-satisfied');
expect(conditions.find(item => item.conditionId === 'gpo-privileged-rights-deviate')?.state).toBe('satisfied');
});
it('catalogues convenience PIN sign-in and the redirected default OU by their exact identities (catalog 1.11.0)', () => {
const pinTarget = { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE', key: 'SOFTWARE\\Policies\\Microsoft\\Windows\\System', valueName: 'AllowDomainPINLogon' };
const siblingTarget = { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE', key: 'SOFTWARE\\Policies\\Microsoft\\Windows\\System', valueName: 'SyntheticSibling' };
const result = buildAssessmentContext({
projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: '2026-10-13T08:00:00Z', repositoryRevision: 1,
observations: [observation('obs-pin', 'id-pin'), observation('obs-sibling', 'id-sibling')],
analyses: [{ run: { id: 'a' }, resolutions: [resolution('obs-pin', 'id-pin', pinTarget), resolution('obs-sibling', 'id-sibling', siblingTarget)] }] as never,
evaluations: [evaluation('e-pin', 'id-pin', 'fail', true, 'AllowDomainPINLogon'), evaluation('e-sibling', 'id-sibling', 'fail', true, 'SyntheticSibling')],
scannerFindings: [{
resultProjectionId: 'scanner-finding:occ-ou', sourceKind: 'scanner-finding', sourceResultId: 'occ-ou', sourceTool: 'pingcastle',
sourceRuleId: 'S-DefaultOUChanged', outcomeNormalized: 'fail', title: 'Check if default OU location has been changed within the domain.',
sourceObservedAt: '2026-10-01T00:00:00Z'
}] as unknown as UnifiedResultProjection[],
catalog: RELEASED_ASSESSMENT_CONTEXT_CATALOG
});
// Only the exact value name is catalogued; a sibling value under the same key stays an uncatalogued deviation.
expect(result.evaluations.map(item => `${item.identityKey}:${item.conditionId}`)).toEqual([
'id-pin:gpo-convenience-pin-sign-in-deviates', 'id-sibling:uncatalogued-deviations-gpo']);
expect(result.settings.find(item => item.identityKey === 'id-pin')?.clusterIds).toEqual(['physical-local-access-exposure']);
expect(result.observations.find(item => item.occurrenceId === 'occ-ou')).toMatchObject({ matchedBy: 'rule-id', conditionId: 'ad-default-container-redirected' });
expect(result.findings.find(item => item.occurrenceId === 'occ-ou')?.clusterIds).toEqual(['detection-blind-spots']);
});
it('places the catalog 1.12.0 rules of every kind by exact identity, with trusts in recovery', () => {
const targets: Array<[string, Record<string, string>]> = [
['id-rdp', { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Microsoft\\Windows NT\\Terminal Services', valueName: 'fAllowFullControl' }],
['id-anon', { kind: 'security-template', section: 'System Access', key: 'LSAAnonymousNameLookup' }],
['id-lmhash', { kind: 'security-template', section: 'Registry Values', key: 'MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\NoLMHash' }],
['id-printers', { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Microsoft\\Windows NT\\Printers\\PointAndPrint', valueName: 'RestrictDriverInstallationToAdministrators' }],
['id-smartscreen', { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Microsoft\\Windows\\System', valueName: 'EnableSmartScreen' }]
];
const finding = (tool: string, ruleId: string, occurrence: string, title: string) => ({
resultProjectionId: `scanner-finding:${occurrence}`, sourceKind: 'scanner-finding', sourceResultId: occurrence, sourceTool: tool,
sourceRuleId: ruleId, outcomeNormalized: 'fail', title, sourceObservedAt: '2026-10-01T00:00:00Z'
}) as unknown as UnifiedResultProjection;
const result = buildAssessmentContext({
projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: '2026-10-13T08:00:00Z', repositoryRevision: 1,
observations: targets.map(([identity]) => observation(`obs-${identity}`, identity)),
analyses: [{ run: { id: 'a' }, resolutions: targets.map(([identity, target]) => resolution(`obs-${identity}`, identity, target)) }] as never,
evaluations: targets.map(([identity], index) => evaluation(`e${index}`, identity, 'fail', true, identity)),
scannerFindings: [
finding('purple-knight', 'SI000027', 'occ-asrep', 'Users with Kerberos pre-authentication disabled'),
finding('purple-knight', 'SI000088', 'occ-trust', 'Outbound forest trust with SID History enabled'),
finding('maester', 'EIDSCA.AM03', 'occ-number-matching', 'Authentication Method - Microsoft Authenticator - Require number matching for push notifications.')
],
catalog: RELEASED_ASSESSMENT_CONTEXT_CATALOG
});
const setting = (identity: string) => result.settings.find(item => item.identityKey === identity)?.clusterIds;
expect(setting('id-rdp')).toEqual(['lateral-movement-exposure']);
expect(setting('id-anon')).toEqual(['ad-reconnaissance-exposure']);
expect(setting('id-lmhash')).toEqual(['password-guessing-exposure']);
// The Microsoft baseline area table places the printer policy tree in credential relay.
expect(setting('id-printers')).toEqual(['credential-relay-exposure']);
// A value-specific rule under Windows\System does not take over its sibling AllowDomainPINLogon.
expect(setting('id-smartscreen')).toEqual(['malicious-code-execution-exposure']);
const placed = (occurrence: string) => result.findings.find(item => item.occurrenceId === occurrence)?.clusterIds;
expect(placed('occ-asrep')).toEqual(['kerberoasting-exposure']);
// User decision 2026-10-12: trusts belong to recovery.
expect(placed('occ-trust')).toEqual(['ad-recovery-gaps']);
expect(placed('occ-number-matching')).toEqual(['cloud-sign-in-protection-gaps']);
expect(result.coverage).toMatchObject({ gpoUncatalogued: 0, scannerUncatalogued: 0 });
});
});