197 lines
7.8 KiB
TypeScript
197 lines
7.8 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { makeSheet } from '../../src/importers/sheet-factory';
|
|
import { isFileReference, linkResultSheets, linkedSheetScore, resultReferenceFields } from '../../src/profiles/purple-knight/result-linker';
|
|
|
|
describe('Purple Knight result linking', () => {
|
|
it('links an exact referenced result file', () => {
|
|
const findings = makeSheet(
|
|
'Indicator Results',
|
|
[['Name', 'Results'], ['Synthetic indicator', 'SYN-001_results.csv']],
|
|
'',
|
|
'assessment.xlsx'
|
|
);
|
|
const results = makeSheet('SYN-001_results.csv', [['Object', 'Value'], ['Synthetic', '1']], '', 'SYN-001_results.csv');
|
|
const record = findings.analysis.records[0];
|
|
expect(linkedSheetScore(results, ['SYN-001_results.csv'], ['SYN-001'])).toBe(120);
|
|
expect(linkResultSheets(record, [findings, results], findings.id)).toEqual([results]);
|
|
});
|
|
|
|
it('does not treat a PingCastle-style "Details" field as an external result reference', () => {
|
|
const sheet = makeSheet(
|
|
'PingCastle rule',
|
|
[['Rule ID', 'Details'], ['S-Synthetic', 'The detail can be found in the Synthetic User List']],
|
|
'',
|
|
'ad_hc_synthetic.example.html'
|
|
);
|
|
const record = sheet.analysis.records[0];
|
|
expect(resultReferenceFields(record)).toEqual([]);
|
|
});
|
|
|
|
it('recognises a real file name or path as a file reference', () => {
|
|
expect(isFileReference('SYN-001_results.csv')).toBe(true);
|
|
expect(isFileReference('exports/SYN-001_results.json')).toBe(true);
|
|
});
|
|
|
|
it('does not treat a plain result sentence as a file reference', () => {
|
|
// Maester schreibt seine Testergebnismeldung in dasselbe Feld, das bei
|
|
// Purple Knight einen Dateinamen enthaelt (siehe resultReferenceFields).
|
|
expect(isFileReference('No conditional access policy securing security info registration.')).toBe(false);
|
|
});
|
|
|
|
it('does not link a CIS-CAT sheet into a Maester finding', () => {
|
|
const findings = makeSheet(
|
|
'Maester findings',
|
|
[['Title', 'Result'], ['Synthetic MFA finding', 'No matching synthetic policy was found.']],
|
|
'',
|
|
'maester-synthetic.html',
|
|
{ toolId: 'maester' }
|
|
);
|
|
const cisCatSheet = makeSheet(
|
|
'CIS-CAT Pro · 2026-07-31 · Synthetic Policy',
|
|
[['Note'], ['No matching synthetic policy was found.']],
|
|
'',
|
|
'cis-cat-synthetic.html',
|
|
{ toolId: 'cis-cat' }
|
|
);
|
|
|
|
expect(linkResultSheets(findings.analysis.records[0], [findings, cisCatSheet], findings.id)).toEqual([]);
|
|
});
|
|
|
|
it('does not fuzzy-link Maester result prose to an unrelated Maester sheet', () => {
|
|
const findings = makeSheet(
|
|
'Maester findings',
|
|
[['Title', 'Result'], ['Synthetic MFA finding', 'No matching synthetic policy was found.']],
|
|
'',
|
|
'maester-synthetic.html',
|
|
{ toolId: 'maester' }
|
|
);
|
|
const unrelated = makeSheet(
|
|
'No matching synthetic policy was found',
|
|
[['Value'], ['Unrelated source table']],
|
|
'',
|
|
'maester-synthetic.html',
|
|
{ toolId: 'maester' }
|
|
);
|
|
|
|
expect(linkResultSheets(findings.analysis.records[0], [findings, unrelated], findings.id)).toEqual([]);
|
|
});
|
|
|
|
it('keeps result links within the record tool in a mixed project sheet', () => {
|
|
const findings = makeSheet(
|
|
'Synthetic project · All Records',
|
|
[
|
|
['Source: Scan', 'Title', 'Result'],
|
|
['Maester · 2026-07-31', 'Synthetic Maester finding', 'SI000055']
|
|
],
|
|
'',
|
|
'Synthetic project',
|
|
{ isProjectSheet: true }
|
|
);
|
|
const maesterSheet = makeSheet('SI000055', [['Value'], ['Maester evidence']], '', 'maester-synthetic.html', {
|
|
toolId: 'maester'
|
|
});
|
|
const cisCatSheet = makeSheet('SI000055', [['Value'], ['CIS-CAT evidence']], '', 'cis-cat-synthetic.html', {
|
|
toolId: 'cis-cat'
|
|
});
|
|
|
|
expect(linkResultSheets(findings.analysis.records[0], [findings, cisCatSheet, maesterSheet], findings.id))
|
|
.toEqual([maesterSheet]);
|
|
});
|
|
|
|
it('links every CIS-CAT source table referenced by the same imported HTML rule', () => {
|
|
const findings = makeSheet(
|
|
'assessmentResultTable',
|
|
[
|
|
['Benchmark Item', 'Results'],
|
|
['1.1.1 Ensure the synthetic setting is enabled', [
|
|
'source-table:3 "Assessment Evidence · Synthetic Item"',
|
|
'source-table:4 "CIS Controls V7.0 · CIS Control Information"'
|
|
].join('\n')]
|
|
],
|
|
'',
|
|
'CIS-CAT-SYNTHETIC.html',
|
|
{ sourceTableIndex: 2 }
|
|
);
|
|
const evidence = makeSheet('Synthetic Item', [['Name', 'Value'], ['Setting', 'Disabled']], '', 'CIS-CAT-SYNTHETIC.html', {
|
|
sourceTableIndex: 3
|
|
});
|
|
const controls = makeSheet('CIS Control Information', [['Control', 'Value'], ['Control', 'Synthetic']], '', 'CIS-CAT-SYNTHETIC.html', {
|
|
sourceTableIndex: 4
|
|
});
|
|
const unrelated = makeSheet('Synthetic Item', [['Name', 'Value'], ['Other', 'Tenant']], '', 'other.html', {
|
|
sourceTableIndex: 3
|
|
});
|
|
|
|
expect(linkResultSheets(findings.analysis.records[0], [findings, evidence, controls, unrelated], findings.id))
|
|
.toMatchObject([
|
|
{ name: 'Assessment Evidence · Synthetic Item', sourceFile: 'CIS-CAT-SYNTHETIC.html' },
|
|
{ name: 'CIS Controls V7.0 · CIS Control Information', sourceFile: 'CIS-CAT-SYNTHETIC.html' }
|
|
]);
|
|
});
|
|
|
|
/**
|
|
* Regression: In der Projektansicht ("<Projekt> · All Records") ist
|
|
* `record.sourceFile` der Projektname, nicht die Quelldatei - die steht dort
|
|
* als Provenienzfeld "Source: File". Der exakte Verweis `source-table:N`
|
|
* fand deshalb kein Blatt und fiel auf die Namensaehnlichkeit zurueck. Da
|
|
* CIS-CAT dieselbe Belegtabelle je Regel erneut ausgibt, zog diese
|
|
* Notloesung jede gleichnamige Tabelle des gesamten Berichts in das
|
|
* Ergebnisfenster: aus einer Tabelle wurden Dutzende mit tausenden Zeilen.
|
|
*/
|
|
it('folgt dem Quelltabellenverweis auch in der Projektansicht', () => {
|
|
const findings = makeSheet(
|
|
'Dummy · All Records',
|
|
[
|
|
['Source: File', 'Benchmark Item', 'Results'],
|
|
[
|
|
'CIS-CAT-SYNTHETIC.html',
|
|
'17.7.5 Ensure the synthetic audit setting is enabled',
|
|
'source-table:3 "Assessment Evidence · Audit Item"'
|
|
]
|
|
],
|
|
'',
|
|
'Dummy',
|
|
{ isProjectSheet: true }
|
|
);
|
|
const evidenceRows = [['Name', 'Value'], ['Other Policy Change Events', 'AUDIT_NONE']];
|
|
// Dieselbe Tabelle, wie der Bericht sie je Regel erneut ausgibt.
|
|
const sheetsForEveryRule = [3, 9, 15].map(sourceTableIndex => makeSheet(
|
|
'CIS-CAT Pro · 2026-07-30 · Audit Item',
|
|
evidenceRows,
|
|
'',
|
|
'CIS-CAT-SYNTHETIC.html',
|
|
{ sourceSheet: 'Audit Item', sourceTableIndex }
|
|
));
|
|
|
|
const linked = linkResultSheets(findings.analysis.records[0], [findings, ...sheetsForEveryRule], findings.id);
|
|
|
|
expect(linked).toHaveLength(1);
|
|
expect(linked[0]).toMatchObject({ name: 'Assessment Evidence · Audit Item', sourceTableIndex: 3 });
|
|
});
|
|
|
|
/**
|
|
* Ein expliziter Verweis ist abschliessend: loest er sich nicht auf, ist das
|
|
* Ergebnis leer. Die Namensaehnlichkeit darf nicht einspringen und
|
|
* irgendwelche gleichnamigen Tabellen anbieten - der Reader weist stattdessen
|
|
* auf die fehlende Ergebnisdatei hin.
|
|
*/
|
|
it('erfindet keinen Ersatz, wenn ein Quelltabellenverweis ins Leere zeigt', () => {
|
|
const findings = makeSheet(
|
|
'Dummy · All Records',
|
|
[
|
|
['Source: File', 'Benchmark Item', 'Results'],
|
|
['CIS-CAT-SYNTHETIC.html', '17.7.5 Synthetic', 'source-table:99 "Assessment Evidence · Audit Item"']
|
|
],
|
|
'',
|
|
'Dummy',
|
|
{ isProjectSheet: true }
|
|
);
|
|
const evidence = makeSheet('CIS-CAT Pro · 2026-07-30 · Audit Item', [['Name'], ['Value']], '', 'CIS-CAT-SYNTHETIC.html', {
|
|
sourceSheet: 'Audit Item',
|
|
sourceTableIndex: 3
|
|
});
|
|
|
|
expect(linkResultSheets(findings.analysis.records[0], [findings, evidence], findings.id)).toEqual([]);
|
|
});
|
|
});
|