Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve purl input #2223

Merged
merged 2 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}} 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
208 changes: 145 additions & 63 deletions grype/pkg/purl_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,98 +8,188 @@ 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, distroName, distroVersion, err := purlToPackage(rawLine)
if err != nil {
return nil, fmt.Errorf("unable to decode purl %s: %w", rawLine, err)
return nil, Context{}, err
}

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 distroName != "" {
if _, ok := distros[distroName]; !ok {
distros[distroName] = strset.New()
}
distros[distroName].Add(distroVersion)
}
if p != nil {
packages = append(packages, *p)
}
}

if qualifier.Key == "epoch" {
epoch = qualifier.Value
if err := scanner.Err(); err != nil {
return nil, ctx, err
}

// 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) (*Package, string, string, error) {
purl, err := packageurl.FromString(rawLine)
if err != nil {
return nil, "", "", fmt.Errorf("unable to decode purl %s: %w", rawLine, err)
}

if purl.Type == packageurl.TypeRPM && !strings.HasPrefix(purl.Version, fmt.Sprintf("%s:", epoch)) {
purl.Version = fmt.Sprintf("%s:%s", epoch, purl.Version)
var cpes []cpe.CPE
var upstreams []UpstreamPackage
var distroName, distroVersion string
epoch := "0"

pkgType := pkg.TypeByName(purl.Type)

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 != "" && version != "" {
distroName = name
distroVersion = 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(),
})
version := purl.Version
if purl.Type == packageurl.TypeRPM && !strings.HasPrefix(purl.Version, fmt.Sprintf("%s:", epoch)) {
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: version,
Type: pkgType,
Language: pkg.LanguageByName(purl.Type),
PURL: purl.String(),
Upstreams: upstreams,
}, distroName, distroVersion, 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 {
if pkgType == pkg.RpmPkg {
return handleSourceRPM(pkgName, value)
}
return handleDefaultUpstream(pkgName, value)
}

path := strings.TrimPrefix(userInput, purlInputPrefix)
func handleDefaultUpstream(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 +203,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
Loading