import { describe, expect, it } from 'vitest'; import type { FindingRecord } from '../../src/domain/finding'; import { findingOutcomeMark, promotedResultSheetTitlesFor, resultMessageFor, resultSectionsFor, resultSheetRowHighlightedFor, resultSheetTitleFor, resultStatusFieldFor, showResultSheetTitlesFor } from '../../src/features/linked-results/result-message'; function maesterRecord(fields: Array<[string, string]>): FindingRecord { const built = fields.map(([label, value], index) => ({ label, value, index })); return { id: 'record', index: 0, title: '', fields: built, filled: built, meta: [] }; } describe('resultMessageFor', () => { it('liest den Maester-Ergebnistext aus der Spalte "Result"', () => { const record = maesterRecord([ ['ID', 'MT.1011'], ['Status', 'Failed'], ['Result', 'No conditional access policy securing security info registration.'], ['Source: Scan', 'Maester · 2026-07-27T00:00:00.000Z'] ]); expect(resultMessageFor(record, 'maester')).toBe('No conditional access policy securing security info registration.'); }); it('liefert nichts fuer ein Toolprofil ohne resultMessageHeaders (z. B. Purple Knight)', () => { const record = maesterRecord([ ['Name', 'Some finding'], ['Result', 'IOE Found'], ['Result Message', 'Every user can register an application.'] ]); expect(resultMessageFor(record, 'purple-knight')).toBeUndefined(); }); // CIS-CAT hat keinen einzelnen werkzeuggebundenen Ergebnistext: sein Befund // steht in den Belegtabellen und in den aufgeklappten Abschnitten // (siehe resultSectionsFor), nicht in einer Ergebnisspalte. it('liefert fuer CIS-CAT keinen einzelnen Ergebnistext', () => { const record = maesterRecord([ ['CIS-CAT Details', '## Description\n\nText\n\n## Rule Result XML\n\n```xml\nfail\n```'], ['Rule Result XML', '```xml\nfail\n```'] ]); expect(resultMessageFor(record, 'cis-cat')).toBeUndefined(); }); it('liefert nichts, wenn kein Toolprofil aufgeloest werden kann', () => { const record = maesterRecord([['Result', 'Text ohne Werkzeugbezug']]); expect(resultMessageFor(record, undefined)).toBeUndefined(); }); }); describe('resultSectionsFor', () => { /** * Die Rahmenwerk-Zuordnungen hinter "More" gehoeren zu den Belegdaten und * damit in das Results-Fenster - genau wie die versteckten * Assessment-Evidence-Tabellen, die als verknuepfte Blaetter dorthin kommen. * * Das Rule Result XML ausdruecklich nicht: es bleibt ein aufklappbares * Detail im Finding (siehe `cisCatOptionalFields`), weil es nicht der Befund * ist, sondern seine maschinenlesbare Rohfassung. */ it('nimmt die Rahmenwerk-Zuordnungen auf, das Rule Result XML aber nicht', () => { const record = maesterRecord([ ['Benchmark Item', '1.1.1 Ensure the synthetic setting is enabled'], ['Description', 'Synthetic description.'], ['Control Mappings', '### CIS Controls V7.0\n\n- Control 1: Synthetic Inventory'], ['Rule Result XML', '```xml\nfail\n```'] ]); expect(resultSectionsFor(record, 'cis-cat')).toEqual([ { title: 'Control Mappings', value: '### CIS Controls V7.0\n\n- Control 1: Synthetic Inventory' } ]); }); it('behaelt den werkzeuggebundenen Ergebnistext als eigenen Abschnitt', () => { const record = maesterRecord([['Result', 'No conditional access policy securing security info registration.']]); expect(resultSectionsFor(record, 'maester')).toEqual([ { title: 'Test result', value: 'No conditional access policy securing security info registration.' } ]); }); it('strukturiert einen Maester-Graphfehler, ohne den Originaltext zu verlieren', () => { const raw = 'Error. An error occurred while running the test. Invoke-MgGraphRequest: /synthetic/Invoke-MgGraphRequestCache.ps1:49 | GET https://graph.microsoft.com/v1.0/example HTTP/2.0 403 Forbidden Cache-Control: no-cache | {"error":{"code":"Authorization_RequestDenied","message":"Synthetic permission denied.","innerError":{"date":"2026-09-03T10:00:00Z","request-id":"request-synthetic","client-request-id":"client-synthetic"}}}'; const [section] = resultSectionsFor(maesterRecord([['Result', raw]]), 'maester'); expect(section.diagnostic).toMatchObject({ message: 'Synthetic permission denied.', code: 'Authorization_RequestDenied', method: 'GET', endpoint: 'https://graph.microsoft.com/v1.0/example', httpStatus: '403 Forbidden', source: 'Invoke-MgGraphRequestCache.ps1 · line 49', requestId: 'request-synthetic', clientRequestId: 'client-synthetic' }); expect(section.diagnostic?.raw).toBe(raw); }); it('liefert nichts fuer ein Toolprofil ohne Ergebnisabschnitte (z. B. Purple Knight)', () => { const record = maesterRecord([['Name', 'Some finding'], ['Result', 'IOE Found']]); expect(resultSectionsFor(record, 'purple-knight')).toEqual([]); }); }); describe('showResultSheetTitlesFor', () => { it('aktiviert fachliche Belegueberschriften fuer CIS-CAT und PingCastle', () => { expect(showResultSheetTitlesFor('cis-cat')).toBe(true); expect(showResultSheetTitlesFor('pingcastle')).toBe(true); expect(showResultSheetTitlesFor('maester')).toBe(false); expect(showResultSheetTitlesFor(undefined)).toBe(false); expect(resultSheetTitleFor('cis-cat', 'Assessment Evidence \u00b7 Complex Check')).toBe('Assessment'); expect(resultSheetTitleFor('cis-cat', 'CIS Controls V7.0 \u00b7 CIS Control Information')).toBe('CIS Controls V7.0'); expect(resultSheetTitleFor('cis-cat', 'CIS Critical Security Controls V8.0 \u00b7 CIS Control Information')).toBe('CIS Critical Security Controls V8.0'); expect(resultSheetTitleFor('cis-cat', 'Synthetic Framework V1.0 \u00b7 Synthetic Framework Information')).toBe('Synthetic Framework V1.0'); expect(resultSheetTitleFor('pingcastle', 'PingCastle \u00b7 Admin Groups → Administrators')).toBe('Admin Groups → Administrators'); expect(resultSheetTitleFor('maester', 'Assessment Evidence')).toBeUndefined(); }); }); describe('resultSheetRowHighlightedFor', () => { it('markiert den CIS-CAT-Hinweis, aber nicht denselben Text bei anderen Werkzeugen', () => { const record = maesterRecord([['Hinweis', 'No matching system items were found.']]); expect(resultSheetRowHighlightedFor('cis-cat', record)).toBe(true); expect(resultSheetRowHighlightedFor('maester', record)).toBe(false); expect(resultSheetRowHighlightedFor(undefined, record)).toBe(false); }); }); describe('resultStatusFieldFor', () => { it.each(['Passed', 'Failed'])('bevorzugt bei Purple Knight Status=%s vor einem SI-Verweis in Result', status => { const record = maesterRecord([ ['Result', 'SI000067'], ['Status', status], ['ShortName', 'SI000067'] ]); expect(resultStatusFieldFor(record, 'purple-knight')).toMatchObject({ label: 'Status', value: status }); }); it('bleibt mit Purple-Knight-Exporten kompatibel, die nur Result als Status fuehren', () => { const record = maesterRecord([['Result', 'Passed'], ['Short Name', 'SI000067']]); expect(resultStatusFieldFor(record, 'purple-knight')).toMatchObject({ label: 'Result', value: 'Passed' }); }); }); it.each([['IOE Found', 'Failed'], ['Pass', 'Passed']])('beschriftet den Purple-Knight-Rohstatus %s als %s', (source, expected) => { const record = maesterRecord([['Status', source], ['Result', 'SI000067']]); expect(resultStatusFieldFor(record, 'purple-knight')).toMatchObject({ label: 'Status', value: expected }); }); describe('findingOutcomeMark', () => { it('traegt bei Defender-Schwachstellen ohne Statusspalte den Ausfall-Outcome nach', () => { const record = maesterRecord([ ['CVE', 'CVE-2026-25786'], ['Severity', 'Critical'], ['Beschreibung', 'Synthetic description'] ]); expect(findingOutcomeMark(record, 'defender-for-iot-vulnerabilities')).toEqual({ outcome: 'warning', label: 'Warning' }); }); it('behaelt das Quell-Statuswort, wo das Werkzeug eine Statusspalte fuehrt', () => { const record = maesterRecord([['Status', 'IOE Found'], ['Result', 'SI000067']]); expect(findingOutcomeMark(record, 'purple-knight')).toEqual({ outcome: 'fail', label: 'Failed' }); }); it('bleibt ohne Statusfeld und ohne deklarierten Ausfall-Outcome leer', () => { const record = maesterRecord([['ID', 'MT.1011'], ['Name', 'Some finding']]); expect(findingOutcomeMark(record, 'maester')).toBeUndefined(); }); }); describe('promotedResultSheetTitlesFor', () => { it('ordnet mehrere Controls derselben Framework-Ueberschrift ihren Tabellen in Reihenfolge zu', () => { const sections = [{ title: 'Control Mappings', value: [ '### CIS Controls V7.0', '', '- Control 9: Limitation and Control of Network Ports, Protocols, and Services', '', '- Control 11: Secure Configuration for Network Devices', '', '### CIS Critical Security Controls V8.0', '', '- Control 4: Secure Configuration of Enterprise Assets and Software' ].join('\n') }]; const sheets = [ { id: 'v7-9', name: 'CIS Controls V7.0 \u00b7 CIS Control Information' }, { id: 'v7-11', name: 'CIS Controls V7.0 \u00b7 CIS Control Information 2' }, { id: 'v8-4', name: 'CIS Critical Security Controls V8.0 \u00b7 CIS Control Information' } ]; const promoted = promotedResultSheetTitlesFor('cis-cat', sections, sheets); expect([...promoted.tableTitles]).toEqual([ ['v7-9', 'Control 9: Limitation and Control of Network Ports, Protocols, and Services'], ['v7-11', 'Control 11: Secure Configuration for Network Devices'], ['v8-4', 'Control 4: Secure Configuration of Enterprise Assets and Software'] ]); expect([...promoted.consumedSectionIndexes]).toEqual([0]); }); it('laesst die urspruengliche Liste sichtbar, wenn eine zugehoerige Tabelle fehlt', () => { const sections = [{ title: 'Control Mappings', value: '### CIS Controls V7.0\n\n- Control 9: First\n\n- Control 11: Second' }]; const promoted = promotedResultSheetTitlesFor('cis-cat', sections, [ { id: 'only-one', name: 'CIS Controls V7.0 \u00b7 CIS Control Information' } ]); expect(promoted.tableTitles.size).toBe(0); expect(promoted.consumedSectionIndexes.size).toBe(0); }); });