mirror of
https://github.com/anchore/syft.git
synced 2026-10-22 11:02:24 +02:00
Compare commits
19
Commits
+1
-1
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -89,13 +89,13 @@ jobs:
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 #v4.4.0
|
||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 #v4.5.1
|
||||
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@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 #v4.5.1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run zizmor"
|
||||
uses: zizmorcore/zizmor-action@6599ee8b7a49aef6a770f63d261d214911a7ce02 # v0.6.0
|
||||
uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1
|
||||
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
|
||||
|
||||
+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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -34,14 +34,14 @@ 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.23
|
||||
github.com/go-git/go-billy/v5 v5.9.0
|
||||
github.com/go-git/go-billy/v5 v5.9.1
|
||||
github.com/go-git/go-git/v5 v5.19.1
|
||||
github.com/go-test/deep v1.1.1
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||
@@ -59,11 +59,11 @@ require (
|
||||
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.19.1
|
||||
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
|
||||
@@ -83,7 +83,7 @@ require (
|
||||
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/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
|
||||
@@ -98,7 +98,7 @@ require (
|
||||
golang.org/x/time v0.15.0
|
||||
golang.org/x/tools v0.48.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.54.0
|
||||
modernc.org/sqlite v1.55.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -333,8 +333,8 @@ 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=
|
||||
@@ -418,8 +418,8 @@ 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=
|
||||
@@ -624,8 +624,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 +645,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.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
||||
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
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 +670,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=
|
||||
@@ -947,8 +947,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=
|
||||
@@ -1535,8 +1535,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog=
|
||||
modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
|
||||
modernc.org/sqlite v1.55.0 h1:hIFh0MCH0rGinQ/4KYb5/UbCkRkb+UP+OkLCVWa5MTM=
|
||||
modernc.org/sqlite v1.55.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
|
||||
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=
|
||||
|
||||
@@ -152,6 +152,31 @@ 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. This is used to present a hardlink as the underlying type it points to (at the
|
||||
// hardlink's own path) so that image results are in parity with directory results, which cannot distinguish a
|
||||
// hardlink from a regular file. Note that RealPath (the hardlink's own path) intentionally differs from
|
||||
// ref.RealPath (the target's path); callers must not assume the two agree for these locations.
|
||||
func NewVirtualLocationFromImage(realPath, accessPath string, ref file.Reference, img *image.Image) Location {
|
||||
// the FileSystemID comes from the target ref's layer. a hardlink and its target are always materialized in the
|
||||
// same layer tar (a tar hardlink entry can only reference a file within the same archive), so the target's layer
|
||||
// digest is also the hardlink path's layer.
|
||||
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{
|
||||
|
||||
@@ -53,8 +53,20 @@ 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)
|
||||
|
||||
// if the access path is itself a hardlink, surface it as the underlying type it points to, at its own path
|
||||
// (bound to the target's content), so that image results are in parity with directory results (which cannot
|
||||
// tell a hardlink from a regular file). the path-based lookup is required because the search that produced ref
|
||||
// already followed the basename link, collapsing the hardlink onto its target.
|
||||
if ownRef, targetRef, ok := r.hardLinkAtPath(accessPath, r.layers[layerPos]); ok {
|
||||
if !uniqueFileIDs.Contains(ownRef) {
|
||||
uniqueFileIDs.Add(ownRef)
|
||||
uniqueLocations = append(uniqueLocations, file.NewVirtualLocationFromImage(string(ownRef.RealPath), accessPath, targetRef, r.img))
|
||||
}
|
||||
return uniqueLocations, nil
|
||||
}
|
||||
|
||||
// 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 +77,41 @@ 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
|
||||
}
|
||||
|
||||
// hardLinkAtPath returns the hardlink's own reference and its resolved target reference when the basename of path is a
|
||||
// hardlink within the given layer. ok is false when path does not exist there or is not a hardlink. The lookup does
|
||||
// not follow the basename link so that the hardlink's own path is preserved. This adds a tree walk per
|
||||
// matched ref (path x layer); if it shows up in profiles, fold the hardlink check into the existing search resolution.
|
||||
func (r *ContainerImageAllLayers) hardLinkAtPath(path string, layerIdx int) (stereoscopeFile.Reference, stereoscopeFile.Reference, bool) {
|
||||
var own stereoscopeFile.Reference
|
||||
// use the squashed-to-layer view (not just this layer's additions) so a hardlink added in a lower layer is still
|
||||
// detected when the path is searched at a higher layer; otherwise it would collapse onto its target there.
|
||||
exists, resolution, err := r.img.Layers[layerIdx].SquashedTree.File(stereoscopeFile.Path(path))
|
||||
if err != nil || !exists || !resolution.HasReference() {
|
||||
return own, own, false
|
||||
}
|
||||
target, ok := r.resolveHardLinkTarget(*resolution.Reference, layerIdx)
|
||||
if !ok {
|
||||
return own, own, false
|
||||
}
|
||||
return *resolution.Reference, target, true
|
||||
}
|
||||
|
||||
// FilesByPath returns all file.References that match the given paths from any layer in the image.
|
||||
@@ -112,14 +143,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 +188,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 +262,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])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,6 +284,11 @@ func (r *ContainerImageAllLayers) AllLocations(ctx context.Context) <-chan file.
|
||||
tree := r.img.Layers[layerIdx].Tree
|
||||
for _, ref := range tree.AllFiles(stereoscopeFile.AllTypes()...) {
|
||||
l := file.NewLocationFromImage(string(ref.RealPath), ref, r.img)
|
||||
// surface a hardlink as the underlying type it points to (at its own path) so image results match
|
||||
// directory results, which cannot distinguish a hardlink from a regular file.
|
||||
if targetRef, ok := r.resolveHardLinkTarget(ref, layerIdx); ok {
|
||||
l = file.NewVirtualLocationFromImage(string(ref.RealPath), string(ref.RealPath), targetRef, r.img)
|
||||
}
|
||||
r.annotateLocation(&l)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -273,6 +306,21 @@ func (r *ContainerImageAllLayers) FileMetadataByLocation(location file.Location)
|
||||
return fileMetadataByLocation(r.img, location)
|
||||
}
|
||||
|
||||
// resolveHardLinkTarget returns the reference of a hardlink's underlying target (resolved relative to the given layer)
|
||||
// when ref is a hardlink; ok is false otherwise. No resolution is performed for non-hardlinks (symlinks keep their
|
||||
// existing resolution semantics).
|
||||
func (r *ContainerImageAllLayers) resolveHardLinkTarget(ref stereoscopeFile.Reference, layerIdx int) (stereoscopeFile.Reference, bool) {
|
||||
metadata, err := r.img.FileCatalog.Get(ref)
|
||||
if err != nil || metadata.Type != stereoscopeFile.TypeHardLink {
|
||||
return ref, false
|
||||
}
|
||||
resolved, err := r.img.ResolveLinkByLayerSquash(ref, layerIdx)
|
||||
if err != nil || !resolved.HasReference() {
|
||||
return ref, false
|
||||
}
|
||||
return *resolved.Reference, true
|
||||
}
|
||||
|
||||
func (r *ContainerImageAllLayers) annotateLocation(l *file.Location) {
|
||||
if !r.markVisibility || l == nil {
|
||||
return
|
||||
@@ -282,26 +330,24 @@ 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.
|
||||
// SearchByPath always follows basename links, so a hardlink surfaced at its own path (whose reference is its
|
||||
// target) still resolves to the target here and is correctly considered visible.
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -40,6 +40,17 @@ func (r *ContainerImageSquash) FilesByPath(paths ...string) ([]file.Location, er
|
||||
uniqueLocations := make([]file.Location, 0)
|
||||
|
||||
for _, path := range paths {
|
||||
// if the requested path is itself a hardlink, surface it at its own path (bound to its target's content)
|
||||
// rather than collapsing it onto the target's path, so that image results are in parity with directory
|
||||
// results (which cannot tell a hardlink from a regular file).
|
||||
if ownRef, targetRef, ok := r.hardLinkAtPath(path); ok {
|
||||
if !uniqueFileIDs.Contains(ownRef) {
|
||||
uniqueFileIDs.Add(ownRef)
|
||||
uniqueLocations = append(uniqueLocations, file.NewVirtualLocationFromImage(string(ownRef.RealPath), path, targetRef, r.img))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
ref, err := r.img.SquashedSearchContext.SearchByPath(path, filetree.FollowBasenameLinks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -82,7 +93,7 @@ func (r *ContainerImageSquash) FilesByPath(paths ...string) ([]file.Location, er
|
||||
//
|
||||
//nolint:gocognit
|
||||
func (r *ContainerImageSquash) FilesByGlob(patterns ...string) ([]file.Location, error) {
|
||||
uniqueFileIDs := stereoscopeFile.NewFileReferenceSet()
|
||||
uniqueCoordinates := file.NewCoordinateSet()
|
||||
uniqueLocations := make([]file.Location, 0)
|
||||
|
||||
for _, pattern := range patterns {
|
||||
@@ -116,10 +127,14 @@ func (r *ContainerImageSquash) FilesByGlob(patterns ...string) ([]file.Location,
|
||||
return nil, fmt.Errorf("failed to find files by path (result=%+v): %w", result, err)
|
||||
}
|
||||
for _, resolvedLocation := range resolvedLocations {
|
||||
if uniqueFileIDs.Contains(resolvedLocation.Reference()) {
|
||||
// dedup on the surfaced coordinate rather than the underlying reference: distinct hardlinks share a
|
||||
// single target reference but each has its own real path, so a reference-based dedup would collapse
|
||||
// them back onto one entry (the exact behavior this parity fix removes). symlink resolutions keep
|
||||
// their target's real path, so they still collapse as before.
|
||||
if uniqueCoordinates.Contains(resolvedLocation.Coordinates) {
|
||||
continue
|
||||
}
|
||||
uniqueFileIDs.Add(resolvedLocation.Reference())
|
||||
uniqueCoordinates.Add(resolvedLocation.Coordinates)
|
||||
uniqueLocations = append(uniqueLocations, resolvedLocation)
|
||||
}
|
||||
}
|
||||
@@ -179,10 +194,16 @@ func (r *ContainerImageSquash) AllLocations(ctx context.Context) <-chan file.Loc
|
||||
go func() {
|
||||
defer close(results)
|
||||
for _, ref := range r.img.SquashedTree().AllFiles(stereoscopeFile.AllTypes()...) {
|
||||
loc := file.NewLocationFromImage(string(ref.RealPath), ref, r.img)
|
||||
// surface a hardlink as the underlying type it points to (at its own path) so image results match
|
||||
// directory results, which cannot distinguish a hardlink from a regular file.
|
||||
if targetRef, ok := r.resolveHardLinkTarget(ref); ok {
|
||||
loc = file.NewVirtualLocationFromImage(string(ref.RealPath), string(ref.RealPath), targetRef, r.img)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case results <- file.NewLocationFromImage(string(ref.RealPath), ref, r.img):
|
||||
case results <- loc:
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -190,6 +211,37 @@ func (r *ContainerImageSquash) AllLocations(ctx context.Context) <-chan file.Loc
|
||||
return results
|
||||
}
|
||||
|
||||
// hardLinkAtPath returns the hardlink's own reference and its resolved target reference when the basename of path is a
|
||||
// hardlink. ok is false when path does not exist or is not a hardlink. The lookup does not follow the basename link so
|
||||
// that the hardlink's own path is preserved. This adds a tree walk per FilesByPath path; if it shows up in
|
||||
// profiles, fold the hardlink check into the existing SearchByPath resolution.
|
||||
func (r *ContainerImageSquash) hardLinkAtPath(path string) (stereoscopeFile.Reference, stereoscopeFile.Reference, bool) {
|
||||
var own stereoscopeFile.Reference
|
||||
exists, resolution, err := r.img.SquashedTree().File(stereoscopeFile.Path(path))
|
||||
if err != nil || !exists || !resolution.HasReference() {
|
||||
return own, own, false
|
||||
}
|
||||
target, ok := r.resolveHardLinkTarget(*resolution.Reference)
|
||||
if !ok {
|
||||
return own, own, false
|
||||
}
|
||||
return *resolution.Reference, target, true
|
||||
}
|
||||
|
||||
// resolveHardLinkTarget returns the reference of a hardlink's underlying target when ref is a hardlink; ok is false
|
||||
// otherwise. No resolution is performed for non-hardlinks (symlinks keep their existing resolution semantics).
|
||||
func (r *ContainerImageSquash) resolveHardLinkTarget(ref stereoscopeFile.Reference) (stereoscopeFile.Reference, bool) {
|
||||
metadata, err := r.img.FileCatalog.Get(ref)
|
||||
if err != nil || metadata.Type != stereoscopeFile.TypeHardLink {
|
||||
return ref, false
|
||||
}
|
||||
resolved, err := r.img.ResolveLinkByImageSquash(ref)
|
||||
if err != nil || !resolved.HasReference() {
|
||||
return ref, false
|
||||
}
|
||||
return *resolved.Reference, true
|
||||
}
|
||||
|
||||
func (r *ContainerImageSquash) FilesByMIMEType(types ...string) ([]file.Location, error) {
|
||||
refs, err := r.img.SquashedSearchContext.SearchByMIMEType(types...)
|
||||
if err != nil {
|
||||
|
||||
@@ -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,87 @@ 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("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])
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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,17 @@ 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{
|
||||
@@ -3036,8 +3069,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
|
||||
@@ -889,7 +898,13 @@ func DefaultClassifiers() []binutils.Classifier {
|
||||
{
|
||||
Class: "gzip-binary",
|
||||
FileGlob: "**/gzip",
|
||||
// GNU gzip assembles its version banner at runtime from argv[0], so the version is stored as a
|
||||
// bare NUL-delimited token with nothing to anchor against. Require a gzip-specific string
|
||||
// elsewhere in the file before trusting that token, otherwise any binary reachable by a path
|
||||
// named "gzip" (such as the busybox multicall binary, which provides a gzip applet by symlink)
|
||||
// is reported as GNU gzip with an arbitrary version.
|
||||
EvidenceMatcher: m.FileContentsVersionMatcher(
|
||||
`not in gzip format|bug-gzip@gnu\.org|GZIP environment variable`,
|
||||
`\x00(?P<version>[0-9]+\.[0-9]+)\x00`,
|
||||
),
|
||||
Package: "gzip",
|
||||
|
||||
Binary file not shown.
+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.
+24
@@ -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
|
||||
|
||||
@@ -2333,6 +2333,9 @@
|
||||
"@ais-ltd/strategyen": [
|
||||
"cpe:2.3:a:ais-ltd:strategyen:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@amoy/common": [
|
||||
"cpe:2.3:a:amoyjs:common:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@angular/core": [
|
||||
"cpe:2.3:a:angular:angular:*:*:*:*:*:node.js:*:*",
|
||||
"cpe:2.3:a:angularjs:angularjs:*:*:*:*:*:*:*:*"
|
||||
@@ -4252,6 +4255,9 @@
|
||||
"json8-merge-patch": [
|
||||
"cpe:2.3:a:json8-merge-patch_project:json8-merge-patch:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"jsonpath": [
|
||||
"cpe:2.3:a:dchester:jsonpath:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"jsonpointer": [
|
||||
"cpe:2.3:a:janl:jsonpointer:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
@@ -6858,6 +6864,9 @@
|
||||
"pydantic-ai": [
|
||||
"cpe:2.3:a:pydantic:pydantic_ai:*:*:*:*:*:python:*:*"
|
||||
],
|
||||
"pydantic-settings": [
|
||||
"cpe:2.3:a:pydantic:pydantic-settings:*:*:*:*:*:*:*:*"
|
||||
],
|
||||
"pydash": [
|
||||
"cpe:2.3:a:derrickgilland:pydash:*:*:*:*:*:python:*:*"
|
||||
],
|
||||
@@ -7208,6 +7217,9 @@
|
||||
"encoded_id-rails": [
|
||||
"cpe:2.3:a:diaconou:encodedid\\:\\:rails:*:*:*:*:*:ruby:*:*"
|
||||
],
|
||||
"excon": [
|
||||
"cpe:2.3:a:excon_project:excon:*:*:*:*:*:ruby:*:*"
|
||||
],
|
||||
"faye": [
|
||||
"cpe:2.3:a:faye_project:faye:*:*:*:*:*:ruby:*:*"
|
||||
],
|
||||
@@ -7615,6 +7627,9 @@
|
||||
"ammonia": [
|
||||
"cpe:2.3:a:ammonia_project:ammonia:*:*:*:*:*:rust:*:*"
|
||||
],
|
||||
"anchor-lang": [
|
||||
"cpe:2.3:a:anchor-lang:anchor:*:*:*:*:*:rust:*:*"
|
||||
],
|
||||
"anymap": [
|
||||
"cpe:2.3:a:anymap_project:anymap:*:*:*:*:*:rust:*:*"
|
||||
],
|
||||
@@ -7984,6 +7999,9 @@
|
||||
"one2html": [
|
||||
"cpe:2.3:a:msiemens:one2html:*:*:*:*:*:rust:*:*"
|
||||
],
|
||||
"openssl": [
|
||||
"cpe:2.3:a:sfackler:openssl:*:*:*:*:*:rust:*:*"
|
||||
],
|
||||
"outer_cgi": [
|
||||
"cpe:2.3:a:outer_cgi_project:outer_cgi:*:*:*:*:*:rust:*:*"
|
||||
],
|
||||
@@ -8883,6 +8901,9 @@
|
||||
"album-and-image-gallery-plus-lightbox": [
|
||||
"cpe:2.3:a:essentialplugin:album_and_image_gallery_plus_lightbox:*:*:*:*:free:wordpress:*:*"
|
||||
],
|
||||
"alemha-watermark": [
|
||||
"cpe:2.3:a:mnbaa:alemha_watermark:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"alfred-click-collect": [
|
||||
"cpe:2.3:a:alfred24_click_\\\u0026_collect_project:alfred24_click_\\\u0026_collect:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -9510,6 +9531,9 @@
|
||||
"barcode-scanner-lite-pos-to-manage-products-inventory-and-orders": [
|
||||
"cpe:2.3:a:ukrsolution:barcode_scanner_and_inventory_manager:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"base64-encoderdecoder": [
|
||||
"cpe:2.3:a:mranderson:base64_encoder\\/decoder:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"basic-interactive-world-map": [
|
||||
"cpe:2.3:a:wpmapplugins:basic_interactive_world_map:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -9964,6 +9988,9 @@
|
||||
"bsuite": [
|
||||
"cpe:2.3:a:bsuite_project:bsuite:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"bu-section-editing": [
|
||||
"cpe:2.3:a:bu:bu_section_editing:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"bubble-menu": [
|
||||
"cpe:2.3:a:wow-company:bubble_menu:*:*:*:*:free:wordpress:*:*"
|
||||
],
|
||||
@@ -11688,6 +11715,9 @@
|
||||
"donation-thermometer": [
|
||||
"cpe:2.3:a:donation_thermometer_project:donation_thermometer:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"donations-block": [
|
||||
"cpe:2.3:a:bharatkambariya:donation_block_for_paypal:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"donations-for-woocommerce": [
|
||||
"cpe:2.3:a:wpzone:potent_donations_for_woocommerce:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -11775,6 +11805,9 @@
|
||||
"dtabs": [
|
||||
"cpe:2.3:a:dtabs_project:dtabs:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"dts-simple-share": [
|
||||
"cpe:2.3:a:missionmike:simple_share:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"duitku-social-payment-gateway": [
|
||||
"cpe:2.3:a:duitku:duitku_payment_gateway:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -12763,6 +12796,9 @@
|
||||
"five-minute-webshop": [
|
||||
"cpe:2.3:a:five_minute_webshop_project:five_minute_webshop:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"fl3r-accessibility-suite": [
|
||||
"cpe:2.3:a:fl3r:fl3r_accessibility_suite:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"fl3r-feelbox": [
|
||||
"cpe:2.3:a:armandofiore:fl3r_feelbox:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:fl3r-feelbox_project:fl3r-feelbox:*:*:*:*:*:wordpress:*:*"
|
||||
@@ -13280,6 +13316,9 @@
|
||||
"gf-block-ips": [
|
||||
"cpe:2.3:a:brightplugins:block_ips_for_gravity_forms:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"gf-constant-contact": [
|
||||
"cpe:2.3:a:rocketgenius:constant_contact:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"gf-custom-style": [
|
||||
"cpe:2.3:a:alefypimentel:gf_custom_style:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -15469,6 +15508,9 @@
|
||||
"mihdan-index-now": [
|
||||
"cpe:2.3:a:kobzarev:index_now:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"mini-course-generator": [
|
||||
"cpe:2.3:a:minicoursegenerator:mini_course_generator:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"mini-mail-dashboard-widget": [
|
||||
"cpe:2.3:a:mini_mail_dashboard_widget_project:mini_mail_dashboard_widget:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -15502,6 +15544,9 @@
|
||||
"ml-slider": [
|
||||
"cpe:2.3:a:metaslider:slider\\,_gallery\\,_and_carousel:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"mm-breaking-news": [
|
||||
"cpe:2.3:a:mm-breaking_news_project:mm-breaking_news:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"mmm-file-list": [
|
||||
"cpe:2.3:a:mediamanifesto:mmm_simple_file_list:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -15588,6 +15633,9 @@
|
||||
"mortgage-calculators-wp": [
|
||||
"cpe:2.3:a:lenderd:mortgage_calculators_wp:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"mortgage-loan-calculator": [
|
||||
"cpe:2.3:a:mlcalc:mortgage_loan_calculator:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"most-and-least-read-posts-widget": [
|
||||
"cpe:2.3:a:whiletrue:most_and_least_read_posts_widget:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -16081,6 +16129,9 @@
|
||||
"official-sendle-shipping-method": [
|
||||
"cpe:2.3:a:joovii:sendle_shipping:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"offload-videos-bunny-netaws-s3": [
|
||||
"cpe:2.3:a:webgarh:offload_videos:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"og-tags": [
|
||||
"cpe:2.3:a:og_tags_project:og_tags:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -17247,7 +17298,10 @@
|
||||
"cpe:2.3:a:wepupil:quiz_expert_-_easy_quiz_maker\\,_exam_and_test_manager:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"quiz-maker": [
|
||||
"cpe:2.3:a:ays-pro:quiz_maker:*:*:*:*:*:wordpress:*:*"
|
||||
"cpe:2.3:a:ays-pro:quiz_maker:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:ays-pro:quiz_maker:*:*:*:*:agency:wordpress:*:*",
|
||||
"cpe:2.3:a:ays-pro:quiz_maker:*:*:*:*:business:wordpress:*:*",
|
||||
"cpe:2.3:a:ays-pro:quiz_maker:*:*:*:*:developer:wordpress:*:*"
|
||||
],
|
||||
"quiz-master-next": [
|
||||
"cpe:2.3:a:expresstech:quiz_and_survey_master:*:*:*:*:*:wordpress:*:*",
|
||||
@@ -17825,6 +17879,9 @@
|
||||
"schema-app-structured-data-for-schemaorg": [
|
||||
"cpe:2.3:a:schemaapp:schema_app_structured_data:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"school-management": [
|
||||
"cpe:2.3:a:dasinfomedia:school_management_system:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"school-management-system": [
|
||||
"cpe:2.3:a:weblizar:school_management_-_education_\\\u0026_learning_management:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -18209,6 +18266,9 @@
|
||||
"simple-author-box": [
|
||||
"cpe:2.3:a:webfactoryltd:simple_author_box:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"simple-backup": [
|
||||
"cpe:2.3:a:mywebsiteadvisor:simple_backup:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"simple-banner": [
|
||||
"cpe:2.3:a:simple_banner_project:simple_banner:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -18991,6 +19051,9 @@
|
||||
"srbtranslatin": [
|
||||
"cpe:2.3:a:srbtranslatin_project:srbtranslatin:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ssv-events": [
|
||||
"cpe:2.3:a:moridrin:ssv_events:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"st-daily-tip": [
|
||||
"cpe:2.3:a:sanskruti:st-daily-tip:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -19105,6 +19168,9 @@
|
||||
"strategery-migrations": [
|
||||
"cpe:2.3:a:strategery_migrations_project:strategery_migrations:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"stratum": [
|
||||
"cpe:2.3:a:motopress:stratum:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"stray-quotes": [
|
||||
"cpe:2.3:a:unalignedcode:stray_random_quotes:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -19135,6 +19201,9 @@
|
||||
"subaccounts-for-woocommerce": [
|
||||
"cpe:2.3:a:mediaticus:subaccounts_for_woocommerce:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"sublanguage": [
|
||||
"cpe:2.3:a:maximeschoeni:sublanguage:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"subscribe-sidebar": [
|
||||
"cpe:2.3:a:blubrry:subscribe_sidebar:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -19571,12 +19640,18 @@
|
||||
"timetics": [
|
||||
"cpe:2.3:a:arraytics:wp_timetics:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"timezonecalculator": [
|
||||
"cpe:2.3:a:bernhard-riedl:timezonecalculator:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"timthumb-vulnerability-scanner": [
|
||||
"cpe:2.3:a:peterbutler:timthumb_vulnerability_scanner:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"tinychat-roomspy": [
|
||||
"cpe:2.3:a:tinychat:room_spy:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"tinymce-and-tinymce-advanced-professsional-formats-and-styles": [
|
||||
"cpe:2.3:a:blackbam:tinymce_and_tinymce_advanced_professsional_formats_and_styles:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"tinymce-custom-styles": [
|
||||
"cpe:2.3:a:tinymce_custom_styles_project:tinymce_custom_styles:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -19645,6 +19720,9 @@
|
||||
"tournamatch": [
|
||||
"cpe:2.3:a:tournamatch:tournamatch:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"tournament-bracket-generator": [
|
||||
"cpe:2.3:a:blakelong:tournament_bracket_generator:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"tp-education": [
|
||||
"cpe:2.3:a:themepalace:tp_education:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -20412,6 +20490,9 @@
|
||||
"weaverx-theme-support": [
|
||||
"cpe:2.3:a:weavertheme:weaver_xtreme_theme_support:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"web-application-firewall": [
|
||||
"cpe:2.3:a:miniorange:web_application_firewall:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"web-directory-free": [
|
||||
"cpe:2.3:a:salephpscripts:web_directory_free:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -20941,6 +21022,9 @@
|
||||
"wooemailreport": [
|
||||
"cpe:2.3:a:eggemplo:woocommerce_email_report:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wooexim": [
|
||||
"cpe:2.3:a:bulktheme:wooexim:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wooframework-tweaks": [
|
||||
"cpe:2.3:a:woocommerce:wooframework_tweaks:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -21754,6 +21838,9 @@
|
||||
"wp-jobs": [
|
||||
"cpe:2.3:a:intensewp:wp_jobs:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-jobs2careers": [
|
||||
"cpe:2.3:a:skipthedrive:wp_talroo:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-jquery-lightbox": [
|
||||
"cpe:2.3:a:lightpress:lightbox:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -22730,7 +22817,8 @@
|
||||
"cpe:2.3:a:maennchen1:wpshopgermany_-_protected_shops:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wpshopify": [
|
||||
"cpe:2.3:a:shopwp_project:shopwp:*:*:*:*:*:wordpress:*:*"
|
||||
"cpe:2.3:a:shopwp_project:shopwp:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:wpshop:shopwp:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wpsimpletools-log-viewer": [
|
||||
"cpe:2.3:a:wpsimpletools:basic_log_viewer:*:*:*:*:*:wordpress:*:*"
|
||||
@@ -23504,6 +23592,9 @@
|
||||
"tijaji": [
|
||||
"cpe:2.3:a:tijaji:tijaji:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"total": [
|
||||
"cpe:2.3:a:hashthemes:total:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"transcend": [
|
||||
"cpe:2.3:a:cpothemes:transcend:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package snapsource
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
@@ -21,6 +22,11 @@ type snapManifest struct {
|
||||
|
||||
const manifestLocation = "/meta/snap.yaml"
|
||||
|
||||
// errNoManifest means the payload is a readable squashfs that simply does not carry a snap manifest.
|
||||
// This is distinct from a manifest that is present but unusable, and callers are expected to treat
|
||||
// the two differently.
|
||||
var errNoManifest = errors.New("no snap manifest file found")
|
||||
|
||||
func parseManifest(resolver file.Resolver) (*snapManifest, error) {
|
||||
locations, err := resolver.FilesByPath(manifestLocation)
|
||||
if err != nil {
|
||||
@@ -28,7 +34,7 @@ func parseManifest(resolver file.Resolver) (*snapManifest, error) {
|
||||
}
|
||||
|
||||
if len(locations) == 0 {
|
||||
return nil, fmt.Errorf("no snap manifest file found")
|
||||
return nil, errNoManifest
|
||||
}
|
||||
|
||||
if len(locations) > 1 {
|
||||
@@ -48,9 +54,8 @@ func parseManifest(resolver file.Resolver) (*snapManifest, error) {
|
||||
return nil, fmt.Errorf("unable to decode snap manifest file: %w", err)
|
||||
}
|
||||
|
||||
if manifest.Name == "" || manifest.Version == "" {
|
||||
return nil, fmt.Errorf("invalid snap manifest file: missing name or version")
|
||||
}
|
||||
|
||||
// note: name and version are deliberately not validated here. a manifest that decoded but is
|
||||
// missing them still carries usable fields (summary, base, grade, ...) and throwing it away would
|
||||
// describe less than we know. the caller surfaces the missing identity.
|
||||
return &manifest, nil
|
||||
}
|
||||
|
||||
@@ -77,22 +77,33 @@ func getRemoteSnapFile(ctx context.Context, fs afero.Fs, getter intFile.Getter,
|
||||
return newSnapFileFromRemote(ctx, fs, cfg, getter, info)
|
||||
}
|
||||
|
||||
func newSnapFileFromRemote(ctx context.Context, fs afero.Fs, cfg Config, getter intFile.Getter, info *remoteSnap) (*snapFile, error) {
|
||||
func newSnapFileFromRemote(ctx context.Context, fs afero.Fs, cfg Config, getter intFile.Getter, info *remoteSnap) (_ *snapFile, err error) {
|
||||
t, err := afero.TempDir(fs, "", "syft-snap-")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
|
||||
closer := func() error {
|
||||
return fs.RemoveAll(t)
|
||||
}
|
||||
|
||||
// any failure past this point must not leave the temp directory (or a partially downloaded snap)
|
||||
// behind. On success the caller owns the directory by way of snapFile.Cleanup.
|
||||
defer func() {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if closeErr := closer(); closeErr != nil {
|
||||
log.WithFields("error", closeErr, "directory", t).Warn("unable to remove temp directory for snap")
|
||||
}
|
||||
}()
|
||||
|
||||
snapFilePath := path.Join(t, path.Base(info.URL))
|
||||
err = downloadSnap(getter, info, snapFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download snap file: %w", err)
|
||||
}
|
||||
|
||||
closer := func() error {
|
||||
return fs.RemoveAll(t)
|
||||
}
|
||||
|
||||
mimeType, digests, err := getSnapFileInfo(ctx, fs, snapFilePath, cfg.DigestAlgorithms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -3,6 +3,7 @@ package snapsource
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -119,7 +120,21 @@ func newFromPath(cfg Config, f *snapFile) (source.Source, error) {
|
||||
closer: f.Cleanup,
|
||||
}
|
||||
|
||||
return s, s.extractManifest()
|
||||
if err := s.extractManifest(); err != nil {
|
||||
// It is tried on any path ending in .snap or .squashfs, so declining one is routine and
|
||||
// a later provider usually succeeds. When the user asked for --from snap there is no
|
||||
// fallback and this error reaches them intact.
|
||||
log.WithFields("error", err, "path", f.Path).Debug("unable to read snap")
|
||||
|
||||
// never hand back a source alongside an error: callers that discard the source on error would
|
||||
// leave behind the open squashfs file and the temp directory holding a downloaded snap
|
||||
if closeErr := s.Close(); closeErr != nil {
|
||||
log.WithFields("error", closeErr, "path", f.Path).Warn("unable to clean up snap source")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *snapSource) extractManifest() error {
|
||||
@@ -128,13 +143,26 @@ func (s *snapSource) extractManifest() error {
|
||||
return fmt.Errorf("unable to create snap file resolver: %w", err)
|
||||
}
|
||||
|
||||
// a manifest problem must never fail the source. this source is the only provider that can descend
|
||||
// into squashfs (filesource cannot), so returning an error here would fall through to a provider
|
||||
// that reports zero packages for a payload that has plenty. describe what we can instead.
|
||||
manifest, err := parseManifest(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse snap manifest file: %w", err)
|
||||
switch {
|
||||
case errors.Is(err, errNoManifest):
|
||||
// not every squashfs payload is a snap, and one without a manifest is still worth cataloging
|
||||
log.WithFields("path", s.squashfsPath).Debug("no snap manifest file found")
|
||||
return nil
|
||||
case err != nil:
|
||||
// the manifest is there but unusable, which is worth surfacing: the SBOM will have no source
|
||||
// name or version to show for it
|
||||
log.WithFields("error", err, "path", s.squashfsPath).Warn("unable to parse snap manifest file")
|
||||
return nil
|
||||
}
|
||||
|
||||
if manifest != nil {
|
||||
s.manifest = *manifest
|
||||
s.manifest = *manifest
|
||||
|
||||
if s.manifest.Name == "" || s.manifest.Version == "" {
|
||||
log.WithFields("path", s.squashfsPath).Warn("snap manifest is missing name or version")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -176,25 +204,34 @@ func (s snapSource) Describe() source.Description {
|
||||
}
|
||||
}
|
||||
|
||||
// Close releases everything the source holds. Every step runs even if an earlier one fails, since
|
||||
// bailing early would skip the temp directory removal and leave a downloaded snap on disk. Safe to
|
||||
// call more than once.
|
||||
func (s *snapSource) Close() error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
var errs error
|
||||
if s.squashFileCloser != nil {
|
||||
if err := s.squashFileCloser(); err != nil {
|
||||
return fmt.Errorf("unable to close snap resolver: %w", err)
|
||||
errs = errors.Join(errs, fmt.Errorf("unable to close snap resolver: %w", err))
|
||||
}
|
||||
s.squashFileCloser = nil
|
||||
}
|
||||
s.resolver = nil
|
||||
if s.fs != nil {
|
||||
if err := s.fs.Close(); err != nil {
|
||||
return fmt.Errorf("unable to close snap squashfs: %w", err)
|
||||
errs = errors.Join(errs, fmt.Errorf("unable to close snap squashfs: %w", err))
|
||||
}
|
||||
s.fs = nil
|
||||
}
|
||||
if s.closer != nil {
|
||||
if err := s.closer(); err != nil {
|
||||
return fmt.Errorf("unable to close snap source: %w", err)
|
||||
errs = errors.Join(errs, fmt.Errorf("unable to close snap source: %w", err))
|
||||
}
|
||||
s.closer = nil
|
||||
}
|
||||
return nil
|
||||
return errs
|
||||
}
|
||||
|
||||
func (s *snapSource) FileResolver(_ source.Scope) (file.Resolver, error) {
|
||||
|
||||
@@ -2,15 +2,23 @@ package snapsource
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
diskFile "github.com/diskfs/go-diskfs/backend/file"
|
||||
"github.com/diskfs/go-diskfs/filesystem/squashfs"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/go-homedir"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func TestNewFromLocal(t *testing.T) {
|
||||
@@ -84,3 +92,205 @@ func TestNewFromLocal(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFromPathOnResolverError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
// remote snaps carry a cleanup closure for the temp directory holding the download; local snaps
|
||||
// have none, since the file belongs to the user
|
||||
remote bool
|
||||
wantCleanupRuns int
|
||||
wantFileRemoved bool
|
||||
}{
|
||||
{
|
||||
name: "downloaded snap is cleaned up",
|
||||
remote: true,
|
||||
wantCleanupRuns: 1,
|
||||
wantFileRemoved: true,
|
||||
},
|
||||
{
|
||||
name: "local snap file is left in place",
|
||||
remote: false,
|
||||
wantCleanupRuns: 0,
|
||||
wantFileRemoved: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// the resolver reads the squashfs from the real filesystem, so the payload must live on disk
|
||||
tempDir := t.TempDir()
|
||||
snapPath := filepath.Join(tempDir, "not-really-a-snap.snap")
|
||||
require.NoError(t, os.WriteFile(snapPath, []byte("not squashfs"), 0600))
|
||||
|
||||
var cleanupRuns int
|
||||
f := &snapFile{
|
||||
Path: snapPath,
|
||||
MimeType: "text/plain",
|
||||
}
|
||||
if tt.remote {
|
||||
f.Cleanup = func() error {
|
||||
cleanupRuns++
|
||||
return os.RemoveAll(tempDir)
|
||||
}
|
||||
}
|
||||
|
||||
got, err := newFromPath(Config{Request: snapPath}, f)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unable to create snap file resolver")
|
||||
|
||||
// a source returned alongside an error is discarded by callers without being closed, which
|
||||
// would leave the downloaded snap behind
|
||||
assert.Nil(t, got, "expected nil source on error")
|
||||
assert.Equal(t, tt.wantCleanupRuns, cleanupRuns)
|
||||
|
||||
_, statErr := os.Stat(snapPath)
|
||||
if tt.wantFileRemoved {
|
||||
assert.True(t, os.IsNotExist(statErr), "snap file was not cleaned up")
|
||||
} else {
|
||||
assert.NoError(t, statErr, "the user's snap file must never be removed")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// a manifest problem must never fail the source: snapsource is the only provider that can descend
|
||||
// into squashfs, so an error here falls through to filesource and every package inside the payload
|
||||
// silently disappears from the SBOM.
|
||||
func TestNewFromPathToleratesManifestProblems(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
manifest string // written to /meta/snap.yaml; empty means no manifest at all
|
||||
wantName string
|
||||
wantVersion string
|
||||
wantSummary string
|
||||
}{
|
||||
{
|
||||
name: "no manifest at all",
|
||||
manifest: "",
|
||||
},
|
||||
{
|
||||
name: "manifest is not valid yaml",
|
||||
manifest: "name: etcd\n\tversion: 3.4\n",
|
||||
},
|
||||
{
|
||||
name: "manifest is missing name and version",
|
||||
manifest: "summary: a thing\nbase: core22\n",
|
||||
wantSummary: "a thing",
|
||||
},
|
||||
{
|
||||
name: "manifest is complete",
|
||||
manifest: "name: etcd\nversion: 3.4.0\nsummary: a thing\n",
|
||||
wantName: "etcd",
|
||||
wantVersion: "3.4.0",
|
||||
wantSummary: "a thing",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
snapPath := writeSquashfs(t, tt.manifest)
|
||||
|
||||
src, err := newFromPath(Config{Request: snapPath}, &snapFile{Path: snapPath})
|
||||
require.NoError(t, err, "a manifest problem must not fail the source")
|
||||
require.NotNil(t, src)
|
||||
t.Cleanup(func() { _ = src.Close() })
|
||||
|
||||
desc := src.Describe()
|
||||
assert.Equal(t, tt.wantName, desc.Name)
|
||||
assert.Equal(t, tt.wantVersion, desc.Version)
|
||||
|
||||
meta, ok := desc.Metadata.(source.SnapMetadata)
|
||||
require.True(t, ok, "expected snap metadata")
|
||||
assert.Equal(t, tt.wantSummary, meta.Summary, "fields that did decode must survive")
|
||||
|
||||
// the whole point: the payload is still walkable, so cataloging sees what is inside
|
||||
r, err := src.FileResolver(source.SquashedScope)
|
||||
require.NoError(t, err)
|
||||
locations, err := r.FilesByPath("/payload.txt")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, locations, 1, "contents must remain catalogable regardless of the manifest")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnapSourceClose(t *testing.T) {
|
||||
newSource := func(closeErr, cleanupErr error) (*snapSource, *int) {
|
||||
var cleanupRuns int
|
||||
return &snapSource{
|
||||
mutex: &sync.Mutex{},
|
||||
squashFileCloser: func() error {
|
||||
return closeErr
|
||||
},
|
||||
closer: func() error {
|
||||
cleanupRuns++
|
||||
return cleanupErr
|
||||
},
|
||||
}, &cleanupRuns
|
||||
}
|
||||
|
||||
t.Run("temp directory is removed even when closing the file handle fails", func(t *testing.T) {
|
||||
s, cleanupRuns := newSource(errors.New("handle is busy"), nil)
|
||||
|
||||
err := s.Close()
|
||||
|
||||
// the whole point: a failure here used to return early and skip the cleanup below, leaving a
|
||||
// downloaded snap on disk
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "handle is busy")
|
||||
assert.Equal(t, 1, *cleanupRuns, "temp directory removal was skipped")
|
||||
})
|
||||
|
||||
t.Run("every failure is reported, not just the first", func(t *testing.T) {
|
||||
s, _ := newSource(errors.New("handle is busy"), errors.New("directory is busy"))
|
||||
|
||||
err := s.Close()
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "handle is busy")
|
||||
assert.Contains(t, err.Error(), "directory is busy")
|
||||
})
|
||||
|
||||
t.Run("is safe to call twice", func(t *testing.T) {
|
||||
s, cleanupRuns := newSource(nil, nil)
|
||||
|
||||
require.NoError(t, s.Close())
|
||||
require.NoError(t, s.Close())
|
||||
assert.Equal(t, 1, *cleanupRuns, "cleanup must not run again on a second close")
|
||||
})
|
||||
}
|
||||
|
||||
// writeSquashfs builds a real squashfs image containing /payload.txt and, when manifest is non-empty,
|
||||
// /meta/snap.yaml. Returns the path to the image.
|
||||
func writeSquashfs(t *testing.T, manifest string) string {
|
||||
t.Helper()
|
||||
|
||||
snapPath := filepath.Join(t.TempDir(), "test.snap")
|
||||
f, err := os.Create(snapPath)
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, f.Close()) }()
|
||||
|
||||
fs, err := squashfs.Create(diskFile.New(f, false), 0, 0, 4096)
|
||||
require.NoError(t, err)
|
||||
|
||||
write := func(path, contents string) {
|
||||
t.Helper()
|
||||
w, err := fs.OpenFile(path, os.O_CREATE|os.O_RDWR)
|
||||
require.NoError(t, err)
|
||||
_, err = w.Write([]byte(contents))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// note: go-diskfs wants paths relative to the image root, without a leading slash
|
||||
require.NoError(t, fs.Mkdir("."))
|
||||
write("payload.txt", "hello")
|
||||
|
||||
if manifest != "" {
|
||||
require.NoError(t, fs.Mkdir("meta"))
|
||||
write(strings.TrimPrefix(manifestLocation, "/"), manifest)
|
||||
}
|
||||
|
||||
require.NoError(t, fs.Finalize(squashfs.FinalizeOptions{}))
|
||||
return snapPath
|
||||
}
|
||||
|
||||
@@ -264,6 +264,10 @@ func TestNewSnapFileFromRemote(t *testing.T) {
|
||||
|
||||
_, err = fs.Stat(result.Path)
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
|
||||
// cleanup owns the whole temp directory, not just the snap file within it
|
||||
_, err = fs.Stat(filepath.Dir(result.Path))
|
||||
assert.True(t, os.IsNotExist(err), "temp directory outlived the cleanup closure")
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -315,6 +319,75 @@ func TestNewSnapFileFromRemote(t *testing.T) {
|
||||
expectError: true,
|
||||
errorMsg: "failed to download snap file",
|
||||
},
|
||||
{
|
||||
name: "download writes partial data then fails",
|
||||
cfg: Config{
|
||||
DigestAlgorithms: []crypto.Hash{crypto.SHA256},
|
||||
},
|
||||
info: &remoteSnap{
|
||||
snapIdentity: snapIdentity{
|
||||
Name: "partial-snap",
|
||||
Channel: "stable",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
URL: "https://api.snapcraft.io/download/partial_snap.snap",
|
||||
},
|
||||
setupMock: func(mockGetter *mockFileGetter, fs afero.Fs) {
|
||||
mockGetter.On("GetFile", mock.AnythingOfType("string"), "https://api.snapcraft.io/download/partial_snap.snap", mock.Anything).Run(func(args mock.Arguments) {
|
||||
// simulate a transfer that writes some bytes before the connection drops
|
||||
require.NoError(t, createMockSquashfsFile(fs, args.String(0)))
|
||||
}).Return(fmt.Errorf("connection reset by peer"))
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "failed to download snap file",
|
||||
},
|
||||
{
|
||||
// isSquashFSFile trusts the .snap extension, so non-squashfs content under that name is accepted
|
||||
name: "downloaded file has a snap extension but non-snap content",
|
||||
cfg: Config{
|
||||
DigestAlgorithms: []crypto.Hash{crypto.SHA256},
|
||||
},
|
||||
info: &remoteSnap{
|
||||
snapIdentity: snapIdentity{
|
||||
Name: "not-a-snap",
|
||||
Channel: "stable",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
URL: "https://api.snapcraft.io/download/not_a_snap.snap",
|
||||
},
|
||||
setupMock: func(mockGetter *mockFileGetter, fs afero.Fs) {
|
||||
mockGetter.On("GetFile", mock.AnythingOfType("string"), "https://api.snapcraft.io/download/not_a_snap.snap", mock.Anything).Run(func(args mock.Arguments) {
|
||||
require.NoError(t, afero.WriteFile(fs, args.String(0), []byte("not squashfs"), 0644))
|
||||
}).Return(nil)
|
||||
},
|
||||
expectError: false,
|
||||
validate: func(t *testing.T, result *snapFile, fs afero.Fs) {
|
||||
assert.NotNil(t, result)
|
||||
assert.Contains(t, result.Path, "not_a_snap.snap")
|
||||
assert.NotNil(t, result.Cleanup)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "downloaded file is not a snap",
|
||||
cfg: Config{
|
||||
DigestAlgorithms: []crypto.Hash{crypto.SHA256},
|
||||
},
|
||||
info: &remoteSnap{
|
||||
snapIdentity: snapIdentity{
|
||||
Name: "not-a-snap",
|
||||
Channel: "stable",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
URL: "https://api.snapcraft.io/download/not_a_snap.tar.gz",
|
||||
},
|
||||
setupMock: func(mockGetter *mockFileGetter, fs afero.Fs) {
|
||||
mockGetter.On("GetFile", mock.AnythingOfType("string"), "https://api.snapcraft.io/download/not_a_snap.tar.gz", mock.Anything).Run(func(args mock.Arguments) {
|
||||
require.NoError(t, afero.WriteFile(fs, args.String(0), []byte("not squashfs"), 0644))
|
||||
}).Return(nil)
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "not a valid squashfs/snap file",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -334,8 +407,18 @@ func TestNewSnapFileFromRemote(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), tt.errorMsg)
|
||||
}
|
||||
assert.Nil(t, result)
|
||||
|
||||
// the temp directory and any partially downloaded payload must not be left behind. The
|
||||
// download destination handed to the getter lives directly under the temp directory.
|
||||
require.Len(t, mockGetter.Calls, 1)
|
||||
tempDir := filepath.Dir(mockGetter.Calls[0].Arguments.String(0))
|
||||
_, statErr := fs.Stat(tempDir)
|
||||
assert.True(t, os.IsNotExist(statErr), "temp directory %q was not cleaned up after failure", tempDir)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
if result.Cleanup != nil {
|
||||
t.Cleanup(func() { _ = result.Cleanup() })
|
||||
}
|
||||
if tt.validate != nil {
|
||||
tt.validate(t, result, fs)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user