93 lines
7.1 KiB
TypeScript
93 lines
7.1 KiB
TypeScript
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 { buildAssessmentContext } from '../../src/assessment-context/context-engine';
|
|
import { CONTEXT_RECOGNITION_TERMS, 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('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']);
|
|
});
|
|
});
|