Repository: stackrox/stackrox
Pull request #14981: ROX-27399: adding hash to CVE ID
Author: login: dashrews78
Labels: name: area/central
Created: 2025-04-14T16:36:02Z | Merged: 2025-04-22T16:57:58Z

### Description

<!--
A detailed explanation of the changes in your PR. Feel free to remove this
section if the title of your PR is sufficiently descriptive. To learn more
about contributing to this project, check "*.md" files under:
    https://github.com/stackrox/stackrox/tree/master/.github
-->

There is a possibility that a CVE could occur multiple times in the same component.  We need to store both occurrences unless truly duplicates.  So we are going to hash the object and store that as part of the key so that we store each occurrence.

### User-facing documentation

- [ ] CHANGELOG is updated **OR** update is not needed
- [ ] [documentation PR](https://spaces.redhat.com/display/StackRox/Submitting+a+User+Documentation+Pull+Request) is created and is linked above **OR** is not needed

### Testing and quality

<!--
General Availability requirements: https://github.com/stackrox/stackrox/blob/master/PR_GA.md
Feature Flags usage: https://github.com/stackrox/stackrox/blob/master/pkg/features/README.md
-->

- [ ] the change is production ready: the change is GA or otherwise the functionality is gated by a feature flag
- [ ] CI results are inspected

#### Automated testing

<!--
If no tests have been contributed, please explain why unless it's obvious,
e.g., the PR is a one-line comment change.
-->

- [ ] added unit tests
- [ ] added e2e tests
- [ ] added regression tests
- [ ] added compatibility tests
- [ ] modified existing tests

#### How I validated my change

<!--
Use this space to explain **how you validated** that **your change functions
exactly how you expect it**. Feel free to attach JSON snippets, curl commands,
screenshots, etc. Apply a simple benchmark: would the information you provided
convince any reviewer or any external reader that you did enough to validate
your change.

It is acceptable to assume trust and keep this section light, e.g. as a
bullet-point list.

It is acceptable to skip testing in cases when CI is sufficient, or it's a
markdown or code comment change only. It is also acceptable to skip testing for
changes that are too taxing to test before merging. In such case you are
responsible for the change after it gets merged which includes reverting,
fixing, etc. Make sure you validate the change ASAP after it gets merged or
explain in PR when the validation will be performed. Explain here why you
skipped testing in case you did so.

Have you created automated tests for your change? Explain here which validation
activities you did manually and why so.
-->

Updated unit tests.  Much further and thorough testing will occur under https://issues.redhat.com/browse/ROX-27407


Diff:
diff --git a/central/cve/converter/utils/convert_utils_v2.go b/central/cve/converter/utils/convert_utils_v2.go
index a604110db7e29..0baa01ea9ee97 100644
--- a/central/cve/converter/utils/convert_utils_v2.go
+++ b/central/cve/converter/utils/convert_utils_v2.go
@@ -1,8 +1,6 @@
 package utils
 
 import (
-	"strconv"
-
 	"github.com/stackrox/rox/generated/storage"
 	"github.com/stackrox/rox/pkg/cve"
 	"google.golang.org/protobuf/types/known/timestamppb"
@@ -47,7 +45,7 @@ func ImageCVEV2ToEmbeddedVulnerability(vuln *storage.ImageCVEV2) *storage.Embedd
 }
 
 // EmbeddedVulnerabilityToImageCVEV2 converts *storage.EmbeddedVulnerability object to *storage.ImageCVEV2 object
-func EmbeddedVulnerabilityToImageCVEV2(imageID string, componentID string, cveIndex int, from *storage.EmbeddedVulnerability) *storage.ImageCVEV2 {
+func EmbeddedVulnerabilityToImageCVEV2(imageID string, componentID string, from *storage.EmbeddedVulnerability) (*storage.ImageCVEV2, error) {
 	var nvdCvss float32
 	nvdVersion := storage.CvssScoreVersion_UNKNOWN_VERSION
 	for _, score := range from.GetCvssMetrics() {
@@ -55,7 +53,6 @@ func EmbeddedVulnerabilityToImageCVEV2(imageID string, componentID string, cveIn
 			if score.GetCvssv3() != nil {
 				nvdCvss = score.GetCvssv3().GetScore()
 				nvdVersion = storage.CvssScoreVersion_V3
-
 			} else if score.GetCvssv2() != nil {
 				nvdCvss = score.GetCvssv2().GetScore()
 				nvdVersion = storage.CvssScoreVersion_V2
@@ -83,8 +80,13 @@ func EmbeddedVulnerabilityToImageCVEV2(imageID string, componentID string, cveIn
 		firstImageOccurrence = timestamppb.Now()
 	}
 
+	cveID, err := cve.IDV2(from, componentID)
+	if err != nil {
+		return nil, err
+	}
+
 	ret := &storage.ImageCVEV2{
-		Id:          cve.IDV2(from.GetCve(), componentID, strconv.Itoa(cveIndex)),
+		Id:          cveID,
 		ComponentId: componentID,
 		CveBaseInfo: &storage.CVEInfo{
 			Cve:          from.GetCve(),
@@ -117,5 +119,5 @@ func EmbeddedVulnerabilityToImageCVEV2(imageID string, componentID string, cveIn
 		}
 	}
 
-	return ret
+	return ret, nil
 }
diff --git a/central/cve/converter/utils/convert_utils_v2_test.go b/central/cve/converter/utils/convert_utils_v2_test.go
index 0d81481d002f0..fc108f13921f9 100644
--- a/central/cve/converter/utils/convert_utils_v2_test.go
+++ b/central/cve/converter/utils/convert_utils_v2_test.go
@@ -8,6 +8,8 @@ import (
 	"github.com/stackrox/rox/pkg/protoassert"
 	"github.com/stackrox/rox/pkg/protocompat"
 	"github.com/stackrox/rox/pkg/scancomponent"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 type componentPieces struct {
@@ -130,10 +132,40 @@ var (
 			Epss:                  nil,
 		},
 	}
+)
+
+func TestImageCVEV2ToEmbeddedCVEs(t *testing.T) {
+	for idx, imageVuln := range getTestCVEs(t) {
+		embeddedVuln := ImageCVEV2ToEmbeddedVulnerability(imageVuln)
+		protoassert.Equal(t, testVulns[idx], embeddedVuln)
+	}
+}
+
+func TestEmbeddedCVEToImageCVEV2(t *testing.T) {
+	for idx, embeddedVuln := range testVulns {
+		componentInfo := getComponentInfo(t)
+		convertedVuln, err := EmbeddedVulnerabilityToImageCVEV2(componentInfo[idx].imageID, componentInfo[idx].componentID, embeddedVuln)
+		assert.NoError(t, err)
+		protoassert.Equal(t, getTestCVEs(t)[idx], convertedVuln)
+	}
+}
+
+func getTestComponentID(t *testing.T) string {
+	id, err := scancomponent.ComponentIDV2(testComponent, "sha")
+	require.NoError(t, err)
+	return id
+}
+
+func getTestCVEID(t *testing.T, testCVE *storage.EmbeddedVulnerability, componentID string) string {
+	id, err := cve.IDV2(testCVE, componentID)
+	require.NoError(t, err)
+	return id
+}
 
-	testCVEs = []*storage.ImageCVEV2{
+func getTestCVEs(t *testing.T) []*storage.ImageCVEV2 {
+	return []*storage.ImageCVEV2{
 		{
-			Id:      cve.IDV2("cve1", getTestComponentID(), "0"),
+			Id:      getTestCVEID(t, testVulns[0], getTestComponentID(t)),
 			ImageId: "sha",
 			CveBaseInfo: &storage.CVEInfo{
 				Cve:          "cve1",
@@ -197,10 +229,10 @@ var (
 			State:                0,
 			IsFixable:            false,
 			HasFixedBy:           nil,
-			ComponentId:          getTestComponentID(),
+			ComponentId:          getTestComponentID(t),
 		},
 		{
-			Id:      cve.IDV2("cve2", getTestComponentID(), "1"),
+			Id:      getTestCVEID(t, testVulns[1], getTestComponentID(t)),
 			ImageId: "sha",
 			CveBaseInfo: &storage.CVEInfo{
 				Cve:          "cve2",
@@ -239,40 +271,22 @@ var (
 			HasFixedBy: &storage.ImageCVEV2_FixedBy{
 				FixedBy: "ver3",
 			},
-			ComponentId: getTestComponentID(),
+			ComponentId: getTestComponentID(t),
 		},
 	}
+}
 
-	componentInfo = []*componentPieces{
+func getComponentInfo(t *testing.T) []*componentPieces {
+	return []*componentPieces{
 		{
 			imageID:     "sha",
-			componentID: getTestComponentID(),
+			componentID: getTestComponentID(t),
 			cveIndex:    0,
 		},
 		{
 			imageID:     "sha",
-			componentID: getTestComponentID(),
+			componentID: getTestComponentID(t),
 			cveIndex:    1,
 		},
 	}
-)
-
-func TestImageCVEV2ToEmbeddedCVEs(t *testing.T) {
-	for idx, imageVuln := range testCVEs {
-		embeddedVuln := ImageCVEV2ToEmbeddedVulnerability(imageVuln)
-		protoassert.Equal(t, testVulns[idx], embeddedVuln)
-	}
-}
-
-func TestEmbeddedCVEToImageCVEV2(t *testing.T) {
-	for idx, embeddedVuln := range testVulns {
-		convertedVuln := EmbeddedVulnerabilityToImageCVEV2(componentInfo[idx].imageID, componentInfo[idx].componentID, componentInfo[idx].cveIndex, embeddedVuln)
-		protoassert.Equal(t, testCVEs[idx], convertedVuln)
-	}
-}
-
-func getTestComponentID() string {
-	id, _ := scancomponent.ComponentIDV2(testComponent, "sha")
-
-	return id
 }
diff --git a/central/graphql/resolvers/image_components.go b/central/graphql/resolvers/image_components.go
index 86048c38c4b69..7114986bc6903 100644
--- a/central/graphql/resolvers/image_components.go
+++ b/central/graphql/resolvers/image_components.go
@@ -337,11 +337,15 @@ func getImageCVEV2Resolvers(ctx context.Context, root *Resolver, imageID string,
 		return nil, err
 	}
 	resolvers := make([]ImageVulnerabilityResolver, 0, len(component.GetVulns()))
-	for idx, vuln := range component.GetVulns() {
+	for _, vuln := range component.GetVulns() {
 		if !predicate.Matches(vuln) {
 			continue
 		}
-		converted := cveConverter.EmbeddedVulnerabilityToImageCVEV2(imageID, componentID, idx, vuln)
+		converted, err := cveConverter.EmbeddedVulnerabilityToImageCVEV2(imageID, componentID, vuln)
+		if err != nil {
+			return nil, err
+		}
+
 		resolver, err := root.wrapImageCVEV2(converted, true, nil)
 		if err != nil {
 			return nil, err
@@ -704,11 +708,9 @@ func (resolver *imageComponentV2Resolver) TopImageVulnerability(ctx context.Cont
 	// Short path. Full image is embedded when image scan resolver is called.
 	if embeddedComponent := embeddedobjs.ComponentFromContext(resolver.ctx); embeddedComponent != nil {
 		var topVuln *storage.EmbeddedVulnerability
-		var topIndex int
-		for idx, vuln := range embeddedComponent.GetVulns() {
+		for _, vuln := range embeddedComponent.GetVulns() {
 			if topVuln == nil || vuln.GetCvss() > topVuln.GetCvss() {
 				topVuln = vuln
-				topIndex = idx
 			}
 		}
 		if topVuln == nil {
@@ -718,9 +720,12 @@ func (resolver *imageComponentV2Resolver) TopImageVulnerability(ctx context.Cont
 		if err != nil {
 			return nil, err
 		}
-		return resolver.root.wrapImageCVEV2WithContext(resolver.ctx,
-			cveConverter.EmbeddedVulnerabilityToImageCVEV2(resolver.ImageId(resolver.ctx), componentID, topIndex, topVuln), true, nil,
-		)
+
+		convertedTopVuln, err := cveConverter.EmbeddedVulnerabilityToImageCVEV2(resolver.ImageId(resolver.ctx), componentID, topVuln)
+		if err != nil {
+			return nil, err
+		}
+		return resolver.root.wrapImageCVEV2WithContext(resolver.ctx, convertedTopVuln, true, nil)
 	}
 
 	return resolver.root.TopImageVulnerability(resolver.imageComponentScopeContext(ctx), RawQuery{})
diff --git a/central/graphql/resolvers/image_components_v2_postgres_test.go b/central/graphql/resolvers/image_components_v2_postgres_test.go
index 9164b20ebf8ae..416711d4f3914 100644
--- a/central/graphql/resolvers/image_components_v2_postgres_test.go
+++ b/central/graphql/resolvers/image_components_v2_postgres_test.go
@@ -11,13 +11,11 @@ import (
 	"github.com/stackrox/rox/central/views/imagecveflat"
 	imagesView "github.com/stackrox/rox/central/views/images"
 	"github.com/stackrox/rox/generated/storage"
-	"github.com/stackrox/rox/pkg/cve"
 	"github.com/stackrox/rox/pkg/features"
 	"github.com/stackrox/rox/pkg/fixtures/fixtureconsts"
 	"github.com/stackrox/rox/pkg/grpc/authz/allow"
 	"github.com/stackrox/rox/pkg/postgres/pgtest"
 	"github.com/stackrox/rox/pkg/sac"
-	"github.com/stackrox/rox/pkg/scancomponent"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/stretchr/testify/suite"
@@ -33,17 +31,6 @@ const (
 	comp42 = "comp4image2"
 )
 
-var (
-	componentIDMap = map[string]string{
-		comp11: getTestComponentID(testImages()[0].GetScan().GetComponents()[0], "sha1"),
-		comp12: getTestComponentID(testImages()[1].GetScan().GetComponents()[0], "sha2"),
-		comp21: getTestComponentID(testImages()[0].GetScan().GetComponents()[1], "sha1"),
-		comp31: getTestComponentID(testImages()[0].GetScan().GetComponents()[2], "sha1"),
-		comp32: getTestComponentID(testImages()[1].GetScan().GetComponents()[1], "sha2"),
-		comp42: getTestComponentID(testImages()[1].GetScan().GetComponents()[2], "sha2"),
-	}
-)
-
 func TestGraphQLImageComponentV2Endpoints(t *testing.T) {
 	suite.Run(t, new(GraphQLImageComponentV2TestSuite))
 }
@@ -63,9 +50,10 @@ Remaining TODO tasks:
 type GraphQLImageComponentV2TestSuite struct {
 	suite.Suite
 
-	ctx      context.Context
-	testDB   *pgtest.TestPostgres
-	resolver *Resolver
+	ctx            context.Context
+	testDB         *pgtest.TestPostgres
+	resolver       *Resolver
+	componentIDMap map[string]string
 }
 
 func (s *GraphQLImageComponentV2TestSuite) SetupSuite() {
@@ -99,6 +87,8 @@ func (s *GraphQLImageComponentV2TestSuite) SetupSuite() {
 		err := s.resolver.ImageDataStore.UpsertImage(s.ctx, image)
 		s.NoError(err)
 	}
+
+	s.componentIDMap = s.getComponentIDMap()
 }
 
 func (s *GraphQLImageComponentV2TestSuite) TearDownSuite() {
@@ -125,12 +115,12 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponents() {
 	ctx := SetAuthorizerOverride(s.ctx, allow.Anonymous())
 
 	expectedIDs := []string{
-		componentIDMap[comp11],
-		componentIDMap[comp12],
-		componentIDMap[comp21],
-		componentIDMap[comp31],
-		componentIDMap[comp32],
-		componentIDMap[comp42],
+		s.componentIDMap[comp11],
+		s.componentIDMap[comp12],
+		s.componentIDMap[comp21],
+		s.componentIDMap[comp31],
+		s.componentIDMap[comp32],
+		s.componentIDMap[comp42],
 	}
 	expectedCount := int32(len(expectedIDs))
 
@@ -169,18 +159,18 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponentsScoped() {
 			"sha1",
 			"sha1",
 			[]string{
-				componentIDMap[comp11],
-				componentIDMap[comp21],
-				componentIDMap[comp31],
+				s.componentIDMap[comp11],
+				s.componentIDMap[comp21],
+				s.componentIDMap[comp31],
 			},
 		},
 		{
 			"sha2",
 			"sha2",
 			[]string{
-				componentIDMap[comp12],
-				componentIDMap[comp32],
-				componentIDMap[comp42],
+				s.componentIDMap[comp12],
+				s.componentIDMap[comp32],
+				s.componentIDMap[comp42],
 			},
 		},
 	}
@@ -219,14 +209,14 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponentsScopeTree() {
 			"sha1",
 			map[string][]string{
 				"cve-2018-1": {
-					componentIDMap[comp11],
-					componentIDMap[comp21],
+					s.componentIDMap[comp11],
+					s.componentIDMap[comp21],
 				},
 				"cve-2019-1": {
-					componentIDMap[comp31],
+					s.componentIDMap[comp31],
 				},
 				"cve-2019-2": {
-					componentIDMap[comp31],
+					s.componentIDMap[comp31],
 				},
 			},
 		},
@@ -235,19 +225,19 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponentsScopeTree() {
 			"sha2",
 			map[string][]string{
 				"cve-2018-1": {
-					componentIDMap[comp12],
+					s.componentIDMap[comp12],
 				},
 				"cve-2019-1": {
-					componentIDMap[comp32],
+					s.componentIDMap[comp32],
 				},
 				"cve-2019-2": {
-					componentIDMap[comp32],
+					s.componentIDMap[comp32],
 				},
 				"cve-2017-1": {
-					componentIDMap[comp42],
+					s.componentIDMap[comp42],
 				},
 				"cve-2017-2": {
-					componentIDMap[comp42],
+					s.componentIDMap[comp42],
 				},
 			},
 		},
@@ -293,7 +283,7 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponentMiss() {
 func (s *GraphQLImageComponentV2TestSuite) TestImageComponentHit() {
 	ctx := SetAuthorizerOverride(s.ctx, allow.Anonymous())
 
-	compID := graphql.ID(componentIDMap[comp11])
+	compID := graphql.ID(s.componentIDMap[comp11])
 
 	comp, err := s.resolver.ImageComponent(ctx, IDQuery{ID: &compID})
 	assert.NoError(s.T(), err)
@@ -310,32 +300,32 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponentImages() {
 	}{
 		{
 			"comp1image1",
-			componentIDMap[comp11],
+			s.componentIDMap[comp11],
 			[]string{"sha1"},
 		},
 		{
 			"comp1image2",
-			componentIDMap[comp12],
+			s.componentIDMap[comp12],
 			[]string{"sha2"},
 		},
 		{
 			"comp2image1",
-			componentIDMap[comp21],
+			s.componentIDMap[comp21],
 			[]string{"sha1"},
 		},
 		{
 			"comp3image1",
-			componentIDMap[comp31],
+			s.componentIDMap[comp31],
 			[]string{"sha1"},
 		},
 		{
 			"comp3image2",
-			componentIDMap[comp32],
+			s.componentIDMap[comp32],
 			[]string{"sha2"},
 		},
 		{
 			"comp4image2",
-			componentIDMap[comp42],
+			s.componentIDMap[comp42],
 			[]string{"sha2"},
 		},
 	}
@@ -368,9 +358,14 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponentImageVulnerabilitie
 	}{
 		{
 			"comp1os1",
-			componentIDMap[comp11],
+			s.componentIDMap[comp11],
 			[]string{
-				cve.IDV2("cve-2018-1", componentIDMap[comp11], "0"),
+				getTestCVEID(s.T(), &storage.EmbeddedVulnerability{Cve: "cve-2018-1",
+					SetFixedBy: &storage.EmbeddedVulnerability_FixedBy{
+						FixedBy: "1.1",
+					},
+					Severity: storage.VulnerabilitySeverity_CRITICAL_VULNERABILITY_SEVERITY,
+				}, s.componentIDMap[comp11]),
 			},
 			&VulnerabilityCounterResolver{
 				all:       &VulnerabilityFixableCounterResolver{0, 1},
@@ -382,9 +377,14 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponentImageVulnerabilitie
 		},
 		{
 			"comp2os1",
-			componentIDMap[comp21],
+			s.componentIDMap[comp21],
 			[]string{
-				cve.IDV2("cve-2018-1", componentIDMap[comp21], "0"),
+				getTestCVEID(s.T(), &storage.EmbeddedVulnerability{Cve: "cve-2018-1",
+					SetFixedBy: &storage.EmbeddedVulnerability_FixedBy{
+						FixedBy: "1.5",
+					},
+					Severity: storage.VulnerabilitySeverity_CRITICAL_VULNERABILITY_SEVERITY,
+				}, s.componentIDMap[comp21]),
 			},
 			&VulnerabilityCounterResolver{
 				all:       &VulnerabilityFixableCounterResolver{0, 1},
@@ -396,10 +396,16 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponentImageVulnerabilitie
 		},
 		{
 			"comp3os1",
-			componentIDMap[comp31],
+			s.componentIDMap[comp31],
 			[]string{
-				cve.IDV2("cve-2019-1", componentIDMap[comp31], "0"),
-				cve.IDV2("cve-2019-2", componentIDMap[comp31], "1"),
+				getTestCVEID(s.T(), &storage.EmbeddedVulnerability{Cve: "cve-2019-1",
+					Cvss:     4,
+					Severity: storage.VulnerabilitySeverity_MODERATE_VULNERABILITY_SEVERITY,
+				}, s.componentIDMap[comp31]),
+				getTestCVEID(s.T(), &storage.EmbeddedVulnerability{Cve: "cve-2019-2",
+					Cvss:     3,
+					Severity: storage.VulnerabilitySeverity_LOW_VULNERABILITY_SEVERITY,
+				}, s.componentIDMap[comp31]),
 			},
 			&VulnerabilityCounterResolver{
 				all:       &VulnerabilityFixableCounterResolver{0, 0},
@@ -411,9 +417,14 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponentImageVulnerabilitie
 		},
 		{
 			"comp1os2",
-			componentIDMap[comp12],
+			s.componentIDMap[comp12],
 			[]string{
-				cve.IDV2("cve-2018-1", componentIDMap[comp12], "0"),
+				getTestCVEID(s.T(), &storage.EmbeddedVulnerability{Cve: "cve-2018-1",
+					SetFixedBy: &storage.EmbeddedVulnerability_FixedBy{
+						FixedBy: "1.1",
+					},
+					Severity: storage.VulnerabilitySeverity_CRITICAL_VULNERABILITY_SEVERITY,
+				}, s.componentIDMap[comp12]),
 			},
 			&VulnerabilityCounterResolver{
 				all:       &VulnerabilityFixableCounterResolver{0, 1},
@@ -425,10 +436,16 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponentImageVulnerabilitie
 		},
 		{
 			"comp3os2",
-			componentIDMap[comp32],
+			s.componentIDMap[comp32],
 			[]string{
-				cve.IDV2("cve-2019-1", componentIDMap[comp32], "0"),
-				cve.IDV2("cve-2019-2", componentIDMap[comp32], "1"),
+				getTestCVEID(s.T(), &storage.EmbeddedVulnerability{Cve: "cve-2019-1",
+					Cvss:     4,
+					Severity: storage.VulnerabilitySeverity_MODERATE_VULNERABILITY_SEVERITY,
+				}, s.componentIDMap[comp32]),
+				getTestCVEID(s.T(), &storage.EmbeddedVulnerability{Cve: "cve-2019-2",
+					Cvss:     3,
+					Severity: storage.VulnerabilitySeverity_LOW_VULNERABILITY_SEVERITY,
+				}, s.componentIDMap[comp32]),
 			},
 			&VulnerabilityCounterResolver{
 				all:       &VulnerabilityFixableCounterResolver{0, 0},
@@ -440,10 +457,16 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponentImageVulnerabilitie
 		},
 		{
 			"comp4os2",
-			componentIDMap[comp42],
+			s.componentIDMap[comp42],
 			[]string{
-				cve.IDV2("cve-2017-1", componentIDMap[comp42], "0"),
-				cve.IDV2("cve-2017-2", componentIDMap[comp42], "1"),
+				getTestCVEID(s.T(), &storage.EmbeddedVulnerability{
+					Cve:      "cve-2017-1",
+					Severity: storage.VulnerabilitySeverity_IMPORTANT_VULNERABILITY_SEVERITY,
+				}, s.componentIDMap[comp42]),
+				getTestCVEID(s.T(), &storage.EmbeddedVulnerability{
+					Cve:      "cve-2017-2",
+					Severity: storage.VulnerabilitySeverity_IMPORTANT_VULNERABILITY_SEVERITY,
+				}, s.componentIDMap[comp42]),
 			},
 			&VulnerabilityCounterResolver{
 				all:       &VulnerabilityFixableCounterResolver{0, 0},
@@ -487,32 +510,32 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponentDeployments() {
 	}{
 		{
 			"comp1os1",
-			componentIDMap[comp11],
+			s.componentIDMap[comp11],
 			[]string{fixtureconsts.Deployment1, fixtureconsts.Deployment2},
 		},
 		{
 			"comp2os1",
-			componentIDMap[comp21],
+			s.componentIDMap[comp21],
 			[]string{fixtureconsts.Deployment1, fixtureconsts.Deployment2},
 		},
 		{
 			"comp3os1",
-			componentIDMap[comp31],
+			s.componentIDMap[comp31],
 			[]string{fixtureconsts.Deployment1, fixtureconsts.Deployment2},
 		},
 		{
 			"comp1os2",
-			componentIDMap[comp12],
+			s.componentIDMap[comp12],
 			[]string{fixtureconsts.Deployment1, fixtureconsts.Deployment3},
 		},
 		{
 			"comp3os2",
-			componentIDMap[comp32],
+			s.componentIDMap[comp32],
 			[]string{fixtureconsts.Deployment1, fixtureconsts.Deployment3},
 		},
 		{
 			"comp4os2",
-			componentIDMap[comp42],
+			s.componentIDMap[comp42],
 			[]string{fixtureconsts.Deployment1, fixtureconsts.Deployment3},
 		},
 	}
@@ -537,9 +560,12 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponentDeployments() {
 func (s *GraphQLImageComponentV2TestSuite) TestTopImageVulnerability() {
 	ctx := SetAuthorizerOverride(s.ctx, allow.Anonymous())
 
-	comp := s.getImageComponentResolver(ctx, componentIDMap[comp31])
+	comp := s.getImageComponentResolver(ctx, s.componentIDMap[comp31])
 
-	expectedID := graphql.ID(cve.IDV2("cve-2019-1", componentIDMap[comp31], "0"))
+	expectedID := graphql.ID(getTestCVEID(s.T(), &storage.EmbeddedVulnerability{Cve: "cve-2019-1",
+		Cvss:     4,
+		Severity: storage.VulnerabilitySeverity_MODERATE_VULNERABILITY_SEVERITY,
+	}, s.componentIDMap[comp31]))
 
 	vuln, err := comp.TopImageVulnerability(ctx)
 	assert.NoError(s.T(), err)
@@ -563,8 +589,13 @@ func (s *GraphQLImageComponentV2TestSuite) getImageComponentResolver(ctx context
 	return vuln
 }
 
-func getTestComponentID(testComponent *storage.EmbeddedImageScanComponent, imageID string) string {
-	id, _ := scancomponent.ComponentIDV2(testComponent, imageID)
-
-	return id
+func (s *GraphQLImageComponentV2TestSuite) getComponentIDMap() map[string]string {
+	return map[string]string{
+		comp11: getTestComponentID(s.T(), testImages()[0].GetScan().GetComponents()[0], "sha1"),
+		comp12: getTestComponentID(s.T(), testImages()[1].GetScan().GetComponents()[0], "sha2"),
+		comp21: getTestComponentID(s.T(), testImages()[0].GetScan().GetComponents()[1], "sha1"),
+		comp31: getTestComponentID(s.T(), testImages()[0].GetScan().GetComponents()[2], "sha1"),
+		comp32: getTestComponentID(s.T(), testImages()[1].GetScan().GetComponents()[1], "sha2"),
+		comp42: getTestComponentID(s.T(), testImages()[1].GetScan().GetComponents()[2], "sha2"),
+	}
 }
diff --git a/central/graphql/resolvers/image_vulnerabilities_v2_test.go b/central/graphql/resolvers/image_vulnerabilities_v2_test.go
index 62f47363e934c..a82d0a4e9eb6c 100644
--- a/central/graphql/resolvers/image_vulnerabilities_v2_test.go
+++ b/central/graphql/resolvers/image_vulnerabilities_v2_test.go
@@ -12,7 +12,6 @@ import (
 	imagesView "github.com/stackrox/rox/central/views/images"
 	v1 "github.com/stackrox/rox/generated/api/v1"
 	"github.com/stackrox/rox/generated/storage"
-	"github.com/stackrox/rox/pkg/cve"
 	"github.com/stackrox/rox/pkg/features"
 	"github.com/stackrox/rox/pkg/fixtures"
 	"github.com/stackrox/rox/pkg/grpc/authz/allow"
@@ -38,18 +37,6 @@ const (
 )
 
 var (
-	cveIDMap = map[string]string{
-		cve111: cve.IDV2("cve-2018-1", componentIDMap[comp11], "0"),
-		cve121: cve.IDV2("cve-2018-1", componentIDMap[comp21], "0"),
-		cve231: cve.IDV2("cve-2019-1", componentIDMap[comp31], "0"),
-		cve331: cve.IDV2("cve-2019-2", componentIDMap[comp31], "1"),
-		cve112: cve.IDV2("cve-2018-1", componentIDMap[comp12], "0"),
-		cve232: cve.IDV2("cve-2019-1", componentIDMap[comp32], "0"),
-		cve332: cve.IDV2("cve-2019-2", componentIDMap[comp32], "1"),
-		cve442: cve.IDV2("cve-2017-1", componentIDMap[comp42], "0"),
-		cve542: cve.IDV2("cve-2017-2", componentIDMap[comp42], "1"),
-	}
-
 	distinctCVEs = []string{"cve-2018-1", "cve-2019-1", "cve-2019-2", "cve-2017-1", "cve-2017-2"}
 )
 
@@ -77,6 +64,9 @@ type GraphQLImageVulnerabilityV2TestSuite struct {
 	ctx      context.Context
 	testDB   *pgtest.TestPostgres
 	resolver *Resolver
+
+	cveIDMap       map[string]string
+	componentIDMap map[string]string
 }
 
 func (s *GraphQLImageVulnerabilityV2TestSuite) SetupSuite() {
@@ -131,6 +121,9 @@ func (s *GraphQLImageVulnerabilityV2TestSuite) SetupSuite() {
 	} {
 		s.NoError(s.resolver.vulnReqStore.AddRequest(s.ctx, vulnReq))
 	}
+
+	s.componentIDMap = s.getComponentIDMap()
+	s.cveIDMap = s.getIDMap()
 }
 
 func (s *GraphQLImageVulnerabilityV2TestSuite) TestUnauthorizedImageVulnerabilityEndpoint() {
@@ -252,18 +245,18 @@ func (s *GraphQLImageVulnerabilityV2TestSuite) TestImageVulnerabilitiesFixedByVe
 
 	scopedCtx := scoped.Context(ctx, scoped.Scope{
 		Level: v1.SearchCategory_IMAGE_COMPONENTS_V2,
-		IDs:   []string{componentIDMap[comp11]},
+		IDs:   []string{s.componentIDMap[comp11]},
 	})
-	vuln := s.getImageVulnerabilityResolver(scopedCtx, cveIDMap[cve111])
+	vuln := s.getImageVulnerabilityResolver(scopedCtx, s.cveIDMap[cve111])
 	fixedBy, err := vuln.FixedByVersion(ctx)
 	s.NoError(err)
 	s.Equal("1.1", fixedBy)
 
 	scopedCtx = scoped.Context(ctx, scoped.Scope{
 		Level: v1.SearchCategory_IMAGE_COMPONENTS_V2,
-		IDs:   []string{componentIDMap[comp21]},
+		IDs:   []string{s.componentIDMap[comp21]},
 	})
-	vuln = s.getImageVulnerabilityResolver(scopedCtx, cveIDMap[cve121])
+	vuln = s.getImageVulnerabilityResolver(scopedCtx, s.cveIDMap[cve121])
 
 	fixedBy, err = vuln.FixedByVersion(ctx)
 	s.NoError(err)
@@ -271,9 +264,9 @@ func (s *GraphQLImageVulnerabilityV2TestSuite) TestImageVulnerabilitiesFixedByVe
 
 	scopedCtx = scoped.Context(ctx, scoped.Scope{
 		Level: v1.SearchCategory_IMAGE_COMPONENTS_V2,
-		IDs:   []string{componentIDMap[comp12]},
+		IDs:   []string{s.componentIDMap[comp42]},
 	})
-	vuln = s.getImageVulnerabilityResolver(scopedCtx, cveIDMap[cve442])
+	vuln = s.getImageVulnerabilityResolver(scopedCtx, s.cveIDMap[cve442])
 
 	fixedBy, err = vuln.FixedByVersion(ctx)
 	s.NoError(err)
@@ -333,7 +326,7 @@ func (s *GraphQLImageVulnerabilityV2TestSuite) TestImageVulnerabilityMiss() {
 func (s *GraphQLImageVulnerabilityV2TestSuite) TestImageVulnerabilityHit() {
 	ctx := SetAuthorizerOverride(s.ctx, allow.Anonymous())
 
-	vulnID := graphql.ID(cveIDMap[cve111])
+	vulnID := graphql.ID(s.cveIDMap[cve111])
 
 	vuln, err := s.resolver.ImageVulnerability(ctx, IDQuery{ID: &vulnID})
 	s.NoError(err)
@@ -352,7 +345,7 @@ func (s *GraphQLImageVulnerabilityV2TestSuite) TestTopImageVulnerability() {
 
 	image := s.getImageResolver(ctx, "sha1")
 
-	expected := graphql.ID(cveIDMap[cve231])
+	expected := graphql.ID(s.cveIDMap[cve231])
 	topVuln, err := image.TopImageVulnerability(ctx, RawQuery{})
 	s.NoError(err)
 	s.Equal(expected, topVuln.Id(ctx))
@@ -361,7 +354,7 @@ func (s *GraphQLImageVulnerabilityV2TestSuite) TestTopImageVulnerability() {
 func (s *GraphQLImageVulnerabilityV2TestSuite) TestImageVulnerabilityImages() {
 	ctx := SetAuthorizerOverride(s.ctx, allow.Anonymous())
 
-	vuln := s.getImageVulnerabilityResolver(ctx, cveIDMap[cve111])
+	vuln := s.getImageVulnerabilityResolver(ctx, s.cveIDMap[cve111])
 
 	images, err := vuln.Images(ctx, PaginatedQuery{})
 	s.NoError(err)
@@ -373,7 +366,7 @@ func (s *GraphQLImageVulnerabilityV2TestSuite) TestImageVulnerabilityImages() {
 	s.NoError(err)
 	s.Equal(int32(len(images)), count)
 
-	vuln = s.getImageVulnerabilityResolver(ctx, cveIDMap[cve442])
+	vuln = s.getImageVulnerabilityResolver(ctx, s.cveIDMap[cve442])
 
 	images, err = vuln.Images(ctx, PaginatedQuery{})
 	s.NoError(err)
@@ -389,25 +382,25 @@ func (s *GraphQLImageVulnerabilityV2TestSuite) TestImageVulnerabilityImages() {
 func (s *GraphQLImageVulnerabilityV2TestSuite) TestImageVulnerabilityImageComponents() {
 	ctx := SetAuthorizerOverride(s.ctx, allow.Anonymous())
 
-	vuln := s.getImageVulnerabilityResolver(ctx, cveIDMap[cve111])
+	vuln := s.getImageVulnerabilityResolver(ctx, s.cveIDMap[cve111])
 
 	comps, err := vuln.ImageComponents(ctx, PaginatedQuery{})
 	s.NoError(err)
 	s.Equal(1, len(comps))
 	idList := getIDList(ctx, comps)
-	s.ElementsMatch([]string{componentIDMap[comp11]}, idList)
+	s.ElementsMatch([]string{s.componentIDMap[comp11]}, idList)
 
 	count, err := vuln.ImageComponentCount(ctx, RawQuery{})
 	s.NoError(err)
 	s.Equal(int32(len(comps)), count)
 
-	vuln = s.getImageVulnerabilityResolver(ctx, cveIDMap[cve442])
+	vuln = s.getImageVulnerabilityResolver(ctx, s.cveIDMap[cve442])
 
 	comps, err = vuln.ImageComponents(ctx, PaginatedQuery{})
 	s.NoError(err)
 	s.Equal(1, len(comps))
 	idList = getIDList(ctx, comps)
-	s.ElementsMatch([]string{componentIDMap[comp42]}, idList)
+	s.ElementsMatch([]string{s.componentIDMap[comp42]}, idList)
 
 	count, err = vuln.ImageComponentCount(ctx, RawQuery{})
 	s.NoError(err)
@@ -754,3 +747,59 @@ func getCVEList(ctx context.Context, vulns []ImageVulnerabilityResolver) []strin
 	}
 	return cveList
 }
+
+func (s *GraphQLImageVulnerabilityV2TestSuite) getIDMap() map[string]string {
+	return map[string]string{
+		cve111: getTestCVEID(s.T(), &storage.EmbeddedVulnerability{Cve: "cve-2018-1",
+			SetFixedBy: &storage.EmbeddedVulnerability_FixedBy{
+				FixedBy: "1.1",
+			},
+			Severity: storage.VulnerabilitySeverity_CRITICAL_VULNERABILITY_SEVERITY,
+		}, s.componentIDMap[comp11]),
+		cve121: getTestCVEID(s.T(), &storage.EmbeddedVulnerability{Cve: "cve-2018-1",
+			SetFixedBy: &storage.EmbeddedVulnerability_FixedBy{
+				FixedBy: "1.5",
+			},
+			Severity: storage.VulnerabilitySeverity_CRITICAL_VULNERABILITY_SEVERITY,
+		}, s.componentIDMap[comp21]),
+		cve231: getTestCVEID(s.T(), &storage.EmbeddedVulnerability{Cve: "cve-2019-1",
+			Cvss:     4,
+			Severity: storage.VulnerabilitySeverity_MODERATE_VULNERABILITY_SEVERITY,
+		}, s.componentIDMap[comp31]),
+		cve331: getTestCVEID(s.T(), &storage.EmbeddedVulnerability{Cve: "cve-2019-2",
+			Cvss:     3,
+			Severity: storage.VulnerabilitySeverity_LOW_VULNERABILITY_SEVERITY,
+		}, s.componentIDMap[comp31]),
+		cve112: getTestCVEID(s.T(), &storage.EmbeddedVulnerability{Cve: "cve-2018-1",
+			SetFixedBy: &storage.EmbeddedVulnerability_FixedBy{
+				FixedBy: "1.1",
+			},
+			Severity: storage.VulnerabilitySeverity_CRITICAL_VULNERABILITY_SEVERITY,
+		}, s.componentIDMap[comp12]),
+		cve232: getTestCVEID(s.T(), &storage.EmbeddedVulnerability{Cve: "cve-2019-1",
+			Severity: storage.VulnerabilitySeverity_MODERATE_VULNERABILITY_SEVERITY,
+			Cvss:     4,
+		}, s.componentIDMap[comp32]),
+		cve332: getTestCVEID(s.T(), &storage.EmbeddedVulnerability{Cve: "cve-2019-2",
+			Severity: storage.VulnerabilitySeverity_LOW_VULNERABILITY_SEVERITY,
+			Cvss:     3,
+		}, s.componentIDMap[comp32]),
+		cve442: getTestCVEID(s.T(), &storage.EmbeddedVulnerability{Cve: "cve-2017-1",
+			Severity: storage.VulnerabilitySeverity_IMPORTANT_VULNERABILITY_SEVERITY,
+		}, s.componentIDMap[comp42]),
+		cve542: getTestCVEID(s.T(), &storage.EmbeddedVulnerability{Cve: "cve-2017-2",
+			Severity: storage.VulnerabilitySeverity_IMPORTANT_VULNERABILITY_SEVERITY,
+		}, s.componentIDMap[comp42]),
+	}
+}
+
+func (s *GraphQLImageVulnerabilityV2TestSuite) getComponentIDMap() map[string]string {
+	return map[string]string{
+		comp11: getTestComponentID(s.T(), testImages()[0].GetScan().GetComponents()[0], "sha1"),
+		comp12: getTestComponentID(s.T(), testImages()[1].GetScan().GetComponents()[0], "sha2"),
+		comp21: getTestComponentID(s.T(), testImages()[0].GetScan().GetComponents()[1], "sha1"),
+		comp31: getTestComponentID(s.T(), testImages()[0].GetScan().GetComponents()[2], "sha1"),
+		comp32: getTestComponentID(s.T(), testImages()[1].GetScan().GetComponents()[1], "sha2"),
+		comp42: getTestComponentID(s.T(), testImages()[1].GetScan().GetComponents()[2], "sha2"),
+	}
+}
diff --git a/central/graphql/resolvers/test_utils.go b/central/graphql/resolvers/test_utils.go
index c2ee3becce91b..72d63a9f7b6b6 100644
--- a/central/graphql/resolvers/test_utils.go
+++ b/central/graphql/resolvers/test_utils.go
@@ -10,6 +10,7 @@ import (
 	"github.com/stackrox/rox/central/cve/converter/v2"
 	"github.com/stackrox/rox/central/graphql/resolvers/loaders"
 	"github.com/stackrox/rox/generated/storage"
+	"github.com/stackrox/rox/pkg/cve"
 	"github.com/stackrox/rox/pkg/fixtures"
 	"github.com/stackrox/rox/pkg/fixtures/fixtureconsts"
 	"github.com/stackrox/rox/pkg/grpc/authn"
@@ -18,6 +19,7 @@ import (
 	nodeConverter "github.com/stackrox/rox/pkg/nodes/converter"
 	"github.com/stackrox/rox/pkg/protocompat"
 	"github.com/stackrox/rox/pkg/sac"
+	"github.com/stackrox/rox/pkg/scancomponent"
 	"github.com/stackrox/rox/pkg/search"
 	pgSearch "github.com/stackrox/rox/pkg/search/postgres"
 	"github.com/stackrox/rox/pkg/utils"
@@ -750,3 +752,17 @@ func contextWithClusterPerm(t testing.TB, ctrl *gomock.Controller) context.Conte
 	id.EXPECT().Permissions().Return(map[string]storage.Access{"Cluster": storage.Access_READ_ACCESS}).AnyTimes()
 	return authn.ContextWithIdentity(sac.WithAllAccess(loaders.WithLoaderContext(context.Background())), id, t)
 }
+
+func getTestComponentID(t *testing.T, testComponent *storage.EmbeddedImageScanComponent, imageID string) string {
+	id, err := scancomponent.ComponentIDV2(testComponent, imageID)
+	require.NoError(t, err)
+
+	return id
+}
+
+func getTestCVEID(t *testing.T, testCVE *storage.EmbeddedVulnerability, componentID string) string {
+	id, err := cve.IDV2(testCVE, componentID)
+	require.NoError(t, err)
+
+	return id
+}
diff --git a/central/image/datastore/datastore_test_constructors.go b/central/image/datastore/datastore_test_constructors.go
index 4507b45e2f806..3f6ae602e8002 100644
--- a/central/image/datastore/datastore_test_constructors.go
+++ b/central/image/datastore/datastore_test_constructors.go
@@ -3,16 +3,24 @@ package datastore
 import (
 	"testing"
 
+	"github.com/stackrox/rox/central/image/datastore/keyfence"
+	"github.com/stackrox/rox/central/image/datastore/store"
 	postgresStore "github.com/stackrox/rox/central/image/datastore/store/postgres"
+	pgStoreV2 "github.com/stackrox/rox/central/image/datastore/store/v2/postgres"
 	"github.com/stackrox/rox/central/ranking"
 	riskDS "github.com/stackrox/rox/central/risk/datastore"
-	"github.com/stackrox/rox/pkg/concurrency"
+	"github.com/stackrox/rox/pkg/features"
 	"github.com/stackrox/rox/pkg/postgres"
 )
 
 // GetTestPostgresDataStore provides a datastore connected to postgres for testing purposes.
 func GetTestPostgresDataStore(t testing.TB, pool postgres.DB) DataStore {
-	dbstore := postgresStore.New(pool, false, concurrency.NewKeyFence())
+	var dbstore store.Store
+	if features.FlattenCVEData.Enabled() {
+		dbstore = pgStoreV2.New(pool, false, keyfence.ImageKeyFenceSingleton())
+	} else {
+		dbstore = postgresStore.New(pool, false, keyfence.ImageKeyFenceSingleton())
+	}
 	riskStore := riskDS.GetTestPostgresDataStore(t, pool)
 	imageRanker := ranking.ImageRanker()
 	imageComponentRanker := ranking.ComponentRanker()
diff --git a/central/image/datastore/store/common/v2/parts_test.go b/central/image/datastore/store/common/v2/parts_test.go
index f065de278e4e9..87dd1565829df 100644
--- a/central/image/datastore/store/common/v2/parts_test.go
+++ b/central/image/datastore/store/common/v2/parts_test.go
@@ -14,6 +14,8 @@ import (
 )
 
 var (
+	ts = protocompat.TimestampNow()
+
 	testComponents = []*storage.EmbeddedImageScanComponent{
 		{
 			Name:    "comp1",
@@ -28,10 +30,46 @@ var (
 			Version: "ver1",
 		},
 	}
+
+	testCVEs = map[string]*storage.EmbeddedVulnerability{
+		"cve1comp1": {
+			Cve:                   "cve1",
+			VulnerabilityType:     storage.EmbeddedVulnerability_IMAGE_VULNERABILITY,
+			VulnerabilityTypes:    []storage.EmbeddedVulnerability_VulnerabilityType{storage.EmbeddedVulnerability_IMAGE_VULNERABILITY},
+			FirstImageOccurrence:  ts,
+			FirstSystemOccurrence: ts,
+		},
+		"cve2comp1": {
+			Cve:                "cve2",
+			VulnerabilityType:  storage.EmbeddedVulnerability_IMAGE_VULNERABILITY,
+			VulnerabilityTypes: []storage.EmbeddedVulnerability_VulnerabilityType{storage.EmbeddedVulnerability_IMAGE_VULNERABILITY},
+			SetFixedBy: &storage.EmbeddedVulnerability_FixedBy{
+				FixedBy: "ver3",
+			},
+			FirstImageOccurrence:  ts,
+			FirstSystemOccurrence: ts,
+		},
+		"cve1comp2": {
+			Cve:                "cve1",
+			VulnerabilityType:  storage.EmbeddedVulnerability_IMAGE_VULNERABILITY,
+			VulnerabilityTypes: []storage.EmbeddedVulnerability_VulnerabilityType{storage.EmbeddedVulnerability_IMAGE_VULNERABILITY},
+			SetFixedBy: &storage.EmbeddedVulnerability_FixedBy{
+				FixedBy: "ver2",
+			},
+			FirstImageOccurrence:  ts,
+			FirstSystemOccurrence: ts,
+		},
+		"cve2comp2": {
+			Cve:                   "cve2",
+			VulnerabilityType:     storage.EmbeddedVulnerability_IMAGE_VULNERABILITY,
+			VulnerabilityTypes:    []storage.EmbeddedVulnerability_VulnerabilityType{storage.EmbeddedVulnerability_IMAGE_VULNERABILITY},
+			FirstImageOccurrence:  ts,
+			FirstSystemOccurrence: ts,
+		},
+	}
 )
 
 func TestSplitAndMergeImage(t *testing.T) {
-	ts := protocompat.TimestampNow()
 	image := &storage.Image{
 		Id: "sha",
 		Name: &storage.ImageName{
@@ -219,7 +257,7 @@ func TestSplitAndMergeImage(t *testing.T) {
 							ImageCveId:       cve.ID("cve1", ""),
 						},
 						CVEV2: &storage.ImageCVEV2{
-							Id:      cve.IDV2("cve1", getTestComponentID(testComponents[1], "sha"), "0"),
+							Id:      getTestCVEID(testCVEs["cve1comp1"], getTestComponentID(testComponents[1], "sha")),
 							ImageId: "sha",
 							CveBaseInfo: &storage.CVEInfo{
 								Cve:       "cve1",
@@ -249,7 +287,7 @@ func TestSplitAndMergeImage(t *testing.T) {
 							IsFixable: true,
 						},
 						CVEV2: &storage.ImageCVEV2{
-							Id:      cve.IDV2("cve2", getTestComponentID(testComponents[1], "sha"), "1"),
+							Id:      getTestCVEID(testCVEs["cve2comp1"], getTestComponentID(testComponents[1], "sha")),
 							ImageId: "sha",
 							CveBaseInfo: &storage.CVEInfo{
 								Cve:       "cve2",
@@ -309,7 +347,7 @@ func TestSplitAndMergeImage(t *testing.T) {
 							IsFixable: true,
 						},
 						CVEV2: &storage.ImageCVEV2{
-							Id:      cve.IDV2("cve1", getTestComponentID(testComponents[2], "sha"), "0"),
+							Id:      getTestCVEID(testCVEs["cve1comp2"], getTestComponentID(testComponents[1], "sha")),
 							ImageId: "sha",
 							CveBaseInfo: &storage.CVEInfo{
 								Cve:       "cve1",
@@ -339,7 +377,7 @@ func TestSplitAndMergeImage(t *testing.T) {
 							ImageCveId:       cve.ID("cve2", ""),
 						},
 						CVEV2: &storage.ImageCVEV2{
-							Id:      cve.IDV2("cve2", getTestComponentID(testComponents[2], "sha"), "1"),
+							Id:      getTestCVEID(testCVEs["cve2comp2"], getTestComponentID(testComponents[1], "sha")),
 							ImageId: "sha",
 							CveBaseInfo: &storage.CVEInfo{
 								Cve:       "cve2",
@@ -404,3 +442,9 @@ func getTestComponentID(testComponent *storage.EmbeddedImageScanComponent, image
 
 	return id
 }
+
+func getTestCVEID(testCVE *storage.EmbeddedVulnerability, componentID string) string {
+	id, _ := cve.IDV2(testCVE, componentID)
+
+	return id
+}
diff --git a/central/image/datastore/store/common/v2/split_v2.go b/central/image/datastore/store/common/v2/split_v2.go
index 5778ca28e2e92..d622154879816 100644
--- a/central/image/datastore/store/common/v2/split_v2.go
+++ b/central/image/datastore/store/common/v2/split_v2.go
@@ -14,9 +14,14 @@ func splitComponentsV2(parts ImageParts) ([]ComponentParts, error) {
 			return nil, err
 		}
 
+		cves, err := splitCVEsV2(parts.Image.GetId(), generatedComponentV2.GetId(), component)
+		if err != nil {
+			return nil, err
+		}
+
 		cp := ComponentParts{
 			ComponentV2: generatedComponentV2,
-			Children:    splitCVEsV2(parts.Image.Id, generatedComponentV2.GetId(), component),
+			Children:    cves,
 		}
 
 		ret = append(ret, cp)
@@ -25,10 +30,13 @@ func splitComponentsV2(parts ImageParts) ([]ComponentParts, error) {
 	return ret, nil
 }
 
-func splitCVEsV2(imageID string, componentID string, embedded *storage.EmbeddedImageScanComponent) []CVEParts {
+func splitCVEsV2(imageID string, componentID string, embedded *storage.EmbeddedImageScanComponent) ([]CVEParts, error) {
 	ret := make([]CVEParts, 0, len(embedded.GetVulns()))
-	for cveIndex, cve := range embedded.GetVulns() {
-		convertedCVE := utils.EmbeddedVulnerabilityToImageCVEV2(imageID, componentID, cveIndex, cve)
+	for _, cve := range embedded.GetVulns() {
+		convertedCVE, err := utils.EmbeddedVulnerabilityToImageCVEV2(imageID, componentID, cve)
+		if err != nil {
+			return nil, err
+		}
 
 		cp := CVEParts{
 			CVEV2: convertedCVE,
@@ -36,7 +44,7 @@ func splitCVEsV2(imageID string, componentID string, embedded *storage.EmbeddedI
 		ret = append(ret, cp)
 	}
 
-	return ret
+	return ret, nil
 }
 
 // GenerateImageComponentV2 returns top-level image component from embedded component.
diff --git a/central/views/imagecveflat/view_test.go b/central/views/imagecveflat/view_test.go
index 56993bdeedcff..3960013232d7d 100644
--- a/central/views/imagecveflat/view_test.go
+++ b/central/views/imagecveflat/view_test.go
@@ -6,7 +6,6 @@ import (
 	"context"
 	"fmt"
 	"sort"
-	"strconv"
 	"testing"
 	"time"
 
@@ -38,6 +37,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/suite"
 	"go.uber.org/mock/gomock"
+	"google.golang.org/protobuf/types/known/timestamppb"
 )
 
 type testCase struct {
@@ -199,7 +199,7 @@ func (s *ImageCVEFlatViewTestSuite) TestGetImageCVEFlat() {
 			}
 			assert.NoError(t, err)
 
-			expected := compileExpected(s.testImages, tc.matchFilter, tc.readOptions, tc.less)
+			expected := s.compileExpected(s.testImages, tc.matchFilter, tc.readOptions, tc.less)
 			assert.Equal(t, len(expected), len(actual))
 			assert.ElementsMatch(t, expected, actual)
 			if tc.testOrder {
@@ -237,7 +237,7 @@ func (s *ImageCVEFlatViewTestSuite) TestGetImageCVEFlatSAC() {
 					return false
 				})
 
-				expected := compileExpected(s.testImages, &matchFilter, tc.readOptions, tc.less)
+				expected := s.compileExpected(s.testImages, &matchFilter, tc.readOptions, tc.less)
 				assert.Equal(t, len(expected), len(actual))
 				assert.ElementsMatch(t, expected, actual)
 			})
@@ -280,7 +280,7 @@ func (s *ImageCVEFlatViewTestSuite) TestGetImageCVEFlatWithPagination() {
 				}
 				assert.NoError(t, err)
 
-				expected := compileExpected(s.testImages, tc.matchFilter, tc.readOptions, tc.less)
+				expected := s.compileExpected(s.testImages, tc.matchFilter, tc.readOptions, tc.less)
 
 				assert.Equal(t, len(expected), len(actual))
 				assert.EqualValues(t, expected, actual)
@@ -307,7 +307,7 @@ func (s *ImageCVEFlatViewTestSuite) TestCountImageCVEFlat() {
 			}
 			assert.NoError(t, err)
 
-			expected := compileExpected(s.testImages, tc.matchFilter, tc.readOptions, nil)
+			expected := s.compileExpected(s.testImages, tc.matchFilter, tc.readOptions, nil)
 			assert.Equal(t, len(expected), actual)
 		})
 	}
@@ -341,7 +341,7 @@ func (s *ImageCVEFlatViewTestSuite) TestCountImageCVEFlatSAC() {
 					return false
 				})
 
-				expected := compileExpected(s.testImages, &matchFilter, tc.readOptions, tc.less)
+				expected := s.compileExpected(s.testImages, &matchFilter, tc.readOptions, tc.less)
 				assert.Equal(t, len(expected), actual)
 			})
 		}
@@ -520,13 +520,14 @@ func (s *ImageCVEFlatViewTestSuite) testCases() []testCase {
 				IDs:   []string{"sha256:6ef31316f4f9e0c31a8f4e602ba287a210d66934f91b1616f1c9b957201d025c"},
 				Level: v1.SearchCategory_IMAGES,
 				Parent: &scoped.Scope{
-					IDs: []string{cve.IDV2("CVE-2022-1552", getTestComponentID(&storage.EmbeddedImageScanComponent{
-						Name:         "postgresql-libs",
-						Version:      "8.4.20-6.el6",
-						Source:       storage.SourceType_OS,
-						Location:     "",
-						Architecture: "",
-					}, "sha256:05dd8ed5c76ad3c9f06481770828cf17b8c89f1e406c91d548426dd70fe94560"), "20")},
+					IDs: []string{getTestCVEID(getTestCVE(),
+						getTestComponentID(&storage.EmbeddedImageScanComponent{
+							Name:         "postgresql-libs",
+							Version:      "8.4.20-6.el6",
+							Source:       storage.SourceType_OS,
+							Location:     "",
+							Architecture: "",
+						}, "sha256:05dd8ed5c76ad3c9f06481770828cf17b8c89f1e406c91d548426dd70fe94560"))},
 					Level: v1.SearchCategory_IMAGE_VULNERABILITIES,
 				},
 			}),
@@ -752,7 +753,7 @@ func applyPaginationProps(baseTc *testCase, paginationTc testCase) {
 	baseTc.less = paginationTc.less
 }
 
-func compileExpected(images []*storage.Image, filter *filterImpl, options views.ReadOptions, less lessFunc) []CveFlat {
+func (s *ImageCVEFlatViewTestSuite) compileExpected(images []*storage.Image, filter *filterImpl, options views.ReadOptions, less lessFunc) []CveFlat {
 	cveMap := make(map[string]*imageCVEFlatResponse)
 
 	for _, image := range images {
@@ -762,7 +763,7 @@ func compileExpected(images []*storage.Image, filter *filterImpl, options views.
 
 		var seenForImage set.Set[string]
 		for _, component := range image.GetScan().GetComponents() {
-			for vulnIdx, vuln := range component.GetVulns() {
+			for _, vuln := range component.GetVulns() {
 				if !filter.matchVuln(vuln) {
 					continue
 				}
@@ -813,7 +814,9 @@ func compileExpected(images []*storage.Image, filter *filterImpl, options views.
 					val.Severity = pointers.Pointer(vuln.GetSeverity())
 				}
 
-				id := cve.IDV2(val.GetCVE(), getTestComponentID(component, image.GetId()), strconv.Itoa(vulnIdx))
+				id, err := cve.IDV2(vuln, getTestComponentID(component, image.GetId()))
+				s.NoError(err)
+
 				var found bool
 				for _, seenID := range val.GetCVEIDs() {
 					if seenID == id {
@@ -903,3 +906,44 @@ func getTestComponentID(testComponent *storage.EmbeddedImageScanComponent, image
 
 	return id
 }
+
+func getTestCVEID(testCVE *storage.EmbeddedVulnerability, componentID string) string {
+	id, _ := cve.IDV2(testCVE, componentID)
+
+	return id
+}
+
+func getTestCVE() *storage.EmbeddedVulnerability {
+	parsedTime, _ := time.Parse(time.RFC3339, "2022-05-12T00:00:00Z")
+
+	return &storage.EmbeddedVulnerability{
+		Cve:          "CVE-2022-1552",
+		Cvss:         8.8,
+		Summary:      "DOCUMENTATION: A flaw was found in PostgreSQL. There is an issue with incomplete efforts to operate safely when a privileged user is maintaining another user's objects. The Autovacuum, REINDEX, CREATE INDEX, REFRESH MATERIALIZED VIEW, CLUSTER, and pg_amcheck commands activated relevant protections too late or not at all during the process. This flaw allows an attacker with permission to create non-temporary objects in at least one schema to execute arbitrary SQL functions under a superuser identity.                           MITIGATION: Red Hat has investigated whether a possible mitigation exists for this issue, and has not been able to identify a practical example. Please update the affected package as soon as possible.",
+		Link:         "https://access.redhat.com/security/cve/CVE-2022-1552",
+		ScoreVersion: storage.EmbeddedVulnerability_V3,
+		CvssV3: &storage.CVSSV3{
+			Vector:              "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
+			ExploitabilityScore: 2.8,
+			ImpactScore:         5.9,
+			AttackVector:        storage.CVSSV3_ATTACK_NETWORK,
+			AttackComplexity:    storage.CVSSV3_COMPLEXITY_LOW,
+			PrivilegesRequired:  storage.CVSSV3_PRIVILEGE_LOW,
+			UserInteraction:     storage.CVSSV3_UI_NONE,
+			Scope:               storage.CVSSV3_UNCHANGED,
+			Confidentiality:     storage.CVSSV3_IMPACT_HIGH,
+			Integrity:           storage.CVSSV3_IMPACT_HIGH,
+			Availability:        storage.CVSSV3_IMPACT_HIGH,
+			Score:               8.8,
+			Severity:            storage.CVSSV3_HIGH,
+		},
+		PublishedOn:       timestamppb.New(parsedTime),
+		VulnerabilityType: storage.EmbeddedVulnerability_IMAGE_VULNERABILITY,
+		VulnerabilityTypes: []storage.EmbeddedVulnerability_VulnerabilityType{
+			storage.EmbeddedVulnerability_IMAGE_VULNERABILITY,
+		},
+		Suppressed: false,
+		Severity:   storage.VulnerabilitySeverity_IMPORTANT_VULNERABILITY_SEVERITY,
+		State:      storage.VulnerabilityState_OBSERVED,
+	}
+}
diff --git a/generated/storage/vulnerability.pb.go b/generated/storage/vulnerability.pb.go
index 7aaccefd5ef15..05bcfc3f4f60f 100644
--- a/generated/storage/vulnerability.pb.go
+++ b/generated/storage/vulnerability.pb.go
@@ -148,15 +148,15 @@ type EmbeddedVulnerability struct {
 	// For internal purposes only.
 	VulnerabilityType  EmbeddedVulnerability_VulnerabilityType   `protobuf:"varint,11,opt,name=vulnerability_type,json=vulnerabilityType,proto3,enum=storage.EmbeddedVulnerability_VulnerabilityType" json:"vulnerability_type,omitempty"`
 	VulnerabilityTypes []EmbeddedVulnerability_VulnerabilityType `protobuf:"varint,18,rep,packed,name=vulnerability_types,json=vulnerabilityTypes,proto3,enum=storage.EmbeddedVulnerability_VulnerabilityType" json:"vulnerability_types,omitempty" hash:"ignore"` // @gotags: hash:"ignore"
-	Suppressed         bool                                      `protobuf:"varint,12,opt,name=suppressed,proto3" json:"suppressed,omitempty" search:"CVE Snoozed"`                                                                                                       // @gotags: search:"CVE Snoozed"
-	SuppressActivation *timestamppb.Timestamp                    `protobuf:"bytes,13,opt,name=suppress_activation,json=suppressActivation,proto3" json:"suppress_activation,omitempty"`
-	SuppressExpiry     *timestamppb.Timestamp                    `protobuf:"bytes,14,opt,name=suppress_expiry,json=suppressExpiry,proto3" json:"suppress_expiry,omitempty"`
+	Suppressed         bool                                      `protobuf:"varint,12,opt,name=suppressed,proto3" json:"suppressed,omitempty" search:"CVE Snoozed" hash:"ignore"`                                                                                                       // @gotags: search:"CVE Snoozed" hash:"ignore"
+	SuppressActivation *timestamppb.Timestamp                    `protobuf:"bytes,13,opt,name=suppress_activation,json=suppressActivation,proto3" json:"suppress_activation,omitempty" hash:"ignore"`                                                              // @gotags: hash:"ignore"
+	SuppressExpiry     *timestamppb.Timestamp                    `protobuf:"bytes,14,opt,name=suppress_expiry,json=suppressExpiry,proto3" json:"suppress_expiry,omitempty" hash:"ignore"`                                                                          // @gotags: hash:"ignore"
 	// Time when the CVE was first seen, for this specific distro, in the system.
 	FirstSystemOccurrence *timestamppb.Timestamp `protobuf:"bytes,15,opt,name=first_system_occurrence,json=firstSystemOccurrence,proto3" json:"first_system_occurrence,omitempty" policy:"First System Occurrence Timestamp" hash:"ignore"` // @gotags: policy:"First System Occurrence Timestamp" hash:"ignore"
 	// Time when the CVE was first seen in this image.
 	FirstImageOccurrence *timestamppb.Timestamp `protobuf:"bytes,16,opt,name=first_image_occurrence,json=firstImageOccurrence,proto3" json:"first_image_occurrence,omitempty" policy:"First Image Occurrence Timestamp" hash:"ignore"` // @gotags: policy:"First Image Occurrence Timestamp" hash:"ignore"
 	Severity             VulnerabilitySeverity  `protobuf:"varint,19,opt,name=severity,proto3,enum=storage.VulnerabilitySeverity" json:"severity,omitempty" policy:"Severity"`                   // @gotags: policy:"Severity"
-	State                VulnerabilityState     `protobuf:"varint,20,opt,name=state,proto3,enum=storage.VulnerabilityState" json:"state,omitempty" search:"Vulnerability State"`                            // @gotags: search:"Vulnerability State"
+	State                VulnerabilityState     `protobuf:"varint,20,opt,name=state,proto3,enum=storage.VulnerabilityState" json:"state,omitempty" search:"Vulnerability State" hash:"ignore"`                            // @gotags: search:"Vulnerability State" hash:"ignore"
 	// cvss_metrics stores list of cvss scores from different sources like nvd, Redhat etc
 	CvssMetrics   []*CVSSScore `protobuf:"bytes,21,rep,name=cvss_metrics,json=cvssMetrics,proto3" json:"cvss_metrics,omitempty"`
 	NvdCvss       float32      `protobuf:"fixed32,22,opt,name=nvd_cvss,json=nvdCvss,proto3" json:"nvd_cvss,omitempty" search:"NVD CVSS,store"` // @gotags: search:"NVD CVSS,store"
diff --git a/pkg/cve/cve.go b/pkg/cve/cve.go
index ba843b1ce68be..3b95a17bb1764 100644
--- a/pkg/cve/cve.go
+++ b/pkg/cve/cve.go
@@ -1,6 +1,9 @@
 package cve
 
 import (
+	"strconv"
+
+	"github.com/mitchellh/hashstructure/v2"
 	"github.com/pkg/errors"
 	"github.com/stackrox/rox/generated/storage"
 	pgSearch "github.com/stackrox/rox/pkg/search/postgres"
@@ -24,8 +27,13 @@ func ID(cve, os string) string {
 }
 
 // IDV2 creates a CVE ID from the given cve name, component id and index of CVE within the component.
-func IDV2(cve, componentID, cveIndex string) string {
-	return pgSearch.IDFromPks([]string{cve, componentID, cveIndex})
+func IDV2(cve *storage.EmbeddedVulnerability, componentID string) (string, error) {
+	hash, err := hashstructure.Hash(cve, hashstructure.FormatV2, &hashstructure.HashOptions{ZeroNil: true})
+	if err != nil {
+		return "", err
+	}
+
+	return pgSearch.IDFromPks([]string{cve.GetCve(), strconv.FormatUint(hash, 10), componentID}), nil
 }
 
 // IDToParts return the CVE ID parts—cve and operating system.
diff --git a/proto/storage/vulnerability.proto b/proto/storage/vulnerability.proto
index 2f81e5bfd1ed9..5d1f6ea142099 100644
--- a/proto/storage/vulnerability.proto
+++ b/proto/storage/vulnerability.proto
@@ -42,9 +42,9 @@ message EmbeddedVulnerability {
   // For internal purposes only.
   VulnerabilityType vulnerability_type = 11;
   repeated VulnerabilityType vulnerability_types = 18; // @gotags: hash:"ignore"
-  bool suppressed = 12; // @gotags: search:"CVE Snoozed"
-  google.protobuf.Timestamp suppress_activation = 13;
-  google.protobuf.Timestamp suppress_expiry = 14;
+  bool suppressed = 12; // @gotags: search:"CVE Snoozed" hash:"ignore"
+  google.protobuf.Timestamp suppress_activation = 13; // @gotags: hash:"ignore"
+  google.protobuf.Timestamp suppress_expiry = 14; // @gotags: hash:"ignore"
   // Time when the CVE was first seen, for this specific distro, in the system.
   google.protobuf.Timestamp first_system_occurrence = 15; // @gotags: policy:"First System Occurrence Timestamp" hash:"ignore"
   // Time when the CVE was first seen in this image.
@@ -53,7 +53,7 @@ message EmbeddedVulnerability {
   reserved 17;
 
   VulnerabilitySeverity severity = 19; // @gotags: policy:"Severity"
-  VulnerabilityState state = 20; // @gotags: search:"Vulnerability State"
+  VulnerabilityState state = 20; // @gotags: search:"Vulnerability State" hash:"ignore"
   // cvss_metrics stores list of cvss scores from different sources like nvd, Redhat etc
   repeated CVSSScore cvss_metrics = 21;
   float nvd_cvss = 22; // @gotags: search:"NVD CVSS,store"


Reviews:
user: login: sourcery-ai[bot]
body: Hey @dashrews78 - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- The description and validation sections of the PR template still contain the `change me!` placeholder.
- Consider renaming `getTestCVEID` to `getTestImageCVEID` to better reflect its purpose.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟢 **General issues**: all looks good
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟢 **Complexity**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T10:16:58Z
user: login: charmik-redhat
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T11:52:45Z
user: login: dcaravel
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T23:01:03Z
user: login: RTann
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T23:06:07Z
user: login: dcaravel
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T00:16:57Z
user: login: dashrews78
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T09:44:12Z
user: login: RTann
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T22:31:08Z
user: login: dashrews78
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:05:03Z
user: login: dashrews78
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:17:27Z
user: login: dashrews78
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:25:20Z
user: login: charmik-redhat
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T17:21:02Z
user: login: charmik-redhat
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T17:21:40Z
user: login: dashrews78
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T17:26:26Z

Review comments:
diff_hunk: @@ -38,7 +38,7 @@ message EmbeddedVulnerability {
   CVSSV2 cvss_v2 = 6;
   CVSSV3 cvss_v3 = 7;
   google.protobuf.Timestamp published_on = 9; // @gotags: search:"CVE Published On"
-  google.protobuf.Timestamp last_modified = 10;
+  google.protobuf.Timestamp last_modified = 10; // @gotags: hash:"ignore"
path: proto/storage/vulnerability.proto
user: login: charmik-redhat
body: I think we can hash ignore `suppressed`, `suppress_activation`, `suppress_expiry` and `state` as those are all deferral related fields and not provided by scanner.
created_at: 2025-04-15T11:52:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,13 +38,13 @@ message EmbeddedVulnerability {
   CVSSV2 cvss_v2 = 6;
   CVSSV3 cvss_v3 = 7;
   google.protobuf.Timestamp published_on = 9; // @gotags: search:"CVE Published On"
-  google.protobuf.Timestamp last_modified = 10;
+  google.protobuf.Timestamp last_modified = 10; // @gotags: hash:"ignore"
path: proto/storage/vulnerability.proto
user: login: dcaravel
body: Last modified is a field that comes from the vuln sources - we have had users look at this field to determine if our 'data is up to date'. What are the implications of including this in the hash so that changes to it get persisted?
created_at: 2025-04-16T23:00:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,13 +38,13 @@ message EmbeddedVulnerability {
   CVSSV2 cvss_v2 = 6;
   CVSSV3 cvss_v3 = 7;
   google.protobuf.Timestamp published_on = 9; // @gotags: search:"CVE Published On"
-  google.protobuf.Timestamp last_modified = 10;
+  google.protobuf.Timestamp last_modified = 10; // @gotags: hash:"ignore"
path: proto/storage/vulnerability.proto
user: login: RTann
body: just a heads up Scanner V4 does not track a vulnerability's last modified time, and I don't know when/if we plan on changing that. Would require Claircore changes
created_at: 2025-04-16T23:06:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,13 +38,13 @@ message EmbeddedVulnerability {
   CVSSV2 cvss_v2 = 6;
   CVSSV3 cvss_v3 = 7;
   google.protobuf.Timestamp published_on = 9; // @gotags: search:"CVE Published On"
-  google.protobuf.Timestamp last_modified = 10;
+  google.protobuf.Timestamp last_modified = 10; // @gotags: hash:"ignore"
path: proto/storage/vulnerability.proto
user: login: dcaravel
body: I looks like ROX-20355 is also closed now (no reason) - comment remains in code here: 

https://github.com/stackrox/stackrox/blob/c23c2f7b6412733407620237ca99315ec55d0dc5/pkg/scanners/scannerv4/convert.go#L164

Looks like the new CVE UI doesn't show the field either - if we're committing to never populating the field again great! If there is a chance of bringing it back then in the short term the default value should not impact the final hash right? If we hash it now then in the future when/if we start populating it again no changes would be needed. (assuming no other implications)
created_at: 2025-04-17T00:16:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,13 +38,13 @@ message EmbeddedVulnerability {
   CVSSV2 cvss_v2 = 6;
   CVSSV3 cvss_v3 = 7;
   google.protobuf.Timestamp published_on = 9; // @gotags: search:"CVE Published On"
-  google.protobuf.Timestamp last_modified = 10;
+  google.protobuf.Timestamp last_modified = 10; // @gotags: hash:"ignore"
path: proto/storage/vulnerability.proto
user: login: dashrews78
body: I will leave the field and unignore it based on this conversation.  This was some good information.
created_at: 2025-04-17T09:44:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -276,3 +278,9 @@ func getTestComponentID() string {
 
 	return id
 }
+
+func getTestCVEID(testCVE *storage.EmbeddedVulnerability, componentID string) string {
+	id, _ := cve.IDV2(testCVE, componentID)
+
path: central/cve/converter/utils/convert_utils_v2_test.go
user: login: RTann
body: would we want to turn this into a `mustGetTestCVEID` and add a `require.NoError(err)` here?
created_at: 2025-04-21T22:23:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -704,11 +708,9 @@ func (resolver *imageComponentV2Resolver) TopImageVulnerability(ctx context.Cont
 	// Short path. Full image is embedded when image scan resolver is called.
 	if embeddedComponent := embeddedobjs.ComponentFromContext(resolver.ctx); embeddedComponent != nil {
 		var topVuln *storage.EmbeddedVulnerability
-		var topIndex int
-		for idx, vuln := range embeddedComponent.GetVulns() {
+		for _, vuln := range embeddedComponent.GetVulns() {
 			if topVuln == nil || vuln.GetCvss() > topVuln.GetCvss() {
path: central/graphql/resolvers/image_components.go
user: login: RTann
body: I'm not very familiar with this part of the codebase. Let's say we have a CVSS 3.1 score and a CVE 2.0 score, and the 2.0 is higher. We'd prefer that over the 3.1 score?
created_at: 2025-04-21T22:26:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -568,3 +604,9 @@ func getTestComponentID(testComponent *storage.EmbeddedImageScanComponent, image
 
 	return id
 }
+
+func getTestCVEID(testCVE *storage.EmbeddedVulnerability, componentID string) string {
path: central/graphql/resolvers/image_components_v2_postgres_test.go
user: login: RTann
body: same idea here
created_at: 2025-04-21T22:27:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,9 +14,14 @@ func splitComponentsV2(parts ImageParts) ([]ComponentParts, error) {
 			return nil, err
 		}
 
+		cves, err := splitCVEsV2(parts.Image.Id, generatedComponentV2.GetId(), component)
path: central/image/datastore/store/common/v2/split_v2.go
user: login: RTann
body: nit: `parts.Image.GetId()`
created_at: 2025-04-21T22:28:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -704,11 +708,9 @@ func (resolver *imageComponentV2Resolver) TopImageVulnerability(ctx context.Cont
 	// Short path. Full image is embedded when image scan resolver is called.
 	if embeddedComponent := embeddedobjs.ComponentFromContext(resolver.ctx); embeddedComponent != nil {
 		var topVuln *storage.EmbeddedVulnerability
-		var topIndex int
-		for idx, vuln := range embeddedComponent.GetVulns() {
+		for _, vuln := range embeddedComponent.GetVulns() {
 			if topVuln == nil || vuln.GetCvss() > topVuln.GetCvss() {
path: central/graphql/resolvers/image_components.go
user: login: dashrews78
body: @RTann this is probably best answered by @ksurabhi91 but the best I can tell is this is purely based on what comes in to us from scanner in the CVSS field.  No consideration is taken into account for the NVD score or whether it was NVD 2 vs NVD 3.  

Definitely outside scope of this PR, but also definitely something we should discuss if we have a bug or are not doing it quite right.
created_at: 2025-04-22T10:05:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -276,3 +278,9 @@ func getTestComponentID() string {
 
 	return id
 }
+
+func getTestCVEID(testCVE *storage.EmbeddedVulnerability, componentID string) string {
+	id, _ := cve.IDV2(testCVE, componentID)
+
path: central/cve/converter/utils/convert_utils_v2_test.go
user: login: dashrews78
body: Moved things around to add `require.NoError(err)`
created_at: 2025-04-22T10:17:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -568,3 +604,9 @@ func getTestComponentID(testComponent *storage.EmbeddedImageScanComponent, image
 
 	return id
 }
+
+func getTestCVEID(testCVE *storage.EmbeddedVulnerability, componentID string) string {
path: central/graphql/resolvers/image_components_v2_postgres_test.go
user: login: dashrews78
body: Similar.  Moved stuff around so I could require no error.
created_at: 2025-04-22T10:25:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -704,11 +708,9 @@ func (resolver *imageComponentV2Resolver) TopImageVulnerability(ctx context.Cont
 	// Short path. Full image is embedded when image scan resolver is called.
 	if embeddedComponent := embeddedobjs.ComponentFromContext(resolver.ctx); embeddedComponent != nil {
 		var topVuln *storage.EmbeddedVulnerability
-		var topIndex int
-		for idx, vuln := range embeddedComponent.GetVulns() {
+		for _, vuln := range embeddedComponent.GetVulns() {
 			if topVuln == nil || vuln.GetCvss() > topVuln.GetCvss() {
path: central/graphql/resolvers/image_components.go
user: login: charmik-redhat
body: As far as I know, when a VulnerabilityReport is converted to `EmbeddedVulnerabilities`, it prefers CVSS V3 over CVSS V2 for each CVE. However, if a component has a mix of some `EmbeddedVulns` having CVSS V2 and some having CVSS V3, then the `topVuln` could be the one with V2 score.

Also, yes definitely out of the scope of this PR.
created_at: 2025-04-22T17:21:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -704,11 +708,9 @@ func (resolver *imageComponentV2Resolver) TopImageVulnerability(ctx context.Cont
 	// Short path. Full image is embedded when image scan resolver is called.
 	if embeddedComponent := embeddedobjs.ComponentFromContext(resolver.ctx); embeddedComponent != nil {
 		var topVuln *storage.EmbeddedVulnerability
-		var topIndex int
-		for idx, vuln := range embeddedComponent.GetVulns() {
+		for _, vuln := range embeddedComponent.GetVulns() {
 			if topVuln == nil || vuln.GetCvss() > topVuln.GetCvss() {
path: central/graphql/resolvers/image_components.go
user: login: dashrews78
body: So we don't lose it.  Further conversation can move to Jira.

https://issues.redhat.com/browse/ROX-29042
created_at: 2025-04-22T17:26:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: dashrews78
created_at: 2025-04-14T16:36:03Z
body: This change is part of the following stack:

- #14961
    - #14981 ◀
        - #15031

<sub>Change managed by [git-spice](https://abhinav.github.io/git-spice/).</sub>
<!-- gs:navigation comment -->

author_association: CONTRIBUTOR
user: login: openshift-ci[bot]
created_at: 2025-04-14T16:36:08Z
body: Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with `/test all`
author_association: NONE
user: login: rhacs-bot
created_at: 2025-04-14T17:02:16Z
body: [//]: # (meta:type=build)

Images are ready for the commit at 8ec3ac9.

To use with deploy scripts, first `export MAIN_IMAGE_TAG=4.8.x-530-g8ec3ac9197`.
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-04-14T18:39:12Z
body: ## [Codecov](https://app.codecov.io/gh/stackrox/stackrox/pull/14981?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) Report
Attention: Patch coverage is `13.46154%` with `45 lines` in your changes missing coverage. Please review.
> Project coverage is 48.98%. Comparing base [(`14a3911`)](https://app.codecov.io/gh/stackrox/stackrox/commit/14a3911853ce6bc58734d6c1e1305a2978b239c1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) to head [(`8ec3ac9`)](https://app.codecov.io/gh/stackrox/stackrox/commit/8ec3ac91976de8d9e980b74ec3e444ff9bf47e0b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox).
> Report is 5 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/stackrox/stackrox/pull/14981?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) | Patch % | Lines |
|---|---|---|
| [...entral/image/datastore/store/common/v2/split\_v2.go](https://app.codecov.io/gh/stackrox/stackrox/pull/14981?src=pr&el=tree&filepath=central%2Fimage%2Fdatastore%2Fstore%2Fcommon%2Fv2%2Fsplit_v2.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-Y2VudHJhbC9pbWFnZS9kYXRhc3RvcmUvc3RvcmUvY29tbW9uL3YyL3NwbGl0X3YyLmdv) | 0.00% | [12 Missing :warning: ](https://app.codecov.io/gh/stackrox/stackrox/pull/14981?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |
| [central/graphql/resolvers/image\_components.go](https://app.codecov.io/gh/stackrox/stackrox/pull/14981?src=pr&el=tree&filepath=central%2Fgraphql%2Fresolvers%2Fimage_components.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-Y2VudHJhbC9ncmFwaHFsL3Jlc29sdmVycy9pbWFnZV9jb21wb25lbnRzLmdv) | 0.00% | [11 Missing :warning: ](https://app.codecov.io/gh/stackrox/stackrox/pull/14981?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |
| [central/graphql/resolvers/test\_utils.go](https://app.codecov.io/gh/stackrox/stackrox/pull/14981?src=pr&el=tree&filepath=central%2Fgraphql%2Fresolvers%2Ftest_utils.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-Y2VudHJhbC9ncmFwaHFsL3Jlc29sdmVycy90ZXN0X3V0aWxzLmdv) | 0.00% | [10 Missing :warning: ](https://app.codecov.io/gh/stackrox/stackrox/pull/14981?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |
| [pkg/cve/cve.go](https://app.codecov.io/gh/stackrox/stackrox/pull/14981?src=pr&el=tree&filepath=pkg%2Fcve%2Fcve.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-cGtnL2N2ZS9jdmUuZ28=) | 0.00% | [6 Missing :warning: ](https://app.codecov.io/gh/stackrox/stackrox/pull/14981?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |
| [central/cve/converter/utils/convert\_utils\_v2.go](https://app.codecov.io/gh/stackrox/stackrox/pull/14981?src=pr&el=tree&filepath=central%2Fcve%2Fconverter%2Futils%2Fconvert_utils_v2.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-Y2VudHJhbC9jdmUvY29udmVydGVyL3V0aWxzL2NvbnZlcnRfdXRpbHNfdjIuZ28=) | 57.14% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/stackrox/stackrox/pull/14981?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |
| [...ral/image/datastore/datastore\_test\_constructors.go](https://app.codecov.io/gh/stackrox/stackrox/pull/14981?src=pr&el=tree&filepath=central%2Fimage%2Fdatastore%2Fdatastore_test_constructors.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-Y2VudHJhbC9pbWFnZS9kYXRhc3RvcmUvZGF0YXN0b3JlX3Rlc3RfY29uc3RydWN0b3JzLmdv) | 50.00% | [1 Missing and 2 partials :warning: ](https://app.codecov.io/gh/stackrox/stackrox/pull/14981?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master   #14981   +/-   ##
=======================================
  Coverage   48.97%   48.98%           
=======================================
  Files        2550     2550           
  Lines      187167   187231   +64     
=======================================
+ Hits        91670    91716   +46     
- Misses      88254    88267   +13     
- Partials     7243     7248    +5     
```

| [Flag](https://app.codecov.io/gh/stackrox/stackrox/pull/14981/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) | Coverage Δ | |
|---|---|---|
| [go-unit-tests](https://app.codecov.io/gh/stackrox/stackrox/pull/14981/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) | `48.98% <13.46%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/stackrox/stackrox/pull/14981?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: dashrews78
created_at: 2025-04-21T17:07:40Z
body: /retest
author_association: CONTRIBUTOR
user: login: dashrews78
created_at: 2025-04-22T14:38:46Z
body: /test gke-operator-e2e-tests
author_association: CONTRIBUTOR
user: login: dashrews78
created_at: 2025-04-22T15:05:27Z
body: /test gke-upgrade-tests
author_association: CONTRIBUTOR
user: login: dashrews78
created_at: 2025-04-22T15:05:35Z
body: /test gke-scanner-v4-install-tests
author_association: CONTRIBUTOR
user: login: openshift-ci[bot]
created_at: 2025-04-22T16:39:59Z
body: @dashrews78: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
ci/prow/gke-qa-e2e-tests | 8ec3ac91976de8d9e980b74ec3e444ff9bf47e0b | [link](https://prow.ci.openshift.org/view/gs/test-platform-results/pr-logs/pull/stackrox_stackrox/14981/pull-ci-stackrox-stackrox-master-gke-qa-e2e-tests/1914665140611977216) | false | `/test gke-qa-e2e-tests`
ci/prow/ocp-4-12-qa-e2e-tests | 8ec3ac91976de8d9e980b74ec3e444ff9bf47e0b | [link](https://prow.ci.openshift.org/view/gs/test-platform-results/pr-logs/pull/stackrox_stackrox/14981/pull-ci-stackrox-stackrox-master-ocp-4-12-qa-e2e-tests/1914665185872711680) | false | `/test ocp-4-12-qa-e2e-tests`
ci/prow/ocp-4-18-qa-e2e-tests | 8ec3ac91976de8d9e980b74ec3e444ff9bf47e0b | [link](https://prow.ci.openshift.org/view/gs/test-platform-results/pr-logs/pull/stackrox_stackrox/14981/pull-ci-stackrox-stackrox-master-ocp-4-18-qa-e2e-tests/1914665219431337984) | false | `/test ocp-4-18-qa-e2e-tests`
ci/prow/ocp-4-12-nongroovy-e2e-tests | 8ec3ac91976de8d9e980b74ec3e444ff9bf47e0b | [link](https://prow.ci.openshift.org/view/gs/test-platform-results/pr-logs/pull/stackrox_stackrox/14981/pull-ci-stackrox-stackrox-master-ocp-4-12-nongroovy-e2e-tests/1914665173289799680) | false | `/test ocp-4-12-nongroovy-e2e-tests`
ci/prow/gke-scanner-v4-install-tests | 8ec3ac91976de8d9e980b74ec3e444ff9bf47e0b | [link](https://prow.ci.openshift.org/view/gs/test-platform-results/pr-logs/pull/stackrox_stackrox/14981/pull-ci-stackrox-stackrox-master-gke-scanner-v4-install-tests/1914697104173830144) | false | `/test gke-scanner-v4-install-tests`
ci/prow/gke-upgrade-tests | 8ec3ac91976de8d9e980b74ec3e444ff9bf47e0b | [link](https://prow.ci.openshift.org/view/gs/test-platform-results/pr-logs/pull/stackrox_stackrox/14981/pull-ci-stackrox-stackrox-master-gke-upgrade-tests/1914697071382761472) | false | `/test gke-upgrade-tests`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=stackrox&repo=stackrox&pr=14981). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:dashrews78).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
author_association: NONE
user: login: dashrews78
created_at: 2025-04-22T16:55:30Z
body: /test ocp-4-12-nongroovy-e2e-tests
author_association: CONTRIBUTOR