Skip to content

Commit

Permalink
fix version handling for top level and dependency components (#855)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
pxp928 authored May 19, 2023
1 parent f43dacb commit cb2b6fe
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 46 deletions.
25 changes: 21 additions & 4 deletions pkg/assembler/helpers/purl.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package helpers

import (
"fmt"
"net/url"
"path/filepath"
"strings"

Expand All @@ -27,6 +28,7 @@ import (
const (
PurlTypeGuac = "guac"
PurlFilesGuac = "pkg:guac/files/"
PurlPkgGuac = "pkg:guac/pkg/"
)

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

func SanitizeString(s string) string {
escapedName := ""
if strings.Contains(s, "/") {
var ns []string
for _, item := range strings.Split(s, "/") {
ns = append(ns, url.QueryEscape(item))
}
escapedName = strings.Join(ns, "/")
} else {
escapedName = url.QueryEscape(s)
}
return escapedName
}

func GuacPkgPurl(pkgName string, pkgVersion *string) string {
escapedName := SanitizeString(pkgName)
if pkgVersion == nil {
return fmt.Sprintf("pkg:guac/pkg/%s", pkgName)
return fmt.Sprintf(PurlPkgGuac+"%s", escapedName)
}
return fmt.Sprintf("pkg:guac/pkg/%s@%s", pkgName, *pkgVersion)
return fmt.Sprintf(PurlPkgGuac+"%s@%s", escapedName, *pkgVersion)
}

func GuacFilePurl(alg string, digest string, filename *string) string {
s := fmt.Sprintf(PurlFilesGuac+"%s:%s", strings.ToLower(alg), digest)
if filename != nil {
s += fmt.Sprintf("#%s", *filename)
s += fmt.Sprintf("#%s", SanitizeString(*filename))
}
return s
}

func GuacGenericPurl(s string) string {
return fmt.Sprintf("pkg:guac/generic/%s", s)
return fmt.Sprintf("pkg:guac/generic/%s", SanitizeString(s))
}
107 changes: 68 additions & 39 deletions pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import (
"github.com/guacsec/guac/pkg/logging"
)

const topCdxPurlGuac string = "pkg:guac/cdx/"

type cyclonedxParser struct {
doc *processor.Document
packagePackages map[string][]*model.PkgInputSpec
Expand Down Expand Up @@ -76,35 +78,12 @@ func (c *cyclonedxParser) getTopLevelPackage(cdxBom *cdx.BOM) error {
purl := cdxBom.Metadata.Component.PackageURL
if cdxBom.Metadata.Component.PackageURL == "" {
if cdxBom.Metadata.Component.Type == cdx.ComponentTypeContainer {
splitImage := strings.Split(cdxBom.Metadata.Component.Name, "/")
splitTag := strings.Split(splitImage[len(splitImage)-1], ":")
var repositoryURL string
var tag string

switch len(splitImage) {
case 3:
repositoryURL = splitImage[0] + "/" + splitImage[1] + "/" + splitTag[0]
case 2:
repositoryURL = splitImage[0] + "/" + splitTag[0]
case 1:
repositoryURL = splitImage[0]
default:
repositoryURL = ""
}

if len(splitTag) == 2 {
tag = splitTag[1]
}
if repositoryURL != "" {
purl = guacCDXPkgPurl(repositoryURL, cdxBom.Metadata.Component.Version, tag)
} else {
purl = guacCDXPkgPurl(cdxBom.Metadata.Component.Name, cdxBom.Metadata.Component.Version, tag)
}
purl = parseContainerType(cdxBom.Metadata.Component.Name, cdxBom.Metadata.Component.Version, true)
} else if cdxBom.Metadata.Component.Type == cdx.ComponentTypeFile {
// example: file type ("/home/work/test/build/webserver/")
purl = guacCDXFilePurl(cdxBom.Metadata.Component.Name, cdxBom.Metadata.Component.Version)
// example: file type ("/home/work/test/build/webserver")
purl = guacCDXFilePurl(cdxBom.Metadata.Component.Name, cdxBom.Metadata.Component.Version, true)
} else {
purl = guacCDXPkgPurl(cdxBom.Metadata.Component.Name, cdxBom.Metadata.Component.Version, "")
purl = guacCDXPkgPurl(cdxBom.Metadata.Component.Name, cdxBom.Metadata.Component.Version, "", true)
}
}

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

func parseContainerType(name string, version string, topLevel bool) string {
splitImage := strings.Split(name, "/")
splitTag := strings.Split(splitImage[len(splitImage)-1], ":")
var repositoryURL string
var tag string

switch len(splitImage) {
case 3:
repositoryURL = splitImage[0] + "/" + splitImage[1] + "/" + splitTag[0]
case 2:
repositoryURL = splitImage[0] + "/" + splitTag[0]
case 1:
repositoryURL = splitImage[0]
default:
repositoryURL = ""
}

if len(splitTag) == 2 {
tag = splitTag[1]
}
if repositoryURL != "" {
return guacCDXPkgPurl(repositoryURL, version, tag, topLevel)
} else {
return guacCDXPkgPurl(name, version, tag, topLevel)
}
}

func (c *cyclonedxParser) getPackages(cdxBom *cdx.BOM) error {
if cdxBom.Components != nil {
for _, comp := range *cdxBom.Components {
Expand All @@ -139,8 +145,10 @@ func (c *cyclonedxParser) getPackages(cdxBom *cdx.BOM) error {
if comp.Type != cdx.ComponentTypeOS {
purl := comp.PackageURL
if purl == "" {
if comp.Type == cdx.ComponentTypeFile {
purl = guacCDXFilePurl(comp.Name, comp.Version)
if comp.Type == cdx.ComponentTypeContainer {
purl = parseContainerType(comp.Name, comp.Version, false)
} else if comp.Type == cdx.ComponentTypeFile {
purl = guacCDXFilePurl(comp.Name, comp.Version, false)
} else {
purl = asmhelpers.GuacPkgPurl(comp.Name, &comp.Version)
}
Expand Down Expand Up @@ -257,25 +265,46 @@ func (s *cyclonedxParser) getPackageElement(elementID string) []*model.PkgInputS
return nil
}

func guacCDXFilePurl(fileName string, version string) string {
if version != "" {
splitVersion := strings.Split(version, ":")
return asmhelpers.GuacFilePurl(splitVersion[0], splitVersion[1], &fileName)
func guacCDXFilePurl(fileName string, version string, topLevel bool) string {
escapedName := asmhelpers.SanitizeString(fileName)
if topLevel {
if version != "" {
splitVersion := strings.Split(version, ":")
if len(splitVersion) == 2 {
s := fmt.Sprintf(topCdxPurlGuac+"%s:%s", strings.ToLower(splitVersion[0]), splitVersion[1])
s += fmt.Sprintf("#%s", escapedName)
return s
}
}
return topCdxPurlGuac + escapedName
} else {
return asmhelpers.PurlFilesGuac + fileName
if version != "" {
splitVersion := strings.Split(version, ":")
if len(splitVersion) == 2 {
return asmhelpers.GuacFilePurl(splitVersion[0], splitVersion[1], &escapedName)
}
}
return asmhelpers.PurlFilesGuac + escapedName
}
}

func guacCDXPkgPurl(componentName string, version string, tag string) string {
func guacCDXPkgPurl(componentName string, version string, tag string, topLevel bool) string {
purl := ""
typeNamespaceString := ""
escapedName := asmhelpers.SanitizeString(componentName)
if topLevel {
typeNamespaceString = topCdxPurlGuac
} else {
typeNamespaceString = asmhelpers.PurlPkgGuac
}
if version != "" && tag != "" {
purl = "pkg:guac/cdx/" + componentName + "@" + version + "?tag=" + tag
purl = typeNamespaceString + escapedName + "@" + version + "?tag=" + tag
} else if version != "" {
purl = "pkg:guac/cdx/" + componentName + "@" + version
purl = typeNamespaceString + escapedName + "@" + version
} else if tag != "" {
purl = "pkg:guac/cdx/" + componentName + "?tag=" + tag
purl = typeNamespaceString + escapedName + "?tag=" + tag
} else {
purl = "pkg:guac/cdx/" + componentName
purl = typeNamespaceString + escapedName
}
return purl
}
164 changes: 162 additions & 2 deletions pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ func Test_cyclonedxParser_addRootPackage(t *testing.T) {
},
},
},
wantPurl: "pkg:guac/files/sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870#/home/work/test/build/webserver",
wantPurl: "pkg:guac/cdx/sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870#/home/work/test/build/webserver",
}, {
name: "file type - purl nor provided, version not provided",
cdxBom: &cdx.BOM{
Expand All @@ -258,7 +258,7 @@ func Test_cyclonedxParser_addRootPackage(t *testing.T) {
},
},
},
wantPurl: "pkg:guac/files/home/work/test/build/webserver",
wantPurl: "pkg:guac/cdx/home/work/test/build/webserver",
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -287,3 +287,163 @@ func Test_cyclonedxParser_addRootPackage(t *testing.T) {
})
}
}

func Test_cyclonedxParser_getComponentPackages(t *testing.T) {
tests := []struct {
name string
cdxBom *cdx.BOM
wantPurl string
}{{
name: "purl provided",
cdxBom: &cdx.BOM{
Components: &[]cdx.Component{{
Name: "gcr.io/distroless/static:nonroot",
Type: cdx.ComponentTypeContainer,
Version: "sha256:6ad5b696af3ca05a048bd29bf0f623040462638cb0b29c8d702cbb2805687388",
PackageURL: "pkg:oci/static@sha256:6ad5b696af3ca05a048bd29bf0f623040462638cb0b29c8d702cbb2805687388?repository_url=gcr.io/distroless/static&tag=nonroot",
}},
},
wantPurl: "pkg:oci/static@sha256:6ad5b696af3ca05a048bd29bf0f623040462638cb0b29c8d702cbb2805687388?repository_url=gcr.io/distroless/static&tag=nonroot",
}, {
name: "gcr.io/distroless/static:nonroot - purl not provided",
cdxBom: &cdx.BOM{
Components: &[]cdx.Component{{
Name: "gcr.io/distroless/static:nonroot",
Type: cdx.ComponentTypeContainer,
Version: "sha256:6ad5b696af3ca05a048bd29bf0f623040462638cb0b29c8d702cbb2805687388",
}},
},
wantPurl: "pkg:guac/pkg/gcr.io/distroless/static@sha256:6ad5b696af3ca05a048bd29bf0f623040462638cb0b29c8d702cbb2805687388?tag=nonroot",
}, {
name: "gcr.io/distroless/static - purl not provided, tag not specified",

cdxBom: &cdx.BOM{
Components: &[]cdx.Component{{
Name: "gcr.io/distroless/static",
Type: cdx.ComponentTypeContainer,
Version: "sha256:6ad5b696af3ca05a048bd29bf0f623040462638cb0b29c8d702cbb2805687388",
}},
},
wantPurl: "pkg:guac/pkg/gcr.io/distroless/static@sha256:6ad5b696af3ca05a048bd29bf0f623040462638cb0b29c8d702cbb2805687388?tag=",
}, {
name: "gcr.io/distroless/static - purl not provided, tag not specified, version not specified",

cdxBom: &cdx.BOM{
Components: &[]cdx.Component{{
Name: "gcr.io/distroless/static",
Type: cdx.ComponentTypeContainer,
}},
},
wantPurl: "pkg:guac/pkg/gcr.io/distroless/static@?tag=",
}, {
name: "library/debian:latest - purl not provided, assume docker.io",

cdxBom: &cdx.BOM{
Components: &[]cdx.Component{{
Name: "library/debian:latest",
Type: cdx.ComponentTypeContainer,
Version: "sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
}},
},
wantPurl: "pkg:guac/pkg/library/debian@sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870?tag=latest",
}, {
name: "library/debian - purl not provided, tag not specified",
cdxBom: &cdx.BOM{
Components: &[]cdx.Component{{
Name: "library/debian",
Type: cdx.ComponentTypeContainer,
Version: "sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
}},
},
wantPurl: "pkg:guac/pkg/library/debian@sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870?tag=",
}, {
name: "library - purl not provided, tag not specified",
cdxBom: &cdx.BOM{
Components: &[]cdx.Component{{
Name: "library",
Type: cdx.ComponentTypeContainer,
Version: "sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
}},
},
wantPurl: "pkg:guac/pkg/library@sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870?tag=",
}, {
name: "name split length too long, tag not specified",
cdxBom: &cdx.BOM{
Components: &[]cdx.Component{{
Name: "ghcr.io/guacsec/guac/guacsec",
Type: cdx.ComponentTypeContainer,
Version: "sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
}},
},
wantPurl: "pkg:guac/pkg/ghcr.io/guacsec/guac/guacsec@sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
}, {
name: "name contains local registry, tag specified",
cdxBom: &cdx.BOM{
Components: &[]cdx.Component{{
Name: "foo.registry.com:4443/myapp/debian:latest",
Type: cdx.ComponentTypeContainer,
Version: "sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
}},
},
wantPurl: "pkg:guac/pkg/foo.registry.com:4443/myapp/debian@sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870?tag=latest",
}, {
name: "ComponentTypeLibrary",

cdxBom: &cdx.BOM{
Components: &[]cdx.Component{{
Name: "ghcr.io/guacsec/guac/guacsec",
Type: cdx.ComponentTypeLibrary,
Version: "sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
}},
},
wantPurl: "pkg:guac/pkg/ghcr.io/guacsec/guac/guacsec@sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
}, {
name: "file type - purl nor provided, version provided",
cdxBom: &cdx.BOM{
Components: &[]cdx.Component{{
Name: "/home/work/test/build/webserver",
Type: cdx.ComponentTypeFile,
Version: "sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870",
}},
},
wantPurl: "pkg:guac/files/sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870#/home/work/test/build/webserver",
}, {
name: "file type - purl nor provided, version not provided",
cdxBom: &cdx.BOM{
Components: &[]cdx.Component{{
Name: "/home/work/test/build/webserver",
Type: cdx.ComponentTypeFile,
}},
},
wantPurl: "pkg:guac/files/home/work/test/build/webserver",
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &cyclonedxParser{
doc: &processor.Document{
SourceInformation: processor.SourceInformation{
Collector: "test",
Source: "test",
},
},
packagePackages: map[string][]*model.PkgInputSpec{},
identifierStrings: &common.IdentifierStrings{},
}
c.cdxBom = tt.cdxBom
if err := c.getPackages(tt.cdxBom); err != nil {
t.Errorf("Failed to getTopLevelPackage %s", err)
}
wantPackage, err := asmhelpers.PurlToPkg(tt.wantPurl)
if err != nil {
t.Errorf("Failed to parse purl %v %v", tt.wantPurl, err)
}
for _, comp := range *tt.cdxBom.Components {
if d := cmp.Diff(*wantPackage, *c.packagePackages[comp.BOMRef][0]); len(d) != 0 {
t.Errorf("addRootPackage failed to produce expected package for %v", tt.name)
t.Errorf("spdx.GetPredicate mismatch values (+got, -expected): %s", d)
}
}

})
}
}
Loading

0 comments on commit cb2b6fe

Please sign in to comment.