Compare commits

...
12 Commits
Author SHA1 Message Date
dependabot[bot] 33419c8f39 chore(deps): bump github.com/pb33f/ordered-map/v2 from 2.3.1 to 2.3.2 (#5400)
Bumps [github.com/pb33f/ordered-map/v2](https://github.com/pb33f/ordered-map) from 2.3.1 to 2.3.2.
- [Changelog](https://github.com/pb33f/ordered-map/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pb33f/ordered-map/compare/v2.3.1...v2.3.2)

---
updated-dependencies:
- dependency-name: github.com/pb33f/ordered-map/v2
  dependency-version: 2.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-09 13:42:52 +00:00
dependabot[bot] 6790272518 chore(deps): bump github.com/anchore/go-make in /.make (#5402)
Bumps [github.com/anchore/go-make](https://github.com/anchore/go-make) from 0.8.1 to 0.9.1.
- [Release notes](https://github.com/anchore/go-make/releases)
- [Commits](https://github.com/anchore/go-make/compare/v0.8.1...v0.9.1)

---
updated-dependencies:
- dependency-name: github.com/anchore/go-make
  dependency-version: 0.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-09 13:37:22 +00:00
Peter McConnell 740beb6dc4 fix: search for the .NET bundle marker without holding the file (#5393)
Signed-off-by: Peter McConnell <peter.mcconnell@upwind.io>
2026-10-09 09:36:52 -04:00
dependabot[bot] 3e41abadc4 chore(deps): bump github.com/go-git/go-billy/v5 from 5.9.1 to 5.9.2 (#5399)
Bumps [github.com/go-git/go-billy/v5](https://github.com/go-git/go-billy) from 5.9.1 to 5.9.2.
- [Release notes](https://github.com/go-git/go-billy/releases)
- [Commits](https://github.com/go-git/go-billy/compare/v5.9.1...v5.9.2)

---
updated-dependencies:
- dependency-name: github.com/go-git/go-billy/v5
  dependency-version: 5.9.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-09 13:35:51 +00:00
dependabot[bot] 0a050b0e27 chore(deps): bump github.com/gpustack/gguf-parser-go (#5401)
Bumps [github.com/gpustack/gguf-parser-go](https://github.com/gpustack/gguf-parser-go) from 0.26.3 to 0.26.4.
- [Release notes](https://github.com/gpustack/gguf-parser-go/releases)
- [Commits](https://github.com/gpustack/gguf-parser-go/compare/v0.26.3...v0.26.4)

---
updated-dependencies:
- dependency-name: github.com/gpustack/gguf-parser-go
  dependency-version: 0.26.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-09 13:35:03 +00:00
dependabot[bot] fc8bd6c9d8 chore(deps): bump anchore/sbom-action from 0.24.2 to 0.24.3 (#5395)
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.24.2 to 0.24.3.
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](https://github.com/anchore/sbom-action/compare/3ad7283483fc7af8ff2b4ea19663c2d5ca935e26...66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-version: 0.24.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-09 13:31:27 +00:00
dependabot[bot] 7e713d106e chore(deps): bump modernc.org/sqlite from 1.59.0 to 1.60.1 (#5398)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.59.0 to 1.60.1.
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.59.0...v1.60.1)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.60.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-09 13:29:55 +00:00
dependabot[bot] 1f5ac47990 chore(deps): bump anchore/go-make/.github/actions/setup (#5397)
Bumps [anchore/go-make/.github/actions/setup](https://github.com/anchore/go-make) from 0.8.1 to 0.9.1.
- [Release notes](https://github.com/anchore/go-make/releases)
- [Commits](https://github.com/anchore/go-make/compare/fa5421b3bf24c9d18f11a736c5eb4172f2b7d683...27a9f35f41b5fe86d68815844efd2ba816e9ba6b)

---
updated-dependencies:
- dependency-name: anchore/go-make/.github/actions/setup
  dependency-version: 0.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-09 13:29:42 +00:00
dependabot[bot] 28e38c29f3 chore(deps): bump anchore/go-make/.github/actions/setup (#5396)
Bumps [anchore/go-make/.github/actions/setup](https://github.com/anchore/go-make) from 0.8.1 to 0.9.1.
- [Release notes](https://github.com/anchore/go-make/releases)
- [Commits](https://github.com/anchore/go-make/compare/fa5421b3bf24c9d18f11a736c5eb4172f2b7d683...27a9f35f41b5fe86d68815844efd2ba816e9ba6b)

---
updated-dependencies:
- dependency-name: anchore/go-make/.github/actions/setup
  dependency-version: 0.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-09 13:27:40 +00:00
Silas ParkerandAlex Goodman 546c872c9d 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 <silas@srp.me.uk>

* 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 <wagoodman@users.noreply.github.com>

* 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 <wagoodman@users.noreply.github.com>

* 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 <wagoodman@users.noreply.github.com>

* 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 <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Silas Parker <silas@srp.me.uk>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-10-07 20:41:13 +00:00
MetalandAlex Goodman b0a8de7bf6 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 <wagoodman@users.noreply.github.com>

---------

Signed-off-by: JasonMetal <935216773@qq.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-10-07 20:28:15 +00:00
anchore-oss-update-botandanchore-oss-update-bot d9489c2230 chore(deps): update tool versions (#5381)
Signed-off-by: anchore-oss-update-bot <anchore-oss-update-bot@users.noreply.github.com>
Co-authored-by: anchore-oss-update-bot <anchore-oss-update-bot@users.noreply.github.com>
2026-10-07 10:27:20 +00:00
26 changed files with 585 additions and 102 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ tools:
# used to parse JSON/YAML annotations on the fixture cache image # used to parse JSON/YAML annotations on the fixture cache image
- name: yq - name: yq
version: version:
want: v4.53.6 want: v4.54.1
method: github-release method: github-release
with: with:
repo: mikefarah/yq repo: mikefarah/yq
+1 -1
View File
@@ -30,7 +30,7 @@ runs:
using: "composite" using: "composite"
steps: steps:
- name: Setup go + go-make tooling - name: Setup go + go-make tooling
uses: anchore/go-make/.github/actions/setup@fa5421b3bf24c9d18f11a736c5eb4172f2b7d683 # v0.8.1 uses: anchore/go-make/.github/actions/setup@27a9f35f41b5fe86d68815844efd2ba816e9ba6b # v0.9.1
with: with:
go-version: ${{ inputs.go-version }} go-version: ${{ inputs.go-version }}
cache-key-prefix: ${{ inputs.cache-key-prefix }} cache-key-prefix: ${{ inputs.cache-key-prefix }}
+1 -1
View File
@@ -118,7 +118,7 @@ jobs:
# for updating brew formula in anchore/homebrew-syft # for updating brew formula in anchore/homebrew-syft
GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }} GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}
- uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 #v0.24.2 - uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c #v0.24.3
continue-on-error: true continue-on-error: true
with: with:
file: go.mod file: go.mod
+3 -3
View File
@@ -99,7 +99,7 @@ jobs:
# go-make's setup only (not ./.github/actions/bootstrap): binny ships no windows release, so binny tools # go-make's setup only (not ./.github/actions/bootstrap): binny ships no windows release, so binny tools
# are unavailable here (`make unit` skips the fixture refresh on windows) # are unavailable here (`make unit` skips the fixture refresh on windows)
- name: Setup go + go-make tooling - name: Setup go + go-make tooling
uses: anchore/go-make/.github/actions/setup@fa5421b3bf24c9d18f11a736c5eb4172f2b7d683 # v0.8.1 uses: anchore/go-make/.github/actions/setup@27a9f35f41b5fe86d68815844efd2ba816e9ba6b # v0.9.1
with: with:
# keep in sync with the go-version default in ./.github/actions/bootstrap # keep in sync with the go-version default in ./.github/actions/bootstrap
go-version: "1.26.2" go-version: "1.26.2"
@@ -195,7 +195,7 @@ jobs:
# go-make's setup only (not ./.github/actions/bootstrap): the test only needs make + the snapshot binary # go-make's setup only (not ./.github/actions/bootstrap): the test only needs make + the snapshot binary
- name: Setup go + go-make tooling - name: Setup go + go-make tooling
uses: anchore/go-make/.github/actions/setup@fa5421b3bf24c9d18f11a736c5eb4172f2b7d683 # v0.8.1 uses: anchore/go-make/.github/actions/setup@27a9f35f41b5fe86d68815844efd2ba816e9ba6b # v0.9.1
with: with:
# keep in sync with the go-version default in ./.github/actions/bootstrap # keep in sync with the go-version default in ./.github/actions/bootstrap
go-version: "1.26.2" go-version: "1.26.2"
@@ -228,7 +228,7 @@ jobs:
# go-make's setup only (not ./.github/actions/bootstrap): the test only needs make + the snapshot binary # go-make's setup only (not ./.github/actions/bootstrap): the test only needs make + the snapshot binary
- name: Setup go + go-make tooling - name: Setup go + go-make tooling
uses: anchore/go-make/.github/actions/setup@fa5421b3bf24c9d18f11a736c5eb4172f2b7d683 # v0.8.1 uses: anchore/go-make/.github/actions/setup@27a9f35f41b5fe86d68815844efd2ba816e9ba6b # v0.9.1
with: with:
# keep in sync with the go-version default in ./.github/actions/bootstrap # keep in sync with the go-version default in ./.github/actions/bootstrap
go-version: "1.26.2" go-version: "1.26.2"
+4 -4
View File
@@ -1,12 +1,12 @@
module github.com/anchore/syft/.make module github.com/anchore/syft/.make
go 1.25.8 go 1.26.0
require github.com/anchore/go-make v0.8.1 require github.com/anchore/go-make v0.9.1
require ( require (
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect github.com/goccy/go-yaml v1.19.2 // indirect
golang.org/x/mod v0.40.0 // indirect golang.org/x/mod v0.41.0 // indirect
golang.org/x/sys v0.47.0 // indirect golang.org/x/sys v0.48.0 // indirect
) )
+6 -6
View File
@@ -1,10 +1,10 @@
github.com/anchore/go-make v0.8.1 h1:GAhPXR+9qVQhFB1pfVlHFOzxAt3dTDTUIA7Tp7nx9tg= github.com/anchore/go-make v0.9.1 h1:OnC830XJEhdHUPhR8jLpnjmLTco9UThNZfHpLefqd94=
github.com/anchore/go-make v0.8.1/go.mod h1:xALbVsPsDoJnJxdkjPlxOLZV7GbAdT5Pv3gd0kP1Lz0= github.com/anchore/go-make v0.9.1/go.mod h1:1KhqPt0STAHLVEngwrXxYsuFftD62Nx497rNujwo8IY=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+25
View File
@@ -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())
}
}
+9 -1
View File
@@ -41,6 +41,8 @@ func AppClioSetupConfig(id clio.Identification, out io.Writer) *clio.SetupConfig
stereoscope.SetBus(state.Bus) stereoscope.SetBus(state.Bus)
bus.Set(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) redact.Set(state.RedactStore)
log.Set(state.Logger) log.Set(state.Logger)
@@ -48,8 +50,14 @@ func AppClioSetupConfig(id clio.Identification, out io.Writer) *clio.SetupConfig
return nil 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 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 return clioCfg
} }
@@ -0,0 +1,32 @@
package internal
import (
"io"
"testing"
"github.com/stretchr/testify/require"
"github.com/anchore/clio"
"github.com/anchore/go-logger/adapter/discard"
gologgerredact "github.com/anchore/go-logger/adapter/redact"
"github.com/anchore/syft/internal/redact"
)
func TestAppClioSetupConfigInitializerPanicsWhileRunInFlight(t *testing.T) {
t.Cleanup(redact.Reset)
cfg := AppClioSetupConfig(clio.Identification{Name: "syft"}, io.Discard)
require.Len(t, cfg.Initializers, 1)
newState := func() *clio.State {
return &clio.State{Logger: discard.New(), RedactStore: gologgerredact.NewStore()}
}
require.NoError(t, cfg.Initializers[0](newState()))
// a second run starting before the first has finished must not silently replace the store, otherwise
// secrets added to the first store would no longer be redacted
require.PanicsWithValue(t, "replace existing redaction store (probably unintentional)", func() {
_ = cfg.Initializers[0](newState())
})
}
+6 -6
View File
@@ -41,7 +41,7 @@ require (
github.com/facebookincubator/nvdtools v0.1.5 github.com/facebookincubator/nvdtools v0.1.5
github.com/github/go-spdx/v2 v2.7.0 github.com/github/go-spdx/v2 v2.7.0
github.com/gkampitakis/go-snaps v0.5.23 github.com/gkampitakis/go-snaps v0.5.23
github.com/go-git/go-billy/v5 v5.9.1 github.com/go-git/go-billy/v5 v5.9.2
github.com/go-git/go-git/v5 v5.19.2 github.com/go-git/go-git/v5 v5.19.2
github.com/go-test/deep v1.1.1 github.com/go-test/deep v1.1.1
github.com/go-viper/mapstructure/v2 v2.5.0 github.com/go-viper/mapstructure/v2 v2.5.0
@@ -52,7 +52,7 @@ require (
github.com/google/licensecheck v0.3.1 github.com/google/licensecheck v0.3.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gookit/color v1.6.1 github.com/gookit/color v1.6.1
github.com/gpustack/gguf-parser-go v0.26.3 github.com/gpustack/gguf-parser-go v0.26.4
github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-getter v1.8.9 github.com/hashicorp/go-getter v1.8.9
github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-multierror v1.1.1
@@ -97,11 +97,11 @@ require (
golang.org/x/net v0.59.0 golang.org/x/net v0.59.0
golang.org/x/time v0.16.0 golang.org/x/time v0.16.0
golang.org/x/tools v0.50.0 golang.org/x/tools v0.50.0
modernc.org/sqlite v1.59.0 modernc.org/sqlite v1.60.1
) )
require ( require (
github.com/pb33f/ordered-map/v2 v2.3.1 github.com/pb33f/ordered-map/v2 v2.3.2
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd
howett.net/plist v1.0.1 howett.net/plist v1.0.1
) )
@@ -265,6 +265,7 @@ require (
github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opencontainers/runtime-spec v1.3.0 // indirect github.com/opencontainers/runtime-spec v1.3.0 // indirect
github.com/opencontainers/selinux v1.15.1 // indirect github.com/opencontainers/selinux v1.15.1 // indirect
github.com/pb33f/go-yaml v0.1.0 // indirect
github.com/pborman/indent v1.2.1 // indirect github.com/pborman/indent v1.2.1 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pierrec/lz4/v4 v4.1.26 // indirect
@@ -328,7 +329,6 @@ require (
go.opentelemetry.io/otel/trace v1.46.0 // indirect go.opentelemetry.io/otel/trace v1.46.0 // indirect
go.podman.io/image/v5 v5.41.2 // indirect go.podman.io/image/v5 v5.41.2 // indirect
go.podman.io/storage v1.64.1 // indirect go.podman.io/storage v1.64.1 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/crypto v0.57.0 // indirect golang.org/x/crypto v0.57.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect
@@ -345,7 +345,7 @@ require (
google.golang.org/grpc v1.83.2 // indirect google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.12 // indirect google.golang.org/protobuf v1.36.12 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
modernc.org/libc v1.75.7 // indirect modernc.org/libc v1.77.1 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.12.1 // indirect modernc.org/memory v1.12.1 // indirect
) )
+16 -16
View File
@@ -434,8 +434,8 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= 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 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.9.1 h1:8U73XiOTfINdItHVa6z4Gv7ToObcZ6grkqQbLryLCdA= github.com/go-git/go-billy/v5 v5.9.2 h1:OXFSRyz4g20upsGDJgQG9Bak1l/ZEv8GHVYB52O71sE=
github.com/go-git/go-billy/v5 v5.9.1/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0= github.com/go-git/go-billy/v5 v5.9.2/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 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-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY= github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
@@ -579,8 +579,8 @@ github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gpustack/gguf-parser-go v0.26.3 h1:F63PlUPIW56HGU5k4Crv0JAG9ojtz/TuwzgMYlb82Ec= github.com/gpustack/gguf-parser-go v0.26.4 h1:B9CRrQBcIB7I/MJFZvzPZ+e+cbc1xBzQJcmpi9l/mKU=
github.com/gpustack/gguf-parser-go v0.26.3/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0= github.com/gpustack/gguf-parser-go v0.26.4/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 h1:mymLUKThnV9wFvogOK8NnsMP9/vlhnjXY98gr2QIGW8= github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 h1:mymLUKThnV9wFvogOK8NnsMP9/vlhnjXY98gr2QIGW8=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74/go.mod h1:Bh9qYL8ehmDxSg14Tk8oxFCP90XHfs6NxV1D84884xA= github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74/go.mod h1:Bh9qYL8ehmDxSg14Tk8oxFCP90XHfs6NxV1D84884xA=
@@ -808,8 +808,10 @@ github.com/opencontainers/selinux v1.15.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZ
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= github.com/pb33f/go-yaml v0.1.0 h1:zZ6Gg+ATbumXlXZNLMHIe7+zTkVqe20lx+TSYRrXEuY=
github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= github.com/pb33f/go-yaml v0.1.0/go.mod h1:QvWdkHP5VjFYAeHMQUkaPQqREaQnBNOXmWzAjNrnSdo=
github.com/pb33f/ordered-map/v2 v2.3.2 h1:wDyaZ2Pv9QLh64X4utCeD5Zoi9nIv2aW3lwv172O5PQ=
github.com/pb33f/ordered-map/v2 v2.3.2/go.mod h1:1OhFrXu3OYw3kM+FXF+Ug3ugmmZ9LE8z0JfQMLvtV0w=
github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM= github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM=
github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw= github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
@@ -1084,8 +1086,6 @@ go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -1610,10 +1610,10 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
modernc.org/cc/v4 v4.29.2 h1:h6+9ciCnPKutf4I03CvheAvDLX7+IHlqR6Iy6J+cgd8= modernc.org/cc/v4 v4.29.7 h1:q+NXGJ0bK3b4TXFYQQVr9pYETGnmwFWkrUzJnMya/Tg=
modernc.org/cc/v4 v4.29.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= modernc.org/cc/v4 v4.29.7/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.35.0 h1:F+TUsmw09QxLzmi3aeYYGxjAXarmZaKgj3mKQHNaA8w= modernc.org/ccgo/v4 v4.36.1 h1:ZNIUZAryN0UgnJwtyxrdEzcFc3yD4Cu4AzjfPXsLsIE=
modernc.org/ccgo/v4 v4.35.0/go.mod h1:qrVGs9S3Sr2Ztcg9ve+kTAYMp5a3YvWjo+SoN06kJ5I= modernc.org/ccgo/v4 v4.36.1/go.mod h1:rrtGc2QkS239nYb/mQNuBMyjq3/y3ZXWbBjPoV3wqzA=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
@@ -1622,8 +1622,8 @@ modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc=
modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.75.7 h1:o3DTP9/0p9pKmY2WCKQaySW6wIiZhNM7wc2lUoyhfew= modernc.org/libc v1.77.1 h1:Ct8j47QtiZ1Enj2DtFXQtUqrPCAjdCmPjtCuvrYQ0Hs=
modernc.org/libc v1.75.7/go.mod h1:bO5o2ztHxBb2rjz0PgdHN0sSMw57CgxGFLZ3Qd/QpVQ= modernc.org/libc v1.77.1/go.mod h1:87/pZ4L6nD1zqW4nItuS12YO7hN1igAah34xjnQo/W0=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g= modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
@@ -1632,8 +1632,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= 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 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.59.0 h1:X1es1GpqBlS/5T+vbM4HLUdaa8OtQx468DF2vrx+38A= modernc.org/sqlite v1.60.1 h1:/blz53O951KWFOso4QQvEs/Fq6cDBKLtMVrYNSeJVKw=
modernc.org/sqlite v1.59.0/go.mod h1:+paeT2A3iPRHkQDwG7oA6Tk0zQd5woMEI8q7orfry8k= modernc.org/sqlite v1.60.1/go.mod h1:1dIoEagfDE72QytD5scH1lxARtaUgKgHC/NuApA27r0=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+8
View File
@@ -13,6 +13,14 @@ func Set(s redact.Store) {
store = s 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 { func Get() redact.Store {
return store return store
} }
@@ -75,6 +75,7 @@ catalogers:
method: glob # AUTO-GENERATED method: glob # AUTO-GENERATED
criteria: # AUTO-GENERATED criteria: # AUTO-GENERATED
- '**/*.deb' - '**/*.deb'
- '**/*.ipk'
metadata_types: # AUTO-GENERATED metadata_types: # AUTO-GENERATED
- pkg.DpkgArchiveEntry - pkg.DpkgArchiveEntry
package_types: # AUTO-GENERATED package_types: # AUTO-GENERATED
+2 -2
View File
@@ -18,8 +18,8 @@ func NewDBCataloger() pkg.Cataloger {
WithProcessors(dependency.Processor(dbEntryDependencySpecifier)) 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 { func NewArchiveCataloger() pkg.Cataloger {
return generic.NewCataloger("deb-archive-cataloger"). return generic.NewCataloger("deb-archive-cataloger").
WithParserByGlobs(parseDebArchive, "**/*.deb") WithParserByGlobs(parseDebArchive, "**/*.deb", "**/*.ipk")
} }
@@ -276,6 +276,36 @@ func TestDpkgArchiveCataloger(t *testing.T) {
}, },
}, },
}, },
}, {
name: "image-single-ipk",
expected: []pkg.Package{
{
Name: "libz1",
Version: "1.3.2-r0",
FoundBy: "deb-archive-cataloger",
Locations: file.NewLocationSet(
file.NewLocation("/libz1_1.3.2-r0_x86-64-v3.ipk"),
),
Type: pkg.DebPkg,
Metadata: pkg.DpkgArchiveEntry{
Package: "libz1",
Source: "zlib",
SourceVersion: "1.3.2",
Version: "1.3.2-r0",
Architecture: "x86-64-v3",
Maintainer: "Poky Maintainers <poky@lists.yoctoproject.org>",
InstalledSize: 0,
Description: `Zlib Compression Library
Zlib is a general-purpose, patent-free, lossless data compression library
which is used by many different programs.`,
Provides: []string{"zlib"},
Depends: []string{"musl (>= 1.2.6+git0+9fa28ece75)"},
},
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocationsWithContext(ctx, "Zlib"),
),
},
},
}, },
} }
+61 -15
View File
@@ -9,6 +9,8 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/scylladb/go-set/strset"
"github.com/anchore/packageurl-go" "github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal" "github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
@@ -61,32 +63,65 @@ func newDpkgPackage(ctx context.Context, d dpkgExtractedMetadata, dbLocation fil
return p 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 // 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. // 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 { 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 == "" { if raw == "" {
return nil return nil
} }
licenseStrings := []string{raw} if len(raw) > maxDeclaredLicenseLen {
if _, err := license.ParseExpression(raw); err != nil { log.WithFields("length", len(raw)).Debug("ignoring oversized declared license field")
licenseStrings = strings.Fields(raw) 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 { func newDebArchivePackage(ctx context.Context, location file.Location, metadata pkg.DpkgArchiveEntry, licenseStrings []string) pkg.Package {
p := pkg.Package{ p := pkg.Package{
Name: metadata.Package, Name: metadata.Package,
Version: metadata.Version, Version: metadata.Version,
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, licenseStrings...)...), Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, licenseStrings...)...),
Type: pkg.DebPkg, Type: pkg.DebPkg,
PURL: packageURL( PURL: packageURL(pkg.DpkgDBEntry(metadata), archiveDistro(location)),
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"}},
),
Metadata: metadata, Metadata: metadata,
Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
} }
@@ -95,6 +130,17 @@ func newDebArchivePackage(ctx context.Context, location file.Location, metadata
return p 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) // 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 { func packageURL(m pkg.DpkgDBEntry, distro *linux.Release) string {
if distro == nil { if distro == nil {
+31
View File
@@ -3,6 +3,7 @@ package debian
import ( import (
"context" "context"
"sort" "sort"
"strings"
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
@@ -145,6 +146,36 @@ func Test_extractDeclaredLicenses(t *testing.T) {
raw: "GPL-2.0 BSD-3-Clause", raw: "GPL-2.0 BSD-3-Clause",
expected: []string{"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 { for _, test := range tests {
+46 -15
View File
@@ -2,6 +2,7 @@ package debian
import ( import (
"archive/tar" "archive/tar"
"bytes"
"context" "context"
"errors" "errors"
"fmt" "fmt"
@@ -23,18 +24,27 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/generic" "github.com/anchore/syft/syft/pkg/cataloger/generic"
) )
// parseDebArchive parses a Debian package archive (.deb) file and returns the packages it contains. // parseDebArchive parses a Debian package archive (.deb) or opkg package (.ipk) file and returns the packages it
// A .deb file is an ar archive containing three main files: // contains. Both are ar archives containing three main files:
// - debian-binary: Version of the .deb format (usually "2.0") // - debian-binary: Version of the archive format (usually "2.0")
// - control.tar.gz/xz/zst: Contains package metadata (control file, md5sums, conffiles) // - 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) // - 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) { 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 metadata *pkg.DpkgArchiveEntry
var licenses []string var licenses []string
var ctrlLicenses []string
var unknownErr error var unknownErr error
var sawControl, sawData bool var sawControl, sawData bool
for { for {
@@ -56,7 +66,7 @@ func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen
if err != nil { if err != nil {
return nil, nil, unknown.New(reader.Location, fmt.Errorf("failed to decompress control.tar.* file: %w", err)) 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 { switch {
case err != nil && metadata == nil: case err != nil && metadata == nil:
return nil, nil, unknown.New(reader.Location, fmt.Errorf("failed to process control.tar.* file: %w", err)) 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")) 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 // a partial parse still yields a usable package, so report what went wrong alongside it rather than
// dropping either one // dropping either one
return []pkg.Package{ 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 // 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 // error, along with any licenses declared in the control file's License field. A non-nil error with
// clipped file listing); nil metadata means nothing usable was found and the caller should treat it // non-nil metadata means the package is usable but incomplete (e.g. a clipped file listing); nil
// as fatal. // metadata means nothing usable was found and the caller should treat it as fatal.
func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) { func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, []string, error) {
defer internal.CloseAndLogError(dcReader, "") defer internal.CloseAndLogError(dcReader, "")
tarReader := tar.NewReader(dcReader) tarReader := tar.NewReader(dcReader)
@@ -140,6 +154,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
var files []pkg.DpkgFileRecord var files []pkg.DpkgFileRecord
var confFileRecords []pkg.DpkgFileRecord var confFileRecords []pkg.DpkgFileRecord
var listingErr error var listingErr error
var licenses []string
for { for {
header, err := tarReader.Next() header, err := tarReader.Next()
@@ -147,7 +162,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
break break
} }
if err != nil { 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) { switch filepath.Base(header.Name) {
@@ -155,12 +170,13 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
// parseDpkgStatus already streams via bufio.Reader // parseDpkgStatus already streams via bufio.Reader
entries, err := parseDpkgStatus(tarReader) entries, err := parseDpkgStatus(tarReader)
if err != nil { 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 { 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()) entry := pkg.DpkgArchiveEntry(entries[0].toDpkgEntry())
licenses = declaredLicenseValues(entries[0].License)
metadata = &entry metadata = &entry
case "md5sums": case "md5sums":
// parseDpkgMD5Info streams via bufio.Scanner and reports its own clipping/scan errors // 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 { 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 { 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 // a clipped or unreadable file listing leaves the package usable, so hand both back and let the
// caller decide // 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 // a .deb's member sizes are bounded by the archive, but what they decompress to is not: xz and zstd
@@ -26,12 +26,13 @@ import (
func TestProcessControlTar(t *testing.T) { func TestProcessControlTar(t *testing.T) {
tarBytes := createTestTarWithControlFiles(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.NoError(t, err)
require.NotNil(t, metadata) require.NotNil(t, metadata)
assert.Equal(t, "test-package", metadata.Package) assert.Equal(t, "test-package", metadata.Package)
assert.Equal(t, []string{"MIT"}, licenses)
assert.Equal(t, "1.0.0", metadata.Version) assert.Equal(t, "1.0.0", metadata.Version)
// md5sums should have been parsed into file records // 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") 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) { func TestProcessControlTar_ConfigFileMarking(t *testing.T) {
// Create a tar where conffiles lists paths that overlap with md5sums entries // Create a tar where conffiles lists paths that overlap with md5sums entries
var buf bytes.Buffer var buf bytes.Buffer
@@ -61,9 +70,10 @@ func TestProcessControlTar_ConfigFileMarking(t *testing.T) {
require.NoError(t, tw.Close()) 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.NoError(t, err)
require.Len(t, metadata.Files, 3) 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[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") 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 var buf bytes.Buffer
tw := tar.NewWriter(&buf) tw := tar.NewWriter(&buf)
controlContent := "Package: test-package\nVersion: 1.0.0\nArchitecture: all\nMaintainer: Test <test@example.com>\nDescription: Test package\n" controlContent := "Package: test-package\nVersion: 1.0.0\nArchitecture: all\nMaintainer: Test <test@example.com>\nDescription: Test package\nLicense: MIT\n"
writeTarEntry(t, tw, "control", controlContent) writeTarEntry(t, tw, "control", controlContent)
md5Content := "d41d8cd98f00b204e9800998ecf8427e usr/bin/test-command\n" 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)) writeTarEntry(t, tw, "md5sums", strings.Repeat("d41d8cd98f00b204e9800998ecf8427e usr/bin/x\n", 20000))
require.NoError(t, tw.Close()) 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.ErrorIs(t, err, errDecompressedTooLarge)
require.NotNil(t, metadata, "a control tar read failure after \"control\" was parsed must not drop the package") 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)) writeTarEntry(t, tw, "md5sums", strings.Repeat("d41d8cd98f00b204e9800998ecf8427e usr/bin/x\n", maxDpkgFileRecords+10))
require.NoError(t, tw.Close()) 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.Error(t, err, "clipping must be reported")
require.ErrorIs(t, err, errClippedFileListing) require.ErrorIs(t, err, errClippedFileListing)
@@ -418,3 +428,18 @@ func Test_decompressionStream_failsEarlyInsteadOfBuffering(t *testing.T) {
assert.Less(t, bounded, uint64(8*intFile.MB), assert.Less(t, bounded, uint64(8*intFile.MB),
"a bounded stream must stop at the cap, not buffer the whole member and check afterwards") "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)
}
+10 -2
View File
@@ -183,9 +183,11 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (*dpkgExtractedMetadata, error)
} }
sourceName, sourceVersion := extractSourceVersion(raw.Source) sourceName, sourceVersion := extractSourceVersion(raw.Source)
if sourceName != "" {
raw.Source = sourceName
}
if sourceVersion != "" { if sourceVersion != "" {
raw.SourceVersion = sourceVersion raw.SourceVersion = sourceVersion
raw.Source = sourceName
} }
if raw.Package == "" { if raw.Package == "" {
@@ -263,8 +265,14 @@ func extractAllFields(reader *bufio.Reader) (map[string]any, error) {
} }
// If the source entry string is of the form "<name> (<version>)" then parse and return the components, if // If the source entry string is of the form "<name> (<version>)" then parse and return the components, if
// of the "<name>" form, then return name and nil // of the "<name>" form, then return name and nil. Yocto (opkg/ipk) instead writes the bitbake recipe filename
// ("<name>_<version>.bb"), which is split the same way.
func extractSourceVersion(source string) (string, string) { 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 // special handling for the Source field since it has formatted data
match := internal.MatchNamedCaptureGroups(sourceRegexp, source) match := internal.MatchNamedCaptureGroups(sourceRegexp, source)
return match["name"], match["version"] return match["name"], match["version"]
@@ -366,6 +366,16 @@ func TestSourceVersionExtract(t *testing.T) {
input: "", input: "",
expected: []string{"", ""}, 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 { for _, test := range tests {
@@ -0,0 +1,21 @@
# assembles a Yocto-style .ipk the same way bitbake's package_ipk does (opkg-build: a GNU ar archive of
# debian-binary, control.tar.gz and data.tar.zst) without paying for a full Yocto build. control/ is copied
# verbatim from libz1_1.3.2-r0_x86-64-v3.ipk built by poky-tiny (wrynose); the library itself is a stub
# since only the archive shape and control metadata matter here.
FROM alpine:3.18.4@sha256:48d9183eb12a05c99bcc0bf44a003607b8e941e1d4f41f9ad12bdcc4b5672f86 AS build
RUN apk add --no-cache binutils zstd
COPY control /pkg/control
RUN mkdir -p /pkg/data/usr/lib \
&& echo stub > /pkg/data/usr/lib/libz.so.1.3.2 \
&& ln -s libz.so.1.3.2 /pkg/data/usr/lib/libz.so.1 \
&& cd /pkg \
&& echo 2.0 > debian-binary \
&& tar -C control -czf control.tar.gz . \
&& tar -C data -cf - . | zstd -q -o data.tar.zst \
&& ar rcD /libz1_1.3.2-r0_x86-64-v3.ipk debian-binary control.tar.gz data.tar.zst
FROM scratch
COPY --from=build /libz1_1.3.2-r0_x86-64-v3.ipk /
@@ -0,0 +1,15 @@
Package: libz1
Version: 1.3.2-r0
Description: Zlib Compression Library
Zlib is a general-purpose, patent-free, lossless data compression library
which is used by many different programs.
Section: libs
Priority: optional
Maintainer: Poky Maintainers <poky@lists.yoctoproject.org>
License: Zlib
Architecture: x86-64-v3
OE: zlib
Homepage: http://zlib.net/
Depends: musl (>= 1.2.6+git0+9fa28ece75)
Provides: zlib
Source: zlib_1.3.2.bb
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
set -e
if [ x"$D" = "x" ]; then
if [ -x /sbin/ldconfig ]; then /sbin/ldconfig ; fi
fi
@@ -12,18 +12,25 @@ import (
) )
const ( const (
// maxBundleSearchSize bounds the bytes findSignatureOffset will hold at once while looking for the // maxBundleSearchSize bounds how far into a file findSignatureOffset looks for the bundle marker.
// bundle marker.
// //
// The marker sits inside the executable structure, so the search legitimately covers a whole // The marker sits inside the executable structure, so the search legitimately covers a whole
// single-file bundle, which routinely runs past 100MB and can reach a few hundred for an app that // single-file bundle, which routinely runs past 100MB and can reach a few hundred for an app that
// embeds sizable assets. Clamping to the file length alone is not enough: a mostly empty file costs // embeds sizable assets. Clamping to the file length alone is not enough: a mostly empty file costs
// almost nothing inside a compressed layer, so a small artifact can still authorize a multi-gigabyte // almost nothing inside a compressed layer, so a small artifact can still authorize an unbounded
// allocation. The trade-off is that a bundle larger than this loses its deps.json rather than being // amount of reading. The trade-off is that a bundle larger than this loses its deps.json rather than
// cataloged, which is the correct direction to fail when the alternative is OOM-killing the scan. // being cataloged, which is the correct direction to fail. This mirrors maxDeclaredSectionSize in
// This mirrors maxDeclaredSectionSize in syft/internal/elfutil. // syft/internal/elfutil. What the search holds in memory is bounded separately, by
// markerSearchWindowSize, so raising this costs time rather than resident bytes.
maxBundleSearchSize = 512 * intFile.MB maxBundleSearchSize = 512 * intFile.MB
// markerSearchWindowSize is how much of a file findSignatureOffset holds at once.
markerSearchWindowSize = 256 * intFile.KB
// bundleHeaderOffsetSize is the width of the little-endian header offset stored immediately before
// the marker.
bundleHeaderOffsetSize = 8
// maxDepsJSONSize bounds an embedded deps.json. These are dependency manifests, so real ones are // maxDepsJSONSize bounds an embedded deps.json. These are dependency manifests, so real ones are
// measured in KB even for large applications. // measured in KB even for large applications.
maxDepsJSONSize = 3 * intFile.MB maxDepsJSONSize = 3 * intFile.MB
@@ -37,13 +44,26 @@ const (
) )
// dotNetBundleSignature is the SHA-256 hash of ".net core bundle" used to identify single-file bundles. // dotNetBundleSignature is the SHA-256 hash of ".net core bundle" used to identify single-file bundles.
var dotNetBundleSignature = []byte{ var dotNetBundleSignature = [32]byte{
0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38, 0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32, 0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18, 0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae, 0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae,
} }
// markerSearchOverlap is how much of each search window carries over into the next: the bytes a marker
// could straddle the boundary with, plus the offset that precedes it. Retaining both is what lets a
// windowed search give the same answer as reading the file whole - a marker split across two reads is
// still matched, and the offset in front of it is always in the same window as the match.
const markerSearchOverlap = bundleHeaderOffsetSize + len(dotNetBundleSignature) - 1
// the slide at the end of findSignatureOffset carries markerSearchOverlap bytes into the next window and
// advances by the rest, so a window no wider than the overlap would take a negative slice index and never
// move. The window is five orders of magnitude clear of that today; this is what refuses to compile if
// anyone shrinks it to the marker's own scale, which is the tempting thing to do to make a test cheaper.
// The -1 is what makes equality fail too: the window has to exceed the overlap, not merely match it.
const _ = uint(markerSearchWindowSize - markerSearchOverlap - 1)
// ExtractDepsJSON returns the deps.json embedded in the .NET single-file bundle in r, or "" if r carries no // ExtractDepsJSON returns the deps.json embedded in the .NET single-file bundle in r, or "" if r carries no
// bundle marker. // bundle marker.
// //
@@ -94,29 +114,17 @@ func findBundleHeaderOffset(r unionreader.UnionReader, searchLimit int64) (int64
clamped = true clamped = true
} }
// this scans a whole executable, routinely over 100MB for a single-file bundle, so the buffer is sized headerOffset, found, err := findSignatureOffset(r, limit)
// exactly once. An append-growing read holds both arrays at its final growth and would cost well over if err != nil {
// twice the file's own size for the same result.
searchData := make([]byte, limit)
// a short read is not fatal here: the marker may well be in what we did get, so search the bytes we
// actually hold. ReadAt reports a short read as io.EOF, and may report a full one that way too, so the
// count is what says how much there is to search.
n, err := r.ReadAt(searchData, 0)
if err != nil && !errors.Is(err, io.EOF) {
return 0, err return 0, err
} }
if !found {
idx := bytes.Index(searchData[:n], dotNetBundleSignature)
if idx == -1 || idx < 8 {
if clamped { if clamped {
return 0, fmt.Errorf("no bundle marker in the first %d bytes and the rest of the %d byte file was not searched", maxBundleSearchSize, size) return 0, fmt.Errorf("no bundle marker in the first %d bytes and the rest of the %d byte file was not searched", maxBundleSearchSize, size)
} }
return 0, nil return 0, nil
} }
headerOffset := int64(binary.LittleEndian.Uint64(searchData[idx-8 : idx]))
if headerOffset == 0 { if headerOffset == 0 {
// the marker is compiled into every apphost; only publishing as a single file fills in the offset // the marker is compiled into every apphost; only publishing as a single file fills in the offset
return 0, nil return 0, nil
@@ -131,6 +139,52 @@ func findBundleHeaderOffset(r unionreader.UnionReader, searchLimit int64) (int64
return headerOffset, nil return headerOffset, nil
} }
// findSignatureOffset searches the first limit bytes of r for the bundle marker and returns the header
// offset stored in the 8 bytes immediately before it, or false if the marker is not there.
func findSignatureOffset(r unionreader.UnionReader, limit int64) (int64, bool, error) {
// nothing shorter than the marker plus the offset in front of it can hold a usable match
if limit < int64(bundleHeaderOffsetSize+len(dotNetBundleSignature)) {
return 0, false, nil
}
buf := make([]byte, min(int64(markerSearchWindowSize), limit))
var (
windowStart int64 // offset in the file that buf[0] holds
filled int // bytes of buf that hold file content
)
for {
// a short read is not fatal: the marker may well be in what we did get. ReadAt reports a short
// read as io.EOF, and may report a full one that way too, so the count is what says how much
// there is to search.
want := min(int64(len(buf)-filled), limit-windowStart-int64(filled))
n, err := r.ReadAt(buf[filled:int64(filled)+want], windowStart+int64(filled))
filled += n
if err != nil && !errors.Is(err, io.EOF) {
return 0, false, err
}
if idx := bytes.Index(buf[:filled], dotNetBundleSignature[:]); idx >= 0 {
// only reachable in the first window: after a slide the overlap guarantees that a match not
// already seen begins at least bundleHeaderOffsetSize into the buffer
if idx < bundleHeaderOffsetSize {
return 0, false, nil
}
return int64(binary.LittleEndian.Uint64(buf[idx-bundleHeaderOffsetSize : idx])), true, nil
}
// a buffer the read could not fill means the reader ran out before the limit did
if filled < len(buf) || windowStart+int64(filled) >= limit {
return 0, false, nil
}
copy(buf, buf[filled-markerSearchOverlap:filled])
windowStart += int64(filled - markerSearchOverlap)
filled = markerSearchOverlap
}
}
// dotNetBundleHeader represents the fixed portion of the bundle header (version 1+) // dotNetBundleHeader represents the fixed portion of the bundle header (version 1+)
type dotNetBundleHeader struct { type dotNetBundleHeader struct {
MajorVersion uint32 MajorVersion uint32
@@ -16,7 +16,7 @@ import (
func fileWithSignatureAt(size, sigStart int, headerOffset uint64) []byte { func fileWithSignatureAt(size, sigStart int, headerOffset uint64) []byte {
data := make([]byte, size) data := make([]byte, size)
binary.LittleEndian.PutUint64(data[sigStart-8:sigStart], headerOffset) binary.LittleEndian.PutUint64(data[sigStart-8:sigStart], headerOffset)
copy(data[sigStart:], dotNetBundleSignature) copy(data[sigStart:], dotNetBundleSignature[:])
return data return data
} }
@@ -74,7 +74,7 @@ func TestFindBundleHeaderOffset(t *testing.T) {
{ {
// there is no room for the 8-byte header offset before the signature // there is no room for the 8-byte header offset before the signature
name: "signature too close to the start to carry an offset", name: "signature too close to the start to carry an offset",
data: append(append([]byte{0, 0}, dotNetBundleSignature...), make([]byte, 32)...), data: append(append([]byte{0, 0}, dotNetBundleSignature[:]...), make([]byte, 32)...),
searchLimit: 128, searchLimit: 128,
}, },
{ {
@@ -295,3 +295,136 @@ func TestFindDepsJSONInManifest_PlausibleEntryCountIsWalked(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Empty(t, got, "no deps.json entry in this manifest") assert.Empty(t, got, "no deps.json entry in this manifest")
} }
// A windowed search only gives the same answer as reading the file whole if a marker lying across a
// window boundary - or one whose header offset lies across it - is still matched. These are the offsets
// where an overlap that is absent, too small, or applied to the wrong end of the buffer goes wrong.
func TestFindBundleHeaderOffset_MarkerAcrossWindowBoundary(t *testing.T) {
const window = markerSearchWindowSize
const headerOffset = 0x1234
size := window + 4096
for _, tt := range []struct {
name string
sigStart int
}{
{"whole marker inside the first window", window - 4096},
{"header offset split across the boundary", window - 4},
{"header offset ends exactly at the boundary", window},
{"signature split one byte into the second window", window - len(dotNetBundleSignature) + 1},
{"signature split down the middle", window - len(dotNetBundleSignature)/2},
{"signature starts one byte before the boundary", window - 1},
{"whole marker inside the second window", window + 2048},
} {
t.Run(tt.name, func(t *testing.T) {
data := fileWithSignatureAt(size, tt.sigStart, headerOffset)
got, err := findBundleHeaderOffset(readSeekCloser{bytes.NewReader(data)}, int64(size))
require.NoError(t, err)
assert.Equal(t, int64(headerOffset), got)
})
}
}
// recordingReader records the largest single read it is asked for. The search buffer is what bounds
// that length, so this is what fails if the implementation ever sizes a buffer against the file again.
type recordingReader struct {
*bytes.Reader
largestRead int
}
func (r *recordingReader) ReadAt(p []byte, off int64) (int, error) {
if len(p) > r.largestRead {
r.largestRead = len(p)
}
return r.Reader.ReadAt(p, off)
}
func (*recordingReader) Close() error { return nil }
// Holding an executable at its full size to look for 32 bytes is what made this the largest single
// consumer of memory in a scan of a Windows volume, so the bound matters more than the result here.
func TestFindBundleHeaderOffset_ReadsAreBoundedByTheWindow(t *testing.T) {
size := 5 * markerSearchWindowSize
data := fileWithSignatureAt(size, size-1024, 0x1234)
r := &recordingReader{Reader: bytes.NewReader(data)}
got, err := findBundleHeaderOffset(r, int64(size))
require.NoError(t, err)
require.Equal(t, int64(0x1234), got, "the marker must still be found at the far end of the file")
assert.LessOrEqual(t, r.largestRead, markerSearchWindowSize,
"no single read may exceed the search window, whatever the file's size")
}
// Every window after the first begins with bytes already searched. Re-reporting a match from that
// overlap would be harmless, but failing to advance past it would spin forever.
func TestFindBundleHeaderOffset_UnmarkedFileTerminates(t *testing.T) {
for _, size := range []int{
markerSearchWindowSize - 1,
markerSearchWindowSize,
markerSearchWindowSize + 1,
markerSearchWindowSize + markerSearchOverlap,
3 * markerSearchWindowSize,
} {
got, err := findBundleHeaderOffset(readSeekCloser{bytes.NewReader(make([]byte, size))}, int64(size))
require.NoError(t, err)
assert.Zero(t, got)
}
}
// lateShortReader reads cleanly until shortFrom and only then hands back fewer bytes than were asked for.
// shortReader above comes up short on its very first read, which the search answers before it has slid at
// all; the reader running out partway through a windowed search is a different branch.
type lateShortReader struct {
*bytes.Reader
shortFrom int64 // a read starting here or beyond stops after shortLen bytes
shortLen int
}
func (r *lateShortReader) ReadAt(p []byte, off int64) (int, error) {
if off < r.shortFrom || len(p) <= r.shortLen {
return r.Reader.ReadAt(p, off)
}
n, _ := r.Reader.ReadAt(p[:r.shortLen], off)
return n, io.EOF
}
func (*lateShortReader) Close() error { return nil }
// A squashfs block that decompresses short makes unionreader return fewer bytes than were asked for, and
// nothing says that lands in the first window. Whatever the search did get still has to be searched, and the
// bytes the reader never produced have to end it rather than being read as a file with no bundle in it.
func TestFindBundleHeaderOffset_ShortReadAfterASlideSearchesWhatWasRead(t *testing.T) {
const window = markerSearchWindowSize
const shortLen = 4096 // what the second window gets before the reader gives up
const headerOffset = 0x1234
size := 2*window + 8192
for _, tt := range []struct {
name string
sigStart int
want int64
}{
{"marker inside the bytes the second window did get", window + 1024, headerOffset},
{"marker straddling the boundary into those bytes", window - 4, headerOffset},
{"marker beyond where the reader gave up", window + shortLen + 1024, 0},
} {
t.Run(tt.name, func(t *testing.T) {
// honest about its length: only the read past the first window comes up short
r := &lateShortReader{
Reader: bytes.NewReader(fileWithSignatureAt(size, tt.sigStart, headerOffset)),
shortFrom: window,
shortLen: shortLen,
}
got, err := findBundleHeaderOffset(r, int64(size))
require.NoError(t, err, "a reader that runs out is not a parse failure")
assert.Equal(t, tt.want, got)
})
}
}