Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit 37037e9

Browse files
committed
Add query for image details
1 parent ba294b2 commit 37037e9

File tree

6 files changed

+163
-104
lines changed

6 files changed

+163
-104
lines changed

query/async.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@ import (
2424

2525
type queryResult struct {
2626
Cves []types.Cve
27-
BaseImages []types.BaseImage
27+
BaseImages []types.BaseImageMatch
28+
Image *types.BaseImage
2829
Error error
2930
}
3031

3132
func ForCvesAndBaseImagesAsync(sb *types.Sbom, includeCves bool, includeBaseImages bool, workspace string, apiKey string) *types.Sbom {
32-
resultChan := make(chan queryResult, 2)
33+
resultChan := make(chan queryResult, 3)
3334
var wg sync.WaitGroup
3435
if includeCves {
3536
wg.Add(1)
@@ -49,7 +50,7 @@ func ForCvesAndBaseImagesAsync(sb *types.Sbom, includeCves bool, includeBaseImag
4950
}()
5051
}
5152
if includeBaseImages {
52-
wg.Add(1)
53+
wg.Add(2)
5354
go func() {
5455
defer wg.Done()
5556
bi, err := ForBaseImageInGraphQL(sb.Source.Image.Config, true)
@@ -64,6 +65,20 @@ func ForCvesAndBaseImagesAsync(sb *types.Sbom, includeCves bool, includeBaseImag
6465
}
6566
}
6667
}()
68+
go func() {
69+
defer wg.Done()
70+
bi, err := ForImageInGraphQL(sb)
71+
if err != nil {
72+
resultChan <- queryResult{
73+
Error: err,
74+
}
75+
}
76+
if bi != nil && bi.ImageDetailsByDigest.Digest != "" {
77+
resultChan <- queryResult{
78+
Image: &bi.ImageDetailsByDigest,
79+
}
80+
}
81+
}()
6782
}
6883
wg.Wait()
6984
close(resultChan)
@@ -75,6 +90,9 @@ func ForCvesAndBaseImagesAsync(sb *types.Sbom, includeCves bool, includeBaseImag
7590
if result.Cves != nil {
7691
sb.Vulnerabilities = result.Cves
7792
}
93+
if result.Image != nil {
94+
sb.Source.Image.Details = result.Image
95+
}
7896
}
7997

8098
return sb

query/base.go

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -246,12 +246,7 @@ func ForBaseImageInGraphQL(cfg *v1.ConfigFile, excludeSelf bool) (*types.BaseIma
246246
for ii, _ := range q.ImagesByDiffIds {
247247
for bi, _ := range q.ImagesByDiffIds[ii].Images {
248248
count++
249-
if q.ImagesByDiffIds[ii].Images[bi].Repository.Host == "hub.docker.com" && strings.Index(q.ImagesByDiffIds[ii].Images[bi].Repository.Repo, "/") < 0 {
250-
q.ImagesByDiffIds[ii].Images[bi].Repository.Badge = "OFFICIAL"
251-
}
252-
if q.ImagesByDiffIds[ii].Images[bi].Repository.Badge != "" {
253-
q.ImagesByDiffIds[ii].Images[bi].Repository.Badge = strings.ToLower(q.ImagesByDiffIds[ii].Images[bi].Repository.Badge)
254-
}
249+
q.ImagesByDiffIds[ii].Images[bi].Repository = normalizeRepository(&q.ImagesByDiffIds[ii].Images[bi]).Repository
255250
}
256251
}
257252
if count == 1 {
@@ -261,3 +256,35 @@ func ForBaseImageInGraphQL(cfg *v1.ConfigFile, excludeSelf bool) (*types.BaseIma
261256
}
262257
return &q, nil
263258
}
259+
260+
func ForImageInGraphQL(sb *types.Sbom) (*types.ImageByDigestQuery, error) {
261+
url := "https://api.dso.docker.com/v1/graphql"
262+
client := graphql.NewClient(url, nil)
263+
variables := map[string]interface{}{
264+
"digest": sb.Source.Image.Digest,
265+
"os": sb.Source.Image.Platform.Os,
266+
"architecture": sb.Source.Image.Platform.Architecture,
267+
"variant": sb.Source.Image.Platform.Variant,
268+
}
269+
270+
var q types.ImageByDigestQuery
271+
err := client.Query(context.Background(), &q, variables)
272+
if err != nil {
273+
fmt.Sprintf("error %v", err)
274+
return nil, errors.Wrapf(err, "failed to run query")
275+
}
276+
if q.ImageDetailsByDigest.Digest != "" {
277+
q.ImageDetailsByDigest.Repository = normalizeRepository(&q.ImageDetailsByDigest).Repository
278+
}
279+
return &q, nil
280+
}
281+
282+
func normalizeRepository(image *types.BaseImage) *types.BaseImage {
283+
if image.Repository.Host == "hub.docker.com" && strings.Index(image.Repository.Repo, "/") < 0 {
284+
image.Repository.Badge = "OFFICIAL"
285+
}
286+
if image.Repository.Badge != "" {
287+
image.Repository.Badge = strings.ToLower(image.Repository.Badge)
288+
}
289+
return image
290+
}

registry/save.go

Lines changed: 58 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,11 @@ func (i ImageId) String() string {
6464
}
6565

6666
type ImageCache struct {
67-
Digest string
68-
Name string
67+
Id string
68+
Digest string
69+
Name string
70+
Tags []string
71+
6972
Image *v1.Image
7073
ImagePath string
7174
Ref *name.Reference
@@ -180,57 +183,75 @@ func SaveImage(image string, cli command.Cli) (*ImageCache, error) {
180183
return tarFileName, nil
181184
}
182185

183-
desc, err := remote.Get(ref, withAuth())
184-
if err != nil {
186+
// check local first because it is the fastest
187+
im, _, err := cli.Client().ImageInspectWithRaw(context.Background(), image)
188+
if err == nil {
185189
img, err := daemon.Image(ImageId{name: image}, daemon.WithClient(cli.Client()))
186190
if err != nil {
187191
return nil, errors.Wrapf(err, "failed to pull image: %s", image)
188-
} else {
189-
im, _, err := cli.Client().ImageInspectWithRaw(context.Background(), image)
190-
if err != nil {
191-
return nil, errors.Wrapf(err, "failed to get local image: %s", image)
192-
}
193-
imagePath, err := createPaths(im.ID)
194-
if err != nil {
195-
return nil, errors.Wrapf(err, "failed to create cache paths")
196-
}
197-
return &ImageCache{
198-
Digest: im.ID,
199-
Name: image,
200-
Image: &img,
201-
Ref: &ref,
202-
ImagePath: imagePath,
203-
copy: true,
204-
cli: cli,
205-
}, nil
206192
}
207-
} else {
208-
img, err := desc.Image()
193+
imagePath, err := createPaths(im.ID)
209194
if err != nil {
210-
return nil, errors.Wrapf(err, "failed to pull image: %s", image)
195+
return nil, errors.Wrapf(err, "failed to create cache paths")
211196
}
212-
var digest string
213-
identifier := ref.Identifier()
214-
if strings.HasPrefix(identifier, "sha256:") {
215-
digest = identifier
216-
} else {
217-
digestHash, _ := img.Digest()
218-
digest = digestHash.String()
197+
var name, digest string
198+
tags := make([]string, 0)
199+
for _, d := range im.RepoDigests {
200+
name = strings.Split(d, "@")[0]
201+
digest = strings.Split(d, "@")[1]
219202
}
220-
imagePath, err := createPaths(digest)
221-
if err != nil {
222-
return nil, errors.Wrapf(err, "failed to create cache paths")
203+
for _, t := range im.RepoTags {
204+
name = strings.Split(t, ":")[0]
205+
tags = append(tags, strings.Split(t, ":")[1])
223206
}
224207
return &ImageCache{
225-
Digest: digest,
226-
Name: image,
208+
Id: im.ID,
209+
Digest: digest,
210+
Name: name,
211+
Tags: tags,
212+
227213
Image: &img,
228214
Ref: &ref,
229215
ImagePath: imagePath,
230216
copy: true,
231217
cli: cli,
232218
}, nil
233219
}
220+
// try remote image next
221+
desc, err := remote.Get(ref, withAuth())
222+
if err != nil {
223+
return nil, errors.Wrapf(err, "failed to pull image: %s", image)
224+
}
225+
img, err := desc.Image()
226+
if err != nil {
227+
return nil, errors.Wrapf(err, "failed to pull image: %s", image)
228+
}
229+
var digest string
230+
tags := make([]string, 0)
231+
identifier := ref.Identifier()
232+
if strings.HasPrefix(identifier, "sha256:") {
233+
digest = identifier
234+
} else {
235+
digestHash, _ := img.Digest()
236+
digest = digestHash.String()
237+
tags = append(tags, identifier)
238+
}
239+
imagePath, err := createPaths(digest)
240+
if err != nil {
241+
return nil, errors.Wrapf(err, "failed to create cache paths")
242+
}
243+
return &ImageCache{
244+
Id: digest,
245+
Digest: digest,
246+
Name: image,
247+
Image: &img,
248+
Tags: tags,
249+
250+
Ref: &ref,
251+
ImagePath: imagePath,
252+
copy: true,
253+
cli: cli,
254+
}, nil
234255
}
235256

236257
func withAuth() remote.Option {

sbom/index.go

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import (
3030
"github.com/docker/index-cli-plugin/query"
3131
"github.com/docker/index-cli-plugin/registry"
3232
"github.com/docker/index-cli-plugin/types"
33-
"github.com/google/go-containerregistry/pkg/name"
3433
v1 "github.com/google/go-containerregistry/pkg/v1"
3534
"github.com/pkg/errors"
3635
)
@@ -80,7 +79,7 @@ func IndexImage(image string, cli command.Cli) (*types.Sbom, error) {
8079

8180
func indexImage(cache *registry.ImageCache, cli command.Cli) (*types.Sbom, error) {
8281
configFilePath := cli.ConfigFile().Filename
83-
sbomFilePath := filepath.Join(filepath.Dir(configFilePath), "sbom", "sha256", cache.Digest[7:], "sbom.json")
82+
sbomFilePath := filepath.Join(filepath.Dir(configFilePath), "sbom", "sha256", cache.Id[7:], "sbom.json")
8483
if sbom := cachedSbom(sbomFilePath); sbom != nil {
8584
return sbom, nil
8685
}
@@ -118,27 +117,14 @@ func indexImage(cache *registry.ImageCache, cli command.Cli) (*types.Sbom, error
118117
config, _ := (*cache.Image).RawConfigFile()
119118
c, _ := (*cache.Image).ConfigFile()
120119
m, _ := (*cache.Image).Manifest()
121-
d, _ := (*cache.Image).Digest()
122-
123-
var tag []string
124-
if cache.Name != "" {
125-
ref, err := name.ParseReference(cache.Name)
126-
if err != nil {
127-
return nil, errors.Wrapf(err, "failed to parse reference: %s", cache.Name)
128-
}
129-
cache.Name = ref.Context().String()
130-
if !strings.HasPrefix(ref.Identifier(), "sha256:") {
131-
tag = []string{ref.Identifier()}
132-
}
133-
}
134120

135121
sbom := types.Sbom{
136122
Artifacts: packages,
137123
Source: types.Source{
138124
Type: "image",
139125
Image: types.ImageSource{
140126
Name: cache.Name,
141-
Digest: d.String(),
127+
Digest: cache.Digest,
142128
Manifest: m,
143129
Config: c,
144130
RawManifest: base64.StdEncoding.EncodeToString(manifest),
@@ -159,8 +145,8 @@ func indexImage(cache *registry.ImageCache, cli command.Cli) (*types.Sbom, error
159145
},
160146
}
161147

162-
if len(tag) > 0 {
163-
sbom.Source.Image.Tags = &tag
148+
if len(cache.Tags) > 0 {
149+
sbom.Source.Image.Tags = &cache.Tags
164150
}
165151

166152
js, err := json.MarshalIndent(sbom, "", " ")

types/graphql_types.go

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,50 @@
11
package types
22

33
type BaseImage struct {
4-
DiffIds []string `graphql:"matches" json:"diff_ids,omitempty"`
5-
Images []struct {
6-
CreatedAt string `graphql:"createdAt" json:"created_at,omitempty"`
7-
Digest string `graphql:"digest" json:"digest,omitempty"`
8-
Repository struct {
9-
Badge string `graphql:"badge" json:"badge,omitempty"`
10-
Host string `graphql:"hostName" json:"host,omitempty"`
11-
Repo string `graphql:"repoName" json:"repo,omitempty"`
12-
SupportedTags []string `graphql:"supportedTags" json:"supported_tags,omitempty"`
13-
PreferredTags []string `graphql:"preferredTags" json:"preferred_tags,omitempty"`
14-
} `graphql:"repository" json:"repository"`
15-
Tags []struct {
16-
Current bool `graphql:"current" json:"current"`
17-
Name string `graphql:"name" json:"name,omitempty"`
18-
Supported bool `graphql:"supported" json:"supported"`
19-
} `graphql:"tags" json:"tags,omitempty"`
20-
DockerFile struct {
21-
Commit struct {
22-
Repository struct {
23-
Org string `graphql:"orgName" json:"org,omitempty"`
24-
Repo string `graphql:"repoName" json:"repo,omitempty"`
25-
} `graphql:"repository" json:"repository,omitempty"`
26-
Sha string `graphql:"sha" json:"sha,omitempty"`
27-
} `json:"commit,omitempty"`
28-
Path string `graphql:"path" json:"path,omitempty"`
29-
} `graphql:"dockerFile" json:"docker_file,omitempty"`
30-
PackageCount int `graphql:"packageCount" json:"package_count,omitempty"`
31-
VulnerabilityReport struct {
32-
Critical int `graphql:"critical" json:"critical,omitempty"`
33-
High int `graphql:"high" json:"high,omitempty"`
34-
Medium int `graphql:"medium" json:"medium,omitempty"`
35-
Low int `graphql:"low" json:"low,omitempty"`
36-
Unspecified int `graphql:"unspecified" json:"unspecified,omitempty"`
37-
Total int `graphql:"total" json:"total,omitempty"`
38-
} `graphql:"vulnerabilityReport" json:"vulnerability_report"`
39-
} `graphql:"images" json:"images,omitempty"`
4+
CreatedAt string `graphql:"createdAt" json:"created_at,omitempty"`
5+
Digest string `graphql:"digest" json:"digest,omitempty"`
6+
Repository struct {
7+
Badge string `graphql:"badge" json:"badge,omitempty"`
8+
Host string `graphql:"hostName" json:"host,omitempty"`
9+
Repo string `graphql:"repoName" json:"repo,omitempty"`
10+
SupportedTags []string `graphql:"supportedTags" json:"supported_tags,omitempty"`
11+
PreferredTags []string `graphql:"preferredTags" json:"preferred_tags,omitempty"`
12+
} `graphql:"repository" json:"repository"`
13+
Tags []struct {
14+
Current bool `graphql:"current" json:"current"`
15+
Name string `graphql:"name" json:"name,omitempty"`
16+
Supported bool `graphql:"supported" json:"supported"`
17+
} `graphql:"tags" json:"tags,omitempty"`
18+
DockerFile struct {
19+
Commit struct {
20+
Repository struct {
21+
Org string `graphql:"orgName" json:"org,omitempty"`
22+
Repo string `graphql:"repoName" json:"repo,omitempty"`
23+
} `graphql:"repository" json:"repository,omitempty"`
24+
Sha string `graphql:"sha" json:"sha,omitempty"`
25+
} `json:"commit,omitempty"`
26+
Path string `graphql:"path" json:"path,omitempty"`
27+
} `graphql:"dockerFile" json:"docker_file,omitempty"`
28+
PackageCount int `graphql:"packageCount" json:"package_count,omitempty"`
29+
VulnerabilityReport struct {
30+
Critical int `graphql:"critical" json:"critical,omitempty"`
31+
High int `graphql:"high" json:"high,omitempty"`
32+
Medium int `graphql:"medium" json:"medium,omitempty"`
33+
Low int `graphql:"low" json:"low,omitempty"`
34+
Unspecified int `graphql:"unspecified" json:"unspecified,omitempty"`
35+
Total int `graphql:"total" json:"total,omitempty"`
36+
} `graphql:"vulnerabilityReport" json:"vulnerability_report"`
37+
}
38+
39+
type BaseImageMatch struct {
40+
DiffIds []string `graphql:"matches" json:"diff_ids,omitempty"`
41+
Images []BaseImage `graphql:"images" json:"images,omitempty"`
4042
}
4143

4244
type BaseImagesByDiffIdsQuery struct {
43-
ImagesByDiffIds []BaseImage `graphql:"imagesByDiffIds(context: {}, diffIds: $diffIds)"`
45+
ImagesByDiffIds []BaseImageMatch `graphql:"imagesByDiffIds(context: {}, diffIds: $diffIds)"`
46+
}
47+
48+
type ImageByDigestQuery struct {
49+
ImageDetailsByDigest BaseImage `graphql:"imageDetailsByDigest(context: {}, digest: $digest, platform: {os: $os, architecture: $architecture, variant: $variant})"`
4450
}

types/types.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ type ImageSource struct {
114114
Distro Distro `json:"distro"`
115115
Platform Platform `json:"platform"`
116116
Size int64 `json:"size"`
117+
Details *BaseImage `json:"details,omitempty""`
117118
}
118119

119120
type Descriptor struct {
@@ -123,9 +124,9 @@ type Descriptor struct {
123124
}
124125

125126
type Source struct {
126-
Type string `json:"type"`
127-
Image ImageSource `json:"image"`
128-
BaseImages []BaseImage `json:"base_images,omitempty"`
127+
Type string `json:"type"`
128+
Image ImageSource `json:"image"`
129+
BaseImages []BaseImageMatch `json:"base_images,omitempty"`
129130
}
130131

131132
type Sbom struct {

0 commit comments

Comments
 (0)