mirror of
https://github.com/anchore/syft.git
synced 2026-10-13 06:37:22 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddc490c5c5 | ||
|
|
46bac23392 | ||
|
|
07752f0741 | ||
|
|
f8ffa7382e | ||
|
|
c2ea35ef32 | ||
|
|
c0109a7ad1 | ||
|
|
33419c8f39 | ||
|
|
6790272518 | ||
|
|
740beb6dc4 | ||
|
|
3e41abadc4 | ||
|
|
0a050b0e27 | ||
|
|
fc8bd6c9d8 | ||
|
|
7e713d106e | ||
|
|
1f5ac47990 | ||
|
|
28e38c29f3 | ||
|
|
546c872c9d | ||
|
|
b0a8de7bf6 | ||
|
|
d9489c2230 | ||
|
|
b254e6d92f | ||
|
|
29b92a58c1 | ||
|
|
6c9bb732dd | ||
|
|
6d2493bae4 | ||
|
|
bc68299565 | ||
|
|
c33fa32cf1 | ||
|
|
cc326e45a6 | ||
|
|
ef356eccea | ||
|
|
db7e445c54 | ||
|
|
d654c00bd6 | ||
|
|
c00be184b5 | ||
|
|
29d2de72c8 |
+1
-1
@@ -24,7 +24,7 @@ tools:
|
||||
# used to parse JSON/YAML annotations on the fixture cache image
|
||||
- name: yq
|
||||
version:
|
||||
want: v4.53.6
|
||||
want: v4.54.1
|
||||
method: github-release
|
||||
with:
|
||||
repo: mikefarah/yq
|
||||
|
||||
@@ -30,7 +30,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup go + go-make tooling
|
||||
uses: anchore/go-make/.github/actions/setup@fa5421b3bf24c9d18f11a736c5eb4172f2b7d683 # v0.8.1
|
||||
uses: anchore/go-make/.github/actions/setup@abf41688f36f2cbec13105747abbe8e60f5c56ca # v0.9.3
|
||||
with:
|
||||
go-version: ${{ inputs.go-version }}
|
||||
cache-key-prefix: ${{ inputs.cache-key-prefix }}
|
||||
|
||||
@@ -21,6 +21,11 @@ updates:
|
||||
- dependency-name: "github.com/aquasecurity/go-version"
|
||||
- dependency-name: "github.com/knqyf263/go-apk-version"
|
||||
- dependency-name: "github.com/knqyf263/go-deb-version"
|
||||
# v1 changed SlicesAsSets hashing, which changes every artifact ID syft produces
|
||||
- dependency-name: "github.com/gohugoio/hashstructure"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# go-make module and action refs are bumped together by automation
|
||||
- dependency-name: "github.com/anchore/go-make"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directories:
|
||||
@@ -34,3 +39,6 @@ updates:
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- "dependencies"
|
||||
ignore:
|
||||
# go-make module and action refs are bumped together by automation
|
||||
- dependency-name: "anchore/go-make/*"
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
# these are checks that should be run on pull-request and merges to main.
|
||||
# we do NOT want to kick off a release if these have not been verified on main.
|
||||
# Please see the validations.yaml workflow for the names that should be used here.
|
||||
checks: '["Acceptance tests (Linux)", "Acceptance tests (Mac)", "Build snapshot artifacts", "CLI tests (Linux)", "Integration tests", "Static analysis", "Unit tests"]'
|
||||
checks: '["Acceptance tests (Linux)", "Acceptance tests (Mac)", "Build snapshot artifacts", "CLI tests (Linux)", "Containers-storage tests (Podman)", "Containers-storage tests (Buildah)", "Integration tests", "Static analysis", "Unit tests"]'
|
||||
|
||||
release:
|
||||
needs: [check-gate, version-available]
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
# for updating brew formula in anchore/homebrew-syft
|
||||
GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}
|
||||
|
||||
- uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 #v0.24.2
|
||||
- uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c #v0.24.3
|
||||
continue-on-error: true
|
||||
with:
|
||||
file: go.mod
|
||||
|
||||
@@ -51,8 +51,19 @@ jobs:
|
||||
name: "Unit tests"
|
||||
runs-on: ubuntu-latest
|
||||
needs: [Unit-Test-Linux, Unit-Test-Windows]
|
||||
# a skipped job satisfies a required check, so this job must always run and fail explicitly
|
||||
# unless every dependency succeeded (otherwise a failing PR can still be auto-merged)
|
||||
if: ${{ always() }}
|
||||
steps:
|
||||
- run: echo All unit tests passed!
|
||||
- name: Require all unit test jobs to succeed
|
||||
env:
|
||||
NEEDS: ${{ toJSON(needs) }}
|
||||
run: |
|
||||
echo "$NEEDS" | jq -e 'length > 0 and all(.[]; .result == "success")' > /dev/null || {
|
||||
echo "$NEEDS" | jq -r 'to_entries[] | "\(.key): \(.value.result)"'
|
||||
exit 1
|
||||
}
|
||||
echo All unit tests passed!
|
||||
|
||||
Unit-Test-Linux:
|
||||
name: "Unit tests (Linux)"
|
||||
@@ -85,16 +96,24 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# go-make's setup only (not ./.github/actions/bootstrap): no apt, binny tools, or fixture cache are needed
|
||||
# go-make's setup only (not ./.github/actions/bootstrap): binny ships no windows release, so binny tools
|
||||
# are unavailable here (`make unit` skips the fixture refresh on windows)
|
||||
- name: Setup go + go-make tooling
|
||||
uses: anchore/go-make/.github/actions/setup@fa5421b3bf24c9d18f11a736c5eb4172f2b7d683 # v0.8.1
|
||||
uses: anchore/go-make/.github/actions/setup@abf41688f36f2cbec13105747abbe8e60f5c56ca # v0.9.3
|
||||
with:
|
||||
# keep in sync with the go-version default in ./.github/actions/bootstrap
|
||||
go-version: "1.26.2"
|
||||
|
||||
# the full unit suite leans on linux docker fixtures, so this runs the windows-relevant subset
|
||||
# the binny-free version of `make download-test-fixture-cache` (keep the oras version in sync with .binny.yaml)
|
||||
- name: Download test fixture cache
|
||||
shell: bash
|
||||
run: |
|
||||
curl -sSfL https://github.com/oras-project/oras/releases/download/v1.3.4/oras_1.3.4_windows_amd64.zip -o "$RUNNER_TEMP/oras.zip"
|
||||
unzip -q "$RUNNER_TEMP/oras.zip" oras.exe -d "$RUNNER_TEMP"
|
||||
"$RUNNER_TEMP/oras.exe" pull ghcr.io/anchore/oss-cache/syft-test-fixture-cache:v2
|
||||
|
||||
- name: Run unit tests
|
||||
run: make unit:windows
|
||||
run: make unit
|
||||
|
||||
Integration-Test:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
@@ -158,6 +177,72 @@ jobs:
|
||||
path: snapshot/
|
||||
retention-days: 30
|
||||
|
||||
Containers-Storage-Podman:
|
||||
name: "Containers-storage tests (Podman)"
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1
|
||||
with:
|
||||
name: snapshot
|
||||
path: snapshot
|
||||
|
||||
# go-make's setup only (not ./.github/actions/bootstrap): the test only needs make + the snapshot binary
|
||||
- name: Setup go + go-make tooling
|
||||
uses: anchore/go-make/.github/actions/setup@abf41688f36f2cbec13105747abbe8e60f5c56ca # v0.9.3
|
||||
with:
|
||||
# keep in sync with the go-version default in ./.github/actions/bootstrap
|
||||
go-version: "1.26.2"
|
||||
|
||||
- name: Install Podman
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y jq podman uidmap
|
||||
|
||||
- name: Scan image from Podman's local store
|
||||
run: make containers-storage-test
|
||||
env:
|
||||
BUILDER: podman
|
||||
|
||||
Containers-Storage-Buildah:
|
||||
name: "Containers-storage tests (Buildah)"
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1
|
||||
with:
|
||||
name: snapshot
|
||||
path: snapshot
|
||||
|
||||
# go-make's setup only (not ./.github/actions/bootstrap): the test only needs make + the snapshot binary
|
||||
- name: Setup go + go-make tooling
|
||||
uses: anchore/go-make/.github/actions/setup@abf41688f36f2cbec13105747abbe8e60f5c56ca # v0.9.3
|
||||
with:
|
||||
# keep in sync with the go-version default in ./.github/actions/bootstrap
|
||||
go-version: "1.26.2"
|
||||
|
||||
- name: Install Buildah
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y buildah jq uidmap
|
||||
|
||||
- name: Scan image from Buildah's local store
|
||||
run: make containers-storage-test
|
||||
env:
|
||||
BUILDER: buildah
|
||||
|
||||
Acceptance-Linux:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "Acceptance tests (Linux)"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
version: "2"
|
||||
run:
|
||||
tests: false
|
||||
# keep in sync with buildTags in .make/main.go
|
||||
build-tags:
|
||||
- containers_image_openpgp
|
||||
- exclude_graphdriver_btrfs
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
|
||||
+4
-6
@@ -15,6 +15,7 @@ builds:
|
||||
binary: syft
|
||||
goos: [linux]
|
||||
goarch: [amd64, arm64, ppc64le, riscv64, s390x]
|
||||
tags: [containers_image_openpgp, exclude_graphdriver_btrfs]
|
||||
mod_timestamp: &build-timestamp '{{ .CommitTimestamp }}'
|
||||
ldflags: &build-ldflags |
|
||||
-w
|
||||
@@ -167,16 +168,13 @@ sboms:
|
||||
- "--output"
|
||||
- "json=$document"
|
||||
|
||||
# the signature, certificate, and transparency log proof all land in a single sigstore bundle.
|
||||
signs:
|
||||
- cmd: .tool/cosign
|
||||
signature: "${artifact}.sig"
|
||||
certificate: "${artifact}.pem"
|
||||
signature: "${artifact}.sigstore.json"
|
||||
args:
|
||||
- "sign-blob"
|
||||
- "--use-signing-config=false"
|
||||
- "--oidc-issuer=https://token.actions.githubusercontent.com"
|
||||
- "--output-certificate=${certificate}"
|
||||
- "--output-signature=${signature}"
|
||||
- "--bundle=${signature}"
|
||||
- "${artifact}"
|
||||
- "--yes"
|
||||
artifacts: checksum
|
||||
|
||||
+5
-5
@@ -1,12 +1,12 @@
|
||||
module github.com/anchore/syft/.make
|
||||
|
||||
go 1.25.8
|
||||
go 1.26.0
|
||||
|
||||
require github.com/anchore/go-make v0.8.1
|
||||
require github.com/anchore/go-make v0.9.3
|
||||
|
||||
require (
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.2 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
golang.org/x/mod v0.40.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
)
|
||||
|
||||
+8
-8
@@ -1,10 +1,10 @@
|
||||
github.com/anchore/go-make v0.8.1 h1:GAhPXR+9qVQhFB1pfVlHFOzxAt3dTDTUIA7Tp7nx9tg=
|
||||
github.com/anchore/go-make v0.8.1/go.mod h1:xALbVsPsDoJnJxdkjPlxOLZV7GbAdT5Pv3gd0kP1Lz0=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/anchore/go-make v0.9.3 h1:D+MkpvEJsOGwkOqKN0rd2zYi9jEzwKtZreNysH0DenI=
|
||||
github.com/anchore/go-make v0.9.3/go.mod h1:4HuELygGhxex2SaCEgfCbZ0geJ3xFkqtWpmt1NrYwPg=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.2 h1:eF7W7HWKg3z9NrWV9pTLnNeoXaqq3Tq9DNKXVMfoCnw=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
|
||||
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
|
||||
+31
-15
@@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -17,6 +16,12 @@ import (
|
||||
"github.com/anchore/go-make/tasks/gotest"
|
||||
)
|
||||
|
||||
// buildTags must match the linux build in .goreleaser.yaml so tests and lint compile the same code that ships
|
||||
// (containers_image_openpgp compiles in stereoscope's real containers-storage provider instead of its stub).
|
||||
// exclude_graphdriver_btrfs drops the cgo-only btrfs driver, which needs libbtrfs headers whenever cgo is on (e.g.
|
||||
// under -race); release builds are CGO_ENABLED=0 and never include it anyway.
|
||||
const buildTags = "containers_image_openpgp,exclude_graphdriver_btrfs"
|
||||
|
||||
func main() {
|
||||
Makefile(
|
||||
// shared anchore tasks
|
||||
@@ -30,6 +35,7 @@ func main() {
|
||||
gotest.Name("unit"),
|
||||
gotest.ExcludeGlob("**/test/**"),
|
||||
gotest.CoverageThreshold(62),
|
||||
gotest.Tags(buildTags),
|
||||
race(),
|
||||
),
|
||||
|
||||
@@ -53,7 +59,7 @@ func main() {
|
||||
raceFlag = " -race"
|
||||
}
|
||||
Run(
|
||||
"go test -count=1 -timeout=30m"+raceFlag+" ./cmd/syft/internal/test/integration/...",
|
||||
"go test -count=1 -timeout=30m -tags="+buildTags+raceFlag+" ./cmd/syft/internal/test/integration/...",
|
||||
run.Env("GODEBUG", "dontfreezetheworld=1"),
|
||||
)
|
||||
},
|
||||
@@ -67,7 +73,7 @@ func main() {
|
||||
Log("race detector disabled (RACE=false); skipping race smoke")
|
||||
return
|
||||
}
|
||||
Run("go run -race cmd/syft/main.go anchore/test_images:grype-quality-dotnet-69f15d2")
|
||||
Run("go run -race -tags="+buildTags+" cmd/syft/main.go anchore/test_images:grype-quality-dotnet-69f15d2")
|
||||
},
|
||||
},
|
||||
|
||||
@@ -97,19 +103,20 @@ func main() {
|
||||
},
|
||||
},
|
||||
|
||||
// windows unit tests: deliberately not hooked into "test" (no RunsOn). The full unit suite leans on
|
||||
// linux docker fixture images and posix-only assumptions, so this covers the packages that own
|
||||
// host path handling (where windows bugs live) and skips the tests that need docker image fixtures.
|
||||
// ponytail: curated package list, widen it as more of the suite is made windows-safe.
|
||||
// containers-storage tests: build a fixture image with BUILDER (podman or buildah) into an isolated local store
|
||||
// and scan it with the snapshot binary. Linux only, and not hooked into "test" since it needs the builder installed.
|
||||
Task{
|
||||
Name: "unit:windows",
|
||||
Description: "run the windows-relevant subset of unit tests",
|
||||
Name: "containers-storage-test",
|
||||
Description: "Run containers-storage tests (BUILDER=podman|buildah)",
|
||||
Run: func() {
|
||||
Run("go test -count=1"+
|
||||
` -skip "Image|Squash|AllLayers|MixFileTypes|GivenCoordinates"`+
|
||||
" ./syft/internal/windows/... ./syft/internal/fileresolver/... ./syft/source/directorysource/... ./syft/source/filesource/..."+
|
||||
" ./syft/file ./syft/file/cataloger/filedigest/... ./syft/file/cataloger/filecontent/...",
|
||||
run.Stdout(os.Stderr),
|
||||
bin := snapshotBinPath()
|
||||
if !file.Exists(bin) {
|
||||
Log("snapshot binary not found at %s; building single-target snapshot", bin)
|
||||
Run("make snapshot:single-target")
|
||||
}
|
||||
Run(
|
||||
"bash test/containers-storage/source-test.sh "+config.Env("BUILDER", "podman"),
|
||||
run.Env("SYFT_BINARY_LOCATION", bin),
|
||||
)
|
||||
},
|
||||
},
|
||||
@@ -157,7 +164,7 @@ func main() {
|
||||
Task{
|
||||
Name: "unit:syft",
|
||||
RunsOn: lang.List("unit"),
|
||||
Dependencies: Deps("refresh-fixtures"),
|
||||
Dependencies: unitFixtureDeps(),
|
||||
},
|
||||
Task{
|
||||
Name: "clean:syft",
|
||||
@@ -185,6 +192,15 @@ func raceEnabled() bool {
|
||||
return config.CI && !config.Windows
|
||||
}
|
||||
|
||||
// unitFixtureDeps skips the fixture cache refresh on windows: it needs binny-managed tools (binny ships
|
||||
// no windows release) and linux docker, so fixture-backed tests there have to skip themselves instead.
|
||||
func unitFixtureDeps() []string {
|
||||
if config.Windows {
|
||||
return nil
|
||||
}
|
||||
return Deps("refresh-fixtures")
|
||||
}
|
||||
|
||||
// race applies raceEnabled() to a gotest suite. gotest exposes no functional option
|
||||
// for the race detector, but gotest.Config.Race is exported.
|
||||
func race() gotest.Option {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Syft Contributors
|
||||
|
||||
The following Syft components were contributed by external authors/organizations.
|
||||
This file credits notable contributions from external authors and organizations; see `git log` for everyone who has contributed to Syft.
|
||||
|
||||
## GraalVM Native Image
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
Copyright (c) Anchore, Inc.
|
||||
|
||||
See LICENSE for the full license text.
|
||||
See CONTRIBUTORS.md and git log for a full list of contributors.
|
||||
@@ -61,6 +61,18 @@ syft <image> -o cyclonedx-json
|
||||
syft <image> -o spdx-json=./spdx.json -o cyclonedx-json=./cdx.json
|
||||
```
|
||||
|
||||
### Local containers-storage images
|
||||
|
||||
On Linux, syft can scan images from a local containers-storage store (e.g. built with Buildah or Podman) when asked explicitly; plain image references never look there:
|
||||
|
||||
```bash
|
||||
syft --from containers-storage localhost/myimage:latest
|
||||
|
||||
# rootless stores must be read from inside the builder's user namespace
|
||||
podman unshare syft --from containers-storage localhost/myimage:latest
|
||||
```
|
||||
|
||||
This is included in the Linux release binaries. When building from source, add `-tags containers_image_openpgp,exclude_graphdriver_btrfs`.
|
||||
|
||||
> [!TIP]
|
||||
> **Check out the [Getting Started guide](https://oss.anchore.com/docs/guides/sbom/getting-started/)** to explore all of the capabilities and features.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/internal/redact"
|
||||
)
|
||||
|
||||
func TestCommandCanRunMultipleTimes(t *testing.T) {
|
||||
// https://github.com/anchore/syft/issues/2285
|
||||
t.Cleanup(redact.Reset)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
_, cmd := create(clio.Identification{Name: "syft"}, io.Discard)
|
||||
cmd.SetArgs([]string{"cataloger", "list", "-o", "json"})
|
||||
require.NoError(t, cmd.Execute())
|
||||
|
||||
// the store is released at the end of each run so the next one can set its own
|
||||
require.Nil(t, redact.Get())
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,8 @@ func AppClioSetupConfig(id clio.Identification, out io.Writer) *clio.SetupConfig
|
||||
stereoscope.SetBus(state.Bus)
|
||||
bus.Set(state.Bus)
|
||||
|
||||
// the redact store is process-global and only cleared when a run ends (see the post-run below), so
|
||||
// setting it while another run is still in flight panics rather than splitting secrets across stores.
|
||||
redact.Set(state.RedactStore)
|
||||
|
||||
log.Set(state.Logger)
|
||||
@@ -48,8 +50,14 @@ func AppClioSetupConfig(id clio.Identification, out io.Writer) *clio.SetupConfig
|
||||
return nil
|
||||
},
|
||||
).
|
||||
WithPostRuns(func(_ *clio.State, _ error) {
|
||||
WithPostRuns(func(state *clio.State, _ error) {
|
||||
stereoscope.Cleanup() //nolint:staticcheck // we don't have access to the image object here
|
||||
|
||||
// release the redact store now that nothing in this run can report anymore, so a later run in the same
|
||||
// process (e.g. syft embedded as a library) can set its own. Only release the store this run set.
|
||||
if redact.Get() == state.RedactStore {
|
||||
redact.Reset()
|
||||
}
|
||||
})
|
||||
return clioCfg
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/go-logger/adapter/discard"
|
||||
gologgerredact "github.com/anchore/go-logger/adapter/redact"
|
||||
"github.com/anchore/syft/internal/redact"
|
||||
)
|
||||
|
||||
func TestAppClioSetupConfigInitializerPanicsWhileRunInFlight(t *testing.T) {
|
||||
t.Cleanup(redact.Reset)
|
||||
|
||||
cfg := AppClioSetupConfig(clio.Identification{Name: "syft"}, io.Discard)
|
||||
require.Len(t, cfg.Initializers, 1)
|
||||
|
||||
newState := func() *clio.State {
|
||||
return &clio.State{Logger: discard.New(), RedactStore: gologgerredact.NewStore()}
|
||||
}
|
||||
|
||||
require.NoError(t, cfg.Initializers[0](newState()))
|
||||
|
||||
// a second run starting before the first has finished must not silently replace the store, otherwise
|
||||
// secrets added to the first store would no longer be redacted
|
||||
require.PanicsWithValue(t, "replace existing redaction store (probably unintentional)", func() {
|
||||
_ = cfg.Initializers[0](newState())
|
||||
})
|
||||
}
|
||||
@@ -114,6 +114,10 @@ func RunConvert(ctx context.Context, opts *ConvertOptions, userInput string) err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// output files are opened up front; release them if we bail out before writing
|
||||
if c, ok := writer.(io.Closer); ok {
|
||||
defer func() { _ = c.Close() }()
|
||||
}
|
||||
}
|
||||
|
||||
for _, output := range unchanged {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -181,6 +182,10 @@ func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, use
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// output files are opened up front; release them if we bail out before writing
|
||||
if c, ok := writer.(io.Closer); ok {
|
||||
defer func() { _ = c.Close() }()
|
||||
}
|
||||
|
||||
sources := opts.From
|
||||
if len(sources) == 0 {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -22,9 +23,10 @@ func Test_defaultDir(t *testing.T) {
|
||||
homeDir := filepath.Join(tmpDir, "fake-home")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
expected string
|
||||
name string
|
||||
env map[string]string
|
||||
expected string
|
||||
skipOnWindows bool
|
||||
}{
|
||||
{
|
||||
name: "no-xdg",
|
||||
@@ -32,6 +34,8 @@ func Test_defaultDir(t *testing.T) {
|
||||
"HOME": homeDir,
|
||||
},
|
||||
expected: homeDir,
|
||||
// xdg falls back to LOCALAPPDATA on windows, not HOME
|
||||
skipOnWindows: true,
|
||||
},
|
||||
{
|
||||
name: "xdg-cache",
|
||||
@@ -59,6 +63,9 @@ func Test_defaultDir(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if test.skipOnWindows && runtime.GOOS == "windows" {
|
||||
t.Skip("posix-only cache dir fallback")
|
||||
}
|
||||
restoreCache(t)
|
||||
|
||||
for k, v := range test.env {
|
||||
|
||||
@@ -36,7 +36,7 @@ var _ clio.PostLoader = (*imageSource)(nil)
|
||||
func (o *sourceConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
|
||||
descriptions.Add(&o.File.Digests, `the file digest algorithms to use on the scanned file (options: "md5", "sha1", "sha224", "sha256", "sha384", "sha512")`)
|
||||
descriptions.Add(&o.Image.DefaultPullSource, `allows users to specify which image source should be used to generate the sbom
|
||||
valid values are: registry, docker, podman`)
|
||||
valid values are: registry, docker, podman, containers-storage`)
|
||||
}
|
||||
|
||||
type imageSource struct {
|
||||
@@ -74,7 +74,7 @@ func (c *imageSource) PostLoad() error {
|
||||
return checkDefaultSourceValues(c.DefaultPullSource)
|
||||
}
|
||||
|
||||
var validDefaultSourceValues = []string{"registry", "docker", "podman", ""}
|
||||
var validDefaultSourceValues = []string{"registry", "docker", "podman", "containers-storage", ""}
|
||||
|
||||
func checkDefaultSourceValues(source string) error {
|
||||
validValues := strset.New(validDefaultSourceValues...)
|
||||
|
||||
@@ -2,10 +2,11 @@ package options
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -20,7 +21,10 @@ import (
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
var _ sbom.Writer = (*sbomMultiWriter)(nil)
|
||||
var _ interface {
|
||||
io.Closer
|
||||
sbom.Writer
|
||||
} = (*sbomMultiWriter)(nil)
|
||||
|
||||
var _ interface {
|
||||
io.Closer
|
||||
@@ -164,6 +168,12 @@ func newSBOMMultiWriter(options ...sbomWriterDescription) (_ *sbomMultiWriter, e
|
||||
}
|
||||
|
||||
out := &sbomMultiWriter{}
|
||||
defer func() {
|
||||
// don't leak the files already opened when a later output fails
|
||||
if err != nil {
|
||||
_ = out.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
for _, option := range options {
|
||||
switch len(option.Path) {
|
||||
@@ -172,8 +182,8 @@ func newSBOMMultiWriter(options ...sbomWriterDescription) (_ *sbomMultiWriter, e
|
||||
format: option.Format,
|
||||
})
|
||||
default:
|
||||
// create any missing subdirectories
|
||||
dir := path.Dir(option.Path)
|
||||
// create any missing subdirectories (option.Path is a host path, so this must be filepath)
|
||||
dir := filepath.Dir(option.Path)
|
||||
if dir != "" {
|
||||
s, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
@@ -210,6 +220,20 @@ func (m *sbomMultiWriter) Write(s sbom.SBOM) (errs error) {
|
||||
return errs
|
||||
}
|
||||
|
||||
// Close releases any files held by the writers. Write already closes each file it writes to, so this matters
|
||||
// when the SBOM is never written (e.g. an error after the writer was created).
|
||||
func (m *sbomMultiWriter) Close() error {
|
||||
var errs error
|
||||
for _, w := range m.writers {
|
||||
if c, ok := w.(io.Closer); ok {
|
||||
if err := c.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// sbomStreamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup
|
||||
type sbomStreamWriter struct {
|
||||
format sbom.FormatEncoder
|
||||
|
||||
@@ -202,9 +202,9 @@ func Test_newSBOMMultiWriter(t *testing.T) {
|
||||
if test.err {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = mw.Close() })
|
||||
|
||||
assert.Len(t, mw.writers, len(test.expected))
|
||||
|
||||
|
||||
@@ -18,16 +18,16 @@ require (
|
||||
github.com/anchore/go-homedir v0.1.1
|
||||
github.com/anchore/go-logger v0.2.0
|
||||
github.com/anchore/go-macholibre v0.1.1
|
||||
github.com/anchore/go-rpmdb v0.2.0
|
||||
github.com/anchore/go-rpmdb v0.2.2
|
||||
github.com/anchore/go-sync v0.1.2
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||
github.com/anchore/packageurl-go v0.2.0
|
||||
github.com/anchore/stereoscope v0.3.3
|
||||
github.com/anchore/stereoscope v0.3.4
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/aquasecurity/go-pep440-version v0.0.1
|
||||
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.2
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/charmbracelet/bubbles v1.0.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
@@ -41,8 +41,8 @@ require (
|
||||
github.com/facebookincubator/nvdtools v0.1.5
|
||||
github.com/github/go-spdx/v2 v2.7.0
|
||||
github.com/gkampitakis/go-snaps v0.5.23
|
||||
github.com/go-git/go-billy/v5 v5.9.1
|
||||
github.com/go-git/go-git/v5 v5.19.2
|
||||
github.com/go-git/go-billy/v5 v5.9.2
|
||||
github.com/go-git/go-git/v5 v5.19.3
|
||||
github.com/go-test/deep v1.1.1
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||
github.com/goccy/go-yaml v1.19.2
|
||||
@@ -52,7 +52,7 @@ require (
|
||||
github.com/google/licensecheck v0.3.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gookit/color v1.6.1
|
||||
github.com/gpustack/gguf-parser-go v0.26.3
|
||||
github.com/gpustack/gguf-parser-go v0.26.4
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2
|
||||
github.com/hashicorp/go-getter v1.8.9
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
@@ -62,7 +62,7 @@ require (
|
||||
github.com/jedib0t/go-pretty/v6 v6.8.3
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
|
||||
github.com/klauspost/compress v1.20.0
|
||||
github.com/klauspost/compress v1.20.1
|
||||
github.com/magiconair/properties v1.18.12
|
||||
github.com/mholt/archives v0.1.5
|
||||
github.com/moby/sys/mountinfo v0.7.2
|
||||
@@ -83,7 +83,7 @@ require (
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/ulikunitz/xz v0.5.16
|
||||
github.com/ulikunitz/xz v0.5.17
|
||||
github.com/vbatts/go-mtree v0.7.0
|
||||
github.com/vifraa/gopom v1.0.0
|
||||
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651
|
||||
@@ -96,12 +96,12 @@ require (
|
||||
golang.org/x/mod v0.41.0
|
||||
golang.org/x/net v0.59.0
|
||||
golang.org/x/time v0.16.0
|
||||
golang.org/x/tools v0.50.0
|
||||
modernc.org/sqlite v1.59.0
|
||||
golang.org/x/tools v0.51.0
|
||||
modernc.org/sqlite v1.60.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/pb33f/ordered-map/v2 v2.3.1
|
||||
github.com/pb33f/ordered-map/v2 v2.3.2
|
||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd
|
||||
howett.net/plist v1.0.1
|
||||
)
|
||||
@@ -188,7 +188,7 @@ require (
|
||||
github.com/containers/ocicrypt v1.3.2 // indirect
|
||||
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.7.0 // indirect
|
||||
github.com/docker/cli v29.8.1+incompatible // indirect
|
||||
github.com/docker/cli v29.8.2+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.8 // indirect
|
||||
github.com/docker/go-connections v0.8.1 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
@@ -224,7 +224,6 @@ require (
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
@@ -265,11 +264,12 @@ require (
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.3.0 // indirect
|
||||
github.com/opencontainers/selinux v1.15.1 // indirect
|
||||
github.com/pb33f/go-yaml v0.1.0 // indirect
|
||||
github.com/pborman/indent v1.2.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.26 // indirect
|
||||
github.com/piprate/json-gold v0.7.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.7.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pkg/profile v1.7.0 // indirect
|
||||
github.com/pkg/xattr v0.4.12 // indirect
|
||||
@@ -281,11 +281,11 @@ require (
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.1 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.11.1 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sigstore/fulcio v1.8.7 // indirect
|
||||
github.com/sigstore/protobuf-specs v0.5.1 // indirect
|
||||
github.com/sigstore/sigstore v1.10.8 // indirect
|
||||
github.com/sigstore/protobuf-specs v0.5.2 // indirect
|
||||
github.com/sigstore/sigstore v1.10.11 // indirect
|
||||
github.com/sirupsen/logrus v1.10.2 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d // indirect
|
||||
@@ -299,7 +299,7 @@ require (
|
||||
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/sylabs/sif/v2 v2.24.1 // indirect
|
||||
github.com/sylabs/sif/v2 v2.24.2 // indirect
|
||||
github.com/sylabs/squashfs v1.0.6 // indirect
|
||||
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
|
||||
github.com/therootcompany/xz v1.0.1 // indirect
|
||||
@@ -328,7 +328,6 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.46.0 // indirect
|
||||
go.podman.io/image/v5 v5.41.2 // indirect
|
||||
go.podman.io/storage v1.64.1 // indirect
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
@@ -345,7 +344,7 @@ require (
|
||||
google.golang.org/grpc v1.83.2 // indirect
|
||||
google.golang.org/protobuf v1.36.12 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
modernc.org/libc v1.75.7 // indirect
|
||||
modernc.org/libc v1.77.1 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.12.1 // indirect
|
||||
)
|
||||
|
||||
@@ -140,8 +140,8 @@ github.com/anchore/go-lzo v0.1.1 h1:IwL/fvkdtlIrYIXck6WxZ3nb8WjjHziYYmGxlooyOnM=
|
||||
github.com/anchore/go-lzo v0.1.1/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=
|
||||
github.com/anchore/go-macholibre v0.1.1 h1:sTBHi3S1WVIOCamc6NyT8Sn9mBmD30DCTfjfVm4k6/M=
|
||||
github.com/anchore/go-macholibre v0.1.1/go.mod h1:YNq2610RlvGCK0Za+Klz/OCMtUaEnwwAGA/VfUAQaro=
|
||||
github.com/anchore/go-rpmdb v0.2.0 h1:ixcyMg/guhBL9XjBVwhTv9CG48xO07Qkg2uDX2+zx/c=
|
||||
github.com/anchore/go-rpmdb v0.2.0/go.mod h1:ATsRlpCXstnoYfzqBfhwGw0U2dolx1BBQ9rAU8jWBAw=
|
||||
github.com/anchore/go-rpmdb v0.2.2 h1:44VC7mvFAxV9t5JUZYg3S9sqG+E94iZvyWACMDq9hSk=
|
||||
github.com/anchore/go-rpmdb v0.2.2/go.mod h1:Y64EbetdqhryMjtk8IzXkdCQw/aNDx+feZCT9aj6rjs=
|
||||
github.com/anchore/go-struct-converter v0.2.0-rc2 h1:q+859fW2/jbHJHB2etbNfRlFwYpknyvbqqk1hUdamQ4=
|
||||
github.com/anchore/go-struct-converter v0.2.0-rc2/go.mod h1:cDBA5vhcR62nXWo8QH9/Kk2807o65ISaHPNPX66L+Uw=
|
||||
github.com/anchore/go-sync v0.1.2 h1:RDD5RCanrWqhbtBulqPSZpERadUlgcYjpybYzYd3Uk8=
|
||||
@@ -150,8 +150,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
|
||||
github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk=
|
||||
github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE=
|
||||
github.com/anchore/stereoscope v0.3.3 h1:sur8G2THAOnOrSXDjIc1SXzQwco8hjFBtGFq1jFvNVA=
|
||||
github.com/anchore/stereoscope v0.3.3/go.mod h1:LxL4iOFtB8XMbA1MykUUTkmtQmDuYdbr4+TkBkCfljU=
|
||||
github.com/anchore/stereoscope v0.3.4 h1:dT5AfjCTXUJowMiRIc+VBQVwvwnIEwnxxwA1GwAkMxw=
|
||||
github.com/anchore/stereoscope v0.3.4/go.mod h1:+HxqHTt7dvH8mea0mf3ulqkf084VvHiT1grpAQf5uEs=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
@@ -229,8 +229,8 @@ github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef h1:TSFnfbbu2oAO
|
||||
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef/go.mod h1:9iglf1GG4oNRJ39bZ5AZrjgAFD2RwQbXw6Qf7Cs47wo=
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.2 h1:eF7W7HWKg3z9NrWV9pTLnNeoXaqq3Tq9DNKXVMfoCnw=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
|
||||
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
||||
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
|
||||
@@ -353,8 +353,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/docker/cli v29.8.1+incompatible h1:qYL1bCp6cRw2SB1xmLlIOPyV171dilw9W2Jew38vy9c=
|
||||
github.com/docker/cli v29.8.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.8.2+incompatible h1:2zgdFuoFst2T80oS42vhKGxkd0JRo305eIEKTsxR7pQ=
|
||||
github.com/docker/cli v29.8.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker-credential-helpers v0.9.8 h1:bIREROb7So6PRlq6KTtdS9MPEjC29OQRkFNlvK2OX8Q=
|
||||
@@ -428,18 +428,18 @@ github.com/gkampitakis/ciinfo v0.3.4 h1:5eBSibVuSMbb/H6Elc0IIEFbkzCJi3lm94n0+U7Z
|
||||
github.com/gkampitakis/ciinfo v0.3.4/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
|
||||
github.com/gkampitakis/go-snaps v0.5.23 h1:okh5QR48zpUjpWtu65AtqxdCY8huJq+dEDuUzd1PuKg=
|
||||
github.com/gkampitakis/go-snaps v0.5.23/go.mod h1:uy3lVzCCRRsAwYqSocyw5fY8xRLCYEfqoOJNxr8HonM=
|
||||
github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
|
||||
github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0=
|
||||
github.com/glebarez/go-sqlite v1.23.0 h1:FyhIq4jqmgphQAUlY79zPldYGwISEZikaDfhiGWkkaI=
|
||||
github.com/glebarez/go-sqlite v1.23.0/go.mod h1:IIYrOH3L0rHY3jb4IXOHoWdklNajSGUN2eJcvK8WrnI=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.9.1 h1:8U73XiOTfINdItHVa6z4Gv7ToObcZ6grkqQbLryLCdA=
|
||||
github.com/go-git/go-billy/v5 v5.9.1/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
|
||||
github.com/go-git/go-billy/v5 v5.9.2 h1:OXFSRyz4g20upsGDJgQG9Bak1l/ZEv8GHVYB52O71sE=
|
||||
github.com/go-git/go-billy/v5 v5.9.2/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
|
||||
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
|
||||
github.com/go-git/go-git/v5 v5.19.3 h1:qHttbZ+Am7wEjdTpR1XMQ4rl42FK9SIXzZ4o9x7s6M4=
|
||||
github.com/go-git/go-git/v5 v5.19.3/go.mod h1:Ye4C8dVigkqrv9UsB4NMdSLV4yAIkLiIhW61gcOyhl4=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@@ -579,8 +579,8 @@ github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
|
||||
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gpustack/gguf-parser-go v0.26.3 h1:F63PlUPIW56HGU5k4Crv0JAG9ojtz/TuwzgMYlb82Ec=
|
||||
github.com/gpustack/gguf-parser-go v0.26.3/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=
|
||||
github.com/gpustack/gguf-parser-go v0.26.4 h1:B9CRrQBcIB7I/MJFZvzPZ+e+cbc1xBzQJcmpi9l/mKU=
|
||||
github.com/gpustack/gguf-parser-go v0.26.4/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 h1:mymLUKThnV9wFvogOK8NnsMP9/vlhnjXY98gr2QIGW8=
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74/go.mod h1:Bh9qYL8ehmDxSg14Tk8oxFCP90XHfs6NxV1D84884xA=
|
||||
@@ -666,11 +666,9 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@@ -808,8 +806,10 @@ github.com/opencontainers/selinux v1.15.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZ
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY=
|
||||
github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ=
|
||||
github.com/pb33f/go-yaml v0.1.0 h1:zZ6Gg+ATbumXlXZNLMHIe7+zTkVqe20lx+TSYRrXEuY=
|
||||
github.com/pb33f/go-yaml v0.1.0/go.mod h1:QvWdkHP5VjFYAeHMQUkaPQqREaQnBNOXmWzAjNrnSdo=
|
||||
github.com/pb33f/ordered-map/v2 v2.3.2 h1:wDyaZ2Pv9QLh64X4utCeD5Zoi9nIv2aW3lwv172O5PQ=
|
||||
github.com/pb33f/ordered-map/v2 v2.3.2/go.mod h1:1OhFrXu3OYw3kM+FXF+Ug3ugmmZ9LE8z0JfQMLvtV0w=
|
||||
github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM=
|
||||
github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw=
|
||||
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
@@ -821,8 +821,8 @@ github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY
|
||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/piprate/json-gold v0.7.0 h1:bEMirgA5y8Z2loTQfxyIFfY+EflxH1CTP6r/KIlcJNw=
|
||||
github.com/piprate/json-gold v0.7.0/go.mod h1:RVhE35veDX19r5gfUAR+IYHkAUuPwJO8Ie/qVeFaIzw=
|
||||
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
||||
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
github.com/pjbgf/sha1cd v0.7.0 h1:ZRNPKHj+gfkLBf0KJv/p1Hmz+7bqCT5o311YX0nq+DA=
|
||||
github.com/pjbgf/sha1cd v0.7.0/go.mod h1:pKR5Li+qTCo+ebqITZfwPlJGoCFCvSO00qxjbNtTUFQ=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -893,8 +893,8 @@ github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd7
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=
|
||||
github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.11.0 h1:iuCR9kcMFD4QurdKrGvPLoKZLv9YvwPYVr0473BdtFs=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.11.0/go.mod h1:+PMOTjUGwHj2vcZ+TFKlb1tXRbrdWE1LYDT5i9JC80Q=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.11.1 h1:ayahDPjfSIqKegyt5YVGEvQ7SAi72GHXTy+b2YjUd5w=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.11.1/go.mod h1:UyOjhoZLi76ir63u1ptubEevVe6dlNPmNxwgVvKqFFY=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
@@ -903,10 +903,10 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sigstore/fulcio v1.8.7 h1:d7QJQxukgXarqPCT9uDNeyzt1Bbt/biw58/VfOnwI/o=
|
||||
github.com/sigstore/fulcio v1.8.7/go.mod h1:7fX+QyigZxRGKTgHvTfOfxwwniCDxK02PVloHy5/vg4=
|
||||
github.com/sigstore/protobuf-specs v0.5.1 h1:/5OPaNuolRJmQfeZLayJGFXMpsRJEdgC6ah1/+7Px7U=
|
||||
github.com/sigstore/protobuf-specs v0.5.1/go.mod h1:DRBzpFuE+LnvQMN10/dU6nBeKwVLGEQ6o2FovN2Rats=
|
||||
github.com/sigstore/sigstore v1.10.8 h1:1Mgkxvkw4AXMfIP1DOjc6kw0GkUgA8pGVpveN/EfOq4=
|
||||
github.com/sigstore/sigstore v1.10.8/go.mod h1:f9+B/4iaYimvUkySyb2mvc73n3RLqNn24grHZM/ET8M=
|
||||
github.com/sigstore/protobuf-specs v0.5.2 h1:RSWWUY8QrVTxbYH00jY/jg2e7YnjzrpwP+PeHTMll0E=
|
||||
github.com/sigstore/protobuf-specs v0.5.2/go.mod h1:DRBzpFuE+LnvQMN10/dU6nBeKwVLGEQ6o2FovN2Rats=
|
||||
github.com/sigstore/sigstore v1.10.11 h1:fjrwDfR0AWmRONYV/hKGJyqbT0osMtaBz9FTZ6MH9XI=
|
||||
github.com/sigstore/sigstore v1.10.11/go.mod h1:ecPk+ftLqP8/TmzQFJmn0+QrL2yUQIJvyzVKNmDiw6U=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
@@ -970,8 +970,8 @@ github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUm
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/sylabs/sif/v2 v2.24.1 h1:OhTOfTwBaGXfbWYWXK9C6Pojma3A1bTaNJ6CEhyuKok=
|
||||
github.com/sylabs/sif/v2 v2.24.1/go.mod h1:PRq9MoP0g+p0qE5ZRGrOhyVAUdHrPsAVmXHgoiW6VU0=
|
||||
github.com/sylabs/sif/v2 v2.24.2 h1:rdkFf6z3xLC6yKweU7S9marlKfkS91Ht0ruIri6YWFY=
|
||||
github.com/sylabs/sif/v2 v2.24.2/go.mod h1:bbrkk0JhTX+xNJcnUtmDkivoO2r4DGWM3LxNR0U2ooA=
|
||||
github.com/sylabs/squashfs v1.0.6 h1:PvJcDzxr+vIm2kH56mEMbaOzvGu79gK7P7IX+R7BDZI=
|
||||
github.com/sylabs/squashfs v1.0.6/go.mod h1:DlDeUawVXLWAsSRa085Eo0ZenGzAB32JdAUFaB0LZfE=
|
||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA=
|
||||
@@ -994,8 +994,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.16 h1:ld6NyySjx5lowVKwJvMRLnW5nxKX/xnpSiFYZ/Lxur0=
|
||||
github.com/ulikunitz/xz v0.5.16/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw=
|
||||
github.com/ulikunitz/xz v0.5.17 h1:flR0y/x1hgM8EGV1AW3Xll6T413G0glV8UfBwR617V4=
|
||||
github.com/ulikunitz/xz v0.5.17/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw=
|
||||
github.com/vbatts/go-mtree v0.7.0 h1:ytmOc3MTRidZiBi9VBCyZ2BHe4fZS47L5v7BVXDWW4E=
|
||||
github.com/vbatts/go-mtree v0.7.0/go.mod h1:EjdpFC+LZy1TXbRGNa1MKKgjQ+7ew3foMFJK8o4/TdY=
|
||||
github.com/vbatts/tar-split v0.12.3 h1:Cd46rkGXI3Td4yrVNwU8ripbxFaQbmesqhjBUUYAJSw=
|
||||
@@ -1084,8 +1084,6 @@ go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -1412,8 +1410,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
|
||||
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
|
||||
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
|
||||
golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1610,10 +1608,10 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
||||
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
modernc.org/cc/v4 v4.29.2 h1:h6+9ciCnPKutf4I03CvheAvDLX7+IHlqR6Iy6J+cgd8=
|
||||
modernc.org/cc/v4 v4.29.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.35.0 h1:F+TUsmw09QxLzmi3aeYYGxjAXarmZaKgj3mKQHNaA8w=
|
||||
modernc.org/ccgo/v4 v4.35.0/go.mod h1:qrVGs9S3Sr2Ztcg9ve+kTAYMp5a3YvWjo+SoN06kJ5I=
|
||||
modernc.org/cc/v4 v4.29.7 h1:q+NXGJ0bK3b4TXFYQQVr9pYETGnmwFWkrUzJnMya/Tg=
|
||||
modernc.org/cc/v4 v4.29.7/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.36.1 h1:ZNIUZAryN0UgnJwtyxrdEzcFc3yD4Cu4AzjfPXsLsIE=
|
||||
modernc.org/ccgo/v4 v4.36.1/go.mod h1:rrtGc2QkS239nYb/mQNuBMyjq3/y3ZXWbBjPoV3wqzA=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
@@ -1622,8 +1620,8 @@ modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc=
|
||||
modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.75.7 h1:o3DTP9/0p9pKmY2WCKQaySW6wIiZhNM7wc2lUoyhfew=
|
||||
modernc.org/libc v1.75.7/go.mod h1:bO5o2ztHxBb2rjz0PgdHN0sSMw57CgxGFLZ3Qd/QpVQ=
|
||||
modernc.org/libc v1.77.1 h1:Ct8j47QtiZ1Enj2DtFXQtUqrPCAjdCmPjtCuvrYQ0Hs=
|
||||
modernc.org/libc v1.77.1/go.mod h1:87/pZ4L6nD1zqW4nItuS12YO7hN1igAah34xjnQo/W0=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
|
||||
@@ -1632,8 +1630,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.59.0 h1:X1es1GpqBlS/5T+vbM4HLUdaa8OtQx468DF2vrx+38A=
|
||||
modernc.org/sqlite v1.59.0/go.mod h1:+paeT2A3iPRHkQDwG7oA6Tk0zQd5woMEI8q7orfry8k=
|
||||
modernc.org/sqlite v1.60.1 h1:/blz53O951KWFOso4QQvEs/Fq6cDBKLtMVrYNSeJVKw=
|
||||
modernc.org/sqlite v1.60.1/go.mod h1:1dIoEagfDE72QytD5scH1lxARtaUgKgHC/NuApA27r0=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
+49
-20
@@ -8,7 +8,6 @@ REPO="${PROJECT_NAME}"
|
||||
GITHUB_DOWNLOAD_PREFIX=https://github.com/${OWNER}/${REPO}/releases/download
|
||||
INSTALL_SH_BASE_URL=https://get.anchore.io/${PROJECT_NAME}
|
||||
LEGACY_INSTALL_SH_BASE_URL=https://raw.githubusercontent.com/${OWNER}/${PROJECT_NAME}
|
||||
PROGRAM_ARGS=$@
|
||||
|
||||
# signature verification options
|
||||
|
||||
@@ -19,6 +18,8 @@ VERIFY_SIGN=false
|
||||
VERIFY_SIGN_SUPPORTED_VERSION=v0.104.0
|
||||
# this is the earliest tag in the repo where the -v flag was introduced to this install.sh script
|
||||
VERIFY_SIGN_FLAG_VERSION=v1.6.0
|
||||
# releases at or after this version are signed with a sigstore bundle (.sigstore.json); earlier releases ship a separate .sig and .pem
|
||||
VERIFY_SIGN_BUNDLE_VERSION=v1.53.0
|
||||
|
||||
# do not change the name of this parameter (this must always be backwards compatible)
|
||||
DOWNLOAD_TAG_INSTALL_SCRIPT=${DOWNLOAD_TAG_INSTALL_SCRIPT:-true}
|
||||
@@ -386,8 +387,8 @@ download_github_release_checksums_files() (
|
||||
complete_url=$(github_release_asset_url "${download_url}" "${name}" "${version}" "${filename}")
|
||||
output_path="${output_dir}/${complete_filename}"
|
||||
|
||||
http_download "${output_path}" "${complete_url}" ""
|
||||
asset_file_exists "${output_path}"
|
||||
http_download "${output_path}" "${complete_url}" "" || return 1
|
||||
asset_file_exists "${output_path}" || return 1
|
||||
|
||||
log_trace "download_github_release_checksums_files() returned '${output_path}' for file '${complete_filename}'"
|
||||
|
||||
@@ -585,28 +586,31 @@ download_and_install_asset() (
|
||||
install_asset "${asset_filepath}" "${install_path}" "${binary}"
|
||||
)
|
||||
|
||||
# verify_sign [checksums-file-path] [certificate-reference] [signature-reference] [version]
|
||||
# verify_sign [checksums-file-path] [verification-material-flags...]
|
||||
#
|
||||
# attempts verify the signature of the checksums file from the release workflow in Github Actions run against the main branch.
|
||||
# the verification material is either "--bundle <path>" or (for older releases) "--certificate <ref> --signature <ref>".
|
||||
#
|
||||
verify_sign() {
|
||||
checksums_file=$1
|
||||
cert_reference=$2
|
||||
sig_reference=$3
|
||||
shift
|
||||
|
||||
log_trace "verifying artifact $1"
|
||||
log_trace "verifying artifact ${checksums_file}"
|
||||
|
||||
log_file=$(mktemp)
|
||||
|
||||
${COSIGN_BINARY} \
|
||||
verify-blob "$checksums_file" \
|
||||
--certificate "$cert_reference" \
|
||||
--signature "$sig_reference" \
|
||||
"$@" \
|
||||
--certificate-identity "https://github.com/${OWNER}/${REPO}/.github/workflows/release.yaml@refs/heads/main" \
|
||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" > "${log_file}" 2>&1
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
log_err "$(cat "${log_file}")"
|
||||
# cosign older than v2.4.2 can't read the bundle's verification material and reports a missing cert
|
||||
if [ "$1" = "--bundle" ] && grep -q "bundle does not contain cert" "${log_file}"; then
|
||||
log_err "note: releases >= ${VERIFY_SIGN_BUNDLE_VERSION} are verified with a sigstore bundle, which requires cosign v2.5.0 or newer (check '${COSIGN_BINARY} version')"
|
||||
fi
|
||||
rm -f "${log_file}"
|
||||
return 1
|
||||
fi
|
||||
@@ -630,7 +634,10 @@ download_asset() (
|
||||
|
||||
log_trace "download_asset(url=${download_url}, destination=${destination}, name=${name}, os=${os}, arch=${arch}, version=${version}, format=${format})"
|
||||
|
||||
checksums_filepath=$(download_github_release_checksums "${download_url}" "${name}" "${version}" "${destination}")
|
||||
if ! checksums_filepath=$(download_github_release_checksums "${download_url}" "${name}" "${version}" "${destination}"); then
|
||||
log_err "unable to download checksums file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_trace "checksums content:\n$(cat ${checksums_filepath})"
|
||||
|
||||
@@ -642,13 +649,25 @@ download_asset() (
|
||||
fi
|
||||
|
||||
if [ "$VERIFY_SIGN" = true ]; then
|
||||
checksum_sig_file_url=$(github_release_checksums_sig_url "${download_url}" "${name}" "${version}")
|
||||
log_trace "checksums signature url: ${checksum_sig_file_url}"
|
||||
if compare_semver "${version}" "${VERIFY_SIGN_BUNDLE_VERSION}"; then
|
||||
if ! checksums_bundle_filepath=$(download_github_release_checksums_files "${download_url}" "${name}" "${version}" "${destination}" "checksums.txt.sigstore.json"); then
|
||||
log_err "unable to download checksums signature bundle"
|
||||
return 1
|
||||
fi
|
||||
log_trace "checksums bundle: ${checksums_bundle_filepath}"
|
||||
|
||||
checksums_cert_file_url=$(github_release_checksums_cert_url "${download_url}" "${name}" "${version}")
|
||||
log_trace "checksums certificate url: ${checksums_cert_file_url}"
|
||||
set -- --bundle "${checksums_bundle_filepath}"
|
||||
else
|
||||
checksum_sig_file_url=$(github_release_checksums_sig_url "${download_url}" "${name}" "${version}")
|
||||
log_trace "checksums signature url: ${checksum_sig_file_url}"
|
||||
|
||||
if ! verify_sign "${checksums_filepath}" "${checksums_cert_file_url}" "${checksum_sig_file_url}"; then
|
||||
checksums_cert_file_url=$(github_release_checksums_cert_url "${download_url}" "${name}" "${version}")
|
||||
log_trace "checksums certificate url: ${checksums_cert_file_url}"
|
||||
|
||||
set -- --certificate "${checksums_cert_file_url}" --signature "${checksum_sig_file_url}"
|
||||
fi
|
||||
|
||||
if ! verify_sign "${checksums_filepath}" "$@"; then
|
||||
log_err "signature verification failed"
|
||||
return 1
|
||||
fi
|
||||
@@ -657,9 +676,15 @@ download_asset() (
|
||||
|
||||
asset_url="${download_url}/${asset_filename}"
|
||||
asset_filepath="${destination}/${asset_filename}"
|
||||
http_download "${asset_filepath}" "${asset_url}" ""
|
||||
if ! http_download "${asset_filepath}" "${asset_url}" ""; then
|
||||
log_err "unable to download asset '${asset_url}'"
|
||||
return 1
|
||||
fi
|
||||
|
||||
hash_sha256_verify "${asset_filepath}" "${checksums_filepath}"
|
||||
# this is what ties the asset to the (optionally signature verified) checksums file, so a mismatch must stop the install
|
||||
if ! hash_sha256_verify "${asset_filepath}" "${checksums_filepath}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_trace "download_asset_by_checksums_file() returned '${asset_filepath}'"
|
||||
|
||||
@@ -765,7 +790,7 @@ prep_signature_verification() {
|
||||
fi
|
||||
}
|
||||
|
||||
main() (
|
||||
parse_install_args() {
|
||||
# parse arguments
|
||||
|
||||
# note: never change default install directory (this must always be backwards compatible)
|
||||
@@ -805,6 +830,11 @@ EOF
|
||||
|
||||
set +u
|
||||
tag=$1
|
||||
set -u
|
||||
}
|
||||
|
||||
main() (
|
||||
parse_install_args "$@"
|
||||
|
||||
if [ -z "${tag}" ]; then
|
||||
log_info "checking github for the current release tag"
|
||||
@@ -812,7 +842,6 @@ EOF
|
||||
else
|
||||
log_info "checking github for release tag='${tag}'"
|
||||
fi
|
||||
set -u
|
||||
|
||||
if ! tag=$(get_release_tag "${OWNER}" "${REPO}" "${tag}"); then
|
||||
log_err "unable to find tag='${tag}'"
|
||||
@@ -843,7 +872,7 @@ EOF
|
||||
log_warn "failed to fetch from ${INSTALL_SH_BASE_URL}, trying fallback URL"
|
||||
install_script=$(http_copy "${LEGACY_INSTALL_SH_BASE_URL}/${tag}/install.sh" "")
|
||||
fi
|
||||
echo "${install_script}" | sh -s -- ${PROGRAM_ARGS}
|
||||
echo "${install_script}" | sh -s -- "$@"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
|
||||
Vendored
+4
@@ -6,6 +6,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -95,6 +96,9 @@ func Test_makeDiskKey(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_errors(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("windows ignores unix permission bits on directories")
|
||||
}
|
||||
tmp := t.TempDir()
|
||||
cache := filepath.Join(tmp, "cache")
|
||||
// make a non-writable directory
|
||||
|
||||
@@ -212,7 +212,7 @@ func parseGenericCatalogerFunction(funcDecl *ast.FuncDecl, filePath, repoRoot st
|
||||
// extractPackageNameFromPath extracts the package name from a cataloger source file path
|
||||
// e.g., "syft/pkg/cataloger/swift/cataloger.go" -> "swift"
|
||||
func extractPackageNameFromPath(filePath string) string {
|
||||
parts := strings.Split(filePath, string(filepath.Separator))
|
||||
parts := strings.Split(filepath.ToSlash(filePath), "/")
|
||||
|
||||
// find the LAST occurrence of "cataloger" in the path
|
||||
// (to handle test fixtures with multiple "cataloger" segments)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -337,7 +338,7 @@ func TestResolveImportPath(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := resolveImportPath(tt.importPath, testRepoRoot)
|
||||
require.Equal(t, tt.want, got)
|
||||
require.Equal(t, filepath.FromSlash(tt.want), got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/diskfs/go-diskfs/filesystem"
|
||||
@@ -25,7 +25,8 @@ func WalkDiskDir(fsys filesystem.FileSystem, root string, fn WalkDiskDirFunc) er
|
||||
}
|
||||
|
||||
for _, info := range infos {
|
||||
p := filepath.Join(root, info.Name())
|
||||
// squashfs paths are posix regardless of host os
|
||||
p := path.Join(root, info.Name())
|
||||
err = walkDiskDir(fsys, p, info, fn)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.SkipDir) {
|
||||
@@ -74,7 +75,7 @@ func walkDiskDir(fsys filesystem.FileSystem, name string, d os.FileInfo, walkDir
|
||||
}
|
||||
|
||||
for _, d1 := range dirs {
|
||||
name1 := filepath.Join(name, d1.Name())
|
||||
name1 := path.Join(name, d1.Name())
|
||||
if err := walkDiskDir(fsys, name1, d1, walkDirFn); err != nil {
|
||||
if errors.Is(err, fs.SkipDir) {
|
||||
break
|
||||
|
||||
@@ -19,6 +19,8 @@ func createTestFS(t *testing.T) filesystem.FileSystem {
|
||||
filename := "test.squashfs"
|
||||
f, err := os.Create(filepath.Join(dir, filename))
|
||||
require.NoError(t, err)
|
||||
// windows can't remove the temp dir while the backing file is open
|
||||
t.Cleanup(func() { _ = f.Close() })
|
||||
|
||||
b := file.New(f, false)
|
||||
fsys, err := squashfs.Create(b, 0, 0, 4096)
|
||||
|
||||
@@ -84,7 +84,8 @@ func ExtractGlobsFromTarToUniqueTempFile(ctx context.Context, archivePath, dir s
|
||||
|
||||
func matchesAnyGlob(name string, globs ...string) bool {
|
||||
for _, glob := range globs {
|
||||
if matches, err := doublestar.PathMatch(glob, name); err == nil && matches {
|
||||
// tar entry names are always posix, so don't match with the host separator (PathMatch)
|
||||
if matches, err := doublestar.Match(glob, name); err == nil && matches {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/anchore/syft/internal/os"
|
||||
"github.com/anchore/syft/internal/sbomsync"
|
||||
"github.com/anchore/syft/internal/task"
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
@@ -84,6 +85,7 @@ func Test_EnvironmentTask(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
tarPath := imagetest.GetFixtureImageTarPath(t, test.name)
|
||||
|
||||
// get the source
|
||||
|
||||
@@ -13,6 +13,14 @@ func Set(s redact.Store) {
|
||||
store = s
|
||||
}
|
||||
|
||||
// Reset clears the global redaction store. It must only be called once a command
|
||||
// run has fully finished (nothing left to Add or Apply), so that a later run can
|
||||
// Set its own store. Calling it while a run is in flight would let a second Set
|
||||
// split secrets across two stores and leak the ones in the dropped store.
|
||||
func Reset() {
|
||||
store = nil
|
||||
}
|
||||
|
||||
func Get() redact.Store {
|
||||
return store
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var dockerServerOS = sync.OnceValue(func() string {
|
||||
out, err := exec.Command("docker", "version", "--format", "{{.Server.Os}}").Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
})
|
||||
|
||||
// SkipWithoutLinuxContainers skips tests whose fixtures are linux container images (built or run via docker).
|
||||
// This only kicks in on windows, where the docker daemon is often running windows containers (or is absent),
|
||||
// so the linux fixture images can't be built. Everywhere else the fixture tooling is left to fail loudly.
|
||||
func SkipWithoutLinuxContainers(t testing.TB) {
|
||||
t.Helper()
|
||||
if runtime.GOOS != "windows" {
|
||||
return
|
||||
}
|
||||
if os := dockerServerOS(); os != "linux" {
|
||||
t.Skipf("linux container fixtures unavailable (docker server os=%q)", os)
|
||||
}
|
||||
}
|
||||
@@ -265,8 +265,9 @@ func TestLazyCreation(t *testing.T) {
|
||||
// root dir is not created until needed
|
||||
assert.Equal(t, "", td.root)
|
||||
|
||||
_, _, err := td.NewFile("trigger-*")
|
||||
f, _, err := td.NewFile("trigger-*")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
assert.NotEmpty(t, td.root)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
intFile "github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
@@ -92,6 +93,7 @@ func TestDigestsCataloger(t *testing.T) {
|
||||
func TestDigestsCataloger_MixFileTypes(t *testing.T) {
|
||||
testImage := "image-file-type-mix"
|
||||
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
||||
|
||||
src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
|
||||
@@ -162,6 +164,7 @@ func TestDigestsCataloger_MixFileTypes(t *testing.T) {
|
||||
func TestFileDigestCataloger_GivenCoordinates(t *testing.T) {
|
||||
testImage := "image-file-type-mix"
|
||||
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
||||
|
||||
c := NewCataloger([]crypto.Hash{crypto.SHA256})
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/anchore/syft/syft/source/stereoscopesource"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
func TestFileMetadataCataloger(t *testing.T) {
|
||||
testImage := "image-file-type-mix"
|
||||
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
||||
|
||||
c := NewCataloger()
|
||||
@@ -157,6 +159,7 @@ func TestFileMetadataCataloger(t *testing.T) {
|
||||
func TestFileMetadataCataloger_GivenCoordinates(t *testing.T) {
|
||||
testImage := "image-file-type-mix"
|
||||
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
||||
|
||||
c := NewCataloger()
|
||||
|
||||
@@ -2,6 +2,7 @@ package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/anchore/syft/syft/source/directorysource"
|
||||
@@ -19,15 +21,16 @@ import (
|
||||
func Test_allRegularFiles(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func() file.Resolver
|
||||
setup func(t *testing.T) file.Resolver
|
||||
wantRealPaths *strset.Set
|
||||
wantAccessPaths *strset.Set
|
||||
}{
|
||||
{
|
||||
name: "image",
|
||||
setup: func() file.Resolver {
|
||||
setup: func(t *testing.T) file.Resolver {
|
||||
testImage := "image-file-type-mix"
|
||||
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
||||
|
||||
s := stereoscopesource.New(img, stereoscopesource.ImageConfig{
|
||||
@@ -44,7 +47,12 @@ func Test_allRegularFiles(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "directory",
|
||||
setup: func() file.Resolver {
|
||||
setup: func(t *testing.T) file.Resolver {
|
||||
if runtime.GOOS == "windows" {
|
||||
// file symlinks from a git checkout don't index as access paths on windows yet (same gap the
|
||||
// fileresolver symlink fixtures skip for)
|
||||
t.Skip("fixture relies on symlinks")
|
||||
}
|
||||
s, err := directorysource.NewFromPath("testdata/symlinked-root/nested/link-root")
|
||||
require.NoError(t, err)
|
||||
r, err := s.FileResolver(source.SquashedScope)
|
||||
@@ -57,7 +65,7 @@ func Test_allRegularFiles(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resolver := tt.setup()
|
||||
resolver := tt.setup(t)
|
||||
locations := AllRegularFiles(context.Background(), resolver)
|
||||
realLocations := strset.New()
|
||||
virtualLocations := strset.New()
|
||||
|
||||
@@ -2,6 +2,7 @@ package github
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/format/internal/testutil"
|
||||
@@ -11,6 +12,10 @@ var updateSnapshot = flag.Bool("update-github", false, "update the *.golden file
|
||||
var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing")
|
||||
|
||||
func TestGithubDirectoryEncoder(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
// the golden snapshot embeds the posix source path (and the source ID derived from it)
|
||||
t.Skip("directory snapshot is posix path specific")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
testutil.AssertEncoderAgainstGoldenSnapshot(t,
|
||||
testutil.EncoderSnapshotTestConfig{
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
@@ -35,6 +36,7 @@ func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBO
|
||||
case true:
|
||||
img = imagetest.GetGoldenFixtureImage(t, testImage)
|
||||
default:
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img = imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"flag"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -90,6 +91,10 @@ func TestEscapeHTML(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDirectoryEncoder(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
// the golden snapshot embeds the posix source path (and the source ID derived from it)
|
||||
t.Skip("directory snapshot is posix path specific")
|
||||
}
|
||||
cfg := DefaultEncoderConfig()
|
||||
cfg.Pretty = true
|
||||
enc, err := NewFormatEncoderWithConfig(cfg)
|
||||
|
||||
@@ -2,6 +2,7 @@ package text
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/format/internal/testutil"
|
||||
@@ -11,6 +12,10 @@ var updateSnapshot = flag.Bool("update-text", false, "update the *.golden files
|
||||
var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing")
|
||||
|
||||
func TestTextDirectoryEncoder(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
// the golden snapshot embeds the posix source path (and the source ID derived from it)
|
||||
t.Skip("directory snapshot is posix path specific")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
testutil.AssertEncoderAgainstGoldenSnapshot(t,
|
||||
testutil.EncoderSnapshotTestConfig{
|
||||
|
||||
@@ -3,6 +3,7 @@ package syft
|
||||
import (
|
||||
"crypto"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/anchore/go-collections"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
@@ -10,6 +11,11 @@ import (
|
||||
"github.com/anchore/syft/syft/source/sourceproviders"
|
||||
)
|
||||
|
||||
// GetSourceConfig controls which source providers are tried, and in what order, when resolving user input to a source.
|
||||
//
|
||||
// The "containers-storage" provider (local Buildah / Podman store) is never part of automatic resolution: it is only
|
||||
// used when named in Sources or DefaultImagePullSource. It is also only functional when built with the
|
||||
// containers_image_openpgp build tag (as syft's Linux release binaries are); otherwise it always returns an error.
|
||||
type GetSourceConfig struct {
|
||||
// SourceProviderConfig may optionally be provided to be used when constructing the default set of source providers, unused if All specified
|
||||
SourceProviderConfig *sourceproviders.Config
|
||||
@@ -64,6 +70,12 @@ func (c *GetSourceConfig) WithDefaultImagePullSource(defaultImagePullSource stri
|
||||
func (c *GetSourceConfig) getProviders(userInput string) ([]source.Provider, error) {
|
||||
providers := collections.TaggedValueSet[source.Provider]{}.Join(sourceproviders.All(userInput, c.SourceProviderConfig)...)
|
||||
|
||||
// opening a containers-storage store runs full graph driver init (mounts, locks, possible store writes), which is
|
||||
// too invasive for an "is this image local?" probe on every plain image reference. Only use it when asked by name.
|
||||
if !c.requestsSource(image.ContainersStorageSource) {
|
||||
providers = providers.Remove(image.ContainersStorageSource)
|
||||
}
|
||||
|
||||
// if the "default image pull source" is set, we move this as the first pull source
|
||||
if c.DefaultImagePullSource != "" {
|
||||
base := providers.Remove(sourceproviders.PullTag)
|
||||
@@ -84,6 +96,10 @@ func (c *GetSourceConfig) getProviders(userInput string) ([]source.Provider, err
|
||||
return providers.Values(), nil
|
||||
}
|
||||
|
||||
func (c *GetSourceConfig) requestsSource(name string) bool {
|
||||
return c.DefaultImagePullSource == name || slices.Contains(c.Sources, name)
|
||||
}
|
||||
|
||||
func DefaultGetSourceConfig() *GetSourceConfig {
|
||||
return &GetSourceConfig{
|
||||
SourceProviderConfig: sourceproviders.DefaultConfig(),
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package syft
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/anchore/syft/syft/source/sourceproviders"
|
||||
)
|
||||
|
||||
@@ -17,8 +23,9 @@ func TestGetProviders_DefaultImagePullSource(t *testing.T) {
|
||||
t.Errorf("Expected no error for DefaultImagePullSource parameter, got: %v", err)
|
||||
}
|
||||
|
||||
if len(providers) != len(allSourceProviders) {
|
||||
t.Errorf("Expected %d providers, got %d", len(allSourceProviders), len(providers))
|
||||
// everything except containers-storage, which is only used when requested by name
|
||||
if len(providers) != len(allSourceProviders)-1 {
|
||||
t.Errorf("Expected %d providers, got %d", len(allSourceProviders)-1, len(providers))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,3 +43,55 @@ func TestGetProviders_Sources(t *testing.T) {
|
||||
t.Errorf("Expected 2 providers, got %d", len(providers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProviders_ContainersStorageOnlyWhenRequested(t *testing.T) {
|
||||
const storage = image.ContainersStorageSource
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *GetSourceConfig
|
||||
wantFound bool
|
||||
// if set, containers-storage must be ordered ahead of this provider
|
||||
wantBefore string
|
||||
}{
|
||||
{
|
||||
name: "excluded from automatic resolution",
|
||||
cfg: DefaultGetSourceConfig(),
|
||||
},
|
||||
{
|
||||
name: "excluded when selecting all pull sources",
|
||||
cfg: DefaultGetSourceConfig().WithSources(sourceproviders.PullTag),
|
||||
},
|
||||
{
|
||||
name: "excluded when a different default pull source is set",
|
||||
cfg: DefaultGetSourceConfig().WithDefaultImagePullSource(stereoscope.RegistryTag),
|
||||
},
|
||||
{
|
||||
name: "included when explicitly selected",
|
||||
cfg: DefaultGetSourceConfig().WithSources(storage),
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
name: "included ahead of other pull sources when it is the default pull source",
|
||||
cfg: DefaultGetSourceConfig().WithDefaultImagePullSource(storage),
|
||||
wantFound: true,
|
||||
wantBefore: image.DockerDaemonSource,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
providers, err := tt.cfg.getProviders("localhost/myimage:latest")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, providers)
|
||||
|
||||
assert.Equal(t, tt.wantFound, providerIndex(providers, storage) >= 0)
|
||||
if tt.wantBefore != "" {
|
||||
assert.Less(t, providerIndex(providers, storage), providerIndex(providers, tt.wantBefore))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func providerIndex(providers []source.Provider, name string) int {
|
||||
return slices.IndexFunc(providers, func(p source.Provider) bool { return p.Name() == name })
|
||||
}
|
||||
|
||||
@@ -210,8 +210,13 @@ func (r ChrootContext) ToChrootPath(nativePath string) string {
|
||||
prefix = r.base
|
||||
} else {
|
||||
// we need to account for the cwd relative to the running process and the given root for the directory resolver
|
||||
prefix = filepath.Clean(filepath.Join(r.cwd, r.cwdRelativeToRoot))
|
||||
prefix += string(filepath.Separator)
|
||||
prefix = r.cwdRelativeToRoot
|
||||
// on windows an absolute root stays absolute here (see ChangeDirectory), and joining it onto the cwd
|
||||
// would nest one volume inside another (D:\cwd\C:\root)
|
||||
if !filepath.IsAbs(prefix) {
|
||||
prefix = filepath.Join(r.cwd, prefix)
|
||||
}
|
||||
prefix = filepath.Clean(prefix) + string(filepath.Separator)
|
||||
}
|
||||
responsePath = strings.TrimPrefix(responsePath, prefix)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package fileresolver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// windows CI runs from D: while t.TempDir() lives on C:, so the root and cwd don't share a volume
|
||||
func TestChrootContext_ToChrootPath_rootOnAnotherVolume(t *testing.T) {
|
||||
ctx := ChrootContext{
|
||||
root: `C:\scan\root`,
|
||||
cwd: `D:\work`,
|
||||
cwdRelativeToRoot: `C:\scan\root`,
|
||||
}
|
||||
assert.Equal(t, "dir/file.txt", ctx.ToChrootPath("/c/scan/root/dir/file.txt"))
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
@@ -91,6 +92,7 @@ func TestAllLayersResolver_FilesByPath(t *testing.T) {
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
|
||||
|
||||
resolver, err := NewFromContainerImageAllLayers(img)
|
||||
@@ -201,6 +203,7 @@ func TestAllLayersResolver_FilesByGlob(t *testing.T) {
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
|
||||
|
||||
resolver, err := NewFromContainerImageAllLayers(img)
|
||||
@@ -249,6 +252,7 @@ func Test_imageAllLayersResolver_FilesByMIMEType(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.fixtureName, func(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureName)
|
||||
|
||||
resolver, err := NewFromContainerImageAllLayers(img)
|
||||
@@ -266,6 +270,7 @@ func Test_imageAllLayersResolver_FilesByMIMEType(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_imageAllLayersResolver_hasFilesystemIDInLocation(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-duplicate-path")
|
||||
|
||||
resolver, err := NewFromContainerImageAllLayers(img)
|
||||
@@ -326,6 +331,7 @@ func TestAllLayersImageResolver_FilesContents(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
|
||||
|
||||
resolver, err := NewFromContainerImageAllLayers(img)
|
||||
@@ -353,6 +359,7 @@ func TestAllLayersImageResolver_FilesContents(t *testing.T) {
|
||||
|
||||
func TestAllLayersImageResolver_FilesContents_errorOnDirRequest(t *testing.T) {
|
||||
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
|
||||
|
||||
resolver, err := NewFromContainerImageAllLayers(img)
|
||||
@@ -500,6 +507,7 @@ func Test_imageAllLayersResolver_resolvesLinks(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
|
||||
|
||||
resolver, err := NewFromContainerImageAllLayers(img)
|
||||
@@ -514,6 +522,7 @@ func Test_imageAllLayersResolver_resolvesLinks(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAllLayersResolver_AllLocations(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-files-deleted")
|
||||
|
||||
resolver, err := NewFromContainerImageAllLayers(img)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"go.uber.org/goleak"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
@@ -115,6 +116,7 @@ func Test_ContainerImageDeepSquash_FilesByPath(t *testing.T) {
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
|
||||
|
||||
resolver, err := NewFromContainerImageDeepSquash(img)
|
||||
@@ -231,6 +233,7 @@ func Test_ContainerImageDeepSquash_FilesByGlob(t *testing.T) {
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
|
||||
|
||||
resolver, err := NewFromContainerImageDeepSquash(img)
|
||||
@@ -288,6 +291,7 @@ func Test_ContainerImageDeepSquash_FilesByMIMEType(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.fixtureName, func(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureName)
|
||||
|
||||
resolver, err := NewFromContainerImageDeepSquash(img)
|
||||
@@ -305,6 +309,7 @@ func Test_ContainerImageDeepSquash_FilesByMIMEType(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_ContainerImageDeepSquash_hasFilesystemIDInLocation(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-duplicate-path")
|
||||
|
||||
resolver, err := NewFromContainerImageDeepSquash(img)
|
||||
@@ -365,6 +370,7 @@ func Test_ContainerImageDeepSquash_FilesContents(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
|
||||
|
||||
resolver, err := NewFromContainerImageDeepSquash(img)
|
||||
@@ -389,6 +395,7 @@ func Test_ContainerImageDeepSquash_FilesContents(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_ContainerImageDeepSquash_FilesContents_errorOnDirRequest(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
|
||||
|
||||
resolver, err := NewFromContainerImageDeepSquash(img)
|
||||
@@ -557,6 +564,7 @@ func Test_ContainerImageDeepSquash_resolvesLinks(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
|
||||
|
||||
resolver, err := NewFromContainerImageDeepSquash(img)
|
||||
@@ -571,6 +579,7 @@ func Test_ContainerImageDeepSquash_resolvesLinks(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_ContainerImageDeepSquash_AllLocations(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-files-deleted")
|
||||
|
||||
resolver, err := NewFromContainerImageDeepSquash(img)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
@@ -75,6 +76,7 @@ func TestImageSquashResolver_FilesByPath(t *testing.T) {
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
|
||||
|
||||
resolver, err := NewFromContainerImageSquash(img)
|
||||
@@ -184,6 +186,7 @@ func TestImageSquashResolver_FilesByGlob(t *testing.T) {
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
|
||||
|
||||
resolver, err := NewFromContainerImageSquash(img)
|
||||
@@ -241,6 +244,7 @@ func Test_imageSquashResolver_FilesByMIMEType(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.fixtureName, func(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureName)
|
||||
|
||||
resolver, err := NewFromContainerImageSquash(img)
|
||||
@@ -258,6 +262,7 @@ func Test_imageSquashResolver_FilesByMIMEType(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_imageSquashResolver_hasFilesystemIDInLocation(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-duplicate-path")
|
||||
|
||||
resolver, err := NewFromContainerImageSquash(img)
|
||||
@@ -316,6 +321,7 @@ func TestSquashImageResolver_FilesContents(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
|
||||
|
||||
resolver, err := NewFromContainerImageSquash(img)
|
||||
@@ -341,6 +347,7 @@ func TestSquashImageResolver_FilesContents(t *testing.T) {
|
||||
|
||||
func TestSquashImageResolver_FilesContents_errorOnDirRequest(t *testing.T) {
|
||||
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
|
||||
|
||||
resolver, err := NewFromContainerImageSquash(img)
|
||||
@@ -499,6 +506,7 @@ func Test_imageSquashResolver_resolvesLinks(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
|
||||
|
||||
resolver, err := NewFromContainerImageSquash(img)
|
||||
@@ -545,6 +553,7 @@ func locationSorter(a, b file.Location) int {
|
||||
}
|
||||
|
||||
func TestSquashResolver_AllLocations(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-files-deleted")
|
||||
|
||||
resolver, err := NewFromContainerImageSquash(img)
|
||||
@@ -580,6 +589,7 @@ func TestSquashResolver_AllLocations(t *testing.T) {
|
||||
// content and metadata), so that image results are in parity with directory results (which cannot distinguish a
|
||||
// hardlink from a regular file). this must hold for both the squashed and all-layers resolvers.
|
||||
func TestImageResolvers_Hardlinks(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-hardlinks")
|
||||
|
||||
resolvers := map[string]file.Resolver{}
|
||||
|
||||
@@ -351,17 +351,12 @@ func (r directoryIndexer) addFileToIndex(p string, info os.FileInfo) error {
|
||||
func (r directoryIndexer) addSymlinkToIndex(p string, info os.FileInfo) (string, error) {
|
||||
linkTarget, err := os.Readlink(p)
|
||||
if err != nil {
|
||||
isOnWindows := windows.HostRunningOnWindows()
|
||||
if isOnWindows {
|
||||
p = windows.FromPosix(p)
|
||||
}
|
||||
|
||||
linkTarget, err = filepath.EvalSymlinks(p)
|
||||
|
||||
if isOnWindows {
|
||||
p = windows.ToPosix(p)
|
||||
nativePath := p
|
||||
if windows.HostRunningOnWindows() {
|
||||
nativePath = windows.FromPosix(p)
|
||||
}
|
||||
|
||||
linkTarget, err = filepath.EvalSymlinks(nativePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to readlink for path=%q: %w", p, err)
|
||||
}
|
||||
@@ -414,16 +409,21 @@ func (r directoryIndexer) addSymlinkToIndex(p string, info os.FileInfo) (string,
|
||||
}
|
||||
}
|
||||
|
||||
ref, err := r.tree.AddSymLink(file.Path(p), file.Path(linkTarget))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
targetAbsPath := linkTarget
|
||||
if !filepath.IsAbs(targetAbsPath) {
|
||||
targetAbsPath = filepath.Clean(filepath.Join(path.Dir(p), linkTarget))
|
||||
}
|
||||
|
||||
// the tree is posix (like p), but the joins above produce native separators on windows
|
||||
if windows.HostRunningOnWindows() {
|
||||
linkTarget = windows.ToPosix(linkTarget)
|
||||
}
|
||||
|
||||
ref, err := r.tree.AddSymLink(file.Path(p), file.Path(linkTarget))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
metadata := NewMetadataFromPath(p, info)
|
||||
metadata.LinkDestination = linkTarget
|
||||
r.index.Add(*ref, metadata)
|
||||
|
||||
@@ -35,9 +35,19 @@ func nativeOSFileOpener(ref stereoscopeFile.Reference) (io.ReadCloser, error) {
|
||||
return stereoscopeFile.NewLazyReadCloser(filePath), nil
|
||||
}
|
||||
|
||||
// virtual reports whether the tree is not backed by the host filesystem (e.g. a squashfs), in which case there is
|
||||
// no chroot and request/response paths are already the posix tree paths. Mapping them through the host (as the
|
||||
// chroot does) would root them on the current volume on windows (/payload.txt -> /d/payload.txt).
|
||||
func (r FiletreeResolver) virtual() bool {
|
||||
return r.Chroot == ChrootContext{}
|
||||
}
|
||||
|
||||
// requestPath converts a user (chroot) path into the path the file tree is keyed on. The tree is always posix
|
||||
// (windows paths are volume-encoded, e.g. /c/some/path), while ToNativePath yields a native host path.
|
||||
func (r *FiletreeResolver) requestPath(userPath string) (string, error) {
|
||||
if r.virtual() {
|
||||
return userPath, nil
|
||||
}
|
||||
nativePath, err := r.Chroot.ToNativePath(userPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -50,6 +60,9 @@ func (r *FiletreeResolver) requestPath(userPath string) (string, error) {
|
||||
|
||||
// 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 {
|
||||
if r.virtual() {
|
||||
return path
|
||||
}
|
||||
return r.Chroot.ToChrootPath(path)
|
||||
}
|
||||
|
||||
@@ -111,6 +124,9 @@ func (r FiletreeResolver) FilesByPath(userPaths ...string) ([]file.Location, err
|
||||
|
||||
// requestGlob is the glob equivalent of requestPath.
|
||||
func (r FiletreeResolver) requestGlob(pattern string) (string, error) {
|
||||
if r.virtual() {
|
||||
return pattern, nil
|
||||
}
|
||||
glob, err := r.Chroot.ToNativeGlob(pattern)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -522,14 +523,15 @@ func TestSafeTensorsMergeProcessor(t *testing.T) {
|
||||
|
||||
t.Run("dir scan: config.json _name_or_path beats the parent directory fallback", func(t *testing.T) {
|
||||
// When a sibling config.json carries _name_or_path
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"),
|
||||
// (location paths are posix, so keep the temp dir slash-separated for windows)
|
||||
dir := filepath.ToSlash(t.TempDir())
|
||||
require.NoError(t, os.WriteFile(path.Join(dir, "config.json"),
|
||||
[]byte(`{"_name_or_path":"org/preferred-name"}`), 0o644))
|
||||
stPath := filepath.Join(dir, "weights.safetensors")
|
||||
stPath := path.Join(dir, "weights.safetensors")
|
||||
p := dirPkg(stPath, pkg.SafeTensorsModelInfo{
|
||||
Format: "safetensors", TensorCount: 1, MetadataHash: "abc",
|
||||
})
|
||||
resolver := file.NewMockResolverForPaths(filepath.Join(dir, "config.json"))
|
||||
resolver := file.NewMockResolverForPaths(path.Join(dir, "config.json"))
|
||||
out, _, err := safeTensorsMergeProcessor(context.Background(), resolver, []pkg.Package{p}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out, 1)
|
||||
|
||||
@@ -114,7 +114,7 @@ func fetchPkgFiles(base string, resolver file.Resolver) ([]pkg.AlpmFileRecord, f
|
||||
|
||||
func fetchBackupFiles(base string, resolver file.Resolver) ([]pkg.AlpmFileRecord, file.Location, error) {
|
||||
// We only really do this to get any backup database entries from the files database
|
||||
target := filepath.Join(base, "files")
|
||||
target := path.Join(base, "files")
|
||||
|
||||
loc, err := getLocation(target, resolver)
|
||||
if err != nil {
|
||||
@@ -395,7 +395,9 @@ func parseMtreeWithLimits(r io.Reader, maxSize int64, maxLines int) ([]pkg.AlpmF
|
||||
if ok := ignoredFiles[f.Name]; ok {
|
||||
continue
|
||||
}
|
||||
path := fmt.Sprintf("/%s", f.Name)
|
||||
// go-mtree runs filepath.Clean on entry names, which turns them into host paths on windows.
|
||||
// ponytail: a vis-encoded backslash (\134) in a name gets mangled on windows only, unvis first if that matters
|
||||
path := fmt.Sprintf("/%s", filepath.ToSlash(f.Name))
|
||||
fileFields["path"] = path
|
||||
for _, kv := range f.Keywords {
|
||||
kw := string(kv.Keyword())
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
@@ -3169,6 +3170,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())
|
||||
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureImage)
|
||||
src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
|
||||
Reference: test.fixtureImage,
|
||||
|
||||
+5
-4
@@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -123,10 +124,10 @@ func TestAllStorePaths(t *testing.T) {
|
||||
},
|
||||
dest: "dest",
|
||||
expected: []string{
|
||||
"dest/test/1.0/platform1/test1",
|
||||
"dest/test/1.0/platform1/test2",
|
||||
"dest/test/1.0/platform2/test1",
|
||||
"dest/test/1.0/platform2/test2",
|
||||
filepath.FromSlash("dest/test/1.0/platform1/test1"),
|
||||
filepath.FromSlash("dest/test/1.0/platform1/test2"),
|
||||
filepath.FromSlash("dest/test/1.0/platform2/test1"),
|
||||
filepath.FromSlash("dest/test/1.0/platform2/test2"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
@@ -167,7 +168,7 @@ func getLogicalKey(managedBinaryPath string) (*LogicalEntryKey, error) {
|
||||
OrgName: items[0],
|
||||
Version: items[1],
|
||||
Platform: items[2],
|
||||
Filename: filepath.Join(items[3:]...),
|
||||
Filename: path.Join(items[3:]...), // posix, since this is part of the logical key
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -64,8 +64,8 @@ func TestListAllBinaries(t *testing.T) {
|
||||
|
||||
assert.Equal(t,
|
||||
Entries{
|
||||
LogicalEntryKey{OrgName: "busybox", Version: "1.3.6", Platform: "linux-amd64", Filename: "busybox"}: EntryInfo{IsConfigured: true, BinaryPath: "testdata/bin/busybox/1.3.6/linux-amd64/busybox", SnippetPath: ""},
|
||||
LogicalEntryKey{OrgName: "busybox", Version: "1.3.6", Platform: "linux-arm64", Filename: "busybox"}: EntryInfo{IsConfigured: false, BinaryPath: "testdata/bin/busybox/1.3.6/linux-arm64/busybox", SnippetPath: ""},
|
||||
LogicalEntryKey{OrgName: "busybox", Version: "1.3.6", Platform: "linux-amd64", Filename: "busybox"}: EntryInfo{IsConfigured: true, BinaryPath: filepath.FromSlash("testdata/bin/busybox/1.3.6/linux-amd64/busybox"), SnippetPath: ""},
|
||||
LogicalEntryKey{OrgName: "busybox", Version: "1.3.6", Platform: "linux-arm64", Filename: "busybox"}: EntryInfo{IsConfigured: false, BinaryPath: filepath.FromSlash("testdata/bin/busybox/1.3.6/linux-arm64/busybox"), SnippetPath: ""},
|
||||
},
|
||||
entries,
|
||||
)
|
||||
@@ -105,10 +105,10 @@ func TestListAllEntries(t *testing.T) {
|
||||
|
||||
assert.Equal(t,
|
||||
Entries{
|
||||
LogicalEntryKey{OrgName: "busybox", Version: "1.3.6", Platform: "linux-amd64", Filename: "busybox"}: EntryInfo{IsConfigured: true, BinaryPath: "testdata/bin/busybox/1.3.6/linux-amd64/busybox", SnippetPath: "testdata/snippets/busybox/1.3.6/linux-amd64/busybox"},
|
||||
LogicalEntryKey{OrgName: "busybox", Version: "1.3.6", Platform: "linux-arm64", Filename: "busybox"}: EntryInfo{IsConfigured: false, BinaryPath: "testdata/bin/busybox/1.3.6/linux-arm64/busybox", SnippetPath: "testdata/snippets/busybox/1.3.6/linux-arm64/busybox"},
|
||||
LogicalEntryKey{OrgName: "busybox", Version: "1.3.6", Platform: "linux-amd64", Filename: "busybox"}: EntryInfo{IsConfigured: true, BinaryPath: filepath.FromSlash("testdata/bin/busybox/1.3.6/linux-amd64/busybox"), SnippetPath: filepath.FromSlash("testdata/snippets/busybox/1.3.6/linux-amd64/busybox")},
|
||||
LogicalEntryKey{OrgName: "busybox", Version: "1.3.6", Platform: "linux-arm64", Filename: "busybox"}: EntryInfo{IsConfigured: false, BinaryPath: filepath.FromSlash("testdata/bin/busybox/1.3.6/linux-arm64/busybox"), SnippetPath: filepath.FromSlash("testdata/snippets/busybox/1.3.6/linux-arm64/busybox")},
|
||||
// note the standalone snippet!
|
||||
LogicalEntryKey{OrgName: "postgres", Version: "9.6.10", Platform: "linux-amd64", Filename: "postgres"}: EntryInfo{IsConfigured: false, BinaryPath: "", SnippetPath: "testdata/snippets/postgres/9.6.10/linux-amd64/postgres"},
|
||||
LogicalEntryKey{OrgName: "postgres", Version: "9.6.10", Platform: "linux-amd64", Filename: "postgres"}: EntryInfo{IsConfigured: false, BinaryPath: "", SnippetPath: filepath.FromSlash("testdata/snippets/postgres/9.6.10/linux-amd64/postgres")},
|
||||
},
|
||||
entries,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -13,7 +14,7 @@ func TestSplitFilepath(t *testing.T) {
|
||||
path string
|
||||
expected []string
|
||||
}{
|
||||
{path: "a/b/c", expected: []string{"a", "b", "c"}},
|
||||
{path: filepath.Join("a", "b", "c"), expected: []string{"a", "b", "c"}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -3,7 +3,9 @@ package testutil
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -18,10 +20,11 @@ import (
|
||||
// not specify the "bin" or "snippets" parent path... this is determined logically [snippets > binary unless told
|
||||
// otherwise]). Path should also be to the directory containing the binary or snippets of interest (not the binaries
|
||||
// or snippets itself).
|
||||
func SnippetOrBinary(t *testing.T, path string, requireBinary bool) string {
|
||||
func SnippetOrBinary(t *testing.T, logicalPath string, requireBinary bool) string {
|
||||
t.Helper()
|
||||
|
||||
require.Len(t, internal.SplitFilepath(path), 3, "path must be a in the form <name>/<version>/<arch>")
|
||||
// logicalPath is a posix key, not a host path
|
||||
require.Len(t, strings.Split(logicalPath, "/"), 3, "path must be a in the form <name>/<version>/<arch>")
|
||||
|
||||
// cd to testdata directory and load the config
|
||||
|
||||
@@ -43,11 +46,11 @@ func SnippetOrBinary(t *testing.T, path string, requireBinary bool) string {
|
||||
|
||||
var fixturePath string
|
||||
for k, v := range entries.Sorted() {
|
||||
if filepath.Dir(k.Path()) == path {
|
||||
if path.Dir(k.Path()) == logicalPath {
|
||||
// prefer the snippet over the binary
|
||||
if !requireBinary {
|
||||
if v.SnippetPath != "" {
|
||||
t.Logf("using snippet for %q", path)
|
||||
t.Logf("using snippet for %q", logicalPath)
|
||||
require.NoError(t, validateSnippet(v.BinaryPath, v.SnippetPath))
|
||||
fixturePath = v.SnippetPath
|
||||
break
|
||||
@@ -56,10 +59,10 @@ func SnippetOrBinary(t *testing.T, path string, requireBinary bool) string {
|
||||
fixturePath = v.BinaryPath
|
||||
break
|
||||
}
|
||||
t.Fatalf("no binary or snippet found for %q", path)
|
||||
t.Fatalf("no binary or snippet found for %q", logicalPath)
|
||||
}
|
||||
if v.BinaryPath != "" {
|
||||
t.Logf("forcing the use of the original binary for %q", path)
|
||||
t.Logf("forcing the use of the original binary for %q", logicalPath)
|
||||
fixturePath = v.BinaryPath
|
||||
break
|
||||
}
|
||||
@@ -68,12 +71,12 @@ func SnippetOrBinary(t *testing.T, path string, requireBinary bool) string {
|
||||
t.Skip("no binary found, but is covered by a snippet. Please add this case to the 'binary/testdata/config.yaml' and recreate the snippet")
|
||||
}
|
||||
|
||||
t.Fatalf("no binary found for %q", path)
|
||||
t.Fatalf("no binary found for %q", logicalPath)
|
||||
}
|
||||
}
|
||||
|
||||
if fixturePath == "" {
|
||||
t.Fatalf("no fixture found for %q", path)
|
||||
t.Fatalf("no fixture found for %q", logicalPath)
|
||||
}
|
||||
|
||||
// this should be relative to the tests-fixtures directory and should be the directory containing the binary or
|
||||
@@ -101,6 +104,7 @@ func validateSnippet(binaryPath, snippetPath string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
expected, err := internal.Sha256SumFile(f)
|
||||
if err != nil {
|
||||
|
||||
@@ -5,7 +5,7 @@ package bitnami
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
@@ -73,7 +73,7 @@ func parseSBOM(_ context.Context, resolver file.Resolver, _ *generic.Environment
|
||||
|
||||
// Bitnami packages reported in a SPDX file are shipped under the same directory
|
||||
// as the SPDX file itself.
|
||||
metadata.Path = filepath.Dir(reader.RealPath)
|
||||
metadata.Path = path.Dir(reader.RealPath)
|
||||
if p.ID() != mainPkgID {
|
||||
metadata.Files = packageFiles(s.Relationships, p, metadata.Path)
|
||||
secondaryPkgsFiles = append(secondaryPkgsFiles, metadata.Files...)
|
||||
|
||||
@@ -3,7 +3,6 @@ package bitnami
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
@@ -73,7 +72,7 @@ func packageFiles(relationships []artifact.Relationship, p pkg.Package, baseDire
|
||||
|
||||
// mainPkgFiles returns the files owned by the main package in the SPDX file.
|
||||
func mainPkgFiles(resolver file.Resolver, spdxFilePath string, secondaryPkgsFiles []string) ([]string, error) {
|
||||
ownedPathGlob := fmt.Sprintf("%s/**", filepath.Dir(spdxFilePath))
|
||||
ownedPathGlob := fmt.Sprintf("%s/**", path.Dir(spdxFilePath))
|
||||
ownedLocations, err := resolver.FilesByGlob(ownedPathGlob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -67,7 +67,7 @@ func parseComponentsJSON(_ context.Context, _ file.Resolver, _ *generic.Environm
|
||||
Revision: revision,
|
||||
Architecture: entry.Arch,
|
||||
Distro: entry.Distro,
|
||||
Path: filepath.Join(filepath.Dir(reader.RealPath), name),
|
||||
Path: path.Join(path.Dir(reader.RealPath), name),
|
||||
}
|
||||
|
||||
p := pkg.Package{
|
||||
|
||||
@@ -75,6 +75,7 @@ catalogers:
|
||||
method: glob # AUTO-GENERATED
|
||||
criteria: # AUTO-GENERATED
|
||||
- '**/*.deb'
|
||||
- '**/*.ipk'
|
||||
metadata_types: # AUTO-GENERATED
|
||||
- pkg.DpkgArchiveEntry
|
||||
package_types: # AUTO-GENERATED
|
||||
|
||||
@@ -18,8 +18,8 @@ func NewDBCataloger() pkg.Cataloger {
|
||||
WithProcessors(dependency.Processor(dbEntryDependencySpecifier))
|
||||
}
|
||||
|
||||
// NewArchiveCataloger returns a new Debian package cataloger object capable of parsing .deb archive files
|
||||
// NewArchiveCataloger returns a new Debian package cataloger object capable of parsing .deb and .ipk (opkg) archive files
|
||||
func NewArchiveCataloger() pkg.Cataloger {
|
||||
return generic.NewCataloger("deb-archive-cataloger").
|
||||
WithParserByGlobs(parseDebArchive, "**/*.deb")
|
||||
WithParserByGlobs(parseDebArchive, "**/*.deb", "**/*.ipk")
|
||||
}
|
||||
|
||||
@@ -276,6 +276,36 @@ func TestDpkgArchiveCataloger(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "image-single-ipk",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "libz1",
|
||||
Version: "1.3.2-r0",
|
||||
FoundBy: "deb-archive-cataloger",
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewLocation("/libz1_1.3.2-r0_x86-64-v3.ipk"),
|
||||
),
|
||||
Type: pkg.DebPkg,
|
||||
Metadata: pkg.DpkgArchiveEntry{
|
||||
Package: "libz1",
|
||||
Source: "zlib",
|
||||
SourceVersion: "1.3.2",
|
||||
Version: "1.3.2-r0",
|
||||
Architecture: "x86-64-v3",
|
||||
Maintainer: "Poky Maintainers <poky@lists.yoctoproject.org>",
|
||||
InstalledSize: 0,
|
||||
Description: `Zlib Compression Library
|
||||
Zlib is a general-purpose, patent-free, lossless data compression library
|
||||
which is used by many different programs.`,
|
||||
Provides: []string{"zlib"},
|
||||
Depends: []string{"musl (>= 1.2.6+git0+9fa28ece75)"},
|
||||
},
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicenseFromLocationsWithContext(ctx, "Zlib"),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
@@ -61,32 +63,65 @@ func newDpkgPackage(ctx context.Context, d dpkgExtractedMetadata, dbLocation fil
|
||||
return p
|
||||
}
|
||||
|
||||
// a declared License field holds a single expression, a few hundred bytes at most in practice. Anything
|
||||
// much larger only feeds the expression parser (which recurses on parentheses) attacker-sized input.
|
||||
const maxDeclaredLicenseLen = 4096
|
||||
|
||||
var (
|
||||
// opkg/ipkg (Yocto via bitbake) declare licenses with "&" for AND and "|" for OR
|
||||
bitbakeLicenseOperators = strings.NewReplacer("&", " AND ", "|", " OR ")
|
||||
tightenParens = strings.NewReplacer("( ", "(", " )", ")")
|
||||
licenseListSeparators = strings.NewReplacer("(", " ", ")", " ")
|
||||
)
|
||||
|
||||
// extractDeclaredLicenses converts a License field from the status DB into a license set. Returns nil
|
||||
// for empty input so standard dpkg entries (which never declare License inline) incur no allocation.
|
||||
// Mirrors the alpine cataloger's approach: keep the value whole if it parses as a valid SPDX expression,
|
||||
// otherwise split on whitespace to handle space-separated lists.
|
||||
func extractDeclaredLicenses(ctx context.Context, raw string, dbLocation file.Location) []pkg.License {
|
||||
values := declaredLicenseValues(raw)
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return pkg.NewLicensesFromLocationWithContext(ctx, dbLocation, values...)
|
||||
}
|
||||
|
||||
// declaredLicenseValues converts an inline License field (opkg status DB or an archive control file) into
|
||||
// license values. Bitbake operators are rewritten to SPDX ones and the value is kept whole if it then parses
|
||||
// as a valid SPDX expression (mirroring the alpine cataloger), otherwise it is treated as a list of license
|
||||
// names with operators and grouping dropped.
|
||||
func declaredLicenseValues(raw string) []string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
licenseStrings := []string{raw}
|
||||
if _, err := license.ParseExpression(raw); err != nil {
|
||||
licenseStrings = strings.Fields(raw)
|
||||
if len(raw) > maxDeclaredLicenseLen {
|
||||
log.WithFields("length", len(raw)).Debug("ignoring oversized declared license field")
|
||||
return nil
|
||||
}
|
||||
return pkg.NewLicensesFromLocationWithContext(ctx, dbLocation, licenseStrings...)
|
||||
|
||||
expr := tightenParens.Replace(strings.Join(strings.Fields(bitbakeLicenseOperators.Replace(raw)), " "))
|
||||
if _, err := license.ParseExpression(expr); err == nil {
|
||||
return []string{expr}
|
||||
}
|
||||
|
||||
var values []string
|
||||
seen := strset.New()
|
||||
for _, field := range strings.Fields(licenseListSeparators.Replace(expr)) {
|
||||
if field == "AND" || field == "OR" || seen.Has(field) {
|
||||
continue
|
||||
}
|
||||
seen.Add(field)
|
||||
values = append(values, field)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func newDebArchivePackage(ctx context.Context, location file.Location, metadata pkg.DpkgArchiveEntry, licenseStrings []string) pkg.Package {
|
||||
p := pkg.Package{
|
||||
Name: metadata.Package,
|
||||
Version: metadata.Version,
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, licenseStrings...)...),
|
||||
Type: pkg.DebPkg,
|
||||
PURL: packageURL(
|
||||
pkg.DpkgDBEntry(metadata),
|
||||
// we don't know the distro information, but since this is a deb file then we can reasonably assume it is a debian-based distro
|
||||
&linux.Release{IDLike: []string{"debian"}},
|
||||
),
|
||||
Name: metadata.Package,
|
||||
Version: metadata.Version,
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, licenseStrings...)...),
|
||||
Type: pkg.DebPkg,
|
||||
PURL: packageURL(pkg.DpkgDBEntry(metadata), archiveDistro(location)),
|
||||
Metadata: metadata,
|
||||
Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
|
||||
}
|
||||
@@ -95,6 +130,17 @@ func newDebArchivePackage(ctx context.Context, location file.Location, metadata
|
||||
return p
|
||||
}
|
||||
|
||||
// archiveDistro guesses the distro an archive was built for. We don't know the distro information, but a .deb
|
||||
// can reasonably be assumed to target a debian-based distro. An .ipk targets opkg-based distros (Yocto,
|
||||
// OpenWrt) which have no purl type, so it gets no distro and therefore no PURL, the same as packages found in
|
||||
// an opkg status DB.
|
||||
func archiveDistro(location file.Location) *linux.Release {
|
||||
if strings.EqualFold(path.Ext(location.Path()), ".ipk") {
|
||||
return nil
|
||||
}
|
||||
return &linux.Release{IDLike: []string{"debian"}}
|
||||
}
|
||||
|
||||
// PackageURL returns the PURL for the specific Debian package (see https://github.com/package-url/purl-spec)
|
||||
func packageURL(m pkg.DpkgDBEntry, distro *linux.Release) string {
|
||||
if distro == nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package debian
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -145,6 +146,36 @@ func Test_extractDeclaredLicenses(t *testing.T) {
|
||||
raw: "GPL-2.0 BSD-3-Clause",
|
||||
expected: []string{"GPL-2.0", "BSD-3-Clause"},
|
||||
},
|
||||
{
|
||||
name: "bitbake operators become an SPDX expression",
|
||||
raw: "MIT & (BSD-3-Clause | Apache-2.0) & ( GPL-2.0-or-later )",
|
||||
expected: []string{"MIT AND (BSD-3-Clause OR Apache-2.0) AND (GPL-2.0-or-later)"},
|
||||
},
|
||||
{
|
||||
name: "bitbake grouping is preserved rather than split",
|
||||
raw: "(MIT & BSD-3-Clause) | Apache-2.0",
|
||||
expected: []string{"(MIT AND BSD-3-Clause) OR Apache-2.0"},
|
||||
},
|
||||
{
|
||||
name: "parenthesized alternatives are preserved",
|
||||
raw: "(MIT) | (Apache-2.0)",
|
||||
expected: []string{"(MIT) OR (Apache-2.0)"},
|
||||
},
|
||||
{
|
||||
name: "non-SPDX names drop operators and grouping",
|
||||
raw: "GPLv2+ & (LGPLv2.1 | BSD) & GPLv2+",
|
||||
expected: []string{"GPLv2+", "LGPLv2.1", "BSD"},
|
||||
},
|
||||
{
|
||||
name: "oversized field is ignored",
|
||||
raw: strings.Repeat("MIT & ", maxDeclaredLicenseLen),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "deeply nested parens are ignored",
|
||||
raw: strings.Repeat("(", maxDeclaredLicenseLen) + "MIT" + strings.Repeat(")", maxDeclaredLicenseLen),
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -2,6 +2,7 @@ package debian
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -23,18 +24,27 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
)
|
||||
|
||||
// parseDebArchive parses a Debian package archive (.deb) file and returns the packages it contains.
|
||||
// A .deb file is an ar archive containing three main files:
|
||||
// - debian-binary: Version of the .deb format (usually "2.0")
|
||||
// parseDebArchive parses a Debian package archive (.deb) or opkg package (.ipk) file and returns the packages it
|
||||
// contains. Both are ar archives containing three main files:
|
||||
// - debian-binary: Version of the archive format (usually "2.0")
|
||||
// - control.tar.gz/xz/zst: Contains package metadata (control file, md5sums, conffiles)
|
||||
// - data.tar.gz/xz/zst: Contains the actual files to be installed (not processed by this cataloger)
|
||||
//
|
||||
// This function extracts and processes the control information to create package metadata.
|
||||
// This function extracts and processes the control information to create package metadata. Licenses come
|
||||
// from copyright files in the data tar, falling back to the control file's License field (common for .ipk).
|
||||
// Only ar-framed .ipk files are supported; the older gzipped-tar framing (e.g. OpenWrt's ipkg-build) is
|
||||
// rejected as an invalid archive.
|
||||
func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
arReader := ar.NewReader(reader)
|
||||
validatedReader, err := newValidatedArReader(reader)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid ar archive: %w", err)
|
||||
}
|
||||
|
||||
arReader := ar.NewReader(validatedReader)
|
||||
|
||||
var metadata *pkg.DpkgArchiveEntry
|
||||
var licenses []string
|
||||
var ctrlLicenses []string
|
||||
var unknownErr error
|
||||
var sawControl, sawData bool
|
||||
for {
|
||||
@@ -56,7 +66,7 @@ func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen
|
||||
if err != nil {
|
||||
return nil, nil, unknown.New(reader.Location, fmt.Errorf("failed to decompress control.tar.* file: %w", err))
|
||||
}
|
||||
metadata, err = processControlTar(dcReader)
|
||||
metadata, ctrlLicenses, err = processControlTar(dcReader)
|
||||
switch {
|
||||
case err != nil && metadata == nil:
|
||||
return nil, nil, unknown.New(reader.Location, fmt.Errorf("failed to process control.tar.* file: %w", err))
|
||||
@@ -87,6 +97,10 @@ func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen
|
||||
return nil, nil, unknown.New(reader.Location, fmt.Errorf("no application found described in .dpkg archive"))
|
||||
}
|
||||
|
||||
if len(licenses) == 0 && len(ctrlLicenses) > 0 {
|
||||
licenses = ctrlLicenses
|
||||
}
|
||||
|
||||
// a partial parse still yields a usable package, so report what went wrong alongside it rather than
|
||||
// dropping either one
|
||||
return []pkg.Package{
|
||||
@@ -128,10 +142,10 @@ func processDataTar(dcReader io.ReadCloser) ([]string, error) {
|
||||
}
|
||||
|
||||
// processControlTar always returns whatever metadata it managed to parse, even alongside a non-nil
|
||||
// error. A non-nil error with non-nil metadata means the package is usable but incomplete (e.g. a
|
||||
// clipped file listing); nil metadata means nothing usable was found and the caller should treat it
|
||||
// as fatal.
|
||||
func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
|
||||
// error, along with any licenses declared in the control file's License field. A non-nil error with
|
||||
// non-nil metadata means the package is usable but incomplete (e.g. a clipped file listing); nil
|
||||
// metadata means nothing usable was found and the caller should treat it as fatal.
|
||||
func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, []string, error) {
|
||||
defer internal.CloseAndLogError(dcReader, "")
|
||||
|
||||
tarReader := tar.NewReader(dcReader)
|
||||
@@ -140,6 +154,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
|
||||
var files []pkg.DpkgFileRecord
|
||||
var confFileRecords []pkg.DpkgFileRecord
|
||||
var listingErr error
|
||||
var licenses []string
|
||||
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
@@ -147,7 +162,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return metadata, fmt.Errorf("failed to read control tar: %w", err)
|
||||
return metadata, nil, fmt.Errorf("failed to read control tar: %w", err)
|
||||
}
|
||||
|
||||
switch filepath.Base(header.Name) {
|
||||
@@ -155,12 +170,13 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
|
||||
// parseDpkgStatus already streams via bufio.Reader
|
||||
entries, err := parseDpkgStatus(tarReader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse control file: %w", err)
|
||||
return nil, nil, fmt.Errorf("failed to parse control file: %w", err)
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
return nil, fmt.Errorf("no package entries found in control file")
|
||||
return nil, nil, fmt.Errorf("no package entries found in control file")
|
||||
}
|
||||
entry := pkg.DpkgArchiveEntry(entries[0].toDpkgEntry())
|
||||
licenses = declaredLicenseValues(entries[0].License)
|
||||
metadata = &entry
|
||||
case "md5sums":
|
||||
// parseDpkgMD5Info streams via bufio.Scanner and reports its own clipping/scan errors
|
||||
@@ -176,7 +192,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
|
||||
}
|
||||
|
||||
if metadata == nil {
|
||||
return nil, fmt.Errorf("control file not found in archive")
|
||||
return nil, nil, fmt.Errorf("control file not found in archive")
|
||||
}
|
||||
|
||||
if len(confFileRecords) > 0 && len(files) > 0 {
|
||||
@@ -195,7 +211,22 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
|
||||
|
||||
// a clipped or unreadable file listing leaves the package usable, so hand both back and let the
|
||||
// caller decide
|
||||
return metadata, listingErr
|
||||
return metadata, licenses, listingErr
|
||||
}
|
||||
|
||||
// newValidatedArReader checks for the ar global header before handing the stream to the ar reader, which
|
||||
// otherwise skips those bytes unchecked and reads garbage headers from non-ar input.
|
||||
func newValidatedArReader(reader io.ReadCloser) (io.ReadCloser, error) {
|
||||
prefix := make([]byte, len(ar.GLOBAL_HEADER))
|
||||
if _, err := io.ReadFull(reader, prefix); err != nil {
|
||||
return nil, fmt.Errorf("failed to read ar header: %w", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(prefix, []byte(ar.GLOBAL_HEADER)) {
|
||||
return nil, fmt.Errorf("expected ar header %q, got %q", string(ar.GLOBAL_HEADER), string(prefix))
|
||||
}
|
||||
|
||||
return io.NopCloser(io.MultiReader(bytes.NewReader(prefix), reader)), nil
|
||||
}
|
||||
|
||||
// a .deb's member sizes are bounded by the archive, but what they decompress to is not: xz and zstd
|
||||
|
||||
@@ -26,12 +26,13 @@ import (
|
||||
func TestProcessControlTar(t *testing.T) {
|
||||
tarBytes := createTestTarWithControlFiles(t)
|
||||
|
||||
metadata, err := processControlTar(io.NopCloser(bytes.NewReader(tarBytes)))
|
||||
metadata, licenses, err := processControlTar(io.NopCloser(bytes.NewReader(tarBytes)))
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, metadata)
|
||||
|
||||
assert.Equal(t, "test-package", metadata.Package)
|
||||
assert.Equal(t, []string{"MIT"}, licenses)
|
||||
assert.Equal(t, "1.0.0", metadata.Version)
|
||||
|
||||
// md5sums should have been parsed into file records
|
||||
@@ -43,6 +44,14 @@ func TestProcessControlTar(t *testing.T) {
|
||||
assert.True(t, metadata.Files[0].IsConfigFile, "file listed in conffiles should be marked as config")
|
||||
}
|
||||
|
||||
func TestParseDebArchive_RejectsInvalidArchiveHeader(t *testing.T) {
|
||||
reader := file.NewLocationReadCloser(file.NewLocation("test.deb"), io.NopCloser(bytes.NewReader([]byte("not-an-ar-archive"))))
|
||||
|
||||
_, _, err := parseDebArchive(context.Background(), nil, nil, reader)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "ar archive")
|
||||
}
|
||||
|
||||
func TestProcessControlTar_ConfigFileMarking(t *testing.T) {
|
||||
// Create a tar where conffiles lists paths that overlap with md5sums entries
|
||||
var buf bytes.Buffer
|
||||
@@ -61,9 +70,10 @@ func TestProcessControlTar_ConfigFileMarking(t *testing.T) {
|
||||
|
||||
require.NoError(t, tw.Close())
|
||||
|
||||
metadata, err := processControlTar(io.NopCloser(bytes.NewReader(buf.Bytes())))
|
||||
metadata, licenses, err := processControlTar(io.NopCloser(bytes.NewReader(buf.Bytes())))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, metadata.Files, 3)
|
||||
assert.Empty(t, licenses)
|
||||
|
||||
assert.True(t, metadata.Files[0].IsConfigFile, "first file should be marked as config file")
|
||||
assert.True(t, metadata.Files[1].IsConfigFile, "second file should be marked as config file")
|
||||
@@ -75,7 +85,7 @@ func createTestTarWithControlFiles(t *testing.T) []byte {
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
|
||||
controlContent := "Package: test-package\nVersion: 1.0.0\nArchitecture: all\nMaintainer: Test <test@example.com>\nDescription: Test package\n"
|
||||
controlContent := "Package: test-package\nVersion: 1.0.0\nArchitecture: all\nMaintainer: Test <test@example.com>\nDescription: Test package\nLicense: MIT\n"
|
||||
writeTarEntry(t, tw, "control", controlContent)
|
||||
|
||||
md5Content := "d41d8cd98f00b204e9800998ecf8427e usr/bin/test-command\n"
|
||||
@@ -165,7 +175,7 @@ func Test_processControlTar_rejectsABombedControlMember(t *testing.T) {
|
||||
writeTarEntry(t, tw, "md5sums", strings.Repeat("d41d8cd98f00b204e9800998ecf8427e usr/bin/x\n", 20000))
|
||||
require.NoError(t, tw.Close())
|
||||
|
||||
metadata, err := processControlTar(newBoundedReadCloser(io.NopCloser(bytes.NewReader(tarBuf.Bytes())), 4096))
|
||||
metadata, _, err := processControlTar(newBoundedReadCloser(io.NopCloser(bytes.NewReader(tarBuf.Bytes())), 4096))
|
||||
|
||||
require.ErrorIs(t, err, errDecompressedTooLarge)
|
||||
require.NotNil(t, metadata, "a control tar read failure after \"control\" was parsed must not drop the package")
|
||||
@@ -181,7 +191,7 @@ func Test_processControlTar_clippedListingIsUsableButReported(t *testing.T) {
|
||||
writeTarEntry(t, tw, "md5sums", strings.Repeat("d41d8cd98f00b204e9800998ecf8427e usr/bin/x\n", maxDpkgFileRecords+10))
|
||||
require.NoError(t, tw.Close())
|
||||
|
||||
metadata, err := processControlTar(io.NopCloser(bytes.NewReader(tarBuf.Bytes())))
|
||||
metadata, _, err := processControlTar(io.NopCloser(bytes.NewReader(tarBuf.Bytes())))
|
||||
|
||||
require.Error(t, err, "clipping must be reported")
|
||||
require.ErrorIs(t, err, errClippedFileListing)
|
||||
@@ -418,3 +428,18 @@ func Test_decompressionStream_failsEarlyInsteadOfBuffering(t *testing.T) {
|
||||
assert.Less(t, bounded, uint64(8*intFile.MB),
|
||||
"a bounded stream must stop at the cap, not buffer the whole member and check afterwards")
|
||||
}
|
||||
|
||||
func TestProcessControlTar_DeclaredLicense(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
|
||||
controlContent := "Package: test-package\nVersion: 1.0.0\nArchitecture: all\nLicense: MIT & (BSD-3-Clause | Apache-2.0) & ( GPL-2.0-or-later )\n"
|
||||
writeTarEntry(t, tw, "control", controlContent)
|
||||
|
||||
require.NoError(t, tw.Close())
|
||||
|
||||
metadata, licenses, err := processControlTar(io.NopCloser(bytes.NewReader(buf.Bytes())))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, metadata)
|
||||
assert.Equal(t, []string{"MIT AND (BSD-3-Clause OR Apache-2.0) AND (GPL-2.0-or-later)"}, licenses)
|
||||
}
|
||||
|
||||
@@ -183,9 +183,11 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (*dpkgExtractedMetadata, error)
|
||||
}
|
||||
|
||||
sourceName, sourceVersion := extractSourceVersion(raw.Source)
|
||||
if sourceName != "" {
|
||||
raw.Source = sourceName
|
||||
}
|
||||
if sourceVersion != "" {
|
||||
raw.SourceVersion = sourceVersion
|
||||
raw.Source = sourceName
|
||||
}
|
||||
|
||||
if raw.Package == "" {
|
||||
@@ -263,8 +265,14 @@ func extractAllFields(reader *bufio.Reader) (map[string]any, error) {
|
||||
}
|
||||
|
||||
// If the source entry string is of the form "<name> (<version>)" then parse and return the components, if
|
||||
// of the "<name>" form, then return name and nil
|
||||
// of the "<name>" form, then return name and nil. Yocto (opkg/ipk) instead writes the bitbake recipe filename
|
||||
// ("<name>_<version>.bb"), which is split the same way.
|
||||
func extractSourceVersion(source string) (string, string) {
|
||||
if recipe, ok := strings.CutSuffix(source, ".bb"); ok && !strings.ContainsAny(recipe, " (") {
|
||||
// bitbake reserves "_" as the recipe name / version separator
|
||||
name, version, _ := strings.Cut(recipe, "_")
|
||||
return name, version
|
||||
}
|
||||
// special handling for the Source field since it has formatted data
|
||||
match := internal.MatchNamedCaptureGroups(sourceRegexp, source)
|
||||
return match["name"], match["version"]
|
||||
|
||||
@@ -366,6 +366,16 @@ func TestSourceVersionExtract(t *testing.T) {
|
||||
input: "",
|
||||
expected: []string{"", ""},
|
||||
},
|
||||
{
|
||||
name: "bitbake recipe with version",
|
||||
input: "zlib_1.3.2.bb",
|
||||
expected: []string{"zlib", "1.3.2"},
|
||||
},
|
||||
{
|
||||
name: "bitbake recipe without version",
|
||||
input: "packagegroup-core-boot.bb",
|
||||
expected: []string{"packagegroup-core-boot", ""},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# assembles a Yocto-style .ipk the same way bitbake's package_ipk does (opkg-build: a GNU ar archive of
|
||||
# debian-binary, control.tar.gz and data.tar.zst) without paying for a full Yocto build. control/ is copied
|
||||
# verbatim from libz1_1.3.2-r0_x86-64-v3.ipk built by poky-tiny (wrynose); the library itself is a stub
|
||||
# since only the archive shape and control metadata matter here.
|
||||
FROM alpine:3.18.4@sha256:48d9183eb12a05c99bcc0bf44a003607b8e941e1d4f41f9ad12bdcc4b5672f86 AS build
|
||||
|
||||
RUN apk add --no-cache binutils zstd
|
||||
|
||||
COPY control /pkg/control
|
||||
|
||||
RUN mkdir -p /pkg/data/usr/lib \
|
||||
&& echo stub > /pkg/data/usr/lib/libz.so.1.3.2 \
|
||||
&& ln -s libz.so.1.3.2 /pkg/data/usr/lib/libz.so.1 \
|
||||
&& cd /pkg \
|
||||
&& echo 2.0 > debian-binary \
|
||||
&& tar -C control -czf control.tar.gz . \
|
||||
&& tar -C data -cf - . | zstd -q -o data.tar.zst \
|
||||
&& ar rcD /libz1_1.3.2-r0_x86-64-v3.ipk debian-binary control.tar.gz data.tar.zst
|
||||
|
||||
FROM scratch
|
||||
COPY --from=build /libz1_1.3.2-r0_x86-64-v3.ipk /
|
||||
@@ -0,0 +1,15 @@
|
||||
Package: libz1
|
||||
Version: 1.3.2-r0
|
||||
Description: Zlib Compression Library
|
||||
Zlib is a general-purpose, patent-free, lossless data compression library
|
||||
which is used by many different programs.
|
||||
Section: libs
|
||||
Priority: optional
|
||||
Maintainer: Poky Maintainers <poky@lists.yoctoproject.org>
|
||||
License: Zlib
|
||||
Architecture: x86-64-v3
|
||||
OE: zlib
|
||||
Homepage: http://zlib.net/
|
||||
Depends: musl (>= 1.2.6+git0+9fa28ece75)
|
||||
Provides: zlib
|
||||
Source: zlib_1.3.2.bb
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if [ x"$D" = "x" ]; then
|
||||
if [ -x /sbin/ldconfig ]; then /sbin/ldconfig ; fi
|
||||
fi
|
||||
@@ -4,8 +4,9 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
@@ -18,6 +19,8 @@ import (
|
||||
|
||||
var _ generic.Parser = parseDotnetPackagesLock
|
||||
|
||||
var packagesLockTypePrecedence = []string{"Project", "Direct", "CentralTransitive", "Transitive"}
|
||||
|
||||
type dotnetPackagesLock struct {
|
||||
Version int `json:"version"`
|
||||
Dependencies map[string]map[string]dotnetPackagesLockDep `json:"dependencies"`
|
||||
@@ -31,11 +34,38 @@ type dotnetPackagesLockDep struct {
|
||||
Dependencies map[string]string `json:"dependencies,omitempty"`
|
||||
}
|
||||
|
||||
func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { //nolint:funlen
|
||||
var pkgs []pkg.Package
|
||||
var pkgMap = make(map[string]pkg.Package)
|
||||
var relationships []artifact.Relationship
|
||||
type packagesLockEntry struct {
|
||||
name string
|
||||
dep dotnetPackagesLockDep
|
||||
}
|
||||
|
||||
func (e packagesLockEntry) nameVersion() string {
|
||||
return createNameAndVersion(e.name, e.dep.Resolved)
|
||||
}
|
||||
|
||||
// key identifies the package an entry describes. NuGet package IDs are case-insensitive, so two spellings of the
|
||||
// same ID and version are one package.
|
||||
func (e packagesLockEntry) key() string {
|
||||
return createNameAndVersion(strings.ToLower(e.name), e.dep.Resolved)
|
||||
}
|
||||
|
||||
type packagesLockFramework struct {
|
||||
name string
|
||||
entries []packagesLockEntry
|
||||
// byName is keyed by the lowercased package name, since NuGet package IDs are case-insensitive.
|
||||
byName map[string]packagesLockEntry
|
||||
}
|
||||
|
||||
func (f packagesLockFramework) find(name string) (packagesLockEntry, bool) {
|
||||
e, ok := f.byName[strings.ToLower(name)]
|
||||
return e, ok
|
||||
}
|
||||
|
||||
type packagesLockEdge struct {
|
||||
child, parent artifact.ID
|
||||
}
|
||||
|
||||
func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
dec := json.NewDecoder(reader)
|
||||
|
||||
// unmarshal file
|
||||
@@ -44,70 +74,21 @@ func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Envi
|
||||
return nil, nil, fmt.Errorf("failed to parse packages.lock.json file: %w", err)
|
||||
}
|
||||
|
||||
// collect all deps here
|
||||
allDependencies := make(map[string]dotnetPackagesLockDep)
|
||||
|
||||
var names []string
|
||||
for _, dependencies := range lockFile.Dependencies {
|
||||
for name, dep := range dependencies {
|
||||
depNameVersion := createNameAndVersion(name, dep.Resolved)
|
||||
|
||||
if slices.Contains(names, depNameVersion) {
|
||||
continue
|
||||
}
|
||||
|
||||
names = append(names, depNameVersion)
|
||||
allDependencies[depNameVersion] = dep
|
||||
}
|
||||
}
|
||||
|
||||
// sort the names so that the order of the packages is deterministic
|
||||
sort.Strings(names)
|
||||
frameworks := newPackagesLockFrameworks(lockFile)
|
||||
|
||||
// create artifact for each pkg
|
||||
for _, nameVersion := range names {
|
||||
name, _ := extractNameAndVersion(nameVersion)
|
||||
var pkgs []pkg.Package
|
||||
pkgMap := make(map[string]pkg.Package)
|
||||
|
||||
dep := allDependencies[nameVersion]
|
||||
dotnetPkg := newDotnetPackagesLockPackage(name, dep, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
|
||||
for _, entry := range mergePackagesLockEntries(frameworks) {
|
||||
dotnetPkg := newDotnetPackagesLockPackage(entry.name, entry.dep, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
|
||||
if dotnetPkg != nil {
|
||||
pkgs = append(pkgs, *dotnetPkg)
|
||||
pkgMap[nameVersion] = *dotnetPkg
|
||||
pkgMap[entry.key()] = *dotnetPkg
|
||||
}
|
||||
}
|
||||
|
||||
// fill up relationships
|
||||
for depNameVersion, dep := range allDependencies {
|
||||
parentPkg, ok := pkgMap[depNameVersion]
|
||||
if !ok {
|
||||
log.Debugf("package \"%s\" not found in map of all packages", depNameVersion)
|
||||
continue
|
||||
}
|
||||
|
||||
for childDepName, childDepVersion := range dep.Dependencies {
|
||||
childDepNameVersion := createNameAndVersion(childDepName, childDepVersion)
|
||||
|
||||
// try and find pkg for dependency with exact name and version
|
||||
childPkg, ok := pkgMap[childDepNameVersion]
|
||||
if !ok {
|
||||
// no exact match found, lets match on name only, lockfile will contain other version of pkg
|
||||
cpkg, ok := findPkgByName(childDepName, pkgMap)
|
||||
if !ok {
|
||||
log.Debugf("dependency \"%s\" of package \"%s\" not found in map of all packages", childDepNameVersion, depNameVersion)
|
||||
continue
|
||||
}
|
||||
|
||||
childPkg = *cpkg
|
||||
}
|
||||
|
||||
rel := artifact.Relationship{
|
||||
From: childPkg,
|
||||
To: parentPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
}
|
||||
relationships = append(relationships, rel)
|
||||
}
|
||||
}
|
||||
relationships := packagesLockRelationships(frameworks, pkgMap)
|
||||
|
||||
// sort the relationships for deterministic output
|
||||
relationship.Sort(relationships)
|
||||
@@ -115,6 +96,147 @@ func parseDotnetPackagesLock(_ context.Context, _ file.Resolver, _ *generic.Envi
|
||||
return pkgs, relationships, nil
|
||||
}
|
||||
|
||||
func newPackagesLockFrameworks(lockFile dotnetPackagesLock) []packagesLockFramework {
|
||||
var frameworks []packagesLockFramework
|
||||
|
||||
for _, frameworkName := range slices.Sorted(maps.Keys(lockFile.Dependencies)) {
|
||||
deps := lockFile.Dependencies[frameworkName]
|
||||
framework := packagesLockFramework{
|
||||
name: frameworkName,
|
||||
byName: make(map[string]packagesLockEntry, len(deps)),
|
||||
}
|
||||
|
||||
for _, name := range slices.Sorted(maps.Keys(deps)) {
|
||||
entry := packagesLockEntry{name: name, dep: deps[name]}
|
||||
framework.entries = append(framework.entries, entry)
|
||||
framework.byName[strings.ToLower(name)] = entry
|
||||
}
|
||||
|
||||
frameworks = append(frameworks, framework)
|
||||
}
|
||||
|
||||
return frameworks
|
||||
}
|
||||
|
||||
func mergePackagesLockEntries(frameworks []packagesLockFramework) []packagesLockEntry {
|
||||
merged := make(map[string]packagesLockEntry)
|
||||
|
||||
for _, framework := range frameworks {
|
||||
for _, entry := range framework.entries {
|
||||
key := entry.key()
|
||||
if existing, ok := merged[key]; ok && !isMoreDirect(entry.dep.Type, existing.dep.Type) {
|
||||
continue
|
||||
}
|
||||
merged[key] = entry
|
||||
}
|
||||
}
|
||||
|
||||
var entries []packagesLockEntry
|
||||
for _, key := range slices.Sorted(maps.Keys(merged)) {
|
||||
entries = append(entries, merged[key])
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
func isMoreDirect(a, b string) bool {
|
||||
rankA, rankB := packagesLockTypeRank(a), packagesLockTypeRank(b)
|
||||
if rankA != rankB {
|
||||
return rankA < rankB
|
||||
}
|
||||
return a < b
|
||||
}
|
||||
|
||||
func packagesLockTypeRank(t string) int {
|
||||
if i := slices.Index(packagesLockTypePrecedence, t); i >= 0 {
|
||||
return i
|
||||
}
|
||||
return len(packagesLockTypePrecedence)
|
||||
}
|
||||
|
||||
func packagesLockRelationships(frameworks []packagesLockFramework, pkgMap map[string]pkg.Package) []artifact.Relationship {
|
||||
var relationships []artifact.Relationship
|
||||
|
||||
frameworksByName := make(map[string]packagesLockFramework, len(frameworks))
|
||||
for _, framework := range frameworks {
|
||||
frameworksByName[framework.name] = framework
|
||||
}
|
||||
|
||||
seen := make(map[packagesLockEdge]struct{})
|
||||
for _, framework := range frameworks {
|
||||
base, hasBase := frameworksByName[baseFrameworkName(framework.name)]
|
||||
if !hasBase || base.name == framework.name {
|
||||
base = packagesLockFramework{}
|
||||
}
|
||||
|
||||
for _, entry := range framework.entries {
|
||||
parentPkg, ok := pkgMap[entry.key()]
|
||||
if !ok {
|
||||
log.Debugf("package %q not found in map of all packages", entry.nameVersion())
|
||||
continue
|
||||
}
|
||||
|
||||
relationships = append(relationships, packagesLockEntryRelationships(entry, parentPkg, framework, base, pkgMap, seen)...)
|
||||
}
|
||||
}
|
||||
|
||||
return relationships
|
||||
}
|
||||
|
||||
func packagesLockEntryRelationships(entry packagesLockEntry, parentPkg pkg.Package, framework, base packagesLockFramework, pkgMap map[string]pkg.Package, seen map[packagesLockEdge]struct{}) []artifact.Relationship {
|
||||
var relationships []artifact.Relationship
|
||||
|
||||
for _, childName := range slices.Sorted(maps.Keys(entry.dep.Dependencies)) {
|
||||
child, ok := findPackagesLockDependency(childName, framework, base)
|
||||
if !ok {
|
||||
log.Debugf("dependency %q of package %q not found under target framework %q", childName, entry.nameVersion(), framework.name)
|
||||
continue
|
||||
}
|
||||
|
||||
childPkg, ok := pkgMap[child.key()]
|
||||
if !ok {
|
||||
log.Debugf("package %q not found in map of all packages", child.nameVersion())
|
||||
continue
|
||||
}
|
||||
|
||||
// a package listing itself (possibly under another casing) is not a dependency
|
||||
if childPkg.ID() == parentPkg.ID() {
|
||||
continue
|
||||
}
|
||||
|
||||
key := packagesLockEdge{child: childPkg.ID(), parent: parentPkg.ID()}
|
||||
if _, exists := seen[key]; exists {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
|
||||
relationships = append(relationships, artifact.Relationship{
|
||||
From: childPkg,
|
||||
To: parentPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
})
|
||||
}
|
||||
|
||||
return relationships
|
||||
}
|
||||
|
||||
// findPackagesLockDependency finds the entry a dependency edge points at. The version on an edge is only the lower
|
||||
// bound of a version range, so the edge resolves to whatever version its target framework resolved. Runtime-specific
|
||||
// sections such as "net8.0/win-x64" may reference packages listed only in their base framework ("net8.0"). An edge
|
||||
// that resolves nowhere is dropped rather than guessed from another target framework.
|
||||
func findPackagesLockDependency(name string, framework, base packagesLockFramework) (packagesLockEntry, bool) {
|
||||
if entry, ok := framework.find(name); ok {
|
||||
return entry, true
|
||||
}
|
||||
return base.find(name)
|
||||
}
|
||||
|
||||
// baseFrameworkName returns the target framework of a runtime-specific section, such as "net8.0" for "net8.0/win-x64".
|
||||
func baseFrameworkName(frameworkName string) string {
|
||||
name, _, _ := strings.Cut(frameworkName, "/")
|
||||
return name
|
||||
}
|
||||
|
||||
func newDotnetPackagesLockPackage(name string, dep dotnetPackagesLockDep, locations ...file.Location) *pkg.Package {
|
||||
metadata := pkg.DotnetPackagesLockEntry{
|
||||
Name: name,
|
||||
@@ -150,14 +272,3 @@ func packagesLockPackageURL(name, version string) string {
|
||||
"",
|
||||
).ToString()
|
||||
}
|
||||
|
||||
func findPkgByName(pkgName string, pkgMap map[string]pkg.Package) (*pkg.Package, bool) {
|
||||
for pkgNameVersion, pkg := range pkgMap {
|
||||
name, _ := extractNameAndVersion(pkgNameVersion)
|
||||
if name == pkgName {
|
||||
return &pkg, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package dotnet
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
@@ -197,3 +200,304 @@ func TestParseDotnetPackagesLock(t *testing.T) {
|
||||
|
||||
pkgtest.TestFileParser(t, fixture, parseDotnetPackagesLock, expectedPkgs, expectedRelationships)
|
||||
}
|
||||
|
||||
func TestParseDotnetPackagesLock_multipleTargetFrameworks(t *testing.T) {
|
||||
fixture := "testdata/packages.lock-multi-framework.json"
|
||||
fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture))
|
||||
|
||||
myLibPkg := pkg.Package{
|
||||
Name: "MyLib",
|
||||
Version: "1.0.0",
|
||||
PURL: "pkg:nuget/MyLib@1.0.0",
|
||||
Locations: fixtureLocationSet,
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
Metadata: pkg.DotnetPackagesLockEntry{
|
||||
Name: "MyLib",
|
||||
Version: "1.0.0",
|
||||
ContentHash: "mylibhash==",
|
||||
Type: "Direct",
|
||||
},
|
||||
}
|
||||
|
||||
log4net2Pkg := pkg.Package{
|
||||
Name: "log4net",
|
||||
Version: "2.0.5",
|
||||
PURL: "pkg:nuget/log4net@2.0.5",
|
||||
Locations: fixtureLocationSet,
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
Metadata: pkg.DotnetPackagesLockEntry{
|
||||
Name: "log4net",
|
||||
Version: "2.0.5",
|
||||
ContentHash: "log4net205hash==",
|
||||
Type: "Transitive",
|
||||
},
|
||||
}
|
||||
|
||||
log4net1Pkg := pkg.Package{
|
||||
Name: "log4net",
|
||||
Version: "1.2.15",
|
||||
PURL: "pkg:nuget/log4net@1.2.15",
|
||||
Locations: fixtureLocationSet,
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
Metadata: pkg.DotnetPackagesLockEntry{
|
||||
Name: "log4net",
|
||||
Version: "1.2.15",
|
||||
ContentHash: "log4net1215hash==",
|
||||
Type: "Transitive",
|
||||
},
|
||||
}
|
||||
|
||||
// resolved to the same version under both frameworks (spelled "newtonsoft.json" under one), but declared "Direct" by
|
||||
// only one of them
|
||||
newtonsoftPkg := pkg.Package{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
|
||||
Locations: fixtureLocationSet,
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
Metadata: pkg.DotnetPackagesLockEntry{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
ContentHash: "newtonsoft1303hash==",
|
||||
Type: "Direct",
|
||||
},
|
||||
}
|
||||
|
||||
// only listed under the runtime-specific "net8.0/win-x64" section, and depends on a package from "net8.0"
|
||||
myLibNativePkg := pkg.Package{
|
||||
Name: "runtime.win-x64.MyLib.Native",
|
||||
Version: "1.0.0",
|
||||
PURL: "pkg:nuget/runtime.win-x64.MyLib.Native@1.0.0",
|
||||
Locations: fixtureLocationSet,
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
Metadata: pkg.DotnetPackagesLockEntry{
|
||||
Name: "runtime.win-x64.MyLib.Native",
|
||||
Version: "1.0.0",
|
||||
ContentHash: "mylibnativehash==",
|
||||
Type: "Transitive",
|
||||
},
|
||||
}
|
||||
|
||||
// only listed under "net8.0", so the "netstandard2.0" edge to it must be dropped rather than guessed
|
||||
serilogPkg := pkg.Package{
|
||||
Name: "Serilog",
|
||||
Version: "3.1.1",
|
||||
PURL: "pkg:nuget/Serilog@3.1.1",
|
||||
Locations: fixtureLocationSet,
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
Metadata: pkg.DotnetPackagesLockEntry{
|
||||
Name: "Serilog",
|
||||
Version: "3.1.1",
|
||||
ContentHash: "serilog311hash==",
|
||||
Type: "Transitive",
|
||||
},
|
||||
}
|
||||
|
||||
expectedPkgs := []pkg.Package{
|
||||
myLibPkg,
|
||||
newtonsoftPkg,
|
||||
log4net1Pkg,
|
||||
log4net2Pkg,
|
||||
myLibNativePkg,
|
||||
serilogPkg,
|
||||
}
|
||||
|
||||
// the same package is resolved to a different version per target framework, so both edges must be captured, while
|
||||
// the Newtonsoft.Json edge declared by both frameworks must appear only once
|
||||
expectedRelationships := []artifact.Relationship{
|
||||
{
|
||||
From: log4net1Pkg,
|
||||
To: myLibPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
{
|
||||
From: log4net2Pkg,
|
||||
To: myLibPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
{
|
||||
From: log4net2Pkg,
|
||||
To: myLibNativePkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
{
|
||||
From: newtonsoftPkg,
|
||||
To: myLibPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
}
|
||||
|
||||
pkgtest.TestFileParser(t, fixture, parseDotnetPackagesLock, expectedPkgs, expectedRelationships)
|
||||
}
|
||||
|
||||
func TestParseDotnetPackagesLock_projectReference(t *testing.T) {
|
||||
fixture := "testdata/packages.lock-project-reference.json"
|
||||
fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture))
|
||||
|
||||
newtonsoftPkg := pkg.Package{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
|
||||
Locations: fixtureLocationSet,
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
Metadata: pkg.DotnetPackagesLockEntry{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
ContentHash: "newtonsoft1303hash==",
|
||||
Type: "Transitive",
|
||||
},
|
||||
}
|
||||
|
||||
// a project reference has no resolved version or content hash
|
||||
myLibPkg := pkg.Package{
|
||||
Name: "mylib",
|
||||
PURL: "pkg:nuget/mylib",
|
||||
Locations: fixtureLocationSet,
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
Metadata: pkg.DotnetPackagesLockEntry{
|
||||
Name: "mylib",
|
||||
Type: "Project",
|
||||
},
|
||||
}
|
||||
|
||||
expectedPkgs := []pkg.Package{
|
||||
newtonsoftPkg,
|
||||
myLibPkg,
|
||||
}
|
||||
|
||||
// project references declare their dependencies as version ranges, which resolve to the version the target
|
||||
// framework resolved
|
||||
expectedRelationships := []artifact.Relationship{
|
||||
{
|
||||
From: newtonsoftPkg,
|
||||
To: myLibPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
}
|
||||
|
||||
pkgtest.TestFileParser(t, fixture, parseDotnetPackagesLock, expectedPkgs, expectedRelationships)
|
||||
}
|
||||
|
||||
func Test_findPackagesLockDependency(t *testing.T) {
|
||||
frameworks := newPackagesLockFrameworks(dotnetPackagesLock{
|
||||
Dependencies: map[string]map[string]dotnetPackagesLockDep{
|
||||
"net8.0": {
|
||||
"log4net": {Resolved: "2.0.5"},
|
||||
},
|
||||
"net8.0/win-x64": {
|
||||
"runtime.win-x64.Native": {Resolved: "1.0.0"},
|
||||
},
|
||||
"netstandard2.0": {
|
||||
"log4net": {Resolved: "1.2.15"},
|
||||
"Newtonsoft.Json": {Resolved: "13.0.3"},
|
||||
},
|
||||
},
|
||||
})
|
||||
net8, rid, netstandard := frameworks[0], frameworks[1], frameworks[2]
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
depName string
|
||||
framework packagesLockFramework
|
||||
base packagesLockFramework
|
||||
wantVersion string
|
||||
wantFound bool
|
||||
}{
|
||||
{
|
||||
name: "resolves to the version pinned by its own target framework",
|
||||
depName: "log4net",
|
||||
framework: netstandard,
|
||||
wantVersion: "1.2.15",
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
name: "package names are matched case-insensitively",
|
||||
depName: "LOG4NET",
|
||||
framework: net8,
|
||||
wantVersion: "2.0.5",
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
name: "runtime-specific sections fall back to their base target framework",
|
||||
depName: "log4net",
|
||||
framework: rid,
|
||||
base: net8,
|
||||
wantVersion: "2.0.5",
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
name: "a package absent from the target framework is not guessed from another one",
|
||||
depName: "Newtonsoft.Json",
|
||||
framework: net8,
|
||||
wantFound: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, found := findPackagesLockDependency(tt.depName, tt.framework, tt.base)
|
||||
require.Equal(t, tt.wantFound, found)
|
||||
if !tt.wantFound {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.wantVersion, got.dep.Resolved)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mergePackagesLockEntries_typePrecedence(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
types map[string]string // target framework -> type
|
||||
wantType string
|
||||
}{
|
||||
{
|
||||
name: "direct wins over transitive",
|
||||
types: map[string]string{"net8.0": "Transitive", "netstandard2.0": "Direct"},
|
||||
wantType: "Direct",
|
||||
},
|
||||
{
|
||||
name: "central transitive wins over transitive regardless of framework order",
|
||||
types: map[string]string{"net8.0": "Transitive", "netstandard2.0": "CentralTransitive"},
|
||||
wantType: "CentralTransitive",
|
||||
},
|
||||
{
|
||||
name: "central transitive wins over transitive in the reverse framework order",
|
||||
types: map[string]string{"net8.0": "CentralTransitive", "netstandard2.0": "Transitive"},
|
||||
wantType: "CentralTransitive",
|
||||
},
|
||||
{
|
||||
name: "direct wins over central transitive",
|
||||
types: map[string]string{"net472": "CentralTransitive", "net8.0": "Direct", "netstandard2.0": "Transitive"},
|
||||
wantType: "Direct",
|
||||
},
|
||||
{
|
||||
name: "known types win over unknown ones",
|
||||
types: map[string]string{"net8.0": "Unknown", "netstandard2.0": "Transitive"},
|
||||
wantType: "Transitive",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
lockFile := dotnetPackagesLock{Dependencies: map[string]map[string]dotnetPackagesLockDep{}}
|
||||
for framework, depType := range tt.types {
|
||||
lockFile.Dependencies[framework] = map[string]dotnetPackagesLockDep{
|
||||
"Newtonsoft.Json": {Type: depType, Resolved: "13.0.3"},
|
||||
}
|
||||
}
|
||||
|
||||
entries := mergePackagesLockEntries(newPackagesLockFrameworks(lockFile))
|
||||
require.Len(t, entries, 1)
|
||||
assert.Equal(t, tt.wantType, entries[0].dep.Type)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net8.0": {
|
||||
"MyLib": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.0, )",
|
||||
"resolved": "1.0.0",
|
||||
"contentHash": "mylibhash==",
|
||||
"dependencies": {
|
||||
"log4net": "2.0.5",
|
||||
"Newtonsoft.Json": "13.0.3"
|
||||
}
|
||||
},
|
||||
"log4net": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.0.5",
|
||||
"contentHash": "log4net205hash=="
|
||||
},
|
||||
"Newtonsoft.Json": {
|
||||
"type": "Direct",
|
||||
"requested": "[13.0.3, )",
|
||||
"resolved": "13.0.3",
|
||||
"contentHash": "newtonsoft1303hash=="
|
||||
},
|
||||
"Serilog": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.1",
|
||||
"contentHash": "serilog311hash=="
|
||||
}
|
||||
},
|
||||
"net8.0/win-x64": {
|
||||
"runtime.win-x64.MyLib.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.0",
|
||||
"contentHash": "mylibnativehash==",
|
||||
"dependencies": {
|
||||
"log4net": "2.0.0",
|
||||
"RUNTIME.WIN-X64.MYLIB.NATIVE": "1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"netstandard2.0": {
|
||||
"MyLib": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.0, )",
|
||||
"resolved": "1.0.0",
|
||||
"contentHash": "mylibhash==",
|
||||
"dependencies": {
|
||||
"log4net": "1.2.15",
|
||||
"newtonsoft.json": "13.0.3",
|
||||
"Serilog": "3.1.1"
|
||||
}
|
||||
},
|
||||
"log4net": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.2.15",
|
||||
"contentHash": "log4net1215hash=="
|
||||
},
|
||||
"newtonsoft.json": {
|
||||
"type": "Transitive",
|
||||
"resolved": "13.0.3",
|
||||
"contentHash": "newtonsoft1303hash=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net8.0": {
|
||||
"Newtonsoft.Json": {
|
||||
"type": "Transitive",
|
||||
"resolved": "13.0.3",
|
||||
"contentHash": "newtonsoft1303hash=="
|
||||
},
|
||||
"mylib": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "[13.0.3, )"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
@@ -202,10 +205,23 @@ func TestFindAllLicenseCandidatesUpwards(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create in-memory filesystem
|
||||
fs := afero.NewMemMapFs()
|
||||
tt.setupFS(fs)
|
||||
setupFS, startDir, stopAt, expectedFiles := afero.Fs(fs), tt.startDir, tt.stopAt, tt.expectedFiles
|
||||
if runtime.GOOS == "windows" {
|
||||
// the finder wants native absolute paths, so root the posix-style cases on a drive
|
||||
setupFS = afero.NewBasePathFs(fs, `C:\`)
|
||||
startDir, stopAt = winAbs(startDir), winAbs(stopAt)
|
||||
expectedFiles = nil
|
||||
if tt.expectedFiles != nil {
|
||||
expectedFiles = []string{}
|
||||
}
|
||||
for _, f := range tt.expectedFiles {
|
||||
expectedFiles = append(expectedFiles, winAbs(f))
|
||||
}
|
||||
}
|
||||
tt.setupFS(setupFS)
|
||||
|
||||
// Run the function
|
||||
result, err := findAllLicenseCandidatesUpwards(tt.startDir, tt.stopAt, fs)
|
||||
result, err := findAllLicenseCandidatesUpwards(startDir, stopAt, fs)
|
||||
|
||||
// Check error expectation
|
||||
if tt.expectedError {
|
||||
@@ -214,7 +230,15 @@ func TestFindAllLicenseCandidatesUpwards(t *testing.T) {
|
||||
}
|
||||
|
||||
require.NoError(t, err, tt.description)
|
||||
assert.Equal(t, tt.expectedFiles, result, tt.description)
|
||||
assert.Equal(t, expectedFiles, result, tt.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// winAbs roots posix absolute paths on C:\, leaving relative ones relative (some cases assert they're rejected)
|
||||
func winAbs(p string) string {
|
||||
if !strings.HasPrefix(p, "/") {
|
||||
return p
|
||||
}
|
||||
return filepath.Join(`C:\`, filepath.FromSlash(p))
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/fileresolver"
|
||||
@@ -30,6 +31,8 @@ import (
|
||||
|
||||
// make will run the default make target for the given test fixture path
|
||||
func runMakeTarget(t *testing.T, fixtureName string) {
|
||||
// the fixture binaries are built inside linux containers
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
cwd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
fixtureDir := filepath.Join(cwd, "testdata/", fixtureName)
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/fileresolver"
|
||||
"github.com/anchore/syft/syft/internal/windows"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
)
|
||||
@@ -38,7 +39,12 @@ func newGoModCataloger(opts CatalogerConfig) *goModCataloger {
|
||||
|
||||
// parseGoModFile takes a go.mod and tries to resolve and lists all packages discovered.
|
||||
func (c *goModCataloger) parseGoModFile(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) (pkgs []pkg.Package, relationships []artifact.Relationship, err error) {
|
||||
modDir := filepath.Dir(string(reader.Location.Reference().RealPath))
|
||||
realPath := string(reader.Location.Reference().RealPath)
|
||||
if windows.HostRunningOnWindows() {
|
||||
// directory resolvers hand back volume-encoded posix paths (/c/...), the go toolchain needs the native path
|
||||
realPath = windows.FromPosix(realPath)
|
||||
}
|
||||
modDir := filepath.Dir(realPath)
|
||||
digests, err := parseGoSumFile(resolver, reader)
|
||||
if err != nil {
|
||||
log.Debugf("unable to get go.sum: %v", err)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
)
|
||||
|
||||
// every case here is one the magic scan must reject, asserted on parseUPXInfo's errNotUPX result.
|
||||
@@ -114,6 +115,7 @@ func TestParseUPXInfo_ValidHeader(t *testing.T) {
|
||||
// ignores that padding if there is some. A build that needs none passes either way, which is how the bug
|
||||
// went unnoticed. The case without padding is covered by TestDecompressUPX_TailExtentsArePlacedPastTheLoader.
|
||||
func TestImageSmallUPXNeedsLoaderPadding(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-small-upx")
|
||||
rc, err := img.OpenPathFromSquash(stereoscopeFile.Path("/run-me"))
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -2439,12 +2439,18 @@
|
||||
"@backstage/plugin-catalog-backend": [
|
||||
"cpe:2.3:a:linuxfoundation:backstage\\/plugin-catalog-backend-module-unprocessed:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@backstage/plugin-catalog-unprocessed-entities": [
|
||||
"cpe:2.3:a:linuxfoundation:backstage\\/plugin-catalog-unprocessed-entities:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@backstage/plugin-catalog-unprocessed-entities-common": [
|
||||
"cpe:2.3:a:linuxfoundation:backstage\\/plugin-catalog-unprocessed-entities-common:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@backstage/plugin-scaffolder-backend": [
|
||||
"cpe:2.3:a:linuxfoundation:backstage\\/plugin-scaffolder-backend:*:*:*:*:*:*:*:*"
|
||||
],
|
||||
"@backstage/plugin-scaffolder-backend-module-sentry": [
|
||||
"cpe:2.3:a:linuxfoundation:backstage\\/plugin-scaffolder-backend-module-sentry:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@backstage/plugin-techdocs": [
|
||||
"cpe:2.3:a:linuxfoundation:\\@backstage\\/plugin-techdocs:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
@@ -3834,11 +3840,18 @@
|
||||
"cpe:2.3:a:gaoxuyan_project:gaoxuyan:*:*:*:*:*:*:*:*"
|
||||
],
|
||||
"gatsby-plugin-mdx": [
|
||||
"cpe:2.3:a:gatsbyjs:gatsby-plugin-mdx:*:*:*:*:*:node.js:*:*",
|
||||
"cpe:2.3:a:gatsbyjs:gatsby:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"gatsby-plugin-sharp": [
|
||||
"cpe:2.3:a:gatsbyjs:gatsby-plugin-sharp:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"gatsby-source-wordpress": [
|
||||
"cpe:2.3:a:gatsbyjs:gatsby-source-wordpress:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"gatsby-transformer-remark": [
|
||||
"cpe:2.3:a:gatsbyjs:gatsby-transformer-remark:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"gedi": [
|
||||
"cpe:2.3:a:gedi_project:gedi:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
@@ -6265,6 +6278,9 @@
|
||||
"vega-functions": [
|
||||
"cpe:2.3:a:vega-functions_project:vega-functions:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"vega-selections": [
|
||||
"cpe:2.3:a:vega_project:vega:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"velocityjs": [
|
||||
"cpe:2.3:a:shepherdwind:velocity.js:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
@@ -6975,6 +6991,9 @@
|
||||
"openlit": [
|
||||
"cpe:2.3:a:openlit:openlit_software_development_kit:*:*:*:*:*:python:*:*"
|
||||
],
|
||||
"openml": [
|
||||
"cpe:2.3:a:openml:openml:*:*:*:*:*:python:*:*"
|
||||
],
|
||||
"openpyxl": [
|
||||
"cpe:2.3:a:python:openpyxl:*:*:*:*:*:*:*:*"
|
||||
],
|
||||
@@ -8796,6 +8815,9 @@
|
||||
"adaptive-images": [
|
||||
"cpe:2.3:a:nevma:adaptive_images:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"adbuddy-adblocker-detection": [
|
||||
"cpe:2.3:a:netfunkdesign:adbuddy\\+_\\(adblocker_detection\\):*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"add-actions-and-filters": [
|
||||
"cpe:2.3:a:add_shortcodes_actions_and_filters_project:add_shortcodes_actions_and_filters:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -8948,7 +8970,8 @@
|
||||
"cpe:2.3:a:niceit:advance_post_prefix:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"advance-search": [
|
||||
"cpe:2.3:a:advance_search_project:advance_search:*:*:*:*:*:wordpress:*:*"
|
||||
"cpe:2.3:a:advance_search_project:advance_search:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:mndpsingh287:advanced_search:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"advanced-access-manager": [
|
||||
"cpe:2.3:a:vasyltech:advanced_access_manager:*:*:*:*:*:wordpress:*:*"
|
||||
@@ -9098,6 +9121,9 @@
|
||||
"agp-font-awesome-collection": [
|
||||
"cpe:2.3:a:profosbox:agp_font_awesome_collection:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ahathat": [
|
||||
"cpe:2.3:a:mitchelllevy:ahathat:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ai-assistant-by-10web": [
|
||||
"cpe:2.3:a:10web:ai_assistant:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -9524,6 +9550,9 @@
|
||||
"asf-allow-svg-files": [
|
||||
"cpe:2.3:a:allow_svg_files_project:allow_svg_files:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"asgard": [
|
||||
"cpe:2.3:a:outself:asgard_security_scanner:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"asgaros-forum": [
|
||||
"cpe:2.3:a:asgaros:asgaros_forum:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -9803,6 +9832,9 @@
|
||||
"banner-effect-header": [
|
||||
"cpe:2.3:a:banner_effect_header_project:banner_effect_header:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"banner-garden": [
|
||||
"cpe:2.3:a:karacsi_maci:banner_garden:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"banner-management-for-woocommerce": [
|
||||
"cpe:2.3:a:dotstore:woocommerce_category_banner_management:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:multidots:banner_management_for_woocommerce:*:*:*:*:*:wordpress:*:*"
|
||||
@@ -10077,6 +10109,9 @@
|
||||
"blue-admin": [
|
||||
"cpe:2.3:a:blue-admin_project:blue-admin:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"blue-triad-ezanalytics": [
|
||||
"cpe:2.3:a:jbahlquist:blue_triad_ezanalytics:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"bluet-keywords-tooltip-generator": [
|
||||
"cpe:2.3:a:tooltipy:tooltipy:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -10334,6 +10369,9 @@
|
||||
"bulk-image-resizer": [
|
||||
"cpe:2.3:a:giuliopanda:bulk_images_optimizer:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"bulk-me-now": [
|
||||
"cpe:2.3:a:ombu:bulk_me_now\\!:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"bulk-noindex-nofollow-toolkit-by-mad-fish": [
|
||||
"cpe:2.3:a:madfishdigital:bulk_noindex_\\\u0026_nofollow_toolkit:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -10355,6 +10393,9 @@
|
||||
"business": [
|
||||
"cpe:2.3:a:snilesh:business:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"business-card-by-esterox-100": [
|
||||
"cpe:2.3:a:esterox:business_card:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"business-directory-plugin": [
|
||||
"cpe:2.3:a:businessdirectoryplugin:business_directory:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:strategy11:business_directory_plugin_-_easy_listing_directories:*:*:*:*:*:wordpress:*:*"
|
||||
@@ -10542,7 +10583,8 @@
|
||||
"cpe:2.3:a:vinojcardoza:ajax_post_search:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"cardoza-facebook-like-box": [
|
||||
"cpe:2.3:a:facebook_like_box_project:facebook_like_box:*:*:*:*:*:wordpress:*:*"
|
||||
"cpe:2.3:a:facebook_like_box_project:facebook_like_box:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:johnnash1975:easy_social_like_box_popup_sidebar_widget:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"cardoza-wordpress-poll": [
|
||||
"cpe:2.3:a:cardozatechnologies:wordpress_poll:*:*:*:*:*:wordpress:*:*",
|
||||
@@ -11201,6 +11243,9 @@
|
||||
"contact-list": [
|
||||
"cpe:2.3:a:tammersoft:contact_list:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"contact-us-by-lord-linus": [
|
||||
"cpe:2.3:a:lordlinus:contact_us:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"contact-us-page-contact-people": [
|
||||
"cpe:2.3:a:a3rev:contact_us_page_-_contact_people:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -11371,6 +11416,9 @@
|
||||
"countdown-builder": [
|
||||
"cpe:2.3:a:edmonsoft:countdown_builder:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"countdown-timer-block": [
|
||||
"cpe:2.3:a:flickdevs:countdown_timer_for_wordpress_block_editor:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"countdown-timer-for-elementor": [
|
||||
"cpe:2.3:a:flickdevs:countdown_timer_for_elementor:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -11575,6 +11623,9 @@
|
||||
"cpe:2.3:a:bestwebsoft:custom_admin:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:bestwebsoft:custom_admin_page:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"custom-author-base": [
|
||||
"cpe:2.3:a:jfarthing:custom_author_base:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"custom-banners": [
|
||||
"cpe:2.3:a:goldplugins:custom_banners:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -11651,6 +11702,9 @@
|
||||
"custom-options-plus": [
|
||||
"cpe:2.3:a:leocaseiro:custom_options_plus:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"custom-order-statuses-for-woocommerce": [
|
||||
"cpe:2.3:a:nuggethon:custom_order_status_manager_for_woocommerce:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"custom-permalinks": [
|
||||
"cpe:2.3:a:samiahmedsiddiqui:custom_permalinks:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -11882,6 +11936,9 @@
|
||||
"demon-image-annotation": [
|
||||
"cpe:2.3:a:superwhite:demon_image_annotation:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"dental-optimizer-patient-generator-app": [
|
||||
"cpe:2.3:a:healthygrid:dental_optimizer_patient_generator_app:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"deny-all-firewall": [
|
||||
"cpe:2.3:a:deny_all_firewall_project:deny_all_firewall:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -12028,6 +12085,9 @@
|
||||
"donorbox-donation-form": [
|
||||
"cpe:2.3:a:donorbox:donorbox:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"dont-muck-my-markup": [
|
||||
"cpe:2.3:a:martynchamberlin:dont_muck_my_markup:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"doofinder-for-woocommerce": [
|
||||
"cpe:2.3:a:doofinder:doofinder:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -12397,6 +12457,9 @@
|
||||
"ecpay-ecommerce-for-woocommerce": [
|
||||
"cpe:2.3:a:ecpay:ecpay_ecommerce_for_woocommerce:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ect-homepage-products": [
|
||||
"cpe:2.3:a:etemplates:ect_home_page_products:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ecwid-shopping-cart": [
|
||||
"cpe:2.3:a:lightspeedhq:ecwid_ecommerce_shopping_cart:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -13127,7 +13190,8 @@
|
||||
"cpe:2.3:a:mehanoid:flatpm:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"flattr": [
|
||||
"cpe:2.3:a:fkrauthan:wp-mpdf:*:*:*:*:*:wordpress:*:*"
|
||||
"cpe:2.3:a:fkrauthan:wp-mpdf:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:flattr:flattr:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"flexi": [
|
||||
"cpe:2.3:a:odude:flexi:*:*:*:*:*:wordpress:*:*"
|
||||
@@ -13160,6 +13224,9 @@
|
||||
"flipping-cards": [
|
||||
"cpe:2.3:a:info-d-74:flipping_cards:*:*:*:*:free:wordpress:*:*"
|
||||
],
|
||||
"float-block": [
|
||||
"cpe:2.3:a:computy:float_block:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"float-menu": [
|
||||
"cpe:2.3:a:wow-company:float_menu:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -13422,6 +13489,9 @@
|
||||
"full-customer": [
|
||||
"cpe:2.3:a:full:full_-_customer:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"full-screen-page-background-image-slideshow": [
|
||||
"cpe:2.3:a:mohsinrasool:full_screen_\\(page\\)_background_image_slideshow:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"full-site-editing": [
|
||||
"cpe:2.3:a:automattic:wordpress.com_editing_toolkit:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -13558,6 +13628,9 @@
|
||||
"gdpr-framework": [
|
||||
"cpe:2.3:a:data443:gdpr_framework:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"gearside-developer-dashboard": [
|
||||
"cpe:2.3:a:gearside:gearside_developer_dashboard:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"gecka-terms-thumbnails": [
|
||||
"cpe:2.3:a:gecka:terms_thumbnails:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -13567,6 +13640,9 @@
|
||||
"generate-pdf-using-contact-form-7": [
|
||||
"cpe:2.3:a:zealousweb:generate_pdf_using_contact_form_7:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"generateblocks": [
|
||||
"cpe:2.3:a:generatepress:generateblocks:*:*:*:*:free:wordpress:*:*"
|
||||
],
|
||||
"generatepress-premium": [
|
||||
"cpe:2.3:a:generatepress:generatepress:*:*:*:*:premium:wordpress:*:*"
|
||||
],
|
||||
@@ -13952,6 +14028,9 @@
|
||||
"gtmetrix-for-wordpress": [
|
||||
"cpe:2.3:a:gtmetrix:gtmetrix:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"gtpayment-donation": [
|
||||
"cpe:2.3:a:harryhe:gtpayment_donations:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"gtranslate": [
|
||||
"cpe:2.3:a:gtranslate:gtranslate:*:*:*:*:enterprise:wordpress:*:*",
|
||||
"cpe:2.3:a:gtranslate:gtranslate:*:*:*:*:pro:wordpress:*:*",
|
||||
@@ -14693,6 +14772,9 @@
|
||||
"iws-geo-form-fields": [
|
||||
"cpe:2.3:a:iws-geo-form-fields_project:iws-geo-form-fields:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"javo-core": [
|
||||
"cpe:2.3:a:javothemes:javo_core:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"jayj-quicktag": [
|
||||
"cpe:2.3:a:jayj_quicktag_project:jayj_quicktag:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -14969,6 +15051,9 @@
|
||||
"kwayy-html-sitemap": [
|
||||
"cpe:2.3:a:kwayyinfotech:kwayy_html_sitemap:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"l-squared-hub-wp-virtual-device": [
|
||||
"cpe:2.3:a:lsquared:l_squared_hub:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"labtools": [
|
||||
"cpe:2.3:a:creativityjuice:labtools:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -15196,6 +15281,9 @@
|
||||
"link-optimizer-lite": [
|
||||
"cpe:2.3:a:link_optimizer_lite_project:link_optimizer_lite:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"link-party": [
|
||||
"cpe:2.3:a:evanliewer:illi_link_party\\!:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"link-to-bible": [
|
||||
"cpe:2.3:a:bibleserver:link_to_bible:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -15531,6 +15619,9 @@
|
||||
"malinky-ajax-pagination": [
|
||||
"cpe:2.3:a:malinky:malinky-ajax-pagination:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"mambo-joomla-importer": [
|
||||
"cpe:2.3:a:misterpah:mambo_joomla_importer:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"manage-notification-emails": [
|
||||
"cpe:2.3:a:freeamigos:manage_notification_e-mails:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -15592,6 +15683,9 @@
|
||||
"marketing-and-seo-booster": [
|
||||
"cpe:2.3:a:secretlab:marketing_and_seo_booster:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"marketing-optimizer": [
|
||||
"cpe:2.3:a:marketingoptimizer:marketing_optimizer:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"marketo-forms-and-tracking": [
|
||||
"cpe:2.3:a:hutchhouse:marketo_forms_and_tracking:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -16039,6 +16133,9 @@
|
||||
"mq-woocommerce-products-price-bulk-edit": [
|
||||
"cpe:2.3:a:mq-woocommerce-products-price-bulk-edit_project:mq-woocommerce-products-price-bulk-edit:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"mrlegend-typedjs": [
|
||||
"cpe:2.3:a:mrlegend1235:typed_js:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ms-registration": [
|
||||
"cpe:2.3:a:alphaefficiencyteam:custom_login_and_registration:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -16290,6 +16387,9 @@
|
||||
"news-kit-elementor-addons": [
|
||||
"cpe:2.3:a:blazethemes:news_kit_elementor_addons:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"news-list": [
|
||||
"cpe:2.3:a:mahinsha:newsticker:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"newsletter": [
|
||||
"cpe:2.3:a:thenewsletterplugin:newsletter:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -16305,6 +16405,9 @@
|
||||
"newsletter-popup": [
|
||||
"cpe:2.3:a:mndpsingh287:newsletter_popup:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"newsletter2go": [
|
||||
"cpe:2.3:a:newsletter2go:newsletter2go:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"newsletters-lite": [
|
||||
"cpe:2.3:a:tribulant:newsletters:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:tribulant:newsletters:*:*:*:*:free:wordpress:*:*"
|
||||
@@ -16324,7 +16427,8 @@
|
||||
],
|
||||
"nextend-facebook-connect": [
|
||||
"cpe:2.3:a:nextendweb:facebook_connect:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:nextendweb:nextend_facebook_connect:*:*:*:*:*:wordpress:*:*"
|
||||
"cpe:2.3:a:nextendweb:nextend_facebook_connect:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:nextendweb:nextend_social_login:*:*:*:*:free:wordpress:*:*"
|
||||
],
|
||||
"nextend-twitter-connect": [
|
||||
"cpe:2.3:a:nextendweb:nextend_twitter_connect:*:*:*:*:*:wordpress:*:*"
|
||||
@@ -16364,6 +16468,9 @@
|
||||
"ninja-job-board": [
|
||||
"cpe:2.3:a:wpmanageninja:ninja_job_board:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ninja-page-categories-and-tags": [
|
||||
"cpe:2.3:a:ninja_pages_project:ninja_pages:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ninja-tables": [
|
||||
"cpe:2.3:a:wpmanageninja:ninja_tables:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -16407,6 +16514,9 @@
|
||||
"nofollow-links": [
|
||||
"cpe:2.3:a:nofollow_links_project:nofollow_links:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"nokaut-offers-box": [
|
||||
"cpe:2.3:a:nokautpl:nokaut_offers_box:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"nokia-mapsplaces": [
|
||||
"cpe:2.3:a:nokia_maps_\\\u0026_places_project:nokia_maps_\\\u0026_places:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -16456,6 +16566,9 @@
|
||||
"ns-woocommerce-watermark": [
|
||||
"cpe:2.3:a:nsthemes:ns_watermark_for_woocommerce:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ntzantispam": [
|
||||
"cpe:2.3:a:ionutstaicu:ntz_atispam:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"oauth-client": [
|
||||
"cpe:2.3:a:miniorange:oauth_2.0_client_for_sso:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -17618,6 +17731,9 @@
|
||||
"pushbiz": [
|
||||
"cpe:2.3:a:csimplifyit:pushbiz:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"pvn-auth-popup": [
|
||||
"cpe:2.3:a:freebiesdownload:pvn_auth_popup:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"pwa-for-wp": [
|
||||
"cpe:2.3:a:magazine3:pwa_for_wp_\\\u0026_amp:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -18054,6 +18170,9 @@
|
||||
"responsive-vector-maps": [
|
||||
"cpe:2.3:a:thinkupthemes:responsive_vector_maps:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"responsive-video": [
|
||||
"cpe:2.3:a:kirstyburgoine:responsive_video:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"responsive-video-embed": [
|
||||
"cpe:2.3:a:lepileppanen:responsive_video_embed:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -18069,6 +18188,9 @@
|
||||
"restaurant-reservations": [
|
||||
"cpe:2.3:a:fivestarplugins:five_star_restaurant_reservations:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"restaurant-solutions-checklist": [
|
||||
"cpe:2.3:a:josephlopreste:restaurant_solutions_-_checklist:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"restrict-content": [
|
||||
"cpe:2.3:a:liquidweb:restrict_content:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -18590,6 +18712,9 @@
|
||||
"shortcode-addons": [
|
||||
"cpe:2.3:a:oxilab:shortcode_addons:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"shortcode-cleaner-lite": [
|
||||
"cpe:2.3:a:jozoor:shortcode_cleaner_lite:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"shortcode-factory": [
|
||||
"cpe:2.3:a:wpmadeasy:shortcode_factory:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -18972,12 +19097,18 @@
|
||||
"simple-video-embedder": [
|
||||
"cpe:2.3:a:simple_video_embedder_project:simple_video_embedder:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"simple-video-management-system": [
|
||||
"cpe:2.3:a:maximize:simple_video_management_system:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"simple-yearly-archive": [
|
||||
"cpe:2.3:a:simple_yearly_archive_project:simple_yearly_archive:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"simple-youtube-responsive": [
|
||||
"cpe:2.3:a:simple_youtube_responsive_project:simple_youtube_responsive:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"simplebooklet": [
|
||||
"cpe:2.3:a:simplebooklet:simplebooklet_pdf_viewer_and_embedder:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"simplemap": [
|
||||
"cpe:2.3:a:simplemap-plugin:simplemap_store_locator:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -19448,6 +19579,9 @@
|
||||
"spiffy-calendar": [
|
||||
"cpe:2.3:a:spiffyplugins:spiffy_calendar:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"spiritual-gifts-survey": [
|
||||
"cpe:2.3:a:mynamedia:spiritual_gifts_survey_\\(and_optional_s.h.a.p.e_survey\\):*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"splash-header": [
|
||||
"cpe:2.3:a:zeesweb:splash_header:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -20258,6 +20392,9 @@
|
||||
"ttv-easy-embed-player": [
|
||||
"cpe:2.3:a:streamweasels:twitch_player:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"tube-video-ads-lite": [
|
||||
"cpe:2.3:a:gualdoni:tube_video_ads_lite:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"tubepress": [
|
||||
"cpe:2.3:a:tubepress:tubepress:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -20416,6 +20553,9 @@
|
||||
"cpe:2.3:a:ultimatemember:ultimate_member:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:ultimatemember:user_profile_\\\u0026_membership:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ultimate-noindex-nofollow-tool": [
|
||||
"cpe:2.3:a:jonkemp:ultimate_noindex_nofollow_tool:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ultimate-noindex-nofollow-tool-ii": [
|
||||
"cpe:2.3:a:ultimate_noindex_nofollow_tool_ii_project:ultimate_noindex_nofollow_tool_ii:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -20551,6 +20691,9 @@
|
||||
"url-media-uploader": [
|
||||
"cpe:2.3:a:apprhyme:url_media_uploader:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"url-params": [
|
||||
"cpe:2.3:a:jeremyshapiro:url_params:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"url-shortify": [
|
||||
"cpe:2.3:a:kaizencoders:url_shortify:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -20678,6 +20821,9 @@
|
||||
"vayu-blocks": [
|
||||
"cpe:2.3:a:themehunk:vayu_blocks:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"vc-addons-by-bit14": [
|
||||
"cpe:2.3:a:pagebuilderaddons:web_and_woocommerce_addons_for_wpbakery_builder:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"vc-tabs": [
|
||||
"cpe:2.3:a:oxilab:responsive_tabs:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -20780,6 +20926,9 @@
|
||||
"visual-portfolio": [
|
||||
"cpe:2.3:a:visualportfolio:visual_portfolio\\,_photo_gallery_\\\u0026_post_grid:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"visual-sound": [
|
||||
"cpe:2.3:a:freakingwildchild:visual_sound:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"visualcomposer": [
|
||||
"cpe:2.3:a:visualcomposer:visual_composer_website_builder:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -20950,6 +21099,9 @@
|
||||
"web-application-firewall": [
|
||||
"cpe:2.3:a:miniorange:web_application_firewall:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"web-cam": [
|
||||
"cpe:2.3:a:murtuzamakda52:web-cam:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"web-directory-free": [
|
||||
"cpe:2.3:a:salephpscripts:web_directory_free:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -21015,6 +21167,9 @@
|
||||
"website-article-monetization-by-magenet": [
|
||||
"cpe:2.3:a:magenet:website_article_monetization:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"website-contact-form-with-file-upload": [
|
||||
"cpe:2.3:a:najeebmedia:website_contact_form_with_file_upload:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"website-file-changes-monitor": [
|
||||
"cpe:2.3:a:melapress:melapress_file_monitor:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -21120,6 +21275,9 @@
|
||||
"widgets-on-pages": [
|
||||
"cpe:2.3:a:widgets-on-pages_project:widgets-on-pages:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"widgets-reset": [
|
||||
"cpe:2.3:a:justintadlock:widgets_reset:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"winterlock": [
|
||||
"cpe:2.3:a:wp_system_log_project:wp_system_log:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -21127,7 +21285,11 @@
|
||||
"cpe:2.3:a:themeinprogress:wip_custom_login:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wise-chat": [
|
||||
"cpe:2.3:a:kaine:wise_chat:*:*:*:*:*:wordpress:*:*"
|
||||
"cpe:2.3:a:kaine:wise_chat:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:kainex:wise_chat:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wise-forms": [
|
||||
"cpe:2.3:a:kainex:wise_forms:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wiseagentleadform": [
|
||||
"cpe:2.3:a:wiseagent:wise_agent_capture_forms:*:*:*:*:*:wordpress:*:*"
|
||||
@@ -21227,6 +21389,9 @@
|
||||
"woo-ecommerce-tracking-for-google-and-facebook": [
|
||||
"cpe:2.3:a:multidots:enhanced_ecommerce_google_analytics_for_woocommerce:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"woo-edit-templates": [
|
||||
"cpe:2.3:a:ioannup:edit_woocommerce_templates:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"woo-esto": [
|
||||
"cpe:2.3:a:rebing:woocommerce_esto:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -21539,6 +21704,9 @@
|
||||
"wordpress-access-control": [
|
||||
"cpe:2.3:a:brandonwamboldt:wordpress_access_control:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wordpress-activity-o-meter": [
|
||||
"cpe:2.3:a:erwinwolff:wordpress_activity-o-meter:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wordpress-backup-to-dropbox": [
|
||||
"cpe:2.3:a:wordpress_backup_to_dropbox_project:wordpress_backup_to_dropbox:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -21598,6 +21766,9 @@
|
||||
"wordpress-tooltips": [
|
||||
"cpe:2.3:a:tooltips:wordpress_tooltips:*:*:*:*:free:wordpress:*:*"
|
||||
],
|
||||
"wordpress-twitterbot": [
|
||||
"cpe:2.3:a:corbyboy:marketing_twitter_bot:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wordpress-users": [
|
||||
"cpe:2.3:a:jonathonkemp:wordpress_users:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -21610,6 +21781,9 @@
|
||||
"wow-carousel-for-divi-lite": [
|
||||
"cpe:2.3:a:elegantthemes:divi_carousel:*:*:*:*:free:wordpress:*:*"
|
||||
],
|
||||
"wow-entrance-effects-wee": [
|
||||
"cpe:2.3:a:darkosxrc:wow_entrance_effects_\\(wee\\!\\):*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wow-moodboard-lite": [
|
||||
"cpe:2.3:a:wownmedia:wow_moodboard:*:*:*:*:lite:wordpress:*:*"
|
||||
],
|
||||
@@ -21632,6 +21806,9 @@
|
||||
"wp-accessibility-helper": [
|
||||
"cpe:2.3:a:volkov:wp_accessibility_helper:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-action-network": [
|
||||
"cpe:2.3:a:actionnetwork:action_network:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-admin-logo-changer": [
|
||||
"cpe:2.3:a:wp_admin_logo_changer_project:wp_admin_logo_changer:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -21641,6 +21818,9 @@
|
||||
"wp-airbnb-review-slider": [
|
||||
"cpe:2.3:a:ljapps:wp_airbnb_review_slider:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-ajax-contact-form": [
|
||||
"cpe:2.3:a:sportsdanka:wp_ajax_contact_form:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-all-export": [
|
||||
"cpe:2.3:a:soflyy:export_any_wordpress_data_to_xml\\/csv:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:soflyy:wp_all_export:*:*:*:*:pro:wordpress:*:*"
|
||||
@@ -22034,6 +22214,9 @@
|
||||
"wp-email-capture": [
|
||||
"cpe:2.3:a:winwar:wp_email_capture:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-email-newsletter": [
|
||||
"cpe:2.3:a:megamindstechnologies:wordpress_email_newsletter:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-email-users": [
|
||||
"cpe:2.3:a:techspawn:wp-email-users:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -22168,6 +22351,9 @@
|
||||
"wp-gdpr-core": [
|
||||
"cpe:2.3:a:appsaloon:wp-gdpr:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-geshi-highlight": [
|
||||
"cpe:2.3:a:jgehrcke:wp-geshi-highlight:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-githuber-md": [
|
||||
"cpe:2.3:a:terryl:wp_githuber_md:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -22261,6 +22447,9 @@
|
||||
"wp-image-slideshow": [
|
||||
"cpe:2.3:a:gopiplus:wp_image_slideshow:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-image-uploader": [
|
||||
"cpe:2.3:a:ivanm:wp_image_uploader:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-image-zoooom": [
|
||||
"cpe:2.3:a:silkypress:wp_image_zoom:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -22460,6 +22649,9 @@
|
||||
"wp-multi-store-locator": [
|
||||
"cpe:2.3:a:wpexperts:wp_multi_store_locator:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-multitasking": [
|
||||
"cpe:2.3:a:ngothang:wp_multitasking:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-nested-pages": [
|
||||
"cpe:2.3:a:kylephillips:nested_pages:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -22472,6 +22664,9 @@
|
||||
"wp-olivecart": [
|
||||
"cpe:2.3:a:wp-olivecart_project:wp-olivecart:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-online-users-stats": [
|
||||
"cpe:2.3:a:hk1993:wp_online_users_stats:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-open-graph": [
|
||||
"cpe:2.3:a:custom4web:wp_open_graph:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -22903,6 +23098,9 @@
|
||||
"wp-symposium-pro": [
|
||||
"cpe:2.3:a:wpsymposiumpro:wp_symposium_pro:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-syntax": [
|
||||
"cpe:2.3:a:connections-pro:wp-syntax:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-t-wap": [
|
||||
"cpe:2.3:a:wp-t-wap_project:wp-t-wap:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -22948,6 +23146,9 @@
|
||||
"wp-timed-popup": [
|
||||
"cpe:2.3:a:timed_popup_project:timed_popup:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-tithely": [
|
||||
"cpe:2.3:a:jordangillman:tithe.ly_giving_button:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-todo": [
|
||||
"cpe:2.3:a:delower:wp_to_do:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -23570,6 +23771,9 @@
|
||||
"z-url-preview": [
|
||||
"cpe:2.3:a:z-url_preview_project:z-url_preview:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"zalomeni": [
|
||||
"cpe:2.3:a:kybernaut:zalomeni:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"zarinpal-paid-downloads": [
|
||||
"cpe:2.3:a:amini7:zarinpal_paid_download:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
|
||||
@@ -12,18 +12,25 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// maxBundleSearchSize bounds the bytes findSignatureOffset will hold at once while looking for the
|
||||
// bundle marker.
|
||||
// maxBundleSearchSize bounds how far into a file findSignatureOffset looks for the bundle marker.
|
||||
//
|
||||
// The marker sits inside the executable structure, so the search legitimately covers a whole
|
||||
// single-file bundle, which routinely runs past 100MB and can reach a few hundred for an app that
|
||||
// embeds sizable assets. Clamping to the file length alone is not enough: a mostly empty file costs
|
||||
// almost nothing inside a compressed layer, so a small artifact can still authorize a multi-gigabyte
|
||||
// allocation. The trade-off is that a bundle larger than this loses its deps.json rather than being
|
||||
// cataloged, which is the correct direction to fail when the alternative is OOM-killing the scan.
|
||||
// This mirrors maxDeclaredSectionSize in syft/internal/elfutil.
|
||||
// almost nothing inside a compressed layer, so a small artifact can still authorize an unbounded
|
||||
// amount of reading. The trade-off is that a bundle larger than this loses its deps.json rather than
|
||||
// being cataloged, which is the correct direction to fail. This mirrors maxDeclaredSectionSize in
|
||||
// syft/internal/elfutil. What the search holds in memory is bounded separately, by
|
||||
// markerSearchWindowSize, so raising this costs time rather than resident bytes.
|
||||
maxBundleSearchSize = 512 * intFile.MB
|
||||
|
||||
// markerSearchWindowSize is how much of a file findSignatureOffset holds at once.
|
||||
markerSearchWindowSize = 256 * intFile.KB
|
||||
|
||||
// bundleHeaderOffsetSize is the width of the little-endian header offset stored immediately before
|
||||
// the marker.
|
||||
bundleHeaderOffsetSize = 8
|
||||
|
||||
// maxDepsJSONSize bounds an embedded deps.json. These are dependency manifests, so real ones are
|
||||
// measured in KB even for large applications.
|
||||
maxDepsJSONSize = 3 * intFile.MB
|
||||
@@ -37,13 +44,26 @@ const (
|
||||
)
|
||||
|
||||
// dotNetBundleSignature is the SHA-256 hash of ".net core bundle" used to identify single-file bundles.
|
||||
var dotNetBundleSignature = []byte{
|
||||
var dotNetBundleSignature = [32]byte{
|
||||
0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
|
||||
0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
|
||||
0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
|
||||
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae,
|
||||
}
|
||||
|
||||
// markerSearchOverlap is how much of each search window carries over into the next: the bytes a marker
|
||||
// could straddle the boundary with, plus the offset that precedes it. Retaining both is what lets a
|
||||
// windowed search give the same answer as reading the file whole - a marker split across two reads is
|
||||
// still matched, and the offset in front of it is always in the same window as the match.
|
||||
const markerSearchOverlap = bundleHeaderOffsetSize + len(dotNetBundleSignature) - 1
|
||||
|
||||
// the slide at the end of findSignatureOffset carries markerSearchOverlap bytes into the next window and
|
||||
// advances by the rest, so a window no wider than the overlap would take a negative slice index and never
|
||||
// move. The window is five orders of magnitude clear of that today; this is what refuses to compile if
|
||||
// anyone shrinks it to the marker's own scale, which is the tempting thing to do to make a test cheaper.
|
||||
// The -1 is what makes equality fail too: the window has to exceed the overlap, not merely match it.
|
||||
const _ = uint(markerSearchWindowSize - markerSearchOverlap - 1)
|
||||
|
||||
// ExtractDepsJSON returns the deps.json embedded in the .NET single-file bundle in r, or "" if r carries no
|
||||
// bundle marker.
|
||||
//
|
||||
@@ -94,29 +114,17 @@ func findBundleHeaderOffset(r unionreader.UnionReader, searchLimit int64) (int64
|
||||
clamped = true
|
||||
}
|
||||
|
||||
// this scans a whole executable, routinely over 100MB for a single-file bundle, so the buffer is sized
|
||||
// exactly once. An append-growing read holds both arrays at its final growth and would cost well over
|
||||
// twice the file's own size for the same result.
|
||||
searchData := make([]byte, limit)
|
||||
|
||||
// a short read is not fatal here: the marker may well be in what we did get, so search the bytes we
|
||||
// actually hold. ReadAt reports a short read as io.EOF, and may report a full one that way too, so the
|
||||
// count is what says how much there is to search.
|
||||
n, err := r.ReadAt(searchData, 0)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
headerOffset, found, err := findSignatureOffset(r, limit)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
idx := bytes.Index(searchData[:n], dotNetBundleSignature)
|
||||
if idx == -1 || idx < 8 {
|
||||
if !found {
|
||||
if clamped {
|
||||
return 0, fmt.Errorf("no bundle marker in the first %d bytes and the rest of the %d byte file was not searched", maxBundleSearchSize, size)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
headerOffset := int64(binary.LittleEndian.Uint64(searchData[idx-8 : idx]))
|
||||
|
||||
if headerOffset == 0 {
|
||||
// the marker is compiled into every apphost; only publishing as a single file fills in the offset
|
||||
return 0, nil
|
||||
@@ -131,6 +139,52 @@ func findBundleHeaderOffset(r unionreader.UnionReader, searchLimit int64) (int64
|
||||
return headerOffset, nil
|
||||
}
|
||||
|
||||
// findSignatureOffset searches the first limit bytes of r for the bundle marker and returns the header
|
||||
// offset stored in the 8 bytes immediately before it, or false if the marker is not there.
|
||||
func findSignatureOffset(r unionreader.UnionReader, limit int64) (int64, bool, error) {
|
||||
// nothing shorter than the marker plus the offset in front of it can hold a usable match
|
||||
if limit < int64(bundleHeaderOffsetSize+len(dotNetBundleSignature)) {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
buf := make([]byte, min(int64(markerSearchWindowSize), limit))
|
||||
|
||||
var (
|
||||
windowStart int64 // offset in the file that buf[0] holds
|
||||
filled int // bytes of buf that hold file content
|
||||
)
|
||||
|
||||
for {
|
||||
// a short read is not fatal: the marker may well be in what we did get. ReadAt reports a short
|
||||
// read as io.EOF, and may report a full one that way too, so the count is what says how much
|
||||
// there is to search.
|
||||
want := min(int64(len(buf)-filled), limit-windowStart-int64(filled))
|
||||
n, err := r.ReadAt(buf[filled:int64(filled)+want], windowStart+int64(filled))
|
||||
filled += n
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
if idx := bytes.Index(buf[:filled], dotNetBundleSignature[:]); idx >= 0 {
|
||||
// only reachable in the first window: after a slide the overlap guarantees that a match not
|
||||
// already seen begins at least bundleHeaderOffsetSize into the buffer
|
||||
if idx < bundleHeaderOffsetSize {
|
||||
return 0, false, nil
|
||||
}
|
||||
return int64(binary.LittleEndian.Uint64(buf[idx-bundleHeaderOffsetSize : idx])), true, nil
|
||||
}
|
||||
|
||||
// a buffer the read could not fill means the reader ran out before the limit did
|
||||
if filled < len(buf) || windowStart+int64(filled) >= limit {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
copy(buf, buf[filled-markerSearchOverlap:filled])
|
||||
windowStart += int64(filled - markerSearchOverlap)
|
||||
filled = markerSearchOverlap
|
||||
}
|
||||
}
|
||||
|
||||
// dotNetBundleHeader represents the fixed portion of the bundle header (version 1+)
|
||||
type dotNetBundleHeader struct {
|
||||
MajorVersion uint32
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
func fileWithSignatureAt(size, sigStart int, headerOffset uint64) []byte {
|
||||
data := make([]byte, size)
|
||||
binary.LittleEndian.PutUint64(data[sigStart-8:sigStart], headerOffset)
|
||||
copy(data[sigStart:], dotNetBundleSignature)
|
||||
copy(data[sigStart:], dotNetBundleSignature[:])
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ func TestFindBundleHeaderOffset(t *testing.T) {
|
||||
{
|
||||
// there is no room for the 8-byte header offset before the signature
|
||||
name: "signature too close to the start to carry an offset",
|
||||
data: append(append([]byte{0, 0}, dotNetBundleSignature...), make([]byte, 32)...),
|
||||
data: append(append([]byte{0, 0}, dotNetBundleSignature[:]...), make([]byte, 32)...),
|
||||
searchLimit: 128,
|
||||
},
|
||||
{
|
||||
@@ -295,3 +295,136 @@ func TestFindDepsJSONInManifest_PlausibleEntryCountIsWalked(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, got, "no deps.json entry in this manifest")
|
||||
}
|
||||
|
||||
// A windowed search only gives the same answer as reading the file whole if a marker lying across a
|
||||
// window boundary - or one whose header offset lies across it - is still matched. These are the offsets
|
||||
// where an overlap that is absent, too small, or applied to the wrong end of the buffer goes wrong.
|
||||
func TestFindBundleHeaderOffset_MarkerAcrossWindowBoundary(t *testing.T) {
|
||||
const window = markerSearchWindowSize
|
||||
const headerOffset = 0x1234
|
||||
size := window + 4096
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
sigStart int
|
||||
}{
|
||||
{"whole marker inside the first window", window - 4096},
|
||||
{"header offset split across the boundary", window - 4},
|
||||
{"header offset ends exactly at the boundary", window},
|
||||
{"signature split one byte into the second window", window - len(dotNetBundleSignature) + 1},
|
||||
{"signature split down the middle", window - len(dotNetBundleSignature)/2},
|
||||
{"signature starts one byte before the boundary", window - 1},
|
||||
{"whole marker inside the second window", window + 2048},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data := fileWithSignatureAt(size, tt.sigStart, headerOffset)
|
||||
|
||||
got, err := findBundleHeaderOffset(readSeekCloser{bytes.NewReader(data)}, int64(size))
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(headerOffset), got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// recordingReader records the largest single read it is asked for. The search buffer is what bounds
|
||||
// that length, so this is what fails if the implementation ever sizes a buffer against the file again.
|
||||
type recordingReader struct {
|
||||
*bytes.Reader
|
||||
largestRead int
|
||||
}
|
||||
|
||||
func (r *recordingReader) ReadAt(p []byte, off int64) (int, error) {
|
||||
if len(p) > r.largestRead {
|
||||
r.largestRead = len(p)
|
||||
}
|
||||
return r.Reader.ReadAt(p, off)
|
||||
}
|
||||
|
||||
func (*recordingReader) Close() error { return nil }
|
||||
|
||||
// Holding an executable at its full size to look for 32 bytes is what made this the largest single
|
||||
// consumer of memory in a scan of a Windows volume, so the bound matters more than the result here.
|
||||
func TestFindBundleHeaderOffset_ReadsAreBoundedByTheWindow(t *testing.T) {
|
||||
size := 5 * markerSearchWindowSize
|
||||
data := fileWithSignatureAt(size, size-1024, 0x1234)
|
||||
r := &recordingReader{Reader: bytes.NewReader(data)}
|
||||
|
||||
got, err := findBundleHeaderOffset(r, int64(size))
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0x1234), got, "the marker must still be found at the far end of the file")
|
||||
assert.LessOrEqual(t, r.largestRead, markerSearchWindowSize,
|
||||
"no single read may exceed the search window, whatever the file's size")
|
||||
}
|
||||
|
||||
// Every window after the first begins with bytes already searched. Re-reporting a match from that
|
||||
// overlap would be harmless, but failing to advance past it would spin forever.
|
||||
func TestFindBundleHeaderOffset_UnmarkedFileTerminates(t *testing.T) {
|
||||
for _, size := range []int{
|
||||
markerSearchWindowSize - 1,
|
||||
markerSearchWindowSize,
|
||||
markerSearchWindowSize + 1,
|
||||
markerSearchWindowSize + markerSearchOverlap,
|
||||
3 * markerSearchWindowSize,
|
||||
} {
|
||||
got, err := findBundleHeaderOffset(readSeekCloser{bytes.NewReader(make([]byte, size))}, int64(size))
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Zero(t, got)
|
||||
}
|
||||
}
|
||||
|
||||
// lateShortReader reads cleanly until shortFrom and only then hands back fewer bytes than were asked for.
|
||||
// shortReader above comes up short on its very first read, which the search answers before it has slid at
|
||||
// all; the reader running out partway through a windowed search is a different branch.
|
||||
type lateShortReader struct {
|
||||
*bytes.Reader
|
||||
shortFrom int64 // a read starting here or beyond stops after shortLen bytes
|
||||
shortLen int
|
||||
}
|
||||
|
||||
func (r *lateShortReader) ReadAt(p []byte, off int64) (int, error) {
|
||||
if off < r.shortFrom || len(p) <= r.shortLen {
|
||||
return r.Reader.ReadAt(p, off)
|
||||
}
|
||||
|
||||
n, _ := r.Reader.ReadAt(p[:r.shortLen], off)
|
||||
return n, io.EOF
|
||||
}
|
||||
|
||||
func (*lateShortReader) Close() error { return nil }
|
||||
|
||||
// A squashfs block that decompresses short makes unionreader return fewer bytes than were asked for, and
|
||||
// nothing says that lands in the first window. Whatever the search did get still has to be searched, and the
|
||||
// bytes the reader never produced have to end it rather than being read as a file with no bundle in it.
|
||||
func TestFindBundleHeaderOffset_ShortReadAfterASlideSearchesWhatWasRead(t *testing.T) {
|
||||
const window = markerSearchWindowSize
|
||||
const shortLen = 4096 // what the second window gets before the reader gives up
|
||||
const headerOffset = 0x1234
|
||||
size := 2*window + 8192
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
sigStart int
|
||||
want int64
|
||||
}{
|
||||
{"marker inside the bytes the second window did get", window + 1024, headerOffset},
|
||||
{"marker straddling the boundary into those bytes", window - 4, headerOffset},
|
||||
{"marker beyond where the reader gave up", window + shortLen + 1024, 0},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// honest about its length: only the read past the first window comes up short
|
||||
r := &lateShortReader{
|
||||
Reader: bytes.NewReader(fileWithSignatureAt(size, tt.sigStart, headerOffset)),
|
||||
shortFrom: window,
|
||||
shortLen: shortLen,
|
||||
}
|
||||
|
||||
got, err := findBundleHeaderOffset(r, int64(size))
|
||||
|
||||
require.NoError(t, err, "a reader that runs out is not a parse failure")
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/anchore/syft/syft/source/stereoscopesource"
|
||||
@@ -199,6 +200,7 @@ func fixtureResolvers(t *testing.T, fixtures ...string) map[string]file.Resolver
|
||||
continue
|
||||
}
|
||||
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", fixture)
|
||||
|
||||
s := stereoscopesource.New(img, stereoscopesource.ImageConfig{
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/anchore/syft/internal/cmptest"
|
||||
"github.com/anchore/syft/internal/licenses"
|
||||
"github.com/anchore/syft/internal/relationship"
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
"github.com/anchore/syft/internal/tmpdir"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
@@ -150,9 +151,11 @@ func (p *CatalogTester) FromFile(t *testing.T, path string) *CatalogTester {
|
||||
|
||||
fixture, err := os.Open(path)
|
||||
require.NoError(t, err)
|
||||
// parsers don't own the reader (the generic cataloger closes it), so close it here
|
||||
t.Cleanup(func() { _ = fixture.Close() })
|
||||
|
||||
p.reader = file.LocationReadCloser{
|
||||
Location: file.NewVirtualLocationFromDirectory(fixture.Name(), fixture.Name(), *stereofile.NewFileReference(stereofile.Path(absPath))),
|
||||
Location: file.NewVirtualLocationFromDirectory(filepath.ToSlash(fixture.Name()), filepath.ToSlash(fixture.Name()), *stereofile.NewFileReference(stereofile.Path(absPath))),
|
||||
ReadCloser: fixture,
|
||||
}
|
||||
return p
|
||||
@@ -201,6 +204,7 @@ func (p *CatalogTester) WithImageResolver(t *testing.T, fixtureName string) *Cat
|
||||
return p
|
||||
}
|
||||
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", fixtureName)
|
||||
|
||||
s := stereoscopesource.New(img, stereoscopesource.ImageConfig{
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/license"
|
||||
@@ -1605,6 +1607,9 @@ func generateJavaBuildFixture(t *testing.T, fixturePath string) {
|
||||
return
|
||||
}
|
||||
|
||||
// the fixtures are built with maven/gradle inside linux containers
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
|
||||
makeTask := strings.TrimPrefix(fixturePath, "testdata/java-builds/")
|
||||
t.Log(color.Bold.Sprintf("Generating Fixture from 'make %s'", makeTask))
|
||||
|
||||
@@ -1630,7 +1635,8 @@ func generateJavaMetadataJarFixture(t *testing.T, fixtureName string, fileExtens
|
||||
return fixturePath
|
||||
}
|
||||
|
||||
makeTask := filepath.Join("cache", fixtureName+"."+fileExtension)
|
||||
// make targets are posix paths regardless of host
|
||||
makeTask := path.Join("cache", fixtureName+"."+fileExtension)
|
||||
t.Log(color.Bold.Sprintf("Generating Fixture from 'make %s'", makeTask))
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
|
||||
@@ -47,6 +47,8 @@ func Test_defaultMavenLocalRepoDir(t *testing.T) {
|
||||
home, err := filepath.Abs(test.home)
|
||||
require.NoError(t, err)
|
||||
t.Setenv("HOME", home)
|
||||
// os.UserHomeDir reads USERPROFILE on windows
|
||||
t.Setenv("USERPROFILE", home)
|
||||
}
|
||||
got := defaultMavenLocalRepoDir()
|
||||
require.Equal(t, test.expected, got)
|
||||
|
||||
@@ -31,7 +31,7 @@ func MockRepo(t *testing.T, dir string) (url string) {
|
||||
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
matches, err := doublestar.Glob(os.DirFS(dir), filepath.Join("**", "*.pom"))
|
||||
matches, err := doublestar.Glob(os.DirFS(dir), "**/*.pom")
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, match := range matches {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/internal/packagemetadata"
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
@@ -523,6 +524,7 @@ func TestCpanCatalogers_metadataTypes(t *testing.T) {
|
||||
func imageResolver(t *testing.T, fixture string) file.Resolver {
|
||||
t.Helper()
|
||||
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", fixture)
|
||||
resolver, err := stereoscopesource.New(img, stereoscopesource.ImageConfig{Reference: fixture}).
|
||||
FileResolver(source.SquashedScope)
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
@@ -53,7 +52,7 @@ func fetchInstalledFiles(resolver file.Resolver, metadataLocation file.Location,
|
||||
// to reconcile the installed-files.txt path to the same layer (or the next adjacent lower layer).
|
||||
|
||||
// find the installed-files.txt file relative to the directory where the METADATA file resides (in path AND layer structure)
|
||||
installedFilesPath := filepath.Join(filepath.Dir(metadataLocation.RealPath), "installed-files.txt")
|
||||
installedFilesPath := path.Join(path.Dir(metadataLocation.RealPath), "installed-files.txt")
|
||||
installedFilesRef := resolver.RelativeFileByPath(metadataLocation, installedFilesPath)
|
||||
|
||||
if installedFilesRef != nil {
|
||||
@@ -83,7 +82,7 @@ func fetchRecordFiles(resolver file.Resolver, metadataLocation file.Location) (f
|
||||
// to reconcile the RECORD path to the same layer (or the next adjacent lower layer).
|
||||
|
||||
// find the RECORD file relative to the directory where the METADATA file resides (in path AND layer structure)
|
||||
recordPath := filepath.Join(filepath.Dir(metadataLocation.RealPath), "RECORD")
|
||||
recordPath := path.Join(path.Dir(metadataLocation.RealPath), "RECORD")
|
||||
recordRef := resolver.RelativeFileByPath(metadataLocation, recordPath)
|
||||
|
||||
if recordRef != nil {
|
||||
@@ -107,8 +106,8 @@ func fetchRecordFiles(resolver file.Resolver, metadataLocation file.Location) (f
|
||||
// fetchTopLevelPackages finds a corresponding top_level.txt file for the given python package metadata file and returns the set of package names contained.
|
||||
func fetchTopLevelPackages(resolver file.Resolver, metadataLocation file.Location) (pkgs []string, sources []file.Location, err error) {
|
||||
// a top_level.txt file specifies the python top-level packages (provided by this python package) installed into site-packages
|
||||
parentDir := filepath.Dir(metadataLocation.RealPath)
|
||||
topLevelPath := filepath.Join(parentDir, "top_level.txt")
|
||||
parentDir := path.Dir(metadataLocation.RealPath)
|
||||
topLevelPath := path.Join(parentDir, "top_level.txt")
|
||||
topLevelLocation := resolver.RelativeFileByPath(metadataLocation, topLevelPath)
|
||||
|
||||
if topLevelLocation == nil {
|
||||
@@ -157,8 +156,8 @@ type vcsInfo struct {
|
||||
}
|
||||
|
||||
func fetchDirectURLData(resolver file.Resolver, metadataLocation file.Location) (d *pkg.PythonDirectURLOriginInfo, sources []file.Location, err error) {
|
||||
parentDir := filepath.Dir(metadataLocation.RealPath)
|
||||
directURLPath := filepath.Join(parentDir, "direct_url.json")
|
||||
parentDir := path.Dir(metadataLocation.RealPath)
|
||||
directURLPath := path.Join(parentDir, "direct_url.json")
|
||||
directURLLocation := resolver.RelativeFileByPath(metadataLocation, directURLPath)
|
||||
|
||||
if directURLLocation == nil {
|
||||
@@ -269,7 +268,7 @@ func findLicenses(ctx context.Context, resolver file.Resolver, m parsedData) pkg
|
||||
continue
|
||||
}
|
||||
|
||||
if licenses.IsLicenseFile(filepath.Base(f.Path)) {
|
||||
if licenses.IsLicenseFile(path.Base(f.Path)) {
|
||||
candidatePaths.Add(path.Join(m.SitePackagesRootPath, f.Path))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package python
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
@@ -48,13 +48,13 @@ func parseWheelOrEggMetadata(locationReader file.LocationReadCloser) (parsedData
|
||||
}
|
||||
|
||||
// add additional metadata not stored in the egg/wheel metadata file
|
||||
path := locationReader.Path()
|
||||
metadataPath := locationReader.Path()
|
||||
|
||||
pd.SitePackagesRootPath = determineSitePackagesRootPath(path)
|
||||
pd.SitePackagesRootPath = determineSitePackagesRootPath(metadataPath)
|
||||
if pd.Licenses != "" || pd.LicenseExpression != "" {
|
||||
pd.LicenseFilePath = path
|
||||
pd.LicenseFilePath = metadataPath
|
||||
} else if pd.LicenseFile != "" {
|
||||
pd.LicenseFilePath = filepath.Join(filepath.Dir(path), pd.LicenseFile)
|
||||
pd.LicenseFilePath = path.Join(path.Dir(metadataPath), pd.LicenseFile)
|
||||
}
|
||||
|
||||
pd.DistInfoLocation = locationReader.Location
|
||||
@@ -144,18 +144,18 @@ func getFieldType(key, in string) any {
|
||||
// isEggRegularFile determines if the specified path is the regular file variant
|
||||
// of egg metadata (as opposed to a directory that contains more metadata
|
||||
// files).
|
||||
func isEggRegularFile(path string) bool {
|
||||
return doublestar.MatchUnvalidated(eggInfoGlob, path)
|
||||
func isEggRegularFile(p string) bool {
|
||||
return doublestar.MatchUnvalidated(eggInfoGlob, p)
|
||||
}
|
||||
|
||||
// determineSitePackagesRootPath returns the path of the site packages root,
|
||||
// given the egg metadata file or directory specified in the path.
|
||||
func determineSitePackagesRootPath(path string) string {
|
||||
if isEggRegularFile(path) {
|
||||
return filepath.Clean(filepath.Dir(path))
|
||||
func determineSitePackagesRootPath(p string) string {
|
||||
if isEggRegularFile(p) {
|
||||
return path.Clean(path.Dir(p))
|
||||
}
|
||||
|
||||
return filepath.Clean(filepath.Dir(filepath.Dir(path)))
|
||||
return path.Clean(path.Dir(path.Dir(p)))
|
||||
}
|
||||
|
||||
// handleFieldBodyContinuation returns the updated value for the specified field after processing the specified line.
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -87,11 +88,13 @@ func parseInstalledFiles(reader io.Reader, location, sitePackagesRootPath string
|
||||
}
|
||||
|
||||
if location != "" && sitePackagesRootPath != "" {
|
||||
joinedPath := filepath.Join(filepath.Dir(location), line)
|
||||
joinedPath := path.Join(path.Dir(location), line)
|
||||
// the path package has no Rel, and filepath.Rel hands back host separators on windows
|
||||
line, err = filepath.Rel(sitePackagesRootPath, joinedPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
line = filepath.ToSlash(line)
|
||||
}
|
||||
|
||||
installedFile := pkg.PythonFileRecord{
|
||||
|
||||
@@ -122,8 +122,7 @@ func TestParsePackageResolved_empty(t *testing.T) {
|
||||
|
||||
dir := t.TempDir()
|
||||
fixture = filepath.Join(dir, "Package.resolved")
|
||||
_, err := os.Create(fixture)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(fixture, nil, 0600))
|
||||
|
||||
pkgtest.TestFileParser(t, fixture, parsePackageResolved, nil, nil)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ import (
|
||||
)
|
||||
|
||||
func TestNewFromLocal(t *testing.T) {
|
||||
// the request is resolved with filepath.Abs, which adds a volume on windows
|
||||
localSnap, err := filepath.Abs("/test/local.snap")
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg Config
|
||||
@@ -40,13 +44,13 @@ func TestNewFromLocal(t *testing.T) {
|
||||
{
|
||||
name: "local file exists",
|
||||
cfg: Config{
|
||||
Request: "/test/local.snap",
|
||||
Request: localSnap,
|
||||
DigestAlgorithms: []crypto.Hash{crypto.SHA256},
|
||||
},
|
||||
setup: func(fs afero.Fs) {
|
||||
require.NoError(t, createMockSquashfsFile(fs, "/test/local.snap"))
|
||||
require.NoError(t, createMockSquashfsFile(fs, localSnap))
|
||||
},
|
||||
wantRequest: "/test/local.snap",
|
||||
wantRequest: localSnap,
|
||||
},
|
||||
{
|
||||
name: "resolve home dir exists",
|
||||
@@ -65,18 +69,18 @@ func TestNewFromLocal(t *testing.T) {
|
||||
{
|
||||
name: "local file with architecture specified",
|
||||
cfg: Config{
|
||||
Request: "/test/local.snap",
|
||||
Request: localSnap,
|
||||
Platform: &image.Platform{
|
||||
Architecture: "arm64",
|
||||
},
|
||||
},
|
||||
setup: func(fs afero.Fs) {
|
||||
require.NoError(t, createMockSquashfsFile(fs, "/test/local.snap"))
|
||||
require.NoError(t, createMockSquashfsFile(fs, localSnap))
|
||||
},
|
||||
wantErr: func(t assert.TestingT, err error, msgAndArgs ...any) bool {
|
||||
return assert.Error(t, err, msgAndArgs...) && assert.Contains(t, err.Error(), "architecture cannot be specified for local snap files", msgAndArgs...)
|
||||
},
|
||||
wantRequest: "/test/local.snap",
|
||||
wantRequest: localSnap,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -288,6 +292,8 @@ func writeSquashfs(t *testing.T, manifest string) string {
|
||||
require.NoError(t, err)
|
||||
_, err = w.Write([]byte(contents))
|
||||
require.NoError(t, err)
|
||||
// close before finalizing: windows directory listings can report a stale (zero) size for open files
|
||||
require.NoError(t, w.Close())
|
||||
}
|
||||
|
||||
// note: go-diskfs wants paths relative to the image root, without a leading slash
|
||||
|
||||
@@ -145,7 +145,9 @@ func TestNewSnapFromFile(t *testing.T) {
|
||||
DigestAlgorithms: []crypto.Hash{crypto.SHA256},
|
||||
},
|
||||
setup: func() string {
|
||||
path := "/test/valid.snap"
|
||||
// newSnapFromFile resolves the request with filepath.Abs, which adds a volume on windows
|
||||
path, err := filepath.Abs("/test/valid.snap")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, createMockSquashfsFile(fs, path))
|
||||
return path
|
||||
},
|
||||
@@ -179,7 +181,9 @@ func TestNewSnapFromFile(t *testing.T) {
|
||||
name: "path is directory",
|
||||
cfg: Config{},
|
||||
setup: func() string {
|
||||
path := "/test/directory"
|
||||
// newSnapFromFile resolves the request with filepath.Abs, which adds a volume on windows
|
||||
path, err := filepath.Abs("/test/directory")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, fs.MkdirAll(path, 0755))
|
||||
return path
|
||||
},
|
||||
@@ -471,7 +475,9 @@ func TestGetSnapFileInfo(t *testing.T) {
|
||||
{
|
||||
name: "path is directory",
|
||||
setup: func() string {
|
||||
path := "/test/directory"
|
||||
// newSnapFromFile resolves the request with filepath.Abs, which adds a volume on windows
|
||||
path, err := filepath.Abs("/test/directory")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, fs.MkdirAll(path, 0755))
|
||||
return path
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/internal/testutils"
|
||||
"github.com/anchore/syft/syft/internal/testutil"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
@@ -78,6 +79,7 @@ func Test_StereoscopeImage_Exclusions(t *testing.T) {
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
testutils.SkipWithoutLinuxContainers(t)
|
||||
imageName := strings.SplitN(imagetest.PrepareFixtureImage(t, "docker-archive", test.input), ":", 2)[1]
|
||||
|
||||
img, err := stereoscope.GetImage(context.TODO(), imageName)
|
||||
|
||||
Executable
+138
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bash
|
||||
# build an image with podman or buildah into an isolated containers-storage store, then verify syft can scan it out
|
||||
# of that store (for both the vfs and overlay drivers) and that plain image references never touch the store.
|
||||
#
|
||||
# usage: SYFT_BINARY_LOCATION=/path/to/syft source-test.sh podman|buildah
|
||||
set -euo pipefail
|
||||
|
||||
builder="${1:?expected podman or buildah}"
|
||||
case "$builder" in
|
||||
podman|buildah) ;;
|
||||
*)
|
||||
printf 'unsupported image builder: %s\n' "$builder" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
syft_binary="${SYFT_BINARY_LOCATION:?expected SYFT_BINARY_LOCATION to point at a linux syft binary built with containers-storage support}"
|
||||
if [[ ! -f "$syft_binary" ]]; then
|
||||
printf 'syft binary not found at %s\n' "$syft_binary" >&2
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$syft_binary"
|
||||
|
||||
test_dir="$(mktemp -d)"
|
||||
# the overlay driver bind-mounts its graphroot onto itself (in the builder's user namespace when rootless), which must
|
||||
# be unmounted before the store can be removed. Cleanup is best-effort so it never masks the test result.
|
||||
cleanup_store() {
|
||||
umount -l "$1"/*/graphroot/overlay 2>/dev/null || true
|
||||
rm -rf "$1"
|
||||
}
|
||||
cleanup() {
|
||||
if [[ "$(id -u)" -eq 0 ]]; then
|
||||
cleanup_store "$test_dir"
|
||||
else
|
||||
"$builder" unshare bash -c "$(declare -f cleanup_store); cleanup_store \"\$1\"" _ "$test_dir"
|
||||
fi || printf 'warning: unable to fully clean up %s\n' "$test_dir" >&2
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# a rootless store can only be opened from inside the builder's user namespace (as podman/buildah/skopeo do for
|
||||
# themselves), so rootless users must run syft under "<builder> unshare". Root opens the store directly.
|
||||
run_syft() {
|
||||
if [[ "$(id -u)" -eq 0 ]]; then
|
||||
"$syft_binary" "$@"
|
||||
else
|
||||
"$builder" unshare "$syft_binary" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
build_context="$test_dir/context"
|
||||
mkdir -p "$build_context"
|
||||
cat > "$build_context/package.json" <<'EOF'
|
||||
{"name":"syft-cs-fixture","version":"1.2.3"}
|
||||
EOF
|
||||
cat > "$build_context/Containerfile" <<'EOF'
|
||||
FROM scratch
|
||||
COPY package.json /app/node_modules/syft-cs-fixture/package.json
|
||||
EOF
|
||||
|
||||
# note: the image name deliberately avoids the string "containers-storage" so stderr greps below are unambiguous
|
||||
image_ref="localhost/syft-cs-fixture:latest"
|
||||
missing_ref="localhost/syft-cs-missing:latest"
|
||||
|
||||
for driver in vfs overlay; do
|
||||
store_dir="$test_dir/$driver"
|
||||
mkdir -p "$store_dir"
|
||||
export CONTAINERS_STORAGE_CONF="$store_dir/storage.conf"
|
||||
cat > "$CONTAINERS_STORAGE_CONF" <<EOF
|
||||
[storage]
|
||||
driver = "$driver"
|
||||
graphroot = "$store_dir/graphroot"
|
||||
rootless_storage_path = "$store_dir/graphroot"
|
||||
runroot = "$store_dir/runroot"
|
||||
EOF
|
||||
|
||||
printf 'Building %s with %s (%s driver)\n' "$image_ref" "$builder" "$driver"
|
||||
case "$builder" in
|
||||
podman)
|
||||
podman build --pull=never --tag "$image_ref" "$build_context"
|
||||
;;
|
||||
buildah)
|
||||
buildah build --pull=false --tag "$image_ref" "$build_context"
|
||||
;;
|
||||
esac
|
||||
|
||||
for scan_mode in from-flag scheme default-pull-source; do
|
||||
printf 'Testing %s resolution with %s (%s driver)\n' "$scan_mode" "$builder" "$driver"
|
||||
out="$store_dir/$scan_mode"
|
||||
case "$scan_mode" in
|
||||
from-flag)
|
||||
run_syft -vv --from containers-storage "$image_ref" --output json > "$out.json" 2> "$out.stderr"
|
||||
;;
|
||||
scheme)
|
||||
run_syft -vv "containers-storage:$image_ref" --output json > "$out.json" 2> "$out.stderr"
|
||||
;;
|
||||
default-pull-source)
|
||||
SYFT_SOURCE_IMAGE_DEFAULT_PULL_SOURCE=containers-storage \
|
||||
run_syft -vv "$image_ref" --output json > "$out.json" 2> "$out.stderr"
|
||||
;;
|
||||
esac
|
||||
|
||||
# prove the image came out of the store, not from some other provider that happened to answer
|
||||
if ! grep -q 'copied image from containers-storage' "$out.stderr"; then
|
||||
printf 'Expected %s scan to resolve via containers-storage\n' "$scan_mode" >&2
|
||||
cat "$out.stderr" >&2
|
||||
exit 1
|
||||
fi
|
||||
jq -e --arg image_ref "$image_ref" '
|
||||
.source.type == "image" and
|
||||
.source.metadata.userInput == $image_ref and
|
||||
any(.artifacts[]; .name == "syft-cs-fixture" and .version == "1.2.3" and .type == "npm")
|
||||
' "$out.json" >/dev/null
|
||||
done
|
||||
|
||||
printf 'Testing plain reference skips containers-storage with %s (%s driver)\n' "$builder" "$driver"
|
||||
# the image only exists in the local store, so automatic resolution (daemons, then registry) must fail without
|
||||
# ever trying containers-storage
|
||||
if run_syft "$image_ref" --output json > "$store_dir/plain.json" 2> "$store_dir/plain.stderr"; then
|
||||
printf 'Expected plain reference scan to fail since the image only exists in containers-storage\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -q 'containers-storage' "$store_dir/plain.stderr"; then
|
||||
printf 'Plain reference unexpectedly attempted containers-storage resolution\n' >&2
|
||||
cat "$store_dir/plain.stderr" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Testing missing image with %s (%s driver)\n' "$builder" "$driver"
|
||||
if run_syft --from containers-storage "$missing_ref" --output json > "$store_dir/missing.json" 2> "$store_dir/missing.stderr"; then
|
||||
printf 'Expected the missing containers-storage image scan to fail\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q 'does not resolve to an image ID' "$store_dir/missing.stderr"; then
|
||||
printf 'Expected a containers-storage "image not found" error\n' >&2
|
||||
cat "$store_dir/missing.stderr" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
@@ -43,3 +43,9 @@ run_test_case test_download_release_asset "${release}" "linux" "amd64" "deb" "ap
|
||||
run_test_case test_download_release_asset "${release}" "linux" "arm64" "tar.gz" "application/gzip"
|
||||
run_test_case test_download_release_asset "${release}" "linux" "arm64" "rpm" "application/x-rpm"
|
||||
run_test_case test_download_release_asset "${release}" "linux" "arm64" "deb" "application/vnd.debian.binary-package"
|
||||
|
||||
# releases before VERIFY_SIGN_BUNDLE_VERSION are signed with a separate .sig and .pem instead of a sigstore bundle,
|
||||
# so keep exercising that path against a fixed release
|
||||
legacy_release="v1.52.0"
|
||||
|
||||
run_test_case test_download_release_asset "${legacy_release}" "linux" "amd64" "tar.gz" "application/gzip"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user