143 lines
10 KiB
TypeScript
143 lines
10 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { FindingRecord } from '../../src/domain/finding';
|
|
import type { UnifiedResultProjection } from '../../src/domain/unified-result';
|
|
import {
|
|
classifySecurityDomain, securityDomainAssignmentForRecord, securityDomainBuckets, securityDomainCatalog,
|
|
securityDomainIdForRecord, securityDomainIdForUnifiedResult, securityDomainTierCoverage
|
|
} from '../../src/evaluation/security-domains';
|
|
|
|
// Synthetic Raw Data records only (user decision 2026-10-04: Security Domains come from Raw Data).
|
|
function record(fields: Record<string, string>, title = 'Synthetic check'): FindingRecord {
|
|
return {
|
|
id: `synthetic-${Math.random()}`, index: 0, title, filled: [], meta: [],
|
|
fields: Object.entries(fields).map(([label, value], index) => ({ label, value, index }))
|
|
} as FindingRecord;
|
|
}
|
|
|
|
const assign = (fields: Record<string, string>) => securityDomainAssignmentForRecord(record(fields));
|
|
|
|
describe('Security Domain classifier (structured Raw Data, six domains)', () => {
|
|
it('uses exactly the six agreed domains and no GPO, Authentication or Unclassified entry', () => {
|
|
expect(securityDomainCatalog.map(domain => domain.label)).toEqual([
|
|
'Identity & Authentication', 'Privileged Access & Delegation', 'AD Infrastructure & PKI',
|
|
'System Hardening', 'Cloud & Collaboration', 'Monitoring & Recovery'
|
|
]);
|
|
});
|
|
|
|
it('tier 1: Purple Knight native categories, including Group Policy Security as System Hardening', () => {
|
|
const scan = 'Purple Knight for Active Directory · 2026-01-01T00:00:00.000Z';
|
|
expect(assign({ 'Source: Scan': scan, Category: 'Account Security' })).toMatchObject({ domainId: 'identity', tier: 1, method: 'native-category' });
|
|
expect(assign({ 'Source: Scan': scan, Category: 'Kerberos Security' }).domainId).toBe('identity');
|
|
expect(assign({ 'Source: Scan': scan, Category: 'AD Delegation' }).domainId).toBe('privilege');
|
|
expect(assign({ 'Source: Scan': scan, Category: 'AD Infrastructure Security' }).domainId).toBe('infrastructure');
|
|
expect(assign({ 'Source: Scan': scan, Category: 'Group Policy Security' }).domainId).toBe('hardening');
|
|
});
|
|
|
|
it('tier 1: PingCastle exact rule ID first, native category as the profile fallback', () => {
|
|
const scan = 'PingCastle · 2026-01-01T00:00:00.000Z';
|
|
expect(assign({ 'Source: Scan': scan, 'Source Rule ID': 'A-AuditDC', Category: 'Anomalies' }))
|
|
.toMatchObject({ domainId: 'recovery', tier: 1, method: 'native-rule-id', field: 'Source Rule ID' });
|
|
expect(assign({ 'Source: Scan': scan, 'Source Rule ID': 'A-CertTempCustomSubject', Category: 'Anomalies' }).domainId).toBe('infrastructure');
|
|
expect(assign({ 'Source: Scan': scan, 'Source Rule ID': 'S-NEW-SYNTHETIC', Category: 'Privileged Accounts' }))
|
|
.toMatchObject({ domainId: 'privilege', tier: 1, method: 'native-category' });
|
|
});
|
|
|
|
it('tier 2: CIS-CAT by the report section title path, the most specific section winning', () => {
|
|
const scan = 'CIS-CAT Pro · 2026-01-01T00:00:00.000Z';
|
|
expect(assign({ 'Source: Scan': scan, 'Benchmark section': 'Local Policies > User Rights Assignment' }))
|
|
.toMatchObject({ domainId: 'privilege', tier: 2, method: 'report-section' });
|
|
expect(assign({ 'Source: Scan': scan, 'Benchmark section': 'Account Policies > Password Policy' }).domainId).toBe('identity');
|
|
expect(assign({ 'Source: Scan': scan, 'Benchmark section': 'Advanced Audit Policy Configuration > Logon/Logoff' }).domainId).toBe('recovery');
|
|
expect(assign({ 'Source: Scan': scan, 'Benchmark section': 'Windows Defender Firewall with Advanced Security (formerly Windows Firewall with Advanced Security) > Public Profile' }).domainId).toBe('hardening');
|
|
expect(assign({ 'Source: Scan': scan, 'Benchmark section': 'Administrative Templates (Computer) > Windows Components > Event Log Service > Security' }).domainId).toBe('recovery');
|
|
expect(assign({ 'Source: Scan': scan, 'Benchmark section': 'Administrative Templates (Computer) > Windows Components > Search' }).domainId).toBe('hardening');
|
|
});
|
|
|
|
it('never classifies CIS-CAT by the bare section number or the title text', () => {
|
|
const scan = 'CIS-CAT Pro · 2026-01-01T00:00:00.000Z';
|
|
const result = assign({ 'Source: Scan': scan, 'Source Rule ID': '2.2.1', 'Benchmark Item': '2.2.1 Ensure audit account password privilege' });
|
|
expect(result).toMatchObject({ domainId: 'hardening', tier: 5, method: 'source-fallback' });
|
|
});
|
|
|
|
it('tier 3: Maester suite and control IDs; Entra checks are Identity & Authentication', () => {
|
|
const scan = 'Maester · 2026-01-01T00:00:00.000Z';
|
|
expect(assign({ 'Source: Scan': scan, 'Source Rule ID': 'EIDSCA.AF01', Category: 'EIDSCA' }))
|
|
.toMatchObject({ domainId: 'identity', tier: 3, method: 'suite-control-id' });
|
|
expect(assign({ 'Source: Scan': scan, 'Source Rule ID': 'CISA.MS.AAD.3.1v1', Category: 'CISA' }).domainId).toBe('identity');
|
|
expect(assign({ 'Source: Scan': scan, 'Source Rule ID': 'CISA.MS.EXO.1.1v1', Category: 'CISA' }).domainId).toBe('cloud');
|
|
expect(assign({ 'Source: Scan': scan, 'Source Rule ID': 'ORCA.104', Category: 'ORCA' }).domainId).toBe('cloud');
|
|
expect(assign({ 'Source: Scan': scan, 'Source Rule ID': 'MT.1001', Category: 'Maester/Entra', Tags: 'Maester, CA, Security' }).domainId).toBe('identity');
|
|
expect(assign({ 'Source: Scan': scan, 'Source Rule ID': 'MT.1045', Category: 'Maester/Teams' }).domainId).toBe('cloud');
|
|
});
|
|
|
|
it('tier 1: a native Maester Privileged tag marks privileged access, also in JSON-style tag lists', () => {
|
|
const scan = 'Maester · 2026-01-01T00:00:00.000Z';
|
|
expect(assign({ 'Source: Scan': scan, 'Source Rule ID': 'MT.1031', Category: 'Maester/Entra', Tags: 'Privileged, Security, PIM' }))
|
|
.toMatchObject({ domainId: 'privilege', tier: 1, method: 'native-tag' });
|
|
expect(assign({ 'Source: Scan': scan, ID: 'MT.1031', Block: 'Maester', Tag: '["Privileged","Security"]' }).domainId).toBe('privilege');
|
|
});
|
|
|
|
it('tier 4: GPO settings by technical policy identity', () => {
|
|
const scan = 'GPOs · 2026-01-01T00:00:00.000Z';
|
|
expect(assign({ 'Source: Scan': scan, 'Policy type': 'security-template', 'Policy section': 'Privilege Rights' }))
|
|
.toMatchObject({ domainId: 'privilege', tier: 4, method: 'policy-section' });
|
|
expect(assign({ 'Source: Scan': scan, 'Policy type': 'security-template', 'Policy section': 'Kerberos Policy' }).domainId).toBe('identity');
|
|
expect(assign({ 'Source: Scan': scan, 'Policy type': 'advanced-audit-policy', 'Policy section': 'system audit subcategory' }).domainId).toBe('recovery');
|
|
expect(assign({ 'Source: Scan': scan, 'Policy type': 'registry-value', 'Registry path': 'HKEY_LOCAL_MACHINE\\Software\\Policies\\Microsoft\\Windows\\EventLog\\Security' }))
|
|
.toMatchObject({ domainId: 'recovery', tier: 4, method: 'registry-key' });
|
|
expect(assign({ 'Source: Scan': scan, 'Policy type': 'security-template', 'Policy section': 'Registry Values',
|
|
'Registry path': 'MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\NoLMHash' }).domainId).toBe('identity');
|
|
expect(assign({ 'Source: Scan': scan, 'Policy type': 'security-template', 'Policy section': 'Registry Values',
|
|
'Registry path': 'MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\EnableLUA' }).domainId).toBe('privilege');
|
|
// A key boundary is required: `...\Lsa` must not match `...\LsaSynthetic`.
|
|
expect(assign({ 'Source: Scan': scan, 'Policy type': 'registry-value', 'Registry path': 'HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Control\\LsaSynthetic' }).domainId).toBe('hardening');
|
|
});
|
|
|
|
it('tier 5: the source fallback, refined by Purple Knight target, and a visible global fallback', () => {
|
|
expect(assign({ 'Source: Scan': 'PingCastle · x', 'Source Rule ID': 'X-UNKNOWN' }))
|
|
.toMatchObject({ domainId: 'infrastructure', tier: 5, method: 'source-fallback' });
|
|
expect(assign({ 'Source: Scan': 'Purple Knight for Entra ID · x', Category: 'Synthetic Category', Target: 'Entra ID' }))
|
|
.toMatchObject({ domainId: 'identity', tier: 5, field: 'Target' });
|
|
expect(assign({ 'Source: Scan': 'Synthetic Scanner · x', Title: 'Account password backup' }))
|
|
.toMatchObject({ domainId: 'hardening', tier: 5, method: 'global-fallback' });
|
|
});
|
|
|
|
it('reads the tool from the single-tool sheet when the row has no Source: Scan', () => {
|
|
const row = record({ 'Benchmark section': 'Local Policies > User Rights Assignment' });
|
|
expect(securityDomainIdForRecord(row)).toBe('hardening');
|
|
expect(securityDomainIdForRecord(row, 'cis-cat')).toBe('privilege');
|
|
});
|
|
|
|
it('assigns by the check, never by its outcome, title or prose', () => {
|
|
const base = { 'Source: Scan': 'PingCastle · x', 'Source Rule ID': 'P-AdminLogin' };
|
|
const failed = record({ ...base, Status: 'Fail', Description: 'Backup audit firewall cloud' }, 'Cloud backup firewall');
|
|
const passed = record({ ...base, Status: 'Pass' }, 'Unrelated title');
|
|
expect(securityDomainIdForRecord(failed)).toBe('privilege');
|
|
expect(securityDomainIdForRecord(passed)).toBe('privilege');
|
|
});
|
|
|
|
it('puts every record in exactly one domain, so the domains sum to the total, and reports tier coverage', () => {
|
|
const records = [
|
|
record({ 'Source: Scan': 'PingCastle · x', 'Source Rule ID': 'A-AuditDC' }),
|
|
record({ 'Source: Scan': 'Maester · x', 'Source Rule ID': 'ORCA.100' }),
|
|
record({ 'Source: Scan': 'GPOs · x', 'Policy type': 'registry-value', 'Registry path': 'HKLM\\Software\\Synthetic' }),
|
|
record({ 'Source: Scan': 'Unknown · x' }),
|
|
record({})
|
|
];
|
|
const buckets = securityDomainBuckets(records);
|
|
expect([...buckets.keys()]).toEqual(securityDomainCatalog.map(domain => domain.id));
|
|
expect([...buckets.values()].reduce((sum, items) => sum + items.length, 0)).toBe(records.length);
|
|
const coverage = securityDomainTierCoverage(records);
|
|
expect(coverage.get('group-policy')?.get(5)).toBe(1);
|
|
expect(coverage.get('unmapped')?.get(5)).toBe(2);
|
|
});
|
|
|
|
it('classifies a report result without a Work record through the same tables', () => {
|
|
const result = { sourceKind: 'scanner-finding', sourceTool: 'pingcastle', sourceRuleId: 'A-AuditDC', categories: [] } as unknown as UnifiedResultProjection;
|
|
const policy = { sourceKind: 'policy-evaluation', sourceTool: 'GPOs', policyDeliveryMechanism: 'advanced-audit-policy', categories: [] } as unknown as UnifiedResultProjection;
|
|
expect(securityDomainIdForUnifiedResult(result)).toBe('recovery');
|
|
expect(securityDomainIdForUnifiedResult(policy)).toBe('recovery');
|
|
expect(classifySecurityDomain(() => '').method).toBe('global-fallback');
|
|
});
|
|
});
|