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()));