Skip to content

Commit

Permalink
improve purl input
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
  • Loading branch information
wagoodman committed Oct 30, 2024
1 parent b3f3dd4 commit 4b48e7f
Show file tree
Hide file tree
Showing 14 changed files with 358 additions and 149 deletions.
3 changes: 2 additions & 1 deletion cmd/grype/cli/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ You can also explicitly specify the scheme to use:
{{.appName}} file:path/to/yourfile read directly from a file on disk
{{.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 purls from a path on disk
{{.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)
You can also pipe in Syft JSON directly:
syft yourimage:tag -o json | {{.appName}}
Expand Down
4 changes: 2 additions & 2 deletions grype/pkg/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ func Provide(userInput string, config ProviderConfig) ([]Package, Context, *sbom
return packages, ctx, s, err
}

packages, err = purlProvider(userInput)
packages, ctx, err = purlProvider(userInput)
if !errors.Is(err, errDoesNotProvide) {
return packages, Context{}, s, err
return packages, ctx, s, err
}

return syftProvider(userInput, config)
Expand Down
205 changes: 142 additions & 63 deletions grype/pkg/purl_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,98 +8,185 @@ import (
"strings"

"github.com/mitchellh/go-homedir"
"github.com/scylladb/go-set/strset"

"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
)

const (
purlInputPrefix = "purl:"
cpesQualifierKey = "cpes"
purlInputPrefix = "purl:"
singlePurlInputPrefix = "pkg:"
cpesQualifierKey = "cpes"
)

type errEmptyPurlFile struct {
purlFilepath string
}

func (e errEmptyPurlFile) Error() string {
return fmt.Sprintf("purl file is empty: %s", e.purlFilepath)
}

func purlProvider(userInput string) ([]Package, error) {
p, err := getPurlPackages(userInput)
return p, err
}

func getPurlPackages(userInput string) ([]Package, error) {
func purlProvider(userInput string) ([]Package, Context, error) {
reader, err := getPurlReader(userInput)
if err != nil {
return nil, err
return nil, Context{}, err
}

return decodePurlFile(reader)
}

func decodePurlFile(reader io.Reader) ([]Package, error) {
func decodePurlFile(reader io.Reader) ([]Package, Context, error) {
scanner := bufio.NewScanner(reader)
packages := []Package{}
var packages []Package
var ctx Context

distros := make(map[string]*strset.Set)
for scanner.Scan() {
rawLine := scanner.Text()
purl, err := packageurl.FromString(rawLine)
p, err := purlToPackage(rawLine, distros)
if err != nil {
return nil, fmt.Errorf("unable to decode purl %s: %w", rawLine, err)
return nil, Context{}, err
}
if p != nil {
packages = append(packages, *p)
}
}

cpes := []cpe.CPE{}
epoch := "0"
for _, qualifier := range purl.Qualifiers {
if qualifier.Key == cpesQualifierKey {
rawCpes := strings.Split(qualifier.Value, ",")
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)
}
cpes = append(cpes, c)
}
}
if err := scanner.Err(); err != nil {
return nil, ctx, err
}

if qualifier.Key == "epoch" {
epoch = qualifier.Value
// if there is one distro (with one version) represented, use that
if len(distros) == 1 {
for name, versions := range distros {
if versions.Size() == 1 {
version := versions.List()[0]
var codename string
// if there are no digits in the version, it is likely a codename
if !strings.ContainsAny(version, "0123456789") {
codename = version
version = ""
}
ctx.Distro = &linux.Release{
Name: name,
ID: name,
IDLike: []string{name},
Version: version,
VersionCodename: codename,
}
}
}
}

return packages, ctx, nil
}

func purlToPackage(rawLine string, distros map[string]*strset.Set) (*Package, error) {
purl, err := packageurl.FromString(rawLine)
if err != nil {
return nil, fmt.Errorf("unable to decode purl %s: %w", rawLine, err)
}

cpes := []cpe.CPE{}
epoch := "0"
var upstreams []UpstreamPackage

pkgType := pkg.TypeByName(purl.Type)

if purl.Type == packageurl.TypeRPM && !strings.HasPrefix(purl.Version, fmt.Sprintf("%s:", epoch)) {
purl.Version = fmt.Sprintf("%s:%s", epoch, purl.Version)
for _, qualifier := range purl.Qualifiers {
switch qualifier.Key {
case cpesQualifierKey:
rawCpes := strings.Split(qualifier.Value, ",")
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)
}
cpes = append(cpes, c)
}
case pkg.PURLQualifierEpoch:
epoch = qualifier.Value
case pkg.PURLQualifierUpstream:
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)
}
}
}

packages = append(packages, Package{
ID: ID(purl.String()),
CPEs: cpes,
Name: purl.Name,
Version: purl.Version,
Type: pkg.TypeByName(purl.Type),
Language: pkg.LanguageByName(purl.Type),
PURL: purl.String(),
})
if purl.Type == packageurl.TypeRPM && !strings.HasPrefix(purl.Version, fmt.Sprintf("%s:", epoch)) {
purl.Version = fmt.Sprintf("%s:%s", epoch, purl.Version)
}

if err := scanner.Err(); err != nil {
return nil, err
return &Package{
ID: ID(purl.String()),
CPEs: cpes,
Name: purl.Name,
Version: purl.Version,
Type: pkgType,
Language: pkg.LanguageByName(purl.Type),
PURL: purl.String(),
Upstreams: upstreams,
}, nil
}

func parseDistroQualifier(value string) (string, string) {
fields := strings.SplitN(value, "-", 2)
switch len(fields) {
case 2:
return fields[0], fields[1]
case 1:
return fields[0], ""
}
return packages, nil
return "", ""
}

func getPurlReader(userInput string) (r io.Reader, err error) {
if !explicitlySpecifyingPurl(userInput) {
return nil, errDoesNotProvide
func parseUpstream(pkgName string, value string, pkgType pkg.Type) []UpstreamPackage {
switch pkgType {
case pkg.RpmPkg:
return handleSourceRPM(pkgName, value)
case pkg.DebPkg:
return handleDebianSource(pkgName, value)
}
return nil
}

path := strings.TrimPrefix(userInput, purlInputPrefix)
func handleDebianSource(pkgName string, value string) []UpstreamPackage {
fields := strings.Split(value, "@")
switch len(fields) {
case 2:
if fields[0] == pkgName {
return nil
}
return []UpstreamPackage{
{
Name: fields[0],
Version: fields[1],
},
}
case 1:
if fields[0] == pkgName {
return nil
}
return []UpstreamPackage{
{
Name: fields[0],
},
}
}
return nil
}

return openPurlFile(path)
func getPurlReader(userInput string) (r io.Reader, err error) {
switch {
case strings.HasPrefix(userInput, purlInputPrefix):
path := strings.TrimPrefix(userInput, purlInputPrefix)
return openPurlFile(path)
case strings.HasPrefix(userInput, singlePurlInputPrefix):
return strings.NewReader(userInput), nil
}
return nil, errDoesNotProvide
}

func openPurlFile(path string) (*os.File, error) {
Expand All @@ -113,13 +200,5 @@ func openPurlFile(path string) (*os.File, error) {
return nil, fmt.Errorf("unable to open file %s: %w", expandedPath, err)
}

if !fileHasContent(f) {
return nil, errEmptyPurlFile{path}
}

return f, nil
}

func explicitlySpecifyingPurl(userInput string) bool {
return strings.HasPrefix(userInput, purlInputPrefix)
}
Loading

0 comments on commit 4b48e7f

Please sign in to comment.