import type { ImportFileRecord, ToolIdentity } from './canonical-model'; export const POSSIBLE_DUPLICATE_TOLERANCE_MS = 60_000; function usableToolId(tool?: ToolIdentity): string | undefined { const id = tool?.id.trim().toLocaleLowerCase('de-DE'); return id && id !== 'generic' && id !== 'unknown' && id !== 'multiple' ? id : undefined; } function timestamp(value?: string | number): number | undefined { const parsed = typeof value === 'number' ? value : value ? Date.parse(value) : Number.NaN; return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined; } export function isPossibleDuplicateScan( leftTool: ToolIdentity | undefined, leftDate: string | number | undefined, rightTool: ToolIdentity | undefined, rightDate: string | number | undefined, toleranceMs = POSSIBLE_DUPLICATE_TOLERANCE_MS ): boolean { const leftToolId = usableToolId(leftTool); const rightToolId = usableToolId(rightTool); const leftTimestamp = timestamp(leftDate); const rightTimestamp = timestamp(rightDate); return Boolean(leftToolId && leftToolId === rightToolId && leftTimestamp !== undefined && rightTimestamp !== undefined && Math.abs(leftTimestamp - rightTimestamp) <= toleranceMs); } export function findPossibleDuplicateImportFile( files: ImportFileRecord[], tool: ToolIdentity | undefined, lastModified: number, toleranceMs = POSSIBLE_DUPLICATE_TOLERANCE_MS ): ImportFileRecord | undefined { return files.find(file => isPossibleDuplicateScan( tool, lastModified, file.detectedTool, file.lastModifiedAt ?? file.importedAt, toleranceMs )); } export function possibleDuplicateLabel(file: ImportFileRecord): string { const date = file.lastModifiedAt ?? file.importedAt; return `${file.detectedTool.name} from ${new Date(date).toLocaleString('en-US')}`; } /** * Werkzeuge betten den Zeitpunkt der Erhebung in den Dateinamen ein, etwa * Purple Knight mit * `Security_Assessment_Report_domain.local_2026_07_22_08_23_22_(Indicators_results).csv`. * Derselbe Bericht in einem anderen Ausgabeformat (HTML statt CSV) hat andere * Bytes und eine andere Dateisystemzeit, aber denselben Namensstamm und * denselben eingebetteten Zeitstempel - er ist ueber den SHA-256-Abgleich * deshalb nicht als Wiederholung erkennbar. */ export const SAME_REPORT_NAME_TOLERANCE_MS = 2 * 60 * 60 * 1000; const EMBEDDED_TIMESTAMP = /(\d{4})[-_.]?(\d{2})[-_.]?(\d{2})[-_.T ]+(\d{2})[-_.:]?(\d{2})[-_.:]?(\d{2})/; export interface ScanReportName { /** Namensstamm ohne Zeitstempel, Teilbericht-Suffix und Endung. */ base: string; /** Aus dem Dateinamen gelesener Erhebungszeitpunkt. */ timestamp: number; } /** * Zerlegt einen Dateinamen in Namensstamm und eingebetteten Zeitstempel. * Liefert undefined, wenn der Name keinen Zeitstempel traegt - dann darf nicht * ueber den Namen verglichen werden, weil Werkzeuge wie PingCastle bei jedem * Lauf denselben konstanten Dateinamen erzeugen (`ad_hc_domain.html`) und * sonst jeder neue Scan faelschlich als Wiederholung gaelte. */ export function scanReportName(fileName: string): ScanReportName | undefined { const withoutExtension = fileName.replace(/\.[^.]+$/, ''); const match = EMBEDDED_TIMESTAMP.exec(withoutExtension); if (!match) return undefined; const [, year, month, day, hour, minute, second] = match; const timestamp = Date.parse(`${year}-${month}-${day}T${hour}:${minute}:${second}`); if (!Number.isFinite(timestamp)) return undefined; const base = withoutExtension .replace(EMBEDDED_TIMESTAMP, ' ') // Teilbericht-Suffix eines mehrteiligen Exports, z. B. "_(Indicators_results)". .replace(/[([{][^)\]}]*[)\]}]\s*$/, ' ') .toLocaleLowerCase('de-DE') .replace(/[^a-z0-9]+/g, ''); return base ? { base, timestamp } : undefined; } /** * Gehoeren beide Dateinamen zum selben Bericht? Dafuer muessen Namensstamm * uebereinstimmen und die eingebetteten Zeitstempel innerhalb der Toleranz * liegen. */ export function isSameScanReportName( leftFileName: string, rightFileName: string, toleranceMs = SAME_REPORT_NAME_TOLERANCE_MS ): boolean { const left = scanReportName(leftFileName); const right = scanReportName(rightFileName); return Boolean(left && right && left.base === right.base && Math.abs(left.timestamp - right.timestamp) <= toleranceMs); } export function findSameScanReportFile( files: ImportFileRecord[], fileName: string, toleranceMs = SAME_REPORT_NAME_TOLERANCE_MS ): ImportFileRecord | undefined { return files.find(file => isSameScanReportName(fileName, file.fileName, toleranceMs)); } export function sameScanReportLabel(file: ImportFileRecord): string { const parsed = scanReportName(file.fileName); const date = parsed ? new Date(parsed.timestamp) : new Date(file.lastModifiedAt ?? file.importedAt); return `${file.fileName} (report from ${date.toLocaleString('en-US')})`; }