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);
});
});