mirror of
https://github.com/anchore/syft.git
synced 2026-11-01 15:06:36 +01:00
* feat: catalog CPAN distributions installed by perl clients
Adds `perl` as a language and `cpan` as a package type, with two catalogers behind them.
- `perl-cpan-installed-cataloger` reads `.meta/*/install.json` (cpanm, cpm, carton) and `auto/**/.packlist` (anything installed through `ExtUtils::MakeMaker` or `Module::Build`, including CPAN.pm). Both globs are unanchored, so a local-lib or carton application tree is found the same as a system install.
- `perl-cpan-meta-cataloger` reads an unpacked release's own `META.json` or `META.yml`, gated on a sibling `MANIFEST` so source checkouts are ignored.
Packages are keyed on the **distribution**, not the module, because that is what CPAN, MetaCPAN and the advisory data all use. `LWP.pm` belongs to `libwww-perl` and reporting it as `LWP` would make every advisory for it unreachable. The distribution name comes from `install.json`'s `dist` field, and from the `auto/` path for packlists.
purls are `pkg:cpan/<distribution>@<version>`, with the PAUSE author added as an `author` qualifier when the evidence carries it. Metadata comes in two types: `cpan-distribution` for installed evidence and `cpan-unpacked-release` for a release sitting on disk. The tiers differ in more than a name, so a consumer should not have to string-match a cataloger name to tell them apart: an unpacked release has no PAUSE path and therefore no author and no file list, and "installed and loadable by the interpreter" is a materially stronger claim than "a source tree exists here".
A packlist's version comes from `perllocal.pod`. EUMM writes it and the packlist from the same variables in the same install target, so the `auto/` path segments and the perllocal `Module` name are the same key, and the recorded `VERSION` is what was evaluated at build time rather than what can be read back statically. Scraping `$VERSION` out of the main `.pm` is the fallback, since `NO_PERLLOCAL` suppresses the file and Module::Build never writes one. Three rules pick the stanza: dashed module name, longest `installed into` libdir that prefixes the packlist path, and last match wins because the file is append-only. Without the libdir rule a second perl on the image silently supplies the version.
Where scraping is the fallback, it reads more than a plain `our $VERSION = '...'`. A version declared in the package statement (`package Foo::Bar v1.0.0;`) counts, which matters because a distribution can declare it that way and carry no `$VERSION` at all: `CPAN::02Packages::Search` is one, and without this it reports no version and matches nothing. Fully qualified `$Foo::Bar::VERSION` counts too, which is what older Dist::Zilla emitted and what real `JSON::PP` 2.27300 still carries. `qv('1.2.3')` is handled. A version computed at runtime is not, and those are reported without one rather than guessed at.
Packlist entries are parsed the way `ExtUtils::Packlist` writes them: a line can carry space-separated metadata after the path (`/path/to/File.pm type=file`), which is what `installperl` produces, so the suffix is stripped rather than the line being cut at its first space.
`META.yml` is read alongside `META.json`, because about 43% of current CPAN releases ship no `META.json` and they skew old, which is where the advisories are. Where both sit in one directory the `META.json` wins. A `META.yml` a strict parser rejects skips that directory rather than failing the scan, which is routine rather than defensive for pre-spec releases.
A packlist is literally a file list, so its paths are surfaced through `pkg.FileOwner`. They are reported as recorded, unfiltered: a path the packlist claims and the filesystem lacks means the file was removed or overwritten out from under the installer, which is worth seeing rather than hiding.
Build leftovers under `~/.cpanm/work` and `~/.cpan/build` are skipped. They genuinely are unpacked release tarballs, `MANIFEST` included, so the `MANIFEST` gate admits them and a path exclusion is the only signal available. Without it every distribution on an image that did not clean up is reported twice. The cost is that a tarball deliberately kept under `~/.cpan/build` stops being reported.
Two behaviors that look wrong but are not:
- a distribution can be reported twice at different versions. `libwww-perl` 5.836 bundles `HTTP-Date`, `HTTP-Message` and `LWP-MediaTypes`, and installing it over the modern standalone releases overwrites their `.pm` files. Both versions are genuinely present, so the merge refuses to pair disagreeing versions rather than hiding one.
- a distribution with no readable version is reported without one rather than dropped, so it stays visible.
The coverage boundary is stated in full in the `package perl` doc comment. In short: modules installed from distro packages are out of scope, since packagers strip CPAN metadata with `NO_PACKLIST` and deb, rpm and apk already report them. Core and dual-life distributions bundled with the interpreter have no coverage, because nothing on disk carries their versions; the interpreter itself is reported separately. Vendored trees, `App::FatPacker` output and PAR archives are invisible, because what they carry is module identity with no offline map to a distribution. A packlist-derived name is the installer's `NAME` and may not be the distribution name, which is left to the vulnerability data to resolve.
One correction worth calling out, since it is easy to arrive at twice: a sibling `MANIFEST` is the only thing separating an unpacked release from a source checkout. An earlier version of the guard also rejected any tree containing a `dist.ini`, on the theory that it marked a Dist::Zilla source tree. dzil ships `dist.ini` *inside* the tarballs it builds and lists it in the generated `MANIFEST`, so that exclusion was silently skipping real releases, `URI` among them.
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
* test(cli): bump coverage image package count for the new perl fixture
The CPAN fixture added to `image-pkg-coverage` (`URI-5.35`) is shared with the CLI suite through a symlink, so the squashed package count goes from 42 to 43.
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
* fix(perl): take CPAN distribution name and version from install.json dist
cpanm writes the main module's `$VERSION` into `version` and the release distvname into `dist`, and the two often disagree (`Carp-Assert-More-2.9.0` records `2.009000`). Trimming `version` off `dist` failed on those, so the whole distvname became the package name, and the module version never paired with the packlist record for the same install, which reported the distribution twice.
- name and version are now split out of `dist` at the last `-` followed by a version, as CPAN::DistnameInfo does
- a `-TRIAL` suffix is dropped, so `Try-Tiny-0.26-TRIAL` reports as `Try-Tiny` `0.26`
- the PAUSE path fallback uses the same split
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
---------
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
285 lines
6.7 KiB
Go
285 lines
6.7 KiB
Go
package pkg
|
|
|
|
import (
|
|
"github.com/anchore/packageurl-go"
|
|
)
|
|
|
|
// Type represents a Package Type for or within a language ecosystem (there may be multiple package types within a language ecosystem)
|
|
type Type string
|
|
|
|
func (t Type) String() string {
|
|
return string(t)
|
|
}
|
|
|
|
const (
|
|
// the full set of supported packages
|
|
UnknownPkg Type = "UnknownPackage"
|
|
AlpmPkg Type = "alpm"
|
|
ApkPkg Type = "apk"
|
|
BinaryPkg Type = "binary"
|
|
BitnamiPkg Type = "bitnami"
|
|
CocoapodsPkg Type = "pod"
|
|
ConanPkg Type = "conan"
|
|
CondaPkg Type = "conda"
|
|
CpanPkg Type = "cpan"
|
|
DartPubPkg Type = "dart-pub"
|
|
DebPkg Type = "deb"
|
|
DotnetPkg Type = "dotnet"
|
|
ErlangOTPPkg Type = "erlang-otp"
|
|
GemPkg Type = "gem"
|
|
GithubActionPkg Type = "github-action"
|
|
GithubActionWorkflowPkg Type = "github-action-workflow"
|
|
GoModulePkg Type = "go-module"
|
|
GraalVMNativeImagePkg Type = "graalvm-native-image"
|
|
HackagePkg Type = "hackage"
|
|
HexPkg Type = "hex"
|
|
JavaPkg Type = "java-archive"
|
|
JenkinsPluginPkg Type = "jenkins-plugin"
|
|
KbPkg Type = "msrc-kb"
|
|
LinuxKernelPkg Type = "linux-kernel"
|
|
LinuxKernelModulePkg Type = "linux-kernel-module"
|
|
NixPkg Type = "nix"
|
|
NpmPkg Type = "npm"
|
|
OpamPkg Type = "opam"
|
|
PhpComposerPkg Type = "php-composer"
|
|
PhpPeclPkg Type = "php-pecl" // Deprecated: will be removed in syft v2.0
|
|
PhpPearPkg Type = "php-pear"
|
|
PortagePkg Type = "portage"
|
|
PythonPkg Type = "python"
|
|
Rpkg Type = "R-package"
|
|
LuaRocksPkg Type = "lua-rocks"
|
|
RpmPkg Type = "rpm"
|
|
RustPkg Type = "rust-crate"
|
|
SwiftPkg Type = "swift"
|
|
SwiplPackPkg Type = "swiplpack"
|
|
TerraformPkg Type = "terraform"
|
|
VcpkgPkg Type = "vcpkg"
|
|
WordpressPluginPkg Type = "wordpress-plugin"
|
|
HomebrewPkg Type = "homebrew"
|
|
AppleAppBundlePkg Type = "apple-app-bundle"
|
|
ModelPkg Type = "model"
|
|
)
|
|
|
|
// AllPkgs represents all supported package types
|
|
var AllPkgs = []Type{
|
|
AlpmPkg,
|
|
ApkPkg,
|
|
BinaryPkg,
|
|
BitnamiPkg,
|
|
CocoapodsPkg,
|
|
ConanPkg,
|
|
CondaPkg,
|
|
CpanPkg,
|
|
DartPubPkg,
|
|
DebPkg,
|
|
DotnetPkg,
|
|
ErlangOTPPkg,
|
|
GemPkg,
|
|
GithubActionPkg,
|
|
GithubActionWorkflowPkg,
|
|
GraalVMNativeImagePkg,
|
|
GoModulePkg,
|
|
HackagePkg,
|
|
HexPkg,
|
|
JavaPkg,
|
|
JenkinsPluginPkg,
|
|
KbPkg,
|
|
LinuxKernelPkg,
|
|
LinuxKernelModulePkg,
|
|
NixPkg,
|
|
NpmPkg,
|
|
OpamPkg,
|
|
PhpComposerPkg,
|
|
PhpPeclPkg,
|
|
PhpPearPkg,
|
|
PortagePkg,
|
|
PythonPkg,
|
|
Rpkg,
|
|
LuaRocksPkg,
|
|
RpmPkg,
|
|
RustPkg,
|
|
SwiftPkg,
|
|
SwiplPackPkg,
|
|
TerraformPkg,
|
|
VcpkgPkg,
|
|
WordpressPluginPkg,
|
|
HomebrewPkg,
|
|
AppleAppBundlePkg,
|
|
ModelPkg,
|
|
}
|
|
|
|
// PackageURLType returns the PURL package type for the current package.
|
|
//
|
|
//nolint:funlen, gocyclo
|
|
func (t Type) PackageURLType() string {
|
|
switch t {
|
|
case AlpmPkg:
|
|
return "alpm"
|
|
case ApkPkg:
|
|
return packageurl.TypeAlpine
|
|
case BitnamiPkg:
|
|
return packageurl.TypeBitnami
|
|
case CocoapodsPkg:
|
|
return packageurl.TypeCocoapods
|
|
case ConanPkg:
|
|
return packageurl.TypeConan
|
|
case CondaPkg:
|
|
return packageurl.TypeGeneric
|
|
case CpanPkg:
|
|
return packageurl.TypeCpan
|
|
case DartPubPkg:
|
|
return packageurl.TypePub
|
|
case DebPkg:
|
|
return "deb"
|
|
case DotnetPkg:
|
|
return packageurl.TypeNuget
|
|
case ErlangOTPPkg:
|
|
return packageurl.TypeOTP
|
|
case GemPkg:
|
|
return packageurl.TypeGem
|
|
case HexPkg:
|
|
return packageurl.TypeHex
|
|
case GithubActionPkg, GithubActionWorkflowPkg:
|
|
// note: this is not a real purl type, but it is the closest thing we have for now
|
|
return packageurl.TypeGithub
|
|
case GoModulePkg:
|
|
return packageurl.TypeGolang
|
|
case HackagePkg:
|
|
return packageurl.TypeHackage
|
|
case JavaPkg, JenkinsPluginPkg:
|
|
return packageurl.TypeMaven
|
|
case LinuxKernelPkg:
|
|
return "generic/linux-kernel"
|
|
case LinuxKernelModulePkg:
|
|
return packageurl.TypeGeneric
|
|
case PhpComposerPkg:
|
|
return packageurl.TypeComposer
|
|
case PhpPearPkg, PhpPeclPkg:
|
|
return "pear"
|
|
case PythonPkg:
|
|
return packageurl.TypePyPi
|
|
case PortagePkg:
|
|
return "portage"
|
|
case LuaRocksPkg:
|
|
return packageurl.TypeLuaRocks
|
|
case NixPkg:
|
|
return "nix"
|
|
case NpmPkg:
|
|
return packageurl.TypeNPM
|
|
case OpamPkg:
|
|
return "opam"
|
|
case Rpkg:
|
|
return packageurl.TypeCran
|
|
case RpmPkg:
|
|
return packageurl.TypeRPM
|
|
case RustPkg:
|
|
return "cargo"
|
|
case SwiftPkg:
|
|
return packageurl.TypeSwift
|
|
case SwiplPackPkg:
|
|
return "swiplpack"
|
|
case TerraformPkg:
|
|
return "terraform"
|
|
case VcpkgPkg:
|
|
return "vcpkg"
|
|
case WordpressPluginPkg:
|
|
return "wordpress-plugin"
|
|
case HomebrewPkg:
|
|
return "homebrew"
|
|
default:
|
|
// TODO: should this be a "generic" purl type instead?
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func TypeFromPURL(p string) Type {
|
|
purl, err := packageurl.FromString(p)
|
|
if err != nil {
|
|
return UnknownPkg
|
|
}
|
|
|
|
ptype := purl.Type
|
|
if ptype == "generic" {
|
|
ptype = purl.Name
|
|
}
|
|
return TypeByName(ptype)
|
|
}
|
|
|
|
//nolint:funlen,gocyclo
|
|
func TypeByName(name string) Type {
|
|
switch name {
|
|
case "alpm":
|
|
return AlpmPkg
|
|
case packageurl.TypeAlpine, "alpine":
|
|
return ApkPkg
|
|
case packageurl.TypeBitnami:
|
|
return BitnamiPkg
|
|
case packageurl.TypeDebian:
|
|
return DebPkg
|
|
case packageurl.TypeComposer:
|
|
return PhpComposerPkg
|
|
case "pear", "pecl":
|
|
return PhpPearPkg
|
|
case packageurl.TypeGolang:
|
|
return GoModulePkg
|
|
case packageurl.TypeGem:
|
|
return GemPkg
|
|
case "cargo", "crate":
|
|
return RustPkg
|
|
case "conda":
|
|
return CondaPkg
|
|
case packageurl.TypeCpan:
|
|
return CpanPkg
|
|
case packageurl.TypePub:
|
|
return DartPubPkg
|
|
case "dotnet", packageurl.TypeNuget: // "dotnet" is here to support legacy use cases; "nuget" is the canonical purl type
|
|
return DotnetPkg
|
|
case packageurl.TypeCocoapods:
|
|
return CocoapodsPkg
|
|
case packageurl.TypeConan:
|
|
return ConanPkg
|
|
case packageurl.TypeHackage:
|
|
return HackagePkg
|
|
case packageurl.TypeHex:
|
|
return HexPkg
|
|
case packageurl.TypeLuaRocks:
|
|
return LuaRocksPkg
|
|
case packageurl.TypeMaven:
|
|
return JavaPkg
|
|
case packageurl.TypeNPM:
|
|
return NpmPkg
|
|
case packageurl.TypePyPi:
|
|
return PythonPkg
|
|
case "portage":
|
|
return PortagePkg
|
|
case packageurl.TypeOTP:
|
|
return ErlangOTPPkg
|
|
case "linux-kernel":
|
|
return LinuxKernelPkg
|
|
case "linux-kernel-module":
|
|
return LinuxKernelModulePkg
|
|
case "nix":
|
|
return NixPkg
|
|
case "opam":
|
|
return OpamPkg
|
|
case packageurl.TypeCran:
|
|
return Rpkg
|
|
case packageurl.TypeRPM:
|
|
return RpmPkg
|
|
case packageurl.TypeSwift:
|
|
return SwiftPkg
|
|
case "swiplpack":
|
|
return SwiplPackPkg
|
|
case "terraform":
|
|
return TerraformPkg
|
|
case "vcpkg":
|
|
return VcpkgPkg
|
|
case "wordpress-plugin":
|
|
return WordpressPluginPkg
|
|
case "homebrew":
|
|
return HomebrewPkg
|
|
default:
|
|
return UnknownPkg
|
|
}
|
|
}
|