mirror of
https://github.com/anchore/syft.git
synced 2026-10-20 01:57:38 +02:00
Compare commits
+6
-19
@@ -26,7 +26,7 @@ tools:
|
||||
# used for linting
|
||||
- name: golangci-lint
|
||||
version:
|
||||
want: v2.1.6
|
||||
want: v2.2.1
|
||||
method: github-release
|
||||
with:
|
||||
repo: golangci/golangci-lint
|
||||
@@ -34,7 +34,7 @@ tools:
|
||||
# used for showing the changelog at release
|
||||
- name: glow
|
||||
version:
|
||||
want: v2.1.0
|
||||
want: v2.1.1
|
||||
method: github-release
|
||||
with:
|
||||
repo: charmbracelet/glow
|
||||
@@ -42,7 +42,7 @@ tools:
|
||||
# used for signing the checksums file at release
|
||||
- name: cosign
|
||||
version:
|
||||
want: v2.5.0
|
||||
want: v2.5.2
|
||||
method: github-release
|
||||
with:
|
||||
repo: sigstore/cosign
|
||||
@@ -58,7 +58,7 @@ tools:
|
||||
# used to release all artifacts
|
||||
- name: goreleaser
|
||||
version:
|
||||
want: v2.9.0
|
||||
want: v2.10.2
|
||||
method: github-release
|
||||
with:
|
||||
repo: goreleaser/goreleaser
|
||||
@@ -87,23 +87,10 @@ tools:
|
||||
with:
|
||||
repo: wagoodman/go-bouncer
|
||||
|
||||
# used for showing benchmark testing
|
||||
- name: benchstat
|
||||
version:
|
||||
want: latest
|
||||
method: go-proxy
|
||||
with:
|
||||
module: golang.org/x/perf
|
||||
allow-unresolved-version: true
|
||||
method: go-install
|
||||
with:
|
||||
entrypoint: cmd/benchstat
|
||||
module: golang.org/x/perf
|
||||
|
||||
# used for running all local and CI tasks
|
||||
- name: task
|
||||
version:
|
||||
want: v3.43.3
|
||||
want: v3.44.0
|
||||
method: github-release
|
||||
with:
|
||||
repo: go-task/task
|
||||
@@ -111,7 +98,7 @@ tools:
|
||||
# used for triggering a release
|
||||
- name: gh
|
||||
version:
|
||||
want: v2.72.0
|
||||
want: v2.74.2
|
||||
method: github-release
|
||||
with:
|
||||
repo: cli/cli
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f #v3.28.18
|
||||
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b #v3.29.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f #v3.28.18
|
||||
uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b #v3.29.2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -70,4 +70,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f #v3.28.18
|
||||
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b #v3.29.2
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
- name: Delete existing comment
|
||||
if: ${{ hashFiles( env.CI_COMMENT_FILE ) == '' }}
|
||||
uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db #v2.9.2
|
||||
uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 #v2.9.3
|
||||
with:
|
||||
header: ${{ env.COMMENT_HEADER }}
|
||||
hide: true
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
|
||||
- name: Add comment
|
||||
if: ${{ hashFiles( env.CI_COMMENT_FILE ) != '' }}
|
||||
uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db #v2.9.2
|
||||
uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 #v2.9.3
|
||||
with:
|
||||
header: ${{ env.COMMENT_HEADER }}
|
||||
path: ${{ env.CI_COMMENT_FILE }}
|
||||
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
# for updating brew formula in anchore/homebrew-syft
|
||||
GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}
|
||||
|
||||
- uses: anchore/sbom-action@e11c554f704a0b820cbf8c51673f6945e0731532 #v0.20.0
|
||||
- uses: anchore/sbom-action@9246b90769f852b3a8921f330c59e0b3f439d6e9 #v0.20.1
|
||||
continue-on-error: true
|
||||
with:
|
||||
file: go.mod
|
||||
|
||||
@@ -76,6 +76,9 @@ jobs:
|
||||
- name: Build snapshot artifacts
|
||||
run: make snapshot
|
||||
|
||||
- name: Smoke test snapshot build
|
||||
run: make snapshot-smoke-test
|
||||
|
||||
# why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach).
|
||||
# see https://github.com/actions/upload-artifact/issues/199 for more info
|
||||
- name: Upload snapshot artifacts
|
||||
@@ -195,7 +198,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.8.2
|
||||
uses: sigstore/cosign-installer@v3.9.1
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
|
||||
|
||||
|
||||
+187
-119
@@ -1,11 +1,11 @@
|
||||
version: 2
|
||||
project_name: syft
|
||||
|
||||
release:
|
||||
prerelease: auto
|
||||
draft: false
|
||||
|
||||
env:
|
||||
# required to support multi architecture docker builds
|
||||
- DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
- CGO_ENABLED=0
|
||||
|
||||
@@ -13,14 +13,8 @@ builds:
|
||||
- id: linux-build
|
||||
dir: ./cmd/syft
|
||||
binary: syft
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- ppc64le
|
||||
- s390x
|
||||
# set the modified timestamp on the output binary to the git timestamp to ensure a reproducible build
|
||||
goos: [linux]
|
||||
goarch: [amd64, arm64, ppc64le, s390x]
|
||||
mod_timestamp: &build-timestamp '{{ .CommitTimestamp }}'
|
||||
ldflags: &build-ldflags |
|
||||
-w
|
||||
@@ -34,11 +28,8 @@ builds:
|
||||
- id: darwin-build
|
||||
dir: ./cmd/syft
|
||||
binary: syft
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
goos: [darwin]
|
||||
goarch: [amd64, arm64]
|
||||
mod_timestamp: *build-timestamp
|
||||
ldflags: *build-ldflags
|
||||
hooks:
|
||||
@@ -50,115 +41,42 @@ builds:
|
||||
- id: windows-build
|
||||
dir: ./cmd/syft
|
||||
binary: syft
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
goos: [windows]
|
||||
goarch: [amd64]
|
||||
mod_timestamp: *build-timestamp
|
||||
ldflags: *build-ldflags
|
||||
|
||||
archives:
|
||||
- id: linux-archives
|
||||
builds:
|
||||
- linux-build
|
||||
|
||||
# note: the signing process is depending on tar.gz archives. If this format changes then .github/scripts/apple-signing/*.sh will need to be adjusted
|
||||
builds: [linux-build]
|
||||
- id: darwin-archives
|
||||
builds:
|
||||
- darwin-build
|
||||
|
||||
builds: [darwin-build]
|
||||
- id: windows-archives
|
||||
format: zip
|
||||
builds:
|
||||
- windows-build
|
||||
builds: [windows-build]
|
||||
|
||||
nfpms:
|
||||
- license: "Apache 2.0"
|
||||
maintainer: "Anchore, Inc"
|
||||
homepage: &website "https://github.com/anchore/syft"
|
||||
description: &description "A tool that generates a Software Bill Of Materials (SBOM) from container images and filesystems"
|
||||
formats:
|
||||
- rpm
|
||||
- deb
|
||||
formats: [rpm, deb]
|
||||
|
||||
brews:
|
||||
- repository:
|
||||
owner: anchore
|
||||
name: homebrew-syft
|
||||
token: "{{.Env.GITHUB_BREW_TOKEN}}"
|
||||
ids:
|
||||
- darwin-archives
|
||||
- linux-archives
|
||||
ids: [darwin-archives, linux-archives]
|
||||
homepage: *website
|
||||
description: *description
|
||||
license: "Apache License 2.0"
|
||||
|
||||
dockers:
|
||||
# production images...
|
||||
- image_templates:
|
||||
- anchore/syft:debug
|
||||
- anchore/syft:{{.Tag}}-debug
|
||||
- ghcr.io/anchore/syft:debug
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug
|
||||
goarch: amd64
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:debug-arm64v8
|
||||
- anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
- ghcr.io/anchore/syft:debug-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
goarch: arm64
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64/v8"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:debug-ppc64le
|
||||
- anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
- ghcr.io/anchore/syft:debug-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
goarch: ppc64le
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/ppc64le"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:debug-s390x
|
||||
- anchore/syft:{{.Tag}}-debug-s390x
|
||||
- ghcr.io/anchore/syft:debug-s390x
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
|
||||
goarch: s390x
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/s390x"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:latest
|
||||
- anchore/syft:{{.Tag}}
|
||||
- ghcr.io/anchore/syft:latest
|
||||
- ghcr.io/anchore/syft:{{.Tag}}
|
||||
- anchore/syft:{{.Tag}}-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-amd64
|
||||
goarch: amd64
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
@@ -208,51 +126,201 @@ dockers:
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
# nonroot images...
|
||||
- image_templates:
|
||||
- anchore/syft:{{.Tag}}-nonroot-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64
|
||||
goarch: amd64
|
||||
dockerfile: Dockerfile.nonroot
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:{{.Tag}}-nonroot-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-arm64v8
|
||||
goarch: arm64
|
||||
dockerfile: Dockerfile.nonroot
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64/v8"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:{{.Tag}}-nonroot-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-ppc64le
|
||||
goarch: ppc64le
|
||||
dockerfile: Dockerfile.nonroot
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/ppc64le"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:{{.Tag}}-nonroot-s390x
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x
|
||||
goarch: s390x
|
||||
dockerfile: Dockerfile.nonroot
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/s390x"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
# debug images...
|
||||
- image_templates:
|
||||
- anchore/syft:{{.Tag}}-debug-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
|
||||
goarch: amd64
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
goarch: arm64
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64/v8"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
goarch: ppc64le
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/ppc64le"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
- image_templates:
|
||||
- anchore/syft:{{.Tag}}-debug-s390x
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
|
||||
goarch: s390x
|
||||
dockerfile: Dockerfile.debug
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/s390x"
|
||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||
|
||||
docker_manifests:
|
||||
- name_template: anchore/syft:latest
|
||||
image_templates:
|
||||
- anchore/syft:{{.Tag}}
|
||||
- anchore/syft:{{.Tag}}-arm64v8
|
||||
- anchore/syft:{{.Tag}}-ppc64le
|
||||
- anchore/syft:{{.Tag}}-s390x
|
||||
|
||||
- name_template: anchore/syft:debug
|
||||
- anchore/syft:{{.Tag}}-debug
|
||||
- anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
- anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
- anchore/syft:{{.Tag}}-debug-s390x
|
||||
|
||||
- name_template: anchore/syft:{{.Tag}}
|
||||
image_templates:
|
||||
- anchore/syft:{{.Tag}}
|
||||
- anchore/syft:{{.Tag}}-amd64
|
||||
- anchore/syft:{{.Tag}}-arm64v8
|
||||
- anchore/syft:{{.Tag}}-ppc64le
|
||||
- anchore/syft:{{.Tag}}-s390x
|
||||
|
||||
- name_template: ghcr.io/anchore/syft:latest
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:{{.Tag}}
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-s390x
|
||||
|
||||
- name_template: anchore/syft:{{.Tag}}
|
||||
image_templates:
|
||||
- anchore/syft:{{.Tag}}-amd64
|
||||
- anchore/syft:{{.Tag}}-arm64v8
|
||||
- anchore/syft:{{.Tag}}-ppc64le
|
||||
- anchore/syft:{{.Tag}}-s390x
|
||||
|
||||
- name_template: ghcr.io/anchore/syft:{{.Tag}}
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-s390x
|
||||
|
||||
# nonroot images...
|
||||
- name_template: anchore/syft:nonroot
|
||||
image_templates:
|
||||
- anchore/syft:{{.Tag}}-nonroot-amd64
|
||||
- anchore/syft:{{.Tag}}-nonroot-arm64v8
|
||||
- anchore/syft:{{.Tag}}-nonroot-ppc64le
|
||||
- anchore/syft:{{.Tag}}-nonroot-s390x
|
||||
|
||||
- name_template: ghcr.io/anchore/syft:nonroot
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x
|
||||
|
||||
- name_template: anchore/syft:{{.Tag}}-nonroot
|
||||
image_templates:
|
||||
- anchore/syft:{{.Tag}}-nonroot-amd64
|
||||
- anchore/syft:{{.Tag}}-nonroot-arm64v8
|
||||
- anchore/syft:{{.Tag}}-nonroot-ppc64le
|
||||
- anchore/syft:{{.Tag}}-nonroot-s390x
|
||||
|
||||
- name_template: ghcr.io/anchore/syft:{{.Tag}}-nonroot
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x
|
||||
|
||||
# debug images...
|
||||
- name_template: anchore/syft:debug
|
||||
image_templates:
|
||||
- anchore/syft:{{.Tag}}-debug-amd64
|
||||
- anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
- anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
- anchore/syft:{{.Tag}}-debug-s390x
|
||||
|
||||
- name_template: ghcr.io/anchore/syft:debug
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
|
||||
|
||||
- name_template: ghcr.io/anchore/syft:{{.Tag}}
|
||||
- name_template: anchore/syft:{{.Tag}}-debug
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:{{.Tag}}
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-s390x
|
||||
- anchore/syft:{{.Tag}}-debug-amd64
|
||||
- anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
- anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
- anchore/syft:{{.Tag}}-debug-s390x
|
||||
|
||||
- name_template: ghcr.io/anchore/syft:{{.Tag}}-debug
|
||||
image_templates:
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
|
||||
- ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
|
||||
|
||||
sboms:
|
||||
- artifacts: archive
|
||||
# this is relative to the snapshot/dist directory, not the root of the repo
|
||||
cmd: ../.tool/syft
|
||||
documents:
|
||||
- "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.sbom"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM gcr.io/distroless/static-debian12:debug@sha256:5c474275684bbf271ec40502ab50158b2f9826de5877d8feec27e22e8d6ee3d2 AS build
|
||||
FROM gcr.io/distroless/static-debian12:latest AS build
|
||||
|
||||
FROM scratch
|
||||
# needed for version check HTTPS request
|
||||
|
||||
+3
-1
@@ -1,10 +1,12 @@
|
||||
FROM gcr.io/distroless/static-debian12:debug@sha256:5c474275684bbf271ec40502ab50158b2f9826de5877d8feec27e22e8d6ee3d2
|
||||
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
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
|
||||
# create the /tmp dir, which is needed for image content cache
|
||||
WORKDIR /tmp
|
||||
|
||||
COPY syft /
|
||||
|
||||
USER nonroot
|
||||
|
||||
ARG BUILD_DATE
|
||||
ARG BUILD_VERSION
|
||||
ARG VCS_REF
|
||||
ARG VCS_URL
|
||||
|
||||
LABEL org.opencontainers.image.created=$BUILD_DATE
|
||||
LABEL org.opencontainers.image.title="syft"
|
||||
LABEL org.opencontainers.image.description="CLI tool and library for generating a Software Bill of Materials from container images and filesystems"
|
||||
LABEL org.opencontainers.image.source=$VCS_URL
|
||||
LABEL org.opencontainers.image.revision=$VCS_REF
|
||||
LABEL org.opencontainers.image.vendor="Anchore, Inc."
|
||||
LABEL org.opencontainers.image.version=$BUILD_VERSION
|
||||
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
||||
LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/syft/main/README.md"
|
||||
LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png"
|
||||
LABEL io.artifacthub.package.license="Apache-2.0"
|
||||
|
||||
ENTRYPOINT ["/syft"]
|
||||
+37
-46
@@ -27,10 +27,12 @@ vars:
|
||||
sh: uname -s | tr '[:upper:]' '[:lower:]'
|
||||
ARCH:
|
||||
sh: |
|
||||
[ "$(uname -m)" = "x86_64" ] && echo "amd64_v1" || { [ "$(uname -m)" = "aarch64" ] && echo "arm64_v8.0" || echo $(uname -m); }
|
||||
[ "$(uname -m)" = "x86_64" ] && echo "amd64_v1" || { [ "$(uname -m)" = "aarch64" ] && echo "arm64_v8.0" || [ "$(uname -m)" = "arm64" ] && echo "arm64_v8.0" || echo $(uname -m); }
|
||||
PROJECT_ROOT:
|
||||
sh: echo $PWD
|
||||
|
||||
# note: the snapshot dir must be a relative path starting with ./
|
||||
# e.g. when installing snapshot debs from a local path, ./ forces the deb to be installed in the current working directory instead of referencing a package name
|
||||
SNAPSHOT_DIR: ./snapshot
|
||||
SNAPSHOT_BIN: "{{ .PROJECT_ROOT }}/{{ .SNAPSHOT_DIR }}/{{ .OS }}-build_{{ .OS }}_{{ .ARCH }}/{{ .PROJECT }}"
|
||||
SNAPSHOT_CMD: "{{ .TOOL_DIR }}/goreleaser release --config {{ .TMP_DIR }}/goreleaser.yaml --clean --snapshot --skip=publish --skip=sign"
|
||||
@@ -76,6 +78,7 @@ tasks:
|
||||
- task: integration
|
||||
- task: validate-cyclonedx-schema
|
||||
- task: test-utils
|
||||
- task: snapshot
|
||||
- task: cli
|
||||
- task: check-docker-cache
|
||||
|
||||
@@ -230,10 +233,7 @@ tasks:
|
||||
|
||||
cli:
|
||||
desc: Run CLI tests
|
||||
# note: we don't want to regenerate the snapshot unless we have to. In CI it's probable
|
||||
# that the cache being restored with the correct binary will be rebuilt since the timestamps
|
||||
# and local checksums will not line up.
|
||||
deps: [tools, snapshot]
|
||||
deps: [tools]
|
||||
cmds:
|
||||
- cmd: "echo 'testing binary: {{ .SNAPSHOT_BIN }}'"
|
||||
silent: true
|
||||
@@ -250,27 +250,6 @@ tasks:
|
||||
- cmd: .github/scripts/labeler_test.py
|
||||
|
||||
|
||||
## Benchmark test targets #################################
|
||||
|
||||
benchmark:
|
||||
deps: [tmpdir]
|
||||
generates:
|
||||
- "{{ .TMP_DIR }}/benchmark-main.txt"
|
||||
cmds:
|
||||
- "go test -p 1 -run=^Benchmark -bench=. -count=7 -benchmem ./... | tee {{ .TMP_DIR }}/benchmark-{{ .VERSION }}.txt"
|
||||
- |
|
||||
bash -c "(test -s {{ .TMP_DIR }}/benchmark-main.txt && \
|
||||
{{ .TOOL_DIR }}/benchstat {{ .TMP_DIR }}/benchmark-main.txt {{ .TMP_DIR }}/benchmark-{{ .VERSION }}.txt || \
|
||||
{{ .TOOL_DIR }}/benchstat {{ .TMP_DIR }}/benchmark-{{ .VERSION }}.txt) \
|
||||
| tee {{ .TMP_DIR }}/benchstat.txt"
|
||||
|
||||
show-benchstat:
|
||||
deps: [benchmark, tmpdir]
|
||||
cmds:
|
||||
- cmd: "cat {{ .TMP_DIR }}/benchstat.txt"
|
||||
silent: true
|
||||
|
||||
|
||||
## Test-fixture-related targets #################################
|
||||
|
||||
fingerprints:
|
||||
@@ -286,12 +265,12 @@ tasks:
|
||||
YELLOW='\033[0;33m'
|
||||
RESET='\033[0m'
|
||||
|
||||
echo -e "${YELLOW}creating fingerprint files for non-docker fixtures...${RESET}"
|
||||
echo -e "${YELLOW}creating fingerprint files for non-docker fixtures...${RESET}"
|
||||
for dir in $(find . -type d -name 'test-fixtures'); do
|
||||
if [ -f "$dir/Makefile" ]; then
|
||||
# for debugging...
|
||||
#echo -e "${YELLOW}• calculating fingerprints in $dir... ${RESET}"
|
||||
|
||||
|
||||
(make -C "$dir" fingerprint)
|
||||
fi
|
||||
done
|
||||
@@ -320,18 +299,18 @@ tasks:
|
||||
BOLD='\033[1m'
|
||||
PURPLE='\033[0;35m'
|
||||
RESET='\033[0m'
|
||||
|
||||
|
||||
# if DOWNLOAD_TEST_FIXTURE_CACHE is set to 'false', then skip the cache download and always build
|
||||
if [ "$DOWNLOAD_TEST_FIXTURE_CACHE" = "false" ]; then
|
||||
echo -e "${BOLD}${PURPLE}skipping cache download, rebuilding cache...${RESET}"
|
||||
{{ .TASK }} build-fixtures
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
LATEST_FINGERPRINT=$(docker manifest inspect {{ .CACHE_IMAGE }} | {{ .YQ }} -r '.annotations.fingerprint')
|
||||
|
||||
echo "latest cache: $LATEST_FINGERPRINT"
|
||||
|
||||
|
||||
if [ -f {{ .LAST_CACHE_PULL_FILE }} ]; then
|
||||
LAST_PULL_FINGERPRINT=$(cat {{ .LAST_CACHE_PULL_FILE }} | {{ .YQ }} -r '.digest')
|
||||
else
|
||||
@@ -339,41 +318,41 @@ tasks:
|
||||
{{ .TASK }} download-test-fixture-cache
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
{{ .TASK }} fingerprints
|
||||
|
||||
WANT_FINGERPRINT=$(cat {{ .CACHE_PATHS_FILE }} | {{ .YQ }} -r '.digest')
|
||||
WANT_FINGERPRINT=$(cat {{ .CACHE_PATHS_FILE }} | {{ .YQ }} -r '.digest')
|
||||
|
||||
echo "desired cache: $WANT_FINGERPRINT"
|
||||
echo "last pulled cache: $LAST_PULL_FINGERPRINT"
|
||||
|
||||
|
||||
# if we already have the latest cache, skip the refresh
|
||||
if [ "$LAST_PULL_FINGERPRINT" = "$WANT_FINGERPRINT" ]; then
|
||||
echo -e "${BOLD}${PURPLE}already have the latest cache (skipping cache download)${RESET}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
# at this point we only refresh the cache if we want the same cache that is currently available.
|
||||
# we don't by default refresh the cache if the cache if it is simply different from what we have,
|
||||
# because we may be working on a code change that doesn't require a cache refresh (but could trigger one,
|
||||
# which would be annoying to deal with in a development workflow).
|
||||
|
||||
|
||||
if [ "$LATEST_FINGERPRINT" = "$WANT_FINGERPRINT" ]; then
|
||||
echo -e "${BOLD}${PURPLE}found newer cache! downloading cache...${RESET}"
|
||||
{{ .TASK }} download-test-fixture-cache
|
||||
else
|
||||
echo -e "${BOLD}${PURPLE}found different cache, but isn't clear if it's newer (skipping cache download and manually building)${RESET}"
|
||||
|
||||
|
||||
{{ .YQ }} eval '.paths[] | "\(.digest) \(.path)"' {{ .LAST_CACHE_PULL_FILE }} > .tmp/last_cache_lines
|
||||
{{ .YQ }} eval '.paths[] | "\(.digest) \(.path)"' {{ .CACHE_PATHS_FILE }} > .tmp/cache_lines
|
||||
diff .tmp/last_cache_lines .tmp/cache_lines || true
|
||||
|
||||
|
||||
echo -e "${BOLD}${PURPLE}diff with more context...${RESET}"
|
||||
|
||||
|
||||
diff -U10000 {{ .LAST_CACHE_PULL_FILE }} {{ .CACHE_PATHS_FILE }} || true
|
||||
|
||||
|
||||
echo -e "${BOLD}${PURPLE}detected changes to input material, manually building fixtures...${RESET}"
|
||||
|
||||
|
||||
{{ .TASK }} build-fixtures
|
||||
fi
|
||||
|
||||
@@ -386,7 +365,7 @@ tasks:
|
||||
BOLD='\033[1m'
|
||||
YELLOW='\033[0;33m'
|
||||
RESET='\033[0m'
|
||||
|
||||
|
||||
# Use a for loop with command substitution to avoid subshell issues
|
||||
for dir in $(find . -type d -name 'test-fixtures'); do
|
||||
if [ -f "$dir/Makefile" ]; then
|
||||
@@ -424,15 +403,15 @@ tasks:
|
||||
set -eu
|
||||
oras_command="{{ .ORAS }} push {{ .CACHE_IMAGE }}"
|
||||
|
||||
paths=$(cat {{ .CACHE_PATHS_FILE }} | {{ .YQ }} -r '.paths[].path')
|
||||
paths=$(cat {{ .CACHE_PATHS_FILE }} | {{ .YQ }} -r '.paths[].path')
|
||||
for path in $paths; do
|
||||
oras_command+=" $path"
|
||||
done
|
||||
oras_command+=" {{ .CACHE_PATHS_FILE }}"
|
||||
|
||||
|
||||
oras_command+=" --annotation org.opencontainers.image.source=https://github.com/{{ .OWNER }}/{{ .PROJECT }}"
|
||||
oras_command+=" --annotation fingerprint=$(cat {{ .CACHE_PATHS_FILE }} | {{ .YQ }} -r '.digest')"
|
||||
|
||||
|
||||
echo "Executing: $oras_command"
|
||||
eval $oras_command
|
||||
|
||||
@@ -573,6 +552,18 @@ tasks:
|
||||
|
||||
- "{{ .SNAPSHOT_CMD }}"
|
||||
|
||||
snapshot-smoke-test:
|
||||
desc: Run a smoke test on the snapshot builds + docker images
|
||||
cmds:
|
||||
- cmd: "echo 'testing snapshot binary: {{ .SNAPSHOT_BIN }}'"
|
||||
silent: true
|
||||
- cmd: "test -f {{ .SNAPSHOT_BIN }} || (find {{ .SNAPSHOT_DIR }} && echo '\nno snapshot found for {{ .SNAPSHOT_BIN }}' && false)"
|
||||
silent: true
|
||||
- "{{ .SNAPSHOT_BIN }} version"
|
||||
- "{{ .SNAPSHOT_BIN }} scan alpine:latest"
|
||||
- docker run --rm anchore/syft:latest version
|
||||
- docker run --rm anchore/syft:latest scan alpine:latest
|
||||
|
||||
changelog:
|
||||
desc: Generate a changelog
|
||||
deps: [tools]
|
||||
@@ -650,7 +641,7 @@ tasks:
|
||||
BOLD='\033[1m'
|
||||
YELLOW='\033[0;33m'
|
||||
RESET='\033[0m'
|
||||
|
||||
|
||||
# Use a for loop with command substitution to avoid subshell issues
|
||||
for dir in $(find . -type d -name 'test-fixtures'); do
|
||||
if [ -f "$dir/Makefile" ]; then
|
||||
|
||||
+6
-1
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/cmd/syft/internal"
|
||||
"github.com/anchore/syft/cmd/syft/internal/commands"
|
||||
constants "github.com/anchore/syft/internal"
|
||||
)
|
||||
|
||||
// Application constructs the `syft packages` command and aliases the root command to `syft packages`.
|
||||
@@ -47,7 +48,7 @@ func create(id clio.Identification, out io.Writer) (clio.Application, *cobra.Com
|
||||
commands.Cataloger(app),
|
||||
commands.Attest(app),
|
||||
commands.Convert(app),
|
||||
clio.VersionCommand(id),
|
||||
clio.VersionCommand(id, schemaVersion),
|
||||
clio.ConfigCommand(app, nil),
|
||||
cranecmd.NewCmdAuthLogin(id.Name), // syft login uses the same command as crane
|
||||
)
|
||||
@@ -59,3 +60,7 @@ func create(id clio.Identification, out io.Writer) (clio.Application, *cobra.Com
|
||||
|
||||
return app, rootCmd
|
||||
}
|
||||
|
||||
func schemaVersion() (string, any) {
|
||||
return "SchemaVersion", constants.JSONSchemaVersion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
[TestHandler_handlePullSourceStarted/snap_download_in_progress - 1]
|
||||
⠋ Downloading snap file... ━━━━━━━━━━━━━━━━━━━━ example-app_1.0_amd64.snap
|
||||
---
|
||||
|
||||
[TestHandler_handlePullSourceStarted/snap_download_complete - 1]
|
||||
✔ Snap downloaded successfully example-app_1.0_amd64.snap
|
||||
---
|
||||
@@ -26,7 +26,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
|
||||
{
|
||||
name: "cataloging task in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
value := &monitor.CatalogerTaskProgress{
|
||||
value := &monitor.TaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage("some stage"),
|
||||
Manual: progress.NewManual(100),
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
|
||||
{
|
||||
name: "cataloging sub task in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
value := &monitor.CatalogerTaskProgress{
|
||||
value := &monitor.TaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage("some stage"),
|
||||
Manual: progress.NewManual(100),
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
|
||||
{
|
||||
name: "cataloging sub task complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
value := &monitor.CatalogerTaskProgress{
|
||||
value := &monitor.TaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage("some stage"),
|
||||
Manual: progress.NewManual(100),
|
||||
}
|
||||
@@ -94,7 +94,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
|
||||
{
|
||||
name: "cataloging sub task complete -- hide stage",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
value := &monitor.CatalogerTaskProgress{
|
||||
value := &monitor.TaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage("some stage"),
|
||||
Manual: progress.NewManual(100),
|
||||
}
|
||||
@@ -117,7 +117,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
|
||||
{
|
||||
name: "cataloging sub task complete with removal",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
value := &monitor.CatalogerTaskProgress{
|
||||
value := &monitor.TaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage("some stage"),
|
||||
Manual: progress.NewManual(100),
|
||||
}
|
||||
@@ -162,7 +162,7 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
|
||||
}
|
||||
|
||||
// note: this line / event is not under test, only needed to show a sub status
|
||||
kickoffEvent := &monitor.CatalogerTaskProgress{
|
||||
kickoffEvent := &monitor.TaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage(""),
|
||||
Manual: progress.NewManual(-1),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
)
|
||||
|
||||
func (m *Handler) handlePullSourceStarted(e partybus.Event) []tea.Model {
|
||||
prog, info, err := syftEventParsers.ParsePullSourceStarted(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Debug("unable to parse event")
|
||||
return nil
|
||||
}
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
Default: info.Title.Default,
|
||||
Running: info.Title.WhileRunning,
|
||||
Success: info.Title.OnSuccess,
|
||||
},
|
||||
taskprogress.WithStagedProgressable(prog),
|
||||
)
|
||||
|
||||
tsk.HideOnSuccess = info.HideOnSuccess
|
||||
tsk.HideStageOnSuccess = info.HideStageOnSuccess
|
||||
tsk.HideProgressOnSuccess = true
|
||||
|
||||
if info.Context != "" {
|
||||
tsk.Context = []string{info.Context}
|
||||
}
|
||||
|
||||
return []tea.Model{tsk}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
)
|
||||
|
||||
func TestHandler_handlePullSourceStarted(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
iterations int
|
||||
}{
|
||||
{
|
||||
name: "snap download in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
stage := progress.NewAtomicStage("")
|
||||
manual := progress.NewManual(0)
|
||||
manual.SetTotal(1000000) // 1MB file
|
||||
manual.Set(250000) // 25% downloaded
|
||||
|
||||
taskProg := &monitor.TaskProgress{
|
||||
AtomicStage: stage,
|
||||
Manual: manual,
|
||||
}
|
||||
|
||||
genericTask := monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: "Downloading snap",
|
||||
WhileRunning: "Downloading snap file...",
|
||||
OnSuccess: "Snap downloaded",
|
||||
},
|
||||
Context: "example-app_1.0_amd64.snap",
|
||||
HideOnSuccess: false,
|
||||
HideStageOnSuccess: true,
|
||||
ID: "snap-download-123",
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: event.PullSourceStarted,
|
||||
Source: genericTask,
|
||||
Value: taskProg,
|
||||
}
|
||||
},
|
||||
iterations: 5,
|
||||
},
|
||||
{
|
||||
name: "snap download complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
stage := progress.NewAtomicStage("")
|
||||
manual := progress.NewManual(0)
|
||||
manual.SetTotal(1000000) // 1MB file
|
||||
manual.Set(1000000) // 100% downloaded
|
||||
manual.SetCompleted()
|
||||
|
||||
taskProg := &monitor.TaskProgress{
|
||||
AtomicStage: stage,
|
||||
Manual: manual,
|
||||
}
|
||||
|
||||
genericTask := monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: "Downloading snap",
|
||||
WhileRunning: "Downloading snap file...",
|
||||
OnSuccess: "Snap downloaded successfully",
|
||||
},
|
||||
Context: "example-app_1.0_amd64.snap",
|
||||
HideOnSuccess: false,
|
||||
HideStageOnSuccess: true,
|
||||
ID: "snap-download-123",
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: event.PullSourceStarted,
|
||||
Source: genericTask,
|
||||
Value: taskProg,
|
||||
}
|
||||
},
|
||||
iterations: 3,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.handlePullSourceStarted(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
gotModel := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
|
||||
got := gotModel.View()
|
||||
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ func New(cfg HandlerConfig) *Handler {
|
||||
stereoscopeEvent.FetchImage: simpleHandler(h.handleFetchImage),
|
||||
syftEvent.FileIndexingStarted: simpleHandler(h.handleFileIndexingStarted),
|
||||
syftEvent.AttestationStarted: simpleHandler(h.handleAttestationStarted),
|
||||
syftEvent.PullSourceStarted: simpleHandler(h.handlePullSourceStarted),
|
||||
syftEvent.CatalogerTaskStarted: h.handleCatalogerTaskStarted,
|
||||
})
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ func AppClioSetupConfig(id clio.Identification, out io.Writer) *clio.SetupConfig
|
||||
redact.Set(state.RedactStore)
|
||||
|
||||
log.Set(state.Logger)
|
||||
stereoscope.SetLogger(state.Logger)
|
||||
stereoscope.SetLogger(state.Logger.Nested("from", "stereoscope"))
|
||||
return nil
|
||||
},
|
||||
).
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestPackageDeduplication(t *testing.T) {
|
||||
"curl-minimal-7.76.1-31.el9": 3, // curl upgrade layer + all above layers
|
||||
"wget-1.21.1-8.el9_4": 4, // wget + all above layers
|
||||
"vsftpd-3.0.5-6.el9": 2, // vsftpd + all above layers
|
||||
"httpd-2.4.62-1.el9_5.2": 1, // last layer
|
||||
"httpd-2.4.62-4.el9": 1, // last layer
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -51,7 +51,7 @@ func TestPackageDeduplication(t *testing.T) {
|
||||
"curl-minimal-7.76.1-31.el9": 1, // upgrade
|
||||
"wget-1.21.1-8.el9_4": 1,
|
||||
"vsftpd-3.0.5-6.el9": 1,
|
||||
"httpd-2.4.62-1.el9_5.2": 1,
|
||||
"httpd-2.4.62-4.el9": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ go 1.24.1
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/CycloneDX/cyclonedx-go v0.9.2
|
||||
github.com/Masterminds/semver/v3 v3.3.1
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/OneOfOne/xxhash v1.2.8
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||
@@ -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.6
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/aquasecurity/go-pep440-version v0.0.1
|
||||
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
|
||||
@@ -34,22 +35,25 @@ require (
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/dave/jennifer v1.7.1
|
||||
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
|
||||
github.com/diskfs/go-diskfs v1.6.1-0.20250601133945-2af1c7ece24c
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/elliotchance/phpserialize v1.4.0
|
||||
github.com/facebookincubator/nvdtools v0.1.5
|
||||
github.com/github/go-spdx/v2 v2.3.3
|
||||
github.com/gkampitakis/go-snaps v0.5.11
|
||||
github.com/gkampitakis/go-snaps v0.5.13
|
||||
github.com/go-git/go-billy/v5 v5.6.2
|
||||
github.com/go-git/go-git/v5 v5.16.0
|
||||
github.com/go-git/go-git/v5 v5.16.2
|
||||
github.com/go-test/deep v1.1.1
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0
|
||||
github.com/gohugoio/hashstructure v0.5.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/go-containerregistry v0.20.3
|
||||
github.com/google/go-containerregistry v0.20.6
|
||||
github.com/google/licensecheck v0.3.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gookit/color v1.5.4
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2
|
||||
github.com/hashicorp/go-getter v1.7.8
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/hcl/v2 v2.23.0
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
@@ -57,12 +61,11 @@ 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.2
|
||||
github.com/mholt/archives v0.1.3
|
||||
github.com/moby/sys/mountinfo v0.7.2
|
||||
github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/olekukonko/tablewriter v1.0.7
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.22
|
||||
@@ -72,7 +75,7 @@ require (
|
||||
github.com/sassoftware/go-rpmutils v0.4.0
|
||||
// pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5
|
||||
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
|
||||
github.com/sergi/go-diff v1.4.0
|
||||
github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb
|
||||
github.com/spdx/tools-golang v0.5.5
|
||||
github.com/spf13/afero v1.14.0
|
||||
@@ -85,14 +88,20 @@ require (
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1
|
||||
go.uber.org/goleak v1.3.0
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
|
||||
golang.org/x/mod v0.24.0
|
||||
golang.org/x/net v0.40.0
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
|
||||
golang.org/x/mod v0.25.0
|
||||
golang.org/x/net v0.41.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.37.0
|
||||
modernc.org/sqlite v1.38.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.116.0 // indirect
|
||||
cloud.google.com/go/auth v0.9.9 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.7.0 // indirect
|
||||
cloud.google.com/go/iam v1.2.2 // indirect
|
||||
cloud.google.com/go/storage v1.43.0 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect
|
||||
@@ -103,14 +112,17 @@ require (
|
||||
github.com/ProtonMail/go-crypto v1.2.0 // indirect
|
||||
github.com/STARRY-S/zip v0.2.1 // indirect
|
||||
github.com/agext/levenshtein v1.2.1 // indirect; indirectt
|
||||
github.com/anchore/go-lzo v0.1.0 // indirect
|
||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
|
||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/aquasecurity/go-version v0.0.1 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.122 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/becheran/wildmatch-go v1.0.0 // indirect
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.0 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
@@ -124,46 +136,54 @@ require (
|
||||
github.com/containerd/containerd v1.7.27 // indirect
|
||||
github.com/containerd/containerd/api v1.8.0 // indirect
|
||||
github.com/containerd/continuity v0.4.4 // indirect
|
||||
github.com/containerd/errdefs v0.3.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/fifo v1.1.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||
github.com/containerd/ttrpc v1.2.7 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.1.1 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.2.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/cli v28.1.1+incompatible // indirect
|
||||
github.com/docker/cli v28.3.0+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v28.1.1+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||
github.com/docker/docker v28.2.2+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/felixge/fgprof v0.9.5 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gkampitakis/ciinfo v0.3.1 // indirect
|
||||
github.com/gkampitakis/ciinfo v0.3.2 // indirect
|
||||
github.com/gkampitakis/go-diff v1.3.2 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-restruct/restruct v1.2.0-alpha // indirect
|
||||
github.com/goccy/go-yaml v1.15.13 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-safetemp v1.0.0 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
@@ -177,9 +197,11 @@ require (
|
||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||
github.com/minio/minlz v1.0.0 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
@@ -194,6 +216,8 @@ require (
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/nwaples/rardecode v1.1.3 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.1.0 // indirect
|
||||
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
|
||||
github.com/olekukonko/ll v0.0.8 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.1.0 // indirect
|
||||
github.com/opencontainers/selinux v1.11.0 // indirect
|
||||
@@ -203,6 +227,7 @@ require (
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pkg/profile v1.7.0 // indirect
|
||||
github.com/pkg/xattr v0.4.9 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
@@ -210,13 +235,14 @@ require (
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.1 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.5 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/viper v1.20.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/sylabs/sif/v2 v2.21.1 // indirect
|
||||
github.com/sylabs/squashfs v1.0.6 // indirect
|
||||
@@ -226,7 +252,7 @@ require (
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/vbatts/tar-split v0.11.6 // indirect
|
||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
@@ -235,34 +261,37 @@ require (
|
||||
github.com/zclconf/go-cty v1.13.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
|
||||
go.opentelemetry.io/otel v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.33.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/api v0.203.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
|
||||
google.golang.org/grpc v1.67.3 // indirect
|
||||
google.golang.org/protobuf v1.36.4 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
modernc.org/libc v1.62.1 // indirect
|
||||
modernc.org/libc v1.65.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.9.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
retract (
|
||||
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
|
||||
)
|
||||
|
||||
// adds RSA Headers from RPMDB via https://github.com/knqyf263/go-rpmdb/pull/58 (in review)
|
||||
replace github.com/knqyf263/go-rpmdb => github.com/anchore/go-rpmdb v0.0.0-20250515153519-38be2efee8ed
|
||||
|
||||
+32
-2
@@ -36,8 +36,8 @@ func Notify(message string) {
|
||||
})
|
||||
}
|
||||
|
||||
func StartCatalogerTask(info monitor.GenericTask, size int64, initialStage string) *monitor.CatalogerTaskProgress {
|
||||
t := &monitor.CatalogerTaskProgress{
|
||||
func StartCatalogerTask(info monitor.GenericTask, size int64, initialStage string) *monitor.TaskProgress {
|
||||
t := &monitor.TaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage(initialStage),
|
||||
Manual: progress.NewManual(size),
|
||||
}
|
||||
@@ -50,3 +50,33 @@ func StartCatalogerTask(info monitor.GenericTask, size int64, initialStage strin
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
func StartPullSourceTask(info monitor.GenericTask, size int64, initialStage string) *monitor.TaskProgress {
|
||||
t := &monitor.TaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage(initialStage),
|
||||
Manual: progress.NewManual(size),
|
||||
}
|
||||
|
||||
Publish(partybus.Event{
|
||||
Type: event.PullSourceStarted,
|
||||
Source: info,
|
||||
Value: progress.StagedProgressable(t),
|
||||
})
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
func StartIndexingFiles(path string) *monitor.TaskProgress {
|
||||
t := &monitor.TaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage(""),
|
||||
Manual: progress.NewManual(-1),
|
||||
}
|
||||
|
||||
Publish(partybus.Event{
|
||||
Type: event.FileIndexingStarted,
|
||||
Source: path,
|
||||
Value: progress.StagedProgressable(t),
|
||||
})
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
@@ -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.33"
|
||||
JSONSchemaVersion = "16.0.34"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/go-getter"
|
||||
"github.com/hashicorp/go-getter/helper/url"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/internal"
|
||||
)
|
||||
|
||||
var (
|
||||
archiveExtensions = getterDecompressorNames()
|
||||
ErrNonArchiveSource = fmt.Errorf("non-archive sources are not supported for directory destinations")
|
||||
)
|
||||
|
||||
type Getter interface {
|
||||
// GetFile downloads the give URL into the given path. The URL must reference a single file.
|
||||
GetFile(dst, src string, monitor ...*progress.Manual) error
|
||||
|
||||
// GetToDir downloads the resource found at the `src` URL into the given `dst` directory.
|
||||
// The directory must already exist, and the remote resource MUST BE AN ARCHIVE (e.g. `.tar.gz`).
|
||||
GetToDir(dst, src string, monitor ...*progress.Manual) error
|
||||
}
|
||||
|
||||
type HashiGoGetter struct {
|
||||
httpGetter getter.HttpGetter
|
||||
}
|
||||
|
||||
// NewGetter creates and returns a new Getter. Providing an http.Client is optional. If one is provided,
|
||||
// it will be used for all HTTP(S) getting; otherwise, go-getter's default getters will be used.
|
||||
func NewGetter(id clio.Identification, httpClient *http.Client) *HashiGoGetter {
|
||||
return &HashiGoGetter{
|
||||
httpGetter: getter.HttpGetter{
|
||||
Client: httpClient,
|
||||
Header: http.Header{
|
||||
"User-Agent": []string{fmt.Sprintf("%v %v", id.Name, id.Version)},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (g HashiGoGetter) GetFile(dst, src string, monitors ...*progress.Manual) error {
|
||||
if len(monitors) > 1 {
|
||||
return fmt.Errorf("multiple monitors provided, which is not allowed")
|
||||
}
|
||||
|
||||
return getterClient(dst, src, false, g.httpGetter, monitors).Get()
|
||||
}
|
||||
|
||||
func (g HashiGoGetter) GetToDir(dst, src string, monitors ...*progress.Manual) error {
|
||||
// though there are multiple getters, only the http/https getter requires extra validation
|
||||
if err := validateHTTPSource(src); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(monitors) > 1 {
|
||||
return fmt.Errorf("multiple monitors provided, which is not allowed")
|
||||
}
|
||||
|
||||
return getterClient(dst, src, true, g.httpGetter, monitors).Get()
|
||||
}
|
||||
|
||||
func validateHTTPSource(src string) error {
|
||||
// we are ignoring any sources that are not destined to use the http getter object
|
||||
if !internal.HasAnyOfPrefixes(src, "http://", "https://") {
|
||||
return nil
|
||||
}
|
||||
|
||||
u, err := url.Parse(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad URL provided %q: %w", src, err)
|
||||
}
|
||||
// only allow for sources with archive extensions
|
||||
if !internal.HasAnyOfSuffixes(u.Path, archiveExtensions...) {
|
||||
return ErrNonArchiveSource
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getterClient(dst, src string, dir bool, httpGetter getter.HttpGetter, monitors []*progress.Manual) *getter.Client {
|
||||
client := &getter.Client{
|
||||
Src: src,
|
||||
Dst: dst,
|
||||
Dir: dir,
|
||||
Getters: map[string]getter.Getter{
|
||||
"http": &httpGetter,
|
||||
"https": &httpGetter,
|
||||
// note: these are the default getters from https://github.com/hashicorp/go-getter/blob/v1.5.9/get.go#L68-L74
|
||||
// it is possible that other implementations need to account for custom httpclient injection, however,
|
||||
// that has not been accounted for at this time.
|
||||
"file": new(getter.FileGetter),
|
||||
"git": new(getter.GitGetter),
|
||||
"gcs": new(getter.GCSGetter),
|
||||
"hg": new(getter.HgGetter),
|
||||
"s3": new(getter.S3Getter),
|
||||
},
|
||||
Options: mapToGetterClientOptions(monitors),
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func withProgress(monitor *progress.Manual) func(client *getter.Client) error {
|
||||
return getter.WithProgress(
|
||||
&progressAdapter{monitor: monitor},
|
||||
)
|
||||
}
|
||||
|
||||
func mapToGetterClientOptions(monitors []*progress.Manual) []getter.ClientOption {
|
||||
var result []getter.ClientOption
|
||||
|
||||
for _, monitor := range monitors {
|
||||
result = append(result, withProgress(monitor))
|
||||
}
|
||||
|
||||
// derived from https://github.com/hashicorp/go-getter/blob/v2.2.3/decompress.go#L23-L63
|
||||
fileSizeLimit := int64(5 * file.GB)
|
||||
|
||||
dec := getter.LimitedDecompressors(0, fileSizeLimit)
|
||||
|
||||
result = append(result, getter.WithDecompressors(dec))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type readCloser struct {
|
||||
progress.Reader
|
||||
}
|
||||
|
||||
func (c *readCloser) Close() error { return nil }
|
||||
|
||||
type progressAdapter struct {
|
||||
monitor *progress.Manual
|
||||
}
|
||||
|
||||
func (a *progressAdapter) TrackProgress(_ string, currentSize, totalSize int64, stream io.ReadCloser) io.ReadCloser {
|
||||
a.monitor.Set(currentSize)
|
||||
a.monitor.SetTotal(totalSize)
|
||||
return &readCloser{
|
||||
Reader: *progress.NewProxyReader(stream, a.monitor),
|
||||
}
|
||||
}
|
||||
|
||||
func getterDecompressorNames() (names []string) {
|
||||
for name := range getter.Decompressors {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
)
|
||||
|
||||
func TestGetter_GetFile(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
prepareClient func(*http.Client)
|
||||
assert assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "client trusts server's CA",
|
||||
assert: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "client doesn't trust server's CA",
|
||||
prepareClient: removeTrustedCAs,
|
||||
assert: assertUnknownAuthorityError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
requestPath := "/foo"
|
||||
|
||||
server := newTestServer(t, withResponseForPath(t, requestPath, testFileContent))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
httpClient := getClient(t, server)
|
||||
if tc.prepareClient != nil {
|
||||
tc.prepareClient(httpClient)
|
||||
}
|
||||
|
||||
getter := NewGetter(testID, httpClient)
|
||||
requestURL := createRequestURL(t, server, requestPath)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
tempFile := path.Join(tempDir, "some-destination-file")
|
||||
|
||||
err := getter.GetFile(tempFile, requestURL)
|
||||
tc.assert(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetter_GetToDir_FilterNonArchivesWired(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
source string
|
||||
assert assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "error out on non-archive sources",
|
||||
source: "http://localhost/something.txt",
|
||||
assert: assertErrNonArchiveSource,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
test.assert(t, NewGetter(testID, nil).GetToDir(t.TempDir(), test.source))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetter_validateHttpSource(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
source string
|
||||
assert assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "error out on non-archive sources",
|
||||
source: "http://localhost/something.txt",
|
||||
assert: assertErrNonArchiveSource,
|
||||
},
|
||||
{
|
||||
name: "filter out non-archive sources with get param",
|
||||
source: "https://localhost/vulnerability-db_v3_2021-11-21T08:15:44Z.txt?checksum=sha256%3Ac402d01fa909a3fa85a5c6733ef27a3a51a9105b6c62b9152adbd24c08358911",
|
||||
assert: assertErrNonArchiveSource,
|
||||
},
|
||||
{
|
||||
name: "ignore non http-https input",
|
||||
source: "s3://bucket/something.txt",
|
||||
assert: assert.NoError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
test.assert(t, validateHTTPSource(test.source))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetter_GetToDir_CertConcerns(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
prepareClient func(*http.Client)
|
||||
assert assert.ErrorAssertionFunc
|
||||
}{
|
||||
|
||||
{
|
||||
name: "client trusts server's CA",
|
||||
assert: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "client doesn't trust server's CA",
|
||||
prepareClient: removeTrustedCAs,
|
||||
assert: assertUnknownAuthorityError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
requestPath := "/foo.tar"
|
||||
tarball := createTarball("foo", testFileContent)
|
||||
|
||||
server := newTestServer(t, withResponseForPath(t, requestPath, tarball))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
httpClient := getClient(t, server)
|
||||
if tc.prepareClient != nil {
|
||||
tc.prepareClient(httpClient)
|
||||
}
|
||||
|
||||
getter := NewGetter(testID, httpClient)
|
||||
requestURL := createRequestURL(t, server, requestPath)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
|
||||
err := getter.GetToDir(tempDir, requestURL)
|
||||
tc.assert(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertUnknownAuthorityError(t assert.TestingT, err error, _ ...interface{}) bool {
|
||||
return assert.ErrorAs(t, err, &x509.UnknownAuthorityError{})
|
||||
}
|
||||
|
||||
func assertErrNonArchiveSource(t assert.TestingT, err error, _ ...interface{}) bool {
|
||||
return assert.ErrorIs(t, err, ErrNonArchiveSource)
|
||||
}
|
||||
|
||||
func removeTrustedCAs(client *http.Client) {
|
||||
client.Transport.(*http.Transport).TLSClientConfig.RootCAs = x509.NewCertPool()
|
||||
}
|
||||
|
||||
// createTarball makes a single-file tarball and returns it as a byte slice.
|
||||
func createTarball(filename string, content []byte) []byte {
|
||||
tarBuffer := new(bytes.Buffer)
|
||||
tarWriter := tar.NewWriter(tarBuffer)
|
||||
tarWriter.WriteHeader(&tar.Header{
|
||||
Name: filename,
|
||||
Size: int64(len(content)),
|
||||
Mode: 0600,
|
||||
})
|
||||
tarWriter.Write(content)
|
||||
tarWriter.Close()
|
||||
|
||||
return tarBuffer.Bytes()
|
||||
}
|
||||
|
||||
type muxOption func(mux *http.ServeMux)
|
||||
|
||||
func withResponseForPath(t *testing.T, path string, response []byte) muxOption {
|
||||
t.Helper()
|
||||
|
||||
return func(mux *http.ServeMux) {
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, req *http.Request) {
|
||||
t.Logf("server handling request: %s %s", req.Method, req.URL)
|
||||
|
||||
_, err := w.Write(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var testID = clio.Identification{
|
||||
Name: "test-app",
|
||||
Version: "v0.5.3",
|
||||
}
|
||||
|
||||
func newTestServer(t *testing.T, muxOptions ...muxOption) *httptest.Server {
|
||||
t.Helper()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
for _, option := range muxOptions {
|
||||
option(mux)
|
||||
}
|
||||
|
||||
server := httptest.NewTLSServer(mux)
|
||||
t.Logf("new TLS server listening at %s", getHost(t, server))
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func createRequestURL(t *testing.T, server *httptest.Server, path string) string {
|
||||
t.Helper()
|
||||
|
||||
// TODO: Figure out how to get this value from the server without hardcoding it here
|
||||
const testServerCertificateName = "example.com"
|
||||
|
||||
serverURL, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set URL hostname to value from TLS certificate
|
||||
serverURL.Host = fmt.Sprintf("%s:%s", testServerCertificateName, serverURL.Port())
|
||||
|
||||
serverURL.Path = path
|
||||
|
||||
return serverURL.String()
|
||||
}
|
||||
|
||||
// getClient returns an http.Client that can be used to contact the test TLS server.
|
||||
func getClient(t *testing.T, server *httptest.Server) *http.Client {
|
||||
t.Helper()
|
||||
|
||||
httpClient := server.Client()
|
||||
transport := httpClient.Transport.(*http.Transport)
|
||||
|
||||
serverHost := getHost(t, server)
|
||||
|
||||
transport.DialContext = func(_ context.Context, _, addr string) (net.Conn, error) {
|
||||
t.Logf("client dialing %q for host %q", serverHost, addr)
|
||||
|
||||
// Ensure the client dials our test server
|
||||
return net.Dial("tcp", serverHost)
|
||||
}
|
||||
|
||||
return httpClient
|
||||
}
|
||||
|
||||
// getHost extracts the host value from a server URL string.
|
||||
// e.g. given a server with URL "http://1.2.3.4:5000/foo", getHost returns "1.2.3.4:5000"
|
||||
func getHost(t *testing.T, server *httptest.Server) string {
|
||||
t.Helper()
|
||||
|
||||
u, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return u.Hostname() + ":" + u.Port()
|
||||
}
|
||||
|
||||
var testFileContent = []byte("This is the content of a test file!\n")
|
||||
@@ -0,0 +1,89 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/diskfs/go-diskfs/filesystem"
|
||||
)
|
||||
|
||||
type WalkDiskDirFunc func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error
|
||||
|
||||
// WalkDiskDir walks the file tree within the go-diskfs filesystem at root, calling fn for each file or directory in the tree, including root.
|
||||
// This is meant to mimic the behavior of fs.WalkDir in the standard library.
|
||||
func WalkDiskDir(fsys filesystem.FileSystem, root string, fn WalkDiskDirFunc) error {
|
||||
infos, err := fsys.ReadDir(root)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(infos) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, info := range infos {
|
||||
p := filepath.Join(root, info.Name())
|
||||
err = walkDiskDir(fsys, p, info, fn)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.SkipDir) {
|
||||
continue
|
||||
}
|
||||
if errors.Is(err, fs.SkipAll) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func walkDiskDir(fsys filesystem.FileSystem, name string, d os.FileInfo, walkDirFn WalkDiskDirFunc) error {
|
||||
if err := walkDirFn(fsys, name, d, nil); err != nil {
|
||||
if errors.Is(err, fs.SkipDir) && (d == nil || d.IsDir()) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
isDir := d != nil && d.IsDir()
|
||||
if d == nil {
|
||||
_, err := fsys.ReadDir(name)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
isDir = true
|
||||
}
|
||||
|
||||
if !isDir {
|
||||
return nil
|
||||
}
|
||||
|
||||
dirs, err := fsys.ReadDir(name)
|
||||
if err != nil {
|
||||
err = walkDirFn(fsys, name, d, err)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.SkipDir) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, d1 := range dirs {
|
||||
name1 := filepath.Join(name, d1.Name())
|
||||
if err := walkDiskDir(fsys, name1, d1, walkDirFn); err != nil {
|
||||
if errors.Is(err, fs.SkipDir) {
|
||||
break
|
||||
}
|
||||
if errors.Is(err, fs.SkipAll) {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/diskfs/go-diskfs/backend/file"
|
||||
"github.com/diskfs/go-diskfs/filesystem"
|
||||
"github.com/diskfs/go-diskfs/filesystem/squashfs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func createTestFS(t *testing.T) filesystem.FileSystem {
|
||||
dir := t.TempDir()
|
||||
|
||||
filename := "test.squashfs"
|
||||
f, err := os.Create(filepath.Join(dir, filename))
|
||||
require.NoError(t, err)
|
||||
|
||||
b := file.New(f, false)
|
||||
fsys, err := squashfs.Create(b, 0, 0, 4096)
|
||||
require.NoError(t, err)
|
||||
|
||||
testFiles := []struct {
|
||||
path string
|
||||
content string
|
||||
isDir bool
|
||||
}{
|
||||
{"/file1.txt", "content of file1", false},
|
||||
{"/file2.txt", "content of file2", false},
|
||||
{"/dir1", "", true},
|
||||
{"/dir1/subfile1.txt", "content of subfile1", false},
|
||||
{"/dir1/subfile2.txt", "content of subfile2", false},
|
||||
{"/dir1/subdir1", "", true},
|
||||
{"/dir1/subdir1/deepfile.txt", "deep content", false},
|
||||
{"/dir2", "", true},
|
||||
{"/dir2/anotherfile.txt", "another content", false},
|
||||
{"/emptydir", "", true},
|
||||
}
|
||||
|
||||
for _, tf := range testFiles {
|
||||
if tf.isDir {
|
||||
err := fsys.Mkdir(tf.path)
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
f, err := fsys.OpenFile(tf.path, os.O_CREATE|os.O_RDWR)
|
||||
require.NoError(t, err)
|
||||
_, err = f.Write([]byte(tf.content))
|
||||
require.NoError(t, err)
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
return fsys
|
||||
}
|
||||
|
||||
func TestWalkDiskDir_CompleteTraversal(t *testing.T) {
|
||||
fsys := createTestFS(t)
|
||||
|
||||
var visitedPaths []string
|
||||
err := WalkDiskDir(fsys, "/", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
|
||||
require.NoError(t, err)
|
||||
visitedPaths = append(visitedPaths, path)
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedPaths := []string{
|
||||
"/file1.txt",
|
||||
"/file2.txt",
|
||||
"/dir1",
|
||||
"/dir1/subfile1.txt",
|
||||
"/dir1/subfile2.txt",
|
||||
"/dir1/subdir1",
|
||||
"/dir1/subdir1/deepfile.txt",
|
||||
"/dir2",
|
||||
"/dir2/anotherfile.txt",
|
||||
"/emptydir",
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, expectedPaths, visitedPaths)
|
||||
}
|
||||
|
||||
func TestWalkDiskDir_FileInfoCorrect(t *testing.T) {
|
||||
fsys := createTestFS(t)
|
||||
|
||||
var fileInfos []struct {
|
||||
path string
|
||||
isDir bool
|
||||
name string
|
||||
}
|
||||
|
||||
err := WalkDiskDir(fsys, "/", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, d)
|
||||
fileInfos = append(fileInfos, struct {
|
||||
path string
|
||||
isDir bool
|
||||
name string
|
||||
}{
|
||||
path: path,
|
||||
isDir: d.IsDir(),
|
||||
name: d.Name(),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, fi := range fileInfos {
|
||||
expectedName := filepath.Base(fi.path)
|
||||
assert.Equal(t, expectedName, fi.name)
|
||||
|
||||
if fi.path == "/dir1" || fi.path == "/dir2" || fi.path == "/emptydir" || fi.path == "/dir1/subdir1" {
|
||||
assert.True(t, fi.isDir, "Expected %s to be directory", fi.path)
|
||||
} else {
|
||||
assert.False(t, fi.isDir, "Expected %s to be file", fi.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkDiskDir_SkipDir(t *testing.T) {
|
||||
fsys := createTestFS(t)
|
||||
|
||||
var visitedPaths []string
|
||||
err := WalkDiskDir(fsys, "/", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
|
||||
require.NoError(t, err)
|
||||
visitedPaths = append(visitedPaths, path)
|
||||
if path == "/dir1" {
|
||||
return fs.SkipDir
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, visitedPaths, "/dir1")
|
||||
assert.NotContains(t, visitedPaths, "/dir1/subfile1.txt")
|
||||
assert.NotContains(t, visitedPaths, "/dir1/subfile2.txt")
|
||||
assert.NotContains(t, visitedPaths, "/dir1/subdir1")
|
||||
assert.NotContains(t, visitedPaths, "/dir1/subdir1/deepfile.txt")
|
||||
|
||||
assert.Contains(t, visitedPaths, "/dir2")
|
||||
assert.Contains(t, visitedPaths, "/dir2/anotherfile.txt")
|
||||
}
|
||||
|
||||
func TestWalkDiskDir_SkipAll(t *testing.T) {
|
||||
fsys := createTestFS(t)
|
||||
|
||||
var visitedPaths []string
|
||||
err := WalkDiskDir(fsys, "/", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
|
||||
require.NoError(t, err)
|
||||
visitedPaths = append(visitedPaths, path)
|
||||
if path == "/dir1" {
|
||||
return fs.SkipAll
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, visitedPaths, "/dir1")
|
||||
|
||||
assert.NotContains(t, visitedPaths, "/file1.txt")
|
||||
assert.NotContains(t, visitedPaths, "/file2.txt")
|
||||
assert.NotContains(t, visitedPaths, "/dir1/subfile1.txt")
|
||||
assert.NotContains(t, visitedPaths, "/dir2")
|
||||
assert.NotContains(t, visitedPaths, "/dir2/anotherfile.txt")
|
||||
assert.NotContains(t, visitedPaths, "/emptydir")
|
||||
}
|
||||
|
||||
func TestWalkDiskDir_EmptyDirectory(t *testing.T) {
|
||||
fs := createTestFS(t)
|
||||
|
||||
var visitedPaths []string
|
||||
err := WalkDiskDir(fs, "/emptydir", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
|
||||
require.NoError(t, err)
|
||||
visitedPaths = append(visitedPaths, path)
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, visitedPaths)
|
||||
}
|
||||
|
||||
func TestWalkDiskDir_NonexistentPath(t *testing.T) {
|
||||
fs := createTestFS(t)
|
||||
|
||||
err := WalkDiskDir(fs, "/nonexistent", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestWalkDiskDir_WalkFunctionError(t *testing.T) {
|
||||
fs := createTestFS(t)
|
||||
|
||||
customErr := assert.AnError
|
||||
err := WalkDiskDir(fs, "/", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
|
||||
if path == "/file1.txt" {
|
||||
return customErr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, customErr, err)
|
||||
}
|
||||
|
||||
func TestWalkDiskDir_SubdirectoryTraversal(t *testing.T) {
|
||||
fs := createTestFS(t)
|
||||
|
||||
var visitedPaths []string
|
||||
err := WalkDiskDir(fs, "/dir1", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
|
||||
require.NoError(t, err)
|
||||
visitedPaths = append(visitedPaths, path)
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedPaths := []string{
|
||||
"/dir1/subfile1.txt",
|
||||
"/dir1/subfile2.txt",
|
||||
"/dir1/subdir1",
|
||||
"/dir1/subdir1/deepfile.txt",
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, expectedPaths, visitedPaths)
|
||||
}
|
||||
|
||||
func TestWalkDiskDir_SingleFile(t *testing.T) {
|
||||
fs := createTestFS(t)
|
||||
|
||||
var visitedPaths []string
|
||||
err := WalkDiskDir(fs, "/file1.txt", func(fsys filesystem.FileSystem, path string, d os.FileInfo, err error) error {
|
||||
require.NoError(t, err)
|
||||
visitedPaths = append(visitedPaths, path)
|
||||
return nil
|
||||
})
|
||||
|
||||
// we are providing a file path, not a directory
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, visitedPaths)
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -13,6 +13,17 @@ func HasAnyOfPrefixes(input string, prefixes ...string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// HasAnyOfSuffixes returns an indication if the given string has any of the given suffixes.
|
||||
func HasAnyOfSuffixes(input string, suffixes ...string) bool {
|
||||
for _, suffix := range suffixes {
|
||||
if strings.HasSuffix(input, suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func TruncateMiddleEllipsis(input string, maxLen int) string {
|
||||
if len(input) <= maxLen {
|
||||
return input
|
||||
|
||||
@@ -7,6 +7,63 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHasAnyOfSuffixes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
suffixes []string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "go case",
|
||||
input: "this has something",
|
||||
suffixes: []string{
|
||||
"has something",
|
||||
"has NOT something",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no match",
|
||||
input: "this has something",
|
||||
suffixes: []string{
|
||||
"has NOT something",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
input: "this has something",
|
||||
suffixes: []string{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "positive match last",
|
||||
input: "this has something",
|
||||
suffixes: []string{
|
||||
"that does not have",
|
||||
"something",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
suffixes: []string{
|
||||
"that does not have",
|
||||
"this has",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, HasAnyOfSuffixes(test.input, test.suffixes...))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasAnyOfPrefixes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func RunTask(ctx context.Context, tsk Task, resolver file.Resolver, s sbomsync.Builder, prog *monitor.CatalogerTaskProgress) error {
|
||||
func RunTask(ctx context.Context, tsk Task, resolver file.Resolver, s sbomsync.Builder, prog *monitor.TaskProgress) error {
|
||||
err := runTaskSafely(ctx, tsk, resolver, s)
|
||||
unknowns, remainingErrors := unknown.ExtractCoordinateErrors(err)
|
||||
if len(unknowns) > 0 {
|
||||
|
||||
@@ -17,7 +17,7 @@ func Test_TaskExecutor_PanicHandling(t *testing.T) {
|
||||
panic("something bad happened")
|
||||
})
|
||||
|
||||
err := RunTask(context.Background(), tsk, nil, nil, &monitor.CatalogerTaskProgress{
|
||||
err := RunTask(context.Background(), tsk, nil, nil, &monitor.TaskProgress{
|
||||
Manual: progress.NewManual(-1),
|
||||
})
|
||||
|
||||
|
||||
@@ -154,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"),
|
||||
|
||||
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.33/document",
|
||||
"$id": "anchore.io/schema/syft/json/16.0.34/document",
|
||||
"$ref": "#/$defs/Document",
|
||||
"$defs": {
|
||||
"AlpmDbEntry": {
|
||||
@@ -1971,6 +1971,9 @@
|
||||
{
|
||||
"$ref": "#/$defs/OpamPackage"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/PeBinary"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/PhpComposerInstalledEntry"
|
||||
},
|
||||
@@ -2045,6 +2048,17 @@
|
||||
"purl"
|
||||
]
|
||||
},
|
||||
"PeBinary": {
|
||||
"properties": {
|
||||
"VersionResources": {
|
||||
"$ref": "#/$defs/KeyValues"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"VersionResources"
|
||||
]
|
||||
},
|
||||
"PhpComposerAuthors": {
|
||||
"properties": {
|
||||
"name": {
|
||||
|
||||
+3
-3
@@ -144,14 +144,14 @@ func setContextExecutors(ctx context.Context, cfg *CreateSBOMConfig) context.Con
|
||||
return ctx
|
||||
}
|
||||
|
||||
func monitorPackageCount(prog *monitor.CatalogerTaskProgress) func(s *sbom.SBOM) {
|
||||
func monitorPackageCount(prog *monitor.TaskProgress) func(s *sbom.SBOM) {
|
||||
return func(s *sbom.SBOM) {
|
||||
count := humanize.Comma(int64(s.Artifacts.Packages.PackageCount()))
|
||||
prog.AtomicStage.Set(fmt.Sprintf("%s packages", count))
|
||||
}
|
||||
}
|
||||
|
||||
func monitorPackageCatalogingTask() *monitor.CatalogerTaskProgress {
|
||||
func monitorPackageCatalogingTask() *monitor.TaskProgress {
|
||||
info := monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: "Packages",
|
||||
@@ -164,7 +164,7 @@ func monitorPackageCatalogingTask() *monitor.CatalogerTaskProgress {
|
||||
return bus.StartCatalogerTask(info, -1, "")
|
||||
}
|
||||
|
||||
func monitorCatalogingTask(srcID artifact.ID, tasks [][]task.Task) *monitor.CatalogerTaskProgress {
|
||||
func monitorCatalogingTask(srcID artifact.ID, tasks [][]task.Task) *monitor.TaskProgress {
|
||||
info := monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: "Catalog contents",
|
||||
|
||||
@@ -464,6 +464,8 @@ func findDefaultTags(src source.Description) ([]string, error) {
|
||||
return []string{pkgcataloging.ImageTag, filecataloging.FileTag}, nil
|
||||
case source.FileMetadata, source.DirectoryMetadata:
|
||||
return []string{pkgcataloging.DirectoryTag, filecataloging.FileTag}, nil
|
||||
case source.SnapMetadata:
|
||||
return []string{pkgcataloging.InstalledTag, filecataloging.FileTag}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unable to determine default cataloger tag for source type=%T", m)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ const (
|
||||
// CatalogerTaskStarted is a partybus event that occurs when starting a task within a cataloger
|
||||
CatalogerTaskStarted partybus.EventType = typePrefix + "-cataloger-task-started"
|
||||
|
||||
// PullSourceStarted is a partybus event that occurs when starting to pull a source (does not overlap with stereoscope image pull events,
|
||||
// this covers any additional sources such as snap and git repos).
|
||||
PullSourceStarted partybus.EventType = typePrefix + "-pull-source-started"
|
||||
|
||||
// Events exclusively for the CLI
|
||||
|
||||
// CLIAppUpdateAvailable is a partybus event that occurs when an application update is available
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"github.com/wagoodman/go-progress"
|
||||
)
|
||||
|
||||
const (
|
||||
TopLevelCatalogingTaskID = "cataloging"
|
||||
PackageCatalogingTaskID = "package-cataloging"
|
||||
)
|
||||
|
||||
type CatalogerTaskProgress struct {
|
||||
*progress.AtomicStage
|
||||
*progress.Manual
|
||||
}
|
||||
@@ -6,6 +6,11 @@ import (
|
||||
"github.com/wagoodman/go-progress"
|
||||
)
|
||||
|
||||
const (
|
||||
TopLevelCatalogingTaskID = "cataloging"
|
||||
PackageCatalogingTaskID = "package-cataloging"
|
||||
)
|
||||
|
||||
type ShellProgress struct {
|
||||
io.Reader
|
||||
progress.Progressable
|
||||
@@ -34,3 +39,8 @@ type GenericTask struct {
|
||||
ParentID string
|
||||
Context string
|
||||
}
|
||||
|
||||
type TaskProgress struct {
|
||||
*progress.AtomicStage
|
||||
*progress.Manual
|
||||
}
|
||||
|
||||
@@ -77,6 +77,26 @@ func ParseCatalogerTaskStarted(e partybus.Event) (progress.StagedProgressable, *
|
||||
return mon, &source, nil
|
||||
}
|
||||
|
||||
func ParsePullSourceStarted(e partybus.Event) (progress.StagedProgressable, *monitor.GenericTask, error) {
|
||||
if err := checkEventType(e.Type, event.PullSourceStarted); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var mon progress.StagedProgressable
|
||||
|
||||
source, ok := e.Source.(monitor.GenericTask)
|
||||
if !ok {
|
||||
return nil, nil, newPayloadErr(e.Type, "Source", e.Source)
|
||||
}
|
||||
|
||||
mon, ok = e.Value.(progress.StagedProgressable)
|
||||
if !ok {
|
||||
mon = nil
|
||||
}
|
||||
|
||||
return mon, &source, nil
|
||||
}
|
||||
|
||||
func ParseAttestationStartedEvent(e partybus.Event) (io.Reader, progress.Progressable, *monitor.GenericTask, error) {
|
||||
if err := checkEventType(e.Type, event.AttestationStarted); err != nil {
|
||||
return nil, nil, nil, err
|
||||
|
||||
@@ -106,7 +106,7 @@ func processExecutableLocation(loc file.Location, resolver file.Resolver) (*file
|
||||
return processExecutable(loc, uReader)
|
||||
}
|
||||
|
||||
func catalogingProgress(locations int64) *monitor.CatalogerTaskProgress {
|
||||
func catalogingProgress(locations int64) *monitor.TaskProgress {
|
||||
info := monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: "Executables",
|
||||
|
||||
@@ -113,7 +113,7 @@ func (i *Cataloger) catalogLocation(resolver file.Resolver, location file.Locati
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func catalogingProgress(locations int64) *monitor.CatalogerTaskProgress {
|
||||
func catalogingProgress(locations int64) *monitor.TaskProgress {
|
||||
info := monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: "File contents",
|
||||
|
||||
@@ -112,7 +112,7 @@ func (i *Cataloger) catalogLocation(ctx context.Context, resolver file.Resolver,
|
||||
return digests, nil
|
||||
}
|
||||
|
||||
func catalogingProgress(locations int64) *monitor.CatalogerTaskProgress {
|
||||
func catalogingProgress(locations int64) *monitor.TaskProgress {
|
||||
info := monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: "File digests",
|
||||
|
||||
@@ -76,7 +76,7 @@ func (i *Cataloger) Catalog(ctx context.Context, resolver file.Resolver, coordin
|
||||
return results, errs
|
||||
}
|
||||
|
||||
func catalogingProgress(locations int64) *monitor.CatalogerTaskProgress {
|
||||
func catalogingProgress(locations int64) *monitor.TaskProgress {
|
||||
info := monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: "File metadata",
|
||||
|
||||
@@ -37,6 +37,7 @@ const (
|
||||
prefixImage = "Image"
|
||||
prefixDirectory = "Directory"
|
||||
prefixFile = "File"
|
||||
prefixSnap = "Snap"
|
||||
prefixUnknown = "Unknown"
|
||||
)
|
||||
|
||||
@@ -228,6 +229,18 @@ func toRootPackage(s source.Description) *spdx.Package {
|
||||
Value: d.Value,
|
||||
})
|
||||
}
|
||||
|
||||
case source.SnapMetadata:
|
||||
prefix = prefixSnap
|
||||
purpose = spdxPrimaryPurposeContainer
|
||||
|
||||
for _, d := range m.Digests {
|
||||
checksums = append(checksums, spdx.Checksum{
|
||||
Algorithm: toChecksumAlgorithm(d.Algorithm),
|
||||
Value: d.Value,
|
||||
})
|
||||
}
|
||||
|
||||
default:
|
||||
prefix = prefixUnknown
|
||||
purpose = spdxPrimaryPurposeOther
|
||||
|
||||
@@ -237,6 +237,82 @@ func Test_toFormatModel(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "snap",
|
||||
in: sbom.SBOM{
|
||||
Source: source.Description{
|
||||
Name: "etcd",
|
||||
Version: "3.4.36",
|
||||
Metadata: source.SnapMetadata{
|
||||
Summary: "Distributed reliable key-value store",
|
||||
Base: "core18",
|
||||
Grade: "stable",
|
||||
Confinement: "strict",
|
||||
Architectures: []string{
|
||||
"amd64",
|
||||
},
|
||||
Digests: []file.Digest{
|
||||
{
|
||||
Algorithm: "sha256",
|
||||
Value: "d34db33f",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Artifacts: sbom.Artifacts{
|
||||
Packages: pkg.NewCollection(pkg.Package{
|
||||
Name: "pkg-1",
|
||||
Version: "version-1",
|
||||
}),
|
||||
},
|
||||
},
|
||||
expected: &spdx.Document{
|
||||
SPDXIdentifier: "DOCUMENT",
|
||||
SPDXVersion: spdx.Version,
|
||||
DataLicense: spdx.DataLicense,
|
||||
DocumentName: "etcd",
|
||||
Packages: []*spdx.Package{
|
||||
{
|
||||
PackageSPDXIdentifier: "Package-pkg-1-pkg-1",
|
||||
PackageName: "pkg-1",
|
||||
PackageVersion: "version-1",
|
||||
PackageSupplier: &spdx.Supplier{
|
||||
Supplier: "NOASSERTION",
|
||||
},
|
||||
},
|
||||
{
|
||||
PackageSPDXIdentifier: "DocumentRoot-Snap-etcd",
|
||||
PackageName: "etcd",
|
||||
PackageVersion: "3.4.36",
|
||||
PrimaryPackagePurpose: "CONTAINER",
|
||||
PackageChecksums: []spdx.Checksum{{Algorithm: "SHA256", Value: "d34db33f"}},
|
||||
PackageSupplier: &spdx.Supplier{
|
||||
Supplier: "NOASSERTION",
|
||||
},
|
||||
},
|
||||
},
|
||||
Relationships: []*spdx.Relationship{
|
||||
{
|
||||
RefA: spdx.DocElementID{
|
||||
ElementRefID: "DocumentRoot-Snap-etcd",
|
||||
},
|
||||
RefB: spdx.DocElementID{
|
||||
ElementRefID: "Package-pkg-1-pkg-1",
|
||||
},
|
||||
Relationship: spdx.RelationshipContains,
|
||||
},
|
||||
{
|
||||
RefA: spdx.DocElementID{
|
||||
ElementRefID: "DOCUMENT",
|
||||
},
|
||||
RefB: spdx.DocElementID{
|
||||
ElementRefID: "DocumentRoot-Snap-etcd",
|
||||
},
|
||||
Relationship: spdx.RelationshipDescribes,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
"github.com/spdx/tools-golang/spdx"
|
||||
"github.com/spdx/tools-golang/spdx/v2/common"
|
||||
|
||||
@@ -45,7 +46,7 @@ func ToSyftModel(doc *spdx.Document) (*sbom.SBOM, error) {
|
||||
},
|
||||
}
|
||||
|
||||
collectSyftPackages(s, spdxIDMap, doc.Packages)
|
||||
collectSyftPackages(s, spdxIDMap, doc)
|
||||
|
||||
collectSyftFiles(s, spdxIDMap, doc)
|
||||
|
||||
@@ -279,8 +280,12 @@ func findLinuxReleaseByPURL(doc *spdx.Document) *linux.Release {
|
||||
return nil
|
||||
}
|
||||
|
||||
func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]any, packages []*spdx.Package) {
|
||||
for _, p := range packages {
|
||||
func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]any, doc *spdx.Document) {
|
||||
skipIDs := packageIDsToSkip(doc)
|
||||
for _, p := range doc.Packages {
|
||||
if p == nil || skipIDs.Has(string(p.PackageSPDXIdentifier)) {
|
||||
continue
|
||||
}
|
||||
syftPkg := toSyftPackage(p)
|
||||
spdxIDMap[string(p.PackageSPDXIdentifier)] = syftPkg
|
||||
s.Artifacts.Packages.Add(syftPkg)
|
||||
@@ -657,3 +662,15 @@ func extractCPEs(p *spdx.Package) (cpes []cpe.CPE) {
|
||||
}
|
||||
return cpes
|
||||
}
|
||||
|
||||
// packageIDsToSkip returns a set of packageIDs that should not be imported
|
||||
func packageIDsToSkip(doc *spdx.Document) *strset.Set {
|
||||
skipIDs := strset.New()
|
||||
for i := 0; i < len(doc.Relationships); i++ {
|
||||
r := doc.Relationships[i]
|
||||
if r != nil && r.Relationship == spdx.RelationshipGeneratedFrom {
|
||||
skipIDs.Add(string(r.RefB.ElementRefID))
|
||||
}
|
||||
}
|
||||
return skipIDs
|
||||
}
|
||||
|
||||
@@ -759,3 +759,37 @@ func Test_useSPDXIdentifierOverDerivedSyftArtifactID(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, s.Artifacts.Packages.Package("1"))
|
||||
}
|
||||
|
||||
func Test_skipsPackagesWithGeneratedFromRelationship(t *testing.T) {
|
||||
doc := &spdx.Document{
|
||||
SPDXVersion: "SPDX-2.3",
|
||||
Packages: []*spdx.Package{
|
||||
{
|
||||
PackageName: "package-1",
|
||||
PackageSPDXIdentifier: "1",
|
||||
PackageVersion: "1.0.5",
|
||||
},
|
||||
{
|
||||
PackageName: "package-1-src",
|
||||
PackageSPDXIdentifier: "1-src",
|
||||
PackageVersion: "1.0.5-src",
|
||||
},
|
||||
},
|
||||
Relationships: []*spdx.Relationship{
|
||||
{
|
||||
Relationship: spdx.RelationshipGeneratedFrom,
|
||||
RefA: common.DocElementID{ // package 1
|
||||
ElementRefID: spdx.ElementID("1"),
|
||||
},
|
||||
RefB: common.DocElementID{ // generated from package 1-src
|
||||
ElementRefID: spdx.ElementID("1-src"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
s, err := ToSyftModel(doc)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, s.Artifacts.Packages.Package("1"))
|
||||
assert.Nil(t, s.Artifacts.Packages.Package("1-src"))
|
||||
}
|
||||
|
||||
@@ -130,6 +130,11 @@ func toPath(s source.Description, p pkg.Package) string {
|
||||
return fmt.Sprintf("%s/%s", path, packagePath)
|
||||
}
|
||||
return packagePath
|
||||
case source.SnapMetadata:
|
||||
if inputPath != "" {
|
||||
return fmt.Sprintf("%s:/%s", inputPath, packagePath)
|
||||
}
|
||||
return packagePath
|
||||
}
|
||||
}
|
||||
return inputPath
|
||||
|
||||
@@ -173,6 +173,11 @@ func Test_toGithubModel(t *testing.T) {
|
||||
metadata: source.FileMetadata{Path: "./archive.tar.gz"},
|
||||
testPath: "archive.tar.gz:/etc",
|
||||
},
|
||||
{
|
||||
name: "snap",
|
||||
metadata: source.SnapMetadata{},
|
||||
testPath: "name:/etc",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -180,6 +185,7 @@ func Test_toGithubModel(t *testing.T) {
|
||||
s := sbomFixture()
|
||||
|
||||
if test.metadata != nil {
|
||||
s.Source.Name = "name"
|
||||
s.Source.Metadata = test.metadata
|
||||
}
|
||||
actual := ToGithubModel(&s)
|
||||
|
||||
@@ -71,31 +71,14 @@ func Backfill(p *pkg.Package) {
|
||||
}
|
||||
}
|
||||
|
||||
func setJavaMetadataFromPurl(p *pkg.Package, purl packageurl.PackageURL) {
|
||||
func setJavaMetadataFromPurl(p *pkg.Package, _ packageurl.PackageURL) {
|
||||
if p.Type != pkg.JavaPkg {
|
||||
return
|
||||
}
|
||||
if purl.Namespace != "" {
|
||||
if p.Metadata == nil {
|
||||
p.Metadata = pkg.JavaArchive{}
|
||||
}
|
||||
meta, got := p.Metadata.(pkg.JavaArchive)
|
||||
if got && meta.PomProperties == nil {
|
||||
meta.PomProperties = &pkg.JavaPomProperties{}
|
||||
p.Metadata = meta
|
||||
}
|
||||
if meta.PomProperties != nil {
|
||||
// capture the group id from the purl if it is not already set
|
||||
if meta.PomProperties.ArtifactID == "" {
|
||||
meta.PomProperties.ArtifactID = purl.Name
|
||||
}
|
||||
if meta.PomProperties.GroupID == "" {
|
||||
meta.PomProperties.GroupID = purl.Namespace
|
||||
}
|
||||
if meta.PomProperties.Version == "" {
|
||||
meta.PomProperties.Version = purl.Version
|
||||
}
|
||||
}
|
||||
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{}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -101,13 +101,9 @@ func Test_Backfill(t *testing.T) {
|
||||
Language: pkg.Java,
|
||||
Name: "some-thing",
|
||||
Version: "1.2.3",
|
||||
Metadata: pkg.JavaArchive{
|
||||
PomProperties: &pkg.JavaPomProperties{
|
||||
GroupID: "org.apache",
|
||||
ArtifactID: "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{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package helpers
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
@@ -90,15 +91,19 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package {
|
||||
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)
|
||||
}
|
||||
@@ -111,6 +116,41 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package {
|
||||
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 != "" {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,15 @@ func Test_DocumentName(t *testing.T) {
|
||||
},
|
||||
expected: "some/path/to/place",
|
||||
},
|
||||
{
|
||||
name: "snap",
|
||||
srcMetadata: source.Description{
|
||||
Name: "some/name",
|
||||
// there is nothing in the snap metadata that indicates a name
|
||||
Metadata: source.SnapMetadata{},
|
||||
},
|
||||
expected: "some/name",
|
||||
},
|
||||
{
|
||||
name: "named",
|
||||
srcMetadata: source.Description{
|
||||
|
||||
@@ -16,6 +16,7 @@ const (
|
||||
InputImage = "image"
|
||||
InputDirectory = "dir"
|
||||
InputFile = "file"
|
||||
InputSnap = "snap"
|
||||
)
|
||||
|
||||
func DocumentNameAndNamespace(src source.Description, desc sbom.Descriptor) (string, string) {
|
||||
@@ -33,6 +34,8 @@ func DocumentNamespace(name string, src source.Description, desc sbom.Descriptor
|
||||
input = InputDirectory
|
||||
case source.FileMetadata:
|
||||
input = InputFile
|
||||
case source.SnapMetadata:
|
||||
input = InputSnap
|
||||
}
|
||||
|
||||
uniqueID := uuid.Must(uuid.NewRandom())
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func Test_documentNamespace(t *testing.T) {
|
||||
func Test_DocumentNamespace(t *testing.T) {
|
||||
tracker := sourcemetadata.NewCompletionTester(t)
|
||||
|
||||
tests := []struct {
|
||||
@@ -53,6 +53,14 @@ func Test_documentNamespace(t *testing.T) {
|
||||
},
|
||||
expected: "https://anchore.com/syft/file/my-name-",
|
||||
},
|
||||
{
|
||||
name: "snap",
|
||||
inputName: "my-name",
|
||||
src: source.Description{
|
||||
Metadata: source.SnapMetadata{},
|
||||
},
|
||||
expected: "https://anchore.com/syft/snap/my-name-",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -112,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{
|
||||
|
||||
@@ -68,15 +68,15 @@ func AssertEncoderAgainstGoldenSnapshot(t *testing.T, cfg EncoderSnapshotTestCon
|
||||
if cfg.IsJSON {
|
||||
require.JSONEq(t, string(expected), string(actual))
|
||||
} else {
|
||||
requireEqual(t, expected, actual)
|
||||
requireEqual(t, string(expected), string(actual))
|
||||
}
|
||||
}
|
||||
|
||||
func requireEqual(t *testing.T, expected any, actual any) {
|
||||
func requireEqual(t *testing.T, expected string, actual string) {
|
||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||
// uncomment to debug
|
||||
// t.Logf("expected: %s", expected)
|
||||
// t.Logf("actual: %s", actual)
|
||||
// t.Logf("expected:\n%s", expected)
|
||||
// t.Logf("actual:\n%s", actual)
|
||||
t.Fatalf("mismatched output: %s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,13 +165,9 @@ func TestDecoder_Decode(t *testing.T) {
|
||||
Type: pkg.JavaPkg,
|
||||
PURL: "pkg:maven/org.apache/some-pkg@4.11.3",
|
||||
Language: pkg.Java,
|
||||
Metadata: pkg.JavaArchive{
|
||||
PomProperties: &pkg.JavaPomProperties{
|
||||
GroupID: "org.apache",
|
||||
ArtifactID: "some-pkg",
|
||||
Version: "4.11.3",
|
||||
},
|
||||
},
|
||||
// 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{},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -114,12 +114,12 @@ func TestOutOfDateParser(t *testing.T) {
|
||||
name: "warning when document version is unparseable",
|
||||
documentVersion: "some-nonsense",
|
||||
parserVersion: "3.1.0",
|
||||
want: fmt.Errorf("error comparing document schema version with parser schema version: %w", errors.New("Invalid Semantic Version")),
|
||||
want: fmt.Errorf("error comparing document schema version with parser schema version: %w", errors.New("invalid semantic version")),
|
||||
}, {
|
||||
name: "warning when parser version is unparseable",
|
||||
documentVersion: "7.1.0",
|
||||
parserVersion: "some-nonsense",
|
||||
want: fmt.Errorf("error comparing document schema version with parser schema version: %w", errors.New("Invalid Semantic Version")),
|
||||
want: fmt.Errorf("error comparing document schema version with parser schema version: %w", errors.New("invalid semantic version")),
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Document represents the syft cataloging findings as a JSON document
|
||||
type Document struct {
|
||||
Artifacts []Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
|
||||
@@ -11,6 +16,25 @@ type Document struct {
|
||||
Schema Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape
|
||||
}
|
||||
|
||||
func (d *Document) UnmarshalJSON(data []byte) error {
|
||||
type Alias *Document
|
||||
aux := Alias(d)
|
||||
|
||||
if err := json.Unmarshal(data, aux); err != nil {
|
||||
return fmt.Errorf("could not unmarshal syft JSON document: %w", err)
|
||||
}
|
||||
|
||||
// in previous versions of anchorectl, the file modes were stored as decimal values instead of octal.
|
||||
if d.Schema.Version == "1.0.0" && d.Descriptor.Name == "anchorectl" {
|
||||
// convert all file modes from decimal to octal
|
||||
for i := range d.Files {
|
||||
d.Files[i].Metadata.Mode = convertBase10ToBase8(d.Files[i].Metadata.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Descriptor describes what created the document as well as surrounding metadata
|
||||
type Descriptor struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDocumentUnmarshalJSON_SchemaDetection(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
jsonData string
|
||||
modes []int
|
||||
}{
|
||||
{
|
||||
name: "schema version 1.0.0 + anchorectl",
|
||||
jsonData: `{
|
||||
"files": [
|
||||
{"metadata": {"mode": 493}},
|
||||
{"metadata": {"mode": 420}}
|
||||
],
|
||||
"schema": {"version": "1.0.0"},
|
||||
"descriptor": {
|
||||
"name": "anchorectl"
|
||||
}
|
||||
}`,
|
||||
modes: []int{755, 644},
|
||||
},
|
||||
{
|
||||
name: "schema version 1.0.0 + syft",
|
||||
jsonData: `{
|
||||
"files": [
|
||||
{"metadata": {"mode": 755}},
|
||||
{"metadata": {"mode": 644}}
|
||||
],
|
||||
"schema": {"version": "1.0.0"},
|
||||
"descriptor": {
|
||||
"name": "syft"
|
||||
}
|
||||
}`,
|
||||
modes: []int{755, 644},
|
||||
},
|
||||
{
|
||||
name: "schema version 2.0.0 + anchorectl",
|
||||
jsonData: `{
|
||||
"files": [
|
||||
{"metadata": {"mode": 755}},
|
||||
{"metadata": {"mode": 644}}
|
||||
],
|
||||
"schema": {"version": "2.0.0"},
|
||||
"descriptor": {
|
||||
"name": "anchorectl"
|
||||
}
|
||||
}`,
|
||||
modes: []int{755, 644},
|
||||
},
|
||||
{
|
||||
name: "missing schema version should not convert modes",
|
||||
jsonData: `{
|
||||
"files": [
|
||||
{"metadata": {"mode": 755}}
|
||||
],
|
||||
"schema": {}
|
||||
}`,
|
||||
modes: []int{755},
|
||||
},
|
||||
{
|
||||
name: "empty files array with version 1.0.0",
|
||||
jsonData: `{
|
||||
"files": [],
|
||||
"schema": {"version": "1.0.0"},
|
||||
"descriptor": {
|
||||
"name": "anchorectl"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var doc Document
|
||||
|
||||
err := json.Unmarshal([]byte(tt.jsonData), &doc)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to unmarshal JSON: %v", err)
|
||||
}
|
||||
|
||||
var modes []int
|
||||
for _, file := range doc.Files {
|
||||
modes = append(modes, file.Metadata.Mode)
|
||||
}
|
||||
|
||||
require.Len(t, doc.Files, len(tt.modes), "Unexpected number of files")
|
||||
assert.Equal(t, tt.modes, modes, "File modes do not match expected values")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
stereoFile "github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/license"
|
||||
)
|
||||
@@ -26,6 +31,57 @@ type FileMetadataEntry struct {
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
type auxFileMetadataEntry FileMetadataEntry
|
||||
type fileMetadataEntryWithLegacyHint struct {
|
||||
*auxFileMetadataEntry `json:",inline"`
|
||||
LegacyHint any `json:"FileInfo"`
|
||||
}
|
||||
|
||||
func (f *FileMetadataEntry) UnmarshalJSON(data []byte) error {
|
||||
aux := fileMetadataEntryWithLegacyHint{
|
||||
auxFileMetadataEntry: (*auxFileMetadataEntry)(f),
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err == nil {
|
||||
fieldsSpecified := f.Mode != 0 || f.Type != "" || f.LinkDestination != "" ||
|
||||
f.UserID != 0 || f.GroupID != 0 || f.MIMEType != "" || f.Size != 0
|
||||
if aux.LegacyHint == nil && fieldsSpecified {
|
||||
// we should have at least one field set to a non-zero value... (this is not a legacy shape)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var legacy sbomImportLegacyFileMetadataEntry
|
||||
if err := json.Unmarshal(data, &legacy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !legacy.Type.WasInt {
|
||||
// this occurs for document shapes from a non-import path and indicates that the mode has already been converted to octal.
|
||||
// That being said, we want to handle all legacy shapes the same, so we will convert this to base 10 for consistency.
|
||||
legacy.Mode = convertBase8ToBase10(legacy.Mode)
|
||||
}
|
||||
|
||||
f.Mode = legacy.Mode
|
||||
f.Type = legacy.Type.Value
|
||||
f.LinkDestination = legacy.LinkDestination
|
||||
f.UserID = legacy.UserID
|
||||
f.GroupID = legacy.GroupID
|
||||
f.MIMEType = legacy.MIMEType
|
||||
f.Size = legacy.Size
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type sbomImportLegacyFileMetadataEntry struct {
|
||||
Mode int `json:"Mode"`
|
||||
Type intOrStringFileType `json:"Type"`
|
||||
LinkDestination string `json:"LinkDestination"`
|
||||
UserID int `json:"UserID"`
|
||||
GroupID int `json:"GroupID"`
|
||||
MIMEType string `json:"MIMEType"`
|
||||
Size int64 `json:"Size"`
|
||||
}
|
||||
|
||||
type FileLicense struct {
|
||||
Value string `json:"value"`
|
||||
SPDXExpression string `json:"spdxExpression"`
|
||||
@@ -38,3 +94,39 @@ type FileLicenseEvidence struct {
|
||||
Offset int `json:"offset"`
|
||||
Extent int `json:"extent"`
|
||||
}
|
||||
|
||||
type intOrStringFileType struct {
|
||||
Value string
|
||||
WasInt bool
|
||||
}
|
||||
|
||||
func (lt *intOrStringFileType) UnmarshalJSON(data []byte) error {
|
||||
var str string
|
||||
if err := json.Unmarshal(data, &str); err == nil {
|
||||
lt.Value = str
|
||||
return nil
|
||||
}
|
||||
|
||||
var num stereoFile.Type
|
||||
if err := json.Unmarshal(data, &num); err != nil {
|
||||
return fmt.Errorf("file.Type must be either string or int, got: %s", string(data))
|
||||
}
|
||||
|
||||
lt.Value = num.String()
|
||||
lt.WasInt = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertBase10ToBase8(rawMode int) int {
|
||||
octalStr := fmt.Sprintf("%o", rawMode)
|
||||
// we don't need to check that this is a valid octal string since the input is always an integer
|
||||
result, _ := strconv.Atoi(octalStr)
|
||||
return result
|
||||
}
|
||||
|
||||
func convertBase8ToBase10(octalMode int) int {
|
||||
octalStr := strconv.Itoa(octalMode)
|
||||
result, _ := strconv.ParseInt(octalStr, 8, 64)
|
||||
|
||||
return int(result)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_FileMetadataEntry_UnmarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
jsonData []byte
|
||||
expected FileMetadataEntry
|
||||
}{
|
||||
{
|
||||
name: "unmarshal current format",
|
||||
jsonData: []byte(`{
|
||||
"mode": 644,
|
||||
"type": "RegularFile",
|
||||
"linkDestination": "/usr/bin/python3",
|
||||
"userID": 1000,
|
||||
"groupID": 1000,
|
||||
"mimeType": "text/plain",
|
||||
"size": 10174
|
||||
}`),
|
||||
expected: FileMetadataEntry{
|
||||
Mode: 644,
|
||||
Type: "RegularFile",
|
||||
LinkDestination: "/usr/bin/python3",
|
||||
UserID: 1000,
|
||||
GroupID: 1000,
|
||||
MIMEType: "text/plain",
|
||||
Size: 10174,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unmarshal legacy image add internal document format",
|
||||
jsonData: []byte(`{
|
||||
"FileInfo": {},
|
||||
"Mode": 644,
|
||||
"Type": "RegularFile",
|
||||
"LinkDestination": "/usr/bin/python3",
|
||||
"UserID": 1000,
|
||||
"GroupID": 1000,
|
||||
"MIMEType": "text/plain",
|
||||
"Size": 10174
|
||||
}`),
|
||||
expected: FileMetadataEntry{
|
||||
Mode: 420, // important! we convert this to base 10 so that all documents are consistent
|
||||
Type: "RegularFile",
|
||||
LinkDestination: "/usr/bin/python3",
|
||||
UserID: 1000,
|
||||
GroupID: 1000,
|
||||
MIMEType: "text/plain",
|
||||
Size: 10174,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unmarshal legacy sbom import format",
|
||||
jsonData: []byte(`{
|
||||
"FileInfo": {},
|
||||
"Mode": 644,
|
||||
"Type": 0,
|
||||
"LinkDestination": "/usr/bin/python3",
|
||||
"UserID": 1000,
|
||||
"GroupID": 1000,
|
||||
"MIMEType": "text/plain",
|
||||
"Size": 10174
|
||||
}`),
|
||||
expected: FileMetadataEntry{
|
||||
Mode: 644,
|
||||
Type: "RegularFile",
|
||||
LinkDestination: "/usr/bin/python3",
|
||||
UserID: 1000,
|
||||
GroupID: 1000,
|
||||
MIMEType: "text/plain",
|
||||
Size: 10174,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unmarshal minimal current format",
|
||||
jsonData: []byte(`{
|
||||
"mode": 0,
|
||||
"type": "RegularFile",
|
||||
"userID": 0,
|
||||
"groupID": 0,
|
||||
"size": 0
|
||||
}`),
|
||||
expected: FileMetadataEntry{
|
||||
Type: "RegularFile",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unmarshal minimal legacy format",
|
||||
jsonData: []byte(`{
|
||||
"FileInfo": {},
|
||||
"Mode": 0,
|
||||
"Type": "RegularFile",
|
||||
"UserID": 0,
|
||||
"GroupID": 0,
|
||||
"Size": 0
|
||||
}`),
|
||||
expected: FileMetadataEntry{
|
||||
Type: "RegularFile",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var actual FileMetadataEntry
|
||||
err := actual.UnmarshalJSON(test.jsonData)
|
||||
require.NoError(t, err)
|
||||
|
||||
if diff := cmp.Diff(test.expected, actual); diff != "" {
|
||||
t.Errorf("FileMetadataEntry mismatch (-expected +actual):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_intOrStringFileType_UnmarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
jsonData []byte
|
||||
expected string
|
||||
expectedWasInt bool
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
// string inputs - should pass through unchanged
|
||||
{
|
||||
name: "string RegularFile",
|
||||
jsonData: []byte(`"RegularFile"`),
|
||||
expected: "RegularFile",
|
||||
},
|
||||
{
|
||||
name: "string HardLink",
|
||||
jsonData: []byte(`"HardLink"`),
|
||||
expected: "HardLink",
|
||||
},
|
||||
{
|
||||
name: "string Directory",
|
||||
jsonData: []byte(`"Directory"`),
|
||||
expected: "Directory",
|
||||
},
|
||||
{
|
||||
name: "string custom value",
|
||||
jsonData: []byte(`"CustomFileType"`),
|
||||
expected: "CustomFileType",
|
||||
},
|
||||
// integer inputs - should convert to string representation
|
||||
{
|
||||
name: "int 0 (TypeRegular)",
|
||||
jsonData: []byte(`0`),
|
||||
expected: "RegularFile",
|
||||
expectedWasInt: true,
|
||||
},
|
||||
{
|
||||
name: "int 1 (TypeHardLink)",
|
||||
jsonData: []byte(`1`),
|
||||
expected: "HardLink",
|
||||
expectedWasInt: true,
|
||||
},
|
||||
{
|
||||
name: "int 2 (TypeSymLink)",
|
||||
jsonData: []byte(`2`),
|
||||
expected: "SymbolicLink",
|
||||
expectedWasInt: true,
|
||||
},
|
||||
{
|
||||
name: "int 3 (TypeCharacterDevice)",
|
||||
jsonData: []byte(`3`),
|
||||
expected: "CharacterDevice",
|
||||
expectedWasInt: true,
|
||||
},
|
||||
{
|
||||
name: "int 4 (TypeBlockDevice)",
|
||||
jsonData: []byte(`4`),
|
||||
expected: "BlockDevice",
|
||||
expectedWasInt: true,
|
||||
},
|
||||
{
|
||||
name: "int 5 (TypeDirectory)",
|
||||
jsonData: []byte(`5`),
|
||||
expected: "Directory",
|
||||
expectedWasInt: true,
|
||||
},
|
||||
{
|
||||
name: "int 6 (TypeFIFO)",
|
||||
jsonData: []byte(`6`),
|
||||
expected: "FIFONode",
|
||||
expectedWasInt: true,
|
||||
},
|
||||
{
|
||||
name: "int 7 (TypeSocket)",
|
||||
jsonData: []byte(`7`),
|
||||
expected: "Socket",
|
||||
expectedWasInt: true,
|
||||
},
|
||||
{
|
||||
name: "int 8 (TypeIrregular)",
|
||||
jsonData: []byte(`8`),
|
||||
expected: "IrregularFile",
|
||||
expectedWasInt: true,
|
||||
},
|
||||
{
|
||||
name: "unknown int",
|
||||
jsonData: []byte(`99`),
|
||||
expected: "Unknown",
|
||||
expectedWasInt: true,
|
||||
},
|
||||
{
|
||||
name: "negative int",
|
||||
jsonData: []byte(`-1`),
|
||||
expected: "Unknown",
|
||||
expectedWasInt: true,
|
||||
},
|
||||
{
|
||||
name: "null value",
|
||||
jsonData: []byte(`null`),
|
||||
},
|
||||
{
|
||||
name: "invalid JSON",
|
||||
jsonData: []byte(`{`),
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "boolean value",
|
||||
jsonData: []byte(`true`),
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "array value",
|
||||
jsonData: []byte(`[]`),
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "object value",
|
||||
jsonData: []byte(`{}`),
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "float value",
|
||||
jsonData: []byte(`1.5`),
|
||||
wantErr: require.Error,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if test.wantErr == nil {
|
||||
test.wantErr = require.NoError
|
||||
}
|
||||
var ft intOrStringFileType
|
||||
err := ft.UnmarshalJSON(test.jsonData)
|
||||
test.wantErr(t, err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, test.expected, ft.Value)
|
||||
assert.Equal(t, test.expectedWasInt, ft.WasInt)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_convertBase10ToBase8(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input int
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
name: "no permissions",
|
||||
input: 0,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "symlink + rwxrwxrwx",
|
||||
input: 134218239,
|
||||
expected: 1000000777,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual := convertBase10ToBase8(tt.input)
|
||||
|
||||
require.Equal(t, tt.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_convertBase8ToBase10(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input int
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
name: "no permissions",
|
||||
input: 0,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "symlink + rwxrwxrwx",
|
||||
input: 1000000777,
|
||||
expected: 134218239,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual := convertBase8ToBase10(tt.input)
|
||||
|
||||
require.Equal(t, tt.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,51 @@ func TestSource_UnmarshalJSON(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "snap",
|
||||
input: []byte(`{
|
||||
"id": "foobar",
|
||||
"name": "etcd",
|
||||
"version": "3.4.36",
|
||||
"type": "snap",
|
||||
"metadata": {
|
||||
"summary": "Distributed reliable key-value store",
|
||||
"base": "core18",
|
||||
"grade": "stable",
|
||||
"confinement": "strict",
|
||||
"architectures": [
|
||||
"amd64"
|
||||
],
|
||||
"digests": [
|
||||
{
|
||||
"algorithm": "sha256",
|
||||
"value": "6700d789d2c38b0f7513058ddcea8f9a275e2206b4621a772eb065e12069956e"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`),
|
||||
expected: &Source{
|
||||
ID: "foobar",
|
||||
Name: "etcd",
|
||||
Version: "3.4.36",
|
||||
Type: "snap",
|
||||
Metadata: source.SnapMetadata{
|
||||
Summary: "Distributed reliable key-value store",
|
||||
Base: "core18",
|
||||
Grade: "stable",
|
||||
Confinement: "strict",
|
||||
Architectures: []string{
|
||||
"amd64",
|
||||
},
|
||||
Digests: []file.Digest{
|
||||
{
|
||||
Algorithm: "sha256",
|
||||
Value: "6700d789d2c38b0f7513058ddcea8f9a275e2206b4621a772eb065e12069956e",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unknown source type",
|
||||
input: []byte(`{
|
||||
|
||||
@@ -127,6 +127,36 @@ func Test_toSourceModel(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "snap",
|
||||
src: source.Description{
|
||||
ID: "test-id",
|
||||
Name: "some-name",
|
||||
Version: "some-version",
|
||||
Metadata: source.SnapMetadata{
|
||||
Summary: "some summary",
|
||||
Base: "some/base",
|
||||
Grade: "some grade",
|
||||
Confinement: "some confinement",
|
||||
Architectures: []string{"x86_64", "arm64"},
|
||||
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
||||
},
|
||||
},
|
||||
expected: model.Source{
|
||||
ID: "test-id",
|
||||
Name: "some-name",
|
||||
Version: "some-version",
|
||||
Type: "snap",
|
||||
Metadata: source.SnapMetadata{
|
||||
Summary: "some summary",
|
||||
Base: "some/base",
|
||||
Grade: "some grade",
|
||||
Confinement: "some confinement",
|
||||
Architectures: []string{"x86_64", "arm64"},
|
||||
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
// below are regression tests for when the name/version are not provided
|
||||
// historically we've hoisted up the name/version from the metadata, now it is a simple pass-through
|
||||
{
|
||||
|
||||
@@ -144,13 +144,12 @@ func toSyftFiles(files []model.File) sbom.Artifacts {
|
||||
}
|
||||
|
||||
func safeFileModeConvert(val int) (fs.FileMode, error) {
|
||||
if val < math.MinInt32 || val > math.MaxInt32 {
|
||||
// Value is out of the range that int32 can represent
|
||||
mode, err := strconv.ParseInt(strconv.Itoa(val), 8, 64)
|
||||
if mode < 0 || mode > math.MaxUint32 {
|
||||
// value is out of the range that int32 can represent
|
||||
return 0, fmt.Errorf("value %d is out of the range that int32 can represent", val)
|
||||
}
|
||||
|
||||
// Safe to convert to os.FileMode
|
||||
mode, err := strconv.ParseInt(strconv.Itoa(val), 8, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -100,6 +100,36 @@ func Test_toSyftSourceData(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "snap",
|
||||
src: model.Source{
|
||||
ID: "the-id",
|
||||
Name: "some-name",
|
||||
Version: "some-version",
|
||||
Type: "snap",
|
||||
Metadata: source.SnapMetadata{
|
||||
Summary: "something!",
|
||||
Base: "base!",
|
||||
Grade: "grade!",
|
||||
Confinement: "confined!",
|
||||
Architectures: []string{"arch!"},
|
||||
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest!"}},
|
||||
},
|
||||
},
|
||||
expected: &source.Description{
|
||||
ID: "the-id",
|
||||
Name: "some-name",
|
||||
Version: "some-version",
|
||||
Metadata: source.SnapMetadata{
|
||||
Summary: "something!",
|
||||
Base: "base!",
|
||||
Grade: "grade!",
|
||||
Confinement: "confined!",
|
||||
Architectures: []string{"arch!"},
|
||||
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest!"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
// below are regression tests for when the name/version are not provided
|
||||
// historically we've hoisted up the name/version from the metadata, now it is a simple pass-through
|
||||
{
|
||||
@@ -491,6 +521,12 @@ func Test_safeFileModeConvert(t *testing.T) {
|
||||
want: os.FileMode(511), // 777 in octal equals 511 in decimal
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid perm with symlink type",
|
||||
val: 1000000777, // symlink + rwxrwxrwx
|
||||
want: os.FileMode(0o1000000777), // 134218239
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "outside int32 high",
|
||||
val: int(math.MaxInt32) + 1,
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/renderer"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
@@ -66,25 +68,90 @@ func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error {
|
||||
columns = append(columns, "") // add a column for duplicate annotations
|
||||
rows = markDuplicateRows(rows)
|
||||
|
||||
table := tablewriter.NewWriter(writer)
|
||||
table := newTableWriter(writer, columns)
|
||||
|
||||
table.SetHeader(columns)
|
||||
table.SetHeaderLine(false)
|
||||
table.SetBorder(false)
|
||||
table.SetAutoWrapText(false)
|
||||
table.SetAutoFormatHeaders(true)
|
||||
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetCenterSeparator("")
|
||||
table.SetColumnSeparator("")
|
||||
table.SetRowSeparator("")
|
||||
table.SetTablePadding(" ")
|
||||
table.SetNoWhiteSpace(true)
|
||||
if err := table.Bulk(rows); err != nil {
|
||||
return fmt.Errorf("failed to add table rows: %w", err)
|
||||
}
|
||||
|
||||
table.AppendBulk(rows)
|
||||
table.Render()
|
||||
return table.Render()
|
||||
}
|
||||
|
||||
return nil
|
||||
func newTableWriter(writer io.Writer, columns []string) *tablewriter.Table {
|
||||
// Here’s a simplified diagram of a table with a header, rows, and footer:
|
||||
//
|
||||
// [Borders.Top]
|
||||
// | Header1 | Header2 | (Line below header: Lines.ShowTop)
|
||||
// [Separators.BetweenRows]
|
||||
// | Row1 | Row1 |
|
||||
// [Separators.BetweenRows]
|
||||
// | Row2 | Row2 |
|
||||
// [Lines.ShowBottom]
|
||||
// | Footer1 | Footer2 |
|
||||
// [Borders.Bottom]
|
||||
//
|
||||
// So for example:
|
||||
// ┌──────┬─────┐ <- Borders.Top
|
||||
// │ NAME │ AGE │
|
||||
// ├──────┼─────┤ <- Lines.ShowTop
|
||||
// │ Alice│ 25 │
|
||||
// ├──────┼─────┤ <- Separators.BetweenRows
|
||||
// │ Bob │ 30 │
|
||||
// ├──────┼─────┤ <- Lines.ShowBottom
|
||||
// │ Total│ 2 │
|
||||
// └──────┴─────┘ <- Borders.Bottom
|
||||
|
||||
return tablewriter.NewTable(writer,
|
||||
tablewriter.WithHeader(columns),
|
||||
tablewriter.WithHeaderAutoFormat(tw.On),
|
||||
tablewriter.WithHeaderAutoWrap(tw.WrapNone),
|
||||
tablewriter.WithHeaderAlignment(tw.AlignLeft),
|
||||
tablewriter.WithRowAutoFormat(tw.Off),
|
||||
tablewriter.WithRowAutoWrap(tw.WrapNone),
|
||||
tablewriter.WithRowAlignment(tw.AlignLeft),
|
||||
tablewriter.WithTrimSpace(tw.On),
|
||||
tablewriter.WithAutoHide(tw.On),
|
||||
tablewriter.WithRenderer(renderer.NewBlueprint()),
|
||||
tablewriter.WithBehavior(
|
||||
tw.Behavior{
|
||||
TrimSpace: tw.On,
|
||||
AutoHide: tw.On,
|
||||
},
|
||||
),
|
||||
tablewriter.WithPadding(
|
||||
tw.Padding{
|
||||
Left: "",
|
||||
Right: " ",
|
||||
Top: "",
|
||||
Bottom: "",
|
||||
},
|
||||
),
|
||||
tablewriter.WithRendition(
|
||||
tw.Rendition{
|
||||
Symbols: tw.NewSymbols(tw.StyleNone),
|
||||
Borders: tw.Border{
|
||||
Left: tw.Off,
|
||||
Top: tw.Off,
|
||||
Right: tw.Off,
|
||||
Bottom: tw.Off,
|
||||
},
|
||||
Settings: tw.Settings{
|
||||
Separators: tw.Separators{
|
||||
ShowHeader: tw.Off,
|
||||
ShowFooter: tw.Off,
|
||||
BetweenRows: tw.Off,
|
||||
BetweenColumns: tw.Off,
|
||||
},
|
||||
Lines: tw.Lines{
|
||||
ShowTop: tw.Off,
|
||||
ShowBottom: tw.Off,
|
||||
ShowHeaderLine: tw.Off,
|
||||
ShowFooterLine: tw.Off,
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func markDuplicateRows(items [][]string) [][]string {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
NAME VERSION TYPE
|
||||
package-1 1.0.1 python
|
||||
package-2 2.0.1 deb
|
||||
NAME VERSION TYPE
|
||||
package-1 1.0.1 python
|
||||
package-2 2.0.1 deb
|
||||
|
||||
+27
-9
@@ -34,26 +34,44 @@ func GetSource(ctx context.Context, userInput string, cfg *GetSourceConfig) (sou
|
||||
errs = append(errs, fmt.Errorf("%s: %w", p.Name(), err))
|
||||
}
|
||||
}
|
||||
if err := validateSourcePlatform(src, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if src != nil {
|
||||
// if we have a non-image type and platform is specified, it's an error
|
||||
if cfg.SourceProviderConfig.Platform != nil {
|
||||
meta := src.Describe().Metadata
|
||||
switch meta.(type) {
|
||||
case *source.ImageMetadata, source.ImageMetadata:
|
||||
default:
|
||||
return src, fmt.Errorf("platform specified with non-image source")
|
||||
}
|
||||
}
|
||||
return src, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
return nil, fmt.Errorf("no source providers were able to resolve the input %q", userInput)
|
||||
}
|
||||
|
||||
if len(fileNotFoundProviders) > 0 {
|
||||
errs = append(errs, fmt.Errorf("additionally, the following providers failed with %w: %s", os.ErrNotExist, strings.Join(fileNotFoundProviders, ", ")))
|
||||
}
|
||||
|
||||
return nil, sourceError(userInput, errs...)
|
||||
}
|
||||
|
||||
func validateSourcePlatform(src source.Source, cfg *GetSourceConfig) error {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
if cfg == nil || cfg.SourceProviderConfig == nil || cfg.SourceProviderConfig.Platform == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
meta := src.Describe().Metadata
|
||||
switch meta.(type) {
|
||||
case *source.ImageMetadata, source.ImageMetadata:
|
||||
return nil
|
||||
case *source.SnapMetadata, source.SnapMetadata:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("platform is not supported for this source type")
|
||||
}
|
||||
}
|
||||
|
||||
func sourceError(userInput string, errs ...error) error {
|
||||
switch len(errs) {
|
||||
case 0:
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package syft
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/syft/internal/sourcemetadata"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/anchore/syft/syft/source/sourceproviders"
|
||||
)
|
||||
|
||||
type mockSource struct {
|
||||
source.Source
|
||||
desc source.Description
|
||||
}
|
||||
|
||||
func (s mockSource) Describe() source.Description {
|
||||
return s.desc
|
||||
}
|
||||
|
||||
func TestValidateSourcePlatform_NilSource(t *testing.T) {
|
||||
cfg := &GetSourceConfig{
|
||||
SourceProviderConfig: &sourceproviders.Config{
|
||||
Platform: &image.Platform{
|
||||
Architecture: "amd64",
|
||||
OS: "linux",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := validateSourcePlatform(nil, cfg)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for nil source, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSourcePlatform_NilPlatformConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *GetSourceConfig
|
||||
}{
|
||||
{
|
||||
name: "nil config",
|
||||
cfg: nil,
|
||||
},
|
||||
{
|
||||
name: "nil SourceProviderConfig",
|
||||
cfg: &GetSourceConfig{
|
||||
SourceProviderConfig: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil Platform",
|
||||
cfg: &GetSourceConfig{
|
||||
SourceProviderConfig: &sourceproviders.Config{
|
||||
Platform: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
src := mockSource{
|
||||
desc: source.Description{
|
||||
Metadata: &source.ImageMetadata{},
|
||||
},
|
||||
}
|
||||
|
||||
err := validateSourcePlatform(src, tt.cfg)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for nil platform, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSourcePlatform_SupportedMetadataTypes(t *testing.T) {
|
||||
tracker := sourcemetadata.NewCompletionTester(t)
|
||||
cfg := &GetSourceConfig{
|
||||
SourceProviderConfig: &sourceproviders.Config{
|
||||
Platform: &image.Platform{
|
||||
Architecture: "amd64",
|
||||
OS: "linux",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
metadata any
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "image",
|
||||
metadata: source.ImageMetadata{},
|
||||
},
|
||||
{
|
||||
name: "snap",
|
||||
metadata: source.SnapMetadata{},
|
||||
},
|
||||
{
|
||||
name: "dir",
|
||||
metadata: source.DirectoryMetadata{},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "file",
|
||||
metadata: source.FileMetadata{},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.wantErr == nil {
|
||||
tt.wantErr = require.NoError
|
||||
}
|
||||
tracker.Tested(t, tt.metadata)
|
||||
|
||||
src := mockSource{
|
||||
desc: source.Description{
|
||||
Metadata: tt.metadata,
|
||||
},
|
||||
}
|
||||
|
||||
err := validateSourcePlatform(src, cfg)
|
||||
tt.wantErr(t, err, "Expected no error for %s, got: %v", tt.name, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSourcePlatform_UnsupportedMetadataTypes(t *testing.T) {
|
||||
cfg := &GetSourceConfig{
|
||||
SourceProviderConfig: &sourceproviders.Config{
|
||||
Platform: &image.Platform{
|
||||
Architecture: "amd64",
|
||||
OS: "linux",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
metadata interface{}
|
||||
}{
|
||||
{
|
||||
name: "string metadata",
|
||||
metadata: "unsupported",
|
||||
},
|
||||
{
|
||||
name: "int metadata",
|
||||
metadata: 42,
|
||||
},
|
||||
{
|
||||
name: "nil metadata",
|
||||
metadata: nil,
|
||||
},
|
||||
{
|
||||
name: "custom struct",
|
||||
metadata: struct{ Name string }{Name: "test"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
src := mockSource{
|
||||
desc: source.Description{
|
||||
Metadata: tt.metadata,
|
||||
},
|
||||
}
|
||||
|
||||
err := validateSourcePlatform(src, cfg)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for %s, got nil", tt.name)
|
||||
}
|
||||
|
||||
expectedMsg := "platform is not supported for this source type"
|
||||
if err.Error() != expectedMsg {
|
||||
t.Errorf("Expected error message %q, got %q", expectedMsg, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSourcePlatform_ValidCombination(t *testing.T) {
|
||||
cfg := &GetSourceConfig{
|
||||
SourceProviderConfig: &sourceproviders.Config{
|
||||
Platform: &image.Platform{
|
||||
Architecture: "amd64",
|
||||
OS: "linux",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
src := mockSource{
|
||||
desc: source.Description{
|
||||
Metadata: &source.ImageMetadata{},
|
||||
},
|
||||
}
|
||||
|
||||
err := validateSourcePlatform(src, cfg)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for valid combination, got: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ var _ file.Resolver = (*Directory)(nil)
|
||||
|
||||
// Directory implements path and content access for the directory data source.
|
||||
type Directory struct {
|
||||
filetreeResolver
|
||||
FiletreeResolver
|
||||
path string
|
||||
indexer *directoryIndexer
|
||||
}
|
||||
@@ -39,10 +39,11 @@ func newFromDirectoryWithoutIndex(root string, base string, pathFilters ...PathI
|
||||
|
||||
return &Directory{
|
||||
path: cleanRoot,
|
||||
filetreeResolver: filetreeResolver{
|
||||
chroot: *chroot,
|
||||
tree: filetree.New(),
|
||||
index: filetree.NewIndex(),
|
||||
FiletreeResolver: FiletreeResolver{
|
||||
Chroot: *chroot,
|
||||
Tree: filetree.New(),
|
||||
Index: filetree.NewIndex(),
|
||||
Opener: nativeOSFileOpener,
|
||||
},
|
||||
indexer: newDirectoryIndexer(cleanRoot, cleanBase, pathFilters...),
|
||||
}, nil
|
||||
@@ -57,9 +58,9 @@ func (r *Directory) buildIndex() error {
|
||||
return err
|
||||
}
|
||||
|
||||
r.tree = tree
|
||||
r.index = index
|
||||
r.searchContext = filetree.NewSearchContext(tree, index)
|
||||
r.Tree = tree
|
||||
r.Index = index
|
||||
r.SearchContext = filetree.NewSearchContext(tree, index)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,14 +9,12 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/internal/windows"
|
||||
)
|
||||
|
||||
@@ -64,14 +62,14 @@ func (r *directoryIndexer) build() (filetree.Reader, filetree.IndexReader, error
|
||||
return r.tree, r.index, indexAllRoots(r.path, r.indexTree)
|
||||
}
|
||||
|
||||
func indexAllRoots(root string, indexer func(string, *progress.Stage) ([]string, error)) error {
|
||||
func indexAllRoots(root string, indexer func(string, *progress.AtomicStage) ([]string, error)) error {
|
||||
// why account for multiple roots? To cover cases when there is a symlink that references above the root path,
|
||||
// in which case we need to additionally index where the link resolves to. it's for this reason why the filetree
|
||||
// must be relative to the root of the filesystem (and not just relative to the given path).
|
||||
pathsToIndex := []string{root}
|
||||
fullPathsMap := map[string]struct{}{}
|
||||
|
||||
stager, prog := indexingProgress(root)
|
||||
prog := bus.StartIndexingFiles(root)
|
||||
defer prog.SetCompleted()
|
||||
loop:
|
||||
for {
|
||||
@@ -85,7 +83,7 @@ loop:
|
||||
currentPath, pathsToIndex = pathsToIndex[0], pathsToIndex[1:]
|
||||
}
|
||||
|
||||
additionalRoots, err := indexer(currentPath, stager)
|
||||
additionalRoots, err := indexer(currentPath, prog.AtomicStage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to index filesystem path=%q: %w", currentPath, err)
|
||||
}
|
||||
@@ -101,7 +99,7 @@ loop:
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *directoryIndexer) indexTree(root string, stager *progress.Stage) ([]string, error) {
|
||||
func (r *directoryIndexer) indexTree(root string, stager *progress.AtomicStage) ([]string, error) {
|
||||
log.WithFields("path", root).Trace("indexing filetree")
|
||||
|
||||
var roots []string
|
||||
@@ -144,7 +142,7 @@ func (r *directoryIndexer) indexTree(root string, stager *progress.Stage) ([]str
|
||||
|
||||
err = filepath.Walk(root,
|
||||
func(path string, info os.FileInfo, err error) error {
|
||||
stager.Current = path
|
||||
stager.Set(path)
|
||||
|
||||
newRoot, err := r.indexPath(path, info, err)
|
||||
|
||||
@@ -179,7 +177,7 @@ func isRealPath(root string) (bool, error) {
|
||||
return rootParent == realRootParent, nil
|
||||
}
|
||||
|
||||
func (r *directoryIndexer) indexBranch(root string, stager *progress.Stage) ([]string, error) {
|
||||
func (r *directoryIndexer) indexBranch(root string, stager *progress.AtomicStage) ([]string, error) {
|
||||
rootRealPath, err := filepath.EvalSymlinks(root)
|
||||
if err != nil {
|
||||
var pathErr *os.PathError
|
||||
@@ -213,7 +211,7 @@ func (r *directoryIndexer) indexBranch(root string, stager *progress.Stage) ([]s
|
||||
targetPath = p
|
||||
}
|
||||
|
||||
stager.Current = targetPath
|
||||
stager.Set(targetPath)
|
||||
|
||||
lstat, err := os.Lstat(targetPath)
|
||||
newRoot, err := r.indexPath(targetPath, lstat, err)
|
||||
@@ -492,22 +490,3 @@ func requireFileInfo(_, _ string, info os.FileInfo, _ error) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func indexingProgress(path string) (*progress.Stage, *progress.Manual) {
|
||||
stage := &progress.Stage{}
|
||||
prog := progress.NewManual(-1)
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.FileIndexingStarted,
|
||||
Source: path,
|
||||
Value: struct {
|
||||
progress.Stager
|
||||
progress.Progressable
|
||||
}{
|
||||
Stager: progress.Stager(stage),
|
||||
Progressable: prog,
|
||||
},
|
||||
})
|
||||
|
||||
return stage, prog
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ type indexerMock struct {
|
||||
additionalRoots map[string][]string
|
||||
}
|
||||
|
||||
func (m *indexerMock) indexer(s string, _ *progress.Stage) ([]string, error) {
|
||||
func (m *indexerMock) indexer(s string, _ *progress.AtomicStage) ([]string, error) {
|
||||
m.observedRoots = append(m.observedRoots, s)
|
||||
return m.additionalRoots[s], nil
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ var _ file.Resolver = (*File)(nil)
|
||||
|
||||
// File implements path and content access for the file data source.
|
||||
type File struct {
|
||||
filetreeResolver
|
||||
FiletreeResolver
|
||||
path string
|
||||
indexer *fileIndexer
|
||||
}
|
||||
@@ -29,10 +29,11 @@ func NewFromFile(parent, path string, pathFilters ...PathIndexVisitor) (*File, e
|
||||
|
||||
file := &File{
|
||||
path: path,
|
||||
filetreeResolver: filetreeResolver{
|
||||
chroot: *chroot,
|
||||
tree: filetree.New(),
|
||||
index: filetree.NewIndex(),
|
||||
FiletreeResolver: FiletreeResolver{
|
||||
Chroot: *chroot,
|
||||
Tree: filetree.New(),
|
||||
Index: filetree.NewIndex(),
|
||||
Opener: nativeOSFileOpener,
|
||||
},
|
||||
indexer: newFileIndexer(path, cleanBase, pathFilters...),
|
||||
}
|
||||
@@ -49,9 +50,9 @@ func (r *File) buildIndex() error {
|
||||
return err
|
||||
}
|
||||
|
||||
r.tree = tree
|
||||
r.index = index
|
||||
r.searchContext = filetree.NewSearchContext(tree, index)
|
||||
r.Tree = tree
|
||||
r.Index = index
|
||||
r.SearchContext = filetree.NewSearchContext(tree, index)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/internal/windows"
|
||||
)
|
||||
@@ -49,14 +50,14 @@ func (r *fileIndexer) build() (filetree.Reader, filetree.IndexReader, error) {
|
||||
|
||||
// Index file at the given path
|
||||
// A file indexer simply indexes the file and its directory.
|
||||
func index(path string, indexer func(string, *progress.Stage) error) error {
|
||||
func index(path string, indexer func(string, *progress.AtomicStage) error) error {
|
||||
// We want to index the file at the provided path and its parent directory.
|
||||
// We need to probably check that we have file access
|
||||
// We also need to determine what to do when the file itself is a symlink.
|
||||
stager, prog := indexingProgress(path)
|
||||
prog := bus.StartIndexingFiles(path)
|
||||
defer prog.SetCompleted()
|
||||
|
||||
err := indexer(path, stager)
|
||||
err := indexer(path, prog.AtomicStage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to index filesystem path=%q: %w", path, err)
|
||||
}
|
||||
@@ -70,7 +71,7 @@ func index(path string, indexer func(string, *progress.Stage) error) error {
|
||||
// permissions errors on the file at path or its parent directory will return an error.
|
||||
// Filter functions provided to the indexer are honoured, so if the path provided (or its parent
|
||||
// directory) is filtered by a filter function, an error is returned.
|
||||
func (r *fileIndexer) indexPath(path string, stager *progress.Stage) error {
|
||||
func (r *fileIndexer) indexPath(path string, stager *progress.AtomicStage) error {
|
||||
log.WithFields("path", path).Trace("indexing file path")
|
||||
|
||||
absPath, err := filepath.Abs(path)
|
||||
@@ -105,14 +106,14 @@ func (r *fileIndexer) indexPath(path string, stager *progress.Stage) error {
|
||||
return fmt.Errorf("unable to stat parent of file=%q: %w", absSymlinkFreeParent, err)
|
||||
}
|
||||
|
||||
stager.Current = absSymlinkFreeParent
|
||||
stager.Set(absSymlinkFreeParent)
|
||||
indexParentErr := r.filterAndIndex(absSymlinkFreeParent, parentFi)
|
||||
if indexParentErr != nil {
|
||||
return indexParentErr
|
||||
}
|
||||
|
||||
// We have indexed the parent successfully, now attempt to index the file.
|
||||
stager.Current = absSymlinkFreeFilePath
|
||||
stager.Set(absSymlinkFreeFilePath)
|
||||
indexFileErr := r.filterAndIndex(absSymlinkFreeFilePath, fi)
|
||||
if indexFileErr != nil {
|
||||
return indexFileErr
|
||||
|
||||
@@ -14,33 +14,46 @@ import (
|
||||
"github.com/anchore/syft/syft/internal/windows"
|
||||
)
|
||||
|
||||
type filetreeResolver struct {
|
||||
chroot ChrootContext
|
||||
tree filetree.Reader
|
||||
index filetree.IndexReader
|
||||
searchContext filetree.Searcher
|
||||
// TODO: consider making a constructor for this
|
||||
type FiletreeResolver struct {
|
||||
Chroot ChrootContext
|
||||
Tree filetree.Reader
|
||||
Index filetree.IndexReader
|
||||
SearchContext filetree.Searcher
|
||||
Opener func(stereoscopeFile.Reference) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
func (r *filetreeResolver) requestPath(userPath string) (string, error) {
|
||||
return r.chroot.ToNativePath(userPath)
|
||||
func nativeOSFileOpener(ref stereoscopeFile.Reference) (io.ReadCloser, error) {
|
||||
// RealPath is posix so for windows file resolver we need to translate
|
||||
// to its true on disk path.
|
||||
filePath := string(ref.RealPath)
|
||||
if windows.HostRunningOnWindows() {
|
||||
filePath = windows.FromPosix(filePath)
|
||||
}
|
||||
|
||||
return stereoscopeFile.NewLazyReadCloser(filePath), nil
|
||||
}
|
||||
|
||||
func (r *FiletreeResolver) requestPath(userPath string) (string, error) {
|
||||
return r.Chroot.ToNativePath(userPath)
|
||||
}
|
||||
|
||||
// responsePath takes a path from the underlying fs domain and converts it to a path that is relative to the root of the file resolver.
|
||||
func (r filetreeResolver) responsePath(path string) string {
|
||||
return r.chroot.ToChrootPath(path)
|
||||
func (r FiletreeResolver) responsePath(path string) string {
|
||||
return r.Chroot.ToChrootPath(path)
|
||||
}
|
||||
|
||||
// HasPath indicates if the given path exists in the underlying source.
|
||||
func (r *filetreeResolver) HasPath(userPath string) bool {
|
||||
func (r *FiletreeResolver) HasPath(userPath string) bool {
|
||||
requestPath, err := r.requestPath(userPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return r.tree.HasPath(stereoscopeFile.Path(requestPath))
|
||||
return r.Tree.HasPath(stereoscopeFile.Path(requestPath))
|
||||
}
|
||||
|
||||
// FilesByPath returns all file.References that match the given paths from the file index.
|
||||
func (r filetreeResolver) FilesByPath(userPaths ...string) ([]file.Location, error) {
|
||||
func (r FiletreeResolver) FilesByPath(userPaths ...string) ([]file.Location, error) {
|
||||
var references = make([]file.Location, 0)
|
||||
|
||||
for _, userPath := range userPaths {
|
||||
@@ -51,7 +64,7 @@ func (r filetreeResolver) FilesByPath(userPaths ...string) ([]file.Location, err
|
||||
}
|
||||
|
||||
// we should be resolving symlinks and preserving this information as a AccessPath to the real file
|
||||
ref, err := r.searchContext.SearchByPath(userStrPath, filetree.FollowBasenameLinks)
|
||||
ref, err := r.SearchContext.SearchByPath(userStrPath, filetree.FollowBasenameLinks)
|
||||
if err != nil {
|
||||
log.Tracef("unable to evaluate symlink for path=%q : %+v", userPath, err)
|
||||
continue
|
||||
@@ -61,7 +74,7 @@ func (r filetreeResolver) FilesByPath(userPaths ...string) ([]file.Location, err
|
||||
continue
|
||||
}
|
||||
|
||||
entry, err := r.index.Get(*ref.Reference)
|
||||
entry, err := r.Index.Get(*ref.Reference)
|
||||
if err != nil {
|
||||
log.Warnf("unable to get file by path=%q : %+v", userPath, err)
|
||||
continue
|
||||
@@ -90,12 +103,12 @@ func (r filetreeResolver) FilesByPath(userPaths ...string) ([]file.Location, err
|
||||
return references, nil
|
||||
}
|
||||
|
||||
func (r filetreeResolver) requestGlob(pattern string) (string, error) {
|
||||
return r.chroot.ToNativeGlob(pattern)
|
||||
func (r FiletreeResolver) requestGlob(pattern string) (string, error) {
|
||||
return r.Chroot.ToNativeGlob(pattern)
|
||||
}
|
||||
|
||||
// FilesByGlob returns all file.References that match the given path glob pattern from any layer in the image.
|
||||
func (r filetreeResolver) FilesByGlob(patterns ...string) ([]file.Location, error) {
|
||||
func (r FiletreeResolver) FilesByGlob(patterns ...string) ([]file.Location, error) {
|
||||
uniqueFileIDs := stereoscopeFile.NewFileReferenceSet()
|
||||
uniqueLocations := make([]file.Location, 0)
|
||||
|
||||
@@ -104,7 +117,7 @@ func (r filetreeResolver) FilesByGlob(patterns ...string) ([]file.Location, erro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refVias, err := r.searchContext.SearchByGlob(requestGlob, filetree.FollowBasenameLinks)
|
||||
refVias, err := r.SearchContext.SearchByGlob(requestGlob, filetree.FollowBasenameLinks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -112,7 +125,7 @@ func (r filetreeResolver) FilesByGlob(patterns ...string) ([]file.Location, erro
|
||||
if !refVia.HasReference() || uniqueFileIDs.Contains(*refVia.Reference) {
|
||||
continue
|
||||
}
|
||||
entry, err := r.index.Get(*refVia.Reference)
|
||||
entry, err := r.Index.Get(*refVia.Reference)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get file metadata for reference %s: %w", refVia.RealPath, err)
|
||||
}
|
||||
@@ -137,7 +150,7 @@ func (r filetreeResolver) FilesByGlob(patterns ...string) ([]file.Location, erro
|
||||
|
||||
// RelativeFileByPath fetches a single file at the given path relative to the layer squash of the given reference.
|
||||
// This is helpful when attempting to find a file that is in the same layer or lower as another file.
|
||||
func (r *filetreeResolver) RelativeFileByPath(_ file.Location, path string) *file.Location {
|
||||
func (r *FiletreeResolver) RelativeFileByPath(_ file.Location, path string) *file.Location {
|
||||
paths, err := r.FilesByPath(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -151,12 +164,12 @@ func (r *filetreeResolver) RelativeFileByPath(_ file.Location, path string) *fil
|
||||
|
||||
// FileContentsByLocation fetches file contents for a single file reference relative to a directory.
|
||||
// If the path does not exist an error is returned.
|
||||
func (r filetreeResolver) FileContentsByLocation(location file.Location) (io.ReadCloser, error) {
|
||||
func (r FiletreeResolver) FileContentsByLocation(location file.Location) (io.ReadCloser, error) {
|
||||
if location.RealPath == "" {
|
||||
return nil, errors.New("empty path given")
|
||||
}
|
||||
|
||||
entry, err := r.index.Get(location.Reference())
|
||||
entry, err := r.Index.Get(location.Reference())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -166,21 +179,14 @@ func (r filetreeResolver) FileContentsByLocation(location file.Location) (io.Rea
|
||||
return nil, fmt.Errorf("cannot read contents of non-file %q", location.Reference().RealPath)
|
||||
}
|
||||
|
||||
// RealPath is posix so for windows file resolver we need to translate
|
||||
// to its true on disk path.
|
||||
filePath := string(location.Reference().RealPath)
|
||||
if windows.HostRunningOnWindows() {
|
||||
filePath = windows.FromPosix(filePath)
|
||||
}
|
||||
|
||||
return stereoscopeFile.NewLazyReadCloser(filePath), nil
|
||||
return r.Opener(location.Reference())
|
||||
}
|
||||
|
||||
func (r *filetreeResolver) AllLocations(ctx context.Context) <-chan file.Location {
|
||||
func (r *FiletreeResolver) AllLocations(ctx context.Context) <-chan file.Location {
|
||||
results := make(chan file.Location)
|
||||
go func() {
|
||||
defer close(results)
|
||||
for _, ref := range r.tree.AllFiles(stereoscopeFile.AllTypes()...) {
|
||||
for _, ref := range r.Tree.AllFiles(stereoscopeFile.AllTypes()...) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
@@ -192,8 +198,8 @@ func (r *filetreeResolver) AllLocations(ctx context.Context) <-chan file.Locatio
|
||||
return results
|
||||
}
|
||||
|
||||
func (r *filetreeResolver) FileMetadataByLocation(location file.Location) (file.Metadata, error) {
|
||||
entry, err := r.index.Get(location.Reference())
|
||||
func (r *FiletreeResolver) FileMetadataByLocation(location file.Location) (file.Metadata, error) {
|
||||
entry, err := r.Index.Get(location.Reference())
|
||||
if err != nil {
|
||||
return file.Metadata{}, fmt.Errorf("location: %+v : %w", location, os.ErrNotExist)
|
||||
}
|
||||
@@ -201,11 +207,11 @@ func (r *filetreeResolver) FileMetadataByLocation(location file.Location) (file.
|
||||
return entry.Metadata, nil
|
||||
}
|
||||
|
||||
func (r *filetreeResolver) FilesByMIMEType(types ...string) ([]file.Location, error) {
|
||||
func (r *FiletreeResolver) FilesByMIMEType(types ...string) ([]file.Location, error) {
|
||||
uniqueFileIDs := stereoscopeFile.NewFileReferenceSet()
|
||||
uniqueLocations := make([]file.Location, 0)
|
||||
|
||||
refVias, err := r.searchContext.SearchByMIMEType(types...)
|
||||
refVias, err := r.SearchContext.SearchByMIMEType(types...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -971,7 +971,7 @@ func Test_directoryResolver_FileContentsByLocation(t *testing.T) {
|
||||
r, err := NewFromDirectory(".", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
exists, existingPath, err := r.tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt")))
|
||||
exists, existingPath, err := r.Tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt")))
|
||||
require.True(t, exists)
|
||||
require.NoError(t, err)
|
||||
require.True(t, existingPath.HasReference())
|
||||
@@ -1271,7 +1271,7 @@ func TestDirectoryResolver_FilesContents_errorOnDirRequest(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
for loc := range resolver.AllLocations(ctx) {
|
||||
entry, err := resolver.index.Get(loc.Reference())
|
||||
entry, err := resolver.Index.Get(loc.Reference())
|
||||
require.NoError(t, err)
|
||||
if entry.Metadata.IsDir() {
|
||||
dirLoc = &loc
|
||||
@@ -1451,7 +1451,7 @@ func TestFileResolver_FilesByGlob(t *testing.T) {
|
||||
|
||||
resolver, err := NewFromFile(parentPath, filePath)
|
||||
assert.NoError(t, err)
|
||||
refs, err := resolver.FilesByGlob("*.txt")
|
||||
refs, err := resolver.FilesByGlob("**/*.txt")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, refs, 1)
|
||||
@@ -1500,7 +1500,7 @@ func Test_fileResolver_FileContentsByLocation(t *testing.T) {
|
||||
r, err := NewFromFile(parentPath, filePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
exists, existingPath, err := r.tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt")))
|
||||
exists, existingPath, err := r.Tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt")))
|
||||
require.True(t, exists)
|
||||
require.NoError(t, err)
|
||||
require.True(t, existingPath.HasReference())
|
||||
@@ -1553,7 +1553,7 @@ func TestFileResolver_AllLocations_errorOnDirRequest(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
for loc := range resolver.AllLocations(ctx) {
|
||||
entry, err := resolver.index.Get(loc.Reference())
|
||||
entry, err := resolver.Index.Get(loc.Reference())
|
||||
require.NoError(t, err)
|
||||
if dirLoc == nil && entry.Metadata.IsDir() {
|
||||
dirLoc = &loc
|
||||
|
||||
@@ -42,6 +42,7 @@ func AllTypes() []any {
|
||||
pkg.NpmPackage{},
|
||||
pkg.NpmPackageLockEntry{},
|
||||
pkg.OpamPackage{},
|
||||
pkg.PEBinary{},
|
||||
pkg.PhpComposerInstalledEntry{},
|
||||
pkg.PhpComposerLockEntry{},
|
||||
pkg.PhpPearEntry{},
|
||||
|
||||
@@ -94,6 +94,7 @@ var jsonTypes = makeJSONTypes(
|
||||
jsonNames(pkg.NpmPackage{}, "javascript-npm-package", "NpmPackageJsonMetadata"),
|
||||
jsonNames(pkg.NpmPackageLockEntry{}, "javascript-npm-package-lock-entry", "NpmPackageLockJsonMetadata"),
|
||||
jsonNames(pkg.YarnLockEntry{}, "javascript-yarn-lock-entry", "YarnLockJsonMetadata"),
|
||||
jsonNames(pkg.PEBinary{}, "pe-binary"),
|
||||
jsonNames(pkg.PhpComposerLockEntry{}, "php-composer-lock-entry", "PhpComposerJsonMetadata"),
|
||||
jsonNamesWithoutLookup(pkg.PhpComposerInstalledEntry{}, "php-composer-installed-entry", "PhpComposerJsonMetadata"), // the legacy value is split into two types, where the other is preferred
|
||||
jsonNames(pkg.PhpPeclEntry{}, "php-pecl-entry", "PhpPeclMetadata"),
|
||||
|
||||
@@ -6,5 +6,5 @@ import "github.com/anchore/syft/syft/source"
|
||||
|
||||
// AllTypes returns a list of all source metadata types that syft supports (that are represented in the source.Description.Metadata field).
|
||||
func AllTypes() []any {
|
||||
return []any{source.DirectoryMetadata{}, source.FileMetadata{}, source.ImageMetadata{}}
|
||||
return []any{source.DirectoryMetadata{}, source.FileMetadata{}, source.ImageMetadata{}, source.SnapMetadata{}}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ var jsonNameFromType = map[reflect.Type][]string{
|
||||
reflect.TypeOf(source.DirectoryMetadata{}): {"directory", "dir"},
|
||||
reflect.TypeOf(source.FileMetadata{}): {"file"},
|
||||
reflect.TypeOf(source.ImageMetadata{}): {"image"},
|
||||
reflect.TypeOf(source.SnapMetadata{}): {"snap"},
|
||||
}
|
||||
|
||||
func AllTypeNames() []string {
|
||||
|
||||
@@ -4,6 +4,9 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/diskfs/go-diskfs/filesystem/squashfs"
|
||||
|
||||
macho "github.com/anchore/go-macholibre"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
@@ -48,12 +51,15 @@ func GetUnionReader(readerCloser io.ReadCloser) (UnionReader, error) {
|
||||
// file.LocationReadCloser embeds a ReadCloser, which is likely
|
||||
// to implement UnionReader. Check whether the embedded read closer
|
||||
// implements UnionReader, and just return that if so.
|
||||
r, ok := readerCloser.(file.LocationReadCloser)
|
||||
if ok {
|
||||
ur, ok := r.ReadCloser.(UnionReader)
|
||||
if ok {
|
||||
return ur, nil
|
||||
}
|
||||
|
||||
if r, ok := readerCloser.(file.LocationReadCloser); ok {
|
||||
return GetUnionReader(r.ReadCloser)
|
||||
}
|
||||
|
||||
if r, ok := readerCloser.(*squashfs.File); ok {
|
||||
// seeking is implemented, but not io.ReaderAt. Lets wrap it to prevent from degrading performance
|
||||
// by copying all data.
|
||||
return newReaderAtAdapter(r), nil
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(readerCloser)
|
||||
@@ -75,3 +81,58 @@ func GetUnionReader(readerCloser io.ReadCloser) (UnionReader, error) {
|
||||
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
type readerAtAdapter struct {
|
||||
io.ReadSeekCloser
|
||||
mu *sync.Mutex
|
||||
}
|
||||
|
||||
func newReaderAtAdapter(rs io.ReadSeekCloser) UnionReader {
|
||||
return &readerAtAdapter{
|
||||
ReadSeekCloser: rs,
|
||||
mu: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *readerAtAdapter) Read(p []byte) (n int, err error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.ReadSeekCloser.Read(p)
|
||||
}
|
||||
|
||||
func (r *readerAtAdapter) Seek(offset int64, whence int) (int64, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.ReadSeekCloser.Seek(offset, whence)
|
||||
}
|
||||
|
||||
func (r *readerAtAdapter) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
currentPos, err := r.ReadSeekCloser.Seek(0, io.SeekCurrent) // save current pos
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
_, err = r.ReadSeekCloser.Seek(off, io.SeekStart) // seek to absolute position `off`
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n, err = r.ReadSeekCloser.Read(p) // read from that absolute position
|
||||
|
||||
// restore the position for the stateful read/seek operations
|
||||
if restoreErr := r.restorePosition(currentPos); restoreErr != nil {
|
||||
if err == nil {
|
||||
err = restoreErr
|
||||
}
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *readerAtAdapter) restorePosition(pos int64) error {
|
||||
_, err := r.ReadSeekCloser.Seek(pos, io.SeekStart)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package unionreader
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -61,3 +63,275 @@ func Test_getUnionReader_fileLocationReadCloser(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, p, ur)
|
||||
}
|
||||
|
||||
func TestReaderAtAdapter_ReadAt(t *testing.T) {
|
||||
testData := "Hello, World! This is a test string for ReadAt."
|
||||
|
||||
t.Run("basic functionality", func(t *testing.T) {
|
||||
reader := newReadSeekCloser(strings.NewReader(testData))
|
||||
adapter := newReaderAtAdapter(reader)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
offset int64
|
||||
length int
|
||||
expected string
|
||||
}{
|
||||
{name: "read from beginning", offset: 0, length: 5, expected: "Hello"},
|
||||
{name: "read from middle", offset: 7, length: 5, expected: "World"},
|
||||
{name: "read from end", offset: int64(len(testData) - 4), length: 4, expected: "dAt."},
|
||||
{name: "read single character", offset: 12, length: 1, expected: "!"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buf := make([]byte, tt.length)
|
||||
n, err := adapter.ReadAt(buf, tt.offset)
|
||||
|
||||
if err != nil && err != io.EOF {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
result := string(buf[:n])
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected %q, got %q", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("edge cases", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data string
|
||||
offset int64
|
||||
bufSize int
|
||||
expectedN int
|
||||
expectedErr error
|
||||
expectedStr string
|
||||
}{
|
||||
{
|
||||
name: "beyond EOF",
|
||||
data: "Hello",
|
||||
offset: 10,
|
||||
bufSize: 5,
|
||||
expectedN: 0,
|
||||
expectedErr: io.EOF,
|
||||
expectedStr: "",
|
||||
},
|
||||
{
|
||||
name: "partial read",
|
||||
data: "Hello",
|
||||
offset: 2,
|
||||
bufSize: 10,
|
||||
expectedN: 3,
|
||||
expectedErr: nil,
|
||||
expectedStr: "llo",
|
||||
},
|
||||
{
|
||||
name: "empty buffer",
|
||||
data: "Hello",
|
||||
offset: 0,
|
||||
bufSize: 0,
|
||||
expectedN: 0,
|
||||
expectedErr: nil,
|
||||
expectedStr: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := newReadSeekCloser(strings.NewReader(tt.data))
|
||||
adapter := newReaderAtAdapter(reader)
|
||||
|
||||
buf := make([]byte, tt.bufSize)
|
||||
n, err := adapter.ReadAt(buf, tt.offset)
|
||||
|
||||
if err != tt.expectedErr {
|
||||
t.Errorf("Expected error %v, got %v", tt.expectedErr, err)
|
||||
}
|
||||
|
||||
if n != tt.expectedN {
|
||||
t.Errorf("Expected %d bytes read, got %d", tt.expectedN, n)
|
||||
}
|
||||
|
||||
result := string(buf[:n])
|
||||
if result != tt.expectedStr {
|
||||
t.Errorf("Expected %q, got %q", tt.expectedStr, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple reads from same position", func(t *testing.T) {
|
||||
reader := newReadSeekCloser(strings.NewReader(testData))
|
||||
adapter := newReaderAtAdapter(reader)
|
||||
|
||||
// read the same data multiple times
|
||||
for i := 0; i < 3; i++ {
|
||||
buf := make([]byte, 5)
|
||||
n, err := adapter.ReadAt(buf, 7)
|
||||
|
||||
if err != nil && err != io.EOF {
|
||||
t.Fatalf("ReadAt %d failed: %v", i, err)
|
||||
}
|
||||
|
||||
result := string(buf[:n])
|
||||
if result != "World" {
|
||||
t.Errorf("ReadAt %d: expected 'World', got %q", i, result)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("concurrent access", func(t *testing.T) {
|
||||
td := "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
reader := newReadSeekCloser(strings.NewReader(td))
|
||||
adapter := newReaderAtAdapter(reader)
|
||||
|
||||
const numGoroutines = 10
|
||||
const numReads = 100
|
||||
|
||||
var wg sync.WaitGroup
|
||||
results := make(chan bool, numGoroutines*numReads)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < numReads; j++ {
|
||||
offset := int64(goroutineID % len(td))
|
||||
buf := make([]byte, 1)
|
||||
|
||||
n, err := adapter.ReadAt(buf, offset)
|
||||
if err != nil && err != io.EOF {
|
||||
results <- false
|
||||
return
|
||||
}
|
||||
|
||||
if n > 0 {
|
||||
expected := td[offset]
|
||||
if buf[0] != expected {
|
||||
results <- false
|
||||
return
|
||||
}
|
||||
}
|
||||
results <- true
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(results)
|
||||
|
||||
successCount := 0
|
||||
totalCount := 0
|
||||
for success := range results {
|
||||
totalCount++
|
||||
if success {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
|
||||
if successCount != totalCount {
|
||||
t.Errorf("Concurrent reads failed: %d/%d successful", successCount, totalCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReaderAtAdapter_PositionHandling(t *testing.T) {
|
||||
testData := "Hello, World!"
|
||||
|
||||
t.Run("preserves position after ReadAt", func(t *testing.T) {
|
||||
reader := newReadSeekCloser(strings.NewReader(testData))
|
||||
adapter := newReaderAtAdapter(reader)
|
||||
|
||||
// move to a specific position
|
||||
initialPos := int64(7)
|
||||
_, err := adapter.Seek(initialPos, io.SeekStart)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to seek: %v", err)
|
||||
}
|
||||
|
||||
// read using ReadAt
|
||||
buf := make([]byte, 5)
|
||||
_, err = adapter.ReadAt(buf, 0)
|
||||
if err != nil && err != io.EOF {
|
||||
t.Fatalf("ReadAt failed: %v", err)
|
||||
}
|
||||
|
||||
// verify position is preserved
|
||||
currentPos, err := adapter.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get current position: %v", err)
|
||||
}
|
||||
|
||||
if currentPos != initialPos {
|
||||
t.Errorf("Position not preserved. Expected %d, got %d", initialPos, currentPos)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("does not affect regular reads", func(t *testing.T) {
|
||||
reader := newReadSeekCloser(strings.NewReader(testData))
|
||||
adapter := newReaderAtAdapter(reader)
|
||||
|
||||
// read first few bytes normally
|
||||
normalBuf := make([]byte, 5)
|
||||
n, err := adapter.Read(normalBuf)
|
||||
if err != nil {
|
||||
t.Fatalf("Normal read failed: %v", err)
|
||||
}
|
||||
if string(normalBuf[:n]) != "Hello" {
|
||||
t.Errorf("Expected 'Hello', got %q", string(normalBuf[:n]))
|
||||
}
|
||||
|
||||
// use ReadAt to read from a different position
|
||||
readAtBuf := make([]byte, 5)
|
||||
n, err = adapter.ReadAt(readAtBuf, 7)
|
||||
if err != nil && err != io.EOF {
|
||||
t.Fatalf("ReadAt failed: %v", err)
|
||||
}
|
||||
if string(readAtBuf[:n]) != "World" {
|
||||
t.Errorf("Expected 'World', got %q", string(readAtBuf[:n]))
|
||||
}
|
||||
|
||||
// continue normal reading - should pick up where we left off
|
||||
continueBuf := make([]byte, 2)
|
||||
n, err = adapter.Read(continueBuf)
|
||||
if err != nil {
|
||||
t.Fatalf("Continue read failed: %v", err)
|
||||
}
|
||||
if string(continueBuf[:n]) != ", " {
|
||||
t.Errorf("Expected ', ', got %q", string(continueBuf[:n]))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReaderAtAdapter_Close(t *testing.T) {
|
||||
reader := newReadSeekCloser(bytes.NewReader([]byte("test data")))
|
||||
adapter := newReaderAtAdapter(reader)
|
||||
|
||||
// test that adapter can be closed
|
||||
err := adapter.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close failed: %v", err)
|
||||
}
|
||||
|
||||
if !reader.closed {
|
||||
t.Error("Underlying reader was not closed")
|
||||
}
|
||||
}
|
||||
|
||||
type readSeekCloser struct {
|
||||
io.ReadSeeker
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newReadSeekCloser(rs io.ReadSeeker) *readSeekCloser {
|
||||
return &readSeekCloser{ReadSeeker: rs}
|
||||
}
|
||||
|
||||
func (r *readSeekCloser) Close() error {
|
||||
r.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -46,3 +46,8 @@ type ELFBinaryPackageNoteJSONPayload struct {
|
||||
// Commit is the commit hash of the source repository for which the binary was built from
|
||||
Commit string `json:"commit,omitempty"`
|
||||
}
|
||||
|
||||
// PEBinary represents metadata captured from a Portable Executable formatted binary (dll, exe, etc.)
|
||||
type PEBinary struct {
|
||||
VersionResources KeyValues
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ func newELFPackage(ctx context.Context, metadata elfBinaryPackageNotes, location
|
||||
Name: metadata.Name,
|
||||
Version: metadata.Version,
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, metadata.License)),
|
||||
PURL: packageURL(metadata),
|
||||
PURL: elfPackageURL(metadata),
|
||||
Type: pkgType(metadata.Type),
|
||||
Locations: locations,
|
||||
Metadata: metadata.ELFBinaryPackageNoteJSONPayload,
|
||||
@@ -26,7 +26,7 @@ func newELFPackage(ctx context.Context, metadata elfBinaryPackageNotes, location
|
||||
return p
|
||||
}
|
||||
|
||||
func packageURL(metadata elfBinaryPackageNotes) string {
|
||||
func elfPackageURL(metadata elfBinaryPackageNotes) string {
|
||||
var qualifiers []packageurl.Qualifier
|
||||
|
||||
os, osVersion := osNameAndVersionFromMetadata(metadata)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||
)
|
||||
|
||||
func Test_ELF_Package_Cataloger(t *testing.T) {
|
||||
func Test_ELFPackageCataloger(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
|
||||
@@ -130,7 +130,7 @@ func Test_packageURL(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.want, packageURL(test.metadata))
|
||||
assert.Equal(t, test.want, elfPackageURL(test.metadata))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
var (
|
||||
// spaceRegex includes nbsp (#160) considered to be a space character
|
||||
spaceRegex = regexp.MustCompile(`[\s\xa0]+`)
|
||||
numberRegex = regexp.MustCompile(`\d`)
|
||||
)
|
||||
|
||||
func newPEPackage(versionResources map[string]string, f file.Location) pkg.Package {
|
||||
name := findNameFromVR(versionResources)
|
||||
|
||||
if name == "" {
|
||||
// it's possible that the version resources are empty, so we fall back to the file name
|
||||
name = strings.TrimSuffix(strings.TrimSuffix(path.Base(f.RealPath), ".exe"), ".dll")
|
||||
}
|
||||
|
||||
p := pkg.Package{
|
||||
Name: name,
|
||||
Version: findVersionFromVR(versionResources),
|
||||
Locations: file.NewLocationSet(f.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
|
||||
Type: pkg.BinaryPkg,
|
||||
Metadata: newPEBinaryVersionResourcesFromMap(versionResources),
|
||||
}
|
||||
|
||||
p.SetID()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func newPEBinaryVersionResourcesFromMap(vr map[string]string) pkg.PEBinary {
|
||||
var kvs pkg.KeyValues
|
||||
for k, v := range vr {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
kvs = append(kvs, pkg.KeyValue{
|
||||
Key: k,
|
||||
Value: spaceNormalize(v),
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(kvs, func(i, j int) bool {
|
||||
return kvs[i].Key < kvs[j].Key
|
||||
})
|
||||
|
||||
return pkg.PEBinary{
|
||||
VersionResources: kvs,
|
||||
}
|
||||
}
|
||||
|
||||
func findNameFromVR(versionResources map[string]string) string {
|
||||
// PE files not authored by Microsoft tend to use ProductName as an identifier.
|
||||
nameFields := []string{"ProductName", "FileDescription", "InternalName", "OriginalFilename"}
|
||||
|
||||
if isMicrosoftVR(versionResources) {
|
||||
// for Microsoft files, prioritize FileDescription.
|
||||
nameFields = []string{"FileDescription", "InternalName", "OriginalFilename", "ProductName"}
|
||||
}
|
||||
|
||||
var name string
|
||||
for _, field := range nameFields {
|
||||
value := spaceNormalize(versionResources[field])
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
name = value
|
||||
break
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
func isMicrosoftVR(versionResources map[string]string) bool {
|
||||
return strings.Contains(strings.ToLower(versionResources["CompanyName"]), "microsoft") ||
|
||||
strings.Contains(strings.ToLower(versionResources["ProductName"]), "microsoft")
|
||||
}
|
||||
|
||||
// spaceNormalize trims and normalizes whitespace in a string.
|
||||
func spaceNormalize(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
// ensure valid UTF-8.
|
||||
value = strings.ToValidUTF8(value, "")
|
||||
// consolidate all whitespace.
|
||||
value = spaceRegex.ReplaceAllString(value, " ")
|
||||
// remove non-printable characters.
|
||||
value = regexp.MustCompile(`[\x00-\x1f]`).ReplaceAllString(value, "")
|
||||
// consolidate again and trim.
|
||||
value = spaceRegex.ReplaceAllString(value, " ")
|
||||
value = strings.TrimSpace(value)
|
||||
return value
|
||||
}
|
||||
|
||||
func findVersionFromVR(versionResources map[string]string) string {
|
||||
productVersion := extractVersionFromResourcesValue(versionResources["ProductVersion"])
|
||||
fileVersion := extractVersionFromResourcesValue(versionResources["FileVersion"])
|
||||
|
||||
if productVersion != "" {
|
||||
return productVersion
|
||||
}
|
||||
|
||||
return fileVersion
|
||||
}
|
||||
|
||||
func extractVersionFromResourcesValue(version string) string {
|
||||
version = strings.TrimSpace(version)
|
||||
out := ""
|
||||
for i, f := range strings.Fields(version) {
|
||||
if containsNumber(out) && !containsNumber(f) {
|
||||
return out
|
||||
}
|
||||
if i == 0 {
|
||||
out = f
|
||||
} else {
|
||||
out += " " + f
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func containsNumber(s string) bool {
|
||||
return numberRegex.MatchString(s)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/pe"
|
||||
)
|
||||
|
||||
// NewPEPackageCataloger returns a cataloger that interprets packages from DLL and EXE files.
|
||||
func NewPEPackageCataloger() pkg.Cataloger {
|
||||
return generic.NewCataloger("pe-binary-package-cataloger").
|
||||
WithParserByGlobs(parsePE, "**/*.dll", "**/*.exe")
|
||||
}
|
||||
|
||||
func parsePE(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
f, err := pe.Read(reader)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse PE file %q: %w", reader.RealPath, err)
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
return nil, nil, unknown.Newf(reader, "unable to determine packages")
|
||||
}
|
||||
|
||||
if f.CLR.HasEvidenceOfCLR() {
|
||||
// this is for a .NET application, which is covered by other catalogers already
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
p := newPEPackage(f.VersionResources, f.Location)
|
||||
|
||||
return []pkg.Package{p}, nil, nil
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||
)
|
||||
|
||||
func Test_PEPackageCataloger(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
fixture string
|
||||
expected []pkg.Package
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "non-.NET package",
|
||||
fixture: "image-jruby",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "JRuby",
|
||||
Version: "9.3.15.0",
|
||||
Type: pkg.BinaryPkg,
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewLocation("/jruby_windows_9_3_15_0.exe"),
|
||||
),
|
||||
FoundBy: "pe-binary-package-cataloger",
|
||||
Metadata: pkg.PEBinary{
|
||||
VersionResources: pkg.KeyValues{
|
||||
{Key: "CompanyName", Value: "JRuby Dev Team"},
|
||||
{Key: "FileDescription", Value: "JRuby"},
|
||||
{Key: "FileVersion", Value: "9.3.15.0"},
|
||||
{Key: "InternalName", Value: "jruby"},
|
||||
{Key: "LegalCopyright", Value: "JRuby Dev Team"},
|
||||
{Key: "OriginalFilename", Value: "jruby_windows-x32_9_3_15_0.exe"},
|
||||
{Key: "ProductName", Value: "JRuby"},
|
||||
{Key: "ProductVersion", Value: "9.3.15.0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ignore .NET packages",
|
||||
fixture: "image-dotnet-app",
|
||||
expected: nil, // expect nothing!
|
||||
},
|
||||
}
|
||||
|
||||
for _, v := range cases {
|
||||
t.Run(v.name, func(t *testing.T) {
|
||||
pkgtest.NewCatalogTester().
|
||||
WithImageResolver(t, v.fixture).
|
||||
IgnoreLocationLayer(). // this fixture can be rebuilt, thus the layer ID will change
|
||||
Expects(v.expected, nil).
|
||||
TestCataloger(t, NewPEPackageCataloger())
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
FROM alpine:latest AS builder
|
||||
|
||||
RUN apk add --no-cache wget unzip
|
||||
RUN mkdir -p /app
|
||||
RUN wget -O /app/minver.nupkg https://www.nuget.org/api/v2/package/minver-cli/4.3.0
|
||||
RUN unzip /app/minver.nupkg -d /app/minver
|
||||
|
||||
from busybox:latest
|
||||
|
||||
COPY --from=builder /app/minver /minver
|
||||
@@ -0,0 +1,7 @@
|
||||
FROM --platform=linux/amd64 alpine:latest AS builder
|
||||
|
||||
RUN wget -O jruby_windows_9_3_15_0.exe https://s3.amazonaws.com/jruby.org/downloads/9.3.15.0/jruby_windows_9_3_15_0.exe
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /jruby_windows_9_3_15_0.exe /jruby_windows_9_3_15_0.exe
|
||||
@@ -473,7 +473,7 @@ func readPEFile(resolver file.Resolver, loc file.Location) (*logicalPE, error) {
|
||||
}
|
||||
defer internal.CloseAndLogError(reader, loc.RealPath)
|
||||
|
||||
ldpe, err := getLogicalDotnetPE(file.NewLocationReadCloser(loc, reader))
|
||||
ldpe, err := readLogicalPE(file.NewLocationReadCloser(loc, reader))
|
||||
if err != nil {
|
||||
return nil, unknown.New(loc, fmt.Errorf("unable to parse PE file: %w", err))
|
||||
}
|
||||
@@ -482,7 +482,7 @@ func readPEFile(resolver file.Resolver, loc file.Location) (*logicalPE, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !ldpe.CLR.hasEvidenceOfCLR() {
|
||||
if !ldpe.CLR.HasEvidenceOfCLR() {
|
||||
// this is not a .NET binary
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -252,8 +252,8 @@ func getLogicalDepsJSON(deps depsJSON, lm *libmanJSON) logicalDepsJSON {
|
||||
if !bundlingDetected && knownBundlers.Has(name) {
|
||||
bundlingDetected = true
|
||||
}
|
||||
p.AnyChildClaimsDLLs = searchForDLLClaims(packageMap, p.dependencyNameVersions()...)
|
||||
p.AnyChildHasDLLs = searchForDLLEvidence(packageMap, p.dependencyNameVersions()...)
|
||||
p.AnyChildClaimsDLLs = searchForDLLClaims(packageMap, strset.New(), p.dependencyNameVersions()...)
|
||||
p.AnyChildHasDLLs = searchForDLLEvidence(packageMap, strset.New(), p.dependencyNameVersions()...)
|
||||
packages[p.NameVersion] = *p
|
||||
}
|
||||
|
||||
@@ -286,31 +286,35 @@ func mergeSets(s1, s2 *strset.Set) *strset.Set {
|
||||
type visitorFunc func(p *logicalDepsJSONPackage) bool
|
||||
|
||||
// searchForDLLEvidence recursively searches for executables found for any of the given nameVersions and children recursively.
|
||||
func searchForDLLEvidence(packageMap map[string]*logicalDepsJSONPackage, nameVersions ...string) bool {
|
||||
func searchForDLLEvidence(packageMap map[string]*logicalDepsJSONPackage, visited *strset.Set, nameVersions ...string) bool {
|
||||
return traverseDependencies(packageMap, func(p *logicalDepsJSONPackage) bool {
|
||||
return p.FoundDLLs(true)
|
||||
}, nameVersions...)
|
||||
}, visited, nameVersions...)
|
||||
}
|
||||
|
||||
// searchForDLLClaims recursively searches for DLL claims in the deps.json for any of the given nameVersions and children recursively.
|
||||
func searchForDLLClaims(packageMap map[string]*logicalDepsJSONPackage, nameVersions ...string) bool {
|
||||
func searchForDLLClaims(packageMap map[string]*logicalDepsJSONPackage, visited *strset.Set, nameVersions ...string) bool {
|
||||
return traverseDependencies(packageMap, func(p *logicalDepsJSONPackage) bool {
|
||||
return p.ClaimsDLLs(true)
|
||||
}, nameVersions...)
|
||||
}, visited, nameVersions...)
|
||||
}
|
||||
|
||||
func traverseDependencies(packageMap map[string]*logicalDepsJSONPackage, visitor visitorFunc, nameVersions ...string) bool {
|
||||
func traverseDependencies(packageMap map[string]*logicalDepsJSONPackage, visitor visitorFunc, visited *strset.Set, nameVersions ...string) bool {
|
||||
if len(nameVersions) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, nameVersion := range nameVersions {
|
||||
if visited.Has(nameVersion) {
|
||||
continue
|
||||
}
|
||||
visited.Add(nameVersion)
|
||||
if p, ok := packageMap[nameVersion]; ok {
|
||||
if visitor(p) {
|
||||
return true
|
||||
}
|
||||
|
||||
if traverseDependencies(packageMap, visitor, p.dependencyNameVersions()...) {
|
||||
if traverseDependencies(packageMap, visitor, visited, p.dependencyNameVersions()...) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,11 @@ func TestGetLogicalDepsJSON_MergeTargets(t *testing.T) {
|
||||
"lib/netcoreapp3.1/Microsoft.CodeAnalysis.CSharp.dll": {},
|
||||
},
|
||||
},
|
||||
"Microsoft.CodeAnalysis.Common/4.0.0": {
|
||||
Dependencies: map[string]string{
|
||||
"Microsoft.CodeAnalysis.CSharp": "4.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Libraries: map[string]depsLibrary{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user