Assessor_2.0/tests/profiles/result-linker.test.ts

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([]);
});
});