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');
});
it('shows the canonical Source Rule ID once in the Work reader', () => {
const record: FindingRecord = {
...maesterRecord,
fields: maesterRecord.fields.map(field => field.label === 'ID'
? { ...field, label: 'Source Rule ID' }
: field)
};
const selected = selectFindingFields(record, record.title, false, 'maester');
expect(selected.sourceRuleId?.value).toBe('MT.1011');
expect(selected.summary.map(field => field.label)).not.toContain('ID');
expect(selected.remainingFields.map(field => field.label)).not.toContain('Source Rule ID');
const html = renderToStaticMarkup(createElement(FindingReader, {
record, title: record.title, toolId: 'maester', linkedSheets: [], resultReferences: [], search: '',
position: 0, recordCount: 1, onPrevious() {}, onNext() {}, onReview() {}
}));
expect(html).toContain('data-source-rule-id="true"');
expect(html).toContain('
Source Rule ID
MT.1011
');
});
it('presents the retained Maester fields in the native report hierarchy without duplicating the ID in the title', () => {
const sourcePath = 'C:\\Repo\\Customer\\Maester\\tests\\cisa\\exchange\\Test-MtCisaAuditLogRetention.Tests.ps1';
const cisaRecord: FindingRecord = {
id: 'maester-cisa-exo-17-3', index: 0, filled: [], meta: [],
title: 'CISA.MS.EXO.17.3: Audit logs SHALL be maintained for at least the minimum duration dictated by OMB M-21-31 (Appendix C).',
fields: [
{ index: 0, label: 'ID', value: 'CISA.MS.EXO.17.3' },
{ index: 1, label: 'Title', value: 'Audit logs SHALL be maintained for at least the minimum duration dictated by OMB M-21-31 (Appendix C).' },
{ index: 2, label: 'Result', value: 'Failed' },
{ index: 3, label: 'Severity', value: 'Medium' },
{ index: 4, label: 'Tag', value: '["MS.EXO","MS.EXO.17.3","CISA.MS.EXO.17.3","CISA","Security","All"]' },
{ index: 5, label: 'Block', value: 'CISA' },
{ index: 6, label: 'Duration', value: '00:00:00' },
{ index: 7, label: 'ErrorMessage', value: '["Synthetic execution detail"]' },
{ index: 8, label: 'Name', value: 'CISA.MS.EXO.17.3: Audit logs SHALL be maintained for at least the minimum duration dictated by OMB M-21-31 (Appendix C).' },
{ index: 9, label: 'TestScriptFile', value: sourcePath },
{ index: 10, label: 'TestScript', value: '$result = Test-MtCisaAuditLogRetention\n\nif ($null -ne $result) {\n $result | Should -Be $true -Because "enabled."\n}' },
{ index: 11, label: 'Index', value: '63' }
]
};
const title = maesterFindingTitle(cisaRecord, cisaRecord.title);
const selected = selectFindingFields(cisaRecord, title, false, 'maester');
expect(title).toBe('Audit logs SHALL be maintained for at least the minimum duration dictated by OMB M-21-31 (Appendix C).');
expect(selected.summary.map(field => field.label)).toEqual(['ID', 'Test result', 'Test details', 'Category', 'Tags', 'Source']);
expect(selected.summary.find(field => field.label === 'ID')?.value).toBe('CISA.MS.EXO.17.3');
expect(selected.summary.find(field => field.label === 'Test result')?.value).toBe('Test failed.');
expect(selected.summary.find(field => field.label === 'Tags')?.value).toBe('MS.EXO · MS.EXO.17.3 · CISA.MS.EXO.17.3 · CISA · Security · All');
expect(selected.optionalFields).toContainEqual(expect.objectContaining({ label: 'Technical execution detail', value: 'Synthetic execution detail' }));
expect(selected.remainingFields.map(field => field.label)).not.toEqual(expect.arrayContaining([
'ID', 'Block', 'Duration', 'ErrorMessage', 'Name', 'TestScriptFile', 'TestScript', 'Index'
]));
const html = renderToStaticMarkup(createElement(FindingReader, {
record: cisaRecord, title, toolId: 'maester', linkedSheets: [], resultReferences: ['Failed'], search: '',
position: 0, recordCount: 1, onPrevious() {}, onNext() {}, onReview() {}
}));
expect(html).toContain('
Audit logs SHALL be maintained');
expect(html).not.toContain('CISA.MS.EXO.17.3:');
expect(html).toContain('ID
CISA.MS.EXO.17.3
');
expect(html).toContain('
Test result
Test failed.
');
expect(html).toContain('
Test details
');
expect(html).toContain('class="rich-text-code"');
expect(html).toContain('
Category
CISA
');
expect(html).toContain('
Tags
');
expect(html).toContain('
Source
');
expect(html).toContain('Technical execution detail');
expect(html).not.toContain('Results (reference found)');
});
});