mirror of
https://github.com/anchore/syft.git
synced 2026-10-12 14:17:20 +02:00
Compare commits
77
Commits
+8
-8
@@ -2,7 +2,7 @@ tools:
|
||||
# we want to use a pinned version of binny to manage the toolchain (so binny manages itself!)
|
||||
- name: binny
|
||||
version:
|
||||
want: v0.9.0
|
||||
want: v0.10.0
|
||||
method: github-release
|
||||
with:
|
||||
repo: anchore/binny
|
||||
@@ -26,7 +26,7 @@ tools:
|
||||
# used for linting
|
||||
- name: golangci-lint
|
||||
version:
|
||||
want: v2.4.0
|
||||
want: v2.5.0
|
||||
method: github-release
|
||||
with:
|
||||
repo: golangci/golangci-lint
|
||||
@@ -42,7 +42,7 @@ tools:
|
||||
# used for signing the checksums file at release
|
||||
- name: cosign
|
||||
version:
|
||||
want: v2.5.3
|
||||
want: v3.0.2
|
||||
method: github-release
|
||||
with:
|
||||
repo: sigstore/cosign
|
||||
@@ -58,7 +58,7 @@ tools:
|
||||
# used to release all artifacts
|
||||
- name: goreleaser
|
||||
version:
|
||||
want: v2.11.2
|
||||
want: v2.12.5
|
||||
method: github-release
|
||||
with:
|
||||
repo: goreleaser/goreleaser
|
||||
@@ -90,7 +90,7 @@ tools:
|
||||
# used for running all local and CI tasks
|
||||
- name: task
|
||||
version:
|
||||
want: v3.44.1
|
||||
want: v3.45.4
|
||||
method: github-release
|
||||
with:
|
||||
repo: go-task/task
|
||||
@@ -98,7 +98,7 @@ tools:
|
||||
# used for triggering a release
|
||||
- name: gh
|
||||
version:
|
||||
want: v2.78.0
|
||||
want: v2.81.0
|
||||
method: github-release
|
||||
with:
|
||||
repo: cli/cli
|
||||
@@ -106,7 +106,7 @@ tools:
|
||||
# used to upload test fixture cache
|
||||
- name: oras
|
||||
version:
|
||||
want: v1.2.3
|
||||
want: v1.3.0
|
||||
method: github-release
|
||||
with:
|
||||
repo: oras-project/oras
|
||||
@@ -114,7 +114,7 @@ tools:
|
||||
# used to upload test fixture cache
|
||||
- name: yq
|
||||
version:
|
||||
want: v4.47.1
|
||||
want: v4.48.1
|
||||
method: github-release
|
||||
with:
|
||||
repo: mikefarah/yq
|
||||
|
||||
@@ -9,6 +9,9 @@ permit:
|
||||
- Unlicense
|
||||
|
||||
ignore-packages:
|
||||
# https://github.com/sorairolake/lzip-go/blob/34a2615d2abf740175c6b0a835baa08364e09430/go.sum.license#L3
|
||||
# has `SPDX-License-Identifier: Apache-2.0 OR MIT`, both of which are acceptable
|
||||
- github.com/sorairolake/lzip-go
|
||||
# packageurl-go is released under the MIT license located in the root of the repo at /mit.LICENSE
|
||||
- github.com/anchore/packageurl-go
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
# note: go mod and build is automatically cached on default with v4+
|
||||
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
if: inputs.go-version != ''
|
||||
with:
|
||||
go-version: ${{ inputs.go-version }}
|
||||
@@ -37,7 +37,7 @@ runs:
|
||||
- name: Restore tool cache
|
||||
if: inputs.tools == 'true'
|
||||
id: tool-cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tool
|
||||
key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('.binny.yaml') }}
|
||||
@@ -63,7 +63,7 @@ runs:
|
||||
|
||||
- name: Restore ORAS cache from github actions
|
||||
if: inputs.download-test-fixture-cache == 'true'
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp/oras-cache
|
||||
key: ${{ inputs.cache-key-prefix }}-oras-cache
|
||||
|
||||
@@ -41,13 +41,13 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 #v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 #v6.0.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@3c3833e0f8c1c83d449a7478aa59c036a9165498 #v3.29.5
|
||||
uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 #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@3c3833e0f8c1c83d449a7478aa59c036a9165498 #v3.29.5
|
||||
uses: github/codeql-action/autobuild@f443b600d91635bebf5b0d9ebc620189c0d6fba5 #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@3c3833e0f8c1c83d449a7478aa59c036a9165498 #v3.29.5
|
||||
uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 #v3.29.5
|
||||
|
||||
@@ -161,13 +161,13 @@ jobs:
|
||||
# for updating brew formula in anchore/homebrew-syft
|
||||
GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}
|
||||
|
||||
- uses: anchore/sbom-action@da167eac915b4e86f08b264dbdbc867b61be6f0c #v0.20.5
|
||||
- uses: anchore/sbom-action@f8bdd1d8ac5e901a77a92f111440fdb1b593736b #v0.20.6
|
||||
continue-on-error: true
|
||||
with:
|
||||
file: go.mod
|
||||
artifact-name: sbom.spdx.json
|
||||
|
||||
- uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e #v3.18.0
|
||||
- uses: 8398a7/action-slack@77eaa4f1c608a7d68b38af4e3f739dcd8cba273e #v3.19.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
Update CPE dictionary index based on the latest available CPE dictionary
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e #v3.18.0
|
||||
- uses: 8398a7/action-slack@77eaa4f1c608a7d68b38af4e3f739dcd8cba273e #v3.19.0
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
fields: workflow,eventName,job
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run zizmor"
|
||||
uses: zizmorcore/zizmor-action@5ca5fc7a4779c5263a3ffa0e1f693009994446d1 # v0.1.2
|
||||
uses: zizmorcore/zizmor-action@e673c3917a1aef3c65c972347ed84ccd013ecda4 # v0.2.0
|
||||
with:
|
||||
config-file: .github/zizmor.yml
|
||||
sarif-upload: true
|
||||
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
# why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach).
|
||||
# see https://github.com/actions/upload-artifact/issues/199 for more info
|
||||
- name: Upload snapshot artifacts
|
||||
uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 #v4.2.4
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 #v4.3.0
|
||||
with:
|
||||
# we need to preserve the snapshot data itself as well as the task data that confirms if the
|
||||
# snapshot build is stale or not. Otherwise the downstream jobs will attempt to rebuild the snapshot
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
|
||||
- name: Download snapshot build
|
||||
id: snapshot-cache
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 #v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 #v4.3.0
|
||||
with:
|
||||
path: |
|
||||
snapshot
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
|
||||
- run: npm install @actions/artifact@2.2.2
|
||||
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const { readdirSync } = require('fs')
|
||||
@@ -175,7 +175,7 @@ jobs:
|
||||
|
||||
- name: Download snapshot build
|
||||
id: snapshot-cache
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 #v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 #v4.3.0
|
||||
with:
|
||||
path: |
|
||||
snapshot
|
||||
@@ -210,7 +210,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0
|
||||
with:
|
||||
@@ -225,7 +225,7 @@ jobs:
|
||||
|
||||
- name: Download snapshot build
|
||||
id: snapshot-cache
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 #v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 #v4.3.0
|
||||
with:
|
||||
path: |
|
||||
snapshot
|
||||
@@ -262,7 +262,7 @@ jobs:
|
||||
|
||||
- name: Download snapshot build
|
||||
id: snapshot-cache
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 #v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 #v4.3.0
|
||||
with:
|
||||
path: |
|
||||
snapshot
|
||||
|
||||
@@ -16,6 +16,8 @@ bin/
|
||||
/snapshot
|
||||
/.tool
|
||||
/.task
|
||||
/generate
|
||||
/specs
|
||||
|
||||
# changelog generation
|
||||
CHANGELOG.md
|
||||
|
||||
+2
-1
@@ -42,7 +42,7 @@ builds:
|
||||
dir: ./cmd/syft
|
||||
binary: syft
|
||||
goos: [windows]
|
||||
goarch: [amd64]
|
||||
goarch: [amd64, arm64]
|
||||
mod_timestamp: *build-timestamp
|
||||
ldflags: *build-ldflags
|
||||
|
||||
@@ -337,6 +337,7 @@ signs:
|
||||
certificate: "${artifact}.pem"
|
||||
args:
|
||||
- "sign-blob"
|
||||
- "--use-signing-config=false"
|
||||
- "--oidc-issuer=https://token.actions.githubusercontent.com"
|
||||
- "--output-certificate=${certificate}"
|
||||
- "--output-signature=${signature}"
|
||||
|
||||
+3
-3
@@ -27,7 +27,7 @@ Also double check that the docker context being used is the default context. If
|
||||
|
||||
`docker context use default`
|
||||
|
||||
After cloning the following step can help you get setup:
|
||||
After cloning, the following steps can help you get setup:
|
||||
1. run `make bootstrap` to download go mod dependencies, create the `/.tmp` dir, and download helper utilities.
|
||||
2. run `make` to view the selection of developer commands in the Makefile
|
||||
3. run `make build` to build the release snapshot binaries and packages
|
||||
@@ -120,7 +120,7 @@ sequenceDiagram
|
||||
source-->>+sbom: add source to SBOM struct
|
||||
source-->>+catalog: pass src to generate catalog
|
||||
catalog-->-sbom: add cataloging results onto SBOM
|
||||
sbom-->>encoder: pass SBOM and format desiered to syft encoder
|
||||
sbom-->>encoder: pass SBOM and format desired to syft encoder
|
||||
encoder-->>source: return bytes that are the SBOM of the original input
|
||||
|
||||
Note right of catalog: cataloger configuration is done based on src
|
||||
@@ -186,7 +186,7 @@ Cataloger names should be unique and named with the following rules of thumb in
|
||||
- Use lowercase letters, numbers, and hyphens only
|
||||
- Use hyphens to separate words
|
||||
- Catalogers for language ecosystems should start with the language name (e.g. `python-` for a cataloger that raises up python packages)
|
||||
- Distinct between when the cataloger is searching for evidence of installed packages vs declared packages. For example, there are currently two different gemspec-based catalogers, the `ruby-gemspec-cataloger` and `ruby-installed-gemspec-cataloger`, where the latter requires that the gemspec is found within a `specifications` directory (which means it was installed, not just at the root of a source repo).
|
||||
- Distinguish between when the cataloger is searching for evidence of installed packages vs declared packages. For example, there are currently two different gemspec-based catalogers, the `ruby-gemspec-cataloger` and `ruby-installed-gemspec-cataloger`, where the latter requires that the gemspec is found within a `specifications` directory (which means it was installed, not just at the root of a source repo).
|
||||
|
||||
#### Building a new Cataloger
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ Note that flags using the @<version> can be used for earlier versions of each sp
|
||||
- Elixir (mix)
|
||||
- Erlang (rebar3)
|
||||
- Go (go.mod, Go binaries)
|
||||
- GitHub (workflows, actions)
|
||||
- Haskell (cabal, stack)
|
||||
- Java (jar, ear, war, par, sar, nar, native-image)
|
||||
- JavaScript (npm, yarn)
|
||||
|
||||
+1
-1
@@ -29,4 +29,4 @@ To report a security issue, please email
|
||||
with a description of the issue, the steps you took to create the issue,
|
||||
affected versions, and, if known, mitigations for the issue.
|
||||
|
||||
All support will be made on the best effort base, so please indicate the "urgency level" of the vulnerability as Critical, High, Medium or Low.
|
||||
All support will be made on a best effort basis, so please indicate the "urgency level" of the vulnerability as Critical, High, Medium or Low.
|
||||
|
||||
+1
-1
@@ -502,7 +502,7 @@ tasks:
|
||||
generate-json-schema:
|
||||
desc: Generate a new JSON schema
|
||||
cmds:
|
||||
- "cd syft/internal && go generate . && cd jsonschema && go run . && go fmt ../..."
|
||||
- "cd ./internal && go generate . && cd ./jsonschema && go run . && go fmt ../..."
|
||||
|
||||
generate-license-list:
|
||||
desc: Generate an updated license processing code off of the latest available SPDX license list
|
||||
|
||||
@@ -4,7 +4,7 @@ go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/CycloneDX/cyclonedx-go v0.9.2
|
||||
github.com/CycloneDX/cyclonedx-go v0.9.3
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/OneOfOne/xxhash v1.2.8
|
||||
@@ -24,14 +24,14 @@ 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.9-0.20250826202322-ef061ea78385
|
||||
github.com/anchore/stereoscope v0.1.11
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/aquasecurity/go-pep440-version v0.0.1
|
||||
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||
github.com/bmatcuk/doublestar/v4 v4.9.1
|
||||
github.com/charmbracelet/bubbles v0.21.0
|
||||
github.com/charmbracelet/bubbletea v1.3.6
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/dave/jennifer v1.7.1
|
||||
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
|
||||
@@ -41,19 +41,19 @@ require (
|
||||
github.com/elliotchance/phpserialize v1.4.0
|
||||
github.com/facebookincubator/nvdtools v0.1.5
|
||||
github.com/github/go-spdx/v2 v2.3.3
|
||||
github.com/gkampitakis/go-snaps v0.5.14
|
||||
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.2
|
||||
github.com/go-git/go-git/v5 v5.16.3
|
||||
github.com/go-test/deep v1.1.1
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0
|
||||
github.com/gohugoio/hashstructure v0.5.0
|
||||
github.com/gohugoio/hashstructure v0.6.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/go-containerregistry v0.20.6
|
||||
github.com/google/licensecheck v0.3.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gookit/color v1.5.4
|
||||
github.com/gookit/color v1.6.0
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2
|
||||
github.com/hashicorp/go-getter v1.7.9
|
||||
github.com/hashicorp/go-getter v1.8.2
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/hcl/v2 v2.24.0
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
@@ -62,13 +62,13 @@ require (
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
|
||||
github.com/magiconair/properties v1.8.10
|
||||
github.com/mholt/archives v0.1.3
|
||||
github.com/mholt/archives v0.1.5
|
||||
github.com/moby/sys/mountinfo v0.7.2
|
||||
github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1
|
||||
github.com/olekukonko/tablewriter v1.0.9
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.22
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.23
|
||||
github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
|
||||
github.com/sanity-io/litter v1.5.8
|
||||
@@ -78,10 +78,10 @@ require (
|
||||
github.com/sergi/go-diff v1.4.0
|
||||
github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb
|
||||
github.com/spdx/tools-golang v0.5.5
|
||||
github.com/spf13/afero v1.14.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.11.0
|
||||
github.com/vbatts/go-mtree v0.5.4
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/vbatts/go-mtree v0.6.0
|
||||
github.com/vifraa/gopom v1.0.0
|
||||
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651
|
||||
github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0
|
||||
@@ -90,9 +90,9 @@ require (
|
||||
go.uber.org/goleak v1.3.0
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||
golang.org/x/mod v0.27.0
|
||||
golang.org/x/net v0.43.0
|
||||
modernc.org/sqlite v1.38.2
|
||||
golang.org/x/mod v0.29.0
|
||||
golang.org/x/net v0.46.0
|
||||
modernc.org/sqlite v1.39.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -109,25 +109,24 @@ require (
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.7 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.2.0 // indirect
|
||||
github.com/STARRY-S/zip v0.2.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||
github.com/agext/levenshtein v1.2.1 // indirect; indirectt
|
||||
github.com/anchore/go-lzo v0.1.0 // indirect
|
||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
|
||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/aquasecurity/go-version v0.0.1 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.122 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/becheran/wildmatch-go v1.0.0 // indirect
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.1 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.9.3 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
@@ -145,9 +144,9 @@ require (
|
||||
github.com/containerd/typeurl/v2 v2.2.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/cli v28.3.2+incompatible // indirect
|
||||
github.com/docker/cli v28.5.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v28.3.3+incompatible // indirect
|
||||
github.com/docker/docker v28.5.1+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||
@@ -155,7 +154,7 @@ require (
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/felixge/fgprof v0.9.5 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
@@ -175,14 +174,12 @@ require (
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-safetemp v1.0.0 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
|
||||
github.com/iancoleman/orderedmap v0.3.0
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
@@ -191,13 +188,13 @@ require (
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/maruel/natural v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||
github.com/minio/minlz v1.0.0 // indirect
|
||||
github.com/minio/minlz v1.0.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
@@ -213,7 +210,7 @@ require (
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/nwaples/rardecode v1.1.3 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.1.0 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
|
||||
github.com/olekukonko/errors v1.1.0 // indirect
|
||||
github.com/olekukonko/ll v0.0.9 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
@@ -235,21 +232,21 @@ require (
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.5 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/spf13/viper v1.20.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/sylabs/sif/v2 v2.21.1 // indirect
|
||||
github.com/sylabs/sif/v2 v2.22.0 // indirect
|
||||
github.com/sylabs/squashfs v1.0.6 // indirect
|
||||
github.com/therootcompany/xz v1.0.1 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
@@ -267,28 +264,50 @@ require (
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/term v0.34.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/term v0.36.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.35.0
|
||||
golang.org/x/tools v0.38.0
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/api v0.203.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
|
||||
google.golang.org/grpc v1.67.3 // indirect
|
||||
google.golang.org/protobuf v1.36.4 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/libc v1.66.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.80.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect
|
||||
github.com/aws/smithy-go v1.22.4 // indirect
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.65 // indirect
|
||||
)
|
||||
|
||||
retract (
|
||||
v1.25.0 // published with a replace directive (confusing for API users)
|
||||
v0.53.2
|
||||
|
||||
Vendored
+1
-1
@@ -18,7 +18,7 @@ based on the _structure_ of the provided type.
|
||||
If the structure changes in any way it will end up with a new version key and all will re populate this new key,
|
||||
ignoring cached values from older, different versions.
|
||||
The resolver will store items using the `json` package to serialize/deserialize values, so to save space
|
||||
it is encouraged to use `omitempty`. For example:
|
||||
it is encouraged to use `omitempty`. For example:
|
||||
|
||||
```go
|
||||
type myCacheItem struct {
|
||||
|
||||
@@ -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.39"
|
||||
JSONSchemaVersion = "16.0.40"
|
||||
)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Please see [schema/json/README.md](../../schema/json/README.md) for more information on the JSON schema files in this directory.
|
||||
@@ -0,0 +1,159 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/invopop/jsonschema"
|
||||
)
|
||||
|
||||
func copyAliasFieldComments(commentMap map[string]string, repoRoot string) {
|
||||
// find all type aliases by parsing Go source files
|
||||
aliases := findTypeAliases(repoRoot)
|
||||
|
||||
// for each alias, copy field comments from the source type
|
||||
for aliasName, sourceName := range aliases {
|
||||
// find all field comments for the source type
|
||||
for key, comment := range commentMap {
|
||||
// check if this is a field comment for the source type
|
||||
// format: "github.com/anchore/syft/syft/pkg.SourceType.FieldName"
|
||||
if strings.Contains(key, "."+sourceName+".") {
|
||||
// create the corresponding key for the alias
|
||||
aliasKey := strings.Replace(key, "."+sourceName+".", "."+aliasName+".", 1)
|
||||
commentMap[aliasKey] = comment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func findTypeAliases(repoRoot string) map[string]string {
|
||||
aliases := make(map[string]string)
|
||||
fset := token.NewFileSet()
|
||||
|
||||
// walk through all Go files in the repo
|
||||
err := filepath.Walk(repoRoot, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// parse the file
|
||||
file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// look for type alias declarations
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
typeSpec, ok := n.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// check if this is a type alias (e.g., type A B where B is an identifier)
|
||||
ident, ok := typeSpec.Type.(*ast.Ident)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// store the alias mapping: aliasName -> sourceName
|
||||
aliases[typeSpec.Name.Name] = ident.Name
|
||||
return true
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: failed to find type aliases: %v\n", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return aliases
|
||||
}
|
||||
|
||||
func hasDescriptionInAlternatives(schema *jsonschema.Schema) bool {
|
||||
// check oneOf alternatives
|
||||
for _, alt := range schema.OneOf {
|
||||
if alt.Description != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// check anyOf alternatives
|
||||
for _, alt := range schema.AnyOf {
|
||||
if alt.Description != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func warnMissingDescriptions(schema *jsonschema.Schema, metadataNames []string) { //nolint:gocognit
|
||||
var missingTypeDescriptions []string
|
||||
var missingFieldDescriptions []string
|
||||
|
||||
// check metadata types for missing descriptions
|
||||
for _, name := range metadataNames {
|
||||
def, ok := schema.Definitions[name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// check if type has a description
|
||||
if def.Description == "" {
|
||||
missingTypeDescriptions = append(missingTypeDescriptions, name)
|
||||
}
|
||||
|
||||
// check if fields have descriptions
|
||||
if def.Properties != nil {
|
||||
for _, fieldName := range def.Properties.Keys() {
|
||||
fieldSchemaRaw, _ := def.Properties.Get(fieldName)
|
||||
fieldSchema, ok := fieldSchemaRaw.(*jsonschema.Schema)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip if field has a description
|
||||
if fieldSchema.Description != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip if field is a reference (descriptions come from the referenced type)
|
||||
if fieldSchema.Ref != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip if field is an array/object with items that are references
|
||||
if fieldSchema.Items != nil && fieldSchema.Items.Ref != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip if field uses oneOf/anyOf with descriptions in the alternatives
|
||||
if hasDescriptionInAlternatives(fieldSchema) {
|
||||
continue
|
||||
}
|
||||
|
||||
missingFieldDescriptions = append(missingFieldDescriptions, fmt.Sprintf("%s.%s", name, fieldName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// report findings
|
||||
if len(missingTypeDescriptions) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "\nwarning: %d metadata types are missing descriptions:\n", len(missingTypeDescriptions))
|
||||
for _, name := range missingTypeDescriptions {
|
||||
fmt.Fprintf(os.Stderr, " - %s\n", name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingFieldDescriptions) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "\nwarning: %d fields are missing descriptions:\n", len(missingFieldDescriptions))
|
||||
for _, field := range missingFieldDescriptions {
|
||||
fmt.Fprintf(os.Stderr, " - %s\n", field)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/iancoleman/orderedmap"
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestCopyAliasFieldComments verifies that field comments from source types are correctly copied to alias types.
|
||||
// This is important for type aliases like `type RpmArchive RpmDBEntry` where the alias should inherit all field descriptions.
|
||||
func TestCopyAliasFieldComments(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
commentMap map[string]string
|
||||
aliases map[string]string
|
||||
wantComments map[string]string
|
||||
}{
|
||||
{
|
||||
name: "copies field comments from source type to alias",
|
||||
commentMap: map[string]string{
|
||||
"github.com/anchore/syft/syft/pkg.RpmDBEntry": "RpmDBEntry represents all captured data from a RPM DB package entry.",
|
||||
"github.com/anchore/syft/syft/pkg.RpmDBEntry.Name": "Name is the RPM package name.",
|
||||
"github.com/anchore/syft/syft/pkg.RpmDBEntry.Epoch": "Epoch is the version epoch.",
|
||||
},
|
||||
aliases: map[string]string{
|
||||
"RpmArchive": "RpmDBEntry",
|
||||
},
|
||||
wantComments: map[string]string{
|
||||
"github.com/anchore/syft/syft/pkg.RpmDBEntry": "RpmDBEntry represents all captured data from a RPM DB package entry.",
|
||||
"github.com/anchore/syft/syft/pkg.RpmDBEntry.Name": "Name is the RPM package name.",
|
||||
"github.com/anchore/syft/syft/pkg.RpmDBEntry.Epoch": "Epoch is the version epoch.",
|
||||
"github.com/anchore/syft/syft/pkg.RpmArchive.Name": "Name is the RPM package name.",
|
||||
"github.com/anchore/syft/syft/pkg.RpmArchive.Epoch": "Epoch is the version epoch.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "handles multiple aliases",
|
||||
commentMap: map[string]string{
|
||||
"github.com/anchore/syft/syft/pkg.DpkgDBEntry": "DpkgDBEntry represents data from dpkg.",
|
||||
"github.com/anchore/syft/syft/pkg.DpkgDBEntry.Package": "Package is the package name.",
|
||||
"github.com/anchore/syft/syft/pkg.DpkgDBEntry.Architecture": "Architecture is the target arch.",
|
||||
},
|
||||
aliases: map[string]string{
|
||||
"DpkgArchiveEntry": "DpkgDBEntry",
|
||||
"DpkgSnapshot": "DpkgDBEntry",
|
||||
},
|
||||
wantComments: map[string]string{
|
||||
"github.com/anchore/syft/syft/pkg.DpkgDBEntry": "DpkgDBEntry represents data from dpkg.",
|
||||
"github.com/anchore/syft/syft/pkg.DpkgDBEntry.Package": "Package is the package name.",
|
||||
"github.com/anchore/syft/syft/pkg.DpkgDBEntry.Architecture": "Architecture is the target arch.",
|
||||
"github.com/anchore/syft/syft/pkg.DpkgArchiveEntry.Package": "Package is the package name.",
|
||||
"github.com/anchore/syft/syft/pkg.DpkgArchiveEntry.Architecture": "Architecture is the target arch.",
|
||||
"github.com/anchore/syft/syft/pkg.DpkgSnapshot.Package": "Package is the package name.",
|
||||
"github.com/anchore/syft/syft/pkg.DpkgSnapshot.Architecture": "Architecture is the target arch.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "does not copy non-field comments",
|
||||
commentMap: map[string]string{
|
||||
"github.com/anchore/syft/syft/pkg.SomeType": "SomeType struct comment.",
|
||||
"github.com/anchore/syft/syft/pkg.SomeType.Field": "Field comment.",
|
||||
},
|
||||
aliases: map[string]string{
|
||||
"AliasType": "SomeType",
|
||||
},
|
||||
wantComments: map[string]string{
|
||||
"github.com/anchore/syft/syft/pkg.SomeType": "SomeType struct comment.",
|
||||
"github.com/anchore/syft/syft/pkg.SomeType.Field": "Field comment.",
|
||||
"github.com/anchore/syft/syft/pkg.AliasType.Field": "Field comment.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// create temp dir for testing
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// create a test go file with type aliases
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
content := "package test\n\n"
|
||||
for alias, source := range tt.aliases {
|
||||
content += "type " + alias + " " + source + "\n"
|
||||
}
|
||||
err := os.WriteFile(testFile, []byte(content), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// make a copy of the comment map since the function modifies it
|
||||
commentMap := make(map[string]string)
|
||||
for k, v := range tt.commentMap {
|
||||
commentMap[k] = v
|
||||
}
|
||||
|
||||
// run the function
|
||||
copyAliasFieldComments(commentMap, tmpDir)
|
||||
|
||||
// verify results
|
||||
assert.Equal(t, tt.wantComments, commentMap)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindTypeAliases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fileContent string
|
||||
wantAliases map[string]string
|
||||
}{
|
||||
{
|
||||
name: "finds simple type alias",
|
||||
fileContent: `package test
|
||||
|
||||
type RpmArchive RpmDBEntry
|
||||
type DpkgArchiveEntry DpkgDBEntry
|
||||
`,
|
||||
wantAliases: map[string]string{
|
||||
"RpmArchive": "RpmDBEntry",
|
||||
"DpkgArchiveEntry": "DpkgDBEntry",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ignores struct definitions",
|
||||
fileContent: `package test
|
||||
|
||||
type MyStruct struct {
|
||||
Field string
|
||||
}
|
||||
|
||||
type AliasType BaseType
|
||||
`,
|
||||
wantAliases: map[string]string{
|
||||
"AliasType": "BaseType",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ignores interface definitions",
|
||||
fileContent: `package test
|
||||
|
||||
type MyInterface interface {
|
||||
Method()
|
||||
}
|
||||
|
||||
type AliasType BaseType
|
||||
`,
|
||||
wantAliases: map[string]string{
|
||||
"AliasType": "BaseType",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "handles multiple files",
|
||||
fileContent: `package test
|
||||
|
||||
type Alias1 Base1
|
||||
type Alias2 Base2
|
||||
`,
|
||||
wantAliases: map[string]string{
|
||||
"Alias1": "Base1",
|
||||
"Alias2": "Base2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// create temp dir
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// write test file
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(tt.fileContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// run function
|
||||
aliases := findTypeAliases(tmpDir)
|
||||
|
||||
// verify
|
||||
assert.Equal(t, tt.wantAliases, aliases)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasDescriptionInAlternatives(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schema *jsonschema.Schema
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "returns true when oneOf has description",
|
||||
schema: &jsonschema.Schema{
|
||||
OneOf: []*jsonschema.Schema{
|
||||
{Description: "First alternative"},
|
||||
{Type: "null"},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "returns true when anyOf has description",
|
||||
schema: &jsonschema.Schema{
|
||||
AnyOf: []*jsonschema.Schema{
|
||||
{Description: "First alternative"},
|
||||
{Type: "null"},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "returns false when no alternatives have descriptions",
|
||||
schema: &jsonschema.Schema{
|
||||
OneOf: []*jsonschema.Schema{
|
||||
{Type: "integer"},
|
||||
{Type: "null"},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "returns false when no oneOf or anyOf",
|
||||
schema: &jsonschema.Schema{
|
||||
Type: "string",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "returns true when any alternative in oneOf has description",
|
||||
schema: &jsonschema.Schema{
|
||||
OneOf: []*jsonschema.Schema{
|
||||
{Type: "integer"},
|
||||
{Type: "string", Description: "Second alternative"},
|
||||
{Type: "null"},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := hasDescriptionInAlternatives(tt.schema)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarnMissingDescriptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schema *jsonschema.Schema
|
||||
metadataNames []string
|
||||
wantTypeWarnings int
|
||||
wantFieldWarnings int
|
||||
}{
|
||||
{
|
||||
name: "no warnings when all types have descriptions",
|
||||
schema: &jsonschema.Schema{
|
||||
Definitions: map[string]*jsonschema.Schema{
|
||||
"TypeA": {
|
||||
Description: "Type A description",
|
||||
Properties: newOrderedMap(map[string]*jsonschema.Schema{
|
||||
"field1": {Type: "string", Description: "Field 1"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
metadataNames: []string{"TypeA"},
|
||||
wantTypeWarnings: 0,
|
||||
wantFieldWarnings: 0,
|
||||
},
|
||||
{
|
||||
name: "warns about missing type description",
|
||||
schema: &jsonschema.Schema{
|
||||
Definitions: map[string]*jsonschema.Schema{
|
||||
"TypeA": {
|
||||
Properties: newOrderedMap(map[string]*jsonschema.Schema{
|
||||
"field1": {Type: "string", Description: "Field 1"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
metadataNames: []string{"TypeA"},
|
||||
wantTypeWarnings: 1,
|
||||
wantFieldWarnings: 0,
|
||||
},
|
||||
{
|
||||
name: "warns about missing field description",
|
||||
schema: &jsonschema.Schema{
|
||||
Definitions: map[string]*jsonschema.Schema{
|
||||
"TypeA": {
|
||||
Description: "Type A description",
|
||||
Properties: newOrderedMap(map[string]*jsonschema.Schema{
|
||||
"field1": {Type: "string"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
metadataNames: []string{"TypeA"},
|
||||
wantTypeWarnings: 0,
|
||||
wantFieldWarnings: 1,
|
||||
},
|
||||
{
|
||||
name: "skips fields with references",
|
||||
schema: &jsonschema.Schema{
|
||||
Definitions: map[string]*jsonschema.Schema{
|
||||
"TypeA": {
|
||||
Description: "Type A description",
|
||||
Properties: newOrderedMap(map[string]*jsonschema.Schema{
|
||||
"field1": {Ref: "#/$defs/OtherType"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
metadataNames: []string{"TypeA"},
|
||||
wantTypeWarnings: 0,
|
||||
wantFieldWarnings: 0,
|
||||
},
|
||||
{
|
||||
name: "skips fields with items that are references",
|
||||
schema: &jsonschema.Schema{
|
||||
Definitions: map[string]*jsonschema.Schema{
|
||||
"TypeA": {
|
||||
Description: "Type A description",
|
||||
Properties: newOrderedMap(map[string]*jsonschema.Schema{
|
||||
"field1": {
|
||||
Type: "array",
|
||||
Items: &jsonschema.Schema{Ref: "#/$defs/OtherType"},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
metadataNames: []string{"TypeA"},
|
||||
wantTypeWarnings: 0,
|
||||
wantFieldWarnings: 0,
|
||||
},
|
||||
{
|
||||
name: "skips fields with oneOf containing descriptions",
|
||||
schema: &jsonschema.Schema{
|
||||
Definitions: map[string]*jsonschema.Schema{
|
||||
"TypeA": {
|
||||
Description: "Type A description",
|
||||
Properties: newOrderedMap(map[string]*jsonschema.Schema{
|
||||
"field1": {
|
||||
OneOf: []*jsonschema.Schema{
|
||||
{Type: "integer", Description: "Integer value"},
|
||||
{Type: "null"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
metadataNames: []string{"TypeA"},
|
||||
wantTypeWarnings: 0,
|
||||
wantFieldWarnings: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// capture stderr output would require more complex testing
|
||||
// for now, just verify the function runs without panicking
|
||||
require.NotPanics(t, func() {
|
||||
warnMissingDescriptions(tt.schema, tt.metadataNames)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// helper to create an ordered map from a regular map
|
||||
func newOrderedMap(m map[string]*jsonschema.Schema) *orderedmap.OrderedMap {
|
||||
om := orderedmap.New()
|
||||
for k, v := range m {
|
||||
om.Set(k, v)
|
||||
}
|
||||
return om
|
||||
}
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
"github.com/invopop/jsonschema"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/packagemetadata"
|
||||
syftJsonModel "github.com/anchore/syft/syft/format/syftjson/model"
|
||||
"github.com/anchore/syft/syft/internal/packagemetadata"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -26,6 +26,17 @@ are not captured (empty interfaces). This means that pkg.Package.Metadata is not
|
||||
can be extended to include specific package metadata struct shapes in the future.
|
||||
*/
|
||||
|
||||
var repoRoot string
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
repoRoot, err = packagemetadata.RepoRoot()
|
||||
if err != nil {
|
||||
fmt.Println("unable to determine repo root")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
write(encode(build()))
|
||||
}
|
||||
@@ -60,7 +71,7 @@ func assembleTypeContainer(items []any) (any, map[string]string) {
|
||||
}
|
||||
|
||||
if len(typesMissingNames) > 0 {
|
||||
fmt.Println("the following types are missing JSON names (manually curated in ./syft/internal/packagemetadata/names.go):")
|
||||
fmt.Println("the following types are missing JSON names (manually curated in ./internal/packagemetadata/names.go):")
|
||||
for _, t := range typesMissingNames {
|
||||
fmt.Println(" - ", t.Name())
|
||||
}
|
||||
@@ -78,6 +89,38 @@ func build() *jsonschema.Schema {
|
||||
Namer: func(r reflect.Type) string {
|
||||
return strings.TrimPrefix(r.Name(), "JSON")
|
||||
},
|
||||
CommentMap: make(map[string]string),
|
||||
}
|
||||
|
||||
// extract comments from Go source files to enrich schema descriptions
|
||||
//
|
||||
// note: AddGoComments parses from the module root and creates keys like "syft/pkg.TypeName",
|
||||
// but the reflector expects fully qualified paths like "github.com/anchore/syft/syft/pkg.TypeName".
|
||||
// We fix up the keys after extraction to match the expected format.
|
||||
if err := reflector.AddGoComments("github.com/anchore/syft", repoRoot); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: failed to extract Go comments: %v\n", err)
|
||||
} else {
|
||||
// fix up comment map keys to use fully qualified import paths
|
||||
// note: AddGoComments includes the absolute repo path WITHOUT the leading slash
|
||||
repoRootNoSlash := strings.TrimPrefix(repoRoot, "/")
|
||||
fixedMap := make(map[string]string)
|
||||
for k, v := range reflector.CommentMap {
|
||||
newKey := k
|
||||
if !strings.HasPrefix(k, "github.com/") {
|
||||
// key doesn't have module prefix, add it
|
||||
newKey = "github.com/anchore/syft/" + k
|
||||
} else if strings.Contains(k, repoRootNoSlash) {
|
||||
// key has the absolute repo path embedded, strip it
|
||||
// format: github.com/anchore/syft/Users/wagoodman/code/syft-manual/syft/pkg.Type
|
||||
// should be: github.com/anchore/syft/syft/pkg.Type
|
||||
newKey = strings.Replace(k, repoRootNoSlash+"/", "", 1)
|
||||
}
|
||||
fixedMap[newKey] = v
|
||||
}
|
||||
reflector.CommentMap = fixedMap
|
||||
|
||||
// copy field comments for type aliases (e.g., type RpmArchive RpmDBEntry)
|
||||
copyAliasFieldComments(reflector.CommentMap, repoRoot)
|
||||
}
|
||||
|
||||
pkgMetadataContainer, pkgMetadataMapping := assembleTypeContainer(packagemetadata.AllTypes())
|
||||
@@ -130,6 +173,9 @@ func build() *jsonschema.Schema {
|
||||
"anyOf": metadataTypes,
|
||||
})
|
||||
|
||||
// warn about missing descriptions
|
||||
warnMissingDescriptions(documentSchema, metadataNames)
|
||||
|
||||
return documentSchema
|
||||
}
|
||||
|
||||
@@ -148,11 +194,6 @@ func encode(schema *jsonschema.Schema) []byte {
|
||||
}
|
||||
|
||||
func write(schema []byte) {
|
||||
repoRoot, err := packagemetadata.RepoRoot()
|
||||
if err != nil {
|
||||
fmt.Println("unable to determine repo root")
|
||||
os.Exit(1)
|
||||
}
|
||||
schemaPath := filepath.Join(repoRoot, "schema", "json", fmt.Sprintf("schema-%s.json", internal.JSONSchemaVersion))
|
||||
latestSchemaPath := filepath.Join(repoRoot, "schema", "json", "schema-latest.json")
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@ var knownNonMetadataTypeNames = strset.New(
|
||||
|
||||
// these are names that would be removed due to common convention (e.g. used within another metadata type) but are
|
||||
// known to be metadata types themselves. Adding to this list will prevent the removal of the type from the schema.
|
||||
var knownMetadaTypeNames = strset.New(
|
||||
var knownMetadataTypeNames = strset.New(
|
||||
"DotnetPortableExecutableEntry",
|
||||
)
|
||||
|
||||
@@ -72,7 +72,7 @@ func findMetadataDefinitionNames(paths ...string) ([]string, error) {
|
||||
}
|
||||
|
||||
// any definition that is used within another struct should not be considered a top-level metadata definition
|
||||
removeNames := strset.Difference(usedNames, knownMetadaTypeNames)
|
||||
removeNames := strset.Difference(usedNames, knownMetadataTypeNames)
|
||||
names.Remove(removeNames.List()...)
|
||||
|
||||
// remove known exceptions, that is, types exported in the pkg Package that are not used
|
||||
+2
-2
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/dave/jennifer/jen"
|
||||
|
||||
"github.com/anchore/syft/syft/internal/packagemetadata"
|
||||
"github.com/anchore/syft/internal/packagemetadata"
|
||||
)
|
||||
|
||||
// This program is invoked from syft/internal and generates packagemetadata/generated.go
|
||||
@@ -31,7 +31,7 @@ func main() {
|
||||
fmt.Printf("updating package metadata type list with %+v types\n", len(typeNames))
|
||||
|
||||
f := jen.NewFile("packagemetadata")
|
||||
f.HeaderComment("DO NOT EDIT: generated by syft/internal/packagemetadata/generate/main.go")
|
||||
f.HeaderComment("DO NOT EDIT: generated by internal/packagemetadata/generate/main.go")
|
||||
f.ImportName(pkgImport, "pkg")
|
||||
f.Comment("AllTypes returns a list of all pkg metadata types that syft supports (that are represented in the pkg.Package.Metadata field).")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// DO NOT EDIT: generated by syft/internal/packagemetadata/generate/main.go
|
||||
// DO NOT EDIT: generated by internal/packagemetadata/generate/main.go
|
||||
|
||||
package packagemetadata
|
||||
|
||||
@@ -61,6 +61,7 @@ func AllTypes() []any {
|
||||
pkg.RubyGemspec{},
|
||||
pkg.RustBinaryAuditEntry{},
|
||||
pkg.RustCargoLockEntry{},
|
||||
pkg.SnapEntry{},
|
||||
pkg.SwiftPackageManagerResolvedEntry{},
|
||||
pkg.SwiplPackEntry{},
|
||||
pkg.TerraformLockProviderEntry{},
|
||||
@@ -115,6 +115,7 @@ var jsonTypes = makeJSONTypes(
|
||||
jsonNames(pkg.OpamPackage{}, "opam-package"),
|
||||
jsonNames(pkg.RustCargoLockEntry{}, "rust-cargo-lock-entry", "RustCargoPackageMetadata"),
|
||||
jsonNamesWithoutLookup(pkg.RustBinaryAuditEntry{}, "rust-cargo-audit-entry", "RustCargoPackageMetadata"), // the legacy value is split into two types, where the other is preferred
|
||||
jsonNames(pkg.SnapEntry{}, "snap-entry"),
|
||||
jsonNames(pkg.WordpressPluginEntry{}, "wordpress-plugin-entry", "WordpressMetadata"),
|
||||
jsonNames(pkg.HomebrewFormula{}, "homebrew-formula"),
|
||||
jsonNames(pkg.LuaRocksPackage{}, "luarocks-package"),
|
||||
+2
-2
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/dave/jennifer/jen"
|
||||
|
||||
"github.com/anchore/syft/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/internal/sourcemetadata"
|
||||
)
|
||||
|
||||
// This program is invoked from syft/internal and generates sourcemetadata/generated.go
|
||||
@@ -25,7 +25,7 @@ func main() {
|
||||
fmt.Printf("updating source metadata type list with %+v types\n", len(typeNames))
|
||||
|
||||
f := jen.NewFile("sourcemetadata")
|
||||
f.HeaderComment("DO NOT EDIT: generated by syft/internal/sourcemetadata/generate/main.go")
|
||||
f.HeaderComment("DO NOT EDIT: generated by internal/sourcemetadata/generate/main.go")
|
||||
f.ImportName(srcImport, "source")
|
||||
f.Comment("AllTypes returns a list of all source metadata types that syft supports (that are represented in the source.Description.Metadata field).")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// DO NOT EDIT: generated by syft/internal/sourcemetadata/generate/main.go
|
||||
// DO NOT EDIT: generated by internal/sourcemetadata/generate/main.go
|
||||
|
||||
package sourcemetadata
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/ruby"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/rust"
|
||||
sbomCataloger "github.com/anchore/syft/syft/pkg/cataloger/sbom"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/snap"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/swift"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/swipl"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/terraform"
|
||||
@@ -173,6 +174,7 @@ func DefaultPackageTaskFactories() Factories {
|
||||
newSimplePackageTaskFactory(terraform.NewLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "terraform"),
|
||||
newSimplePackageTaskFactory(homebrew.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "homebrew"),
|
||||
newSimplePackageTaskFactory(conda.NewCondaMetaCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.PackageTag, "conda"),
|
||||
newSimplePackageTaskFactory(snap.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "snap"),
|
||||
|
||||
// deprecated catalogers ////////////////////////////////////////
|
||||
// these are catalogers that should not be selectable other than specific inclusion via name or "deprecated" tag (to remain backwards compatible)
|
||||
|
||||
@@ -13,6 +13,6 @@ prior spec version (e.g. if updating to 1.7, compare the files in this directory
|
||||
equivalents).
|
||||
|
||||
One can also update the schemas and observe the errors in order to make the necessary updates.
|
||||
At the time of writing, the cyclonedx.xsd needed modifications to link to the local spdx.xsd,
|
||||
and also to changes the minOccurs for a license tag to 0. (The json schema does not require
|
||||
At the time of writing, the cyclonedx.xsd needed modifications to link to the local spdx.xsd,
|
||||
and also to change the minOccurs for a license tag to 0. (The json schema does not require
|
||||
modification for the generated file to lint properly, but can simply be copy/pasted).
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1233
-599
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/scylladb/go-set/strset"
|
||||
)
|
||||
|
||||
// CoordinateSet provides a unique collection of Coordinates with set operations.
|
||||
type CoordinateSet struct {
|
||||
set map[Coordinates]struct{}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,11 @@ import (
|
||||
|
||||
// Coordinates contains the minimal information needed to describe how to find a file within any possible source object (e.g. image and directory sources)
|
||||
type Coordinates struct {
|
||||
RealPath string `json:"path" cyclonedx:"path"` // The path where all path ancestors have no hardlinks / symlinks
|
||||
FileSystemID string `json:"layerID,omitempty" cyclonedx:"layerID"` // An ID representing the filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank.
|
||||
// RealPath is the canonical absolute form of the path accessed (all symbolic links have been followed and relative path components like '.' and '..' have been removed).
|
||||
RealPath string `json:"path" cyclonedx:"path"`
|
||||
|
||||
// FileSystemID is an ID representing and entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank.
|
||||
FileSystemID string `json:"layerID,omitempty" cyclonedx:"layerID"`
|
||||
}
|
||||
|
||||
func NewCoordinates(realPath, fsID string) Coordinates {
|
||||
|
||||
+5
-1
@@ -1,6 +1,10 @@
|
||||
package file
|
||||
|
||||
// Digest represents a cryptographic hash of file contents.
|
||||
type Digest struct {
|
||||
// Algorithm specifies the hash algorithm used (e.g., "sha256", "md5").
|
||||
Algorithm string `json:"algorithm"`
|
||||
Value string `json:"value"`
|
||||
|
||||
// Value is the hexadecimal string representation of the hash.
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
+36
-16
@@ -1,40 +1,60 @@
|
||||
package file
|
||||
|
||||
type (
|
||||
ExecutableFormat string
|
||||
// ExecutableFormat represents the binary executable format type.
|
||||
ExecutableFormat string
|
||||
|
||||
// RelocationReadOnly indicates the RELRO security protection level applied to an ELF binary.
|
||||
RelocationReadOnly string
|
||||
)
|
||||
|
||||
const (
|
||||
ELF ExecutableFormat = "elf"
|
||||
MachO ExecutableFormat = "macho"
|
||||
PE ExecutableFormat = "pe"
|
||||
ELF ExecutableFormat = "elf" // Executable and Linkable Format used on Unix-like systems
|
||||
MachO ExecutableFormat = "macho" // Mach object file format used on macOS and iOS
|
||||
PE ExecutableFormat = "pe" // Portable Executable format used on Windows
|
||||
|
||||
RelocationReadOnlyNone RelocationReadOnly = "none"
|
||||
RelocationReadOnlyPartial RelocationReadOnly = "partial"
|
||||
RelocationReadOnlyFull RelocationReadOnly = "full"
|
||||
RelocationReadOnlyNone RelocationReadOnly = "none" // no RELRO protection
|
||||
RelocationReadOnlyPartial RelocationReadOnly = "partial" // partial RELRO protection
|
||||
RelocationReadOnlyFull RelocationReadOnly = "full" // full RELRO protection
|
||||
)
|
||||
|
||||
// Executable contains metadata about binary files and their security features.
|
||||
type Executable struct {
|
||||
// Format denotes either ELF, Mach-O, or PE
|
||||
Format ExecutableFormat `json:"format" yaml:"format" mapstructure:"format"`
|
||||
|
||||
HasExports bool `json:"hasExports" yaml:"hasExports" mapstructure:"hasExports"`
|
||||
HasEntrypoint bool `json:"hasEntrypoint" yaml:"hasEntrypoint" mapstructure:"hasEntrypoint"`
|
||||
ImportedLibraries []string `json:"importedLibraries" yaml:"importedLibraries" mapstructure:"importedLibraries"`
|
||||
// HasExports indicates whether the binary exports symbols.
|
||||
HasExports bool `json:"hasExports" yaml:"hasExports" mapstructure:"hasExports"`
|
||||
|
||||
// HasEntrypoint indicates whether the binary has an entry point function.
|
||||
HasEntrypoint bool `json:"hasEntrypoint" yaml:"hasEntrypoint" mapstructure:"hasEntrypoint"`
|
||||
|
||||
// ImportedLibraries lists the shared libraries required by this executable.
|
||||
ImportedLibraries []string `json:"importedLibraries" yaml:"importedLibraries" mapstructure:"importedLibraries"`
|
||||
|
||||
// ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF.
|
||||
ELFSecurityFeatures *ELFSecurityFeatures `json:"elfSecurityFeatures,omitempty" yaml:"elfSecurityFeatures" mapstructure:"elfSecurityFeatures"`
|
||||
}
|
||||
|
||||
// ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries.
|
||||
type ELFSecurityFeatures struct {
|
||||
// SymbolTableStripped indicates whether debugging symbols have been removed.
|
||||
SymbolTableStripped bool `json:"symbolTableStripped" yaml:"symbolTableStripped" mapstructure:"symbolTableStripped"`
|
||||
|
||||
// classic protections
|
||||
// StackCanary indicates whether stack smashing protection is enabled.
|
||||
StackCanary *bool `json:"stackCanary,omitempty" yaml:"stackCanary" mapstructure:"stackCanary"`
|
||||
|
||||
StackCanary *bool `json:"stackCanary,omitempty" yaml:"stackCanary" mapstructure:"stackCanary"`
|
||||
NoExecutable bool `json:"nx" yaml:"nx" mapstructure:"nx"`
|
||||
RelocationReadOnly RelocationReadOnly `json:"relRO" yaml:"relRO" mapstructure:"relRO"`
|
||||
PositionIndependentExecutable bool `json:"pie" yaml:"pie" mapstructure:"pie"`
|
||||
DynamicSharedObject bool `json:"dso" yaml:"dso" mapstructure:"dso"`
|
||||
// NoExecutable indicates whether NX (no-execute) protection is enabled for the stack.
|
||||
NoExecutable bool `json:"nx" yaml:"nx" mapstructure:"nx"`
|
||||
|
||||
// RelocationReadOnly indicates the RELRO protection level.
|
||||
RelocationReadOnly RelocationReadOnly `json:"relRO" yaml:"relRO" mapstructure:"relRO"`
|
||||
|
||||
// PositionIndependentExecutable indicates whether the binary is compiled as PIE.
|
||||
PositionIndependentExecutable bool `json:"pie" yaml:"pie" mapstructure:"pie"`
|
||||
|
||||
// DynamicSharedObject indicates whether the binary is a shared library.
|
||||
DynamicSharedObject bool `json:"dso" yaml:"dso" mapstructure:"dso"`
|
||||
|
||||
// LlvmSafeStack represents a compiler-based security mechanism that separates the stack into a safe stack for storing return addresses and other critical data, and an unsafe stack for everything else, to mitigate stack-based memory corruption errors
|
||||
// see https://clang.llvm.org/docs/SafeStack.html
|
||||
|
||||
+22
-7
@@ -5,18 +5,33 @@ import (
|
||||
"github.com/anchore/syft/syft/license"
|
||||
)
|
||||
|
||||
// License represents license information discovered within a file.
|
||||
type License struct {
|
||||
Value string
|
||||
SPDXExpression string
|
||||
Type license.Type
|
||||
LicenseEvidence *LicenseEvidence // evidence from license classifier
|
||||
Contents string `hash:"ignore"`
|
||||
// Value is the raw license string as found in the file.
|
||||
Value string
|
||||
|
||||
// SPDXExpression is the parsed SPDX license expression if available.
|
||||
SPDXExpression string
|
||||
|
||||
// Type categorizes how the license was determined (e.g., declared, concluded -- following the same semantics as SPDX).
|
||||
Type license.Type
|
||||
|
||||
LicenseEvidence *LicenseEvidence
|
||||
|
||||
// Contents optionally stores the full license text.
|
||||
Contents string `hash:"ignore"`
|
||||
}
|
||||
|
||||
// LicenseEvidence contains details from license classifier analysis.
|
||||
type LicenseEvidence struct {
|
||||
// Confidence is a score indicating certainty of the license match.
|
||||
Confidence int
|
||||
Offset int
|
||||
Extent int
|
||||
|
||||
// Offset is the byte position where the license text begins in the file.
|
||||
Offset int
|
||||
|
||||
// Extent is the length in bytes of the matched license text.
|
||||
Extent int
|
||||
}
|
||||
|
||||
func NewLicense(value string) License {
|
||||
|
||||
@@ -27,18 +27,24 @@ type Location struct {
|
||||
LocationMetadata `cyclonedx:""`
|
||||
}
|
||||
|
||||
// LocationData contains the core identifying information for a file location.
|
||||
type LocationData struct {
|
||||
Coordinates `cyclonedx:""` // Empty string here means there is no intermediate property name, e.g. syft:locations:0:path without "coordinates"
|
||||
// note: it is IMPORTANT to ignore anything but the coordinates for a Location when considering the ID (hash value)
|
||||
// since the coordinates are the minimally correct ID for a location (symlinks should not come into play)
|
||||
AccessPath string `hash:"ignore" json:"accessPath"` // The path to the file which may or may not have hardlinks / symlinks
|
||||
ref file.Reference `hash:"ignore"` // The file reference relative to the stereoscope.FileCatalog that has more information about this location.
|
||||
|
||||
// AccessPath is the path used to retrieve file contents (which may or may not have hardlinks / symlinks in the path)
|
||||
AccessPath string `hash:"ignore" json:"accessPath"`
|
||||
|
||||
// ref is the stereoscope file reference relative to the stereoscope.FileCatalog that has more information about this location.
|
||||
ref file.Reference `hash:"ignore"`
|
||||
}
|
||||
|
||||
func (l LocationData) Reference() file.Reference {
|
||||
return l.ref
|
||||
}
|
||||
|
||||
// LocationMetadata provides additional contextual information about a file location.
|
||||
type LocationMetadata struct {
|
||||
Annotations map[string]string `json:"annotations,omitempty"` // Arbitrary key-value pairs that can be used to annotate a location
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package file
|
||||
|
||||
import "io"
|
||||
|
||||
// LocationReadCloser combines a Location with a ReadCloser for accessing file content with location metadata.
|
||||
type LocationReadCloser struct {
|
||||
Location
|
||||
io.ReadCloser
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
// LocationSet provides a unique collection of Locations with metadata and set operations.
|
||||
type LocationSet struct {
|
||||
set map[LocationData]LocationMetadata
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
var locationSorterWithoutLayers = LocationSorter(nil)
|
||||
|
||||
// Locations is a sortable slice of Location values.
|
||||
type Locations []Location
|
||||
|
||||
func (l Locations) Len() int {
|
||||
|
||||
@@ -18,6 +18,7 @@ type ContentResolver interface {
|
||||
FileContentsByLocation(Location) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
// MetadataResolver provides file metadata lookup by location.
|
||||
type MetadataResolver interface {
|
||||
FileMetadataByLocation(Location) (Metadata, error)
|
||||
}
|
||||
@@ -51,6 +52,7 @@ type PathResolver interface {
|
||||
RelativeFileByPath(_ Location, path string) *Location
|
||||
}
|
||||
|
||||
// LocationResolver provides iteration over all file locations in a source.
|
||||
type LocationResolver interface {
|
||||
// AllLocations returns a channel of all file references from the underlying source.
|
||||
// The implementation for this may vary, however, generally the following considerations should be made:
|
||||
@@ -59,6 +61,7 @@ type LocationResolver interface {
|
||||
AllLocations(ctx context.Context) <-chan Location
|
||||
}
|
||||
|
||||
// WritableResolver extends Resolver with the ability to write file content.
|
||||
type WritableResolver interface {
|
||||
Resolver
|
||||
|
||||
|
||||
@@ -4,13 +4,25 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SearchResult represents a match found during content scanning, such as secret detection.
|
||||
type SearchResult struct {
|
||||
// Classification identifies the type or category of the matched content.
|
||||
Classification string `json:"classification"`
|
||||
LineNumber int64 `json:"lineNumber"`
|
||||
LineOffset int64 `json:"lineOffset"`
|
||||
SeekPosition int64 `json:"seekPosition"`
|
||||
Length int64 `json:"length"`
|
||||
Value string `json:"value,omitempty"`
|
||||
|
||||
// LineNumber is the 1-indexed line number where the match was found.
|
||||
LineNumber int64 `json:"lineNumber"`
|
||||
|
||||
// LineOffset is the character offset from the start of the line where the match begins.
|
||||
LineOffset int64 `json:"lineOffset"`
|
||||
|
||||
// SeekPosition is the absolute byte offset from the start of the file.
|
||||
SeekPosition int64 `json:"seekPosition"`
|
||||
|
||||
// Length is the size in bytes of the matched content.
|
||||
Length int64 `json:"length"`
|
||||
|
||||
// Value optionally contains the actual matched content.
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (s SearchResult) String() string {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package file
|
||||
|
||||
const (
|
||||
NoFilesSelection Selection = "none"
|
||||
FilesOwnedByPackageSelection Selection = "owned-by-package"
|
||||
AllFilesSelection Selection = "all"
|
||||
NoFilesSelection Selection = "none" // no files are selected
|
||||
FilesOwnedByPackageSelection Selection = "owned-by-package" // only files owned by packages are selected
|
||||
AllFilesSelection Selection = "all" // all files are selected
|
||||
)
|
||||
|
||||
// Selection defines which files should be included during cataloging operations.
|
||||
type Selection string
|
||||
|
||||
@@ -15,10 +15,10 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/internal/relationship"
|
||||
"github.com/anchore/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/format/internal/spdxutil/helpers"
|
||||
"github.com/anchore/syft/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/internal/packagemetadata"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/format/internal"
|
||||
"github.com/anchore/syft/syft/internal/packagemetadata"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/internal/packagemetadata"
|
||||
"github.com/anchore/syft/internal/packagemetadata"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
@@ -47,6 +47,7 @@ func Test_OriginatorSupplier(t *testing.T) {
|
||||
pkg.PythonUvLockEntry{},
|
||||
pkg.RustBinaryAuditEntry{},
|
||||
pkg.RustCargoLockEntry{},
|
||||
pkg.SnapEntry{},
|
||||
pkg.SwiftPackageManagerResolvedEntry{},
|
||||
pkg.SwiplPackEntry{},
|
||||
pkg.OpamPackage{},
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/packagemetadata"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/packagemetadata"
|
||||
"github.com/anchore/syft/syft/license"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/iancoleman/strcase"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/internal/packagemetadata"
|
||||
"github.com/anchore/syft/internal/packagemetadata"
|
||||
)
|
||||
|
||||
type schema struct {
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/packagemetadata"
|
||||
"github.com/anchore/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
formatInternal "github.com/anchore/syft/syft/format/internal"
|
||||
"github.com/anchore/syft/syft/format/syftjson/model"
|
||||
"github.com/anchore/syft/syft/internal/packagemetadata"
|
||||
"github.com/anchore/syft/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/format/syftjson/model"
|
||||
"github.com/anchore/syft/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
stereoFile "github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/format/syftjson/model"
|
||||
"github.com/anchore/syft/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/anchore/syft/syft/source/sourceproviders"
|
||||
)
|
||||
|
||||
@@ -1 +1 @@
|
||||
this mimics a partial layout on a linux system within /sys/devices/virtual to help with ensuring globbing for files does not end up in a infinite loop
|
||||
This mimics a partial layout on a Linux system within /sys/devices/virtual to help ensure globbing for files does not end up in an infinite loop.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Please see [schema/json/README.md](../../../schema/json/README.md) for more information on the JSON schema files in this directory.
|
||||
+64
-22
@@ -13,32 +13,74 @@ var _ FileOwner = (*AlpmDBEntry)(nil)
|
||||
|
||||
const AlpmDBGlob = "**/var/lib/pacman/local/**/desc"
|
||||
|
||||
// AlpmDBEntry is a struct that represents the package data stored in the pacman fla-filet stores for arch linux.
|
||||
// AlpmDBEntry is a struct that represents the package data stored in the pacman flat-file stores for arch linux.
|
||||
type AlpmDBEntry struct {
|
||||
BasePackage string `mapstructure:"base" json:"basepackage" cyclonedx:"basepackage"`
|
||||
Package string `mapstructure:"name" json:"package" cyclonedx:"package"`
|
||||
Version string `mapstructure:"version" json:"version" cyclonedx:"version"`
|
||||
Description string `mapstructure:"desc" json:"description" cyclonedx:"description"`
|
||||
Architecture string `mapstructure:"arch" json:"architecture" cyclonedx:"architecture"`
|
||||
Size int `mapstructure:"size" json:"size" cyclonedx:"size"`
|
||||
Packager string `mapstructure:"packager" json:"packager"`
|
||||
URL string `mapstructure:"url" json:"url"`
|
||||
Validation string `mapstructure:"validation" json:"validation"`
|
||||
Reason int `mapstructure:"reason" json:"reason"`
|
||||
Files []AlpmFileRecord `mapstructure:"files" json:"files"`
|
||||
Backup []AlpmFileRecord `mapstructure:"backup" json:"backup"`
|
||||
Provides []string `mapstructure:"provides" json:"provides,omitempty"`
|
||||
Depends []string `mapstructure:"depends" json:"depends,omitempty"`
|
||||
// BasePackage is the base package name this package was built from (source package in Arch build system)
|
||||
BasePackage string `mapstructure:"base" json:"basepackage" cyclonedx:"basepackage"`
|
||||
|
||||
// Package is the package name as found in the desc file
|
||||
Package string `mapstructure:"name" json:"package" cyclonedx:"package"`
|
||||
|
||||
// Version is the package version as found in the desc file
|
||||
Version string `mapstructure:"version" json:"version" cyclonedx:"version"`
|
||||
|
||||
// Description is a human-readable package description
|
||||
Description string `mapstructure:"desc" json:"description" cyclonedx:"description"`
|
||||
|
||||
// Architecture is the target CPU architecture as defined in Arch architecture spec (e.g. x86_64, aarch64, or "any" for arch-independent packages)
|
||||
Architecture string `mapstructure:"arch" json:"architecture" cyclonedx:"architecture"`
|
||||
|
||||
// Size is the installed size in bytes
|
||||
Size int `mapstructure:"size" json:"size" cyclonedx:"size"`
|
||||
|
||||
// Packager is the name and email of the person who packaged this (RFC822 format)
|
||||
Packager string `mapstructure:"packager" json:"packager"`
|
||||
|
||||
// URL is the upstream project URL
|
||||
URL string `mapstructure:"url" json:"url"`
|
||||
|
||||
// Validation is the validation method used for package integrity (e.g. pgp signature, sha256 checksum)
|
||||
Validation string `mapstructure:"validation" json:"validation"`
|
||||
|
||||
// Reason is the installation reason tracked by pacman (0=explicitly installed by user, 1=installed as dependency)
|
||||
Reason int `mapstructure:"reason" json:"reason"`
|
||||
|
||||
// Files are the files installed by this package
|
||||
Files []AlpmFileRecord `mapstructure:"files" json:"files"`
|
||||
|
||||
// Backup is the list of configuration files that pacman backs up before upgrades
|
||||
Backup []AlpmFileRecord `mapstructure:"backup" json:"backup"`
|
||||
|
||||
// Provides are virtual packages provided by this package (allows other packages to depend on capabilities rather than specific packages)
|
||||
Provides []string `mapstructure:"provides" json:"provides,omitempty"`
|
||||
|
||||
// Depends are the runtime dependencies required by this package
|
||||
Depends []string `mapstructure:"depends" json:"depends,omitempty"`
|
||||
}
|
||||
|
||||
type AlpmFileRecord struct {
|
||||
Path string `mapstruture:"path" json:"path,omitempty"`
|
||||
Type string `mapstructure:"type" json:"type,omitempty"`
|
||||
UID string `mapstructure:"uid" json:"uid,omitempty"`
|
||||
GID string `mapstructure:"gid" json:"gid,omitempty"`
|
||||
Time time.Time `mapstructure:"time" json:"time,omitempty"`
|
||||
Size string `mapstructure:"size" json:"size,omitempty"`
|
||||
Link string `mapstructure:"link" json:"link,omitempty"`
|
||||
// Path is the file path relative to the filesystem root
|
||||
Path string `mapstruture:"path" json:"path,omitempty"`
|
||||
|
||||
// Type is the file type (e.g. regular file, directory, symlink)
|
||||
Type string `mapstructure:"type" json:"type,omitempty"`
|
||||
|
||||
// UID is the file owner user ID as recorded by pacman
|
||||
UID string `mapstructure:"uid" json:"uid,omitempty"`
|
||||
|
||||
// GID is the file owner group ID as recorded by pacman
|
||||
GID string `mapstructure:"gid" json:"gid,omitempty"`
|
||||
|
||||
// Time is the file modification timestamp
|
||||
Time time.Time `mapstructure:"time" json:"time,omitempty"`
|
||||
|
||||
// Size is the file size in bytes
|
||||
Size string `mapstructure:"size" json:"size,omitempty"`
|
||||
|
||||
// Link is the symlink target path if this is a symlink
|
||||
Link string `mapstructure:"link" json:"link,omitempty"`
|
||||
|
||||
// Digests contains file content hashes for integrity verification
|
||||
Digests []file.Digest `mapstructure:"digests" json:"digest,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
+56
-19
@@ -23,22 +23,50 @@ var _ FileOwner = (*ApkDBEntry)(nil)
|
||||
// - https://git.alpinelinux.org/apk-tools/tree/src/package.c
|
||||
// - https://git.alpinelinux.org/apk-tools/tree/src/database.c
|
||||
type ApkDBEntry struct {
|
||||
Package string `mapstructure:"P" json:"package"`
|
||||
OriginPackage string `mapstructure:"o" json:"originPackage" cyclonedx:"originPackage"`
|
||||
Maintainer string `mapstructure:"m" json:"maintainer"`
|
||||
Version string `mapstructure:"V" json:"version"`
|
||||
Architecture string `mapstructure:"A" json:"architecture"`
|
||||
URL string `mapstructure:"U" json:"url"`
|
||||
Description string `mapstructure:"T" json:"description"`
|
||||
Size int `mapstructure:"S" json:"size" cyclonedx:"size"`
|
||||
InstalledSize int `mapstructure:"I" json:"installedSize" cyclonedx:"installedSize"`
|
||||
Dependencies []string `mapstructure:"D" json:"pullDependencies" cyclonedx:"pullDependencies"`
|
||||
Provides []string `mapstructure:"p" json:"provides" cyclonedx:"provides"`
|
||||
Checksum string `mapstructure:"C" json:"pullChecksum" cyclonedx:"pullChecksum"`
|
||||
GitCommit string `mapstructure:"c" json:"gitCommitOfApkPort" cyclonedx:"gitCommitOfApkPort"`
|
||||
Files []ApkFileRecord `json:"files"`
|
||||
// Package is the package name as found in the installed file
|
||||
Package string `mapstructure:"P" json:"package"`
|
||||
|
||||
// OriginPackage is the original source package name this binary was built from (used to track which aport/source built this)
|
||||
OriginPackage string `mapstructure:"o" json:"originPackage" cyclonedx:"originPackage"`
|
||||
|
||||
// Maintainer is the package maintainer name and email
|
||||
Maintainer string `mapstructure:"m" json:"maintainer"`
|
||||
|
||||
// Version is the package version as found in the installed file
|
||||
Version string `mapstructure:"V" json:"version"`
|
||||
|
||||
// Architecture is the target CPU architecture
|
||||
Architecture string `mapstructure:"A" json:"architecture"`
|
||||
|
||||
// URL is the upstream project URL
|
||||
URL string `mapstructure:"U" json:"url"`
|
||||
|
||||
// Description is a human-readable package description
|
||||
Description string `mapstructure:"T" json:"description"`
|
||||
|
||||
// Size is the package archive size in bytes (.apk file size)
|
||||
Size int `mapstructure:"S" json:"size" cyclonedx:"size"`
|
||||
|
||||
// InstalledSize is the total size of installed files in bytes
|
||||
InstalledSize int `mapstructure:"I" json:"installedSize" cyclonedx:"installedSize"`
|
||||
|
||||
// Dependencies are the runtime dependencies required by this package
|
||||
Dependencies []string `mapstructure:"D" json:"pullDependencies" cyclonedx:"pullDependencies"`
|
||||
|
||||
// Provides are virtual packages provided by this package (for capability-based dependencies)
|
||||
Provides []string `mapstructure:"p" json:"provides" cyclonedx:"provides"`
|
||||
|
||||
// Checksum is the package content checksum for integrity verification
|
||||
Checksum string `mapstructure:"C" json:"pullChecksum" cyclonedx:"pullChecksum"`
|
||||
|
||||
// GitCommit is the git commit hash of the APK port definition in Alpine's aports repository
|
||||
GitCommit string `mapstructure:"c" json:"gitCommitOfApkPort" cyclonedx:"gitCommitOfApkPort"`
|
||||
|
||||
// Files are the files installed by this package
|
||||
Files []ApkFileRecord `json:"files"`
|
||||
}
|
||||
|
||||
// spaceDelimitedStringSlice is an internal helper type for unmarshaling space-delimited strings from JSON into a string slice.
|
||||
type spaceDelimitedStringSlice []string
|
||||
|
||||
func (m *ApkDBEntry) UnmarshalJSON(data []byte) error {
|
||||
@@ -95,11 +123,20 @@ func (a *spaceDelimitedStringSlice) UnmarshalJSON(data []byte) error {
|
||||
|
||||
// ApkFileRecord represents a single file listing and metadata from a APK DB entry (which may have many of these file records).
|
||||
type ApkFileRecord struct {
|
||||
Path string `json:"path"`
|
||||
OwnerUID string `json:"ownerUid,omitempty"`
|
||||
OwnerGID string `json:"ownerGid,omitempty"`
|
||||
Permissions string `json:"permissions,omitempty"`
|
||||
Digest *file.Digest `json:"digest,omitempty"`
|
||||
// Path is the file path relative to the filesystem root
|
||||
Path string `json:"path"`
|
||||
|
||||
// OwnerUID is the file owner user ID
|
||||
OwnerUID string `json:"ownerUid,omitempty"`
|
||||
|
||||
// OwnerGID is the file owner group ID
|
||||
OwnerGID string `json:"ownerGid,omitempty"`
|
||||
|
||||
// Permissions is the file permission mode string (e.g. "0755", "0644")
|
||||
Permissions string `json:"permissions,omitempty"`
|
||||
|
||||
// Digest is the file content hash for integrity verification
|
||||
Digest *file.Digest `json:"digest,omitempty"`
|
||||
}
|
||||
|
||||
func (m ApkDBEntry) OwnedFiles() (result []string) {
|
||||
|
||||
+6
-4
@@ -13,9 +13,9 @@ type ClassifierMatch struct {
|
||||
Location file.Location `mapstructure:"Location" json:"location"`
|
||||
}
|
||||
|
||||
// ELFBinaryPackageNoteJSONPayload Represents metadata captured from the .note.package section of the binary
|
||||
// ELFBinaryPackageNoteJSONPayload Represents metadata captured from the .note.package section of an ELF-formatted binary
|
||||
type ELFBinaryPackageNoteJSONPayload struct {
|
||||
// these are well-known fields as defined by systemd ELF package metadata "spec" https://systemd.io/ELF_PACKAGE_METADATA/
|
||||
// (these are well-known fields as defined by systemd ELF package metadata "spec" https://systemd.io/ELF_PACKAGE_METADATA/)
|
||||
|
||||
// Type is the type of the package (e.g. "rpm", "deb", "apk", etc.)
|
||||
Type string `json:"type,omitempty"`
|
||||
@@ -23,7 +23,7 @@ type ELFBinaryPackageNoteJSONPayload struct {
|
||||
// Architecture of the binary package (e.g. "amd64", "arm", etc.)
|
||||
Architecture string `json:"architecture,omitempty"`
|
||||
|
||||
// OS CPE is a CPE name for the OS, typically corresponding to CPE_NAME in os-release (e.g. cpe:/o:fedoraproject:fedora:33)
|
||||
// OSCPE is a CPE name for the OS, typically corresponding to CPE_NAME in os-release (e.g. cpe:/o:fedoraproject:fedora:33)
|
||||
OSCPE string `json:"osCPE,omitempty"`
|
||||
|
||||
// OS is the OS name, typically corresponding to ID in os-release (e.g. "fedora")
|
||||
@@ -32,7 +32,8 @@ type ELFBinaryPackageNoteJSONPayload struct {
|
||||
// osVersion is the version of the OS, typically corresponding to VERSION_ID in os-release (e.g. "33")
|
||||
OSVersion string `json:"osVersion,omitempty"`
|
||||
|
||||
// these are additional fields that are not part of the systemd spec
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// (these are additional fields that are not part of the systemd spec)
|
||||
|
||||
// System is a context-specific name for the system that the binary package is intended to run on or a part of
|
||||
System string `json:"system,omitempty"`
|
||||
@@ -49,5 +50,6 @@ type ELFBinaryPackageNoteJSONPayload struct {
|
||||
|
||||
// PEBinary represents metadata captured from a Portable Executable formatted binary (dll, exe, etc.)
|
||||
type PEBinary struct {
|
||||
// VersionResources contains key-value pairs extracted from the PE file's version resource section (e.g., FileVersion, ProductName, CompanyName).
|
||||
VersionResources KeyValues
|
||||
}
|
||||
|
||||
+20
-7
@@ -3,13 +3,26 @@ package pkg
|
||||
// BitnamiSBOMEntry represents all captured data from Bitnami packages
|
||||
// described in Bitnami' SPDX files.
|
||||
type BitnamiSBOMEntry struct {
|
||||
Name string `mapstructure:"name" json:"name"`
|
||||
Architecture string `mapstructure:"arch" json:"arch"`
|
||||
Distro string `mapstructure:"distro" json:"distro"`
|
||||
Revision string `mapstructure:"revision" json:"revision"`
|
||||
Version string `mapstructure:"version" json:"version"`
|
||||
Path string `mapstructure:"path" json:"path"`
|
||||
Files []string `mapstructure:"files" json:"files"`
|
||||
// Name is the package name as found in the Bitnami SPDX file
|
||||
Name string `mapstructure:"name" json:"name"`
|
||||
|
||||
// Architecture is the target CPU architecture (amd64 or arm64 in Bitnami images)
|
||||
Architecture string `mapstructure:"arch" json:"arch"`
|
||||
|
||||
// Distro is the distribution name this package is for (base OS like debian, ubuntu, etc.)
|
||||
Distro string `mapstructure:"distro" json:"distro"`
|
||||
|
||||
// Revision is the Bitnami-specific package revision number (incremented for Bitnami rebuilds of same upstream version)
|
||||
Revision string `mapstructure:"revision" json:"revision"`
|
||||
|
||||
// Version is the package version as found in the Bitnami SPDX file
|
||||
Version string `mapstructure:"version" json:"version"`
|
||||
|
||||
// Path is the installation path in the filesystem where the package is located
|
||||
Path string `mapstructure:"path" json:"path"`
|
||||
|
||||
// Files are the file paths owned by this package (tracked via SPDX relationships)
|
||||
Files []string `mapstructure:"files" json:"files"`
|
||||
}
|
||||
|
||||
func (b BitnamiSBOMEntry) OwnedFiles() (result []string) {
|
||||
|
||||
@@ -1392,6 +1392,17 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
|
||||
Metadata: metadata("ffmpeg-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "ffmpeg-shared-libs/5.1.4/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
Name: "ffmpeg",
|
||||
Version: "5.1.4",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/ffmpeg@5.1.4",
|
||||
Locations: locations("libavcodec-9aae324f.so.59.37.100"),
|
||||
Metadata: metadata("ffmpeg-library"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -639,6 +639,30 @@ func DefaultClassifiers() []binutils.Classifier {
|
||||
PURL: mustPURL("pkg:generic/ffmpeg@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:ffmpeg:ffmpeg:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
|
||||
},
|
||||
{
|
||||
Class: "ffmpeg-library",
|
||||
FileGlob: "**/libav*",
|
||||
EvidenceMatcher: binutils.MatchAny(
|
||||
// Primary pattern: FFmpeg version found in most libraries
|
||||
m.FileContentsVersionMatcher(`(?m)FFmpeg version (?P<version>[0-9]+\.[0-9]+(\.[0-9]+)?)`),
|
||||
// Fallback: library-specific version patterns for libavcodec and libavformat
|
||||
m.FileContentsVersionMatcher(`(?m)Lavc(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
m.FileContentsVersionMatcher(`(?m)Lavf(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
),
|
||||
Package: "ffmpeg",
|
||||
PURL: mustPURL("pkg:generic/ffmpeg@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:ffmpeg:ffmpeg:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
|
||||
},
|
||||
{
|
||||
Class: "ffmpeg-library",
|
||||
FileGlob: "**/libswresample*",
|
||||
EvidenceMatcher: m.FileContentsVersionMatcher(
|
||||
// FFmpeg version pattern for libswresample
|
||||
`(?m)FFmpeg version (?P<version>[0-9]+\.[0-9]+(\.[0-9]+)?)`),
|
||||
Package: "ffmpeg",
|
||||
PURL: mustPURL("pkg:generic/ffmpeg@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:ffmpeg:ffmpeg:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
|
||||
},
|
||||
}
|
||||
|
||||
return append(classifiers, defaultJavaClassifiers()...)
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||
)
|
||||
|
||||
func Test_JavaBinaryImage(t *testing.T) {
|
||||
// no match for platform in manifest on Apple Silicon
|
||||
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
|
||||
t.Skip("Java binary images not supported on Apple Silicon (darwin/arm64)")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
image string
|
||||
expected []string
|
||||
|
||||
BIN
Binary file not shown.
@@ -830,3 +830,12 @@ from-images:
|
||||
platform: linux/arm64
|
||||
paths:
|
||||
- /usr/local/bin/ffmpeg
|
||||
|
||||
- name: ffmpeg-shared-libs
|
||||
version: 5.1.4
|
||||
images:
|
||||
- ref: demisto/opencv:1.0.0.111908@sha256:abba3aab213990c666900a93db03420238527b757bf5825543cf7b4c5e11ac65
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/local/lib/python3.11/site-packages/opencv_contrib_python.libs/libavcodec-9aae324f.so.59.37.100
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ Contents of section .note.package:
|
||||
```
|
||||
|
||||
#### Binaries related to Libraries
|
||||
The resulting SBOM shoult show the following relationships:
|
||||
The resulting SBOM should show the following relationships:
|
||||
```
|
||||
elfbinwithnestedlib -> libhello_world.so
|
||||
elfwithparallellibbin2 -> libhello_world.so
|
||||
|
||||
@@ -2,11 +2,15 @@ package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config"
|
||||
)
|
||||
|
||||
@@ -195,3 +199,17 @@ func (e Entries) BinaryFromImageHasSnippet(cfg config.BinaryFromImage) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (e Entries) Sorted() iter.Seq2[LogicalEntryKey, EntryInfo] {
|
||||
keys := maps.Keys(e)
|
||||
slices.SortFunc(keys, func(a, b LogicalEntryKey) int {
|
||||
return strings.Compare(a.Path(), b.Path())
|
||||
})
|
||||
return func(yield func(LogicalEntryKey, EntryInfo) bool) {
|
||||
for _, k := range keys {
|
||||
if !yield(k, e[k]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func SnippetOrBinary(t *testing.T, path string, requireBinary bool) string {
|
||||
require.NoError(t, err)
|
||||
|
||||
var fixturePath string
|
||||
for k, v := range entries {
|
||||
for k, v := range entries.Sorted() {
|
||||
if filepath.Dir(k.Path()) == path {
|
||||
// prefer the snippet over the binary
|
||||
if !requireBinary {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
version "github.com/bitnami/go-version/pkg/version"
|
||||
"github.com/bitnami/go-version/pkg/version"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
|
||||
@@ -6,7 +6,7 @@ This folder contains the test data for the Conan package manager.
|
||||
|
||||
The conan lock file is created in the following way.
|
||||
|
||||
We explicitly use a package which has dependencies, which in turn also have dependendencies.
|
||||
We explicitly use a package which has dependencies, which in turn also have dependencies.
|
||||
This is necessary to verify that the dependency tree is properly parsed.
|
||||
|
||||
1. Use `conan lock create --reference "mfast/1.2.2#c6f6387c9b99780f0ee05e25f99d0f39"`
|
||||
|
||||
@@ -2,17 +2,21 @@ package dotnet
|
||||
|
||||
type CatalogerConfig struct {
|
||||
// DepPackagesMustHaveDLL allows for deps.json packages to be included only if there is a DLL on disk for that package.
|
||||
// app-config: dotnet.dep-packages-must-have-dll
|
||||
DepPackagesMustHaveDLL bool `mapstructure:"dep-packages-must-have-dll" json:"dep-packages-must-have-dll" yaml:"dep-packages-must-have-dll"`
|
||||
|
||||
// DepPackagesMustClaimDLL allows for deps.json packages to be included only if there is a runtime/resource DLL claimed in the deps.json targets section.
|
||||
// This does not require such claimed DLLs to exist on disk. The behavior of this
|
||||
// app-config: dotnet.dep-packages-must-claim-dll
|
||||
DepPackagesMustClaimDLL bool `mapstructure:"dep-packages-must-claim-dll" json:"dep-packages-must-claim-dll" yaml:"dep-packages-must-claim-dll"`
|
||||
|
||||
// PropagateDLLClaimsToParents allows for deps.json packages to be included if any child (transitive) package claims a DLL. This applies to both the claims configuration and evidence-on-disk configurations.
|
||||
// app-config: dotnet.propagate-dll-claims-to-parents
|
||||
PropagateDLLClaimsToParents bool `mapstructure:"propagate-dll-claims-to-parents" json:"propagate-dll-claims-to-parents" yaml:"propagate-dll-claims-to-parents"`
|
||||
|
||||
// RelaxDLLClaimsWhenBundlingDetected will look for indications of IL bundle tooling via deps.json package names
|
||||
// and, if found (and this config option is enabled), will relax the DepPackagesMustClaimDLL value to `false` only in those cases.
|
||||
// app-config: dotnet.relax-dll-claims-when-bundling-detected
|
||||
RelaxDLLClaimsWhenBundlingDetected bool `mapstructure:"relax-dll-claims-when-bundling-detected" json:"relax-dll-claims-when-bundling-detected" yaml:"relax-dll-claims-when-bundling-detected"`
|
||||
}
|
||||
|
||||
|
||||
@@ -19,19 +19,48 @@ var (
|
||||
)
|
||||
|
||||
type CatalogerConfig struct {
|
||||
SearchLocalModCacheLicenses bool `yaml:"search-local-mod-cache-licenses" json:"search-local-mod-cache-licenses" mapstructure:"search-local-mod-cache-licenses"`
|
||||
LocalModCacheDir string `yaml:"local-mod-cache-dir" json:"local-mod-cache-dir" mapstructure:"local-mod-cache-dir"`
|
||||
SearchLocalVendorLicenses bool `yaml:"search-local-vendor-licenses" json:"search-local-vendor-licenses" mapstructure:"search-local-vendor-licenses"`
|
||||
LocalVendorDir string `yaml:"local-vendor-dir" json:"local-vendor-dir" mapstructure:"local-vendor-dir"`
|
||||
SearchRemoteLicenses bool `yaml:"search-remote-licenses" json:"search-remote-licenses" mapstructure:"search-remote-licenses"`
|
||||
Proxies []string `yaml:"proxies,omitempty" json:"proxies,omitempty" mapstructure:"proxies"`
|
||||
NoProxy []string `yaml:"no-proxy,omitempty" json:"no-proxy,omitempty" mapstructure:"no-proxy"`
|
||||
MainModuleVersion MainModuleVersionConfig `yaml:"main-module-version" json:"main-module-version" mapstructure:"main-module-version"`
|
||||
// SearchLocalModCacheLicenses enables searching for go package licenses in the local GOPATH mod cache.
|
||||
// app-config: golang.search-local-mod-cache-licenses
|
||||
SearchLocalModCacheLicenses bool `yaml:"search-local-mod-cache-licenses" json:"search-local-mod-cache-licenses" mapstructure:"search-local-mod-cache-licenses"`
|
||||
|
||||
// LocalModCacheDir specifies the location of the local go module cache directory. When not set, syft will attempt to discover the GOPATH env or default to $HOME/go.
|
||||
// app-config: golang.local-mod-cache-dir
|
||||
LocalModCacheDir string `yaml:"local-mod-cache-dir" json:"local-mod-cache-dir" mapstructure:"local-mod-cache-dir"`
|
||||
|
||||
// SearchLocalVendorLicenses enables searching for go package licenses in the local vendor directory relative to the go.mod file.
|
||||
// app-config: golang.search-local-vendor-licenses
|
||||
SearchLocalVendorLicenses bool `yaml:"search-local-vendor-licenses" json:"search-local-vendor-licenses" mapstructure:"search-local-vendor-licenses"`
|
||||
|
||||
// LocalVendorDir specifies the location of the local vendor directory. When not set, syft will search for a vendor directory relative to the go.mod file.
|
||||
// app-config: golang.local-vendor-dir
|
||||
LocalVendorDir string `yaml:"local-vendor-dir" json:"local-vendor-dir" mapstructure:"local-vendor-dir"`
|
||||
|
||||
// SearchRemoteLicenses enables downloading go package licenses from the upstream go proxy (typically proxy.golang.org).
|
||||
// app-config: golang.search-remote-licenses
|
||||
SearchRemoteLicenses bool `yaml:"search-remote-licenses" json:"search-remote-licenses" mapstructure:"search-remote-licenses"`
|
||||
|
||||
// Proxies is a list of go module proxies to use when fetching go module metadata and licenses. When not set, syft will use the GOPROXY env or default to https://proxy.golang.org,direct.
|
||||
// app-config: golang.proxy
|
||||
Proxies []string `yaml:"proxies,omitempty" json:"proxies,omitempty" mapstructure:"proxies"`
|
||||
|
||||
// NoProxy is a list of glob patterns that match go module names that should not be fetched from the go proxy. When not set, syft will use the GOPRIVATE and GONOPROXY env vars.
|
||||
// app-config: golang.no-proxy
|
||||
NoProxy []string `yaml:"no-proxy,omitempty" json:"no-proxy,omitempty" mapstructure:"no-proxy"`
|
||||
|
||||
MainModuleVersion MainModuleVersionConfig `yaml:"main-module-version" json:"main-module-version" mapstructure:"main-module-version"`
|
||||
}
|
||||
|
||||
type MainModuleVersionConfig struct {
|
||||
FromLDFlags bool `yaml:"from-ld-flags" json:"from-ld-flags" mapstructure:"from-ld-flags"`
|
||||
FromContents bool `yaml:"from-contents" json:"from-contents" mapstructure:"from-contents"`
|
||||
// FromLDFlags enables parsing the main module version from the -ldflags build settings.
|
||||
// app-config: golang.main-module-version.from-ld-flags
|
||||
FromLDFlags bool `yaml:"from-ld-flags" json:"from-ld-flags" mapstructure:"from-ld-flags"`
|
||||
|
||||
// FromContents enables parsing the main module version from the binary contents. This is useful when the version is embedded in the binary but not in the build settings.
|
||||
// app-config: golang.main-module-version.from-contents
|
||||
FromContents bool `yaml:"from-contents" json:"from-contents" mapstructure:"from-contents"`
|
||||
|
||||
// FromBuildSettings enables parsing the main module version from the go build settings.
|
||||
// app-config: golang.main-module-version.from-build-settings
|
||||
FromBuildSettings bool `yaml:"from-build-settings" json:"from-build-settings" mapstructure:"from-build-settings"`
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@ import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
@@ -228,6 +230,11 @@ func candidateVendors(p pkg.Package) []string {
|
||||
vendors.union(candidateVendorsForWordpressPlugin(p))
|
||||
}
|
||||
|
||||
if p.Type == pkg.BinaryPkg && endsWithNumber(p.Name) {
|
||||
// add binary package digit-suffix variations (e.g. Qt5 -> Qt)
|
||||
addBinaryPackageDigitVariations(vendors)
|
||||
}
|
||||
|
||||
// We should no longer be generating vendor candidates with these values ["" and "*"]
|
||||
// (since CPEs will match any other value)
|
||||
vendors.removeByValue("")
|
||||
@@ -286,6 +293,9 @@ func candidateProductSet(p pkg.Package) fieldCandidateSet {
|
||||
if prod != "" {
|
||||
products.addValue(prod)
|
||||
}
|
||||
case p.Type == pkg.BinaryPkg && endsWithNumber(p.Name):
|
||||
// add binary package digit-suffix variations (e.g. Qt5 -> Qt)
|
||||
addBinaryPackageDigitVariations(products)
|
||||
}
|
||||
|
||||
switch p.Metadata.(type) {
|
||||
@@ -404,3 +414,33 @@ func addDelimiterVariations(fields fieldCandidateSet) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// removeTrailingDigits removes all trailing digits from a string
|
||||
func removeTrailingDigits(s string) string {
|
||||
re := regexp.MustCompile(`\d+$`)
|
||||
return re.ReplaceAllString(s, "")
|
||||
}
|
||||
|
||||
// addBinaryPackageDigitVariations adds variations with trailing digits removed for binary packages.For binary package types only, when the name ends with a digit, add a new variation with all suffix-digits removed (e.g. Qt5 -> Qt). This helps generate additional CPE permutations for better vulnerability matching.
|
||||
func addBinaryPackageDigitVariations(fields fieldCandidateSet) {
|
||||
candidatesForVariations := fields.copy()
|
||||
for _, candidate := range candidatesForVariations.values() {
|
||||
// Check if the candidate ends with a digit
|
||||
if len(candidate) > 0 && candidate[len(candidate)-1] >= '0' && candidate[len(candidate)-1] <= '9' {
|
||||
// Create variation with all suffix digits removed
|
||||
withoutDigits := removeTrailingDigits(candidate)
|
||||
if withoutDigits != "" && withoutDigits != candidate {
|
||||
fields.addValue(withoutDigits)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func endsWithNumber(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
r := []rune(s)
|
||||
last := r[len(r)-1]
|
||||
return unicode.IsDigit(last)
|
||||
}
|
||||
|
||||
@@ -1145,3 +1145,67 @@ func TestDictionaryFindIsWired(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddBinaryPackageDigitVariations tests the heuristic for binary package types
|
||||
// where names ending with digits get variations with all suffix-digits removed (e.g. Qt5 -> Qt).
|
||||
// This improves vulnerability matching for binary packages like Qt6, libfoo123, etc.
|
||||
func TestAddBinaryPackageDigitVariations(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
packageType pkg.Type
|
||||
inputCandidates []string
|
||||
expectedPresent []string // These should be present in the result
|
||||
expectedAbsent []string // These should NOT be present in the result
|
||||
}{
|
||||
{
|
||||
name: "Qt5 binary package example",
|
||||
packageType: pkg.BinaryPkg,
|
||||
inputCandidates: []string{"Qt5"},
|
||||
expectedPresent: []string{"Qt5", "Qt"},
|
||||
expectedAbsent: []string{},
|
||||
},
|
||||
{
|
||||
name: "package with trailing digits",
|
||||
packageType: pkg.BinaryPkg,
|
||||
inputCandidates: []string{"Qt5", "libfoo123", "bar42", "baz"},
|
||||
expectedPresent: []string{"Qt5", "Qt", "libfoo123", "libfoo", "bar42", "bar", "baz"},
|
||||
expectedAbsent: []string{},
|
||||
},
|
||||
{
|
||||
name: "multiple trailing digits",
|
||||
inputCandidates: []string{"Qt872", "package999"},
|
||||
expectedPresent: []string{"Qt872", "Qt", "package999", "package"},
|
||||
expectedAbsent: []string{},
|
||||
},
|
||||
{
|
||||
name: "package without trailing digits",
|
||||
inputCandidates: []string{"QtCore", "libfoo", "bar"},
|
||||
expectedPresent: []string{"QtCore", "libfoo", "bar"},
|
||||
expectedAbsent: []string{"QtCor", "libfo", "ba"},
|
||||
},
|
||||
{
|
||||
name: "empty candidate set",
|
||||
packageType: pkg.BinaryPkg,
|
||||
inputCandidates: []string{},
|
||||
expectedPresent: []string{},
|
||||
expectedAbsent: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
fields := newFieldCandidateSet(test.inputCandidates...)
|
||||
addBinaryPackageDigitVariations(fields)
|
||||
|
||||
values := fields.uniqueValues()
|
||||
|
||||
for _, expected := range test.expectedPresent {
|
||||
assert.Contains(t, values, expected, "expected %q to be present", expected)
|
||||
}
|
||||
|
||||
for _, notExpected := range test.expectedAbsent {
|
||||
assert.NotContains(t, values, notExpected, "expected %q to be absent", notExpected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,6 +336,9 @@ func (p *CatalogTester) assertPkgs(t *testing.T, pkgs []pkg.Package, relationshi
|
||||
opts = append(opts, p.compareOptions...)
|
||||
opts = append(opts, cmp.Reporter(&r))
|
||||
|
||||
// ignore the "FoundBy" field on relationships as it is set in the generic cataloger before it's presence on the relationship
|
||||
opts = append(opts, cmpopts.IgnoreFields(pkg.Package{}, "FoundBy"))
|
||||
|
||||
// order should not matter
|
||||
relationship.Sort(p.expectedRelationships)
|
||||
relationship.Sort(relationships)
|
||||
|
||||
@@ -257,10 +257,14 @@ func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name, version, lics, err := j.discoverNameVersionLicense(ctx, manifest)
|
||||
name, version, lics, parsedPom, err := j.discoverNameVersionLicense(ctx, manifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pkgPomProject *pkg.JavaPomProject
|
||||
if parsedPom != nil {
|
||||
pkgPomProject = newPomProject(ctx, nil, parsedPom.path, parsedPom.project)
|
||||
}
|
||||
|
||||
return &pkg.Package{
|
||||
// TODO: maybe select name should just have a pom properties in it?
|
||||
@@ -275,12 +279,13 @@ func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package,
|
||||
Metadata: pkg.JavaArchive{
|
||||
VirtualPath: j.location.Path(),
|
||||
Manifest: manifest,
|
||||
PomProject: pkgPomProject,
|
||||
ArchiveDigests: digests,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (j *archiveParser) discoverNameVersionLicense(ctx context.Context, manifest *pkg.JavaManifest) (string, string, []pkg.License, error) {
|
||||
func (j *archiveParser) discoverNameVersionLicense(ctx context.Context, manifest *pkg.JavaManifest) (string, string, []pkg.License, *parsedPomProject, error) {
|
||||
// we use j.location because we want to associate the license declaration with where we discovered the contents in the manifest
|
||||
// TODO: when we support locations of paths within archives we should start passing the specific manifest location object instead of the top jar
|
||||
lics := pkg.NewLicensesFromLocationWithContext(ctx, j.location, selectLicenses(manifest)...)
|
||||
@@ -302,7 +307,7 @@ func (j *archiveParser) discoverNameVersionLicense(ctx context.Context, manifest
|
||||
if len(lics) == 0 {
|
||||
fileLicenses, err := j.getLicenseFromFileInArchive(ctx)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
return "", "", nil, parsedPom, err
|
||||
}
|
||||
if fileLicenses != nil {
|
||||
lics = append(lics, fileLicenses...)
|
||||
@@ -317,7 +322,7 @@ func (j *archiveParser) discoverNameVersionLicense(ctx context.Context, manifest
|
||||
lics = j.findLicenseFromJavaMetadata(ctx, groupID, artifactID, version, parsedPom, manifest)
|
||||
}
|
||||
|
||||
return artifactID, version, lics, nil
|
||||
return artifactID, version, lics, parsedPom, nil
|
||||
}
|
||||
|
||||
// findLicenseFromJavaMetadata attempts to find license information from all available maven metadata properties and pom info
|
||||
@@ -387,43 +392,93 @@ type parsedPomProject struct {
|
||||
|
||||
// discoverMainPackageFromPomInfo attempts to resolve maven groupId, artifactId, version and other info from found pom information
|
||||
func (j *archiveParser) discoverMainPackageFromPomInfo(ctx context.Context) (group, name, version string, parsedPom *parsedPomProject) {
|
||||
var pomProperties pkg.JavaPomProperties
|
||||
|
||||
// Find the pom.properties/pom.xml if the names seem like a plausible match
|
||||
properties, _ := pomPropertiesByParentPath(j.archivePath, j.location, j.fileManifest.GlobMatch(false, pomPropertiesGlob))
|
||||
projects, _ := pomProjectByParentPath(j.archivePath, j.location, j.fileManifest.GlobMatch(false, pomXMLGlob))
|
||||
|
||||
// map of all the artifacts in the pom properties, in order to chek exact match with the filename
|
||||
artifactsMap := j.buildArtifactsMap(properties)
|
||||
pomProperties, parsedPom := j.findBestPomMatch(properties, projects, artifactsMap)
|
||||
|
||||
parsedPom = j.handleSinglePomXML(properties, projects, parsedPom)
|
||||
|
||||
return j.resolveIdentity(ctx, pomProperties, parsedPom)
|
||||
}
|
||||
|
||||
func (j *archiveParser) buildArtifactsMap(properties map[string]pkg.JavaPomProperties) *strset.Set {
|
||||
artifactsMap := strset.New()
|
||||
for _, propertiesObj := range properties {
|
||||
artifactsMap.Add(propertiesObj.ArtifactID)
|
||||
}
|
||||
return artifactsMap
|
||||
}
|
||||
|
||||
func (j *archiveParser) findBestPomMatch(properties map[string]pkg.JavaPomProperties,
|
||||
projects map[string]*parsedPomProject, artifactsMap *strset.Set) (pkg.JavaPomProperties, *parsedPomProject) {
|
||||
var pomProperties pkg.JavaPomProperties
|
||||
var parsedPom *parsedPomProject
|
||||
|
||||
for parentPath, propertiesObj := range sortedIter(properties) {
|
||||
// the logic for selecting the best name is as follows:
|
||||
// if we find an artifact id AND group id which are both contained in the filename
|
||||
// OR if we have an artifact id that exactly matches the filename, prefer this
|
||||
// OTHERWISE track the first matching pom properties with a pom.xml
|
||||
// FINALLY return the first matching pom properties
|
||||
if artifactIDMatchesFilename(propertiesObj.ArtifactID, j.fileInfo.name, artifactsMap) {
|
||||
if pomProperties.ArtifactID == "" { // keep the first match, or overwrite if we find more specific entries
|
||||
pomProperties = propertiesObj
|
||||
}
|
||||
if proj, exists := projects[parentPath]; exists {
|
||||
if parsedPom == nil { // keep the first matching artifact if we don't find an exact match or groupid + artfiact id match
|
||||
pomProperties = propertiesObj // set this, as it may not be the first entry found
|
||||
parsedPom = proj
|
||||
}
|
||||
// if artifact ID is the entire filename or BOTH artifactID and groupID are contained in the artifact, prefer this match
|
||||
if strings.Contains(j.fileInfo.name, propertiesObj.GroupID) || j.fileInfo.name == propertiesObj.ArtifactID {
|
||||
pomProperties = propertiesObj // this is an exact match, use it
|
||||
parsedPom = proj
|
||||
break
|
||||
}
|
||||
}
|
||||
if !artifactIDMatchesFilename(propertiesObj.ArtifactID, j.fileInfo.name, artifactsMap) {
|
||||
continue
|
||||
}
|
||||
|
||||
pomProperties, parsedPom = j.updateMatchIfBetter(pomProperties, parsedPom, propertiesObj, parentPath, projects)
|
||||
|
||||
if j.isExactMatch(propertiesObj, parsedPom) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return pomProperties, parsedPom
|
||||
}
|
||||
|
||||
func (j *archiveParser) updateMatchIfBetter(currentProps pkg.JavaPomProperties, currentPom *parsedPomProject,
|
||||
newProps pkg.JavaPomProperties, parentPath string, projects map[string]*parsedPomProject) (pkg.JavaPomProperties, *parsedPomProject) {
|
||||
// Keep the first match
|
||||
if currentProps.ArtifactID == "" {
|
||||
proj, hasProject := projects[parentPath]
|
||||
if hasProject {
|
||||
return newProps, proj
|
||||
}
|
||||
return newProps, currentPom
|
||||
}
|
||||
|
||||
proj, hasProject := projects[parentPath]
|
||||
if !hasProject {
|
||||
return currentProps, currentPom
|
||||
}
|
||||
|
||||
// Keep the first matching artifact with a pom.xml
|
||||
if currentPom == nil {
|
||||
return newProps, proj
|
||||
}
|
||||
|
||||
// Prefer exact matches
|
||||
if j.isExactMatch(newProps, proj) {
|
||||
return newProps, proj
|
||||
}
|
||||
|
||||
return currentProps, currentPom
|
||||
}
|
||||
|
||||
func (j *archiveParser) isExactMatch(props pkg.JavaPomProperties, pom *parsedPomProject) bool {
|
||||
if pom == nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(j.fileInfo.name, props.GroupID) || j.fileInfo.name == props.ArtifactID
|
||||
}
|
||||
|
||||
func (j *archiveParser) handleSinglePomXML(properties map[string]pkg.JavaPomProperties,
|
||||
projects map[string]*parsedPomProject, currentPom *parsedPomProject) *parsedPomProject {
|
||||
if len(properties) == 0 && len(projects) == 1 {
|
||||
for _, projectsObj := range projects {
|
||||
return projectsObj
|
||||
}
|
||||
}
|
||||
return currentPom
|
||||
}
|
||||
|
||||
func (j *archiveParser) resolveIdentity(ctx context.Context, pomProperties pkg.JavaPomProperties,
|
||||
parsedPom *parsedPomProject) (group, name, version string, pom *parsedPomProject) {
|
||||
group = pomProperties.GroupID
|
||||
name = pomProperties.ArtifactID
|
||||
version = pomProperties.Version
|
||||
|
||||
@@ -79,9 +79,12 @@ func TestSearchMavenForLicenses(t *testing.T) {
|
||||
ReadCloser: fixture,
|
||||
}, tc.detectNested, tc.config)
|
||||
defer cleanupFn()
|
||||
require.NoError(t, err)
|
||||
|
||||
// assert licenses are discovered from upstream
|
||||
_, _, _, parsedPom := ap.discoverMainPackageFromPomInfo(context.Background())
|
||||
require.NotNil(t, parsedPom, "expected to find pom information in the fixture")
|
||||
require.NotNil(t, parsedPom.project, "expected parsedPom to have a project")
|
||||
resolvedLicenses, _ := ap.maven.ResolveLicenses(context.Background(), parsedPom.project)
|
||||
assert.Equal(t, tc.expectedLicenses, toPkgLicenses(ctx, nil, resolvedLicenses))
|
||||
})
|
||||
@@ -148,10 +151,23 @@ func TestParseJar(t *testing.T) {
|
||||
},
|
||||
PomProperties: &pkg.JavaPomProperties{
|
||||
Path: "META-INF/maven/io.jenkins.plugins/example-jenkins-plugin/pom.properties",
|
||||
Name: "",
|
||||
GroupID: "io.jenkins.plugins",
|
||||
ArtifactID: "example-jenkins-plugin",
|
||||
Version: "1.0-SNAPSHOT",
|
||||
},
|
||||
PomProject: &pkg.JavaPomProject{
|
||||
Path: "META-INF/maven/io.jenkins.plugins/example-jenkins-plugin/pom.xml",
|
||||
Name: "Example Jenkins Plugin",
|
||||
GroupID: "io.jenkins.plugins",
|
||||
ArtifactID: "example-jenkins-plugin",
|
||||
Version: "1.0-SNAPSHOT",
|
||||
Parent: &pkg.JavaPomParent{
|
||||
GroupID: "org.jenkins-ci.plugins",
|
||||
ArtifactID: "plugin",
|
||||
Version: "4.46",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -189,6 +205,14 @@ func TestParseJar(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
// PomProject: &pkg.JavaPomProject{
|
||||
// Path: "META-INF/maven/io.jenkins.plugins/example-jenkins-plugin/pom.xml",
|
||||
// Parent: &pkg.JavaPomParent{GroupID: "org.jenkins-ci.plugins", ArtifactID: "plugin", Version: "4.46"},
|
||||
// GroupID: "io.jenkins.plugins",
|
||||
// ArtifactID: "example-jenkins-plugin",
|
||||
// Version: "1.0-SNAPSHOT",
|
||||
// Name: "Example Jenkins Plugin",
|
||||
// },
|
||||
},
|
||||
},
|
||||
"joda-time": {
|
||||
@@ -286,6 +310,12 @@ func TestParseJar(t *testing.T) {
|
||||
ArtifactID: "example-java-app-maven",
|
||||
Version: "0.1.0",
|
||||
},
|
||||
PomProject: &pkg.JavaPomProject{
|
||||
Path: "META-INF/maven/org.anchore/example-java-app-maven/pom.xml",
|
||||
GroupID: "org.anchore",
|
||||
ArtifactID: "example-java-app-maven",
|
||||
Version: "0.1.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"joda-time": {
|
||||
@@ -1127,6 +1157,13 @@ func Test_parseJavaArchive_regressions(t *testing.T) {
|
||||
GroupID: "org.apache.directory.api",
|
||||
ArtifactID: "api-all",
|
||||
Version: "2.0.0",
|
||||
}, PomProject: &pkg.JavaPomProject{
|
||||
Path: "META-INF/maven/org.apache.directory.api/api-all/pom.xml",
|
||||
ArtifactID: "api-all",
|
||||
GroupID: "org.apache.directory.api",
|
||||
Version: "2.0.0",
|
||||
Name: "Apache Directory API All",
|
||||
Parent: &pkg.JavaPomParent{GroupID: "org.apache.directory.api", ArtifactID: "api-parent", Version: "2.0.0"},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1163,6 +1200,46 @@ func Test_parseJavaArchive_regressions(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
micronautAop := pkg.Package{
|
||||
Name: "micronaut-aop",
|
||||
Version: "4.9.11",
|
||||
PURL: "pkg:maven/io.micronaut/micronaut-aop@4.9.11",
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/jar-metadata/cache/micronaut-aop-4.9.11.jar")),
|
||||
Type: pkg.JavaPkg,
|
||||
Language: pkg.Java,
|
||||
Metadata: pkg.JavaArchive{
|
||||
VirtualPath: "test-fixtures/jar-metadata/cache/micronaut-aop-4.9.11.jar",
|
||||
Manifest: &pkg.JavaManifest{
|
||||
Main: []pkg.KeyValue{
|
||||
{
|
||||
Key: "Manifest-Version",
|
||||
Value: "1.0",
|
||||
},
|
||||
{
|
||||
Key: "Automatic-Module-Name",
|
||||
Value: "io.micronaut.micronaut_aop",
|
||||
},
|
||||
{
|
||||
Key: "Implementation-Version",
|
||||
Value: "4.9.11",
|
||||
},
|
||||
{
|
||||
Key: "Implementation-Title",
|
||||
Value: "Micronaut Core",
|
||||
},
|
||||
},
|
||||
}, PomProject: &pkg.JavaPomProject{
|
||||
Path: "META-INF/maven/io.micronaut/micronaut-aop/pom.xml",
|
||||
ArtifactID: "micronaut-aop",
|
||||
GroupID: "io.micronaut",
|
||||
Version: "4.9.11",
|
||||
Name: "Micronaut Core",
|
||||
Description: "Core components supporting the Micronaut Framework",
|
||||
URL: "https://micronaut.io",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fixtureName string
|
||||
@@ -1220,6 +1297,16 @@ func Test_parseJavaArchive_regressions(t *testing.T) {
|
||||
{Key: "Specification-Version", Value: "2.15.2"},
|
||||
},
|
||||
},
|
||||
PomProject: &pkg.JavaPomProject{
|
||||
Path: "META-INF/maven/com.fasterxml.jackson.core/jackson-core/pom.xml",
|
||||
ArtifactID: "jackson-core",
|
||||
GroupID: "com.fasterxml.jackson.core",
|
||||
Version: "2.15.2",
|
||||
Name: "Jackson-core",
|
||||
Description: "Core Jackson processing abstractions (aka Streaming API), implementation for JSON",
|
||||
URL: "https://github.com/FasterXML/jackson-core",
|
||||
Parent: &pkg.JavaPomParent{GroupID: "com.fasterxml.jackson", ArtifactID: "jackson-base", Version: "2.15.2"},
|
||||
},
|
||||
// not under test
|
||||
//ArchiveDigests: []file.Digest{{Algorithm: "sha1", Value: "d8bc1d9c428c96fe447e2c429fc4304d141024df"}},
|
||||
},
|
||||
@@ -1275,6 +1362,16 @@ func Test_parseJavaArchive_regressions(t *testing.T) {
|
||||
{Key: "Specification-Version", Value: "2.15.2"},
|
||||
},
|
||||
},
|
||||
PomProject: &pkg.JavaPomProject{
|
||||
Path: "META-INF/maven/com.fasterxml.jackson.core/jackson-core/pom.xml",
|
||||
ArtifactID: "jackson-core",
|
||||
GroupID: "com.fasterxml.jackson.core",
|
||||
Version: "2.15.2",
|
||||
Name: "Jackson-core",
|
||||
Description: "Core Jackson processing abstractions (aka Streaming API), implementation for JSON",
|
||||
URL: "https://github.com/FasterXML/jackson-core",
|
||||
Parent: &pkg.JavaPomParent{GroupID: "com.fasterxml.jackson", ArtifactID: "jackson-base", Version: "2.15.2"},
|
||||
},
|
||||
// not under test
|
||||
//ArchiveDigests: []file.Digest{{Algorithm: "sha1", Value: "abd3e329270fc54a2acaceb45420fd5710ecefd5"}},
|
||||
},
|
||||
@@ -1341,6 +1438,14 @@ func Test_parseJavaArchive_regressions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "micronaut-aop",
|
||||
fixtureName: "micronaut-aop-4.9.11",
|
||||
fileExtension: "jar",
|
||||
expectedPkgs: []pkg.Package{
|
||||
micronautAop,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -41,11 +41,14 @@ func NewPomCataloger(cfg ArchiveCatalogerConfig) pkg.Cataloger {
|
||||
// Note: Older versions of lockfiles aren't supported yet
|
||||
func NewGradleLockfileCataloger() pkg.Cataloger {
|
||||
return generic.NewCataloger("java-gradle-lockfile-cataloger").
|
||||
WithParserByGlobs(parseGradleLockfile, gradleLockfileGlob)
|
||||
WithParserByGlobs(parseGradleLockfile, "**/gradle.lockfile*")
|
||||
}
|
||||
|
||||
// NewJvmDistributionCataloger returns packages representing JDK/JRE installations (of multiple distribution types).
|
||||
func NewJvmDistributionCataloger() pkg.Cataloger {
|
||||
return generic.NewCataloger("java-jvm-cataloger").
|
||||
WithParserByGlobs(parseJVMRelease, jvmReleaseGlob)
|
||||
// this is a very permissive glob that will match more than just the JVM release file.
|
||||
// we started with "**/{java,jvm}/*/release", but this prevents scanning JVM archive contents (e.g. jdk8u402.zip).
|
||||
// this approach lets us check more files for JVM release info, but be rather silent about errors.
|
||||
WithParserByGlobs(parseJVMRelease, "**/release")
|
||||
}
|
||||
|
||||
@@ -9,12 +9,30 @@ import (
|
||||
|
||||
type ArchiveCatalogerConfig struct {
|
||||
cataloging.ArchiveSearchConfig `yaml:",inline" json:"" mapstructure:",squash"`
|
||||
UseNetwork bool `yaml:"use-network" json:"use-network" mapstructure:"use-network"`
|
||||
UseMavenLocalRepository bool `yaml:"use-maven-localrepository" json:"use-maven-localrepository" mapstructure:"use-maven-localrepository"`
|
||||
MavenLocalRepositoryDir string `yaml:"maven-localrepository-dir" json:"maven-localrepository-dir" mapstructure:"maven-localrepository-dir"`
|
||||
MavenBaseURL string `yaml:"maven-base-url" json:"maven-base-url" mapstructure:"maven-base-url"`
|
||||
MaxParentRecursiveDepth int `yaml:"max-parent-recursive-depth" json:"max-parent-recursive-depth" mapstructure:"max-parent-recursive-depth"`
|
||||
ResolveTransitiveDependencies bool `yaml:"resolve-transitive-dependencies" json:"resolve-transitive-dependencies" mapstructure:"resolve-transitive-dependencies"`
|
||||
|
||||
// UseNetwork enables network operations for java package metadata enrichment, such as fetching parent POMs and license information.
|
||||
// app-config: java.use-network
|
||||
UseNetwork bool `yaml:"use-network" json:"use-network" mapstructure:"use-network"`
|
||||
|
||||
// UseMavenLocalRepository enables searching the local maven repository (~/.m2/repository by default) for parent POMs and other metadata.
|
||||
// app-config: java.use-maven-local-repository
|
||||
UseMavenLocalRepository bool `yaml:"use-maven-localrepository" json:"use-maven-localrepository" mapstructure:"use-maven-localrepository"`
|
||||
|
||||
// MavenLocalRepositoryDir specifies the location of the local maven repository. When not set, defaults to ~/.m2/repository.
|
||||
// app-config: java.maven-local-repository-dir
|
||||
MavenLocalRepositoryDir string `yaml:"maven-localrepository-dir" json:"maven-localrepository-dir" mapstructure:"maven-localrepository-dir"`
|
||||
|
||||
// MavenBaseURL specifies the base URL(s) to use for fetching POMs and metadata from maven central or other repositories. When not set, defaults to https://repo1.maven.org/maven2.
|
||||
// app-config: java.maven-url
|
||||
MavenBaseURL string `yaml:"maven-base-url" json:"maven-base-url" mapstructure:"maven-base-url"`
|
||||
|
||||
// MaxParentRecursiveDepth limits how many parent POMs will be fetched recursively before stopping. This prevents infinite loops or excessively deep parent chains.
|
||||
// app-config: java.max-parent-recursive-depth
|
||||
MaxParentRecursiveDepth int `yaml:"max-parent-recursive-depth" json:"max-parent-recursive-depth" mapstructure:"max-parent-recursive-depth"`
|
||||
|
||||
// ResolveTransitiveDependencies enables resolving transitive dependencies for java packages found within archives.
|
||||
// app-config: java.resolve-transitive-dependencies
|
||||
ResolveTransitiveDependencies bool `yaml:"resolve-transitive-dependencies" json:"resolve-transitive-dependencies" mapstructure:"resolve-transitive-dependencies"`
|
||||
}
|
||||
|
||||
func DefaultArchiveCatalogerConfig() ArchiveCatalogerConfig {
|
||||
|
||||
@@ -508,7 +508,7 @@ func (ni nativeImagePE) fetchPkgs() (pkgs []pkg.Package, relationships []artifac
|
||||
}
|
||||
|
||||
// fetchPkgs provides the packages available in a UnionReader.
|
||||
func fetchPkgs(reader unionreader.UnionReader, filename string) ([]pkg.Package, []artifact.Relationship) {
|
||||
func fetchPkgs(reader unionreader.UnionReader, location file.Location) ([]pkg.Package, []artifact.Relationship) {
|
||||
var pkgs []pkg.Package
|
||||
var relationships []artifact.Relationship
|
||||
imageFormats := []func(string, io.ReaderAt) (nativeImage, error){newElf, newMachO, newPE}
|
||||
@@ -520,6 +520,7 @@ func fetchPkgs(reader unionreader.UnionReader, filename string) ([]pkg.Package,
|
||||
log.Debugf("failed to open the java native-image binary: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
filename := location.RealPath
|
||||
for _, r := range readers {
|
||||
for _, makeNativeImage := range imageFormats {
|
||||
ni, err := makeNativeImage(filename, r)
|
||||
@@ -534,6 +535,10 @@ func fetchPkgs(reader unionreader.UnionReader, filename string) ([]pkg.Package,
|
||||
log.Tracef("unable to extract SBOM from possible java native-image %s: %v", filename, err)
|
||||
continue
|
||||
}
|
||||
// Associate extracted packages with the native image location
|
||||
for i := range newPkgs {
|
||||
newPkgs[i].Locations.Add(location)
|
||||
}
|
||||
pkgs = append(pkgs, newPkgs...)
|
||||
relationships = append(relationships, newRelationships...)
|
||||
}
|
||||
@@ -574,6 +579,6 @@ func processLocation(location file.Location, resolver file.Resolver) ([]pkg.Pack
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pkgs, relationships := fetchPkgs(reader, location.RealPath)
|
||||
pkgs, relationships := fetchPkgs(reader, location)
|
||||
return pkgs, relationships, nil
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
)
|
||||
|
||||
const gradleLockfileGlob = "**/gradle.lockfile*"
|
||||
|
||||
// lockfileDependency represents a single dependency in the gradle.lockfile file
|
||||
type lockfileDependency struct {
|
||||
Group string
|
||||
|
||||
@@ -22,10 +22,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// this is a very permissive glob that will match more than just the JVM release file.
|
||||
// we started with "**/{java,jvm}/*/release", but this prevents scanning JVM archive contents (e.g. jdk8u402.zip).
|
||||
// this approach lets us check more files for JVM release info, but be rather silent about errors.
|
||||
jvmReleaseGlob = "**/release"
|
||||
oracleVendor = "oracle"
|
||||
openJdkProduct = "openjdk"
|
||||
jre = "jre"
|
||||
|
||||
@@ -13,6 +13,7 @@ API_ALL_SOURCES = api-all-2.0.0-sources
|
||||
SPRING_INSTRUMENTATION = spring-instrumentation-4.3.0-1.0
|
||||
MULTIPLE_MATCHING = multiple-matching-2.11.5
|
||||
ORG_MULTIPLE_THENAME = org.multiple-thename
|
||||
MICRONAUT_AOP = micronaut-aop-4.9.11
|
||||
|
||||
|
||||
.DEFAULT_GOAL := fixtures
|
||||
@@ -23,7 +24,7 @@ fixtures: $(CACHE_DIR)
|
||||
# requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted
|
||||
fingerprint: $(FINGERPRINT_FILE)
|
||||
|
||||
$(CACHE_DIR): $(CACHE_DIR)/$(JACKSON_CORE).jar $(CACHE_DIR)/$(SBT_JACKSON_CORE).jar $(CACHE_DIR)/$(OPENSAML_CORE).jar $(CACHE_DIR)/$(API_ALL_SOURCES).jar $(CACHE_DIR)/$(SPRING_INSTRUMENTATION).jar $(CACHE_DIR)/$(MULTIPLE_MATCHING).jar
|
||||
$(CACHE_DIR): $(CACHE_DIR)/$(JACKSON_CORE).jar $(CACHE_DIR)/$(SBT_JACKSON_CORE).jar $(CACHE_DIR)/$(OPENSAML_CORE).jar $(CACHE_DIR)/$(API_ALL_SOURCES).jar $(CACHE_DIR)/$(SPRING_INSTRUMENTATION).jar $(CACHE_DIR)/$(MULTIPLE_MATCHING).jar $(CACHE_DIR)/$(MICRONAUT_AOP).jar
|
||||
|
||||
$(CACHE_DIR)/$(JACKSON_CORE).jar:
|
||||
mkdir -p $(CACHE_DIR)
|
||||
@@ -53,6 +54,10 @@ $(CACHE_DIR)/$(ORG_MULTIPLE_THENAME).jar:
|
||||
mkdir -p $(CACHE_DIR)
|
||||
cd $(ORG_MULTIPLE_THENAME) && zip -r $(CACHE_PATH)/$(ORG_MULTIPLE_THENAME).jar .
|
||||
|
||||
$(CACHE_DIR)/$(MICRONAUT_AOP).jar:
|
||||
mkdir -p $(CACHE_DIR)
|
||||
cd $(MICRONAUT_AOP) && zip -r $(CACHE_PATH)/$(MICRONAUT_AOP).jar .
|
||||
|
||||
# Jenkins plugins typically do not have the version included in the archive name,
|
||||
# so it is important to not include it in the generated test fixture
|
||||
$(CACHE_DIR)/gradle.hpi:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Jar-Metadata test fixtures
|
||||
|
||||
Each directory is the name of a jar to be created (simply a zip) based on the contents of the directory.
|
||||
This prevents us from having to create real jars by hand or keep binaries in the repo. This also means we dont need the
|
||||
This prevents us from having to create real jars by hand or keep binaries in the repo. This also means we don't need the
|
||||
entire jar, only the necessary metadata for testing.
|
||||
|
||||
### api-all-2.0.0-sources
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
Manifest-Version: 1.0
|
||||
Automatic-Module-Name: io.micronaut.micronaut_aop
|
||||
Implementation-Version: 4.9.11
|
||||
Implementation-Title: Micronaut Core
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<!-- This module was also published with a richer model, Gradle metadata, -->
|
||||
<!-- which should be used instead. Do not delete the following line which -->
|
||||
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
|
||||
<!-- that they should prefer consuming it instead. -->
|
||||
<!-- do_not_remove: published-with-gradle-metadata -->
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>io.micronaut</groupId>
|
||||
<artifactId>micronaut-aop</artifactId>
|
||||
<version>4.9.11</version>
|
||||
<name>Micronaut Core</name>
|
||||
<description>Core components supporting the Micronaut Framework</description>
|
||||
<url>https://micronaut.io</url>
|
||||
</project>
|
||||
@@ -3,9 +3,15 @@ package javascript
|
||||
const npmBaseURL = "https://registry.npmjs.org"
|
||||
|
||||
type CatalogerConfig struct {
|
||||
SearchRemoteLicenses bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"`
|
||||
NPMBaseURL string `json:"npm-base-url" yaml:"npm-base-url" mapstructure:"npm-base-url"`
|
||||
IncludeDevDependencies bool `json:"include-dev-dependencies" yaml:"include-dev-dependencies" mapstructure:"include-dev-dependencies"`
|
||||
// SearchRemoteLicenses enables querying the NPM registry API to retrieve license information for packages that are missing license data in their local metadata.
|
||||
// app-config: javascript.search-remote-licenses
|
||||
SearchRemoteLicenses bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"`
|
||||
// NPMBaseURL specifies the base URL for the NPM registry API used when searching for remote license information.
|
||||
// app-config: javascript.npm-base-url
|
||||
NPMBaseURL string `json:"npm-base-url" yaml:"npm-base-url" mapstructure:"npm-base-url"`
|
||||
// IncludeDevDependencies controls whether development dependencies should be included in the catalog results, in addition to production dependencies.
|
||||
// app-config: javascript.include-dev-dependencies
|
||||
IncludeDevDependencies bool `json:"include-dev-dependencies" yaml:"include-dev-dependencies" mapstructure:"include-dev-dependencies"`
|
||||
}
|
||||
|
||||
func DefaultCatalogerConfig() CatalogerConfig {
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -21,92 +21,186 @@ import (
|
||||
// integrity check
|
||||
var _ generic.Parser = parsePnpmLock
|
||||
|
||||
type pnpmLockYaml struct {
|
||||
Version string `json:"lockfileVersion" yaml:"lockfileVersion"`
|
||||
Dependencies map[string]interface{} `json:"dependencies" yaml:"dependencies"`
|
||||
Packages map[string]interface{} `json:"packages" yaml:"packages"`
|
||||
// pnpmPackage holds the raw name and version extracted from the lockfile.
|
||||
type pnpmPackage struct {
|
||||
Name string
|
||||
Version string
|
||||
}
|
||||
|
||||
// pnpmLockfileParser defines the interface for parsing different versions of pnpm lockfiles.
|
||||
type pnpmLockfileParser interface {
|
||||
Parse(version float64, data []byte) ([]pnpmPackage, error)
|
||||
}
|
||||
|
||||
// pnpmV6LockYaml represents the structure of pnpm lockfiles for versions < 9.0.
|
||||
type pnpmV6LockYaml struct {
|
||||
Dependencies map[string]interface{} `yaml:"dependencies"`
|
||||
Packages map[string]interface{} `yaml:"packages"`
|
||||
}
|
||||
|
||||
// pnpmV9LockYaml represents the structure of pnpm lockfiles for versions >= 9.0.
|
||||
type pnpmV9LockYaml struct {
|
||||
LockfileVersion string `yaml:"lockfileVersion"`
|
||||
Importers map[string]interface{} `yaml:"importers"` // Using interface{} for forward compatibility
|
||||
Packages map[string]interface{} `yaml:"packages"`
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, fmt.Errorf("failed to unmarshal pnpm v6 lockfile: %w", err)
|
||||
}
|
||||
|
||||
packages := make(map[string]pnpmPackage)
|
||||
|
||||
// Direct dependencies
|
||||
for name, info := range p.Dependencies {
|
||||
ver, err := parseVersionField(name, info)
|
||||
if err != nil {
|
||||
log.WithFields("package", name, "error", err).Trace("unable to parse pnpm dependency")
|
||||
continue
|
||||
}
|
||||
key := name + "@" + ver
|
||||
packages[key] = pnpmPackage{Name: name, Version: ver}
|
||||
}
|
||||
|
||||
splitChar := "/"
|
||||
if version >= 6.0 {
|
||||
splitChar = "@"
|
||||
}
|
||||
|
||||
// All transitive dependencies
|
||||
for key := range p.Packages {
|
||||
name, ver, ok := parsePnpmPackageKey(key, splitChar)
|
||||
if !ok {
|
||||
log.WithFields("key", key).Trace("unable to parse pnpm package key")
|
||||
continue
|
||||
}
|
||||
pkgKey := name + "@" + ver
|
||||
packages[pkgKey] = pnpmPackage{Name: name, Version: ver}
|
||||
}
|
||||
|
||||
return toSortedSlice(packages), nil
|
||||
}
|
||||
|
||||
// Parse implements the PnpmLockfileParser interface for v9+ lockfiles.
|
||||
func (p *pnpmV9LockYaml) Parse(_ float64, data []byte) ([]pnpmPackage, error) {
|
||||
if err := yaml.Unmarshal(data, p); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal pnpm v9 lockfile: %w", err)
|
||||
}
|
||||
|
||||
packages := make(map[string]pnpmPackage)
|
||||
|
||||
// In v9, all resolved dependencies are listed in the top-level "packages" field.
|
||||
// The key format is like /<name>@<version> or /<name>@<version>(<peer-deps>).
|
||||
for key := range p.Packages {
|
||||
// The separator for name and version is consistently '@' in v9+ keys.
|
||||
name, ver, ok := parsePnpmPackageKey(key, "@")
|
||||
if !ok {
|
||||
log.WithFields("key", key).Trace("unable to parse pnpm v9 package key")
|
||||
continue
|
||||
}
|
||||
pkgKey := name + "@" + ver
|
||||
packages[pkgKey] = pnpmPackage{Name: name, Version: ver}
|
||||
}
|
||||
|
||||
return toSortedSlice(packages), nil
|
||||
}
|
||||
|
||||
// newPnpmLockfileParser is a factory function that returns the correct parser for the given lockfile version.
|
||||
func newPnpmLockfileParser(version float64) pnpmLockfileParser {
|
||||
if version >= 9.0 {
|
||||
return &pnpmV9LockYaml{}
|
||||
}
|
||||
return &pnpmV6LockYaml{}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
bytes, err := io.ReadAll(reader)
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to load pnpm-lock.yaml file: %w", err)
|
||||
}
|
||||
|
||||
var pkgs []pkg.Package
|
||||
var lockFile pnpmLockYaml
|
||||
var lockfile struct {
|
||||
Version string `yaml:"lockfileVersion"`
|
||||
}
|
||||
if err := yaml.Unmarshal(data, &lockfile); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse pnpm-lock.yaml version: %w", err)
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(bytes, &lockFile); err != nil {
|
||||
version, err := strconv.ParseFloat(lockfile.Version, 64)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid lockfile version %q: %w", lockfile.Version, err)
|
||||
}
|
||||
|
||||
parser := newPnpmLockfileParser(version)
|
||||
pnpmPkgs, err := parser.Parse(version, data)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse pnpm-lock.yaml file: %w", err)
|
||||
}
|
||||
|
||||
lockVersion, _ := strconv.ParseFloat(lockFile.Version, 64)
|
||||
|
||||
for name, info := range lockFile.Dependencies {
|
||||
version := ""
|
||||
|
||||
switch info := info.(type) {
|
||||
case string:
|
||||
version = info
|
||||
case map[string]interface{}:
|
||||
v, ok := info["version"]
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
ver, ok := v.(string)
|
||||
if ok {
|
||||
version = parseVersion(ver)
|
||||
}
|
||||
default:
|
||||
log.Tracef("unsupported pnpm dependency type: %+v", info)
|
||||
continue
|
||||
}
|
||||
|
||||
if hasPkg(pkgs, name, version) {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgs = append(pkgs, newPnpmPackage(ctx, resolver, reader.Location, name, version))
|
||||
packages := make([]pkg.Package, len(pnpmPkgs))
|
||||
for i, p := range pnpmPkgs {
|
||||
packages[i] = newPnpmPackage(ctx, resolver, reader.Location, p.Name, p.Version)
|
||||
}
|
||||
|
||||
packageNameRegex := regexp.MustCompile(`^/?([^(]*)(?:\(.*\))*$`)
|
||||
splitChar := "/"
|
||||
if lockVersion >= 6.0 {
|
||||
splitChar = "@"
|
||||
}
|
||||
|
||||
// parse packages from packages section of pnpm-lock.yaml
|
||||
for nameVersion := range lockFile.Packages {
|
||||
nameVersion = packageNameRegex.ReplaceAllString(nameVersion, "$1")
|
||||
nameVersionSplit := strings.Split(strings.TrimPrefix(nameVersion, "/"), splitChar)
|
||||
|
||||
// last element in split array is version
|
||||
version := nameVersionSplit[len(nameVersionSplit)-1]
|
||||
|
||||
// construct name from all array items other than last item (version)
|
||||
name := strings.Join(nameVersionSplit[:len(nameVersionSplit)-1], splitChar)
|
||||
|
||||
if hasPkg(pkgs, name, version) {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgs = append(pkgs, newPnpmPackage(ctx, resolver, reader.Location, name, version))
|
||||
}
|
||||
|
||||
pkg.Sort(pkgs)
|
||||
|
||||
return pkgs, nil, unknown.IfEmptyf(pkgs, "unable to determine packages")
|
||||
return packages, nil, unknown.IfEmptyf(packages, "unable to determine packages")
|
||||
}
|
||||
|
||||
func hasPkg(pkgs []pkg.Package, name, version string) bool {
|
||||
for _, p := range pkgs {
|
||||
if p.Name == name && p.Version == version {
|
||||
return true
|
||||
// parseVersionField extracts the version string from a dependency entry.
|
||||
func parseVersionField(name string, info interface{}) (string, error) {
|
||||
switch v := info.(type) {
|
||||
case string:
|
||||
return v, nil
|
||||
case map[string]interface{}:
|
||||
if ver, ok := v["version"].(string); ok {
|
||||
// e.g., "1.2.3(react@17.0.0)" -> "1.2.3"
|
||||
return strings.SplitN(ver, "(", 2)[0], nil
|
||||
}
|
||||
return "", fmt.Errorf("version field is not a string for %q", name)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported dependency type %T for %q", info, name)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseVersion(version string) string {
|
||||
return strings.SplitN(version, "(", 2)[0]
|
||||
// parsePnpmPackageKey extracts the package name and version from a lockfile package key.
|
||||
// Handles formats like:
|
||||
// - /@babel/runtime/7.16.7
|
||||
// - /@types/node@14.18.12
|
||||
// - /is-glob@4.0.3
|
||||
// - /@babel/helper-plugin-utils@7.24.7(@babel/core@7.24.7)
|
||||
func parsePnpmPackageKey(key, separator string) (name, version string, ok bool) {
|
||||
// Strip peer dependency information, e.g., (...)
|
||||
key = strings.SplitN(key, "(", 2)[0]
|
||||
|
||||
// Strip leading slash
|
||||
key = strings.TrimPrefix(key, "/")
|
||||
|
||||
parts := strings.Split(key, separator)
|
||||
if len(parts) < 2 {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
version = parts[len(parts)-1]
|
||||
name = strings.Join(parts[:len(parts)-1], separator)
|
||||
|
||||
return name, version, true
|
||||
}
|
||||
|
||||
// toSortedSlice converts the map of packages to a sorted slice for deterministic output.
|
||||
func toSortedSlice(packages map[string]pnpmPackage) []pnpmPackage {
|
||||
pkgs := make([]pnpmPackage, 0, len(packages))
|
||||
for _, p := range packages {
|
||||
pkgs = append(pkgs, p)
|
||||
}
|
||||
|
||||
sort.Slice(pkgs, func(i, j int) bool {
|
||||
if pkgs[i].Name == pkgs[j].Name {
|
||||
return pkgs[i].Version < pkgs[j].Version
|
||||
}
|
||||
return pkgs[i].Name < pkgs[j].Name
|
||||
})
|
||||
|
||||
return pkgs
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user