import { describe, expect, it } from 'vitest'; import { defaultReview } from '../../src/domain/review'; import { makeSheet } from '../../src/importers/sheet-factory'; import { genericProfile } from '../../src/profiles/generic/generic-profile'; import { purpleKnightProfile } from '../../src/profiles/purple-knight/purple-knight-profile'; import { cisCatProfile } from '../../src/profiles/cis-cat/cis-cat-profile'; import { deriveDynamicFacets, recountFacetsAgainstActiveFilters, sourceScanNavigationSheet } from '../../src/features/search-filter/dynamic-facets'; import { filterAndSortRecords } from '../../src/features/search-filter/filter-records'; import { securityDomainCatalog } from '../../src/features/dashboard/security-domains'; const sheet = makeSheet('Findings', [ ['Title', 'Severity', 'Status', 'Score', 'Description'], ['A', 'High', 'Open', '9', 'A'.repeat(120)], ['B', 'High', 'Open', '7', 'B'.repeat(120)], ['C', 'Medium', 'Closed', '5', 'C'.repeat(120)], ['D', 'Low', 'Open', '2', 'D'.repeat(120)] ], '', 'generic.csv'); describe('dynamic facets', () => { it('derives useful category filters but excludes score ranges and unsuitable free text', () => { const facets = deriveDynamicFacets(sheet, null, genericProfile, {}); expect(facets.find(facet => facet.id === 'field:1')?.kind).toBe('category'); // Die rohe Statusspalte erscheint nicht mehr als eigener Filter: seit das // generische Profil ein Statusvokabular besitzt, uebernimmt die Facette // "Ergebnisstatus" diese Aufgabe. Zwei Filter fuer dieselbe Spalte waren // fuer die Bedienung nicht unterscheidbar. expect(facets.some(facet => facet.id === 'field:2')).toBe(false); // "Open" normalisiert auf Outcome unknown und ist nach demselben // kanonischen Praedikat wie in der Result DB keine Abweichung. expect(facets.find(facet => facet.id === 'profile:status')?.values).toEqual([]); expect(facets.some(facet => facet.id === 'field:3')).toBe(false); expect(facets.some(facet => facet.id === 'field:4')).toBe(false); }); it('combines category facets, numeric ranges and full-text search', () => { const records = filterAndSortRecords(sheet, { activeProfile: genericProfile, search: 'b', sortColumn: 'all', sortDirection: 'desc', statusFilter: 'all', facetFilters: { 'field:1': { values: ['high'] }, 'field:3': { minimum: '6', maximum: '8' } }, actionOnly: false, reviews: {} }); expect(records.map(record => record.title)).toEqual(['B']); }); it('removes facet values that have no hits under the other active filters', () => { const facets = deriveDynamicFacets(sheet, null, genericProfile, {}); const counted = recountFacetsAgainstActiveFilters({ facets, records: sheet.analysis.records, filters: {}, reviews: {}, statusFilter: 'deviation', matchesStatus: (record, status) => genericProfile.matchesStatus(record, status) }); expect(counted.filter(facet => !facet.preserveEmptyValues).flatMap(facet => facet.values).every(value => value.count > 0)).toBe(true); expect(counted.find(facet => facet.id === 'field:1')?.values.map(value => value.label)).not.toContain('Medium'); }); it('exposes declarative Purple Knight status filters through the common facet model', () => { const purpleSheet = makeSheet('Indicator Results', [ ['Name', 'Weight', 'Result'], ['Finding A', '10', 'IOEs found'], ['Finding B', '2', 'Passed'], ['Finding C', '1', 'Not Relevant'] ], '', 'purple-knight.csv'); const facet = deriveDynamicFacets(purpleSheet, null, purpleKnightProfile, {})[0]; expect(facet).toMatchObject({ id: 'profile:status', source: 'profile-status', label: 'Result Status' }); expect(facet.values.find(value => value.value === 'ioes-found')).toMatchObject({ count: 1 }); }); // Purple Knight verteilt mehrwertige MITRE-Zuordnungen auf nummerierte // Geschwisterspalten. Getrennt ergaeben sie je Position einen eigenen Filter // mit positionsabhaengigen Zahlen, die sich zudem nur UND-verknuepfen liessen. it('merges numbered sibling columns of a multi-valued source field into one facet', () => { const mitreSheet = makeSheet('Findings', [ ['Title', 'MITRE ATT&CK_1', 'MITRE ATT&CK_2', 'MITRE ATT&CK_3'], ['A', 'Privilege Escalation', 'Credential Access', ''], ['B', 'Credential Access', 'Privilege Escalation', ''], ['C', 'Privilege Escalation', '', ''], ['D', 'Lateral Movement', 'Credential Access', 'Privilege Escalation'] ], '', 'purple-knight.csv'); const facets = deriveDynamicFacets(mitreSheet, null, genericProfile, {}); const mitreFacets = facets.filter(facet => facet.label.toLowerCase().includes('mitre')); expect(mitreFacets).toHaveLength(1); expect(mitreFacets[0].label).toBe('MITRE ATT&CK'); expect(mitreFacets[0].columnIndexes).toEqual([1, 2, 3]); // Jeder Datensatz zaehlt je Wert genau einmal, unabhaengig von der Position. expect(mitreFacets[0].values.find(value => value.label === 'Privilege Escalation')?.count).toBe(4); expect(mitreFacets[0].values.find(value => value.label === 'Credential Access')?.count).toBe(3); }); it('matches a multi-valued field when any of its columns carries the selected value', () => { const mitreSheet = makeSheet('Findings', [ ['Title', 'MITRE ATT&CK_1', 'MITRE ATT&CK_2'], ['A', 'Privilege Escalation', 'Credential Access'], ['B', 'Credential Access', 'Privilege Escalation'], ['C', 'Lateral Movement', ''] ], '', 'purple-knight.csv'); const facet = deriveDynamicFacets(mitreSheet, null, genericProfile, {}) .find(item => item.label === 'MITRE ATT&CK')!; const records = filterAndSortRecords(mitreSheet, { activeProfile: genericProfile, search: '', sortColumn: 'all', sortDirection: 'desc', statusFilter: 'all', facetFilters: { [facet.id]: { values: ['privilege escalation'] } }, actionOnly: false, reviews: {} }); // A traegt den Wert in Spalte 1, B in Spalte 2 - beide muessen passen. expect(records.map(record => record.title).sort()).toEqual(['A', 'B']); }); // Ohne ausdrueckliche numerische Sortierung ordnet die Arbeitsansicht nach // der kanonischen Severity. Das ersetzt das Sortieren nach werkzeugeigenen // Punktespalten, die zwischen Werkzeugen nicht vergleichbar sind. it('orders records by canonical severity when no numeric sort is active', () => { const severitySheet = makeSheet('Findings', [ ['Title', 'Severity'], ['A', 'Low'], ['B', 'Critical'], ['C', 'Informational'], ['D', 'HIGH'], ['E', 'Mittel'] ], '', 'generic.csv'); const records = filterAndSortRecords(severitySheet, { activeProfile: genericProfile, search: '', sortColumn: 'all', sortDirection: 'desc', statusFilter: 'all', facetFilters: {}, actionOnly: false, reviews: {} }); // Gross-/Kleinschreibung und deutsche Schreibweise werden mit abgebildet. expect(records.map(record => record.title)).toEqual(['B', 'D', 'E', 'A', 'C']); }); it('adds review facets only when useful repeated workflow values exist', () => { const records = sheet.analysis.records; const reviews = { [records[0].id]: { ...defaultReview(), status: 'Offen', owner: 'Team A' }, [records[1].id]: { ...defaultReview(), status: 'In Arbeit', owner: 'Team B' }, [records[2].id]: { ...defaultReview(), status: 'Offen', owner: 'Team A' } }; const facets = deriveDynamicFacets(sheet, null, genericProfile, reviews); expect(facets.find(facet => facet.id === 'review:status')?.values).toHaveLength(2); expect(facets.find(facet => facet.id === 'review:owner')?.values).toHaveLength(2); }); it('keeps the complete GPO filter set together and Source: Scan permanently last', () => { const gpoSheet = makeSheet('Group Policy', [ ['Title', 'Status', 'Category', 'Baseline', 'Comparison', 'Conflict', 'Description source', 'Diagnostics', 'Duplicates', 'Imported', 'Policy section', 'Policy type', 'Source: Scan'], ['A', 'Fail', 'GPOs', 'One', 'Mismatch', 'Yes', 'ADML', 'None', 'No', 'First', 'Registry', 'Policy', 'GPOs · Synthetic'], ['B', 'Pass', 'GPOs', 'Two', 'Match', 'No', 'Derived', 'Review', 'Yes', 'Second', 'Security', 'Preference', 'GPOs · Synthetic'] ], '', 'synthetic', { toolId: 'group-policy' }); const facets = deriveDynamicFacets(gpoSheet, null, genericProfile, {}); expect(facets.map(facet => facet.label)).toEqual([ 'Result Status', 'Category', 'Security Domain', 'Baseline', 'Comparison', 'Conflict', 'Description source', 'Diagnostics', 'Duplicates', 'Imported', 'Policy section', 'Policy type', 'Source: Scan' ]); expect(facets.find(facet => facet.label === 'Security Domain')?.values) .toEqual(securityDomainCatalog.map(domain => ({ value: domain.id, label: domain.label, count: domain.id === 'gpo' ? 2 : 0 }))); expect(facets.at(-1)?.values).toEqual([{ value: 'gpos · synthetic', label: 'GPOs · Synthetic', count: 2 }]); }); it('uses the Client Info Security Domains as a real Work filter', () => { const domainSheet = makeSheet('Findings', [ ['Title', 'Category', 'Status'], ['Inactive account', 'Identity', 'Fail'], ['Firewall disabled', 'System hardening', 'Fail'], ['Audit logging disabled', 'Monitoring', 'Fail'] ], '', 'synthetic.csv'); const facet = deriveDynamicFacets(domainSheet, null, genericProfile, {}) .find(item => item.id === 'derived:security-domain')!; expect(facet.values.map(value => value.label)).toEqual(securityDomainCatalog.map(domain => domain.label)); expect(facet.values.filter(value => value.count > 0).map(value => value.value)).toEqual(['identity', 'hardening', 'recovery']); const records = filterAndSortRecords(domainSheet, { activeProfile: genericProfile, search: '', sortColumn: 'all', sortDirection: 'asc', statusFilter: 'all', facetFilters: { [facet.id]: { values: ['hardening'] } }, actionOnly: false, reviews: {} }); expect(records.map(record => record.title)).toEqual(['Firewall disabled']); }); it('searches visible Work titles and fields without case sensitivity', () => { const title = 'Presence of Admin accounts which do not have the flag "This account is sensitive and cannot be delegated": 17'; const searchSheet = makeSheet('PingCastle findings', [ ['Title', 'Source Rule ID', 'Status', 'Source: Scan'], [title, 'S-ADMIN-DELEGATION', 'Matched', 'PingCastle · Synthetic'], ['Unrelated synthetic finding', 'S-OTHER', 'Matched', 'PingCastle · Synthetic'] ], '', 'synthetic-pingcastle.html', { toolId: 'pingcastle' }); const options = { activeProfile: genericProfile, sortColumn: 'all', sortDirection: 'asc' as const, statusFilter: 'all', facetFilters: {}, actionOnly: false, reviews: {} }; expect(filterAndSortRecords(searchSheet, { ...options, search: ' PRESENCE OF ADMIN ACCOUNTS WHICH DO NOT HAVE THE FLAG ' }).map(record => record.title)).toEqual([title]); expect(filterAndSortRecords(searchSheet, { ...options, search: 's-admin-delegation' }).map(record => record.title)).toEqual([title]); }); it('classifies Maester ORCA and Entra suites instead of exposing them as Unclassified', () => { const maesterSheet = makeSheet('Maester findings', [ ['Title', 'Block', 'ID', 'Tag', 'Status', 'Source: Scan'], ['ORCA.104: High Confidence Phish action set to Quarantine message.', 'ORCA', 'ORCA.104', '["ORCA","ORCA.104","EXO","Security","All"]', 'Failed', 'Maester · Synthetic'], ['Default Settings control is configured.', 'EIDSCA', 'EIDSCA.ST09', '["EIDSCA","EIDSCA.ST09"]', 'Failed', 'Maester · Synthetic'], ['Conditional access policy is missing.', 'Maester', 'MT.1001', '["Maester/Entra","Security"]', 'Failed', 'Maester · Synthetic'] ], '', 'Maester.html', { toolId: 'maester' }); const facet = deriveDynamicFacets(maesterSheet, null, genericProfile, {}) .find(item => item.id === 'derived:security-domain')!; expect(facet.values.find(value => value.value === 'cloud')?.count).toBe(2); expect(facet.values.find(value => value.value === 'authentication')?.count).toBe(1); expect(facet.values.some(value => value.value === 'unclassified')).toBe(false); expect(filterAndSortRecords(maesterSheet, { activeProfile: genericProfile, search: '', sortColumn: 'all', sortDirection: 'asc', statusFilter: 'all', facetFilters: { [facet.id]: { values: ['cloud'] } }, actionOnly: false, reviews: {} }).map(record => record.title)).toEqual([ 'ORCA.104: High Confidence Phish action set to Quarantine message.', 'Default Settings control is configured.' ]); }); it('classifies the retained PingCastle infrastructure and hardening findings from Work', () => { const pingCastleSheet = makeSheet('PingCastle findings', [ ['Title', 'Source Rule ID', 'Technical explanation', 'Status', 'Source: Scan'], ['Obsolete OS (Windows 7)', '', '', 'Matched', 'PingCastle · Synthetic'], ['Check if the Channel Binding is enabled for LDAPS', '', '', 'Matched', 'PingCastle · Synthetic'], ['Hardened Paths weakness', '', '', 'Matched', 'PingCastle · Synthetic'], ['Verify if there are restrictions for internet connectivity of script engines', '', '', 'Matched', 'PingCastle · Synthetic'], ['Check if OUs and Containers are protected from accidental deletion.', '', '', 'Matched', 'PingCastle · Synthetic'], ['Check if the mitigation for CVE-2021-42291 has been enabled', '', '', 'Matched', 'PingCastle · Synthetic'], ['Ensure that the rootDse Anonymous Binding is disabled', '', '', 'Matched', 'PingCastle · Synthetic'], ['Check if NTFRS is used to replicate SYSVOL', '', '', 'Matched', 'PingCastle · Synthetic'], ['Check if LDAPS is using TLS 1.0 or TLS 1.1.', '', '', 'Matched', 'PingCastle · Synthetic'], ['Ensure that the Print Spooler service cannot be abused to get the DC credentials', 'A-DC-Spooler', '', 'Matched', 'PingCastle · Synthetic'], ['Synthetic service configuration without a descriptive scanner title', 'CUSTOM-PRINT-SPOOLER-CONTROL', '', 'Matched', 'Synthetic Scanner'], ['Synthetic finding with narrative-only evidence', '', 'The Print Spooler service should be disabled on affected systems.', 'Matched', 'Synthetic Scanner'] ], '', 'synthetic-pingcastle.html', { toolId: 'pingcastle' }); const facet = deriveDynamicFacets(pingCastleSheet, null, genericProfile, {}) .find(item => item.id === 'derived:security-domain')!; expect(facet.values.find(value => value.value === 'hardening')?.count).toBe(7); expect(facet.values.find(value => value.value === 'authentication')?.count).toBe(3); expect(facet.values.find(value => value.value === 'infrastructure')?.count).toBe(2); expect(facet.values.some(value => value.value === 'unclassified')).toBe(false); expect(filterAndSortRecords(pingCastleSheet, { activeProfile: genericProfile, search: '', sortColumn: 'all', sortDirection: 'asc', statusFilter: 'all', facetFilters: { [facet.id]: { values: ['hardening'] } }, actionOnly: false, reviews: {} }).map(record => record.title)).toContain( 'Ensure that the Print Spooler service cannot be abused to get the DC credentials'); }); it('uses Unclassified only when available fallback evidence would require guessing', () => { const ambiguousSheet = makeSheet('Synthetic findings', [ ['Title', 'Technical explanation', 'Status', 'Source: Scan'], ['Opaque finding', 'Review account passwords and backup monitoring together.', 'Failed', 'Synthetic Scanner'] ], '', 'synthetic.csv', { toolId: 'generic' }); const facet = deriveDynamicFacets(ambiguousSheet, null, genericProfile, {}) .find(item => item.id === 'derived:security-domain')!; expect(facet.values.find(value => value.value === 'unclassified')?.count).toBe(1); }); it('provides a final Source: Scan control when a sheet has no explicit scan column', () => { const fallbackSheet = makeSheet('Findings', [['Title', 'Severity'], ['A', 'High'], ['B', 'Low']], '', 'synthetic.csv'); const facets = deriveDynamicFacets(fallbackSheet, { scanTool: 'Synthetic Scanner' } as never, genericProfile, {}); expect(facets.at(-1)).toMatchObject({ label: 'Source: Scan', source: 'provenance' }); expect(facets.at(-1)?.values).toEqual([{ value: 'synthetic scanner', label: 'Synthetic Scanner', count: 2 }]); }); it('offers every workspace scan from a dedicated GPO sheet and navigates to the aggregate', () => { const aggregate = makeSheet('Client · All Records', [ ['Title', 'Source: Scan'], ['Policy', 'GPOs · 2026-09-07'], ['Directory', 'PingCastle · 2026-09-06'], ['Cloud', 'Maester · 2026-09-05'] ], '', 'synthetic', { isProjectSheet: true }); const gpo = makeSheet('GPOs', [ ['Title', 'Status', 'Source: Scan'], ['Policy', 'Fail', 'GPOs · 2026-09-07'] ], '', 'synthetic', { toolId: 'group-policy' }); const dataset = { sheets: [aggregate, gpo], scanTool: 'Multiple Tools' } as never; const facets = deriveDynamicFacets(gpo, dataset, genericProfile, {}); const sourceScan = facets.at(-1); expect(sourceScan).toMatchObject({ id: 'provenance:source-scan', label: 'Source: Scan' }); expect(sourceScan?.values.map(value => value.label)).toEqual([ 'GPOs · 2026-09-07', 'Maester · 2026-09-05', 'PingCastle · 2026-09-06' ]); expect(sourceScanNavigationSheet(dataset, gpo)?.id).toBe(aggregate.id); expect(sourceScanNavigationSheet(dataset, aggregate)).toBeUndefined(); const recounted = recountFacetsAgainstActiveFilters({ facets, records: gpo.analysis.records, filters: {}, reviews: {}, statusFilter: 'all', matchesStatus: () => true }); expect(recounted.at(-1)?.values).toEqual(sourceScan?.values); }); it('derives and applies a hierarchical CIS category filter', () => { const cisSheet = makeSheet('assessmentResultTable', [ ['Rule ID', 'Benchmark Item', 'Result'], ['17.2.4', '17.2.4 Ensure account management auditing', 'Fail'], ['17.8.1', '17.8.1 Ensure privilege use auditing', 'Fail'], ['18.5.5.1', '18.5.5.1 Ensure a network policy', 'Fail'] ], '', 'cis-cat-synthetic.html', { toolId: 'cis-cat' }); const facets = deriveDynamicFacets(cisSheet, null, cisCatProfile, {}); const facet = facets.find(item => item.id === 'derived:cis-benchmark-category'); expect(facet?.label).toBe('CIS Category'); expect(facet?.values.map(value => value.label)).toEqual(expect.arrayContaining([ 'CIS 17', 'CIS 17.2', 'CIS 17.8', 'CIS 18', 'CIS 18.5', 'CIS 18.5.5' ])); const records = filterAndSortRecords(cisSheet, { activeProfile: cisCatProfile, search: '', sortColumn: 'all', sortDirection: 'desc', statusFilter: 'all', facetFilters: { [facet!.id]: { values: ['cis 17.2'] } }, actionOnly: false, reviews: {} }); expect(records.map(record => record.title)).toEqual(['17.2.4 Ensure account management auditing']); }); });