Compare commits

...
Author SHA1 Message Date
Alex Goodman cc326e45a6 fix: preserve quoted args when deferring to tagged install script (#5363)
The installer word-split the original arguments when piping into a tagged release's `install.sh`, so `-b '/tmp/syft install path'` arrived as three words and the trailing word was treated as a release tag.

- argument parsing moved into `parse_install_args` so `main` keeps its original args
- tagged script is invoked with `"$@"` instead of the unquoted `PROGRAM_ARGS`

Port of anchore/grype#3738

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-10-01 15:00:50 -04:00
Alex GoodmanandKeith Zantow ef356eccea fix: sign release checksums with a sigstore bundle (#5362)
* fix: sign release checksums with a sigstore bundle

The cosign update rejects the legacy `--output-certificate` / `--output-signature` flags alongside the new bundle format, which broke signing during the v1.53.0 release.

- checksums are now signed into a single `checksums.txt.sigstore.json` bundle (signature, certificate, and tlog proof)
- `install.sh -v` verifies with `--bundle` for releases >= v1.53.0 and keeps the `.sig` / `.pem` flow for older releases

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* chore: smooth the cutover to sigstore bundle verification

- `install.sh -v` now hints that bundle verification needs cosign v2.5.0 or newer when verification of a bundle-signed release fails
- install tests keep exercising the legacy `.sig` / `.pem` path against v1.52.0 once the latest release is bundle-signed
- touch the install test environment Dockerfiles so the cached images rebuild with a current cosign

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix: only hint at the cosign minimum when cosign is actually too old

- `install.sh -v` used to print the cosign v2.5.0 note on any failed bundle verification, including a missing bundle or a bad signature. It now only prints it when cosign reports it couldn't read the bundle (`bundle does not contain cert`), which is how v2.2.4 through v2.4.1 fail on a sigstore bundle.
- `verify_sign` is now covered with a stub cosign, so the bundle path is exercised before any bundle-signed release is latest

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix: stop the install when a release asset is missing or fails its checksum

- `install.sh` now stops when the checksums file, the signature bundle, or the asset can't be downloaded, instead of handing an empty or error-page file to the next step
- a sha256 mismatch between the asset and the checksums file now stops the install. Before this the mismatch was logged and the asset was installed anyway, so `-v` only ever vouched for `checksums.txt`, not the binary

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* chore: pin cosign in the install test images

The install test images pulled whatever cosign was latest, unverified. They now install cosign v3.1.3 checked against its published sha256, so the install tests always run with a cosign that can verify sigstore bundles (releases >= v1.53.0 need v2.5.0 or newer).

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Keith Zantow <kzantow@gmail.com>
2026-10-01 15:00:37 -04:00
dependabot[bot] db7e445c54 chore(deps): bump github.com/ulikunitz/xz from 0.5.16 to 0.5.17 (#5366)
Bumps [github.com/ulikunitz/xz](https://github.com/ulikunitz/xz) from 0.5.16 to 0.5.17.
- [Commits](https://github.com/ulikunitz/xz/compare/v0.5.16...v0.5.17)

---
updated-dependencies:
- dependency-name: github.com/ulikunitz/xz
  dependency-version: 0.5.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 18:03:47 +00:00
dependabot[bot] d654c00bd6 chore(deps): bump github.com/bmatcuk/doublestar/v4 from 4.10.0 to 4.10.2 (#5365)
Bumps [github.com/bmatcuk/doublestar/v4](https://github.com/bmatcuk/doublestar) from 4.10.0 to 4.10.2.
- [Release notes](https://github.com/bmatcuk/doublestar/releases)
- [Commits](https://github.com/bmatcuk/doublestar/compare/v4.10.0...v4.10.2)

---
updated-dependencies:
- dependency-name: github.com/bmatcuk/doublestar/v4
  dependency-version: 4.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 18:00:41 +00:00
Alex Goodman c00be184b5 fix: revert hashstructure to v0.6.0 to keep artifact IDs stable (#5364)
* fix: revert hashstructure to v0.6.0 to keep artifact IDs stable

hashstructure v1 changed `SlicesAsSets` hashing (every slice now gets a finalizing hash pass, and duplicate elements are deduplicated), which changed nearly every artifact ID syft produces.

- pin `github.com/gohugoio/hashstructure` back to v0.6.0 and have dependabot ignore major bumps
- make the aggregate `Unit tests` job fail when a dependency fails; previously it was skipped, which satisfies the required check and let #5277 auto-merge with failing unit tests

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* ci: fail unit test gate on any failed, cancelled, or skipped dependency

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* ci: require every unit test dependency to succeed

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-10-01 17:40:34 +00:00
dependabot[bot] 29d2de72c8 chore(deps): bump github.com/gohugoio/hashstructure from 0.6.0 to 1.1.0 (#5277)
Bumps [github.com/gohugoio/hashstructure](https://github.com/gohugoio/hashstructure) from 0.6.0 to 1.1.0.
- [Release notes](https://github.com/gohugoio/hashstructure/releases)
- [Commits](https://github.com/gohugoio/hashstructure/compare/v0.6.0...v1.1.0)

---
updated-dependencies:
- dependency-name: github.com/gohugoio/hashstructure
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 15:35:21 +00:00
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
67 changed files with 8608 additions and 152 deletions
+3
View File
@@ -21,6 +21,9 @@ updates:
- dependency-name: "github.com/aquasecurity/go-version"
- dependency-name: "github.com/knqyf263/go-apk-version"
- dependency-name: "github.com/knqyf263/go-deb-version"
# v1 changed SlicesAsSets hashing, which changes every artifact ID syft produces
- dependency-name: "github.com/gohugoio/hashstructure"
update-types: ["version-update:semver-major"]
- package-ecosystem: "github-actions"
directories:
+12 -1
View File
@@ -51,8 +51,19 @@ jobs:
name: "Unit tests"
runs-on: ubuntu-latest
needs: [Unit-Test-Linux, Unit-Test-Windows]
# a skipped job satisfies a required check, so this job must always run and fail explicitly
# unless every dependency succeeded (otherwise a failing PR can still be auto-merged)
if: ${{ always() }}
steps:
- run: echo All unit tests passed!
- name: Require all unit test jobs to succeed
env:
NEEDS: ${{ toJSON(needs) }}
run: |
echo "$NEEDS" | jq -e 'length > 0 and all(.[]; .result == "success")' > /dev/null || {
echo "$NEEDS" | jq -r 'to_entries[] | "\(.key): \(.value.result)"'
exit 1
}
echo All unit tests passed!
Unit-Test-Linux:
name: "Unit tests (Linux)"
+3 -6
View File
@@ -167,16 +167,13 @@ sboms:
- "--output"
- "json=$document"
# the signature, certificate, and transparency log proof all land in a single sigstore bundle.
signs:
- cmd: .tool/cosign
signature: "${artifact}.sig"
certificate: "${artifact}.pem"
signature: "${artifact}.sigstore.json"
args:
- "sign-blob"
- "--use-signing-config=false"
- "--oidc-issuer=https://token.actions.githubusercontent.com"
- "--output-certificate=${certificate}"
- "--output-signature=${signature}"
- "--bundle=${signature}"
- "${artifact}"
- "--yes"
artifacts: checksum
@@ -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"
}
+55 -31
View File
@@ -22,12 +22,12 @@ 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
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/bmatcuk/doublestar/v4 v4.10.0
github.com/bmatcuk/doublestar/v4 v4.10.2
github.com/cespare/xxhash/v2 v2.3.0
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
@@ -83,7 +83,7 @@ require (
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.12.1
github.com/ulikunitz/xz v0.5.16
github.com/ulikunitz/xz v0.5.17
github.com/vbatts/go-mtree v0.7.0
github.com/vifraa/gopom v1.0.0
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651
@@ -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
+161 -67
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=
@@ -225,8 +229,8 @@ github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef h1:TSFnfbbu2oAO
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef/go.mod h1:9iglf1GG4oNRJ39bZ5AZrjgAFD2RwQbXw6Qf7Cs47wo=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.10.2 h1:eF7W7HWKg3z9NrWV9pTLnNeoXaqq3Tq9DNKXVMfoCnw=
github.com/bmatcuk/doublestar/v4 v4.10.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
@@ -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=
@@ -947,10 +994,16 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
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/ulikunitz/xz v0.5.17 h1:flR0y/x1hgM8EGV1AW3Xll6T413G0glV8UfBwR617V4=
github.com/ulikunitz/xz v0.5.17/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=
+49 -20
View File
@@ -8,7 +8,6 @@ REPO="${PROJECT_NAME}"
GITHUB_DOWNLOAD_PREFIX=https://github.com/${OWNER}/${REPO}/releases/download
INSTALL_SH_BASE_URL=https://get.anchore.io/${PROJECT_NAME}
LEGACY_INSTALL_SH_BASE_URL=https://raw.githubusercontent.com/${OWNER}/${PROJECT_NAME}
PROGRAM_ARGS=$@
# signature verification options
@@ -19,6 +18,8 @@ VERIFY_SIGN=false
VERIFY_SIGN_SUPPORTED_VERSION=v0.104.0
# this is the earliest tag in the repo where the -v flag was introduced to this install.sh script
VERIFY_SIGN_FLAG_VERSION=v1.6.0
# releases at or after this version are signed with a sigstore bundle (.sigstore.json); earlier releases ship a separate .sig and .pem
VERIFY_SIGN_BUNDLE_VERSION=v1.53.0
# do not change the name of this parameter (this must always be backwards compatible)
DOWNLOAD_TAG_INSTALL_SCRIPT=${DOWNLOAD_TAG_INSTALL_SCRIPT:-true}
@@ -386,8 +387,8 @@ download_github_release_checksums_files() (
complete_url=$(github_release_asset_url "${download_url}" "${name}" "${version}" "${filename}")
output_path="${output_dir}/${complete_filename}"
http_download "${output_path}" "${complete_url}" ""
asset_file_exists "${output_path}"
http_download "${output_path}" "${complete_url}" "" || return 1
asset_file_exists "${output_path}" || return 1
log_trace "download_github_release_checksums_files() returned '${output_path}' for file '${complete_filename}'"
@@ -585,28 +586,31 @@ download_and_install_asset() (
install_asset "${asset_filepath}" "${install_path}" "${binary}"
)
# verify_sign [checksums-file-path] [certificate-reference] [signature-reference] [version]
# verify_sign [checksums-file-path] [verification-material-flags...]
#
# attempts verify the signature of the checksums file from the release workflow in Github Actions run against the main branch.
# the verification material is either "--bundle <path>" or (for older releases) "--certificate <ref> --signature <ref>".
#
verify_sign() {
checksums_file=$1
cert_reference=$2
sig_reference=$3
shift
log_trace "verifying artifact $1"
log_trace "verifying artifact ${checksums_file}"
log_file=$(mktemp)
${COSIGN_BINARY} \
verify-blob "$checksums_file" \
--certificate "$cert_reference" \
--signature "$sig_reference" \
"$@" \
--certificate-identity "https://github.com/${OWNER}/${REPO}/.github/workflows/release.yaml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" > "${log_file}" 2>&1
if [ $? -ne 0 ]; then
log_err "$(cat "${log_file}")"
# cosign older than v2.4.2 can't read the bundle's verification material and reports a missing cert
if [ "$1" = "--bundle" ] && grep -q "bundle does not contain cert" "${log_file}"; then
log_err "note: releases >= ${VERIFY_SIGN_BUNDLE_VERSION} are verified with a sigstore bundle, which requires cosign v2.5.0 or newer (check '${COSIGN_BINARY} version')"
fi
rm -f "${log_file}"
return 1
fi
@@ -630,7 +634,10 @@ download_asset() (
log_trace "download_asset(url=${download_url}, destination=${destination}, name=${name}, os=${os}, arch=${arch}, version=${version}, format=${format})"
checksums_filepath=$(download_github_release_checksums "${download_url}" "${name}" "${version}" "${destination}")
if ! checksums_filepath=$(download_github_release_checksums "${download_url}" "${name}" "${version}" "${destination}"); then
log_err "unable to download checksums file"
return 1
fi
log_trace "checksums content:\n$(cat ${checksums_filepath})"
@@ -642,13 +649,25 @@ download_asset() (
fi
if [ "$VERIFY_SIGN" = true ]; then
checksum_sig_file_url=$(github_release_checksums_sig_url "${download_url}" "${name}" "${version}")
log_trace "checksums signature url: ${checksum_sig_file_url}"
if compare_semver "${version}" "${VERIFY_SIGN_BUNDLE_VERSION}"; then
if ! checksums_bundle_filepath=$(download_github_release_checksums_files "${download_url}" "${name}" "${version}" "${destination}" "checksums.txt.sigstore.json"); then
log_err "unable to download checksums signature bundle"
return 1
fi
log_trace "checksums bundle: ${checksums_bundle_filepath}"
checksums_cert_file_url=$(github_release_checksums_cert_url "${download_url}" "${name}" "${version}")
log_trace "checksums certificate url: ${checksums_cert_file_url}"
set -- --bundle "${checksums_bundle_filepath}"
else
checksum_sig_file_url=$(github_release_checksums_sig_url "${download_url}" "${name}" "${version}")
log_trace "checksums signature url: ${checksum_sig_file_url}"
if ! verify_sign "${checksums_filepath}" "${checksums_cert_file_url}" "${checksum_sig_file_url}"; then
checksums_cert_file_url=$(github_release_checksums_cert_url "${download_url}" "${name}" "${version}")
log_trace "checksums certificate url: ${checksums_cert_file_url}"
set -- --certificate "${checksums_cert_file_url}" --signature "${checksum_sig_file_url}"
fi
if ! verify_sign "${checksums_filepath}" "$@"; then
log_err "signature verification failed"
return 1
fi
@@ -657,9 +676,15 @@ download_asset() (
asset_url="${download_url}/${asset_filename}"
asset_filepath="${destination}/${asset_filename}"
http_download "${asset_filepath}" "${asset_url}" ""
if ! http_download "${asset_filepath}" "${asset_url}" ""; then
log_err "unable to download asset '${asset_url}'"
return 1
fi
hash_sha256_verify "${asset_filepath}" "${checksums_filepath}"
# this is what ties the asset to the (optionally signature verified) checksums file, so a mismatch must stop the install
if ! hash_sha256_verify "${asset_filepath}" "${checksums_filepath}"; then
return 1
fi
log_trace "download_asset_by_checksums_file() returned '${asset_filepath}'"
@@ -765,7 +790,7 @@ prep_signature_verification() {
fi
}
main() (
parse_install_args() {
# parse arguments
# note: never change default install directory (this must always be backwards compatible)
@@ -805,6 +830,11 @@ EOF
set +u
tag=$1
set -u
}
main() (
parse_install_args "$@"
if [ -z "${tag}" ]; then
log_info "checking github for the current release tag"
@@ -812,7 +842,6 @@ EOF
else
log_info "checking github for release tag='${tag}'"
fi
set -u
if ! tag=$(get_release_tag "${OWNER}" "${REPO}" "${tag}"); then
log_err "unable to find tag='${tag}'"
@@ -843,7 +872,7 @@ EOF
log_warn "failed to fetch from ${INSTALL_SH_BASE_URL}, trying fallback URL"
install_script=$(http_copy "${LEGACY_INSTALL_SH_BASE_URL}/${tag}/install.sh" "")
fi
echo "${install_script}" | sh -s -- ${PROGRAM_ARGS}
echo "${install_script}" | sh -s -- "$@"
exit $?
fi
+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"
},
@@ -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,
+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) {
@@ -43,3 +43,9 @@ run_test_case test_download_release_asset "${release}" "linux" "amd64" "deb" "ap
run_test_case test_download_release_asset "${release}" "linux" "arm64" "tar.gz" "application/gzip"
run_test_case test_download_release_asset "${release}" "linux" "arm64" "rpm" "application/x-rpm"
run_test_case test_download_release_asset "${release}" "linux" "arm64" "deb" "application/vnd.debian.binary-package"
# releases before VERIFY_SIGN_BUNDLE_VERSION are signed with a separate .sig and .pem instead of a sigstore bundle,
# so keep exercising that path against a fixed release
legacy_release="v1.52.0"
run_test_case test_download_release_asset "${legacy_release}" "linux" "amd64" "tar.gz" "application/gzip"
@@ -87,3 +87,61 @@ test_prep_signature_verification() {
}
run_test_case test_prep_signature_verification
# ensure the verification material is passed through to cosign and the old-cosign hint only shows when warranted
test_verify_sign() {
# verify_sign [checksums-file-path] [verification-material-flags...]
OG_COSIGN_BINARY=${COSIGN_BINARY}
tmpdir=$(mktemp -d)
args_file="${tmpdir}/args"
hint="requires cosign v2.5.0 or newer"
# stub cosign that records its arguments, then prints STUB_OUTPUT and exits with STUB_EXIT
COSIGN_BINARY="${tmpdir}/cosign"
cat > "${COSIGN_BINARY}" <<STUB
#!/bin/sh
echo "\$@" > "${args_file}"
echo "\${STUB_OUTPUT}"
exit "\${STUB_EXIT}"
STUB
chmod +x "${COSIGN_BINARY}"
identity="--certificate-identity https://github.com/${OWNER}/${REPO}/.github/workflows/release.yaml@refs/heads/main --certificate-oidc-issuer https://token.actions.githubusercontent.com"
# bundle verification succeeds
STUB_OUTPUT="Verified OK" STUB_EXIT=0 verify_sign "checksums.txt" --bundle "checksums.txt.sigstore.json" >/dev/null 2>&1
assertEquals "0" "$?" "bundle verification should succeed"
assertEquals "verify-blob checksums.txt --bundle checksums.txt.sigstore.json ${identity}" "$(cat "${args_file}")" "unexpected cosign args for bundle"
# legacy verification succeeds
STUB_OUTPUT="Verified OK" STUB_EXIT=0 verify_sign "checksums.txt" --certificate "c.pem" --signature "c.sig" >/dev/null 2>&1
assertEquals "0" "$?" "legacy verification should succeed"
assertEquals "verify-blob checksums.txt --certificate c.pem --signature c.sig ${identity}" "$(cat "${args_file}")" "unexpected cosign args for legacy"
# cosign too old to read the bundle: fail with the hint (this is what cosign v2.2.4 through v2.4.1 print)
output=$(STUB_OUTPUT="Error: bundle does not contain cert for verification, please provide public key" STUB_EXIT=1 verify_sign "checksums.txt" --bundle "b.json" 2>&1)
assertEquals "1" "$?" "old cosign should fail verification"
assertContains "${output}" "${hint}" "old cosign should get the version hint"
# bad signature (e.g. tampered checksums): fail without the hint
output=$(STUB_OUTPUT="Error: invalid signature when validating ASN.1 encoded signature" STUB_EXIT=1 verify_sign "checksums.txt" --bundle "b.json" 2>&1)
assertEquals "1" "$?" "bad signature should fail verification"
assertNotContains "${output}" "${hint}" "bad signature should not get the version hint"
# unparseable bundle (e.g. a 404 body): fail without the hint
output=$(STUB_OUTPUT="Error: invalid character 'N' looking for beginning of value" STUB_EXIT=1 verify_sign "checksums.txt" --bundle "b.json" 2>&1)
assertEquals "1" "$?" "unparseable bundle should fail verification"
assertNotContains "${output}" "${hint}" "unparseable bundle should not get the version hint"
# legacy failure never gets the bundle hint, even with the same output
output=$(STUB_OUTPUT="Error: bundle does not contain cert for verification, please provide public key" STUB_EXIT=1 verify_sign "checksums.txt" --certificate "c.pem" --signature "c.sig" 2>&1)
assertEquals "1" "$?" "legacy failure should fail verification"
assertNotContains "${output}" "${hint}" "legacy failure should not get the version hint"
COSIGN_BINARY=${OG_COSIGN_BINARY}
rm -rf -- "${tmpdir}"
}
run_test_case test_verify_sign
+84
View File
@@ -0,0 +1,84 @@
. test_harness.sh
# serve release assets from a local fixture dir instead of the network (a missing file is a failed download)
http_download() {
src="${FIXTURE_DIR}/${2##*/}"
[ -f "${src}" ] || return 1
cp "${src}" "$1"
}
# ensure every missing or mismatched piece of a release stops download_asset before anything is installed
test_download_asset_failures() {
# download_asset [release-url-prefix] [download-path] [name] [os] [arch] [version] [format]
# we are expecting error messages, which is confusing to look at in passing tests... disable logging for now
log_set_priority -1
OG_COSIGN_BINARY=${COSIGN_BINARY}
tmpdir=$(mktemp -d)
FIXTURE_DIR="${tmpdir}/release"
dest="${tmpdir}/dest"
args_file="${tmpdir}/cosign-args"
mkdir -p "${FIXTURE_DIR}" "${dest}"
# stub cosign that records it was called and always verifies
COSIGN_BINARY="${tmpdir}/cosign"
printf '#!/bin/sh\necho "$@" > "%s"\n' "${args_file}" > "${COSIGN_BINARY}"
chmod +x "${COSIGN_BINARY}"
asset="syft_1.54.0_linux_amd64.tar.gz"
echo "the real asset" > "${FIXTURE_DIR}/${asset}"
echo "$(hash_sha256 "${FIXTURE_DIR}/${asset}") ${asset}" > "${FIXTURE_DIR}/syft_1.54.0_checksums.txt"
echo "{}" > "${FIXTURE_DIR}/syft_1.54.0_checksums.txt.sigstore.json"
run() {
rm -rf "${dest:?}"/* "${args_file}"
download_asset "https://example.invalid/v1.54.0" "${dest}" "syft" "linux" "amd64" "1.54.0" "tar.gz"
}
# happy path, verified and unverified
VERIFY_SIGN=false
asset_path=$(run)
assertEquals "0" "$?" "complete release should download"
assertEquals "${dest}/${asset}" "${asset_path}" "unexpected asset path"
VERIFY_SIGN=true
run >/dev/null
assertEquals "0" "$?" "complete release should verify"
assertContains "$(cat "${args_file}")" "--bundle ${dest}/syft_1.54.0_checksums.txt.sigstore.json" "bundle should be passed to cosign"
# missing bundle: fail before cosign is ever called
mv "${FIXTURE_DIR}/syft_1.54.0_checksums.txt.sigstore.json" "${tmpdir}/bundle"
run >/dev/null
assertEquals "1" "$?" "missing bundle should fail"
assertFilesDoesNotExist "${args_file}" "cosign should not be called without a bundle"
mv "${tmpdir}/bundle" "${FIXTURE_DIR}/syft_1.54.0_checksums.txt.sigstore.json"
for VERIFY_SIGN in true false; do
# asset does not match the checksums file
echo "a tampered asset" > "${FIXTURE_DIR}/${asset}"
run >/dev/null
assertEquals "1" "$?" "checksum mismatch should fail (verify=${VERIFY_SIGN})"
echo "the real asset" > "${FIXTURE_DIR}/${asset}"
# missing asset
mv "${FIXTURE_DIR}/${asset}" "${tmpdir}/asset"
run >/dev/null
assertEquals "1" "$?" "missing asset should fail (verify=${VERIFY_SIGN})"
mv "${tmpdir}/asset" "${FIXTURE_DIR}/${asset}"
# missing checksums file
mv "${FIXTURE_DIR}/syft_1.54.0_checksums.txt" "${tmpdir}/checksums"
run >/dev/null
assertEquals "1" "$?" "missing checksums should fail (verify=${VERIFY_SIGN})"
mv "${tmpdir}/checksums" "${FIXTURE_DIR}/syft_1.54.0_checksums.txt"
done
COSIGN_BINARY=${OG_COSIGN_BINARY}
rm -rf -- "${tmpdir}"
log_set_priority 0
}
run_test_case test_download_asset_failures
+41
View File
@@ -0,0 +1,41 @@
. test_harness.sh
# make certain main defers to the (stubbed) tagged release script regardless of the caller's env
DOWNLOAD_TAG_INSTALL_SCRIPT=true
get_release_tag() {
printf '%s\n' "$3"
}
prep_signature_verification() {
return 0
}
# the "tagged release script" records each argument it receives, one bracketed arg per line
http_copy() {
cat <<'SCRIPT'
printf '[%s]\n' "$@" > "$INSTALL_ARG_CAPTURE"
SCRIPT
}
test_release_script_receives_original_args() {
test_dir=$(mktemp -d)
INSTALL_ARG_CAPTURE="${test_dir}/actual"
export INSTALL_ARG_CAPTURE
# note: paths are read here instead of passed to run_test_case, which word-splits its arguments
while IFS= read -r install_path; do
main -b "${install_path}" -d v0.80.0
printf '[%s]\n' -b "${install_path}" -d v0.80.0 > "${test_dir}/expected"
assertFilesEqual "${test_dir}/expected" "${INSTALL_ARG_CAPTURE}" "args forwarded to the release script should match the original args (path='${install_path}')"
done <<'PATHS'
/tmp/bin with spaces
/tmp/bin*
/tmp/it's "quoted"
PATHS
rm -rf -- "${test_dir}"
}
run_test_case test_release_script_receives_original_args
@@ -1,5 +1,9 @@
FROM alpine:3.6
RUN apk update && apk add python3 wget curl unzip make ca-certificates jq
RUN curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64" && \
mv cosign-linux-amd64 /usr/local/bin/cosign && \
# cosign is pinned (and checksum verified) so the install tests always run with a cosign that can verify
# sigstore bundles (releases >= v1.53.0 need v2.5.0 or newer)
ARG COSIGN_VERSION=v3.1.3
ARG COSIGN_SHA256=4629c757b7618056f8ddd7e2625ae9fdd94c0372a65049520bc7d9df9efc7f71
RUN curl -sSfL -o /usr/local/bin/cosign "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64" && \
echo "${COSIGN_SHA256} /usr/local/bin/cosign" | sha256sum -c - && \
chmod +x /usr/local/bin/cosign
@@ -5,17 +5,22 @@ RUN apk update && apk add ca-certificates
# so let docker pull the image that matches the hosts architecture first and then pull the correct asset
FROM busybox:1.36.1-musl
# cosign is pinned (and checksum verified) so the install tests always run with a cosign that can verify
# sigstore bundles (releases >= v1.53.0 need v2.5.0 or newer)
ARG COSIGN_VERSION=v3.1.3
ARG COSIGN_SHA256_AMD64=4629c757b7618056f8ddd7e2625ae9fdd94c0372a65049520bc7d9df9efc7f71
ARG COSIGN_SHA256_ARM64=c5d324e091826b0d7a78eb16fef316450b4eb9aaec045611c08ba06f5e73220a
RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "x86_64" ]; then \
COSIGN_ARCH="amd64"; \
COSIGN_ARCH="amd64"; COSIGN_SHA256="${COSIGN_SHA256_AMD64}"; \
elif [ "$ARCH" = "aarch64" ]; then \
COSIGN_ARCH="arm64"; \
COSIGN_ARCH="arm64"; COSIGN_SHA256="${COSIGN_SHA256_ARM64}"; \
else \
echo "Unsupported architecture: $ARCH" && exit 1; \
fi && \
echo "Downloading cosign for $COSIGN_ARCH" && \
wget https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-${COSIGN_ARCH} && \
mv cosign-linux-${COSIGN_ARCH} /bin/cosign && \
echo "Downloading cosign ${COSIGN_VERSION} for $COSIGN_ARCH" && \
wget -O /bin/cosign https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-${COSIGN_ARCH} && \
echo "${COSIGN_SHA256} /bin/cosign" | sha256sum -c - && \
chmod +x /bin/cosign
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
@@ -1,5 +1,9 @@
FROM --platform=linux/amd64 ubuntu:20.04@sha256:33a5cc25d22c45900796a1aca487ad7a7cb09f09ea00b779e3b2026b4fc2faba
RUN apt update -y && apt install make python3 curl unzip jq -y
RUN LATEST_VERSION=$(curl https://api.github.com/repos/sigstore/cosign/releases/latest | grep tag_name | cut -d : -f2 | tr -d "v\", ") && \
curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign_${LATEST_VERSION}_amd64.deb" && \
dpkg -i cosign_${LATEST_VERSION}_amd64.deb
# cosign is pinned (and checksum verified) so the install tests always run with a cosign that can verify
# sigstore bundles (releases >= v1.53.0 need v2.5.0 or newer)
ARG COSIGN_VERSION=v3.1.3
ARG COSIGN_SHA256=4629c757b7618056f8ddd7e2625ae9fdd94c0372a65049520bc7d9df9efc7f71
RUN curl -sSfL -o /usr/local/bin/cosign "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64" && \
echo "${COSIGN_SHA256} /usr/local/bin/cosign" | sha256sum -c - && \
chmod +x /usr/local/bin/cosign
+25
View File
@@ -56,6 +56,31 @@ assertFileExists() {
fi
}
assertContains() {
haystack=$1
needle=$2
msg=$3
case "$haystack" in
*"$needle"*) ;;
*)
echo "assertContains failed: '$needle' not found in '$haystack' $msg"
exit 2
;;
esac
}
assertNotContains() {
haystack=$1
needle=$2
msg=$3
case "$haystack" in
*"$needle"*)
echo "assertNotContains failed: '$needle' found in '$haystack' $msg"
exit 2
;;
esac
}
assertFilesEqual() {
want=$1
got=$2