Compare commits

..
Author SHA1 Message Date
anchore-oss-update-botandanchore-oss-update-bot ae649b09cf chore(deps): update anchore dependencies (#5304)
Signed-off-by: anchore-oss-update-bot <anchore-oss-update-bot@users.noreply.github.com>
Co-authored-by: anchore-oss-update-bot <anchore-oss-update-bot@users.noreply.github.com>
2026-10-01 11:11:11 -04:00
Alex Goodman f76ae8e57c feat: report the perl interpreter as a cpan package (#5132)
The `perl-binary` classifier now emits `pkg:cpan/perl@<version>` instead of `pkg:generic/perl@<version>`.

`perl` is itself a CPAN distribution and carries advisories under that name, so typing it as `generic` left every one of them unreachable.

This changes an existing purl. Anything keyed on `pkg:generic/perl` (allowlists, policy, SBOM diffs) needs updating.

Builds on the cpan cataloging branch, which adds the package type this uses.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-30 15:45:46 -04:00
King StarandWill Murphy 509f1a1a5b fix(java): filter Tomcat server CPE for embedded EL (#5322)
Signed-off-by: King Star <54024410+jstar0@users.noreply.github.com>
Co-authored-by: Will Murphy <willmurphyscode@users.noreply.github.com>
2026-09-30 15:34:29 -04:00
Alex Goodman 8d5567c423 feat: catalog CPAN distributions installed by perl clients (#5131)
* 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>
2026-09-30 14:15:05 -04:00
63 changed files with 8298 additions and 1218 deletions
@@ -524,4 +524,12 @@ var commonTestCases = []testCase{
"kong": "3.7.0-0",
},
},
{
name: "find cpan distribution",
pkgType: pkg.CpanPkg,
pkgLanguage: pkg.Perl,
pkgInfo: map[string]string{
"URI": "5.35",
},
},
}
@@ -0,0 +1,9 @@
{
"abstract": "Uniform Resource Identifiers (absolute and relative)",
"license": ["perl_5"],
"meta-spec": {"version": 2},
"name": "URI",
"prereqs": {"runtime": {"requires": {"Carp": "0", "perl": "5.008001"}}},
"release_status": "stable",
"version": "5.35"
}
@@ -0,0 +1,11 @@
{
"dist": "URI-5.35",
"name": "URI",
"pathname": "O/OA/OALDERS/URI-5.35.tar.gz",
"provides": {
"URI": {"file": "lib/URI.pm", "version": "5.35"},
"URI::Escape": {"file": "lib/URI/Escape.pm", "version": "5.35"}
},
"target": "URI",
"version": "5.35"
}
+53 -29
View File
@@ -22,7 +22,7 @@ require (
github.com/anchore/go-sync v0.1.2
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.2.0
github.com/anchore/stereoscope v0.3.2
github.com/anchore/stereoscope v0.3.3
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
github.com/aquasecurity/go-pep440-version v0.0.1
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
@@ -115,6 +115,7 @@ require (
cloud.google.com/go/iam v1.12.0 // indirect
cloud.google.com/go/monitoring v1.30.0 // indirect
cloud.google.com/go/storage v1.65.1 // indirect
cyphar.com/go-pathrs v0.2.5 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/DataDog/zstd v1.5.5 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect
@@ -122,9 +123,10 @@ require (
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 // indirect
github.com/Microsoft/hcsshim v0.15.0-rc.1 // indirect
github.com/Microsoft/hcsshim v0.15.0-rc.4 // indirect
github.com/ProtonMail/go-crypto v1.4.1 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/agext/levenshtein v1.2.1 // indirect
github.com/anchore/go-lzo v0.1.1 // indirect
github.com/anchore/go-struct-converter v0.2.0-rc2 // indirect
@@ -169,20 +171,25 @@ require (
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/containerd/cgroups/v3 v3.1.3 // indirect
github.com/containerd/containerd/api v1.11.1 // indirect
github.com/containerd/containerd/v2 v2.3.6 // indirect
github.com/containerd/containerd/api v1.12.0 // indirect
github.com/containerd/containerd/v2 v2.4.1 // indirect
github.com/containerd/continuity v0.5.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/fifo v1.1.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/log v0.2.0 // indirect
github.com/containerd/log/otel v0.1.0 // indirect
github.com/containerd/platforms v1.0.0-rc.5 // indirect
github.com/containerd/plugin v1.1.0 // indirect
github.com/containerd/ttrpc v1.2.8 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/docker/cli v29.8.0+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
github.com/containerd/ttrpc v1.2.9 // indirect
github.com/containerd/typeurl/v2 v2.3.0 // indirect
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
github.com/containers/ocicrypt v1.3.2 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
github.com/cyphar/filepath-securejoin v0.7.0 // indirect
github.com/docker/cli v29.8.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.8 // indirect
github.com/docker/go-connections v0.8.1 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
@@ -192,17 +199,17 @@ require (
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/felixge/fgprof v0.9.5 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/felixge/httpsnoop v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
github.com/gkampitakis/ciinfo v0.3.4 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.5 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-restruct/restruct v1.2.0-alpha // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/go-intervals v0.0.2 // indirect
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.20 // indirect
@@ -226,10 +233,13 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect
github.com/mattn/go-runewidth v0.0.21 // indirect
github.com/mattn/go-runewidth v0.0.27 // indirect
github.com/mattn/go-sqlite3 v1.14.48 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/pkcs11 v1.1.2 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect
github.com/mistifyio/go-zfs/v4 v4.0.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
@@ -238,10 +248,10 @@ require (
github.com/moby/locker v1.0.1 // indirect
github.com/moby/moby/api v1.56.0 // indirect
github.com/moby/moby/client v0.6.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/capability v0.4.0 // indirect
github.com/moby/sys/signal v0.7.1 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/sys/user v0.4.1 // indirect
github.com/moby/sys/userns v0.2.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
@@ -254,6 +264,7 @@ require (
github.com/olekukonko/ll v0.1.6 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opencontainers/runtime-spec v1.3.0 // indirect
github.com/opencontainers/selinux v1.15.1 // indirect
github.com/pborman/indent v1.2.1 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
@@ -264,46 +275,59 @@ require (
github.com/pkg/xattr v0.4.12 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/proglottis/gpgme v0.1.6 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/sigstore/fulcio v1.8.7 // indirect
github.com/sigstore/protobuf-specs v0.5.1 // indirect
github.com/sigstore/sigstore v1.10.8 // indirect
github.com/sirupsen/logrus v1.10.2 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d // indirect
github.com/smallstep/pkcs7 v0.2.1 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/sylabs/sif/v2 v2.24.1 // indirect
github.com/sylabs/squashfs v1.0.6 // indirect
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/tidwall/gjson v1.19.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/vbatts/tar-split v0.12.3 // indirect
github.com/vbauerster/cupwriter v0.0.4 // indirect
github.com/vbauerster/mpb/v8 v8.14.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/zclconf/go-cty v1.19.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.45.0 // indirect
go.opentelemetry.io/otel/metric v1.45.0 // indirect
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
go.opentelemetry.io/otel/trace v1.45.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 // indirect
go.opentelemetry.io/otel v1.46.0 // indirect
go.opentelemetry.io/otel/metric v1.46.0 // indirect
go.opentelemetry.io/otel/sdk v1.46.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.46.0 // indirect
go.opentelemetry.io/otel/trace v1.46.0 // indirect
go.podman.io/image/v5 v5.41.2 // indirect
go.podman.io/storage v1.64.1 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/crypto v0.57.0 // indirect
@@ -316,8 +340,8 @@ require (
gonum.org/v1/gonum v0.17.0 // indirect
google.golang.org/api v0.294.0 // indirect
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.12 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
+157 -63
View File
@@ -67,6 +67,8 @@ cloud.google.com/go/storage v1.65.1 h1:LRRpBJUTf+OXDPX9jZUKZ3mSLIsz3htG+qUpeNZov
cloud.google.com/go/storage v1.65.1/go.mod h1:UsS9OgFg/XHOSYakQ8ZtLWWeyGkk1WnmD/GsGfN0BHM=
cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E=
cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0=
cyphar.com/go-pathrs v0.2.5 h1:SnX9FBvnoyn3lUs1dkMgZ52bAETpirNu3FTRh5HlRik=
cyphar.com/go-pathrs v0.2.5/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
@@ -99,8 +101,8 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY=
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0=
github.com/Microsoft/hcsshim v0.15.0-rc.1 h1:FbbwtQmiD+BVHynGkx5S65JkLyhkEiiTP8nrpmg2SZw=
github.com/Microsoft/hcsshim v0.15.0-rc.1/go.mod h1:HWvvUPIy9HF6LotILj1G4VyS065rcLQ6tqj6tMUdOfI=
github.com/Microsoft/hcsshim v0.15.0-rc.4 h1:aZFX4LH0S20Lgjq0wG61StIClj7im4yzrxIClkaR8Z8=
github.com/Microsoft/hcsshim v0.15.0-rc.4/go.mod h1:BA9CBztgu4h/6Jsvo1O1M4qjWw09PoYpaEYgexPE578=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
@@ -108,6 +110,8 @@ github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE=
@@ -146,8 +150,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk=
github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE=
github.com/anchore/stereoscope v0.3.2 h1:A2FDMSU56m3rErO1e0CtWAVcDS8tc2iX4UXTw1hyPnI=
github.com/anchore/stereoscope v0.3.2/go.mod h1:/IUDVDka+4gufc8kDw+JO3gFl9PkpCjJ2r3bd9UWouM=
github.com/anchore/stereoscope v0.3.3 h1:sur8G2THAOnOrSXDjIc1SXzQwco8hjFBtGFq1jFvNVA=
github.com/anchore/stereoscope v0.3.3/go.mod h1:LxL4iOFtB8XMbA1MykUUTkmtQmDuYdbr4+TkBkCfljU=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
@@ -294,10 +298,10 @@ github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ=
github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=
github.com/containerd/containerd/api v1.11.1 h1:h8nfoDW9+fNsC/9TwiAHj8B1GzXKtR4eFtkhi/X5RLU=
github.com/containerd/containerd/api v1.11.1/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw=
github.com/containerd/containerd/v2 v2.3.6 h1:huoqZXaDW1x1kgPvFC4rMwxv92vUJnqgq7I5b1rSddM=
github.com/containerd/containerd/v2 v2.3.6/go.mod h1:qaUSWKk9EchqbudSbhv1MxI8dXKGYHLpZtMQdkEkVZ4=
github.com/containerd/containerd/api v1.12.0 h1:kuQm82SbDrCuO4n7hf2L8zsBtZLuympyq5X/VotfX2A=
github.com/containerd/containerd/api v1.12.0/go.mod h1:EBcSzoi9Vl18cdODaXUCskf3D2NT8lsSXeZJnU5jIUc=
github.com/containerd/containerd/v2 v2.4.1 h1:DUx/ZJN7cEu0WuzHClDB+68H/bqMEH5pWoEjf0ae4hc=
github.com/containerd/containerd/v2 v2.4.1/go.mod h1:vgLdtvl3prFk1d3ZVqQcsDD5Q9IKM+vAIdU54U85w+I=
github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg=
github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@@ -306,23 +310,33 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY=
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/log v0.2.0 h1:BewD/umNgVnoczglOpX8eRMyEy5t5iPlu5AIpnWDONc=
github.com/containerd/log v0.2.0/go.mod h1:/M7L7CXKcPTfNC74XzaK+5H5KbO5+4lJVpuVI6vRLoM=
github.com/containerd/log/otel v0.1.0 h1:Az5rFFo0+c4v2yC8ROR63sWsZpKl/vhtUnUhqLSpE6U=
github.com/containerd/log/otel v0.1.0/go.mod h1:65C5iYF2xIQByCyxzoO2KKKK1+/tGxD4XqhdlrTtvzE=
github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8oyaRarxdmSU=
github.com/containerd/platforms v1.0.0-rc.5/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=
github.com/containerd/plugin v1.1.0 h1:O+7lczNJVMy8rz0YNx3xGB8tTf5qY4i5abF041Ew19U=
github.com/containerd/plugin v1.1.0/go.mod h1:qBTum+A8lJ6lO44A19Eo7y1OlcLj4OWFH1DA/vnHmcc=
github.com/containerd/ttrpc v1.2.8 h1:xbVu6D4qF2jihdh9rDVOKqUMiFBQk6YctTdo1zk087Y=
github.com/containerd/ttrpc v1.2.8/go.mod h1:wyZW2K79t4Hfcxl+GUvkZqRBzJlqFFvgEeeWXa42tyE=
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw=
github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY=
github.com/containerd/ttrpc v1.2.9 h1:ha0ak962T0s3CA/RoZ6S6xiWZQF24GrBaEpiGX1uihg=
github.com/containerd/ttrpc v1.2.9/go.mod h1:jjtQRwXm4DL3KsHKW8vDiUOV6wO0hi6IPhmJhxU7aEs=
github.com/containerd/typeurl/v2 v2.3.0 h1:HZHPhRWo5XMy3QGQoPrUzbW/2ckwjfweHmOwlkIrPAQ=
github.com/containerd/typeurl/v2 v2.3.0/go.mod h1:Qk+PAdUYArVj41TnGi6rJ+48RF0PkcTc4i/taoBcK0w=
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA=
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY=
github.com/containers/ocicrypt v1.3.2 h1:MuqHSfiPpGzoAQdgDSX85FgixSgIm9mSDXTLTgugY5E=
github.com/containers/ocicrypt v1.3.2/go.mod h1:ntBZabYG0rlvstB/1rK/ba2laaU5EHE0pD5ioHoPTq4=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q=
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE=
github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4=
github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -339,10 +353,12 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v29.8.0+incompatible h1:ih0c2jq/nN7QfES8zIfwSzIhRScjs4ehER+kZ60aeSk=
github.com/docker/cli v29.8.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/cli v29.8.1+incompatible h1:qYL1bCp6cRw2SB1xmLlIOPyV171dilw9W2Jew38vy9c=
github.com/docker/cli v29.8.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.9.8 h1:bIREROb7So6PRlq6KTtdS9MPEjC29OQRkFNlvK2OX8Q=
github.com/docker/docker-credential-helpers v0.9.8/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -381,8 +397,8 @@ github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMD
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/erofs/go-erofs v0.3.0 h1:o/W5ABAA3sHYl97WL93dacKEfeDpJhdFf3c2snAti7I=
github.com/erofs/go-erofs v0.3.0/go.mod h1:XkSeN9MHszGd4+3gcEjadJLYHCQpWzJ7/8yznzMuzJs=
github.com/erofs/go-erofs v0.3.1 h1:Sux82Jq9yvyYhIoLgSHDp741p/+370HsOj9dAh1+VVs=
github.com/erofs/go-erofs v0.3.1/go.mod h1:XkSeN9MHszGd4+3gcEjadJLYHCQpWzJ7/8yznzMuzJs=
github.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c/go.mod h1:QGzNH9ujQ2ZUr/CjDGZGWeDAVStrWNjHeEcjJL96Nuk=
github.com/facebookincubator/nvdtools v0.1.5 h1:jbmDT1nd6+k+rlvKhnkgMokrCAzHoASWE5LtHbX2qFQ=
github.com/facebookincubator/nvdtools v0.1.5/go.mod h1:Kh55SAWnjckS96TBSrXI99KrEKH4iB0OJby3N8GRJO4=
@@ -396,13 +412,13 @@ github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0C
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -427,8 +443,8 @@ github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuw
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU6eA=
github.com/go-jose/go-jose/v4 v4.1.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
@@ -453,7 +469,6 @@ github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/gohugoio/hashstructure v0.6.0 h1:7wMB/2CfXoThFYhdWRGv3u3rUM761Cq29CxUW+NltUg=
github.com/gohugoio/hashstructure v0.6.0/go.mod h1:lapVLk9XidheHG1IQ4ZSbyYrXcaILU1ZEP/+vno5rBQ=
@@ -507,10 +522,13 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.22.1 h1:RZuuSYhTvlDvtsK+NkutoCZ//C0X2ebLK8X8l3ULs84=
github.com/google/go-containerregistry v0.22.1/go.mod h1:bJR35SK8XgisYmhg/FMQ/5RK0S/XrOAqLBV5/LR2XE0=
github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM=
github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs=
github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY=
@@ -543,6 +561,7 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.20 h1:t/xL64VUoN69MuMRQuJETqYGOw4Z9mSRJK9epIEtwFk=
@@ -558,6 +577,8 @@ github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj
github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gpustack/gguf-parser-go v0.26.3 h1:F63PlUPIW56HGU5k4Crv0JAG9ojtz/TuwzgMYlb82Ec=
github.com/gpustack/gguf-parser-go v0.26.3/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
@@ -692,8 +713,10 @@ github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsRe
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0=
github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs=
github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
@@ -702,10 +725,14 @@ github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPp
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ=
github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=
github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/mistifyio/go-zfs/v4 v4.0.0 h1:sU0+5dX45tdDK5xNZ3HBi95nxUc48FS92qbIZEvpAg4=
github.com/mistifyio/go-zfs/v4 v4.0.0/go.mod h1:weotFtXTHvBwhr9Mv96KYnDkTPBOHFUbm9cBmQpesL0=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
@@ -727,16 +754,16 @@ github.com/moby/moby/api v1.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ=
github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs=
github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ=
github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0=
github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0=
github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y=
github.com/moby/sys/userns v0.2.1 h1:4OvdM7BcPkASbuouHsbW3aeMJSFlYDldBRnXVZhaRk8=
github.com/moby/sys/userns v0.2.1/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -776,6 +803,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg=
github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.15.1 h1:ERxeh5caJvCzNAKdI8WQbJmB1LDTn4BuaAg8wihLBpA=
github.com/opencontainers/selinux v1.15.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -814,6 +843,8 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/proglottis/gpgme v0.1.6 h1:8WpQ8VWggLdxkuTnW+sZ1r1t92XBNd8GZNDhQ4Rz+98=
github.com/proglottis/gpgme v0.1.6/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
@@ -826,8 +857,8 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY=
github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -853,6 +884,8 @@ github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg=
github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI=
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvvtIRwBrU/aegEYJYmvev0cHAwo17zZQ=
@@ -860,21 +893,31 @@ github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd7
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=
github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/secure-systems-lab/go-securesystemslib v0.11.0 h1:iuCR9kcMFD4QurdKrGvPLoKZLv9YvwPYVr0473BdtFs=
github.com/secure-systems-lab/go-securesystemslib v0.11.0/go.mod h1:+PMOTjUGwHj2vcZ+TFKlb1tXRbrdWE1LYDT5i9JC80Q=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sigstore/fulcio v1.8.7 h1:d7QJQxukgXarqPCT9uDNeyzt1Bbt/biw58/VfOnwI/o=
github.com/sigstore/fulcio v1.8.7/go.mod h1:7fX+QyigZxRGKTgHvTfOfxwwniCDxK02PVloHy5/vg4=
github.com/sigstore/protobuf-specs v0.5.1 h1:/5OPaNuolRJmQfeZLayJGFXMpsRJEdgC6ah1/+7Px7U=
github.com/sigstore/protobuf-specs v0.5.1/go.mod h1:DRBzpFuE+LnvQMN10/dU6nBeKwVLGEQ6o2FovN2Rats=
github.com/sigstore/sigstore v1.10.8 h1:1Mgkxvkw4AXMfIP1DOjc6kw0GkUgA8pGVpveN/EfOq4=
github.com/sigstore/sigstore v1.10.8/go.mod h1:f9+B/4iaYimvUkySyb2mvc73n3RLqNn24grHZM/ET8M=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d h1:3VwvTjiRPA7cqtgOWddEL+JrcijMlXUmj99c/6YyZoY=
github.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d/go.mod h1:tAG61zBM1DYRaGIPloumExGvScf08oHuo0kFoOqdbT0=
github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
@@ -904,6 +947,8 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/spiffe/go-spiffe/v2 v2.7.0 h1:uXe1MflJoHw58wAUvxVlcM7WpKtijWG7I1UidcGh6g4=
github.com/spiffe/go-spiffe/v2 v2.7.0/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U=
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw=
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -931,6 +976,8 @@ github.com/sylabs/squashfs v1.0.6 h1:PvJcDzxr+vIm2kH56mEMbaOzvGu79gK7P7IX+R7BDZI
github.com/sylabs/squashfs v1.0.6/go.mod h1:DlDeUawVXLWAsSRa085Eo0ZenGzAB32JdAUFaB0LZfE=
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA=
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=
github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc=
github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/terminalstatic/go-xsd-validate v0.1.8 h1:UVrTCy1j3DhwaYTTUF+QYO/Nan13S0tf+Jwi+p45Bf0=
github.com/terminalstatic/go-xsd-validate v0.1.8/go.mod h1:1kb47fi2c6onlf+B7UrrQ9VYraOhcYwFm3iG+J6F4Zo=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
@@ -951,6 +998,12 @@ github.com/ulikunitz/xz v0.5.16 h1:ld6NyySjx5lowVKwJvMRLnW5nxKX/xnpSiFYZ/Lxur0=
github.com/ulikunitz/xz v0.5.16/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw=
github.com/vbatts/go-mtree v0.7.0 h1:ytmOc3MTRidZiBi9VBCyZ2BHe4fZS47L5v7BVXDWW4E=
github.com/vbatts/go-mtree v0.7.0/go.mod h1:EjdpFC+LZy1TXbRGNa1MKKgjQ+7ew3foMFJK8o4/TdY=
github.com/vbatts/tar-split v0.12.3 h1:Cd46rkGXI3Td4yrVNwU8ripbxFaQbmesqhjBUUYAJSw=
github.com/vbatts/tar-split v0.12.3/go.mod h1:sQOc6OlqGCr7HkGx/IDBeKiTIvqhmj8KffNhEXG4Nq0=
github.com/vbauerster/cupwriter v0.0.4 h1:9sBPe0uXWLZuWQU5lqVbhyFlxX6c09asST/YfatFAys=
github.com/vbauerster/cupwriter v0.0.4/go.mod h1:IFyzS6Xis5dnBH/rdAhrnuzg3c+KkUqEN6yE8lhJlDw=
github.com/vbauerster/mpb/v8 v8.14.0 h1:55SR80dptMfASxIG/oCEkBXgBhxeSu4GrVsjl16oKmA=
github.com/vbauerster/mpb/v8 v8.14.0/go.mod h1:HgpQPKfcWe3kbuGGPmi+jatHreMase5C3Fp5dpdAy0Q=
github.com/vifraa/gopom v1.0.0 h1:L9XlKbyvid8PAIK8nr0lihMApJQg/12OBvMA28BcWh0=
github.com/vifraa/gopom v1.0.0/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o=
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA=
@@ -972,6 +1025,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -994,31 +1049,33 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw=
go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 h1:B2h3uqicet1CT2N5TOFhS+Gq++9i0/CLmaxvhmhtP5s=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0/go.mod h1:dylvB+ZiiwMvsDij9O84Uy7SijLgHMX4mbkncds+4Sw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU=
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q=
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns=
go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI=
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
go.opentelemetry.io/otel/metric/x v0.68.0 h1:TA/cBT23D3MnxYPwHL7YFOdYGdx0A0v+s7Mzotpd1dU=
go.opentelemetry.io/otel/metric/x v0.68.0/go.mod h1:agudOmvWhwUTjgibWDzxD2PoWYnpw5Ht5jISYOD2Hd4=
go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.podman.io/image/v5 v5.41.2 h1:YnVaFrGLPWSBmipKfNvlOsGcBtk5bUxlPxQ+A6CS/uo=
go.podman.io/image/v5 v5.41.2/go.mod h1:T/bzxEFpdpAzyHabPmcq5K+my3wBsht22sFkFjJSKT4=
go.podman.io/storage v1.64.1 h1:DqbG8scW/bD3xHiSRzV7O5fzn46qVC+SwmUN6C6Fr5w=
go.podman.io/storage v1.64.1/go.mod h1:ft0DzCRMZs1yn0rszmLYXEEwgd7IIc66JRwSRPUDwwc=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
@@ -1043,6 +1100,10 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1084,6 +1145,10 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1129,7 +1194,12 @@ golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -1163,6 +1233,11 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1239,11 +1314,22 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1256,6 +1342,11 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1318,6 +1409,9 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1434,10 +1528,10 @@ google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ6
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d h1:C9v1o0/4quuhOAfmRXA2j+we0PqZIp8traLdeogF3Ms=
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d/go.mod h1:Wz2wFJntZFmLGo7pLDXZ3wYk5hyc0Mb+SkHhDDXT+lU=
google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d h1:QwnJwPte4XXAkhPu26LTDIahnsMSUV0kK8HkxbC+Pc4=
google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d/go.mod h1:WRrQ7/7N19PypuT0fxLOL5Lq0waoiRri4FbtHDEKrGE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 h1:izFU9hz7aeLI/Mi1J0991ae+xcwRLr7hTqWnB/9aIIU=
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5/go.mod h1:3LhxRw4YYkf+ylAfgaY9JlVLFKhokkCV8duhLLe7+t0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 h1:1VUiZAXyC+zmiFYi+WLtBzr68Cj8wOofHjjrA/kkizc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+14 -1
View File
@@ -502,6 +502,19 @@ func convertToJSONSchemaTypesFromMetadata(metadataTypes []string) []string {
return result
}
// packageTypeConstName renders a package type using its go constant name, which is what the detector
// "type" field reports (e.g. "BinaryPkg").
// a switch rather than a derivation, since pkg.Type values don't reliably map back to their constant
// names (e.g. "java-archive" is JavaPkg); add an arm when a classifier declares a new type.
func packageTypeConstName(t pkg.Type) string {
switch t {
case pkg.CpanPkg:
return "CpanPkg"
default:
return "BinaryPkg"
}
}
// EnrichWithBinaryClassifier enriches an entry with binary classifier detectors if it's the binary-classifier-cataloger
func (e *EnrichmentData) EnrichWithBinaryClassifier(catalogerName string, entry *capabilities.CatalogerEntry) {
// special handling for binary-classifier-cataloger: auto-generate one detector per classifier
@@ -523,7 +536,7 @@ func (e *EnrichmentData) EnrichWithBinaryClassifier(catalogerName string, entry
Name: classifier.Package,
PURL: purlStr,
CPEs: cpeStrings,
Type: "BinaryPkg",
Type: packageTypeConstName(classifier.PackageType()),
},
}
+2 -1
View File
@@ -3,7 +3,7 @@ package internal
const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "16.1.10"
JSONSchemaVersion = "16.1.11"
// Changelog
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
@@ -17,4 +17,5 @@ const (
// 16.1.8 - add VcpkgManifest metadata type for vcpkg manifest support
// 16.1.9 - add Symbols (grouped by owning package import path) to GolangBinaryBuildinfoEntry metadata
// 16.1.10 - add SafeTensorsModelInfo metadata type for the safetensors AI model cataloger
// 16.1.11 - add CpanDistribution and CpanUnpackedRelease metadata types for the perl catalogers
)
+2
View File
@@ -19,6 +19,8 @@ func AllTypes() []any {
pkg.ConanfileEntry{},
pkg.ConaninfoEntry{},
pkg.CondaMetaPackage{},
pkg.CpanDistribution{},
pkg.CpanUnpackedRelease{},
pkg.DartPubspec{},
pkg.DartPubspecLockEntry{},
pkg.DenoLockEntry{},
+2
View File
@@ -71,6 +71,8 @@ var jsonTypes = makeJSONTypes(
jsonNames(pkg.ConanV2LockEntry{}, "c-conan-lock-v2-entry"),
jsonNames(pkg.ConanfileEntry{}, "c-conan-file-entry", "ConanMetadataType"),
jsonNames(pkg.ConaninfoEntry{}, "c-conan-info-entry"),
jsonNames(pkg.CpanDistribution{}, "cpan-distribution"),
jsonNames(pkg.CpanUnpackedRelease{}, "cpan-unpacked-release"),
jsonNames(pkg.DartPubspecLockEntry{}, "dart-pubspec-lock-entry", "DartPubMetadata"),
jsonNames(pkg.DartPubspec{}, "dart-pubspec"),
jsonNames(pkg.DenoLockEntry{}, "deno-lock-entry"),
@@ -279,6 +279,46 @@ func TestOwnershipByFilesRelationship(t *testing.T) {
return []pkg.Package{parent, child}, nil
},
},
{
// a CPAN distribution owns whatever its .packlist recorded, which is how a distribution that
// installed a binary or a shared object is related to the package cataloged from it
name: "cpan-distribution-owns-packlist-paths",
setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
parent := pkg.Package{
Locations: file.NewLocationSet(
file.NewLocation("/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux/auto/Net/SSLeay/.packlist"),
),
Type: pkg.CpanPkg,
Metadata: pkg.CpanDistribution{
MainModule: "Net::SSLeay",
Files: []string{
"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux/Net/SSLeay.pm",
"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux/auto/Net/SSLeay/SSLeay.so",
},
},
}
parent.SetID()
child := pkg.Package{
Locations: file.NewLocationSet(
file.NewLocation("/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux/auto/Net/SSLeay/SSLeay.so"),
),
Type: pkg.BinaryPkg,
}
child.SetID()
return []pkg.Package{parent, child}, []artifact.Relationship{
{
From: parent,
To: child,
Type: artifact.OwnershipByFileOverlapRelationship,
Data: ownershipByFilesMetadata{
Files: []string{"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux/auto/Net/SSLeay/SSLeay.so"},
},
},
}
},
},
}
for _, test := range tests {
+6
View File
@@ -27,6 +27,7 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/lua"
"github.com/anchore/syft/syft/pkg/cataloger/nix"
"github.com/anchore/syft/syft/pkg/cataloger/ocaml"
"github.com/anchore/syft/syft/pkg/cataloger/perl"
"github.com/anchore/syft/syft/pkg/cataloger/php"
"github.com/anchore/syft/syft/pkg/cataloger/python"
"github.com/anchore/syft/syft/pkg/cataloger/r"
@@ -132,6 +133,10 @@ func DefaultPackageTaskFactories() Factories {
newSimplePackageTaskFactory(swift.NewSwiftPackageManagerCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "swift", "spm"),
newSimplePackageTaskFactory(swipl.NewSwiplPackCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "swipl", "pack"),
newSimplePackageTaskFactory(ocaml.NewOpamPackageManagerCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "ocaml", "opam"),
// note: no image tag -- an unpacked release's own META.json describes a distribution present on
// disk but not installed into the interpreter's @INC, and declared information is not cataloged
// inside container images
newSimplePackageTaskFactory(perl.NewCpanMetaCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "perl", "cpan"),
// language-specific package for both image and directory scans (but not necessarily declared) ////////////////////////////////////////
newPackageTaskFactory(
@@ -162,6 +167,7 @@ func DefaultPackageTaskFactories() Factories {
pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "nix",
),
newSimplePackageTaskFactory(lua.NewPackageCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "lua"),
newSimplePackageTaskFactory(perl.NewCpanInstalledCataloger, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "perl", "cpan"),
// other package catalogers ///////////////////////////////////////////////////////////////////////////
newPackageTaskFactory(
File diff suppressed because it is too large Load Diff
+73 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.1.10/document",
"$id": "anchore.io/schema/syft/json/16.1.11/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@@ -729,6 +729,72 @@
],
"description": "Coordinates contains the minimal information needed to describe how to find a file within any possible source object (e.g."
},
"CpanDistribution": {
"properties": {
"dist": {
"type": "string",
"description": "Dist is the distvname install.json recorded (e.g. URI-5.35), the raw value the package name and\nversion were recovered from"
},
"mainModule": {
"type": "string",
"description": "MainModule is the module name recovered from the .packlist path (e.g. LWP for libwww-perl).\nEmpty when the name came from install.json, which is authoritative. Set, it means the name is the\ninstaller's NAME and may not be the distribution name."
},
"author": {
"type": "string",
"description": "Author is the PAUSE ID of the distribution author (e.g. OALDERS), parsed out of Path"
},
"path": {
"type": "string",
"description": "Path is the raw PAUSE path of the release archive (e.g. O/OA/OALDERS/URI-5.35.tar.gz)"
},
"modules": {
"items": {
"$ref": "#/$defs/CpanModule"
},
"type": "array",
"description": "Modules are the modules this distribution provides, sorted by name"
},
"files": {
"items": {
"type": "string"
},
"type": "array",
"description": "Files are the paths the .packlist recorded as installed by this distribution, kept in the\npacklist's own order and unfiltered: a path the packlist claims and the filesystem lacks means the\nfile was removed or overwritten out from under the installer, which is signal rather than noise"
}
},
"type": "object",
"description": "CpanDistribution describes a CPAN distribution installed on disk, as recorded by a CPAN client's install.json and by the .packlist an installer wrote."
},
"CpanModule": {
"properties": {
"name": {
"type": "string",
"description": "Name is the module name as it would be used in a perl `use` statement (e.g. URI::Escape)"
},
"version": {
"type": "string",
"description": "Version is the module version, which may differ from the distribution version"
}
},
"type": "object",
"required": [
"name"
],
"description": "CpanModule is a single perl module provided by a CPAN distribution."
},
"CpanUnpackedRelease": {
"properties": {
"modules": {
"items": {
"$ref": "#/$defs/CpanModule"
},
"type": "array",
"description": "Modules are the modules the release declares it provides, sorted by name"
}
},
"type": "object",
"description": "CpanUnpackedRelease describes an unpacked CPAN release found on disk, read from the release's own META.json or META.yml."
},
"DartPubspec": {
"properties": {
"homepage": {
@@ -2772,6 +2838,12 @@
{
"$ref": "#/$defs/CondaMetadataEntry"
},
{
"$ref": "#/$defs/CpanDistribution"
},
{
"$ref": "#/$defs/CpanUnpackedRelease"
},
{
"$ref": "#/$defs/DartPubspec"
},
-39
View File
@@ -114,45 +114,6 @@ func TestCycloneDxImageEncoder(t *testing.T) {
)
}
func TestCycloneDxSourcePackageEncoder(t *testing.T) {
dir := t.TempDir()
testutil.AssertEncoderAgainstGoldenSnapshot(t,
testutil.EncoderSnapshotTestConfig{
Subject: testutil.SourcePackagesInput(t, dir),
Format: getEncoder(t),
UpdateSnapshot: *updateSnapshot,
PersistRedactionsInSnapshot: true,
IsJSON: true,
Redactor: redactor(dir),
},
)
}
func TestCycloneDxSourcePackageEncoder_SpecVersions(t *testing.T) {
s := testutil.SourcePackagesInput(t, t.TempDir())
tests := []struct {
version string
want bool
}{
{version: "1.4", want: false},
{version: "1.5", want: false},
{version: "1.6", want: true},
{version: cyclonedxutil.DefaultVersion, want: true},
}
for _, tt := range tests {
t.Run(tt.version, func(t *testing.T) {
enc, err := NewFormatEncoderWithConfig(EncoderConfig{Version: tt.version})
require.NoError(t, err)
var buf bytes.Buffer
require.NoError(t, enc.Encode(&buf, s))
// the source-distribution reference type only exists from spec 1.6
assert.Equal(t, tt.want, strings.Contains(buf.String(), `"source-distribution"`))
})
}
}
func redactor(values ...string) testutil.Redactor {
return testutil.NewRedactions().
WithValuesRedacted(values...).
@@ -1,275 +0,0 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.7",
"serialNumber": "urn:uuid:redacted",
"version": 1,
"metadata": {
"timestamp": "timestamp:redacted",
"tools": {
"components": [
{
"type": "application",
"author": "anchore",
"name": "syft",
"version": "v0.42.0-bogus"
}
]
},
"component": {
"bom-ref": "redacted",
"type": "file",
"name": "some/path"
}
},
"components": [
{
"bom-ref": "pkg:deb/debian/attr@1%3A2.4.47-2%2Bb1?arch=amd64&distro=debian-12&package-id=5cc0e8979d7f5115&upstream=attr%401%3A2.4.47-2",
"type": "library",
"name": "attr",
"version": "1:2.4.47-2+b1",
"purl": "pkg:deb/debian/attr@1%3A2.4.47-2%2Bb1?arch=amd64&distro=debian-12&upstream=attr%401%3A2.4.47-2",
"externalReferences": [
{
"url": "pkg:deb/debian/attr@1%3A2.4.47-2?arch=source&distro=debian-12",
"type": "source-distribution"
}
],
"properties": [
{
"name": "syft:package:foundBy",
"value": "dpkg-db-cataloger"
},
{
"name": "syft:package:type",
"value": "deb"
},
{
"name": "syft:package:metadataType",
"value": "dpkg-db-entry"
},
{
"name": "syft:location:0:path",
"value": "/var/lib/dpkg/status"
},
{
"name": "syft:metadata:installedSize",
"value": "0"
},
{
"name": "syft:metadata:source",
"value": "attr"
},
{
"name": "syft:metadata:sourceVersion",
"value": "1:2.4.47-2"
}
]
},
{
"bom-ref": "pkg:rpm/centos/bind-export-libs@9.11.13-3.el8?arch=x86_64&distro=centos-8&epoch=32&package-id=6008fca05ca7b418&upstream=bind-9.11.13-3.el8.src.rpm",
"type": "library",
"name": "bind-export-libs",
"version": "32:9.11.13-3.el8",
"purl": "pkg:rpm/centos/bind-export-libs@9.11.13-3.el8?arch=x86_64&distro=centos-8&epoch=32&upstream=bind-9.11.13-3.el8.src.rpm",
"externalReferences": [
{
"url": "pkg:rpm/centos/bind@9.11.13-3.el8?arch=src&distro=centos-8&epoch=32",
"type": "source-distribution"
}
],
"properties": [
{
"name": "syft:package:foundBy",
"value": "rpm-db-cataloger"
},
{
"name": "syft:package:type",
"value": "rpm"
},
{
"name": "syft:package:metadataType",
"value": "rpm-db-entry"
},
{
"name": "syft:location:0:path",
"value": "/var/lib/rpm/Packages"
},
{
"name": "syft:metadata:epoch",
"value": "32"
},
{
"name": "syft:metadata:release",
"value": "3.el8"
},
{
"name": "syft:metadata:size",
"value": "0"
},
{
"name": "syft:metadata:sourceRpm",
"value": "bind-9.11.13-3.el8.src.rpm"
}
]
},
{
"bom-ref": "pkg:apk/alpine/busybox@1.35.0-r17?arch=x86_64&distro=alpine-3.16.3&package-id=aa74af1190de1c40",
"type": "library",
"name": "busybox",
"version": "1.35.0-r17",
"purl": "pkg:apk/alpine/busybox@1.35.0-r17?arch=x86_64&distro=alpine-3.16.3",
"properties": [
{
"name": "syft:package:foundBy",
"value": "apk-db-cataloger"
},
{
"name": "syft:package:type",
"value": "apk"
},
{
"name": "syft:package:metadataType",
"value": "apk-db-entry"
},
{
"name": "syft:location:0:path",
"value": "/lib/apk/db/installed"
},
{
"name": "syft:metadata:installedSize",
"value": "0"
},
{
"name": "syft:metadata:originPackage",
"value": "busybox"
},
{
"name": "syft:metadata:size",
"value": "0"
}
]
},
{
"bom-ref": "pkg:alpm/arch/gcc-libs@13.2.1-3?arch=x86_64&distro=arch-rolling&package-id=58a2fcf749a9a3cc&upstream=gcc",
"type": "library",
"name": "gcc-libs",
"version": "13.2.1-3",
"purl": "pkg:alpm/arch/gcc-libs@13.2.1-3?arch=x86_64&distro=arch-rolling&upstream=gcc",
"properties": [
{
"name": "syft:package:foundBy",
"value": "alpm-db-cataloger"
},
{
"name": "syft:package:type",
"value": "alpm"
},
{
"name": "syft:package:metadataType",
"value": "alpm-db-entry"
},
{
"name": "syft:location:0:path",
"value": "/var/lib/pacman/local/gcc-libs-13.2.1-3/desc"
},
{
"name": "syft:metadata:architecture",
"value": "x86_64"
},
{
"name": "syft:metadata:basepackage",
"value": "gcc"
},
{
"name": "syft:metadata:package",
"value": "gcc-libs"
},
{
"name": "syft:metadata:size",
"value": "0"
},
{
"name": "syft:metadata:version",
"value": "13.2.1-3"
}
]
},
{
"bom-ref": "pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64&distro=alpine-3.16.3&package-id=16a8c7b19fde6f4f&upstream=libc-dev",
"type": "library",
"name": "libc-utils",
"version": "0.7.2-r3",
"purl": "pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64&distro=alpine-3.16.3&upstream=libc-dev",
"properties": [
{
"name": "syft:package:foundBy",
"value": "apk-db-cataloger"
},
{
"name": "syft:package:type",
"value": "apk"
},
{
"name": "syft:package:metadataType",
"value": "apk-db-entry"
},
{
"name": "syft:location:0:path",
"value": "/lib/apk/db/installed"
},
{
"name": "syft:metadata:installedSize",
"value": "0"
},
{
"name": "syft:metadata:originPackage",
"value": "libc-dev"
},
{
"name": "syft:metadata:size",
"value": "0"
}
]
},
{
"bom-ref": "pkg:deb/debian/libpam-runtime@1.5.2-6%2Bdeb12u1?arch=all&distro=debian-12&package-id=7b4ddc97df57aec4&upstream=pam",
"type": "library",
"name": "libpam-runtime",
"version": "1.5.2-6+deb12u1",
"purl": "pkg:deb/debian/libpam-runtime@1.5.2-6%2Bdeb12u1?arch=all&distro=debian-12&upstream=pam",
"externalReferences": [
{
"url": "pkg:deb/debian/pam@1.5.2-6%2Bdeb12u1?arch=source&distro=debian-12",
"type": "source-distribution"
}
],
"properties": [
{
"name": "syft:package:foundBy",
"value": "dpkg-db-cataloger"
},
{
"name": "syft:package:type",
"value": "deb"
},
{
"name": "syft:package:metadataType",
"value": "dpkg-db-entry"
},
{
"name": "syft:location:0:path",
"value": "/var/lib/dpkg/status"
},
{
"name": "syft:metadata:installedSize",
"value": "0"
},
{
"name": "syft:metadata:source",
"value": "pam"
}
]
}
]
}
-14
View File
@@ -87,20 +87,6 @@ func TestCycloneDxImageEncoder(t *testing.T) {
)
}
func TestCycloneDxSourcePackageEncoder(t *testing.T) {
dir := t.TempDir()
testutil.AssertEncoderAgainstGoldenSnapshot(t,
testutil.EncoderSnapshotTestConfig{
Subject: testutil.SourcePackagesInput(t, dir),
Format: getEncoder(t),
UpdateSnapshot: *updateSnapshot,
PersistRedactionsInSnapshot: true,
IsJSON: false,
Redactor: redactor(dir),
},
)
}
func redactor(values ...string) testutil.Redactor {
return testutil.NewRedactions().
WithValuesRedacted(values...).
@@ -1,121 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.7" serialNumber="redacted" version="1">
<metadata>
<timestamp>redacted</timestamp>
<tools>
<components>
<component type="application">
<author>anchore</author>
<name>syft</name>
<version>v0.42.0-bogus</version>
</component>
</components>
</tools>
<component bom-ref="redacted" type="file">
<name>some/path</name>
</component>
</metadata>
<components>
<component bom-ref="pkg:deb/debian/attr@1%3A2.4.47-2%2Bb1?arch=amd64&amp;distro=debian-12&amp;package-id=5cc0e8979d7f5115&amp;upstream=attr%401%3A2.4.47-2" type="library">
<name>attr</name>
<version>1:2.4.47-2+b1</version>
<purl>pkg:deb/debian/attr@1%3A2.4.47-2%2Bb1?arch=amd64&amp;distro=debian-12&amp;upstream=attr%401%3A2.4.47-2</purl>
<externalReferences>
<reference type="source-distribution">
<url>pkg:deb/debian/attr@1%3A2.4.47-2?arch=source&amp;distro=debian-12</url>
</reference>
</externalReferences>
<properties>
<property name="syft:package:foundBy">dpkg-db-cataloger</property>
<property name="syft:package:type">deb</property>
<property name="syft:package:metadataType">dpkg-db-entry</property>
<property name="syft:location:0:path">/var/lib/dpkg/status</property>
<property name="syft:metadata:installedSize">0</property>
<property name="syft:metadata:source">attr</property>
<property name="syft:metadata:sourceVersion">1:2.4.47-2</property>
</properties>
</component>
<component bom-ref="pkg:rpm/centos/bind-export-libs@9.11.13-3.el8?arch=x86_64&amp;distro=centos-8&amp;epoch=32&amp;package-id=6008fca05ca7b418&amp;upstream=bind-9.11.13-3.el8.src.rpm" type="library">
<name>bind-export-libs</name>
<version>32:9.11.13-3.el8</version>
<purl>pkg:rpm/centos/bind-export-libs@9.11.13-3.el8?arch=x86_64&amp;distro=centos-8&amp;epoch=32&amp;upstream=bind-9.11.13-3.el8.src.rpm</purl>
<externalReferences>
<reference type="source-distribution">
<url>pkg:rpm/centos/bind@9.11.13-3.el8?arch=src&amp;distro=centos-8&amp;epoch=32</url>
</reference>
</externalReferences>
<properties>
<property name="syft:package:foundBy">rpm-db-cataloger</property>
<property name="syft:package:type">rpm</property>
<property name="syft:package:metadataType">rpm-db-entry</property>
<property name="syft:location:0:path">/var/lib/rpm/Packages</property>
<property name="syft:metadata:epoch">32</property>
<property name="syft:metadata:release">3.el8</property>
<property name="syft:metadata:size">0</property>
<property name="syft:metadata:sourceRpm">bind-9.11.13-3.el8.src.rpm</property>
</properties>
</component>
<component bom-ref="pkg:apk/alpine/busybox@1.35.0-r17?arch=x86_64&amp;distro=alpine-3.16.3&amp;package-id=aa74af1190de1c40" type="library">
<name>busybox</name>
<version>1.35.0-r17</version>
<purl>pkg:apk/alpine/busybox@1.35.0-r17?arch=x86_64&amp;distro=alpine-3.16.3</purl>
<properties>
<property name="syft:package:foundBy">apk-db-cataloger</property>
<property name="syft:package:type">apk</property>
<property name="syft:package:metadataType">apk-db-entry</property>
<property name="syft:location:0:path">/lib/apk/db/installed</property>
<property name="syft:metadata:installedSize">0</property>
<property name="syft:metadata:originPackage">busybox</property>
<property name="syft:metadata:size">0</property>
</properties>
</component>
<component bom-ref="pkg:alpm/arch/gcc-libs@13.2.1-3?arch=x86_64&amp;distro=arch-rolling&amp;package-id=58a2fcf749a9a3cc&amp;upstream=gcc" type="library">
<name>gcc-libs</name>
<version>13.2.1-3</version>
<purl>pkg:alpm/arch/gcc-libs@13.2.1-3?arch=x86_64&amp;distro=arch-rolling&amp;upstream=gcc</purl>
<properties>
<property name="syft:package:foundBy">alpm-db-cataloger</property>
<property name="syft:package:type">alpm</property>
<property name="syft:package:metadataType">alpm-db-entry</property>
<property name="syft:location:0:path">/var/lib/pacman/local/gcc-libs-13.2.1-3/desc</property>
<property name="syft:metadata:architecture">x86_64</property>
<property name="syft:metadata:basepackage">gcc</property>
<property name="syft:metadata:package">gcc-libs</property>
<property name="syft:metadata:size">0</property>
<property name="syft:metadata:version">13.2.1-3</property>
</properties>
</component>
<component bom-ref="pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64&amp;distro=alpine-3.16.3&amp;package-id=16a8c7b19fde6f4f&amp;upstream=libc-dev" type="library">
<name>libc-utils</name>
<version>0.7.2-r3</version>
<purl>pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64&amp;distro=alpine-3.16.3&amp;upstream=libc-dev</purl>
<properties>
<property name="syft:package:foundBy">apk-db-cataloger</property>
<property name="syft:package:type">apk</property>
<property name="syft:package:metadataType">apk-db-entry</property>
<property name="syft:location:0:path">/lib/apk/db/installed</property>
<property name="syft:metadata:installedSize">0</property>
<property name="syft:metadata:originPackage">libc-dev</property>
<property name="syft:metadata:size">0</property>
</properties>
</component>
<component bom-ref="pkg:deb/debian/libpam-runtime@1.5.2-6%2Bdeb12u1?arch=all&amp;distro=debian-12&amp;package-id=7b4ddc97df57aec4&amp;upstream=pam" type="library">
<name>libpam-runtime</name>
<version>1.5.2-6+deb12u1</version>
<purl>pkg:deb/debian/libpam-runtime@1.5.2-6%2Bdeb12u1?arch=all&amp;distro=debian-12&amp;upstream=pam</purl>
<externalReferences>
<reference type="source-distribution">
<url>pkg:deb/debian/pam@1.5.2-6%2Bdeb12u1?arch=source&amp;distro=debian-12</url>
</reference>
</externalReferences>
<properties>
<property name="syft:package:foundBy">dpkg-db-cataloger</property>
<property name="syft:package:type">deb</property>
<property name="syft:package:metadataType">dpkg-db-entry</property>
<property name="syft:location:0:path">/var/lib/dpkg/status</property>
<property name="syft:metadata:installedSize">0</property>
<property name="syft:metadata:source">pam</property>
</properties>
</component>
</components>
</bom>
@@ -2,7 +2,6 @@ package cyclonedxutil
import (
"io"
"slices"
"github.com/CycloneDX/cyclonedx-go"
@@ -30,35 +29,9 @@ func NewEncoder(version string, format cyclonedx.BOMFileFormat, pretty bool) (En
func (e Encoder) Encode(writer io.Writer, s sbom.SBOM) error {
bom := cyclonedxhelpers.ToFormatModel(s)
if e.version < cyclonedx.SpecVersion1_6 {
// cyclonedx-go does not downgrade this 1.6+ reference type for older spec versions
// (see https://github.com/CycloneDX/cyclonedx-go/issues/291), so drop it rather than emit an invalid document
dropExternalReferenceType(bom.Components, cyclonedx.ERTypeSourceDistribution)
}
enc := cyclonedx.NewBOMEncoder(writer, e.format)
enc.SetPretty(e.pretty)
enc.SetEscapeHTML(false)
return enc.EncodeVersion(bom, e.version)
}
func dropExternalReferenceType(components *[]cyclonedx.Component, refType cyclonedx.ExternalReferenceType) {
if components == nil {
return
}
for i := range *components {
c := &(*components)[i]
dropExternalReferenceType(c.Components, refType)
if c.ExternalReferences == nil {
continue
}
refs := slices.DeleteFunc(*c.ExternalReferences, func(r cyclonedx.ExternalReference) bool {
return r.Type == refType
})
if len(refs) == 0 {
c.ExternalReferences = nil
continue
}
c.ExternalReferences = &refs
}
}
@@ -7,7 +7,6 @@ import (
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/file"
syftFile "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
@@ -80,162 +79,12 @@ func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference {
}
}
}
if srcRef := encodeSourcePackageExternalReference(p); srcRef != nil {
refs = append(refs, *srcRef)
}
if len(refs) > 0 {
return &refs
}
return nil
}
// encodeSourcePackageExternalReference returns a "source-distribution" reference holding the PURL of the distro
// source package that a binary OS package was built from (e.g. libpam-runtime -> pkg:deb/debian/pam@...?arch=source).
// It is only emitted when the source differs from the binary package (by name or version) and when the package PURL
// is of the type implied by the metadata. The main consumer is vulnerability matching, since distros tend to report
// vulnerabilities against the source package rather than each binary package built from it.
//
// why an external reference and not pedigree: `component.pedigree.ancestors` looks like the obvious spot for "this
// binary came from that source", but pedigree describes code lineage (forks, patched variants, and the patches that
// separate them). A binary built unmodified from its own distro source package is the same code in a different form,
// not a fork, and CycloneDX components are deliberately agnostic to source vs binary form (see the comments from the
// CycloneDX maintainers in https://github.com/CycloneDX/specification/issues/612#issuecomment-2958800363). The
// ancestor slot is better left for the upstream project a distro patched (e.g. Linux-PAM for Debian's pam), which
// syft does not know today.
//
// why "source-distribution": the spec defines it as "the location where the source code distributable can be
// obtained", and a deb/rpm source package is exactly that distributable (a .dsc and tarballs in the Debian source
// pool, a .src.rpm in an SRPMS repo). The value is a PURL (an identifier) rather than a download URL, which the
// schema allows since external reference URLs are URIs of any scheme, and a CycloneDX maintainer confirmed this exact
// shape is valid in https://github.com/CycloneDX/specification/issues/612#issuecomment-5428057874. A first-class
// "source" component type was discussed in that issue and deferred to CycloneDX 2.0.
func encodeSourcePackageExternalReference(p pkg.Package) *cyclonedx.ExternalReference {
if p.PURL == "" {
return nil
}
src := sourcePackageOf(p)
if src == nil {
return nil
}
// the binary PURL is only consulted for the namespace and a small set of qualifiers (e.g. distro), everything
// else describes the binary package and must not be restated as a fact about the source package.
binPurl, err := packageurl.FromString(p.PURL)
if err != nil || binPurl.Type != src.purlType {
return nil
}
qualifiers := map[string]string{}
for _, q := range binPurl.Qualifiers {
if src.keepQualifier(q.Key) {
qualifiers[q.Key] = q.Value
}
}
if src.arch != "" {
qualifiers[pkg.PURLQualifierArch] = src.arch
}
srcPurl := packageurl.NewPackageURL(
src.purlType,
binPurl.Namespace,
src.name,
src.version,
packageurl.QualifiersFromMap(qualifiers),
"",
)
return &cyclonedx.ExternalReference{
Type: cyclonedx.ERTypeSourceDistribution,
URL: srcPurl.ToString(),
}
}
// sourcePackage is the identity of the source package a binary OS package was built from.
type sourcePackage struct {
purlType string
name string
version string
// arch is the ecosystem's source architecture marker ("source" for deb, "src" or "nosrc" for rpm)
arch string
}
func (s sourcePackage) keepQualifier(key string) bool {
switch key {
case pkg.PURLQualifierDistro:
return true
case pkg.PURLQualifierEpoch, pkg.PURLQualifierRpmModularity:
return s.purlType == pkg.RpmPkg.PackageURLType()
}
return false
}
// sourcePackageOf returns the source package for p, or nil when there is none or it is the same as the binary.
//
// only deb and rpm are supported, since those are the ecosystems that publish real source packages with their own
// PURL identity (arch=source / arch=src). apk (origin) and alpm (pkgbase) are intentionally left out: there the
// "source" is a build recipe rather than a published source distributable, and its name is usually also the name of
// a real binary package (e.g. pkg:apk/alpine/libc-dev@0.7.2-r3 is an installable package, not a source archive), so
// a "source-distribution" reference would point at a binary package. Whether those ecosystems have a meaningful
// source PURL is still an open question, and until then the `upstream` PURL qualifier continues to carry that info.
func sourcePackageOf(p pkg.Package) *sourcePackage {
var src sourcePackage
var binName, binVersion string
switch m := p.Metadata.(type) {
case pkg.DpkgDBEntry:
src = dpkgSourcePackage(m)
binName, binVersion = m.Package, m.Version
case pkg.DpkgArchiveEntry:
src = dpkgSourcePackage(pkg.DpkgDBEntry(m))
binName, binVersion = m.Package, m.Version
case pkg.RpmDBEntry:
src = rpmSourcePackage(m.SourceRpm)
binName, binVersion = m.Name, m.Version+"-"+m.Release
case pkg.RpmArchive:
src = rpmSourcePackage(m.SourceRpm)
binName, binVersion = m.Name, m.Version+"-"+m.Release
default:
return nil
}
if src.name == "" || src.version == "" {
return nil
}
if src.name == binName && src.version == binVersion {
return nil
}
return &src
}
func dpkgSourcePackage(entry pkg.DpkgDBEntry) sourcePackage {
version := entry.SourceVersion
if version == "" {
version = entry.Version
}
return sourcePackage{purlType: pkg.DebPkg.PackageURLType(), name: entry.Source, version: version, arch: "source"}
}
// rpmSourcePackage parses a source RPM filename of the form <name>-<version>-<release>.(no)src.rpm.
func rpmSourcePackage(sourceRpm string) sourcePackage {
var arch string
switch {
case strings.HasSuffix(sourceRpm, ".src.rpm"):
arch = "src"
case strings.HasSuffix(sourceRpm, ".nosrc.rpm"):
arch = "nosrc"
default:
return sourcePackage{}
}
nvr := strings.TrimSuffix(sourceRpm, "."+arch+".rpm")
release := strings.LastIndex(nvr, "-")
if release < 0 {
return sourcePackage{}
}
ver := strings.LastIndex(nvr[:release], "-")
if ver < 0 {
return sourcePackage{}
}
return sourcePackage{purlType: pkg.RpmPkg.PackageURLType(), name: nvr[:ver], version: nvr[ver+1:], arch: arch}
}
// supported algorithm in cycloneDX as of 1.4
// "MD5", "SHA-1", "SHA-256", "SHA-384", "SHA-512",
// "SHA3-256", "SHA3-384", "SHA3-512", "BLAKE2b-256", "BLAKE2b-384", "BLAKE2b-512", "BLAKE3"
@@ -144,342 +144,6 @@ func Test_encodeExternalReferences(t *testing.T) {
}
}
func Test_encodeSourcePackage(t *testing.T) {
rpmEpoch := 32
tests := []struct {
name string
input pkg.Package
expected *cyclonedx.ExternalReference
}{
{
name: "no metadata",
input: pkg.Package{},
expected: nil,
},
{
name: "no PURL",
input: pkg.Package{
Name: "libpam-runtime",
Version: "1.5.2-6+deb12u1",
Type: pkg.DebPkg,
Metadata: pkg.DpkgDBEntry{
Package: "libpam-runtime",
Version: "1.5.2-6+deb12u1",
Source: "pam",
},
},
expected: nil,
},
{
// apk origins are build recipes, not published source packages, so they are not supported
name: "from apk is not supported",
input: pkg.Package{
Name: "libc-utils",
Version: "0.7.2-r3",
Type: pkg.ApkPkg,
PURL: "pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64&distro=alpine-3.16.3&upstream=libc-dev",
Metadata: pkg.ApkDBEntry{
Package: "libc-utils",
OriginPackage: "libc-dev",
Version: "0.7.2-r3",
Architecture: "x86_64",
},
},
expected: nil,
},
{
name: "from deb",
input: pkg.Package{
Name: "libpam-runtime",
Version: "1.5.2-6+deb12u1",
Type: pkg.DebPkg,
PURL: "pkg:deb/debian/libpam-runtime@1.5.2-6%2Bdeb12u1?arch=all&distro=debian-12&upstream=pam",
Metadata: pkg.DpkgDBEntry{
Package: "libpam-runtime",
Version: "1.5.2-6+deb12u1",
Source: "pam",
Architecture: "all",
},
},
expected: &cyclonedx.ExternalReference{
Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:deb/debian/pam@1.5.2-6%2Bdeb12u1?arch=source&distro=debian-12",
},
},
{
name: "from deb with source version",
input: pkg.Package{
Name: "attr",
Version: "1:2.4.47-2+b1",
Type: pkg.DebPkg,
PURL: "pkg:deb/debian/attr@1%3A2.4.47-2%2Bb1?arch=amd64&distro=debian-12&upstream=attr%401%3A2.4.47-2",
Metadata: pkg.DpkgDBEntry{
Package: "attr",
Version: "1:2.4.47-2+b1",
Source: "attr",
SourceVersion: "1:2.4.47-2",
Architecture: "amd64",
},
},
expected: &cyclonedx.ExternalReference{
Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:deb/debian/attr@1%3A2.4.47-2?arch=source&distro=debian-12",
},
},
{
name: "from deb with empty source",
input: pkg.Package{
Name: "some-package",
Version: "1.0.0",
Type: pkg.DebPkg,
PURL: "pkg:deb/debian/some-package@1.0.0?arch=all&distro=debian-12",
Metadata: pkg.DpkgDBEntry{
Package: "some-package",
Version: "1.0.0",
Architecture: "all",
},
},
expected: nil,
},
{
name: "from deb archive",
input: pkg.Package{
Name: "libpam-runtime",
Version: "1.5.2-6+deb12u1",
Type: pkg.DebPkg,
PURL: "pkg:deb/libpam-runtime@1.5.2-6%2Bdeb12u1?arch=all&upstream=pam",
Metadata: pkg.DpkgArchiveEntry{
Package: "libpam-runtime",
Version: "1.5.2-6+deb12u1",
Source: "pam",
Architecture: "all",
},
},
expected: &cyclonedx.ExternalReference{
Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:deb/pam@1.5.2-6%2Bdeb12u1?arch=source",
},
},
{
name: "from rpm",
input: pkg.Package{
Name: "bind-export-libs",
Version: "32:9.11.13-3.el8",
Type: pkg.RpmPkg,
PURL: "pkg:rpm/centos/bind-export-libs@9.11.13-3.el8?arch=x86_64&distro=centos-8&epoch=32&upstream=bind-9.11.13-3.el8.src.rpm",
Metadata: pkg.RpmDBEntry{
Name: "bind-export-libs",
Version: "9.11.13",
Release: "3.el8",
Epoch: &rpmEpoch,
Arch: "x86_64",
SourceRpm: "bind-9.11.13-3.el8.src.rpm",
},
},
expected: &cyclonedx.ExternalReference{
Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:rpm/centos/bind@9.11.13-3.el8?arch=src&distro=centos-8&epoch=32",
},
},
{
name: "from rpm with source = bin",
input: pkg.Package{
Name: "some-package",
Version: "32:1.0.0-1.el8",
Type: pkg.RpmPkg,
PURL: "pkg:rpm/centos/some-package@1.0.0-1.el8?arch=x86_64&distro=centos-8&epoch=32&upstream=some-package-1.0.0-1.el8.src.rpm",
Metadata: pkg.RpmDBEntry{
Name: "some-package",
Version: "1.0.0",
Release: "1.el8",
Epoch: &rpmEpoch,
Arch: "x86_64",
SourceRpm: "some-package-1.0.0-1.el8.src.rpm",
},
},
expected: nil,
},
{
name: "from rpm archive",
input: pkg.Package{
Name: "bind-export-libs",
Version: "32:9.11.13-3.el8",
Type: pkg.RpmPkg,
PURL: "pkg:rpm/bind-export-libs@9.11.13-3.el8?arch=x86_64&epoch=32&upstream=bind-9.11.13-3.el8.src.rpm",
Metadata: pkg.RpmArchive{
Name: "bind-export-libs",
Version: "9.11.13",
Release: "3.el8",
Epoch: &rpmEpoch,
Arch: "x86_64",
SourceRpm: "bind-9.11.13-3.el8.src.rpm",
},
},
expected: &cyclonedx.ExternalReference{
Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:rpm/bind@9.11.13-3.el8?arch=src&epoch=32",
},
},
{
// alpm pkgbases are build recipes, not published source packages, so they are not supported
name: "from alpm is not supported",
input: pkg.Package{
Name: "gcc-libs",
Version: "13.2.1-3",
Type: pkg.AlpmPkg,
PURL: "pkg:alpm/arch/gcc-libs@13.2.1-3?arch=x86_64&distro=arch-rolling&upstream=gcc",
Metadata: pkg.AlpmDBEntry{
Package: "gcc-libs",
Version: "13.2.1-3",
BasePackage: "gcc",
Architecture: "x86_64",
},
},
expected: nil,
},
{
name: "from rpm with dashes in the source name",
input: pkg.Package{
Name: "perl-Foo-Bar-libs",
Type: pkg.RpmPkg,
PURL: "pkg:rpm/redhat/perl-Foo-Bar-libs@1.0-1.el8?arch=x86_64&distro=rhel-8",
Metadata: pkg.RpmDBEntry{
Name: "perl-Foo-Bar-libs",
Version: "1.0",
Release: "1.el8",
SourceRpm: "perl-Foo-Bar-1.0-1.el8.src.rpm",
},
},
expected: &cyclonedx.ExternalReference{
Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:rpm/redhat/perl-Foo-Bar@1.0-1.el8?arch=src&distro=rhel-8",
},
},
{
name: "from rpm nosrc",
input: pkg.Package{
Name: "some-firmware",
Type: pkg.RpmPkg,
PURL: "pkg:rpm/redhat/some-firmware@1.0-1.el8?arch=noarch&distro=rhel-8",
Metadata: pkg.RpmDBEntry{
Name: "some-firmware",
Version: "1.0",
Release: "1.el8",
SourceRpm: "firmware-1.0-1.el8.nosrc.rpm",
},
},
expected: &cyclonedx.ExternalReference{
Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:rpm/redhat/firmware@1.0-1.el8?arch=nosrc&distro=rhel-8",
},
},
{
name: "from rpm keeps rpmmod",
input: pkg.Package{
Name: "nodejs-libs",
Type: pkg.RpmPkg,
PURL: "pkg:rpm/redhat/nodejs-libs@18.0-1.el8?arch=x86_64&distro=rhel-8&rpmmod=nodejs%3A18",
Metadata: pkg.RpmDBEntry{
Name: "nodejs-libs",
Version: "18.0",
Release: "1.el8",
SourceRpm: "nodejs-18.0-1.el8.src.rpm",
},
},
expected: &cyclonedx.ExternalReference{
Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:rpm/redhat/nodejs@18.0-1.el8?arch=src&distro=rhel-8&rpmmod=nodejs%3A18",
},
},
{
name: "from rpm with malformed source rpm",
input: pkg.Package{
Name: "gpg-pubkey",
Type: pkg.RpmPkg,
PURL: "pkg:rpm/redhat/gpg-pubkey@1-1?distro=rhel-8",
Metadata: pkg.RpmDBEntry{
Name: "gpg-pubkey",
Version: "1",
Release: "1",
SourceRpm: "(none)",
},
},
expected: nil,
},
{
name: "from rpm with non-source rpm filename",
input: pkg.Package{
Name: "bash",
Type: pkg.RpmPkg,
PURL: "pkg:rpm/redhat/bash@5.1-2?arch=x86_64&distro=rhel-8",
Metadata: pkg.RpmDBEntry{
Name: "bash",
Version: "5.1",
Release: "2",
SourceRpm: "bash-5.1-2.x86_64.rpm",
},
},
expected: nil,
},
{
name: "from rpm with too few dashes",
input: pkg.Package{
Name: "bash",
Type: pkg.RpmPkg,
PURL: "pkg:rpm/redhat/bash@5.1-2?arch=x86_64&distro=rhel-8",
Metadata: pkg.RpmDBEntry{
Name: "bash",
Version: "5.1",
Release: "2",
SourceRpm: "bash-5.1.src.rpm",
},
},
expected: nil,
},
{
name: "binary qualifiers and subpath are not carried over",
input: pkg.Package{
Name: "libssl3",
Type: pkg.DebPkg,
PURL: "pkg:deb/debian/libssl3@3.0.0-1?arch=amd64&distro=debian-12&checksum=sha256:dead&download_url=https://example.com/x.deb&cpes=cpe:2.3:a:x:x:1:*:*:*:*:*:*:*#sub/dir",
Metadata: pkg.DpkgDBEntry{
Package: "libssl3",
Version: "3.0.0-1",
Source: "openssl",
},
},
expected: &cyclonedx.ExternalReference{
Type: cyclonedx.ERTypeSourceDistribution, URL: "pkg:deb/debian/openssl@3.0.0-1?arch=source&distro=debian-12",
},
},
{
name: "PURL type does not match metadata",
input: pkg.Package{
Name: "x",
Type: pkg.DebPkg,
PURL: "pkg:npm/x@1",
Metadata: pkg.DpkgDBEntry{
Package: "x",
Version: "1",
Source: "lodash",
},
},
expected: nil,
},
{
name: "invalid PURL",
input: pkg.Package{
Name: "x",
Type: pkg.DebPkg,
PURL: "not-a-purl",
Metadata: pkg.DpkgDBEntry{
Package: "x",
Version: "1",
Source: "y",
},
},
expected: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, encodeSourcePackageExternalReference(test.input))
})
}
}
func Test_isValidExternalRef(t *testing.T) {
tests := []struct {
name string
@@ -20,6 +20,8 @@ func Test_OriginatorSupplier(t *testing.T) {
pkg.ConanfileEntry{},
pkg.ConaninfoEntry{},
pkg.CondaMetaPackage{},
pkg.CpanDistribution{}, // the author is a PAUSE ID, not a person or organization name
pkg.CpanUnpackedRelease{},
pkg.DartPubspecLockEntry{},
pkg.DartPubspec{},
pkg.DotnetDepsEntry{},
@@ -66,6 +66,8 @@ func SourceInfo(p pkg.Package) string {
answer = "acquired package info from nix store path"
case pkg.Rpkg:
answer = "acquired package info from R-package DESCRIPTION file"
case pkg.CpanPkg:
answer = "acquired package info from CPAN distribution metadata"
case pkg.LuaRocksPkg:
answer = "acquired package info from Rockspec package file"
case pkg.SwiftPkg:
@@ -287,6 +287,14 @@ func Test_SourceInfo(t *testing.T) {
"acquired package info from R-package DESCRIPTION file",
},
},
{
input: pkg.Package{
Type: pkg.CpanPkg,
},
expected: []string{
"acquired package info from CPAN distribution metadata",
},
},
{
input: pkg.Package{
Type: pkg.LuaRocksPkg,
@@ -1,146 +0,0 @@
package testutil
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source/directorysource"
)
func SourcePackagesInput(t testing.TB, dir string) sbom.SBOM {
path := filepath.Join(dir, "some", "path")
require.NoError(t, os.MkdirAll(path, 0755))
src, err := directorysource.New(
directorysource.Config{
Path: path,
Base: dir,
},
)
require.NoError(t, err)
return sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: newSourcePackageCatalog(),
},
Source: src.Describe(),
Descriptor: sbom.Descriptor{
Name: "syft",
Version: "v0.42.0-bogus",
},
}
}
//nolint:funlen
func newSourcePackageCatalog() *pkg.Collection {
epoch := 32
catalog := pkg.NewCollection()
// apk: the origin differs from the package name, but apk is not supported so no source reference is expected
catalog.Add(pkg.Package{
Name: "libc-utils",
Version: "0.7.2-r3",
Type: pkg.ApkPkg,
FoundBy: "apk-db-cataloger",
Locations: file.NewLocationSet(file.NewLocation("/lib/apk/db/installed")),
PURL: "pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64&distro=alpine-3.16.3&upstream=libc-dev",
Metadata: pkg.ApkDBEntry{
Package: "libc-utils",
OriginPackage: "libc-dev",
Version: "0.7.2-r3",
Architecture: "x86_64",
},
})
// apk: negative control -- the package is its own origin
catalog.Add(pkg.Package{
Name: "busybox",
Version: "1.35.0-r17",
Type: pkg.ApkPkg,
FoundBy: "apk-db-cataloger",
Locations: file.NewLocationSet(file.NewLocation("/lib/apk/db/installed")),
PURL: "pkg:apk/alpine/busybox@1.35.0-r17?arch=x86_64&distro=alpine-3.16.3",
Metadata: pkg.ApkDBEntry{
Package: "busybox",
OriginPackage: "busybox",
Version: "1.35.0-r17",
Architecture: "x86_64",
},
})
// deb: source name differs, and the source version is implied by the binary version
catalog.Add(pkg.Package{
Name: "libpam-runtime",
Version: "1.5.2-6+deb12u1",
Type: pkg.DebPkg,
FoundBy: "dpkg-db-cataloger",
Locations: file.NewLocationSet(file.NewLocation("/var/lib/dpkg/status")),
PURL: "pkg:deb/debian/libpam-runtime@1.5.2-6%2Bdeb12u1?arch=all&distro=debian-12&upstream=pam",
Metadata: pkg.DpkgDBEntry{
Package: "libpam-runtime",
Version: "1.5.2-6+deb12u1",
Source: "pam",
Architecture: "all",
},
})
// deb: the source version differs from the binary version
catalog.Add(pkg.Package{
Name: "attr",
Version: "1:2.4.47-2+b1",
Type: pkg.DebPkg,
FoundBy: "dpkg-db-cataloger",
Locations: file.NewLocationSet(file.NewLocation("/var/lib/dpkg/status")),
PURL: "pkg:deb/debian/attr@1%3A2.4.47-2%2Bb1?arch=amd64&distro=debian-12&upstream=attr%401%3A2.4.47-2",
Metadata: pkg.DpkgDBEntry{
Package: "attr",
Version: "1:2.4.47-2+b1",
Source: "attr",
SourceVersion: "1:2.4.47-2",
Architecture: "amd64",
},
})
// rpm: name and version both come out of the source RPM filename, and the epoch is carried over
catalog.Add(pkg.Package{
Name: "bind-export-libs",
Version: "32:9.11.13-3.el8",
Type: pkg.RpmPkg,
FoundBy: "rpm-db-cataloger",
Locations: file.NewLocationSet(file.NewLocation("/var/lib/rpm/Packages")),
PURL: "pkg:rpm/centos/bind-export-libs@9.11.13-3.el8?arch=x86_64&distro=centos-8&epoch=32&upstream=bind-9.11.13-3.el8.src.rpm",
Metadata: pkg.RpmDBEntry{
Name: "bind-export-libs",
Version: "9.11.13",
Release: "3.el8",
Epoch: &epoch,
Arch: "x86_64",
SourceRpm: "bind-9.11.13-3.el8.src.rpm",
},
})
// alpm: the base package differs from the package name, but alpm is not supported so no source reference is expected
catalog.Add(pkg.Package{
Name: "gcc-libs",
Version: "13.2.1-3",
Type: pkg.AlpmPkg,
FoundBy: "alpm-db-cataloger",
Locations: file.NewLocationSet(file.NewLocation("/var/lib/pacman/local/gcc-libs-13.2.1-3/desc")),
PURL: "pkg:alpm/arch/gcc-libs@13.2.1-3?arch=x86_64&distro=arch-rolling&upstream=gcc",
Metadata: pkg.AlpmDBEntry{
Package: "gcc-libs",
Version: "13.2.1-3",
BasePackage: "gcc",
Architecture: "x86_64",
},
})
return catalog
}
+2 -2
View File
@@ -155,10 +155,10 @@ catalogers:
packages:
- class: perl-binary
name: perl
purl: pkg:generic/perl
purl: pkg:cpan/perl
cpes:
- cpe:2.3:a:perl:perl:*:*:*:*:*:*:*:*
type: BinaryPkg
type: CpanPkg
- method: glob
criteria:
- '**/composer*'
@@ -460,8 +460,8 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
expected: pkg.Package{
Name: "perl",
Version: "5.12.5",
Type: "binary",
PURL: "pkg:generic/perl@5.12.5",
Type: "cpan",
PURL: "pkg:cpan/perl@5.12.5",
Locations: locations("perl"),
Metadata: metadata("perl-binary"),
},
@@ -473,8 +473,8 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
expected: pkg.Package{
Name: "perl",
Version: "5.20.0",
Type: "binary",
PURL: "pkg:generic/perl@5.20.0",
Type: "cpan",
PURL: "pkg:cpan/perl@5.20.0",
Locations: locations("perl"),
Metadata: metadata("perl-binary"),
},
@@ -486,8 +486,8 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
expected: pkg.Package{
Name: "perl",
Version: "5.37.8",
Type: "binary",
PURL: "pkg:generic/perl@5.37.8",
Type: "cpan",
PURL: "pkg:cpan/perl@5.37.8",
Locations: locations("perl"),
Metadata: metadata("perl-binary"),
},
@@ -3187,6 +3187,58 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
}
}
// Test_Cataloger_PackageTypes covers what assertPackagesAreEqual deliberately ignores: the package type and CPEs.
// The perl interpreter is reported as cpan so it lines up with the rest of the CPAN ecosystem, everything else
// keeps the default binary type.
func Test_Cataloger_PackageTypes(t *testing.T) {
tests := []struct {
logicalFixture string
wantType pkg.Type
wantPURL string
wantCPEs []string
}{
{
logicalFixture: "perl/5.20.0/linux-amd64",
wantType: pkg.CpanPkg,
wantPURL: "pkg:cpan/perl@5.20.0",
wantCPEs: []string{"cpe:2.3:a:perl:perl:5.20.0:*:*:*:*:*:*:*"},
},
{
logicalFixture: "haproxy/1.5.14/linux-amd64",
wantType: pkg.BinaryPkg,
wantPURL: "pkg:generic/haproxy@1.5.14",
wantCPEs: []string{"cpe:2.3:a:haproxy:haproxy:1.5.14:*:*:*:*:*:*:*"},
},
}
for _, test := range tests {
t.Run(test.logicalFixture, func(t *testing.T) {
c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())
path := testutil.SnippetOrBinary(t, test.logicalFixture, *mustUseOriginalBinaries)
src, err := directorysource.NewFromPath(path)
require.NoError(t, err)
resolver, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err)
packages, _, err := c.Catalog(context.Background(), resolver)
require.NoError(t, err)
require.Len(t, packages, 1)
var cpes []string
for _, c := range packages[0].CPEs {
cpes = append(cpes, c.Attributes.BindToFmtString())
}
assert.Equal(t, test.wantType, packages[0].Type)
assert.Equal(t, test.wantPURL, packages[0].PURL)
assert.Equal(t, test.wantCPEs, cpes)
})
}
}
func TestClassifierCataloger_DefaultClassifiers_NegativeCases(t *testing.T) {
c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())
+3 -1
View File
@@ -5,6 +5,7 @@ import (
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/binutils"
)
@@ -251,8 +252,9 @@ func DefaultClassifiers() []binutils.Classifier {
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)\/usr\/local\/lib\/perl\d\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
Package: "perl",
PURL: mustPURL("pkg:generic/perl@version"),
PURL: mustPURL("pkg:cpan/perl@version"),
CPEs: singleCPE("cpe:2.3:a:perl:perl:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
Type: pkg.CpanPkg,
},
{
Class: "php-composer-binary",
@@ -50,6 +50,17 @@ type Classifier struct {
// CPEs are the specific CPEs we want to include for this binary with updated version information
CPEs []cpe.CPE `json:"cpes"`
// Type optionally overrides the package type reported for matches; when unset pkg.BinaryPkg is used
Type pkg.Type `json:"-"`
}
// PackageType is the package type reported for matches of this classifier, defaulting to pkg.BinaryPkg.
func (cfg Classifier) PackageType() pkg.Type {
if cfg.Type == "" {
return pkg.BinaryPkg
}
return cfg.Type
}
func (cfg Classifier) MarshalJSON() ([]byte, error) {
@@ -35,7 +35,7 @@ func NewClassifierPackage(classifier Classifier, location file.Location, matchMe
Locations: file.NewLocationSet(
location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
),
Type: pkg.BinaryPkg,
Type: classifier.PackageType(),
CPEs: cpes,
FoundBy: catalogerName,
Metadata: pkg.BinarySignature{
@@ -12,6 +12,7 @@ import (
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/directorysource"
)
@@ -102,6 +103,35 @@ func Test_ClassifierCPEs(t *testing.T) {
}
}
func Test_ClassifierPackageType(t *testing.T) {
tests := []struct {
name string
classifier Classifier
want pkg.Type
}{
{
name: "defaults to binary when unset",
classifier: Classifier{Class: "some-binary", Package: "some-app"},
want: pkg.BinaryPkg,
},
{
name: "honors an explicit type",
classifier: Classifier{Class: "perl-binary", Package: "perl", Type: pkg.CpanPkg},
want: pkg.CpanPkg,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.want, test.classifier.PackageType())
p := NewClassifierPackage(test.classifier, file.NewLocation("/usr/bin/app"), map[string]string{"version": "1.2.3"}, "cataloger-name")
require.NotNil(t, p)
assert.Equal(t, test.want, p.Type)
})
}
}
func TestClassifier_MarshalJSON(t *testing.T) {
tests := []struct {
@@ -14,6 +14,7 @@ type filterFn func(cpe cpe.Attributes, p pkg.Package) bool
var cpeFilters = []filterFn{
disallowJiraClientServerMismatch,
disallowTomcatServerCPEForEmbeddedEL,
disallowJenkinsServerCPEForPluginPackage,
disallowJenkinsCPEsNotAssociatedWithJenkins,
disallowNonParseableCPEs,
@@ -71,3 +72,18 @@ func disallowJiraClientServerMismatch(c cpe.Attributes, p pkg.Package) bool {
}
return false
}
// disallowTomcatServerCPEForEmbeddedEL prevents the server-level Tomcat CPE from being
// assigned to the EL-only embedded artifact. Keep its artifact-specific CPE candidates.
func disallowTomcatServerCPEForEmbeddedEL(c cpe.Attributes, p pkg.Package) bool {
if c.Vendor != "apache" || c.Product != "tomcat" {
return false
}
metadata, ok := p.Metadata.(pkg.JavaArchive)
if !ok || metadata.PomProperties == nil {
return false
}
return metadata.PomProperties.GroupID == "org.apache.tomcat.embed" && metadata.PomProperties.ArtifactID == "tomcat-embed-el"
}
@@ -218,11 +218,15 @@ func boundCandidates(candidates []string) []string {
}
func candidateTargetSw(p pkg.Package) []string {
switch p.Type {
case pkg.WordpressPluginPkg:
switch {
case p.Type == pkg.WordpressPluginPkg:
return []string{"wordpress"}
case pkg.RustPkg:
case p.Type == pkg.RustPkg:
return []string{"rust"}
case p.Language == pkg.Perl:
// NVD records CPAN distributions with perl in target_sw, e.g.
// cpe:2.3:a:mojolicious:mojolicious:*:*:*:*:*:perl:*:*
return []string{"perl"}
}
return []string{cpe.Any}
@@ -795,6 +795,25 @@ func TestGeneratePackageCPEs(t *testing.T) {
"cpe:2.3:a:wow_estore:wp_coder:2.5.1:*:*:*:*:wordpress:*:*",
},
},
{
// NVD records CPAN distributions with perl in target_sw
name: "cpan distribution",
p: pkg.Package{
Name: "Mojolicious",
Version: "9.10",
Type: pkg.CpanPkg,
Language: pkg.Perl,
Metadata: pkg.CpanDistribution{
Author: "SRI",
Path: "S/SR/SRI/Mojolicious-9.10.tar.gz",
},
},
expected: []string{
// note: the distribution name keeps its casing here, as it does for every other ecosystem.
// NVD spells the same CPE lowercase (cpe:2.3:a:mojolicious:mojolicious:*:*:*:*:*:perl:*:*)
"cpe:2.3:a:Mojolicious:Mojolicious:9.10:*:*:*:*:perl:*:*",
},
},
{
name: "dotnet deps.json",
p: pkg.Package{
@@ -944,6 +963,39 @@ func TestGeneratePackageCPEs(t *testing.T) {
}
}
func TestTomcatEmbeddedCPEs(t *testing.T) {
packageFor := func(artifactID string) pkg.Package {
return pkg.Package{
Name: artifactID,
Version: "11.0.20",
Language: pkg.Java,
Type: pkg.JavaPkg,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.apache.tomcat.embed",
ArtifactID: artifactID,
},
},
}
}
hasCPE := func(cpes []cpe.CPE, vendor, product string) bool {
for _, generated := range cpes {
if generated.Attributes.Vendor == vendor && generated.Attributes.Product == product {
return true
}
}
return false
}
elCPEs := FromPackageAttributes(packageFor("tomcat-embed-el"))
assert.False(t, hasCPE(elCPEs, "apache", "tomcat"), "EL-only module should not inherit the server CPE")
assert.True(t, hasCPE(elCPEs, "apache", "tomcat-embed-el"), "keep the artifact-specific CPE candidate")
coreCPEs := FromPackageAttributes(packageFor("tomcat-embed-core"))
assert.True(t, hasCPE(coreCPEs, "apache", "tomcat"), "core module should retain the Tomcat server CPE")
}
func TestCandidateProducts(t *testing.T) {
tests := []struct {
name string
+128
View File
@@ -0,0 +1,128 @@
# Cataloger capabilities. See ../README.md for documentation.
catalogers:
- ecosystem: perl # MANUAL
name: perl-cpan-meta-cataloger # AUTO-GENERATED
type: generic # AUTO-GENERATED
source: # AUTO-GENERATED
file: syft/pkg/cataloger/perl/cataloger.go
function: NewCpanMetaCataloger
selectors: # AUTO-GENERATED
- cpan
- declared
- directory
- language
- package
- perl
parsers: # AUTO-GENERATED structure
- function: parseReleaseMeta
detector: # AUTO-GENERATED
method: glob # AUTO-GENERATED
criteria: # AUTO-GENERATED
- '**/META.json'
- '**/META.yml'
metadata_types: # AUTO-GENERATED
- pkg.CpanUnpackedRelease
package_types: # AUTO-GENERATED
- cpan
purl_types: # AUTO-GENERATED
- cpan
json_schema_types: # AUTO-GENERATED
- CpanUnpackedRelease
capabilities: # MANUAL - preserved across regeneration
- name: license
default: true
comment: the x_spdx_expression or license field of an unpacked release's own META.json or META.yml
- name: dependency.depth
default: []
comment: release meta prereqs are not resolved; only installed evidence produces edges
- name: dependency.edges
default: ""
- name: dependency.kinds
default: []
- name: package_manager.files.listing
default: false
- name: package_manager.files.digests
default: false
- name: package_manager.package_integrity_hash
default: false
- ecosystem: perl # MANUAL
name: perl-cpan-installed-cataloger # AUTO-GENERATED
type: generic # AUTO-GENERATED
source: # AUTO-GENERATED
file: syft/pkg/cataloger/perl/cataloger.go
function: NewCpanInstalledCataloger
selectors: # AUTO-GENERATED
- cpan
- directory
- image
- installed
- language
- package
- perl
parsers: # AUTO-GENERATED structure
- function: parsePacklist
detector: # AUTO-GENERATED
method: glob # AUTO-GENERATED
criteria: # AUTO-GENERATED
- '**/auto/**/.packlist'
metadata_types: # AUTO-GENERATED
- pkg.CpanDistribution
package_types: # AUTO-GENERATED
- cpan
purl_types: # AUTO-GENERATED
- cpan
json_schema_types: # AUTO-GENERATED
- CpanDistribution
capabilities: # MANUAL - preserved across regeneration
- name: license
default: false
comment: a packlist carries no metadata beyond the file list
- name: dependency.depth
default: []
- name: dependency.edges
default: ""
- name: dependency.kinds
default: []
- name: package_manager.files.listing
default: true
comment: the packlist is a file list, and it is surfaced through OwnedFiles so ownership relationships are emitted
- name: package_manager.files.digests
default: false
- name: package_manager.package_integrity_hash
default: false
- function: parseInstallJSON
detector: # AUTO-GENERATED
method: glob # AUTO-GENERATED
criteria: # AUTO-GENERATED
- '**/.meta/*/install.json'
metadata_types: # AUTO-GENERATED
- pkg.CpanDistribution
package_types: # AUTO-GENERATED
- cpan
purl_types: # AUTO-GENERATED
- cpan
json_schema_types: # AUTO-GENERATED
- CpanDistribution
capabilities: # MANUAL - preserved across regeneration
- name: license
default: true
comment: the x_spdx_expression or license field of the MYMETA.json written beside install.json
- name: dependency.depth
default:
- direct
- indirect
comment: prereqs are resolved across every cataloged distribution, so the graph is the full installed closure
- name: dependency.edges
default: complete
- name: dependency.kinds
default:
- runtime
- build
comment: develop and test prereqs describe how the distribution was authored, not what a deployed copy needs
- name: package_manager.files.listing
default: false
- name: package_manager.files.digests
default: false
- name: package_manager.package_integrity_hash
default: false
+62
View File
@@ -0,0 +1,62 @@
// Package perl provides Cataloger implementations relating to CPAN distributions within the Perl
// language ecosystem.
//
// Only distribution-level identity is emitted. Three evidence sources are read: install.json under a
// .meta directory, .packlist paired with perllocal.pod, and an unpacked release's own META.json or
// META.yml alongside a MANIFEST.
//
// The coverage boundary that follows from that:
//
// - CPAN-client installs are cataloged wherever they live, including local-lib trees such as an
// application's local/lib/perl5. cpanm, cpm, and carton write install.json only when the
// distribution was resolved from a mirror; a local directory or tarball install (cpanm .,
// cpanm ./Foo-1.0.tar.gz) writes none, and those fall back to the packlist pass. CPAN.pm never
// writes install.json at all.
// - A packlist is written by the ExtUtils::MakeMaker and Module::Build install targets unless
// suppressed with NO_PACKLIST, which is what distro packagers set.
// - A packlist-derived name is the installer's NAME, which is usually but not always the
// distribution name: libwww-perl installs to auto/LWP/.packlist. Resolving those to
// distributions is left to the vulnerability data, which carries the mapping.
// - Perl modules installed from distro packages are not cataloged here. They carry no CPAN-native
// metadata, since packagers suppress it, and they are already reported by the deb, rpm, and apk
// catalogers.
// - Core and dual-life distributions bundled with the interpreter are not cataloged. No on-disk
// artifact carries their versions. The interpreter itself is reported separately.
// - Build leftovers under ~/.cpanm/work and ~/.cpan/build are skipped. They are unpacked release
// tarballs, MANIFEST included, but they describe a copy that is already installed elsewhere in
// the same tree.
// - Vendored .pm trees, App::FatPacker output, and PAR archives are invisible. What they carry is
// module identity, and there is no offline map from a module to the distribution that shipped
// it. Fatpacked output does embed the .pm sources verbatim, $VERSION lines included, so the
// missing fact is the distribution rather than the version.
// - A cpanfile or cpanfile.snapshot is not parsed yet. The snapshot is a resolved lockfile and
// the only evidence in a source checkout or CI workspace, where local/ does not exist, so it
// is deferred rather than out of scope.
package perl
import (
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
// NewCpanInstalledCataloger returns a cataloger for CPAN distributions installed on disk.
// Globs are deliberately unanchored so local-lib trees (carton, cpanm -L) are found wherever they live.
func NewCpanInstalledCataloger() pkg.Cataloger {
return generic.NewCataloger("perl-cpan-installed-cataloger").
WithParserByGlobs(parseInstallJSON, "**/.meta/*/install.json").
WithParserByGlobs(parsePacklist, "**/auto/**/.packlist").
// registration order is execution order, and resolvePacklistVersions has to precede
// mergeDistributions: merging pairs a packlist package to its install.json package only when the
// versions agree or the packlist version is absent, so filling versions in afterwards splits every
// distribution whose two evidence kinds disagree into two packages
WithResolvingProcessors(resolvePacklistVersions).
WithProcessors(mergeDistributions).
WithResolvingProcessors(resolveDependencies)
}
// NewCpanMetaCataloger returns a cataloger for unpacked CPAN releases: release tarballs and vendored
// distribution trees. CPAN client build leftovers are excluded, see isBuildLeftover.
func NewCpanMetaCataloger() pkg.Cataloger {
return generic.NewCataloger("perl-cpan-meta-cataloger").
WithParserByGlobs(parseReleaseMeta, "**/META.json", "**/META.yml")
}
+567
View File
@@ -0,0 +1,567 @@
package perl
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/internal/packagemetadata"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/stereoscopesource"
)
// every fixture here is an image built by a Dockerfile under testdata/, one per evidence tier. The
// installs are performed by the real clients inside the image, so the on-disk layout, the file
// contents and the versions are whatever the toolchain actually produced.
//
// an image is only how the fixture is delivered. perl-cpan-meta-cataloger is registered declared and
// directory, never image, so through the CLI it never runs against a container image at all; the tests
// below that drive it through an image resolver are parser tests, and the file tree they read is what
// they are actually asserting on.
// TestCpanInstalledCataloger_mirrorInstalls covers the only case that produces an install.json:
// cpanm, cpm and carton resolving distributions from a mirror. The image installs libwww-perl and its
// whole dependency closure with cpanm, one distribution with NO_PACKLIST, one with carton into an
// application's local/, and one with cpm into an unrelated prefix.
func TestCpanInstalledCataloger_mirrorInstalls(t *testing.T) {
pkgtest.NewCatalogTester().
WithImageResolver(t, "image-cpan-mirror-installs").
ExpectsPackageStrings([]string{
// preinstalled in the base image by its own EUMM installs: a packlist and a perllocal.pod
// stanza, no .meta, which is why these three have no author qualifier on their purl
"App-cpanminus @ 1.7049 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/App/cpanminus/.packlist)",
"IO-Socket-SSL @ 2.099 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/IO/Socket/SSL/.packlist)",
"Net-SSLeay @ 1.96 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/Net/SSLeay/.packlist)",
// cpm, into a prefix that is not under /usr, which is the whole reason the globs are unanchored
"Capture-Tiny @ 0.48 (/srv/api/local/lib/perl5/x86_64-linux-gnu/.meta/Capture-Tiny-0.48/install.json)",
// carton, into local/lib/perl5 beside the cpanfile
"Text-CSV @ 2.06 (/app/local/lib/perl5/x86_64-linux-gnu/.meta/Text-CSV-2.06/install.json)",
// installed with NO_PACKLIST and NO_PERLLOCAL, the way distro packagers do it, so
// install.json is the only evidence there is
"JSON-PP @ 4.16 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/JSON-PP-4.16/install.json)",
// libwww-perl is reported under its distribution name even though install.json's `name` field
// says LWP and its packlist is auto/LWP/.packlist. There is deliberately no LWP package here:
// reporting one is the bug this image exists to catch.
"libwww-perl @ 6.83 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/libwww-perl-6.83/install.json)",
// the rest of the closure cpanm pulled in for libwww-perl
"Clone @ 0.50 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/Clone-0.50/install.json)",
"Encode-Locale @ 1.05 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/Encode-Locale-1.05/install.json)",
"File-Listing @ 6.16 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/File-Listing-6.16/install.json)",
"HTML-Parser @ 3.85 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/HTML-Parser-3.85/install.json)",
"HTML-Tagset @ 3.24 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/HTML-Tagset-3.24/install.json)",
"HTTP-Cookies @ 6.12 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/HTTP-Cookies-6.12/install.json)",
"HTTP-Date @ 6.08 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/HTTP-Date-6.08/install.json)",
"HTTP-Message @ 7.04 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/HTTP-Message-7.04/install.json)",
"HTTP-Negotiate @ 6.01 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/HTTP-Negotiate-6.01/install.json)",
"IO-HTML @ 1.004 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/IO-HTML-1.004/install.json)",
"LWP-MediaTypes @ 6.04 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/LWP-MediaTypes-6.04/install.json)",
"MIME-Base32 @ 1.303 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/MIME-Base32-1.303/install.json)",
"Net-HTTP @ 6.24 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/Net-HTTP-6.24/install.json)",
"TimeDate @ 2.35 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/TimeDate-2.35/install.json)",
"Try-Tiny @ 0.32 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/Try-Tiny-0.32/install.json)",
"URI @ 5.35 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/URI-5.35/install.json)",
"WWW-RobotRules @ 6.03 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/WWW-RobotRules-6.03/install.json)",
// note: no package for the core perl packlist, which has no auto/ segment, and none for the
// distributions perllocal.pod names but that have no packlist of their own
}).
TestCataloger(t, NewCpanInstalledCataloger())
}
// TestCpanInstalledCataloger_prereqRelationships pins the dependency graph read out of the
// MYMETA.json files cpanm left beside each install.json. Prereqs name modules, so each one has to be
// resolved through the provides map of the cataloged set; a prereq that resolves to nothing (a core
// module such as Carp or strict, or a distribution that is simply not installed) is dropped.
//
// Packages are stringed without their locations here, because the full form makes each relationship
// unreadable.
func TestCpanInstalledCataloger_prereqRelationships(t *testing.T) {
pkgtest.NewCatalogTester().
WithImageResolver(t, "image-cpan-mirror-installs").
WithPackageStringer(func(p pkg.Package) string { return p.Name + " @ " + p.Version }).
ExpectsRelationshipStrings([]string{
"Clone @ 0.50 [dependency-of] HTTP-Message @ 7.04",
"Encode-Locale @ 1.05 [dependency-of] HTTP-Message @ 7.04",
"Encode-Locale @ 1.05 [dependency-of] libwww-perl @ 6.83",
"File-Listing @ 6.16 [dependency-of] libwww-perl @ 6.83",
"HTML-Parser @ 3.85 [dependency-of] libwww-perl @ 6.83",
"HTML-Tagset @ 3.24 [dependency-of] HTML-Parser @ 3.85",
"HTTP-Cookies @ 6.12 [dependency-of] libwww-perl @ 6.83",
"HTTP-Date @ 6.08 [dependency-of] File-Listing @ 6.16",
"HTTP-Date @ 6.08 [dependency-of] HTTP-Cookies @ 6.12",
"HTTP-Date @ 6.08 [dependency-of] HTTP-Message @ 7.04",
"HTTP-Date @ 6.08 [dependency-of] libwww-perl @ 6.83",
"HTTP-Message @ 7.04 [dependency-of] HTML-Parser @ 3.85",
"HTTP-Message @ 7.04 [dependency-of] HTTP-Cookies @ 6.12",
"HTTP-Message @ 7.04 [dependency-of] HTTP-Negotiate @ 6.01",
"HTTP-Message @ 7.04 [dependency-of] libwww-perl @ 6.83",
"HTTP-Negotiate @ 6.01 [dependency-of] libwww-perl @ 6.83",
"IO-HTML @ 1.004 [dependency-of] HTTP-Message @ 7.04",
"LWP-MediaTypes @ 6.04 [dependency-of] HTTP-Message @ 7.04",
"LWP-MediaTypes @ 6.04 [dependency-of] libwww-perl @ 6.83",
// URI 5.35 needs MIME::Base32 to build, which is why cpanm installed it at all
"MIME-Base32 @ 1.303 [dependency-of] URI @ 5.35",
"Net-HTTP @ 6.24 [dependency-of] libwww-perl @ 6.83",
// the module TimeDate provides is Date::Format, so this only resolves through the provides map
"TimeDate @ 2.35 [dependency-of] HTTP-Date @ 6.08",
"Try-Tiny @ 0.32 [dependency-of] libwww-perl @ 6.83",
"URI @ 5.35 [dependency-of] HTML-Parser @ 3.85",
"URI @ 5.35 [dependency-of] HTTP-Message @ 7.04",
"URI @ 5.35 [dependency-of] Net-HTTP @ 6.24",
"URI @ 5.35 [dependency-of] WWW-RobotRules @ 6.03",
"URI @ 5.35 [dependency-of] libwww-perl @ 6.83",
"WWW-RobotRules @ 6.03 [dependency-of] libwww-perl @ 6.83",
}).
TestCataloger(t, NewCpanInstalledCataloger())
}
func TestCpanInstalledCataloger_mirrorInstallEvidence(t *testing.T) {
pkgtest.NewCatalogTester().
WithImageResolver(t, "image-cpan-mirror-installs").
ExpectsAssertion(func(t *testing.T, pkgs []pkg.Package, _ []artifact.Relationship) {
byName := make(map[string]pkg.Package)
for _, p := range pkgs {
byName[p.Name] = p
}
uri := byName["URI"]
require.NotEmpty(t, uri.Name)
assert.Equal(t, "pkg:cpan/URI@5.35?author=OALDERS", uri.PURL)
assert.Equal(t, pkg.Perl, uri.Language)
assert.Equal(t, pkg.CpanPkg, uri.Type)
// licenses come from the MYMETA.json beside install.json, and are not duplicated into
// metadata. URI's own x_spdx_expression is what reaches the package, not the perl_5 token
// beside it: the expression is already SPDX, where perl_5 is a CPAN::Meta shortname that
// would have to be translated.
require.Len(t, uri.Licenses.ToSlice(), 1)
assert.Equal(t, "Artistic-1.0-Perl OR GPL-1.0-or-later", uri.Licenses.ToSlice()[0].Value)
// install.json, its sibling MYMETA.json, the packlist and the perllocal.pod that resolved
// the packlist's version are all evidence. The main .pm is not: perllocal.pod answered
// first, so it was never scraped.
assert.ElementsMatch(t, []string{
"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/URI-5.35/install.json",
"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/URI-5.35/MYMETA.json",
"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/URI/.packlist",
"/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/perllocal.pod",
}, realPaths(uri))
// a distribution with no x_spdx_expression falls back to the CPAN::Meta license token
require.Len(t, byName["Capture-Tiny"].Licenses.ToSlice(), 1)
assert.Equal(t, "apache_2_0", byName["Capture-Tiny"].Licenses.ToSlice()[0].Value)
// a packlist-derived package carries no author, so the purl gets no qualifier
assert.Equal(t, "pkg:cpan/IO-Socket-SSL@2.099", byName["IO-Socket-SSL"].PURL)
// libwww-perl's packlist is auto/LWP/.packlist, so it shares no (name, version) key with
// the distribution and is paired through the provides module map instead
assert.NotContains(t, byName, "LWP", "the module-named packlist must not become its own package")
lwp := byName["libwww-perl"]
require.NotEmpty(t, lwp.Name)
assert.Equal(t, "pkg:cpan/libwww-perl@6.83?author=OALDERS", lwp.PURL)
assert.ElementsMatch(t, []string{
"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/libwww-perl-6.83/install.json",
"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/libwww-perl-6.83/MYMETA.json",
"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/LWP/.packlist",
"/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/perllocal.pod",
}, realPaths(lwp))
// carton's packlist points at the application-local copy, not the site_perl one
assert.ElementsMatch(t, []string{
"/app/local/lib/perl5/x86_64-linux-gnu/.meta/Text-CSV-2.06/install.json",
"/app/local/lib/perl5/x86_64-linux-gnu/.meta/Text-CSV-2.06/MYMETA.json",
"/app/local/lib/perl5/x86_64-linux-gnu/auto/Text/CSV/.packlist",
"/app/local/lib/perl5/x86_64-linux-gnu/perllocal.pod",
}, realPaths(byName["Text-CSV"]))
// name provenance: Dist records the distvname install.json was read from, MainModule
// records that the name was inferred from an auto/ path instead. Never both, and a merged
// package keeps install.json's.
metadata := func(name string) pkg.CpanDistribution {
md, ok := byName[name].Metadata.(pkg.CpanDistribution)
require.True(t, ok, name)
return md
}
assert.Equal(t, "libwww-perl-6.83", metadata("libwww-perl").Dist)
assert.Empty(t, metadata("libwww-perl").MainModule)
assert.Equal(t, "IO::Socket::SSL", metadata("IO-Socket-SSL").MainModule)
assert.Empty(t, metadata("IO-Socket-SSL").Dist)
// the paths are reported as the packlist recorded them, not filtered to the ones still on
// disk. IO/Socket/SSL/Utils.pm is deleted in the image on purpose: a path the packlist
// claims and the filesystem lacks means the file was removed or overwritten, which is
// exactly what an SBOM consumer wants to see.
assert.Equal(t, []string{
"/usr/local/lib/perl5/site_perl/5.40.4/IO/Socket/SSL.pm",
"/usr/local/lib/perl5/site_perl/5.40.4/IO/Socket/SSL.pod",
"/usr/local/lib/perl5/site_perl/5.40.4/IO/Socket/SSL/Intercept.pm",
"/usr/local/lib/perl5/site_perl/5.40.4/IO/Socket/SSL/PublicSuffix.pm",
"/usr/local/lib/perl5/site_perl/5.40.4/IO/Socket/SSL/Utils.pm",
}, metadata("IO-Socket-SSL").OwnedFiles())
// the merge has to carry the packlist's file list onto the install.json package that won,
// or a merged distribution would own nothing
assert.Contains(t, metadata("libwww-perl").OwnedFiles(), "/usr/local/lib/perl5/site_perl/5.40.4/LWP.pm")
assert.Contains(t, metadata("libwww-perl").OwnedFiles(), "/usr/local/bin/lwp-request")
// NO_PACKLIST leaves an install.json and nothing else, so there is no file list at all
assert.Empty(t, metadata("JSON-PP").OwnedFiles())
}).
TestCataloger(t, NewCpanInstalledCataloger())
}
// TestCpanInstalledCataloger_packlistOnly covers the installs that write no install.json, which is
// the majority of real images and where the version has to come from perllocal.pod or the installed
// .pm. The image runs ExtUtils::MakeMaker directly, cpanm against local tarballs, and CPAN.pm; the
// Dockerfile asserts that none of them left a .meta directory behind.
func TestCpanInstalledCataloger_packlistOnly(t *testing.T) {
pkgtest.NewCatalogTester().
WithImageResolver(t, "image-cpan-packlist-only").
ExpectsPackageStrings([]string{
"App-cpanminus @ 1.7049 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/App/cpanminus/.packlist)",
// Encode's $VERSION is declared bare and computed with sprintf from an RCS Revision keyword
// inside a BEGIN block, so nothing static can read it. This version exists only because
// perllocal.pod records what EUMM evaluated at build time.
"Encode @ 3.24 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/Encode/.packlist)",
"IO-Socket-SSL @ 2.099 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/IO/Socket/SSL/.packlist)",
// installed with NO_PERLLOCAL, so this one had to be scraped out of JSON/PP.pm
"JSON-PP @ 4.16 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/JSON/PP/.packlist)",
// libwww-perl's EUMM NAME is LWP, so an installed tree only ever yields "LWP". Resolving
// that back to the distribution is left to the vulnerability data, which carries the map.
"LWP @ 5.836 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/LWP/.packlist)",
"Net-SSLeay @ 1.96 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/Net/SSLeay/.packlist)",
// installed at 2.02 and then upgraded to 2.06. perllocal.pod is append-only, so both
// stanzas are still there and the later one has to win.
"Text-CSV @ 2.06 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/Text/CSV/.packlist)",
}).
ExpectsAssertion(func(t *testing.T, pkgs []pkg.Package, _ []artifact.Relationship) {
byName := make(map[string]pkg.Package)
for _, p := range pkgs {
byName[p.Name] = p
}
// the perllocal.pod that supplied the version is recorded as supporting evidence, and no
// .pm was read at all
assert.ElementsMatch(t, []string{
"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/Encode/.packlist",
"/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/perllocal.pod",
}, realPaths(byName["Encode"]))
// NO_PERLLOCAL wrote no stanza, so the .pm named by the packlist is the evidence instead
assert.ElementsMatch(t, []string{
"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/JSON/PP/.packlist",
"/usr/local/lib/perl5/site_perl/5.40.4/JSON/PP.pm",
}, realPaths(byName["JSON-PP"]))
// no install.json anywhere, so no distribution has an author and no purl has a qualifier
for _, p := range pkgs {
assert.NotContains(t, p.PURL, "author=", p.Name)
assert.Empty(t, p.Licenses.ToSlice(), p.Name)
}
}).
TestCataloger(t, NewCpanInstalledCataloger())
}
// TestCpanInstalledCataloger_twoInstallTrees covers an image carrying two perl lib trees, each with
// its own perllocal.pod naming the same module at a different version. Picking the stanza from the
// wrong tree would be silent, so both are pinned.
//
// The same image installs Text-CSV 2.06 from a mirror into both trees, so both hold an install.json
// agreeing on name and version. Those are two installed things and stay two packages: the tree is
// part of what pairs evidence, not the name and version alone.
func TestCpanInstalledCataloger_twoInstallTrees(t *testing.T) {
pkgtest.NewCatalogTester().
WithImageResolver(t, "image-cpan-two-install-trees").
ExpectsPackageStrings([]string{
"App-cpanminus @ 1.7049 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/App/cpanminus/.packlist)",
"IO-Socket-SSL @ 2.099 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/IO/Socket/SSL/.packlist)",
"JSON-PP @ 2.27300 (/opt/app/lib/perl5/x86_64-linux-gnu/auto/JSON/PP/.packlist)",
"JSON-PP @ 4.16 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/JSON/PP/.packlist)",
"Net-SSLeay @ 1.96 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/Net/SSLeay/.packlist)",
"Text-CSV @ 2.06 (/opt/app/lib/perl5/x86_64-linux-gnu/.meta/Text-CSV-2.06/install.json)",
"Text-CSV @ 2.06 (/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/Text-CSV-2.06/install.json)",
}).
ExpectsAssertion(func(t *testing.T, pkgs []pkg.Package, _ []artifact.Relationship) {
byVersion := make(map[string]pkg.Package)
var textCSV []pkg.Package
for _, p := range pkgs {
switch p.Name {
case "JSON-PP":
byVersion[p.Version] = p
case "Text-CSV":
textCSV = append(textCSV, p)
}
}
require.Len(t, byVersion, 2)
// each tree's Text-CSV carries only its own tree's evidence, and within a tree the
// install.json and the packlist still merged into the one package
require.Len(t, textCSV, 2)
byTree := make(map[string][]string)
for _, p := range textCSV {
byTree[installTree(p)] = realPaths(p)
}
require.Len(t, byTree, 2)
assert.ElementsMatch(t, []string{
"/opt/app/lib/perl5/x86_64-linux-gnu/.meta/Text-CSV-2.06/install.json",
"/opt/app/lib/perl5/x86_64-linux-gnu/.meta/Text-CSV-2.06/MYMETA.json",
"/opt/app/lib/perl5/x86_64-linux-gnu/auto/Text/CSV/.packlist",
"/opt/app/lib/perl5/x86_64-linux-gnu/perllocal.pod",
}, byTree["/opt/app/lib/perl5/x86_64-linux-gnu"])
assert.ElementsMatch(t, []string{
"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/Text-CSV-2.06/install.json",
"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/Text-CSV-2.06/MYMETA.json",
"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/Text/CSV/.packlist",
"/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/perllocal.pod",
}, byTree["/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu"])
// each tree's version came from that tree's own perllocal.pod. Both installs are of the
// same module, so without the libdir rule one of these would carry the other's version.
assert.Contains(t, realPaths(byVersion["2.27300"]), "/opt/app/lib/perl5/x86_64-linux-gnu/perllocal.pod")
assert.Contains(t, realPaths(byVersion["4.16"]), "/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/perllocal.pod")
// ExtUtils::Packlist merges an existing packlist for the same module when writing a new
// one, so the local-lib install's packlist genuinely claims the site_perl copy too. The
// file list is reported as written rather than filtered to the prefix.
owned := byVersion["2.27300"].Metadata.(pkg.CpanDistribution).OwnedFiles()
assert.Contains(t, owned, "/opt/app/lib/perl5/JSON/PP.pm")
assert.Contains(t, owned, "/usr/local/lib/perl5/site_perl/5.40.4/JSON/PP.pm")
}).
TestCataloger(t, NewCpanInstalledCataloger())
}
// TestCpanMetaCataloger_unpackedReleases covers release tarballs unpacked but never installed, plus a
// source repository working tree.
func TestCpanMetaCataloger_unpackedReleases(t *testing.T) {
pkgtest.NewCatalogTester().
WithImageResolver(t, "image-cpan-unpacked-releases").
ExpectsPackageStrings([]string{
// CPAN Meta Spec 1.4, which is all that roughly 43% of current releases ship. Genuine EUMM
// 6.56 output: license is a bare string rather than an array and version is an unquoted
// number, both of which the lenient field types absorb.
"Text-CSV @ 1.21 (/opt/releases/Text-CSV-1.21/META.yml)",
// a Dist::Zilla release, which ships dist.ini inside the tarball and lists it in its own
// MANIFEST, so a dist.ini beside a META.json says nothing about whether this is a release.
// It ships both meta files and META.json is reported, once.
"Try-Tiny @ 0.32 (/opt/releases/Try-Tiny-0.32/META.json)",
// nothing from /opt/releases/Parallel-Depend-4.10: its META.yml is real EUMM output that no
// strict YAML parser accepts, because the distribution's multi-line ABSTRACT was written
// straight into `abstract:` with no quoting and no block scalar. That has to skip the
// directory, not fail the scan.
// nothing from /src/libwww-perl either: a committed META.json records the last release and
// drifts from the working tree (6.83 there against 6.84 in lib/LWP.pm on the same commit).
// The absence of a MANIFEST is what rejects it, since dzil generates one at build time.
}).
ExpectsAssertion(func(t *testing.T, pkgs []pkg.Package, _ []artifact.Relationship) {
byName := make(map[string]pkg.Package)
for _, p := range pkgs {
byName[p.Name] = p
}
// Try-Tiny's META.json carries an x_spdx_expression, which is preferred over the
// license token beside it
require.Len(t, byName["Try-Tiny"].Licenses.ToSlice(), 1)
assert.Equal(t, "MIT", byName["Try-Tiny"].Licenses.ToSlice()[0].Value)
// spec 1.4 has no x_spdx_expression, so the bare license string is what is left
require.Len(t, byName["Text-CSV"].Licenses.ToSlice(), 1)
assert.Equal(t, "perl", byName["Text-CSV"].Licenses.ToSlice()[0].Value)
}).
TestCataloger(t, NewCpanMetaCataloger())
}
// TestCpanCatalogers_buildLeftoversAndVendoredRelease covers the ordinary state of any tree that
// installed distributions and did not clean up after itself: cpanm leaves ~/.cpanm/work and CPAN.pm
// leaves ~/.cpan/build, neither by choice of the user.
//
// Both leftovers genuinely are unpacked release tarballs, MANIFEST included, so the sibling-MANIFEST
// gate is working correctly and still admits them. They have to be excluded on the path instead, or
// every distribution in such a tree is reported twice: once from its installed evidence and once from
// the copy the client unpacked to build it. Both catalogers are registered for directory scans, so
// that is where the duplication would be user-visible.
//
// The same fixture carries a release deliberately unpacked under /opt/vendor, which is the other half
// of the path rule and is reported. It is the same distribution and version as the installed copy,
// and it produces a second package rather than merging, for the reason
// TestCpanCatalogers_vendoredReleaseCannotMerge documents.
func TestCpanCatalogers_buildLeftoversAndVendoredRelease(t *testing.T) {
found := catalogBoth(t, "image-cpan-build-leftovers")
var names []string
for _, p := range found {
names = append(names, p.Name+" @ "+p.Version+" ("+p.FoundBy+")")
}
// ElementsMatch rather than Equal: the two HTML-Tagset packages agree on name and version, so
// pkg.Collection.Sorted breaks the tie on package ID, which is a structural hash and not something
// worth pinning
assert.ElementsMatch(t, []string{
"App-cpanminus @ 1.7049 (perl-cpan-installed-cataloger)",
"HTML-Tagset @ 3.24 (perl-cpan-installed-cataloger)",
"HTML-Tagset @ 3.24 (perl-cpan-meta-cataloger)",
"IO-Socket-SSL @ 2.099 (perl-cpan-installed-cataloger)",
"MIME-Base32 @ 1.303 (perl-cpan-installed-cataloger)",
"Net-SSLeay @ 1.96 (perl-cpan-installed-cataloger)",
"URI @ 5.35 (perl-cpan-installed-cataloger)",
}, names, "each installed distribution is reported once, nothing comes from the leftover trees, and the vendored copy is reported separately")
}
// TestCpanCatalogers_vendoredReleaseCannotMerge asserts what happens when a release deliberately
// unpacked outside any build directory sits beside the installed copy of the same distribution and
// version.
//
// The intuitive expectation is one package carrying both locations as evidence. That cannot hold, and
// this test pins the real behavior at two packages rather than asserting the intuition and failing.
//
// Why: the two packages come from two different catalogers, so nothing inside either cataloger can
// pair them. mergeDistributions only ever sees one cataloger's output. The only cross-cataloger merge
// syft has is pkg.Collection.add, at syft/pkg/collection.go:119, which merges on artifact.ID equality
// alone. artifact.ID is a structural hash of the whole Package (syft/pkg/package.go:58, via
// artifact.IDByHash), and Locations is a hashed field: the `hash:"ignore"` tags at
// syft/pkg/package.go:30-51 exempt FoundBy, Language, CPEs, and PURL, but not Locations, Licenses, or
// Metadata. Two packages whose locations differ therefore cannot have the same ID, and two packages
// found at the same location are the same evidence and not this case at all. So "same identity,
// merged locations" is unreachable by construction, for any pair of catalogers, not just these two.
func TestCpanCatalogers_vendoredReleaseCannotMerge(t *testing.T) {
var installed, unpacked pkg.Package
for _, p := range catalogBoth(t, "image-cpan-build-leftovers") {
if p.Name != "HTML-Tagset" {
continue
}
// the two agree on name and version, so the duplication is visible and consistent
assert.Equal(t, "3.24", p.Version)
assert.Equal(t, "pkg:cpan/HTML-Tagset@3.24", p.PURL)
switch p.FoundBy {
case "perl-cpan-installed-cataloger":
installed = p
case "perl-cpan-meta-cataloger":
unpacked = p
}
}
require.NotEmpty(t, installed.Name)
require.NotEmpty(t, unpacked.Name)
assert.ElementsMatch(t, []string{
"/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/auto/HTML/Tagset/.packlist",
"/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/perllocal.pod",
}, realPaths(installed))
assert.ElementsMatch(t, []string{
"/opt/vendor/HTML-Tagset-3.24/META.json",
}, realPaths(unpacked))
// Locations, Licenses and Metadata are the whole of the difference here, and every other differing
// field (FoundBy, PURL, CPEs) is `hash:"ignore"`. Overwriting just those three hashed fields makes
// the IDs equal, which is the direct demonstration of what keeps the two packages apart.
require.NotEqual(t, installed.ID(), unpacked.ID())
assert.Empty(t, installed.Licenses.ToSlice(), "a packlist carries no license")
assert.Equal(t, "artistic_2", unpacked.Licenses.ToSlice()[0].Value, "META.json does, so Licenses differs too")
relocated := unpacked
relocated.Locations = installed.Locations
relocated.Licenses = installed.Licenses
relocated.Metadata = installed.Metadata
relocated.SetID()
assert.Equal(t, installed.ID(), relocated.ID(), "with the hashed fields equalized the IDs match, and only then")
}
// TestCpanCatalogers_coreDistributionsNotCataloged pins the zero coverage of distributions bundled
// inside the interpreter, which the package doc comment states as a gap.
//
// The image is a perl with no CPAN installs at all: Encode and Storable sit in the core lib tree, the
// only packlist is perl's own and has no auto/ segment, and there is no .meta and no distro package.
// Nothing on disk carries a core distribution's version.
//
// Storable.pm does declare a readable $VERSION, so the interesting half is that no package is
// invented from it, and neither is one invented from the perllocal.pod stanzas that name the
// distributions the base image preinstalled into a site_perl this image does not have. The
// interpreter's version is never used as a stopgap either: a distribution name carrying a wrong or
// absent version matches either everything or nothing in an advisory range, which is worse than the
// gap. Closing this needs a generated Module::CoreList table keyed by perl version.
func TestCpanCatalogers_coreDistributionsNotCataloged(t *testing.T) {
assert.Empty(t, catalogBoth(t, "image-perl-core-only"))
}
// TestCpanCatalogers_metadataTypes pins the metadata type each cataloger emits along with the JSON
// name it serializes as. Both are user-visible API: a consumer decides whether a finding is installed
// and loadable or merely unpacked on disk from the type name, and must not have to string match
// FoundBy.
func TestCpanCatalogers_metadataTypes(t *testing.T) {
installed := catalogImage(t, "image-cpan-mirror-installs", NewCpanInstalledCataloger())
unpacked := catalogImage(t, "image-cpan-unpacked-releases", NewCpanMetaCataloger())
require.NotEmpty(t, installed)
for _, p := range installed {
assert.IsType(t, pkg.CpanDistribution{}, p.Metadata, p.Name)
assert.Equal(t, "cpan-distribution", packagemetadata.JSONName(p.Metadata), p.Name)
}
require.NotEmpty(t, unpacked)
for _, p := range unpacked {
assert.IsType(t, pkg.CpanUnpackedRelease{}, p.Metadata, p.Name)
assert.Equal(t, "cpan-unpacked-release", packagemetadata.JSONName(p.Metadata), p.Name)
}
}
// imageResolver resolves an image fixture the way pkgtest's WithImageResolver does, for the tests
// that need the resolver itself rather than the CatalogTester around it.
func imageResolver(t *testing.T, fixture string) file.Resolver {
t.Helper()
img := imagetest.GetFixtureImage(t, "docker-archive", fixture)
resolver, err := stereoscopesource.New(img, stereoscopesource.ImageConfig{Reference: fixture}).
FileResolver(source.SquashedScope)
require.NoError(t, err)
return resolver
}
func catalogImage(t *testing.T, fixture string, cataloger pkg.Cataloger) []pkg.Package {
t.Helper()
pkgs, _, err := cataloger.Catalog(pkgtest.Context(t), imageResolver(t, fixture))
require.NoError(t, err)
return pkgs
}
// catalogBoth runs both catalogers over one image and aggregates into a pkg.Collection, which is what
// the real cataloging pipeline does, so any cross-cataloger merge that production performs happens
// here too.
func catalogBoth(t *testing.T, fixture string) []pkg.Package {
t.Helper()
resolver := imageResolver(t, fixture)
collection := pkg.NewCollection()
for _, cataloger := range []pkg.Cataloger{NewCpanInstalledCataloger(), NewCpanMetaCataloger()} {
pkgs, _, err := cataloger.Catalog(pkgtest.Context(t), resolver)
require.NoError(t, err)
collection.Add(pkgs...)
}
return collection.Sorted()
}
func realPaths(p pkg.Package) []string {
var paths []string
for _, l := range p.Locations.ToSlice() {
paths = append(paths, l.RealPath)
}
return paths
}
+185
View File
@@ -0,0 +1,185 @@
package perl
import (
"encoding/json"
"io"
"path"
"go.yaml.in/yaml/v3"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/file"
)
// cpanMeta models the parts of a CPAN Meta Spec document syft uses. One shape covers all three:
// META.json (spec 2), MYMETA.json (the configure-time rendering of META.json), and META.yml (spec 1.4).
type cpanMeta struct {
Name string `json:"name" yaml:"name"`
Version scalar `json:"version" yaml:"version"`
Abstract string `json:"abstract" yaml:"abstract"`
License stringList `json:"license" yaml:"license"`
Provides map[string]providesEntry `json:"provides" yaml:"provides"`
// SPDX is preferred over License when present: a single SPDX expression the author wrote, rather than
// a CPAN::Meta license token that has to be translated
SPDX string `json:"x_spdx_expression" yaml:"x_spdx_expression"`
// Prereqs is the spec 2 shape: phase -> relation -> module -> version
Prereqs map[string]map[string]map[string]scalar `json:"prereqs" yaml:"prereqs"`
// Requires and BuildRequires are the spec 1.4 shape, which has no phase nesting. develop and test
// have no 1.4 equivalent, which costs nothing because they are excluded anyway.
Requires map[string]scalar `json:"requires" yaml:"requires"`
BuildRequires map[string]scalar `json:"build_requires" yaml:"build_requires"`
}
type providesEntry struct {
Version scalar `json:"version" yaml:"version"`
}
// scalar is a value that CPAN metadata writes as either a quoted string or a bare number. Old
// distributions ship `"version": 1.23` where the spec calls for a string, and a strict string field
// there would fail the whole document rather than one field.
type scalar string
func (s *scalar) UnmarshalJSON(b []byte) error {
raw := string(b)
if raw == "null" {
return nil
}
if len(raw) >= 2 && raw[0] == '"' {
var str string
if err := json.Unmarshal(b, &str); err != nil {
return err
}
*s = scalar(str)
return nil
}
*s = scalar(raw)
return nil
}
// UnmarshalYAML exists for the same reason UnmarshalJSON does, and separately from it: the YAML decoder
// does not honor json.Unmarshaler, so without this a `version: 1.23` in a META.yml would fail the whole
// document. A YAML scalar node's value is already the text as written, so there is nothing to convert.
func (s *scalar) UnmarshalYAML(value *yaml.Node) error {
if value.Tag == "!!null" {
return nil
}
*s = scalar(value.Value)
return nil
}
// stringList is a value that is a list of strings in CPAN Meta Spec 2 but a bare string in spec 1.4.
type stringList []string
func (s *stringList) UnmarshalJSON(b []byte) error {
var many []string
if err := json.Unmarshal(b, &many); err == nil {
*s = many
return nil
}
var one string
if err := json.Unmarshal(b, &one); err == nil {
*s = stringList{one}
}
return nil
}
func (s *stringList) UnmarshalYAML(value *yaml.Node) error {
var many []string
if err := value.Decode(&many); err == nil {
*s = many
return nil
}
var one string
if err := value.Decode(&one); err == nil {
*s = stringList{one}
}
return nil
}
// readMyMeta reads the MYMETA.json that cpanm, cpm, and carton write beside install.json. A missing or
// unreadable MYMETA is not an error: the distribution is still real, it just has fewer facts attached.
func readMyMeta(resolver file.Resolver, installJSONLocation file.Location) (*file.Location, *cpanMeta) {
if resolver == nil {
return nil, nil
}
p := path.Join(path.Dir(installJSONLocation.Path()), "MYMETA.json")
locations, err := resolver.FilesByPath(p)
if err != nil || len(locations) == 0 {
return nil, nil
}
m, err := decodeMeta(resolver, locations[0])
if err != nil {
log.WithFields("path", p, "error", err).Debug("unable to parse CPAN MYMETA.json")
return nil, nil
}
return &locations[0], m
}
func decodeMeta(resolver file.Resolver, location file.Location) (*cpanMeta, error) {
return decode(resolver, location, func(r io.Reader, m *cpanMeta) error {
return json.NewDecoder(r).Decode(m)
})
}
// decodeMetaYAML reads a spec 1.4 META.yml. CPAN META.yml files are rejected by strict YAML parsers often
// enough to matter: pre-6.46 ExtUtils::MakeMaker writes values with no quoting or escaping at all, so a
// multi-line ABSTRACT, a value that opens a quote it never closes, and a value starting with >= (which
// YAML reads as a folded block scalar) all get emitted verbatim. Measured over 454 releases from 2004 to
// 2009, five fail. That is a routine failure, not a defensive one, and the caller degrades on it.
func decodeMetaYAML(resolver file.Resolver, location file.Location) (*cpanMeta, error) {
return decode(resolver, location, func(r io.Reader, m *cpanMeta) error {
return yaml.NewDecoder(r).Decode(m)
})
}
func decode(resolver file.Resolver, location file.Location, into func(io.Reader, *cpanMeta) error) (*cpanMeta, error) {
reader, err := resolver.FileContentsByLocation(location)
if err != nil {
return nil, err
}
defer internal.CloseAndLogError(reader, location.Path())
var m cpanMeta
if err := into(reader, &m); err != nil {
return nil, err
}
return &m, nil
}
// prereqModules returns the module names this distribution requires at runtime and build time.
// develop and test phases are excluded: they describe how the distribution was authored, not what a
// deployed copy of it needs. A spec 1.4 document has no phases at all, so its flat requires and
// build_requires maps feed the same two buckets.
func (m *cpanMeta) prereqModules() []string {
var modules []string
for _, phase := range []string{"runtime", "build"} {
for module := range m.Prereqs[phase]["requires"] {
modules = append(modules, module)
}
}
for _, flat := range []map[string]scalar{m.Requires, m.BuildRequires} {
for module := range flat {
modules = append(modules, module)
}
}
return modules
}
// licenseExpressions returns the license values to record on the package. x_spdx_expression wins when
// present: it is a single SPDX expression, which syft's license parsing understands directly, rather
// than a CPAN license shortname such as perl_5 or artistic_2.
func (m *cpanMeta) licenseExpressions() []string {
if m.SPDX != "" {
return []string{m.SPDX}
}
return m.License
}
+128
View File
@@ -0,0 +1,128 @@
package perl
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.yaml.in/yaml/v3"
)
// yaml.v3 does not honor json.Unmarshaler, so the lenient types that already tolerate both license and
// version shapes in JSON need their own UnmarshalYAML or a spec 1.4 META.yml fails on one field.
func Test_cpanMeta_lenientFieldsInYAML(t *testing.T) {
tests := []struct {
name string
contents string
expectedVersion scalar
expectedLicense stringList
}{
{
// what a real ExtUtils::MakeMaker 6.x META.yml writes: version unquoted, license a bare string
name: "spec 1.4 shapes",
contents: "name: Text-CSV\nversion: 1.21\nlicense: perl\n",
expectedVersion: "1.21",
expectedLicense: stringList{"perl"},
},
{
name: "quoted version and a license list",
contents: "name: URI\nversion: '5.35'\nlicense:\n - perl_5\n - artistic_2\n",
expectedVersion: "5.35",
expectedLicense: stringList{"perl_5", "artistic_2"},
},
{
// a version that would read as a v-string rather than a number
name: "v-string version",
contents: "name: App-cpm\nversion: v1.1.4\n",
expectedVersion: "v1.1.4",
},
{
name: "explicit nulls",
contents: "name: Foo\nversion: ~\nlicense: ~\n",
expectedVersion: "",
expectedLicense: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var m cpanMeta
require.NoError(t, yaml.Unmarshal([]byte(tt.contents), &m))
assert.Equal(t, tt.expectedVersion, m.Version)
assert.Equal(t, tt.expectedLicense, m.License)
})
}
}
func Test_cpanMeta_licenseExpressions(t *testing.T) {
tests := []struct {
name string
contents string
expected []string
}{
{
// an SPDX expression is what syft's license parsing understands directly, where perl_5 is a
// CPAN::Meta token that has to be translated
name: "x_spdx_expression wins",
contents: `{"license":["perl_5"],"x_spdx_expression":"Artistic-1.0-Perl OR GPL-1.0-or-later"}`,
expected: []string{"Artistic-1.0-Perl OR GPL-1.0-or-later"},
},
{
name: "license is used when there is no expression",
contents: `{"license":["perl_5"]}`,
expected: []string{"perl_5"},
},
{
name: "single-string license, as spec 1.4 writes it",
contents: `{"license":"perl"}`,
expected: []string{"perl"},
},
{
name: "neither present",
contents: `{"name":"Foo"}`,
expected: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var m cpanMeta
require.NoError(t, json.Unmarshal([]byte(tt.contents), &m))
assert.Equal(t, tt.expected, m.licenseExpressions())
})
}
}
func Test_cpanMeta_prereqModules(t *testing.T) {
tests := []struct {
name string
contents string
expected []string
}{
{
name: "spec 2 prereqs, keyed by phase",
contents: `{"prereqs":{
"runtime": {"requires": {"MIME::Base64": "2.1"}},
"build": {"requires": {"ExtUtils::MakeMaker": "0"}},
"test": {"requires": {"Test::More": "0.88"}},
"develop": {"requires": {"Pod::Coverage": "0"}}
}}`,
expected: []string{"MIME::Base64", "ExtUtils::MakeMaker"},
},
{
// spec 1.4 has no phase nesting at all, so the two flat maps feed the same two buckets
name: "spec 1.4 flat requirements",
contents: `{"requires":{"IO::Handle":"0"},"build_requires":{"Test::More":"0.47"}}`,
expected: []string{"IO::Handle", "Test::More"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var m cpanMeta
require.NoError(t, json.Unmarshal([]byte(tt.contents), &m))
assert.ElementsMatch(t, tt.expected, m.prereqModules())
})
}
}
+74
View File
@@ -0,0 +1,74 @@
package perl
import (
"sort"
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
// newCpanPackage builds a package for a CPAN distribution. Note that the name and version are passed
// separately rather than living on the metadata struct: they would be verbatim copies of the package
// fields, so they are not duplicated into metadata.
//
// The metadata is an `any` because the two evidence tiers carry different types, and the author is an
// explicit parameter for the same reason: pkg.CpanUnpackedRelease has no PAUSE path, so there is no
// field on it the purl could be built from.
func newCpanPackage(name, version, author string, md any, licenses []pkg.License, locations ...file.Location) pkg.Package {
p := pkg.Package{
Name: name,
Version: version,
PURL: packageURL(name, version, author),
Locations: file.NewLocationSet(locations...),
Licenses: pkg.NewLicenseSet(licenses...),
Language: pkg.Perl,
Type: pkg.CpanPkg,
Metadata: md,
}
p.SetID()
return p
}
// packageURL returns the PURL for a CPAN distribution. The distribution name is used verbatim: CPAN
// names are case sensitive. The author is emitted as a qualifier rather than as the namespace, which
// is what the current purl spec prefers, and is omitted entirely when unknown rather than guessed.
func packageURL(name, version, author string) string {
var qualifiers packageurl.Qualifiers
if author != "" {
qualifiers = append(qualifiers, packageurl.Qualifier{Key: "author", Value: author})
}
return packageurl.NewPackageURL(packageurl.TypeCpan, "", name, version, qualifiers, "").ToString()
}
// authorFromPathname extracts the CPAN author (PAUSE) ID from a PAUSE path such as
// O/OA/OALDERS/URI-5.35.tar.gz. A path with fewer than three segments yields no author rather than a
// partial one.
func authorFromPathname(p string) string {
parts := strings.Split(p, "/")
if len(parts) < 3 {
return ""
}
return parts[2]
}
// modulesFromProvides converts a CPAN metadata `provides` map into a stable, sorted module list.
// The `file` field is dropped: the installed path is mechanical from the module name.
func modulesFromProvides(provides map[string]providesEntry) []pkg.CpanModule {
if len(provides) == 0 {
return nil
}
modules := make([]pkg.CpanModule, 0, len(provides))
for name, entry := range provides {
modules = append(modules, pkg.CpanModule{Name: name, Version: string(entry.Version)})
}
sort.Slice(modules, func(i, j int) bool { return modules[i].Name < modules[j].Name })
return modules
}
+76
View File
@@ -0,0 +1,76 @@
package perl
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_packageURL(t *testing.T) {
tests := []struct {
name string
dist string
version string
author string
want string
}{
{
name: "author available",
dist: "URI",
version: "5.35",
author: "OALDERS",
want: "pkg:cpan/URI@5.35?author=OALDERS",
},
{
name: "no author known, e.g. a packlist-derived package",
dist: "Text-CSV",
version: "2.06",
want: "pkg:cpan/Text-CSV@2.06",
},
{
name: "case is preserved",
dist: "JSON-MaybeXS",
version: "1.004008",
author: "ETHER",
want: "pkg:cpan/JSON-MaybeXS@1.004008?author=ETHER",
},
{
name: "v-prefixed version is preserved",
dist: "CPAN-02Packages-Search",
version: "v1.0.0",
author: "SKAJI",
want: "pkg:cpan/CPAN-02Packages-Search@v1.0.0?author=SKAJI",
},
{
name: "no version",
dist: "IO-Socket-SSL",
want: "pkg:cpan/IO-Socket-SSL",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, packageURL(tt.dist, tt.version, tt.author))
})
}
}
func Test_authorFromPathname(t *testing.T) {
tests := []struct {
pathname string
want string
}{
{pathname: "O/OA/OALDERS/URI-5.35.tar.gz", want: "OALDERS"},
{pathname: "S/SK/SKAJI/CPAN-02Packages-Search-v1.0.0.tar.gz", want: "SKAJI"},
// not enough segments to name an author: better to omit than to guess
{pathname: "OALDERS/URI-5.35.tar.gz", want: ""},
{pathname: "URI-5.35.tar.gz", want: ""},
{pathname: "", want: ""},
}
for _, tt := range tests {
t.Run(tt.pathname, func(t *testing.T) {
assert.Equal(t, tt.want, authorFromPathname(tt.pathname))
})
}
}
@@ -0,0 +1,109 @@
package perl
import (
"context"
"encoding/json"
"path"
"regexp"
"strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
// installJSON is the record cpanm (>= 1.5000), cpm, and carton write for every distribution they install.
type installJSON struct {
// Name is the MODULE name, not the distribution: libwww-perl records "LWP" here, and
// CPAN-02Packages-Search records "CPAN::02Packages::Search". It is never the package name.
Name string `json:"name"`
Dist string `json:"dist"` // distribution with version, e.g. libwww-perl-5.836
Version scalar `json:"version"`
Pathname string `json:"pathname"` // PAUSE path, e.g. O/OA/OALDERS/URI-5.35.tar.gz
Provides map[string]providesEntry `json:"provides"`
}
func parseInstallJSON(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
var doc installJSON
if err := json.NewDecoder(reader).Decode(&doc); err != nil {
log.WithFields("path", reader.Path(), "error", err).Debug("unable to parse CPAN install.json")
return nil, nil, nil
}
name, version := distributionNameVersion(doc)
if name == "" || version == "" {
log.WithFields("path", reader.Path()).Debug("CPAN install.json is missing a distribution name or version")
return nil, nil, nil
}
locations := []file.Location{reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)}
var licenses []pkg.License
if myMetaLocation, myMeta := readMyMeta(resolver, reader.Location); myMeta != nil {
licenses = pkg.NewLicensesFromLocationWithContext(ctx, *myMetaLocation, myMeta.licenseExpressions()...)
locations = append(locations, myMetaLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation))
}
md := pkg.CpanDistribution{
Dist: doc.Dist,
Author: authorFromPathname(doc.Pathname),
Path: doc.Pathname,
Modules: modulesFromProvides(doc.Provides),
}
return []pkg.Package{newCpanPackage(name, version, md.Author, md, licenses, locations...)}, nil, nil
}
// distributionNameVersion resolves the distribution name and version, which every consumer of these
// packages is keyed by. The `name` field cannot be used: it holds the module name, so libwww-perl would
// be reported as LWP and CGI-Session as CGI::Session, which no advisory or index is filed against.
//
// Both come from `dist` rather than trimming `version` off it, because cpanm writes the main module's
// $VERSION into `version` and the release's distvname into `dist`, and the two often disagree:
// Carp-Assert-More-2.9.0 records version 2.009000, and Class-Rebirth-1.003 records 1.000. Trimming fails
// on those, and the resulting version would never pair with the packlist record for the same install.
func distributionNameVersion(doc installJSON) (string, string) {
distvname := doc.Dist
if distvname == "" {
// older cpanm and hand-rolled records omit dist; the PAUSE path basename carries the same
// <Dist>-<Version>.tar.gz shape
distvname = distvnameFromPathname(doc.Pathname)
}
if name, version := splitDistvname(distvname); name != "" {
return name, version
}
// no version suffix to split off, so dist is already the bare name
return distvname, string(doc.Version)
}
// distvnamePattern splits a distvname at the last dash followed by a version, the rule
// CPAN::DistnameInfo applies. A -TRIAL suffix is a maturity flag and not part of either half: MetaCPAN
// files Try-Tiny-0.26-TRIAL as distribution Try-Tiny, version 0.26.
var distvnamePattern = regexp.MustCompile(`^(.+)-(v?[0-9][^-]*)(?:-TRIAL[0-9]*)?$`)
func splitDistvname(distvname string) (string, string) {
match := distvnamePattern.FindStringSubmatch(distvname)
if match == nil {
return "", ""
}
return match[1], match[2]
}
// distvnameFromPathname recovers the distvname from a PAUSE path such as
// G/GA/GAAS/libwww-perl-5.836.tar.gz, which is the only evidence left when `dist` is absent.
func distvnameFromPathname(pathname string) string {
base := path.Base(pathname)
if base == "." || base == "/" {
return ""
}
for _, ext := range []string{".tar.gz", ".tar.bz2", ".tgz", ".zip"} {
base = strings.TrimSuffix(base, ext)
}
return base
}
@@ -0,0 +1,215 @@
package perl
import (
"context"
"encoding/json"
"io"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
// the record shape here is cpanm's, reduced to the fields the parser reads. What matters is that
// `name` is the module (CPAN::02Packages::Search) and must not become the package name, and that the
// version is a v-string, which the split of `dist` has to keep whole rather than treat as three segments.
//
// The byte-level shape of what the clients really write is covered against real files by
// TestInstallJSON_clientFormatting.
const installJSONFixture = `{"target":"CPAN::02Packages::Search","module_name":"CPAN::02Packages::Search",` +
`"version":"v1.0.0","dist":"CPAN-02Packages-Search-v1.0.0","pathname":"S/SK/SKAJI/CPAN-02Packages-Search-v1.0.0.tar.gz",` +
`"provides":{"CPAN::02Packages::Search":{"file":"lib/CPAN/02Packages/Search.pm","version":"v1.0.0"}},` +
`"name":"CPAN::02Packages::Search"}`
func TestParseInstallJSON(t *testing.T) {
const location = "usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux-gnu/.meta/CPAN-02Packages-Search-v1.0.0/install.json"
expected := []pkg.Package{
{
Name: "CPAN-02Packages-Search",
Version: "v1.0.0",
PURL: "pkg:cpan/CPAN-02Packages-Search@v1.0.0?author=SKAJI",
Locations: file.NewLocationSet(file.NewLocation(location)),
Language: pkg.Perl,
Type: pkg.CpanPkg,
Metadata: pkg.CpanDistribution{
Dist: "CPAN-02Packages-Search-v1.0.0",
Author: "SKAJI",
Path: "S/SK/SKAJI/CPAN-02Packages-Search-v1.0.0.tar.gz",
// the provides map, sorted by module name, with per-module versions: a distribution
// built with inherit_version = 0 ships modules at versions other than its own
Modules: []pkg.CpanModule{
{Name: "CPAN::02Packages::Search", Version: "v1.0.0"},
},
},
},
}
// no MYMETA.json is reachable from a single-string resolver, so the package is reported with no
// licenses and no error
pkgtest.NewCatalogTester().
FromString(location, installJSONFixture).
Expects(expected, []artifact.Relationship(nil)).
TestParser(t, parseInstallJSON)
}
// libwww-perl is the canonical name mismatch: `name` is LWP while the distribution every advisory and
// index is filed against is libwww-perl. Reporting LWP here is the bug this guards. The end-to-end
// form of it is TestCpanInstalledCataloger_mirrorInstallEvidence.
func TestParseInstallJSON_distributionNameNotModuleName(t *testing.T) {
const record = `{"name":"LWP","version":"6.83","dist":"libwww-perl-6.83",` +
`"pathname":"O/OA/OALDERS/libwww-perl-6.83.tar.gz",` +
`"provides":{"LWP":{"file":"lib/LWP.pm","version":"6.83"},"LWP::UserAgent":{"file":"lib/LWP/UserAgent.pm","version":"6.83"}}}`
pkgs := parseString(t, "libwww-perl-6.83/install.json", record)
require.Len(t, pkgs, 1)
assert.Equal(t, "libwww-perl", pkgs[0].Name, "the package name must be the distribution, not the LWP module")
assert.Equal(t, "pkg:cpan/libwww-perl@6.83?author=OALDERS", pkgs[0].PURL)
// the module name is not lost, it belongs in the provides map
assert.Equal(t, []string{"LWP", "LWP::UserAgent"}, providedModules(pkgs[0]))
}
// TestInstallJSON_clientFormatting records what was measured about cpm versus cpanm, since the
// question of whether cpm needs its own coverage kept coming up. Both files are read out of the
// image, where cpanm (through carton) and cpm each installed one distribution.
//
// Finding: they are NOT byte-identical. cpanm writes compact JSON with no trailing newline; cpm writes
// canonical JSON, indented three spaces with spaces around the colons and a trailing newline. The
// record itself has the same shape, including `dist` carrying the version suffix, which is worth
// stating because it is the field the distribution name is derived from.
//
// So anything that reads these files by decoding JSON is unaffected by the difference; anything that
// pattern matches the bytes is not.
func TestInstallJSON_clientFormatting(t *testing.T) {
resolver := imageResolver(t, "image-cpan-mirror-installs")
read := func(path string) string {
locations, err := resolver.FilesByPath(path)
require.NoError(t, err)
require.Len(t, locations, 1, path)
reader, err := resolver.FileContentsByLocation(locations[0])
require.NoError(t, err)
defer internal.CloseAndLogError(reader, path)
contents, err := io.ReadAll(reader)
require.NoError(t, err)
return string(contents)
}
// written by cpanm, which carton shells out to
cpanmWritten := read("/app/local/lib/perl5/x86_64-linux-gnu/.meta/Text-CSV-2.06/install.json")
cpmWritten := read("/srv/api/local/lib/perl5/x86_64-linux-gnu/.meta/Capture-Tiny-0.48/install.json")
assert.NotContains(t, cpanmWritten, "\n", "cpanm writes a single compact line")
assert.Contains(t, cpmWritten, "\"dist\" : ", "cpm writes canonical JSON, with spaces around the colons")
assert.Contains(t, cpmWritten, "\n", "and a trailing newline")
var fromCpanm, fromCpm installJSON
require.NoError(t, json.Unmarshal([]byte(cpanmWritten), &fromCpanm))
require.NoError(t, json.Unmarshal([]byte(cpmWritten), &fromCpm))
assert.Equal(t, "Text-CSV-2.06", fromCpanm.Dist)
assert.Equal(t, "Capture-Tiny-0.48", fromCpm.Dist, "cpm does carry the version suffix on dist, just as cpanm does")
}
// cpanm writes the main module's $VERSION into `version` and the release's distvname into `dist`, so the
// two disagree whenever the module is versioned differently from the release. Name and version must both
// come from `dist`, or the name is the whole distvname and the version never pairs with the packlist
// record for the same install. The cases are the ones reported on the PR, from real cpanm installs.
func TestParseInstallJSON_nameAndVersionFromDist(t *testing.T) {
tests := []struct {
name string
record string
wantName string
wantVersion string
}{
{
name: "module version formatted differently from the release",
record: `{"name":"Carp::Assert::More","version":"2.009000","dist":"Carp-Assert-More-2.9.0"}`,
wantName: "Carp-Assert-More",
wantVersion: "2.9.0",
},
{
name: "module version behind the release",
record: `{"name":"Class::Rebirth","version":"1.000","dist":"Class-Rebirth-1.003"}`,
wantName: "Class-Rebirth",
wantVersion: "1.003",
},
{
name: "TRIAL is a maturity flag, not part of the name or version",
record: `{"name":"Try::Tiny","version":"0.26","dist":"Try-Tiny-0.26-TRIAL"}`,
wantName: "Try-Tiny",
wantVersion: "0.26",
},
{
name: "v-string version stays whole",
record: `{"name":"CPAN::02Packages::Search","version":"v1.0.0","dist":"CPAN-02Packages-Search-v1.0.0"}`,
wantName: "CPAN-02Packages-Search",
wantVersion: "v1.0.0",
},
{
name: "no dist falls back to the PAUSE path",
record: `{"name":"LWP","version":"5.836","pathname":"G/GA/GAAS/libwww-perl-5.836.tar.gz"}`,
wantName: "libwww-perl",
wantVersion: "5.836",
},
{
name: "dist with no version suffix keeps the recorded version",
record: `{"name":"URI","version":"5.35","dist":"URI"}`,
wantName: "URI",
wantVersion: "5.35",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pkgs := parseString(t, "install.json", tt.record)
require.Len(t, pkgs, 1)
assert.Equal(t, tt.wantName, pkgs[0].Name)
assert.Equal(t, tt.wantVersion, pkgs[0].Version)
})
}
}
func parseString(t *testing.T, location, content string) []pkg.Package {
t.Helper()
pkgs, _, err := parseInstallJSON(context.Background(), nil, nil,
file.NewLocationReadCloser(file.NewLocation(location), io.NopCloser(strings.NewReader(content))))
require.NoError(t, err)
return pkgs
}
func TestParseInstallJSON_degrades(t *testing.T) {
tests := []struct {
name string
content string
}{
{name: "unparseable json", content: "not json at all\n"},
{
// never fall back to splitting the containing directory name
name: "missing name and version",
content: `{"provides":{"URI":{"file":"lib/URI.pm"}}}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pkgtest.NewCatalogTester().
FromString("URI-5.35/install.json", tt.content).
Expects(nil, nil).
TestParser(t, parseInstallJSON)
})
}
}
+143
View File
@@ -0,0 +1,143 @@
package perl
import (
"bufio"
"context"
"io"
"regexp"
"strings"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
// versionPattern matches the $VERSION assignment perl modules declare, e.g. `our $VERSION = '2.06';`.
// The value is captured as written so that both decimal (1.303) and v-string (v1.1.4) forms survive intact.
//
// Requiring a literal after the `=` is what makes the pattern decline the computed forms, which cannot be
// read statically at all: real Encode.pm is a bare `our $VERSION;` followed by
// `$VERSION = sprintf "%d.%02d", q$Revision: 3.24 $ =~ /(\d+)/g;` inside a BEGIN block. Those come from
// perllocal.pod instead, which records what the installer evaluated.
//
// The optional package prefix covers the fully qualified form older Dist::Zilla wrote,
// `$JSON::PP::VERSION = '2.27300';`, and qv() covers `our $VERSION = qv('1.2.3');`.
var versionPattern = regexp.MustCompile(`^[ \t]*(?:our[ \t]+|my[ \t]+)?\$(?:[A-Za-z_][A-Za-z0-9_]*::)*VERSION[ \t]*=[ \t]*(?:qv\()?['"]?(v?[0-9][0-9._]*)`)
// packageVersionPattern matches a version declared in the package statement itself,
// `package CPAN::02Packages::Search v1.0.0;`, which perl 5.12 and later allow. A distribution using it
// need carry no $VERSION assignment anywhere: real CPAN::02Packages::Search v1.0.0 does not.
var packageVersionPattern = regexp.MustCompile(`^[ \t]*package[ \t]+[A-Za-z_][A-Za-z0-9_:]*[ \t]+(v?[0-9][0-9._]*)[ \t]*[;{]`)
// packlistMetadataSuffix matches the trailing `key=value` metadata that ExtUtils::Packlist::read strips
// from a packlist line, following the `/^(.*?)( \w+=.*)$/` it uses. Cutting at the first space instead
// truncates any real path that contains one, which corrupts both the owned-file list and the main-.pm
// version fallback.
var packlistMetadataSuffix = regexp.MustCompile(` \w+=.*$`)
// parsePacklist catalogs a distribution installed by any ExtUtils::MakeMaker or Module::Build run,
// which covers CPAN.pm (which writes no .meta) and the perl image's own preinstalled distributions.
func parsePacklist(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
name := distNameFromPacklistPath(reader.Path())
if name == "" {
// the perl core packlist has no auto/<Dist> segment; it lists thousands of files and describes
// no single distribution
return nil, nil, nil
}
var files []string
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
// packlist lines are "<path>" or "<path> type=link from=<path>"
if f := strings.TrimSpace(packlistMetadataSuffix.ReplaceAllString(scanner.Text(), "")); f != "" {
files = append(files, f)
}
}
location := reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)
md := pkg.CpanDistribution{MainModule: moduleFromDashedName(name), Files: files}
// the version is left to resolvePacklistVersions, which prefers perllocal.pod and needs the whole
// cataloged set to find it. A distribution with no readable version is still reported, so that it
// stays visible.
return []pkg.Package{newCpanPackage(name, "", "", md, nil, location)}, nil, nil
}
// distNameFromPacklistPath derives the distribution name from the packlist path, e.g.
// .../auto/IO/Socket/SSL/.packlist yields IO-Socket-SSL. A path with no auto/ segment yields "".
func distNameFromPacklistPath(p string) string {
parts := strings.Split(strings.Trim(p, "/"), "/")
last := -1
for i, part := range parts {
if part == "auto" {
last = i
}
}
if last < 0 || last+1 >= len(parts)-1 {
return ""
}
return strings.Join(parts[last+1:len(parts)-1], "-")
}
// mainModuleFile picks the .pm file in the packlist that corresponds to the distribution's main module,
// e.g. Text-CSV is expected to install Text/CSV.pm.
//
// The suffix can match more than one entry, and taking the first is wrong: libwww-perl's packlist lists
// both Bundle/LWP.pm (5.835) and LWP.pm (5.836), with the Bundle copy first. The main module sits at the
// lib root, so the shallowest match is the right one; a nested match is some other module that merely
// ends in the same basename.
func mainModuleFile(dist string, files []string) string {
suffix := "/" + strings.ReplaceAll(dist, "-", "/") + ".pm"
best := ""
bestDepth := 0
for _, f := range files {
if !strings.HasSuffix(f, suffix) {
continue
}
if depth := strings.Count(f, "/"); best == "" || depth < bestDepth {
best, bestDepth = f, depth
}
}
return best
}
func findFile(resolver file.Resolver, p string) (file.Location, bool) {
if resolver == nil || p == "" {
return file.Location{}, false
}
locations, err := resolver.FilesByPath(p)
if err != nil || len(locations) == 0 {
return file.Location{}, false
}
return locations[0], true
}
func readVersionFromPM(resolver file.Resolver, location file.Location) string {
reader, err := resolver.FileContentsByLocation(location)
if err != nil {
return ""
}
defer internal.CloseAndLogError(reader, location.Path())
return versionFromPM(reader)
}
func versionFromPM(r io.Reader) string {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
for _, pattern := range []*regexp.Regexp{versionPattern, packageVersionPattern} {
if match := pattern.FindStringSubmatch(line); match != nil {
return strings.TrimRight(match[1], ".")
}
}
}
return ""
}
@@ -0,0 +1,105 @@
package perl
import (
"context"
"io"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
func Test_distNameFromPacklistPath(t *testing.T) {
tests := []struct {
path string
want string
}{
{path: "/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux/auto/Text/CSV/.packlist", want: "Text-CSV"},
{path: "/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux/auto/IO/Socket/SSL/.packlist", want: "IO-Socket-SSL"},
{path: "auto/URI/.packlist", want: "URI"},
// the perl core install has a packlist with no auto/ segment; it must produce nothing
{path: "/usr/local/lib/perl5/5.40.4/x86_64-linux/.packlist", want: ""},
{path: "/some/auto/.packlist", want: ""},
}
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
assert.Equal(t, tt.want, distNameFromPacklistPath(tt.path))
})
}
}
func Test_mainModuleFile(t *testing.T) {
files := []string{
"/usr/local/lib/perl5/site_perl/5.40.4/Text/CSV_PP.pm",
"/usr/local/lib/perl5/site_perl/5.40.4/Text/CSV.pm",
}
assert.Equal(t, "/usr/local/lib/perl5/site_perl/5.40.4/Text/CSV.pm", mainModuleFile("Text-CSV", files))
assert.Equal(t, "", mainModuleFile("Text-CSV-XS", files))
}
func Test_versionFromPM(t *testing.T) {
tests := []struct {
name string
contents string
want string
}{
{name: "our, single quoted", contents: "package Text::CSV;\n\nour $VERSION = '2.06';\n", want: "2.06"},
// what Text::CSV and JSON::PP really do: use vars, then assign inside a BEGIN block
{name: "assigned inside BEGIN", contents: "use vars qw( $VERSION );\n\nBEGIN {\n $VERSION = '2.06';\n}\n", want: "2.06"},
{name: "bare assignment", contents: "$VERSION = \"1.303\";\n", want: "1.303"},
{name: "unquoted decimal", contents: "our $VERSION = 5.35;\n", want: "5.35"},
{name: "v-string is preserved", contents: "our $VERSION = 'v1.1.4';\n", want: "v1.1.4"},
// the fully qualified form older Dist::Zilla wrote, as real JSON::PP 2.27300 carries
{name: "package qualified", contents: "package JSON::PP;\n\n$JSON::PP::VERSION = '2.27300';\n", want: "2.27300"},
{name: "qv wrapper", contents: "our $VERSION = qv('1.2.3');\n", want: "1.2.3"},
// perl 5.12 and later allow the version in the package statement, and a distribution using it
// need carry no $VERSION at all
{name: "package statement v-string", contents: "package CPAN::02Packages::Search v1.0.0;\nuse v5.24;\n", want: "v1.0.0"},
{name: "package statement decimal", contents: "package Foo::Bar 1.23;\n", want: "1.23"},
{name: "package statement block form", contents: "package Foo::Bar v2.0.1 {\n}\n", want: "v2.0.1"},
// a $VERSION assignment later in the file still wins over a version-less package statement
{name: "package statement without a version", contents: "package Foo::Bar;\nour $VERSION = '1.00';\n", want: "1.00"},
// $XS_VERSION and friends must not be mistaken for $VERSION
{name: "other version variables ignored", contents: "our $XS_VERSION = '9.99';\nour $VERSION = '1.00';\n", want: "1.00"},
// real Encode.pm: declared bare, then computed from an RCS keyword inside BEGIN. Nothing static can
// read that, and guessing at it would be worse than the empty version perllocal.pod then fills in.
{name: "computed from an RCS Revision keyword", contents: "our $VERSION;\nBEGIN {\n $VERSION = sprintf \"%d.%02d\", q$Revision: 3.24 $ =~ /(\\d+)/g;\n}\n", want: ""},
{name: "no version at all", contents: "package Foo;\n1;\n", want: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, versionFromPM(strings.NewReader(tt.contents)))
})
}
}
// Test_parsePacklist_metadataSuffix pins the cut against what ExtUtils::Packlist::read does, which is to
// strip a suffix matching /^(.*?)( \w+=.*)$/ rather than to cut at the first space. A perl tree under a
// directory whose name contains a space is ordinary on macOS and Windows.
func Test_parsePacklist_metadataSuffix(t *testing.T) {
const packlist = `/usr/local/lib/perl5/site_perl/5.40.4/Text/CSV.pm
/Users/me/My Perl/lib/Text/CSV_PP.pm
/usr/local/lib/perl5/site_perl/5.40.4/auto/Text/CSV/CSV.so type=file
/usr/local/bin/csvdiff type=link from=/usr/local/bin/csvdiff-2.06
`
pkgs, _, err := parsePacklist(context.Background(), nil, nil,
file.NewLocationReadCloser(file.NewLocation("usr/local/lib/perl5/site_perl/5.40.4/aarch64-linux-gnu/auto/Text/CSV/.packlist"),
io.NopCloser(strings.NewReader(packlist))))
require.NoError(t, err)
require.Len(t, pkgs, 1)
assert.Equal(t, []string{
"/usr/local/lib/perl5/site_perl/5.40.4/Text/CSV.pm",
"/Users/me/My Perl/lib/Text/CSV_PP.pm",
"/usr/local/lib/perl5/site_perl/5.40.4/auto/Text/CSV/CSV.so",
"/usr/local/bin/csvdiff",
}, pkgs[0].Metadata.(pkg.CpanDistribution).Files)
}
@@ -0,0 +1,100 @@
package perl
import (
"context"
"path"
"strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
// parseReleaseMeta catalogs the single distribution described by the META.json or META.yml at the root of
// an unpacked CPAN distribution: a release tarball or a vendored distribution tree.
//
// Both formats are read because META.json is not the common case. Measured on MetaCPAN's file index over
// 41,722 latest releases, root level only: 23,714 ship a META.json (57%), 37,114 a META.yml (89%), and
// 41,008 a MANIFEST (98%). Reading only META.json skips roughly 43% of current releases, skewed toward the
// old ones where the advisories concentrate.
func parseReleaseMeta(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
if isBuildLeftover(reader.Path()) || !isUnpackedRelease(resolver, reader.Location) {
return nil, nil, nil
}
isYAML := strings.EqualFold(path.Base(reader.Path()), "META.yml")
// where both sit in one directory the META.yml invocation yields nothing. META.json is the spec 2
// document and is what release tooling generates last, so where they disagree it is the newer
// rendering. Skipping at this end keeps the cataloger free of dedup processors.
if isYAML && hasSiblingMetaJSON(resolver, reader.Location) {
return nil, nil, nil
}
decoder := decodeMeta
if isYAML {
decoder = decodeMetaYAML
}
m, err := decoder(resolver, reader.Location)
if err != nil {
// a META.yml a strict parser rejects is routine rather than exceptional, so this skips the
// directory and never fails the scan
log.WithFields("path", reader.Path(), "error", err).Debug("unable to parse CPAN meta file")
return nil, nil, nil
}
if m.Name == "" || m.Version == "" {
log.WithFields("path", reader.Path()).Debug("CPAN meta file is missing a name or version")
return nil, nil, nil
}
location := reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)
licenses := pkg.NewLicensesFromLocationWithContext(ctx, reader.Location, m.licenseExpressions()...)
md := pkg.CpanUnpackedRelease{Modules: modulesFromProvides(m.Provides)}
return []pkg.Package{newCpanPackage(m.Name, string(m.Version), "", md, licenses, location)}, nil, nil
}
// isUnpackedRelease decides whether a meta file describes an unpacked distribution or a source
// repository. A committed META.json records the last release and drifts from the working tree
// (libwww-perl carries 6.83 in META.json and 6.84 in lib/LWP.pm on the same commit), so it must not be
// trusted for version. A sibling MANIFEST is what tells the two apart: release tarballs ship one, while a
// source checkout does not, since the MANIFEST is generated at build time.
func isUnpackedRelease(resolver file.Resolver, metaLocation file.Location) bool {
_, ok := findFile(resolver, path.Join(path.Dir(metaLocation.Path()), "MANIFEST"))
return ok
}
// buildLeftoverDirs are the trees CPAN clients unpack into. A cpanm work directory genuinely is an
// unpacked release tarball, MANIFEST included, so the isUnpackedRelease gate is working correctly and
// still admits it; a path exclusion is the only signal available. What makes it a leftover is that a copy
// of the same distribution was installed elsewhere in the same tree, and the two would be reported as
// separate packages, since two catalogers reporting at different locations can never share a package ID.
// Both catalogers run on directory scans, so that duplication is reachable there.
//
// cpanm leaves ~/.cpanm/work/<epoch>/<Dist>-<Version>/ and CPAN.pm leaves
// ~/.cpan/build/<Dist>-<Version>-XXXX/ with a random suffix, neither of which is cleaned up by default,
// so any tree that did not rm -rf them reported every distribution it installed twice.
//
// The honest cost: an unpacked tarball a user deliberately keeps under ~/.cpan/build stops being reported.
var buildLeftoverDirs = []string{".cpanm/work", ".cpan/build"}
func isBuildLeftover(p string) bool {
normalized := normalizePath(p)
for _, dir := range buildLeftoverDirs {
if strings.Contains(normalized, "/"+dir+"/") {
return true
}
}
return false
}
func hasSiblingMetaJSON(resolver file.Resolver, metaLocation file.Location) bool {
_, ok := findFile(resolver, path.Join(path.Dir(metaLocation.Path()), "META.json"))
return ok
}
@@ -0,0 +1,32 @@
package perl
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_isBuildLeftover(t *testing.T) {
tests := []struct {
path string
want bool
}{
{path: "/root/.cpanm/work/1785183030.2/Clone-0.50/META.json", want: true},
{path: "/root/.cpan/build/Encode-3.24-0/META.json", want: true},
// a directory scan reports paths relative to its root
{path: "root/.cpanm/work/1785183030.2/Clone-0.50/META.yml", want: true},
{path: "/home/app/.cpan/build/Text-CSV-2.06-mQ3Ktb/META.json", want: true},
// a release deliberately unpacked somewhere else is still reported
{path: "/opt/vendor/Text-CSV-2.06/META.json", want: false},
{path: "/src/Encode-3.24/META.json", want: false},
// the client's own directories are not build trees; only work/ and build/ under them are
{path: "/root/.cpanm/latest-build/META.json", want: false},
{path: "/root/.cpan/sources/META.json", want: false},
}
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
assert.Equal(t, tt.want, isBuildLeftover(tt.path))
})
}
}
+224
View File
@@ -0,0 +1,224 @@
package perl
import (
"bufio"
"context"
"io"
"regexp"
"strings"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
// perllocal.pod stanzas look like this, one per install:
//
// =head2 Mon Jul 27 20:10:38 2026: C<Module> L<Net::HTTP|Net::HTTP>
// =item * C<installed into: /usr/local/lib/perl5/site_perl/5.40.4>
// =item * C<VERSION: 6.24>
//
// The date prefix has changed across ExtUtils::MakeMaker versions (localtime became
// gmtime($ENV{SOURCE_DATE_EPOCH} || time) for reproducible builds), so the stanza is recognized by the
// C<Module> marker rather than by anything in the heading. Every EUMM version checked emits =head2, so
// the heading level is not known to vary; keying on the marker just means nothing here depends on it.
var (
perllocalModulePattern = regexp.MustCompile(`C<Module>\s+L<([^|>]+)`)
perllocalInstalledInto = regexp.MustCompile(`C<installed into:\s*([^>]*)>`)
perllocalVersionPattern = regexp.MustCompile(`C<VERSION:\s*([^>]*)>`)
)
// perllocalEntry is one Module stanza of a perllocal.pod, in the order it was written.
type perllocalEntry struct {
Module string // as written, e.g. Net::HTTP
InstalledInto string // the INSTALLSITELIB the installer recorded
Version string
}
// perllocalPod is a parsed perllocal.pod together with where it was found, since the file that supplied
// a version is recorded as supporting evidence on the package.
type perllocalPod struct {
location file.Location
entries []perllocalEntry
}
// resolvePacklistVersions fills in the version of every package that came from a .packlist.
//
// perllocal.pod is the primary source because ExtUtils::MakeMaker writes it and the packlist from the
// same variables in the same install target: the packlist goes to $(SITEARCHEXP)/auto/$(FULLEXT)/.packlist
// while the stanza records "Module" "$(NAME)" with VERSION "$(VERSION)", and FULLEXT is NAME with :: turned
// into /. So the auto/ path segments and the stanza's module name are the same key. $(VERSION) is what EUMM
// evaluated at build time, so a version the module computes rather than declares is recorded correctly,
// which static .pm parsing cannot do at all.
//
// This is a processor rather than a per-packlist read because perllocal.pod is not a sibling of anything:
// EUMM writes it to DESTINSTALLARCHLIB, the core arch directory, even for site installs, so it has to be
// found with a resolver glob. There is one per install tree against many packlists, so it is parsed once
// here rather than once per packlist.
//
// It must run before mergeDistributions. Merging pairs a packlist package to its install.json package only
// when the versions agree or the packlist version is absent, so filling the version in afterwards would
// leave every distribution whose two evidence kinds disagree split into two packages.
func resolvePacklistVersions(_ context.Context, resolver file.Resolver, pkgs []pkg.Package, rels []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) {
if resolver == nil || len(pkgs) == 0 {
return pkgs, rels, err
}
var pods []perllocalPod
for _, location := range perllocalLocations(resolver) {
if entries := readPerllocal(resolver, location); len(entries) > 0 {
pods = append(pods, perllocalPod{location: location, entries: entries})
}
}
for i := range pkgs {
md, ok := pkgs[i].Metadata.(pkg.CpanDistribution)
if !ok || md.MainModule == "" || pkgs[i].Version != "" {
// not a packlist-derived package, so there is nothing to resolve
continue
}
version, evidence := packlistVersion(resolver, pods, pkgs[i], md)
if version == "" && evidence == nil {
// still emitted with an empty version rather than dropped, so the distribution stays visible
continue
}
pkgs[i].Version = version
pkgs[i].PURL = packageURL(pkgs[i].Name, version, md.Author)
if evidence != nil {
locations := pkgs[i].Locations
locations.Add(evidence.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation))
pkgs[i].Locations = locations
}
// Version and Locations both participate in the package ID
pkgs[i].SetID()
}
return pkgs, rels, err
}
// packlistVersion resolves one packlist package's version, preferring perllocal.pod and falling back to
// scraping $VERSION out of the distribution's main .pm. The fallback is not vestigial: NO_PERLLOCAL
// suppresses the file the same way NO_PACKLIST suppresses the packlist, and Module::Build and
// Module::Build::Tiny write packlists but no perllocal.pod at all.
func packlistVersion(resolver file.Resolver, pods []perllocalPod, p pkg.Package, md pkg.CpanDistribution) (string, *file.Location) {
packlistPath := primaryPath(p)
if version, location := perllocalVersion(pods, p.Name, packlistPath); version != "" {
return version, location
}
pmLocation, ok := findFile(resolver, mainModuleFile(p.Name, md.Files))
if !ok {
return "", nil
}
return readVersionFromPM(resolver, pmLocation), &pmLocation
}
// perllocalVersion picks the stanza that describes the packlist at packlistPath, by three rules:
//
// 1. Key on the dashed module name: auto/Net/HTTP/.packlist and "Module Net::HTTP" both give Net-HTTP.
// 2. The stanza's "installed into" libdir must be a path prefix of the packlist, since in every standard
// perl layout the arch directory nests under the lib directory. Where an image carries more than one
// perl, the core libdir of one tree can also prefix-match another tree's packlist, so the longest
// matching libdir wins. Without that a version from the wrong perl silently wins, which is worse than
// no version at all.
// 3. The file is append-only, so an upgrade leaves a stale stanza beside the current one forever. Stanzas
// are chronological, so the last match wins. The dates are not parsed; file order is the ordering.
func perllocalVersion(pods []perllocalPod, dashedName, packlistPath string) (string, *file.Location) {
var (
version string
evidence *file.Location
bestLibdir int
)
for i := range pods {
for _, entry := range pods[i].entries {
if entry.Version == "" || strings.ReplaceAll(entry.Module, "::", "-") != dashedName {
continue
}
libdir := strings.TrimSuffix(entry.InstalledInto, "/")
if libdir == "" || !strings.HasPrefix(normalizePath(packlistPath), normalizePath(libdir)+"/") {
continue
}
// >= rather than > so that the last stanza of an equally specific libdir wins
if len(libdir) >= bestLibdir {
version, evidence, bestLibdir = entry.Version, &pods[i].location, len(libdir)
}
}
}
return version, evidence
}
// normalizePath makes the resolver's relative paths and perllocal's absolute libdirs comparable. A
// directory scan reports paths relative to its root, while perllocal records what the installer saw.
func normalizePath(p string) string {
return "/" + strings.TrimPrefix(p, "/")
}
func perllocalLocations(resolver file.Resolver) []file.Location {
locations, err := resolver.FilesByGlob("**/perllocal.pod")
if err != nil {
log.WithFields("error", err).Debug("unable to search for perllocal.pod")
return nil
}
return locations
}
func readPerllocal(resolver file.Resolver, location file.Location) []perllocalEntry {
reader, err := resolver.FileContentsByLocation(location)
if err != nil {
log.WithFields("path", location.Path(), "error", err).Debug("unable to read perllocal.pod")
return nil
}
defer internal.CloseAndLogError(reader, location.Path())
return parsePerllocal(reader)
}
func parsePerllocal(r io.Reader) []perllocalEntry {
var entries []perllocalEntry
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
if match := perllocalModulePattern.FindStringSubmatch(line); match != nil {
entries = append(entries, perllocalEntry{Module: strings.TrimSpace(match[1])})
continue
}
if len(entries) == 0 {
continue
}
current := &entries[len(entries)-1]
if match := perllocalInstalledInto.FindStringSubmatch(line); match != nil {
current.InstalledInto = strings.TrimSpace(match[1])
}
if match := perllocalVersionPattern.FindStringSubmatch(line); match != nil {
current.Version = strings.TrimSpace(match[1])
}
}
return entries
}
// primaryPath returns the path of the evidence the package was built from, which for a packlist package
// is the .packlist itself.
func primaryPath(p pkg.Package) string {
for _, location := range p.Locations.ToSlice() {
if location.Annotations[pkg.EvidenceAnnotationKey] == pkg.PrimaryEvidenceAnnotation {
return location.Path()
}
}
return ""
}
+170
View File
@@ -0,0 +1,170 @@
package perl
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file"
)
func Test_parsePerllocal(t *testing.T) {
// two stanzas as ExtUtils::MakeMaker writes them, with the second module upgraded in place so both a
// stale entry and a current one are present. Real EUMM emits =head2; the =head1 here is a deliberate
// variation, to pin that the parser keys on the C<Module> marker and not on the heading level.
const pod = `=head0 L<Perl documentation|perl>
=head1 Sun Jul 26 18:04:12 2026: C<Module> L<Encode|Encode>
=over 4
=item *
C<installed into: /usr/local/lib/perl5/site_perl/5.40.4>
=item *
C<LINKTYPE: dynamic>
=item *
C<VERSION: 3.24>
=item *
C<EXE_FILES: bin/enc2xs>
=back
=head2 Mon Jul 27 20:10:38 2026: C<Module> L<Net::HTTP|Net::HTTP>
=over 4
=item *
C<installed into: /usr/local/lib/perl5/site_perl/5.40.4>
=item *
C<VERSION: 6.24>
=back
`
assert.Equal(t, []perllocalEntry{
{Module: "Encode", InstalledInto: "/usr/local/lib/perl5/site_perl/5.40.4", Version: "3.24"},
{Module: "Net::HTTP", InstalledInto: "/usr/local/lib/perl5/site_perl/5.40.4", Version: "6.24"},
}, parsePerllocal(strings.NewReader(pod)))
}
func Test_parsePerllocal_degrades(t *testing.T) {
tests := []struct {
name string
contents string
}{
{name: "empty", contents: ""},
{name: "no module stanzas", contents: "=head0 L<Perl documentation|perl>\n"},
// a stanza body with no C<Module> heading before it has nothing to attach to
{name: "orphaned body", contents: "=item *\n\nC<VERSION: 1.00>\n"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Empty(t, parsePerllocal(strings.NewReader(tt.contents)))
})
}
}
func Test_perllocalVersion(t *testing.T) {
pods := []perllocalPod{
{
location: file.NewLocation("/usr/local/lib/perl5/5.40.4/aarch64-linux-gnu/perllocal.pod"),
entries: []perllocalEntry{
{Module: "Net::HTTP", InstalledInto: "/usr/local/lib/perl5/site_perl/5.40.4", Version: "6.24"},
// the file is append-only, so an upgrade leaves the earlier stanza in place forever
{Module: "Text::CSV", InstalledInto: "/usr/local/lib/perl5/site_perl/5.40.4", Version: "2.02"},
{Module: "Text::CSV", InstalledInto: "/usr/local/lib/perl5/site_perl/5.40.4", Version: "2.06"},
// a stanza whose libdir belongs to another install tree entirely
{Module: "JSON::PP", InstalledInto: "/opt/perl-5.36.0/lib/perl5/site_perl/5.36.0", Version: "2.27300"},
// an INSTALL_BASE install and a local::lib nested inside it both prefix-match one packlist
{Module: "Try::Tiny", InstalledInto: "/opt/app/lib/perl5", Version: "0.30"},
{Module: "Try::Tiny", InstalledInto: "/opt/app/lib/perl5/local", Version: "0.32"},
{Module: "Carp", InstalledInto: "/usr/local/lib/perl5/site_perl/5.40.4"},
},
},
}
tests := []struct {
name string
dashedName string
packlistPath string
want string
}{
{
name: "module name dashes to the packlist name",
dashedName: "Net-HTTP",
packlistPath: "/usr/local/lib/perl5/site_perl/5.40.4/aarch64-linux-gnu/auto/Net/HTTP/.packlist",
want: "6.24",
},
{
name: "the last stanza wins over a stale one",
dashedName: "Text-CSV",
packlistPath: "/usr/local/lib/perl5/site_perl/5.40.4/aarch64-linux-gnu/auto/Text/CSV/.packlist",
want: "2.06",
},
{
// without the libdir rule the other perl's version would silently win, which is worse than
// reporting no version at all
name: "a stanza from another install tree is not used",
dashedName: "JSON-PP",
packlistPath: "/usr/local/lib/perl5/site_perl/5.40.4/aarch64-linux-gnu/auto/JSON/PP/.packlist",
want: "",
},
{
name: "the longest matching libdir wins",
dashedName: "Try-Tiny",
packlistPath: "/opt/app/lib/perl5/local/aarch64-linux-gnu/auto/Try/Tiny/.packlist",
want: "0.32",
},
{
name: "the outer libdir still matches its own packlist",
dashedName: "Try-Tiny",
packlistPath: "/opt/app/lib/perl5/aarch64-linux-gnu/auto/Try/Tiny/.packlist",
want: "0.30",
},
{
// a directory scan reports paths relative to its root while perllocal records absolute libdirs
name: "relative packlist path from a directory scan",
dashedName: "Net-HTTP",
packlistPath: "usr/local/lib/perl5/site_perl/5.40.4/aarch64-linux-gnu/auto/Net/HTTP/.packlist",
want: "6.24",
},
{
name: "no stanza for this distribution",
dashedName: "IO-Socket-SSL",
packlistPath: "/usr/local/lib/perl5/site_perl/5.40.4/aarch64-linux-gnu/auto/IO/Socket/SSL/.packlist",
want: "",
},
{
name: "a stanza with no VERSION is not a match",
dashedName: "Carp",
packlistPath: "/usr/local/lib/perl5/site_perl/5.40.4/aarch64-linux-gnu/auto/Carp/.packlist",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
version, evidence := perllocalVersion(pods, tt.dashedName, tt.packlistPath)
assert.Equal(t, tt.want, version)
if tt.want == "" {
assert.Nil(t, evidence)
return
}
require.NotNil(t, evidence)
assert.Equal(t, "/usr/local/lib/perl5/5.40.4/aarch64-linux-gnu/perllocal.pod", evidence.Path())
})
}
}
+225
View File
@@ -0,0 +1,225 @@
package perl
import (
"context"
"path"
"strings"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/dependency"
)
// mergeDistributions collapses the two kinds of installed evidence into one package per installed
// distribution. A distribution installed by cpanm has both an install.json and a .packlist.
//
// Pairing is scoped to one install tree, so two installs of the same distribution and version into
// separate trees stay two packages. A scan target can hold more than one project tree, and preserving
// that there are two trees is the point: collapsing them asserts one installed thing where there are
// two and loses which tree each belongs to. Multiple projects in one scan target are treated the same
// way elsewhere.
//
// (name, version) alone is not enough to pair them either. A packlist lives at auto/<Module>/.packlist, so its
// name is derived from a MODULE path, while install.json yields the distribution: libwww-perl's packlist
// is auto/LWP/.packlist and produces "LWP" against the distribution "libwww-perl". Those never collide,
// so pairing also consults the `provides` module map, which lists LWP under libwww-perl. Without that
// step every distribution whose main module is named differently from the distribution produces two
// packages, one with the right name and no version and one with the wrong name and the right version,
// and neither one matches an advisory.
func mergeDistributions(pkgs []pkg.Package, rels []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) {
if len(pkgs) < 2 {
return pkgs, rels, err
}
var merged []pkg.Package
indexByKey := make(map[string]int)
// install.json evidence is placed first so that the module index is complete before any packlist
// package needs to resolve against it, independent of the order the parsers ran in
ordered := make([]pkg.Package, 0, len(pkgs))
for _, p := range pkgs {
if fromInstallJSON(p) {
ordered = append(ordered, p)
}
}
for _, p := range pkgs {
if !fromInstallJSON(p) {
ordered = append(ordered, p)
}
}
indexByModule := make(map[string]int)
for _, p := range ordered {
tree := installTree(p)
key := tree + "|" + p.Name + "@" + p.Version
if idx, exists := indexByKey[key]; exists {
merged[idx] = mergeInto(merged[idx], p)
continue
}
// a packlist package names a module, so fold it into whichever distribution in the same tree
// provides it. The version must still agree, or be unreadable: a packlist and an install.json
// in one tree that disagree describe one install that overwrote another's files, and merging
// them would report a single version and hide the other.
if !fromInstallJSON(p) {
if idx, exists := indexByModule[tree+"|"+moduleFromDashedName(p.Name)]; exists &&
(p.Version == "" || p.Version == merged[idx].Version) {
merged[idx] = mergeInto(merged[idx], p)
continue
}
}
indexByKey[key] = len(merged)
for _, m := range providedModules(p) {
// first distribution to claim a module wins; duplicate claims are not expected and
// silently reassigning would make the result order dependent
if _, taken := indexByModule[tree+"|"+m]; !taken {
indexByModule[tree+"|"+m] = len(merged)
}
}
merged = append(merged, p)
}
// locations and metadata drive the package ID, and both may have changed
for i := range merged {
merged[i].SetID()
}
return merged, rels, err
}
// installTree returns the library tree the package's own evidence was found in: the directory holding
// the .meta or auto directory the file sits under. Both kinds of evidence for one install land there,
// in sibling directories rather than the same one, so
// /opt/app/lib/perl5/x86_64-linux-gnu/.meta/Text-CSV-2.06/install.json and
// /opt/app/lib/perl5/x86_64-linux-gnu/auto/Text/CSV/.packlist both yield
// /opt/app/lib/perl5/x86_64-linux-gnu and still pair.
//
// The owned-file list cannot stand in for this: ExtUtils::Packlist merges an existing packlist for the
// same module when it writes a new one, so a local-lib install's packlist legitimately claims files
// living outside its own prefix.
func installTree(p pkg.Package) string {
for _, location := range p.Locations.ToSlice() {
if tree := libRoot(location.Path()); tree != "" {
return tree
}
}
return ""
}
// libRoot cuts a path at the last auto/ or .meta/ segment, which is where a lib tree ends and the
// installer's own bookkeeping begins. A path with neither, such as a perllocal.pod or a scraped .pm,
// yields "": those are supporting evidence and never the only location a package has.
func libRoot(p string) string {
parts := strings.Split(p, "/")
for i := len(parts) - 1; i > 0; i-- {
if parts[i] == "auto" || parts[i] == ".meta" {
return strings.Join(parts[:i], "/")
}
}
return ""
}
// moduleFromDashedName converts a packlist-derived name back into the module it came from, since
// auto/IO/Socket/SSL/.packlist flattens IO::Socket::SSL into IO-Socket-SSL.
func moduleFromDashedName(name string) string {
return strings.ReplaceAll(name, "-", "::")
}
// mergeInto keeps the install.json-derived package, which is the higher fidelity of the two, and folds
// the other's evidence into it.
func mergeInto(a, b pkg.Package) pkg.Package {
winner, loser := a, b
if !fromInstallJSON(a) && fromInstallJSON(b) {
winner, loser = b, a
}
locations := file.NewLocationSet(winner.Locations.ToSlice()...)
locations.Add(loser.Locations.ToSlice()...)
winner.Locations = locations
licenses := pkg.NewLicenseSet(winner.Licenses.ToSlice()...)
licenses.Add(loser.Licenses.ToSlice()...)
winner.Licenses = licenses
// the file list only ever comes from a packlist, so the install.json winner has to take the loser's or
// the merged package would own nothing. OwnedFiles dedupes, which covers a distribution that merged
// more than one packlist.
from, fromOK := loser.Metadata.(pkg.CpanDistribution)
into, intoOK := winner.Metadata.(pkg.CpanDistribution)
if fromOK && intoOK && len(from.Files) > 0 {
into.Files = append(into.Files, from.Files...)
winner.Metadata = into
}
return winner
}
// fromInstallJSON reports whether a package was built from an install.json rather than from a
// .packlist. MainModule is only ever set by the packlist parser, so its absence is the direct signal.
func fromInstallJSON(p pkg.Package) bool {
md, ok := p.Metadata.(pkg.CpanDistribution)
return ok && md.MainModule == ""
}
// resolveDependencies turns MYMETA.json prereqs into relationships. Prereqs name modules while packages
// are keyed by distribution, so each prereq is looked up in the provides map of the cataloged set. A
// prereq that resolves to nothing (a core module such as Carp, strict, or perl, or a distribution that
// simply is not installed) is dropped rather than turned into a placeholder package.
func resolveDependencies(_ context.Context, resolver file.Resolver, pkgs []pkg.Package, rels []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) {
if resolver == nil || len(pkgs) == 0 {
return pkgs, rels, err
}
requires := make(map[artifact.ID][]string)
for i := range pkgs {
if pkgs[i].ID() == "" {
pkgs[i].SetID()
}
requires[pkgs[i].ID()] = prereqModules(resolver, pkgs[i])
}
specifier := func(p pkg.Package) dependency.Specification {
return dependency.Specification{
ProvidesRequires: dependency.ProvidesRequires{
Provides: providedModules(p),
Requires: requires[p.ID()],
},
}
}
return pkgs, append(rels, dependency.Resolve(specifier, pkgs)...), err
}
func providedModules(p pkg.Package) []string {
md, ok := p.Metadata.(pkg.CpanDistribution)
if !ok {
return nil
}
modules := make([]string, 0, len(md.Modules))
for _, m := range md.Modules {
modules = append(modules, m.Name)
}
return modules
}
// prereqModules re-reads the MYMETA.json already recorded as evidence on the package. It is read a
// second time (the parser reads it for licenses) rather than carried on the package, since prereqs
// belong in relationships and not in the metadata struct.
func prereqModules(resolver file.Resolver, p pkg.Package) []string {
for _, location := range p.Locations.ToSlice() {
if path.Base(location.Path()) != "MYMETA.json" {
continue
}
m, err := decodeMeta(resolver, location)
if err != nil {
continue
}
return m.prereqModules()
}
return nil
}
+100
View File
@@ -0,0 +1,100 @@
package perl
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
func Test_mergeDistributions(t *testing.T) {
fromInstallJSON := newCpanPackage("URI", "5.35", "OALDERS",
pkg.CpanDistribution{
Dist: "URI-5.35",
Author: "OALDERS",
Path: "O/OA/OALDERS/URI-5.35.tar.gz",
Modules: []pkg.CpanModule{{Name: "URI", Version: "5.35"}},
},
[]pkg.License{pkg.NewLicenseWithContext(context.Background(), "perl_5")},
file.NewLocation("/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux/.meta/URI-5.35/install.json"),
)
fromPacklist := newCpanPackage("URI", "5.35", "", pkg.CpanDistribution{MainModule: "URI"}, nil,
file.NewLocation("/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux/auto/URI/.packlist"),
)
otherVersion := newCpanPackage("URI", "5.34", "", pkg.CpanDistribution{MainModule: "URI"}, nil,
file.NewLocation("/opt/lib/perl5/x86_64-linux/auto/URI/.packlist"),
)
// the packlist arriving first must not change which package wins
for _, order := range [][]pkg.Package{
{fromInstallJSON, fromPacklist, otherVersion},
{fromPacklist, fromInstallJSON, otherVersion},
} {
merged, _, err := mergeDistributions(order, nil, nil)
require.NoError(t, err)
require.Len(t, merged, 2)
uri := merged[0]
assert.Equal(t, "5.35", uri.Version)
assert.Equal(t, "pkg:cpan/URI@5.35?author=OALDERS", uri.PURL)
assert.Equal(t, "OALDERS", uri.Metadata.(pkg.CpanDistribution).Author)
assert.Len(t, uri.Licenses.ToSlice(), 1)
assert.Len(t, uri.Locations.ToSlice(), 2, "the packlist should be kept as additional evidence")
// a different version of the same distribution is a different package
assert.Equal(t, "5.34", merged[1].Version)
}
}
// a packlist lives at auto/<Module>/.packlist, so for any distribution whose main module is named
// differently from the distribution the two kinds of evidence never share a (name, version) key.
// libwww-perl is the canonical case: auto/LWP/.packlist against the distribution libwww-perl.
func Test_mergeDistributions_packlistNamedAfterModule(t *testing.T) {
fromInstallJSON := newCpanPackage("libwww-perl", "5.836", "GAAS",
pkg.CpanDistribution{
Dist: "libwww-perl-5.836",
Author: "GAAS",
Path: "G/GA/GAAS/libwww-perl-5.836.tar.gz",
Modules: []pkg.CpanModule{{Name: "LWP", Version: "5.836"}, {Name: "LWP::UserAgent", Version: "5.834"}},
},
nil,
file.NewLocation("/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux/.meta/libwww-perl-5.836/install.json"),
)
fromPacklist := newCpanPackage("LWP", "5.836", "", pkg.CpanDistribution{MainModule: "LWP"}, nil,
file.NewLocation("/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux/auto/LWP/.packlist"),
)
// a packlist whose version could not be read still belongs to the distribution providing it
versionless := newCpanPackage("LWP-UserAgent", "", "", pkg.CpanDistribution{MainModule: "LWP::UserAgent"}, nil,
file.NewLocation("/usr/local/lib/perl5/site_perl/5.40.4/x86_64-linux/auto/LWP/UserAgent/.packlist"),
)
// the same module installed into a second lib tree is a second install, and the module index is
// scoped to a tree so that it cannot be claimed by the distribution in the first one
otherTree := newCpanPackage("LWP", "5.836", "", pkg.CpanDistribution{MainModule: "LWP"}, nil,
file.NewLocation("/opt/lib/perl5/x86_64-linux/auto/LWP/.packlist"),
)
for _, order := range [][]pkg.Package{
{fromInstallJSON, fromPacklist, versionless, otherTree},
{otherTree, versionless, fromPacklist, fromInstallJSON},
} {
merged, _, err := mergeDistributions(order, nil, nil)
require.NoError(t, err)
require.Len(t, merged, 2, "the same tree's module-named packlists belong to libwww-perl, the other tree's does not")
assert.Equal(t, "libwww-perl", merged[0].Name)
assert.Equal(t, "pkg:cpan/libwww-perl@5.836?author=GAAS", merged[0].PURL)
assert.Len(t, merged[0].Locations.ToSlice(), 3)
assert.Equal(t, "LWP", merged[1].Name)
assert.Equal(t, []string{"/opt/lib/perl5/x86_64-linux/auto/LWP/.packlist"}, realPaths(merged[1]))
}
}
@@ -0,0 +1,44 @@
# an image that installed distributions and did not clean up after itself, which is the ordinary
# state of anything built with a CPAN client: cpanm leaves ~/.cpanm/work and CPAN.pm leaves
# ~/.cpan/build, neither by choice of the user. Both leftovers genuinely are unpacked release
# tarballs, MANIFEST included, so nothing about their contents disqualifies them and they have to be
# excluded on the path.
#
# A release deliberately unpacked outside any build directory sits beside the installed copy of the
# same distribution and version, so the same image shows both halves of the path rule: the leftover
# is skipped and the vendored tree is not.
#
# The digest is perl:5.40-slim's linux/amd64 manifest; see image-cpan-mirror-installs/Dockerfile.
FROM perl:5.40-slim@sha256:af886c2fe0170cdf0bd83fa8254c7a55da227d5f20988a2e96c836646f96aa7a AS builder
# cpanm, mirror-resolved so these carry install.json, and pinned by PAUSE path so the versions do
# not drift. The unpacked build trees are left in /root/.cpanm/work.
RUN cpanm --notest \
R/RE/REHSACK/MIME-Base32-1.303.tar.gz \
O/OA/OALDERS/URI-5.35.tar.gz
# CPAN.pm, which writes no .meta and leaves its build tree in /root/.cpan/build under a
# randomly suffixed directory
RUN PERL_MM_USE_DEFAULT=1 cpan -T -i P/PE/PETDANCE/HTML-Tagset-3.24.tar.gz
# the same release, unpacked somewhere a user would keep it on purpose. Nothing pairs this with the
# installed copy: the two come from different catalogers, and two packages found at different
# locations can never share a package ID.
RUN mkdir -p /opt/vendor \
&& curl -sSfL https://backpan.perl.org/authors/id/P/PE/PETDANCE/HTML-Tagset-3.24.tar.gz \
| tar xz -C /opt/vendor
# the clients' download caches under ~/.cpanm/sources and ~/.cpan/sources are not evidence of
# anything and are the bulk of the bytes, so only the unpacked build trees come along
RUN mkdir -p /out/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu /out/root/.cpanm /out/root/.cpan /out/opt \
&& cp -a /usr/local/lib/perl5/site_perl /out/usr/local/lib/perl5/site_perl \
&& cp -a /usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/perllocal.pod \
/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/.packlist \
/out/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/ \
&& cp -a /root/.cpanm/work /out/root/.cpanm/work \
&& cp -a /root/.cpan/build /out/root/.cpan/build \
&& cp -a /opt/vendor /out/opt/vendor
FROM scratch
COPY --from=builder /out/ /
@@ -0,0 +1,84 @@
# mirror-resolved CPAN client installs: cpanm, cpm and carton. This is the only case where an
# install.json is written, so it is also the only image carrying MYMETA.json and a `provides` map.
#
# The digest is perl:5.40-slim's linux/amd64 manifest rather than its index, which pins the
# architecture as well as the contents: perl's lib layout embeds the arch name
# (.../5.40.4/x86_64-linux-gnu/), so a build on an arm host would move every path under test.
FROM perl:5.40-slim@sha256:af886c2fe0170cdf0bd83fa8254c7a55da227d5f20988a2e96c836646f96aa7a AS builder
# Clone and HTML::Parser are XS and the slim image ships no compiler
RUN apt-get update \
&& apt-get install -y --no-install-recommends gcc libc6-dev \
&& rm -rf /var/lib/apt/lists/*
# cpm and carton go into a throwaway prefix: their own dependency trees are large and are not under
# test, and nothing from /tools reaches the final image
RUN cpanm --notest --quiet -L /tools App::cpm Carton
# cpanm, site install of libwww-perl and its whole dependency closure.
#
# LWP is the module name; the distribution is libwww-perl, so this is the name mismatch that makes
# install.json's `name` field unusable as a package name, and it installs to auto/LWP/.packlist
# rather than auto/libwww-perl/. The closure comes along on purpose: it is what gives the image real
# MYMETA.json prereqs to resolve into relationships.
#
# Every distribution is named by its PAUSE path, which is what pins the version. A bare module name
# resolves to whatever the index says today, so the closure would drift the moment any of these
# authors releases again. cpanm still treats a PAUSE path as a mirror resolution, so install.json is
# written for all of them, which a URL or a local tarball would not do. Dependencies come first so
# that nothing in the list is ever satisfied by an unpinned fetch.
RUN cpanm --notest \
E/ET/ETHER/Try-Tiny-0.32.tar.gz \
A/AT/ATOOMIC/Clone-0.50.tar.gz \
G/GA/GAAS/Encode-Locale-1.05.tar.gz \
P/PE/PETDANCE/HTML-Tagset-3.24.tar.gz \
C/CJ/CJM/IO-HTML-1.004.tar.gz \
O/OA/OALDERS/LWP-MediaTypes-6.04.tar.gz \
R/RE/REHSACK/MIME-Base32-1.303.tar.gz \
A/AT/ATOOMIC/TimeDate-2.35.tar.gz \
O/OA/OALDERS/URI-5.35.tar.gz \
O/OA/OALDERS/HTTP-Date-6.08.tar.gz \
O/OA/OALDERS/HTTP-Message-7.04.tar.gz \
O/OA/OALDERS/HTML-Parser-3.85.tar.gz \
P/PL/PLICEASE/File-Listing-6.16.tar.gz \
O/OA/OALDERS/HTTP-Cookies-6.12.tar.gz \
G/GA/GAAS/HTTP-Negotiate-6.01.tar.gz \
O/OA/OALDERS/Net-HTTP-6.24.tar.gz \
O/OA/OALDERS/WWW-RobotRules-6.03.tar.gz \
O/OA/OALDERS/libwww-perl-6.83.tar.gz
# a distribution installed the way distro packagers do it: install.json and no packlist at all.
# NO_PACKLIST and NO_PERLLOCAL are ExtUtils::MakeMaker knobs, so this has to be an EUMM
# distribution; a Module::Build::Tiny one would ignore both and write its packlist anyway.
RUN PERL_MM_OPT="NO_PACKLIST=1 NO_PERLLOCAL=1" cpanm --notest I/IS/ISHIGAKI/JSON-PP-4.16.tar.gz
# carton, writing local/lib/perl5 beside the cpanfile it read. The cpanfile and cpanfile.snapshot
# are deliberately left in place and deliberately not parsed by the cataloger.
RUN mkdir -p /app \
&& printf "requires 'Text::CSV', '== 2.06';\n" > /app/cpanfile \
&& cd /app \
&& PERL5LIB=/tools/lib/perl5 PATH=/tools/bin:$PATH carton install
# cpm, into an arbitrary prefix. cpm serializes install.json differently from cpanm (canonical JSON,
# three-space indent, spaces around the colons, trailing newline) and that difference is under test.
RUN PERL5LIB=/tools/lib/perl5 /tools/bin/cpm install -L /srv/api/local --no-test 'Capture::Tiny@0.48'
# a path a packlist claims and the filesystem lacks is what an overwritten or removed file looks
# like, and the owned-file list is expected to report it anyway
RUN rm /usr/local/lib/perl5/site_perl/5.40.4/IO/Socket/SSL/Utils.pm
# assemble only what the catalogers read: the site_perl tree, the core arch directory's
# perllocal.pod and packlist, and the two local-lib trees. The rest of the core lib tree is tens of
# megabytes of interpreter that carries no CPAN evidence.
RUN mkdir -p /out/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu \
&& cp -a /usr/local/lib/perl5/site_perl /out/usr/local/lib/perl5/site_perl \
&& cp -a /usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/perllocal.pod \
/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/.packlist \
/out/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/ \
&& mkdir -p /out/app /out/srv/api \
&& cp -a /app/cpanfile /app/cpanfile.snapshot /app/local /out/app/ \
&& cp -a /srv/api/local /out/srv/api/local
FROM scratch
COPY --from=builder /out/ /
@@ -0,0 +1,69 @@
# installs that leave a .packlist and no install.json, which is the majority of real Perl images and
# where the interesting failures live: the distribution name has to come out of the auto/ path and
# the version out of perllocal.pod or the installed .pm.
#
# Four real installers are exercised, none of which writes a .meta directory:
#
# - ExtUtils::MakeMaker run directly (perl Makefile.PL && make install), which is what distro
# packagers and vendored builds do. It resolves no dependencies at all.
# - cpanm against a local tarball. cpanm's save_meta returns early unless the distribution was
# resolved from a mirror, so a local source writes no install.json.
# - CPAN.pm, which never writes a .meta directory in any circumstance.
# - an EUMM install with NO_PERLLOCAL, which is the case that forces the version to be scraped out
# of the installed .pm.
#
# Old versions are pinned and pinned versions come from backpan: the mirrors drop them and answer
# with an HTML 404 body, which only fails later as "gzip: stdin: not in gzip format".
#
# The digest is perl:5.40-slim's linux/amd64 manifest; see image-cpan-mirror-installs/Dockerfile.
FROM perl:5.40-slim@sha256:af886c2fe0170cdf0bd83fa8254c7a55da227d5f20988a2e96c836646f96aa7a AS builder
# Encode is XS and the slim image ships no compiler
RUN apt-get update \
&& apt-get install -y --no-install-recommends gcc libc6-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /tarballs
RUN curl -sSfLO https://backpan.perl.org/authors/id/G/GA/GAAS/libwww-perl-5.836.tar.gz \
&& curl -sSfLO https://backpan.perl.org/authors/id/I/IS/ISHIGAKI/Text-CSV-2.02.tar.gz \
&& curl -sSfLO https://backpan.perl.org/authors/id/I/IS/ISHIGAKI/Text-CSV-2.06.tar.gz \
&& curl -sSfLO https://backpan.perl.org/authors/id/I/IS/ISHIGAKI/JSON-PP-4.16.tar.gz
# ExtUtils::MakeMaker, run by hand. libwww-perl's EUMM NAME is LWP, so the packlist lands at
# auto/LWP/.packlist and the distribution is only ever recoverable as "LWP" from an installed tree.
# EUMM installs no prerequisites and only warns about the missing ones, which is what keeps this
# image free of the mirror-resolved records those would have written.
RUN tar xzf libwww-perl-5.836.tar.gz \
&& cd libwww-perl-5.836 \
&& perl Makefile.PL \
&& make \
&& make install
# cpanm from a local tarball, twice, upgrading in place. perllocal.pod is append-only, so 2.02's
# stanza stays beside 2.06's forever and the later one has to win.
RUN cpanm --notest ./Text-CSV-2.02.tar.gz \
&& cpanm --notest ./Text-CSV-2.06.tar.gz
# a packlist with no perllocal.pod stanza at all, which is what NO_PERLLOCAL leaves and what
# Module::Build::Tiny produces by default. The version can then only come from the installed .pm.
RUN PERL_MM_OPT="NO_PERLLOCAL=1" cpanm --notest ./JSON-PP-4.16.tar.gz
# CPAN.pm. Encode's $VERSION is declared bare and computed with sprintf from an RCS Revision keyword
# inside a BEGIN block, so no static read of Encode.pm can recover it; only perllocal.pod, which
# records what EUMM evaluated at build time, has the value.
RUN PERL_MM_USE_DEFAULT=1 cpan -T -i D/DA/DANKOGAI/Encode-3.24.tar.gz
# guard the premise of the whole image rather than trusting it
RUN if find /usr/local/lib/perl5 -name install.json | grep -q .; then \
echo "an install.json was written; this image is supposed to have none"; exit 1; \
fi
RUN mkdir -p /out/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu \
&& cp -a /usr/local/lib/perl5/site_perl /out/usr/local/lib/perl5/site_perl \
&& cp -a /usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/perllocal.pod \
/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/.packlist \
/out/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/
FROM scratch
COPY --from=builder /out/ /
@@ -0,0 +1,46 @@
# two perl lib trees on one image, each with its own perllocal.pod naming the same module at a
# different version. Picking the stanza from the wrong tree would be silent, and both installed .pm
# files are the real ones, so scraping cannot cover for it: each tree would be reported at the
# other's version.
#
# Both JSON-PP installs come from local tarballs, so neither writes an install.json and the version
# can only come from perllocal.pod. A second full perl is not needed; a local-lib prefix reproduces
# the same ambiguity, because cpanm -L writes a perllocal.pod into the prefix's own arch directory.
#
# The image also carries one distribution installed into both trees at the same version, from a
# mirror, so that both trees hold an install.json agreeing on name and version. Those two records
# describe two installed things and are only distinguishable by the tree they landed in.
#
# The digest is perl:5.40-slim's linux/amd64 manifest; see image-cpan-mirror-installs/Dockerfile.
FROM perl:5.40-slim@sha256:af886c2fe0170cdf0bd83fa8254c7a55da227d5f20988a2e96c836646f96aa7a AS builder
WORKDIR /tarballs
RUN curl -sSfLO https://backpan.perl.org/authors/id/M/MA/MAKAMAKA/JSON-PP-2.27300.tar.gz \
&& curl -sSfLO https://backpan.perl.org/authors/id/I/IS/ISHIGAKI/JSON-PP-4.16.tar.gz
# the site install. 4.16 declares `our $VERSION = '4.16';`, which is scrapable, so this tree is the
# one where perllocal.pod and the .pm agree.
RUN cpanm --notest ./JSON-PP-4.16.tar.gz
# the second tree. 2.27300 declares the fully qualified `$JSON::PP::VERSION = '2.27300';` form that
# older Dist::Zilla wrote.
RUN cpanm --notest -L /opt/app ./JSON-PP-2.27300.tar.gz
# the same distribution and version into both trees. Resolving from a mirror is what makes cpanm
# write an install.json, and the PAUSE path pins the version the way a bare module name would not.
# Text-CSV is pure perl and pulls in nothing that is not core, so this needs no compiler and adds no
# unpinned fetch.
RUN cpanm --notest I/IS/ISHIGAKI/Text-CSV-2.06.tar.gz \
&& cpanm --notest -L /opt/app I/IS/ISHIGAKI/Text-CSV-2.06.tar.gz
RUN mkdir -p /out/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu \
&& cp -a /usr/local/lib/perl5/site_perl /out/usr/local/lib/perl5/site_perl \
&& cp -a /usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/perllocal.pod \
/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/.packlist \
/out/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu/ \
&& mkdir -p /out/opt \
&& cp -a /opt/app /out/opt/app
FROM scratch
COPY --from=builder /out/ /
@@ -0,0 +1,43 @@
# release tarballs unpacked but never installed, plus a source repository working tree. Nothing here
# has a packlist or an install.json, so the whole image is meta-file evidence.
#
# Versions are pinned, and pinned versions have to come from backpan: the mirrors drop old releases
# and answer with an HTML 404 that only fails later, at the point of untarring.
#
# The digest is perl:5.40-slim's linux/amd64 manifest rather than its index, so every image in this
# directory is built from the same architecture; see image-cpan-mirror-installs/Dockerfile.
FROM perl:5.40-slim@sha256:af886c2fe0170cdf0bd83fa8254c7a55da227d5f20988a2e96c836646f96aa7a AS builder
WORKDIR /opt/releases
# a Dist::Zilla release: it ships dist.ini inside the tarball, and MANIFEST lists it, so a dist.ini
# beside a META.json says nothing about whether this is a release or a checkout. It also ships both
# META.json and META.yml, which is the ordinary case for dzil and where META.json has to win.
RUN curl -sSfL https://backpan.perl.org/authors/id/E/ET/ETHER/Try-Tiny-0.32.tar.gz | tar xz
# CPAN Meta Spec 1.4, which is all that roughly 43% of current releases ship. Genuine
# ExtUtils::MakeMaker 6.56 output: license is a bare string rather than an array, version is an
# unquoted number, and there is no provides block because EUMM does not generate one.
RUN curl -sSfL https://backpan.perl.org/authors/id/M/MA/MAKAMAKA/Text-CSV-1.21.tar.gz | tar xz
# a META.yml no strict YAML parser accepts: EUMM wrote this distribution's multi-line ABSTRACT
# straight into `abstract:` with no quoting and no block scalar, so the continuation lines land at
# column zero. This has to skip the directory rather than fail the scan.
RUN curl -sSfL https://backpan.perl.org/authors/id/L/LE/LEMBARK/Parallel-Depend-4.10.tar.gz | tar xz
# a source repository working tree, pinned to one commit. Its committed META.json records the last
# release at 6.83 while lib/LWP.pm on the same commit says 6.84, which is why a committed META.json
# cannot be trusted for version. There is no MANIFEST, because Dist::Zilla generates it at build
# time, and that absence is the only thing separating this from a release.
#
# This is GitHub's tarball of the commit rather than a clone: it is the same working tree, it needs
# no git in the image, and it leaves no .git directory to prune.
RUN mkdir -p /src \
&& curl -sSfL https://codeload.github.com/libwww-perl/libwww-perl/tar.gz/65c6384c3d9e8a4f88a7fbe84307895dc78ccceb \
| tar xz -C /src \
&& mv /src/libwww-perl-* /src/libwww-perl
FROM scratch
COPY --from=builder /opt/releases /opt/releases
COPY --from=builder /src/libwww-perl /src/libwww-perl
@@ -0,0 +1,24 @@
# a perl with no CPAN installs at all, which pins the zero coverage of distributions bundled inside
# the interpreter. site_perl is dropped entirely, so what is left is the core lib tree: Encode and
# Storable sit in it, the only packlist is perl's own and has no auto/ segment, and there is no
# .meta and no distro package. Nothing here carries a core distribution's version.
#
# perllocal.pod comes along even though it names the distributions the base image preinstalled,
# because a perllocal.pod stanza on its own must not invent a package. Storable.pm does declare a
# readable $VERSION, so the interesting half is that no package is made out of it either.
#
# The digest is perl:5.40-slim's linux/amd64 manifest; see image-cpan-mirror-installs/Dockerfile.
FROM perl:5.40-slim@sha256:af886c2fe0170cdf0bd83fa8254c7a55da227d5f20988a2e96c836646f96aa7a AS builder
# the whole core lib tree is tens of megabytes of interpreter, so this takes the two dual-life
# distributions under discussion, their shared libraries, and the two files the catalogers look for
RUN O=/out/usr/local/lib/perl5/5.40.4/x86_64-linux-gnu \
&& mkdir -p $O/auto/Encode $O/auto/Storable \
&& cd /usr/local/lib/perl5/5.40.4/x86_64-linux-gnu \
&& cp -a Encode.pm Storable.pm perllocal.pod .packlist $O/ \
&& cp -a auto/Encode/Encode.so $O/auto/Encode/ \
&& cp -a auto/Storable/Storable.so $O/auto/Storable/
FROM scratch
COPY --from=builder /out/ /
+5
View File
@@ -23,6 +23,7 @@ const (
JavaScript Language = "javascript"
Lua Language = "lua"
OCaml Language = "ocaml"
Perl Language = "perl"
PHP Language = "php"
Python Language = "python"
R Language = "R"
@@ -45,6 +46,7 @@ var AllLanguages = []Language{
JavaScript,
Lua,
OCaml,
Perl,
PHP,
Python,
R,
@@ -80,6 +82,9 @@ func LanguageByName(name string) Language {
return JavaScript
case packageurl.TypeLuaRocks, string(Lua):
return Lua
// note: packageurl.TypeCpan and CpanPkg are the same string, so only one can be listed here
case packageurl.TypeCpan, string(Perl):
return Perl
case packageurl.TypePyPi, string(Python):
return Python
case packageurl.TypeGem, string(Ruby):
+4
View File
@@ -90,6 +90,10 @@ func TestLanguageFromPURL(t *testing.T) {
purl: "pkg:opam/ocaml-base-compiler@5.2.0",
want: OCaml,
},
{
purl: "pkg:cpan/URI@5.35?author=OALDERS",
want: Perl,
},
}
var languages = strset.New()
+61
View File
@@ -0,0 +1,61 @@
package pkg
import (
"sort"
"github.com/scylladb/go-set/strset"
)
// CpanDistribution describes a CPAN distribution installed on disk, as recorded by a CPAN client's
// install.json and by the .packlist an installer wrote. There is deliberately no name or version
// field: both would be verbatim copies of the package name and version. Licenses live on the package
// and dependencies live in relationships.
type CpanDistribution struct {
// Dist is the distvname install.json recorded (e.g. URI-5.35), the raw value the package name and
// version were recovered from
Dist string `json:"dist,omitempty"`
// MainModule is the module name recovered from the .packlist path (e.g. LWP for libwww-perl).
// Empty when the name came from install.json, which is authoritative. Set, it means the name is the
// installer's NAME and may not be the distribution name.
MainModule string `json:"mainModule,omitempty"`
// Author is the PAUSE ID of the distribution author (e.g. OALDERS), parsed out of Path
Author string `json:"author,omitempty"`
// Path is the raw PAUSE path of the release archive (e.g. O/OA/OALDERS/URI-5.35.tar.gz)
Path string `json:"path,omitempty"`
// Modules are the modules this distribution provides, sorted by name
Modules []CpanModule `json:"modules,omitempty"`
// Files are the paths the .packlist recorded as installed by this distribution, kept in the
// packlist's own order and unfiltered: a path the packlist claims and the filesystem lacks means the
// file was removed or overwritten out from under the installer, which is signal rather than noise
Files []string `json:"files,omitempty"`
}
// OwnedFiles satisfies pkg.FileOwner, so a distribution that installed files a distro package also
// owns is related to it by file ownership rather than reported as an unrelated duplicate.
func (m CpanDistribution) OwnedFiles() (result []string) {
result = strset.New(m.Files...).List()
sort.Strings(result)
return
}
// CpanUnpackedRelease describes an unpacked CPAN release found on disk, read from the release's own
// META.json or META.yml. Weaker evidence than an installed distribution: the code is present but
// nothing says the interpreter can load it. It has no PAUSE path, so it has no author.
type CpanUnpackedRelease struct {
// Modules are the modules the release declares it provides, sorted by name
Modules []CpanModule `json:"modules,omitempty"`
}
// CpanModule is a single perl module provided by a CPAN distribution. Shared by both types above.
type CpanModule struct {
// Name is the module name as it would be used in a perl `use` statement (e.g. URI::Escape)
Name string `json:"name"`
// Version is the module version, which may differ from the distribution version
Version string `json:"version,omitempty"`
}
+48
View File
@@ -0,0 +1,48 @@
package pkg
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCpanDistribution_OwnedFiles(t *testing.T) {
tests := []struct {
name string
metadata CpanDistribution
expected []string
}{
{
name: "sorted and deduplicated",
metadata: CpanDistribution{
// a distribution that merged more than one packlist can claim the same path twice
Files: []string{"/lib/URI.pm", "/lib/URI/Escape.pm", "/lib/URI.pm"},
},
expected: []string{"/lib/URI.pm", "/lib/URI/Escape.pm"},
},
{
// this is what a NO_PACKLIST install leaves behind: an install.json and nothing else
name: "install.json only owns nothing",
metadata: CpanDistribution{Dist: "URI-5.35", Modules: []CpanModule{{Name: "URI"}}},
expected: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.metadata.OwnedFiles())
})
}
}
func TestCpanDistribution_isFileOwner(t *testing.T) {
var owner any = CpanDistribution{}
_, ok := owner.(FileOwner)
require.True(t, ok, "the file-ownership relationship workers only see metadata that satisfies FileOwner")
// the unpacked-release tier has no packlist, so it deliberately does not
var release any = CpanUnpackedRelease{}
_, ok = release.(FileOwner)
assert.False(t, ok)
}
+6
View File
@@ -21,6 +21,7 @@ const (
CocoapodsPkg Type = "pod"
ConanPkg Type = "conan"
CondaPkg Type = "conda"
CpanPkg Type = "cpan"
DartPubPkg Type = "dart-pub"
DebPkg Type = "deb"
DotnetPkg Type = "dotnet"
@@ -68,6 +69,7 @@ var AllPkgs = []Type{
CocoapodsPkg,
ConanPkg,
CondaPkg,
CpanPkg,
DartPubPkg,
DebPkg,
DotnetPkg,
@@ -123,6 +125,8 @@ func (t Type) PackageURLType() string {
return packageurl.TypeConan
case CondaPkg:
return packageurl.TypeGeneric
case CpanPkg:
return packageurl.TypeCpan
case DartPubPkg:
return packageurl.TypePub
case DebPkg:
@@ -224,6 +228,8 @@ func TypeByName(name string) Type {
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
+4
View File
@@ -125,6 +125,10 @@ func TestTypeFromPURL(t *testing.T) {
purl: "pkg:luarocks/kong@3.7.0",
expected: LuaRocksPkg,
},
{
purl: "pkg:cpan/URI@5.35?author=OALDERS",
expected: CpanPkg,
},
{
purl: "pkg:swift/github.com/apple/swift-numerics/swift-numerics@1.0.2",
expected: SwiftPkg,
+1 -1
View File
@@ -9,7 +9,7 @@ import (
const (
// this is the number of packages that should be found in the image-pkg-coverage fixture image
// when analyzed with the squashed scope.
coverageImageSquashedPackageCount = 42
coverageImageSquashedPackageCount = 43
)
func TestPackagesCmdFlags(t *testing.T) {