diff --git a/cmd/grype/cli/commands/root.go b/cmd/grype/cli/commands/root.go index 3548eba8815..c0c350dbc58 100644 --- a/cmd/grype/cli/commands/root.go +++ b/cmd/grype/cli/commands/root.go @@ -66,7 +66,7 @@ You can also explicitly specify the scheme to use: {{.appName}} sbom:path/to/syft.json read Syft JSON from path on disk {{.appName}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required) {{.appName}} purl:path/to/purl/file read a newline separated file of package URLs from a path on disk - {{.appName}} pkg:PURL read a single package PURL directly (e.g. pkg:apk/openssl@3.2.1?distro=alpine-3.20.3) + {{.appName}} PURL read a single package PURL directly (e.g. pkg:apk/openssl@3.2.1?distro=alpine-3.20.3) You can also pipe in Syft JSON directly: syft yourimage:tag -o json | {{.appName}} diff --git a/grype/pkg/purl_provider.go b/grype/pkg/purl_provider.go index a64a05d73ed..ace7ae1e0bd 100644 --- a/grype/pkg/purl_provider.go +++ b/grype/pkg/purl_provider.go @@ -39,10 +39,16 @@ func decodePurlFile(reader io.Reader) ([]Package, Context, error) { distros := make(map[string]*strset.Set) for scanner.Scan() { rawLine := scanner.Text() - p, err := purlToPackage(rawLine, distros) + p, distroName, distroVersion, err := purlToPackage(rawLine) if err != nil { return nil, Context{}, err } + if distroName != "" { + if _, ok := distros[distroName]; !ok { + distros[distroName] = strset.New() + } + distros[distroName].Add(distroVersion) + } if p != nil { packages = append(packages, *p) } @@ -77,15 +83,16 @@ func decodePurlFile(reader io.Reader) ([]Package, Context, error) { return packages, ctx, nil } -func purlToPackage(rawLine string, distros map[string]*strset.Set) (*Package, error) { +func purlToPackage(rawLine string) (*Package, string, string, error) { purl, err := packageurl.FromString(rawLine) if err != nil { - return nil, fmt.Errorf("unable to decode purl %s: %w", rawLine, err) + return nil, "", "", fmt.Errorf("unable to decode purl %s: %w", rawLine, err) } - cpes := []cpe.CPE{} - epoch := "0" + var cpes []cpe.CPE var upstreams []UpstreamPackage + var distroName, distroVersion string + epoch := "0" pkgType := pkg.TypeByName(purl.Type) @@ -96,7 +103,7 @@ func purlToPackage(rawLine string, distros map[string]*strset.Set) (*Package, er for _, rawCpe := range rawCpes { c, err := cpe.New(rawCpe, "") if err != nil { - return nil, fmt.Errorf("unable to decode cpe %s in purl %s: %w", rawCpe, rawLine, err) + return nil, "", "", fmt.Errorf("unable to decode cpe %s in purl %s: %w", rawCpe, rawLine, err) } cpes = append(cpes, c) } @@ -106,29 +113,28 @@ func purlToPackage(rawLine string, distros map[string]*strset.Set) (*Package, er upstreams = append(upstreams, parseUpstream(purl.Name, qualifier.Value, pkgType)...) case pkg.PURLQualifierDistro: name, version := parseDistroQualifier(qualifier.Value) - if name != "" { - if _, ok := distros[name]; !ok { - distros[name] = strset.New() - } - distros[name].Add(version) + if name != "" && version != "" { + distroName = name + distroVersion = version } } } + version := purl.Version if purl.Type == packageurl.TypeRPM && !strings.HasPrefix(purl.Version, fmt.Sprintf("%s:", epoch)) { - purl.Version = fmt.Sprintf("%s:%s", epoch, purl.Version) + version = fmt.Sprintf("%s:%s", epoch, purl.Version) } return &Package{ ID: ID(purl.String()), CPEs: cpes, Name: purl.Name, - Version: purl.Version, + Version: version, Type: pkgType, Language: pkg.LanguageByName(purl.Type), PURL: purl.String(), Upstreams: upstreams, - }, nil + }, distroName, distroVersion, nil } func parseDistroQualifier(value string) (string, string) { @@ -143,16 +149,13 @@ func parseDistroQualifier(value string) (string, string) { } func parseUpstream(pkgName string, value string, pkgType pkg.Type) []UpstreamPackage { - switch pkgType { - case pkg.RpmPkg: + if pkgType == pkg.RpmPkg { return handleSourceRPM(pkgName, value) - case pkg.DebPkg: - return handleDebianSource(pkgName, value) } - return nil + return handleDefaultUpstream(pkgName, value) } -func handleDebianSource(pkgName string, value string) []UpstreamPackage { +func handleDefaultUpstream(pkgName string, value string) []UpstreamPackage { fields := strings.Split(value, "@") switch len(fields) { case 2: diff --git a/grype/pkg/purl_provider_test.go b/grype/pkg/purl_provider_test.go index 5b5bb0c8f3b..6d68dc76a95 100644 --- a/grype/pkg/purl_provider_test.go +++ b/grype/pkg/purl_provider_test.go @@ -57,6 +57,95 @@ func Test_PurlProvider(t *testing.T) { }, }, }, + { + name: "default upstream", + userInput: "pkg:apk/libcrypto3@3.3.2?upstream=openssl", + context: Context{}, + pkgs: []Package{ + { + Name: "libcrypto3", + Version: "3.3.2", + Type: pkg.ApkPkg, + PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl", + Upstreams: []UpstreamPackage{ + { + Name: "openssl", + }, + }, + }, + }, + }, + { + name: "upstream with version", + userInput: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1", // %40 is @ + context: Context{}, + pkgs: []Package{ + { + Name: "libcrypto3", + Version: "3.3.2", + Type: pkg.ApkPkg, + PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1", + Upstreams: []UpstreamPackage{ + { + Name: "openssl", + Version: "3.2.1", + }, + }, + }, + }, + }, + { + name: "upstream for source RPM", + userInput: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm", + context: Context{ + Distro: &linux.Release{ + Name: "rhel", + ID: "rhel", + IDLike: []string{"rhel"}, + Version: "8.10", + }, + }, + pkgs: []Package{ + { + Name: "systemd-x", + Version: "0:239-82.el8_10.2", + Type: pkg.RpmPkg, + PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm", + Upstreams: []UpstreamPackage{ + { + Name: "systemd", + Version: "239-82.el8_10.2", + }, + }, + }, + }, + }, + { + name: "RPM with epoch", + userInput: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm", + context: Context{ + Distro: &linux.Release{ + Name: "rhel", + ID: "rhel", + IDLike: []string{"rhel"}, + Version: "8.10", + }, + }, + pkgs: []Package{ + { + Name: "dbus-common", + Version: "1:1.12.8-26.el8", + Type: pkg.RpmPkg, + PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm", + Upstreams: []UpstreamPackage{ + { + Name: "dbus", + Version: "1.12.8-26.el8", + }, + }, + }, + }, + }, { name: "takes multiple purls", userInput: "purl:test-fixtures/purl/valid-purl.txt",