From cb2b6fe5f6449d5225f1e3f55ad5f2d4741dc5d5 Mon Sep 17 00:00:00 2001 From: Parth Patel <88045217+pxp928@users.noreply.github.com> Date: Fri, 19 May 2023 15:41:34 -0400 Subject: [PATCH] fix version handling for top level and dependency components (#855) * fix version handling for top level and dependency components Signed-off-by: pxp928 * add sanitization to name string in generated purl Signed-off-by: pxp928 --------- Signed-off-by: pxp928 --- pkg/assembler/helpers/purl.go | 25 ++- .../parser/cyclonedx/parser_cyclonedx.go | 107 +++++++----- .../parser/cyclonedx/parser_cyclonedx_test.go | 164 +++++++++++++++++- pkg/ingestor/parser/spdx/parse_spdx.go | 2 +- 4 files changed, 252 insertions(+), 46 deletions(-) diff --git a/pkg/assembler/helpers/purl.go b/pkg/assembler/helpers/purl.go index c9a1576e79..43a0aaed0b 100644 --- a/pkg/assembler/helpers/purl.go +++ b/pkg/assembler/helpers/purl.go @@ -17,6 +17,7 @@ package helpers import ( "fmt" + "net/url" "path/filepath" "strings" @@ -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 @@ -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)) } diff --git a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go index adbf40bd46..aaa58f75c8 100644 --- a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go +++ b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go @@ -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 @@ -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) } } @@ -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 { @@ -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) } @@ -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 } diff --git a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go index 48faf123e8..fd7f67fadd 100644 --- a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go +++ b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go @@ -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{ @@ -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) { @@ -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) + } + } + + }) + } +} diff --git a/pkg/ingestor/parser/spdx/parse_spdx.go b/pkg/ingestor/parser/spdx/parse_spdx.go index 571b13815f..a1156a62a8 100644 --- a/pkg/ingestor/parser/spdx/parse_spdx.go +++ b/pkg/ingestor/parser/spdx/parse_spdx.go @@ -79,7 +79,7 @@ func (s *spdxParser) getTopLevelPackage() error { // Currently create TopLevel package as well in some cases where we guess that the SPDX document // may not encode it - var purl string = "pkg:guac/spdx/" + s.spdxDoc.DocumentName + var purl string = "pkg:guac/spdx/" + asmhelpers.SanitizeString(s.spdxDoc.DocumentName) if purl != "" { topPackage, err := asmhelpers.PurlToPkg(purl)