From 7056dd6040791d5e5b6a761a6ea1a65ed36dfa06 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 2 Jul 2026 00:38:13 +0000 Subject: [PATCH] fix: dedupe sentinel report artifact findings --- scripts/src/hwlab-node-web-sentinel-p5.ts | 45 ++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/scripts/src/hwlab-node-web-sentinel-p5.ts b/scripts/src/hwlab-node-web-sentinel-p5.ts index 069cd71e..f1976833 100644 --- a/scripts/src/hwlab-node-web-sentinel-p5.ts +++ b/scripts/src/hwlab-node-web-sentinel-p5.ts @@ -336,8 +336,9 @@ function compactSentinelReportRawPayload( function mergeSentinelReportFindings(primary: readonly Record[], artifact: readonly Record[]): Record[] { const merged: Record[] = []; const seen = new Set(); + const aliases = sentinelReportFindingIdentityAliases(primary); for (const item of [...primary, ...artifact]) { - const id = sentinelReportFindingIdentity(item); + const id = sentinelReportCanonicalFindingIdentity(item, aliases); const key = id ?? `${JSON.stringify(item).slice(0, 160)}:${stringAtNullable(item, "severity") ?? stringAtNullable(item, "level") ?? ""}`; if (seen.has(key)) continue; seen.add(key); @@ -346,6 +347,48 @@ function mergeSentinelReportFindings(primary: readonly Record[] return merged; } +function sentinelReportFindingIdentityAliases(items: readonly Record[]): Map { + const aliases = new Map(); + for (const item of items) { + const canonical = sentinelReportFindingCanonicalCandidate(item) ?? sentinelReportFindingIdentity(item); + if (canonical === null) continue; + for (const candidate of sentinelReportFindingIdentityCandidates(item)) aliases.set(candidate, canonical); + } + return aliases; +} + +function sentinelReportCanonicalFindingIdentity(item: Record, aliases: ReadonlyMap): string | null { + for (const candidate of sentinelReportFindingIdentityCandidates(item)) { + const canonical = aliases.get(candidate); + if (canonical !== undefined) return canonical; + } + return sentinelReportFindingCanonicalCandidate(item) ?? sentinelReportFindingIdentity(item); +} + +function sentinelReportFindingCanonicalCandidate(item: Record): string | null { + const check = record(item.check); + return stringAtNullable(item, "checkCode") + ?? stringAtNullable(check, "code") + ?? stringAtNullable(item, "checkId") + ?? stringAtNullable(check, "id"); +} + +function sentinelReportFindingIdentityCandidates(item: Record): readonly string[] { + const check = record(item.check); + const candidates = [ + stringAtNullable(item, "finding_id"), + stringAtNullable(item, "findingId"), + stringAtNullable(item, "id"), + stringAtNullable(item, "kind"), + stringAtNullable(item, "code"), + stringAtNullable(item, "checkId"), + stringAtNullable(item, "checkCode"), + stringAtNullable(check, "id"), + stringAtNullable(check, "code"), + ]; + return [...new Set(candidates.filter((item): item is string => item !== null))]; +} + function sentinelReportFindingIdentity(item: Record): string | null { return stringAtNullable(item, "finding_id") ?? stringAtNullable(item, "findingId")