Skip to content

Commit cb2b6fe

Browse files
authored
fix version handling for top level and dependency components (#855)
* fix version handling for top level and dependency components Signed-off-by: pxp928 <[email protected]> * add sanitization to name string in generated purl Signed-off-by: pxp928 <[email protected]> --------- Signed-off-by: pxp928 <[email protected]>
1 parent f43dacb commit cb2b6fe

File tree

4 files changed

+252
-46
lines changed

4 files changed

+252
-46
lines changed

pkg/assembler/helpers/purl.go

+21-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package helpers
1717

1818
import (
1919
"fmt"
20+
"net/url"
2021
"path/filepath"
2122
"strings"
2223

@@ -27,6 +28,7 @@ import (
2728
const (
2829
PurlTypeGuac = "guac"
2930
PurlFilesGuac = "pkg:guac/files/"
31+
PurlPkgGuac = "pkg:guac/pkg/"
3032
)
3133

3234
// PurlToPkg converts a purl URI string into a graphql package node
@@ -159,21 +161,36 @@ func pkg(typ, namespace, name, version, subpath string, qualifiers map[string]st
159161
return p
160162
}
161163

164+
func SanitizeString(s string) string {
165+
escapedName := ""
166+
if strings.Contains(s, "/") {
167+
var ns []string
168+
for _, item := range strings.Split(s, "/") {
169+
ns = append(ns, url.QueryEscape(item))
170+
}
171+
escapedName = strings.Join(ns, "/")
172+
} else {
173+
escapedName = url.QueryEscape(s)
174+
}
175+
return escapedName
176+
}
177+
162178
func GuacPkgPurl(pkgName string, pkgVersion *string) string {
179+
escapedName := SanitizeString(pkgName)
163180
if pkgVersion == nil {
164-
return fmt.Sprintf("pkg:guac/pkg/%s", pkgName)
181+
return fmt.Sprintf(PurlPkgGuac+"%s", escapedName)
165182
}
166-
return fmt.Sprintf("pkg:guac/pkg/%s@%s", pkgName, *pkgVersion)
183+
return fmt.Sprintf(PurlPkgGuac+"%s@%s", escapedName, *pkgVersion)
167184
}
168185

169186
func GuacFilePurl(alg string, digest string, filename *string) string {
170187
s := fmt.Sprintf(PurlFilesGuac+"%s:%s", strings.ToLower(alg), digest)
171188
if filename != nil {
172-
s += fmt.Sprintf("#%s", *filename)
189+
s += fmt.Sprintf("#%s", SanitizeString(*filename))
173190
}
174191
return s
175192
}
176193

177194
func GuacGenericPurl(s string) string {
178-
return fmt.Sprintf("pkg:guac/generic/%s", s)
195+
return fmt.Sprintf("pkg:guac/generic/%s", SanitizeString(s))
179196
}

pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go

+68-39
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import (
3232
"github.com/guacsec/guac/pkg/logging"
3333
)
3434

35+
const topCdxPurlGuac string = "pkg:guac/cdx/"
36+
3537
type cyclonedxParser struct {
3638
doc *processor.Document
3739
packagePackages map[string][]*model.PkgInputSpec
@@ -76,35 +78,12 @@ func (c *cyclonedxParser) getTopLevelPackage(cdxBom *cdx.BOM) error {
7678
purl := cdxBom.Metadata.Component.PackageURL
7779
if cdxBom.Metadata.Component.PackageURL == "" {
7880
if cdxBom.Metadata.Component.Type == cdx.ComponentTypeContainer {
79-
splitImage := strings.Split(cdxBom.Metadata.Component.Name, "/")
80-
splitTag := strings.Split(splitImage[len(splitImage)-1], ":")
81-
var repositoryURL string
82-
var tag string
83-
84-
switch len(splitImage) {
85-
case 3:
86-
repositoryURL = splitImage[0] + "/" + splitImage[1] + "/" + splitTag[0]
87-
case 2:
88-
repositoryURL = splitImage[0] + "/" + splitTag[0]
89-
case 1:
90-
repositoryURL = splitImage[0]
91-
default:
92-
repositoryURL = ""
93-
}
94-
95-
if len(splitTag) == 2 {
96-
tag = splitTag[1]
97-
}
98-
if repositoryURL != "" {
99-
purl = guacCDXPkgPurl(repositoryURL, cdxBom.Metadata.Component.Version, tag)
100-
} else {
101-
purl = guacCDXPkgPurl(cdxBom.Metadata.Component.Name, cdxBom.Metadata.Component.Version, tag)
102-
}
81+
purl = parseContainerType(cdxBom.Metadata.Component.Name, cdxBom.Metadata.Component.Version, true)
10382
} else if cdxBom.Metadata.Component.Type == cdx.ComponentTypeFile {
104-
// example: file type ("/home/work/test/build/webserver/")
105-
purl = guacCDXFilePurl(cdxBom.Metadata.Component.Name, cdxBom.Metadata.Component.Version)
83+
// example: file type ("/home/work/test/build/webserver")
84+
purl = guacCDXFilePurl(cdxBom.Metadata.Component.Name, cdxBom.Metadata.Component.Version, true)
10685
} else {
107-
purl = guacCDXPkgPurl(cdxBom.Metadata.Component.Name, cdxBom.Metadata.Component.Version, "")
86+
purl = guacCDXPkgPurl(cdxBom.Metadata.Component.Name, cdxBom.Metadata.Component.Version, "", true)
10887
}
10988
}
11089

@@ -130,6 +109,33 @@ func (c *cyclonedxParser) getTopLevelPackage(cdxBom *cdx.BOM) error {
130109
return nil
131110
}
132111

112+
func parseContainerType(name string, version string, topLevel bool) string {
113+
splitImage := strings.Split(name, "/")
114+
splitTag := strings.Split(splitImage[len(splitImage)-1], ":")
115+
var repositoryURL string
116+
var tag string
117+
118+
switch len(splitImage) {
119+
case 3:
120+
repositoryURL = splitImage[0] + "/" + splitImage[1] + "/" + splitTag[0]
121+
case 2:
122+
repositoryURL = splitImage[0] + "/" + splitTag[0]
123+
case 1:
124+
repositoryURL = splitImage[0]
125+
default:
126+
repositoryURL = ""
127+
}
128+
129+
if len(splitTag) == 2 {
130+
tag = splitTag[1]
131+
}
132+
if repositoryURL != "" {
133+
return guacCDXPkgPurl(repositoryURL, version, tag, topLevel)
134+
} else {
135+
return guacCDXPkgPurl(name, version, tag, topLevel)
136+
}
137+
}
138+
133139
func (c *cyclonedxParser) getPackages(cdxBom *cdx.BOM) error {
134140
if cdxBom.Components != nil {
135141
for _, comp := range *cdxBom.Components {
@@ -139,8 +145,10 @@ func (c *cyclonedxParser) getPackages(cdxBom *cdx.BOM) error {
139145
if comp.Type != cdx.ComponentTypeOS {
140146
purl := comp.PackageURL
141147
if purl == "" {
142-
if comp.Type == cdx.ComponentTypeFile {
143-
purl = guacCDXFilePurl(comp.Name, comp.Version)
148+
if comp.Type == cdx.ComponentTypeContainer {
149+
purl = parseContainerType(comp.Name, comp.Version, false)
150+
} else if comp.Type == cdx.ComponentTypeFile {
151+
purl = guacCDXFilePurl(comp.Name, comp.Version, false)
144152
} else {
145153
purl = asmhelpers.GuacPkgPurl(comp.Name, &comp.Version)
146154
}
@@ -257,25 +265,46 @@ func (s *cyclonedxParser) getPackageElement(elementID string) []*model.PkgInputS
257265
return nil
258266
}
259267

260-
func guacCDXFilePurl(fileName string, version string) string {
261-
if version != "" {
262-
splitVersion := strings.Split(version, ":")
263-
return asmhelpers.GuacFilePurl(splitVersion[0], splitVersion[1], &fileName)
268+
func guacCDXFilePurl(fileName string, version string, topLevel bool) string {
269+
escapedName := asmhelpers.SanitizeString(fileName)
270+
if topLevel {
271+
if version != "" {
272+
splitVersion := strings.Split(version, ":")
273+
if len(splitVersion) == 2 {
274+
s := fmt.Sprintf(topCdxPurlGuac+"%s:%s", strings.ToLower(splitVersion[0]), splitVersion[1])
275+
s += fmt.Sprintf("#%s", escapedName)
276+
return s
277+
}
278+
}
279+
return topCdxPurlGuac + escapedName
264280
} else {
265-
return asmhelpers.PurlFilesGuac + fileName
281+
if version != "" {
282+
splitVersion := strings.Split(version, ":")
283+
if len(splitVersion) == 2 {
284+
return asmhelpers.GuacFilePurl(splitVersion[0], splitVersion[1], &escapedName)
285+
}
286+
}
287+
return asmhelpers.PurlFilesGuac + escapedName
266288
}
267289
}
268290

269-
func guacCDXPkgPurl(componentName string, version string, tag string) string {
291+
func guacCDXPkgPurl(componentName string, version string, tag string, topLevel bool) string {
270292
purl := ""
293+
typeNamespaceString := ""
294+
escapedName := asmhelpers.SanitizeString(componentName)
295+
if topLevel {
296+
typeNamespaceString = topCdxPurlGuac
297+
} else {
298+
typeNamespaceString = asmhelpers.PurlPkgGuac
299+
}
271300
if version != "" && tag != "" {
272-
purl = "pkg:guac/cdx/" + componentName + "@" + version + "?tag=" + tag
301+
purl = typeNamespaceString + escapedName + "@" + version + "?tag=" + tag
273302
} else if version != "" {
274-
purl = "pkg:guac/cdx/" + componentName + "@" + version
303+
purl = typeNamespaceString + escapedName + "@" + version
275304
} else if tag != "" {
276-
purl = "pkg:guac/cdx/" + componentName + "?tag=" + tag
305+
purl = typeNamespaceString + escapedName + "?tag=" + tag
277306
} else {
278-
purl = "pkg:guac/cdx/" + componentName
307+
purl = typeNamespaceString + escapedName
279308
}
280309
return purl
281310
}

pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go

+162-2
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ func Test_cyclonedxParser_addRootPackage(t *testing.T) {
247247
},
248248
},
249249
},
250-
wantPurl: "pkg:guac/files/sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870#/home/work/test/build/webserver",
250+
wantPurl: "pkg:guac/cdx/sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870#/home/work/test/build/webserver",
251251
}, {
252252
name: "file type - purl nor provided, version not provided",
253253
cdxBom: &cdx.BOM{
@@ -258,7 +258,7 @@ func Test_cyclonedxParser_addRootPackage(t *testing.T) {
258258
},
259259
},
260260
},
261-
wantPurl: "pkg:guac/files/home/work/test/build/webserver",
261+
wantPurl: "pkg:guac/cdx/home/work/test/build/webserver",
262262
}}
263263
for _, tt := range tests {
264264
t.Run(tt.name, func(t *testing.T) {
@@ -287,3 +287,163 @@ func Test_cyclonedxParser_addRootPackage(t *testing.T) {
287287
})
288288
}
289289
}
290+
291+
func Test_cyclonedxParser_getComponentPackages(t *testing.T) {
292+
tests := []struct {
293+
name string
294+
cdxBom *cdx.BOM
295+
wantPurl string
296+
}{{
297+
name: "purl provided",
298+
cdxBom: &cdx.BOM{
299+
Components: &[]cdx.Component{{
300+
Name: "gcr.io/distroless/static:nonroot",
301+
Type: cdx.ComponentTypeContainer,
302+
Version: "sha256:6ad5b696af3ca05a048bd29bf0f623040462638cb0b29c8d702cbb2805687388",
303+
PackageURL: "pkg:oci/static@sha256:6ad5b696af3ca05a048bd29bf0f623040462638cb0b29c8d702cbb2805687388?repository_url=gcr.io/distroless/static&tag=nonroot",
304+
}},
305+
},
306+
wantPurl: "pkg:oci/static@sha256:6ad5b696af3ca05a048bd29bf0f623040462638cb0b29c8d702cbb2805687388?repository_url=gcr.io/distroless/static&tag=nonroot",
307+
}, {
308+
name: "gcr.io/distroless/static:nonroot - purl not provided",
309+
cdxBom: &cdx.BOM{
310+
Components: &[]cdx.Component{{
311+
Name: "gcr.io/distroless/static:nonroot",
312+
Type: cdx.ComponentTypeContainer,
313+
Version: "sha256:6ad5b696af3ca05a048bd29bf0f623040462638cb0b29c8d702cbb2805687388",
314+
}},
315+
},
316+
wantPurl: "pkg:guac/pkg/gcr.io/distroless/static@sha256:6ad5b696af3ca05a048bd29bf0f623040462638cb0b29c8d702cbb2805687388?tag=nonroot",
317+
}, {
318+
name: "gcr.io/distroless/static - purl not provided, tag not specified",
319+
320+
cdxBom: &cdx.BOM{
321+
Components: &[]cdx.Component{{
322+
Name: "gcr.io/distroless/static",
323+
Type: cdx.ComponentTypeContainer,
324+
Version: "sha256:6ad5b696af3ca05a048bd29bf0f623040462638cb0b29c8d702cbb2805687388",
325+
}},
326+
},
327+
wantPurl: "pkg:guac/pkg/gcr.io/distroless/static@sha256:6ad5b696af3ca05a048bd29bf0f623040462638cb0b29c8d702cbb2805687388?tag=",
328+
}, {
329+
name: "gcr.io/distroless/static - purl not provided, tag not specified, version not specified",
330+
331+
cdxBom: &cdx.BOM{
332+
Components: &[]cdx.Component{{
333+
Name: "gcr.io/distroless/static",
334+
Type: cdx.ComponentTypeContainer,
335+
}},
336+
},
337+
wantPurl: "pkg:guac/pkg/gcr.io/distroless/static@?tag=",
338+
}, {
339+
name: "library/debian:latest - purl not provided, assume docker.io",
340+
341+
cdxBom: &cdx.BOM{
342+
Components: &[]cdx.Component{{
343+
Name: "library/debian:latest",
344+
Type: cdx.ComponentTypeContainer,
345+
Version: "sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
346+
}},
347+
},
348+
wantPurl: "pkg:guac/pkg/library/debian@sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870?tag=latest",
349+
}, {
350+
name: "library/debian - purl not provided, tag not specified",
351+
cdxBom: &cdx.BOM{
352+
Components: &[]cdx.Component{{
353+
Name: "library/debian",
354+
Type: cdx.ComponentTypeContainer,
355+
Version: "sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
356+
}},
357+
},
358+
wantPurl: "pkg:guac/pkg/library/debian@sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870?tag=",
359+
}, {
360+
name: "library - purl not provided, tag not specified",
361+
cdxBom: &cdx.BOM{
362+
Components: &[]cdx.Component{{
363+
Name: "library",
364+
Type: cdx.ComponentTypeContainer,
365+
Version: "sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
366+
}},
367+
},
368+
wantPurl: "pkg:guac/pkg/library@sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870?tag=",
369+
}, {
370+
name: "name split length too long, tag not specified",
371+
cdxBom: &cdx.BOM{
372+
Components: &[]cdx.Component{{
373+
Name: "ghcr.io/guacsec/guac/guacsec",
374+
Type: cdx.ComponentTypeContainer,
375+
Version: "sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
376+
}},
377+
},
378+
wantPurl: "pkg:guac/pkg/ghcr.io/guacsec/guac/guacsec@sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
379+
}, {
380+
name: "name contains local registry, tag specified",
381+
cdxBom: &cdx.BOM{
382+
Components: &[]cdx.Component{{
383+
Name: "foo.registry.com:4443/myapp/debian:latest",
384+
Type: cdx.ComponentTypeContainer,
385+
Version: "sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
386+
}},
387+
},
388+
wantPurl: "pkg:guac/pkg/foo.registry.com:4443/myapp/debian@sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870?tag=latest",
389+
}, {
390+
name: "ComponentTypeLibrary",
391+
392+
cdxBom: &cdx.BOM{
393+
Components: &[]cdx.Component{{
394+
Name: "ghcr.io/guacsec/guac/guacsec",
395+
Type: cdx.ComponentTypeLibrary,
396+
Version: "sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
397+
}},
398+
},
399+
wantPurl: "pkg:guac/pkg/ghcr.io/guacsec/guac/guacsec@sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
400+
}, {
401+
name: "file type - purl nor provided, version provided",
402+
cdxBom: &cdx.BOM{
403+
Components: &[]cdx.Component{{
404+
Name: "/home/work/test/build/webserver",
405+
Type: cdx.ComponentTypeFile,
406+
Version: "sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
407+
}},
408+
},
409+
wantPurl: "pkg:guac/files/sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870#/home/work/test/build/webserver",
410+
}, {
411+
name: "file type - purl nor provided, version not provided",
412+
cdxBom: &cdx.BOM{
413+
Components: &[]cdx.Component{{
414+
Name: "/home/work/test/build/webserver",
415+
Type: cdx.ComponentTypeFile,
416+
}},
417+
},
418+
wantPurl: "pkg:guac/files/home/work/test/build/webserver",
419+
}}
420+
for _, tt := range tests {
421+
t.Run(tt.name, func(t *testing.T) {
422+
c := &cyclonedxParser{
423+
doc: &processor.Document{
424+
SourceInformation: processor.SourceInformation{
425+
Collector: "test",
426+
Source: "test",
427+
},
428+
},
429+
packagePackages: map[string][]*model.PkgInputSpec{},
430+
identifierStrings: &common.IdentifierStrings{},
431+
}
432+
c.cdxBom = tt.cdxBom
433+
if err := c.getPackages(tt.cdxBom); err != nil {
434+
t.Errorf("Failed to getTopLevelPackage %s", err)
435+
}
436+
wantPackage, err := asmhelpers.PurlToPkg(tt.wantPurl)
437+
if err != nil {
438+
t.Errorf("Failed to parse purl %v %v", tt.wantPurl, err)
439+
}
440+
for _, comp := range *tt.cdxBom.Components {
441+
if d := cmp.Diff(*wantPackage, *c.packagePackages[comp.BOMRef][0]); len(d) != 0 {
442+
t.Errorf("addRootPackage failed to produce expected package for %v", tt.name)
443+
t.Errorf("spdx.GetPredicate mismatch values (+got, -expected): %s", d)
444+
}
445+
}
446+
447+
})
448+
}
449+
}

0 commit comments

Comments
 (0)