mirror of
https://github.com/anchore/syft.git
synced 2026-10-13 22:57:19 +02:00
Compare commits
70
Commits
+5
-18
@@ -34,7 +34,7 @@ tools:
|
||||
# used for showing the changelog at release
|
||||
- name: glow
|
||||
version:
|
||||
want: v2.1.0
|
||||
want: v2.1.1
|
||||
method: github-release
|
||||
with:
|
||||
repo: charmbracelet/glow
|
||||
@@ -58,7 +58,7 @@ tools:
|
||||
# used to release all artifacts
|
||||
- name: goreleaser
|
||||
version:
|
||||
want: v2.9.0
|
||||
want: v2.10.2
|
||||
method: github-release
|
||||
with:
|
||||
repo: goreleaser/goreleaser
|
||||
@@ -87,23 +87,10 @@ tools:
|
||||
with:
|
||||
repo: wagoodman/go-bouncer
|
||||
|
||||
# used for showing benchmark testing
|
||||
- name: benchstat
|
||||
version:
|
||||
want: latest
|
||||
method: go-proxy
|
||||
with:
|
||||
module: golang.org/x/perf
|
||||
allow-unresolved-version: true
|
||||
method: go-install
|
||||
with:
|
||||
entrypoint: cmd/benchstat
|
||||
module: golang.org/x/perf
|
||||
|
||||
# used for running all local and CI tasks
|
||||
- name: task
|
||||
version:
|
||||
want: v3.43.3
|
||||
want: v3.44.0
|
||||
method: github-release
|
||||
with:
|
||||
repo: go-task/task
|
||||
@@ -111,7 +98,7 @@ tools:
|
||||
# used for triggering a release
|
||||
- name: gh
|
||||
version:
|
||||
want: v2.72.0
|
||||
want: v2.74.0
|
||||
method: github-release
|
||||
with:
|
||||
repo: cli/cli
|
||||
@@ -127,7 +114,7 @@ tools:
|
||||
# used to upload test fixture cache
|
||||
- name: yq
|
||||
version:
|
||||
want: v4.45.2
|
||||
want: v4.45.4
|
||||
method: github-release
|
||||
with:
|
||||
repo: mikefarah/yq
|
||||
|
||||
@@ -29,7 +29,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
# note: go mod and build is automatically cached on default with v4+
|
||||
- uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
|
||||
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
if: inputs.go-version != ''
|
||||
with:
|
||||
go-version: ${{ inputs.go-version }}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
name: "Benchmark testing"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
Benchmark-Test:
|
||||
name: "Benchmark tests"
|
||||
runs-on: ubuntu-24.04
|
||||
# note: we want benchmarks to run on pull_request events in order to publish results to a sticky comment, and
|
||||
# we also want to run on push such that merges to main are recorded to the cache. For this reason we don't filter
|
||||
# the job by event.
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- name: Restore base benchmark result
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 #v4.2.3
|
||||
with:
|
||||
path: test/results/benchmark-main.txt
|
||||
# use base sha for PR or new commit hash for main push in benchmark result key
|
||||
key: ${{ runner.os }}-bench-${{ (github.event.pull_request.base.sha != github.event.after) && github.event.pull_request.base.sha || github.event.after }}
|
||||
|
||||
- name: Run benchmark tests
|
||||
id: benchmark
|
||||
run: |
|
||||
REF_NAME=${GITHUB_REF##*/} make benchmark
|
||||
OUTPUT=$(make show-benchstat)
|
||||
OUTPUT="${OUTPUT//'%'/'%25'}" # URL encode all '%' characters
|
||||
OUTPUT="${OUTPUT//$'\n'/'%0A'}" # URL encode all '\n' characters
|
||||
OUTPUT="${OUTPUT//$'\r'/'%0D'}" # URL encode all '\r' characters
|
||||
echo "result=$OUTPUT" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: benchmark-test-results
|
||||
path: test/results/**/*
|
||||
|
||||
- name: Update PR benchmark results comment
|
||||
uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db #v2.9.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
header: benchmark
|
||||
message: |
|
||||
### Benchmark Test Results
|
||||
|
||||
<details>
|
||||
<summary>Benchmark results from the latest changes vs base branch</summary>
|
||||
|
||||
```
|
||||
${{ steps.benchmark.outputs.result }}
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -39,13 +39,13 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b #v5.4.0
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 #v5.5.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b #v3.28.17
|
||||
uses: github/codeql-action/init@fca7ace96b7d713c7035871441bd52efbe39e27e #v3.28.19
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@60168efe1c415ce0f5521ea06d5c2062adbeed1b #v3.28.17
|
||||
uses: github/codeql-action/autobuild@fca7ace96b7d713c7035871441bd52efbe39e27e #v3.28.19
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -70,4 +70,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b #v3.28.17
|
||||
uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441bd52efbe39e27e #v3.28.19
|
||||
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
# for updating brew formula in anchore/homebrew-syft
|
||||
GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}
|
||||
|
||||
- uses: anchore/sbom-action@9f7302141466aa6482940f15371237e9d9f4c34a #v0.19.0
|
||||
- uses: anchore/sbom-action@e11c554f704a0b820cbf8c51673f6945e0731532 #v0.20.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
file: go.mod
|
||||
|
||||
@@ -76,6 +76,9 @@ jobs:
|
||||
- name: Build snapshot artifacts
|
||||
run: make snapshot
|
||||
|
||||
- name: Smoke test snapshot build
|
||||
run: make snapshot-smoke-test
|
||||
|
||||
# why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach).
|
||||
# see https://github.com/actions/upload-artifact/issues/199 for more info
|
||||
- name: Upload snapshot artifacts
|
||||
|
||||
+99
-112
@@ -1,11 +1,11 @@
|
||||
version: 2
|
||||
project_name: syft
|
||||
|
||||
release:
|
||||
prerelease: auto
|
||||
draft: false
|
||||
|
||||
env:
|
||||
# required to support multi architecture docker builds
|
||||
- DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
- CGO_ENABLED=0
|
||||
|
||||
@@ -13,14 +13,8 @@ builds:
|
||||
- id: linux-build
|
||||
dir: ./cmd/syft
|
||||
binary: syft
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- ppc64le
|
||||
- s390x
|
||||
# set the modified timestamp on the output binary to the git timestamp to ensure a reproducible build
|
||||
goos: [linux]
|
||||
goarch: [amd64, arm64, ppc64le, s390x]
|
||||
mod_timestamp: &build-timestamp '{{ .CommitTimestamp }}'
|
||||
ldflags: &build-ldflags |
|
||||
-w
|
||||
@@ -34,11 +28,8 @@ builds:
|
||||
- id: darwin-build
|
||||
dir: ./cmd/syft
|
||||
binary: syft
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
goos: [darwin]
|
||||
goarch: [amd64, arm64]
|
||||
mod_timestamp: *build-timestamp
|
||||
ldflags: *build-ldflags
|
||||
hooks:
|
||||
@@ -50,115 +41,42 @@ builds:
|
||||
- id: windows-build
|
||||
dir: ./cmd/syft
|
||||
binary: syft
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
goos: [windows]
|
||||
goarch: [amd64]
|
||||
mod_timestamp: *build-timestamp
|
||||
ldflags: *build-ldflags
|
||||
|
||||
archives:
|
||||
- id: linux-archives
|
||||
builds:
|
||||
- linux-build
|
||||
|
||||
# note: the signing process is depending on tar.gz archives. If this format changes then .github/scripts/apple-signing/*.sh will need to be adjusted
|
||||
builds: [linux-build]
|
||||
- id: darwin-archives
|
||||
builds:
|
||||
- darwin-build
|
||||
|
||||
builds: [darwin-build]
|
||||
- id: windows-archives
|
||||
format: zip
|
||||
builds:
|
||||
- windows-build
|
||||
builds: [windows-build]
|
||||
|
||||
nfpms:
|
||||
- license: "Apache 2.0"
|
||||
maintainer: "Anchore, Inc"
|
||||
homepage: &website "https://github.com/anchore/syft"
|
||||
description: &description "A tool that generates a Software Bill Of Materials (SBOM) from container images and filesystems"
|
||||
formats:
|
||||
- rpm
|
||||
- deb
|
||||
formats: [rpm, deb]
|
||||
|
||||
brews:
|
||||
- repository:
|
||||
owner: anchore
|
||||
name: homebrew-syft
|
||||
token: "{{.Env.GITHUB_BREW_TOKEN}}"
|
||||
ids:
|
||||
- darwin-archives
|
||||
- linux-archives
|
||||
ids: [darwin-archives, linux-archives]
|
||||
homepage: *website
|
||||
description: *description
|
||||
license: "Apache License 2.0"
|
||||
|
||||
dockers:
|
||||
# production images...
|
||||
- image_templates:
|
||||
- anchore/syft:debug
|
||||
- anchore/syft:{{.Tag}}-debug
|
||||
- ghcr.io/anchore/syft:debug
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug
|
||||
goarch: amd64
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:debug-arm64v8
|
||||
- anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
- ghcr.io/anchore/syft:debug-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
goarch: arm64
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64/v8"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:debug-ppc64le
|
||||
- anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
- ghcr.io/anchore/syft:debug-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
goarch: ppc64le
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/ppc64le"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:debug-s390x
|
||||
- anchore/syft:{{.Tag}}-debug-s390x
|
||||
- ghcr.io/anchore/syft:debug-s390x
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
|
||||
goarch: s390x
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/s390x"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:latest
|
||||
- anchore/syft:{{.Tag}}
|
||||
- ghcr.io/anchore/syft:latest
|
||||
- ghcr.io/anchore/syft:{{.Tag}}
|
||||
- anchore/syft:{{.Tag}}-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-amd64
|
||||
goarch: amd64
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
@@ -208,51 +126,120 @@ dockers:
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
# debug images...
|
||||
- image_templates:
|
||||
- anchore/syft:{{.Tag}}-debug-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
|
||||
goarch: amd64
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
goarch: arm64
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64/v8"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
goarch: ppc64le
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/ppc64le"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:{{.Tag}}-debug-s390x
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
|
||||
goarch: s390x
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/s390x"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
docker_manifests:
|
||||
# anchore/syft manifests...
|
||||
- name_template: anchore/syft:latest
|
||||
image_templates:
|
||||
- anchore/syft:{{.Tag}}
|
||||
- anchore/syft:{{.Tag}}-amd64
|
||||
- anchore/syft:{{.Tag}}-arm64v8
|
||||
- anchore/syft:{{.Tag}}-ppc64le
|
||||
- anchore/syft:{{.Tag}}-s390x
|
||||
|
||||
- name_template: anchore/syft:{{.Tag}}
|
||||
image_templates:
|
||||
- anchore/syft:{{.Tag}}-amd64
|
||||
- anchore/syft:{{.Tag}}-arm64v8
|
||||
- anchore/syft:{{.Tag}}-ppc64le
|
||||
- anchore/syft:{{.Tag}}-s390x
|
||||
|
||||
- name_template: anchore/syft:debug
|
||||
- anchore/syft:{{.Tag}}-debug
|
||||
image_templates:
|
||||
- anchore/syft:{{.Tag}}-debug-amd64
|
||||
- anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
- anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
- anchore/syft:{{.Tag}}-debug-s390x
|
||||
|
||||
- name_template: anchore/syft:{{.Tag}}
|
||||
- name_template: anchore/syft:{{.Tag}}-debug
|
||||
image_templates:
|
||||
- anchore/syft:{{.Tag}}
|
||||
- anchore/syft:{{.Tag}}-arm64v8
|
||||
- anchore/syft:{{.Tag}}-ppc64le
|
||||
- anchore/syft:{{.Tag}}-s390x
|
||||
- anchore/syft:{{.Tag}}-debug-amd64
|
||||
- anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
- anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
- anchore/syft:{{.Tag}}-debug-s390x
|
||||
|
||||
# ghcr.io/anchore/syft manifests...
|
||||
- name_template: ghcr.io/anchore/syft:latest
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:{{.Tag}}
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-s390x
|
||||
|
||||
- name_template: ghcr.io/anchore/syft:{{.Tag}}
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-s390x
|
||||
|
||||
- name_template: ghcr.io/anchore/syft:debug
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
|
||||
|
||||
- name_template: ghcr.io/anchore/syft:{{.Tag}}
|
||||
- name_template: ghcr.io/anchore/syft:{{.Tag}}-debug
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:{{.Tag}}
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-s390x
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
|
||||
|
||||
sboms:
|
||||
- artifacts: archive
|
||||
# this is relative to the snapshot/dist directory, not the root of the repo
|
||||
cmd: ../.tool/syft
|
||||
documents:
|
||||
- "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.sbom"
|
||||
|
||||
+3
-5
@@ -1,14 +1,12 @@
|
||||
FROM gcr.io/distroless/static-debian11:debug@sha256:a0a404776dec98be120089ae42bbdfbe48c177921d856937d124d48eb8c0b951 AS build
|
||||
|
||||
FROM scratch
|
||||
# needed for version check HTTPS request
|
||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
|
||||
# create the /tmp dir, which is needed for image content cache
|
||||
WORKDIR /tmp
|
||||
|
||||
COPY syft /
|
||||
|
||||
USER nonroot
|
||||
|
||||
ARG BUILD_DATE
|
||||
ARG BUILD_VERSION
|
||||
ARG VCS_REF
|
||||
|
||||
+3
-1
@@ -1,10 +1,12 @@
|
||||
FROM gcr.io/distroless/static-debian11:debug@sha256:a0a404776dec98be120089ae42bbdfbe48c177921d856937d124d48eb8c0b951
|
||||
FROM gcr.io/distroless/static-debian12:debug-nonroot
|
||||
|
||||
# create the /tmp dir, which is needed for image content cache
|
||||
WORKDIR /tmp
|
||||
|
||||
COPY syft /
|
||||
|
||||
USER nonroot
|
||||
|
||||
ARG BUILD_DATE
|
||||
ARG BUILD_VERSION
|
||||
ARG VCS_REF
|
||||
|
||||
+20
-30
@@ -27,10 +27,12 @@ vars:
|
||||
sh: uname -s | tr '[:upper:]' '[:lower:]'
|
||||
ARCH:
|
||||
sh: |
|
||||
[ "$(uname -m)" = "x86_64" ] && echo "amd64_v1" || { [ "$(uname -m)" = "aarch64" ] && echo "arm64_v8.0" || echo $(uname -m); }
|
||||
[ "$(uname -m)" = "x86_64" ] && echo "amd64_v1" || { [ "$(uname -m)" = "aarch64" ] && echo "arm64_v8.0" || [ "$(uname -m)" = "arm64" ] && echo "arm64_v8.0" || echo $(uname -m); }
|
||||
PROJECT_ROOT:
|
||||
sh: echo $PWD
|
||||
|
||||
# note: the snapshot dir must be a relative path starting with ./
|
||||
# e.g. when installing snapshot debs from a local path, ./ forces the deb to be installed in the current working directory instead of referencing a package name
|
||||
SNAPSHOT_DIR: ./snapshot
|
||||
SNAPSHOT_BIN: "{{ .PROJECT_ROOT }}/{{ .SNAPSHOT_DIR }}/{{ .OS }}-build_{{ .OS }}_{{ .ARCH }}/{{ .PROJECT }}"
|
||||
SNAPSHOT_CMD: "{{ .TOOL_DIR }}/goreleaser release --config {{ .TMP_DIR }}/goreleaser.yaml --clean --snapshot --skip=publish --skip=sign"
|
||||
@@ -75,7 +77,6 @@ tasks:
|
||||
- task: unit
|
||||
- task: integration
|
||||
- task: validate-cyclonedx-schema
|
||||
- task: benchmark
|
||||
- task: test-utils
|
||||
- task: cli
|
||||
- task: check-docker-cache
|
||||
@@ -221,7 +222,8 @@ tasks:
|
||||
cmds:
|
||||
- "go test -v ./cmd/syft/internal/test/integration"
|
||||
# exercise most of the CLI with the data race detector
|
||||
- "go run -race cmd/syft/main.go alpine:latest"
|
||||
# we use a larger image to ensure we're using multiple catalogers at a time
|
||||
- "go run -race cmd/syft/main.go anchore/test_images:grype-quality-dotnet-69f15d2"
|
||||
|
||||
validate-cyclonedx-schema:
|
||||
desc: Validate that Syft produces valid CycloneDX documents
|
||||
@@ -230,10 +232,7 @@ tasks:
|
||||
|
||||
cli:
|
||||
desc: Run CLI tests
|
||||
# note: we don't want to regenerate the snapshot unless we have to. In CI it's probable
|
||||
# that the cache being restored with the correct binary will be rebuilt since the timestamps
|
||||
# and local checksums will not line up.
|
||||
deps: [tools, snapshot]
|
||||
deps: [tools]
|
||||
cmds:
|
||||
- cmd: "echo 'testing binary: {{ .SNAPSHOT_BIN }}'"
|
||||
silent: true
|
||||
@@ -250,27 +249,6 @@ tasks:
|
||||
- cmd: .github/scripts/labeler_test.py
|
||||
|
||||
|
||||
## Benchmark test targets #################################
|
||||
|
||||
benchmark:
|
||||
deps: [tmpdir]
|
||||
generates:
|
||||
- "{{ .TMP_DIR }}/benchmark-main.txt"
|
||||
cmds:
|
||||
- "go test -p 1 -run=^Benchmark -bench=. -count=7 -benchmem ./... | tee {{ .TMP_DIR }}/benchmark-{{ .VERSION }}.txt"
|
||||
- |
|
||||
bash -c "(test -s {{ .TMP_DIR }}/benchmark-main.txt && \
|
||||
{{ .TOOL_DIR }}/benchstat {{ .TMP_DIR }}/benchmark-main.txt {{ .TMP_DIR }}/benchmark-{{ .VERSION }}.txt || \
|
||||
{{ .TOOL_DIR }}/benchstat {{ .TMP_DIR }}/benchmark-{{ .VERSION }}.txt) \
|
||||
| tee {{ .TMP_DIR }}/benchstat.txt"
|
||||
|
||||
show-benchstat:
|
||||
deps: [benchmark, tmpdir]
|
||||
cmds:
|
||||
- cmd: "cat {{ .TMP_DIR }}/benchstat.txt"
|
||||
silent: true
|
||||
|
||||
|
||||
## Test-fixture-related targets #################################
|
||||
|
||||
fingerprints:
|
||||
@@ -452,8 +430,8 @@ tasks:
|
||||
find . | grep cache | grep tar | xargs du
|
||||
echo "total $total_size KB"
|
||||
|
||||
if [ "$total_size" -gt 1048576 ]; then
|
||||
echo 'docker cache is larger than 1GB'
|
||||
if [ "$total_size" -gt 5242880 ]; then
|
||||
echo 'docker cache is larger than 5GB'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -573,6 +551,18 @@ tasks:
|
||||
|
||||
- "{{ .SNAPSHOT_CMD }}"
|
||||
|
||||
snapshot-smoke-test:
|
||||
desc: Run a smoke test on the snapshot builds + docker images
|
||||
cmds:
|
||||
- cmd: "echo 'testing snapshot binary: {{ .SNAPSHOT_BIN }}'"
|
||||
silent: true
|
||||
- cmd: "test -f {{ .SNAPSHOT_BIN }} || (find {{ .SNAPSHOT_DIR }} && echo '\nno snapshot found for {{ .SNAPSHOT_BIN }}' && false)"
|
||||
silent: true
|
||||
- "{{ .SNAPSHOT_BIN }} version"
|
||||
- "{{ .SNAPSHOT_BIN }} scan alpine:latest"
|
||||
- docker run --rm anchore/syft:latest version
|
||||
- docker run --rm anchore/syft:latest scan alpine:latest
|
||||
|
||||
changelog:
|
||||
desc: Generate a changelog
|
||||
deps: [tools]
|
||||
|
||||
+6
-1
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/cmd/syft/internal"
|
||||
"github.com/anchore/syft/cmd/syft/internal/commands"
|
||||
constants "github.com/anchore/syft/internal"
|
||||
)
|
||||
|
||||
// Application constructs the `syft packages` command and aliases the root command to `syft packages`.
|
||||
@@ -47,7 +48,7 @@ func create(id clio.Identification, out io.Writer) (clio.Application, *cobra.Com
|
||||
commands.Cataloger(app),
|
||||
commands.Attest(app),
|
||||
commands.Convert(app),
|
||||
clio.VersionCommand(id),
|
||||
clio.VersionCommand(id, schemaVersion),
|
||||
clio.ConfigCommand(app, nil),
|
||||
cranecmd.NewCmdAuthLogin(id.Name), // syft login uses the same command as crane
|
||||
)
|
||||
@@ -59,3 +60,7 @@ func create(id clio.Identification, out io.Writer) (clio.Application, *cobra.Com
|
||||
|
||||
return app, rootCmd
|
||||
}
|
||||
|
||||
func schemaVersion() (string, any) {
|
||||
return "SchemaVersion", constants.JSONSchemaVersion
|
||||
}
|
||||
|
||||
@@ -158,8 +158,8 @@ func (cfg Catalog) ToFilesConfig() filecataloging.Config {
|
||||
|
||||
func (cfg Catalog) ToLicenseConfig() cataloging.LicenseConfig {
|
||||
return cataloging.LicenseConfig{
|
||||
IncludeUnkownLicenseContent: cfg.License.IncludeUnknownLicenseContent,
|
||||
Coverage: cfg.License.LicenseCoverage,
|
||||
IncludeContent: cfg.License.Content,
|
||||
Coverage: cfg.License.Coverage,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
)
|
||||
|
||||
type licenseConfig struct {
|
||||
IncludeUnknownLicenseContent bool `yaml:"include-unknown-license-content" json:"include-unknown-license-content" mapstructure:"include-unknown-license-content"`
|
||||
LicenseCoverage float64 `yaml:"license-coverage" json:"license-coverage" mapstructure:"license-coverage"`
|
||||
Content cataloging.LicenseContent `yaml:"content" json:"content" mapstructure:"content"`
|
||||
Coverage float64 `yaml:"coverage" json:"coverage" mapstructure:"coverage"`
|
||||
AvailableLicenseContent []cataloging.LicenseContent `yaml:"-" json:"-" mapstructure:"-"`
|
||||
}
|
||||
|
||||
var _ interface {
|
||||
@@ -14,15 +19,29 @@ var _ interface {
|
||||
} = (*licenseConfig)(nil)
|
||||
|
||||
func (o *licenseConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
|
||||
descriptions.Add(&o.IncludeUnknownLicenseContent, `include the content of a license in the SBOM when syft
|
||||
cannot determine a valid SPDX ID for the given license`)
|
||||
descriptions.Add(&o.LicenseCoverage, `adjust the percent as a fraction of the total text, in normalized words, that
|
||||
descriptions.Add(&o.Content, fmt.Sprintf("include the content of licenses in the SBOM for a given syft scan; valid values are: %s", o.AvailableLicenseContent))
|
||||
|
||||
descriptions.Add(&o.Coverage, `adjust the percent as a fraction of the total text, in normalized words, that
|
||||
matches any valid license for the given inputs, expressed as a percentage across all of the licenses matched.`)
|
||||
}
|
||||
|
||||
func (o *licenseConfig) PostLoad() error {
|
||||
validContent := internal.NewSet(o.AvailableLicenseContent...)
|
||||
if !validContent.Contains(o.Content) {
|
||||
return fmt.Errorf("could not use %q as license content option; valid values are: %v", o.Content, validContent.ToSlice())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func defaultLicenseConfig() licenseConfig {
|
||||
cfg := cataloging.DefaultLicenseConfig()
|
||||
return licenseConfig{
|
||||
IncludeUnknownLicenseContent: false,
|
||||
LicenseCoverage: 75,
|
||||
Content: cfg.IncludeContent,
|
||||
Coverage: cfg.Coverage,
|
||||
AvailableLicenseContent: []cataloging.LicenseContent{
|
||||
cataloging.LicenseContentIncludeAll,
|
||||
cataloging.LicenseContentIncludeUnknown,
|
||||
cataloging.LicenseContentExcludeAll,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||
"github.com/anchore/syft/syft/format/cyclonedxxml"
|
||||
"github.com/anchore/syft/syft/format/github"
|
||||
"github.com/anchore/syft/syft/format/purls"
|
||||
"github.com/anchore/syft/syft/format/spdxjson"
|
||||
"github.com/anchore/syft/syft/format/spdxtagvalue"
|
||||
"github.com/anchore/syft/syft/format/syftjson"
|
||||
@@ -127,6 +128,7 @@ func supportedIDs() []sbom.FormatID {
|
||||
table.ID,
|
||||
text.ID,
|
||||
template.ID,
|
||||
purls.ID,
|
||||
|
||||
// encoders that support multiple versions
|
||||
cyclonedxxml.ID,
|
||||
|
||||
@@ -501,6 +501,13 @@ var commonTestCases = []testCase{
|
||||
"Akismet Anti-spam: Spam Protection": "5.3",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "find homebrew",
|
||||
pkgType: pkg.HomebrewPkg,
|
||||
pkgInfo: map[string]string{
|
||||
"afflib": "1.2.3",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "find php pear/pecl package",
|
||||
pkgType: pkg.PhpPearPkg,
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestPackageDeduplication(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
scope: source.AllLayersScope,
|
||||
packageCount: 178, // without deduplication this would be ~600
|
||||
packageCount: 175, // without deduplication this would be ~600
|
||||
instanceCount: map[string]int{
|
||||
"basesystem": 1,
|
||||
"wget": 1,
|
||||
@@ -30,15 +30,15 @@ func TestPackageDeduplication(t *testing.T) {
|
||||
locationCount: map[string]int{
|
||||
"basesystem-11-13.el9": 5, // in all layers
|
||||
"curl-minimal-7.76.1-26.el9_3.2.0.1": 2, // base + wget layer
|
||||
"curl-minimal-7.76.1-29.el9_4.1": 3, // curl upgrade layer + all above layers
|
||||
"curl-minimal-7.76.1-31.el9": 3, // curl upgrade layer + all above layers
|
||||
"wget-1.21.1-8.el9_4": 4, // wget + all above layers
|
||||
"vsftpd-3.0.5-5.el9": 2, // vsftpd + all above layers
|
||||
"httpd-2.4.57-11.el9_4.1": 1, // last layer
|
||||
"vsftpd-3.0.5-6.el9": 2, // vsftpd + all above layers
|
||||
"httpd-2.4.62-1.el9_5.2": 1, // last layer
|
||||
},
|
||||
},
|
||||
{
|
||||
scope: source.SquashedScope,
|
||||
packageCount: 172,
|
||||
packageCount: 169,
|
||||
instanceCount: map[string]int{
|
||||
"basesystem": 1,
|
||||
"wget": 1,
|
||||
@@ -47,11 +47,11 @@ func TestPackageDeduplication(t *testing.T) {
|
||||
"httpd": 1, // rpm, binary is now excluded by overlap
|
||||
},
|
||||
locationCount: map[string]int{
|
||||
"basesystem-11-13.el9": 1,
|
||||
"curl-minimal-7.76.1-29.el9_4.1": 1, // upgrade
|
||||
"wget-1.21.1-8.el9_4": 1,
|
||||
"vsftpd-3.0.5-5.el9": 1,
|
||||
"httpd-2.4.57-11.el9_4.1": 1,
|
||||
"basesystem-11-13.el9": 1,
|
||||
"curl-minimal-7.76.1-31.el9": 1, // upgrade
|
||||
"wget-1.21.1-8.el9_4": 1,
|
||||
"vsftpd-3.0.5-6.el9": 1,
|
||||
"httpd-2.4.62-1.el9_5.2": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,18 +3,18 @@ package integration
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func TestPhotonPackageRegression(t *testing.T) { // Regression: https://github.com/anchore/syft/pull/1997
|
||||
sbom, _ := catalogFixtureImage(t, "image-photon-all-layers", source.AllLayersScope)
|
||||
var packages []pkg.Package
|
||||
for p := range sbom.Artifacts.Packages.Enumerate() {
|
||||
packages = append(packages, p)
|
||||
var count int
|
||||
for range sbom.Artifacts.Packages.Enumerate(pkg.RpmPkg) {
|
||||
count++
|
||||
}
|
||||
|
||||
if len(packages) < 1 {
|
||||
t.Errorf("failed to find packages for photon distro; wanted > 0 got 0")
|
||||
}
|
||||
assert.Greater(t, count, 0, "expected to find RPM packages in the SBOM (but did not)")
|
||||
}
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
desc "Advanced Forensic Format"
|
||||
homepage "https://github.com/sshock/AFFLIBv3"
|
||||
@@ -19,11 +19,12 @@ require (
|
||||
github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d
|
||||
github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722
|
||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb
|
||||
github.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec
|
||||
github.com/anchore/go-sync v0.0.0-20250326131806-4eda43a485b6
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||
github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115
|
||||
github.com/anchore/stereoscope v0.1.4
|
||||
github.com/anchore/stereoscope v0.1.5-0.20250604132324-344e29f37f05
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/aquasecurity/go-pep440-version v0.0.1
|
||||
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
|
||||
@@ -39,14 +40,14 @@ require (
|
||||
github.com/elliotchance/phpserialize v1.4.0
|
||||
github.com/facebookincubator/nvdtools v0.1.5
|
||||
github.com/github/go-spdx/v2 v2.3.3
|
||||
github.com/gkampitakis/go-snaps v0.5.11
|
||||
github.com/gkampitakis/go-snaps v0.5.13
|
||||
github.com/go-git/go-billy/v5 v5.6.2
|
||||
github.com/go-git/go-git/v5 v5.16.0
|
||||
github.com/go-git/go-git/v5 v5.16.2
|
||||
github.com/go-test/deep v1.1.1
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1
|
||||
github.com/gohugoio/hashstructure v0.5.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/go-containerregistry v0.20.3
|
||||
github.com/google/go-containerregistry v0.20.5
|
||||
github.com/google/licensecheck v0.3.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gookit/color v1.5.4
|
||||
@@ -57,9 +58,8 @@ require (
|
||||
github.com/jedib0t/go-pretty/v6 v6.6.7
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
|
||||
github.com/knqyf263/go-rpmdb v0.1.1
|
||||
github.com/magiconair/properties v1.8.10
|
||||
github.com/mholt/archives v0.1.1
|
||||
github.com/mholt/archives v0.1.2
|
||||
github.com/moby/sys/mountinfo v0.7.2
|
||||
github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
@@ -72,7 +72,7 @@ require (
|
||||
github.com/sassoftware/go-rpmutils v0.4.0
|
||||
// pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5
|
||||
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
|
||||
github.com/sergi/go-diff v1.4.0
|
||||
github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb
|
||||
github.com/spdx/tools-golang v0.5.5
|
||||
github.com/spf13/afero v1.14.0
|
||||
@@ -85,11 +85,11 @@ require (
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1
|
||||
go.uber.org/goleak v1.3.0
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
|
||||
golang.org/x/mod v0.24.0
|
||||
golang.org/x/net v0.40.0
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
|
||||
golang.org/x/mod v0.25.0
|
||||
golang.org/x/net v0.41.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.37.0
|
||||
modernc.org/sqlite v1.38.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -100,11 +100,11 @@ require (
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.7 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.2.0 // indirect
|
||||
github.com/STARRY-S/zip v0.2.1 // indirect
|
||||
github.com/agext/levenshtein v1.2.1 // indirect; indirectt
|
||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/aquasecurity/go-version v0.0.1 // indirect
|
||||
@@ -133,10 +133,10 @@ require (
|
||||
github.com/containerd/typeurl/v2 v2.1.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/cli v28.1.1+incompatible // indirect
|
||||
github.com/docker/cli v28.2.2+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v28.1.1+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
@@ -147,13 +147,13 @@ require (
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gkampitakis/ciinfo v0.3.1 // indirect
|
||||
github.com/gkampitakis/ciinfo v0.3.2 // indirect
|
||||
github.com/gkampitakis/go-diff v1.3.2 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-restruct/restruct v1.2.0-alpha // indirect
|
||||
github.com/goccy/go-yaml v1.15.13 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
@@ -226,7 +226,7 @@ require (
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/vbatts/tar-split v0.11.6 // indirect
|
||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
@@ -235,31 +235,32 @@ require (
|
||||
github.com/zclconf/go-cty v1.13.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
|
||||
go.opentelemetry.io/otel v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.33.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
|
||||
google.golang.org/grpc v1.67.3 // indirect
|
||||
google.golang.org/protobuf v1.36.4 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
modernc.org/libc v1.62.1 // indirect
|
||||
modernc.org/libc v1.65.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.9.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
retract (
|
||||
v1.25.0 // published with a replace directive (confusing for API users)
|
||||
v0.53.2
|
||||
v0.53.1 // Published accidentally with incorrect license in depdencies
|
||||
v0.53.1 // published accidentally with incorrect license in depdencies
|
||||
)
|
||||
|
||||
@@ -78,8 +78,8 @@ github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs=
|
||||
github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
|
||||
github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
|
||||
@@ -110,6 +110,8 @@ github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 h1:2SqmFgE7h+Ql4
|
||||
github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722/go.mod h1:oFuE8YuTCM+spgMXhePGzk3asS94yO9biUfDzVTFqNw=
|
||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU=
|
||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec h1:SjjPMOXTzpuU1ZME4XeoHyek+dry3/C7I8gzaCo02eg=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec/go.mod h1:eQVa6QFGzKy0qMcnW2pez0XBczvgwSjw9vA23qifEyU=
|
||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
|
||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=
|
||||
github.com/anchore/go-sync v0.0.0-20250326131806-4eda43a485b6 h1:Ha+LSCVuXYSYGi7wIkJK6G8g6jI3LH7y6LbyEVyp4Io=
|
||||
@@ -120,11 +122,11 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
|
||||
github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiEjnoGJZ1+Ah0ZZ/mKKqNhGcUZBl0s7PTTDzvY=
|
||||
github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI=
|
||||
github.com/anchore/stereoscope v0.1.4 h1:e+iT9UdUzLBabWGe84hn5sTHDRioY+4IHsVzJXuJlek=
|
||||
github.com/anchore/stereoscope v0.1.4/go.mod h1:omWgXDEp/XfqCJlZXIByEo1c3ArZg/qTJ5LBKVLAIdw=
|
||||
github.com/anchore/stereoscope v0.1.5-0.20250604132324-344e29f37f05 h1:MKTDwRrC7A+eRZvGFJ8TzLiKytFH1GBpPdjLTlcoO4A=
|
||||
github.com/anchore/stereoscope v0.1.5-0.20250604132324-344e29f37f05/go.mod h1:S5xxMIo1BK+V+p+6SF/wzS4pZ2cTnpk6L+UJbf5IjsQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ=
|
||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
@@ -262,14 +264,14 @@ github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da/go.mod h1:B3tI9iGHi4i
|
||||
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k=
|
||||
github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A=
|
||||
github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I=
|
||||
github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
|
||||
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
|
||||
@@ -323,12 +325,12 @@ github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFA
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/github/go-spdx/v2 v2.3.3 h1:QI7evnHWEfWkT54eJwkoV/f3a0xD3gLlnVmT5wQG6LE=
|
||||
github.com/github/go-spdx/v2 v2.3.3/go.mod h1:2ZxKsOhvBp+OYBDlsGnUMcchLeo2mrpEBn2L1C+U3IQ=
|
||||
github.com/gkampitakis/ciinfo v0.3.1 h1:lzjbemlGI4Q+XimPg64ss89x8Mf3xihJqy/0Mgagapo=
|
||||
github.com/gkampitakis/ciinfo v0.3.1/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
|
||||
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
|
||||
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
|
||||
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
|
||||
github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
|
||||
github.com/gkampitakis/go-snaps v0.5.11 h1:LFG0ggUKR+KEiiaOvFCmLgJ5NO2zf93AxxddkBn3LdQ=
|
||||
github.com/gkampitakis/go-snaps v0.5.11/go.mod h1:PcKmy8q5Se7p48ywpogN5Td13reipz1Iivah4wrTIvY=
|
||||
github.com/gkampitakis/go-snaps v0.5.13 h1:Hhjmvv1WboSCxkR9iU2mj5PQ8tsz/y8ECGrIbjjPF8Q=
|
||||
github.com/gkampitakis/go-snaps v0.5.13/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
|
||||
github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
|
||||
github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
@@ -339,8 +341,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ=
|
||||
github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
|
||||
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@@ -364,8 +366,8 @@ github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
github.com/goccy/go-yaml v1.15.13 h1:Xd87Yddmr2rC1SLLTm2MNDcTjeO/GYo0JGiww6gSTDg=
|
||||
github.com/goccy/go-yaml v1.15.13/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
@@ -426,8 +428,8 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
|
||||
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
|
||||
github.com/google/go-containerregistry v0.20.5 h1:4RnlYcDs5hoA++CeFjlbZ/U9Yp1EuWr+UhhTyYQjOP0=
|
||||
github.com/google/go-containerregistry v0.20.5/go.mod h1:Q14vdOOzug02bwnhMkZKD4e30pDaD9W65qzXpyzF49E=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs=
|
||||
github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY=
|
||||
@@ -548,8 +550,6 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/knqyf263/go-rpmdb v0.1.1 h1:oh68mTCvp1XzxdU7EfafcWzzfstUZAEa3MW0IJye584=
|
||||
github.com/knqyf263/go-rpmdb v0.1.1/go.mod h1:9LQcoMCMQ9vrF7HcDtXfvqGO4+ddxFQ8+YF/0CVGDww=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
@@ -600,8 +600,8 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mholt/archives v0.1.1 h1:c7J3qXN1FB54y0qiUXiq9Bxk4eCUc8pdXWwOhZdRzeY=
|
||||
github.com/mholt/archives v0.1.1/go.mod h1:FQVz01Q2uXKB/35CXeW/QFO23xT+hSCGZHVtha78U4I=
|
||||
github.com/mholt/archives v0.1.2 h1:UBSe5NfYKHI1sy+S5dJsEsG9jsKKk8NJA4HCC+xTI4A=
|
||||
github.com/mholt/archives v0.1.2/go.mod h1:D7QzTHgw3ctfS6wgOO9dN+MFgdZpbksGCxprUOwZWDs=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
@@ -751,8 +751,8 @@ github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEW
|
||||
github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
@@ -836,8 +836,8 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/vbatts/go-mtree v0.5.4 h1:OMAb8jaCyiFA7zXj0Zc/oARcxBDBoeu2LizjB8BVJl0=
|
||||
github.com/vbatts/go-mtree v0.5.4/go.mod h1:5GqJbVhm9BBiCc4K5uc/c42FPgXulHaQs4sFUEfIWMo=
|
||||
github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
|
||||
github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
|
||||
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
|
||||
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
||||
github.com/vifraa/gopom v1.0.0 h1:L9XlKbyvid8PAIK8nr0lihMApJQg/12OBvMA28BcWh0=
|
||||
github.com/vifraa/gopom v1.0.0/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o=
|
||||
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA=
|
||||
@@ -885,20 +885,22 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
||||
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
|
||||
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
|
||||
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
|
||||
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
|
||||
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
@@ -925,8 +927,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -937,8 +939,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -966,8 +968,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1012,8 +1014,8 @@ golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1043,8 +1045,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1137,8 +1139,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1199,8 +1201,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1390,26 +1392,26 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic=
|
||||
modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU=
|
||||
modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
|
||||
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA=
|
||||
modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s=
|
||||
modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo=
|
||||
modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc=
|
||||
modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
|
||||
modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
||||
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
||||
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
|
||||
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package cmptest
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/anchore/syft/internal/evidence"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
@@ -29,9 +32,41 @@ func BuildOptions(licenseCmp LicenseComparer, locationCmp LocationComparer) []cm
|
||||
cmpopts.IgnoreFields(pkg.Package{}, "id"), // note: ID is not deterministic for test purposes
|
||||
cmpopts.SortSlices(pkg.Less),
|
||||
cmpopts.SortSlices(DefaultRelationshipComparer),
|
||||
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](locationCmp)),
|
||||
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](locationCmp, locationSorter)),
|
||||
cmp.Comparer(buildSetComparer[pkg.License, pkg.LicenseSet](licenseCmp)),
|
||||
cmp.Comparer(locationCmp),
|
||||
cmp.Comparer(licenseCmp),
|
||||
}
|
||||
}
|
||||
|
||||
// LocationSorter always sorts by evidence annotations first, then by access path, then by real path.
|
||||
// This intentionally does not consider layer details since some test fixtures have no layer information
|
||||
// on the left side of the comparison (expected) and does on the right side (actual).
|
||||
func locationSorter(a, b file.Location) int {
|
||||
// compare by evidence annotations first...
|
||||
aEvidence := a.Annotations[evidence.AnnotationKey]
|
||||
bEvidence := b.Annotations[evidence.AnnotationKey]
|
||||
|
||||
if aEvidence != bEvidence {
|
||||
if aEvidence == evidence.PrimaryAnnotation {
|
||||
return -1
|
||||
}
|
||||
if bEvidence == evidence.PrimaryAnnotation {
|
||||
return 1
|
||||
}
|
||||
|
||||
if aEvidence > bEvidence {
|
||||
return -1
|
||||
}
|
||||
if bEvidence > aEvidence {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// ...then by paths
|
||||
if a.AccessPath != b.AccessPath {
|
||||
return strings.Compare(a.AccessPath, b.AccessPath)
|
||||
}
|
||||
|
||||
return strings.Compare(a.RealPath, b.RealPath)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ func DefaultLicenseComparer(x, y pkg.License) bool {
|
||||
return cmp.Equal(
|
||||
x, y,
|
||||
cmp.Comparer(DefaultLocationComparer),
|
||||
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](DefaultLocationComparer)),
|
||||
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](DefaultLocationComparer, locationSorter)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,6 @@ func LicenseComparerWithoutLocationLayer(x, y pkg.License) bool {
|
||||
return cmp.Equal(
|
||||
x, y,
|
||||
cmp.Comparer(LocationComparerWithoutLayer),
|
||||
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](LocationComparerWithoutLayer)),
|
||||
cmp.Comparer(buildSetComparer[file.Location, file.LocationSet](LocationComparerWithoutLayer, locationSorter)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package cmptest
|
||||
|
||||
import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
@@ -9,7 +10,7 @@ import (
|
||||
type LocationComparer func(x, y file.Location) bool
|
||||
|
||||
func DefaultLocationComparer(x, y file.Location) bool {
|
||||
return cmp.Equal(x.Coordinates, y.Coordinates) && cmp.Equal(x.AccessPath, y.AccessPath)
|
||||
return cmp.Equal(x.Coordinates, y.Coordinates, cmpopts.IgnoreUnexported(file.Coordinates{})) && cmp.Equal(x.AccessPath, y.AccessPath)
|
||||
}
|
||||
|
||||
func LocationComparerWithoutLayer(x, y file.Location) bool {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package cmptest
|
||||
|
||||
type slicer[T any] interface {
|
||||
ToSlice() []T
|
||||
ToSlice(sorter ...func(a, b T) int) []T
|
||||
}
|
||||
|
||||
func buildSetComparer[T any, S slicer[T]](l func(x, y T) bool) func(x, y S) bool {
|
||||
func buildSetComparer[T any, S slicer[T]](l func(x, y T) bool, sorters ...func(a, b T) int) func(x, y S) bool {
|
||||
return func(x, y S) bool {
|
||||
xs := x.ToSlice()
|
||||
ys := y.ToSlice()
|
||||
xs := x.ToSlice(sorters...)
|
||||
ys := y.ToSlice(sorters...)
|
||||
|
||||
if len(xs) != len(ys) {
|
||||
return false
|
||||
|
||||
@@ -3,5 +3,5 @@ package internal
|
||||
const (
|
||||
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
||||
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
||||
JSONSchemaVersion = "16.0.28"
|
||||
JSONSchemaVersion = "16.0.34"
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func TestSetContextLicenseScanner(t *testing.T) {
|
||||
scanner := testScanner(true)
|
||||
scanner := testScanner()
|
||||
ctx := context.Background()
|
||||
ctx = SetContextLicenseScanner(ctx, scanner)
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestSetContextLicenseScanner(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIsContextLicenseScannerSet(t *testing.T) {
|
||||
scanner := testScanner(true)
|
||||
scanner := testScanner()
|
||||
ctx := context.Background()
|
||||
require.False(t, IsContextLicenseScannerSet(ctx))
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestIsContextLicenseScannerSet(t *testing.T) {
|
||||
|
||||
func TestContextLicenseScanner(t *testing.T) {
|
||||
t.Run("with scanner", func(t *testing.T) {
|
||||
scanner := testScanner(true)
|
||||
scanner := testScanner()
|
||||
ctx := SetContextLicenseScanner(context.Background(), scanner)
|
||||
s, err := ContextLicenseScanner(ctx)
|
||||
if err != nil || s != scanner {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package licenses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
func (s *scanner) FindEvidence(_ context.Context, reader io.Reader) (evidence []Evidence, content []byte, err error) {
|
||||
if s.scanner == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
content, err = io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
cov := s.scanner(content)
|
||||
if cov.Percent < s.coverageThreshold {
|
||||
// unknown or no licenses here
|
||||
// => check return content to Search to process
|
||||
return nil, content, nil
|
||||
}
|
||||
|
||||
evidence = make([]Evidence, 0)
|
||||
for _, m := range cov.Match {
|
||||
evidence = append(evidence, Evidence{
|
||||
ID: m.ID,
|
||||
Type: m.Type,
|
||||
Start: m.Start,
|
||||
End: m.End,
|
||||
IsURL: m.IsURL,
|
||||
})
|
||||
}
|
||||
return evidence, content, nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package licenses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/licensecheck"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDefaultScanner_FindEvidence(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
fixture string
|
||||
wantIDs []string // expected license IDs
|
||||
minMatch int // minimum # of matches required
|
||||
}{
|
||||
{
|
||||
name: "Single licenses are able to be recognized and returned Apache 2.0",
|
||||
fixture: "test-fixtures/apache-license-2.0",
|
||||
wantIDs: []string{"Apache-2.0"},
|
||||
minMatch: 1,
|
||||
},
|
||||
{
|
||||
name: "Multiple Licenses are returned as evidence with duplicates at different offset",
|
||||
fixture: "test-fixtures/multi-license",
|
||||
wantIDs: []string{
|
||||
"MIT",
|
||||
"MIT",
|
||||
"NCSA",
|
||||
"Apache-2.0",
|
||||
"Zlib",
|
||||
"Unlicense",
|
||||
"BSD-2-Clause",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
},
|
||||
minMatch: 2,
|
||||
},
|
||||
}
|
||||
|
||||
scanner := testScanner()
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
filePath := filepath.Clean(tc.fixture)
|
||||
f, err := os.Open(filePath)
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
evidence, content, err := scanner.FindEvidence(context.Background(), f)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, content)
|
||||
require.GreaterOrEqual(t, len(evidence), tc.minMatch, "expected at least %d matches", tc.minMatch)
|
||||
|
||||
var foundIDs []string
|
||||
for _, ev := range evidence {
|
||||
foundIDs = append(foundIDs, ev.ID)
|
||||
}
|
||||
|
||||
require.ElementsMatch(t, tc.wantIDs, foundIDs, "expected license IDs %v, but got %v", tc.wantIDs, foundIDs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testScanner() Scanner {
|
||||
return &scanner{
|
||||
coverageThreshold: DefaultCoverageThreshold,
|
||||
scanner: licensecheck.Scan,
|
||||
}
|
||||
}
|
||||
|
||||
func mustOpen(fixture string) []byte {
|
||||
content, err := os.ReadFile(fixture)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
@@ -8,33 +8,37 @@ import (
|
||||
"github.com/google/licensecheck"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultCoverageThreshold = 75 // determined by experimentation
|
||||
DefaultIncludeLicenseContent = false
|
||||
UnknownLicensePrefix = unknownLicenseType + "_"
|
||||
DefaultCoverageThreshold = 75 // determined by experimentation
|
||||
|
||||
unknownLicenseType = "UNKNOWN"
|
||||
)
|
||||
|
||||
type Evidence struct {
|
||||
ID string // License identifier. (See licenses/README.md.)
|
||||
Type licensecheck.Type // The type of the license: BSD, MIT, etc.
|
||||
Start int // Start offset of match in text; match is at text[Start:End].
|
||||
End int // End offset of match in text.
|
||||
IsURL bool // Whether match is a URL.
|
||||
}
|
||||
|
||||
type Scanner interface {
|
||||
IdentifyLicenseIDs(context.Context, io.Reader) ([]string, []byte, error)
|
||||
FileSearch(context.Context, file.LocationReadCloser) ([]file.License, error)
|
||||
PkgSearch(context.Context, file.LocationReadCloser) ([]pkg.License, error)
|
||||
FindEvidence(context.Context, io.Reader) ([]Evidence, []byte, error)
|
||||
}
|
||||
|
||||
var _ Scanner = (*scanner)(nil)
|
||||
|
||||
type scanner struct {
|
||||
coverageThreshold float64 // between 0 and 100
|
||||
includeLicenseContent bool
|
||||
scanner func([]byte) licensecheck.Coverage
|
||||
coverageThreshold float64 // between 0 and 100
|
||||
scanner func([]byte) licensecheck.Coverage
|
||||
}
|
||||
|
||||
type ScannerConfig struct {
|
||||
CoverageThreshold float64
|
||||
IncludeLicenseContent bool
|
||||
Scanner func([]byte) licensecheck.Coverage
|
||||
CoverageThreshold float64
|
||||
Scanner func([]byte) licensecheck.Coverage
|
||||
}
|
||||
|
||||
type Option func(*scanner)
|
||||
@@ -45,12 +49,6 @@ func WithCoverage(coverage float64) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithIncludeLicenseContent(includeLicenseContent bool) Option {
|
||||
return func(s *scanner) {
|
||||
s.includeLicenseContent = includeLicenseContent
|
||||
}
|
||||
}
|
||||
|
||||
// NewDefaultScanner returns a scanner that uses a new instance of the default licensecheck package scanner.
|
||||
func NewDefaultScanner(o ...Option) (Scanner, error) {
|
||||
s, err := licensecheck.NewScanner(licensecheck.BuiltinLicenses())
|
||||
@@ -58,10 +56,10 @@ func NewDefaultScanner(o ...Option) (Scanner, error) {
|
||||
log.WithFields("error", err).Trace("unable to create default license scanner")
|
||||
return nil, fmt.Errorf("unable to create default license scanner: %w", err)
|
||||
}
|
||||
|
||||
newScanner := &scanner{
|
||||
coverageThreshold: DefaultCoverageThreshold,
|
||||
includeLicenseContent: DefaultIncludeLicenseContent,
|
||||
scanner: s.Scan,
|
||||
coverageThreshold: DefaultCoverageThreshold,
|
||||
scanner: s.Scan,
|
||||
}
|
||||
|
||||
for _, opt := range o {
|
||||
@@ -78,8 +76,7 @@ func NewScanner(c *ScannerConfig) (Scanner, error) {
|
||||
}
|
||||
|
||||
return &scanner{
|
||||
coverageThreshold: c.CoverageThreshold,
|
||||
includeLicenseContent: c.IncludeLicenseContent,
|
||||
scanner: c.Scanner,
|
||||
coverageThreshold: c.CoverageThreshold,
|
||||
scanner: c.Scanner,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
package licenses
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/licensecheck"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIdentifyLicenseIDs(t *testing.T) {
|
||||
type expectation struct {
|
||||
yieldError bool
|
||||
ids []string
|
||||
content []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
expected expectation
|
||||
}{
|
||||
{
|
||||
name: "apache license 2.0",
|
||||
in: `test-fixtures/apache-license-2.0`,
|
||||
expected: expectation{
|
||||
yieldError: false,
|
||||
ids: []string{"Apache-2.0"},
|
||||
content: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom license includes content for IdentifyLicenseIDs",
|
||||
in: "test-fixtures/nvidia-software-and-cuda-supplement",
|
||||
expected: expectation{
|
||||
yieldError: false,
|
||||
ids: []string{},
|
||||
content: mustOpen("test-fixtures/nvidia-software-and-cuda-supplement"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
content, err := os.ReadFile(test.in)
|
||||
require.NoError(t, err)
|
||||
ids, content, err := testScanner(false).IdentifyLicenseIDs(context.TODO(), bytes.NewReader(content))
|
||||
if test.expected.yieldError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, ids, len(test.expected.ids))
|
||||
require.Len(t, content, len(test.expected.content))
|
||||
|
||||
if len(test.expected.ids) > 0 {
|
||||
require.Equal(t, ids, test.expected.ids)
|
||||
}
|
||||
|
||||
if len(test.expected.content) > 0 {
|
||||
require.Equal(t, content, test.expected.content)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testScanner(includeLicenseContent bool) Scanner {
|
||||
return &scanner{
|
||||
coverageThreshold: DefaultCoverageThreshold,
|
||||
includeLicenseContent: includeLicenseContent,
|
||||
scanner: licensecheck.Scan,
|
||||
}
|
||||
}
|
||||
|
||||
func mustOpen(fixture string) []byte {
|
||||
content, err := os.ReadFile(fixture)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
package licenses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/license"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
const (
|
||||
unknownLicenseType = "UNKNOWN"
|
||||
UnknownLicensePrefix = unknownLicenseType + "_"
|
||||
)
|
||||
|
||||
func getCustomLicenseContentHash(contents []byte) string {
|
||||
hash := sha256.Sum256(contents)
|
||||
return fmt.Sprintf("%x", hash[:])
|
||||
}
|
||||
|
||||
func (s *scanner) IdentifyLicenseIDs(_ context.Context, reader io.Reader) ([]string, []byte, error) {
|
||||
if s.scanner == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
cov := s.scanner(content)
|
||||
if cov.Percent < s.coverageThreshold {
|
||||
// unknown or no licenses here
|
||||
// => check return content to Search to process
|
||||
return nil, content, nil
|
||||
}
|
||||
|
||||
var ids []string
|
||||
for _, m := range cov.Match {
|
||||
ids = append(ids, m.ID)
|
||||
}
|
||||
return ids, nil, nil
|
||||
}
|
||||
|
||||
// PkgSearch scans the contents of a license file to attempt to determine the type of license it is
|
||||
func (s *scanner) PkgSearch(ctx context.Context, reader file.LocationReadCloser) (licenses []pkg.License, err error) {
|
||||
licenses = make([]pkg.License, 0)
|
||||
|
||||
ids, content, err := s.IdentifyLicenseIDs(ctx, reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// IdentifyLicenseIDs can only return a list of ID or content
|
||||
// These return values are mutually exclusive.
|
||||
// If the scanner threshold for matching scores < 75% then we return the license full content
|
||||
if len(ids) > 0 {
|
||||
for _, id := range ids {
|
||||
lic := pkg.NewLicenseFromLocations(id, reader.Location)
|
||||
lic.Type = license.Concluded
|
||||
|
||||
licenses = append(licenses, lic)
|
||||
}
|
||||
} else if len(content) > 0 {
|
||||
// harmonize line endings to unix compatible first:
|
||||
// 1. \r\n => \n (Windows => UNIX)
|
||||
// 2. \r => \n (Macintosh => UNIX)
|
||||
content = []byte(strings.ReplaceAll(strings.ReplaceAll(string(content), "\r\n", "\n"), "\r", "\n"))
|
||||
|
||||
lic := pkg.NewLicenseFromLocations(unknownLicenseType, reader.Location)
|
||||
lic.SPDXExpression = UnknownLicensePrefix + getCustomLicenseContentHash(content)
|
||||
if s.includeLicenseContent {
|
||||
lic.Contents = string(content)
|
||||
}
|
||||
lic.Type = license.Declared
|
||||
|
||||
licenses = append(licenses, lic)
|
||||
}
|
||||
|
||||
return licenses, nil
|
||||
}
|
||||
|
||||
// FileSearch scans the contents of a license file to attempt to determine the type of license it is
|
||||
func (s *scanner) FileSearch(ctx context.Context, reader file.LocationReadCloser) (licenses []file.License, err error) {
|
||||
licenses = make([]file.License, 0)
|
||||
|
||||
ids, content, err := s.IdentifyLicenseIDs(ctx, reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// IdentifyLicenseIDs can only return a list of ID or content
|
||||
// These return values are mutually exclusive.
|
||||
// If the scanner threshold for matching scores < 75% then we return the license full content
|
||||
if len(ids) > 0 {
|
||||
for _, id := range ids {
|
||||
lic := file.NewLicense(id)
|
||||
lic.Type = license.Concluded
|
||||
|
||||
licenses = append(licenses, lic)
|
||||
}
|
||||
} else if len(content) > 0 {
|
||||
// harmonize line endings to unix compatible first:
|
||||
// 1. \r\n => \n (Windows => UNIX)
|
||||
// 2. \r => \n (Macintosh => UNIX)
|
||||
content = []byte(strings.ReplaceAll(strings.ReplaceAll(string(content), "\r\n", "\n"), "\r", "\n"))
|
||||
|
||||
lic := file.NewLicense(unknownLicenseType)
|
||||
lic.SPDXExpression = UnknownLicensePrefix + getCustomLicenseContentHash(content)
|
||||
if s.includeLicenseContent {
|
||||
lic.Contents = string(content)
|
||||
}
|
||||
lic.Type = license.Declared
|
||||
|
||||
licenses = append(licenses, lic)
|
||||
}
|
||||
|
||||
return licenses, nil
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
package licenses
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
type bytesReadCloser struct {
|
||||
bytes.Buffer
|
||||
}
|
||||
|
||||
func (brc *bytesReadCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newBytesReadCloser(data []byte) *bytesReadCloser {
|
||||
return &bytesReadCloser{
|
||||
Buffer: *bytes.NewBuffer(data),
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchFileLicenses(t *testing.T) {
|
||||
type expectation struct {
|
||||
yieldError bool
|
||||
licenses []file.License
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
includeUnkownLicenseContent bool
|
||||
expected expectation
|
||||
}{
|
||||
{
|
||||
name: "apache license 2.0",
|
||||
in: "test-fixtures/apache-license-2.0",
|
||||
expected: expectation{
|
||||
yieldError: false,
|
||||
licenses: []file.License{
|
||||
{
|
||||
Value: "Apache-2.0",
|
||||
SPDXExpression: "Apache-2.0",
|
||||
Type: "concluded",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
content, err := os.ReadFile(test.in)
|
||||
require.NoError(t, err)
|
||||
s := testScanner(false)
|
||||
result, err := s.FileSearch(ctx, file.NewLocationReadCloser(file.NewLocation("LICENSE"), io.NopCloser(bytes.NewReader(content))))
|
||||
if test.expected.yieldError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, result, len(test.expected.licenses))
|
||||
|
||||
if len(test.expected.licenses) > 0 {
|
||||
require.Equal(t, test.expected.licenses, result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchPkgLicenses(t *testing.T) {
|
||||
type expectation struct {
|
||||
wantErr require.ErrorAssertionFunc
|
||||
licenses []pkg.License
|
||||
}
|
||||
|
||||
testLocation := file.NewLocation("LICENSE")
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
includeUnkownLicenseContent bool
|
||||
expected expectation
|
||||
}{
|
||||
{
|
||||
name: "apache license 2.0",
|
||||
in: "test-fixtures/apache-license-2.0",
|
||||
expected: expectation{
|
||||
licenses: []pkg.License{
|
||||
{
|
||||
Value: "Apache-2.0",
|
||||
SPDXExpression: "Apache-2.0",
|
||||
Type: "concluded",
|
||||
URLs: nil,
|
||||
Locations: file.NewLocationSet(testLocation),
|
||||
Contents: "",
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom license no content by default",
|
||||
in: "test-fixtures/nvidia-software-and-cuda-supplement",
|
||||
expected: expectation{
|
||||
licenses: []pkg.License{
|
||||
{
|
||||
Value: "UNKNOWN",
|
||||
SPDXExpression: "UNKNOWN_eebcea3ab1d1a28e671de90119ffcfb35fe86951e4af1b17af52b7a82fcf7d0a",
|
||||
Type: "declared",
|
||||
URLs: nil,
|
||||
Locations: file.NewLocationSet(testLocation),
|
||||
Contents: "",
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom license with content when scanner has content config",
|
||||
in: "test-fixtures/nvidia-software-and-cuda-supplement",
|
||||
includeUnkownLicenseContent: true,
|
||||
expected: expectation{
|
||||
licenses: []pkg.License{
|
||||
{
|
||||
Value: "UNKNOWN",
|
||||
SPDXExpression: "UNKNOWN_eebcea3ab1d1a28e671de90119ffcfb35fe86951e4af1b17af52b7a82fcf7d0a",
|
||||
Type: "declared",
|
||||
URLs: nil,
|
||||
Locations: file.NewLocationSet(testLocation),
|
||||
Contents: string(mustOpen("test-fixtures/nvidia-software-and-cuda-supplement")),
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
content, err := os.ReadFile(test.in)
|
||||
require.NoError(t, err)
|
||||
s := testScanner(test.includeUnkownLicenseContent)
|
||||
result, err := s.PkgSearch(ctx, file.NewLocationReadCloser(file.NewLocation("LICENSE"), io.NopCloser(bytes.NewReader(content))))
|
||||
if test.expected.wantErr != nil {
|
||||
test.expected.wantErr(t, err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, result, len(test.expected.licenses))
|
||||
|
||||
if len(test.expected.licenses) > 0 {
|
||||
require.Equal(t, test.expected.licenses, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
Emscripten is available under 2 licenses, the MIT license and the
|
||||
University of Illinois/NCSA Open Source License.
|
||||
|
||||
Both are permissive open source licenses, with little if any
|
||||
practical difference between them.
|
||||
|
||||
The reason for offering both is that (1) the MIT license is
|
||||
well-known, while (2) the University of Illinois/NCSA Open Source
|
||||
License allows Emscripten's code to be integrated upstream into
|
||||
LLVM, which uses that license, should the opportunity arise.
|
||||
|
||||
Additionally, the binaryen project is available under the Apache License
|
||||
Version 2.0.
|
||||
|
||||
The full text of all three licenses follows.
|
||||
|
||||
==============================================================================
|
||||
|
||||
Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
==============================================================================
|
||||
|
||||
Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file.
|
||||
All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal with the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimers.
|
||||
|
||||
Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimers
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
Neither the names of Mozilla,
|
||||
nor the names of its contributors may be used to endorse
|
||||
or promote products derived from this Software without specific prior
|
||||
written permission.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
||||
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE.
|
||||
|
||||
==============================================================================
|
||||
|
||||
This program uses portions of Node.js source code located in src/library_path.js,
|
||||
in accordance with the terms of the MIT license. Node's license follows:
|
||||
|
||||
"""
|
||||
Copyright Joyent, Inc. and other Node contributors. All rights reserved.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
==============================================================================
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
==============================================================================
|
||||
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2014 Sam Lantinga <slouken@libsdl.org>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
==============================================================================
|
||||
|
||||
Files: tools/filelock.py
|
||||
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org>
|
||||
|
||||
==============================================================================
|
||||
|
||||
Files: tools/eliminator/node_modules/uglify-js/... tools/node_modules/terser/...
|
||||
|
||||
Distributed under the BSD license:
|
||||
|
||||
Copyright 2012 (c) Mihai Bazon <mihai.bazon@gmail.com>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
* Redistributions of source code must retain the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials
|
||||
provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS” AND ANY
|
||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
||||
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
||||
|
||||
==============================================================================
|
||||
|
||||
Files: system/include/webgpu/webgpu.h
|
||||
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2019, "WebGPU native" developers
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
==============================================================================
|
||||
|
||||
Copyright (c) 2005-2011 David Schultz <das@FreeBSD.ORG>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
||||
@@ -92,6 +92,12 @@ func findOwnershipByFilesRelationships(resolver file.Resolver, catalog *pkg.Coll
|
||||
return relationships
|
||||
}
|
||||
|
||||
// Build a map of real paths to packages that directly own them. We'll use this
|
||||
// to check if a file is already owned by the same type of package when we're
|
||||
// determining ownership via symlink.
|
||||
directOwnership := directOwnersByPath(catalog)
|
||||
|
||||
// Now establish relationships, considering symlinks
|
||||
for _, candidateOwnerPkg := range catalog.Sorted() {
|
||||
id := candidateOwnerPkg.ID()
|
||||
if candidateOwnerPkg.Metadata == nil {
|
||||
@@ -105,16 +111,34 @@ func findOwnershipByFilesRelationships(resolver file.Resolver, catalog *pkg.Coll
|
||||
}
|
||||
|
||||
for _, ownedFilePath := range pkgFileOwner.OwnedFiles() {
|
||||
// find the first path that results in a hit
|
||||
for _, ownedPath := range allPaths(ownedFilePath, resolver) {
|
||||
if matchesAny(ownedPath, globsForbiddenFromBeingOwned) {
|
||||
if ownedFilePath == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// find paths that result in a hit (includes resolving symlinks)
|
||||
resolvedPaths := resolvePaths(ownedFilePath, resolver)
|
||||
|
||||
for _, resolvedPath := range resolvedPaths {
|
||||
if matchesAny(resolvedPath, globsForbiddenFromBeingOwned) {
|
||||
// we skip over known exceptions to file ownership, such as the RPM package owning
|
||||
// the RPM DB path, otherwise the RPM package would "own" all RPMs, which is not intended
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip claiming ownership via symlink if another package of the same type
|
||||
// directly owns this real path. This is the specific fix for the issue where a
|
||||
// symlink shouldn't allow a package to claim ownership when another package of
|
||||
// the same type directly owns the real file.
|
||||
if resolvedPath != ownedFilePath { // This is a resolved symlink path
|
||||
// Check if another package of the same type directly owns this path
|
||||
if paths := directOwnership[candidateOwnerPkg.Type]; paths != nil && paths.Has(resolvedPath) {
|
||||
// Skip this path - a package of the same type directly owns it
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// look for package(s) in the catalog that may be owned by this package and mark the relationship
|
||||
for _, subPackage := range catalog.PackagesByPath(ownedPath) {
|
||||
for _, subPackage := range catalog.PackagesByPath(resolvedPath) {
|
||||
subID := subPackage.ID()
|
||||
if subID == id {
|
||||
continue
|
||||
@@ -126,7 +150,7 @@ func findOwnershipByFilesRelationships(resolver file.Resolver, catalog *pkg.Coll
|
||||
if _, exists := relationships[id][subID]; !exists {
|
||||
relationships[id][subID] = strset.New()
|
||||
}
|
||||
relationships[id][subID].Add(ownedPath)
|
||||
relationships[id][subID].Add(resolvedPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,7 +159,40 @@ func findOwnershipByFilesRelationships(resolver file.Resolver, catalog *pkg.Coll
|
||||
return relationships
|
||||
}
|
||||
|
||||
func allPaths(ownedFilePath string, resolver file.Resolver) []string {
|
||||
func directOwnersByPath(catalog *pkg.Collection) map[pkg.Type]*strset.Set {
|
||||
directOwnership := map[pkg.Type]*strset.Set{}
|
||||
|
||||
// First, identify direct ownership of all files
|
||||
for _, p := range catalog.Sorted() {
|
||||
if p.Metadata == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// check to see if this is a file owner
|
||||
pkgFileOwner, ok := p.Metadata.(pkg.FileOwner)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ownedFilePath := range pkgFileOwner.OwnedFiles() {
|
||||
if ownedFilePath == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Register direct ownership
|
||||
paths := directOwnership[p.Type]
|
||||
if paths == nil {
|
||||
paths = strset.New()
|
||||
directOwnership[p.Type] = paths
|
||||
}
|
||||
paths.Add(ownedFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
return directOwnership
|
||||
}
|
||||
|
||||
func resolvePaths(ownedFilePath string, resolver file.Resolver) []string {
|
||||
// though we have a string path, we need to resolve symlinks and other filesystem oddities since we cannot assume this is a real path
|
||||
var locs []file.Location
|
||||
var err error
|
||||
|
||||
@@ -78,6 +78,64 @@ func TestOwnershipByFilesRelationship(t *testing.T) {
|
||||
return []pkg.Package{parent, child}, []artifact.Relationship{relationship}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only-real-file-owner-gets-ownership-relationship-not-symlink-owner",
|
||||
resolver: mockFR{
|
||||
translate: map[string]string{
|
||||
"/usr/bin/jenkins.war-symlink": "/usr/share/jenkins/jenkins.war", // symlink to the real file
|
||||
},
|
||||
},
|
||||
setup: func(t testing.TB) ([]pkg.Package, []artifact.Relationship) {
|
||||
// Package that owns the symlink
|
||||
symlinkOwner := pkg.Package{
|
||||
Type: pkg.ApkPkg,
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "jenkins-symlink-pkg",
|
||||
Files: []pkg.ApkFileRecord{
|
||||
{Path: "/usr/bin/jenkins.war-symlink"}, // this symlinks to the real file
|
||||
},
|
||||
},
|
||||
}
|
||||
symlinkOwner.SetID()
|
||||
|
||||
// Package that owns the real file
|
||||
realFileOwner := pkg.Package{
|
||||
Type: pkg.ApkPkg,
|
||||
Metadata: pkg.ApkDBEntry{
|
||||
Package: "jenkins-real-pkg",
|
||||
Files: []pkg.ApkFileRecord{
|
||||
{Path: "/usr/share/jenkins/jenkins.war"}, // the real file
|
||||
},
|
||||
},
|
||||
}
|
||||
realFileOwner.SetID()
|
||||
|
||||
// A third package that is "located at" the real file path
|
||||
// This simulates a package being discovered at the jenkins.war location
|
||||
consumerPackage := pkg.Package{
|
||||
Type: pkg.JavaPkg,
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewVirtualLocation("/usr/share/jenkins/jenkins.war", "/usr/share/jenkins/jenkins.war"),
|
||||
),
|
||||
}
|
||||
consumerPackage.SetID()
|
||||
|
||||
// Only the real file owner should have an ownership relationship with the consumer package
|
||||
// The symlink owner should NOT have a relationship with the consumer
|
||||
relationship := artifact.Relationship{
|
||||
From: realFileOwner,
|
||||
To: consumerPackage,
|
||||
Type: artifact.OwnershipByFileOverlapRelationship,
|
||||
Data: ownershipByFilesMetadata{
|
||||
Files: []string{
|
||||
"/usr/share/jenkins/jenkins.war",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return []pkg.Package{symlinkOwner, realFileOwner, consumerPackage}, []artifact.Relationship{relationship}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "misses-by-dead-symlink",
|
||||
resolver: mockFR{
|
||||
|
||||
@@ -107,6 +107,11 @@ func finalizePkgCatalogerResults(cfg CatalogingFactoryConfig, resolver file.Path
|
||||
}
|
||||
}
|
||||
|
||||
// we want to know if the user wants to preserve license content or not in the final SBOM
|
||||
// note: this looks incorrect, but pkg.License.Content is NOT used to compute the Package ID
|
||||
// this does NOT change the reproducibility of the Package ID
|
||||
applyLicenseContentRules(&p, cfg.LicenseConfig)
|
||||
|
||||
pkgs[i] = p
|
||||
}
|
||||
return pkgs, relationships
|
||||
@@ -262,3 +267,29 @@ func packageFileOwnershipRelationships(p pkg.Package, resolver file.PathResolver
|
||||
}
|
||||
return relationships, nil
|
||||
}
|
||||
|
||||
func applyLicenseContentRules(p *pkg.Package, cfg cataloging.LicenseConfig) {
|
||||
if p.Licenses.Empty() {
|
||||
return
|
||||
}
|
||||
|
||||
licenses := p.Licenses.ToSlice()
|
||||
for i := range licenses {
|
||||
l := &licenses[i]
|
||||
switch cfg.IncludeContent {
|
||||
case cataloging.LicenseContentIncludeUnknown:
|
||||
// we have an SPDX expression, which means this is NOT an unknown license
|
||||
// remove the content, we are only including content for unknown licenses by default
|
||||
if l.SPDXExpression != "" {
|
||||
licenses[i].Contents = ""
|
||||
}
|
||||
case cataloging.LicenseContentIncludeAll:
|
||||
// always include the content
|
||||
default:
|
||||
// clear it all out
|
||||
licenses[i].Contents = ""
|
||||
}
|
||||
}
|
||||
|
||||
p.Licenses = pkg.NewLicenseSet(licenses...)
|
||||
}
|
||||
|
||||
@@ -117,6 +117,149 @@ func TestFilterNonCompliantPackages(t *testing.T) {
|
||||
assert.Equal(t, p2, droppedPkgs[0])
|
||||
}
|
||||
|
||||
func TestApplyLicenseContentRules(t *testing.T) {
|
||||
licenseWithSPDX := pkg.License{
|
||||
SPDXExpression: "MIT",
|
||||
Contents: "MIT license content",
|
||||
}
|
||||
licenseWithoutSPDX := pkg.License{
|
||||
Value: "License-Not-A-SPDX-Expression",
|
||||
Contents: "Non-SPDX license content",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
inputLicenses []pkg.License
|
||||
cfg cataloging.LicenseConfig
|
||||
expectedLicenses []pkg.License
|
||||
}{
|
||||
{
|
||||
name: "LicenseContentIncludeUnknown",
|
||||
inputLicenses: []pkg.License{
|
||||
licenseWithSPDX,
|
||||
licenseWithoutSPDX,
|
||||
},
|
||||
cfg: cataloging.LicenseConfig{
|
||||
IncludeContent: cataloging.LicenseContentIncludeUnknown,
|
||||
},
|
||||
expectedLicenses: []pkg.License{
|
||||
{
|
||||
SPDXExpression: "MIT",
|
||||
Contents: "", // content cleared for SPDX license
|
||||
},
|
||||
{
|
||||
Value: "License-Not-A-SPDX-Expression",
|
||||
Contents: "Non-SPDX license content", // content preserved for non-SPDX
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LicenseContentExcludeAll",
|
||||
inputLicenses: []pkg.License{
|
||||
licenseWithSPDX,
|
||||
licenseWithoutSPDX,
|
||||
},
|
||||
cfg: cataloging.LicenseConfig{
|
||||
IncludeContent: cataloging.LicenseContentExcludeAll,
|
||||
},
|
||||
expectedLicenses: []pkg.License{
|
||||
{
|
||||
SPDXExpression: "MIT",
|
||||
Contents: "", // content cleared
|
||||
},
|
||||
{
|
||||
Value: "License-Not-A-SPDX-Expression",
|
||||
Contents: "", // content cleared
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LicenseContentIncludeAll",
|
||||
inputLicenses: []pkg.License{
|
||||
licenseWithSPDX,
|
||||
licenseWithoutSPDX,
|
||||
},
|
||||
cfg: cataloging.LicenseConfig{
|
||||
IncludeContent: cataloging.LicenseContentIncludeAll,
|
||||
},
|
||||
expectedLicenses: []pkg.License{
|
||||
{
|
||||
SPDXExpression: "MIT",
|
||||
Contents: "MIT license content", // content preserved
|
||||
},
|
||||
{
|
||||
Value: "License-Not-A-SPDX-Expression",
|
||||
Contents: "Non-SPDX license content", // content preserved
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default license config should be LicenseContentExcludeAll",
|
||||
inputLicenses: []pkg.License{
|
||||
licenseWithSPDX,
|
||||
licenseWithoutSPDX,
|
||||
},
|
||||
cfg: cataloging.DefaultLicenseConfig(),
|
||||
expectedLicenses: []pkg.License{
|
||||
{
|
||||
SPDXExpression: "MIT",
|
||||
},
|
||||
{
|
||||
Value: "License-Not-A-SPDX-Expression",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid license content cataloging config results in the default case",
|
||||
inputLicenses: []pkg.License{
|
||||
licenseWithSPDX,
|
||||
licenseWithoutSPDX,
|
||||
},
|
||||
cfg: cataloging.LicenseConfig{
|
||||
IncludeContent: cataloging.LicenseContent("invalid"),
|
||||
},
|
||||
expectedLicenses: []pkg.License{
|
||||
{
|
||||
SPDXExpression: "MIT",
|
||||
},
|
||||
{
|
||||
Value: "License-Not-A-SPDX-Expression",
|
||||
Contents: "", // content all removed
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Empty licenses",
|
||||
inputLicenses: []pkg.License{},
|
||||
cfg: cataloging.LicenseConfig{
|
||||
IncludeContent: cataloging.LicenseContentIncludeAll,
|
||||
},
|
||||
expectedLicenses: []pkg.License{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
inputPkg := &pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(tt.inputLicenses...),
|
||||
}
|
||||
|
||||
inputPkg.SetID()
|
||||
originalID := inputPkg.ID()
|
||||
|
||||
applyLicenseContentRules(inputPkg, tt.cfg)
|
||||
|
||||
assert.Equal(t, originalID, inputPkg.ID(), "package ID changed unexpectedly")
|
||||
|
||||
actualLicenses := inputPkg.Licenses.ToSlice()
|
||||
expectedLicenses := pkg.NewLicenseSet(tt.expectedLicenses...).ToSlice()
|
||||
|
||||
assert.Equal(t, expectedLicenses, actualLicenses, "license contents do not match expected values")
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyComplianceRules_DropAndStub(t *testing.T) {
|
||||
p := pkg.Package{Name: "", Version: ""}
|
||||
p.SetID()
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/githubactions"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/golang"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/haskell"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/homebrew"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/java"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/javascript"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/kernel"
|
||||
@@ -76,6 +77,7 @@ func DefaultPackageTaskFactories() Factories {
|
||||
// language-specific package declared catalogers ///////////////////////////////////////////////////////////////////////////
|
||||
newSimplePackageTaskFactory(cpp.NewConanCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "cpp", "conan"),
|
||||
newSimplePackageTaskFactory(dart.NewPubspecLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"),
|
||||
newSimplePackageTaskFactory(dart.NewPubspecCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"),
|
||||
newSimplePackageTaskFactory(elixir.NewMixLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "elixir"),
|
||||
newSimplePackageTaskFactory(erlang.NewRebarLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "erlang"),
|
||||
newSimplePackageTaskFactory(erlang.NewOTPCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "erlang", "otp"),
|
||||
@@ -152,7 +154,8 @@ func DefaultPackageTaskFactories() Factories {
|
||||
},
|
||||
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary",
|
||||
),
|
||||
newSimplePackageTaskFactory(binary.NewELFPackageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary", "elf-package"),
|
||||
newSimplePackageTaskFactory(binary.NewELFPackageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary", "elf-package", "elf"),
|
||||
newSimplePackageTaskFactory(binary.NewPEPackageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary", "pe-package", "pe", "dll", "exe"),
|
||||
newSimplePackageTaskFactory(githubactions.NewActionUsageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "github", "github-actions"),
|
||||
newSimplePackageTaskFactory(githubactions.NewWorkflowUsageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "github", "github-actions"),
|
||||
newSimplePackageTaskFactory(java.NewJvmDistributionCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "java", "jvm", "jdk", "jre"),
|
||||
@@ -162,10 +165,12 @@ func DefaultPackageTaskFactories() Factories {
|
||||
},
|
||||
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "linux", "kernel",
|
||||
),
|
||||
newSimplePackageTaskFactory(php.NewInterpreterCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary", "php"),
|
||||
newSimplePackageTaskFactory(sbomCataloger.NewCataloger, "sbom"), // note: not evidence of installed packages
|
||||
newSimplePackageTaskFactory(bitnamiSbomCataloger.NewCataloger, "bitnami", pkgcataloging.InstalledTag, pkgcataloging.ImageTag),
|
||||
newSimplePackageTaskFactory(wordpress.NewWordpressPluginCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "wordpress"),
|
||||
newSimplePackageTaskFactory(terraform.NewLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "terraform"),
|
||||
newSimplePackageTaskFactory(homebrew.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "homebrew"),
|
||||
|
||||
// deprecated catalogers ////////////////////////////////////////
|
||||
// these are catalogers that should not be selectable other than specific inclusion via name or "deprecated" tag (to remain backwards compatible)
|
||||
@@ -173,6 +178,5 @@ func DefaultPackageTaskFactories() Factories {
|
||||
newSimplePackageTaskFactory(dotnet.NewDotnetPortableExecutableCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
|
||||
newSimplePackageTaskFactory(php.NewPeclCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
|
||||
newSimplePackageTaskFactory(nix.NewStoreCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "anchore.io/schema/syft/json/16.0.28/document",
|
||||
"$id": "anchore.io/schema/syft/json/16.0.34/document",
|
||||
"$ref": "#/$defs/Document",
|
||||
"$defs": {
|
||||
"AlpmDbEntry": {
|
||||
@@ -408,6 +408,49 @@
|
||||
"path"
|
||||
]
|
||||
},
|
||||
"DartPubspec": {
|
||||
"properties": {
|
||||
"homepage": {
|
||||
"type": "string"
|
||||
},
|
||||
"repository": {
|
||||
"type": "string"
|
||||
},
|
||||
"documentation": {
|
||||
"type": "string"
|
||||
},
|
||||
"publish_to": {
|
||||
"type": "string"
|
||||
},
|
||||
"environment": {
|
||||
"$ref": "#/$defs/DartPubspecEnvironment"
|
||||
},
|
||||
"platforms": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"ignored_advisories": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"DartPubspecEnvironment": {
|
||||
"properties": {
|
||||
"sdk": {
|
||||
"type": "string"
|
||||
},
|
||||
"flutter": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"DartPubspecLockEntry": {
|
||||
"properties": {
|
||||
"name": {
|
||||
@@ -1072,6 +1115,20 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"HomebrewFormula": {
|
||||
"properties": {
|
||||
"tap": {
|
||||
"type": "string"
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"IDLikes": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
@@ -1391,9 +1448,6 @@
|
||||
"value": {
|
||||
"type": "string"
|
||||
},
|
||||
"fullText": {
|
||||
"type": "string"
|
||||
},
|
||||
"spdxExpression": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1419,7 +1473,6 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"value",
|
||||
"fullText",
|
||||
"spdxExpression",
|
||||
"type",
|
||||
"urls",
|
||||
@@ -1837,6 +1890,9 @@
|
||||
{
|
||||
"$ref": "#/$defs/CocoaPodfileLockEntry"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/DartPubspec"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/DartPubspecLockEntry"
|
||||
},
|
||||
@@ -1879,6 +1935,9 @@
|
||||
{
|
||||
"$ref": "#/$defs/HaskellHackageStackLockEntry"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/HomebrewFormula"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/JavaArchive"
|
||||
},
|
||||
@@ -1912,6 +1971,9 @@
|
||||
{
|
||||
"$ref": "#/$defs/OpamPackage"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/PeBinary"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/PhpComposerInstalledEntry"
|
||||
},
|
||||
@@ -1986,6 +2048,17 @@
|
||||
"purl"
|
||||
]
|
||||
},
|
||||
"PeBinary": {
|
||||
"properties": {
|
||||
"VersionResources": {
|
||||
"$ref": "#/$defs/KeyValues"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"VersionResources"
|
||||
]
|
||||
},
|
||||
"PhpComposerAuthors": {
|
||||
"properties": {
|
||||
"name": {
|
||||
@@ -2266,6 +2339,9 @@
|
||||
"installedSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"licenses": {
|
||||
"type": "string"
|
||||
},
|
||||
"files": {
|
||||
"items": {
|
||||
"$ref": "#/$defs/PortageFileRecord"
|
||||
@@ -2615,6 +2691,12 @@
|
||||
"sourceRpm": {
|
||||
"type": "string"
|
||||
},
|
||||
"signatures": {
|
||||
"items": {
|
||||
"$ref": "#/$defs/RpmSignature"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -2683,6 +2765,12 @@
|
||||
"sourceRpm": {
|
||||
"type": "string"
|
||||
},
|
||||
"signatures": {
|
||||
"items": {
|
||||
"$ref": "#/$defs/RpmSignature"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -2759,6 +2847,29 @@
|
||||
"flags"
|
||||
]
|
||||
},
|
||||
"RpmSignature": {
|
||||
"properties": {
|
||||
"algo": {
|
||||
"type": "string"
|
||||
},
|
||||
"hash": {
|
||||
"type": "string"
|
||||
},
|
||||
"created": {
|
||||
"type": "string"
|
||||
},
|
||||
"issuer": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"algo",
|
||||
"hash",
|
||||
"created",
|
||||
"issuer"
|
||||
]
|
||||
},
|
||||
"RubyGemspec": {
|
||||
"properties": {
|
||||
"name": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
(registered 2021-11-05, last updated 2021-11-05)
|
||||
(registered 2021-11-05, last updated 2025-13-05)
|
||||
|
||||
Media type name: application
|
||||
Media subtype name: vnd.syft+json
|
||||
@@ -60,11 +60,11 @@ General Comments:
|
||||
|
||||
Person to contact for further information:
|
||||
|
||||
1. Name: Dan Luhring
|
||||
2. Email: dan.luhring@anchore.com
|
||||
1. Name: Dan Nurmi
|
||||
2. Email: opensource@anchore.com
|
||||
|
||||
Intended usage: Common
|
||||
The Syft SBOM format is an open-source software bill of materials specification. It is intended to be exchanged between different parties of the software supply chain.
|
||||
|
||||
|
||||
Author/Change controller: Dan Luhring, on behalf of Anchore
|
||||
Author/Change controller: Dan Nurmi, on behalf of Anchore
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
package cataloging
|
||||
|
||||
import "github.com/anchore/syft/internal/licenses"
|
||||
import (
|
||||
"github.com/anchore/syft/internal/licenses"
|
||||
)
|
||||
|
||||
// LicenseContent controls when license content should be included in the SBOM.
|
||||
type LicenseContent string
|
||||
|
||||
const (
|
||||
LicenseContentIncludeAll LicenseContent = "all"
|
||||
LicenseContentIncludeUnknown LicenseContent = "unknown"
|
||||
LicenseContentExcludeAll LicenseContent = "none"
|
||||
)
|
||||
|
||||
type LicenseConfig struct {
|
||||
IncludeUnkownLicenseContent bool `json:"include-unknown-license-content" yaml:"include-unknown-license-content" mapstructure:"include-unknown-license-content"`
|
||||
Coverage float64 `json:"coverage" yaml:"coverage" mapstructure:"coverage"`
|
||||
// IncludeContent controls whether license copy discovered should be included in the SBOM.
|
||||
IncludeContent LicenseContent `json:"include-content" yaml:"include-content" mapstructure:"include-content"`
|
||||
|
||||
// Coverage is the percentage of text that must match a license for it to be considered a match.
|
||||
Coverage float64 `json:"coverage" yaml:"coverage" mapstructure:"coverage"`
|
||||
}
|
||||
|
||||
func DefaultLicenseConfig() LicenseConfig {
|
||||
return LicenseConfig{
|
||||
IncludeUnkownLicenseContent: licenses.DefaultIncludeLicenseContent,
|
||||
Coverage: licenses.DefaultCoverageThreshold,
|
||||
IncludeContent: LicenseContentExcludeAll,
|
||||
Coverage: licenses.DefaultCoverageThreshold,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,6 @@ func setupContext(ctx context.Context, cfg *CreateSBOMConfig) (context.Context,
|
||||
func SetContextLicenseScanner(ctx context.Context, cfg cataloging.LicenseConfig) (context.Context, error) {
|
||||
// inject a single license scanner and content config for all package cataloging tasks into context
|
||||
licenseScanner, err := licenses.NewDefaultScanner(
|
||||
licenses.WithIncludeLicenseContent(cfg.IncludeUnkownLicenseContent),
|
||||
licenses.WithCoverage(cfg.Coverage),
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -255,6 +255,7 @@ func (c *CreateSBOMConfig) selectTasks(src source.Description) ([]task.Task, []t
|
||||
RelationshipsConfig: c.Relationships,
|
||||
DataGenerationConfig: c.DataGeneration,
|
||||
PackagesConfig: c.Packages,
|
||||
LicenseConfig: c.Licenses,
|
||||
ComplianceConfig: c.Compliance,
|
||||
FilesConfig: c.Files,
|
||||
}
|
||||
|
||||
@@ -59,7 +59,34 @@ func (s CoordinateSet) Paths() []string {
|
||||
return pathSlice
|
||||
}
|
||||
|
||||
func (s CoordinateSet) ToSlice() []Coordinates {
|
||||
func (s CoordinateSet) ToSlice(sorters ...func(a, b Coordinates) int) []Coordinates {
|
||||
coordinates := s.ToUnorderedSlice()
|
||||
|
||||
var sorted bool
|
||||
for _, sorter := range sorters {
|
||||
if sorter == nil {
|
||||
continue
|
||||
}
|
||||
sort.Slice(coordinates, func(i, j int) bool {
|
||||
return sorter(coordinates[i], coordinates[j]) < 0
|
||||
})
|
||||
sorted = true
|
||||
break
|
||||
}
|
||||
|
||||
if !sorted {
|
||||
sort.SliceStable(coordinates, func(i, j int) bool {
|
||||
if coordinates[i].FileSystemID == coordinates[j].FileSystemID {
|
||||
return coordinates[i].RealPath < coordinates[j].RealPath
|
||||
}
|
||||
return coordinates[i].FileSystemID < coordinates[j].FileSystemID
|
||||
})
|
||||
}
|
||||
|
||||
return coordinates
|
||||
}
|
||||
|
||||
func (s CoordinateSet) ToUnorderedSlice() []Coordinates {
|
||||
if s.set == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -69,12 +96,6 @@ func (s CoordinateSet) ToSlice() []Coordinates {
|
||||
coordinates[idx] = v
|
||||
idx++
|
||||
}
|
||||
sort.SliceStable(coordinates, func(i, j int) bool {
|
||||
if coordinates[i].RealPath == coordinates[j].RealPath {
|
||||
return coordinates[i].FileSystemID < coordinates[j].FileSystemID
|
||||
}
|
||||
return coordinates[i].RealPath < coordinates[j].RealPath
|
||||
})
|
||||
return coordinates
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"github.com/gohugoio/hashstructure"
|
||||
@@ -54,7 +55,28 @@ func (s LocationSet) Contains(l Location) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s LocationSet) ToSlice() []Location {
|
||||
func (s LocationSet) ToSlice(sorters ...func(a, b Location) int) []Location {
|
||||
locations := s.ToUnorderedSlice()
|
||||
|
||||
var sorted bool
|
||||
for _, sorter := range sorters {
|
||||
if sorter == nil {
|
||||
continue
|
||||
}
|
||||
slices.SortFunc(locations, sorter)
|
||||
sorted = true
|
||||
break
|
||||
}
|
||||
|
||||
if !sorted {
|
||||
// though no sorter was passed, we need to guarantee a stable ordering between calls
|
||||
sort.Sort(Locations(locations))
|
||||
}
|
||||
|
||||
return locations
|
||||
}
|
||||
|
||||
func (s LocationSet) ToUnorderedSlice() []Location {
|
||||
if s.set == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -67,7 +89,6 @@ func (s LocationSet) ToSlice() []Location {
|
||||
}
|
||||
idx++
|
||||
}
|
||||
sort.Sort(Locations(locations))
|
||||
return locations
|
||||
}
|
||||
|
||||
|
||||
@@ -89,8 +89,8 @@ func TestLocationSet_SortPaths(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
set := NewLocationSet(test.input...)
|
||||
assert.Equal(t, test.expected, set.ToSlice())
|
||||
actual := NewLocationSet(test.input...).ToSlice()
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+107
-20
@@ -1,6 +1,12 @@
|
||||
package file
|
||||
|
||||
import "github.com/anchore/syft/internal/evidence"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/evidence"
|
||||
)
|
||||
|
||||
var locationSorterWithoutLayers = LocationSorter(nil)
|
||||
|
||||
type Locations []Location
|
||||
|
||||
@@ -9,27 +15,108 @@ func (l Locations) Len() int {
|
||||
}
|
||||
|
||||
func (l Locations) Less(i, j int) bool {
|
||||
liEvidence := l[i].Annotations[evidence.AnnotationKey]
|
||||
ljEvidence := l[j].Annotations[evidence.AnnotationKey]
|
||||
if liEvidence == ljEvidence {
|
||||
if l[i].RealPath == l[j].RealPath {
|
||||
if l[i].AccessPath == l[j].AccessPath {
|
||||
return l[i].FileSystemID < l[j].FileSystemID
|
||||
}
|
||||
return l[i].AccessPath < l[j].AccessPath
|
||||
}
|
||||
return l[i].RealPath < l[j].RealPath
|
||||
}
|
||||
if liEvidence == evidence.PrimaryAnnotation {
|
||||
return true
|
||||
}
|
||||
if ljEvidence == evidence.PrimaryAnnotation {
|
||||
return false
|
||||
}
|
||||
|
||||
return liEvidence > ljEvidence
|
||||
return locationSorterWithoutLayers(l[i], l[j]) < 0
|
||||
}
|
||||
|
||||
func (l Locations) Swap(i, j int) {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
|
||||
// LocationSorter creates a comparison function (slices.SortFunc) for Location objects based on layer order
|
||||
func LocationSorter(layers []string) func(a, b Location) int { //nolint:gocognit
|
||||
var layerOrderByDigest map[string]int
|
||||
if len(layers) > 0 {
|
||||
layerOrderByDigest = make(map[string]int)
|
||||
for i, digest := range layers {
|
||||
layerOrderByDigest[digest] = i
|
||||
}
|
||||
}
|
||||
|
||||
return func(a, b Location) int {
|
||||
// compare by evidence annotations first...
|
||||
aEvidence := a.Annotations[evidence.AnnotationKey]
|
||||
bEvidence := b.Annotations[evidence.AnnotationKey]
|
||||
|
||||
if aEvidence != bEvidence {
|
||||
if aEvidence == evidence.PrimaryAnnotation {
|
||||
return -1
|
||||
}
|
||||
if bEvidence == evidence.PrimaryAnnotation {
|
||||
return 1
|
||||
}
|
||||
|
||||
if aEvidence > bEvidence {
|
||||
return -1
|
||||
}
|
||||
if bEvidence > aEvidence {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// ...then by layer order
|
||||
if layerOrderByDigest != nil {
|
||||
// we're given layer order details
|
||||
aLayerIdx, aExists := layerOrderByDigest[a.FileSystemID]
|
||||
bLayerIdx, bExists := layerOrderByDigest[b.FileSystemID]
|
||||
|
||||
if aLayerIdx != bLayerIdx {
|
||||
if !aExists && !bExists {
|
||||
return strings.Compare(a.FileSystemID, b.FileSystemID)
|
||||
}
|
||||
if !aExists {
|
||||
return 1
|
||||
}
|
||||
if !bExists {
|
||||
return -1
|
||||
}
|
||||
|
||||
return aLayerIdx - bLayerIdx
|
||||
}
|
||||
} else if a.FileSystemID != b.FileSystemID {
|
||||
// no layer info given, legacy behavior is to sort lexicographically
|
||||
return strings.Compare(a.FileSystemID, b.FileSystemID)
|
||||
}
|
||||
|
||||
// ...then by paths
|
||||
if a.AccessPath != b.AccessPath {
|
||||
return strings.Compare(a.AccessPath, b.AccessPath)
|
||||
}
|
||||
|
||||
return strings.Compare(a.RealPath, b.RealPath)
|
||||
}
|
||||
}
|
||||
|
||||
// CoordinatesSorter creates a comparison function (slices.SortFunc) for Coordinate objects based on layer order
|
||||
func CoordinatesSorter(layers []string) func(a, b Coordinates) int {
|
||||
var layerOrderByDigest map[string]int
|
||||
if len(layers) > 0 {
|
||||
layerOrderByDigest = make(map[string]int)
|
||||
for i, digest := range layers {
|
||||
layerOrderByDigest[digest] = i
|
||||
}
|
||||
}
|
||||
|
||||
return func(a, b Coordinates) int {
|
||||
// ...then by layer order
|
||||
if layerOrderByDigest != nil {
|
||||
aLayerIdx, aExists := layerOrderByDigest[a.FileSystemID]
|
||||
bLayerIdx, bExists := layerOrderByDigest[b.FileSystemID]
|
||||
|
||||
if aLayerIdx != bLayerIdx {
|
||||
if !aExists && !bExists {
|
||||
return strings.Compare(a.FileSystemID, b.FileSystemID)
|
||||
}
|
||||
if !aExists {
|
||||
return 1
|
||||
}
|
||||
if !bExists {
|
||||
return -1
|
||||
}
|
||||
|
||||
return aLayerIdx - bLayerIdx
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Compare(a.RealPath, b.RealPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,557 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/anchore/syft/internal/evidence"
|
||||
)
|
||||
|
||||
func TestLocationAndCoordinatesSorters(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
layers []string
|
||||
locs []Location
|
||||
coords []Coordinates
|
||||
wantLocs []string
|
||||
wantCoords []string
|
||||
}{
|
||||
{
|
||||
name: "empty location slice",
|
||||
layers: []string{"fsid-1"},
|
||||
locs: []Location{},
|
||||
coords: []Coordinates{},
|
||||
wantLocs: []string{},
|
||||
wantCoords: []string{},
|
||||
},
|
||||
{
|
||||
name: "nil layer list",
|
||||
layers: nil,
|
||||
locs: []Location{
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/a",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
AccessPath: "/a",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/b",
|
||||
FileSystemID: "fsid-2",
|
||||
},
|
||||
AccessPath: "/b",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
coords: []Coordinates{
|
||||
{
|
||||
RealPath: "/a",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
{
|
||||
RealPath: "/b",
|
||||
FileSystemID: "fsid-2",
|
||||
},
|
||||
},
|
||||
wantLocs: []string{
|
||||
"/a (/a) @ fsid-1 map[]",
|
||||
"/b (/b) @ fsid-2 map[]",
|
||||
},
|
||||
wantCoords: []string{
|
||||
"/a @ fsid-1",
|
||||
"/b @ fsid-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sort by evidence type only",
|
||||
layers: []string{"fsid-1"},
|
||||
locs: []Location{
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/a",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
AccessPath: "/a",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{
|
||||
evidence.AnnotationKey: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/b",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
AccessPath: "/b",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{
|
||||
evidence.AnnotationKey: evidence.SupportingAnnotation,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/c",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
AccessPath: "/c",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{
|
||||
evidence.AnnotationKey: evidence.PrimaryAnnotation,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
coords: []Coordinates{
|
||||
{
|
||||
RealPath: "/a",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
{
|
||||
RealPath: "/b",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
{
|
||||
RealPath: "/c",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
},
|
||||
wantLocs: []string{
|
||||
"/c (/c) @ fsid-1 map[evidence:primary]",
|
||||
"/b (/b) @ fsid-1 map[evidence:supporting]",
|
||||
"/a (/a) @ fsid-1 map[evidence:]",
|
||||
},
|
||||
wantCoords: []string{
|
||||
"/a @ fsid-1",
|
||||
"/b @ fsid-1",
|
||||
"/c @ fsid-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "same evidence type, sort by layer",
|
||||
layers: []string{"fsid-1", "fsid-2", "fsid-3"},
|
||||
locs: []Location{
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/a",
|
||||
FileSystemID: "fsid-3",
|
||||
},
|
||||
AccessPath: "/a",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{
|
||||
evidence.AnnotationKey: evidence.PrimaryAnnotation,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/b",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
AccessPath: "/b",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{
|
||||
evidence.AnnotationKey: evidence.PrimaryAnnotation,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/c",
|
||||
FileSystemID: "fsid-2",
|
||||
},
|
||||
AccessPath: "/c",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{
|
||||
evidence.AnnotationKey: evidence.PrimaryAnnotation,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
coords: []Coordinates{
|
||||
{
|
||||
RealPath: "/a",
|
||||
FileSystemID: "fsid-3",
|
||||
},
|
||||
{
|
||||
RealPath: "/b",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
{
|
||||
RealPath: "/c",
|
||||
FileSystemID: "fsid-2",
|
||||
},
|
||||
},
|
||||
wantLocs: []string{
|
||||
"/b (/b) @ fsid-1 map[evidence:primary]",
|
||||
"/c (/c) @ fsid-2 map[evidence:primary]",
|
||||
"/a (/a) @ fsid-3 map[evidence:primary]",
|
||||
},
|
||||
wantCoords: []string{
|
||||
"/b @ fsid-1",
|
||||
"/c @ fsid-2",
|
||||
"/a @ fsid-3",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "same evidence and layer, sort by access path",
|
||||
layers: []string{"fsid-1"},
|
||||
locs: []Location{
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/x",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
AccessPath: "/c",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{
|
||||
evidence.AnnotationKey: evidence.PrimaryAnnotation,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/y",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
AccessPath: "/a",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{
|
||||
evidence.AnnotationKey: evidence.PrimaryAnnotation,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/z",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
AccessPath: "/b",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{
|
||||
evidence.AnnotationKey: evidence.PrimaryAnnotation,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
coords: []Coordinates{
|
||||
{
|
||||
RealPath: "/x",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
{
|
||||
RealPath: "/y",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
{
|
||||
RealPath: "/z",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
},
|
||||
wantLocs: []string{
|
||||
"/y (/a) @ fsid-1 map[evidence:primary]",
|
||||
"/z (/b) @ fsid-1 map[evidence:primary]",
|
||||
"/x (/c) @ fsid-1 map[evidence:primary]",
|
||||
},
|
||||
wantCoords: []string{
|
||||
"/x @ fsid-1",
|
||||
"/y @ fsid-1",
|
||||
"/z @ fsid-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "same evidence, layer, and access path - sort by real path",
|
||||
layers: []string{"fsid-1"},
|
||||
locs: []Location{
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/c",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
AccessPath: "/same",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{
|
||||
evidence.AnnotationKey: evidence.PrimaryAnnotation,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/a",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
AccessPath: "/same",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{
|
||||
evidence.AnnotationKey: evidence.PrimaryAnnotation,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/b",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
AccessPath: "/same",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{
|
||||
evidence.AnnotationKey: evidence.PrimaryAnnotation,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
coords: []Coordinates{
|
||||
{
|
||||
RealPath: "/c",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
{
|
||||
RealPath: "/a",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
{
|
||||
RealPath: "/b",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
},
|
||||
wantLocs: []string{
|
||||
"/a (/same) @ fsid-1 map[evidence:primary]",
|
||||
"/b (/same) @ fsid-1 map[evidence:primary]",
|
||||
"/c (/same) @ fsid-1 map[evidence:primary]",
|
||||
},
|
||||
wantCoords: []string{
|
||||
"/a @ fsid-1",
|
||||
"/b @ fsid-1",
|
||||
"/c @ fsid-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unknown layers",
|
||||
layers: []string{"fsid-1", "fsid-2"},
|
||||
locs: []Location{
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/a",
|
||||
FileSystemID: "unknown-1",
|
||||
},
|
||||
AccessPath: "/a",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/b",
|
||||
FileSystemID: "unknown-2",
|
||||
},
|
||||
AccessPath: "/b",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
coords: []Coordinates{
|
||||
{
|
||||
RealPath: "/a",
|
||||
FileSystemID: "unknown-1",
|
||||
},
|
||||
{
|
||||
RealPath: "/b",
|
||||
FileSystemID: "unknown-2",
|
||||
},
|
||||
},
|
||||
wantLocs: []string{
|
||||
"/a (/a) @ unknown-1 map[]",
|
||||
"/b (/b) @ unknown-2 map[]",
|
||||
},
|
||||
wantCoords: []string{
|
||||
"/a @ unknown-1",
|
||||
"/b @ unknown-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed known and unknown layers",
|
||||
layers: []string{"fsid-1", "fsid-2"},
|
||||
locs: []Location{
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/a",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
AccessPath: "/a",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/b",
|
||||
FileSystemID: "unknown",
|
||||
},
|
||||
AccessPath: "/b",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
coords: []Coordinates{
|
||||
{
|
||||
RealPath: "/a",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
{
|
||||
RealPath: "/b",
|
||||
FileSystemID: "unknown",
|
||||
},
|
||||
},
|
||||
wantLocs: []string{
|
||||
"/a (/a) @ fsid-1 map[]",
|
||||
"/b (/b) @ unknown map[]",
|
||||
},
|
||||
wantCoords: []string{
|
||||
"/a @ fsid-1",
|
||||
"/b @ unknown",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "evidence comparison when one has none",
|
||||
layers: []string{"fsid-1"},
|
||||
locs: []Location{
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/a",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
AccessPath: "/a",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{
|
||||
// No evidence.AnnotationKey
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: "/b",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
AccessPath: "/b",
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{
|
||||
evidence.AnnotationKey: evidence.PrimaryAnnotation,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
coords: []Coordinates{
|
||||
{
|
||||
RealPath: "/a",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
{
|
||||
RealPath: "/b",
|
||||
FileSystemID: "fsid-1",
|
||||
},
|
||||
},
|
||||
wantLocs: []string{
|
||||
"/b (/b) @ fsid-1 map[evidence:primary]",
|
||||
"/a (/a) @ fsid-1 map[]",
|
||||
},
|
||||
wantCoords: []string{
|
||||
"/a @ fsid-1",
|
||||
"/b @ fsid-1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Run("Location", func(t *testing.T) {
|
||||
locs := make([]Location, len(tt.locs))
|
||||
copy(locs, tt.locs)
|
||||
|
||||
slices.SortFunc(locs, LocationSorter(tt.layers))
|
||||
|
||||
got := make([]string, len(locs))
|
||||
for i, loc := range locs {
|
||||
got[i] = fmt.Sprintf("%s (%s) @ %s %s",
|
||||
loc.RealPath,
|
||||
loc.AccessPath,
|
||||
loc.FileSystemID,
|
||||
loc.LocationMetadata.Annotations)
|
||||
}
|
||||
|
||||
if d := cmp.Diff(tt.wantLocs, got); d != "" {
|
||||
t.Errorf("LocationSorter() mismatch (-want +got):\n%s", d)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Coordinates", func(t *testing.T) {
|
||||
coords := make([]Coordinates, len(tt.coords))
|
||||
copy(coords, tt.coords)
|
||||
|
||||
slices.SortFunc(coords, CoordinatesSorter(tt.layers))
|
||||
|
||||
got := make([]string, len(coords))
|
||||
for i, coord := range coords {
|
||||
got[i] = fmt.Sprintf("%s @ %s",
|
||||
coord.RealPath,
|
||||
coord.FileSystemID)
|
||||
}
|
||||
|
||||
if d := cmp.Diff(tt.wantCoords, got); d != "" {
|
||||
t.Errorf("CoordinatesSorter() mismatch (-want +got):\n%s", d)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -42,17 +42,18 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
|
||||
cdxBOM.SerialNumber = uuid.New().URN()
|
||||
cdxBOM.Metadata = toBomDescriptor(s.Descriptor.Name, s.Descriptor.Version, s.Source)
|
||||
|
||||
coordinates, locationSorter := getCoordinates(s)
|
||||
|
||||
// Packages
|
||||
packages := s.Artifacts.Packages.Sorted()
|
||||
components := make([]cyclonedx.Component, len(packages))
|
||||
for i, p := range packages {
|
||||
components[i] = helpers.EncodeComponent(p)
|
||||
components[i] = helpers.EncodeComponent(p, locationSorter)
|
||||
}
|
||||
components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...)
|
||||
|
||||
// Files
|
||||
artifacts := s.Artifacts
|
||||
coordinates := s.AllCoordinates()
|
||||
|
||||
for _, coordinate := range coordinates {
|
||||
var metadata *file.Metadata
|
||||
// File Info
|
||||
@@ -95,6 +96,21 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM {
|
||||
return cdxBOM
|
||||
}
|
||||
|
||||
func getCoordinates(s sbom.SBOM) ([]file.Coordinates, func(a, b file.Location) int) {
|
||||
var layers []string
|
||||
if m, ok := s.Source.Metadata.(source.ImageMetadata); ok {
|
||||
for _, l := range m.Layers {
|
||||
layers = append(layers, l.Digest)
|
||||
}
|
||||
}
|
||||
|
||||
coordSorter := file.CoordinatesSorter(layers)
|
||||
coordinates := s.AllCoordinates()
|
||||
|
||||
slices.SortFunc(coordinates, coordSorter)
|
||||
return coordinates, file.LocationSorter(layers)
|
||||
}
|
||||
|
||||
func digestsToHashes(digests []file.Digest) []cyclonedx.Hash {
|
||||
var hashes []cyclonedx.Hash
|
||||
for _, digest := range digests {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -15,13 +14,13 @@ import (
|
||||
"github.com/spdx/tools-golang/spdx"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
internallicenses "github.com/anchore/syft/internal/licenses"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/mimetype"
|
||||
"github.com/anchore/syft/internal/relationship"
|
||||
"github.com/anchore/syft/internal/spdxlicense"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
formatInternal "github.com/anchore/syft/syft/format/internal"
|
||||
"github.com/anchore/syft/syft/format/internal/spdxutil/helpers"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
@@ -49,7 +48,7 @@ func ToFormatModel(s sbom.SBOM) *spdx.Document {
|
||||
name, namespace := helpers.DocumentNameAndNamespace(s.Source, s.Descriptor)
|
||||
|
||||
rels := relationship.NewIndex(s.Relationships...)
|
||||
packages := toPackages(rels, s.Artifacts.Packages, s)
|
||||
packages, otherLicenses := toPackages(rels, s.Artifacts.Packages, s)
|
||||
|
||||
allRelationships := toRelationships(rels.All())
|
||||
|
||||
@@ -155,7 +154,7 @@ func ToFormatModel(s sbom.SBOM) *spdx.Document {
|
||||
Packages: packages,
|
||||
Files: toFiles(s),
|
||||
Relationships: allRelationships,
|
||||
OtherLicenses: toOtherLicenses(s.Artifacts.Packages),
|
||||
OtherLicenses: convertOtherLicense(otherLicenses),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,21 +267,34 @@ func toRootPackage(s source.Description) *spdx.Package {
|
||||
}
|
||||
|
||||
func toSPDXID(identifiable artifact.Identifiable) spdx.ElementID {
|
||||
id := string(identifiable.ID())
|
||||
if strings.HasPrefix(id, "SPDXRef-") {
|
||||
// this is already an SPDX ID, no need to change it (except for the prefix)
|
||||
return spdx.ElementID(helpers.SanitizeElementID(strings.TrimPrefix(id, "SPDXRef-")))
|
||||
}
|
||||
maxLen := 40
|
||||
id := ""
|
||||
switch it := identifiable.(type) {
|
||||
case pkg.Package:
|
||||
if strings.HasPrefix(id, "Package-") {
|
||||
// this is already an SPDX ID, no need to change it
|
||||
return spdx.ElementID(helpers.SanitizeElementID(id))
|
||||
}
|
||||
switch {
|
||||
case it.Type != "" && it.Name != "":
|
||||
id = fmt.Sprintf("Package-%s-%s-%s", it.Type, it.Name, it.ID())
|
||||
id = fmt.Sprintf("Package-%s-%s-%s", it.Type, it.Name, id)
|
||||
case it.Name != "":
|
||||
id = fmt.Sprintf("Package-%s-%s", it.Name, it.ID())
|
||||
id = fmt.Sprintf("Package-%s-%s", it.Name, id)
|
||||
case it.Type != "":
|
||||
id = fmt.Sprintf("Package-%s-%s", it.Type, it.ID())
|
||||
id = fmt.Sprintf("Package-%s-%s", it.Type, id)
|
||||
default:
|
||||
id = fmt.Sprintf("Package-%s", it.ID())
|
||||
id = fmt.Sprintf("Package-%s", id)
|
||||
}
|
||||
case file.Coordinates:
|
||||
if strings.HasPrefix(id, "File-") {
|
||||
// this is already an SPDX ID, no need to change it. Note: there is no way to reach this case today
|
||||
// from within syft, however, this covers future cases where the ID can be overridden
|
||||
return spdx.ElementID(helpers.SanitizeElementID(id))
|
||||
}
|
||||
p := ""
|
||||
parts := strings.Split(it.RealPath, "/")
|
||||
for i := len(parts); i > 0; i-- {
|
||||
@@ -296,9 +308,7 @@ func toSPDXID(identifiable artifact.Identifiable) spdx.ElementID {
|
||||
}
|
||||
p = path.Join(part, p)
|
||||
}
|
||||
id = fmt.Sprintf("File-%s-%s", p, it.ID())
|
||||
default:
|
||||
id = string(identifiable.ID())
|
||||
id = fmt.Sprintf("File-%s-%s", p, id)
|
||||
}
|
||||
// NOTE: the spdx library prepend SPDXRef-, so we don't do it here
|
||||
return spdx.ElementID(helpers.SanitizeElementID(id))
|
||||
@@ -307,16 +317,18 @@ func toSPDXID(identifiable artifact.Identifiable) spdx.ElementID {
|
||||
// packages populates all Package Information from the package Collection (see https://spdx.github.io/spdx-spec/3-package-information/)
|
||||
//
|
||||
//nolint:funlen
|
||||
func toPackages(rels *relationship.Index, catalog *pkg.Collection, sbom sbom.SBOM) (results []*spdx.Package) {
|
||||
func toPackages(rels *relationship.Index, catalog *pkg.Collection, sbom sbom.SBOM) (results []*spdx.Package, otherLicenses []spdx.OtherLicense) {
|
||||
otherLicenseSet := helpers.NewSPDXOtherLicenseSet()
|
||||
for _, p := range catalog.Sorted() {
|
||||
// name should be guaranteed to be unique, but semantically useful and stable
|
||||
// name should be guaranteed to be unique but semantically useful and stable
|
||||
id := toSPDXID(p)
|
||||
|
||||
// If the Concluded License is not the same as the Declared License, a written explanation should be provided
|
||||
// in the Comments on License field (section 7.16). With respect to NOASSERTION, a written explanation in
|
||||
// the Comments on License field (section 7.16) is preferred.
|
||||
// extract these correctly to the spdx license format
|
||||
concluded, declared := helpers.License(p)
|
||||
concluded, declared, ol := helpers.License(p)
|
||||
otherLicenseSet.Add(ol...)
|
||||
|
||||
// two ways to get filesAnalyzed == true:
|
||||
// 1. syft has generated a sha1 digest for the package itself - usually in the java cataloger
|
||||
@@ -475,7 +487,7 @@ func toPackages(rels *relationship.Index, catalog *pkg.Collection, sbom sbom.SBO
|
||||
PackageAttributionTexts: nil,
|
||||
})
|
||||
}
|
||||
return results
|
||||
return results, otherLicenseSet.ToSlice()
|
||||
}
|
||||
|
||||
func toPackageChecksums(p pkg.Package) ([]spdx.Checksum, bool) {
|
||||
@@ -602,14 +614,19 @@ func lookupRelationship(ty artifact.RelationshipType) (bool, helpers.Relationshi
|
||||
func toFiles(s sbom.SBOM) (results []*spdx.File) {
|
||||
artifacts := s.Artifacts
|
||||
|
||||
for _, coordinates := range s.AllCoordinates() {
|
||||
_, coordinateSorter := formatInternal.GetLocationSorters(s)
|
||||
|
||||
coordinates := s.AllCoordinates()
|
||||
slices.SortFunc(coordinates, coordinateSorter)
|
||||
|
||||
for _, c := range coordinates {
|
||||
var metadata *file.Metadata
|
||||
if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists {
|
||||
if metadataForLocation, exists := artifacts.FileMetadata[c]; exists {
|
||||
metadata = &metadataForLocation
|
||||
}
|
||||
|
||||
var digests []file.Digest
|
||||
if digestsForLocation, exists := artifacts.FileDigests[coordinates]; exists {
|
||||
if digestsForLocation, exists := artifacts.FileDigests[c]; exists {
|
||||
digests = digestsForLocation
|
||||
}
|
||||
|
||||
@@ -625,18 +642,18 @@ func toFiles(s sbom.SBOM) (results []*spdx.File) {
|
||||
// TODO: add file classifications (?) and content as a snippet
|
||||
|
||||
var comment string
|
||||
if coordinates.FileSystemID != "" {
|
||||
comment = fmt.Sprintf("layerID: %s", coordinates.FileSystemID)
|
||||
if c.FileSystemID != "" {
|
||||
comment = fmt.Sprintf("layerID: %s", c.FileSystemID)
|
||||
}
|
||||
|
||||
relativePath, err := convertAbsoluteToRelative(coordinates.RealPath)
|
||||
relativePath, err := convertAbsoluteToRelative(c.RealPath)
|
||||
if err != nil {
|
||||
log.Debugf("unable to convert relative path '%s' to absolute path: %s", coordinates.RealPath, err)
|
||||
relativePath = coordinates.RealPath
|
||||
log.Debugf("unable to convert relative path '%s' to absolute path: %s", c.RealPath, err)
|
||||
relativePath = c.RealPath
|
||||
}
|
||||
|
||||
results = append(results, &spdx.File{
|
||||
FileSPDXIdentifier: toSPDXID(coordinates),
|
||||
FileSPDXIdentifier: toSPDXID(c),
|
||||
FileComment: comment,
|
||||
// required, no attempt made to determine license information
|
||||
LicenseConcluded: noAssertion,
|
||||
@@ -723,81 +740,14 @@ func toFileTypes(metadata *file.Metadata) (ty []string) {
|
||||
return ty
|
||||
}
|
||||
|
||||
// other licenses are for licenses from the pkg.Package that do not have a valid SPDX Expression
|
||||
// OR are an expression that is a single `License-Ref-*`
|
||||
func toOtherLicenses(catalog *pkg.Collection) []*spdx.OtherLicense {
|
||||
licenses := map[string]helpers.SPDXLicense{}
|
||||
|
||||
for p := range catalog.Enumerate() {
|
||||
declaredLicenses, concludedLicenses := helpers.ParseLicenses(p.Licenses.ToSlice())
|
||||
for _, l := range declaredLicenses {
|
||||
if l.Value != "" {
|
||||
licenses[l.ID] = l
|
||||
}
|
||||
if l.ID != "" && isLicenseRef(l.ID) {
|
||||
licenses[l.ID] = l
|
||||
}
|
||||
}
|
||||
for _, l := range concludedLicenses {
|
||||
if l.Value != "" {
|
||||
licenses[l.ID] = l
|
||||
}
|
||||
if l.ID != "" && isLicenseRef(l.ID) {
|
||||
licenses[l.ID] = l
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result []*spdx.OtherLicense
|
||||
|
||||
var ids []string
|
||||
for licenseID := range licenses {
|
||||
ids = append(ids, licenseID)
|
||||
}
|
||||
|
||||
slices.Sort(ids)
|
||||
for _, id := range ids {
|
||||
license := licenses[id]
|
||||
value := license.Value
|
||||
fullText := license.FullText
|
||||
// handle cases where LicenseRef needs to be included in hasExtractedLicensingInfos
|
||||
if license.Value == "" {
|
||||
value, _ = strings.CutPrefix(license.ID, "LicenseRef-")
|
||||
}
|
||||
other := &spdx.OtherLicense{
|
||||
LicenseIdentifier: license.ID,
|
||||
}
|
||||
if fullText != "" {
|
||||
other.ExtractedText = fullText
|
||||
} else {
|
||||
other.ExtractedText = value
|
||||
}
|
||||
customPrefix := spdxlicense.LicenseRefPrefix + helpers.SanitizeElementID(internallicenses.UnknownLicensePrefix)
|
||||
if strings.HasPrefix(license.ID, customPrefix) {
|
||||
other.LicenseName = strings.TrimPrefix(license.ID, customPrefix)
|
||||
other.LicenseComment = strings.Trim(internallicenses.UnknownLicensePrefix, "-_")
|
||||
}
|
||||
result = append(result, other)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
var licenseRefRegEx = regexp.MustCompile(`^LicenseRef-[A-Za-z0-9_-]+$`)
|
||||
|
||||
// isSingularLicenseRef checks if the string is a singular LicenseRef-* identifier
|
||||
func isLicenseRef(s string) bool {
|
||||
// Match the input string against the regex
|
||||
return licenseRefRegEx.MatchString(s)
|
||||
}
|
||||
|
||||
// TODO: handle SPDX excludes file case
|
||||
// f file is an "excludes" file, skip it /* exclude SPDX analysis file(s) */
|
||||
// see: https://spdx.github.io/spdx-spec/v2.3/package-information/#79-package-verification-code-field
|
||||
// the above link contains the SPDX algorithm for a package verification code
|
||||
func newPackageVerificationCode(rels *relationship.Index, p pkg.Package, sbom sbom.SBOM) *spdx.PackageVerificationCode {
|
||||
// key off of the contains relationship;
|
||||
// spdx validator will fail if a package claims to contain a file but no sha1 provided
|
||||
// if a sha1 for a file is provided then the validator will fail if the package does not have
|
||||
// key off of the spdx contains relationship;
|
||||
// spdx validator will fail if a package claims to contain a file, but no sha1 provided
|
||||
// if a sha1 for a file is provided, then the validator will fail if the package does not have
|
||||
// a package verification code
|
||||
coordinates := rels.Coordinates(p, artifact.ContainsRelationship)
|
||||
var digests []file.Digest
|
||||
@@ -870,3 +820,15 @@ func convertAbsoluteToRelative(absPath string) (string, error) {
|
||||
|
||||
return relPath, nil
|
||||
}
|
||||
|
||||
func convertOtherLicense(otherLicenses []spdx.OtherLicense) []*spdx.OtherLicense {
|
||||
if len(otherLicenses) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]*spdx.OtherLicense, 0, len(otherLicenses))
|
||||
for i := range otherLicenses {
|
||||
result = append(result, &otherLicenses[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -736,7 +738,7 @@ func Test_H1Digest(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
catalog := pkg.NewCollection(test.pkg)
|
||||
pkgs := toPackages(relationship.NewIndex(), catalog, s)
|
||||
pkgs, _ := toPackages(relationship.NewIndex(), catalog, s)
|
||||
require.Len(t, pkgs, 1)
|
||||
for _, p := range pkgs {
|
||||
if test.expectedDigest == "" {
|
||||
@@ -753,29 +755,31 @@ func Test_H1Digest(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_OtherLicenses(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tests := []struct {
|
||||
name string
|
||||
pkg pkg.Package
|
||||
expected []*spdx.OtherLicense
|
||||
expected []spdx.OtherLicense
|
||||
}{
|
||||
{
|
||||
name: "no licenseRef",
|
||||
pkg: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(),
|
||||
},
|
||||
expected: nil,
|
||||
expected: []spdx.OtherLicense{},
|
||||
},
|
||||
{
|
||||
name: "single licenseRef",
|
||||
pkg: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("foobar"),
|
||||
pkg.NewLicenseWithContext(ctx, "foobar"),
|
||||
),
|
||||
},
|
||||
expected: []*spdx.OtherLicense{
|
||||
expected: []spdx.OtherLicense{
|
||||
{
|
||||
LicenseIdentifier: "LicenseRef-foobar",
|
||||
ExtractedText: "foobar",
|
||||
LicenseName: "foobar",
|
||||
ExtractedText: "NOASSERTION",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -783,18 +787,20 @@ func Test_OtherLicenses(t *testing.T) {
|
||||
name: "multiple licenseRef",
|
||||
pkg: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("internal made up license name"),
|
||||
pkg.NewLicense("new apple license 2.0"),
|
||||
pkg.NewLicenseWithContext(ctx, "internal made up license name"),
|
||||
pkg.NewLicenseWithContext(ctx, "new apple license 2.0"),
|
||||
),
|
||||
},
|
||||
expected: []*spdx.OtherLicense{
|
||||
expected: []spdx.OtherLicense{
|
||||
{
|
||||
LicenseIdentifier: "LicenseRef-internal-made-up-license-name",
|
||||
ExtractedText: "internal made up license name",
|
||||
ExtractedText: "NOASSERTION",
|
||||
LicenseName: "internal made up license name",
|
||||
},
|
||||
{
|
||||
LicenseIdentifier: "LicenseRef-new-apple-license-2.0",
|
||||
ExtractedText: "new apple license 2.0",
|
||||
ExtractedText: "NOASSERTION",
|
||||
LicenseName: "new apple license 2.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -802,31 +808,27 @@ func Test_OtherLicenses(t *testing.T) {
|
||||
name: "LicenseRef as a valid spdx expression",
|
||||
pkg: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("LicenseRef-Fedora-Public-Domain"),
|
||||
pkg.NewLicenseWithContext(ctx, "LicenseRef-Fedora-Public-Domain"),
|
||||
),
|
||||
},
|
||||
expected: []*spdx.OtherLicense{
|
||||
{
|
||||
LicenseIdentifier: "LicenseRef-Fedora-Public-Domain",
|
||||
ExtractedText: "Fedora-Public-Domain",
|
||||
},
|
||||
},
|
||||
expected: []spdx.OtherLicense{},
|
||||
},
|
||||
{
|
||||
name: "LicenseRef as a valid spdx expression does not otherize compound spdx expressions",
|
||||
pkg: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("(MIT AND LicenseRef-Fedora-Public-Domain)"),
|
||||
pkg.NewLicenseWithContext(ctx, "(MIT AND LicenseRef-Fedora-Public-Domain)"),
|
||||
),
|
||||
},
|
||||
expected: nil,
|
||||
expected: []spdx.OtherLicense{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
catalog := pkg.NewCollection(test.pkg)
|
||||
otherLicenses := toOtherLicenses(catalog)
|
||||
rels := relationship.NewIndex()
|
||||
_, otherLicenses := toPackages(rels, catalog, sbom.SBOM{})
|
||||
require.Len(t, otherLicenses, len(test.expected))
|
||||
require.Equal(t, test.expected, otherLicenses)
|
||||
})
|
||||
@@ -861,6 +863,34 @@ func Test_toSPDXID(t *testing.T) {
|
||||
},
|
||||
expected: "Package-npm-some-package",
|
||||
},
|
||||
{
|
||||
name: "package with existing SPDX ID",
|
||||
it: func() pkg.Package {
|
||||
p := pkg.Package{
|
||||
Type: pkg.NpmPkg,
|
||||
Name: "some-package",
|
||||
}
|
||||
// SPDXRef- prefix is removed on decode (when everything is working as it should)
|
||||
p.OverrideID("Package-npm-some-package-extra!")
|
||||
return p
|
||||
}(),
|
||||
// note: we still sanitize out the "!" which is not allowed in SPDX IDs
|
||||
expected: "Package-npm-some-package-extra",
|
||||
},
|
||||
{
|
||||
name: "package with existing SPDX Ref",
|
||||
it: func() pkg.Package {
|
||||
p := pkg.Package{
|
||||
Type: pkg.NpmPkg,
|
||||
Name: "some-package",
|
||||
}
|
||||
// someone incorrectly added SPDXRef- prefix
|
||||
p.OverrideID("SPDXRef-Package-npm-some-package-extra!")
|
||||
return p
|
||||
}(),
|
||||
// note: we still sanitize out the "!" which is not allowed in SPDX IDs
|
||||
expected: "Package-npm-some-package-extra",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -874,18 +904,19 @@ func Test_toSPDXID(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_otherLicenses(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
pkg1 := pkg.Package{
|
||||
Name: "first-pkg",
|
||||
Version: "1.1",
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("MIT"),
|
||||
pkg.NewLicenseWithContext(ctx, "MIT"),
|
||||
),
|
||||
}
|
||||
pkg2 := pkg.Package{
|
||||
Name: "second-pkg",
|
||||
Version: "2.2",
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("non spdx license"),
|
||||
pkg.NewLicenseWithContext(ctx, "non spdx license"),
|
||||
),
|
||||
}
|
||||
bigText := `
|
||||
@@ -895,7 +926,7 @@ func Test_otherLicenses(t *testing.T) {
|
||||
Name: "third-pkg",
|
||||
Version: "3.3",
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense(bigText),
|
||||
pkg.NewLicenseWithContext(ctx, bigText),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -910,22 +941,25 @@ func Test_otherLicenses(t *testing.T) {
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "other licenses includes original text",
|
||||
name: "other licenses must include some original text",
|
||||
packages: []pkg.Package{pkg2},
|
||||
expected: []*spdx.OtherLicense{
|
||||
{
|
||||
LicenseIdentifier: "LicenseRef-non-spdx-license",
|
||||
ExtractedText: "non spdx license",
|
||||
LicenseName: "non spdx license",
|
||||
ExtractedText: "NOASSERTION",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "big licenses get hashed",
|
||||
name: "big licenses get hashed and space is trimmed",
|
||||
packages: []pkg.Package{pkg3},
|
||||
expected: []*spdx.OtherLicense{
|
||||
{
|
||||
LicenseIdentifier: "LicenseRef-e9a1e42833d3e456f147052f4d312101bd171a0798893169fe596ca6b55c049e",
|
||||
ExtractedText: bigText,
|
||||
LicenseIdentifier: "LicenseRef-3f17782eef51ae86f18fdd6832f5918e2b40f688b52c9adc07ba6ec1024ef408",
|
||||
// Carries through the syft-json license value when we shasum large texts
|
||||
LicenseName: "sha256:3f17782eef51ae86f18fdd6832f5918e2b40f688b52c9adc07ba6ec1024ef408",
|
||||
ExtractedText: strings.TrimSpace(bigText),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
"github.com/spdx/tools-golang/spdx"
|
||||
"github.com/spdx/tools-golang/spdx/v2/common"
|
||||
|
||||
@@ -18,6 +20,7 @@ import (
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/format/internal"
|
||||
"github.com/anchore/syft/syft/format/internal/spdxutil/helpers"
|
||||
"github.com/anchore/syft/syft/license"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
@@ -43,7 +46,7 @@ func ToSyftModel(doc *spdx.Document) (*sbom.SBOM, error) {
|
||||
},
|
||||
}
|
||||
|
||||
collectSyftPackages(s, spdxIDMap, doc.Packages)
|
||||
collectSyftPackages(s, spdxIDMap, doc)
|
||||
|
||||
collectSyftFiles(s, spdxIDMap, doc)
|
||||
|
||||
@@ -277,8 +280,12 @@ func findLinuxReleaseByPURL(doc *spdx.Document) *linux.Release {
|
||||
return nil
|
||||
}
|
||||
|
||||
func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]any, packages []*spdx.Package) {
|
||||
for _, p := range packages {
|
||||
func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]any, doc *spdx.Document) {
|
||||
skipIDs := packageIDsToSkip(doc)
|
||||
for _, p := range doc.Packages {
|
||||
if p == nil || skipIDs.Has(string(p.PackageSPDXIdentifier)) {
|
||||
continue
|
||||
}
|
||||
syftPkg := toSyftPackage(p)
|
||||
spdxIDMap[string(p.PackageSPDXIdentifier)] = syftPkg
|
||||
s.Artifacts.Packages.Add(syftPkg)
|
||||
@@ -509,7 +516,14 @@ func toSyftPackage(p *spdx.Package) pkg.Package {
|
||||
Metadata: extractMetadata(p, info),
|
||||
}
|
||||
|
||||
sP.SetID()
|
||||
internal.Backfill(sP)
|
||||
|
||||
if p.PackageSPDXIdentifier != "" {
|
||||
// always prefer the IDs from the SBOM over derived IDs
|
||||
sP.OverrideID(artifact.ID(p.PackageSPDXIdentifier))
|
||||
} else {
|
||||
sP.SetID()
|
||||
}
|
||||
|
||||
return *sP
|
||||
}
|
||||
@@ -527,14 +541,14 @@ func parseSPDXLicenses(p *spdx.Package) []pkg.License {
|
||||
|
||||
// concluded
|
||||
if p.PackageLicenseConcluded != helpers.NOASSERTION && p.PackageLicenseConcluded != helpers.NONE && p.PackageLicenseConcluded != "" {
|
||||
l := pkg.NewLicense(cleanSPDXID(p.PackageLicenseConcluded))
|
||||
l := pkg.NewLicenseWithContext(context.TODO(), cleanSPDXID(p.PackageLicenseConcluded))
|
||||
l.Type = license.Concluded
|
||||
licenses = append(licenses, l)
|
||||
}
|
||||
|
||||
// declared
|
||||
if p.PackageLicenseDeclared != helpers.NOASSERTION && p.PackageLicenseDeclared != helpers.NONE && p.PackageLicenseDeclared != "" {
|
||||
l := pkg.NewLicense(cleanSPDXID(p.PackageLicenseDeclared))
|
||||
l := pkg.NewLicenseWithContext(context.TODO(), cleanSPDXID(p.PackageLicenseDeclared))
|
||||
l.Type = license.Declared
|
||||
licenses = append(licenses, l)
|
||||
}
|
||||
@@ -648,3 +662,15 @@ func extractCPEs(p *spdx.Package) (cpes []cpe.CPE) {
|
||||
}
|
||||
return cpes
|
||||
}
|
||||
|
||||
// packageIDsToSkip returns a set of packageIDs that should not be imported
|
||||
func packageIDsToSkip(doc *spdx.Document) *strset.Set {
|
||||
skipIDs := strset.New()
|
||||
for i := 0; i < len(doc.Relationships); i++ {
|
||||
r := doc.Relationships[i]
|
||||
if r != nil && r.Relationship == spdx.RelationshipGeneratedFrom {
|
||||
skipIDs.Add(string(r.RefB.ElementRefID))
|
||||
}
|
||||
}
|
||||
return skipIDs
|
||||
}
|
||||
|
||||
@@ -664,7 +664,7 @@ func Test_directPackageFiles(t *testing.T) {
|
||||
Packages: []*spdx.Package{
|
||||
{
|
||||
PackageName: "some-package",
|
||||
PackageSPDXIdentifier: "1",
|
||||
PackageSPDXIdentifier: "1", // important!
|
||||
PackageVersion: "1.0.5",
|
||||
Files: []*spdx.File{
|
||||
{
|
||||
@@ -689,7 +689,7 @@ func Test_directPackageFiles(t *testing.T) {
|
||||
Name: "some-package",
|
||||
Version: "1.0.5",
|
||||
}
|
||||
p.SetID()
|
||||
p.OverrideID("1") // the same as the spdxID on the package element
|
||||
f := file.Location{
|
||||
LocationData: file.LocationData{
|
||||
Coordinates: file.Coordinates{
|
||||
@@ -730,3 +730,66 @@ func Test_directPackageFiles(t *testing.T) {
|
||||
|
||||
require.Equal(t, s, got)
|
||||
}
|
||||
|
||||
func Test_useSPDXIdentifierOverDerivedSyftArtifactID(t *testing.T) {
|
||||
doc := &spdx.Document{
|
||||
SPDXVersion: "SPDX-2.3",
|
||||
Packages: []*spdx.Package{
|
||||
{
|
||||
PackageName: "some-package",
|
||||
PackageSPDXIdentifier: "1", // important!
|
||||
PackageVersion: "1.0.5",
|
||||
Files: []*spdx.File{
|
||||
{
|
||||
FileName: "some-file",
|
||||
FileSPDXIdentifier: "2",
|
||||
Checksums: []spdx.Checksum{
|
||||
{
|
||||
Algorithm: "SHA1",
|
||||
Value: "a8d733c64f9123",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
s, err := ToSyftModel(doc)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, s.Artifacts.Packages.Package("1"))
|
||||
}
|
||||
|
||||
func Test_skipsPackagesWithGeneratedFromRelationship(t *testing.T) {
|
||||
doc := &spdx.Document{
|
||||
SPDXVersion: "SPDX-2.3",
|
||||
Packages: []*spdx.Package{
|
||||
{
|
||||
PackageName: "package-1",
|
||||
PackageSPDXIdentifier: "1",
|
||||
PackageVersion: "1.0.5",
|
||||
},
|
||||
{
|
||||
PackageName: "package-1-src",
|
||||
PackageSPDXIdentifier: "1-src",
|
||||
PackageVersion: "1.0.5-src",
|
||||
},
|
||||
},
|
||||
Relationships: []*spdx.Relationship{
|
||||
{
|
||||
Relationship: spdx.RelationshipGeneratedFrom,
|
||||
RefA: common.DocElementID{ // package 1
|
||||
ElementRefID: spdx.ElementID("1"),
|
||||
},
|
||||
RefB: common.DocElementID{ // generated from package 1-src
|
||||
ElementRefID: spdx.ElementID("1-src"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
s, err := ToSyftModel(doc)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, s.Artifacts.Packages.Package("1"))
|
||||
assert.Nil(t, s.Artifacts.Packages.Package("1-src"))
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error)
|
||||
return nil, "", "", fmt.Errorf("unsupported cyclonedx json document version")
|
||||
}
|
||||
|
||||
_, err = reader.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return nil, id, version, fmt.Errorf("unable to seek to start of CycloneDX JSON SBOM: %+v", err)
|
||||
}
|
||||
doc, err := d.decoder.Decode(reader)
|
||||
if err != nil {
|
||||
return nil, id, version, fmt.Errorf("unable to decode cyclonedx json document: %w", err)
|
||||
|
||||
@@ -2,6 +2,7 @@ package cyclonedxjson
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -43,6 +44,7 @@ func TestDecoder_Decode(t *testing.T) {
|
||||
t.Run(test.file, func(t *testing.T) {
|
||||
reader, err := os.Open(filepath.Join("test-fixtures", test.file))
|
||||
require.NoError(t, err)
|
||||
reset := func() { _, err = reader.Seek(0, io.SeekStart); require.NoError(t, err) }
|
||||
|
||||
dec := NewFormatDecoder()
|
||||
|
||||
@@ -51,6 +53,7 @@ func TestDecoder_Decode(t *testing.T) {
|
||||
assert.Equal(t, sbom.FormatID(""), formatID)
|
||||
assert.Equal(t, "", formatVersion)
|
||||
|
||||
reset()
|
||||
_, decodeID, decodeVersion, err := dec.Decode(reader)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, sbom.FormatID(""), decodeID)
|
||||
@@ -61,6 +64,7 @@ func TestDecoder_Decode(t *testing.T) {
|
||||
assert.Equal(t, ID, formatID)
|
||||
assert.NotEmpty(t, formatVersion)
|
||||
|
||||
reset()
|
||||
bom, decodeID, decodeVersion, err := dec.Decode(reader)
|
||||
require.NotNil(t, bom)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -3,6 +3,7 @@ package cyclonedxjson
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -116,6 +117,14 @@ func TestCycloneDxImageEncoder(t *testing.T) {
|
||||
func redactor(values ...string) testutil.Redactor {
|
||||
return testutil.NewRedactions().
|
||||
WithValuesRedacted(values...).
|
||||
WithPatternRedactorSpec(
|
||||
testutil.PatternReplacement{
|
||||
// only the source component bom-ref (not package or other component bom-refs)
|
||||
Search: regexp.MustCompile(`"component": \{[^}]*"bom-ref":\s*"(?P<redact>.+)"[^}]*}`),
|
||||
Groups: []string{"redact"}, // use the regex to anchore the search, but only replace bytes within the capture group
|
||||
Replace: "redacted",
|
||||
},
|
||||
).
|
||||
WithPatternRedactors(
|
||||
map[string]string{
|
||||
// UUIDs
|
||||
@@ -126,9 +135,6 @@ func redactor(values ...string) testutil.Redactor {
|
||||
|
||||
// image hashes
|
||||
`sha256:[A-Fa-f0-9]{64}`: `sha256:redacted`,
|
||||
|
||||
// BOM refs
|
||||
`"bom-ref":\s*"[^"]+"`: `"bom-ref":"redacted"`,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+4
-4
@@ -17,14 +17,14 @@
|
||||
]
|
||||
},
|
||||
"component": {
|
||||
"bom-ref":"redacted",
|
||||
"bom-ref": "redacted",
|
||||
"type": "file",
|
||||
"name": "some/path"
|
||||
}
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"bom-ref":"redacted",
|
||||
"bom-ref": "f04d218ff5ff50db",
|
||||
"type": "library",
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
@@ -61,7 +61,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"bom-ref":"redacted",
|
||||
"bom-ref": "pkg:deb/debian/package-2@2.0.1?package-id=39392bb5e270f669",
|
||||
"type": "library",
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
@@ -91,7 +91,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"bom-ref":"redacted",
|
||||
"bom-ref": "os:debian@1.2.3",
|
||||
"type": "operating-system",
|
||||
"name": "debian",
|
||||
"version": "1.2.3",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
]
|
||||
},
|
||||
"component": {
|
||||
"bom-ref":"redacted",
|
||||
"bom-ref": "redacted",
|
||||
"type": "container",
|
||||
"name": "user-image-input",
|
||||
"version": "sha256:redacted"
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"bom-ref":"redacted",
|
||||
"bom-ref": "2f52f617f1548337",
|
||||
"type": "library",
|
||||
"name": "package-1",
|
||||
"version": "1.0.1",
|
||||
@@ -66,7 +66,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"bom-ref":"redacted",
|
||||
"bom-ref": "pkg:deb/debian/package-2@2.0.1?package-id=4b756c6f6fb127a3",
|
||||
"type": "library",
|
||||
"name": "package-2",
|
||||
"version": "2.0.1",
|
||||
@@ -100,7 +100,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"bom-ref":"redacted",
|
||||
"bom-ref": "os:debian@1.2.3",
|
||||
"type": "operating-system",
|
||||
"name": "debian",
|
||||
"version": "1.2.3",
|
||||
|
||||
@@ -41,6 +41,10 @@ func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error)
|
||||
return nil, "", "", fmt.Errorf("unsupported cyclonedx xml document version")
|
||||
}
|
||||
|
||||
_, err = reader.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return nil, id, version, fmt.Errorf("unable to seek to start of CycloneDX XML SBOM: %w", err)
|
||||
}
|
||||
doc, err := d.decoder.Decode(reader)
|
||||
if err != nil {
|
||||
return nil, id, version, fmt.Errorf("unable to decode cyclonedx xml document: %w", err)
|
||||
|
||||
@@ -2,6 +2,7 @@ package cyclonedxxml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -44,6 +45,8 @@ func TestDecoder_Decode(t *testing.T) {
|
||||
reader, err := os.Open(filepath.Join("test-fixtures", test.file))
|
||||
require.NoError(t, err)
|
||||
|
||||
reset := func() { _, err = reader.Seek(0, io.SeekStart); require.NoError(t, err) }
|
||||
|
||||
dec := NewFormatDecoder()
|
||||
|
||||
formatID, formatVersion := dec.Identify(reader)
|
||||
@@ -51,6 +54,7 @@ func TestDecoder_Decode(t *testing.T) {
|
||||
assert.Equal(t, sbom.FormatID(""), formatID)
|
||||
assert.Equal(t, "", formatVersion)
|
||||
|
||||
reset()
|
||||
_, decodeID, decodeVersion, err := dec.Decode(reader)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, sbom.FormatID(""), decodeID)
|
||||
@@ -61,6 +65,7 @@ func TestDecoder_Decode(t *testing.T) {
|
||||
assert.Equal(t, ID, formatID)
|
||||
assert.NotEmpty(t, formatVersion)
|
||||
|
||||
reset()
|
||||
bom, decodeID, decodeVersion, err := dec.Decode(reader)
|
||||
require.NotNil(t, bom)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -90,16 +90,32 @@ func TestCycloneDxImageEncoder(t *testing.T) {
|
||||
func redactor(values ...string) testutil.Redactor {
|
||||
return testutil.NewRedactions().
|
||||
WithValuesRedacted(values...).
|
||||
WithPatternRedactorSpec(
|
||||
testutil.PatternReplacement{
|
||||
// only the source component bom-ref (not package or other component bom-refs)
|
||||
Search: regexp.MustCompile(`<component bom-ref="(?P<redact>[^"]*)" type="file">`),
|
||||
Groups: []string{"redact"}, // use the regex to anchor the search, but only replace bytes within the capture group
|
||||
Replace: "redacted",
|
||||
},
|
||||
).
|
||||
WithPatternRedactorSpec(
|
||||
testutil.PatternReplacement{
|
||||
// only the source component bom-ref (not package or other component bom-refs)
|
||||
Search: regexp.MustCompile(`<component bom-ref="(?P<redact>[^"]*)" type="container">`),
|
||||
Groups: []string{"redact"}, // use the regex to anchor the search, but only replace bytes within the capture group
|
||||
Replace: "redacted",
|
||||
},
|
||||
).
|
||||
WithPatternRedactors(
|
||||
map[string]string{
|
||||
// dates
|
||||
`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`: `redacted`,
|
||||
|
||||
// image hashes and BOM refs
|
||||
// image hashes
|
||||
`sha256:[A-Za-z0-9]{64}`: `sha256:redacted`,
|
||||
|
||||
// serial numbers and BOM refs
|
||||
`(serialNumber|bom-ref)="[^"]+"`: `$1="redacted"`,
|
||||
// serial numbers
|
||||
`(serialNumber)="[^"]+"`: `$1="redacted"`,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+3
-3
@@ -16,7 +16,7 @@
|
||||
</component>
|
||||
</metadata>
|
||||
<components>
|
||||
<component bom-ref="redacted" type="library">
|
||||
<component bom-ref="f04d218ff5ff50db" type="library">
|
||||
<name>package-1</name>
|
||||
<version>1.0.1</version>
|
||||
<licenses>
|
||||
@@ -34,7 +34,7 @@
|
||||
<property name="syft:location:0:path">/some/path/pkg1</property>
|
||||
</properties>
|
||||
</component>
|
||||
<component bom-ref="redacted" type="library">
|
||||
<component bom-ref="pkg:deb/debian/package-2@2.0.1?package-id=39392bb5e270f669" type="library">
|
||||
<name>package-2</name>
|
||||
<version>2.0.1</version>
|
||||
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
|
||||
@@ -47,7 +47,7 @@
|
||||
<property name="syft:metadata:installedSize">0</property>
|
||||
</properties>
|
||||
</component>
|
||||
<component bom-ref="redacted" type="operating-system">
|
||||
<component bom-ref="os:debian@1.2.3" type="operating-system">
|
||||
<name>debian</name>
|
||||
<version>1.2.3</version>
|
||||
<description>debian</description>
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
</component>
|
||||
</components>
|
||||
</tools>
|
||||
<component bom-ref="redacted" type="container">
|
||||
<component bom-ref="f28a4ba3ddfdddad" type="container">
|
||||
<name>user-image-input</name>
|
||||
<version>sha256:redacted</version>
|
||||
</component>
|
||||
</metadata>
|
||||
<components>
|
||||
<component bom-ref="redacted" type="library">
|
||||
<component bom-ref="2f52f617f1548337" type="library">
|
||||
<name>package-1</name>
|
||||
<version>1.0.1</version>
|
||||
<licenses>
|
||||
@@ -36,7 +36,7 @@
|
||||
<property name="syft:location:0:path">/somefile-1.txt</property>
|
||||
</properties>
|
||||
</component>
|
||||
<component bom-ref="redacted" type="library">
|
||||
<component bom-ref="pkg:deb/debian/package-2@2.0.1?package-id=4b756c6f6fb127a3" type="library">
|
||||
<name>package-2</name>
|
||||
<version>2.0.1</version>
|
||||
<cpe>cpe:2.3:*:some:package:2:*:*:*:*:*:*:*</cpe>
|
||||
@@ -50,7 +50,7 @@
|
||||
<property name="syft:metadata:installedSize">0</property>
|
||||
</properties>
|
||||
</component>
|
||||
<component bom-ref="redacted" type="operating-system">
|
||||
<component bom-ref="os:debian@1.2.3" type="operating-system">
|
||||
<name>debian</name>
|
||||
<version>1.2.3</version>
|
||||
<description>debian</description>
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||
"github.com/anchore/syft/syft/format/cyclonedxxml"
|
||||
"github.com/anchore/syft/syft/format/purls"
|
||||
"github.com/anchore/syft/syft/format/spdxjson"
|
||||
"github.com/anchore/syft/syft/format/spdxtagvalue"
|
||||
"github.com/anchore/syft/syft/format/syftjson"
|
||||
@@ -24,6 +25,7 @@ func Decoders() []sbom.FormatDecoder {
|
||||
cyclonedxjson.NewFormatDecoder(),
|
||||
spdxtagvalue.NewFormatDecoder(),
|
||||
spdxjson.NewFormatDecoder(),
|
||||
purls.NewFormatDecoder(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@ func (c *DecoderCollection) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, stri
|
||||
|
||||
var bestID sbom.FormatID
|
||||
for _, d := range c.decoders {
|
||||
_, err = reader.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return nil, "", "", fmt.Errorf("unable to seek to start of SBOM: %w", err)
|
||||
}
|
||||
id, version := d.Identify(reader)
|
||||
if id == "" || version == "" {
|
||||
if id != "" {
|
||||
@@ -42,6 +46,10 @@ func (c *DecoderCollection) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, stri
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = reader.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return nil, "", "", fmt.Errorf("unable to seek to start of SBOM: %w", err)
|
||||
}
|
||||
return d.Decode(reader)
|
||||
}
|
||||
|
||||
@@ -65,6 +73,10 @@ func (c *DecoderCollection) Identify(r io.Reader) (sbom.FormatID, string) {
|
||||
}
|
||||
|
||||
for _, d := range c.decoders {
|
||||
_, err = reader.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
log.Debugf("unable to seek to start of SBOM: %v", err)
|
||||
}
|
||||
id, version := d.Identify(reader)
|
||||
if id != "" && version != "" {
|
||||
return id, version
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||
"github.com/anchore/syft/syft/format/cyclonedxxml"
|
||||
"github.com/anchore/syft/syft/format/github"
|
||||
"github.com/anchore/syft/syft/format/purls"
|
||||
"github.com/anchore/syft/syft/format/spdxjson"
|
||||
"github.com/anchore/syft/syft/format/spdxtagvalue"
|
||||
"github.com/anchore/syft/syft/format/syftjson"
|
||||
@@ -62,6 +63,7 @@ func (o EncodersConfig) Encoders() ([]sbom.FormatEncoder, error) {
|
||||
l.addWithErr(syftjson.ID)(o.syftJSONEncoders())
|
||||
l.add(table.ID)(table.NewFormatEncoder())
|
||||
l.add(text.ID)(text.NewFormatEncoder())
|
||||
l.add(purls.ID)(purls.NewFormatEncoder())
|
||||
l.add(github.ID)(github.NewFormatEncoder())
|
||||
l.addWithErr(cyclonedxxml.ID)(o.cyclonedxXMLEncoders())
|
||||
l.addWithErr(cyclonedxjson.ID)(o.cyclonedxJSONEncoders())
|
||||
|
||||
@@ -37,6 +37,7 @@ func expectedDefaultEncoders() *strset.Set {
|
||||
expected.Add("syft-table@") // no version
|
||||
expected.Add("syft-text@") // no version
|
||||
expected.Add("github-json@") // no version
|
||||
expected.Add("purls@") // no version
|
||||
for _, v := range spdxjson.SupportedVersions() {
|
||||
expected.Add("spdx-json@" + v)
|
||||
}
|
||||
@@ -108,6 +109,7 @@ func TestEncodersConfig_Encoders(t *testing.T) {
|
||||
expected.Add("syft-table@") // no version
|
||||
expected.Add("syft-text@") // no version
|
||||
expected.Add("github-json@") // no version
|
||||
expected.Add("purls@") // no version
|
||||
expected.Add("spdx-json@" + spdxutil.DefaultVersion)
|
||||
expected.Add("spdx-tag-value@" + spdxutil.DefaultVersion)
|
||||
expected.Add("cyclonedx-json@" + cyclonedxutil.DefaultVersion)
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
// Backfill takes all information present in the package and attempts to fill in any missing information
|
||||
// from any available sources, such as the Metadata and PURL.
|
||||
//
|
||||
// Backfill does not call p.SetID(), but this needs to be called later to ensure it's up to date
|
||||
func Backfill(p *pkg.Package) {
|
||||
if p.PURL == "" {
|
||||
return
|
||||
}
|
||||
|
||||
purl, err := packageurl.FromString(p.PURL)
|
||||
if err != nil {
|
||||
log.Debugf("unable to parse purl: %s: %w", p.PURL, err)
|
||||
return
|
||||
}
|
||||
|
||||
var cpes []cpe.CPE
|
||||
epoch := ""
|
||||
|
||||
for _, qualifier := range purl.Qualifiers {
|
||||
switch qualifier.Key {
|
||||
case pkg.PURLQualifierCPES:
|
||||
rawCpes := strings.Split(qualifier.Value, ",")
|
||||
for _, rawCpe := range rawCpes {
|
||||
c, err := cpe.New(rawCpe, cpe.DeclaredSource)
|
||||
if err != nil {
|
||||
log.Debugf("unable to decode cpe %s in purl %s: %w", rawCpe, p.PURL, err)
|
||||
continue
|
||||
}
|
||||
cpes = append(cpes, c)
|
||||
}
|
||||
case pkg.PURLQualifierEpoch:
|
||||
epoch = qualifier.Value
|
||||
}
|
||||
}
|
||||
|
||||
if p.Type == "" {
|
||||
p.Type = pkg.TypeFromPURL(p.PURL)
|
||||
}
|
||||
if p.Language == "" {
|
||||
p.Language = pkg.LanguageFromPURL(p.PURL)
|
||||
}
|
||||
if p.Name == "" {
|
||||
p.Name = nameFromPurl(purl)
|
||||
}
|
||||
|
||||
setVersionFromPurl(p, purl, epoch)
|
||||
|
||||
if p.Language == pkg.Java {
|
||||
setJavaMetadataFromPurl(p, purl)
|
||||
}
|
||||
|
||||
for _, c := range cpes {
|
||||
if slices.Contains(p.CPEs, c) {
|
||||
continue
|
||||
}
|
||||
p.CPEs = append(p.CPEs, c)
|
||||
}
|
||||
}
|
||||
|
||||
func setJavaMetadataFromPurl(p *pkg.Package, _ packageurl.PackageURL) {
|
||||
if p.Type != pkg.JavaPkg {
|
||||
return
|
||||
}
|
||||
if p.Metadata == nil {
|
||||
// since we don't know if the purl elements directly came from pom properties or the manifest,
|
||||
// we can only go as far as to set the type to JavaArchive, but not fill in the group id and artifact id
|
||||
p.Metadata = pkg.JavaArchive{}
|
||||
}
|
||||
}
|
||||
|
||||
func setVersionFromPurl(p *pkg.Package, purl packageurl.PackageURL, epoch string) {
|
||||
if p.Version == "" {
|
||||
p.Version = purl.Version
|
||||
}
|
||||
|
||||
if epoch != "" && p.Type == pkg.RpmPkg && !epochPrefix.MatchString(p.Version) {
|
||||
p.Version = fmt.Sprintf("%s:%s", epoch, p.Version)
|
||||
}
|
||||
}
|
||||
|
||||
var epochPrefix = regexp.MustCompile(`^\d+:`)
|
||||
|
||||
// nameFromPurl returns the syft package name of the package from the purl. If the purl includes a namespace,
|
||||
// the name is prefixed as appropriate based on the PURL type
|
||||
func nameFromPurl(purl packageurl.PackageURL) string {
|
||||
if !nameExcludesPurlNamespace(purl.Type) && purl.Namespace != "" {
|
||||
return fmt.Sprintf("%s/%s", purl.Namespace, purl.Name)
|
||||
}
|
||||
return purl.Name
|
||||
}
|
||||
|
||||
func nameExcludesPurlNamespace(purlType string) bool {
|
||||
switch purlType {
|
||||
case packageurl.TypeAlpine,
|
||||
packageurl.TypeAlpm,
|
||||
packageurl.TypeConan,
|
||||
packageurl.TypeCpan,
|
||||
packageurl.TypeDebian,
|
||||
packageurl.TypeMaven,
|
||||
packageurl.TypeQpkg,
|
||||
packageurl.TypeRPM,
|
||||
packageurl.TypeSWID:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Test_Backfill(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in pkg.Package
|
||||
expected pkg.Package
|
||||
}{
|
||||
{
|
||||
name: "npm type",
|
||||
in: pkg.Package{
|
||||
PURL: "pkg:npm/test@3.0.0",
|
||||
},
|
||||
expected: pkg.Package{
|
||||
PURL: "pkg:npm/test@3.0.0",
|
||||
Type: pkg.NpmPkg,
|
||||
Language: pkg.JavaScript,
|
||||
Name: "test",
|
||||
Version: "3.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rpm no epoch",
|
||||
in: pkg.Package{
|
||||
PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&upstream=dbus-1.12.8-26.el8.src.rpm",
|
||||
},
|
||||
expected: pkg.Package{
|
||||
PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&upstream=dbus-1.12.8-26.el8.src.rpm",
|
||||
Type: pkg.RpmPkg,
|
||||
Name: "dbus-common",
|
||||
Version: "1.12.8-26.el8",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rpm epoch",
|
||||
in: pkg.Package{
|
||||
PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm",
|
||||
},
|
||||
expected: pkg.Package{
|
||||
PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm",
|
||||
Type: pkg.RpmPkg,
|
||||
Name: "dbus-common",
|
||||
Version: "1:1.12.8-26.el8",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bad cpe",
|
||||
in: pkg.Package{
|
||||
PURL: "pkg:npm/testp@3.0.0?cpes=cpe:2.3a:testv:testp:3.0.0:*:*:*:*:*:*:*",
|
||||
},
|
||||
expected: pkg.Package{
|
||||
PURL: "pkg:npm/testp@3.0.0?cpes=cpe:2.3a:testv:testp:3.0.0:*:*:*:*:*:*:*",
|
||||
Type: pkg.NpmPkg,
|
||||
Language: pkg.JavaScript,
|
||||
Name: "testp",
|
||||
Version: "3.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good cpe",
|
||||
in: pkg.Package{
|
||||
PURL: "pkg:npm/testp@3.0.0?cpes=cpe:2.3:a:testv:testp:3.0.0:*:*:*:*:*:*:*",
|
||||
},
|
||||
expected: pkg.Package{
|
||||
PURL: "pkg:npm/testp@3.0.0?cpes=cpe:2.3:a:testv:testp:3.0.0:*:*:*:*:*:*:*",
|
||||
Type: pkg.NpmPkg,
|
||||
Language: pkg.JavaScript,
|
||||
Name: "testp",
|
||||
Version: "3.0.0",
|
||||
CPEs: []cpe.CPE{
|
||||
{
|
||||
Attributes: cpe.Attributes{
|
||||
Part: "a",
|
||||
Vendor: "testv",
|
||||
Product: "testp",
|
||||
Version: "3.0.0",
|
||||
},
|
||||
Source: cpe.DeclaredSource,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "java type",
|
||||
in: pkg.Package{
|
||||
PURL: "pkg:maven/org.apache/some-thing@1.2.3",
|
||||
},
|
||||
expected: pkg.Package{
|
||||
PURL: "pkg:maven/org.apache/some-thing@1.2.3",
|
||||
Type: pkg.JavaPkg,
|
||||
Language: pkg.Java,
|
||||
Name: "some-thing",
|
||||
Version: "1.2.3",
|
||||
// we intentionally don't claim we found a pom properties file with a groupID from the purl.
|
||||
// but we do claim that we found java data with an empty type.
|
||||
Metadata: pkg.JavaArchive{},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
Backfill(&tt.in)
|
||||
tt.in.OverrideID("")
|
||||
require.Equal(t, tt.expected, tt.in)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_nameFromPurl(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
in: "pkg:npm/some-name@3.0.0",
|
||||
expected: "some-name",
|
||||
},
|
||||
{
|
||||
in: "pkg:maven/org.apache/some-name@1.2.3",
|
||||
expected: "some-name",
|
||||
},
|
||||
{
|
||||
in: "pkg:deb/debian/some-name@3.0.0",
|
||||
expected: "some-name",
|
||||
},
|
||||
{
|
||||
in: "pkg:rpm/redhat/some-name@3.0.0",
|
||||
expected: "some-name",
|
||||
},
|
||||
{
|
||||
in: "pkg:gem/some-name@3.0.0",
|
||||
expected: "some-name",
|
||||
},
|
||||
{
|
||||
in: "pkg:apk/alpine/some-name@3.0.0",
|
||||
expected: "some-name",
|
||||
},
|
||||
{
|
||||
in: "pkg:docker/some-org/some-name@3.0.0",
|
||||
expected: "some-org/some-name",
|
||||
},
|
||||
{
|
||||
in: "pkg:npm/some-name@3.0.0",
|
||||
expected: "some-name",
|
||||
},
|
||||
{
|
||||
in: "pkg:npm/some-org/some-name@3.0.0",
|
||||
expected: "some-org/some-name",
|
||||
},
|
||||
{
|
||||
in: "pkg:oci/library/mysql@8.1.0",
|
||||
expected: "library/mysql",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.in, func(t *testing.T) {
|
||||
p, err := packageurl.FromString(tt.in)
|
||||
require.NoError(t, err)
|
||||
got := nameFromPurl(p)
|
||||
require.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
package cyclonedxutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/syft/syft/format/internal/stream"
|
||||
)
|
||||
|
||||
type Decoder struct {
|
||||
@@ -20,19 +17,10 @@ func NewDecoder(format cyclonedx.BOMFileFormat) Decoder {
|
||||
}
|
||||
|
||||
func (d Decoder) Decode(r io.Reader) (*cyclonedx.BOM, error) {
|
||||
reader, err := stream.SeekableReader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
doc := &cyclonedx.BOM{
|
||||
Components: &[]cyclonedx.Component{},
|
||||
}
|
||||
if _, err := reader.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, fmt.Errorf("unable to seek to start of CycloneDX SBOM: %w", err)
|
||||
}
|
||||
|
||||
err = cyclonedx.NewBOMDecoder(reader, d.format).Decode(doc)
|
||||
err := cyclonedx.NewBOMDecoder(r, d.format).Decode(doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/format/internal"
|
||||
"github.com/anchore/syft/syft/internal/packagemetadata"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func EncodeComponent(p pkg.Package) cyclonedx.Component {
|
||||
func EncodeComponent(p pkg.Package, locationSorter func(a, b file.Location) int) cyclonedx.Component {
|
||||
props := EncodeProperties(p, "syft:package")
|
||||
|
||||
if p.Metadata != nil {
|
||||
@@ -23,7 +26,7 @@ func EncodeComponent(p pkg.Package) cyclonedx.Component {
|
||||
}
|
||||
|
||||
props = append(props, encodeCPEs(p)...)
|
||||
locations := p.Locations.ToSlice()
|
||||
locations := p.Locations.ToSlice(locationSorter)
|
||||
if len(locations) > 0 {
|
||||
props = append(props, EncodeProperties(locations, "syft:location")...)
|
||||
}
|
||||
@@ -84,31 +87,106 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package {
|
||||
}
|
||||
|
||||
p := &pkg.Package{
|
||||
Name: c.Name,
|
||||
Version: c.Version,
|
||||
Locations: decodeLocations(values),
|
||||
Licenses: pkg.NewLicenseSet(decodeLicenses(c)...),
|
||||
CPEs: decodeCPEs(c),
|
||||
PURL: c.PackageURL,
|
||||
}
|
||||
|
||||
// note: this may write in syft package type information
|
||||
DecodeInto(p, values, "syft:package", CycloneDXFields)
|
||||
|
||||
metadataType := values["syft:package:metadataType"]
|
||||
|
||||
p.Metadata = decodePackageMetadata(values, c, metadataType)
|
||||
|
||||
// this will either use the purl from the component or generate a new one based off of any type information
|
||||
// that was decoded above.
|
||||
p.PURL = getPURL(c, p.Type)
|
||||
|
||||
if p.Type == "" {
|
||||
p.Type = pkg.TypeFromPURL(p.PURL)
|
||||
}
|
||||
|
||||
if p.Language == "" {
|
||||
p.Language = pkg.LanguageFromPURL(p.PURL)
|
||||
}
|
||||
setPackageName(p, c)
|
||||
|
||||
internal.Backfill(p)
|
||||
p.SetID()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func getPURL(c *cyclonedx.Component, ty pkg.Type) string {
|
||||
if c.PackageURL != "" {
|
||||
// if there is a purl that where the namespace does not match the group information, we may
|
||||
// accidentally drop group. We should consider adding group as a top-level syft package field.
|
||||
return c.PackageURL
|
||||
}
|
||||
|
||||
if strings.HasPrefix(c.BOMRef, "pkg:") {
|
||||
// the bomref is a purl, so try to use that as the purl
|
||||
_, err := packageurl.FromString(c.BOMRef)
|
||||
if err == nil {
|
||||
return c.BOMRef
|
||||
}
|
||||
}
|
||||
|
||||
if ty == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
tyStr := ty.PackageURLType()
|
||||
switch tyStr {
|
||||
case "", packageurl.TypeGeneric:
|
||||
return ""
|
||||
}
|
||||
|
||||
purl := packageurl.PackageURL{
|
||||
Type: tyStr,
|
||||
Namespace: c.Group,
|
||||
Name: c.Name,
|
||||
Version: c.Version,
|
||||
}
|
||||
|
||||
return purl.ToString()
|
||||
}
|
||||
|
||||
func setPackageName(p *pkg.Package, c *cyclonedx.Component) {
|
||||
name := c.Name
|
||||
if c.Group != "" {
|
||||
switch p.Type {
|
||||
case pkg.JavaPkg:
|
||||
if p.Metadata == nil {
|
||||
p.Metadata = pkg.JavaArchive{}
|
||||
}
|
||||
var pomProperties *pkg.JavaPomProperties
|
||||
javaMetadata, ok := p.Metadata.(pkg.JavaArchive)
|
||||
if ok {
|
||||
pomProperties = javaMetadata.PomProperties
|
||||
if pomProperties == nil {
|
||||
pomProperties = &pkg.JavaPomProperties{}
|
||||
javaMetadata.PomProperties = pomProperties
|
||||
p.Metadata = javaMetadata
|
||||
}
|
||||
}
|
||||
if pomProperties != nil {
|
||||
if pomProperties.ArtifactID == "" {
|
||||
pomProperties.ArtifactID = c.Name
|
||||
}
|
||||
if pomProperties.GroupID == "" {
|
||||
pomProperties.GroupID = c.Group
|
||||
}
|
||||
if pomProperties.Version == "" {
|
||||
pomProperties.Version = p.Version
|
||||
}
|
||||
}
|
||||
default:
|
||||
name = fmt.Sprintf("%s/%s", c.Group, name)
|
||||
}
|
||||
}
|
||||
p.Name = name
|
||||
}
|
||||
|
||||
func decodeLocations(vals map[string]string) file.LocationSet {
|
||||
v := Decode(reflect.TypeOf([]file.Location{}), vals, "syft:location", CycloneDXFields)
|
||||
out, ok := v.([]file.Location)
|
||||
|
||||
@@ -152,7 +152,7 @@ func Test_encodeComponentProperties(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := EncodeComponent(test.input)
|
||||
c := EncodeComponent(test.input, file.LocationSorter(nil))
|
||||
if test.expected == nil {
|
||||
if c.Properties != nil {
|
||||
t.Fatalf("expected no properties, got: %+v", *c.Properties)
|
||||
@@ -212,7 +212,7 @@ func Test_encodeCompomentType(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.pkg.ID()
|
||||
p := EncodeComponent(tt.pkg)
|
||||
p := EncodeComponent(tt.pkg, file.LocationSorter(nil))
|
||||
assert.Equal(t, tt.want, p)
|
||||
})
|
||||
}
|
||||
@@ -275,6 +275,7 @@ func Test_decodeComponent(t *testing.T) {
|
||||
component cyclonedx.Component
|
||||
wantLanguage pkg.Language
|
||||
wantMetadata any
|
||||
wantPURL string
|
||||
}{
|
||||
{
|
||||
name: "derive language from pURL if missing",
|
||||
@@ -286,6 +287,18 @@ func Test_decodeComponent(t *testing.T) {
|
||||
BOMRef: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
|
||||
},
|
||||
wantLanguage: pkg.Java,
|
||||
wantPURL: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
|
||||
},
|
||||
{
|
||||
name: "derive language from bomref if missing",
|
||||
component: cyclonedx.Component{
|
||||
Name: "ch.qos.logback/logback-classic",
|
||||
Version: "1.2.3",
|
||||
Type: "library",
|
||||
BOMRef: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
|
||||
},
|
||||
wantLanguage: pkg.Java,
|
||||
wantPURL: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
|
||||
},
|
||||
{
|
||||
name: "handle RpmdbMetadata type without properties",
|
||||
@@ -303,6 +316,7 @@ func Test_decodeComponent(t *testing.T) {
|
||||
},
|
||||
},
|
||||
wantMetadata: pkg.RpmDBEntry{},
|
||||
wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
|
||||
},
|
||||
{
|
||||
name: "handle RpmdbMetadata type with properties",
|
||||
@@ -326,6 +340,24 @@ func Test_decodeComponent(t *testing.T) {
|
||||
wantMetadata: pkg.RpmDBEntry{
|
||||
Release: "some-release",
|
||||
},
|
||||
wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
|
||||
},
|
||||
{
|
||||
name: "generate a purl from package type",
|
||||
component: cyclonedx.Component{
|
||||
Name: "log4j",
|
||||
Group: "org.apache.logging.log4j",
|
||||
Version: "2.0.4",
|
||||
Type: "library",
|
||||
BOMRef: "log4j",
|
||||
Properties: &[]cyclonedx.Property{
|
||||
{
|
||||
Name: "syft:package:type",
|
||||
Value: "java-archive",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantPURL: "pkg:maven/org.apache.logging.log4j/log4j@2.0.4",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -338,9 +370,122 @@ func Test_decodeComponent(t *testing.T) {
|
||||
if tt.wantMetadata != nil {
|
||||
assert.Truef(t, reflect.DeepEqual(tt.wantMetadata, p.Metadata), "metadata should match: %+v != %+v", tt.wantMetadata, p.Metadata)
|
||||
}
|
||||
if tt.wantMetadata == nil && tt.wantLanguage == "" {
|
||||
|
||||
if tt.wantPURL != "" {
|
||||
assert.Equal(t, tt.wantPURL, p.PURL, "purl should match")
|
||||
}
|
||||
|
||||
if tt.wantMetadata == nil && tt.wantLanguage == "" && tt.wantPURL == "" {
|
||||
t.Fatal("this is a useless test, please remove it")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
component *cyclonedx.Component
|
||||
pkgType pkg.Type
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "component with PackageURL",
|
||||
component: &cyclonedx.Component{
|
||||
PackageURL: "pkg:npm/lodash@4.17.21",
|
||||
Name: "lodash",
|
||||
Version: "4.17.20", // different version to verify PackageURL is used
|
||||
Group: "npm",
|
||||
},
|
||||
pkgType: pkg.NpmPkg,
|
||||
expected: "pkg:npm/lodash@4.17.21",
|
||||
},
|
||||
{
|
||||
name: "component with BOMRef as valid PURL",
|
||||
component: &cyclonedx.Component{
|
||||
BOMRef: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
|
||||
Name: "commons-lang3",
|
||||
Version: "3.11.0", // different version to verify BOMRef is used
|
||||
Group: "org.apache.commons",
|
||||
},
|
||||
pkgType: pkg.JavaPkg,
|
||||
expected: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
|
||||
},
|
||||
{
|
||||
name: "component with BOMRef not a valid PURL",
|
||||
component: &cyclonedx.Component{
|
||||
BOMRef: "not-a-purl-ref",
|
||||
Name: "commons-lang3",
|
||||
Version: "3.12.0",
|
||||
Group: "org.apache.commons",
|
||||
},
|
||||
pkgType: pkg.JavaPkg,
|
||||
expected: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
|
||||
},
|
||||
{
|
||||
name: "component with unknown package type",
|
||||
component: &cyclonedx.Component{
|
||||
Name: "some-component",
|
||||
Version: "1.0.0",
|
||||
Group: "org.example",
|
||||
},
|
||||
pkgType: pkg.UnknownPkg,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "component with empty package type",
|
||||
component: &cyclonedx.Component{
|
||||
Name: "some-component",
|
||||
Version: "1.0.0",
|
||||
Group: "org.example",
|
||||
},
|
||||
pkgType: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "component with generic package type",
|
||||
component: &cyclonedx.Component{
|
||||
Name: "some-component",
|
||||
Version: "1.0.0",
|
||||
Group: "org.example",
|
||||
},
|
||||
pkgType: pkg.LinuxKernelModulePkg,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "component with valid package type",
|
||||
component: &cyclonedx.Component{
|
||||
Name: "react",
|
||||
Version: "18.2.0",
|
||||
Group: "facebook",
|
||||
},
|
||||
pkgType: pkg.NpmPkg,
|
||||
expected: "pkg:npm/facebook/react@18.2.0",
|
||||
},
|
||||
{
|
||||
name: "component with no group",
|
||||
component: &cyclonedx.Component{
|
||||
Name: "express",
|
||||
Version: "4.18.2",
|
||||
},
|
||||
pkgType: pkg.NpmPkg,
|
||||
expected: "pkg:npm/express@4.18.2",
|
||||
},
|
||||
{
|
||||
name: "component with no version",
|
||||
component: &cyclonedx.Component{
|
||||
Name: "express",
|
||||
Group: "npm",
|
||||
},
|
||||
pkgType: pkg.NpmPkg,
|
||||
expected: "pkg:npm/npm/express",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := getPURL(tt.component, tt.pkgType)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
@@ -66,12 +65,16 @@ func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[str
|
||||
case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary:
|
||||
p := decodeComponent(component)
|
||||
idMap[component.BOMRef] = p
|
||||
syftID := extractSyftPacakgeID(component.BOMRef)
|
||||
if component.BOMRef != "" {
|
||||
// always prefer the IDs from the SBOM over derived IDs
|
||||
p.OverrideID(artifact.ID(component.BOMRef))
|
||||
} else {
|
||||
p.SetID()
|
||||
}
|
||||
syftID := p.ID()
|
||||
if syftID != "" {
|
||||
idMap[syftID] = p
|
||||
idMap[string(syftID)] = p
|
||||
}
|
||||
// TODO there must be a better way than needing to call this manually:
|
||||
p.SetID()
|
||||
s.Artifacts.Packages.Add(*p)
|
||||
}
|
||||
|
||||
@@ -82,19 +85,6 @@ func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[str
|
||||
}
|
||||
}
|
||||
|
||||
func extractSyftPacakgeID(i string) string {
|
||||
instance, err := packageurl.FromString(i)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, q := range instance.Qualifiers {
|
||||
if q.Key == "package-id" {
|
||||
return q.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func linuxReleaseFromComponents(components []cyclonedx.Component) *linux.Release {
|
||||
for i := range components {
|
||||
component := &components[i]
|
||||
|
||||
@@ -421,3 +421,39 @@ func Test_decodeDependencies(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_useBomRefOverDerivedSyftArtifactID(t *testing.T) {
|
||||
|
||||
packageWithId := cyclonedx.Component{
|
||||
BOMRef: "pkg:maven/org.springframework.boot/spring-boot-starter-test?package-id=646a5a71a4abeee0",
|
||||
Type: cyclonedx.ComponentTypeLibrary,
|
||||
Name: "spring-boot-starter-test",
|
||||
Version: "",
|
||||
PackageURL: "pkg:maven/org.springframework.boot/spring-boot-starter-test",
|
||||
}
|
||||
|
||||
packageWithoutId := cyclonedx.Component{
|
||||
BOMRef: "pkg:maven/org.springframework.boot/spring-boot-starter-webflux",
|
||||
Type: cyclonedx.ComponentTypeLibrary,
|
||||
Name: "spring-boot-starter-webflux",
|
||||
Version: "",
|
||||
PackageURL: "pkg:maven/org.springframework.boot/spring-boot-starter-webflux",
|
||||
}
|
||||
|
||||
bom := cyclonedx.BOM{Metadata: nil,
|
||||
Components: &[]cyclonedx.Component{
|
||||
packageWithId,
|
||||
packageWithoutId,
|
||||
}}
|
||||
|
||||
s, err := ToSyftModel(&bom)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, s.Artifacts.Packages.Package("pkg:maven/org.springframework.boot/spring-boot-starter-test?package-id=646a5a71a4abeee0"))
|
||||
|
||||
pkgsWithoutID := s.Artifacts.Packages.PackagesByName(packageWithoutId.Name)
|
||||
|
||||
assert.Len(t, pkgsWithoutID, 1)
|
||||
assert.NotEqual(t, "", pkgsWithoutID[0].ID())
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
@@ -58,11 +59,11 @@ func decodeLicenses(c *cyclonedx.Component) []pkg.License {
|
||||
// these fields are mutually exclusive in the spec
|
||||
switch {
|
||||
case l.License != nil && l.License.ID != "":
|
||||
licenses = append(licenses, pkg.NewLicenseFromURLs(l.License.ID, l.License.URL))
|
||||
licenses = append(licenses, pkg.NewLicenseFromURLsWithContext(context.TODO(), l.License.ID, l.License.URL))
|
||||
case l.License != nil && l.License.Name != "":
|
||||
licenses = append(licenses, pkg.NewLicenseFromURLs(l.License.Name, l.License.URL))
|
||||
licenses = append(licenses, pkg.NewLicenseFromURLsWithContext(context.TODO(), l.License.Name, l.License.URL))
|
||||
case l.Expression != "":
|
||||
licenses = append(licenses, pkg.NewLicense(l.Expression))
|
||||
licenses = append(licenses, pkg.NewLicenseWithContext(context.TODO(), l.Expression))
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func Test_encodeLicense(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
@@ -25,7 +27,7 @@ func Test_encodeLicense(t *testing.T) {
|
||||
name: "no SPDX licenses",
|
||||
input: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("RandomLicense"),
|
||||
pkg.NewLicenseWithContext(ctx, "RandomLicense"),
|
||||
),
|
||||
},
|
||||
expected: &cyclonedx.Licenses{
|
||||
@@ -40,8 +42,8 @@ func Test_encodeLicense(t *testing.T) {
|
||||
name: "single SPDX ID and Non SPDX ID",
|
||||
input: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("mit"),
|
||||
pkg.NewLicense("FOOBAR"),
|
||||
pkg.NewLicenseWithContext(ctx, "mit"),
|
||||
pkg.NewLicenseWithContext(ctx, "FOOBAR"),
|
||||
),
|
||||
},
|
||||
expected: &cyclonedx.Licenses{
|
||||
@@ -61,7 +63,7 @@ func Test_encodeLicense(t *testing.T) {
|
||||
name: "with complex SPDX license expression",
|
||||
input: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("MIT AND GPL-3.0-only"),
|
||||
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"),
|
||||
),
|
||||
},
|
||||
expected: &cyclonedx.Licenses{
|
||||
@@ -74,8 +76,8 @@ func Test_encodeLicense(t *testing.T) {
|
||||
name: "with multiple complex SPDX license expression",
|
||||
input: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("MIT AND GPL-3.0-only"),
|
||||
pkg.NewLicense("MIT AND GPL-3.0-only WITH Classpath-exception-2.0"),
|
||||
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"),
|
||||
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only WITH Classpath-exception-2.0"),
|
||||
),
|
||||
},
|
||||
expected: &cyclonedx.Licenses{
|
||||
@@ -88,9 +90,9 @@ func Test_encodeLicense(t *testing.T) {
|
||||
name: "with multiple URLs and expressions",
|
||||
input: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicenseFromURLs("MIT", "https://opensource.org/licenses/MIT", "https://spdx.org/licenses/MIT.html"),
|
||||
pkg.NewLicense("MIT AND GPL-3.0-only"),
|
||||
pkg.NewLicenseFromURLs("FakeLicense", "htts://someurl.com"),
|
||||
pkg.NewLicenseFromURLsWithContext(ctx, "MIT", "https://opensource.org/licenses/MIT", "https://spdx.org/licenses/MIT.html"),
|
||||
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"),
|
||||
pkg.NewLicenseFromURLsWithContext(ctx, "FakeLicense", "htts://someurl.com"),
|
||||
),
|
||||
},
|
||||
expected: &cyclonedx.Licenses{
|
||||
@@ -123,8 +125,8 @@ func Test_encodeLicense(t *testing.T) {
|
||||
name: "with multiple values licenses are deduplicated",
|
||||
input: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("Apache-2"),
|
||||
pkg.NewLicense("Apache-2.0"),
|
||||
pkg.NewLicenseWithContext(ctx, "Apache-2"),
|
||||
pkg.NewLicenseWithContext(ctx, "Apache-2.0"),
|
||||
),
|
||||
},
|
||||
expected: &cyclonedx.Licenses{
|
||||
@@ -139,9 +141,9 @@ func Test_encodeLicense(t *testing.T) {
|
||||
name: "with multiple URLs and single with no URLs",
|
||||
input: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("MIT"),
|
||||
pkg.NewLicenseFromURLs("MIT", "https://opensource.org/licenses/MIT", "https://spdx.org/licenses/MIT.html"),
|
||||
pkg.NewLicense("MIT AND GPL-3.0-only"),
|
||||
pkg.NewLicenseWithContext(ctx, "MIT"),
|
||||
pkg.NewLicenseFromURLsWithContext(ctx, "MIT", "https://opensource.org/licenses/MIT", "https://spdx.org/licenses/MIT.html"),
|
||||
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"),
|
||||
),
|
||||
},
|
||||
expected: &cyclonedx.Licenses{
|
||||
@@ -167,7 +169,7 @@ func Test_encodeLicense(t *testing.T) {
|
||||
{
|
||||
name: "single parenthesized SPDX expression",
|
||||
input: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValues("(MIT OR Apache-2.0)")...),
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, "(MIT OR Apache-2.0)")...),
|
||||
},
|
||||
expected: &cyclonedx.Licenses{
|
||||
{
|
||||
@@ -179,7 +181,7 @@ func Test_encodeLicense(t *testing.T) {
|
||||
name: "single license AND to parenthesized SPDX expression",
|
||||
// (LGPL-3.0-or-later OR GPL-2.0-or-later OR (LGPL-3.0-or-later AND GPL-2.0-or-later)) AND GFDL-1.3-invariants-or-later
|
||||
input: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValues("(LGPL-3.0-or-later OR GPL-2.0-or-later OR (LGPL-3.0-or-later AND GPL-2.0-or-later)) AND GFDL-1.3-invariants-or-later")...),
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, "(LGPL-3.0-or-later OR GPL-2.0-or-later OR (LGPL-3.0-or-later AND GPL-2.0-or-later)) AND GFDL-1.3-invariants-or-later")...),
|
||||
},
|
||||
expected: &cyclonedx.Licenses{
|
||||
{
|
||||
@@ -248,7 +250,6 @@ func TestDecodeLicenses(t *testing.T) {
|
||||
Value: "RandomLicense",
|
||||
// CycloneDX specification doesn't give a field for determining the license type
|
||||
Type: license.Declared,
|
||||
URLs: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -268,7 +269,6 @@ func TestDecodeLicenses(t *testing.T) {
|
||||
Value: "MIT",
|
||||
SPDXExpression: "MIT",
|
||||
Type: license.Declared,
|
||||
URLs: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func GetLocationSorters(s sbom.SBOM) (func(a, b file.Location) int, func(a, b file.Coordinates) int) {
|
||||
var layers []string
|
||||
if m, ok := s.Source.Metadata.(source.ImageMetadata); ok {
|
||||
for _, l := range m.Layers {
|
||||
layers = append(layers, l.Digest)
|
||||
}
|
||||
}
|
||||
return file.LocationSorter(layers), file.CoordinatesSorter(layers)
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spdx/tools-golang/spdx"
|
||||
|
||||
"github.com/anchore/syft/internal/spdxlicense"
|
||||
"github.com/anchore/syft/syft/license"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func License(p pkg.Package) (concluded, declared string) {
|
||||
func License(p pkg.Package) (concluded, declared string, otherLicenses []spdx.OtherLicense) {
|
||||
// source: https://spdx.github.io/spdx-spec/v2.3/package-information/#713-concluded-license-field
|
||||
// The options to populate this field are limited to:
|
||||
// A valid SPDX License Expression as defined in Annex D;
|
||||
@@ -21,15 +22,15 @@ func License(p pkg.Package) (concluded, declared string) {
|
||||
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
|
||||
|
||||
if p.Licenses.Empty() {
|
||||
return NOASSERTION, NOASSERTION
|
||||
return NOASSERTION, NOASSERTION, nil
|
||||
}
|
||||
|
||||
// take all licenses and assume an AND expression;
|
||||
// for information about license expressions see:
|
||||
// https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/
|
||||
pc, pd := ParseLicenses(p.Licenses.ToSlice())
|
||||
pc, pd, ol := ParseLicenses(p.Licenses.ToSlice())
|
||||
|
||||
return joinLicenses(pc), joinLicenses(pd)
|
||||
return joinLicenses(pc), joinLicenses(pd), ol
|
||||
}
|
||||
|
||||
func joinLicenses(licenses []SPDXLicense) string {
|
||||
@@ -58,14 +59,32 @@ func joinLicenses(licenses []SPDXLicense) string {
|
||||
}
|
||||
|
||||
type SPDXLicense struct {
|
||||
ID string
|
||||
Value string
|
||||
FullText string
|
||||
// Valid SPDX ID OR License Value (should have LicenseRef- prefix and be sanitized)
|
||||
// OR combination of the above as a valid SPDX License Expression as defined in Annex D.
|
||||
// https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/
|
||||
ID string
|
||||
// If the SPDX license is not on the SPDX License List
|
||||
LicenseName string
|
||||
FullText string // 0..1 (Mandatory, one) if there is a License Identifier assigned (LicenseRef).
|
||||
URLs []string
|
||||
}
|
||||
|
||||
func ParseLicenses(raw []pkg.License) (concluded, declared []SPDXLicense) {
|
||||
func ParseLicenses(raw []pkg.License) (concluded, declared []SPDXLicense, otherLicenses []spdx.OtherLicense) {
|
||||
for _, l := range raw {
|
||||
candidate := createSPDXLicense(l)
|
||||
|
||||
// this determines if the candidate falls under https://spdx.github.io/spdx-spec/v2.3/other-licensing-information-detected/#
|
||||
// of the SPDX spec, where:
|
||||
// - we should not have a complex SPDX expression
|
||||
// - if a single license, it should not be a known license (on the SPDX license list)
|
||||
if l.SPDXExpression == "" && strings.Contains(candidate.ID, spdxlicense.LicenseRefPrefix) {
|
||||
otherLicenses = append(otherLicenses, spdx.OtherLicense{
|
||||
LicenseIdentifier: candidate.ID,
|
||||
ExtractedText: candidate.FullText,
|
||||
LicenseName: candidate.LicenseName,
|
||||
LicenseCrossReferences: candidate.URLs,
|
||||
})
|
||||
}
|
||||
switch l.Type {
|
||||
case license.Concluded:
|
||||
concluded = append(concluded, candidate)
|
||||
@@ -74,35 +93,70 @@ func ParseLicenses(raw []pkg.License) (concluded, declared []SPDXLicense) {
|
||||
}
|
||||
}
|
||||
|
||||
return concluded, declared
|
||||
return concluded, declared, otherLicenses
|
||||
}
|
||||
|
||||
func createSPDXLicense(l pkg.License) SPDXLicense {
|
||||
candidate := SPDXLicense{
|
||||
ID: generateLicenseID(l),
|
||||
FullText: l.Contents,
|
||||
// source: https://spdx.github.io/spdx-spec/v2.3/other-licensing-information-detected/#102-extracted-text-field
|
||||
// we need to populate this field in the spdx document if we have a license ref
|
||||
// 0..1 (Mandatory, one) if there is a License Identifier assigned (LicenseRef).
|
||||
ft := NOASSERTION
|
||||
if l.Contents != "" {
|
||||
ft = l.Contents
|
||||
}
|
||||
|
||||
if l.SPDXExpression == "" {
|
||||
candidate.Value = l.Value
|
||||
return SPDXLicense{
|
||||
ID: generateLicenseID(l),
|
||||
LicenseName: l.Value,
|
||||
FullText: ft,
|
||||
URLs: l.URLs,
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
|
||||
// generateLicenseID generates a license ID for the given license, which is either the license value or the SPDX expression.
|
||||
func generateLicenseID(l pkg.License) string {
|
||||
if l.SPDXExpression != "" {
|
||||
return l.SPDXExpression
|
||||
}
|
||||
if l.Value != "" {
|
||||
return spdxlicense.LicenseRefPrefix + SanitizeElementID(l.Value)
|
||||
|
||||
// syft format includes the algo for the sha in the values
|
||||
// we can strip this and just make LicenseRef-<sum> for spdx consumption
|
||||
id := strings.ReplaceAll(l.Value, "sha256:", "")
|
||||
if !strings.HasPrefix(id, "LicenseRef-") {
|
||||
id = "LicenseRef-" + id
|
||||
}
|
||||
return licenseSum(l.Contents)
|
||||
return SanitizeElementID(id)
|
||||
}
|
||||
|
||||
func licenseSum(s string) string {
|
||||
if len(s) <= 64 {
|
||||
return spdxlicense.LicenseRefPrefix + SanitizeElementID(s)
|
||||
type SPDXOtherLicenseSet struct {
|
||||
set map[string]spdx.OtherLicense
|
||||
}
|
||||
|
||||
func NewSPDXOtherLicenseSet() *SPDXOtherLicenseSet {
|
||||
return &SPDXOtherLicenseSet{
|
||||
set: make(map[string]spdx.OtherLicense),
|
||||
}
|
||||
hash := sha256.Sum256([]byte(s))
|
||||
return fmt.Sprintf("%s%x", spdxlicense.LicenseRefPrefix, hash)
|
||||
}
|
||||
|
||||
func (s *SPDXOtherLicenseSet) Add(licenses ...spdx.OtherLicense) {
|
||||
for _, l := range licenses {
|
||||
s.set[l.LicenseIdentifier] = l
|
||||
}
|
||||
}
|
||||
|
||||
type ByLicenseIdentifier []spdx.OtherLicense
|
||||
|
||||
func (o ByLicenseIdentifier) Len() int { return len(o) }
|
||||
func (o ByLicenseIdentifier) Swap(i, j int) { o[i], o[j] = o[j], o[i] }
|
||||
func (o ByLicenseIdentifier) Less(i, j int) bool {
|
||||
return o[i].LicenseIdentifier < o[j].LicenseIdentifier
|
||||
}
|
||||
|
||||
func (s *SPDXOtherLicenseSet) ToSlice() []spdx.OtherLicense {
|
||||
values := make([]spdx.OtherLicense, 0, len(s.set))
|
||||
for _, v := range s.set {
|
||||
values = append(values, v)
|
||||
}
|
||||
sort.Sort(ByLicenseIdentifier(values))
|
||||
return values
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/internal/spdxlicense"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
)
|
||||
|
||||
func Test_License(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
type expected struct {
|
||||
concluded string
|
||||
declared string
|
||||
@@ -31,7 +33,7 @@ func Test_License(t *testing.T) {
|
||||
{
|
||||
name: "no SPDX licenses",
|
||||
input: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicense("made-up")),
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "made-up")),
|
||||
},
|
||||
expected: expected{
|
||||
concluded: "NOASSERTION",
|
||||
@@ -41,7 +43,7 @@ func Test_License(t *testing.T) {
|
||||
{
|
||||
name: "with SPDX license",
|
||||
input: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicense("MIT")),
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "MIT")),
|
||||
},
|
||||
expected: struct {
|
||||
concluded string
|
||||
@@ -55,8 +57,8 @@ func Test_License(t *testing.T) {
|
||||
name: "with SPDX license expression",
|
||||
input: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("MIT"),
|
||||
pkg.NewLicense("GPL-3.0-only"),
|
||||
pkg.NewLicenseWithContext(ctx, "MIT"),
|
||||
pkg.NewLicenseWithContext(ctx, "GPL-3.0-only"),
|
||||
),
|
||||
},
|
||||
expected: expected{
|
||||
@@ -69,9 +71,9 @@ func Test_License(t *testing.T) {
|
||||
name: "includes valid LicenseRef-",
|
||||
input: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("one thing first"),
|
||||
pkg.NewLicense("two things/#$^second"),
|
||||
pkg.NewLicense("MIT"),
|
||||
pkg.NewLicenseWithContext(ctx, "one thing first"),
|
||||
pkg.NewLicenseWithContext(ctx, "two things/#$^second"),
|
||||
pkg.NewLicenseWithContext(ctx, "MIT"),
|
||||
),
|
||||
},
|
||||
expected: expected{
|
||||
@@ -84,9 +86,9 @@ func Test_License(t *testing.T) {
|
||||
name: "join parentheses correctly",
|
||||
input: pkg.Package{
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("one thing first"),
|
||||
pkg.NewLicense("MIT AND GPL-3.0-only"),
|
||||
pkg.NewLicense("MIT OR APACHE-2.0"),
|
||||
pkg.NewLicenseWithContext(ctx, "one thing first"),
|
||||
pkg.NewLicenseWithContext(ctx, "MIT AND GPL-3.0-only"),
|
||||
pkg.NewLicenseWithContext(ctx, "MIT OR APACHE-2.0"),
|
||||
),
|
||||
},
|
||||
expected: expected{
|
||||
@@ -98,7 +100,7 @@ func Test_License(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c, d := License(test.input)
|
||||
c, d, _ := License(test.input)
|
||||
assert.Equal(t, test.expected.concluded, c)
|
||||
assert.Equal(t, test.expected.declared, d)
|
||||
})
|
||||
@@ -123,11 +125,13 @@ func TestGenerateLicenseID(t *testing.T) {
|
||||
{
|
||||
name: "Uses value if no SPDX expression",
|
||||
license: pkg.License{
|
||||
Value: "MIT",
|
||||
Value: "my-sweet-custom-license",
|
||||
},
|
||||
expected: spdxlicense.LicenseRefPrefix + "MIT",
|
||||
expected: spdxlicense.LicenseRefPrefix + "my-sweet-custom-license",
|
||||
},
|
||||
{
|
||||
// note: this is an oversight of the SPDX spec. It does NOT allow "+" in the ID even though they are
|
||||
// significant to the licenses in the expressions below
|
||||
name: "Long value is sanitized correctly",
|
||||
license: pkg.License{
|
||||
Value: "LGPLv2+ and LGPLv2+ with exceptions and GPLv2+ and GPLv2+ with exceptions and BSD and Inner-Net and ISC and Public Domain and GFDL",
|
||||
@@ -135,13 +139,6 @@ func TestGenerateLicenseID(t *testing.T) {
|
||||
expected: spdxlicense.LicenseRefPrefix +
|
||||
"LGPLv2--and-LGPLv2--with-exceptions-and-GPLv2--and-GPLv2--with-exceptions-and-BSD-and-Inner-Net-and-ISC-and-Public-Domain-and-GFDL",
|
||||
},
|
||||
{
|
||||
name: "Uses hash of contents when nothing else is provided",
|
||||
license: pkg.License{
|
||||
Contents: "This is a very long custom license text that should be hashed because it's more than 64 characters long.",
|
||||
},
|
||||
expected: "", // We'll verify it starts with the correct prefix
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -160,34 +157,92 @@ func TestGenerateLicenseID(t *testing.T) {
|
||||
func Test_joinLicenses(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
args []SPDXLicense
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "multiple licenses",
|
||||
args: []string{"MIT", "GPL-3.0-only"},
|
||||
args: []SPDXLicense{{ID: "MIT"}, {ID: "GPL-3.0-only"}},
|
||||
want: "MIT AND GPL-3.0-only",
|
||||
},
|
||||
{
|
||||
name: "multiple licenses with complex expressions",
|
||||
args: []string{"MIT AND Apache", "GPL-3.0-only"},
|
||||
args: []SPDXLicense{{ID: "MIT AND Apache"}, {ID: "GPL-3.0-only"}},
|
||||
want: "(MIT AND Apache) AND GPL-3.0-only",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equalf(t, tt.want, joinLicenses(toSpdxLicenses(tt.args)), "joinLicenses(%v)", tt.args)
|
||||
assert.Equalf(t, tt.want, joinLicenses(tt.args), "joinLicenses(%v)", tt.args)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func toSpdxLicenses(ids []string) (licenses []SPDXLicense) {
|
||||
for _, l := range ids {
|
||||
license := SPDXLicense{ID: l}
|
||||
if strings.HasPrefix(l, spdxlicense.LicenseRefPrefix) {
|
||||
license.Value = l
|
||||
}
|
||||
licenses = append(licenses, license)
|
||||
func TestCreateSPDXLicenseAndGenerateLicenseID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.License
|
||||
expected SPDXLicense
|
||||
}{
|
||||
{
|
||||
name: "SPDX expression used as ID",
|
||||
input: pkg.License{
|
||||
SPDXExpression: "MIT",
|
||||
Value: "MIT",
|
||||
Contents: "",
|
||||
},
|
||||
expected: SPDXLicense{
|
||||
ID: "MIT",
|
||||
LicenseName: "MIT",
|
||||
FullText: "NOASSERTION",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LicenseRef with contents",
|
||||
input: pkg.License{
|
||||
Value: "sha256:123abc",
|
||||
Contents: "license contents here",
|
||||
},
|
||||
expected: SPDXLicense{
|
||||
ID: "LicenseRef-123abc",
|
||||
LicenseName: "sha256:123abc",
|
||||
FullText: "license contents here",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LicenseRef without contents",
|
||||
input: pkg.License{
|
||||
Value: "custom-license",
|
||||
Contents: "",
|
||||
},
|
||||
expected: SPDXLicense{
|
||||
ID: "LicenseRef-custom-license",
|
||||
LicenseName: "custom-license",
|
||||
FullText: "NOASSERTION",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URL is passed through",
|
||||
input: pkg.License{
|
||||
SPDXExpression: "MIT",
|
||||
URLs: []string{
|
||||
"https://example.com/license",
|
||||
},
|
||||
},
|
||||
expected: SPDXLicense{
|
||||
ID: "MIT",
|
||||
FullText: "NOASSERTION",
|
||||
URLs: []string{"https://example.com/license"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
license := createSPDXLicense(tt.input)
|
||||
if d := cmp.Diff(tt.expected, license); d != "" {
|
||||
t.Errorf("createSPDXLicense() mismatch (-want +got):\n%s", d)
|
||||
}
|
||||
})
|
||||
}
|
||||
return licenses
|
||||
}
|
||||
|
||||
@@ -50,6 +50,14 @@ func Originator(p pkg.Package) (typ string, author string) { //nolint: gocyclo,f
|
||||
case pkg.DotnetPortableExecutableEntry:
|
||||
typ = orgType
|
||||
author = metadata.CompanyName
|
||||
case pkg.PEBinary:
|
||||
// this is a known common keyword used in version resources
|
||||
// for more info see: https://learn.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource
|
||||
val, ok := metadata.VersionResources.Get("CompanyName")
|
||||
if ok {
|
||||
typ = orgType
|
||||
author = val
|
||||
}
|
||||
|
||||
case pkg.DpkgDBEntry:
|
||||
author = metadata.Maintainer
|
||||
@@ -127,6 +135,7 @@ func Originator(p pkg.Package) (typ string, author string) { //nolint: gocyclo,f
|
||||
// it seems that the vast majority of the time the author is an org, not a person
|
||||
typ = orgType
|
||||
author = metadata.Author
|
||||
|
||||
case pkg.SwiplPackEntry:
|
||||
author = formatPersonOrOrg(metadata.Author, metadata.AuthorEmail)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ func Test_OriginatorSupplier(t *testing.T) {
|
||||
pkg.ConanfileEntry{},
|
||||
pkg.ConaninfoEntry{},
|
||||
pkg.DartPubspecLockEntry{},
|
||||
pkg.DartPubspec{},
|
||||
pkg.DotnetDepsEntry{},
|
||||
pkg.DotnetPackagesLockEntry{},
|
||||
pkg.ELFBinaryPackageNoteJSONPayload{},
|
||||
@@ -26,6 +27,7 @@ func Test_OriginatorSupplier(t *testing.T) {
|
||||
pkg.ErlangRebarLockEntry{},
|
||||
pkg.GolangBinaryBuildinfoEntry{},
|
||||
pkg.GolangModuleEntry{},
|
||||
pkg.HomebrewFormula{},
|
||||
pkg.HackageStackYamlLockEntry{},
|
||||
pkg.HackageStackYamlEntry{},
|
||||
pkg.LinuxKernel{},
|
||||
@@ -110,6 +112,21 @@ func Test_OriginatorSupplier(t *testing.T) {
|
||||
originator: "Organization: Microsoft Corporation",
|
||||
supplier: "Organization: Microsoft Corporation",
|
||||
},
|
||||
{
|
||||
name: "from PE binary",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.PEBinary{
|
||||
VersionResources: pkg.KeyValues{
|
||||
{
|
||||
Key: "CompanyName",
|
||||
Value: "Microsoft Corporation",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
originator: "Organization: Microsoft Corporation",
|
||||
supplier: "Organization: Microsoft Corporation",
|
||||
},
|
||||
{
|
||||
name: "from dpkg DB",
|
||||
input: pkg.Package{
|
||||
|
||||
@@ -76,6 +76,8 @@ func SourceInfo(p pkg.Package) string {
|
||||
answer = "acquired package info from GitHub Actions workflow file or composite action file"
|
||||
case pkg.WordpressPluginPkg:
|
||||
answer = "acquired package info from found wordpress plugin PHP source files"
|
||||
case pkg.HomebrewPkg:
|
||||
answer = "acquired package info from Homebrew formula"
|
||||
case pkg.TerraformPkg:
|
||||
answer = "acquired package info from Terraform dependency lock file"
|
||||
default:
|
||||
|
||||
@@ -327,6 +327,14 @@ func Test_SourceInfo(t *testing.T) {
|
||||
"acquired package info from found wordpress plugin PHP source files",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.HomebrewPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"acquired package info from Homebrew formula",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.TerraformPkg,
|
||||
|
||||
@@ -6,13 +6,18 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// SeekableReader takes an io.Reader and returns an io.ReadSeeker relative to the current position of the reader.
|
||||
// Users of this function expect to be able to reset the reader to the current position, not potentially reset the
|
||||
// reader prior to the location when this reader is provided. An example is a reader with multiple JSON
|
||||
// documents separated by newlines (JSONL). After reading the first document, if a call is made to decode
|
||||
// the second and Seek(0, SeekStart) is called it would reset the overall reader back to the first document.
|
||||
func SeekableReader(reader io.Reader) (io.ReadSeeker, error) {
|
||||
if reader == nil {
|
||||
return nil, fmt.Errorf("no bytes provided")
|
||||
}
|
||||
|
||||
if r, ok := reader.(io.ReadSeeker); ok {
|
||||
return r, nil
|
||||
return getOffsetReadSeeker(r)
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(reader)
|
||||
@@ -22,3 +27,53 @@ func SeekableReader(reader io.Reader) (io.ReadSeeker, error) {
|
||||
|
||||
return bytes.NewReader(content), nil
|
||||
}
|
||||
|
||||
type offsetReadSeeker struct {
|
||||
rdr io.ReadSeeker
|
||||
offset int64
|
||||
}
|
||||
|
||||
// getOffsetReadSeeker returns a new io.ReadSeeker that may wrap another io.ReadSeeker with the current offset, so
|
||||
// seek calls will be relative to the _current_ position, rather than relative to the reader itself
|
||||
func getOffsetReadSeeker(r io.ReadSeeker) (io.ReadSeeker, error) {
|
||||
if r == nil {
|
||||
return nil, fmt.Errorf("no reader provided")
|
||||
}
|
||||
pos, err := r.Seek(0, io.SeekCurrent)
|
||||
if pos == 0 {
|
||||
// if the ReadSeeker is currently at 0, we don't need to track an offset
|
||||
return r, nil
|
||||
}
|
||||
return &offsetReadSeeker{
|
||||
rdr: r,
|
||||
offset: pos,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (o *offsetReadSeeker) Read(p []byte) (n int, err error) {
|
||||
return o.rdr.Read(p)
|
||||
}
|
||||
|
||||
func (o *offsetReadSeeker) Seek(offset int64, whence int) (int64, error) {
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
if offset < 0 {
|
||||
return 0, fmt.Errorf("cannot seek < 0")
|
||||
}
|
||||
newOffset, err := o.rdr.Seek(o.offset+offset, io.SeekStart)
|
||||
return newOffset - o.offset, err
|
||||
case io.SeekCurrent:
|
||||
currentOffset, err := o.rdr.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot seek current: %w", err)
|
||||
}
|
||||
if currentOffset-o.offset+offset < 0 {
|
||||
return 0, fmt.Errorf("cannot seek < 0")
|
||||
}
|
||||
newOffset, err := o.rdr.Seek(offset, io.SeekCurrent)
|
||||
return newOffset - o.offset, err
|
||||
}
|
||||
return 0, fmt.Errorf("only SeekStart and SeekCurrent supported")
|
||||
}
|
||||
|
||||
var _ io.ReadSeeker = (*offsetReadSeeker)(nil)
|
||||
|
||||
@@ -3,10 +3,9 @@ package stream
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -26,7 +25,7 @@ func TestSeekableReader(t *testing.T) {
|
||||
name: "empty reader",
|
||||
input: bytes.NewBuffer([]byte{}), // does not implement io.Seeker (but does implement io.Reader)
|
||||
assert: func(input io.Reader, got io.ReadSeeker) {
|
||||
impl, ok := got.(*bytes.Reader) // implements bytes.Reader
|
||||
impl, ok := got.(*bytes.Reader) // contents are copied to a byte slice, accessed via bytes.Reader
|
||||
require.True(t, ok)
|
||||
_, err := impl.Seek(0, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
@@ -37,38 +36,28 @@ func TestSeekableReader(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "empty read seeker",
|
||||
input: bytes.NewReader([]byte{}), // implements io.ReadSeeker
|
||||
input: strings.NewReader(""), // implements io.ReadSeeker, not offset
|
||||
assert: func(input io.Reader, got io.ReadSeeker) {
|
||||
impl, ok := got.(*bytes.Reader)
|
||||
_, ok := got.(*strings.Reader) // same ReadSeeker is returned when not offset
|
||||
require.True(t, ok)
|
||||
_, err := impl.Seek(0, io.SeekStart)
|
||||
_, err := got.Seek(0, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
content, err := io.ReadAll(impl)
|
||||
content, err := io.ReadAll(got)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte{}, content)
|
||||
|
||||
// assert this is the same read seeker (reflect tt.input pointer is the same as the impl pointer
|
||||
inputImpl, ok := input.(*bytes.Reader)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, reflect.ValueOf(inputImpl).Pointer(), reflect.ValueOf(impl).Pointer())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-empty read seeker",
|
||||
input: bytes.NewReader([]byte("hello world!")), // implements io.ReadSeeker
|
||||
input: strings.NewReader("hello world!"), // implements io.ReadSeeker, not offset
|
||||
assert: func(input io.Reader, got io.ReadSeeker) {
|
||||
impl, ok := got.(*bytes.Reader)
|
||||
_, ok := got.(*strings.Reader) // same ReadSeeker is returned when not offset
|
||||
require.True(t, ok)
|
||||
_, err := impl.Seek(0, io.SeekStart)
|
||||
_, err := got.Seek(0, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
content, err := io.ReadAll(impl)
|
||||
content, err := io.ReadAll(got)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("hello world!"), content)
|
||||
|
||||
// assert this is the same read seeker (reflect tt.input pointer is the same as the impl pointer
|
||||
inputImpl, ok := input.(*bytes.Reader)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, reflect.ValueOf(inputImpl).Pointer(), reflect.ValueOf(impl).Pointer())
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -84,6 +73,32 @@ func TestSeekableReader(t *testing.T) {
|
||||
require.Equal(t, []byte("hello world!"), content)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "position zero read seeker",
|
||||
input: strings.NewReader("a string reader"), // implements io.ReadSeeker at position 0
|
||||
assert: func(input io.Reader, got io.ReadSeeker) {
|
||||
_, ok := got.(*strings.Reader) // returns the same ReadSeeker
|
||||
require.True(t, ok)
|
||||
_, err := got.Seek(0, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
content, err := io.ReadAll(got)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("a string reader"), content)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "offset read seeker",
|
||||
input: moveOffset(t, bytes.NewReader([]byte{1, 2, 3, 4, 5}), 3), // implements io.ReadSeeker, with an offset
|
||||
assert: func(input io.Reader, got io.ReadSeeker) {
|
||||
_, ok := got.(*offsetReadSeeker) // returns an offset-tracking ReadSeeker
|
||||
require.True(t, ok)
|
||||
_, err := got.Seek(0, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
content, err := io.ReadAll(got)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte{4, 5}, content)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -99,3 +114,113 @@ func TestSeekableReader(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_offsetReadSeeker(t *testing.T) {
|
||||
abcd1234 := func() io.ReadSeeker { return strings.NewReader("abcd1234") }
|
||||
abcd1234offset := func(offset int) func() io.ReadSeeker {
|
||||
return func() io.ReadSeeker {
|
||||
r := strings.NewReader("abcd1234")
|
||||
_, err := r.Seek(int64(offset), io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input func() io.ReadSeeker
|
||||
seek int64
|
||||
seek2 int64
|
||||
whence int
|
||||
expected string
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "basic reader",
|
||||
input: abcd1234,
|
||||
seek: 0,
|
||||
whence: io.SeekStart,
|
||||
expected: "abcd1234",
|
||||
},
|
||||
{
|
||||
name: "basic reader offset",
|
||||
input: abcd1234offset(1),
|
||||
seek: 0,
|
||||
whence: io.SeekStart,
|
||||
expected: "bcd1234",
|
||||
},
|
||||
{
|
||||
name: "basic reader offset both",
|
||||
input: abcd1234offset(2),
|
||||
seek: 2,
|
||||
whence: io.SeekStart,
|
||||
expected: "1234",
|
||||
},
|
||||
{
|
||||
name: "basic reader offset seek current",
|
||||
input: abcd1234offset(1),
|
||||
seek: -1,
|
||||
whence: io.SeekCurrent,
|
||||
wantErr: require.Error, // would be < current, which is an error
|
||||
},
|
||||
{
|
||||
name: "valid negative offset from current",
|
||||
input: abcd1234offset(1),
|
||||
seek: 2,
|
||||
seek2: -1,
|
||||
whence: io.SeekCurrent,
|
||||
expected: "cd1234",
|
||||
},
|
||||
{
|
||||
name: "basic reader offset multiple",
|
||||
input: abcd1234offset(2),
|
||||
seek: 3,
|
||||
seek2: 2,
|
||||
whence: io.SeekCurrent,
|
||||
expected: "4",
|
||||
},
|
||||
{
|
||||
name: "bad whence",
|
||||
input: abcd1234,
|
||||
seek: 1,
|
||||
whence: io.SeekEnd,
|
||||
wantErr: require.Error,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rdr := tt.input()
|
||||
|
||||
off, err := rdr.Seek(0, io.SeekCurrent)
|
||||
require.NoError(t, err)
|
||||
|
||||
// construct new offsetReadSeeker
|
||||
sr := offsetReadSeeker{rdr: rdr, offset: off}
|
||||
|
||||
_, err = sr.Seek(tt.seek, tt.whence)
|
||||
if tt.seek2 != 0 {
|
||||
require.NoError(t, err)
|
||||
_, err = sr.Seek(tt.seek2, tt.whence)
|
||||
}
|
||||
if tt.wantErr != nil {
|
||||
tt.wantErr(t, err)
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, err := sr.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expected, string(buf[:n]))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func moveOffset(t *testing.T, reader io.ReadSeeker, offset int64) io.Reader {
|
||||
pos, err := reader.Seek(offset, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, offset, pos)
|
||||
return reader
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -98,7 +99,7 @@ func DirectoryInputWithAuthorField(t testing.TB) sbom.SBOM {
|
||||
|
||||
func newDirectoryCatalog() *pkg.Collection {
|
||||
catalog := pkg.NewCollection()
|
||||
|
||||
ctx := context.TODO()
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-1",
|
||||
@@ -110,7 +111,7 @@ func newDirectoryCatalog() *pkg.Collection {
|
||||
),
|
||||
Language: pkg.Python,
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("MIT"),
|
||||
pkg.NewLicenseWithContext(ctx, "MIT"),
|
||||
),
|
||||
Metadata: pkg.PythonPackage{
|
||||
Name: "package-1",
|
||||
@@ -149,7 +150,7 @@ func newDirectoryCatalog() *pkg.Collection {
|
||||
|
||||
func newDirectoryCatalogWithAuthorField() *pkg.Collection {
|
||||
catalog := pkg.NewCollection()
|
||||
|
||||
ctx := context.TODO()
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-1",
|
||||
@@ -161,7 +162,7 @@ func newDirectoryCatalogWithAuthorField() *pkg.Collection {
|
||||
),
|
||||
Language: pkg.Python,
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("MIT"),
|
||||
pkg.NewLicenseWithContext(ctx, "MIT"),
|
||||
),
|
||||
Metadata: pkg.PythonPackage{
|
||||
Name: "package-1",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -98,7 +99,7 @@ func populateImageCatalog(catalog *pkg.Collection, img *image.Image) {
|
||||
// TODO: this helper function is coupled to the image-simple fixture, which seems like a bad idea
|
||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||
|
||||
ctx := context.TODO()
|
||||
// populate catalog with test data
|
||||
if ref1 != nil {
|
||||
catalog.Add(pkg.Package{
|
||||
@@ -111,7 +112,7 @@ func populateImageCatalog(catalog *pkg.Collection, img *image.Image) {
|
||||
FoundBy: "the-cataloger-1",
|
||||
Language: pkg.Python,
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicense("MIT"),
|
||||
pkg.NewLicenseWithContext(ctx, "MIT"),
|
||||
),
|
||||
Metadata: pkg.PythonPackage{
|
||||
Name: "package-1",
|
||||
|
||||
@@ -28,6 +28,7 @@ func (r RedactorFn) Redact(b []byte) []byte {
|
||||
|
||||
type PatternReplacement struct {
|
||||
Search *regexp.Regexp
|
||||
Groups []string
|
||||
Replace string
|
||||
}
|
||||
|
||||
@@ -39,7 +40,67 @@ func NewPatternReplacement(r *regexp.Regexp) PatternReplacement {
|
||||
}
|
||||
|
||||
func (p PatternReplacement) Redact(b []byte) []byte {
|
||||
return p.Search.ReplaceAll(b, []byte(p.Replace))
|
||||
if len(p.Groups) == 0 {
|
||||
return p.Search.ReplaceAll(b, []byte(p.Replace))
|
||||
}
|
||||
|
||||
return p.redactNamedGroups(b)
|
||||
}
|
||||
|
||||
func (p PatternReplacement) redactNamedGroups(b []byte) []byte {
|
||||
groupsToReplace := make(map[string]bool)
|
||||
for _, g := range p.Groups {
|
||||
groupsToReplace[g] = true
|
||||
}
|
||||
|
||||
subexpNames := p.Search.SubexpNames()
|
||||
|
||||
return p.Search.ReplaceAllFunc(b, func(match []byte) []byte {
|
||||
indexes := p.Search.FindSubmatchIndex(match)
|
||||
if indexes == nil {
|
||||
return match
|
||||
}
|
||||
|
||||
result := make([]byte, len(match))
|
||||
copy(result, match)
|
||||
|
||||
// keep track of the offset as we replace groups
|
||||
offset := 0
|
||||
|
||||
// process each named group
|
||||
for i, name := range subexpNames {
|
||||
// skip the full match (i==0) and groups we don't want to replace
|
||||
if i == 0 || !groupsToReplace[name] {
|
||||
continue
|
||||
}
|
||||
|
||||
// get the start and end positions of this group
|
||||
startPos := indexes[2*i]
|
||||
endPos := indexes[2*i+1]
|
||||
|
||||
// skip if the group didn't match
|
||||
if startPos < 0 || endPos < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// adjust positions based on previous replacements
|
||||
startPos += offset
|
||||
endPos += offset
|
||||
|
||||
// replace the group with our replacement text
|
||||
beforeGroup := result[:startPos]
|
||||
afterGroup := result[endPos:]
|
||||
|
||||
// calculate the new offset
|
||||
oldLen := endPos - startPos
|
||||
newLen := len(p.Replace)
|
||||
offset += (newLen - oldLen)
|
||||
|
||||
result = append(beforeGroup, append([]byte(p.Replace), afterGroup...)...) //nolint:gocritic
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
// Replace by value //////////////////////////////
|
||||
@@ -86,6 +147,13 @@ func (r *Redactions) WithPatternRedactors(values map[string]string) *Redactions
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Redactions) WithPatternRedactorSpec(values ...PatternReplacement) *Redactions {
|
||||
for _, v := range values {
|
||||
r.redactors = append(r.redactors, v)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Redactions) WithValueRedactors(values map[string]string) *Redactions {
|
||||
for k, v := range values {
|
||||
r.redactors = append(r.redactors,
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package purls
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/format/internal"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
var _ sbom.FormatDecoder = (*decoder)(nil)
|
||||
|
||||
type decoder struct{}
|
||||
|
||||
func NewFormatDecoder() sbom.FormatDecoder {
|
||||
return decoder{}
|
||||
}
|
||||
|
||||
func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error) {
|
||||
if r == nil {
|
||||
return nil, "", "", fmt.Errorf("no reader provided")
|
||||
}
|
||||
s, err := toSyftModel(r)
|
||||
return s, ID, version, err
|
||||
}
|
||||
|
||||
func (d decoder) Identify(r io.Reader) (sbom.FormatID, string) {
|
||||
if r == nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
// skip whitespace only lines
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "pkg:") {
|
||||
_, err := packageurl.FromString(line)
|
||||
if err != nil {
|
||||
log.WithFields("error", err, "line", line).Debug("unable to parse purl")
|
||||
continue
|
||||
}
|
||||
return ID, version
|
||||
}
|
||||
// not a purl, so we can't identify the format as a list of purls
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func toSyftModel(r io.Reader) (*sbom.SBOM, error) {
|
||||
var errs []error
|
||||
pkgs := pkg.NewCollection()
|
||||
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip invalid PURLs
|
||||
_, err := packageurl.FromString(line)
|
||||
if err != nil {
|
||||
log.WithFields("error", err, "line", line).Debug("unable to parse purl")
|
||||
continue
|
||||
}
|
||||
p := pkg.Package{
|
||||
// name, version and other properties set during Backfill
|
||||
PURL: line,
|
||||
}
|
||||
|
||||
internal.Backfill(&p)
|
||||
p.SetID()
|
||||
pkgs.Add(p)
|
||||
}
|
||||
|
||||
return &sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
Packages: pkgs,
|
||||
},
|
||||
}, errors.Join(errs...)
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package purls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/internal/cmptest"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func TestDecoder_Decode(t *testing.T) {
|
||||
tests := []struct {
|
||||
purl string
|
||||
expected []pkg.Package
|
||||
}{
|
||||
{
|
||||
purl: "pkg:generic/some-package@1.2.3",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "some-package",
|
||||
Type: pkg.UnknownPkg,
|
||||
Version: "1.2.3",
|
||||
PURL: "pkg:generic/some-package@1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
purl: "pkg:npm/some-package@1.2.3",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "some-package",
|
||||
Type: pkg.NpmPkg,
|
||||
Language: pkg.JavaScript,
|
||||
Version: "1.2.3",
|
||||
PURL: "pkg:npm/some-package@1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
purl: "pkg:apk/curl@7.61.1",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "curl",
|
||||
Version: "7.61.1",
|
||||
Type: pkg.ApkPkg,
|
||||
PURL: "pkg:apk/curl@7.61.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
purl: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "sysv-rc",
|
||||
Version: "2.88dsf-59",
|
||||
Type: pkg.DebPkg,
|
||||
PURL: "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
purl: "pkg:apk/libcrypto3@3.3.2?upstream=openssl",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "libcrypto3",
|
||||
Version: "3.3.2",
|
||||
Type: pkg.ApkPkg,
|
||||
PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
purl: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1", // %40 is @
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "libcrypto3",
|
||||
Version: "3.3.2",
|
||||
Type: pkg.ApkPkg,
|
||||
PURL: "pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
purl: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "systemd-x",
|
||||
Version: "239-82.el8_10.2",
|
||||
Type: pkg.RpmPkg,
|
||||
PURL: "pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
purl: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "dbus-common",
|
||||
Version: "1:1.12.8-26.el8",
|
||||
Type: pkg.RpmPkg,
|
||||
PURL: "pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
purl: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "curl",
|
||||
Version: "7.61.1",
|
||||
Type: pkg.ApkPkg,
|
||||
PURL: "pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
purl: "pkg:golang/k8s.io/ingress-nginx@v1.11.2",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "k8s.io/ingress-nginx",
|
||||
Version: "v1.11.2",
|
||||
Type: pkg.GoModulePkg,
|
||||
Language: pkg.Go,
|
||||
PURL: "pkg:golang/k8s.io/ingress-nginx@v1.11.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
purl: "pkg:golang/github.com/wazuh/wazuh@v4.5.0",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "github.com/wazuh/wazuh",
|
||||
Version: "v4.5.0",
|
||||
Type: pkg.GoModulePkg,
|
||||
PURL: "pkg:golang/github.com/wazuh/wazuh@v4.5.0",
|
||||
Language: pkg.Go,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
purl: "pkg:golang/wazuh@v4.5.0",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "wazuh",
|
||||
Version: "v4.5.0",
|
||||
Type: pkg.GoModulePkg,
|
||||
PURL: "pkg:golang/wazuh@v4.5.0",
|
||||
Language: pkg.Go,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
purl: "pkg:maven/org.apache/some-pkg@4.11.3",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "some-pkg",
|
||||
Version: "4.11.3",
|
||||
Type: pkg.JavaPkg,
|
||||
PURL: "pkg:maven/org.apache/some-pkg@4.11.3",
|
||||
Language: pkg.Java,
|
||||
// we intentionally do not claim we found a pom properties file (don't derive this from the purl).
|
||||
// but we need a metadata allocated since all Java packages have a this metadata type (a consistency point)
|
||||
Metadata: pkg.JavaArchive{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.purl, func(t *testing.T) {
|
||||
dec := NewFormatDecoder()
|
||||
got, _, _, err := dec.Decode(strings.NewReader(test.purl))
|
||||
require.NoError(t, err)
|
||||
|
||||
if diff := cmp.Diff(test.expected, got.Artifacts.Packages.Sorted(), cmptest.DefaultOptions()...); diff != "" {
|
||||
t.Errorf("expected packages (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DecodeEncodeCycle(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
input: "pkg:generic/some-package@1.2.3",
|
||||
},
|
||||
{
|
||||
name: "multiple",
|
||||
input: "pkg:generic/pkg1\npkg:generic/pkg2\n\npkg:npm/@vercel/ncc@2.9.5",
|
||||
},
|
||||
{
|
||||
name: "java",
|
||||
input: "pkg:maven/org.apache/some-thing@4.11.3",
|
||||
},
|
||||
{
|
||||
name: "leading whitespace",
|
||||
input: " \n \t pkg:maven/org.apache/some-thing@4.11.3",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dec := NewFormatDecoder()
|
||||
decodedSBOM, _, _, err := dec.Decode(strings.NewReader(tt.input))
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
enc := NewFormatEncoder()
|
||||
require.NoError(t, enc.Encode(&buf, *decodedSBOM))
|
||||
|
||||
in := strings.TrimSpace(regexp.MustCompile(`\s+`).ReplaceAllString(strings.TrimSpace(tt.input), "\n"))
|
||||
expected := strings.Split(in, "\n")
|
||||
slices.Sort(expected)
|
||||
|
||||
got := strings.Split(strings.TrimSpace(buf.String()), "\n")
|
||||
slices.Sort(got)
|
||||
require.EqualValues(t, expected, got)
|
||||
|
||||
for _, item := range got {
|
||||
// require every result is a valid PURL -- no whitespace lines, etc.
|
||||
_, err = packageurl.FromString(item)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package purls
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
const ID sbom.FormatID = "purls"
|
||||
const version = "1"
|
||||
|
||||
type encoder struct {
|
||||
}
|
||||
|
||||
func NewFormatEncoder() sbom.FormatEncoder {
|
||||
return encoder{}
|
||||
}
|
||||
|
||||
func (e encoder) ID() sbom.FormatID {
|
||||
return ID
|
||||
}
|
||||
|
||||
func (e encoder) Aliases() []string {
|
||||
return []string{
|
||||
"purl",
|
||||
}
|
||||
}
|
||||
|
||||
func (e encoder) Version() string {
|
||||
return sbom.AnyVersion
|
||||
}
|
||||
|
||||
func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error {
|
||||
output := strset.New()
|
||||
for _, p := range s.Artifacts.Packages.Sorted() {
|
||||
purl := strings.TrimSpace(p.PURL)
|
||||
if purl == "" || output.Has(purl) {
|
||||
continue
|
||||
}
|
||||
// ensure syft doesn't output invalid PURLs in this format
|
||||
_, err := packageurl.FromString(purl)
|
||||
if err != nil {
|
||||
log.Debugf("invalid purl: %q", purl)
|
||||
continue
|
||||
}
|
||||
output.Add(purl)
|
||||
_, err = writer.Write([]byte(purl))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write([]byte("\n"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package purls
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/format/internal/testutil"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
var updateSnapshot = flag.Bool("update-purls", false, "update the *.golden files for purls format")
|
||||
|
||||
func Test_Encoder(t *testing.T) {
|
||||
pkgs := []pkg.Package{
|
||||
{
|
||||
Name: "npmtest",
|
||||
Version: "1.5.1",
|
||||
Type: pkg.NpmPkg,
|
||||
Language: pkg.JavaScript,
|
||||
PURL: "pkg:npm/npmtest@1.5.1",
|
||||
},
|
||||
{
|
||||
Name: "npmtest2",
|
||||
Version: "1.5.1",
|
||||
Type: pkg.NpmPkg,
|
||||
Language: pkg.JavaScript,
|
||||
PURL: "pkg:npm/npmtest@1.5.1", // duplicate should not be included
|
||||
},
|
||||
{
|
||||
Name: "npmtest",
|
||||
Version: "3.1.1",
|
||||
Type: pkg.NpmPkg,
|
||||
Language: pkg.JavaScript,
|
||||
PURL: "http://npm/npmtest@3.1.1", // invalid PURL should not be included
|
||||
},
|
||||
{
|
||||
Name: "javatest",
|
||||
Version: "0.30.1",
|
||||
Type: pkg.JavaPkg,
|
||||
Language: pkg.Java,
|
||||
PURL: "pkg:maven/org.apache/javatest@0.30.1",
|
||||
},
|
||||
{
|
||||
Type: pkg.UnknownPkg,
|
||||
PURL: "pkg:generic/generic@1.2.3",
|
||||
},
|
||||
}
|
||||
testutil.AssertEncoderAgainstGoldenSnapshot(t,
|
||||
testutil.EncoderSnapshotTestConfig{
|
||||
Subject: sbom.SBOM{Artifacts: sbom.Artifacts{
|
||||
Packages: pkg.NewCollection(pkgs...),
|
||||
}},
|
||||
Format: NewFormatEncoder(),
|
||||
UpdateSnapshot: *updateSnapshot,
|
||||
PersistRedactionsInSnapshot: true,
|
||||
IsJSON: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pkg:generic/generic@1.2.3
|
||||
pkg:maven/org.apache/javatest@0.30.1
|
||||
pkg:npm/npmtest@1.5.1
|
||||
@@ -2,6 +2,7 @@ package spdxjson
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -91,6 +92,8 @@ func TestDecoder_Decode(t *testing.T) {
|
||||
reader, err := os.Open(filepath.Join("test-fixtures", "spdx", test.name))
|
||||
require.NoError(t, err)
|
||||
|
||||
reset := func() { _, err = reader.Seek(0, io.SeekStart); require.NoError(t, err) }
|
||||
|
||||
dec := NewFormatDecoder()
|
||||
|
||||
formatID, formatVersion := dec.Identify(reader)
|
||||
@@ -98,6 +101,7 @@ func TestDecoder_Decode(t *testing.T) {
|
||||
assert.Equal(t, test.id, formatID)
|
||||
assert.Equal(t, test.version, formatVersion)
|
||||
|
||||
reset()
|
||||
_, decodeID, decodeVersion, err := dec.Decode(reader)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, test.id, decodeID)
|
||||
@@ -108,6 +112,7 @@ func TestDecoder_Decode(t *testing.T) {
|
||||
assert.Equal(t, test.id, formatID)
|
||||
assert.Equal(t, test.version, formatVersion)
|
||||
|
||||
reset()
|
||||
s, decodeID, decodeVersion, err := dec.Decode(reader)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user