Compare commits

..
56 Commits
Author SHA1 Message Date
anchore-actions-token-generator[bot]andwagoodman e8b62ab9ac chore(deps): update anchore dependencies (#4047)
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-07-02 16:09:46 +00:00
anchore-actions-token-generator[bot]andwagoodman 2af1bca83f chore(deps): update anchore dependencies (#4045)
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-07-02 11:50:29 -04:00
Carlos Tadeu Panato JuniorandAlex Goodman 2111d4d0e4 chore: upgrade tablewriter dependency to use new API (#3990)
* upgrade tablewriter

Signed-off-by: Carlos Panato <ctadeu@gmail.com>

* remove header line whitespace

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

* fix static analysis

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

---------

Signed-off-by: Carlos Panato <ctadeu@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-07-01 16:16:16 -04:00
dependabot[bot]andAlex Goodman 179cc70a36 chore(deps): bump github.com/Masterminds/semver/v3 from 3.3.1 to 3.4.0 (#4040)
* chore(deps): bump github.com/Masterminds/semver/v3 from 3.3.1 to 3.4.0

Bumps [github.com/Masterminds/semver/v3](https://github.com/Masterminds/semver) from 3.3.1 to 3.4.0.
- [Release notes](https://github.com/Masterminds/semver/releases)
- [Changelog](https://github.com/Masterminds/semver/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Masterminds/semver/compare/v3.3.1...v3.4.0)

---
updated-dependencies:
- dependency-name: github.com/Masterminds/semver/v3
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* update error message expectations

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-07-01 19:21:02 +00:00
Christopher Angelo Phillips 1e3d2a2927 chore: update tests to read from latest test-fixture-cache and fix cache publish (#4042)
* feat: update integration test with correct package for httpd

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

* chore: update integration and cli tests with new upstream expectations

- php interpreter 8.3.21 => 8.3.22
- runCycloneDXInDocker update for local arm64 qemu emulation CycloneDX
- getSyftBinaryLocationByOS update to detect arm64 v8.0 artifact path

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

* chore: add snalshot to test command for fixture builds

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

* chore: update cdx in docker for all GOOS

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

---------

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2025-07-01 14:11:36 +00:00
dependabot[bot] 421afac532 chore(deps): bump github.com/mholt/archives from 0.1.2 to 0.1.3 (#4032)
Bumps [github.com/mholt/archives](https://github.com/mholt/archives) from 0.1.2 to 0.1.3.
- [Release notes](https://github.com/mholt/archives/releases)
- [Commits](https://github.com/mholt/archives/compare/v0.1.2...v0.1.3)

---
updated-dependencies:
- dependency-name: github.com/mholt/archives
  dependency-version: 0.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-30 17:58:41 -04:00
dependabot[bot] b0b10acb40 chore(deps): bump marocchino/sticky-pull-request-comment (#4019)
Bumps [marocchino/sticky-pull-request-comment](https://github.com/marocchino/sticky-pull-request-comment) from 2.9.2 to 2.9.3.
- [Release notes](https://github.com/marocchino/sticky-pull-request-comment/releases)
- [Commits](https://github.com/marocchino/sticky-pull-request-comment/compare/67d0dec7b07ed060a405f9b2a64b8ab319fdd7db...d2ad0de260ae8b0235ce059e63f2949ba9e05943)

---
updated-dependencies:
- dependency-name: marocchino/sticky-pull-request-comment
  dependency-version: 2.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-30 17:58:32 -04:00
dependabot[bot] b90028bd1f chore(deps): bump sigstore/cosign-installer from 3.9.0 to 3.9.1 (#4022)
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.9.0 to 3.9.1.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.9.0...v3.9.1)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-version: 3.9.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-30 17:58:23 -04:00
anchore-actions-token-generator[bot]andspiffcs b5a1b309ca chore(deps): update tools to latest versions (#4035)
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-30 17:17:49 -04:00
anchore-actions-token-generator[bot]andwagoodman 841f963e70 chore(deps): update CPE dictionary index (#4037)
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-30 17:17:34 -04:00
dependabot[bot] ba59f57bfe chore(deps): bump github/codeql-action from 3.29.0 to 3.29.2 (#4039)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.0 to 3.29.2.
- [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/ce28f5bb42b7a9f2c824e633a3f6ee835bab6858...181d5eefc20863364f96762470ba6f862bdef56b)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.29.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-30 17:17:20 -04:00
Alex Goodman 2bda086423 Add ability to scan snaps (as a source) (#3929) 2025-06-25 16:53:35 -04:00
anchore-actions-token-generator[bot]andwagoodman 4eb8ba4575 chore(deps): update CPE dictionary index (#4021)
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-23 11:30:19 -04:00
anchore-actions-token-generator[bot]andspiffcs 49115355d4 chore(deps): update tools to latest versions (#4016)
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-19 14:02:03 -04:00
anchore-actions-token-generator[bot]andspiffcs d9eb1d7c1b chore(deps): update tools to latest versions (#4012)
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-17 11:35:22 -04:00
dependabot[bot] 32a30f76c6 chore(deps): bump github.com/go-viper/mapstructure/v2 (#4014)
Bumps [github.com/go-viper/mapstructure/v2](https://github.com/go-viper/mapstructure) from 2.2.1 to 2.3.0.
- [Release notes](https://github.com/go-viper/mapstructure/releases)
- [Changelog](https://github.com/go-viper/mapstructure/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-viper/mapstructure/compare/v2.2.1...v2.3.0)

---
updated-dependencies:
- dependency-name: github.com/go-viper/mapstructure/v2
  dependency-version: 2.3.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-17 11:35:09 -04:00
dependabot[bot] b52b13c03c chore(deps): bump sigstore/cosign-installer from 3.8.2 to 3.9.0 (#4015)
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.8.2 to 3.9.0.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.8.2...v3.9.0)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-version: 3.9.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-17 11:34:58 -04:00
anchore-actions-token-generator[bot]andwagoodman 0bfda2c514 chore(deps): update CPE dictionary index (#4007)
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-16 11:15:50 -04:00
dependabot[bot] 0b57d03958 chore(deps): bump anchore/sbom-action from 0.20.0 to 0.20.1 (#4008)
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.20.0 to 0.20.1.
- [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/e11c554f704a0b820cbf8c51673f6945e0731532...9246b90769f852b3a8921f330c59e0b3f439d6e9)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-version: 0.20.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 11:15:37 -04:00
dependabot[bot] 72f9c42562 chore(deps): bump github.com/google/go-containerregistry (#4009)
Bumps [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) from 0.20.5 to 0.20.6.
- [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.5...v0.20.6)

---
updated-dependencies:
- dependency-name: github.com/google/go-containerregistry
  dependency-version: 0.20.6
  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-16 11:15:22 -04:00
anchore-actions-token-generator[bot]andspiffcs 181e180284 chore(deps): update tools to latest versions (#3992)
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-13 10:38:44 -04:00
dependabot[bot] c19558dd73 chore(deps): bump github/codeql-action from 3.28.19 to 3.29.0 (#4000)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.19 to 3.29.0.
- [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/fca7ace96b7d713c7035871441bd52efbe39e27e...ce28f5bb42b7a9f2c824e633a3f6ee835bab6858)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.29.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-12 10:37:53 -04:00
Keith Zantow 10f0631710 fix: provide separate nonroot image (#3998)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
2025-06-11 17:00:55 -04:00
Alex Goodman 96c34ffc43 account for non-import shapes (#3997)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-06-11 13:11:40 -04:00
Alex Goodman 79b6d5daa4 Allow decoding of anchorectl json files (#3973)
* allow decoding of import sbom file shape

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

* address formatting

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

* add file mode and type processing

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

* use type to interpret the raw value

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

* safe mode convert should use uint32

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

* simpler decoder type

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

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-06-10 15:03:50 -04:00
dependabot[bot] cfa7cc5be9 chore(deps): bump github.com/anchore/stereoscope (#3991)
Bumps [github.com/anchore/stereoscope](https://github.com/anchore/stereoscope) from 0.1.5-0.20250604132324-344e29f37f05 to 0.1.5.
- [Release notes](https://github.com/anchore/stereoscope/releases)
- [Changelog](https://github.com/anchore/stereoscope/blob/main/RELEASE.md)
- [Commits](https://github.com/anchore/stereoscope/commits/v0.1.5)

---
updated-dependencies:
- dependency-name: github.com/anchore/stereoscope
  dependency-version: 0.1.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-06-10 11:47:40 -04:00
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
102 changed files with 6682 additions and 618 deletions
+6 -19
View File
@@ -26,7 +26,7 @@ tools:
# used for linting
- name: golangci-lint
version:
want: v2.1.6
want: v2.2.1
method: github-release
with:
repo: golangci/golangci-lint
@@ -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
@@ -42,7 +42,7 @@ tools:
# used for signing the checksums file at release
- name: cosign
version:
want: v2.5.0
want: v2.5.2
method: github-release
with:
repo: sigstore/cosign
@@ -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.2
method: github-release
with:
repo: cli/cli
+3 -3
View File
@@ -45,7 +45,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f #v3.28.18
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b #v3.29.2
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@ff0a06e83cb2de871e5a09832bc6a81e7276941f #v3.28.18
uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b #v3.29.2
# ℹ️ 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@ff0a06e83cb2de871e5a09832bc6a81e7276941f #v3.28.18
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b #v3.29.2
+2 -2
View File
@@ -40,7 +40,7 @@ jobs:
- name: Delete existing comment
if: ${{ hashFiles( env.CI_COMMENT_FILE ) == '' }}
uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db #v2.9.2
uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 #v2.9.3
with:
header: ${{ env.COMMENT_HEADER }}
hide: true
@@ -48,7 +48,7 @@ jobs:
- name: Add comment
if: ${{ hashFiles( env.CI_COMMENT_FILE ) != '' }}
uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db #v2.9.2
uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 #v2.9.3
with:
header: ${{ env.COMMENT_HEADER }}
path: ${{ env.CI_COMMENT_FILE }}
+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@e11c554f704a0b820cbf8c51673f6945e0731532 #v0.20.0
- uses: anchore/sbom-action@9246b90769f852b3a8921f330c59e0b3f439d6e9 #v0.20.1
continue-on-error: true
with:
file: go.mod
+4 -1
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
@@ -195,7 +198,7 @@ jobs:
runs-on: macos-latest
steps:
- name: Install Cosign
uses: sigstore/cosign-installer@v3.8.2
uses: sigstore/cosign-installer@v3.9.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
+187 -119
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,201 @@ dockers:
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
# nonroot images...
- image_templates:
- anchore/syft:{{.Tag}}-nonroot-amd64
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64
goarch: amd64
dockerfile: Dockerfile.nonroot
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}}-nonroot-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-arm64v8
goarch: arm64
dockerfile: Dockerfile.nonroot
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}}-nonroot-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-ppc64le
goarch: ppc64le
dockerfile: Dockerfile.nonroot
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}}-nonroot-s390x
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x
goarch: s390x
dockerfile: Dockerfile.nonroot
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}}"
# 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:
- name_template: anchore/syft:latest
image_templates:
- anchore/syft:{{.Tag}}
- anchore/syft:{{.Tag}}-arm64v8
- anchore/syft:{{.Tag}}-ppc64le
- anchore/syft:{{.Tag}}-s390x
- name_template: anchore/syft:debug
- anchore/syft:{{.Tag}}-debug
- anchore/syft:{{.Tag}}-debug-arm64v8
- anchore/syft:{{.Tag}}-debug-ppc64le
- anchore/syft:{{.Tag}}-debug-s390x
- name_template: anchore/syft:{{.Tag}}
image_templates:
- anchore/syft:{{.Tag}}
- anchore/syft:{{.Tag}}-amd64
- anchore/syft:{{.Tag}}-arm64v8
- anchore/syft:{{.Tag}}-ppc64le
- anchore/syft:{{.Tag}}-s390x
- 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: anchore/syft:{{.Tag}}
image_templates:
- anchore/syft:{{.Tag}}-amd64
- anchore/syft:{{.Tag}}-arm64v8
- anchore/syft:{{.Tag}}-ppc64le
- 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
# nonroot images...
- name_template: anchore/syft:nonroot
image_templates:
- anchore/syft:{{.Tag}}-nonroot-amd64
- anchore/syft:{{.Tag}}-nonroot-arm64v8
- anchore/syft:{{.Tag}}-nonroot-ppc64le
- anchore/syft:{{.Tag}}-nonroot-s390x
- name_template: ghcr.io/anchore/syft:nonroot
image_templates:
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x
- name_template: anchore/syft:{{.Tag}}-nonroot
image_templates:
- anchore/syft:{{.Tag}}-nonroot-amd64
- anchore/syft:{{.Tag}}-nonroot-arm64v8
- anchore/syft:{{.Tag}}-nonroot-ppc64le
- anchore/syft:{{.Tag}}-nonroot-s390x
- name_template: ghcr.io/anchore/syft:{{.Tag}}-nonroot
image_templates:
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-arm64v8
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-ppc64le
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x
# debug images...
- name_template: anchore/syft: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: 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: 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
- anchore/syft:{{.Tag}}-debug-amd64
- anchore/syft:{{.Tag}}-debug-arm64v8
- anchore/syft:{{.Tag}}-debug-ppc64le
- anchore/syft:{{.Tag}}-debug-s390x
- name_template: ghcr.io/anchore/syft:{{.Tag}}-debug
image_templates:
- 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 -1
View File
@@ -1,10 +1,12 @@
FROM gcr.io/distroless/static-debian12:debug
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
+27
View File
@@ -0,0 +1,27 @@
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
ARG VCS_URL
LABEL org.opencontainers.image.created=$BUILD_DATE
LABEL org.opencontainers.image.title="syft"
LABEL org.opencontainers.image.description="CLI tool and library for generating a Software Bill of Materials from container images and filesystems"
LABEL org.opencontainers.image.source=$VCS_URL
LABEL org.opencontainers.image.revision=$VCS_REF
LABEL org.opencontainers.image.vendor="Anchore, Inc."
LABEL org.opencontainers.image.version=$BUILD_VERSION
LABEL org.opencontainers.image.licenses="Apache-2.0"
LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/syft/main/README.md"
LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png"
LABEL io.artifacthub.package.license="Apache-2.0"
ENTRYPOINT ["/syft"]
+37 -46
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"
@@ -76,6 +78,7 @@ tasks:
- task: integration
- task: validate-cyclonedx-schema
- task: test-utils
- task: snapshot
- task: cli
- task: check-docker-cache
@@ -230,10 +233,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 +250,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:
@@ -286,12 +265,12 @@ tasks:
YELLOW='\033[0;33m'
RESET='\033[0m'
echo -e "${YELLOW}creating fingerprint files for non-docker fixtures...${RESET}"
echo -e "${YELLOW}creating fingerprint files for non-docker fixtures...${RESET}"
for dir in $(find . -type d -name 'test-fixtures'); do
if [ -f "$dir/Makefile" ]; then
# for debugging...
#echo -e "${YELLOW}• calculating fingerprints in $dir... ${RESET}"
(make -C "$dir" fingerprint)
fi
done
@@ -320,18 +299,18 @@ tasks:
BOLD='\033[1m'
PURPLE='\033[0;35m'
RESET='\033[0m'
# if DOWNLOAD_TEST_FIXTURE_CACHE is set to 'false', then skip the cache download and always build
if [ "$DOWNLOAD_TEST_FIXTURE_CACHE" = "false" ]; then
echo -e "${BOLD}${PURPLE}skipping cache download, rebuilding cache...${RESET}"
{{ .TASK }} build-fixtures
exit 0
fi
LATEST_FINGERPRINT=$(docker manifest inspect {{ .CACHE_IMAGE }} | {{ .YQ }} -r '.annotations.fingerprint')
echo "latest cache: $LATEST_FINGERPRINT"
if [ -f {{ .LAST_CACHE_PULL_FILE }} ]; then
LAST_PULL_FINGERPRINT=$(cat {{ .LAST_CACHE_PULL_FILE }} | {{ .YQ }} -r '.digest')
else
@@ -339,41 +318,41 @@ tasks:
{{ .TASK }} download-test-fixture-cache
exit 0
fi
{{ .TASK }} fingerprints
WANT_FINGERPRINT=$(cat {{ .CACHE_PATHS_FILE }} | {{ .YQ }} -r '.digest')
WANT_FINGERPRINT=$(cat {{ .CACHE_PATHS_FILE }} | {{ .YQ }} -r '.digest')
echo "desired cache: $WANT_FINGERPRINT"
echo "last pulled cache: $LAST_PULL_FINGERPRINT"
# if we already have the latest cache, skip the refresh
if [ "$LAST_PULL_FINGERPRINT" = "$WANT_FINGERPRINT" ]; then
echo -e "${BOLD}${PURPLE}already have the latest cache (skipping cache download)${RESET}"
exit 0
fi
# at this point we only refresh the cache if we want the same cache that is currently available.
# we don't by default refresh the cache if the cache if it is simply different from what we have,
# because we may be working on a code change that doesn't require a cache refresh (but could trigger one,
# which would be annoying to deal with in a development workflow).
if [ "$LATEST_FINGERPRINT" = "$WANT_FINGERPRINT" ]; then
echo -e "${BOLD}${PURPLE}found newer cache! downloading cache...${RESET}"
{{ .TASK }} download-test-fixture-cache
else
echo -e "${BOLD}${PURPLE}found different cache, but isn't clear if it's newer (skipping cache download and manually building)${RESET}"
{{ .YQ }} eval '.paths[] | "\(.digest) \(.path)"' {{ .LAST_CACHE_PULL_FILE }} > .tmp/last_cache_lines
{{ .YQ }} eval '.paths[] | "\(.digest) \(.path)"' {{ .CACHE_PATHS_FILE }} > .tmp/cache_lines
diff .tmp/last_cache_lines .tmp/cache_lines || true
echo -e "${BOLD}${PURPLE}diff with more context...${RESET}"
diff -U10000 {{ .LAST_CACHE_PULL_FILE }} {{ .CACHE_PATHS_FILE }} || true
echo -e "${BOLD}${PURPLE}detected changes to input material, manually building fixtures...${RESET}"
{{ .TASK }} build-fixtures
fi
@@ -386,7 +365,7 @@ tasks:
BOLD='\033[1m'
YELLOW='\033[0;33m'
RESET='\033[0m'
# Use a for loop with command substitution to avoid subshell issues
for dir in $(find . -type d -name 'test-fixtures'); do
if [ -f "$dir/Makefile" ]; then
@@ -424,15 +403,15 @@ tasks:
set -eu
oras_command="{{ .ORAS }} push {{ .CACHE_IMAGE }}"
paths=$(cat {{ .CACHE_PATHS_FILE }} | {{ .YQ }} -r '.paths[].path')
paths=$(cat {{ .CACHE_PATHS_FILE }} | {{ .YQ }} -r '.paths[].path')
for path in $paths; do
oras_command+=" $path"
done
oras_command+=" {{ .CACHE_PATHS_FILE }}"
oras_command+=" --annotation org.opencontainers.image.source=https://github.com/{{ .OWNER }}/{{ .PROJECT }}"
oras_command+=" --annotation fingerprint=$(cat {{ .CACHE_PATHS_FILE }} | {{ .YQ }} -r '.digest')"
echo "Executing: $oras_command"
eval $oras_command
@@ -573,6 +552,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]
@@ -650,7 +641,7 @@ tasks:
BOLD='\033[1m'
YELLOW='\033[0;33m'
RESET='\033[0m'
# Use a for loop with command substitution to avoid subshell issues
for dir in $(find . -type d -name 'test-fixtures'); do
if [ -f "$dir/Makefile" ]; then
+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
}
@@ -0,0 +1,8 @@
[TestHandler_handlePullSourceStarted/snap_download_in_progress - 1]
⠋ Downloading snap file... ━━━━━━━━━━━━━━━━━━━━ example-app_1.0_amd64.snap
---
[TestHandler_handlePullSourceStarted/snap_download_complete - 1]
✔ Snap downloaded successfully example-app_1.0_amd64.snap
---
@@ -26,7 +26,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
{
name: "cataloging task in progress",
eventFn: func(t *testing.T) partybus.Event {
value := &monitor.CatalogerTaskProgress{
value := &monitor.TaskProgress{
AtomicStage: progress.NewAtomicStage("some stage"),
Manual: progress.NewManual(100),
}
@@ -48,7 +48,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
{
name: "cataloging sub task in progress",
eventFn: func(t *testing.T) partybus.Event {
value := &monitor.CatalogerTaskProgress{
value := &monitor.TaskProgress{
AtomicStage: progress.NewAtomicStage("some stage"),
Manual: progress.NewManual(100),
}
@@ -71,7 +71,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
{
name: "cataloging sub task complete",
eventFn: func(t *testing.T) partybus.Event {
value := &monitor.CatalogerTaskProgress{
value := &monitor.TaskProgress{
AtomicStage: progress.NewAtomicStage("some stage"),
Manual: progress.NewManual(100),
}
@@ -94,7 +94,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
{
name: "cataloging sub task complete -- hide stage",
eventFn: func(t *testing.T) partybus.Event {
value := &monitor.CatalogerTaskProgress{
value := &monitor.TaskProgress{
AtomicStage: progress.NewAtomicStage("some stage"),
Manual: progress.NewManual(100),
}
@@ -117,7 +117,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
{
name: "cataloging sub task complete with removal",
eventFn: func(t *testing.T) partybus.Event {
value := &monitor.CatalogerTaskProgress{
value := &monitor.TaskProgress{
AtomicStage: progress.NewAtomicStage("some stage"),
Manual: progress.NewManual(100),
}
@@ -162,7 +162,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
}
// note: this line / event is not under test, only needed to show a sub status
kickoffEvent := &monitor.CatalogerTaskProgress{
kickoffEvent := &monitor.TaskProgress{
AtomicStage: progress.NewAtomicStage(""),
Manual: progress.NewManual(-1),
}
+37
View File
@@ -0,0 +1,37 @@
package ui
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/wagoodman/go-partybus"
"github.com/anchore/bubbly/bubbles/taskprogress"
"github.com/anchore/syft/internal/log"
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
)
func (m *Handler) handlePullSourceStarted(e partybus.Event) []tea.Model {
prog, info, err := syftEventParsers.ParsePullSourceStarted(e)
if err != nil {
log.WithFields("error", err).Debug("unable to parse event")
return nil
}
tsk := m.newTaskProgress(
taskprogress.Title{
Default: info.Title.Default,
Running: info.Title.WhileRunning,
Success: info.Title.OnSuccess,
},
taskprogress.WithStagedProgressable(prog),
)
tsk.HideOnSuccess = info.HideOnSuccess
tsk.HideStageOnSuccess = info.HideStageOnSuccess
tsk.HideProgressOnSuccess = true
if info.Context != "" {
tsk.Context = []string{info.Context}
}
return []tea.Model{tsk}
}
+121
View File
@@ -0,0 +1,121 @@
package ui
import (
"testing"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/gkampitakis/go-snaps/snaps"
"github.com/stretchr/testify/require"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
"github.com/anchore/bubbly/bubbles/taskprogress"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/event/monitor"
)
func TestHandler_handlePullSourceStarted(t *testing.T) {
tests := []struct {
name string
eventFn func(*testing.T) partybus.Event
iterations int
}{
{
name: "snap download in progress",
eventFn: func(t *testing.T) partybus.Event {
stage := progress.NewAtomicStage("")
manual := progress.NewManual(0)
manual.SetTotal(1000000) // 1MB file
manual.Set(250000) // 25% downloaded
taskProg := &monitor.TaskProgress{
AtomicStage: stage,
Manual: manual,
}
genericTask := monitor.GenericTask{
Title: monitor.Title{
Default: "Downloading snap",
WhileRunning: "Downloading snap file...",
OnSuccess: "Snap downloaded",
},
Context: "example-app_1.0_amd64.snap",
HideOnSuccess: false,
HideStageOnSuccess: true,
ID: "snap-download-123",
}
return partybus.Event{
Type: event.PullSourceStarted,
Source: genericTask,
Value: taskProg,
}
},
iterations: 5,
},
{
name: "snap download complete",
eventFn: func(t *testing.T) partybus.Event {
stage := progress.NewAtomicStage("")
manual := progress.NewManual(0)
manual.SetTotal(1000000) // 1MB file
manual.Set(1000000) // 100% downloaded
manual.SetCompleted()
taskProg := &monitor.TaskProgress{
AtomicStage: stage,
Manual: manual,
}
genericTask := monitor.GenericTask{
Title: monitor.Title{
Default: "Downloading snap",
WhileRunning: "Downloading snap file...",
OnSuccess: "Snap downloaded successfully",
},
Context: "example-app_1.0_amd64.snap",
HideOnSuccess: false,
HideStageOnSuccess: true,
ID: "snap-download-123",
}
return partybus.Event{
Type: event.PullSourceStarted,
Source: genericTask,
Value: taskProg,
}
},
iterations: 3,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
event := tt.eventFn(t)
handler := New(DefaultHandlerConfig())
handler.WindowSize = tea.WindowSizeMsg{
Width: 100,
Height: 80,
}
models := handler.handlePullSourceStarted(event)
require.Len(t, models, 1)
model := models[0]
tsk, ok := model.(taskprogress.Model)
require.True(t, ok)
gotModel := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
Time: time.Now(),
Sequence: tsk.Sequence(),
ID: tsk.ID(),
})
got := gotModel.View()
t.Log(got)
snaps.MatchSnapshot(t, got)
})
}
}
+1
View File
@@ -57,6 +57,7 @@ func New(cfg HandlerConfig) *Handler {
stereoscopeEvent.FetchImage: simpleHandler(h.handleFetchImage),
syftEvent.FileIndexingStarted: simpleHandler(h.handleFileIndexingStarted),
syftEvent.AttestationStarted: simpleHandler(h.handleAttestationStarted),
syftEvent.PullSourceStarted: simpleHandler(h.handlePullSourceStarted),
syftEvent.CatalogerTaskStarted: h.handleCatalogerTaskStarted,
})
+1 -1
View File
@@ -44,7 +44,7 @@ func AppClioSetupConfig(id clio.Identification, out io.Writer) *clio.SetupConfig
redact.Set(state.RedactStore)
log.Set(state.Logger)
stereoscope.SetLogger(state.Logger)
stereoscope.SetLogger(state.Logger.Nested("from", "stereoscope"))
return nil
},
).
@@ -33,7 +33,7 @@ func TestPackageDeduplication(t *testing.T) {
"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-6.el9": 2, // vsftpd + all above layers
"httpd-2.4.62-1.el9_5.2": 1, // last layer
"httpd-2.4.62-4.el9": 1, // last layer
},
},
{
@@ -51,7 +51,7 @@ func TestPackageDeduplication(t *testing.T) {
"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,
"httpd-2.4.62-4.el9": 1,
},
},
}
+64 -33
View File
@@ -5,7 +5,7 @@ go 1.24.1
require (
github.com/BurntSushi/toml v1.5.0
github.com/CycloneDX/cyclonedx-go v0.9.2
github.com/Masterminds/semver/v3 v3.3.1
github.com/Masterminds/semver/v3 v3.4.0
github.com/Masterminds/sprig/v3 v3.3.0
github.com/OneOfOne/xxhash v1.2.8
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
@@ -24,7 +24,7 @@ require (
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.6
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
@@ -35,22 +35,25 @@ require (
github.com/charmbracelet/lipgloss v1.1.0
github.com/dave/jennifer v1.7.1
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
github.com/diskfs/go-diskfs v1.6.1-0.20250601133945-2af1c7ece24c
github.com/distribution/reference v0.6.0
github.com/dustin/go-humanize v1.0.1
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/go-viper/mapstructure/v2 v2.3.0
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.6
github.com/google/licensecheck v0.3.1
github.com/google/uuid v1.6.0
github.com/gookit/color v1.5.4
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-getter v1.7.8
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/hcl/v2 v2.23.0
github.com/iancoleman/strcase v0.3.0
@@ -59,10 +62,10 @@ require (
github.com/jinzhu/copier v0.4.0
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
github.com/magiconair/properties v1.8.10
github.com/mholt/archives v0.1.2
github.com/mholt/archives v0.1.3
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
github.com/olekukonko/tablewriter v1.0.7
github.com/opencontainers/go-digest v1.0.0
github.com/pelletier/go-toml v1.9.5
github.com/quasilyte/go-ruleguard/dsl v0.3.22
@@ -72,7 +75,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,14 +88,20 @@ 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 (
cloud.google.com/go v0.116.0 // indirect
cloud.google.com/go/auth v0.9.9 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
cloud.google.com/go/compute/metadata v0.7.0 // indirect
cloud.google.com/go/iam v1.2.2 // indirect
cloud.google.com/go/storage v1.43.0 // indirect
dario.cat/mergo v1.0.1 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect
@@ -103,14 +112,17 @@ require (
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-lzo v0.1.0 // indirect
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // 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
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go v1.44.122 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/becheran/wildmatch-go v1.0.0 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.0 // indirect
github.com/bodgit/windows v1.0.1 // indirect
@@ -124,46 +136,54 @@ require (
github.com/containerd/containerd v1.7.27 // indirect
github.com/containerd/containerd/api v1.8.0 // indirect
github.com/containerd/continuity v0.4.4 // indirect
github.com/containerd/errdefs v0.3.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/fifo v1.1.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
github.com/containerd/ttrpc v1.2.7 // indirect
github.com/containerd/typeurl/v2 v2.1.1 // indirect
github.com/containerd/typeurl/v2 v2.2.0 // 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.3.0+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 v28.2.2+incompatible // 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
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/felixge/fgprof v0.9.5 // indirect
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/logr v1.4.3 // 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
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
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
@@ -177,9 +197,11 @@ require (
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
@@ -194,6 +216,8 @@ require (
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/nwaples/rardecode/v2 v2.1.0 // indirect
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
github.com/olekukonko/ll v0.0.8 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opencontainers/runtime-spec v1.1.0 // indirect
github.com/opencontainers/selinux v1.11.0 // indirect
@@ -203,6 +227,7 @@ require (
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/profile v1.7.0 // indirect
github.com/pkg/xattr v0.4.9 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
@@ -210,13 +235,14 @@ require (
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/sorairolake/lzip-go v0.3.5 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/viper v1.20.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/sylabs/sif/v2 v2.21.1 // indirect
github.com/sylabs/squashfs v1.0.6 // indirect
@@ -226,7 +252,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,28 +261,33 @@ 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/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.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/oauth2 v0.30.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/time v0.7.0 // indirect
golang.org/x/tools v0.34.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/api v0.203.0 // indirect
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f // 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 (
+1101 -77
View File
File diff suppressed because it is too large Load Diff
+32 -2
View File
@@ -36,8 +36,8 @@ func Notify(message string) {
})
}
func StartCatalogerTask(info monitor.GenericTask, size int64, initialStage string) *monitor.CatalogerTaskProgress {
t := &monitor.CatalogerTaskProgress{
func StartCatalogerTask(info monitor.GenericTask, size int64, initialStage string) *monitor.TaskProgress {
t := &monitor.TaskProgress{
AtomicStage: progress.NewAtomicStage(initialStage),
Manual: progress.NewManual(size),
}
@@ -50,3 +50,33 @@ func StartCatalogerTask(info monitor.GenericTask, size int64, initialStage strin
return t
}
func StartPullSourceTask(info monitor.GenericTask, size int64, initialStage string) *monitor.TaskProgress {
t := &monitor.TaskProgress{
AtomicStage: progress.NewAtomicStage(initialStage),
Manual: progress.NewManual(size),
}
Publish(partybus.Event{
Type: event.PullSourceStarted,
Source: info,
Value: progress.StagedProgressable(t),
})
return t
}
func StartIndexingFiles(path string) *monitor.TaskProgress {
t := &monitor.TaskProgress{
AtomicStage: progress.NewAtomicStage(""),
Manual: progress.NewManual(-1),
}
Publish(partybus.Event{
Type: event.FileIndexingStarted,
Source: path,
Value: progress.StagedProgressable(t),
})
return t
}
+154
View File
@@ -0,0 +1,154 @@
package file
import (
"fmt"
"io"
"net/http"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/go-getter/helper/url"
"github.com/wagoodman/go-progress"
"github.com/anchore/clio"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal"
)
var (
archiveExtensions = getterDecompressorNames()
ErrNonArchiveSource = fmt.Errorf("non-archive sources are not supported for directory destinations")
)
type Getter interface {
// GetFile downloads the give URL into the given path. The URL must reference a single file.
GetFile(dst, src string, monitor ...*progress.Manual) error
// GetToDir downloads the resource found at the `src` URL into the given `dst` directory.
// The directory must already exist, and the remote resource MUST BE AN ARCHIVE (e.g. `.tar.gz`).
GetToDir(dst, src string, monitor ...*progress.Manual) error
}
type HashiGoGetter struct {
httpGetter getter.HttpGetter
}
// NewGetter creates and returns a new Getter. Providing an http.Client is optional. If one is provided,
// it will be used for all HTTP(S) getting; otherwise, go-getter's default getters will be used.
func NewGetter(id clio.Identification, httpClient *http.Client) *HashiGoGetter {
return &HashiGoGetter{
httpGetter: getter.HttpGetter{
Client: httpClient,
Header: http.Header{
"User-Agent": []string{fmt.Sprintf("%v %v", id.Name, id.Version)},
},
},
}
}
func (g HashiGoGetter) GetFile(dst, src string, monitors ...*progress.Manual) error {
if len(monitors) > 1 {
return fmt.Errorf("multiple monitors provided, which is not allowed")
}
return getterClient(dst, src, false, g.httpGetter, monitors).Get()
}
func (g HashiGoGetter) GetToDir(dst, src string, monitors ...*progress.Manual) error {
// though there are multiple getters, only the http/https getter requires extra validation
if err := validateHTTPSource(src); err != nil {
return err
}
if len(monitors) > 1 {
return fmt.Errorf("multiple monitors provided, which is not allowed")
}
return getterClient(dst, src, true, g.httpGetter, monitors).Get()
}
func validateHTTPSource(src string) error {
// we are ignoring any sources that are not destined to use the http getter object
if !internal.HasAnyOfPrefixes(src, "http://", "https://") {
return nil
}
u, err := url.Parse(src)
if err != nil {
return fmt.Errorf("bad URL provided %q: %w", src, err)
}
// only allow for sources with archive extensions
if !internal.HasAnyOfSuffixes(u.Path, archiveExtensions...) {
return ErrNonArchiveSource
}
return nil
}
func getterClient(dst, src string, dir bool, httpGetter getter.HttpGetter, monitors []*progress.Manual) *getter.Client {
client := &getter.Client{
Src: src,
Dst: dst,
Dir: dir,
Getters: map[string]getter.Getter{
"http": &httpGetter,
"https": &httpGetter,
// note: these are the default getters from https://github.com/hashicorp/go-getter/blob/v1.5.9/get.go#L68-L74
// it is possible that other implementations need to account for custom httpclient injection, however,
// that has not been accounted for at this time.
"file": new(getter.FileGetter),
"git": new(getter.GitGetter),
"gcs": new(getter.GCSGetter),
"hg": new(getter.HgGetter),
"s3": new(getter.S3Getter),
},
Options: mapToGetterClientOptions(monitors),
}
return client
}
func withProgress(monitor *progress.Manual) func(client *getter.Client) error {
return getter.WithProgress(
&progressAdapter{monitor: monitor},
)
}
func mapToGetterClientOptions(monitors []*progress.Manual) []getter.ClientOption {
var result []getter.ClientOption
for _, monitor := range monitors {
result = append(result, withProgress(monitor))
}
// derived from https://github.com/hashicorp/go-getter/blob/v2.2.3/decompress.go#L23-L63
fileSizeLimit := int64(5 * file.GB)
dec := getter.LimitedDecompressors(0, fileSizeLimit)
result = append(result, getter.WithDecompressors(dec))
return result
}
type readCloser struct {
progress.Reader
}
func (c *readCloser) Close() error { return nil }
type progressAdapter struct {
monitor *progress.Manual
}
func (a *progressAdapter) TrackProgress(_ string, currentSize, totalSize int64, stream io.ReadCloser) io.ReadCloser {
a.monitor.Set(currentSize)
a.monitor.SetTotal(totalSize)
return &readCloser{
Reader: *progress.NewProxyReader(stream, a.monitor),
}
}
func getterDecompressorNames() (names []string) {
for name := range getter.Decompressors {
names = append(names, name)
}
return names
}
+268
View File
@@ -0,0 +1,268 @@
package file
import (
"archive/tar"
"bytes"
"context"
"crypto/x509"
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/url"
"path"
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/clio"
)
func TestGetter_GetFile(t *testing.T) {
testCases := []struct {
name string
prepareClient func(*http.Client)
assert assert.ErrorAssertionFunc
}{
{
name: "client trusts server's CA",
assert: assert.NoError,
},
{
name: "client doesn't trust server's CA",
prepareClient: removeTrustedCAs,
assert: assertUnknownAuthorityError,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
requestPath := "/foo"
server := newTestServer(t, withResponseForPath(t, requestPath, testFileContent))
t.Cleanup(server.Close)
httpClient := getClient(t, server)
if tc.prepareClient != nil {
tc.prepareClient(httpClient)
}
getter := NewGetter(testID, httpClient)
requestURL := createRequestURL(t, server, requestPath)
tempDir := t.TempDir()
tempFile := path.Join(tempDir, "some-destination-file")
err := getter.GetFile(tempFile, requestURL)
tc.assert(t, err)
})
}
}
func TestGetter_GetToDir_FilterNonArchivesWired(t *testing.T) {
testCases := []struct {
name string
source string
assert assert.ErrorAssertionFunc
}{
{
name: "error out on non-archive sources",
source: "http://localhost/something.txt",
assert: assertErrNonArchiveSource,
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
test.assert(t, NewGetter(testID, nil).GetToDir(t.TempDir(), test.source))
})
}
}
func TestGetter_validateHttpSource(t *testing.T) {
testCases := []struct {
name string
source string
assert assert.ErrorAssertionFunc
}{
{
name: "error out on non-archive sources",
source: "http://localhost/something.txt",
assert: assertErrNonArchiveSource,
},
{
name: "filter out non-archive sources with get param",
source: "https://localhost/vulnerability-db_v3_2021-11-21T08:15:44Z.txt?checksum=sha256%3Ac402d01fa909a3fa85a5c6733ef27a3a51a9105b6c62b9152adbd24c08358911",
assert: assertErrNonArchiveSource,
},
{
name: "ignore non http-https input",
source: "s3://bucket/something.txt",
assert: assert.NoError,
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
test.assert(t, validateHTTPSource(test.source))
})
}
}
func TestGetter_GetToDir_CertConcerns(t *testing.T) {
testCases := []struct {
name string
prepareClient func(*http.Client)
assert assert.ErrorAssertionFunc
}{
{
name: "client trusts server's CA",
assert: assert.NoError,
},
{
name: "client doesn't trust server's CA",
prepareClient: removeTrustedCAs,
assert: assertUnknownAuthorityError,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
requestPath := "/foo.tar"
tarball := createTarball("foo", testFileContent)
server := newTestServer(t, withResponseForPath(t, requestPath, tarball))
t.Cleanup(server.Close)
httpClient := getClient(t, server)
if tc.prepareClient != nil {
tc.prepareClient(httpClient)
}
getter := NewGetter(testID, httpClient)
requestURL := createRequestURL(t, server, requestPath)
tempDir := t.TempDir()
err := getter.GetToDir(tempDir, requestURL)
tc.assert(t, err)
})
}
}
func assertUnknownAuthorityError(t assert.TestingT, err error, _ ...interface{}) bool {
return assert.ErrorAs(t, err, &x509.UnknownAuthorityError{})
}
func assertErrNonArchiveSource(t assert.TestingT, err error, _ ...interface{}) bool {
return assert.ErrorIs(t, err, ErrNonArchiveSource)
}
func removeTrustedCAs(client *http.Client) {
client.Transport.(*http.Transport).TLSClientConfig.RootCAs = x509.NewCertPool()
}
// createTarball makes a single-file tarball and returns it as a byte slice.
func createTarball(filename string, content []byte) []byte {
tarBuffer := new(bytes.Buffer)
tarWriter := tar.NewWriter(tarBuffer)
tarWriter.WriteHeader(&tar.Header{
Name: filename,
Size: int64(len(content)),
Mode: 0600,
})
tarWriter.Write(content)
tarWriter.Close()
return tarBuffer.Bytes()
}
type muxOption func(mux *http.ServeMux)
func withResponseForPath(t *testing.T, path string, response []byte) muxOption {
t.Helper()
return func(mux *http.ServeMux) {
mux.HandleFunc(path, func(w http.ResponseWriter, req *http.Request) {
t.Logf("server handling request: %s %s", req.Method, req.URL)
_, err := w.Write(response)
if err != nil {
t.Fatal(err)
}
})
}
}
var testID = clio.Identification{
Name: "test-app",
Version: "v0.5.3",
}
func newTestServer(t *testing.T, muxOptions ...muxOption) *httptest.Server {
t.Helper()
mux := http.NewServeMux()
for _, option := range muxOptions {
option(mux)
}
server := httptest.NewTLSServer(mux)
t.Logf("new TLS server listening at %s", getHost(t, server))
return server
}
func createRequestURL(t *testing.T, server *httptest.Server, path string) string {
t.Helper()
// TODO: Figure out how to get this value from the server without hardcoding it here
const testServerCertificateName = "example.com"
serverURL, err := url.Parse(server.URL)
if err != nil {
t.Fatal(err)
}
// Set URL hostname to value from TLS certificate
serverURL.Host = fmt.Sprintf("%s:%s", testServerCertificateName, serverURL.Port())
serverURL.Path = path
return serverURL.String()
}
// getClient returns an http.Client that can be used to contact the test TLS server.
func getClient(t *testing.T, server *httptest.Server) *http.Client {
t.Helper()
httpClient := server.Client()
transport := httpClient.Transport.(*http.Transport)
serverHost := getHost(t, server)
transport.DialContext = func(_ context.Context, _, addr string) (net.Conn, error) {
t.Logf("client dialing %q for host %q", serverHost, addr)
// Ensure the client dials our test server
return net.Dial("tcp", serverHost)
}
return httpClient
}
// getHost extracts the host value from a server URL string.
// e.g. given a server with URL "http://1.2.3.4:5000/foo", getHost returns "1.2.3.4:5000"
func getHost(t *testing.T, server *httptest.Server) string {
t.Helper()
u, err := url.Parse(server.URL)
if err != nil {
t.Fatal(err)
}
return u.Hostname() + ":" + u.Port()
}
var testFileContent = []byte("This is the content of a test file!\n")
+89
View File
@@ -0,0 +1,89 @@
package file
import (
"errors"
"io/fs"
"os"
"path/filepath"
"github.com/diskfs/go-diskfs/filesystem"
)
type WalkDiskDirFunc func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error
// WalkDiskDir walks the file tree within the go-diskfs filesystem at root, calling fn for each file or directory in the tree, including root.
// This is meant to mimic the behavior of fs.WalkDir in the standard library.
func WalkDiskDir(fsys filesystem.FileSystem, root string, fn WalkDiskDirFunc) error {
infos, err := fsys.ReadDir(root)
if err != nil {
return err
}
if len(infos) == 0 {
return nil
}
for _, info := range infos {
p := filepath.Join(root, info.Name())
err = walkDiskDir(fsys, p, info, fn)
if err != nil {
if errors.Is(err, fs.SkipDir) {
continue
}
if errors.Is(err, fs.SkipAll) {
return nil
}
return err
}
}
return err
}
func walkDiskDir(fsys filesystem.FileSystem, name string, d os.FileInfo, walkDirFn WalkDiskDirFunc) error {
if err := walkDirFn(fsys, name, d, nil); err != nil {
if errors.Is(err, fs.SkipDir) && (d == nil || d.IsDir()) {
return nil
}
return err
}
isDir := d != nil && d.IsDir()
if d == nil {
_, err := fsys.ReadDir(name)
if err != nil {
return nil
}
isDir = true
}
if !isDir {
return nil
}
dirs, err := fsys.ReadDir(name)
if err != nil {
err = walkDirFn(fsys, name, d, err)
if err != nil {
if errors.Is(err, fs.SkipDir) {
return nil
}
return err
}
}
for _, d1 := range dirs {
name1 := filepath.Join(name, d1.Name())
if err := walkDiskDir(fsys, name1, d1, walkDirFn); err != nil {
if errors.Is(err, fs.SkipDir) {
break
}
if errors.Is(err, fs.SkipAll) {
return err
}
return err
}
}
return nil
}
+250
View File
@@ -0,0 +1,250 @@
package file
import (
"io/fs"
"os"
"path/filepath"
"testing"
"github.com/diskfs/go-diskfs/backend/file"
"github.com/diskfs/go-diskfs/filesystem"
"github.com/diskfs/go-diskfs/filesystem/squashfs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func createTestFS(t *testing.T) filesystem.FileSystem {
dir := t.TempDir()
filename := "test.squashfs"
f, err := os.Create(filepath.Join(dir, filename))
require.NoError(t, err)
b := file.New(f, false)
fsys, err := squashfs.Create(b, 0, 0, 4096)
require.NoError(t, err)
testFiles := []struct {
path string
content string
isDir bool
}{
{"/file1.txt", "content of file1", false},
{"/file2.txt", "content of file2", false},
{"/dir1", "", true},
{"/dir1/subfile1.txt", "content of subfile1", false},
{"/dir1/subfile2.txt", "content of subfile2", false},
{"/dir1/subdir1", "", true},
{"/dir1/subdir1/deepfile.txt", "deep content", false},
{"/dir2", "", true},
{"/dir2/anotherfile.txt", "another content", false},
{"/emptydir", "", true},
}
for _, tf := range testFiles {
if tf.isDir {
err := fsys.Mkdir(tf.path)
require.NoError(t, err)
} else {
f, err := fsys.OpenFile(tf.path, os.O_CREATE|os.O_RDWR)
require.NoError(t, err)
_, err = f.Write([]byte(tf.content))
require.NoError(t, err)
f.Close()
}
}
return fsys
}
func TestWalkDiskDir_CompleteTraversal(t *testing.T) {
fsys := createTestFS(t)
var visitedPaths []string
err := WalkDiskDir(fsys, "/", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
require.NoError(t, err)
visitedPaths = append(visitedPaths, path)
return nil
})
require.NoError(t, err)
expectedPaths := []string{
"/file1.txt",
"/file2.txt",
"/dir1",
"/dir1/subfile1.txt",
"/dir1/subfile2.txt",
"/dir1/subdir1",
"/dir1/subdir1/deepfile.txt",
"/dir2",
"/dir2/anotherfile.txt",
"/emptydir",
}
assert.ElementsMatch(t, expectedPaths, visitedPaths)
}
func TestWalkDiskDir_FileInfoCorrect(t *testing.T) {
fsys := createTestFS(t)
var fileInfos []struct {
path string
isDir bool
name string
}
err := WalkDiskDir(fsys, "/", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
require.NoError(t, err)
require.NotNil(t, d)
fileInfos = append(fileInfos, struct {
path string
isDir bool
name string
}{
path: path,
isDir: d.IsDir(),
name: d.Name(),
})
return nil
})
require.NoError(t, err)
for _, fi := range fileInfos {
expectedName := filepath.Base(fi.path)
assert.Equal(t, expectedName, fi.name)
if fi.path == "/dir1" || fi.path == "/dir2" || fi.path == "/emptydir" || fi.path == "/dir1/subdir1" {
assert.True(t, fi.isDir, "Expected %s to be directory", fi.path)
} else {
assert.False(t, fi.isDir, "Expected %s to be file", fi.path)
}
}
}
func TestWalkDiskDir_SkipDir(t *testing.T) {
fsys := createTestFS(t)
var visitedPaths []string
err := WalkDiskDir(fsys, "/", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
require.NoError(t, err)
visitedPaths = append(visitedPaths, path)
if path == "/dir1" {
return fs.SkipDir
}
return nil
})
require.NoError(t, err)
assert.Contains(t, visitedPaths, "/dir1")
assert.NotContains(t, visitedPaths, "/dir1/subfile1.txt")
assert.NotContains(t, visitedPaths, "/dir1/subfile2.txt")
assert.NotContains(t, visitedPaths, "/dir1/subdir1")
assert.NotContains(t, visitedPaths, "/dir1/subdir1/deepfile.txt")
assert.Contains(t, visitedPaths, "/dir2")
assert.Contains(t, visitedPaths, "/dir2/anotherfile.txt")
}
func TestWalkDiskDir_SkipAll(t *testing.T) {
fsys := createTestFS(t)
var visitedPaths []string
err := WalkDiskDir(fsys, "/", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
require.NoError(t, err)
visitedPaths = append(visitedPaths, path)
if path == "/dir1" {
return fs.SkipAll
}
return nil
})
require.NoError(t, err)
assert.Contains(t, visitedPaths, "/dir1")
assert.NotContains(t, visitedPaths, "/file1.txt")
assert.NotContains(t, visitedPaths, "/file2.txt")
assert.NotContains(t, visitedPaths, "/dir1/subfile1.txt")
assert.NotContains(t, visitedPaths, "/dir2")
assert.NotContains(t, visitedPaths, "/dir2/anotherfile.txt")
assert.NotContains(t, visitedPaths, "/emptydir")
}
func TestWalkDiskDir_EmptyDirectory(t *testing.T) {
fs := createTestFS(t)
var visitedPaths []string
err := WalkDiskDir(fs, "/emptydir", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
require.NoError(t, err)
visitedPaths = append(visitedPaths, path)
return nil
})
require.NoError(t, err)
assert.Empty(t, visitedPaths)
}
func TestWalkDiskDir_NonexistentPath(t *testing.T) {
fs := createTestFS(t)
err := WalkDiskDir(fs, "/nonexistent", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
return nil
})
assert.Error(t, err)
}
func TestWalkDiskDir_WalkFunctionError(t *testing.T) {
fs := createTestFS(t)
customErr := assert.AnError
err := WalkDiskDir(fs, "/", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
if path == "/file1.txt" {
return customErr
}
return nil
})
assert.Error(t, err)
assert.Equal(t, customErr, err)
}
func TestWalkDiskDir_SubdirectoryTraversal(t *testing.T) {
fs := createTestFS(t)
var visitedPaths []string
err := WalkDiskDir(fs, "/dir1", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
require.NoError(t, err)
visitedPaths = append(visitedPaths, path)
return nil
})
require.NoError(t, err)
expectedPaths := []string{
"/dir1/subfile1.txt",
"/dir1/subfile2.txt",
"/dir1/subdir1",
"/dir1/subdir1/deepfile.txt",
}
assert.ElementsMatch(t, expectedPaths, visitedPaths)
}
func TestWalkDiskDir_SingleFile(t *testing.T) {
fs := createTestFS(t)
var visitedPaths []string
err := WalkDiskDir(fs, "/file1.txt", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
require.NoError(t, err)
visitedPaths = append(visitedPaths, path)
return nil
})
// we are providing a file path, not a directory
require.Error(t, err)
assert.Empty(t, visitedPaths)
}
+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{
+11
View File
@@ -13,6 +13,17 @@ func HasAnyOfPrefixes(input string, prefixes ...string) bool {
return false
}
// HasAnyOfSuffixes returns an indication if the given string has any of the given suffixes.
func HasAnyOfSuffixes(input string, suffixes ...string) bool {
for _, suffix := range suffixes {
if strings.HasSuffix(input, suffix) {
return true
}
}
return false
}
func TruncateMiddleEllipsis(input string, maxLen int) string {
if len(input) <= maxLen {
return input
+57
View File
@@ -7,6 +7,63 @@ import (
"github.com/stretchr/testify/assert"
)
func TestHasAnyOfSuffixes(t *testing.T) {
tests := []struct {
name string
input string
suffixes []string
expected bool
}{
{
name: "go case",
input: "this has something",
suffixes: []string{
"has something",
"has NOT something",
},
expected: true,
},
{
name: "no match",
input: "this has something",
suffixes: []string{
"has NOT something",
},
expected: false,
},
{
name: "empty",
input: "this has something",
suffixes: []string{},
expected: false,
},
{
name: "positive match last",
input: "this has something",
suffixes: []string{
"that does not have",
"something",
},
expected: true,
},
{
name: "empty input",
input: "",
suffixes: []string{
"that does not have",
"this has",
},
expected: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, HasAnyOfSuffixes(test.input, test.suffixes...))
})
}
}
func TestHasAnyOfPrefixes(t *testing.T) {
tests := []struct {
name string
+1 -1
View File
@@ -15,7 +15,7 @@ import (
"github.com/anchore/syft/syft/sbom"
)
func RunTask(ctx context.Context, tsk Task, resolver file.Resolver, s sbomsync.Builder, prog *monitor.CatalogerTaskProgress) error {
func RunTask(ctx context.Context, tsk Task, resolver file.Resolver, s sbomsync.Builder, prog *monitor.TaskProgress) error {
err := runTaskSafely(ctx, tsk, resolver, s)
unknowns, remainingErrors := unknown.ExtractCoordinateErrors(err)
if len(unknowns) > 0 {
+1 -1
View File
@@ -17,7 +17,7 @@ func Test_TaskExecutor_PanicHandling(t *testing.T) {
panic("something bad happened")
})
err := RunTask(context.Background(), tsk, nil, nil, &monitor.CatalogerTaskProgress{
err := RunTask(context.Background(), tsk, nil, nil, &monitor.TaskProgress{
Manual: progress.NewManual(-1),
})
+3 -3
View File
@@ -144,14 +144,14 @@ func setContextExecutors(ctx context.Context, cfg *CreateSBOMConfig) context.Con
return ctx
}
func monitorPackageCount(prog *monitor.CatalogerTaskProgress) func(s *sbom.SBOM) {
func monitorPackageCount(prog *monitor.TaskProgress) func(s *sbom.SBOM) {
return func(s *sbom.SBOM) {
count := humanize.Comma(int64(s.Artifacts.Packages.PackageCount()))
prog.AtomicStage.Set(fmt.Sprintf("%s packages", count))
}
}
func monitorPackageCatalogingTask() *monitor.CatalogerTaskProgress {
func monitorPackageCatalogingTask() *monitor.TaskProgress {
info := monitor.GenericTask{
Title: monitor.Title{
Default: "Packages",
@@ -164,7 +164,7 @@ func monitorPackageCatalogingTask() *monitor.CatalogerTaskProgress {
return bus.StartCatalogerTask(info, -1, "")
}
func monitorCatalogingTask(srcID artifact.ID, tasks [][]task.Task) *monitor.CatalogerTaskProgress {
func monitorCatalogingTask(srcID artifact.ID, tasks [][]task.Task) *monitor.TaskProgress {
info := monitor.GenericTask{
Title: monitor.Title{
Default: "Catalog contents",
+2
View File
@@ -464,6 +464,8 @@ func findDefaultTags(src source.Description) ([]string, error) {
return []string{pkgcataloging.ImageTag, filecataloging.FileTag}, nil
case source.FileMetadata, source.DirectoryMetadata:
return []string{pkgcataloging.DirectoryTag, filecataloging.FileTag}, nil
case source.SnapMetadata:
return []string{pkgcataloging.InstalledTag, filecataloging.FileTag}, nil
default:
return nil, fmt.Errorf("unable to determine default cataloger tag for source type=%T", m)
}
+4
View File
@@ -23,6 +23,10 @@ const (
// CatalogerTaskStarted is a partybus event that occurs when starting a task within a cataloger
CatalogerTaskStarted partybus.EventType = typePrefix + "-cataloger-task-started"
// PullSourceStarted is a partybus event that occurs when starting to pull a source (does not overlap with stereoscope image pull events,
// this covers any additional sources such as snap and git repos).
PullSourceStarted partybus.EventType = typePrefix + "-pull-source-started"
// Events exclusively for the CLI
// CLIAppUpdateAvailable is a partybus event that occurs when an application update is available
@@ -1,15 +0,0 @@
package monitor
import (
"github.com/wagoodman/go-progress"
)
const (
TopLevelCatalogingTaskID = "cataloging"
PackageCatalogingTaskID = "package-cataloging"
)
type CatalogerTaskProgress struct {
*progress.AtomicStage
*progress.Manual
}
+10
View File
@@ -6,6 +6,11 @@ import (
"github.com/wagoodman/go-progress"
)
const (
TopLevelCatalogingTaskID = "cataloging"
PackageCatalogingTaskID = "package-cataloging"
)
type ShellProgress struct {
io.Reader
progress.Progressable
@@ -34,3 +39,8 @@ type GenericTask struct {
ParentID string
Context string
}
type TaskProgress struct {
*progress.AtomicStage
*progress.Manual
}
+20
View File
@@ -77,6 +77,26 @@ func ParseCatalogerTaskStarted(e partybus.Event) (progress.StagedProgressable, *
return mon, &source, nil
}
func ParsePullSourceStarted(e partybus.Event) (progress.StagedProgressable, *monitor.GenericTask, error) {
if err := checkEventType(e.Type, event.PullSourceStarted); err != nil {
return nil, nil, err
}
var mon progress.StagedProgressable
source, ok := e.Source.(monitor.GenericTask)
if !ok {
return nil, nil, newPayloadErr(e.Type, "Source", e.Source)
}
mon, ok = e.Value.(progress.StagedProgressable)
if !ok {
mon = nil
}
return mon, &source, nil
}
func ParseAttestationStartedEvent(e partybus.Event) (io.Reader, progress.Progressable, *monitor.GenericTask, error) {
if err := checkEventType(e.Type, event.AttestationStarted); err != nil {
return nil, nil, nil, err
+1 -1
View File
@@ -106,7 +106,7 @@ func processExecutableLocation(loc file.Location, resolver file.Resolver) (*file
return processExecutable(loc, uReader)
}
func catalogingProgress(locations int64) *monitor.CatalogerTaskProgress {
func catalogingProgress(locations int64) *monitor.TaskProgress {
info := monitor.GenericTask{
Title: monitor.Title{
Default: "Executables",
+1 -1
View File
@@ -113,7 +113,7 @@ func (i *Cataloger) catalogLocation(resolver file.Resolver, location file.Locati
return buf.String(), nil
}
func catalogingProgress(locations int64) *monitor.CatalogerTaskProgress {
func catalogingProgress(locations int64) *monitor.TaskProgress {
info := monitor.GenericTask{
Title: monitor.Title{
Default: "File contents",
+1 -1
View File
@@ -112,7 +112,7 @@ func (i *Cataloger) catalogLocation(ctx context.Context, resolver file.Resolver,
return digests, nil
}
func catalogingProgress(locations int64) *monitor.CatalogerTaskProgress {
func catalogingProgress(locations int64) *monitor.TaskProgress {
info := monitor.GenericTask{
Title: monitor.Title{
Default: "File digests",
@@ -76,7 +76,7 @@ func (i *Cataloger) Catalog(ctx context.Context, resolver file.Resolver, coordin
return results, errs
}
func catalogingProgress(locations int64) *monitor.CatalogerTaskProgress {
func catalogingProgress(locations int64) *monitor.TaskProgress {
info := monitor.GenericTask{
Title: monitor.Title{
Default: "File metadata",
@@ -37,6 +37,7 @@ const (
prefixImage = "Image"
prefixDirectory = "Directory"
prefixFile = "File"
prefixSnap = "Snap"
prefixUnknown = "Unknown"
)
@@ -228,6 +229,18 @@ func toRootPackage(s source.Description) *spdx.Package {
Value: d.Value,
})
}
case source.SnapMetadata:
prefix = prefixSnap
purpose = spdxPrimaryPurposeContainer
for _, d := range m.Digests {
checksums = append(checksums, spdx.Checksum{
Algorithm: toChecksumAlgorithm(d.Algorithm),
Value: d.Value,
})
}
default:
prefix = prefixUnknown
purpose = spdxPrimaryPurposeOther
@@ -237,6 +237,82 @@ func Test_toFormatModel(t *testing.T) {
},
},
},
{
name: "snap",
in: sbom.SBOM{
Source: source.Description{
Name: "etcd",
Version: "3.4.36",
Metadata: source.SnapMetadata{
Summary: "Distributed reliable key-value store",
Base: "core18",
Grade: "stable",
Confinement: "strict",
Architectures: []string{
"amd64",
},
Digests: []file.Digest{
{
Algorithm: "sha256",
Value: "d34db33f",
},
},
},
},
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(pkg.Package{
Name: "pkg-1",
Version: "version-1",
}),
},
},
expected: &spdx.Document{
SPDXIdentifier: "DOCUMENT",
SPDXVersion: spdx.Version,
DataLicense: spdx.DataLicense,
DocumentName: "etcd",
Packages: []*spdx.Package{
{
PackageSPDXIdentifier: "Package-pkg-1-pkg-1",
PackageName: "pkg-1",
PackageVersion: "version-1",
PackageSupplier: &spdx.Supplier{
Supplier: "NOASSERTION",
},
},
{
PackageSPDXIdentifier: "DocumentRoot-Snap-etcd",
PackageName: "etcd",
PackageVersion: "3.4.36",
PrimaryPackagePurpose: "CONTAINER",
PackageChecksums: []spdx.Checksum{{Algorithm: "SHA256", Value: "d34db33f"}},
PackageSupplier: &spdx.Supplier{
Supplier: "NOASSERTION",
},
},
},
Relationships: []*spdx.Relationship{
{
RefA: spdx.DocElementID{
ElementRefID: "DocumentRoot-Snap-etcd",
},
RefB: spdx.DocElementID{
ElementRefID: "Package-pkg-1-pkg-1",
},
Relationship: spdx.RelationshipContains,
},
{
RefA: spdx.DocElementID{
ElementRefID: "DOCUMENT",
},
RefB: spdx.DocElementID{
ElementRefID: "DocumentRoot-Snap-etcd",
},
Relationship: spdx.RelationshipDescribes,
},
},
},
},
}
for _, test := range tests {
@@ -10,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"
@@ -45,7 +46,7 @@ func ToSyftModel(doc *spdx.Document) (*sbom.SBOM, error) {
},
}
collectSyftPackages(s, spdxIDMap, doc.Packages)
collectSyftPackages(s, spdxIDMap, doc)
collectSyftFiles(s, spdxIDMap, doc)
@@ -279,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)
@@ -657,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
}
@@ -759,3 +759,37 @@ func Test_useSPDXIdentifierOverDerivedSyftArtifactID(t *testing.T) {
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"))
}
@@ -130,6 +130,11 @@ func toPath(s source.Description, p pkg.Package) string {
return fmt.Sprintf("%s/%s", path, packagePath)
}
return packagePath
case source.SnapMetadata:
if inputPath != "" {
return fmt.Sprintf("%s:/%s", inputPath, packagePath)
}
return packagePath
}
}
return inputPath
@@ -173,6 +173,11 @@ func Test_toGithubModel(t *testing.T) {
metadata: source.FileMetadata{Path: "./archive.tar.gz"},
testPath: "archive.tar.gz:/etc",
},
{
name: "snap",
metadata: source.SnapMetadata{},
testPath: "name:/etc",
},
}
for _, test := range tests {
@@ -180,6 +185,7 @@ func Test_toGithubModel(t *testing.T) {
s := sbomFixture()
if test.metadata != nil {
s.Source.Name = "name"
s.Source.Metadata = test.metadata
}
actual := ToGithubModel(&s)
@@ -45,6 +45,15 @@ func Test_DocumentName(t *testing.T) {
},
expected: "some/path/to/place",
},
{
name: "snap",
srcMetadata: source.Description{
Name: "some/name",
// there is nothing in the snap metadata that indicates a name
Metadata: source.SnapMetadata{},
},
expected: "some/name",
},
{
name: "named",
srcMetadata: source.Description{
@@ -16,6 +16,7 @@ const (
InputImage = "image"
InputDirectory = "dir"
InputFile = "file"
InputSnap = "snap"
)
func DocumentNameAndNamespace(src source.Description, desc sbom.Descriptor) (string, string) {
@@ -33,6 +34,8 @@ func DocumentNamespace(name string, src source.Description, desc sbom.Descriptor
input = InputDirectory
case source.FileMetadata:
input = InputFile
case source.SnapMetadata:
input = InputSnap
}
uniqueID := uuid.Must(uuid.NewRandom())
@@ -12,7 +12,7 @@ import (
"github.com/anchore/syft/syft/source"
)
func Test_documentNamespace(t *testing.T) {
func Test_DocumentNamespace(t *testing.T) {
tracker := sourcemetadata.NewCompletionTester(t)
tests := []struct {
@@ -53,6 +53,14 @@ func Test_documentNamespace(t *testing.T) {
},
expected: "https://anchore.com/syft/file/my-name-",
},
{
name: "snap",
inputName: "my-name",
src: source.Description{
Metadata: source.SnapMetadata{},
},
expected: "https://anchore.com/syft/snap/my-name-",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
+4 -4
View File
@@ -68,15 +68,15 @@ func AssertEncoderAgainstGoldenSnapshot(t *testing.T, cfg EncoderSnapshotTestCon
if cfg.IsJSON {
require.JSONEq(t, string(expected), string(actual))
} else {
requireEqual(t, expected, actual)
requireEqual(t, string(expected), string(actual))
}
}
func requireEqual(t *testing.T, expected any, actual any) {
func requireEqual(t *testing.T, expected string, actual string) {
if diff := cmp.Diff(expected, actual); diff != "" {
// uncomment to debug
// t.Logf("expected: %s", expected)
// t.Logf("actual: %s", actual)
// t.Logf("expected:\n%s", expected)
// t.Logf("actual:\n%s", actual)
t.Fatalf("mismatched output: %s", diff)
}
}
+2 -2
View File
@@ -114,12 +114,12 @@ func TestOutOfDateParser(t *testing.T) {
name: "warning when document version is unparseable",
documentVersion: "some-nonsense",
parserVersion: "3.1.0",
want: fmt.Errorf("error comparing document schema version with parser schema version: %w", errors.New("Invalid Semantic Version")),
want: fmt.Errorf("error comparing document schema version with parser schema version: %w", errors.New("invalid semantic version")),
}, {
name: "warning when parser version is unparseable",
documentVersion: "7.1.0",
parserVersion: "some-nonsense",
want: fmt.Errorf("error comparing document schema version with parser schema version: %w", errors.New("Invalid Semantic Version")),
want: fmt.Errorf("error comparing document schema version with parser schema version: %w", errors.New("invalid semantic version")),
}}
for _, tt := range tests {
+24
View File
@@ -1,5 +1,10 @@
package model
import (
"encoding/json"
"fmt"
)
// Document represents the syft cataloging findings as a JSON document
type Document struct {
Artifacts []Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
@@ -11,6 +16,25 @@ type Document struct {
Schema Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape
}
func (d *Document) UnmarshalJSON(data []byte) error {
type Alias *Document
aux := Alias(d)
if err := json.Unmarshal(data, aux); err != nil {
return fmt.Errorf("could not unmarshal syft JSON document: %w", err)
}
// in previous versions of anchorectl, the file modes were stored as decimal values instead of octal.
if d.Schema.Version == "1.0.0" && d.Descriptor.Name == "anchorectl" {
// convert all file modes from decimal to octal
for i := range d.Files {
d.Files[i].Metadata.Mode = convertBase10ToBase8(d.Files[i].Metadata.Mode)
}
}
return nil
}
// Descriptor describes what created the document as well as surrounding metadata
type Descriptor struct {
Name string `json:"name"`
@@ -0,0 +1,99 @@
package model
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDocumentUnmarshalJSON_SchemaDetection(t *testing.T) {
tests := []struct {
name string
jsonData string
modes []int
}{
{
name: "schema version 1.0.0 + anchorectl",
jsonData: `{
"files": [
{"metadata": {"mode": 493}},
{"metadata": {"mode": 420}}
],
"schema": {"version": "1.0.0"},
"descriptor": {
"name": "anchorectl"
}
}`,
modes: []int{755, 644},
},
{
name: "schema version 1.0.0 + syft",
jsonData: `{
"files": [
{"metadata": {"mode": 755}},
{"metadata": {"mode": 644}}
],
"schema": {"version": "1.0.0"},
"descriptor": {
"name": "syft"
}
}`,
modes: []int{755, 644},
},
{
name: "schema version 2.0.0 + anchorectl",
jsonData: `{
"files": [
{"metadata": {"mode": 755}},
{"metadata": {"mode": 644}}
],
"schema": {"version": "2.0.0"},
"descriptor": {
"name": "anchorectl"
}
}`,
modes: []int{755, 644},
},
{
name: "missing schema version should not convert modes",
jsonData: `{
"files": [
{"metadata": {"mode": 755}}
],
"schema": {}
}`,
modes: []int{755},
},
{
name: "empty files array with version 1.0.0",
jsonData: `{
"files": [],
"schema": {"version": "1.0.0"},
"descriptor": {
"name": "anchorectl"
}
}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var doc Document
err := json.Unmarshal([]byte(tt.jsonData), &doc)
if err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
var modes []int
for _, file := range doc.Files {
modes = append(modes, file.Metadata.Mode)
}
require.Len(t, doc.Files, len(tt.modes), "Unexpected number of files")
assert.Equal(t, tt.modes, modes, "File modes do not match expected values")
})
}
}
+92
View File
@@ -1,6 +1,11 @@
package model
import (
"encoding/json"
"fmt"
"strconv"
stereoFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/license"
)
@@ -26,6 +31,57 @@ type FileMetadataEntry struct {
Size int64 `json:"size"`
}
type auxFileMetadataEntry FileMetadataEntry
type fileMetadataEntryWithLegacyHint struct {
*auxFileMetadataEntry `json:",inline"`
LegacyHint any `json:"FileInfo"`
}
func (f *FileMetadataEntry) UnmarshalJSON(data []byte) error {
aux := fileMetadataEntryWithLegacyHint{
auxFileMetadataEntry: (*auxFileMetadataEntry)(f),
}
if err := json.Unmarshal(data, &aux); err == nil {
fieldsSpecified := f.Mode != 0 || f.Type != "" || f.LinkDestination != "" ||
f.UserID != 0 || f.GroupID != 0 || f.MIMEType != "" || f.Size != 0
if aux.LegacyHint == nil && fieldsSpecified {
// we should have at least one field set to a non-zero value... (this is not a legacy shape)
return nil
}
}
var legacy sbomImportLegacyFileMetadataEntry
if err := json.Unmarshal(data, &legacy); err != nil {
return err
}
if !legacy.Type.WasInt {
// this occurs for document shapes from a non-import path and indicates that the mode has already been converted to octal.
// That being said, we want to handle all legacy shapes the same, so we will convert this to base 10 for consistency.
legacy.Mode = convertBase8ToBase10(legacy.Mode)
}
f.Mode = legacy.Mode
f.Type = legacy.Type.Value
f.LinkDestination = legacy.LinkDestination
f.UserID = legacy.UserID
f.GroupID = legacy.GroupID
f.MIMEType = legacy.MIMEType
f.Size = legacy.Size
return nil
}
type sbomImportLegacyFileMetadataEntry struct {
Mode int `json:"Mode"`
Type intOrStringFileType `json:"Type"`
LinkDestination string `json:"LinkDestination"`
UserID int `json:"UserID"`
GroupID int `json:"GroupID"`
MIMEType string `json:"MIMEType"`
Size int64 `json:"Size"`
}
type FileLicense struct {
Value string `json:"value"`
SPDXExpression string `json:"spdxExpression"`
@@ -38,3 +94,39 @@ type FileLicenseEvidence struct {
Offset int `json:"offset"`
Extent int `json:"extent"`
}
type intOrStringFileType struct {
Value string
WasInt bool
}
func (lt *intOrStringFileType) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err == nil {
lt.Value = str
return nil
}
var num stereoFile.Type
if err := json.Unmarshal(data, &num); err != nil {
return fmt.Errorf("file.Type must be either string or int, got: %s", string(data))
}
lt.Value = num.String()
lt.WasInt = true
return nil
}
func convertBase10ToBase8(rawMode int) int {
octalStr := fmt.Sprintf("%o", rawMode)
// we don't need to check that this is a valid octal string since the input is always an integer
result, _ := strconv.Atoi(octalStr)
return result
}
func convertBase8ToBase10(octalMode int) int {
octalStr := strconv.Itoa(octalMode)
result, _ := strconv.ParseInt(octalStr, 8, 64)
return int(result)
}
+320
View File
@@ -0,0 +1,320 @@
package model
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_FileMetadataEntry_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
jsonData []byte
expected FileMetadataEntry
}{
{
name: "unmarshal current format",
jsonData: []byte(`{
"mode": 644,
"type": "RegularFile",
"linkDestination": "/usr/bin/python3",
"userID": 1000,
"groupID": 1000,
"mimeType": "text/plain",
"size": 10174
}`),
expected: FileMetadataEntry{
Mode: 644,
Type: "RegularFile",
LinkDestination: "/usr/bin/python3",
UserID: 1000,
GroupID: 1000,
MIMEType: "text/plain",
Size: 10174,
},
},
{
name: "unmarshal legacy image add internal document format",
jsonData: []byte(`{
"FileInfo": {},
"Mode": 644,
"Type": "RegularFile",
"LinkDestination": "/usr/bin/python3",
"UserID": 1000,
"GroupID": 1000,
"MIMEType": "text/plain",
"Size": 10174
}`),
expected: FileMetadataEntry{
Mode: 420, // important! we convert this to base 10 so that all documents are consistent
Type: "RegularFile",
LinkDestination: "/usr/bin/python3",
UserID: 1000,
GroupID: 1000,
MIMEType: "text/plain",
Size: 10174,
},
},
{
name: "unmarshal legacy sbom import format",
jsonData: []byte(`{
"FileInfo": {},
"Mode": 644,
"Type": 0,
"LinkDestination": "/usr/bin/python3",
"UserID": 1000,
"GroupID": 1000,
"MIMEType": "text/plain",
"Size": 10174
}`),
expected: FileMetadataEntry{
Mode: 644,
Type: "RegularFile",
LinkDestination: "/usr/bin/python3",
UserID: 1000,
GroupID: 1000,
MIMEType: "text/plain",
Size: 10174,
},
},
{
name: "unmarshal minimal current format",
jsonData: []byte(`{
"mode": 0,
"type": "RegularFile",
"userID": 0,
"groupID": 0,
"size": 0
}`),
expected: FileMetadataEntry{
Type: "RegularFile",
},
},
{
name: "unmarshal minimal legacy format",
jsonData: []byte(`{
"FileInfo": {},
"Mode": 0,
"Type": "RegularFile",
"UserID": 0,
"GroupID": 0,
"Size": 0
}`),
expected: FileMetadataEntry{
Type: "RegularFile",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var actual FileMetadataEntry
err := actual.UnmarshalJSON(test.jsonData)
require.NoError(t, err)
if diff := cmp.Diff(test.expected, actual); diff != "" {
t.Errorf("FileMetadataEntry mismatch (-expected +actual):\n%s", diff)
}
})
}
}
func Test_intOrStringFileType_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
jsonData []byte
expected string
expectedWasInt bool
wantErr require.ErrorAssertionFunc
}{
// string inputs - should pass through unchanged
{
name: "string RegularFile",
jsonData: []byte(`"RegularFile"`),
expected: "RegularFile",
},
{
name: "string HardLink",
jsonData: []byte(`"HardLink"`),
expected: "HardLink",
},
{
name: "string Directory",
jsonData: []byte(`"Directory"`),
expected: "Directory",
},
{
name: "string custom value",
jsonData: []byte(`"CustomFileType"`),
expected: "CustomFileType",
},
// integer inputs - should convert to string representation
{
name: "int 0 (TypeRegular)",
jsonData: []byte(`0`),
expected: "RegularFile",
expectedWasInt: true,
},
{
name: "int 1 (TypeHardLink)",
jsonData: []byte(`1`),
expected: "HardLink",
expectedWasInt: true,
},
{
name: "int 2 (TypeSymLink)",
jsonData: []byte(`2`),
expected: "SymbolicLink",
expectedWasInt: true,
},
{
name: "int 3 (TypeCharacterDevice)",
jsonData: []byte(`3`),
expected: "CharacterDevice",
expectedWasInt: true,
},
{
name: "int 4 (TypeBlockDevice)",
jsonData: []byte(`4`),
expected: "BlockDevice",
expectedWasInt: true,
},
{
name: "int 5 (TypeDirectory)",
jsonData: []byte(`5`),
expected: "Directory",
expectedWasInt: true,
},
{
name: "int 6 (TypeFIFO)",
jsonData: []byte(`6`),
expected: "FIFONode",
expectedWasInt: true,
},
{
name: "int 7 (TypeSocket)",
jsonData: []byte(`7`),
expected: "Socket",
expectedWasInt: true,
},
{
name: "int 8 (TypeIrregular)",
jsonData: []byte(`8`),
expected: "IrregularFile",
expectedWasInt: true,
},
{
name: "unknown int",
jsonData: []byte(`99`),
expected: "Unknown",
expectedWasInt: true,
},
{
name: "negative int",
jsonData: []byte(`-1`),
expected: "Unknown",
expectedWasInt: true,
},
{
name: "null value",
jsonData: []byte(`null`),
},
{
name: "invalid JSON",
jsonData: []byte(`{`),
wantErr: require.Error,
},
{
name: "boolean value",
jsonData: []byte(`true`),
wantErr: require.Error,
},
{
name: "array value",
jsonData: []byte(`[]`),
wantErr: require.Error,
},
{
name: "object value",
jsonData: []byte(`{}`),
wantErr: require.Error,
},
{
name: "float value",
jsonData: []byte(`1.5`),
wantErr: require.Error,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.wantErr == nil {
test.wantErr = require.NoError
}
var ft intOrStringFileType
err := ft.UnmarshalJSON(test.jsonData)
test.wantErr(t, err)
if err != nil {
return
}
assert.Equal(t, test.expected, ft.Value)
assert.Equal(t, test.expectedWasInt, ft.WasInt)
})
}
}
func Test_convertBase10ToBase8(t *testing.T) {
tests := []struct {
name string
input int
expected int
}{
{
name: "no permissions",
input: 0,
expected: 0,
},
{
name: "symlink + rwxrwxrwx",
input: 134218239,
expected: 1000000777,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := convertBase10ToBase8(tt.input)
require.Equal(t, tt.expected, actual)
})
}
}
func Test_convertBase8ToBase10(t *testing.T) {
tests := []struct {
name string
input int
expected int
}{
{
name: "no permissions",
input: 0,
expected: 0,
},
{
name: "symlink + rwxrwxrwx",
input: 1000000777,
expected: 134218239,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := convertBase8ToBase10(tt.input)
require.Equal(t, tt.expected, actual)
})
}
}
+45
View File
@@ -145,6 +145,51 @@ func TestSource_UnmarshalJSON(t *testing.T) {
},
},
},
{
name: "snap",
input: []byte(`{
"id": "foobar",
"name": "etcd",
"version": "3.4.36",
"type": "snap",
"metadata": {
"summary": "Distributed reliable key-value store",
"base": "core18",
"grade": "stable",
"confinement": "strict",
"architectures": [
"amd64"
],
"digests": [
{
"algorithm": "sha256",
"value": "6700d789d2c38b0f7513058ddcea8f9a275e2206b4621a772eb065e12069956e"
}
]
}
}`),
expected: &Source{
ID: "foobar",
Name: "etcd",
Version: "3.4.36",
Type: "snap",
Metadata: source.SnapMetadata{
Summary: "Distributed reliable key-value store",
Base: "core18",
Grade: "stable",
Confinement: "strict",
Architectures: []string{
"amd64",
},
Digests: []file.Digest{
{
Algorithm: "sha256",
Value: "6700d789d2c38b0f7513058ddcea8f9a275e2206b4621a772eb065e12069956e",
},
},
},
},
},
{
name: "unknown source type",
input: []byte(`{
@@ -127,6 +127,36 @@ func Test_toSourceModel(t *testing.T) {
},
},
},
{
name: "snap",
src: source.Description{
ID: "test-id",
Name: "some-name",
Version: "some-version",
Metadata: source.SnapMetadata{
Summary: "some summary",
Base: "some/base",
Grade: "some grade",
Confinement: "some confinement",
Architectures: []string{"x86_64", "arm64"},
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
},
},
expected: model.Source{
ID: "test-id",
Name: "some-name",
Version: "some-version",
Type: "snap",
Metadata: source.SnapMetadata{
Summary: "some summary",
Base: "some/base",
Grade: "some grade",
Confinement: "some confinement",
Architectures: []string{"x86_64", "arm64"},
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
},
},
},
// below are regression tests for when the name/version are not provided
// historically we've hoisted up the name/version from the metadata, now it is a simple pass-through
{
+3 -4
View File
@@ -144,13 +144,12 @@ func toSyftFiles(files []model.File) sbom.Artifacts {
}
func safeFileModeConvert(val int) (fs.FileMode, error) {
if val < math.MinInt32 || val > math.MaxInt32 {
// Value is out of the range that int32 can represent
mode, err := strconv.ParseInt(strconv.Itoa(val), 8, 64)
if mode < 0 || mode > math.MaxUint32 {
// value is out of the range that int32 can represent
return 0, fmt.Errorf("value %d is out of the range that int32 can represent", val)
}
// Safe to convert to os.FileMode
mode, err := strconv.ParseInt(strconv.Itoa(val), 8, 64)
if err != nil {
return 0, err
}
@@ -100,6 +100,36 @@ func Test_toSyftSourceData(t *testing.T) {
},
},
},
{
name: "snap",
src: model.Source{
ID: "the-id",
Name: "some-name",
Version: "some-version",
Type: "snap",
Metadata: source.SnapMetadata{
Summary: "something!",
Base: "base!",
Grade: "grade!",
Confinement: "confined!",
Architectures: []string{"arch!"},
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest!"}},
},
},
expected: &source.Description{
ID: "the-id",
Name: "some-name",
Version: "some-version",
Metadata: source.SnapMetadata{
Summary: "something!",
Base: "base!",
Grade: "grade!",
Confinement: "confined!",
Architectures: []string{"arch!"},
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest!"}},
},
},
},
// below are regression tests for when the name/version are not provided
// historically we've hoisted up the name/version from the metadata, now it is a simple pass-through
{
@@ -491,6 +521,12 @@ func Test_safeFileModeConvert(t *testing.T) {
want: os.FileMode(511), // 777 in octal equals 511 in decimal
wantErr: false,
},
{
name: "valid perm with symlink type",
val: 1000000777, // symlink + rwxrwxrwx
want: os.FileMode(0o1000000777), // 134218239
wantErr: false,
},
{
name: "outside int32 high",
val: int(math.MaxInt32) + 1,
+83 -16
View File
@@ -8,6 +8,8 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"github.com/anchore/syft/syft/sbom"
)
@@ -66,25 +68,90 @@ func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error {
columns = append(columns, "") // add a column for duplicate annotations
rows = markDuplicateRows(rows)
table := tablewriter.NewWriter(writer)
table := newTableWriter(writer, columns)
table.SetHeader(columns)
table.SetHeaderLine(false)
table.SetBorder(false)
table.SetAutoWrapText(false)
table.SetAutoFormatHeaders(true)
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetTablePadding(" ")
table.SetNoWhiteSpace(true)
if err := table.Bulk(rows); err != nil {
return fmt.Errorf("failed to add table rows: %w", err)
}
table.AppendBulk(rows)
table.Render()
return table.Render()
}
return nil
func newTableWriter(writer io.Writer, columns []string) *tablewriter.Table {
// Here’s a simplified diagram of a table with a header, rows, and footer:
//
// [Borders.Top]
// | Header1 | Header2 | (Line below header: Lines.ShowTop)
// [Separators.BetweenRows]
// | Row1 | Row1 |
// [Separators.BetweenRows]
// | Row2 | Row2 |
// [Lines.ShowBottom]
// | Footer1 | Footer2 |
// [Borders.Bottom]
//
// So for example:
// ┌──────┬─────┐ <- Borders.Top
// │ NAME │ AGE │
// ├──────┼─────┤ <- Lines.ShowTop
// │ Alice│ 25 │
// ├──────┼─────┤ <- Separators.BetweenRows
// │ Bob │ 30 │
// ├──────┼─────┤ <- Lines.ShowBottom
// │ Total│ 2 │
// └──────┴─────┘ <- Borders.Bottom
return tablewriter.NewTable(writer,
tablewriter.WithHeader(columns),
tablewriter.WithHeaderAutoFormat(tw.On),
tablewriter.WithHeaderAutoWrap(tw.WrapNone),
tablewriter.WithHeaderAlignment(tw.AlignLeft),
tablewriter.WithRowAutoFormat(tw.Off),
tablewriter.WithRowAutoWrap(tw.WrapNone),
tablewriter.WithRowAlignment(tw.AlignLeft),
tablewriter.WithTrimSpace(tw.On),
tablewriter.WithAutoHide(tw.On),
tablewriter.WithRenderer(renderer.NewBlueprint()),
tablewriter.WithBehavior(
tw.Behavior{
TrimSpace: tw.On,
AutoHide: tw.On,
},
),
tablewriter.WithPadding(
tw.Padding{
Left: "",
Right: " ",
Top: "",
Bottom: "",
},
),
tablewriter.WithRendition(
tw.Rendition{
Symbols: tw.NewSymbols(tw.StyleNone),
Borders: tw.Border{
Left: tw.Off,
Top: tw.Off,
Right: tw.Off,
Bottom: tw.Off,
},
Settings: tw.Settings{
Separators: tw.Separators{
ShowHeader: tw.Off,
ShowFooter: tw.Off,
BetweenRows: tw.Off,
BetweenColumns: tw.Off,
},
Lines: tw.Lines{
ShowTop: tw.Off,
ShowBottom: tw.Off,
ShowHeaderLine: tw.Off,
ShowFooterLine: tw.Off,
},
},
},
),
)
}
func markDuplicateRows(items [][]string) [][]string {
@@ -1,3 +1,3 @@
NAME VERSION TYPE
package-1 1.0.1 python
package-2 2.0.1 deb
NAME VERSION TYPE
package-1 1.0.1 python
package-2 2.0.1 deb
+27 -9
View File
@@ -34,26 +34,44 @@ func GetSource(ctx context.Context, userInput string, cfg *GetSourceConfig) (sou
errs = append(errs, fmt.Errorf("%s: %w", p.Name(), err))
}
}
if err := validateSourcePlatform(src, cfg); err != nil {
return nil, err
}
if src != nil {
// if we have a non-image type and platform is specified, it's an error
if cfg.SourceProviderConfig.Platform != nil {
meta := src.Describe().Metadata
switch meta.(type) {
case *source.ImageMetadata, source.ImageMetadata:
default:
return src, fmt.Errorf("platform specified with non-image source")
}
}
return src, nil
}
}
if len(errs) == 0 {
return nil, fmt.Errorf("no source providers were able to resolve the input %q", userInput)
}
if len(fileNotFoundProviders) > 0 {
errs = append(errs, fmt.Errorf("additionally, the following providers failed with %w: %s", os.ErrNotExist, strings.Join(fileNotFoundProviders, ", ")))
}
return nil, sourceError(userInput, errs...)
}
func validateSourcePlatform(src source.Source, cfg *GetSourceConfig) error {
if src == nil {
return nil
}
if cfg == nil || cfg.SourceProviderConfig == nil || cfg.SourceProviderConfig.Platform == nil {
return nil
}
meta := src.Describe().Metadata
switch meta.(type) {
case *source.ImageMetadata, source.ImageMetadata:
return nil
case *source.SnapMetadata, source.SnapMetadata:
return nil
default:
return fmt.Errorf("platform is not supported for this source type")
}
}
func sourceError(userInput string, errs ...error) error {
switch len(errs) {
case 0:
+208
View File
@@ -0,0 +1,208 @@
package syft
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/syft/internal/sourcemetadata"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/sourceproviders"
)
type mockSource struct {
source.Source
desc source.Description
}
func (s mockSource) Describe() source.Description {
return s.desc
}
func TestValidateSourcePlatform_NilSource(t *testing.T) {
cfg := &GetSourceConfig{
SourceProviderConfig: &sourceproviders.Config{
Platform: &image.Platform{
Architecture: "amd64",
OS: "linux",
},
},
}
err := validateSourcePlatform(nil, cfg)
if err != nil {
t.Errorf("Expected no error for nil source, got: %v", err)
}
}
func TestValidateSourcePlatform_NilPlatformConfig(t *testing.T) {
tests := []struct {
name string
cfg *GetSourceConfig
}{
{
name: "nil config",
cfg: nil,
},
{
name: "nil SourceProviderConfig",
cfg: &GetSourceConfig{
SourceProviderConfig: nil,
},
},
{
name: "nil Platform",
cfg: &GetSourceConfig{
SourceProviderConfig: &sourceproviders.Config{
Platform: nil,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
src := mockSource{
desc: source.Description{
Metadata: &source.ImageMetadata{},
},
}
err := validateSourcePlatform(src, tt.cfg)
if err != nil {
t.Errorf("Expected no error for nil platform, got: %v", err)
}
})
}
}
func TestValidateSourcePlatform_SupportedMetadataTypes(t *testing.T) {
tracker := sourcemetadata.NewCompletionTester(t)
cfg := &GetSourceConfig{
SourceProviderConfig: &sourceproviders.Config{
Platform: &image.Platform{
Architecture: "amd64",
OS: "linux",
},
},
}
tests := []struct {
name string
metadata any
wantErr require.ErrorAssertionFunc
}{
{
name: "image",
metadata: source.ImageMetadata{},
},
{
name: "snap",
metadata: source.SnapMetadata{},
},
{
name: "dir",
metadata: source.DirectoryMetadata{},
wantErr: require.Error,
},
{
name: "file",
metadata: source.FileMetadata{},
wantErr: require.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = require.NoError
}
tracker.Tested(t, tt.metadata)
src := mockSource{
desc: source.Description{
Metadata: tt.metadata,
},
}
err := validateSourcePlatform(src, cfg)
tt.wantErr(t, err, "Expected no error for %s, got: %v", tt.name, err)
})
}
}
func TestValidateSourcePlatform_UnsupportedMetadataTypes(t *testing.T) {
cfg := &GetSourceConfig{
SourceProviderConfig: &sourceproviders.Config{
Platform: &image.Platform{
Architecture: "amd64",
OS: "linux",
},
},
}
tests := []struct {
name string
metadata interface{}
}{
{
name: "string metadata",
metadata: "unsupported",
},
{
name: "int metadata",
metadata: 42,
},
{
name: "nil metadata",
metadata: nil,
},
{
name: "custom struct",
metadata: struct{ Name string }{Name: "test"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
src := mockSource{
desc: source.Description{
Metadata: tt.metadata,
},
}
err := validateSourcePlatform(src, cfg)
if err == nil {
t.Errorf("Expected error for %s, got nil", tt.name)
}
expectedMsg := "platform is not supported for this source type"
if err.Error() != expectedMsg {
t.Errorf("Expected error message %q, got %q", expectedMsg, err.Error())
}
})
}
}
func TestValidateSourcePlatform_ValidCombination(t *testing.T) {
cfg := &GetSourceConfig{
SourceProviderConfig: &sourceproviders.Config{
Platform: &image.Platform{
Architecture: "amd64",
OS: "linux",
},
},
}
src := mockSource{
desc: source.Description{
Metadata: &source.ImageMetadata{},
},
}
err := validateSourcePlatform(src, cfg)
if err != nil {
t.Errorf("Expected no error for valid combination, got: %v", err)
}
}
+9 -8
View File
@@ -14,7 +14,7 @@ var _ file.Resolver = (*Directory)(nil)
// Directory implements path and content access for the directory data source.
type Directory struct {
filetreeResolver
FiletreeResolver
path string
indexer *directoryIndexer
}
@@ -39,10 +39,11 @@ func newFromDirectoryWithoutIndex(root string, base string, pathFilters ...PathI
return &Directory{
path: cleanRoot,
filetreeResolver: filetreeResolver{
chroot: *chroot,
tree: filetree.New(),
index: filetree.NewIndex(),
FiletreeResolver: FiletreeResolver{
Chroot: *chroot,
Tree: filetree.New(),
Index: filetree.NewIndex(),
Opener: nativeOSFileOpener,
},
indexer: newDirectoryIndexer(cleanRoot, cleanBase, pathFilters...),
}, nil
@@ -57,9 +58,9 @@ func (r *Directory) buildIndex() error {
return err
}
r.tree = tree
r.index = index
r.searchContext = filetree.NewSearchContext(tree, index)
r.Tree = tree
r.Index = index
r.SearchContext = filetree.NewSearchContext(tree, index)
return nil
}
@@ -9,14 +9,12 @@ import (
"path/filepath"
"strings"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/internal/windows"
)
@@ -64,14 +62,14 @@ func (r *directoryIndexer) build() (filetree.Reader, filetree.IndexReader, error
return r.tree, r.index, indexAllRoots(r.path, r.indexTree)
}
func indexAllRoots(root string, indexer func(string, *progress.Stage) ([]string, error)) error {
func indexAllRoots(root string, indexer func(string, *progress.AtomicStage) ([]string, error)) error {
// why account for multiple roots? To cover cases when there is a symlink that references above the root path,
// in which case we need to additionally index where the link resolves to. it's for this reason why the filetree
// must be relative to the root of the filesystem (and not just relative to the given path).
pathsToIndex := []string{root}
fullPathsMap := map[string]struct{}{}
stager, prog := indexingProgress(root)
prog := bus.StartIndexingFiles(root)
defer prog.SetCompleted()
loop:
for {
@@ -85,7 +83,7 @@ loop:
currentPath, pathsToIndex = pathsToIndex[0], pathsToIndex[1:]
}
additionalRoots, err := indexer(currentPath, stager)
additionalRoots, err := indexer(currentPath, prog.AtomicStage)
if err != nil {
return fmt.Errorf("unable to index filesystem path=%q: %w", currentPath, err)
}
@@ -101,7 +99,7 @@ loop:
return nil
}
func (r *directoryIndexer) indexTree(root string, stager *progress.Stage) ([]string, error) {
func (r *directoryIndexer) indexTree(root string, stager *progress.AtomicStage) ([]string, error) {
log.WithFields("path", root).Trace("indexing filetree")
var roots []string
@@ -144,7 +142,7 @@ func (r *directoryIndexer) indexTree(root string, stager *progress.Stage) ([]str
err = filepath.Walk(root,
func(path string, info os.FileInfo, err error) error {
stager.Current = path
stager.Set(path)
newRoot, err := r.indexPath(path, info, err)
@@ -179,7 +177,7 @@ func isRealPath(root string) (bool, error) {
return rootParent == realRootParent, nil
}
func (r *directoryIndexer) indexBranch(root string, stager *progress.Stage) ([]string, error) {
func (r *directoryIndexer) indexBranch(root string, stager *progress.AtomicStage) ([]string, error) {
rootRealPath, err := filepath.EvalSymlinks(root)
if err != nil {
var pathErr *os.PathError
@@ -213,7 +211,7 @@ func (r *directoryIndexer) indexBranch(root string, stager *progress.Stage) ([]s
targetPath = p
}
stager.Current = targetPath
stager.Set(targetPath)
lstat, err := os.Lstat(targetPath)
newRoot, err := r.indexPath(targetPath, lstat, err)
@@ -492,22 +490,3 @@ func requireFileInfo(_, _ string, info os.FileInfo, _ error) error {
}
return nil
}
func indexingProgress(path string) (*progress.Stage, *progress.Manual) {
stage := &progress.Stage{}
prog := progress.NewManual(-1)
bus.Publish(partybus.Event{
Type: event.FileIndexingStarted,
Source: path,
Value: struct {
progress.Stager
progress.Progressable
}{
Stager: progress.Stager(stage),
Progressable: prog,
},
})
return stage, prog
}
@@ -25,7 +25,7 @@ type indexerMock struct {
additionalRoots map[string][]string
}
func (m *indexerMock) indexer(s string, _ *progress.Stage) ([]string, error) {
func (m *indexerMock) indexer(s string, _ *progress.AtomicStage) ([]string, error) {
m.observedRoots = append(m.observedRoots, s)
return m.additionalRoots[s], nil
}
+9 -8
View File
@@ -12,7 +12,7 @@ var _ file.Resolver = (*File)(nil)
// File implements path and content access for the file data source.
type File struct {
filetreeResolver
FiletreeResolver
path string
indexer *fileIndexer
}
@@ -29,10 +29,11 @@ func NewFromFile(parent, path string, pathFilters ...PathIndexVisitor) (*File, e
file := &File{
path: path,
filetreeResolver: filetreeResolver{
chroot: *chroot,
tree: filetree.New(),
index: filetree.NewIndex(),
FiletreeResolver: FiletreeResolver{
Chroot: *chroot,
Tree: filetree.New(),
Index: filetree.NewIndex(),
Opener: nativeOSFileOpener,
},
indexer: newFileIndexer(path, cleanBase, pathFilters...),
}
@@ -49,9 +50,9 @@ func (r *File) buildIndex() error {
return err
}
r.tree = tree
r.index = index
r.searchContext = filetree.NewSearchContext(tree, index)
r.Tree = tree
r.Index = index
r.SearchContext = filetree.NewSearchContext(tree, index)
return nil
}
+7 -6
View File
@@ -9,6 +9,7 @@ import (
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/internal/windows"
)
@@ -49,14 +50,14 @@ func (r *fileIndexer) build() (filetree.Reader, filetree.IndexReader, error) {
// Index file at the given path
// A file indexer simply indexes the file and its directory.
func index(path string, indexer func(string, *progress.Stage) error) error {
func index(path string, indexer func(string, *progress.AtomicStage) error) error {
// We want to index the file at the provided path and its parent directory.
// We need to probably check that we have file access
// We also need to determine what to do when the file itself is a symlink.
stager, prog := indexingProgress(path)
prog := bus.StartIndexingFiles(path)
defer prog.SetCompleted()
err := indexer(path, stager)
err := indexer(path, prog.AtomicStage)
if err != nil {
return fmt.Errorf("unable to index filesystem path=%q: %w", path, err)
}
@@ -70,7 +71,7 @@ func index(path string, indexer func(string, *progress.Stage) error) error {
// permissions errors on the file at path or its parent directory will return an error.
// Filter functions provided to the indexer are honoured, so if the path provided (or its parent
// directory) is filtered by a filter function, an error is returned.
func (r *fileIndexer) indexPath(path string, stager *progress.Stage) error {
func (r *fileIndexer) indexPath(path string, stager *progress.AtomicStage) error {
log.WithFields("path", path).Trace("indexing file path")
absPath, err := filepath.Abs(path)
@@ -105,14 +106,14 @@ func (r *fileIndexer) indexPath(path string, stager *progress.Stage) error {
return fmt.Errorf("unable to stat parent of file=%q: %w", absSymlinkFreeParent, err)
}
stager.Current = absSymlinkFreeParent
stager.Set(absSymlinkFreeParent)
indexParentErr := r.filterAndIndex(absSymlinkFreeParent, parentFi)
if indexParentErr != nil {
return indexParentErr
}
// We have indexed the parent successfully, now attempt to index the file.
stager.Current = absSymlinkFreeFilePath
stager.Set(absSymlinkFreeFilePath)
indexFileErr := r.filterAndIndex(absSymlinkFreeFilePath, fi)
if indexFileErr != nil {
return indexFileErr
+42 -36
View File
@@ -14,33 +14,46 @@ import (
"github.com/anchore/syft/syft/internal/windows"
)
type filetreeResolver struct {
chroot ChrootContext
tree filetree.Reader
index filetree.IndexReader
searchContext filetree.Searcher
// TODO: consider making a constructor for this
type FiletreeResolver struct {
Chroot ChrootContext
Tree filetree.Reader
Index filetree.IndexReader
SearchContext filetree.Searcher
Opener func(stereoscopeFile.Reference) (io.ReadCloser, error)
}
func (r *filetreeResolver) requestPath(userPath string) (string, error) {
return r.chroot.ToNativePath(userPath)
func nativeOSFileOpener(ref stereoscopeFile.Reference) (io.ReadCloser, error) {
// RealPath is posix so for windows file resolver we need to translate
// to its true on disk path.
filePath := string(ref.RealPath)
if windows.HostRunningOnWindows() {
filePath = windows.FromPosix(filePath)
}
return stereoscopeFile.NewLazyReadCloser(filePath), nil
}
func (r *FiletreeResolver) requestPath(userPath string) (string, error) {
return r.Chroot.ToNativePath(userPath)
}
// responsePath takes a path from the underlying fs domain and converts it to a path that is relative to the root of the file resolver.
func (r filetreeResolver) responsePath(path string) string {
return r.chroot.ToChrootPath(path)
func (r FiletreeResolver) responsePath(path string) string {
return r.Chroot.ToChrootPath(path)
}
// HasPath indicates if the given path exists in the underlying source.
func (r *filetreeResolver) HasPath(userPath string) bool {
func (r *FiletreeResolver) HasPath(userPath string) bool {
requestPath, err := r.requestPath(userPath)
if err != nil {
return false
}
return r.tree.HasPath(stereoscopeFile.Path(requestPath))
return r.Tree.HasPath(stereoscopeFile.Path(requestPath))
}
// FilesByPath returns all file.References that match the given paths from the file index.
func (r filetreeResolver) FilesByPath(userPaths ...string) ([]file.Location, error) {
func (r FiletreeResolver) FilesByPath(userPaths ...string) ([]file.Location, error) {
var references = make([]file.Location, 0)
for _, userPath := range userPaths {
@@ -51,7 +64,7 @@ func (r filetreeResolver) FilesByPath(userPaths ...string) ([]file.Location, err
}
// we should be resolving symlinks and preserving this information as a AccessPath to the real file
ref, err := r.searchContext.SearchByPath(userStrPath, filetree.FollowBasenameLinks)
ref, err := r.SearchContext.SearchByPath(userStrPath, filetree.FollowBasenameLinks)
if err != nil {
log.Tracef("unable to evaluate symlink for path=%q : %+v", userPath, err)
continue
@@ -61,7 +74,7 @@ func (r filetreeResolver) FilesByPath(userPaths ...string) ([]file.Location, err
continue
}
entry, err := r.index.Get(*ref.Reference)
entry, err := r.Index.Get(*ref.Reference)
if err != nil {
log.Warnf("unable to get file by path=%q : %+v", userPath, err)
continue
@@ -90,12 +103,12 @@ func (r filetreeResolver) FilesByPath(userPaths ...string) ([]file.Location, err
return references, nil
}
func (r filetreeResolver) requestGlob(pattern string) (string, error) {
return r.chroot.ToNativeGlob(pattern)
func (r FiletreeResolver) requestGlob(pattern string) (string, error) {
return r.Chroot.ToNativeGlob(pattern)
}
// FilesByGlob returns all file.References that match the given path glob pattern from any layer in the image.
func (r filetreeResolver) FilesByGlob(patterns ...string) ([]file.Location, error) {
func (r FiletreeResolver) FilesByGlob(patterns ...string) ([]file.Location, error) {
uniqueFileIDs := stereoscopeFile.NewFileReferenceSet()
uniqueLocations := make([]file.Location, 0)
@@ -104,7 +117,7 @@ func (r filetreeResolver) FilesByGlob(patterns ...string) ([]file.Location, erro
if err != nil {
return nil, err
}
refVias, err := r.searchContext.SearchByGlob(requestGlob, filetree.FollowBasenameLinks)
refVias, err := r.SearchContext.SearchByGlob(requestGlob, filetree.FollowBasenameLinks)
if err != nil {
return nil, err
}
@@ -112,7 +125,7 @@ func (r filetreeResolver) FilesByGlob(patterns ...string) ([]file.Location, erro
if !refVia.HasReference() || uniqueFileIDs.Contains(*refVia.Reference) {
continue
}
entry, err := r.index.Get(*refVia.Reference)
entry, err := r.Index.Get(*refVia.Reference)
if err != nil {
return nil, fmt.Errorf("unable to get file metadata for reference %s: %w", refVia.RealPath, err)
}
@@ -137,7 +150,7 @@ func (r filetreeResolver) FilesByGlob(patterns ...string) ([]file.Location, erro
// RelativeFileByPath fetches a single file at the given path relative to the layer squash of the given reference.
// This is helpful when attempting to find a file that is in the same layer or lower as another file.
func (r *filetreeResolver) RelativeFileByPath(_ file.Location, path string) *file.Location {
func (r *FiletreeResolver) RelativeFileByPath(_ file.Location, path string) *file.Location {
paths, err := r.FilesByPath(path)
if err != nil {
return nil
@@ -151,12 +164,12 @@ func (r *filetreeResolver) RelativeFileByPath(_ file.Location, path string) *fil
// FileContentsByLocation fetches file contents for a single file reference relative to a directory.
// If the path does not exist an error is returned.
func (r filetreeResolver) FileContentsByLocation(location file.Location) (io.ReadCloser, error) {
func (r FiletreeResolver) FileContentsByLocation(location file.Location) (io.ReadCloser, error) {
if location.RealPath == "" {
return nil, errors.New("empty path given")
}
entry, err := r.index.Get(location.Reference())
entry, err := r.Index.Get(location.Reference())
if err != nil {
return nil, err
}
@@ -166,21 +179,14 @@ func (r filetreeResolver) FileContentsByLocation(location file.Location) (io.Rea
return nil, fmt.Errorf("cannot read contents of non-file %q", location.Reference().RealPath)
}
// RealPath is posix so for windows file resolver we need to translate
// to its true on disk path.
filePath := string(location.Reference().RealPath)
if windows.HostRunningOnWindows() {
filePath = windows.FromPosix(filePath)
}
return stereoscopeFile.NewLazyReadCloser(filePath), nil
return r.Opener(location.Reference())
}
func (r *filetreeResolver) AllLocations(ctx context.Context) <-chan file.Location {
func (r *FiletreeResolver) AllLocations(ctx context.Context) <-chan file.Location {
results := make(chan file.Location)
go func() {
defer close(results)
for _, ref := range r.tree.AllFiles(stereoscopeFile.AllTypes()...) {
for _, ref := range r.Tree.AllFiles(stereoscopeFile.AllTypes()...) {
select {
case <-ctx.Done():
return
@@ -192,8 +198,8 @@ func (r *filetreeResolver) AllLocations(ctx context.Context) <-chan file.Locatio
return results
}
func (r *filetreeResolver) FileMetadataByLocation(location file.Location) (file.Metadata, error) {
entry, err := r.index.Get(location.Reference())
func (r *FiletreeResolver) FileMetadataByLocation(location file.Location) (file.Metadata, error) {
entry, err := r.Index.Get(location.Reference())
if err != nil {
return file.Metadata{}, fmt.Errorf("location: %+v : %w", location, os.ErrNotExist)
}
@@ -201,11 +207,11 @@ func (r *filetreeResolver) FileMetadataByLocation(location file.Location) (file.
return entry.Metadata, nil
}
func (r *filetreeResolver) FilesByMIMEType(types ...string) ([]file.Location, error) {
func (r *FiletreeResolver) FilesByMIMEType(types ...string) ([]file.Location, error) {
uniqueFileIDs := stereoscopeFile.NewFileReferenceSet()
uniqueLocations := make([]file.Location, 0)
refVias, err := r.searchContext.SearchByMIMEType(types...)
refVias, err := r.SearchContext.SearchByMIMEType(types...)
if err != nil {
return nil, err
}
@@ -971,7 +971,7 @@ func Test_directoryResolver_FileContentsByLocation(t *testing.T) {
r, err := NewFromDirectory(".", "")
require.NoError(t, err)
exists, existingPath, err := r.tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt")))
exists, existingPath, err := r.Tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt")))
require.True(t, exists)
require.NoError(t, err)
require.True(t, existingPath.HasReference())
@@ -1271,7 +1271,7 @@ func TestDirectoryResolver_FilesContents_errorOnDirRequest(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for loc := range resolver.AllLocations(ctx) {
entry, err := resolver.index.Get(loc.Reference())
entry, err := resolver.Index.Get(loc.Reference())
require.NoError(t, err)
if entry.Metadata.IsDir() {
dirLoc = &loc
@@ -1451,7 +1451,7 @@ func TestFileResolver_FilesByGlob(t *testing.T) {
resolver, err := NewFromFile(parentPath, filePath)
assert.NoError(t, err)
refs, err := resolver.FilesByGlob("*.txt")
refs, err := resolver.FilesByGlob("**/*.txt")
assert.NoError(t, err)
assert.Len(t, refs, 1)
@@ -1500,7 +1500,7 @@ func Test_fileResolver_FileContentsByLocation(t *testing.T) {
r, err := NewFromFile(parentPath, filePath)
require.NoError(t, err)
exists, existingPath, err := r.tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt")))
exists, existingPath, err := r.Tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt")))
require.True(t, exists)
require.NoError(t, err)
require.True(t, existingPath.HasReference())
@@ -1553,7 +1553,7 @@ func TestFileResolver_AllLocations_errorOnDirRequest(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for loc := range resolver.AllLocations(ctx) {
entry, err := resolver.index.Get(loc.Reference())
entry, err := resolver.Index.Get(loc.Reference())
require.NoError(t, err)
if dirLoc == nil && entry.Metadata.IsDir() {
dirLoc = &loc
+1 -1
View File
@@ -6,5 +6,5 @@ import "github.com/anchore/syft/syft/source"
// AllTypes returns a list of all source metadata types that syft supports (that are represented in the source.Description.Metadata field).
func AllTypes() []any {
return []any{source.DirectoryMetadata{}, source.FileMetadata{}, source.ImageMetadata{}}
return []any{source.DirectoryMetadata{}, source.FileMetadata{}, source.ImageMetadata{}, source.SnapMetadata{}}
}
+1
View File
@@ -11,6 +11,7 @@ var jsonNameFromType = map[reflect.Type][]string{
reflect.TypeOf(source.DirectoryMetadata{}): {"directory", "dir"},
reflect.TypeOf(source.FileMetadata{}): {"file"},
reflect.TypeOf(source.ImageMetadata{}): {"image"},
reflect.TypeOf(source.SnapMetadata{}): {"snap"},
}
func AllTypeNames() []string {
+67 -6
View File
@@ -4,6 +4,9 @@ import (
"bytes"
"fmt"
"io"
"sync"
"github.com/diskfs/go-diskfs/filesystem/squashfs"
macho "github.com/anchore/go-macholibre"
"github.com/anchore/syft/internal/log"
@@ -48,12 +51,15 @@ func GetUnionReader(readerCloser io.ReadCloser) (UnionReader, error) {
// file.LocationReadCloser embeds a ReadCloser, which is likely
// to implement UnionReader. Check whether the embedded read closer
// implements UnionReader, and just return that if so.
r, ok := readerCloser.(file.LocationReadCloser)
if ok {
ur, ok := r.ReadCloser.(UnionReader)
if ok {
return ur, nil
}
if r, ok := readerCloser.(file.LocationReadCloser); ok {
return GetUnionReader(r.ReadCloser)
}
if r, ok := readerCloser.(*squashfs.File); ok {
// seeking is implemented, but not io.ReaderAt. Lets wrap it to prevent from degrading performance
// by copying all data.
return newReaderAtAdapter(r), nil
}
b, err := io.ReadAll(readerCloser)
@@ -75,3 +81,58 @@ func GetUnionReader(readerCloser io.ReadCloser) (UnionReader, error) {
return reader, nil
}
type readerAtAdapter struct {
io.ReadSeekCloser
mu *sync.Mutex
}
func newReaderAtAdapter(rs io.ReadSeekCloser) UnionReader {
return &readerAtAdapter{
ReadSeekCloser: rs,
mu: &sync.Mutex{},
}
}
func (r *readerAtAdapter) Read(p []byte) (n int, err error) {
r.mu.Lock()
defer r.mu.Unlock()
return r.ReadSeekCloser.Read(p)
}
func (r *readerAtAdapter) Seek(offset int64, whence int) (int64, error) {
r.mu.Lock()
defer r.mu.Unlock()
return r.ReadSeekCloser.Seek(offset, whence)
}
func (r *readerAtAdapter) ReadAt(p []byte, off int64) (n int, err error) {
r.mu.Lock()
defer r.mu.Unlock()
currentPos, err := r.ReadSeekCloser.Seek(0, io.SeekCurrent) // save current pos
if err != nil {
return 0, err
}
_, err = r.ReadSeekCloser.Seek(off, io.SeekStart) // seek to absolute position `off`
if err != nil {
return 0, err
}
n, err = r.ReadSeekCloser.Read(p) // read from that absolute position
// restore the position for the stateful read/seek operations
if restoreErr := r.restorePosition(currentPos); restoreErr != nil {
if err == nil {
err = restoreErr
}
}
return n, err
}
func (r *readerAtAdapter) restorePosition(pos int64) error {
_, err := r.ReadSeekCloser.Seek(pos, io.SeekStart)
return err
}
@@ -1,8 +1,10 @@
package unionreader
import (
"bytes"
"io"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
@@ -61,3 +63,275 @@ func Test_getUnionReader_fileLocationReadCloser(t *testing.T) {
require.NoError(t, err)
require.Equal(t, p, ur)
}
func TestReaderAtAdapter_ReadAt(t *testing.T) {
testData := "Hello, World! This is a test string for ReadAt."
t.Run("basic functionality", func(t *testing.T) {
reader := newReadSeekCloser(strings.NewReader(testData))
adapter := newReaderAtAdapter(reader)
tests := []struct {
name string
offset int64
length int
expected string
}{
{name: "read from beginning", offset: 0, length: 5, expected: "Hello"},
{name: "read from middle", offset: 7, length: 5, expected: "World"},
{name: "read from end", offset: int64(len(testData) - 4), length: 4, expected: "dAt."},
{name: "read single character", offset: 12, length: 1, expected: "!"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := make([]byte, tt.length)
n, err := adapter.ReadAt(buf, tt.offset)
if err != nil && err != io.EOF {
t.Fatalf("Unexpected error: %v", err)
}
result := string(buf[:n])
if result != tt.expected {
t.Errorf("Expected %q, got %q", tt.expected, result)
}
})
}
})
t.Run("edge cases", func(t *testing.T) {
tests := []struct {
name string
data string
offset int64
bufSize int
expectedN int
expectedErr error
expectedStr string
}{
{
name: "beyond EOF",
data: "Hello",
offset: 10,
bufSize: 5,
expectedN: 0,
expectedErr: io.EOF,
expectedStr: "",
},
{
name: "partial read",
data: "Hello",
offset: 2,
bufSize: 10,
expectedN: 3,
expectedErr: nil,
expectedStr: "llo",
},
{
name: "empty buffer",
data: "Hello",
offset: 0,
bufSize: 0,
expectedN: 0,
expectedErr: nil,
expectedStr: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reader := newReadSeekCloser(strings.NewReader(tt.data))
adapter := newReaderAtAdapter(reader)
buf := make([]byte, tt.bufSize)
n, err := adapter.ReadAt(buf, tt.offset)
if err != tt.expectedErr {
t.Errorf("Expected error %v, got %v", tt.expectedErr, err)
}
if n != tt.expectedN {
t.Errorf("Expected %d bytes read, got %d", tt.expectedN, n)
}
result := string(buf[:n])
if result != tt.expectedStr {
t.Errorf("Expected %q, got %q", tt.expectedStr, result)
}
})
}
})
t.Run("multiple reads from same position", func(t *testing.T) {
reader := newReadSeekCloser(strings.NewReader(testData))
adapter := newReaderAtAdapter(reader)
// read the same data multiple times
for i := 0; i < 3; i++ {
buf := make([]byte, 5)
n, err := adapter.ReadAt(buf, 7)
if err != nil && err != io.EOF {
t.Fatalf("ReadAt %d failed: %v", i, err)
}
result := string(buf[:n])
if result != "World" {
t.Errorf("ReadAt %d: expected 'World', got %q", i, result)
}
}
})
t.Run("concurrent access", func(t *testing.T) {
td := "0123456789abcdefghijklmnopqrstuvwxyz"
reader := newReadSeekCloser(strings.NewReader(td))
adapter := newReaderAtAdapter(reader)
const numGoroutines = 10
const numReads = 100
var wg sync.WaitGroup
results := make(chan bool, numGoroutines*numReads)
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()
for j := 0; j < numReads; j++ {
offset := int64(goroutineID % len(td))
buf := make([]byte, 1)
n, err := adapter.ReadAt(buf, offset)
if err != nil && err != io.EOF {
results <- false
return
}
if n > 0 {
expected := td[offset]
if buf[0] != expected {
results <- false
return
}
}
results <- true
}
}(i)
}
wg.Wait()
close(results)
successCount := 0
totalCount := 0
for success := range results {
totalCount++
if success {
successCount++
}
}
if successCount != totalCount {
t.Errorf("Concurrent reads failed: %d/%d successful", successCount, totalCount)
}
})
}
func TestReaderAtAdapter_PositionHandling(t *testing.T) {
testData := "Hello, World!"
t.Run("preserves position after ReadAt", func(t *testing.T) {
reader := newReadSeekCloser(strings.NewReader(testData))
adapter := newReaderAtAdapter(reader)
// move to a specific position
initialPos := int64(7)
_, err := adapter.Seek(initialPos, io.SeekStart)
if err != nil {
t.Fatalf("Failed to seek: %v", err)
}
// read using ReadAt
buf := make([]byte, 5)
_, err = adapter.ReadAt(buf, 0)
if err != nil && err != io.EOF {
t.Fatalf("ReadAt failed: %v", err)
}
// verify position is preserved
currentPos, err := adapter.Seek(0, io.SeekCurrent)
if err != nil {
t.Fatalf("Failed to get current position: %v", err)
}
if currentPos != initialPos {
t.Errorf("Position not preserved. Expected %d, got %d", initialPos, currentPos)
}
})
t.Run("does not affect regular reads", func(t *testing.T) {
reader := newReadSeekCloser(strings.NewReader(testData))
adapter := newReaderAtAdapter(reader)
// read first few bytes normally
normalBuf := make([]byte, 5)
n, err := adapter.Read(normalBuf)
if err != nil {
t.Fatalf("Normal read failed: %v", err)
}
if string(normalBuf[:n]) != "Hello" {
t.Errorf("Expected 'Hello', got %q", string(normalBuf[:n]))
}
// use ReadAt to read from a different position
readAtBuf := make([]byte, 5)
n, err = adapter.ReadAt(readAtBuf, 7)
if err != nil && err != io.EOF {
t.Fatalf("ReadAt failed: %v", err)
}
if string(readAtBuf[:n]) != "World" {
t.Errorf("Expected 'World', got %q", string(readAtBuf[:n]))
}
// continue normal reading - should pick up where we left off
continueBuf := make([]byte, 2)
n, err = adapter.Read(continueBuf)
if err != nil {
t.Fatalf("Continue read failed: %v", err)
}
if string(continueBuf[:n]) != ", " {
t.Errorf("Expected ', ', got %q", string(continueBuf[:n]))
}
})
}
func TestReaderAtAdapter_Close(t *testing.T) {
reader := newReadSeekCloser(bytes.NewReader([]byte("test data")))
adapter := newReaderAtAdapter(reader)
// test that adapter can be closed
err := adapter.Close()
if err != nil {
t.Errorf("Close failed: %v", err)
}
if !reader.closed {
t.Error("Underlying reader was not closed")
}
}
type readSeekCloser struct {
io.ReadSeeker
closed bool
}
func newReadSeekCloser(rs io.ReadSeeker) *readSeekCloser {
return &readSeekCloser{ReadSeeker: rs}
}
func (r *readSeekCloser) Close() error {
r.closed = true
return nil
}
+12 -8
View File
@@ -252,8 +252,8 @@ func getLogicalDepsJSON(deps depsJSON, lm *libmanJSON) logicalDepsJSON {
if !bundlingDetected && knownBundlers.Has(name) {
bundlingDetected = true
}
p.AnyChildClaimsDLLs = searchForDLLClaims(packageMap, p.dependencyNameVersions()...)
p.AnyChildHasDLLs = searchForDLLEvidence(packageMap, p.dependencyNameVersions()...)
p.AnyChildClaimsDLLs = searchForDLLClaims(packageMap, strset.New(), p.dependencyNameVersions()...)
p.AnyChildHasDLLs = searchForDLLEvidence(packageMap, strset.New(), p.dependencyNameVersions()...)
packages[p.NameVersion] = *p
}
@@ -286,31 +286,35 @@ func mergeSets(s1, s2 *strset.Set) *strset.Set {
type visitorFunc func(p *logicalDepsJSONPackage) bool
// searchForDLLEvidence recursively searches for executables found for any of the given nameVersions and children recursively.
func searchForDLLEvidence(packageMap map[string]*logicalDepsJSONPackage, nameVersions ...string) bool {
func searchForDLLEvidence(packageMap map[string]*logicalDepsJSONPackage, visited *strset.Set, nameVersions ...string) bool {
return traverseDependencies(packageMap, func(p *logicalDepsJSONPackage) bool {
return p.FoundDLLs(true)
}, nameVersions...)
}, visited, nameVersions...)
}
// searchForDLLClaims recursively searches for DLL claims in the deps.json for any of the given nameVersions and children recursively.
func searchForDLLClaims(packageMap map[string]*logicalDepsJSONPackage, nameVersions ...string) bool {
func searchForDLLClaims(packageMap map[string]*logicalDepsJSONPackage, visited *strset.Set, nameVersions ...string) bool {
return traverseDependencies(packageMap, func(p *logicalDepsJSONPackage) bool {
return p.ClaimsDLLs(true)
}, nameVersions...)
}, visited, nameVersions...)
}
func traverseDependencies(packageMap map[string]*logicalDepsJSONPackage, visitor visitorFunc, nameVersions ...string) bool {
func traverseDependencies(packageMap map[string]*logicalDepsJSONPackage, visitor visitorFunc, visited *strset.Set, nameVersions ...string) bool {
if len(nameVersions) == 0 {
return false
}
for _, nameVersion := range nameVersions {
if visited.Has(nameVersion) {
continue
}
visited.Add(nameVersion)
if p, ok := packageMap[nameVersion]; ok {
if visitor(p) {
return true
}
if traverseDependencies(packageMap, visitor, p.dependencyNameVersions()...) {
if traverseDependencies(packageMap, visitor, visited, p.dependencyNameVersions()...) {
return true
}
}
@@ -149,6 +149,11 @@ func TestGetLogicalDepsJSON_MergeTargets(t *testing.T) {
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.dll": {},
},
},
"Microsoft.CodeAnalysis.Common/4.0.0": {
Dependencies: map[string]string{
"Microsoft.CodeAnalysis.CSharp": "4.0.0",
},
},
},
},
Libraries: map[string]depsLibrary{
@@ -486,7 +486,6 @@ var defaultCandidateAdditions = buildCandidateLookup(
candidateKey{PkgName: "dnsmasq", Vendor: "dnsmasq"},
candidateAddition{AdditionalVendors: []string{"thekelleys"}},
},
//
// Binary packages
{
pkg.BinaryPkg,
@@ -630,7 +629,38 @@ var defaultCandidateRemovals = buildCandidateRemovalLookup(
candidateKey{PkgName: "grpc"},
candidateRemovals{ProductsToRemove: []string{"grpc"}},
},
// PHP Packages
// Rust packages
{
pkg.RustPkg,
candidateKey{PkgName: "hyper"},
// Avoid matching CVE-2024-23741
candidateRemovals{VendorsToRemove: []string{"vercel"}},
},
{
pkg.RustPkg,
candidateKey{PkgName: "opentelemetry"},
// Avoid matching CVE-2023-45142
candidateRemovals{ProductsToRemove: []string{"opentelemetry"}},
},
{
pkg.RustPkg,
candidateKey{PkgName: "prometheus"},
// Avoid matching CVE-2019-3826
candidateRemovals{VendorsToRemove: []string{"prometheus"}},
},
{
pkg.RustPkg,
candidateKey{PkgName: "phf"},
// Avoid matching CVE-2000-1186
candidateRemovals{VendorsToRemove: []string{"phf"}},
},
{
pkg.RustPkg,
candidateKey{PkgName: "redis"},
// Avoid matching CVE-2022-24735
candidateRemovals{VendorsToRemove: []string{"redis"}},
},
// PHP packages
{
pkg.PhpPearPkg,
candidateKey{PkgName: "redis"},
@@ -3848,6 +3848,9 @@
"localhost-now": [
"cpe:2.3:a:localhost-now_project:localhost-now:*:*:*:*:*:node.js:*:*"
],
"lockfile-lint-api": [
"cpe:2.3:a:lirantal:lockfile-lint-api:*:*:*:*:*:node.js:*:*"
],
"locutus": [
"cpe:2.3:a:locutus:locutus:*:*:*:*:*:node.js:*:*"
],
@@ -4113,6 +4116,9 @@
"cpe:2.3:a:mysql_project:mysql:*:*:*:*:*:node.js:*:*",
"cpe:2.3:a:mysqljs:mysql:*:*:*:*:*:node.js:*:*"
],
"mysql2": [
"cpe:2.3:a:sidorares:mysql2:*:*:*:*:*:node.js:*:*"
],
"mysqljs": [
"cpe:2.3:a:mysqljs_project:mysqljs:*:*:*:*:*:node.js:*:*"
],
@@ -7072,6 +7078,9 @@
"versionize": [
"cpe:2.3:a:versionize_project:versionize:*:*:*:*:*:rust:*:*"
],
"wasmi": [
"cpe:2.3:a:wasmi-labs:wasmi:*:*:*:*:*:rust:*:*"
],
"wasmtime": [
"cpe:2.3:a:bytecodealliance:wasmtime:*:*:*:*:*:rust:*:*"
],
@@ -7546,6 +7555,7 @@
"advanced-custom-fields": [
"cpe:2.3:a:advancedcustomfields:advanced_custom_fields:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:advancedcustomfields:advanced_custom_fields:*:*:*:*:-:wordpress:*:*",
"cpe:2.3:a:advancedcustomfields:advanced_custom_fields:*:*:*:*:free:wordpress:*:*",
"cpe:2.3:a:advancedcustomfields:advanced_custom_fields:*:*:*:*:pro:wordpress:*:*"
],
"advanced-database-cleaner": [
@@ -7675,6 +7685,9 @@
"ajax-bootmodal-login": [
"cpe:2.3:a:ajax_bootmodal_login_project:ajax_bootmodal_login:*:*:*:*:*:wordpress:*:*"
],
"ajax-filter-posts": [
"cpe:2.3:a:addonmaster:post_grid_master:*:*:*:*:*:wordpress:*:*"
],
"ajax-load-more": [
"cpe:2.3:a:connekthq:ajax_load_more:*:*:*:*:*:wordpress:*:*"
],
@@ -7738,7 +7751,7 @@
"cpe:2.3:a:vsourz:all_in_one_redirection:*:*:*:*:*:wordpress:*:*"
],
"all-in-one-schemaorg-rich-snippets": [
"cpe:2.3:a:brainstormforce:schema:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:brainstormforce:schema:*:*:*:*:-:wordpress:*:*"
],
"all-in-one-seo-pack": [
"cpe:2.3:a:aioseo:all_in_one_seo:*:*:*:*:*:wordpress:*:*",
@@ -7880,6 +7893,9 @@
"animate-it": [
"cpe:2.3:a:eleopard:animate_it\\!:*:*:*:*:*:wordpress:*:*"
],
"animated-al-list": [
"cpe:2.3:a:alexdtn:animated_al_list:*:*:*:*:*:wordpress:*:*"
],
"animated-counters": [
"cpe:2.3:a:eralion:animated_counters:*:*:*:*:*:wordpress:*:*"
],
@@ -8025,6 +8041,9 @@
"arprice-responsive-pricing-table": [
"cpe:2.3:a:reputeinfosystems:arprice_lite:*:*:*:*:*:wordpress:*:*"
],
"artibot": [
"cpe:2.3:a:artibot:artibot:*:*:*:*:*:wordpress:*:*"
],
"article-directory": [
"cpe:2.3:a:article_directory_project:article_directory:*:*:*:*:*:wordpress:*:*"
],
@@ -8137,6 +8156,9 @@
"auto-post-thumbnail": [
"cpe:2.3:a:cm-wp:auto_featured_image:*:*:*:*:*:wordpress:*:*"
],
"auto-prune-posts": [
"cpe:2.3:a:klarned:auto_prune_posts:*:*:*:*:*:wordpress:*:*"
],
"auto-refresh-single-page": [
"cpe:2.3:a:rymera:auto_refresh_single_page:*:*:*:*:*:wordpress:*:*"
],
@@ -8211,6 +8233,9 @@
"awesome-weather": [
"cpe:2.3:a:awesome_weather_widget_project:awesome_weather_widget:*:*:*:*:*:wordpress:*:*"
],
"awin-advertiser-tracking": [
"cpe:2.3:a:awin:awin_-_advertiser_tracking_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
"awin-data-feed": [
"cpe:2.3:a:awin:awin_data_feed:*:*:*:*:*:wordpress:*:*"
],
@@ -8369,6 +8394,9 @@
"cpe:2.3:a:web-x:be-popia-compliant:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:web-x:be_popia_compliant:*:*:*:*:*:wordpress:*:*"
],
"beaf-before-and-after-gallery": [
"cpe:2.3:a:themefic:ultimate_before_after_image_slider_\\\u0026_gallery:*:*:*:*:*:wordpress:*:*"
],
"beam-me-up-scotty": [
"cpe:2.3:a:outtheboxthemes:beam_me_up_scotty:*:*:*:*:*:wordpress:*:*"
],
@@ -8632,6 +8660,9 @@
"bookshelf": [
"cpe:2.3:a:bookshelf_project:bookshelf:*:*:*:*:*:wordpress:*:*"
],
"bookster": [
"cpe:2.3:a:wpbookster:bookster:*:*:*:*:*:wordpress:*:*"
],
"boostify-header-footer-builder": [
"cpe:2.3:a:woostify:boostify_header_footer_builder_for_elementor:*:*:*:*:*:wordpress:*:*"
],
@@ -8811,6 +8842,9 @@
"button-block": [
"cpe:2.3:a:bplugins:button_block:*:*:*:*:*:wordpress:*:*"
],
"button-contact-vr": [
"cpe:2.3:a:buttonizer:call_\\/_chat_\\/_contact_button:*:*:*:*:*:wordpress:*:*"
],
"button-generation": [
"cpe:2.3:a:wow-company:button_generator:*:*:*:*:*:wordpress:*:*"
],
@@ -8943,6 +8977,9 @@
"canto": [
"cpe:2.3:a:canto:canto:*:*:*:*:*:wordpress:*:*"
],
"canvasflow": [
"cpe:2.3:a:canvasflow:canvasflow:*:*:*:*:*:wordpress:*:*"
],
"canvasio3d-light": [
"cpe:2.3:a:virtuellwerk:canvasio3d_light:*:*:*:*:*:wordpress:*:*"
],
@@ -8995,6 +9032,9 @@
"cars-seller-auto-classifieds-script": [
"cpe:2.3:a:cars-seller-auto-classifieds-script_project:cars-seller-auto-classifieds-script:*:*:*:*:*:wordpress:*:*"
],
"cart-tracking-for-woocommerce": [
"cpe:2.3:a:wpdever:cart_tracking_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
"cart66-lite": [
"cpe:2.3:a:reality66:cart66_lite:*:*:*:*:*:wordpress:*:*"
],
@@ -9234,6 +9274,9 @@
"clickdesk-live-support-chat-plugin": [
"cpe:2.3:a:clickdesk:clickdesk:*:*:*:*:*:wordpress:*:*"
],
"clickwhale": [
"cpe:2.3:a:flowdee:clickwhale:*:*:*:*:*:wordpress:*:*"
],
"clictracker": [
"cpe:2.3:a:wp_clictracker_project:wp_clictracker:*:*:*:*:*:wordpress:*:*"
],
@@ -9350,6 +9393,9 @@
"colorful-categories": [
"cpe:2.3:a:gesundheit-bewegt:colorful_categories:*:*:*:*:*:wordpress:*:*"
],
"colorlib-coming-soon-maintenance": [
"cpe:2.3:a:colorlib:coming_soon_\\\u0026_maintenance_mode:*:*:*:*:*:wordpress:*:*"
],
"column-matic": [
"cpe:2.3:a:column-matic_project:column-matic:*:*:*:*:*:wordpress:*:*"
],
@@ -9721,6 +9767,9 @@
"core-web-vitals-pagespeed-booster": [
"cpe:2.3:a:magazine3:core_web_vitals_\\\u0026_pagespeed_booster:*:*:*:*:*:wordpress:*:*"
],
"coreactivity": [
"cpe:2.3:a:dev4press:coreactivity:*:*:*:*:*:wordpress:*:*"
],
"corner-ad": [
"cpe:2.3:a:dwbooster:corner_ad:*:*:*:*:*:wordpress:*:*"
],
@@ -10083,6 +10132,12 @@
"cpe:2.3:a:fastlinemedia:customizer_export\\/import:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:wpbeaverbuilder:customizer_export\\/import:*:*:*:*:*:wordpress:*:*"
],
"cww-companion": [
"cpe:2.3:a:codeworkweb:cww_companion:*:*:*:*:*:wordpress:*:*"
],
"cyan-backup": [
"cpe:2.3:a:toolstack:cyan_backup:*:*:*:*:*:wordpress:*:*"
],
"cybersoldier": [
"cpe:2.3:a:webbigt:cybersoldier:*:*:*:*:*:wordpress:*:*"
],
@@ -10179,7 +10234,8 @@
"cpe:2.3:a:webtoffee:decorator:*:*:*:*:*:wordpress:*:*"
],
"defender-security": [
"cpe:2.3:a:wmpudev:defender_security:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:wpmudev:defender:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:wpmudev:defender:*:*:*:*:free:wordpress:*:*",
"cpe:2.3:a:wpmudev:defender_security:*:*:*:*:*:wordpress:*:*"
],
"definitive-addons-for-elementor": [
@@ -10307,9 +10363,15 @@
"dmsguestbook": [
"cpe:2.3:a:dmsguestbook_project:dmsguestbook:*:*:*:*:*:wordpress:*:*"
],
"dn-footer-contacts": [
"cpe:2.3:a:digireturn:footer_contacts_bar:*:*:*:*:*:wordpress:*:*"
],
"dn-popup": [
"cpe:2.3:a:digireturn:dn_popup:*:*:*:*:*:wordpress:*:*"
],
"dn-shipping-by-weight": [
"cpe:2.3:a:digireturn:shipping_by_weight_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
"document-emberdder": [
"cpe:2.3:a:bplugins:document_embedder:*:*:*:*:*:wordpress:*:*"
],
@@ -10327,7 +10389,7 @@
"cpe:2.3:a:wedevs:dokan:*:*:*:*:*:wordpress:*:*"
],
"dokan-pro": [
"cpe:2.3:a:dokan:dokan_pro_plugin:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:dokan:dokan:*:*:*:*:pro:wordpress:*:*"
],
"dologin": [
"cpe:2.3:a:wpdo:dologin_security:*:*:*:*:*:wordpress:*:*"
@@ -10744,6 +10806,9 @@
"eg-attachments": [
"cpe:2.3:a:egeorjon:eg-attachments:*:*:*:*:*:wordpress:*:*"
],
"ekc-tournament-manager": [
"cpe:2.3:a:lukashuser:ekc_tournament_manager:*:*:*:*:*:wordpress:*:*"
],
"elastic-email-sender": [
"cpe:2.3:a:elasticemail:elastic_email_sender:*:*:*:*:*:wordpress:*:*"
],
@@ -10978,7 +11043,8 @@
"cpe:2.3:a:estatik:estatik:*:*:*:*:*:wordpress:*:*"
],
"estatik-mortgage-calculator": [
"cpe:2.3:a:estatik:estatik_mortgage_calculator:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:estatik:estatik_mortgage_calculator:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:estatik:mortgage_calculator:*:*:*:*:*:wordpress:*:*"
],
"etsy-shop": [
"cpe:2.3:a:etsy_shop_project:etsy_shop:*:*:*:*:*:wordpress:*:*"
@@ -11235,6 +11301,9 @@
"fast-search-powered-by-solr": [
"cpe:2.3:a:fast-search-powered-by-solr_project:fast-search-powered-by-solr:*:*:*:*:*:wordpress:*:*"
],
"fast-tube": [
"cpe:2.3:a:caspie:fast_tube:*:*:*:*:*:wordpress:*:*"
],
"fast-velocity-minify": [
"cpe:2.3:a:fastvelocity:minify:*:*:*:*:*:wordpress:*:*"
],
@@ -11281,6 +11350,9 @@
"featured-image-from-url": [
"cpe:2.3:a:fifu:featured_image_from_url:*:*:*:*:*:wordpress:*:*"
],
"featured-image-plus": [
"cpe:2.3:a:krasenslavov:featured_image_plus:*:*:*:*:*:wordpress:*:*"
],
"feed-changer": [
"cpe:2.3:a:wp-master:feed_changer_\\\u0026_remover:*:*:*:*:*:wordpress:*:*"
],
@@ -11302,6 +11374,9 @@
"feedzy-rss-feeds": [
"cpe:2.3:a:themeisle:rss_aggregator_by_feedzy:*:*:*:*:*:wordpress:*:*"
],
"fetch-jft": [
"cpe:2.3:a:pjaudiomv:fetch_jft:*:*:*:*:*:wordpress:*:*"
],
"fg-drupal-to-wp": [
"cpe:2.3:a:fredericgilles:fg_drupal:*:*:*:*:*:wordpress:*:*"
],
@@ -11368,6 +11443,7 @@
"cpe:2.3:a:find_my_blocks_project:find_my_blocks:*:*:*:*:*:wordpress:*:*"
],
"first-graders-toolbox": [
"cpe:2.3:a:atakanau:1_click_disable_all:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:atakanau:one_click_disable_all:*:*:*:*:*:wordpress:*:*"
],
"fish-and-ships": [
@@ -11389,6 +11465,9 @@
"flat-preloader": [
"cpe:2.3:a:flat_preloader_project:flat_preloader:*:*:*:*:*:wordpress:*:*"
],
"flatpm-wp": [
"cpe:2.3:a:mehanoid:flatpm:*:*:*:*:*:wordpress:*:*"
],
"flexi": [
"cpe:2.3:a:odude:flexi:*:*:*:*:*:wordpress:*:*"
],
@@ -11426,6 +11505,9 @@
"floating-button": [
"cpe:2.3:a:wow-company:floating_button:*:*:*:*:*:wordpress:*:*"
],
"floating-contact": [
"cpe:2.3:a:just-a-web-developer:floating_contact_button:*:*:*:*:*:wordpress:*:*"
],
"floating-div": [
"cpe:2.3:a:floating_div_project:floating_div:*:*:*:*:*:wordpress:*:*"
],
@@ -11475,6 +11557,9 @@
"fluid-responsive-slideshow": [
"cpe:2.3:a:tonjoostudio:fluid-responsive-slideshow:*:*:*:*:*:wordpress:*:*"
],
"flynax-bridge": [
"cpe:2.3:a:flynax:flynax_bridge:*:*:*:*:*:wordpress:*:*"
],
"folders": [
"cpe:2.3:a:premio:folders:*:*:*:*:*:wordpress:*:*"
],
@@ -11661,6 +11746,9 @@
"funnel-builder": [
"cpe:2.3:a:funnelkit:funnel_builder:*:*:*:*:*:wordpress:*:*"
],
"funnel-builder-pro": [
"cpe:2.3:a:funnelkit:funnel_builder:*:*:*:*:*:wordpress:*:*"
],
"funnelforms-free": [
"cpe:2.3:a:funnelforms:funnelforms_free:*:*:*:*:*:wordpress:*:*"
],
@@ -11770,6 +11858,9 @@
"gdpr-data-request-form": [
"cpe:2.3:a:whodunit:gdpr_data_request_form:*:*:*:*:*:wordpress:*:*"
],
"gdpr-framework": [
"cpe:2.3:a:data443:gdpr_framework:*:*:*:*:*:wordpress:*:*"
],
"gecka-terms-thumbnails": [
"cpe:2.3:a:gecka:terms_thumbnails:*:*:*:*:*:wordpress:*:*"
],
@@ -11849,6 +11940,9 @@
"cpe:2.3:a:ghost:ghost:*:*:*:*:*:node.js:*:*",
"cpe:2.3:a:ghost:ghost:*:*:*:*:*:wordpress:*:*"
],
"gianism": [
"cpe:2.3:a:takahashifumiki:gianism:*:*:*:*:*:wordpress:*:*"
],
"gift-certificate-creator": [
"cpe:2.3:a:bobcares:gift-certificate-creator:*:*:*:*:*:wordpress:*:*"
],
@@ -11883,6 +11977,9 @@
"global-content-blocks": [
"cpe:2.3:a:global_content_blocks_project:global_content_blocks:*:*:*:*:*:wordpress:*:*"
],
"global-gallery": [
"cpe:2.3:a:lcweb:global_gallery:*:*:*:*:*:wordpress:*:*"
],
"glossary-by-codeat": [
"cpe:2.3:a:codeat:glossary:*:*:*:*:*:wordpress:*:*"
],
@@ -12056,7 +12153,7 @@
"cpe:2.3:a:greeklish-permalink_project:greeklish-permalink:*:*:*:*:*:wordpress:*:*"
],
"greenshift-animation-and-page-builder-blocks": [
"cpe:2.3:a:greenshiftwp:greenshift_-_animation_and_page_builder_blocks:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:wpsoul:greenshift:*:*:*:*:*:wordpress:*:*"
],
"greenwallet-gateway": [
"cpe:2.3:a:envision\\\u0026company:woocommerce_green_wallet_gateway:*:*:*:*:*:wordpress:*:*"
@@ -12071,7 +12168,8 @@
"cpe:2.3:a:groundhogg:groundhogg:*:*:*:*:*:wordpress:*:*"
],
"gs-logo-slider": [
"cpe:2.3:a:gsplugins:gs_logo_slider:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:gsplugins:gs_logo_slider:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:gsplugins:logo_slider:*:*:*:*:*:wordpress:*:*"
],
"gs-portfolio": [
"cpe:2.3:a:gsplugins:gs_filterable_portfolio:*:*:*:*:*:wordpress:*:*"
@@ -12425,6 +12523,9 @@
"ideapush": [
"cpe:2.3:a:northernbeacheswebsites:ideapush:*:*:*:*:*:wordpress:*:*"
],
"idonate": [
"cpe:2.3:a:themeatelier:idonate:*:*:*:*:*:wordpress:*:*"
],
"idx-broker-platinum": [
"cpe:2.3:a:idxbroker:idx_broker:*:*:*:*:platinum:wordpress:*:*",
"cpe:2.3:a:idxbroker:impress_for_idx_broker:*:*:*:*:platinum:wordpress:*:*"
@@ -12680,6 +12781,9 @@
"instant-images": [
"cpe:2.3:a:connekthq:instant_images_-_one_click_unsplash_uploads:*:*:*:*:*:wordpress:*:*"
],
"instantio": [
"cpe:2.3:a:themefic:instantio:*:*:*:*:*:wordpress:*:*"
],
"instawp-connect": [
"cpe:2.3:a:instawp:instawp_connect:*:*:*:*:*:wordpress:*:*"
],
@@ -12815,6 +12919,9 @@
"jetpack": [
"cpe:2.3:a:automattic:jetpack:*:*:*:*:*:wordpress:*:*"
],
"jetpack-boost": [
"cpe:2.3:a:automattic:jetpack_boost:*:*:*:*:*:wordpress:*:*"
],
"jetwidgets-for-elementor": [
"cpe:2.3:a:crocoblock:jetwidgets_for_elementor:*:*:*:*:*:wordpress:*:*"
],
@@ -12920,6 +13027,9 @@
"just-tables": [
"cpe:2.3:a:hasthemes:justtables:*:*:*:*:*:wordpress:*:*"
],
"justrows-free": [
"cpe:2.3:a:canaveralstudio:justrows_free:*:*:*:*:*:wordpress:*:*"
],
"jvm-rich-text-icons": [
"cpe:2.3:a:jorisvm:jvm_gutenberg_rich_text_icons:*:*:*:*:*:wordpress:*:*"
],
@@ -12953,6 +13063,9 @@
"kb-support": [
"cpe:2.3:a:logon:kb_support:*:*:*:*:*:wordpress:*:*"
],
"kbucket": [
"cpe:2.3:a:optimalaccess:kbucket:*:*:*:*:*:wordpress:*:*"
],
"keep-backup-daily": [
"cpe:2.3:a:androidbubbles:keep_backup_daily:*:*:*:*:*:wordpress:*:*"
],
@@ -12992,6 +13105,9 @@
"kk-star-ratings": [
"cpe:2.3:a:kamalkhan:kk_star_ratings:*:*:*:*:*:wordpress:*:*"
],
"kkprogressbar": [
"cpe:2.3:a:krzysztof-furtak:kkprogressbar2:*:*:*:*:free:wordpress:*:*"
],
"klarna-checkout-for-woocommerce": [
"cpe:2.3:a:klarna:klarna_checkout_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
@@ -13034,6 +13150,9 @@
"labtools": [
"cpe:2.3:a:creativityjuice:labtools:*:*:*:*:*:wordpress:*:*"
],
"ladipage": [
"cpe:2.3:a:ladipage:ladipage:*:*:*:*:*:wordpress:*:*"
],
"lana-downloads-manager": [
"cpe:2.3:a:lana:lana_downloads_manager:*:*:*:*:*:wordpress:*:*"
],
@@ -13137,7 +13256,8 @@
"cpe:2.3:a:wplearnmanager:wp_learn_manager:*:*:*:*:*:wordpress:*:*"
],
"learning-management-system": [
"cpe:2.3:a:masteriyo:masteriyo:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:themegrill:masteriyo:*:*:*:*:free:wordpress:*:*",
"cpe:2.3:a:themegrill:masteriyo:*:*:*:*:pro:wordpress:*:*"
],
"learnpress": [
"cpe:2.3:a:thimpress:learnpress:*:*:*:*:*:wordpress:*:*",
@@ -13155,6 +13275,9 @@
"legal-pages": [
"cpe:2.3:a:wpwax:legal_pages:*:*:*:*:*:wordpress:*:*"
],
"legull": [
"cpe:2.3:a:alwayscurious:legull:*:*:*:*:*:wordpress:*:*"
],
"leira-cron-jobs": [
"cpe:2.3:a:leira:cron_jobs:*:*:*:*:*:wordpress:*:*"
],
@@ -13307,6 +13430,9 @@
"loco-translate": [
"cpe:2.3:a:loco_translate_project:loco_translate:*:*:*:*:*:wordpress:*:*"
],
"logdash-activity-log": [
"cpe:2.3:a:deryckoe:logdash_activity_log:*:*:*:*:*:wordpress:*:*"
],
"loggedin": [
"cpe:2.3:a:duckdev:loggedin:*:*:*:*:*:wordpress:*:*"
],
@@ -13373,6 +13499,9 @@
"logwpmail": [
"cpe:2.3:a:premierethemes:log_wp_mail:*:*:*:*:*:wordpress:*:*"
],
"lokalyze-call-now": [
"cpe:2.3:a:lokalyze:call_me_now:*:*:*:*:*:wordpress:*:*"
],
"ltl-freight-quotes-unishippers-edition": [
"cpe:2.3:a:eniture:ltl_freight_quotes:*:*:*:*:unishippers:wordpress:*:*"
],
@@ -13382,6 +13511,9 @@
"luckywp-table-of-contents": [
"cpe:2.3:a:theluckywp:luckywp_table_of_contents:*:*:*:*:*:wordpress:*:*"
],
"lupsonline-link-netwerk": [
"cpe:2.3:a:lupsonline:seo_flow:*:*:*:*:*:wordpress:*:*"
],
"lws-affiliation": [
"cpe:2.3:a:lws:affiliation:*:*:*:*:*:wordpress:*:*"
],
@@ -13467,7 +13599,7 @@
"cpe:2.3:a:mailoptin:mailoptin:*:*:*:*:*:wordpress:*:*"
],
"mailpoet": [
"cpe:2.3:a:mailpoet:mailpoet:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:automattic:mailpoet:*:*:*:*:free:wordpress:*:*"
],
"mailtree-log-mail": [
"cpe:2.3:a:oacstudio:mailtree_log_mail:*:*:*:*:*:wordpress:*:*"
@@ -13475,6 +13607,9 @@
"maintenance": [
"cpe:2.3:a:webfactoryltd:maintenance:*:*:*:*:*:wordpress:*:*"
],
"maintenance-notice": [
"cpe:2.3:a:codevibrant:maintenance_notice:*:*:*:*:*:wordpress:*:*"
],
"maintenance-page": [
"cpe:2.3:a:themegrill:maintenance_page:*:*:*:*:*:wordpress:*:*"
],
@@ -13618,6 +13753,9 @@
"maz-loader": [
"cpe:2.3:a:feataholic:maz_loader:*:*:*:*:*:wordpress:*:*"
],
"mb-custom-post-type": [
"cpe:2.3:a:deluxeblogtips:mb_custom_post_types_\\\u0026_custom_taxonomies:*:*:*:*:*:wordpress:*:*"
],
"mdc-youtube-downloader": [
"cpe:2.3:a:mdc_youtube_downloader_project:mdc_youtube_downloader:*:*:*:*:*:wordpress:*:*"
],
@@ -13643,6 +13781,9 @@
"media-library-plus": [
"cpe:2.3:a:maxfoundry:media_library_folders:*:*:*:*:*:wordpress:*:*"
],
"media-library-tools": [
"cpe:2.3:a:wptinysolutions:media_library_tools:*:*:*:*:*:wordpress:*:*"
],
"media-list": [
"cpe:2.3:a:drelton:medialist:*:*:*:*:*:wordpress:*:*"
],
@@ -13671,9 +13812,7 @@
"cpe:2.3:a:mediavine:create:*:*:*:*:*:wordpress:*:*"
],
"meeting-scheduler-by-vcita": [
"cpe:2.3:a:vcita:online_booking_\\\u0026_scheduling_calendar:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:vcita:online_booking_\\\u0026_scheduling_calendar_for_wordpress:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:vcita:online_booking_\\\u0026_scheduling_calendar_for_wordpress_by_vcita:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:vcita:online_booking_\\\u0026_scheduling_calendar:*:*:*:*:*:wordpress:*:*"
],
"mega-addons-for-visual-composer": [
"cpe:2.3:a:topdigitaltrends:mega_addons_for_wpbakery_page_builder:*:*:*:*:*:wordpress:*:*"
@@ -13723,6 +13862,9 @@
"membership-simplified-for-oap-members-only": [
"cpe:2.3:a:membership_simplified_project:membership_simplified:*:*:*:*:*:wordpress:*:*"
],
"memberspace": [
"cpe:2.3:a:memberspace:memberspace:*:*:*:*:*:wordpress:*:*"
],
"memphis-documents-library": [
"cpe:2.3:a:memphis_documents_library_project:memphis_documents_library:*:*:*:*:*:wordpress:*:*"
],
@@ -13849,6 +13991,9 @@
"mobile-browser-color-select": [
"cpe:2.3:a:script:mobile_browser_color_select:*:*:*:*:*:wordpress:*:*"
],
"mobile-contact-bar": [
"cpe:2.3:a:annabansaghi:mobile_contact_bar:*:*:*:*:*:wordpress:*:*"
],
"mobile-events-manager": [
"cpe:2.3:a:mobile_events_manager_project:mobile_events_manager:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:mobileeventsmanager:mobile_events_manager:*:*:*:*:*:wordpress:*:*"
@@ -14204,6 +14349,9 @@
"newsletter-optin-box": [
"cpe:2.3:a:noptin:noptin:*:*:*:*:*:wordpress:*:*"
],
"newsletter-popup": [
"cpe:2.3:a:mndpsingh287:newsletter_popup:*:*:*:*:*:wordpress:*:*"
],
"newsletters-lite": [
"cpe:2.3:a:tribulant:newsletters:*:*:*:*:*:wordpress:*:*"
],
@@ -14468,6 +14616,9 @@
"openpgp-form-encryption": [
"cpe:2.3:a:arnesonium:openpgp_form_encryption:*:*:*:*:*:wordpress:*:*"
],
"opensheetmusicdisplay": [
"cpe:2.3:a:opensheetmusicdisplay:opensheetmusicdisplay:*:*:*:*:*:wordpress:*:*"
],
"ops-robots-txt": [
"cpe:2.3:a:ops-robots-txt_project:ops-robots-txt:*:*:*:*:*:wordpress:*:*"
],
@@ -14603,6 +14754,9 @@
"pagepost-content-shortcode": [
"cpe:2.3:a:page\\/post_content_shortcode_project:page\\/post_content_shortcode:*:*:*:*:*:wordpress:*:*"
],
"pagerank-tools": [
"cpe:2.3:a:mahype:pagerank_tools:*:*:*:*:*:wordpress:*:*"
],
"pagination": [
"cpe:2.3:a:bestwebsoft:pagination:*:*:*:*:*:wordpress:*:*"
],
@@ -14659,6 +14813,9 @@
"payment-forms-for-paystack": [
"cpe:2.3:a:paystack:payment_forms_for_paystack:*:*:*:*:*:wordpress:*:*"
],
"payment-gateway-for-telcell": [
"cpe:2.3:a:hkdigital:payment_gateway_for_telcell:*:*:*:*:*:wordpress:*:*"
],
"payment-gateway-stripe-and-woocommerce-integration": [
"cpe:2.3:a:webtoffee:stripe_payment_plugin_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
@@ -15057,6 +15214,9 @@
"post-highlights": [
"cpe:2.3:a:post_highlights_projects:post_highlights:*:*:*:*:*:wordpress:*:*"
],
"post-in-page-for-elementor": [
"cpe:2.3:a:migaweb:post_in_page_for_elementor:*:*:*:*:*:wordpress:*:*"
],
"post-index": [
"cpe:2.3:a:post_index_project:post_index:*:*:*:*:*:wordpress:*:*"
],
@@ -15073,12 +15233,14 @@
"cpe:2.3:a:post_pay_counter_project:post_pay_counter:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:thecrowned:post_pay_counter:*:*:*:*:*:wordpress:*:*"
],
"post-slider-and-carousel": [
"cpe:2.3:a:infornweb:post_slider_and_post_carousel:*:*:*:*:free:wordpress:*:*"
],
"post-slider-carousel": [
"cpe:2.3:a:i13websolution:post_sliders_\\\u0026_post_grids:*:*:*:*:*:wordpress:*:*"
],
"post-smtp": [
"cpe:2.3:a:wpexperts:post_smtp:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:wpexperts:post_smtp_mailer:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:wpexperts:post_smtp:*:*:*:*:*:wordpress:*:*"
],
"post-snippets": [
"cpe:2.3:a:postsnippets:post_snippets:*:*:*:*:*:wordpress:*:*"
@@ -15427,6 +15589,9 @@
"cpe:2.3:a:quick_chat_project:quick_chat:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:techytalk:quick_chat:*:*:*:*:*:wordpress:*:*"
],
"quick-code": [
"cpe:2.3:a:gwycon:quick_code:*:*:*:*:*:wordpress:*:*"
],
"quick-contact-form": [
"cpe:2.3:a:fullworksplugins:quick_contact_form:*:*:*:*:*:wordpress:*:*"
],
@@ -15458,6 +15623,9 @@
"quickswish": [
"cpe:2.3:a:hasthemes:quickswish:*:*:*:*:*:wordpress:*:*"
],
"quillforms": [
"cpe:2.3:a:mdmag:quill_forms:*:*:*:*:*:wordpress:*:*"
],
"quiz-expert": [
"cpe:2.3:a:wepupil:quiz_expert_-_easy_quiz_maker\\,_exam_and_test_manager:*:*:*:*:*:wordpress:*:*"
],
@@ -16014,7 +16182,8 @@
"cpe:2.3:a:scoutnet:kalender:*:*:*:*:*:wordpress:*:*"
],
"scratch-win-giveaways-for-website-facebook": [
"cpe:2.3:a:akashmalik:scratch_\\\u0026_win:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:akashmalik:scratch_\\\u0026_win:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:appsmav:scratch_\\\u0026_win:*:*:*:*:*:wordpress:*:*"
],
"scribble-maps": [
"cpe:2.3:a:scribblemaps:scribble_maps:*:*:*:*:*:wordpress:*:*"
@@ -16131,6 +16300,9 @@
"sendpress": [
"cpe:2.3:a:pressified:sendpress:*:*:*:*:*:wordpress:*:*"
],
"sendpulse-email-marketing-newsletter": [
"cpe:2.3:a:sendpulse:sendpulse_email_marketing_newsletter:*:*:*:*:*:wordpress:*:*"
],
"sendpulse-web-push": [
"cpe:2.3:a:sendpulse:free_web_push:*:*:*:*:*:wordpress:*:*"
],
@@ -16205,6 +16377,9 @@
"cpe:2.3:a:legalweb:wp_dsgvo_tools:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:shapepress:wp_dsgvo_tools:*:*:*:*:*:wordpress:*:*"
],
"share-buttons": [
"cpe:2.3:a:artlosk:social_share_buttons:*:*:*:*:*:wordpress:*:*"
],
"share-on-diaspora": [
"cpe:2.3:a:share_on_diaspora_project:share_on_diaspora:*:*:*:*:*:wordpress:*:*"
],
@@ -16285,7 +16460,8 @@
],
"shortcodes-ultimate": [
"cpe:2.3:a:getshortcodes:shortcodes_ultimate:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:getshortcodes:shortcodes_ultimate:*:*:*:*:-:wordpress:*:*"
"cpe:2.3:a:getshortcodes:shortcodes_ultimate:*:*:*:*:-:wordpress:*:*",
"cpe:2.3:a:getshortcodes:shortcodes_ultimate:*:*:*:*:pro:wordpress:*:*"
],
"shortcut-macros": [
"cpe:2.3:a:shortcut_macros_project:shortcut_macros:*:*:*:*:*:wordpress:*:*"
@@ -16436,6 +16612,12 @@
"cpe:2.3:a:simplefilelist:simple-file-list:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:simplefilelist:simple_file_list:*:*:*:*:*:wordpress:*:*"
],
"simple-form": [
"cpe:2.3:a:devsabbirahmed:simple_form:*:*:*:*:*:wordpress:*:*"
],
"simple-gallery-with-filter": [
"cpe:2.3:a:come2theweb:simple_gallery_with_filter:*:*:*:*:*:wordpress:*:*"
],
"simple-history": [
"cpe:2.3:a:simple_history_project:simple_history:*:*:*:*:*:wordpress:*:*"
],
@@ -16464,6 +16646,9 @@
"simple-ldap-login": [
"cpe:2.3:a:objectiv:simple_ldap_login:*:*:*:*:*:wordpress:*:*"
],
"simple-lightbox": [
"cpe:2.3:a:archetyped:simple_lightbox:*:*:*:*:*:wordpress:*:*"
],
"simple-link-directory": [
"cpe:2.3:a:quantumcloud:simple_link_directory:*:*:*:*:*:wordpress:*:*"
],
@@ -16846,7 +17031,8 @@
"cpe:2.3:a:wpmudev:smartcrawl:*:*:*:*:*:wordpress:*:*"
],
"smartsearchwp": [
"cpe:2.3:a:smartsearchwp:smartsearchwp:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:smartsearchwp:smartsearchwp:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:webdigit:chatbot_with_chatgpt:*:*:*:*:*:wordpress:*:*"
],
"smartsoftbutton-widget-de-botones-de-chat": [
"cpe:2.3:a:smartsoft:button_widget_smartsoft:*:*:*:*:*:wordpress:*:*"
@@ -16920,6 +17106,9 @@
"social-media-feather": [
"cpe:2.3:a:sharethis:social_media_feather:*:*:*:*:*:wordpress:*:*"
],
"social-media-widget": [
"cpe:2.3:a:bmwebproperties:social_media_widget:*:*:*:*:*:wordpress:*:*"
],
"social-networks-auto-poster-facebook-twitter-g": [
"cpe:2.3:a:nextscripts:social_networks_auto_poster:*:*:*:*:*:wordpress:*:*"
],
@@ -17023,6 +17212,9 @@
"spice-starter-sites": [
"cpe:2.3:a:spicethemes:spice_starter_sites:*:*:*:*:*:wordpress:*:*"
],
"spider-contacts": [
"cpe:2.3:a:10web:spidercontacts:*:*:*:*:*:wordpress:*:*"
],
"spider-event-calendar": [
"cpe:2.3:a:web-dorado:spider_calendar:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:web-dorado:spider_event_calendar:*:*:*:*:*:*:*:*"
@@ -17264,6 +17456,9 @@
"support-chat": [
"cpe:2.3:a:ninjateam:click_to_chat:*:*:*:*:*:wordpress:*:*"
],
"support-svg": [
"cpe:2.3:a:sayedulsayem:support_svg:*:*:*:*:*:wordpress:*:*"
],
"supportcandy": [
"cpe:2.3:a:supportcandy:supportcandy:*:*:*:*:*:wordpress:*:*"
],
@@ -17591,6 +17786,9 @@
"thirstyaffiliates": [
"cpe:2.3:a:caseproof:thirstyaffiliates_affiliate_link_manager:*:*:*:*:*:wordpress:*:*"
],
"threepress": [
"cpe:2.3:a:kerryoco:threepress:*:*:*:*:*:wordpress:*:*"
],
"thumbs-rating": [
"cpe:2.3:a:quicoto:thumbs_rating:*:*:*:*:*:wordpress:*:*"
],
@@ -17691,6 +17889,9 @@
"tourfic": [
"cpe:2.3:a:themefic:tourfic:*:*:*:*:*:wordpress:*:*"
],
"tournamatch": [
"cpe:2.3:a:tournamatch:tournamatch:*:*:*:*:*:wordpress:*:*"
],
"tp-education": [
"cpe:2.3:a:themepalace:tp_education:*:*:*:*:*:wordpress:*:*"
],
@@ -17706,6 +17907,9 @@
"tracking-code-manager": [
"cpe:2.3:a:data443:tracking_code_manager:*:*:*:*:*:wordpress:*:*"
],
"tradedoubler-affiliate-tracker": [
"cpe:2.3:a:tradedoubler:grow:*:*:*:*:*:wordpress:*:*"
],
"tradetracker-store": [
"cpe:2.3:a:wpaffiliatefeed:tradetracker-store:*:*:*:*:*:wordpress:*:*"
],
@@ -17718,6 +17922,9 @@
"transbank-webpay-plus-rest": [
"cpe:2.3:a:transbank:transbank_webpay_rest:*:*:*:*:*:wordpress:*:*"
],
"transfinanz": [
"cpe:2.3:a:kwmsources:transfinanz:*:*:*:*:*:wordpress:*:*"
],
"transients-manager": [
"cpe:2.3:a:wpbeginner:transients_manager:*:*:*:*:*:wordpress:*:*"
],
@@ -17893,6 +18100,9 @@
"ultimate-classified-listings": [
"cpe:2.3:a:webcodingplace:ultimate_classified_listings:*:*:*:*:*:wordpress:*:*"
],
"ultimate-coming-soon": [
"cpe:2.3:a:rstheme:ultimate_coming_soon_\\\u0026_maintenance:*:*:*:*:*:wordpress:*:*"
],
"ultimate-dashboard": [
"cpe:2.3:a:davidvongries:ultimate_dashboard:*:*:*:*:*:wordpress:*:*"
],
@@ -18059,6 +18269,9 @@
"user-activity-log": [
"cpe:2.3:a:solwininfotech:user_activity_log:*:*:*:*:*:wordpress:*:*"
],
"user-activity-tracking-and-log": [
"cpe:2.3:a:mooveagency:user_activity_tracking_and_log:*:*:*:*:*:wordpress:*:*"
],
"user-avatar-reloaded": [
"cpe:2.3:a:wpexperts:user_avatar-reloaded:*:*:*:*:*:wordpress:*:*"
],
@@ -18336,6 +18549,9 @@
"wc-affiliate": [
"cpe:2.3:a:codexpert:wc_affiliate:*:*:*:*:*:wordpress:*:*"
],
"wc-checkout-getnet": [
"cpe:2.3:a:coffee-code:plugin_oficial:*:*:*:*:*:wordpress:*:*"
],
"wc-dynamic-pricing-and-discounts": [
"cpe:2.3:a:rightpress:woocommerce_dynamic_pricing_\\\u0026_discounts:*:*:*:*:*:wordpress:*:*"
],
@@ -18536,6 +18752,9 @@
"whizzy": [
"cpe:2.3:a:upqode:whizzy:*:*:*:*:*:wordpress:*:*"
],
"wholesale-market": [
"cpe:2.3:a:cedcommerce:wholesale_market:*:*:*:*:*:wordpress:*:*"
],
"wholesale-pricing-woocommerce": [
"cpe:2.3:a:wpfactory:quantity_dynamic_pricing_\\\u0026_bulk_discounts_for_woocommerce:*:*:*:*:free:wordpress:*:*"
],
@@ -18563,6 +18782,9 @@
"widget-twitter": [
"cpe:2.3:a:web-dorado:wd_widgettwitter:*:*:*:*:*:wordpress:*:*"
],
"widget4call": [
"cpe:2.3:a:apidaze:widget4call:*:*:*:*:*:wordpress:*:*"
],
"widgetkit-for-elementor": [
"cpe:2.3:a:themesgrove:all-in-one_addons_for_elementor:*:*:*:*:*:wordpress:*:*"
],
@@ -18745,6 +18967,9 @@
"woo-shipping-dpd-baltic": [
"cpe:2.3:a:dpdgroup:woocommerce_shipping:*:*:*:*:*:wordpress:*:*"
],
"woo-smart-quick-view": [
"cpe:2.3:a:wpclever:wpc_smart_quick_view_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
"woo-smart-wishlist": [
"cpe:2.3:a:wpclever:wpc_smart_wishlist_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
@@ -18812,6 +19037,9 @@
"woocommerce-amazon-affiliates-light-version": [
"cpe:2.3:a:wzone_project:wzone:*:*:*:*:lite:wordpress:*:*"
],
"woocommerce-and-wp-emember-integration": [
"cpe:2.3:a:tipsandtricks-hq:wp_emember:*:*:*:*:*:wordpress:*:*"
],
"woocommerce-catalog-enquiry": [
"cpe:2.3:a:multivendorx:product_catalog_mode_for_woocommerce:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:wc-marketplace:wc_catalog_enquiry:*:*:*:*:*:wordpress:*:*"
@@ -18997,7 +19225,8 @@
"cpe:2.3:a:wordpress_popular_posts_project:wordpress_popular_posts:*:*:*:*:*:wordpress:*:*"
],
"wordpress-popup": [
"cpe:2.3:a:incsub:hustle:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:incsub:hustle:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:wpmudev:hustle:*:*:*:*:*:wordpress:*:*"
],
"wordpress-seo": [
"cpe:2.3:a:yoast:wordpress_seo:*:*:*:*:*:wordpress:*:*",
@@ -19095,6 +19324,9 @@
"wp-ban": [
"cpe:2.3:a:wp-ban_project:wp-ban:*:*:*:*:*:wordpress:*:*"
],
"wp-base-booking-of-appointments-services-and-events": [
"cpe:2.3:a:wp-base:wp_base_booking_of_appointments\\,_services_and_events:*:*:*:*:*:wordpress:*:*"
],
"wp-best-quiz": [
"cpe:2.3:a:subina:wp_best_quiz:*:*:*:*:*:wordpress:*:*"
],
@@ -19228,6 +19460,9 @@
"wp-commentnavi": [
"cpe:2.3:a:wp-commentnavi_project:wp-commentnavi:*:*:*:*:*:wordpress:*:*"
],
"wp-compress-image-optimizer": [
"cpe:2.3:a:wpcompress:wp_compress:*:*:*:*:*:wordpress:*:*"
],
"wp-concours": [
"cpe:2.3:a:olyos:wp-concours:*:*:*:*:*:wordpress:*:*"
],
@@ -19400,6 +19635,9 @@
"wp-education": [
"cpe:2.3:a:hasthemes:wp_education:*:*:*:*:*:wordpress:*:*"
],
"wp-eggdrop": [
"cpe:2.3:a:backie:wp-eggdrop:*:*:*:*:*:wordpress:*:*"
],
"wp-email": [
"cpe:2.3:a:wp-email_project:wp-email:*:*:*:*:*:wordpress:*:*"
],
@@ -19627,6 +19865,9 @@
"wp-imageflow2": [
"cpe:2.3:a:spiffyplugins:wp_flow_plus:*:*:*:*:*:wordpress:*:*"
],
"wp-imagezoom": [
"cpe:2.3:a:aueda:wp-imagezoom:*:*:*:*:*:wordpress:*:*"
],
"wp-import-export-lite": [
"cpe:2.3:a:vjinfotech:wp_import_export_lite:*:*:*:*:*:wordpress:*:*"
],
@@ -20231,7 +20472,7 @@
"cpe:2.3:a:wp-t-wap_project:wp-t-wap:*:*:*:*:*:wordpress:*:*"
],
"wp-table-builder": [
"cpe:2.3:a:wptablebuilder:wp_table_builder:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:dotcamp:wp_table_builder:*:*:*:*:*:wordpress:*:*"
],
"wp-table-reloaded": [
"cpe:2.3:a:wp-table_reloaded_project:wp-table_reloaded:*:*:*:*:*:wordpress:*:*"
@@ -20493,7 +20734,8 @@
"cpe:2.3:a:wpforms:wpforms:*:*:*:*:pro:wordpress:*:*"
],
"wpforms-lite": [
"cpe:2.3:a:wpforms:contact_form:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:wpforms:contact_form:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:wpforms:wpforms:*:*:*:*:lite:wordpress:*:*"
],
"wpforo": [
"cpe:2.3:a:gvectors:wpforo:*:*:*:*:*:wordpress:*:*",
@@ -20552,6 +20794,9 @@
"wpmarketplace": [
"cpe:2.3:a:wpmarketplace_project:wpmarketplace:*:*:*:*:*:wordpress:*:*"
],
"wpmovielibrary": [
"cpe:2.3:a:caercam:wpmovielibrary:*:*:*:*:*:wordpress:*:*"
],
"wpo365-login": [
"cpe:2.3:a:wpo365:wordpress_\\+_azure_ad_\\/_microsoft_office_365:*:*:*:*:*:wordpress:*:*"
],
@@ -20564,6 +20809,9 @@
"wppizza": [
"cpe:2.3:a:wp-pizza:wppizza:*:*:*:*:*:wordpress:*:*"
],
"wprequal": [
"cpe:2.3:a:kevinbrent:wprequal:*:*:*:*:*:wordpress:*:*"
],
"wps-child-theme-generator": [
"cpe:2.3:a:wpserveur:wps_child_theme_generator:*:*:*:*:*:wordpress:*:*"
],
@@ -20836,6 +21084,9 @@
"z-companion": [
"cpe:2.3:a:wpzita:z_companion:*:*:*:*:*:wordpress:*:*"
],
"z-downloads": [
"cpe:2.3:a:urbanbase:z-downloads:*:*:*:*:*:wordpress:*:*"
],
"z-url-preview": [
"cpe:2.3:a:z-url_preview_project:z-url_preview:*:*:*:*:*:wordpress:*:*"
],
@@ -21222,6 +21473,9 @@
"shapely": [
"cpe:2.3:a:colorlib:shapely:*:*:*:*:*:wordpress:*:*"
],
"simplecharm": [
"cpe:2.3:a:kmfoysal06:simplecharm:*:*:*:*:*:wordpress:*:*"
],
"sinatra": [
"cpe:2.3:a:sinatrateam:sinatra:*:*:*:*:*:wordpress:*:*"
],
@@ -16,7 +16,6 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/mimetype"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format/cyclonedxjson"
@@ -509,7 +508,7 @@ func (ni nativeImagePE) fetchPkgs() (pkgs []pkg.Package, relationships []artifac
}
// fetchPkgs provides the packages available in a UnionReader.
func fetchPkgs(reader unionreader.UnionReader, filename string) ([]pkg.Package, []artifact.Relationship, error) {
func fetchPkgs(reader unionreader.UnionReader, filename string) ([]pkg.Package, []artifact.Relationship) {
var pkgs []pkg.Package
var relationships []artifact.Relationship
imageFormats := []func(string, io.ReaderAt) (nativeImage, error){newElf, newMachO, newPE}
@@ -519,9 +518,8 @@ func fetchPkgs(reader unionreader.UnionReader, filename string) ([]pkg.Package,
readers, err := unionreader.GetReaders(reader)
if err != nil {
log.Debugf("failed to open the java native-image binary: %v", err)
return nil, nil, nil
return nil, nil
}
var unknowns error
for _, r := range readers {
for _, makeNativeImage := range imageFormats {
ni, err := makeNativeImage(filename, r)
@@ -534,14 +532,13 @@ func fetchPkgs(reader unionreader.UnionReader, filename string) ([]pkg.Package,
newPkgs, newRelationships, err := ni.fetchPkgs()
if err != nil {
log.Tracef("unable to extract SBOM from possible java native-image %s: %v", filename, err)
unknowns = unknown.Join(unknowns, fmt.Errorf("unable to extract SBOM from possible java native-image %s: %w", filename, err))
continue
}
pkgs = append(pkgs, newPkgs...)
relationships = append(relationships, newRelationships...)
}
}
return pkgs, relationships, unknowns
return pkgs, relationships
}
// Catalog attempts to find any native image executables reachable from a resolver.
@@ -552,18 +549,17 @@ func (c *nativeImageCataloger) Catalog(_ context.Context, resolver file.Resolver
if err != nil {
return pkgs, nil, fmt.Errorf("failed to find binaries by mime types: %w", err)
}
var errs error
for _, location := range fileMatches {
newPkgs, newRelationships, err := processLocation(location, resolver)
if err != nil {
errs = unknown.Append(errs, location, err)
continue
return nil, nil, err
}
pkgs = append(pkgs, newPkgs...)
relationships = append(relationships, newRelationships...)
}
return pkgs, relationships, errs
return pkgs, relationships, nil
}
func processLocation(location file.Location, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
@@ -578,6 +574,6 @@ func processLocation(location file.Location, resolver file.Resolver) ([]pkg.Pack
if err != nil {
return nil, nil, err
}
pkgs, relationships, err := fetchPkgs(reader, location.RealPath)
return pkgs, relationships, err
pkgs, relationships := fetchPkgs(reader, location.RealPath)
return pkgs, relationships, nil
}
+1 -1
View File
@@ -11,5 +11,5 @@ import (
// NewOpamPackageManagerCataloger returns a new cataloger object for OCaml opam.
func NewOpamPackageManagerCataloger() pkg.Cataloger {
return generic.NewCataloger("opam-cataloger").
WithParserByGlobs(parseOpamPackage, "*opam")
WithParserByGlobs(parseOpamPackage, "**/*opam")
}
@@ -18,54 +18,54 @@ func Test_InterpreterCataloger(t *testing.T) {
fixture: "image-extensions",
expectedPkgs: []string{
// interpreters
"php-cli @ 8.3.21 (/usr/local/bin/php)",
"php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"php-cli @ 8.3.22 (/usr/local/bin/php)",
"php-fpm @ 8.3.22 (/usr/local/sbin/php-fpm)",
// extensions
"bcmath @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/bcmath.so)",
"exif @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/exif.so)",
"ftp @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ftp.so)",
"gd @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gd.so)",
"gmp @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gmp.so)",
"intl @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/intl.so)",
"ldap @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ldap.so)",
"opcache @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/opcache.so)",
"pcntl @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pcntl.so)",
"pdo_mysql @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_mysql.so)",
"pdo_pgsql @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_pgsql.so)",
"sodium @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sodium.so)",
"sysvsem @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sysvsem.so)",
"bcmath @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/bcmath.so)",
"exif @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/exif.so)",
"ftp @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ftp.so)",
"gd @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gd.so)",
"gmp @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gmp.so)",
"intl @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/intl.so)",
"ldap @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ldap.so)",
"opcache @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/opcache.so)",
"pcntl @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pcntl.so)",
"pdo_mysql @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_mysql.so)",
"pdo_pgsql @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_pgsql.so)",
"sodium @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sodium.so)",
"sysvsem @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sysvsem.so)",
"zip @ 1.22.3 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/zip.so)",
},
expectedRels: []string{
"bcmath @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/bcmath.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"bcmath @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/bcmath.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"exif @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/exif.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"exif @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/exif.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"ftp @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ftp.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"ftp @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ftp.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"gd @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gd.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"gd @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gd.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"gmp @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gmp.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"gmp @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gmp.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"intl @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/intl.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"intl @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/intl.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"ldap @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ldap.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"ldap @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ldap.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"opcache @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/opcache.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"opcache @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/opcache.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"pcntl @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pcntl.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"pcntl @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pcntl.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"pdo_mysql @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_mysql.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"pdo_mysql @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_mysql.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"pdo_pgsql @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_pgsql.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"pdo_pgsql @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_pgsql.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"sodium @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sodium.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"sodium @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sodium.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"sysvsem @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sysvsem.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"sysvsem @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sysvsem.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"zip @ 1.22.3 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/zip.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"zip @ 1.22.3 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/zip.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"bcmath @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/bcmath.so) [dependency-of] php-cli @ 8.3.22 (/usr/local/bin/php)",
"bcmath @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/bcmath.so) [dependency-of] php-fpm @ 8.3.22 (/usr/local/sbin/php-fpm)",
"exif @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/exif.so) [dependency-of] php-cli @ 8.3.22 (/usr/local/bin/php)",
"exif @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/exif.so) [dependency-of] php-fpm @ 8.3.22 (/usr/local/sbin/php-fpm)",
"ftp @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ftp.so) [dependency-of] php-cli @ 8.3.22 (/usr/local/bin/php)",
"ftp @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ftp.so) [dependency-of] php-fpm @ 8.3.22 (/usr/local/sbin/php-fpm)",
"gd @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gd.so) [dependency-of] php-cli @ 8.3.22 (/usr/local/bin/php)",
"gd @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gd.so) [dependency-of] php-fpm @ 8.3.22 (/usr/local/sbin/php-fpm)",
"gmp @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gmp.so) [dependency-of] php-cli @ 8.3.22 (/usr/local/bin/php)",
"gmp @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gmp.so) [dependency-of] php-fpm @ 8.3.22 (/usr/local/sbin/php-fpm)",
"intl @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/intl.so) [dependency-of] php-cli @ 8.3.22 (/usr/local/bin/php)",
"intl @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/intl.so) [dependency-of] php-fpm @ 8.3.22 (/usr/local/sbin/php-fpm)",
"ldap @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ldap.so) [dependency-of] php-cli @ 8.3.22 (/usr/local/bin/php)",
"ldap @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ldap.so) [dependency-of] php-fpm @ 8.3.22 (/usr/local/sbin/php-fpm)",
"opcache @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/opcache.so) [dependency-of] php-cli @ 8.3.22 (/usr/local/bin/php)",
"opcache @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/opcache.so) [dependency-of] php-fpm @ 8.3.22 (/usr/local/sbin/php-fpm)",
"pcntl @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pcntl.so) [dependency-of] php-cli @ 8.3.22 (/usr/local/bin/php)",
"pcntl @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pcntl.so) [dependency-of] php-fpm @ 8.3.22 (/usr/local/sbin/php-fpm)",
"pdo_mysql @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_mysql.so) [dependency-of] php-cli @ 8.3.22 (/usr/local/bin/php)",
"pdo_mysql @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_mysql.so) [dependency-of] php-fpm @ 8.3.22 (/usr/local/sbin/php-fpm)",
"pdo_pgsql @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_pgsql.so) [dependency-of] php-cli @ 8.3.22 (/usr/local/bin/php)",
"pdo_pgsql @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_pgsql.so) [dependency-of] php-fpm @ 8.3.22 (/usr/local/sbin/php-fpm)",
"sodium @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sodium.so) [dependency-of] php-cli @ 8.3.22 (/usr/local/bin/php)",
"sodium @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sodium.so) [dependency-of] php-fpm @ 8.3.22 (/usr/local/sbin/php-fpm)",
"sysvsem @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sysvsem.so) [dependency-of] php-cli @ 8.3.22 (/usr/local/bin/php)",
"sysvsem @ 8.3.22 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sysvsem.so) [dependency-of] php-fpm @ 8.3.22 (/usr/local/sbin/php-fpm)",
"zip @ 1.22.3 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/zip.so) [dependency-of] php-cli @ 8.3.22 (/usr/local/bin/php)",
"zip @ 1.22.3 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/zip.so) [dependency-of] php-fpm @ 8.3.22 (/usr/local/sbin/php-fpm)",
},
},
{
@@ -716,10 +716,15 @@ func Test_PackageCataloger_SitePackageRelationships(t *testing.T) {
"certifi @ 2020.12.5 (/usr/local/lib/python3.9/dist-packages) [dependency-of] requests @ 2.25.0 (/usr/local/lib/python3.9/dist-packages)",
"certifi @ 2020.12.5 (/usr/local/lib/python3.9/dist-packages) [dependency-of] urllib3 @ 1.26.18 (/usr/local/lib/python3.9/dist-packages)", // available when extra == "secure", but another dependency is primarily installing it
"chardet @ 3.0.4 (/usr/local/lib/python3.9/dist-packages) [dependency-of] requests @ 2.25.0 (/usr/local/lib/python3.9/dist-packages)",
"distlib @ 0.3.9 (/usr/local/lib/python3.9/dist-packages) [dependency-of] virtualenv @ 20.31.2 (/usr/local/lib/python3.9/dist-packages)",
"filelock @ 3.18.0 (/usr/local/lib/python3.9/dist-packages) [dependency-of] virtualenv @ 20.31.2 (/usr/local/lib/python3.9/dist-packages)",
"idna @ 2.10 (/usr/local/lib/python3.9/dist-packages) [dependency-of] requests @ 2.25.0 (/usr/local/lib/python3.9/dist-packages)",
"idna @ 2.10 (/usr/local/lib/python3.9/dist-packages) [dependency-of] urllib3 @ 1.26.18 (/usr/local/lib/python3.9/dist-packages)", // available when extra == "secure", but another dependency is primarily installing it
"platformdirs @ 4.3.8 (/usr/local/lib/python3.9/dist-packages) [dependency-of] virtualenv @ 20.31.2 (/usr/local/lib/python3.9/dist-packages)",
"six @ 1.16.0 (/usr/local/lib/python3.9/dist-packages) [dependency-of] blessed @ 1.20.0 (/usr/local/lib/python3.9/dist-packages)",
"soupsieve @ 2.2.1 (/usr/local/lib/python3.9/dist-packages) [dependency-of] beautifulsoup4 @ 4.9.3 (/usr/local/lib/python3.9/dist-packages)",
"urllib3 @ 1.26.18 (/usr/local/lib/python3.9/dist-packages) [dependency-of] requests @ 2.25.0 (/usr/local/lib/python3.9/dist-packages)",
"virtualenv @ 20.31.2 (/usr/local/lib/python3.9/dist-packages) [dependency-of] filelock @ 3.18.0 (/usr/local/lib/python3.9/dist-packages)", // available when extra == "testing", but we are installing it
"wcwidth @ 0.2.13 (/usr/local/lib/python3.9/dist-packages) [dependency-of] blessed @ 1.20.0 (/usr/local/lib/python3.9/dist-packages)",
// purely python 3.8 dist-packages
@@ -736,9 +741,14 @@ func Test_PackageCataloger_SitePackageRelationships(t *testing.T) {
"certifi @ 2020.12.5 (/usr/local/lib/python3.8/dist-packages) [dependency-of] requests @ 2.25.0 (/usr/local/lib/python3.8/dist-packages)",
"certifi @ 2020.12.5 (/usr/local/lib/python3.8/dist-packages) [dependency-of] urllib3 @ 1.26.18 (/usr/local/lib/python3.8/dist-packages)", // available when extra == "secure", but another dependency is primarily installing it
"chardet @ 3.0.4 (/usr/local/lib/python3.8/dist-packages) [dependency-of] requests @ 2.25.0 (/usr/local/lib/python3.8/dist-packages)",
"distlib @ 0.3.9 (/usr/local/lib/python3.8/dist-packages) [dependency-of] virtualenv @ 20.31.2 (/usr/local/lib/python3.8/dist-packages)",
"filelock @ 3.16.1 (/usr/local/lib/python3.8/dist-packages) [dependency-of] virtualenv @ 20.31.2 (/usr/local/lib/python3.8/dist-packages)",
"idna @ 2.10 (/usr/local/lib/python3.8/dist-packages) [dependency-of] requests @ 2.25.0 (/usr/local/lib/python3.8/dist-packages)",
"idna @ 2.10 (/usr/local/lib/python3.8/dist-packages) [dependency-of] urllib3 @ 1.26.18 (/usr/local/lib/python3.8/dist-packages)", // available when extra == "secure", but another dependency is primarily installing it
"platformdirs @ 4.3.6 (/usr/local/lib/python3.8/dist-packages) [dependency-of] virtualenv @ 20.31.2 (/usr/local/lib/python3.8/dist-packages)",
"soupsieve @ 2.2 (/usr/local/lib/python3.8/dist-packages) [dependency-of] beautifulsoup4 @ 4.9.2 (/usr/local/lib/python3.8/dist-packages)",
"urllib3 @ 1.26.18 (/usr/local/lib/python3.8/dist-packages) [dependency-of] requests @ 2.25.0 (/usr/local/lib/python3.8/dist-packages)",
"virtualenv @ 20.31.2 (/usr/local/lib/python3.8/dist-packages) [dependency-of] filelock @ 3.16.1 (/usr/local/lib/python3.8/dist-packages)", // available when extra == "testing", but we are installing it
"xmod @ 1.8.1 (/usr/local/lib/python3.8/dist-packages) [dependency-of] runs @ 1.2.2 (/usr/local/lib/python3.8/dist-packages)",
// project 1 virtual env
@@ -759,6 +769,7 @@ func Test_PackageCataloger_SitePackageRelationships(t *testing.T) {
"blessed @ 1.20.0 (/usr/local/lib/python3.9/dist-packages) [dependency-of] inquirer @ 3.0.0 (/app/project1/venv/lib/python3.9/site-packages)", // note: depends on global site package!
"python-editor @ 1.0.4 (/usr/local/lib/python3.9/dist-packages) [dependency-of] inquirer @ 3.0.0 (/app/project1/venv/lib/python3.9/site-packages)", // note: depends on global site package!
"readchar @ 4.2.1 (/app/project1/venv/lib/python3.9/site-packages) [dependency-of] inquirer @ 3.0.0 (/app/project1/venv/lib/python3.9/site-packages)",
"setuptools @ 44.0.0 (/app/project1/venv/lib/python3.9/site-packages) [dependency-of] virtualenv @ 20.31.2 (/usr/local/lib/python3.9/dist-packages)", // available when extra == "test", but we are installing it
"soupsieve @ 2.3 (/app/project1/venv/lib/python3.9/site-packages) [dependency-of] beautifulsoup4 @ 4.10.0 (/app/project1/venv/lib/python3.9/site-packages)",
// project 2 virtual env
+6 -4
View File
@@ -115,14 +115,16 @@ func wheelEggDependencySpecifier(p pkg.Package) dependency.Specification {
}
}
// extractPackageName removes any extras or version constraints from a given Requires-Dist field value (and
// extractPackageName removes any extras, version constraints or environment markers from a given Requires-Dist field value (and
// semantically similar fields), leaving only the package name.
func extractPackageName(s string) string {
// examples:
// html5lib ; extra == 'html5lib' --> html5lib
// soupsieve (>1.2) --> soupsieve
// requests [security,tests] --> requests
// requests >= 2.8.1 --> requests
// requests (>= 2.8.1) --> requests
// requests ; python_version < "2.7" --> requests
return strings.TrimSpace(internal.SplitAny(s, "(;")[0])
return strings.TrimSpace(internal.SplitAny(s, "[(<!=>~;")[0])
}
func extractPackageNames(ss []string) []string {
var names []string
+18 -19
View File
@@ -4,7 +4,6 @@ import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"path"
"path/filepath"
@@ -16,6 +15,7 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/licenses"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
@@ -26,17 +26,15 @@ import (
// fields are governed by the PyPA core metadata specification (https://packaging.python.org/en/latest/specifications/core-metadata/).
func parseWheelOrEgg(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
pd, sources, err := assembleEggOrWheelMetadata(resolver, reader.Location)
if err != nil {
return nil, nil, err
}
if pd == nil {
return nil, nil, nil
return nil, nil, err
}
// This can happen for Python 2.7 where it is reported from an egg-info, but Python is
// the actual runtime, it isn't a "package". The special-casing here allows to skip it
if pd.Name == "Python" {
return nil, nil, nil
return nil, nil, err
}
pkgs := []pkg.Package{
@@ -47,11 +45,11 @@ func parseWheelOrEgg(ctx context.Context, resolver file.Resolver, _ *generic.Env
),
}
return pkgs, nil, nil
return pkgs, nil, err
}
// fetchInstalledFiles finds a corresponding installed-files.txt file for the given python package metadata file and returns the set of file records contained.
func fetchInstalledFiles(resolver file.Resolver, metadataLocation file.Location, sitePackagesRootPath string) (files []pkg.PythonFileRecord, sources []file.Location, err error) {
func fetchInstalledFiles(resolver file.Resolver, metadataLocation file.Location, sitePackagesRootPath string) (files []pkg.PythonFileRecord, sources []file.Location, retErr error) {
// we've been given a file reference to a specific wheel METADATA file. note: this may be for a directory
// or for an image... for an image the METADATA file may be present within multiple layers, so it is important
// to reconcile the installed-files.txt path to the same layer (or the next adjacent lower layer).
@@ -72,8 +70,7 @@ func fetchInstalledFiles(resolver file.Resolver, metadataLocation file.Location,
// parse the installed-files contents
installedFiles, err := parseInstalledFiles(installedFilesContents, metadataLocation.RealPath, sitePackagesRootPath)
if err != nil {
log.WithFields("error", err, "path", metadataLocation.RealPath).Trace("unable to parse installed-files.txt for python package")
return files, sources, nil
retErr = unknown.Newf(*installedFilesRef, "unable to parse installed-files.txt for python package: %w", retErr)
}
files = append(files, installedFiles...)
@@ -82,7 +79,7 @@ func fetchInstalledFiles(resolver file.Resolver, metadataLocation file.Location,
}
// fetchRecordFiles finds a corresponding RECORD file for the given python package metadata file and returns the set of file records contained.
func fetchRecordFiles(resolver file.Resolver, metadataLocation file.Location) (files []pkg.PythonFileRecord, sources []file.Location, err error) {
func fetchRecordFiles(resolver file.Resolver, metadataLocation file.Location) (files []pkg.PythonFileRecord, sources []file.Location, retErr error) {
// we've been given a file reference to a specific wheel METADATA file. note: this may be for a directory
// or for an image... for an image the METADATA file may be present within multiple layers, so it is important
// to reconcile the RECORD path to the same layer (or the next adjacent lower layer).
@@ -101,11 +98,12 @@ func fetchRecordFiles(resolver file.Resolver, metadataLocation file.Location) (f
defer internal.CloseAndLogError(recordContents, recordPath)
// parse the record contents
records := parseWheelOrEggRecord(recordContents)
var records []pkg.PythonFileRecord
records, retErr = parseWheelOrEggRecord(file.NewLocationReadCloser(*recordRef, recordContents))
files = append(files, records...)
}
return files, sources, nil
return files, sources, retErr
}
// fetchTopLevelPackages finds a corresponding top_level.txt file for the given python package metadata file and returns the set of package names contained.
@@ -133,7 +131,7 @@ func fetchTopLevelPackages(resolver file.Resolver, metadataLocation file.Locatio
}
if err := scanner.Err(); err != nil {
return nil, nil, fmt.Errorf("could not read python package top_level.txt: %w", err)
return nil, nil, err
}
return pkgs, sources, nil
@@ -216,14 +214,15 @@ func assembleEggOrWheelMetadata(resolver file.Resolver, metadataLocation file.Lo
}
// attach any python files found for the given wheel/egg installation
var errs error
r, s, err := fetchRecordFiles(resolver, metadataLocation)
if err != nil {
return nil, nil, err
errs = unknown.Joinf(errs, "could not read python package RECORD file: %w", err)
}
if len(r) == 0 {
r, s, err = fetchInstalledFiles(resolver, metadataLocation, pd.SitePackagesRootPath)
if err != nil {
return nil, nil, err
errs = unknown.Joinf(errs, "could not read python package installed-files.txt: %w", err)
}
}
@@ -233,7 +232,7 @@ func assembleEggOrWheelMetadata(resolver file.Resolver, metadataLocation file.Lo
// attach any top-level package names found for the given wheel/egg installation
p, s, err := fetchTopLevelPackages(resolver, metadataLocation)
if err != nil {
return nil, nil, err
errs = unknown.Joinf(errs, "could not read python package top_level.txt: %w", err)
}
sources = append(sources, s...)
pd.TopLevelPackages = p
@@ -241,12 +240,12 @@ func assembleEggOrWheelMetadata(resolver file.Resolver, metadataLocation file.Lo
// attach any direct-url package data found for the given wheel/egg installation
d, s, err := fetchDirectURLData(resolver, metadataLocation)
if err != nil {
return nil, nil, err
errs = unknown.Joinf(errs, "could not read python package direct_url.json: %w", err)
}
sources = append(sources, s...)
pd.DirectURLOrigin = d
return &pd, sources, nil
return &pd, sources, errs
}
func findLicenses(ctx context.Context, resolver file.Resolver, m parsedData) pkg.LicenseSet {
@@ -10,23 +10,31 @@ import (
"strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
// parseWheelOrEggRecord takes a Python Egg or Wheel (which share the same format and values for our purposes),
// returning all Python packages listed.
func parseWheelOrEggRecord(reader io.Reader) []pkg.PythonFileRecord {
func parseWheelOrEggRecord(reader file.LocationReadCloser) ([]pkg.PythonFileRecord, error) {
var records []pkg.PythonFileRecord
r := csv.NewReader(reader)
for {
recordList, err := r.Read()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
log.Debugf("unable to read python record file: %w", err)
continue
if errors.Is(err, io.EOF) {
break
}
var parseErr *csv.ParseError
if errors.As(err, &parseErr) {
log.WithFields("error", parseErr).Debug("unable to read python record entry (skipping entry)")
continue
}
// probably an I/O error... we could have missed some package content, so we include this location as an unknown
return records, unknown.Newf(reader.Coordinates, "unable to read python record file: %w", err)
}
if len(recordList) != 3 {
@@ -62,7 +70,7 @@ func parseWheelOrEggRecord(reader io.Reader) []pkg.PythonFileRecord {
records = append(records, record)
}
return records
return records, nil
}
func parseInstalledFiles(reader io.Reader, location, sitePackagesRootPath string) ([]pkg.PythonFileRecord, error) {
@@ -5,7 +5,9 @@ import (
"testing"
"github.com/go-test/deep"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
@@ -46,7 +48,8 @@ func TestParseWheelEggRecord(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err)
}
actual := parseWheelOrEggRecord(fixture)
actual, err := parseWheelOrEggRecord(file.NewLocationReadCloser(file.NewLocation(test.Fixture), fixture))
require.NoError(t, err, "failed to parse: %+v", err)
for _, d := range deep.Equal(actual, test.ExpectedMetadata) {
t.Errorf("diff: %+v", d)
@@ -59,7 +59,7 @@ func TestTerraformCataloger(t *testing.T) {
Metadata: pkg.TerraformLockProviderEntry{
URL: "registry.terraform.io/hashicorp/google",
Version: "6.8.0",
Constraints: "6.8.0",
Constraints: "",
Hashes: []string{
"h1:GlCaVPk6eKMg2ZbRY7C5tUeHGNIABT+qFtMl8+XWZHM=",
"zh:1b78f4451f1617092eb6891c9c13eda79671060601c40947feea6794c732157a",
@@ -26,7 +26,6 @@ provider "registry.terraform.io/hashicorp/aws" {
provider "registry.terraform.io/hashicorp/google" {
version = "6.8.0"
constraints = "6.8.0"
hashes = [
"h1:GlCaVPk6eKMg2ZbRY7C5tUeHGNIABT+qFtMl8+XWZHM=",
"zh:1b78f4451f1617092eb6891c9c13eda79671060601c40947feea6794c732157a",
+1 -1
View File
@@ -3,7 +3,7 @@ package pkg
// TerraformLockProviderEntry represents a single provider entry in a Terraform dependency lock file (.terraform.lock.hcl).
type TerraformLockProviderEntry struct {
URL string `hcl:",label" json:"url"`
Constraints string `hcl:"constraints" json:"constraints"`
Constraints string `hcl:"constraints,optional" json:"constraints"`
Version string `hcl:"version" json:"version"`
Hashes []string `hcl:"hashes" json:"hashes"`
}
+23
View File
@@ -0,0 +1,23 @@
package source
import "github.com/anchore/syft/syft/file"
type SnapMetadata struct {
// Summary is a brief description of the snap package
Summary string `yaml:"summary" json:"summary,omitempty"`
// Base is the base snap this package builds upon
Base string `yaml:"base" json:"base,omitempty"`
// Grade is the development stage (stable, candidate, beta, edge)
Grade string `yaml:"grade" json:"grade,omitempty"`
// Confinement is the security isolation level (strict, classic, devmode)
Confinement string `yaml:"confinement" json:"confinement,omitempty"`
// Architectures are the supported CPU architectures
Architectures []string `yaml:"architectures" json:"architectures,omitempty"`
// Digests are hashes of the snap squashfs files
Digests []file.Digest `yaml:"digests" json:"digests,omitempty"`
}
+56
View File
@@ -0,0 +1,56 @@
package snapsource
import (
"fmt"
"github.com/goccy/go-yaml"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/file"
)
type snapManifest struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Summary string `yaml:"summary"`
Base string `yaml:"base"`
Grade string `yaml:"grade"`
Confinement string `yaml:"confinement"`
Architectures []string `yaml:"architectures"`
}
const manifestLocation = "/meta/snap.yaml"
func parseManifest(resolver file.Resolver) (*snapManifest, error) {
locations, err := resolver.FilesByPath(manifestLocation)
if err != nil {
return nil, fmt.Errorf("unable to find snap manifest file: %w", err)
}
if len(locations) == 0 {
return nil, fmt.Errorf("no snap manifest file found")
}
if len(locations) > 1 {
return nil, fmt.Errorf("multiple snap manifest files found")
}
manifestFile := locations[0]
reader, err := resolver.FileContentsByLocation(manifestFile)
if err != nil {
return nil, fmt.Errorf("unable to read snap manifest file: %w", err)
}
defer internal.CloseAndLogError(reader, manifestFile.RealPath)
var manifest snapManifest
if err := yaml.NewDecoder(reader).Decode(&manifest); err != nil {
return nil, fmt.Errorf("unable to decode snap manifest file: %w", err)
}
if manifest.Name == "" || manifest.Version == "" {
return nil, fmt.Errorf("invalid snap manifest file: missing name or version")
}
return &manifest, nil
}
+249
View File
@@ -0,0 +1,249 @@
package snapsource
import (
"context"
"crypto"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"github.com/spf13/afero"
stereoFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal/bus"
intFile "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/event/monitor"
"github.com/anchore/syft/syft/file"
)
type snapFile struct {
Path string
Digests []file.Digest
MimeType string
Cleanup func() error
}
type remoteSnap struct {
snapIdentity
URL string
}
type snapIdentity struct {
Name string
Channel string
Architecture string
}
func (s snapIdentity) String() string {
parts := []string{s.Name}
if s.Channel != "" {
parts = append(parts, fmt.Sprintf("@%s", s.Channel))
}
if s.Architecture != "" {
parts = append(parts, fmt.Sprintf(" (%s)", s.Architecture))
}
return strings.Join(parts, "")
}
func getRemoteSnapFile(ctx context.Context, fs afero.Fs, getter intFile.Getter, cfg Config) (*snapFile, error) {
if cfg.Request == "" {
return nil, fmt.Errorf("invalid request: %q", cfg.Request)
}
var architecture string
if cfg.Platform != nil {
architecture = cfg.Platform.Architecture
}
info, err := resolveRemoteSnap(cfg.Request, architecture)
if err != nil {
return nil, err
}
return newSnapFileFromRemote(ctx, fs, cfg, getter, info)
}
func newSnapFileFromRemote(ctx context.Context, fs afero.Fs, cfg Config, getter intFile.Getter, info *remoteSnap) (*snapFile, error) {
t, err := afero.TempDir(fs, "", "syft-snap-")
if err != nil {
return nil, fmt.Errorf("failed to create temp directory: %w", err)
}
snapFilePath := path.Join(t, path.Base(info.URL))
err = downloadSnap(getter, info, snapFilePath)
if err != nil {
return nil, fmt.Errorf("failed to download snap file: %w", err)
}
closer := func() error {
return fs.RemoveAll(t)
}
mimeType, digests, err := getSnapFileInfo(ctx, fs, snapFilePath, cfg.DigestAlgorithms)
if err != nil {
return nil, err
}
return &snapFile{
Path: snapFilePath,
Digests: digests,
MimeType: mimeType,
Cleanup: closer,
}, nil
}
func newSnapFromFile(ctx context.Context, fs afero.Fs, cfg Config) (*snapFile, error) {
var architecture string
if cfg.Platform != nil {
architecture = cfg.Platform.Architecture
}
if architecture != "" {
return nil, fmt.Errorf("architecture cannot be specified for local snap files: %q", cfg.Request)
}
absPath, err := filepath.Abs(cfg.Request)
if err != nil {
return nil, fmt.Errorf("unable to get absolute path of snap: %w", err)
}
mimeType, digests, err := getSnapFileInfo(ctx, fs, absPath, cfg.DigestAlgorithms)
if err != nil {
return nil, err
}
return &snapFile{
Path: absPath,
Digests: digests,
MimeType: mimeType,
// note: we have no closer since this is the user's file (never delete it)
}, nil
}
func getSnapFileInfo(ctx context.Context, fs afero.Fs, path string, hashes []crypto.Hash) (string, []file.Digest, error) {
fileMeta, err := fs.Stat(path)
if err != nil {
return "", nil, fmt.Errorf("unable to stat path=%q: %w", path, err)
}
if fileMeta.IsDir() {
return "", nil, fmt.Errorf("given path is a directory, not a snap file: %q", path)
}
fh, err := fs.Open(path)
if err != nil {
return "", nil, fmt.Errorf("unable to open file=%q: %w", path, err)
}
defer fh.Close()
mimeType := stereoFile.MIMEType(fh)
if !isSquashFSFile(mimeType, path) {
return "", nil, fmt.Errorf("not a valid squashfs/snap file: %q (mime-type=%q)", path, mimeType)
}
var digests []file.Digest
if len(hashes) > 0 {
if _, err := fh.Seek(0, 0); err != nil {
return "", nil, fmt.Errorf("unable to reset file position: %w", err)
}
digests, err = intFile.NewDigestsFromFile(ctx, fh, hashes)
if err != nil {
return "", nil, fmt.Errorf("unable to calculate digests for file=%q: %w", path, err)
}
}
return mimeType, digests, nil
}
// resolveRemoteSnap parses a snap request and returns the appropriate path or URL
// The request can be:
// - A snap name (e.g., "etcd")
// - A snap name with channel (e.g., "etcd@beta" or "etcd@2.3/stable")
func resolveRemoteSnap(request, architecture string) (*remoteSnap, error) {
if architecture == "" {
architecture = defaultArchitecture
}
snapName, channel := parseSnapRequest(request)
id := snapIdentity{
Name: snapName,
Channel: channel,
Architecture: architecture,
}
client := newSnapcraftClient()
downloadURL, err := client.GetSnapDownloadURL(id)
if err != nil {
return nil, err
}
log.WithFields("url", downloadURL, "name", snapName, "channel", channel, "architecture", architecture).Debugf("snap resolved")
return &remoteSnap{
snapIdentity: id,
URL: downloadURL,
}, nil
}
// parseSnapRequest parses a snap request into name and channel
// Examples:
// - "etcd" -> name="etcd", channel="stable" (default)
// - "etcd@beta" -> name="etcd", channel="beta"
// - "etcd@2.3/stable" -> name="etcd", channel="2.3/stable"
func parseSnapRequest(request string) (name, channel string) {
parts := strings.SplitN(request, "@", 2)
name = parts[0]
if len(parts) == 2 {
channel = parts[1]
}
if channel == "" {
channel = defaultChannel
}
return name, channel
}
func downloadSnap(getter intFile.Getter, info *remoteSnap, dest string) error {
log.WithFields("url", info.URL, "destination", dest).Debug("downloading snap file")
prog := bus.StartPullSourceTask(monitor.GenericTask{
Title: monitor.Title{
Default: "Download snap",
WhileRunning: "Downloading snap",
OnSuccess: "Downloaded snap",
},
HideOnSuccess: false,
HideStageOnSuccess: true,
ID: "",
ParentID: "",
Context: info.String(),
}, -1, "")
if err := getter.GetFile(dest, info.URL, prog.Manual); err != nil {
prog.SetError(err)
return fmt.Errorf("failed to download snap file at %q: %w", info.URL, err)
}
prog.SetCompleted()
return nil
}
// fileExists checks if a file exists and is not a directory
func fileExists(fs afero.Fs, path string) bool {
info, err := fs.Stat(path)
if os.IsNotExist(err) {
return false
}
return err == nil && !info.IsDir()
}
+378
View File
@@ -0,0 +1,378 @@
package snapsource
import (
"context"
"crypto"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"github.com/OneOfOne/xxhash"
diskFile "github.com/diskfs/go-diskfs/backend/file"
"github.com/diskfs/go-diskfs/filesystem"
"github.com/diskfs/go-diskfs/filesystem/squashfs"
"github.com/hashicorp/go-cleanhttp"
"github.com/opencontainers/go-digest"
"github.com/spf13/afero"
"github.com/anchore/clio"
"github.com/anchore/go-homedir"
stereoFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/internal/bus"
intFile "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/event/monitor"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/fileresolver"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/internal"
)
var _ source.Source = (*snapSource)(nil)
type Config struct {
ID clio.Identification
Request string
Platform *image.Platform
Exclude source.ExcludeConfig
DigestAlgorithms []crypto.Hash
Alias source.Alias
fs afero.Fs
}
type snapSource struct {
id artifact.ID
config Config
resolver file.Resolver
mutex *sync.Mutex
manifest snapManifest
digests []file.Digest
fs filesystem.FileSystem
squashfsPath string
squashFileCloser func() error
closer func() error
}
func NewFromLocal(cfg Config) (source.Source, error) {
f, err := getLocalSnapFile(&cfg)
if err != nil {
return nil, err
}
return newFromPath(cfg, f)
}
func getLocalSnapFile(cfg *Config) (*snapFile, error) {
expandedPath, err := homedir.Expand(cfg.Request)
if err != nil {
return nil, fmt.Errorf("unable to expand path %q: %w", cfg.Request, err)
}
cfg.Request = filepath.Clean(expandedPath)
if cfg.fs == nil {
cfg.fs = afero.NewOsFs()
}
if !fileExists(cfg.fs, cfg.Request) {
return nil, fmt.Errorf("snap file %q does not exist", cfg.Request)
}
log.WithFields("path", cfg.Request).Debug("snap is a local file")
return newSnapFromFile(context.Background(), cfg.fs, *cfg)
}
func NewFromRemote(cfg Config) (source.Source, error) {
expandedPath, err := homedir.Expand(cfg.Request)
if err != nil {
return nil, fmt.Errorf("unable to expand path %q: %w", cfg.Request, err)
}
cfg.Request = filepath.Clean(expandedPath)
if cfg.fs == nil {
cfg.fs = afero.NewOsFs()
}
client := intFile.NewGetter(cfg.ID, cleanhttp.DefaultClient())
f, err := getRemoteSnapFile(context.Background(), cfg.fs, client, cfg)
if err != nil {
return nil, err
}
return newFromPath(cfg, f)
}
func newFromPath(cfg Config, f *snapFile) (source.Source, error) {
s := &snapSource{
id: deriveID(cfg.Request, cfg.Alias.Name, cfg.Alias.Version, f.Digests),
config: cfg,
mutex: &sync.Mutex{},
digests: f.Digests,
squashfsPath: f.Path,
closer: f.Cleanup,
}
return s, s.extractManifest()
}
func (s *snapSource) extractManifest() error {
r, err := s.FileResolver(source.SquashedScope)
if err != nil {
return fmt.Errorf("unable to create snap file resolver: %w", err)
}
manifest, err := parseManifest(r)
if err != nil {
return fmt.Errorf("unable to parse snap manifest file: %w", err)
}
if manifest != nil {
s.manifest = *manifest
}
return nil
}
func (s snapSource) ID() artifact.ID {
return s.id
}
func (s snapSource) NameVersion() (string, string) {
name := s.manifest.Name
version := s.manifest.Version
if !s.config.Alias.IsEmpty() {
a := s.config.Alias
if a.Name != "" {
name = a.Name
}
if a.Version != "" {
version = a.Version
}
}
return name, version
}
func (s snapSource) Describe() source.Description {
name, version := s.NameVersion()
return source.Description{
ID: string(s.id),
Name: name,
Version: version,
Metadata: source.SnapMetadata{
Summary: s.manifest.Summary,
Base: s.manifest.Base,
Grade: s.manifest.Grade,
Confinement: s.manifest.Confinement,
Architectures: s.manifest.Architectures,
Digests: s.digests,
},
}
}
func (s *snapSource) Close() error {
if s.squashFileCloser != nil {
if err := s.squashFileCloser(); err != nil {
return fmt.Errorf("unable to close snap resolver: %w", err)
}
s.squashFileCloser = nil
}
s.resolver = nil
if s.fs != nil {
if err := s.fs.Close(); err != nil {
return fmt.Errorf("unable to close snap squashfs: %w", err)
}
}
if s.closer != nil {
if err := s.closer(); err != nil {
return fmt.Errorf("unable to close snap source: %w", err)
}
}
return nil
}
func (s *snapSource) FileResolver(_ source.Scope) (file.Resolver, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.resolver != nil {
return s.resolver, nil
}
log.Debugf("parsing squashfs file: %s", s.squashfsPath)
f, err := os.Open(s.squashfsPath)
if err != nil {
return nil, fmt.Errorf("unable to open squashfs file: %w", err)
}
s.squashFileCloser = func() error {
if err := f.Close(); err != nil {
return fmt.Errorf("unable to close squashfs file: %w", err)
}
return nil
}
fileMeta, err := f.Stat()
if err != nil {
return nil, fmt.Errorf("unable to stat squashfs file: %w", err)
}
size := fileMeta.Size()
fileCatalog := image.NewFileCatalog()
prog := bus.StartIndexingFiles(filepath.Base(s.squashfsPath))
b := diskFile.New(f, true)
fs, err := squashfs.Read(b, fileMeta.Size(), 0, 0)
if err != nil {
err := fmt.Errorf("unable to open squashfs file: %w", err)
prog.SetError(err)
return nil, err
}
tree := filetree.New()
if err := intFile.WalkDiskDir(fs, "/", squashfsVisitor(tree, fileCatalog, &size, prog)); err != nil {
err := fmt.Errorf("failed to walk squashfs file=%q: %w", s.squashfsPath, err)
prog.SetError(err)
return nil, err
}
prog.SetCompleted()
s.resolver = &fileresolver.FiletreeResolver{
Chroot: fileresolver.ChrootContext{},
Tree: tree,
Index: fileCatalog.Index,
SearchContext: filetree.NewSearchContext(tree, fileCatalog.Index),
Opener: func(ref stereoFile.Reference) (io.ReadCloser, error) {
return fileCatalog.Open(ref)
},
}
s.fs = fs
return s.resolver, nil
}
type linker interface {
Readlink() (string, error)
}
func squashfsVisitor(ft filetree.Writer, fileCatalog *image.FileCatalog, size *int64, prog *monitor.TaskProgress) intFile.WalkDiskDirFunc {
builder := filetree.NewBuilder(ft, fileCatalog.Index)
return func(fsys filesystem.FileSystem, path string, d os.FileInfo, walkErr error) error {
if walkErr != nil {
log.WithFields("error", walkErr, "path", path).Trace("unable to walk squash file path")
return walkErr
}
prog.AtomicStage.Set(path)
var f filesystem.File
var mimeType string
var err error
if !d.IsDir() {
f, err = fsys.OpenFile(path, os.O_RDONLY)
if err != nil {
log.WithFields("error", err, "path", path).Trace("unable to open squash file path")
} else {
defer f.Close()
mimeType = stereoFile.MIMEType(f)
}
}
var ty stereoFile.Type
var linkPath string
switch {
case d.IsDir():
// in some implementations, the mode does not indicate a directory, so we check the FileInfo type explicitly
ty = stereoFile.TypeDirectory
default:
ty = stereoFile.TypeFromMode(d.Mode())
if ty == stereoFile.TypeSymLink && f != nil {
if l, ok := f.(linker); ok {
linkPath, _ = l.Readlink()
}
}
}
metadata := stereoFile.Metadata{
FileInfo: d,
Path: path,
LinkDestination: linkPath,
Type: ty,
MIMEType: mimeType,
}
fileReference, err := builder.Add(metadata)
if err != nil {
return err
}
if fileReference == nil {
return nil
}
if size != nil {
*(size) += metadata.Size()
}
fileCatalog.AssociateOpener(*fileReference, func() (io.ReadCloser, error) {
return fsys.OpenFile(path, os.O_RDONLY)
})
prog.Increment()
return nil
}
}
func isSquashFSFile(mimeType, path string) bool {
if mimeType == "application/vnd.squashfs" || mimeType == "application/x-squashfs" {
return true
}
ext := filepath.Ext(path)
return ext == ".snap" || ext == ".squashfs"
}
func deriveID(path, name, version string, digests []file.Digest) artifact.ID {
var xxhDigest string
for _, d := range digests {
if strings.ToLower(strings.ReplaceAll(d.Algorithm, "-", "")) == "xxh64" {
xxhDigest = d.Value
break
}
}
if xxhDigest == "" {
xxhDigest = digestOfFileContents(path)
}
info := fmt.Sprintf("%s:%s@%s", xxhDigest, name, version)
return internal.ArtifactIDFromDigest(digest.SHA256.FromString(info).String())
}
// return the xxhash64 of the file contents, or the xxhash64 of the path if the file cannot be read
func digestOfFileContents(path string) string {
f, err := os.Open(path)
if err != nil {
return digestOfReader(strings.NewReader(path))
}
defer f.Close()
return digestOfReader(f)
}
func digestOfReader(r io.Reader) string {
hasher := xxhash.New64()
_, _ = io.Copy(hasher, r)
return fmt.Sprintf("%x", hasher.Sum(nil))
}
@@ -0,0 +1,54 @@
package snapsource
import (
"context"
"crypto"
"github.com/anchore/syft/syft/source"
)
type snapSourceProvider struct {
local bool
path string
exclude source.ExcludeConfig
digestAlgorithms []crypto.Hash
alias source.Alias
}
// NewLocalSourceProvider creates a new provider for snap files from a local path.
func NewLocalSourceProvider(path string, exclude source.ExcludeConfig, digestAlgorithms []crypto.Hash, alias source.Alias) source.Provider {
return &snapSourceProvider{
local: true,
path: path,
exclude: exclude,
digestAlgorithms: digestAlgorithms,
alias: alias,
}
}
// NewRemoteSourceProvider creates a new provider for snap files from a remote location.
func NewRemoteSourceProvider(path string, exclude source.ExcludeConfig, digestAlgorithms []crypto.Hash, alias source.Alias) source.Provider {
return &snapSourceProvider{
path: path,
exclude: exclude,
digestAlgorithms: digestAlgorithms,
alias: alias,
}
}
func (p snapSourceProvider) Name() string {
return "snap"
}
func (p snapSourceProvider) Provide(_ context.Context) (source.Source, error) {
cfg := Config{
Request: p.path,
Exclude: p.exclude,
DigestAlgorithms: p.digestAlgorithms,
Alias: p.alias,
}
if p.local {
return NewFromLocal(cfg)
}
return NewFromRemote(cfg)
}
@@ -0,0 +1,86 @@
package snapsource
import (
"crypto"
"fmt"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/go-homedir"
"github.com/anchore/stereoscope/pkg/image"
)
func TestNewFromLocal(t *testing.T) {
tests := []struct {
name string
cfg Config
setup func(fs afero.Fs)
wantRequest string
wantErr assert.ErrorAssertionFunc
}{
{
name: "local file exists",
cfg: Config{
Request: "/test/local.snap",
DigestAlgorithms: []crypto.Hash{crypto.SHA256},
},
setup: func(fs afero.Fs) {
require.NoError(t, createMockSquashfsFile(fs, "/test/local.snap"))
},
wantRequest: "/test/local.snap",
},
{
name: "resolve home dir exists",
cfg: Config{
Request: "~/test/local.snap",
DigestAlgorithms: []crypto.Hash{crypto.SHA256},
},
wantErr: assert.Error,
wantRequest: func() string {
homeDir, err := homedir.Expand("~/test/local.snap")
require.NoError(t, err, "failed to expand home directory")
require.NotContains(t, homeDir, "~")
return homeDir
}(),
},
{
name: "local file with architecture specified",
cfg: Config{
Request: "/test/local.snap",
Platform: &image.Platform{
Architecture: "arm64",
},
},
setup: func(fs afero.Fs) {
require.NoError(t, createMockSquashfsFile(fs, "/test/local.snap"))
},
wantErr: func(t assert.TestingT, err error, msgAndArgs ...interface{}) bool {
return assert.Error(t, err, msgAndArgs...) && assert.Contains(t, err.Error(), "architecture cannot be specified for local snap files", msgAndArgs...)
},
wantRequest: "/test/local.snap",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = assert.NoError
}
tt.cfg.fs = afero.NewMemMapFs() // Use an in-memory filesystem for testing
if tt.setup != nil {
tt.setup(tt.cfg.fs)
}
got, err := getLocalSnapFile(&tt.cfg)
tt.wantErr(t, err, fmt.Sprintf("NewFromLocal(%v)", tt.cfg))
assert.Equal(t, tt.wantRequest, tt.cfg.Request, "expected request path to match")
if err != nil {
require.Nil(t, got, "expected nil source on error")
return
}
require.NotNil(t, got, "expected non-nil source on success")
})
}
}
+612
View File
@@ -0,0 +1,612 @@
package snapsource
import (
"context"
"crypto"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/wagoodman/go-progress"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/internal/file"
)
func TestSnapIdentity_String(t *testing.T) {
tests := []struct {
name string
identity snapIdentity
expected string
}{
{
name: "name only",
identity: snapIdentity{
Name: "etcd",
},
expected: "etcd",
},
{
name: "name with channel",
identity: snapIdentity{
Name: "etcd",
Channel: "stable",
},
expected: "etcd@stable",
},
{
name: "name with architecture",
identity: snapIdentity{
Name: "etcd",
Architecture: "amd64",
},
expected: "etcd (amd64)",
},
{
name: "name with channel and architecture",
identity: snapIdentity{
Name: "etcd",
Channel: "beta",
Architecture: "arm64",
},
expected: "etcd@beta (arm64)",
},
{
name: "empty channel with architecture",
identity: snapIdentity{
Name: "mysql",
Channel: "",
Architecture: "amd64",
},
expected: "mysql (amd64)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.identity.String()
assert.Equal(t, tt.expected, result)
})
}
}
func TestFileExists(t *testing.T) {
fs := afero.NewMemMapFs()
tests := []struct {
name string
setup func() string
expected bool
}{
{
name: "file exists",
setup: func() string {
path := "/test/file.snap"
require.NoError(t, createMockSquashfsFile(fs, path))
return path
},
expected: true,
},
{
name: "file does not exist",
setup: func() string {
return "/nonexistent/file.snap"
},
expected: false,
},
{
name: "path is directory",
setup: func() string {
path := "/test/dir"
require.NoError(t, fs.MkdirAll(path, 0755))
return path
},
expected: false,
},
{
name: "file exists in subdirectory",
setup: func() string {
path := "/deep/nested/path/file.snap"
require.NoError(t, createMockSquashfsFile(fs, path))
return path
},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path := tt.setup()
result := fileExists(fs, path)
assert.Equal(t, tt.expected, result)
})
}
}
func TestNewSnapFromFile(t *testing.T) {
ctx := context.Background()
fs := afero.NewMemMapFs()
tests := []struct {
name string
cfg Config
setup func() string
expectError bool
errorMsg string
}{
{
name: "valid local snap file",
cfg: Config{
DigestAlgorithms: []crypto.Hash{crypto.SHA256},
},
setup: func() string {
path := "/test/valid.snap"
require.NoError(t, createMockSquashfsFile(fs, path))
return path
},
expectError: false,
},
{
name: "architecture specified for local file",
cfg: Config{
Platform: &image.Platform{
Architecture: "arm64",
},
},
setup: func() string {
path := "/test/valid.snap"
require.NoError(t, createMockSquashfsFile(fs, path))
return path
},
expectError: true,
errorMsg: "architecture cannot be specified for local snap files",
},
{
name: "file does not exist",
cfg: Config{},
setup: func() string {
return "/nonexistent/file.snap"
},
expectError: true,
errorMsg: "unable to stat path",
},
{
name: "path is directory",
cfg: Config{},
setup: func() string {
path := "/test/directory"
require.NoError(t, fs.MkdirAll(path, 0755))
return path
},
expectError: true,
errorMsg: "given path is a directory",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path := tt.setup()
tt.cfg.Request = path
result, err := newSnapFromFile(ctx, fs, tt.cfg)
if tt.expectError {
assert.Error(t, err)
if tt.errorMsg != "" {
assert.Contains(t, err.Error(), tt.errorMsg)
}
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, path, result.Path)
assert.NotEmpty(t, result.MimeType)
assert.NotEmpty(t, result.Digests)
assert.Nil(t, result.Cleanup) // Local files don't have cleanup
}
})
}
}
func TestNewSnapFileFromRemote(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
cfg Config
info *remoteSnap
setupMock func(*mockFileGetter, afero.Fs)
expectError bool
errorMsg string
validate func(t *testing.T, result *snapFile, fs afero.Fs)
}{
{
name: "successful remote snap download",
cfg: Config{
DigestAlgorithms: []crypto.Hash{crypto.SHA256},
},
info: &remoteSnap{
snapIdentity: snapIdentity{
Name: "etcd",
Channel: "stable",
Architecture: "amd64",
},
URL: "https://api.snapcraft.io/download/etcd_123.snap",
},
setupMock: func(mockGetter *mockFileGetter, fs afero.Fs) {
mockGetter.On("GetFile", mock.MatchedBy(func(dst string) bool {
// expect destination to end with etcd_123.snap
return filepath.Base(dst) == "etcd_123.snap"
}), "https://api.snapcraft.io/download/etcd_123.snap", mock.Anything).Run(func(args mock.Arguments) {
// simulate successful download by creating the file
dst := args.String(0)
require.NoError(t, createMockSquashfsFile(fs, dst))
}).Return(nil)
},
expectError: false,
validate: func(t *testing.T, result *snapFile, fs afero.Fs) {
assert.NotNil(t, result)
assert.Contains(t, result.Path, "etcd_123.snap")
assert.NotEmpty(t, result.MimeType)
assert.NotEmpty(t, result.Digests)
assert.NotNil(t, result.Cleanup)
_, err := fs.Stat(result.Path)
assert.NoError(t, err)
err = result.Cleanup()
require.NoError(t, err)
_, err = fs.Stat(result.Path)
assert.True(t, os.IsNotExist(err))
},
},
{
name: "successful download with no digest algorithms",
cfg: Config{
DigestAlgorithms: []crypto.Hash{}, // no digests requested
},
info: &remoteSnap{
snapIdentity: snapIdentity{
Name: "mysql",
Channel: "8.0/stable",
Architecture: "arm64",
},
URL: "https://api.snapcraft.io/download/mysql_456.snap",
},
setupMock: func(mockGetter *mockFileGetter, fs afero.Fs) {
mockGetter.On("GetFile", mock.MatchedBy(func(dst string) bool {
return filepath.Base(dst) == "mysql_456.snap"
}), "https://api.snapcraft.io/download/mysql_456.snap", mock.Anything).Run(func(args mock.Arguments) {
dst := args.String(0)
require.NoError(t, createMockSquashfsFile(fs, dst))
}).Return(nil)
},
expectError: false,
validate: func(t *testing.T, result *snapFile, fs afero.Fs) {
assert.NotNil(t, result)
assert.Contains(t, result.Path, "mysql_456.snap")
assert.NotEmpty(t, result.MimeType)
assert.Empty(t, result.Digests) // no digests requested
assert.NotNil(t, result.Cleanup)
},
},
{
name: "download fails",
cfg: Config{
DigestAlgorithms: []crypto.Hash{crypto.SHA256},
},
info: &remoteSnap{
snapIdentity: snapIdentity{
Name: "failing-snap",
Channel: "stable",
Architecture: "amd64",
},
URL: "https://api.snapcraft.io/download/failing_snap.snap",
},
setupMock: func(mockGetter *mockFileGetter, fs afero.Fs) {
mockGetter.On("GetFile", mock.AnythingOfType("string"), "https://api.snapcraft.io/download/failing_snap.snap", mock.Anything).Return(fmt.Errorf("network timeout"))
},
expectError: true,
errorMsg: "failed to download snap file",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fs := afero.NewOsFs()
mockGetter := &mockFileGetter{}
if tt.setupMock != nil {
tt.setupMock(mockGetter, fs)
}
result, err := newSnapFileFromRemote(ctx, fs, tt.cfg, mockGetter, tt.info)
if tt.expectError {
require.Error(t, err)
if tt.errorMsg != "" {
assert.Contains(t, err.Error(), tt.errorMsg)
}
assert.Nil(t, result)
} else {
require.NoError(t, err)
if tt.validate != nil {
tt.validate(t, result, fs)
}
}
mockGetter.AssertExpectations(t)
})
}
}
func TestGetSnapFileInfo(t *testing.T) {
ctx := context.Background()
fs := afero.NewMemMapFs()
tests := []struct {
name string
setup func() string
hashes []crypto.Hash
expectError bool
errorMsg string
}{
{
name: "valid squashfs file with hashes",
setup: func() string {
path := "/test/valid.snap"
require.NoError(t, createMockSquashfsFile(fs, path))
return path
},
hashes: []crypto.Hash{crypto.SHA256, crypto.MD5},
expectError: false,
},
{
name: "valid squashfs file without hashes",
setup: func() string {
path := "/test/valid.snap"
require.NoError(t, createMockSquashfsFile(fs, path))
return path
},
hashes: []crypto.Hash{},
expectError: false,
},
{
name: "file does not exist",
setup: func() string {
return "/nonexistent/file.snap"
},
expectError: true,
errorMsg: "unable to stat path",
},
{
name: "path is directory",
setup: func() string {
path := "/test/directory"
require.NoError(t, fs.MkdirAll(path, 0755))
return path
},
expectError: true,
errorMsg: "given path is a directory",
},
{
name: "invalid file format",
setup: func() string {
path := "/test/invalid.txt"
require.NoError(t, fs.MkdirAll(filepath.Dir(path), 0755))
file, err := fs.Create(path)
require.NoError(t, err)
defer file.Close()
_, err = file.Write([]byte("not a squashfs file"))
require.NoError(t, err)
return path
},
expectError: true,
errorMsg: "not a valid squashfs/snap file",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path := tt.setup()
mimeType, digests, err := getSnapFileInfo(ctx, fs, path, tt.hashes)
if tt.expectError {
assert.Error(t, err)
if tt.errorMsg != "" {
assert.Contains(t, err.Error(), tt.errorMsg)
}
} else {
assert.NoError(t, err)
assert.NotEmpty(t, mimeType)
if len(tt.hashes) > 0 {
assert.Len(t, digests, len(tt.hashes))
} else {
assert.Empty(t, digests)
}
}
})
}
}
func TestDownloadSnap(t *testing.T) {
mockGetter := &mockFileGetter{}
tests := []struct {
name string
info *remoteSnap
dest string
setupMock func()
expectError bool
errorMsg string
}{
{
name: "successful download",
info: &remoteSnap{
snapIdentity: snapIdentity{
Name: "etcd",
Channel: "stable",
Architecture: "amd64",
},
URL: "https://example.com/etcd.snap",
},
dest: "/tmp/etcd.snap",
setupMock: func() {
mockGetter.On("GetFile", "/tmp/etcd.snap", "https://example.com/etcd.snap", mock.AnythingOfType("[]*progress.Manual")).Return(nil)
},
expectError: false,
},
{
name: "download fails",
info: &remoteSnap{
snapIdentity: snapIdentity{
Name: "etcd",
Channel: "stable",
Architecture: "amd64",
},
URL: "https://example.com/etcd.snap",
},
dest: "/tmp/etcd.snap",
setupMock: func() {
mockGetter.On("GetFile", "/tmp/etcd.snap", "https://example.com/etcd.snap", mock.AnythingOfType("[]*progress.Manual")).Return(fmt.Errorf("network error"))
},
expectError: true,
errorMsg: "failed to download snap file",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// reset mock for each test
mockGetter.ExpectedCalls = nil
if tt.setupMock != nil {
tt.setupMock()
}
err := downloadSnap(mockGetter, tt.info, tt.dest)
if tt.expectError {
assert.Error(t, err)
if tt.errorMsg != "" {
assert.Contains(t, err.Error(), tt.errorMsg)
}
} else {
assert.NoError(t, err)
}
mockGetter.AssertExpectations(t)
})
}
}
func TestParseSnapRequest(t *testing.T) {
tests := []struct {
name string
request string
expectedName string
expectedChannel string
}{
{
name: "snap name only - uses default channel",
request: "etcd",
expectedName: "etcd",
expectedChannel: "stable",
},
{
name: "snap with beta channel",
request: "etcd@beta",
expectedName: "etcd",
expectedChannel: "beta",
},
{
name: "snap with edge channel",
request: "etcd@edge",
expectedName: "etcd",
expectedChannel: "edge",
},
{
name: "snap with version track",
request: "etcd@2.3/stable",
expectedName: "etcd",
expectedChannel: "2.3/stable",
},
{
name: "snap with complex channel path",
request: "mysql@8.0/candidate",
expectedName: "mysql",
expectedChannel: "8.0/candidate",
},
{
name: "snap with multiple @ symbols - only first is delimiter",
request: "app@beta@test",
expectedName: "app",
expectedChannel: "beta@test",
},
{
name: "empty snap name with channel",
request: "@stable",
expectedName: "",
expectedChannel: "stable",
},
{
name: "snap name with empty channel - uses default",
request: "etcd@",
expectedName: "etcd",
expectedChannel: "stable",
},
{
name: "hyphenated snap name",
request: "hello-world@stable",
expectedName: "hello-world",
expectedChannel: "stable",
},
{
name: "snap name with numbers",
request: "app123",
expectedName: "app123",
expectedChannel: "stable",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
name, channel := parseSnapRequest(tt.request)
assert.Equal(t, tt.expectedName, name)
assert.Equal(t, tt.expectedChannel, channel)
})
}
}
type mockFileGetter struct {
mock.Mock
file.Getter
}
func (m *mockFileGetter) GetFile(dst, src string, monitor ...*progress.Manual) error {
args := m.Called(dst, src, monitor)
return args.Error(0)
}
func createMockSquashfsFile(fs afero.Fs, path string) error {
dir := filepath.Dir(path)
if err := fs.MkdirAll(dir, 0755); err != nil {
return err
}
file, err := fs.Create(path)
if err != nil {
return err
}
defer file.Close()
// write squashfs magic header
_, err = file.Write([]byte("hsqs"))
return err
}
+159
View File
@@ -0,0 +1,159 @@
package snapsource
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/anchore/syft/internal/log"
)
const (
defaultChannel = "stable"
defaultArchitecture = "amd64"
defaultSeries = "16"
)
// snapcraftClient handles interactions with the Snapcraft API
type snapcraftClient struct {
InfoAPIURL string
FindAPIURL string
HTTPClient *http.Client
}
// newSnapcraftClient creates a new Snapcraft API client with default settings
func newSnapcraftClient() *snapcraftClient {
return &snapcraftClient{
InfoAPIURL: "https://api.snapcraft.io/v2/snaps/info/",
FindAPIURL: "https://api.snapcraft.io/v2/snaps/find",
HTTPClient: &http.Client{},
}
}
// snapcraftInfo represents the response from the snapcraft info API
type snapcraftInfo struct {
ChannelMap []snapChannelMapEntry `json:"channel-map"`
}
type snapChannelMapEntry struct {
Channel snapChannel `json:"channel"`
Download snapDownload `json:"download"`
}
type snapChannel struct {
Architecture string `json:"architecture"`
Name string `json:"name"`
}
type snapDownload struct {
URL string `json:"url"`
}
// snapFindResponse represents the response from the snapcraft find API (search v2)
type snapFindResponse struct {
Results []struct {
Name string `json:"name"`
SnapID string `json:"snap-id"`
Snap struct{} `json:"snap"`
} `json:"results"`
}
// GetSnapDownloadURL retrieves the download URL for a snap package
func (c *snapcraftClient) GetSnapDownloadURL(id snapIdentity) (string, error) {
apiURL := c.InfoAPIURL + id.Name
log.WithFields("name", id.Name, "channel", id.Channel, "architecture", id.Architecture).Trace("requesting snap info")
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
if err != nil {
return "", fmt.Errorf("failed to create HTTP request: %w", err)
}
req.Header.Set("Snap-Device-Series", defaultSeries)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return "", fmt.Errorf("failed to send HTTP request: %w", err)
}
defer resp.Body.Close()
// handle 404 case - check if snap exists via find API
if resp.StatusCode == http.StatusNotFound {
log.WithFields("name", id.Name).Debug("snap info not found, checking if snap exists via find API")
exists, snapID, findErr := c.CheckSnapExists(id.Name)
if findErr != nil {
return "", fmt.Errorf("failed to check if snap exists: %w", findErr)
}
if exists {
return "", fmt.Errorf("found snap '%s' (id=%s) but it is unavailable for download", id.Name, snapID)
}
return "", fmt.Errorf("no snap found with name '%s'", id.Name)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("API request failed with status code %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
}
var info snapcraftInfo
if err := json.Unmarshal(body, &info); err != nil {
return "", fmt.Errorf("failed to parse JSON response: %w", err)
}
for _, cm := range info.ChannelMap {
if cm.Channel.Architecture == id.Architecture && cm.Channel.Name == id.Channel {
return cm.Download.URL, nil
}
}
return "", fmt.Errorf("no matching snap found for %s", id.String())
}
// CheckSnapExists uses the find API (search v2) to check if a snap exists
func (c *snapcraftClient) CheckSnapExists(snapName string) (bool, string, error) {
req, err := http.NewRequest(http.MethodGet, c.FindAPIURL, nil)
if err != nil {
return false, "", fmt.Errorf("failed to create find request: %w", err)
}
q := req.URL.Query()
q.Add("name-startswith", snapName)
req.URL.RawQuery = q.Encode()
req.Header.Set("Snap-Device-Series", defaultSeries)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return false, "", fmt.Errorf("failed to send find request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return false, "", fmt.Errorf("find API request failed with status code %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, "", fmt.Errorf("failed to read find response body: %w", err)
}
var findResp snapFindResponse
if err := json.Unmarshal(body, &findResp); err != nil {
return false, "", fmt.Errorf("failed to parse find JSON response: %w", err)
}
// Look for exact name match
for _, result := range findResp.Results {
if result.Name == snapName {
return true, result.SnapID, nil
}
}
return false, "", nil
}
@@ -0,0 +1,383 @@
package snapsource
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSnapcraftClient_CheckSnapExists(t *testing.T) {
tests := []struct {
name string
snapName string
mockResponse snapFindResponse
statusCode int
expectedExists bool
expectedSnapID string
expectError require.ErrorAssertionFunc
errorContains string
}{
{
name: "snap exists",
snapName: "jp-ledger",
statusCode: http.StatusOK,
mockResponse: snapFindResponse{
Results: []struct {
Name string `json:"name"`
SnapID string `json:"snap-id"`
Snap struct{} `json:"snap"`
}{
{
Name: "jp-ledger",
SnapID: "jyDlMmifyQhSWGPM9fnKc1HSD7E6c47e",
Snap: struct{}{},
},
},
},
expectedExists: true,
expectedSnapID: "jyDlMmifyQhSWGPM9fnKc1HSD7E6c47e",
expectError: require.NoError,
},
{
name: "snap does not exist",
snapName: "nonexistent-snap",
statusCode: http.StatusOK,
mockResponse: snapFindResponse{
Results: []struct {
Name string `json:"name"`
SnapID string `json:"snap-id"`
Snap struct{} `json:"snap"`
}{},
},
expectedExists: false,
expectedSnapID: "",
expectError: require.NoError,
},
{
name: "multiple results - exact match found",
snapName: "test-snap",
statusCode: http.StatusOK,
mockResponse: snapFindResponse{
Results: []struct {
Name string `json:"name"`
SnapID string `json:"snap-id"`
Snap struct{} `json:"snap"`
}{
{
Name: "test-snap-extra",
SnapID: "wrong-id",
Snap: struct{}{},
},
{
Name: "test-snap",
SnapID: "correct-id",
Snap: struct{}{},
},
},
},
expectedExists: true,
expectedSnapID: "correct-id",
expectError: require.NoError,
},
{
name: "find API returns 404",
snapName: "test",
statusCode: http.StatusNotFound,
expectError: require.Error,
errorContains: "find API request failed with status code 404",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
findServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, defaultSeries, r.Header.Get("Snap-Device-Series"))
assert.Equal(t, tt.snapName, r.URL.Query().Get("name-startswith"))
w.WriteHeader(tt.statusCode)
if tt.statusCode == http.StatusOK {
responseBytes, err := json.Marshal(tt.mockResponse)
require.NoError(t, err)
w.Write(responseBytes)
}
}))
defer findServer.Close()
client := &snapcraftClient{
FindAPIURL: findServer.URL,
HTTPClient: &http.Client{},
}
exists, snapID, err := client.CheckSnapExists(tt.snapName)
tt.expectError(t, err)
if err != nil && tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
return
}
assert.Equal(t, tt.expectedExists, exists)
assert.Equal(t, tt.expectedSnapID, snapID)
})
}
}
func TestSnapcraftClient_GetSnapDownloadURL(t *testing.T) {
tests := []struct {
name string
snapID snapIdentity
infoResponse snapcraftInfo
infoStatusCode int
findResponse *snapFindResponse
findStatusCode int
expectedURL string
expectError require.ErrorAssertionFunc
errorContains string
}{
{
name: "successful download URL retrieval",
snapID: snapIdentity{
Name: "etcd",
Channel: "stable",
Architecture: "amd64",
},
infoStatusCode: http.StatusOK,
infoResponse: snapcraftInfo{
ChannelMap: []snapChannelMapEntry{
{
Channel: snapChannel{
Architecture: "amd64",
Name: "stable",
},
Download: snapDownload{
URL: "https://api.snapcraft.io/api/v1/snaps/download/etcd_123.snap",
},
},
},
},
expectedURL: "https://api.snapcraft.io/api/v1/snaps/download/etcd_123.snap",
expectError: require.NoError,
},
{
name: "region-locked snap - exists but unavailable",
snapID: snapIdentity{
Name: "jp-ledger",
Channel: "stable",
Architecture: "amd64",
},
infoStatusCode: http.StatusNotFound,
findStatusCode: http.StatusOK,
findResponse: &snapFindResponse{
Results: []struct {
Name string `json:"name"`
SnapID string `json:"snap-id"`
Snap struct{} `json:"snap"`
}{
{
Name: "jp-ledger",
SnapID: "jyDlMmifyQhSWGPM9fnKc1HSD7E6c47e",
Snap: struct{}{},
},
},
},
expectError: require.Error,
errorContains: "found snap 'jp-ledger' (id=jyDlMmifyQhSWGPM9fnKc1HSD7E6c47e) but it is unavailable for download",
},
{
name: "snap truly does not exist",
snapID: snapIdentity{
Name: "nonexistent",
Channel: "stable",
Architecture: "amd64",
},
infoStatusCode: http.StatusNotFound,
findStatusCode: http.StatusOK,
findResponse: &snapFindResponse{
Results: []struct {
Name string `json:"name"`
SnapID string `json:"snap-id"`
Snap struct{} `json:"snap"`
}{},
},
expectError: require.Error,
errorContains: "no snap found with name 'nonexistent'",
},
{
name: "multiple architectures - find correct one",
snapID: snapIdentity{
Name: "mysql",
Channel: "stable",
Architecture: "arm64",
},
infoStatusCode: http.StatusOK,
infoResponse: snapcraftInfo{
ChannelMap: []snapChannelMapEntry{
{
Channel: snapChannel{
Architecture: "amd64",
Name: "stable",
},
Download: snapDownload{
URL: "https://api.snapcraft.io/api/v1/snaps/download/mysql_amd64.snap",
},
},
{
Channel: snapChannel{
Architecture: "arm64",
Name: "stable",
},
Download: snapDownload{
URL: "https://api.snapcraft.io/api/v1/snaps/download/mysql_arm64.snap",
},
},
},
},
expectedURL: "https://api.snapcraft.io/api/v1/snaps/download/mysql_arm64.snap",
expectError: require.NoError,
},
{
name: "snap not found - no matching architecture",
snapID: snapIdentity{
Name: "etcd",
Channel: "stable",
Architecture: "s390x",
},
infoStatusCode: http.StatusOK,
infoResponse: snapcraftInfo{
ChannelMap: []snapChannelMapEntry{
{
Channel: snapChannel{
Architecture: "amd64",
Name: "stable",
},
Download: snapDownload{
URL: "https://api.snapcraft.io/api/v1/snaps/download/etcd_123.snap",
},
},
},
},
expectError: require.Error,
errorContains: "no matching snap found",
},
{
name: "API returns 500",
snapID: snapIdentity{
Name: "etcd",
Channel: "stable",
Architecture: "amd64",
},
infoStatusCode: http.StatusInternalServerError,
expectError: require.Error,
errorContains: "API request failed with status code 500",
},
{
name: "find API fails when checking 404",
snapID: snapIdentity{
Name: "test-snap",
Channel: "stable",
Architecture: "amd64",
},
infoStatusCode: http.StatusNotFound,
findStatusCode: http.StatusInternalServerError,
expectError: require.Error,
errorContains: "failed to check if snap exists",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.expectError == nil {
tt.expectError = require.NoError
}
infoServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, defaultSeries, r.Header.Get("Snap-Device-Series"))
expectedPath := "/" + tt.snapID.Name
assert.Equal(t, expectedPath, r.URL.Path)
w.WriteHeader(tt.infoStatusCode)
if tt.infoStatusCode == http.StatusOK {
responseBytes, err := json.Marshal(tt.infoResponse)
require.NoError(t, err)
w.Write(responseBytes)
}
}))
defer infoServer.Close()
var findServer *httptest.Server
if tt.findResponse != nil || tt.findStatusCode != 0 {
findServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, defaultSeries, r.Header.Get("Snap-Device-Series"))
assert.Equal(t, tt.snapID.Name, r.URL.Query().Get("name-startswith"))
statusCode := tt.findStatusCode
if statusCode == 0 {
statusCode = http.StatusOK
}
w.WriteHeader(statusCode)
if tt.findResponse != nil && statusCode == http.StatusOK {
responseBytes, err := json.Marshal(tt.findResponse)
require.NoError(t, err)
w.Write(responseBytes)
}
}))
defer findServer.Close()
}
client := &snapcraftClient{
InfoAPIURL: infoServer.URL + "/",
HTTPClient: &http.Client{},
}
if findServer != nil {
client.FindAPIURL = findServer.URL
}
url, err := client.GetSnapDownloadURL(tt.snapID)
tt.expectError(t, err)
if err != nil {
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
return
}
assert.Equal(t, tt.expectedURL, url)
})
}
}
func TestSnapcraftClient_GetSnapDownloadURL_InvalidJSON(t *testing.T) {
infoServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("invalid json"))
}))
defer infoServer.Close()
client := &snapcraftClient{
InfoAPIURL: infoServer.URL + "/",
HTTPClient: &http.Client{},
}
snapID := snapIdentity{
Name: "etcd",
Channel: "stable",
Architecture: "amd64",
}
_, err := client.GetSnapDownloadURL(snapID)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse JSON response")
}
func TestNewSnapcraftClient(t *testing.T) {
client := newSnapcraftClient()
assert.Equal(t, "https://api.snapcraft.io/v2/snaps/info/", client.InfoAPIURL)
assert.Equal(t, "https://api.snapcraft.io/v2/snaps/find", client.FindAPIURL)
assert.NotNil(t, client.HTTPClient)
}
@@ -7,6 +7,7 @@ import (
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/directorysource"
"github.com/anchore/syft/syft/source/filesource"
"github.com/anchore/syft/syft/source/snapsource"
"github.com/anchore/syft/syft/source/stereoscopesource"
)
@@ -14,6 +15,7 @@ const (
FileTag = stereoscope.FileTag
DirTag = stereoscope.DirTag
PullTag = stereoscope.PullTag
SnapTag = "snap"
)
// All returns all the configured source providers known to syft
@@ -24,13 +26,25 @@ func All(userInput string, cfg *Config) []collections.TaggedValue[source.Provide
stereoscopeProviders := stereoscopeSourceProviders(userInput, cfg)
return collections.TaggedValueSet[source.Provider]{}.
// 1. try all specific, local sources first...
// --from file, dir, oci-archive, etc.
Join(stereoscopeProviders.Select(FileTag, DirTag)...).
// --from snap (local only)
Join(tagProvider(snapsource.NewLocalSourceProvider(userInput, cfg.Exclude, cfg.DigestAlgorithms, cfg.Alias), SnapTag)).
// 2. try unspecific, local sources after other local sources last...
Join(tagProvider(filesource.NewSourceProvider(userInput, cfg.Exclude, cfg.DigestAlgorithms, cfg.Alias), FileTag)).
Join(tagProvider(directorysource.NewSourceProvider(userInput, cfg.Exclude, cfg.Alias, cfg.BasePath), DirTag)).
// 3. try remote sources after everything else...
// --from docker, registry, etc.
Join(stereoscopeProviders.Select(PullTag)...)
Join(stereoscopeProviders.Select(PullTag)...).
// --from snap (remote only)
Join(tagProvider(snapsource.NewRemoteSourceProvider(userInput, cfg.Exclude, cfg.DigestAlgorithms, cfg.Alias), SnapTag))
}
func stereoscopeSourceProviders(userInput string, cfg *Config) collections.TaggedValueSet[source.Provider] {
@@ -35,7 +35,5 @@ func TestDirectoryScanCompletesWithinTimeout(t *testing.T) {
for _, traitFn := range assertions {
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
logOutputOnFailure(t, cmd, stdout, stderr)
}

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