From 6f9326c9f2c920ef1e3c88634e3e2f90c20682af Mon Sep 17 00:00:00 2001 From: cdu Date: Tue, 6 Jan 2026 14:43:33 -0800 Subject: [PATCH 01/18] ROX-32105: Fix base image graphql interface --- central/graphql/resolvers/image_components.go | 1 + 1 file changed, 1 insertion(+) diff --git a/central/graphql/resolvers/image_components.go b/central/graphql/resolvers/image_components.go index 11e1fb5ccaaac..21ab51beb04f8 100644 --- a/central/graphql/resolvers/image_components.go +++ b/central/graphql/resolvers/image_components.go @@ -24,6 +24,7 @@ func init() { utils.Must(schema.AddType("ImageComponentV2", []string{ "architecture: String!", "fixedBy: String!", + "fromBaseImage: Boolean!", "id: ID!", "imageId: String!", "name: String!", From 4bec747a9e585ca36d5ee7b2ac0b26831c3af933 Mon Sep 17 00:00:00 2001 From: cdu Date: Tue, 6 Jan 2026 22:46:10 -0800 Subject: [PATCH 02/18] one more --- central/graphql/resolvers/image_components.go | 1 + 1 file changed, 1 insertion(+) diff --git a/central/graphql/resolvers/image_components.go b/central/graphql/resolvers/image_components.go index 21ab51beb04f8..7a30b04af2512 100644 --- a/central/graphql/resolvers/image_components.go +++ b/central/graphql/resolvers/image_components.go @@ -41,6 +41,7 @@ func init() { "deploymentCount(query: String, scopeQuery: String): Int!", "deployments(query: String, scopeQuery: String, pagination: Pagination): [Deployment!]!", "fixedBy: String!", + "fromBaseImage: Boolean!", "id: ID!", "imageCount(query: String, scopeQuery: String): Int!", "images(query: String, scopeQuery: String, pagination: Pagination): [Image!]!", From 69b6bc01cd6df3572b2b7721b3b601d969592374 Mon Sep 17 00:00:00 2001 From: cdu Date: Mon, 12 Jan 2026 00:05:15 -0800 Subject: [PATCH 03/18] add tests --- .../image_components_v2_postgres_test.go | 3 +++ central/graphql/resolvers/images_test.go | 12 ++++++++++++ central/graphql/resolvers/test_utils.go | 19 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/central/graphql/resolvers/image_components_v2_postgres_test.go b/central/graphql/resolvers/image_components_v2_postgres_test.go index fa27588e7afdf..410d68b724596 100644 --- a/central/graphql/resolvers/image_components_v2_postgres_test.go +++ b/central/graphql/resolvers/image_components_v2_postgres_test.go @@ -134,6 +134,9 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponents() { for _, component := range comps { verifyLocationAndLayerIndex(ctx, s.T(), component, emptyLocationMap[string(component.Id(ctx))]) + + fromBaseImage := component.FromBaseImage(ctx) + assert.True(s.T(), fromBaseImage) } count, err := s.resolver.ImageComponentCount(ctx, RawQuery{}) diff --git a/central/graphql/resolvers/images_test.go b/central/graphql/resolvers/images_test.go index 361bb36961622..66d4d68689c9d 100644 --- a/central/graphql/resolvers/images_test.go +++ b/central/graphql/resolvers/images_test.go @@ -267,6 +267,18 @@ func (s *ImageResolversTestSuite) TestDeployments() { assert.Equal(t, int32(expectedCVESevCount.moderate), moderate.Total(testCtx)) assert.Equal(t, int32(expectedCVESevCount.low), low.Total(testCtx)) + // Test BaseImageInfo field for each image resolver. + expectedImage := expectedImages[imageID] + actualBaseImageInfo, err := image.BaseImageInfo(testCtx) + assert.NoError(t, err) + assert.Len(t, actualBaseImageInfo, len(expectedImage.GetBaseImageInfo())) + for i, baseInfo := range actualBaseImageInfo { + expectedBaseInfo := expectedImage.GetBaseImageInfo()[i] + assert.Equal(t, expectedBaseInfo.GetBaseImageId(), baseInfo.BaseImageId(testCtx)) + assert.Equal(t, expectedBaseInfo.GetBaseImageFullName(), baseInfo.BaseImageFullName(testCtx)) + assert.Equal(t, expectedBaseInfo.GetBaseImageDigest(), baseInfo.BaseImageDigest(testCtx)) + } + // Test image -> deployments -> images imageDeployments, err := image.Deployments(testCtx, paginatedQ) assert.NoError(t, err) diff --git a/central/graphql/resolvers/test_utils.go b/central/graphql/resolvers/test_utils.go index cc6fee0d9e619..dc13aa4298157 100644 --- a/central/graphql/resolvers/test_utils.go +++ b/central/graphql/resolvers/test_utils.go @@ -160,6 +160,13 @@ func testImages() []*storage.Image { }, ScanTime: t1, }, + BaseImageInfo: []*storage.BaseImageInfo{ + { + BaseImageId: "base-sha1", + BaseImageFullName: "ubuntu:18.04", + BaseImageDigest: "sha256:ubuntu18", + }, + }, }, { Id: "sha2", @@ -230,6 +237,18 @@ func testImages() []*storage.Image { }, ScanTime: t2, }, + BaseImageInfo: []*storage.BaseImageInfo{ + { + BaseImageId: "base-sha2", + BaseImageFullName: "alpine:3.12", + BaseImageDigest: "sha256:alpine312", + }, + { + BaseImageId: "base-sha3", + BaseImageFullName: "busybox:latest", + BaseImageDigest: "sha256:busybox1", + }, + }, }, } } From 4021079e281c78647388aa68e27ef1668e0033f8 Mon Sep 17 00:00:00 2001 From: cdu Date: Mon, 12 Jan 2026 13:53:37 -0800 Subject: [PATCH 04/18] fix field name --- central/graphql/resolvers/image_components.go | 6 +++--- central/graphql/resolvers/image_components_utilities.go | 2 +- .../graphql/resolvers/image_components_v2_postgres_test.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/central/graphql/resolvers/image_components.go b/central/graphql/resolvers/image_components.go index 7a30b04af2512..6b6e14890ec50 100644 --- a/central/graphql/resolvers/image_components.go +++ b/central/graphql/resolvers/image_components.go @@ -24,7 +24,7 @@ func init() { utils.Must(schema.AddType("ImageComponentV2", []string{ "architecture: String!", "fixedBy: String!", - "fromBaseImage: Boolean!", + "inBaseImageLayer: Boolean!", "id: ID!", "imageId: String!", "name: String!", @@ -41,7 +41,7 @@ func init() { "deploymentCount(query: String, scopeQuery: String): Int!", "deployments(query: String, scopeQuery: String, pagination: Pagination): [Deployment!]!", "fixedBy: String!", - "fromBaseImage: Boolean!", + "inBaseImageLayer: Boolean!", "id: ID!", "imageCount(query: String, scopeQuery: String): Int!", "images(query: String, scopeQuery: String, pagination: Pagination): [Image!]!", @@ -97,7 +97,7 @@ type ImageComponentResolver interface { TopImageVulnerability(ctx context.Context) (ImageVulnerabilityResolver, error) UnusedVarSink(ctx context.Context, args RawQuery) *int32 Version(ctx context.Context) string - FromBaseImage(ctx context.Context) bool + InBaseImageLayer(ctx context.Context) bool // deprecated functions diff --git a/central/graphql/resolvers/image_components_utilities.go b/central/graphql/resolvers/image_components_utilities.go index e7159d49637b1..e9db5ed1e5ffd 100644 --- a/central/graphql/resolvers/image_components_utilities.go +++ b/central/graphql/resolvers/image_components_utilities.go @@ -129,6 +129,6 @@ func (resolver *imageComponentV2Resolver) Version(_ context.Context) string { return resolver.data.GetVersion() } -func (resolver *imageComponentV2Resolver) FromBaseImage(ctx context.Context) bool { +func (resolver *imageComponentV2Resolver) InBaseImageLayer(ctx context.Context) bool { return resolver.data.GetFromBaseImage() } diff --git a/central/graphql/resolvers/image_components_v2_postgres_test.go b/central/graphql/resolvers/image_components_v2_postgres_test.go index 410d68b724596..48ca666fc427a 100644 --- a/central/graphql/resolvers/image_components_v2_postgres_test.go +++ b/central/graphql/resolvers/image_components_v2_postgres_test.go @@ -135,8 +135,8 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponents() { for _, component := range comps { verifyLocationAndLayerIndex(ctx, s.T(), component, emptyLocationMap[string(component.Id(ctx))]) - fromBaseImage := component.FromBaseImage(ctx) - assert.True(s.T(), fromBaseImage) + inBaseImageLayer := component.InBaseImageLayer(ctx) + assert.True(s.T(), inBaseImageLayer) } count, err := s.resolver.ImageComponentCount(ctx, RawQuery{}) From fe1d3ed09e55def3b9e4195243ae984bfe3f01f4 Mon Sep 17 00:00:00 2001 From: cdu Date: Fri, 16 Jan 2026 12:38:35 -0800 Subject: [PATCH 05/18] resolve review comment --- .../resolvers/image_components_v2_postgres_test.go | 14 ++++++++++---- central/graphql/resolvers/test_utils.go | 7 ------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/central/graphql/resolvers/image_components_v2_postgres_test.go b/central/graphql/resolvers/image_components_v2_postgres_test.go index 48ca666fc427a..d31eab72f83f2 100644 --- a/central/graphql/resolvers/image_components_v2_postgres_test.go +++ b/central/graphql/resolvers/image_components_v2_postgres_test.go @@ -126,7 +126,14 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponents() { comp32: false, comp42: true, } - + inBaseImageLayerMap := map[string]bool{ + s.componentIDMap[comp11]: false, + s.componentIDMap[comp12]: true, + s.componentIDMap[comp21]: false, + s.componentIDMap[comp31]: false, + s.componentIDMap[comp32]: true, + s.componentIDMap[comp42]: true, + } comps, err := s.resolver.ImageComponents(ctx, PaginatedQuery{}) assert.NoError(s.T(), err) assert.Equal(s.T(), expectedCount, int32(len(comps))) @@ -134,9 +141,8 @@ func (s *GraphQLImageComponentV2TestSuite) TestImageComponents() { for _, component := range comps { verifyLocationAndLayerIndex(ctx, s.T(), component, emptyLocationMap[string(component.Id(ctx))]) - - inBaseImageLayer := component.InBaseImageLayer(ctx) - assert.True(s.T(), inBaseImageLayer) + expectedInBaseImage := inBaseImageLayerMap[string(component.Id(ctx))] + assert.Equal(s.T(), expectedInBaseImage, component.InBaseImageLayer(ctx)) } count, err := s.resolver.ImageComponentCount(ctx, RawQuery{}) diff --git a/central/graphql/resolvers/test_utils.go b/central/graphql/resolvers/test_utils.go index dc13aa4298157..bb113742a1e0a 100644 --- a/central/graphql/resolvers/test_utils.go +++ b/central/graphql/resolvers/test_utils.go @@ -160,13 +160,6 @@ func testImages() []*storage.Image { }, ScanTime: t1, }, - BaseImageInfo: []*storage.BaseImageInfo{ - { - BaseImageId: "base-sha1", - BaseImageFullName: "ubuntu:18.04", - BaseImageDigest: "sha256:ubuntu18", - }, - }, }, { Id: "sha2", From 0cfda3959b2cd13eea9fde3374ee6dbaf13c2479 Mon Sep 17 00:00:00 2001 From: cdu Date: Fri, 16 Jan 2026 15:46:26 -0800 Subject: [PATCH 06/18] Add baseimage resolver and skip baseimageinfo --- central/graphql/resolvers/gen/main.go | 9 +++ central/graphql/resolvers/generated.go | 76 ------------------------ central/graphql/resolvers/images.go | 67 ++++++++++++++++++++- central/graphql/resolvers/images_test.go | 34 ++++++++--- central/graphql/resolvers/images_v2.go | 7 +++ 5 files changed, 107 insertions(+), 86 deletions(-) diff --git a/central/graphql/resolvers/gen/main.go b/central/graphql/resolvers/gen/main.go index 29ac8258bac0b..3ca94ad62b263 100644 --- a/central/graphql/resolvers/gen/main.go +++ b/central/graphql/resolvers/gen/main.go @@ -69,6 +69,7 @@ var ( reflect.TypeOf((*v1.SearchResult)(nil)), }, SkipResolvers: []reflect.Type{ + reflect.TypeOf(storage.BaseImageInfo{}), reflect.TypeOf(storage.EmbeddedVulnerability{}), reflect.TypeOf(storage.EmbeddedImageScanComponent{}), reflect.TypeOf(storage.EmbeddedNodeScanComponent{}), @@ -82,6 +83,10 @@ var ( ParentType: reflect.TypeOf(storage.Image{}), FieldName: "Scan", }, + { + ParentType: reflect.TypeOf(storage.Image{}), + FieldName: "BaseImageInfo", + }, { ParentType: reflect.TypeOf(storage.ImageV2{}), FieldName: "Scan", @@ -90,6 +95,10 @@ var ( ParentType: reflect.TypeOf(storage.ImageV2{}), FieldName: "ScanStats", }, + { + ParentType: reflect.TypeOf(storage.ImageV2{}), + FieldName: "BaseImageInfo", + }, { ParentType: reflect.TypeOf(storage.ImageScan{}), FieldName: "Components", diff --git a/central/graphql/resolvers/generated.go b/central/graphql/resolvers/generated.go index 9bcf6b8f3ac8d..a5bf6971b297b 100644 --- a/central/graphql/resolvers/generated.go +++ b/central/graphql/resolvers/generated.go @@ -161,11 +161,6 @@ func registerGeneratedTypes(builder generator.SchemaBuilder) { utils.Must(builder.AddType("AzureProviderMetadata", []string{ "subscriptionId: String!", })) - utils.Must(builder.AddType("BaseImageInfo", []string{ - "baseImageDigest: String!", - "baseImageFullName: String!", - "baseImageId: String!", - })) generator.RegisterProtoEnum(builder, reflect.TypeOf(storage.BooleanOperator(0))) utils.Must(builder.AddType("CSCC", []string{ "serviceAccount: String!", @@ -723,7 +718,6 @@ func registerGeneratedTypes(builder generator.SchemaBuilder) { "value: String!", })) utils.Must(builder.AddType("Image", []string{ - "baseImageInfo: [BaseImageInfo]!", "id: ID!", "isClusterLocal: Boolean!", "lastUpdated: Time", @@ -787,7 +781,6 @@ func registerGeneratedTypes(builder generator.SchemaBuilder) { })) generator.RegisterProtoEnum(builder, reflect.TypeOf(storage.ImageSignatureVerificationResult_Status(0))) utils.Must(builder.AddType("ImageV2", []string{ - "baseImageInfo: [BaseImageInfo]!", "digest: String!", "id: ID!", "isClusterLocal: Boolean!", @@ -3066,63 +3059,6 @@ func (resolver *azureProviderMetadataResolver) SubscriptionId(ctx context.Contex return value } -type baseImageInfoResolver struct { - ctx context.Context - root *Resolver - data *storage.BaseImageInfo -} - -func (resolver *Resolver) wrapBaseImageInfo(value *storage.BaseImageInfo, ok bool, err error) (*baseImageInfoResolver, error) { - if !ok || err != nil || value == nil { - return nil, err - } - return &baseImageInfoResolver{root: resolver, data: value}, nil -} - -func (resolver *Resolver) wrapBaseImageInfos(values []*storage.BaseImageInfo, err error) ([]*baseImageInfoResolver, error) { - if err != nil || len(values) == 0 { - return nil, err - } - output := make([]*baseImageInfoResolver, len(values)) - for i, v := range values { - output[i] = &baseImageInfoResolver{root: resolver, data: v} - } - return output, nil -} - -func (resolver *Resolver) wrapBaseImageInfoWithContext(ctx context.Context, value *storage.BaseImageInfo, ok bool, err error) (*baseImageInfoResolver, error) { - if !ok || err != nil || value == nil { - return nil, err - } - return &baseImageInfoResolver{ctx: ctx, root: resolver, data: value}, nil -} - -func (resolver *Resolver) wrapBaseImageInfosWithContext(ctx context.Context, values []*storage.BaseImageInfo, err error) ([]*baseImageInfoResolver, error) { - if err != nil || len(values) == 0 { - return nil, err - } - output := make([]*baseImageInfoResolver, len(values)) - for i, v := range values { - output[i] = &baseImageInfoResolver{ctx: ctx, root: resolver, data: v} - } - return output, nil -} - -func (resolver *baseImageInfoResolver) BaseImageDigest(ctx context.Context) string { - value := resolver.data.GetBaseImageDigest() - return value -} - -func (resolver *baseImageInfoResolver) BaseImageFullName(ctx context.Context) string { - value := resolver.data.GetBaseImageFullName() - return value -} - -func (resolver *baseImageInfoResolver) BaseImageId(ctx context.Context) string { - value := resolver.data.GetBaseImageId() - return value -} - func toBooleanOperator(value *string) storage.BooleanOperator { if value != nil { return storage.BooleanOperator(storage.BooleanOperator_value[*value]) @@ -8689,12 +8625,6 @@ func (resolver *imageResolver) ensureData(ctx context.Context) { } } -func (resolver *imageResolver) BaseImageInfo(ctx context.Context) ([]*baseImageInfoResolver, error) { - resolver.ensureData(ctx) - value := resolver.data.GetBaseImageInfo() - return resolver.root.wrapBaseImageInfos(value, nil) -} - func (resolver *imageResolver) Id(ctx context.Context) graphql.ID { value := resolver.data.GetId() if resolver.data == nil { @@ -9392,12 +9322,6 @@ func (resolver *imageV2Resolver) ensureData(ctx context.Context) { } } -func (resolver *imageV2Resolver) BaseImageInfo(ctx context.Context) ([]*baseImageInfoResolver, error) { - resolver.ensureData(ctx) - value := resolver.data.GetBaseImageInfo() - return resolver.root.wrapBaseImageInfos(value, nil) -} - func (resolver *imageV2Resolver) Digest(ctx context.Context) string { resolver.ensureData(ctx) value := resolver.data.GetDigest() diff --git a/central/graphql/resolvers/images.go b/central/graphql/resolvers/images.go index afdb3a04b9659..55a967b601a73 100644 --- a/central/graphql/resolvers/images.go +++ b/central/graphql/resolvers/images.go @@ -48,9 +48,9 @@ type ImageResolver interface { Signature(ctx context.Context) (*imageSignatureResolver, error) SignatureVerificationData(ctx context.Context) (*imageSignatureVerificationDataResolver, error) TopCvss(ctx context.Context) float64 - BaseImageInfo(ctx context.Context) ([]*baseImageInfoResolver, error) UnknownCveCount(ctx context.Context) int32 + BaseImage(ctx context.Context) (*baseImageResolver, error) Deployments(ctx context.Context, args PaginatedQuery) ([]*deploymentResolver, error) DeploymentCount(ctx context.Context, args RawQuery) (int32, error) TopImageVulnerability(ctx context.Context, args RawQuery) (ImageVulnerabilityResolver, error) @@ -88,8 +88,14 @@ func registerImageWatchStatus(s string) string { func init() { schema := getBuilder() utils.Must( + schema.AddType("BaseImage", []string{ + "imageSha: String!", + "names: [String!]!", + "created: Time", + }), // NOTE: This list is and should remain alphabetically ordered schema.AddExtraResolvers("Image", []string{ + "baseImage: BaseImage", "deploymentCount(query: String): Int!", "deployments(query: String, pagination: Pagination): [Deployment!]!", "imageComponentCount(query: String): Int!", @@ -515,3 +521,62 @@ func (resolver *imageResolver) TopCvss(_ context.Context) float64 { } return float64(value) } + +func (resolver *imageResolver) BaseImage(ctx context.Context) (*baseImageResolver, error) { + resolver.ensureData(ctx) + baseImageInfos := resolver.data.GetBaseImageInfo() + return resolver.root.wrapBaseImage(baseImageInfos) +} + +// baseImageResolver resolves base image information +type baseImageResolver struct { + root *Resolver + data *baseImageData +} + +type baseImageData struct { + imageSha string + names []string + created *graphql.Time +} + +func (resolver *Resolver) wrapBaseImage(baseImageInfos []*storage.BaseImageInfo) (*baseImageResolver, error) { + if len(baseImageInfos) == 0 { + return nil, nil + } + + // All entries should have the same digest, take the first one + imageSha := baseImageInfos[0].GetBaseImageDigest() + + // Collect all full names + names := make([]string, 0, len(baseImageInfos)) + for _, info := range baseImageInfos { + names = append(names, info.GetBaseImageFullName()) + } + + // TODO: Use actual created timestamp when it becomes available from storage + created := graphql.Time{Time: time.Now()} + + data := &baseImageData{ + imageSha: imageSha, + names: names, + created: &created, + } + + return &baseImageResolver{ + root: resolver, + data: data, + }, nil +} + +func (resolver *baseImageResolver) ImageSha(_ context.Context) string { + return resolver.data.imageSha +} + +func (resolver *baseImageResolver) Names(_ context.Context) []string { + return resolver.data.names +} + +func (resolver *baseImageResolver) Created(_ context.Context) (*graphql.Time, error) { + return resolver.data.created, nil +} diff --git a/central/graphql/resolvers/images_test.go b/central/graphql/resolvers/images_test.go index 66d4d68689c9d..5801b638c6766 100644 --- a/central/graphql/resolvers/images_test.go +++ b/central/graphql/resolvers/images_test.go @@ -267,16 +267,32 @@ func (s *ImageResolversTestSuite) TestDeployments() { assert.Equal(t, int32(expectedCVESevCount.moderate), moderate.Total(testCtx)) assert.Equal(t, int32(expectedCVESevCount.low), low.Total(testCtx)) - // Test BaseImageInfo field for each image resolver. - expectedImage := expectedImages[imageID] - actualBaseImageInfo, err := image.BaseImageInfo(testCtx) + // Test BaseImage field + actualBaseImage, err := image.BaseImage(testCtx) assert.NoError(t, err) - assert.Len(t, actualBaseImageInfo, len(expectedImage.GetBaseImageInfo())) - for i, baseInfo := range actualBaseImageInfo { - expectedBaseInfo := expectedImage.GetBaseImageInfo()[i] - assert.Equal(t, expectedBaseInfo.GetBaseImageId(), baseInfo.BaseImageId(testCtx)) - assert.Equal(t, expectedBaseInfo.GetBaseImageFullName(), baseInfo.BaseImageFullName(testCtx)) - assert.Equal(t, expectedBaseInfo.GetBaseImageDigest(), baseInfo.BaseImageDigest(testCtx)) + + expectedImage := expectedImages[imageID] + baseImageInfos := expectedImage.GetBaseImageInfo() + if len(baseImageInfos) == 0 { + assert.Nil(t, actualBaseImage) + } else { + require.NotNil(t, actualBaseImage) + + // Test imageSha (should be the digest from the first base image info) + expectedSha := baseImageInfos[0].GetBaseImageDigest() + assert.Equal(t, expectedSha, actualBaseImage.ImageSha(testCtx)) + + // Test name array + expectedNames := make([]string, 0, len(baseImageInfos)) + for _, info := range baseImageInfos { + expectedNames = append(expectedNames, info.GetBaseImageFullName()) + } + assert.Equal(t, expectedNames, actualBaseImage.Names(testCtx)) + + // Test created timestamp (placeholder until actual data is available) + actualCreated, err := actualBaseImage.Created(testCtx) + assert.NoError(t, err) + assert.NotNil(t, actualCreated) } // Test image -> deployments -> images diff --git a/central/graphql/resolvers/images_v2.go b/central/graphql/resolvers/images_v2.go index 94dbe92ecda64..9ea15e44e716a 100644 --- a/central/graphql/resolvers/images_v2.go +++ b/central/graphql/resolvers/images_v2.go @@ -23,6 +23,7 @@ func init() { schema := getBuilder() utils.Must( schema.AddExtraResolvers("ImageV2", []string{ + "baseImage: BaseImage", "deploymentCount(query: String): Int!", "deployments(query: String, pagination: Pagination): [Deployment!]!", "imageComponentCount(query: String): Int!", @@ -393,3 +394,9 @@ func (resolver *imageV2Resolver) FixableLowCveCount(ctx context.Context) int32 { resolver.ensureData(ctx) return resolver.data.GetScanStats().GetFixableLowCveCount() } + +func (resolver *imageV2Resolver) BaseImage(ctx context.Context) (*baseImageResolver, error) { + resolver.ensureData(ctx) + baseImageInfos := resolver.data.GetBaseImageInfo() + return resolver.root.wrapBaseImage(baseImageInfos) +} From 6eb5a4482600dab10ee35f6e42f5aa38f47a070a Mon Sep 17 00:00:00 2001 From: cdu Date: Tue, 20 Jan 2026 14:16:03 -0800 Subject: [PATCH 07/18] fix --- central/graphql/resolvers/images_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/central/graphql/resolvers/images_test.go b/central/graphql/resolvers/images_test.go index 5801b638c6766..a2d613b38014e 100644 --- a/central/graphql/resolvers/images_test.go +++ b/central/graphql/resolvers/images_test.go @@ -271,7 +271,8 @@ func (s *ImageResolversTestSuite) TestDeployments() { actualBaseImage, err := image.BaseImage(testCtx) assert.NoError(t, err) - expectedImage := expectedImages[imageID] + expectedImage, exists := expectedImages[imageID] + require.True(t, exists, "Expected image %s not found in expectedImages map", imageID) baseImageInfos := expectedImage.GetBaseImageInfo() if len(baseImageInfos) == 0 { assert.Nil(t, actualBaseImage) From 371803ed26ccf10e17c7955da949a4fb6e7a1483 Mon Sep 17 00:00:00 2001 From: Saif Chaudhry Date: Tue, 13 Jan 2026 17:02:53 -0800 Subject: [PATCH 08/18] ROX-32647: Add base image assessment card to Image Details page Add an expandable card to the Vulnerabilities tab on the Image Details page that displays detected base image information. The card shows: - Base image name with link to base image detail page - Truncated digest with copy button The card is gated behind the ROX_BASE_IMAGE_DETECTION feature flag and only renders when baseImageInfo has entries. Age field is stubbed out pending backend addition of baseImageCreated field to the BaseImageInfo GraphQL type. Signed-off-by: Saif Chaudhry --- .../WorkloadCves/Image/ImagePage.tsx | 1 + .../Image/ImagePageVulnerabilities.tsx | 14 +++ .../components/BaseImageAssessmentCard.tsx | 110 ++++++++++++++++++ .../components/ImageDetailBadges.tsx | 16 +++ 4 files changed, 141 insertions(+) create mode 100644 ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/BaseImageAssessmentCard.tsx diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePage.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePage.tsx index a095493c1e3dc..655e1fbea58a9 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePage.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePage.tsx @@ -316,6 +316,7 @@ function ImagePage({ tag: '', } } + baseImageInfo={imageData?.baseImageInfo ?? []} refetchAll={refetchAll} pagination={pagination} vulnerabilityState={vulnerabilityState} diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageVulnerabilities.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageVulnerabilities.tsx index 04a06b6a1589f..070ed2bfe5056 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageVulnerabilities.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageVulnerabilities.tsx @@ -64,6 +64,8 @@ import { imageCVESearchFilterConfig, imageComponentSearchFilterConfig, } from '../../searchFilterConfig'; +import BaseImageAssessmentCard from '../components/BaseImageAssessmentCard'; +import type { BaseImageInfo } from '../components/ImageDetailBadges'; export const imageVulnerabilitiesQuery = gql` ${imageMetadataContextFragment} @@ -97,6 +99,7 @@ export type ImagePageVulnerabilitiesProps = { remote: string; tag: string; }; + baseImageInfo: BaseImageInfo[]; refetchAll: () => void; pagination: UseURLPaginationResult; vulnerabilityState: VulnerabilityState; @@ -109,6 +112,7 @@ export type ImagePageVulnerabilitiesProps = { function ImagePageVulnerabilities({ imageId, imageName, + baseImageInfo, refetchAll, pagination, vulnerabilityState, @@ -118,6 +122,7 @@ function ImagePageVulnerabilities({ setSearchFilter, }: ImagePageVulnerabilitiesProps) { const { isFeatureFlagEnabled } = useFeatureFlags(); + const isBaseImageDetectionEnabled = isFeatureFlagEnabled('ROX_BASE_IMAGE_DETECTION'); const { analyticsTrack } = useAnalytics(); const trackAppliedFilter = createFilterTracker(analyticsTrack); @@ -247,6 +252,15 @@ function ImagePageVulnerabilities({ Review and triage vulnerability data scanned on this image + {isBaseImageDetectionEnabled && baseImageInfo.length > 0 && ( + + + + )} "sha256:abc123def456" + */ +function truncateDigest(digest: string): string { + const parts = digest.split(':'); + if (parts.length === 2) { + const [algorithm, hash] = parts; + return `${algorithm}:${hash.slice(0, 12)}`; + } + return digest.slice(0, 19); +} + +function BaseImageAssessmentCard({ baseImageInfo }: BaseImageAssessmentCardProps) { + const [isExpanded, setIsExpanded] = useState(true); + const { urlBuilder } = useWorkloadCveViewContext(); + + const onToggle = (_event: ReactMouseEvent, expanded: boolean) => { + setIsExpanded(expanded); + }; + + // Only render if there's at least one base image + if (baseImageInfo.length === 0) { + return null; + } + + // For now, display the first detected base image + // TODO: Handle multiple base images if needed in the future + const baseImage = baseImageInfo[0]; + const imageDetailPath = urlBuilder.imageDetails(baseImage.baseImageId, 'OBSERVED'); + + return ( + + + + + + Detected base image + + + + + {baseImage.baseImageFullName} + + + + + + + Digest + + + {truncateDigest(baseImage.baseImageDigest)} + + + + {/* TODO: Uncomment when backend adds 'baseImageCreated' field to BaseImageInfo GraphQL type + {baseImage.baseImageCreated && ( + + Age + + {getDistanceStrict(baseImage.baseImageCreated, new Date())} + + + )} + */} + + + + + ); +} + +export default BaseImageAssessmentCard; diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/ImageDetailBadges.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/ImageDetailBadges.tsx index 808abbc52f6e2..7cf162b95f6f2 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/ImageDetailBadges.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/ImageDetailBadges.tsx @@ -6,6 +6,14 @@ import type { SignatureVerificationResult } from '../../types'; import SignatureCountLabel from './SignatureCountLabel'; import VerifiedSignatureLabel, { getVerifiedSignatureInResults } from './VerifiedSignatureLabel'; +export type BaseImageInfo = { + baseImageId: string; + baseImageFullName: string; + baseImageDigest: string; + // TODO: Uncomment when backend adds 'baseImageCreated' field to BaseImageInfo GraphQL type + // baseImageCreated: string | null; +}; + export type ImageDetails = { deploymentCount: number; operatingSystem: string; @@ -22,6 +30,7 @@ export type ImageDetails = { signatureVerificationData: { results: SignatureVerificationResult[]; } | null; + baseImageInfo: BaseImageInfo[]; }; export const imageDetailsFragment = gql` @@ -49,6 +58,13 @@ export const imageDetailsFragment = gql` verifierId } } + baseImageInfo { + baseImageId + baseImageFullName + baseImageDigest + # TODO: Uncomment when backend adds 'baseImageCreated' field to BaseImageInfo GraphQL type + # baseImageCreated + } } `; From 27881fb636c0ec2311507c677863d839e200c4ab Mon Sep 17 00:00:00 2001 From: Saif Chaudhry Date: Tue, 13 Jan 2026 17:51:20 -0800 Subject: [PATCH 09/18] ROX-32647: Improve base image assessment card styling and support multiple images - Move card to its own section with gray background for visual separation - Support displaying multiple base images with dividers between them - Show full digest instead of truncated version (consistent with SHA display) - Update field labels to 'Image name', 'Image digest', 'Image age' - Add baseImageCreated field support (commented out in GraphQL until backend ready) - Fix horizontal alignment to match content below Signed-off-by: Saif Chaudhry --- .../Image/ImagePageVulnerabilities.tsx | 8 +- .../components/BaseImageAssessmentCard.tsx | 121 +++++++++--------- .../components/ImageDetailBadges.tsx | 3 +- 3 files changed, 65 insertions(+), 67 deletions(-) diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageVulnerabilities.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageVulnerabilities.tsx index 070ed2bfe5056..bf75f61fb7685 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageVulnerabilities.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageVulnerabilities.tsx @@ -252,16 +252,12 @@ function ImagePageVulnerabilities({ Review and triage vulnerability data scanned on this image + {isBaseImageDetectionEnabled && baseImageInfo.length > 0 && ( - + )} - "sha256:abc123def456" - */ -function truncateDigest(digest: string): string { - const parts = digest.split(':'); - if (parts.length === 2) { - const [algorithm, hash] = parts; - return `${algorithm}:${hash.slice(0, 12)}`; - } - return digest.slice(0, 19); -} - function BaseImageAssessmentCard({ baseImageInfo }: BaseImageAssessmentCardProps) { const [isExpanded, setIsExpanded] = useState(true); const { urlBuilder } = useWorkloadCveViewContext(); @@ -49,11 +38,6 @@ function BaseImageAssessmentCard({ baseImageInfo }: BaseImageAssessmentCardProps return null; } - // For now, display the first detected base image - // TODO: Handle multiple base images if needed in the future - const baseImage = baseImageInfo[0]; - const imageDetailPath = urlBuilder.imageDetails(baseImage.baseImageId, 'OBSERVED'); - return ( @@ -62,45 +46,64 @@ function BaseImageAssessmentCard({ baseImageInfo }: BaseImageAssessmentCardProps onToggle={onToggle} isExpanded={isExpanded} > - - - Detected base image - - - - - {baseImage.baseImageFullName} - - - - - - - Digest - - - {truncateDigest(baseImage.baseImageDigest)} - - - - {/* TODO: Uncomment when backend adds 'baseImageCreated' field to BaseImageInfo GraphQL type - {baseImage.baseImageCreated && ( - - Age - - {getDistanceStrict(baseImage.baseImageCreated, new Date())} - - - )} - */} - + + {baseImageInfo.map((baseImage, index) => { + const imageDetailPath = urlBuilder.imageDetails( + baseImage.baseImageId, + 'OBSERVED' + ); + return ( + + {index > 0 && } + + + Image name + + + + + {baseImage.baseImageFullName} + + + + + + + Image digest + + + {baseImage.baseImageDigest} + + + + {baseImage.baseImageCreated && ( + + Image age + + {getDistanceStrict( + baseImage.baseImageCreated, + new Date() + )} + + + )} + + + ); + })} + diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/ImageDetailBadges.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/ImageDetailBadges.tsx index 7cf162b95f6f2..b433750474994 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/ImageDetailBadges.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/ImageDetailBadges.tsx @@ -10,8 +10,7 @@ export type BaseImageInfo = { baseImageId: string; baseImageFullName: string; baseImageDigest: string; - // TODO: Uncomment when backend adds 'baseImageCreated' field to BaseImageInfo GraphQL type - // baseImageCreated: string | null; + baseImageCreated?: string; }; export type ImageDetails = { From 4446c364ea428f72c0bad0d2c3908444c5c77288 Mon Sep 17 00:00:00 2001 From: Saif Chaudhry Date: Tue, 13 Jan 2026 18:03:37 -0800 Subject: [PATCH 10/18] ROX-32647: Simplify BaseImageAssessmentCard component - Remove redundant empty array check (parent handles this) - Default to collapsed state for less visual noise - Remove unnecessary Flex wrapper around image link - Use Fragment instead of StackItem for cleaner structure Signed-off-by: Saif Chaudhry --- .../components/BaseImageAssessmentCard.tsx | 31 +++++-------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/BaseImageAssessmentCard.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/BaseImageAssessmentCard.tsx index 21ff4dc18e600..ce447b6bcc92c 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/BaseImageAssessmentCard.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/BaseImageAssessmentCard.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { Fragment, useState } from 'react'; import type { MouseEvent as ReactMouseEvent } from 'react'; import { Card, @@ -10,10 +10,7 @@ import { DescriptionListTerm, Divider, ExpandableSection, - Flex, - FlexItem, Stack, - StackItem, } from '@patternfly/react-core'; import { Link } from 'react-router-dom-v5-compat'; @@ -26,18 +23,13 @@ export type BaseImageAssessmentCardProps = { }; function BaseImageAssessmentCard({ baseImageInfo }: BaseImageAssessmentCardProps) { - const [isExpanded, setIsExpanded] = useState(true); + const [isExpanded, setIsExpanded] = useState(false); const { urlBuilder } = useWorkloadCveViewContext(); const onToggle = (_event: ReactMouseEvent, expanded: boolean) => { setIsExpanded(expanded); }; - // Only render if there's at least one base image - if (baseImageInfo.length === 0) { - return null; - } - return ( @@ -53,8 +45,8 @@ function BaseImageAssessmentCard({ baseImageInfo }: BaseImageAssessmentCardProps 'OBSERVED' ); return ( - - {index > 0 && } + + {index > 0 && } Image name - - - - {baseImage.baseImageFullName} - - - + + {baseImage.baseImageFullName} + @@ -100,7 +85,7 @@ function BaseImageAssessmentCard({ baseImageInfo }: BaseImageAssessmentCardProps )} - + ); })} From d054bf027b4f99c232a83aaa02b52bf5264fa498 Mon Sep 17 00:00:00 2001 From: Saif Chaudhry Date: Wed, 14 Jan 2026 10:39:52 -0800 Subject: [PATCH 11/18] ROX-32647: Enable inBaseImageLayer field and remove mock data - Add inBaseImageLayer to GraphQL fragment for accurate layer type display - Replace hardcoded mock data in getBaseImages() with actual API call Signed-off-by: Saif Chaudhry --- .../WorkloadCves/Tables/ImageComponentVulnerabilitiesTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Tables/ImageComponentVulnerabilitiesTable.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Tables/ImageComponentVulnerabilitiesTable.tsx index a0994ad858a75..3193993763fda 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Tables/ImageComponentVulnerabilitiesTable.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Tables/ImageComponentVulnerabilitiesTable.tsx @@ -26,7 +26,7 @@ export const imageComponentVulnerabilitiesFragment = gql` location source layerIndex - # TODO: Add inBaseImageLayer field once backend implements it + inBaseImageLayer imageVulnerabilities(query: $query) { severity fixedByVersion From d6fbffed83781626e454bc369a073561a947d7b0 Mon Sep 17 00:00:00 2001 From: Saif Chaudhry Date: Mon, 19 Jan 2026 15:21:47 -0800 Subject: [PATCH 12/18] test(ui): addressing comments Signed-off-by: Saif Chaudhry --- .../WorkloadCves/Image/ImagePageVulnerabilities.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageVulnerabilities.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageVulnerabilities.tsx index bf75f61fb7685..771bfbe5638f5 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageVulnerabilities.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageVulnerabilities.tsx @@ -254,7 +254,7 @@ function ImagePageVulnerabilities({ {isBaseImageDetectionEnabled && baseImageInfo.length > 0 && ( - + )} From 49ea8d694a6542d081f851d08a25e77f77bd6ef0 Mon Sep 17 00:00:00 2001 From: Saif Chaudhry Date: Mon, 19 Jan 2026 15:27:36 -0800 Subject: [PATCH 13/18] fix(base-images): correctly unwrap nested response in addBaseImage The backend returns CreateBaseImageReferenceResponse with baseImageReference nested inside, but the service was typed to expect BaseImageReference directly. This fixes the type annotation and response unwrapping to match the actual API response shape. Resolves: ROX-32571 Signed-off-by: Saif Chaudhry --- ui/apps/platform/src/services/BaseImagesService.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ui/apps/platform/src/services/BaseImagesService.ts b/ui/apps/platform/src/services/BaseImagesService.ts index 3a87b473d42d0..84d2d58ac38c2 100644 --- a/ui/apps/platform/src/services/BaseImagesService.ts +++ b/ui/apps/platform/src/services/BaseImagesService.ts @@ -15,6 +15,10 @@ export type BaseImagesResponse = { baseImageReferences: BaseImageReference[]; }; +export type CreateBaseImageReferenceResponse = { + baseImageReference: BaseImageReference; +}; + /** * Fetch the list of configured base images. */ @@ -32,8 +36,11 @@ export function addBaseImage( baseImageTagPattern: string ): Promise { return axios - .post(baseImagesUrl, { baseImageRepoPath, baseImageTagPattern }) - .then((response) => response.data); + .post(baseImagesUrl, { + baseImageRepoPath, + baseImageTagPattern, + }) + .then((response) => response.data.baseImageReference); } /** From 33bd83836d8ab8f6ecc139d5d43f67ec6eb2c860 Mon Sep 17 00:00:00 2001 From: Saif Chaudhry Date: Mon, 19 Jan 2026 18:43:21 -0800 Subject: [PATCH 14/18] ROX-32647: Update base image assessment to use new GraphQL resolver The backend now exposes a consolidated `baseImage` resolver instead of the raw `baseImageInfo` proto field. This change aligns the UI with the new GraphQL schema which returns a single BaseImage object with an array of names rather than an array of BaseImageInfo objects. - Update GraphQL fragments to use `baseImage { imageSha, names, created }` - Simplify BaseImageAssessmentCard to work with single object - Use imageSha (digest) as the image ID for detail links Signed-off-by: Saif Chaudhry --- .../WorkloadCves/Image/ImagePage.tsx | 2 +- .../Image/ImagePageVulnerabilities.tsx | 10 +- .../components/BaseImageAssessmentCard.tsx | 104 ++++++++---------- .../components/ImageDetailBadges.tsx | 26 +++-- 4 files changed, 68 insertions(+), 74 deletions(-) diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePage.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePage.tsx index 655e1fbea58a9..b8f3f3b496809 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePage.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePage.tsx @@ -316,7 +316,7 @@ function ImagePage({ tag: '', } } - baseImageInfo={imageData?.baseImageInfo ?? []} + baseImage={imageData?.baseImage ?? null} refetchAll={refetchAll} pagination={pagination} vulnerabilityState={vulnerabilityState} diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageVulnerabilities.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageVulnerabilities.tsx index 771bfbe5638f5..454084cb90b41 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageVulnerabilities.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageVulnerabilities.tsx @@ -65,7 +65,7 @@ import { imageComponentSearchFilterConfig, } from '../../searchFilterConfig'; import BaseImageAssessmentCard from '../components/BaseImageAssessmentCard'; -import type { BaseImageInfo } from '../components/ImageDetailBadges'; +import type { BaseImage } from '../components/ImageDetailBadges'; export const imageVulnerabilitiesQuery = gql` ${imageMetadataContextFragment} @@ -99,7 +99,7 @@ export type ImagePageVulnerabilitiesProps = { remote: string; tag: string; }; - baseImageInfo: BaseImageInfo[]; + baseImage: BaseImage | null; refetchAll: () => void; pagination: UseURLPaginationResult; vulnerabilityState: VulnerabilityState; @@ -112,7 +112,7 @@ export type ImagePageVulnerabilitiesProps = { function ImagePageVulnerabilities({ imageId, imageName, - baseImageInfo, + baseImage, refetchAll, pagination, vulnerabilityState, @@ -253,9 +253,9 @@ function ImagePageVulnerabilities({ Review and triage vulnerability data scanned on this image - {isBaseImageDetectionEnabled && baseImageInfo.length > 0 && ( + {isBaseImageDetectionEnabled && baseImage && ( - + )} @@ -38,57 +40,47 @@ function BaseImageAssessmentCard({ baseImageInfo }: BaseImageAssessmentCardProps onToggle={onToggle} isExpanded={isExpanded} > - - {baseImageInfo.map((baseImage, index) => { - const imageDetailPath = urlBuilder.imageDetails( - baseImage.baseImageId, - 'OBSERVED' - ); - return ( - - {index > 0 && } - - - Image name - - - {baseImage.baseImageFullName} - - - - - Image digest - - - {baseImage.baseImageDigest} - - - - {baseImage.baseImageCreated && ( - - Image age - - {getDistanceStrict( - baseImage.baseImageCreated, - new Date() - )} - - - )} - - - ); - })} - + + + + {baseImage.names.length > 1 ? 'Image names' : 'Image name'} + + + + {baseImage.names.map((name) => ( + + {name} + + ))} + + + + + Image digest + + + {baseImage.imageSha} + + + + {baseImage.created && ( + + Image age + + {getDistanceStrict(baseImage.created, new Date())} + + + )} + diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/ImageDetailBadges.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/ImageDetailBadges.tsx index b433750474994..2a70b40c94c06 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/ImageDetailBadges.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/ImageDetailBadges.tsx @@ -6,11 +6,10 @@ import type { SignatureVerificationResult } from '../../types'; import SignatureCountLabel from './SignatureCountLabel'; import VerifiedSignatureLabel, { getVerifiedSignatureInResults } from './VerifiedSignatureLabel'; -export type BaseImageInfo = { - baseImageId: string; - baseImageFullName: string; - baseImageDigest: string; - baseImageCreated?: string; +export type BaseImage = { + imageSha: string; + names: string[]; + created?: string; }; export type ImageDetails = { @@ -29,7 +28,7 @@ export type ImageDetails = { signatureVerificationData: { results: SignatureVerificationResult[]; } | null; - baseImageInfo: BaseImageInfo[]; + baseImage: BaseImage | null; }; export const imageDetailsFragment = gql` @@ -57,12 +56,10 @@ export const imageDetailsFragment = gql` verifierId } } - baseImageInfo { - baseImageId - baseImageFullName - baseImageDigest - # TODO: Uncomment when backend adds 'baseImageCreated' field to BaseImageInfo GraphQL type - # baseImageCreated + baseImage { + imageSha + names + created } } `; @@ -92,6 +89,11 @@ export const imageV2DetailsFragment = gql` verifierId } } + baseImage { + imageSha + names + created + } } `; From 50be8401c4e2d6ff9c0adc4e3260f5edcbb7363f Mon Sep 17 00:00:00 2001 From: Saif Chaudhry Date: Mon, 19 Jan 2026 19:10:58 -0800 Subject: [PATCH 15/18] ROX-32647: Use LabelGroup for base image names Display image names as compact labels in a LabelGroup with overflow handling. Shows 3 labels by default with expandable overflow for additional names. Signed-off-by: Saif Chaudhry --- .../components/BaseImageAssessmentCard.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/BaseImageAssessmentCard.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/BaseImageAssessmentCard.tsx index bf92799f32eab..bdc6c7eeaa6ef 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/BaseImageAssessmentCard.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/components/BaseImageAssessmentCard.tsx @@ -9,7 +9,8 @@ import { DescriptionListGroup, DescriptionListTerm, ExpandableSection, - Stack, + Label, + LabelGroup, } from '@patternfly/react-core'; import { Link } from 'react-router-dom-v5-compat'; @@ -51,13 +52,22 @@ function BaseImageAssessmentCard({ baseImage }: BaseImageAssessmentCardProps) { {baseImage.names.length > 1 ? 'Image names' : 'Image name'} - + {baseImage.names.map((name) => ( - + ))} - + From e6a4f884cc4bc0ee935ba9146dfa22557bcfb6bb Mon Sep 17 00:00:00 2001 From: cdu Date: Thu, 22 Jan 2026 17:21:39 -0800 Subject: [PATCH 16/18] integrate with created --- central/graphql/resolvers/images.go | 12 +++++++----- central/graphql/resolvers/images_test.go | 9 ++++++++- central/graphql/resolvers/test_utils.go | 4 ++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/central/graphql/resolvers/images.go b/central/graphql/resolvers/images.go index 55a967b601a73..031af997abb41 100644 --- a/central/graphql/resolvers/images.go +++ b/central/graphql/resolvers/images.go @@ -545,8 +545,13 @@ func (resolver *Resolver) wrapBaseImage(baseImageInfos []*storage.BaseImageInfo) return nil, nil } - // All entries should have the same digest, take the first one + // All entries should have the same digest and create time, take the first one imageSha := baseImageInfos[0].GetBaseImageDigest() + createTimestamp := baseImageInfos[0].GetCreated() + created, err := protocompat.ConvertTimestampToGraphqlTimeOrError(createTimestamp) + if err != nil { + return nil, err + } // Collect all full names names := make([]string, 0, len(baseImageInfos)) @@ -554,13 +559,10 @@ func (resolver *Resolver) wrapBaseImage(baseImageInfos []*storage.BaseImageInfo) names = append(names, info.GetBaseImageFullName()) } - // TODO: Use actual created timestamp when it becomes available from storage - created := graphql.Time{Time: time.Now()} - data := &baseImageData{ imageSha: imageSha, names: names, - created: &created, + created: created, } return &baseImageResolver{ diff --git a/central/graphql/resolvers/images_test.go b/central/graphql/resolvers/images_test.go index a2d613b38014e..637f8fbf38e4d 100644 --- a/central/graphql/resolvers/images_test.go +++ b/central/graphql/resolvers/images_test.go @@ -6,6 +6,7 @@ import ( "context" "strings" "testing" + "time" "github.com/graph-gophers/graphql-go" "github.com/stackrox/rox/central/graphql/resolvers/loaders" @@ -18,6 +19,7 @@ import ( "github.com/stackrox/rox/pkg/grpc/authz/allow" "github.com/stackrox/rox/pkg/pointers" "github.com/stackrox/rox/pkg/postgres/pgtest" + "github.com/stackrox/rox/pkg/protocompat" "github.com/stackrox/rox/pkg/sac" "github.com/stackrox/rox/pkg/sac/resources" "github.com/stretchr/testify/assert" @@ -290,10 +292,15 @@ func (s *ImageResolversTestSuite) TestDeployments() { } assert.Equal(t, expectedNames, actualBaseImage.Names(testCtx)) - // Test created timestamp (placeholder until actual data is available) + // Test created timestamp actualCreated, err := actualBaseImage.Created(testCtx) assert.NoError(t, err) assert.NotNil(t, actualCreated) + expectedTimestamp, err := protocompat.ConvertTimeToTimestampOrError(time.Unix(0, 3000)) + assert.NoError(t, err) + expectedCreated, err := protocompat.ConvertTimestampToGraphqlTimeOrError(expectedTimestamp) + assert.NoError(t, err) + assert.Equal(t, expectedCreated, actualCreated) } // Test image -> deployments -> images diff --git a/central/graphql/resolvers/test_utils.go b/central/graphql/resolvers/test_utils.go index bb113742a1e0a..9250da2ba7f3b 100644 --- a/central/graphql/resolvers/test_utils.go +++ b/central/graphql/resolvers/test_utils.go @@ -96,6 +96,8 @@ func testImages() []*storage.Image { utils.CrashOnError(err) t2, err := protocompat.ConvertTimeToTimestampOrError(time.Unix(0, 2000)) utils.CrashOnError(err) + t3, err := protocompat.ConvertTimeToTimestampOrError(time.Unix(0, 3000)) + utils.CrashOnError(err) return []*storage.Image{ { Id: "sha1", @@ -235,11 +237,13 @@ func testImages() []*storage.Image { BaseImageId: "base-sha2", BaseImageFullName: "alpine:3.12", BaseImageDigest: "sha256:alpine312", + Created: t3, }, { BaseImageId: "base-sha3", BaseImageFullName: "busybox:latest", BaseImageDigest: "sha256:busybox1", + Created: t3, }, }, }, From 9230d7b5b608ac083e0d02e6392db4a35166f3e4 Mon Sep 17 00:00:00 2001 From: cdu Date: Fri, 23 Jan 2026 11:15:14 -0800 Subject: [PATCH 17/18] stablize --- central/graphql/resolvers/images_test.go | 6 +++--- central/graphql/resolvers/test_utils.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/central/graphql/resolvers/images_test.go b/central/graphql/resolvers/images_test.go index 637f8fbf38e4d..661ddb5e34f1b 100644 --- a/central/graphql/resolvers/images_test.go +++ b/central/graphql/resolvers/images_test.go @@ -286,9 +286,9 @@ func (s *ImageResolversTestSuite) TestDeployments() { assert.Equal(t, expectedSha, actualBaseImage.ImageSha(testCtx)) // Test name array - expectedNames := make([]string, 0, len(baseImageInfos)) - for _, info := range baseImageInfos { - expectedNames = append(expectedNames, info.GetBaseImageFullName()) + expectedNames := []string{ + baseImageInfos[1].GetBaseImageFullName(), + baseImageInfos[0].GetBaseImageFullName(), } assert.Equal(t, expectedNames, actualBaseImage.Names(testCtx)) diff --git a/central/graphql/resolvers/test_utils.go b/central/graphql/resolvers/test_utils.go index 9250da2ba7f3b..d8ffb0e60db62 100644 --- a/central/graphql/resolvers/test_utils.go +++ b/central/graphql/resolvers/test_utils.go @@ -235,13 +235,13 @@ func testImages() []*storage.Image { BaseImageInfo: []*storage.BaseImageInfo{ { BaseImageId: "base-sha2", - BaseImageFullName: "alpine:3.12", + BaseImageFullName: "busybox:latest", BaseImageDigest: "sha256:alpine312", Created: t3, }, { BaseImageId: "base-sha3", - BaseImageFullName: "busybox:latest", + BaseImageFullName: "alpine:3.12", BaseImageDigest: "sha256:busybox1", Created: t3, }, From 1bb5b8d283011cac1a8e2b1ca0b04608b0c1498a Mon Sep 17 00:00:00 2001 From: cdu Date: Fri, 23 Jan 2026 14:47:09 -0800 Subject: [PATCH 18/18] one missing line --- central/graphql/resolvers/images.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/central/graphql/resolvers/images.go b/central/graphql/resolvers/images.go index 031af997abb41..ce9de9967635a 100644 --- a/central/graphql/resolvers/images.go +++ b/central/graphql/resolvers/images.go @@ -2,6 +2,7 @@ package resolvers import ( "context" + "slices" "time" "github.com/graph-gophers/graphql-go" @@ -559,6 +560,8 @@ func (resolver *Resolver) wrapBaseImage(baseImageInfos []*storage.BaseImageInfo) names = append(names, info.GetBaseImageFullName()) } + // Stablize the names + slices.Sort(names) data := &baseImageData{ imageSha: imageSha, names: names,