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, 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[]]>) => ({ 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; // 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[]) => 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) => ({ 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]> = [ ['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]> = [ ['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 }); }); });