Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6f9326c
ROX-32105: Fix base image graphql interface
c-du Jan 6, 2026
4bec747
one more
c-du Jan 7, 2026
69b6bc0
add tests
c-du Jan 12, 2026
4021079
fix field name
c-du Jan 12, 2026
fe1d3ed
resolve review comment
c-du Jan 16, 2026
0cfda39
Add baseimage resolver and skip baseimageinfo
c-du Jan 16, 2026
1be1c31
Merge branch 'master' into base-image-graphql
c-du Jan 17, 2026
05b5f3a
Merge branch 'base-image-graphql' into cong/graphql-info
c-du Jan 19, 2026
cfbcfd1
Merge branch 'master' into cong/graphql-info
c-du Jan 20, 2026
6eb5a44
fix
c-du Jan 20, 2026
371803e
ROX-32647: Add base image assessment card to Image Details page
sachaudh Jan 14, 2026
27881fb
ROX-32647: Improve base image assessment card styling and support mul…
sachaudh Jan 14, 2026
4446c36
ROX-32647: Simplify BaseImageAssessmentCard component
sachaudh Jan 14, 2026
d054bf0
ROX-32647: Enable inBaseImageLayer field and remove mock data
sachaudh Jan 14, 2026
d6fbffe
test(ui): addressing comments
sachaudh Jan 19, 2026
49ea8d6
fix(base-images): correctly unwrap nested response in addBaseImage
sachaudh Jan 19, 2026
33bd838
ROX-32647: Update base image assessment to use new GraphQL resolver
sachaudh Jan 20, 2026
50be840
ROX-32647: Use LabelGroup for base image names
sachaudh Jan 20, 2026
7ea335a
Merge branch 'master' into cong/graphql-info
c-du Jan 23, 2026
e6a4f88
integrate with created
c-du Jan 23, 2026
9230d7b
stablize
c-du Jan 23, 2026
1bb5b8d
one missing line
c-du Jan 23, 2026
f96cd86
Merge branch 'cong/graphql-info' into ROX-32647-base-image-assessment…
c-du Jan 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions central/graphql/resolvers/gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}),
Expand All @@ -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",
Expand All @@ -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",
Expand Down
82 changes: 0 additions & 82 deletions central/graphql/resolvers/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 71 additions & 1 deletion central/graphql/resolvers/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package resolvers

import (
"context"
"slices"
"time"

"github.com/graph-gophers/graphql-go"
Expand Down Expand Up @@ -48,9 +49,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)
Expand Down Expand Up @@ -88,8 +89,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!",
Expand Down Expand Up @@ -515,3 +522,66 @@ 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 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))
for _, info := range baseImageInfos {
names = append(names, info.GetBaseImageFullName())
}

// Stablize the names
slices.Sort(names)
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
}
42 changes: 33 additions & 9 deletions central/graphql/resolvers/images_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
"strings"
"testing"
"time"

"github.com/graph-gophers/graphql-go"
"github.com/stackrox/rox/central/graphql/resolvers/loaders"
Expand All @@ -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"
Expand Down Expand Up @@ -267,16 +269,38 @@ 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, 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)
} 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 := []string{
baseImageInfos[1].GetBaseImageFullName(),
baseImageInfos[0].GetBaseImageFullName(),
}
assert.Equal(t, expectedNames, actualBaseImage.Names(testCtx))

// 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
Expand Down
7 changes: 7 additions & 0 deletions central/graphql/resolvers/images_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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!",
Expand Down Expand Up @@ -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)
}
8 changes: 6 additions & 2 deletions central/graphql/resolvers/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -233,13 +235,15 @@ 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,
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ function ImagePage({
tag: '',
}
}
baseImage={imageData?.baseImage ?? null}
refetchAll={refetchAll}
pagination={pagination}
vulnerabilityState={vulnerabilityState}
Expand Down
Loading
Loading