123 lines
4.8 KiB
TypeScript
123 lines
4.8 KiB
TypeScript
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')})`;
|
|
}
|