From e0f2e230a64e0822ba3f9c3e4ac89f6226feecfa Mon Sep 17 00:00:00 2001 From: Anso Date: Fri, 17 Apr 2026 14:23:23 -0400 Subject: [PATCH] fix(security): relabel unchanged bucket for cross-image comparisons (#662) When comparing scans from two different image refs, same-CVE+package matches are not necessarily the same finding. Relabel the pill and row status from 'Unchanged' to 'Shared' in that case to convey that the caveat in the warning banner applies. --- .../src/components/ScanComparisonSheet.tsx | 9 +++-- .../__tests__/ScanComparisonSheet.test.tsx | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ScanComparisonSheet.tsx b/frontend/src/components/ScanComparisonSheet.tsx index a96131a5..7b644af1 100644 --- a/frontend/src/components/ScanComparisonSheet.tsx +++ b/frontend/src/components/ScanComparisonSheet.tsx @@ -241,9 +241,14 @@ export function ScanComparisonSheet({ size="sm" className="h-7 text-xs px-2.5" onClick={() => { setFilter('unchanged'); setPage(0); }} + title={ + crossImage + ? 'Same CVE and package name appear in both images. Because the images differ, this does not necessarily mean the finding is literally unchanged.' + : 'Findings present in both scans' + } > - Unchanged ({data.unchanged.length}) + {crossImage ? 'Shared' : 'Unchanged'} ({data.unchanged.length}) {needsPagination && (
@@ -349,7 +354,7 @@ export function ScanComparisonSheet({ {filter === 'unchanged' && ( - Unchanged + {crossImage ? 'Shared' : 'Unchanged'} )} diff --git a/frontend/src/components/__tests__/ScanComparisonSheet.test.tsx b/frontend/src/components/__tests__/ScanComparisonSheet.test.tsx index 9db1300f..2e9849d7 100644 --- a/frontend/src/components/__tests__/ScanComparisonSheet.test.tsx +++ b/frontend/src/components/__tests__/ScanComparisonSheet.test.tsx @@ -171,6 +171,41 @@ describe('ScanComparisonSheet', () => { expect(screen.queryByText(/findings per scan/i)).toBeNull(); }); + it('relabels the unchanged bucket as "Shared" for cross-image comparisons', async () => { + mockedFetch.mockResolvedValueOnce( + jsonResponse(200, result({ + scanB: { id: 2, image_ref: 'alpine:3.19', scanned_at: 1_700_000_010_000 }, + unchanged: [vuln({ vulnerability_id: 'CVE-U', pkg_name: 'pu' })], + })), + ); + + const user = userEvent.setup(); + render( {}} />); + + await waitFor(() => + expect(screen.getByRole('button', { name: /Shared \(1\)/ })).toBeInTheDocument(), + ); + expect(screen.queryByRole('button', { name: /Unchanged/ })).toBeNull(); + + await user.click(screen.getByRole('button', { name: /Shared \(1\)/ })); + expect(screen.getByText('Shared')).toBeInTheDocument(); + }); + + it('keeps the "Unchanged" label when both scans use the same image', async () => { + mockedFetch.mockResolvedValueOnce( + jsonResponse(200, result({ + unchanged: [vuln({ vulnerability_id: 'CVE-U', pkg_name: 'pu' })], + })), + ); + + render( {}} />); + + await waitFor(() => + expect(screen.getByRole('button', { name: /Unchanged \(1\)/ })).toBeInTheDocument(), + ); + expect(screen.queryByRole('button', { name: /Shared/ })).toBeNull(); + }); + it('reloads when the scan ids change', async () => { mockedFetch.mockResolvedValue(jsonResponse(200, result()));