import type { DatasetSheet, ImportedDataset } from '../domain/dataset'; import type { FindingRecord } from '../domain/finding'; import type { Review } from '../domain/review'; import { toolIdForRecord } from '../importers/status-normalization'; import { recordResultSheets } from '../features/linked-results/record-results'; import { resultSectionsFor, resultStatusFieldFor } from '../features/linked-results/result-message'; import { isFileReference, resultReferenceValues } from '../profiles/purple-knight/result-linker'; import { formatPolicyComparisonValue } from '../profiles/group-policy/presentation'; import { policySourceGroups, policySourceValueLines } from '../profiles/group-policy/source-evidence'; import { parseMarkdownBlocks, type InlineToken } from '../shared/markdown'; import { rowsToSpreadsheetSafeCsv } from './csv-export'; import bundledInter from '@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?inline'; export const WORK_VIEW_EXPORT_VERSION = '1.0.0'; export const WORK_VIEW_COLUMNS = ['Soll-Zustand', 'Abweichungen'] as const; export interface WorkExportBlock { label: string; value: string; rich?: boolean; lines?: Array<{ text: string; deviating?: true }> } export interface WorkExportEntry { id: string; title: string; status: string; expected: WorkExportBlock[]; observed: WorkExportBlock[]; resultSheetIds: string[]; resultRowCount: number; } export interface WorkViewExport { version: string; generatedAt: string; context: WorkExportBlock[]; entries: WorkExportEntry[]; } export interface WorkViewExportInput { dataset: ImportedDataset; sheet: DatasetSheet; /** Already filtered and sorted by Work; never select again in an exporter. */ records: readonly FindingRecord[]; generatedAt: string; context?: WorkExportBlock[]; reviews?: Record; titleColumn?: string; } const EXPECTED = /^(?:baseline value|expected(?: value| result)?|desired(?: value| state)?|recommended value|soll(?:-zustand|wert)?)$/i; const SOLUTION = /^(?:remediation(?: guidance| recommendation| action)?|advised solution|solution|recommendation|resolution)$/i; const AUDIT = /^(?:audit(?: procedure)?|verification|verify|test procedure)$/i; const GUIDANCE = /^(?:description|rationale|impact|technical explanation|documentation|references?|notes)$/i; const CONTEXT = /^(?:id|rule id|shortname|technical identity|scope|target|registry path|baseline|policy type|policy section|imported|evaluated|source: .+|source file|source sheet)$/i; const TITLES = /^(?:title|name|finding|benchmark item|check title)$/i; const NEUTRAL = new Set(['Not configured', 'No baseline rule', 'Not comparable', 'Review required']); const GPO_SCOPE = 'Vergleich importierter GPO-Backups, kein Nachweis der wirksamen Endpoint-Konfiguration. Baseline-Varianten sind getrennte Empfehlungen; ihre Anwendbarkeit und Zielrolle vor einer Änderung bestätigen. Keine GPO-Priorität aus der Quellenreihenfolge ableiten.'; function sourceBlocks(record: FindingRecord): WorkExportBlock[] { return policySourceGroups(record.policySourceEvidence ?? []).flatMap(group => [ { label: group.label, value: [group.backupLabel, `Backup-Identität: ${group.id}`].filter(Boolean).join('\n') }, ...group.observations.map(source => { const lines = [ `Evidence: ${source.sourceReference}`, `Observation: ${source.id}`, source.sequence === undefined ? '' : `Source sequence: ${source.sequence}` ].filter(Boolean).map(text => ({ text })); const instructions = source.values.flatMap((value, index) => policySourceValueLines(value, source.deviationFields?.[index], source.deviationItems?.[index])); return { label: 'GPO-Quellbeleg', value: [...lines, ...instructions].map(line => line.text).join('\n'), lines: [...lines, ...instructions] }; }) ]); } function entryFor(input: WorkViewExportInput, record: FindingRecord): WorkExportEntry { const tool = toolIdForRecord(record, input.sheet.toolId ?? input.dataset.profileId); const gpo = tool === 'group-policy'; const statusField = resultStatusFieldFor(record, tool); const status = statusField?.value ?? 'Unknown'; const title = record.fields.find(field => field.label === input.titleColumn)?.value || record.title; const expected: WorkExportBlock[] = []; const observed: WorkExportBlock[] = [{ label: 'Result Status', value: status }]; const context = [`Finding: ${title}`, `Record ID: ${record.id}`, `Source tool: ${tool ?? 'Unknown'}`, record.findingOccurrenceId ? `Occurrence: ${record.findingOccurrenceId}` : '', record.scanRunId ? `Scan run: ${record.scanRunId}` : '', record.importFileId ? `Import file: ${record.importFileId}` : '']; const resultSheets = recordResultSheets(record, input.dataset.sheets, input.sheet.id, tool); const sections = resultSectionsFor(record, tool); const sectionValues = new Set(sections.map(section => section.value)); let hasExpected = false; let hasSolution = false; let hasAudit = false; for (const field of record.fields) { if (!field.value || TITLES.test(field.label) || field.index === statusField?.index) continue; if (CONTEXT.test(field.label)) { context.push(`${field.label}: ${field.value}`); continue; } if (sectionValues.has(field.value)) continue; if (EXPECTED.test(field.label)) { hasExpected = true; expected.push({ label: 'Sollwert · ' + field.label, value: gpo ? formatPolicyComparisonValue(field.value) : field.value }); // The reader formatter is intentionally human-facing; retain the typed original // too so empty collections, strings, zero and type codes remain unambiguous. if (gpo) expected.push({ label: 'Sollwert · typisierter Originalwert', value: field.value }); } else if (SOLUTION.test(field.label)) { hasSolution = true; expected.push({ label: 'Behebung laut Quelle · ' + field.label, value: field.value, rich: true }); } else if (AUDIT.test(field.label)) { hasAudit = true; expected.push({ label: 'Prüfung laut Quelle · ' + field.label, value: field.value, rich: true }); } else if (GUIDANCE.test(field.label) && !gpo) { // Maester retains its complete remediation within Description. Do not cut it off. expected.push({ label: 'Regel und Anleitung laut Quelle · ' + field.label, value: field.value, rich: true }); } else if (gpo && field.label === 'Configured Value') { if (!record.policySourceEvidence?.length) observed.push({ label: 'Konfiguration im Backup', value: formatPolicyComparisonValue(field.value) }); } else if (gpo && ['Sources', 'GPO conflict details', 'GPO duplicate details'].includes(field.label)) { if (!resultSheets.length && !record.policySourceEvidence?.length) observed.push({ label: field.label, value: field.value }); } else if (/^(?:results?|verwandte daten)$/i.test(field.label) && resultSheets.length) { // The matching sheets below carry these references in full, not only their filenames. } else observed.push({ label: field.label, value: field.value, rich: !gpo && /^(?:description|details?|result message)$/i.test(field.label) }); } expected.unshift({ label: 'Finding und Geltungsbereich', value: context.filter(Boolean).join('\n') }); if (gpo) expected.push({ label: 'Geltungsgrenze', value: GPO_SCOPE }); if (gpo && NEUTRAL.has(status)) { expected.push({ label: 'Einordnung', value: status === 'Not configured' ? 'Score-neutral: Empfehlung im analysierten Backup-Set nicht konfiguriert. Keine automatische Behebungspflicht; vollständiges Baseline-Hardening ist ein eigener Auftrag.' : status === 'No baseline rule' ? 'Score-neutral: Keine anwendbare Baseline-Regel vorhanden. Kein belegter Sollwert und keine daraus ableitbare Behebungspflicht.' : 'Score-neutral: Vergleich nicht abschließend möglich. Zuerst Anwendbarkeit, technische Vergleichbarkeit und Diagnostik klären; keine Abweichung erfinden.' }); } if (!hasExpected) expected.push({ label: 'Sollwert', value: 'Kein separater Sollwert hinterlegt. Maßgeblich sind die wiedergegebenen Quell-Anleitungen; es wird kein Wert aus dem Finding-Titel abgeleitet.' }); if (!hasSolution) expected.push({ label: 'Behebung', value: 'Keine separate Behebungsanleitung hinterlegt. Eingebettete Anleitung und Referenzen im Regeltext prüfen. Fehlen diese ebenfalls, ist eine fachliche Klärung erforderlich.' }); if (!hasAudit) expected.push({ label: 'Nachweis nach Änderung', value: gpo ? 'Nach bestätigter Anwendbarkeit und freigegebener Änderung neue GPO-Backups importieren und dieselbe technische Einstellung erneut auswerten. Wirksamkeit am Endpoint separat prüfen; der Backup-Vergleich beweist sie nicht.' : 'Nach freigegebener Änderung dieselbe Regel im gleichen Scope erneut mit dem Quellwerkzeug ausführen. Nur ein vergleichbarer Rescan belegt die Behebung; Review-Status und Abwesenheit eines Findings reichen nicht.' }); observed.push(...sourceBlocks(record)); const referencedTables = [...new Set(resultReferenceValues(record).flatMap(value => [...value.matchAll(/source-table:(\d+)/g)].map(match => Number(match[1]))))]; const linkedTables = new Set(resultSheets.map(sheet => sheet.sourceTableIndex)); const missingTables = referencedTables.filter(index => !linkedTables.has(index)); if (missingTables.length) observed.push({ label: 'Results · fehlende Belege', value: `Referenzierte Ergebnistabellen nicht geladen: ${missingTables.map(index => `source-table:${index}`).join(', ')}. Die verfügbaren Belege folgen; der Nachweis ist unvollständig.` }); for (const section of sections) observed.push({ label: 'Results · ' + section.title, value: section.value, rich: !/xml/i.test(section.title) }); let resultRowCount = 0; for (const sheet of resultSheets) { observed.push({ label: 'Results · ' + sheet.name, value: [`Sheet ID: ${sheet.id}`, sheet.sourceFile ? `Source: ${sheet.sourceFile}` : '', sheet.resultPreamble ?? '', `Zeilen: ${sheet.analysis.records.length}`].filter(Boolean).join('\n') }); for (const [index, row] of sheet.analysis.records.entries()) { resultRowCount++; // Vertical name/value records remain readable with arbitrarily wide source tables. observed.push({ label: `${sheet.name} · Zeile ${index + 1}${row.highlighted ? ' · vom Quellbericht hervorgehoben' : ''}`, value: sheet.analysis.headers.map(header => `${header}: ${row.fields.find(field => field.label === header)?.value ?? ''}`).join('\n') }); } } if (!resultSheets.length && !sections.length) { const refs = resultReferenceValues(record).filter(isFileReference); observed.push({ label: 'Results', value: refs.length ? `Referenzierte Results nicht geladen: ${[...new Set(refs)].join(', ')}. Diese Belege fehlen im Export; Quelldateien im selben Projekt laden.` : 'Keine zusätzlichen Results hinterlegt. Dies ist kein Nachweis, dass keine Abweichung besteht.' }); } const review = input.reviews?.[record.id]; if (review) { const plan = [review.nextStep && `Next step: ${review.nextStep}`, review.prerequisites && `Prerequisites: ${review.prerequisites}`, review.access && `Access: ${review.access}`, review.owner && `Owner: ${review.owner}`, review.due && `Due: ${review.due}`].filter(Boolean); if (plan.length) expected.push({ label: 'Geplante Maßnahme · ungeprüfte Review-Angaben', value: plan.join('\n') }); observed.push({ label: 'Review · kein technischer Behebungsnachweis', value: [ `Status: ${review.status}`, `Priority: ${review.priority}`, review.comment && `Comment: ${review.comment}`, review.evidence && `Evidence (Review): ${review.evidence}`, review.riskAcceptance ? `Risk acceptance: ${JSON.stringify(review.riskAcceptance)}` : '' ].filter(Boolean).join('\n') }); } return { id: record.id, title, status, expected, observed, resultSheetIds: resultSheets.map(sheet => sheet.id), resultRowCount }; } /** Click-time, platform-neutral, frozen projection shared by all three view formats. */ export function buildWorkViewExport(input: WorkViewExportInput): WorkViewExport { return { version: WORK_VIEW_EXPORT_VERSION, generatedAt: input.generatedAt, context: [{ label: 'Ansicht', value: `${input.dataset.fileName} · ${input.sheet.name}` }, { label: 'Exportzeitpunkt', value: input.generatedAt }, { label: 'Treffer', value: String(input.records.length) }, ...(input.context ?? []), { label: 'Umfang', value: 'Alle Treffer der aktuellen Filterung und Sortierung, einschließlich Results. Nicht passende oder noch nicht bewertete Treffer bleiben unverändert; keine Neuberechnung. Quelltexte bleiben in ihrer Originalsprache.' }], entries: input.records.map(record => entryFor(input, record)) }; } function inlineText(tokens: InlineToken[]): string { return tokens.map(token => token.kind === 'strong' || token.kind === 'emphasis' ? inlineText(token.children) : token.kind === 'link' ? `${inlineText(token.children)} (${token.href})` : token.text).join(''); } function blockText(block: WorkExportBlock): string { if (!block.rich) return block.value; return parseMarkdownBlocks(block.value).map(part => part.kind === 'code' ? part.text : part.kind === 'list' ? part.items.map((item, index) => `${part.ordered ? `${index + 1}.` : '•'} ${inlineText(item)}`).join('\n') : part.kind === 'table' ? part.table.rows.map((row, index) => `Zeile ${index + 1}\n` + part.table.headers.map((header, column) => `${header}: ${row[column] ?? ''}`).join('\n')).join('\n\n') : inlineText(part.content)).join('\n\n'); } const blocksText = (blocks: WorkExportBlock[]) => blocks.map(block => `${block.label}\n${blockText(block)}`).join('\n\n'); const escapeHtml = (value: string) => value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); const markdownCell = (value: string) => escapeHtml(value).replace(/[\\`*_{}\[\]()#!~]/g, '\\$&').replaceAll('|', '|').replace(/\r\n?|\n/g, '
'); export function workViewCsvRows(report: WorkViewExport): string[][] { // A CSV has no cell-size limit, but Excel silently loses oversized cells. Explicit // continuation rows retain two columns and the same finding ID, never new findings. const chunks = (text: string) => { const result: string[] = []; while (text.length > 30000) { let end = text.lastIndexOf('\n', 30000); if (end < 15000) end = 30000; else end += 1; if (/[\uD800-\uDBFF]/.test(text[end - 1])) end--; result.push(text.slice(0, end)); text = text.slice(end); } result.push(text); return result; }; return [[...WORK_VIEW_COLUMNS], ...report.entries.flatMap(entry => { const left = chunks(`${blocksText(report.context)}\n\n${blocksText(entry.expected)}`); const right = chunks(blocksText(entry.observed)); const count = Math.max(left.length, right.length); return Array.from({ length: count }, (_, index) => { const prefix = count > 1 ? `Finding ID: ${entry.id}\nTeil ${index + 1}/${count} · Fortsetzungszeile desselben Findings\n\n` : ''; return [prefix + (left[index] ?? ''), prefix + (right[index] ?? '')]; }); })]; } export function renderWorkViewCsv(report: WorkViewExport, delimiter = ';'): string { return '\ufeff' + rowsToSpreadsheetSafeCsv(workViewCsvRows(report), delimiter); } export function renderWorkViewMarkdown(report: WorkViewExport): string { return `# ASSESSOR · Aktuelle Work-Ansicht\n\n${markdownCell(blocksText(report.context))}\n\n| ${WORK_VIEW_COLUMNS.join(' | ')} |\n| --- | --- |\n` + report.entries.map(entry => `| ${markdownCell(blocksText(entry.expected))} | ${markdownCell(blocksText(entry.observed))} |`).join('\n') + '\n'; } export function renderWorkViewHtml(report: WorkViewExport): string { const blocks = (items: WorkExportBlock[]) => items.map(item => `

${escapeHtml(item.label)}

${item.lines ? item.lines.map(line => line.deviating ? `${escapeHtml(line.text)}` : escapeHtml(line.text)).join('\n') : escapeHtml(blockText(item))}
`).join(''); return `ASSESSOR · Work-Ansicht

ASSESSOR · Aktuelle Work-Ansicht

${blocks(report.context)}
${report.entries.map((entry, index) => `
${index + 1} / ${report.entries.length} · ${escapeHtml(entry.status)}

${escapeHtml(entry.title)}

Soll-ZustandAbweichungen
${blocks(entry.expected)}${blocks(entry.observed)}
`).join('')}
`; }