Assessor_2.0/src/features/dashboard/security-domains.ts

55 lines
3.3 KiB
TypeScript

import type { FindingRecord } from '../../domain/finding';
export const SECURITY_DOMAIN_FACET_ID = 'derived:security-domain';
export const securityDomainCatalog = [
{ id: 'identity', label: 'Identity & Account Hygiene', glyph: 'user', pattern: /account|identity|user|password|stale|inactive|honey\s*pot/i },
{ id: 'privilege', label: 'Privilege & Attack Paths', glyph: 'shield', pattern: /privileg|admin|delegat|control\s*path|attack\s*path|tier\s*0|pim/i },
{ id: 'authentication', label: 'Authentication & Access', glyph: 'lock', pattern: /authenticat|\bmfa\b|conditional\s*access|kerberos|sign[ -]?in|break[ -]?glass/i },
{ id: 'infrastructure', label: 'AD Infrastructure & PKI', glyph: 'server', pattern: /domain\s*controller|forest|\btrust|certificate|\bpki\b|\bdns\b|replication|schema/i },
{ id: 'hardening', label: 'System Hardening', glyph: 'code', pattern: /cis-cat|benchmark|hardening|firewall|attack\s*surface|\basr\b|defender|operating\s*system/i },
{ id: 'gpo', label: 'GPO & Baseline', glyph: 'policy', pattern: /\bgpo\b|group\s*policy|baseline|policy\s*(?:setting|deviation|conflict)|duplicates?/i },
{ id: 'cloud', label: 'Cloud & Applications', glyph: 'cloud', pattern: /\bcloud\b|\bentra\b|application|\bapps?\b|exchange|teams|sharepoint|guest|intune|purview/i },
{ id: 'recovery', label: 'Monitoring & Recovery', glyph: 'chart', pattern: /audit|monitor|logging|\bwef\b|backup|recover|recycle\s*bin|sensor|event\s*log/i }
] as const;
export const unclassifiedSecurityDomain = { id: 'unclassified', label: 'Unclassified' } as const;
const recordDomainEvidenceCache = new WeakMap<FindingRecord, { text: string; explicitGpo: boolean }>();
function recordDomainEvidence(record: FindingRecord): { text: string; explicitGpo: boolean } {
let cached = recordDomainEvidenceCache.get(record);
if (cached === undefined) {
const fields = record.fields;
cached = {
text: [record.title, ...fields
.filter(field => /^(?:title|name|finding|description|category|subcategory|sub-category|categories|control area|policy section|policy type)$/i.test(field.label.trim()))
.map(field => field.value)].join(' '),
explicitGpo: fields.some(field => /^category$/i.test(field.label.trim()) && /^gpos?$/i.test(field.value.trim()))
};
recordDomainEvidenceCache.set(record, cached);
}
return cached;
}
/** The one shared Client Info / Work classification for a retained record. */
export function securityDomainIdForRecord(record: FindingRecord): string {
const evidence = recordDomainEvidence(record);
if (evidence.explicitGpo) return 'gpo';
return securityDomainCatalog.find(domain => domain.pattern.test(evidence.text))?.id ?? unclassifiedSecurityDomain.id;
}
/** Assign every record once so Client Info distributions reconcile to the total. */
export function dashboardDomainAssignment(records: FindingRecord[]): Map<string, FindingRecord[]> {
const buckets = new Map<string, FindingRecord[]>([
...securityDomainCatalog.map(domain => [domain.id, [] as FindingRecord[]] as const),
[unclassifiedSecurityDomain.id, [] as FindingRecord[]] as const
]);
records.forEach(record => buckets.get(securityDomainIdForRecord(record))!.push(record));
return buckets;
}
export function dashboardDomainRecords(records: FindingRecord[], domainId: string): FindingRecord[] {
return dashboardDomainAssignment(records).get(domainId) ?? [];
}