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