github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/purl/purl.go (about)

     1  package purl
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"strings"
     7  
     8  	cn "github.com/google/go-containerregistry/pkg/name"
     9  	version "github.com/knqyf263/go-rpm-version"
    10  	packageurl "github.com/package-url/packageurl-go"
    11  	"golang.org/x/xerrors"
    12  
    13  	ftypes "github.com/devseccon/trivy/pkg/fanal/types"
    14  	"github.com/devseccon/trivy/pkg/scanner/utils"
    15  	"github.com/devseccon/trivy/pkg/types"
    16  )
    17  
    18  const (
    19  	TypeOCI  = "oci"
    20  	TypeDart = "dart"
    21  
    22  	// TypeK8s is a custom type for Kubernetes components in PURL.
    23  	//  - namespace: The service provider such as EKS or GKE. It is not case sensitive and must be lowercased.
    24  	//     Known namespaces:
    25  	//       - empty (upstream)
    26  	//       - eks (AWS)
    27  	//       - aks (GCP)
    28  	//       - gke (Azure)
    29  	//       - rke (Rancher)
    30  	//  - name: The k8s component name and is case sensitive.
    31  	//  - version: The combined version and release of a component.
    32  	//
    33  	//  Examples:
    34  	//    - pkg:k8s/upstream/k8s.io%2Fapiserver@1.24.1
    35  	//    - pkg:k8s/eks/k8s.io%2Fkube-proxy@1.26.2-eksbuild.1
    36  	TypeK8s = "k8s"
    37  
    38  	NamespaceEKS = "eks"
    39  	NamespaceAKS = "aks"
    40  	NamespaceGKE = "gke"
    41  	NamespaceRKE = "rke"
    42  	NamespaceOCP = "ocp"
    43  
    44  	TypeUnknown = "unknown"
    45  )
    46  
    47  type PackageURL struct {
    48  	packageurl.PackageURL
    49  	FilePath string
    50  }
    51  
    52  func FromString(purl string) (*PackageURL, error) {
    53  	p, err := packageurl.FromString(purl)
    54  	if err != nil {
    55  		return nil, xerrors.Errorf("failed to parse purl(%s): %w", purl, err)
    56  	}
    57  
    58  	return &PackageURL{
    59  		PackageURL: p,
    60  	}, nil
    61  }
    62  
    63  func (p *PackageURL) Package() *ftypes.Package {
    64  	pkg := &ftypes.Package{
    65  		Name:    p.Name,
    66  		Version: p.Version,
    67  	}
    68  	for _, q := range p.Qualifiers {
    69  		switch q.Key {
    70  		case "arch":
    71  			pkg.Arch = q.Value
    72  		case "modularitylabel":
    73  			pkg.Modularitylabel = q.Value
    74  		case "epoch":
    75  			epoch, err := strconv.Atoi(q.Value)
    76  			if err == nil {
    77  				pkg.Epoch = epoch
    78  			}
    79  		}
    80  	}
    81  
    82  	// CocoaPods purl has no namespace, but has subpath
    83  	// https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#cocoapods
    84  	if p.Type == packageurl.TypeCocoapods && p.Subpath != "" {
    85  		// CocoaPods uses <moduleName>/<submoduleName> format for package name
    86  		// e.g. `pkg:cocoapods/GoogleUtilities@7.5.2#NSData+zlib` => `GoogleUtilities/NSData+zlib`
    87  		pkg.Name = p.Name + "/" + p.Subpath
    88  	}
    89  
    90  	if p.Type == packageurl.TypeRPM {
    91  		rpmVer := version.NewVersion(p.Version)
    92  		pkg.Release = rpmVer.Release()
    93  		pkg.Version = rpmVer.Version()
    94  	}
    95  
    96  	// Return packages without namespace.
    97  	// OS packages are not supposed to have namespace.
    98  	if p.Namespace == "" || p.Class() == types.ClassOSPkg {
    99  		return pkg
   100  	}
   101  
   102  	// TODO: replace with packageurl.TypeGradle once they add it.
   103  	if p.Type == packageurl.TypeMaven || p.Type == string(ftypes.Gradle) {
   104  		// Maven and Gradle packages separate ":"
   105  		// e.g. org.springframework:spring-core
   106  		pkg.Name = p.Namespace + ":" + p.Name
   107  	} else {
   108  		pkg.Name = p.Namespace + "/" + p.Name
   109  	}
   110  
   111  	return pkg
   112  }
   113  
   114  // LangType returns an application type in Trivy
   115  // nolint: gocyclo
   116  func (p *PackageURL) LangType() ftypes.LangType {
   117  	switch p.Type {
   118  	case packageurl.TypeComposer:
   119  		return ftypes.Composer
   120  	case packageurl.TypeMaven:
   121  		return ftypes.Jar
   122  	case packageurl.TypeGem:
   123  		return ftypes.GemSpec
   124  	case packageurl.TypeConda:
   125  		return ftypes.CondaPkg
   126  	case packageurl.TypePyPi:
   127  		return ftypes.PythonPkg
   128  	case packageurl.TypeGolang:
   129  		return ftypes.GoBinary
   130  	case packageurl.TypeNPM:
   131  		return ftypes.NodePkg
   132  	case packageurl.TypeCargo:
   133  		return ftypes.Cargo
   134  	case packageurl.TypeNuget:
   135  		return ftypes.NuGet
   136  	case packageurl.TypeSwift:
   137  		return ftypes.Swift
   138  	case packageurl.TypeCocoapods:
   139  		return ftypes.Cocoapods
   140  	case packageurl.TypeHex:
   141  		return ftypes.Hex
   142  	case packageurl.TypeConan:
   143  		return ftypes.Conan
   144  	case TypeDart: // TODO: replace with packageurl.TypeDart once they add it.
   145  		return ftypes.Pub
   146  	case packageurl.TypeBitnami:
   147  		return ftypes.Bitnami
   148  	case TypeK8s:
   149  		switch p.Namespace {
   150  		case NamespaceEKS:
   151  			return ftypes.EKS
   152  		case NamespaceGKE:
   153  			return ftypes.GKE
   154  		case NamespaceAKS:
   155  			return ftypes.AKS
   156  		case NamespaceRKE:
   157  			return ftypes.RKE
   158  		case NamespaceOCP:
   159  			return ftypes.OCP
   160  		case "":
   161  			return ftypes.K8sUpstream
   162  		}
   163  		return TypeUnknown
   164  	default:
   165  		return TypeUnknown
   166  	}
   167  }
   168  
   169  func (p *PackageURL) Class() types.ResultClass {
   170  	switch p.Type {
   171  	case packageurl.TypeApk, packageurl.TypeDebian, packageurl.TypeRPM:
   172  		// OS packages
   173  		return types.ClassOSPkg
   174  	default:
   175  		if p.LangType() == TypeUnknown {
   176  			return types.ClassUnknown
   177  		}
   178  		// Language-specific packages
   179  		return types.ClassLangPkg
   180  	}
   181  }
   182  
   183  func (p *PackageURL) BOMRef() string {
   184  	// 'bom-ref' must be unique within BOM, but PURLs may conflict
   185  	// when the same packages are installed in an artifact.
   186  	// In that case, we prefer to make PURLs unique by adding file paths,
   187  	// rather than using UUIDs, even if it is not PURL technically.
   188  	// ref. https://cyclonedx.org/use-cases/#dependency-graph
   189  	purl := p.PackageURL // so that it will not override the qualifiers below
   190  	if p.FilePath != "" {
   191  		purl.Qualifiers = append(purl.Qualifiers,
   192  			packageurl.Qualifier{
   193  				Key:   "file_path",
   194  				Value: p.FilePath,
   195  			},
   196  		)
   197  	}
   198  	return purl.String()
   199  }
   200  
   201  // nolint: gocyclo
   202  func NewPackageURL(t ftypes.TargetType, metadata types.Metadata, pkg ftypes.Package) (*PackageURL, error) {
   203  	var qualifiers packageurl.Qualifiers
   204  	if metadata.OS != nil {
   205  		qualifiers = parseQualifier(pkg)
   206  		pkg.Epoch = 0 // we moved Epoch to qualifiers so we don't need it in version
   207  	}
   208  
   209  	ptype := purlType(t)
   210  	name := pkg.Name
   211  	ver := utils.FormatVersion(pkg)
   212  	namespace := ""
   213  	subpath := ""
   214  
   215  	switch ptype {
   216  	case packageurl.TypeRPM:
   217  		ns, qs := parseRPM(metadata.OS, pkg.Modularitylabel)
   218  		namespace = string(ns)
   219  		qualifiers = append(qualifiers, qs...)
   220  	case packageurl.TypeDebian:
   221  		qualifiers = append(qualifiers, parseDeb(metadata.OS)...)
   222  		if metadata.OS != nil {
   223  			namespace = string(metadata.OS.Family)
   224  		}
   225  	case packageurl.TypeApk:
   226  		var qs packageurl.Qualifiers
   227  		name, namespace, qs = parseApk(name, metadata.OS)
   228  		qualifiers = append(qualifiers, qs...)
   229  	case packageurl.TypeMaven, string(ftypes.Gradle): // TODO: replace with packageurl.TypeGradle once they add it.
   230  		namespace, name = parseMaven(name)
   231  	case packageurl.TypePyPi:
   232  		name = parsePyPI(name)
   233  	case packageurl.TypeComposer:
   234  		namespace, name = parseComposer(name)
   235  	case packageurl.TypeGolang:
   236  		namespace, name = parseGolang(name)
   237  		if name == "" {
   238  			return nil, nil
   239  		}
   240  	case packageurl.TypeNPM:
   241  		namespace, name = parseNpm(name)
   242  	case packageurl.TypeSwift:
   243  		namespace, name = parseSwift(name)
   244  	case packageurl.TypeCocoapods:
   245  		name, subpath = parseCocoapods(name)
   246  	case packageurl.TypeOCI:
   247  		purl, err := parseOCI(metadata)
   248  		if err != nil {
   249  			return nil, err
   250  		}
   251  		if purl.Type == "" {
   252  			return nil, nil
   253  		}
   254  		return &PackageURL{PackageURL: purl}, nil
   255  	}
   256  
   257  	return &PackageURL{
   258  		PackageURL: *packageurl.NewPackageURL(ptype, namespace, name, ver, qualifiers, subpath),
   259  		FilePath:   pkg.FilePath,
   260  	}, nil
   261  }
   262  
   263  // ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#oci
   264  func parseOCI(metadata types.Metadata) (packageurl.PackageURL, error) {
   265  	if len(metadata.RepoDigests) == 0 {
   266  		return *packageurl.NewPackageURL("", "", "", "", nil, ""), nil
   267  	}
   268  
   269  	digest, err := cn.NewDigest(metadata.RepoDigests[0])
   270  	if err != nil {
   271  		return packageurl.PackageURL{}, xerrors.Errorf("failed to parse digest: %w", err)
   272  	}
   273  
   274  	name := strings.ToLower(digest.RepositoryStr())
   275  	index := strings.LastIndex(name, "/")
   276  	if index != -1 {
   277  		name = name[index+1:]
   278  	}
   279  
   280  	var qualifiers packageurl.Qualifiers
   281  	if repoURL := digest.Repository.Name(); repoURL != "" {
   282  		qualifiers = append(qualifiers, packageurl.Qualifier{
   283  			Key:   "repository_url",
   284  			Value: repoURL,
   285  		})
   286  	}
   287  	if arch := metadata.ImageConfig.Architecture; arch != "" {
   288  		qualifiers = append(qualifiers, packageurl.Qualifier{
   289  			Key:   "arch",
   290  			Value: metadata.ImageConfig.Architecture,
   291  		})
   292  	}
   293  
   294  	return *packageurl.NewPackageURL(packageurl.TypeOCI, "", name, digest.DigestStr(), qualifiers, ""), nil
   295  }
   296  
   297  // ref. https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#apk
   298  func parseApk(pkgName string, fos *ftypes.OS) (string, string, packageurl.Qualifiers) {
   299  	// the name must be lowercase
   300  	pkgName = strings.ToLower(pkgName)
   301  
   302  	if fos == nil {
   303  		return pkgName, "", nil
   304  	}
   305  
   306  	// the namespace must be lowercase
   307  	ns := strings.ToLower(string(fos.Family))
   308  	qs := packageurl.Qualifiers{
   309  		{
   310  			Key:   "distro",
   311  			Value: fos.Name,
   312  		},
   313  	}
   314  
   315  	return pkgName, ns, qs
   316  }
   317  
   318  // ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#deb
   319  func parseDeb(fos *ftypes.OS) packageurl.Qualifiers {
   320  
   321  	if fos == nil {
   322  		return packageurl.Qualifiers{}
   323  	}
   324  
   325  	distro := fmt.Sprintf("%s-%s", fos.Family, fos.Name)
   326  	return packageurl.Qualifiers{
   327  		{
   328  			Key:   "distro",
   329  			Value: distro,
   330  		},
   331  	}
   332  }
   333  
   334  // ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#rpm
   335  func parseRPM(fos *ftypes.OS, modularityLabel string) (ftypes.OSType, packageurl.Qualifiers) {
   336  	if fos == nil {
   337  		return "", packageurl.Qualifiers{}
   338  	}
   339  
   340  	// SLES string has whitespace
   341  	family := fos.Family
   342  	if fos.Family == ftypes.SLES {
   343  		family = "sles"
   344  	}
   345  
   346  	qualifiers := packageurl.Qualifiers{
   347  		{
   348  			Key:   "distro",
   349  			Value: fmt.Sprintf("%s-%s", family, fos.Name),
   350  		},
   351  	}
   352  
   353  	if modularityLabel != "" {
   354  		qualifiers = append(qualifiers, packageurl.Qualifier{
   355  			Key:   "modularitylabel",
   356  			Value: modularityLabel,
   357  		})
   358  	}
   359  	return family, qualifiers
   360  }
   361  
   362  // ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#maven
   363  func parseMaven(pkgName string) (string, string) {
   364  	// The group id is the "namespace" and the artifact id is the "name".
   365  	name := strings.ReplaceAll(pkgName, ":", "/")
   366  	return parsePkgName(name)
   367  }
   368  
   369  // ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#golang
   370  func parseGolang(pkgName string) (string, string) {
   371  	// The PURL will be skipped when the package name is a local path, since it can't identify a software package.
   372  	if strings.HasPrefix(pkgName, "./") || strings.HasPrefix(pkgName, "../") {
   373  		return "", ""
   374  	}
   375  	name := strings.ToLower(pkgName)
   376  	return parsePkgName(name)
   377  }
   378  
   379  // ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#pypi
   380  func parsePyPI(pkgName string) string {
   381  	// PyPi treats - and _ as the same character and is not case-sensitive.
   382  	// Therefore a Pypi package name must be lowercased and underscore "_" replaced with a dash "-".
   383  	return strings.ToLower(strings.ReplaceAll(pkgName, "_", "-"))
   384  }
   385  
   386  // ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#composer
   387  func parseComposer(pkgName string) (string, string) {
   388  	return parsePkgName(pkgName)
   389  }
   390  
   391  // ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#swift
   392  func parseSwift(pkgName string) (string, string) {
   393  	return parsePkgName(pkgName)
   394  }
   395  
   396  // ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#cocoapods
   397  func parseCocoapods(pkgName string) (string, string) {
   398  	var subpath string
   399  	pkgName, subpath, _ = strings.Cut(pkgName, "/")
   400  	return pkgName, subpath
   401  }
   402  
   403  // ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#npm
   404  func parseNpm(pkgName string) (string, string) {
   405  	// the name must be lowercased
   406  	name := strings.ToLower(pkgName)
   407  	return parsePkgName(name)
   408  }
   409  
   410  func purlType(t ftypes.TargetType) string {
   411  	switch t {
   412  	case ftypes.Jar, ftypes.Pom, ftypes.Gradle:
   413  		return packageurl.TypeMaven
   414  	case ftypes.Bundler, ftypes.GemSpec:
   415  		return packageurl.TypeGem
   416  	case ftypes.NuGet, ftypes.DotNetCore:
   417  		return packageurl.TypeNuget
   418  	case ftypes.CondaPkg:
   419  		return packageurl.TypeConda
   420  	case ftypes.PythonPkg, ftypes.Pip, ftypes.Pipenv, ftypes.Poetry:
   421  		return packageurl.TypePyPi
   422  	case ftypes.GoBinary, ftypes.GoModule:
   423  		return packageurl.TypeGolang
   424  	case ftypes.Npm, ftypes.NodePkg, ftypes.Yarn, ftypes.Pnpm:
   425  		return packageurl.TypeNPM
   426  	case ftypes.Cocoapods:
   427  		return packageurl.TypeCocoapods
   428  	case ftypes.Swift:
   429  		return packageurl.TypeSwift
   430  	case ftypes.Hex:
   431  		return packageurl.TypeHex
   432  	case ftypes.Conan:
   433  		return packageurl.TypeConan
   434  	case ftypes.Pub:
   435  		return TypeDart // TODO: replace with packageurl.TypeDart once they add it.
   436  	case ftypes.RustBinary, ftypes.Cargo:
   437  		return packageurl.TypeCargo
   438  	case ftypes.Alpine:
   439  		return packageurl.TypeApk
   440  	case ftypes.Debian, ftypes.Ubuntu:
   441  		return packageurl.TypeDebian
   442  	case ftypes.RedHat, ftypes.CentOS, ftypes.Rocky, ftypes.Alma,
   443  		ftypes.Amazon, ftypes.Fedora, ftypes.Oracle, ftypes.OpenSUSE,
   444  		ftypes.OpenSUSELeap, ftypes.OpenSUSETumbleweed, ftypes.SLES, ftypes.Photon:
   445  		return packageurl.TypeRPM
   446  	case TypeOCI:
   447  		return packageurl.TypeOCI
   448  	}
   449  	return string(t)
   450  }
   451  
   452  func parseQualifier(pkg ftypes.Package) packageurl.Qualifiers {
   453  	qualifiers := packageurl.Qualifiers{}
   454  	if pkg.Arch != "" {
   455  		qualifiers = append(qualifiers, packageurl.Qualifier{
   456  			Key:   "arch",
   457  			Value: pkg.Arch,
   458  		})
   459  	}
   460  	if pkg.Epoch != 0 {
   461  		qualifiers = append(qualifiers, packageurl.Qualifier{
   462  			Key:   "epoch",
   463  			Value: strconv.Itoa(pkg.Epoch),
   464  		})
   465  	}
   466  	return qualifiers
   467  }
   468  
   469  func parsePkgName(name string) (string, string) {
   470  	var namespace string
   471  	index := strings.LastIndex(name, "/")
   472  	if index != -1 {
   473  		namespace = name[:index]
   474  		name = name[index+1:]
   475  	}
   476  	return namespace, name
   477  
   478  }