Failed');
expect(html).not.toContain('data-outcome="unknown">SI000067');
});
it('shows GPO customer and baseline values as one readable comparison', () => {
const policyRecord: FindingRecord = {
id: 'gpo-comparison', index: 0, title: 'Synthetic policy setting', filled: [], meta: [],
fields: [
{ index: 0, label: 'Title', value: 'Synthetic policy setting' },
{ index: 1, label: 'Status', value: 'Failed' },
{ index: 2, label: 'Configured Value', value: '[{"kind":"registry-policy","valueName":"SyntheticSetting","data":{"kind":"number","value":12},"operation":"set-value","typeCode":4}]' },
{ index: 3, label: 'Baseline value', value: '{"kind":"number","value":14}' },
{ index: 4, label: 'Baseline', value: 'Synthetic Microsoft Baseline' }
]
};
const selected = selectFindingFields(policyRecord, policyRecord.title, false, 'group-policy');
const html = renderToStaticMarkup(createElement(FindingReader, {
record: policyRecord, title: policyRecord.title, linkedSheets: [], resultReferences: [], search: '',
position: 0, recordCount: 1, toolId: 'group-policy', onPrevious: () => undefined,
onNext: () => undefined, onReview: () => undefined
}));
expect(selected.policyComparisonFields.map(field => field.label)).toEqual(['Configured Value', 'Baseline value']);
expect(selected.summary.map(field => field.label)).not.toEqual(expect.arrayContaining(['Configured Value', 'Baseline value']));
expect(html).toContain('class="policy-value-comparison" data-outcome="fail"');
expect(html).toContain('Configured customer value');
expect(html).toContain('Expected baseline value');
expect(html).toContain('
Policy Analyzer search keySyntheticSetting
');
expect(policyAnalyzerSearchKeys(policyRecord, policyRecord.title, 'group-policy')).toEqual(['SyntheticSetting']);
expect(html).toContain('
SyntheticSetting
');
expect(html).toContain('
Data: 12');
expect(html).toContain('
Operation: set-value');
expect(html).toContain('\nType Code: 4');
expect(html).toContain('
14
');
expect(html).not.toContain('Value Name: SyntheticSetting');
expect(html).not.toContain('"kind"');
});
it('derives a Policy Analyzer key from a technical GPO title without changing the title and omits preferences', () => {
const technical: FindingRecord = {
id: 'gpo-technical', index: 0,
title: 'Registry Values · MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\InactivityTimeoutSecs',
filled: [], meta: [], fields: [
{ index: 0, label: 'Title', value: 'Registry Values · MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\InactivityTimeoutSecs' },
{ index: 1, label: 'Status', value: 'Fail' },
{ index: 2, label: 'Policy type', value: 'security-template' }
]
};
expect(policyAnalyzerSearchKeys(technical, technical.title, 'group-policy')).toEqual(['InactivityTimeoutSecs']);
const preference: FindingRecord = {
...technical,
id: 'gpo-preference',
title: 'registry · HKEY_CURRENT_USER\\Software\\Microsoft\\Office\\16.0\\Outlook\\AutoDiscover\\ExcludeScpLookup',
fields: [
{ index: 0, label: 'Title', value: 'registry · HKEY_CURRENT_USER\\Software\\Microsoft\\Office\\16.0\\Outlook\\AutoDiscover\\ExcludeScpLookup' },
{ index: 1, label: 'Status', value: 'Not comparable' },
{ index: 2, label: 'Policy type', value: 'group-policy-preference' },
{ index: 3, label: 'Configured Value', value: '[{"kind":"registry-preference","key":"Software\\\\Microsoft\\\\Office\\\\16.0\\\\Outlook\\\\AutoDiscover","valueName":"ExcludeScpLookup"}]' }
]
};
expect(policyAnalyzerSearchKeys(preference, preference.title, 'group-policy')).toEqual(['AutoDiscover', 'ExcludeScpLookup']);
});
it('ordnet auch die alte PurpleKnight-Schreibweise Short Name den Zusatzinformationen zu', () => {
const legacyShortName: FindingRecord = {
...record,
fields: record.fields.map(field => field.label === 'ShortName' ? { ...field, label: 'Short Name' } : field)
};
const selected = selectFindingFields(legacyShortName, legacyShortName.title, false, 'purple-knight');
expect(selected.summary.map(field => field.label)).not.toContain('Short Name');
expect(selected.remainingFields.map(field => field.label)).toContain('Short Name');
});
it('verschiebt alte PurpleKnight-HTML-Details in Results und loest Framework-JSON auf', () => {
const legacy: FindingRecord = {
...record,
id: 'purple-knight-legacy-html',
fields: [
{ index: 0, label: 'Name', value: 'Synthetic legacy indicator' },
{ index: 1, label: 'Status', value: 'IOE Found' },
{ index: 2, label: 'ResultDetail', value: '[["Object: Synthetic","Ignored: False"]]' },
{ index: 3, label: 'Framework', value: JSON.stringify([
{ Name: 'MITRE ATT&CK', Tags: ['Discovery'] },
{ Name: 'MITRE D3FEND', Tags: ['Harden'] },
{ Name: 'ANSSI', Tags: ['vuln_synthetic'] }
]) },
{ index: 4, label: 'Grade', value: 'F' },
{ index: 5, label: 'Exposure Type', value: 'IOE' }
]
};
const selected = selectFindingFields(legacy, legacy.title, true, 'purple-knight');
expect(selected.mitre.map(field => [field.label, field.value])).toEqual([
['MITRE ATT&CK_1', 'Discovery'], ['MITRE D3FEND_1', 'Harden']
]);
expect(selected.remainingFields.map(field => [field.label, field.value])).toContainEqual(['ANSSI_1', 'vuln_synthetic']);
expect([...selected.summary, ...selected.remainingFields].map(field => field.label)).not.toEqual(
expect.arrayContaining(['ResultDetail', 'Framework', 'Grade', 'Exposure Type'])
);
});
it('shows PingCastle rule points and report scores as badges while leaving score formulas readable', () => {
const scoringRecord: FindingRecord = {
...record,
fields: [
...record.fields,
{ index: 11, label: 'Points', value: '20' },
{ index: 12, label: 'Category Score', value: '85' },
{ index: 13, label: 'Domain Risk Level', value: '100' },
{ index: 14, label: 'Scoring rule', value: '20 points if present' }
]
};
const selected = selectFindingFields(scoringRecord, scoringRecord.title);
expect(selected.badges.map(field => field.label)).toEqual([
'Severity',
'Points',
'Category Score',
'Domain Risk Level'
]);
expect(selected.summary.map(field => field.label)).toContain('Scoring rule');
});
it('hides PingCastle Details from reader text and Additional Information when structured Results exist', () => {
const pingCastleRecord: FindingRecord = {
id: 'pingcastle-detail-layout',
index: 0,
title: 'Synthetic PingCastle finding',
fields: [
{ index: 0, label: 'Rule ID', value: 'S-Synthetic' },
{ index: 1, label: 'Title', value: 'Synthetic PingCastle finding' },
{ index: 2, label: 'Status', value: 'Matched' },
{ index: 3, label: 'Description', value: 'Synthetic description.' },
{ index: 4, label: 'Technical explanation', value: 'Synthetic technical explanation.' },
{ index: 5, label: 'Advised solution', value: 'Synthetic advised solution.' },
{ index: 6, label: 'Details', value: 'Readable synthetic detail.\nSecond detail line.' },
{ index: 7, label: 'Points Scale', value: '10 points' },
{ index: 8, label: 'Verwandte Daten', value: 'file-synthetic::rule-details-S-Synthetic' }
],
filled: [],
meta: []
};
const selected = selectFindingFields(pingCastleRecord, pingCastleRecord.title, true, 'pingcastle');
expect(selected.summary.map(field => field.label)).toEqual([
'Description',
'Technical explanation',
'Advised solution'
]);
expect(selected.remainingFields.map(field => field.label)).toEqual(['Rule ID', 'Points Scale']);
const html = renderToStaticMarkup(createElement(FindingReader, {
record: pingCastleRecord,
title: pingCastleRecord.title,
linkedSheets: [makeSheet('Synthetic evidence', [['Field'], ['Value']], '', 'synthetic.html')],
resultReferences: [],
search: '',
position: 0,
recordCount: 1,
toolId: 'pingcastle',
onPrevious: () => undefined,
onNext: () => undefined,
onReview: () => undefined
}));
expect(html).not.toContain('Readable synthetic detail.');
expect(html).not.toContain('
Details | ');
expect(html).not.toContain('Verwandte Daten');
expect(html).not.toContain('file-synthetic::rule-details-S-Synthetic');
});
it('keeps PingCastle Details readable when no structured Results exist', () => {
const pingCastleRecord: FindingRecord = {
id: 'pingcastle-detail-fallback',
index: 0,
title: 'Synthetic PingCastle finding',
fields: [
{ index: 0, label: 'Title', value: 'Synthetic PingCastle finding' },
{ index: 1, label: 'Technical explanation', value: 'Synthetic technical explanation.' },
{ index: 2, label: 'Details', value: 'Readable fallback detail.' },
{ index: 3, label: 'Verwandte Daten', value: 'file-synthetic::rule-details-S-Synthetic' }
],
filled: [],
meta: []
};
const selected = selectFindingFields(pingCastleRecord, pingCastleRecord.title, false, 'pingcastle');
expect(selected.summary.map(field => field.label)).toEqual(['Technical explanation', 'Details']);
});
// Ergebnistabellen gehoeren ausschliesslich in das schwebende Fenster. Inline
// standen sie ein zweites Mal in der Lesespalte; ausserdem bleibt so
// ausgeschlossen, dass der Reader grosse Ergebnismengen mitrendert.
it('never renders linked result tables in the reader itself', () => {
const linkedSheet = makeSheet('Results', [
['Evidence'],
['EXPENSIVE-LINKED-RESULT-CONTENT']
], '', 'results.csv');
const html = renderToStaticMarkup(createElement(FindingReader, {
record,
title: record.title,
linkedSheets: [linkedSheet],
resultReferences: ['results.csv'],
search: '',
position: 0,
recordCount: 1,
onPrevious: () => undefined,
onNext: () => undefined,
onReview: () => undefined
}));
expect(html).not.toContain('EXPENSIVE-LINKED-RESULT-CONTENT');
expect(html).not.toContain('Verknüpfte Results');
});
// Ohne geladene Ergebnisdatei ist die Results-Schaltflaeche abgeschaltet -
// dann bleibt der Hinweis die einzige Erklaerung fuer die Referenz.
it('uses the matching Result-Fensterfarbe for an open Results button', () => {
const html = renderToStaticMarkup(createElement(FindingReader, {
record,
title: record.title,
linkedSheets: [],
resultReferences: [],
search: '',
position: 0,
recordCount: 1,
onPrevious: () => undefined,
onNext: () => undefined,
onReview: () => undefined,
onResults: () => undefined,
resultsOpen: true,
resultColor: '#059669'
}));
expect(html).toContain('results-btn is-open');
expect(html).toContain('--result-color:#059669');
});
it('keeps the hint when a reference exists but its result file is missing', () => {
const html = renderToStaticMarkup(createElement(FindingReader, {
record,
title: record.title,
linkedSheets: [],
resultReferences: ['results.csv'],
search: '',
position: 0,
recordCount: 1,
onPrevious: () => undefined,
onNext: () => undefined,
onReview: () => undefined
}));
expect(html).toContain('Results (reference found)');
});
// Maester legt seine Testergebnismeldung selbst in die Spalte "Result".
// Anders als bei Purple Knight (kurzes Statuswort) ist das hier bereits der
// vollstaendige Befund - er gehoert deshalb in das Results-Fenster (siehe
// `resultMessageFor`/`FloatingResultsWindow`), nicht ein zweites Mal in den
// Finding-Fliesstext. "Result" bleibt im Reader deshalb fuer jedes Werkzeug
// einheitlich ausgeblendet.
const maesterRecord: FindingRecord = {
id: 'finding-2',
index: 0,
title: 'At least one Conditional Access policy is configured to secure security info registration only from a trusted location.',
fields: [
{ index: 0, label: 'ID', value: 'MT.1011' },
{ index: 1, label: 'Title', value: 'At least one Conditional Access policy is configured to secure security info registration only from a trusted location.' },
{ index: 2, label: 'Severity', value: 'High' },
{ index: 3, label: 'Status', value: 'Failed' },
{ index: 4, label: 'Category', value: 'Maester/Entra' },
{ index: 5, label: 'Description', value: 'Checks if the tenant has at least one conditional access policy securing security info registration.' },
{ index: 6, label: 'Result', value: 'No conditional access policy securing security info registration.' },
{ index: 7, label: 'Tags', value: 'Maester, CA, Security, MT.1011' }
],
filled: [],
meta: []
};
it('keeps the Maester "Result" field out of the finding text and additional information', () => {
const selected = selectFindingFields(maesterRecord, maesterRecord.title);
expect(selected.summary.map(field => field.label)).not.toContain('Result');
expect(selected.remainingFields.map(field => field.label)).not.toContain('Result');
});
});