Compare commits

..
30 Commits
Author SHA1 Message Date
anchore-oss-update-botandanchore-oss-update-bot ddc490c5c5 chore(deps): update anchore dependencies (#5387)
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-12 16:29:27 -04:00
dependabot[bot] 46bac23392 chore(deps): bump golang.org/x/tools from 0.50.0 to 0.51.0 (#5417)
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.50.0 to 0.51.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.50.0...v0.51.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-version: 0.51.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-12 20:23:06 +00:00
dependabot[bot] 07752f0741 chore(deps): bump github.com/go-git/go-git/v5 from 5.19.2 to 5.19.3 (#5416)
Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.19.2 to 5.19.3.
- [Release notes](https://github.com/go-git/go-git/releases)
- [Changelog](https://github.com/go-git/go-git/blob/main/HISTORY.md)
- [Commits](https://github.com/go-git/go-git/compare/v5.19.2...v5.19.3)

---
updated-dependencies:
- dependency-name: github.com/go-git/go-git/v5
  dependency-version: 5.19.3
  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-12 20:21:35 +00:00
Alex Goodman f8ffa7382e chore: bump go-make module and action together (#5415)
The go-make module and its action need to move in lockstep, but dependabot bumps them in separate PRs that drift apart. Automation now bumps both together, so dependabot ignores go-make in both ecosystems.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-10-12 15:57:20 -04:00
Alex Goodman c2ea35ef32 chore: add COPYRIGHT file and update CONTRIBUTORS.md (#5414)
Fixes #5394

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-10-12 15:00:02 -04:00
anchore-oss-update-botandanchore-oss-update-bot c0109a7ad1 chore(deps): update CPE dictionary index (#5409)
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-12 01:56:57 +00:00
dependabot[bot] 33419c8f39 chore(deps): bump github.com/pb33f/ordered-map/v2 from 2.3.1 to 2.3.2 (#5400)
Bumps [github.com/pb33f/ordered-map/v2](https://github.com/pb33f/ordered-map) from 2.3.1 to 2.3.2.
- [Changelog](https://github.com/pb33f/ordered-map/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pb33f/ordered-map/compare/v2.3.1...v2.3.2)

---
updated-dependencies:
- dependency-name: github.com/pb33f/ordered-map/v2
  dependency-version: 2.3.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-09 13:42:52 +00:00
dependabot[bot] 6790272518 chore(deps): bump github.com/anchore/go-make in /.make (#5402)
Bumps [github.com/anchore/go-make](https://github.com/anchore/go-make) from 0.8.1 to 0.9.1.
- [Release notes](https://github.com/anchore/go-make/releases)
- [Commits](https://github.com/anchore/go-make/compare/v0.8.1...v0.9.1)

---
updated-dependencies:
- dependency-name: github.com/anchore/go-make
  dependency-version: 0.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-09 13:37:22 +00:00
Peter McConnell 740beb6dc4 fix: search for the .NET bundle marker without holding the file (#5393)
Signed-off-by: Peter McConnell <peter.mcconnell@upwind.io>
2026-10-09 09:36:52 -04:00
dependabot[bot] 3e41abadc4 chore(deps): bump github.com/go-git/go-billy/v5 from 5.9.1 to 5.9.2 (#5399)
Bumps [github.com/go-git/go-billy/v5](https://github.com/go-git/go-billy) from 5.9.1 to 5.9.2.
- [Release notes](https://github.com/go-git/go-billy/releases)
- [Commits](https://github.com/go-git/go-billy/compare/v5.9.1...v5.9.2)

---
updated-dependencies:
- dependency-name: github.com/go-git/go-billy/v5
  dependency-version: 5.9.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-09 13:35:51 +00:00
dependabot[bot] 0a050b0e27 chore(deps): bump github.com/gpustack/gguf-parser-go (#5401)
Bumps [github.com/gpustack/gguf-parser-go](https://github.com/gpustack/gguf-parser-go) from 0.26.3 to 0.26.4.
- [Release notes](https://github.com/gpustack/gguf-parser-go/releases)
- [Commits](https://github.com/gpustack/gguf-parser-go/compare/v0.26.3...v0.26.4)

---
updated-dependencies:
- dependency-name: github.com/gpustack/gguf-parser-go
  dependency-version: 0.26.4
  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-09 13:35:03 +00:00
dependabot[bot] fc8bd6c9d8 chore(deps): bump anchore/sbom-action from 0.24.2 to 0.24.3 (#5395)
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.24.2 to 0.24.3.
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](https://github.com/anchore/sbom-action/compare/3ad7283483fc7af8ff2b4ea19663c2d5ca935e26...66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-version: 0.24.3
  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-09 13:31:27 +00:00
dependabot[bot] 7e713d106e chore(deps): bump modernc.org/sqlite from 1.59.0 to 1.60.1 (#5398)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.59.0 to 1.60.1.
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.59.0...v1.60.1)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.60.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-09 13:29:55 +00:00
dependabot[bot] 1f5ac47990 chore(deps): bump anchore/go-make/.github/actions/setup (#5397)
Bumps [anchore/go-make/.github/actions/setup](https://github.com/anchore/go-make) from 0.8.1 to 0.9.1.
- [Release notes](https://github.com/anchore/go-make/releases)
- [Commits](https://github.com/anchore/go-make/compare/fa5421b3bf24c9d18f11a736c5eb4172f2b7d683...27a9f35f41b5fe86d68815844efd2ba816e9ba6b)

---
updated-dependencies:
- dependency-name: anchore/go-make/.github/actions/setup
  dependency-version: 0.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-09 13:29:42 +00:00
dependabot[bot] 28e38c29f3 chore(deps): bump anchore/go-make/.github/actions/setup (#5396)
Bumps [anchore/go-make/.github/actions/setup](https://github.com/anchore/go-make) from 0.8.1 to 0.9.1.
- [Release notes](https://github.com/anchore/go-make/releases)
- [Commits](https://github.com/anchore/go-make/compare/fa5421b3bf24c9d18f11a736c5eb4172f2b7d683...27a9f35f41b5fe86d68815844efd2ba816e9ba6b)

---
updated-dependencies:
- dependency-name: anchore/go-make/.github/actions/setup
  dependency-version: 0.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-09 13:27:40 +00:00
Silas ParkerandAlex Goodman 546c872c9d Add support for scanning IPK packages using the deb-archive-cataloger (#5111)
* Add support for scanning IPK packages using the deb-archive-cataloger

Signed-off-by: Silas Parker <silas@srp.me.uk>

* fix(debian): share License field parsing between opkg status and archive control files

- the control-file `License:` fallback now goes through the same helper as the opkg status DB path, so a package reports the same licenses whether found installed or as an archive
- bitbake `&` / `|` are rewritten to SPDX `AND` / `OR` and the whole value is validated as one expression instead of split by hand, which kept grouping like `(A & B) | C` intact
- non-SPDX values fall back to a deduped list of names with operators and parens dropped (previously the status DB path emitted `&`, `(BSD`, etc as licenses)
- fields over 4KB are ignored, bounding work per archive and keeping deep paren nesting away from the recursive expression parser

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

* fix(debian): split Yocto recipe filenames in the Source field

Yocto writes the bitbake recipe filename (`Source: zlib_1.3.2.bb`) where dpkg expects a source package name. This is now split into `Source: zlib` / `SourceVersion: 1.3.2` for both ipk archives and opkg status DB entries, instead of surfacing the `.bb` filename as the upstream package.

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

* fix(debian): do not label ipk archives as Debian packages

- `.ipk` archives no longer get a `pkg:deb` PURL from the assumed debian distro; opkg-based distros (Yocto, OpenWrt) have no purl type, so these now match packages found in an opkg status DB, which already get no PURL
- docs now cover `.ipk` and note that only ar-framed ipks are supported (OpenWrt's older gzipped-tar framing is rejected as an invalid archive)

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

* test(debian): assemble the ipk fixture at build time instead of committing it

The fixture Dockerfile now builds a Yocto-shaped `.ipk` from a checked-in `control/` directory (copied from the original poky-tiny build) using `tar`, `zstd` and binutils `ar`, the same way bitbake's `package_ipk` packs one. This drops the prebuilt binary and the 20-30 minute Yocto `Dockerfile.build` while keeping the same archive layout and control metadata.

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

---------

Signed-off-by: Silas Parker <silas@srp.me.uk>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-10-07 20:41:13 +00:00
MetalandAlex Goodman b0a8de7bf6 fix(redact): reset redaction store between command runs (#5386)
* fix(redact): reset redaction store between command runs

cli.Command() may be invoked more than once in the same process (e.g.
when syft is embedded as a library). The clio initializer calls
internal/redact.Set on every command execution, but the redact store is
process-global and Set panics when a store already exists, so the second
invocation dies with "replace existing redaction store (probably
unintentional)".

Add redact.Reset() to clear the previous run's store and call it in the
initializer before Set. The double-Set guard in Set is kept: an
unexpected second Set within a single run still panics.

Add TestAppClioSetupConfigInitializerCanRunMultipleTimes which runs the
initializer twice; it panics with the exact reported message on the old
code and passes with the fix.

Fixes #2285

Signed-off-by: JasonMetal <935216773@qq.com>

* fix(redact): release the redact store at the end of each run

Clear the global redact store in the post-run hook instead of resetting it
before every `Set`. The double-`Set` panic keeps its meaning: a store is only
present while a run is in flight, so a second run starting mid-run still
panics rather than splitting secrets across two stores and leaking the ones
in the dropped store. Sequential runs in the same process work since the
previous run releases its store on the way out.

A run only releases the store it set, so it never clears a store owned by
someone else.

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

---------

Signed-off-by: JasonMetal <935216773@qq.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-10-07 20:28:15 +00:00
anchore-oss-update-botandanchore-oss-update-bot d9489c2230 chore(deps): update tool versions (#5381)
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-07 10:27:20 +00:00
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
107 changed files with 2427 additions and 390 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ tools:
# used to parse JSON/YAML annotations on the fixture cache image
- name: yq
version:
want: v4.53.6
want: v4.54.1
method: github-release
with:
repo: mikefarah/yq
+1 -1
View File
@@ -30,7 +30,7 @@ runs:
using: "composite"
steps:
- name: Setup go + go-make tooling
uses: anchore/go-make/.github/actions/setup@fa5421b3bf24c9d18f11a736c5eb4172f2b7d683 # v0.8.1
uses: anchore/go-make/.github/actions/setup@abf41688f36f2cbec13105747abbe8e60f5c56ca # v0.9.3
with:
go-version: ${{ inputs.go-version }}
cache-key-prefix: ${{ inputs.cache-key-prefix }}
+8
View File
@@ -21,6 +21,11 @@ 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"]
# go-make module and action refs are bumped together by automation
- dependency-name: "github.com/anchore/go-make"
- package-ecosystem: "github-actions"
directories:
@@ -34,3 +39,6 @@ updates:
open-pull-requests-limit: 10
labels:
- "dependencies"
ignore:
# go-make module and action refs are bumped together by automation
- dependency-name: "anchore/go-make/*"
+2 -2
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]
@@ -118,7 +118,7 @@ jobs:
# for updating brew formula in anchore/homebrew-syft
GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}
- uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 #v0.24.2
- uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c #v0.24.3
continue-on-error: true
with:
file: go.mod
+90 -5
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
uses: anchore/go-make/.github/actions/setup@abf41688f36f2cbec13105747abbe8e60f5c56ca # v0.9.3
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@abf41688f36f2cbec13105747abbe8e60f5c56ca # v0.9.3
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@abf41688f36f2cbec13105747abbe8e60f5c56ca # v0.9.3
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
+5 -5
View File
@@ -1,12 +1,12 @@
module github.com/anchore/syft/.make
go 1.25.8
go 1.26.0
require github.com/anchore/go-make v0.8.1
require github.com/anchore/go-make v0.9.3
require (
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.2 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
golang.org/x/mod v0.40.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/mod v0.41.0 // indirect
golang.org/x/sys v0.48.0 // indirect
)
+8 -8
View File
@@ -1,10 +1,10 @@
github.com/anchore/go-make v0.8.1 h1:GAhPXR+9qVQhFB1pfVlHFOzxAt3dTDTUIA7Tp7nx9tg=
github.com/anchore/go-make v0.8.1/go.mod h1:xALbVsPsDoJnJxdkjPlxOLZV7GbAdT5Pv3gd0kP1Lz0=
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/anchore/go-make v0.9.3 h1:D+MkpvEJsOGwkOqKN0rd2zYi9jEzwKtZreNysH0DenI=
github.com/anchore/go-make v0.9.3/go.mod h1:4HuELygGhxex2SaCEgfCbZ0geJ3xFkqtWpmt1NrYwPg=
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/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+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 {
+1 -1
View File
@@ -1,6 +1,6 @@
# Syft Contributors
The following Syft components were contributed by external authors/organizations.
This file credits notable contributions from external authors and organizations; see `git log` for everyone who has contributed to Syft.
## GraalVM Native Image
+4
View File
@@ -0,0 +1,4 @@
Copyright (c) Anchore, Inc.
See LICENSE for the full license text.
See CONTRIBUTORS.md and git log for a full list of contributors.
+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.
+25
View File
@@ -0,0 +1,25 @@
package cli
import (
"io"
"testing"
"github.com/stretchr/testify/require"
"github.com/anchore/clio"
"github.com/anchore/syft/internal/redact"
)
func TestCommandCanRunMultipleTimes(t *testing.T) {
// https://github.com/anchore/syft/issues/2285
t.Cleanup(redact.Reset)
for i := 0; i < 2; i++ {
_, cmd := create(clio.Identification{Name: "syft"}, io.Discard)
cmd.SetArgs([]string{"cataloger", "list", "-o", "json"})
require.NoError(t, cmd.Execute())
// the store is released at the end of each run so the next one can set its own
require.Nil(t, redact.Get())
}
}
+9 -1
View File
@@ -41,6 +41,8 @@ func AppClioSetupConfig(id clio.Identification, out io.Writer) *clio.SetupConfig
stereoscope.SetBus(state.Bus)
bus.Set(state.Bus)
// the redact store is process-global and only cleared when a run ends (see the post-run below), so
// setting it while another run is still in flight panics rather than splitting secrets across stores.
redact.Set(state.RedactStore)
log.Set(state.Logger)
@@ -48,8 +50,14 @@ func AppClioSetupConfig(id clio.Identification, out io.Writer) *clio.SetupConfig
return nil
},
).
WithPostRuns(func(_ *clio.State, _ error) {
WithPostRuns(func(state *clio.State, _ error) {
stereoscope.Cleanup() //nolint:staticcheck // we don't have access to the image object here
// release the redact store now that nothing in this run can report anymore, so a later run in the same
// process (e.g. syft embedded as a library) can set its own. Only release the store this run set.
if redact.Get() == state.RedactStore {
redact.Reset()
}
})
return clioCfg
}
@@ -0,0 +1,32 @@
package internal
import (
"io"
"testing"
"github.com/stretchr/testify/require"
"github.com/anchore/clio"
"github.com/anchore/go-logger/adapter/discard"
gologgerredact "github.com/anchore/go-logger/adapter/redact"
"github.com/anchore/syft/internal/redact"
)
func TestAppClioSetupConfigInitializerPanicsWhileRunInFlight(t *testing.T) {
t.Cleanup(redact.Reset)
cfg := AppClioSetupConfig(clio.Identification{Name: "syft"}, io.Discard)
require.Len(t, cfg.Initializers, 1)
newState := func() *clio.State {
return &clio.State{Logger: discard.New(), RedactStore: gologgerredact.NewStore()}
}
require.NoError(t, cfg.Initializers[0](newState()))
// a second run starting before the first has finished must not silently replace the store, otherwise
// secrets added to the first store would no longer be redacted
require.PanicsWithValue(t, "replace existing redaction store (probably unintentional)", func() {
_ = cfg.Initializers[0](newState())
})
}
+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))
+19 -20
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.3
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
@@ -41,8 +41,8 @@ require (
github.com/facebookincubator/nvdtools v0.1.5
github.com/github/go-spdx/v2 v2.7.0
github.com/gkampitakis/go-snaps v0.5.23
github.com/go-git/go-billy/v5 v5.9.1
github.com/go-git/go-git/v5 v5.19.2
github.com/go-git/go-billy/v5 v5.9.2
github.com/go-git/go-git/v5 v5.19.3
github.com/go-test/deep v1.1.1
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/goccy/go-yaml v1.19.2
@@ -52,7 +52,7 @@ require (
github.com/google/licensecheck v0.3.1
github.com/google/uuid v1.6.0
github.com/gookit/color v1.6.1
github.com/gpustack/gguf-parser-go v0.26.3
github.com/gpustack/gguf-parser-go v0.26.4
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-getter v1.8.9
github.com/hashicorp/go-multierror v1.1.1
@@ -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
@@ -96,12 +96,12 @@ require (
golang.org/x/mod v0.41.0
golang.org/x/net v0.59.0
golang.org/x/time v0.16.0
golang.org/x/tools v0.50.0
modernc.org/sqlite v1.59.0
golang.org/x/tools v0.51.0
modernc.org/sqlite v1.60.1
)
require (
github.com/pb33f/ordered-map/v2 v2.3.1
github.com/pb33f/ordered-map/v2 v2.3.2
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd
howett.net/plist v1.0.1
)
@@ -188,7 +188,7 @@ require (
github.com/containers/ocicrypt v1.3.2 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
github.com/cyphar/filepath-securejoin v0.7.0 // indirect
github.com/docker/cli v29.8.1+incompatible // indirect
github.com/docker/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
@@ -224,7 +224,6 @@ require (
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
@@ -265,11 +264,12 @@ require (
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/pb33f/go-yaml v0.1.0 // 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
github.com/piprate/json-gold v0.7.0 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/pjbgf/sha1cd v0.7.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/profile v1.7.0 // indirect
github.com/pkg/xattr v0.4.12 // indirect
@@ -281,11 +281,11 @@ require (
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.11.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sigstore/fulcio v1.8.7 // indirect
github.com/sigstore/protobuf-specs v0.5.1 // indirect
github.com/sigstore/sigstore v1.10.8 // indirect
github.com/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
@@ -299,7 +299,7 @@ require (
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
@@ -328,7 +328,6 @@ require (
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
golang.org/x/oauth2 v0.36.0 // indirect
@@ -345,7 +344,7 @@ require (
google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.12 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
modernc.org/libc v1.75.7 // indirect
modernc.org/libc v1.77.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.12.1 // indirect
)
+44 -46
View File
@@ -140,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=
@@ -150,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.3 h1:sur8G2THAOnOrSXDjIc1SXzQwco8hjFBtGFq1jFvNVA=
github.com/anchore/stereoscope v0.3.3/go.mod h1:LxL4iOFtB8XMbA1MykUUTkmtQmDuYdbr4+TkBkCfljU=
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=
@@ -229,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=
@@ -353,8 +353,8 @@ 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.1+incompatible h1:qYL1bCp6cRw2SB1xmLlIOPyV171dilw9W2Jew38vy9c=
github.com/docker/cli v29.8.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
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=
@@ -428,18 +428,18 @@ 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=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.9.1 h1:8U73XiOTfINdItHVa6z4Gv7ToObcZ6grkqQbLryLCdA=
github.com/go-git/go-billy/v5 v5.9.1/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
github.com/go-git/go-billy/v5 v5.9.2 h1:OXFSRyz4g20upsGDJgQG9Bak1l/ZEv8GHVYB52O71sE=
github.com/go-git/go-billy/v5 v5.9.2/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
github.com/go-git/go-git/v5 v5.19.3 h1:qHttbZ+Am7wEjdTpR1XMQ4rl42FK9SIXzZ4o9x7s6M4=
github.com/go-git/go-git/v5 v5.19.3/go.mod h1:Ye4C8dVigkqrv9UsB4NMdSLV4yAIkLiIhW61gcOyhl4=
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=
@@ -579,8 +579,8 @@ 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/gpustack/gguf-parser-go v0.26.4 h1:B9CRrQBcIB7I/MJFZvzPZ+e+cbc1xBzQJcmpi9l/mKU=
github.com/gpustack/gguf-parser-go v0.26.4/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 h1:mymLUKThnV9wFvogOK8NnsMP9/vlhnjXY98gr2QIGW8=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74/go.mod h1:Bh9qYL8ehmDxSg14Tk8oxFCP90XHfs6NxV1D84884xA=
@@ -666,11 +666,9 @@ 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=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -808,8 +806,10 @@ github.com/opencontainers/selinux v1.15.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZ
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=
github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY=
github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ=
github.com/pb33f/go-yaml v0.1.0 h1:zZ6Gg+ATbumXlXZNLMHIe7+zTkVqe20lx+TSYRrXEuY=
github.com/pb33f/go-yaml v0.1.0/go.mod h1:QvWdkHP5VjFYAeHMQUkaPQqREaQnBNOXmWzAjNrnSdo=
github.com/pb33f/ordered-map/v2 v2.3.2 h1:wDyaZ2Pv9QLh64X4utCeD5Zoi9nIv2aW3lwv172O5PQ=
github.com/pb33f/ordered-map/v2 v2.3.2/go.mod h1:1OhFrXu3OYw3kM+FXF+Ug3ugmmZ9LE8z0JfQMLvtV0w=
github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM=
github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
@@ -821,8 +821,8 @@ github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/piprate/json-gold v0.7.0 h1:bEMirgA5y8Z2loTQfxyIFfY+EflxH1CTP6r/KIlcJNw=
github.com/piprate/json-gold v0.7.0/go.mod h1:RVhE35veDX19r5gfUAR+IYHkAUuPwJO8Ie/qVeFaIzw=
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pjbgf/sha1cd v0.7.0 h1:ZRNPKHj+gfkLBf0KJv/p1Hmz+7bqCT5o311YX0nq+DA=
github.com/pjbgf/sha1cd v0.7.0/go.mod h1:pKR5Li+qTCo+ebqITZfwPlJGoCFCvSO00qxjbNtTUFQ=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -893,8 +893,8 @@ github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd7
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=
github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/secure-systems-lab/go-securesystemslib v0.11.0 h1:iuCR9kcMFD4QurdKrGvPLoKZLv9YvwPYVr0473BdtFs=
github.com/secure-systems-lab/go-securesystemslib v0.11.0/go.mod h1:+PMOTjUGwHj2vcZ+TFKlb1tXRbrdWE1LYDT5i9JC80Q=
github.com/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=
@@ -903,10 +903,10 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sigstore/fulcio v1.8.7 h1:d7QJQxukgXarqPCT9uDNeyzt1Bbt/biw58/VfOnwI/o=
github.com/sigstore/fulcio v1.8.7/go.mod h1:7fX+QyigZxRGKTgHvTfOfxwwniCDxK02PVloHy5/vg4=
github.com/sigstore/protobuf-specs v0.5.1 h1:/5OPaNuolRJmQfeZLayJGFXMpsRJEdgC6ah1/+7Px7U=
github.com/sigstore/protobuf-specs v0.5.1/go.mod h1:DRBzpFuE+LnvQMN10/dU6nBeKwVLGEQ6o2FovN2Rats=
github.com/sigstore/sigstore v1.10.8 h1:1Mgkxvkw4AXMfIP1DOjc6kw0GkUgA8pGVpveN/EfOq4=
github.com/sigstore/sigstore v1.10.8/go.mod h1:f9+B/4iaYimvUkySyb2mvc73n3RLqNn24grHZM/ET8M=
github.com/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=
@@ -970,8 +970,8 @@ 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=
@@ -994,8 +994,8 @@ 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=
@@ -1084,8 +1084,6 @@ go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -1412,8 +1410,8 @@ 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/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1610,10 +1608,10 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
modernc.org/cc/v4 v4.29.2 h1:h6+9ciCnPKutf4I03CvheAvDLX7+IHlqR6Iy6J+cgd8=
modernc.org/cc/v4 v4.29.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.35.0 h1:F+TUsmw09QxLzmi3aeYYGxjAXarmZaKgj3mKQHNaA8w=
modernc.org/ccgo/v4 v4.35.0/go.mod h1:qrVGs9S3Sr2Ztcg9ve+kTAYMp5a3YvWjo+SoN06kJ5I=
modernc.org/cc/v4 v4.29.7 h1:q+NXGJ0bK3b4TXFYQQVr9pYETGnmwFWkrUzJnMya/Tg=
modernc.org/cc/v4 v4.29.7/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.36.1 h1:ZNIUZAryN0UgnJwtyxrdEzcFc3yD4Cu4AzjfPXsLsIE=
modernc.org/ccgo/v4 v4.36.1/go.mod h1:rrtGc2QkS239nYb/mQNuBMyjq3/y3ZXWbBjPoV3wqzA=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
@@ -1622,8 +1620,8 @@ modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc=
modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.75.7 h1:o3DTP9/0p9pKmY2WCKQaySW6wIiZhNM7wc2lUoyhfew=
modernc.org/libc v1.75.7/go.mod h1:bO5o2ztHxBb2rjz0PgdHN0sSMw57CgxGFLZ3Qd/QpVQ=
modernc.org/libc v1.77.1 h1:Ct8j47QtiZ1Enj2DtFXQtUqrPCAjdCmPjtCuvrYQ0Hs=
modernc.org/libc v1.77.1/go.mod h1:87/pZ4L6nD1zqW4nItuS12YO7hN1igAah34xjnQo/W0=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
@@ -1632,8 +1630,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.59.0 h1:X1es1GpqBlS/5T+vbM4HLUdaa8OtQx468DF2vrx+38A=
modernc.org/sqlite v1.59.0/go.mod h1:+paeT2A3iPRHkQDwG7oA6Tk0zQd5woMEI8q7orfry8k=
modernc.org/sqlite v1.60.1 h1:/blz53O951KWFOso4QQvEs/Fq6cDBKLtMVrYNSeJVKw=
modernc.org/sqlite v1.60.1/go.mod h1:1dIoEagfDE72QytD5scH1lxARtaUgKgHC/NuApA27r0=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+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)
})
}
}
+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
+8
View File
@@ -13,6 +13,14 @@ func Set(s redact.Store) {
store = s
}
// Reset clears the global redaction store. It must only be called once a command
// run has fully finished (nothing left to Add or Apply), so that a later run can
// Set its own store. Calling it while a run is in flight would let a second Set
// split secrets across two stores and leak the ones in the dropped store.
func Reset() {
store = nil
}
func Get() redact.Store {
return store
}
+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)
@@ -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{
@@ -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())
@@ -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"
@@ -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,
@@ -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{
@@ -75,6 +75,7 @@ catalogers:
method: glob # AUTO-GENERATED
criteria: # AUTO-GENERATED
- '**/*.deb'
- '**/*.ipk'
metadata_types: # AUTO-GENERATED
- pkg.DpkgArchiveEntry
package_types: # AUTO-GENERATED
+2 -2
View File
@@ -18,8 +18,8 @@ func NewDBCataloger() pkg.Cataloger {
WithProcessors(dependency.Processor(dbEntryDependencySpecifier))
}
// NewArchiveCataloger returns a new Debian package cataloger object capable of parsing .deb archive files
// NewArchiveCataloger returns a new Debian package cataloger object capable of parsing .deb and .ipk (opkg) archive files
func NewArchiveCataloger() pkg.Cataloger {
return generic.NewCataloger("deb-archive-cataloger").
WithParserByGlobs(parseDebArchive, "**/*.deb")
WithParserByGlobs(parseDebArchive, "**/*.deb", "**/*.ipk")
}
@@ -276,6 +276,36 @@ func TestDpkgArchiveCataloger(t *testing.T) {
},
},
},
}, {
name: "image-single-ipk",
expected: []pkg.Package{
{
Name: "libz1",
Version: "1.3.2-r0",
FoundBy: "deb-archive-cataloger",
Locations: file.NewLocationSet(
file.NewLocation("/libz1_1.3.2-r0_x86-64-v3.ipk"),
),
Type: pkg.DebPkg,
Metadata: pkg.DpkgArchiveEntry{
Package: "libz1",
Source: "zlib",
SourceVersion: "1.3.2",
Version: "1.3.2-r0",
Architecture: "x86-64-v3",
Maintainer: "Poky Maintainers <poky@lists.yoctoproject.org>",
InstalledSize: 0,
Description: `Zlib Compression Library
Zlib is a general-purpose, patent-free, lossless data compression library
which is used by many different programs.`,
Provides: []string{"zlib"},
Depends: []string{"musl (>= 1.2.6+git0+9fa28ece75)"},
},
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocationsWithContext(ctx, "Zlib"),
),
},
},
},
}
+61 -15
View File
@@ -9,6 +9,8 @@ import (
"sort"
"strings"
"github.com/scylladb/go-set/strset"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
@@ -61,32 +63,65 @@ func newDpkgPackage(ctx context.Context, d dpkgExtractedMetadata, dbLocation fil
return p
}
// a declared License field holds a single expression, a few hundred bytes at most in practice. Anything
// much larger only feeds the expression parser (which recurses on parentheses) attacker-sized input.
const maxDeclaredLicenseLen = 4096
var (
// opkg/ipkg (Yocto via bitbake) declare licenses with "&" for AND and "|" for OR
bitbakeLicenseOperators = strings.NewReplacer("&", " AND ", "|", " OR ")
tightenParens = strings.NewReplacer("( ", "(", " )", ")")
licenseListSeparators = strings.NewReplacer("(", " ", ")", " ")
)
// extractDeclaredLicenses converts a License field from the status DB into a license set. Returns nil
// for empty input so standard dpkg entries (which never declare License inline) incur no allocation.
// Mirrors the alpine cataloger's approach: keep the value whole if it parses as a valid SPDX expression,
// otherwise split on whitespace to handle space-separated lists.
func extractDeclaredLicenses(ctx context.Context, raw string, dbLocation file.Location) []pkg.License {
values := declaredLicenseValues(raw)
if len(values) == 0 {
return nil
}
return pkg.NewLicensesFromLocationWithContext(ctx, dbLocation, values...)
}
// declaredLicenseValues converts an inline License field (opkg status DB or an archive control file) into
// license values. Bitbake operators are rewritten to SPDX ones and the value is kept whole if it then parses
// as a valid SPDX expression (mirroring the alpine cataloger), otherwise it is treated as a list of license
// names with operators and grouping dropped.
func declaredLicenseValues(raw string) []string {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
licenseStrings := []string{raw}
if _, err := license.ParseExpression(raw); err != nil {
licenseStrings = strings.Fields(raw)
if len(raw) > maxDeclaredLicenseLen {
log.WithFields("length", len(raw)).Debug("ignoring oversized declared license field")
return nil
}
return pkg.NewLicensesFromLocationWithContext(ctx, dbLocation, licenseStrings...)
expr := tightenParens.Replace(strings.Join(strings.Fields(bitbakeLicenseOperators.Replace(raw)), " "))
if _, err := license.ParseExpression(expr); err == nil {
return []string{expr}
}
var values []string
seen := strset.New()
for _, field := range strings.Fields(licenseListSeparators.Replace(expr)) {
if field == "AND" || field == "OR" || seen.Has(field) {
continue
}
seen.Add(field)
values = append(values, field)
}
return values
}
func newDebArchivePackage(ctx context.Context, location file.Location, metadata pkg.DpkgArchiveEntry, licenseStrings []string) pkg.Package {
p := pkg.Package{
Name: metadata.Package,
Version: metadata.Version,
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, licenseStrings...)...),
Type: pkg.DebPkg,
PURL: packageURL(
pkg.DpkgDBEntry(metadata),
// we don't know the distro information, but since this is a deb file then we can reasonably assume it is a debian-based distro
&linux.Release{IDLike: []string{"debian"}},
),
Name: metadata.Package,
Version: metadata.Version,
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, licenseStrings...)...),
Type: pkg.DebPkg,
PURL: packageURL(pkg.DpkgDBEntry(metadata), archiveDistro(location)),
Metadata: metadata,
Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
}
@@ -95,6 +130,17 @@ func newDebArchivePackage(ctx context.Context, location file.Location, metadata
return p
}
// archiveDistro guesses the distro an archive was built for. We don't know the distro information, but a .deb
// can reasonably be assumed to target a debian-based distro. An .ipk targets opkg-based distros (Yocto,
// OpenWrt) which have no purl type, so it gets no distro and therefore no PURL, the same as packages found in
// an opkg status DB.
func archiveDistro(location file.Location) *linux.Release {
if strings.EqualFold(path.Ext(location.Path()), ".ipk") {
return nil
}
return &linux.Release{IDLike: []string{"debian"}}
}
// PackageURL returns the PURL for the specific Debian package (see https://github.com/package-url/purl-spec)
func packageURL(m pkg.DpkgDBEntry, distro *linux.Release) string {
if distro == nil {
+31
View File
@@ -3,6 +3,7 @@ package debian
import (
"context"
"sort"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
@@ -145,6 +146,36 @@ func Test_extractDeclaredLicenses(t *testing.T) {
raw: "GPL-2.0 BSD-3-Clause",
expected: []string{"GPL-2.0", "BSD-3-Clause"},
},
{
name: "bitbake operators become an SPDX expression",
raw: "MIT & (BSD-3-Clause | Apache-2.0) & ( GPL-2.0-or-later )",
expected: []string{"MIT AND (BSD-3-Clause OR Apache-2.0) AND (GPL-2.0-or-later)"},
},
{
name: "bitbake grouping is preserved rather than split",
raw: "(MIT & BSD-3-Clause) | Apache-2.0",
expected: []string{"(MIT AND BSD-3-Clause) OR Apache-2.0"},
},
{
name: "parenthesized alternatives are preserved",
raw: "(MIT) | (Apache-2.0)",
expected: []string{"(MIT) OR (Apache-2.0)"},
},
{
name: "non-SPDX names drop operators and grouping",
raw: "GPLv2+ & (LGPLv2.1 | BSD) & GPLv2+",
expected: []string{"GPLv2+", "LGPLv2.1", "BSD"},
},
{
name: "oversized field is ignored",
raw: strings.Repeat("MIT & ", maxDeclaredLicenseLen),
expected: nil,
},
{
name: "deeply nested parens are ignored",
raw: strings.Repeat("(", maxDeclaredLicenseLen) + "MIT" + strings.Repeat(")", maxDeclaredLicenseLen),
expected: nil,
},
}
for _, test := range tests {
+46 -15
View File
@@ -2,6 +2,7 @@ package debian
import (
"archive/tar"
"bytes"
"context"
"errors"
"fmt"
@@ -23,18 +24,27 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
// parseDebArchive parses a Debian package archive (.deb) file and returns the packages it contains.
// A .deb file is an ar archive containing three main files:
// - debian-binary: Version of the .deb format (usually "2.0")
// parseDebArchive parses a Debian package archive (.deb) or opkg package (.ipk) file and returns the packages it
// contains. Both are ar archives containing three main files:
// - debian-binary: Version of the archive format (usually "2.0")
// - control.tar.gz/xz/zst: Contains package metadata (control file, md5sums, conffiles)
// - data.tar.gz/xz/zst: Contains the actual files to be installed (not processed by this cataloger)
//
// This function extracts and processes the control information to create package metadata.
// This function extracts and processes the control information to create package metadata. Licenses come
// from copyright files in the data tar, falling back to the control file's License field (common for .ipk).
// Only ar-framed .ipk files are supported; the older gzipped-tar framing (e.g. OpenWrt's ipkg-build) is
// rejected as an invalid archive.
func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
arReader := ar.NewReader(reader)
validatedReader, err := newValidatedArReader(reader)
if err != nil {
return nil, nil, fmt.Errorf("invalid ar archive: %w", err)
}
arReader := ar.NewReader(validatedReader)
var metadata *pkg.DpkgArchiveEntry
var licenses []string
var ctrlLicenses []string
var unknownErr error
var sawControl, sawData bool
for {
@@ -56,7 +66,7 @@ func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen
if err != nil {
return nil, nil, unknown.New(reader.Location, fmt.Errorf("failed to decompress control.tar.* file: %w", err))
}
metadata, err = processControlTar(dcReader)
metadata, ctrlLicenses, err = processControlTar(dcReader)
switch {
case err != nil && metadata == nil:
return nil, nil, unknown.New(reader.Location, fmt.Errorf("failed to process control.tar.* file: %w", err))
@@ -87,6 +97,10 @@ func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen
return nil, nil, unknown.New(reader.Location, fmt.Errorf("no application found described in .dpkg archive"))
}
if len(licenses) == 0 && len(ctrlLicenses) > 0 {
licenses = ctrlLicenses
}
// a partial parse still yields a usable package, so report what went wrong alongside it rather than
// dropping either one
return []pkg.Package{
@@ -128,10 +142,10 @@ func processDataTar(dcReader io.ReadCloser) ([]string, error) {
}
// processControlTar always returns whatever metadata it managed to parse, even alongside a non-nil
// error. A non-nil error with non-nil metadata means the package is usable but incomplete (e.g. a
// clipped file listing); nil metadata means nothing usable was found and the caller should treat it
// as fatal.
func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
// error, along with any licenses declared in the control file's License field. A non-nil error with
// non-nil metadata means the package is usable but incomplete (e.g. a clipped file listing); nil
// metadata means nothing usable was found and the caller should treat it as fatal.
func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, []string, error) {
defer internal.CloseAndLogError(dcReader, "")
tarReader := tar.NewReader(dcReader)
@@ -140,6 +154,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
var files []pkg.DpkgFileRecord
var confFileRecords []pkg.DpkgFileRecord
var listingErr error
var licenses []string
for {
header, err := tarReader.Next()
@@ -147,7 +162,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
break
}
if err != nil {
return metadata, fmt.Errorf("failed to read control tar: %w", err)
return metadata, nil, fmt.Errorf("failed to read control tar: %w", err)
}
switch filepath.Base(header.Name) {
@@ -155,12 +170,13 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
// parseDpkgStatus already streams via bufio.Reader
entries, err := parseDpkgStatus(tarReader)
if err != nil {
return nil, fmt.Errorf("failed to parse control file: %w", err)
return nil, nil, fmt.Errorf("failed to parse control file: %w", err)
}
if len(entries) == 0 {
return nil, fmt.Errorf("no package entries found in control file")
return nil, nil, fmt.Errorf("no package entries found in control file")
}
entry := pkg.DpkgArchiveEntry(entries[0].toDpkgEntry())
licenses = declaredLicenseValues(entries[0].License)
metadata = &entry
case "md5sums":
// parseDpkgMD5Info streams via bufio.Scanner and reports its own clipping/scan errors
@@ -176,7 +192,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
}
if metadata == nil {
return nil, fmt.Errorf("control file not found in archive")
return nil, nil, fmt.Errorf("control file not found in archive")
}
if len(confFileRecords) > 0 && len(files) > 0 {
@@ -195,7 +211,22 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
// a clipped or unreadable file listing leaves the package usable, so hand both back and let the
// caller decide
return metadata, listingErr
return metadata, licenses, listingErr
}
// newValidatedArReader checks for the ar global header before handing the stream to the ar reader, which
// otherwise skips those bytes unchecked and reads garbage headers from non-ar input.
func newValidatedArReader(reader io.ReadCloser) (io.ReadCloser, error) {
prefix := make([]byte, len(ar.GLOBAL_HEADER))
if _, err := io.ReadFull(reader, prefix); err != nil {
return nil, fmt.Errorf("failed to read ar header: %w", err)
}
if !bytes.Equal(prefix, []byte(ar.GLOBAL_HEADER)) {
return nil, fmt.Errorf("expected ar header %q, got %q", string(ar.GLOBAL_HEADER), string(prefix))
}
return io.NopCloser(io.MultiReader(bytes.NewReader(prefix), reader)), nil
}
// a .deb's member sizes are bounded by the archive, but what they decompress to is not: xz and zstd
@@ -26,12 +26,13 @@ import (
func TestProcessControlTar(t *testing.T) {
tarBytes := createTestTarWithControlFiles(t)
metadata, err := processControlTar(io.NopCloser(bytes.NewReader(tarBytes)))
metadata, licenses, err := processControlTar(io.NopCloser(bytes.NewReader(tarBytes)))
require.NoError(t, err)
require.NotNil(t, metadata)
assert.Equal(t, "test-package", metadata.Package)
assert.Equal(t, []string{"MIT"}, licenses)
assert.Equal(t, "1.0.0", metadata.Version)
// md5sums should have been parsed into file records
@@ -43,6 +44,14 @@ func TestProcessControlTar(t *testing.T) {
assert.True(t, metadata.Files[0].IsConfigFile, "file listed in conffiles should be marked as config")
}
func TestParseDebArchive_RejectsInvalidArchiveHeader(t *testing.T) {
reader := file.NewLocationReadCloser(file.NewLocation("test.deb"), io.NopCloser(bytes.NewReader([]byte("not-an-ar-archive"))))
_, _, err := parseDebArchive(context.Background(), nil, nil, reader)
require.Error(t, err)
assert.Contains(t, err.Error(), "ar archive")
}
func TestProcessControlTar_ConfigFileMarking(t *testing.T) {
// Create a tar where conffiles lists paths that overlap with md5sums entries
var buf bytes.Buffer
@@ -61,9 +70,10 @@ func TestProcessControlTar_ConfigFileMarking(t *testing.T) {
require.NoError(t, tw.Close())
metadata, err := processControlTar(io.NopCloser(bytes.NewReader(buf.Bytes())))
metadata, licenses, err := processControlTar(io.NopCloser(bytes.NewReader(buf.Bytes())))
require.NoError(t, err)
require.Len(t, metadata.Files, 3)
assert.Empty(t, licenses)
assert.True(t, metadata.Files[0].IsConfigFile, "first file should be marked as config file")
assert.True(t, metadata.Files[1].IsConfigFile, "second file should be marked as config file")
@@ -75,7 +85,7 @@ func createTestTarWithControlFiles(t *testing.T) []byte {
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
controlContent := "Package: test-package\nVersion: 1.0.0\nArchitecture: all\nMaintainer: Test <test@example.com>\nDescription: Test package\n"
controlContent := "Package: test-package\nVersion: 1.0.0\nArchitecture: all\nMaintainer: Test <test@example.com>\nDescription: Test package\nLicense: MIT\n"
writeTarEntry(t, tw, "control", controlContent)
md5Content := "d41d8cd98f00b204e9800998ecf8427e usr/bin/test-command\n"
@@ -165,7 +175,7 @@ func Test_processControlTar_rejectsABombedControlMember(t *testing.T) {
writeTarEntry(t, tw, "md5sums", strings.Repeat("d41d8cd98f00b204e9800998ecf8427e usr/bin/x\n", 20000))
require.NoError(t, tw.Close())
metadata, err := processControlTar(newBoundedReadCloser(io.NopCloser(bytes.NewReader(tarBuf.Bytes())), 4096))
metadata, _, err := processControlTar(newBoundedReadCloser(io.NopCloser(bytes.NewReader(tarBuf.Bytes())), 4096))
require.ErrorIs(t, err, errDecompressedTooLarge)
require.NotNil(t, metadata, "a control tar read failure after \"control\" was parsed must not drop the package")
@@ -181,7 +191,7 @@ func Test_processControlTar_clippedListingIsUsableButReported(t *testing.T) {
writeTarEntry(t, tw, "md5sums", strings.Repeat("d41d8cd98f00b204e9800998ecf8427e usr/bin/x\n", maxDpkgFileRecords+10))
require.NoError(t, tw.Close())
metadata, err := processControlTar(io.NopCloser(bytes.NewReader(tarBuf.Bytes())))
metadata, _, err := processControlTar(io.NopCloser(bytes.NewReader(tarBuf.Bytes())))
require.Error(t, err, "clipping must be reported")
require.ErrorIs(t, err, errClippedFileListing)
@@ -418,3 +428,18 @@ func Test_decompressionStream_failsEarlyInsteadOfBuffering(t *testing.T) {
assert.Less(t, bounded, uint64(8*intFile.MB),
"a bounded stream must stop at the cap, not buffer the whole member and check afterwards")
}
func TestProcessControlTar_DeclaredLicense(t *testing.T) {
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
controlContent := "Package: test-package\nVersion: 1.0.0\nArchitecture: all\nLicense: MIT & (BSD-3-Clause | Apache-2.0) & ( GPL-2.0-or-later )\n"
writeTarEntry(t, tw, "control", controlContent)
require.NoError(t, tw.Close())
metadata, licenses, err := processControlTar(io.NopCloser(bytes.NewReader(buf.Bytes())))
require.NoError(t, err)
require.NotNil(t, metadata)
assert.Equal(t, []string{"MIT AND (BSD-3-Clause OR Apache-2.0) AND (GPL-2.0-or-later)"}, licenses)
}
+10 -2
View File
@@ -183,9 +183,11 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (*dpkgExtractedMetadata, error)
}
sourceName, sourceVersion := extractSourceVersion(raw.Source)
if sourceName != "" {
raw.Source = sourceName
}
if sourceVersion != "" {
raw.SourceVersion = sourceVersion
raw.Source = sourceName
}
if raw.Package == "" {
@@ -263,8 +265,14 @@ func extractAllFields(reader *bufio.Reader) (map[string]any, error) {
}
// If the source entry string is of the form "<name> (<version>)" then parse and return the components, if
// of the "<name>" form, then return name and nil
// of the "<name>" form, then return name and nil. Yocto (opkg/ipk) instead writes the bitbake recipe filename
// ("<name>_<version>.bb"), which is split the same way.
func extractSourceVersion(source string) (string, string) {
if recipe, ok := strings.CutSuffix(source, ".bb"); ok && !strings.ContainsAny(recipe, " (") {
// bitbake reserves "_" as the recipe name / version separator
name, version, _ := strings.Cut(recipe, "_")
return name, version
}
// special handling for the Source field since it has formatted data
match := internal.MatchNamedCaptureGroups(sourceRegexp, source)
return match["name"], match["version"]
@@ -366,6 +366,16 @@ func TestSourceVersionExtract(t *testing.T) {
input: "",
expected: []string{"", ""},
},
{
name: "bitbake recipe with version",
input: "zlib_1.3.2.bb",
expected: []string{"zlib", "1.3.2"},
},
{
name: "bitbake recipe without version",
input: "packagegroup-core-boot.bb",
expected: []string{"packagegroup-core-boot", ""},
},
}
for _, test := range tests {
@@ -0,0 +1,21 @@
# assembles a Yocto-style .ipk the same way bitbake's package_ipk does (opkg-build: a GNU ar archive of
# debian-binary, control.tar.gz and data.tar.zst) without paying for a full Yocto build. control/ is copied
# verbatim from libz1_1.3.2-r0_x86-64-v3.ipk built by poky-tiny (wrynose); the library itself is a stub
# since only the archive shape and control metadata matter here.
FROM alpine:3.18.4@sha256:48d9183eb12a05c99bcc0bf44a003607b8e941e1d4f41f9ad12bdcc4b5672f86 AS build
RUN apk add --no-cache binutils zstd
COPY control /pkg/control
RUN mkdir -p /pkg/data/usr/lib \
&& echo stub > /pkg/data/usr/lib/libz.so.1.3.2 \
&& ln -s libz.so.1.3.2 /pkg/data/usr/lib/libz.so.1 \
&& cd /pkg \
&& echo 2.0 > debian-binary \
&& tar -C control -czf control.tar.gz . \
&& tar -C data -cf - . | zstd -q -o data.tar.zst \
&& ar rcD /libz1_1.3.2-r0_x86-64-v3.ipk debian-binary control.tar.gz data.tar.zst
FROM scratch
COPY --from=build /libz1_1.3.2-r0_x86-64-v3.ipk /
@@ -0,0 +1,15 @@
Package: libz1
Version: 1.3.2-r0
Description: Zlib Compression Library
Zlib is a general-purpose, patent-free, lossless data compression library
which is used by many different programs.
Section: libs
Priority: optional
Maintainer: Poky Maintainers <poky@lists.yoctoproject.org>
License: Zlib
Architecture: x86-64-v3
OE: zlib
Homepage: http://zlib.net/
Depends: musl (>= 1.2.6+git0+9fa28ece75)
Provides: zlib
Source: zlib_1.3.2.bb
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
set -e
if [ x"$D" = "x" ]; then
if [ -x /sbin/ldconfig ]; then /sbin/ldconfig ; fi
fi
+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)
@@ -2439,12 +2439,18 @@
"@backstage/plugin-catalog-backend": [
"cpe:2.3:a:linuxfoundation:backstage\\/plugin-catalog-backend-module-unprocessed:*:*:*:*:*:node.js:*:*"
],
"@backstage/plugin-catalog-unprocessed-entities": [
"cpe:2.3:a:linuxfoundation:backstage\\/plugin-catalog-unprocessed-entities:*:*:*:*:*:node.js:*:*"
],
"@backstage/plugin-catalog-unprocessed-entities-common": [
"cpe:2.3:a:linuxfoundation:backstage\\/plugin-catalog-unprocessed-entities-common:*:*:*:*:*:node.js:*:*"
],
"@backstage/plugin-scaffolder-backend": [
"cpe:2.3:a:linuxfoundation:backstage\\/plugin-scaffolder-backend:*:*:*:*:*:*:*:*"
],
"@backstage/plugin-scaffolder-backend-module-sentry": [
"cpe:2.3:a:linuxfoundation:backstage\\/plugin-scaffolder-backend-module-sentry:*:*:*:*:*:node.js:*:*"
],
"@backstage/plugin-techdocs": [
"cpe:2.3:a:linuxfoundation:\\@backstage\\/plugin-techdocs:*:*:*:*:*:node.js:*:*"
],
@@ -3834,11 +3840,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 +6278,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 +6991,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 +8815,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 +8970,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 +9121,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 +9550,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 +9832,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 +10109,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 +10369,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 +10393,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 +10583,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 +11243,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 +11416,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 +11623,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 +11702,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 +11936,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 +12085,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 +12457,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 +13190,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 +13224,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 +13489,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 +13628,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 +13640,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 +14028,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 +14772,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 +15051,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 +15281,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 +15619,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 +15683,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 +16133,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 +16387,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 +16405,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 +16427,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 +16468,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 +16514,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 +16566,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 +17731,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 +18170,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 +18188,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 +18712,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 +19097,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 +19579,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 +20392,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 +20553,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 +20691,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 +20821,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 +20926,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 +21099,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 +21167,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 +21275,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 +21285,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 +21389,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 +21704,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 +21766,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 +21781,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 +21806,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 +21818,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 +22214,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 +22351,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 +22447,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 +22649,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 +22664,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 +23098,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 +23146,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 +23771,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:*:*"
],
@@ -12,18 +12,25 @@ import (
)
const (
// maxBundleSearchSize bounds the bytes findSignatureOffset will hold at once while looking for the
// bundle marker.
// maxBundleSearchSize bounds how far into a file findSignatureOffset looks for the bundle marker.
//
// The marker sits inside the executable structure, so the search legitimately covers a whole
// single-file bundle, which routinely runs past 100MB and can reach a few hundred for an app that
// embeds sizable assets. Clamping to the file length alone is not enough: a mostly empty file costs
// almost nothing inside a compressed layer, so a small artifact can still authorize a multi-gigabyte
// allocation. The trade-off is that a bundle larger than this loses its deps.json rather than being
// cataloged, which is the correct direction to fail when the alternative is OOM-killing the scan.
// This mirrors maxDeclaredSectionSize in syft/internal/elfutil.
// almost nothing inside a compressed layer, so a small artifact can still authorize an unbounded
// amount of reading. The trade-off is that a bundle larger than this loses its deps.json rather than
// being cataloged, which is the correct direction to fail. This mirrors maxDeclaredSectionSize in
// syft/internal/elfutil. What the search holds in memory is bounded separately, by
// markerSearchWindowSize, so raising this costs time rather than resident bytes.
maxBundleSearchSize = 512 * intFile.MB
// markerSearchWindowSize is how much of a file findSignatureOffset holds at once.
markerSearchWindowSize = 256 * intFile.KB
// bundleHeaderOffsetSize is the width of the little-endian header offset stored immediately before
// the marker.
bundleHeaderOffsetSize = 8
// maxDepsJSONSize bounds an embedded deps.json. These are dependency manifests, so real ones are
// measured in KB even for large applications.
maxDepsJSONSize = 3 * intFile.MB
@@ -37,13 +44,26 @@ const (
)
// dotNetBundleSignature is the SHA-256 hash of ".net core bundle" used to identify single-file bundles.
var dotNetBundleSignature = []byte{
var dotNetBundleSignature = [32]byte{
0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae,
}
// markerSearchOverlap is how much of each search window carries over into the next: the bytes a marker
// could straddle the boundary with, plus the offset that precedes it. Retaining both is what lets a
// windowed search give the same answer as reading the file whole - a marker split across two reads is
// still matched, and the offset in front of it is always in the same window as the match.
const markerSearchOverlap = bundleHeaderOffsetSize + len(dotNetBundleSignature) - 1
// the slide at the end of findSignatureOffset carries markerSearchOverlap bytes into the next window and
// advances by the rest, so a window no wider than the overlap would take a negative slice index and never
// move. The window is five orders of magnitude clear of that today; this is what refuses to compile if
// anyone shrinks it to the marker's own scale, which is the tempting thing to do to make a test cheaper.
// The -1 is what makes equality fail too: the window has to exceed the overlap, not merely match it.
const _ = uint(markerSearchWindowSize - markerSearchOverlap - 1)
// ExtractDepsJSON returns the deps.json embedded in the .NET single-file bundle in r, or "" if r carries no
// bundle marker.
//
@@ -94,29 +114,17 @@ func findBundleHeaderOffset(r unionreader.UnionReader, searchLimit int64) (int64
clamped = true
}
// this scans a whole executable, routinely over 100MB for a single-file bundle, so the buffer is sized
// exactly once. An append-growing read holds both arrays at its final growth and would cost well over
// twice the file's own size for the same result.
searchData := make([]byte, limit)
// a short read is not fatal here: the marker may well be in what we did get, so search the bytes we
// actually hold. ReadAt reports a short read as io.EOF, and may report a full one that way too, so the
// count is what says how much there is to search.
n, err := r.ReadAt(searchData, 0)
if err != nil && !errors.Is(err, io.EOF) {
headerOffset, found, err := findSignatureOffset(r, limit)
if err != nil {
return 0, err
}
idx := bytes.Index(searchData[:n], dotNetBundleSignature)
if idx == -1 || idx < 8 {
if !found {
if clamped {
return 0, fmt.Errorf("no bundle marker in the first %d bytes and the rest of the %d byte file was not searched", maxBundleSearchSize, size)
}
return 0, nil
}
headerOffset := int64(binary.LittleEndian.Uint64(searchData[idx-8 : idx]))
if headerOffset == 0 {
// the marker is compiled into every apphost; only publishing as a single file fills in the offset
return 0, nil
@@ -131,6 +139,52 @@ func findBundleHeaderOffset(r unionreader.UnionReader, searchLimit int64) (int64
return headerOffset, nil
}
// findSignatureOffset searches the first limit bytes of r for the bundle marker and returns the header
// offset stored in the 8 bytes immediately before it, or false if the marker is not there.
func findSignatureOffset(r unionreader.UnionReader, limit int64) (int64, bool, error) {
// nothing shorter than the marker plus the offset in front of it can hold a usable match
if limit < int64(bundleHeaderOffsetSize+len(dotNetBundleSignature)) {
return 0, false, nil
}
buf := make([]byte, min(int64(markerSearchWindowSize), limit))
var (
windowStart int64 // offset in the file that buf[0] holds
filled int // bytes of buf that hold file content
)
for {
// a short read is not fatal: the marker may well be in what we did get. ReadAt reports a short
// read as io.EOF, and may report a full one that way too, so the count is what says how much
// there is to search.
want := min(int64(len(buf)-filled), limit-windowStart-int64(filled))
n, err := r.ReadAt(buf[filled:int64(filled)+want], windowStart+int64(filled))
filled += n
if err != nil && !errors.Is(err, io.EOF) {
return 0, false, err
}
if idx := bytes.Index(buf[:filled], dotNetBundleSignature[:]); idx >= 0 {
// only reachable in the first window: after a slide the overlap guarantees that a match not
// already seen begins at least bundleHeaderOffsetSize into the buffer
if idx < bundleHeaderOffsetSize {
return 0, false, nil
}
return int64(binary.LittleEndian.Uint64(buf[idx-bundleHeaderOffsetSize : idx])), true, nil
}
// a buffer the read could not fill means the reader ran out before the limit did
if filled < len(buf) || windowStart+int64(filled) >= limit {
return 0, false, nil
}
copy(buf, buf[filled-markerSearchOverlap:filled])
windowStart += int64(filled - markerSearchOverlap)
filled = markerSearchOverlap
}
}
// dotNetBundleHeader represents the fixed portion of the bundle header (version 1+)
type dotNetBundleHeader struct {
MajorVersion uint32
@@ -16,7 +16,7 @@ import (
func fileWithSignatureAt(size, sigStart int, headerOffset uint64) []byte {
data := make([]byte, size)
binary.LittleEndian.PutUint64(data[sigStart-8:sigStart], headerOffset)
copy(data[sigStart:], dotNetBundleSignature)
copy(data[sigStart:], dotNetBundleSignature[:])
return data
}
@@ -74,7 +74,7 @@ func TestFindBundleHeaderOffset(t *testing.T) {
{
// there is no room for the 8-byte header offset before the signature
name: "signature too close to the start to carry an offset",
data: append(append([]byte{0, 0}, dotNetBundleSignature...), make([]byte, 32)...),
data: append(append([]byte{0, 0}, dotNetBundleSignature[:]...), make([]byte, 32)...),
searchLimit: 128,
},
{
@@ -295,3 +295,136 @@ func TestFindDepsJSONInManifest_PlausibleEntryCountIsWalked(t *testing.T) {
require.NoError(t, err)
assert.Empty(t, got, "no deps.json entry in this manifest")
}
// A windowed search only gives the same answer as reading the file whole if a marker lying across a
// window boundary - or one whose header offset lies across it - is still matched. These are the offsets
// where an overlap that is absent, too small, or applied to the wrong end of the buffer goes wrong.
func TestFindBundleHeaderOffset_MarkerAcrossWindowBoundary(t *testing.T) {
const window = markerSearchWindowSize
const headerOffset = 0x1234
size := window + 4096
for _, tt := range []struct {
name string
sigStart int
}{
{"whole marker inside the first window", window - 4096},
{"header offset split across the boundary", window - 4},
{"header offset ends exactly at the boundary", window},
{"signature split one byte into the second window", window - len(dotNetBundleSignature) + 1},
{"signature split down the middle", window - len(dotNetBundleSignature)/2},
{"signature starts one byte before the boundary", window - 1},
{"whole marker inside the second window", window + 2048},
} {
t.Run(tt.name, func(t *testing.T) {
data := fileWithSignatureAt(size, tt.sigStart, headerOffset)
got, err := findBundleHeaderOffset(readSeekCloser{bytes.NewReader(data)}, int64(size))
require.NoError(t, err)
assert.Equal(t, int64(headerOffset), got)
})
}
}
// recordingReader records the largest single read it is asked for. The search buffer is what bounds
// that length, so this is what fails if the implementation ever sizes a buffer against the file again.
type recordingReader struct {
*bytes.Reader
largestRead int
}
func (r *recordingReader) ReadAt(p []byte, off int64) (int, error) {
if len(p) > r.largestRead {
r.largestRead = len(p)
}
return r.Reader.ReadAt(p, off)
}
func (*recordingReader) Close() error { return nil }
// Holding an executable at its full size to look for 32 bytes is what made this the largest single
// consumer of memory in a scan of a Windows volume, so the bound matters more than the result here.
func TestFindBundleHeaderOffset_ReadsAreBoundedByTheWindow(t *testing.T) {
size := 5 * markerSearchWindowSize
data := fileWithSignatureAt(size, size-1024, 0x1234)
r := &recordingReader{Reader: bytes.NewReader(data)}
got, err := findBundleHeaderOffset(r, int64(size))
require.NoError(t, err)
require.Equal(t, int64(0x1234), got, "the marker must still be found at the far end of the file")
assert.LessOrEqual(t, r.largestRead, markerSearchWindowSize,
"no single read may exceed the search window, whatever the file's size")
}
// Every window after the first begins with bytes already searched. Re-reporting a match from that
// overlap would be harmless, but failing to advance past it would spin forever.
func TestFindBundleHeaderOffset_UnmarkedFileTerminates(t *testing.T) {
for _, size := range []int{
markerSearchWindowSize - 1,
markerSearchWindowSize,
markerSearchWindowSize + 1,
markerSearchWindowSize + markerSearchOverlap,
3 * markerSearchWindowSize,
} {
got, err := findBundleHeaderOffset(readSeekCloser{bytes.NewReader(make([]byte, size))}, int64(size))
require.NoError(t, err)
assert.Zero(t, got)
}
}
// lateShortReader reads cleanly until shortFrom and only then hands back fewer bytes than were asked for.
// shortReader above comes up short on its very first read, which the search answers before it has slid at
// all; the reader running out partway through a windowed search is a different branch.
type lateShortReader struct {
*bytes.Reader
shortFrom int64 // a read starting here or beyond stops after shortLen bytes
shortLen int
}
func (r *lateShortReader) ReadAt(p []byte, off int64) (int, error) {
if off < r.shortFrom || len(p) <= r.shortLen {
return r.Reader.ReadAt(p, off)
}
n, _ := r.Reader.ReadAt(p[:r.shortLen], off)
return n, io.EOF
}
func (*lateShortReader) Close() error { return nil }
// A squashfs block that decompresses short makes unionreader return fewer bytes than were asked for, and
// nothing says that lands in the first window. Whatever the search did get still has to be searched, and the
// bytes the reader never produced have to end it rather than being read as a file with no bundle in it.
func TestFindBundleHeaderOffset_ShortReadAfterASlideSearchesWhatWasRead(t *testing.T) {
const window = markerSearchWindowSize
const shortLen = 4096 // what the second window gets before the reader gives up
const headerOffset = 0x1234
size := 2*window + 8192
for _, tt := range []struct {
name string
sigStart int
want int64
}{
{"marker inside the bytes the second window did get", window + 1024, headerOffset},
{"marker straddling the boundary into those bytes", window - 4, headerOffset},
{"marker beyond where the reader gave up", window + shortLen + 1024, 0},
} {
t.Run(tt.name, func(t *testing.T) {
// honest about its length: only the read past the first window comes up short
r := &lateShortReader{
Reader: bytes.NewReader(fileWithSignatureAt(size, tt.sigStart, headerOffset)),
shortFrom: window,
shortLen: shortLen,
}
got, err := findBundleHeaderOffset(r, int64(size))
require.NoError(t, err, "a reader that runs out is not a parse failure")
assert.Equal(t, tt.want, got)
})
}
}
@@ -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 {
@@ -8,6 +8,7 @@ import (
"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"
@@ -523,6 +524,7 @@ func TestCpanCatalogers_metadataTypes(t *testing.T) {
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)
+7 -8
View File
@@ -5,7 +5,6 @@ import (
"context"
"encoding/json"
"path"
"path/filepath"
"strings"
"github.com/scylladb/go-set/strset"
@@ -53,7 +52,7 @@ func fetchInstalledFiles(resolver file.Resolver, metadataLocation file.Location,
// to reconcile the installed-files.txt path to the same layer (or the next adjacent lower layer).
// find the installed-files.txt file relative to the directory where the METADATA file resides (in path AND layer structure)
installedFilesPath := filepath.Join(filepath.Dir(metadataLocation.RealPath), "installed-files.txt")
installedFilesPath := path.Join(path.Dir(metadataLocation.RealPath), "installed-files.txt")
installedFilesRef := resolver.RelativeFileByPath(metadataLocation, installedFilesPath)
if installedFilesRef != nil {
@@ -83,7 +82,7 @@ func fetchRecordFiles(resolver file.Resolver, metadataLocation file.Location) (f
// to reconcile the RECORD path to the same layer (or the next adjacent lower layer).
// find the RECORD file relative to the directory where the METADATA file resides (in path AND layer structure)
recordPath := filepath.Join(filepath.Dir(metadataLocation.RealPath), "RECORD")
recordPath := path.Join(path.Dir(metadataLocation.RealPath), "RECORD")
recordRef := resolver.RelativeFileByPath(metadataLocation, recordPath)
if recordRef != nil {
@@ -107,8 +106,8 @@ func fetchRecordFiles(resolver file.Resolver, metadataLocation file.Location) (f
// fetchTopLevelPackages finds a corresponding top_level.txt file for the given python package metadata file and returns the set of package names contained.
func fetchTopLevelPackages(resolver file.Resolver, metadataLocation file.Location) (pkgs []string, sources []file.Location, err error) {
// a top_level.txt file specifies the python top-level packages (provided by this python package) installed into site-packages
parentDir := filepath.Dir(metadataLocation.RealPath)
topLevelPath := filepath.Join(parentDir, "top_level.txt")
parentDir := path.Dir(metadataLocation.RealPath)
topLevelPath := path.Join(parentDir, "top_level.txt")
topLevelLocation := resolver.RelativeFileByPath(metadataLocation, topLevelPath)
if topLevelLocation == nil {
@@ -157,8 +156,8 @@ type vcsInfo struct {
}
func fetchDirectURLData(resolver file.Resolver, metadataLocation file.Location) (d *pkg.PythonDirectURLOriginInfo, sources []file.Location, err error) {
parentDir := filepath.Dir(metadataLocation.RealPath)
directURLPath := filepath.Join(parentDir, "direct_url.json")
parentDir := path.Dir(metadataLocation.RealPath)
directURLPath := path.Join(parentDir, "direct_url.json")
directURLLocation := resolver.RelativeFileByPath(metadataLocation, directURLPath)
if directURLLocation == nil {
@@ -269,7 +268,7 @@ func findLicenses(ctx context.Context, resolver file.Resolver, m parsedData) pkg
continue
}
if licenses.IsLicenseFile(filepath.Base(f.Path)) {
if licenses.IsLicenseFile(path.Base(f.Path)) {
candidatePaths.Add(path.Join(m.SitePackagesRootPath, f.Path))
}
}
@@ -2,7 +2,7 @@ package python
import (
"fmt"
"path/filepath"
"path"
"strings"
"github.com/bmatcuk/doublestar/v4"
@@ -48,13 +48,13 @@ func parseWheelOrEggMetadata(locationReader file.LocationReadCloser) (parsedData
}
// add additional metadata not stored in the egg/wheel metadata file
path := locationReader.Path()
metadataPath := locationReader.Path()
pd.SitePackagesRootPath = determineSitePackagesRootPath(path)
pd.SitePackagesRootPath = determineSitePackagesRootPath(metadataPath)
if pd.Licenses != "" || pd.LicenseExpression != "" {
pd.LicenseFilePath = path
pd.LicenseFilePath = metadataPath
} else if pd.LicenseFile != "" {
pd.LicenseFilePath = filepath.Join(filepath.Dir(path), pd.LicenseFile)
pd.LicenseFilePath = path.Join(path.Dir(metadataPath), pd.LicenseFile)
}
pd.DistInfoLocation = locationReader.Location
@@ -144,18 +144,18 @@ func getFieldType(key, in string) any {
// isEggRegularFile determines if the specified path is the regular file variant
// of egg metadata (as opposed to a directory that contains more metadata
// files).
func isEggRegularFile(path string) bool {
return doublestar.MatchUnvalidated(eggInfoGlob, path)
func isEggRegularFile(p string) bool {
return doublestar.MatchUnvalidated(eggInfoGlob, p)
}
// determineSitePackagesRootPath returns the path of the site packages root,
// given the egg metadata file or directory specified in the path.
func determineSitePackagesRootPath(path string) string {
if isEggRegularFile(path) {
return filepath.Clean(filepath.Dir(path))
func determineSitePackagesRootPath(p string) string {
if isEggRegularFile(p) {
return path.Clean(path.Dir(p))
}
return filepath.Clean(filepath.Dir(filepath.Dir(path)))
return path.Clean(path.Dir(path.Dir(p)))
}
// handleFieldBodyContinuation returns the updated value for the specified field after processing the specified line.
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"path"
"path/filepath"
"strings"
@@ -87,11 +88,13 @@ func parseInstalledFiles(reader io.Reader, location, sitePackagesRootPath string
}
if location != "" && sitePackagesRootPath != "" {
joinedPath := filepath.Join(filepath.Dir(location), line)
joinedPath := path.Join(path.Dir(location), line)
// the path package has no Rel, and filepath.Rel hands back host separators on windows
line, err = filepath.Rel(sitePackagesRootPath, joinedPath)
if err != nil {
return nil, err
}
line = filepath.ToSlash(line)
}
installedFile := pkg.PythonFileRecord{
@@ -122,8 +122,7 @@ func TestParsePackageResolved_empty(t *testing.T) {
dir := t.TempDir()
fixture = filepath.Join(dir, "Package.resolved")
_, err := os.Create(fixture)
require.NoError(t, err)
require.NoError(t, os.WriteFile(fixture, nil, 0600))
pkgtest.TestFileParser(t, fixture, parsePackageResolved, nil, nil)
}
+12 -6
View File
@@ -30,6 +30,10 @@ import (
)
func TestNewFromLocal(t *testing.T) {
// the request is resolved with filepath.Abs, which adds a volume on windows
localSnap, err := filepath.Abs("/test/local.snap")
require.NoError(t, err)
tests := []struct {
name string
cfg Config
@@ -40,13 +44,13 @@ func TestNewFromLocal(t *testing.T) {
{
name: "local file exists",
cfg: Config{
Request: "/test/local.snap",
Request: localSnap,
DigestAlgorithms: []crypto.Hash{crypto.SHA256},
},
setup: func(fs afero.Fs) {
require.NoError(t, createMockSquashfsFile(fs, "/test/local.snap"))
require.NoError(t, createMockSquashfsFile(fs, localSnap))
},
wantRequest: "/test/local.snap",
wantRequest: localSnap,
},
{
name: "resolve home dir exists",
@@ -65,18 +69,18 @@ func TestNewFromLocal(t *testing.T) {
{
name: "local file with architecture specified",
cfg: Config{
Request: "/test/local.snap",
Request: localSnap,
Platform: &image.Platform{
Architecture: "arm64",
},
},
setup: func(fs afero.Fs) {
require.NoError(t, createMockSquashfsFile(fs, "/test/local.snap"))
require.NoError(t, createMockSquashfsFile(fs, localSnap))
},
wantErr: func(t assert.TestingT, err error, msgAndArgs ...any) bool {
return assert.Error(t, err, msgAndArgs...) && assert.Contains(t, err.Error(), "architecture cannot be specified for local snap files", msgAndArgs...)
},
wantRequest: "/test/local.snap",
wantRequest: localSnap,
},
}
for _, tt := range tests {
@@ -288,6 +292,8 @@ func writeSquashfs(t *testing.T, manifest string) string {
require.NoError(t, err)
_, err = w.Write([]byte(contents))
require.NoError(t, err)
// close before finalizing: windows directory listings can report a stale (zero) size for open files
require.NoError(t, w.Close())
}
// note: go-diskfs wants paths relative to the image root, without a leading slash
+9 -3
View File
@@ -145,7 +145,9 @@ func TestNewSnapFromFile(t *testing.T) {
DigestAlgorithms: []crypto.Hash{crypto.SHA256},
},
setup: func() string {
path := "/test/valid.snap"
// newSnapFromFile resolves the request with filepath.Abs, which adds a volume on windows
path, err := filepath.Abs("/test/valid.snap")
require.NoError(t, err)
require.NoError(t, createMockSquashfsFile(fs, path))
return path
},
@@ -179,7 +181,9 @@ func TestNewSnapFromFile(t *testing.T) {
name: "path is directory",
cfg: Config{},
setup: func() string {
path := "/test/directory"
// newSnapFromFile resolves the request with filepath.Abs, which adds a volume on windows
path, err := filepath.Abs("/test/directory")
require.NoError(t, err)
require.NoError(t, fs.MkdirAll(path, 0755))
return path
},
@@ -471,7 +475,9 @@ func TestGetSnapFileInfo(t *testing.T) {
{
name: "path is directory",
setup: func() string {
path := "/test/directory"
// newSnapFromFile resolves the request with filepath.Abs, which adds a volume on windows
path, err := filepath.Abs("/test/directory")
require.NoError(t, err)
require.NoError(t, fs.MkdirAll(path, 0755))
return path
},
@@ -10,6 +10,7 @@ import (
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/internal/testutils"
"github.com/anchore/syft/syft/internal/testutil"
"github.com/anchore/syft/syft/source"
)
@@ -78,6 +79,7 @@ func Test_StereoscopeImage_Exclusions(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
testutils.SkipWithoutLinuxContainers(t)
imageName := strings.SplitN(imagetest.PrepareFixtureImage(t, "docker-archive", test.input), ":", 2)[1]
img, err := stereoscope.GetImage(context.TODO(), imageName)
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env bash
# build an image with podman or buildah into an isolated containers-storage store, then verify syft can scan it out
# of that store (for both the vfs and overlay drivers) and that plain image references never touch the store.
#
# usage: SYFT_BINARY_LOCATION=/path/to/syft source-test.sh podman|buildah
set -euo pipefail
builder="${1:?expected podman or buildah}"
case "$builder" in
podman|buildah) ;;
*)
printf 'unsupported image builder: %s\n' "$builder" >&2
exit 2
;;
esac
syft_binary="${SYFT_BINARY_LOCATION:?expected SYFT_BINARY_LOCATION to point at a linux syft binary built with containers-storage support}"
if [[ ! -f "$syft_binary" ]]; then
printf 'syft binary not found at %s\n' "$syft_binary" >&2
exit 1
fi
chmod +x "$syft_binary"
test_dir="$(mktemp -d)"
# the overlay driver bind-mounts its graphroot onto itself (in the builder's user namespace when rootless), which must
# be unmounted before the store can be removed. Cleanup is best-effort so it never masks the test result.
cleanup_store() {
umount -l "$1"/*/graphroot/overlay 2>/dev/null || true
rm -rf "$1"
}
cleanup() {
if [[ "$(id -u)" -eq 0 ]]; then
cleanup_store "$test_dir"
else
"$builder" unshare bash -c "$(declare -f cleanup_store); cleanup_store \"\$1\"" _ "$test_dir"
fi || printf 'warning: unable to fully clean up %s\n' "$test_dir" >&2
}
trap cleanup EXIT
# a rootless store can only be opened from inside the builder's user namespace (as podman/buildah/skopeo do for
# themselves), so rootless users must run syft under "<builder> unshare". Root opens the store directly.
run_syft() {
if [[ "$(id -u)" -eq 0 ]]; then
"$syft_binary" "$@"
else
"$builder" unshare "$syft_binary" "$@"
fi
}
build_context="$test_dir/context"
mkdir -p "$build_context"
cat > "$build_context/package.json" <<'EOF'
{"name":"syft-cs-fixture","version":"1.2.3"}
EOF
cat > "$build_context/Containerfile" <<'EOF'
FROM scratch
COPY package.json /app/node_modules/syft-cs-fixture/package.json
EOF
# note: the image name deliberately avoids the string "containers-storage" so stderr greps below are unambiguous
image_ref="localhost/syft-cs-fixture:latest"
missing_ref="localhost/syft-cs-missing:latest"
for driver in vfs overlay; do
store_dir="$test_dir/$driver"
mkdir -p "$store_dir"
export CONTAINERS_STORAGE_CONF="$store_dir/storage.conf"
cat > "$CONTAINERS_STORAGE_CONF" <<EOF
[storage]
driver = "$driver"
graphroot = "$store_dir/graphroot"
rootless_storage_path = "$store_dir/graphroot"
runroot = "$store_dir/runroot"
EOF
printf 'Building %s with %s (%s driver)\n' "$image_ref" "$builder" "$driver"
case "$builder" in
podman)
podman build --pull=never --tag "$image_ref" "$build_context"
;;
buildah)
buildah build --pull=false --tag "$image_ref" "$build_context"
;;
esac
for scan_mode in from-flag scheme default-pull-source; do
printf 'Testing %s resolution with %s (%s driver)\n' "$scan_mode" "$builder" "$driver"
out="$store_dir/$scan_mode"
case "$scan_mode" in
from-flag)
run_syft -vv --from containers-storage "$image_ref" --output json > "$out.json" 2> "$out.stderr"
;;
scheme)
run_syft -vv "containers-storage:$image_ref" --output json > "$out.json" 2> "$out.stderr"
;;
default-pull-source)
SYFT_SOURCE_IMAGE_DEFAULT_PULL_SOURCE=containers-storage \
run_syft -vv "$image_ref" --output json > "$out.json" 2> "$out.stderr"
;;
esac
# prove the image came out of the store, not from some other provider that happened to answer
if ! grep -q 'copied image from containers-storage' "$out.stderr"; then
printf 'Expected %s scan to resolve via containers-storage\n' "$scan_mode" >&2
cat "$out.stderr" >&2
exit 1
fi
jq -e --arg image_ref "$image_ref" '
.source.type == "image" and
.source.metadata.userInput == $image_ref and
any(.artifacts[]; .name == "syft-cs-fixture" and .version == "1.2.3" and .type == "npm")
' "$out.json" >/dev/null
done
printf 'Testing plain reference skips containers-storage with %s (%s driver)\n' "$builder" "$driver"
# the image only exists in the local store, so automatic resolution (daemons, then registry) must fail without
# ever trying containers-storage
if run_syft "$image_ref" --output json > "$store_dir/plain.json" 2> "$store_dir/plain.stderr"; then
printf 'Expected plain reference scan to fail since the image only exists in containers-storage\n' >&2
exit 1
fi
if grep -q 'containers-storage' "$store_dir/plain.stderr"; then
printf 'Plain reference unexpectedly attempted containers-storage resolution\n' >&2
cat "$store_dir/plain.stderr" >&2
exit 1
fi
printf 'Testing missing image with %s (%s driver)\n' "$builder" "$driver"
if run_syft --from containers-storage "$missing_ref" --output json > "$store_dir/missing.json" 2> "$store_dir/missing.stderr"; then
printf 'Expected the missing containers-storage image scan to fail\n' >&2
exit 1
fi
if ! grep -q 'does not resolve to an image ID' "$store_dir/missing.stderr"; then
printf 'Expected a containers-storage "image not found" error\n' >&2
cat "$store_dir/missing.stderr" >&2
exit 1
fi
done
@@ -43,3 +43,9 @@ run_test_case test_download_release_asset "${release}" "linux" "amd64" "deb" "ap
run_test_case test_download_release_asset "${release}" "linux" "arm64" "tar.gz" "application/gzip"
run_test_case test_download_release_asset "${release}" "linux" "arm64" "rpm" "application/x-rpm"
run_test_case test_download_release_asset "${release}" "linux" "arm64" "deb" "application/vnd.debian.binary-package"
# releases before VERIFY_SIGN_BUNDLE_VERSION are signed with a separate .sig and .pem instead of a sigstore bundle,
# so keep exercising that path against a fixed release
legacy_release="v1.52.0"
run_test_case test_download_release_asset "${legacy_release}" "linux" "amd64" "tar.gz" "application/gzip"

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