import type { JsonValue } from '../domain/canonical-model'; import type { DecodedObjectView, DecodedSourceDocument } from '../domain/import-intermediate'; import type { FindingCandidate, SourceExtractor, SourceFieldCandidate } from '../domain/source-extraction'; import { stringifyCell } from '../shared/text'; import { cisCatSourceExtractor } from './cis-cat-source-extractor'; import { pingCastleSourceExtractor } from './pingcastle-source-extractor'; import { maesterHtmlSourceExtractor } from './maester-html-source-extractor'; import { purpleKnightHtmlSourceExtractor } from './purple-knight-html-source-extractor'; function safeId(value: string): string { return value.replace(/[^a-zA-Z0-9_-]/g, '-'); } function objectFields( fileName: string, value: JsonValue, objectPath: string ): SourceFieldCandidate[] { const entries = value && typeof value === 'object' && !Array.isArray(value) ? Object.entries(value) : [['value', value] as const]; return entries.map(([key, item]) => ({ key, label: key, originalValue: item, decodedValue: stringifyCell(item), source: { fileName, objectPath: `${objectPath}.${key}` } })); } function objectCandidates(document: DecodedSourceDocument, view: DecodedObjectView): FindingCandidate[] { const values = Array.isArray(view.value) ? view.value : [view.value]; return values.map((value, index) => { const objectPath = Array.isArray(view.value) ? `$[${index}]` : '$'; return { version: 1, id: `candidate-${safeId(view.id)}-${index}`, source: { fileName: document.file.name, objectPath }, fields: objectFields(document.file.name, value, objectPath) }; }); } export const nativeObjectSourceExtractor: SourceExtractor = { id: 'native-object-source-extractor', version: '1.0.0', detect(document) { const count = document.views.filter(view => view.kind === 'object').length; return count ? { score: 1, evidence: [`${count} native Objekt- oder Arrayansicht(en) vorhanden.`] } : { score: 0, evidence: [] }; }, extract(document) { const candidates = document.views .filter((view): view is DecodedObjectView => view.kind === 'object') .flatMap(view => objectCandidates(document, view)); return { version: 1, candidates, diagnostics: [] }; } }; export const genericTableSourceExtractor: SourceExtractor = { id: 'generic-table-source-extractor', version: '1.0.0', fallback: true, detect(document) { const count = document.tables.length; return count ? { score: 0.1, evidence: [`${count} kompatible Tabellenansicht(en) vorhanden.`] } : { score: 0, evidence: [] }; }, extract(document) { const candidates = document.tables.flatMap(table => { const header = table.headerCandidates[0]; return table.rows .filter(row => row.index !== header?.rowIndex && row.cells.some(cell => cell.decodedValue.trim())) .map(row => ({ version: 1 as const, id: `candidate-${safeId(table.id)}-${row.index}`, source: { fileName: document.file.name, tableIndex: table.index, rowIndex: row.index }, fields: row.cells.map(cell => ({ key: `column-${cell.source.columnIndex ?? 0}`, label: header?.headers[cell.source.columnIndex ?? 0] || `Spalte ${(cell.source.columnIndex ?? 0) + 1}`, originalValue: cell.rawValue, decodedValue: cell.decodedValue, source: { ...cell.source, fileName: document.file.name } })) })); }); return { version: 1, candidates, diagnostics: [] }; } }; export const sourceExtractors: SourceExtractor[] = [ pingCastleSourceExtractor, cisCatSourceExtractor, purpleKnightHtmlSourceExtractor, maesterHtmlSourceExtractor, nativeObjectSourceExtractor, genericTableSourceExtractor ];