mirror of
https://github.com/anchore/syft.git
synced 2026-10-17 00:27:21 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02ba369d13 | ||
|
|
74073aa481 | ||
|
|
0b234b3256 | ||
|
|
b4ee6e4696 | ||
|
|
2be26a1af9 | ||
|
|
30e9e65608 | ||
|
|
e6d364437a | ||
|
|
c55e1cf2fd | ||
|
|
356e601cd5 | ||
|
|
ee05a595b6 | ||
|
|
05c6817c07 | ||
|
|
d85c72a08e | ||
|
|
1ca1fa73d3 | ||
|
|
a675c81143 | ||
|
|
875daec82b | ||
|
|
fb59c4e65c | ||
|
|
c4800b2393 | ||
|
|
eb58f698bd | ||
|
|
de98400c39 | ||
|
|
9e520d1442 | ||
|
|
3ec2e33d73 | ||
|
|
40a4ec72b9 | ||
|
|
268897ca82 | ||
|
|
031de8ddab | ||
|
|
1d25dfe4da | ||
|
|
0c049806bd | ||
|
|
9addda3da5 | ||
|
|
767c83d513 | ||
|
|
392539dc28 | ||
|
|
3dd674c53b | ||
|
|
fd7517bbba | ||
|
|
c935b71c9e | ||
|
|
77031752fa | ||
|
|
12d5544b2a | ||
|
|
cc13d1dbf1 | ||
|
|
af937d74c0 | ||
|
|
4017979670 | ||
|
|
82d273fac5 | ||
|
|
1417c1989b | ||
|
|
91a0032987 | ||
|
|
f91bf4528b | ||
|
|
c5fc6998d3 | ||
|
|
d3734ddeb2 | ||
|
|
bf82010f3c | ||
|
|
7ca1f22395 | ||
|
|
93cf89363b | ||
|
|
34ef7dcbfe | ||
|
|
29edf90038 | ||
|
|
766907e6ed | ||
|
|
e820f2d21a | ||
|
|
39c9c690df | ||
|
|
c4597a08a2 | ||
|
|
236755a6b1 | ||
|
|
168eb64235 | ||
|
|
490732aa58 | ||
|
|
fd4796b0d8 | ||
|
|
78f39ee3a5 | ||
|
|
5c6cf08a8c | ||
|
|
8bb3b5eed1 | ||
|
|
21cca30e5b | ||
|
|
ed76e96749 | ||
|
|
ab508169e6 | ||
|
|
360dbc04aa | ||
|
|
58a033f924 | ||
|
|
04a6fa1b41 | ||
|
|
d63c774fa9 | ||
|
|
3eee102f63 | ||
|
|
20cc3df387 | ||
|
|
4782a94029 | ||
|
|
2293641e3b | ||
|
|
9dd9ce0bdd | ||
|
|
949ac70369 | ||
|
|
07fb23487f | ||
|
|
f45586b457 | ||
|
|
0fe98ed1f2 | ||
|
|
5f67883fe8 | ||
|
|
d288dd67ae | ||
|
|
da745b13e8 | ||
|
|
68da404bd7 | ||
|
|
35bf33bcf0 | ||
|
|
7f73d6a603 | ||
|
|
ef40eec38d | ||
|
|
80212d87c2 | ||
|
|
b14123ecc1 | ||
|
|
e00aeb16e4 | ||
|
|
fa6f6d70f0 | ||
|
|
0975d80dc0 | ||
|
|
4d823eaaa7 | ||
|
|
138e42b428 | ||
|
|
ecd8fb7740 | ||
|
|
dd639c09b2 | ||
|
|
31a352d030 | ||
|
|
08e913a2ce | ||
|
|
5ef5b1badc | ||
|
|
b236f8c1f7 | ||
|
|
476ce60768 | ||
|
|
16223e6dd7 | ||
|
|
1286689419 | ||
|
|
295454945c | ||
|
|
86baeeb481 | ||
|
|
2dcf5163b8 | ||
|
|
12b8ba47fb | ||
|
|
138d9ce2a0 | ||
|
|
8a229b16c4 | ||
|
|
9af0098a68 | ||
|
|
2840ea6653 | ||
|
|
b38e40e3e6 | ||
|
|
a6eedecd31 | ||
|
|
691b1357b2 | ||
|
|
c890e7f17f | ||
|
|
29fd7d0dec | ||
|
|
289137ff3c | ||
|
|
22ffd5209d | ||
|
|
a0852ebf98 | ||
|
|
d380ad08b5 | ||
|
|
ae9534203d | ||
|
|
dc1ef98237 | ||
|
|
36a3af41d9 | ||
|
|
3e2bc6ed09 |
+2
-2
@@ -16,7 +16,7 @@ tools:
|
||||
# used to upload test fixture cache
|
||||
- name: oras
|
||||
version:
|
||||
want: v1.3.2
|
||||
want: v1.3.3
|
||||
method: github-release
|
||||
with:
|
||||
repo: oras-project/oras
|
||||
@@ -24,7 +24,7 @@ tools:
|
||||
# used to parse JSON/YAML annotations on the fixture cache image
|
||||
- name: yq
|
||||
version:
|
||||
want: v4.53.3
|
||||
want: v4.53.6
|
||||
method: github-release
|
||||
with:
|
||||
repo: mikefarah/yq
|
||||
|
||||
@@ -47,7 +47,17 @@ runs:
|
||||
APT_PACKAGES: ${{ inputs.bootstrap-apt-packages }}
|
||||
run: |
|
||||
IFS=' ' read -ra packages <<< "$APT_PACKAGES"
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y "${packages[@]}"
|
||||
# the AMI already ships most of these, and `apt update` costs ~40MB of index (minutes,
|
||||
# when archive.ubuntu.com throttles us) to find that out. only touch apt if we must.
|
||||
missing=()
|
||||
for pkg in "${packages[@]}"; do
|
||||
dpkg -s "$pkg" >/dev/null 2>&1 || missing+=("$pkg")
|
||||
done
|
||||
if [ ${#missing[@]} -eq 0 ]; then
|
||||
echo "all apt packages already present: ${packages[*]}"
|
||||
exit 0
|
||||
fi
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y "${missing[@]}"
|
||||
|
||||
# ORAS cache: restore-only on non-default branches / forks
|
||||
- name: Restore ORAS cache from github actions
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
if: ${{ github.event.inputs.phase == 'all' }}
|
||||
permissions:
|
||||
contents: read # required for fetching tags
|
||||
uses: anchore/workflows/.github/workflows/check-version-available.yaml@b0c30a80409130d329aaa356fd64a34d8c0b3375 # v0.7.2
|
||||
uses: anchore/workflows/.github/workflows/check-version-available.yaml@7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4 # v0.8.0
|
||||
with:
|
||||
version: ${{ github.event.inputs.version }}
|
||||
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
packages: write # required for publishing release artifacts to GitHub packages
|
||||
id-token: write # required for keyless signing (cosign/sigstore OIDC)
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
@@ -83,14 +83,19 @@ jobs:
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
# dockers_v2 builds multi-platform manifests in a single buildx invocation, which requires a
|
||||
# docker-container driver builder (the default builder on the runner cannot build multi-platform).
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 #v4.4.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f #v4.6.0
|
||||
with:
|
||||
username: ${{ secrets.ANCHOREOSSWRITE_DH_USERNAME }}
|
||||
password: ${{ secrets.ANCHOREOSSWRITE_DH_PAT }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 #v4.4.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f #v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -113,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@e22c389904149dbc22b58101806040fa8d37a610 #v0.24.0
|
||||
- uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 #v0.24.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
file: go.mod
|
||||
|
||||
@@ -20,12 +20,12 @@ jobs:
|
||||
contents: read
|
||||
security-events: write # for uploading SARIF results
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run zizmor"
|
||||
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
|
||||
uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3
|
||||
with:
|
||||
# there is a pass/fail gate as a repo ruleset (if there is no ruleset configured then the action will pass by default)
|
||||
advanced-security: true
|
||||
|
||||
@@ -21,14 +21,17 @@ jobs:
|
||||
name: "Static analysis"
|
||||
# runs-on.com: memory & general purpose instances for testing
|
||||
# spot enabled: ok to interrupt non-production workloads
|
||||
# tmpfs: faster io-intensive workflows
|
||||
# tmpfs intentionally omitted -- it RAM-backs /home/runner, /tmp and /var/lib/docker at
|
||||
# 100% of instance memory, starving `make unit` of headroom to link with -race.
|
||||
# volume: the caches and docker images tmpfs used to hold. gp3 baseline throughput is
|
||||
# enough; a 4 vCPU instance can't pull much more than that anyway.
|
||||
# note: s3-cache intentionally omitted -- PR runs are untrusted and must not write to the
|
||||
# shared cache backend that the trusted release workflow reads from (cache poisoning).
|
||||
runs-on: &test-runner "runs-on=${{ github.run_id }}/cpu=4+8/ram=32+128/family=r5+r6+r7+r8+m4+m5+m6+m7+m8/spot=price-capacity-optimized/extras=tmpfs"
|
||||
runs-on: &test-runner "runs-on=${{ github.run_id }}/cpu=4+8/ram=16+32/family=m5+m6+m7+m8+c5+c6+c7+c8/spot=price-capacity-optimized/volume=100gb"
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -50,7 +53,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -72,7 +75,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -105,7 +108,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -135,7 +138,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -179,7 +182,7 @@ jobs:
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -213,7 +216,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ bin/
|
||||
/.tool
|
||||
/.task
|
||||
/generate
|
||||
/spec
|
||||
/specs
|
||||
mise.toml
|
||||
.make/.make
|
||||
|
||||
+52
-9
@@ -3,8 +3,10 @@ package main
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
. "github.com/anchore/go-make"
|
||||
"github.com/anchore/go-make/config"
|
||||
"github.com/anchore/go-make/file"
|
||||
"github.com/anchore/go-make/lang"
|
||||
"github.com/anchore/go-make/run"
|
||||
@@ -27,22 +29,43 @@ func main() {
|
||||
gotest.Name("unit"),
|
||||
gotest.ExcludeGlob("**/test/**"),
|
||||
gotest.CoverageThreshold(62),
|
||||
race(),
|
||||
),
|
||||
|
||||
// integration tests: native go-make Task. The race-detector smoke against a
|
||||
// real image stays bundled here (RunsOn integration) so `make integration`
|
||||
// behaves like the Taskfile version did.
|
||||
gotest.Tasks(
|
||||
gotest.Name("integration"),
|
||||
gotest.IncludeGlob("./cmd/syft/internal/test/integration/..."),
|
||||
gotest.Verbose(),
|
||||
gotest.NoCoverage(),
|
||||
),
|
||||
// integration tests: run `go test` directly instead of via gotest.Tasks(), which
|
||||
// has no way to set a timeout. The suite is a single package of ~36 sequential
|
||||
// tests over 18 docker fixture images, so with a cold fixture cache (every image
|
||||
// built + saved inline, under -race) it runs well past `go test`'s default 10m
|
||||
// timeout -- which is exactly the case when regenerating the fixture cache from
|
||||
// scratch. -count=1 keeps the go test cache from short-circuiting a run whose
|
||||
// side effect (the built fixtures) is the thing we're after.
|
||||
//
|
||||
// The race-detector smoke against a real image stays bundled here (RunsOn
|
||||
// integration) so `make integration` behaves like the Taskfile version did.
|
||||
Task{
|
||||
Name: "integration",
|
||||
Description: "run integration tests",
|
||||
RunsOn: lang.List("test"),
|
||||
Run: func() {
|
||||
raceFlag := ""
|
||||
if raceEnabled() {
|
||||
raceFlag = " -race"
|
||||
}
|
||||
Run(
|
||||
"go test -count=1 -timeout=30m -v"+raceFlag+" ./cmd/syft/internal/test/integration/...",
|
||||
run.Env("GODEBUG", "dontfreezetheworld=1"),
|
||||
)
|
||||
},
|
||||
},
|
||||
Task{
|
||||
Name: "integration:race-smoke",
|
||||
Description: "exercise the CLI with the race detector",
|
||||
RunsOn: lang.List("integration"),
|
||||
Run: func() {
|
||||
if !raceEnabled() {
|
||||
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")
|
||||
},
|
||||
},
|
||||
@@ -132,6 +155,26 @@ func main() {
|
||||
)
|
||||
}
|
||||
|
||||
// raceEnabled is the single switch for the race detector across every test suite.
|
||||
// Unset it and we keep go-make's behavior (on in CI, off locally and on windows);
|
||||
// set RACE=false to turn it off everywhere -- worth doing when rebuilding the test
|
||||
// fixture cache from scratch, where every suite is dominated by building docker
|
||||
// fixtures and the race detector only adds wall clock. RACE=true forces it on.
|
||||
func raceEnabled() bool {
|
||||
if enabled, err := strconv.ParseBool(config.Env("RACE", "")); err == nil {
|
||||
return enabled
|
||||
}
|
||||
return config.CI && !config.Windows
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return func(c *gotest.Config) {
|
||||
c.Race = raceEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
// snapshotBinPath replicates the SNAPSHOT_BIN computation from the prior Taskfile:
|
||||
// <repoRoot>/snapshot/<os>-build_<os>_<arch>/syft, where arch maps amd64->amd64_v1
|
||||
// and arm64->arm64_v8.0 to match goreleaser's per-target output directory naming.
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/go-make/config"
|
||||
"github.com/anchore/go-make/tasks/gotest"
|
||||
)
|
||||
|
||||
func TestRaceEnabled(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env string // "" means RACE is unset
|
||||
ci bool
|
||||
want bool
|
||||
}{
|
||||
{name: "default in CI", ci: true, want: true},
|
||||
{name: "default locally", ci: false, want: false},
|
||||
{name: "explicitly off in CI", env: "false", ci: true, want: false},
|
||||
{name: "explicitly on locally", env: "true", ci: false, want: true},
|
||||
{name: "unparseable falls back to default", env: "yes-please", ci: true, want: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// always go through t.Setenv (it registers the restore) so an ambient
|
||||
// RACE from the caller's environment can't leak into the unset cases
|
||||
t.Setenv("RACE", tt.env)
|
||||
if tt.env == "" {
|
||||
os.Unsetenv("RACE")
|
||||
}
|
||||
orig := config.CI
|
||||
config.CI = tt.ci
|
||||
t.Cleanup(func() { config.CI = orig })
|
||||
|
||||
if got := raceEnabled(); got != tt.want {
|
||||
t.Errorf("raceEnabled() = %v, want %v", got, tt.want)
|
||||
}
|
||||
|
||||
// the gotest suites get the same answer through the functional option
|
||||
var cfg gotest.Config
|
||||
race()(&cfg)
|
||||
if cfg.Race != tt.want {
|
||||
t.Errorf("race() set Race = %v, want %v", cfg.Race, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/anchore/syft/actions/workflows/validations.yaml" target="_blank"><img alt="Validations" src="https://github.com/anchore/syft/actions/workflows/validations.yaml/badge.svg"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/anchore/syft" target="_blank"><img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/anchore/syft"></a>
|
||||
<a href="https://github.com/anchore/syft/releases/latest" target="_blank"><img alt="GitHub release" src="https://img.shields.io/github/release/anchore/syft.svg"></a>
|
||||
<a href="https://github.com/anchore/syft" target="_blank"><img alt="GitHub go.mod Go version" src="https://img.shields.io/github/go-mod/go-version/anchore/syft.svg"></a>
|
||||
<a href="https://github.com/anchore/syft/blob/main/LICENSE" target="_blank"><img alt="License: Apache-2.0" src="https://img.shields.io/badge/License-Apache%202.0-blue.svg"></a>
|
||||
|
||||
@@ -125,6 +125,7 @@ tasks:
|
||||
desc: Run tests for pipeline utils
|
||||
cmds:
|
||||
- cmd: .github/scripts/labeler_test.py
|
||||
- cmd: go test -C .make ./...
|
||||
|
||||
snapshot-smoke-test:
|
||||
desc: Run a smoke test on the snapshot builds + docker images
|
||||
|
||||
@@ -1,24 +1,34 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/go-homedir"
|
||||
"github.com/anchore/syft/cmd/syft/internal/options"
|
||||
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/spillbuf"
|
||||
"github.com/anchore/syft/internal/tmpdir"
|
||||
"github.com/anchore/syft/syft/format"
|
||||
"github.com/anchore/syft/syft/format/syftjson"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
const (
|
||||
convertExample = ` {{.appName}} {{.command}} img.syft.json -o spdx-json convert a syft SBOM to spdx-json, output goes to stdout
|
||||
{{.appName}} {{.command}} img.syft.json -o cyclonedx-json=img.cdx.json convert a syft SBOM to CycloneDX, output is written to the file "img.cdx.json"
|
||||
{{.appName}} {{.command}} - -o spdx-json convert an SBOM from STDIN to spdx-json
|
||||
SYFT_CONVERT_PASSTHROUGH_EXACT_FORMAT=true {{.appName}} {{.command}} img.syft.json -o syft-json=out.syft.json copy the input to "out.syft.json" unchanged if it is already syft-json at the current schema version
|
||||
`
|
||||
)
|
||||
|
||||
@@ -26,6 +36,7 @@ type ConvertOptions struct {
|
||||
options.Config `yaml:",inline" mapstructure:",squash"`
|
||||
options.Output `yaml:",inline" mapstructure:",squash"`
|
||||
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
|
||||
Convert options.Convert `yaml:"convert" json:"convert" mapstructure:"convert"`
|
||||
}
|
||||
|
||||
func Convert(app clio.Application) *cobra.Command {
|
||||
@@ -34,6 +45,7 @@ func Convert(app clio.Application) *cobra.Command {
|
||||
opts := &ConvertOptions{
|
||||
UpdateCheck: options.DefaultUpdateCheck(),
|
||||
Output: options.DefaultOutput(),
|
||||
Convert: options.DefaultConvert(),
|
||||
}
|
||||
|
||||
return app.SetupCommand(&cobra.Command{
|
||||
@@ -46,11 +58,11 @@ func Convert(app clio.Application) *cobra.Command {
|
||||
}),
|
||||
Args: validateConvertArgs,
|
||||
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
restoreStdout := ui.CaptureStdoutToTraceLog()
|
||||
defer restoreStdout()
|
||||
|
||||
return RunConvert(opts, args[0])
|
||||
return RunConvert(cmd.Context(), opts, args[0])
|
||||
},
|
||||
}, opts)
|
||||
}
|
||||
@@ -59,30 +71,63 @@ func validateConvertArgs(cmd *cobra.Command, args []string) error {
|
||||
return validateArgs(cmd, args, "an SBOM argument is required")
|
||||
}
|
||||
|
||||
func RunConvert(opts *ConvertOptions, userInput string) error {
|
||||
func RunConvert(ctx context.Context, opts *ConvertOptions, userInput string) error {
|
||||
log.Warn("convert is an experimental feature, run `syft convert -h` for help")
|
||||
|
||||
writer, err := opts.SBOMWriter()
|
||||
// a temp dir is only needed when STDIN is a pipe and has to be spilled to disk; it is created lazily on
|
||||
// first use (under the user's TMPDIR), so a file input never touches it. Mirrors syft.CreateSBOM, which
|
||||
// also honors a TempDir already attached to the context.
|
||||
td := tmpdir.FromContext(ctx)
|
||||
if td == nil {
|
||||
_, td = tmpdir.Root(ctx, "syft-convert")
|
||||
defer func() {
|
||||
_ = td.Cleanup()
|
||||
}()
|
||||
}
|
||||
|
||||
reader, inputInfo, err := openConvertInput(userInput, td)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = reader.Close()
|
||||
}()
|
||||
|
||||
var reader io.ReadSeekCloser
|
||||
outputs := opts.Outputs
|
||||
var unchanged []string
|
||||
if opts.Convert.PassthroughExactFormat {
|
||||
log.Warn("convert.passthrough-exact-format is enabled: syft-json input that already exactly matches a requested output format and schema version will be copied to that output unchanged (this does not apply to any other input format)")
|
||||
|
||||
if userInput == "-" {
|
||||
// though os.Stdin is an os.File, it does not support seeking
|
||||
// you will get errors such as "seek /dev/stdin: illegal seek".
|
||||
// We need to buffer what we read.
|
||||
reader = internal.NewBufferedSeeker(os.Stdin)
|
||||
} else {
|
||||
f, err := os.Open(userInput)
|
||||
unchanged, outputs, err = partitionOutputsBySourceFormat(opts.Output, reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open SBOM file: %w", err)
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
reader = f
|
||||
}
|
||||
|
||||
// resolve (and validate) every output that needs converting before writing anything, so that
|
||||
// a bad -o value fails the command without leaving partial results behind
|
||||
var writer sbom.Writer
|
||||
if len(outputs) > 0 {
|
||||
outputOpts := opts.Output
|
||||
outputOpts.Outputs = outputs
|
||||
writer, err = outputOpts.SBOMWriter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, output := range unchanged {
|
||||
if err := writeUnchanged(output, opts.LegacyFile, inputInfo, reader); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if writer == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := reader.Seek(0, io.SeekStart); err != nil {
|
||||
return fmt.Errorf("unable to seek to start of SBOM: %w", err)
|
||||
}
|
||||
|
||||
s, _, _, err := format.Decode(reader)
|
||||
@@ -100,3 +145,171 @@ func RunConvert(opts *ConvertOptions, userInput string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// openConvertInput opens the SBOM document at the given path, or STDIN when "-". The returned reader is seekable so
|
||||
// the document can be identified, copied, and decoded in turn without being loaded into memory. The returned
|
||||
// os.FileInfo describes the file the document is being read from, so that writing back over it can be recognized;
|
||||
// it is nil when the input is not a file (a pipe or a terminal on STDIN).
|
||||
func openConvertInput(userInput string, td *tmpdir.TempDir) (io.ReadSeekCloser, os.FileInfo, error) {
|
||||
if userInput == "-" {
|
||||
return openStdin(td)
|
||||
}
|
||||
|
||||
f, err := os.Open(userInput)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to open SBOM file: %w", err)
|
||||
}
|
||||
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
// the document is readable, we just cannot recognize it as an output destination
|
||||
log.WithFields("error", err, "path", userInput).Debug("unable to stat SBOM file")
|
||||
}
|
||||
return f, info, nil
|
||||
}
|
||||
|
||||
// openStdin returns a seekable view of STDIN. When STDIN is a regular file (such as a shell redirect) it is read in
|
||||
// place. A pipe or terminal cannot seek (you will get errors such as "seek /dev/stdin: illegal seek"), so its
|
||||
// contents are drained into a spill buffer: a small head stays in memory and everything beyond it goes to a temp
|
||||
// file under td, so an arbitrarily large piped SBOM never has to be held in memory to be made seekable.
|
||||
func openStdin(td *tmpdir.TempDir) (io.ReadSeekCloser, os.FileInfo, error) {
|
||||
if info, err := os.Stdin.Stat(); err == nil && info.Mode().IsRegular() {
|
||||
if start, err := os.Stdin.Seek(0, io.SeekCurrent); err == nil {
|
||||
// a redirect reads the file in place, so it is just as much an input file as a path argument is
|
||||
return readSeekNopCloser{io.NewSectionReader(os.Stdin, start, info.Size()-start)}, info, nil
|
||||
}
|
||||
}
|
||||
|
||||
buf := spillbuf.New(td)
|
||||
if _, err := io.Copy(io.NewOffsetWriter(buf, 0), os.Stdin); err != nil {
|
||||
_ = buf.Close()
|
||||
return nil, nil, fmt.Errorf("failed to read SBOM from STDIN: %w", err)
|
||||
}
|
||||
|
||||
return &spilledReader{
|
||||
SectionReader: io.NewSectionReader(buf, 0, buf.Size()),
|
||||
buf: buf,
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
type readSeekNopCloser struct {
|
||||
io.ReadSeeker
|
||||
}
|
||||
|
||||
func (readSeekNopCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// spilledReader is a seekable view over a spill buffer; closing it releases the buffer's memory and spill file.
|
||||
type spilledReader struct {
|
||||
*io.SectionReader
|
||||
buf *spillbuf.Buffer
|
||||
}
|
||||
|
||||
func (r *spilledReader) Close() error {
|
||||
return r.buf.Close()
|
||||
}
|
||||
|
||||
// partitionOutputsBySourceFormat splits the requested outputs (in "<format>[@<version>][=<path>]" form) into those
|
||||
// that exactly match the format and schema version of a syft-json input document (which can be written out
|
||||
// unchanged) and those that still require conversion. Input in any other format is always converted. Outputs that
|
||||
// cannot be resolved to an encoder are left for the writer to report on.
|
||||
func partitionOutputsBySourceFormat(output options.Output, reader io.ReadSeeker) (unchanged []string, toConvert []string, err error) {
|
||||
if _, err := reader.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to seek to start of SBOM: %w", err)
|
||||
}
|
||||
|
||||
id, version := format.Identify(reader)
|
||||
if id == "" || version == "" {
|
||||
// let decoding surface the appropriate error
|
||||
return nil, output.Outputs, nil
|
||||
}
|
||||
|
||||
if id != syftjson.ID {
|
||||
log.WithFields("format", id, "version", version).Debug("input is not syft-json, passthrough does not apply")
|
||||
return nil, output.Outputs, nil
|
||||
}
|
||||
|
||||
encoders, err := output.Encoders()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
collection := format.NewEncoderCollection(encoders...)
|
||||
|
||||
for _, out := range output.Outputs {
|
||||
name, _, _ := strings.Cut(strings.TrimSpace(out), "=")
|
||||
enc := collection.GetByString(name)
|
||||
if enc != nil && enc.ID() == id && enc.Version() == version {
|
||||
log.WithFields("format", id, "version", version, "output", out).Warn("input already exactly matches the requested output format and schema version, copying it unchanged instead of converting it")
|
||||
unchanged = append(unchanged, out)
|
||||
continue
|
||||
}
|
||||
toConvert = append(toConvert, out)
|
||||
}
|
||||
|
||||
return unchanged, toConvert, nil
|
||||
}
|
||||
|
||||
// writeUnchanged copies the SBOM document as-is to the destination described by the given "<format>[=<path>]"
|
||||
// output value, falling back to the (deprecated) --file path, and finally to the report bus (STDOUT). When the
|
||||
// destination is the input file itself there is nothing to do.
|
||||
func writeUnchanged(output string, defaultFile string, inputInfo os.FileInfo, reader io.ReadSeeker) error {
|
||||
if _, err := reader.Seek(0, io.SeekStart); err != nil {
|
||||
return fmt.Errorf("unable to seek to start of SBOM: %w", err)
|
||||
}
|
||||
|
||||
_, path, _ := strings.Cut(strings.TrimSpace(output), "=")
|
||||
if path == "" {
|
||||
path = defaultFile
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
// the report bus only carries strings, so STDOUT output must be materialized in memory
|
||||
content, err := io.ReadAll(reader) //nolint:gocritic // the whole SBOM is the report; there is no meaningful bound
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read SBOM: %w", err)
|
||||
}
|
||||
bus.Report(string(content))
|
||||
return nil
|
||||
}
|
||||
|
||||
expandedPath, err := homedir.Expand(path)
|
||||
if err != nil {
|
||||
log.Warnf("could not expand given writer output path=%q: %w", path, err)
|
||||
expandedPath = path
|
||||
}
|
||||
|
||||
if isSameFile(inputInfo, expandedPath) {
|
||||
log.WithFields("path", expandedPath).Info("output is the input file, leaving it as-is")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(expandedPath), 0755); err != nil {
|
||||
return fmt.Errorf("unable to create report directory: %w", err)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(expandedPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create report file: %w", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(f, reader); err != nil {
|
||||
_ = f.Close()
|
||||
return fmt.Errorf("unable to write report file: %w", err)
|
||||
}
|
||||
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
// isSameFile reports whether the given path names the file the input is being read from. Comparing the input's
|
||||
// os.FileInfo rather than its path covers a shell redirect onto STDIN, which has no path to compare.
|
||||
func isSameFile(inputInfo os.FileInfo, path string) bool {
|
||||
if inputInfo == nil {
|
||||
return false
|
||||
}
|
||||
pathInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return os.SameFile(inputInfo, pathInfo)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,448 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/syft/cmd/syft/internal/options"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/spillbuf"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/format"
|
||||
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||
"github.com/anchore/syft/syft/format/spdxjson"
|
||||
"github.com/anchore/syft/syft/format/syftjson"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func convertTestSBOM() sbom.SBOM {
|
||||
catalog := pkg.NewCollection()
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "musl-utils",
|
||||
Version: "1.2.3-r4",
|
||||
Type: pkg.ApkPkg,
|
||||
FoundBy: "apk-db-cataloger",
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewLocation("/lib/apk/db/installed"),
|
||||
),
|
||||
PURL: "pkg:apk/alpine/musl-utils@1.2.3-r4",
|
||||
})
|
||||
|
||||
return sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
Packages: catalog,
|
||||
},
|
||||
Source: source.Description{
|
||||
ID: "some-id",
|
||||
Name: "some-dir",
|
||||
Version: "latest",
|
||||
Metadata: source.DirectoryMetadata{
|
||||
Path: "/some/path",
|
||||
},
|
||||
},
|
||||
Descriptor: sbom.Descriptor{
|
||||
Name: "syft",
|
||||
Version: "v0.0.0-test",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func mustConvertEncoder(enc sbom.FormatEncoder, err error) sbom.FormatEncoder {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return enc
|
||||
}
|
||||
|
||||
func encodeConvertTestSBOM(t *testing.T, enc sbom.FormatEncoder) []byte {
|
||||
buf := &bytes.Buffer{}
|
||||
require.NoError(t, enc.Encode(buf, convertTestSBOM()))
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// prettySyftJSON returns the test SBOM as pretty-printed syft-json. Pretty input makes a verbatim copy
|
||||
// distinguishable from a re-encode, since re-encoding always produces compact output here.
|
||||
func prettySyftJSON(t *testing.T) []byte {
|
||||
cfg := syftjson.DefaultEncoderConfig()
|
||||
cfg.Pretty = true
|
||||
return encodeConvertTestSBOM(t, mustConvertEncoder(syftjson.NewFormatEncoderWithConfig(cfg)))
|
||||
}
|
||||
|
||||
func Test_partitionOutputsBySourceFormat(t *testing.T) {
|
||||
syftEncoder := mustConvertEncoder(syftjson.NewFormatEncoderWithConfig(syftjson.DefaultEncoderConfig()))
|
||||
syftVersion := syftEncoder.Version()
|
||||
syftContent := encodeConvertTestSBOM(t, syftEncoder)
|
||||
|
||||
// the same document claiming an older schema version
|
||||
olderSyftContent := bytes.ReplaceAll(syftContent, []byte(`"version":"`+syftVersion+`"`), []byte(`"version":"16.0.0"`))
|
||||
require.NotEqual(t, syftContent, olderSyftContent)
|
||||
|
||||
cdxContent := encodeConvertTestSBOM(t, mustConvertEncoder(cyclonedxjson.NewFormatEncoderWithConfig(cyclonedxjson.DefaultEncoderConfig())))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
content []byte
|
||||
outputs []string
|
||||
wantUnchanged []string
|
||||
wantToConvert []string
|
||||
}{
|
||||
{
|
||||
name: "syft-json matches the bare format name",
|
||||
content: syftContent,
|
||||
outputs: []string{"syft-json"},
|
||||
wantUnchanged: []string{"syft-json"},
|
||||
},
|
||||
{
|
||||
name: "syft-json matches its alias",
|
||||
content: syftContent,
|
||||
outputs: []string{"json"},
|
||||
wantUnchanged: []string{"json"},
|
||||
},
|
||||
{
|
||||
name: "syft-json matches an explicit version and file path",
|
||||
content: syftContent,
|
||||
outputs: []string{"syft-json@" + syftVersion + "=out/some.syft.json"},
|
||||
wantUnchanged: []string{"syft-json@" + syftVersion + "=out/some.syft.json"},
|
||||
},
|
||||
{
|
||||
name: "syft-json at an older schema version requires conversion",
|
||||
content: olderSyftContent,
|
||||
outputs: []string{"syft-json"},
|
||||
wantToConvert: []string{"syft-json"},
|
||||
},
|
||||
{
|
||||
name: "a different output format requires conversion",
|
||||
content: syftContent,
|
||||
outputs: []string{"spdx-json"},
|
||||
wantToConvert: []string{"spdx-json"},
|
||||
},
|
||||
{
|
||||
name: "mixed outputs are partitioned",
|
||||
content: syftContent,
|
||||
outputs: []string{"syft-json=a.json", "spdx-json=b.json", "cyclonedx-xml"},
|
||||
wantUnchanged: []string{"syft-json=a.json"},
|
||||
wantToConvert: []string{"spdx-json=b.json", "cyclonedx-xml"},
|
||||
},
|
||||
{
|
||||
name: "unknown format is left for the writer to report",
|
||||
content: syftContent,
|
||||
outputs: []string{"bogus"},
|
||||
wantToConvert: []string{"bogus"},
|
||||
},
|
||||
{
|
||||
name: "non syft-json input is always converted, even when it matches the output",
|
||||
content: cdxContent,
|
||||
outputs: []string{"cyclonedx-json"},
|
||||
wantToConvert: []string{"cyclonedx-json"},
|
||||
},
|
||||
{
|
||||
name: "unidentifiable content is left for the decoder to report",
|
||||
content: []byte("definitely not an sbom"),
|
||||
outputs: []string{"syft-json"},
|
||||
wantToConvert: []string{"syft-json"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output := options.DefaultOutput()
|
||||
output.Outputs = tt.outputs
|
||||
|
||||
unchanged, toConvert, err := partitionOutputsBySourceFormat(output, bytes.NewReader(tt.content))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantUnchanged, unchanged)
|
||||
assert.Equal(t, tt.wantToConvert, toConvert)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_RunConvert_passthroughExactFormat(t *testing.T) {
|
||||
syftContent := prettySyftJSON(t)
|
||||
|
||||
newOpts := func(passthrough bool, outputs ...string) *ConvertOptions {
|
||||
opts := &ConvertOptions{
|
||||
Output: options.DefaultOutput(),
|
||||
Convert: options.Convert{
|
||||
PassthroughExactFormat: passthrough,
|
||||
},
|
||||
}
|
||||
opts.Outputs = outputs
|
||||
return opts
|
||||
}
|
||||
|
||||
writeInput := func(t *testing.T, content []byte) string {
|
||||
input := filepath.Join(t.TempDir(), "input.json")
|
||||
require.NoError(t, os.WriteFile(input, content, 0600))
|
||||
return input
|
||||
}
|
||||
|
||||
t.Run("matching output is copied unchanged while others are converted", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
syftOut := filepath.Join(dir, "out.syft.json")
|
||||
spdxOut := filepath.Join(dir, "out.spdx.json")
|
||||
|
||||
opts := newOpts(true, "syft-json="+syftOut, "spdx-json="+spdxOut)
|
||||
require.NoError(t, RunConvert(context.Background(), opts, writeInput(t, syftContent)))
|
||||
|
||||
got, err := os.ReadFile(syftOut)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, syftContent, got)
|
||||
|
||||
spdx, err := os.ReadFile(spdxOut)
|
||||
require.NoError(t, err)
|
||||
id, _ := format.Identify(bytes.NewReader(spdx))
|
||||
assert.Equal(t, spdxjson.ID, id)
|
||||
})
|
||||
|
||||
t.Run("STDIN input is copied unchanged while others are converted", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
syftOut := filepath.Join(dir, "out.syft.json")
|
||||
spdxOut := filepath.Join(dir, "out.spdx.json")
|
||||
|
||||
stdin, err := os.Open(writeInput(t, syftContent))
|
||||
require.NoError(t, err)
|
||||
defer stdin.Close()
|
||||
|
||||
originalStdin := os.Stdin
|
||||
os.Stdin = stdin
|
||||
t.Cleanup(func() { os.Stdin = originalStdin })
|
||||
|
||||
opts := newOpts(true, "syft-json="+syftOut, "spdx-json="+spdxOut)
|
||||
require.NoError(t, RunConvert(context.Background(), opts, "-"))
|
||||
|
||||
got, err := os.ReadFile(syftOut)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, syftContent, got)
|
||||
|
||||
spdx, err := os.ReadFile(spdxOut)
|
||||
require.NoError(t, err)
|
||||
id, _ := format.Identify(bytes.NewReader(spdx))
|
||||
assert.Equal(t, spdxjson.ID, id)
|
||||
})
|
||||
|
||||
t.Run("piped STDIN input is copied unchanged while others are converted", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
syftOut := filepath.Join(dir, "out.syft.json")
|
||||
spdxOut := filepath.Join(dir, "out.spdx.json")
|
||||
|
||||
pipeReader, pipeWriter, err := os.Pipe()
|
||||
require.NoError(t, err)
|
||||
defer pipeReader.Close()
|
||||
go func() {
|
||||
defer pipeWriter.Close()
|
||||
_, _ = pipeWriter.Write(syftContent)
|
||||
}()
|
||||
|
||||
originalStdin := os.Stdin
|
||||
os.Stdin = pipeReader
|
||||
t.Cleanup(func() { os.Stdin = originalStdin })
|
||||
|
||||
opts := newOpts(true, "syft-json="+syftOut, "spdx-json="+spdxOut)
|
||||
require.NoError(t, RunConvert(context.Background(), opts, "-"))
|
||||
|
||||
got, err := os.ReadFile(syftOut)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, syftContent, got)
|
||||
|
||||
spdx, err := os.ReadFile(spdxOut)
|
||||
require.NoError(t, err)
|
||||
id, _ := format.Identify(bytes.NewReader(spdx))
|
||||
assert.Equal(t, spdxjson.ID, id)
|
||||
})
|
||||
|
||||
t.Run("without the option the matching output is re-encoded", func(t *testing.T) {
|
||||
syftOut := filepath.Join(t.TempDir(), "out.syft.json")
|
||||
|
||||
opts := newOpts(false, "syft-json="+syftOut)
|
||||
require.NoError(t, RunConvert(context.Background(), opts, writeInput(t, syftContent)))
|
||||
|
||||
got, err := os.ReadFile(syftOut)
|
||||
require.NoError(t, err)
|
||||
id, _ := format.Identify(bytes.NewReader(got))
|
||||
assert.Equal(t, syftjson.ID, id)
|
||||
// the input was pretty-printed and the encoder is not configured to be, so a re-encode cannot reproduce it
|
||||
assert.NotEqual(t, syftContent, got)
|
||||
})
|
||||
|
||||
t.Run("non syft-json input is re-encoded even when it matches the output", func(t *testing.T) {
|
||||
cdxContent := encodeConvertTestSBOM(t, mustConvertEncoder(cyclonedxjson.NewFormatEncoderWithConfig(cyclonedxjson.DefaultEncoderConfig())))
|
||||
cdxOut := filepath.Join(t.TempDir(), "out.cdx.json")
|
||||
|
||||
opts := newOpts(true, "cyclonedx-json="+cdxOut)
|
||||
require.NoError(t, RunConvert(context.Background(), opts, writeInput(t, cdxContent)))
|
||||
|
||||
got, err := os.ReadFile(cdxOut)
|
||||
require.NoError(t, err)
|
||||
id, _ := format.Identify(bytes.NewReader(got))
|
||||
assert.Equal(t, cyclonedxjson.ID, id)
|
||||
// a fresh serial number is minted on every encode, so a round trip never reproduces the input
|
||||
assert.NotEqual(t, cdxContent, got)
|
||||
})
|
||||
|
||||
t.Run("the deprecated --file path is honored", func(t *testing.T) {
|
||||
syftOut := filepath.Join(t.TempDir(), "out.syft.json")
|
||||
|
||||
opts := newOpts(true, "syft-json")
|
||||
opts.LegacyFile = syftOut
|
||||
require.NoError(t, RunConvert(context.Background(), opts, writeInput(t, syftContent)))
|
||||
|
||||
got, err := os.ReadFile(syftOut)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, syftContent, got)
|
||||
})
|
||||
|
||||
t.Run("writing over the input file leaves it intact", func(t *testing.T) {
|
||||
input := writeInput(t, syftContent)
|
||||
|
||||
opts := newOpts(true, "syft-json="+input)
|
||||
require.NoError(t, RunConvert(context.Background(), opts, input))
|
||||
|
||||
got, err := os.ReadFile(input)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, syftContent, got)
|
||||
})
|
||||
|
||||
t.Run("writing over a redirected STDIN input leaves it intact", func(t *testing.T) {
|
||||
input := writeInput(t, syftContent)
|
||||
|
||||
stdin, err := os.Open(input)
|
||||
require.NoError(t, err)
|
||||
defer stdin.Close()
|
||||
|
||||
originalStdin := os.Stdin
|
||||
os.Stdin = stdin
|
||||
t.Cleanup(func() { os.Stdin = originalStdin })
|
||||
|
||||
opts := newOpts(true, "syft-json="+input)
|
||||
require.NoError(t, RunConvert(context.Background(), opts, "-"))
|
||||
|
||||
got, err := os.ReadFile(input)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, syftContent, got)
|
||||
})
|
||||
|
||||
t.Run("an invalid output fails before anything is written", func(t *testing.T) {
|
||||
syftOut := filepath.Join(t.TempDir(), "out.syft.json")
|
||||
|
||||
opts := newOpts(true, "syft-json="+syftOut, "bogus")
|
||||
require.Error(t, RunConvert(context.Background(), opts, writeInput(t, syftContent)))
|
||||
|
||||
assert.NoFileExists(t, syftOut)
|
||||
})
|
||||
|
||||
t.Run("a missing input file is reported", func(t *testing.T) {
|
||||
syftOut := filepath.Join(t.TempDir(), "out.syft.json")
|
||||
|
||||
opts := newOpts(true, "syft-json="+syftOut)
|
||||
err := RunConvert(context.Background(), opts, filepath.Join(t.TempDir(), "does-not-exist.json"))
|
||||
require.ErrorContains(t, err, "failed to open SBOM file")
|
||||
|
||||
assert.NoFileExists(t, syftOut)
|
||||
})
|
||||
|
||||
t.Run("unrecognized input is left to the decoder to reject", func(t *testing.T) {
|
||||
syftOut := filepath.Join(t.TempDir(), "out.syft.json")
|
||||
|
||||
opts := newOpts(true, "syft-json="+syftOut)
|
||||
err := RunConvert(context.Background(), opts, writeInput(t, []byte("definitely not an sbom")))
|
||||
require.ErrorContains(t, err, "failed to decode SBOM")
|
||||
})
|
||||
}
|
||||
|
||||
// largeSyftJSON returns a pretty-printed syft-json document of at least minBytes, built from many small packages.
|
||||
func largeSyftJSON(t *testing.T, minBytes int64) []byte {
|
||||
catalog := pkg.NewCollection()
|
||||
for i := 0; i < int(minBytes/100); i++ {
|
||||
catalog.Add(pkg.Package{
|
||||
Name: fmt.Sprintf("pkg-%d", i),
|
||||
Version: "1.0.0",
|
||||
Type: pkg.ApkPkg,
|
||||
PURL: fmt.Sprintf("pkg:apk/alpine/pkg-%d@1.0.0", i),
|
||||
})
|
||||
}
|
||||
s := convertTestSBOM()
|
||||
s.Artifacts.Packages = catalog
|
||||
|
||||
cfg := syftjson.DefaultEncoderConfig()
|
||||
cfg.Pretty = true
|
||||
enc := mustConvertEncoder(syftjson.NewFormatEncoderWithConfig(cfg))
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
require.NoError(t, enc.Encode(buf, s))
|
||||
require.Greater(t, int64(buf.Len()), minBytes)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func Test_RunConvert_pipedInputLargerThanMemoryLimitSpillsToDisk(t *testing.T) {
|
||||
// a piped document bigger than the spill buffer's in-memory tier is written to a temp file under TMPDIR;
|
||||
// the conversion must identify, copy and clean up from there rather than holding the document in memory
|
||||
dir := t.TempDir()
|
||||
syftOut := filepath.Join(dir, "out.syft.json")
|
||||
spillRoot := filepath.Join(dir, "spill")
|
||||
require.NoError(t, os.Mkdir(spillRoot, 0o755))
|
||||
t.Setenv("TMPDIR", spillRoot)
|
||||
|
||||
content := largeSyftJSON(t, 2*spillbuf.DefaultMemLimit)
|
||||
|
||||
pipeReader, pipeWriter, err := os.Pipe()
|
||||
require.NoError(t, err)
|
||||
defer pipeReader.Close()
|
||||
go func() {
|
||||
defer pipeWriter.Close()
|
||||
_, _ = pipeWriter.Write(content)
|
||||
}()
|
||||
|
||||
originalStdin := os.Stdin
|
||||
os.Stdin = pipeReader
|
||||
t.Cleanup(func() { os.Stdin = originalStdin })
|
||||
|
||||
opts := &ConvertOptions{
|
||||
Output: options.DefaultOutput(),
|
||||
Convert: options.Convert{
|
||||
PassthroughExactFormat: true,
|
||||
},
|
||||
}
|
||||
opts.Outputs = []string{"syft-json=" + syftOut}
|
||||
require.NoError(t, RunConvert(context.Background(), opts, "-"))
|
||||
|
||||
got, err := os.ReadFile(syftOut)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, content, got)
|
||||
|
||||
entries, err := os.ReadDir(spillRoot)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, entries, "the spill directory must be removed once the conversion finishes")
|
||||
}
|
||||
|
||||
func Test_writeUnchanged_stdout(t *testing.T) {
|
||||
// with no output path the document is published on the report bus, which is how the CLI writes to STDOUT
|
||||
b := partybus.NewBus()
|
||||
subscription := b.Subscribe()
|
||||
bus.Set(b)
|
||||
t.Cleanup(func() {
|
||||
bus.Set(nil)
|
||||
})
|
||||
|
||||
content := []byte(`{"some":"document"}`)
|
||||
require.NoError(t, writeUnchanged("syft-json", "", nil, bytes.NewReader(content)))
|
||||
|
||||
select {
|
||||
case e := <-subscription.Events():
|
||||
assert.Equal(t, event.CLIReport, e.Type)
|
||||
assert.Equal(t, string(content), e.Value)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("no report was published")
|
||||
}
|
||||
}
|
||||
@@ -199,7 +199,8 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
|
||||
WithFromLDFlags(cfg.Golang.MainModuleVersion.FromLDFlags),
|
||||
).
|
||||
WithUsePackagesLib(*multiLevelOption(true, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.UsePackagesLib)).
|
||||
WithCaptureSymbols(cfg.Golang.CaptureSymbols),
|
||||
WithCaptureSymbols(cfg.Golang.CaptureSymbols).
|
||||
WithCaptureSymbolsModules(cfg.Golang.CaptureSymbolsModules),
|
||||
JavaScript: javascript.DefaultCatalogerConfig().
|
||||
WithIncludeDevDependencies(*multiLevelOption(false, cfg.JavaScript.IncludeDevDependencies)).
|
||||
WithSearchRemoteLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.JavaScript, task.Node, task.NPM), cfg.JavaScript.SearchRemoteLicenses)).
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"github.com/anchore/clio"
|
||||
)
|
||||
|
||||
var _ clio.FieldDescriber = (*Convert)(nil)
|
||||
|
||||
// Convert holds the options specific to the convert command. These are deliberately configuration-only (config file
|
||||
// or environment) rather than flags: they change how the command behaves for every invocation in a pipeline and are
|
||||
// not expected to vary from one call to the next.
|
||||
type Convert struct {
|
||||
PassthroughExactFormat bool `yaml:"passthrough-exact-format" json:"passthrough-exact-format" mapstructure:"passthrough-exact-format"`
|
||||
}
|
||||
|
||||
func DefaultConvert() Convert {
|
||||
return Convert{
|
||||
PassthroughExactFormat: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Convert) DescribeFields(descriptions clio.FieldDescriptionSet) {
|
||||
descriptions.Add(&o.PassthroughExactFormat, `when a syft-json input SBOM already exactly matches the requested output format and schema version, copy it to that output unchanged instead of decoding and re-encoding it.
|
||||
Note: this only applies to syft-json input, and only when the document's schema version is identical to the schema version this version of syft produces. Any other input is always converted.
|
||||
Note: output written this way is a byte-for-byte copy of the input, so format options (such as 'format.pretty' or 'format.json.legacy') are not applied to it.
|
||||
Note: to keep memory use minimal on large documents, give the input as a file path (or a shell redirect) and write the output with '-o <format>=<path>'. Piped input, or output to STDOUT, requires the whole document to be held in memory.`)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/golang"
|
||||
)
|
||||
@@ -19,6 +20,7 @@ type golangConfig struct {
|
||||
MainModuleVersion golangMainModuleVersionConfig `json:"main-module-version" yaml:"main-module-version" mapstructure:"main-module-version"`
|
||||
UsePackagesLib *bool `json:"use-packages-lib" yaml:"use-packages-lib" mapstructure:"use-packages-lib"`
|
||||
CaptureSymbols cataloging.SymbolScope `json:"capture-symbols" yaml:"capture-symbols" mapstructure:"capture-symbols"`
|
||||
CaptureSymbolsModules []string `json:"capture-symbols-modules" yaml:"capture-symbols-modules" mapstructure:"capture-symbols-modules"`
|
||||
}
|
||||
|
||||
var _ interface {
|
||||
@@ -42,8 +44,16 @@ if unset this defaults to $GONOPROXY`)
|
||||
always show (devel) as the version. Use these options to control heuristics to guess
|
||||
a more accurate version from the binary.`)
|
||||
descriptions.Add(&o.UsePackagesLib, `use the golang.org/x/tools/go/packages library, which executes golang tooling found on the path in addition to potential network access to get the most accurate results`)
|
||||
// note: descriptions must be static string literals; the app config discovery that generates the
|
||||
// capability docs reads them straight out of the AST
|
||||
descriptions.Add(&o.CaptureSymbols, `capture function symbols from the binary symbol table (pclntab). valid values are:
|
||||
"none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib)`)
|
||||
"none" (disabled), "stdlib" (only the synthetic stdlib package), "extended-stdlib" (stdlib plus every
|
||||
module under golang.org/x/), and "all" (all module packages plus stdlib)`)
|
||||
descriptions.Add(&o.CaptureSymbolsModules, `glob patterns matched against go module paths (e.g. github.com/klauspost/**) that should have symbols
|
||||
captured in addition to whatever capture-symbols selects. ** crosses path separators, * does not.
|
||||
a trailing major version suffix is ignored when matching, so github.com/foo/* covers github.com/foo/bar/v2;
|
||||
spelling a suffix out in the pattern selects only that major version.
|
||||
this can only widen the selection, never narrow it, and is inert when capture-symbols is none`)
|
||||
descriptions.Add(&o.MainModuleVersion.FromLDFlags, `look for LD flags that appear to be setting a version (e.g. -X main.version=1.0.0)`)
|
||||
descriptions.Add(&o.MainModuleVersion.FromBuildSettings, `use the build settings (e.g. vcs.version & vcs.time) to craft a v0 pseudo version
|
||||
(e.g. v0.0.0-20220308212642-53e6d0aaf6fb) when a more accurate version cannot be found otherwise`)
|
||||
@@ -51,7 +61,24 @@ a more accurate version from the binary.`)
|
||||
}
|
||||
|
||||
func (o *golangConfig) PostLoad() error {
|
||||
raw := strings.TrimSpace(string(o.CaptureSymbols))
|
||||
o.CaptureSymbols = o.CaptureSymbols.Parse()
|
||||
|
||||
// an unrecognized value still resolves to "none", but say so rather than silently capturing nothing.
|
||||
// stay quiet for unset and an explicit "none", which is the default and would otherwise warn on nearly
|
||||
// every scan.
|
||||
if o.CaptureSymbols == cataloging.SymbolScopeNone && raw != "" && !strings.EqualFold(raw, string(cataloging.SymbolScopeNone)) {
|
||||
log.Warnf("unknown golang.capture-symbols value %q, defaulting to %q (valid values: none, stdlib, extended-stdlib, all)", raw, cataloging.SymbolScopeNone)
|
||||
}
|
||||
|
||||
// trim only, deliberately not Flatten: viper already splits a comma-separated scalar (env var or a bare
|
||||
// yaml string) into a slice before this point, and there is no CLI flag feeding this key. The one thing
|
||||
// Flatten would add is splitting commas *inside* a list entry, which silently breaks doublestar brace
|
||||
// alternation like github.com/{foo,bar}/**. Viper's split does not trim, so that part is still needed.
|
||||
for i, pattern := range o.CaptureSymbolsModules {
|
||||
o.CaptureSymbolsModules[i] = strings.TrimSpace(pattern)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -76,7 +103,8 @@ func defaultGolangConfig() golangConfig {
|
||||
FromContents: def.MainModuleVersion.FromContents,
|
||||
FromBuildSettings: def.MainModuleVersion.FromBuildSettings,
|
||||
},
|
||||
UsePackagesLib: nil, // this defaults to true, which is the API default
|
||||
CaptureSymbols: def.CaptureSymbols,
|
||||
UsePackagesLib: nil, // this defaults to true, which is the API default
|
||||
CaptureSymbols: def.CaptureSymbols,
|
||||
CaptureSymbolsModules: def.CaptureSymbolsModules,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ import (
|
||||
|
||||
func Test_golangConfig_PostLoad(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg golangConfig
|
||||
expected cataloging.SymbolScope
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
name string
|
||||
cfg golangConfig
|
||||
expected cataloging.SymbolScope
|
||||
expectedModules []string
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "normalize all",
|
||||
@@ -25,6 +26,32 @@ func Test_golangConfig_PostLoad(t *testing.T) {
|
||||
cfg: golangConfig{CaptureSymbols: "stdlib"},
|
||||
expected: cataloging.SymbolScopeStdlib,
|
||||
},
|
||||
{
|
||||
name: "normalize extended-stdlib",
|
||||
cfg: golangConfig{CaptureSymbols: " Extended-Stdlib "},
|
||||
expected: cataloging.SymbolScopeExtendedStdlib,
|
||||
},
|
||||
{
|
||||
name: "module patterns keep embedded commas",
|
||||
cfg: golangConfig{
|
||||
CaptureSymbols: "stdlib",
|
||||
// brace alternation contains a comma; splitting on it would corrupt the pattern
|
||||
CaptureSymbolsModules: []string{"github.com/{foo,bar}/**", "golang.org/x/**"},
|
||||
},
|
||||
expected: cataloging.SymbolScopeStdlib,
|
||||
expectedModules: []string{"github.com/{foo,bar}/**", "golang.org/x/**"},
|
||||
},
|
||||
{
|
||||
// viper splits a comma-separated scalar (env var or bare yaml string) but does not trim,
|
||||
// so a leading space would otherwise survive into a pattern that silently matches nothing
|
||||
name: "module patterns are trimmed",
|
||||
cfg: golangConfig{
|
||||
CaptureSymbols: "stdlib",
|
||||
CaptureSymbolsModules: []string{"golang.org/x/**", " github.com/foo/** "},
|
||||
},
|
||||
expected: cataloging.SymbolScopeStdlib,
|
||||
expectedModules: []string{"golang.org/x/**", "github.com/foo/**"},
|
||||
},
|
||||
{
|
||||
name: "empty defaults to none",
|
||||
cfg: golangConfig{CaptureSymbols: ""},
|
||||
@@ -32,7 +59,7 @@ func Test_golangConfig_PostLoad(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "invalid value defaults to none",
|
||||
cfg: golangConfig{CaptureSymbols: "bogus"},
|
||||
cfg: golangConfig{CaptureSymbols: "stdlbi"},
|
||||
expected: cataloging.SymbolScopeNone,
|
||||
},
|
||||
{
|
||||
@@ -40,6 +67,16 @@ func Test_golangConfig_PostLoad(t *testing.T) {
|
||||
cfg: golangConfig{CaptureSymbols: "true"},
|
||||
expected: cataloging.SymbolScopeNone,
|
||||
},
|
||||
{
|
||||
name: "explicit none resolves to none",
|
||||
cfg: golangConfig{CaptureSymbols: "none"},
|
||||
expected: cataloging.SymbolScopeNone,
|
||||
},
|
||||
{
|
||||
name: "explicit none is not case sensitive",
|
||||
cfg: golangConfig{CaptureSymbols: " NONE "},
|
||||
expected: cataloging.SymbolScopeNone,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -52,6 +89,7 @@ func Test_golangConfig_PostLoad(t *testing.T) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.expected, tt.cfg.CaptureSymbols)
|
||||
assert.Equal(t, tt.expectedModules, tt.cfg.CaptureSymbolsModules)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// TestMultiPlatformOCIImageSelection verifies that syft can load a single platform out of a
|
||||
// multi-platform OCI image on disk (both --from oci-dir and --from oci-archive) by selecting the
|
||||
// image that matches the requested platform.
|
||||
func TestMultiPlatformOCIImageSelection(t *testing.T) {
|
||||
remoteImage := "docker.io/library/busybox:1.38.0"
|
||||
|
||||
// Per-platform image config digests within the multi-platform index, obtained from the OCI layout
|
||||
// (see anchore/stereoscope integration tests: TestPlatformSelectionWithOciLocalSources).
|
||||
expectedDigest := map[string]string{
|
||||
"arm64": "sha256:e0e8b3cbfed68a90084781e2962f9c0deead51c5a3f11a488eef0283a4284bc2",
|
||||
"s390x": "sha256:0cf160e720a8e4f20883b72276a6eaded83b79539f3f1d39e35a3336154b9960",
|
||||
"amd64": "sha256:c6348fa86ba0fb2108c9334f5fe913ddc6d853313e655891f133a0127c30099f",
|
||||
"ppc64le": "sha256:b144fc0e06537d07956cde340b878a81a717e394edb736d3110858a12a6635cb",
|
||||
}
|
||||
|
||||
// syft --from source tag -> stereoscope OCI source used to prepare the local fixture
|
||||
sources := map[string]image.Source{
|
||||
"oci-dir": image.OciDirectorySource,
|
||||
"oci-archive": image.OciTarballSource,
|
||||
}
|
||||
|
||||
for from, imageSource := range sources {
|
||||
t.Run(from, func(t *testing.T) {
|
||||
localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, remoteImage)
|
||||
for _, arch := range []string{"amd64", "arm64", "s390x", "ppc64le"} {
|
||||
t.Run(fmt.Sprintf("linux/%s", arch), func(t *testing.T) {
|
||||
platform, err := image.NewPlatform("linux/" + arch)
|
||||
require.NoError(t, err)
|
||||
|
||||
src, err := syft.GetSource(
|
||||
context.Background(),
|
||||
localPath,
|
||||
syft.DefaultGetSourceConfig().WithSources(from).WithPlatform(platform),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, src.Close())
|
||||
})
|
||||
|
||||
meta, ok := src.Describe().Metadata.(source.ImageMetadata)
|
||||
require.True(t, ok, "expected image metadata, got %T", src.Describe().Metadata)
|
||||
|
||||
// The raw config of the selected image must match the requested platform...
|
||||
assertConfigPlatform(t, meta.RawConfig, "linux", arch)
|
||||
assert.Equal(t, "linux", meta.OS)
|
||||
assert.Equal(t, arch, meta.Architecture)
|
||||
// ...and it must be the exact per-platform image from the multi-platform index.
|
||||
assert.Equal(t, expectedDigest[arch], meta.ID)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMultiPlatformOCIImageSelection_UnavailablePlatform verifies that requesting a platform not present
|
||||
// in the multi-platform OCI image results in an error rather than silently selecting the wrong image.
|
||||
func TestMultiPlatformOCIImageSelection_UnavailablePlatform(t *testing.T) {
|
||||
remoteImage := "docker.io/library/busybox:1.38.0"
|
||||
|
||||
// windows/amd64 is not present in this linux-only multi-platform image
|
||||
platform, err := image.NewPlatform("windows/amd64")
|
||||
require.NoError(t, err)
|
||||
|
||||
sources := map[string]image.Source{
|
||||
"oci-dir": image.OciDirectorySource,
|
||||
"oci-archive": image.OciTarballSource,
|
||||
}
|
||||
|
||||
for from, imageSource := range sources {
|
||||
t.Run(from, func(t *testing.T) {
|
||||
localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, remoteImage)
|
||||
|
||||
_, err := syft.GetSource(
|
||||
context.Background(),
|
||||
localPath,
|
||||
syft.DefaultGetSourceConfig().WithSources(from).WithPlatform(platform),
|
||||
)
|
||||
require.ErrorContains(t, err, "windows/amd64")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMultiPlatformOCIImageSelection_DefaultPlatform verifies that not specifying a platform results
|
||||
// in the current platform being selected.
|
||||
func TestMultiPlatformOCIImageSelection_DefaultPlatform(t *testing.T) {
|
||||
remoteImage := "docker.io/library/busybox:1.38.0"
|
||||
|
||||
sources := map[string]image.Source{
|
||||
"oci-dir": image.OciDirectorySource,
|
||||
"oci-archive": image.OciTarballSource,
|
||||
}
|
||||
|
||||
for from, imageSource := range sources {
|
||||
t.Run(from, func(t *testing.T) {
|
||||
localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, remoteImage)
|
||||
|
||||
src, err := syft.GetSource(
|
||||
context.Background(),
|
||||
localPath,
|
||||
syft.DefaultGetSourceConfig().WithSources(from),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, src.Close())
|
||||
})
|
||||
|
||||
meta, ok := src.Describe().Metadata.(source.ImageMetadata)
|
||||
require.True(t, ok, "expected image metadata, got %T", src.Describe().Metadata)
|
||||
|
||||
// The raw config of the selected image must match the current platform
|
||||
assertConfigPlatform(t, meta.RawConfig, "linux", runtime.GOARCH)
|
||||
assert.Equal(t, "linux", meta.OS)
|
||||
assert.Equal(t, runtime.GOARCH, meta.Architecture)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// assertConfigPlatform asserts the os/architecture recorded in a raw OCI image config document.
|
||||
func assertConfigPlatform(t *testing.T, rawConfig []byte, os, architecture string) {
|
||||
t.Helper()
|
||||
require.NotEmpty(t, rawConfig)
|
||||
|
||||
var cfg struct {
|
||||
OS string `json:"os"`
|
||||
Architecture string `json:"architecture"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(rawConfig, &cfg))
|
||||
|
||||
assert.Equal(t, os, cfg.OS)
|
||||
assert.Equal(t, architecture, cfg.Architecture)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// TestSinglePlatformOCIImage covers the single-image (non-index) OCI layouts, where syft used to report
|
||||
// empty os/architecture metadata and silently accept any --platform. Both now come from the image config.
|
||||
func TestSinglePlatformOCIImage(t *testing.T) {
|
||||
// a platform this fixture is never built for, so the mismatch is not host dependent
|
||||
unavailablePlatform, err := image.NewPlatform("linux/ppc64le")
|
||||
require.NoError(t, err)
|
||||
|
||||
sources := map[string]image.Source{
|
||||
"oci-dir": image.OciDirectorySource,
|
||||
"oci-archive": image.OciTarballSource,
|
||||
}
|
||||
|
||||
for from, imageSource := range sources {
|
||||
t.Run(from, func(t *testing.T) {
|
||||
localPath := strings.TrimPrefix(imagetest.PrepareFixtureImage(t, imageSource, "image-distro-id"), string(imageSource)+":")
|
||||
|
||||
t.Run("reports the platform from the image config", func(t *testing.T) {
|
||||
src, err := syft.GetSource(
|
||||
context.Background(),
|
||||
localPath,
|
||||
syft.DefaultGetSourceConfig().WithSources(from),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, src.Close())
|
||||
})
|
||||
|
||||
meta, ok := src.Describe().Metadata.(source.ImageMetadata)
|
||||
require.True(t, ok, "expected image metadata, got %T", src.Describe().Metadata)
|
||||
|
||||
// the fixture is built for whatever the local docker daemon runs, so assert against the
|
||||
// config rather than a fixed platform
|
||||
assert.NotEmpty(t, meta.OS)
|
||||
assert.NotEmpty(t, meta.Architecture)
|
||||
assertConfigPlatform(t, meta.RawConfig, meta.OS, meta.Architecture)
|
||||
})
|
||||
|
||||
t.Run("rejects a platform the image does not match", func(t *testing.T) {
|
||||
_, err := syft.GetSource(
|
||||
context.Background(),
|
||||
localPath,
|
||||
syft.DefaultGetSourceConfig().WithSources(from).WithPlatform(unavailablePlatform),
|
||||
)
|
||||
require.ErrorContains(t, err, "linux/ppc64le")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,6 @@
|
||||
!image-pkg-coverage/pkgs/java/*.hpi
|
||||
|
||||
**/go.sum
|
||||
!image-go-bin-arch-coverage/go.sum
|
||||
!image-go-bin-arch-coverage/go.sum
|
||||
|
||||
cache/
|
||||
+2
-3
@@ -8,8 +8,6 @@ RUN mkdir tmp
|
||||
|
||||
WORKDIR /usr/share/jenkins/tmp
|
||||
|
||||
RUN apt-get update 2>&1 > /dev/null && apt-get install -y less zip 2>&1 > /dev/null
|
||||
|
||||
RUN unzip ../jenkins.war 2>&1 > /dev/null
|
||||
|
||||
RUN rm -rf ./META-INF/MANIFEST.MF ./WEB-INF ./jsbundles ./scripts ./css
|
||||
@@ -18,7 +16,8 @@ WORKDIR /usr/share/jenkins
|
||||
|
||||
RUN rm -rf jenkins.war
|
||||
|
||||
RUN cd ./tmp && zip -r ../jenkins.war . && cd ..
|
||||
# use jar (from the JDK in the base image) to rezip -- M keeps it manifest-less, which is the point of this fixture
|
||||
RUN cd ./tmp && jar cfM ../jenkins.war . && cd ..
|
||||
|
||||
RUN rm -rf ./tmp
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN unxz /lib/modules/6.0.7-301.fc37.x86_64/kernel/drivers/tty/ttynull.ko.xz
|
||||
# dotnet pkg coverage
|
||||
# https://nuget.info/packages/DocuSign.eSign.dll/6.8.0
|
||||
# https://github.com/docusign/docusign-esign-csharp-client/blob/master/LICENSE
|
||||
RUN curl -LO https://www.nuget.org/api/v2/package/DocuSign.eSign.dll/6.8.0
|
||||
RUN curl -fLO https://www.nuget.org/api/v2/package/DocuSign.eSign.dll/6.8.0
|
||||
RUN unzip 6.8.0
|
||||
RUN chmod 600 lib/net462/DocuSign.eSign.dll
|
||||
RUN rm 6.8.0
|
||||
|
||||
@@ -4,7 +4,7 @@ go 1.26.3
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.6.0
|
||||
github.com/CycloneDX/cyclonedx-go v0.11.0
|
||||
github.com/CycloneDX/cyclonedx-go v0.12.0
|
||||
github.com/Masterminds/semver/v3 v3.5.0
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/OneOfOne/xxhash v1.2.8
|
||||
@@ -18,11 +18,11 @@ require (
|
||||
github.com/anchore/go-homedir v0.1.1
|
||||
github.com/anchore/go-logger v0.1.1
|
||||
github.com/anchore/go-macholibre v0.1.1
|
||||
github.com/anchore/go-rpmdb v0.1.0
|
||||
github.com/anchore/go-sync v0.1.1
|
||||
github.com/anchore/go-rpmdb v0.2.0
|
||||
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.2.2
|
||||
github.com/anchore/stereoscope v0.3.2
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/aquasecurity/go-pep440-version v0.0.1
|
||||
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
|
||||
@@ -34,36 +34,36 @@ require (
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/dave/jennifer v1.7.1
|
||||
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
|
||||
github.com/diskfs/go-diskfs v1.9.3
|
||||
github.com/diskfs/go-diskfs v1.9.4
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/elliotchance/phpserialize v1.4.0
|
||||
github.com/facebookincubator/nvdtools v0.1.5
|
||||
github.com/github/go-spdx/v2 v2.7.0
|
||||
github.com/gkampitakis/go-snaps v0.5.22
|
||||
github.com/go-git/go-billy/v5 v5.9.0
|
||||
github.com/go-git/go-git/v5 v5.19.1
|
||||
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-test/deep v1.1.1
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||
github.com/goccy/go-yaml v1.19.2
|
||||
github.com/gohugoio/hashstructure v0.6.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/go-containerregistry v0.21.7
|
||||
github.com/google/go-containerregistry v0.22.1
|
||||
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.24.1
|
||||
github.com/gpustack/gguf-parser-go v0.26.3
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2
|
||||
github.com/hashicorp/go-getter v1.8.6
|
||||
github.com/hashicorp/go-getter v1.8.9
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/hcl/v2 v2.24.0
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
github.com/invopop/jsonschema v0.14.0
|
||||
github.com/jedib0t/go-pretty/v6 v6.8.1
|
||||
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.19.0
|
||||
github.com/magiconair/properties v1.8.10
|
||||
github.com/klauspost/compress v1.20.0
|
||||
github.com/magiconair/properties v1.18.11
|
||||
github.com/mholt/archives v0.1.5
|
||||
github.com/moby/sys/mountinfo v0.7.2
|
||||
github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1
|
||||
@@ -82,8 +82,8 @@ require (
|
||||
github.com/spdx/tools-golang v0.6.0-rc4
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/ulikunitz/xz v0.5.16
|
||||
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
|
||||
@@ -91,14 +91,14 @@ require (
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1
|
||||
go.uber.org/goleak v1.3.0
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
go.yaml.in/yaml/v3 v3.0.5
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
||||
golang.org/x/mod v0.37.0
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/time v0.15.0
|
||||
golang.org/x/tools v0.47.0
|
||||
golang.org/x/mod v0.41.0
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/time v0.16.0
|
||||
golang.org/x/tools v0.49.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.53.0
|
||||
modernc.org/sqlite v1.58.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -108,19 +108,19 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cel.dev/expr v0.25.2 // indirect
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.2 // indirect
|
||||
cloud.google.com/go/auth v0.23.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.3 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.3 // indirect
|
||||
cloud.google.com/go/storage v1.61.3 // indirect
|
||||
cloud.google.com/go/iam v1.12.0 // indirect
|
||||
cloud.google.com/go/monitoring v1.30.0 // indirect
|
||||
cloud.google.com/go/storage v1.65.1 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/DataDog/zstd v1.5.5 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 // indirect
|
||||
github.com/Microsoft/hcsshim v0.15.0-rc.1 // indirect
|
||||
@@ -133,25 +133,24 @@ require (
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/aquasecurity/go-version v0.0.1 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect
|
||||
github.com/aws/smithy-go v1.24.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.44.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.40 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.39 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.40 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.41 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.10.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.41 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.108.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.6.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.34.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.39.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.46.0 // indirect
|
||||
github.com/aws/smithy-go v1.28.1 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/becheran/wildmatch-go v1.0.0 // indirect
|
||||
@@ -168,35 +167,34 @@ require (
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
|
||||
github.com/containerd/cgroups/v3 v3.1.3 // indirect
|
||||
github.com/containerd/containerd/api v1.11.1 // indirect
|
||||
github.com/containerd/containerd/v2 v2.3.2 // indirect
|
||||
github.com/containerd/containerd/v2 v2.3.5 // indirect
|
||||
github.com/containerd/continuity v0.5.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/fifo v1.1.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v1.0.0-rc.4 // indirect
|
||||
github.com/containerd/platforms v1.0.0-rc.5 // indirect
|
||||
github.com/containerd/plugin v1.1.0 // indirect
|
||||
github.com/containerd/ttrpc v1.2.8 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.2.3 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/cli v29.5.3+incompatible // indirect
|
||||
github.com/docker/cli v29.8.0+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.5 // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/docker/go-connections v0.8.1 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/felixge/fgprof v0.9.5 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
|
||||
github.com/gkampitakis/ciinfo v0.3.4 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
@@ -205,13 +203,13 @@ require (
|
||||
github.com/go-restruct/restruct v1.2.0-alpha // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
|
||||
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.20 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.24.0 // indirect
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||
github.com/hashicorp/go-version v1.9.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/henvic/httpretty v0.1.4 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
@@ -226,7 +224,7 @@ require (
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/maruel/natural v1.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.21 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
@@ -238,8 +236,8 @@ require (
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/moby/api v1.54.2 // indirect
|
||||
github.com/moby/moby/client v0.4.1 // indirect
|
||||
github.com/moby/moby/api v1.56.0 // indirect
|
||||
github.com/moby/moby/client v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/sys/signal v0.7.1 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
@@ -257,7 +255,7 @@ require (
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.3.0 // indirect
|
||||
github.com/pborman/indent v1.2.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.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
|
||||
@@ -265,7 +263,6 @@ require (
|
||||
github.com/pkg/profile v1.7.0 // indirect
|
||||
github.com/pkg/xattr v0.4.12 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
@@ -281,8 +278,8 @@ require (
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spf13/viper v1.21.0 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.7.0 // 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/squashfs v1.0.6 // indirect
|
||||
@@ -299,34 +296,34 @@ require (
|
||||
github.com/zclconf/go-cty v1.16.3 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // 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.53.0 // indirect
|
||||
golang.org/x/crypto v0.56.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/term v0.44.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
gonum.org/v1/gonum v0.17.0 // indirect
|
||||
google.golang.org/api v0.271.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect
|
||||
google.golang.org/grpc v1.80.0 // indirect
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||
google.golang.org/api v0.294.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect
|
||||
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.73.4 // indirect
|
||||
modernc.org/libc v1.75.6 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/memory v1.12.1 // indirect
|
||||
)
|
||||
|
||||
retract (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
|
||||
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
@@ -31,8 +31,8 @@ cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0c
|
||||
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
||||
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
|
||||
cloud.google.com/go/auth v0.23.2 h1:pxSCpfiji41hpzpPdMCftEUCezpgpqmmDdYiAjCKXxo=
|
||||
cloud.google.com/go/auth v0.23.2/go.mod h1:4DhBRcqvtljQN3dJ57qtqbib5ZGCYE5f2crfiiC2EM0=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
@@ -46,14 +46,14 @@ cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCB
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
|
||||
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||
cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
|
||||
cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
|
||||
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
|
||||
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
||||
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
|
||||
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
||||
cloud.google.com/go/iam v1.12.0 h1:Aki3bX9aHUDKPHfnRJfDcTdVedvy6quGBQcTqx3DRXk=
|
||||
cloud.google.com/go/iam v1.12.0/go.mod h1:FEZ4lXpADAC2AIpQY7LANNjjwyQ2jK439CI2VaD+sLY=
|
||||
cloud.google.com/go/logging v1.19.0 h1:NCqhdVUg3wQ8Cobdf16FDSuTGi3+6+hdSBHrY5TsR6Q=
|
||||
cloud.google.com/go/logging v1.19.0/go.mod h1:i40NZCHC9Gqvod4yE+yQfDWwlgwW/SrshkkGibCHxcA=
|
||||
cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM=
|
||||
cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0=
|
||||
cloud.google.com/go/monitoring v1.30.0 h1:r/d+JUbyKmJ8b07iznuKfzVzrIXTWxHQ3lBRm3x2LlY=
|
||||
cloud.google.com/go/monitoring v1.30.0/go.mod h1:htlUR0QWVMrjFzZmN4LGnMAve9xB/eduwjmINxVZ8RM=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
@@ -63,10 +63,10 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg=
|
||||
cloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk=
|
||||
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
|
||||
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
|
||||
cloud.google.com/go/storage v1.65.1 h1:LRRpBJUTf+OXDPX9jZUKZ3mSLIsz3htG+qUpeNZovyA=
|
||||
cloud.google.com/go/storage v1.65.1/go.mod h1:UsS9OgFg/XHOSYakQ8ZtLWWeyGkk1WnmD/GsGfN0BHM=
|
||||
cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E=
|
||||
cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
@@ -77,19 +77,19 @@ github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/CycloneDX/cyclonedx-go v0.11.0 h1:GokP8FiRC+foiuwWhSSLpSD5H4hSWtGnR3wo7apkBFI=
|
||||
github.com/CycloneDX/cyclonedx-go v0.11.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8=
|
||||
github.com/CycloneDX/cyclonedx-go v0.12.0 h1:/7Jum36UA6V043tQZ/fE3jf+Nf9gn/qxUFfd7QReMy8=
|
||||
github.com/CycloneDX/cyclonedx-go v0.12.0/go.mod h1:V2577HhxDDCDLYfkm55WJrz16nHTfyQZwcWUBSG7Z28=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
|
||||
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 h1:jLdiS1vO+XJFyDSWRHBx56r4s/NNtcl5J6KyCcWUX/w=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0/go.mod h1:8lmpHY+1VRoteiOwyrQMDt1YGXOrFKCz+1wJW7n3ODY=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0 h1:cSjUzZ7KU8hicTgzaSv9NmSyM9fTVK3y5lsBUl3wOis=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0/go.mod h1:dzcEjy1WJ0Q4u9twNR3LcLhNoYMRCrMCMafpxa0TjPQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 h1:RoO5+d7uCmDqovLrHCr2/BuViUXvdcrNxyNM1pN9dDQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0/go.mod h1:YqwkQPrWSC7+byyc1VlKbWLBF5JsW5IoL6xUkemYSXk=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
||||
@@ -136,18 +136,18 @@ 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.1.0 h1:Q8dc208/HYzCqhx0L1zurfm1UPil24hlo9NjkdFmLdE=
|
||||
github.com/anchore/go-rpmdb v0.1.0/go.mod h1:eQVa6QFGzKy0qMcnW2pez0XBczvgwSjw9vA23qifEyU=
|
||||
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-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.1 h1:91SZ+YqUIIHmf2jPAYZPuHqM/ZqnK1pVJDWtET6+AJE=
|
||||
github.com/anchore/go-sync v0.1.1/go.mod h1:3haGsk2BnaoVhsfqae8Kd0FKIAILE1Hw69P4Xo5iVBw=
|
||||
github.com/anchore/go-sync v0.1.2 h1:RDD5RCanrWqhbtBulqPSZpERadUlgcYjpybYzYd3Uk8=
|
||||
github.com/anchore/go-sync v0.1.2/go.mod h1:grjZH059bCHM6f6jh+Y7VYvX/q8YcjkDJqG6nplHGkE=
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods=
|
||||
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.2.2 h1:SGTLGoF6GHmKEn9Bb6LYpzzgz9nhvMZgN/c4fTSQjYY=
|
||||
github.com/anchore/stereoscope v0.2.2/go.mod h1:ylJXJKebLctP7u9ewguB1d0zUETJxvAA7r2DiuljDTM=
|
||||
github.com/anchore/stereoscope v0.3.2 h1:A2FDMSU56m3rErO1e0CtWAVcDS8tc2iX4UXTw1hyPnI=
|
||||
github.com/anchore/stereoscope v0.3.2/go.mod h1:/IUDVDka+4gufc8kDw+JO3gFl9PkpCjJ2r3bd9UWouM=
|
||||
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=
|
||||
@@ -169,44 +169,42 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aws/aws-sdk-go-v2 v1.44.0 h1:4IbaHhtzy+4h37z4JQyO9a2QsiCml3CNYHtq5hIHigo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.44.0/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 h1:GPRlPwz40I2B2VrBEASOA3Bi77NyeqejNLkifosX0rs=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20/go.mod h1:g7PNzKcsOKWb4fkSRBA7BZVAS6Y8IcxzN+nRohhQ1Q8=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.40 h1:lAVC9gMmKusmqDRe32dPtgKl/BWvJmMJoWELKHCAObw=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.40/go.mod h1:8xOJLbe/hOj1g4PVsfJYV7O2byq+UGET1onDdUgbwqc=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.39 h1:XOg8LC3Kgnsa3WiPQjc7Bi8k5IBN92cPYfIV9XMFss0=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.39/go.mod h1:GonTDBQ+mTpCVNwaHjj0PagspfrYYMEqOx7FehoEP/I=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.40 h1:r5aGipEVgI9aT/tAGjdrPbDQvIAKdTrS3rUPQtG4Rmo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.40/go.mod h1:vOD3CnPxAdkL6MWZeROkZsTlskklMFfgVFkHzx/oZpY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40 h1:UIXlbijuB2XK1Kr57fo8iIxCuaSHJzwZ1uo+2tbEYIk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40/go.mod h1:wcEsL6jscjZjVUinb0Q5qD/GXOG1yT3GNfmT9HuDwzU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40 h1:xLQVRDs2NddDmK9BEyh5KSlJ1Gpy5/GIJXrV6WcVGAE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40/go.mod h1:XRXnpFVFGLaEVK+olDdFIM1vNa04ETW452oFGEPUxAo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.41 h1:nv/ILuCY0yXACzMQwvtt/HbqDDjemZiI0AeDbxGQlnU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.41/go.mod h1:dzvOSpxaPqQ3j0xS6Lc1vyVuWW0RBj7s/QqYpzu3Q/0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 h1:bAdDl/HkGCcGPoe25ToSHEw23VIxt6CT5fLcg111BKg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19/go.mod h1:KaUzbLxv4CeSxh6ZCl9B4m7CuFenS8kUEaDs+f/DQr4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.10.0 h1:U8/A0RRBaEspzH1uul3JHLbypXwEGUkRkvoT9f0ATcM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.10.0/go.mod h1:UELStX5KwtJNtQxa+UuF8dc3z4UYc40e8yHYJSozNwY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40 h1:gr3Fw1cxZXNCdeo/lQ7isHEHzvHVM7z75qb2zW9aMjw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40/go.mod h1:8z/9CmfnQhiuXD7Ykbcg4a/whSWsniE0ODSx9uwVzfk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.41 h1:Q9DIKDuJix/oJnQxFpQ26L0EwVa/YNo4k2kbktrjQjE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.41/go.mod h1:x+TuqkOIG1SZS0+yN54sExGA9ZpjhPO6vPdYnpTFX1M=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.108.0 h1:Yp+x5PKXEmoqHsgP/pAkBy5Tyq1UlXAzM0OInh0vxWw=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.108.0/go.mod h1:locV6DtXyp7Xzr2BG6jtsbeBi3YAWJ/CY4xUThYmIwQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.6.0 h1:agcr0j8YeFEzdXNo17Rg9MbbjLRjrimabwNtji4e+lU=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.6.0/go.mod h1:qU5PxgQ4JiUOOMotzfO3+5oUda5W+8JDVKyLQqlrJik=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.34.0 h1:FxaN8/sn61DTXNI6Gt678tFJUY8iUsCchm6Y/F/RjaA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.34.0/go.mod h1:vu4OY6s8LJtT8BtYG2LD6BGSZMptkYn3o5hvCPB22jc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.39.0 h1:crWKPeGYTBTuBxQ3p73kjfJvt4brUIsr+Fuypko8FxY=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.39.0/go.mod h1:HjjZVhaBz0JBR/kbWKThmNDhFKS7y6EURuk493tJk9Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.46.0 h1:IZ63JdogSNNjex/jsODNv7jGDcO/xJYd9FsgyfCsp1g=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.46.0/go.mod h1:I+rwAf3spG5dITBaAo3xXRowk8kiOhtU1kYxfvCTC44=
|
||||
github.com/aws/smithy-go v1.28.1 h1:R/nXH00c8qcfCzQVELtRw+eLQWtzv+VAIEFJ1/xxXlQ=
|
||||
github.com/aws/smithy-go v1.28.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||
@@ -290,14 +288,14 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
|
||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
||||
github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ=
|
||||
github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=
|
||||
github.com/containerd/containerd/api v1.11.1 h1:h8nfoDW9+fNsC/9TwiAHj8B1GzXKtR4eFtkhi/X5RLU=
|
||||
github.com/containerd/containerd/api v1.11.1/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw=
|
||||
github.com/containerd/containerd/v2 v2.3.2 h1:eLven1YxRMkeiKu7IcMrPKE+gn8sGR1DqHbbshMEvWM=
|
||||
github.com/containerd/containerd/v2 v2.3.2/go.mod h1:rHKGm3VW6wNrINb3x8mNT+w7qYXFVElTt/8HTuxVhD4=
|
||||
github.com/containerd/containerd/v2 v2.3.5 h1:9MYlI81gUcOZ0WsCkSMtvOU7rTR3hqAoa2eCzhoLlkA=
|
||||
github.com/containerd/containerd/v2 v2.3.5/go.mod h1:RXDyLPaI3zoO7dFdAW9/54W4cix+z3A6larieufC9mg=
|
||||
github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg=
|
||||
github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
@@ -308,8 +306,8 @@ github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY
|
||||
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4=
|
||||
github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=
|
||||
github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8oyaRarxdmSU=
|
||||
github.com/containerd/platforms v1.0.0-rc.5/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=
|
||||
github.com/containerd/plugin v1.1.0 h1:O+7lczNJVMy8rz0YNx3xGB8tTf5qY4i5abF041Ew19U=
|
||||
github.com/containerd/plugin v1.1.0/go.mod h1:qBTum+A8lJ6lO44A19Eo7y1OlcLj4OWFH1DA/vnHmcc=
|
||||
github.com/containerd/ttrpc v1.2.8 h1:xbVu6D4qF2jihdh9rDVOKqUMiFBQk6YctTdo1zk087Y=
|
||||
@@ -333,18 +331,18 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc
|
||||
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da h1:ZOjWpVsFZ06eIhnh4mkaceTiVoktdU67+M7KDHJ268M=
|
||||
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da/go.mod h1:B3tI9iGHi4imdLi4Asdha1Sc6feLMTfPLXh9IUYmysk=
|
||||
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
|
||||
github.com/diskfs/go-diskfs v1.9.3 h1:cLciNCeZ4QAXVxyPJDr1ZJ9N9CCG3rQlQ/z/Cs/cNDM=
|
||||
github.com/diskfs/go-diskfs v1.9.3/go.mod h1:TePJORO83Adh5pb2SqsxAwaP0fofFxKLkxctiS/9OQc=
|
||||
github.com/diskfs/go-diskfs v1.9.4 h1:0j2d7eG4IjyxL6+ChWbDPocdBCF6HQ4HBWU2WDYWVnc=
|
||||
github.com/diskfs/go-diskfs v1.9.4/go.mod h1:TePJORO83Adh5pb2SqsxAwaP0fofFxKLkxctiS/9OQc=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/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.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs=
|
||||
github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.8.0+incompatible h1:ih0c2jq/nN7QfES8zIfwSzIhRScjs4ehER+kZ60aeSk=
|
||||
github.com/docker/cli v29.8.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
|
||||
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
||||
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
|
||||
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
|
||||
@@ -371,14 +369,14 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.
|
||||
github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/erofs/go-erofs v0.3.0 h1:o/W5ABAA3sHYl97WL93dacKEfeDpJhdFf3c2snAti7I=
|
||||
@@ -389,8 +387,8 @@ github.com/facebookincubator/nvdtools v0.1.5/go.mod h1:Kh55SAWnjckS96TBSrXI99KrE
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
|
||||
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
|
||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||
@@ -403,27 +401,27 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
|
||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
|
||||
github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/github/go-spdx/v2 v2.7.0 h1:GzfXx4wFdlilARxmFRXW/mgUy3A4vSqZocCMFV6XFdQ=
|
||||
github.com/github/go-spdx/v2 v2.7.0/go.mod h1:Ftc45YYG1WzpzwEPKRVm9Jv8vDqOrN4gWoCkK+bHer0=
|
||||
github.com/gkampitakis/ciinfo v0.3.4 h1:5eBSibVuSMbb/H6Elc0IIEFbkzCJi3lm94n0+U7Z0KY=
|
||||
github.com/gkampitakis/ciinfo v0.3.4/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
|
||||
github.com/gkampitakis/go-snaps v0.5.22 h1:xg9omphRnbDnimMCl1KqznC4krlxOGpkB0vDSfX2P7M=
|
||||
github.com/gkampitakis/go-snaps v0.5.22/go.mod h1:uy3lVzCCRRsAwYqSocyw5fY8xRLCYEfqoOJNxr8HonM=
|
||||
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/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.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
|
||||
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
||||
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-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.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
|
||||
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
||||
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-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=
|
||||
@@ -509,8 +507,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU=
|
||||
github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE=
|
||||
github.com/google/go-containerregistry v0.22.1 h1:RZuuSYhTvlDvtsK+NkutoCZ//C0X2ebLK8X8l3ULs84=
|
||||
github.com/google/go-containerregistry v0.22.1/go.mod h1:bJR35SK8XgisYmhg/FMQ/5RK0S/XrOAqLBV5/LR2XE0=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs=
|
||||
github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY=
|
||||
@@ -537,32 +535,32 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
|
||||
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.20 h1:t/xL64VUoN69MuMRQuJETqYGOw4Z9mSRJK9epIEtwFk=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.20/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
||||
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
||||
github.com/googleapis/gax-go/v2 v2.24.0 h1:myMaPYyF9MecEmvQqMqomIwn9t/4KCZN9qnwsS76wlg=
|
||||
github.com/googleapis/gax-go/v2 v2.24.0/go.mod h1:IaTHBDd7NHxSCiu0vEs8pQZu4dGZrWwuSoxCnk16OFM=
|
||||
github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
|
||||
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
|
||||
github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
|
||||
github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
|
||||
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/gpustack/gguf-parser-go v0.24.1 h1:nTYtL8HFK6ZhB90RKBu4oX2b3ZHpJLrMmKRfL9w9Cyc=
|
||||
github.com/gpustack/gguf-parser-go v0.24.1/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=
|
||||
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/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 h1:vTCWu1wbdYo7PEZFem/rlr01+Un+wwVmI7wiegFdRLk=
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72/go.mod h1:Vn+BBgKQHVQYdVQ4NZDICE1Brb+JfaONyDHr3q07oQc=
|
||||
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=
|
||||
github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
|
||||
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -572,8 +570,8 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-getter v1.8.6 h1:9sQboWULaydVphxc4S64oAI4YqpuCk7nPmvbk131ebY=
|
||||
github.com/hashicorp/go-getter v1.8.6/go.mod h1:nVH12eOV2P58dIiL3rsU6Fh3wLeJEKBOJzhMmzlSWoo=
|
||||
github.com/hashicorp/go-getter v1.8.9 h1:1AOTMUmz/S/GuqOTE6+bg6nwPXTEbKr3Tc/T/lVS7is=
|
||||
github.com/hashicorp/go-getter v1.8.9/go.mod h1:qI7vH/m552bXutKe4UkWptOJl4bo2KeO/CSOoh4s0ps=
|
||||
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
@@ -589,8 +587,8 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
|
||||
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
@@ -624,8 +622,8 @@ github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8i
|
||||
github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jedib0t/go-pretty/v6 v6.8.1 h1:0fkCNhjrX0zPpwkWaDYU5VMrygg41Tu197mWILIJoqQ=
|
||||
github.com/jedib0t/go-pretty/v6 v6.8.1/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
|
||||
github.com/jedib0t/go-pretty/v6 v6.8.3 h1:yVSk5aemoYHCvcrtqyXklwqcgHQIQzmy/oUzFlmffSQ=
|
||||
github.com/jedib0t/go-pretty/v6 v6.8.3/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
@@ -645,8 +643,8 @@ 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.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
|
||||
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/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=
|
||||
@@ -670,8 +668,8 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
|
||||
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/magiconair/properties v1.18.11 h1:j5ozYZl0zCjG7ahMDH0GWIobOvvUzT0BdAguG0ViKy0=
|
||||
github.com/magiconair/properties v1.18.11/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/maruel/natural v1.3.0 h1:VsmCsBmEyrR46RomtgHs5hbKADGRVtliHTyCOLFBpsg=
|
||||
github.com/maruel/natural v1.3.0/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
|
||||
@@ -688,8 +686,8 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
|
||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||
@@ -723,10 +721,10 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
|
||||
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
|
||||
github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
|
||||
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
|
||||
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
|
||||
github.com/moby/moby/api v1.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ=
|
||||
github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
|
||||
github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs=
|
||||
github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ=
|
||||
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
||||
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
@@ -786,8 +784,8 @@ github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrp
|
||||
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
|
||||
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
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=
|
||||
@@ -902,14 +900,14 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
|
||||
github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
|
||||
github.com/spiffe/go-spiffe/v2 v2.7.0 h1:uXe1MflJoHw58wAUvxVlcM7WpKtijWG7I1UidcGh6g4=
|
||||
github.com/spiffe/go-spiffe/v2 v2.7.0/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
@@ -920,8 +918,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
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=
|
||||
@@ -931,8 +929,8 @@ 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=
|
||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=
|
||||
github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo=
|
||||
github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw=
|
||||
github.com/terminalstatic/go-xsd-validate v0.1.8 h1:UVrTCy1j3DhwaYTTUF+QYO/Nan13S0tf+Jwi+p45Bf0=
|
||||
github.com/terminalstatic/go-xsd-validate v0.1.8/go.mod h1:1kb47fi2c6onlf+B7UrrQ9VYraOhcYwFm3iG+J6F4Zo=
|
||||
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
|
||||
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
@@ -947,8 +945,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.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/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/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/vifraa/gopom v1.0.0 h1:L9XlKbyvid8PAIK8nr0lihMApJQg/12OBvMA28BcWh0=
|
||||
@@ -998,32 +996,35 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
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=
|
||||
@@ -1040,8 +1041,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
|
||||
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1081,8 +1082,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
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/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1127,8 +1128,8 @@ golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1160,8 +1161,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1236,13 +1237,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1253,13 +1254,13 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -1315,8 +1316,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
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=
|
||||
@@ -1357,8 +1358,8 @@ google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdr
|
||||
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
|
||||
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
|
||||
google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
|
||||
google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY=
|
||||
google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q=
|
||||
google.golang.org/api v0.294.0 h1:8gASjJxdtcIieB3OqbkLcF0FfbXVNqKtU5iozD1ssvA=
|
||||
google.golang.org/api v0.294.0/go.mod h1:02qB8+Ox1ZFzcaKFMguy1nQLJmSIyvV6Ff4txJEXtl4=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -1429,12 +1430,12 @@ google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ6
|
||||
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d h1:C9v1o0/4quuhOAfmRXA2j+we0PqZIp8traLdeogF3Ms=
|
||||
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d/go.mod h1:Wz2wFJntZFmLGo7pLDXZ3wYk5hyc0Mb+SkHhDDXT+lU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d h1:QwnJwPte4XXAkhPu26LTDIahnsMSUV0kK8HkxbC+Pc4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d/go.mod h1:WRrQ7/7N19PypuT0fxLOL5Lq0waoiRri4FbtHDEKrGE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -1462,8 +1463,8 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
|
||||
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
@@ -1478,8 +1479,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -1513,30 +1514,30 @@ 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.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
|
||||
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
|
||||
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
|
||||
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/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=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
|
||||
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
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.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
|
||||
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
|
||||
modernc.org/libc v1.75.6 h1:yKk8qo+Di4gkmvRboK8ocCqH22FiUCR6jRy2OwtCRus=
|
||||
modernc.org/libc v1.75.6/go.mod h1:bO5o2ztHxBb2rjz0PgdHN0sSMw57CgxGFLZ3Qd/QpVQ=
|
||||
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.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
|
||||
modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
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.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
|
||||
modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
|
||||
modernc.org/sqlite v1.58.0 h1:38u40/bwkfM7f0Myhosl+SEMltSDxnGdQf8o6Kjmys0=
|
||||
modernc.org/sqlite v1.58.0/go.mod h1:rsD2CckafgObKC4DhBlGBf+RiHxkc3hINGt1Xw32tVY=
|
||||
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=
|
||||
|
||||
@@ -15,7 +15,9 @@ application: # AUTO-GENERATED - application-level config keys
|
||||
- key: dotnet.relax-dll-claims-when-bundling-detected
|
||||
description: show all packages from the deps.json if bundling tooling is present as a dependency (e.g. ILRepack)
|
||||
- key: golang.capture-symbols
|
||||
description: 'capture function symbols from the binary symbol table (pclntab). valid values are: "none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib)'
|
||||
description: 'capture function symbols from the binary symbol table (pclntab). valid values are: "none" (disabled), "stdlib" (only the synthetic stdlib package), "extended-stdlib" (stdlib plus every module under golang.org/x/), and "all" (all module packages plus stdlib)'
|
||||
- key: golang.capture-symbols-modules
|
||||
description: glob patterns matched against go module paths (e.g. github.com/klauspost/**) that should have symbols captured in addition to whatever capture-symbols selects. ** crosses path separators, * does not. a trailing major version suffix is ignored when matching, so github.com/foo/* covers github.com/foo/bar/v2; spelling a suffix out in the pattern selects only that major version. this can only widen the selection, never narrow it, and is inert when capture-symbols is none
|
||||
- key: golang.local-mod-cache-dir
|
||||
description: specify an explicit go mod cache directory, if unset this defaults to $GOPATH/pkg/mod or $HOME/go/pkg/mod
|
||||
- key: golang.local-vendor-dir
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package file
|
||||
|
||||
import "io"
|
||||
|
||||
// ReaderSize reports the number of bytes behind r. The bool is false when that cannot be determined, and
|
||||
// callers must branch on it rather than treating the zero as a size.
|
||||
//
|
||||
// Binary parsers read sizes out of the files they are parsing and allocate against them. Those sizes are
|
||||
// 32- and 64-bit fields under the control of whoever produced the file, so a truncated or hostile input
|
||||
// can declare gigabytes that are not there. The read would fail afterwards either way; what matters is
|
||||
// refusing before the allocation, and that needs a real byte count to weigh the claim against.
|
||||
//
|
||||
// The bool is the whole point of the signature. An earlier version returned 0 for every failure mode, and
|
||||
// a caller that forgot to special-case it got a bound of "nothing is allowed" or, more often, silently
|
||||
// fell back to an unbounded path. Making the caller name the failure keeps that from happening by
|
||||
// omission.
|
||||
//
|
||||
// Both *bytes.Reader and *io.SectionReader answer directly. A file handle only has Seek, so fall back to
|
||||
// a save-and-restore seek, which leaves the caller's cursor where it found it. That fallback is not
|
||||
// atomic: it moves the cursor and puts it back, so a reader being read concurrently can observe the
|
||||
// intermediate position.
|
||||
//
|
||||
// Neither answer is taken on faith, because a reader can report a length it cannot deliver: an
|
||||
// *io.SectionReader answers with the length it was constructed with, and debug/elf and friends construct
|
||||
// theirs with a nominal one, so io.NewSectionReader(r, 0, 1<<63-1) would otherwise report a bound that
|
||||
// permits everything as if it had been measured. The last byte is read back to confirm the count, which is
|
||||
// one ReadAt whatever the size, and the read is what the caller was going to weigh anyway.
|
||||
//
|
||||
// A wrapper that embeds io.ReaderAt as an interface promotes only ReadAt, so it answers no size here
|
||||
// however sizable the reader underneath it is. A type meant to be bounded needs to forward Size() itself.
|
||||
func ReaderSize(r io.ReaderAt) (int64, bool) {
|
||||
size, ok := reportedSize(r)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
// ReadAt may return io.EOF alongside a full read, so the count is what says the byte is there
|
||||
var last [1]byte
|
||||
if n, _ := r.ReadAt(last[:], size-1); n != len(last) {
|
||||
return 0, false
|
||||
}
|
||||
return size, true
|
||||
}
|
||||
|
||||
// reportedSize is what the reader says about itself, before ReaderSize checks whether it can back it up.
|
||||
func reportedSize(r io.ReaderAt) (int64, bool) {
|
||||
if sr, ok := r.(interface{ Size() int64 }); ok {
|
||||
size := sr.Size()
|
||||
return size, size > 0
|
||||
}
|
||||
s, ok := r.(io.Seeker)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
cur, err := s.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
size, err := s.Seek(0, io.SeekEnd)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
if _, err := s.Seek(cur, io.SeekStart); err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return size, size > 0
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReaderSize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
r io.ReaderAt
|
||||
want int64
|
||||
wantOK bool
|
||||
}{
|
||||
{
|
||||
name: "bytes.Reader answers directly",
|
||||
r: bytes.NewReader(make([]byte, 1234)),
|
||||
want: 1234,
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
// an empty reader is not a size to bound against, so it reports not-ok along with everything
|
||||
// else that cannot be measured
|
||||
name: "empty bytes.Reader",
|
||||
r: bytes.NewReader(nil),
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "SectionReader over a real length",
|
||||
r: io.NewSectionReader(bytes.NewReader(make([]byte, 500)), 100, 300),
|
||||
want: 300,
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "seek-only reader falls back to seeking",
|
||||
r: seekOnly{bytes.NewReader(make([]byte, 4096))},
|
||||
want: 4096,
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "reader with neither Size nor Seek cannot be measured",
|
||||
r: readAtOnly{bytes.NewReader(make([]byte, 4096))},
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
// the shape debug/elf, debug/pe and debug/macho build internally. Taking this at its word hands
|
||||
// back a bound that permits everything, reported as if it had been measured.
|
||||
name: "SectionReader over a nominal length reports what it can deliver",
|
||||
r: io.NewSectionReader(bytes.NewReader(make([]byte, 500)), 0, 1<<63-1),
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
// a Size() a type simply got wrong is the same failure: the bytes are what decides
|
||||
name: "a size the reader cannot back up is refused",
|
||||
r: overstatedSize{ReaderAt: bytes.NewReader(make([]byte, 10)), size: 1 << 30},
|
||||
want: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
size, ok := ReaderSize(tt.r)
|
||||
assert.Equal(t, tt.want, size)
|
||||
assert.Equal(t, tt.wantOK, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReaderSize_FileHandle(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "f.bin")
|
||||
require.NoError(t, os.WriteFile(path, make([]byte, 7777), 0o600))
|
||||
f, err := os.Open(path)
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
// the cursor has to come back where it was found: the caller may be mid-read, and an *os.File is the
|
||||
// one input here that carries a shared position
|
||||
_, err = f.Seek(101, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
|
||||
size, ok := ReaderSize(f)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, int64(7777), size)
|
||||
|
||||
at, err := f.Seek(0, io.SeekCurrent)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(101), at, "measuring the file must not move the caller's cursor")
|
||||
}
|
||||
|
||||
// TestReaderSize_WrapperMustForwardSize pins the shape that made every bound expressed against this
|
||||
// helper go silently inert. A type that embeds io.ReaderAt as an interface promotes only ReadAt, so it
|
||||
// answers 0 here however sizable the reader underneath is, and a caller that treats 0 as "fall back to
|
||||
// unbounded" then stops bounding anything. Forwarding Size() is what fixes it, and this is the assertion
|
||||
// that a new wrapper has to satisfy.
|
||||
func TestReaderSize_WrapperMustForwardSize(t *testing.T) {
|
||||
inner := bytes.NewReader(make([]byte, 2048))
|
||||
|
||||
size, ok := ReaderSize(embedsReaderAt{inner})
|
||||
assert.False(t, ok, "a wrapper that only embeds the interface cannot be measured")
|
||||
assert.Equal(t, int64(0), size)
|
||||
|
||||
size, ok = ReaderSize(forwardsSize{inner})
|
||||
assert.True(t, ok, "a wrapper meant to be bounded has to forward Size itself")
|
||||
assert.Equal(t, int64(2048), size)
|
||||
}
|
||||
|
||||
type seekOnly struct{ inner *bytes.Reader }
|
||||
|
||||
func (s seekOnly) ReadAt(p []byte, off int64) (int, error) { return s.inner.ReadAt(p, off) }
|
||||
func (s seekOnly) Seek(off int64, whence int) (int64, error) {
|
||||
return s.inner.Seek(off, whence)
|
||||
}
|
||||
|
||||
type readAtOnly struct{ inner *bytes.Reader }
|
||||
|
||||
func (r readAtOnly) ReadAt(p []byte, off int64) (int, error) { return r.inner.ReadAt(p, off) }
|
||||
|
||||
type embedsReaderAt struct{ io.ReaderAt }
|
||||
|
||||
type forwardsSize struct{ io.ReaderAt }
|
||||
|
||||
func (f forwardsSize) Size() int64 {
|
||||
size, _ := ReaderSize(f.ReaderAt)
|
||||
return size
|
||||
}
|
||||
|
||||
// overstatedSize answers with a size larger than the bytes behind it, the way a wrapper that returns a
|
||||
// declared length rather than a measured one would.
|
||||
type overstatedSize struct {
|
||||
io.ReaderAt
|
||||
size int64
|
||||
}
|
||||
|
||||
func (o overstatedSize) Size() int64 { return o.size }
|
||||
@@ -0,0 +1,330 @@
|
||||
// Package spillbuf provides a sparse, offset-addressed byte buffer that keeps a bounded amount in memory
|
||||
// and spills the rest to a temp file.
|
||||
//
|
||||
// It exists for the shape where output arrives out of order, at offsets the input itself declares: a
|
||||
// decompressor placing extents, an archive rebuilding a file from chunks. A plain []byte cannot do that
|
||||
// without either pre-sizing to a length the input claims or growing to the furthest offset it names, and
|
||||
// both hand a hostile input an allocation knob.
|
||||
//
|
||||
// The buffer reports only what it actually stored. Size is the contiguous run written from offset zero,
|
||||
// and a read past it is io.EOF rather than the zeros an unwritten region would otherwise hand back. That
|
||||
// distinction is the whole point: sparse storage serves holes as zeros for free, so a reader that reported
|
||||
// a length covering them would let a write offset stand in for real output. Anything sizing an allocation
|
||||
// against this reader (debug/elf's saferio does exactly that) would then allocate bytes nothing produced.
|
||||
//
|
||||
// Allocation is bounded by the memory limit rather than by how much is written: filling 1MB and filling
|
||||
// 64MB cost about the same, because everything past the limit goes to the file. Only memory is bounded
|
||||
// here: how much ends up on disk is whatever the caller writes, so bounding that stays the caller's job.
|
||||
package spillbuf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"slices"
|
||||
|
||||
"github.com/anchore/syft/internal/tmpdir"
|
||||
)
|
||||
|
||||
// DefaultMemLimit is how much a buffer keeps in memory before it spills, when the caller does not say.
|
||||
// Small on purpose: the memory tier saves a temp file for the common small case, it is not a place to
|
||||
// hold output. A limit large enough to matter is a limit large enough to be an amplification path.
|
||||
const DefaultMemLimit int64 = 1 << 20 // 1MB
|
||||
|
||||
// ErrNoTempDir is returned when a buffer needs to spill and has nowhere to spill to.
|
||||
var ErrNoTempDir = errors.New("no temp dir available to spill to")
|
||||
|
||||
// Buffer is a sparse, offset-addressed buffer. The zero value is not usable; call New.
|
||||
//
|
||||
// Storage is two tiers with a fixed boundary: bytes below memLimit live in mem, bytes at or past it live
|
||||
// in file. Nothing ever moves between them, so an offset belongs to exactly one tier for the life of the
|
||||
// buffer and the only case needing care is a range that straddles the boundary.
|
||||
//
|
||||
// Not safe for concurrent use. Every consumer writes from a single goroutine, and guarding it would cost
|
||||
// each write an uncontended lock for no reader.
|
||||
type Buffer struct {
|
||||
td *tmpdir.TempDir
|
||||
memLimit int64
|
||||
|
||||
// mem holds [0, memLimit). Grown geometrically to fit what has been written, never past the limit.
|
||||
mem []byte
|
||||
|
||||
// file holds [memLimit, ...), created on the first write that reaches there, so a buffer that stays
|
||||
// small never touches disk. remove deletes it on Close.
|
||||
file *os.File
|
||||
remove func()
|
||||
|
||||
// written is the set of ranges stored, sorted by start and merged, so runs of adjacent writes collapse
|
||||
// to one entry rather than growing per call. Everything the buffer reports comes from it.
|
||||
written []extent
|
||||
|
||||
closed bool
|
||||
}
|
||||
|
||||
// extent is a half-open byte range that has been written.
|
||||
//
|
||||
// Unexported on purpose. What a caller needs to know about the contents is answered by Size and FirstGap;
|
||||
// handing out the bookkeeping invites re-deriving those at the call site and getting them subtly wrong,
|
||||
// which is the bug this package exists to prevent.
|
||||
type extent struct {
|
||||
start, end int64
|
||||
}
|
||||
|
||||
// Option configures a Buffer.
|
||||
type Option func(*Buffer)
|
||||
|
||||
// WithMemLimit sets how many bytes are held in memory before the buffer spills to disk. Zero sends every
|
||||
// write to the temp file. Negative is treated as zero.
|
||||
//
|
||||
// Peak allocation is a small multiple of this, since growing the tier holds the old copy alongside the
|
||||
// new one and the new one may be rounded up past what was asked for.
|
||||
func WithMemLimit(n int64) Option {
|
||||
return func(b *Buffer) {
|
||||
b.memLimit = max(n, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// New returns a buffer that spills into td.
|
||||
//
|
||||
// td may be nil only when every write is known to stay inside the memory limit; a write past it then
|
||||
// fails with ErrNoTempDir rather than allocating. Nothing is created on disk until a write needs it.
|
||||
func New(td *tmpdir.TempDir, opts ...Option) *Buffer {
|
||||
b := &Buffer{td: td, memLimit: DefaultMemLimit}
|
||||
for _, opt := range opts {
|
||||
opt(b)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// split divides a range of n bytes starting at off across the two tiers, reporting how many bytes fall in
|
||||
// each. The file portion starts at off+inMem.
|
||||
func (b *Buffer) split(off, n int64) (inMem, inFile int64) {
|
||||
if off >= b.memLimit {
|
||||
return 0, n
|
||||
}
|
||||
inMem = min(n, b.memLimit-off)
|
||||
return inMem, n - inMem
|
||||
}
|
||||
|
||||
// fileOffset translates a buffer offset at or past the limit into an offset within the spill file.
|
||||
func (b *Buffer) fileOffset(off int64) int64 { return off - b.memLimit }
|
||||
|
||||
// WriteAt stores p at off. Writes may land anywhere, in any order, and may overlap. A write that fails
|
||||
// leaves the buffer as it was.
|
||||
func (b *Buffer) WriteAt(p []byte, off int64) (int, error) {
|
||||
if b.closed {
|
||||
return 0, fmt.Errorf("spillbuf: write to a closed buffer: %w", os.ErrClosed)
|
||||
}
|
||||
if off < 0 {
|
||||
return 0, fmt.Errorf("spillbuf: negative offset %d", off)
|
||||
}
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
// subtraction form so off+len(p) cannot wrap into a small in-range end
|
||||
if off > math.MaxInt64-int64(len(p)) {
|
||||
return 0, fmt.Errorf("spillbuf: write of %d bytes at %d overflows", len(p), off)
|
||||
}
|
||||
|
||||
inMem, inFile := b.split(off, int64(len(p)))
|
||||
// the fallible half goes first: once the memory tier is stamped the old bytes are gone, so a file
|
||||
// write that fails afterward would report failure over a buffer it had already changed
|
||||
if inFile > 0 {
|
||||
if err := b.writeFile(p[inMem:], b.fileOffset(off+inMem)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if inMem > 0 {
|
||||
b.writeMem(p[:inMem], off)
|
||||
}
|
||||
|
||||
b.record(extent{start: off, end: off + int64(len(p))})
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (b *Buffer) writeMem(p []byte, off int64) {
|
||||
if need := off + int64(len(p)); int64(len(b.mem)) < need {
|
||||
// geometric, capped at the limit. Sizing to exactly what each write needs looks tidier and is
|
||||
// quadratic: a caller streaming a block in 32KB chunks reallocates and copies the whole tier every
|
||||
// chunk, so filling 1MB costs ~16MB of allocation.
|
||||
grown := min(b.memLimit, max(need, 2*int64(len(b.mem))))
|
||||
// sized to exactly the geometric target rather than grown through append: append rounds the new
|
||||
// capacity up past what was asked for, and the tier is already capped, so the slack is pure waste
|
||||
mem := make([]byte, grown)
|
||||
copy(mem, b.mem)
|
||||
b.mem = mem
|
||||
}
|
||||
copy(b.mem[off:], p)
|
||||
}
|
||||
|
||||
func (b *Buffer) writeFile(p []byte, off int64) error {
|
||||
if b.file == nil {
|
||||
if b.td == nil {
|
||||
return ErrNoTempDir
|
||||
}
|
||||
f, remove, err := b.td.NewFile("syft-spill-*.bin") //nolint:gocritic // removal happens in Close
|
||||
if err != nil {
|
||||
return fmt.Errorf("spillbuf: unable to create spill file: %w", err)
|
||||
}
|
||||
b.file, b.remove = f, remove
|
||||
}
|
||||
if _, err := b.file.WriteAt(p, off); err != nil {
|
||||
return fmt.Errorf("spillbuf: unable to write to spill file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadAt returns bytes this buffer actually stored. A read starting at or past Size is io.EOF, and one
|
||||
// running past it is short, because everything beyond is a hole the buffer never held.
|
||||
func (b *Buffer) ReadAt(p []byte, off int64) (int, error) {
|
||||
if b.closed {
|
||||
return 0, fmt.Errorf("spillbuf: read from a closed buffer: %w", os.ErrClosed)
|
||||
}
|
||||
if off < 0 {
|
||||
return 0, fmt.Errorf("spillbuf: negative offset %d", off)
|
||||
}
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
size := b.Size()
|
||||
if off >= size {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
// bounded by Size, so every byte in [off, off+want) was written and both tiers really hold theirs
|
||||
want := min(int64(len(p)), size-off)
|
||||
inMem, inFile := b.split(off, want)
|
||||
|
||||
// guarded rather than relying on a zero-length copy: when off is past the limit the slice expression
|
||||
// itself is out of range, since mem is only as long as what was written into it
|
||||
if inMem > 0 {
|
||||
copy(p[:inMem], b.mem[off:off+inMem])
|
||||
}
|
||||
if inFile > 0 {
|
||||
if err := readFullAt(b.file, p[inMem:want], b.fileOffset(off+inMem)); err != nil {
|
||||
return int(inMem), fmt.Errorf("spillbuf: unable to read spill file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if want < int64(len(p)) {
|
||||
return int(want), io.EOF
|
||||
}
|
||||
return int(want), nil
|
||||
}
|
||||
|
||||
// readFullAt fills p from r. A short read is an error rather than a short return: the caller has already
|
||||
// bounded the range by what was written, so the file failing to deliver means it is not what we left.
|
||||
func readFullAt(r io.ReaderAt, p []byte, off int64) error {
|
||||
for read := 0; read < len(p); {
|
||||
n, err := r.ReadAt(p[read:], off+int64(read))
|
||||
read += n
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Size reports the contiguous run written from offset zero, which is the length this buffer is willing to
|
||||
// stand behind. Deliberately not the furthest offset written: see the package doc for why the two are not
|
||||
// interchangeable.
|
||||
//
|
||||
// record keeps the set merged and sorted, so the run from zero is at most the first two entries and the
|
||||
// loop stops there: the cost this adds to every ReadAt is effectively constant, not a scan of the set.
|
||||
func (b *Buffer) Size() int64 {
|
||||
var covered int64
|
||||
for _, e := range b.written {
|
||||
if e.start > covered {
|
||||
break
|
||||
}
|
||||
covered = max(covered, e.end)
|
||||
}
|
||||
return covered
|
||||
}
|
||||
|
||||
// FirstGap returns the offset of the earliest unwritten run of at least size bytes lying entirely within
|
||||
// [0, within), and whether there is one. Callers that place output at offsets of their own choosing use it
|
||||
// to fill what they left behind, without needing the buffer's bookkeeping.
|
||||
//
|
||||
// The arithmetic is in subtraction form throughout: within comes from caller input and the result is used
|
||||
// as a write offset, so a wrap here would be a write nobody bounded.
|
||||
func (b *Buffer) FirstGap(size, within int64) (int64, bool) {
|
||||
if b.closed || size < 0 || within < 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
var at int64
|
||||
for _, e := range b.written {
|
||||
// once the cursor is at the bound there is nothing left to offer, and it only moves forward
|
||||
if at >= within {
|
||||
return 0, false
|
||||
}
|
||||
// the gap ahead of this extent ends at whichever comes first, the extent or the bound
|
||||
end := min(e.start, within)
|
||||
if end > at && end-at >= size {
|
||||
return at, true
|
||||
}
|
||||
at = max(at, e.end)
|
||||
}
|
||||
if at <= within && within-at >= size {
|
||||
return at, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// record merges e into the written set, keeping it sorted by start with no overlapping or adjacent pairs.
|
||||
func (b *Buffer) record(e extent) {
|
||||
if e.end <= e.start {
|
||||
return
|
||||
}
|
||||
|
||||
// inserted in place rather than appended and re-sorted: the set is already ordered, so a run of
|
||||
// ascending writes (the common case) stays linear
|
||||
i, _ := slices.BinarySearchFunc(b.written, e, func(x, y extent) int {
|
||||
return cmpInt64(x.start, y.start)
|
||||
})
|
||||
b.written = slices.Insert(b.written, i, e)
|
||||
|
||||
merged := b.written[:1]
|
||||
for _, cur := range b.written[1:] {
|
||||
last := &merged[len(merged)-1]
|
||||
if cur.start <= last.end {
|
||||
last.end = max(last.end, cur.end)
|
||||
continue
|
||||
}
|
||||
merged = append(merged, cur)
|
||||
}
|
||||
b.written = merged
|
||||
}
|
||||
|
||||
func cmpInt64(x, y int64) int {
|
||||
switch {
|
||||
case x < y:
|
||||
return -1
|
||||
case x > y:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Close releases the spill file, if one was ever created, and drops the memory tier. Safe on a nil
|
||||
// receiver and safe to call more than once. Reads and writes after it fail with os.ErrClosed.
|
||||
func (b *Buffer) Close() error {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
b.mem = nil
|
||||
b.written = nil
|
||||
b.closed = true
|
||||
if b.file == nil {
|
||||
return nil
|
||||
}
|
||||
err := b.file.Close()
|
||||
if b.remove != nil {
|
||||
b.remove()
|
||||
}
|
||||
b.file, b.remove = nil, nil
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,872 @@
|
||||
package spillbuf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/internal/tmpdir"
|
||||
)
|
||||
|
||||
func newTestBuffer(t *testing.T, opts ...Option) *Buffer {
|
||||
t.Helper()
|
||||
b := New(tmpdir.FromPath(t.TempDir()), opts...)
|
||||
t.Cleanup(func() { _ = b.Close() })
|
||||
return b
|
||||
}
|
||||
|
||||
func mustWrite(t *testing.T, b *Buffer, off int64, p []byte) {
|
||||
t.Helper()
|
||||
n, err := b.WriteAt(p, off)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(p), n, "WriteAt must report the full write")
|
||||
}
|
||||
|
||||
// readAll returns everything the buffer is willing to hand back.
|
||||
func readAll(t *testing.T, b *Buffer) []byte {
|
||||
t.Helper()
|
||||
out := make([]byte, b.Size())
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
n, err := b.ReadAt(out, 0)
|
||||
require.NoError(t, err, "a read of exactly Size bytes is not short")
|
||||
require.Equal(t, len(out), n)
|
||||
return out
|
||||
}
|
||||
|
||||
func repeat(b byte, n int) []byte { return bytes.Repeat([]byte{b}, n) }
|
||||
|
||||
// --- the core property: report only what was stored -------------------------------------------------
|
||||
|
||||
// TestSizeIsTheContiguousPrefix is the property the package exists for. A sparse buffer hands its holes
|
||||
// back as zeros it never stored, so reporting a length that covers them lets a write offset stand in for
|
||||
// real output: whatever sizes an allocation against this reader then allocates bytes nothing produced.
|
||||
func TestSizeIsTheContiguousPrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
writes []extent // start is the offset, end-start the length
|
||||
wantSize int64
|
||||
}{
|
||||
{
|
||||
name: "nothing written",
|
||||
wantSize: 0,
|
||||
},
|
||||
{
|
||||
name: "one run from zero",
|
||||
writes: []extent{{0, 100}},
|
||||
wantSize: 100,
|
||||
},
|
||||
{
|
||||
name: "a run that does not start at zero covers nothing",
|
||||
writes: []extent{{10, 100}},
|
||||
wantSize: 0,
|
||||
},
|
||||
{
|
||||
name: "adjacent runs join",
|
||||
writes: []extent{{0, 50}, {50, 100}},
|
||||
wantSize: 100,
|
||||
},
|
||||
{
|
||||
name: "a hole stops the prefix",
|
||||
writes: []extent{{0, 50}, {60, 100}},
|
||||
wantSize: 50,
|
||||
},
|
||||
{
|
||||
name: "the hole is filled later",
|
||||
writes: []extent{{0, 50}, {60, 100}, {50, 60}},
|
||||
wantSize: 100,
|
||||
},
|
||||
{
|
||||
name: "out of order still resolves",
|
||||
writes: []extent{{60, 100}, {0, 50}, {50, 60}},
|
||||
wantSize: 100,
|
||||
},
|
||||
{
|
||||
name: "overlapping runs",
|
||||
writes: []extent{{0, 60}, {40, 100}},
|
||||
wantSize: 100,
|
||||
},
|
||||
{
|
||||
name: "a run fully inside another",
|
||||
writes: []extent{{0, 100}, {20, 40}},
|
||||
wantSize: 100,
|
||||
},
|
||||
{
|
||||
name: "far placement is not coverage",
|
||||
writes: []extent{{0, 64}, {1 << 20, 1<<20 + 64}},
|
||||
wantSize: 64,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b := newTestBuffer(t)
|
||||
for _, w := range tt.writes {
|
||||
mustWrite(t, b, w.start, repeat('x', int(w.end-w.start)))
|
||||
}
|
||||
assert.Equal(t, tt.wantSize, b.Size(), "Size is the contiguous prefix")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadPastSizeIsEOFNotZeros is the other half of the same property. Sparse storage returns zeros for
|
||||
// a hole for free; handing those out would make the hole indistinguishable from real content.
|
||||
func TestReadPastSizeIsEOFNotZeros(t *testing.T) {
|
||||
b := newTestBuffer(t)
|
||||
mustWrite(t, b, 0, repeat('A', 64))
|
||||
mustWrite(t, b, 4096, repeat('B', 64)) // past a hole
|
||||
|
||||
require.Equal(t, int64(64), b.Size())
|
||||
|
||||
t.Run("a read starting in the hole is EOF", func(t *testing.T) {
|
||||
p := make([]byte, 16)
|
||||
n, err := b.ReadAt(p, 100)
|
||||
assert.Zero(t, n)
|
||||
assert.ErrorIs(t, err, io.EOF)
|
||||
})
|
||||
|
||||
t.Run("a read starting at the far extent is EOF, not the bytes there", func(t *testing.T) {
|
||||
p := make([]byte, 64)
|
||||
n, err := b.ReadAt(p, 4096)
|
||||
assert.Zero(t, n)
|
||||
assert.ErrorIs(t, err, io.EOF)
|
||||
assert.Equal(t, make([]byte, 64), p, "nothing may be copied out")
|
||||
})
|
||||
|
||||
t.Run("a read running off the end is short and says so", func(t *testing.T) {
|
||||
p := make([]byte, 128)
|
||||
n, err := b.ReadAt(p, 0)
|
||||
assert.Equal(t, 64, n, "only the stored prefix comes back")
|
||||
assert.ErrorIs(t, err, io.EOF)
|
||||
assert.Equal(t, repeat('A', 64), p[:64])
|
||||
assert.Equal(t, make([]byte, 64), p[64:], "the rest is untouched")
|
||||
})
|
||||
}
|
||||
|
||||
// TestSparsePlacementIsNotAnAllocationKnob pins the amplification directly: a tiny payload parked at a
|
||||
// huge offset must not cost memory proportional to the offset.
|
||||
func TestSparsePlacementIsNotAnAllocationKnob(t *testing.T) {
|
||||
const far = 512 << 20 // 512MB out
|
||||
|
||||
b := newTestBuffer(t, WithMemLimit(1<<20))
|
||||
mustWrite(t, b, 0, repeat('A', 64))
|
||||
|
||||
var before, after runtime.MemStats
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&before)
|
||||
mustWrite(t, b, far, repeat('B', 64))
|
||||
runtime.ReadMemStats(&after)
|
||||
|
||||
allocated := after.TotalAlloc - before.TotalAlloc
|
||||
t.Logf("writing 64 bytes at offset %d allocated %d bytes", far, allocated)
|
||||
assert.Less(t, allocated, uint64(1<<20),
|
||||
"a far placement must cost its own bytes, not its offset")
|
||||
assert.Equal(t, int64(64), b.Size(), "and it must not count toward what the buffer will deliver")
|
||||
}
|
||||
|
||||
// --- tiers ------------------------------------------------------------------------------------------
|
||||
|
||||
func TestMemoryTierNeverTouchesDisk(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
b := New(tmpdir.FromPath(dir), WithMemLimit(4096))
|
||||
t.Cleanup(func() { _ = b.Close() })
|
||||
|
||||
mustWrite(t, b, 0, repeat('A', 4096)) // exactly the limit
|
||||
|
||||
assert.Equal(t, repeat('A', 4096), readAll(t, b))
|
||||
assert.Nil(t, b.file, "a buffer that stays inside the limit must not create a file")
|
||||
assert.Empty(t, dirEntries(t, dir), "and must leave nothing on disk")
|
||||
}
|
||||
|
||||
func TestSpillCreatesTheFileOnlyWhenNeeded(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
b := New(tmpdir.FromPath(dir), WithMemLimit(4096))
|
||||
t.Cleanup(func() { _ = b.Close() })
|
||||
|
||||
mustWrite(t, b, 0, repeat('A', 4096))
|
||||
require.Empty(t, dirEntries(t, dir))
|
||||
|
||||
mustWrite(t, b, 4096, repeat('B', 1)) // one byte past the limit
|
||||
assert.NotNil(t, b.file)
|
||||
assert.Len(t, dirEntries(t, dir), 1, "exactly one spill file")
|
||||
}
|
||||
|
||||
func TestWriteStraddlingTheLimit(t *testing.T) {
|
||||
const limit = 1024
|
||||
b := newTestBuffer(t, WithMemLimit(limit))
|
||||
|
||||
payload := make([]byte, 2048)
|
||||
for i := range payload {
|
||||
payload[i] = byte(i % 251)
|
||||
}
|
||||
// starts below the limit and ends well past it, so the write is split across both tiers
|
||||
mustWrite(t, b, limit-512, payload)
|
||||
|
||||
require.Equal(t, int64(0), b.Size(), "nothing was written at zero yet")
|
||||
mustWrite(t, b, 0, repeat('Z', limit-512))
|
||||
|
||||
got := readAll(t, b)
|
||||
require.Len(t, got, limit-512+2048)
|
||||
assert.Equal(t, repeat('Z', limit-512), got[:limit-512], "the memory-only part")
|
||||
assert.Equal(t, payload, got[limit-512:], "the straddling part reads back whole")
|
||||
}
|
||||
|
||||
func TestMemLimitZeroSendsEverythingToDisk(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
b := New(tmpdir.FromPath(dir), WithMemLimit(0))
|
||||
t.Cleanup(func() { _ = b.Close() })
|
||||
|
||||
mustWrite(t, b, 0, repeat('A', 16))
|
||||
assert.Equal(t, repeat('A', 16), readAll(t, b))
|
||||
assert.Len(t, dirEntries(t, dir), 1, "with no memory tier the first byte spills")
|
||||
assert.Nil(t, b.mem)
|
||||
}
|
||||
|
||||
func TestNegativeMemLimitIsTreatedAsZero(t *testing.T) {
|
||||
b := newTestBuffer(t, WithMemLimit(-1))
|
||||
mustWrite(t, b, 0, repeat('A', 8))
|
||||
assert.Equal(t, repeat('A', 8), readAll(t, b))
|
||||
}
|
||||
|
||||
func TestDefaultMemLimitApplies(t *testing.T) {
|
||||
b := New(nil)
|
||||
t.Cleanup(func() { _ = b.Close() })
|
||||
assert.Equal(t, DefaultMemLimit, b.memLimit)
|
||||
}
|
||||
|
||||
// --- no temp dir ------------------------------------------------------------------------------------
|
||||
|
||||
func TestNoTempDir(t *testing.T) {
|
||||
t.Run("a write inside the memory limit needs no temp dir", func(t *testing.T) {
|
||||
b := New(nil, WithMemLimit(4096))
|
||||
t.Cleanup(func() { _ = b.Close() })
|
||||
|
||||
mustWrite(t, b, 0, repeat('A', 4096))
|
||||
assert.Equal(t, repeat('A', 4096), readAll(t, b))
|
||||
})
|
||||
|
||||
t.Run("a write past it fails rather than allocating", func(t *testing.T) {
|
||||
b := New(nil, WithMemLimit(4096))
|
||||
t.Cleanup(func() { _ = b.Close() })
|
||||
|
||||
_, err := b.WriteAt(repeat('B', 1), 4096)
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrNoTempDir)
|
||||
})
|
||||
}
|
||||
|
||||
// --- input validation -------------------------------------------------------------------------------
|
||||
|
||||
func TestWriteAtRejectsBadInput(t *testing.T) {
|
||||
b := newTestBuffer(t)
|
||||
|
||||
t.Run("negative offset", func(t *testing.T) {
|
||||
_, err := b.WriteAt([]byte("x"), -1)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("an offset plus length that would overflow", func(t *testing.T) {
|
||||
_, err := b.WriteAt(repeat('x', 16), math.MaxInt64-8)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "overflow",
|
||||
"the check must be a subtraction, not off+len wrapping into an in-range end")
|
||||
})
|
||||
|
||||
t.Run("an empty write is a no-op, not an extent", func(t *testing.T) {
|
||||
n, err := b.WriteAt(nil, 500)
|
||||
require.NoError(t, err)
|
||||
assert.Zero(t, n)
|
||||
assert.Empty(t, b.written, "a zero-length write records nothing")
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadAtRejectsBadInput(t *testing.T) {
|
||||
b := newTestBuffer(t)
|
||||
mustWrite(t, b, 0, repeat('A', 16))
|
||||
|
||||
t.Run("negative offset", func(t *testing.T) {
|
||||
_, err := b.ReadAt(make([]byte, 4), -1)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("empty read", func(t *testing.T) {
|
||||
n, err := b.ReadAt(nil, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.Zero(t, n)
|
||||
})
|
||||
|
||||
t.Run("read from an empty buffer", func(t *testing.T) {
|
||||
empty := newTestBuffer(t)
|
||||
n, err := empty.ReadAt(make([]byte, 4), 0)
|
||||
assert.Zero(t, n)
|
||||
assert.ErrorIs(t, err, io.EOF)
|
||||
})
|
||||
}
|
||||
|
||||
// TestReadAtHonorsTheReaderAtContract pins what io.ReaderAt requires: a short read must come with a
|
||||
// non-nil error, and a full read must not invent one. Parsers built on io.ReaderAt (debug/elf among them)
|
||||
// rely on this to tell "the file ends here" from "try again".
|
||||
func TestReadAtHonorsTheReaderAtContract(t *testing.T) {
|
||||
b := newTestBuffer(t, WithMemLimit(64))
|
||||
mustWrite(t, b, 0, repeat('A', 200)) // spans both tiers
|
||||
|
||||
for _, size := range []int{1, 63, 64, 65, 199, 200} {
|
||||
p := make([]byte, size)
|
||||
n, err := b.ReadAt(p, 0)
|
||||
assert.Equal(t, size, n, "size=%d", size)
|
||||
assert.NoError(t, err, "a read fully inside the buffer must not report EOF (size=%d)", size)
|
||||
}
|
||||
|
||||
p := make([]byte, 201)
|
||||
n, err := b.ReadAt(p, 0)
|
||||
assert.Equal(t, 200, n)
|
||||
assert.ErrorIs(t, err, io.EOF, "one byte past the end is short and must say so")
|
||||
}
|
||||
|
||||
func TestReadAtEveryOffsetAndLength(t *testing.T) {
|
||||
const limit = 32
|
||||
b := newTestBuffer(t, WithMemLimit(limit))
|
||||
|
||||
want := make([]byte, 100)
|
||||
for i := range want {
|
||||
want[i] = byte(i)
|
||||
}
|
||||
mustWrite(t, b, 0, want)
|
||||
|
||||
// every offset x length pair, so a tier-boundary off-by-one cannot hide
|
||||
for off := 0; off <= len(want); off++ {
|
||||
for l := 0; l <= len(want)-off+2; l++ {
|
||||
p := make([]byte, l)
|
||||
n, err := b.ReadAt(p, int64(off))
|
||||
|
||||
expected := len(want) - off
|
||||
if l < expected {
|
||||
expected = l
|
||||
}
|
||||
assert.Equal(t, expected, n, "off=%d len=%d", off, l)
|
||||
assert.Equal(t, want[off:off+expected], p[:expected], "off=%d len=%d", off, l)
|
||||
|
||||
switch {
|
||||
case l == 0:
|
||||
assert.NoError(t, err, "off=%d len=0", off)
|
||||
case off >= len(want):
|
||||
assert.ErrorIs(t, err, io.EOF, "off=%d len=%d", off, l)
|
||||
case l > expected:
|
||||
assert.ErrorIs(t, err, io.EOF, "off=%d len=%d", off, l)
|
||||
default:
|
||||
assert.NoError(t, err, "off=%d len=%d", off, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- overwrite semantics ----------------------------------------------------------------------------
|
||||
|
||||
func TestOverwrite(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
memLimit int64
|
||||
}{
|
||||
{"in memory", 1 << 20},
|
||||
{"on disk", 0},
|
||||
{"across the boundary", 8},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b := newTestBuffer(t, WithMemLimit(tt.memLimit))
|
||||
mustWrite(t, b, 0, repeat('A', 16))
|
||||
mustWrite(t, b, 4, repeat('B', 8))
|
||||
|
||||
want := append(append(repeat('A', 4), repeat('B', 8)...), repeat('A', 4)...)
|
||||
assert.Equal(t, want, readAll(t, b), "the later write wins over the range it covers")
|
||||
assert.Equal(t, int64(16), b.Size(), "an overwrite does not extend the buffer")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- extents ----------------------------------------------------------------------------------------
|
||||
|
||||
func TestWrittenExtentsAreSortedAndMerged(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
writes []extent
|
||||
want []extent
|
||||
}{
|
||||
{
|
||||
name: "disjoint stay separate, in order",
|
||||
writes: []extent{{100, 150}, {0, 50}},
|
||||
want: []extent{{0, 50}, {100, 150}},
|
||||
},
|
||||
{
|
||||
name: "adjacent merge",
|
||||
writes: []extent{{0, 50}, {50, 100}},
|
||||
want: []extent{{0, 100}},
|
||||
},
|
||||
{
|
||||
name: "overlapping merge",
|
||||
writes: []extent{{0, 60}, {40, 100}},
|
||||
want: []extent{{0, 100}},
|
||||
},
|
||||
{
|
||||
name: "a write bridging two runs collapses all three",
|
||||
writes: []extent{{0, 20}, {80, 100}, {20, 80}},
|
||||
want: []extent{{0, 100}},
|
||||
},
|
||||
{
|
||||
name: "a contained write changes nothing",
|
||||
writes: []extent{{0, 100}, {20, 40}},
|
||||
want: []extent{{0, 100}},
|
||||
},
|
||||
{
|
||||
name: "identical writes collapse",
|
||||
writes: []extent{{0, 10}, {0, 10}, {0, 10}},
|
||||
want: []extent{{0, 10}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b := newTestBuffer(t)
|
||||
for _, w := range tt.writes {
|
||||
mustWrite(t, b, w.start, repeat('x', int(w.end-w.start)))
|
||||
}
|
||||
assert.Equal(t, tt.want, b.written)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrittenExtentsStayCompactUnderManyAdjacentWrites(t *testing.T) {
|
||||
b := newTestBuffer(t)
|
||||
for i := int64(0); i < 1000; i++ {
|
||||
mustWrite(t, b, i*8, repeat('A', 8))
|
||||
}
|
||||
assert.Len(t, b.written, 1, "a run of adjacent writes must collapse to one extent, not 1000")
|
||||
assert.Equal(t, int64(8000), b.Size())
|
||||
}
|
||||
|
||||
// --- lifecycle --------------------------------------------------------------------------------------
|
||||
|
||||
func TestCloseRemovesTheSpillFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
b := New(tmpdir.FromPath(dir), WithMemLimit(0))
|
||||
mustWrite(t, b, 0, repeat('A', 32))
|
||||
require.Len(t, dirEntries(t, dir), 1)
|
||||
|
||||
require.NoError(t, b.Close())
|
||||
assert.Empty(t, dirEntries(t, dir), "Close must take the spill file with it")
|
||||
}
|
||||
|
||||
func TestCloseIsIdempotent(t *testing.T) {
|
||||
b := New(tmpdir.FromPath(t.TempDir()), WithMemLimit(0))
|
||||
mustWrite(t, b, 0, repeat('A', 32))
|
||||
|
||||
require.NoError(t, b.Close())
|
||||
assert.NoError(t, b.Close(), "a second Close is a no-op, not a double-close")
|
||||
assert.NoError(t, b.Close())
|
||||
}
|
||||
|
||||
func TestCloseOnNilReceiver(t *testing.T) {
|
||||
var b *Buffer
|
||||
assert.NotPanics(t, func() {
|
||||
assert.NoError(t, b.Close())
|
||||
})
|
||||
}
|
||||
|
||||
func TestCloseWithoutASpillFile(t *testing.T) {
|
||||
b := New(nil, WithMemLimit(1<<20))
|
||||
mustWrite(t, b, 0, repeat('A', 32))
|
||||
assert.NoError(t, b.Close(), "a memory-only buffer closes cleanly with no file to remove")
|
||||
}
|
||||
|
||||
func TestWriteAfterCloseFails(t *testing.T) {
|
||||
b := New(tmpdir.FromPath(t.TempDir()))
|
||||
mustWrite(t, b, 0, repeat('A', 8))
|
||||
require.NoError(t, b.Close())
|
||||
|
||||
_, err := b.WriteAt(repeat('B', 8), 0)
|
||||
require.Error(t, err, "a closed buffer must refuse writes rather than resurrect its file")
|
||||
assert.ErrorIs(t, err, os.ErrClosed)
|
||||
}
|
||||
|
||||
// TestReadAfterCloseFails pins that a closed buffer reads as closed rather than as empty. Close drops the
|
||||
// extents, so without the check every read would come back io.EOF and a caller could not tell a buffer it
|
||||
// still owns from one somebody already released.
|
||||
func TestReadAfterCloseFails(t *testing.T) {
|
||||
b := New(tmpdir.FromPath(t.TempDir()))
|
||||
mustWrite(t, b, 0, repeat('A', 8))
|
||||
require.NoError(t, b.Close())
|
||||
|
||||
n, err := b.ReadAt(make([]byte, 8), 0)
|
||||
assert.Zero(t, n)
|
||||
assert.ErrorIs(t, err, os.ErrClosed)
|
||||
|
||||
at, ok := b.FirstGap(8, 100)
|
||||
assert.False(t, ok, "and a closed buffer offers nowhere to write")
|
||||
assert.Zero(t, at)
|
||||
}
|
||||
|
||||
// TestWriteFailureLeavesTheBufferUnchanged pins the ordering inside WriteAt: the fallible half runs first,
|
||||
// so a spill file that cannot be created does not take the memory tier down with it.
|
||||
func TestWriteFailureLeavesTheBufferUnchanged(t *testing.T) {
|
||||
// FromPath takes the directory as given, so a path that does not exist makes file creation fail
|
||||
b := New(tmpdir.FromPath(filepath.Join(t.TempDir(), "no-such-dir")), WithMemLimit(16))
|
||||
t.Cleanup(func() { _ = b.Close() })
|
||||
|
||||
mustWrite(t, b, 0, repeat('A', 8))
|
||||
|
||||
_, err := b.WriteAt(repeat('B', 32), 0) // straddles the limit, so it needs the spill file
|
||||
require.Error(t, err)
|
||||
|
||||
assert.Equal(t, int64(8), b.Size(), "a failed write records nothing")
|
||||
assert.Nil(t, b.file, "and leaves no half-made spill file behind")
|
||||
|
||||
p := make([]byte, 8)
|
||||
n, err := b.ReadAt(p, 0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 8, n)
|
||||
assert.Equal(t, repeat('A', 8), p, "the memory tier still holds what it held")
|
||||
|
||||
mustWrite(t, b, 8, repeat('C', 8)) // inside the limit, so it still needs no file
|
||||
assert.Equal(t, append(repeat('A', 8), repeat('C', 8)...), readAll(t, b))
|
||||
}
|
||||
|
||||
// --- interface compliance ---------------------------------------------------------------------------
|
||||
|
||||
func TestBufferSatisfiesTheStandardInterfaces(t *testing.T) {
|
||||
b := newTestBuffer(t)
|
||||
var (
|
||||
_ io.WriterAt = b
|
||||
_ io.ReaderAt = b
|
||||
_ io.Closer = b
|
||||
)
|
||||
assert.Implements(t, (*io.ReaderAt)(nil), b)
|
||||
assert.Implements(t, (*io.WriterAt)(nil), b)
|
||||
}
|
||||
|
||||
// TestWorksThroughIOOffsetWriter pins the composition every consumer uses: stream a decoded block into
|
||||
// the buffer at a chosen offset without the writer knowing where it lands.
|
||||
func TestWorksThroughIOOffsetWriter(t *testing.T) {
|
||||
b := newTestBuffer(t, WithMemLimit(16))
|
||||
|
||||
w := io.NewOffsetWriter(b, 8)
|
||||
n, err := io.Copy(w, bytes.NewReader(repeat('B', 32)))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(32), n)
|
||||
|
||||
mustWrite(t, b, 0, repeat('A', 8))
|
||||
|
||||
got := readAll(t, b)
|
||||
assert.Equal(t, append(repeat('A', 8), repeat('B', 32)...), got)
|
||||
}
|
||||
|
||||
// TestWorksWithIOSectionReader pins the other direction: parsers wrap an io.ReaderAt in a SectionReader,
|
||||
// and it must see a consistent length.
|
||||
func TestWorksWithIOSectionReader(t *testing.T) {
|
||||
b := newTestBuffer(t)
|
||||
mustWrite(t, b, 0, repeat('A', 100))
|
||||
|
||||
sr := io.NewSectionReader(b, 0, b.Size())
|
||||
got, err := io.ReadAll(sr)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, repeat('A', 100), got)
|
||||
}
|
||||
|
||||
// --- model-based randomized testing -----------------------------------------------------------------
|
||||
|
||||
// TestAgainstAReferenceModel drives random writes through the buffer and a dumb reference at the same
|
||||
// time, then compares every readable byte. This is what catches tier-boundary and merge bugs that
|
||||
// hand-written cases miss.
|
||||
func TestAgainstAReferenceModel(t *testing.T) {
|
||||
const universe = 8192
|
||||
|
||||
for _, limit := range []int64{0, 1, 64, 1000, 4096, universe * 2} {
|
||||
t.Run("memLimit="+itoa(limit), func(t *testing.T) {
|
||||
for seed := int64(0); seed < 40; seed++ {
|
||||
rng := rand.New(rand.NewSource(seed)) //nolint:gosec // deterministic test input, not crypto
|
||||
b := newTestBuffer(t, WithMemLimit(limit))
|
||||
|
||||
model := make([]byte, universe)
|
||||
written := make([]bool, universe)
|
||||
|
||||
for op := 0; op < 40; op++ {
|
||||
off := rng.Int63n(universe)
|
||||
l := rng.Int63n(256) + 1
|
||||
if off+l > universe {
|
||||
l = universe - off
|
||||
}
|
||||
payload := make([]byte, l)
|
||||
for i := range payload {
|
||||
payload[i] = byte(rng.Intn(256))
|
||||
}
|
||||
|
||||
mustWrite(t, b, off, payload)
|
||||
copy(model[off:], payload)
|
||||
for i := off; i < off+l; i++ {
|
||||
written[i] = true
|
||||
}
|
||||
}
|
||||
|
||||
// the reference contiguous prefix
|
||||
var want int64
|
||||
for want < universe && written[want] {
|
||||
want++
|
||||
}
|
||||
|
||||
require.Equal(t, want, b.Size(), "seed=%d limit=%d", seed, limit)
|
||||
if want == 0 {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, model[:want], readAll(t, b), "seed=%d limit=%d", seed, limit)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRandomReadsMatchTheModel checks partial reads at arbitrary offsets, which the whole-buffer
|
||||
// comparison above cannot reach.
|
||||
func TestRandomReadsMatchTheModel(t *testing.T) {
|
||||
rng := rand.New(rand.NewSource(7)) //nolint:gosec // deterministic test input, not crypto
|
||||
b := newTestBuffer(t, WithMemLimit(97))
|
||||
|
||||
model := make([]byte, 1000)
|
||||
for i := range model {
|
||||
model[i] = byte(rng.Intn(256))
|
||||
}
|
||||
// written in random-sized chunks, in order, so the whole range is covered
|
||||
for off := 0; off < len(model); {
|
||||
l := rng.Intn(50) + 1
|
||||
if off+l > len(model) {
|
||||
l = len(model) - off
|
||||
}
|
||||
mustWrite(t, b, int64(off), model[off:off+l])
|
||||
off += l
|
||||
}
|
||||
require.Equal(t, int64(len(model)), b.Size())
|
||||
|
||||
for i := 0; i < 500; i++ {
|
||||
off := rng.Intn(len(model))
|
||||
l := rng.Intn(len(model)-off) + 1
|
||||
p := make([]byte, l)
|
||||
n, err := b.ReadAt(p, int64(off))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, l, n)
|
||||
require.Equal(t, model[off:off+l], p, "off=%d len=%d", off, l)
|
||||
}
|
||||
}
|
||||
|
||||
// --- helpers ----------------------------------------------------------------------------------------
|
||||
|
||||
func dirEntries(t *testing.T, dir string) []os.DirEntry {
|
||||
t.Helper()
|
||||
entries, err := os.ReadDir(dir)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
// tmpdir.NewFile creates the spill file inside the root it is given
|
||||
var files []os.DirEntry
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
sub, err := os.ReadDir(filepath.Join(dir, e.Name()))
|
||||
require.NoError(t, err)
|
||||
for _, s := range sub {
|
||||
files = append(files, s)
|
||||
}
|
||||
continue
|
||||
}
|
||||
files = append(files, e)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func itoa(n int64) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
var digits []byte
|
||||
for n > 0 {
|
||||
digits = append([]byte{byte('0' + n%10)}, digits...)
|
||||
n /= 10
|
||||
}
|
||||
return string(digits)
|
||||
}
|
||||
|
||||
// TestMemoryTierGrowsAmortized is the regression test for quadratic growth. Sizing the tier to exactly
|
||||
// what each write needs reallocates and copies the whole thing per call, so a caller streaming a block in
|
||||
// small chunks (which is what io.Copy through an OffsetWriter does) pays O(n^2): filling a 1MB tier in
|
||||
// 32KB chunks cost ~16MB of allocation and showed up as a 19MB spike decompressing a 16MB payload.
|
||||
func TestMemoryTierGrowsAmortized(t *testing.T) {
|
||||
const (
|
||||
limit = 1 << 20
|
||||
chunk = 32 << 10
|
||||
)
|
||||
b := newTestBuffer(t, WithMemLimit(limit))
|
||||
|
||||
var before, after runtime.MemStats
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&before)
|
||||
for off := int64(0); off < limit; off += chunk {
|
||||
mustWrite(t, b, off, repeat('A', chunk))
|
||||
}
|
||||
runtime.ReadMemStats(&after)
|
||||
|
||||
allocated := after.TotalAlloc - before.TotalAlloc
|
||||
t.Logf("filling a %d byte tier in %d byte chunks allocated %d bytes", limit, chunk, allocated)
|
||||
assert.Less(t, allocated, uint64(4*limit),
|
||||
"growth has to be amortized, not a fresh copy of the whole tier per write")
|
||||
assert.Equal(t, int64(limit), b.Size())
|
||||
}
|
||||
|
||||
// TestMemoryTierNeverExceedsTheLimit pins the cap that geometric growth could otherwise overshoot.
|
||||
func TestMemoryTierNeverExceedsTheLimit(t *testing.T) {
|
||||
const limit = 1000
|
||||
b := newTestBuffer(t, WithMemLimit(limit))
|
||||
|
||||
for off := int64(0); off < 4*limit; off += 100 {
|
||||
mustWrite(t, b, off, repeat('A', 100))
|
||||
assert.LessOrEqual(t, int64(len(b.mem)), int64(limit),
|
||||
"the memory tier may never grow past its own limit (off=%d)", off)
|
||||
}
|
||||
assert.Equal(t, int64(4*limit), b.Size())
|
||||
}
|
||||
|
||||
// TestFirstGap covers the placement question a caller writing at offsets of its own choosing has to ask.
|
||||
// It moved here from the UPX cataloger, which used to walk an exported extent list to answer it itself.
|
||||
func TestFirstGap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
writes []extent
|
||||
size int64
|
||||
within int64
|
||||
wantAt int64
|
||||
wantOK bool
|
||||
}{
|
||||
{
|
||||
name: "an empty buffer starts at zero",
|
||||
size: 16,
|
||||
within: 100,
|
||||
wantAt: 0, wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "the gap between two runs comes first",
|
||||
writes: []extent{{0, 1000}, {1016, 2000}},
|
||||
size: 16,
|
||||
within: 3000,
|
||||
wantAt: 1000, wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "with the gap filled the tail is what is left",
|
||||
writes: []extent{{0, 2000}},
|
||||
size: 1000,
|
||||
within: 3000,
|
||||
wantAt: 2000, wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "a gap too small is skipped for a later one",
|
||||
writes: []extent{{0, 100}, {108, 200}},
|
||||
size: 50,
|
||||
within: 1000,
|
||||
wantAt: 200, wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "nowhere left to fit is reported rather than squeezed in",
|
||||
writes: []extent{{0, 1000}, {1016, 2000}},
|
||||
size: 1001,
|
||||
within: 3000,
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "a run past the declared total must not wrap the remaining-space subtraction",
|
||||
writes: []extent{{0, 200}},
|
||||
size: 8,
|
||||
within: 100,
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "exactly filling the tail is allowed",
|
||||
writes: []extent{{0, 900}},
|
||||
size: 100,
|
||||
within: 1000,
|
||||
wantAt: 900, wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "a gap between two runs is still cut off by the bound",
|
||||
writes: []extent{{0, 10}, {1000, 1010}},
|
||||
size: 45,
|
||||
within: 50,
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "a gap between two runs that the bound still leaves room in",
|
||||
writes: []extent{{0, 10}, {1000, 1010}},
|
||||
size: 30,
|
||||
within: 50,
|
||||
wantAt: 10, wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "a negative size is refused rather than treated as zero",
|
||||
size: -1,
|
||||
within: 100,
|
||||
wantOK: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b := newTestBuffer(t)
|
||||
for _, w := range tt.writes {
|
||||
mustWrite(t, b, w.start, repeat('x', int(w.end-w.start)))
|
||||
}
|
||||
|
||||
at, ok := b.FirstGap(tt.size, tt.within)
|
||||
assert.Equal(t, tt.wantOK, ok)
|
||||
if tt.wantOK {
|
||||
assert.Equal(t, tt.wantAt, at)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFirstGapNeverOverlapsWhatWasWritten is the property behind the table: whatever offset comes back has
|
||||
// to be usable, so writing size bytes there must not land on top of anything already stored.
|
||||
func TestFirstGapNeverOverlapsWhatWasWritten(t *testing.T) {
|
||||
rng := rand.New(rand.NewSource(11)) //nolint:gosec // deterministic test input, not crypto
|
||||
const universe = 4096
|
||||
|
||||
for seed := 0; seed < 200; seed++ {
|
||||
b := newTestBuffer(t)
|
||||
occupied := make([]bool, universe)
|
||||
|
||||
for op := 0; op < 6; op++ {
|
||||
// deliberately reaches past universe too, so the bound has to cut a between-extents gap short
|
||||
off := rng.Int63n(2 * universe)
|
||||
l := rng.Int63n(200) + 1
|
||||
mustWrite(t, b, off, repeat('x', int(l)))
|
||||
for i := off; i < off+l && i < universe; i++ {
|
||||
occupied[i] = true
|
||||
}
|
||||
}
|
||||
|
||||
size := rng.Int63n(100) + 1
|
||||
at, ok := b.FirstGap(size, universe)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
require.LessOrEqual(t, at+size, int64(universe), "the result must fit inside the declared total")
|
||||
for i := at; i < at+size; i++ {
|
||||
require.False(t, occupied[i], "FirstGap returned %d for %d bytes, but %d is taken", at, size, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,3 +35,24 @@ Create the new schema by running `make generate-json-schema` from the root of th
|
||||
- If there is an existing schema for the given version and the new schema **does not** match the existing schema, an error is shown indicating to increment the version appropriately (see the "Versioning" section)
|
||||
|
||||
***Note: never delete a JSON schema and never change an existing JSON schema once it has been published in a release!*** Only add new schemas with a newly incremented version. All previous schema files must be stored in the `schema/json/` directory.
|
||||
|
||||
### Exception: `description`-only corrections
|
||||
|
||||
A published schema may be amended in place for one narrow case: the change touches only `description` text and leaves the data shape identical. Descriptions are documentation carried alongside the schema rather than constraints a validator evaluates, so correcting one cannot invalidate a document that already validated against that version. Minting a new version instead would leave the old one permanently describing the tool incorrectly, and spend a version number on no semantic change.
|
||||
|
||||
This applies when **every** one of the following holds:
|
||||
|
||||
- the only differences are `description` values
|
||||
- no field, type, enum, `required` entry, or `$ref` is added, removed, or altered
|
||||
- the `$id` version is unchanged
|
||||
|
||||
Anything else, including adding a field that happens to be optional, is a schema change and needs a version bump per the "Versioning" section above.
|
||||
|
||||
The generator blocks an in-place edit by design, since it refuses to overwrite a file that differs from what it would produce. To amend one, delete the schema file and regenerate so it is rewritten from the current Go doc comments:
|
||||
|
||||
```bash
|
||||
rm schema/json/schema-$VERSION.json
|
||||
make generate-json-schema
|
||||
```
|
||||
|
||||
The result is byte-identical to what the generator produces, so do not hand-edit the JSON. Re-running `make generate-json-schema` afterwards should report `No change to the existing schema!`, and `make check-json-schema-drift` should pass. Confirm with `git diff` that the only changes are the intended description lines.
|
||||
|
||||
@@ -1662,7 +1662,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"type": "object",
|
||||
"description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, while\nthe \"stdlib\" scope populates only the stdlib package."
|
||||
"description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, the\n\"extended-stdlib\" scope populates the stdlib package plus every module under golang.org/x/, and the\n\"stdlib\" scope populates only the stdlib package. The capture-symbols-modules glob patterns populate\nany additional modules they match."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
|
||||
@@ -1662,7 +1662,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"type": "object",
|
||||
"description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, while\nthe \"stdlib\" scope populates only the stdlib package."
|
||||
"description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, the\n\"extended-stdlib\" scope populates the stdlib package plus every module under golang.org/x/, and the\n\"stdlib\" scope populates only the stdlib package. The capture-symbols-modules glob patterns populate\nany additional modules they match."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
|
||||
@@ -12,6 +12,10 @@ const (
|
||||
// SymbolScopeStdlib captures symbols only for the synthetic "stdlib" package, leaving module packages without symbols.
|
||||
SymbolScopeStdlib SymbolScope = "stdlib"
|
||||
|
||||
// SymbolScopeExtendedStdlib captures symbols for the synthetic "stdlib" package as well as every module
|
||||
// under golang.org/x/ (the extended standard library).
|
||||
SymbolScopeExtendedStdlib SymbolScope = "extended-stdlib"
|
||||
|
||||
// SymbolScopeAll captures symbols for all module packages as well as the synthetic "stdlib" package.
|
||||
SymbolScopeAll SymbolScope = "all"
|
||||
)
|
||||
@@ -21,6 +25,8 @@ func (s SymbolScope) Parse() SymbolScope {
|
||||
switch strings.ToLower(strings.TrimSpace(string(s))) {
|
||||
case string(SymbolScopeAll):
|
||||
return SymbolScopeAll
|
||||
case string(SymbolScopeExtendedStdlib):
|
||||
return SymbolScopeExtendedStdlib
|
||||
case string(SymbolScopeStdlib):
|
||||
return SymbolScopeStdlib
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@ func Test_SymbolScope_Parse(t *testing.T) {
|
||||
{" all ", SymbolScopeAll},
|
||||
{"stdlib", SymbolScopeStdlib},
|
||||
{"Stdlib", SymbolScopeStdlib},
|
||||
{"extended-stdlib", SymbolScopeExtendedStdlib},
|
||||
{"Extended-Stdlib", SymbolScopeExtendedStdlib},
|
||||
{"EXTENDED-STDLIB", SymbolScopeExtendedStdlib},
|
||||
{" extended-stdlib ", SymbolScopeExtendedStdlib},
|
||||
{"extended_stdlib", SymbolScopeNone},
|
||||
{"none", SymbolScopeNone},
|
||||
{"", SymbolScopeNone},
|
||||
{"true", SymbolScopeNone},
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ func (c Attributes) String() string {
|
||||
|
||||
// sanitize is a modified version of WFNize function from nvdtools
|
||||
// that quotes all the allowed punctation chars with a slash and replaces
|
||||
// spaces with underscores. It differs from the upstream implmentation as
|
||||
// spaces with underscores. It differs from the upstream implementation as
|
||||
// it does not use the buggy nvdtools implementation, specifically the "addSlashesAt" part of the
|
||||
// function which stops the loop as soon as it encounters ":" a valid
|
||||
// character for a WFN attribute after quoting, but the way nvdtools
|
||||
|
||||
@@ -10,11 +10,12 @@ import (
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/elfutil"
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
)
|
||||
|
||||
func findELFFeatures(data *file.Executable, reader unionreader.UnionReader) error {
|
||||
f, err := elf.NewFile(reader)
|
||||
f, err := elfutil.NewFile(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+8
-2
@@ -1,6 +1,12 @@
|
||||
FROM gcc:9.5.0
|
||||
|
||||
RUN apt update -y && apt install -y clang cmake git make m4 pkg-config zlib1g-dev
|
||||
# bullseye is EOL: deb.debian.org dropped it, so pull from archive.debian.org (whose Release files are
|
||||
# permanently expired, hence Check-Valid-Until). bullseye-security is deliberately omitted -- its index is
|
||||
# still served but the pool behind it 404s, and it has not been moved to the archive yet. These are throwaway
|
||||
# fixture build tools, so base bullseye package versions are fine.
|
||||
RUN printf 'deb http://archive.debian.org/debian bullseye main\ndeb http://archive.debian.org/debian bullseye-updates main\n' > /etc/apt/sources.list \
|
||||
&& apt -o Acquire::Check-Valid-Until=false update -y \
|
||||
&& apt install -y clang cmake git make m4 pkg-config zlib1g-dev
|
||||
|
||||
## from https://github.com/runsafesecurity/selfrando/blob/tb-v0.4.2/docs/linux-build-instructions.md
|
||||
#RUN git clone https://github.com/runsafesecurity/selfrando.git && \
|
||||
@@ -13,4 +19,4 @@ RUN apt update -y && apt install -y clang cmake git make m4 pkg-config zlib1g-de
|
||||
#RUN cd selfrando && make -j`nprocs --all`
|
||||
#RUN cd selfrando && make install
|
||||
|
||||
RUN curl -o /bin/checksec https://raw.githubusercontent.com/slimm609/checksec.sh/2.6.0/checksec && chmod +x /bin/checksec
|
||||
RUN curl -f -o /bin/checksec https://raw.githubusercontent.com/slimm609/checksec.sh/2.6.0/checksec && chmod +x /bin/checksec
|
||||
|
||||
@@ -152,6 +152,26 @@ func NewLocationFromImage(accessPath string, ref file.Reference, img *image.Imag
|
||||
}
|
||||
}
|
||||
|
||||
// NewVirtualLocationFromImage creates a new Location surfaced at realPath but whose contents and metadata are
|
||||
// described by the given reference. Note that RealPath may differ from ref.RealPath; callers must not assume the two
|
||||
// agree for these locations. The FileSystemID comes from the given ref's layer.
|
||||
func NewVirtualLocationFromImage(realPath, accessPath string, ref file.Reference, img *image.Image) Location {
|
||||
layer := img.FileCatalog.Layer(ref)
|
||||
return Location{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: realPath,
|
||||
FileSystemID: layer.Metadata.Digest,
|
||||
},
|
||||
AccessPath: accessPath,
|
||||
ref: ref,
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewLocationFromDirectory creates a new Location representing the given path (extracted from the Reference) relative to the given directory.
|
||||
func NewLocationFromDirectory(responsePath string, fd string, ref file.Reference) Location {
|
||||
return Location{
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package spdxhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spdx/tools-golang/spdx"
|
||||
"github.com/spdx/tools-golang/spdx/v2/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// The tests in this file cover findRootPackages, which locates the packages an SPDX document declares it DESCRIBES
|
||||
// (or which declare themselves DESCRIBED_BY the document). Until it was rewritten, this function compared every
|
||||
// package against every relationship, so its cost grew with packages x relationships. On real documents that made
|
||||
// SPDX decoding quadratic in document size: a 119MB SPDX JSON document (70k packages, 153k relationships) spent
|
||||
// 72s here, a 238MB document 266s, and a 1.3GB document did not finish within 90 minutes. Encoding the same
|
||||
// documents, and decoding equivalently sized syft-json or CycloneDX documents, is linear and takes seconds.
|
||||
//
|
||||
// Test_findRootPackages pins the exact semantics of the original implementation, including the corner cases a
|
||||
// rewrite could easily change: result order follows package order, an element described twice appears twice, and
|
||||
// only edges that touch the DOCUMENT element count. Test_findRootPackages_scalesLinearly is the regression guard
|
||||
// for the performance fix itself.
|
||||
|
||||
func Test_findRootPackages(t *testing.T) {
|
||||
pkgA := &spdx.Package{PackageSPDXIdentifier: "Package-a"}
|
||||
pkgB := &spdx.Package{PackageSPDXIdentifier: "Package-b"}
|
||||
pkgC := &spdx.Package{PackageSPDXIdentifier: "Package-c"}
|
||||
|
||||
describes := func(id common.ElementID) *spdx.Relationship {
|
||||
return &spdx.Relationship{
|
||||
RefA: common.DocElementID{ElementRefID: "DOCUMENT"},
|
||||
RefB: common.DocElementID{ElementRefID: id},
|
||||
Relationship: spdx.RelationshipDescribes,
|
||||
}
|
||||
}
|
||||
describedBy := func(id common.ElementID) *spdx.Relationship {
|
||||
return &spdx.Relationship{
|
||||
RefA: common.DocElementID{ElementRefID: id},
|
||||
RefB: common.DocElementID{ElementRefID: "DOCUMENT"},
|
||||
Relationship: spdx.RelationshipDescribedBy,
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
packages []*spdx.Package
|
||||
relationships []*spdx.Relationship
|
||||
want []*spdx.Package
|
||||
}{
|
||||
{
|
||||
name: "no relationships",
|
||||
packages: []*spdx.Package{pkgA, pkgB},
|
||||
relationships: nil,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "document describes one package",
|
||||
packages: []*spdx.Package{pkgA, pkgB, pkgC},
|
||||
relationships: []*spdx.Relationship{
|
||||
{RefA: common.DocElementID{ElementRefID: "Package-a"}, RefB: common.DocElementID{ElementRefID: "Package-b"}, Relationship: spdx.RelationshipContains},
|
||||
describes("Package-b"),
|
||||
},
|
||||
want: []*spdx.Package{pkgB},
|
||||
},
|
||||
{
|
||||
name: "package described by the document",
|
||||
packages: []*spdx.Package{pkgA, pkgB, pkgC},
|
||||
relationships: []*spdx.Relationship{
|
||||
describedBy("Package-c"),
|
||||
},
|
||||
want: []*spdx.Package{pkgC},
|
||||
},
|
||||
{
|
||||
name: "multiple roots are returned in package order",
|
||||
packages: []*spdx.Package{pkgA, pkgB, pkgC},
|
||||
relationships: []*spdx.Relationship{
|
||||
describes("Package-c"),
|
||||
describedBy("Package-a"),
|
||||
},
|
||||
want: []*spdx.Package{pkgA, pkgC},
|
||||
},
|
||||
{
|
||||
name: "duplicate edges yield duplicate roots (preserved behavior)",
|
||||
packages: []*spdx.Package{pkgA, pkgB},
|
||||
relationships: []*spdx.Relationship{
|
||||
describes("Package-a"),
|
||||
describedBy("Package-a"),
|
||||
},
|
||||
want: []*spdx.Package{pkgA, pkgA},
|
||||
},
|
||||
{
|
||||
name: "describes edges not from the document are ignored",
|
||||
packages: []*spdx.Package{pkgA, pkgB},
|
||||
relationships: []*spdx.Relationship{
|
||||
{RefA: common.DocElementID{ElementRefID: "Package-a"}, RefB: common.DocElementID{ElementRefID: "Package-b"}, Relationship: spdx.RelationshipDescribes},
|
||||
{RefA: common.DocElementID{ElementRefID: "Package-b"}, RefB: common.DocElementID{ElementRefID: "Package-a"}, Relationship: spdx.RelationshipDescribedBy},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "described element that is not a package is ignored",
|
||||
packages: []*spdx.Package{pkgA},
|
||||
relationships: []*spdx.Relationship{
|
||||
describes("File-1"),
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "nil packages and relationships are skipped",
|
||||
packages: []*spdx.Package{nil, pkgA},
|
||||
relationships: []*spdx.Relationship{
|
||||
nil,
|
||||
describes("Package-a"),
|
||||
},
|
||||
want: []*spdx.Package{pkgA},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := findRootPackages(&spdx.Document{
|
||||
Packages: tt.packages,
|
||||
Relationships: tt.relationships,
|
||||
})
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test_findRootPackages_scalesLinearly guards against reintroducing a packages x relationships scan. At 50k
|
||||
// packages and 100k relationships (5e9 comparisons) the previous implementation needs on the order of a minute on
|
||||
// a fast laptop and several minutes on a CI runner, whereas the indexed implementation completes in about a
|
||||
// millisecond. The 10s bound is therefore loose enough never to flake on slow hardware yet far below what any
|
||||
// quadratic implementation could achieve.
|
||||
func Test_findRootPackages_scalesLinearly(t *testing.T) {
|
||||
doc := largeSPDXDocument(50_000, 100_000)
|
||||
|
||||
start := time.Now()
|
||||
got := findRootPackages(doc)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
require.Len(t, got, 1)
|
||||
assert.Equal(t, common.ElementID("Package-0"), got[0].PackageSPDXIdentifier)
|
||||
assert.Less(t, elapsed, 10*time.Second, "findRootPackages took %s on %d packages and %d relationships", elapsed, len(doc.Packages), len(doc.Relationships))
|
||||
}
|
||||
|
||||
// Benchmark_findRootPackages measures the same 50k x 100k document as the scaling test so the two can be compared
|
||||
// directly: run with `go test ./syft/format/common/spdxhelpers/ -run xxx -bench findRootPackages`.
|
||||
func Benchmark_findRootPackages(b *testing.B) {
|
||||
doc := largeSPDXDocument(50_000, 100_000)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = findRootPackages(doc)
|
||||
}
|
||||
}
|
||||
|
||||
// largeSPDXDocument builds a document with the given number of packages and relationships where the document
|
||||
// describes exactly the first package and every other relationship links packages to one another.
|
||||
func largeSPDXDocument(packages int, relationships int) *spdx.Document {
|
||||
doc := &spdx.Document{}
|
||||
for i := 0; i < packages; i++ {
|
||||
doc.Packages = append(doc.Packages, &spdx.Package{
|
||||
PackageSPDXIdentifier: common.ElementID(fmt.Sprintf("Package-%d", i)),
|
||||
})
|
||||
}
|
||||
doc.Relationships = append(doc.Relationships, &spdx.Relationship{
|
||||
RefA: common.DocElementID{ElementRefID: "DOCUMENT"},
|
||||
RefB: common.DocElementID{ElementRefID: "Package-0"},
|
||||
Relationship: spdx.RelationshipDescribes,
|
||||
})
|
||||
for i := 1; i < relationships; i++ {
|
||||
doc.Relationships = append(doc.Relationships, &spdx.Relationship{
|
||||
RefA: common.DocElementID{ElementRefID: common.ElementID(fmt.Sprintf("Package-%d", i%packages))},
|
||||
RefB: common.DocElementID{ElementRefID: common.ElementID(fmt.Sprintf("Package-%d", (i+1)%packages))},
|
||||
Relationship: spdx.RelationshipDependsOn,
|
||||
})
|
||||
}
|
||||
return doc
|
||||
}
|
||||
@@ -83,20 +83,30 @@ func removeRelationships(relationships []*spdx.Relationship, spdxID spdx.Element
|
||||
}
|
||||
|
||||
func findRootPackages(doc *spdx.Document) (out []*spdx.Package) {
|
||||
// index the DESCRIBES / DESCRIBED_BY relationships once, counting how many
|
||||
// times each element is described by the document, so that the result
|
||||
// matches the previous packages x relationships scan exactly (one entry per
|
||||
// matching relationship) without the quadratic cost.
|
||||
describedCount := make(map[spdx.ElementID]int)
|
||||
for _, r := range doc.Relationships {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case r.RefA.ElementRefID == "DOCUMENT" && r.Relationship == spdx.RelationshipDescribes:
|
||||
describedCount[r.RefB.ElementRefID]++
|
||||
case r.RefB.ElementRefID == "DOCUMENT" && r.Relationship == spdx.RelationshipDescribedBy:
|
||||
describedCount[r.RefA.ElementRefID]++
|
||||
}
|
||||
}
|
||||
if len(describedCount) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, p := range doc.Packages {
|
||||
for _, r := range doc.Relationships {
|
||||
describes := r.RefA.ElementRefID == "DOCUMENT" &&
|
||||
r.Relationship == spdx.RelationshipDescribes &&
|
||||
r.RefB.ElementRefID == p.PackageSPDXIdentifier
|
||||
|
||||
describedBy := r.RefB.ElementRefID == "DOCUMENT" &&
|
||||
r.Relationship == spdx.RelationshipDescribedBy &&
|
||||
r.RefA.ElementRefID == p.PackageSPDXIdentifier
|
||||
|
||||
if !describes && !describedBy {
|
||||
continue
|
||||
}
|
||||
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
for i := 0; i < describedCount[p.PackageSPDXIdentifier]; i++ {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ func init() {
|
||||
|
||||
func Decoders() []sbom.FormatDecoder {
|
||||
return []sbom.FormatDecoder{
|
||||
// syft-json must remain the first decoder tried: it is by far the most common input, and its decoder can
|
||||
// identify a syft document from the tail of the file alone. Trying anything else first would pay for a full
|
||||
// parse of a large document by another decoder before reaching the common path.
|
||||
syftjson.NewFormatDecoder(),
|
||||
cyclonedxxml.NewFormatDecoder(),
|
||||
cyclonedxjson.NewFormatDecoder(),
|
||||
|
||||
@@ -72,8 +72,21 @@ func (o *offsetReadSeeker) Seek(offset int64, whence int) (int64, error) {
|
||||
}
|
||||
newOffset, err := o.rdr.Seek(offset, io.SeekCurrent)
|
||||
return newOffset - o.offset, err
|
||||
case io.SeekEnd:
|
||||
newOffset, err := o.rdr.Seek(offset, io.SeekEnd)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot seek end: %w", err)
|
||||
}
|
||||
if newOffset < o.offset {
|
||||
// landed before the start of this view; move back to the start so the position stays valid
|
||||
if _, err := o.rdr.Seek(o.offset, io.SeekStart); err != nil {
|
||||
return 0, fmt.Errorf("cannot seek end: %w", err)
|
||||
}
|
||||
return 0, fmt.Errorf("cannot seek < 0")
|
||||
}
|
||||
return newOffset - o.offset, nil
|
||||
}
|
||||
return 0, fmt.Errorf("only SeekStart and SeekCurrent supported")
|
||||
return 0, fmt.Errorf("only SeekStart, SeekCurrent, and SeekEnd supported")
|
||||
}
|
||||
|
||||
var _ io.ReadSeeker = (*offsetReadSeeker)(nil)
|
||||
|
||||
@@ -179,11 +179,25 @@ func Test_offsetReadSeeker(t *testing.T) {
|
||||
whence: io.SeekCurrent,
|
||||
expected: "4",
|
||||
},
|
||||
{
|
||||
name: "seek end",
|
||||
input: abcd1234offset(2),
|
||||
seek: -4,
|
||||
whence: io.SeekEnd,
|
||||
expected: "1234",
|
||||
},
|
||||
{
|
||||
name: "seek end before the start of the view",
|
||||
input: abcd1234offset(2),
|
||||
seek: -7,
|
||||
whence: io.SeekEnd,
|
||||
wantErr: require.Error, // would be < offset, which is an error
|
||||
},
|
||||
{
|
||||
name: "bad whence",
|
||||
input: abcd1234,
|
||||
seek: 1,
|
||||
whence: io.SeekEnd,
|
||||
whence: 99,
|
||||
wantErr: require.Error,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
{
|
||||
"referenceCategory": "PACKAGE-MANAGER",
|
||||
"referenceType": "purl",
|
||||
"referenceLocator": "pkg:oci/user-image-input@sha256%3A2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch="
|
||||
"referenceLocator": "pkg:oci/user-image-input@sha256%3A2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch=amd64"
|
||||
}
|
||||
],
|
||||
"primaryPackagePurpose": "CONTAINER"
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
{
|
||||
"referenceCategory": "PACKAGE-MANAGER",
|
||||
"referenceType": "purl",
|
||||
"referenceLocator": "pkg:oci/user-image-input@sha256%3A2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch="
|
||||
"referenceLocator": "pkg:oci/user-image-input@sha256%3A2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch=amd64"
|
||||
}
|
||||
],
|
||||
"primaryPackagePurpose": "CONTAINER"
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ PackageChecksum: SHA256: 2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf4
|
||||
PackageLicenseConcluded: NOASSERTION
|
||||
PackageLicenseDeclared: NOASSERTION
|
||||
PackageCopyrightText: NOASSERTION
|
||||
ExternalRef: PACKAGE-MANAGER purl pkg:oci/user-image-input@sha256%3A2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch=
|
||||
ExternalRef: PACKAGE-MANAGER purl pkg:oci/user-image-input@sha256%3A2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch=amd64
|
||||
|
||||
##### Package: package-2
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ PackageChecksum: SHA256: 2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf4
|
||||
PackageLicenseConcluded: NOASSERTION
|
||||
PackageLicenseDeclared: NOASSERTION
|
||||
PackageCopyrightText: NOASSERTION
|
||||
ExternalRef: PACKAGE-MANAGER purl pkg:oci/user-image-input@sha256%3A2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch=
|
||||
ExternalRef: PACKAGE-MANAGER purl pkg:oci/user-image-input@sha256%3A2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch=amd64
|
||||
|
||||
##### Package: package-2
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package syftjson
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
@@ -15,6 +17,12 @@ import (
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
// identifyTailSize is how much of the end of a document is inspected when identifying it from its trailing schema
|
||||
// block, before falling back to parsing the whole document.
|
||||
const identifyTailSize = 2 * 1024 * 1024
|
||||
|
||||
var schemaKey = []byte(`"schema"`)
|
||||
|
||||
var _ sbom.FormatDecoder = (*decoder)(nil)
|
||||
|
||||
type decoder struct{}
|
||||
@@ -45,6 +53,12 @@ func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error)
|
||||
return nil, "", "", fmt.Errorf("unable to decode syft-json document: %w", err)
|
||||
}
|
||||
|
||||
if doc.Schema.Version == "" {
|
||||
// Identify found a schema block that the parsed document does not have, so it was describing something
|
||||
// else in the stream; returning the empty document it decoded into would be a silently wrong answer
|
||||
return nil, "", "", fmt.Errorf("not a syft-json document")
|
||||
}
|
||||
|
||||
if err := checkSupportedSchema(doc.Schema.Version, internal.JSONSchemaVersion); err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
@@ -57,6 +71,15 @@ func (d decoder) Identify(r io.Reader) (sbom.FormatID, string) {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// fast path: syft writes the schema block as the last key of the document (see model.Document), so for seekable
|
||||
// input the format and version can be read from the tail without parsing a potentially very large document.
|
||||
// Anything the tail cannot vouch for falls through to the full parse below.
|
||||
if rs, ok := r.(io.ReadSeeker); ok {
|
||||
if id, version, ok := identifyFromTail(rs); ok {
|
||||
return id, version
|
||||
}
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
Schema model.Schema `json:"schema"`
|
||||
}
|
||||
@@ -78,6 +101,160 @@ func (d decoder) Identify(r io.Reader) (sbom.FormatID, string) {
|
||||
return ID, doc.Schema.Version
|
||||
}
|
||||
|
||||
// identifyFromTail reads at most identifyTailSize bytes from the end of the reader and attempts to identify the
|
||||
// document from a top-level schema block found there. The reader is returned to its original position afterwards.
|
||||
func identifyFromTail(rs io.ReadSeeker) (sbom.FormatID, string, bool) {
|
||||
start, err := rs.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
id, version, ok := readTail(rs, start)
|
||||
|
||||
if _, err := rs.Seek(start, io.SeekStart); err != nil {
|
||||
// without the original position the caller cannot safely parse the document afterwards
|
||||
log.WithFields("error", err).Debug("unable to restore reader position after inspecting document tail")
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
return id, version, ok
|
||||
}
|
||||
|
||||
// readTail does the seeking and reading for identifyFromTail, leaving the reader wherever it ends up.
|
||||
func readTail(rs io.ReadSeeker, start int64) (sbom.FormatID, string, bool) {
|
||||
end, err := rs.Seek(0, io.SeekEnd)
|
||||
if err != nil || end <= start {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// the tail only describes the document the reader is pointed at if that document starts where the reader is,
|
||||
// so check the first byte: bytes ahead of it (a byte order mark, a log line prepended by a wrapper) would
|
||||
// otherwise be accepted silently, leaving Identify claiming a document that cannot be decoded
|
||||
if !opensObject(rs, start) {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
tailLen := min(end-start, int64(identifyTailSize))
|
||||
tail := make([]byte, tailLen)
|
||||
if _, err := rs.Seek(end-tailLen, io.SeekStart); err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
if _, err := io.ReadFull(rs, tail); err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
return identifyFromTailBytes(tail)
|
||||
}
|
||||
|
||||
// opensObject reports whether the document at the given offset begins with a JSON object.
|
||||
func opensObject(rs io.ReadSeeker, start int64) bool {
|
||||
if _, err := rs.Seek(start, io.SeekStart); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
head := make([]byte, 64)
|
||||
n, err := io.ReadFull(rs, head)
|
||||
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return false
|
||||
}
|
||||
|
||||
head = bytes.TrimLeft(head[:n], " \t\r\n")
|
||||
return len(head) > 0 && head[0] == '{'
|
||||
}
|
||||
|
||||
// identifyFromTailBytes looks for a syft schema block among the trailing bytes of a JSON document and, when one is
|
||||
// found directly within the top-level object, returns the format and schema version it declares. The bytes may
|
||||
// begin anywhere within the document, but must run to its very end.
|
||||
func identifyFromTailBytes(tail []byte) (sbom.FormatID, string, bool) {
|
||||
// find the last "schema" key. An unescaped quote cannot occur inside a JSON string, so a match whose opening
|
||||
// quote is not preceded by a backslash is a genuine string token (and a key if a colon follows it).
|
||||
idx := len(tail)
|
||||
for {
|
||||
idx = bytes.LastIndex(tail[:idx], schemaKey)
|
||||
if idx <= 0 {
|
||||
// not found, or found at the very start of the tail where we cannot tell whether it is escaped
|
||||
return "", "", false
|
||||
}
|
||||
if tail[idx-1] != '\\' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
rest := bytes.TrimLeft(tail[idx+len(schemaKey):], " \t\r\n")
|
||||
if len(rest) == 0 || rest[0] != ':' {
|
||||
// a string value that happens to read "schema", not a key
|
||||
return "", "", false
|
||||
}
|
||||
rest = rest[1:]
|
||||
|
||||
var schema model.Schema
|
||||
dec := json.NewDecoder(bytes.NewReader(rest))
|
||||
if err := dec.Decode(&schema); err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// the schema block only identifies the document when it belongs to the top-level object
|
||||
if !closesTopLevelObject(rest[dec.InputOffset():]) {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
if schema.Version == "" || !strings.Contains(schema.URL, "anchore/syft") {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
return ID, schema.Version, true
|
||||
}
|
||||
|
||||
// closesTopLevelObject reports whether the given bytes, which follow a value somewhere within a JSON document, run
|
||||
// to the end of that document closing exactly one enclosing object (and nothing else) at the very end. When true,
|
||||
// the value they follow sits directly within the top-level object. Braces inside strings are ignored.
|
||||
func closesTopLevelObject(b []byte) bool {
|
||||
var (
|
||||
depth int
|
||||
inString bool
|
||||
escaped bool
|
||||
closed bool
|
||||
)
|
||||
for _, c := range b {
|
||||
if closed {
|
||||
// only whitespace may follow the top-level object
|
||||
if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
if inString {
|
||||
switch {
|
||||
case escaped:
|
||||
escaped = false
|
||||
case c == '\\':
|
||||
escaped = true
|
||||
case c == '"':
|
||||
inString = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch c {
|
||||
case '"':
|
||||
inString = true
|
||||
case '{', '[':
|
||||
depth++
|
||||
case '}':
|
||||
depth--
|
||||
if depth == -1 {
|
||||
closed = true
|
||||
}
|
||||
case ']':
|
||||
depth--
|
||||
if depth == -1 {
|
||||
// the enclosing container is an array, not the top-level object
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return closed
|
||||
}
|
||||
|
||||
func checkSupportedSchema(documentVersion string, parserVersion string) error {
|
||||
documentV, err := semver.NewVersion(documentVersion)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
package syftjson
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/format/internal/stream"
|
||||
"github.com/anchore/syft/syft/format/internal/testutil"
|
||||
)
|
||||
|
||||
const (
|
||||
testIdentifySchemaURL = "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-16.0.0.json"
|
||||
testIdentifySchemaBlock = `"schema":{"version":"16.0.0","url":"` + testIdentifySchemaURL + `"}`
|
||||
)
|
||||
|
||||
func Test_identifyFromTailBytes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
doc string
|
||||
wantVersion string
|
||||
wantOK bool
|
||||
}{
|
||||
{
|
||||
name: "syft key order (schema is the last key)",
|
||||
doc: `{"artifacts":[],"descriptor":{"name":"syft"},` + testIdentifySchemaBlock + `}`,
|
||||
wantVersion: "16.0.0",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "alphabetical key order (schema is followed by source)",
|
||||
doc: `{"artifacts":[],` + testIdentifySchemaBlock + `,"source":{"id":"x","name":"img","type":"image","metadata":{"tags":["a","b"],"layers":[{"digest":"sha256:1"}]}}}`,
|
||||
wantVersion: "16.0.0",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "pretty printed with a trailing newline",
|
||||
doc: "{\n \"artifacts\": [],\n \"schema\": {\n \"version\": \"16.0.0\",\n \"url\": \"" + testIdentifySchemaURL + "\"\n }\n}\n",
|
||||
wantVersion: "16.0.0",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "nested schema key is not mistaken for the document schema",
|
||||
doc: `{"artifacts":[{"metadata":{` + testIdentifySchemaBlock + `}}],"descriptor":{}}`,
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "top-level schema wins over an earlier nested one",
|
||||
doc: `{"artifacts":[{"metadata":{"schema":{"version":"1","url":"anchore/syft"}}}],` + testIdentifySchemaBlock + `}`,
|
||||
wantVersion: "16.0.0",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "escaped schema text (with braces) inside a trailing string is ignored",
|
||||
doc: `{"artifacts":[],` + testIdentifySchemaBlock + `,"source":{"name":"contains \"schema\":{\"version\":\"9\"} text"}}`,
|
||||
wantVersion: "16.0.0",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "array wrapped document",
|
||||
doc: `[{"artifacts":[],` + testIdentifySchemaBlock + `}]`,
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "truncated document",
|
||||
doc: `{"artifacts":[],` + testIdentifySchemaBlock,
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "trailing content after the document",
|
||||
doc: `{"artifacts":[],` + testIdentifySchemaBlock + `}{"another":"document"}`,
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "schema value is not an object",
|
||||
doc: `{"artifacts":[],"schema":"16.0.0"}`,
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "schema is a string value rather than a key",
|
||||
doc: `{"artifacts":[],"kind":"schema"}`,
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "not a syft schema url",
|
||||
doc: `{"artifacts":[],"schema":{"version":"1.0","url":"https://example.com/schema.json"}}`,
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "no schema key at all",
|
||||
doc: `{"bomFormat":"CycloneDX","specVersion":"1.6","components":[]}`,
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "schema key at the very start of the tail cannot be vouched for",
|
||||
doc: testIdentifySchemaBlock + `}`,
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
doc: "",
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
// the key's text is: he said "schema
|
||||
name: "an escaped quote does not make a string into the schema key",
|
||||
doc: `{"he said \"schema":{"version":"1.0","url":"https://example.com/anchore/syft"}}`,
|
||||
wantOK: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
id, version, ok := identifyFromTailBytes([]byte(tt.doc))
|
||||
assert.Equal(t, tt.wantOK, ok)
|
||||
assert.Equal(t, tt.wantVersion, version)
|
||||
if tt.wantOK {
|
||||
assert.Equal(t, ID, id)
|
||||
} else {
|
||||
assert.Empty(t, id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Identify_largeDocument(t *testing.T) {
|
||||
// build a document comfortably larger than the tail window so only the tail path could avoid a full parse
|
||||
unit := `{"name":"pkg","version":"1.0"},`
|
||||
padding := strings.Repeat(unit, (identifyTailSize/len(unit))+1024)
|
||||
doc := `{"artifacts":[` + padding + `{"name":"last"}],` + testIdentifySchemaBlock + `}`
|
||||
require.Greater(t, len(doc), identifyTailSize)
|
||||
|
||||
t.Run("seekable reader", func(t *testing.T) {
|
||||
r := strings.NewReader(doc)
|
||||
|
||||
id, version := NewFormatDecoder().Identify(r)
|
||||
assert.Equal(t, ID, id)
|
||||
assert.Equal(t, "16.0.0", version)
|
||||
|
||||
// the reader is left where it started so callers can go on to read the document
|
||||
pos, err := r.Seek(0, io.SeekCurrent)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(0), pos)
|
||||
})
|
||||
|
||||
t.Run("non-seekable reader falls back to a full parse", func(t *testing.T) {
|
||||
r := struct{ io.Reader }{strings.NewReader(doc)}
|
||||
|
||||
id, version := NewFormatDecoder().Identify(r)
|
||||
assert.Equal(t, ID, id)
|
||||
assert.Equal(t, "16.0.0", version)
|
||||
})
|
||||
|
||||
t.Run("reader positioned at a later document", func(t *testing.T) {
|
||||
first := `{"artifacts":[],"schema":{"version":"1.0.0","url":"` + testIdentifySchemaURL + `"}}`
|
||||
r := strings.NewReader(first + "\n" + doc)
|
||||
_, err := r.Seek(int64(len(first)+1), io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
|
||||
// mirror what the decoder collection does: seeks become relative to the current position
|
||||
sr, err := stream.SeekableReader(r)
|
||||
require.NoError(t, err)
|
||||
|
||||
id, version := NewFormatDecoder().Identify(sr)
|
||||
assert.Equal(t, ID, id)
|
||||
assert.Equal(t, "16.0.0", version)
|
||||
|
||||
pos, err := sr.Seek(0, io.SeekCurrent)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(0), pos)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Identify_agreesWithFullParse(t *testing.T) {
|
||||
// the tail is only a shortcut: whatever it decides must match what parsing the whole document says, including
|
||||
// for documents it has to decline.
|
||||
//
|
||||
// note: a stream holding several concatenated documents is deliberately not covered. Identifying the document
|
||||
// at the reader's position would mean proving no other document ends before the stream does, which means
|
||||
// reading all of it, which is the cost the tail path exists to avoid. Decode still reports the document it
|
||||
// actually parsed (see Test_Decode_rejectsADocumentTheTailDidNotDescribe).
|
||||
syftDoc := `{"artifacts":[],` + testIdentifySchemaBlock + `}`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
doc string
|
||||
wantVersion string
|
||||
}{
|
||||
{name: "plain document", doc: syftDoc, wantVersion: "16.0.0"},
|
||||
{name: "byte order mark before the document", doc: "\xef\xbb\xbf" + syftDoc},
|
||||
{name: "content before the document", doc: "WARN something happened\n" + syftDoc},
|
||||
{name: "not a syft document", doc: `{"bomFormat":"CycloneDX","specVersion":"1.6","components":[]}`},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := NewFormatDecoder()
|
||||
|
||||
// a bytes.Reader is seekable, so this takes the tail path
|
||||
fastID, fastVersion := d.Identify(bytes.NewReader([]byte(tt.doc)))
|
||||
|
||||
// readerOnly hides Seek, so this takes the full parse
|
||||
slowID, slowVersion := d.Identify(readerOnly{strings.NewReader(tt.doc)})
|
||||
|
||||
assert.Equal(t, slowID, fastID, "fast and slow paths disagree on the format")
|
||||
assert.Equal(t, slowVersion, fastVersion, "fast and slow paths disagree on the version")
|
||||
assert.Equal(t, tt.wantVersion, fastVersion)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// readerOnly hides any Seek method the underlying reader has, forcing the full parse.
|
||||
type readerOnly struct {
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func Test_Decode_rejectsADocumentTheTailDidNotDescribe(t *testing.T) {
|
||||
// the tail can find a schema block belonging to a later document in the stream; whatever it decides, Decode
|
||||
// must not hand back the empty SBOM that parsing the first document into model.Document would produce
|
||||
doc := `{"bomFormat":"CycloneDX","specVersion":"1.6","components":[{"type":"library","name":"pkg-a"}]}` +
|
||||
"\n" + `{"artifacts":[],` + testIdentifySchemaBlock + `}`
|
||||
|
||||
s, _, _, err := NewFormatDecoder().Decode(bytes.NewReader([]byte(doc)))
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, s)
|
||||
}
|
||||
|
||||
func Test_encoderWritesSchemaLast(t *testing.T) {
|
||||
// identification reads the schema block from the tail of the document (see model.Document), which depends on
|
||||
// the encoder always writing it as the final key
|
||||
s := testutil.DirectoryInput(t, t.TempDir())
|
||||
|
||||
for _, pretty := range []bool{false, true} {
|
||||
cfg := DefaultEncoderConfig()
|
||||
cfg.Pretty = pretty
|
||||
|
||||
enc, err := NewFormatEncoderWithConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
require.NoError(t, enc.Encode(&buf, s))
|
||||
|
||||
keys := topLevelKeys(t, buf.Bytes())
|
||||
require.NotEmpty(t, keys)
|
||||
assert.Equal(t, "schema", keys[len(keys)-1], "pretty=%v", pretty)
|
||||
|
||||
id, version, ok := identifyFromTailBytes(buf.Bytes())
|
||||
assert.True(t, ok, "pretty=%v", pretty)
|
||||
assert.Equal(t, ID, id)
|
||||
assert.Equal(t, internal.JSONSchemaVersion, version)
|
||||
}
|
||||
}
|
||||
|
||||
// noEndSeeker behaves like a reader that buffers a stream as it goes: it can seek within what it has, but cannot
|
||||
// seek relative to the end.
|
||||
type noEndSeeker struct {
|
||||
io.ReadSeeker
|
||||
}
|
||||
|
||||
func (s noEndSeeker) Seek(offset int64, whence int) (int64, error) {
|
||||
if whence == io.SeekEnd {
|
||||
return 0, errors.New("SeekEnd is not supported")
|
||||
}
|
||||
return s.ReadSeeker.Seek(offset, whence)
|
||||
}
|
||||
|
||||
func Test_Identify_fallsBackWhenTailIsUnavailable(t *testing.T) {
|
||||
doc := `{"artifacts":[],` + testIdentifySchemaBlock + `}`
|
||||
r := noEndSeeker{strings.NewReader(doc)}
|
||||
|
||||
id, version := NewFormatDecoder().Identify(r)
|
||||
assert.Equal(t, ID, id)
|
||||
assert.Equal(t, "16.0.0", version)
|
||||
}
|
||||
|
||||
func Test_identifyFromTail_readerEdgeCases(t *testing.T) {
|
||||
doc := `{"artifacts":[],` + testIdentifySchemaBlock + `}`
|
||||
|
||||
t.Run("empty reader", func(t *testing.T) {
|
||||
r := strings.NewReader("")
|
||||
|
||||
_, _, ok := identifyFromTail(r)
|
||||
assert.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("reader already at the end has nothing to inspect and keeps its position", func(t *testing.T) {
|
||||
r := strings.NewReader(doc)
|
||||
_, err := r.Seek(0, io.SeekEnd)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, ok := identifyFromTail(r)
|
||||
assert.False(t, ok)
|
||||
|
||||
pos, err := r.Seek(0, io.SeekCurrent)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(len(doc)), pos)
|
||||
})
|
||||
|
||||
t.Run("whitespace around the colon and CRLF line endings", func(t *testing.T) {
|
||||
crlf := "{\r\n \"artifacts\": [],\r\n \"schema\" : {\r\n \"version\": \"16.0.0\",\r\n \"url\": \"" + testIdentifySchemaURL + "\"\r\n }\r\n}\r\n"
|
||||
|
||||
id, version, ok := identifyFromTailBytes([]byte(crlf))
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, ID, id)
|
||||
assert.Equal(t, "16.0.0", version)
|
||||
})
|
||||
|
||||
t.Run("document ending inside a string is not trusted", func(t *testing.T) {
|
||||
truncated := `{"artifacts":[],` + testIdentifySchemaBlock + `,"source":{"name":"unterminated`
|
||||
|
||||
_, _, ok := identifyFromTailBytes([]byte(truncated))
|
||||
assert.False(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
func topLevelKeys(t *testing.T, doc []byte) []string {
|
||||
dec := json.NewDecoder(bytes.NewReader(doc))
|
||||
|
||||
tok, err := dec.Token()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, json.Delim('{'), tok)
|
||||
|
||||
var keys []string
|
||||
for dec.More() {
|
||||
tok, err := dec.Token()
|
||||
require.NoError(t, err)
|
||||
keys = append(keys, tok.(string))
|
||||
|
||||
var value json.RawMessage
|
||||
require.NoError(t, dec.Decode(&value))
|
||||
}
|
||||
return keys
|
||||
}
|
||||
@@ -5,6 +5,14 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Field order matters: encoding/json writes struct fields in declaration order, and Schema must remain the last
|
||||
// field so that the schema block is always the final key of the encoded document. Format identification relies on
|
||||
// finding the schema block by reading only the tail of a (potentially very large) document; moving Schema earlier
|
||||
// would not break decoding but would silently force a full parse of the whole document to identify it.
|
||||
//
|
||||
// (This block is deliberately separated from the doc comment below: the JSON schema generator lifts doc comments
|
||||
// into schema descriptions, and changing the description would alter the published schema.)
|
||||
|
||||
// Document represents the syft cataloging findings as a JSON document
|
||||
type Document struct {
|
||||
Artifacts []Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
|
||||
|
||||
@@ -105,8 +105,8 @@
|
||||
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NTgsImRpZ2VzdCI6InNoYTI1NjozNWE2NjU4ZTI0ZmFiOTJlYWU5ZWM2ZmMyNTJkZWM1ODk4NmM0YzAwNzg5MTc1OGQ0ZDM3YzdlNDNmYmJlMGM1In0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpkZmVmZTYxOGM4OWIwOGZlZjBmOWM3ZjFhMjY4MjUyMWRkZGJlMDNkNjY3OGY0YTlmYjliMDc4MzgxZDhlYjQ1In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OjM4ZGRjMjg0N2ZiNmJjYWZkNzQwMWI0YmYyN2MxMDAxNGI1ZDYwZTI0MDBiYzE4ODg5MGM3Y2I3Y2RkN2NkNmMifV19",
|
||||
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8ifSwiY3JlYXRlZCI6IjIwMjQtMTEtMDRUMDQ6MTM6NDMuMzQwNTA3MTc1WiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDI0LTExLTA0VDA0OjEzOjQzLjMyMDg2OTk2OFoiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMS50eHQgL3NvbWVmaWxlLTEudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9LHsiY3JlYXRlZCI6IjIwMjQtMTEtMDRUMDQ6MTM6NDMuMzQwNTA3MTc1WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZGZlZmU2MThjODliMDhmZWYwZjljN2YxYTI2ODI1MjFkZGRiZTAzZDY2NzhmNGE5ZmI5YjA3ODM4MWQ4ZWI0NSIsInNoYTI1NjozOGRkYzI4NDdmYjZiY2FmZDc0MDFiNGJmMjdjMTAwMTRiNWQ2MGUyNDAwYmMxODg4OTBjN2NiN2NkZDdjZDZjIl19fQ==",
|
||||
"repoDigests": [],
|
||||
"architecture": "",
|
||||
"os": ""
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
"distro": {
|
||||
|
||||
@@ -175,6 +175,8 @@ func toSyftFileType(ty string) stereoscopeFile.Type {
|
||||
case "SymbolicLink":
|
||||
return stereoscopeFile.TypeSymLink
|
||||
case "HardLink":
|
||||
// kept for reading SBOMs written before stereoscope adopted hardlinks at index time (they are emitted as
|
||||
// regular files now, except for malformed link entries)
|
||||
return stereoscopeFile.TypeHardLink
|
||||
case "Directory":
|
||||
return stereoscopeFile.TypeDirectory
|
||||
|
||||
@@ -0,0 +1,469 @@
|
||||
/*
|
||||
Package elfutil provides an ELF opener that rejects sections whose compression headers declare an
|
||||
implausible decompressed size.
|
||||
|
||||
debug/elf sizes a section's buffer from the section's own compression header, and a highly compressible
|
||||
stream really does deliver the bytes that header promises, so internal/saferio's chunked read does not
|
||||
help: it appends its way to every declared byte, and Go's 1.25x slice growth puts the lifetime cost near
|
||||
five times that. 512MB of zeros is 510KB of zlib, and that file drives elf.NewFile through 2.6GB of
|
||||
allocation and returns no error, which is a fatal Go OOM rather than a recoverable panic. Zlib caps out
|
||||
near 1000:1, but debug/elf also accepts zstd, which passes 32000:1 on zeroed input, so the input needed
|
||||
to declare a given size is smaller still.
|
||||
|
||||
The check runs in two parts, because debug/elf expands sections at two different times:
|
||||
|
||||
- the section-name string table is the one section elf.NewFile expands on its own, so it has to be
|
||||
bounded against the raw bytes before the call is made. That is CheckSectionNameTable, and the walk
|
||||
of the section table it needs is why this package decodes any ELF structures by hand at all.
|
||||
- every other section is expanded lazily by (*Section).Open, so those are bounded after the parse,
|
||||
where names, types and decompressed sizes are resolved already.
|
||||
|
||||
Only the sections syft can actually drive debug/elf into decompressing are bounded. DWARF is deliberately
|
||||
excluded: syft never asks for it, so a binary carrying a large compressed .debug_info is cataloged rather
|
||||
than skipped over a section nobody reads.
|
||||
|
||||
Sources:
|
||||
- https://www.sco.com/developers/gabi/latest/ch4.sheader.html
|
||||
- https://groups.google.com/g/generic-abi/c/satyPkuMisk
|
||||
- https://docs.oracle.com/en/operating-systems/solaris/oracle-solaris/11.4/linkers-libraries/gnu-style-section-compression.html
|
||||
- https://sourceware.org/pipermail/binutils/2015-July/089559.html
|
||||
- https://cs.opensource.google/go/go/+/refs/tags/go1.26.5:src/debug/elf/file.go
|
||||
- https://go.dev/doc/go1.19#linker
|
||||
*/
|
||||
package elfutil
|
||||
|
||||
import (
|
||||
"debug/elf"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
intFile "github.com/anchore/syft/internal/file"
|
||||
)
|
||||
|
||||
// maxDeclaredSectionSize bounds the decompressed size any single reachable section may declare.
|
||||
//
|
||||
// The bound only ever applies to a compressed section, and the gABI allows compression only on
|
||||
// non-allocable ones, so nothing syft reads on the hot path (.data, .text, notes) can reach it. The
|
||||
// realistic worst case is a compressed .symtab/.strtab on a very large binary, well under this. The
|
||||
// trade-off is that a binary whose symbol or string table decompresses past this is skipped rather than
|
||||
// cataloged; that is the correct direction to fail, since the alternative is OOM-killing the whole scan.
|
||||
//
|
||||
// Note that this is a per-section bound, not a per-file one: a file may hold several sections that each
|
||||
// sit just under it. That is deliberate, since the sections syft actually reads are few.
|
||||
const maxDeclaredSectionSize uint64 = 128 * intFile.MB
|
||||
|
||||
// ErrDeclaredSizeExceeded marks a file this package refused to parse because a section declared more
|
||||
// decompressed bytes than maxDeclaredSectionSize allows.
|
||||
//
|
||||
// Exported so a caller can tell "syft declined to expand this" apart from "this file is broken". The
|
||||
// distinction matters where the two are reported differently: the refusal is a real gap in the SBOM and
|
||||
// worth surfacing, while a corrupt or non-Go executable is neither, and the golang cataloger sees far
|
||||
// more of the latter than the former.
|
||||
var ErrDeclaredSizeExceeded = errors.New("declared decompressed size over the limit")
|
||||
|
||||
// legacyZlibHeaderSize is the size of the .zdebug header: the "ZLIB" magic plus a big-endian size.
|
||||
const legacyZlibHeaderSize = 12
|
||||
|
||||
// sectionsReadByName are the sections syft asks debug/elf for by name. Everything else it reaches by
|
||||
// section type, which reachableSections covers separately. Not every one of these ends in a read of the
|
||||
// contents (.text is used for its address, .symtab only for a nil check), but bounding any name syft asks
|
||||
// for is cheaper to keep true than tracking which lookups reach Data.
|
||||
//
|
||||
// Add to this list when a cataloger starts reading a new section by name. The ruleguard rule only stops a
|
||||
// new debug/elf call site; it cannot see a new (*Section).Data call behind an already-bounded open.
|
||||
var sectionsReadByName = map[string]struct{}{
|
||||
".symtab": {},
|
||||
".data": {},
|
||||
".text": {},
|
||||
".gopclntab": {},
|
||||
".note.package": {},
|
||||
".modinfo": {},
|
||||
}
|
||||
|
||||
// wire sizes of the structures we decode, which differ between the two ELF classes
|
||||
var (
|
||||
sizeSection32 = int64(binary.Size(elf.Section32{}))
|
||||
sizeSection64 = int64(binary.Size(elf.Section64{}))
|
||||
sizeChdr32 = int64(binary.Size(elf.Chdr32{}))
|
||||
sizeChdr64 = int64(binary.Size(elf.Chdr64{}))
|
||||
)
|
||||
|
||||
// NewFile parses an ELF file, rejecting it if a section syft may read declares a decompressed size this
|
||||
// process should not be asked to allocate. It is a drop-in for elf.NewFile, except that a rejection is
|
||||
// reported as a plain error rather than an *elf.FormatError, so callers that distinguish "not an ELF"
|
||||
// from "bad ELF" treat it as the latter.
|
||||
func NewFile(r io.ReaderAt) (*elf.File, error) {
|
||||
if err := CheckSectionNameTable(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := elf.NewFile(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := checkReachableSections(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// CheckAllSections rejects ELF decompression bombs before debug/elf can expand one, without keeping the
|
||||
// parse.
|
||||
//
|
||||
// The attack it stops: a compressed section's header declares its own decompressed size, and debug/elf
|
||||
// believes that number, allocating it up front when the section is opened. Nothing forces the declared
|
||||
// size to match what the compressed bytes actually yield, so a small file can name an enormous one. In
|
||||
// the case this was written for, a 260KB ELF declaring a compressed .symtab drove 1.3GB of allocation.
|
||||
// This walks the section headers and refuses any reachable section declaring more than
|
||||
// maxDeclaredSectionSize, returning ErrDeclaredSizeExceeded.
|
||||
//
|
||||
// It is a strict superset of CheckSectionNameTable, which is what "All" in the name marks: reach for that
|
||||
// narrower one only where a full elf.NewFile parse on every file is not worth paying for. Like it, this
|
||||
// is for a caller whose debug/elf call is made inside another package, but it is the whole check rather
|
||||
// than the eager half: goversion reads .symtab and the string table it links, and those are expanded
|
||||
// lazily, so the name table bound alone leaves them unbounded.
|
||||
//
|
||||
// Anything that is not an ELF this package understands passes through untouched, since a caller reaching
|
||||
// for this parses other containers too (goversion takes PE and Mach-O). A file elf.NewFile cannot parse
|
||||
// passes through as well: describing a malformed file is the caller's job, not this gate's.
|
||||
func CheckAllSections(r io.ReaderAt) error {
|
||||
if _, _, ok := identify(r); !ok {
|
||||
return nil
|
||||
}
|
||||
if err := CheckSectionNameTable(r); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := elf.NewFile(r)
|
||||
if err != nil {
|
||||
return nil //nolint:nilerr // the caller's own parse reports the format error
|
||||
}
|
||||
return checkReachableSections(f)
|
||||
}
|
||||
|
||||
// checkReachableSections bounds every section syft can drive debug/elf into decompressing. This runs
|
||||
// after the parse because (*Section).Open expands lazily, so nothing has been allocated yet.
|
||||
func checkReachableSections(f *elf.File) error {
|
||||
for i := range reachableSections(f) {
|
||||
s := f.Sections[i]
|
||||
declared, claimed := declaredSectionSize(s)
|
||||
if claimed && declared > maxDeclaredSectionSize {
|
||||
return fmt.Errorf("%w: elf section %q declares %d decompressed bytes, over the %d byte limit",
|
||||
ErrDeclaredSizeExceeded, s.Name, declared, maxDeclaredSectionSize)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// reachableSections reports which sections syft can actually reach. It marks by type generously, since
|
||||
// the debug/elf accessors all go through SectionByType, which returns only the first section of a type: a
|
||||
// second SHT_SYMTAB is bounded here despite being unreachable. DWARF is absent by design: nothing in syft
|
||||
// calls File.DWARF, so debug/elf is never asked to expand a .debug_* section.
|
||||
func reachableSections(f *elf.File) map[int]struct{} {
|
||||
reach := make(map[int]struct{})
|
||||
mark := func(i int) {
|
||||
if i >= 0 && i < len(f.Sections) {
|
||||
reach[i] = struct{}{}
|
||||
}
|
||||
}
|
||||
for i, s := range f.Sections {
|
||||
switch s.Type {
|
||||
case elf.SHT_SYMTAB, elf.SHT_DYNSYM, elf.SHT_DYNAMIC:
|
||||
// File.Symbols, File.DynamicSymbols and File.DynString each read one of these plus the
|
||||
// string table its Link field points at
|
||||
mark(i)
|
||||
mark(int(s.Link))
|
||||
case elf.SHT_GNU_VERSYM, elf.SHT_GNU_VERDEF, elf.SHT_GNU_VERNEED:
|
||||
// File.DynamicSymbols pulls the symbol version tables in behind the caller's back, via
|
||||
// gnuVersionInit. Strictly they are only reachable alongside a .dynsym, and VERDEF/VERNEED
|
||||
// only alongside a VERSYM, but they are marked unconditionally rather than tracked: the only
|
||||
// file that loses out carries an oversized version table and no dynamic symbols at all,
|
||||
// which is not something a toolchain emits.
|
||||
mark(i)
|
||||
}
|
||||
if _, ok := sectionsReadByName[s.Name]; ok {
|
||||
mark(i)
|
||||
}
|
||||
}
|
||||
return reach
|
||||
}
|
||||
|
||||
// declaredSectionSize reports the decompressed size a section claims, if it claims one at all.
|
||||
//
|
||||
// ELF grew two separate ways of saying "this section is compressed", and (*Section).Open honors both, so
|
||||
// this dispatches the same way Open does and in the same order. That correspondence is the whole design:
|
||||
// wherever Open returns a decompressing reader, this reports the size that reader will expand to. Missing
|
||||
// one is the bug this package exists to prevent, so when a Go release changes Open, this is what has to be
|
||||
// re-read against it.
|
||||
//
|
||||
// It deliberately over-reports in one place, an unrecognized ch_type: Open hands back an error reader for
|
||||
// anything but zlib and zstd, so nothing is allocated, but the type is not exposed on elf.Section and
|
||||
// re-reading the header for it would only spare a file debug/elf cannot decompress anyway.
|
||||
func declaredSectionSize(s *elf.Section) (uint64, bool) {
|
||||
// SHT_NOBITS marks a section that occupies no bytes in the file, .bss being the familiar one: it
|
||||
// declares a size the loader zero-fills at run time rather than a size stored anywhere. Its sh_offset
|
||||
// is only a conceptual placement and routinely lands inside some other section's bytes. Two things
|
||||
// follow, and both say to test this before looking at compression at all:
|
||||
// - Open tests it first too, ahead of any compression, and hands back a reader that fails every
|
||||
// read. Nothing is allocated for the declared size, however large it is.
|
||||
// - the gABI forbids SHF_COMPRESSED here, but elf.NewFile does not enforce that and parses a
|
||||
// compression header anyway. The Size it leaves on the section was decoded from whatever bytes lie
|
||||
// at that borrowed offset, so it is not a figure to bound anything against.
|
||||
if s.Type == elf.SHT_NOBITS {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
if s.Flags&elf.SHF_COMPRESSED != 0 {
|
||||
return chdrDeclaredSize(s)
|
||||
}
|
||||
return zdebugDeclaredSize(s)
|
||||
}
|
||||
|
||||
// chdrDeclaredSize reports the size declared by the modern compression form: an SHF_COMPRESSED flag,
|
||||
// plus an Elf32_Chdr or Elf64_Chdr at the head of the section giving the algorithm and the decompressed
|
||||
// size. This is the form standardized in the gABI and the one any current toolchain emits.
|
||||
func chdrDeclaredSize(s *elf.Section) (uint64, bool) {
|
||||
// SHF_ALLOC marks a section the loader maps into the running process, and nothing decompresses a
|
||||
// section on its way into memory, so the gABI makes the two flags mutually exclusive. Open enforces
|
||||
// that rather than tolerating it: a section setting both gets an error reader, never a decompressing
|
||||
// one, so a size declared alongside SHF_ALLOC is never allocated.
|
||||
if s.Flags&elf.SHF_ALLOC != 0 {
|
||||
return 0, false
|
||||
}
|
||||
// elf.NewFile decodes the Chdr during the parse and overwrites Size with ch_size, leaving the on-disk
|
||||
// figure in FileSize. So the number a hostile file controls is already here, and no second read of the
|
||||
// file is needed.
|
||||
return s.Size, true
|
||||
}
|
||||
|
||||
// zdebugDeclaredSize reports the size declared by the legacy compression form. Before SHF_COMPRESSED was
|
||||
// added to the gABI, GNU tooling signaled a compressed debug section by renaming it, .debug_info to
|
||||
// .zdebug_info, and prefixing the payload with a header of its own: the magic "ZLIB" followed by the
|
||||
// decompressed size. Toolchains default to the gABI form now, Go's own linker having emitted .zdebug up
|
||||
// to 1.19, but this is not vestigial: nothing ties the prefix to a debug section, so a crafted file can
|
||||
// hang it on a section syft does read, an SHT_SYMTAB named .zdebug_x being the obvious one.
|
||||
func zdebugDeclaredSize(s *elf.Section) (uint64, bool) {
|
||||
// no flag distinguishes this form, so the section name is the only signal, and Open gates on the name
|
||||
// before it so much as looks at the bytes. Matching that gate exactly is what keeps an ordinary
|
||||
// section whose contents happen to begin with "ZLIB" out of scope: Open will never decompress it, so
|
||||
// neither may this.
|
||||
if !strings.HasPrefix(s.Name, ".zdebug") {
|
||||
return 0, false
|
||||
}
|
||||
// unlike the Chdr above, nothing in the parse has looked at this header, so read it here. Reading off
|
||||
// the section is only safe because SHF_COMPRESSED is clear on this path, since elf.NewFile leaves the
|
||||
// embedded ReaderAt nil for a compressed one. Open treats a short read or absent magic as "not really
|
||||
// compressed after all" and serves the bytes as they lie.
|
||||
var hdr [legacyZlibHeaderSize]byte
|
||||
if n, _ := s.ReadAt(hdr[:], 0); n != legacyZlibHeaderSize || string(hdr[:4]) != "ZLIB" {
|
||||
return 0, false
|
||||
}
|
||||
// the size is big-endian whatever byte order the rest of the file uses. The header predates the gABI
|
||||
// mechanism and was simply defined that way, carrying no endianness of its own.
|
||||
return binary.BigEndian.Uint64(hdr[4:]), true
|
||||
}
|
||||
|
||||
// CheckSectionNameTable bounds the one section elf.NewFile expands on its own. Anything it cannot walk is
|
||||
// passed through untouched, leaving elf.NewFile to describe the file. It does not repeat every check
|
||||
// elf.NewFile makes ahead of that expansion, though, so a file that is both malformed and oversized can
|
||||
// come back with this error rather than an *elf.FormatError.
|
||||
//
|
||||
// NewFile calls this itself. It is exported for the callers that cannot use NewFile because the
|
||||
// debug/elf call is made for them inside another package, debug/buildinfo being the one syft reaches:
|
||||
// gate the reader on this and the eager section-name table read is bounded, which is everything that
|
||||
// package expands today, since it reaches .go.buildinfo through the program headers instead.
|
||||
//
|
||||
// This is deliberately the eager half only, so it is the wrong gate for a caller whose parser goes on to
|
||||
// read sections of its own. Use CheckAllSections for those: a package that reads symbols expands sections
|
||||
// this never looks at, and the difference is the whole reason both exist.
|
||||
func CheckSectionNameTable(r io.ReaderAt) error {
|
||||
class, order, ok := identify(r)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
sh, ok := sectionNameTableHeader(r, class, order)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
// the name table's own name is not resolved at the point elf.NewFile reads it, so debug/elf cannot
|
||||
// take the legacy .zdebug path for this section: only a compression header can apply
|
||||
if sh.flags&uint64(elf.SHF_COMPRESSED) == 0 {
|
||||
return nil
|
||||
}
|
||||
declared, err := compressedSize(r, class, order, sh.offset)
|
||||
if err != nil {
|
||||
return nil //nolint:nilerr // truncated section; elf.NewFile will say so
|
||||
}
|
||||
if declared > maxDeclaredSectionSize {
|
||||
return fmt.Errorf("%w: elf section name table declares %d decompressed bytes, over the %d byte limit",
|
||||
ErrDeclaredSizeExceeded, declared, maxDeclaredSectionSize)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// identify decodes the class and byte order every later read depends on, out of the file's 16
|
||||
// identification bytes. It reports false for anything that is not an ELF this package understands, so
|
||||
// elf.NewFile remains the single source of format errors.
|
||||
func identify(r io.ReaderAt) (elf.Class, binary.ByteOrder, bool) {
|
||||
var ident [16]byte
|
||||
if _, err := io.ReadFull(io.NewSectionReader(r, 0, int64(len(ident))), ident[:]); err != nil {
|
||||
return 0, nil, false
|
||||
}
|
||||
if string(ident[:4]) != elf.ELFMAG {
|
||||
return 0, nil, false
|
||||
}
|
||||
|
||||
class := elf.Class(ident[elf.EI_CLASS])
|
||||
if class != elf.ELFCLASS32 && class != elf.ELFCLASS64 {
|
||||
return 0, nil, false
|
||||
}
|
||||
|
||||
switch elf.Data(ident[elf.EI_DATA]) {
|
||||
case elf.ELFDATA2LSB:
|
||||
return class, binary.LittleEndian, true
|
||||
case elf.ELFDATA2MSB:
|
||||
return class, binary.BigEndian, true
|
||||
}
|
||||
return 0, nil, false
|
||||
}
|
||||
|
||||
// sectionNameTableHeader locates the header of the section syft's own parse cannot reach, the one
|
||||
// elf.NewFile expands for itself. It reports false whenever the file names no name table or is malformed
|
||||
// enough that nothing gets decompressed: either way there is nothing to bound, and describing the file is
|
||||
// elf.NewFile's job rather than this package's.
|
||||
func sectionNameTableHeader(r io.ReaderAt, class elf.Class, order binary.ByteOrder) (sectionHeader, bool) {
|
||||
shoff, shentsize, shnum, shstrndx, err := fileHeader(r, class, order)
|
||||
if err != nil || shoff <= 0 || shentsize < sectionHeaderSize(class) {
|
||||
return sectionHeader{}, false
|
||||
}
|
||||
|
||||
// a zero e_shnum means the real count lives in section 0's size field, and an e_shstrndx of
|
||||
// SHN_XINDEX means the real string table index lives in that same header's link field
|
||||
if shnum == 0 {
|
||||
sh, err := readSectionHeader(r, class, order, shoff)
|
||||
if err != nil {
|
||||
return sectionHeader{}, false
|
||||
}
|
||||
shnum = sh.size
|
||||
if shstrndx == uint64(elf.SHN_XINDEX) {
|
||||
shstrndx = uint64(sh.link)
|
||||
}
|
||||
}
|
||||
|
||||
// an index of zero means the file has no section name table, so elf.NewFile returns before reading one.
|
||||
// An out-of-range index expands nothing either, though elf.NewFile only rejects it when e_shnum was
|
||||
// non-zero; reached through SHN_XINDEX it indexes out of range and panics instead. That is a
|
||||
// recoverable panic the task executor already contains, unlike the OOM this package exists to stop.
|
||||
if shstrndx == 0 || shstrndx >= shnum {
|
||||
return sectionHeader{}, false
|
||||
}
|
||||
// the index is file-controlled, so guard the multiply and then the sum against a table the file
|
||||
// places past the end of the address space
|
||||
if shstrndx > uint64(math.MaxInt64)/uint64(shentsize) {
|
||||
return sectionHeader{}, false
|
||||
}
|
||||
off := shoff + int64(shstrndx)*shentsize
|
||||
if off < 0 {
|
||||
return sectionHeader{}, false
|
||||
}
|
||||
|
||||
sh, err := readSectionHeader(r, class, order, off)
|
||||
if err != nil {
|
||||
return sectionHeader{}, false
|
||||
}
|
||||
return sh, true
|
||||
}
|
||||
|
||||
// sectionHeader is the handful of fields we need out of an Elf32_Shdr or an Elf64_Shdr.
|
||||
type sectionHeader struct {
|
||||
flags uint64
|
||||
offset int64
|
||||
size uint64
|
||||
link uint32
|
||||
}
|
||||
|
||||
// fileHeader returns the section table location and shape out of the ELF file header.
|
||||
func fileHeader(r io.ReaderAt, class elf.Class, order binary.ByteOrder) (shoff, shentsize int64, shnum, shstrndx uint64, err error) {
|
||||
if class == elf.ELFCLASS32 {
|
||||
var h elf.Header32
|
||||
if err := binary.Read(io.NewSectionReader(r, 0, int64(binary.Size(h))), order, &h); err != nil {
|
||||
return 0, 0, 0, 0, err
|
||||
}
|
||||
return int64(h.Shoff), int64(h.Shentsize), uint64(h.Shnum), uint64(h.Shstrndx), nil
|
||||
}
|
||||
var h elf.Header64
|
||||
if err := binary.Read(io.NewSectionReader(r, 0, int64(binary.Size(h))), order, &h); err != nil {
|
||||
return 0, 0, 0, 0, err
|
||||
}
|
||||
// Shoff is a uint64, so a file may claim an offset that does not fit in an int64; the caller rejects
|
||||
// a negative result rather than wrapping into a plausible-looking one
|
||||
return int64(h.Shoff), int64(h.Shentsize), uint64(h.Shnum), uint64(h.Shstrndx), nil
|
||||
}
|
||||
|
||||
// readSectionHeader decodes one section header. Reads go through binary.Read, which uses io.ReadFull, so
|
||||
// a short read is an error rather than a partially-zeroed header.
|
||||
func readSectionHeader(r io.ReaderAt, class elf.Class, order binary.ByteOrder, off int64) (sectionHeader, error) {
|
||||
sr := io.NewSectionReader(r, off, sectionHeaderSize(class))
|
||||
if class == elf.ELFCLASS32 {
|
||||
var sh elf.Section32
|
||||
if err := binary.Read(sr, order, &sh); err != nil {
|
||||
return sectionHeader{}, err
|
||||
}
|
||||
return sectionHeader{
|
||||
flags: uint64(sh.Flags),
|
||||
offset: int64(sh.Off),
|
||||
size: uint64(sh.Size),
|
||||
link: sh.Link,
|
||||
}, nil
|
||||
}
|
||||
var sh elf.Section64
|
||||
if err := binary.Read(sr, order, &sh); err != nil {
|
||||
return sectionHeader{}, err
|
||||
}
|
||||
return sectionHeader{
|
||||
flags: sh.Flags,
|
||||
offset: int64(sh.Off),
|
||||
size: sh.Size,
|
||||
link: sh.Link,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// compressedSize reads the decompressed size out of a section's SHF_COMPRESSED header. Chdr64 carries a
|
||||
// blank field that binary.Size counts and binary.Read skips, so the decoded offsets match what debug/elf
|
||||
// reaches for with unsafe.Offsetof.
|
||||
func compressedSize(r io.ReaderAt, class elf.Class, order binary.ByteOrder, off int64) (uint64, error) {
|
||||
if off < 0 {
|
||||
return 0, fmt.Errorf("negative section offset")
|
||||
}
|
||||
sr := io.NewSectionReader(r, off, compressionHeaderSize(class))
|
||||
if class == elf.ELFCLASS32 {
|
||||
var ch elf.Chdr32
|
||||
if err := binary.Read(sr, order, &ch); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint64(ch.Size), nil
|
||||
}
|
||||
var ch elf.Chdr64
|
||||
if err := binary.Read(sr, order, &ch); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return ch.Size, nil
|
||||
}
|
||||
|
||||
func sectionHeaderSize(class elf.Class) int64 {
|
||||
if class == elf.ELFCLASS32 {
|
||||
return sizeSection32
|
||||
}
|
||||
return sizeSection64
|
||||
}
|
||||
|
||||
func compressionHeaderSize(class elf.Class) int64 {
|
||||
if class == elf.ELFCLASS32 {
|
||||
return sizeChdr32
|
||||
}
|
||||
return sizeChdr64
|
||||
}
|
||||
@@ -0,0 +1,736 @@
|
||||
package elfutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"debug/elf"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// section describes one section to place in a fixture. The compression headers are deliberately
|
||||
// separable from the payload so a fixture can claim far more than it can deliver, which is the whole
|
||||
// shape of the attack under test.
|
||||
type section struct {
|
||||
name string
|
||||
typ elf.SectionType
|
||||
flags elf.SectionFlag
|
||||
link uint32
|
||||
body []byte
|
||||
compressed bool // emit a SHF_COMPRESSED chdr ahead of body
|
||||
legacyZlib bool // emit a 12-byte .zdebug-style ZLIB header ahead of body
|
||||
declaredSize uint64
|
||||
offsetOf string // borrow another section's file offset, for SHT_NOBITS fixtures
|
||||
shSize uint64 // override sh_size, since an SHT_NOBITS section's size is not its span in the file
|
||||
}
|
||||
|
||||
type buildOpts struct {
|
||||
// nameTable overrides the generated .shstrtab, so a fixture can compress the one section
|
||||
// elf.NewFile expands on its own.
|
||||
nameTable *section
|
||||
// extendedShnum parks the real section count in section 0 and zeroes e_shnum.
|
||||
extendedShnum bool
|
||||
// xindexShstrndx sets e_shstrndx to SHN_XINDEX so the real index comes from section 0's link.
|
||||
xindexShstrndx bool
|
||||
}
|
||||
|
||||
// buildELF assembles a minimal ELF: a null section, then secs, then a generated .shstrtab that
|
||||
// e_shstrndx points at.
|
||||
func buildELF(t *testing.T, class elf.Class, order binary.ByteOrder, secs []section, opts buildOpts) []byte {
|
||||
t.Helper()
|
||||
|
||||
nameTable := section{name: ".shstrtab", typ: elf.SHT_STRTAB}
|
||||
if opts.nameTable != nil {
|
||||
nameTable = *opts.nameTable
|
||||
nameTable.name = ".shstrtab"
|
||||
nameTable.typ = elf.SHT_STRTAB
|
||||
}
|
||||
|
||||
all := append([]section{{}}, secs...)
|
||||
all = append(all, nameTable)
|
||||
shstrndx := len(all) - 1
|
||||
|
||||
// the name table's body is the names of every section, so it has to be built before the payloads
|
||||
var names bytes.Buffer
|
||||
names.WriteByte(0)
|
||||
nameOff := make([]uint32, len(all))
|
||||
for i, s := range all {
|
||||
if s.name == "" {
|
||||
continue
|
||||
}
|
||||
nameOff[i] = uint32(names.Len())
|
||||
names.WriteString(s.name)
|
||||
names.WriteByte(0)
|
||||
}
|
||||
all[shstrndx].body = names.Bytes()
|
||||
// a fixture that compresses the name table without rigging a size gets an honest one, so the file is
|
||||
// readable rather than merely parseable
|
||||
if all[shstrndx].compressed && all[shstrndx].declaredSize == 0 {
|
||||
all[shstrndx].declaredSize = uint64(names.Len())
|
||||
all[shstrndx].body = deflate(t, names.Bytes())
|
||||
}
|
||||
|
||||
is32 := class == elf.ELFCLASS32
|
||||
ehsize, shentsize := binary.Size(elf.Header64{}), binary.Size(elf.Section64{})
|
||||
machine := elf.EM_X86_64
|
||||
if is32 {
|
||||
ehsize, shentsize = binary.Size(elf.Header32{}), binary.Size(elf.Section32{})
|
||||
machine = elf.EM_386
|
||||
}
|
||||
|
||||
shoff := ehsize
|
||||
dataOff := shoff + len(all)*shentsize
|
||||
|
||||
// lay the payloads out and remember where each landed, so an SHT_NOBITS fixture can point at one
|
||||
payloads := make([][]byte, len(all))
|
||||
offsets := make(map[string]uint64)
|
||||
cursor := uint64(dataOff)
|
||||
for i, s := range all {
|
||||
payloads[i] = sectionContent(t, class, order, s)
|
||||
offsets[s.name] = cursor
|
||||
cursor += uint64(len(payloads[i]))
|
||||
}
|
||||
|
||||
var ident [16]byte
|
||||
copy(ident[:], elf.ELFMAG)
|
||||
ident[elf.EI_CLASS] = byte(class)
|
||||
ident[elf.EI_DATA] = byte(elf.ELFDATA2LSB)
|
||||
if order == binary.BigEndian {
|
||||
ident[elf.EI_DATA] = byte(elf.ELFDATA2MSB)
|
||||
}
|
||||
ident[elf.EI_VERSION] = byte(elf.EV_CURRENT)
|
||||
|
||||
shnum := uint16(len(all))
|
||||
if opts.extendedShnum {
|
||||
shnum = 0
|
||||
}
|
||||
strndx := uint16(shstrndx)
|
||||
if opts.xindexShstrndx {
|
||||
strndx = uint16(elf.SHN_XINDEX)
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
if is32 {
|
||||
write(t, buf, order, elf.Header32{
|
||||
Ident: ident, Type: uint16(elf.ET_REL), Machine: uint16(machine), Version: uint32(elf.EV_CURRENT),
|
||||
Shoff: uint32(shoff), Ehsize: uint16(ehsize), Shentsize: uint16(shentsize),
|
||||
Shnum: shnum, Shstrndx: strndx,
|
||||
})
|
||||
} else {
|
||||
write(t, buf, order, elf.Header64{
|
||||
Ident: ident, Type: uint16(elf.ET_REL), Machine: uint16(machine), Version: uint32(elf.EV_CURRENT),
|
||||
Shoff: uint64(shoff), Ehsize: uint16(ehsize), Shentsize: uint16(shentsize),
|
||||
Shnum: shnum, Shstrndx: strndx,
|
||||
})
|
||||
}
|
||||
|
||||
off := uint64(dataOff)
|
||||
for i, s := range all {
|
||||
size := uint64(len(payloads[i]))
|
||||
if s.shSize != 0 {
|
||||
size = s.shSize
|
||||
}
|
||||
secOff := off
|
||||
if s.offsetOf != "" {
|
||||
secOff = offsets[s.offsetOf]
|
||||
}
|
||||
flags := s.flags
|
||||
if s.compressed {
|
||||
// the content header and the flag always travel together in a real file
|
||||
flags |= elf.SHF_COMPRESSED
|
||||
}
|
||||
link := s.link
|
||||
// section 0 carries the extended count and the extended name table index
|
||||
if i == 0 {
|
||||
if opts.extendedShnum {
|
||||
size = uint64(len(all))
|
||||
}
|
||||
if opts.xindexShstrndx {
|
||||
link = uint32(shstrndx)
|
||||
}
|
||||
}
|
||||
if is32 {
|
||||
write(t, buf, order, elf.Section32{
|
||||
Name: nameOff[i], Type: uint32(s.typ), Flags: uint32(flags), Link: link,
|
||||
Off: uint32(secOff), Size: uint32(size), Addralign: 1,
|
||||
})
|
||||
} else {
|
||||
write(t, buf, order, elf.Section64{
|
||||
Name: nameOff[i], Type: uint32(s.typ), Flags: uint64(flags), Link: link,
|
||||
Off: secOff, Size: size, Addralign: 1,
|
||||
})
|
||||
}
|
||||
off += uint64(len(payloads[i]))
|
||||
}
|
||||
|
||||
require.Equal(t, dataOff, buf.Len(), "fixture layout drifted from the declared offsets")
|
||||
for _, p := range payloads {
|
||||
buf.Write(p)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// sectionContent wraps the body in whichever compression header the fixture asks for.
|
||||
func sectionContent(t *testing.T, class elf.Class, order binary.ByteOrder, s section) []byte {
|
||||
t.Helper()
|
||||
|
||||
switch {
|
||||
case s.legacyZlib:
|
||||
hdr := make([]byte, legacyZlibHeaderSize)
|
||||
copy(hdr, "ZLIB")
|
||||
binary.BigEndian.PutUint64(hdr[4:], s.declaredSize)
|
||||
return append(hdr, s.body...)
|
||||
case s.compressed:
|
||||
buf := &bytes.Buffer{}
|
||||
if class == elf.ELFCLASS32 {
|
||||
write(t, buf, order, elf.Chdr32{
|
||||
Type: uint32(elf.COMPRESS_ZLIB), Size: uint32(s.declaredSize), Addralign: 1,
|
||||
})
|
||||
} else {
|
||||
write(t, buf, order, elf.Chdr64{
|
||||
Type: uint32(elf.COMPRESS_ZLIB), Size: s.declaredSize, Addralign: 1,
|
||||
})
|
||||
}
|
||||
buf.Write(s.body)
|
||||
return buf.Bytes()
|
||||
default:
|
||||
return s.body
|
||||
}
|
||||
}
|
||||
|
||||
func write(t *testing.T, w io.Writer, order binary.ByteOrder, v any) {
|
||||
t.Helper()
|
||||
require.NoError(t, binary.Write(w, order, v))
|
||||
}
|
||||
|
||||
// deflate returns a real zlib stream, so a fixture using it actually delivers the bytes it promises.
|
||||
func deflate(t *testing.T, payload []byte) []byte {
|
||||
t.Helper()
|
||||
buf := &bytes.Buffer{}
|
||||
zw := zlib.NewWriter(buf)
|
||||
_, err := zw.Write(payload)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, zw.Close())
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// classes is the matrix every structural case runs under, since the header layouts differ per class and
|
||||
// the compression header's size field is a different width in each.
|
||||
var classes = []struct {
|
||||
name string
|
||||
class elf.Class
|
||||
order binary.ByteOrder
|
||||
}{
|
||||
{"64-bit little-endian", elf.ELFCLASS64, binary.LittleEndian},
|
||||
{"64-bit big-endian", elf.ELFCLASS64, binary.BigEndian},
|
||||
{"32-bit little-endian", elf.ELFCLASS32, binary.LittleEndian},
|
||||
{"32-bit big-endian", elf.ELFCLASS32, binary.BigEndian},
|
||||
}
|
||||
|
||||
const overLimit = maxDeclaredSectionSize + 1
|
||||
|
||||
func TestNewFile_RejectsOversizedReachableSections(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
secs []section
|
||||
opts buildOpts
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "compressed .symtab",
|
||||
secs: []section{
|
||||
{name: ".symtab", typ: elf.SHT_SYMTAB, link: 2, compressed: true, declaredSize: overLimit},
|
||||
{name: ".strtab", typ: elf.SHT_STRTAB},
|
||||
},
|
||||
wantErr: `".symtab"`,
|
||||
},
|
||||
{
|
||||
name: "the string table a .symtab links to",
|
||||
secs: []section{
|
||||
{name: ".symtab", typ: elf.SHT_SYMTAB, link: 2},
|
||||
{name: ".strtab", typ: elf.SHT_STRTAB, compressed: true, declaredSize: overLimit},
|
||||
},
|
||||
wantErr: `".strtab"`,
|
||||
},
|
||||
{
|
||||
name: "compressed .dynsym",
|
||||
secs: []section{
|
||||
{name: ".dynsym", typ: elf.SHT_DYNSYM, link: 2, compressed: true, declaredSize: overLimit},
|
||||
{name: ".dynstr", typ: elf.SHT_STRTAB},
|
||||
},
|
||||
wantErr: `".dynsym"`,
|
||||
},
|
||||
{
|
||||
name: "compressed .dynamic",
|
||||
secs: []section{
|
||||
{name: ".dynamic", typ: elf.SHT_DYNAMIC, compressed: true, declaredSize: overLimit},
|
||||
},
|
||||
wantErr: `".dynamic"`,
|
||||
},
|
||||
{
|
||||
name: "compressed .note.package, reached by name rather than type",
|
||||
secs: []section{
|
||||
{name: ".note.package", typ: elf.SHT_NOTE, compressed: true, declaredSize: overLimit},
|
||||
},
|
||||
wantErr: `".note.package"`,
|
||||
},
|
||||
{
|
||||
name: "compressed .modinfo, read by the kernel module cataloger",
|
||||
secs: []section{
|
||||
{name: ".modinfo", typ: elf.SHT_PROGBITS, compressed: true, declaredSize: overLimit},
|
||||
},
|
||||
wantErr: `".modinfo"`,
|
||||
},
|
||||
{
|
||||
// File.DynamicSymbols reads these via gnuVersionInit without the caller naming them
|
||||
name: "compressed .gnu.version alongside a .dynsym",
|
||||
secs: []section{
|
||||
{name: ".dynsym", typ: elf.SHT_DYNSYM, link: 2},
|
||||
{name: ".dynstr", typ: elf.SHT_STRTAB},
|
||||
{name: ".gnu.version", typ: elf.SHT_GNU_VERSYM, compressed: true, declaredSize: overLimit},
|
||||
},
|
||||
wantErr: `".gnu.version"`,
|
||||
},
|
||||
{
|
||||
name: "compressed .gnu.version_r alongside a .dynsym",
|
||||
secs: []section{
|
||||
{name: ".dynsym", typ: elf.SHT_DYNSYM, link: 2},
|
||||
{name: ".dynstr", typ: elf.SHT_STRTAB},
|
||||
{name: ".gnu.version_r", typ: elf.SHT_GNU_VERNEED, compressed: true, declaredSize: overLimit},
|
||||
},
|
||||
wantErr: `".gnu.version_r"`,
|
||||
},
|
||||
{
|
||||
name: "compressed .gnu.version_d alongside a .dynsym",
|
||||
secs: []section{
|
||||
{name: ".dynsym", typ: elf.SHT_DYNSYM, link: 2},
|
||||
{name: ".dynstr", typ: elf.SHT_STRTAB},
|
||||
{name: ".gnu.version_d", typ: elf.SHT_GNU_VERDEF, compressed: true, declaredSize: overLimit},
|
||||
},
|
||||
wantErr: `".gnu.version_d"`,
|
||||
},
|
||||
{
|
||||
// the one section elf.NewFile expands itself, so this has to be caught before the parse
|
||||
name: "compressed section name table",
|
||||
opts: buildOpts{nameTable: §ion{compressed: true, declaredSize: overLimit}},
|
||||
wantErr: "section name table",
|
||||
},
|
||||
{
|
||||
// debug/elf gates the legacy form on the name, so a reachable section named this way reaches it
|
||||
name: "legacy .zdebug header on a section reached by type",
|
||||
secs: []section{
|
||||
{name: ".zdebug_symtab", typ: elf.SHT_SYMTAB, link: 2, legacyZlib: true, declaredSize: overLimit},
|
||||
{name: ".strtab", typ: elf.SHT_STRTAB},
|
||||
},
|
||||
wantErr: `".zdebug_symtab"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
for _, c := range classes {
|
||||
t.Run(tt.name+"/"+c.name, func(t *testing.T) {
|
||||
data := buildELF(t, c.class, c.order, tt.secs, tt.opts)
|
||||
_, err := NewFile(bytes.NewReader(data))
|
||||
require.Error(t, err)
|
||||
// the fixtures are hand-assembled ELF bytes, so a bad one would satisfy require.Error on
|
||||
// its own. Naming the section proves the rejection is the one we set up, and naming the
|
||||
// limit proves it came from this package rather than from debug/elf.
|
||||
// the sentinel is what the callers key their reporting on, so it is asserted here rather
|
||||
// than only from the packages downstream that consume it
|
||||
assert.ErrorIs(t, err, ErrDeclaredSizeExceeded)
|
||||
assert.Contains(t, err.Error(), tt.wantErr)
|
||||
assert.Contains(t, err.Error(), fmt.Sprint(maxDeclaredSectionSize))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewFile_AcceptsWhatDebugELFWouldNotExpand covers the false-negative direction: rejecting a whole
|
||||
// binary over a section debug/elf is never driven to decompress drops real packages from the SBOM.
|
||||
//
|
||||
// Every fixture here is a file debug/elf parses without complaint, and every one must come back from
|
||||
// NewFile without complaint too. An oversized claim is only worth rejecting when debug/elf can actually
|
||||
// be made to allocate against it, so a claim it will never act on has to be ignored outright rather than
|
||||
// merely tolerated.
|
||||
func TestNewFile_AcceptsWhatDebugELFWouldNotExpand(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
secs []section
|
||||
why string
|
||||
}{
|
||||
{
|
||||
// only File.DWARF expands a .debug_* section, and nothing in syft calls it, so this claim is
|
||||
// never acted on. reachableSections leaves DWARF out on purpose: compressed debug info is
|
||||
// routinely enormous in a legitimate binary, and bounding it would skip the binary entirely.
|
||||
//
|
||||
// non-allocable and SHF_COMPRESSED is deliberately the one shape debug/elf will expand, so the
|
||||
// name is the only thing keeping this accepted. Adding SHF_ALLOC would make it pass for the
|
||||
// allocable case's reason below and stop saying anything about DWARF.
|
||||
name: "oversized compressed DWARF",
|
||||
secs: []section{
|
||||
{name: ".debug_info", typ: elf.SHT_PROGBITS, compressed: true, declaredSize: overLimit},
|
||||
},
|
||||
why: "syft never calls File.DWARF, so debug/elf is never asked to expand it",
|
||||
},
|
||||
{
|
||||
// the same section in the older form toolchains emitted before SHF_COMPRESSED existed. The
|
||||
// header is different, the reader that would expand it is the same one syft never calls.
|
||||
//
|
||||
// SHF_COMPRESSED is deliberately absent: with it set, debug/elf takes the modern path and the
|
||||
// legacy header is never consulted, so the .zdebug name gate would go untested.
|
||||
name: "oversized legacy .zdebug DWARF",
|
||||
secs: []section{
|
||||
{name: ".zdebug_info", typ: elf.SHT_PROGBITS, legacyZlib: true, declaredSize: overLimit},
|
||||
},
|
||||
why: "same, via the legacy form",
|
||||
},
|
||||
{
|
||||
// reachable by type and named the way debug/elf gates the legacy form on, so the name half of
|
||||
// that gate passes and the magic is the only thing left to reject the claim. Without the magic
|
||||
// check the 0xff bytes behind it decode as a declared size of 2^64-1.
|
||||
name: "a .zdebug-named reachable section whose bytes are not a legacy header",
|
||||
secs: []section{
|
||||
{name: ".zdebug_symtab", typ: elf.SHT_SYMTAB, link: 2,
|
||||
body: append([]byte("NOPE"), bytes.Repeat([]byte{0xff}, 32)...)},
|
||||
{name: ".strtab", typ: elf.SHT_STRTAB},
|
||||
},
|
||||
why: "the legacy form needs the magic as well as the name",
|
||||
},
|
||||
{
|
||||
// the other half of the same gate: fewer bytes present than a legacy header needs. The magic is
|
||||
// intact and some of the size field is present, so a short read has to read as "no claim"
|
||||
// rather than as a size decoded out of a partly-filled buffer, which here would be huge.
|
||||
name: "a .zdebug-named reachable section too short to hold a legacy header",
|
||||
secs: []section{
|
||||
{name: ".zdebug_symtab", typ: elf.SHT_SYMTAB, link: 2,
|
||||
body: append([]byte("ZLIB"), bytes.Repeat([]byte{0xff}, 4)...)},
|
||||
{name: ".strtab", typ: elf.SHT_STRTAB},
|
||||
},
|
||||
why: "a truncated legacy header is not a claim",
|
||||
},
|
||||
{
|
||||
// an ordinary uncompressed note that happens to open with those four bytes. debug/elf takes
|
||||
// the legacy path only for a .zdebug-prefixed name, so it reads this as content and nothing
|
||||
// here is a size claim at all. A check that sniffed for the magic instead would decode the
|
||||
// 0xff padding behind it as a declared size and throw the binary away.
|
||||
//
|
||||
// .note.package because it has to be a section syft actually reads, or reachableSections skips
|
||||
// it and the case passes without exercising anything. No flags for the same reason the name is
|
||||
// not .zdebug-prefixed: either one would put debug/elf on a path where the bytes are a header.
|
||||
name: "an uncompressed section whose bytes happen to start with ZLIB",
|
||||
secs: []section{
|
||||
{name: ".note.package", typ: elf.SHT_NOTE, body: append([]byte("ZLIB"), bytes.Repeat([]byte{0xff}, 32)...)},
|
||||
},
|
||||
why: "debug/elf gates the legacy form on the section name, not the magic, so this is not a claim at all",
|
||||
},
|
||||
{
|
||||
// a section that occupies memory at runtime cannot be compressed on disk, so (*Section).Open
|
||||
// hands back the raw bytes rather than expanding one that says it is. The claim is real and
|
||||
// oversized, and still costs nothing.
|
||||
//
|
||||
// this is the ".symtab" reject case with one bit added: .data is read by name just as .symtab
|
||||
// is, so SHF_ALLOC is the only reason the two outcomes differ.
|
||||
name: "an allocable compressed section",
|
||||
secs: []section{
|
||||
{name: ".data", typ: elf.SHT_PROGBITS, flags: elf.SHF_COMPRESSED | elf.SHF_ALLOC,
|
||||
compressed: true, declaredSize: overLimit},
|
||||
},
|
||||
why: "debug/elf refuses to decompress an allocable section",
|
||||
},
|
||||
{
|
||||
// the hardest one to see: sh_offset points into .rodata's payload, so elf.NewFile reads that
|
||||
// compression header and .data comes back carrying an oversized Size of its own. Reading Size
|
||||
// alone would reject here, but a SHT_NOBITS section holds no file bytes and its reader yields
|
||||
// none, so the size is never allocated against. sh_size is set by hand because such a section
|
||||
// has a size without occupying any of the file, and a zero one is a file debug/elf rejects.
|
||||
//
|
||||
// SHF_COMPRESSED has to be set for the size to be a claim at all, and .data has to be the
|
||||
// borrower rather than the donor, since the donor is unreachable by name and never checked.
|
||||
name: "an SHT_NOBITS section borrowing another section's compression header",
|
||||
secs: []section{
|
||||
{name: ".rodata", typ: elf.SHT_PROGBITS, compressed: true, declaredSize: overLimit},
|
||||
{name: ".data", typ: elf.SHT_NOBITS, flags: elf.SHF_COMPRESSED, offsetOf: ".rodata",
|
||||
shSize: 4096},
|
||||
},
|
||||
why: "debug/elf never yields bytes for SHT_NOBITS, whatever Size it ends up carrying",
|
||||
},
|
||||
{
|
||||
// a string table is reached through some symbol table's sh_link, never on its own, so one
|
||||
// nothing points at is one nothing reads. This is the reject test's linked-.strtab case with
|
||||
// the .symtab removed, and it is the case an implementation that bounded every SHT_STRTAB
|
||||
// rather than following the links would fail.
|
||||
name: "an oversized string table that no symbol table links",
|
||||
secs: []section{
|
||||
{name: ".strtab", typ: elf.SHT_STRTAB, compressed: true, declaredSize: overLimit},
|
||||
},
|
||||
why: "reachability runs through sh_link, not section type",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
for _, c := range classes {
|
||||
t.Run(tt.name+"/"+c.name, func(t *testing.T) {
|
||||
data := buildELF(t, c.class, c.order, tt.secs, buildOpts{})
|
||||
|
||||
// "elfutil accepted it" only means something if debug/elf accepts it too, so a fixture
|
||||
// that drifts into being malformed fails here rather than passing for the wrong reason
|
||||
_, err := elf.NewFile(bytes.NewReader(data))
|
||||
require.NoError(t, err, "fixture is supposed to be a file debug/elf accepts")
|
||||
|
||||
_, err = NewFile(bytes.NewReader(data))
|
||||
require.NoError(t, err, tt.why)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewFile_AcceptsAndReadsSectionUnderTheLimit guards the risk the bound introduces: a legitimately
|
||||
// compressed section must still be readable, not merely accepted.
|
||||
func TestNewFile_AcceptsAndReadsSectionUnderTheLimit(t *testing.T) {
|
||||
for _, c := range classes {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
payload := bytes.Repeat([]byte("syft"), 4096)
|
||||
data := buildELF(t, c.class, c.order, []section{
|
||||
{name: ".note.package", typ: elf.SHT_NOTE, flags: elf.SHF_COMPRESSED,
|
||||
compressed: true, declaredSize: uint64(len(payload)), body: deflate(t, payload)},
|
||||
}, buildOpts{})
|
||||
|
||||
f, err := NewFile(bytes.NewReader(data))
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := f.Section(".note.package").Data()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, payload, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewFile_AcceptsAndReadsCompressedSectionNameTable covers the other half of CheckSectionNameTable.
|
||||
// That check runs before the parse, off a hand-decoded section header rather than anything debug/elf has
|
||||
// resolved, so a version of it that rejected every compressed name table outright, or that read the size
|
||||
// out of the wrong offset, would still pass every rejection case. Resolving the names proves the file was
|
||||
// left intact and not merely let through.
|
||||
func TestNewFile_AcceptsAndReadsCompressedSectionNameTable(t *testing.T) {
|
||||
for _, c := range classes {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
data := buildELF(t, c.class, c.order,
|
||||
[]section{{name: ".note.package", typ: elf.SHT_NOTE}},
|
||||
buildOpts{nameTable: §ion{compressed: true}})
|
||||
|
||||
f, err := NewFile(bytes.NewReader(data))
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, f.Section(".note.package"), "section names did not survive the name table")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewFile_BombDoesNotAllocate is the end-to-end case: a real zlib stream that genuinely delivers
|
||||
// every byte its header promises, so without the bound debug/elf allocates all of them.
|
||||
func TestNewFile_BombDoesNotAllocate(t *testing.T) {
|
||||
const declared = maxDeclaredSectionSize + 1
|
||||
compressed := deflate(t, make([]byte, declared))
|
||||
t.Logf("%d declared bytes compress to %d", declared, len(compressed))
|
||||
|
||||
data := buildELF(t, elf.ELFCLASS64, binary.LittleEndian, []section{
|
||||
{name: ".symtab", typ: elf.SHT_SYMTAB, link: 2, flags: elf.SHF_COMPRESSED,
|
||||
compressed: true, declaredSize: declared, body: compressed},
|
||||
{name: ".strtab", typ: elf.SHT_STRTAB},
|
||||
}, buildOpts{})
|
||||
|
||||
var before, after runtime.MemStats
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&before)
|
||||
|
||||
_, err := NewFile(bytes.NewReader(data))
|
||||
|
||||
runtime.ReadMemStats(&after)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "over the")
|
||||
|
||||
// the guard has to reject before the allocation, not after it. The margin is loose on purpose: this
|
||||
// is asserting "nothing near the declared size was allocated", not a precise budget.
|
||||
const margin = 32 * 1024 * 1024
|
||||
assert.Less(t, after.TotalAlloc-before.TotalAlloc, uint64(margin),
|
||||
"parsing allocated far more than the input warrants")
|
||||
}
|
||||
|
||||
// TestNewFile_MatchesDebugELFOnStructuralEdges pins the invariant the passthrough returns depend on:
|
||||
// wherever elfutil declines to check, elf.NewFile must still be the one deciding.
|
||||
func TestNewFile_MatchesDebugELFOnStructuralEdges(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
secs []section
|
||||
opts buildOpts
|
||||
}{
|
||||
{name: "plain file", secs: []section{{name: ".text", typ: elf.SHT_PROGBITS, flags: elf.SHF_ALLOC}}},
|
||||
{name: "extended section count", opts: buildOpts{extendedShnum: true}},
|
||||
{name: "extended name table index", opts: buildOpts{xindexShstrndx: true}},
|
||||
{name: "both extended forms", opts: buildOpts{extendedShnum: true, xindexShstrndx: true}},
|
||||
{
|
||||
name: "symtab linking a nonexistent string table",
|
||||
secs: []section{{name: ".symtab", typ: elf.SHT_SYMTAB, link: 99}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
for _, c := range classes {
|
||||
t.Run(tt.name+"/"+c.name, func(t *testing.T) {
|
||||
data := buildELF(t, c.class, c.order, tt.secs, tt.opts)
|
||||
|
||||
_, want := elf.NewFile(bytes.NewReader(data))
|
||||
_, got := NewFile(bytes.NewReader(data))
|
||||
|
||||
if want == nil {
|
||||
assert.NoError(t, got, "elfutil rejected a file debug/elf accepts")
|
||||
return
|
||||
}
|
||||
require.Error(t, got, "elfutil accepted a file debug/elf rejects")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewFile_ExtendedShnumOverflowStillChecks covers the case a signed conversion used to swallow: a
|
||||
// count that does not fit an int64 must not read as "no sections" and skip the check.
|
||||
func TestNewFile_ExtendedShnumOverflowStillChecks(t *testing.T) {
|
||||
data := buildELF(t, elf.ELFCLASS64, binary.LittleEndian, nil,
|
||||
buildOpts{nameTable: §ion{compressed: true, declaredSize: overLimit}})
|
||||
|
||||
// rewrite section 0's size to a count that overflows int64, and zero e_shnum so it is consulted
|
||||
// e_shnum sits at 0x3c in an Elf64_Ehdr, and sh_size at +32 in the Elf64_Shdr that follows it
|
||||
shoff := binary.Size(elf.Header64{})
|
||||
binary.LittleEndian.PutUint16(data[0x3c:], 0)
|
||||
binary.LittleEndian.PutUint64(data[shoff+32:], uint64(1)<<63)
|
||||
|
||||
_, err := NewFile(bytes.NewReader(data))
|
||||
require.Error(t, err, "a bogus section count must not disable the check")
|
||||
}
|
||||
|
||||
// TestCheckAllSections covers the standalone gate, used by callers whose debug/elf parse happens inside
|
||||
// another package (goversion, here) rather than through NewFile.
|
||||
func TestCheckAllSections(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data func(t *testing.T) []byte
|
||||
// wantErr is empty when CheckAllSections must return nil.
|
||||
wantErr string
|
||||
// nameTableMisses marks a fixture CheckSectionNameTable alone does not reject, since the oversized
|
||||
// section here is one debug/elf only expands lazily, after the parse CheckAllSections goes on to do.
|
||||
nameTableMisses bool
|
||||
}{
|
||||
{
|
||||
name: "non-ELF reader",
|
||||
data: func(t *testing.T) []byte { return []byte("not an elf") },
|
||||
},
|
||||
{
|
||||
name: "valid ident but a body elf.NewFile cannot parse",
|
||||
data: func(t *testing.T) []byte {
|
||||
full := buildELF(t, elf.ELFCLASS64, binary.LittleEndian, nil, buildOpts{})
|
||||
truncated := full[:binary.Size(elf.Header64{})+4]
|
||||
_, err := elf.NewFile(bytes.NewReader(truncated))
|
||||
require.Error(t, err, "fixture is supposed to be rejected by debug/elf")
|
||||
return truncated
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "clean small ELF",
|
||||
data: func(t *testing.T) []byte {
|
||||
data := buildELF(t, elf.ELFCLASS64, binary.LittleEndian,
|
||||
[]section{{name: ".text", typ: elf.SHT_PROGBITS, flags: elf.SHF_ALLOC}}, buildOpts{})
|
||||
_, err := elf.NewFile(bytes.NewReader(data))
|
||||
require.NoError(t, err, "fixture is supposed to be a file debug/elf accepts")
|
||||
return data
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "oversized section name table",
|
||||
data: func(t *testing.T) []byte {
|
||||
return buildELF(t, elf.ELFCLASS64, binary.LittleEndian, nil,
|
||||
buildOpts{nameTable: §ion{compressed: true, declaredSize: overLimit}})
|
||||
},
|
||||
wantErr: "section name table",
|
||||
},
|
||||
{
|
||||
name: "oversized .symtab, expanded lazily rather than during the parse",
|
||||
data: func(t *testing.T) []byte {
|
||||
data := buildELF(t, elf.ELFCLASS64, binary.LittleEndian, []section{
|
||||
{name: ".symtab", typ: elf.SHT_SYMTAB, link: 2, compressed: true, declaredSize: overLimit},
|
||||
{name: ".strtab", typ: elf.SHT_STRTAB},
|
||||
}, buildOpts{})
|
||||
_, err := elf.NewFile(bytes.NewReader(data))
|
||||
require.NoError(t, err, "fixture is supposed to be a file debug/elf accepts")
|
||||
return data
|
||||
},
|
||||
wantErr: `".symtab"`,
|
||||
nameTableMisses: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data := tt.data(t)
|
||||
|
||||
if tt.nameTableMisses {
|
||||
require.NoError(t, CheckSectionNameTable(bytes.NewReader(data)),
|
||||
"the name table check alone doesn't reach a section debug/elf only expands lazily")
|
||||
}
|
||||
|
||||
err := CheckAllSections(bytes.NewReader(data))
|
||||
if tt.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
return
|
||||
}
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrDeclaredSizeExceeded)
|
||||
assert.Contains(t, err.Error(), tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFile_PassesThroughNonELF(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
}{
|
||||
{"empty", nil},
|
||||
{"shorter than e_ident", []byte{0x7f, 'E'}},
|
||||
{"bad magic", bytes.Repeat([]byte{0xab}, 128)},
|
||||
{"bad class", func() []byte {
|
||||
b := bytes.Repeat([]byte{0}, 128)
|
||||
copy(b, elf.ELFMAG)
|
||||
b[elf.EI_CLASS] = 9
|
||||
return b
|
||||
}()},
|
||||
{"bad byte order", func() []byte {
|
||||
b := bytes.Repeat([]byte{0}, 128)
|
||||
copy(b, elf.ELFMAG)
|
||||
b[elf.EI_CLASS] = byte(elf.ELFCLASS64)
|
||||
b[elf.EI_DATA] = 9
|
||||
return b
|
||||
}()},
|
||||
{"truncated after the header", func() []byte {
|
||||
data := buildELF(t, elf.ELFCLASS64, binary.LittleEndian, nil, buildOpts{})
|
||||
return data[:binary.Size(elf.Header64{})+4]
|
||||
}()},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, want := elf.NewFile(bytes.NewReader(tt.data))
|
||||
_, got := NewFile(bytes.NewReader(tt.data))
|
||||
|
||||
require.Error(t, want, "fixture is supposed to be rejected by debug/elf")
|
||||
require.Error(t, got)
|
||||
// the rejection must be debug/elf's, phrased its way, not ours
|
||||
assert.NotContains(t, got.Error(), "over the")
|
||||
assert.Equal(t, fmt.Sprint(want), fmt.Sprint(got))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -53,8 +53,8 @@ func (r *ContainerImageAllLayers) HasPath(path string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *ContainerImageAllLayers) fileByRef(ref stereoscopeFile.Reference, uniqueFileIDs stereoscopeFile.ReferenceSet, layerIdx int) ([]stereoscopeFile.Reference, error) {
|
||||
uniqueFiles := make([]stereoscopeFile.Reference, 0)
|
||||
func (r *ContainerImageAllLayers) locationsByRef(ref stereoscopeFile.Reference, accessPath string, uniqueFileIDs stereoscopeFile.ReferenceSet, layerPos int) ([]file.Location, error) {
|
||||
uniqueLocations := make([]file.Location, 0)
|
||||
|
||||
// since there is potentially considerable work for each symlink/hardlink that needs to be resolved, let's check to see if this is a symlink/hardlink first
|
||||
entry, err := r.img.FileCatalog.Get(ref)
|
||||
@@ -65,22 +65,22 @@ func (r *ContainerImageAllLayers) fileByRef(ref stereoscopeFile.Reference, uniqu
|
||||
if entry.Type == stereoscopeFile.TypeHardLink || entry.Type == stereoscopeFile.TypeSymLink {
|
||||
// a link may resolve in this layer or higher, assuming a squashed tree is used to search
|
||||
// we should search all possible resolutions within the valid source
|
||||
for _, subLayerIdx := range r.layers[layerIdx:] {
|
||||
for _, subLayerIdx := range r.layers[layerPos:] {
|
||||
resolvedRef, err := r.img.ResolveLinkByLayerSquash(ref, subLayerIdx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve link from layer (layer=%d ref=%+v): %w", subLayerIdx, ref, err)
|
||||
}
|
||||
if resolvedRef.HasReference() && !uniqueFileIDs.Contains(*resolvedRef.Reference) {
|
||||
uniqueFileIDs.Add(*resolvedRef.Reference)
|
||||
uniqueFiles = append(uniqueFiles, *resolvedRef.Reference)
|
||||
uniqueLocations = append(uniqueLocations, file.NewLocationFromImage(accessPath, *resolvedRef.Reference, r.img))
|
||||
}
|
||||
}
|
||||
} else if !uniqueFileIDs.Contains(ref) {
|
||||
uniqueFileIDs.Add(ref)
|
||||
uniqueFiles = append(uniqueFiles, ref)
|
||||
uniqueLocations = append(uniqueLocations, file.NewLocationFromImage(accessPath, ref, r.img))
|
||||
}
|
||||
|
||||
return uniqueFiles, nil
|
||||
return uniqueLocations, nil
|
||||
}
|
||||
|
||||
// FilesByPath returns all file.References that match the given paths from any layer in the image.
|
||||
@@ -112,14 +112,13 @@ func (r *ContainerImageAllLayers) FilesByPath(paths ...string) ([]file.Location,
|
||||
}
|
||||
}
|
||||
|
||||
results, err := r.fileByRef(*ref.Reference, uniqueFileIDs, idx)
|
||||
locations, err := r.locationsByRef(*ref.Reference, path, uniqueFileIDs, idx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, result := range results {
|
||||
l := file.NewLocationFromImage(path, result, r.img)
|
||||
r.annotateLocation(&l)
|
||||
uniqueLocations = append(uniqueLocations, l)
|
||||
for i := range locations {
|
||||
r.annotateLocation(&locations[i])
|
||||
uniqueLocations = append(uniqueLocations, locations[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,14 +157,13 @@ func (r *ContainerImageAllLayers) FilesByGlob(patterns ...string) ([]file.Locati
|
||||
}
|
||||
}
|
||||
|
||||
refResults, err := r.fileByRef(*result.Reference, uniqueFileIDs, idx)
|
||||
locations, err := r.locationsByRef(*result.Reference, string(result.RequestPath), uniqueFileIDs, idx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, refResult := range refResults {
|
||||
l := file.NewLocationFromImage(string(result.RequestPath), refResult, r.img)
|
||||
r.annotateLocation(&l)
|
||||
uniqueLocations = append(uniqueLocations, l)
|
||||
for i := range locations {
|
||||
r.annotateLocation(&locations[i])
|
||||
uniqueLocations = append(uniqueLocations, locations[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,14 +231,13 @@ func (r *ContainerImageAllLayers) FilesByMIMEType(types ...string) ([]file.Locat
|
||||
continue
|
||||
}
|
||||
|
||||
refResults, err := r.fileByRef(*ref.Reference, uniqueFileIDs, idx)
|
||||
locations, err := r.locationsByRef(*ref.Reference, string(ref.RequestPath), uniqueFileIDs, idx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, refResult := range refResults {
|
||||
l := file.NewLocationFromImage(string(ref.RequestPath), refResult, r.img)
|
||||
r.annotateLocation(&l)
|
||||
uniqueLocations = append(uniqueLocations, l)
|
||||
for i := range locations {
|
||||
r.annotateLocation(&locations[i])
|
||||
uniqueLocations = append(uniqueLocations, locations[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,26 +279,22 @@ func (r *ContainerImageAllLayers) annotateLocation(l *file.Location) {
|
||||
annotation := file.VisibleAnnotation
|
||||
|
||||
// if we find a location for a path that matches the query (e.g. **/node_modules) but is not present in the squashed tree, skip it
|
||||
ref, err := r.img.SquashedSearchContext.SearchByPath(l.RealPath, filetree.DoNotFollowDeadBasenameLinks)
|
||||
if err != nil || !ref.HasReference() {
|
||||
annotation = file.HiddenAnnotation
|
||||
} else if ref.ID() != givenRef.ID() {
|
||||
// we may have the path in the squashed tree, but this must not be in the same layer
|
||||
if !r.pathResolvesToRef(l.RealPath, givenRef) {
|
||||
annotation = file.HiddenAnnotation
|
||||
}
|
||||
|
||||
// not only should the real path to the file exist, but the way we took to get there should also exist
|
||||
// (e.g. if we are looking for /etc/passwd, but the real path is /etc/passwd -> /etc/passwd-1, then we should
|
||||
// make certain that /etc/passwd-1 exists)
|
||||
if annotation == file.VisibleAnnotation && l.AccessPath != "" {
|
||||
ref, err := r.img.SquashedSearchContext.SearchByPath(l.AccessPath, filetree.DoNotFollowDeadBasenameLinks)
|
||||
if err != nil || !ref.HasReference() {
|
||||
annotation = file.HiddenAnnotation
|
||||
} else if ref.ID() != givenRef.ID() {
|
||||
// we may have the path in the squashed tree, but this must not be in the same layer
|
||||
annotation = file.HiddenAnnotation
|
||||
}
|
||||
if annotation == file.VisibleAnnotation && l.AccessPath != "" && !r.pathResolvesToRef(l.AccessPath, givenRef) {
|
||||
annotation = file.HiddenAnnotation
|
||||
}
|
||||
|
||||
l.Annotations[file.VisibleAnnotationKey] = annotation
|
||||
}
|
||||
|
||||
// pathResolvesToRef reports whether the given path in the squashed tree resolves to the given reference.
|
||||
func (r *ContainerImageAllLayers) pathResolvesToRef(path string, target stereoscopeFile.Reference) bool {
|
||||
ref, err := r.img.SquashedSearchContext.SearchByPath(path, filetree.DoNotFollowDeadBasenameLinks)
|
||||
return err == nil && ref.HasReference() && ref.ID() == target.ID()
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
@@ -574,3 +575,105 @@ func TestSquashResolver_AllLocations(t *testing.T) {
|
||||
|
||||
assert.ElementsMatchf(t, expected, pathsList, "expected all paths to be indexed, but found different paths: \n%s", cmp.Diff(expected, paths.List()))
|
||||
}
|
||||
|
||||
// a hardlink should be surfaced at its own path as the underlying type it points to (a regular file with the target's
|
||||
// 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) {
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-hardlinks")
|
||||
|
||||
resolvers := map[string]file.Resolver{}
|
||||
squash, err := NewFromContainerImageSquash(img)
|
||||
require.NoError(t, err)
|
||||
resolvers["squashed"] = squash
|
||||
|
||||
allLayers, err := NewFromContainerImageAllLayers(img)
|
||||
require.NoError(t, err)
|
||||
resolvers["all-layers"] = allLayers
|
||||
|
||||
const wantContents = "hardlinked contents\n"
|
||||
|
||||
// asserts a location surfaces the hardlink at its own path as a regular file with the target's content, and is
|
||||
// not marked hidden (all-layers annotates visibility; squash leaves the annotation unset, which must not read as
|
||||
// hidden either).
|
||||
assertHardlink := func(t *testing.T, resolver file.Resolver, wantPath string, loc file.Location) {
|
||||
t.Helper()
|
||||
assert.Equal(t, wantPath, loc.RealPath, "expected the hardlink's own path, not the target's")
|
||||
assert.NotEqual(t, file.HiddenAnnotation, loc.Annotations[file.VisibleAnnotationKey], "path=%s should not be hidden", wantPath)
|
||||
|
||||
meta, err := resolver.FileMetadataByLocation(loc)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, stereoscopeFile.TypeRegular, meta.Type, "path=%s", wantPath)
|
||||
|
||||
reader, err := resolver.FileContentsByLocation(loc)
|
||||
require.NoError(t, err)
|
||||
actual, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, wantContents, string(actual), "path=%s", wantPath)
|
||||
}
|
||||
|
||||
for name, resolver := range resolvers {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Run("FilesByPath surfaces a hardlink at its own path with the target's content", func(t *testing.T) {
|
||||
for _, path := range []string{"/hardlink-a", "/hardlink-b", "/file.txt"} {
|
||||
locs, err := resolver.FilesByPath(path)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, locs, 1, "path=%s", path)
|
||||
assertHardlink(t, resolver, path, locs[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FilesByGlob surfaces every hardlink in a matched set, not just one", func(t *testing.T) {
|
||||
// the whole point of #5019: a glob that matches multiple hardlinks to the same target must return
|
||||
// all of them (each at its own path), not collapse them onto a single entry.
|
||||
locs, err := resolver.FilesByGlob("**/hardlink-*")
|
||||
require.NoError(t, err)
|
||||
|
||||
byPath := map[string]file.Location{}
|
||||
for _, loc := range locs {
|
||||
_, dup := byPath[loc.RealPath]
|
||||
assert.Falsef(t, dup, "path %s emitted more than once", loc.RealPath)
|
||||
byPath[loc.RealPath] = loc
|
||||
}
|
||||
for _, path := range []string{"/hardlink-a", "/hardlink-b"} {
|
||||
loc, ok := byPath[path]
|
||||
require.Truef(t, ok, "expected glob to surface %s", path)
|
||||
assertHardlink(t, resolver, path, loc)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FilesByMIMEType surfaces every hardlink name", func(t *testing.T) {
|
||||
// this is the surface #5029 could not fix and stereoscope's index-time adoption does: MIME-driven
|
||||
// catalogers see each name. it also pins that the names are separately addressable (distinct
|
||||
// references), since this resolver dedups on reference and would return one entry if they shared it.
|
||||
locs, err := resolver.FilesByMIMEType("text/plain")
|
||||
require.NoError(t, err)
|
||||
|
||||
byPath := map[string]file.Location{}
|
||||
for _, loc := range locs {
|
||||
byPath[loc.RealPath] = loc
|
||||
}
|
||||
for _, path := range []string{"/file.txt", "/hardlink-a", "/hardlink-b"} {
|
||||
loc, ok := byPath[path]
|
||||
require.Truef(t, ok, "expected MIME search to surface %s (got %v)", path, byPath)
|
||||
assertHardlink(t, resolver, path, loc)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AllLocations includes every hardlink path as a regular file exactly once", func(t *testing.T) {
|
||||
counts := map[string]int{}
|
||||
locsByPath := map[string]file.Location{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
for loc := range resolver.AllLocations(ctx) {
|
||||
counts[loc.RealPath]++
|
||||
locsByPath[loc.RealPath] = loc
|
||||
}
|
||||
for _, path := range []string{"/file.txt", "/hardlink-a", "/hardlink-b"} {
|
||||
require.Equalf(t, 1, counts[path], "expected %s to be reported exactly once by AllLocations", path)
|
||||
assertHardlink(t, resolver, path, locsByPath[path])
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,14 @@ func (r *directoryIndexer) indexTree(root string, stager *progress.AtomicStage)
|
||||
|
||||
shouldIndexFullTree, err := isRealPath(root)
|
||||
if err != nil {
|
||||
// a symlink can resolve into a directory we don't have permission to traverse (or into a path
|
||||
// that no longer exists). Like any other inaccessible path, this should be recorded and skipped
|
||||
// with a warning rather than aborting the entire scan (see #3286).
|
||||
var pathErr *os.PathError
|
||||
if errors.As(err, &pathErr) {
|
||||
r.isFileAccessErr(root, err)
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -286,6 +286,42 @@ func TestDirectoryIndexer_index_survive_badSymlink(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDirectoryIndexer_index_survive_inaccessibleSymlinkTarget(t *testing.T) {
|
||||
// a symlink that resolves into a directory we don't have permission to traverse should be skipped
|
||||
// with a warning like any other inaccessible path, not abort the entire scan (see #3286).
|
||||
if os.Geteuid() == 0 {
|
||||
t.Skip("cannot run as root: root bypasses the directory permissions that trigger the bug")
|
||||
}
|
||||
|
||||
// resolve any symlinks in the temp path (e.g. macOS /var -> /private/var) so the indexer root is a
|
||||
// real path -- otherwise indexing takes the symlinked-branch code path and never reaches the bug.
|
||||
root, err := filepath.EvalSymlinks(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
|
||||
// root/
|
||||
// ├── restricted/ (chmod 0000 -- cannot be traversed)
|
||||
// │ └── nested/
|
||||
// │ └── target.txt
|
||||
// └── link -> root/restricted/nested/target.txt
|
||||
nested := filepath.Join(root, "restricted", "nested")
|
||||
require.NoError(t, os.MkdirAll(nested, 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(nested, "target.txt"), []byte("hello"), 0o644))
|
||||
require.NoError(t, os.Symlink(filepath.Join(root, "restricted", "nested", "target.txt"), filepath.Join(root, "link")))
|
||||
|
||||
restricted := filepath.Join(root, "restricted")
|
||||
require.NoError(t, os.Chmod(restricted, 0o000))
|
||||
// restore permissions so t.TempDir cleanup can remove the tree
|
||||
t.Cleanup(func() { _ = os.Chmod(restricted, 0o755) })
|
||||
|
||||
// base is empty, matching a plain `syft <path>` invocation (the reporter's scenario)
|
||||
indexer := newDirectoryIndexer(root, "")
|
||||
_, _, err = indexer.build()
|
||||
require.NoError(t, err)
|
||||
|
||||
// the inaccessible path should be recorded rather than silently dropped
|
||||
require.NotEmpty(t, indexer.errPaths)
|
||||
}
|
||||
|
||||
func TestDirectoryIndexer_SkipsAlreadyVisitedLinkDestinations(t *testing.T) {
|
||||
var observedPaths []string
|
||||
pathObserver := func(_, p string, _ os.FileInfo, _ error) error {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# LAYER 0:
|
||||
FROM busybox:1.34.0@sha256:e8e5cca392e3cf056fcdb3093e7ac2bf83fcf28b3bcf5818fe8ae71cf360c231
|
||||
|
||||
# LAYER 1: a regular file with two hardlinks to it, all created in the same layer so the layer tar contains one
|
||||
# regular file entry and two hardlink (tar TypeLink) entries pointing at it.
|
||||
RUN echo "hardlinked contents" > /file.txt && ln /file.txt /hardlink-a && ln /file.txt /hardlink-b
|
||||
|
||||
# squash representation (of the files added here; busybox base also contributes hardlinked applets under /bin)
|
||||
# .
|
||||
# ├── file.txt
|
||||
# ├── hardlink-a (hardlink -> file.txt)
|
||||
# └── hardlink-b (hardlink -> file.txt)
|
||||
@@ -120,9 +120,19 @@ func (r *readerAtAdapter) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n, err = r.ReadSeekCloser.Read(p) // read from that absolute position
|
||||
// io.ReaderAt requires len(p) bytes or a non-nil error, and the underlying reader honors neither half of
|
||||
// that: a squashfs block that decompresses short returns fewer bytes with err == nil, and a read landing
|
||||
// exactly on the end of the file returns a full buffer alongside io.EOF. io.ReadFull normalizes both.
|
||||
n, err = io.ReadFull(r.ReadSeekCloser, p)
|
||||
// ReadAt reports a short read at the end of the file as io.EOF. compared by identity on purpose:
|
||||
// io.ReadFull returns this sentinel bare, and errors.Is would also downgrade a wrapped one from the
|
||||
// underlying reader, turning genuine stream corruption into a benign end of file
|
||||
if err == io.ErrUnexpectedEOF {
|
||||
err = io.EOF
|
||||
}
|
||||
|
||||
// restore the position for the stateful read/seek operations
|
||||
// restore the position for the stateful read/seek operations. a read error wins over a restore failure:
|
||||
// callers compare against io.EOF, and masking it would strand them without the data they did get
|
||||
if restoreErr := r.restorePosition(currentPos); restoreErr != nil {
|
||||
if err == nil {
|
||||
err = restoreErr
|
||||
|
||||
@@ -2,6 +2,7 @@ package unionreader
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -120,12 +121,14 @@ func TestReaderAtAdapter_ReadAt(t *testing.T) {
|
||||
expectedStr: "",
|
||||
},
|
||||
{
|
||||
// io.ReaderAt requires a non-nil error whenever it returns fewer than len(p) bytes, so a
|
||||
// buffer that runs off the end of the file reports io.EOF alongside what it did read
|
||||
name: "partial read",
|
||||
data: "Hello",
|
||||
offset: 2,
|
||||
bufSize: 10,
|
||||
expectedN: 3,
|
||||
expectedErr: nil,
|
||||
expectedErr: io.EOF,
|
||||
expectedStr: "llo",
|
||||
},
|
||||
{
|
||||
@@ -335,3 +338,111 @@ func (r *readSeekCloser) Close() error {
|
||||
r.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// scriptedReadSeeker replays a fixed sequence of Read results so tests can express the read shapes a
|
||||
// squashfs-backed reader actually produces, none of which an io.ReaderAt may pass through to callers:
|
||||
// a short count with a nil error, a full count paired with io.EOF, or a failure partway through a buffer.
|
||||
type scriptedReadSeeker struct {
|
||||
reads []scriptedRead
|
||||
next int
|
||||
offset int64
|
||||
}
|
||||
|
||||
type scriptedRead struct {
|
||||
data string
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *scriptedReadSeeker) Read(p []byte) (int, error) {
|
||||
if r.next >= len(r.reads) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
read := r.reads[r.next]
|
||||
r.next++
|
||||
n := copy(p, read.data)
|
||||
r.offset += int64(n)
|
||||
return n, read.err
|
||||
}
|
||||
|
||||
func (r *scriptedReadSeeker) Seek(offset int64, whence int) (int64, error) {
|
||||
if whence == io.SeekCurrent {
|
||||
return r.offset, nil
|
||||
}
|
||||
r.offset = offset
|
||||
return offset, nil
|
||||
}
|
||||
|
||||
func (r *scriptedReadSeeker) Close() error { return nil }
|
||||
|
||||
func TestReaderAtAdapter_ReadAtHonorsReaderAtContract(t *testing.T) {
|
||||
errBoom := errors.New("boom")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
reads []scriptedRead
|
||||
bufSize int
|
||||
expectedN int
|
||||
expectedErr error
|
||||
expectedStr string
|
||||
}{
|
||||
{
|
||||
// before io.ReadFull the caller got the first 3 bytes and a nil error, and any parser sizing a
|
||||
// struct off the result read zero padding it had no way to detect
|
||||
name: "fills the buffer across short reads",
|
||||
reads: []scriptedRead{{data: "abc"}, {data: "def"}, {data: "ghi"}},
|
||||
bufSize: 9,
|
||||
expectedN: 9,
|
||||
expectedErr: nil,
|
||||
expectedStr: "abcdefghi",
|
||||
},
|
||||
{
|
||||
// squashfs pairs io.EOF with a full buffer when a read lands exactly on the end of the file;
|
||||
// bytes.Reader.ReadAt returns nil there, and callers that treat any error as fatal rely on it
|
||||
name: "drops io.EOF when the read still filled the buffer",
|
||||
reads: []scriptedRead{{data: "abc"}, {data: "def", err: io.EOF}},
|
||||
bufSize: 6,
|
||||
expectedN: 6,
|
||||
expectedErr: nil,
|
||||
expectedStr: "abcdef",
|
||||
},
|
||||
{
|
||||
// a short tail must surface as io.EOF rather than io.ErrUnexpectedEOF so callers comparing
|
||||
// against io.EOF keep working
|
||||
name: "reports a short tail as io.EOF",
|
||||
reads: []scriptedRead{{data: "abc"}, {data: "de", err: io.EOF}},
|
||||
bufSize: 10,
|
||||
expectedN: 5,
|
||||
expectedErr: io.EOF,
|
||||
expectedStr: "abcde",
|
||||
},
|
||||
{
|
||||
// the io.ErrUnexpectedEOF remap must not swallow a real read failure, and the byte count has to
|
||||
// survive it, otherwise callers cannot tell how much of the buffer is trustworthy
|
||||
name: "propagates a failure partway through the buffer",
|
||||
reads: []scriptedRead{{data: "abc"}, {data: "", err: errBoom}},
|
||||
bufSize: 10,
|
||||
expectedN: 3,
|
||||
expectedErr: errBoom,
|
||||
expectedStr: "abc",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := &scriptedReadSeeker{reads: tt.reads}
|
||||
adapter := newReaderAtAdapter(reader)
|
||||
|
||||
buf := make([]byte, tt.bufSize)
|
||||
n, err := adapter.ReadAt(buf, 7)
|
||||
|
||||
require.ErrorIs(t, err, tt.expectedErr)
|
||||
assert.Equal(t, tt.expectedN, n)
|
||||
assert.Equal(t, tt.expectedStr, string(buf[:n]))
|
||||
|
||||
// the position must be restored even though the read spanned multiple underlying calls
|
||||
pos, err := adapter.Seek(0, io.SeekCurrent)
|
||||
require.NoError(t, err)
|
||||
assert.Zero(t, pos)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,12 +32,15 @@ type parsedData struct {
|
||||
pkg.ApkDBEntry
|
||||
}
|
||||
|
||||
const maxApkDBFieldSize = 10 * 1024 * 1024
|
||||
|
||||
// parseApkDB parses packages from a given APK "installed" flat-file DB. For more
|
||||
// information on specific fields, see https://wiki.alpinelinux.org/wiki/Apk_spec.
|
||||
//
|
||||
//nolint:funlen
|
||||
func parseApkDB(ctx context.Context, resolver file.Resolver, env *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Buffer(nil, maxApkDBFieldSize)
|
||||
|
||||
var errs error
|
||||
var apks []parsedData
|
||||
|
||||
@@ -719,6 +719,25 @@ func Test_processChecksum(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseApkDBAllowsLargeFieldValues(t *testing.T) {
|
||||
contents := strings.Join([]string{
|
||||
"P:large-description",
|
||||
"V:1.0-r0",
|
||||
"A:x86_64",
|
||||
"S:1",
|
||||
"I:1",
|
||||
"T:" + strings.Repeat("a", 70*1024),
|
||||
"",
|
||||
}, "\n")
|
||||
reader := file.NewLocationReadCloser(file.NewLocation("large-installed-db"), io.NopCloser(strings.NewReader(contents)))
|
||||
|
||||
pkgs, _, err := parseApkDB(context.Background(), nil, new(generic.Environment), reader)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pkgs, 1)
|
||||
assert.Equal(t, "large-description", pkgs[0].Name)
|
||||
}
|
||||
|
||||
func Test_parseApkDB_expectedPkgNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
fixture string
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package arch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
@@ -12,7 +16,23 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||
)
|
||||
|
||||
const bombFixture = "testdata/installed/var/lib/pacman/local/bombpkg-1.0-1/mtree"
|
||||
|
||||
// writeBombFixture generates bombpkg's mtree: one line over maxMtreeLines, so the cataloger rejects
|
||||
// it while walking testdata/installed. Generated rather than committed so the size that makes it a
|
||||
// bomb tracks the constant instead of hiding in a gzip blob.
|
||||
func writeBombFixture(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
listing := append([]byte("#mtree\n"), bytes.Repeat([]byte("./f\n"), maxMtreeLines+1)...)
|
||||
compressed, err := io.ReadAll(gzipOf(t, listing))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(bombFixture, compressed, 0o600))
|
||||
}
|
||||
|
||||
func TestAlpmUnknowns(t *testing.T) {
|
||||
writeBombFixture(t)
|
||||
|
||||
pkgtest.NewCatalogTester().
|
||||
FromDirectory(t, "testdata/installed").
|
||||
WithCompareOptions(cmpopts.IgnoreFields(pkg.AlpmFileRecord{}, "Time")).
|
||||
@@ -21,11 +41,14 @@ func TestAlpmUnknowns(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAlpmCataloger(t *testing.T) {
|
||||
writeBombFixture(t)
|
||||
|
||||
gmpDbLocation := file.NewLocation("var/lib/pacman/local/gmp-6.2.1-2/desc")
|
||||
treeSitterDbLocation := file.NewLocation("var/lib/pacman/local/tree-sitter-0.22.6-1/desc")
|
||||
emacsDbLocation := file.NewLocation("var/lib/pacman/local/emacs-29.3-3/desc")
|
||||
fuzzyDbLocation := file.NewLocation("var/lib/pacman/local/fuzzy-1.2-3/desc")
|
||||
madeupDbLocation := file.NewLocation("var/lib/pacman/local/madeup-20.30-4/desc")
|
||||
bombDbLocation := file.NewLocation("var/lib/pacman/local/bombpkg-1.0-1/desc")
|
||||
ctx := context.TODO()
|
||||
|
||||
treeSitterPkg := pkg.Package{
|
||||
@@ -119,6 +142,26 @@ func TestAlpmCataloger(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
// bombPkg's listing exceeds maxMtreeLines, so it emits with an empty file list, not dropped
|
||||
bombPkg := pkg.Package{
|
||||
Name: "bombpkg",
|
||||
Version: "1.0-1",
|
||||
Type: pkg.AlpmPkg,
|
||||
FoundBy: "alpm-db-cataloger",
|
||||
Locations: file.NewLocationSet(bombDbLocation),
|
||||
Metadata: pkg.AlpmDBEntry{
|
||||
BasePackage: "bombpkg",
|
||||
Package: "bombpkg",
|
||||
Version: "1.0-1",
|
||||
Description: "Package whose mtree listing exceeds the parser's line bound",
|
||||
Architecture: "x86_64",
|
||||
Size: 1,
|
||||
Reason: 1,
|
||||
Files: []pkg.AlpmFileRecord{},
|
||||
Backup: []pkg.AlpmFileRecord{},
|
||||
},
|
||||
}
|
||||
|
||||
gmpPkg := pkg.Package{
|
||||
Name: "gmp",
|
||||
Version: "6.2.1-2",
|
||||
@@ -283,6 +326,7 @@ func TestAlpmCataloger(t *testing.T) {
|
||||
fuzzyPkg,
|
||||
madeupPkg,
|
||||
gmpPkg,
|
||||
bombPkg,
|
||||
}
|
||||
|
||||
expectedRelationships := []artifact.Relationship{
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"github.com/vbatts/go-mtree"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
intFile "github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
@@ -216,6 +218,10 @@ func parseDatabase(b *bufio.Scanner) (*parsedData, error) {
|
||||
var backup []map[string]any
|
||||
for _, f := range strings.Split(value, "\n") {
|
||||
fields := strings.SplitN(f, "\t", 2)
|
||||
// without the tab there is no digest, and fields[1] would panic
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
p := fmt.Sprintf("/%s", fields[0])
|
||||
if ok := ignoredFiles[p]; !ok {
|
||||
backup = append(backup, map[string]any{
|
||||
@@ -241,6 +247,12 @@ func parseDatabase(b *bufio.Scanner) (*parsedData, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// a field over the scanner's 1MB token cap ends the scan early, which would otherwise look
|
||||
// like a complete package
|
||||
if err := b.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parsePkgFiles(pkgFields)
|
||||
}
|
||||
|
||||
@@ -277,15 +289,107 @@ func parsePkgFiles(pkgFields map[string]any) (*parsedData, error) {
|
||||
return &entry, nil
|
||||
}
|
||||
|
||||
// maxMtreeSize bounds the decompressed listing. texlive-fontsextra, the largest package Arch
|
||||
// ships, runs 16 to 27MB.
|
||||
//
|
||||
// this caps bytes, not memory: go-mtree allocates per field, so a crafted listing can still cost
|
||||
// about a gigabyte, times cataloger parallelism.
|
||||
const maxMtreeSize = 64 * intFile.MB
|
||||
|
||||
// maxMtreeLines bounds entry count, which the byte cap does not: go-mtree keeps a ~128 byte entry
|
||||
// per line, blanks included. 300k is ~2.8x the largest package Arch ships.
|
||||
const maxMtreeLines = 300_000
|
||||
|
||||
var (
|
||||
errMtreeTooLarge = errors.New("mtree file is larger than the max allowed size")
|
||||
errTooManyMtreeLines = errors.New("mtree file has more lines than allowed")
|
||||
|
||||
// errMtreeLineContinued rejects a line ending in a backslash, which readers treat as "join with
|
||||
// the next line". We refuse the file instead, for two reasons.
|
||||
//
|
||||
// cost: go-mtree joins with a quadratic string concat, so work grows as lines squared. 100k
|
||||
// continued lines is a 1.2MB listing costing ~3.7s and ~52GB of churn. The bounds above are
|
||||
// linear and never see it, and no line cap helps, since real packages reach ~107k lines.
|
||||
//
|
||||
// spec: mtree(5) says "Each line is interpreted independently" and documents no continuation,
|
||||
// nor does Arch's ALPM-MTREEv2(5). Readers accept them anyway (BSD mtree via fparseln(3),
|
||||
// libarchive in readline()), but no writer emits one: libarchive escapes a literal backslash as
|
||||
// \134, and Arch builds .MTREE with it. So a continuation here is crafted, not a package.
|
||||
errMtreeLineContinued = errors.New("mtree file uses line continuations")
|
||||
)
|
||||
|
||||
// lineLimitedReader fails the read once the stream carries more than max newlines, or a line
|
||||
// continuation. Counting while the bytes flow is what bounds entries: the parser builds one per
|
||||
// line before returning any of them.
|
||||
//
|
||||
// continuations are refused rather than joined, unlike every other mtree reader. See
|
||||
// errMtreeLineContinued. Both \n and \r\n count, since go-mtree strips a trailing carriage return
|
||||
// before testing for the backslash.
|
||||
//
|
||||
// a tripped bound is latched, so reading past the first error does not come back clean.
|
||||
type lineLimitedReader struct {
|
||||
reader io.Reader
|
||||
lines int
|
||||
max int
|
||||
lastByte byte
|
||||
err error
|
||||
}
|
||||
|
||||
func (l *lineLimitedReader) Read(p []byte) (int, error) {
|
||||
if l.err != nil {
|
||||
return 0, l.err
|
||||
}
|
||||
|
||||
n, err := l.reader.Read(p)
|
||||
|
||||
// prev carries over the last byte of the previous read, so a backslash split from its line
|
||||
// ending is still caught
|
||||
prev := l.lastByte
|
||||
for _, c := range p[:n] {
|
||||
if (c == '\n' || c == '\r') && prev == '\\' {
|
||||
l.err = errMtreeLineContinued
|
||||
return n, l.err
|
||||
}
|
||||
if c == '\n' {
|
||||
l.lines++
|
||||
}
|
||||
prev = c
|
||||
}
|
||||
l.lastByte = prev
|
||||
|
||||
if l.lines > l.max {
|
||||
l.err = fmt.Errorf("%w (max %d)", errTooManyMtreeLines, l.max)
|
||||
return n, l.err
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
func parseMtree(r io.Reader) ([]pkg.AlpmFileRecord, error) {
|
||||
var err error
|
||||
return parseMtreeWithLimits(r, maxMtreeSize, maxMtreeLines)
|
||||
}
|
||||
|
||||
// parseMtreeWithLimits lets the boundary tests use small exact limits instead of the shipped caps.
|
||||
// maxSize must be below math.MaxInt64, since the limiter carries a byte of headroom.
|
||||
func parseMtreeWithLimits(r io.Reader, maxSize int64, maxLines int) ([]pkg.AlpmFileRecord, error) {
|
||||
var entries []pkg.AlpmFileRecord
|
||||
|
||||
r, err = gzip.NewReader(r)
|
||||
gzReader, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
specDh, err := mtree.ParseSpec(r)
|
||||
defer internal.CloseAndLogError(gzReader, "mtree")
|
||||
|
||||
// the line cap trips mid-stream, the byte cap cannot: io.LimitedReader reports a drained budget
|
||||
// as io.EOF, which the parser reads as a complete listing, so it is checked after the parse. The
|
||||
// extra byte tells a drained budget from a listing ending exactly at the cap.
|
||||
sizeLimited := &io.LimitedReader{R: gzReader, N: maxSize + 1}
|
||||
specDh, err := mtree.ParseSpec(&lineLimitedReader{reader: sizeLimited, max: maxLines})
|
||||
|
||||
// before err: a drained budget means the parser saw only part of the listing
|
||||
if sizeLimited.N <= 0 {
|
||||
return nil, fmt.Errorf("%w (%d bytes)", errMtreeTooLarge, maxSize)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2,13 +2,19 @@ package arch
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
intFile "github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
@@ -155,6 +161,26 @@ func TestDatabaseParser(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseAlpmDBEntry_malformedDatabase(t *testing.T) {
|
||||
t.Run("backup line with no tab", func(t *testing.T) {
|
||||
// this used to panic, which drops every package rather than the one
|
||||
entry, err := parseAlpmDBEntry(strings.NewReader("%NAME%\nfoo\n\n%BACKUP%\nnotabhere\n\n"))
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, entry)
|
||||
require.Empty(t, entry.Backup)
|
||||
})
|
||||
|
||||
t.Run("field over the scanner's token cap", func(t *testing.T) {
|
||||
// everything after the oversize field is lost, so this has to error rather than look complete
|
||||
db := "%NAME%\nfoo\n\n%DESC%\n" + strings.Repeat("x", 2*intFile.MB) + "\n\n%ARCH%\nx86_64\n\n"
|
||||
|
||||
_, err := parseAlpmDBEntry(strings.NewReader(db))
|
||||
|
||||
require.ErrorIs(t, err, bufio.ErrTooLong)
|
||||
})
|
||||
}
|
||||
|
||||
func parseTime(stime string) time.Time {
|
||||
t, _ := time.Parse(time.RFC3339, stime)
|
||||
return t
|
||||
@@ -207,6 +233,7 @@ func TestMtreeParse(t *testing.T) {
|
||||
|
||||
reader := bufio.NewReader(f)
|
||||
|
||||
// a normal listing must not trip any of the bounds
|
||||
entry, err := parseMtree(reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -217,3 +244,243 @@ func TestMtreeParse(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// mtreeSpec builds a valid listing of n files. Boundary tests need the parser to reach the end of
|
||||
// it, which filler bytes never do.
|
||||
func mtreeSpec(n int) []byte {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("#mtree\n")
|
||||
buf.WriteString("/set type=file uid=0 gid=0 mode=644\n")
|
||||
for i := range n {
|
||||
fmt.Fprintf(&buf, "./file%d time=1649595592.0 size=10 sha256digest=%064x\n", i, i)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func gzipOf(t *testing.T, data []byte) io.Reader {
|
||||
t.Helper()
|
||||
|
||||
var buf bytes.Buffer
|
||||
w := gzip.NewWriter(&buf)
|
||||
_, err := w.Write(data)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, w.Close())
|
||||
|
||||
return bytes.NewReader(buf.Bytes())
|
||||
}
|
||||
|
||||
// gzipOfRepeated builds a gzip member decompressing to n bytes of one repeated byte, so a few KB of
|
||||
// input expands to whatever size the caller asks for.
|
||||
func gzipOfRepeated(t *testing.T, payload byte, n int64) *bytes.Reader {
|
||||
t.Helper()
|
||||
|
||||
var buf bytes.Buffer
|
||||
w := gzip.NewWriter(&buf)
|
||||
chunk := bytes.Repeat([]byte{payload}, 32*1024)
|
||||
for remaining := n; remaining > 0; {
|
||||
size := min(remaining, int64(len(chunk)))
|
||||
written, err := w.Write(chunk[:size])
|
||||
require.NoError(t, err)
|
||||
remaining -= int64(written)
|
||||
}
|
||||
require.NoError(t, w.Close())
|
||||
|
||||
return bytes.NewReader(buf.Bytes())
|
||||
}
|
||||
|
||||
func Test_parseMtree_boundsDecompressedSize(t *testing.T) {
|
||||
// limits come from the fixture, so both sides of the boundary are exact
|
||||
spec := mtreeSpec(50)
|
||||
|
||||
t.Run("a listing at the cap parses whole", func(t *testing.T) {
|
||||
records, err := parseMtreeWithLimits(gzipOf(t, spec), int64(len(spec)), maxMtreeLines)
|
||||
|
||||
// records, not just no error: landing exactly on the cap must not truncate
|
||||
require.NoError(t, err)
|
||||
require.Len(t, records, 50)
|
||||
require.Equal(t, "/file0", records[0].Path)
|
||||
require.Equal(t, "/file49", records[49].Path)
|
||||
})
|
||||
|
||||
t.Run("rejects a listing one byte past the cap", func(t *testing.T) {
|
||||
_, err := parseMtreeWithLimits(gzipOf(t, spec), int64(len(spec))-1, maxMtreeLines)
|
||||
|
||||
require.ErrorIs(t, err, errMtreeTooLarge)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_parseMtree_shippedConstants pins the production values. The boundary tests parameterize the
|
||||
// limits, so they would pass unchanged if a constant regressed. Driving a real 64MB listing through
|
||||
// the parser instead cost 12s of a 16s race run.
|
||||
func Test_parseMtree_shippedConstants(t *testing.T) {
|
||||
require.Equal(t, int64(64*intFile.MB), int64(maxMtreeSize))
|
||||
require.Equal(t, 300_000, maxMtreeLines)
|
||||
}
|
||||
|
||||
func Test_parseMtree_boundsLineCount(t *testing.T) {
|
||||
// the byte cap does not bound memory: the parser keeps an entry per line, blanks included
|
||||
spec := mtreeSpec(50)
|
||||
lines := bytes.Count(spec, []byte("\n")) // the two header lines get entries of their own
|
||||
|
||||
t.Run("a listing at the cap parses whole", func(t *testing.T) {
|
||||
records, err := parseMtreeWithLimits(gzipOf(t, spec), maxMtreeSize, lines)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, records, 50)
|
||||
})
|
||||
|
||||
t.Run("rejects a listing one line past the cap", func(t *testing.T) {
|
||||
_, err := parseMtreeWithLimits(gzipOf(t, spec), maxMtreeSize, lines-1)
|
||||
|
||||
require.ErrorIs(t, err, errTooManyMtreeLines)
|
||||
})
|
||||
|
||||
t.Run("rejects a bomb at the production limits without reading all of it", func(t *testing.T) {
|
||||
// what the cap exists for: a few KB expanding past the line cap while inside the byte cap
|
||||
bomb := gzipOfRepeated(t, '\n', 4*1024*1024)
|
||||
require.Less(t, bomb.Size(), int64(64*1024), "payload should be small enough to be worth rejecting")
|
||||
|
||||
counted := &countingReader{reader: bomb}
|
||||
|
||||
_, err := parseMtree(counted)
|
||||
|
||||
require.ErrorIs(t, err, errTooManyMtreeLines)
|
||||
|
||||
// what matters is giving up mid-read, not after building the whole listing. Bytes consumed is
|
||||
// the stable way to check that; an allocation delta drifts with GC and parallelism.
|
||||
require.Less(t, counted.n, bomb.Size(),
|
||||
"the whole bomb was read, so the cap tripped after the parse rather than during it")
|
||||
})
|
||||
}
|
||||
|
||||
// countingReader records how many bytes were pulled through it.
|
||||
type countingReader struct {
|
||||
reader io.Reader
|
||||
n int64
|
||||
}
|
||||
|
||||
func (c *countingReader) Read(p []byte) (int, error) {
|
||||
n, err := c.reader.Read(p)
|
||||
c.n += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Test_parseMtree_sizeCapTakesPrecedenceOverLineCap pins which error wins when one read exceeds both
|
||||
// bounds. No other test exceeds both at once, so a reorder would go unnoticed.
|
||||
func Test_parseMtree_sizeCapTakesPrecedenceOverLineCap(t *testing.T) {
|
||||
spec := mtreeSpec(50)
|
||||
|
||||
// maxLines=0 trips on the first newline; maxSize=20 is drained by that same read
|
||||
_, err := parseMtreeWithLimits(gzipOf(t, spec), 20, 0)
|
||||
|
||||
require.ErrorIs(t, err, errMtreeTooLarge)
|
||||
require.NotErrorIs(t, err, errTooManyMtreeLines)
|
||||
}
|
||||
|
||||
func Test_parseMtree_rejectsLineContinuations(t *testing.T) {
|
||||
// go-mtree strips a trailing carriage return before testing for the backslash, so CRLF continues
|
||||
// a line too
|
||||
for _, lineEnding := range []string{"\\\n", "\\\r\n"} {
|
||||
t.Run(fmt.Sprintf("%q", lineEnding), func(t *testing.T) {
|
||||
spec := mtreeSpec(5)
|
||||
spec = bytes.Replace(spec, []byte("size=10 sha256digest="), []byte("size=10 "+lineEnding+"sha256digest="), 1)
|
||||
|
||||
_, err := parseMtree(gzipOf(t, spec))
|
||||
|
||||
require.ErrorIs(t, err, errMtreeLineContinued)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// scriptedReader replays a fixed sequence of Read results, driving the straddling case directly.
|
||||
type scriptedReader struct {
|
||||
chunks []string
|
||||
}
|
||||
|
||||
func (r *scriptedReader) Read(p []byte) (int, error) {
|
||||
if len(r.chunks) == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := copy(p, r.chunks[0])
|
||||
// keep what did not fit, so a short buffer still obeys io.Reader
|
||||
if n < len(r.chunks[0]) {
|
||||
r.chunks[0] = r.chunks[0][n:]
|
||||
} else {
|
||||
r.chunks = r.chunks[1:]
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func Test_lineLimitedReader_rejectsStraddlingContinuation(t *testing.T) {
|
||||
// the backslash and its line ending land in separate Read calls, which happens for real: the
|
||||
// io.LimitedReader truncates the buffer and flate returns short reads.
|
||||
tests := []struct {
|
||||
name string
|
||||
chunks []string
|
||||
}{
|
||||
{name: "lf", chunks: []string{"...\\", "\n..."}},
|
||||
{name: "crlf", chunks: []string{"...\\", "\r\n..."}},
|
||||
{name: "crlf split at the carriage return", chunks: []string{"...\\\r", "\n..."}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
lr := &lineLimitedReader{reader: &scriptedReader{chunks: tt.chunks}, max: 100}
|
||||
|
||||
var err error
|
||||
buf := make([]byte, 16)
|
||||
for err == nil {
|
||||
_, err = lr.Read(buf)
|
||||
}
|
||||
|
||||
require.ErrorIs(t, err, errMtreeLineContinued)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_lineLimitedReader_latchesTheError(t *testing.T) {
|
||||
// a tripped bound must not come back clean on the next read
|
||||
lr := &lineLimitedReader{reader: &scriptedReader{chunks: []string{"a\\\nb", "clean"}}, max: 100}
|
||||
|
||||
buf := make([]byte, 16)
|
||||
_, first := lr.Read(buf)
|
||||
require.ErrorIs(t, first, errMtreeLineContinued)
|
||||
|
||||
n, second := lr.Read(buf)
|
||||
|
||||
require.ErrorIs(t, second, errMtreeLineContinued)
|
||||
require.Zero(t, n)
|
||||
}
|
||||
|
||||
func Test_parseMtree_malformedInput(t *testing.T) {
|
||||
t.Run("not gzip", func(t *testing.T) {
|
||||
_, err := parseMtree(bytes.NewReader(mtreeSpec(2)))
|
||||
|
||||
require.ErrorIs(t, err, gzip.ErrHeader)
|
||||
})
|
||||
|
||||
t.Run("truncated gzip", func(t *testing.T) {
|
||||
spec := mtreeSpec(50)
|
||||
var buf bytes.Buffer
|
||||
_, err := io.Copy(&buf, gzipOf(t, spec))
|
||||
require.NoError(t, err)
|
||||
|
||||
// the cap sits just above what the truncated stream delivers, so the size check and the
|
||||
// truncation actually race. The shipped 64MB cap could never fire here.
|
||||
_, err = parseMtreeWithLimits(bytes.NewReader(buf.Bytes()[:buf.Len()/2]), int64(len(spec)), maxMtreeLines)
|
||||
|
||||
// must fail, but not as a size-bound trip
|
||||
require.Error(t, err)
|
||||
require.NotErrorIs(t, err, errMtreeTooLarge)
|
||||
})
|
||||
|
||||
t.Run("single line over the scanner's token cap", func(t *testing.T) {
|
||||
// the third bound is go-mtree's scanner, at the default 64KB token limit. Nothing else here
|
||||
// would catch a dependency bump that calls scanner.Buffer.
|
||||
spec := append([]byte("#mtree\n./big "), bytes.Repeat([]byte("a"), 128*1024)...)
|
||||
|
||||
_, err := parseMtree(gzipOf(t, append(spec, '\n')))
|
||||
|
||||
require.ErrorContains(t, err, "token too long")
|
||||
})
|
||||
}
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# generated by writeBombFixture, see cataloger_test.go
|
||||
/mtree
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
%NAME%
|
||||
bombpkg
|
||||
|
||||
%VERSION%
|
||||
1.0-1
|
||||
|
||||
%BASE%
|
||||
bombpkg
|
||||
|
||||
%DESC%
|
||||
Package whose mtree listing exceeds the parser's line bound
|
||||
|
||||
%ARCH%
|
||||
x86_64
|
||||
|
||||
%SIZE%
|
||||
1
|
||||
|
||||
%REASON%
|
||||
1
|
||||
@@ -433,6 +433,16 @@ catalogers:
|
||||
cpes:
|
||||
- cpe:2.3:a:deno:deno:*:*:*:*:*:*:*:*
|
||||
type: BinaryPkg
|
||||
- method: glob
|
||||
criteria:
|
||||
- '**/bun'
|
||||
packages:
|
||||
- class: bun-binary
|
||||
name: bun
|
||||
purl: pkg:generic/bun
|
||||
cpes:
|
||||
- cpe:2.3:a:oven-sh:bun:*:*:*:*:*:*:*:*
|
||||
type: BinaryPkg
|
||||
- method: glob
|
||||
criteria:
|
||||
- '**/ghc*'
|
||||
|
||||
@@ -1499,6 +1499,28 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
|
||||
Metadata: metadata("deno-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "deno/1.11.3/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
Name: "deno",
|
||||
Version: "1.11.3",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/deno@1.11.3",
|
||||
Locations: locations("deno"),
|
||||
Metadata: metadata("deno-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "deno/1.11.4/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
Name: "deno",
|
||||
Version: "1.11.4",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/deno@1.11.4",
|
||||
Locations: locations("deno"),
|
||||
Metadata: metadata("deno-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "deno/1.16.4/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
@@ -1565,6 +1587,61 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
|
||||
Metadata: metadata("deno-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "deno/2.9.2/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
Name: "deno",
|
||||
Version: "2.9.2",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/deno@2.9.2",
|
||||
Locations: locations("deno"),
|
||||
Metadata: metadata("deno-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "bun/0.5.9/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
Name: "bun",
|
||||
Version: "0.5.9",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/bun@0.5.9",
|
||||
Locations: locations("bun"),
|
||||
Metadata: metadata("bun-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "bun/1.0.0/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
Name: "bun",
|
||||
Version: "1.0.0",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/bun@1.0.0",
|
||||
Locations: locations("bun"),
|
||||
Metadata: metadata("bun-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "bun/1.1.0/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
Name: "bun",
|
||||
Version: "1.1.0",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/bun@1.1.0",
|
||||
Locations: locations("bun"),
|
||||
Metadata: metadata("bun-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "bun/1.3.5/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
Name: "bun",
|
||||
Version: "1.3.5",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/bun@1.3.5",
|
||||
Locations: locations("bun"),
|
||||
Metadata: metadata("bun-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "haskell-ghc/7.10.3/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
@@ -2217,6 +2294,39 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
|
||||
Metadata: metadata("grafana-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "grafana/12.4.3-security-02/linux-arm",
|
||||
expected: pkg.Package{
|
||||
Name: "grafana",
|
||||
Version: "12.4.3",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/grafana@12.4.3",
|
||||
Locations: locations("grafana"),
|
||||
Metadata: metadata("grafana-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "grafana/12.0.0-security-01/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
Name: "grafana",
|
||||
Version: "12.0.0",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/grafana@12.0.0",
|
||||
Locations: locations("grafana"),
|
||||
Metadata: metadata("grafana-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "grafana/11.0.5-security-01/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
Name: "grafana",
|
||||
Version: "11.0.5",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/grafana@11.0.5",
|
||||
Locations: locations("grafana"),
|
||||
Metadata: metadata("grafana-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "grafana/12.3.1/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
@@ -2536,6 +2646,39 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
|
||||
Metadata: metadata("ingress-nginx-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "nginx-ingress-controller/1.9.6/linux-arm64",
|
||||
expected: pkg.Package{
|
||||
Name: "nginx-ingress-controller",
|
||||
Version: "1.9.6",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/nginx-ingress-controller@1.9.6",
|
||||
Locations: locations("nginx-ingress-controller"),
|
||||
Metadata: metadata("ingress-nginx-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "nginx-ingress-controller/1.9.6/linux-arm",
|
||||
expected: pkg.Package{
|
||||
Name: "nginx-ingress-controller",
|
||||
Version: "1.9.6",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/nginx-ingress-controller@1.9.6",
|
||||
Locations: locations("nginx-ingress-controller"),
|
||||
Metadata: metadata("ingress-nginx-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "nginx-ingress-controller/1.9.6/linux-s390x",
|
||||
expected: pkg.Package{
|
||||
Name: "nginx-ingress-controller",
|
||||
Version: "1.9.6",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/nginx-ingress-controller@1.9.6",
|
||||
Locations: locations("nginx-ingress-controller"),
|
||||
Metadata: metadata("ingress-nginx-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "nginx-ingress-controller/1.7.1/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
@@ -2992,8 +3135,8 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
|
||||
Name: "busybox",
|
||||
Version: "1.35.0",
|
||||
PURL: "pkg:generic/busybox@1.35.0",
|
||||
Locations: locations("/bin/["),
|
||||
Metadata: metadata("busybox-binary", "/bin/[", "/bin/busybox"),
|
||||
Locations: locations("/bin/busybox"),
|
||||
Metadata: metadata("busybox-binary", "/bin/busybox"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -588,11 +588,20 @@ func DefaultClassifiers() []binutils.Classifier {
|
||||
Class: "deno-binary",
|
||||
FileGlob: "**/deno",
|
||||
EvidenceMatcher: binutils.MatchAny(
|
||||
m.FileContentsVersionMatcher(
|
||||
// Deno/2.6.3Deno/
|
||||
// Deno/1.41.0cli/
|
||||
`Deno/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)(Deno/|cli/)`,
|
||||
),
|
||||
m.FileContentsVersionMatcher(
|
||||
// Deno/2.6.3
|
||||
// Deno/1.41.0
|
||||
`Deno/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`,
|
||||
),
|
||||
m.FileContentsVersionMatcher(
|
||||
// cli/tools/standalone.rsdeno-canary/f4bed1081456089559c82441a13c4fb700840cac1.11.3dlwindows
|
||||
`deno-canary/[0-9a-z]{40}(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`,
|
||||
),
|
||||
m.FileContentsVersionMatcher(
|
||||
// deno::tools::standalonedeno-65db94feba9d4d51a09b74629f566dbc90484fbarelease/v1.29.4windows
|
||||
// cli/tools/standalone.rsdeno-74064c9d8c222b33b2a552ea0af1054f57002a96release/v1.28.3windows
|
||||
@@ -611,6 +620,20 @@ func DefaultClassifiers() []binutils.Classifier {
|
||||
PURL: mustPURL("pkg:generic/deno@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:deno:deno:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
|
||||
},
|
||||
{
|
||||
Class: "bun-binary",
|
||||
FileGlob: "**/bun",
|
||||
EvidenceMatcher: binutils.MatchAny(
|
||||
// bun 1.1.0
|
||||
// Bun v1.0.0
|
||||
m.FileContentsVersionMatcher(`(?m)[Bb]un v?(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
// bun-1.1.0
|
||||
m.FileContentsVersionMatcher(`(?m)bun-(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
),
|
||||
Package: "bun",
|
||||
PURL: mustPURL("pkg:generic/bun@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:oven-sh:bun:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "haskell-ghc-binary",
|
||||
FileGlob: "**/ghc*",
|
||||
@@ -876,6 +899,11 @@ func DefaultClassifiers() []binutils.Classifier {
|
||||
Class: "gzip-binary",
|
||||
FileGlob: "**/gzip",
|
||||
EvidenceMatcher: m.FileContentsVersionMatcher(
|
||||
// GNU gzip keeps the version as a bare NUL-delimited token, so require an identifying
|
||||
// string from the program as well. Without it any binary that happens to be named gzip
|
||||
// (such as the busybox multicall binary behind a gzip applet symlink) would have an
|
||||
// arbitrary NUL-delimited "N.N" token reported as a GNU gzip version.
|
||||
`%s: %s: not in gzip format`,
|
||||
`\x00(?P<version>[0-9]+\.[0-9]+)\x00`,
|
||||
),
|
||||
Package: "gzip",
|
||||
@@ -1026,6 +1054,11 @@ func DefaultClassifiers() []binutils.Classifier {
|
||||
EvidenceMatcher: binutils.MatchAny(
|
||||
// [NUL][NUL][NUL][NUL]12.2.0-258092[NUL][NUL][NUL][NUL]
|
||||
m.FileContentsVersionMatcher(`\x00+(?P<version>[0-9]{2}\.[0-9]+\.[0-9]+\-[0-9]{6,})\x00+`),
|
||||
// security patch releases embed the raw version constant with no "release-" prefix
|
||||
// and are not always preceded by NUL bytes (e.g. on arm builds):
|
||||
// [NUL]11.0.5+security-01[NUL][NUL]call frame too large
|
||||
// [NUL]12.4.3+security-02[NUL][NUL]
|
||||
m.FileContentsVersionMatcher(`\x00(?P<version>[0-9]{1,2}\.[0-9]+\.[0-9]+)\+security-[0-9]+\x00`),
|
||||
// [NUL][NUL][NUL][NUL]release-12.3.2+security-01[NUL][NUL][NUL][NUL]
|
||||
// [NUL][NUL][NUL][NUL]release-12.3.1[NUL][NUL][NUL][NUL]
|
||||
m.FileContentsVersionMatcher(`\x00+release-(?P<version>[0-9]{2}\.[0-9]+\.[0-9]+(-beta[0-9]|-test|-preview)?)(\+security-[0-9]+)?\x00+`),
|
||||
@@ -1130,6 +1163,11 @@ func DefaultClassifiers() []binutils.Classifier {
|
||||
Class: "ingress-nginx-binary",
|
||||
FileGlob: "**/nginx-ingress-controller",
|
||||
EvidenceMatcher: binutils.MatchAny(
|
||||
// the release is injected with -ldflags -X, which lands it in its own NUL-padded data symbol.
|
||||
// the surrounding bytes are an arch-specific float constant pool, so only the padding is portable.
|
||||
// e.g. v1.9.6[NUL][NUL] on each of linux/amd64, linux/arm, linux/arm64, and linux/s390x
|
||||
// note: one trailing NUL is not enough -- on s390x that matches a vendored "v1.19.0" earlier in the file
|
||||
m.FileContentsVersionMatcher(`v(?P<version>[0-9]+\.[0-9]+\.[0-9]+(\-(alpha|beta)\.[0-9]+)?)\x00\x00`),
|
||||
// [NUL][NUL]v1.15.1[NUL][NUL]@e[ETX][NUL][NUL][NUL][NUL]go1.26.1[NUL][NUL][NUL]
|
||||
// �v1.15.1[NUL][NUL]�z[ETX][NUL][NUL][NUL][NUL]go1.24.4[NUL][NUL][NUL]
|
||||
m.FileContentsVersionMatcher(`v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00+.{0,50}go[0-9]+\.[0-9]+(\-(alpha|beta)\.[0-9])?\.[0-9]+\x00+`),
|
||||
|
||||
@@ -3,7 +3,6 @@ package binary
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"debug/elf"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -14,6 +13,7 @@ import (
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/elfutil"
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
@@ -146,7 +146,7 @@ func getELFNotes(r file.LocationReadCloser) (*elfBinaryPackageNotes, error) {
|
||||
return nil, fmt.Errorf("unable to get union reader for binary: %w", err)
|
||||
}
|
||||
|
||||
f, err := elf.NewFile(unionReader)
|
||||
f, err := elfutil.NewFile(unionReader)
|
||||
if f == nil || err != nil {
|
||||
log.WithFields("file", r.Location.Path(), "error", err).Trace("unable to parse binary as ELF")
|
||||
return nil, nil
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/anchore/syft/syft/internal/elfutil"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/internal/manager/internal"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/internal/manager/internal/config"
|
||||
)
|
||||
@@ -203,7 +204,7 @@ const (
|
||||
)
|
||||
|
||||
func getPlatformElf(f *os.File) string {
|
||||
elfFile, err := elf.NewFile(f)
|
||||
elfFile, err := elfutil.NewFile(f)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
@@ -214,6 +215,14 @@ func getPlatformElf(f *os.File) string {
|
||||
arch = amd64
|
||||
case elf.EM_AARCH64:
|
||||
arch = arm64
|
||||
case elf.EM_ARM:
|
||||
arch = "arm"
|
||||
case elf.EM_S390:
|
||||
arch = "s390x"
|
||||
case elf.EM_386:
|
||||
arch = "386"
|
||||
case elf.EM_RISCV:
|
||||
arch = "riscv64"
|
||||
// TODO...
|
||||
default:
|
||||
arch = fmt.Sprintf("unknown-%x", elfFile.Machine)
|
||||
|
||||
@@ -36,5 +36,11 @@ func parsePE(_ context.Context, _ file.Resolver, _ *generic.Environment, reader
|
||||
|
||||
p := newPEPackage(f.VersionResources, f.Location)
|
||||
|
||||
// the file was usable but not fully parsed, which for this cataloger means the package name and version
|
||||
// may have come from the filename rather than from version resources. Worth reporting alongside it.
|
||||
if f.ParseErr != nil {
|
||||
return []pkg.Package{p}, nil, unknown.New(reader, f.ParseErr)
|
||||
}
|
||||
|
||||
return []pkg.Package{p}, nil, nil
|
||||
}
|
||||
|
||||
Binary file not shown.
+12
@@ -0,0 +1,12 @@
|
||||
name: bun
|
||||
offset: 1000000
|
||||
length: 100
|
||||
snippetSha256: 0000000000000000000000000000000000000000000000000000000000000001
|
||||
fileSha256: 0000000000000000000000000000000000000000000000000000000000000001
|
||||
|
||||
### byte snippet to follow ###
|
||||
]
|
||||
|
||||
Bun v0.5.9
|
||||
bun-0.5.9
|
||||
JavaScript runtime for developing, testing, and bundling
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
name: bun
|
||||
offset: 1000000
|
||||
length: 100
|
||||
snippetSha256: 0000000000000000000000000000000000000000000000000000000000000002
|
||||
fileSha256: 0000000000000000000000000000000000000000000000000000000000000002
|
||||
|
||||
### byte snippet to follow ###
|
||||
]
|
||||
|
||||
Bun v1.0.0
|
||||
bun-1.0.0
|
||||
JavaScript runtime for developing, testing, and bundling
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
name: bun
|
||||
offset: 1000000
|
||||
length: 100
|
||||
snippetSha256: 0000000000000000000000000000000000000000000000000000000000000003
|
||||
fileSha256: 0000000000000000000000000000000000000000000000000000000000000003
|
||||
|
||||
### byte snippet to follow ###
|
||||
]
|
||||
|
||||
Bun v1.1.0
|
||||
bun-1.1.0
|
||||
JavaScript runtime for developing, testing, and bundling
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
name: bun
|
||||
offset: 1000000
|
||||
length: 100
|
||||
snippetSha256: 0000000000000000000000000000000000000000000000000000000000000004
|
||||
fileSha256: 0000000000000000000000000000000000000000000000000000000000000004
|
||||
|
||||
### byte snippet to follow ###
|
||||
]
|
||||
|
||||
Bun v1.3.5
|
||||
bun-1.3.5
|
||||
JavaScript runtime for developing, testing, and bundling
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
name: deno
|
||||
offset: 4221667
|
||||
length: 120
|
||||
snippetSha256: 89509d15b73fd2d901b20ebfe82c67fd27f70e7b01dcbf376e7e80e6c80f97d9
|
||||
fileSha256: 530a95c93ddfa41bd4d72d665e677d5af9a4275f1520e638305e60af5083a6d6
|
||||
|
||||
### byte snippet to follow ###
|
||||
g ctrl+d or close()
|
||||
Error: Uncaught cli/tools/standalone.rsdeno-canary/f4bed1081456089559c82441a13c4fb700840cac1.11.3dlw
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
name: deno
|
||||
offset: 4225763
|
||||
length: 120
|
||||
snippetSha256: b47e8cf36bdf93e4bc63a2d7bd0a5d759975f12751a66908661777eba7e90025
|
||||
fileSha256: 1a55c23bb0b666d1b26d9d16f3a9d27210b6b13421031f7f18ff18abfcb8664f
|
||||
|
||||
### byte snippet to follow ###
|
||||
g ctrl+d or close()
|
||||
Error: Uncaught cli/tools/standalone.rsdeno-canary/ed16f4332263e81bf00a39ebc2200291b1fcbb8e1.11.4dlw
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
syft/pkg/cataloger/binary/testdata/classifiers/snippets/grafana/12.4.3-security-02/linux-arm/grafana
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+57
-1
@@ -140,6 +140,22 @@ from-images:
|
||||
paths:
|
||||
- /usr/bin/deno
|
||||
|
||||
- name: deno
|
||||
version: 1.11.3
|
||||
images:
|
||||
- ref: denoland/deno:1.11.3@sha256:340d071dc71c8403c75713694db04f59b8cb7f6c7244798cd7447d55f2750596
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/bin/deno
|
||||
|
||||
- name: deno
|
||||
version: 1.11.4
|
||||
images:
|
||||
- ref: denoland/deno:1.11.4@sha256:8e9a449a50b71c779ad1c10ec79055b20a331e78a3852818fac47bc3e85b49f4
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/bin/deno
|
||||
|
||||
- name: deno
|
||||
version: 1.16.4
|
||||
images:
|
||||
@@ -188,6 +204,14 @@ from-images:
|
||||
paths:
|
||||
- /usr/bin/deno
|
||||
|
||||
- name: deno
|
||||
version: 2.9.2
|
||||
images:
|
||||
- ref: denoland/deno:ubuntu-2.9.2@sha256:0d27941f871700ebd95953c36fb1a817135c20f5afe3a645935672dfab72b2e9
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/bin/deno
|
||||
|
||||
# haskell:7.10.3 detail information is missing.
|
||||
- name: haskell-ghc
|
||||
version: 7.10.3
|
||||
@@ -1299,6 +1323,30 @@ from-images:
|
||||
paths:
|
||||
- /usr/share/grafana/bin/grafana
|
||||
|
||||
- name: grafana
|
||||
version: 12.4.3-security-02
|
||||
images:
|
||||
- ref: grafana/grafana:12.4.3-security-02@sha256:fe5bc62ae401120c48b4cd50db122a632bfca8d48a7115a32cc7c9b80c19dc48
|
||||
platform: linux/arm
|
||||
paths:
|
||||
- /usr/share/grafana/bin/grafana
|
||||
|
||||
- name: grafana
|
||||
version: 12.0.0-security-01
|
||||
images:
|
||||
- ref: grafana/grafana:12.0.0-security-01@sha256:6fc2fee54f8551a3ae452a2dd14fd524143026e46a0f06b0302c79117fe89fcf
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/share/grafana/bin/grafana
|
||||
|
||||
- name: grafana
|
||||
version: 11.0.5-security-01
|
||||
images:
|
||||
- ref: grafana/grafana:11.0.5-security-01@sha256:0ba037897c6f22f26b7d289eb1f651ed4d9d552ea454ace60307ecd0585cbee1
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/share/grafana/bin/grafana
|
||||
|
||||
- name: grafana
|
||||
version: 12.0.0
|
||||
images:
|
||||
@@ -1546,8 +1594,16 @@ from-images:
|
||||
|
||||
- version: 1.9.6
|
||||
images:
|
||||
- ref: registry.k8s.io/ingress-nginx/controller:v1.9.6@sha256:1405cc613bd95b2c6edd8b2a152510ae91c7e62aea4698500d23b2145960ab9c
|
||||
# note: per-platform manifest digests (not the index digest) -- the extraction step creates a
|
||||
# container from the ref alone, so each platform needs its own uniquely addressable ref
|
||||
- ref: registry.k8s.io/ingress-nginx/controller:v1.9.6@sha256:0939639a1f338a9eaaa490fd38b4a7881e47a7fd1a473baf8749ce15952b55b8
|
||||
platform: linux/amd64
|
||||
- ref: registry.k8s.io/ingress-nginx/controller:v1.9.6@sha256:5fb823cc617cbfbee4b250ad5e059c23078ea63e9a7e6acb6d4d30e4456eb0be
|
||||
platform: linux/arm64
|
||||
- ref: registry.k8s.io/ingress-nginx/controller:v1.9.6@sha256:dcea8d021e15cf27f59249ee0cfd211559ef9eb60b10651c3af299499848cf8b
|
||||
platform: linux/arm
|
||||
- ref: registry.k8s.io/ingress-nginx/controller:v1.9.6@sha256:fc53a604eb2d17fa0cf7a3f266d052c2f32fb97209869057d6826d1815cf622d
|
||||
platform: linux/s390x
|
||||
paths:
|
||||
- /nginx-ingress-controller
|
||||
|
||||
|
||||
@@ -285,6 +285,7 @@ func TestDpkgArchiveCataloger(t *testing.T) {
|
||||
pkgtest.NewCatalogTester().
|
||||
WithImageResolver(t, tt.name).
|
||||
IgnoreLocationLayer(). // this fixture can be rebuilt, thus the layer ID will change
|
||||
WithErrorAssertion(require.NoError).
|
||||
Expects(tt.expected, nil).
|
||||
TestCataloger(t, c)
|
||||
})
|
||||
|
||||
@@ -202,7 +202,11 @@ func getAdditionalFileListing(resolver file.Resolver, dbLocation file.Location,
|
||||
if md5Reader != nil && md5Location != nil {
|
||||
defer internal.CloseAndLogError(md5Reader, md5Location.AccessPath)
|
||||
// attach the file list
|
||||
files = append(files, parseDpkgMD5Info(md5Reader)...)
|
||||
records, err := parseDpkgMD5Info(md5Reader)
|
||||
if err != nil {
|
||||
log.Debugf("dpkg md5sums file %q: %v", md5Location.RealPath, err)
|
||||
}
|
||||
files = append(files, records...)
|
||||
|
||||
// keep a record of the file where this was discovered
|
||||
locations = append(locations, *md5Location)
|
||||
@@ -213,7 +217,11 @@ func getAdditionalFileListing(resolver file.Resolver, dbLocation file.Location,
|
||||
if conffilesReader != nil && conffilesLocation != nil {
|
||||
defer internal.CloseAndLogError(conffilesReader, conffilesLocation.AccessPath)
|
||||
// attach the file list
|
||||
files = append(files, parseDpkgConffileInfo(conffilesReader)...)
|
||||
records, err := parseDpkgConffileInfo(conffilesReader)
|
||||
if err != nil {
|
||||
log.Debugf("dpkg conffiles file %q: %v", conffilesLocation.RealPath, err)
|
||||
}
|
||||
files = append(files, records...)
|
||||
|
||||
// keep a record of the file where this was discovered
|
||||
locations = append(locations, *conffilesLocation)
|
||||
|
||||
@@ -3,8 +3,10 @@ package debian
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -13,6 +15,7 @@ import (
|
||||
"github.com/mholt/archives"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
intFile "github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
@@ -33,6 +36,7 @@ func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen
|
||||
var metadata *pkg.DpkgArchiveEntry
|
||||
var licenses []string
|
||||
var unknownErr error
|
||||
var sawControl, sawData bool
|
||||
for {
|
||||
header, err := arReader.Next()
|
||||
if err == io.EOF {
|
||||
@@ -43,19 +47,28 @@ func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(header.Name, "control.tar"):
|
||||
// a .deb has exactly one control.tar.* and one data.tar.* member; anything past the first of
|
||||
// each is attacker-supplied filler that would otherwise buy a fresh decompression budget
|
||||
case !sawControl && strings.HasPrefix(header.Name, "control.tar"):
|
||||
sawControl = true
|
||||
// Decompress the control.tar.* file
|
||||
dcReader, err := decompressionStream(ctx, arReader, header.Name)
|
||||
dcReader, err := decompressionStream(ctx, arReader, header.Name, maxControlTarSize)
|
||||
if err != nil {
|
||||
return nil, nil, unknown.New(reader.Location, fmt.Errorf("failed to decompress control.tar.* file: %w", err))
|
||||
}
|
||||
metadata, err = processControlTar(dcReader)
|
||||
if err != nil {
|
||||
switch {
|
||||
case err != nil && metadata == nil:
|
||||
return nil, nil, unknown.New(reader.Location, fmt.Errorf("failed to process control.tar.* file: %w", err))
|
||||
case err != nil:
|
||||
// metadata came back usable but incomplete (a clipped file listing); keep the package and
|
||||
// record why it is partial
|
||||
unknownErr = unknown.Append(unknownErr, reader.Location, fmt.Errorf("partial control.tar.* file: %w", err))
|
||||
}
|
||||
case strings.HasPrefix(header.Name, "data.tar"):
|
||||
case !sawData && strings.HasPrefix(header.Name, "data.tar"):
|
||||
sawData = true
|
||||
// Decompress the data.tar.* file
|
||||
dcReader, err := decompressionStream(ctx, arReader, header.Name)
|
||||
dcReader, err := decompressionStream(ctx, arReader, header.Name, maxDataTarSize)
|
||||
if err != nil {
|
||||
return nil, nil, unknown.New(reader.Location, fmt.Errorf("failed to decompress data.tar.* file: %w", err))
|
||||
}
|
||||
@@ -64,15 +77,21 @@ func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen
|
||||
unknownErr = unknown.Append(unknownErr, reader.Location, fmt.Errorf("failed to process data.tar.* file: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if sawControl && sawData {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if metadata == nil {
|
||||
return nil, nil, unknown.New(reader.Location, fmt.Errorf("no application found described in .dpkg archive"))
|
||||
}
|
||||
|
||||
// a partial parse still yields a usable package, so report what went wrong alongside it rather than
|
||||
// dropping either one
|
||||
return []pkg.Package{
|
||||
newDebArchivePackage(ctx, reader.Location, *metadata, licenses),
|
||||
}, nil, nil
|
||||
}, nil, unknownErr
|
||||
}
|
||||
|
||||
// this is the pattern you'd expect to see in a tar header for a debian package license file ()
|
||||
@@ -81,6 +100,7 @@ var archiveHeaderLicensePathPattern = regexp.MustCompile(`^\.?/usr/share/doc/[^/
|
||||
func processDataTar(dcReader io.ReadCloser) ([]string, error) {
|
||||
defer internal.CloseAndLogError(dcReader, "")
|
||||
var licenses []string
|
||||
var copyrightFiles int
|
||||
|
||||
tarReader := tar.NewReader(dcReader)
|
||||
for {
|
||||
@@ -96,6 +116,10 @@ func processDataTar(dcReader io.ReadCloser) ([]string, error) {
|
||||
// TODO: in the future we can add archive sub indexes to the locations to see where within
|
||||
// the dpkg archive the license was found
|
||||
if archiveHeaderLicensePathPattern.MatchString(header.Name) {
|
||||
copyrightFiles++
|
||||
if copyrightFiles > maxCopyrightFiles {
|
||||
return licenses, fmt.Errorf("copyright file count exceeds %d; remaining files not read", maxCopyrightFiles)
|
||||
}
|
||||
licenses = append(licenses, parseLicensesFromCopyright(tarReader)...)
|
||||
}
|
||||
}
|
||||
@@ -103,6 +127,10 @@ func processDataTar(dcReader io.ReadCloser) ([]string, error) {
|
||||
return licenses, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
defer internal.CloseAndLogError(dcReader, "")
|
||||
|
||||
@@ -111,6 +139,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
|
||||
var metadata *pkg.DpkgArchiveEntry
|
||||
var files []pkg.DpkgFileRecord
|
||||
var confFileRecords []pkg.DpkgFileRecord
|
||||
var listingErr error
|
||||
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
@@ -118,7 +147,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read control tar: %w", err)
|
||||
return metadata, fmt.Errorf("failed to read control tar: %w", err)
|
||||
}
|
||||
|
||||
switch filepath.Base(header.Name) {
|
||||
@@ -134,11 +163,15 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
|
||||
entry := pkg.DpkgArchiveEntry(entries[0].toDpkgEntry())
|
||||
metadata = &entry
|
||||
case "md5sums":
|
||||
// parseDpkgMD5Info already streams via bufio.Scanner
|
||||
files = parseDpkgMD5Info(tarReader)
|
||||
// parseDpkgMD5Info streams via bufio.Scanner and reports its own clipping/scan errors
|
||||
var err error
|
||||
files, err = parseDpkgMD5Info(tarReader)
|
||||
listingErr = errors.Join(listingErr, err)
|
||||
case "conffiles":
|
||||
// parseDpkgConffileInfo already streams via bufio.Scanner
|
||||
confFileRecords = parseDpkgConffileInfo(tarReader)
|
||||
// parseDpkgConffileInfo streams via bufio.Scanner and reports its own clipping/scan errors
|
||||
var err error
|
||||
confFileRecords, err = parseDpkgConffileInfo(tarReader)
|
||||
listingErr = errors.Join(listingErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,10 +192,36 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
|
||||
}
|
||||
|
||||
metadata.Files = files
|
||||
return metadata, nil
|
||||
|
||||
// a clipped or unreadable file listing leaves the package usable, so hand both back and let the
|
||||
// caller decide
|
||||
return metadata, listingErr
|
||||
}
|
||||
|
||||
func decompressionStream(ctx context.Context, r io.Reader, filePath string) (io.ReadCloser, error) {
|
||||
// a .deb's member sizes are bounded by the archive, but what they decompress to is not: xz and zstd
|
||||
// both exceed gzip's ~1032:1, so a few KB member can expand without limit. The two members get
|
||||
// different budgets because they hold different things.
|
||||
const (
|
||||
// control.tar holds only metadata (control, md5sums, conffiles). Even a package shipping a hundred
|
||||
// thousand files lands around 12MB of md5sums text.
|
||||
maxControlTarSize int64 = 16 * intFile.MB
|
||||
|
||||
// data.tar is the package payload, which is legitimately large (texlive and cuda ship hundreds of
|
||||
// MB), so this is only here to stop the tar walk from spinning forever on a bomb. It mirrors the
|
||||
// perFileReadLimit already used for archive extraction in internal/file/copy.go.
|
||||
maxDataTarSize int64 = 2 * intFile.GB
|
||||
|
||||
// a real .deb ships exactly one copyright file per package.
|
||||
maxCopyrightFiles = 1000
|
||||
)
|
||||
|
||||
// sentinels so callers (and tests) can match with errors.Is instead of substring matching.
|
||||
var (
|
||||
errDecompressedTooLarge = errors.New("decompressed stream is larger than the max allowed size")
|
||||
errClippedFileListing = errors.New("file listing exceeds the max allowed entries")
|
||||
)
|
||||
|
||||
func decompressionStream(ctx context.Context, r io.Reader, filePath string, maxDecompressed int64) (io.ReadCloser, error) {
|
||||
format, stream, err := archives.Identify(ctx, filePath, r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to identify compression format: %w", err)
|
||||
@@ -178,5 +237,45 @@ func decompressionStream(ctx context.Context, r io.Reader, filePath string) (io.
|
||||
return nil, fmt.Errorf("failed to create decompression reader: %w", err)
|
||||
}
|
||||
|
||||
return rc, nil
|
||||
return newBoundedReadCloser(rc, maxDecompressed), nil
|
||||
}
|
||||
|
||||
// boundedReadCloser fails the read past a byte budget rather than truncating.
|
||||
//
|
||||
// io.LimitedReader is the obvious thing to reach for here and is wrong for this: past its limit it
|
||||
// just stops returning bytes, and both consumers of this are tar readers, which read a truncated
|
||||
// stream as a clean end of archive. Together that emits a package silently missing files, which is
|
||||
// the exact failure the bound exists to catch. The signal is recoverable by checking
|
||||
// LimitedReader.N after the walk finishes (the same shape as safeCopy in internal/file/copy.go,
|
||||
// which counts bytes off io.Copy), but nothing obliges a caller to write that check and a missed
|
||||
// one is silent all over again. Erroring at the Read is the version that cannot be forgotten.
|
||||
//
|
||||
// net/http's MaxBytesReader has precisely these semantics and is stdlib, but reaching into the HTTP
|
||||
// package to parse a .deb leaves the next reader wondering what it is doing here.
|
||||
type boundedReadCloser struct {
|
||||
io.ReadCloser
|
||||
remaining int64
|
||||
limit int64
|
||||
}
|
||||
|
||||
func newBoundedReadCloser(rc io.ReadCloser, limit int64) *boundedReadCloser {
|
||||
// budget is one byte past the cap so a stream of exactly limit bytes still reaches EOF normally,
|
||||
// and only the byte after the cap trips the error. Clamp so a limit near math.MaxInt64 can't wrap
|
||||
// remaining negative and fail every read immediately.
|
||||
if limit < 0 || limit > math.MaxInt64-1 {
|
||||
limit = math.MaxInt64 - 1
|
||||
}
|
||||
return &boundedReadCloser{ReadCloser: rc, remaining: limit + 1, limit: limit}
|
||||
}
|
||||
|
||||
func (b *boundedReadCloser) Read(p []byte) (int, error) {
|
||||
if b.remaining <= 0 {
|
||||
return 0, fmt.Errorf("%w (%d bytes)", errDecompressedTooLarge, b.limit)
|
||||
}
|
||||
if int64(len(p)) > b.remaining {
|
||||
p = p[:b.remaining]
|
||||
}
|
||||
n, err := b.ReadCloser.Read(p)
|
||||
b.remaining -= int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
@@ -3,11 +3,22 @@ package debian
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/iotest"
|
||||
|
||||
"github.com/blakesmith/ar"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
)
|
||||
|
||||
func TestProcessControlTar(t *testing.T) {
|
||||
@@ -85,3 +96,273 @@ func writeTarEntry(t *testing.T, tw *tar.Writer, name, content string) {
|
||||
_, err := tw.Write([]byte(content))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_boundedReadCloser(t *testing.T) {
|
||||
payload := []byte("0123456789")
|
||||
|
||||
t.Run("a stream at exactly the cap reads through whole", func(t *testing.T) {
|
||||
// the positive assertion is the point: it proves the one-byte-past budget does not truncate
|
||||
// at-cap content, which a bare "did not error" check would not catch
|
||||
r := newBoundedReadCloser(io.NopCloser(bytes.NewReader(payload)), int64(len(payload)))
|
||||
|
||||
got, err := io.ReadAll(r)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, payload, got)
|
||||
})
|
||||
|
||||
t.Run("a stream past the cap errors rather than truncating", func(t *testing.T) {
|
||||
r := newBoundedReadCloser(io.NopCloser(bytes.NewReader(payload)), int64(len(payload)-1))
|
||||
|
||||
_, err := io.ReadAll(r)
|
||||
|
||||
require.ErrorIs(t, err, errDecompressedTooLarge)
|
||||
})
|
||||
|
||||
t.Run("the underlying read error is not masked", func(t *testing.T) {
|
||||
r := newBoundedReadCloser(io.NopCloser(iotest.ErrReader(errBoom)), 1024)
|
||||
|
||||
_, err := io.ReadAll(r)
|
||||
|
||||
require.ErrorIs(t, err, errBoom)
|
||||
})
|
||||
}
|
||||
|
||||
var errBoom = errors.New("boom")
|
||||
|
||||
func Test_decompressionStream_boundsDecompressedSize(t *testing.T) {
|
||||
// a highly compressible payload standing in for a decompression bomb: tiny compressed, large out
|
||||
var raw bytes.Buffer
|
||||
raw.Write(make([]byte, 512*1024))
|
||||
|
||||
var gz bytes.Buffer
|
||||
gw := gzip.NewWriter(&gz)
|
||||
_, err := gw.Write(raw.Bytes())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, gw.Close())
|
||||
|
||||
require.Less(t, gz.Len(), 4096, "compressed payload should be tiny relative to what it expands to")
|
||||
|
||||
rc, err := decompressionStream(context.Background(), bytes.NewReader(gz.Bytes()), "control.tar.gz", 64*1024)
|
||||
require.NoError(t, err, "the bound is enforced on read, not on open")
|
||||
t.Cleanup(func() { _ = rc.Close() })
|
||||
|
||||
_, err = io.ReadAll(rc)
|
||||
|
||||
require.ErrorIs(t, err, errDecompressedTooLarge)
|
||||
}
|
||||
|
||||
func Test_processControlTar_rejectsABombedControlMember(t *testing.T) {
|
||||
// end to end through the real consumer: a tar reader treats a truncated stream as a clean end of
|
||||
// archive, so this asserts the bomb surfaces as an error instead of a package missing its files.
|
||||
// "control" parses fully before the md5sums stream dies, so this also covers the Fix C contract: a
|
||||
// control tar read failure must not drop the package that was already parsed.
|
||||
var tarBuf bytes.Buffer
|
||||
tw := tar.NewWriter(&tarBuf)
|
||||
writeTarEntry(t, tw, "control", "Package: test-package\nVersion: 1.0.0\n")
|
||||
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))
|
||||
|
||||
require.ErrorIs(t, err, errDecompressedTooLarge)
|
||||
require.NotNil(t, metadata, "a control tar read failure after \"control\" was parsed must not drop the package")
|
||||
assert.Equal(t, "test-package", metadata.Package)
|
||||
}
|
||||
|
||||
func Test_processControlTar_clippedListingIsUsableButReported(t *testing.T) {
|
||||
// the record cap must not drop the package: metadata comes back usable AND an error explains why the
|
||||
// file list is short, so the caller can record it as an unknown instead of discarding the package
|
||||
var tarBuf bytes.Buffer
|
||||
tw := tar.NewWriter(&tarBuf)
|
||||
writeTarEntry(t, tw, "control", "Package: test-package\nVersion: 1.0.0\n")
|
||||
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())))
|
||||
|
||||
require.Error(t, err, "clipping must be reported")
|
||||
require.ErrorIs(t, err, errClippedFileListing)
|
||||
require.NotNil(t, metadata, "the package must survive a clipped file listing")
|
||||
assert.Equal(t, "test-package", metadata.Package)
|
||||
assert.Len(t, metadata.Files, maxDpkgFileRecords)
|
||||
}
|
||||
|
||||
// buildDeb assembles a minimal .deb (an ar archive of debian-binary + control.tar.gz + data.tar.gz).
|
||||
func buildDeb(t *testing.T, controlTar, dataTar []byte) []byte {
|
||||
t.Helper()
|
||||
|
||||
gzipped := func(b []byte) []byte {
|
||||
var out bytes.Buffer
|
||||
w := gzip.NewWriter(&out)
|
||||
_, err := w.Write(b)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, w.Close())
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
aw := ar.NewWriter(&buf)
|
||||
require.NoError(t, aw.WriteGlobalHeader())
|
||||
|
||||
for _, m := range []struct {
|
||||
name string
|
||||
body []byte
|
||||
}{
|
||||
{"debian-binary", []byte("2.0\n")},
|
||||
{"control.tar.gz", gzipped(controlTar)},
|
||||
{"data.tar.gz", gzipped(dataTar)},
|
||||
} {
|
||||
require.NoError(t, aw.WriteHeader(&ar.Header{Name: m.name, Size: int64(len(m.body))}))
|
||||
_, err := aw.Write(m.body)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func Test_parseDebArchive_surfacesPartialParseAsUnknown(t *testing.T) {
|
||||
// guards a silent failure: unknownErr was accumulated and then dropped on return, so a data.tar that
|
||||
// failed to parse produced a clean-looking SBOM with no record that anything went wrong. The property
|
||||
// that actually matters is that the returned error is a *unknown.CoordinateError: that's what decides
|
||||
// between "recorded as an unknown" (this case) and "aborts the entire scan" (anything else), per
|
||||
// internal/task/executor.go and syft/create_sbom.go. A plain substring match on "data.tar" would pass
|
||||
// just as well if unknown.Append were swapped for errors.Join, which is the regression that matters.
|
||||
var controlTar bytes.Buffer
|
||||
ctw := tar.NewWriter(&controlTar)
|
||||
writeTarEntry(t, ctw, "control", "Package: test-package\nVersion: 1.0.0\nArchitecture: all\n")
|
||||
require.NoError(t, ctw.Close())
|
||||
|
||||
// a data.tar that is not a valid tar at all, so processDataTar errors
|
||||
dataTar := []byte(strings.Repeat("not a tar file at all", 100))
|
||||
|
||||
debBytes := buildDeb(t, controlTar.Bytes(), dataTar)
|
||||
|
||||
loc := file.NewVirtualLocation("/test.deb", "/test.deb")
|
||||
pkgs, _, err := parseDebArchive(context.Background(), nil, &generic.Environment{},
|
||||
file.LocationReadCloser{
|
||||
Location: loc,
|
||||
ReadCloser: io.NopCloser(bytes.NewReader(debBytes)),
|
||||
})
|
||||
|
||||
require.Len(t, pkgs, 1, "the package must still be emitted")
|
||||
assert.Equal(t, "test-package", pkgs[0].Name)
|
||||
require.Error(t, err, "a failed data.tar parse must not be swallowed")
|
||||
|
||||
coordinateErrors, remaining := unknown.ExtractCoordinateErrors(err)
|
||||
require.NoError(t, remaining, "no non-coordinate errors should remain in the chain")
|
||||
require.Len(t, coordinateErrors, 1)
|
||||
assert.Equal(t, loc.Coordinates, coordinateErrors[0].Coordinates)
|
||||
assert.Contains(t, coordinateErrors[0].Reason.Error(), "data.tar")
|
||||
}
|
||||
|
||||
func Test_parseDebArchive_controlTarBombRejectedByRealSizeBound(t *testing.T) {
|
||||
// end to end through the real entrypoint using the real maxControlTarSize (16MB), not a swapped-in
|
||||
// test value: nothing else in this suite exercises that constant at its actual call site. The bomb
|
||||
// entry is written before "control", so "control" is never reached and processControlTar returns nil
|
||||
// metadata alongside the error -- this exercises parseDebArchive's fatal branch (metadata == nil).
|
||||
var controlTar bytes.Buffer
|
||||
ctw := tar.NewWriter(&controlTar)
|
||||
writeTarEntry(t, ctw, "bomb", strings.Repeat("\x00", 20*1024*1024))
|
||||
writeTarEntry(t, ctw, "control", "Package: test-package\nVersion: 1.0.0\n")
|
||||
require.NoError(t, ctw.Close())
|
||||
|
||||
debBytes := buildDeb(t, controlTar.Bytes(), nil)
|
||||
require.Less(t, len(debBytes), 64*1024, "compressed .deb should stay tiny relative to what it expands to")
|
||||
|
||||
loc := file.NewVirtualLocation("/bomb.deb", "/bomb.deb")
|
||||
pkgs, _, err := parseDebArchive(context.Background(), nil, &generic.Environment{},
|
||||
file.LocationReadCloser{
|
||||
Location: loc,
|
||||
ReadCloser: io.NopCloser(bytes.NewReader(debBytes)),
|
||||
})
|
||||
|
||||
require.Empty(t, pkgs, "no package should be emitted when the control tar never parses")
|
||||
require.Error(t, err)
|
||||
|
||||
coordinateErrors, remaining := unknown.ExtractCoordinateErrors(err)
|
||||
require.NoError(t, remaining, "no non-coordinate errors should remain in the chain")
|
||||
require.Len(t, coordinateErrors, 1)
|
||||
require.ErrorIs(t, coordinateErrors[0].Reason, errDecompressedTooLarge)
|
||||
}
|
||||
|
||||
// arMember is one named, already-encoded ar entry body, for tests that need control over the raw
|
||||
// member list (order, duplicates) that buildDeb's fixed three-member shape does not allow.
|
||||
type arMember struct {
|
||||
name string
|
||||
body []byte
|
||||
}
|
||||
|
||||
// buildDebWithMembers is buildDeb generalized to an arbitrary member list.
|
||||
func buildDebWithMembers(t *testing.T, members []arMember) []byte {
|
||||
t.Helper()
|
||||
|
||||
var buf bytes.Buffer
|
||||
aw := ar.NewWriter(&buf)
|
||||
require.NoError(t, aw.WriteGlobalHeader())
|
||||
|
||||
for _, m := range members {
|
||||
require.NoError(t, aw.WriteHeader(&ar.Header{Name: m.name, Size: int64(len(m.body))}))
|
||||
_, err := aw.Write(m.body)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func Test_parseDebArchive_ignoresDuplicateArMembers(t *testing.T) {
|
||||
// a .deb has exactly one control.tar.* and one data.tar.*; a second data.tar.* member is
|
||||
// attacker-supplied filler that would otherwise buy a fresh decompression budget, and here would also
|
||||
// fail to parse as a tar at all -- so this proves the walk stops after the first of each kind rather
|
||||
// than merely surviving a well-formed duplicate.
|
||||
gzipped := func(b []byte) []byte {
|
||||
var out bytes.Buffer
|
||||
w := gzip.NewWriter(&out)
|
||||
_, err := w.Write(b)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, w.Close())
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
var controlTar bytes.Buffer
|
||||
ctw := tar.NewWriter(&controlTar)
|
||||
writeTarEntry(t, ctw, "control", "Package: test-package\nVersion: 1.0.0\n")
|
||||
require.NoError(t, ctw.Close())
|
||||
|
||||
var firstDataTar bytes.Buffer
|
||||
dtw := tar.NewWriter(&firstDataTar)
|
||||
require.NoError(t, dtw.Close())
|
||||
|
||||
debBytes := buildDebWithMembers(t, []arMember{
|
||||
{"debian-binary", []byte("2.0\n")},
|
||||
// the duplicate sits before control.tar.gz on purpose: the loop cannot have stopped yet at that
|
||||
// point, so only the per-member guard can keep it from being read
|
||||
{"data.tar.gz", gzipped(firstDataTar.Bytes())},
|
||||
{"data.tar.gz", []byte("not a tar file at all")}, // not even gzipped; would error if ever read
|
||||
{"control.tar.gz", gzipped(controlTar.Bytes())},
|
||||
})
|
||||
|
||||
pkgs, _, err := parseDebArchive(context.Background(), nil, &generic.Environment{},
|
||||
file.LocationReadCloser{
|
||||
Location: file.NewVirtualLocation("/test.deb", "/test.deb"),
|
||||
ReadCloser: io.NopCloser(bytes.NewReader(debBytes)),
|
||||
})
|
||||
|
||||
require.NoError(t, err, "the second data.tar.* member must never be reached")
|
||||
require.Len(t, pkgs, 1)
|
||||
}
|
||||
|
||||
func Test_processDataTar_capsCopyrightFiles(t *testing.T) {
|
||||
// a real .deb ships exactly one copyright file per package; this asserts the walk stops and errors
|
||||
// rather than accumulating an unbounded number of license reads
|
||||
var tarBuf bytes.Buffer
|
||||
tw := tar.NewWriter(&tarBuf)
|
||||
for i := 0; i < maxCopyrightFiles+10; i++ {
|
||||
writeTarEntry(t, tw, fmt.Sprintf("/usr/share/doc/pkg%d/copyright", i), "License: MIT\n")
|
||||
}
|
||||
require.NoError(t, tw.Close())
|
||||
|
||||
_, err := processDataTar(io.NopCloser(bytes.NewReader(tarBuf.Bytes())))
|
||||
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -141,7 +141,12 @@ func (d *dpkgExtractedMetadata) toDpkgEntry() pkg.DpkgDBEntry {
|
||||
|
||||
// there may be an optional conffiles section that we should persist as files
|
||||
if d.Conffiles != "" {
|
||||
entry.Files = parseDpkgConffileInfo(strings.NewReader(d.Conffiles))
|
||||
var err error
|
||||
entry.Files, err = parseDpkgConffileInfo(strings.NewReader(d.Conffiles))
|
||||
if err != nil {
|
||||
// toDpkgEntry has no error return, a log line is all that's available here
|
||||
log.Debugf("dpkg status conffiles field for package %q: %v", d.Package, err)
|
||||
}
|
||||
}
|
||||
|
||||
if entry.Files == nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package debian
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
@@ -9,10 +10,27 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func parseDpkgMD5Info(reader io.Reader) (findings []pkg.DpkgFileRecord) {
|
||||
// maxDpkgFileRecords bounds the file lists parsed out of md5sums and conffiles. A byte cap on the
|
||||
// enclosing stream is not enough on its own: the shortest line that still yields a record is 4 bytes,
|
||||
// so 16MB of control.tar buys millions of records.
|
||||
//
|
||||
// The number comes from measuring what a record actually retains, not from the struct size. A
|
||||
// DpkgFileRecord is 32 bytes, but a populated one also heap-allocates a file.Digest and three string
|
||||
// backing arrays: 121 bytes each with realistic Debian paths, 70 with minimal ones. At this bound that
|
||||
// is ~24MB per package, and the effective ceiling is that times cataloger parallelism (NumCPU*4).
|
||||
// The largest real Debian packages ship on the order of 10^4 files, so this leaves ~5x headroom.
|
||||
const maxDpkgFileRecords = 200_000
|
||||
|
||||
// parseDpkgMD5Info returns the records it managed to parse, plus a non-nil error when the record bound
|
||||
// was hit (wrapping errClippedFileListing) or the scanner failed partway through.
|
||||
func parseDpkgMD5Info(reader io.Reader) ([]pkg.DpkgFileRecord, error) {
|
||||
var findings []pkg.DpkgFileRecord
|
||||
scanner := bufio.NewScanner(reader)
|
||||
|
||||
for scanner.Scan() {
|
||||
if len(findings) >= maxDpkgFileRecords {
|
||||
return findings, fmt.Errorf("%w: dpkg md5sums listing exceeds %d entries", errClippedFileListing, maxDpkgFileRecords)
|
||||
}
|
||||
line := scanner.Text()
|
||||
fields := strings.SplitN(line, " ", 2)
|
||||
if len(fields) == 2 {
|
||||
@@ -29,13 +47,22 @@ func parseDpkgMD5Info(reader io.Reader) (findings []pkg.DpkgFileRecord) {
|
||||
})
|
||||
}
|
||||
}
|
||||
return findings
|
||||
if err := scanner.Err(); err != nil {
|
||||
return findings, fmt.Errorf("failed to scan dpkg md5sums: %w", err)
|
||||
}
|
||||
return findings, nil
|
||||
}
|
||||
|
||||
func parseDpkgConffileInfo(reader io.Reader) (findings []pkg.DpkgFileRecord) {
|
||||
// parseDpkgConffileInfo returns the records it managed to parse, plus a non-nil error when the record
|
||||
// bound was hit (wrapping errClippedFileListing) or the scanner failed partway through.
|
||||
func parseDpkgConffileInfo(reader io.Reader) ([]pkg.DpkgFileRecord, error) {
|
||||
var findings []pkg.DpkgFileRecord
|
||||
scanner := bufio.NewScanner(reader)
|
||||
|
||||
for scanner.Scan() {
|
||||
if len(findings) >= maxDpkgFileRecords {
|
||||
return findings, fmt.Errorf("%w: dpkg conffiles listing exceeds %d entries", errClippedFileListing, maxDpkgFileRecords)
|
||||
}
|
||||
line := strings.Trim(scanner.Text(), " \n")
|
||||
fields := strings.SplitN(line, " ", 2)
|
||||
|
||||
@@ -70,5 +97,8 @@ func parseDpkgConffileInfo(reader io.Reader) (findings []pkg.DpkgFileRecord) {
|
||||
findings = append(findings, record)
|
||||
}
|
||||
}
|
||||
return findings
|
||||
if err := scanner.Err(); err != nil {
|
||||
return findings, fmt.Errorf("failed to scan dpkg conffiles: %w", err)
|
||||
}
|
||||
return findings, nil
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package debian
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
@@ -45,7 +48,8 @@ func TestMD5SumInfoParsing(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, f.Close()) })
|
||||
|
||||
actual := parseDpkgMD5Info(f)
|
||||
actual, err := parseDpkgMD5Info(f)
|
||||
require.NoError(t, err)
|
||||
|
||||
if diff := cmp.Diff(test.expected, actual); diff != "" {
|
||||
t.Errorf("unexpected md5 files (-want +got):\n%s", diff)
|
||||
@@ -79,7 +83,8 @@ func TestConffileInfoParsing(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, f.Close()) })
|
||||
|
||||
actual := parseDpkgConffileInfo(f)
|
||||
actual, err := parseDpkgConffileInfo(f)
|
||||
require.NoError(t, err)
|
||||
|
||||
if diff := cmp.Diff(test.expected, actual); diff != "" {
|
||||
t.Errorf("unexpected md5 files (-want +got):\n%s", diff)
|
||||
@@ -88,3 +93,49 @@ func TestConffileInfoParsing(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseDpkgInfoFiles_boundRecordCount(t *testing.T) {
|
||||
// a byte cap on the enclosing stream cannot bound these on its own, since the shortest line that
|
||||
// still yields a record is a few bytes. exercises the real maxDpkgFileRecords const directly, since
|
||||
// the limit is no longer injectable.
|
||||
t.Run("md5sums stops at the record bound", func(t *testing.T) {
|
||||
buf := strings.Repeat("d41d8cd98f00b204e9800998ecf8427e usr/bin/x\n", maxDpkgFileRecords+10)
|
||||
|
||||
got, err := parseDpkgMD5Info(strings.NewReader(buf))
|
||||
|
||||
assert.Len(t, got, maxDpkgFileRecords)
|
||||
require.ErrorIs(t, err, errClippedFileListing)
|
||||
})
|
||||
|
||||
t.Run("conffiles stops at the record bound", func(t *testing.T) {
|
||||
buf := strings.Repeat("/etc/x.conf\n", maxDpkgFileRecords+10)
|
||||
|
||||
got, err := parseDpkgConffileInfo(strings.NewReader(buf))
|
||||
|
||||
assert.Len(t, got, maxDpkgFileRecords)
|
||||
require.ErrorIs(t, err, errClippedFileListing)
|
||||
})
|
||||
|
||||
t.Run("a listing under the bound is returned whole", func(t *testing.T) {
|
||||
// guards against the bound clipping legitimate input
|
||||
got, err := parseDpkgMD5Info(strings.NewReader(
|
||||
"d41d8cd98f00b204e9800998ecf8427e usr/bin/a\n" +
|
||||
"d41d8cd98f00b204e9800998ecf8427e usr/bin/b\n"))
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
assert.Equal(t, "/usr/bin/a", got[0].Path)
|
||||
assert.Equal(t, "/usr/bin/b", got[1].Path)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_parseDpkgMD5Info_scannerError(t *testing.T) {
|
||||
// a single line longer than bufio.MaxScanTokenSize with no trailing newline trips the scanner's
|
||||
// internal error rather than yielding a clean (and silently incomplete) empty list
|
||||
line := strings.Repeat("a", bufio.MaxScanTokenSize+1)
|
||||
|
||||
got, err := parseDpkgMD5Info(strings.NewReader(line))
|
||||
|
||||
assert.Empty(t, got)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ FROM ubuntu:22.04 AS downloader
|
||||
|
||||
RUN apt-get update && apt-get install -y curl
|
||||
|
||||
RUN curl -o zlib1g.deb http://archive.ubuntu.com/ubuntu/pool/main/z/zlib/zlib1g_1.3.dfsg-3.1ubuntu2.1_amd64.deb
|
||||
# launchpad's +files URLs are immutable, unlike pool/main which only keeps the current version
|
||||
RUN curl -fsSL -o zlib1g.deb https://launchpad.net/ubuntu/+archive/primary/+files/zlib1g_1.3.dfsg-3.1ubuntu2.1_amd64.deb
|
||||
|
||||
FROM scratch
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user