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|\bldaps?\b|channel\s*binding|anonymous\s*binding|rootdse|tls\s*1\.[01]/i }, { id: 'infrastructure', label: 'AD Infrastructure & PKI', glyph: 'server', pattern: /domain\s*controller|forest|\btrust|certificate|\bpki\b|\bdns\b|replication|schema|organizational\s*units?|\bous?\b.*accidental\s*deletion|\bntfrs\b|\bsysvol\b/i }, { id: 'hardening', label: 'System Hardening', glyph: 'code', pattern: /cis-cat|benchmark|hardening|hardened\s*paths?|firewall|print\s*spooler|\bspooler\b|attack\s*surface|\basr\b|defender|operating\s*system|obsolete\s+os|unsupported\s+(?:os|operating\s*system)|script\s*engines?|\bcve-\d+/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 MAESTER_CLOUD_SUITE = /(?:^|[\s,;|\["'])(?:orca(?:\.|\b)|exo(?=$|[\s,;|\]"'])|eidsca(?:\.|\b)|cisa\.ms\.(?:exo|aad)(?:\.|\b)|maester\/(?:exchange|teams|entra)(?=$|[\s,;|\]"']))/i; const recordDomainEvidenceCache = new WeakMap(); function recordDomainEvidence(record: FindingRecord): { text: string; narrativeText: string; explicitGpo: boolean; scannerSuiteText: string; } { 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|source rule id)$/i.test(field.label.trim())) .map(field => field.value)].join(' '), narrativeText: fields .filter(field => /^(?:technical explanation|rationale|recommendation|remediation(?: guidance| recommendation)?|advised solution|solution|impact|details|test details)$/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())), // Maester exposes stable suite/control identifiers in BLOCK, ID and TAG. // They are fallback evidence only: descriptive security-domain matches // above retain priority and the source values remain unchanged. scannerSuiteText: [record.title, ...fields .filter(field => /^(?:block|id|tag|tags|category|categories)$/i.test(field.label.trim())) .map(field => field.value)].join(' ') }; 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'; const descriptiveMatch = securityDomainCatalog.find(domain => domain.pattern.test(evidence.text)); if (descriptiveMatch) return descriptiveMatch.id; // Broader narrative fields are useful fallback evidence, but they may mention // several domains in one remediation. Assign only when the evidence is unique; // otherwise keeping Unclassified is more honest than inventing a category. const narrativeMatches = securityDomainCatalog.filter(domain => domain.pattern.test(evidence.narrativeText)); if (narrativeMatches.length === 1) return narrativeMatches[0].id; if (MAESTER_CLOUD_SUITE.test(evidence.scannerSuiteText)) return 'cloud'; return unclassifiedSecurityDomain.id; } /** Assign every record once so Client Info distributions reconcile to the total. */ export function dashboardDomainAssignment(records: FindingRecord[]): Map { const buckets = new Map([ ...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) ?? []; }