Compare commits

...
Author SHA1 Message Date
anchore-oss-update-botandanchore-oss-update-bot b254e6d92f chore(deps): update anchore dependencies (#5367)
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-06 10:36:33 -04:00
29b92a58c1 Add container-storage support for Podman and Buildah (#5368)
* feat: add containers-storage support for Podman and Buildah, update tests and documentation

Signed-off-by: Bruce Clark <bruce.clark@mandg.com>

* test: update containers-storage tests to validate package.json and handle missing images

Signed-off-by: Bruce Clark <bruce.clark@mandg.com>

* test: fix rootless containers-storage test setup

Install uidmap for Podman and Buildah CI jobs.
Align temporary rootful and rootless storage paths.
Clean up rootless stores within the builder's user namespace.

Signed-off-by: Bruce Clark <bruce.clark@mandg.com>

* fix: only use containers-storage when explicitly requested

- containers-storage is no longer part of automatic image resolution; it's used only for `--from containers-storage`, the `containers-storage:` scheme, or `default-pull-source: containers-storage` (now an accepted value). Opening a store runs full graph driver init (mounts, locks, store writes), which is too invasive as a probe on every plain image reference.
- rootless stores must be read from inside the builder's user namespace (`podman unshare syft ...`), documented in the README.
- the `containers_image_openpgp` tag is now applied to unit/integration tests and lint, so dev tooling compiles the same provider that ships.
- containers-storage CI runs through `make containers-storage-test` and covers both vfs and overlay stores.

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

* fix: exclude cgo btrfs driver and unmount overlay store in cleanup

- cgo-enabled linux builds (`-race` tests, lint) compile go.podman.io's btrfs graph driver, which needs libbtrfs headers. Add `exclude_graphdriver_btrfs` alongside `containers_image_openpgp` everywhere the tag is set; release builds are `CGO_ENABLED=0` and never included it.
- the overlay driver bind-mounts its graphroot inside the rootless user namespace, so the containers-storage test unmounts it before removing the store, and cleanup no longer fails the run.

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

---------

Signed-off-by: Bruce Clark <bruce.clark@mandg.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Bruce Clark <bruce.clark@mandg.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-10-05 15:02:37 +00:00
Alex Goodman 6c9bb732dd ci: run the full unit suite on windows (#5370)
* ci: run the full unit suite on windows via make unit

Drops the curated `unit:windows` subset; the windows job now bootstraps like linux and runs `make unit`. Exceptions get added as failures show up.

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

* ci: skip fixture refresh for windows unit tests

binny has no windows release, so the bootstrap action can't install the tools the fixture refresh needs.

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

* test: skip linux container fixtures when docker can't run them

Windows CI runners only run windows containers, so image fixtures go through `testutils.SkipWithoutLinuxContainers`. No behavior change on linux/mac.

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

* fix: close leaked file handles and posix squashfs paths on windows

- output writers are now closed on early returns in `scan`/`convert` (open handles block cleanup on windows)
- `newSBOMMultiWriter` used `path.Dir` on host paths, so output subdirectories were never created on windows
- squashfs walk paths were built with `filepath.Join`
- skip a few posix-only test cases (dir perms, xdg fallback, go-rpmdb ndb handle leak upstream)

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

* fix(golang): resolve go source module dir natively on windows

- `parseGoModFile` passed the resolver's posix (`/c/...`) path to the go toolchain, so go source resolution silently found nothing on windows
- skip container-built fixtures (go archs, java builds) and golden image tarballs on windows; stereoscope's layer cache filenames contain `:`, which windows rejects

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

* fix: keep posix paths posix on windows

- `ChrootContext.ToChrootPath` nested volumes (`D:\cwd\C:\root`) when the scan root was on another drive, so the prefix was never trimmed
- bitnami, python wheel/egg, and alpm parsers used `filepath` on posix location paths
- alpm backup-file lookups silently missed on windows

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

* ci: pull the test fixture cache for windows unit tests

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

* fix: don't map virtual file tree paths through the host on windows

`FiletreeResolver` without a chroot (snaps) ran request paths through `filepath.Abs`, rooting them on the current volume (`/payload.txt` -> `/d/payload.txt`), so snap scans on windows found nothing.

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

* fix: match tar entry globs as posix on windows

- `matchesAnyGlob` used `doublestar.PathMatch` (host separator) on tar entry names, so tar-wrapped java archives silently produced nothing on windows
- test helpers now treat binary snippet keys and `FromFile` locations as posix

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

* incorporate windows path fix

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

* incorporate go-rpmdb close fix

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

* close squashfs fixture writers before finalizing

- windows dir listings can report a zero size for files still open, so the manifest was packed empty

(also adds a temporary windows diagnostic for the binary cataloger)

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

* temp: surface windows binary cataloger diagnostics

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

* fix: keep directory symlink targets posix on windows

- the directory indexer joined symlink targets with native separators, so links in the posix file tree pointed nowhere and globs on link names found nothing
- binary fixture logical keys used native separators, so nested snippets were picked as the fixture dir on windows

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

* keep addSymlinkToIndex under the funlen limit

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

* drop windows skips covered by the stereoscope and go-rpmdb bumps

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

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-10-05 10:41:03 -04:00
anchore-oss-update-botandanchore-oss-update-bot 6d2493bae4 chore(deps): update CPE dictionary index (#5375)
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-05 01:52:49 +00:00
dependabot[bot] bc68299565 chore(deps): bump github.com/klauspost/compress from 1.20.0 to 1.20.1 (#5369)
Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.20.0 to 1.20.1.
- [Release notes](https://github.com/klauspost/compress/releases)
- [Commits](https://github.com/klauspost/compress/compare/v1.20.0...v1.20.1)

---
updated-dependencies:
- dependency-name: github.com/klauspost/compress
  dependency-version: 1.20.1
  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-02 13:30:02 +00:00
Christopher Angelo PhillipsandAlex Goodman c33fa32cf1 fix(dotnet): resolve packages.lock dependencies deterministically (#5210)
* fix(dotnet): resolve packages.lock dependencies within their target framework

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>

* fix(dotnet): make packages.lock package metadata deterministic across target frameworks

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>

* fix: parse packages lock tests

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>

* fix: update lockfile parse to read into stable structure

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>

* fix: remove stale comments

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>

* test: new test for project entry case

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>

* fix(dotnet): key packages.lock packages case-insensitively

NuGet IDs are case-insensitive, and edge lookup already treated them that way, but package identity used the exact-case name. Two spellings of one ID and version across target frameworks became two packages, and type precedence never ran between them.

- packages merge on the lowercased name + version, display name comes from the winning entry
- a package listing itself (under any casing) no longer produces a self edge

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

---------

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-10-01 21:21:09 +00:00
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
134 changed files with 10099 additions and 368 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:
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
# these are checks that should be run on pull-request and merges to main.
# we do NOT want to kick off a release if these have not been verified on main.
# Please see the validations.yaml workflow for the names that should be used here.
checks: '["Acceptance tests (Linux)", "Acceptance tests (Mac)", "Build snapshot artifacts", "CLI tests (Linux)", "Integration tests", "Static analysis", "Unit tests"]'
checks: '["Acceptance tests (Linux)", "Acceptance tests (Mac)", "Build snapshot artifacts", "CLI tests (Linux)", "Containers-storage tests (Podman)", "Containers-storage tests (Buildah)", "Integration tests", "Static analysis", "Unit tests"]'
release:
needs: [check-gate, version-available]
+89 -4
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)"
@@ -85,16 +96,24 @@ jobs:
with:
persist-credentials: false
# go-make's setup only (not ./.github/actions/bootstrap): no apt, binny tools, or fixture cache are needed
# go-make's setup only (not ./.github/actions/bootstrap): binny ships no windows release, so binny tools
# are unavailable here (`make unit` skips the fixture refresh on windows)
- name: Setup go + go-make tooling
uses: anchore/go-make/.github/actions/setup@fa5421b3bf24c9d18f11a736c5eb4172f2b7d683 # v0.8.1
with:
# keep in sync with the go-version default in ./.github/actions/bootstrap
go-version: "1.26.2"
# the full unit suite leans on linux docker fixtures, so this runs the windows-relevant subset
# the binny-free version of `make download-test-fixture-cache` (keep the oras version in sync with .binny.yaml)
- name: Download test fixture cache
shell: bash
run: |
curl -sSfL https://github.com/oras-project/oras/releases/download/v1.3.4/oras_1.3.4_windows_amd64.zip -o "$RUNNER_TEMP/oras.zip"
unzip -q "$RUNNER_TEMP/oras.zip" oras.exe -d "$RUNNER_TEMP"
"$RUNNER_TEMP/oras.exe" pull ghcr.io/anchore/oss-cache/syft-test-fixture-cache:v2
- name: Run unit tests
run: make unit:windows
run: make unit
Integration-Test:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
@@ -158,6 +177,72 @@ jobs:
path: snapshot/
retention-days: 30
Containers-Storage-Podman:
name: "Containers-storage tests (Podman)"
needs: [Build-Snapshot-Artifacts]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1
with:
name: snapshot
path: snapshot
# go-make's setup only (not ./.github/actions/bootstrap): the test only needs make + the snapshot binary
- name: Setup go + go-make tooling
uses: anchore/go-make/.github/actions/setup@fa5421b3bf24c9d18f11a736c5eb4172f2b7d683 # v0.8.1
with:
# keep in sync with the go-version default in ./.github/actions/bootstrap
go-version: "1.26.2"
- name: Install Podman
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends -y jq podman uidmap
- name: Scan image from Podman's local store
run: make containers-storage-test
env:
BUILDER: podman
Containers-Storage-Buildah:
name: "Containers-storage tests (Buildah)"
needs: [Build-Snapshot-Artifacts]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1
with:
name: snapshot
path: snapshot
# go-make's setup only (not ./.github/actions/bootstrap): the test only needs make + the snapshot binary
- name: Setup go + go-make tooling
uses: anchore/go-make/.github/actions/setup@fa5421b3bf24c9d18f11a736c5eb4172f2b7d683 # v0.8.1
with:
# keep in sync with the go-version default in ./.github/actions/bootstrap
go-version: "1.26.2"
- name: Install Buildah
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends -y buildah jq uidmap
- name: Scan image from Buildah's local store
run: make containers-storage-test
env:
BUILDER: buildah
Acceptance-Linux:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
name: "Acceptance tests (Linux)"
+4
View File
@@ -1,6 +1,10 @@
version: "2"
run:
tests: false
# keep in sync with buildTags in .make/main.go
build-tags:
- containers_image_openpgp
- exclude_graphdriver_btrfs
linters:
default: none
enable:
+4 -6
View File
@@ -15,6 +15,7 @@ builds:
binary: syft
goos: [linux]
goarch: [amd64, arm64, ppc64le, riscv64, s390x]
tags: [containers_image_openpgp, exclude_graphdriver_btrfs]
mod_timestamp: &build-timestamp '{{ .CommitTimestamp }}'
ldflags: &build-ldflags |
-w
@@ -167,16 +168,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
+31 -15
View File
@@ -1,7 +1,6 @@
package main
import (
"os"
"path/filepath"
"runtime"
"strconv"
@@ -17,6 +16,12 @@ import (
"github.com/anchore/go-make/tasks/gotest"
)
// buildTags must match the linux build in .goreleaser.yaml so tests and lint compile the same code that ships
// (containers_image_openpgp compiles in stereoscope's real containers-storage provider instead of its stub).
// exclude_graphdriver_btrfs drops the cgo-only btrfs driver, which needs libbtrfs headers whenever cgo is on (e.g.
// under -race); release builds are CGO_ENABLED=0 and never include it anyway.
const buildTags = "containers_image_openpgp,exclude_graphdriver_btrfs"
func main() {
Makefile(
// shared anchore tasks
@@ -30,6 +35,7 @@ func main() {
gotest.Name("unit"),
gotest.ExcludeGlob("**/test/**"),
gotest.CoverageThreshold(62),
gotest.Tags(buildTags),
race(),
),
@@ -53,7 +59,7 @@ func main() {
raceFlag = " -race"
}
Run(
"go test -count=1 -timeout=30m"+raceFlag+" ./cmd/syft/internal/test/integration/...",
"go test -count=1 -timeout=30m -tags="+buildTags+raceFlag+" ./cmd/syft/internal/test/integration/...",
run.Env("GODEBUG", "dontfreezetheworld=1"),
)
},
@@ -67,7 +73,7 @@ func main() {
Log("race detector disabled (RACE=false); skipping race smoke")
return
}
Run("go run -race cmd/syft/main.go anchore/test_images:grype-quality-dotnet-69f15d2")
Run("go run -race -tags="+buildTags+" cmd/syft/main.go anchore/test_images:grype-quality-dotnet-69f15d2")
},
},
@@ -97,19 +103,20 @@ func main() {
},
},
// windows unit tests: deliberately not hooked into "test" (no RunsOn). The full unit suite leans on
// linux docker fixture images and posix-only assumptions, so this covers the packages that own
// host path handling (where windows bugs live) and skips the tests that need docker image fixtures.
// ponytail: curated package list, widen it as more of the suite is made windows-safe.
// containers-storage tests: build a fixture image with BUILDER (podman or buildah) into an isolated local store
// and scan it with the snapshot binary. Linux only, and not hooked into "test" since it needs the builder installed.
Task{
Name: "unit:windows",
Description: "run the windows-relevant subset of unit tests",
Name: "containers-storage-test",
Description: "Run containers-storage tests (BUILDER=podman|buildah)",
Run: func() {
Run("go test -count=1"+
` -skip "Image|Squash|AllLayers|MixFileTypes|GivenCoordinates"`+
" ./syft/internal/windows/... ./syft/internal/fileresolver/... ./syft/source/directorysource/... ./syft/source/filesource/..."+
" ./syft/file ./syft/file/cataloger/filedigest/... ./syft/file/cataloger/filecontent/...",
run.Stdout(os.Stderr),
bin := snapshotBinPath()
if !file.Exists(bin) {
Log("snapshot binary not found at %s; building single-target snapshot", bin)
Run("make snapshot:single-target")
}
Run(
"bash test/containers-storage/source-test.sh "+config.Env("BUILDER", "podman"),
run.Env("SYFT_BINARY_LOCATION", bin),
)
},
},
@@ -157,7 +164,7 @@ func main() {
Task{
Name: "unit:syft",
RunsOn: lang.List("unit"),
Dependencies: Deps("refresh-fixtures"),
Dependencies: unitFixtureDeps(),
},
Task{
Name: "clean:syft",
@@ -185,6 +192,15 @@ func raceEnabled() bool {
return config.CI && !config.Windows
}
// unitFixtureDeps skips the fixture cache refresh on windows: it needs binny-managed tools (binny ships
// no windows release) and linux docker, so fixture-backed tests there have to skip themselves instead.
func unitFixtureDeps() []string {
if config.Windows {
return nil
}
return Deps("refresh-fixtures")
}
// race applies raceEnabled() to a gotest suite. gotest exposes no functional option
// for the race detector, but gotest.Config.Race is exported.
func race() gotest.Option {
+12
View File
@@ -61,6 +61,18 @@ syft <image> -o cyclonedx-json
syft <image> -o spdx-json=./spdx.json -o cyclonedx-json=./cdx.json
```
### Local containers-storage images
On Linux, syft can scan images from a local containers-storage store (e.g. built with Buildah or Podman) when asked explicitly; plain image references never look there:
```bash
syft --from containers-storage localhost/myimage:latest
# rootless stores must be read from inside the builder's user namespace
podman unshare syft --from containers-storage localhost/myimage:latest
```
This is included in the Linux release binaries. When building from source, add `-tags containers_image_openpgp,exclude_graphdriver_btrfs`.
> [!TIP]
> **Check out the [Getting Started guide](https://oss.anchore.com/docs/guides/sbom/getting-started/)** to explore all of the capabilities and features.
+4
View File
@@ -114,6 +114,10 @@ func RunConvert(ctx context.Context, opts *ConvertOptions, userInput string) err
if err != nil {
return err
}
// output files are opened up front; release them if we bail out before writing
if c, ok := writer.(io.Closer); ok {
defer func() { _ = c.Close() }()
}
}
for _, output := range unchanged {
+5
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"reflect"
"strings"
@@ -181,6 +182,10 @@ func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, use
if err != nil {
return err
}
// output files are opened up front; release them if we bail out before writing
if c, ok := writer.(io.Closer); ok {
defer func() { _ = c.Close() }()
}
sources := opts.From
if len(sources) == 0 {
+10 -3
View File
@@ -4,6 +4,7 @@ import (
"io"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
@@ -22,9 +23,10 @@ func Test_defaultDir(t *testing.T) {
homeDir := filepath.Join(tmpDir, "fake-home")
tests := []struct {
name string
env map[string]string
expected string
name string
env map[string]string
expected string
skipOnWindows bool
}{
{
name: "no-xdg",
@@ -32,6 +34,8 @@ func Test_defaultDir(t *testing.T) {
"HOME": homeDir,
},
expected: homeDir,
// xdg falls back to LOCALAPPDATA on windows, not HOME
skipOnWindows: true,
},
{
name: "xdg-cache",
@@ -59,6 +63,9 @@ func Test_defaultDir(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.skipOnWindows && runtime.GOOS == "windows" {
t.Skip("posix-only cache dir fallback")
}
restoreCache(t)
for k, v := range test.env {
+2 -2
View File
@@ -36,7 +36,7 @@ var _ clio.PostLoader = (*imageSource)(nil)
func (o *sourceConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&o.File.Digests, `the file digest algorithms to use on the scanned file (options: "md5", "sha1", "sha224", "sha256", "sha384", "sha512")`)
descriptions.Add(&o.Image.DefaultPullSource, `allows users to specify which image source should be used to generate the sbom
valid values are: registry, docker, podman`)
valid values are: registry, docker, podman, containers-storage`)
}
type imageSource struct {
@@ -74,7 +74,7 @@ func (c *imageSource) PostLoad() error {
return checkDefaultSourceValues(c.DefaultPullSource)
}
var validDefaultSourceValues = []string{"registry", "docker", "podman", ""}
var validDefaultSourceValues = []string{"registry", "docker", "podman", "containers-storage", ""}
func checkDefaultSourceValues(source string) error {
validValues := strset.New(validDefaultSourceValues...)
+28 -4
View File
@@ -2,10 +2,11 @@ package options
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"sort"
"strings"
@@ -20,7 +21,10 @@ import (
"github.com/anchore/syft/syft/sbom"
)
var _ sbom.Writer = (*sbomMultiWriter)(nil)
var _ interface {
io.Closer
sbom.Writer
} = (*sbomMultiWriter)(nil)
var _ interface {
io.Closer
@@ -164,6 +168,12 @@ func newSBOMMultiWriter(options ...sbomWriterDescription) (_ *sbomMultiWriter, e
}
out := &sbomMultiWriter{}
defer func() {
// don't leak the files already opened when a later output fails
if err != nil {
_ = out.Close()
}
}()
for _, option := range options {
switch len(option.Path) {
@@ -172,8 +182,8 @@ func newSBOMMultiWriter(options ...sbomWriterDescription) (_ *sbomMultiWriter, e
format: option.Format,
})
default:
// create any missing subdirectories
dir := path.Dir(option.Path)
// create any missing subdirectories (option.Path is a host path, so this must be filepath)
dir := filepath.Dir(option.Path)
if dir != "" {
s, err := os.Stat(dir)
if err != nil {
@@ -210,6 +220,20 @@ func (m *sbomMultiWriter) Write(s sbom.SBOM) (errs error) {
return errs
}
// Close releases any files held by the writers. Write already closes each file it writes to, so this matters
// when the SBOM is never written (e.g. an error after the writer was created).
func (m *sbomMultiWriter) Close() error {
var errs error
for _, w := range m.writers {
if c, ok := w.(io.Closer); ok {
if err := c.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
errs = multierror.Append(errs, err)
}
}
}
return errs
}
// sbomStreamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup
type sbomStreamWriter struct {
format sbom.FormatEncoder
+2 -2
View File
@@ -202,9 +202,9 @@ func Test_newSBOMMultiWriter(t *testing.T) {
if test.err {
assert.Error(t, err)
return
} else {
assert.NoError(t, err)
}
require.NoError(t, err)
t.Cleanup(func() { _ = mw.Close() })
assert.Len(t, mw.writers, len(test.expected))
@@ -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"
}
+58 -34
View File
@@ -18,16 +18,16 @@ require (
github.com/anchore/go-homedir v0.1.1
github.com/anchore/go-logger v0.2.0
github.com/anchore/go-macholibre v0.1.1
github.com/anchore/go-rpmdb v0.2.0
github.com/anchore/go-rpmdb v0.2.2
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.4
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
@@ -62,7 +62,7 @@ require (
github.com/jedib0t/go-pretty/v6 v6.8.3
github.com/jinzhu/copier v0.4.0
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
github.com/klauspost/compress v1.20.0
github.com/klauspost/compress v1.20.1
github.com/magiconair/properties v1.18.12
github.com/mholt/archives v0.1.5
github.com/moby/sys/mountinfo v0.7.2
@@ -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.2+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.1 // 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.2 // indirect
github.com/sigstore/sigstore v1.10.11 // 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/sif/v2 v2.24.2 // 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
+169 -75
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=
@@ -136,8 +140,8 @@ github.com/anchore/go-lzo v0.1.1 h1:IwL/fvkdtlIrYIXck6WxZ3nb8WjjHziYYmGxlooyOnM=
github.com/anchore/go-lzo v0.1.1/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=
github.com/anchore/go-macholibre v0.1.1 h1:sTBHi3S1WVIOCamc6NyT8Sn9mBmD30DCTfjfVm4k6/M=
github.com/anchore/go-macholibre v0.1.1/go.mod h1:YNq2610RlvGCK0Za+Klz/OCMtUaEnwwAGA/VfUAQaro=
github.com/anchore/go-rpmdb v0.2.0 h1:ixcyMg/guhBL9XjBVwhTv9CG48xO07Qkg2uDX2+zx/c=
github.com/anchore/go-rpmdb v0.2.0/go.mod h1:ATsRlpCXstnoYfzqBfhwGw0U2dolx1BBQ9rAU8jWBAw=
github.com/anchore/go-rpmdb v0.2.2 h1:44VC7mvFAxV9t5JUZYg3S9sqG+E94iZvyWACMDq9hSk=
github.com/anchore/go-rpmdb v0.2.2/go.mod h1:Y64EbetdqhryMjtk8IzXkdCQw/aNDx+feZCT9aj6rjs=
github.com/anchore/go-struct-converter v0.2.0-rc2 h1:q+859fW2/jbHJHB2etbNfRlFwYpknyvbqqk1hUdamQ4=
github.com/anchore/go-struct-converter v0.2.0-rc2/go.mod h1:cDBA5vhcR62nXWo8QH9/Kk2807o65ISaHPNPX66L+Uw=
github.com/anchore/go-sync v0.1.2 h1:RDD5RCanrWqhbtBulqPSZpERadUlgcYjpybYzYd3Uk8=
@@ -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.4 h1:dT5AfjCTXUJowMiRIc+VBQVwvwnIEwnxxwA1GwAkMxw=
github.com/anchore/stereoscope v0.3.4/go.mod h1:+HxqHTt7dvH8mea0mf3ulqkf084VvHiT1grpAQf5uEs=
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.2+incompatible h1:2zgdFuoFst2T80oS42vhKGxkd0JRo305eIEKTsxR7pQ=
github.com/docker/cli v29.8.2+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=
@@ -412,8 +428,8 @@ github.com/gkampitakis/ciinfo v0.3.4 h1:5eBSibVuSMbb/H6Elc0IIEFbkzCJi3lm94n0+U7Z
github.com/gkampitakis/ciinfo v0.3.4/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/go-snaps v0.5.23 h1:okh5QR48zpUjpWtu65AtqxdCY8huJq+dEDuUzd1PuKg=
github.com/gkampitakis/go-snaps v0.5.23/go.mod h1:uy3lVzCCRRsAwYqSocyw5fY8xRLCYEfqoOJNxr8HonM=
github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0=
github.com/glebarez/go-sqlite v1.23.0 h1:FyhIq4jqmgphQAUlY79zPldYGwISEZikaDfhiGWkkaI=
github.com/glebarez/go-sqlite v1.23.0/go.mod h1:IIYrOH3L0rHY3jb4IXOHoWdklNajSGUN2eJcvK8WrnI=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
@@ -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=
@@ -645,8 +666,8 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
@@ -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.1 h1:ayahDPjfSIqKegyt5YVGEvQ7SAi72GHXTy+b2YjUd5w=
github.com/secure-systems-lab/go-securesystemslib v0.11.1/go.mod h1:UyOjhoZLi76ir63u1ptubEevVe6dlNPmNxwgVvKqFFY=
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.2 h1:RSWWUY8QrVTxbYH00jY/jg2e7YnjzrpwP+PeHTMll0E=
github.com/sigstore/protobuf-specs v0.5.2/go.mod h1:DRBzpFuE+LnvQMN10/dU6nBeKwVLGEQ6o2FovN2Rats=
github.com/sigstore/sigstore v1.10.11 h1:fjrwDfR0AWmRONYV/hKGJyqbT0osMtaBz9FTZ6MH9XI=
github.com/sigstore/sigstore v1.10.11/go.mod h1:ecPk+ftLqP8/TmzQFJmn0+QrL2yUQIJvyzVKNmDiw6U=
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=
@@ -925,12 +970,14 @@ github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUm
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/sylabs/sif/v2 v2.24.1 h1:OhTOfTwBaGXfbWYWXK9C6Pojma3A1bTaNJ6CEhyuKok=
github.com/sylabs/sif/v2 v2.24.1/go.mod h1:PRq9MoP0g+p0qE5ZRGrOhyVAUdHrPsAVmXHgoiW6VU0=
github.com/sylabs/sif/v2 v2.24.2 h1:rdkFf6z3xLC6yKweU7S9marlKfkS91Ht0ruIri6YWFY=
github.com/sylabs/sif/v2 v2.24.2/go.mod h1:bbrkk0JhTX+xNJcnUtmDkivoO2r4DGWM3LxNR0U2ooA=
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
+4
View File
@@ -6,6 +6,7 @@ import (
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
@@ -95,6 +96,9 @@ func Test_makeDiskKey(t *testing.T) {
}
func Test_errors(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("windows ignores unix permission bits on directories")
}
tmp := t.TempDir()
cache := filepath.Join(tmp, "cache")
// make a non-writable directory
@@ -212,7 +212,7 @@ func parseGenericCatalogerFunction(funcDecl *ast.FuncDecl, filePath, repoRoot st
// extractPackageNameFromPath extracts the package name from a cataloger source file path
// e.g., "syft/pkg/cataloger/swift/cataloger.go" -> "swift"
func extractPackageNameFromPath(filePath string) string {
parts := strings.Split(filePath, string(filepath.Separator))
parts := strings.Split(filepath.ToSlash(filePath), "/")
// find the LAST occurrence of "cataloger" in the path
// (to handle test fixtures with multiple "cataloger" segments)
@@ -4,6 +4,7 @@ import (
"go/ast"
"go/parser"
"go/token"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
@@ -337,7 +338,7 @@ func TestResolveImportPath(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := resolveImportPath(tt.importPath, testRepoRoot)
require.Equal(t, tt.want, got)
require.Equal(t, filepath.FromSlash(tt.want), got)
})
}
}
+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
)
+4 -3
View File
@@ -4,7 +4,7 @@ import (
"errors"
"io/fs"
"os"
"path/filepath"
"path"
"strings"
"github.com/diskfs/go-diskfs/filesystem"
@@ -25,7 +25,8 @@ func WalkDiskDir(fsys filesystem.FileSystem, root string, fn WalkDiskDirFunc) er
}
for _, info := range infos {
p := filepath.Join(root, info.Name())
// squashfs paths are posix regardless of host os
p := path.Join(root, info.Name())
err = walkDiskDir(fsys, p, info, fn)
if err != nil {
if errors.Is(err, fs.SkipDir) {
@@ -74,7 +75,7 @@ func walkDiskDir(fsys filesystem.FileSystem, name string, d os.FileInfo, walkDir
}
for _, d1 := range dirs {
name1 := filepath.Join(name, d1.Name())
name1 := path.Join(name, d1.Name())
if err := walkDiskDir(fsys, name1, d1, walkDirFn); err != nil {
if errors.Is(err, fs.SkipDir) {
break
+2
View File
@@ -19,6 +19,8 @@ func createTestFS(t *testing.T) filesystem.FileSystem {
filename := "test.squashfs"
f, err := os.Create(filepath.Join(dir, filename))
require.NoError(t, err)
// windows can't remove the temp dir while the backing file is open
t.Cleanup(func() { _ = f.Close() })
b := file.New(f, false)
fsys, err := squashfs.Create(b, 0, 0, 4096)
+2 -1
View File
@@ -84,7 +84,8 @@ func ExtractGlobsFromTarToUniqueTempFile(ctx context.Context, archivePath, dir s
func matchesAnyGlob(name string, globs ...string) bool {
for _, glob := range globs {
if matches, err := doublestar.PathMatch(glob, name); err == nil && matches {
// tar entry names are always posix, so don't match with the host separator (PathMatch)
if matches, err := doublestar.Match(glob, name); err == nil && matches {
return true
}
}
+2
View File
@@ -16,6 +16,7 @@ import (
"github.com/anchore/syft/internal/os"
"github.com/anchore/syft/internal/sbomsync"
"github.com/anchore/syft/internal/task"
"github.com/anchore/syft/internal/testutils"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/linux"
@@ -84,6 +85,7 @@ func Test_EnvironmentTask(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
tarPath := imagetest.GetFixtureImageTarPath(t, test.name)
// get the source
+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(
+30
View File
@@ -0,0 +1,30 @@
package testutils
import (
"os/exec"
"runtime"
"strings"
"sync"
"testing"
)
var dockerServerOS = sync.OnceValue(func() string {
out, err := exec.Command("docker", "version", "--format", "{{.Server.Os}}").Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
})
// SkipWithoutLinuxContainers skips tests whose fixtures are linux container images (built or run via docker).
// This only kicks in on windows, where the docker daemon is often running windows containers (or is absent),
// so the linux fixture images can't be built. Everywhere else the fixture tooling is left to fail loudly.
func SkipWithoutLinuxContainers(t testing.TB) {
t.Helper()
if runtime.GOOS != "windows" {
return
}
if os := dockerServerOS(); os != "linux" {
t.Skipf("linux container fixtures unavailable (docker server os=%q)", os)
}
}
+2 -1
View File
@@ -265,8 +265,9 @@ func TestLazyCreation(t *testing.T) {
// root dir is not created until needed
assert.Equal(t, "", td.root)
_, _, err := td.NewFile("trigger-*")
f, _, err := td.NewFile("trigger-*")
require.NoError(t, err)
require.NoError(t, f.Close())
assert.NotEmpty(t, td.root)
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"
},
@@ -15,6 +15,7 @@ import (
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/imagetest"
intFile "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/testutils"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source"
@@ -92,6 +93,7 @@ func TestDigestsCataloger(t *testing.T) {
func TestDigestsCataloger_MixFileTypes(t *testing.T) {
testImage := "image-file-type-mix"
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
@@ -162,6 +164,7 @@ func TestDigestsCataloger_MixFileTypes(t *testing.T) {
func TestFileDigestCataloger_GivenCoordinates(t *testing.T) {
testImage := "image-file-type-mix"
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
c := NewCataloger([]crypto.Hash{crypto.SHA256})
@@ -10,6 +10,7 @@ import (
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/internal/testutils"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/stereoscopesource"
@@ -18,6 +19,7 @@ import (
func TestFileMetadataCataloger(t *testing.T) {
testImage := "image-file-type-mix"
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
c := NewCataloger()
@@ -157,6 +159,7 @@ func TestFileMetadataCataloger(t *testing.T) {
func TestFileMetadataCataloger_GivenCoordinates(t *testing.T) {
testImage := "image-file-type-mix"
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
c := NewCataloger()
@@ -2,6 +2,7 @@ package internal
import (
"context"
"runtime"
"testing"
"github.com/google/go-cmp/cmp"
@@ -10,6 +11,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/internal/testutils"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/directorysource"
@@ -19,15 +21,16 @@ import (
func Test_allRegularFiles(t *testing.T) {
tests := []struct {
name string
setup func() file.Resolver
setup func(t *testing.T) file.Resolver
wantRealPaths *strset.Set
wantAccessPaths *strset.Set
}{
{
name: "image",
setup: func() file.Resolver {
setup: func(t *testing.T) file.Resolver {
testImage := "image-file-type-mix"
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
s := stereoscopesource.New(img, stereoscopesource.ImageConfig{
@@ -44,7 +47,12 @@ func Test_allRegularFiles(t *testing.T) {
},
{
name: "directory",
setup: func() file.Resolver {
setup: func(t *testing.T) file.Resolver {
if runtime.GOOS == "windows" {
// file symlinks from a git checkout don't index as access paths on windows yet (same gap the
// fileresolver symlink fixtures skip for)
t.Skip("fixture relies on symlinks")
}
s, err := directorysource.NewFromPath("testdata/symlinked-root/nested/link-root")
require.NoError(t, err)
r, err := s.FileResolver(source.SquashedScope)
@@ -57,7 +65,7 @@ func Test_allRegularFiles(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resolver := tt.setup()
resolver := tt.setup(t)
locations := AllRegularFiles(context.Background(), resolver)
realLocations := strset.New()
virtualLocations := strset.New()
+5
View File
@@ -2,6 +2,7 @@ package github
import (
"flag"
"runtime"
"testing"
"github.com/anchore/syft/syft/format/internal/testutil"
@@ -11,6 +12,10 @@ var updateSnapshot = flag.Bool("update-github", false, "update the *.golden file
var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing")
func TestGithubDirectoryEncoder(t *testing.T) {
if runtime.GOOS == "windows" {
// the golden snapshot embeds the posix source path (and the source ID derived from it)
t.Skip("directory snapshot is posix path specific")
}
dir := t.TempDir()
testutil.AssertEncoderAgainstGoldenSnapshot(t,
testutil.EncoderSnapshotTestConfig{
@@ -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,
@@ -11,6 +11,7 @@ import (
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/internal/testutils"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/linux"
@@ -35,6 +36,7 @@ func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBO
case true:
img = imagetest.GetGoldenFixtureImage(t, testImage)
default:
testutils.SkipWithoutLinuxContainers(t)
img = imagetest.GetFixtureImage(t, "docker-archive", testImage)
}
+5
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"flag"
"runtime"
"strings"
"testing"
@@ -90,6 +91,10 @@ func TestEscapeHTML(t *testing.T) {
}
func TestDirectoryEncoder(t *testing.T) {
if runtime.GOOS == "windows" {
// the golden snapshot embeds the posix source path (and the source ID derived from it)
t.Skip("directory snapshot is posix path specific")
}
cfg := DefaultEncoderConfig()
cfg.Pretty = true
enc, err := NewFormatEncoderWithConfig(cfg)
+5
View File
@@ -2,6 +2,7 @@ package text
import (
"flag"
"runtime"
"testing"
"github.com/anchore/syft/syft/format/internal/testutil"
@@ -11,6 +12,10 @@ var updateSnapshot = flag.Bool("update-text", false, "update the *.golden files
var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing")
func TestTextDirectoryEncoder(t *testing.T) {
if runtime.GOOS == "windows" {
// the golden snapshot embeds the posix source path (and the source ID derived from it)
t.Skip("directory snapshot is posix path specific")
}
dir := t.TempDir()
testutil.AssertEncoderAgainstGoldenSnapshot(t,
testutil.EncoderSnapshotTestConfig{
+16
View File
@@ -3,6 +3,7 @@ package syft
import (
"crypto"
"fmt"
"slices"
"github.com/anchore/go-collections"
"github.com/anchore/stereoscope/pkg/image"
@@ -10,6 +11,11 @@ import (
"github.com/anchore/syft/syft/source/sourceproviders"
)
// GetSourceConfig controls which source providers are tried, and in what order, when resolving user input to a source.
//
// The "containers-storage" provider (local Buildah / Podman store) is never part of automatic resolution: it is only
// used when named in Sources or DefaultImagePullSource. It is also only functional when built with the
// containers_image_openpgp build tag (as syft's Linux release binaries are); otherwise it always returns an error.
type GetSourceConfig struct {
// SourceProviderConfig may optionally be provided to be used when constructing the default set of source providers, unused if All specified
SourceProviderConfig *sourceproviders.Config
@@ -64,6 +70,12 @@ func (c *GetSourceConfig) WithDefaultImagePullSource(defaultImagePullSource stri
func (c *GetSourceConfig) getProviders(userInput string) ([]source.Provider, error) {
providers := collections.TaggedValueSet[source.Provider]{}.Join(sourceproviders.All(userInput, c.SourceProviderConfig)...)
// opening a containers-storage store runs full graph driver init (mounts, locks, possible store writes), which is
// too invasive for an "is this image local?" probe on every plain image reference. Only use it when asked by name.
if !c.requestsSource(image.ContainersStorageSource) {
providers = providers.Remove(image.ContainersStorageSource)
}
// if the "default image pull source" is set, we move this as the first pull source
if c.DefaultImagePullSource != "" {
base := providers.Remove(sourceproviders.PullTag)
@@ -84,6 +96,10 @@ func (c *GetSourceConfig) getProviders(userInput string) ([]source.Provider, err
return providers.Values(), nil
}
func (c *GetSourceConfig) requestsSource(name string) bool {
return c.DefaultImagePullSource == name || slices.Contains(c.Sources, name)
}
func DefaultGetSourceConfig() *GetSourceConfig {
return &GetSourceConfig{
SourceProviderConfig: sourceproviders.DefaultConfig(),
+61 -2
View File
@@ -1,9 +1,15 @@
package syft
import (
"slices"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/sourceproviders"
)
@@ -17,8 +23,9 @@ func TestGetProviders_DefaultImagePullSource(t *testing.T) {
t.Errorf("Expected no error for DefaultImagePullSource parameter, got: %v", err)
}
if len(providers) != len(allSourceProviders) {
t.Errorf("Expected %d providers, got %d", len(allSourceProviders), len(providers))
// everything except containers-storage, which is only used when requested by name
if len(providers) != len(allSourceProviders)-1 {
t.Errorf("Expected %d providers, got %d", len(allSourceProviders)-1, len(providers))
}
}
@@ -36,3 +43,55 @@ func TestGetProviders_Sources(t *testing.T) {
t.Errorf("Expected 2 providers, got %d", len(providers))
}
}
func TestGetProviders_ContainersStorageOnlyWhenRequested(t *testing.T) {
const storage = image.ContainersStorageSource
tests := []struct {
name string
cfg *GetSourceConfig
wantFound bool
// if set, containers-storage must be ordered ahead of this provider
wantBefore string
}{
{
name: "excluded from automatic resolution",
cfg: DefaultGetSourceConfig(),
},
{
name: "excluded when selecting all pull sources",
cfg: DefaultGetSourceConfig().WithSources(sourceproviders.PullTag),
},
{
name: "excluded when a different default pull source is set",
cfg: DefaultGetSourceConfig().WithDefaultImagePullSource(stereoscope.RegistryTag),
},
{
name: "included when explicitly selected",
cfg: DefaultGetSourceConfig().WithSources(storage),
wantFound: true,
},
{
name: "included ahead of other pull sources when it is the default pull source",
cfg: DefaultGetSourceConfig().WithDefaultImagePullSource(storage),
wantFound: true,
wantBefore: image.DockerDaemonSource,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
providers, err := tt.cfg.getProviders("localhost/myimage:latest")
require.NoError(t, err)
require.NotEmpty(t, providers)
assert.Equal(t, tt.wantFound, providerIndex(providers, storage) >= 0)
if tt.wantBefore != "" {
assert.Less(t, providerIndex(providers, storage), providerIndex(providers, tt.wantBefore))
}
})
}
}
func providerIndex(providers []source.Provider, name string) int {
return slices.IndexFunc(providers, func(p source.Provider) bool { return p.Name() == name })
}
+7 -2
View File
@@ -210,8 +210,13 @@ func (r ChrootContext) ToChrootPath(nativePath string) string {
prefix = r.base
} else {
// we need to account for the cwd relative to the running process and the given root for the directory resolver
prefix = filepath.Clean(filepath.Join(r.cwd, r.cwdRelativeToRoot))
prefix += string(filepath.Separator)
prefix = r.cwdRelativeToRoot
// on windows an absolute root stays absolute here (see ChangeDirectory), and joining it onto the cwd
// would nest one volume inside another (D:\cwd\C:\root)
if !filepath.IsAbs(prefix) {
prefix = filepath.Join(r.cwd, prefix)
}
prefix = filepath.Clean(prefix) + string(filepath.Separator)
}
responsePath = strings.TrimPrefix(responsePath, prefix)
}
@@ -0,0 +1,17 @@
package fileresolver
import (
"testing"
"github.com/stretchr/testify/assert"
)
// windows CI runs from D: while t.TempDir() lives on C:, so the root and cwd don't share a volume
func TestChrootContext_ToChrootPath_rootOnAnotherVolume(t *testing.T) {
ctx := ChrootContext{
root: `C:\scan\root`,
cwd: `D:\work`,
cwdRelativeToRoot: `C:\scan\root`,
}
assert.Equal(t, "dir/file.txt", ctx.ToChrootPath("/c/scan/root/dir/file.txt"))
}
@@ -12,6 +12,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/internal/testutils"
"github.com/anchore/syft/syft/file"
)
@@ -91,6 +92,7 @@ func TestAllLayersResolver_FilesByPath(t *testing.T) {
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := NewFromContainerImageAllLayers(img)
@@ -201,6 +203,7 @@ func TestAllLayersResolver_FilesByGlob(t *testing.T) {
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := NewFromContainerImageAllLayers(img)
@@ -249,6 +252,7 @@ func Test_imageAllLayersResolver_FilesByMIMEType(t *testing.T) {
}
for _, test := range tests {
t.Run(test.fixtureName, func(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureName)
resolver, err := NewFromContainerImageAllLayers(img)
@@ -266,6 +270,7 @@ func Test_imageAllLayersResolver_FilesByMIMEType(t *testing.T) {
}
func Test_imageAllLayersResolver_hasFilesystemIDInLocation(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-duplicate-path")
resolver, err := NewFromContainerImageAllLayers(img)
@@ -326,6 +331,7 @@ func TestAllLayersImageResolver_FilesContents(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := NewFromContainerImageAllLayers(img)
@@ -353,6 +359,7 @@ func TestAllLayersImageResolver_FilesContents(t *testing.T) {
func TestAllLayersImageResolver_FilesContents_errorOnDirRequest(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := NewFromContainerImageAllLayers(img)
@@ -500,6 +507,7 @@ func Test_imageAllLayersResolver_resolvesLinks(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := NewFromContainerImageAllLayers(img)
@@ -514,6 +522,7 @@ func Test_imageAllLayersResolver_resolvesLinks(t *testing.T) {
}
func TestAllLayersResolver_AllLocations(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-files-deleted")
resolver, err := NewFromContainerImageAllLayers(img)
@@ -16,6 +16,7 @@ import (
"go.uber.org/goleak"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/internal/testutils"
"github.com/anchore/syft/syft/file"
)
@@ -115,6 +116,7 @@ func Test_ContainerImageDeepSquash_FilesByPath(t *testing.T) {
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := NewFromContainerImageDeepSquash(img)
@@ -231,6 +233,7 @@ func Test_ContainerImageDeepSquash_FilesByGlob(t *testing.T) {
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := NewFromContainerImageDeepSquash(img)
@@ -288,6 +291,7 @@ func Test_ContainerImageDeepSquash_FilesByMIMEType(t *testing.T) {
for _, test := range tests {
t.Run(test.fixtureName, func(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureName)
resolver, err := NewFromContainerImageDeepSquash(img)
@@ -305,6 +309,7 @@ func Test_ContainerImageDeepSquash_FilesByMIMEType(t *testing.T) {
}
func Test_ContainerImageDeepSquash_hasFilesystemIDInLocation(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-duplicate-path")
resolver, err := NewFromContainerImageDeepSquash(img)
@@ -365,6 +370,7 @@ func Test_ContainerImageDeepSquash_FilesContents(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := NewFromContainerImageDeepSquash(img)
@@ -389,6 +395,7 @@ func Test_ContainerImageDeepSquash_FilesContents(t *testing.T) {
}
func Test_ContainerImageDeepSquash_FilesContents_errorOnDirRequest(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := NewFromContainerImageDeepSquash(img)
@@ -557,6 +564,7 @@ func Test_ContainerImageDeepSquash_resolvesLinks(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := NewFromContainerImageDeepSquash(img)
@@ -571,6 +579,7 @@ func Test_ContainerImageDeepSquash_resolvesLinks(t *testing.T) {
}
func Test_ContainerImageDeepSquash_AllLocations(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-files-deleted")
resolver, err := NewFromContainerImageDeepSquash(img)
@@ -16,6 +16,7 @@ import (
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/internal/testutils"
"github.com/anchore/syft/syft/file"
)
@@ -75,6 +76,7 @@ func TestImageSquashResolver_FilesByPath(t *testing.T) {
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := NewFromContainerImageSquash(img)
@@ -184,6 +186,7 @@ func TestImageSquashResolver_FilesByGlob(t *testing.T) {
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := NewFromContainerImageSquash(img)
@@ -241,6 +244,7 @@ func Test_imageSquashResolver_FilesByMIMEType(t *testing.T) {
for _, test := range tests {
t.Run(test.fixtureName, func(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureName)
resolver, err := NewFromContainerImageSquash(img)
@@ -258,6 +262,7 @@ func Test_imageSquashResolver_FilesByMIMEType(t *testing.T) {
}
func Test_imageSquashResolver_hasFilesystemIDInLocation(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-duplicate-path")
resolver, err := NewFromContainerImageSquash(img)
@@ -316,6 +321,7 @@ func TestSquashImageResolver_FilesContents(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := NewFromContainerImageSquash(img)
@@ -341,6 +347,7 @@ func TestSquashImageResolver_FilesContents(t *testing.T) {
func TestSquashImageResolver_FilesContents_errorOnDirRequest(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := NewFromContainerImageSquash(img)
@@ -499,6 +506,7 @@ func Test_imageSquashResolver_resolvesLinks(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := NewFromContainerImageSquash(img)
@@ -545,6 +553,7 @@ func locationSorter(a, b file.Location) int {
}
func TestSquashResolver_AllLocations(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-files-deleted")
resolver, err := NewFromContainerImageSquash(img)
@@ -580,6 +589,7 @@ func TestSquashResolver_AllLocations(t *testing.T) {
// content and metadata), so that image results are in parity with directory results (which cannot distinguish a
// hardlink from a regular file). this must hold for both the squashed and all-layers resolvers.
func TestImageResolvers_Hardlinks(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-hardlinks")
resolvers := map[string]file.Resolver{}
+14 -14
View File
@@ -351,17 +351,12 @@ func (r directoryIndexer) addFileToIndex(p string, info os.FileInfo) error {
func (r directoryIndexer) addSymlinkToIndex(p string, info os.FileInfo) (string, error) {
linkTarget, err := os.Readlink(p)
if err != nil {
isOnWindows := windows.HostRunningOnWindows()
if isOnWindows {
p = windows.FromPosix(p)
}
linkTarget, err = filepath.EvalSymlinks(p)
if isOnWindows {
p = windows.ToPosix(p)
nativePath := p
if windows.HostRunningOnWindows() {
nativePath = windows.FromPosix(p)
}
linkTarget, err = filepath.EvalSymlinks(nativePath)
if err != nil {
return "", fmt.Errorf("unable to readlink for path=%q: %w", p, err)
}
@@ -414,16 +409,21 @@ func (r directoryIndexer) addSymlinkToIndex(p string, info os.FileInfo) (string,
}
}
ref, err := r.tree.AddSymLink(file.Path(p), file.Path(linkTarget))
if err != nil {
return "", err
}
targetAbsPath := linkTarget
if !filepath.IsAbs(targetAbsPath) {
targetAbsPath = filepath.Clean(filepath.Join(path.Dir(p), linkTarget))
}
// the tree is posix (like p), but the joins above produce native separators on windows
if windows.HostRunningOnWindows() {
linkTarget = windows.ToPosix(linkTarget)
}
ref, err := r.tree.AddSymLink(file.Path(p), file.Path(linkTarget))
if err != nil {
return "", err
}
metadata := NewMetadataFromPath(p, info)
metadata.LinkDestination = linkTarget
r.index.Add(*ref, metadata)
@@ -35,9 +35,19 @@ func nativeOSFileOpener(ref stereoscopeFile.Reference) (io.ReadCloser, error) {
return stereoscopeFile.NewLazyReadCloser(filePath), nil
}
// virtual reports whether the tree is not backed by the host filesystem (e.g. a squashfs), in which case there is
// no chroot and request/response paths are already the posix tree paths. Mapping them through the host (as the
// chroot does) would root them on the current volume on windows (/payload.txt -> /d/payload.txt).
func (r FiletreeResolver) virtual() bool {
return r.Chroot == ChrootContext{}
}
// requestPath converts a user (chroot) path into the path the file tree is keyed on. The tree is always posix
// (windows paths are volume-encoded, e.g. /c/some/path), while ToNativePath yields a native host path.
func (r *FiletreeResolver) requestPath(userPath string) (string, error) {
if r.virtual() {
return userPath, nil
}
nativePath, err := r.Chroot.ToNativePath(userPath)
if err != nil {
return "", err
@@ -50,6 +60,9 @@ func (r *FiletreeResolver) requestPath(userPath string) (string, error) {
// responsePath takes a path from the underlying fs domain and converts it to a path that is relative to the root of the file resolver.
func (r FiletreeResolver) responsePath(path string) string {
if r.virtual() {
return path
}
return r.Chroot.ToChrootPath(path)
}
@@ -111,6 +124,9 @@ func (r FiletreeResolver) FilesByPath(userPaths ...string) ([]file.Location, err
// requestGlob is the glob equivalent of requestPath.
func (r FiletreeResolver) requestGlob(pattern string) (string, error) {
if r.virtual() {
return pattern, nil
}
glob, err := r.Chroot.ToNativeGlob(pattern)
if err != nil {
return "", err
@@ -8,6 +8,7 @@ import (
"io"
"math"
"os"
"path"
"path/filepath"
"slices"
"strings"
@@ -522,14 +523,15 @@ func TestSafeTensorsMergeProcessor(t *testing.T) {
t.Run("dir scan: config.json _name_or_path beats the parent directory fallback", func(t *testing.T) {
// When a sibling config.json carries _name_or_path
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"),
// (location paths are posix, so keep the temp dir slash-separated for windows)
dir := filepath.ToSlash(t.TempDir())
require.NoError(t, os.WriteFile(path.Join(dir, "config.json"),
[]byte(`{"_name_or_path":"org/preferred-name"}`), 0o644))
stPath := filepath.Join(dir, "weights.safetensors")
stPath := path.Join(dir, "weights.safetensors")
p := dirPkg(stPath, pkg.SafeTensorsModelInfo{
Format: "safetensors", TensorCount: 1, MetadataHash: "abc",
})
resolver := file.NewMockResolverForPaths(filepath.Join(dir, "config.json"))
resolver := file.NewMockResolverForPaths(path.Join(dir, "config.json"))
out, _, err := safeTensorsMergeProcessor(context.Background(), resolver, []pkg.Package{p}, nil, nil)
require.NoError(t, err)
require.Len(t, out, 1)
+4 -2
View File
@@ -114,7 +114,7 @@ func fetchPkgFiles(base string, resolver file.Resolver) ([]pkg.AlpmFileRecord, f
func fetchBackupFiles(base string, resolver file.Resolver) ([]pkg.AlpmFileRecord, file.Location, error) {
// We only really do this to get any backup database entries from the files database
target := filepath.Join(base, "files")
target := path.Join(base, "files")
loc, err := getLocation(target, resolver)
if err != nil {
@@ -395,7 +395,9 @@ func parseMtreeWithLimits(r io.Reader, maxSize int64, maxLines int) ([]pkg.AlpmF
if ok := ignoredFiles[f.Name]; ok {
continue
}
path := fmt.Sprintf("/%s", f.Name)
// go-mtree runs filepath.Clean on entry names, which turns them into host paths on windows.
// ponytail: a vis-encoded backslash (\134) in a name gets mangled on windows only, unvis first if that matters
path := fmt.Sprintf("/%s", filepath.ToSlash(f.Name))
fileFields["path"] = path
for _, kv := range f.Keywords {
kw := string(kv.Keyword())
+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*'
@@ -17,6 +17,7 @@ import (
"github.com/anchore/packageurl-go"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/internal/testutils"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
@@ -460,8 +461,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 +474,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 +487,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"),
},
@@ -3169,6 +3170,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureImage)
src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
Reference: test.fixtureImage,
@@ -3187,6 +3189,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",
@@ -1,6 +1,7 @@
package config
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@@ -123,10 +124,10 @@ func TestAllStorePaths(t *testing.T) {
},
dest: "dest",
expected: []string{
"dest/test/1.0/platform1/test1",
"dest/test/1.0/platform1/test2",
"dest/test/1.0/platform2/test1",
"dest/test/1.0/platform2/test2",
filepath.FromSlash("dest/test/1.0/platform1/test1"),
filepath.FromSlash("dest/test/1.0/platform1/test2"),
filepath.FromSlash("dest/test/1.0/platform2/test1"),
filepath.FromSlash("dest/test/1.0/platform2/test2"),
},
},
}
@@ -4,6 +4,7 @@ import (
"fmt"
"iter"
"os"
"path"
"path/filepath"
"slices"
"sort"
@@ -167,7 +168,7 @@ func getLogicalKey(managedBinaryPath string) (*LogicalEntryKey, error) {
OrgName: items[0],
Version: items[1],
Platform: items[2],
Filename: filepath.Join(items[3:]...),
Filename: path.Join(items[3:]...), // posix, since this is part of the logical key
}, nil
}
@@ -64,8 +64,8 @@ func TestListAllBinaries(t *testing.T) {
assert.Equal(t,
Entries{
LogicalEntryKey{OrgName: "busybox", Version: "1.3.6", Platform: "linux-amd64", Filename: "busybox"}: EntryInfo{IsConfigured: true, BinaryPath: "testdata/bin/busybox/1.3.6/linux-amd64/busybox", SnippetPath: ""},
LogicalEntryKey{OrgName: "busybox", Version: "1.3.6", Platform: "linux-arm64", Filename: "busybox"}: EntryInfo{IsConfigured: false, BinaryPath: "testdata/bin/busybox/1.3.6/linux-arm64/busybox", SnippetPath: ""},
LogicalEntryKey{OrgName: "busybox", Version: "1.3.6", Platform: "linux-amd64", Filename: "busybox"}: EntryInfo{IsConfigured: true, BinaryPath: filepath.FromSlash("testdata/bin/busybox/1.3.6/linux-amd64/busybox"), SnippetPath: ""},
LogicalEntryKey{OrgName: "busybox", Version: "1.3.6", Platform: "linux-arm64", Filename: "busybox"}: EntryInfo{IsConfigured: false, BinaryPath: filepath.FromSlash("testdata/bin/busybox/1.3.6/linux-arm64/busybox"), SnippetPath: ""},
},
entries,
)
@@ -105,10 +105,10 @@ func TestListAllEntries(t *testing.T) {
assert.Equal(t,
Entries{
LogicalEntryKey{OrgName: "busybox", Version: "1.3.6", Platform: "linux-amd64", Filename: "busybox"}: EntryInfo{IsConfigured: true, BinaryPath: "testdata/bin/busybox/1.3.6/linux-amd64/busybox", SnippetPath: "testdata/snippets/busybox/1.3.6/linux-amd64/busybox"},
LogicalEntryKey{OrgName: "busybox", Version: "1.3.6", Platform: "linux-arm64", Filename: "busybox"}: EntryInfo{IsConfigured: false, BinaryPath: "testdata/bin/busybox/1.3.6/linux-arm64/busybox", SnippetPath: "testdata/snippets/busybox/1.3.6/linux-arm64/busybox"},
LogicalEntryKey{OrgName: "busybox", Version: "1.3.6", Platform: "linux-amd64", Filename: "busybox"}: EntryInfo{IsConfigured: true, BinaryPath: filepath.FromSlash("testdata/bin/busybox/1.3.6/linux-amd64/busybox"), SnippetPath: filepath.FromSlash("testdata/snippets/busybox/1.3.6/linux-amd64/busybox")},
LogicalEntryKey{OrgName: "busybox", Version: "1.3.6", Platform: "linux-arm64", Filename: "busybox"}: EntryInfo{IsConfigured: false, BinaryPath: filepath.FromSlash("testdata/bin/busybox/1.3.6/linux-arm64/busybox"), SnippetPath: filepath.FromSlash("testdata/snippets/busybox/1.3.6/linux-arm64/busybox")},
// note the standalone snippet!
LogicalEntryKey{OrgName: "postgres", Version: "9.6.10", Platform: "linux-amd64", Filename: "postgres"}: EntryInfo{IsConfigured: false, BinaryPath: "", SnippetPath: "testdata/snippets/postgres/9.6.10/linux-amd64/postgres"},
LogicalEntryKey{OrgName: "postgres", Version: "9.6.10", Platform: "linux-amd64", Filename: "postgres"}: EntryInfo{IsConfigured: false, BinaryPath: "", SnippetPath: filepath.FromSlash("testdata/snippets/postgres/9.6.10/linux-amd64/postgres")},
},
entries,
)
@@ -1,6 +1,7 @@
package internal
import (
"path/filepath"
"strings"
"testing"
@@ -13,7 +14,7 @@ func TestSplitFilepath(t *testing.T) {
path string
expected []string
}{
{path: "a/b/c", expected: []string{"a", "b", "c"}},
{path: filepath.Join("a", "b", "c"), expected: []string{"a", "b", "c"}},
}
for _, test := range tests {
@@ -3,7 +3,9 @@ package testutil
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/require"
@@ -18,10 +20,11 @@ import (
// not specify the "bin" or "snippets" parent path... this is determined logically [snippets > binary unless told
// otherwise]). Path should also be to the directory containing the binary or snippets of interest (not the binaries
// or snippets itself).
func SnippetOrBinary(t *testing.T, path string, requireBinary bool) string {
func SnippetOrBinary(t *testing.T, logicalPath string, requireBinary bool) string {
t.Helper()
require.Len(t, internal.SplitFilepath(path), 3, "path must be a in the form <name>/<version>/<arch>")
// logicalPath is a posix key, not a host path
require.Len(t, strings.Split(logicalPath, "/"), 3, "path must be a in the form <name>/<version>/<arch>")
// cd to testdata directory and load the config
@@ -43,11 +46,11 @@ func SnippetOrBinary(t *testing.T, path string, requireBinary bool) string {
var fixturePath string
for k, v := range entries.Sorted() {
if filepath.Dir(k.Path()) == path {
if path.Dir(k.Path()) == logicalPath {
// prefer the snippet over the binary
if !requireBinary {
if v.SnippetPath != "" {
t.Logf("using snippet for %q", path)
t.Logf("using snippet for %q", logicalPath)
require.NoError(t, validateSnippet(v.BinaryPath, v.SnippetPath))
fixturePath = v.SnippetPath
break
@@ -56,10 +59,10 @@ func SnippetOrBinary(t *testing.T, path string, requireBinary bool) string {
fixturePath = v.BinaryPath
break
}
t.Fatalf("no binary or snippet found for %q", path)
t.Fatalf("no binary or snippet found for %q", logicalPath)
}
if v.BinaryPath != "" {
t.Logf("forcing the use of the original binary for %q", path)
t.Logf("forcing the use of the original binary for %q", logicalPath)
fixturePath = v.BinaryPath
break
}
@@ -68,12 +71,12 @@ func SnippetOrBinary(t *testing.T, path string, requireBinary bool) string {
t.Skip("no binary found, but is covered by a snippet. Please add this case to the 'binary/testdata/config.yaml' and recreate the snippet")
}
t.Fatalf("no binary found for %q", path)
t.Fatalf("no binary found for %q", logicalPath)
}
}
if fixturePath == "" {
t.Fatalf("no fixture found for %q", path)
t.Fatalf("no fixture found for %q", logicalPath)
}
// this should be relative to the tests-fixtures directory and should be the directory containing the binary or
@@ -101,6 +104,7 @@ func validateSnippet(binaryPath, snippetPath string) error {
if err != nil {
return err
}
defer f.Close()
expected, err := internal.Sha256SumFile(f)
if err != nil {
+2 -2
View File
@@ -5,7 +5,7 @@ package bitnami
import (
"context"
"path/filepath"
"path"
"strings"
"github.com/anchore/syft/internal/log"
@@ -73,7 +73,7 @@ func parseSBOM(_ context.Context, resolver file.Resolver, _ *generic.Environment
// Bitnami packages reported in a SPDX file are shipped under the same directory
// as the SPDX file itself.
metadata.Path = filepath.Dir(reader.RealPath)
metadata.Path = path.Dir(reader.RealPath)
if p.ID() != mainPkgID {
metadata.Files = packageFiles(s.Relationships, p, metadata.Path)
secondaryPkgsFiles = append(secondaryPkgsFiles, metadata.Files...)
+1 -2
View File
@@ -3,7 +3,6 @@ package bitnami
import (
"fmt"
"path"
"path/filepath"
"slices"
"strings"
@@ -73,7 +72,7 @@ func packageFiles(relationships []artifact.Relationship, p pkg.Package, baseDire
// mainPkgFiles returns the files owned by the main package in the SPDX file.
func mainPkgFiles(resolver file.Resolver, spdxFilePath string, secondaryPkgsFiles []string) ([]string, error) {
ownedPathGlob := fmt.Sprintf("%s/**", filepath.Dir(spdxFilePath))
ownedPathGlob := fmt.Sprintf("%s/**", path.Dir(spdxFilePath))
ownedLocations, err := resolver.FilesByGlob(ownedPathGlob)
if err != nil {
return nil, err
@@ -4,7 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"path"
"sort"
"strings"
@@ -67,7 +67,7 @@ func parseComponentsJSON(_ context.Context, _ file.Resolver, _ *generic.Environm
Revision: revision,
Architecture: entry.Arch,
Distro: entry.Distro,
Path: filepath.Join(filepath.Dir(reader.RealPath), name),
Path: path.Join(path.Dir(reader.RealPath), name),
}
p := pkg.Package{
+183 -72
View File
@@ -4,8 +4,9 @@ import (
"context"
"encoding/json"
"fmt"
"maps"
"slices"
"sort"
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log"
@@ -18,6 +19,8 @@ import (
var _ generic.Parser = parseDotnetPackagesLock
var packagesLockTypePrecedence = []string{"Project", "Direct", "CentralTransitive", "Transitive"}
type dotnetPackagesLock struct {
Version int `json:"version"`
Dependencies map[string]map[string]dotnetPackagesLockDep `json:"dependencies"`
@@ -31,11 +34,38 @@ type dotnetPackagesLockDep struct {
Dependencies map[string]string `json:"dependencies,omitempty"`
}
func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { //nolint:funlen
var pkgs []pkg.Package
var pkgMap = make(map[string]pkg.Package)
var relationships []artifact.Relationship
type packagesLockEntry struct {
name string
dep dotnetPackagesLockDep
}
func (e packagesLockEntry) nameVersion() string {
return createNameAndVersion(e.name, e.dep.Resolved)
}
// key identifies the package an entry describes. NuGet package IDs are case-insensitive, so two spellings of the
// same ID and version are one package.
func (e packagesLockEntry) key() string {
return createNameAndVersion(strings.ToLower(e.name), e.dep.Resolved)
}
type packagesLockFramework struct {
name string
entries []packagesLockEntry
// byName is keyed by the lowercased package name, since NuGet package IDs are case-insensitive.
byName map[string]packagesLockEntry
}
func (f packagesLockFramework) find(name string) (packagesLockEntry, bool) {
e, ok := f.byName[strings.ToLower(name)]
return e, ok
}
type packagesLockEdge struct {
child, parent artifact.ID
}
func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
dec := json.NewDecoder(reader)
// unmarshal file
@@ -44,70 +74,21 @@ func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Envi
return nil, nil, fmt.Errorf("failed to parse packages.lock.json file: %w", err)
}
// collect all deps here
allDependencies := make(map[string]dotnetPackagesLockDep)
var names []string
for _, dependencies := range lockFile.Dependencies {
for name, dep := range dependencies {
depNameVersion := createNameAndVersion(name, dep.Resolved)
if slices.Contains(names, depNameVersion) {
continue
}
names = append(names, depNameVersion)
allDependencies[depNameVersion] = dep
}
}
// sort the names so that the order of the packages is deterministic
sort.Strings(names)
frameworks := newPackagesLockFrameworks(lockFile)
// create artifact for each pkg
for _, nameVersion := range names {
name, _ := extractNameAndVersion(nameVersion)
var pkgs []pkg.Package
pkgMap := make(map[string]pkg.Package)
dep := allDependencies[nameVersion]
dotnetPkg := newDotnetPackagesLockPackage(name, dep, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
for _, entry := range mergePackagesLockEntries(frameworks) {
dotnetPkg := newDotnetPackagesLockPackage(entry.name, entry.dep, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
if dotnetPkg != nil {
pkgs = append(pkgs, *dotnetPkg)
pkgMap[nameVersion] = *dotnetPkg
pkgMap[entry.key()] = *dotnetPkg
}
}
// fill up relationships
for depNameVersion, dep := range allDependencies {
parentPkg, ok := pkgMap[depNameVersion]
if !ok {
log.Debugf("package \"%s\" not found in map of all packages", depNameVersion)
continue
}
for childDepName, childDepVersion := range dep.Dependencies {
childDepNameVersion := createNameAndVersion(childDepName, childDepVersion)
// try and find pkg for dependency with exact name and version
childPkg, ok := pkgMap[childDepNameVersion]
if !ok {
// no exact match found, lets match on name only, lockfile will contain other version of pkg
cpkg, ok := findPkgByName(childDepName, pkgMap)
if !ok {
log.Debugf("dependency \"%s\" of package \"%s\" not found in map of all packages", childDepNameVersion, depNameVersion)
continue
}
childPkg = *cpkg
}
rel := artifact.Relationship{
From: childPkg,
To: parentPkg,
Type: artifact.DependencyOfRelationship,
}
relationships = append(relationships, rel)
}
}
relationships := packagesLockRelationships(frameworks, pkgMap)
// sort the relationships for deterministic output
relationship.Sort(relationships)
@@ -115,6 +96,147 @@ func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Envi
return pkgs, relationships, nil
}
func newPackagesLockFrameworks(lockFile dotnetPackagesLock) []packagesLockFramework {
var frameworks []packagesLockFramework
for _, frameworkName := range slices.Sorted(maps.Keys(lockFile.Dependencies)) {
deps := lockFile.Dependencies[frameworkName]
framework := packagesLockFramework{
name: frameworkName,
byName: make(map[string]packagesLockEntry, len(deps)),
}
for _, name := range slices.Sorted(maps.Keys(deps)) {
entry := packagesLockEntry{name: name, dep: deps[name]}
framework.entries = append(framework.entries, entry)
framework.byName[strings.ToLower(name)] = entry
}
frameworks = append(frameworks, framework)
}
return frameworks
}
func mergePackagesLockEntries(frameworks []packagesLockFramework) []packagesLockEntry {
merged := make(map[string]packagesLockEntry)
for _, framework := range frameworks {
for _, entry := range framework.entries {
key := entry.key()
if existing, ok := merged[key]; ok && !isMoreDirect(entry.dep.Type, existing.dep.Type) {
continue
}
merged[key] = entry
}
}
var entries []packagesLockEntry
for _, key := range slices.Sorted(maps.Keys(merged)) {
entries = append(entries, merged[key])
}
return entries
}
func isMoreDirect(a, b string) bool {
rankA, rankB := packagesLockTypeRank(a), packagesLockTypeRank(b)
if rankA != rankB {
return rankA < rankB
}
return a < b
}
func packagesLockTypeRank(t string) int {
if i := slices.Index(packagesLockTypePrecedence, t); i >= 0 {
return i
}
return len(packagesLockTypePrecedence)
}
func packagesLockRelationships(frameworks []packagesLockFramework, pkgMap map[string]pkg.Package) []artifact.Relationship {
var relationships []artifact.Relationship
frameworksByName := make(map[string]packagesLockFramework, len(frameworks))
for _, framework := range frameworks {
frameworksByName[framework.name] = framework
}
seen := make(map[packagesLockEdge]struct{})
for _, framework := range frameworks {
base, hasBase := frameworksByName[baseFrameworkName(framework.name)]
if !hasBase || base.name == framework.name {
base = packagesLockFramework{}
}
for _, entry := range framework.entries {
parentPkg, ok := pkgMap[entry.key()]
if !ok {
log.Debugf("package %q not found in map of all packages", entry.nameVersion())
continue
}
relationships = append(relationships, packagesLockEntryRelationships(entry, parentPkg, framework, base, pkgMap, seen)...)
}
}
return relationships
}
func packagesLockEntryRelationships(entry packagesLockEntry, parentPkg pkg.Package, framework, base packagesLockFramework, pkgMap map[string]pkg.Package, seen map[packagesLockEdge]struct{}) []artifact.Relationship {
var relationships []artifact.Relationship
for _, childName := range slices.Sorted(maps.Keys(entry.dep.Dependencies)) {
child, ok := findPackagesLockDependency(childName, framework, base)
if !ok {
log.Debugf("dependency %q of package %q not found under target framework %q", childName, entry.nameVersion(), framework.name)
continue
}
childPkg, ok := pkgMap[child.key()]
if !ok {
log.Debugf("package %q not found in map of all packages", child.nameVersion())
continue
}
// a package listing itself (possibly under another casing) is not a dependency
if childPkg.ID() == parentPkg.ID() {
continue
}
key := packagesLockEdge{child: childPkg.ID(), parent: parentPkg.ID()}
if _, exists := seen[key]; exists {
continue
}
seen[key] = struct{}{}
relationships = append(relationships, artifact.Relationship{
From: childPkg,
To: parentPkg,
Type: artifact.DependencyOfRelationship,
})
}
return relationships
}
// findPackagesLockDependency finds the entry a dependency edge points at. The version on an edge is only the lower
// bound of a version range, so the edge resolves to whatever version its target framework resolved. Runtime-specific
// sections such as "net8.0/win-x64" may reference packages listed only in their base framework ("net8.0"). An edge
// that resolves nowhere is dropped rather than guessed from another target framework.
func findPackagesLockDependency(name string, framework, base packagesLockFramework) (packagesLockEntry, bool) {
if entry, ok := framework.find(name); ok {
return entry, true
}
return base.find(name)
}
// baseFrameworkName returns the target framework of a runtime-specific section, such as "net8.0" for "net8.0/win-x64".
func baseFrameworkName(frameworkName string) string {
name, _, _ := strings.Cut(frameworkName, "/")
return name
}
func newDotnetPackagesLockPackage(name string, dep dotnetPackagesLockDep, locations ...file.Location) *pkg.Package {
metadata := pkg.DotnetPackagesLockEntry{
Name: name,
@@ -150,14 +272,3 @@ func packagesLockPackageURL(name, version string) string {
"",
).ToString()
}
func findPkgByName(pkgName string, pkgMap map[string]pkg.Package) (*pkg.Package, bool) {
for pkgNameVersion, pkg := range pkgMap {
name, _ := extractNameAndVersion(pkgNameVersion)
if name == pkgName {
return &pkg, true
}
}
return nil, false
}
@@ -3,6 +3,9 @@ package dotnet
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
@@ -197,3 +200,304 @@ func TestParseDotnetPackagesLock(t *testing.T) {
pkgtest.TestFileParser(t, fixture, parseDotnetPackagesLock, expectedPkgs, expectedRelationships)
}
func TestParseDotnetPackagesLock_multipleTargetFrameworks(t *testing.T) {
fixture := "testdata/packages.lock-multi-framework.json"
fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture))
myLibPkg := pkg.Package{
Name: "MyLib",
Version: "1.0.0",
PURL: "pkg:nuget/MyLib@1.0.0",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetPackagesLockEntry{
Name: "MyLib",
Version: "1.0.0",
ContentHash: "mylibhash==",
Type: "Direct",
},
}
log4net2Pkg := pkg.Package{
Name: "log4net",
Version: "2.0.5",
PURL: "pkg:nuget/log4net@2.0.5",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetPackagesLockEntry{
Name: "log4net",
Version: "2.0.5",
ContentHash: "log4net205hash==",
Type: "Transitive",
},
}
log4net1Pkg := pkg.Package{
Name: "log4net",
Version: "1.2.15",
PURL: "pkg:nuget/log4net@1.2.15",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetPackagesLockEntry{
Name: "log4net",
Version: "1.2.15",
ContentHash: "log4net1215hash==",
Type: "Transitive",
},
}
// resolved to the same version under both frameworks (spelled "newtonsoft.json" under one), but declared "Direct" by
// only one of them
newtonsoftPkg := pkg.Package{
Name: "Newtonsoft.Json",
Version: "13.0.3",
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetPackagesLockEntry{
Name: "Newtonsoft.Json",
Version: "13.0.3",
ContentHash: "newtonsoft1303hash==",
Type: "Direct",
},
}
// only listed under the runtime-specific "net8.0/win-x64" section, and depends on a package from "net8.0"
myLibNativePkg := pkg.Package{
Name: "runtime.win-x64.MyLib.Native",
Version: "1.0.0",
PURL: "pkg:nuget/runtime.win-x64.MyLib.Native@1.0.0",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetPackagesLockEntry{
Name: "runtime.win-x64.MyLib.Native",
Version: "1.0.0",
ContentHash: "mylibnativehash==",
Type: "Transitive",
},
}
// only listed under "net8.0", so the "netstandard2.0" edge to it must be dropped rather than guessed
serilogPkg := pkg.Package{
Name: "Serilog",
Version: "3.1.1",
PURL: "pkg:nuget/Serilog@3.1.1",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetPackagesLockEntry{
Name: "Serilog",
Version: "3.1.1",
ContentHash: "serilog311hash==",
Type: "Transitive",
},
}
expectedPkgs := []pkg.Package{
myLibPkg,
newtonsoftPkg,
log4net1Pkg,
log4net2Pkg,
myLibNativePkg,
serilogPkg,
}
// the same package is resolved to a different version per target framework, so both edges must be captured, while
// the Newtonsoft.Json edge declared by both frameworks must appear only once
expectedRelationships := []artifact.Relationship{
{
From: log4net1Pkg,
To: myLibPkg,
Type: artifact.DependencyOfRelationship,
},
{
From: log4net2Pkg,
To: myLibPkg,
Type: artifact.DependencyOfRelationship,
},
{
From: log4net2Pkg,
To: myLibNativePkg,
Type: artifact.DependencyOfRelationship,
},
{
From: newtonsoftPkg,
To: myLibPkg,
Type: artifact.DependencyOfRelationship,
},
}
pkgtest.TestFileParser(t, fixture, parseDotnetPackagesLock, expectedPkgs, expectedRelationships)
}
func TestParseDotnetPackagesLock_projectReference(t *testing.T) {
fixture := "testdata/packages.lock-project-reference.json"
fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture))
newtonsoftPkg := pkg.Package{
Name: "Newtonsoft.Json",
Version: "13.0.3",
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetPackagesLockEntry{
Name: "Newtonsoft.Json",
Version: "13.0.3",
ContentHash: "newtonsoft1303hash==",
Type: "Transitive",
},
}
// a project reference has no resolved version or content hash
myLibPkg := pkg.Package{
Name: "mylib",
PURL: "pkg:nuget/mylib",
Locations: fixtureLocationSet,
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
Metadata: pkg.DotnetPackagesLockEntry{
Name: "mylib",
Type: "Project",
},
}
expectedPkgs := []pkg.Package{
newtonsoftPkg,
myLibPkg,
}
// project references declare their dependencies as version ranges, which resolve to the version the target
// framework resolved
expectedRelationships := []artifact.Relationship{
{
From: newtonsoftPkg,
To: myLibPkg,
Type: artifact.DependencyOfRelationship,
},
}
pkgtest.TestFileParser(t, fixture, parseDotnetPackagesLock, expectedPkgs, expectedRelationships)
}
func Test_findPackagesLockDependency(t *testing.T) {
frameworks := newPackagesLockFrameworks(dotnetPackagesLock{
Dependencies: map[string]map[string]dotnetPackagesLockDep{
"net8.0": {
"log4net": {Resolved: "2.0.5"},
},
"net8.0/win-x64": {
"runtime.win-x64.Native": {Resolved: "1.0.0"},
},
"netstandard2.0": {
"log4net": {Resolved: "1.2.15"},
"Newtonsoft.Json": {Resolved: "13.0.3"},
},
},
})
net8, rid, netstandard := frameworks[0], frameworks[1], frameworks[2]
tests := []struct {
name string
depName string
framework packagesLockFramework
base packagesLockFramework
wantVersion string
wantFound bool
}{
{
name: "resolves to the version pinned by its own target framework",
depName: "log4net",
framework: netstandard,
wantVersion: "1.2.15",
wantFound: true,
},
{
name: "package names are matched case-insensitively",
depName: "LOG4NET",
framework: net8,
wantVersion: "2.0.5",
wantFound: true,
},
{
name: "runtime-specific sections fall back to their base target framework",
depName: "log4net",
framework: rid,
base: net8,
wantVersion: "2.0.5",
wantFound: true,
},
{
name: "a package absent from the target framework is not guessed from another one",
depName: "Newtonsoft.Json",
framework: net8,
wantFound: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, found := findPackagesLockDependency(tt.depName, tt.framework, tt.base)
require.Equal(t, tt.wantFound, found)
if !tt.wantFound {
return
}
assert.Equal(t, tt.wantVersion, got.dep.Resolved)
})
}
}
func Test_mergePackagesLockEntries_typePrecedence(t *testing.T) {
tests := []struct {
name string
types map[string]string // target framework -> type
wantType string
}{
{
name: "direct wins over transitive",
types: map[string]string{"net8.0": "Transitive", "netstandard2.0": "Direct"},
wantType: "Direct",
},
{
name: "central transitive wins over transitive regardless of framework order",
types: map[string]string{"net8.0": "Transitive", "netstandard2.0": "CentralTransitive"},
wantType: "CentralTransitive",
},
{
name: "central transitive wins over transitive in the reverse framework order",
types: map[string]string{"net8.0": "CentralTransitive", "netstandard2.0": "Transitive"},
wantType: "CentralTransitive",
},
{
name: "direct wins over central transitive",
types: map[string]string{"net472": "CentralTransitive", "net8.0": "Direct", "netstandard2.0": "Transitive"},
wantType: "Direct",
},
{
name: "known types win over unknown ones",
types: map[string]string{"net8.0": "Unknown", "netstandard2.0": "Transitive"},
wantType: "Transitive",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lockFile := dotnetPackagesLock{Dependencies: map[string]map[string]dotnetPackagesLockDep{}}
for framework, depType := range tt.types {
lockFile.Dependencies[framework] = map[string]dotnetPackagesLockDep{
"Newtonsoft.Json": {Type: depType, Resolved: "13.0.3"},
}
}
entries := mergePackagesLockEntries(newPackagesLockFrameworks(lockFile))
require.Len(t, entries, 1)
assert.Equal(t, tt.wantType, entries[0].dep.Type)
})
}
}
@@ -0,0 +1,67 @@
{
"version": 1,
"dependencies": {
"net8.0": {
"MyLib": {
"type": "Direct",
"requested": "[1.0.0, )",
"resolved": "1.0.0",
"contentHash": "mylibhash==",
"dependencies": {
"log4net": "2.0.5",
"Newtonsoft.Json": "13.0.3"
}
},
"log4net": {
"type": "Transitive",
"resolved": "2.0.5",
"contentHash": "log4net205hash=="
},
"Newtonsoft.Json": {
"type": "Direct",
"requested": "[13.0.3, )",
"resolved": "13.0.3",
"contentHash": "newtonsoft1303hash=="
},
"Serilog": {
"type": "Transitive",
"resolved": "3.1.1",
"contentHash": "serilog311hash=="
}
},
"net8.0/win-x64": {
"runtime.win-x64.MyLib.Native": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "mylibnativehash==",
"dependencies": {
"log4net": "2.0.0",
"RUNTIME.WIN-X64.MYLIB.NATIVE": "1.0.0"
}
}
},
"netstandard2.0": {
"MyLib": {
"type": "Direct",
"requested": "[1.0.0, )",
"resolved": "1.0.0",
"contentHash": "mylibhash==",
"dependencies": {
"log4net": "1.2.15",
"newtonsoft.json": "13.0.3",
"Serilog": "3.1.1"
}
},
"log4net": {
"type": "Transitive",
"resolved": "1.2.15",
"contentHash": "log4net1215hash=="
},
"newtonsoft.json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "newtonsoft1303hash=="
}
}
}
}
@@ -0,0 +1,18 @@
{
"version": 1,
"dependencies": {
"net8.0": {
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "newtonsoft1303hash=="
},
"mylib": {
"type": "Project",
"dependencies": {
"Newtonsoft.Json": "[13.0.3, )"
}
}
}
}
}
@@ -1,6 +1,9 @@
package golang
import (
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/spf13/afero"
@@ -202,10 +205,23 @@ func TestFindAllLicenseCandidatesUpwards(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
// Create in-memory filesystem
fs := afero.NewMemMapFs()
tt.setupFS(fs)
setupFS, startDir, stopAt, expectedFiles := afero.Fs(fs), tt.startDir, tt.stopAt, tt.expectedFiles
if runtime.GOOS == "windows" {
// the finder wants native absolute paths, so root the posix-style cases on a drive
setupFS = afero.NewBasePathFs(fs, `C:\`)
startDir, stopAt = winAbs(startDir), winAbs(stopAt)
expectedFiles = nil
if tt.expectedFiles != nil {
expectedFiles = []string{}
}
for _, f := range tt.expectedFiles {
expectedFiles = append(expectedFiles, winAbs(f))
}
}
tt.setupFS(setupFS)
// Run the function
result, err := findAllLicenseCandidatesUpwards(tt.startDir, tt.stopAt, fs)
result, err := findAllLicenseCandidatesUpwards(startDir, stopAt, fs)
// Check error expectation
if tt.expectedError {
@@ -214,7 +230,15 @@ func TestFindAllLicenseCandidatesUpwards(t *testing.T) {
}
require.NoError(t, err, tt.description)
assert.Equal(t, tt.expectedFiles, result, tt.description)
assert.Equal(t, expectedFiles, result, tt.description)
})
}
}
// winAbs roots posix absolute paths on C:\, leaving relative ones relative (some cases assert they're rejected)
func winAbs(p string) string {
if !strings.HasPrefix(p, "/") {
return p
}
return filepath.Join(`C:\`, filepath.FromSlash(p))
}
@@ -20,6 +20,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/internal/testutils"
"github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/fileresolver"
@@ -30,6 +31,8 @@ import (
// make will run the default make target for the given test fixture path
func runMakeTarget(t *testing.T, fixtureName string) {
// the fixture binaries are built inside linux containers
testutils.SkipWithoutLinuxContainers(t)
cwd, err := os.Getwd()
require.NoError(t, err)
fixtureDir := filepath.Join(cwd, "testdata/", fixtureName)
+7 -1
View File
@@ -20,6 +20,7 @@ import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/fileresolver"
"github.com/anchore/syft/syft/internal/windows"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
@@ -38,7 +39,12 @@ func newGoModCataloger(opts CatalogerConfig) *goModCataloger {
// parseGoModFile takes a go.mod and tries to resolve and lists all packages discovered.
func (c *goModCataloger) parseGoModFile(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) (pkgs []pkg.Package, relationships []artifact.Relationship, err error) {
modDir := filepath.Dir(string(reader.Location.Reference().RealPath))
realPath := string(reader.Location.Reference().RealPath)
if windows.HostRunningOnWindows() {
// directory resolvers hand back volume-encoded posix paths (/c/...), the go toolchain needs the native path
realPath = windows.FromPosix(realPath)
}
modDir := filepath.Dir(realPath)
digests, err := parseGoSumFile(resolver, reader)
if err != nil {
log.Debugf("unable to get go.sum: %v", err)
+2
View File
@@ -10,6 +10,7 @@ import (
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/internal/testutils"
)
// every case here is one the magic scan must reject, asserted on parseUPXInfo's errNotUPX result.
@@ -114,6 +115,7 @@ func TestParseUPXInfo_ValidHeader(t *testing.T) {
// ignores that padding if there is some. A build that needs none passes either way, which is how the bug
// went unnoticed. The case without padding is covered by TestDecompressUPX_TailExtentsArePlacedPastTheLoader.
func TestImageSmallUPXNeedsLoaderPadding(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", "image-small-upx")
rc, err := img.OpenPathFromSquash(stereoscopeFile.Path("/run-me"))
require.NoError(t, err)
@@ -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 {
@@ -3834,11 +3834,18 @@
"cpe:2.3:a:gaoxuyan_project:gaoxuyan:*:*:*:*:*:*:*:*"
],
"gatsby-plugin-mdx": [
"cpe:2.3:a:gatsbyjs:gatsby-plugin-mdx:*:*:*:*:*:node.js:*:*",
"cpe:2.3:a:gatsbyjs:gatsby:*:*:*:*:*:node.js:*:*"
],
"gatsby-plugin-sharp": [
"cpe:2.3:a:gatsbyjs:gatsby-plugin-sharp:*:*:*:*:*:node.js:*:*"
],
"gatsby-source-wordpress": [
"cpe:2.3:a:gatsbyjs:gatsby-source-wordpress:*:*:*:*:*:node.js:*:*"
],
"gatsby-transformer-remark": [
"cpe:2.3:a:gatsbyjs:gatsby-transformer-remark:*:*:*:*:*:node.js:*:*"
],
"gedi": [
"cpe:2.3:a:gedi_project:gedi:*:*:*:*:*:node.js:*:*"
],
@@ -6265,6 +6272,9 @@
"vega-functions": [
"cpe:2.3:a:vega-functions_project:vega-functions:*:*:*:*:*:node.js:*:*"
],
"vega-selections": [
"cpe:2.3:a:vega_project:vega:*:*:*:*:*:node.js:*:*"
],
"velocityjs": [
"cpe:2.3:a:shepherdwind:velocity.js:*:*:*:*:*:node.js:*:*"
],
@@ -6975,6 +6985,9 @@
"openlit": [
"cpe:2.3:a:openlit:openlit_software_development_kit:*:*:*:*:*:python:*:*"
],
"openml": [
"cpe:2.3:a:openml:openml:*:*:*:*:*:python:*:*"
],
"openpyxl": [
"cpe:2.3:a:python:openpyxl:*:*:*:*:*:*:*:*"
],
@@ -8796,6 +8809,9 @@
"adaptive-images": [
"cpe:2.3:a:nevma:adaptive_images:*:*:*:*:*:wordpress:*:*"
],
"adbuddy-adblocker-detection": [
"cpe:2.3:a:netfunkdesign:adbuddy\\+_\\(adblocker_detection\\):*:*:*:*:*:wordpress:*:*"
],
"add-actions-and-filters": [
"cpe:2.3:a:add_shortcodes_actions_and_filters_project:add_shortcodes_actions_and_filters:*:*:*:*:*:wordpress:*:*"
],
@@ -8948,7 +8964,8 @@
"cpe:2.3:a:niceit:advance_post_prefix:*:*:*:*:*:wordpress:*:*"
],
"advance-search": [
"cpe:2.3:a:advance_search_project:advance_search:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:advance_search_project:advance_search:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:mndpsingh287:advanced_search:*:*:*:*:*:wordpress:*:*"
],
"advanced-access-manager": [
"cpe:2.3:a:vasyltech:advanced_access_manager:*:*:*:*:*:wordpress:*:*"
@@ -9098,6 +9115,9 @@
"agp-font-awesome-collection": [
"cpe:2.3:a:profosbox:agp_font_awesome_collection:*:*:*:*:*:wordpress:*:*"
],
"ahathat": [
"cpe:2.3:a:mitchelllevy:ahathat:*:*:*:*:*:wordpress:*:*"
],
"ai-assistant-by-10web": [
"cpe:2.3:a:10web:ai_assistant:*:*:*:*:*:wordpress:*:*"
],
@@ -9524,6 +9544,9 @@
"asf-allow-svg-files": [
"cpe:2.3:a:allow_svg_files_project:allow_svg_files:*:*:*:*:*:wordpress:*:*"
],
"asgard": [
"cpe:2.3:a:outself:asgard_security_scanner:*:*:*:*:*:wordpress:*:*"
],
"asgaros-forum": [
"cpe:2.3:a:asgaros:asgaros_forum:*:*:*:*:*:wordpress:*:*"
],
@@ -9803,6 +9826,9 @@
"banner-effect-header": [
"cpe:2.3:a:banner_effect_header_project:banner_effect_header:*:*:*:*:*:wordpress:*:*"
],
"banner-garden": [
"cpe:2.3:a:karacsi_maci:banner_garden:*:*:*:*:*:wordpress:*:*"
],
"banner-management-for-woocommerce": [
"cpe:2.3:a:dotstore:woocommerce_category_banner_management:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:multidots:banner_management_for_woocommerce:*:*:*:*:*:wordpress:*:*"
@@ -10077,6 +10103,9 @@
"blue-admin": [
"cpe:2.3:a:blue-admin_project:blue-admin:*:*:*:*:*:wordpress:*:*"
],
"blue-triad-ezanalytics": [
"cpe:2.3:a:jbahlquist:blue_triad_ezanalytics:*:*:*:*:*:wordpress:*:*"
],
"bluet-keywords-tooltip-generator": [
"cpe:2.3:a:tooltipy:tooltipy:*:*:*:*:*:wordpress:*:*"
],
@@ -10334,6 +10363,9 @@
"bulk-image-resizer": [
"cpe:2.3:a:giuliopanda:bulk_images_optimizer:*:*:*:*:*:wordpress:*:*"
],
"bulk-me-now": [
"cpe:2.3:a:ombu:bulk_me_now\\!:*:*:*:*:*:wordpress:*:*"
],
"bulk-noindex-nofollow-toolkit-by-mad-fish": [
"cpe:2.3:a:madfishdigital:bulk_noindex_\\\u0026_nofollow_toolkit:*:*:*:*:*:wordpress:*:*"
],
@@ -10355,6 +10387,9 @@
"business": [
"cpe:2.3:a:snilesh:business:*:*:*:*:*:wordpress:*:*"
],
"business-card-by-esterox-100": [
"cpe:2.3:a:esterox:business_card:*:*:*:*:*:wordpress:*:*"
],
"business-directory-plugin": [
"cpe:2.3:a:businessdirectoryplugin:business_directory:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:strategy11:business_directory_plugin_-_easy_listing_directories:*:*:*:*:*:wordpress:*:*"
@@ -10542,7 +10577,8 @@
"cpe:2.3:a:vinojcardoza:ajax_post_search:*:*:*:*:*:wordpress:*:*"
],
"cardoza-facebook-like-box": [
"cpe:2.3:a:facebook_like_box_project:facebook_like_box:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:facebook_like_box_project:facebook_like_box:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:johnnash1975:easy_social_like_box_popup_sidebar_widget:*:*:*:*:*:wordpress:*:*"
],
"cardoza-wordpress-poll": [
"cpe:2.3:a:cardozatechnologies:wordpress_poll:*:*:*:*:*:wordpress:*:*",
@@ -11201,6 +11237,9 @@
"contact-list": [
"cpe:2.3:a:tammersoft:contact_list:*:*:*:*:*:wordpress:*:*"
],
"contact-us-by-lord-linus": [
"cpe:2.3:a:lordlinus:contact_us:*:*:*:*:*:wordpress:*:*"
],
"contact-us-page-contact-people": [
"cpe:2.3:a:a3rev:contact_us_page_-_contact_people:*:*:*:*:*:wordpress:*:*"
],
@@ -11371,6 +11410,9 @@
"countdown-builder": [
"cpe:2.3:a:edmonsoft:countdown_builder:*:*:*:*:*:wordpress:*:*"
],
"countdown-timer-block": [
"cpe:2.3:a:flickdevs:countdown_timer_for_wordpress_block_editor:*:*:*:*:*:wordpress:*:*"
],
"countdown-timer-for-elementor": [
"cpe:2.3:a:flickdevs:countdown_timer_for_elementor:*:*:*:*:*:wordpress:*:*"
],
@@ -11575,6 +11617,9 @@
"cpe:2.3:a:bestwebsoft:custom_admin:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:bestwebsoft:custom_admin_page:*:*:*:*:*:wordpress:*:*"
],
"custom-author-base": [
"cpe:2.3:a:jfarthing:custom_author_base:*:*:*:*:*:wordpress:*:*"
],
"custom-banners": [
"cpe:2.3:a:goldplugins:custom_banners:*:*:*:*:*:wordpress:*:*"
],
@@ -11651,6 +11696,9 @@
"custom-options-plus": [
"cpe:2.3:a:leocaseiro:custom_options_plus:*:*:*:*:*:wordpress:*:*"
],
"custom-order-statuses-for-woocommerce": [
"cpe:2.3:a:nuggethon:custom_order_status_manager_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
"custom-permalinks": [
"cpe:2.3:a:samiahmedsiddiqui:custom_permalinks:*:*:*:*:*:wordpress:*:*"
],
@@ -11882,6 +11930,9 @@
"demon-image-annotation": [
"cpe:2.3:a:superwhite:demon_image_annotation:*:*:*:*:*:wordpress:*:*"
],
"dental-optimizer-patient-generator-app": [
"cpe:2.3:a:healthygrid:dental_optimizer_patient_generator_app:*:*:*:*:*:wordpress:*:*"
],
"deny-all-firewall": [
"cpe:2.3:a:deny_all_firewall_project:deny_all_firewall:*:*:*:*:*:wordpress:*:*"
],
@@ -12028,6 +12079,9 @@
"donorbox-donation-form": [
"cpe:2.3:a:donorbox:donorbox:*:*:*:*:*:wordpress:*:*"
],
"dont-muck-my-markup": [
"cpe:2.3:a:martynchamberlin:dont_muck_my_markup:*:*:*:*:*:wordpress:*:*"
],
"doofinder-for-woocommerce": [
"cpe:2.3:a:doofinder:doofinder:*:*:*:*:*:wordpress:*:*"
],
@@ -12397,6 +12451,9 @@
"ecpay-ecommerce-for-woocommerce": [
"cpe:2.3:a:ecpay:ecpay_ecommerce_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
"ect-homepage-products": [
"cpe:2.3:a:etemplates:ect_home_page_products:*:*:*:*:*:wordpress:*:*"
],
"ecwid-shopping-cart": [
"cpe:2.3:a:lightspeedhq:ecwid_ecommerce_shopping_cart:*:*:*:*:*:wordpress:*:*"
],
@@ -13127,7 +13184,8 @@
"cpe:2.3:a:mehanoid:flatpm:*:*:*:*:*:wordpress:*:*"
],
"flattr": [
"cpe:2.3:a:fkrauthan:wp-mpdf:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:fkrauthan:wp-mpdf:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:flattr:flattr:*:*:*:*:*:wordpress:*:*"
],
"flexi": [
"cpe:2.3:a:odude:flexi:*:*:*:*:*:wordpress:*:*"
@@ -13160,6 +13218,9 @@
"flipping-cards": [
"cpe:2.3:a:info-d-74:flipping_cards:*:*:*:*:free:wordpress:*:*"
],
"float-block": [
"cpe:2.3:a:computy:float_block:*:*:*:*:*:wordpress:*:*"
],
"float-menu": [
"cpe:2.3:a:wow-company:float_menu:*:*:*:*:*:wordpress:*:*"
],
@@ -13422,6 +13483,9 @@
"full-customer": [
"cpe:2.3:a:full:full_-_customer:*:*:*:*:*:wordpress:*:*"
],
"full-screen-page-background-image-slideshow": [
"cpe:2.3:a:mohsinrasool:full_screen_\\(page\\)_background_image_slideshow:*:*:*:*:*:wordpress:*:*"
],
"full-site-editing": [
"cpe:2.3:a:automattic:wordpress.com_editing_toolkit:*:*:*:*:*:wordpress:*:*"
],
@@ -13558,6 +13622,9 @@
"gdpr-framework": [
"cpe:2.3:a:data443:gdpr_framework:*:*:*:*:*:wordpress:*:*"
],
"gearside-developer-dashboard": [
"cpe:2.3:a:gearside:gearside_developer_dashboard:*:*:*:*:*:wordpress:*:*"
],
"gecka-terms-thumbnails": [
"cpe:2.3:a:gecka:terms_thumbnails:*:*:*:*:*:wordpress:*:*"
],
@@ -13567,6 +13634,9 @@
"generate-pdf-using-contact-form-7": [
"cpe:2.3:a:zealousweb:generate_pdf_using_contact_form_7:*:*:*:*:*:wordpress:*:*"
],
"generateblocks": [
"cpe:2.3:a:generatepress:generateblocks:*:*:*:*:free:wordpress:*:*"
],
"generatepress-premium": [
"cpe:2.3:a:generatepress:generatepress:*:*:*:*:premium:wordpress:*:*"
],
@@ -13952,6 +14022,9 @@
"gtmetrix-for-wordpress": [
"cpe:2.3:a:gtmetrix:gtmetrix:*:*:*:*:*:wordpress:*:*"
],
"gtpayment-donation": [
"cpe:2.3:a:harryhe:gtpayment_donations:*:*:*:*:*:wordpress:*:*"
],
"gtranslate": [
"cpe:2.3:a:gtranslate:gtranslate:*:*:*:*:enterprise:wordpress:*:*",
"cpe:2.3:a:gtranslate:gtranslate:*:*:*:*:pro:wordpress:*:*",
@@ -14693,6 +14766,9 @@
"iws-geo-form-fields": [
"cpe:2.3:a:iws-geo-form-fields_project:iws-geo-form-fields:*:*:*:*:*:wordpress:*:*"
],
"javo-core": [
"cpe:2.3:a:javothemes:javo_core:*:*:*:*:*:wordpress:*:*"
],
"jayj-quicktag": [
"cpe:2.3:a:jayj_quicktag_project:jayj_quicktag:*:*:*:*:*:wordpress:*:*"
],
@@ -14969,6 +15045,9 @@
"kwayy-html-sitemap": [
"cpe:2.3:a:kwayyinfotech:kwayy_html_sitemap:*:*:*:*:*:wordpress:*:*"
],
"l-squared-hub-wp-virtual-device": [
"cpe:2.3:a:lsquared:l_squared_hub:*:*:*:*:*:wordpress:*:*"
],
"labtools": [
"cpe:2.3:a:creativityjuice:labtools:*:*:*:*:*:wordpress:*:*"
],
@@ -15196,6 +15275,9 @@
"link-optimizer-lite": [
"cpe:2.3:a:link_optimizer_lite_project:link_optimizer_lite:*:*:*:*:*:wordpress:*:*"
],
"link-party": [
"cpe:2.3:a:evanliewer:illi_link_party\\!:*:*:*:*:*:wordpress:*:*"
],
"link-to-bible": [
"cpe:2.3:a:bibleserver:link_to_bible:*:*:*:*:*:wordpress:*:*"
],
@@ -15531,6 +15613,9 @@
"malinky-ajax-pagination": [
"cpe:2.3:a:malinky:malinky-ajax-pagination:*:*:*:*:*:wordpress:*:*"
],
"mambo-joomla-importer": [
"cpe:2.3:a:misterpah:mambo_joomla_importer:*:*:*:*:*:wordpress:*:*"
],
"manage-notification-emails": [
"cpe:2.3:a:freeamigos:manage_notification_e-mails:*:*:*:*:*:wordpress:*:*"
],
@@ -15592,6 +15677,9 @@
"marketing-and-seo-booster": [
"cpe:2.3:a:secretlab:marketing_and_seo_booster:*:*:*:*:*:wordpress:*:*"
],
"marketing-optimizer": [
"cpe:2.3:a:marketingoptimizer:marketing_optimizer:*:*:*:*:*:wordpress:*:*"
],
"marketo-forms-and-tracking": [
"cpe:2.3:a:hutchhouse:marketo_forms_and_tracking:*:*:*:*:*:wordpress:*:*"
],
@@ -16039,6 +16127,9 @@
"mq-woocommerce-products-price-bulk-edit": [
"cpe:2.3:a:mq-woocommerce-products-price-bulk-edit_project:mq-woocommerce-products-price-bulk-edit:*:*:*:*:*:wordpress:*:*"
],
"mrlegend-typedjs": [
"cpe:2.3:a:mrlegend1235:typed_js:*:*:*:*:*:wordpress:*:*"
],
"ms-registration": [
"cpe:2.3:a:alphaefficiencyteam:custom_login_and_registration:*:*:*:*:*:wordpress:*:*"
],
@@ -16290,6 +16381,9 @@
"news-kit-elementor-addons": [
"cpe:2.3:a:blazethemes:news_kit_elementor_addons:*:*:*:*:*:wordpress:*:*"
],
"news-list": [
"cpe:2.3:a:mahinsha:newsticker:*:*:*:*:*:wordpress:*:*"
],
"newsletter": [
"cpe:2.3:a:thenewsletterplugin:newsletter:*:*:*:*:*:wordpress:*:*"
],
@@ -16305,6 +16399,9 @@
"newsletter-popup": [
"cpe:2.3:a:mndpsingh287:newsletter_popup:*:*:*:*:*:wordpress:*:*"
],
"newsletter2go": [
"cpe:2.3:a:newsletter2go:newsletter2go:*:*:*:*:*:wordpress:*:*"
],
"newsletters-lite": [
"cpe:2.3:a:tribulant:newsletters:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:tribulant:newsletters:*:*:*:*:free:wordpress:*:*"
@@ -16324,7 +16421,8 @@
],
"nextend-facebook-connect": [
"cpe:2.3:a:nextendweb:facebook_connect:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:nextendweb:nextend_facebook_connect:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:nextendweb:nextend_facebook_connect:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:nextendweb:nextend_social_login:*:*:*:*:free:wordpress:*:*"
],
"nextend-twitter-connect": [
"cpe:2.3:a:nextendweb:nextend_twitter_connect:*:*:*:*:*:wordpress:*:*"
@@ -16364,6 +16462,9 @@
"ninja-job-board": [
"cpe:2.3:a:wpmanageninja:ninja_job_board:*:*:*:*:*:wordpress:*:*"
],
"ninja-page-categories-and-tags": [
"cpe:2.3:a:ninja_pages_project:ninja_pages:*:*:*:*:*:wordpress:*:*"
],
"ninja-tables": [
"cpe:2.3:a:wpmanageninja:ninja_tables:*:*:*:*:*:wordpress:*:*"
],
@@ -16407,6 +16508,9 @@
"nofollow-links": [
"cpe:2.3:a:nofollow_links_project:nofollow_links:*:*:*:*:*:wordpress:*:*"
],
"nokaut-offers-box": [
"cpe:2.3:a:nokautpl:nokaut_offers_box:*:*:*:*:*:wordpress:*:*"
],
"nokia-mapsplaces": [
"cpe:2.3:a:nokia_maps_\\\u0026_places_project:nokia_maps_\\\u0026_places:*:*:*:*:*:wordpress:*:*"
],
@@ -16456,6 +16560,9 @@
"ns-woocommerce-watermark": [
"cpe:2.3:a:nsthemes:ns_watermark_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
"ntzantispam": [
"cpe:2.3:a:ionutstaicu:ntz_atispam:*:*:*:*:*:wordpress:*:*"
],
"oauth-client": [
"cpe:2.3:a:miniorange:oauth_2.0_client_for_sso:*:*:*:*:*:wordpress:*:*"
],
@@ -17618,6 +17725,9 @@
"pushbiz": [
"cpe:2.3:a:csimplifyit:pushbiz:*:*:*:*:*:wordpress:*:*"
],
"pvn-auth-popup": [
"cpe:2.3:a:freebiesdownload:pvn_auth_popup:*:*:*:*:*:wordpress:*:*"
],
"pwa-for-wp": [
"cpe:2.3:a:magazine3:pwa_for_wp_\\\u0026_amp:*:*:*:*:*:wordpress:*:*"
],
@@ -18054,6 +18164,9 @@
"responsive-vector-maps": [
"cpe:2.3:a:thinkupthemes:responsive_vector_maps:*:*:*:*:*:wordpress:*:*"
],
"responsive-video": [
"cpe:2.3:a:kirstyburgoine:responsive_video:*:*:*:*:*:wordpress:*:*"
],
"responsive-video-embed": [
"cpe:2.3:a:lepileppanen:responsive_video_embed:*:*:*:*:*:wordpress:*:*"
],
@@ -18069,6 +18182,9 @@
"restaurant-reservations": [
"cpe:2.3:a:fivestarplugins:five_star_restaurant_reservations:*:*:*:*:*:wordpress:*:*"
],
"restaurant-solutions-checklist": [
"cpe:2.3:a:josephlopreste:restaurant_solutions_-_checklist:*:*:*:*:*:wordpress:*:*"
],
"restrict-content": [
"cpe:2.3:a:liquidweb:restrict_content:*:*:*:*:*:wordpress:*:*"
],
@@ -18590,6 +18706,9 @@
"shortcode-addons": [
"cpe:2.3:a:oxilab:shortcode_addons:*:*:*:*:*:wordpress:*:*"
],
"shortcode-cleaner-lite": [
"cpe:2.3:a:jozoor:shortcode_cleaner_lite:*:*:*:*:*:wordpress:*:*"
],
"shortcode-factory": [
"cpe:2.3:a:wpmadeasy:shortcode_factory:*:*:*:*:*:wordpress:*:*"
],
@@ -18972,12 +19091,18 @@
"simple-video-embedder": [
"cpe:2.3:a:simple_video_embedder_project:simple_video_embedder:*:*:*:*:*:wordpress:*:*"
],
"simple-video-management-system": [
"cpe:2.3:a:maximize:simple_video_management_system:*:*:*:*:*:wordpress:*:*"
],
"simple-yearly-archive": [
"cpe:2.3:a:simple_yearly_archive_project:simple_yearly_archive:*:*:*:*:*:wordpress:*:*"
],
"simple-youtube-responsive": [
"cpe:2.3:a:simple_youtube_responsive_project:simple_youtube_responsive:*:*:*:*:*:wordpress:*:*"
],
"simplebooklet": [
"cpe:2.3:a:simplebooklet:simplebooklet_pdf_viewer_and_embedder:*:*:*:*:*:wordpress:*:*"
],
"simplemap": [
"cpe:2.3:a:simplemap-plugin:simplemap_store_locator:*:*:*:*:*:wordpress:*:*"
],
@@ -19448,6 +19573,9 @@
"spiffy-calendar": [
"cpe:2.3:a:spiffyplugins:spiffy_calendar:*:*:*:*:*:wordpress:*:*"
],
"spiritual-gifts-survey": [
"cpe:2.3:a:mynamedia:spiritual_gifts_survey_\\(and_optional_s.h.a.p.e_survey\\):*:*:*:*:*:wordpress:*:*"
],
"splash-header": [
"cpe:2.3:a:zeesweb:splash_header:*:*:*:*:*:wordpress:*:*"
],
@@ -20258,6 +20386,9 @@
"ttv-easy-embed-player": [
"cpe:2.3:a:streamweasels:twitch_player:*:*:*:*:*:wordpress:*:*"
],
"tube-video-ads-lite": [
"cpe:2.3:a:gualdoni:tube_video_ads_lite:*:*:*:*:*:wordpress:*:*"
],
"tubepress": [
"cpe:2.3:a:tubepress:tubepress:*:*:*:*:*:wordpress:*:*"
],
@@ -20416,6 +20547,9 @@
"cpe:2.3:a:ultimatemember:ultimate_member:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:ultimatemember:user_profile_\\\u0026_membership:*:*:*:*:*:wordpress:*:*"
],
"ultimate-noindex-nofollow-tool": [
"cpe:2.3:a:jonkemp:ultimate_noindex_nofollow_tool:*:*:*:*:*:wordpress:*:*"
],
"ultimate-noindex-nofollow-tool-ii": [
"cpe:2.3:a:ultimate_noindex_nofollow_tool_ii_project:ultimate_noindex_nofollow_tool_ii:*:*:*:*:*:wordpress:*:*"
],
@@ -20551,6 +20685,9 @@
"url-media-uploader": [
"cpe:2.3:a:apprhyme:url_media_uploader:*:*:*:*:*:wordpress:*:*"
],
"url-params": [
"cpe:2.3:a:jeremyshapiro:url_params:*:*:*:*:*:wordpress:*:*"
],
"url-shortify": [
"cpe:2.3:a:kaizencoders:url_shortify:*:*:*:*:*:wordpress:*:*"
],
@@ -20678,6 +20815,9 @@
"vayu-blocks": [
"cpe:2.3:a:themehunk:vayu_blocks:*:*:*:*:*:wordpress:*:*"
],
"vc-addons-by-bit14": [
"cpe:2.3:a:pagebuilderaddons:web_and_woocommerce_addons_for_wpbakery_builder:*:*:*:*:*:wordpress:*:*"
],
"vc-tabs": [
"cpe:2.3:a:oxilab:responsive_tabs:*:*:*:*:*:wordpress:*:*"
],
@@ -20780,6 +20920,9 @@
"visual-portfolio": [
"cpe:2.3:a:visualportfolio:visual_portfolio\\,_photo_gallery_\\\u0026_post_grid:*:*:*:*:*:wordpress:*:*"
],
"visual-sound": [
"cpe:2.3:a:freakingwildchild:visual_sound:*:*:*:*:*:wordpress:*:*"
],
"visualcomposer": [
"cpe:2.3:a:visualcomposer:visual_composer_website_builder:*:*:*:*:*:wordpress:*:*"
],
@@ -20950,6 +21093,9 @@
"web-application-firewall": [
"cpe:2.3:a:miniorange:web_application_firewall:*:*:*:*:*:wordpress:*:*"
],
"web-cam": [
"cpe:2.3:a:murtuzamakda52:web-cam:*:*:*:*:*:wordpress:*:*"
],
"web-directory-free": [
"cpe:2.3:a:salephpscripts:web_directory_free:*:*:*:*:*:wordpress:*:*"
],
@@ -21015,6 +21161,9 @@
"website-article-monetization-by-magenet": [
"cpe:2.3:a:magenet:website_article_monetization:*:*:*:*:*:wordpress:*:*"
],
"website-contact-form-with-file-upload": [
"cpe:2.3:a:najeebmedia:website_contact_form_with_file_upload:*:*:*:*:*:wordpress:*:*"
],
"website-file-changes-monitor": [
"cpe:2.3:a:melapress:melapress_file_monitor:*:*:*:*:*:wordpress:*:*"
],
@@ -21120,6 +21269,9 @@
"widgets-on-pages": [
"cpe:2.3:a:widgets-on-pages_project:widgets-on-pages:*:*:*:*:*:wordpress:*:*"
],
"widgets-reset": [
"cpe:2.3:a:justintadlock:widgets_reset:*:*:*:*:*:wordpress:*:*"
],
"winterlock": [
"cpe:2.3:a:wp_system_log_project:wp_system_log:*:*:*:*:*:wordpress:*:*"
],
@@ -21127,7 +21279,11 @@
"cpe:2.3:a:themeinprogress:wip_custom_login:*:*:*:*:*:wordpress:*:*"
],
"wise-chat": [
"cpe:2.3:a:kaine:wise_chat:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:kaine:wise_chat:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:kainex:wise_chat:*:*:*:*:*:wordpress:*:*"
],
"wise-forms": [
"cpe:2.3:a:kainex:wise_forms:*:*:*:*:*:wordpress:*:*"
],
"wiseagentleadform": [
"cpe:2.3:a:wiseagent:wise_agent_capture_forms:*:*:*:*:*:wordpress:*:*"
@@ -21227,6 +21383,9 @@
"woo-ecommerce-tracking-for-google-and-facebook": [
"cpe:2.3:a:multidots:enhanced_ecommerce_google_analytics_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
"woo-edit-templates": [
"cpe:2.3:a:ioannup:edit_woocommerce_templates:*:*:*:*:*:wordpress:*:*"
],
"woo-esto": [
"cpe:2.3:a:rebing:woocommerce_esto:*:*:*:*:*:wordpress:*:*"
],
@@ -21539,6 +21698,9 @@
"wordpress-access-control": [
"cpe:2.3:a:brandonwamboldt:wordpress_access_control:*:*:*:*:*:wordpress:*:*"
],
"wordpress-activity-o-meter": [
"cpe:2.3:a:erwinwolff:wordpress_activity-o-meter:*:*:*:*:*:wordpress:*:*"
],
"wordpress-backup-to-dropbox": [
"cpe:2.3:a:wordpress_backup_to_dropbox_project:wordpress_backup_to_dropbox:*:*:*:*:*:wordpress:*:*"
],
@@ -21598,6 +21760,9 @@
"wordpress-tooltips": [
"cpe:2.3:a:tooltips:wordpress_tooltips:*:*:*:*:free:wordpress:*:*"
],
"wordpress-twitterbot": [
"cpe:2.3:a:corbyboy:marketing_twitter_bot:*:*:*:*:*:wordpress:*:*"
],
"wordpress-users": [
"cpe:2.3:a:jonathonkemp:wordpress_users:*:*:*:*:*:wordpress:*:*"
],
@@ -21610,6 +21775,9 @@
"wow-carousel-for-divi-lite": [
"cpe:2.3:a:elegantthemes:divi_carousel:*:*:*:*:free:wordpress:*:*"
],
"wow-entrance-effects-wee": [
"cpe:2.3:a:darkosxrc:wow_entrance_effects_\\(wee\\!\\):*:*:*:*:*:wordpress:*:*"
],
"wow-moodboard-lite": [
"cpe:2.3:a:wownmedia:wow_moodboard:*:*:*:*:lite:wordpress:*:*"
],
@@ -21632,6 +21800,9 @@
"wp-accessibility-helper": [
"cpe:2.3:a:volkov:wp_accessibility_helper:*:*:*:*:*:wordpress:*:*"
],
"wp-action-network": [
"cpe:2.3:a:actionnetwork:action_network:*:*:*:*:*:wordpress:*:*"
],
"wp-admin-logo-changer": [
"cpe:2.3:a:wp_admin_logo_changer_project:wp_admin_logo_changer:*:*:*:*:*:wordpress:*:*"
],
@@ -21641,6 +21812,9 @@
"wp-airbnb-review-slider": [
"cpe:2.3:a:ljapps:wp_airbnb_review_slider:*:*:*:*:*:wordpress:*:*"
],
"wp-ajax-contact-form": [
"cpe:2.3:a:sportsdanka:wp_ajax_contact_form:*:*:*:*:*:wordpress:*:*"
],
"wp-all-export": [
"cpe:2.3:a:soflyy:export_any_wordpress_data_to_xml\\/csv:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:soflyy:wp_all_export:*:*:*:*:pro:wordpress:*:*"
@@ -22034,6 +22208,9 @@
"wp-email-capture": [
"cpe:2.3:a:winwar:wp_email_capture:*:*:*:*:*:wordpress:*:*"
],
"wp-email-newsletter": [
"cpe:2.3:a:megamindstechnologies:wordpress_email_newsletter:*:*:*:*:*:wordpress:*:*"
],
"wp-email-users": [
"cpe:2.3:a:techspawn:wp-email-users:*:*:*:*:*:wordpress:*:*"
],
@@ -22168,6 +22345,9 @@
"wp-gdpr-core": [
"cpe:2.3:a:appsaloon:wp-gdpr:*:*:*:*:*:wordpress:*:*"
],
"wp-geshi-highlight": [
"cpe:2.3:a:jgehrcke:wp-geshi-highlight:*:*:*:*:*:wordpress:*:*"
],
"wp-githuber-md": [
"cpe:2.3:a:terryl:wp_githuber_md:*:*:*:*:*:wordpress:*:*"
],
@@ -22261,6 +22441,9 @@
"wp-image-slideshow": [
"cpe:2.3:a:gopiplus:wp_image_slideshow:*:*:*:*:*:wordpress:*:*"
],
"wp-image-uploader": [
"cpe:2.3:a:ivanm:wp_image_uploader:*:*:*:*:*:wordpress:*:*"
],
"wp-image-zoooom": [
"cpe:2.3:a:silkypress:wp_image_zoom:*:*:*:*:*:wordpress:*:*"
],
@@ -22460,6 +22643,9 @@
"wp-multi-store-locator": [
"cpe:2.3:a:wpexperts:wp_multi_store_locator:*:*:*:*:*:wordpress:*:*"
],
"wp-multitasking": [
"cpe:2.3:a:ngothang:wp_multitasking:*:*:*:*:*:wordpress:*:*"
],
"wp-nested-pages": [
"cpe:2.3:a:kylephillips:nested_pages:*:*:*:*:*:wordpress:*:*"
],
@@ -22472,6 +22658,9 @@
"wp-olivecart": [
"cpe:2.3:a:wp-olivecart_project:wp-olivecart:*:*:*:*:*:wordpress:*:*"
],
"wp-online-users-stats": [
"cpe:2.3:a:hk1993:wp_online_users_stats:*:*:*:*:*:wordpress:*:*"
],
"wp-open-graph": [
"cpe:2.3:a:custom4web:wp_open_graph:*:*:*:*:*:wordpress:*:*"
],
@@ -22903,6 +23092,9 @@
"wp-symposium-pro": [
"cpe:2.3:a:wpsymposiumpro:wp_symposium_pro:*:*:*:*:*:wordpress:*:*"
],
"wp-syntax": [
"cpe:2.3:a:connections-pro:wp-syntax:*:*:*:*:*:wordpress:*:*"
],
"wp-t-wap": [
"cpe:2.3:a:wp-t-wap_project:wp-t-wap:*:*:*:*:*:wordpress:*:*"
],
@@ -22948,6 +23140,9 @@
"wp-timed-popup": [
"cpe:2.3:a:timed_popup_project:timed_popup:*:*:*:*:*:wordpress:*:*"
],
"wp-tithely": [
"cpe:2.3:a:jordangillman:tithe.ly_giving_button:*:*:*:*:*:wordpress:*:*"
],
"wp-todo": [
"cpe:2.3:a:delower:wp_to_do:*:*:*:*:*:wordpress:*:*"
],
@@ -23570,6 +23765,9 @@
"z-url-preview": [
"cpe:2.3:a:z-url_preview_project:z-url_preview:*:*:*:*:*:wordpress:*:*"
],
"zalomeni": [
"cpe:2.3:a:kybernaut:zalomeni:*:*:*:*:*:wordpress:*:*"
],
"zarinpal-paid-downloads": [
"cpe:2.3:a:amini7:zarinpal_paid_download:*:*:*:*:*:wordpress:*:*"
],
@@ -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
@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/internal/testutils"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/stereoscopesource"
@@ -199,6 +200,7 @@ func fixtureResolvers(t *testing.T, fixtures ...string) map[string]file.Resolver
continue
}
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", fixture)
s := stereoscopesource.New(img, stereoscopesource.ImageConfig{
@@ -24,6 +24,7 @@ import (
"github.com/anchore/syft/internal/cmptest"
"github.com/anchore/syft/internal/licenses"
"github.com/anchore/syft/internal/relationship"
"github.com/anchore/syft/internal/testutils"
"github.com/anchore/syft/internal/tmpdir"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
@@ -150,9 +151,11 @@ func (p *CatalogTester) FromFile(t *testing.T, path string) *CatalogTester {
fixture, err := os.Open(path)
require.NoError(t, err)
// parsers don't own the reader (the generic cataloger closes it), so close it here
t.Cleanup(func() { _ = fixture.Close() })
p.reader = file.LocationReadCloser{
Location: file.NewVirtualLocationFromDirectory(fixture.Name(), fixture.Name(), *stereofile.NewFileReference(stereofile.Path(absPath))),
Location: file.NewVirtualLocationFromDirectory(filepath.ToSlash(fixture.Name()), filepath.ToSlash(fixture.Name()), *stereofile.NewFileReference(stereofile.Path(absPath))),
ReadCloser: fixture,
}
return p
@@ -201,6 +204,7 @@ func (p *CatalogTester) WithImageResolver(t *testing.T, fixtureName string) *Cat
return p
}
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", fixtureName)
s := stereoscopesource.New(img, stereoscopesource.ImageConfig{
@@ -6,6 +6,7 @@ import (
"io"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"syscall"
@@ -18,6 +19,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/internal/testutils"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/license"
@@ -1605,6 +1607,9 @@ func generateJavaBuildFixture(t *testing.T, fixturePath string) {
return
}
// the fixtures are built with maven/gradle inside linux containers
testutils.SkipWithoutLinuxContainers(t)
makeTask := strings.TrimPrefix(fixturePath, "testdata/java-builds/")
t.Log(color.Bold.Sprintf("Generating Fixture from 'make %s'", makeTask))
@@ -1630,7 +1635,8 @@ func generateJavaMetadataJarFixture(t *testing.T, fixtureName string, fileExtens
return fixturePath
}
makeTask := filepath.Join("cache", fixtureName+"."+fileExtension)
// make targets are posix paths regardless of host
makeTask := path.Join("cache", fixtureName+"."+fileExtension)
t.Log(color.Bold.Sprintf("Generating Fixture from 'make %s'", makeTask))
cwd, err := os.Getwd()
@@ -47,6 +47,8 @@ func Test_defaultMavenLocalRepoDir(t *testing.T) {
home, err := filepath.Abs(test.home)
require.NoError(t, err)
t.Setenv("HOME", home)
// os.UserHomeDir reads USERPROFILE on windows
t.Setenv("USERPROFILE", home)
}
got := defaultMavenLocalRepoDir()
require.Equal(t, test.expected, got)
@@ -31,7 +31,7 @@ func MockRepo(t *testing.T, dir string) (url string) {
t.Cleanup(server.Close)
matches, err := doublestar.Glob(os.DirFS(dir), filepath.Join("**", "*.pom"))
matches, err := doublestar.Glob(os.DirFS(dir), "**/*.pom")
require.NoError(t, err)
for _, match := range matches {
+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")
}
+569
View File
@@ -0,0 +1,569 @@
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/internal/testutils"
"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()
testutils.SkipWithoutLinuxContainers(t)
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
}

Some files were not shown because too many files have changed in this diff Show More