Compare commits

...
32 Commits
Author SHA1 Message Date
anchore-actions-token-generator[bot]andspiffcs 4c93394bc2 chore(deps): update anchore dependencies (#4330)
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-11-03 12:44:07 -05:00
kdt523 3e4e82f03e Canonicalize Ghostscript CPE/PURL for ghostscript packages from PE Binaries (#4308)
* binary(pe): canonicalize Ghostscript CPE to artifex:ghostscript and add generic purl for PE (#4275)\n\n- Detect Ghostscript via PE version resources and set purl pkg:generic/ghostscript@<version>\n- Add PE-specific CPE candidates: vendor 'artifex', product 'ghostscript'\n- Add focused unit tests for purl and CPE generation

Signed-off-by: kdt523 <krushna.datir231@vit.edu>

* fix: gofmt formatting for static analysis pass (pe-ghostscript-cpe-purl-4275)

Signed-off-by: kdt523 <krushna.datir231@vit.edu>

---------

Signed-off-by: kdt523 <krushna.datir231@vit.edu>
2025-11-03 14:54:48 +00:00
dependabot[bot] 793b0a346f chore(deps): bump github/codeql-action from 4.31.1 to 4.31.2 (#4325)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.1 to 4.31.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/5fe9434cd24fe243e33e7f3305f8a5b519b70280...0499de31b99561a6d14a36a5f662c2a54f91beee)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.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-11-03 09:11:20 -05:00
dependabot[bot] a0dac519db chore(deps): bump github.com/hashicorp/go-getter from 1.8.2 to 1.8.3 (#4326)
Bumps [github.com/hashicorp/go-getter](https://github.com/hashicorp/go-getter) from 1.8.2 to 1.8.3.
- [Release notes](https://github.com/hashicorp/go-getter/releases)
- [Changelog](https://github.com/hashicorp/go-getter/blob/main/.goreleaser.yml)
- [Commits](https://github.com/hashicorp/go-getter/compare/v1.8.2...v1.8.3)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/go-getter
  dependency-version: 1.8.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-11-03 09:11:12 -05:00
dependabot[bot] 34f5e521c1 chore(deps): bump modernc.org/sqlite from 1.39.1 to 1.40.0 (#4329)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.39.1 to 1.40.0.
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.39.1...v1.40.0)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.40.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-11-03 09:11:05 -05:00
dependabot[bot] 774b1e97b9 chore(deps): bump github/codeql-action from 4.31.0 to 4.31.1 (#4321)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.0 to 4.31.1.
- [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/4e94bd11f71e507f7f87df81788dff88d1dacbfb...5fe9434cd24fe243e33e7f3305f8a5b519b70280)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.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-10-30 13:19:57 -04:00
Alex Goodman 538430d65d describe cataloger capabilities via test observations (#4318)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-10-30 13:19:42 -04:00
Alex Goodman 5db3a9bf55 add workflow to create PR for spdx license list updates (#4319)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-10-30 12:14:13 -04:00
StepanandAlex Goodman efc2f0012c fix: go binary replace handling in path (#4156)
* Fix issue with relative paths on go binary

Signed-off-by: Stepan <stepworm@yandex.ru>

* Linting

Signed-off-by: Stepan <stepworm@yandex.ru>

---------

Signed-off-by: Stepan <stepworm@yandex.ru>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-10-29 15:59:47 +00:00
kyounghoonJangandAlex Goodman c5c1454848 feat(java): Add support for .far (Feature Archive) files (#4193)
* feat(java): add support for .far archivesEnables the Java cataloger to recognize and catalog dependencies within .far files, which are used in Apache Sling applications.

Signed-off-by: Kyounghoon Jang <matkimchi_@naver.com>

* feat(java): Add tests for .far (Feature Archive) file support

Signed-off-by: Kyounghoon Jang <matkimchi_@naver.com>

---------

Signed-off-by: Kyounghoon Jang <matkimchi_@naver.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-10-29 15:41:27 +00:00
Kudryavcev Nikolay f5c765192c Refactor fileresolver to not require base path (#4298)
* ref: close source in test and examples

Signed-off-by: Kudryavcev Nikolay <kydry.nikolau@gmail.com>

* ref: pretty file/directory source resolver (make them more similar)

Signed-off-by: Kudryavcev Nikolay <kydry.nikolau@gmail.com>

* ref: move absoluteSymlinkFreePathToParent to file resolver

Signed-off-by: Kudryavcev Nikolay <kydry.nikolau@gmail.com>

* revert breaking change

Signed-off-by: Kudryavcev Nikolay <kydry.nikolau@gmail.com>

---------

Signed-off-by: Kudryavcev Nikolay <kydry.nikolau@gmail.com>
2025-10-29 10:41:18 -04:00
Will Murphy 728feea620 ci: use apple creds before pushing tags (#4313)
We have had a few releases fail because the Apple credentials needed
some sort of fix. These release were operationally more interesting
because they failed after pushing a git tag (which effectively releases
the golagn package). Therefore, try to use these creds early, before
there's a tag pushed.

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
2025-10-29 10:07:47 -04:00
dependabot[bot] 45fb52dca1 chore(deps): bump github.com/jedib0t/go-pretty/v6 from 6.6.8 to 6.6.9 (#4315)
Bumps [github.com/jedib0t/go-pretty/v6](https://github.com/jedib0t/go-pretty) from 6.6.8 to 6.6.9.
- [Release notes](https://github.com/jedib0t/go-pretty/releases)
- [Commits](https://github.com/jedib0t/go-pretty/compare/v6.6.8...v6.6.9)

---
updated-dependencies:
- dependency-name: github.com/jedib0t/go-pretty/v6
  dependency-version: 6.6.9
  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-10-29 10:06:37 -04:00
Rez Moss 45bf8b14ab fix: omit records with empty PURL in GitHub format (#4312)
Signed-off-by: Rez Moss <hi@rezmoss.com>
2025-10-28 18:34:10 -04:00
Brian Muenzenmeyer 9478cd974b docs: update template link in README.md (#4306)
Signed-off-by: Brian Muenzenmeyer <brian.muenzenmeyer@gmail.com>
2025-10-28 11:29:07 -04:00
Will Murphy 0d9ea69a66 Respect "rpmmod" PURL qualifier (#4314)
Red Hat purls the RPM modularity info in a query param in the PURLs in
their vulnerability data. It would be nice if Syft respected this
qualifier so that Grype can use it when a Red Hat purl is passed.

Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
2025-10-28 09:35:11 -04:00
dependabot[bot] bee78c0b16 chore(deps): bump github/codeql-action from 4.30.9 to 4.31.0 (#4310)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.30.9 to 4.31.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/16140ae1a102900babc80a33c44059580f687047...4e94bd11f71e507f7f87df81788dff88d1dacbfb)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.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-10-27 10:43:04 -04:00
dependabot[bot] 88bbcbe9c6 chore(deps): bump anchore/sbom-action from 0.20.8 to 0.20.9 (#4305) 2025-10-27 02:03:09 -04:00
anchore-actions-token-generator[bot] e0680eb704 chore(deps): update tools to latest versions (#4307) 2025-10-27 02:02:47 -04:00
Marc 16f851c5d9 feat: include .rar files as Java archives for Java resource adapters (#4137)
Signed-off-by: Marc Thomas <marc.thomas@t-systems.com>
2025-10-24 11:55:02 -04:00
Ross Kirk d5ca1ad543 fix: ignore dpkg entries with "deinstall" status (#4231)
Signed-off-by: Ross Kirk <ross.kirk@upwind.io>
2025-10-23 16:23:58 -04:00
anchore-actions-token-generator[bot]andspiffcs 8be463911c chore(deps): update tools to latest versions (#4302)
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-10-22 09:38:18 -04:00
dependabot[bot] 44b7b0947c chore(deps): bump github.com/github/go-spdx/v2 from 2.3.3 to 2.3.4 (#4301) 2025-10-21 09:34:26 -04:00
dependabot[bot] 675075e882 chore(deps): bump github/codeql-action from 4.30.8 to 4.30.9 (#4299)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.30.8 to 4.30.9.
- [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/f443b600d91635bebf5b0d9ebc620189c0d6fba5...16140ae1a102900babc80a33c44059580f687047)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.30.9
  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-10-20 10:08:39 -04:00
JoeyShapiro 31b2c4c090 support universal (fat) mach-o binary files (#4278)
Signed-off-by: Joseph Shapiro <joeyashapiro@gmail.com>
2025-10-17 13:41:59 -04:00
dependabot[bot] 07029ead8a chore(deps): bump sigstore/cosign-installer from 3.10.0 to 4.0.0 (#4296)
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.10.0 to 4.0.0.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/d7543c93d881b35a8faa02e8e3605f69b7a1ce62...faadad0cce49287aee09b3a48701e75088a2c6ad)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-17 10:22:20 -04:00
dependabot[bot] f4de1e863c chore(deps): bump anchore/sbom-action from 0.20.7 to 0.20.8 (#4297)
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.20.7 to 0.20.8.
- [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/d8a2c0130026bf585de5c176ab8f7ce62d75bf04...aa0e114b2e19480f157109b9922bda359bd98b90)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-version: 0.20.8
  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-10-17 10:22:10 -04:00
JoeyShapiro 538b4a2194 convert posix path back to windows (#4285)
Signed-off-by: Joseph Shapiro <joeyashapiro@gmail.com>
2025-10-17 09:29:06 -04:00
Kudryavcev Nikolay fc74b07369 Remove duplicate image source providers (#4289)
Signed-off-by: Kudryavcev Nikolay <kydry.nikolau@gmail.com>
2025-10-16 16:19:11 -04:00
dependabot[bot] 6627c5214c chore(deps): bump anchore/sbom-action from 0.20.6 to 0.20.7 (#4293)
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.20.6 to 0.20.7.
- [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/f8bdd1d8ac5e901a77a92f111440fdb1b593736b...d8a2c0130026bf585de5c176ab8f7ce62d75bf04)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-version: 0.20.7
  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-10-16 13:57:17 -04:00
Tim Olshansky c0f32e1dba feat: add option to fetch remote licenses for pnpm-lock.yaml files (#4286)
Signed-off-by: Tim Olshansky <456103+timols@users.noreply.github.com>
2025-10-16 12:23:06 -04:00
Pavel BuchartandKeith Zantow e923db2a94 Add PDM parser (#4234)
Signed-off-by: Pavel Buchart <pavel@buchart.cz>
Signed-off-by: Keith Zantow <kzantow@gmail.com>
Co-authored-by: Keith Zantow <kzantow@gmail.com>
2025-10-16 08:50:44 -04:00
84 changed files with 6715 additions and 333 deletions
+2 -2
View File
@@ -58,7 +58,7 @@ tools:
# used to release all artifacts
- name: goreleaser
version:
want: v2.12.5
want: v2.12.7
method: github-release
with:
repo: goreleaser/goreleaser
@@ -98,7 +98,7 @@ tools:
# used for triggering a release
- name: gh
version:
want: v2.82.0
want: v2.82.1
method: github-release
with:
repo: cli/cli
+3 -3
View File
@@ -47,7 +47,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 #v3.29.5
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee #v3.29.5
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -58,7 +58,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@f443b600d91635bebf5b0d9ebc620189c0d6fba5 #v3.29.5
uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee #v3.29.5
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -72,4 +72,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 #v3.29.5
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee #v3.29.5
+11 -1
View File
@@ -19,6 +19,16 @@ jobs:
with:
persist-credentials: false
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- name: Validate Apple notarization credentials
run: .tool/quill submission list
env:
QUILL_NOTARY_ISSUER: ${{ secrets.APPLE_NOTARY_ISSUER }}
QUILL_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
QUILL_NOTARY_KEY: ${{ secrets.APPLE_NOTARY_KEY }}
- name: Check if running on main
if: github.ref != 'refs/heads/main'
# we are using the following flag when running `cosign blob-verify` for checksum signature verification:
@@ -161,7 +171,7 @@ jobs:
# for updating brew formula in anchore/homebrew-syft
GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}
- uses: anchore/sbom-action@f8bdd1d8ac5e901a77a92f111440fdb1b593736b #v0.20.6
- uses: anchore/sbom-action@8e94d75ddd33f69f691467e42275782e4bfefe84 #v0.20.9
continue-on-error: true
with:
file: go.mod
@@ -0,0 +1,54 @@
name: PR to update SPDX license list
on:
schedule:
- cron: "0 6 * * 1" # every monday at 6 AM UTC
workflow_dispatch:
permissions:
contents: read
env:
SLACK_NOTIFICATIONS: true
jobs:
upgrade-spdx-license-list:
runs-on: ubuntu-latest
if: github.repository == 'anchore/syft' # only run for main repo
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
with:
persist-credentials: false
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- run: |
make generate-license-list
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0
id: generate-token
with:
app_id: ${{ secrets.TOKEN_APP_ID }}
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e #v7.0.8
with:
signoff: true
delete-branch: true
branch: auto/latest-spdx-license-list
labels: dependencies
commit-message: "chore(deps): update SPDX license list"
title: "chore(deps): update SPDX license list"
body: |
Update SPDX license list based on the latest available list from spdx.org
token: ${{ steps.generate-token.outputs.token }}
- uses: 8398a7/action-slack@77eaa4f1c608a7d68b38af4e3f739dcd8cba273e #v3.19.0
with:
status: ${{ job.status }}
fields: workflow,eventName,job
text: Syft SPDX license list update failed
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
if: ${{ failure() && env.SLACK_NOTIFICATIONS == 'true' }}
+1 -1
View File
@@ -210,7 +210,7 @@ jobs:
runs-on: macos-latest
steps:
- name: Install Cosign
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
with:
+1
View File
@@ -2,6 +2,7 @@
go.work
go.work.sum
.tool-versions
.python-version
# app configuration
/.syft.yaml
+2 -2
View File
@@ -116,7 +116,7 @@ Where the `formats` available are:
- `spdx-json@2.2`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
- `github-json`: A JSON report conforming to GitHub's dependency snapshot format.
- `syft-table`: A columnar summary (default).
- `template`: Lets the user specify the output format. See ["Using templates"](#using-templates) below.
- `template`: Lets the user specify the output format. See ["Using templates"](https://github.com/anchore/syft/wiki/using-templates) below.
Note that flags using the @<version> can be used for earlier versions of each specification as well.
@@ -135,7 +135,7 @@ Note that flags using the @<version> can be used for earlier versions of each sp
- Go (go.mod, Go binaries)
- GitHub (workflows, actions)
- Haskell (cabal, stack)
- Java (jar, ear, war, par, sar, nar, native-image)
- Java (jar, ear, war, par, sar, nar, rar, native-image)
- JavaScript (npm, yarn)
- Jenkins Plugins (jpi, hpi)
- Linux kernel archives (vmlinz)
-1
View File
@@ -253,7 +253,6 @@ func generateSBOMForAttestation(ctx context.Context, id clio.Identification, opt
}
src, err := getSource(ctx, opts, userInput, stereoscope.RegistryTag)
if err != nil {
return nil, err
}
-1
View File
@@ -185,7 +185,6 @@ func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, use
}
src, err := getSource(ctx, &opts.Catalog, userInput, sources...)
if err != nil {
return err
}
@@ -25,7 +25,6 @@ func BenchmarkImagePackageCatalogers(b *testing.B) {
// get the source object for the image
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
require.NoError(b, err)
b.Cleanup(func() {
require.NoError(b, theSource.Close())
})
@@ -38,11 +38,11 @@ func catalogFixtureImageWithConfig(t *testing.T, fixtureImageName string, cfg *s
// get the source to build an SBOM against
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, theSource.Close())
})
// build the SBOM
s, err := syft.CreateSBOM(context.Background(), theSource, cfg)
require.NoError(t, err)
@@ -66,7 +66,7 @@ func catalogDirectory(t *testing.T, dir string, catalogerSelection ...string) (s
func catalogDirectoryWithConfig(t *testing.T, dir string, cfg *syft.CreateSBOMConfig) (sbom.SBOM, source.Source) {
cfg.CatalogerSelection = cfg.CatalogerSelection.WithDefaults(pkgcataloging.DirectoryTag)
// get the source to build an sbom against
// get the source to build an SBOM against
theSource, err := syft.GetSource(context.Background(), dir, syft.DefaultGetSourceConfig().WithSources("dir"))
require.NoError(t, err)
t.Cleanup(func() {
+1 -1
View File
@@ -23,6 +23,7 @@ const defaultImage = "alpine:3.19"
func main() {
// automagically get a source.Source for arbitrary string input
src := getSource(imageReference())
defer src.Close()
// will catalog the given source and return a SBOM keeping in mind several configurable options
sbom := getSBOM(src)
@@ -46,7 +47,6 @@ func getSource(input string) source.Source {
fmt.Println("detecting source type for input:", input, "...")
src, err := syft.GetSource(context.Background(), input, nil)
if err != nil {
panic(err)
}
+1 -1
View File
@@ -19,6 +19,7 @@ const defaultImage = "alpine:3.19"
func main() {
// automagically get a source.Source for arbitrary string input
src := getSource(imageReference())
defer src.Close()
// catalog the given source and return a SBOM
sbom := getSBOM(src)
@@ -40,7 +41,6 @@ func imageReference() string {
func getSource(input string) source.Source {
src, err := syft.GetSource(context.Background(), input, nil)
if err != nil {
panic(err)
}
+1 -1
View File
@@ -19,6 +19,7 @@ const defaultImage = "alpine:3.19"
func main() {
// automagically get a source.Source for arbitrary string input
src := getSource(imageReference())
defer src.Close()
// catalog the given source and return a SBOM
// let's explicitly use catalogers that are:
@@ -44,7 +45,6 @@ func imageReference() string {
func getSource(input string) source.Source {
src, err := syft.GetSource(context.Background(), input, nil)
if err != nil {
panic(err)
}
+1
View File
@@ -15,6 +15,7 @@ func main() {
image := "alpine:3.19"
src, _ := syft.GetSource(context.Background(), image, syft.DefaultGetSourceConfig().WithSources("registry"))
defer src.Close()
sbom, _ := syft.CreateSBOM(context.Background(), src, syft.DefaultCreateSBOMConfig())
+6 -6
View File
@@ -15,7 +15,7 @@ require (
github.com/anchore/bubbly v0.0.0-20231115134915-def0aba654a9
github.com/anchore/clio v0.0.0-20250319180342-2cfe4b0cb716
github.com/anchore/fangs v0.0.0-20250319222917-446a1e748ec2
github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537
github.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c
github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d
github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb
@@ -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.11
github.com/anchore/stereoscope v0.1.12
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
@@ -40,7 +40,7 @@ require (
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/github/go-spdx/v2 v2.3.4
github.com/gkampitakis/go-snaps v0.5.15
github.com/go-git/go-billy/v5 v5.6.2
github.com/go-git/go-git/v5 v5.16.3
@@ -53,12 +53,12 @@ require (
github.com/google/uuid v1.6.0
github.com/gookit/color v1.6.0
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-getter v1.8.2
github.com/hashicorp/go-getter v1.8.3
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/hcl/v2 v2.24.0
github.com/iancoleman/strcase v0.3.0
github.com/invopop/jsonschema v0.7.0
github.com/jedib0t/go-pretty/v6 v6.6.8
github.com/jedib0t/go-pretty/v6 v6.6.9
github.com/jinzhu/copier v0.4.0
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
github.com/magiconair/properties v1.8.10
@@ -92,7 +92,7 @@ require (
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
golang.org/x/mod v0.29.0
golang.org/x/net v0.46.0
modernc.org/sqlite v1.39.1
modernc.org/sqlite v1.40.0
)
require (
+12 -12
View File
@@ -116,8 +116,8 @@ github.com/anchore/clio v0.0.0-20250319180342-2cfe4b0cb716 h1:2sIdYJlQESEnyk3Y0W
github.com/anchore/clio v0.0.0-20250319180342-2cfe4b0cb716/go.mod h1:Utb9i4kwiCWvqAIxZaJeMIXFO9uOgQXlvH2BfbfO/zI=
github.com/anchore/fangs v0.0.0-20250319222917-446a1e748ec2 h1:GC2QaO0YsmjpsZ4rtVKv9DnproIxqqn+qkskpc+i8MA=
github.com/anchore/fangs v0.0.0-20250319222917-446a1e748ec2/go.mod h1:XUbUECwVKuD3qYRUj+QZIOHjyyXua2gFmVjKA40iHXA=
github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537 h1:GjNGuwK5jWjJMyVppBjYS54eOiiSNv4Ba869k4wh72Q=
github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537/go.mod h1:1aiktV46ATCkuVg0O573ZrH56BUawTECPETbZyBcqT8=
github.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c h1:eoJXyC0n7DZ4YvySG/ETdYkTar2Due7eH+UmLK6FbrA=
github.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c/go.mod h1:1aiktV46ATCkuVg0O573ZrH56BUawTECPETbZyBcqT8=
github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d h1:gT69osH9AsdpOfqxbRwtxcNnSZ1zg4aKy2BevO3ZBdc=
github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d/go.mod h1:PhSnuFYknwPZkOWKB1jXBNToChBA+l0FjwOxtViIc50=
github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 h1:2SqmFgE7h+Ql4VyBzhjLkRF/3gDrcpUBj8LjvvO6OOM=
@@ -138,8 +138,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiEjnoGJZ1+Ah0ZZ/mKKqNhGcUZBl0s7PTTDzvY=
github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI=
github.com/anchore/stereoscope v0.1.11 h1:YP/XUNcJyMbOOPAWPkeZNCVlKKTRO2cnBTEeUW6I40Y=
github.com/anchore/stereoscope v0.1.11/go.mod h1:G3PZlzPbxFhylj9pQwtqfVPaahuWmy/UCtv5FTIIMvg=
github.com/anchore/stereoscope v0.1.12 h1:4T/10G7Nb98UoJBKVvAIhsAtrR63lZXxMJb/Qfw5inw=
github.com/anchore/stereoscope v0.1.12/go.mod h1:G3PZlzPbxFhylj9pQwtqfVPaahuWmy/UCtv5FTIIMvg=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
@@ -385,8 +385,8 @@ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/github/go-spdx/v2 v2.3.3 h1:QI7evnHWEfWkT54eJwkoV/f3a0xD3gLlnVmT5wQG6LE=
github.com/github/go-spdx/v2 v2.3.3/go.mod h1:2ZxKsOhvBp+OYBDlsGnUMcchLeo2mrpEBn2L1C+U3IQ=
github.com/github/go-spdx/v2 v2.3.4 h1:6VNAsYWvQge+SOeoubTlH81MY21d5uekXNIRGfXMNXo=
github.com/github/go-spdx/v2 v2.3.4/go.mod h1:7LYNCshU2Gj17qZ0heJ5CQUKWWmpd98K7o93K8fJSMk=
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
@@ -556,8 +556,8 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-getter v1.8.2 h1:CGCK+bZQLl44PYiwJweVzfpjg7bBwtuXu3AGcLiod2o=
github.com/hashicorp/go-getter v1.8.2/go.mod h1:CUTt9x2bCtJ/sV8ihgrITL3IUE+0BE1j/e4n5P/GIM4=
github.com/hashicorp/go-getter v1.8.3 h1:gIS+oTNv3kyYAvlUVgMR46MiG0bM0KuSON/KZEvRoRg=
github.com/hashicorp/go-getter v1.8.3/go.mod h1:CUTt9x2bCtJ/sV8ihgrITL3IUE+0BE1j/e4n5P/GIM4=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
@@ -609,8 +609,8 @@ github.com/invopop/jsonschema v0.7.0 h1:2vgQcBz1n256N+FpX3Jq7Y17AjYt46Ig3zIWyy77
github.com/invopop/jsonschema v0.7.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc=
github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/jedib0t/go-pretty/v6 v6.6.9 h1:PQecJLK3L8ODuVyMe2223b61oRJjrKnmXAncbWTv9MY=
github.com/jedib0t/go-pretty/v6 v6.6.9/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
@@ -1509,8 +1509,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4=
modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/sqlite v1.40.0 h1:bNWEDlYhNPAUdUdBzjAvn8icAs/2gaKlj4vM+tQ6KdQ=
modernc.org/sqlite v1.40.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
@@ -0,0 +1,46 @@
package pkgtestobservation
import "time"
// Observations represents capability observations during testing
type Observations struct {
License bool `json:"license"`
Relationships Relationship `json:"relationships"`
FileListing Count `json:"file_listing"`
FileDigests Count `json:"file_digests"`
IntegrityHash Count `json:"integrity_hash"`
}
// Relationship tracks dependency relationship observations
type Relationship struct {
Found bool `json:"found"`
Count int `json:"count"`
}
// Count tracks whether a capability was found and how many times
type Count struct {
Found bool `json:"found"`
Count int `json:"count"`
}
// Test is the root structure for test-observations.json
type Test struct {
Package string `json:"package"`
UpdatedAt time.Time `json:"updated_at"`
Catalogers map[string]*Cataloger `json:"catalogers"`
Parsers map[string]*Parser `json:"parsers"`
}
// Parser captures all observations for a parser
type Parser struct {
MetadataTypes []string `json:"metadata_types"`
PackageTypes []string `json:"package_types"`
Observations Observations `json:"observations"`
}
// Cataloger captures all observations for a cataloger
type Cataloger struct {
MetadataTypes []string `json:"metadata_types"`
PackageTypes []string `json:"package_types"`
Observations Observations `json:"observations"`
}
+1 -1
View File
@@ -3,5 +3,5 @@ package internal
const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "16.0.40"
JSONSchemaVersion = "16.0.41"
)
+4
View File
@@ -81,6 +81,10 @@ func Test_EnvironmentTask(t *testing.T) {
// get the source
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, theSource.Close())
})
resolver, err := theSource.FileResolver(source.SquashedScope)
require.NoError(t, err)
+1
View File
@@ -51,6 +51,7 @@ func AllTypes() []any {
pkg.PhpPeclEntry{},
pkg.PortageEntry{},
pkg.PythonPackage{},
pkg.PythonPdmLockEntry{},
pkg.PythonPipfileLockEntry{},
pkg.PythonPoetryLockEntry{},
pkg.PythonRequirementsEntry{},
+1
View File
@@ -102,6 +102,7 @@ var jsonTypes = makeJSONTypes(
jsonNames(pkg.PhpPearEntry{}, "php-pear-entry"),
jsonNames(pkg.PortageEntry{}, "portage-db-entry", "PortageMetadata"),
jsonNames(pkg.PythonPackage{}, "python-package", "PythonPackageMetadata"),
jsonNames(pkg.PythonPdmLockEntry{}, "python-pdm-lock-entry"),
jsonNames(pkg.PythonPipfileLockEntry{}, "python-pipfile-lock-entry", "PythonPipfileLockMetadata"),
jsonNames(pkg.PythonPoetryLockEntry{}, "python-poetry-lock-entry", "PythonPoetryLockMetadata"),
jsonNames(pkg.PythonRequirementsEntry{}, "python-pip-requirements-entry", "PythonRequirementsMetadata"),
@@ -10,7 +10,6 @@ import (
"sort"
"strings"
"text/template"
"time"
)
// This program generates license_list.go.
@@ -20,8 +19,7 @@ const (
)
var tmp = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at {{ .Timestamp }}
// using data from {{ .URL }}
// This file was generated using data from {{ .URL }}
package spdxlicense
const Version = {{ printf "%q" .Version }}
@@ -78,13 +76,11 @@ func run() error {
urlToLicense := buildURLToLicenseMap(result)
err = tmp.Execute(f, struct {
Timestamp time.Time
URL string
Version string
LicenseIDs map[string]string
URLToLicense map[string]string
}{
Timestamp: time.Now(),
URL: url,
Version: result.Version,
LicenseIDs: licenseIDs,
File diff suppressed because it is too large Load Diff
+33 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.0.40/document",
"$id": "anchore.io/schema/syft/json/16.0.41/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@@ -2549,6 +2549,9 @@
{
"$ref": "#/$defs/PythonPackage"
},
{
"$ref": "#/$defs/PythonPdmLockEntry"
},
{
"$ref": "#/$defs/PythonPipRequirementsEntry"
},
@@ -3131,6 +3134,35 @@
],
"description": "PythonPackage represents all captured data for a python egg or wheel package (specifically as outlined in the PyPA core metadata specification https://packaging.python.org/en/latest/specifications/core-metadata/)."
},
"PythonPdmLockEntry": {
"properties": {
"summary": {
"type": "string",
"description": "Summary provides a description of the package"
},
"files": {
"items": {
"$ref": "#/$defs/PythonFileRecord"
},
"type": "array",
"description": "Files are the package files with their paths and hash digests"
},
"dependencies": {
"items": {
"type": "string"
},
"type": "array",
"description": "Dependencies are the dependency specifications, without environment qualifiers"
}
},
"type": "object",
"required": [
"summary",
"files",
"dependencies"
],
"description": "PythonPdmLockEntry represents a single package entry within a pdm.lock file."
},
"PythonPipRequirementsEntry": {
"properties": {
"name": {
+27 -8
View File
@@ -3,6 +3,7 @@ package executable
import (
"debug/macho"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
)
@@ -19,20 +20,38 @@ const (
func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader) error {
// TODO: support security features
// TODO: support multi-architecture binaries
f, err := macho.NewFile(reader)
// a universal binary may have multiple architectures, so we need to check each one
readers, err := unionreader.GetReaders(reader)
if err != nil {
return err
}
libs, err := f.ImportedLibraries()
if err != nil {
return err
var libs []string
for _, r := range readers {
f, err := macho.NewFile(r)
if err != nil {
return err
}
rLibs, err := f.ImportedLibraries()
if err != nil {
return err
}
libs = append(libs, rLibs...)
// TODO handle only some having entrypoints/exports? If that is even practical
// only check for entrypoint if we don't already have one
if !data.HasEntrypoint {
data.HasEntrypoint = machoHasEntrypoint(f)
}
// only check for exports if we don't already have them
if !data.HasExports {
data.HasExports = machoHasExports(f)
}
}
data.ImportedLibraries = libs
data.HasEntrypoint = machoHasEntrypoint(f)
data.HasExports = machoHasExports(f)
// de-duplicate libraries
data.ImportedLibraries = internal.NewSet(libs...).ToSlice()
return nil
}
@@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
)
@@ -83,3 +84,39 @@ func Test_machoHasExports(t *testing.T) {
})
}
}
func Test_machoUniversal(t *testing.T) {
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
t.Helper()
f, err := os.Open(filepath.Join("test-fixtures/shared-info", fixture))
require.NoError(t, err)
return f
}
tests := []struct {
name string
fixture string
want file.Executable
}{
{
name: "universal lib",
fixture: "bin/libhello_universal.dylib",
want: file.Executable{HasExports: true, HasEntrypoint: false},
},
{
name: "universal application",
fixture: "bin/hello_mac_universal",
want: file.Executable{HasExports: false, HasEntrypoint: true},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var data file.Executable
err := findMachoFeatures(&data, readerForFixture(t, tt.fixture))
require.NoError(t, err)
assert.Equal(t, tt.want.HasEntrypoint, data.HasEntrypoint)
assert.Equal(t, tt.want.HasExports, data.HasExports)
})
}
}
@@ -2,13 +2,13 @@
BIN=../../bin
all: $(BIN)/hello_linux $(BIN)/hello.exe $(BIN)/hello_mac
all: $(BIN)/hello_linux $(BIN)/hello.exe $(BIN)/hello_mac $(BIN)/hello_mac_universal
linux: $(BIN)/libhello.so
windows: $(BIN)/libhello.dll
mac: $(BIN)/libhello.dylib
mac: $(BIN)/libhello.dylib $(BIN)/hello_mac_universal
$(BIN)/hello_linux:
gcc hello.c -o $(BIN)/hello_linux
@@ -19,5 +19,8 @@ $(BIN)/hello.exe:
$(BIN)/hello_mac:
o64-clang hello.c -o $(BIN)/hello_mac
$(BIN)/hello_mac_universal:
o64-clang -arch arm64 -arch x86_64 hello.c -o $(BIN)/hello_mac_universal
clean:
rm -f $(BIN)/hello_linux $(BIN)/hello.exe $(BIN)/hello_mac
rm -f $(BIN)/hello_linux $(BIN)/hello.exe $(BIN)/hello_mac $(BIN)/hello_mac_universal
@@ -2,13 +2,13 @@
BIN=../../bin
all: $(BIN)/libhello.so $(BIN)/libhello.dll $(BIN)/libhello.dylib
all: $(BIN)/libhello.so $(BIN)/libhello.dll $(BIN)/libhello.dylib $(BIN)/libhello_universal.dylib
linux: $(BIN)/libhello.so
windows: $(BIN)/libhello.dll
mac: $(BIN)/libhello.dylib
mac: $(BIN)/libhello.dylib $(BIN)/libhello_universal.dylib
$(BIN)/libhello.so:
gcc -shared -fPIC -o $(BIN)/libhello.so hello.c
@@ -19,5 +19,8 @@ $(BIN)/libhello.dll:
$(BIN)/libhello.dylib:
o64-clang -dynamiclib -o $(BIN)/libhello.dylib hello.c
$(BIN)/libhello_universal.dylib:
o64-clang -dynamiclib -arch arm64 -arch x86_64 hello.c -o $(BIN)/libhello_universal.dylib
clean:
rm -f $(BIN)/libhello.so $(BIN)/hello.dll $(BIN)/libhello.dylib $(BIN)/libhello.a
rm -f $(BIN)/libhello.so $(BIN)/hello.dll $(BIN)/libhello.dylib $(BIN)/libhello.a $(BIN)/libhello_universal.dylib
@@ -87,6 +87,9 @@ func toGithubManifests(s *sbom.SBOM) Manifests {
}
name := dependencyName(p)
if name == "" || p.PURL == "" {
continue
}
manifest.Resolved[name] = DependencyNode{
PackageURL: p.PURL,
Metadata: toDependencyMetadata(p),
@@ -16,11 +16,6 @@
"source_location": "redacted/some/path/some/path/pkg1"
},
"resolved": {
"": {
"package_url": "a-purl-2",
"relationship": "direct",
"scope": "runtime"
},
"pkg:deb/debian/package-2@2.0.1": {
"package_url": "pkg:deb/debian/package-2@2.0.1",
"relationship": "direct",
@@ -17,13 +17,6 @@
},
"metadata": {
"syft:filesystem":"redacted"
},
"resolved": {
"": {
"package_url": "a-purl-1",
"relationship": "direct",
"scope": "runtime"
}
}
},
"user-image-input:/somefile-2.txt": {
+36
View File
@@ -29,6 +29,7 @@ func Backfill(p *pkg.Package) {
var cpes []cpe.CPE
epoch := ""
rpmmod := ""
for _, qualifier := range purl.Qualifiers {
switch qualifier.Key {
@@ -44,6 +45,8 @@ func Backfill(p *pkg.Package) {
}
case pkg.PURLQualifierEpoch:
epoch = qualifier.Value
case pkg.PURLQualifierRpmModularity:
rpmmod = qualifier.Value
}
}
@@ -63,6 +66,10 @@ func Backfill(p *pkg.Package) {
setJavaMetadataFromPurl(p, purl)
}
if p.Type == pkg.RpmPkg {
setRpmMetadataFromPurl(p, rpmmod)
}
for _, c := range cpes {
if slices.Contains(p.CPEs, c) {
continue
@@ -82,6 +89,35 @@ func setJavaMetadataFromPurl(p *pkg.Package, _ packageurl.PackageURL) {
}
}
func setRpmMetadataFromPurl(p *pkg.Package, rpmmod string) {
if p.Type != pkg.RpmPkg {
return
}
if rpmmod == "" {
return
}
if p.Metadata == nil {
p.Metadata = pkg.RpmDBEntry{
ModularityLabel: &rpmmod,
}
return
}
switch m := p.Metadata.(type) {
case pkg.RpmDBEntry:
if m.ModularityLabel == nil {
m.ModularityLabel = &rpmmod
p.Metadata = m
}
case pkg.RpmArchive:
if m.ModularityLabel == nil {
m.ModularityLabel = &rpmmod
p.Metadata = m
}
}
}
func setVersionFromPurl(p *pkg.Package, purl packageurl.PackageURL, epoch string) {
if p.Version == "" {
p.Version = purl.Version
+19
View File
@@ -53,6 +53,21 @@ func Test_Backfill(t *testing.T) {
Version: "1:1.12.8-26.el8",
},
},
{
name: "rpm with rpmmod",
in: pkg.Package{
PURL: "pkg:rpm/redhat/httpd@2.4.37-51?arch=x86_64&distro=rhel-8.7&rpmmod=httpd:2.4",
},
expected: pkg.Package{
PURL: "pkg:rpm/redhat/httpd@2.4.37-51?arch=x86_64&distro=rhel-8.7&rpmmod=httpd:2.4",
Type: pkg.RpmPkg,
Name: "httpd",
Version: "2.4.37-51",
Metadata: pkg.RpmDBEntry{
ModularityLabel: strRef("httpd:2.4"),
},
},
},
{
name: "bad cpe",
in: pkg.Package{
@@ -171,3 +186,7 @@ func Test_nameFromPurl(t *testing.T) {
})
}
}
func strRef(s string) *string {
return &s
}
@@ -42,6 +42,7 @@ func Test_OriginatorSupplier(t *testing.T) {
pkg.PhpPeclEntry{},
pkg.PortageEntry{},
pkg.PythonPipfileLockEntry{},
pkg.PythonPdmLockEntry{},
pkg.PythonRequirementsEntry{},
pkg.PythonPoetryLockEntry{},
pkg.PythonUvLockEntry{},
@@ -342,6 +343,25 @@ func Test_OriginatorSupplier(t *testing.T) {
originator: "Person: auth (auth@auth.gov)",
supplier: "Person: auth (auth@auth.gov)",
},
{
name: "from python PDM lock",
input: pkg.Package{
Metadata: pkg.PythonPdmLockEntry{
Files: []pkg.PythonFileRecord{
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651",
},
},
},
Summary: "A test package",
},
},
originator: "",
supplier: "",
},
{
name: "from r -- maintainer > author",
input: pkg.Package{
+37
View File
@@ -0,0 +1,37 @@
package syft
import (
"testing"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/syft/source/sourceproviders"
)
func TestGetProviders_DefaultImagePullSource(t *testing.T) {
userInput := ""
cfg := &GetSourceConfig{DefaultImagePullSource: stereoscope.RegistryTag}
allSourceProviders := sourceproviders.All(userInput, cfg.SourceProviderConfig)
providers, err := cfg.getProviders(userInput)
if err != nil {
t.Errorf("Expected no error for DefaultImagePullSource parameter, got: %v", err)
}
if len(providers) != len(allSourceProviders) {
t.Errorf("Expected %d providers, got %d", len(allSourceProviders), len(providers))
}
}
func TestGetProviders_Sources(t *testing.T) {
userInput := ""
cfg := &GetSourceConfig{Sources: []string{stereoscope.RegistryTag}}
providers, err := cfg.getProviders(userInput)
if err != nil {
t.Errorf("Expected no error for Sources parameter, got: %v", err)
}
if len(providers) != 1 {
t.Errorf("Expected 1 providers, got %d", len(providers))
}
}
+5 -5
View File
@@ -19,16 +19,16 @@ type Directory struct {
indexer *directoryIndexer
}
func NewFromDirectory(root string, base string, pathFilters ...PathIndexVisitor) (*Directory, error) {
r, err := newFromDirectoryWithoutIndex(root, base, pathFilters...)
func NewFromDirectory(root, base string, pathFilters ...PathIndexVisitor) (*Directory, error) {
resolver, err := newFromDirectoryWithoutIndex(root, base, pathFilters...)
if err != nil {
return nil, err
}
return r, r.buildIndex()
return resolver, resolver.buildIndex()
}
func newFromDirectoryWithoutIndex(root string, base string, pathFilters ...PathIndexVisitor) (*Directory, error) {
func newFromDirectoryWithoutIndex(root, base string, pathFilters ...PathIndexVisitor) (*Directory, error) {
chroot, err := NewChrootContextFromCWD(root, base)
if err != nil {
return nil, fmt.Errorf("unable to interpret chroot context: %w", err)
@@ -66,6 +66,6 @@ func (r *Directory) buildIndex() error {
}
// Stringer to represent a directory path data source
func (r Directory) String() string {
func (r *Directory) String() string {
return fmt.Sprintf("dir:%s", r.path)
}
@@ -322,7 +322,7 @@ func (r directoryIndexer) addDirectoryToIndex(p string, info os.FileInfo) error
return err
}
metadata := file.NewMetadataFromPath(p, info)
metadata := NewMetadataFromPath(p, info)
r.index.Add(*ref, metadata)
return nil
@@ -334,7 +334,7 @@ func (r directoryIndexer) addFileToIndex(p string, info os.FileInfo) error {
return err
}
metadata := file.NewMetadataFromPath(p, info)
metadata := NewMetadataFromPath(p, info)
r.index.Add(*ref, metadata)
return nil
@@ -416,7 +416,7 @@ func (r directoryIndexer) addSymlinkToIndex(p string, info os.FileInfo) (string,
targetAbsPath = filepath.Clean(filepath.Join(path.Dir(p), linkTarget))
}
metadata := file.NewMetadataFromPath(p, info)
metadata := NewMetadataFromPath(p, info)
metadata.LinkDestination = linkTarget
r.index.Add(*ref, metadata)
+20 -8
View File
@@ -17,17 +17,31 @@ type File struct {
indexer *fileIndexer
}
// parent should be the symlink free absolute path to the parent directory
// NewFromFile single file analyser
// path is the filepath of the file we're creating content access for
func NewFromFile(parent, path string, pathFilters ...PathIndexVisitor) (*File, error) {
chroot, err := NewChrootContextFromCWD(parent, parent)
func NewFromFile(path string, pathFilters ...PathIndexVisitor) (*File, error) {
resolver, err := newFromFileWithoutIndex(path, pathFilters...)
if err != nil {
return nil, err
}
return resolver, resolver.buildIndex()
}
func newFromFileWithoutIndex(path string, pathFilters ...PathIndexVisitor) (*File, error) {
absParentDir, err := absoluteSymlinkFreePathToParent(path)
if err != nil {
return nil, err
}
chroot, err := NewChrootContextFromCWD(absParentDir, absParentDir)
if err != nil {
return nil, fmt.Errorf("unable to interpret chroot context: %w", err)
}
cleanBase := chroot.Base()
file := &File{
return &File{
path: path,
FiletreeResolver: FiletreeResolver{
Chroot: *chroot,
@@ -36,9 +50,7 @@ func NewFromFile(parent, path string, pathFilters ...PathIndexVisitor) (*File, e
Opener: nativeOSFileOpener,
},
indexer: newFileIndexer(path, cleanBase, pathFilters...),
}
return file, file.buildIndex()
}, nil
}
func (r *File) buildIndex() error {
@@ -58,6 +70,6 @@ func (r *File) buildIndex() error {
}
// Stringer to represent a file path data source
func (r File) String() string {
func (r *File) String() string {
return fmt.Sprintf("file:%s", r.path)
}
+2 -2
View File
@@ -173,7 +173,7 @@ func (r *fileIndexer) addDirectoryToIndex(path string, info os.FileInfo) error {
return err
}
metadata := file.NewMetadataFromPath(path, info)
metadata := NewMetadataFromPath(path, info)
r.index.Add(*ref, metadata)
return nil
@@ -185,7 +185,7 @@ func (r *fileIndexer) addFileToIndex(path string, info os.FileInfo) error {
return err
}
metadata := file.NewMetadataFromPath(path, info)
metadata := NewMetadataFromPath(path, info)
r.index.Add(*ref, metadata)
return nil
@@ -1384,9 +1384,10 @@ func TestFileResolver_FilesByPath(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, parentPath)
resolver, err := NewFromFile(parentPath, tt.filePath)
resolver, err := NewFromFile(tt.filePath)
require.NoError(t, err)
require.NotNil(t, resolver)
assert.Equal(t, resolver.Chroot.Base(), parentPath)
refs, err := resolver.FilesByPath(tt.fileByPathInput)
require.NoError(t, err)
@@ -1431,8 +1432,11 @@ func TestFileResolver_MultipleFilesByPath(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, parentPath)
resolver, err := NewFromFile(parentPath, filePath)
resolver, err := NewFromFile(filePath)
assert.NoError(t, err)
require.NotNil(t, resolver)
assert.Equal(t, resolver.Chroot.Base(), parentPath)
refs, err := resolver.FilesByPath(tt.input...)
assert.NoError(t, err)
@@ -1449,8 +1453,11 @@ func TestFileResolver_FilesByGlob(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, parentPath)
resolver, err := NewFromFile(parentPath, filePath)
resolver, err := NewFromFile(filePath)
assert.NoError(t, err)
require.NotNil(t, resolver)
assert.Equal(t, resolver.Chroot.Base(), parentPath)
refs, err := resolver.FilesByGlob("**/*.txt")
assert.NoError(t, err)
@@ -1476,8 +1483,11 @@ func Test_fileResolver_FilesByMIMEType(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, parentPath)
resolver, err := NewFromFile(parentPath, filePath)
resolver, err := NewFromFile(filePath)
assert.NoError(t, err)
require.NotNil(t, resolver)
assert.Equal(t, resolver.Chroot.Base(), parentPath)
locations, err := resolver.FilesByMIMEType(test.mimeType)
assert.NoError(t, err)
assert.Equal(t, test.expectedPaths.Size(), len(locations))
@@ -1497,10 +1507,12 @@ func Test_fileResolver_FileContentsByLocation(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, parentPath)
r, err := NewFromFile(parentPath, filePath)
resolver, err := NewFromFile(filePath)
require.NoError(t, err)
require.NotNil(t, resolver)
assert.Equal(t, resolver.Chroot.Base(), parentPath)
exists, existingPath, err := r.Tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt")))
exists, existingPath, err := resolver.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())
@@ -1525,7 +1537,7 @@ func Test_fileResolver_FileContentsByLocation(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, err := r.FileContentsByLocation(test.location)
actual, err := resolver.FileContentsByLocation(test.location)
if test.err {
require.Error(t, err)
return
@@ -1546,8 +1558,11 @@ func TestFileResolver_AllLocations_errorOnDirRequest(t *testing.T) {
parentPath, err := absoluteSymlinkFreePathToParent(filePath)
require.NoError(t, err)
require.NotNil(t, parentPath)
resolver, err := NewFromFile(parentPath, filePath)
resolver, err := NewFromFile(filePath)
require.NoError(t, err)
require.NotNil(t, resolver)
assert.Equal(t, resolver.Chroot.Base(), parentPath)
var dirLoc *file.Location
ctx, cancel := context.WithCancel(context.Background())
@@ -1575,8 +1590,11 @@ func TestFileResolver_AllLocations(t *testing.T) {
parentPath, err := absoluteSymlinkFreePathToParent(filePath)
require.NoError(t, err)
require.NotNil(t, parentPath)
resolver, err := NewFromFile(parentPath, filePath)
resolver, err := NewFromFile(filePath)
require.NoError(t, err)
require.NotNil(t, resolver)
assert.Equal(t, resolver.Chroot.Base(), parentPath)
paths := strset.New()
for loc := range resolver.AllLocations(context.Background()) {
@@ -1600,8 +1618,11 @@ func Test_FileResolver_AllLocationsDoesNotLeakGoRoutine(t *testing.T) {
parentPath, err := absoluteSymlinkFreePathToParent(filePath)
require.NoError(t, err)
require.NotNil(t, parentPath)
resolver, err := NewFromFile(parentPath, filePath)
resolver, err := NewFromFile(filePath)
require.NoError(t, err)
require.NotNil(t, resolver)
assert.Equal(t, resolver.Chroot.Base(), parentPath)
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
+20
View File
@@ -0,0 +1,20 @@
//go:build !windows
package fileresolver
import (
"os"
"syscall"
)
// getXid is the UID GID system info for unix
func getXid(info os.FileInfo) (uid, gid int) {
uid = -1
gid = -1
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
uid = int(stat.Uid)
gid = int(stat.Gid)
}
return uid, gid
}
+12
View File
@@ -0,0 +1,12 @@
//go:build windows
package fileresolver
import (
"os"
)
// getXid is a placeholder for windows file information
func getXid(info os.FileInfo) (uid, gid int) {
return -1, -1
}
+44
View File
@@ -0,0 +1,44 @@
package fileresolver
import (
"os"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/internal/windows"
)
func NewMetadataFromPath(path string, info os.FileInfo) file.Metadata {
var mimeType string
uid, gid := getXid(info)
ty := file.TypeFromMode(info.Mode())
if ty == file.TypeRegular {
usablePath := path
// denormalize the path back to windows so we can open the file
if windows.HostRunningOnWindows() {
usablePath = windows.FromPosix(usablePath)
}
f, err := os.Open(usablePath)
if err != nil {
// TODO: it may be that the file is inaccessible, however, this is not an error or a warning. In the future we need to track these as known-unknowns
f = nil
} else {
defer internal.CloseAndLogError(f, usablePath)
}
mimeType = file.MIMEType(f)
}
return file.Metadata{
FileInfo: info,
Path: path,
Type: ty,
// unsupported across platforms
UserID: uid,
GroupID: gid,
MIMEType: mimeType,
}
}
@@ -0,0 +1,51 @@
package fileresolver
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/file"
)
func TestFileMetadataFromPath(t *testing.T) {
tests := []struct {
path string
expectedType file.Type
expectedMIMEType string
}{
{
path: "test-fixtures/symlinks-simple/readme",
expectedType: file.TypeRegular,
expectedMIMEType: "text/plain",
},
{
path: "test-fixtures/symlinks-simple/link_to_new_readme",
expectedType: file.TypeSymLink,
expectedMIMEType: "",
},
{
path: "test-fixtures/symlinks-simple/link_to_link_to_new_readme",
expectedType: file.TypeSymLink,
expectedMIMEType: "",
},
{
path: "test-fixtures/symlinks-simple",
expectedType: file.TypeDirectory,
expectedMIMEType: "",
},
}
for _, test := range tests {
t.Run(test.path, func(t *testing.T) {
info, err := os.Lstat(test.path)
require.NoError(t, err)
actual := NewMetadataFromPath(test.path, info)
assert.Equal(t, test.expectedMIMEType, actual.MIMEType, "unexpected MIME type for %s", test.path)
assert.Equal(t, test.expectedType, actual.Type, "unexpected type for %s", test.path)
})
}
}
+2
View File
@@ -0,0 +1,2 @@
# these are generated by pkgtest helpers, no need to check them in
**/test-fixtures/test-observations.json
+14
View File
@@ -6,6 +6,7 @@ import (
"sort"
"strings"
packageurl "github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
@@ -32,6 +33,19 @@ func newPEPackage(versionResources map[string]string, f file.Location) pkg.Packa
Metadata: newPEBinaryVersionResourcesFromMap(versionResources),
}
// If this appears to be Ghostscript, emit a canonical generic purl
// Example expected: pkg:generic/ghostscript@<version>
prod := strings.ToLower(spaceNormalize(versionResources["ProductName"]))
if prod == "" {
// fall back to FileDescription if ProductName is missing
prod = strings.ToLower(spaceNormalize(versionResources["FileDescription"]))
}
if p.Version != "" && strings.Contains(prod, "ghostscript") {
// build a generic PURL for ghostscript
purl := packageurl.NewPackageURL(packageurl.TypeGeneric, "", "ghostscript", p.Version, nil, "").ToString()
p.PURL = purl
}
p.SetID()
return p
@@ -0,0 +1,24 @@
package binary
import (
"testing"
"github.com/anchore/syft/syft/file"
)
func TestGhostscriptPEGeneratesGenericPURL(t *testing.T) {
vr := map[string]string{
"CompanyName": "Artifex Software, Inc.",
"ProductName": "GPL Ghostscript",
"FileDescription": "Ghostscript Interpreter",
"ProductVersion": "9.54.0",
}
loc := file.NewLocation("/usr/bin/gswin64c.exe")
p := newPEPackage(vr, loc)
expected := "pkg:generic/ghostscript@9.54.0"
if p.PURL != expected {
t.Fatalf("expected purl %q, got %q", expected, p.PURL)
}
}
@@ -24,6 +24,10 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
const (
deinstallStatus string = "deinstall"
)
var (
errEndOfPackages = fmt.Errorf("no more packages to read")
sourceRegexp = regexp.MustCompile(`(?P<name>\S+)( \((?P<version>.*)\))?`)
@@ -112,6 +116,7 @@ type dpkgExtractedMetadata struct {
Provides string `mapstructure:"Provides"`
Depends string `mapstructure:"Depends"`
PreDepends string `mapstructure:"PreDepends"` // note: original doc is Pre-Depends
Status string `mapstructure:"Status"`
}
// parseDpkgStatusEntry returns an individual Dpkg entry, or returns errEndOfPackages if there are no more packages to parse from the reader.
@@ -134,6 +139,11 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (*pkg.DpkgDBEntry, error) {
return nil, err
}
// Skip entries which have been removed but not purged, e.g. "rc" status in dpkg -l
if strings.Contains(raw.Status, deinstallStatus) {
return nil, nil
}
sourceName, sourceVersion := extractSourceVersion(raw.Source)
if sourceVersion != "" {
raw.SourceVersion = sourceVersion
@@ -237,6 +237,37 @@ func Test_parseDpkgStatus(t *testing.T) {
},
},
},
{
name: "deinstall status packages are ignored",
fixturePath: "test-fixtures/var/lib/dpkg/status.d/deinstall",
expected: []pkg.DpkgDBEntry{
{
Package: "linux-image-6.14.0-1012-aws",
Source: "linux-signed-aws-6.14",
Version: "6.14.0-1012.12~24.04.1",
Architecture: "amd64",
InstalledSize: 15221,
Maintainer: "Canonical Kernel Team <kernel-team@lists.ubuntu.com>",
Description: `Signed kernel image aws
A kernel image for aws. This version of it is signed with
Canonical's signing key.`,
Provides: []string{"fuse-module",
"linux-image",
"spl-dkms",
"spl-modules",
"v4l2loopback-dkms",
"v4l2loopback-modules",
"zfs-dkms",
"zfs-modules"},
Depends: []string{
"kmod",
"linux-base (>= 4.5ubuntu1~16.04.1)",
"linux-modules-6.14.0-1012-aws",
},
Files: []pkg.DpkgFileRecord{},
},
},
},
}
for _, test := range tests {
@@ -0,0 +1,38 @@
Package: linux-image-6.14.0-1012-aws
Status: install ok installed
Priority: optional
Section: kernel
Installed-Size: 15221
Maintainer: Canonical Kernel Team <kernel-team@lists.ubuntu.com>
Architecture: amd64
Source: linux-signed-aws-6.14
Version: 6.14.0-1012.12~24.04.1
Provides: fuse-module, linux-image, spl-dkms, spl-modules, v4l2loopback-dkms, v4l2loopback-modules, zfs-dkms, zfs-modules
Depends: kmod, linux-base (>= 4.5ubuntu1~16.04.1), linux-modules-6.14.0-1012-aws
Recommends: grub-pc | grub-efi-amd64 | grub-efi-ia32 | grub | lilo, initramfs-tools | linux-initramfs-tool
Suggests: bpftool, linux-perf, linux-aws-6.14-doc-6.14.0 | linux-aws-6.14-source-6.14.0, linux-aws-6.14-tools, linux-headers-6.14.0-1012-aws
Conflicts: linux-image-unsigned-6.14.0-1012-aws
Description: Signed kernel image aws
A kernel image for aws. This version of it is signed with
Canonical's signing key.
Built-Using: linux-aws-6.14 (= 6.14.0-1012.12~24.04.1)
Package: linux-image-6.8.0-1029-aws
Status: deinstall ok config-files
Priority: optional
Section: kernel
Installed-Size: 14591
Maintainer: Canonical Kernel Team <kernel-team@lists.ubuntu.com>
Architecture: amd64
Source: linux-signed-aws
Version: 6.8.0-1029.31
Config-Version: 6.8.0-1029.31
Provides: fuse-module, linux-image, spl-dkms, spl-modules, v4l2loopback-dkms, v4l2loopback-modules, zfs-dkms, zfs-modules
Depends: kmod, linux-base (>= 4.5ubuntu1~16.04.1), linux-modules-6.8.0-1029-aws
Recommends: grub-pc | grub-efi-amd64 | grub-efi-ia32 | grub | lilo, initramfs-tools | linux-initramfs-tool
Suggests: fdutils, linux-aws-doc-6.8.0 | linux-aws-source-6.8.0, linux-aws-tools, linux-headers-6.8.0-1029-aws
Conflicts: linux-image-unsigned-6.8.0-1029-aws
Description: Signed kernel image aws
A kernel image for aws. This version of it is signed with
Canonical's signing key.
Built-Using: linux-aws (= 6.8.0-1029.31)
+9 -2
View File
@@ -10,7 +10,14 @@ import (
)
func (c *goBinaryCataloger) newGoBinaryPackage(dep *debug.Module, m pkg.GolangBinaryBuildinfoEntry, licenses []pkg.License, locations ...file.Location) pkg.Package {
// Similar to syft/pkg/cataloger/golang/parse_go_mod.go logic - use original path for relative replacements
finalPath := dep.Path
if dep.Replace != nil {
if strings.HasPrefix(dep.Replace.Path, ".") || strings.HasPrefix(dep.Replace.Path, "/") {
finalPath = dep.Path
} else {
finalPath = dep.Replace.Path
}
dep = dep.Replace
}
@@ -23,10 +30,10 @@ func (c *goBinaryCataloger) newGoBinaryPackage(dep *debug.Module, m pkg.GolangBi
}
p := pkg.Package{
Name: dep.Path,
Name: finalPath,
Version: version,
Licenses: pkg.NewLicenseSet(licenses...),
PURL: packageURL(dep.Path, version),
PURL: packageURL(finalPath, version),
Language: pkg.Go,
Type: pkg.GoModulePkg,
Locations: file.NewLocationSet(locations...),
+65
View File
@@ -1,6 +1,7 @@
package golang
import (
"runtime/debug"
"testing"
"github.com/stretchr/testify/assert"
@@ -54,3 +55,67 @@ func Test_packageURL(t *testing.T) {
})
}
}
func Test_newGoBinaryPackage_relativeReplace(t *testing.T) {
tests := []struct {
name string
dep *debug.Module
expectedName string
}{
{
name: "relative replace with ../",
dep: &debug.Module{
Path: "github.com/aws/aws-sdk-go-v2",
Version: "(devel)",
Replace: &debug.Module{
Path: "../../",
Version: "(devel)",
},
},
expectedName: "github.com/aws/aws-sdk-go-v2", // should use original path, not relative
},
{
name: "relative replace with ./",
dep: &debug.Module{
Path: "github.com/example/module",
Version: "v1.0.0",
Replace: &debug.Module{
Path: "./local",
Version: "v0.0.0",
},
},
expectedName: "github.com/example/module", // should use original path
},
{
name: "absolute replace",
dep: &debug.Module{
Path: "github.com/old/module",
Version: "v1.0.0",
Replace: &debug.Module{
Path: "github.com/new/module",
Version: "v2.0.0",
},
},
expectedName: "github.com/new/module", // should use replacement path
},
{
name: "no replace",
dep: &debug.Module{
Path: "github.com/normal/module",
Version: "v1.0.0",
},
expectedName: "github.com/normal/module", // should use original path
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cataloger := &goBinaryCataloger{}
result := cataloger.newGoBinaryPackage(test.dep, pkg.GolangBinaryBuildinfoEntry{}, nil)
assert.Equal(t, test.expectedName, result.Name)
assert.Equal(t, pkg.Go, result.Language)
assert.Equal(t, pkg.GoModulePkg, result.Type)
})
}
}
@@ -0,0 +1,63 @@
package cpegenerate
import (
"strings"
"github.com/anchore/syft/syft/pkg"
)
// candidateVendorsForPE returns vendor candidates for PE (BinaryPkg) packages based on common metadata hints.
// Specifically, normalize Ghostscript binaries to vendor "artifex" when detected.
func candidateVendorsForPE(p pkg.Package) fieldCandidateSet {
candidates := newFieldCandidateSet()
meta, ok := p.Metadata.(pkg.PEBinary)
if !ok {
return candidates
}
var company, product, fileDesc string
for _, kv := range meta.VersionResources {
switch strings.ToLower(kv.Key) {
case "companyname":
company = strings.ToLower(kv.Value)
case "productname":
product = strings.ToLower(kv.Value)
case "filedescription":
fileDesc = strings.ToLower(kv.Value)
}
}
if strings.Contains(product, "ghostscript") || strings.Contains(fileDesc, "ghostscript") || strings.Contains(company, "artifex") {
candidates.addValue("artifex")
}
return candidates
}
// candidateProductsForPE returns product candidates for PE (BinaryPkg) packages based on common metadata hints.
// Specifically, normalize Ghostscript binaries to product "ghostscript" when detected.
func candidateProductsForPE(p pkg.Package) fieldCandidateSet {
candidates := newFieldCandidateSet()
meta, ok := p.Metadata.(pkg.PEBinary)
if !ok {
return candidates
}
var product, fileDesc string
for _, kv := range meta.VersionResources {
switch strings.ToLower(kv.Key) {
case "productname":
product = strings.ToLower(kv.Value)
case "filedescription":
fileDesc = strings.ToLower(kv.Value)
}
}
if strings.Contains(product, "ghostscript") || strings.Contains(fileDesc, "ghostscript") {
candidates.addValue("ghostscript")
}
return candidates
}
@@ -225,6 +225,9 @@ func candidateVendors(p pkg.Package) []string {
vendors.union(candidateVendorsForAPK(p))
case pkg.NpmPackage:
vendors.union(candidateVendorsForJavascript(p))
case pkg.PEBinary:
// Add PE-specific vendor hints (e.g. ghostscript -> artifex)
vendors.union(candidateVendorsForPE(p))
case pkg.WordpressPluginEntry:
vendors.clear()
vendors.union(candidateVendorsForWordpressPlugin(p))
@@ -301,6 +304,9 @@ func candidateProductSet(p pkg.Package) fieldCandidateSet {
switch p.Metadata.(type) {
case pkg.ApkDBEntry:
products.union(candidateProductsForAPK(p))
case pkg.PEBinary:
// Add PE-specific product hints (e.g. ghostscript)
products.union(candidateProductsForPE(p))
case pkg.WordpressPluginEntry:
products.clear()
products.union(candidateProductsForWordpressPlugin(p))
@@ -0,0 +1,39 @@
package cpegenerate
import (
"testing"
"github.com/anchore/syft/syft/pkg"
)
func TestGhostscriptPEGeneratesArtifexCPE(t *testing.T) {
// construct a BinaryPkg with PE metadata resembling Ghostscript
p := pkg.Package{
Name: "GPL Ghostscript",
Version: "9.54.0",
Type: pkg.BinaryPkg,
Metadata: pkg.PEBinary{
VersionResources: pkg.KeyValues{
{Key: "CompanyName", Value: "Artifex Software, Inc."},
{Key: "ProductName", Value: "GPL Ghostscript"},
{Key: "FileDescription", Value: "Ghostscript Interpreter"},
},
},
}
cpes := FromPackageAttributes(p)
if len(cpes) == 0 {
t.Fatalf("expected at least one CPE, got none")
}
found := false
for _, c := range cpes {
if c.Attributes.Vendor == "artifex" && c.Attributes.Product == "ghostscript" && c.Attributes.Version == p.Version {
found = true
break
}
}
if !found {
t.Fatalf("expected to find CPE with vendor 'artifex' and product 'ghostscript' for Ghostscript PE binary; got: %+v", cpes)
}
}
@@ -0,0 +1,514 @@
// Package pkgtest provides test helpers for cataloger and parser testing,
// including automatic observation tracking for capability documentation.
package pkgtest
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"sort"
"sync"
"time"
"github.com/anchore/syft/internal/capabilities/pkgtestobservation"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
)
var (
globalTracker *MetadataTracker
globalTrackerOnce sync.Once
// commonPackageIntegrityFields are common field names used to store integrity hashes in package metadata.
// TODO: this is a best-effort list and may need to be expanded as new package types are added. Don't depend on this list to catch everything - it's only for test validation.
commonPackageIntegrityFields = []string{
"Integrity", "Checksum", "H1Digest",
"OutputHash", "PkgHash", "ContentHash",
"PkgHashExt", "Hash", "IntegrityHash",
}
)
// MetadataTracker collects metadata type and package type usage during test execution
type MetadataTracker struct {
mu sync.Mutex
parserData map[string]map[string]map[string]bool // package -> parser -> metadata types (set)
catalogerData map[string]map[string]bool // cataloger -> metadata types (set)
parserPackageTypes map[string]map[string]map[string]bool // package -> parser -> package types (set)
catalogerPackageTypes map[string]map[string]bool // cataloger -> package types (set)
// unified observations for the current test package
observations *pkgtestobservation.Test
}
// getTracker returns the singleton metadata tracker
func getTracker() *MetadataTracker {
globalTrackerOnce.Do(func() {
globalTracker = &MetadataTracker{
parserData: make(map[string]map[string]map[string]bool),
catalogerData: make(map[string]map[string]bool),
parserPackageTypes: make(map[string]map[string]map[string]bool),
catalogerPackageTypes: make(map[string]map[string]bool),
}
})
return globalTracker
}
// RecordParser records a metadata type usage for a parser function
func (t *MetadataTracker) RecordParser(packageName, parserFunction, metadataType string) {
if packageName == "" || parserFunction == "" || metadataType == "" {
return
}
// filter out non-metadata types
if metadataType == "pkg.Package" || metadataType == "" {
return
}
t.mu.Lock()
defer t.mu.Unlock()
if t.parserData[packageName] == nil {
t.parserData[packageName] = make(map[string]map[string]bool)
}
if t.parserData[packageName][parserFunction] == nil {
t.parserData[packageName][parserFunction] = make(map[string]bool)
}
t.parserData[packageName][parserFunction][metadataType] = true
}
// RecordCataloger records a metadata type usage for a cataloger
func (t *MetadataTracker) RecordCataloger(catalogerName, metadataType string) {
if catalogerName == "" || metadataType == "" {
return
}
// filter out non-metadata types
if metadataType == "pkg.Package" || metadataType == "" {
return
}
t.mu.Lock()
defer t.mu.Unlock()
if t.catalogerData[catalogerName] == nil {
t.catalogerData[catalogerName] = make(map[string]bool)
}
t.catalogerData[catalogerName][metadataType] = true
}
// RecordParserPackageType records a package type usage for a parser function
func (t *MetadataTracker) RecordParserPackageType(packageName, parserFunction, pkgType string) {
if packageName == "" || parserFunction == "" || pkgType == "" {
return
}
// filter out unknown types
if pkgType == pkg.UnknownPkg.String() || pkgType == "" {
return
}
t.mu.Lock()
defer t.mu.Unlock()
if t.parserPackageTypes[packageName] == nil {
t.parserPackageTypes[packageName] = make(map[string]map[string]bool)
}
if t.parserPackageTypes[packageName][parserFunction] == nil {
t.parserPackageTypes[packageName][parserFunction] = make(map[string]bool)
}
t.parserPackageTypes[packageName][parserFunction][pkgType] = true
}
// RecordCatalogerPackageType records a package type usage for a cataloger
func (t *MetadataTracker) RecordCatalogerPackageType(catalogerName, pkgType string) {
if catalogerName == "" || pkgType == "" {
return
}
// filter out unknown types
if pkgType == pkg.UnknownPkg.String() || pkgType == "" {
return
}
t.mu.Lock()
defer t.mu.Unlock()
if t.catalogerPackageTypes[catalogerName] == nil {
t.catalogerPackageTypes[catalogerName] = make(map[string]bool)
}
t.catalogerPackageTypes[catalogerName][pkgType] = true
}
// RecordParserPackageMetadata extracts and records metadata type and package type from a package for a parser
func (t *MetadataTracker) RecordParserPackageMetadata(packageName, parserFunction string, p pkg.Package) {
if p.Metadata != nil {
metadataType := getMetadataTypeName(p.Metadata)
if metadataType != "" {
t.RecordParser(packageName, parserFunction, metadataType)
}
}
// record package type
t.RecordParserPackageType(packageName, parserFunction, string(p.Type))
}
// RecordCatalogerPackageMetadata extracts and records metadata type and package type from a package for a cataloger
func (t *MetadataTracker) RecordCatalogerPackageMetadata(catalogerName string, p pkg.Package) {
if p.Metadata != nil {
metadataType := getMetadataTypeName(p.Metadata)
if metadataType != "" {
t.RecordCataloger(catalogerName, metadataType)
}
}
// record package type
t.RecordCatalogerPackageType(catalogerName, string(p.Type))
}
// aggregateObservations aggregates package and relationship observations into metadata types, package types, and observations.
// this is used by both parser and cataloger observation recording.
func aggregateObservations(
metadataTypes *[]string,
packageTypes *[]string,
obs *pkgtestobservation.Observations,
pkgs []pkg.Package,
relationships []artifact.Relationship,
) {
// aggregate observations from packages
for _, p := range pkgs {
// metadata types
if p.Metadata != nil {
metadataType := getMetadataTypeName(p.Metadata)
if metadataType != "" && !contains(*metadataTypes, metadataType) {
*metadataTypes = append(*metadataTypes, metadataType)
}
}
// package types
pkgType := string(p.Type)
if pkgType != "" && pkgType != pkg.UnknownPkg.String() && !contains(*packageTypes, pkgType) {
*packageTypes = append(*packageTypes, pkgType)
}
// license observation
if !p.Licenses.Empty() {
obs.License = true
}
// file listing observation
if fileOwner, ok := p.Metadata.(pkg.FileOwner); ok {
files := fileOwner.OwnedFiles()
if len(files) > 0 {
obs.FileListing.Found = true
obs.FileListing.Count += len(files)
}
}
// file digests observation
if hasFileDigests(p.Metadata) {
obs.FileDigests.Found = true
obs.FileDigests.Count++
}
// integrity hash observation
if hasIntegrityHash(p.Metadata) {
obs.IntegrityHash.Found = true
obs.IntegrityHash.Count++
}
}
// relationship observations
depCount := countDependencyRelationships(relationships)
if depCount > 0 {
obs.Relationships.Found = true
obs.Relationships.Count = depCount
}
// sort arrays for consistency
sort.Strings(*metadataTypes)
sort.Strings(*packageTypes)
}
// ensureObservationsInitialized ensures t.observations is initialized and package name is set.
// must be called with t.mu locked.
func (t *MetadataTracker) ensureObservationsInitialized(packageName string) {
if t.observations == nil {
t.observations = &pkgtestobservation.Test{
Package: packageName,
Catalogers: make(map[string]*pkgtestobservation.Cataloger),
Parsers: make(map[string]*pkgtestobservation.Parser),
}
return
}
// update package name if not set (for the first test) or if it matches (for subsequent tests in same package)
if t.observations.Package == "" || t.observations.Package == packageName {
t.observations.Package = packageName
}
}
// getOrCreateParser gets an existing parser observation or creates a new one.
// must be called with t.mu locked.
func (t *MetadataTracker) getOrCreateParser(parserFunction string) *pkgtestobservation.Parser {
if t.observations.Parsers[parserFunction] == nil {
t.observations.Parsers[parserFunction] = &pkgtestobservation.Parser{
MetadataTypes: []string{},
PackageTypes: []string{},
Observations: pkgtestobservation.Observations{},
}
}
return t.observations.Parsers[parserFunction]
}
// getOrCreateCataloger gets an existing cataloger observation or creates a new one.
// must be called with t.mu locked.
func (t *MetadataTracker) getOrCreateCataloger(catalogerName string) *pkgtestobservation.Cataloger {
if t.observations.Catalogers[catalogerName] == nil {
t.observations.Catalogers[catalogerName] = &pkgtestobservation.Cataloger{
MetadataTypes: []string{},
PackageTypes: []string{},
Observations: pkgtestobservation.Observations{},
}
}
return t.observations.Catalogers[catalogerName]
}
// RecordParserObservations records comprehensive observations for a parser.
func (t *MetadataTracker) RecordParserObservations(
packageName, parserFunction string,
pkgs []pkg.Package,
relationships []artifact.Relationship,
) {
if packageName == "" || parserFunction == "" {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.ensureObservationsInitialized(packageName)
parser := t.getOrCreateParser(parserFunction)
aggregateObservations(&parser.MetadataTypes, &parser.PackageTypes, &parser.Observations, pkgs, relationships)
}
// RecordCatalogerObservations records comprehensive observations for a cataloger.
func (t *MetadataTracker) RecordCatalogerObservations(
packageName, catalogerName string,
pkgs []pkg.Package,
relationships []artifact.Relationship,
) {
if packageName == "" || catalogerName == "" {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.ensureObservationsInitialized(packageName)
cataloger := t.getOrCreateCataloger(catalogerName)
aggregateObservations(&cataloger.MetadataTypes, &cataloger.PackageTypes, &cataloger.Observations, pkgs, relationships)
}
// ===== Metadata Type and Capability Detection =====
// These functions use reflection to inspect package metadata and detect capabilities.
// They are best-effort and may not catch all cases.
// getMetadataTypeName returns the fully qualified type name of metadata (e.g., "pkg.ApkDBEntry").
// extracts just the last package path segment to keep names concise.
func getMetadataTypeName(metadata interface{}) string {
if metadata == nil {
return ""
}
t := reflect.TypeOf(metadata)
if t == nil {
return ""
}
// handle pointers
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
// return pkg path + type name (e.g., "pkg.ApkDBEntry")
if t.PkgPath() != "" {
// extract just "pkg" from "github.com/anchore/syft/syft/pkg"
pkgPath := lastPathSegment(t.PkgPath())
return pkgPath + "." + t.Name()
}
return t.Name()
}
// lastPathSegment extracts the last segment from a package path.
// for example: "github.com/anchore/syft/syft/pkg" -> "pkg"
func lastPathSegment(path string) string {
for i := len(path) - 1; i >= 0; i-- {
if path[i] == '/' {
return path[i+1:]
}
}
return path
}
// hasIntegrityHash checks if metadata contains an integrity hash field.
// note: this uses a best-effort approach checking common field names.
// DO NOT depend on these values in auto-generated capabilities definitions - use for test validation only.
func hasIntegrityHash(metadata interface{}) bool {
v := dereferenceToStruct(metadata)
if !v.IsValid() || v.Kind() != reflect.Struct {
return false
}
for _, fieldName := range commonPackageIntegrityFields {
if hasPopulatedStringField(v, fieldName) {
return true
}
}
return false
}
// hasFileDigests checks if metadata contains file records with digests.
// note: uses a best-effort approach for detection.
// DO NOT depend on these values in auto-generated capabilities definitions - use for test validation only.
func hasFileDigests(metadata interface{}) bool {
v := dereferenceToStruct(metadata)
if !v.IsValid() || v.Kind() != reflect.Struct {
return false
}
filesField := v.FieldByName("Files")
if !filesField.IsValid() || filesField.Kind() != reflect.Slice {
return false
}
// check if any file record has a Digest field populated
for i := 0; i < filesField.Len(); i++ {
if hasPopulatedDigest(filesField.Index(i)) {
return true
}
}
return false
}
// dereferenceToStruct handles pointer dereferencing and returns the underlying value.
// returns an invalid value if the input is nil or not convertible to a struct.
func dereferenceToStruct(v interface{}) reflect.Value {
if v == nil {
return reflect.Value{}
}
val := reflect.ValueOf(v)
if val.Kind() == reflect.Ptr {
if val.IsNil() {
return reflect.Value{}
}
val = val.Elem()
}
return val
}
// hasPopulatedStringField checks if a struct has a non-empty string field with the given name.
func hasPopulatedStringField(v reflect.Value, fieldName string) bool {
field := v.FieldByName(fieldName)
return field.IsValid() && field.Kind() == reflect.String && field.String() != ""
}
// hasPopulatedDigest checks if a file record has a populated Digest field.
func hasPopulatedDigest(fileRecord reflect.Value) bool {
fileRecord = dereferenceToStruct(fileRecord.Interface())
if !fileRecord.IsValid() || fileRecord.Kind() != reflect.Struct {
return false
}
digestField := fileRecord.FieldByName("Digest")
if !digestField.IsValid() {
return false
}
// check if digest is a pointer and not nil, or a non-zero value
switch digestField.Kind() {
case reflect.Ptr:
return !digestField.IsNil()
case reflect.String:
return digestField.String() != ""
case reflect.Struct:
return !digestField.IsZero()
}
return false
}
// ===== Utility Functions =====
// countDependencyRelationships counts the number of dependency relationships.
func countDependencyRelationships(relationships []artifact.Relationship) int {
count := 0
for _, rel := range relationships {
if rel.Type == artifact.DependencyOfRelationship {
count++
}
}
return count
}
// contains checks if a string slice contains a specific string.
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// ===== Result Writing =====
// WriteResults writes the collected observation data to test-fixtures/test-observations.json.
func (t *MetadataTracker) WriteResults() error {
t.mu.Lock()
defer t.mu.Unlock()
if t.observations == nil {
// no data to write
return nil
}
// create output directory
outDir := "test-fixtures"
if err := os.MkdirAll(outDir, 0755); err != nil {
return err
}
// write unified test-observations.json
t.observations.UpdatedAt = time.Now().UTC()
filename := filepath.Join(outDir, "test-observations.json")
return writeJSONFile(filename, t.observations)
}
// writeJSONFile writes data as pretty-printed JSON to the specified path.
func writeJSONFile(path string, data interface{}) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
return encoder.Encode(data)
}
// WriteResultsIfEnabled writes results if tracking is enabled.
// this is typically called via t.Cleanup() in tests.
func WriteResultsIfEnabled() error {
tracker := getTracker()
return tracker.WriteResults()
}
@@ -1,3 +1,5 @@
// Package pkgtest provides test helpers for cataloger and parser testing,
// including resolver decorators that track file access patterns.
package pkgtest
import (
@@ -13,29 +15,36 @@ import (
var _ file.Resolver = (*ObservingResolver)(nil)
// ObservingResolver wraps a file.Resolver to observe and track all file access patterns.
// it records what paths were queried, what was returned, and what file contents were read.
// this is useful for validating that catalogers use appropriate glob patterns and don't over-read files.
type ObservingResolver struct {
decorated file.Resolver
pathQueries map[string][]string
pathResponses []file.Location
contentQueries []file.Location
emptyPathResponses map[string][]string
pathQueries map[string][]string // method name -> list of query patterns
pathResponses []file.Location // all locations successfully returned
contentQueries []file.Location // all locations whose content was read
emptyPathResponses map[string][]string // method name -> paths that returned empty results
}
// NewObservingResolver creates a new ObservingResolver that wraps the given resolver.
func NewObservingResolver(resolver file.Resolver) *ObservingResolver {
return &ObservingResolver{
decorated: resolver,
pathResponses: make([]file.Location, 0),
emptyPathResponses: make(map[string][]string),
pathQueries: make(map[string][]string),
pathResponses: make([]file.Location, 0),
contentQueries: make([]file.Location, 0),
emptyPathResponses: make(map[string][]string),
}
}
// testing helpers...
// ===== Test Assertion Helpers =====
// these methods are used by tests to validate expected file access patterns.
// ObservedPathQuery checks if a specific path pattern was queried.
func (r *ObservingResolver) ObservedPathQuery(input string) bool {
for _, vs := range r.pathQueries {
for _, v := range vs {
if v == input {
for _, queries := range r.pathQueries {
for _, query := range queries {
if query == input {
return true
}
}
@@ -43,6 +52,7 @@ func (r *ObservingResolver) ObservedPathQuery(input string) bool {
return false
}
// ObservedPathResponses checks if a specific path was returned in any response.
func (r *ObservingResolver) ObservedPathResponses(path string) bool {
for _, loc := range r.pathResponses {
if loc.RealPath == path {
@@ -52,6 +62,7 @@ func (r *ObservingResolver) ObservedPathResponses(path string) bool {
return false
}
// ObservedContentQueries checks if a specific file's content was read.
func (r *ObservingResolver) ObservedContentQueries(path string) bool {
for _, loc := range r.contentQueries {
if loc.RealPath == path {
@@ -61,6 +72,7 @@ func (r *ObservingResolver) ObservedContentQueries(path string) bool {
return false
}
// AllContentQueries returns a deduplicated list of all file paths whose content was read.
func (r *ObservingResolver) AllContentQueries() []string {
observed := strset.New()
for _, loc := range r.contentQueries {
@@ -69,155 +81,166 @@ func (r *ObservingResolver) AllContentQueries() []string {
return observed.List()
}
// AllPathQueries returns all path query patterns grouped by method name.
func (r *ObservingResolver) AllPathQueries() map[string][]string {
return r.pathQueries
}
// PruneUnfulfilledPathResponses removes specified paths from the unfulfilled requests tracking.
// ignore maps method names to paths that should be ignored for that method.
// ignorePaths lists paths that should be ignored for all methods.
func (r *ObservingResolver) PruneUnfulfilledPathResponses(ignore map[string][]string, ignorePaths ...string) {
if ignore == nil {
return
}
// remove any paths that were ignored for specific calls
for k, v := range ignore {
results := r.emptyPathResponses[k]
for _, ig := range v {
for i, result := range results {
if result == ig {
results = append(results[:i], results[i+1:]...)
break
}
}
}
if len(results) > 0 {
r.emptyPathResponses[k] = results
} else {
delete(r.emptyPathResponses, k)
// remove paths ignored for specific methods
for methodName, pathsToIgnore := range ignore {
r.emptyPathResponses[methodName] = removeStrings(r.emptyPathResponses[methodName], pathsToIgnore)
if len(r.emptyPathResponses[methodName]) == 0 {
delete(r.emptyPathResponses, methodName)
}
}
// remove any paths that were ignored for all calls
for _, ig := range ignorePaths {
for k, v := range r.emptyPathResponses {
for i, result := range v {
if result == ig {
v = append(v[:i], v[i+1:]...)
break
}
}
if len(v) > 0 {
r.emptyPathResponses[k] = v
} else {
delete(r.emptyPathResponses, k)
// remove paths ignored for all methods
if len(ignorePaths) > 0 {
for methodName := range r.emptyPathResponses {
r.emptyPathResponses[methodName] = removeStrings(r.emptyPathResponses[methodName], ignorePaths)
if len(r.emptyPathResponses[methodName]) == 0 {
delete(r.emptyPathResponses, methodName)
}
}
}
}
// HasUnfulfilledPathRequests returns true if there are any paths that were queried but returned empty.
func (r *ObservingResolver) HasUnfulfilledPathRequests() bool {
return len(r.emptyPathResponses) > 0
}
// PrettyUnfulfilledPathRequests returns a formatted string of all unfulfilled path requests.
func (r *ObservingResolver) PrettyUnfulfilledPathRequests() string {
var res string
var keys []string
if len(r.emptyPathResponses) == 0 {
return ""
}
var keys []string
for k := range r.emptyPathResponses {
keys = append(keys, k)
}
sort.Strings(keys)
var result string
for _, k := range keys {
res += fmt.Sprintf(" %s: %+v\n", k, r.emptyPathResponses[k])
result += fmt.Sprintf(" %s: %+v\n", k, r.emptyPathResponses[k])
}
return res
return result
}
// For the file path resolver...
// removeStrings removes all occurrences of toRemove from slice.
func removeStrings(slice []string, toRemove []string) []string {
if len(toRemove) == 0 {
return slice
}
func (r *ObservingResolver) addPathQuery(name string, input ...string) {
r.pathQueries[name] = append(r.pathQueries[name], input...)
// create a set for O(1) lookup
removeSet := make(map[string]bool)
for _, s := range toRemove {
removeSet[s] = true
}
// filter the slice
result := make([]string, 0, len(slice))
for _, s := range slice {
if !removeSet[s] {
result = append(result, s)
}
}
return result
}
func (r *ObservingResolver) addPathResponse(locs ...file.Location) {
// ===== Internal Tracking Helpers =====
// recordQuery records a path query for a given method.
func (r *ObservingResolver) recordQuery(methodName string, queries ...string) {
r.pathQueries[methodName] = append(r.pathQueries[methodName], queries...)
}
// recordResponses records successful path responses and tracks any unfulfilled queries.
func (r *ObservingResolver) recordResponses(methodName string, locs []file.Location, queriedPaths ...string) {
r.pathResponses = append(r.pathResponses, locs...)
}
func (r *ObservingResolver) addEmptyPathResponse(name string, locs []file.Location, paths ...string) {
if len(locs) == 0 {
results := r.emptyPathResponses[name]
results = append(results, paths...)
r.emptyPathResponses[name] = results
// track paths that returned no results
if len(locs) == 0 && len(queriedPaths) > 0 {
r.emptyPathResponses[methodName] = append(r.emptyPathResponses[methodName], queriedPaths...)
}
}
// ===== file.Resolver Implementation =====
// these methods delegate to the wrapped resolver while recording observations.
// FilesByPath returns files matching the given paths.
func (r *ObservingResolver) FilesByPath(paths ...string) ([]file.Location, error) {
name := "FilesByPath"
r.addPathQuery(name, paths...)
const methodName = "FilesByPath"
r.recordQuery(methodName, paths...)
locs, err := r.decorated.FilesByPath(paths...)
r.recordResponses(methodName, locs, paths...)
r.addPathResponse(locs...)
r.addEmptyPathResponse(name, locs, paths...)
return locs, err
}
// FilesByGlob returns files matching the given glob patterns.
func (r *ObservingResolver) FilesByGlob(patterns ...string) ([]file.Location, error) {
name := "FilesByGlob"
r.addPathQuery(name, patterns...)
const methodName = "FilesByGlob"
r.recordQuery(methodName, patterns...)
locs, err := r.decorated.FilesByGlob(patterns...)
r.recordResponses(methodName, locs, patterns...)
r.addPathResponse(locs...)
r.addEmptyPathResponse(name, locs, patterns...)
return locs, err
}
// FilesByMIMEType returns files matching the given MIME types.
func (r *ObservingResolver) FilesByMIMEType(types ...string) ([]file.Location, error) {
name := "FilesByMIMEType"
r.addPathQuery(name, types...)
const methodName = "FilesByMIMEType"
r.recordQuery(methodName, types...)
locs, err := r.decorated.FilesByMIMEType(types...)
r.recordResponses(methodName, locs, types...)
r.addPathResponse(locs...)
r.addEmptyPathResponse(name, locs, types...)
return locs, err
}
func (r *ObservingResolver) RelativeFileByPath(l file.Location, path string) *file.Location {
name := "RelativeFileByPath"
r.addPathQuery(name, path)
// RelativeFileByPath returns a file at a path relative to the given location.
func (r *ObservingResolver) RelativeFileByPath(location file.Location, path string) *file.Location {
const methodName = "RelativeFileByPath"
r.recordQuery(methodName, path)
loc := r.decorated.RelativeFileByPath(l, path)
loc := r.decorated.RelativeFileByPath(location, path)
if loc != nil {
r.addPathResponse(*loc)
r.pathResponses = append(r.pathResponses, *loc)
} else {
results := r.emptyPathResponses[name]
results = append(results, path)
r.emptyPathResponses[name] = results
r.emptyPathResponses[methodName] = append(r.emptyPathResponses[methodName], path)
}
return loc
}
// For the content resolver methods...
// FileContentsByLocation returns a reader for the contents of the file at the given location.
func (r *ObservingResolver) FileContentsByLocation(location file.Location) (io.ReadCloser, error) {
r.contentQueries = append(r.contentQueries, location)
reader, err := r.decorated.FileContentsByLocation(location)
return reader, err
return r.decorated.FileContentsByLocation(location)
}
// For the remaining resolver methods...
// AllLocations returns all file locations known to the resolver.
func (r *ObservingResolver) AllLocations(ctx context.Context) <-chan file.Location {
return r.decorated.AllLocations(ctx)
}
func (r *ObservingResolver) HasPath(s string) bool {
return r.decorated.HasPath(s)
// HasPath returns true if the resolver knows about the given path.
func (r *ObservingResolver) HasPath(path string) bool {
return r.decorated.HasPath(path)
}
// FileMetadataByLocation returns metadata for the file at the given location.
func (r *ObservingResolver) FileMetadataByLocation(location file.Location) (file.Metadata, error) {
return r.decorated.FileMetadataByLocation(location)
}
@@ -6,6 +6,8 @@ import (
"io"
"os"
"path/filepath"
"reflect"
"runtime"
"sort"
"strings"
"sync"
@@ -56,6 +58,7 @@ type CatalogTester struct {
packageStringer func(pkg.Package) string
customAssertions []func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship)
context context.Context
skipTestObservations bool
}
func Context() context.Context {
@@ -260,13 +263,23 @@ func (p *CatalogTester) IgnoreUnfulfilledPathResponses(paths ...string) *Catalog
return p
}
func (p *CatalogTester) WithoutTestObserver() *CatalogTester {
p.skipTestObservations = true
return p
}
func (p *CatalogTester) TestParser(t *testing.T, parser generic.Parser) {
t.Helper()
pkgs, relationships, err := parser(p.context, p.resolver, p.env, p.reader)
// only test for errors if explicitly requested
if p.wantErr != nil {
p.wantErr(t, err)
}
// track metadata types for cataloger discovery
p.trackParserMetadata(t, parser, pkgs, relationships)
p.assertPkgs(t, pkgs, relationships)
}
@@ -292,6 +305,9 @@ func (p *CatalogTester) TestCataloger(t *testing.T, cataloger pkg.Cataloger) {
p.wantErr(t, err)
}
// track metadata types for cataloger discovery
p.trackCatalogerMetadata(t, cataloger, pkgs, relationships)
if p.assertResultExpectations {
p.assertPkgs(t, pkgs, relationships)
}
@@ -458,3 +474,163 @@ func stringPackage(p pkg.Package) string {
return fmt.Sprintf("%s @ %s (%s)", p.Name, p.Version, loc)
}
// getFunctionName extracts the function name from a function pointer using reflection
func getFunctionName(fn interface{}) string {
// get the function pointer
ptr := reflect.ValueOf(fn).Pointer()
// get the function details
funcForPC := runtime.FuncForPC(ptr)
if funcForPC == nil {
return ""
}
fullName := funcForPC.Name()
// extract just the function name from the full path
// e.g., "github.com/anchore/syft/syft/pkg/cataloger/python.parseRequirementsTxt"
// -> "parseRequirementsTxt"
parts := strings.Split(fullName, ".")
if len(parts) > 0 {
name := parts[len(parts)-1]
// strip the -fm suffix that Go's reflection adds for methods
// e.g., "parsePackageLock-fm" -> "parsePackageLock"
return strings.TrimSuffix(name, "-fm")
}
return fullName
}
// getCatalogerName extracts the cataloger name from the test context or cataloger name
func getCatalogerName(_ *testing.T, cataloger pkg.Cataloger) string {
// use the cataloger's name method if available
return cataloger.Name()
}
// getPackagePath extracts the package path from a function name
// e.g., "github.com/anchore/syft/syft/pkg/cataloger/python.parseRequirementsTxt" -> "python"
func getPackagePath(fn interface{}) string {
ptr := reflect.ValueOf(fn).Pointer()
funcForPC := runtime.FuncForPC(ptr)
if funcForPC == nil {
return ""
}
fullName := funcForPC.Name()
// extract package name from path
// e.g., "github.com/anchore/syft/syft/pkg/cataloger/python.parseRequirementsTxt"
// -> "python"
if strings.Contains(fullName, "/cataloger/") {
parts := strings.Split(fullName, "/cataloger/")
if len(parts) > 1 {
// get the next segment after "/cataloger/"
remaining := parts[1]
// split by "." to get package name
pkgParts := strings.Split(remaining, ".")
if len(pkgParts) > 0 {
return pkgParts[0]
}
}
}
return ""
}
// getPackagePathFromCataloger extracts the package path from the caller's file path
// For generic catalogers, the cataloger type is from the generic package, but we need
// the package where the test is defined (e.g., rust, python, etc.)
func getPackagePathFromCataloger(_ pkg.Cataloger) string {
// walk up the call stack to find the test file
// we're looking for a file in the cataloger directory structure
for i := 0; i < 10; i++ {
_, file, _, ok := runtime.Caller(i)
if !ok {
break
}
// extract package name from file path
// e.g., "/Users/.../syft/pkg/cataloger/rust/cataloger_test.go" -> "rust"
if strings.Contains(file, "/cataloger/") {
parts := strings.Split(file, "/cataloger/")
if len(parts) > 1 {
// get the next segment after "/cataloger/"
remaining := parts[1]
// split by "/" to get package name
pkgParts := strings.Split(remaining, "/")
if len(pkgParts) > 0 && pkgParts[0] != "internal" {
return pkgParts[0]
}
}
}
}
return ""
}
// trackParserMetadata records metadata types for a parser function
func (p *CatalogTester) trackParserMetadata(t *testing.T, parser generic.Parser, pkgs []pkg.Package, relationships []artifact.Relationship) {
if p.skipTestObservations {
return
}
parserName := getFunctionName(parser)
if parserName == "" {
return
}
// try to infer package name from function path
packageName := getPackagePath(parser)
if packageName == "" {
return
}
tracker := getTracker()
// old tracking (still used by metadata discovery)
for _, pkg := range pkgs {
tracker.RecordParserPackageMetadata(packageName, parserName, pkg)
}
// new unified observations with capability tracking
tracker.RecordParserObservations(packageName, parserName, pkgs, relationships)
// ensure results are written when tests complete
t.Cleanup(func() {
_ = WriteResultsIfEnabled()
})
}
// trackCatalogerMetadata records metadata types for a cataloger
func (p *CatalogTester) trackCatalogerMetadata(t *testing.T, cataloger pkg.Cataloger, pkgs []pkg.Package, relationships []artifact.Relationship) {
if p.skipTestObservations {
return
}
catalogerName := getCatalogerName(t, cataloger)
if catalogerName == "" {
return
}
// try to infer package name from cataloger type
packageName := getPackagePathFromCataloger(cataloger)
if packageName == "" {
return
}
tracker := getTracker()
// old tracking (still used by metadata discovery)
for _, pkg := range pkgs {
tracker.RecordCatalogerPackageMetadata(catalogerName, pkg)
}
// new unified observations with capability tracking
tracker.RecordCatalogerObservations(packageName, catalogerName, pkgs, relationships)
// ensure results are written when tests complete
t.Cleanup(func() {
_ = WriteResultsIfEnabled()
})
}
+1 -1
View File
@@ -108,7 +108,7 @@ func (a archiveFilename) extension() string {
func (a archiveFilename) pkgType() pkg.Type {
switch strings.ToLower(a.extension()) {
case "jar", "war", "ear", "lpkg", "par", "sar", "nar", "kar":
case "jar", "war", "ear", "lpkg", "par", "sar", "nar", "kar", "rar":
return pkg.JavaPkg
case "jpi", "hpi":
return pkg.JenkinsPluginPkg
@@ -187,6 +187,13 @@ func TestExtractInfoFromJavaArchiveFilename(t *testing.T) {
name: "gradle-build-cache",
ty: pkg.JavaPkg,
},
{
filename: "pkg-extra-field-maven-4.3.2-rc1.rar",
version: "4.3.2-rc1",
extension: "rar",
name: "pkg-extra-field-maven",
ty: pkg.JavaPkg,
},
}
for _, test := range tests {
@@ -37,6 +37,7 @@ var archiveFormatGlobs = []string{
"**/*.jpi",
"**/*.hpi",
"**/*.kar",
"**/*.far",
"**/*.lpkg", // Zip-compressed package used to deploy applications
// (aka plugins) to Liferay Portal server. Those files contains .JAR(s) and a .PROPERTIES file, the latter
// has information about the application and installation requirements.
@@ -45,6 +46,7 @@ var archiveFormatGlobs = []string{
// LifeRay makes it pretty cumbersome to make a such plugins; their docs are
// out of date, and they charge for their IDE. If you find an example
// project that we can build in CI feel free to include it
"**/*.rar", // Java Resource Adapter Archive
}
// javaArchiveHashes are all the current hash algorithms used to calculate archive digests
@@ -29,7 +29,9 @@ func Test_ArchiveCataloger_Globs(t *testing.T) {
"java-archives/example.kar",
"java-archives/example.jpi",
"java-archives/example.hpi",
"java-archives/example.far",
"java-archives/example.lpkg",
"java-archives/example.rar",
"archives/example.zip",
"archives/example.tar",
"archives/example.tar.gz",
@@ -0,0 +1 @@
example archive
@@ -0,0 +1 @@
example archive
+2 -1
View File
@@ -18,8 +18,9 @@ func NewPackageCataloger() pkg.Cataloger {
func NewLockCataloger(cfg CatalogerConfig) pkg.Cataloger {
yarnLockAdapter := newGenericYarnLockAdapter(cfg)
packageLockAdapter := newGenericPackageLockAdapter(cfg)
pnpmLockAdapter := newGenericPnpmLockAdapter(cfg)
return generic.NewCataloger("javascript-lock-cataloger").
WithParserByGlobs(packageLockAdapter.parsePackageLock, "**/package-lock.json").
WithParserByGlobs(yarnLockAdapter.parseYarnLock, "**/yarn.lock").
WithParserByGlobs(parsePnpmLock, "**/pnpm-lock.yaml")
WithParserByGlobs(pnpmLockAdapter.parsePnpmLock, "**/pnpm-lock.yaml")
}
+16 -3
View File
@@ -107,7 +107,7 @@ func newPackageLockV1Package(ctx context.Context, cfg CatalogerConfig, resolver
licenseSet = pkg.NewLicenseSet(licenses...)
}
if err != nil {
log.Debugf("unable to extract licenses from javascript yarn.lock for package %s:%s: %+v", name, version, err)
log.Debugf("unable to extract licenses from javascript package-lock.json for package %s:%s: %+v", name, version, err)
}
}
@@ -140,7 +140,7 @@ func newPackageLockV2Package(ctx context.Context, cfg CatalogerConfig, resolver
licenseSet = pkg.NewLicenseSet(licenses...)
}
if err != nil {
log.Debugf("unable to extract licenses from javascript yarn.lock for package %s:%s: %+v", name, u.Version, err)
log.Debugf("unable to extract licenses from javascript package-lock.json for package %s:%s: %+v", name, u.Version, err)
}
}
@@ -161,7 +161,19 @@ func newPackageLockV2Package(ctx context.Context, cfg CatalogerConfig, resolver
)
}
func newPnpmPackage(ctx context.Context, resolver file.Resolver, location file.Location, name, version string) pkg.Package {
func newPnpmPackage(ctx context.Context, cfg CatalogerConfig, resolver file.Resolver, location file.Location, name, version string) pkg.Package {
var licenseSet pkg.LicenseSet
if cfg.SearchRemoteLicenses {
license, err := getLicenseFromNpmRegistry(cfg.NPMBaseURL, name, version)
if err == nil && license != "" {
licenses := pkg.NewLicensesFromValuesWithContext(ctx, license)
licenseSet = pkg.NewLicenseSet(licenses...)
}
if err != nil {
log.Debugf("unable to extract licenses from javascript pnpm-lock.yaml for package %s:%s: %+v", name, version, err)
}
}
return finalizeLockPkg(
ctx,
resolver,
@@ -169,6 +181,7 @@ func newPnpmPackage(ctx context.Context, resolver file.Resolver, location file.L
pkg.Package{
Name: name,
Version: version,
Licenses: licenseSet,
Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
PURL: packageURL(name, version),
Language: pkg.JavaScript,
@@ -18,9 +18,6 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
// integrity check
var _ generic.Parser = parsePnpmLock
// pnpmPackage holds the raw name and version extracted from the lockfile.
type pnpmPackage struct {
Name string
@@ -45,6 +42,16 @@ type pnpmV9LockYaml struct {
Packages map[string]interface{} `yaml:"packages"`
}
type genericPnpmLockAdapter struct {
cfg CatalogerConfig
}
func newGenericPnpmLockAdapter(cfg CatalogerConfig) genericPnpmLockAdapter {
return genericPnpmLockAdapter{
cfg: cfg,
}
}
// Parse implements the pnpmLockfileParser interface for v6-v8 lockfiles.
func (p *pnpmV6LockYaml) Parse(version float64, data []byte) ([]pnpmPackage, error) {
if err := yaml.Unmarshal(data, p); err != nil {
@@ -116,7 +123,7 @@ func newPnpmLockfileParser(version float64) pnpmLockfileParser {
}
// parsePnpmLock is the main parser function for pnpm-lock.yaml files.
func parsePnpmLock(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
func (a genericPnpmLockAdapter) parsePnpmLock(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
data, err := io.ReadAll(reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to load pnpm-lock.yaml file: %w", err)
@@ -142,7 +149,7 @@ func parsePnpmLock(ctx context.Context, resolver file.Resolver, _ *generic.Envir
packages := make([]pkg.Package, len(pnpmPkgs))
for i, p := range pnpmPkgs {
packages[i] = newPnpmPackage(ctx, resolver, reader.Location, p.Name, p.Version)
packages[i] = newPnpmPackage(ctx, a.cfg, resolver, reader.Location, p.Name, p.Version)
}
return packages, nil, unknown.IfEmptyf(packages, "unable to determine packages")
@@ -1,6 +1,11 @@
package javascript
import (
"context"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/anchore/syft/syft/artifact"
@@ -50,7 +55,8 @@ func TestParsePnpmLock(t *testing.T) {
},
}
pkgtest.TestFileParser(t, fixture, parsePnpmLock, expectedPkgs, expectedRelationships)
adapter := newGenericPnpmLockAdapter(CatalogerConfig{})
pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expectedPkgs, expectedRelationships)
}
func TestParsePnpmV6Lock(t *testing.T) {
@@ -142,7 +148,8 @@ func TestParsePnpmV6Lock(t *testing.T) {
},
}
pkgtest.TestFileParser(t, fixture, parsePnpmLock, expectedPkgs, expectedRelationships)
adapter := newGenericPnpmLockAdapter(CatalogerConfig{})
pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expectedPkgs, expectedRelationships)
}
func TestParsePnpmLockV9(t *testing.T) {
@@ -184,14 +191,101 @@ func TestParsePnpmLockV9(t *testing.T) {
Type: pkg.NpmPkg,
},
}
adapter := newGenericPnpmLockAdapter(CatalogerConfig{})
// TODO: no relationships are under test
pkgtest.TestFileParser(t, fixture, parsePnpmLock, expected, expectedRelationships)
pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expected, expectedRelationships)
}
func TestSearchPnpmForLicenses(t *testing.T) {
ctx := context.TODO()
fixture := "test-fixtures/pnpm-remote/pnpm-lock.yaml"
locations := file.NewLocationSet(file.NewLocation(fixture))
mux, url, teardown := setupNpmRegistry()
defer teardown()
tests := []struct {
name string
fixture string
config CatalogerConfig
requestHandlers []handlerPath
expectedPackages []pkg.Package
}{
{
name: "search remote licenses returns the expected licenses when search is set to true",
config: CatalogerConfig{SearchRemoteLicenses: true},
requestHandlers: []handlerPath{
{
// https://registry.npmjs.org/nanoid/3.3.4
path: "/nanoid/3.3.4",
handler: generateMockNpmRegistryHandler("test-fixtures/pnpm-remote/registry_response.json"),
},
},
expectedPackages: []pkg.Package{
{
Name: "nanoid",
Version: "3.3.4",
Locations: locations,
PURL: "pkg:npm/nanoid@3.3.4",
Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "MIT")),
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// set up the mock server
for _, handler := range tc.requestHandlers {
mux.HandleFunc(handler.path, handler.handler)
}
tc.config.NPMBaseURL = url
adapter := newGenericPnpmLockAdapter(tc.config)
pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, tc.expectedPackages, nil)
})
}
}
func Test_corruptPnpmLock(t *testing.T) {
adapter := newGenericPnpmLockAdapter(CatalogerConfig{})
pkgtest.NewCatalogTester().
FromFile(t, "test-fixtures/corrupt/pnpm-lock.yaml").
WithError().
TestParser(t, parsePnpmLock)
TestParser(t, adapter.parsePnpmLock)
}
func generateMockNpmRegistryHandler(responseFixture string) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
// Copy the file's content to the response writer
file, err := os.Open(responseFixture)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
_, err = io.Copy(w, file)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
// setup sets up a test HTTP server for mocking requests to a particular registry.
// The returned url is injected into the Config so the client uses the test server.
// Tests should register handlers on mux to simulate the expected request/response structure
func setupNpmRegistry() (mux *http.ServeMux, serverURL string, teardown func()) {
// mux is the HTTP request multiplexer used with the test server.
mux = http.NewServeMux()
// We want to ensure that tests catch mistakes where the endpoint URL is
// specified as absolute rather than relative. It only makes a difference
// when there's a non-empty base URL path. So, use that. See issue #752.
apiHandler := http.NewServeMux()
apiHandler.Handle("/", mux)
// server is a test HTTP server used to provide mock API responses.
server := httptest.NewServer(apiHandler)
return mux, server.URL, server.Close
}
@@ -239,7 +239,7 @@ func TestSearchYarnForLicenses(t *testing.T) {
ctx := context.TODO()
fixture := "test-fixtures/yarn-remote/yarn.lock"
locations := file.NewLocationSet(file.NewLocation(fixture))
mux, url, teardown := setup()
mux, url, teardown := setupYarnRegistry()
defer teardown()
tests := []struct {
name string
@@ -255,7 +255,7 @@ func TestSearchYarnForLicenses(t *testing.T) {
{
// https://registry.yarnpkg.com/@babel/code-frame/7.10.4
path: "/@babel/code-frame/7.10.4",
handler: generateMockNPMHandler("test-fixtures/yarn-remote/registry_response.json"),
handler: generateMockYarnRegistryHandler("test-fixtures/yarn-remote/registry_response.json"),
},
},
expectedPackages: []pkg.Package{
@@ -284,7 +284,11 @@ func TestSearchYarnForLicenses(t *testing.T) {
}
tc.config.NPMBaseURL = url
adapter := newGenericYarnLockAdapter(tc.config)
pkgtest.TestFileParser(t, fixture, adapter.parseYarnLock, tc.expectedPackages, nil)
pkgtest.NewCatalogTester().
FromFile(t, fixture).
Expects(tc.expectedPackages, nil).
WithoutTestObserver(). // this is an online test, thus not the default configuration
TestParser(t, adapter.parseYarnLock)
})
}
}
@@ -445,7 +449,7 @@ func TestParseYarnFindPackageVersions(t *testing.T) {
}
}
func generateMockNPMHandler(responseFixture string) func(w http.ResponseWriter, r *http.Request) {
func generateMockYarnRegistryHandler(responseFixture string) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
// Copy the file's content to the response writer
@@ -464,10 +468,10 @@ func generateMockNPMHandler(responseFixture string) func(w http.ResponseWriter,
}
}
// setup sets up a test HTTP server for mocking requests to maven central.
// setup sets up a test HTTP server for mocking requests to a particular registry.
// The returned url is injected into the Config so the client uses the test server.
// Tests should register handlers on mux to simulate the expected request/response structure
func setup() (mux *http.ServeMux, serverURL string, teardown func()) {
func setupYarnRegistry() (mux *http.ServeMux, serverURL string, teardown func()) {
// mux is the HTTP request multiplexer used with the test server.
mux = http.NewServeMux()
@@ -0,0 +1,11 @@
lockfileVersion: 5.4
specifiers:
nanoid: ^3.3.4
dependencies:
nanoid: 3.3.4
packages:
/nanoid/3.3.4:
resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
@@ -0,0 +1,106 @@
{
"name": "nanoid",
"version": "3.3.4",
"keywords": [
"uuid",
"random",
"id",
"url"
],
"author": {
"name": "Andrey Sitnik",
"email": "andrey@sitnik.ru"
},
"license": "MIT",
"_id": "nanoid@3.3.4",
"maintainers": [
{
"name": "ai",
"email": "andrey@sitnik.ru"
}
],
"homepage": "https://github.com/ai/nanoid#readme",
"bugs": {
"url": "https://github.com/ai/nanoid/issues"
},
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"dist": {
"shasum": "730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab",
"tarball": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"fileCount": 24,
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"signatures": [
{
"sig": "MEQCIEXG2ta5bIaT6snvQFKV+m1KjuF4DaCpp186tcPo8vsRAiB2Eg9/6nKRi4lZOfwQC1fgq4EzrFjU8T+uqwGxWEQE8A==",
"keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"
}
],
"unpackedSize": 21583,
"npm-signature": "-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJicQqNACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2Vmp6rw/+IRvv2zOtwi8goF3h1VctIQVWtTtYrobDIVC2W++jyxdbgZoP\r\n2CDj1YWjrr+eM6O6sI1Bj+bF+yoqQ+z8ojtfW3vtRPpjzUf/7Sgs4F2ANshp\r\ne3rqdaQLjpHPriHf6HmPJy3YNJ+7n5TPPGoTEGXAe4eCZdko3XidCMWZdHlf\r\nYQU9CVYiG6mjjORkWw1sYctt8exdcGFMh0QoQq7BEp04QWm04JwvHjUiAgvf\r\nmEQLrNrf9nwzjpnubAJD+1z6fKOc9vUE44MOj2PkPoOr6a+iBBBgwBf45cnj\r\ng8R2G5xzxsRRB0a8XZdp67y3WA8rIaYaUuBFtEWYp7QFoA/tp6AGmHEAhjLa\r\nQKTquG7ejBu21ZsQaxpGc/3WWLEm+7F78GF8CXpQdtg0Kg1eugRotSNnU0SO\r\nPLiyYV4Mw6kXnbVchS5Y+HmcDVEcSBMTve/f1KpmIhJueJ20RCg4MGYZWgI9\r\nNJ1KgH2h4djX4XuoXpcsKnX3oVfinHEMke8sLWXHsMAtOxDipEWgW9cE9hk0\r\n71Y6LAAPBu34pmaj73B0qZiIY7wXxoGWQOCl2STS/VyDG/K9w1T+WiYROu+8\r\nE9Gd+f4qXmdi7Jw6May86DDfauCwBP3gnrB5aeOktCjWsgrrdClN3Hv2pIAN\r\noJcjS3IURf6oeV4+Yw1B5GoJu1Y/6U75fOU=\r\n=IMnM\r\n-----END PGP SIGNATURE-----\r\n"
},
"main": "index.cjs",
"type": "module",
"types": "./index.d.ts",
"module": "index.js",
"browser": {
"./index.js": "./index.browser.js",
"./index.cjs": "./index.browser.cjs",
"./async/index.js": "./async/index.browser.js",
"./async/index.cjs": "./async/index.browser.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
},
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.js",
"browser": "./index.browser.js",
"default": "./index.js",
"require": "./index.cjs"
},
"./async": {
"import": "./async/index.js",
"browser": "./async/index.browser.js",
"default": "./async/index.js",
"require": "./async/index.cjs"
},
"./index.d.ts": "./index.d.ts",
"./non-secure": {
"import": "./non-secure/index.js",
"default": "./non-secure/index.js",
"require": "./non-secure/index.cjs"
},
"./package.json": "./package.json",
"./url-alphabet": {
"import": "./url-alphabet/index.js",
"default": "./url-alphabet/index.js",
"require": "./url-alphabet/index.cjs"
},
"./async/package.json": "./async/package.json",
"./non-secure/package.json": "./non-secure/package.json",
"./url-alphabet/package.json": "./url-alphabet/package.json"
},
"gitHead": "fc5bd0dbba830b1e6f3e572da8e2bc9ddc1b4b44",
"_npmUser": {
"name": "ai",
"email": "andrey@sitnik.ru"
},
"repository": {
"url": "git+https://github.com/ai/nanoid.git",
"type": "git"
},
"_npmVersion": "8.6.0",
"description": "A tiny (116 bytes), secure URL-friendly unique string ID generator",
"directories": {},
"sideEffects": false,
"_nodeVersion": "18.0.0",
"react-native": "index.js",
"_hasShrinkwrap": false,
"_npmOperationalInternal": {
"tmp": "tmp/nanoid_3.3.4_1651575437375_0.2288595018362154",
"host": "s3://npm-registry-packages"
}
}
+2 -1
View File
@@ -30,7 +30,8 @@ func NewPackageCataloger(cfg CatalogerConfig) pkg.Cataloger {
WithParserByGlobs(parsePoetryLock, "**/poetry.lock").
WithParserByGlobs(parsePipfileLock, "**/Pipfile.lock").
WithParserByGlobs(parseSetup, "**/setup.py").
WithParserByGlobs(parseUvLock, "**/uv.lock")
WithParserByGlobs(parseUvLock, "**/uv.lock").
WithParserByGlobs(parsePdmLock, "**/pdm.lock")
}
// NewInstalledPackageCataloger returns a new cataloger for python packages within egg or wheel installation directories.
@@ -454,6 +454,7 @@ func Test_IndexCataloger_Globs(t *testing.T) {
"src/poetry.lock",
"src/Pipfile.lock",
"src/uv.lock",
"src/pdm.lock",
},
},
}
+140
View File
@@ -0,0 +1,140 @@
package python
import (
"context"
"fmt"
"strings"
"github.com/BurntSushi/toml"
"github.com/scylladb/go-set/strset"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
type pdmLock struct {
Metadata struct {
Groups []string `toml:"groups"`
Strategy []string `toml:"strategy"`
LockVersion string `toml:"lock_version"`
ContentHash string `toml:"content_hash"`
} `toml:"metadata"`
Package []pdmLockPackage `toml:"package"`
}
type pdmLockPackage struct {
Name string `toml:"name"`
Version string `toml:"version"`
RequiresPython string `toml:"requires_python"`
Summary string `toml:"summary"`
Dependencies []string `toml:"dependencies"`
Files []pdmLockPackageFile `toml:"files"`
}
type pdmLockPackageFile struct {
File string `toml:"file"`
Hash string `toml:"hash"`
}
var _ generic.Parser = parsePdmLock
// parsePdmLock is a parser function for pdm.lock contents, returning python packages discovered.
func parsePdmLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
var lock pdmLock
_, err := toml.NewDecoder(reader).Decode(&lock)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse pdm.lock file: %w", err)
}
var pkgs []pkg.Package
for _, p := range lock.Package {
var files []pkg.PythonFileRecord
for _, f := range p.Files {
if colonIndex := strings.Index(f.Hash, ":"); colonIndex != -1 {
algorithm := f.Hash[:colonIndex]
value := f.Hash[colonIndex+1:]
files = append(files, pkg.PythonFileRecord{
Path: f.File,
Digest: &pkg.PythonFileDigest{
Algorithm: algorithm,
Value: value,
},
})
}
}
// only store used part of the dependency information
var deps []string
for _, dep := range p.Dependencies {
// remove environment markers (after semicolon)
dep = strings.Split(dep, ";")[0]
dep = strings.TrimSpace(dep)
if dep != "" {
deps = append(deps, dep)
}
}
pythonPkgMetadata := pkg.PythonPdmLockEntry{
Files: files,
Summary: p.Summary,
Dependencies: deps,
}
pkgs = append(pkgs, newPackageForIndexWithMetadata(
p.Name,
p.Version,
pythonPkgMetadata,
reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
))
}
relationships := buildPdmRelationships(pkgs)
return pkgs, relationships, unknown.IfEmptyf(pkgs, "unable to determine packages")
}
func buildPdmRelationships(pkgs []pkg.Package) []artifact.Relationship {
pkgMap := make(map[string]pkg.Package, len(pkgs))
for _, p := range pkgs {
pkgMap[p.Name] = p
}
var relationships []artifact.Relationship
for _, p := range pkgs {
meta, ok := p.Metadata.(pkg.PythonPdmLockEntry)
if !ok {
continue
}
// collect unique dependencies
added := strset.New()
for _, depName := range meta.Dependencies {
// Handle version specifiers
depName = strings.Split(depName, "<")[0]
depName = strings.Split(depName, ">")[0]
depName = strings.Split(depName, "=")[0]
depName = strings.Split(depName, "~")[0]
depName = strings.TrimSpace(depName)
if depName == "" || added.Has(depName) {
continue
}
added.Add(depName)
if dep, exists := pkgMap[depName]; exists {
relationships = append(relationships, artifact.Relationship{
From: dep,
To: p,
Type: artifact.DependencyOfRelationship,
})
}
}
}
return relationships
}
@@ -0,0 +1,363 @@
package python
import (
"testing"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func TestParsePdmLock(t *testing.T) {
fixture := "test-fixtures/pdm-lock/pdm.lock"
locations := file.NewLocationSet(file.NewLocation(fixture))
expectedPkgs := []pkg.Package{
{
Name: "certifi",
Version: "2025.1.31",
PURL: "pkg:pypi/certifi@2025.1.31",
Locations: locations,
Language: pkg.Python,
Type: pkg.PythonPkg,
Metadata: pkg.PythonPdmLockEntry{
Summary: "Python package for providing Mozilla's CA Bundle.",
Files: []pkg.PythonFileRecord{
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651",
},
},
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe",
},
},
},
},
},
{
Name: "chardet",
Version: "3.0.4",
PURL: "pkg:pypi/chardet@3.0.4",
Locations: locations,
Language: pkg.Python,
Type: pkg.PythonPkg,
Metadata: pkg.PythonPdmLockEntry{
Summary: "Universal encoding detector for Python 2 and 3",
Files: []pkg.PythonFileRecord{
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691",
},
},
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
},
},
},
},
},
{
Name: "charset-normalizer",
Version: "2.0.12",
PURL: "pkg:pypi/charset-normalizer@2.0.12",
Locations: locations,
Language: pkg.Python,
Type: pkg.PythonPkg,
Metadata: pkg.PythonPdmLockEntry{
Summary: "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet.",
Files: []pkg.PythonFileRecord{
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df",
},
},
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
},
},
},
},
},
{
Name: "colorama",
Version: "0.3.9",
PURL: "pkg:pypi/colorama@0.3.9",
Locations: locations,
Language: pkg.Python,
Type: pkg.PythonPkg,
Metadata: pkg.PythonPdmLockEntry{
Summary: "Cross-platform colored terminal text.",
Files: []pkg.PythonFileRecord{
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
},
},
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1",
},
},
},
},
},
{
Name: "idna",
Version: "2.7",
PURL: "pkg:pypi/idna@2.7",
Locations: locations,
Language: pkg.Python,
Type: pkg.PythonPkg,
Metadata: pkg.PythonPdmLockEntry{
Summary: "Internationalized Domain Names in Applications (IDNA)",
Files: []pkg.PythonFileRecord{
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
},
},
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16",
},
},
},
},
},
{
Name: "py",
Version: "1.4.34",
PURL: "pkg:pypi/py@1.4.34",
Locations: locations,
Language: pkg.Python,
Type: pkg.PythonPkg,
Metadata: pkg.PythonPdmLockEntry{
Summary: "library with cross-python path, ini-parsing, io, code, log facilities",
Files: []pkg.PythonFileRecord{
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "2ccb79b01769d99115aa600d7eed99f524bf752bba8f041dc1c184853514655a",
},
},
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "0f2d585d22050e90c7d293b6451c83db097df77871974d90efd5a30dc12fcde3",
},
},
},
},
},
{
Name: "pytest",
Version: "3.2.5",
PURL: "pkg:pypi/pytest@3.2.5",
Locations: locations,
Language: pkg.Python,
Type: pkg.PythonPkg,
Metadata: pkg.PythonPdmLockEntry{
Summary: "pytest: simple powerful testing with Python",
Files: []pkg.PythonFileRecord{
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "6d5bd4f7113b444c55a3bbb5c738a3dd80d43563d063fc42dcb0aaefbdd78b81",
},
},
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "241d7e7798d79192a123ceaf64c602b4d233eacf6d6e42ae27caa97f498b7dc6",
},
},
},
Dependencies: []string{
"argparse",
"colorama",
"ordereddict",
"py>=1.4.33",
"setuptools",
},
},
},
{
Name: "requests",
Version: "2.27.1",
PURL: "pkg:pypi/requests@2.27.1",
Locations: locations,
Language: pkg.Python,
Type: pkg.PythonPkg,
Metadata: pkg.PythonPdmLockEntry{
Summary: "Python HTTP for Humans.",
Files: []pkg.PythonFileRecord{
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d",
},
},
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61",
},
},
},
Dependencies: []string{
"certifi>=2017.4.17",
"chardet<5,>=3.0.2",
"charset-normalizer~=2.0.0",
"idna<3,>=2.5",
"idna<4,>=2.5",
"urllib3<1.27,>=1.21.1",
},
},
},
{
Name: "setuptools",
Version: "39.2.0",
PURL: "pkg:pypi/setuptools@39.2.0",
Locations: locations,
Language: pkg.Python,
Type: pkg.PythonPkg,
Metadata: pkg.PythonPdmLockEntry{
Summary: "Easily download, build, install, upgrade, and uninstall Python packages",
Files: []pkg.PythonFileRecord{
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2",
},
},
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926",
},
},
},
},
},
{
Name: "urllib3",
Version: "1.26.20",
PURL: "pkg:pypi/urllib3@1.26.20",
Locations: locations,
Language: pkg.Python,
Type: pkg.PythonPkg,
Metadata: pkg.PythonPdmLockEntry{
Summary: "HTTP library with thread-safe connection pooling, file post, and more.",
Files: []pkg.PythonFileRecord{
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e",
},
},
{
Path: "",
Digest: &pkg.PythonFileDigest{
Algorithm: "sha256",
Value: "40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32",
},
},
},
},
},
}
// Create a map for easy lookup of packages by name
pkgMap := make(map[string]pkg.Package)
for _, p := range expectedPkgs {
pkgMap[p.Name] = p
}
expectedRelationships := []artifact.Relationship{
// pytest dependencies
{
From: pkgMap["colorama"],
To: pkgMap["pytest"],
Type: artifact.DependencyOfRelationship,
},
{
From: pkgMap["py"],
To: pkgMap["pytest"],
Type: artifact.DependencyOfRelationship,
},
{
From: pkgMap["setuptools"],
To: pkgMap["pytest"],
Type: artifact.DependencyOfRelationship,
},
// requests dependencies
{
From: pkgMap["certifi"],
To: pkgMap["requests"],
Type: artifact.DependencyOfRelationship,
},
{
From: pkgMap["chardet"],
To: pkgMap["requests"],
Type: artifact.DependencyOfRelationship,
},
{
From: pkgMap["charset-normalizer"],
To: pkgMap["requests"],
Type: artifact.DependencyOfRelationship,
},
{
From: pkgMap["urllib3"],
To: pkgMap["requests"],
Type: artifact.DependencyOfRelationship,
},
{
From: pkgMap["idna"],
To: pkgMap["requests"],
Type: artifact.DependencyOfRelationship,
},
}
pkgtest.TestFileParser(t, fixture, parsePdmLock, expectedPkgs, expectedRelationships)
}
func Test_corruptPdmLock(t *testing.T) {
pkgtest.NewCatalogTester().
FromFile(t, "test-fixtures/glob-paths/src/pdm.lock").
WithError().
TestParser(t, parsePdmLock)
}
@@ -0,0 +1 @@
bogus
+137
View File
@@ -0,0 +1,137 @@
# This file is @generated by PDM.
# It is not intended for manual editing.
[metadata]
groups = ["default", "security", "tests"]
strategy = ["inherit_metadata", "static_urls"]
lock_version = "4.5.0"
content_hash = "sha256:2584886ac58a0ae70aa36bc0318b62c3e2c89acc9c21ebb9aee74147c0a9dc06"
[[metadata.targets]]
requires_python = ">=3.3"
[[package]]
name = "certifi"
version = "2025.1.31"
requires_python = ">=3.6"
summary = "Python package for providing Mozilla's CA Bundle."
groups = ["security"]
marker = "python_version >= \"3.6\""
files = [
{url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
{url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
]
[[package]]
name = "chardet"
version = "3.0.4"
summary = "Universal encoding detector for Python 2 and 3"
groups = ["default"]
marker = "os_name == \"nt\""
files = [
{url = "https://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec7510b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
{url = "https://files.pythonhosted.org/packages/fc/bb/a5768c230f9ddb03acc9ef3f0d4a3cf93462473795d18e9535498c8f929d/chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
]
[[package]]
name = "charset-normalizer"
version = "2.0.12"
requires_python = ">=3.5.0"
summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
groups = ["security"]
marker = "python_version >= \"3.6\""
files = [
{url = "https://files.pythonhosted.org/packages/06/b3/24afc8868eba069a7f03650ac750a778862dc34941a4bebeb58706715726/charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
{url = "https://files.pythonhosted.org/packages/56/31/7bcaf657fafb3c6db8c787a865434290b726653c912085fbd371e9b92e1c/charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
]
[[package]]
name = "colorama"
version = "0.3.9"
summary = "Cross-platform colored terminal text."
groups = ["tests"]
marker = "sys_platform == \"win32\""
files = [
{url = "https://files.pythonhosted.org/packages/db/c8/7dcf9dbcb22429512708fe3a547f8b6101c0d02137acbd892505aee57adf/colorama-0.3.9-py2.py3-none-any.whl", hash = "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda"},
{url = "https://files.pythonhosted.org/packages/e6/76/257b53926889e2835355d74fec73d82662100135293e17d382e2b74d1669/colorama-0.3.9.tar.gz", hash = "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"},
]
[[package]]
name = "idna"
version = "2.7"
summary = "Internationalized Domain Names in Applications (IDNA)"
groups = ["default", "security"]
files = [
{url = "https://files.pythonhosted.org/packages/4b/2a/0276479a4b3caeb8a8c1af2f8e4355746a97fab05a372e4a2c6a6b876165/idna-2.7-py2.py3-none-any.whl", hash = "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e"},
{url = "https://files.pythonhosted.org/packages/65/c4/80f97e9c9628f3cac9b98bfca0402ede54e0563b56482e3e6e45c43c4935/idna-2.7.tar.gz", hash = "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"},
]
[[package]]
name = "py"
version = "1.4.34"
summary = "library with cross-python path, ini-parsing, io, code, log facilities"
groups = ["tests"]
files = [
{url = "https://files.pythonhosted.org/packages/53/67/9620edf7803ab867b175e4fd23c7b8bd8eba11cb761514dcd2e726ef07da/py-1.4.34-py2.py3-none-any.whl", hash = "sha256:2ccb79b01769d99115aa600d7eed99f524bf752bba8f041dc1c184853514655a"},
{url = "https://files.pythonhosted.org/packages/68/35/58572278f1c097b403879c1e9369069633d1cbad5239b9057944bb764782/py-1.4.34.tar.gz", hash = "sha256:0f2d585d22050e90c7d293b6451c83db097df77871974d90efd5a30dc12fcde3"},
]
[[package]]
name = "pytest"
version = "3.2.5"
summary = "pytest: simple powerful testing with Python"
groups = ["tests"]
dependencies = [
"argparse; python_version == \"2.6\"",
"colorama; sys_platform == \"win32\"",
"ordereddict; python_version == \"2.6\"",
"py>=1.4.33",
"setuptools",
]
files = [
{url = "https://files.pythonhosted.org/packages/1f/f8/8cd74c16952163ce0db0bd95fdd8810cbf093c08be00e6e665ebf0dc3138/pytest-3.2.5.tar.gz", hash = "sha256:6d5bd4f7113b444c55a3bbb5c738a3dd80d43563d063fc42dcb0aaefbdd78b81"},
{url = "https://files.pythonhosted.org/packages/ef/41/d8a61f1b2ba308e96b36106e95024977e30129355fd12087f23e4b9852a1/pytest-3.2.5-py2.py3-none-any.whl", hash = "sha256:241d7e7798d79192a123ceaf64c602b4d233eacf6d6e42ae27caa97f498b7dc6"},
]
[[package]]
name = "requests"
version = "2.27.1"
requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
summary = "Python HTTP for Humans."
groups = ["security"]
marker = "python_version >= \"3.6\""
dependencies = [
"certifi>=2017.4.17",
"chardet<5,>=3.0.2; python_version < \"3\"",
"charset-normalizer~=2.0.0; python_version >= \"3\"",
"idna<3,>=2.5; python_version < \"3\"",
"idna<4,>=2.5; python_version >= \"3\"",
"urllib3<1.27,>=1.21.1",
]
files = [
{url = "https://files.pythonhosted.org/packages/2d/61/08076519c80041bc0ffa1a8af0cbd3bf3e2b62af10435d269a9d0f40564d/requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
{url = "https://files.pythonhosted.org/packages/60/f3/26ff3767f099b73e0efa138a9998da67890793bfa475d8278f84a30fec77/requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
]
[[package]]
name = "setuptools"
version = "39.2.0"
requires_python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*"
summary = "Easily download, build, install, upgrade, and uninstall Python packages"
groups = ["tests"]
files = [
{url = "https://files.pythonhosted.org/packages/1a/04/d6f1159feaccdfc508517dba1929eb93a2854de729fa68da9d5c6b48fa00/setuptools-39.2.0.zip", hash = "sha256:f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2"},
{url = "https://files.pythonhosted.org/packages/7f/e1/820d941153923aac1d49d7fc37e17b6e73bfbd2904959fffbad77900cf92/setuptools-39.2.0-py2.py3-none-any.whl", hash = "sha256:8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926"},
]
[[package]]
name = "urllib3"
version = "1.26.20"
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
summary = "HTTP library with thread-safe connection pooling, file post, and more."
groups = ["security"]
marker = "python_version >= \"3.6\""
files = [
{url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"},
{url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"},
]
@@ -13,7 +13,7 @@ import (
func Test_noSQLiteDriverError(t *testing.T) {
// this test package does must not import the sqlite library
file := "../test-fixtures/Packages"
resolver, err := fileresolver.NewFromFile(file, file)
resolver, err := fileresolver.NewFromFile(file)
require.NoError(t, err)
cataloger := redhat.NewDBCataloger()
+10
View File
@@ -79,6 +79,16 @@ func (m PythonPackage) OwnedFiles() (result []string) {
return result
}
// PythonPdmLockEntry represents a single package entry within a pdm.lock file.
type PythonPdmLockEntry struct {
// Summary provides a description of the package
Summary string `mapstructure:"summary" json:"summary" toml:"summary"`
// Files are the package files with their paths and hash digests
Files []PythonFileRecord `mapstructure:"files" json:"files" toml:"files"`
// Dependencies are the dependency specifications, without environment qualifiers
Dependencies []string `mapstructure:"dependencies" json:"dependencies" toml:"dependencies"`
}
// PythonPipfileLockEntry represents a single package entry within a Pipfile.lock file.
type PythonPipfileLockEntry struct {
// Hashes are the package file hash values in the format "algorithm:digest" for integrity verification.
+4
View File
@@ -7,6 +7,10 @@ import (
// Type represents a Package Type for or within a language ecosystem (there may be multiple package types within a language ecosystem)
type Type string
func (t Type) String() string {
return string(t)
}
const (
// the full set of supported packages
UnknownPkg Type = "UnknownPackage"
+3
View File
@@ -18,6 +18,9 @@ const (
// PURLQualifierUpstream this qualifier is not in the pURL spec, but is used by grype to perform indirect matching based on source information
PURLQualifierUpstream = "upstream"
// PURLQualifierRpmModularity this qualifier is not in the pURL spec, but is used to specify RPM modularity information
PURLQualifierRpmModularity = "rpmmod"
purlCargoPkgType = "cargo"
purlGradlePkgType = "gradle"
)
+70 -69
View File
@@ -30,24 +30,21 @@ type Config struct {
type directorySource struct {
id artifact.ID
config Config
resolver *fileresolver.Directory
resolver file.Resolver
mutex *sync.Mutex
}
func NewFromPath(path string) (source.Source, error) {
cfg := Config{
Path: path,
}
return New(cfg)
return New(Config{Path: path})
}
func New(cfg Config) (source.Source, error) {
fi, err := os.Stat(cfg.Path)
fileMeta, err := os.Stat(cfg.Path)
if err != nil {
return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err)
}
if !fi.IsDir() {
if !fileMeta.IsDir() {
return nil, fmt.Errorf("given path is not a directory: %q", cfg.Path)
}
@@ -58,53 +55,6 @@ func New(cfg Config) (source.Source, error) {
}, nil
}
// deriveIDFromDirectory generates an artifact ID from the given directory config. If an alias is provided, then
// the artifact ID is derived exclusively from the alias name and version. Otherwise, the artifact ID is derived
// from the path provided with an attempt to prune a prefix if a base is given. Since the contents of the directory
// are not considered, there is no semantic meaning to the artifact ID -- this is why the alias is preferred without
// consideration for the path.
func deriveIDFromDirectory(cfg Config) artifact.ID {
var info string
if !cfg.Alias.IsEmpty() {
// don't use any of the path information -- instead use the alias name and version as the artifact ID.
// why? this allows the user to set a dependable stable value for the artifact ID in case the
// scanning root changes (e.g. a user scans a directory, then moves it to a new location and scans again).
info = fmt.Sprintf("%s@%s", cfg.Alias.Name, cfg.Alias.Version)
} else {
log.Warn("no explicit name and version provided for directory source, deriving artifact ID from the given path (which is not ideal)")
info = cleanDirPath(cfg.Path, cfg.Base)
}
return internal.ArtifactIDFromDigest(digest.SHA256.FromString(filepath.Clean(info)).String())
}
func cleanDirPath(path, base string) string {
if path == base {
return path
}
if base != "" {
cleanRoot, rootErr := fileresolver.NormalizeRootDirectory(path)
cleanBase, baseErr := fileresolver.NormalizeBaseDirectory(base)
if rootErr == nil && baseErr == nil {
// allows for normalizing inputs:
// cleanRoot: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001/some/path
// cleanBase: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001
// normalized: some/path
relPath, err := filepath.Rel(cleanBase, cleanRoot)
if err == nil {
path = relPath
}
// this is odd, but this means we can't use base
}
// if the base is not a valid chroot, then just use the path as-is
}
return path
}
func (s directorySource) ID() artifact.ID {
return s.id
}
@@ -118,9 +68,11 @@ func (s directorySource) Describe() source.Description {
if a.Name != "" {
name = a.Name
}
if a.Version != "" {
version = a.Version
}
if a.Supplier != "" {
supplier = a.Supplier
}
@@ -141,29 +93,31 @@ func (s *directorySource) FileResolver(_ source.Scope) (file.Resolver, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.resolver == nil {
exclusionFunctions, err := GetDirectoryExclusionFunctions(s.config.Path, s.config.Exclude.Paths)
if err != nil {
return nil, err
}
// this should be the only file resolver that might have overlap with where files are cached
exclusionFunctions = append(exclusionFunctions, excludeCachePathVisitors()...)
res, err := fileresolver.NewFromDirectory(s.config.Path, s.config.Base, exclusionFunctions...)
if err != nil {
return nil, fmt.Errorf("unable to create directory resolver: %w", err)
}
s.resolver = res
if s.resolver != nil {
return s.resolver, nil
}
exclusionFunctions, err := GetDirectoryExclusionFunctions(s.config.Path, s.config.Exclude.Paths)
if err != nil {
return nil, err
}
// this should be the only file resolver that might have overlap with where files are cached
exclusionFunctions = append(exclusionFunctions, excludeCachePathVisitors()...)
res, err := fileresolver.NewFromDirectory(s.config.Path, s.config.Base, exclusionFunctions...)
if err != nil {
return nil, fmt.Errorf("unable to create directory resolver: %w", err)
}
s.resolver = res
return s.resolver, nil
}
func (s *directorySource) Close() error {
s.mutex.Lock()
defer s.mutex.Unlock()
s.resolver = nil
return nil
}
@@ -221,3 +175,50 @@ func GetDirectoryExclusionFunctions(root string, exclusions []string) ([]fileres
},
}, nil
}
// deriveIDFromDirectory generates an artifact ID from the given directory config. If an alias is provided, then
// the artifact ID is derived exclusively from the alias name and version. Otherwise, the artifact ID is derived
// from the path provided with an attempt to prune a prefix if a base is given. Since the contents of the directory
// are not considered, there is no semantic meaning to the artifact ID -- this is why the alias is preferred without
// consideration for the path.
func deriveIDFromDirectory(cfg Config) artifact.ID {
var info string
if !cfg.Alias.IsEmpty() {
// don't use any of the path information -- instead use the alias name and version as the artifact ID.
// why? this allows the user to set a dependable stable value for the artifact ID in case the
// scanning root changes (e.g. a user scans a directory, then moves it to a new location and scans again).
info = fmt.Sprintf("%s@%s", cfg.Alias.Name, cfg.Alias.Version)
} else {
log.Warn("no explicit name and version provided for directory source, deriving artifact ID from the given path (which is not ideal)")
info = cleanDirPath(cfg.Path, cfg.Base)
}
return internal.ArtifactIDFromDigest(digest.SHA256.FromString(filepath.Clean(info)).String())
}
func cleanDirPath(path, base string) string {
if path == base {
return path
}
if base != "" {
cleanRoot, rootErr := fileresolver.NormalizeRootDirectory(path)
cleanBase, baseErr := fileresolver.NormalizeBaseDirectory(base)
if rootErr == nil && baseErr == nil {
// allows for normalizing inputs:
// cleanRoot: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001/some/path
// cleanBase: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001
// normalized: some/path
relPath, err := filepath.Rel(cleanBase, cleanRoot)
if err == nil {
path = relPath
}
// this is odd, but this means we can't use base
}
// if the base is not a valid chroot, then just use the path as-is
}
return path
}
+40 -59
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"os"
"path"
"path/filepath"
"sync"
"github.com/opencontainers/go-digest"
@@ -50,7 +49,13 @@ func NewFromPath(path string) (source.Source, error) {
}
func New(cfg Config) (source.Source, error) {
fileMeta, err := os.Stat(cfg.Path)
f, err := os.Open(cfg.Path)
if err != nil {
return nil, fmt.Errorf("unable to open file=%q: %w", cfg.Path, err)
}
defer f.Close()
fileMeta, err := f.Stat()
if err != nil {
return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err)
}
@@ -59,33 +64,19 @@ func New(cfg Config) (source.Source, error) {
return nil, fmt.Errorf("given path is a directory: %q", cfg.Path)
}
analysisPath, cleanupFn, err := fileAnalysisPath(cfg.Path, cfg.SkipExtractArchive)
if err != nil {
return nil, fmt.Errorf("unable to extract file analysis path=%q: %w", cfg.Path, err)
}
var digests []file.Digest
if len(cfg.DigestAlgorithms) > 0 {
fh, err := os.Open(cfg.Path)
if err != nil {
return nil, fmt.Errorf("unable to open file=%q: %w", cfg.Path, err)
}
defer fh.Close()
digests, err = intFile.NewDigestsFromFile(context.TODO(), fh, cfg.DigestAlgorithms)
digests, err = intFile.NewDigestsFromFile(context.TODO(), f, cfg.DigestAlgorithms)
if err != nil {
return nil, fmt.Errorf("unable to calculate digests for file=%q: %w", cfg.Path, err)
}
}
fh, err := os.Open(cfg.Path)
analysisPath, cleanupFn, err := fileAnalysisPath(cfg.Path, cfg.SkipExtractArchive)
if err != nil {
return nil, fmt.Errorf("unable to open file=%q: %w", cfg.Path, err)
return nil, fmt.Errorf("unable to extract file analysis path=%q: %w", cfg.Path, err)
}
defer fh.Close()
id, versionDigest := deriveIDFromFile(cfg)
return &fileSource{
@@ -96,26 +87,10 @@ func New(cfg Config) (source.Source, error) {
analysisPath: analysisPath,
digestForVersion: versionDigest,
digests: digests,
mimeType: stereoFile.MIMEType(fh),
mimeType: stereoFile.MIMEType(f),
}, nil
}
// deriveIDFromFile derives an artifact ID from the contents of a file. If an alias is provided, it will be included
// in the ID derivation (along with contents). This way if the user scans the same item but is considered to be
// logically different, then ID will express that.
func deriveIDFromFile(cfg Config) (artifact.ID, string) {
d := digestOfFileContents(cfg.Path)
info := d
if !cfg.Alias.IsEmpty() {
// if the user provided an alias, we want to consider that in the artifact ID. This way if the user
// scans the same item but is considered to be logically different, then ID will express that.
info += fmt.Sprintf(":%s@%s", cfg.Alias.Name, cfg.Alias.Version)
}
return internal.ArtifactIDFromDigest(digest.SHA256.FromString(info).String()), d
}
func (s fileSource) ID() artifact.ID {
return s.id
}
@@ -168,52 +143,56 @@ func (s fileSource) FileResolver(_ source.Scope) (file.Resolver, error) {
if err != nil {
return nil, fmt.Errorf("unable to stat path=%q: %w", s.analysisPath, err)
}
isArchiveAnalysis := fi.IsDir()
absParentDir, err := absoluteSymlinkFreePathToParent(s.analysisPath)
if err != nil {
return nil, err
}
if isArchiveAnalysis {
if isArchiveAnalysis := fi.IsDir(); isArchiveAnalysis {
// this is an analysis of an archive file... we should scan the directory where the archive contents
res, err := fileresolver.NewFromDirectory(s.analysisPath, "", exclusionFunctions...)
if err != nil {
return nil, fmt.Errorf("unable to create directory resolver: %w", err)
}
s.resolver = res
return s.resolver, nil
}
// This is analysis of a single file. Use file indexer.
res, err := fileresolver.NewFromFile(absParentDir, s.analysisPath, exclusionFunctions...)
res, err := fileresolver.NewFromFile(s.analysisPath, exclusionFunctions...)
if err != nil {
return nil, fmt.Errorf("unable to create file resolver: %w", err)
}
s.resolver = res
return s.resolver, nil
}
func absoluteSymlinkFreePathToParent(path string) (string, error) {
absAnalysisPath, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("unable to get absolute path for analysis path=%q: %w", path, err)
}
dereferencedAbsAnalysisPath, err := filepath.EvalSymlinks(absAnalysisPath)
if err != nil {
return "", fmt.Errorf("unable to get absolute path for analysis path=%q: %w", path, err)
}
return filepath.Dir(dereferencedAbsAnalysisPath), nil
}
func (s *fileSource) Close() error {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.closer == nil {
return nil
}
s.resolver = nil
return s.closer()
}
// deriveIDFromFile derives an artifact ID from the contents of a file. If an alias is provided, it will be included
// in the ID derivation (along with contents). This way if the user scans the same item but is considered to be
// logically different, then ID will express that.
func deriveIDFromFile(cfg Config) (artifact.ID, string) {
d := digestOfFileContents(cfg.Path)
info := d
if !cfg.Alias.IsEmpty() {
// if the user provided an alias, we want to consider that in the artifact ID. This way if the user
// scans the same item but is considered to be logically different, then ID will express that.
info += fmt.Sprintf(":%s@%s", cfg.Alias.Name, cfg.Alias.Version)
}
return internal.ArtifactIDFromDigest(digest.SHA256.FromString(info).String()), d
}
// fileAnalysisPath returns the path given, or in the case the path is an archive, the location where the archive
// contents have been made available. A cleanup function is provided for any temp files created (if any).
// Users can disable unpacking archives, allowing individual cataloguers to extract them instead (where
@@ -253,15 +232,17 @@ func fileAnalysisPath(path string, skipExtractArchive bool) (string, func() erro
}
func digestOfFileContents(path string) string {
file, err := os.Open(path)
f, err := os.Open(path)
if err != nil {
return digest.SHA256.FromString(path).String()
}
defer file.Close()
di, err := digest.SHA256.FromReader(file)
defer f.Close()
di, err := digest.SHA256.FromReader(f)
if err != nil {
return digest.SHA256.FromString(path).String()
}
return di.String()
}