Compare commits

...
Author SHA1 Message Date
Alex Goodman 18f9b5ab58 remove benchmark utils (#3982)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-06-09 18:24:49 +00:00
Keith Zantow 9090c69708 fix: exclude packages with SPDX GENERATED_FROM source package indication (#3981)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
2025-06-09 14:12:23 -04:00
dependabot[bot] 1396a14550 chore(deps): bump modernc.org/sqlite from 1.37.1 to 1.38.0 (#3979)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.37.1 to 1.38.0.
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.37.1...v1.38.0)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.38.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>
2025-06-09 10:31:10 -04:00
dependabot[bot] 592bc0af7d chore(deps): bump github.com/go-git/go-git/v5 from 5.16.1 to 5.16.2 (#3978)
Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.16.1 to 5.16.2.
- [Release notes](https://github.com/go-git/go-git/releases)
- [Commits](https://github.com/go-git/go-git/compare/v5.16.1...v5.16.2)

---
updated-dependencies:
- dependency-name: github.com/go-git/go-git/v5
  dependency-version: 5.16.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>
2025-06-09 10:26:47 -04:00
anchore-actions-token-generator[bot]andspiffcs b6b8a8f52e chore(deps): update tools to latest versions (#3977)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: spiffcs <32073428+spiffcs@users.noreply.github.com>
2025-06-09 08:56:36 -04:00
anchore-actions-token-generator[bot]andwagoodman a196cc9215 chore(deps): update CPE dictionary index (#3976)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: wagoodman <590471+wagoodman@users.noreply.github.com>
2025-06-09 08:56:18 -04:00
dependabot[bot] 12c8003317 chore(deps): bump golang.org/x/net from 0.40.0 to 0.41.0 (#3970)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.40.0 to 0.41.0.
- [Commits](https://github.com/golang/net/compare/v0.40.0...v0.41.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.41.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>
2025-06-06 10:23:02 -04:00
dependabot[bot] 0a25c0ec5c chore(deps): bump github.com/sergi/go-diff (#3971)
Bumps [github.com/sergi/go-diff](https://github.com/sergi/go-diff) from 1.3.2-0.20230802210424-5b0b94c5c0d3 to 1.4.0.
- [Commits](https://github.com/sergi/go-diff/commits/v1.4.0)

---
updated-dependencies:
- dependency-name: github.com/sergi/go-diff
  dependency-version: 1.4.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>
2025-06-06 10:22:54 -04:00
Christoph Blessing 5ae11bd1f7 Fix Python package dependency detection (#3965)
Previously a dependency relationship between two Python packages was not
detected if there were no parentheses around the version specifier in
the wheel metadata of the parent package. This commit allows detection
of such relationships.

Signed-off-by: Christoph Blessing <chris24.blessing@gmail.com>
2025-06-06 09:46:16 -04:00
John Vandenberg bc1cbde4f7 fix: Remove three Rust crate false positive CPE matches (#3967)
Signed-off-by: John Vandenberg <jayvdb@gmail.com>
2025-06-06 04:29:06 -04:00
Michael BrileyandAlex Goodman 868a6a7584 Harden Container Runtime with Non-Root User (#3941)
* Update Dockerfile

Signed-off-by: Michael Briley <michael.briley937@gmail.com>

* Update Dockerfile

Signed-off-by: Michael Briley <michael.briley937@gmail.com>

* Update validations.yaml

Signed-off-by: Michael Briley <michael.briley937@gmail.com>

* Update validations.yaml

Signed-off-by: Michael Briley <michael.briley937@gmail.com>

* Update Dockerfile

Signed-off-by: Michael Briley <michael.briley937@gmail.com>

* Update validations.yaml

Signed-off-by: Michael Briley <michael.briley937@gmail.com>

* Update Dockerfile

Signed-off-by: Michael Briley <michael.briley937@gmail.com>

* Update .goreleaser.yaml

Signed-off-by: Michael Briley <michael.briley937@gmail.com>

* Update .goreleaser.yaml

Signed-off-by: Michael Briley <michael.briley937@gmail.com>

* use distroless/static-debian12:nonroot directly

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

* keep manual manifest curation

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

* remove qemu usage

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

* add smoke test for snapshot

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

* split up manifests section with comments

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

* correct ci step name

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

* fix arch condition

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

* keep path prefix

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

---------

Signed-off-by: Michael Briley <michael.briley937@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-06-05 12:36:23 -04:00
John Vandenberg bd894b9c4d fix: Remove two Rust crate false positive CPE matches (#3962)
Rust crates opentelemetry and redis are being given CPEs that
match CVEs such as CVE-2023-45142 and CVE-2022-24735 respectively.
The vendor overrides added here prevent that.

Signed-off-by: John Vandenberg <jayvdb@gmail.com>
2025-06-05 10:28:54 -04:00
dependabot[bot] c36c69779a chore(deps): bump golang.org/x/mod from 0.24.0 to 0.25.0 (#3963)
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.24.0 to 0.25.0.
- [Commits](https://github.com/golang/mod/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/mod
  dependency-version: 0.25.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>
2025-06-05 13:46:58 +00:00
dependabot[bot] cd23ccc6e6 chore(deps): bump github.com/gkampitakis/go-snaps from 0.5.12 to 0.5.13 (#3964)
Bumps [github.com/gkampitakis/go-snaps](https://github.com/gkampitakis/go-snaps) from 0.5.12 to 0.5.13.
- [Release notes](https://github.com/gkampitakis/go-snaps/releases)
- [Commits](https://github.com/gkampitakis/go-snaps/compare/v0.5.12...v0.5.13)

---
updated-dependencies:
- dependency-name: github.com/gkampitakis/go-snaps
  dependency-version: 0.5.13
  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>
2025-06-05 13:45:40 +00:00
Keith Zantow 71d84603c1 fix: bump stereoscope to fix symlink performance issue (#3953)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
2025-06-04 15:50:03 +00:00
dependabot[bot] f2118b568d chore(deps): bump github.com/go-git/go-git/v5 from 5.16.0 to 5.16.1 (#3960)
Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.16.0 to 5.16.1.
- [Release notes](https://github.com/go-git/go-git/releases)
- [Commits](https://github.com/go-git/go-git/compare/v5.16.0...v5.16.1)

---
updated-dependencies:
- dependency-name: github.com/go-git/go-git/v5
  dependency-version: 5.16.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>
2025-06-04 14:02:45 +00:00
dependabot[bot] bb50f1650d chore(deps): bump github/codeql-action from 3.28.18 to 3.28.19 (#3952)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.18 to 3.28.19.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/ff0a06e83cb2de871e5a09832bc6a81e7276941f...fca7ace96b7d713c7035871441bd52efbe39e27e)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.28.19
  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>
2025-06-03 10:20:24 -04:00
Christopher Angelo Phillips a0be514184 feat: add syft schema version to version command (#3949)
---------
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2025-06-02 15:50:21 +00:00
dependabot[bot] 8cc808f8f6 chore(deps): bump github.com/gkampitakis/go-snaps from 0.5.11 to 0.5.12 (#3943)
Bumps [github.com/gkampitakis/go-snaps](https://github.com/gkampitakis/go-snaps) from 0.5.11 to 0.5.12.
- [Release notes](https://github.com/gkampitakis/go-snaps/releases)
- [Commits](https://github.com/gkampitakis/go-snaps/compare/v0.5.11...v0.5.12)

---
updated-dependencies:
- dependency-name: github.com/gkampitakis/go-snaps
  dependency-version: 0.5.12
  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>
2025-06-02 10:46:30 -04:00
anchore-actions-token-generator[bot]andspiffcs 967e2f85bf chore(deps): update tools to latest versions (#3945)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: spiffcs <32073428+spiffcs@users.noreply.github.com>
2025-06-02 10:46:20 -04:00
anchore-actions-token-generator[bot]andwagoodman 339fea9851 chore(deps): update CPE dictionary index (#3947)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: wagoodman <590471+wagoodman@users.noreply.github.com>
2025-06-02 10:43:31 -04:00
Keith Zantow 576e729c84 fix: revert incorrect graalvm unknown behavior (#3944)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
2025-05-30 11:36:57 -04:00
dependabot[bot] b3e8926025 chore(deps): bump github.com/google/go-containerregistry (#3933)
Bumps [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) from 0.20.4 to 0.20.5.
- [Release notes](https://github.com/google/go-containerregistry/releases)
- [Changelog](https://github.com/google/go-containerregistry/blob/main/.goreleaser.yml)
- [Commits](https://github.com/google/go-containerregistry/compare/v0.20.4...v0.20.5)

---
updated-dependencies:
- dependency-name: github.com/google/go-containerregistry
  dependency-version: 0.20.5
  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>
2025-05-28 12:45:51 -04:00
anchore-actions-token-generator[bot]andwagoodman 002ec4510a chore(deps): update CPE dictionary index (#3935)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: wagoodman <590471+wagoodman@users.noreply.github.com>
2025-05-28 12:45:42 -04:00
Thomas Gosteli 684e1e963d fix(terraform): parse provider lock entries without constraints (#3934)
In a .terraform.lock.hcl file in a provider block the `constraints` attribute is actually not required (=optional).

Signed-off-by: Thomas Gosteli <thomas.gosteli@protonmail.ch>
2025-05-27 14:55:19 -04:00
Dan LuhringandKeith Zantow bbf3bb5856 fix(relationship): favor real paths over symlinks for ownership by file (#3923)
Signed-off-by: Dan Luhring <dluhring@chainguard.dev>
Signed-off-by: Keith Zantow <kzantow@gmail.com>
Co-authored-by: Keith Zantow <kzantow@gmail.com>
2025-05-23 14:33:19 -04:00
dependabot[bot] 31c1be6d4d chore(deps): bump modernc.org/sqlite from 1.37.0 to 1.37.1 (#3926)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.37.0 to 1.37.1.
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.37.0...v1.37.1)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.37.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>
2025-05-22 10:47:26 -04:00
Keith Zantow 7bfb4c86a6 fix(dotnet-deps-cataloger): avoid repeated dependency resolution (#3930)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
2025-05-21 21:28:40 -04:00
anchore-actions-token-generator[bot]andspiffcs 18ed8b60f8 chore(deps): update tools to latest versions (#3921)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: spiffcs <32073428+spiffcs@users.noreply.github.com>
2025-05-21 10:56:17 -04:00
dependabot[bot] b5e9f75ef1 chore(deps): bump github.com/google/go-containerregistry (#3925)
Bumps [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) from 0.20.3 to 0.20.4.
- [Release notes](https://github.com/google/go-containerregistry/releases)
- [Changelog](https://github.com/google/go-containerregistry/blob/main/.goreleaser.yml)
- [Commits](https://github.com/google/go-containerregistry/compare/v0.20.3...v0.20.4)

---
updated-dependencies:
- dependency-name: github.com/google/go-containerregistry
  dependency-version: 0.20.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>
2025-05-21 10:55:48 -04:00
Alex Goodman ac883f52ed add cdx group as purl namespace (#3922)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-20 15:56:08 -04:00
Alex Goodman e23ca43a83 add PE binary cataloger (#3911)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-19 14:17:09 -04:00
Christopher Angelo Phillips b4ca04001c chore: update dockerfile base images to latest rolling tags (#3915)
---------
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2025-05-19 13:43:14 +00:00
anchore-actions-token-generator[bot]andwagoodman 828645ec27 chore(deps): update CPE dictionary index (#3913)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: wagoodman <590471+wagoodman@users.noreply.github.com>
2025-05-19 09:16:25 -04:00
Alex Goodman db77b54c01 finalize go mod ref (#3908)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-16 17:36:26 +00:00
Alex Goodman 2d4fe513ec remove benchmark workflow (#3906)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-16 11:08:43 -04:00
Christopher Angelo Phillips e1374f758e fix: update license content filtering default case to be 'none' for no content returned
---------
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2025-05-16 14:25:15 +00:00
dependabot[bot] 945893847f chore(deps): bump github/codeql-action from 3.28.17 to 3.28.18 (#3905)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.17 to 3.28.18.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/60168efe1c415ce0f5521ea06d5c2062adbeed1b...ff0a06e83cb2de871e5a09832bc6a81e7276941f)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.28.18
  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>
2025-05-16 14:16:11 +00:00
sathiya06andAlex Goodman 8cbdd38a63 fix: Make Native Image contains no embedded SBOM Error Discoverable (#3805)
* fix: Make Native Image contains no embedded SBOM Error Discoverable

Signed-off-by: Sathiya Narayanan Venkatesan <sathiyavenkat06@gmail.com>

* adjust error phrasing + wrap error

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

---------

Signed-off-by: Sathiya Narayanan Venkatesan <sathiyavenkat06@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-16 13:54:40 +00:00
Adam McClenaghanandAlex Goodman 8f02bd85f6 fix: Distinguish openjdk vs jdk when using file source (#3895)
* fix: Distinguish openjdk vs jdk when using file source

Signed-off-by: adammcclenaghan <adam@mcclenaghan.co.uk>

* fix: Fix goimport order

Signed-off-by: adammcclenaghan <adam@mcclenaghan.co.uk>

* add comment

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

---------

Signed-off-by: adammcclenaghan <adam@mcclenaghan.co.uk>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-16 13:29:53 +00:00
Alex Goodman 0480b516f6 chore: fix publishing test fixture images (#3896)
* update pkg counts

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

* bump max cache size

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

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-15 14:35:11 -04:00
Christopher Angelo Phillips 2a055690e6 chore: delete unused fixture (#3901)
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2025-05-15 17:30:36 +00:00
Christopher Angelo Phillips 4f73d35051 Include default config licenses (#3900)
* fix: the licenses config was not being carried through causing content to show by default
---------
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2025-05-15 16:48:18 +00:00
Ralph BeanandAlex Goodman b369b02f4f Expose RPM signature information (for RPM DB and RPM archives) (#3179)
* feat: expose rpm signature information

This helps with more confident identification of an rpm.

In theory, two rpms can be built that have the same purl string, and
otherwise look identical in syft's output, but the PGP information
would distinguish them as signed either by different keys, or signed at
different times.

In practice, this usually makes no difference since rpms tend to have
unique name/version/release strings. This just gives increased
confidence about the identity of the rpm found in the db.

Signed-off-by: Ralph Bean <rbean@redhat.com>

* chore: generate json schema

Signed-off-by: Ralph Bean <rbean@redhat.com>

* re-generate json schema

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

* rename to a more generic signature field

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

* rename rpm.pgp to rpm.signatures

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

* split out signature fields

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

* bump json schema

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

* include RPM archives

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

* update json schema

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

* dont fail on unknown signature type

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

---------

Signed-off-by: Ralph Bean <rbean@redhat.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-15 16:01:00 +00:00
dependabot[bot] 5effed06a8 chore(deps): bump github.com/mholt/archives from 0.1.1 to 0.1.2 (#3898)
Bumps [github.com/mholt/archives](https://github.com/mholt/archives) from 0.1.1 to 0.1.2.
- [Release notes](https://github.com/mholt/archives/releases)
- [Commits](https://github.com/mholt/archives/compare/v0.1.1...v0.1.2)

---
updated-dependencies:
- dependency-name: github.com/mholt/archives
  dependency-version: 0.1.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>
2025-05-15 10:23:30 -04:00
dependabot[bot] 5e25d52845 chore(deps): bump anchore/sbom-action from 0.19.0 to 0.20.0 (#3899)
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.19.0 to 0.20.0.
- [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/9f7302141466aa6482940f15371237e9d9f4c34a...e11c554f704a0b820cbf8c51673f6945e0731532)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-version: 0.20.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>
2025-05-15 10:23:20 -04:00
Laurent GoderreandAlex Goodman a8e5b25632 Add PHP interpreter + extensions cataloger (#2585)
* Add PHP extensions binary classifiers

Signed-off-by: Laurent Goderre <laurent.goderre@docker.com>

* [wip] add php extensions cataloger

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

* fix linting

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

* find interpreters + extension

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

* internalize binary cataloger utilities

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

* default to linux/amd64 for test fixtures

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

---------

Signed-off-by: Laurent Goderre <laurent.goderre@docker.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-15 08:22:50 -04:00
Alex Goodman 0521ccaf5e chore: update fixtures based on CI builds (#3894)
* update fixtures based on ci builds

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

* fix typo

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

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-14 21:30:20 +00:00
Christopher Angelo Phillips 3c7018a853 feat: remove full-text before release (#3889)
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2025-05-14 13:12:05 +00:00
Christopher Angelo Phillips e5d7760bb8 feat: improve dpkg cataloger license recognition for "license agreements" (#3888) 2025-05-14 08:41:48 -04:00
Laurent GoderreandAlex Goodman 175a6719a9 Add cataloger for Dart pubspec (#3292)
* Add cataloger for Dart pubspec

Signed-off-by: Laurent Goderre <laurent.goderre@docker.com>

* capture pubspec specific fields

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

---------

Signed-off-by: Laurent Goderre <laurent.goderre@docker.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-13 21:51:49 +00:00
Christopher Angelo PhillipsandAlex Goodman f77d503892 detect license ID from full text when incidentally provided as a value (#3876)
---------
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>
2025-05-13 16:37:18 -04:00
Keith Zantow b4d717fb30 chore: update mimetype contact info (#3887)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
2025-05-13 17:47:05 +00:00
Rez MossandAlex Goodman 12d91f47dc Add a homebrew cataloger (#3724)
* Cataloger homebrew (#4)

* homebrew cataloger

* uptd

* fixed test

* fixed test

* fixed tests

* fixed lint

* inc schema ver

* upt schema

* fixed integration test

* fixed integration tst

* fixed test

Signed-off-by: Rez Moss <hi@rezmoss.com>

* Update parse_homebrew_test.go

Signed-off-by: Rez Moss <hi@rezmoss.com>

* Update parse_homebrew_test.go

fixed DCO

Signed-off-by: Rez Moss <hi@rezmoss.com>



Signed-off-by: Rez Moss <hi@rezmoss.com>

* Update parse_homebrew_test.go

add evd anno to test

Signed-off-by: Rez Moss <hi@rezmoss.com>

* lint

Signed-off-by: Rez Moss <hi@rezmoss.com>

* fixed test

Signed-off-by: Rez Moss <hi@rezmoss.com>

* with PR refactors

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

* regenerate json schema

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

* fix tests

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

* regenerate jsonschema

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

* refactor homebrew parser + add tests

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

* more resiliant variable extraction

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

---------

Signed-off-by: Rez Moss <hi@rezmoss.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-13 13:01:41 -04:00
Weston Steimel de88b973f8 chore: fix some logging output (#3884)
Signed-off-by: Weston Steimel <author@code.w.steimel.me.uk>
2025-05-13 13:15:19 +01:00
Alex Goodman 59b880f26a order locations by container layer order (#3858)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-13 00:02:07 -04:00
Alex Goodman e3e69596bd Translate Portage license strings to SPDX expressions (#1763)
* fix portage license handling

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

* cover license_group file

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

* add licenses to portage metadata in json schema

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

---------

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-12 21:03:51 -04:00
Will Murphy 58392a9717 fix: stop emitting redis redis CPE for PHP PECL redis (#3881)
This prevents the cpe:a:redis:redis... from being emitted for the PHP
Pear / PECL package called redis.

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
2025-05-12 16:17:18 -04:00
Keith Zantow 621d21eb04 feat: Add PURL list input/output format (#3853)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
2025-05-12 13:33:24 -04:00
anchore-actions-token-generator[bot]andwagoodman bea57a4f7d chore(deps): update CPE dictionary index (#3877)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: wagoodman <590471+wagoodman@users.noreply.github.com>
2025-05-12 09:56:30 -04:00
anchore-actions-token-generator[bot]andspiffcs ff575b3f4d chore(deps): update tools to latest versions (#3878)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: spiffcs <32073428+spiffcs@users.noreply.github.com>
2025-05-12 09:56:07 -04:00
Alex Goodman f9d0fa81ab do not search binary contents for version for go package (#3874)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-09 17:49:17 +00:00
Alex Goodman abe5e27b4b fix: remove race when writing errors in generic cataloger (#3875)
* fix generic cataloger race

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

* update race test

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

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-09 17:46:47 +00:00
Alex Goodman a7816dc9e7 clear devel version for go packages (#3873)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-09 13:36:52 -04:00
anchore-actions-token-generator[bot]andspiffcs 0b78186a97 chore(deps): update tools to latest versions (#3871)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: spiffcs <32073428+spiffcs@users.noreply.github.com>
2025-05-09 08:29:20 +00:00
dependabot[bot] 99ef5accc7 chore(deps): bump actions/setup-go from 5.4.0 to 5.5.0 (#3867)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.4.0 to 5.5.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/0aaccfd150d50ccaeb58ebd88d36e91967a5f35b...d35c59abb061a4a6fb18e82ac0862c26744d6ab5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 5.5.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>
2025-05-08 14:27:08 -04:00
dependabot[bot] d634f1438b chore(deps): bump actions/setup-go in /.github/actions/bootstrap (#3868)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.4.0 to 5.5.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/0aaccfd150d50ccaeb58ebd88d36e91967a5f35b...d35c59abb061a4a6fb18e82ac0862c26744d6ab5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 5.5.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>
2025-05-08 14:26:57 -04:00
Alex Goodman 1574fb20ae merge multiple targets for the same dotnet package (#3869)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-08 15:28:08 +00:00
James NeateandAlex Goodman 00c4a4e72a Use package ID from decoded SBOMs when provided (#1872)
* fix: use package id from cyclonedx when provided

Signed-off-by: James Neate <jamesmneate@gmail.com>

* override package IDs from converted SBOMs

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

* fix typo

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

* remove extractSyftID function

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

---------

Signed-off-by: James Neate <jamesmneate@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-08 11:25:30 -04:00
bgoareguer 39396cfff9 feat: upgrade base docker image (#3862)
Signed-off-by: Goareguer Bertrand <bertrand.goareguer@gmail.com>
2025-05-07 14:29:48 +00:00
305 changed files with 29785 additions and 3270 deletions
+5 -18
View File
@@ -34,7 +34,7 @@ tools:
# used for showing the changelog at release
- name: glow
version:
want: v2.1.0
want: v2.1.1
method: github-release
with:
repo: charmbracelet/glow
@@ -58,7 +58,7 @@ tools:
# used to release all artifacts
- name: goreleaser
version:
want: v2.9.0
want: v2.10.2
method: github-release
with:
repo: goreleaser/goreleaser
@@ -87,23 +87,10 @@ tools:
with:
repo: wagoodman/go-bouncer
# used for showing benchmark testing
- name: benchstat
version:
want: latest
method: go-proxy
with:
module: golang.org/x/perf
allow-unresolved-version: true
method: go-install
with:
entrypoint: cmd/benchstat
module: golang.org/x/perf
# used for running all local and CI tasks
- name: task
version:
want: v3.43.3
want: v3.44.0
method: github-release
with:
repo: go-task/task
@@ -111,7 +98,7 @@ tools:
# used for triggering a release
- name: gh
version:
want: v2.72.0
want: v2.74.0
method: github-release
with:
repo: cli/cli
@@ -127,7 +114,7 @@ tools:
# used to upload test fixture cache
- name: yq
version:
want: v4.45.2
want: v4.45.4
method: github-release
with:
repo: mikefarah/yq
+1 -1
View File
@@ -29,7 +29,7 @@ runs:
using: "composite"
steps:
# note: go mod and build is automatically cached on default with v4+
- uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
if: inputs.go-version != ''
with:
go-version: ${{ inputs.go-version }}
-62
View File
@@ -1,62 +0,0 @@
name: "Benchmark testing"
on:
workflow_dispatch:
pull_request:
permissions:
contents: read
jobs:
Benchmark-Test:
name: "Benchmark tests"
runs-on: ubuntu-24.04
# note: we want benchmarks to run on pull_request events in order to publish results to a sticky comment, and
# we also want to run on push such that merges to main are recorded to the cache. For this reason we don't filter
# the job by event.
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- name: Restore base benchmark result
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 #v4.2.3
with:
path: test/results/benchmark-main.txt
# use base sha for PR or new commit hash for main push in benchmark result key
key: ${{ runner.os }}-bench-${{ (github.event.pull_request.base.sha != github.event.after) && github.event.pull_request.base.sha || github.event.after }}
- name: Run benchmark tests
id: benchmark
run: |
REF_NAME=${GITHUB_REF##*/} make benchmark
OUTPUT=$(make show-benchstat)
OUTPUT="${OUTPUT//'%'/'%25'}" # URL encode all '%' characters
OUTPUT="${OUTPUT//$'\n'/'%0A'}" # URL encode all '\n' characters
OUTPUT="${OUTPUT//$'\r'/'%0D'}" # URL encode all '\r' characters
echo "result=$OUTPUT" >> $GITHUB_OUTPUT
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: benchmark-test-results
path: test/results/**/*
- name: Update PR benchmark results comment
uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db #v2.9.2
continue-on-error: true
with:
header: benchmark
message: |
### Benchmark Test Results
<details>
<summary>Benchmark results from the latest changes vs base branch</summary>
```
${{ steps.benchmark.outputs.result }}
```
</details>
+4 -4
View File
@@ -39,13 +39,13 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
- name: Install Go
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b #v5.4.0
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 #v5.5.0
with:
go-version-file: go.mod
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b #v3.28.17
uses: github/codeql-action/init@fca7ace96b7d713c7035871441bd52efbe39e27e #v3.28.19
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -56,7 +56,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@60168efe1c415ce0f5521ea06d5c2062adbeed1b #v3.28.17
uses: github/codeql-action/autobuild@fca7ace96b7d713c7035871441bd52efbe39e27e #v3.28.19
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -70,4 +70,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b #v3.28.17
uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441bd52efbe39e27e #v3.28.19
+1 -1
View File
@@ -148,7 +148,7 @@ jobs:
# for updating brew formula in anchore/homebrew-syft
GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}
- uses: anchore/sbom-action@9f7302141466aa6482940f15371237e9d9f4c34a #v0.19.0
- uses: anchore/sbom-action@e11c554f704a0b820cbf8c51673f6945e0731532 #v0.20.0
continue-on-error: true
with:
file: go.mod
+3
View File
@@ -76,6 +76,9 @@ jobs:
- name: Build snapshot artifacts
run: make snapshot
- name: Smoke test snapshot build
run: make snapshot-smoke-test
# why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach).
# see https://github.com/actions/upload-artifact/issues/199 for more info
- name: Upload snapshot artifacts
+99 -112
View File
@@ -1,11 +1,11 @@
version: 2
project_name: syft
release:
prerelease: auto
draft: false
env:
# required to support multi architecture docker builds
- DOCKER_CLI_EXPERIMENTAL=enabled
- CGO_ENABLED=0
@@ -13,14 +13,8 @@ builds:
- id: linux-build
dir: ./cmd/syft
binary: syft
goos:
- linux
goarch:
- amd64
- arm64
- ppc64le
- s390x
# set the modified timestamp on the output binary to the git timestamp to ensure a reproducible build
goos: [linux]
goarch: [amd64, arm64, ppc64le, s390x]
mod_timestamp: &build-timestamp '{{ .CommitTimestamp }}'
ldflags: &build-ldflags |
-w
@@ -34,11 +28,8 @@ builds:
- id: darwin-build
dir: ./cmd/syft
binary: syft
goos:
- darwin
goarch:
- amd64
- arm64
goos: [darwin]
goarch: [amd64, arm64]
mod_timestamp: *build-timestamp
ldflags: *build-ldflags
hooks:
@@ -50,115 +41,42 @@ builds:
- id: windows-build
dir: ./cmd/syft
binary: syft
goos:
- windows
goarch:
- amd64
goos: [windows]
goarch: [amd64]
mod_timestamp: *build-timestamp
ldflags: *build-ldflags
archives:
- id: linux-archives
builds:
- linux-build
# note: the signing process is depending on tar.gz archives. If this format changes then .github/scripts/apple-signing/*.sh will need to be adjusted
builds: [linux-build]
- id: darwin-archives
builds:
- darwin-build
builds: [darwin-build]
- id: windows-archives
format: zip
builds:
- windows-build
builds: [windows-build]
nfpms:
- license: "Apache 2.0"
maintainer: "Anchore, Inc"
homepage: &website "https://github.com/anchore/syft"
description: &description "A tool that generates a Software Bill Of Materials (SBOM) from container images and filesystems"
formats:
- rpm
- deb
formats: [rpm, deb]
brews:
- repository:
owner: anchore
name: homebrew-syft
token: "{{.Env.GITHUB_BREW_TOKEN}}"
ids:
- darwin-archives
- linux-archives
ids: [darwin-archives, linux-archives]
homepage: *website
description: *description
license: "Apache License 2.0"
dockers:
# production images...
- image_templates:
- anchore/syft:debug
- anchore/syft:{{.Tag}}-debug
- ghcr.io/anchore/syft:debug
- ghcr.io/anchore/syft:{{.Tag}}-debug
goarch: amd64
dockerfile: Dockerfile.debug
use: buildx
build_flag_templates:
- "--platform=linux/amd64"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:debug-arm64v8
- anchore/syft:{{.Tag}}-debug-arm64v8
- ghcr.io/anchore/syft:debug-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
goarch: arm64
dockerfile: Dockerfile.debug
use: buildx
build_flag_templates:
- "--platform=linux/arm64/v8"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:debug-ppc64le
- anchore/syft:{{.Tag}}-debug-ppc64le
- ghcr.io/anchore/syft:debug-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
goarch: ppc64le
dockerfile: Dockerfile.debug
use: buildx
build_flag_templates:
- "--platform=linux/ppc64le"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:debug-s390x
- anchore/syft:{{.Tag}}-debug-s390x
- ghcr.io/anchore/syft:debug-s390x
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
goarch: s390x
dockerfile: Dockerfile.debug
use: buildx
build_flag_templates:
- "--platform=linux/s390x"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:latest
- anchore/syft:{{.Tag}}
- ghcr.io/anchore/syft:latest
- ghcr.io/anchore/syft:{{.Tag}}
- anchore/syft:{{.Tag}}-amd64
- ghcr.io/anchore/syft:{{.Tag}}-amd64
goarch: amd64
dockerfile: Dockerfile
use: buildx
@@ -208,51 +126,120 @@ dockers:
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
# debug images...
- image_templates:
- anchore/syft:{{.Tag}}-debug-amd64
- ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
goarch: amd64
dockerfile: Dockerfile.debug
use: buildx
build_flag_templates:
- "--platform=linux/amd64"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:{{.Tag}}-debug-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
goarch: arm64
dockerfile: Dockerfile.debug
use: buildx
build_flag_templates:
- "--platform=linux/arm64/v8"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:{{.Tag}}-debug-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
goarch: ppc64le
dockerfile: Dockerfile.debug
use: buildx
build_flag_templates:
- "--platform=linux/ppc64le"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
- image_templates:
- anchore/syft:{{.Tag}}-debug-s390x
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
goarch: s390x
dockerfile: Dockerfile.debug
use: buildx
build_flag_templates:
- "--platform=linux/s390x"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
docker_manifests:
# anchore/syft manifests...
- name_template: anchore/syft:latest
image_templates:
- anchore/syft:{{.Tag}}
- anchore/syft:{{.Tag}}-amd64
- anchore/syft:{{.Tag}}-arm64v8
- anchore/syft:{{.Tag}}-ppc64le
- anchore/syft:{{.Tag}}-s390x
- name_template: anchore/syft:{{.Tag}}
image_templates:
- anchore/syft:{{.Tag}}-amd64
- anchore/syft:{{.Tag}}-arm64v8
- anchore/syft:{{.Tag}}-ppc64le
- anchore/syft:{{.Tag}}-s390x
- name_template: anchore/syft:debug
- anchore/syft:{{.Tag}}-debug
image_templates:
- anchore/syft:{{.Tag}}-debug-amd64
- anchore/syft:{{.Tag}}-debug-arm64v8
- anchore/syft:{{.Tag}}-debug-ppc64le
- anchore/syft:{{.Tag}}-debug-s390x
- name_template: anchore/syft:{{.Tag}}
- name_template: anchore/syft:{{.Tag}}-debug
image_templates:
- anchore/syft:{{.Tag}}
- anchore/syft:{{.Tag}}-arm64v8
- anchore/syft:{{.Tag}}-ppc64le
- anchore/syft:{{.Tag}}-s390x
- anchore/syft:{{.Tag}}-debug-amd64
- anchore/syft:{{.Tag}}-debug-arm64v8
- anchore/syft:{{.Tag}}-debug-ppc64le
- anchore/syft:{{.Tag}}-debug-s390x
# ghcr.io/anchore/syft manifests...
- name_template: ghcr.io/anchore/syft:latest
image_templates:
- ghcr.io/anchore/syft:{{.Tag}}
- ghcr.io/anchore/syft:{{.Tag}}-amd64
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-s390x
- name_template: ghcr.io/anchore/syft:{{.Tag}}
image_templates:
- ghcr.io/anchore/syft:{{.Tag}}-amd64
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-s390x
- name_template: ghcr.io/anchore/syft:debug
image_templates:
- ghcr.io/anchore/syft:{{.Tag}}-debug
- ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
- name_template: ghcr.io/anchore/syft:{{.Tag}}
- name_template: ghcr.io/anchore/syft:{{.Tag}}-debug
image_templates:
- ghcr.io/anchore/syft:{{.Tag}}
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-s390x
- ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
sboms:
- artifacts: archive
# this is relative to the snapshot/dist directory, not the root of the repo
cmd: ../.tool/syft
documents:
- "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.sbom"
+3 -5
View File
@@ -1,14 +1,12 @@
FROM gcr.io/distroless/static-debian11:debug@sha256:a0a404776dec98be120089ae42bbdfbe48c177921d856937d124d48eb8c0b951 AS build
FROM scratch
# needed for version check HTTPS request
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
FROM gcr.io/distroless/static-debian12:nonroot
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
COPY syft /
USER nonroot
ARG BUILD_DATE
ARG BUILD_VERSION
ARG VCS_REF
+3 -1
View File
@@ -1,10 +1,12 @@
FROM gcr.io/distroless/static-debian11:debug@sha256:a0a404776dec98be120089ae42bbdfbe48c177921d856937d124d48eb8c0b951
FROM gcr.io/distroless/static-debian12:debug-nonroot
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
COPY syft /
USER nonroot
ARG BUILD_DATE
ARG BUILD_VERSION
ARG VCS_REF
+20 -30
View File
@@ -27,10 +27,12 @@ vars:
sh: uname -s | tr '[:upper:]' '[:lower:]'
ARCH:
sh: |
[ "$(uname -m)" = "x86_64" ] && echo "amd64_v1" || { [ "$(uname -m)" = "aarch64" ] && echo "arm64_v8.0" || echo $(uname -m); }
[ "$(uname -m)" = "x86_64" ] && echo "amd64_v1" || { [ "$(uname -m)" = "aarch64" ] && echo "arm64_v8.0" || [ "$(uname -m)" = "arm64" ] && echo "arm64_v8.0" || echo $(uname -m); }
PROJECT_ROOT:
sh: echo $PWD
# note: the snapshot dir must be a relative path starting with ./
# e.g. when installing snapshot debs from a local path, ./ forces the deb to be installed in the current working directory instead of referencing a package name
SNAPSHOT_DIR: ./snapshot
SNAPSHOT_BIN: "{{ .PROJECT_ROOT }}/{{ .SNAPSHOT_DIR }}/{{ .OS }}-build_{{ .OS }}_{{ .ARCH }}/{{ .PROJECT }}"
SNAPSHOT_CMD: "{{ .TOOL_DIR }}/goreleaser release --config {{ .TMP_DIR }}/goreleaser.yaml --clean --snapshot --skip=publish --skip=sign"
@@ -75,7 +77,6 @@ tasks:
- task: unit
- task: integration
- task: validate-cyclonedx-schema
- task: benchmark
- task: test-utils
- task: cli
- task: check-docker-cache
@@ -221,7 +222,8 @@ tasks:
cmds:
- "go test -v ./cmd/syft/internal/test/integration"
# exercise most of the CLI with the data race detector
- "go run -race cmd/syft/main.go alpine:latest"
# we use a larger image to ensure we're using multiple catalogers at a time
- "go run -race cmd/syft/main.go anchore/test_images:grype-quality-dotnet-69f15d2"
validate-cyclonedx-schema:
desc: Validate that Syft produces valid CycloneDX documents
@@ -230,10 +232,7 @@ tasks:
cli:
desc: Run CLI tests
# note: we don't want to regenerate the snapshot unless we have to. In CI it's probable
# that the cache being restored with the correct binary will be rebuilt since the timestamps
# and local checksums will not line up.
deps: [tools, snapshot]
deps: [tools]
cmds:
- cmd: "echo 'testing binary: {{ .SNAPSHOT_BIN }}'"
silent: true
@@ -250,27 +249,6 @@ tasks:
- cmd: .github/scripts/labeler_test.py
## Benchmark test targets #################################
benchmark:
deps: [tmpdir]
generates:
- "{{ .TMP_DIR }}/benchmark-main.txt"
cmds:
- "go test -p 1 -run=^Benchmark -bench=. -count=7 -benchmem ./... | tee {{ .TMP_DIR }}/benchmark-{{ .VERSION }}.txt"
- |
bash -c "(test -s {{ .TMP_DIR }}/benchmark-main.txt && \
{{ .TOOL_DIR }}/benchstat {{ .TMP_DIR }}/benchmark-main.txt {{ .TMP_DIR }}/benchmark-{{ .VERSION }}.txt || \
{{ .TOOL_DIR }}/benchstat {{ .TMP_DIR }}/benchmark-{{ .VERSION }}.txt) \
| tee {{ .TMP_DIR }}/benchstat.txt"
show-benchstat:
deps: [benchmark, tmpdir]
cmds:
- cmd: "cat {{ .TMP_DIR }}/benchstat.txt"
silent: true
## Test-fixture-related targets #################################
fingerprints:
@@ -452,8 +430,8 @@ tasks:
find . | grep cache | grep tar | xargs du
echo "total $total_size KB"
if [ "$total_size" -gt 1048576 ]; then
echo 'docker cache is larger than 1GB'
if [ "$total_size" -gt 5242880 ]; then
echo 'docker cache is larger than 5GB'
exit 1
fi
@@ -573,6 +551,18 @@ tasks:
- "{{ .SNAPSHOT_CMD }}"
snapshot-smoke-test:
desc: Run a smoke test on the snapshot builds + docker images
cmds:
- cmd: "echo 'testing snapshot binary: {{ .SNAPSHOT_BIN }}'"
silent: true
- cmd: "test -f {{ .SNAPSHOT_BIN }} || (find {{ .SNAPSHOT_DIR }} && echo '\nno snapshot found for {{ .SNAPSHOT_BIN }}' && false)"
silent: true
- "{{ .SNAPSHOT_BIN }} version"
- "{{ .SNAPSHOT_BIN }} scan alpine:latest"
- docker run --rm anchore/syft:latest version
- docker run --rm anchore/syft:latest scan alpine:latest
changelog:
desc: Generate a changelog
deps: [tools]
+6 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/anchore/clio"
"github.com/anchore/syft/cmd/syft/internal"
"github.com/anchore/syft/cmd/syft/internal/commands"
constants "github.com/anchore/syft/internal"
)
// Application constructs the `syft packages` command and aliases the root command to `syft packages`.
@@ -47,7 +48,7 @@ func create(id clio.Identification, out io.Writer) (clio.Application, *cobra.Com
commands.Cataloger(app),
commands.Attest(app),
commands.Convert(app),
clio.VersionCommand(id),
clio.VersionCommand(id, schemaVersion),
clio.ConfigCommand(app, nil),
cranecmd.NewCmdAuthLogin(id.Name), // syft login uses the same command as crane
)
@@ -59,3 +60,7 @@ func create(id clio.Identification, out io.Writer) (clio.Application, *cobra.Com
return app, rootCmd
}
func schemaVersion() (string, any) {
return "SchemaVersion", constants.JSONSchemaVersion
}
+2 -2
View File
@@ -158,8 +158,8 @@ func (cfg Catalog) ToFilesConfig() filecataloging.Config {
func (cfg Catalog) ToLicenseConfig() cataloging.LicenseConfig {
return cataloging.LicenseConfig{
IncludeUnkownLicenseContent: cfg.License.IncludeUnknownLicenseContent,
Coverage: cfg.License.LicenseCoverage,
IncludeContent: cfg.License.Content,
Coverage: cfg.License.Coverage,
}
}
+26 -7
View File
@@ -1,12 +1,17 @@
package options
import (
"fmt"
"github.com/anchore/clio"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/cataloging"
)
type licenseConfig struct {
IncludeUnknownLicenseContent bool `yaml:"include-unknown-license-content" json:"include-unknown-license-content" mapstructure:"include-unknown-license-content"`
LicenseCoverage float64 `yaml:"license-coverage" json:"license-coverage" mapstructure:"license-coverage"`
Content cataloging.LicenseContent `yaml:"content" json:"content" mapstructure:"content"`
Coverage float64 `yaml:"coverage" json:"coverage" mapstructure:"coverage"`
AvailableLicenseContent []cataloging.LicenseContent `yaml:"-" json:"-" mapstructure:"-"`
}
var _ interface {
@@ -14,15 +19,29 @@ var _ interface {
} = (*licenseConfig)(nil)
func (o *licenseConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&o.IncludeUnknownLicenseContent, `include the content of a license in the SBOM when syft
cannot determine a valid SPDX ID for the given license`)
descriptions.Add(&o.LicenseCoverage, `adjust the percent as a fraction of the total text, in normalized words, that
descriptions.Add(&o.Content, fmt.Sprintf("include the content of licenses in the SBOM for a given syft scan; valid values are: %s", o.AvailableLicenseContent))
descriptions.Add(&o.Coverage, `adjust the percent as a fraction of the total text, in normalized words, that
matches any valid license for the given inputs, expressed as a percentage across all of the licenses matched.`)
}
func (o *licenseConfig) PostLoad() error {
validContent := internal.NewSet(o.AvailableLicenseContent...)
if !validContent.Contains(o.Content) {
return fmt.Errorf("could not use %q as license content option; valid values are: %v", o.Content, validContent.ToSlice())
}
return nil
}
func defaultLicenseConfig() licenseConfig {
cfg := cataloging.DefaultLicenseConfig()
return licenseConfig{
IncludeUnknownLicenseContent: false,
LicenseCoverage: 75,
Content: cfg.IncludeContent,
Coverage: cfg.Coverage,
AvailableLicenseContent: []cataloging.LicenseContent{
cataloging.LicenseContentIncludeAll,
cataloging.LicenseContentIncludeUnknown,
cataloging.LicenseContentExcludeAll,
},
}
}
+2
View File
@@ -12,6 +12,7 @@ import (
"github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/format/cyclonedxxml"
"github.com/anchore/syft/syft/format/github"
"github.com/anchore/syft/syft/format/purls"
"github.com/anchore/syft/syft/format/spdxjson"
"github.com/anchore/syft/syft/format/spdxtagvalue"
"github.com/anchore/syft/syft/format/syftjson"
@@ -127,6 +128,7 @@ func supportedIDs() []sbom.FormatID {
table.ID,
text.ID,
template.ID,
purls.ID,
// encoders that support multiple versions
cyclonedxxml.ID,
@@ -501,6 +501,13 @@ var commonTestCases = []testCase{
"Akismet Anti-spam: Spam Protection": "5.3",
},
},
{
name: "find homebrew",
pkgType: pkg.HomebrewPkg,
pkgInfo: map[string]string{
"afflib": "1.2.3",
},
},
{
name: "find php pear/pecl package",
pkgType: pkg.PhpPearPkg,
@@ -19,7 +19,7 @@ func TestPackageDeduplication(t *testing.T) {
}{
{
scope: source.AllLayersScope,
packageCount: 178, // without deduplication this would be ~600
packageCount: 175, // without deduplication this would be ~600
instanceCount: map[string]int{
"basesystem": 1,
"wget": 1,
@@ -30,15 +30,15 @@ func TestPackageDeduplication(t *testing.T) {
locationCount: map[string]int{
"basesystem-11-13.el9": 5, // in all layers
"curl-minimal-7.76.1-26.el9_3.2.0.1": 2, // base + wget layer
"curl-minimal-7.76.1-29.el9_4.1": 3, // curl upgrade layer + all above layers
"curl-minimal-7.76.1-31.el9": 3, // curl upgrade layer + all above layers
"wget-1.21.1-8.el9_4": 4, // wget + all above layers
"vsftpd-3.0.5-5.el9": 2, // vsftpd + all above layers
"httpd-2.4.57-11.el9_4.1": 1, // last layer
"vsftpd-3.0.5-6.el9": 2, // vsftpd + all above layers
"httpd-2.4.62-1.el9_5.2": 1, // last layer
},
},
{
scope: source.SquashedScope,
packageCount: 172,
packageCount: 169,
instanceCount: map[string]int{
"basesystem": 1,
"wget": 1,
@@ -47,11 +47,11 @@ func TestPackageDeduplication(t *testing.T) {
"httpd": 1, // rpm, binary is now excluded by overlap
},
locationCount: map[string]int{
"basesystem-11-13.el9": 1,
"curl-minimal-7.76.1-29.el9_4.1": 1, // upgrade
"wget-1.21.1-8.el9_4": 1,
"vsftpd-3.0.5-5.el9": 1,
"httpd-2.4.57-11.el9_4.1": 1,
"basesystem-11-13.el9": 1,
"curl-minimal-7.76.1-31.el9": 1, // upgrade
"wget-1.21.1-8.el9_4": 1,
"vsftpd-3.0.5-6.el9": 1,
"httpd-2.4.62-1.el9_5.2": 1,
},
},
}
@@ -3,18 +3,18 @@ package integration
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func TestPhotonPackageRegression(t *testing.T) { // Regression: https://github.com/anchore/syft/pull/1997
sbom, _ := catalogFixtureImage(t, "image-photon-all-layers", source.AllLayersScope)
var packages []pkg.Package
for p := range sbom.Artifacts.Packages.Enumerate() {
packages = append(packages, p)
var count int
for range sbom.Artifacts.Packages.Enumerate(pkg.RpmPkg) {
count++
}
if len(packages) < 1 {
t.Errorf("failed to find packages for photon distro; wanted > 0 got 0")
}
assert.Greater(t, count, 0, "expected to find RPM packages in the SBOM (but did not)")
}
@@ -0,0 +1,2 @@
desc "Advanced Forensic Format"
homepage "https://github.com/sshock/AFFLIBv3"
+30 -29
View File
@@ -19,11 +19,12 @@ require (
github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d
github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb
github.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec
github.com/anchore/go-sync v0.0.0-20250326131806-4eda43a485b6
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115
github.com/anchore/stereoscope v0.1.4
github.com/anchore/stereoscope v0.1.5-0.20250604132324-344e29f37f05
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
@@ -39,14 +40,14 @@ require (
github.com/elliotchance/phpserialize v1.4.0
github.com/facebookincubator/nvdtools v0.1.5
github.com/github/go-spdx/v2 v2.3.3
github.com/gkampitakis/go-snaps v0.5.11
github.com/gkampitakis/go-snaps v0.5.13
github.com/go-git/go-billy/v5 v5.6.2
github.com/go-git/go-git/v5 v5.16.0
github.com/go-git/go-git/v5 v5.16.2
github.com/go-test/deep v1.1.1
github.com/go-viper/mapstructure/v2 v2.2.1
github.com/gohugoio/hashstructure v0.5.0
github.com/google/go-cmp v0.7.0
github.com/google/go-containerregistry v0.20.3
github.com/google/go-containerregistry v0.20.5
github.com/google/licensecheck v0.3.1
github.com/google/uuid v1.6.0
github.com/gookit/color v1.5.4
@@ -57,9 +58,8 @@ require (
github.com/jedib0t/go-pretty/v6 v6.6.7
github.com/jinzhu/copier v0.4.0
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
github.com/knqyf263/go-rpmdb v0.1.1
github.com/magiconair/properties v1.8.10
github.com/mholt/archives v0.1.1
github.com/mholt/archives v0.1.2
github.com/moby/sys/mountinfo v0.7.2
github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1
github.com/olekukonko/tablewriter v0.0.5
@@ -72,7 +72,7 @@ require (
github.com/sassoftware/go-rpmutils v0.4.0
// pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
github.com/sergi/go-diff v1.4.0
github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb
github.com/spdx/tools-golang v0.5.5
github.com/spf13/afero v1.14.0
@@ -85,11 +85,11 @@ require (
github.com/xeipuuv/gojsonschema v1.2.0
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1
go.uber.org/goleak v1.3.0
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
golang.org/x/mod v0.24.0
golang.org/x/net v0.40.0
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
golang.org/x/mod v0.25.0
golang.org/x/net v0.41.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.37.0
modernc.org/sqlite v1.38.0
)
require (
@@ -100,11 +100,11 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.11.7 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/ProtonMail/go-crypto v1.2.0 // indirect
github.com/STARRY-S/zip v0.2.1 // indirect
github.com/agext/levenshtein v1.2.1 // indirect; indirectt
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aquasecurity/go-version v0.0.1 // indirect
@@ -133,10 +133,10 @@ require (
github.com/containerd/typeurl/v2 v2.1.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/cli v28.1.1+incompatible // indirect
github.com/docker/cli v28.2.2+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v28.1.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/docker/go-units v0.5.0 // indirect
@@ -147,13 +147,13 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gkampitakis/ciinfo v0.3.1 // indirect
github.com/gkampitakis/ciinfo v0.3.2 // indirect
github.com/gkampitakis/go-diff v1.3.2 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-restruct/restruct v1.2.0-alpha // indirect
github.com/goccy/go-yaml v1.15.13 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/snappy v0.0.4 // indirect
@@ -226,7 +226,7 @@ require (
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/vbatts/tar-split v0.11.6 // indirect
github.com/vbatts/tar-split v0.12.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
@@ -235,31 +235,32 @@ require (
github.com/zclconf/go-cty v1.13.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
go.opentelemetry.io/otel v1.33.0 // indirect
go.opentelemetry.io/otel/metric v1.33.0 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/tools v0.31.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/tools v0.33.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
google.golang.org/grpc v1.67.3 // indirect
google.golang.org/protobuf v1.36.4 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
modernc.org/libc v1.62.1 // indirect
modernc.org/libc v1.65.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.9.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
retract (
v1.25.0 // published with a replace directive (confusing for API users)
v0.53.2
v0.53.1 // Published accidentally with incorrect license in depdencies
v0.53.1 // published accidentally with incorrect license in depdencies
)
+66 -64
View File
@@ -78,8 +78,8 @@ github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs=
github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
@@ -110,6 +110,8 @@ github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 h1:2SqmFgE7h+Ql4
github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722/go.mod h1:oFuE8YuTCM+spgMXhePGzk3asS94yO9biUfDzVTFqNw=
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU=
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk=
github.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec h1:SjjPMOXTzpuU1ZME4XeoHyek+dry3/C7I8gzaCo02eg=
github.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec/go.mod h1:eQVa6QFGzKy0qMcnW2pez0XBczvgwSjw9vA23qifEyU=
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=
github.com/anchore/go-sync v0.0.0-20250326131806-4eda43a485b6 h1:Ha+LSCVuXYSYGi7wIkJK6G8g6jI3LH7y6LbyEVyp4Io=
@@ -120,11 +122,11 @@ 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.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiEjnoGJZ1+Ah0ZZ/mKKqNhGcUZBl0s7PTTDzvY=
github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI=
github.com/anchore/stereoscope v0.1.4 h1:e+iT9UdUzLBabWGe84hn5sTHDRioY+4IHsVzJXuJlek=
github.com/anchore/stereoscope v0.1.4/go.mod h1:omWgXDEp/XfqCJlZXIByEo1c3ArZg/qTJ5LBKVLAIdw=
github.com/anchore/stereoscope v0.1.5-0.20250604132324-344e29f37f05 h1:MKTDwRrC7A+eRZvGFJ8TzLiKytFH1GBpPdjLTlcoO4A=
github.com/anchore/stereoscope v0.1.5-0.20250604132324-344e29f37f05/go.mod h1:S5xxMIo1BK+V+p+6SF/wzS4pZ2cTnpk6L+UJbf5IjsQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ=
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@@ -262,14 +264,14 @@ github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da/go.mod h1:B3tI9iGHi4i
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k=
github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A=
github.com/docker/cli v28.2.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 v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I=
github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
@@ -323,12 +325,12 @@ github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFA
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/github/go-spdx/v2 v2.3.3 h1:QI7evnHWEfWkT54eJwkoV/f3a0xD3gLlnVmT5wQG6LE=
github.com/github/go-spdx/v2 v2.3.3/go.mod h1:2ZxKsOhvBp+OYBDlsGnUMcchLeo2mrpEBn2L1C+U3IQ=
github.com/gkampitakis/ciinfo v0.3.1 h1:lzjbemlGI4Q+XimPg64ss89x8Mf3xihJqy/0Mgagapo=
github.com/gkampitakis/ciinfo v0.3.1/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
github.com/gkampitakis/go-snaps v0.5.11 h1:LFG0ggUKR+KEiiaOvFCmLgJ5NO2zf93AxxddkBn3LdQ=
github.com/gkampitakis/go-snaps v0.5.11/go.mod h1:PcKmy8q5Se7p48ywpogN5Td13reipz1Iivah4wrTIvY=
github.com/gkampitakis/go-snaps v0.5.13 h1:Hhjmvv1WboSCxkR9iU2mj5PQ8tsz/y8ECGrIbjjPF8Q=
github.com/gkampitakis/go-snaps v0.5.13/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
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/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@@ -339,8 +341,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
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.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ=
github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
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=
@@ -364,8 +366,8 @@ github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/goccy/go-yaml v1.15.13 h1:Xd87Yddmr2rC1SLLTm2MNDcTjeO/GYo0JGiww6gSTDg=
github.com/goccy/go-yaml v1.15.13/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -426,8 +428,8 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
github.com/google/go-containerregistry v0.20.5 h1:4RnlYcDs5hoA++CeFjlbZ/U9Yp1EuWr+UhhTyYQjOP0=
github.com/google/go-containerregistry v0.20.5/go.mod h1:Q14vdOOzug02bwnhMkZKD4e30pDaD9W65qzXpyzF49E=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs=
github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY=
@@ -548,8 +550,6 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/knqyf263/go-rpmdb v0.1.1 h1:oh68mTCvp1XzxdU7EfafcWzzfstUZAEa3MW0IJye584=
github.com/knqyf263/go-rpmdb v0.1.1/go.mod h1:9LQcoMCMQ9vrF7HcDtXfvqGO4+ddxFQ8+YF/0CVGDww=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
@@ -600,8 +600,8 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/archives v0.1.1 h1:c7J3qXN1FB54y0qiUXiq9Bxk4eCUc8pdXWwOhZdRzeY=
github.com/mholt/archives v0.1.1/go.mod h1:FQVz01Q2uXKB/35CXeW/QFO23xT+hSCGZHVtha78U4I=
github.com/mholt/archives v0.1.2 h1:UBSe5NfYKHI1sy+S5dJsEsG9jsKKk8NJA4HCC+xTI4A=
github.com/mholt/archives v0.1.2/go.mod h1:D7QzTHgw3ctfS6wgOO9dN+MFgdZpbksGCxprUOwZWDs=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
@@ -751,8 +751,8 @@ github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEW
github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
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.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -836,8 +836,8 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vbatts/go-mtree v0.5.4 h1:OMAb8jaCyiFA7zXj0Zc/oARcxBDBoeu2LizjB8BVJl0=
github.com/vbatts/go-mtree v0.5.4/go.mod h1:5GqJbVhm9BBiCc4K5uc/c42FPgXulHaQs4sFUEfIWMo=
github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/vifraa/gopom v1.0.0 h1:L9XlKbyvid8PAIK8nr0lihMApJQg/12OBvMA28BcWh0=
github.com/vifraa/gopom v1.0.0/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o=
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA=
@@ -885,20 +885,22 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
@@ -925,8 +927,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -937,8 +939,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -966,8 +968,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1012,8 +1014,8 @@ golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1043,8 +1045,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1137,8 +1139,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1199,8 +1201,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
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=
@@ -1390,26 +1392,26 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic=
modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU=
modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA=
modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s=
modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo=
modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc=
modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po=
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.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/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.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
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=
+36 -1
View File
@@ -1,9 +1,12 @@
package cmptest
import (
"strings"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/anchore/syft/internal/evidence"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
@@ -29,9 +32,41 @@ func BuildOptions(licenseCmp LicenseComparer, locationCmp LocationComparer) []cm
cmpopts.IgnoreFields(pkg.Package{}, "id"), // note: ID is not deterministic for test purposes
cmpopts.SortSlices(pkg.Less),
cmpopts.SortSlices(DefaultRelationshipComparer),
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](locationCmp)),
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](locationCmp, locationSorter)),
cmp.Comparer(buildSetComparer[pkg.License, pkg.LicenseSet](licenseCmp)),
cmp.Comparer(locationCmp),
cmp.Comparer(licenseCmp),
}
}
// LocationSorter always sorts by evidence annotations first, then by access path, then by real path.
// This intentionally does not consider layer details since some test fixtures have no layer information
// on the left side of the comparison (expected) and does on the right side (actual).
func locationSorter(a, b file.Location) int {
// compare by evidence annotations first...
aEvidence := a.Annotations[evidence.AnnotationKey]
bEvidence := b.Annotations[evidence.AnnotationKey]
if aEvidence != bEvidence {
if aEvidence == evidence.PrimaryAnnotation {
return -1
}
if bEvidence == evidence.PrimaryAnnotation {
return 1
}
if aEvidence > bEvidence {
return -1
}
if bEvidence > aEvidence {
return 1
}
}
// ...then by paths
if a.AccessPath != b.AccessPath {
return strings.Compare(a.AccessPath, b.AccessPath)
}
return strings.Compare(a.RealPath, b.RealPath)
}
+2 -2
View File
@@ -13,7 +13,7 @@ func DefaultLicenseComparer(x, y pkg.License) bool {
return cmp.Equal(
x, y,
cmp.Comparer(DefaultLocationComparer),
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](DefaultLocationComparer)),
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](DefaultLocationComparer, locationSorter)),
)
}
@@ -21,6 +21,6 @@ func LicenseComparerWithoutLocationLayer(x, y pkg.License) bool {
return cmp.Equal(
x, y,
cmp.Comparer(LocationComparerWithoutLayer),
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](LocationComparerWithoutLayer)),
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](LocationComparerWithoutLayer, locationSorter)),
)
}
+2 -1
View File
@@ -2,6 +2,7 @@ package cmptest
import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/anchore/syft/syft/file"
)
@@ -9,7 +10,7 @@ import (
type LocationComparer func(x, y file.Location) bool
func DefaultLocationComparer(x, y file.Location) bool {
return cmp.Equal(x.Coordinates, y.Coordinates) && cmp.Equal(x.AccessPath, y.AccessPath)
return cmp.Equal(x.Coordinates, y.Coordinates, cmpopts.IgnoreUnexported(file.Coordinates{})) && cmp.Equal(x.AccessPath, y.AccessPath)
}
func LocationComparerWithoutLayer(x, y file.Location) bool {
+4 -4
View File
@@ -1,13 +1,13 @@
package cmptest
type slicer[T any] interface {
ToSlice() []T
ToSlice(sorter ...func(a, b T) int) []T
}
func buildSetComparer[T any, S slicer[T]](l func(x, y T) bool) func(x, y S) bool {
func buildSetComparer[T any, S slicer[T]](l func(x, y T) bool, sorters ...func(a, b T) int) func(x, y S) bool {
return func(x, y S) bool {
xs := x.ToSlice()
ys := y.ToSlice()
xs := x.ToSlice(sorters...)
ys := y.ToSlice(sorters...)
if len(xs) != len(ys) {
return false
+1 -1
View File
@@ -3,5 +3,5 @@ package internal
const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "16.0.28"
JSONSchemaVersion = "16.0.34"
)
+3 -3
View File
@@ -8,7 +8,7 @@ import (
)
func TestSetContextLicenseScanner(t *testing.T) {
scanner := testScanner(true)
scanner := testScanner()
ctx := context.Background()
ctx = SetContextLicenseScanner(ctx, scanner)
@@ -20,7 +20,7 @@ func TestSetContextLicenseScanner(t *testing.T) {
}
func TestIsContextLicenseScannerSet(t *testing.T) {
scanner := testScanner(true)
scanner := testScanner()
ctx := context.Background()
require.False(t, IsContextLicenseScannerSet(ctx))
@@ -30,7 +30,7 @@ func TestIsContextLicenseScannerSet(t *testing.T) {
func TestContextLicenseScanner(t *testing.T) {
t.Run("with scanner", func(t *testing.T) {
scanner := testScanner(true)
scanner := testScanner()
ctx := SetContextLicenseScanner(context.Background(), scanner)
s, err := ContextLicenseScanner(ctx)
if err != nil || s != scanner {
+36
View File
@@ -0,0 +1,36 @@
package licenses
import (
"context"
"io"
)
func (s *scanner) FindEvidence(_ context.Context, reader io.Reader) (evidence []Evidence, content []byte, err error) {
if s.scanner == nil {
return nil, nil, nil
}
content, err = io.ReadAll(reader)
if err != nil {
return nil, nil, err
}
cov := s.scanner(content)
if cov.Percent < s.coverageThreshold {
// unknown or no licenses here
// => check return content to Search to process
return nil, content, nil
}
evidence = make([]Evidence, 0)
for _, m := range cov.Match {
evidence = append(evidence, Evidence{
ID: m.ID,
Type: m.Type,
Start: m.Start,
End: m.End,
IsURL: m.IsURL,
})
}
return evidence, content, nil
}
+81
View File
@@ -0,0 +1,81 @@
package licenses
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/google/licensecheck"
"github.com/stretchr/testify/require"
)
func TestDefaultScanner_FindEvidence(t *testing.T) {
testCases := []struct {
name string
fixture string
wantIDs []string // expected license IDs
minMatch int // minimum # of matches required
}{
{
name: "Single licenses are able to be recognized and returned Apache 2.0",
fixture: "test-fixtures/apache-license-2.0",
wantIDs: []string{"Apache-2.0"},
minMatch: 1,
},
{
name: "Multiple Licenses are returned as evidence with duplicates at different offset",
fixture: "test-fixtures/multi-license",
wantIDs: []string{
"MIT",
"MIT",
"NCSA",
"Apache-2.0",
"Zlib",
"Unlicense",
"BSD-2-Clause",
"BSD-2-Clause",
"BSD-3-Clause",
},
minMatch: 2,
},
}
scanner := testScanner()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
filePath := filepath.Clean(tc.fixture)
f, err := os.Open(filePath)
require.NoError(t, err)
defer f.Close()
evidence, content, err := scanner.FindEvidence(context.Background(), f)
require.NoError(t, err)
require.NotEmpty(t, content)
require.GreaterOrEqual(t, len(evidence), tc.minMatch, "expected at least %d matches", tc.minMatch)
var foundIDs []string
for _, ev := range evidence {
foundIDs = append(foundIDs, ev.ID)
}
require.ElementsMatch(t, tc.wantIDs, foundIDs, "expected license IDs %v, but got %v", tc.wantIDs, foundIDs)
})
}
}
func testScanner() Scanner {
return &scanner{
coverageThreshold: DefaultCoverageThreshold,
scanner: licensecheck.Scan,
}
}
func mustOpen(fixture string) []byte {
content, err := os.ReadFile(fixture)
if err != nil {
panic(err)
}
return content
}
+22 -25
View File
@@ -8,33 +8,37 @@ import (
"github.com/google/licensecheck"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
const (
DefaultCoverageThreshold = 75 // determined by experimentation
DefaultIncludeLicenseContent = false
UnknownLicensePrefix = unknownLicenseType + "_"
DefaultCoverageThreshold = 75 // determined by experimentation
unknownLicenseType = "UNKNOWN"
)
type Evidence struct {
ID string // License identifier. (See licenses/README.md.)
Type licensecheck.Type // The type of the license: BSD, MIT, etc.
Start int // Start offset of match in text; match is at text[Start:End].
End int // End offset of match in text.
IsURL bool // Whether match is a URL.
}
type Scanner interface {
IdentifyLicenseIDs(context.Context, io.Reader) ([]string, []byte, error)
FileSearch(context.Context, file.LocationReadCloser) ([]file.License, error)
PkgSearch(context.Context, file.LocationReadCloser) ([]pkg.License, error)
FindEvidence(context.Context, io.Reader) ([]Evidence, []byte, error)
}
var _ Scanner = (*scanner)(nil)
type scanner struct {
coverageThreshold float64 // between 0 and 100
includeLicenseContent bool
scanner func([]byte) licensecheck.Coverage
coverageThreshold float64 // between 0 and 100
scanner func([]byte) licensecheck.Coverage
}
type ScannerConfig struct {
CoverageThreshold float64
IncludeLicenseContent bool
Scanner func([]byte) licensecheck.Coverage
CoverageThreshold float64
Scanner func([]byte) licensecheck.Coverage
}
type Option func(*scanner)
@@ -45,12 +49,6 @@ func WithCoverage(coverage float64) Option {
}
}
func WithIncludeLicenseContent(includeLicenseContent bool) Option {
return func(s *scanner) {
s.includeLicenseContent = includeLicenseContent
}
}
// NewDefaultScanner returns a scanner that uses a new instance of the default licensecheck package scanner.
func NewDefaultScanner(o ...Option) (Scanner, error) {
s, err := licensecheck.NewScanner(licensecheck.BuiltinLicenses())
@@ -58,10 +56,10 @@ func NewDefaultScanner(o ...Option) (Scanner, error) {
log.WithFields("error", err).Trace("unable to create default license scanner")
return nil, fmt.Errorf("unable to create default license scanner: %w", err)
}
newScanner := &scanner{
coverageThreshold: DefaultCoverageThreshold,
includeLicenseContent: DefaultIncludeLicenseContent,
scanner: s.Scan,
coverageThreshold: DefaultCoverageThreshold,
scanner: s.Scan,
}
for _, opt := range o {
@@ -78,8 +76,7 @@ func NewScanner(c *ScannerConfig) (Scanner, error) {
}
return &scanner{
coverageThreshold: c.CoverageThreshold,
includeLicenseContent: c.IncludeLicenseContent,
scanner: c.Scanner,
coverageThreshold: c.CoverageThreshold,
scanner: c.Scanner,
}, nil
}
-84
View File
@@ -1,84 +0,0 @@
package licenses
import (
"bytes"
"context"
"os"
"testing"
"github.com/google/licensecheck"
"github.com/stretchr/testify/require"
)
func TestIdentifyLicenseIDs(t *testing.T) {
type expectation struct {
yieldError bool
ids []string
content []byte
}
tests := []struct {
name string
in string
expected expectation
}{
{
name: "apache license 2.0",
in: `test-fixtures/apache-license-2.0`,
expected: expectation{
yieldError: false,
ids: []string{"Apache-2.0"},
content: nil,
},
},
{
name: "custom license includes content for IdentifyLicenseIDs",
in: "test-fixtures/nvidia-software-and-cuda-supplement",
expected: expectation{
yieldError: false,
ids: []string{},
content: mustOpen("test-fixtures/nvidia-software-and-cuda-supplement"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
content, err := os.ReadFile(test.in)
require.NoError(t, err)
ids, content, err := testScanner(false).IdentifyLicenseIDs(context.TODO(), bytes.NewReader(content))
if test.expected.yieldError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Len(t, ids, len(test.expected.ids))
require.Len(t, content, len(test.expected.content))
if len(test.expected.ids) > 0 {
require.Equal(t, ids, test.expected.ids)
}
if len(test.expected.content) > 0 {
require.Equal(t, content, test.expected.content)
}
}
})
}
}
func testScanner(includeLicenseContent bool) Scanner {
return &scanner{
coverageThreshold: DefaultCoverageThreshold,
includeLicenseContent: includeLicenseContent,
scanner: licensecheck.Scan,
}
}
func mustOpen(fixture string) []byte {
content, err := os.ReadFile(fixture)
if err != nil {
panic(err)
}
return content
}
-123
View File
@@ -1,123 +0,0 @@
package licenses
import (
"context"
"crypto/sha256"
"fmt"
"io"
"strings"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg"
)
const (
unknownLicenseType = "UNKNOWN"
UnknownLicensePrefix = unknownLicenseType + "_"
)
func getCustomLicenseContentHash(contents []byte) string {
hash := sha256.Sum256(contents)
return fmt.Sprintf("%x", hash[:])
}
func (s *scanner) IdentifyLicenseIDs(_ context.Context, reader io.Reader) ([]string, []byte, error) {
if s.scanner == nil {
return nil, nil, nil
}
content, err := io.ReadAll(reader)
if err != nil {
return nil, nil, err
}
cov := s.scanner(content)
if cov.Percent < s.coverageThreshold {
// unknown or no licenses here
// => check return content to Search to process
return nil, content, nil
}
var ids []string
for _, m := range cov.Match {
ids = append(ids, m.ID)
}
return ids, nil, nil
}
// PkgSearch scans the contents of a license file to attempt to determine the type of license it is
func (s *scanner) PkgSearch(ctx context.Context, reader file.LocationReadCloser) (licenses []pkg.License, err error) {
licenses = make([]pkg.License, 0)
ids, content, err := s.IdentifyLicenseIDs(ctx, reader)
if err != nil {
return nil, err
}
// IdentifyLicenseIDs can only return a list of ID or content
// These return values are mutually exclusive.
// If the scanner threshold for matching scores < 75% then we return the license full content
if len(ids) > 0 {
for _, id := range ids {
lic := pkg.NewLicenseFromLocations(id, reader.Location)
lic.Type = license.Concluded
licenses = append(licenses, lic)
}
} else if len(content) > 0 {
// harmonize line endings to unix compatible first:
// 1. \r\n => \n (Windows => UNIX)
// 2. \r => \n (Macintosh => UNIX)
content = []byte(strings.ReplaceAll(strings.ReplaceAll(string(content), "\r\n", "\n"), "\r", "\n"))
lic := pkg.NewLicenseFromLocations(unknownLicenseType, reader.Location)
lic.SPDXExpression = UnknownLicensePrefix + getCustomLicenseContentHash(content)
if s.includeLicenseContent {
lic.Contents = string(content)
}
lic.Type = license.Declared
licenses = append(licenses, lic)
}
return licenses, nil
}
// FileSearch scans the contents of a license file to attempt to determine the type of license it is
func (s *scanner) FileSearch(ctx context.Context, reader file.LocationReadCloser) (licenses []file.License, err error) {
licenses = make([]file.License, 0)
ids, content, err := s.IdentifyLicenseIDs(ctx, reader)
if err != nil {
return nil, err
}
// IdentifyLicenseIDs can only return a list of ID or content
// These return values are mutually exclusive.
// If the scanner threshold for matching scores < 75% then we return the license full content
if len(ids) > 0 {
for _, id := range ids {
lic := file.NewLicense(id)
lic.Type = license.Concluded
licenses = append(licenses, lic)
}
} else if len(content) > 0 {
// harmonize line endings to unix compatible first:
// 1. \r\n => \n (Windows => UNIX)
// 2. \r => \n (Macintosh => UNIX)
content = []byte(strings.ReplaceAll(strings.ReplaceAll(string(content), "\r\n", "\n"), "\r", "\n"))
lic := file.NewLicense(unknownLicenseType)
lic.SPDXExpression = UnknownLicensePrefix + getCustomLicenseContentHash(content)
if s.includeLicenseContent {
lic.Contents = string(content)
}
lic.Type = license.Declared
licenses = append(licenses, lic)
}
return licenses, nil
}
-166
View File
@@ -1,166 +0,0 @@
package licenses
import (
"bytes"
"context"
"io"
"os"
"testing"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
type bytesReadCloser struct {
bytes.Buffer
}
func (brc *bytesReadCloser) Close() error {
return nil
}
func newBytesReadCloser(data []byte) *bytesReadCloser {
return &bytesReadCloser{
Buffer: *bytes.NewBuffer(data),
}
}
func TestSearchFileLicenses(t *testing.T) {
type expectation struct {
yieldError bool
licenses []file.License
}
tests := []struct {
name string
in string
includeUnkownLicenseContent bool
expected expectation
}{
{
name: "apache license 2.0",
in: "test-fixtures/apache-license-2.0",
expected: expectation{
yieldError: false,
licenses: []file.License{
{
Value: "Apache-2.0",
SPDXExpression: "Apache-2.0",
Type: "concluded",
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := context.TODO()
content, err := os.ReadFile(test.in)
require.NoError(t, err)
s := testScanner(false)
result, err := s.FileSearch(ctx, file.NewLocationReadCloser(file.NewLocation("LICENSE"), io.NopCloser(bytes.NewReader(content))))
if test.expected.yieldError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Len(t, result, len(test.expected.licenses))
if len(test.expected.licenses) > 0 {
require.Equal(t, test.expected.licenses, result)
}
}
})
}
}
func TestSearchPkgLicenses(t *testing.T) {
type expectation struct {
wantErr require.ErrorAssertionFunc
licenses []pkg.License
}
testLocation := file.NewLocation("LICENSE")
tests := []struct {
name string
in string
includeUnkownLicenseContent bool
expected expectation
}{
{
name: "apache license 2.0",
in: "test-fixtures/apache-license-2.0",
expected: expectation{
licenses: []pkg.License{
{
Value: "Apache-2.0",
SPDXExpression: "Apache-2.0",
Type: "concluded",
URLs: nil,
Locations: file.NewLocationSet(testLocation),
Contents: "",
},
},
wantErr: nil,
},
},
{
name: "custom license no content by default",
in: "test-fixtures/nvidia-software-and-cuda-supplement",
expected: expectation{
licenses: []pkg.License{
{
Value: "UNKNOWN",
SPDXExpression: "UNKNOWN_eebcea3ab1d1a28e671de90119ffcfb35fe86951e4af1b17af52b7a82fcf7d0a",
Type: "declared",
URLs: nil,
Locations: file.NewLocationSet(testLocation),
Contents: "",
},
},
wantErr: nil,
},
},
{
name: "custom license with content when scanner has content config",
in: "test-fixtures/nvidia-software-and-cuda-supplement",
includeUnkownLicenseContent: true,
expected: expectation{
licenses: []pkg.License{
{
Value: "UNKNOWN",
SPDXExpression: "UNKNOWN_eebcea3ab1d1a28e671de90119ffcfb35fe86951e4af1b17af52b7a82fcf7d0a",
Type: "declared",
URLs: nil,
Locations: file.NewLocationSet(testLocation),
Contents: string(mustOpen("test-fixtures/nvidia-software-and-cuda-supplement")),
},
},
wantErr: nil,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := context.TODO()
content, err := os.ReadFile(test.in)
require.NoError(t, err)
s := testScanner(test.includeUnkownLicenseContent)
result, err := s.PkgSearch(ctx, file.NewLocationReadCloser(file.NewLocation("LICENSE"), io.NopCloser(bytes.NewReader(content))))
if test.expected.wantErr != nil {
test.expected.wantErr(t, err)
}
require.NoError(t, err)
require.Len(t, result, len(test.expected.licenses))
if len(test.expected.licenses) > 0 {
require.Equal(t, test.expected.licenses, result)
}
})
}
}
@@ -0,0 +1,445 @@
Emscripten is available under 2 licenses, the MIT license and the
University of Illinois/NCSA Open Source License.
Both are permissive open source licenses, with little if any
practical difference between them.
The reason for offering both is that (1) the MIT license is
well-known, while (2) the University of Illinois/NCSA Open Source
License allows Emscripten's code to be integrated upstream into
LLVM, which uses that license, should the opportunity arise.
Additionally, the binaryen project is available under the Apache License
Version 2.0.
The full text of all three licenses follows.
==============================================================================
Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
==============================================================================
Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file.
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal with the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimers.
Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimers
in the documentation and/or other materials provided with the
distribution.
Neither the names of Mozilla,
nor the names of its contributors may be used to endorse
or promote products derived from this Software without specific prior
written permission.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE.
==============================================================================
This program uses portions of Node.js source code located in src/library_path.js,
in accordance with the terms of the MIT license. Node's license follows:
"""
Copyright Joyent, Inc. and other Node contributors. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
"""
==============================================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================
Simple DirectMedia Layer
Copyright (C) 1997-2014 Sam Lantinga <slouken@libsdl.org>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
==============================================================================
Files: tools/filelock.py
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org>
==============================================================================
Files: tools/eliminator/node_modules/uglify-js/... tools/node_modules/terser/...
Distributed under the BSD license:
Copyright 2012 (c) Mihai Bazon <mihai.bazon@gmail.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above
copyright notice, this list of conditions and the following
disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials
provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS” AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
==============================================================================
Files: system/include/webgpu/webgpu.h
BSD 3-Clause License
Copyright (c) 2019, "WebGPU native" developers
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
==============================================================================
Copyright (c) 2005-2011 David Schultz <das@FreeBSD.ORG>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
+63 -6
View File
@@ -92,6 +92,12 @@ func findOwnershipByFilesRelationships(resolver file.Resolver, catalog *pkg.Coll
return relationships
}
// Build a map of real paths to packages that directly own them. We'll use this
// to check if a file is already owned by the same type of package when we're
// determining ownership via symlink.
directOwnership := directOwnersByPath(catalog)
// Now establish relationships, considering symlinks
for _, candidateOwnerPkg := range catalog.Sorted() {
id := candidateOwnerPkg.ID()
if candidateOwnerPkg.Metadata == nil {
@@ -105,16 +111,34 @@ func findOwnershipByFilesRelationships(resolver file.Resolver, catalog *pkg.Coll
}
for _, ownedFilePath := range pkgFileOwner.OwnedFiles() {
// find the first path that results in a hit
for _, ownedPath := range allPaths(ownedFilePath, resolver) {
if matchesAny(ownedPath, globsForbiddenFromBeingOwned) {
if ownedFilePath == "" {
continue
}
// find paths that result in a hit (includes resolving symlinks)
resolvedPaths := resolvePaths(ownedFilePath, resolver)
for _, resolvedPath := range resolvedPaths {
if matchesAny(resolvedPath, globsForbiddenFromBeingOwned) {
// we skip over known exceptions to file ownership, such as the RPM package owning
// the RPM DB path, otherwise the RPM package would "own" all RPMs, which is not intended
continue
}
// Skip claiming ownership via symlink if another package of the same type
// directly owns this real path. This is the specific fix for the issue where a
// symlink shouldn't allow a package to claim ownership when another package of
// the same type directly owns the real file.
if resolvedPath != ownedFilePath { // This is a resolved symlink path
// Check if another package of the same type directly owns this path
if paths := directOwnership[candidateOwnerPkg.Type]; paths != nil && paths.Has(resolvedPath) {
// Skip this path - a package of the same type directly owns it
continue
}
}
// look for package(s) in the catalog that may be owned by this package and mark the relationship
for _, subPackage := range catalog.PackagesByPath(ownedPath) {
for _, subPackage := range catalog.PackagesByPath(resolvedPath) {
subID := subPackage.ID()
if subID == id {
continue
@@ -126,7 +150,7 @@ func findOwnershipByFilesRelationships(resolver file.Resolver, catalog *pkg.Coll
if _, exists := relationships[id][subID]; !exists {
relationships[id][subID] = strset.New()
}
relationships[id][subID].Add(ownedPath)
relationships[id][subID].Add(resolvedPath)
}
}
}
@@ -135,7 +159,40 @@ func findOwnershipByFilesRelationships(resolver file.Resolver, catalog *pkg.Coll
return relationships
}
func allPaths(ownedFilePath string, resolver file.Resolver) []string {
func directOwnersByPath(catalog *pkg.Collection) map[pkg.Type]*strset.Set {
directOwnership := map[pkg.Type]*strset.Set{}
// First, identify direct ownership of all files
for _, p := range catalog.Sorted() {
if p.Metadata == nil {
continue
}
// check to see if this is a file owner
pkgFileOwner, ok := p.Metadata.(pkg.FileOwner)
if !ok {
continue
}
for _, ownedFilePath := range pkgFileOwner.OwnedFiles() {
if ownedFilePath == "" {
continue
}
// Register direct ownership
paths := directOwnership[p.Type]
if paths == nil {
paths = strset.New()
directOwnership[p.Type] = paths
}
paths.Add(ownedFilePath)
}
}
return directOwnership
}
func resolvePaths(ownedFilePath string, resolver file.Resolver) []string {
// though we have a string path, we need to resolve symlinks and other filesystem oddities since we cannot assume this is a real path
var locs []file.Location
var err error
@@ -78,6 +78,64 @@ func TestOwnershipByFilesRelationship(t *testing.T) {
return []pkg.Package{parent, child}, []artifact.Relationship{relationship}
},
},
{
name: "only-real-file-owner-gets-ownership-relationship-not-symlink-owner",
resolver: mockFR{
translate: map[string]string{
"/usr/bin/jenkins.war-symlink": "/usr/share/jenkins/jenkins.war", // symlink to the real file
},
},
setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
// Package that owns the symlink
symlinkOwner := pkg.Package{
Type: pkg.ApkPkg,
Metadata: pkg.ApkDBEntry{
Package: "jenkins-symlink-pkg",
Files: []pkg.ApkFileRecord{
{Path: "/usr/bin/jenkins.war-symlink"}, // this symlinks to the real file
},
},
}
symlinkOwner.SetID()
// Package that owns the real file
realFileOwner := pkg.Package{
Type: pkg.ApkPkg,
Metadata: pkg.ApkDBEntry{
Package: "jenkins-real-pkg",
Files: []pkg.ApkFileRecord{
{Path: "/usr/share/jenkins/jenkins.war"}, // the real file
},
},
}
realFileOwner.SetID()
// A third package that is "located at" the real file path
// This simulates a package being discovered at the jenkins.war location
consumerPackage := pkg.Package{
Type: pkg.JavaPkg,
Locations: file.NewLocationSet(
file.NewVirtualLocation("/usr/share/jenkins/jenkins.war", "/usr/share/jenkins/jenkins.war"),
),
}
consumerPackage.SetID()
// Only the real file owner should have an ownership relationship with the consumer package
// The symlink owner should NOT have a relationship with the consumer
relationship := artifact.Relationship{
From: realFileOwner,
To: consumerPackage,
Type: artifact.OwnershipByFileOverlapRelationship,
Data: ownershipByFilesMetadata{
Files: []string{
"/usr/share/jenkins/jenkins.war",
},
},
}
return []pkg.Package{symlinkOwner, realFileOwner, consumerPackage}, []artifact.Relationship{relationship}
},
},
{
name: "misses-by-dead-symlink",
resolver: mockFR{
+31
View File
@@ -107,6 +107,11 @@ func finalizePkgCatalogerResults(cfg CatalogingFactoryConfig, resolver file.Path
}
}
// we want to know if the user wants to preserve license content or not in the final SBOM
// note: this looks incorrect, but pkg.License.Content is NOT used to compute the Package ID
// this does NOT change the reproducibility of the Package ID
applyLicenseContentRules(&p, cfg.LicenseConfig)
pkgs[i] = p
}
return pkgs, relationships
@@ -262,3 +267,29 @@ func packageFileOwnershipRelationships(p pkg.Package, resolver file.PathResolver
}
return relationships, nil
}
func applyLicenseContentRules(p *pkg.Package, cfg cataloging.LicenseConfig) {
if p.Licenses.Empty() {
return
}
licenses := p.Licenses.ToSlice()
for i := range licenses {
l := &licenses[i]
switch cfg.IncludeContent {
case cataloging.LicenseContentIncludeUnknown:
// we have an SPDX expression, which means this is NOT an unknown license
// remove the content, we are only including content for unknown licenses by default
if l.SPDXExpression != "" {
licenses[i].Contents = ""
}
case cataloging.LicenseContentIncludeAll:
// always include the content
default:
// clear it all out
licenses[i].Contents = ""
}
}
p.Licenses = pkg.NewLicenseSet(licenses...)
}
+143
View File
@@ -117,6 +117,149 @@ func TestFilterNonCompliantPackages(t *testing.T) {
assert.Equal(t, p2, droppedPkgs[0])
}
func TestApplyLicenseContentRules(t *testing.T) {
licenseWithSPDX := pkg.License{
SPDXExpression: "MIT",
Contents: "MIT license content",
}
licenseWithoutSPDX := pkg.License{
Value: "License-Not-A-SPDX-Expression",
Contents: "Non-SPDX license content",
}
tests := []struct {
name string
inputLicenses []pkg.License
cfg cataloging.LicenseConfig
expectedLicenses []pkg.License
}{
{
name: "LicenseContentIncludeUnknown",
inputLicenses: []pkg.License{
licenseWithSPDX,
licenseWithoutSPDX,
},
cfg: cataloging.LicenseConfig{
IncludeContent: cataloging.LicenseContentIncludeUnknown,
},
expectedLicenses: []pkg.License{
{
SPDXExpression: "MIT",
Contents: "", // content cleared for SPDX license
},
{
Value: "License-Not-A-SPDX-Expression",
Contents: "Non-SPDX license content", // content preserved for non-SPDX
},
},
},
{
name: "LicenseContentExcludeAll",
inputLicenses: []pkg.License{
licenseWithSPDX,
licenseWithoutSPDX,
},
cfg: cataloging.LicenseConfig{
IncludeContent: cataloging.LicenseContentExcludeAll,
},
expectedLicenses: []pkg.License{
{
SPDXExpression: "MIT",
Contents: "", // content cleared
},
{
Value: "License-Not-A-SPDX-Expression",
Contents: "", // content cleared
},
},
},
{
name: "LicenseContentIncludeAll",
inputLicenses: []pkg.License{
licenseWithSPDX,
licenseWithoutSPDX,
},
cfg: cataloging.LicenseConfig{
IncludeContent: cataloging.LicenseContentIncludeAll,
},
expectedLicenses: []pkg.License{
{
SPDXExpression: "MIT",
Contents: "MIT license content", // content preserved
},
{
Value: "License-Not-A-SPDX-Expression",
Contents: "Non-SPDX license content", // content preserved
},
},
},
{
name: "default license config should be LicenseContentExcludeAll",
inputLicenses: []pkg.License{
licenseWithSPDX,
licenseWithoutSPDX,
},
cfg: cataloging.DefaultLicenseConfig(),
expectedLicenses: []pkg.License{
{
SPDXExpression: "MIT",
},
{
Value: "License-Not-A-SPDX-Expression",
},
},
},
{
name: "invalid license content cataloging config results in the default case",
inputLicenses: []pkg.License{
licenseWithSPDX,
licenseWithoutSPDX,
},
cfg: cataloging.LicenseConfig{
IncludeContent: cataloging.LicenseContent("invalid"),
},
expectedLicenses: []pkg.License{
{
SPDXExpression: "MIT",
},
{
Value: "License-Not-A-SPDX-Expression",
Contents: "", // content all removed
},
},
},
{
name: "Empty licenses",
inputLicenses: []pkg.License{},
cfg: cataloging.LicenseConfig{
IncludeContent: cataloging.LicenseContentIncludeAll,
},
expectedLicenses: []pkg.License{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
inputPkg := &pkg.Package{
Licenses: pkg.NewLicenseSet(tt.inputLicenses...),
}
inputPkg.SetID()
originalID := inputPkg.ID()
applyLicenseContentRules(inputPkg, tt.cfg)
assert.Equal(t, originalID, inputPkg.ID(), "package ID changed unexpectedly")
actualLicenses := inputPkg.Licenses.ToSlice()
expectedLicenses := pkg.NewLicenseSet(tt.expectedLicenses...).ToSlice()
assert.Equal(t, expectedLicenses, actualLicenses, "license contents do not match expected values")
})
}
}
func TestApplyComplianceRules_DropAndStub(t *testing.T) {
p := pkg.Package{Name: "", Version: ""}
p.SetID()
+6 -2
View File
@@ -17,6 +17,7 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/githubactions"
"github.com/anchore/syft/syft/pkg/cataloger/golang"
"github.com/anchore/syft/syft/pkg/cataloger/haskell"
"github.com/anchore/syft/syft/pkg/cataloger/homebrew"
"github.com/anchore/syft/syft/pkg/cataloger/java"
"github.com/anchore/syft/syft/pkg/cataloger/javascript"
"github.com/anchore/syft/syft/pkg/cataloger/kernel"
@@ -76,6 +77,7 @@ func DefaultPackageTaskFactories() Factories {
// language-specific package declared catalogers ///////////////////////////////////////////////////////////////////////////
newSimplePackageTaskFactory(cpp.NewConanCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "cpp", "conan"),
newSimplePackageTaskFactory(dart.NewPubspecLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"),
newSimplePackageTaskFactory(dart.NewPubspecCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"),
newSimplePackageTaskFactory(elixir.NewMixLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "elixir"),
newSimplePackageTaskFactory(erlang.NewRebarLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "erlang"),
newSimplePackageTaskFactory(erlang.NewOTPCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "erlang", "otp"),
@@ -152,7 +154,8 @@ func DefaultPackageTaskFactories() Factories {
},
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary",
),
newSimplePackageTaskFactory(binary.NewELFPackageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary", "elf-package"),
newSimplePackageTaskFactory(binary.NewELFPackageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary", "elf-package", "elf"),
newSimplePackageTaskFactory(binary.NewPEPackageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary", "pe-package", "pe", "dll", "exe"),
newSimplePackageTaskFactory(githubactions.NewActionUsageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "github", "github-actions"),
newSimplePackageTaskFactory(githubactions.NewWorkflowUsageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "github", "github-actions"),
newSimplePackageTaskFactory(java.NewJvmDistributionCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "java", "jvm", "jdk", "jre"),
@@ -162,10 +165,12 @@ func DefaultPackageTaskFactories() Factories {
},
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "linux", "kernel",
),
newSimplePackageTaskFactory(php.NewInterpreterCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary", "php"),
newSimplePackageTaskFactory(sbomCataloger.NewCataloger, "sbom"), // note: not evidence of installed packages
newSimplePackageTaskFactory(bitnamiSbomCataloger.NewCataloger, "bitnami", pkgcataloging.InstalledTag, pkgcataloging.ImageTag),
newSimplePackageTaskFactory(wordpress.NewWordpressPluginCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "wordpress"),
newSimplePackageTaskFactory(terraform.NewLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "terraform"),
newSimplePackageTaskFactory(homebrew.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "homebrew"),
// deprecated catalogers ////////////////////////////////////////
// these are catalogers that should not be selectable other than specific inclusion via name or "deprecated" tag (to remain backwards compatible)
@@ -173,6 +178,5 @@ func DefaultPackageTaskFactories() Factories {
newSimplePackageTaskFactory(dotnet.NewDotnetPortableExecutableCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
newSimplePackageTaskFactory(php.NewPeclCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
newSimplePackageTaskFactory(nix.NewStoreCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+116 -5
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.0.28/document",
"$id": "anchore.io/schema/syft/json/16.0.34/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@@ -408,6 +408,49 @@
"path"
]
},
"DartPubspec": {
"properties": {
"homepage": {
"type": "string"
},
"repository": {
"type": "string"
},
"documentation": {
"type": "string"
},
"publish_to": {
"type": "string"
},
"environment": {
"$ref": "#/$defs/DartPubspecEnvironment"
},
"platforms": {
"items": {
"type": "string"
},
"type": "array"
},
"ignored_advisories": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"DartPubspecEnvironment": {
"properties": {
"sdk": {
"type": "string"
},
"flutter": {
"type": "string"
}
},
"type": "object"
},
"DartPubspecLockEntry": {
"properties": {
"name": {
@@ -1072,6 +1115,20 @@
},
"type": "object"
},
"HomebrewFormula": {
"properties": {
"tap": {
"type": "string"
},
"homepage": {
"type": "string"
},
"description": {
"type": "string"
}
},
"type": "object"
},
"IDLikes": {
"items": {
"type": "string"
@@ -1391,9 +1448,6 @@
"value": {
"type": "string"
},
"fullText": {
"type": "string"
},
"spdxExpression": {
"type": "string"
},
@@ -1419,7 +1473,6 @@
"type": "object",
"required": [
"value",
"fullText",
"spdxExpression",
"type",
"urls",
@@ -1837,6 +1890,9 @@
{
"$ref": "#/$defs/CocoaPodfileLockEntry"
},
{
"$ref": "#/$defs/DartPubspec"
},
{
"$ref": "#/$defs/DartPubspecLockEntry"
},
@@ -1879,6 +1935,9 @@
{
"$ref": "#/$defs/HaskellHackageStackLockEntry"
},
{
"$ref": "#/$defs/HomebrewFormula"
},
{
"$ref": "#/$defs/JavaArchive"
},
@@ -1912,6 +1971,9 @@
{
"$ref": "#/$defs/OpamPackage"
},
{
"$ref": "#/$defs/PeBinary"
},
{
"$ref": "#/$defs/PhpComposerInstalledEntry"
},
@@ -1986,6 +2048,17 @@
"purl"
]
},
"PeBinary": {
"properties": {
"VersionResources": {
"$ref": "#/$defs/KeyValues"
}
},
"type": "object",
"required": [
"VersionResources"
]
},
"PhpComposerAuthors": {
"properties": {
"name": {
@@ -2266,6 +2339,9 @@
"installedSize": {
"type": "integer"
},
"licenses": {
"type": "string"
},
"files": {
"items": {
"$ref": "#/$defs/PortageFileRecord"
@@ -2615,6 +2691,12 @@
"sourceRpm": {
"type": "string"
},
"signatures": {
"items": {
"$ref": "#/$defs/RpmSignature"
},
"type": "array"
},
"size": {
"type": "integer"
},
@@ -2683,6 +2765,12 @@
"sourceRpm": {
"type": "string"
},
"signatures": {
"items": {
"$ref": "#/$defs/RpmSignature"
},
"type": "array"
},
"size": {
"type": "integer"
},
@@ -2759,6 +2847,29 @@
"flags"
]
},
"RpmSignature": {
"properties": {
"algo": {
"type": "string"
},
"hash": {
"type": "string"
},
"created": {
"type": "string"
},
"issuer": {
"type": "string"
}
},
"type": "object",
"required": [
"algo",
"hash",
"created",
"issuer"
]
},
"RubyGemspec": {
"properties": {
"name": {
+4 -4
View File
@@ -1,4 +1,4 @@
(registered 2021-11-05, last updated 2021-11-05)
(registered 2021-11-05, last updated 2025-13-05)
Media type name: application
Media subtype name: vnd.syft+json
@@ -60,11 +60,11 @@ General Comments:
Person to contact for further information:
1. Name: Dan Luhring
2. Email: dan.luhring@anchore.com
1. Name: Dan Nurmi
2. Email: opensource@anchore.com
Intended usage: Common
The Syft SBOM format is an open-source software bill of materials specification. It is intended to be exchanged between different parties of the software supply chain.
Author/Change controller: Dan Luhring, on behalf of Anchore
Author/Change controller: Dan Nurmi, on behalf of Anchore
+19 -5
View File
@@ -1,15 +1,29 @@
package cataloging
import "github.com/anchore/syft/internal/licenses"
import (
"github.com/anchore/syft/internal/licenses"
)
// LicenseContent controls when license content should be included in the SBOM.
type LicenseContent string
const (
LicenseContentIncludeAll LicenseContent = "all"
LicenseContentIncludeUnknown LicenseContent = "unknown"
LicenseContentExcludeAll LicenseContent = "none"
)
type LicenseConfig struct {
IncludeUnkownLicenseContent bool `json:"include-unknown-license-content" yaml:"include-unknown-license-content" mapstructure:"include-unknown-license-content"`
Coverage float64 `json:"coverage" yaml:"coverage" mapstructure:"coverage"`
// IncludeContent controls whether license copy discovered should be included in the SBOM.
IncludeContent LicenseContent `json:"include-content" yaml:"include-content" mapstructure:"include-content"`
// Coverage is the percentage of text that must match a license for it to be considered a match.
Coverage float64 `json:"coverage" yaml:"coverage" mapstructure:"coverage"`
}
func DefaultLicenseConfig() LicenseConfig {
return LicenseConfig{
IncludeUnkownLicenseContent: licenses.DefaultIncludeLicenseContent,
Coverage: licenses.DefaultCoverageThreshold,
IncludeContent: LicenseContentExcludeAll,
Coverage: licenses.DefaultCoverageThreshold,
}
}
-1
View File
@@ -109,7 +109,6 @@ func setupContext(ctx context.Context, cfg *CreateSBOMConfig) (context.Context,
func SetContextLicenseScanner(ctx context.Context, cfg cataloging.LicenseConfig) (context.Context, error) {
// inject a single license scanner and content config for all package cataloging tasks into context
licenseScanner, err := licenses.NewDefaultScanner(
licenses.WithIncludeLicenseContent(cfg.IncludeUnkownLicenseContent),
licenses.WithCoverage(cfg.Coverage),
)
if err != nil {
+1
View File
@@ -255,6 +255,7 @@ func (c *CreateSBOMConfig) selectTasks(src source.Description) ([]task.Task, []t
RelationshipsConfig: c.Relationships,
DataGenerationConfig: c.DataGeneration,
PackagesConfig: c.Packages,
LicenseConfig: c.Licenses,
ComplianceConfig: c.Compliance,
FilesConfig: c.Files,
}
+28 -7
View File
@@ -59,7 +59,34 @@ func (s CoordinateSet) Paths() []string {
return pathSlice
}
func (s CoordinateSet) ToSlice() []Coordinates {
func (s CoordinateSet) ToSlice(sorters ...func(a, b Coordinates) int) []Coordinates {
coordinates := s.ToUnorderedSlice()
var sorted bool
for _, sorter := range sorters {
if sorter == nil {
continue
}
sort.Slice(coordinates, func(i, j int) bool {
return sorter(coordinates[i], coordinates[j]) < 0
})
sorted = true
break
}
if !sorted {
sort.SliceStable(coordinates, func(i, j int) bool {
if coordinates[i].FileSystemID == coordinates[j].FileSystemID {
return coordinates[i].RealPath < coordinates[j].RealPath
}
return coordinates[i].FileSystemID < coordinates[j].FileSystemID
})
}
return coordinates
}
func (s CoordinateSet) ToUnorderedSlice() []Coordinates {
if s.set == nil {
return nil
}
@@ -69,12 +96,6 @@ func (s CoordinateSet) ToSlice() []Coordinates {
coordinates[idx] = v
idx++
}
sort.SliceStable(coordinates, func(i, j int) bool {
if coordinates[i].RealPath == coordinates[j].RealPath {
return coordinates[i].FileSystemID < coordinates[j].FileSystemID
}
return coordinates[i].RealPath < coordinates[j].RealPath
})
return coordinates
}
+23 -2
View File
@@ -1,6 +1,7 @@
package file
import (
"slices"
"sort"
"github.com/gohugoio/hashstructure"
@@ -54,7 +55,28 @@ func (s LocationSet) Contains(l Location) bool {
return ok
}
func (s LocationSet) ToSlice() []Location {
func (s LocationSet) ToSlice(sorters ...func(a, b Location) int) []Location {
locations := s.ToUnorderedSlice()
var sorted bool
for _, sorter := range sorters {
if sorter == nil {
continue
}
slices.SortFunc(locations, sorter)
sorted = true
break
}
if !sorted {
// though no sorter was passed, we need to guarantee a stable ordering between calls
sort.Sort(Locations(locations))
}
return locations
}
func (s LocationSet) ToUnorderedSlice() []Location {
if s.set == nil {
return nil
}
@@ -67,7 +89,6 @@ func (s LocationSet) ToSlice() []Location {
}
idx++
}
sort.Sort(Locations(locations))
return locations
}
+2 -2
View File
@@ -89,8 +89,8 @@ func TestLocationSet_SortPaths(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
set := NewLocationSet(test.input...)
assert.Equal(t, test.expected, set.ToSlice())
actual := NewLocationSet(test.input...).ToSlice()
assert.Equal(t, test.expected, actual)
})
}
}
+107 -20
View File
@@ -1,6 +1,12 @@
package file
import "github.com/anchore/syft/internal/evidence"
import (
"strings"
"github.com/anchore/syft/internal/evidence"
)
var locationSorterWithoutLayers = LocationSorter(nil)
type Locations []Location
@@ -9,27 +15,108 @@ func (l Locations) Len() int {
}
func (l Locations) Less(i, j int) bool {
liEvidence := l[i].Annotations[evidence.AnnotationKey]
ljEvidence := l[j].Annotations[evidence.AnnotationKey]
if liEvidence == ljEvidence {
if l[i].RealPath == l[j].RealPath {
if l[i].AccessPath == l[j].AccessPath {
return l[i].FileSystemID < l[j].FileSystemID
}
return l[i].AccessPath < l[j].AccessPath
}
return l[i].RealPath < l[j].RealPath
}
if liEvidence == evidence.PrimaryAnnotation {
return true
}
if ljEvidence == evidence.PrimaryAnnotation {
return false
}
return liEvidence > ljEvidence
return locationSorterWithoutLayers(l[i], l[j]) < 0
}
func (l Locations) Swap(i, j int) {
l[i], l[j] = l[j], l[i]
}
// LocationSorter creates a comparison function (slices.SortFunc) for Location objects based on layer order
func LocationSorter(layers []string) func(a, b Location) int { //nolint:gocognit
var layerOrderByDigest map[string]int
if len(layers) > 0 {
layerOrderByDigest = make(map[string]int)
for i, digest := range layers {
layerOrderByDigest[digest] = i
}
}
return func(a, b Location) int {
// compare by evidence annotations first...
aEvidence := a.Annotations[evidence.AnnotationKey]
bEvidence := b.Annotations[evidence.AnnotationKey]
if aEvidence != bEvidence {
if aEvidence == evidence.PrimaryAnnotation {
return -1
}
if bEvidence == evidence.PrimaryAnnotation {
return 1
}
if aEvidence > bEvidence {
return -1
}
if bEvidence > aEvidence {
return 1
}
}
// ...then by layer order
if layerOrderByDigest != nil {
// we're given layer order details
aLayerIdx, aExists := layerOrderByDigest[a.FileSystemID]
bLayerIdx, bExists := layerOrderByDigest[b.FileSystemID]
if aLayerIdx != bLayerIdx {
if !aExists && !bExists {
return strings.Compare(a.FileSystemID, b.FileSystemID)
}
if !aExists {
return 1
}
if !bExists {
return -1
}
return aLayerIdx - bLayerIdx
}
} else if a.FileSystemID != b.FileSystemID {
// no layer info given, legacy behavior is to sort lexicographically
return strings.Compare(a.FileSystemID, b.FileSystemID)
}
// ...then by paths
if a.AccessPath != b.AccessPath {
return strings.Compare(a.AccessPath, b.AccessPath)
}
return strings.Compare(a.RealPath, b.RealPath)
}
}
// CoordinatesSorter creates a comparison function (slices.SortFunc) for Coordinate objects based on layer order
func CoordinatesSorter(layers []string) func(a, b Coordinates) int {
var layerOrderByDigest map[string]int
if len(layers) > 0 {
layerOrderByDigest = make(map[string]int)
for i, digest := range layers {
layerOrderByDigest[digest] = i
}
}
return func(a, b Coordinates) int {
// ...then by layer order
if layerOrderByDigest != nil {
aLayerIdx, aExists := layerOrderByDigest[a.FileSystemID]
bLayerIdx, bExists := layerOrderByDigest[b.FileSystemID]
if aLayerIdx != bLayerIdx {
if !aExists && !bExists {
return strings.Compare(a.FileSystemID, b.FileSystemID)
}
if !aExists {
return 1
}
if !bExists {
return -1
}
return aLayerIdx - bLayerIdx
}
}
return strings.Compare(a.RealPath, b.RealPath)
}
}
+557
View File
@@ -0,0 +1,557 @@
package file
import (
"fmt"
"slices"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/anchore/syft/internal/evidence"
)
func TestLocationAndCoordinatesSorters(t *testing.T) {
tests := []struct {
name string
layers []string
locs []Location
coords []Coordinates
wantLocs []string
wantCoords []string
}{
{
name: "empty location slice",
layers: []string{"fsid-1"},
locs: []Location{},
coords: []Coordinates{},
wantLocs: []string{},
wantCoords: []string{},
},
{
name: "nil layer list",
layers: nil,
locs: []Location{
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
AccessPath: "/a",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/b",
FileSystemID: "fsid-2",
},
AccessPath: "/b",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{},
},
},
},
coords: []Coordinates{
{
RealPath: "/a",
FileSystemID: "fsid-1",
},
{
RealPath: "/b",
FileSystemID: "fsid-2",
},
},
wantLocs: []string{
"/a (/a) @ fsid-1 map[]",
"/b (/b) @ fsid-2 map[]",
},
wantCoords: []string{
"/a @ fsid-1",
"/b @ fsid-2",
},
},
{
name: "sort by evidence type only",
layers: []string{"fsid-1"},
locs: []Location{
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
AccessPath: "/a",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: "",
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
},
AccessPath: "/b",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.SupportingAnnotation,
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/c",
FileSystemID: "fsid-1",
},
AccessPath: "/c",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
},
coords: []Coordinates{
{
RealPath: "/a",
FileSystemID: "fsid-1",
},
{
RealPath: "/b",
FileSystemID: "fsid-1",
},
{
RealPath: "/c",
FileSystemID: "fsid-1",
},
},
wantLocs: []string{
"/c (/c) @ fsid-1 map[evidence:primary]",
"/b (/b) @ fsid-1 map[evidence:supporting]",
"/a (/a) @ fsid-1 map[evidence:]",
},
wantCoords: []string{
"/a @ fsid-1",
"/b @ fsid-1",
"/c @ fsid-1",
},
},
{
name: "same evidence type, sort by layer",
layers: []string{"fsid-1", "fsid-2", "fsid-3"},
locs: []Location{
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/a",
FileSystemID: "fsid-3",
},
AccessPath: "/a",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
},
AccessPath: "/b",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/c",
FileSystemID: "fsid-2",
},
AccessPath: "/c",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
},
coords: []Coordinates{
{
RealPath: "/a",
FileSystemID: "fsid-3",
},
{
RealPath: "/b",
FileSystemID: "fsid-1",
},
{
RealPath: "/c",
FileSystemID: "fsid-2",
},
},
wantLocs: []string{
"/b (/b) @ fsid-1 map[evidence:primary]",
"/c (/c) @ fsid-2 map[evidence:primary]",
"/a (/a) @ fsid-3 map[evidence:primary]",
},
wantCoords: []string{
"/b @ fsid-1",
"/c @ fsid-2",
"/a @ fsid-3",
},
},
{
name: "same evidence and layer, sort by access path",
layers: []string{"fsid-1"},
locs: []Location{
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/x",
FileSystemID: "fsid-1",
},
AccessPath: "/c",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/y",
FileSystemID: "fsid-1",
},
AccessPath: "/a",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/z",
FileSystemID: "fsid-1",
},
AccessPath: "/b",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
},
coords: []Coordinates{
{
RealPath: "/x",
FileSystemID: "fsid-1",
},
{
RealPath: "/y",
FileSystemID: "fsid-1",
},
{
RealPath: "/z",
FileSystemID: "fsid-1",
},
},
wantLocs: []string{
"/y (/a) @ fsid-1 map[evidence:primary]",
"/z (/b) @ fsid-1 map[evidence:primary]",
"/x (/c) @ fsid-1 map[evidence:primary]",
},
wantCoords: []string{
"/x @ fsid-1",
"/y @ fsid-1",
"/z @ fsid-1",
},
},
{
name: "same evidence, layer, and access path - sort by real path",
layers: []string{"fsid-1"},
locs: []Location{
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/c",
FileSystemID: "fsid-1",
},
AccessPath: "/same",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
AccessPath: "/same",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
},
AccessPath: "/same",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
},
coords: []Coordinates{
{
RealPath: "/c",
FileSystemID: "fsid-1",
},
{
RealPath: "/a",
FileSystemID: "fsid-1",
},
{
RealPath: "/b",
FileSystemID: "fsid-1",
},
},
wantLocs: []string{
"/a (/same) @ fsid-1 map[evidence:primary]",
"/b (/same) @ fsid-1 map[evidence:primary]",
"/c (/same) @ fsid-1 map[evidence:primary]",
},
wantCoords: []string{
"/a @ fsid-1",
"/b @ fsid-1",
"/c @ fsid-1",
},
},
{
name: "unknown layers",
layers: []string{"fsid-1", "fsid-2"},
locs: []Location{
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/a",
FileSystemID: "unknown-1",
},
AccessPath: "/a",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/b",
FileSystemID: "unknown-2",
},
AccessPath: "/b",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{},
},
},
},
coords: []Coordinates{
{
RealPath: "/a",
FileSystemID: "unknown-1",
},
{
RealPath: "/b",
FileSystemID: "unknown-2",
},
},
wantLocs: []string{
"/a (/a) @ unknown-1 map[]",
"/b (/b) @ unknown-2 map[]",
},
wantCoords: []string{
"/a @ unknown-1",
"/b @ unknown-2",
},
},
{
name: "mixed known and unknown layers",
layers: []string{"fsid-1", "fsid-2"},
locs: []Location{
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
AccessPath: "/a",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/b",
FileSystemID: "unknown",
},
AccessPath: "/b",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{},
},
},
},
coords: []Coordinates{
{
RealPath: "/a",
FileSystemID: "fsid-1",
},
{
RealPath: "/b",
FileSystemID: "unknown",
},
},
wantLocs: []string{
"/a (/a) @ fsid-1 map[]",
"/b (/b) @ unknown map[]",
},
wantCoords: []string{
"/a @ fsid-1",
"/b @ unknown",
},
},
{
name: "evidence comparison when one has none",
layers: []string{"fsid-1"},
locs: []Location{
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/a",
FileSystemID: "fsid-1",
},
AccessPath: "/a",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
// No evidence.AnnotationKey
},
},
},
{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: "/b",
FileSystemID: "fsid-1",
},
AccessPath: "/b",
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{
evidence.AnnotationKey: evidence.PrimaryAnnotation,
},
},
},
},
coords: []Coordinates{
{
RealPath: "/a",
FileSystemID: "fsid-1",
},
{
RealPath: "/b",
FileSystemID: "fsid-1",
},
},
wantLocs: []string{
"/b (/b) @ fsid-1 map[evidence:primary]",
"/a (/a) @ fsid-1 map[]",
},
wantCoords: []string{
"/a @ fsid-1",
"/b @ fsid-1",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Run("Location", func(t *testing.T) {
locs := make([]Location, len(tt.locs))
copy(locs, tt.locs)
slices.SortFunc(locs, LocationSorter(tt.layers))
got := make([]string, len(locs))
for i, loc := range locs {
got[i] = fmt.Sprintf("%s (%s) @ %s %s",
loc.RealPath,
loc.AccessPath,
loc.FileSystemID,
loc.LocationMetadata.Annotations)
}
if d := cmp.Diff(tt.wantLocs, got); d != "" {
t.Errorf("LocationSorter() mismatch (-want +got):\n%s", d)
}
})
t.Run("Coordinates", func(t *testing.T) {
coords := make([]Coordinates, len(tt.coords))
copy(coords, tt.coords)
slices.SortFunc(coords, CoordinatesSorter(tt.layers))
got := make([]string, len(coords))
for i, coord := range coords {
got[i] = fmt.Sprintf("%s @ %s",
coord.RealPath,
coord.FileSystemID)
}
if d := cmp.Diff(tt.wantCoords, got); d != "" {
t.Errorf("CoordinatesSorter() mismatch (-want +got):\n%s", d)
}
})
})
}
}
@@ -42,17 +42,18 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
cdxBOM.SerialNumber = uuid.New().URN()
cdxBOM.Metadata = toBomDescriptor(s.Descriptor.Name, s.Descriptor.Version, s.Source)
coordinates, locationSorter := getCoordinates(s)
// Packages
packages := s.Artifacts.Packages.Sorted()
components := make([]cyclonedx.Component, len(packages))
for i, p := range packages {
components[i] = helpers.EncodeComponent(p)
components[i] = helpers.EncodeComponent(p, locationSorter)
}
components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...)
// Files
artifacts := s.Artifacts
coordinates := s.AllCoordinates()
for _, coordinate := range coordinates {
var metadata *file.Metadata
// File Info
@@ -95,6 +96,21 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
return cdxBOM
}
func getCoordinates(s sbom.SBOM) ([]file.Coordinates, func(a, b file.Location) int) {
var layers []string
if m, ok := s.Source.Metadata.(source.ImageMetadata); ok {
for _, l := range m.Layers {
layers = append(layers, l.Digest)
}
}
coordSorter := file.CoordinatesSorter(layers)
coordinates := s.AllCoordinates()
slices.SortFunc(coordinates, coordSorter)
return coordinates, file.LocationSorter(layers)
}
func digestsToHashes(digests []file.Digest) []cyclonedx.Hash {
var hashes []cyclonedx.Hash
for _, digest := range digests {
@@ -5,7 +5,6 @@ import (
"crypto/sha1"
"fmt"
"path"
"regexp"
"slices"
"sort"
"strings"
@@ -15,13 +14,13 @@ import (
"github.com/spdx/tools-golang/spdx"
"github.com/anchore/packageurl-go"
internallicenses "github.com/anchore/syft/internal/licenses"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/mimetype"
"github.com/anchore/syft/internal/relationship"
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
formatInternal "github.com/anchore/syft/syft/format/internal"
"github.com/anchore/syft/syft/format/internal/spdxutil/helpers"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
@@ -49,7 +48,7 @@ func ToFormatModel(s sbom.SBOM) *spdx.Document {
name, namespace := helpers.DocumentNameAndNamespace(s.Source, s.Descriptor)
rels := relationship.NewIndex(s.Relationships...)
packages := toPackages(rels, s.Artifacts.Packages, s)
packages, otherLicenses := toPackages(rels, s.Artifacts.Packages, s)
allRelationships := toRelationships(rels.All())
@@ -155,7 +154,7 @@ func ToFormatModel(s sbom.SBOM) *spdx.Document {
Packages: packages,
Files: toFiles(s),
Relationships: allRelationships,
OtherLicenses: toOtherLicenses(s.Artifacts.Packages),
OtherLicenses: convertOtherLicense(otherLicenses),
}
}
@@ -268,21 +267,34 @@ func toRootPackage(s source.Description) *spdx.Package {
}
func toSPDXID(identifiable artifact.Identifiable) spdx.ElementID {
id := string(identifiable.ID())
if strings.HasPrefix(id, "SPDXRef-") {
// this is already an SPDX ID, no need to change it (except for the prefix)
return spdx.ElementID(helpers.SanitizeElementID(strings.TrimPrefix(id, "SPDXRef-")))
}
maxLen := 40
id := ""
switch it := identifiable.(type) {
case pkg.Package:
if strings.HasPrefix(id, "Package-") {
// this is already an SPDX ID, no need to change it
return spdx.ElementID(helpers.SanitizeElementID(id))
}
switch {
case it.Type != "" && it.Name != "":
id = fmt.Sprintf("Package-%s-%s-%s", it.Type, it.Name, it.ID())
id = fmt.Sprintf("Package-%s-%s-%s", it.Type, it.Name, id)
case it.Name != "":
id = fmt.Sprintf("Package-%s-%s", it.Name, it.ID())
id = fmt.Sprintf("Package-%s-%s", it.Name, id)
case it.Type != "":
id = fmt.Sprintf("Package-%s-%s", it.Type, it.ID())
id = fmt.Sprintf("Package-%s-%s", it.Type, id)
default:
id = fmt.Sprintf("Package-%s", it.ID())
id = fmt.Sprintf("Package-%s", id)
}
case file.Coordinates:
if strings.HasPrefix(id, "File-") {
// this is already an SPDX ID, no need to change it. Note: there is no way to reach this case today
// from within syft, however, this covers future cases where the ID can be overridden
return spdx.ElementID(helpers.SanitizeElementID(id))
}
p := ""
parts := strings.Split(it.RealPath, "/")
for i := len(parts); i > 0; i-- {
@@ -296,9 +308,7 @@ func toSPDXID(identifiable artifact.Identifiable) spdx.ElementID {
}
p = path.Join(part, p)
}
id = fmt.Sprintf("File-%s-%s", p, it.ID())
default:
id = string(identifiable.ID())
id = fmt.Sprintf("File-%s-%s", p, id)
}
// NOTE: the spdx library prepend SPDXRef-, so we don't do it here
return spdx.ElementID(helpers.SanitizeElementID(id))
@@ -307,16 +317,18 @@ func toSPDXID(identifiable artifact.Identifiable) spdx.ElementID {
// packages populates all Package Information from the package Collection (see https://spdx.github.io/spdx-spec/3-package-information/)
//
//nolint:funlen
func toPackages(rels *relationship.Index, catalog *pkg.Collection, sbom sbom.SBOM) (results []*spdx.Package) {
func toPackages(rels *relationship.Index, catalog *pkg.Collection, sbom sbom.SBOM) (results []*spdx.Package, otherLicenses []spdx.OtherLicense) {
otherLicenseSet := helpers.NewSPDXOtherLicenseSet()
for _, p := range catalog.Sorted() {
// name should be guaranteed to be unique, but semantically useful and stable
// name should be guaranteed to be unique but semantically useful and stable
id := toSPDXID(p)
// If the Concluded License is not the same as the Declared License, a written explanation should be provided
// in the Comments on License field (section 7.16). With respect to NOASSERTION, a written explanation in
// the Comments on License field (section 7.16) is preferred.
// extract these correctly to the spdx license format
concluded, declared := helpers.License(p)
concluded, declared, ol := helpers.License(p)
otherLicenseSet.Add(ol...)
// two ways to get filesAnalyzed == true:
// 1. syft has generated a sha1 digest for the package itself - usually in the java cataloger
@@ -475,7 +487,7 @@ func toPackages(rels *relationship.Index, catalog *pkg.Collection, sbom sbom.SBO
PackageAttributionTexts: nil,
})
}
return results
return results, otherLicenseSet.ToSlice()
}
func toPackageChecksums(p pkg.Package) ([]spdx.Checksum, bool) {
@@ -602,14 +614,19 @@ func lookupRelationship(ty artifact.RelationshipType) (bool, helpers.Relationshi
func toFiles(s sbom.SBOM) (results []*spdx.File) {
artifacts := s.Artifacts
for _, coordinates := range s.AllCoordinates() {
_, coordinateSorter := formatInternal.GetLocationSorters(s)
coordinates := s.AllCoordinates()
slices.SortFunc(coordinates, coordinateSorter)
for _, c := range coordinates {
var metadata *file.Metadata
if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists {
if metadataForLocation, exists := artifacts.FileMetadata[c]; exists {
metadata = &metadataForLocation
}
var digests []file.Digest
if digestsForLocation, exists := artifacts.FileDigests[coordinates]; exists {
if digestsForLocation, exists := artifacts.FileDigests[c]; exists {
digests = digestsForLocation
}
@@ -625,18 +642,18 @@ func toFiles(s sbom.SBOM) (results []*spdx.File) {
// TODO: add file classifications (?) and content as a snippet
var comment string
if coordinates.FileSystemID != "" {
comment = fmt.Sprintf("layerID: %s", coordinates.FileSystemID)
if c.FileSystemID != "" {
comment = fmt.Sprintf("layerID: %s", c.FileSystemID)
}
relativePath, err := convertAbsoluteToRelative(coordinates.RealPath)
relativePath, err := convertAbsoluteToRelative(c.RealPath)
if err != nil {
log.Debugf("unable to convert relative path '%s' to absolute path: %s", coordinates.RealPath, err)
relativePath = coordinates.RealPath
log.Debugf("unable to convert relative path '%s' to absolute path: %s", c.RealPath, err)
relativePath = c.RealPath
}
results = append(results, &spdx.File{
FileSPDXIdentifier: toSPDXID(coordinates),
FileSPDXIdentifier: toSPDXID(c),
FileComment: comment,
// required, no attempt made to determine license information
LicenseConcluded: noAssertion,
@@ -723,81 +740,14 @@ func toFileTypes(metadata *file.Metadata) (ty []string) {
return ty
}
// other licenses are for licenses from the pkg.Package that do not have a valid SPDX Expression
// OR are an expression that is a single `License-Ref-*`
func toOtherLicenses(catalog *pkg.Collection) []*spdx.OtherLicense {
licenses := map[string]helpers.SPDXLicense{}
for p := range catalog.Enumerate() {
declaredLicenses, concludedLicenses := helpers.ParseLicenses(p.Licenses.ToSlice())
for _, l := range declaredLicenses {
if l.Value != "" {
licenses[l.ID] = l
}
if l.ID != "" && isLicenseRef(l.ID) {
licenses[l.ID] = l
}
}
for _, l := range concludedLicenses {
if l.Value != "" {
licenses[l.ID] = l
}
if l.ID != "" && isLicenseRef(l.ID) {
licenses[l.ID] = l
}
}
}
var result []*spdx.OtherLicense
var ids []string
for licenseID := range licenses {
ids = append(ids, licenseID)
}
slices.Sort(ids)
for _, id := range ids {
license := licenses[id]
value := license.Value
fullText := license.FullText
// handle cases where LicenseRef needs to be included in hasExtractedLicensingInfos
if license.Value == "" {
value, _ = strings.CutPrefix(license.ID, "LicenseRef-")
}
other := &spdx.OtherLicense{
LicenseIdentifier: license.ID,
}
if fullText != "" {
other.ExtractedText = fullText
} else {
other.ExtractedText = value
}
customPrefix := spdxlicense.LicenseRefPrefix + helpers.SanitizeElementID(internallicenses.UnknownLicensePrefix)
if strings.HasPrefix(license.ID, customPrefix) {
other.LicenseName = strings.TrimPrefix(license.ID, customPrefix)
other.LicenseComment = strings.Trim(internallicenses.UnknownLicensePrefix, "-_")
}
result = append(result, other)
}
return result
}
var licenseRefRegEx = regexp.MustCompile(`^LicenseRef-[A-Za-z0-9_-]+$`)
// isSingularLicenseRef checks if the string is a singular LicenseRef-* identifier
func isLicenseRef(s string) bool {
// Match the input string against the regex
return licenseRefRegEx.MatchString(s)
}
// TODO: handle SPDX excludes file case
// f file is an "excludes" file, skip it /* exclude SPDX analysis file(s) */
// see: https://spdx.github.io/spdx-spec/v2.3/package-information/#79-package-verification-code-field
// the above link contains the SPDX algorithm for a package verification code
func newPackageVerificationCode(rels *relationship.Index, p pkg.Package, sbom sbom.SBOM) *spdx.PackageVerificationCode {
// key off of the contains relationship;
// spdx validator will fail if a package claims to contain a file but no sha1 provided
// if a sha1 for a file is provided then the validator will fail if the package does not have
// key off of the spdx contains relationship;
// spdx validator will fail if a package claims to contain a file, but no sha1 provided
// if a sha1 for a file is provided, then the validator will fail if the package does not have
// a package verification code
coordinates := rels.Coordinates(p, artifact.ContainsRelationship)
var digests []file.Digest
@@ -870,3 +820,15 @@ func convertAbsoluteToRelative(absPath string) (string, error) {
return relPath, nil
}
func convertOtherLicense(otherLicenses []spdx.OtherLicense) []*spdx.OtherLicense {
if len(otherLicenses) == 0 {
return nil
}
result := make([]*spdx.OtherLicense, 0, len(otherLicenses))
for i := range otherLicenses {
result = append(result, &otherLicenses[i])
}
return result
}
@@ -1,8 +1,10 @@
package spdxhelpers
import (
"context"
"fmt"
"regexp"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
@@ -736,7 +738,7 @@ func Test_H1Digest(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
catalog := pkg.NewCollection(test.pkg)
pkgs := toPackages(relationship.NewIndex(), catalog, s)
pkgs, _ := toPackages(relationship.NewIndex(), catalog, s)
require.Len(t, pkgs, 1)
for _, p := range pkgs {
if test.expectedDigest == "" {
@@ -753,29 +755,31 @@ func Test_H1Digest(t *testing.T) {
}
func Test_OtherLicenses(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
pkg pkg.Package
expected []*spdx.OtherLicense
expected []spdx.OtherLicense
}{
{
name: "no licenseRef",
pkg: pkg.Package{
Licenses: pkg.NewLicenseSet(),
},
expected: nil,
expected: []spdx.OtherLicense{},
},
{
name: "single licenseRef",
pkg: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("foobar"),
pkg.NewLicenseWithContext(ctx, "foobar"),
),
},
expected: []*spdx.OtherLicense{
expected: []spdx.OtherLicense{
{
LicenseIdentifier: "LicenseRef-foobar",
ExtractedText: "foobar",
LicenseName: "foobar",
ExtractedText: "NOASSERTION",
},
},
},
@@ -783,18 +787,20 @@ func Test_OtherLicenses(t *testing.T) {
name: "multiple licenseRef",
pkg: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("internal made up license name"),
pkg.NewLicense("new apple license 2.0"),
pkg.NewLicenseWithContext(ctx, "internal made up license name"),
pkg.NewLicenseWithContext(ctx, "new apple license 2.0"),
),
},
expected: []*spdx.OtherLicense{
expected: []spdx.OtherLicense{
{
LicenseIdentifier: "LicenseRef-internal-made-up-license-name",
ExtractedText: "internal made up license name",
ExtractedText: "NOASSERTION",
LicenseName: "internal made up license name",
},
{
LicenseIdentifier: "LicenseRef-new-apple-license-2.0",
ExtractedText: "new apple license 2.0",
ExtractedText: "NOASSERTION",
LicenseName: "new apple license 2.0",
},
},
},
@@ -802,31 +808,27 @@ func Test_OtherLicenses(t *testing.T) {
name: "LicenseRef as a valid spdx expression",
pkg: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("LicenseRef-Fedora-Public-Domain"),
pkg.NewLicenseWithContext(ctx, "LicenseRef-Fedora-Public-Domain"),
),
},
expected: []*spdx.OtherLicense{
{
LicenseIdentifier: "LicenseRef-Fedora-Public-Domain",
ExtractedText: "Fedora-Public-Domain",
},
},
expected: []spdx.OtherLicense{},
},
{
name: "LicenseRef as a valid spdx expression does not otherize compound spdx expressions",
pkg: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("(MIT AND LicenseRef-Fedora-Public-Domain)"),
pkg.NewLicenseWithContext(ctx, "(MIT AND LicenseRef-Fedora-Public-Domain)"),
),
},
expected: nil,
expected: []spdx.OtherLicense{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
catalog := pkg.NewCollection(test.pkg)
otherLicenses := toOtherLicenses(catalog)
rels := relationship.NewIndex()
_, otherLicenses := toPackages(rels, catalog, sbom.SBOM{})
require.Len(t, otherLicenses, len(test.expected))
require.Equal(t, test.expected, otherLicenses)
})
@@ -861,6 +863,34 @@ func Test_toSPDXID(t *testing.T) {
},
expected: "Package-npm-some-package",
},
{
name: "package with existing SPDX ID",
it: func() pkg.Package {
p := pkg.Package{
Type: pkg.NpmPkg,
Name: "some-package",
}
// SPDXRef- prefix is removed on decode (when everything is working as it should)
p.OverrideID("Package-npm-some-package-extra!")
return p
}(),
// note: we still sanitize out the "!" which is not allowed in SPDX IDs
expected: "Package-npm-some-package-extra",
},
{
name: "package with existing SPDX Ref",
it: func() pkg.Package {
p := pkg.Package{
Type: pkg.NpmPkg,
Name: "some-package",
}
// someone incorrectly added SPDXRef- prefix
p.OverrideID("SPDXRef-Package-npm-some-package-extra!")
return p
}(),
// note: we still sanitize out the "!" which is not allowed in SPDX IDs
expected: "Package-npm-some-package-extra",
},
}
for _, test := range tests {
@@ -874,18 +904,19 @@ func Test_toSPDXID(t *testing.T) {
}
func Test_otherLicenses(t *testing.T) {
ctx := context.TODO()
pkg1 := pkg.Package{
Name: "first-pkg",
Version: "1.1",
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT"),
pkg.NewLicenseWithContext(ctx, "MIT"),
),
}
pkg2 := pkg.Package{
Name: "second-pkg",
Version: "2.2",
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("non spdx license"),
pkg.NewLicenseWithContext(ctx, "non spdx license"),
),
}
bigText := `
@@ -895,7 +926,7 @@ func Test_otherLicenses(t *testing.T) {
Name: "third-pkg",
Version: "3.3",
Licenses: pkg.NewLicenseSet(
pkg.NewLicense(bigText),
pkg.NewLicenseWithContext(ctx, bigText),
),
}
@@ -910,22 +941,25 @@ func Test_otherLicenses(t *testing.T) {
expected: nil,
},
{
name: "other licenses includes original text",
name: "other licenses must include some original text",
packages: []pkg.Package{pkg2},
expected: []*spdx.OtherLicense{
{
LicenseIdentifier: "LicenseRef-non-spdx-license",
ExtractedText: "non spdx license",
LicenseName: "non spdx license",
ExtractedText: "NOASSERTION",
},
},
},
{
name: "big licenses get hashed",
name: "big licenses get hashed and space is trimmed",
packages: []pkg.Package{pkg3},
expected: []*spdx.OtherLicense{
{
LicenseIdentifier: "LicenseRef-e9a1e42833d3e456f147052f4d312101bd171a0798893169fe596ca6b55c049e",
ExtractedText: bigText,
LicenseIdentifier: "LicenseRef-3f17782eef51ae86f18fdd6832f5918e2b40f688b52c9adc07ba6ec1024ef408",
// Carries through the syft-json license value when we shasum large texts
LicenseName: "sha256:3f17782eef51ae86f18fdd6832f5918e2b40f688b52c9adc07ba6ec1024ef408",
ExtractedText: strings.TrimSpace(bigText),
},
},
},
@@ -1,6 +1,7 @@
package spdxhelpers
import (
"context"
"errors"
"fmt"
"net/url"
@@ -9,6 +10,7 @@ import (
"strconv"
"strings"
"github.com/scylladb/go-set/strset"
"github.com/spdx/tools-golang/spdx"
"github.com/spdx/tools-golang/spdx/v2/common"
@@ -18,6 +20,7 @@ import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format/internal"
"github.com/anchore/syft/syft/format/internal/spdxutil/helpers"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/linux"
@@ -43,7 +46,7 @@ func ToSyftModel(doc *spdx.Document) (*sbom.SBOM, error) {
},
}
collectSyftPackages(s, spdxIDMap, doc.Packages)
collectSyftPackages(s, spdxIDMap, doc)
collectSyftFiles(s, spdxIDMap, doc)
@@ -277,8 +280,12 @@ func findLinuxReleaseByPURL(doc *spdx.Document) *linux.Release {
return nil
}
func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]any, packages []*spdx.Package) {
for _, p := range packages {
func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]any, doc *spdx.Document) {
skipIDs := packageIDsToSkip(doc)
for _, p := range doc.Packages {
if p == nil || skipIDs.Has(string(p.PackageSPDXIdentifier)) {
continue
}
syftPkg := toSyftPackage(p)
spdxIDMap[string(p.PackageSPDXIdentifier)] = syftPkg
s.Artifacts.Packages.Add(syftPkg)
@@ -509,7 +516,14 @@ func toSyftPackage(p *spdx.Package) pkg.Package {
Metadata: extractMetadata(p, info),
}
sP.SetID()
internal.Backfill(sP)
if p.PackageSPDXIdentifier != "" {
// always prefer the IDs from the SBOM over derived IDs
sP.OverrideID(artifact.ID(p.PackageSPDXIdentifier))
} else {
sP.SetID()
}
return *sP
}
@@ -527,14 +541,14 @@ func parseSPDXLicenses(p *spdx.Package) []pkg.License {
// concluded
if p.PackageLicenseConcluded != helpers.NOASSERTION && p.PackageLicenseConcluded != helpers.NONE && p.PackageLicenseConcluded != "" {
l := pkg.NewLicense(cleanSPDXID(p.PackageLicenseConcluded))
l := pkg.NewLicenseWithContext(context.TODO(), cleanSPDXID(p.PackageLicenseConcluded))
l.Type = license.Concluded
licenses = append(licenses, l)
}
// declared
if p.PackageLicenseDeclared != helpers.NOASSERTION && p.PackageLicenseDeclared != helpers.NONE && p.PackageLicenseDeclared != "" {
l := pkg.NewLicense(cleanSPDXID(p.PackageLicenseDeclared))
l := pkg.NewLicenseWithContext(context.TODO(), cleanSPDXID(p.PackageLicenseDeclared))
l.Type = license.Declared
licenses = append(licenses, l)
}
@@ -648,3 +662,15 @@ func extractCPEs(p *spdx.Package) (cpes []cpe.CPE) {
}
return cpes
}
// packageIDsToSkip returns a set of packageIDs that should not be imported
func packageIDsToSkip(doc *spdx.Document) *strset.Set {
skipIDs := strset.New()
for i := 0; i < len(doc.Relationships); i++ {
r := doc.Relationships[i]
if r != nil && r.Relationship == spdx.RelationshipGeneratedFrom {
skipIDs.Add(string(r.RefB.ElementRefID))
}
}
return skipIDs
}
@@ -664,7 +664,7 @@ func Test_directPackageFiles(t *testing.T) {
Packages: []*spdx.Package{
{
PackageName: "some-package",
PackageSPDXIdentifier: "1",
PackageSPDXIdentifier: "1", // important!
PackageVersion: "1.0.5",
Files: []*spdx.File{
{
@@ -689,7 +689,7 @@ func Test_directPackageFiles(t *testing.T) {
Name: "some-package",
Version: "1.0.5",
}
p.SetID()
p.OverrideID("1") // the same as the spdxID on the package element
f := file.Location{
LocationData: file.LocationData{
Coordinates: file.Coordinates{
@@ -730,3 +730,66 @@ func Test_directPackageFiles(t *testing.T) {
require.Equal(t, s, got)
}
func Test_useSPDXIdentifierOverDerivedSyftArtifactID(t *testing.T) {
doc := &spdx.Document{
SPDXVersion: "SPDX-2.3",
Packages: []*spdx.Package{
{
PackageName: "some-package",
PackageSPDXIdentifier: "1", // important!
PackageVersion: "1.0.5",
Files: []*spdx.File{
{
FileName: "some-file",
FileSPDXIdentifier: "2",
Checksums: []spdx.Checksum{
{
Algorithm: "SHA1",
Value: "a8d733c64f9123",
},
},
},
},
},
},
}
s, err := ToSyftModel(doc)
assert.Nil(t, err)
assert.NotNil(t, s.Artifacts.Packages.Package("1"))
}
func Test_skipsPackagesWithGeneratedFromRelationship(t *testing.T) {
doc := &spdx.Document{
SPDXVersion: "SPDX-2.3",
Packages: []*spdx.Package{
{
PackageName: "package-1",
PackageSPDXIdentifier: "1",
PackageVersion: "1.0.5",
},
{
PackageName: "package-1-src",
PackageSPDXIdentifier: "1-src",
PackageVersion: "1.0.5-src",
},
},
Relationships: []*spdx.Relationship{
{
Relationship: spdx.RelationshipGeneratedFrom,
RefA: common.DocElementID{ // package 1
ElementRefID: spdx.ElementID("1"),
},
RefB: common.DocElementID{ // generated from package 1-src
ElementRefID: spdx.ElementID("1-src"),
},
},
},
}
s, err := ToSyftModel(doc)
assert.Nil(t, err)
assert.NotNil(t, s.Artifacts.Packages.Package("1"))
assert.Nil(t, s.Artifacts.Packages.Package("1-src"))
}
+4
View File
@@ -40,6 +40,10 @@ func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error)
return nil, "", "", fmt.Errorf("unsupported cyclonedx json document version")
}
_, err = reader.Seek(0, io.SeekStart)
if err != nil {
return nil, id, version, fmt.Errorf("unable to seek to start of CycloneDX JSON SBOM: %+v", err)
}
doc, err := d.decoder.Decode(reader)
if err != nil {
return nil, id, version, fmt.Errorf("unable to decode cyclonedx json document: %w", err)
@@ -2,6 +2,7 @@ package cyclonedxjson
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
@@ -43,6 +44,7 @@ func TestDecoder_Decode(t *testing.T) {
t.Run(test.file, func(t *testing.T) {
reader, err := os.Open(filepath.Join("test-fixtures", test.file))
require.NoError(t, err)
reset := func() { _, err = reader.Seek(0, io.SeekStart); require.NoError(t, err) }
dec := NewFormatDecoder()
@@ -51,6 +53,7 @@ func TestDecoder_Decode(t *testing.T) {
assert.Equal(t, sbom.FormatID(""), formatID)
assert.Equal(t, "", formatVersion)
reset()
_, decodeID, decodeVersion, err := dec.Decode(reader)
require.Error(t, err)
assert.Equal(t, sbom.FormatID(""), decodeID)
@@ -61,6 +64,7 @@ func TestDecoder_Decode(t *testing.T) {
assert.Equal(t, ID, formatID)
assert.NotEmpty(t, formatVersion)
reset()
bom, decodeID, decodeVersion, err := dec.Decode(reader)
require.NotNil(t, bom)
require.NoError(t, err)
+9 -3
View File
@@ -3,6 +3,7 @@ package cyclonedxjson
import (
"bytes"
"flag"
"regexp"
"strings"
"testing"
@@ -116,6 +117,14 @@ func TestCycloneDxImageEncoder(t *testing.T) {
func redactor(values ...string) testutil.Redactor {
return testutil.NewRedactions().
WithValuesRedacted(values...).
WithPatternRedactorSpec(
testutil.PatternReplacement{
// only the source component bom-ref (not package or other component bom-refs)
Search: regexp.MustCompile(`"component": \{[^}]*"bom-ref":\s*"(?P<redact>.+)"[^}]*}`),
Groups: []string{"redact"}, // use the regex to anchore the search, but only replace bytes within the capture group
Replace: "redacted",
},
).
WithPatternRedactors(
map[string]string{
// UUIDs
@@ -126,9 +135,6 @@ func redactor(values ...string) testutil.Redactor {
// image hashes
`sha256:[A-Fa-f0-9]{64}`: `sha256:redacted`,
// BOM refs
`"bom-ref":\s*"[^"]+"`: `"bom-ref":"redacted"`,
},
)
}
@@ -17,14 +17,14 @@
]
},
"component": {
"bom-ref":"redacted",
"bom-ref": "redacted",
"type": "file",
"name": "some/path"
}
},
"components": [
{
"bom-ref":"redacted",
"bom-ref": "f04d218ff5ff50db",
"type": "library",
"name": "package-1",
"version": "1.0.1",
@@ -61,7 +61,7 @@
]
},
{
"bom-ref":"redacted",
"bom-ref": "pkg:deb/debian/package-2@2.0.1?package-id=39392bb5e270f669",
"type": "library",
"name": "package-2",
"version": "2.0.1",
@@ -91,7 +91,7 @@
]
},
{
"bom-ref":"redacted",
"bom-ref": "os:debian@1.2.3",
"type": "operating-system",
"name": "debian",
"version": "1.2.3",
@@ -17,7 +17,7 @@
]
},
"component": {
"bom-ref":"redacted",
"bom-ref": "redacted",
"type": "container",
"name": "user-image-input",
"version": "sha256:redacted"
@@ -25,7 +25,7 @@
},
"components": [
{
"bom-ref":"redacted",
"bom-ref": "2f52f617f1548337",
"type": "library",
"name": "package-1",
"version": "1.0.1",
@@ -66,7 +66,7 @@
]
},
{
"bom-ref":"redacted",
"bom-ref": "pkg:deb/debian/package-2@2.0.1?package-id=4b756c6f6fb127a3",
"type": "library",
"name": "package-2",
"version": "2.0.1",
@@ -100,7 +100,7 @@
]
},
{
"bom-ref":"redacted",
"bom-ref": "os:debian@1.2.3",
"type": "operating-system",
"name": "debian",
"version": "1.2.3",
+4
View File
@@ -41,6 +41,10 @@ func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error)
return nil, "", "", fmt.Errorf("unsupported cyclonedx xml document version")
}
_, err = reader.Seek(0, io.SeekStart)
if err != nil {
return nil, id, version, fmt.Errorf("unable to seek to start of CycloneDX XML SBOM: %w", err)
}
doc, err := d.decoder.Decode(reader)
if err != nil {
return nil, id, version, fmt.Errorf("unable to decode cyclonedx xml document: %w", err)
+5
View File
@@ -2,6 +2,7 @@ package cyclonedxxml
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
@@ -44,6 +45,8 @@ func TestDecoder_Decode(t *testing.T) {
reader, err := os.Open(filepath.Join("test-fixtures", test.file))
require.NoError(t, err)
reset := func() { _, err = reader.Seek(0, io.SeekStart); require.NoError(t, err) }
dec := NewFormatDecoder()
formatID, formatVersion := dec.Identify(reader)
@@ -51,6 +54,7 @@ func TestDecoder_Decode(t *testing.T) {
assert.Equal(t, sbom.FormatID(""), formatID)
assert.Equal(t, "", formatVersion)
reset()
_, decodeID, decodeVersion, err := dec.Decode(reader)
require.Error(t, err)
assert.Equal(t, sbom.FormatID(""), decodeID)
@@ -61,6 +65,7 @@ func TestDecoder_Decode(t *testing.T) {
assert.Equal(t, ID, formatID)
assert.NotEmpty(t, formatVersion)
reset()
bom, decodeID, decodeVersion, err := dec.Decode(reader)
require.NotNil(t, bom)
require.NoError(t, err)
+19 -3
View File
@@ -90,16 +90,32 @@ func TestCycloneDxImageEncoder(t *testing.T) {
func redactor(values ...string) testutil.Redactor {
return testutil.NewRedactions().
WithValuesRedacted(values...).
WithPatternRedactorSpec(
testutil.PatternReplacement{
// only the source component bom-ref (not package or other component bom-refs)
Search: regexp.MustCompile(`<component bom-ref="(?P<redact>[^"]*)" type="file">`),
Groups: []string{"redact"}, // use the regex to anchor the search, but only replace bytes within the capture group
Replace: "redacted",
},
).
WithPatternRedactorSpec(
testutil.PatternReplacement{
// only the source component bom-ref (not package or other component bom-refs)
Search: regexp.MustCompile(`<component bom-ref="(?P<redact>[^"]*)" type="container">`),
Groups: []string{"redact"}, // use the regex to anchor the search, but only replace bytes within the capture group
Replace: "redacted",
},
).
WithPatternRedactors(
map[string]string{
// dates
`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`: `redacted`,
// image hashes and BOM refs
// image hashes
`sha256:[A-Za-z0-9]{64}`: `sha256:redacted`,
// serial numbers and BOM refs
`(serialNumber|bom-ref)="[^"]+"`: `$1="redacted"`,
// serial numbers
`(serialNumber)="[^"]+"`: `$1="redacted"`,
},
)
}
@@ -16,7 +16,7 @@
</component>
</metadata>
<components>
<component bom-ref="redacted" type="library">
<component bom-ref="f04d218ff5ff50db" type="library">
<name>package-1</name>
<version>1.0.1</version>
<licenses>
@@ -34,7 +34,7 @@
<property name="syft:location:0:path">/some/path/pkg1</property>
</properties>
</component>
<component bom-ref="redacted" type="library">
<component bom-ref="pkg:deb/debian/package-2@2.0.1?package-id=39392bb5e270f669" type="library">
<name>package-2</name>
<version>2.0.1</version>
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
@@ -47,7 +47,7 @@
<property name="syft:metadata:installedSize">0</property>
</properties>
</component>
<component bom-ref="redacted" type="operating-system">
<component bom-ref="os:debian@1.2.3" type="operating-system">
<name>debian</name>
<version>1.2.3</version>
<description>debian</description>
@@ -11,13 +11,13 @@
</component>
</components>
</tools>
<component bom-ref="redacted" type="container">
<component bom-ref="f28a4ba3ddfdddad" type="container">
<name>user-image-input</name>
<version>sha256:redacted</version>
</component>
</metadata>
<components>
<component bom-ref="redacted" type="library">
<component bom-ref="2f52f617f1548337" type="library">
<name>package-1</name>
<version>1.0.1</version>
<licenses>
@@ -36,7 +36,7 @@
<property name="syft:location:0:path">/somefile-1.txt</property>
</properties>
</component>
<component bom-ref="redacted" type="library">
<component bom-ref="pkg:deb/debian/package-2@2.0.1?package-id=4b756c6f6fb127a3" type="library">
<name>package-2</name>
<version>2.0.1</version>
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
@@ -50,7 +50,7 @@
<property name="syft:metadata:installedSize">0</property>
</properties>
</component>
<component bom-ref="redacted" type="operating-system">
<component bom-ref="os:debian@1.2.3" type="operating-system">
<name>debian</name>
<version>1.2.3</version>
<description>debian</description>
+2
View File
@@ -5,6 +5,7 @@ import (
"github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/format/cyclonedxxml"
"github.com/anchore/syft/syft/format/purls"
"github.com/anchore/syft/syft/format/spdxjson"
"github.com/anchore/syft/syft/format/spdxtagvalue"
"github.com/anchore/syft/syft/format/syftjson"
@@ -24,6 +25,7 @@ func Decoders() []sbom.FormatDecoder {
cyclonedxjson.NewFormatDecoder(),
spdxtagvalue.NewFormatDecoder(),
spdxjson.NewFormatDecoder(),
purls.NewFormatDecoder(),
}
}
+12
View File
@@ -34,6 +34,10 @@ func (c *DecoderCollection) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, stri
var bestID sbom.FormatID
for _, d := range c.decoders {
_, err = reader.Seek(0, io.SeekStart)
if err != nil {
return nil, "", "", fmt.Errorf("unable to seek to start of SBOM: %w", err)
}
id, version := d.Identify(reader)
if id == "" || version == "" {
if id != "" {
@@ -42,6 +46,10 @@ func (c *DecoderCollection) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, stri
continue
}
_, err = reader.Seek(0, io.SeekStart)
if err != nil {
return nil, "", "", fmt.Errorf("unable to seek to start of SBOM: %w", err)
}
return d.Decode(reader)
}
@@ -65,6 +73,10 @@ func (c *DecoderCollection) Identify(r io.Reader) (sbom.FormatID, string) {
}
for _, d := range c.decoders {
_, err = reader.Seek(0, io.SeekStart)
if err != nil {
log.Debugf("unable to seek to start of SBOM: %v", err)
}
id, version := d.Identify(reader)
if id != "" && version != "" {
return id, version
+2
View File
@@ -8,6 +8,7 @@ import (
"github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/format/cyclonedxxml"
"github.com/anchore/syft/syft/format/github"
"github.com/anchore/syft/syft/format/purls"
"github.com/anchore/syft/syft/format/spdxjson"
"github.com/anchore/syft/syft/format/spdxtagvalue"
"github.com/anchore/syft/syft/format/syftjson"
@@ -62,6 +63,7 @@ func (o EncodersConfig) Encoders() ([]sbom.FormatEncoder, error) {
l.addWithErr(syftjson.ID)(o.syftJSONEncoders())
l.add(table.ID)(table.NewFormatEncoder())
l.add(text.ID)(text.NewFormatEncoder())
l.add(purls.ID)(purls.NewFormatEncoder())
l.add(github.ID)(github.NewFormatEncoder())
l.addWithErr(cyclonedxxml.ID)(o.cyclonedxXMLEncoders())
l.addWithErr(cyclonedxjson.ID)(o.cyclonedxJSONEncoders())
+2
View File
@@ -37,6 +37,7 @@ func expectedDefaultEncoders() *strset.Set {
expected.Add("syft-table@") // no version
expected.Add("syft-text@") // no version
expected.Add("github-json@") // no version
expected.Add("purls@") // no version
for _, v := range spdxjson.SupportedVersions() {
expected.Add("spdx-json@" + v)
}
@@ -108,6 +109,7 @@ func TestEncodersConfig_Encoders(t *testing.T) {
expected.Add("syft-table@") // no version
expected.Add("syft-text@") // no version
expected.Add("github-json@") // no version
expected.Add("purls@") // no version
expected.Add("spdx-json@" + spdxutil.DefaultVersion)
expected.Add("spdx-tag-value@" + spdxutil.DefaultVersion)
expected.Add("cyclonedx-json@" + cyclonedxutil.DefaultVersion)
+120
View File
@@ -0,0 +1,120 @@
package internal
import (
"fmt"
"regexp"
"slices"
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/pkg"
)
// Backfill takes all information present in the package and attempts to fill in any missing information
// from any available sources, such as the Metadata and PURL.
//
// Backfill does not call p.SetID(), but this needs to be called later to ensure it's up to date
func Backfill(p *pkg.Package) {
if p.PURL == "" {
return
}
purl, err := packageurl.FromString(p.PURL)
if err != nil {
log.Debugf("unable to parse purl: %s: %w", p.PURL, err)
return
}
var cpes []cpe.CPE
epoch := ""
for _, qualifier := range purl.Qualifiers {
switch qualifier.Key {
case pkg.PURLQualifierCPES:
rawCpes := strings.Split(qualifier.Value, ",")
for _, rawCpe := range rawCpes {
c, err := cpe.New(rawCpe, cpe.DeclaredSource)
if err != nil {
log.Debugf("unable to decode cpe %s in purl %s: %w", rawCpe, p.PURL, err)
continue
}
cpes = append(cpes, c)
}
case pkg.PURLQualifierEpoch:
epoch = qualifier.Value
}
}
if p.Type == "" {
p.Type = pkg.TypeFromPURL(p.PURL)
}
if p.Language == "" {
p.Language = pkg.LanguageFromPURL(p.PURL)
}
if p.Name == "" {
p.Name = nameFromPurl(purl)
}
setVersionFromPurl(p, purl, epoch)
if p.Language == pkg.Java {
setJavaMetadataFromPurl(p, purl)
}
for _, c := range cpes {
if slices.Contains(p.CPEs, c) {
continue
}
p.CPEs = append(p.CPEs, c)
}
}
func setJavaMetadataFromPurl(p *pkg.Package, _ packageurl.PackageURL) {
if p.Type != pkg.JavaPkg {
return
}
if p.Metadata == nil {
// since we don't know if the purl elements directly came from pom properties or the manifest,
// we can only go as far as to set the type to JavaArchive, but not fill in the group id and artifact id
p.Metadata = pkg.JavaArchive{}
}
}
func setVersionFromPurl(p *pkg.Package, purl packageurl.PackageURL, epoch string) {
if p.Version == "" {
p.Version = purl.Version
}
if epoch != "" && p.Type == pkg.RpmPkg && !epochPrefix.MatchString(p.Version) {
p.Version = fmt.Sprintf("%s:%s", epoch, p.Version)
}
}
var epochPrefix = regexp.MustCompile(`^\d+:`)
// nameFromPurl returns the syft package name of the package from the purl. If the purl includes a namespace,
// the name is prefixed as appropriate based on the PURL type
func nameFromPurl(purl packageurl.PackageURL) string {
if !nameExcludesPurlNamespace(purl.Type) && purl.Namespace != "" {
return fmt.Sprintf("%s/%s", purl.Namespace, purl.Name)
}
return purl.Name
}
func nameExcludesPurlNamespace(purlType string) bool {
switch purlType {
case packageurl.TypeAlpine,
packageurl.TypeAlpm,
packageurl.TypeConan,
packageurl.TypeCpan,
packageurl.TypeDebian,
packageurl.TypeMaven,
packageurl.TypeQpkg,
packageurl.TypeRPM,
packageurl.TypeSWID:
return true
}
return false
}
+173
View File
@@ -0,0 +1,173 @@
package internal
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/pkg"
)
func Test_Backfill(t *testing.T) {
tests := []struct {
name string
in pkg.Package
expected pkg.Package
}{
{
name: "npm type",
in: pkg.Package{
PURL: "pkg:npm/test@3.0.0",
},
expected: pkg.Package{
PURL: "pkg:npm/test@3.0.0",
Type: pkg.NpmPkg,
Language: pkg.JavaScript,
Name: "test",
Version: "3.0.0",
},
},
{
name: "rpm no epoch",
in: pkg.Package{
PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&upstream=dbus-1.12.8-26.el8.src.rpm",
},
expected: pkg.Package{
PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&upstream=dbus-1.12.8-26.el8.src.rpm",
Type: pkg.RpmPkg,
Name: "dbus-common",
Version: "1.12.8-26.el8",
},
},
{
name: "rpm epoch",
in: pkg.Package{
PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm",
},
expected: pkg.Package{
PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm",
Type: pkg.RpmPkg,
Name: "dbus-common",
Version: "1:1.12.8-26.el8",
},
},
{
name: "bad cpe",
in: pkg.Package{
PURL: "pkg:npm/testp@3.0.0?cpes=cpe:2.3a:testv:testp:3.0.0:*:*:*:*:*:*:*",
},
expected: pkg.Package{
PURL: "pkg:npm/testp@3.0.0?cpes=cpe:2.3a:testv:testp:3.0.0:*:*:*:*:*:*:*",
Type: pkg.NpmPkg,
Language: pkg.JavaScript,
Name: "testp",
Version: "3.0.0",
},
},
{
name: "good cpe",
in: pkg.Package{
PURL: "pkg:npm/testp@3.0.0?cpes=cpe:2.3:a:testv:testp:3.0.0:*:*:*:*:*:*:*",
},
expected: pkg.Package{
PURL: "pkg:npm/testp@3.0.0?cpes=cpe:2.3:a:testv:testp:3.0.0:*:*:*:*:*:*:*",
Type: pkg.NpmPkg,
Language: pkg.JavaScript,
Name: "testp",
Version: "3.0.0",
CPEs: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "testv",
Product: "testp",
Version: "3.0.0",
},
Source: cpe.DeclaredSource,
},
},
},
},
{
name: "java type",
in: pkg.Package{
PURL: "pkg:maven/org.apache/some-thing@1.2.3",
},
expected: pkg.Package{
PURL: "pkg:maven/org.apache/some-thing@1.2.3",
Type: pkg.JavaPkg,
Language: pkg.Java,
Name: "some-thing",
Version: "1.2.3",
// we intentionally don't claim we found a pom properties file with a groupID from the purl.
// but we do claim that we found java data with an empty type.
Metadata: pkg.JavaArchive{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Backfill(&tt.in)
tt.in.OverrideID("")
require.Equal(t, tt.expected, tt.in)
})
}
}
func Test_nameFromPurl(t *testing.T) {
tests := []struct {
in string
expected string
}{
{
in: "pkg:npm/some-name@3.0.0",
expected: "some-name",
},
{
in: "pkg:maven/org.apache/some-name@1.2.3",
expected: "some-name",
},
{
in: "pkg:deb/debian/some-name@3.0.0",
expected: "some-name",
},
{
in: "pkg:rpm/redhat/some-name@3.0.0",
expected: "some-name",
},
{
in: "pkg:gem/some-name@3.0.0",
expected: "some-name",
},
{
in: "pkg:apk/alpine/some-name@3.0.0",
expected: "some-name",
},
{
in: "pkg:docker/some-org/some-name@3.0.0",
expected: "some-org/some-name",
},
{
in: "pkg:npm/some-name@3.0.0",
expected: "some-name",
},
{
in: "pkg:npm/some-org/some-name@3.0.0",
expected: "some-org/some-name",
},
{
in: "pkg:oci/library/mysql@8.1.0",
expected: "library/mysql",
},
}
for _, tt := range tests {
t.Run(tt.in, func(t *testing.T) {
p, err := packageurl.FromString(tt.in)
require.NoError(t, err)
got := nameFromPurl(p)
require.Equal(t, tt.expected, got)
})
}
}
+1 -13
View File
@@ -1,12 +1,9 @@
package cyclonedxutil
import (
"fmt"
"io"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/syft/format/internal/stream"
)
type Decoder struct {
@@ -20,19 +17,10 @@ func NewDecoder(format cyclonedx.BOMFileFormat) Decoder {
}
func (d Decoder) Decode(r io.Reader) (*cyclonedx.BOM, error) {
reader, err := stream.SeekableReader(r)
if err != nil {
return nil, err
}
doc := &cyclonedx.BOM{
Components: &[]cyclonedx.Component{},
}
if _, err := reader.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("unable to seek to start of CycloneDX SBOM: %w", err)
}
err = cyclonedx.NewBOMDecoder(reader, d.format).Decode(doc)
err := cyclonedx.NewBOMDecoder(r, d.format).Decode(doc)
if err != nil {
return nil, err
}
@@ -1,17 +1,20 @@
package helpers
import (
"fmt"
"reflect"
"strings"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format/internal"
"github.com/anchore/syft/syft/internal/packagemetadata"
"github.com/anchore/syft/syft/pkg"
)
func EncodeComponent(p pkg.Package) cyclonedx.Component {
func EncodeComponent(p pkg.Package, locationSorter func(a, b file.Location) int) cyclonedx.Component {
props := EncodeProperties(p, "syft:package")
if p.Metadata != nil {
@@ -23,7 +26,7 @@ func EncodeComponent(p pkg.Package) cyclonedx.Component {
}
props = append(props, encodeCPEs(p)...)
locations := p.Locations.ToSlice()
locations := p.Locations.ToSlice(locationSorter)
if len(locations) > 0 {
props = append(props, EncodeProperties(locations, "syft:location")...)
}
@@ -84,31 +87,106 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package {
}
p := &pkg.Package{
Name: c.Name,
Version: c.Version,
Locations: decodeLocations(values),
Licenses: pkg.NewLicenseSet(decodeLicenses(c)...),
CPEs: decodeCPEs(c),
PURL: c.PackageURL,
}
// note: this may write in syft package type information
DecodeInto(p, values, "syft:package", CycloneDXFields)
metadataType := values["syft:package:metadataType"]
p.Metadata = decodePackageMetadata(values, c, metadataType)
// this will either use the purl from the component or generate a new one based off of any type information
// that was decoded above.
p.PURL = getPURL(c, p.Type)
if p.Type == "" {
p.Type = pkg.TypeFromPURL(p.PURL)
}
if p.Language == "" {
p.Language = pkg.LanguageFromPURL(p.PURL)
}
setPackageName(p, c)
internal.Backfill(p)
p.SetID()
return p
}
func getPURL(c *cyclonedx.Component, ty pkg.Type) string {
if c.PackageURL != "" {
// if there is a purl that where the namespace does not match the group information, we may
// accidentally drop group. We should consider adding group as a top-level syft package field.
return c.PackageURL
}
if strings.HasPrefix(c.BOMRef, "pkg:") {
// the bomref is a purl, so try to use that as the purl
_, err := packageurl.FromString(c.BOMRef)
if err == nil {
return c.BOMRef
}
}
if ty == "" {
return ""
}
tyStr := ty.PackageURLType()
switch tyStr {
case "", packageurl.TypeGeneric:
return ""
}
purl := packageurl.PackageURL{
Type: tyStr,
Namespace: c.Group,
Name: c.Name,
Version: c.Version,
}
return purl.ToString()
}
func setPackageName(p *pkg.Package, c *cyclonedx.Component) {
name := c.Name
if c.Group != "" {
switch p.Type {
case pkg.JavaPkg:
if p.Metadata == nil {
p.Metadata = pkg.JavaArchive{}
}
var pomProperties *pkg.JavaPomProperties
javaMetadata, ok := p.Metadata.(pkg.JavaArchive)
if ok {
pomProperties = javaMetadata.PomProperties
if pomProperties == nil {
pomProperties = &pkg.JavaPomProperties{}
javaMetadata.PomProperties = pomProperties
p.Metadata = javaMetadata
}
}
if pomProperties != nil {
if pomProperties.ArtifactID == "" {
pomProperties.ArtifactID = c.Name
}
if pomProperties.GroupID == "" {
pomProperties.GroupID = c.Group
}
if pomProperties.Version == "" {
pomProperties.Version = p.Version
}
}
default:
name = fmt.Sprintf("%s/%s", c.Group, name)
}
}
p.Name = name
}
func decodeLocations(vals map[string]string) file.LocationSet {
v := Decode(reflect.TypeOf([]file.Location{}), vals, "syft:location", CycloneDXFields)
out, ok := v.([]file.Location)
@@ -152,7 +152,7 @@ func Test_encodeComponentProperties(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := EncodeComponent(test.input)
c := EncodeComponent(test.input, file.LocationSorter(nil))
if test.expected == nil {
if c.Properties != nil {
t.Fatalf("expected no properties, got: %+v", *c.Properties)
@@ -212,7 +212,7 @@ func Test_encodeCompomentType(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.pkg.ID()
p := EncodeComponent(tt.pkg)
p := EncodeComponent(tt.pkg, file.LocationSorter(nil))
assert.Equal(t, tt.want, p)
})
}
@@ -275,6 +275,7 @@ func Test_decodeComponent(t *testing.T) {
component cyclonedx.Component
wantLanguage pkg.Language
wantMetadata any
wantPURL string
}{
{
name: "derive language from pURL if missing",
@@ -286,6 +287,18 @@ func Test_decodeComponent(t *testing.T) {
BOMRef: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
},
wantLanguage: pkg.Java,
wantPURL: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
},
{
name: "derive language from bomref if missing",
component: cyclonedx.Component{
Name: "ch.qos.logback/logback-classic",
Version: "1.2.3",
Type: "library",
BOMRef: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
},
wantLanguage: pkg.Java,
wantPURL: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
},
{
name: "handle RpmdbMetadata type without properties",
@@ -303,6 +316,7 @@ func Test_decodeComponent(t *testing.T) {
},
},
wantMetadata: pkg.RpmDBEntry{},
wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
},
{
name: "handle RpmdbMetadata type with properties",
@@ -326,6 +340,24 @@ func Test_decodeComponent(t *testing.T) {
wantMetadata: pkg.RpmDBEntry{
Release: "some-release",
},
wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
},
{
name: "generate a purl from package type",
component: cyclonedx.Component{
Name: "log4j",
Group: "org.apache.logging.log4j",
Version: "2.0.4",
Type: "library",
BOMRef: "log4j",
Properties: &[]cyclonedx.Property{
{
Name: "syft:package:type",
Value: "java-archive",
},
},
},
wantPURL: "pkg:maven/org.apache.logging.log4j/log4j@2.0.4",
},
}
@@ -338,9 +370,122 @@ func Test_decodeComponent(t *testing.T) {
if tt.wantMetadata != nil {
assert.Truef(t, reflect.DeepEqual(tt.wantMetadata, p.Metadata), "metadata should match: %+v != %+v", tt.wantMetadata, p.Metadata)
}
if tt.wantMetadata == nil && tt.wantLanguage == "" {
if tt.wantPURL != "" {
assert.Equal(t, tt.wantPURL, p.PURL, "purl should match")
}
if tt.wantMetadata == nil && tt.wantLanguage == "" && tt.wantPURL == "" {
t.Fatal("this is a useless test, please remove it")
}
})
}
}
func TestGetPURL(t *testing.T) {
tests := []struct {
name string
component *cyclonedx.Component
pkgType pkg.Type
expected string
}{
{
name: "component with PackageURL",
component: &cyclonedx.Component{
PackageURL: "pkg:npm/lodash@4.17.21",
Name: "lodash",
Version: "4.17.20", // different version to verify PackageURL is used
Group: "npm",
},
pkgType: pkg.NpmPkg,
expected: "pkg:npm/lodash@4.17.21",
},
{
name: "component with BOMRef as valid PURL",
component: &cyclonedx.Component{
BOMRef: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
Name: "commons-lang3",
Version: "3.11.0", // different version to verify BOMRef is used
Group: "org.apache.commons",
},
pkgType: pkg.JavaPkg,
expected: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
},
{
name: "component with BOMRef not a valid PURL",
component: &cyclonedx.Component{
BOMRef: "not-a-purl-ref",
Name: "commons-lang3",
Version: "3.12.0",
Group: "org.apache.commons",
},
pkgType: pkg.JavaPkg,
expected: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
},
{
name: "component with unknown package type",
component: &cyclonedx.Component{
Name: "some-component",
Version: "1.0.0",
Group: "org.example",
},
pkgType: pkg.UnknownPkg,
expected: "",
},
{
name: "component with empty package type",
component: &cyclonedx.Component{
Name: "some-component",
Version: "1.0.0",
Group: "org.example",
},
pkgType: "",
expected: "",
},
{
name: "component with generic package type",
component: &cyclonedx.Component{
Name: "some-component",
Version: "1.0.0",
Group: "org.example",
},
pkgType: pkg.LinuxKernelModulePkg,
expected: "",
},
{
name: "component with valid package type",
component: &cyclonedx.Component{
Name: "react",
Version: "18.2.0",
Group: "facebook",
},
pkgType: pkg.NpmPkg,
expected: "pkg:npm/facebook/react@18.2.0",
},
{
name: "component with no group",
component: &cyclonedx.Component{
Name: "express",
Version: "4.18.2",
},
pkgType: pkg.NpmPkg,
expected: "pkg:npm/express@4.18.2",
},
{
name: "component with no version",
component: &cyclonedx.Component{
Name: "express",
Group: "npm",
},
pkgType: pkg.NpmPkg,
expected: "pkg:npm/npm/express",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getPURL(tt.component, tt.pkgType)
assert.Equal(t, tt.expected, result)
})
}
}
@@ -5,7 +5,6 @@ import (
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
@@ -66,12 +65,16 @@ func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[str
case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary:
p := decodeComponent(component)
idMap[component.BOMRef] = p
syftID := extractSyftPacakgeID(component.BOMRef)
if component.BOMRef != "" {
// always prefer the IDs from the SBOM over derived IDs
p.OverrideID(artifact.ID(component.BOMRef))
} else {
p.SetID()
}
syftID := p.ID()
if syftID != "" {
idMap[syftID] = p
idMap[string(syftID)] = p
}
// TODO there must be a better way than needing to call this manually:
p.SetID()
s.Artifacts.Packages.Add(*p)
}
@@ -82,19 +85,6 @@ func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[str
}
}
func extractSyftPacakgeID(i string) string {
instance, err := packageurl.FromString(i)
if err != nil {
return ""
}
for _, q := range instance.Qualifiers {
if q.Key == "package-id" {
return q.Value
}
}
return ""
}
func linuxReleaseFromComponents(components []cyclonedx.Component) *linux.Release {
for i := range components {
component := &components[i]
@@ -421,3 +421,39 @@ func Test_decodeDependencies(t *testing.T) {
})
}
}
func Test_useBomRefOverDerivedSyftArtifactID(t *testing.T) {
packageWithId := cyclonedx.Component{
BOMRef: "pkg:maven/org.springframework.boot/spring-boot-starter-test?package-id=646a5a71a4abeee0",
Type: cyclonedx.ComponentTypeLibrary,
Name: "spring-boot-starter-test",
Version: "",
PackageURL: "pkg:maven/org.springframework.boot/spring-boot-starter-test",
}
packageWithoutId := cyclonedx.Component{
BOMRef: "pkg:maven/org.springframework.boot/spring-boot-starter-webflux",
Type: cyclonedx.ComponentTypeLibrary,
Name: "spring-boot-starter-webflux",
Version: "",
PackageURL: "pkg:maven/org.springframework.boot/spring-boot-starter-webflux",
}
bom := cyclonedx.BOM{Metadata: nil,
Components: &[]cyclonedx.Component{
packageWithId,
packageWithoutId,
}}
s, err := ToSyftModel(&bom)
assert.Nil(t, err)
assert.NotNil(t, s.Artifacts.Packages.Package("pkg:maven/org.springframework.boot/spring-boot-starter-test?package-id=646a5a71a4abeee0"))
pkgsWithoutID := s.Artifacts.Packages.PackagesByName(packageWithoutId.Name)
assert.Len(t, pkgsWithoutID, 1)
assert.NotEqual(t, "", pkgsWithoutID[0].ID())
}
@@ -1,6 +1,7 @@
package helpers
import (
"context"
"encoding/base64"
"strings"
@@ -58,11 +59,11 @@ func decodeLicenses(c *cyclonedx.Component) []pkg.License {
// these fields are mutually exclusive in the spec
switch {
case l.License != nil && l.License.ID != "":
licenses = append(licenses, pkg.NewLicenseFromURLs(l.License.ID, l.License.URL))
licenses = append(licenses, pkg.NewLicenseFromURLsWithContext(context.TODO(), l.License.ID, l.License.URL))
case l.License != nil && l.License.Name != "":
licenses = append(licenses, pkg.NewLicenseFromURLs(l.License.Name, l.License.URL))
licenses = append(licenses, pkg.NewLicenseFromURLsWithContext(context.TODO(), l.License.Name, l.License.URL))
case l.Expression != "":
licenses = append(licenses, pkg.NewLicense(l.Expression))
licenses = append(licenses, pkg.NewLicenseWithContext(context.TODO(), l.Expression))
default:
}
}
@@ -1,6 +1,7 @@
package helpers
import (
"context"
"testing"
"github.com/CycloneDX/cyclonedx-go"
@@ -12,6 +13,7 @@ import (
)
func Test_encodeLicense(t *testing.T) {
ctx := context.TODO()
tests := []struct {
name string
input pkg.Package
@@ -25,7 +27,7 @@ func Test_encodeLicense(t *testing.T) {
name: "no SPDX licenses",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("RandomLicense"),
pkg.NewLicenseWithContext(ctx, "RandomLicense"),
),
},
expected: &cyclonedx.Licenses{
@@ -40,8 +42,8 @@ func Test_encodeLicense(t *testing.T) {
name: "single SPDX ID and Non SPDX ID",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("mit"),
pkg.NewLicense("FOOBAR"),
pkg.NewLicenseWithContext(ctx, "mit"),
pkg.NewLicenseWithContext(ctx, "FOOBAR"),
),
},
expected: &cyclonedx.Licenses{
@@ -61,7 +63,7 @@ func Test_encodeLicense(t *testing.T) {
name: "with complex SPDX license expression",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT AND GPL-3.0-only"),
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"),
),
},
expected: &cyclonedx.Licenses{
@@ -74,8 +76,8 @@ func Test_encodeLicense(t *testing.T) {
name: "with multiple complex SPDX license expression",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT AND GPL-3.0-only"),
pkg.NewLicense("MIT AND GPL-3.0-only WITH Classpath-exception-2.0"),
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"),
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only WITH Classpath-exception-2.0"),
),
},
expected: &cyclonedx.Licenses{
@@ -88,9 +90,9 @@ func Test_encodeLicense(t *testing.T) {
name: "with multiple URLs and expressions",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromURLs("MIT", "https://opensource.org/licenses/MIT", "https://spdx.org/licenses/MIT.html"),
pkg.NewLicense("MIT AND GPL-3.0-only"),
pkg.NewLicenseFromURLs("FakeLicense", "htts://someurl.com"),
pkg.NewLicenseFromURLsWithContext(ctx, "MIT", "https://opensource.org/licenses/MIT", "https://spdx.org/licenses/MIT.html"),
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"),
pkg.NewLicenseFromURLsWithContext(ctx, "FakeLicense", "htts://someurl.com"),
),
},
expected: &cyclonedx.Licenses{
@@ -123,8 +125,8 @@ func Test_encodeLicense(t *testing.T) {
name: "with multiple values licenses are deduplicated",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("Apache-2"),
pkg.NewLicense("Apache-2.0"),
pkg.NewLicenseWithContext(ctx, "Apache-2"),
pkg.NewLicenseWithContext(ctx, "Apache-2.0"),
),
},
expected: &cyclonedx.Licenses{
@@ -139,9 +141,9 @@ func Test_encodeLicense(t *testing.T) {
name: "with multiple URLs and single with no URLs",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT"),
pkg.NewLicenseFromURLs("MIT", "https://opensource.org/licenses/MIT", "https://spdx.org/licenses/MIT.html"),
pkg.NewLicense("MIT AND GPL-3.0-only"),
pkg.NewLicenseWithContext(ctx, "MIT"),
pkg.NewLicenseFromURLsWithContext(ctx, "MIT", "https://opensource.org/licenses/MIT", "https://spdx.org/licenses/MIT.html"),
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"),
),
},
expected: &cyclonedx.Licenses{
@@ -167,7 +169,7 @@ func Test_encodeLicense(t *testing.T) {
{
name: "single parenthesized SPDX expression",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValues("(MIT OR Apache-2.0)")...),
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, "(MIT OR Apache-2.0)")...),
},
expected: &cyclonedx.Licenses{
{
@@ -179,7 +181,7 @@ func Test_encodeLicense(t *testing.T) {
name: "single license AND to parenthesized SPDX expression",
// (LGPL-3.0-or-later OR GPL-2.0-or-later OR (LGPL-3.0-or-later AND GPL-2.0-or-later)) AND GFDL-1.3-invariants-or-later
input: pkg.Package{
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValues("(LGPL-3.0-or-later OR GPL-2.0-or-later OR (LGPL-3.0-or-later AND GPL-2.0-or-later)) AND GFDL-1.3-invariants-or-later")...),
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, "(LGPL-3.0-or-later OR GPL-2.0-or-later OR (LGPL-3.0-or-later AND GPL-2.0-or-later)) AND GFDL-1.3-invariants-or-later")...),
},
expected: &cyclonedx.Licenses{
{
@@ -248,7 +250,6 @@ func TestDecodeLicenses(t *testing.T) {
Value: "RandomLicense",
// CycloneDX specification doesn't give a field for determining the license type
Type: license.Declared,
URLs: []string{},
},
},
},
@@ -268,7 +269,6 @@ func TestDecodeLicenses(t *testing.T) {
Value: "MIT",
SPDXExpression: "MIT",
Type: license.Declared,
URLs: []string{},
},
},
},
+17
View File
@@ -0,0 +1,17 @@
package internal
import (
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func GetLocationSorters(s sbom.SBOM) (func(a, b file.Location) int, func(a, b file.Coordinates) int) {
var layers []string
if m, ok := s.Source.Metadata.(source.ImageMetadata); ok {
for _, l := range m.Layers {
layers = append(layers, l.Digest)
}
}
return file.LocationSorter(layers), file.CoordinatesSorter(layers)
}
@@ -1,16 +1,17 @@
package helpers
import (
"crypto/sha256"
"fmt"
"sort"
"strings"
"github.com/spdx/tools-golang/spdx"
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg"
)
func License(p pkg.Package) (concluded, declared string) {
func License(p pkg.Package) (concluded, declared string, otherLicenses []spdx.OtherLicense) {
// source: https://spdx.github.io/spdx-spec/v2.3/package-information/#713-concluded-license-field
// The options to populate this field are limited to:
// A valid SPDX License Expression as defined in Annex D;
@@ -21,15 +22,15 @@ func License(p pkg.Package) (concluded, declared string) {
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
if p.Licenses.Empty() {
return NOASSERTION, NOASSERTION
return NOASSERTION, NOASSERTION, nil
}
// take all licenses and assume an AND expression;
// for information about license expressions see:
// https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/
pc, pd := ParseLicenses(p.Licenses.ToSlice())
pc, pd, ol := ParseLicenses(p.Licenses.ToSlice())
return joinLicenses(pc), joinLicenses(pd)
return joinLicenses(pc), joinLicenses(pd), ol
}
func joinLicenses(licenses []SPDXLicense) string {
@@ -58,14 +59,32 @@ func joinLicenses(licenses []SPDXLicense) string {
}
type SPDXLicense struct {
ID string
Value string
FullText string
// Valid SPDX ID OR License Value (should have LicenseRef- prefix and be sanitized)
// OR combination of the above as a valid SPDX License Expression as defined in Annex D.
// https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/
ID string
// If the SPDX license is not on the SPDX License List
LicenseName string
FullText string // 0..1 (Mandatory, one) if there is a License Identifier assigned (LicenseRef).
URLs []string
}
func ParseLicenses(raw []pkg.License) (concluded, declared []SPDXLicense) {
func ParseLicenses(raw []pkg.License) (concluded, declared []SPDXLicense, otherLicenses []spdx.OtherLicense) {
for _, l := range raw {
candidate := createSPDXLicense(l)
// this determines if the candidate falls under https://spdx.github.io/spdx-spec/v2.3/other-licensing-information-detected/#
// of the SPDX spec, where:
// - we should not have a complex SPDX expression
// - if a single license, it should not be a known license (on the SPDX license list)
if l.SPDXExpression == "" && strings.Contains(candidate.ID, spdxlicense.LicenseRefPrefix) {
otherLicenses = append(otherLicenses, spdx.OtherLicense{
LicenseIdentifier: candidate.ID,
ExtractedText: candidate.FullText,
LicenseName: candidate.LicenseName,
LicenseCrossReferences: candidate.URLs,
})
}
switch l.Type {
case license.Concluded:
concluded = append(concluded, candidate)
@@ -74,35 +93,70 @@ func ParseLicenses(raw []pkg.License) (concluded, declared []SPDXLicense) {
}
}
return concluded, declared
return concluded, declared, otherLicenses
}
func createSPDXLicense(l pkg.License) SPDXLicense {
candidate := SPDXLicense{
ID: generateLicenseID(l),
FullText: l.Contents,
// source: https://spdx.github.io/spdx-spec/v2.3/other-licensing-information-detected/#102-extracted-text-field
// we need to populate this field in the spdx document if we have a license ref
// 0..1 (Mandatory, one) if there is a License Identifier assigned (LicenseRef).
ft := NOASSERTION
if l.Contents != "" {
ft = l.Contents
}
if l.SPDXExpression == "" {
candidate.Value = l.Value
return SPDXLicense{
ID: generateLicenseID(l),
LicenseName: l.Value,
FullText: ft,
URLs: l.URLs,
}
return candidate
}
// generateLicenseID generates a license ID for the given license, which is either the license value or the SPDX expression.
func generateLicenseID(l pkg.License) string {
if l.SPDXExpression != "" {
return l.SPDXExpression
}
if l.Value != "" {
return spdxlicense.LicenseRefPrefix + SanitizeElementID(l.Value)
// syft format includes the algo for the sha in the values
// we can strip this and just make LicenseRef-<sum> for spdx consumption
id := strings.ReplaceAll(l.Value, "sha256:", "")
if !strings.HasPrefix(id, "LicenseRef-") {
id = "LicenseRef-" + id
}
return licenseSum(l.Contents)
return SanitizeElementID(id)
}
func licenseSum(s string) string {
if len(s) <= 64 {
return spdxlicense.LicenseRefPrefix + SanitizeElementID(s)
type SPDXOtherLicenseSet struct {
set map[string]spdx.OtherLicense
}
func NewSPDXOtherLicenseSet() *SPDXOtherLicenseSet {
return &SPDXOtherLicenseSet{
set: make(map[string]spdx.OtherLicense),
}
hash := sha256.Sum256([]byte(s))
return fmt.Sprintf("%s%x", spdxlicense.LicenseRefPrefix, hash)
}
func (s *SPDXOtherLicenseSet) Add(licenses ...spdx.OtherLicense) {
for _, l := range licenses {
s.set[l.LicenseIdentifier] = l
}
}
type ByLicenseIdentifier []spdx.OtherLicense
func (o ByLicenseIdentifier) Len() int { return len(o) }
func (o ByLicenseIdentifier) Swap(i, j int) { o[i], o[j] = o[j], o[i] }
func (o ByLicenseIdentifier) Less(i, j int) bool {
return o[i].LicenseIdentifier < o[j].LicenseIdentifier
}
func (s *SPDXOtherLicenseSet) ToSlice() []spdx.OtherLicense {
values := make([]spdx.OtherLicense, 0, len(s.set))
for _, v := range s.set {
values = append(values, v)
}
sort.Sort(ByLicenseIdentifier(values))
return values
}
@@ -1,9 +1,10 @@
package helpers
import (
"strings"
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/internal/spdxlicense"
@@ -11,6 +12,7 @@ import (
)
func Test_License(t *testing.T) {
ctx := context.TODO()
type expected struct {
concluded string
declared string
@@ -31,7 +33,7 @@ func Test_License(t *testing.T) {
{
name: "no SPDX licenses",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(pkg.NewLicense("made-up")),
Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "made-up")),
},
expected: expected{
concluded: "NOASSERTION",
@@ -41,7 +43,7 @@ func Test_License(t *testing.T) {
{
name: "with SPDX license",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")),
Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "MIT")),
},
expected: struct {
concluded string
@@ -55,8 +57,8 @@ func Test_License(t *testing.T) {
name: "with SPDX license expression",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT"),
pkg.NewLicense("GPL-3.0-only"),
pkg.NewLicenseWithContext(ctx, "MIT"),
pkg.NewLicenseWithContext(ctx, "GPL-3.0-only"),
),
},
expected: expected{
@@ -69,9 +71,9 @@ func Test_License(t *testing.T) {
name: "includes valid LicenseRef-",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("one thing first"),
pkg.NewLicense("two things/#$^second"),
pkg.NewLicense("MIT"),
pkg.NewLicenseWithContext(ctx, "one thing first"),
pkg.NewLicenseWithContext(ctx, "two things/#$^second"),
pkg.NewLicenseWithContext(ctx, "MIT"),
),
},
expected: expected{
@@ -84,9 +86,9 @@ func Test_License(t *testing.T) {
name: "join parentheses correctly",
input: pkg.Package{
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("one thing first"),
pkg.NewLicense("MIT AND GPL-3.0-only"),
pkg.NewLicense("MIT OR APACHE-2.0"),
pkg.NewLicenseWithContext(ctx, "one thing first"),
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"),
pkg.NewLicenseWithContext(ctx, "MIT OR APACHE-2.0"),
),
},
expected: expected{
@@ -98,7 +100,7 @@ func Test_License(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c, d := License(test.input)
c, d, _ := License(test.input)
assert.Equal(t, test.expected.concluded, c)
assert.Equal(t, test.expected.declared, d)
})
@@ -123,11 +125,13 @@ func TestGenerateLicenseID(t *testing.T) {
{
name: "Uses value if no SPDX expression",
license: pkg.License{
Value: "MIT",
Value: "my-sweet-custom-license",
},
expected: spdxlicense.LicenseRefPrefix + "MIT",
expected: spdxlicense.LicenseRefPrefix + "my-sweet-custom-license",
},
{
// note: this is an oversight of the SPDX spec. It does NOT allow "+" in the ID even though they are
// significant to the licenses in the expressions below
name: "Long value is sanitized correctly",
license: pkg.License{
Value: "LGPLv2+ and LGPLv2+ with exceptions and GPLv2+ and GPLv2+ with exceptions and BSD and Inner-Net and ISC and Public Domain and GFDL",
@@ -135,13 +139,6 @@ func TestGenerateLicenseID(t *testing.T) {
expected: spdxlicense.LicenseRefPrefix +
"LGPLv2--and-LGPLv2--with-exceptions-and-GPLv2--and-GPLv2--with-exceptions-and-BSD-and-Inner-Net-and-ISC-and-Public-Domain-and-GFDL",
},
{
name: "Uses hash of contents when nothing else is provided",
license: pkg.License{
Contents: "This is a very long custom license text that should be hashed because it's more than 64 characters long.",
},
expected: "", // We'll verify it starts with the correct prefix
},
}
for _, tt := range tests {
@@ -160,34 +157,92 @@ func TestGenerateLicenseID(t *testing.T) {
func Test_joinLicenses(t *testing.T) {
tests := []struct {
name string
args []string
args []SPDXLicense
want string
}{
{
name: "multiple licenses",
args: []string{"MIT", "GPL-3.0-only"},
args: []SPDXLicense{{ID: "MIT"}, {ID: "GPL-3.0-only"}},
want: "MIT AND GPL-3.0-only",
},
{
name: "multiple licenses with complex expressions",
args: []string{"MIT AND Apache", "GPL-3.0-only"},
args: []SPDXLicense{{ID: "MIT AND Apache"}, {ID: "GPL-3.0-only"}},
want: "(MIT AND Apache) AND GPL-3.0-only",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, joinLicenses(toSpdxLicenses(tt.args)), "joinLicenses(%v)", tt.args)
assert.Equalf(t, tt.want, joinLicenses(tt.args), "joinLicenses(%v)", tt.args)
})
}
}
func toSpdxLicenses(ids []string) (licenses []SPDXLicense) {
for _, l := range ids {
license := SPDXLicense{ID: l}
if strings.HasPrefix(l, spdxlicense.LicenseRefPrefix) {
license.Value = l
}
licenses = append(licenses, license)
func TestCreateSPDXLicenseAndGenerateLicenseID(t *testing.T) {
tests := []struct {
name string
input pkg.License
expected SPDXLicense
}{
{
name: "SPDX expression used as ID",
input: pkg.License{
SPDXExpression: "MIT",
Value: "MIT",
Contents: "",
},
expected: SPDXLicense{
ID: "MIT",
LicenseName: "MIT",
FullText: "NOASSERTION",
},
},
{
name: "LicenseRef with contents",
input: pkg.License{
Value: "sha256:123abc",
Contents: "license contents here",
},
expected: SPDXLicense{
ID: "LicenseRef-123abc",
LicenseName: "sha256:123abc",
FullText: "license contents here",
},
},
{
name: "LicenseRef without contents",
input: pkg.License{
Value: "custom-license",
Contents: "",
},
expected: SPDXLicense{
ID: "LicenseRef-custom-license",
LicenseName: "custom-license",
FullText: "NOASSERTION",
},
},
{
name: "URL is passed through",
input: pkg.License{
SPDXExpression: "MIT",
URLs: []string{
"https://example.com/license",
},
},
expected: SPDXLicense{
ID: "MIT",
FullText: "NOASSERTION",
URLs: []string{"https://example.com/license"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
license := createSPDXLicense(tt.input)
if d := cmp.Diff(tt.expected, license); d != "" {
t.Errorf("createSPDXLicense() mismatch (-want +got):\n%s", d)
}
})
}
return licenses
}
@@ -50,6 +50,14 @@ func Originator(p pkg.Package) (typ string, author string) { //nolint: gocyclo,f
case pkg.DotnetPortableExecutableEntry:
typ = orgType
author = metadata.CompanyName
case pkg.PEBinary:
// this is a known common keyword used in version resources
// for more info see: https://learn.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource
val, ok := metadata.VersionResources.Get("CompanyName")
if ok {
typ = orgType
author = val
}
case pkg.DpkgDBEntry:
author = metadata.Maintainer
@@ -127,6 +135,7 @@ func Originator(p pkg.Package) (typ string, author string) { //nolint: gocyclo,f
// it seems that the vast majority of the time the author is an org, not a person
typ = orgType
author = metadata.Author
case pkg.SwiplPackEntry:
author = formatPersonOrOrg(metadata.Author, metadata.AuthorEmail)
}
@@ -19,6 +19,7 @@ func Test_OriginatorSupplier(t *testing.T) {
pkg.ConanfileEntry{},
pkg.ConaninfoEntry{},
pkg.DartPubspecLockEntry{},
pkg.DartPubspec{},
pkg.DotnetDepsEntry{},
pkg.DotnetPackagesLockEntry{},
pkg.ELFBinaryPackageNoteJSONPayload{},
@@ -26,6 +27,7 @@ func Test_OriginatorSupplier(t *testing.T) {
pkg.ErlangRebarLockEntry{},
pkg.GolangBinaryBuildinfoEntry{},
pkg.GolangModuleEntry{},
pkg.HomebrewFormula{},
pkg.HackageStackYamlLockEntry{},
pkg.HackageStackYamlEntry{},
pkg.LinuxKernel{},
@@ -110,6 +112,21 @@ func Test_OriginatorSupplier(t *testing.T) {
originator: "Organization: Microsoft Corporation",
supplier: "Organization: Microsoft Corporation",
},
{
name: "from PE binary",
input: pkg.Package{
Metadata: pkg.PEBinary{
VersionResources: pkg.KeyValues{
{
Key: "CompanyName",
Value: "Microsoft Corporation",
},
},
},
},
originator: "Organization: Microsoft Corporation",
supplier: "Organization: Microsoft Corporation",
},
{
name: "from dpkg DB",
input: pkg.Package{
@@ -76,6 +76,8 @@ func SourceInfo(p pkg.Package) string {
answer = "acquired package info from GitHub Actions workflow file or composite action file"
case pkg.WordpressPluginPkg:
answer = "acquired package info from found wordpress plugin PHP source files"
case pkg.HomebrewPkg:
answer = "acquired package info from Homebrew formula"
case pkg.TerraformPkg:
answer = "acquired package info from Terraform dependency lock file"
default:
@@ -327,6 +327,14 @@ func Test_SourceInfo(t *testing.T) {
"acquired package info from found wordpress plugin PHP source files",
},
},
{
input: pkg.Package{
Type: pkg.HomebrewPkg,
},
expected: []string{
"acquired package info from Homebrew formula",
},
},
{
input: pkg.Package{
Type: pkg.TerraformPkg,
+56 -1
View File
@@ -6,13 +6,18 @@ import (
"io"
)
// SeekableReader takes an io.Reader and returns an io.ReadSeeker relative to the current position of the reader.
// Users of this function expect to be able to reset the reader to the current position, not potentially reset the
// reader prior to the location when this reader is provided. An example is a reader with multiple JSON
// documents separated by newlines (JSONL). After reading the first document, if a call is made to decode
// the second and Seek(0, SeekStart) is called it would reset the overall reader back to the first document.
func SeekableReader(reader io.Reader) (io.ReadSeeker, error) {
if reader == nil {
return nil, fmt.Errorf("no bytes provided")
}
if r, ok := reader.(io.ReadSeeker); ok {
return r, nil
return getOffsetReadSeeker(r)
}
content, err := io.ReadAll(reader)
@@ -22,3 +27,53 @@ func SeekableReader(reader io.Reader) (io.ReadSeeker, error) {
return bytes.NewReader(content), nil
}
type offsetReadSeeker struct {
rdr io.ReadSeeker
offset int64
}
// getOffsetReadSeeker returns a new io.ReadSeeker that may wrap another io.ReadSeeker with the current offset, so
// seek calls will be relative to the _current_ position, rather than relative to the reader itself
func getOffsetReadSeeker(r io.ReadSeeker) (io.ReadSeeker, error) {
if r == nil {
return nil, fmt.Errorf("no reader provided")
}
pos, err := r.Seek(0, io.SeekCurrent)
if pos == 0 {
// if the ReadSeeker is currently at 0, we don't need to track an offset
return r, nil
}
return &offsetReadSeeker{
rdr: r,
offset: pos,
}, err
}
func (o *offsetReadSeeker) Read(p []byte) (n int, err error) {
return o.rdr.Read(p)
}
func (o *offsetReadSeeker) Seek(offset int64, whence int) (int64, error) {
switch whence {
case io.SeekStart:
if offset < 0 {
return 0, fmt.Errorf("cannot seek < 0")
}
newOffset, err := o.rdr.Seek(o.offset+offset, io.SeekStart)
return newOffset - o.offset, err
case io.SeekCurrent:
currentOffset, err := o.rdr.Seek(0, io.SeekCurrent)
if err != nil {
return 0, fmt.Errorf("cannot seek current: %w", err)
}
if currentOffset-o.offset+offset < 0 {
return 0, fmt.Errorf("cannot seek < 0")
}
newOffset, err := o.rdr.Seek(offset, io.SeekCurrent)
return newOffset - o.offset, err
}
return 0, fmt.Errorf("only SeekStart and SeekCurrent supported")
}
var _ io.ReadSeeker = (*offsetReadSeeker)(nil)
@@ -3,10 +3,9 @@ package stream
import (
"bytes"
"io"
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -26,7 +25,7 @@ func TestSeekableReader(t *testing.T) {
name: "empty reader",
input: bytes.NewBuffer([]byte{}), // does not implement io.Seeker (but does implement io.Reader)
assert: func(input io.Reader, got io.ReadSeeker) {
impl, ok := got.(*bytes.Reader) // implements bytes.Reader
impl, ok := got.(*bytes.Reader) // contents are copied to a byte slice, accessed via bytes.Reader
require.True(t, ok)
_, err := impl.Seek(0, io.SeekStart)
require.NoError(t, err)
@@ -37,38 +36,28 @@ func TestSeekableReader(t *testing.T) {
},
{
name: "empty read seeker",
input: bytes.NewReader([]byte{}), // implements io.ReadSeeker
input: strings.NewReader(""), // implements io.ReadSeeker, not offset
assert: func(input io.Reader, got io.ReadSeeker) {
impl, ok := got.(*bytes.Reader)
_, ok := got.(*strings.Reader) // same ReadSeeker is returned when not offset
require.True(t, ok)
_, err := impl.Seek(0, io.SeekStart)
_, err := got.Seek(0, io.SeekStart)
require.NoError(t, err)
content, err := io.ReadAll(impl)
content, err := io.ReadAll(got)
require.NoError(t, err)
require.Equal(t, []byte{}, content)
// assert this is the same read seeker (reflect tt.input pointer is the same as the impl pointer
inputImpl, ok := input.(*bytes.Reader)
require.True(t, ok)
assert.Equal(t, reflect.ValueOf(inputImpl).Pointer(), reflect.ValueOf(impl).Pointer())
},
},
{
name: "non-empty read seeker",
input: bytes.NewReader([]byte("hello world!")), // implements io.ReadSeeker
input: strings.NewReader("hello world!"), // implements io.ReadSeeker, not offset
assert: func(input io.Reader, got io.ReadSeeker) {
impl, ok := got.(*bytes.Reader)
_, ok := got.(*strings.Reader) // same ReadSeeker is returned when not offset
require.True(t, ok)
_, err := impl.Seek(0, io.SeekStart)
_, err := got.Seek(0, io.SeekStart)
require.NoError(t, err)
content, err := io.ReadAll(impl)
content, err := io.ReadAll(got)
require.NoError(t, err)
require.Equal(t, []byte("hello world!"), content)
// assert this is the same read seeker (reflect tt.input pointer is the same as the impl pointer
inputImpl, ok := input.(*bytes.Reader)
require.True(t, ok)
assert.Equal(t, reflect.ValueOf(inputImpl).Pointer(), reflect.ValueOf(impl).Pointer())
},
},
{
@@ -84,6 +73,32 @@ func TestSeekableReader(t *testing.T) {
require.Equal(t, []byte("hello world!"), content)
},
},
{
name: "position zero read seeker",
input: strings.NewReader("a string reader"), // implements io.ReadSeeker at position 0
assert: func(input io.Reader, got io.ReadSeeker) {
_, ok := got.(*strings.Reader) // returns the same ReadSeeker
require.True(t, ok)
_, err := got.Seek(0, io.SeekStart)
require.NoError(t, err)
content, err := io.ReadAll(got)
require.NoError(t, err)
require.Equal(t, []byte("a string reader"), content)
},
},
{
name: "offset read seeker",
input: moveOffset(t, bytes.NewReader([]byte{1, 2, 3, 4, 5}), 3), // implements io.ReadSeeker, with an offset
assert: func(input io.Reader, got io.ReadSeeker) {
_, ok := got.(*offsetReadSeeker) // returns an offset-tracking ReadSeeker
require.True(t, ok)
_, err := got.Seek(0, io.SeekStart)
require.NoError(t, err)
content, err := io.ReadAll(got)
require.NoError(t, err)
require.Equal(t, []byte{4, 5}, content)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -99,3 +114,113 @@ func TestSeekableReader(t *testing.T) {
})
}
}
func Test_offsetReadSeeker(t *testing.T) {
abcd1234 := func() io.ReadSeeker { return strings.NewReader("abcd1234") }
abcd1234offset := func(offset int) func() io.ReadSeeker {
return func() io.ReadSeeker {
r := strings.NewReader("abcd1234")
_, err := r.Seek(int64(offset), io.SeekStart)
require.NoError(t, err)
return r
}
}
tests := []struct {
name string
input func() io.ReadSeeker
seek int64
seek2 int64
whence int
expected string
wantErr require.ErrorAssertionFunc
}{
{
name: "basic reader",
input: abcd1234,
seek: 0,
whence: io.SeekStart,
expected: "abcd1234",
},
{
name: "basic reader offset",
input: abcd1234offset(1),
seek: 0,
whence: io.SeekStart,
expected: "bcd1234",
},
{
name: "basic reader offset both",
input: abcd1234offset(2),
seek: 2,
whence: io.SeekStart,
expected: "1234",
},
{
name: "basic reader offset seek current",
input: abcd1234offset(1),
seek: -1,
whence: io.SeekCurrent,
wantErr: require.Error, // would be < current, which is an error
},
{
name: "valid negative offset from current",
input: abcd1234offset(1),
seek: 2,
seek2: -1,
whence: io.SeekCurrent,
expected: "cd1234",
},
{
name: "basic reader offset multiple",
input: abcd1234offset(2),
seek: 3,
seek2: 2,
whence: io.SeekCurrent,
expected: "4",
},
{
name: "bad whence",
input: abcd1234,
seek: 1,
whence: io.SeekEnd,
wantErr: require.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rdr := tt.input()
off, err := rdr.Seek(0, io.SeekCurrent)
require.NoError(t, err)
// construct new offsetReadSeeker
sr := offsetReadSeeker{rdr: rdr, offset: off}
_, err = sr.Seek(tt.seek, tt.whence)
if tt.seek2 != 0 {
require.NoError(t, err)
_, err = sr.Seek(tt.seek2, tt.whence)
}
if tt.wantErr != nil {
tt.wantErr(t, err)
return
} else {
require.NoError(t, err)
}
buf := make([]byte, 1024)
n, err := sr.Read(buf)
require.NoError(t, err)
require.Equal(t, tt.expected, string(buf[:n]))
})
}
}
func moveOffset(t *testing.T, reader io.ReadSeeker, offset int64) io.Reader {
pos, err := reader.Seek(offset, io.SeekStart)
require.NoError(t, err)
require.Equal(t, offset, pos)
return reader
}
@@ -1,6 +1,7 @@
package testutil
import (
"context"
"os"
"path/filepath"
"testing"
@@ -98,7 +99,7 @@ func DirectoryInputWithAuthorField(t testing.TB) sbom.SBOM {
func newDirectoryCatalog() *pkg.Collection {
catalog := pkg.NewCollection()
ctx := context.TODO()
// populate catalog with test data
catalog.Add(pkg.Package{
Name: "package-1",
@@ -110,7 +111,7 @@ func newDirectoryCatalog() *pkg.Collection {
),
Language: pkg.Python,
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT"),
pkg.NewLicenseWithContext(ctx, "MIT"),
),
Metadata: pkg.PythonPackage{
Name: "package-1",
@@ -149,7 +150,7 @@ func newDirectoryCatalog() *pkg.Collection {
func newDirectoryCatalogWithAuthorField() *pkg.Collection {
catalog := pkg.NewCollection()
ctx := context.TODO()
// populate catalog with test data
catalog.Add(pkg.Package{
Name: "package-1",
@@ -161,7 +162,7 @@ func newDirectoryCatalogWithAuthorField() *pkg.Collection {
),
Language: pkg.Python,
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT"),
pkg.NewLicenseWithContext(ctx, "MIT"),
),
Metadata: pkg.PythonPackage{
Name: "package-1",
+3 -2
View File
@@ -1,6 +1,7 @@
package testutil
import (
"context"
"os"
"path/filepath"
"testing"
@@ -98,7 +99,7 @@ func populateImageCatalog(catalog *pkg.Collection, img *image.Image) {
// TODO: this helper function is coupled to the image-simple fixture, which seems like a bad idea
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
ctx := context.TODO()
// populate catalog with test data
if ref1 != nil {
catalog.Add(pkg.Package{
@@ -111,7 +112,7 @@ func populateImageCatalog(catalog *pkg.Collection, img *image.Image) {
FoundBy: "the-cataloger-1",
Language: pkg.Python,
Licenses: pkg.NewLicenseSet(
pkg.NewLicense("MIT"),
pkg.NewLicenseWithContext(ctx, "MIT"),
),
Metadata: pkg.PythonPackage{
Name: "package-1",
+69 -1
View File
@@ -28,6 +28,7 @@ func (r RedactorFn) Redact(b []byte) []byte {
type PatternReplacement struct {
Search *regexp.Regexp
Groups []string
Replace string
}
@@ -39,7 +40,67 @@ func NewPatternReplacement(r *regexp.Regexp) PatternReplacement {
}
func (p PatternReplacement) Redact(b []byte) []byte {
return p.Search.ReplaceAll(b, []byte(p.Replace))
if len(p.Groups) == 0 {
return p.Search.ReplaceAll(b, []byte(p.Replace))
}
return p.redactNamedGroups(b)
}
func (p PatternReplacement) redactNamedGroups(b []byte) []byte {
groupsToReplace := make(map[string]bool)
for _, g := range p.Groups {
groupsToReplace[g] = true
}
subexpNames := p.Search.SubexpNames()
return p.Search.ReplaceAllFunc(b, func(match []byte) []byte {
indexes := p.Search.FindSubmatchIndex(match)
if indexes == nil {
return match
}
result := make([]byte, len(match))
copy(result, match)
// keep track of the offset as we replace groups
offset := 0
// process each named group
for i, name := range subexpNames {
// skip the full match (i==0) and groups we don't want to replace
if i == 0 || !groupsToReplace[name] {
continue
}
// get the start and end positions of this group
startPos := indexes[2*i]
endPos := indexes[2*i+1]
// skip if the group didn't match
if startPos < 0 || endPos < 0 {
continue
}
// adjust positions based on previous replacements
startPos += offset
endPos += offset
// replace the group with our replacement text
beforeGroup := result[:startPos]
afterGroup := result[endPos:]
// calculate the new offset
oldLen := endPos - startPos
newLen := len(p.Replace)
offset += (newLen - oldLen)
result = append(beforeGroup, append([]byte(p.Replace), afterGroup...)...) //nolint:gocritic
}
return result
})
}
// Replace by value //////////////////////////////
@@ -86,6 +147,13 @@ func (r *Redactions) WithPatternRedactors(values map[string]string) *Redactions
return r
}
func (r *Redactions) WithPatternRedactorSpec(values ...PatternReplacement) *Redactions {
for _, v := range values {
r.redactors = append(r.redactors, v)
}
return r
}
func (r *Redactions) WithValueRedactors(values map[string]string) *Redactions {
for k, v := range values {
r.redactors = append(r.redactors,
+93
View File
@@ -0,0 +1,93 @@
package purls
import (
"bufio"
"errors"
"fmt"
"io"
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/format/internal"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
)
var _ sbom.FormatDecoder = (*decoder)(nil)
type decoder struct{}
func NewFormatDecoder() sbom.FormatDecoder {
return decoder{}
}
func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error) {
if r == nil {
return nil, "", "", fmt.Errorf("no reader provided")
}
s, err := toSyftModel(r)
return s, ID, version, err
}
func (d decoder) Identify(r io.Reader) (sbom.FormatID, string) {
if r == nil {
return "", ""
}
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
// skip whitespace only lines
continue
}
if strings.HasPrefix(line, "pkg:") {
_, err := packageurl.FromString(line)
if err != nil {
log.WithFields("error", err, "line", line).Debug("unable to parse purl")
continue
}
return ID, version
}
// not a purl, so we can't identify the format as a list of purls
return "", ""
}
return "", ""
}
func toSyftModel(r io.Reader) (*sbom.SBOM, error) {
var errs []error
pkgs := pkg.NewCollection()
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
// skip invalid PURLs
_, err := packageurl.FromString(line)
if err != nil {
log.WithFields("error", err, "line", line).Debug("unable to parse purl")
continue
}
p := pkg.Package{
// name, version and other properties set during Backfill
PURL: line,
}
internal.Backfill(&p)
p.SetID()
pkgs.Add(p)
}
return &sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkgs,
},
}, errors.Join(errs...)
}
+237
View File
@@ -0,0 +1,237 @@
package purls
import (
"bytes"
"regexp"
"slices"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/cmptest"
"github.com/anchore/syft/syft/pkg"
)
func TestDecoder_Decode(t *testing.T) {
tests := []struct {
purl string
expected []pkg.Package
}{
{
purl: "pkg:generic/some-package@1.2.3",
expected: []pkg.Package{
{
Name: "some-package",
Type: pkg.UnknownPkg,
Version: "1.2.3",
PURL: "pkg:generic/some-package@1.2.3",
},
},
},
{
purl: "pkg:npm/some-package@1.2.3",
expected: []pkg.Package{
{
Name: "some-package",
Type: pkg.NpmPkg,
Language: pkg.JavaScript,
Version: "1.2.3",
PURL: "pkg:npm/some-package@1.2.3",
},
},
},
{
purl: "pkg:apk/curl@7.61.1",
expected: []pkg.Package{
{
Name: "curl",
Version: "7.61.1",
Type: pkg.ApkPkg,
PURL: "pkg:apk/curl@7.61.1",
},
},
},
{
purl: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit",
expected: []pkg.Package{
{
Name: "sysv-rc",
Version: "2.88dsf-59",
Type: pkg.DebPkg,
PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit",
},
},
},
{
purl: "pkg:apk/libcrypto3@3.3.2?upstream=openssl",
expected: []pkg.Package{
{
Name: "libcrypto3",
Version: "3.3.2",
Type: pkg.ApkPkg,
PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl",
},
},
},
{
purl: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1", // %40 is @
expected: []pkg.Package{
{
Name: "libcrypto3",
Version: "3.3.2",
Type: pkg.ApkPkg,
PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1",
},
},
},
{
purl: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm",
expected: []pkg.Package{
{
Name: "systemd-x",
Version: "239-82.el8_10.2",
Type: pkg.RpmPkg,
PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm",
},
},
},
{
purl: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm",
expected: []pkg.Package{
{
Name: "dbus-common",
Version: "1:1.12.8-26.el8",
Type: pkg.RpmPkg,
PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm",
},
},
},
{
purl: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3",
expected: []pkg.Package{
{
Name: "curl",
Version: "7.61.1",
Type: pkg.ApkPkg,
PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3",
},
},
},
{
purl: "pkg:golang/k8s.io/ingress-nginx@v1.11.2",
expected: []pkg.Package{
{
Name: "k8s.io/ingress-nginx",
Version: "v1.11.2",
Type: pkg.GoModulePkg,
Language: pkg.Go,
PURL: "pkg:golang/k8s.io/ingress-nginx@v1.11.2",
},
},
},
{
purl: "pkg:golang/github.com/wazuh/wazuh@v4.5.0",
expected: []pkg.Package{
{
Name: "github.com/wazuh/wazuh",
Version: "v4.5.0",
Type: pkg.GoModulePkg,
PURL: "pkg:golang/github.com/wazuh/wazuh@v4.5.0",
Language: pkg.Go,
},
},
},
{
purl: "pkg:golang/wazuh@v4.5.0",
expected: []pkg.Package{
{
Name: "wazuh",
Version: "v4.5.0",
Type: pkg.GoModulePkg,
PURL: "pkg:golang/wazuh@v4.5.0",
Language: pkg.Go,
},
},
},
{
purl: "pkg:maven/org.apache/some-pkg@4.11.3",
expected: []pkg.Package{
{
Name: "some-pkg",
Version: "4.11.3",
Type: pkg.JavaPkg,
PURL: "pkg:maven/org.apache/some-pkg@4.11.3",
Language: pkg.Java,
// we intentionally do not claim we found a pom properties file (don't derive this from the purl).
// but we need a metadata allocated since all Java packages have a this metadata type (a consistency point)
Metadata: pkg.JavaArchive{},
},
},
},
}
for _, test := range tests {
t.Run(test.purl, func(t *testing.T) {
dec := NewFormatDecoder()
got, _, _, err := dec.Decode(strings.NewReader(test.purl))
require.NoError(t, err)
if diff := cmp.Diff(test.expected, got.Artifacts.Packages.Sorted(), cmptest.DefaultOptions()...); diff != "" {
t.Errorf("expected packages (-want +got):\n%s", diff)
}
})
}
}
func Test_DecodeEncodeCycle(t *testing.T) {
tests := []struct {
name string
input string
}{
{
name: "basic",
input: "pkg:generic/some-package@1.2.3",
},
{
name: "multiple",
input: "pkg:generic/pkg1\npkg:generic/pkg2\n\npkg:npm/@vercel/ncc@2.9.5",
},
{
name: "java",
input: "pkg:maven/org.apache/some-thing@4.11.3",
},
{
name: "leading whitespace",
input: " \n \t pkg:maven/org.apache/some-thing@4.11.3",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dec := NewFormatDecoder()
decodedSBOM, _, _, err := dec.Decode(strings.NewReader(tt.input))
require.NoError(t, err)
var buf bytes.Buffer
enc := NewFormatEncoder()
require.NoError(t, enc.Encode(&buf, *decodedSBOM))
in := strings.TrimSpace(regexp.MustCompile(`\s+`).ReplaceAllString(strings.TrimSpace(tt.input), "\n"))
expected := strings.Split(in, "\n")
slices.Sort(expected)
got := strings.Split(strings.TrimSpace(buf.String()), "\n")
slices.Sort(got)
require.EqualValues(t, expected, got)
for _, item := range got {
// require every result is a valid PURL -- no whitespace lines, etc.
_, err = packageurl.FromString(item)
require.NoError(t, err)
}
})
}
}
+62
View File
@@ -0,0 +1,62 @@
package purls
import (
"io"
"strings"
"github.com/scylladb/go-set/strset"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/sbom"
)
const ID sbom.FormatID = "purls"
const version = "1"
type encoder struct {
}
func NewFormatEncoder() sbom.FormatEncoder {
return encoder{}
}
func (e encoder) ID() sbom.FormatID {
return ID
}
func (e encoder) Aliases() []string {
return []string{
"purl",
}
}
func (e encoder) Version() string {
return sbom.AnyVersion
}
func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error {
output := strset.New()
for _, p := range s.Artifacts.Packages.Sorted() {
purl := strings.TrimSpace(p.PURL)
if purl == "" || output.Has(purl) {
continue
}
// ensure syft doesn't output invalid PURLs in this format
_, err := packageurl.FromString(purl)
if err != nil {
log.Debugf("invalid purl: %q", purl)
continue
}
output.Add(purl)
_, err = writer.Write([]byte(purl))
if err != nil {
return err
}
_, err = writer.Write([]byte("\n"))
if err != nil {
return err
}
}
return nil
}
+60
View File
@@ -0,0 +1,60 @@
package purls
import (
"flag"
"testing"
"github.com/anchore/syft/syft/format/internal/testutil"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
)
var updateSnapshot = flag.Bool("update-purls", false, "update the *.golden files for purls format")
func Test_Encoder(t *testing.T) {
pkgs := []pkg.Package{
{
Name: "npmtest",
Version: "1.5.1",
Type: pkg.NpmPkg,
Language: pkg.JavaScript,
PURL: "pkg:npm/npmtest@1.5.1",
},
{
Name: "npmtest2",
Version: "1.5.1",
Type: pkg.NpmPkg,
Language: pkg.JavaScript,
PURL: "pkg:npm/npmtest@1.5.1", // duplicate should not be included
},
{
Name: "npmtest",
Version: "3.1.1",
Type: pkg.NpmPkg,
Language: pkg.JavaScript,
PURL: "http://npm/npmtest@3.1.1", // invalid PURL should not be included
},
{
Name: "javatest",
Version: "0.30.1",
Type: pkg.JavaPkg,
Language: pkg.Java,
PURL: "pkg:maven/org.apache/javatest@0.30.1",
},
{
Type: pkg.UnknownPkg,
PURL: "pkg:generic/generic@1.2.3",
},
}
testutil.AssertEncoderAgainstGoldenSnapshot(t,
testutil.EncoderSnapshotTestConfig{
Subject: sbom.SBOM{Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(pkgs...),
}},
Format: NewFormatEncoder(),
UpdateSnapshot: *updateSnapshot,
PersistRedactionsInSnapshot: true,
IsJSON: false,
},
)
}
@@ -0,0 +1,3 @@
pkg:generic/generic@1.2.3
pkg:maven/org.apache/javatest@0.30.1
pkg:npm/npmtest@1.5.1
+5
View File
@@ -2,6 +2,7 @@ package spdxjson
import (
"fmt"
"io"
"os"
"path/filepath"
"testing"
@@ -91,6 +92,8 @@ func TestDecoder_Decode(t *testing.T) {
reader, err := os.Open(filepath.Join("test-fixtures", "spdx", test.name))
require.NoError(t, err)
reset := func() { _, err = reader.Seek(0, io.SeekStart); require.NoError(t, err) }
dec := NewFormatDecoder()
formatID, formatVersion := dec.Identify(reader)
@@ -98,6 +101,7 @@ func TestDecoder_Decode(t *testing.T) {
assert.Equal(t, test.id, formatID)
assert.Equal(t, test.version, formatVersion)
reset()
_, decodeID, decodeVersion, err := dec.Decode(reader)
require.Error(t, err)
assert.Equal(t, test.id, decodeID)
@@ -108,6 +112,7 @@ func TestDecoder_Decode(t *testing.T) {
assert.Equal(t, test.id, formatID)
assert.Equal(t, test.version, formatVersion)
reset()
s, decodeID, decodeVersion, err := dec.Decode(reader)
require.NoError(t, err)

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