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 }