import { describe, expect, it } from 'vitest'; import type { UnifiedResultProjection } from '../../src/domain/unified-result'; import { RELEASED_ASSESSMENT_CONTEXT_CATALOG as catalog } from '../../src/assessment-context/context-catalog'; import { ASSESSMENT_CONTEXT_ENGINE, ASSESSMENT_CONTEXT_ENGINE_VOCABULARY, buildAssessmentContext } from '../../src/assessment-context/context-engine'; import { CONTEXT_RECOGNITION_TERMS, CONTEXT_RECOGNITION_VOCABULARY, recognizeClusters } from '../../src/assessment-context/context-recognition'; // User decision 2026-10-01: the context engine recognizes context on its own, as validly as possible. // Tier 1 uses ATT&CK techniques the scanner declares; tier 2 suggests clusters from distinctive terms of the rule text. const clustersOf = (conditionId: string) => catalog.clusters.filter(cluster => cluster.conditionIds.includes(conditionId)).map(cluster => cluster.id); /** Every catalogued text with the clusters the catalogue assigns it: the ground truth of the vocabulary. */ const truth = [ ...catalog.scannerRules.map(rule => ({ text: rule.ruleText, clusters: clustersOf(rule.conditionId) })), ...catalog.cisSectionRules.map(rule => ({ text: rule.sectionTitle, clusters: clustersOf(rule.conditionId) })), ...catalog.semantics.map(semantic => ({ text: semantic.settingName, clusters: [...new Set(semantic.conditions.flatMap(condition => clustersOf(condition.id)))] })) ]; describe('context recognition vocabulary (tier 2)', () => { it('never contradicts the catalogue: every recognized cluster of a catalogued text is one the catalogue assigns', () => { const contradictions = truth.flatMap(item => recognizeClusters(item.text) .filter(match => !item.clusters.includes(match.clusterId)) .map(match => `${item.text} -> ${match.clusterId} (${match.matchedText})`)); expect(contradictions).toEqual([]); }); it('recognizes most catalogued texts, so it is useful for the ones not yet catalogued', () => { const recognized = truth.filter(item => recognizeClusters(item.text).some(match => item.clusters.includes(match.clusterId))); expect(truth.length).toBeGreaterThan(300); expect(recognized.length / truth.length).toBeGreaterThanOrEqual(0.7); }); it('only names real clusters, never the uncatalogued cluster, with unique term IDs', () => { const clusterIds = new Set(catalog.clusters.map(cluster => cluster.id)); expect(CONTEXT_RECOGNITION_TERMS.filter(term => !clusterIds.has(term.clusterId))).toEqual([]); expect(CONTEXT_RECOGNITION_TERMS.filter(term => term.clusterId === 'other-deviations')).toEqual([]); expect(new Set(CONTEXT_RECOGNITION_TERMS.map(term => term.id)).size).toBe(CONTEXT_RECOGNITION_TERMS.length); }); it('suggests the expected clusters for public scanner rule titles that are not catalogued yet', () => { const named = (text: string) => recognizeClusters(text).map(match => match.clusterId); expect(named('Users with SPN defined')).toEqual(['kerberoasting-exposure']); expect(named('Users with Kerberos pre-authentication disabled')).toEqual(['kerberoasting-exposure']); expect(named('Objects with constrained delegation configured')).toEqual(['kerberos-delegation-abuse']); expect(named('Accounts with Constrained Delegation configured to ghost SPN')).toEqual(['kerberos-delegation-abuse']); expect(named('Non-default principals with DC Sync rights on the domain')).toEqual(['ad-privileged-account-exposure']); expect(named('Domains with obsolete functional levels')).toEqual(['patch-configuration-drift']); // Change indicators count as possible persistence traces (user decision 2026-10-01: context for every deviation). expect(named('AD objects created within the last 10 days')).toEqual(['ad-persistence-indicators']); expect(named('SYSVOL Executable Changes')).toEqual(['ad-persistence-indicators']); // Words that only look related still stay unrecognized instead of being guessed. expect(named('Relatively high number of inactive computer accounts')).toEqual([]); }); it('does not repeat the false matches measured on real CIS benchmark titles (2026-10-01)', () => { const named = (text: string) => recognizeClusters(text).map(match => match.clusterId); // User rights and rename options only share a word with another attack surface. expect(named("Ensure 'Manage auditing and security log' is set to 'Administrators' (DC only)")).toEqual([]); expect(named("Ensure 'Generate security audits' is set to 'LOCAL SERVICE, NETWORK SERVICE'")).toEqual([]); expect(named("Ensure 'Deny log on locally' to include 'Guests'")).toEqual([]); expect(named("Ensure 'Allow log on through Remote Desktop Services' is set to 'Administrators' (DC only)")).toEqual([]); expect(named("Configure 'Accounts: Rename administrator account'")).toEqual([]); expect(named("Configure 'Accounts: Rename guest account'")).toEqual([]); expect(named("Ensure 'Audit Account Lockout' is set to include 'Failure'")).toEqual(['detection-blind-spots']); expect(named("Ensure 'Audit client does not support signing' is set to 'Enabled'")).toEqual([]); expect(named("Ensure 'Set TLS/SSL security policy for IPP printers: Disallow invalid certificate authority'")).toEqual([]); expect(named("Ensure 'Network security: Minimum session security for NTLM SSP based (including secure RPC) clients'")).toEqual(['credential-relay-exposure']); expect(named("Ensure 'MSS: (NoNameReleaseOnDemand) Allow the computer to ignore NetBIOS name release requests'")).toEqual([]); expect(named("Ensure 'Enable OneSettings Auditing' is set to 'Enabled'")).toEqual([]); }); }); describe('vocabulary 1.1.0 for security templates, user rights and cloud findings (2026-10-01)', () => { const named = (text: string) => recognizeClusters(text).map(match => match.clusterId); it('recognizes public setting and rule names that carried no cluster word before', () => { expect(named('System Access · LockoutBadCount')).toEqual(['password-guessing-exposure']); expect(named('Privilege Rights · SeTcbPrivilege')).toEqual(['host-credential-privilege-exposure']); expect(named('Privilege Rights · SeDenyNetworkLogonRight')).toEqual(['lateral-movement-exposure']); expect(named('Privilege Rights · SeEnableDelegationPrivilege')).toEqual(['kerberos-delegation-abuse']); expect(named('POL_AdmPwd_Enabled')).toEqual(['lateral-movement-exposure']); expect(named('Presence of non-supported versions of Windows 10 or Windows 11')).toEqual(['patch-configuration-drift']); expect(named('The password used in Azure AD SSO has not been changed for at least one year')).toEqual(['cloud-sign-in-protection-gaps']); expect(named('GPO linking delegation at the domain level')).toEqual(['ad-privileged-account-exposure']); expect(named('Security defaults not enabled')).toEqual(['cloud-sign-in-protection-gaps']); }); it('places the public scanner titles measured as unplaced on 2026-10-12 (vocabulary 1.2.0)', () => { expect(named('Full Scan Removable Drives should be enabled.')).toEqual(['malicious-code-execution-exposure']); expect(named('Full Scan Mapped Drives should be disabled for performance.')).toEqual(['malicious-code-execution-exposure']); expect(named('Cleaned Malware should be retained for at least 30 days.')).toEqual(['malicious-code-execution-exposure']); expect(named('Number of DC inactive: 2')).toEqual(['ad-recovery-gaps']); // User decision 2026-10-12: trusts belong to recovery. expect(named('At least one inactive trust has been found: 1')).toEqual(['ad-recovery-gaps']); expect(named('Risky delegated permission on AD OUs')).toEqual(['ad-privileged-account-exposure']); expect(named('Set a honeytoken account.')).toEqual(['detection-blind-spots']); expect(named('Configure VPN integration.')).toEqual(['detection-blind-spots']); // User decision 2026-10-12: governance and workload identity findings use the existing cloud clusters. expect(named('Pending approvals for Critical Asset Management should not be present.')).toEqual(['cloud-privileged-access-exposure']); expect(named('No catalog should contain resources without any associated access packages.')).toEqual(['cloud-privileged-access-exposure']); expect(named('Review who can change attributes used by dynamic group rules.')).toEqual(['cloud-privileged-access-exposure']); expect(named('Convert your application(s) to Managed Identity.')).toEqual(['cloud-app-consent-exposure']); expect(named('Resolve unsecure account attributes.')).toEqual(['password-guessing-exposure']); expect(named('Resolve unsecure domain configurations.')).toEqual(['credential-relay-exposure']); // Neighbouring readings stay with their own cluster or unplaced. expect(named('Ensure the default malware filter policy blocks common attachment types')).not.toContain('malicious-code-execution-exposure'); expect(named('Trust computer and user accounts for delegation')).not.toContain('ad-recovery-gaps'); expect(named('Trusted locations should be configured for Conditional Access')).toEqual([]); expect(named('Apps with delegated permissions on Microsoft Graph')).not.toContain('ad-privileged-account-exposure'); expect(named('Relatively high number of inactive computer accounts')).toEqual([]); }); it('publishes every vocabulary release with a new engine version, so stored work-package snapshots refresh (2026-10-12)', () => { // Regression: vocabulary 1.2.0 placed findings in clusters while the stored snapshot kept them unassigned, // because that snapshot is keyed by engine and catalogue version only. expect(ASSESSMENT_CONTEXT_ENGINE_VOCABULARY).toBe(CONTEXT_RECOGNITION_VOCABULARY.version); expect(ASSESSMENT_CONTEXT_ENGINE.version).toBe('1.13.0'); }); it('keeps conditional access trusted locations apart from Office trusted locations', () => { expect(named('Trusted locations should be configured for Conditional Access')).toEqual([]); expect(named(['software', 'policies', 'microsoft', 'office', '16.0', 'excel', 'security', 'trusted locations'].join(String.fromCharCode(92)) + ' · allownetworklocations')) .toEqual(['malicious-code-execution-exposure']); }); });