From b254e6d92f28c3868a755f62fb3ca8f26e9fee76 Mon Sep 17 00:00:00 2001 From: anchore-oss-update-bot Date: Tue, 6 Oct 2026 10:36:33 -0400 Subject: [PATCH 1/4] chore(deps): update anchore dependencies (#5367) Signed-off-by: anchore-oss-update-bot Co-authored-by: anchore-oss-update-bot --- go.mod | 14 +++++++------- go.sum | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index 996aa4ad6..02ae7c253 100644 --- a/go.mod +++ b/go.mod @@ -18,11 +18,11 @@ require ( github.com/anchore/go-homedir v0.1.1 github.com/anchore/go-logger v0.2.0 github.com/anchore/go-macholibre v0.1.1 - github.com/anchore/go-rpmdb v0.2.2-0.20261002172926-a488674d6c87 + github.com/anchore/go-rpmdb v0.2.2 github.com/anchore/go-sync v0.1.2 github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b github.com/anchore/packageurl-go v0.2.0 - github.com/anchore/stereoscope v0.3.4-0.20261002170525-59d3a69590a6 + github.com/anchore/stereoscope v0.3.4 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be github.com/aquasecurity/go-pep440-version v0.0.1 github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef @@ -188,7 +188,7 @@ require ( github.com/containers/ocicrypt v1.3.2 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/cyphar/filepath-securejoin v0.7.0 // indirect - github.com/docker/cli v29.8.1+incompatible // indirect + github.com/docker/cli v29.8.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.8 // indirect github.com/docker/go-connections v0.8.1 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -281,11 +281,11 @@ require ( github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect - github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.11.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sigstore/fulcio v1.8.7 // indirect - github.com/sigstore/protobuf-specs v0.5.1 // indirect - github.com/sigstore/sigstore v1.10.8 // indirect + github.com/sigstore/protobuf-specs v0.5.2 // indirect + github.com/sigstore/sigstore v1.10.11 // indirect github.com/sirupsen/logrus v1.10.2 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d // indirect @@ -299,7 +299,7 @@ require ( github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/sylabs/sif/v2 v2.24.1 // indirect + github.com/sylabs/sif/v2 v2.24.2 // indirect github.com/sylabs/squashfs v1.0.6 // indirect github.com/tchap/go-patricia/v2 v2.3.3 // indirect github.com/therootcompany/xz v1.0.1 // indirect diff --git a/go.sum b/go.sum index b82ae0667..904138143 100644 --- a/go.sum +++ b/go.sum @@ -140,8 +140,8 @@ github.com/anchore/go-lzo v0.1.1 h1:IwL/fvkdtlIrYIXck6WxZ3nb8WjjHziYYmGxlooyOnM= github.com/anchore/go-lzo v0.1.1/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk= github.com/anchore/go-macholibre v0.1.1 h1:sTBHi3S1WVIOCamc6NyT8Sn9mBmD30DCTfjfVm4k6/M= github.com/anchore/go-macholibre v0.1.1/go.mod h1:YNq2610RlvGCK0Za+Klz/OCMtUaEnwwAGA/VfUAQaro= -github.com/anchore/go-rpmdb v0.2.2-0.20261002172926-a488674d6c87 h1:aY7DcEkjgzMRbCCJsGHu8eQeL5I88V8G9ISEWtfrlCU= -github.com/anchore/go-rpmdb v0.2.2-0.20261002172926-a488674d6c87/go.mod h1:Y64EbetdqhryMjtk8IzXkdCQw/aNDx+feZCT9aj6rjs= +github.com/anchore/go-rpmdb v0.2.2 h1:44VC7mvFAxV9t5JUZYg3S9sqG+E94iZvyWACMDq9hSk= +github.com/anchore/go-rpmdb v0.2.2/go.mod h1:Y64EbetdqhryMjtk8IzXkdCQw/aNDx+feZCT9aj6rjs= github.com/anchore/go-struct-converter v0.2.0-rc2 h1:q+859fW2/jbHJHB2etbNfRlFwYpknyvbqqk1hUdamQ4= github.com/anchore/go-struct-converter v0.2.0-rc2/go.mod h1:cDBA5vhcR62nXWo8QH9/Kk2807o65ISaHPNPX66L+Uw= github.com/anchore/go-sync v0.1.2 h1:RDD5RCanrWqhbtBulqPSZpERadUlgcYjpybYzYd3Uk8= @@ -150,8 +150,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk= github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE= -github.com/anchore/stereoscope v0.3.4-0.20261002170525-59d3a69590a6 h1:6l3YwREGEXX/w4tBhRcPb7efYWo4pf8+fmAy0mND4EY= -github.com/anchore/stereoscope v0.3.4-0.20261002170525-59d3a69590a6/go.mod h1:LxL4iOFtB8XMbA1MykUUTkmtQmDuYdbr4+TkBkCfljU= +github.com/anchore/stereoscope v0.3.4 h1:dT5AfjCTXUJowMiRIc+VBQVwvwnIEwnxxwA1GwAkMxw= +github.com/anchore/stereoscope v0.3.4/go.mod h1:+HxqHTt7dvH8mea0mf3ulqkf084VvHiT1grpAQf5uEs= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= @@ -353,8 +353,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= -github.com/docker/cli v29.8.1+incompatible h1:qYL1bCp6cRw2SB1xmLlIOPyV171dilw9W2Jew38vy9c= -github.com/docker/cli v29.8.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.8.2+incompatible h1:2zgdFuoFst2T80oS42vhKGxkd0JRo305eIEKTsxR7pQ= +github.com/docker/cli v29.8.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker-credential-helpers v0.9.8 h1:bIREROb7So6PRlq6KTtdS9MPEjC29OQRkFNlvK2OX8Q= @@ -893,8 +893,8 @@ github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd7 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc= github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= -github.com/secure-systems-lab/go-securesystemslib v0.11.0 h1:iuCR9kcMFD4QurdKrGvPLoKZLv9YvwPYVr0473BdtFs= -github.com/secure-systems-lab/go-securesystemslib v0.11.0/go.mod h1:+PMOTjUGwHj2vcZ+TFKlb1tXRbrdWE1LYDT5i9JC80Q= +github.com/secure-systems-lab/go-securesystemslib v0.11.1 h1:ayahDPjfSIqKegyt5YVGEvQ7SAi72GHXTy+b2YjUd5w= +github.com/secure-systems-lab/go-securesystemslib v0.11.1/go.mod h1:UyOjhoZLi76ir63u1ptubEevVe6dlNPmNxwgVvKqFFY= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= @@ -903,10 +903,10 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sigstore/fulcio v1.8.7 h1:d7QJQxukgXarqPCT9uDNeyzt1Bbt/biw58/VfOnwI/o= github.com/sigstore/fulcio v1.8.7/go.mod h1:7fX+QyigZxRGKTgHvTfOfxwwniCDxK02PVloHy5/vg4= -github.com/sigstore/protobuf-specs v0.5.1 h1:/5OPaNuolRJmQfeZLayJGFXMpsRJEdgC6ah1/+7Px7U= -github.com/sigstore/protobuf-specs v0.5.1/go.mod h1:DRBzpFuE+LnvQMN10/dU6nBeKwVLGEQ6o2FovN2Rats= -github.com/sigstore/sigstore v1.10.8 h1:1Mgkxvkw4AXMfIP1DOjc6kw0GkUgA8pGVpveN/EfOq4= -github.com/sigstore/sigstore v1.10.8/go.mod h1:f9+B/4iaYimvUkySyb2mvc73n3RLqNn24grHZM/ET8M= +github.com/sigstore/protobuf-specs v0.5.2 h1:RSWWUY8QrVTxbYH00jY/jg2e7YnjzrpwP+PeHTMll0E= +github.com/sigstore/protobuf-specs v0.5.2/go.mod h1:DRBzpFuE+LnvQMN10/dU6nBeKwVLGEQ6o2FovN2Rats= +github.com/sigstore/sigstore v1.10.11 h1:fjrwDfR0AWmRONYV/hKGJyqbT0osMtaBz9FTZ6MH9XI= +github.com/sigstore/sigstore v1.10.11/go.mod h1:ecPk+ftLqP8/TmzQFJmn0+QrL2yUQIJvyzVKNmDiw6U= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -970,8 +970,8 @@ github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUm github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/sylabs/sif/v2 v2.24.1 h1:OhTOfTwBaGXfbWYWXK9C6Pojma3A1bTaNJ6CEhyuKok= -github.com/sylabs/sif/v2 v2.24.1/go.mod h1:PRq9MoP0g+p0qE5ZRGrOhyVAUdHrPsAVmXHgoiW6VU0= +github.com/sylabs/sif/v2 v2.24.2 h1:rdkFf6z3xLC6yKweU7S9marlKfkS91Ht0ruIri6YWFY= +github.com/sylabs/sif/v2 v2.24.2/go.mod h1:bbrkk0JhTX+xNJcnUtmDkivoO2r4DGWM3LxNR0U2ooA= github.com/sylabs/squashfs v1.0.6 h1:PvJcDzxr+vIm2kH56mEMbaOzvGu79gK7P7IX+R7BDZI= github.com/sylabs/squashfs v1.0.6/go.mod h1:DlDeUawVXLWAsSRa085Eo0ZenGzAB32JdAUFaB0LZfE= github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA= From d9489c2230b1751f84c6dad031e3492360ddc20f Mon Sep 17 00:00:00 2001 From: anchore-oss-update-bot Date: Wed, 7 Oct 2026 06:27:20 -0400 Subject: [PATCH 2/4] chore(deps): update tool versions (#5381) Signed-off-by: anchore-oss-update-bot Co-authored-by: anchore-oss-update-bot --- .binny.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.binny.yaml b/.binny.yaml index 561c3a792..cbada220d 100644 --- a/.binny.yaml +++ b/.binny.yaml @@ -24,7 +24,7 @@ tools: # used to parse JSON/YAML annotations on the fixture cache image - name: yq version: - want: v4.53.6 + want: v4.54.1 method: github-release with: repo: mikefarah/yq From b0a8de7bf65cdf78ec185be7b28739c309d698c9 Mon Sep 17 00:00:00 2001 From: Metal <911jason@163.com> Date: Thu, 8 Oct 2026 04:28:15 +0800 Subject: [PATCH 3/4] fix(redact): reset redaction store between command runs (#5386) * fix(redact): reset redaction store between command runs cli.Command() may be invoked more than once in the same process (e.g. when syft is embedded as a library). The clio initializer calls internal/redact.Set on every command execution, but the redact store is process-global and Set panics when a store already exists, so the second invocation dies with "replace existing redaction store (probably unintentional)". Add redact.Reset() to clear the previous run's store and call it in the initializer before Set. The double-Set guard in Set is kept: an unexpected second Set within a single run still panics. Add TestAppClioSetupConfigInitializerCanRunMultipleTimes which runs the initializer twice; it panics with the exact reported message on the old code and passes with the fix. Fixes #2285 Signed-off-by: JasonMetal <935216773@qq.com> * fix(redact): release the redact store at the end of each run Clear the global redact store in the post-run hook instead of resetting it before every `Set`. The double-`Set` panic keeps its meaning: a store is only present while a run is in flight, so a second run starting mid-run still panics rather than splitting secrets across two stores and leaking the ones in the dropped store. Sequential runs in the same process work since the previous run releases its store on the way out. A run only releases the store it set, so it never clears a store owned by someone else. Signed-off-by: Alex Goodman --------- Signed-off-by: JasonMetal <935216773@qq.com> Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- cmd/syft/cli/cli_test.go | 25 ++++++++++++++++ cmd/syft/internal/clio_setup_config.go | 10 ++++++- cmd/syft/internal/clio_setup_config_test.go | 32 +++++++++++++++++++++ internal/redact/redact.go | 8 ++++++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 cmd/syft/cli/cli_test.go create mode 100644 cmd/syft/internal/clio_setup_config_test.go diff --git a/cmd/syft/cli/cli_test.go b/cmd/syft/cli/cli_test.go new file mode 100644 index 000000000..f74b70d16 --- /dev/null +++ b/cmd/syft/cli/cli_test.go @@ -0,0 +1,25 @@ +package cli + +import ( + "io" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/anchore/clio" + "github.com/anchore/syft/internal/redact" +) + +func TestCommandCanRunMultipleTimes(t *testing.T) { + // https://github.com/anchore/syft/issues/2285 + t.Cleanup(redact.Reset) + + for i := 0; i < 2; i++ { + _, cmd := create(clio.Identification{Name: "syft"}, io.Discard) + cmd.SetArgs([]string{"cataloger", "list", "-o", "json"}) + require.NoError(t, cmd.Execute()) + + // the store is released at the end of each run so the next one can set its own + require.Nil(t, redact.Get()) + } +} diff --git a/cmd/syft/internal/clio_setup_config.go b/cmd/syft/internal/clio_setup_config.go index f2a665d12..8a9dacc4c 100644 --- a/cmd/syft/internal/clio_setup_config.go +++ b/cmd/syft/internal/clio_setup_config.go @@ -41,6 +41,8 @@ func AppClioSetupConfig(id clio.Identification, out io.Writer) *clio.SetupConfig stereoscope.SetBus(state.Bus) bus.Set(state.Bus) + // the redact store is process-global and only cleared when a run ends (see the post-run below), so + // setting it while another run is still in flight panics rather than splitting secrets across stores. redact.Set(state.RedactStore) log.Set(state.Logger) @@ -48,8 +50,14 @@ func AppClioSetupConfig(id clio.Identification, out io.Writer) *clio.SetupConfig return nil }, ). - WithPostRuns(func(_ *clio.State, _ error) { + WithPostRuns(func(state *clio.State, _ error) { stereoscope.Cleanup() //nolint:staticcheck // we don't have access to the image object here + + // release the redact store now that nothing in this run can report anymore, so a later run in the same + // process (e.g. syft embedded as a library) can set its own. Only release the store this run set. + if redact.Get() == state.RedactStore { + redact.Reset() + } }) return clioCfg } diff --git a/cmd/syft/internal/clio_setup_config_test.go b/cmd/syft/internal/clio_setup_config_test.go new file mode 100644 index 000000000..722732446 --- /dev/null +++ b/cmd/syft/internal/clio_setup_config_test.go @@ -0,0 +1,32 @@ +package internal + +import ( + "io" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/anchore/clio" + "github.com/anchore/go-logger/adapter/discard" + gologgerredact "github.com/anchore/go-logger/adapter/redact" + "github.com/anchore/syft/internal/redact" +) + +func TestAppClioSetupConfigInitializerPanicsWhileRunInFlight(t *testing.T) { + t.Cleanup(redact.Reset) + + cfg := AppClioSetupConfig(clio.Identification{Name: "syft"}, io.Discard) + require.Len(t, cfg.Initializers, 1) + + newState := func() *clio.State { + return &clio.State{Logger: discard.New(), RedactStore: gologgerredact.NewStore()} + } + + require.NoError(t, cfg.Initializers[0](newState())) + + // a second run starting before the first has finished must not silently replace the store, otherwise + // secrets added to the first store would no longer be redacted + require.PanicsWithValue(t, "replace existing redaction store (probably unintentional)", func() { + _ = cfg.Initializers[0](newState()) + }) +} diff --git a/internal/redact/redact.go b/internal/redact/redact.go index 3bb76e269..57ad1c897 100644 --- a/internal/redact/redact.go +++ b/internal/redact/redact.go @@ -13,6 +13,14 @@ func Set(s redact.Store) { store = s } +// Reset clears the global redaction store. It must only be called once a command +// run has fully finished (nothing left to Add or Apply), so that a later run can +// Set its own store. Calling it while a run is in flight would let a second Set +// split secrets across two stores and leak the ones in the dropped store. +func Reset() { + store = nil +} + func Get() redact.Store { return store } From 546c872c9d942216d9ddc0da4909cb06fcac356c Mon Sep 17 00:00:00 2001 From: Silas Parker Date: Wed, 7 Oct 2026 21:41:13 +0100 Subject: [PATCH 4/4] Add support for scanning IPK packages using the deb-archive-cataloger (#5111) * Add support for scanning IPK packages using the deb-archive-cataloger Signed-off-by: Silas Parker * fix(debian): share License field parsing between opkg status and archive control files - the control-file `License:` fallback now goes through the same helper as the opkg status DB path, so a package reports the same licenses whether found installed or as an archive - bitbake `&` / `|` are rewritten to SPDX `AND` / `OR` and the whole value is validated as one expression instead of split by hand, which kept grouping like `(A & B) | C` intact - non-SPDX values fall back to a deduped list of names with operators and parens dropped (previously the status DB path emitted `&`, `(BSD`, etc as licenses) - fields over 4KB are ignored, bounding work per archive and keeping deep paren nesting away from the recursive expression parser Signed-off-by: Alex Goodman * fix(debian): split Yocto recipe filenames in the Source field Yocto writes the bitbake recipe filename (`Source: zlib_1.3.2.bb`) where dpkg expects a source package name. This is now split into `Source: zlib` / `SourceVersion: 1.3.2` for both ipk archives and opkg status DB entries, instead of surfacing the `.bb` filename as the upstream package. Signed-off-by: Alex Goodman * fix(debian): do not label ipk archives as Debian packages - `.ipk` archives no longer get a `pkg:deb` PURL from the assumed debian distro; opkg-based distros (Yocto, OpenWrt) have no purl type, so these now match packages found in an opkg status DB, which already get no PURL - docs now cover `.ipk` and note that only ar-framed ipks are supported (OpenWrt's older gzipped-tar framing is rejected as an invalid archive) Signed-off-by: Alex Goodman * test(debian): assemble the ipk fixture at build time instead of committing it The fixture Dockerfile now builds a Yocto-shaped `.ipk` from a checked-in `control/` directory (copied from the original poky-tiny build) using `tar`, `zstd` and binutils `ar`, the same way bitbake's `package_ipk` packs one. This drops the prebuilt binary and the 20-30 minute Yocto `Dockerfile.build` while keeping the same archive layout and control metadata. Signed-off-by: Alex Goodman --------- Signed-off-by: Silas Parker Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- syft/pkg/cataloger/debian/capabilities.yaml | 1 + syft/pkg/cataloger/debian/cataloger.go | 4 +- syft/pkg/cataloger/debian/cataloger_test.go | 30 ++++++++ syft/pkg/cataloger/debian/package.go | 76 +++++++++++++++---- syft/pkg/cataloger/debian/package_test.go | 31 ++++++++ .../pkg/cataloger/debian/parse_deb_archive.go | 61 +++++++++++---- .../debian/parse_deb_archive_test.go | 35 +++++++-- syft/pkg/cataloger/debian/parse_dpkg_db.go | 12 ++- .../cataloger/debian/parse_dpkg_db_test.go | 10 +++ .../testdata/image-single-ipk/Dockerfile | 21 +++++ .../testdata/image-single-ipk/control/control | 15 ++++ .../image-single-ipk/control/postinst | 5 ++ 12 files changed, 262 insertions(+), 39 deletions(-) create mode 100644 syft/pkg/cataloger/debian/testdata/image-single-ipk/Dockerfile create mode 100644 syft/pkg/cataloger/debian/testdata/image-single-ipk/control/control create mode 100755 syft/pkg/cataloger/debian/testdata/image-single-ipk/control/postinst diff --git a/syft/pkg/cataloger/debian/capabilities.yaml b/syft/pkg/cataloger/debian/capabilities.yaml index 6d1f21e99..fee11b8dd 100644 --- a/syft/pkg/cataloger/debian/capabilities.yaml +++ b/syft/pkg/cataloger/debian/capabilities.yaml @@ -75,6 +75,7 @@ catalogers: method: glob # AUTO-GENERATED criteria: # AUTO-GENERATED - '**/*.deb' + - '**/*.ipk' metadata_types: # AUTO-GENERATED - pkg.DpkgArchiveEntry package_types: # AUTO-GENERATED diff --git a/syft/pkg/cataloger/debian/cataloger.go b/syft/pkg/cataloger/debian/cataloger.go index 183b9d95c..0bdd6d066 100644 --- a/syft/pkg/cataloger/debian/cataloger.go +++ b/syft/pkg/cataloger/debian/cataloger.go @@ -18,8 +18,8 @@ func NewDBCataloger() pkg.Cataloger { WithProcessors(dependency.Processor(dbEntryDependencySpecifier)) } -// NewArchiveCataloger returns a new Debian package cataloger object capable of parsing .deb archive files +// NewArchiveCataloger returns a new Debian package cataloger object capable of parsing .deb and .ipk (opkg) archive files func NewArchiveCataloger() pkg.Cataloger { return generic.NewCataloger("deb-archive-cataloger"). - WithParserByGlobs(parseDebArchive, "**/*.deb") + WithParserByGlobs(parseDebArchive, "**/*.deb", "**/*.ipk") } diff --git a/syft/pkg/cataloger/debian/cataloger_test.go b/syft/pkg/cataloger/debian/cataloger_test.go index 8d38c7b33..1ed9e6d95 100644 --- a/syft/pkg/cataloger/debian/cataloger_test.go +++ b/syft/pkg/cataloger/debian/cataloger_test.go @@ -276,6 +276,36 @@ func TestDpkgArchiveCataloger(t *testing.T) { }, }, }, + }, { + name: "image-single-ipk", + expected: []pkg.Package{ + { + Name: "libz1", + Version: "1.3.2-r0", + FoundBy: "deb-archive-cataloger", + Locations: file.NewLocationSet( + file.NewLocation("/libz1_1.3.2-r0_x86-64-v3.ipk"), + ), + Type: pkg.DebPkg, + Metadata: pkg.DpkgArchiveEntry{ + Package: "libz1", + Source: "zlib", + SourceVersion: "1.3.2", + Version: "1.3.2-r0", + Architecture: "x86-64-v3", + Maintainer: "Poky Maintainers ", + InstalledSize: 0, + Description: `Zlib Compression Library + Zlib is a general-purpose, patent-free, lossless data compression library + which is used by many different programs.`, + Provides: []string{"zlib"}, + Depends: []string{"musl (>= 1.2.6+git0+9fa28ece75)"}, + }, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocationsWithContext(ctx, "Zlib"), + ), + }, + }, }, } diff --git a/syft/pkg/cataloger/debian/package.go b/syft/pkg/cataloger/debian/package.go index 8ad569f0f..c21dc6452 100644 --- a/syft/pkg/cataloger/debian/package.go +++ b/syft/pkg/cataloger/debian/package.go @@ -9,6 +9,8 @@ import ( "sort" "strings" + "github.com/scylladb/go-set/strset" + "github.com/anchore/packageurl-go" "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/log" @@ -61,32 +63,65 @@ func newDpkgPackage(ctx context.Context, d dpkgExtractedMetadata, dbLocation fil return p } +// a declared License field holds a single expression, a few hundred bytes at most in practice. Anything +// much larger only feeds the expression parser (which recurses on parentheses) attacker-sized input. +const maxDeclaredLicenseLen = 4096 + +var ( + // opkg/ipkg (Yocto via bitbake) declare licenses with "&" for AND and "|" for OR + bitbakeLicenseOperators = strings.NewReplacer("&", " AND ", "|", " OR ") + tightenParens = strings.NewReplacer("( ", "(", " )", ")") + licenseListSeparators = strings.NewReplacer("(", " ", ")", " ") +) + // extractDeclaredLicenses converts a License field from the status DB into a license set. Returns nil // for empty input so standard dpkg entries (which never declare License inline) incur no allocation. -// Mirrors the alpine cataloger's approach: keep the value whole if it parses as a valid SPDX expression, -// otherwise split on whitespace to handle space-separated lists. func extractDeclaredLicenses(ctx context.Context, raw string, dbLocation file.Location) []pkg.License { + values := declaredLicenseValues(raw) + if len(values) == 0 { + return nil + } + return pkg.NewLicensesFromLocationWithContext(ctx, dbLocation, values...) +} + +// declaredLicenseValues converts an inline License field (opkg status DB or an archive control file) into +// license values. Bitbake operators are rewritten to SPDX ones and the value is kept whole if it then parses +// as a valid SPDX expression (mirroring the alpine cataloger), otherwise it is treated as a list of license +// names with operators and grouping dropped. +func declaredLicenseValues(raw string) []string { + raw = strings.TrimSpace(raw) if raw == "" { return nil } - licenseStrings := []string{raw} - if _, err := license.ParseExpression(raw); err != nil { - licenseStrings = strings.Fields(raw) + if len(raw) > maxDeclaredLicenseLen { + log.WithFields("length", len(raw)).Debug("ignoring oversized declared license field") + return nil } - return pkg.NewLicensesFromLocationWithContext(ctx, dbLocation, licenseStrings...) + + expr := tightenParens.Replace(strings.Join(strings.Fields(bitbakeLicenseOperators.Replace(raw)), " ")) + if _, err := license.ParseExpression(expr); err == nil { + return []string{expr} + } + + var values []string + seen := strset.New() + for _, field := range strings.Fields(licenseListSeparators.Replace(expr)) { + if field == "AND" || field == "OR" || seen.Has(field) { + continue + } + seen.Add(field) + values = append(values, field) + } + return values } func newDebArchivePackage(ctx context.Context, location file.Location, metadata pkg.DpkgArchiveEntry, licenseStrings []string) pkg.Package { p := pkg.Package{ - Name: metadata.Package, - Version: metadata.Version, - Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, licenseStrings...)...), - Type: pkg.DebPkg, - PURL: packageURL( - pkg.DpkgDBEntry(metadata), - // we don't know the distro information, but since this is a deb file then we can reasonably assume it is a debian-based distro - &linux.Release{IDLike: []string{"debian"}}, - ), + Name: metadata.Package, + Version: metadata.Version, + Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, licenseStrings...)...), + Type: pkg.DebPkg, + PURL: packageURL(pkg.DpkgDBEntry(metadata), archiveDistro(location)), Metadata: metadata, Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), } @@ -95,6 +130,17 @@ func newDebArchivePackage(ctx context.Context, location file.Location, metadata return p } +// archiveDistro guesses the distro an archive was built for. We don't know the distro information, but a .deb +// can reasonably be assumed to target a debian-based distro. An .ipk targets opkg-based distros (Yocto, +// OpenWrt) which have no purl type, so it gets no distro and therefore no PURL, the same as packages found in +// an opkg status DB. +func archiveDistro(location file.Location) *linux.Release { + if strings.EqualFold(path.Ext(location.Path()), ".ipk") { + return nil + } + return &linux.Release{IDLike: []string{"debian"}} +} + // PackageURL returns the PURL for the specific Debian package (see https://github.com/package-url/purl-spec) func packageURL(m pkg.DpkgDBEntry, distro *linux.Release) string { if distro == nil { diff --git a/syft/pkg/cataloger/debian/package_test.go b/syft/pkg/cataloger/debian/package_test.go index 227ebc9d5..b3a8a14da 100644 --- a/syft/pkg/cataloger/debian/package_test.go +++ b/syft/pkg/cataloger/debian/package_test.go @@ -3,6 +3,7 @@ package debian import ( "context" "sort" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -145,6 +146,36 @@ func Test_extractDeclaredLicenses(t *testing.T) { raw: "GPL-2.0 BSD-3-Clause", expected: []string{"GPL-2.0", "BSD-3-Clause"}, }, + { + name: "bitbake operators become an SPDX expression", + raw: "MIT & (BSD-3-Clause | Apache-2.0) & ( GPL-2.0-or-later )", + expected: []string{"MIT AND (BSD-3-Clause OR Apache-2.0) AND (GPL-2.0-or-later)"}, + }, + { + name: "bitbake grouping is preserved rather than split", + raw: "(MIT & BSD-3-Clause) | Apache-2.0", + expected: []string{"(MIT AND BSD-3-Clause) OR Apache-2.0"}, + }, + { + name: "parenthesized alternatives are preserved", + raw: "(MIT) | (Apache-2.0)", + expected: []string{"(MIT) OR (Apache-2.0)"}, + }, + { + name: "non-SPDX names drop operators and grouping", + raw: "GPLv2+ & (LGPLv2.1 | BSD) & GPLv2+", + expected: []string{"GPLv2+", "LGPLv2.1", "BSD"}, + }, + { + name: "oversized field is ignored", + raw: strings.Repeat("MIT & ", maxDeclaredLicenseLen), + expected: nil, + }, + { + name: "deeply nested parens are ignored", + raw: strings.Repeat("(", maxDeclaredLicenseLen) + "MIT" + strings.Repeat(")", maxDeclaredLicenseLen), + expected: nil, + }, } for _, test := range tests { diff --git a/syft/pkg/cataloger/debian/parse_deb_archive.go b/syft/pkg/cataloger/debian/parse_deb_archive.go index 4036e93d4..344a211e8 100644 --- a/syft/pkg/cataloger/debian/parse_deb_archive.go +++ b/syft/pkg/cataloger/debian/parse_deb_archive.go @@ -2,6 +2,7 @@ package debian import ( "archive/tar" + "bytes" "context" "errors" "fmt" @@ -23,18 +24,27 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/generic" ) -// parseDebArchive parses a Debian package archive (.deb) file and returns the packages it contains. -// A .deb file is an ar archive containing three main files: -// - debian-binary: Version of the .deb format (usually "2.0") +// parseDebArchive parses a Debian package archive (.deb) or opkg package (.ipk) file and returns the packages it +// contains. Both are ar archives containing three main files: +// - debian-binary: Version of the archive format (usually "2.0") // - control.tar.gz/xz/zst: Contains package metadata (control file, md5sums, conffiles) // - data.tar.gz/xz/zst: Contains the actual files to be installed (not processed by this cataloger) // -// This function extracts and processes the control information to create package metadata. +// This function extracts and processes the control information to create package metadata. Licenses come +// from copyright files in the data tar, falling back to the control file's License field (common for .ipk). +// Only ar-framed .ipk files are supported; the older gzipped-tar framing (e.g. OpenWrt's ipkg-build) is +// rejected as an invalid archive. func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { - arReader := ar.NewReader(reader) + validatedReader, err := newValidatedArReader(reader) + if err != nil { + return nil, nil, fmt.Errorf("invalid ar archive: %w", err) + } + + arReader := ar.NewReader(validatedReader) var metadata *pkg.DpkgArchiveEntry var licenses []string + var ctrlLicenses []string var unknownErr error var sawControl, sawData bool for { @@ -56,7 +66,7 @@ func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen if err != nil { return nil, nil, unknown.New(reader.Location, fmt.Errorf("failed to decompress control.tar.* file: %w", err)) } - metadata, err = processControlTar(dcReader) + metadata, ctrlLicenses, err = processControlTar(dcReader) switch { case err != nil && metadata == nil: return nil, nil, unknown.New(reader.Location, fmt.Errorf("failed to process control.tar.* file: %w", err)) @@ -87,6 +97,10 @@ func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen return nil, nil, unknown.New(reader.Location, fmt.Errorf("no application found described in .dpkg archive")) } + if len(licenses) == 0 && len(ctrlLicenses) > 0 { + licenses = ctrlLicenses + } + // a partial parse still yields a usable package, so report what went wrong alongside it rather than // dropping either one return []pkg.Package{ @@ -128,10 +142,10 @@ func processDataTar(dcReader io.ReadCloser) ([]string, error) { } // processControlTar always returns whatever metadata it managed to parse, even alongside a non-nil -// error. A non-nil error with non-nil metadata means the package is usable but incomplete (e.g. a -// clipped file listing); nil metadata means nothing usable was found and the caller should treat it -// as fatal. -func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) { +// error, along with any licenses declared in the control file's License field. A non-nil error with +// non-nil metadata means the package is usable but incomplete (e.g. a clipped file listing); nil +// metadata means nothing usable was found and the caller should treat it as fatal. +func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, []string, error) { defer internal.CloseAndLogError(dcReader, "") tarReader := tar.NewReader(dcReader) @@ -140,6 +154,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) { var files []pkg.DpkgFileRecord var confFileRecords []pkg.DpkgFileRecord var listingErr error + var licenses []string for { header, err := tarReader.Next() @@ -147,7 +162,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) { break } if err != nil { - return metadata, fmt.Errorf("failed to read control tar: %w", err) + return metadata, nil, fmt.Errorf("failed to read control tar: %w", err) } switch filepath.Base(header.Name) { @@ -155,12 +170,13 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) { // parseDpkgStatus already streams via bufio.Reader entries, err := parseDpkgStatus(tarReader) if err != nil { - return nil, fmt.Errorf("failed to parse control file: %w", err) + return nil, nil, fmt.Errorf("failed to parse control file: %w", err) } if len(entries) == 0 { - return nil, fmt.Errorf("no package entries found in control file") + return nil, nil, fmt.Errorf("no package entries found in control file") } entry := pkg.DpkgArchiveEntry(entries[0].toDpkgEntry()) + licenses = declaredLicenseValues(entries[0].License) metadata = &entry case "md5sums": // parseDpkgMD5Info streams via bufio.Scanner and reports its own clipping/scan errors @@ -176,7 +192,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) { } if metadata == nil { - return nil, fmt.Errorf("control file not found in archive") + return nil, nil, fmt.Errorf("control file not found in archive") } if len(confFileRecords) > 0 && len(files) > 0 { @@ -195,7 +211,22 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) { // a clipped or unreadable file listing leaves the package usable, so hand both back and let the // caller decide - return metadata, listingErr + return metadata, licenses, listingErr +} + +// newValidatedArReader checks for the ar global header before handing the stream to the ar reader, which +// otherwise skips those bytes unchecked and reads garbage headers from non-ar input. +func newValidatedArReader(reader io.ReadCloser) (io.ReadCloser, error) { + prefix := make([]byte, len(ar.GLOBAL_HEADER)) + if _, err := io.ReadFull(reader, prefix); err != nil { + return nil, fmt.Errorf("failed to read ar header: %w", err) + } + + if !bytes.Equal(prefix, []byte(ar.GLOBAL_HEADER)) { + return nil, fmt.Errorf("expected ar header %q, got %q", string(ar.GLOBAL_HEADER), string(prefix)) + } + + return io.NopCloser(io.MultiReader(bytes.NewReader(prefix), reader)), nil } // a .deb's member sizes are bounded by the archive, but what they decompress to is not: xz and zstd diff --git a/syft/pkg/cataloger/debian/parse_deb_archive_test.go b/syft/pkg/cataloger/debian/parse_deb_archive_test.go index 8b916a476..f3d4b0506 100644 --- a/syft/pkg/cataloger/debian/parse_deb_archive_test.go +++ b/syft/pkg/cataloger/debian/parse_deb_archive_test.go @@ -26,12 +26,13 @@ import ( func TestProcessControlTar(t *testing.T) { tarBytes := createTestTarWithControlFiles(t) - metadata, err := processControlTar(io.NopCloser(bytes.NewReader(tarBytes))) + metadata, licenses, err := processControlTar(io.NopCloser(bytes.NewReader(tarBytes))) require.NoError(t, err) require.NotNil(t, metadata) assert.Equal(t, "test-package", metadata.Package) + assert.Equal(t, []string{"MIT"}, licenses) assert.Equal(t, "1.0.0", metadata.Version) // md5sums should have been parsed into file records @@ -43,6 +44,14 @@ func TestProcessControlTar(t *testing.T) { assert.True(t, metadata.Files[0].IsConfigFile, "file listed in conffiles should be marked as config") } +func TestParseDebArchive_RejectsInvalidArchiveHeader(t *testing.T) { + reader := file.NewLocationReadCloser(file.NewLocation("test.deb"), io.NopCloser(bytes.NewReader([]byte("not-an-ar-archive")))) + + _, _, err := parseDebArchive(context.Background(), nil, nil, reader) + require.Error(t, err) + assert.Contains(t, err.Error(), "ar archive") +} + func TestProcessControlTar_ConfigFileMarking(t *testing.T) { // Create a tar where conffiles lists paths that overlap with md5sums entries var buf bytes.Buffer @@ -61,9 +70,10 @@ func TestProcessControlTar_ConfigFileMarking(t *testing.T) { require.NoError(t, tw.Close()) - metadata, err := processControlTar(io.NopCloser(bytes.NewReader(buf.Bytes()))) + metadata, licenses, err := processControlTar(io.NopCloser(bytes.NewReader(buf.Bytes()))) require.NoError(t, err) require.Len(t, metadata.Files, 3) + assert.Empty(t, licenses) assert.True(t, metadata.Files[0].IsConfigFile, "first file should be marked as config file") assert.True(t, metadata.Files[1].IsConfigFile, "second file should be marked as config file") @@ -75,7 +85,7 @@ func createTestTarWithControlFiles(t *testing.T) []byte { var buf bytes.Buffer tw := tar.NewWriter(&buf) - controlContent := "Package: test-package\nVersion: 1.0.0\nArchitecture: all\nMaintainer: Test \nDescription: Test package\n" + controlContent := "Package: test-package\nVersion: 1.0.0\nArchitecture: all\nMaintainer: Test \nDescription: Test package\nLicense: MIT\n" writeTarEntry(t, tw, "control", controlContent) md5Content := "d41d8cd98f00b204e9800998ecf8427e usr/bin/test-command\n" @@ -165,7 +175,7 @@ func Test_processControlTar_rejectsABombedControlMember(t *testing.T) { writeTarEntry(t, tw, "md5sums", strings.Repeat("d41d8cd98f00b204e9800998ecf8427e usr/bin/x\n", 20000)) require.NoError(t, tw.Close()) - metadata, err := processControlTar(newBoundedReadCloser(io.NopCloser(bytes.NewReader(tarBuf.Bytes())), 4096)) + metadata, _, err := processControlTar(newBoundedReadCloser(io.NopCloser(bytes.NewReader(tarBuf.Bytes())), 4096)) require.ErrorIs(t, err, errDecompressedTooLarge) require.NotNil(t, metadata, "a control tar read failure after \"control\" was parsed must not drop the package") @@ -181,7 +191,7 @@ func Test_processControlTar_clippedListingIsUsableButReported(t *testing.T) { writeTarEntry(t, tw, "md5sums", strings.Repeat("d41d8cd98f00b204e9800998ecf8427e usr/bin/x\n", maxDpkgFileRecords+10)) require.NoError(t, tw.Close()) - metadata, err := processControlTar(io.NopCloser(bytes.NewReader(tarBuf.Bytes()))) + metadata, _, err := processControlTar(io.NopCloser(bytes.NewReader(tarBuf.Bytes()))) require.Error(t, err, "clipping must be reported") require.ErrorIs(t, err, errClippedFileListing) @@ -418,3 +428,18 @@ func Test_decompressionStream_failsEarlyInsteadOfBuffering(t *testing.T) { assert.Less(t, bounded, uint64(8*intFile.MB), "a bounded stream must stop at the cap, not buffer the whole member and check afterwards") } + +func TestProcessControlTar_DeclaredLicense(t *testing.T) { + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + + controlContent := "Package: test-package\nVersion: 1.0.0\nArchitecture: all\nLicense: MIT & (BSD-3-Clause | Apache-2.0) & ( GPL-2.0-or-later )\n" + writeTarEntry(t, tw, "control", controlContent) + + require.NoError(t, tw.Close()) + + metadata, licenses, err := processControlTar(io.NopCloser(bytes.NewReader(buf.Bytes()))) + require.NoError(t, err) + require.NotNil(t, metadata) + assert.Equal(t, []string{"MIT AND (BSD-3-Clause OR Apache-2.0) AND (GPL-2.0-or-later)"}, licenses) +} diff --git a/syft/pkg/cataloger/debian/parse_dpkg_db.go b/syft/pkg/cataloger/debian/parse_dpkg_db.go index daea1b541..0168e5fb5 100644 --- a/syft/pkg/cataloger/debian/parse_dpkg_db.go +++ b/syft/pkg/cataloger/debian/parse_dpkg_db.go @@ -183,9 +183,11 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (*dpkgExtractedMetadata, error) } sourceName, sourceVersion := extractSourceVersion(raw.Source) + if sourceName != "" { + raw.Source = sourceName + } if sourceVersion != "" { raw.SourceVersion = sourceVersion - raw.Source = sourceName } if raw.Package == "" { @@ -263,8 +265,14 @@ func extractAllFields(reader *bufio.Reader) (map[string]any, error) { } // If the source entry string is of the form " ()" then parse and return the components, if -// of the "" form, then return name and nil +// of the "" form, then return name and nil. Yocto (opkg/ipk) instead writes the bitbake recipe filename +// ("_.bb"), which is split the same way. func extractSourceVersion(source string) (string, string) { + if recipe, ok := strings.CutSuffix(source, ".bb"); ok && !strings.ContainsAny(recipe, " (") { + // bitbake reserves "_" as the recipe name / version separator + name, version, _ := strings.Cut(recipe, "_") + return name, version + } // special handling for the Source field since it has formatted data match := internal.MatchNamedCaptureGroups(sourceRegexp, source) return match["name"], match["version"] diff --git a/syft/pkg/cataloger/debian/parse_dpkg_db_test.go b/syft/pkg/cataloger/debian/parse_dpkg_db_test.go index e423c0a08..51b9d13cc 100644 --- a/syft/pkg/cataloger/debian/parse_dpkg_db_test.go +++ b/syft/pkg/cataloger/debian/parse_dpkg_db_test.go @@ -366,6 +366,16 @@ func TestSourceVersionExtract(t *testing.T) { input: "", expected: []string{"", ""}, }, + { + name: "bitbake recipe with version", + input: "zlib_1.3.2.bb", + expected: []string{"zlib", "1.3.2"}, + }, + { + name: "bitbake recipe without version", + input: "packagegroup-core-boot.bb", + expected: []string{"packagegroup-core-boot", ""}, + }, } for _, test := range tests { diff --git a/syft/pkg/cataloger/debian/testdata/image-single-ipk/Dockerfile b/syft/pkg/cataloger/debian/testdata/image-single-ipk/Dockerfile new file mode 100644 index 000000000..64228f6a0 --- /dev/null +++ b/syft/pkg/cataloger/debian/testdata/image-single-ipk/Dockerfile @@ -0,0 +1,21 @@ +# assembles a Yocto-style .ipk the same way bitbake's package_ipk does (opkg-build: a GNU ar archive of +# debian-binary, control.tar.gz and data.tar.zst) without paying for a full Yocto build. control/ is copied +# verbatim from libz1_1.3.2-r0_x86-64-v3.ipk built by poky-tiny (wrynose); the library itself is a stub +# since only the archive shape and control metadata matter here. +FROM alpine:3.18.4@sha256:48d9183eb12a05c99bcc0bf44a003607b8e941e1d4f41f9ad12bdcc4b5672f86 AS build + +RUN apk add --no-cache binutils zstd + +COPY control /pkg/control + +RUN mkdir -p /pkg/data/usr/lib \ + && echo stub > /pkg/data/usr/lib/libz.so.1.3.2 \ + && ln -s libz.so.1.3.2 /pkg/data/usr/lib/libz.so.1 \ + && cd /pkg \ + && echo 2.0 > debian-binary \ + && tar -C control -czf control.tar.gz . \ + && tar -C data -cf - . | zstd -q -o data.tar.zst \ + && ar rcD /libz1_1.3.2-r0_x86-64-v3.ipk debian-binary control.tar.gz data.tar.zst + +FROM scratch +COPY --from=build /libz1_1.3.2-r0_x86-64-v3.ipk / diff --git a/syft/pkg/cataloger/debian/testdata/image-single-ipk/control/control b/syft/pkg/cataloger/debian/testdata/image-single-ipk/control/control new file mode 100644 index 000000000..615e55b1e --- /dev/null +++ b/syft/pkg/cataloger/debian/testdata/image-single-ipk/control/control @@ -0,0 +1,15 @@ +Package: libz1 +Version: 1.3.2-r0 +Description: Zlib Compression Library + Zlib is a general-purpose, patent-free, lossless data compression library + which is used by many different programs. +Section: libs +Priority: optional +Maintainer: Poky Maintainers +License: Zlib +Architecture: x86-64-v3 +OE: zlib +Homepage: http://zlib.net/ +Depends: musl (>= 1.2.6+git0+9fa28ece75) +Provides: zlib +Source: zlib_1.3.2.bb diff --git a/syft/pkg/cataloger/debian/testdata/image-single-ipk/control/postinst b/syft/pkg/cataloger/debian/testdata/image-single-ipk/control/postinst new file mode 100755 index 000000000..ccf99782e --- /dev/null +++ b/syft/pkg/cataloger/debian/testdata/image-single-ipk/control/postinst @@ -0,0 +1,5 @@ +#!/bin/sh +set -e +if [ x"$D" = "x" ]; then + if [ -x /sbin/ldconfig ]; then /sbin/ldconfig ; fi +fi