Compare commits

...
119 Commits
Author SHA1 Message Date
anchore-oss-update-botandanchore-oss-update-bot 02ba369d13 chore(deps): update anchore dependencies (#5236)
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-09-17 10:09:46 -04:00
Jason PaulosandAlex Goodman 74073aa481 fix: update steroscope to include OCI platform metadata (#5128)
* Update steroscope to include OCI platform metadata

Signed-off-by: Jason Paulos <jasonpaulos@users.noreply.github.com>

* chore(deps): point stereoscope at anchore/main

drops the fork `replace` directive and moves to a pseudo-version off stereoscope
main (`a22e5ae`, anchore/stereoscope#657). no tag carries it yet, so this should
move to a real version once one is cut.

`readImageMetadata` now fills os/architecture/variant for every provider, not
just oci. the docker tarball provider set neither before, so image metadata and
the image purl change for docker-archive input too:

- `"architecture": ""` -> `"amd64"`
- `"os": ""` -> `"linux"`
- `?arch=` -> `?arch=amd64`

the empty `arch=` qualifier was never right, but it is user-visible output.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* test: cover single-platform oci layouts

the pr only added assertions to the multiplatform fixture, which already
reported the right platform. the behavior that actually changed is single-image
layouts: they used to report empty os/arch and silently ignore `--platform`, and
now report what the image config says and fail with a platform mismatch.

also drops the non-linux skip on the default-platform test. the oci providers
default to `linux/<GOARCH>` regardless of host os, so it holds on macos too.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Jason Paulos <jasonpaulos@users.noreply.github.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-16 01:40:54 +00:00
Anže Pečar 0b234b3256 perf(javascript): cache npm registry license lookups across runs (#5297)
When javascript.search-remote-licenses is enabled, the lock catalogers
query registry.npmjs.org once per package occurrence, on every run, with
nothing in front of the network call. The same name@version is fetched
again for every lockfile that mentions it, and again on the next scan.

This adds a javascriptLicenseResolver that resolves licenses through
syft's on-disk cache, keyed on "<name>/<version>" and namespaced as
javascript/v1, and routes the package-lock v1, package-lock v2, pnpm,
yarn and bun package constructors through it, leaving
getLicenseFromNpmRegistry itself unchanged. Failed lookups are not
cached: a transient registry failure or a name that 404s must not be able
to blank out licenses for the lifetime of the cache entry, since a
missing license silently weakens downstream license policy checks. A
successful lookup that returns an empty license is a real answer and is
still cached.

Signed-off-by: Anže Pečar <anze@pecar.me>
2026-09-15 17:32:28 -04:00
Alex Goodman b4ee6e4696 fix(debian): bound the decompressed members of a .deb archive (#5293) 2026-09-14 18:17:56 -04:00
Alex Goodman 2be26a1af9 fix(kernel): bound what a compressed kernel module expands to (#5294)
`.ko.gz`/`.ko.xz`/`.ko.zst` modules are spilled to a temp file so ELF parsing
can seek over them. The compressed size is bounded by the file in the image but
what it decompresses to is not, so a few KB of crafted zstd filled the temp
directory until the disk did.

`io.Copy` now stops one byte past a 256MB cap and rejects rather than
truncating, since a partial ELF reads as a corrupt module instead of a rejected
one. The partial spill is removed on the reject path.

The cap is a parameter on an inner function so the tests exercise the boundary
with a few hundred bytes instead of materializing 256MB, matching
`decompressSbomWithLimit` in the graalvm cataloger.

The cap bounds one spill, not the process: peak disk is 256MB times however many
modules are parsed at once, which is cataloger parallelism (`NumCPU*4` by
default, and unbounded when `--parallelism` is negative).

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-14 17:40:41 -04:00
Alex Goodman 30e9e65608 Fix: alpm mtree gzip unbounded processing (#5292)
* fix(arch): bound the decompressed mtree listing

The ALPM cataloger handed a gzip stream straight to `mtree.ParseSpec`, which
materializes every entry before it returns any of them. Nothing capped the
decompressed size, so a crafted mtree in a scanned image expanded until the
process died. 64KB of input is enough to produce 64MB, and the ratio scales.

Reads through a limit now and rejects a listing that exceeds it. The read goes
one byte past the cap so that tripping it is distinguishable from a listing that
simply ends there, since silently truncating would produce a package missing
most of its files. The existing error path already surfaces this as an unknown.

The cap is well above any real package: an mtree names every file with its
digests, and even the largest packages land far under it.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(arch): bound mtree entry count, not just decompressed bytes

The byte cap alone does not bound what the parse retains. go-mtree keeps an
entry per line and appends it unconditionally, blank lines included, so short
lines cost about 1KB of heap each.

Measured on the previous commit: 15KB of gzipped newlines expanding to 16MB,
comfortably inside the 64MB byte cap, drove 8.1GB of peak heap over 74 seconds
and returned no records and a nil error. Same input now rejects in 0.05s at
51MB peak.

Counting lines before parsing is what makes the entry allocation bounded, since
the parser materializes all of them before returning. Lines the parser skips
only make the count high, never low.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(arch): apply the mtree bounds while streaming

Both caps were enforced against a fully buffered listing, so the peak cost
scaled with cataloger parallelism, which runs a goroutine per package. They now
apply as the stream flows: a limited reader for the byte cap and a counting
reader for the entry cap, which has to trip while the parser is still reading
since it materializes every entry before returning any of them.

Both limits are injectable so the boundary tests can be exact on both sides
without allocating their way up to the shipped 64MB. Each cap gets a listing
exactly at it, asserting the records parse whole rather than just that no error
came back, since quiet truncation is the failure a size check invites.

The byte limiter carries one byte of headroom, which is what makes an
overrunning listing distinguishable from one that simply ends at the cap.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(arch): refuse mtree line continuations

go-mtree collapses a line ending in a backslash into the next one with a
quadratic string concat, so the cost is lines times bytes and neither the byte
cap nor the line cap can see it. 199k continued lines sitting inside both caps
measured 4m34s and 6.1TB of allocation churn, and returned no error.

Also re-derived both constants against a real package rather than a guess.
`texlive-fontsextra` is 105,847 files and 1,978 directories, so roughly 108k
lines at 150-250 bytes each. 16MB would have falsely rejected it, so the byte
cap stays at 64MB and the line cap goes 200k -> 300k, which is ~2.8x a real
package instead of 1.85x.

Rejections are now sentinels so tests can name which bound tripped, and the
constants carry the measured ceiling they imply: a crafted listing of minimal
fields still reaches ~1.1GB at the byte cap, since go-mtree allocates per
whitespace-delimited field. Bounding that further means bounding fields rather
than bytes, and legitimate input is already within ~1.3x of the crafted worst
case, so there is little to win.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(arch): refuse a continuation spelled with a carriage return

The refusal matched the literal bytes backslash-newline, but go-mtree decides
"this is a continuation" from the scanner token, and `bufio.ScanLines` drops a
trailing carriage return before testing for the backslash suffix. So a listing
using CRLF ran the quadratic collapse with nothing watching.

Measured: 775KB of gzip expanding to 17MB across 150k lines, inside both other
bounds, took 1m33s and returned a nil error. Same input now rejects in 50ms.

The check is a single byte pass that counts newlines and looks for a backslash
before either line ending, which replaces the two scans and the straddle special
case it took to catch only one spelling. A tripped bound is also latched now:
`bufio.Scanner` stops at the first error, but nothing about the type said a
caller had to, and the refusal was recomputed per chunk.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* test(arch): make the bombpkg fixture able to fail

The fixture was 310k bare newlines, which go-mtree turns into blank entries with
no name, and the parse loop already skips those. So the assertion that a rejected
listing leaves an empty file list held whether or not the bound existed. It is now
a repeated file line: 0 records with the bound, 310,000 without.

Dropped the two tests that asserted nothing as a result. The cataloger-level one
was a weaker restatement of what the golden comparison already pins exactly, and
the shipped-size-cap one drove a real 64MB listing through the parser to prove a
constant had not moved. Asserting the constants directly catches any change rather
than only a loosening.

Race-mode package runtime goes 15.7s -> 4.4s.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* test(arch): make three mtree assertions able to fail

The truncated-gzip case asserted the failure was not misreported as the size
bound, but ran against the shipped 64MB cap on a 3.5KB listing, so the check it
guarded could never fire. It now runs against a cap just above the truncation
point, where the two genuinely race.

The bomb case asserted an allocation delta from `runtime.MemStats`, which is a
process-wide cumulative counter and measures the property indirectly. It now
counts bytes pulled through the reader, which is what "gave up while reading"
actually means and does not drift with GC or parallelism.

Added the one bound that had no test at all. A single line over 64KB is refused
by go-mtree's scanner, not by anything here, so a dependency bump that called
`scanner.Buffer` would remove it silently.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* docs(arch): correct the mtree bound comments and close the gzip reader

The comment said both bounds apply while the listing streams. Only the line cap
does. `io.LimitedReader` signals a drained budget with `io.EOF`, which the parser
reads as the end of a complete listing, so the byte cap is necessarily a check
after the fact. Peak cost is the same either way, since accepting a listing at
the cap costs what rejecting one just past it does, but the comment was claiming
a mechanism that isn't there.

Also trimmed the benchmark writeups down to the reasoning that outlives them.
The exact transient-heap and wall-clock figures are tied to a go-mtree version
with nothing to detect them going stale, and one of them described the CRLF case
as prevented when it wasn't. And `TestMtreeParse` was labelled the regression
guard for the continuation refusal, but its fixture has no backslashes in it.

`gzReader` now gets a `defer Close` to match the other gzip site in the tree.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(arch): stop the alpm database parser panicking on a malformed backup line

A `%BACKUP%` entry is `path\tmd5`, and the parser indexed the second field
without checking the split produced one. A line with no tab panics. The database
is attacker-controlled, and while the task executor recovers, it recovers at task
granularity, so one crafted line drops every Arch package from the SBOM.

The same loop also never checked the scanner's error. A field over the 1MB token
cap ends the scan, and the entry came back populated up to that field and empty
after it, with a nil error saying it was fine. That is the failure the mtree
bounds in this branch exist to avoid, two functions up in the same file.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* docs(arch): trim the comments added by this branch

the comments were carrying reasoning that belongs in the PR description rather
than the code. Kept the parts a future reader cannot recover from the code (the
cap sizing, why continuations are refused, why the byte check runs after the
parse) and cut the rest.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* test(arch): generate the mtree bomb fixture instead of committing it

the fixture is a synthetic listing fully described by a line count and a
repeated line, but it landed as a committed gzip member, which shows neither and
leaves no way to rebuild it when maxMtreeLines moves. the two tests that walk
testdata/installed write it first now, and git ignores it.

the real pacman listings (gmp, tree-sitter) stay committed since those are
captured output rather than something a rule can reproduce.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* correct and simplify comments

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-14 19:59:20 +00:00
JuanandAlex Goodman e6d364437a feat(convert): passthrough-exact-format copies matching syft-json input unchanged (#5279)
* feat(convert): passthrough-exact-format copies matching syft-json input unchanged

When a syft-json input SBOM already exactly matches the requested output
format and schema version, `syft convert` can write the input bytes out
verbatim instead of decoding and re-encoding them. This avoids the lossy
round trip (and the associated cost) for callers that normalise SBOMs into
a target format without knowing up front whether the source already matches.

The behaviour is opt-in and configuration-only: it is enabled with
convert.passthrough-exact-format in the config file or the
SYFT_CONVERT_PASSTHROUGH_EXACT_FORMAT environment variable, and there is
deliberately no command-line flag, since it changes how every invocation in
a pipeline behaves rather than varying call by call. It defaults to off, so
existing behaviour is unchanged.

Pass-through applies only to syft-json input, and only when the document's
schema version is identical to the one this build of syft produces; any
other input is always converted. Outputs that do not match are still
converted, so a mixed set of `-o` values behaves as expected, and every
output is resolved and validated before anything is written. Pass-through
output is a byte-for-byte copy, so format options such as `format.pretty`
and `format.json.legacy` are not applied to it.

When the option is enabled a warning is logged stating so and noting the
syft-json-only scope, and a second warning is logged whenever an input is
found to match and the shortcut is actually taken.

The option lives under a `convert:` configuration section (options.Convert),
mirroring how attest scopes its own options. The description documents the
caveats above and notes that file-path input written with `-o <format>=<path>`
keeps memory minimal, whereas piped input or STDOUT output must hold the
whole document in memory.

Signed-off-by: juanjsebgarcia <github@juangarcia.co.uk>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* perf(convert): stream pass-through output instead of buffering the input

Identify, copy, and decode the input through a seekable reader rather than
loading the whole document into memory first. File inputs are read in place
and pass-through output is an io.Copy from the file. STDIN is used in place
when it is a redirected regular file. A pipe or terminal cannot seek, so its
contents are drained into an internal/spillbuf buffer: the first 1MB stays in
memory and the rest goes to a temp file under the user's TMPDIR (created
lazily, only when something actually spills), giving a seekable view without
ever holding the document in memory. The temp dir is rooted per invocation
via internal/tmpdir, honouring one already attached to the context, and is
removed when the command returns.

Writing pass-through output over the input file itself is now a no-op rather
than truncating the file before it is copied.

Measured on a 1GB syft-json document (Apple M1 Max), file in, file out:

  origin/main full re-encode      86.3s wall   99s CPU   12.0GB max RSS
  in-memory pass-through           7.4s wall  6.7s CPU    4.2GB max RSS
  streaming pass-through           7.0s wall  6.9s CPU    3.1GB max RSS

and the same 1GB document piped over STDIN with pass-through enabled:

  io.ReadAll into memory           1.3s wall  0.8s CPU    2.7GB max RSS
  spill buffer                     1.8s wall  1.0s CPU     66MB max RSS

The remaining cost of the streaming pass-through is entirely format.Identify,
which for syft-json decodes the whole document through a json.Decoder that
buffers the full value: measured alone it takes 7.1s and 3.1GB on the same
input. Reducing that further belongs to the format package.

Signed-off-by: juanjsebgarcia <github@juangarcia.co.uk>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* perf(syftjson): identify documents from their tail instead of a full parse

syft always writes the schema block as the last key of a syft-json document,
so the format and version can be read from the final bytes of the file. The
syft-json decoder's Identify now reads at most 2MB from the end of a seekable
input, finds the last "schema" key there, and verifies it sits directly within
the top-level object by scanning the remaining bytes for the single closing
brace of the document. Anything the tail cannot vouch for (non-seekable input,
a reordered document, a nested key) falls back to the existing full parse, so
behaviour is unchanged for documents the fast path declines.

The verification is position-independent: it also accepts documents whose
keys have been reordered so that other small keys follow schema. It rejects
nested schema objects, array-wrapped documents, truncated input, trailing
content, and schema text inside strings.

Supporting changes:
- the offset-aware reader the decoder collection hands to decoders now
  supports io.SeekEnd, which the tail read requires
- model.Document documents that Schema must remain its last field, and a
  test guards that the encoder writes it last in both compact and pretty
  output
- the decoder list documents that syft-json must stay first

Since Decode calls Identify before parsing, this also removes one of the two
full parses every syft-json conversion used to perform.

Measured on a 1GB syft-json document (Apple M1 Max), file in, file out:

  pass-through, previous commit     7.0s wall   6.9s CPU   3.1GB max RSS
  pass-through, this commit        0.85s wall  0.05s CPU    66MB max RSS
  full re-encode, previous commit  85.3s wall   99s CPU  11.5GB max RSS
  full re-encode, this commit      75.2s wall   91s CPU   9.9GB max RSS

Signed-off-by: juanjsebgarcia <github@juangarcia.co.uk>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(syftjson): only identify from the tail when the document starts where the reader does

The tail scan vouched for the last 2MB, but nothing checked those bytes belonged to the
document the reader was pointed at. So `Identify` started claiming syft-json for files it
cannot decode: a document with a byte order mark or a stray log line in front of it, or a
syft document sitting after something else in the same stream, where `format.Decode` went
on to return an empty SBOM with a nil error.

Two O(1) checks:

- the first non-space byte at the reader's position has to be `{`
- `Decode` errors when the parsed document has no `schema.version`, instead of handing back
  the empty document it decoded into

Streams holding several concatenated documents are deliberately not detected. Proving there
is exactly one top-level value means reading all of it, which is the cost the tail path
exists to avoid, so `Identify` can still name a later document's version on such a file.
`Decode` reports what it actually parsed.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(convert): recognize the input file by identity rather than by path

`convert - -o syft-json=sbom.json < sbom.json` truncated the file and exited 0. The
passthrough guard compared paths, and a shell redirect does not have one, so it was blind to
the one input mode where the truncation was silent.

`openConvertInput` now returns the input's `os.FileInfo`, which the STDIN branch already had
from its `Stat`, and `isSameFile` compares that instead.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: juanjsebgarcia <github@juangarcia.co.uk>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-14 14:33:03 -04:00
Alex Goodman c55e1cf2fd Fix: graalvm unbounded allocation (#5291)
* fix(java): bound user-controlled reads in the graalvm native-image cataloger

Sizes, offsets and lengths in a native-image binary all come from the file
being parsed, and several were acted on directly.

The PE export directory sized a `make([]byte, Size)` from a uint32 in the
optional header, so an 8KB file claiming 4GB reserved 4GB before reading a
byte. It now reads what the file actually holds, bounded by the declared size,
and still requires the whole directory to be present. This also fixes a latent
bug: `unionreader.readerAtAdapter.ReadAt` can return a short read with a nil
error, and the old code discarded the count, so squashfs-sourced binaries could
parse zero padding as export data.

The bounds checks in `decompressSbom` and the PE export walk validated only the
end of a range, computed by adding to a value out of the file. Those sums wrap,
and a wrapped sum compares as in range while the slice that follows it panics.
They now subtract from the known-good length instead. The three
`address - sectionBase` subtractions are unsigned and underflowed on an address
below the base; they share one guarded helper now.

The embedded SBOM decompresses through an `io.LimitedReader`, since the
compressed bytes are bounded by the file but what they expand to is not. The
limit is checked before the decoder's own error, which would otherwise report a
size problem as "not a cyclonedx json document". Hitting it is logged, since
the caller reports parse failures at trace level and a dropped SBOM would
otherwise go unnoticed.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(java): stop at the first format that parses a native image

The format loop tried ELF, Mach-O and PE against every reader and kept going
after one of them parsed, so a file that parses as more than one format
contributes its packages once per format.

No input parses as two formats today, so this is not a behavior change in
practice: `debug/pe` rejects an ELF on the optional-header magic, and both
`debug/elf` and `debug/macho` reject anything without their own magic. It
removes the possibility rather than a live bug.

Note this does not deduplicate across readers, which is the outer loop: a
universal Mach-O carrying the same native image for two architectures still
yields its packages twice.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(java): bound the graalvm export directory read against the file

Follow-up to the hardening earlier in this branch, from review.

The export directory read was still bounded only by file size, so a large
mostly-empty file (cheap to ship inside a compressed layer) was read into
memory in full and then discarded. It now weighs the declared size against an
absolute cap and against the bytes really present before allocating anything,
matching `readDataFromRVA` in `syft/pkg/cataloger/internal/dotnet/pe`. A 64MB
file declaring a 4GB export directory went from ~140MB allocated to ~3.6KB.

Also from the same review:

- dropped the decompressed-SBOM ceiling from 100MB to 16MB. Decoding costs
  roughly 48x the JSON size in allocation and 6x retained, so the old ceiling
  was worth several GB of churn per file, and catalogers run concurrently.

- `symbolOffsets` is now single-address `symbolOffset`. Two same-typed address
  arguments in a row could be transposed at a call site and would produce a
  silently wrong SBOM rather than an error.

- `newMachO` now returns an error on a real read failure. It was falling
  through and dropping the file with no error and no log at all.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(java): reject a short read of the graalvm export directory

Second round of review on this branch.

The export directory read introduced earlier in the branch only consulted the
byte count when `ReadAt` also returned an error, so a reader handing back fewer
bytes with a nil error was accepted and the tail of the buffer stayed zeroed,
then got parsed as a valid export directory. The count decides now, whatever
the error says. `io.ReadFull` would also cover it but spins forever on a reader
that keeps answering `(0, nil)`, which is the same hostile input this is meant
to reject, so the single read stays.

Also in here:

- the decompressed SBOM is buffered and size-checked before the decoder is
  invoked, instead of inferring the overrun from `io.LimitedReader.N` after the
  fact. That check was only correct because the cyclonedx decoder happens to
  drain its reader before parsing, and nothing would have caught it if that
  changed.

- four bounds guards, the `newMachO` error path and the SBOM ceiling had no
  test that failed when they were removed. They do now, verified by deleting
  each one and watching a named test go red. The ceiling in particular could be
  raised to 2GB with the suite still green, since every case derived its sizes
  from the constant.

- `nativeImagePE.reader` is dead now that the read takes the reader as an
  argument.

- three comments claimed guarantees the code does not make: that `symbolOffset`
  cannot be called with transposed arguments, that a rejected file cannot go
  unrecorded when it is only logged at trace level, and a set of measured
  allocation multipliers that nothing pins.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* chore(java): trim the graalvm cataloger comments

Comment pass over everything this branch added. Cut about a quarter of the
comment lines, mostly restatement and detail that was never going to stay true.

Dropped: a production comment naming a test, the measured allocation multiples
behind the two size ceilings, cross-references to the equivalent constants in
`dotnet/pe` and `elfutil`, and test docs that restated their own test name
before saying anything.

Kept the parts that fail silently if someone tidies them away: why the read is
a single `ReadAt` rather than `io.ReadFull`, the note that `readExportDirectory`
uses an RVA directly as a file offset, and the fixture notes explaining why a
`VirtualAddress` is deliberately huge or why no symbol sits at index 0.

Three that were stale rather than verbose:

- `nativeImagePE`'s doc still said it holds the underlying reader, which this
  branch deleted.

- `TestFetchSbomSymbols_FindsAllThreeSymbols`' doc had ended up above
  `TestFetchSbomSymbols_AddressBaseOutOfRangeGuard`, documenting the wrong
  function.

- a comment referenced a commit sha that no longer exists on this branch.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-14 13:49:19 -04:00
anchore-oss-update-botandanchore-oss-update-bot 356e601cd5 chore(deps): update CPE dictionary index (#5290)
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-09-14 09:30:19 -04:00
dependabot[bot] ee05a595b6 chore(deps): bump github.com/hashicorp/go-getter from 1.8.8 to 1.8.9 (#5272)
Bumps [github.com/hashicorp/go-getter](https://github.com/hashicorp/go-getter) from 1.8.8 to 1.8.9.
- [Release notes](https://github.com/hashicorp/go-getter/releases)
- [Changelog](https://github.com/hashicorp/go-getter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/go-getter/compare/v1.8.8...v1.8.9)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/go-getter
  dependency-version: 1.8.9
  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-09-11 21:46:07 +00:00
dependabot[bot] 05c6817c07 chore(deps): bump google.golang.org/grpc from 1.83.1 to 1.83.2 (#5262)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.83.1 to 1.83.2.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.83.1...v1.83.2)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.83.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-11 21:28:05 +00:00
Alex Goodman d85c72a08e fix: harden .NET PE/ELF parsing against malformed binaries (#5267)
* fix(dotnet): bound the bundle signature search by the file, not the headers

The single-file bundle marker search sized its buffer from `calculatePEEndOffset` /
`calculateELFEndOffset`, both of which sum user-controlled header fields. A tiny file
declaring 4GB sections drove an 8.6GB allocation, and on the ELF side the sum could
overflow negative and panic in `make`.

The search now clamps its window to the real file length before sizing anything. The PE
and ELF paths had grown two copies of the same find-and-read sequence, so they collapse
into one `bundle.ExtractDepsJSON`, which lets the signature search and the header reader
both go unexported.

Also guards `read7BitEncodedInt` against returning a negative length, which on a 32-bit
`int` would seek a manifest walk backwards and re-read the same bytes per declared file.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(dotnet): stop a truncated version resource from spinning forever

`readIntoStruct` reported a truncated read as success and left the destination struct
zeroed. Its callers advance their loop counters by the offsets it updates, so the string
table loop consumed nothing, advanced nothing, and never reached its length bound: a
version resource whose header claims more content than the blob carries hung the parse.

A truncated read is now an error, and the two places where hitting the end of a blob is
normal say so explicitly. That distinction is what keeps a well-formed resource whose last
child is `VarFileInfo` from losing its `FileVersion` fallback. io.EOF and
io.ErrUnexpectedEOF mean the same thing here, so both take the same path.

The string table walk moves into its own function, since the two new arms put
`parseVersionResourceSection` over the funlen limit.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(dotnet): bound PE resource parsing by what the file actually holds

Resource sizes and RVAs are user-controlled uint32s that sized buffers before anything
checked the file held that many bytes, so a small crafted section could reserve up to 4GB.

The walk now carries the section's reader and its base RVA for its whole lifetime, so
`reader.Size()` is the single authoritative bound and one `offsetOf` helper rejects every
RVA the section does not hold. That replaces the per-node section value the recursion used
to synthesize, whose size argument was only correct by convention.

Bounding the allocations alone turns the OOM into a hang, though: nothing in the format
stops entries from aliasing, so thousands of them may name one fat blob and every offset
still checks out. A 256KB section built that way drove 24GB of allocation over two minutes
with peak memory flat. Capping entry *count* does not help since the cost is per byte read,
so the walk now charges every read against a budget of a few times the section size. Real
binaries come in at about 1x, since a well-formed section's entries partition it.

A resource tree that stops early no longer fails the whole file. It says nothing about the
CLR directory or an embedded deps.json, and dropping the package from the SBOM over one
malformed section is worse than reporting the fields we did get.

Also widens the entry counts before summing them (two uint16s can sum past 0xFFFF and wrap),
and bounds the name length and section header count against the reader for the same reason.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(dotnet): bound resource recursion depth and cap declared sizes absolutely

Review follow-ups on this branch. The headline one is a real crash: the resource
walk had no depth bound, and the `budget` comment claiming depth "falls out for
free" was wrong, since the budget scales with the section while the stack does
not. A chain of directories at distinct RVAs (which the `dirs` set does not
catch) recursed until the goroutine stack gave out. A 13.7MB crafted PE took the
whole process down with `fatal error: stack overflow`, which is unrecoverable,
so unlike every other bug on this branch no caller could log-and-continue past
it. Now capped at 32 levels, versus the 3 the format actually uses.

The other bounds moved from "the file" to a real ceiling. Clamping an allocation
to the bytes remaining in the file is not enough on its own: a mostly empty file
costs almost nothing inside a compressed layer, so a cheap artifact could still
authorize a multi-gigabyte read. Both the PE data directories and the bundle
marker search now cap the same way `elfutil` already does.

Also hardened in the bundle reader:

- `headerOffset` is validated against the file. It comes straight out of the
  file bytes and everything downstream seeks to it, so it was the least
  trustworthy value in there and the only one not being checked.
- a manifest entry count that could not fit in the remaining bytes is rejected
  rather than walked for hundreds of millions of reads.
- a non-positive `searchLimit` falls back to the whole file instead of reading
  nothing. It is only ever an optimization hint, so treating it as authoritative
  let one bogus ELF `p_offset` hide a bundle from us entirely.
- a short read no longer aborts the search. `unionreader` can return fewer bytes
  than the file reports, and the marker may well be in what we did get.

`pe.Read` now reports partial parses instead of choosing between dropping the
package and hiding the problem. It was inconsistent: a malformed resource tree
was non-fatal while an unreadable resource *section* one call away was fatal, and
the non-fatal path only reached a trace log, so the unknowns task could not see
that a file had been cataloged on incomplete evidence. Both now land on a new
`File.ParseErr`, which the dotnet and PE catalogers join into their unknowns and
still keep the package.

Worth noting for changelog purposes: the `isTruncated` guard fixes a
user-visible bug beyond the hardening. A version resource leaving 1 to 5
trailing bytes cuts the next struct in half, which reports as
`io.ErrUnexpectedEOF` rather than `io.EOF`; that used to propagate out and skip
the `VS_FIXEDFILEINFO` fallback, so those binaries lost their version entirely.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(dotnet): treat a zero bundle offset as unbundled, not malformed

The apphost ships the 32-byte bundle signature compiled in with its 8-byte header
offset left as zero, and only `HostWriter.SetAsBundle` fills that slot in when you
publish as a single file. So a zero offset means "never bundled", which is what
every framework-dependent .NET executable looks like.

The bounds work on this branch folded that case in with the hostile ones, so the
most common shape a .NET binary takes started reporting a parse failure. It
surfaces through the PE cataloger rather than the dotnet one, since an apphost
carries no CLR metadata of its own: the package still comes out, with a spurious
unknown attached to it. `main` had this right and said so in a comment; the
regression is on this branch.

Reproduces on a fixture already in the repo. Of the 53 PE files in
`image-net8-app`, `/app/dotnetapp.exe` was the only one reporting anything, and
the whole image is clean again now.

The three outcomes are now distinct, since collapsing any two of them loses
something the caller needs: zero means there is no bundle, an error means we could
not tell, and a positive offset is the answer.

`Test_Read_DotNetDetection` gains the apphost and, more usefully, an assertion
that a well-formed binary reports no partial parse at all. That one line is what
would have caught this, and it now covers all six fixtures. The unit case that
pinned the old behavior asserted the bug, so it flips.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(dotnet): measure readers with the shared helper and report refusals

Two things, both about not re-deriving what the campaign already has.

`internal/file.ReaderSize` exists on `main` for exactly this job, and `upx.go`
already uses it. Its doc comment is the problem statement here almost verbatim: a
declared size is worth nothing until you weigh it against a byte count the reader
can actually back, which it does by reading the last byte. This branch was
hand-rolling `Seek(0, io.SeekEnd)` in three places, one of them reimplementing the
helper's save-and-restore dance verbatim, and then absorbing the reader-lies case
downstream by tolerating short reads.

Adopting it means the bundle reader and `readDataFromRVA` now take the reader
types they were always being handed (`unionreader.UnionReader` and `io.ReaderAt`),
and the reads move to `ReadAt`, which leaves the caller's cursor alone and drops
two seeks. Net effect is a smaller diff than what it replaces, and the case the
`shortReader` fake was invented for is now caught up front instead of tolerated:
the fake was modeling a lie about length, which is not what a short squashfs read
actually does. It now models the real shape (honest length, read stops early) and
a second reader covers the lie.

Three places also still returned "there is nothing here" when what they meant was
"I could not tell", which is the thing `File.ParseErr` was added to stop:

- an ELF that `elfutil` refused to parse read as "not an ELF". `ErrDeclaredSizeExceeded`
  is exported precisely so a refusal can be told apart from a broken file, and the
  caller upstream already turns an error into an unknown.
- a `deps.json` past the 3MB cap returned "". That is the file's entire dependency
  list, so this handed back an SBOM that looked complete and silently was not.
- the marker search clamped at 512MB and then reported no bundle. It cannot claim
  there is no bundle when it declined to look everywhere one could be, so a miss
  after clamping is now an error.

`readSizeRecorder` records `ReadAt` alongside `Read`, or the allocation-bound tests
would have quietly stopped measuring anything.

Also renames `findSignatureOffset` back to `findBundleHeaderOffset`, since it
returns a header offset and reading it as "no signature" is what the last bug was,
and fixes the stale `ExtractDepsJSONFromBundle` doc comment.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* test(dotnet): assert allocation sizes directly in the bundle search guards

The PE and ELF search guards asserted the largest read the reader was asked for.
That happened to track the allocation, since the whole buffer goes to ReadAt, but
nothing said so. Both now measure the bytes allocated with the same `measureAlloc`
helper the resource guards use, and keep the read-size check as a separate claim.

Also names the reason a reader can report a size it cannot back: `debug/elf` and
`debug/pe` hand out `io.NewSectionReader(r, 0, 1<<63-1)`, whose `Size()` is nominal
rather than measured, so answering a size is not on its own evidence the bytes are
there. Renames `lyingReader` to `unbackedSizeReader` and regroups the two reader
implementations, which had their methods interleaved.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-11 21:25:10 +00:00
dependabot[bot] 1ca1fa73d3 chore(deps): bump github.com/gpustack/gguf-parser-go (#5274)
Bumps [github.com/gpustack/gguf-parser-go](https://github.com/gpustack/gguf-parser-go) from 0.26.0 to 0.26.3.
- [Release notes](https://github.com/gpustack/gguf-parser-go/releases)
- [Commits](https://github.com/gpustack/gguf-parser-go/compare/v0.26.0...v0.26.3)

---
updated-dependencies:
- dependency-name: github.com/gpustack/gguf-parser-go
  dependency-version: 0.26.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-09-11 21:11:53 +00:00
dependabot[bot] a675c81143 chore(deps): bump modernc.org/sqlite from 1.57.0 to 1.58.0 (#5278)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.57.0 to 1.58.0.
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.57.0...v1.58.0)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.58.0
  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-09-11 21:11:47 +00:00
dependabot[bot] 875daec82b chore(deps): bump github.com/google/go-containerregistry (#5275)
Bumps [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) from 0.22.0 to 0.22.1.
- [Release notes](https://github.com/google/go-containerregistry/releases)
- [Commits](https://github.com/google/go-containerregistry/compare/v0.22.0...v0.22.1)

---
updated-dependencies:
- dependency-name: github.com/google/go-containerregistry
  dependency-version: 0.22.1
  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-09-11 21:11:44 +00:00
Juan fb59c4e65c perf(spdx): make findRootPackages linear instead of packages x relationships (#5280)
Decoding an SPDX document into the syft model was quadratic in document
size. findRootPackages (spdxhelpers/to_syft_model.go), which locates the
packages the document DESCRIBES, scanned every relationship for every
package. On a 119MB SPDX JSON document (70,701 packages, 153,301
relationships) that is 1.1e10 comparisons and 91% of all CPU samples; on a
238MB document it is 4.3e10 comparisons and 95%. A 1.3GB document did not
finish in 91 minutes. Everything else on the decode path is map-indexed and
linear, and spdx/tools-golang's own parse is linear (4.5s and 9.3s for the
two documents above). The scan dates from 9480f10c (top-level SPDX package
support, 2023).

The function now indexes the DESCRIBES / DESCRIBED_BY relationships once,
counting how many times each element is described by the DOCUMENT, then
walks the packages once. Semantics are unchanged: results follow package
order, an element described by more than one edge still appears once per
edge (so the existing "exactly one root" check behaves as before), only
edges touching DOCUMENT count, and nil entries are skipped.

Measured on an Apple M1 Max, `syft convert <spdx> -o <format>=<file>`:

  input       target          before (main)      after
  119MB       spdx-json@2.3   68.2s / 1.3GB      10.4s / 1.3GB
  119MB       spdx-json@2.2   70.2s / 1.3GB      12.6s / 1.3GB
  238MB       spdx-json@2.3   291.2s / 2.3GB     20.9s / 2.6GB
  1.3GB       spdx-json@2.3   killed at 91min    115.5s / 13.6GB
  1.3GB       spdx-json@2.2   not attempted      136.3s / 14.1GB
  1.3GB       syft-json       not attempted      102.6s / 14.1GB
  1.3GB       cyclonedx-json  not attempted      114.1s / 13.1GB

Isolating the conversion step (ToSyftModel) from the tools-golang parse:
119MB 72.3s -> 1.5s, 238MB 265.8s -> 3.1s. The remaining decode time and
all of the memory are in tools-golang, which parses the document several
times over; that is a separate, upstream concern.

Output equivalence was checked by converting 10 SPDX documents (the 8 SPDX
fixtures in this repository plus the 119MB and 238MB documents) to
syft-json, spdx-json@2.3, spdx-json@2.2, spdx-tag-value and
cyclonedx-json@1.6 with both binaries and comparing the results with
timestamps and generated ids removed: all 50 pairs are identical.

Tests pin the original semantics (including the duplicate-edge corner case)
and guard the complexity with a 50k x 100k document that must complete in
under 10s; the old implementation takes about a minute on it.

Signed-off-by: juanjsebgarcia <github@juangarcia.co.uk>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-11 21:11:21 +00:00
Alex Goodman c4800b2393 Fix slow pe tests (#5285)
* test(dotnet): speed up pe fixture tests

the pe package took ~90s under -race, nearly all of it loading docker archive
fixtures. two things:

the three bundle fixtures published with `SelfContained=true`, so each `hello.exe`
carried a full .NET runtime (~60MB, 33MB tar). the bundle format is what those
fixtures exercise and it is identical framework-dependent, so they now publish
with `SelfContained=false` (160KB exe, 2MB tar). the race detector multiplies the
layer inflate + diffID hashing by ~15x, which is why the size mattered so much.

`fixtureFile` also loaded the image and built a squashed resolver per table row,
so `image-net8-app` was loaded four times for four files in it. resolvers are now
built once per distinct fixture from the parent test.

87s -> 28.5s under -race, and ~60MB less fixture cache.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(dotnet): read the right file type from V1 single-file bundles

V1 bundles (.NET Core 3.x) predate the `unknown` member at the head of the
manifest file type enum, so their types are all one less than V2+: deps.json is
2, runtimeconfig.json is 3. we were matching on the V2+ value, which silently
matched runtimeconfig.json instead, so every .NET Core 3.x single-file app
reported its runtimeconfig as the embedded deps.json and its dependencies went
unseen.

the existing fixture test asserted the buggy output (it expected
`runtimeOptions`, which only ever appears in a runtimeconfig). the assertions
now pin the framework moniker out of each fixture's real deps.json, so reading
the neighboring file can't pass.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* test(dotnet): drop the 64MB jruby binary from the net8-app fixture

the `image-net8-app` fixture wget'd a 64MB jruby release binary at build time to
serve as "a PE that is not a .NET binary", which was 64MB of the image's 65MB.
every test that loads that image pays for it, in both the dotnet cataloger and
the pe package, and it sits in the fixture cache twice.

the same jruby binary already has its own fixture in the binary cataloger
(`image-jruby`), asserted through the same `pe.Read`, so the coverage is not
lost. the negative case here now uses `/app/dotnetapp.exe` -- the native apphost
that is already in the image -- which pairs nicely with the single file case,
same apphost with the app bundled in and CLR evidence present.

also drops a github release download from the fixture build.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-11 15:27:20 -04:00
Alex Goodman eb58f698bd Fix oom when running unit tests in CI (#5284)
* fix oom when testing

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* right-size CI test runners

the `r` family and `ram=32+128` were sized for the tmpfs footprint rather
than the workload. with the caches back on disk the build+test peak fits in
16GB, so move to `m`/`c` at `ram=16+32`.

- ~27% cheaper per instance-hour on spot, and a much deeper pool to schedule
  against (~20 x86 types vs 7)
- `cpu=4+8` with `ram=16+32` lets price-capacity-optimized opportunistically
  land an 8 vCPU box when it's cheap
- plain `volume=100gb` replaces the provisioned throughput. a 4 vCPU instance
  caps near 150MB/s of EBS and gp3's free baseline is already 125MB/s, so the
  override bought nothing

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-11 14:58:30 -04:00
Alex Goodman de98400c39 fix(test): drop apt from java-no-main-package fixture (#5283)
the base image is digest-pinned to a bullseye snapshot whose InRelease has
expired, so `apt-get update` now exits 100 on any cold fixture build. rather
than disable Check-Valid-Until (which just delays the next break), skip apt
entirely:

- `less` was never used and `unzip` already ships in the base image
- rezip with `jar cfM` from the base JDK instead of installing `zip`

the `-M` matters here: this fixture intentionally has no META-INF/MANIFEST.MF,
and plain `jar cf` would generate one.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-11 14:44:45 -04:00
Alex Goodman 9e520d1442 fix(test): use immutable URL for dpkg archive fixture (#5282)
`archive.ubuntu.com/ubuntu/pool/main` only keeps the current version of a
package, so the pinned `zlib1g 1:1.3.dfsg-3.1ubuntu2.1` URL started 404ing.
`curl` without `-f` wrote the error page into `zlib1g.deb` and exited 0, so
the fixture image built clean and just contained a non-deb, failing
`TestDpkgArchiveCataloger` only when the fixture cache was cold.

Now points at the launchpad `+files` URL, which is immutable. Also added `-f`
to the other fixture Dockerfiles that curl a pinned artifact so the next bit
of URL rot fails the build instead of quietly poisoning a fixture.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-11 13:45:16 -04:00
dependabot[bot] 3ec2e33d73 chore(deps): bump github.com/klauspost/compress from 1.19.2 to 1.20.0 (#5276)
Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.19.2 to 1.20.0.
- [Release notes](https://github.com/klauspost/compress/releases)
- [Commits](https://github.com/klauspost/compress/compare/v1.19.2...v1.20.0)

---
updated-dependencies:
- dependency-name: github.com/klauspost/compress
  dependency-version: 1.20.0
  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-09-11 14:34:14 +00:00
dependabot[bot] 40a4ec72b9 chore(deps): bump golang.org/x/time from 0.15.0 to 0.16.0 (#5273)
Bumps [golang.org/x/time](https://github.com/golang/time) from 0.15.0 to 0.16.0.
- [Commits](https://github.com/golang/time/compare/v0.15.0...v0.16.0)

---
updated-dependencies:
- dependency-name: golang.org/x/time
  dependency-version: 0.16.0
  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-09-11 13:57:52 +00:00
dependabot[bot] 268897ca82 chore(deps): bump zizmorcore/zizmor-action from 0.6.2 to 0.6.3 (#5270)
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.6.2 to 0.6.3.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/3dc1ecc9bcb9e94e9b2c709687979e1298497054...70fb788f84895a7701f5643d103d587e460b5c99)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.6.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-09-11 13:53:48 +00:00
dependabot[bot] 031de8ddab chore(deps): bump golang.org/x/mod from 0.40.0 to 0.41.0 (#5271)
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.40.0 to 0.41.0.
- [Commits](https://github.com/golang/mod/compare/v0.40.0...v0.41.0)

---
updated-dependencies:
- dependency-name: golang.org/x/mod
  dependency-version: 0.41.0
  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-09-11 13:34:10 +00:00
Alex Goodman 1d25dfe4da fix(golang): bound allocations from UPX-packed binary headers (#5195)
* fix(golang): bound UPX decompression by what the input can justify

A UPX block's `sz_unc` was passed straight to `make([]byte, n)`, so four bytes of
attacker input spanned the full uint32 range. A ~300KB file could drive multi-GB
resident memory, which Go reports as a fatal runtime OOM that no `recover` on this
path can contain. Syft parses binaries out of arbitrary images, so that is reachable
from any scan.

Two bounds do the work, and UPX's own invariants make them exact:

- all blocks together reconstruct `p_filesize`, so a running remainder caps the sum.
  Bounding only per-block would leave the total at (block count x original size), so
  the remainder is the part that actually closes it
- `p_filesize` sizes the output buffer, so it is capped both absolutely and against
  the size of the file on disk. The absolute cap alone left a ~60 byte header able to
  claim 500MB; the ratio alone cannot work either, since LZMA encodes a run of N
  equal bytes in O(log N) and a `go:embed` of 120MB of zeros legitimately packs 209x

The output buffer is also allocated on the first block that survives validation
rather than up front, so a header claiming a large size with nothing decodable behind
it costs nothing.

Alongside that, several ways the block loop could be steered off its own buffer:

- `parseELFPTLoadOffsets` bounds-checked program headers with `phStart+phentsize >
  len(buf)`, which overflows for a `p_offset` near 2^64 and lets an out-of-range entry
  through into a slice index. Switched to the subtraction form, and a `phentsize`
  shorter than an ELF64 program header is rejected since the reads use fixed offsets
- block placement had the same overflow shape, and on failure it skipped the copy and
  fell through. `outputOffset` derives from the rejected offset, so a `p_offset` at the
  uint64 ceiling wrapped it to 0 and the next block landed on the reconstructed ELF
  header, still returning success. Out-of-range placement now ends the block chain,
  keeping the blocks already placed since those often carry `.go.buildinfo`
- the UPX 2-byte LZMA header carries `lc`/`lp` as nibbles and `pb` as three bits, so
  they can hold values LZMA does not permit. Folded into the props byte they wrap
  (`lc=15, lp=15, pb=7` gives 465, which truncates to 209) and the stream mis-decodes
  instead of failing
- blocks decode directly into a slice of the output buffer instead of a per-block
  buffer that is then copied, so one block no longer doubles peak memory
- the block count is capped: the size budget alone still allows millions of 12-byte
  blocks, each spinning up an LZMA reader

Verified against the `image-small-upx` fixture, a real `upx --best --lzma` binary:
four blocks, max `sz_unc` equal to `p_blocksize` exactly, blocks summing to 0.68 of
`p_filesize`, and a 1.96x expansion against the packed file, so every bound holds with
headroom on genuine output.

Note `p_blocksize` is deliberately not used as a ceiling on `sz_unc`. It adds nothing
the remainder does not already cover, and real output sits at exactly `p_blocksize`,
so it would run with zero headroom against a value the format does not promise.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(golang): report UPX binaries we cannot unpack as unknowns

A packed Go binary we fail to unpack means its packages are silently missing from the
SBOM. `getBuildInfo` checked the decompression error for nil and discarded it, so a
rejected file surfaced only the original `buildinfo.Read` error, and that one is
deliberately silenced because it is usually just "not a Go binary".

`decompressUPX` now marks the cases worth reporting with `errUPXDecompress`: it got
past the header and the method dispatch and still could not unpack the file. Everything
else stays quiet, which matters more than it sounds. `upx` defaults to NRV2B unless
`--lzma` is passed and only LZMA is implemented here, so treating any decompression
failure as reportable would attach a golang-cataloger unknown to every packed non-Go
binary in an image. Making the reportable case opt in rather than the quiet case a
growing list also means a guard added later is silent by default.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* refactor(golang): drop the vendored xcoff parser

The only thing the golang cataloger read from it was `TargetMachine`, which is the same
two magic bytes `getGOARCHFromBin` had already matched on to dispatch there. So a full
XCOFF walk (string table, symbol table, every relocation table) ran to recover a value
that was already in hand.

`getGOARCHFromBin` reads those two bytes directly now. That is also strictly more
correct: `xcoff.NewFile` bailed with "no symbol table" when `symptr == 0`, so a stripped
XCOFF binary reported no arch at all.

Fixtures move to `testdata/xcoff/`, since they are no longer a package's own testdata.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* feat(internal/file): report a reader's real size behind one helper

Bounds get written as "does this claim exceed what the reader actually holds", and the
subtlety is that a reader's own answer cannot be trusted: an `io.SectionReader` reports
the nominal length it was built with, so one constructed with `1<<63-1` will happily
claim to hold 8EB. Confirming the last byte is readable is what separates a real size
from a nominal one.

Returns `(int64, bool)` rather than a bare size, since there are five ways to not know
and collapsing them to 0 makes every caller reinvent "0 means stop bounding".

The doc also names the wrapper trap: a type embedding `io.ReaderAt` as an interface
promotes only `ReadAt`, so it answers no size at all however large the reader beneath it
is, and a bound written against it silently does nothing.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(golang): bound every section a reader can expand, not just the name table

`CheckSectionNameTable` bounds the one section `elf.NewFile` expands while parsing, which
is enough for a caller that only parses. It is not enough for one that goes on to read
sections: those are expanded lazily, so a 260KB ELF declaring a compressed `.symtab`
drove 1.3GB of allocation through `goversion`, which opens the file with `debug/elf`
inside its own package and cannot be routed through `elfutil.NewFile`.

`CheckAllSections` is that gate, and is deliberately named as the superset so the
relationship to the narrow one is structural rather than documented.

Also exports `ErrDeclaredSizeExceeded`. A refusal here costs the SBOM a package, so
callers report it as an unknown, and matching on an error string to decide that is not
something to build a reporting policy on.

A ruleguard rule now flags `buildinfo.Read` and `version.ReadExeFromReader` outside
`scan_binary.go`. Both open `debug/elf` internally and are only bounded there by the
wrappers that gate the reader first, which the existing rule on `elf.NewFile` could not
see through.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* feat(internal/spillbuf): sparse offset-addressed buffer that spills to disk

For output that arrives out of order, at offsets the input itself declares: a
decompressor placing extents, an archive rebuilding a file from chunks. A plain `[]byte`
cannot do that without either pre-sizing to a length the input claims or growing to the
furthest offset it names, and both hand a hostile input an allocation knob.

The load-bearing property is that it reports only what it actually stored. `Size` is the
contiguous run written from offset zero and a read past it is `io.EOF`, never the zeros
an unwritten region would hand back for free. Sparse storage serving holes as real
content is what lets a write offset stand in for output nothing produced, so anything
sizing an allocation against this reader (`saferio` in `debug/elf` does exactly that)
stays bounded by work actually done.

`FirstGap` answers where the next write fits, so callers filling holes do not need the
extent bookkeeping and cannot re-derive that rule wrongly, and it only reports gaps
lying entirely inside `[0, within)`. The extent type is unexported for the same reason.

The rest of the contract worth stating: reads and writes after `Close` fail with
`os.ErrClosed` rather than looking like an empty buffer, and a write that fails leaves
the buffer unchanged.

Memory is bounded by the limit rather than by how much is written: filling 1MB and
filling 64MB cost about the same. What lands on disk past that limit is the caller's
bound to set, not this package's. Growth is geometric and capped, because sizing the
tier to exactly what each write needs is quadratic once a caller streams through it in
small chunks.

The golang cataloger is the only consumer today, and nothing else in the tree has this
shape.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(golang): rebuild UPX binaries through spillbuf and read the reconstruction everywhere

Two amplification paths were still reachable. A 128 byte input allocated 16MB by
trusting `sz_cpr` before weighing it against the bytes the file actually holds, and the
reconstruction was assembled in a `[]byte` sized from the header's declared original
size, so a 301KB input allocated 300MB.

The reconstruction is a `spillbuf.Buffer` now, which is what makes the second one go
away: blocks are placed at the offsets the file declares, nothing is pre-sized from a
claim, and the reader's length is the contiguous run actually rebuilt. That last part is
the invariant everything downstream leans on. A reader reporting the declared size would
serve the holes between placed blocks as zeros it never stored, and a 64 byte block
parked at `p_filesize-64` then made a 300KB input hand out a 64MB reader: measured, that
drove 269MB through `getBuildInfo` and returned nothing, silently.

Placement and storage are separate concerns. `upx.go` places blocks at the offsets the
file declares and asks the buffer how much was rebuilt and where the next gap is; it
knows nothing about temp files or memory limits.

The remaining bounds are ratios against the input wherever they can be, since an
absolute ceiling only ever drops a large legitimate binary. The one absolute left is on
`p_filesize`, because padding an input is nearly free, and the LZMA dictionary is paid
for in its own block's `sz_cpr`.

Interface change: `scanFile` takes a context and hands back the reconstruction for each
binary that turned out to be packed, and the caller owns it. Everything after the build
info read (crypto settings, arch, symbols, the version scan) reads it, so a packed
binary no longer reports its packages with no symbols at all.

Also narrows what gets reported. This cataloger runs on every executable in an image, so
reporting every `getBuildInfo` error attached an unknown to every corrupt ELF, odd PE and
non-Go Mach-O slice in it. Now only four gaps, the ones that cost the SBOM something
syft could have had: a packed file we could not decode (`errUPXDecompress`), a header
the size bounds refused (`errUPXSizeRefused`), a reconstruction that came up short
(`errUPXPartial`), and an ELF expansion `elfutil` declined
(`ErrDeclaredSizeExceeded`). An unimplemented UPX method stays quiet on purpose: `upx`
defaults to NRV2B and only LZMA is implemented here, so reporting it would attach an
unknown to most packed non-Go binaries in an image.

That filtering is a new pattern. No other cataloger picks which of its errors become
unknowns, and the reason this one does is that it runs on every executable in an image
rather than on files a glob already selected.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* chore: ignore local spec/ planning notes

`/specs` was already ignored; `/spec` is the same thing under the singular name and was
not, so working notes landed in commits.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* refactor(golang): name what the unpack hands back, not where it stores it

the scan's signatures said `*spillbuf.Buffer` all the way down, which told
every reader below the scan that it cares whether the reconstruction lives in
memory or on disk. it does not. two interfaces instead, each naming only what
its side actually uses:

- `unpackedContents` for the consumers: `ReaderAt`, `Closer`, and a `Size` that
  is the contiguous run rebuilt from zero
- `blockSink` for the decoder, which additionally needs `WriterAt` and
  `FirstGap` to place blocks

this costs one thing worth flagging. `Close` on a nil `*spillbuf.Buffer` was
nil-safe, so callers just deferred it; widened into an interface, a nil pointer
becomes a non-nil interface and the same call panics. `closeUnpacked` absorbs
that, and it is now the only way an owner releases contents. `readerFor`,
`seekerFor` and `closeUnpacked` are the three places that resolve the nil.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* docs(elfutil): say that CheckAllSections is the decompression-bomb gate

the doc led with "bounds every section syft can drive debug/elf into
decompressing", which describes the mechanism without ever naming what it is
defending against. a reader landing on the call site could not tell that this
is the decompression-bomb check.

state the attack instead: a compressed section header declares its own
decompressed size, debug/elf believes that number and allocates it on open, and
nothing forces it to match what the compressed bytes actually yield. the 260KB
ELF that drove 1.3GB of allocation now lives in the doc rather than only at the
one call site that happened to mention it.

comments only.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* docs(golang): move the overflow note onto the bounds test it describes

the "subtraction form" note sat above the phStart assignment with ten lines of
wrap analysis between it and the `if` it was talking about, so the subtraction
it names reads as missing. it is `hdrLen-phStart < phentsize`.

move it onto that test and name the form it is avoiding (`phStart+phentsize >
hdrLen`, which can overflow past the buffer end) so the referent is not several
paragraphs away.

comments only.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* chore(golang): drop the internal/ README left over from xcoff

that README existed to record where the vendored xcoff package came from and
that Go keeps it internal, which is a provenance note worth having while
third-party code sat there. dropping xcoff took the reason with it, and
repurposing it to point at gotestdata just made it redundant: gotestdata has
its own README covering why it is under internal/, why it is not testdata/, and
what belongs in each.

internal/ now holds one self-documenting directory.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-10 09:21:19 -04:00
dependabot[bot] 0c049806bd chore(deps): bump github.com/containerd/containerd/v2 (#5265)
Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.3.4 to 2.3.5.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v2.3.4...v2.3.5)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd/v2
  dependency-version: 2.3.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-10 02:29:44 +00:00
Alex Goodman 9addda3da5 fix(test): pull elf fixture build tools from archive.debian.org (#5264)
bullseye went EOL and deb.debian.org dropped it, so the elf executable
fixture image stopped building and took the whole unit test job with it
(fixture generation runs before any test does). main has been red on this
since 2026-09-08.

two separate breakages, both handled here:

- the base repo moved to `archive.debian.org`, whose Release files are
  permanently expired, so `Acquire::Check-Valid-Until=false` is required

- `bullseye-security` is worse off: its index is still served but every
  package in the pool behind it 404s, and it has not landed on the archive
  yet. dropping it is the only option right now. these are throwaway
  fixture build tools, so base bullseye versions are fine.

verified by rebuilding the image and running the fixture `verify` target,
which still reports `PASS: all security features accounted for`.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-09 10:53:58 -04:00
anchore-oss-update-botandanchore-oss-update-bot 767c83d513 chore(deps): update CPE dictionary index (#5256)
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-09-08 11:22:39 -04:00
dependabot[bot] 392539dc28 chore(deps): bump github.com/CycloneDX/cyclonedx-go (#5252)
Bumps [github.com/CycloneDX/cyclonedx-go](https://github.com/CycloneDX/cyclonedx-go) from 0.11.0 to 0.12.0.
- [Release notes](https://github.com/CycloneDX/cyclonedx-go/releases)
- [Commits](https://github.com/CycloneDX/cyclonedx-go/compare/v0.11.0...v0.12.0)

---
updated-dependencies:
- dependency-name: github.com/CycloneDX/cyclonedx-go
  dependency-version: 0.12.0
  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-09-04 13:30:25 +00:00
dependabot[bot] 3dd674c53b chore(deps): bump anchore/sbom-action from 0.24.0 to 0.24.2 (#5250)
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.24.0 to 0.24.2.
- [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/e22c389904149dbc22b58101806040fa8d37a610...3ad7283483fc7af8ff2b4ea19663c2d5ca935e26)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-version: 0.24.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-09-04 13:29:14 +00:00
dependabot[bot] fd7517bbba chore(deps): bump github.com/google/go-containerregistry (#5251)
Bumps [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) from 0.21.9 to 0.22.0.
- [Release notes](https://github.com/google/go-containerregistry/releases)
- [Commits](https://github.com/google/go-containerregistry/compare/v0.21.9...v0.22.0)

---
updated-dependencies:
- dependency-name: github.com/google/go-containerregistry
  dependency-version: 0.22.0
  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-09-04 13:29:06 +00:00
Rez Moss c935b71c9e fixed java-archive-cataloger returns unknown ver for uber-jars , fixed #5163 (#5164)
Signed-off-by: Rez Moss <hi@rezmoss.com>
2026-09-03 16:09:20 -04:00
dependabot[bot] 77031752fa chore(deps): bump google.golang.org/grpc from 1.83.0 to 1.83.1 (#5245)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.83.0 to 1.83.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.83.0...v1.83.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.83.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-02 08:27:04 +00:00
Vaibhav Srivastava 12d5544b2a docs: fix typo implmentation -> implementation (#5243)
Signed-off-by: Vaibhav Srivastava <vaibhavsri1712@gmail.com>
2026-08-31 17:21:05 +00:00
anchore-oss-update-botandanchore-oss-update-bot cc13d1dbf1 chore(deps): update CPE dictionary index (#5244)
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-08-31 13:03:10 -04:00
anchore-oss-update-botandanchore-oss-update-bot af937d74c0 chore(deps): update tool versions (#5242)
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-08-29 10:24:02 +00:00
dependabot[bot] 4017979670 chore(deps): bump docker/setup-buildx-action from 4.2.0 to 4.3.0 (#5237)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/bb05f3f5519dd87d3ba754cc423b652a5edd6d2c...37fe631027851001ddb9b187196cc803df7f5f0e)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.3.0
  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-08-28 13:33:27 +00:00
dependabot[bot] 82d273fac5 chore(deps): bump github.com/gpustack/gguf-parser-go (#5239)
Bumps [github.com/gpustack/gguf-parser-go](https://github.com/gpustack/gguf-parser-go) from 0.25.0 to 0.26.0.
- [Release notes](https://github.com/gpustack/gguf-parser-go/releases)
- [Commits](https://github.com/gpustack/gguf-parser-go/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: github.com/gpustack/gguf-parser-go
  dependency-version: 0.26.0
  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-08-28 13:32:35 +00:00
dependabot[bot] 1417c1989b chore(deps): bump modernc.org/sqlite from 1.56.0 to 1.57.0 (#5238)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.56.0 to 1.57.0.
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.56.0...v1.57.0)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.57.0
  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-08-28 13:31:08 +00:00
anchore-oss-update-botandanchore-oss-update-bot 91a0032987 chore(deps): update anchore dependencies (#5085)
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-08-27 12:25:09 -04:00
Ankush Pathak f91bf4528b fix: correct Apache Derby group ID in purl generation (#5090)
Signed-off-by: Ankush Pathak <ankush.pathak@chainguard.dev>
2026-08-27 10:55:31 -04:00
anchore-oss-update-botandanchore-oss-update-bot c5fc6998d3 chore(deps): update CPE dictionary index (#5221)
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-08-27 09:30:58 -04:00
Pujitha PaladuguandPujitha Paladugu d3734ddeb2 fix(binary): detect grafana security-patch release versions (#5213)
---------
Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>
Co-authored-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>
2026-08-27 04:20:09 +00:00
Matt Van HornandChristopher Phillips bf82010f3c fix(lua): skip rockspec with no package name (#4825)
---------
Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2026-08-21 17:36:47 -04:00
Pujitha PaladuguandPujitha Paladugu 7ca1f22395 fix(dotnet): correct inverted dependency-of relationship direction in packages.lock.json parser (#5143)
Fixes #5125

Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>
Co-authored-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>
2026-08-21 18:53:45 +00:00
Sueun ChoandChristopher Phillips 93cf89363b fix(rpm): keep the epoch when parsing RPM manifest packages (#5201)
---------

Signed-off-by: Sueun Cho <sueun.dev@gmail.com>
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2026-08-21 14:47:23 -04:00
dependabot[bot] 34ef7dcbfe chore(deps): bump golang.org/x/mod from 0.39.0 to 0.40.0 (#5208)
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.39.0 to 0.40.0.
- [Commits](https://github.com/golang/mod/compare/v0.39.0...v0.40.0)

---
updated-dependencies:
- dependency-name: golang.org/x/mod
  dependency-version: 0.40.0
  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-08-21 13:50:49 +00:00
dependabot[bot] 29edf90038 chore(deps): bump github.com/hashicorp/go-getter from 1.8.6 to 1.8.8 (#5207)
Bumps [github.com/hashicorp/go-getter](https://github.com/hashicorp/go-getter) from 1.8.6 to 1.8.8.
- [Release notes](https://github.com/hashicorp/go-getter/releases)
- [Changelog](https://github.com/hashicorp/go-getter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/go-getter/compare/v1.8.6...v1.8.8)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/go-getter
  dependency-version: 1.8.8
  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-08-21 13:50:46 +00:00
dependabot[bot] 766907e6ed chore(deps): bump github.com/stretchr/testify from 1.11.1 to 1.12.0 (#5206)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.11.1 to 1.12.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.11.1...v1.12.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-version: 1.12.0
  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-08-21 13:29:57 +00:00
dependabot[bot] e820f2d21a chore(deps): bump golang.org/x/tools from 0.48.0 to 0.49.0 (#5205)
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.48.0 to 0.49.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.48.0...v0.49.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-version: 0.49.0
  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-08-21 13:29:25 +00:00
Alex Goodman 39c9c690df fix: move image hardlink handling upstream during image indexing (#5196)
* chore(deps): pin stereoscope to the hardlink-inode-adoption branch

temporary pin to anchore/stereoscope#670 so the hardlink changes can be
exercised end to end. needs re-pinning to a release tag before merge.

pulls transitive bumps along with it: docker/cli, docker/go-connections and
gabriel-vasile/mimetype.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix: drop the image resolver hardlink workaround

stereoscope now adopts a hardlink's target at index time, so a hardlinked name
arrives already described as a regular file with the target's size, mime type
and content. the resolver-side fix from #5029 sat on top of that doing the same
job a second time, gated on a file type that no longer shows up, so both
`resolveHardLinkTarget` implementations and the extra tree walk they cost on
every `FilesByPath` are gone.

`file.NewVirtualLocationFromImage` stays as-is. it is exported and syft is v1,
so it keeps working for anyone using it, it just has no callers in syft now.

this picks up `FilesByMIMEType`, which #5029 explicitly could not fix, so
hardlinked names now reach mime-driven catalogers. on images built around
multi-call binaries (busybox and friends) that is a lot more file and executable
entries than before; distro and toolchain images move by about one entry.

one shape regresses: a hardlink stereoscope could not adopt (a link name that is
absent, empty, names a directory, names another un-adopted link, or points into
a lower layer) keeps `TypeHardLink`, and syft goes back to collapsing it onto
its target's path. no mainstream builder emits the cross-layer case.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* bump stereoscope to main

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-08-20 20:17:41 +00:00
addielarue c4597a08a2 fix(file-resolver): don't abort scan on inaccessible symlink target (#5170)
Signed-off-by: addielarue <bhasmerutika@gmail.com>
2026-08-20 16:16:45 -04:00
Christopher Angelo Phillips 236755a6b1 fix(binary): gzip classifier & GNU gzip identity string (#5202)
---------
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2026-08-20 16:02:14 -04:00
Alex Goodman 168eb64235 fix(javascript): pass a yaml node to the pnpm v5 collision test (#5203)
#5188 changed `pnpmV6LockYaml.Parse` to take a `*yaml.Node` and #5175 added
this test against the old `[]byte` signature. both were green on their own
branches, so main does not compile.

the v9 and v6 cases in the same test already go through the `yamlDocument`
helper, this is just the v5 one that was missed.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-08-20 16:01:52 -04:00
Xenira 490732aa58 fix(cpe): fix generated cpe for rust packages
---------
Signed-off-by: Xenira <1288524+Xenira@users.noreply.github.com>
2026-08-20 15:01:24 -04:00
Sueun Cho fd4796b0d8 fix(python): keep epoch-pinned requirements in the SBOM (#5161)
parsePinnedVersion rejected any version constraint containing "!" to
skip the "!=" exclusion operator. PEP 440 epochs use "!" as well
(e.g. "1!2.0.0"), so an exact pin like "pkg == 1!2.0.0" was treated as
unparseable and returned "". parseRequirementsTxt drops a requirement
whose version resolves to "", so the package never made it into the
SBOM at all in either the pinned or guessed path.

Check for the "!=" operator as a substring instead of the bare "!"
character, which leaves the epoch separator alone.

Signed-off-by: Sueun Cho <sueun.dev@gmail.com>
2026-08-20 13:06:17 +00:00
78f39ee3a5 fix(javascript): strip underscore peer-dep suffixes from pnpm v5 lockfile versions (#5175)
pnpm v5 lockfiles encode resolved peer dependencies as an
underscore-delimited suffix of the dependency path, e.g.
/acorn-jsx/5.3.2_acorn@8.8.0 or a hashed form like
/webpack-cli/4.10.0_fzn43tb6bdtdxy2s3aqevve2su. Only the v6+
parenthesized form was stripped, so for v5 lockfiles the suffix leaked
into reported versions and PURLs, and peer variants of the same
name+version were not deduplicated.

Strip the suffix on the v5 code path (version < 6.0) wherever a version
is extracted: top-level dependencies values, packages keys, and
per-package dependencies map values. Only values with a leading digit
are stripped so link:/file:/git specifiers are unaffected.

Fixes #5174

Signed-off-by: codeAnqiang-ma <273298913+codeAnqiang-ma@users.noreply.github.com>
Co-authored-by: codeAnqiang-ma <273298913+codeAnqiang-ma@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 08:40:18 -04:00
5c6cf08a8c fix(binary): detect multi-arch ingress-nginx (#5179)
* fix(binary): detect multi-arch ingress-nginx

Add v1.9.6 ARM64, ARMv7, and s390x fixtures from the published controller image and match their version marker against the nearby Go build version.

Assisted-by: OpenAI Codex
Signed-off-by: Shurong Cao <170531907+CAOShurong@users.noreply.github.com>

* fix(binary): match ingress-nginx release marker on all arches

The release is injected with `-ldflags -X`, so it lands in its own aligned,
NUL-padded data symbol. Only that padding is portable -- the surrounding bytes
are an arch-specific float constant pool, which is why the existing matchers all
anchor on incidental junk like `$a` and `S=v<y5` and only ever worked on amd64.

Matching `v<version>` followed by two NULs finds exactly one hit in every v1.9.6
binary (amd64, arm, arm64, s390x) and in all nine amd64 releases already under
test. Two NULs matter -- with one, s390x matches a vendored `v1.19.0` earlier in
the file and reports the wrong version.

This also drops the ~8KB wildcard window the marker previously needed, so the new
fixtures are ordinary 369 byte snippets instead of 8.4KB ones.

Two things in the fixture tooling had to move for that to work:

- `config.yaml` pinned the multi-arch *index* digest for all four platforms, but
  extraction runs `docker create <ref>` with no `--platform`, so every entry
  resolved to whichever image was pulled last. `make download` failed outright on
  `linux/arm/v7`, since the registry labels that platform `armv7` and docker
  normalizes it to `arm`. Now pinned per-platform, matching the redis entry.

- `write-snippet` only recognized EM_X86_64 and EM_AARCH64, so anything else
  landed in a `linux-unknown-<hex>` directory and had to be placed by hand.

Verified against the real binaries with `-must-use-original-binaries`.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(binary): put non-amd64 snippets where the tests look for them

`write-snippet` used to fall back to `unknown-<hex>` for any ELF machine it
didn't recognize, so three snippets were sitting in directories nothing reads:

    helm/3.12.0/linux-unknown-454d5f53333930      (hex of "EM_S390")
    helm/4.1.4/linux-unknown-454d5f53333930
    redis-server/7.2.5/linux-unknown-454d5f333836 (hex of "EM_386")

The test table asks for `linux-s390x` and `linux-386`, so those three cases were
quietly falling through to downloading the full binaries instead. Renamed to
match, and taught the tool about EM_386 and EM_RISCV so it stops producing dead
directories (there is an existing `linux-riscv64` snippet it also couldn't have
written).

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Shurong Cao <170531907+CAOShurong@users.noreply.github.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Shurong Cao <170531907+CAOShurong@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-08-19 18:15:15 +00:00
Hamody WeandAlex Goodman 8bb3b5eed1 fix(javascript): read every document of a pnpm-lock.yaml stream (#5188)
* fix(javascript): read every document of a pnpm-lock.yaml stream

pnpm-lock.yaml can be a multi-document YAML stream. pnpm keeps config
dependencies and the pinned package-manager version in a leading document
and the project's dependency graph in the next one. That layout has
shipped since pnpm 11 for projects using config dependencies, and pnpm 12
records the package-manager pin by default, so two documents is now the
common case.

The cataloger called yaml.Unmarshal, which reads the first document only.
On such a lockfile the SBOM contains pnpm's own release binaries and none
of the project's dependencies. Both documents declare the same
lockfileVersion and the result is well-formed and non-empty, so nothing
signals that it is wrong.

Split the stream and parse each document, merging the results. A
single-document lockfile yields exactly one document, so its behaviour is
unchanged. A document that omits lockfileVersion falls back to the
version from the first one rather than being dropped.

Fixes #5168

Signed-off-by: hamodywe <iosapk.org@gmail.com>

* refactor(javascript): stream pnpm-lock.yaml documents instead of buffering

The multi-document fix read the whole lockfile, decoded it to a `yaml.Node`
tree, re-marshaled every document back to `[]byte` and then re-parsed each one
twice. `yaml.Node` already has a `Decode` method, so the decoder can walk the
stream straight off the reader and hand each document's node to the parser.

On a 4.8MB single-document lockfile that takes total allocations from 1069MB to
250MB, which is also below where things sat before multi-document support. It
drops the `io.ReadAll` and its `noUnboundedReads` suppression, and puts pnpm in
line with every other YAML cataloger in the tree, all of which already stream.

`pnpmLockfileParser.Parse` now takes a `*yaml.Node` rather than `[]byte`. It is
package-private with two implementations and one production call site.

Documents accumulate into a single map keyed by `name@version`, so the stream
follows one collision rule rather than two: the last entry to appear wins,
which is what already happens within a document. That replaces the separate
first-wins-with-fill-in merge pass. A conflicting integrity between documents
is now traced rather than resolved silently.

Three behaviour fixes come with it:

- an empty or comment-only leading document no longer takes down the whole
  file. It decodes to a null node, not a zero-kind one, so the old guard never
  fired and the stream failed with `invalid lockfile version ""`.
- a malformed document no longer discards the documents that already parsed.
  It is reported through `unknown` instead, matching how the rest of the
  cataloger reports partial results.
- parse errors carry the document index, and their line numbers again point at
  the user's file rather than into a re-marshaled buffer.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* chore(javascript): trim the duplicated pnpm multi-document comment

The six-line explanation of why a pnpm lockfile can hold two documents sat
verbatim in both the parser and its test. Keep it next to the code, where
someone debugging will be, and leave the test pointing at it.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: hamodywe <iosapk.org@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-08-19 18:09:51 +00:00
Oleksandr Vodotiiets 21cca30e5b perf: make orderedIDSet membership checks constant time (#5178)
orderedIDSet.add scanned the entire existing slice for every inserted ID,
so building a Collection was quadratic in the number of packages sharing an
index key. This is most visible in idsByType: package types are few, so that
index accumulates nearly every package in the SBOM into a single set, and
each Add rescans it.

Keep the ordered slice (ordering and dedup semantics are unchanged) and add
a hash index for membership checks. The index is only built once a set grows
beyond a small threshold, so the many single-element sets held by idsByName
and idsByPath do not pay for a map, while the large idsByType sets get
constant-time lookups.

BenchmarkCollectionAdd (Apple M2 Pro), packages sharing one type:

    packages   before        after       speedup
    1,000        1.37 ms      0.76 ms      1.8x
    10,000     100.31 ms      9.80 ms     10.2x
    50,000   2,423.79 ms     34.46 ms     70.3x

The tradeoff is memory: sets past the threshold allocate an index map,
about 14% more bytes at 50,000 packages, with allocation counts unchanged.

Signed-off-by: Oleksandr Vodotiiets <61548316+avodotiiets@users.noreply.github.com>
2026-08-19 18:01:20 +00:00
Keith Zantow ed76e96749 fix: panic when scanning squashfs symlinks (#5119)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
2026-08-19 13:13:42 -04:00
Luan Taraschi ab508169e6 fix(golang): skip remote license lookup for standard library module paths (#5192)
With search-remote-licenses enabled, every module name went to the proxy,
including toolchain binaries whose main module is synthesized from the
package path, such as cmd/cgo. The proxy answers 404, and the direct
fallback then treats the path as a repository host, producing requests
like https://cmd/cgo/info/refs?service=git-upload-pack.

Reuse isStandardImportPath, already in this package, to skip the remote
search for paths whose first element carries no dot. Those are never
publishable module paths, so neither a proxy nor a repository can resolve
them.

Fixes #3149

Signed-off-by: Luan Taraschi <130802253+luantaraschi@users.noreply.github.com>
2026-08-19 10:11:10 -04:00
anchore-oss-update-botandanchore-oss-update-bot 360dbc04aa chore(deps): update CPE dictionary index (#5189)
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-08-18 11:44:00 -04:00
Alex Goodman 58a033f924 Prevent unnecessary allocations when parsing compressed ELF sections (#5187)
* fix(elf): bound compressed ELF section reads

`debug/elf` takes a section's decompressed size from that section's own
compression header, and a highly compressible stream really does deliver the
bytes that header promises, so `internal/saferio` does not help: it faithfully
allocates every one of them. A 2MB input file drives `elf.NewFile` to allocate
over 10GB and return no error, which is a fatal OOM rather than a recoverable
panic.

Which sections get read is not up to the caller. `elf.NewFile` always reads the
section-name string table, and `File.Symbols` reads `.symtab` plus whatever
section its `Link` field points at, so being selective about sections is not
enough to avoid it.

New `elfutil.NewFile` is a drop-in for `elf.NewFile` that rejects a declared
decompressed size over 128MB. Every production call site goes through it, and a
ruleguard rule keeps the next one from going direct.

The check runs in two parts, since `debug/elf` expands sections at two different
times. The section-name string table is the only one `elf.NewFile` expands
itself, so it is checked against the raw bytes before the call; everything else
is expanded lazily by `(*Section).Open` and is checked after the parse, where
names, types and decompressed sizes are already resolved.

Only the sections syft can actually reach are bounded, which keeps the guard
from costing real binaries. DWARF is excluded since nothing calls `File.DWARF`,
so a large compressed `.debug_info` no longer skips the whole file, and sections
`debug/elf` will not decompress anyway (`SHF_ALLOC`, `SHT_NOBITS`) are left
alone. The legacy `.zdebug` form is matched on the section name the way
`debug/elf` gates it rather than on the `ZLIB` magic, so an ordinary section
starting with those four bytes is not mistaken for a compressed one.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(elf): gate debug/buildinfo behind the compressed-section check

`debug/buildinfo.Read` opens ELF files with `debug/elf` itself, and `elf.NewFile`
expands the section-name string table as it parses, so the golang cataloger was
still reachable by the same bomb `elfutil` exists to stop. A 261KB fixture drove
1.4GB of allocation through `buildinfo.Read` and returned no error.

`elfutil.CheckSectionNameTable` is now exported for that case: callers that cannot
use `NewFile` because the `debug/elf` call is made for them inside another package.
Both `buildinfo.Read` call sites go through it, including the UPX-decompressed one.

Also corrects claims that did not hold up:

- the package doc's 2MB-to-10GB figure is not reachable with zlib (~1000:1), so it
  now carries the measured 510KB-to-2.6GB, and names zstd's 32767:1 since that is
  what makes the small inputs possible

- `.go.buildinfo` was listed as a hot-path section elfutil covers, but it is read
  through `debug/buildinfo` and never touches `Section.Data`

- the graalvm comment claimed routing size rejections away from `*elf.FormatError`
  improved reporting; both branches are skipped by the caller and only the
  FormatError branch logs, so it did the opposite

- `sharedLibraries` logged short and truncated files as real ELF failures, since
  `debug/elf` returns a bare `io.EOF` rather than an `*elf.FormatError` for those

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* added test comments around the negative cases

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* additional tests

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* better decomposition and comments

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* use a less brittle constant for error detection

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-08-14 20:08:58 +00:00
Alex Goodman 04a6fa1b41 fix(unionreader): honor the io.ReaderAt contract in readerAtAdapter (#5186)
`readerAtAdapter.ReadAt` seeks and then issues a single `Read`, which breaks the
`io.ReaderAt` contract in both directions against the squashfs reader it exists
to wrap:

- `squashfs.File.Read` copies against the decompressed block length but advances
  its block cursor by the nominal block size, so a block that decompresses short
  silently stops copying and returns fewer bytes with a nil error. `ReadAt`
  forbids that, and callers rely on it: anything decoding a fixed-size structure
  off the result gets zero padding it has no way to detect and parses it as real
  data. The GraalVM PE export table and the UPX block reader both size a buffer
  from a header field and then ignore `n` entirely, so a crafted image drives
  them straight through the padding.

- a read landing exactly on the end of the file returns a *full* buffer paired
  with `io.EOF`. `bytes.Reader.ReadAt` returns nil there, and the callers that
  treat any error as fatal were written against that, so squashfs-resident
  binaries sized near a read boundary were being skipped outright.

`io.ReadFull` normalizes both: it fills the buffer across short reads, and it
clears the error once the buffer is full. A genuinely short tail is reported as
`io.EOF`, which is what `ReadAt` implementations return at the end of a file, and
what the buffering branch of `GetUnionReader` already returns.

Affects squashfs-backed sources (snaps), so in practice the binary catalogers
reading structure out of executables.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-08-14 14:09:10 -04:00
dependabot[bot] d63c774fa9 chore(deps): bump modernc.org/sqlite from 1.55.0 to 1.56.0 (#5183)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.55.0 to 1.56.0.
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.55.0...v1.56.0)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.56.0
  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-08-14 13:34:28 +00:00
dependabot[bot] 3eee102f63 chore(deps): bump github.com/google/go-containerregistry (#5184)
Bumps [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) from 0.21.7 to 0.21.9.
- [Release notes](https://github.com/google/go-containerregistry/releases)
- [Commits](https://github.com/google/go-containerregistry/compare/v0.21.7...v0.21.9)

---
updated-dependencies:
- dependency-name: github.com/google/go-containerregistry
  dependency-version: 0.21.9
  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-08-14 13:34:24 +00:00
dependabot[bot] 20cc3df387 chore(deps): bump zizmorcore/zizmor-action from 0.6.1 to 0.6.2 (#5181)
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.6.1 to 0.6.2.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/6fc4b006235f201fdab3722e17240ab420d580e5...3dc1ecc9bcb9e94e9b2c709687979e1298497054)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.6.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-08-14 13:33:58 +00:00
dependabot[bot] 4782a94029 chore(deps): bump github.com/klauspost/compress from 1.19.1 to 1.19.2 (#5182)
Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.19.1 to 1.19.2.
- [Release notes](https://github.com/klauspost/compress/releases)
- [Commits](https://github.com/klauspost/compress/compare/v1.19.1...v1.19.2)

---
updated-dependencies:
- dependency-name: github.com/klauspost/compress
  dependency-version: 1.19.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-08-14 13:33:38 +00:00
Weston Steimel 2293641e3b fix: add correct CPE vendor/product candidates for Git for Windows PE binary (#5156)
Signed-off-by: Weston Steimel <author@code.w.steimel.me.uk>
2026-08-10 10:26:29 -04:00
anchore-oss-update-botandanchore-oss-update-bot 9dd9ce0bdd chore(deps): update CPE dictionary index (#5166)
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-08-10 10:24:51 -04:00
Ankit 949ac70369 fix(java): map legacy Jackson 1.x (-asl) artifacts to org.codehaus.jackson (#5146)
jackson-mapper-asl, jackson-core-asl, and their sibling artifacts
(jackson-jaxrs, jackson-xc, jackson-smile) predate the convention of
embedding META-INF/maven/.../pom.properties in the jar (they were
built with Ant before ~2014). With no POM metadata to read, syft's
groupIDFromJavaMetadata falls through to using the artifact name
itself as the group ID, e.g.

  pkg:maven/jackson-mapper-asl/jackson-mapper-asl@1.9.13

instead of the correct

  pkg:maven/org.codehaus.jackson/jackson-mapper-asl@1.9.13

(confirmed against the published POM on Maven Central for all five
artifacts). Because the generated purl's namespace doesn't match the
vulnerability database's namespace for these packages, this causes
false negatives in downstream scanning (e.g. Grype cannot match known
CVEs such as CVE-2019-10202 against jackson-mapper-asl).

Add the five artifacts to DefaultArtifactIDToGroupID, the same known-
package-list fallback already used for other jars with incomplete
metadata (e.g. the existing ant-*, spring-ldap* entries).

Fixes #4598

Signed-off-by: ankit090701 <ankitanku090701@gmail.com>
2026-08-07 14:38:14 +00:00
Gunny Patel 07fb23487f feat(golang): detect native Go FIPS 140 mode in binaries (#5155)
Signed-off-by: Gunny Patel <zip159@gmail.com>
2026-08-07 14:36:09 +00:00
Timo f45586b457 Remove Go Report Card badge from README (#5130)
Signed-off-by: Timo <57227498+EchterTimo@users.noreply.github.com>
2026-08-07 10:31:27 -04:00
dependabot[bot] 0fe98ed1f2 chore(deps): bump github.com/go-git/go-git/v5 from 5.19.1 to 5.19.2 (#5159)
Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.19.1 to 5.19.2.
- [Release notes](https://github.com/go-git/go-git/releases)
- [Changelog](https://github.com/go-git/go-git/blob/main/HISTORY.md)
- [Commits](https://github.com/go-git/go-git/compare/v5.19.1...v5.19.2)

---
updated-dependencies:
- dependency-name: github.com/go-git/go-git/v5
  dependency-version: 5.19.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-08-07 13:32:31 +00:00
dependabot[bot] 5f67883fe8 chore(deps): bump go.yaml.in/yaml/v3 from 3.0.4 to 3.0.5 (#5158)
Bumps [go.yaml.in/yaml/v3](https://github.com/yaml/go-yaml) from 3.0.4 to 3.0.5.
- [Commits](https://github.com/yaml/go-yaml/compare/v3.0.4...v3.0.5)

---
updated-dependencies:
- dependency-name: go.yaml.in/yaml/v3
  dependency-version: 3.0.5
  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-08-07 13:31:31 +00:00
dependabot[bot] d288dd67ae chore(deps): bump docker/login-action from 4.5.1 to 4.6.0 (#5157)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.5.1 to 4.6.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/abd2ef45e78c5afb21d64d4ca52ee8550d9572c7...dbcb813823bdd20940b903addbd779551569679f)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.6.0
  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-08-07 13:30:56 +00:00
Alex Goodman da745b13e8 feat(golang): add extended-stdlib scope and module patterns for symbol capture (#5154)
* feat(golang): add extended-stdlib scope and include patterns for symbol capture

`golang.capture-symbols` decides how much symbol data lands in the SBOM for grype's reachability analysis. It's `none`, `stdlib`, or `all` today, and the useful middle is missing: `stdlib` stops at the standard library, `all` multiplies SBOM size.

A new `extended-stdlib` configurable covers stdlib plus everything under `golang.org/x/`:

```yaml
golang:
  capture-symbols: extended-stdlib
```

Also, a new `capture-symbols-include` configurable for modules that are noisy in your binaries but not everyone's. It's unioned with whatever the scope selects, so it only ever widens:

```yaml
golang:
  capture-symbols: extended-stdlib
  capture-symbols-include:
    - github.com/klauspost/**
```

Patterns are standard doublestar globs, which matters because module paths carry `/v2`-style suffixes:

```yaml
golang:
  capture-symbols-include:
    - github.com/klauspost/*     # compress, but not compress/v2
    - github.com/klauspost/**    # both
    - k8s.io/client-go           # exact match only
```

Ordering is `none` < `stdlib` < `extended-stdlib` < `all`. The existing three values
and the `none` default are unchanged, and the include list is inert under `none`.
Presets compile into glob lists internally, so a single matcher answers "does this
module get symbols" instead of a preset branch sitting next to a separate glob branch.

An unrecognized `capture-symbols` value still falls back to `none`, but warns now
instead of doing it silently. A malformed include pattern warns and gets skipped.

One thing worth a look beyond the feature: the `Symbols` field description in the JSON
schema was wrong after this (it claimed only `all` and `stdlib` populate anything), and
that description lives in the already-published `16.1.10`. Rather than bump a version for
a sentence, `16.1.10` is amended in place and `schema/json/README.md` grows an explicit
exception for description-only changes: descriptions only, no shape change of any kind,
`$id` unchanged. Anything else still needs a bump. Happy to split that into its own PR if
you'd rather review the policy separately.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* refactor(golang): rename capture-symbols-include to capture-symbols-modules

The key's entries are go module paths, and `-include` sitting next to `capture-symbols` reads as plausibly taking symbol or package names instead. Those spellings parse and match nothing, which is quieter than the confusion `-include` was picked to avoid, so the name now says what the list holds.

`golang.CatalogerConfig.CaptureSymbolsModules` and `WithCaptureSymbolsModules` rename with it. Nothing behavioral changes; the key is new in this PR so there is no compatibility surface.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* feat(golang): match capture-symbols-modules across major version suffixes

`github.com/anchore/*` covered `github.com/anchore/syft` and silently stopped covering it the day it became `github.com/anchore/syft/v2`. The config keeps parsing, nothing warns, and symbols quietly go missing from the SBOM. Exact paths had the same hole: `github.com/klauspost/compress` did not cover `compress/v2` either, so no spelling short of `**` survived a major bump.

A major version suffix is part of a module's path but not part of its identity, so patterns are now matched against the module path both with and without it, using `module.SplitPathVersion` from `golang.org/x/mod` (already a direct dep, already used in this package for `PseudoVersion`).

```yaml
golang:
  capture-symbols-modules:
    - github.com/klauspost/*          # compress and compress/v2
    - github.com/klauspost/compress   # same module at every major version
    - github.com/klauspost/compress/v2  # v2 alone
```

Only a trailing suffix is a version, which is Go's own rule. In `github.com/anchore/syft/v2/thing` the `v2` is an ordinary path element naming a major subdirectory a nested module lives in, so it stays literal and `github.com/anchore/**/thing` is how you reach it. `/v0` and `/v1` are not valid suffixes and are left alone.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-08-07 12:44:42 +00:00
Alex Goodman 68da404bd7 fix(make): don't let ambient RACE leak into raceEnabled test (#5152)
The two "RACE is unset" cases only skipped the t.Setenv call, so they
inherited whatever RACE was in the environment. Running `make test` with
RACE=false exported job-wide flipped the CI-default case and failed.

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-08-05 10:00:17 -04:00
Alex Goodman 35bf33bcf0 fix(make): explicit integration test timeout + single race switch (#5151)
with a cold fixture cache the integration suite builds and saves 18 docker
images across 36 sequential tests, which walks past `go test`'s default 10m
timeout and takes the fixture cache rebuild down with it. that suite now runs
`go test` directly with `-timeout=30m` (gotest.Tasks() has no timeout option),
plus `-count=1` since the built fixtures are the side effect we're actually
after and a test cache hit would skip producing them.

also adds `RACE` as one switch for the race detector across every suite:

- `RACE=false make test` drops `-race` from unit + integration and skips the
  race smoke, worth doing on a cache rebuild where the wall clock is all
  docker builds anyway
- `RACE=true` forces it on locally
- unset behaves as before: on in CI, off locally and on windows

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-08-04 10:46:35 -04:00
anchore-oss-update-botandanchore-oss-update-bot 7f73d6a603 chore(deps): update CPE dictionary index (#5148)
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-08-03 13:20:36 -04:00
dependabot[bot] ef40eec38d chore(deps): bump github.com/klauspost/compress from 1.19.0 to 1.19.1 (#5140)
Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.19.0 to 1.19.1.
- [Release notes](https://github.com/klauspost/compress/releases)
- [Commits](https://github.com/klauspost/compress/compare/v1.19.0...v1.19.1)

---
updated-dependencies:
- dependency-name: github.com/klauspost/compress
  dependency-version: 1.19.1
  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-07-31 13:50:21 +00:00
dependabot[bot] 80212d87c2 chore(deps): bump github.com/go-git/go-billy/v5 from 5.9.0 to 5.9.1 (#5139)
Bumps [github.com/go-git/go-billy/v5](https://github.com/go-git/go-billy) from 5.9.0 to 5.9.1.
- [Release notes](https://github.com/go-git/go-billy/releases)
- [Commits](https://github.com/go-git/go-billy/compare/v5.9.0...v5.9.1)

---
updated-dependencies:
- dependency-name: github.com/go-git/go-billy/v5
  dependency-version: 5.9.1
  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-07-31 13:35:46 +00:00
dependabot[bot] b14123ecc1 chore(deps): bump github.com/magiconair/properties (#5141)
Bumps [github.com/magiconair/properties](https://github.com/magiconair/properties) from 1.8.10 to 1.18.11.
- [Release notes](https://github.com/magiconair/properties/releases)
- [Commits](https://github.com/magiconair/properties/compare/v1.8.10...v1.18.11)

---
updated-dependencies:
- dependency-name: github.com/magiconair/properties
  dependency-version: 1.18.11
  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-07-31 13:34:20 +00:00
dependabot[bot] e00aeb16e4 chore(deps): bump github.com/ulikunitz/xz from 0.5.15 to 0.5.16 (#5142)
Bumps [github.com/ulikunitz/xz](https://github.com/ulikunitz/xz) from 0.5.15 to 0.5.16.
- [Commits](https://github.com/ulikunitz/xz/compare/v0.5.15...v0.5.16)

---
updated-dependencies:
- dependency-name: github.com/ulikunitz/xz
  dependency-version: 0.5.16
  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-07-31 13:32:58 +00:00
dependabot[bot] fa6f6d70f0 chore(deps): bump modernc.org/sqlite from 1.54.0 to 1.55.0 (#5138)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.54.0 to 1.55.0.
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.54.0...v1.55.0)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.55.0
  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-07-31 13:29:04 +00:00
dependabot[bot] 0975d80dc0 chore(deps): bump github.com/jedib0t/go-pretty/v6 from 6.8.2 to 6.8.3 (#5137)
Bumps [github.com/jedib0t/go-pretty/v6](https://github.com/jedib0t/go-pretty) from 6.8.2 to 6.8.3.
- [Release notes](https://github.com/jedib0t/go-pretty/releases)
- [Commits](https://github.com/jedib0t/go-pretty/compare/v6.8.2...v6.8.3)

---
updated-dependencies:
- dependency-name: github.com/jedib0t/go-pretty/v6
  dependency-version: 6.8.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-07-31 13:28:49 +00:00
dependabot[bot] 4d823eaaa7 chore(deps): bump github.com/diskfs/go-diskfs from 1.9.3 to 1.9.4 (#5136)
Bumps [github.com/diskfs/go-diskfs](https://github.com/diskfs/go-diskfs) from 1.9.3 to 1.9.4.
- [Commits](https://github.com/diskfs/go-diskfs/compare/v1.9.3...v1.9.4)

---
updated-dependencies:
- dependency-name: github.com/diskfs/go-diskfs
  dependency-version: 1.9.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-07-31 13:27:56 +00:00
dependabot[bot] 138e42b428 chore(deps): bump zizmorcore/zizmor-action from 0.6.0 to 0.6.1 (#5134)
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.6.0 to 0.6.1.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/6599ee8b7a49aef6a770f63d261d214911a7ce02...6fc4b006235f201fdab3722e17240ab420d580e5)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.6.1
  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-07-31 13:27:32 +00:00
dependabot[bot] ecd8fb7740 chore(deps): bump docker/login-action from 4.4.0 to 4.5.1 (#5135)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.4.0 to 4.5.1.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/af1e73f918a031802d376d3c8bbc3fe56130a9b0...abd2ef45e78c5afb21d64d4ca52ee8550d9572c7)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.5.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-07-31 13:27:08 +00:00
anchore-oss-update-botandanchore-oss-update-bot dd639c09b2 chore(deps): update tool versions (#5124)
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-07-30 10:59:36 +00:00
Christopher Angelo PhillipsandAlex Goodman 31a352d030 fix(snap): release temp directories on every snap failure path
---------
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-07-29 14:50:20 -04:00
Alex Goodman 08e913a2ce Report each hardlink as its own file when scanning images (#5029)
* report each hardlink as its own file when scanning images

image scans previously collapsed a set of hardlinks onto a single file, so only
one path per inode showed up in results. dir scans report every hardlink path,
which made image vs dir SBOMs of the same filesystem diverge (and produce
different SPDX `packageVerificationCode` values for packages that own hardlinked
files).

now both image resolvers (squash and all-layers) surface each hardlink at its
own path as a regular file bound to the target's content, matching dir scans.

user-facing impact:
- SBOMs for images containing hardlinks will list more `file` entries
- SPDX `packageVerificationCode` values change for affected packages, now
  matching the equivalent `dir:` scan
- adds `file.NewVirtualLocationFromImage` to the public API

fixes #5019

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix busybox test assertion

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-07-29 13:50:43 -04:00
dependabot[bot]andChristopher Phillips 5ef5b1badc chore(deps): bump anchore/workflows/.github/workflows/check-version-available.yaml (#5033)
Bumps [anchore/workflows/.github/workflows/check-version-available.yaml](https://github.com/anchore/workflows) from 0.7.2 to 0.8.0.
- [Release notes](https://github.com/anchore/workflows/releases)
- [Commits](https://github.com/anchore/workflows/compare/b0c30a80409130d329aaa356fd64a34d8c0b3375...7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4)

---
updated-dependencies:
- dependency-name: anchore/workflows/.github/workflows/check-version-available.yaml
  dependency-version: 0.8.0
  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>
Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2026-07-29 17:33:37 +00:00
dependabot[bot]andChristopher Phillips b236f8c1f7 chore(deps): bump github.com/jedib0t/go-pretty/v6 from 6.8.1 to 6.8.2 (#5051)
Bumps [github.com/jedib0t/go-pretty/v6](https://github.com/jedib0t/go-pretty) from 6.8.1 to 6.8.2.
- [Release notes](https://github.com/jedib0t/go-pretty/releases)
- [Commits](https://github.com/jedib0t/go-pretty/compare/v6.8.1...v6.8.2)

---
updated-dependencies:
- dependency-name: github.com/jedib0t/go-pretty/v6
  dependency-version: 6.8.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>
Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2026-07-29 17:30:07 +00:00
Yashas Gunderiaandychampion 476ce60768 fix(binary): detect Deno 1.11 and 2.9 versions
Signed-off-by: ychampion <ychampion@users.noreply.github.com>
Co-authored-by: ychampion <ychampion@users.noreply.github.com>
2026-07-29 16:33:46 +00:00
Keith Zantow 16223e6dd7 fix: consider vendored golang packages in module attribution (#5093)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
2026-07-27 15:20:49 -04:00
Eljees 1286689419 Fix missing nested packages in package-lock.json v1 (#5108)
* fix(javascript): catalog nested package-lock v1 dependencies

Signed-off-by: Eljees <57435526+Eljees@users.noreply.github.com>

* test(javascript): cover nested package-lock v1 dependencies

Signed-off-by: Eljees <57435526+Eljees@users.noreply.github.com>

---------

Signed-off-by: Eljees <57435526+Eljees@users.noreply.github.com>
2026-07-27 15:18:48 -04:00
Eljees 295454945c fix: strip publisher URL from RPM CPE vendor (#5081)
Signed-off-by: Eljees <yurytumanov.r@yandex.ru>
2026-07-27 15:18:27 -04:00
Enes Deniz 86baeeb481 fix(rust): omit Cargo PURLs for local packages (#5105)
Signed-off-by: Enes Deniz <142517728+3nesdeniz@users.noreply.github.com>
2026-07-27 14:50:13 +00:00
Rayan Salhabandcyphercodes 2dcf5163b8 fix(apk): allow large installed db fields (#5100)
Signed-off-by: cyphercodes <cyphercodes@users.noreply.github.com>
Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
2026-07-27 14:36:56 +00:00
Arpit Jain 12b8ba47fb Fix inverted bounds check dropping every Erlang string with a backslash (#5110)
parseErlangString advances past a backslash escape and then checks
len(data) >= *i before reading the escaped byte. That condition is
almost always true (it only turns false once *i runs off the end),
so the intended out-of-range guard fires on the very first escape
character it sees instead of only at EOF. Any rebar.lock or OTP
resource file containing a backslash in a quoted string (a Windows
git path, an escaped quote, anything) fails to parse and the whole
file, and every package in it, gets dropped.

Flip the comparison to *i >= len(data) so the guard only trips when
the escape is genuinely truncated, and add a regression test for a
string with an escaped quote.

Signed-off-by: Arpit Jain <arpitjain099@gmail.com>
2026-07-27 14:22:41 +00:00
Rez Moss 138d9ce2a0 added bun binary classifier (#5103)
* added bun binary classifier

Signed-off-by: Rez Moss <hi@rezmoss.com>

* added bun binary classifier

Signed-off-by: Rez Moss <hi@rezmoss.com>

---------

Signed-off-by: Rez Moss <hi@rezmoss.com>
2026-07-27 10:18:50 -04:00
anchore-oss-update-botandanchore-oss-update-bot 8a229b16c4 chore(deps): update CPE dictionary index (#5109)
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-07-27 10:04:27 -04:00
dependabot[bot] 9af0098a68 chore(deps): bump google.golang.org/grpc from 1.80.0 to 1.82.1 (#5099)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.80.0 to 1.82.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.80.0...v1.82.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.82.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 13:48:24 +00:00
dependabot[bot] 2840ea6653 chore(deps): bump modernc.org/sqlite from 1.53.0 to 1.54.0 (#5098)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.53.0 to 1.54.0.
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.53.0...v1.54.0)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.54.0
  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-07-24 13:29:49 +00:00
dependabot[bot] b38e40e3e6 chore(deps): bump zizmorcore/zizmor-action from 0.5.7 to 0.6.0 (#5096)
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.7 to 0.6.0.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/192e21d79ab29983730a13d1382995c2307fbcaa...6599ee8b7a49aef6a770f63d261d214911a7ce02)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.6.0
  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-07-24 13:29:21 +00:00
dependabot[bot] a6eedecd31 chore(deps): bump github.com/gpustack/gguf-parser-go (#5097)
Bumps [github.com/gpustack/gguf-parser-go](https://github.com/gpustack/gguf-parser-go) from 0.24.1 to 0.25.0.
- [Release notes](https://github.com/gpustack/gguf-parser-go/releases)
- [Commits](https://github.com/gpustack/gguf-parser-go/compare/v0.24.1...v0.25.0)

---
updated-dependencies:
- dependency-name: github.com/gpustack/gguf-parser-go
  dependency-version: 0.25.0
  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-07-24 13:29:04 +00:00
dependabot[bot] 691b1357b2 chore(deps): bump actions/checkout from 7.0.0 to 7.0.1 (#5095)
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  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-07-24 13:27:48 +00:00
Alex Goodman c890e7f17f decode golang symbols (#5089)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-07-23 15:03:36 -04:00
anchore-oss-update-botandanchore-oss-update-bot 29fd7d0dec chore(deps): update anchore dependencies (#5022)
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-07-20 18:04:57 +00:00
Yashas Gunderiaandychampion 289137ff3c Avoid duplicate packages for replaced Go modules (#5069)
* Prevent duplicate Go packages after source replacement resolution

Source analysis already applies module replacements through go/packages. Avoid synthesizing the same replacement again from go.mod while retaining fallback synthesis for modules that source analysis did not resolve.

Constraint: Preserve unimported and local-path replacement cataloging.

Rejected: Deduplicate only during final assembly | That retains redundant license lookup and ambiguous metadata ownership.

Confidence: high

Scope-risk: narrow

Directive: Keep go.mod fallback packages limited to modules absent from source analysis.

Tested: Focused replacement regression, related Go module parser tests, go vet, gofmt, and diff checks.

Not-tested: Docker-backed full cataloger fixtures; local root storage was exhausted by image generation.
Signed-off-by: ychampion <ychampion@users.noreply.github.com>

* Keep replacement fixtures with the Go module test data

Constraint: The maintainer reserves internal/gotestdata for fixtures that need special Go tooling discovery.
Rejected: Leave this fixture in gotestdata | The regression opens its module explicitly and does not need the special location.
Confidence: high
Scope-risk: narrow
Directive: Use internal/gotestdata only when a fixture must avoid Go testdata discovery rules.
Tested: replacement regression repeated 10 times; Go module parser table; go vet for the Go cataloger; gofmt; diff checks.
Not-tested: Full cataloger package; three unrelated parser fixtures fail identically on exact prior head in this environment.
Signed-off-by: ychampion <ychampion@users.noreply.github.com>

---------

Signed-off-by: ychampion <ychampion@users.noreply.github.com>
Co-authored-by: ychampion <ychampion@users.noreply.github.com>
2026-07-20 12:30:56 -04:00
dependabot[bot] 22ffd5209d chore(deps): bump github.com/gkampitakis/go-snaps from 0.5.22 to 0.5.23 (#5079)
Bumps [github.com/gkampitakis/go-snaps](https://github.com/gkampitakis/go-snaps) from 0.5.22 to 0.5.23.
- [Release notes](https://github.com/gkampitakis/go-snaps/releases)
- [Commits](https://github.com/gkampitakis/go-snaps/compare/v0.5.22...v0.5.23)

---
updated-dependencies:
- dependency-name: github.com/gkampitakis/go-snaps
  dependency-version: 0.5.23
  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-07-20 15:28:53 +00:00
Jason PaulosandAlex Goodman a0852ebf98 Add multi-platform OCI image support (#5074)
* Add multi-platform OCI image support

Signed-off-by: Jason Paulos <jasonpaulos@users.noreply.github.com>

* Reduce calls to PrepareMultiplatformFixtureImage in TestMultiPlatformOCIImageSelection

Signed-off-by: Jason Paulos <jasonpaulos@users.noreply.github.com>

* Use smaller image for testing & update stereoscope fork

Signed-off-by: Jason Paulos <jasonpaulos@users.noreply.github.com>

* bump to stereoscope@main after 548 merge

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Jason Paulos <jasonpaulos@users.noreply.github.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-07-20 15:28:29 +00:00
anchore-oss-update-botandanchore-oss-update-bot d380ad08b5 chore(deps): update CPE dictionary index (#5082)
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-07-20 15:21:24 +00:00
dependabot[bot] ae9534203d chore(deps): bump golang.org/x/mod from 0.37.0 to 0.38.0 (#5078)
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.37.0 to 0.38.0.
- [Commits](https://github.com/golang/mod/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/mod
  dependency-version: 0.38.0
  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-07-17 13:52:25 +00:00
dependabot[bot] dc1ef98237 chore(deps): bump golang.org/x/tools from 0.47.0 to 0.48.0 (#5077)
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.47.0 to 0.48.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.47.0...v0.48.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-version: 0.48.0
  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-07-17 13:52:22 +00:00
dependabot[bot] 36a3af41d9 chore(deps): bump golang.org/x/net from 0.56.0 to 0.57.0 (#5076)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.56.0 to 0.57.0.
- [Commits](https://github.com/golang/net/compare/v0.56.0...v0.57.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.57.0
  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-07-17 13:32:07 +00:00
Christopher Angelo Phillips 3e2bc6ed09 ci: add docker-container driver (#5071)
* ci: add docker-container driver

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>

* fix: update to v4.2.0

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>

---------

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2026-07-16 10:21:22 -04:00
212 changed files with 16753 additions and 2601 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ tools:
# used to upload test fixture cache
- name: oras
version:
want: v1.3.2
want: v1.3.3
method: github-release
with:
repo: oras-project/oras
@@ -24,7 +24,7 @@ tools:
# used to parse JSON/YAML annotations on the fixture cache image
- name: yq
version:
want: v4.53.3
want: v4.53.6
method: github-release
with:
repo: mikefarah/yq
+11 -1
View File
@@ -47,7 +47,17 @@ runs:
APT_PACKAGES: ${{ inputs.bootstrap-apt-packages }}
run: |
IFS=' ' read -ra packages <<< "$APT_PACKAGES"
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y "${packages[@]}"
# the AMI already ships most of these, and `apt update` costs ~40MB of index (minutes,
# when archive.ubuntu.com throttles us) to find that out. only touch apt if we must.
missing=()
for pkg in "${packages[@]}"; do
dpkg -s "$pkg" >/dev/null 2>&1 || missing+=("$pkg")
done
if [ ${#missing[@]} -eq 0 ]; then
echo "all apt packages already present: ${packages[*]}"
exit 0
fi
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y "${missing[@]}"
# ORAS cache: restore-only on non-default branches / forks
- name: Restore ORAS cache from github actions
+10 -5
View File
@@ -32,7 +32,7 @@ jobs:
if: ${{ github.event.inputs.phase == 'all' }}
permissions:
contents: read # required for fetching tags
uses: anchore/workflows/.github/workflows/check-version-available.yaml@b0c30a80409130d329aaa356fd64a34d8c0b3375 # v0.7.2
uses: anchore/workflows/.github/workflows/check-version-available.yaml@7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4 # v0.8.0
with:
version: ${{ github.event.inputs.version }}
@@ -75,7 +75,7 @@ jobs:
packages: write # required for publishing release artifacts to GitHub packages
id-token: write # required for keyless signing (cosign/sigstore OIDC)
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
fetch-depth: 0
persist-credentials: true
@@ -83,14 +83,19 @@ jobs:
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
# dockers_v2 builds multi-platform manifests in a single buildx invocation, which requires a
# docker-container driver builder (the default builder on the runner cannot build multi-platform).
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Login to Docker Hub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 #v4.4.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f #v4.6.0
with:
username: ${{ secrets.ANCHOREOSSWRITE_DH_USERNAME }}
password: ${{ secrets.ANCHOREOSSWRITE_DH_PAT }}
- name: Login to GitHub Container Registry
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 #v4.4.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f #v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -113,7 +118,7 @@ jobs:
# for updating brew formula in anchore/homebrew-syft
GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}
- uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 #v0.24.0
- uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 #v0.24.2
continue-on-error: true
with:
file: go.mod
@@ -20,12 +20,12 @@ jobs:
contents: read
security-events: write # for uploading SARIF results
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "Run zizmor"
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3
with:
# there is a pass/fail gate as a repo ruleset (if there is no ruleset configured then the action will pass by default)
advanced-security: true
+12 -9
View File
@@ -21,14 +21,17 @@ jobs:
name: "Static analysis"
# runs-on.com: memory & general purpose instances for testing
# spot enabled: ok to interrupt non-production workloads
# tmpfs: faster io-intensive workflows
# tmpfs intentionally omitted -- it RAM-backs /home/runner, /tmp and /var/lib/docker at
# 100% of instance memory, starving `make unit` of headroom to link with -race.
# volume: the caches and docker images tmpfs used to hold. gp3 baseline throughput is
# enough; a 4 vCPU instance can't pull much more than that anyway.
# note: s3-cache intentionally omitted -- PR runs are untrusted and must not write to the
# shared cache backend that the trusted release workflow reads from (cache poisoning).
runs-on: &test-runner "runs-on=${{ github.run_id }}/cpu=4+8/ram=32+128/family=r5+r6+r7+r8+m4+m5+m6+m7+m8/spot=price-capacity-optimized/extras=tmpfs"
runs-on: &test-runner "runs-on=${{ github.run_id }}/cpu=4+8/ram=16+32/family=m5+m6+m7+m8+c5+c6+c7+c8/spot=price-capacity-optimized/volume=100gb"
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -50,7 +53,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -72,7 +75,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -105,7 +108,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -135,7 +138,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -179,7 +182,7 @@ jobs:
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -213,7 +216,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
+1
View File
@@ -20,6 +20,7 @@ bin/
/.tool
/.task
/generate
/spec
/specs
mise.toml
.make/.make
+52 -9
View File
@@ -3,8 +3,10 @@ package main
import (
"path/filepath"
"runtime"
"strconv"
. "github.com/anchore/go-make"
"github.com/anchore/go-make/config"
"github.com/anchore/go-make/file"
"github.com/anchore/go-make/lang"
"github.com/anchore/go-make/run"
@@ -27,22 +29,43 @@ func main() {
gotest.Name("unit"),
gotest.ExcludeGlob("**/test/**"),
gotest.CoverageThreshold(62),
race(),
),
// integration tests: native go-make Task. The race-detector smoke against a
// real image stays bundled here (RunsOn integration) so `make integration`
// behaves like the Taskfile version did.
gotest.Tasks(
gotest.Name("integration"),
gotest.IncludeGlob("./cmd/syft/internal/test/integration/..."),
gotest.Verbose(),
gotest.NoCoverage(),
),
// integration tests: run `go test` directly instead of via gotest.Tasks(), which
// has no way to set a timeout. The suite is a single package of ~36 sequential
// tests over 18 docker fixture images, so with a cold fixture cache (every image
// built + saved inline, under -race) it runs well past `go test`'s default 10m
// timeout -- which is exactly the case when regenerating the fixture cache from
// scratch. -count=1 keeps the go test cache from short-circuiting a run whose
// side effect (the built fixtures) is the thing we're after.
//
// The race-detector smoke against a real image stays bundled here (RunsOn
// integration) so `make integration` behaves like the Taskfile version did.
Task{
Name: "integration",
Description: "run integration tests",
RunsOn: lang.List("test"),
Run: func() {
raceFlag := ""
if raceEnabled() {
raceFlag = " -race"
}
Run(
"go test -count=1 -timeout=30m -v"+raceFlag+" ./cmd/syft/internal/test/integration/...",
run.Env("GODEBUG", "dontfreezetheworld=1"),
)
},
},
Task{
Name: "integration:race-smoke",
Description: "exercise the CLI with the race detector",
RunsOn: lang.List("integration"),
Run: func() {
if !raceEnabled() {
Log("race detector disabled (RACE=false); skipping race smoke")
return
}
Run("go run -race cmd/syft/main.go anchore/test_images:grype-quality-dotnet-69f15d2")
},
},
@@ -132,6 +155,26 @@ func main() {
)
}
// raceEnabled is the single switch for the race detector across every test suite.
// Unset it and we keep go-make's behavior (on in CI, off locally and on windows);
// set RACE=false to turn it off everywhere -- worth doing when rebuilding the test
// fixture cache from scratch, where every suite is dominated by building docker
// fixtures and the race detector only adds wall clock. RACE=true forces it on.
func raceEnabled() bool {
if enabled, err := strconv.ParseBool(config.Env("RACE", "")); err == nil {
return enabled
}
return config.CI && !config.Windows
}
// race applies raceEnabled() to a gotest suite. gotest exposes no functional option
// for the race detector, but gotest.Config.Race is exported.
func race() gotest.Option {
return func(c *gotest.Config) {
c.Race = raceEnabled()
}
}
// snapshotBinPath replicates the SNAPSHOT_BIN computation from the prior Taskfile:
// <repoRoot>/snapshot/<os>-build_<os>_<arch>/syft, where arch maps amd64->amd64_v1
// and arm64->arm64_v8.0 to match goreleaser's per-target output directory naming.
+49
View File
@@ -0,0 +1,49 @@
package main
import (
"os"
"testing"
"github.com/anchore/go-make/config"
"github.com/anchore/go-make/tasks/gotest"
)
func TestRaceEnabled(t *testing.T) {
tests := []struct {
name string
env string // "" means RACE is unset
ci bool
want bool
}{
{name: "default in CI", ci: true, want: true},
{name: "default locally", ci: false, want: false},
{name: "explicitly off in CI", env: "false", ci: true, want: false},
{name: "explicitly on locally", env: "true", ci: false, want: true},
{name: "unparseable falls back to default", env: "yes-please", ci: true, want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// always go through t.Setenv (it registers the restore) so an ambient
// RACE from the caller's environment can't leak into the unset cases
t.Setenv("RACE", tt.env)
if tt.env == "" {
os.Unsetenv("RACE")
}
orig := config.CI
config.CI = tt.ci
t.Cleanup(func() { config.CI = orig })
if got := raceEnabled(); got != tt.want {
t.Errorf("raceEnabled() = %v, want %v", got, tt.want)
}
// the gotest suites get the same answer through the functional option
var cfg gotest.Config
race()(&cfg)
if cfg.Race != tt.want {
t.Errorf("race() set Race = %v, want %v", cfg.Race, tt.want)
}
})
}
}
-1
View File
@@ -8,7 +8,6 @@
<p align="center">
&nbsp;<a href="https://github.com/anchore/syft/actions/workflows/validations.yaml" target="_blank"><img alt="Validations" src="https://github.com/anchore/syft/actions/workflows/validations.yaml/badge.svg"></a>&nbsp;
&nbsp;<a href="https://goreportcard.com/report/github.com/anchore/syft" target="_blank"><img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/anchore/syft"></a>&nbsp;
&nbsp;<a href="https://github.com/anchore/syft/releases/latest" target="_blank"><img alt="GitHub release" src="https://img.shields.io/github/release/anchore/syft.svg"></a>&nbsp;
&nbsp;<a href="https://github.com/anchore/syft" target="_blank"><img alt="GitHub go.mod Go version" src="https://img.shields.io/github/go-mod/go-version/anchore/syft.svg"></a>&nbsp;
&nbsp;<a href="https://github.com/anchore/syft/blob/main/LICENSE" target="_blank"><img alt="License: Apache-2.0" src="https://img.shields.io/badge/License-Apache%202.0-blue.svg"></a>&nbsp;
+1
View File
@@ -125,6 +125,7 @@ tasks:
desc: Run tests for pipeline utils
cmds:
- cmd: .github/scripts/labeler_test.py
- cmd: go test -C .make ./...
snapshot-smoke-test:
desc: Run a smoke test on the snapshot builds + docker images
+230 -17
View File
@@ -1,24 +1,34 @@
package commands
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/anchore/clio"
"github.com/anchore/go-homedir"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/spillbuf"
"github.com/anchore/syft/internal/tmpdir"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/format/syftjson"
"github.com/anchore/syft/syft/sbom"
)
const (
convertExample = ` {{.appName}} {{.command}} img.syft.json -o spdx-json convert a syft SBOM to spdx-json, output goes to stdout
{{.appName}} {{.command}} img.syft.json -o cyclonedx-json=img.cdx.json convert a syft SBOM to CycloneDX, output is written to the file "img.cdx.json"
{{.appName}} {{.command}} - -o spdx-json convert an SBOM from STDIN to spdx-json
SYFT_CONVERT_PASSTHROUGH_EXACT_FORMAT=true {{.appName}} {{.command}} img.syft.json -o syft-json=out.syft.json copy the input to "out.syft.json" unchanged if it is already syft-json at the current schema version
`
)
@@ -26,6 +36,7 @@ type ConvertOptions struct {
options.Config `yaml:",inline" mapstructure:",squash"`
options.Output `yaml:",inline" mapstructure:",squash"`
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
Convert options.Convert `yaml:"convert" json:"convert" mapstructure:"convert"`
}
func Convert(app clio.Application) *cobra.Command {
@@ -34,6 +45,7 @@ func Convert(app clio.Application) *cobra.Command {
opts := &ConvertOptions{
UpdateCheck: options.DefaultUpdateCheck(),
Output: options.DefaultOutput(),
Convert: options.DefaultConvert(),
}
return app.SetupCommand(&cobra.Command{
@@ -46,11 +58,11 @@ func Convert(app clio.Application) *cobra.Command {
}),
Args: validateConvertArgs,
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
restoreStdout := ui.CaptureStdoutToTraceLog()
defer restoreStdout()
return RunConvert(opts, args[0])
return RunConvert(cmd.Context(), opts, args[0])
},
}, opts)
}
@@ -59,30 +71,63 @@ func validateConvertArgs(cmd *cobra.Command, args []string) error {
return validateArgs(cmd, args, "an SBOM argument is required")
}
func RunConvert(opts *ConvertOptions, userInput string) error {
func RunConvert(ctx context.Context, opts *ConvertOptions, userInput string) error {
log.Warn("convert is an experimental feature, run `syft convert -h` for help")
writer, err := opts.SBOMWriter()
// a temp dir is only needed when STDIN is a pipe and has to be spilled to disk; it is created lazily on
// first use (under the user's TMPDIR), so a file input never touches it. Mirrors syft.CreateSBOM, which
// also honors a TempDir already attached to the context.
td := tmpdir.FromContext(ctx)
if td == nil {
_, td = tmpdir.Root(ctx, "syft-convert")
defer func() {
_ = td.Cleanup()
}()
}
reader, inputInfo, err := openConvertInput(userInput, td)
if err != nil {
return err
}
defer func() {
_ = reader.Close()
}()
var reader io.ReadSeekCloser
outputs := opts.Outputs
var unchanged []string
if opts.Convert.PassthroughExactFormat {
log.Warn("convert.passthrough-exact-format is enabled: syft-json input that already exactly matches a requested output format and schema version will be copied to that output unchanged (this does not apply to any other input format)")
if userInput == "-" {
// though os.Stdin is an os.File, it does not support seeking
// you will get errors such as "seek /dev/stdin: illegal seek".
// We need to buffer what we read.
reader = internal.NewBufferedSeeker(os.Stdin)
} else {
f, err := os.Open(userInput)
unchanged, outputs, err = partitionOutputsBySourceFormat(opts.Output, reader)
if err != nil {
return fmt.Errorf("failed to open SBOM file: %w", err)
return err
}
defer func() {
_ = f.Close()
}()
reader = f
}
// resolve (and validate) every output that needs converting before writing anything, so that
// a bad -o value fails the command without leaving partial results behind
var writer sbom.Writer
if len(outputs) > 0 {
outputOpts := opts.Output
outputOpts.Outputs = outputs
writer, err = outputOpts.SBOMWriter()
if err != nil {
return err
}
}
for _, output := range unchanged {
if err := writeUnchanged(output, opts.LegacyFile, inputInfo, reader); err != nil {
return err
}
}
if writer == nil {
return nil
}
if _, err := reader.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("unable to seek to start of SBOM: %w", err)
}
s, _, _, err := format.Decode(reader)
@@ -100,3 +145,171 @@ func RunConvert(opts *ConvertOptions, userInput string) error {
return nil
}
// openConvertInput opens the SBOM document at the given path, or STDIN when "-". The returned reader is seekable so
// the document can be identified, copied, and decoded in turn without being loaded into memory. The returned
// os.FileInfo describes the file the document is being read from, so that writing back over it can be recognized;
// it is nil when the input is not a file (a pipe or a terminal on STDIN).
func openConvertInput(userInput string, td *tmpdir.TempDir) (io.ReadSeekCloser, os.FileInfo, error) {
if userInput == "-" {
return openStdin(td)
}
f, err := os.Open(userInput)
if err != nil {
return nil, nil, fmt.Errorf("failed to open SBOM file: %w", err)
}
info, err := f.Stat()
if err != nil {
// the document is readable, we just cannot recognize it as an output destination
log.WithFields("error", err, "path", userInput).Debug("unable to stat SBOM file")
}
return f, info, nil
}
// openStdin returns a seekable view of STDIN. When STDIN is a regular file (such as a shell redirect) it is read in
// place. A pipe or terminal cannot seek (you will get errors such as "seek /dev/stdin: illegal seek"), so its
// contents are drained into a spill buffer: a small head stays in memory and everything beyond it goes to a temp
// file under td, so an arbitrarily large piped SBOM never has to be held in memory to be made seekable.
func openStdin(td *tmpdir.TempDir) (io.ReadSeekCloser, os.FileInfo, error) {
if info, err := os.Stdin.Stat(); err == nil && info.Mode().IsRegular() {
if start, err := os.Stdin.Seek(0, io.SeekCurrent); err == nil {
// a redirect reads the file in place, so it is just as much an input file as a path argument is
return readSeekNopCloser{io.NewSectionReader(os.Stdin, start, info.Size()-start)}, info, nil
}
}
buf := spillbuf.New(td)
if _, err := io.Copy(io.NewOffsetWriter(buf, 0), os.Stdin); err != nil {
_ = buf.Close()
return nil, nil, fmt.Errorf("failed to read SBOM from STDIN: %w", err)
}
return &spilledReader{
SectionReader: io.NewSectionReader(buf, 0, buf.Size()),
buf: buf,
}, nil, nil
}
type readSeekNopCloser struct {
io.ReadSeeker
}
func (readSeekNopCloser) Close() error {
return nil
}
// spilledReader is a seekable view over a spill buffer; closing it releases the buffer's memory and spill file.
type spilledReader struct {
*io.SectionReader
buf *spillbuf.Buffer
}
func (r *spilledReader) Close() error {
return r.buf.Close()
}
// partitionOutputsBySourceFormat splits the requested outputs (in "<format>[@<version>][=<path>]" form) into those
// that exactly match the format and schema version of a syft-json input document (which can be written out
// unchanged) and those that still require conversion. Input in any other format is always converted. Outputs that
// cannot be resolved to an encoder are left for the writer to report on.
func partitionOutputsBySourceFormat(output options.Output, reader io.ReadSeeker) (unchanged []string, toConvert []string, err error) {
if _, err := reader.Seek(0, io.SeekStart); err != nil {
return nil, nil, fmt.Errorf("unable to seek to start of SBOM: %w", err)
}
id, version := format.Identify(reader)
if id == "" || version == "" {
// let decoding surface the appropriate error
return nil, output.Outputs, nil
}
if id != syftjson.ID {
log.WithFields("format", id, "version", version).Debug("input is not syft-json, passthrough does not apply")
return nil, output.Outputs, nil
}
encoders, err := output.Encoders()
if err != nil {
return nil, nil, err
}
collection := format.NewEncoderCollection(encoders...)
for _, out := range output.Outputs {
name, _, _ := strings.Cut(strings.TrimSpace(out), "=")
enc := collection.GetByString(name)
if enc != nil && enc.ID() == id && enc.Version() == version {
log.WithFields("format", id, "version", version, "output", out).Warn("input already exactly matches the requested output format and schema version, copying it unchanged instead of converting it")
unchanged = append(unchanged, out)
continue
}
toConvert = append(toConvert, out)
}
return unchanged, toConvert, nil
}
// writeUnchanged copies the SBOM document as-is to the destination described by the given "<format>[=<path>]"
// output value, falling back to the (deprecated) --file path, and finally to the report bus (STDOUT). When the
// destination is the input file itself there is nothing to do.
func writeUnchanged(output string, defaultFile string, inputInfo os.FileInfo, reader io.ReadSeeker) error {
if _, err := reader.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("unable to seek to start of SBOM: %w", err)
}
_, path, _ := strings.Cut(strings.TrimSpace(output), "=")
if path == "" {
path = defaultFile
}
if path == "" {
// the report bus only carries strings, so STDOUT output must be materialized in memory
content, err := io.ReadAll(reader) //nolint:gocritic // the whole SBOM is the report; there is no meaningful bound
if err != nil {
return fmt.Errorf("unable to read SBOM: %w", err)
}
bus.Report(string(content))
return nil
}
expandedPath, err := homedir.Expand(path)
if err != nil {
log.Warnf("could not expand given writer output path=%q: %w", path, err)
expandedPath = path
}
if isSameFile(inputInfo, expandedPath) {
log.WithFields("path", expandedPath).Info("output is the input file, leaving it as-is")
return nil
}
if err := os.MkdirAll(filepath.Dir(expandedPath), 0755); err != nil {
return fmt.Errorf("unable to create report directory: %w", err)
}
f, err := os.OpenFile(expandedPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("unable to create report file: %w", err)
}
if _, err := io.Copy(f, reader); err != nil {
_ = f.Close()
return fmt.Errorf("unable to write report file: %w", err)
}
return f.Close()
}
// isSameFile reports whether the given path names the file the input is being read from. Comparing the input's
// os.FileInfo rather than its path covers a shell redirect onto STDIN, which has no path to compare.
func isSameFile(inputInfo os.FileInfo, path string) bool {
if inputInfo == nil {
return false
}
pathInfo, err := os.Stat(path)
if err != nil {
return false
}
return os.SameFile(inputInfo, pathInfo)
}
+448
View File
@@ -0,0 +1,448 @@
package commands
import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/wagoodman/go-partybus"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/spillbuf"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/format/spdxjson"
"github.com/anchore/syft/syft/format/syftjson"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func convertTestSBOM() sbom.SBOM {
catalog := pkg.NewCollection()
catalog.Add(pkg.Package{
Name: "musl-utils",
Version: "1.2.3-r4",
Type: pkg.ApkPkg,
FoundBy: "apk-db-cataloger",
Locations: file.NewLocationSet(
file.NewLocation("/lib/apk/db/installed"),
),
PURL: "pkg:apk/alpine/musl-utils@1.2.3-r4",
})
return sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: catalog,
},
Source: source.Description{
ID: "some-id",
Name: "some-dir",
Version: "latest",
Metadata: source.DirectoryMetadata{
Path: "/some/path",
},
},
Descriptor: sbom.Descriptor{
Name: "syft",
Version: "v0.0.0-test",
},
}
}
func mustConvertEncoder(enc sbom.FormatEncoder, err error) sbom.FormatEncoder {
if err != nil {
panic(err)
}
return enc
}
func encodeConvertTestSBOM(t *testing.T, enc sbom.FormatEncoder) []byte {
buf := &bytes.Buffer{}
require.NoError(t, enc.Encode(buf, convertTestSBOM()))
return buf.Bytes()
}
// prettySyftJSON returns the test SBOM as pretty-printed syft-json. Pretty input makes a verbatim copy
// distinguishable from a re-encode, since re-encoding always produces compact output here.
func prettySyftJSON(t *testing.T) []byte {
cfg := syftjson.DefaultEncoderConfig()
cfg.Pretty = true
return encodeConvertTestSBOM(t, mustConvertEncoder(syftjson.NewFormatEncoderWithConfig(cfg)))
}
func Test_partitionOutputsBySourceFormat(t *testing.T) {
syftEncoder := mustConvertEncoder(syftjson.NewFormatEncoderWithConfig(syftjson.DefaultEncoderConfig()))
syftVersion := syftEncoder.Version()
syftContent := encodeConvertTestSBOM(t, syftEncoder)
// the same document claiming an older schema version
olderSyftContent := bytes.ReplaceAll(syftContent, []byte(`"version":"`+syftVersion+`"`), []byte(`"version":"16.0.0"`))
require.NotEqual(t, syftContent, olderSyftContent)
cdxContent := encodeConvertTestSBOM(t, mustConvertEncoder(cyclonedxjson.NewFormatEncoderWithConfig(cyclonedxjson.DefaultEncoderConfig())))
tests := []struct {
name string
content []byte
outputs []string
wantUnchanged []string
wantToConvert []string
}{
{
name: "syft-json matches the bare format name",
content: syftContent,
outputs: []string{"syft-json"},
wantUnchanged: []string{"syft-json"},
},
{
name: "syft-json matches its alias",
content: syftContent,
outputs: []string{"json"},
wantUnchanged: []string{"json"},
},
{
name: "syft-json matches an explicit version and file path",
content: syftContent,
outputs: []string{"syft-json@" + syftVersion + "=out/some.syft.json"},
wantUnchanged: []string{"syft-json@" + syftVersion + "=out/some.syft.json"},
},
{
name: "syft-json at an older schema version requires conversion",
content: olderSyftContent,
outputs: []string{"syft-json"},
wantToConvert: []string{"syft-json"},
},
{
name: "a different output format requires conversion",
content: syftContent,
outputs: []string{"spdx-json"},
wantToConvert: []string{"spdx-json"},
},
{
name: "mixed outputs are partitioned",
content: syftContent,
outputs: []string{"syft-json=a.json", "spdx-json=b.json", "cyclonedx-xml"},
wantUnchanged: []string{"syft-json=a.json"},
wantToConvert: []string{"spdx-json=b.json", "cyclonedx-xml"},
},
{
name: "unknown format is left for the writer to report",
content: syftContent,
outputs: []string{"bogus"},
wantToConvert: []string{"bogus"},
},
{
name: "non syft-json input is always converted, even when it matches the output",
content: cdxContent,
outputs: []string{"cyclonedx-json"},
wantToConvert: []string{"cyclonedx-json"},
},
{
name: "unidentifiable content is left for the decoder to report",
content: []byte("definitely not an sbom"),
outputs: []string{"syft-json"},
wantToConvert: []string{"syft-json"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := options.DefaultOutput()
output.Outputs = tt.outputs
unchanged, toConvert, err := partitionOutputsBySourceFormat(output, bytes.NewReader(tt.content))
require.NoError(t, err)
assert.Equal(t, tt.wantUnchanged, unchanged)
assert.Equal(t, tt.wantToConvert, toConvert)
})
}
}
func Test_RunConvert_passthroughExactFormat(t *testing.T) {
syftContent := prettySyftJSON(t)
newOpts := func(passthrough bool, outputs ...string) *ConvertOptions {
opts := &ConvertOptions{
Output: options.DefaultOutput(),
Convert: options.Convert{
PassthroughExactFormat: passthrough,
},
}
opts.Outputs = outputs
return opts
}
writeInput := func(t *testing.T, content []byte) string {
input := filepath.Join(t.TempDir(), "input.json")
require.NoError(t, os.WriteFile(input, content, 0600))
return input
}
t.Run("matching output is copied unchanged while others are converted", func(t *testing.T) {
dir := t.TempDir()
syftOut := filepath.Join(dir, "out.syft.json")
spdxOut := filepath.Join(dir, "out.spdx.json")
opts := newOpts(true, "syft-json="+syftOut, "spdx-json="+spdxOut)
require.NoError(t, RunConvert(context.Background(), opts, writeInput(t, syftContent)))
got, err := os.ReadFile(syftOut)
require.NoError(t, err)
assert.Equal(t, syftContent, got)
spdx, err := os.ReadFile(spdxOut)
require.NoError(t, err)
id, _ := format.Identify(bytes.NewReader(spdx))
assert.Equal(t, spdxjson.ID, id)
})
t.Run("STDIN input is copied unchanged while others are converted", func(t *testing.T) {
dir := t.TempDir()
syftOut := filepath.Join(dir, "out.syft.json")
spdxOut := filepath.Join(dir, "out.spdx.json")
stdin, err := os.Open(writeInput(t, syftContent))
require.NoError(t, err)
defer stdin.Close()
originalStdin := os.Stdin
os.Stdin = stdin
t.Cleanup(func() { os.Stdin = originalStdin })
opts := newOpts(true, "syft-json="+syftOut, "spdx-json="+spdxOut)
require.NoError(t, RunConvert(context.Background(), opts, "-"))
got, err := os.ReadFile(syftOut)
require.NoError(t, err)
assert.Equal(t, syftContent, got)
spdx, err := os.ReadFile(spdxOut)
require.NoError(t, err)
id, _ := format.Identify(bytes.NewReader(spdx))
assert.Equal(t, spdxjson.ID, id)
})
t.Run("piped STDIN input is copied unchanged while others are converted", func(t *testing.T) {
dir := t.TempDir()
syftOut := filepath.Join(dir, "out.syft.json")
spdxOut := filepath.Join(dir, "out.spdx.json")
pipeReader, pipeWriter, err := os.Pipe()
require.NoError(t, err)
defer pipeReader.Close()
go func() {
defer pipeWriter.Close()
_, _ = pipeWriter.Write(syftContent)
}()
originalStdin := os.Stdin
os.Stdin = pipeReader
t.Cleanup(func() { os.Stdin = originalStdin })
opts := newOpts(true, "syft-json="+syftOut, "spdx-json="+spdxOut)
require.NoError(t, RunConvert(context.Background(), opts, "-"))
got, err := os.ReadFile(syftOut)
require.NoError(t, err)
assert.Equal(t, syftContent, got)
spdx, err := os.ReadFile(spdxOut)
require.NoError(t, err)
id, _ := format.Identify(bytes.NewReader(spdx))
assert.Equal(t, spdxjson.ID, id)
})
t.Run("without the option the matching output is re-encoded", func(t *testing.T) {
syftOut := filepath.Join(t.TempDir(), "out.syft.json")
opts := newOpts(false, "syft-json="+syftOut)
require.NoError(t, RunConvert(context.Background(), opts, writeInput(t, syftContent)))
got, err := os.ReadFile(syftOut)
require.NoError(t, err)
id, _ := format.Identify(bytes.NewReader(got))
assert.Equal(t, syftjson.ID, id)
// the input was pretty-printed and the encoder is not configured to be, so a re-encode cannot reproduce it
assert.NotEqual(t, syftContent, got)
})
t.Run("non syft-json input is re-encoded even when it matches the output", func(t *testing.T) {
cdxContent := encodeConvertTestSBOM(t, mustConvertEncoder(cyclonedxjson.NewFormatEncoderWithConfig(cyclonedxjson.DefaultEncoderConfig())))
cdxOut := filepath.Join(t.TempDir(), "out.cdx.json")
opts := newOpts(true, "cyclonedx-json="+cdxOut)
require.NoError(t, RunConvert(context.Background(), opts, writeInput(t, cdxContent)))
got, err := os.ReadFile(cdxOut)
require.NoError(t, err)
id, _ := format.Identify(bytes.NewReader(got))
assert.Equal(t, cyclonedxjson.ID, id)
// a fresh serial number is minted on every encode, so a round trip never reproduces the input
assert.NotEqual(t, cdxContent, got)
})
t.Run("the deprecated --file path is honored", func(t *testing.T) {
syftOut := filepath.Join(t.TempDir(), "out.syft.json")
opts := newOpts(true, "syft-json")
opts.LegacyFile = syftOut
require.NoError(t, RunConvert(context.Background(), opts, writeInput(t, syftContent)))
got, err := os.ReadFile(syftOut)
require.NoError(t, err)
assert.Equal(t, syftContent, got)
})
t.Run("writing over the input file leaves it intact", func(t *testing.T) {
input := writeInput(t, syftContent)
opts := newOpts(true, "syft-json="+input)
require.NoError(t, RunConvert(context.Background(), opts, input))
got, err := os.ReadFile(input)
require.NoError(t, err)
assert.Equal(t, syftContent, got)
})
t.Run("writing over a redirected STDIN input leaves it intact", func(t *testing.T) {
input := writeInput(t, syftContent)
stdin, err := os.Open(input)
require.NoError(t, err)
defer stdin.Close()
originalStdin := os.Stdin
os.Stdin = stdin
t.Cleanup(func() { os.Stdin = originalStdin })
opts := newOpts(true, "syft-json="+input)
require.NoError(t, RunConvert(context.Background(), opts, "-"))
got, err := os.ReadFile(input)
require.NoError(t, err)
assert.Equal(t, syftContent, got)
})
t.Run("an invalid output fails before anything is written", func(t *testing.T) {
syftOut := filepath.Join(t.TempDir(), "out.syft.json")
opts := newOpts(true, "syft-json="+syftOut, "bogus")
require.Error(t, RunConvert(context.Background(), opts, writeInput(t, syftContent)))
assert.NoFileExists(t, syftOut)
})
t.Run("a missing input file is reported", func(t *testing.T) {
syftOut := filepath.Join(t.TempDir(), "out.syft.json")
opts := newOpts(true, "syft-json="+syftOut)
err := RunConvert(context.Background(), opts, filepath.Join(t.TempDir(), "does-not-exist.json"))
require.ErrorContains(t, err, "failed to open SBOM file")
assert.NoFileExists(t, syftOut)
})
t.Run("unrecognized input is left to the decoder to reject", func(t *testing.T) {
syftOut := filepath.Join(t.TempDir(), "out.syft.json")
opts := newOpts(true, "syft-json="+syftOut)
err := RunConvert(context.Background(), opts, writeInput(t, []byte("definitely not an sbom")))
require.ErrorContains(t, err, "failed to decode SBOM")
})
}
// largeSyftJSON returns a pretty-printed syft-json document of at least minBytes, built from many small packages.
func largeSyftJSON(t *testing.T, minBytes int64) []byte {
catalog := pkg.NewCollection()
for i := 0; i < int(minBytes/100); i++ {
catalog.Add(pkg.Package{
Name: fmt.Sprintf("pkg-%d", i),
Version: "1.0.0",
Type: pkg.ApkPkg,
PURL: fmt.Sprintf("pkg:apk/alpine/pkg-%d@1.0.0", i),
})
}
s := convertTestSBOM()
s.Artifacts.Packages = catalog
cfg := syftjson.DefaultEncoderConfig()
cfg.Pretty = true
enc := mustConvertEncoder(syftjson.NewFormatEncoderWithConfig(cfg))
buf := &bytes.Buffer{}
require.NoError(t, enc.Encode(buf, s))
require.Greater(t, int64(buf.Len()), minBytes)
return buf.Bytes()
}
func Test_RunConvert_pipedInputLargerThanMemoryLimitSpillsToDisk(t *testing.T) {
// a piped document bigger than the spill buffer's in-memory tier is written to a temp file under TMPDIR;
// the conversion must identify, copy and clean up from there rather than holding the document in memory
dir := t.TempDir()
syftOut := filepath.Join(dir, "out.syft.json")
spillRoot := filepath.Join(dir, "spill")
require.NoError(t, os.Mkdir(spillRoot, 0o755))
t.Setenv("TMPDIR", spillRoot)
content := largeSyftJSON(t, 2*spillbuf.DefaultMemLimit)
pipeReader, pipeWriter, err := os.Pipe()
require.NoError(t, err)
defer pipeReader.Close()
go func() {
defer pipeWriter.Close()
_, _ = pipeWriter.Write(content)
}()
originalStdin := os.Stdin
os.Stdin = pipeReader
t.Cleanup(func() { os.Stdin = originalStdin })
opts := &ConvertOptions{
Output: options.DefaultOutput(),
Convert: options.Convert{
PassthroughExactFormat: true,
},
}
opts.Outputs = []string{"syft-json=" + syftOut}
require.NoError(t, RunConvert(context.Background(), opts, "-"))
got, err := os.ReadFile(syftOut)
require.NoError(t, err)
assert.Equal(t, content, got)
entries, err := os.ReadDir(spillRoot)
require.NoError(t, err)
assert.Empty(t, entries, "the spill directory must be removed once the conversion finishes")
}
func Test_writeUnchanged_stdout(t *testing.T) {
// with no output path the document is published on the report bus, which is how the CLI writes to STDOUT
b := partybus.NewBus()
subscription := b.Subscribe()
bus.Set(b)
t.Cleanup(func() {
bus.Set(nil)
})
content := []byte(`{"some":"document"}`)
require.NoError(t, writeUnchanged("syft-json", "", nil, bytes.NewReader(content)))
select {
case e := <-subscription.Events():
assert.Equal(t, event.CLIReport, e.Type)
assert.Equal(t, string(content), e.Value)
case <-time.After(time.Second):
t.Fatal("no report was published")
}
}
+2 -1
View File
@@ -199,7 +199,8 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
WithFromLDFlags(cfg.Golang.MainModuleVersion.FromLDFlags),
).
WithUsePackagesLib(*multiLevelOption(true, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.UsePackagesLib)).
WithCaptureSymbols(cfg.Golang.CaptureSymbols),
WithCaptureSymbols(cfg.Golang.CaptureSymbols).
WithCaptureSymbolsModules(cfg.Golang.CaptureSymbolsModules),
JavaScript: javascript.DefaultCatalogerConfig().
WithIncludeDevDependencies(*multiLevelOption(false, cfg.JavaScript.IncludeDevDependencies)).
WithSearchRemoteLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.JavaScript, task.Node, task.NPM), cfg.JavaScript.SearchRemoteLicenses)).
+27
View File
@@ -0,0 +1,27 @@
package options
import (
"github.com/anchore/clio"
)
var _ clio.FieldDescriber = (*Convert)(nil)
// Convert holds the options specific to the convert command. These are deliberately configuration-only (config file
// or environment) rather than flags: they change how the command behaves for every invocation in a pipeline and are
// not expected to vary from one call to the next.
type Convert struct {
PassthroughExactFormat bool `yaml:"passthrough-exact-format" json:"passthrough-exact-format" mapstructure:"passthrough-exact-format"`
}
func DefaultConvert() Convert {
return Convert{
PassthroughExactFormat: false,
}
}
func (o *Convert) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&o.PassthroughExactFormat, `when a syft-json input SBOM already exactly matches the requested output format and schema version, copy it to that output unchanged instead of decoding and re-encoding it.
Note: this only applies to syft-json input, and only when the document's schema version is identical to the schema version this version of syft produces. Any other input is always converted.
Note: output written this way is a byte-for-byte copy of the input, so format options (such as 'format.pretty' or 'format.json.legacy') are not applied to it.
Note: to keep memory use minimal on large documents, give the input as a file path (or a shell redirect) and write the output with '-o <format>=<path>'. Piped input, or output to STDOUT, requires the whole document to be held in memory.`)
}
+31 -3
View File
@@ -4,6 +4,7 @@ import (
"strings"
"github.com/anchore/clio"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/pkg/cataloger/golang"
)
@@ -19,6 +20,7 @@ type golangConfig struct {
MainModuleVersion golangMainModuleVersionConfig `json:"main-module-version" yaml:"main-module-version" mapstructure:"main-module-version"`
UsePackagesLib *bool `json:"use-packages-lib" yaml:"use-packages-lib" mapstructure:"use-packages-lib"`
CaptureSymbols cataloging.SymbolScope `json:"capture-symbols" yaml:"capture-symbols" mapstructure:"capture-symbols"`
CaptureSymbolsModules []string `json:"capture-symbols-modules" yaml:"capture-symbols-modules" mapstructure:"capture-symbols-modules"`
}
var _ interface {
@@ -42,8 +44,16 @@ if unset this defaults to $GONOPROXY`)
always show (devel) as the version. Use these options to control heuristics to guess
a more accurate version from the binary.`)
descriptions.Add(&o.UsePackagesLib, `use the golang.org/x/tools/go/packages library, which executes golang tooling found on the path in addition to potential network access to get the most accurate results`)
// note: descriptions must be static string literals; the app config discovery that generates the
// capability docs reads them straight out of the AST
descriptions.Add(&o.CaptureSymbols, `capture function symbols from the binary symbol table (pclntab). valid values are:
"none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib)`)
"none" (disabled), "stdlib" (only the synthetic stdlib package), "extended-stdlib" (stdlib plus every
module under golang.org/x/), and "all" (all module packages plus stdlib)`)
descriptions.Add(&o.CaptureSymbolsModules, `glob patterns matched against go module paths (e.g. github.com/klauspost/**) that should have symbols
captured in addition to whatever capture-symbols selects. ** crosses path separators, * does not.
a trailing major version suffix is ignored when matching, so github.com/foo/* covers github.com/foo/bar/v2;
spelling a suffix out in the pattern selects only that major version.
this can only widen the selection, never narrow it, and is inert when capture-symbols is none`)
descriptions.Add(&o.MainModuleVersion.FromLDFlags, `look for LD flags that appear to be setting a version (e.g. -X main.version=1.0.0)`)
descriptions.Add(&o.MainModuleVersion.FromBuildSettings, `use the build settings (e.g. vcs.version & vcs.time) to craft a v0 pseudo version
(e.g. v0.0.0-20220308212642-53e6d0aaf6fb) when a more accurate version cannot be found otherwise`)
@@ -51,7 +61,24 @@ a more accurate version from the binary.`)
}
func (o *golangConfig) PostLoad() error {
raw := strings.TrimSpace(string(o.CaptureSymbols))
o.CaptureSymbols = o.CaptureSymbols.Parse()
// an unrecognized value still resolves to "none", but say so rather than silently capturing nothing.
// stay quiet for unset and an explicit "none", which is the default and would otherwise warn on nearly
// every scan.
if o.CaptureSymbols == cataloging.SymbolScopeNone && raw != "" && !strings.EqualFold(raw, string(cataloging.SymbolScopeNone)) {
log.Warnf("unknown golang.capture-symbols value %q, defaulting to %q (valid values: none, stdlib, extended-stdlib, all)", raw, cataloging.SymbolScopeNone)
}
// trim only, deliberately not Flatten: viper already splits a comma-separated scalar (env var or a bare
// yaml string) into a slice before this point, and there is no CLI flag feeding this key. The one thing
// Flatten would add is splitting commas *inside* a list entry, which silently breaks doublestar brace
// alternation like github.com/{foo,bar}/**. Viper's split does not trim, so that part is still needed.
for i, pattern := range o.CaptureSymbolsModules {
o.CaptureSymbolsModules[i] = strings.TrimSpace(pattern)
}
return nil
}
@@ -76,7 +103,8 @@ func defaultGolangConfig() golangConfig {
FromContents: def.MainModuleVersion.FromContents,
FromBuildSettings: def.MainModuleVersion.FromBuildSettings,
},
UsePackagesLib: nil, // this defaults to true, which is the API default
CaptureSymbols: def.CaptureSymbols,
UsePackagesLib: nil, // this defaults to true, which is the API default
CaptureSymbols: def.CaptureSymbols,
CaptureSymbolsModules: def.CaptureSymbolsModules,
}
}
+43 -5
View File
@@ -10,10 +10,11 @@ import (
func Test_golangConfig_PostLoad(t *testing.T) {
tests := []struct {
name string
cfg golangConfig
expected cataloging.SymbolScope
wantErr assert.ErrorAssertionFunc
name string
cfg golangConfig
expected cataloging.SymbolScope
expectedModules []string
wantErr assert.ErrorAssertionFunc
}{
{
name: "normalize all",
@@ -25,6 +26,32 @@ func Test_golangConfig_PostLoad(t *testing.T) {
cfg: golangConfig{CaptureSymbols: "stdlib"},
expected: cataloging.SymbolScopeStdlib,
},
{
name: "normalize extended-stdlib",
cfg: golangConfig{CaptureSymbols: " Extended-Stdlib "},
expected: cataloging.SymbolScopeExtendedStdlib,
},
{
name: "module patterns keep embedded commas",
cfg: golangConfig{
CaptureSymbols: "stdlib",
// brace alternation contains a comma; splitting on it would corrupt the pattern
CaptureSymbolsModules: []string{"github.com/{foo,bar}/**", "golang.org/x/**"},
},
expected: cataloging.SymbolScopeStdlib,
expectedModules: []string{"github.com/{foo,bar}/**", "golang.org/x/**"},
},
{
// viper splits a comma-separated scalar (env var or bare yaml string) but does not trim,
// so a leading space would otherwise survive into a pattern that silently matches nothing
name: "module patterns are trimmed",
cfg: golangConfig{
CaptureSymbols: "stdlib",
CaptureSymbolsModules: []string{"golang.org/x/**", " github.com/foo/** "},
},
expected: cataloging.SymbolScopeStdlib,
expectedModules: []string{"golang.org/x/**", "github.com/foo/**"},
},
{
name: "empty defaults to none",
cfg: golangConfig{CaptureSymbols: ""},
@@ -32,7 +59,7 @@ func Test_golangConfig_PostLoad(t *testing.T) {
},
{
name: "invalid value defaults to none",
cfg: golangConfig{CaptureSymbols: "bogus"},
cfg: golangConfig{CaptureSymbols: "stdlbi"},
expected: cataloging.SymbolScopeNone,
},
{
@@ -40,6 +67,16 @@ func Test_golangConfig_PostLoad(t *testing.T) {
cfg: golangConfig{CaptureSymbols: "true"},
expected: cataloging.SymbolScopeNone,
},
{
name: "explicit none resolves to none",
cfg: golangConfig{CaptureSymbols: "none"},
expected: cataloging.SymbolScopeNone,
},
{
name: "explicit none is not case sensitive",
cfg: golangConfig{CaptureSymbols: " NONE "},
expected: cataloging.SymbolScopeNone,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -52,6 +89,7 @@ func Test_golangConfig_PostLoad(t *testing.T) {
return
}
assert.Equal(t, tt.expected, tt.cfg.CaptureSymbols)
assert.Equal(t, tt.expectedModules, tt.cfg.CaptureSymbolsModules)
})
}
}
@@ -0,0 +1,149 @@
package integration
import (
"context"
"encoding/json"
"fmt"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/source"
)
// TestMultiPlatformOCIImageSelection verifies that syft can load a single platform out of a
// multi-platform OCI image on disk (both --from oci-dir and --from oci-archive) by selecting the
// image that matches the requested platform.
func TestMultiPlatformOCIImageSelection(t *testing.T) {
remoteImage := "docker.io/library/busybox:1.38.0"
// Per-platform image config digests within the multi-platform index, obtained from the OCI layout
// (see anchore/stereoscope integration tests: TestPlatformSelectionWithOciLocalSources).
expectedDigest := map[string]string{
"arm64": "sha256:e0e8b3cbfed68a90084781e2962f9c0deead51c5a3f11a488eef0283a4284bc2",
"s390x": "sha256:0cf160e720a8e4f20883b72276a6eaded83b79539f3f1d39e35a3336154b9960",
"amd64": "sha256:c6348fa86ba0fb2108c9334f5fe913ddc6d853313e655891f133a0127c30099f",
"ppc64le": "sha256:b144fc0e06537d07956cde340b878a81a717e394edb736d3110858a12a6635cb",
}
// syft --from source tag -> stereoscope OCI source used to prepare the local fixture
sources := map[string]image.Source{
"oci-dir": image.OciDirectorySource,
"oci-archive": image.OciTarballSource,
}
for from, imageSource := range sources {
t.Run(from, func(t *testing.T) {
localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, remoteImage)
for _, arch := range []string{"amd64", "arm64", "s390x", "ppc64le"} {
t.Run(fmt.Sprintf("linux/%s", arch), func(t *testing.T) {
platform, err := image.NewPlatform("linux/" + arch)
require.NoError(t, err)
src, err := syft.GetSource(
context.Background(),
localPath,
syft.DefaultGetSourceConfig().WithSources(from).WithPlatform(platform),
)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, src.Close())
})
meta, ok := src.Describe().Metadata.(source.ImageMetadata)
require.True(t, ok, "expected image metadata, got %T", src.Describe().Metadata)
// The raw config of the selected image must match the requested platform...
assertConfigPlatform(t, meta.RawConfig, "linux", arch)
assert.Equal(t, "linux", meta.OS)
assert.Equal(t, arch, meta.Architecture)
// ...and it must be the exact per-platform image from the multi-platform index.
assert.Equal(t, expectedDigest[arch], meta.ID)
})
}
})
}
}
// TestMultiPlatformOCIImageSelection_UnavailablePlatform verifies that requesting a platform not present
// in the multi-platform OCI image results in an error rather than silently selecting the wrong image.
func TestMultiPlatformOCIImageSelection_UnavailablePlatform(t *testing.T) {
remoteImage := "docker.io/library/busybox:1.38.0"
// windows/amd64 is not present in this linux-only multi-platform image
platform, err := image.NewPlatform("windows/amd64")
require.NoError(t, err)
sources := map[string]image.Source{
"oci-dir": image.OciDirectorySource,
"oci-archive": image.OciTarballSource,
}
for from, imageSource := range sources {
t.Run(from, func(t *testing.T) {
localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, remoteImage)
_, err := syft.GetSource(
context.Background(),
localPath,
syft.DefaultGetSourceConfig().WithSources(from).WithPlatform(platform),
)
require.ErrorContains(t, err, "windows/amd64")
})
}
}
// TestMultiPlatformOCIImageSelection_DefaultPlatform verifies that not specifying a platform results
// in the current platform being selected.
func TestMultiPlatformOCIImageSelection_DefaultPlatform(t *testing.T) {
remoteImage := "docker.io/library/busybox:1.38.0"
sources := map[string]image.Source{
"oci-dir": image.OciDirectorySource,
"oci-archive": image.OciTarballSource,
}
for from, imageSource := range sources {
t.Run(from, func(t *testing.T) {
localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, remoteImage)
src, err := syft.GetSource(
context.Background(),
localPath,
syft.DefaultGetSourceConfig().WithSources(from),
)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, src.Close())
})
meta, ok := src.Describe().Metadata.(source.ImageMetadata)
require.True(t, ok, "expected image metadata, got %T", src.Describe().Metadata)
// The raw config of the selected image must match the current platform
assertConfigPlatform(t, meta.RawConfig, "linux", runtime.GOARCH)
assert.Equal(t, "linux", meta.OS)
assert.Equal(t, runtime.GOARCH, meta.Architecture)
})
}
}
// assertConfigPlatform asserts the os/architecture recorded in a raw OCI image config document.
func assertConfigPlatform(t *testing.T, rawConfig []byte, os, architecture string) {
t.Helper()
require.NotEmpty(t, rawConfig)
var cfg struct {
OS string `json:"os"`
Architecture string `json:"architecture"`
}
require.NoError(t, json.Unmarshal(rawConfig, &cfg))
assert.Equal(t, os, cfg.OS)
assert.Equal(t, architecture, cfg.Architecture)
}
@@ -0,0 +1,64 @@
package integration
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/source"
)
// TestSinglePlatformOCIImage covers the single-image (non-index) OCI layouts, where syft used to report
// empty os/architecture metadata and silently accept any --platform. Both now come from the image config.
func TestSinglePlatformOCIImage(t *testing.T) {
// a platform this fixture is never built for, so the mismatch is not host dependent
unavailablePlatform, err := image.NewPlatform("linux/ppc64le")
require.NoError(t, err)
sources := map[string]image.Source{
"oci-dir": image.OciDirectorySource,
"oci-archive": image.OciTarballSource,
}
for from, imageSource := range sources {
t.Run(from, func(t *testing.T) {
localPath := strings.TrimPrefix(imagetest.PrepareFixtureImage(t, imageSource, "image-distro-id"), string(imageSource)+":")
t.Run("reports the platform from the image config", func(t *testing.T) {
src, err := syft.GetSource(
context.Background(),
localPath,
syft.DefaultGetSourceConfig().WithSources(from),
)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, src.Close())
})
meta, ok := src.Describe().Metadata.(source.ImageMetadata)
require.True(t, ok, "expected image metadata, got %T", src.Describe().Metadata)
// the fixture is built for whatever the local docker daemon runs, so assert against the
// config rather than a fixed platform
assert.NotEmpty(t, meta.OS)
assert.NotEmpty(t, meta.Architecture)
assertConfigPlatform(t, meta.RawConfig, meta.OS, meta.Architecture)
})
t.Run("rejects a platform the image does not match", func(t *testing.T) {
_, err := syft.GetSource(
context.Background(),
localPath,
syft.DefaultGetSourceConfig().WithSources(from).WithPlatform(unavailablePlatform),
)
require.ErrorContains(t, err, "linux/ppc64le")
})
})
}
}
@@ -7,4 +7,6 @@
!image-pkg-coverage/pkgs/java/*.hpi
**/go.sum
!image-go-bin-arch-coverage/go.sum
!image-go-bin-arch-coverage/go.sum
cache/
@@ -8,8 +8,6 @@ RUN mkdir tmp
WORKDIR /usr/share/jenkins/tmp
RUN apt-get update 2>&1 > /dev/null && apt-get install -y less zip 2>&1 > /dev/null
RUN unzip ../jenkins.war 2>&1 > /dev/null
RUN rm -rf ./META-INF/MANIFEST.MF ./WEB-INF ./jsbundles ./scripts ./css
@@ -18,7 +16,8 @@ WORKDIR /usr/share/jenkins
RUN rm -rf jenkins.war
RUN cd ./tmp && zip -r ../jenkins.war . && cd ..
# use jar (from the JDK in the base image) to rezip -- M keeps it manifest-less, which is the point of this fixture
RUN cd ./tmp && jar cfM ../jenkins.war . && cd ..
RUN rm -rf ./tmp
@@ -16,7 +16,7 @@ RUN unxz /lib/modules/6.0.7-301.fc37.x86_64/kernel/drivers/tty/ttynull.ko.xz
# dotnet pkg coverage
# https://nuget.info/packages/DocuSign.eSign.dll/6.8.0
# https://github.com/docusign/docusign-esign-csharp-client/blob/master/LICENSE
RUN curl -LO https://www.nuget.org/api/v2/package/DocuSign.eSign.dll/6.8.0
RUN curl -fLO https://www.nuget.org/api/v2/package/DocuSign.eSign.dll/6.8.0
RUN unzip 6.8.0
RUN chmod 600 lib/net462/DocuSign.eSign.dll
RUN rm 6.8.0
+87 -90
View File
@@ -4,7 +4,7 @@ go 1.26.3
require (
github.com/BurntSushi/toml v1.6.0
github.com/CycloneDX/cyclonedx-go v0.11.0
github.com/CycloneDX/cyclonedx-go v0.12.0
github.com/Masterminds/semver/v3 v3.5.0
github.com/Masterminds/sprig/v3 v3.3.0
github.com/OneOfOne/xxhash v1.2.8
@@ -18,11 +18,11 @@ require (
github.com/anchore/go-homedir v0.1.1
github.com/anchore/go-logger v0.1.1
github.com/anchore/go-macholibre v0.1.1
github.com/anchore/go-rpmdb v0.1.0
github.com/anchore/go-sync v0.1.1
github.com/anchore/go-rpmdb v0.2.0
github.com/anchore/go-sync v0.1.2
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.2.0
github.com/anchore/stereoscope v0.2.2
github.com/anchore/stereoscope v0.3.2
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
github.com/aquasecurity/go-pep440-version v0.0.1
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
@@ -34,36 +34,36 @@ require (
github.com/charmbracelet/lipgloss v1.1.0
github.com/dave/jennifer v1.7.1
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
github.com/diskfs/go-diskfs v1.9.3
github.com/diskfs/go-diskfs v1.9.4
github.com/distribution/reference v0.6.0
github.com/dustin/go-humanize v1.0.1
github.com/elliotchance/phpserialize v1.4.0
github.com/facebookincubator/nvdtools v0.1.5
github.com/github/go-spdx/v2 v2.7.0
github.com/gkampitakis/go-snaps v0.5.22
github.com/go-git/go-billy/v5 v5.9.0
github.com/go-git/go-git/v5 v5.19.1
github.com/gkampitakis/go-snaps v0.5.23
github.com/go-git/go-billy/v5 v5.9.1
github.com/go-git/go-git/v5 v5.19.2
github.com/go-test/deep v1.1.1
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/goccy/go-yaml v1.19.2
github.com/gohugoio/hashstructure v0.6.0
github.com/google/go-cmp v0.7.0
github.com/google/go-containerregistry v0.21.7
github.com/google/go-containerregistry v0.22.1
github.com/google/licensecheck v0.3.1
github.com/google/uuid v1.6.0
github.com/gookit/color v1.6.1
github.com/gpustack/gguf-parser-go v0.24.1
github.com/gpustack/gguf-parser-go v0.26.3
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-getter v1.8.6
github.com/hashicorp/go-getter v1.8.9
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/hcl/v2 v2.24.0
github.com/iancoleman/strcase v0.3.0
github.com/invopop/jsonschema v0.14.0
github.com/jedib0t/go-pretty/v6 v6.8.1
github.com/jedib0t/go-pretty/v6 v6.8.3
github.com/jinzhu/copier v0.4.0
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
github.com/klauspost/compress v1.19.0
github.com/magiconair/properties v1.8.10
github.com/klauspost/compress v1.20.0
github.com/magiconair/properties v1.18.11
github.com/mholt/archives v0.1.5
github.com/moby/sys/mountinfo v0.7.2
github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1
@@ -82,8 +82,8 @@ require (
github.com/spdx/tools-golang v0.6.0-rc4
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/ulikunitz/xz v0.5.15
github.com/stretchr/testify v1.12.1
github.com/ulikunitz/xz v0.5.16
github.com/vbatts/go-mtree v0.7.0
github.com/vifraa/gopom v1.0.0
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651
@@ -91,14 +91,14 @@ require (
github.com/xeipuuv/gojsonschema v1.2.0
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1
go.uber.org/goleak v1.3.0
go.yaml.in/yaml/v3 v3.0.4
go.yaml.in/yaml/v3 v3.0.5
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
golang.org/x/mod v0.37.0
golang.org/x/net v0.56.0
golang.org/x/time v0.15.0
golang.org/x/tools v0.47.0
golang.org/x/mod v0.41.0
golang.org/x/net v0.58.0
golang.org/x/time v0.16.0
golang.org/x/tools v0.49.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.53.0
modernc.org/sqlite v1.58.0
)
require (
@@ -108,19 +108,19 @@ require (
)
require (
cel.dev/expr v0.25.1 // indirect
cel.dev/expr v0.25.2 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth v0.23.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
cloud.google.com/go/storage v1.61.3 // indirect
cloud.google.com/go/iam v1.12.0 // indirect
cloud.google.com/go/monitoring v1.30.0 // indirect
cloud.google.com/go/storage v1.65.1 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/DataDog/zstd v1.5.5 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 // indirect
github.com/Microsoft/hcsshim v0.15.0-rc.1 // indirect
@@ -133,25 +133,24 @@ require (
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aquasecurity/go-version v0.0.1 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/aws/aws-sdk-go-v2 v1.44.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.40 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.39 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.40 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.41 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.10.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.41 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.108.0 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.6.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.34.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.39.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.46.0 // indirect
github.com/aws/smithy-go v1.28.1 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/becheran/wildmatch-go v1.0.0 // indirect
@@ -168,35 +167,34 @@ require (
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/containerd/cgroups/v3 v3.1.3 // indirect
github.com/containerd/containerd/api v1.11.1 // indirect
github.com/containerd/containerd/v2 v2.3.2 // indirect
github.com/containerd/containerd/v2 v2.3.5 // indirect
github.com/containerd/continuity v0.5.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/fifo v1.1.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v1.0.0-rc.4 // indirect
github.com/containerd/platforms v1.0.0-rc.5 // indirect
github.com/containerd/plugin v1.1.0 // indirect
github.com/containerd/ttrpc v1.2.8 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/cli v29.5.3+incompatible // indirect
github.com/docker/cli v29.8.0+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/docker/go-connections v0.7.0 // indirect
github.com/docker/go-connections v0.8.1 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/felixge/fgprof v0.9.5 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
github.com/gkampitakis/ciinfo v0.3.4 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
@@ -205,13 +203,13 @@ require (
github.com/go-restruct/restruct v1.2.0-alpha // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.20 // indirect
github.com/googleapis/gax-go/v2 v2.24.0 // indirect
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-version v1.8.0 // indirect
github.com/hashicorp/go-version v1.9.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/henvic/httpretty v0.1.4 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
@@ -226,7 +224,7 @@ require (
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/maruel/natural v1.3.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect
github.com/mattn/go-runewidth v0.0.21 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
@@ -238,8 +236,8 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/moby/api v1.54.2 // indirect
github.com/moby/moby/client v0.4.1 // indirect
github.com/moby/moby/api v1.56.0 // indirect
github.com/moby/moby/client v0.6.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/signal v0.7.1 // indirect
github.com/moby/sys/user v0.4.0 // indirect
@@ -257,7 +255,7 @@ require (
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opencontainers/runtime-spec v1.3.0 // indirect
github.com/pborman/indent v1.2.1 // indirect
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
github.com/piprate/json-gold v0.7.0 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
@@ -265,7 +263,6 @@ require (
github.com/pkg/profile v1.7.0 // indirect
github.com/pkg/xattr v0.4.12 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
@@ -281,8 +278,8 @@ require (
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/sylabs/sif/v2 v2.24.1 // indirect
github.com/sylabs/squashfs v1.0.6 // indirect
@@ -299,34 +296,34 @@ require (
github.com/zclconf/go-cty v1.16.3 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/crypto v0.56.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/term v0.44.0 // indirect
golang.org/x/text v0.38.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/term v0.45.0 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
gonum.org/v1/gonum v0.17.0 // indirect
google.golang.org/api v0.271.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
google.golang.org/api v0.294.0 // indirect
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect
google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.12 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
modernc.org/libc v1.73.4 // indirect
modernc.org/libc v1.75.6 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/memory v1.12.1 // indirect
)
retract (
+194 -193
View File
@@ -1,5 +1,5 @@
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -31,8 +31,8 @@ cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0c
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth v0.23.2 h1:pxSCpfiji41hpzpPdMCftEUCezpgpqmmDdYiAjCKXxo=
cloud.google.com/go/auth v0.23.2/go.mod h1:4DhBRcqvtljQN3dJ57qtqbib5ZGCYE5f2crfiiC2EM0=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
@@ -46,14 +46,14 @@ cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCB
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/iam v1.12.0 h1:Aki3bX9aHUDKPHfnRJfDcTdVedvy6quGBQcTqx3DRXk=
cloud.google.com/go/iam v1.12.0/go.mod h1:FEZ4lXpADAC2AIpQY7LANNjjwyQ2jK439CI2VaD+sLY=
cloud.google.com/go/logging v1.19.0 h1:NCqhdVUg3wQ8Cobdf16FDSuTGi3+6+hdSBHrY5TsR6Q=
cloud.google.com/go/logging v1.19.0/go.mod h1:i40NZCHC9Gqvod4yE+yQfDWwlgwW/SrshkkGibCHxcA=
cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM=
cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0=
cloud.google.com/go/monitoring v1.30.0 h1:r/d+JUbyKmJ8b07iznuKfzVzrIXTWxHQ3lBRm3x2LlY=
cloud.google.com/go/monitoring v1.30.0/go.mod h1:htlUR0QWVMrjFzZmN4LGnMAve9xB/eduwjmINxVZ8RM=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@@ -63,10 +63,10 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg=
cloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk=
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
cloud.google.com/go/storage v1.65.1 h1:LRRpBJUTf+OXDPX9jZUKZ3mSLIsz3htG+qUpeNZovyA=
cloud.google.com/go/storage v1.65.1/go.mod h1:UsS9OgFg/XHOSYakQ8ZtLWWeyGkk1WnmD/GsGfN0BHM=
cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E=
cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
@@ -77,19 +77,19 @@ github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/CycloneDX/cyclonedx-go v0.11.0 h1:GokP8FiRC+foiuwWhSSLpSD5H4hSWtGnR3wo7apkBFI=
github.com/CycloneDX/cyclonedx-go v0.11.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8=
github.com/CycloneDX/cyclonedx-go v0.12.0 h1:/7Jum36UA6V043tQZ/fE3jf+Nf9gn/qxUFfd7QReMy8=
github.com/CycloneDX/cyclonedx-go v0.12.0/go.mod h1:V2577HhxDDCDLYfkm55WJrz16nHTfyQZwcWUBSG7Z28=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 h1:jLdiS1vO+XJFyDSWRHBx56r4s/NNtcl5J6KyCcWUX/w=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0/go.mod h1:8lmpHY+1VRoteiOwyrQMDt1YGXOrFKCz+1wJW7n3ODY=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0 h1:cSjUzZ7KU8hicTgzaSv9NmSyM9fTVK3y5lsBUl3wOis=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0/go.mod h1:dzcEjy1WJ0Q4u9twNR3LcLhNoYMRCrMCMafpxa0TjPQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 h1:RoO5+d7uCmDqovLrHCr2/BuViUXvdcrNxyNM1pN9dDQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0/go.mod h1:YqwkQPrWSC7+byyc1VlKbWLBF5JsW5IoL6xUkemYSXk=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
@@ -136,18 +136,18 @@ github.com/anchore/go-lzo v0.1.1 h1:IwL/fvkdtlIrYIXck6WxZ3nb8WjjHziYYmGxlooyOnM=
github.com/anchore/go-lzo v0.1.1/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=
github.com/anchore/go-macholibre v0.1.1 h1:sTBHi3S1WVIOCamc6NyT8Sn9mBmD30DCTfjfVm4k6/M=
github.com/anchore/go-macholibre v0.1.1/go.mod h1:YNq2610RlvGCK0Za+Klz/OCMtUaEnwwAGA/VfUAQaro=
github.com/anchore/go-rpmdb v0.1.0 h1:Q8dc208/HYzCqhx0L1zurfm1UPil24hlo9NjkdFmLdE=
github.com/anchore/go-rpmdb v0.1.0/go.mod h1:eQVa6QFGzKy0qMcnW2pez0XBczvgwSjw9vA23qifEyU=
github.com/anchore/go-rpmdb v0.2.0 h1:ixcyMg/guhBL9XjBVwhTv9CG48xO07Qkg2uDX2+zx/c=
github.com/anchore/go-rpmdb v0.2.0/go.mod h1:ATsRlpCXstnoYfzqBfhwGw0U2dolx1BBQ9rAU8jWBAw=
github.com/anchore/go-struct-converter v0.2.0-rc2 h1:q+859fW2/jbHJHB2etbNfRlFwYpknyvbqqk1hUdamQ4=
github.com/anchore/go-struct-converter v0.2.0-rc2/go.mod h1:cDBA5vhcR62nXWo8QH9/Kk2807o65ISaHPNPX66L+Uw=
github.com/anchore/go-sync v0.1.1 h1:91SZ+YqUIIHmf2jPAYZPuHqM/ZqnK1pVJDWtET6+AJE=
github.com/anchore/go-sync v0.1.1/go.mod h1:3haGsk2BnaoVhsfqae8Kd0FKIAILE1Hw69P4Xo5iVBw=
github.com/anchore/go-sync v0.1.2 h1:RDD5RCanrWqhbtBulqPSZpERadUlgcYjpybYzYd3Uk8=
github.com/anchore/go-sync v0.1.2/go.mod h1:grjZH059bCHM6f6jh+Y7VYvX/q8YcjkDJqG6nplHGkE=
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods=
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk=
github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE=
github.com/anchore/stereoscope v0.2.2 h1:SGTLGoF6GHmKEn9Bb6LYpzzgz9nhvMZgN/c4fTSQjYY=
github.com/anchore/stereoscope v0.2.2/go.mod h1:ylJXJKebLctP7u9ewguB1d0zUETJxvAA7r2DiuljDTM=
github.com/anchore/stereoscope v0.3.2 h1:A2FDMSU56m3rErO1e0CtWAVcDS8tc2iX4UXTw1hyPnI=
github.com/anchore/stereoscope v0.3.2/go.mod h1:/IUDVDka+4gufc8kDw+JO3gFl9PkpCjJ2r3bd9UWouM=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
@@ -169,44 +169,42 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aws/aws-sdk-go-v2 v1.44.0 h1:4IbaHhtzy+4h37z4JQyO9a2QsiCml3CNYHtq5hIHigo=
github.com/aws/aws-sdk-go-v2 v1.44.0/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 h1:GPRlPwz40I2B2VrBEASOA3Bi77NyeqejNLkifosX0rs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20/go.mod h1:g7PNzKcsOKWb4fkSRBA7BZVAS6Y8IcxzN+nRohhQ1Q8=
github.com/aws/aws-sdk-go-v2/config v1.32.40 h1:lAVC9gMmKusmqDRe32dPtgKl/BWvJmMJoWELKHCAObw=
github.com/aws/aws-sdk-go-v2/config v1.32.40/go.mod h1:8xOJLbe/hOj1g4PVsfJYV7O2byq+UGET1onDdUgbwqc=
github.com/aws/aws-sdk-go-v2/credentials v1.19.39 h1:XOg8LC3Kgnsa3WiPQjc7Bi8k5IBN92cPYfIV9XMFss0=
github.com/aws/aws-sdk-go-v2/credentials v1.19.39/go.mod h1:GonTDBQ+mTpCVNwaHjj0PagspfrYYMEqOx7FehoEP/I=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.40 h1:r5aGipEVgI9aT/tAGjdrPbDQvIAKdTrS3rUPQtG4Rmo=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.40/go.mod h1:vOD3CnPxAdkL6MWZeROkZsTlskklMFfgVFkHzx/oZpY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40 h1:UIXlbijuB2XK1Kr57fo8iIxCuaSHJzwZ1uo+2tbEYIk=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40/go.mod h1:wcEsL6jscjZjVUinb0Q5qD/GXOG1yT3GNfmT9HuDwzU=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40 h1:xLQVRDs2NddDmK9BEyh5KSlJ1Gpy5/GIJXrV6WcVGAE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40/go.mod h1:XRXnpFVFGLaEVK+olDdFIM1vNa04ETW452oFGEPUxAo=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.41 h1:nv/ILuCY0yXACzMQwvtt/HbqDDjemZiI0AeDbxGQlnU=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.41/go.mod h1:dzvOSpxaPqQ3j0xS6Lc1vyVuWW0RBj7s/QqYpzu3Q/0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 h1:bAdDl/HkGCcGPoe25ToSHEw23VIxt6CT5fLcg111BKg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19/go.mod h1:KaUzbLxv4CeSxh6ZCl9B4m7CuFenS8kUEaDs+f/DQr4=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.10.0 h1:U8/A0RRBaEspzH1uul3JHLbypXwEGUkRkvoT9f0ATcM=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.10.0/go.mod h1:UELStX5KwtJNtQxa+UuF8dc3z4UYc40e8yHYJSozNwY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40 h1:gr3Fw1cxZXNCdeo/lQ7isHEHzvHVM7z75qb2zW9aMjw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40/go.mod h1:8z/9CmfnQhiuXD7Ykbcg4a/whSWsniE0ODSx9uwVzfk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.41 h1:Q9DIKDuJix/oJnQxFpQ26L0EwVa/YNo4k2kbktrjQjE=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.41/go.mod h1:x+TuqkOIG1SZS0+yN54sExGA9ZpjhPO6vPdYnpTFX1M=
github.com/aws/aws-sdk-go-v2/service/s3 v1.108.0 h1:Yp+x5PKXEmoqHsgP/pAkBy5Tyq1UlXAzM0OInh0vxWw=
github.com/aws/aws-sdk-go-v2/service/s3 v1.108.0/go.mod h1:locV6DtXyp7Xzr2BG6jtsbeBi3YAWJ/CY4xUThYmIwQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.6.0 h1:agcr0j8YeFEzdXNo17Rg9MbbjLRjrimabwNtji4e+lU=
github.com/aws/aws-sdk-go-v2/service/signin v1.6.0/go.mod h1:qU5PxgQ4JiUOOMotzfO3+5oUda5W+8JDVKyLQqlrJik=
github.com/aws/aws-sdk-go-v2/service/sso v1.34.0 h1:FxaN8/sn61DTXNI6Gt678tFJUY8iUsCchm6Y/F/RjaA=
github.com/aws/aws-sdk-go-v2/service/sso v1.34.0/go.mod h1:vu4OY6s8LJtT8BtYG2LD6BGSZMptkYn3o5hvCPB22jc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.39.0 h1:crWKPeGYTBTuBxQ3p73kjfJvt4brUIsr+Fuypko8FxY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.39.0/go.mod h1:HjjZVhaBz0JBR/kbWKThmNDhFKS7y6EURuk493tJk9Y=
github.com/aws/aws-sdk-go-v2/service/sts v1.46.0 h1:IZ63JdogSNNjex/jsODNv7jGDcO/xJYd9FsgyfCsp1g=
github.com/aws/aws-sdk-go-v2/service/sts v1.46.0/go.mod h1:I+rwAf3spG5dITBaAo3xXRowk8kiOhtU1kYxfvCTC44=
github.com/aws/smithy-go v1.28.1 h1:R/nXH00c8qcfCzQVELtRw+eLQWtzv+VAIEFJ1/xxXlQ=
github.com/aws/smithy-go v1.28.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
@@ -290,14 +288,14 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ=
github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=
github.com/containerd/containerd/api v1.11.1 h1:h8nfoDW9+fNsC/9TwiAHj8B1GzXKtR4eFtkhi/X5RLU=
github.com/containerd/containerd/api v1.11.1/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw=
github.com/containerd/containerd/v2 v2.3.2 h1:eLven1YxRMkeiKu7IcMrPKE+gn8sGR1DqHbbshMEvWM=
github.com/containerd/containerd/v2 v2.3.2/go.mod h1:rHKGm3VW6wNrINb3x8mNT+w7qYXFVElTt/8HTuxVhD4=
github.com/containerd/containerd/v2 v2.3.5 h1:9MYlI81gUcOZ0WsCkSMtvOU7rTR3hqAoa2eCzhoLlkA=
github.com/containerd/containerd/v2 v2.3.5/go.mod h1:RXDyLPaI3zoO7dFdAW9/54W4cix+z3A6larieufC9mg=
github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg=
github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@@ -308,8 +306,8 @@ github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4=
github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=
github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8oyaRarxdmSU=
github.com/containerd/platforms v1.0.0-rc.5/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=
github.com/containerd/plugin v1.1.0 h1:O+7lczNJVMy8rz0YNx3xGB8tTf5qY4i5abF041Ew19U=
github.com/containerd/plugin v1.1.0/go.mod h1:qBTum+A8lJ6lO44A19Eo7y1OlcLj4OWFH1DA/vnHmcc=
github.com/containerd/ttrpc v1.2.8 h1:xbVu6D4qF2jihdh9rDVOKqUMiFBQk6YctTdo1zk087Y=
@@ -333,18 +331,18 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da h1:ZOjWpVsFZ06eIhnh4mkaceTiVoktdU67+M7KDHJ268M=
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da/go.mod h1:B3tI9iGHi4imdLi4Asdha1Sc6feLMTfPLXh9IUYmysk=
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
github.com/diskfs/go-diskfs v1.9.3 h1:cLciNCeZ4QAXVxyPJDr1ZJ9N9CCG3rQlQ/z/Cs/cNDM=
github.com/diskfs/go-diskfs v1.9.3/go.mod h1:TePJORO83Adh5pb2SqsxAwaP0fofFxKLkxctiS/9OQc=
github.com/diskfs/go-diskfs v1.9.4 h1:0j2d7eG4IjyxL6+ChWbDPocdBCF6HQ4HBWU2WDYWVnc=
github.com/diskfs/go-diskfs v1.9.4/go.mod h1:TePJORO83Adh5pb2SqsxAwaP0fofFxKLkxctiS/9OQc=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs=
github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.8.0+incompatible h1:ih0c2jq/nN7QfES8zIfwSzIhRScjs4ehER+kZ60aeSk=
github.com/docker/cli v29.8.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
@@ -371,14 +369,14 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.
github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/erofs/go-erofs v0.3.0 h1:o/W5ABAA3sHYl97WL93dacKEfeDpJhdFf3c2snAti7I=
@@ -389,8 +387,8 @@ github.com/facebookincubator/nvdtools v0.1.5/go.mod h1:Kh55SAWnjckS96TBSrXI99KrE
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
@@ -403,27 +401,27 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/github/go-spdx/v2 v2.7.0 h1:GzfXx4wFdlilARxmFRXW/mgUy3A4vSqZocCMFV6XFdQ=
github.com/github/go-spdx/v2 v2.7.0/go.mod h1:Ftc45YYG1WzpzwEPKRVm9Jv8vDqOrN4gWoCkK+bHer0=
github.com/gkampitakis/ciinfo v0.3.4 h1:5eBSibVuSMbb/H6Elc0IIEFbkzCJi3lm94n0+U7Z0KY=
github.com/gkampitakis/ciinfo v0.3.4/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/go-snaps v0.5.22 h1:xg9omphRnbDnimMCl1KqznC4krlxOGpkB0vDSfX2P7M=
github.com/gkampitakis/go-snaps v0.5.22/go.mod h1:uy3lVzCCRRsAwYqSocyw5fY8xRLCYEfqoOJNxr8HonM=
github.com/gkampitakis/go-snaps v0.5.23 h1:okh5QR48zpUjpWtu65AtqxdCY8huJq+dEDuUzd1PuKg=
github.com/gkampitakis/go-snaps v0.5.23/go.mod h1:uy3lVzCCRRsAwYqSocyw5fY8xRLCYEfqoOJNxr8HonM=
github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
github.com/go-git/go-billy/v5 v5.9.1 h1:8U73XiOTfINdItHVa6z4Gv7ToObcZ6grkqQbLryLCdA=
github.com/go-git/go-billy/v5 v5.9.1/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -509,8 +507,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU=
github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE=
github.com/google/go-containerregistry v0.22.1 h1:RZuuSYhTvlDvtsK+NkutoCZ//C0X2ebLK8X8l3ULs84=
github.com/google/go-containerregistry v0.22.1/go.mod h1:bJR35SK8XgisYmhg/FMQ/5RK0S/XrOAqLBV5/LR2XE0=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs=
github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY=
@@ -537,32 +535,32 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/enterprise-certificate-proxy v0.3.20 h1:t/xL64VUoN69MuMRQuJETqYGOw4Z9mSRJK9epIEtwFk=
github.com/googleapis/enterprise-certificate-proxy v0.3.20/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/googleapis/gax-go/v2 v2.24.0 h1:myMaPYyF9MecEmvQqMqomIwn9t/4KCZN9qnwsS76wlg=
github.com/googleapis/gax-go/v2 v2.24.0/go.mod h1:IaTHBDd7NHxSCiu0vEs8pQZu4dGZrWwuSoxCnk16OFM=
github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
github.com/gpustack/gguf-parser-go v0.24.1 h1:nTYtL8HFK6ZhB90RKBu4oX2b3ZHpJLrMmKRfL9w9Cyc=
github.com/gpustack/gguf-parser-go v0.24.1/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=
github.com/gpustack/gguf-parser-go v0.26.3 h1:F63PlUPIW56HGU5k4Crv0JAG9ojtz/TuwzgMYlb82Ec=
github.com/gpustack/gguf-parser-go v0.26.3/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 h1:vTCWu1wbdYo7PEZFem/rlr01+Un+wwVmI7wiegFdRLk=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72/go.mod h1:Vn+BBgKQHVQYdVQ4NZDICE1Brb+JfaONyDHr3q07oQc=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 h1:mymLUKThnV9wFvogOK8NnsMP9/vlhnjXY98gr2QIGW8=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74/go.mod h1:Bh9qYL8ehmDxSg14Tk8oxFCP90XHfs6NxV1D84884xA=
github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -572,8 +570,8 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-getter v1.8.6 h1:9sQboWULaydVphxc4S64oAI4YqpuCk7nPmvbk131ebY=
github.com/hashicorp/go-getter v1.8.6/go.mod h1:nVH12eOV2P58dIiL3rsU6Fh3wLeJEKBOJzhMmzlSWoo=
github.com/hashicorp/go-getter v1.8.9 h1:1AOTMUmz/S/GuqOTE6+bg6nwPXTEbKr3Tc/T/lVS7is=
github.com/hashicorp/go-getter v1.8.9/go.mod h1:qI7vH/m552bXutKe4UkWptOJl4bo2KeO/CSOoh4s0ps=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
@@ -589,8 +587,8 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
@@ -624,8 +622,8 @@ github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8i
github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jedib0t/go-pretty/v6 v6.8.1 h1:0fkCNhjrX0zPpwkWaDYU5VMrygg41Tu197mWILIJoqQ=
github.com/jedib0t/go-pretty/v6 v6.8.1/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/jedib0t/go-pretty/v6 v6.8.3 h1:yVSk5aemoYHCvcrtqyXklwqcgHQIQzmy/oUzFlmffSQ=
github.com/jedib0t/go-pretty/v6 v6.8.3/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
@@ -645,8 +643,8 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
@@ -670,8 +668,8 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/magiconair/properties v1.18.11 h1:j5ozYZl0zCjG7ahMDH0GWIobOvvUzT0BdAguG0ViKy0=
github.com/magiconair/properties v1.18.11/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/maruel/natural v1.3.0 h1:VsmCsBmEyrR46RomtgHs5hbKADGRVtliHTyCOLFBpsg=
github.com/maruel/natural v1.3.0/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
@@ -688,8 +686,8 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
@@ -723,10 +721,10 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
github.com/moby/moby/api v1.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ=
github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs=
github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
@@ -786,8 +784,8 @@ github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrp
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/piprate/json-gold v0.7.0 h1:bEMirgA5y8Z2loTQfxyIFfY+EflxH1CTP6r/KIlcJNw=
@@ -902,14 +900,14 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/spiffe/go-spiffe/v2 v2.7.0 h1:uXe1MflJoHw58wAUvxVlcM7WpKtijWG7I1UidcGh6g4=
github.com/spiffe/go-spiffe/v2 v2.7.0/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -920,8 +918,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
@@ -931,8 +929,8 @@ github.com/sylabs/squashfs v1.0.6 h1:PvJcDzxr+vIm2kH56mEMbaOzvGu79gK7P7IX+R7BDZI
github.com/sylabs/squashfs v1.0.6/go.mod h1:DlDeUawVXLWAsSRa085Eo0ZenGzAB32JdAUFaB0LZfE=
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA=
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=
github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo=
github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw=
github.com/terminalstatic/go-xsd-validate v0.1.8 h1:UVrTCy1j3DhwaYTTUF+QYO/Nan13S0tf+Jwi+p45Bf0=
github.com/terminalstatic/go-xsd-validate v0.1.8/go.mod h1:1kb47fi2c6onlf+B7UrrQ9VYraOhcYwFm3iG+J6F4Zo=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -947,8 +945,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.16 h1:ld6NyySjx5lowVKwJvMRLnW5nxKX/xnpSiFYZ/Lxur0=
github.com/ulikunitz/xz v0.5.16/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw=
github.com/vbatts/go-mtree v0.7.0 h1:ytmOc3MTRidZiBi9VBCyZ2BHe4fZS47L5v7BVXDWW4E=
github.com/vbatts/go-mtree v0.7.0/go.mod h1:EjdpFC+LZy1TXbRGNa1MKKgjQ+7ew3foMFJK8o4/TdY=
github.com/vifraa/gopom v1.0.0 h1:L9XlKbyvid8PAIK8nr0lihMApJQg/12OBvMA28BcWh0=
@@ -998,32 +996,35 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw=
go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
@@ -1040,8 +1041,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1081,8 +1082,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1127,8 +1128,8 @@ golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1160,8 +1161,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1236,13 +1237,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1253,13 +1254,13 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -1315,8 +1316,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1357,8 +1358,8 @@ google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdr
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY=
google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q=
google.golang.org/api v0.294.0 h1:8gASjJxdtcIieB3OqbkLcF0FfbXVNqKtU5iozD1ssvA=
google.golang.org/api v0.294.0/go.mod h1:02qB8+Ox1ZFzcaKFMguy1nQLJmSIyvV6Ff4txJEXtl4=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1429,12 +1430,12 @@ google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ6
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d h1:C9v1o0/4quuhOAfmRXA2j+we0PqZIp8traLdeogF3Ms=
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d/go.mod h1:Wz2wFJntZFmLGo7pLDXZ3wYk5hyc0Mb+SkHhDDXT+lU=
google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d h1:QwnJwPte4XXAkhPu26LTDIahnsMSUV0kK8HkxbC+Pc4=
google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d/go.mod h1:WRrQ7/7N19PypuT0fxLOL5Lq0waoiRri4FbtHDEKrGE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1462,8 +1463,8 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -1478,8 +1479,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -1513,30 +1514,30 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
modernc.org/cc/v4 v4.29.2 h1:h6+9ciCnPKutf4I03CvheAvDLX7+IHlqR6Iy6J+cgd8=
modernc.org/cc/v4 v4.29.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.35.0 h1:F+TUsmw09QxLzmi3aeYYGxjAXarmZaKgj3mKQHNaA8w=
modernc.org/ccgo/v4 v4.35.0/go.mod h1:qrVGs9S3Sr2Ztcg9ve+kTAYMp5a3YvWjo+SoN06kJ5I=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc=
modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
modernc.org/libc v1.75.6 h1:yKk8qo+Di4gkmvRboK8ocCqH22FiUCR6jRy2OwtCRus=
modernc.org/libc v1.75.6/go.mod h1:bO5o2ztHxBb2rjz0PgdHN0sSMw57CgxGFLZ3Qd/QpVQ=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
modernc.org/sqlite v1.58.0 h1:38u40/bwkfM7f0Myhosl+SEMltSDxnGdQf8o6Kjmys0=
modernc.org/sqlite v1.58.0/go.mod h1:rsD2CckafgObKC4DhBlGBf+RiHxkc3hINGt1Xw32tVY=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+3 -1
View File
@@ -15,7 +15,9 @@ application: # AUTO-GENERATED - application-level config keys
- key: dotnet.relax-dll-claims-when-bundling-detected
description: show all packages from the deps.json if bundling tooling is present as a dependency (e.g. ILRepack)
- key: golang.capture-symbols
description: 'capture function symbols from the binary symbol table (pclntab). valid values are: "none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib)'
description: 'capture function symbols from the binary symbol table (pclntab). valid values are: "none" (disabled), "stdlib" (only the synthetic stdlib package), "extended-stdlib" (stdlib plus every module under golang.org/x/), and "all" (all module packages plus stdlib)'
- key: golang.capture-symbols-modules
description: glob patterns matched against go module paths (e.g. github.com/klauspost/**) that should have symbols captured in addition to whatever capture-symbols selects. ** crosses path separators, * does not. a trailing major version suffix is ignored when matching, so github.com/foo/* covers github.com/foo/bar/v2; spelling a suffix out in the pattern selects only that major version. this can only widen the selection, never narrow it, and is inert when capture-symbols is none
- key: golang.local-mod-cache-dir
description: specify an explicit go mod cache directory, if unset this defaults to $GOPATH/pkg/mod or $HOME/go/pkg/mod
- key: golang.local-vendor-dir
+66
View File
@@ -0,0 +1,66 @@
package file
import "io"
// ReaderSize reports the number of bytes behind r. The bool is false when that cannot be determined, and
// callers must branch on it rather than treating the zero as a size.
//
// Binary parsers read sizes out of the files they are parsing and allocate against them. Those sizes are
// 32- and 64-bit fields under the control of whoever produced the file, so a truncated or hostile input
// can declare gigabytes that are not there. The read would fail afterwards either way; what matters is
// refusing before the allocation, and that needs a real byte count to weigh the claim against.
//
// The bool is the whole point of the signature. An earlier version returned 0 for every failure mode, and
// a caller that forgot to special-case it got a bound of "nothing is allowed" or, more often, silently
// fell back to an unbounded path. Making the caller name the failure keeps that from happening by
// omission.
//
// Both *bytes.Reader and *io.SectionReader answer directly. A file handle only has Seek, so fall back to
// a save-and-restore seek, which leaves the caller's cursor where it found it. That fallback is not
// atomic: it moves the cursor and puts it back, so a reader being read concurrently can observe the
// intermediate position.
//
// Neither answer is taken on faith, because a reader can report a length it cannot deliver: an
// *io.SectionReader answers with the length it was constructed with, and debug/elf and friends construct
// theirs with a nominal one, so io.NewSectionReader(r, 0, 1<<63-1) would otherwise report a bound that
// permits everything as if it had been measured. The last byte is read back to confirm the count, which is
// one ReadAt whatever the size, and the read is what the caller was going to weigh anyway.
//
// A wrapper that embeds io.ReaderAt as an interface promotes only ReadAt, so it answers no size here
// however sizable the reader underneath it is. A type meant to be bounded needs to forward Size() itself.
func ReaderSize(r io.ReaderAt) (int64, bool) {
size, ok := reportedSize(r)
if !ok {
return 0, false
}
// ReadAt may return io.EOF alongside a full read, so the count is what says the byte is there
var last [1]byte
if n, _ := r.ReadAt(last[:], size-1); n != len(last) {
return 0, false
}
return size, true
}
// reportedSize is what the reader says about itself, before ReaderSize checks whether it can back it up.
func reportedSize(r io.ReaderAt) (int64, bool) {
if sr, ok := r.(interface{ Size() int64 }); ok {
size := sr.Size()
return size, size > 0
}
s, ok := r.(io.Seeker)
if !ok {
return 0, false
}
cur, err := s.Seek(0, io.SeekCurrent)
if err != nil {
return 0, false
}
size, err := s.Seek(0, io.SeekEnd)
if err != nil {
return 0, false
}
if _, err := s.Seek(cur, io.SeekStart); err != nil {
return 0, false
}
return size, size > 0
}
+140
View File
@@ -0,0 +1,140 @@
package file
import (
"bytes"
"io"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestReaderSize(t *testing.T) {
tests := []struct {
name string
r io.ReaderAt
want int64
wantOK bool
}{
{
name: "bytes.Reader answers directly",
r: bytes.NewReader(make([]byte, 1234)),
want: 1234,
wantOK: true,
},
{
// an empty reader is not a size to bound against, so it reports not-ok along with everything
// else that cannot be measured
name: "empty bytes.Reader",
r: bytes.NewReader(nil),
want: 0,
},
{
name: "SectionReader over a real length",
r: io.NewSectionReader(bytes.NewReader(make([]byte, 500)), 100, 300),
want: 300,
wantOK: true,
},
{
name: "seek-only reader falls back to seeking",
r: seekOnly{bytes.NewReader(make([]byte, 4096))},
want: 4096,
wantOK: true,
},
{
name: "reader with neither Size nor Seek cannot be measured",
r: readAtOnly{bytes.NewReader(make([]byte, 4096))},
want: 0,
},
{
// the shape debug/elf, debug/pe and debug/macho build internally. Taking this at its word hands
// back a bound that permits everything, reported as if it had been measured.
name: "SectionReader over a nominal length reports what it can deliver",
r: io.NewSectionReader(bytes.NewReader(make([]byte, 500)), 0, 1<<63-1),
want: 0,
},
{
// a Size() a type simply got wrong is the same failure: the bytes are what decides
name: "a size the reader cannot back up is refused",
r: overstatedSize{ReaderAt: bytes.NewReader(make([]byte, 10)), size: 1 << 30},
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
size, ok := ReaderSize(tt.r)
assert.Equal(t, tt.want, size)
assert.Equal(t, tt.wantOK, ok)
})
}
}
func TestReaderSize_FileHandle(t *testing.T) {
path := filepath.Join(t.TempDir(), "f.bin")
require.NoError(t, os.WriteFile(path, make([]byte, 7777), 0o600))
f, err := os.Open(path)
require.NoError(t, err)
defer f.Close()
// the cursor has to come back where it was found: the caller may be mid-read, and an *os.File is the
// one input here that carries a shared position
_, err = f.Seek(101, io.SeekStart)
require.NoError(t, err)
size, ok := ReaderSize(f)
assert.True(t, ok)
assert.Equal(t, int64(7777), size)
at, err := f.Seek(0, io.SeekCurrent)
require.NoError(t, err)
assert.Equal(t, int64(101), at, "measuring the file must not move the caller's cursor")
}
// TestReaderSize_WrapperMustForwardSize pins the shape that made every bound expressed against this
// helper go silently inert. A type that embeds io.ReaderAt as an interface promotes only ReadAt, so it
// answers 0 here however sizable the reader underneath is, and a caller that treats 0 as "fall back to
// unbounded" then stops bounding anything. Forwarding Size() is what fixes it, and this is the assertion
// that a new wrapper has to satisfy.
func TestReaderSize_WrapperMustForwardSize(t *testing.T) {
inner := bytes.NewReader(make([]byte, 2048))
size, ok := ReaderSize(embedsReaderAt{inner})
assert.False(t, ok, "a wrapper that only embeds the interface cannot be measured")
assert.Equal(t, int64(0), size)
size, ok = ReaderSize(forwardsSize{inner})
assert.True(t, ok, "a wrapper meant to be bounded has to forward Size itself")
assert.Equal(t, int64(2048), size)
}
type seekOnly struct{ inner *bytes.Reader }
func (s seekOnly) ReadAt(p []byte, off int64) (int, error) { return s.inner.ReadAt(p, off) }
func (s seekOnly) Seek(off int64, whence int) (int64, error) {
return s.inner.Seek(off, whence)
}
type readAtOnly struct{ inner *bytes.Reader }
func (r readAtOnly) ReadAt(p []byte, off int64) (int, error) { return r.inner.ReadAt(p, off) }
type embedsReaderAt struct{ io.ReaderAt }
type forwardsSize struct{ io.ReaderAt }
func (f forwardsSize) Size() int64 {
size, _ := ReaderSize(f.ReaderAt)
return size
}
// overstatedSize answers with a size larger than the bytes behind it, the way a wrapper that returns a
// declared length rather than a measured one would.
type overstatedSize struct {
io.ReaderAt
size int64
}
func (o overstatedSize) Size() int64 { return o.size }
+330
View File
@@ -0,0 +1,330 @@
// Package spillbuf provides a sparse, offset-addressed byte buffer that keeps a bounded amount in memory
// and spills the rest to a temp file.
//
// It exists for the shape where output arrives out of order, at offsets the input itself declares: a
// decompressor placing extents, an archive rebuilding a file from chunks. A plain []byte cannot do that
// without either pre-sizing to a length the input claims or growing to the furthest offset it names, and
// both hand a hostile input an allocation knob.
//
// The buffer reports only what it actually stored. Size is the contiguous run written from offset zero,
// and a read past it is io.EOF rather than the zeros an unwritten region would otherwise hand back. That
// distinction is the whole point: sparse storage serves holes as zeros for free, so a reader that reported
// a length covering them would let a write offset stand in for real output. Anything sizing an allocation
// against this reader (debug/elf's saferio does exactly that) would then allocate bytes nothing produced.
//
// Allocation is bounded by the memory limit rather than by how much is written: filling 1MB and filling
// 64MB cost about the same, because everything past the limit goes to the file. Only memory is bounded
// here: how much ends up on disk is whatever the caller writes, so bounding that stays the caller's job.
package spillbuf
import (
"errors"
"fmt"
"io"
"math"
"os"
"slices"
"github.com/anchore/syft/internal/tmpdir"
)
// DefaultMemLimit is how much a buffer keeps in memory before it spills, when the caller does not say.
// Small on purpose: the memory tier saves a temp file for the common small case, it is not a place to
// hold output. A limit large enough to matter is a limit large enough to be an amplification path.
const DefaultMemLimit int64 = 1 << 20 // 1MB
// ErrNoTempDir is returned when a buffer needs to spill and has nowhere to spill to.
var ErrNoTempDir = errors.New("no temp dir available to spill to")
// Buffer is a sparse, offset-addressed buffer. The zero value is not usable; call New.
//
// Storage is two tiers with a fixed boundary: bytes below memLimit live in mem, bytes at or past it live
// in file. Nothing ever moves between them, so an offset belongs to exactly one tier for the life of the
// buffer and the only case needing care is a range that straddles the boundary.
//
// Not safe for concurrent use. Every consumer writes from a single goroutine, and guarding it would cost
// each write an uncontended lock for no reader.
type Buffer struct {
td *tmpdir.TempDir
memLimit int64
// mem holds [0, memLimit). Grown geometrically to fit what has been written, never past the limit.
mem []byte
// file holds [memLimit, ...), created on the first write that reaches there, so a buffer that stays
// small never touches disk. remove deletes it on Close.
file *os.File
remove func()
// written is the set of ranges stored, sorted by start and merged, so runs of adjacent writes collapse
// to one entry rather than growing per call. Everything the buffer reports comes from it.
written []extent
closed bool
}
// extent is a half-open byte range that has been written.
//
// Unexported on purpose. What a caller needs to know about the contents is answered by Size and FirstGap;
// handing out the bookkeeping invites re-deriving those at the call site and getting them subtly wrong,
// which is the bug this package exists to prevent.
type extent struct {
start, end int64
}
// Option configures a Buffer.
type Option func(*Buffer)
// WithMemLimit sets how many bytes are held in memory before the buffer spills to disk. Zero sends every
// write to the temp file. Negative is treated as zero.
//
// Peak allocation is a small multiple of this, since growing the tier holds the old copy alongside the
// new one and the new one may be rounded up past what was asked for.
func WithMemLimit(n int64) Option {
return func(b *Buffer) {
b.memLimit = max(n, 0)
}
}
// New returns a buffer that spills into td.
//
// td may be nil only when every write is known to stay inside the memory limit; a write past it then
// fails with ErrNoTempDir rather than allocating. Nothing is created on disk until a write needs it.
func New(td *tmpdir.TempDir, opts ...Option) *Buffer {
b := &Buffer{td: td, memLimit: DefaultMemLimit}
for _, opt := range opts {
opt(b)
}
return b
}
// split divides a range of n bytes starting at off across the two tiers, reporting how many bytes fall in
// each. The file portion starts at off+inMem.
func (b *Buffer) split(off, n int64) (inMem, inFile int64) {
if off >= b.memLimit {
return 0, n
}
inMem = min(n, b.memLimit-off)
return inMem, n - inMem
}
// fileOffset translates a buffer offset at or past the limit into an offset within the spill file.
func (b *Buffer) fileOffset(off int64) int64 { return off - b.memLimit }
// WriteAt stores p at off. Writes may land anywhere, in any order, and may overlap. A write that fails
// leaves the buffer as it was.
func (b *Buffer) WriteAt(p []byte, off int64) (int, error) {
if b.closed {
return 0, fmt.Errorf("spillbuf: write to a closed buffer: %w", os.ErrClosed)
}
if off < 0 {
return 0, fmt.Errorf("spillbuf: negative offset %d", off)
}
if len(p) == 0 {
return 0, nil
}
// subtraction form so off+len(p) cannot wrap into a small in-range end
if off > math.MaxInt64-int64(len(p)) {
return 0, fmt.Errorf("spillbuf: write of %d bytes at %d overflows", len(p), off)
}
inMem, inFile := b.split(off, int64(len(p)))
// the fallible half goes first: once the memory tier is stamped the old bytes are gone, so a file
// write that fails afterward would report failure over a buffer it had already changed
if inFile > 0 {
if err := b.writeFile(p[inMem:], b.fileOffset(off+inMem)); err != nil {
return 0, err
}
}
if inMem > 0 {
b.writeMem(p[:inMem], off)
}
b.record(extent{start: off, end: off + int64(len(p))})
return len(p), nil
}
func (b *Buffer) writeMem(p []byte, off int64) {
if need := off + int64(len(p)); int64(len(b.mem)) < need {
// geometric, capped at the limit. Sizing to exactly what each write needs looks tidier and is
// quadratic: a caller streaming a block in 32KB chunks reallocates and copies the whole tier every
// chunk, so filling 1MB costs ~16MB of allocation.
grown := min(b.memLimit, max(need, 2*int64(len(b.mem))))
// sized to exactly the geometric target rather than grown through append: append rounds the new
// capacity up past what was asked for, and the tier is already capped, so the slack is pure waste
mem := make([]byte, grown)
copy(mem, b.mem)
b.mem = mem
}
copy(b.mem[off:], p)
}
func (b *Buffer) writeFile(p []byte, off int64) error {
if b.file == nil {
if b.td == nil {
return ErrNoTempDir
}
f, remove, err := b.td.NewFile("syft-spill-*.bin") //nolint:gocritic // removal happens in Close
if err != nil {
return fmt.Errorf("spillbuf: unable to create spill file: %w", err)
}
b.file, b.remove = f, remove
}
if _, err := b.file.WriteAt(p, off); err != nil {
return fmt.Errorf("spillbuf: unable to write to spill file: %w", err)
}
return nil
}
// ReadAt returns bytes this buffer actually stored. A read starting at or past Size is io.EOF, and one
// running past it is short, because everything beyond is a hole the buffer never held.
func (b *Buffer) ReadAt(p []byte, off int64) (int, error) {
if b.closed {
return 0, fmt.Errorf("spillbuf: read from a closed buffer: %w", os.ErrClosed)
}
if off < 0 {
return 0, fmt.Errorf("spillbuf: negative offset %d", off)
}
if len(p) == 0 {
return 0, nil
}
size := b.Size()
if off >= size {
return 0, io.EOF
}
// bounded by Size, so every byte in [off, off+want) was written and both tiers really hold theirs
want := min(int64(len(p)), size-off)
inMem, inFile := b.split(off, want)
// guarded rather than relying on a zero-length copy: when off is past the limit the slice expression
// itself is out of range, since mem is only as long as what was written into it
if inMem > 0 {
copy(p[:inMem], b.mem[off:off+inMem])
}
if inFile > 0 {
if err := readFullAt(b.file, p[inMem:want], b.fileOffset(off+inMem)); err != nil {
return int(inMem), fmt.Errorf("spillbuf: unable to read spill file: %w", err)
}
}
if want < int64(len(p)) {
return int(want), io.EOF
}
return int(want), nil
}
// readFullAt fills p from r. A short read is an error rather than a short return: the caller has already
// bounded the range by what was written, so the file failing to deliver means it is not what we left.
func readFullAt(r io.ReaderAt, p []byte, off int64) error {
for read := 0; read < len(p); {
n, err := r.ReadAt(p[read:], off+int64(read))
read += n
if err != nil {
return err
}
}
return nil
}
// Size reports the contiguous run written from offset zero, which is the length this buffer is willing to
// stand behind. Deliberately not the furthest offset written: see the package doc for why the two are not
// interchangeable.
//
// record keeps the set merged and sorted, so the run from zero is at most the first two entries and the
// loop stops there: the cost this adds to every ReadAt is effectively constant, not a scan of the set.
func (b *Buffer) Size() int64 {
var covered int64
for _, e := range b.written {
if e.start > covered {
break
}
covered = max(covered, e.end)
}
return covered
}
// FirstGap returns the offset of the earliest unwritten run of at least size bytes lying entirely within
// [0, within), and whether there is one. Callers that place output at offsets of their own choosing use it
// to fill what they left behind, without needing the buffer's bookkeeping.
//
// The arithmetic is in subtraction form throughout: within comes from caller input and the result is used
// as a write offset, so a wrap here would be a write nobody bounded.
func (b *Buffer) FirstGap(size, within int64) (int64, bool) {
if b.closed || size < 0 || within < 0 {
return 0, false
}
var at int64
for _, e := range b.written {
// once the cursor is at the bound there is nothing left to offer, and it only moves forward
if at >= within {
return 0, false
}
// the gap ahead of this extent ends at whichever comes first, the extent or the bound
end := min(e.start, within)
if end > at && end-at >= size {
return at, true
}
at = max(at, e.end)
}
if at <= within && within-at >= size {
return at, true
}
return 0, false
}
// record merges e into the written set, keeping it sorted by start with no overlapping or adjacent pairs.
func (b *Buffer) record(e extent) {
if e.end <= e.start {
return
}
// inserted in place rather than appended and re-sorted: the set is already ordered, so a run of
// ascending writes (the common case) stays linear
i, _ := slices.BinarySearchFunc(b.written, e, func(x, y extent) int {
return cmpInt64(x.start, y.start)
})
b.written = slices.Insert(b.written, i, e)
merged := b.written[:1]
for _, cur := range b.written[1:] {
last := &merged[len(merged)-1]
if cur.start <= last.end {
last.end = max(last.end, cur.end)
continue
}
merged = append(merged, cur)
}
b.written = merged
}
func cmpInt64(x, y int64) int {
switch {
case x < y:
return -1
case x > y:
return 1
}
return 0
}
// Close releases the spill file, if one was ever created, and drops the memory tier. Safe on a nil
// receiver and safe to call more than once. Reads and writes after it fail with os.ErrClosed.
func (b *Buffer) Close() error {
if b == nil {
return nil
}
b.mem = nil
b.written = nil
b.closed = true
if b.file == nil {
return nil
}
err := b.file.Close()
if b.remove != nil {
b.remove()
}
b.file, b.remove = nil, nil
return err
}
+872
View File
@@ -0,0 +1,872 @@
package spillbuf
import (
"bytes"
"errors"
"io"
"math"
"math/rand"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/internal/tmpdir"
)
func newTestBuffer(t *testing.T, opts ...Option) *Buffer {
t.Helper()
b := New(tmpdir.FromPath(t.TempDir()), opts...)
t.Cleanup(func() { _ = b.Close() })
return b
}
func mustWrite(t *testing.T, b *Buffer, off int64, p []byte) {
t.Helper()
n, err := b.WriteAt(p, off)
require.NoError(t, err)
require.Equal(t, len(p), n, "WriteAt must report the full write")
}
// readAll returns everything the buffer is willing to hand back.
func readAll(t *testing.T, b *Buffer) []byte {
t.Helper()
out := make([]byte, b.Size())
if len(out) == 0 {
return nil
}
n, err := b.ReadAt(out, 0)
require.NoError(t, err, "a read of exactly Size bytes is not short")
require.Equal(t, len(out), n)
return out
}
func repeat(b byte, n int) []byte { return bytes.Repeat([]byte{b}, n) }
// --- the core property: report only what was stored -------------------------------------------------
// TestSizeIsTheContiguousPrefix is the property the package exists for. A sparse buffer hands its holes
// back as zeros it never stored, so reporting a length that covers them lets a write offset stand in for
// real output: whatever sizes an allocation against this reader then allocates bytes nothing produced.
func TestSizeIsTheContiguousPrefix(t *testing.T) {
tests := []struct {
name string
writes []extent // start is the offset, end-start the length
wantSize int64
}{
{
name: "nothing written",
wantSize: 0,
},
{
name: "one run from zero",
writes: []extent{{0, 100}},
wantSize: 100,
},
{
name: "a run that does not start at zero covers nothing",
writes: []extent{{10, 100}},
wantSize: 0,
},
{
name: "adjacent runs join",
writes: []extent{{0, 50}, {50, 100}},
wantSize: 100,
},
{
name: "a hole stops the prefix",
writes: []extent{{0, 50}, {60, 100}},
wantSize: 50,
},
{
name: "the hole is filled later",
writes: []extent{{0, 50}, {60, 100}, {50, 60}},
wantSize: 100,
},
{
name: "out of order still resolves",
writes: []extent{{60, 100}, {0, 50}, {50, 60}},
wantSize: 100,
},
{
name: "overlapping runs",
writes: []extent{{0, 60}, {40, 100}},
wantSize: 100,
},
{
name: "a run fully inside another",
writes: []extent{{0, 100}, {20, 40}},
wantSize: 100,
},
{
name: "far placement is not coverage",
writes: []extent{{0, 64}, {1 << 20, 1<<20 + 64}},
wantSize: 64,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := newTestBuffer(t)
for _, w := range tt.writes {
mustWrite(t, b, w.start, repeat('x', int(w.end-w.start)))
}
assert.Equal(t, tt.wantSize, b.Size(), "Size is the contiguous prefix")
})
}
}
// TestReadPastSizeIsEOFNotZeros is the other half of the same property. Sparse storage returns zeros for
// a hole for free; handing those out would make the hole indistinguishable from real content.
func TestReadPastSizeIsEOFNotZeros(t *testing.T) {
b := newTestBuffer(t)
mustWrite(t, b, 0, repeat('A', 64))
mustWrite(t, b, 4096, repeat('B', 64)) // past a hole
require.Equal(t, int64(64), b.Size())
t.Run("a read starting in the hole is EOF", func(t *testing.T) {
p := make([]byte, 16)
n, err := b.ReadAt(p, 100)
assert.Zero(t, n)
assert.ErrorIs(t, err, io.EOF)
})
t.Run("a read starting at the far extent is EOF, not the bytes there", func(t *testing.T) {
p := make([]byte, 64)
n, err := b.ReadAt(p, 4096)
assert.Zero(t, n)
assert.ErrorIs(t, err, io.EOF)
assert.Equal(t, make([]byte, 64), p, "nothing may be copied out")
})
t.Run("a read running off the end is short and says so", func(t *testing.T) {
p := make([]byte, 128)
n, err := b.ReadAt(p, 0)
assert.Equal(t, 64, n, "only the stored prefix comes back")
assert.ErrorIs(t, err, io.EOF)
assert.Equal(t, repeat('A', 64), p[:64])
assert.Equal(t, make([]byte, 64), p[64:], "the rest is untouched")
})
}
// TestSparsePlacementIsNotAnAllocationKnob pins the amplification directly: a tiny payload parked at a
// huge offset must not cost memory proportional to the offset.
func TestSparsePlacementIsNotAnAllocationKnob(t *testing.T) {
const far = 512 << 20 // 512MB out
b := newTestBuffer(t, WithMemLimit(1<<20))
mustWrite(t, b, 0, repeat('A', 64))
var before, after runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&before)
mustWrite(t, b, far, repeat('B', 64))
runtime.ReadMemStats(&after)
allocated := after.TotalAlloc - before.TotalAlloc
t.Logf("writing 64 bytes at offset %d allocated %d bytes", far, allocated)
assert.Less(t, allocated, uint64(1<<20),
"a far placement must cost its own bytes, not its offset")
assert.Equal(t, int64(64), b.Size(), "and it must not count toward what the buffer will deliver")
}
// --- tiers ------------------------------------------------------------------------------------------
func TestMemoryTierNeverTouchesDisk(t *testing.T) {
dir := t.TempDir()
b := New(tmpdir.FromPath(dir), WithMemLimit(4096))
t.Cleanup(func() { _ = b.Close() })
mustWrite(t, b, 0, repeat('A', 4096)) // exactly the limit
assert.Equal(t, repeat('A', 4096), readAll(t, b))
assert.Nil(t, b.file, "a buffer that stays inside the limit must not create a file")
assert.Empty(t, dirEntries(t, dir), "and must leave nothing on disk")
}
func TestSpillCreatesTheFileOnlyWhenNeeded(t *testing.T) {
dir := t.TempDir()
b := New(tmpdir.FromPath(dir), WithMemLimit(4096))
t.Cleanup(func() { _ = b.Close() })
mustWrite(t, b, 0, repeat('A', 4096))
require.Empty(t, dirEntries(t, dir))
mustWrite(t, b, 4096, repeat('B', 1)) // one byte past the limit
assert.NotNil(t, b.file)
assert.Len(t, dirEntries(t, dir), 1, "exactly one spill file")
}
func TestWriteStraddlingTheLimit(t *testing.T) {
const limit = 1024
b := newTestBuffer(t, WithMemLimit(limit))
payload := make([]byte, 2048)
for i := range payload {
payload[i] = byte(i % 251)
}
// starts below the limit and ends well past it, so the write is split across both tiers
mustWrite(t, b, limit-512, payload)
require.Equal(t, int64(0), b.Size(), "nothing was written at zero yet")
mustWrite(t, b, 0, repeat('Z', limit-512))
got := readAll(t, b)
require.Len(t, got, limit-512+2048)
assert.Equal(t, repeat('Z', limit-512), got[:limit-512], "the memory-only part")
assert.Equal(t, payload, got[limit-512:], "the straddling part reads back whole")
}
func TestMemLimitZeroSendsEverythingToDisk(t *testing.T) {
dir := t.TempDir()
b := New(tmpdir.FromPath(dir), WithMemLimit(0))
t.Cleanup(func() { _ = b.Close() })
mustWrite(t, b, 0, repeat('A', 16))
assert.Equal(t, repeat('A', 16), readAll(t, b))
assert.Len(t, dirEntries(t, dir), 1, "with no memory tier the first byte spills")
assert.Nil(t, b.mem)
}
func TestNegativeMemLimitIsTreatedAsZero(t *testing.T) {
b := newTestBuffer(t, WithMemLimit(-1))
mustWrite(t, b, 0, repeat('A', 8))
assert.Equal(t, repeat('A', 8), readAll(t, b))
}
func TestDefaultMemLimitApplies(t *testing.T) {
b := New(nil)
t.Cleanup(func() { _ = b.Close() })
assert.Equal(t, DefaultMemLimit, b.memLimit)
}
// --- no temp dir ------------------------------------------------------------------------------------
func TestNoTempDir(t *testing.T) {
t.Run("a write inside the memory limit needs no temp dir", func(t *testing.T) {
b := New(nil, WithMemLimit(4096))
t.Cleanup(func() { _ = b.Close() })
mustWrite(t, b, 0, repeat('A', 4096))
assert.Equal(t, repeat('A', 4096), readAll(t, b))
})
t.Run("a write past it fails rather than allocating", func(t *testing.T) {
b := New(nil, WithMemLimit(4096))
t.Cleanup(func() { _ = b.Close() })
_, err := b.WriteAt(repeat('B', 1), 4096)
require.Error(t, err)
assert.ErrorIs(t, err, ErrNoTempDir)
})
}
// --- input validation -------------------------------------------------------------------------------
func TestWriteAtRejectsBadInput(t *testing.T) {
b := newTestBuffer(t)
t.Run("negative offset", func(t *testing.T) {
_, err := b.WriteAt([]byte("x"), -1)
assert.Error(t, err)
})
t.Run("an offset plus length that would overflow", func(t *testing.T) {
_, err := b.WriteAt(repeat('x', 16), math.MaxInt64-8)
require.Error(t, err)
assert.Contains(t, err.Error(), "overflow",
"the check must be a subtraction, not off+len wrapping into an in-range end")
})
t.Run("an empty write is a no-op, not an extent", func(t *testing.T) {
n, err := b.WriteAt(nil, 500)
require.NoError(t, err)
assert.Zero(t, n)
assert.Empty(t, b.written, "a zero-length write records nothing")
})
}
func TestReadAtRejectsBadInput(t *testing.T) {
b := newTestBuffer(t)
mustWrite(t, b, 0, repeat('A', 16))
t.Run("negative offset", func(t *testing.T) {
_, err := b.ReadAt(make([]byte, 4), -1)
assert.Error(t, err)
})
t.Run("empty read", func(t *testing.T) {
n, err := b.ReadAt(nil, 0)
assert.NoError(t, err)
assert.Zero(t, n)
})
t.Run("read from an empty buffer", func(t *testing.T) {
empty := newTestBuffer(t)
n, err := empty.ReadAt(make([]byte, 4), 0)
assert.Zero(t, n)
assert.ErrorIs(t, err, io.EOF)
})
}
// TestReadAtHonorsTheReaderAtContract pins what io.ReaderAt requires: a short read must come with a
// non-nil error, and a full read must not invent one. Parsers built on io.ReaderAt (debug/elf among them)
// rely on this to tell "the file ends here" from "try again".
func TestReadAtHonorsTheReaderAtContract(t *testing.T) {
b := newTestBuffer(t, WithMemLimit(64))
mustWrite(t, b, 0, repeat('A', 200)) // spans both tiers
for _, size := range []int{1, 63, 64, 65, 199, 200} {
p := make([]byte, size)
n, err := b.ReadAt(p, 0)
assert.Equal(t, size, n, "size=%d", size)
assert.NoError(t, err, "a read fully inside the buffer must not report EOF (size=%d)", size)
}
p := make([]byte, 201)
n, err := b.ReadAt(p, 0)
assert.Equal(t, 200, n)
assert.ErrorIs(t, err, io.EOF, "one byte past the end is short and must say so")
}
func TestReadAtEveryOffsetAndLength(t *testing.T) {
const limit = 32
b := newTestBuffer(t, WithMemLimit(limit))
want := make([]byte, 100)
for i := range want {
want[i] = byte(i)
}
mustWrite(t, b, 0, want)
// every offset x length pair, so a tier-boundary off-by-one cannot hide
for off := 0; off <= len(want); off++ {
for l := 0; l <= len(want)-off+2; l++ {
p := make([]byte, l)
n, err := b.ReadAt(p, int64(off))
expected := len(want) - off
if l < expected {
expected = l
}
assert.Equal(t, expected, n, "off=%d len=%d", off, l)
assert.Equal(t, want[off:off+expected], p[:expected], "off=%d len=%d", off, l)
switch {
case l == 0:
assert.NoError(t, err, "off=%d len=0", off)
case off >= len(want):
assert.ErrorIs(t, err, io.EOF, "off=%d len=%d", off, l)
case l > expected:
assert.ErrorIs(t, err, io.EOF, "off=%d len=%d", off, l)
default:
assert.NoError(t, err, "off=%d len=%d", off, l)
}
}
}
}
// --- overwrite semantics ----------------------------------------------------------------------------
func TestOverwrite(t *testing.T) {
tests := []struct {
name string
memLimit int64
}{
{"in memory", 1 << 20},
{"on disk", 0},
{"across the boundary", 8},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := newTestBuffer(t, WithMemLimit(tt.memLimit))
mustWrite(t, b, 0, repeat('A', 16))
mustWrite(t, b, 4, repeat('B', 8))
want := append(append(repeat('A', 4), repeat('B', 8)...), repeat('A', 4)...)
assert.Equal(t, want, readAll(t, b), "the later write wins over the range it covers")
assert.Equal(t, int64(16), b.Size(), "an overwrite does not extend the buffer")
})
}
}
// --- extents ----------------------------------------------------------------------------------------
func TestWrittenExtentsAreSortedAndMerged(t *testing.T) {
tests := []struct {
name string
writes []extent
want []extent
}{
{
name: "disjoint stay separate, in order",
writes: []extent{{100, 150}, {0, 50}},
want: []extent{{0, 50}, {100, 150}},
},
{
name: "adjacent merge",
writes: []extent{{0, 50}, {50, 100}},
want: []extent{{0, 100}},
},
{
name: "overlapping merge",
writes: []extent{{0, 60}, {40, 100}},
want: []extent{{0, 100}},
},
{
name: "a write bridging two runs collapses all three",
writes: []extent{{0, 20}, {80, 100}, {20, 80}},
want: []extent{{0, 100}},
},
{
name: "a contained write changes nothing",
writes: []extent{{0, 100}, {20, 40}},
want: []extent{{0, 100}},
},
{
name: "identical writes collapse",
writes: []extent{{0, 10}, {0, 10}, {0, 10}},
want: []extent{{0, 10}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := newTestBuffer(t)
for _, w := range tt.writes {
mustWrite(t, b, w.start, repeat('x', int(w.end-w.start)))
}
assert.Equal(t, tt.want, b.written)
})
}
}
func TestWrittenExtentsStayCompactUnderManyAdjacentWrites(t *testing.T) {
b := newTestBuffer(t)
for i := int64(0); i < 1000; i++ {
mustWrite(t, b, i*8, repeat('A', 8))
}
assert.Len(t, b.written, 1, "a run of adjacent writes must collapse to one extent, not 1000")
assert.Equal(t, int64(8000), b.Size())
}
// --- lifecycle --------------------------------------------------------------------------------------
func TestCloseRemovesTheSpillFile(t *testing.T) {
dir := t.TempDir()
b := New(tmpdir.FromPath(dir), WithMemLimit(0))
mustWrite(t, b, 0, repeat('A', 32))
require.Len(t, dirEntries(t, dir), 1)
require.NoError(t, b.Close())
assert.Empty(t, dirEntries(t, dir), "Close must take the spill file with it")
}
func TestCloseIsIdempotent(t *testing.T) {
b := New(tmpdir.FromPath(t.TempDir()), WithMemLimit(0))
mustWrite(t, b, 0, repeat('A', 32))
require.NoError(t, b.Close())
assert.NoError(t, b.Close(), "a second Close is a no-op, not a double-close")
assert.NoError(t, b.Close())
}
func TestCloseOnNilReceiver(t *testing.T) {
var b *Buffer
assert.NotPanics(t, func() {
assert.NoError(t, b.Close())
})
}
func TestCloseWithoutASpillFile(t *testing.T) {
b := New(nil, WithMemLimit(1<<20))
mustWrite(t, b, 0, repeat('A', 32))
assert.NoError(t, b.Close(), "a memory-only buffer closes cleanly with no file to remove")
}
func TestWriteAfterCloseFails(t *testing.T) {
b := New(tmpdir.FromPath(t.TempDir()))
mustWrite(t, b, 0, repeat('A', 8))
require.NoError(t, b.Close())
_, err := b.WriteAt(repeat('B', 8), 0)
require.Error(t, err, "a closed buffer must refuse writes rather than resurrect its file")
assert.ErrorIs(t, err, os.ErrClosed)
}
// TestReadAfterCloseFails pins that a closed buffer reads as closed rather than as empty. Close drops the
// extents, so without the check every read would come back io.EOF and a caller could not tell a buffer it
// still owns from one somebody already released.
func TestReadAfterCloseFails(t *testing.T) {
b := New(tmpdir.FromPath(t.TempDir()))
mustWrite(t, b, 0, repeat('A', 8))
require.NoError(t, b.Close())
n, err := b.ReadAt(make([]byte, 8), 0)
assert.Zero(t, n)
assert.ErrorIs(t, err, os.ErrClosed)
at, ok := b.FirstGap(8, 100)
assert.False(t, ok, "and a closed buffer offers nowhere to write")
assert.Zero(t, at)
}
// TestWriteFailureLeavesTheBufferUnchanged pins the ordering inside WriteAt: the fallible half runs first,
// so a spill file that cannot be created does not take the memory tier down with it.
func TestWriteFailureLeavesTheBufferUnchanged(t *testing.T) {
// FromPath takes the directory as given, so a path that does not exist makes file creation fail
b := New(tmpdir.FromPath(filepath.Join(t.TempDir(), "no-such-dir")), WithMemLimit(16))
t.Cleanup(func() { _ = b.Close() })
mustWrite(t, b, 0, repeat('A', 8))
_, err := b.WriteAt(repeat('B', 32), 0) // straddles the limit, so it needs the spill file
require.Error(t, err)
assert.Equal(t, int64(8), b.Size(), "a failed write records nothing")
assert.Nil(t, b.file, "and leaves no half-made spill file behind")
p := make([]byte, 8)
n, err := b.ReadAt(p, 0)
require.NoError(t, err)
require.Equal(t, 8, n)
assert.Equal(t, repeat('A', 8), p, "the memory tier still holds what it held")
mustWrite(t, b, 8, repeat('C', 8)) // inside the limit, so it still needs no file
assert.Equal(t, append(repeat('A', 8), repeat('C', 8)...), readAll(t, b))
}
// --- interface compliance ---------------------------------------------------------------------------
func TestBufferSatisfiesTheStandardInterfaces(t *testing.T) {
b := newTestBuffer(t)
var (
_ io.WriterAt = b
_ io.ReaderAt = b
_ io.Closer = b
)
assert.Implements(t, (*io.ReaderAt)(nil), b)
assert.Implements(t, (*io.WriterAt)(nil), b)
}
// TestWorksThroughIOOffsetWriter pins the composition every consumer uses: stream a decoded block into
// the buffer at a chosen offset without the writer knowing where it lands.
func TestWorksThroughIOOffsetWriter(t *testing.T) {
b := newTestBuffer(t, WithMemLimit(16))
w := io.NewOffsetWriter(b, 8)
n, err := io.Copy(w, bytes.NewReader(repeat('B', 32)))
require.NoError(t, err)
require.Equal(t, int64(32), n)
mustWrite(t, b, 0, repeat('A', 8))
got := readAll(t, b)
assert.Equal(t, append(repeat('A', 8), repeat('B', 32)...), got)
}
// TestWorksWithIOSectionReader pins the other direction: parsers wrap an io.ReaderAt in a SectionReader,
// and it must see a consistent length.
func TestWorksWithIOSectionReader(t *testing.T) {
b := newTestBuffer(t)
mustWrite(t, b, 0, repeat('A', 100))
sr := io.NewSectionReader(b, 0, b.Size())
got, err := io.ReadAll(sr)
require.NoError(t, err)
assert.Equal(t, repeat('A', 100), got)
}
// --- model-based randomized testing -----------------------------------------------------------------
// TestAgainstAReferenceModel drives random writes through the buffer and a dumb reference at the same
// time, then compares every readable byte. This is what catches tier-boundary and merge bugs that
// hand-written cases miss.
func TestAgainstAReferenceModel(t *testing.T) {
const universe = 8192
for _, limit := range []int64{0, 1, 64, 1000, 4096, universe * 2} {
t.Run("memLimit="+itoa(limit), func(t *testing.T) {
for seed := int64(0); seed < 40; seed++ {
rng := rand.New(rand.NewSource(seed)) //nolint:gosec // deterministic test input, not crypto
b := newTestBuffer(t, WithMemLimit(limit))
model := make([]byte, universe)
written := make([]bool, universe)
for op := 0; op < 40; op++ {
off := rng.Int63n(universe)
l := rng.Int63n(256) + 1
if off+l > universe {
l = universe - off
}
payload := make([]byte, l)
for i := range payload {
payload[i] = byte(rng.Intn(256))
}
mustWrite(t, b, off, payload)
copy(model[off:], payload)
for i := off; i < off+l; i++ {
written[i] = true
}
}
// the reference contiguous prefix
var want int64
for want < universe && written[want] {
want++
}
require.Equal(t, want, b.Size(), "seed=%d limit=%d", seed, limit)
if want == 0 {
continue
}
assert.Equal(t, model[:want], readAll(t, b), "seed=%d limit=%d", seed, limit)
}
})
}
}
// TestRandomReadsMatchTheModel checks partial reads at arbitrary offsets, which the whole-buffer
// comparison above cannot reach.
func TestRandomReadsMatchTheModel(t *testing.T) {
rng := rand.New(rand.NewSource(7)) //nolint:gosec // deterministic test input, not crypto
b := newTestBuffer(t, WithMemLimit(97))
model := make([]byte, 1000)
for i := range model {
model[i] = byte(rng.Intn(256))
}
// written in random-sized chunks, in order, so the whole range is covered
for off := 0; off < len(model); {
l := rng.Intn(50) + 1
if off+l > len(model) {
l = len(model) - off
}
mustWrite(t, b, int64(off), model[off:off+l])
off += l
}
require.Equal(t, int64(len(model)), b.Size())
for i := 0; i < 500; i++ {
off := rng.Intn(len(model))
l := rng.Intn(len(model)-off) + 1
p := make([]byte, l)
n, err := b.ReadAt(p, int64(off))
require.NoError(t, err)
require.Equal(t, l, n)
require.Equal(t, model[off:off+l], p, "off=%d len=%d", off, l)
}
}
// --- helpers ----------------------------------------------------------------------------------------
func dirEntries(t *testing.T, dir string) []os.DirEntry {
t.Helper()
entries, err := os.ReadDir(dir)
if errors.Is(err, os.ErrNotExist) {
return nil
}
require.NoError(t, err)
// tmpdir.NewFile creates the spill file inside the root it is given
var files []os.DirEntry
for _, e := range entries {
if e.IsDir() {
sub, err := os.ReadDir(filepath.Join(dir, e.Name()))
require.NoError(t, err)
for _, s := range sub {
files = append(files, s)
}
continue
}
files = append(files, e)
}
return files
}
func itoa(n int64) string {
if n == 0 {
return "0"
}
var digits []byte
for n > 0 {
digits = append([]byte{byte('0' + n%10)}, digits...)
n /= 10
}
return string(digits)
}
// TestMemoryTierGrowsAmortized is the regression test for quadratic growth. Sizing the tier to exactly
// what each write needs reallocates and copies the whole thing per call, so a caller streaming a block in
// small chunks (which is what io.Copy through an OffsetWriter does) pays O(n^2): filling a 1MB tier in
// 32KB chunks cost ~16MB of allocation and showed up as a 19MB spike decompressing a 16MB payload.
func TestMemoryTierGrowsAmortized(t *testing.T) {
const (
limit = 1 << 20
chunk = 32 << 10
)
b := newTestBuffer(t, WithMemLimit(limit))
var before, after runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&before)
for off := int64(0); off < limit; off += chunk {
mustWrite(t, b, off, repeat('A', chunk))
}
runtime.ReadMemStats(&after)
allocated := after.TotalAlloc - before.TotalAlloc
t.Logf("filling a %d byte tier in %d byte chunks allocated %d bytes", limit, chunk, allocated)
assert.Less(t, allocated, uint64(4*limit),
"growth has to be amortized, not a fresh copy of the whole tier per write")
assert.Equal(t, int64(limit), b.Size())
}
// TestMemoryTierNeverExceedsTheLimit pins the cap that geometric growth could otherwise overshoot.
func TestMemoryTierNeverExceedsTheLimit(t *testing.T) {
const limit = 1000
b := newTestBuffer(t, WithMemLimit(limit))
for off := int64(0); off < 4*limit; off += 100 {
mustWrite(t, b, off, repeat('A', 100))
assert.LessOrEqual(t, int64(len(b.mem)), int64(limit),
"the memory tier may never grow past its own limit (off=%d)", off)
}
assert.Equal(t, int64(4*limit), b.Size())
}
// TestFirstGap covers the placement question a caller writing at offsets of its own choosing has to ask.
// It moved here from the UPX cataloger, which used to walk an exported extent list to answer it itself.
func TestFirstGap(t *testing.T) {
tests := []struct {
name string
writes []extent
size int64
within int64
wantAt int64
wantOK bool
}{
{
name: "an empty buffer starts at zero",
size: 16,
within: 100,
wantAt: 0, wantOK: true,
},
{
name: "the gap between two runs comes first",
writes: []extent{{0, 1000}, {1016, 2000}},
size: 16,
within: 3000,
wantAt: 1000, wantOK: true,
},
{
name: "with the gap filled the tail is what is left",
writes: []extent{{0, 2000}},
size: 1000,
within: 3000,
wantAt: 2000, wantOK: true,
},
{
name: "a gap too small is skipped for a later one",
writes: []extent{{0, 100}, {108, 200}},
size: 50,
within: 1000,
wantAt: 200, wantOK: true,
},
{
name: "nowhere left to fit is reported rather than squeezed in",
writes: []extent{{0, 1000}, {1016, 2000}},
size: 1001,
within: 3000,
wantOK: false,
},
{
name: "a run past the declared total must not wrap the remaining-space subtraction",
writes: []extent{{0, 200}},
size: 8,
within: 100,
wantOK: false,
},
{
name: "exactly filling the tail is allowed",
writes: []extent{{0, 900}},
size: 100,
within: 1000,
wantAt: 900, wantOK: true,
},
{
name: "a gap between two runs is still cut off by the bound",
writes: []extent{{0, 10}, {1000, 1010}},
size: 45,
within: 50,
wantOK: false,
},
{
name: "a gap between two runs that the bound still leaves room in",
writes: []extent{{0, 10}, {1000, 1010}},
size: 30,
within: 50,
wantAt: 10, wantOK: true,
},
{
name: "a negative size is refused rather than treated as zero",
size: -1,
within: 100,
wantOK: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := newTestBuffer(t)
for _, w := range tt.writes {
mustWrite(t, b, w.start, repeat('x', int(w.end-w.start)))
}
at, ok := b.FirstGap(tt.size, tt.within)
assert.Equal(t, tt.wantOK, ok)
if tt.wantOK {
assert.Equal(t, tt.wantAt, at)
}
})
}
}
// TestFirstGapNeverOverlapsWhatWasWritten is the property behind the table: whatever offset comes back has
// to be usable, so writing size bytes there must not land on top of anything already stored.
func TestFirstGapNeverOverlapsWhatWasWritten(t *testing.T) {
rng := rand.New(rand.NewSource(11)) //nolint:gosec // deterministic test input, not crypto
const universe = 4096
for seed := 0; seed < 200; seed++ {
b := newTestBuffer(t)
occupied := make([]bool, universe)
for op := 0; op < 6; op++ {
// deliberately reaches past universe too, so the bound has to cut a between-extents gap short
off := rng.Int63n(2 * universe)
l := rng.Int63n(200) + 1
mustWrite(t, b, off, repeat('x', int(l)))
for i := off; i < off+l && i < universe; i++ {
occupied[i] = true
}
}
size := rng.Int63n(100) + 1
at, ok := b.FirstGap(size, universe)
if !ok {
continue
}
require.LessOrEqual(t, at+size, int64(universe), "the result must fit inside the declared total")
for i := at; i < at+size; i++ {
require.False(t, occupied[i], "FirstGap returned %d for %d bytes, but %d is taken", at, size, i)
}
}
}
+21
View File
@@ -35,3 +35,24 @@ Create the new schema by running `make generate-json-schema` from the root of th
- If there is an existing schema for the given version and the new schema **does not** match the existing schema, an error is shown indicating to increment the version appropriately (see the "Versioning" section)
***Note: never delete a JSON schema and never change an existing JSON schema once it has been published in a release!*** Only add new schemas with a newly incremented version. All previous schema files must be stored in the `schema/json/` directory.
### Exception: `description`-only corrections
A published schema may be amended in place for one narrow case: the change touches only `description` text and leaves the data shape identical. Descriptions are documentation carried alongside the schema rather than constraints a validator evaluates, so correcting one cannot invalidate a document that already validated against that version. Minting a new version instead would leave the old one permanently describing the tool incorrectly, and spend a version number on no semantic change.
This applies when **every** one of the following holds:
- the only differences are `description` values
- no field, type, enum, `required` entry, or `$ref` is added, removed, or altered
- the `$id` version is unchanged
Anything else, including adding a field that happens to be optional, is a schema change and needs a version bump per the "Versioning" section above.
The generator blocks an in-place edit by design, since it refuses to overwrite a file that differs from what it would produce. To amend one, delete the schema file and regenerate so it is rewritten from the current Go doc comments:
```bash
rm schema/json/schema-$VERSION.json
make generate-json-schema
```
The result is byte-identical to what the generator produces, so do not hand-edit the JSON. Re-running `make generate-json-schema` afterwards should report `No change to the existing schema!`, and `make check-json-schema-drift` should pass. Confirm with `git diff` that the only changes are the intended description lines.
+1 -1
View File
@@ -1662,7 +1662,7 @@
"type": "array"
},
"type": "object",
"description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, while\nthe \"stdlib\" scope populates only the stdlib package."
"description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, the\n\"extended-stdlib\" scope populates the stdlib package plus every module under golang.org/x/, and the\n\"stdlib\" scope populates only the stdlib package. The capture-symbols-modules glob patterns populate\nany additional modules they match."
}
},
"type": "object",
+1 -1
View File
@@ -1662,7 +1662,7 @@
"type": "array"
},
"type": "object",
"description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, while\nthe \"stdlib\" scope populates only the stdlib package."
"description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, the\n\"extended-stdlib\" scope populates the stdlib package plus every module under golang.org/x/, and the\n\"stdlib\" scope populates only the stdlib package. The capture-symbols-modules glob patterns populate\nany additional modules they match."
}
},
"type": "object",
+6
View File
@@ -12,6 +12,10 @@ const (
// SymbolScopeStdlib captures symbols only for the synthetic "stdlib" package, leaving module packages without symbols.
SymbolScopeStdlib SymbolScope = "stdlib"
// SymbolScopeExtendedStdlib captures symbols for the synthetic "stdlib" package as well as every module
// under golang.org/x/ (the extended standard library).
SymbolScopeExtendedStdlib SymbolScope = "extended-stdlib"
// SymbolScopeAll captures symbols for all module packages as well as the synthetic "stdlib" package.
SymbolScopeAll SymbolScope = "all"
)
@@ -21,6 +25,8 @@ func (s SymbolScope) Parse() SymbolScope {
switch strings.ToLower(strings.TrimSpace(string(s))) {
case string(SymbolScopeAll):
return SymbolScopeAll
case string(SymbolScopeExtendedStdlib):
return SymbolScopeExtendedStdlib
case string(SymbolScopeStdlib):
return SymbolScopeStdlib
}
+5
View File
@@ -16,6 +16,11 @@ func Test_SymbolScope_Parse(t *testing.T) {
{" all ", SymbolScopeAll},
{"stdlib", SymbolScopeStdlib},
{"Stdlib", SymbolScopeStdlib},
{"extended-stdlib", SymbolScopeExtendedStdlib},
{"Extended-Stdlib", SymbolScopeExtendedStdlib},
{"EXTENDED-STDLIB", SymbolScopeExtendedStdlib},
{" extended-stdlib ", SymbolScopeExtendedStdlib},
{"extended_stdlib", SymbolScopeNone},
{"none", SymbolScopeNone},
{"", SymbolScopeNone},
{"true", SymbolScopeNone},
+1 -1
View File
@@ -201,7 +201,7 @@ func (c Attributes) String() string {
// sanitize is a modified version of WFNize function from nvdtools
// that quotes all the allowed punctation chars with a slash and replaces
// spaces with underscores. It differs from the upstream implmentation as
// spaces with underscores. It differs from the upstream implementation as
// it does not use the buggy nvdtools implementation, specifically the "addSlashesAt" part of the
// function which stops the loop as soon as it encounters ":" a valid
// character for a WFN attribute after quoting, but the way nvdtools
+2 -1
View File
@@ -10,11 +10,12 @@ import (
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/elfutil"
"github.com/anchore/syft/syft/internal/unionreader"
)
func findELFFeatures(data *file.Executable, reader unionreader.UnionReader) error {
f, err := elf.NewFile(reader)
f, err := elfutil.NewFile(reader)
if err != nil {
return err
}
+8 -2
View File
@@ -1,6 +1,12 @@
FROM gcc:9.5.0
RUN apt update -y && apt install -y clang cmake git make m4 pkg-config zlib1g-dev
# bullseye is EOL: deb.debian.org dropped it, so pull from archive.debian.org (whose Release files are
# permanently expired, hence Check-Valid-Until). bullseye-security is deliberately omitted -- its index is
# still served but the pool behind it 404s, and it has not been moved to the archive yet. These are throwaway
# fixture build tools, so base bullseye package versions are fine.
RUN printf 'deb http://archive.debian.org/debian bullseye main\ndeb http://archive.debian.org/debian bullseye-updates main\n' > /etc/apt/sources.list \
&& apt -o Acquire::Check-Valid-Until=false update -y \
&& apt install -y clang cmake git make m4 pkg-config zlib1g-dev
## from https://github.com/runsafesecurity/selfrando/blob/tb-v0.4.2/docs/linux-build-instructions.md
#RUN git clone https://github.com/runsafesecurity/selfrando.git && \
@@ -13,4 +19,4 @@ RUN apt update -y && apt install -y clang cmake git make m4 pkg-config zlib1g-de
#RUN cd selfrando && make -j`nprocs --all`
#RUN cd selfrando && make install
RUN curl -o /bin/checksec https://raw.githubusercontent.com/slimm609/checksec.sh/2.6.0/checksec && chmod +x /bin/checksec
RUN curl -f -o /bin/checksec https://raw.githubusercontent.com/slimm609/checksec.sh/2.6.0/checksec && chmod +x /bin/checksec
+20
View File
@@ -152,6 +152,26 @@ func NewLocationFromImage(accessPath string, ref file.Reference, img *image.Imag
}
}
// NewVirtualLocationFromImage creates a new Location surfaced at realPath but whose contents and metadata are
// described by the given reference. Note that RealPath may differ from ref.RealPath; callers must not assume the two
// agree for these locations. The FileSystemID comes from the given ref's layer.
func NewVirtualLocationFromImage(realPath, accessPath string, ref file.Reference, img *image.Image) Location {
layer := img.FileCatalog.Layer(ref)
return Location{
LocationData: LocationData{
Coordinates: Coordinates{
RealPath: realPath,
FileSystemID: layer.Metadata.Digest,
},
AccessPath: accessPath,
ref: ref,
},
LocationMetadata: LocationMetadata{
Annotations: map[string]string{},
},
}
}
// NewLocationFromDirectory creates a new Location representing the given path (extracted from the Reference) relative to the given directory.
func NewLocationFromDirectory(responsePath string, fd string, ref file.Reference) Location {
return Location{
@@ -0,0 +1,181 @@
package spdxhelpers
import (
"fmt"
"testing"
"time"
"github.com/spdx/tools-golang/spdx"
"github.com/spdx/tools-golang/spdx/v2/common"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// The tests in this file cover findRootPackages, which locates the packages an SPDX document declares it DESCRIBES
// (or which declare themselves DESCRIBED_BY the document). Until it was rewritten, this function compared every
// package against every relationship, so its cost grew with packages x relationships. On real documents that made
// SPDX decoding quadratic in document size: a 119MB SPDX JSON document (70k packages, 153k relationships) spent
// 72s here, a 238MB document 266s, and a 1.3GB document did not finish within 90 minutes. Encoding the same
// documents, and decoding equivalently sized syft-json or CycloneDX documents, is linear and takes seconds.
//
// Test_findRootPackages pins the exact semantics of the original implementation, including the corner cases a
// rewrite could easily change: result order follows package order, an element described twice appears twice, and
// only edges that touch the DOCUMENT element count. Test_findRootPackages_scalesLinearly is the regression guard
// for the performance fix itself.
func Test_findRootPackages(t *testing.T) {
pkgA := &spdx.Package{PackageSPDXIdentifier: "Package-a"}
pkgB := &spdx.Package{PackageSPDXIdentifier: "Package-b"}
pkgC := &spdx.Package{PackageSPDXIdentifier: "Package-c"}
describes := func(id common.ElementID) *spdx.Relationship {
return &spdx.Relationship{
RefA: common.DocElementID{ElementRefID: "DOCUMENT"},
RefB: common.DocElementID{ElementRefID: id},
Relationship: spdx.RelationshipDescribes,
}
}
describedBy := func(id common.ElementID) *spdx.Relationship {
return &spdx.Relationship{
RefA: common.DocElementID{ElementRefID: id},
RefB: common.DocElementID{ElementRefID: "DOCUMENT"},
Relationship: spdx.RelationshipDescribedBy,
}
}
tests := []struct {
name string
packages []*spdx.Package
relationships []*spdx.Relationship
want []*spdx.Package
}{
{
name: "no relationships",
packages: []*spdx.Package{pkgA, pkgB},
relationships: nil,
want: nil,
},
{
name: "document describes one package",
packages: []*spdx.Package{pkgA, pkgB, pkgC},
relationships: []*spdx.Relationship{
{RefA: common.DocElementID{ElementRefID: "Package-a"}, RefB: common.DocElementID{ElementRefID: "Package-b"}, Relationship: spdx.RelationshipContains},
describes("Package-b"),
},
want: []*spdx.Package{pkgB},
},
{
name: "package described by the document",
packages: []*spdx.Package{pkgA, pkgB, pkgC},
relationships: []*spdx.Relationship{
describedBy("Package-c"),
},
want: []*spdx.Package{pkgC},
},
{
name: "multiple roots are returned in package order",
packages: []*spdx.Package{pkgA, pkgB, pkgC},
relationships: []*spdx.Relationship{
describes("Package-c"),
describedBy("Package-a"),
},
want: []*spdx.Package{pkgA, pkgC},
},
{
name: "duplicate edges yield duplicate roots (preserved behavior)",
packages: []*spdx.Package{pkgA, pkgB},
relationships: []*spdx.Relationship{
describes("Package-a"),
describedBy("Package-a"),
},
want: []*spdx.Package{pkgA, pkgA},
},
{
name: "describes edges not from the document are ignored",
packages: []*spdx.Package{pkgA, pkgB},
relationships: []*spdx.Relationship{
{RefA: common.DocElementID{ElementRefID: "Package-a"}, RefB: common.DocElementID{ElementRefID: "Package-b"}, Relationship: spdx.RelationshipDescribes},
{RefA: common.DocElementID{ElementRefID: "Package-b"}, RefB: common.DocElementID{ElementRefID: "Package-a"}, Relationship: spdx.RelationshipDescribedBy},
},
want: nil,
},
{
name: "described element that is not a package is ignored",
packages: []*spdx.Package{pkgA},
relationships: []*spdx.Relationship{
describes("File-1"),
},
want: nil,
},
{
name: "nil packages and relationships are skipped",
packages: []*spdx.Package{nil, pkgA},
relationships: []*spdx.Relationship{
nil,
describes("Package-a"),
},
want: []*spdx.Package{pkgA},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := findRootPackages(&spdx.Document{
Packages: tt.packages,
Relationships: tt.relationships,
})
assert.Equal(t, tt.want, got)
})
}
}
// Test_findRootPackages_scalesLinearly guards against reintroducing a packages x relationships scan. At 50k
// packages and 100k relationships (5e9 comparisons) the previous implementation needs on the order of a minute on
// a fast laptop and several minutes on a CI runner, whereas the indexed implementation completes in about a
// millisecond. The 10s bound is therefore loose enough never to flake on slow hardware yet far below what any
// quadratic implementation could achieve.
func Test_findRootPackages_scalesLinearly(t *testing.T) {
doc := largeSPDXDocument(50_000, 100_000)
start := time.Now()
got := findRootPackages(doc)
elapsed := time.Since(start)
require.Len(t, got, 1)
assert.Equal(t, common.ElementID("Package-0"), got[0].PackageSPDXIdentifier)
assert.Less(t, elapsed, 10*time.Second, "findRootPackages took %s on %d packages and %d relationships", elapsed, len(doc.Packages), len(doc.Relationships))
}
// Benchmark_findRootPackages measures the same 50k x 100k document as the scaling test so the two can be compared
// directly: run with `go test ./syft/format/common/spdxhelpers/ -run xxx -bench findRootPackages`.
func Benchmark_findRootPackages(b *testing.B) {
doc := largeSPDXDocument(50_000, 100_000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = findRootPackages(doc)
}
}
// largeSPDXDocument builds a document with the given number of packages and relationships where the document
// describes exactly the first package and every other relationship links packages to one another.
func largeSPDXDocument(packages int, relationships int) *spdx.Document {
doc := &spdx.Document{}
for i := 0; i < packages; i++ {
doc.Packages = append(doc.Packages, &spdx.Package{
PackageSPDXIdentifier: common.ElementID(fmt.Sprintf("Package-%d", i)),
})
}
doc.Relationships = append(doc.Relationships, &spdx.Relationship{
RefA: common.DocElementID{ElementRefID: "DOCUMENT"},
RefB: common.DocElementID{ElementRefID: "Package-0"},
Relationship: spdx.RelationshipDescribes,
})
for i := 1; i < relationships; i++ {
doc.Relationships = append(doc.Relationships, &spdx.Relationship{
RefA: common.DocElementID{ElementRefID: common.ElementID(fmt.Sprintf("Package-%d", i%packages))},
RefB: common.DocElementID{ElementRefID: common.ElementID(fmt.Sprintf("Package-%d", (i+1)%packages))},
Relationship: spdx.RelationshipDependsOn,
})
}
return doc
}
+23 -13
View File
@@ -83,20 +83,30 @@ func removeRelationships(relationships []*spdx.Relationship, spdxID spdx.Element
}
func findRootPackages(doc *spdx.Document) (out []*spdx.Package) {
// index the DESCRIBES / DESCRIBED_BY relationships once, counting how many
// times each element is described by the document, so that the result
// matches the previous packages x relationships scan exactly (one entry per
// matching relationship) without the quadratic cost.
describedCount := make(map[spdx.ElementID]int)
for _, r := range doc.Relationships {
if r == nil {
continue
}
switch {
case r.RefA.ElementRefID == "DOCUMENT" && r.Relationship == spdx.RelationshipDescribes:
describedCount[r.RefB.ElementRefID]++
case r.RefB.ElementRefID == "DOCUMENT" && r.Relationship == spdx.RelationshipDescribedBy:
describedCount[r.RefA.ElementRefID]++
}
}
if len(describedCount) == 0 {
return nil
}
for _, p := range doc.Packages {
for _, r := range doc.Relationships {
describes := r.RefA.ElementRefID == "DOCUMENT" &&
r.Relationship == spdx.RelationshipDescribes &&
r.RefB.ElementRefID == p.PackageSPDXIdentifier
describedBy := r.RefB.ElementRefID == "DOCUMENT" &&
r.Relationship == spdx.RelationshipDescribedBy &&
r.RefA.ElementRefID == p.PackageSPDXIdentifier
if !describes && !describedBy {
continue
}
if p == nil {
continue
}
for i := 0; i < describedCount[p.PackageSPDXIdentifier]; i++ {
out = append(out, p)
}
}
+3
View File
@@ -21,6 +21,9 @@ func init() {
func Decoders() []sbom.FormatDecoder {
return []sbom.FormatDecoder{
// syft-json must remain the first decoder tried: it is by far the most common input, and its decoder can
// identify a syft document from the tail of the file alone. Trying anything else first would pay for a full
// parse of a large document by another decoder before reaching the common path.
syftjson.NewFormatDecoder(),
cyclonedxxml.NewFormatDecoder(),
cyclonedxjson.NewFormatDecoder(),
+14 -1
View File
@@ -72,8 +72,21 @@ func (o *offsetReadSeeker) Seek(offset int64, whence int) (int64, error) {
}
newOffset, err := o.rdr.Seek(offset, io.SeekCurrent)
return newOffset - o.offset, err
case io.SeekEnd:
newOffset, err := o.rdr.Seek(offset, io.SeekEnd)
if err != nil {
return 0, fmt.Errorf("cannot seek end: %w", err)
}
if newOffset < o.offset {
// landed before the start of this view; move back to the start so the position stays valid
if _, err := o.rdr.Seek(o.offset, io.SeekStart); err != nil {
return 0, fmt.Errorf("cannot seek end: %w", err)
}
return 0, fmt.Errorf("cannot seek < 0")
}
return newOffset - o.offset, nil
}
return 0, fmt.Errorf("only SeekStart and SeekCurrent supported")
return 0, fmt.Errorf("only SeekStart, SeekCurrent, and SeekEnd supported")
}
var _ io.ReadSeeker = (*offsetReadSeeker)(nil)
@@ -179,11 +179,25 @@ func Test_offsetReadSeeker(t *testing.T) {
whence: io.SeekCurrent,
expected: "4",
},
{
name: "seek end",
input: abcd1234offset(2),
seek: -4,
whence: io.SeekEnd,
expected: "1234",
},
{
name: "seek end before the start of the view",
input: abcd1234offset(2),
seek: -7,
whence: io.SeekEnd,
wantErr: require.Error, // would be < offset, which is an error
},
{
name: "bad whence",
input: abcd1234,
seek: 1,
whence: io.SeekEnd,
whence: 99,
wantErr: require.Error,
},
}
@@ -81,7 +81,7 @@
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:oci/user-image-input@sha256%3A2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch="
"referenceLocator": "pkg:oci/user-image-input@sha256%3A2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch=amd64"
}
],
"primaryPackagePurpose": "CONTAINER"
@@ -81,7 +81,7 @@
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:oci/user-image-input@sha256%3A2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch="
"referenceLocator": "pkg:oci/user-image-input@sha256%3A2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch=amd64"
}
],
"primaryPackagePurpose": "CONTAINER"
@@ -71,7 +71,7 @@ PackageChecksum: SHA256: 2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf4
PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: NOASSERTION
PackageCopyrightText: NOASSERTION
ExternalRef: PACKAGE-MANAGER purl pkg:oci/user-image-input@sha256%3A2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch=
ExternalRef: PACKAGE-MANAGER purl pkg:oci/user-image-input@sha256%3A2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch=amd64
##### Package: package-2
@@ -21,7 +21,7 @@ PackageChecksum: SHA256: 2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf4
PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: NOASSERTION
PackageCopyrightText: NOASSERTION
ExternalRef: PACKAGE-MANAGER purl pkg:oci/user-image-input@sha256%3A2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch=
ExternalRef: PACKAGE-MANAGER purl pkg:oci/user-image-input@sha256%3A2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch=amd64
##### Package: package-2
+177
View File
@@ -1,7 +1,9 @@
package syftjson
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"strings"
@@ -15,6 +17,12 @@ import (
"github.com/anchore/syft/syft/sbom"
)
// identifyTailSize is how much of the end of a document is inspected when identifying it from its trailing schema
// block, before falling back to parsing the whole document.
const identifyTailSize = 2 * 1024 * 1024
var schemaKey = []byte(`"schema"`)
var _ sbom.FormatDecoder = (*decoder)(nil)
type decoder struct{}
@@ -45,6 +53,12 @@ func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error)
return nil, "", "", fmt.Errorf("unable to decode syft-json document: %w", err)
}
if doc.Schema.Version == "" {
// Identify found a schema block that the parsed document does not have, so it was describing something
// else in the stream; returning the empty document it decoded into would be a silently wrong answer
return nil, "", "", fmt.Errorf("not a syft-json document")
}
if err := checkSupportedSchema(doc.Schema.Version, internal.JSONSchemaVersion); err != nil {
log.Warn(err)
}
@@ -57,6 +71,15 @@ func (d decoder) Identify(r io.Reader) (sbom.FormatID, string) {
return "", ""
}
// fast path: syft writes the schema block as the last key of the document (see model.Document), so for seekable
// input the format and version can be read from the tail without parsing a potentially very large document.
// Anything the tail cannot vouch for falls through to the full parse below.
if rs, ok := r.(io.ReadSeeker); ok {
if id, version, ok := identifyFromTail(rs); ok {
return id, version
}
}
type Document struct {
Schema model.Schema `json:"schema"`
}
@@ -78,6 +101,160 @@ func (d decoder) Identify(r io.Reader) (sbom.FormatID, string) {
return ID, doc.Schema.Version
}
// identifyFromTail reads at most identifyTailSize bytes from the end of the reader and attempts to identify the
// document from a top-level schema block found there. The reader is returned to its original position afterwards.
func identifyFromTail(rs io.ReadSeeker) (sbom.FormatID, string, bool) {
start, err := rs.Seek(0, io.SeekCurrent)
if err != nil {
return "", "", false
}
id, version, ok := readTail(rs, start)
if _, err := rs.Seek(start, io.SeekStart); err != nil {
// without the original position the caller cannot safely parse the document afterwards
log.WithFields("error", err).Debug("unable to restore reader position after inspecting document tail")
return "", "", false
}
return id, version, ok
}
// readTail does the seeking and reading for identifyFromTail, leaving the reader wherever it ends up.
func readTail(rs io.ReadSeeker, start int64) (sbom.FormatID, string, bool) {
end, err := rs.Seek(0, io.SeekEnd)
if err != nil || end <= start {
return "", "", false
}
// the tail only describes the document the reader is pointed at if that document starts where the reader is,
// so check the first byte: bytes ahead of it (a byte order mark, a log line prepended by a wrapper) would
// otherwise be accepted silently, leaving Identify claiming a document that cannot be decoded
if !opensObject(rs, start) {
return "", "", false
}
tailLen := min(end-start, int64(identifyTailSize))
tail := make([]byte, tailLen)
if _, err := rs.Seek(end-tailLen, io.SeekStart); err != nil {
return "", "", false
}
if _, err := io.ReadFull(rs, tail); err != nil {
return "", "", false
}
return identifyFromTailBytes(tail)
}
// opensObject reports whether the document at the given offset begins with a JSON object.
func opensObject(rs io.ReadSeeker, start int64) bool {
if _, err := rs.Seek(start, io.SeekStart); err != nil {
return false
}
head := make([]byte, 64)
n, err := io.ReadFull(rs, head)
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
return false
}
head = bytes.TrimLeft(head[:n], " \t\r\n")
return len(head) > 0 && head[0] == '{'
}
// identifyFromTailBytes looks for a syft schema block among the trailing bytes of a JSON document and, when one is
// found directly within the top-level object, returns the format and schema version it declares. The bytes may
// begin anywhere within the document, but must run to its very end.
func identifyFromTailBytes(tail []byte) (sbom.FormatID, string, bool) {
// find the last "schema" key. An unescaped quote cannot occur inside a JSON string, so a match whose opening
// quote is not preceded by a backslash is a genuine string token (and a key if a colon follows it).
idx := len(tail)
for {
idx = bytes.LastIndex(tail[:idx], schemaKey)
if idx <= 0 {
// not found, or found at the very start of the tail where we cannot tell whether it is escaped
return "", "", false
}
if tail[idx-1] != '\\' {
break
}
}
rest := bytes.TrimLeft(tail[idx+len(schemaKey):], " \t\r\n")
if len(rest) == 0 || rest[0] != ':' {
// a string value that happens to read "schema", not a key
return "", "", false
}
rest = rest[1:]
var schema model.Schema
dec := json.NewDecoder(bytes.NewReader(rest))
if err := dec.Decode(&schema); err != nil {
return "", "", false
}
// the schema block only identifies the document when it belongs to the top-level object
if !closesTopLevelObject(rest[dec.InputOffset():]) {
return "", "", false
}
if schema.Version == "" || !strings.Contains(schema.URL, "anchore/syft") {
return "", "", false
}
return ID, schema.Version, true
}
// closesTopLevelObject reports whether the given bytes, which follow a value somewhere within a JSON document, run
// to the end of that document closing exactly one enclosing object (and nothing else) at the very end. When true,
// the value they follow sits directly within the top-level object. Braces inside strings are ignored.
func closesTopLevelObject(b []byte) bool {
var (
depth int
inString bool
escaped bool
closed bool
)
for _, c := range b {
if closed {
// only whitespace may follow the top-level object
if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
continue
}
return false
}
if inString {
switch {
case escaped:
escaped = false
case c == '\\':
escaped = true
case c == '"':
inString = false
}
continue
}
switch c {
case '"':
inString = true
case '{', '[':
depth++
case '}':
depth--
if depth == -1 {
closed = true
}
case ']':
depth--
if depth == -1 {
// the enclosing container is an array, not the top-level object
return false
}
}
}
return closed
}
func checkSupportedSchema(documentVersion string, parserVersion string) error {
documentV, err := semver.NewVersion(documentVersion)
if err != nil {
@@ -0,0 +1,340 @@
package syftjson
import (
"bytes"
"encoding/json"
"errors"
"io"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/format/internal/stream"
"github.com/anchore/syft/syft/format/internal/testutil"
)
const (
testIdentifySchemaURL = "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-16.0.0.json"
testIdentifySchemaBlock = `"schema":{"version":"16.0.0","url":"` + testIdentifySchemaURL + `"}`
)
func Test_identifyFromTailBytes(t *testing.T) {
tests := []struct {
name string
doc string
wantVersion string
wantOK bool
}{
{
name: "syft key order (schema is the last key)",
doc: `{"artifacts":[],"descriptor":{"name":"syft"},` + testIdentifySchemaBlock + `}`,
wantVersion: "16.0.0",
wantOK: true,
},
{
name: "alphabetical key order (schema is followed by source)",
doc: `{"artifacts":[],` + testIdentifySchemaBlock + `,"source":{"id":"x","name":"img","type":"image","metadata":{"tags":["a","b"],"layers":[{"digest":"sha256:1"}]}}}`,
wantVersion: "16.0.0",
wantOK: true,
},
{
name: "pretty printed with a trailing newline",
doc: "{\n \"artifacts\": [],\n \"schema\": {\n \"version\": \"16.0.0\",\n \"url\": \"" + testIdentifySchemaURL + "\"\n }\n}\n",
wantVersion: "16.0.0",
wantOK: true,
},
{
name: "nested schema key is not mistaken for the document schema",
doc: `{"artifacts":[{"metadata":{` + testIdentifySchemaBlock + `}}],"descriptor":{}}`,
wantOK: false,
},
{
name: "top-level schema wins over an earlier nested one",
doc: `{"artifacts":[{"metadata":{"schema":{"version":"1","url":"anchore/syft"}}}],` + testIdentifySchemaBlock + `}`,
wantVersion: "16.0.0",
wantOK: true,
},
{
name: "escaped schema text (with braces) inside a trailing string is ignored",
doc: `{"artifacts":[],` + testIdentifySchemaBlock + `,"source":{"name":"contains \"schema\":{\"version\":\"9\"} text"}}`,
wantVersion: "16.0.0",
wantOK: true,
},
{
name: "array wrapped document",
doc: `[{"artifacts":[],` + testIdentifySchemaBlock + `}]`,
wantOK: false,
},
{
name: "truncated document",
doc: `{"artifacts":[],` + testIdentifySchemaBlock,
wantOK: false,
},
{
name: "trailing content after the document",
doc: `{"artifacts":[],` + testIdentifySchemaBlock + `}{"another":"document"}`,
wantOK: false,
},
{
name: "schema value is not an object",
doc: `{"artifacts":[],"schema":"16.0.0"}`,
wantOK: false,
},
{
name: "schema is a string value rather than a key",
doc: `{"artifacts":[],"kind":"schema"}`,
wantOK: false,
},
{
name: "not a syft schema url",
doc: `{"artifacts":[],"schema":{"version":"1.0","url":"https://example.com/schema.json"}}`,
wantOK: false,
},
{
name: "no schema key at all",
doc: `{"bomFormat":"CycloneDX","specVersion":"1.6","components":[]}`,
wantOK: false,
},
{
name: "schema key at the very start of the tail cannot be vouched for",
doc: testIdentifySchemaBlock + `}`,
wantOK: false,
},
{
name: "empty",
doc: "",
wantOK: false,
},
{
// the key's text is: he said "schema
name: "an escaped quote does not make a string into the schema key",
doc: `{"he said \"schema":{"version":"1.0","url":"https://example.com/anchore/syft"}}`,
wantOK: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
id, version, ok := identifyFromTailBytes([]byte(tt.doc))
assert.Equal(t, tt.wantOK, ok)
assert.Equal(t, tt.wantVersion, version)
if tt.wantOK {
assert.Equal(t, ID, id)
} else {
assert.Empty(t, id)
}
})
}
}
func Test_Identify_largeDocument(t *testing.T) {
// build a document comfortably larger than the tail window so only the tail path could avoid a full parse
unit := `{"name":"pkg","version":"1.0"},`
padding := strings.Repeat(unit, (identifyTailSize/len(unit))+1024)
doc := `{"artifacts":[` + padding + `{"name":"last"}],` + testIdentifySchemaBlock + `}`
require.Greater(t, len(doc), identifyTailSize)
t.Run("seekable reader", func(t *testing.T) {
r := strings.NewReader(doc)
id, version := NewFormatDecoder().Identify(r)
assert.Equal(t, ID, id)
assert.Equal(t, "16.0.0", version)
// the reader is left where it started so callers can go on to read the document
pos, err := r.Seek(0, io.SeekCurrent)
require.NoError(t, err)
assert.Equal(t, int64(0), pos)
})
t.Run("non-seekable reader falls back to a full parse", func(t *testing.T) {
r := struct{ io.Reader }{strings.NewReader(doc)}
id, version := NewFormatDecoder().Identify(r)
assert.Equal(t, ID, id)
assert.Equal(t, "16.0.0", version)
})
t.Run("reader positioned at a later document", func(t *testing.T) {
first := `{"artifacts":[],"schema":{"version":"1.0.0","url":"` + testIdentifySchemaURL + `"}}`
r := strings.NewReader(first + "\n" + doc)
_, err := r.Seek(int64(len(first)+1), io.SeekStart)
require.NoError(t, err)
// mirror what the decoder collection does: seeks become relative to the current position
sr, err := stream.SeekableReader(r)
require.NoError(t, err)
id, version := NewFormatDecoder().Identify(sr)
assert.Equal(t, ID, id)
assert.Equal(t, "16.0.0", version)
pos, err := sr.Seek(0, io.SeekCurrent)
require.NoError(t, err)
assert.Equal(t, int64(0), pos)
})
}
func Test_Identify_agreesWithFullParse(t *testing.T) {
// the tail is only a shortcut: whatever it decides must match what parsing the whole document says, including
// for documents it has to decline.
//
// note: a stream holding several concatenated documents is deliberately not covered. Identifying the document
// at the reader's position would mean proving no other document ends before the stream does, which means
// reading all of it, which is the cost the tail path exists to avoid. Decode still reports the document it
// actually parsed (see Test_Decode_rejectsADocumentTheTailDidNotDescribe).
syftDoc := `{"artifacts":[],` + testIdentifySchemaBlock + `}`
tests := []struct {
name string
doc string
wantVersion string
}{
{name: "plain document", doc: syftDoc, wantVersion: "16.0.0"},
{name: "byte order mark before the document", doc: "\xef\xbb\xbf" + syftDoc},
{name: "content before the document", doc: "WARN something happened\n" + syftDoc},
{name: "not a syft document", doc: `{"bomFormat":"CycloneDX","specVersion":"1.6","components":[]}`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := NewFormatDecoder()
// a bytes.Reader is seekable, so this takes the tail path
fastID, fastVersion := d.Identify(bytes.NewReader([]byte(tt.doc)))
// readerOnly hides Seek, so this takes the full parse
slowID, slowVersion := d.Identify(readerOnly{strings.NewReader(tt.doc)})
assert.Equal(t, slowID, fastID, "fast and slow paths disagree on the format")
assert.Equal(t, slowVersion, fastVersion, "fast and slow paths disagree on the version")
assert.Equal(t, tt.wantVersion, fastVersion)
})
}
}
// readerOnly hides any Seek method the underlying reader has, forcing the full parse.
type readerOnly struct {
io.Reader
}
func Test_Decode_rejectsADocumentTheTailDidNotDescribe(t *testing.T) {
// the tail can find a schema block belonging to a later document in the stream; whatever it decides, Decode
// must not hand back the empty SBOM that parsing the first document into model.Document would produce
doc := `{"bomFormat":"CycloneDX","specVersion":"1.6","components":[{"type":"library","name":"pkg-a"}]}` +
"\n" + `{"artifacts":[],` + testIdentifySchemaBlock + `}`
s, _, _, err := NewFormatDecoder().Decode(bytes.NewReader([]byte(doc)))
require.Error(t, err)
assert.Nil(t, s)
}
func Test_encoderWritesSchemaLast(t *testing.T) {
// identification reads the schema block from the tail of the document (see model.Document), which depends on
// the encoder always writing it as the final key
s := testutil.DirectoryInput(t, t.TempDir())
for _, pretty := range []bool{false, true} {
cfg := DefaultEncoderConfig()
cfg.Pretty = pretty
enc, err := NewFormatEncoderWithConfig(cfg)
require.NoError(t, err)
var buf bytes.Buffer
require.NoError(t, enc.Encode(&buf, s))
keys := topLevelKeys(t, buf.Bytes())
require.NotEmpty(t, keys)
assert.Equal(t, "schema", keys[len(keys)-1], "pretty=%v", pretty)
id, version, ok := identifyFromTailBytes(buf.Bytes())
assert.True(t, ok, "pretty=%v", pretty)
assert.Equal(t, ID, id)
assert.Equal(t, internal.JSONSchemaVersion, version)
}
}
// noEndSeeker behaves like a reader that buffers a stream as it goes: it can seek within what it has, but cannot
// seek relative to the end.
type noEndSeeker struct {
io.ReadSeeker
}
func (s noEndSeeker) Seek(offset int64, whence int) (int64, error) {
if whence == io.SeekEnd {
return 0, errors.New("SeekEnd is not supported")
}
return s.ReadSeeker.Seek(offset, whence)
}
func Test_Identify_fallsBackWhenTailIsUnavailable(t *testing.T) {
doc := `{"artifacts":[],` + testIdentifySchemaBlock + `}`
r := noEndSeeker{strings.NewReader(doc)}
id, version := NewFormatDecoder().Identify(r)
assert.Equal(t, ID, id)
assert.Equal(t, "16.0.0", version)
}
func Test_identifyFromTail_readerEdgeCases(t *testing.T) {
doc := `{"artifacts":[],` + testIdentifySchemaBlock + `}`
t.Run("empty reader", func(t *testing.T) {
r := strings.NewReader("")
_, _, ok := identifyFromTail(r)
assert.False(t, ok)
})
t.Run("reader already at the end has nothing to inspect and keeps its position", func(t *testing.T) {
r := strings.NewReader(doc)
_, err := r.Seek(0, io.SeekEnd)
require.NoError(t, err)
_, _, ok := identifyFromTail(r)
assert.False(t, ok)
pos, err := r.Seek(0, io.SeekCurrent)
require.NoError(t, err)
assert.Equal(t, int64(len(doc)), pos)
})
t.Run("whitespace around the colon and CRLF line endings", func(t *testing.T) {
crlf := "{\r\n \"artifacts\": [],\r\n \"schema\" : {\r\n \"version\": \"16.0.0\",\r\n \"url\": \"" + testIdentifySchemaURL + "\"\r\n }\r\n}\r\n"
id, version, ok := identifyFromTailBytes([]byte(crlf))
require.True(t, ok)
assert.Equal(t, ID, id)
assert.Equal(t, "16.0.0", version)
})
t.Run("document ending inside a string is not trusted", func(t *testing.T) {
truncated := `{"artifacts":[],` + testIdentifySchemaBlock + `,"source":{"name":"unterminated`
_, _, ok := identifyFromTailBytes([]byte(truncated))
assert.False(t, ok)
})
}
func topLevelKeys(t *testing.T, doc []byte) []string {
dec := json.NewDecoder(bytes.NewReader(doc))
tok, err := dec.Token()
require.NoError(t, err)
require.Equal(t, json.Delim('{'), tok)
var keys []string
for dec.More() {
tok, err := dec.Token()
require.NoError(t, err)
keys = append(keys, tok.(string))
var value json.RawMessage
require.NoError(t, dec.Decode(&value))
}
return keys
}
+8
View File
@@ -5,6 +5,14 @@ import (
"fmt"
)
// Field order matters: encoding/json writes struct fields in declaration order, and Schema must remain the last
// field so that the schema block is always the final key of the encoded document. Format identification relies on
// finding the schema block by reading only the tail of a (potentially very large) document; moving Schema earlier
// would not break decoding but would silently force a full parse of the whole document to identify it.
//
// (This block is deliberately separated from the doc comment below: the JSON schema generator lifts doc comments
// into schema descriptions, and changing the description would alter the published schema.)
// Document represents the syft cataloging findings as a JSON document
type Document struct {
Artifacts []Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
@@ -105,8 +105,8 @@
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NTgsImRpZ2VzdCI6InNoYTI1NjozNWE2NjU4ZTI0ZmFiOTJlYWU5ZWM2ZmMyNTJkZWM1ODk4NmM0YzAwNzg5MTc1OGQ0ZDM3YzdlNDNmYmJlMGM1In0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpkZmVmZTYxOGM4OWIwOGZlZjBmOWM3ZjFhMjY4MjUyMWRkZGJlMDNkNjY3OGY0YTlmYjliMDc4MzgxZDhlYjQ1In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OjM4ZGRjMjg0N2ZiNmJjYWZkNzQwMWI0YmYyN2MxMDAxNGI1ZDYwZTI0MDBiYzE4ODg5MGM3Y2I3Y2RkN2NkNmMifV19",
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8ifSwiY3JlYXRlZCI6IjIwMjQtMTEtMDRUMDQ6MTM6NDMuMzQwNTA3MTc1WiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDI0LTExLTA0VDA0OjEzOjQzLjMyMDg2OTk2OFoiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMS50eHQgL3NvbWVmaWxlLTEudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9LHsiY3JlYXRlZCI6IjIwMjQtMTEtMDRUMDQ6MTM6NDMuMzQwNTA3MTc1WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZGZlZmU2MThjODliMDhmZWYwZjljN2YxYTI2ODI1MjFkZGRiZTAzZDY2NzhmNGE5ZmI5YjA3ODM4MWQ4ZWI0NSIsInNoYTI1NjozOGRkYzI4NDdmYjZiY2FmZDc0MDFiNGJmMjdjMTAwMTRiNWQ2MGUyNDAwYmMxODg4OTBjN2NiN2NkZDdjZDZjIl19fQ==",
"repoDigests": [],
"architecture": "",
"os": ""
"architecture": "amd64",
"os": "linux"
}
},
"distro": {
+2
View File
@@ -175,6 +175,8 @@ func toSyftFileType(ty string) stereoscopeFile.Type {
case "SymbolicLink":
return stereoscopeFile.TypeSymLink
case "HardLink":
// kept for reading SBOMs written before stereoscope adopted hardlinks at index time (they are emitted as
// regular files now, except for malformed link entries)
return stereoscopeFile.TypeHardLink
case "Directory":
return stereoscopeFile.TypeDirectory
+469
View File
@@ -0,0 +1,469 @@
/*
Package elfutil provides an ELF opener that rejects sections whose compression headers declare an
implausible decompressed size.
debug/elf sizes a section's buffer from the section's own compression header, and a highly compressible
stream really does deliver the bytes that header promises, so internal/saferio's chunked read does not
help: it appends its way to every declared byte, and Go's 1.25x slice growth puts the lifetime cost near
five times that. 512MB of zeros is 510KB of zlib, and that file drives elf.NewFile through 2.6GB of
allocation and returns no error, which is a fatal Go OOM rather than a recoverable panic. Zlib caps out
near 1000:1, but debug/elf also accepts zstd, which passes 32000:1 on zeroed input, so the input needed
to declare a given size is smaller still.
The check runs in two parts, because debug/elf expands sections at two different times:
- the section-name string table is the one section elf.NewFile expands on its own, so it has to be
bounded against the raw bytes before the call is made. That is CheckSectionNameTable, and the walk
of the section table it needs is why this package decodes any ELF structures by hand at all.
- every other section is expanded lazily by (*Section).Open, so those are bounded after the parse,
where names, types and decompressed sizes are resolved already.
Only the sections syft can actually drive debug/elf into decompressing are bounded. DWARF is deliberately
excluded: syft never asks for it, so a binary carrying a large compressed .debug_info is cataloged rather
than skipped over a section nobody reads.
Sources:
- https://www.sco.com/developers/gabi/latest/ch4.sheader.html
- https://groups.google.com/g/generic-abi/c/satyPkuMisk
- https://docs.oracle.com/en/operating-systems/solaris/oracle-solaris/11.4/linkers-libraries/gnu-style-section-compression.html
- https://sourceware.org/pipermail/binutils/2015-July/089559.html
- https://cs.opensource.google/go/go/+/refs/tags/go1.26.5:src/debug/elf/file.go
- https://go.dev/doc/go1.19#linker
*/
package elfutil
import (
"debug/elf"
"encoding/binary"
"errors"
"fmt"
"io"
"math"
"strings"
intFile "github.com/anchore/syft/internal/file"
)
// maxDeclaredSectionSize bounds the decompressed size any single reachable section may declare.
//
// The bound only ever applies to a compressed section, and the gABI allows compression only on
// non-allocable ones, so nothing syft reads on the hot path (.data, .text, notes) can reach it. The
// realistic worst case is a compressed .symtab/.strtab on a very large binary, well under this. The
// trade-off is that a binary whose symbol or string table decompresses past this is skipped rather than
// cataloged; that is the correct direction to fail, since the alternative is OOM-killing the whole scan.
//
// Note that this is a per-section bound, not a per-file one: a file may hold several sections that each
// sit just under it. That is deliberate, since the sections syft actually reads are few.
const maxDeclaredSectionSize uint64 = 128 * intFile.MB
// ErrDeclaredSizeExceeded marks a file this package refused to parse because a section declared more
// decompressed bytes than maxDeclaredSectionSize allows.
//
// Exported so a caller can tell "syft declined to expand this" apart from "this file is broken". The
// distinction matters where the two are reported differently: the refusal is a real gap in the SBOM and
// worth surfacing, while a corrupt or non-Go executable is neither, and the golang cataloger sees far
// more of the latter than the former.
var ErrDeclaredSizeExceeded = errors.New("declared decompressed size over the limit")
// legacyZlibHeaderSize is the size of the .zdebug header: the "ZLIB" magic plus a big-endian size.
const legacyZlibHeaderSize = 12
// sectionsReadByName are the sections syft asks debug/elf for by name. Everything else it reaches by
// section type, which reachableSections covers separately. Not every one of these ends in a read of the
// contents (.text is used for its address, .symtab only for a nil check), but bounding any name syft asks
// for is cheaper to keep true than tracking which lookups reach Data.
//
// Add to this list when a cataloger starts reading a new section by name. The ruleguard rule only stops a
// new debug/elf call site; it cannot see a new (*Section).Data call behind an already-bounded open.
var sectionsReadByName = map[string]struct{}{
".symtab": {},
".data": {},
".text": {},
".gopclntab": {},
".note.package": {},
".modinfo": {},
}
// wire sizes of the structures we decode, which differ between the two ELF classes
var (
sizeSection32 = int64(binary.Size(elf.Section32{}))
sizeSection64 = int64(binary.Size(elf.Section64{}))
sizeChdr32 = int64(binary.Size(elf.Chdr32{}))
sizeChdr64 = int64(binary.Size(elf.Chdr64{}))
)
// NewFile parses an ELF file, rejecting it if a section syft may read declares a decompressed size this
// process should not be asked to allocate. It is a drop-in for elf.NewFile, except that a rejection is
// reported as a plain error rather than an *elf.FormatError, so callers that distinguish "not an ELF"
// from "bad ELF" treat it as the latter.
func NewFile(r io.ReaderAt) (*elf.File, error) {
if err := CheckSectionNameTable(r); err != nil {
return nil, err
}
f, err := elf.NewFile(r)
if err != nil {
return nil, err
}
if err := checkReachableSections(f); err != nil {
return nil, err
}
return f, nil
}
// CheckAllSections rejects ELF decompression bombs before debug/elf can expand one, without keeping the
// parse.
//
// The attack it stops: a compressed section's header declares its own decompressed size, and debug/elf
// believes that number, allocating it up front when the section is opened. Nothing forces the declared
// size to match what the compressed bytes actually yield, so a small file can name an enormous one. In
// the case this was written for, a 260KB ELF declaring a compressed .symtab drove 1.3GB of allocation.
// This walks the section headers and refuses any reachable section declaring more than
// maxDeclaredSectionSize, returning ErrDeclaredSizeExceeded.
//
// It is a strict superset of CheckSectionNameTable, which is what "All" in the name marks: reach for that
// narrower one only where a full elf.NewFile parse on every file is not worth paying for. Like it, this
// is for a caller whose debug/elf call is made inside another package, but it is the whole check rather
// than the eager half: goversion reads .symtab and the string table it links, and those are expanded
// lazily, so the name table bound alone leaves them unbounded.
//
// Anything that is not an ELF this package understands passes through untouched, since a caller reaching
// for this parses other containers too (goversion takes PE and Mach-O). A file elf.NewFile cannot parse
// passes through as well: describing a malformed file is the caller's job, not this gate's.
func CheckAllSections(r io.ReaderAt) error {
if _, _, ok := identify(r); !ok {
return nil
}
if err := CheckSectionNameTable(r); err != nil {
return err
}
f, err := elf.NewFile(r)
if err != nil {
return nil //nolint:nilerr // the caller's own parse reports the format error
}
return checkReachableSections(f)
}
// checkReachableSections bounds every section syft can drive debug/elf into decompressing. This runs
// after the parse because (*Section).Open expands lazily, so nothing has been allocated yet.
func checkReachableSections(f *elf.File) error {
for i := range reachableSections(f) {
s := f.Sections[i]
declared, claimed := declaredSectionSize(s)
if claimed && declared > maxDeclaredSectionSize {
return fmt.Errorf("%w: elf section %q declares %d decompressed bytes, over the %d byte limit",
ErrDeclaredSizeExceeded, s.Name, declared, maxDeclaredSectionSize)
}
}
return nil
}
// reachableSections reports which sections syft can actually reach. It marks by type generously, since
// the debug/elf accessors all go through SectionByType, which returns only the first section of a type: a
// second SHT_SYMTAB is bounded here despite being unreachable. DWARF is absent by design: nothing in syft
// calls File.DWARF, so debug/elf is never asked to expand a .debug_* section.
func reachableSections(f *elf.File) map[int]struct{} {
reach := make(map[int]struct{})
mark := func(i int) {
if i >= 0 && i < len(f.Sections) {
reach[i] = struct{}{}
}
}
for i, s := range f.Sections {
switch s.Type {
case elf.SHT_SYMTAB, elf.SHT_DYNSYM, elf.SHT_DYNAMIC:
// File.Symbols, File.DynamicSymbols and File.DynString each read one of these plus the
// string table its Link field points at
mark(i)
mark(int(s.Link))
case elf.SHT_GNU_VERSYM, elf.SHT_GNU_VERDEF, elf.SHT_GNU_VERNEED:
// File.DynamicSymbols pulls the symbol version tables in behind the caller's back, via
// gnuVersionInit. Strictly they are only reachable alongside a .dynsym, and VERDEF/VERNEED
// only alongside a VERSYM, but they are marked unconditionally rather than tracked: the only
// file that loses out carries an oversized version table and no dynamic symbols at all,
// which is not something a toolchain emits.
mark(i)
}
if _, ok := sectionsReadByName[s.Name]; ok {
mark(i)
}
}
return reach
}
// declaredSectionSize reports the decompressed size a section claims, if it claims one at all.
//
// ELF grew two separate ways of saying "this section is compressed", and (*Section).Open honors both, so
// this dispatches the same way Open does and in the same order. That correspondence is the whole design:
// wherever Open returns a decompressing reader, this reports the size that reader will expand to. Missing
// one is the bug this package exists to prevent, so when a Go release changes Open, this is what has to be
// re-read against it.
//
// It deliberately over-reports in one place, an unrecognized ch_type: Open hands back an error reader for
// anything but zlib and zstd, so nothing is allocated, but the type is not exposed on elf.Section and
// re-reading the header for it would only spare a file debug/elf cannot decompress anyway.
func declaredSectionSize(s *elf.Section) (uint64, bool) {
// SHT_NOBITS marks a section that occupies no bytes in the file, .bss being the familiar one: it
// declares a size the loader zero-fills at run time rather than a size stored anywhere. Its sh_offset
// is only a conceptual placement and routinely lands inside some other section's bytes. Two things
// follow, and both say to test this before looking at compression at all:
// - Open tests it first too, ahead of any compression, and hands back a reader that fails every
// read. Nothing is allocated for the declared size, however large it is.
// - the gABI forbids SHF_COMPRESSED here, but elf.NewFile does not enforce that and parses a
// compression header anyway. The Size it leaves on the section was decoded from whatever bytes lie
// at that borrowed offset, so it is not a figure to bound anything against.
if s.Type == elf.SHT_NOBITS {
return 0, false
}
if s.Flags&elf.SHF_COMPRESSED != 0 {
return chdrDeclaredSize(s)
}
return zdebugDeclaredSize(s)
}
// chdrDeclaredSize reports the size declared by the modern compression form: an SHF_COMPRESSED flag,
// plus an Elf32_Chdr or Elf64_Chdr at the head of the section giving the algorithm and the decompressed
// size. This is the form standardized in the gABI and the one any current toolchain emits.
func chdrDeclaredSize(s *elf.Section) (uint64, bool) {
// SHF_ALLOC marks a section the loader maps into the running process, and nothing decompresses a
// section on its way into memory, so the gABI makes the two flags mutually exclusive. Open enforces
// that rather than tolerating it: a section setting both gets an error reader, never a decompressing
// one, so a size declared alongside SHF_ALLOC is never allocated.
if s.Flags&elf.SHF_ALLOC != 0 {
return 0, false
}
// elf.NewFile decodes the Chdr during the parse and overwrites Size with ch_size, leaving the on-disk
// figure in FileSize. So the number a hostile file controls is already here, and no second read of the
// file is needed.
return s.Size, true
}
// zdebugDeclaredSize reports the size declared by the legacy compression form. Before SHF_COMPRESSED was
// added to the gABI, GNU tooling signaled a compressed debug section by renaming it, .debug_info to
// .zdebug_info, and prefixing the payload with a header of its own: the magic "ZLIB" followed by the
// decompressed size. Toolchains default to the gABI form now, Go's own linker having emitted .zdebug up
// to 1.19, but this is not vestigial: nothing ties the prefix to a debug section, so a crafted file can
// hang it on a section syft does read, an SHT_SYMTAB named .zdebug_x being the obvious one.
func zdebugDeclaredSize(s *elf.Section) (uint64, bool) {
// no flag distinguishes this form, so the section name is the only signal, and Open gates on the name
// before it so much as looks at the bytes. Matching that gate exactly is what keeps an ordinary
// section whose contents happen to begin with "ZLIB" out of scope: Open will never decompress it, so
// neither may this.
if !strings.HasPrefix(s.Name, ".zdebug") {
return 0, false
}
// unlike the Chdr above, nothing in the parse has looked at this header, so read it here. Reading off
// the section is only safe because SHF_COMPRESSED is clear on this path, since elf.NewFile leaves the
// embedded ReaderAt nil for a compressed one. Open treats a short read or absent magic as "not really
// compressed after all" and serves the bytes as they lie.
var hdr [legacyZlibHeaderSize]byte
if n, _ := s.ReadAt(hdr[:], 0); n != legacyZlibHeaderSize || string(hdr[:4]) != "ZLIB" {
return 0, false
}
// the size is big-endian whatever byte order the rest of the file uses. The header predates the gABI
// mechanism and was simply defined that way, carrying no endianness of its own.
return binary.BigEndian.Uint64(hdr[4:]), true
}
// CheckSectionNameTable bounds the one section elf.NewFile expands on its own. Anything it cannot walk is
// passed through untouched, leaving elf.NewFile to describe the file. It does not repeat every check
// elf.NewFile makes ahead of that expansion, though, so a file that is both malformed and oversized can
// come back with this error rather than an *elf.FormatError.
//
// NewFile calls this itself. It is exported for the callers that cannot use NewFile because the
// debug/elf call is made for them inside another package, debug/buildinfo being the one syft reaches:
// gate the reader on this and the eager section-name table read is bounded, which is everything that
// package expands today, since it reaches .go.buildinfo through the program headers instead.
//
// This is deliberately the eager half only, so it is the wrong gate for a caller whose parser goes on to
// read sections of its own. Use CheckAllSections for those: a package that reads symbols expands sections
// this never looks at, and the difference is the whole reason both exist.
func CheckSectionNameTable(r io.ReaderAt) error {
class, order, ok := identify(r)
if !ok {
return nil
}
sh, ok := sectionNameTableHeader(r, class, order)
if !ok {
return nil
}
// the name table's own name is not resolved at the point elf.NewFile reads it, so debug/elf cannot
// take the legacy .zdebug path for this section: only a compression header can apply
if sh.flags&uint64(elf.SHF_COMPRESSED) == 0 {
return nil
}
declared, err := compressedSize(r, class, order, sh.offset)
if err != nil {
return nil //nolint:nilerr // truncated section; elf.NewFile will say so
}
if declared > maxDeclaredSectionSize {
return fmt.Errorf("%w: elf section name table declares %d decompressed bytes, over the %d byte limit",
ErrDeclaredSizeExceeded, declared, maxDeclaredSectionSize)
}
return nil
}
// identify decodes the class and byte order every later read depends on, out of the file's 16
// identification bytes. It reports false for anything that is not an ELF this package understands, so
// elf.NewFile remains the single source of format errors.
func identify(r io.ReaderAt) (elf.Class, binary.ByteOrder, bool) {
var ident [16]byte
if _, err := io.ReadFull(io.NewSectionReader(r, 0, int64(len(ident))), ident[:]); err != nil {
return 0, nil, false
}
if string(ident[:4]) != elf.ELFMAG {
return 0, nil, false
}
class := elf.Class(ident[elf.EI_CLASS])
if class != elf.ELFCLASS32 && class != elf.ELFCLASS64 {
return 0, nil, false
}
switch elf.Data(ident[elf.EI_DATA]) {
case elf.ELFDATA2LSB:
return class, binary.LittleEndian, true
case elf.ELFDATA2MSB:
return class, binary.BigEndian, true
}
return 0, nil, false
}
// sectionNameTableHeader locates the header of the section syft's own parse cannot reach, the one
// elf.NewFile expands for itself. It reports false whenever the file names no name table or is malformed
// enough that nothing gets decompressed: either way there is nothing to bound, and describing the file is
// elf.NewFile's job rather than this package's.
func sectionNameTableHeader(r io.ReaderAt, class elf.Class, order binary.ByteOrder) (sectionHeader, bool) {
shoff, shentsize, shnum, shstrndx, err := fileHeader(r, class, order)
if err != nil || shoff <= 0 || shentsize < sectionHeaderSize(class) {
return sectionHeader{}, false
}
// a zero e_shnum means the real count lives in section 0's size field, and an e_shstrndx of
// SHN_XINDEX means the real string table index lives in that same header's link field
if shnum == 0 {
sh, err := readSectionHeader(r, class, order, shoff)
if err != nil {
return sectionHeader{}, false
}
shnum = sh.size
if shstrndx == uint64(elf.SHN_XINDEX) {
shstrndx = uint64(sh.link)
}
}
// an index of zero means the file has no section name table, so elf.NewFile returns before reading one.
// An out-of-range index expands nothing either, though elf.NewFile only rejects it when e_shnum was
// non-zero; reached through SHN_XINDEX it indexes out of range and panics instead. That is a
// recoverable panic the task executor already contains, unlike the OOM this package exists to stop.
if shstrndx == 0 || shstrndx >= shnum {
return sectionHeader{}, false
}
// the index is file-controlled, so guard the multiply and then the sum against a table the file
// places past the end of the address space
if shstrndx > uint64(math.MaxInt64)/uint64(shentsize) {
return sectionHeader{}, false
}
off := shoff + int64(shstrndx)*shentsize
if off < 0 {
return sectionHeader{}, false
}
sh, err := readSectionHeader(r, class, order, off)
if err != nil {
return sectionHeader{}, false
}
return sh, true
}
// sectionHeader is the handful of fields we need out of an Elf32_Shdr or an Elf64_Shdr.
type sectionHeader struct {
flags uint64
offset int64
size uint64
link uint32
}
// fileHeader returns the section table location and shape out of the ELF file header.
func fileHeader(r io.ReaderAt, class elf.Class, order binary.ByteOrder) (shoff, shentsize int64, shnum, shstrndx uint64, err error) {
if class == elf.ELFCLASS32 {
var h elf.Header32
if err := binary.Read(io.NewSectionReader(r, 0, int64(binary.Size(h))), order, &h); err != nil {
return 0, 0, 0, 0, err
}
return int64(h.Shoff), int64(h.Shentsize), uint64(h.Shnum), uint64(h.Shstrndx), nil
}
var h elf.Header64
if err := binary.Read(io.NewSectionReader(r, 0, int64(binary.Size(h))), order, &h); err != nil {
return 0, 0, 0, 0, err
}
// Shoff is a uint64, so a file may claim an offset that does not fit in an int64; the caller rejects
// a negative result rather than wrapping into a plausible-looking one
return int64(h.Shoff), int64(h.Shentsize), uint64(h.Shnum), uint64(h.Shstrndx), nil
}
// readSectionHeader decodes one section header. Reads go through binary.Read, which uses io.ReadFull, so
// a short read is an error rather than a partially-zeroed header.
func readSectionHeader(r io.ReaderAt, class elf.Class, order binary.ByteOrder, off int64) (sectionHeader, error) {
sr := io.NewSectionReader(r, off, sectionHeaderSize(class))
if class == elf.ELFCLASS32 {
var sh elf.Section32
if err := binary.Read(sr, order, &sh); err != nil {
return sectionHeader{}, err
}
return sectionHeader{
flags: uint64(sh.Flags),
offset: int64(sh.Off),
size: uint64(sh.Size),
link: sh.Link,
}, nil
}
var sh elf.Section64
if err := binary.Read(sr, order, &sh); err != nil {
return sectionHeader{}, err
}
return sectionHeader{
flags: sh.Flags,
offset: int64(sh.Off),
size: sh.Size,
link: sh.Link,
}, nil
}
// compressedSize reads the decompressed size out of a section's SHF_COMPRESSED header. Chdr64 carries a
// blank field that binary.Size counts and binary.Read skips, so the decoded offsets match what debug/elf
// reaches for with unsafe.Offsetof.
func compressedSize(r io.ReaderAt, class elf.Class, order binary.ByteOrder, off int64) (uint64, error) {
if off < 0 {
return 0, fmt.Errorf("negative section offset")
}
sr := io.NewSectionReader(r, off, compressionHeaderSize(class))
if class == elf.ELFCLASS32 {
var ch elf.Chdr32
if err := binary.Read(sr, order, &ch); err != nil {
return 0, err
}
return uint64(ch.Size), nil
}
var ch elf.Chdr64
if err := binary.Read(sr, order, &ch); err != nil {
return 0, err
}
return ch.Size, nil
}
func sectionHeaderSize(class elf.Class) int64 {
if class == elf.ELFCLASS32 {
return sizeSection32
}
return sizeSection64
}
func compressionHeaderSize(class elf.Class) int64 {
if class == elf.ELFCLASS32 {
return sizeChdr32
}
return sizeChdr64
}
+736
View File
@@ -0,0 +1,736 @@
package elfutil
import (
"bytes"
"compress/zlib"
"debug/elf"
"encoding/binary"
"fmt"
"io"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// section describes one section to place in a fixture. The compression headers are deliberately
// separable from the payload so a fixture can claim far more than it can deliver, which is the whole
// shape of the attack under test.
type section struct {
name string
typ elf.SectionType
flags elf.SectionFlag
link uint32
body []byte
compressed bool // emit a SHF_COMPRESSED chdr ahead of body
legacyZlib bool // emit a 12-byte .zdebug-style ZLIB header ahead of body
declaredSize uint64
offsetOf string // borrow another section's file offset, for SHT_NOBITS fixtures
shSize uint64 // override sh_size, since an SHT_NOBITS section's size is not its span in the file
}
type buildOpts struct {
// nameTable overrides the generated .shstrtab, so a fixture can compress the one section
// elf.NewFile expands on its own.
nameTable *section
// extendedShnum parks the real section count in section 0 and zeroes e_shnum.
extendedShnum bool
// xindexShstrndx sets e_shstrndx to SHN_XINDEX so the real index comes from section 0's link.
xindexShstrndx bool
}
// buildELF assembles a minimal ELF: a null section, then secs, then a generated .shstrtab that
// e_shstrndx points at.
func buildELF(t *testing.T, class elf.Class, order binary.ByteOrder, secs []section, opts buildOpts) []byte {
t.Helper()
nameTable := section{name: ".shstrtab", typ: elf.SHT_STRTAB}
if opts.nameTable != nil {
nameTable = *opts.nameTable
nameTable.name = ".shstrtab"
nameTable.typ = elf.SHT_STRTAB
}
all := append([]section{{}}, secs...)
all = append(all, nameTable)
shstrndx := len(all) - 1
// the name table's body is the names of every section, so it has to be built before the payloads
var names bytes.Buffer
names.WriteByte(0)
nameOff := make([]uint32, len(all))
for i, s := range all {
if s.name == "" {
continue
}
nameOff[i] = uint32(names.Len())
names.WriteString(s.name)
names.WriteByte(0)
}
all[shstrndx].body = names.Bytes()
// a fixture that compresses the name table without rigging a size gets an honest one, so the file is
// readable rather than merely parseable
if all[shstrndx].compressed && all[shstrndx].declaredSize == 0 {
all[shstrndx].declaredSize = uint64(names.Len())
all[shstrndx].body = deflate(t, names.Bytes())
}
is32 := class == elf.ELFCLASS32
ehsize, shentsize := binary.Size(elf.Header64{}), binary.Size(elf.Section64{})
machine := elf.EM_X86_64
if is32 {
ehsize, shentsize = binary.Size(elf.Header32{}), binary.Size(elf.Section32{})
machine = elf.EM_386
}
shoff := ehsize
dataOff := shoff + len(all)*shentsize
// lay the payloads out and remember where each landed, so an SHT_NOBITS fixture can point at one
payloads := make([][]byte, len(all))
offsets := make(map[string]uint64)
cursor := uint64(dataOff)
for i, s := range all {
payloads[i] = sectionContent(t, class, order, s)
offsets[s.name] = cursor
cursor += uint64(len(payloads[i]))
}
var ident [16]byte
copy(ident[:], elf.ELFMAG)
ident[elf.EI_CLASS] = byte(class)
ident[elf.EI_DATA] = byte(elf.ELFDATA2LSB)
if order == binary.BigEndian {
ident[elf.EI_DATA] = byte(elf.ELFDATA2MSB)
}
ident[elf.EI_VERSION] = byte(elf.EV_CURRENT)
shnum := uint16(len(all))
if opts.extendedShnum {
shnum = 0
}
strndx := uint16(shstrndx)
if opts.xindexShstrndx {
strndx = uint16(elf.SHN_XINDEX)
}
buf := &bytes.Buffer{}
if is32 {
write(t, buf, order, elf.Header32{
Ident: ident, Type: uint16(elf.ET_REL), Machine: uint16(machine), Version: uint32(elf.EV_CURRENT),
Shoff: uint32(shoff), Ehsize: uint16(ehsize), Shentsize: uint16(shentsize),
Shnum: shnum, Shstrndx: strndx,
})
} else {
write(t, buf, order, elf.Header64{
Ident: ident, Type: uint16(elf.ET_REL), Machine: uint16(machine), Version: uint32(elf.EV_CURRENT),
Shoff: uint64(shoff), Ehsize: uint16(ehsize), Shentsize: uint16(shentsize),
Shnum: shnum, Shstrndx: strndx,
})
}
off := uint64(dataOff)
for i, s := range all {
size := uint64(len(payloads[i]))
if s.shSize != 0 {
size = s.shSize
}
secOff := off
if s.offsetOf != "" {
secOff = offsets[s.offsetOf]
}
flags := s.flags
if s.compressed {
// the content header and the flag always travel together in a real file
flags |= elf.SHF_COMPRESSED
}
link := s.link
// section 0 carries the extended count and the extended name table index
if i == 0 {
if opts.extendedShnum {
size = uint64(len(all))
}
if opts.xindexShstrndx {
link = uint32(shstrndx)
}
}
if is32 {
write(t, buf, order, elf.Section32{
Name: nameOff[i], Type: uint32(s.typ), Flags: uint32(flags), Link: link,
Off: uint32(secOff), Size: uint32(size), Addralign: 1,
})
} else {
write(t, buf, order, elf.Section64{
Name: nameOff[i], Type: uint32(s.typ), Flags: uint64(flags), Link: link,
Off: secOff, Size: size, Addralign: 1,
})
}
off += uint64(len(payloads[i]))
}
require.Equal(t, dataOff, buf.Len(), "fixture layout drifted from the declared offsets")
for _, p := range payloads {
buf.Write(p)
}
return buf.Bytes()
}
// sectionContent wraps the body in whichever compression header the fixture asks for.
func sectionContent(t *testing.T, class elf.Class, order binary.ByteOrder, s section) []byte {
t.Helper()
switch {
case s.legacyZlib:
hdr := make([]byte, legacyZlibHeaderSize)
copy(hdr, "ZLIB")
binary.BigEndian.PutUint64(hdr[4:], s.declaredSize)
return append(hdr, s.body...)
case s.compressed:
buf := &bytes.Buffer{}
if class == elf.ELFCLASS32 {
write(t, buf, order, elf.Chdr32{
Type: uint32(elf.COMPRESS_ZLIB), Size: uint32(s.declaredSize), Addralign: 1,
})
} else {
write(t, buf, order, elf.Chdr64{
Type: uint32(elf.COMPRESS_ZLIB), Size: s.declaredSize, Addralign: 1,
})
}
buf.Write(s.body)
return buf.Bytes()
default:
return s.body
}
}
func write(t *testing.T, w io.Writer, order binary.ByteOrder, v any) {
t.Helper()
require.NoError(t, binary.Write(w, order, v))
}
// deflate returns a real zlib stream, so a fixture using it actually delivers the bytes it promises.
func deflate(t *testing.T, payload []byte) []byte {
t.Helper()
buf := &bytes.Buffer{}
zw := zlib.NewWriter(buf)
_, err := zw.Write(payload)
require.NoError(t, err)
require.NoError(t, zw.Close())
return buf.Bytes()
}
// classes is the matrix every structural case runs under, since the header layouts differ per class and
// the compression header's size field is a different width in each.
var classes = []struct {
name string
class elf.Class
order binary.ByteOrder
}{
{"64-bit little-endian", elf.ELFCLASS64, binary.LittleEndian},
{"64-bit big-endian", elf.ELFCLASS64, binary.BigEndian},
{"32-bit little-endian", elf.ELFCLASS32, binary.LittleEndian},
{"32-bit big-endian", elf.ELFCLASS32, binary.BigEndian},
}
const overLimit = maxDeclaredSectionSize + 1
func TestNewFile_RejectsOversizedReachableSections(t *testing.T) {
tests := []struct {
name string
secs []section
opts buildOpts
wantErr string
}{
{
name: "compressed .symtab",
secs: []section{
{name: ".symtab", typ: elf.SHT_SYMTAB, link: 2, compressed: true, declaredSize: overLimit},
{name: ".strtab", typ: elf.SHT_STRTAB},
},
wantErr: `".symtab"`,
},
{
name: "the string table a .symtab links to",
secs: []section{
{name: ".symtab", typ: elf.SHT_SYMTAB, link: 2},
{name: ".strtab", typ: elf.SHT_STRTAB, compressed: true, declaredSize: overLimit},
},
wantErr: `".strtab"`,
},
{
name: "compressed .dynsym",
secs: []section{
{name: ".dynsym", typ: elf.SHT_DYNSYM, link: 2, compressed: true, declaredSize: overLimit},
{name: ".dynstr", typ: elf.SHT_STRTAB},
},
wantErr: `".dynsym"`,
},
{
name: "compressed .dynamic",
secs: []section{
{name: ".dynamic", typ: elf.SHT_DYNAMIC, compressed: true, declaredSize: overLimit},
},
wantErr: `".dynamic"`,
},
{
name: "compressed .note.package, reached by name rather than type",
secs: []section{
{name: ".note.package", typ: elf.SHT_NOTE, compressed: true, declaredSize: overLimit},
},
wantErr: `".note.package"`,
},
{
name: "compressed .modinfo, read by the kernel module cataloger",
secs: []section{
{name: ".modinfo", typ: elf.SHT_PROGBITS, compressed: true, declaredSize: overLimit},
},
wantErr: `".modinfo"`,
},
{
// File.DynamicSymbols reads these via gnuVersionInit without the caller naming them
name: "compressed .gnu.version alongside a .dynsym",
secs: []section{
{name: ".dynsym", typ: elf.SHT_DYNSYM, link: 2},
{name: ".dynstr", typ: elf.SHT_STRTAB},
{name: ".gnu.version", typ: elf.SHT_GNU_VERSYM, compressed: true, declaredSize: overLimit},
},
wantErr: `".gnu.version"`,
},
{
name: "compressed .gnu.version_r alongside a .dynsym",
secs: []section{
{name: ".dynsym", typ: elf.SHT_DYNSYM, link: 2},
{name: ".dynstr", typ: elf.SHT_STRTAB},
{name: ".gnu.version_r", typ: elf.SHT_GNU_VERNEED, compressed: true, declaredSize: overLimit},
},
wantErr: `".gnu.version_r"`,
},
{
name: "compressed .gnu.version_d alongside a .dynsym",
secs: []section{
{name: ".dynsym", typ: elf.SHT_DYNSYM, link: 2},
{name: ".dynstr", typ: elf.SHT_STRTAB},
{name: ".gnu.version_d", typ: elf.SHT_GNU_VERDEF, compressed: true, declaredSize: overLimit},
},
wantErr: `".gnu.version_d"`,
},
{
// the one section elf.NewFile expands itself, so this has to be caught before the parse
name: "compressed section name table",
opts: buildOpts{nameTable: &section{compressed: true, declaredSize: overLimit}},
wantErr: "section name table",
},
{
// debug/elf gates the legacy form on the name, so a reachable section named this way reaches it
name: "legacy .zdebug header on a section reached by type",
secs: []section{
{name: ".zdebug_symtab", typ: elf.SHT_SYMTAB, link: 2, legacyZlib: true, declaredSize: overLimit},
{name: ".strtab", typ: elf.SHT_STRTAB},
},
wantErr: `".zdebug_symtab"`,
},
}
for _, tt := range tests {
for _, c := range classes {
t.Run(tt.name+"/"+c.name, func(t *testing.T) {
data := buildELF(t, c.class, c.order, tt.secs, tt.opts)
_, err := NewFile(bytes.NewReader(data))
require.Error(t, err)
// the fixtures are hand-assembled ELF bytes, so a bad one would satisfy require.Error on
// its own. Naming the section proves the rejection is the one we set up, and naming the
// limit proves it came from this package rather than from debug/elf.
// the sentinel is what the callers key their reporting on, so it is asserted here rather
// than only from the packages downstream that consume it
assert.ErrorIs(t, err, ErrDeclaredSizeExceeded)
assert.Contains(t, err.Error(), tt.wantErr)
assert.Contains(t, err.Error(), fmt.Sprint(maxDeclaredSectionSize))
})
}
}
}
// TestNewFile_AcceptsWhatDebugELFWouldNotExpand covers the false-negative direction: rejecting a whole
// binary over a section debug/elf is never driven to decompress drops real packages from the SBOM.
//
// Every fixture here is a file debug/elf parses without complaint, and every one must come back from
// NewFile without complaint too. An oversized claim is only worth rejecting when debug/elf can actually
// be made to allocate against it, so a claim it will never act on has to be ignored outright rather than
// merely tolerated.
func TestNewFile_AcceptsWhatDebugELFWouldNotExpand(t *testing.T) {
tests := []struct {
name string
secs []section
why string
}{
{
// only File.DWARF expands a .debug_* section, and nothing in syft calls it, so this claim is
// never acted on. reachableSections leaves DWARF out on purpose: compressed debug info is
// routinely enormous in a legitimate binary, and bounding it would skip the binary entirely.
//
// non-allocable and SHF_COMPRESSED is deliberately the one shape debug/elf will expand, so the
// name is the only thing keeping this accepted. Adding SHF_ALLOC would make it pass for the
// allocable case's reason below and stop saying anything about DWARF.
name: "oversized compressed DWARF",
secs: []section{
{name: ".debug_info", typ: elf.SHT_PROGBITS, compressed: true, declaredSize: overLimit},
},
why: "syft never calls File.DWARF, so debug/elf is never asked to expand it",
},
{
// the same section in the older form toolchains emitted before SHF_COMPRESSED existed. The
// header is different, the reader that would expand it is the same one syft never calls.
//
// SHF_COMPRESSED is deliberately absent: with it set, debug/elf takes the modern path and the
// legacy header is never consulted, so the .zdebug name gate would go untested.
name: "oversized legacy .zdebug DWARF",
secs: []section{
{name: ".zdebug_info", typ: elf.SHT_PROGBITS, legacyZlib: true, declaredSize: overLimit},
},
why: "same, via the legacy form",
},
{
// reachable by type and named the way debug/elf gates the legacy form on, so the name half of
// that gate passes and the magic is the only thing left to reject the claim. Without the magic
// check the 0xff bytes behind it decode as a declared size of 2^64-1.
name: "a .zdebug-named reachable section whose bytes are not a legacy header",
secs: []section{
{name: ".zdebug_symtab", typ: elf.SHT_SYMTAB, link: 2,
body: append([]byte("NOPE"), bytes.Repeat([]byte{0xff}, 32)...)},
{name: ".strtab", typ: elf.SHT_STRTAB},
},
why: "the legacy form needs the magic as well as the name",
},
{
// the other half of the same gate: fewer bytes present than a legacy header needs. The magic is
// intact and some of the size field is present, so a short read has to read as "no claim"
// rather than as a size decoded out of a partly-filled buffer, which here would be huge.
name: "a .zdebug-named reachable section too short to hold a legacy header",
secs: []section{
{name: ".zdebug_symtab", typ: elf.SHT_SYMTAB, link: 2,
body: append([]byte("ZLIB"), bytes.Repeat([]byte{0xff}, 4)...)},
{name: ".strtab", typ: elf.SHT_STRTAB},
},
why: "a truncated legacy header is not a claim",
},
{
// an ordinary uncompressed note that happens to open with those four bytes. debug/elf takes
// the legacy path only for a .zdebug-prefixed name, so it reads this as content and nothing
// here is a size claim at all. A check that sniffed for the magic instead would decode the
// 0xff padding behind it as a declared size and throw the binary away.
//
// .note.package because it has to be a section syft actually reads, or reachableSections skips
// it and the case passes without exercising anything. No flags for the same reason the name is
// not .zdebug-prefixed: either one would put debug/elf on a path where the bytes are a header.
name: "an uncompressed section whose bytes happen to start with ZLIB",
secs: []section{
{name: ".note.package", typ: elf.SHT_NOTE, body: append([]byte("ZLIB"), bytes.Repeat([]byte{0xff}, 32)...)},
},
why: "debug/elf gates the legacy form on the section name, not the magic, so this is not a claim at all",
},
{
// a section that occupies memory at runtime cannot be compressed on disk, so (*Section).Open
// hands back the raw bytes rather than expanding one that says it is. The claim is real and
// oversized, and still costs nothing.
//
// this is the ".symtab" reject case with one bit added: .data is read by name just as .symtab
// is, so SHF_ALLOC is the only reason the two outcomes differ.
name: "an allocable compressed section",
secs: []section{
{name: ".data", typ: elf.SHT_PROGBITS, flags: elf.SHF_COMPRESSED | elf.SHF_ALLOC,
compressed: true, declaredSize: overLimit},
},
why: "debug/elf refuses to decompress an allocable section",
},
{
// the hardest one to see: sh_offset points into .rodata's payload, so elf.NewFile reads that
// compression header and .data comes back carrying an oversized Size of its own. Reading Size
// alone would reject here, but a SHT_NOBITS section holds no file bytes and its reader yields
// none, so the size is never allocated against. sh_size is set by hand because such a section
// has a size without occupying any of the file, and a zero one is a file debug/elf rejects.
//
// SHF_COMPRESSED has to be set for the size to be a claim at all, and .data has to be the
// borrower rather than the donor, since the donor is unreachable by name and never checked.
name: "an SHT_NOBITS section borrowing another section's compression header",
secs: []section{
{name: ".rodata", typ: elf.SHT_PROGBITS, compressed: true, declaredSize: overLimit},
{name: ".data", typ: elf.SHT_NOBITS, flags: elf.SHF_COMPRESSED, offsetOf: ".rodata",
shSize: 4096},
},
why: "debug/elf never yields bytes for SHT_NOBITS, whatever Size it ends up carrying",
},
{
// a string table is reached through some symbol table's sh_link, never on its own, so one
// nothing points at is one nothing reads. This is the reject test's linked-.strtab case with
// the .symtab removed, and it is the case an implementation that bounded every SHT_STRTAB
// rather than following the links would fail.
name: "an oversized string table that no symbol table links",
secs: []section{
{name: ".strtab", typ: elf.SHT_STRTAB, compressed: true, declaredSize: overLimit},
},
why: "reachability runs through sh_link, not section type",
},
}
for _, tt := range tests {
for _, c := range classes {
t.Run(tt.name+"/"+c.name, func(t *testing.T) {
data := buildELF(t, c.class, c.order, tt.secs, buildOpts{})
// "elfutil accepted it" only means something if debug/elf accepts it too, so a fixture
// that drifts into being malformed fails here rather than passing for the wrong reason
_, err := elf.NewFile(bytes.NewReader(data))
require.NoError(t, err, "fixture is supposed to be a file debug/elf accepts")
_, err = NewFile(bytes.NewReader(data))
require.NoError(t, err, tt.why)
})
}
}
}
// TestNewFile_AcceptsAndReadsSectionUnderTheLimit guards the risk the bound introduces: a legitimately
// compressed section must still be readable, not merely accepted.
func TestNewFile_AcceptsAndReadsSectionUnderTheLimit(t *testing.T) {
for _, c := range classes {
t.Run(c.name, func(t *testing.T) {
payload := bytes.Repeat([]byte("syft"), 4096)
data := buildELF(t, c.class, c.order, []section{
{name: ".note.package", typ: elf.SHT_NOTE, flags: elf.SHF_COMPRESSED,
compressed: true, declaredSize: uint64(len(payload)), body: deflate(t, payload)},
}, buildOpts{})
f, err := NewFile(bytes.NewReader(data))
require.NoError(t, err)
got, err := f.Section(".note.package").Data()
require.NoError(t, err)
assert.Equal(t, payload, got)
})
}
}
// TestNewFile_AcceptsAndReadsCompressedSectionNameTable covers the other half of CheckSectionNameTable.
// That check runs before the parse, off a hand-decoded section header rather than anything debug/elf has
// resolved, so a version of it that rejected every compressed name table outright, or that read the size
// out of the wrong offset, would still pass every rejection case. Resolving the names proves the file was
// left intact and not merely let through.
func TestNewFile_AcceptsAndReadsCompressedSectionNameTable(t *testing.T) {
for _, c := range classes {
t.Run(c.name, func(t *testing.T) {
data := buildELF(t, c.class, c.order,
[]section{{name: ".note.package", typ: elf.SHT_NOTE}},
buildOpts{nameTable: &section{compressed: true}})
f, err := NewFile(bytes.NewReader(data))
require.NoError(t, err)
assert.NotNil(t, f.Section(".note.package"), "section names did not survive the name table")
})
}
}
// TestNewFile_BombDoesNotAllocate is the end-to-end case: a real zlib stream that genuinely delivers
// every byte its header promises, so without the bound debug/elf allocates all of them.
func TestNewFile_BombDoesNotAllocate(t *testing.T) {
const declared = maxDeclaredSectionSize + 1
compressed := deflate(t, make([]byte, declared))
t.Logf("%d declared bytes compress to %d", declared, len(compressed))
data := buildELF(t, elf.ELFCLASS64, binary.LittleEndian, []section{
{name: ".symtab", typ: elf.SHT_SYMTAB, link: 2, flags: elf.SHF_COMPRESSED,
compressed: true, declaredSize: declared, body: compressed},
{name: ".strtab", typ: elf.SHT_STRTAB},
}, buildOpts{})
var before, after runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&before)
_, err := NewFile(bytes.NewReader(data))
runtime.ReadMemStats(&after)
require.Error(t, err)
assert.Contains(t, err.Error(), "over the")
// the guard has to reject before the allocation, not after it. The margin is loose on purpose: this
// is asserting "nothing near the declared size was allocated", not a precise budget.
const margin = 32 * 1024 * 1024
assert.Less(t, after.TotalAlloc-before.TotalAlloc, uint64(margin),
"parsing allocated far more than the input warrants")
}
// TestNewFile_MatchesDebugELFOnStructuralEdges pins the invariant the passthrough returns depend on:
// wherever elfutil declines to check, elf.NewFile must still be the one deciding.
func TestNewFile_MatchesDebugELFOnStructuralEdges(t *testing.T) {
tests := []struct {
name string
secs []section
opts buildOpts
}{
{name: "plain file", secs: []section{{name: ".text", typ: elf.SHT_PROGBITS, flags: elf.SHF_ALLOC}}},
{name: "extended section count", opts: buildOpts{extendedShnum: true}},
{name: "extended name table index", opts: buildOpts{xindexShstrndx: true}},
{name: "both extended forms", opts: buildOpts{extendedShnum: true, xindexShstrndx: true}},
{
name: "symtab linking a nonexistent string table",
secs: []section{{name: ".symtab", typ: elf.SHT_SYMTAB, link: 99}},
},
}
for _, tt := range tests {
for _, c := range classes {
t.Run(tt.name+"/"+c.name, func(t *testing.T) {
data := buildELF(t, c.class, c.order, tt.secs, tt.opts)
_, want := elf.NewFile(bytes.NewReader(data))
_, got := NewFile(bytes.NewReader(data))
if want == nil {
assert.NoError(t, got, "elfutil rejected a file debug/elf accepts")
return
}
require.Error(t, got, "elfutil accepted a file debug/elf rejects")
})
}
}
}
// TestNewFile_ExtendedShnumOverflowStillChecks covers the case a signed conversion used to swallow: a
// count that does not fit an int64 must not read as "no sections" and skip the check.
func TestNewFile_ExtendedShnumOverflowStillChecks(t *testing.T) {
data := buildELF(t, elf.ELFCLASS64, binary.LittleEndian, nil,
buildOpts{nameTable: &section{compressed: true, declaredSize: overLimit}})
// rewrite section 0's size to a count that overflows int64, and zero e_shnum so it is consulted
// e_shnum sits at 0x3c in an Elf64_Ehdr, and sh_size at +32 in the Elf64_Shdr that follows it
shoff := binary.Size(elf.Header64{})
binary.LittleEndian.PutUint16(data[0x3c:], 0)
binary.LittleEndian.PutUint64(data[shoff+32:], uint64(1)<<63)
_, err := NewFile(bytes.NewReader(data))
require.Error(t, err, "a bogus section count must not disable the check")
}
// TestCheckAllSections covers the standalone gate, used by callers whose debug/elf parse happens inside
// another package (goversion, here) rather than through NewFile.
func TestCheckAllSections(t *testing.T) {
tests := []struct {
name string
data func(t *testing.T) []byte
// wantErr is empty when CheckAllSections must return nil.
wantErr string
// nameTableMisses marks a fixture CheckSectionNameTable alone does not reject, since the oversized
// section here is one debug/elf only expands lazily, after the parse CheckAllSections goes on to do.
nameTableMisses bool
}{
{
name: "non-ELF reader",
data: func(t *testing.T) []byte { return []byte("not an elf") },
},
{
name: "valid ident but a body elf.NewFile cannot parse",
data: func(t *testing.T) []byte {
full := buildELF(t, elf.ELFCLASS64, binary.LittleEndian, nil, buildOpts{})
truncated := full[:binary.Size(elf.Header64{})+4]
_, err := elf.NewFile(bytes.NewReader(truncated))
require.Error(t, err, "fixture is supposed to be rejected by debug/elf")
return truncated
},
},
{
name: "clean small ELF",
data: func(t *testing.T) []byte {
data := buildELF(t, elf.ELFCLASS64, binary.LittleEndian,
[]section{{name: ".text", typ: elf.SHT_PROGBITS, flags: elf.SHF_ALLOC}}, buildOpts{})
_, err := elf.NewFile(bytes.NewReader(data))
require.NoError(t, err, "fixture is supposed to be a file debug/elf accepts")
return data
},
},
{
name: "oversized section name table",
data: func(t *testing.T) []byte {
return buildELF(t, elf.ELFCLASS64, binary.LittleEndian, nil,
buildOpts{nameTable: &section{compressed: true, declaredSize: overLimit}})
},
wantErr: "section name table",
},
{
name: "oversized .symtab, expanded lazily rather than during the parse",
data: func(t *testing.T) []byte {
data := buildELF(t, elf.ELFCLASS64, binary.LittleEndian, []section{
{name: ".symtab", typ: elf.SHT_SYMTAB, link: 2, compressed: true, declaredSize: overLimit},
{name: ".strtab", typ: elf.SHT_STRTAB},
}, buildOpts{})
_, err := elf.NewFile(bytes.NewReader(data))
require.NoError(t, err, "fixture is supposed to be a file debug/elf accepts")
return data
},
wantErr: `".symtab"`,
nameTableMisses: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data := tt.data(t)
if tt.nameTableMisses {
require.NoError(t, CheckSectionNameTable(bytes.NewReader(data)),
"the name table check alone doesn't reach a section debug/elf only expands lazily")
}
err := CheckAllSections(bytes.NewReader(data))
if tt.wantErr == "" {
require.NoError(t, err)
return
}
require.Error(t, err)
assert.ErrorIs(t, err, ErrDeclaredSizeExceeded)
assert.Contains(t, err.Error(), tt.wantErr)
})
}
}
func TestNewFile_PassesThroughNonELF(t *testing.T) {
tests := []struct {
name string
data []byte
}{
{"empty", nil},
{"shorter than e_ident", []byte{0x7f, 'E'}},
{"bad magic", bytes.Repeat([]byte{0xab}, 128)},
{"bad class", func() []byte {
b := bytes.Repeat([]byte{0}, 128)
copy(b, elf.ELFMAG)
b[elf.EI_CLASS] = 9
return b
}()},
{"bad byte order", func() []byte {
b := bytes.Repeat([]byte{0}, 128)
copy(b, elf.ELFMAG)
b[elf.EI_CLASS] = byte(elf.ELFCLASS64)
b[elf.EI_DATA] = 9
return b
}()},
{"truncated after the header", func() []byte {
data := buildELF(t, elf.ELFCLASS64, binary.LittleEndian, nil, buildOpts{})
return data[:binary.Size(elf.Header64{})+4]
}()},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, want := elf.NewFile(bytes.NewReader(tt.data))
_, got := NewFile(bytes.NewReader(tt.data))
require.Error(t, want, "fixture is supposed to be rejected by debug/elf")
require.Error(t, got)
// the rejection must be debug/elf's, phrased its way, not ours
assert.NotContains(t, got.Error(), "over the")
assert.Equal(t, fmt.Sprint(want), fmt.Sprint(got))
})
}
}
@@ -53,8 +53,8 @@ func (r *ContainerImageAllLayers) HasPath(path string) bool {
return false
}
func (r *ContainerImageAllLayers) fileByRef(ref stereoscopeFile.Reference, uniqueFileIDs stereoscopeFile.ReferenceSet, layerIdx int) ([]stereoscopeFile.Reference, error) {
uniqueFiles := make([]stereoscopeFile.Reference, 0)
func (r *ContainerImageAllLayers) locationsByRef(ref stereoscopeFile.Reference, accessPath string, uniqueFileIDs stereoscopeFile.ReferenceSet, layerPos int) ([]file.Location, error) {
uniqueLocations := make([]file.Location, 0)
// since there is potentially considerable work for each symlink/hardlink that needs to be resolved, let's check to see if this is a symlink/hardlink first
entry, err := r.img.FileCatalog.Get(ref)
@@ -65,22 +65,22 @@ func (r *ContainerImageAllLayers) fileByRef(ref stereoscopeFile.Reference, uniqu
if entry.Type == stereoscopeFile.TypeHardLink || entry.Type == stereoscopeFile.TypeSymLink {
// a link may resolve in this layer or higher, assuming a squashed tree is used to search
// we should search all possible resolutions within the valid source
for _, subLayerIdx := range r.layers[layerIdx:] {
for _, subLayerIdx := range r.layers[layerPos:] {
resolvedRef, err := r.img.ResolveLinkByLayerSquash(ref, subLayerIdx)
if err != nil {
return nil, fmt.Errorf("failed to resolve link from layer (layer=%d ref=%+v): %w", subLayerIdx, ref, err)
}
if resolvedRef.HasReference() && !uniqueFileIDs.Contains(*resolvedRef.Reference) {
uniqueFileIDs.Add(*resolvedRef.Reference)
uniqueFiles = append(uniqueFiles, *resolvedRef.Reference)
uniqueLocations = append(uniqueLocations, file.NewLocationFromImage(accessPath, *resolvedRef.Reference, r.img))
}
}
} else if !uniqueFileIDs.Contains(ref) {
uniqueFileIDs.Add(ref)
uniqueFiles = append(uniqueFiles, ref)
uniqueLocations = append(uniqueLocations, file.NewLocationFromImage(accessPath, ref, r.img))
}
return uniqueFiles, nil
return uniqueLocations, nil
}
// FilesByPath returns all file.References that match the given paths from any layer in the image.
@@ -112,14 +112,13 @@ func (r *ContainerImageAllLayers) FilesByPath(paths ...string) ([]file.Location,
}
}
results, err := r.fileByRef(*ref.Reference, uniqueFileIDs, idx)
locations, err := r.locationsByRef(*ref.Reference, path, uniqueFileIDs, idx)
if err != nil {
return nil, err
}
for _, result := range results {
l := file.NewLocationFromImage(path, result, r.img)
r.annotateLocation(&l)
uniqueLocations = append(uniqueLocations, l)
for i := range locations {
r.annotateLocation(&locations[i])
uniqueLocations = append(uniqueLocations, locations[i])
}
}
}
@@ -158,14 +157,13 @@ func (r *ContainerImageAllLayers) FilesByGlob(patterns ...string) ([]file.Locati
}
}
refResults, err := r.fileByRef(*result.Reference, uniqueFileIDs, idx)
locations, err := r.locationsByRef(*result.Reference, string(result.RequestPath), uniqueFileIDs, idx)
if err != nil {
return nil, err
}
for _, refResult := range refResults {
l := file.NewLocationFromImage(string(result.RequestPath), refResult, r.img)
r.annotateLocation(&l)
uniqueLocations = append(uniqueLocations, l)
for i := range locations {
r.annotateLocation(&locations[i])
uniqueLocations = append(uniqueLocations, locations[i])
}
}
}
@@ -233,14 +231,13 @@ func (r *ContainerImageAllLayers) FilesByMIMEType(types ...string) ([]file.Locat
continue
}
refResults, err := r.fileByRef(*ref.Reference, uniqueFileIDs, idx)
locations, err := r.locationsByRef(*ref.Reference, string(ref.RequestPath), uniqueFileIDs, idx)
if err != nil {
return nil, err
}
for _, refResult := range refResults {
l := file.NewLocationFromImage(string(ref.RequestPath), refResult, r.img)
r.annotateLocation(&l)
uniqueLocations = append(uniqueLocations, l)
for i := range locations {
r.annotateLocation(&locations[i])
uniqueLocations = append(uniqueLocations, locations[i])
}
}
}
@@ -282,26 +279,22 @@ func (r *ContainerImageAllLayers) annotateLocation(l *file.Location) {
annotation := file.VisibleAnnotation
// if we find a location for a path that matches the query (e.g. **/node_modules) but is not present in the squashed tree, skip it
ref, err := r.img.SquashedSearchContext.SearchByPath(l.RealPath, filetree.DoNotFollowDeadBasenameLinks)
if err != nil || !ref.HasReference() {
annotation = file.HiddenAnnotation
} else if ref.ID() != givenRef.ID() {
// we may have the path in the squashed tree, but this must not be in the same layer
if !r.pathResolvesToRef(l.RealPath, givenRef) {
annotation = file.HiddenAnnotation
}
// not only should the real path to the file exist, but the way we took to get there should also exist
// (e.g. if we are looking for /etc/passwd, but the real path is /etc/passwd -> /etc/passwd-1, then we should
// make certain that /etc/passwd-1 exists)
if annotation == file.VisibleAnnotation && l.AccessPath != "" {
ref, err := r.img.SquashedSearchContext.SearchByPath(l.AccessPath, filetree.DoNotFollowDeadBasenameLinks)
if err != nil || !ref.HasReference() {
annotation = file.HiddenAnnotation
} else if ref.ID() != givenRef.ID() {
// we may have the path in the squashed tree, but this must not be in the same layer
annotation = file.HiddenAnnotation
}
if annotation == file.VisibleAnnotation && l.AccessPath != "" && !r.pathResolvesToRef(l.AccessPath, givenRef) {
annotation = file.HiddenAnnotation
}
l.Annotations[file.VisibleAnnotationKey] = annotation
}
// pathResolvesToRef reports whether the given path in the squashed tree resolves to the given reference.
func (r *ContainerImageAllLayers) pathResolvesToRef(path string, target stereoscopeFile.Reference) bool {
ref, err := r.img.SquashedSearchContext.SearchByPath(path, filetree.DoNotFollowDeadBasenameLinks)
return err == nil && ref.HasReference() && ref.ID() == target.ID()
}
@@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/file"
)
@@ -574,3 +575,105 @@ func TestSquashResolver_AllLocations(t *testing.T) {
assert.ElementsMatchf(t, expected, pathsList, "expected all paths to be indexed, but found different paths: \n%s", cmp.Diff(expected, paths.List()))
}
// a hardlink should be surfaced at its own path as the underlying type it points to (a regular file with the target's
// content and metadata), so that image results are in parity with directory results (which cannot distinguish a
// hardlink from a regular file). this must hold for both the squashed and all-layers resolvers.
func TestImageResolvers_Hardlinks(t *testing.T) {
img := imagetest.GetFixtureImage(t, "docker-archive", "image-hardlinks")
resolvers := map[string]file.Resolver{}
squash, err := NewFromContainerImageSquash(img)
require.NoError(t, err)
resolvers["squashed"] = squash
allLayers, err := NewFromContainerImageAllLayers(img)
require.NoError(t, err)
resolvers["all-layers"] = allLayers
const wantContents = "hardlinked contents\n"
// asserts a location surfaces the hardlink at its own path as a regular file with the target's content, and is
// not marked hidden (all-layers annotates visibility; squash leaves the annotation unset, which must not read as
// hidden either).
assertHardlink := func(t *testing.T, resolver file.Resolver, wantPath string, loc file.Location) {
t.Helper()
assert.Equal(t, wantPath, loc.RealPath, "expected the hardlink's own path, not the target's")
assert.NotEqual(t, file.HiddenAnnotation, loc.Annotations[file.VisibleAnnotationKey], "path=%s should not be hidden", wantPath)
meta, err := resolver.FileMetadataByLocation(loc)
require.NoError(t, err)
assert.Equal(t, stereoscopeFile.TypeRegular, meta.Type, "path=%s", wantPath)
reader, err := resolver.FileContentsByLocation(loc)
require.NoError(t, err)
actual, err := io.ReadAll(reader)
require.NoError(t, err)
assert.Equal(t, wantContents, string(actual), "path=%s", wantPath)
}
for name, resolver := range resolvers {
t.Run(name, func(t *testing.T) {
t.Run("FilesByPath surfaces a hardlink at its own path with the target's content", func(t *testing.T) {
for _, path := range []string{"/hardlink-a", "/hardlink-b", "/file.txt"} {
locs, err := resolver.FilesByPath(path)
require.NoError(t, err)
require.Len(t, locs, 1, "path=%s", path)
assertHardlink(t, resolver, path, locs[0])
}
})
t.Run("FilesByGlob surfaces every hardlink in a matched set, not just one", func(t *testing.T) {
// the whole point of #5019: a glob that matches multiple hardlinks to the same target must return
// all of them (each at its own path), not collapse them onto a single entry.
locs, err := resolver.FilesByGlob("**/hardlink-*")
require.NoError(t, err)
byPath := map[string]file.Location{}
for _, loc := range locs {
_, dup := byPath[loc.RealPath]
assert.Falsef(t, dup, "path %s emitted more than once", loc.RealPath)
byPath[loc.RealPath] = loc
}
for _, path := range []string{"/hardlink-a", "/hardlink-b"} {
loc, ok := byPath[path]
require.Truef(t, ok, "expected glob to surface %s", path)
assertHardlink(t, resolver, path, loc)
}
})
t.Run("FilesByMIMEType surfaces every hardlink name", func(t *testing.T) {
// this is the surface #5029 could not fix and stereoscope's index-time adoption does: MIME-driven
// catalogers see each name. it also pins that the names are separately addressable (distinct
// references), since this resolver dedups on reference and would return one entry if they shared it.
locs, err := resolver.FilesByMIMEType("text/plain")
require.NoError(t, err)
byPath := map[string]file.Location{}
for _, loc := range locs {
byPath[loc.RealPath] = loc
}
for _, path := range []string{"/file.txt", "/hardlink-a", "/hardlink-b"} {
loc, ok := byPath[path]
require.Truef(t, ok, "expected MIME search to surface %s (got %v)", path, byPath)
assertHardlink(t, resolver, path, loc)
}
})
t.Run("AllLocations includes every hardlink path as a regular file exactly once", func(t *testing.T) {
counts := map[string]int{}
locsByPath := map[string]file.Location{}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for loc := range resolver.AllLocations(ctx) {
counts[loc.RealPath]++
locsByPath[loc.RealPath] = loc
}
for _, path := range []string{"/file.txt", "/hardlink-a", "/hardlink-b"} {
require.Equalf(t, 1, counts[path], "expected %s to be reported exactly once by AllLocations", path)
assertHardlink(t, resolver, path, locsByPath[path])
}
})
})
}
}
@@ -126,6 +126,14 @@ func (r *directoryIndexer) indexTree(root string, stager *progress.AtomicStage)
shouldIndexFullTree, err := isRealPath(root)
if err != nil {
// a symlink can resolve into a directory we don't have permission to traverse (or into a path
// that no longer exists). Like any other inaccessible path, this should be recorded and skipped
// with a warning rather than aborting the entire scan (see #3286).
var pathErr *os.PathError
if errors.As(err, &pathErr) {
r.isFileAccessErr(root, err)
return nil, nil
}
return nil, err
}
@@ -286,6 +286,42 @@ func TestDirectoryIndexer_index_survive_badSymlink(t *testing.T) {
require.NoError(t, err)
}
func TestDirectoryIndexer_index_survive_inaccessibleSymlinkTarget(t *testing.T) {
// a symlink that resolves into a directory we don't have permission to traverse should be skipped
// with a warning like any other inaccessible path, not abort the entire scan (see #3286).
if os.Geteuid() == 0 {
t.Skip("cannot run as root: root bypasses the directory permissions that trigger the bug")
}
// resolve any symlinks in the temp path (e.g. macOS /var -> /private/var) so the indexer root is a
// real path -- otherwise indexing takes the symlinked-branch code path and never reaches the bug.
root, err := filepath.EvalSymlinks(t.TempDir())
require.NoError(t, err)
// root/
// ├── restricted/ (chmod 0000 -- cannot be traversed)
// │ └── nested/
// │ └── target.txt
// └── link -> root/restricted/nested/target.txt
nested := filepath.Join(root, "restricted", "nested")
require.NoError(t, os.MkdirAll(nested, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(nested, "target.txt"), []byte("hello"), 0o644))
require.NoError(t, os.Symlink(filepath.Join(root, "restricted", "nested", "target.txt"), filepath.Join(root, "link")))
restricted := filepath.Join(root, "restricted")
require.NoError(t, os.Chmod(restricted, 0o000))
// restore permissions so t.TempDir cleanup can remove the tree
t.Cleanup(func() { _ = os.Chmod(restricted, 0o755) })
// base is empty, matching a plain `syft <path>` invocation (the reporter's scenario)
indexer := newDirectoryIndexer(root, "")
_, _, err = indexer.build()
require.NoError(t, err)
// the inaccessible path should be recorded rather than silently dropped
require.NotEmpty(t, indexer.errPaths)
}
func TestDirectoryIndexer_SkipsAlreadyVisitedLinkDestinations(t *testing.T) {
var observedPaths []string
pathObserver := func(_, p string, _ os.FileInfo, _ error) error {
@@ -0,0 +1,12 @@
# LAYER 0:
FROM busybox:1.34.0@sha256:e8e5cca392e3cf056fcdb3093e7ac2bf83fcf28b3bcf5818fe8ae71cf360c231
# LAYER 1: a regular file with two hardlinks to it, all created in the same layer so the layer tar contains one
# regular file entry and two hardlink (tar TypeLink) entries pointing at it.
RUN echo "hardlinked contents" > /file.txt && ln /file.txt /hardlink-a && ln /file.txt /hardlink-b
# squash representation (of the files added here; busybox base also contributes hardlinked applets under /bin)
# .
# ├── file.txt
# ├── hardlink-a (hardlink -> file.txt)
# └── hardlink-b (hardlink -> file.txt)
+12 -2
View File
@@ -120,9 +120,19 @@ func (r *readerAtAdapter) ReadAt(p []byte, off int64) (n int, err error) {
return 0, err
}
n, err = r.ReadSeekCloser.Read(p) // read from that absolute position
// io.ReaderAt requires len(p) bytes or a non-nil error, and the underlying reader honors neither half of
// that: a squashfs block that decompresses short returns fewer bytes with err == nil, and a read landing
// exactly on the end of the file returns a full buffer alongside io.EOF. io.ReadFull normalizes both.
n, err = io.ReadFull(r.ReadSeekCloser, p)
// ReadAt reports a short read at the end of the file as io.EOF. compared by identity on purpose:
// io.ReadFull returns this sentinel bare, and errors.Is would also downgrade a wrapped one from the
// underlying reader, turning genuine stream corruption into a benign end of file
if err == io.ErrUnexpectedEOF {
err = io.EOF
}
// restore the position for the stateful read/seek operations
// restore the position for the stateful read/seek operations. a read error wins over a restore failure:
// callers compare against io.EOF, and masking it would strand them without the data they did get
if restoreErr := r.restorePosition(currentPos); restoreErr != nil {
if err == nil {
err = restoreErr
+112 -1
View File
@@ -2,6 +2,7 @@ package unionreader
import (
"bytes"
"errors"
"io"
"strings"
"sync"
@@ -120,12 +121,14 @@ func TestReaderAtAdapter_ReadAt(t *testing.T) {
expectedStr: "",
},
{
// io.ReaderAt requires a non-nil error whenever it returns fewer than len(p) bytes, so a
// buffer that runs off the end of the file reports io.EOF alongside what it did read
name: "partial read",
data: "Hello",
offset: 2,
bufSize: 10,
expectedN: 3,
expectedErr: nil,
expectedErr: io.EOF,
expectedStr: "llo",
},
{
@@ -335,3 +338,111 @@ func (r *readSeekCloser) Close() error {
r.closed = true
return nil
}
// scriptedReadSeeker replays a fixed sequence of Read results so tests can express the read shapes a
// squashfs-backed reader actually produces, none of which an io.ReaderAt may pass through to callers:
// a short count with a nil error, a full count paired with io.EOF, or a failure partway through a buffer.
type scriptedReadSeeker struct {
reads []scriptedRead
next int
offset int64
}
type scriptedRead struct {
data string
err error
}
func (r *scriptedReadSeeker) Read(p []byte) (int, error) {
if r.next >= len(r.reads) {
return 0, io.EOF
}
read := r.reads[r.next]
r.next++
n := copy(p, read.data)
r.offset += int64(n)
return n, read.err
}
func (r *scriptedReadSeeker) Seek(offset int64, whence int) (int64, error) {
if whence == io.SeekCurrent {
return r.offset, nil
}
r.offset = offset
return offset, nil
}
func (r *scriptedReadSeeker) Close() error { return nil }
func TestReaderAtAdapter_ReadAtHonorsReaderAtContract(t *testing.T) {
errBoom := errors.New("boom")
tests := []struct {
name string
reads []scriptedRead
bufSize int
expectedN int
expectedErr error
expectedStr string
}{
{
// before io.ReadFull the caller got the first 3 bytes and a nil error, and any parser sizing a
// struct off the result read zero padding it had no way to detect
name: "fills the buffer across short reads",
reads: []scriptedRead{{data: "abc"}, {data: "def"}, {data: "ghi"}},
bufSize: 9,
expectedN: 9,
expectedErr: nil,
expectedStr: "abcdefghi",
},
{
// squashfs pairs io.EOF with a full buffer when a read lands exactly on the end of the file;
// bytes.Reader.ReadAt returns nil there, and callers that treat any error as fatal rely on it
name: "drops io.EOF when the read still filled the buffer",
reads: []scriptedRead{{data: "abc"}, {data: "def", err: io.EOF}},
bufSize: 6,
expectedN: 6,
expectedErr: nil,
expectedStr: "abcdef",
},
{
// a short tail must surface as io.EOF rather than io.ErrUnexpectedEOF so callers comparing
// against io.EOF keep working
name: "reports a short tail as io.EOF",
reads: []scriptedRead{{data: "abc"}, {data: "de", err: io.EOF}},
bufSize: 10,
expectedN: 5,
expectedErr: io.EOF,
expectedStr: "abcde",
},
{
// the io.ErrUnexpectedEOF remap must not swallow a real read failure, and the byte count has to
// survive it, otherwise callers cannot tell how much of the buffer is trustworthy
name: "propagates a failure partway through the buffer",
reads: []scriptedRead{{data: "abc"}, {data: "", err: errBoom}},
bufSize: 10,
expectedN: 3,
expectedErr: errBoom,
expectedStr: "abc",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reader := &scriptedReadSeeker{reads: tt.reads}
adapter := newReaderAtAdapter(reader)
buf := make([]byte, tt.bufSize)
n, err := adapter.ReadAt(buf, 7)
require.ErrorIs(t, err, tt.expectedErr)
assert.Equal(t, tt.expectedN, n)
assert.Equal(t, tt.expectedStr, string(buf[:n]))
// the position must be restored even though the read spanned multiple underlying calls
pos, err := adapter.Seek(0, io.SeekCurrent)
require.NoError(t, err)
assert.Zero(t, pos)
})
}
}
@@ -32,12 +32,15 @@ type parsedData struct {
pkg.ApkDBEntry
}
const maxApkDBFieldSize = 10 * 1024 * 1024
// parseApkDB parses packages from a given APK "installed" flat-file DB. For more
// information on specific fields, see https://wiki.alpinelinux.org/wiki/Apk_spec.
//
//nolint:funlen
func parseApkDB(ctx context.Context, resolver file.Resolver, env *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
scanner := bufio.NewScanner(reader)
scanner.Buffer(nil, maxApkDBFieldSize)
var errs error
var apks []parsedData
@@ -719,6 +719,25 @@ func Test_processChecksum(t *testing.T) {
}
}
func TestParseApkDBAllowsLargeFieldValues(t *testing.T) {
contents := strings.Join([]string{
"P:large-description",
"V:1.0-r0",
"A:x86_64",
"S:1",
"I:1",
"T:" + strings.Repeat("a", 70*1024),
"",
}, "\n")
reader := file.NewLocationReadCloser(file.NewLocation("large-installed-db"), io.NopCloser(strings.NewReader(contents)))
pkgs, _, err := parseApkDB(context.Background(), nil, new(generic.Environment), reader)
require.NoError(t, err)
require.Len(t, pkgs, 1)
assert.Equal(t, "large-description", pkgs[0].Name)
}
func Test_parseApkDB_expectedPkgNames(t *testing.T) {
tests := []struct {
fixture string
+44
View File
@@ -1,10 +1,14 @@
package arch
import (
"bytes"
"context"
"io"
"os"
"testing"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
@@ -12,7 +16,23 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
const bombFixture = "testdata/installed/var/lib/pacman/local/bombpkg-1.0-1/mtree"
// writeBombFixture generates bombpkg's mtree: one line over maxMtreeLines, so the cataloger rejects
// it while walking testdata/installed. Generated rather than committed so the size that makes it a
// bomb tracks the constant instead of hiding in a gzip blob.
func writeBombFixture(t *testing.T) {
t.Helper()
listing := append([]byte("#mtree\n"), bytes.Repeat([]byte("./f\n"), maxMtreeLines+1)...)
compressed, err := io.ReadAll(gzipOf(t, listing))
require.NoError(t, err)
require.NoError(t, os.WriteFile(bombFixture, compressed, 0o600))
}
func TestAlpmUnknowns(t *testing.T) {
writeBombFixture(t)
pkgtest.NewCatalogTester().
FromDirectory(t, "testdata/installed").
WithCompareOptions(cmpopts.IgnoreFields(pkg.AlpmFileRecord{}, "Time")).
@@ -21,11 +41,14 @@ func TestAlpmUnknowns(t *testing.T) {
}
func TestAlpmCataloger(t *testing.T) {
writeBombFixture(t)
gmpDbLocation := file.NewLocation("var/lib/pacman/local/gmp-6.2.1-2/desc")
treeSitterDbLocation := file.NewLocation("var/lib/pacman/local/tree-sitter-0.22.6-1/desc")
emacsDbLocation := file.NewLocation("var/lib/pacman/local/emacs-29.3-3/desc")
fuzzyDbLocation := file.NewLocation("var/lib/pacman/local/fuzzy-1.2-3/desc")
madeupDbLocation := file.NewLocation("var/lib/pacman/local/madeup-20.30-4/desc")
bombDbLocation := file.NewLocation("var/lib/pacman/local/bombpkg-1.0-1/desc")
ctx := context.TODO()
treeSitterPkg := pkg.Package{
@@ -119,6 +142,26 @@ func TestAlpmCataloger(t *testing.T) {
},
}
// bombPkg's listing exceeds maxMtreeLines, so it emits with an empty file list, not dropped
bombPkg := pkg.Package{
Name: "bombpkg",
Version: "1.0-1",
Type: pkg.AlpmPkg,
FoundBy: "alpm-db-cataloger",
Locations: file.NewLocationSet(bombDbLocation),
Metadata: pkg.AlpmDBEntry{
BasePackage: "bombpkg",
Package: "bombpkg",
Version: "1.0-1",
Description: "Package whose mtree listing exceeds the parser's line bound",
Architecture: "x86_64",
Size: 1,
Reason: 1,
Files: []pkg.AlpmFileRecord{},
Backup: []pkg.AlpmFileRecord{},
},
}
gmpPkg := pkg.Package{
Name: "gmp",
Version: "6.2.1-2",
@@ -283,6 +326,7 @@ func TestAlpmCataloger(t *testing.T) {
fuzzyPkg,
madeupPkg,
gmpPkg,
bombPkg,
}
expectedRelationships := []artifact.Relationship{
+107 -3
View File
@@ -4,6 +4,7 @@ import (
"bufio"
"compress/gzip"
"context"
"errors"
"fmt"
"io"
"path"
@@ -16,6 +17,7 @@ import (
"github.com/vbatts/go-mtree"
"github.com/anchore/syft/internal"
intFile "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
@@ -216,6 +218,10 @@ func parseDatabase(b *bufio.Scanner) (*parsedData, error) {
var backup []map[string]any
for _, f := range strings.Split(value, "\n") {
fields := strings.SplitN(f, "\t", 2)
// without the tab there is no digest, and fields[1] would panic
if len(fields) < 2 {
continue
}
p := fmt.Sprintf("/%s", fields[0])
if ok := ignoredFiles[p]; !ok {
backup = append(backup, map[string]any{
@@ -241,6 +247,12 @@ func parseDatabase(b *bufio.Scanner) (*parsedData, error) {
}
}
// a field over the scanner's 1MB token cap ends the scan early, which would otherwise look
// like a complete package
if err := b.Err(); err != nil {
return nil, err
}
return parsePkgFiles(pkgFields)
}
@@ -277,15 +289,107 @@ func parsePkgFiles(pkgFields map[string]any) (*parsedData, error) {
return &entry, nil
}
// maxMtreeSize bounds the decompressed listing. texlive-fontsextra, the largest package Arch
// ships, runs 16 to 27MB.
//
// this caps bytes, not memory: go-mtree allocates per field, so a crafted listing can still cost
// about a gigabyte, times cataloger parallelism.
const maxMtreeSize = 64 * intFile.MB
// maxMtreeLines bounds entry count, which the byte cap does not: go-mtree keeps a ~128 byte entry
// per line, blanks included. 300k is ~2.8x the largest package Arch ships.
const maxMtreeLines = 300_000
var (
errMtreeTooLarge = errors.New("mtree file is larger than the max allowed size")
errTooManyMtreeLines = errors.New("mtree file has more lines than allowed")
// errMtreeLineContinued rejects a line ending in a backslash, which readers treat as "join with
// the next line". We refuse the file instead, for two reasons.
//
// cost: go-mtree joins with a quadratic string concat, so work grows as lines squared. 100k
// continued lines is a 1.2MB listing costing ~3.7s and ~52GB of churn. The bounds above are
// linear and never see it, and no line cap helps, since real packages reach ~107k lines.
//
// spec: mtree(5) says "Each line is interpreted independently" and documents no continuation,
// nor does Arch's ALPM-MTREEv2(5). Readers accept them anyway (BSD mtree via fparseln(3),
// libarchive in readline()), but no writer emits one: libarchive escapes a literal backslash as
// \134, and Arch builds .MTREE with it. So a continuation here is crafted, not a package.
errMtreeLineContinued = errors.New("mtree file uses line continuations")
)
// lineLimitedReader fails the read once the stream carries more than max newlines, or a line
// continuation. Counting while the bytes flow is what bounds entries: the parser builds one per
// line before returning any of them.
//
// continuations are refused rather than joined, unlike every other mtree reader. See
// errMtreeLineContinued. Both \n and \r\n count, since go-mtree strips a trailing carriage return
// before testing for the backslash.
//
// a tripped bound is latched, so reading past the first error does not come back clean.
type lineLimitedReader struct {
reader io.Reader
lines int
max int
lastByte byte
err error
}
func (l *lineLimitedReader) Read(p []byte) (int, error) {
if l.err != nil {
return 0, l.err
}
n, err := l.reader.Read(p)
// prev carries over the last byte of the previous read, so a backslash split from its line
// ending is still caught
prev := l.lastByte
for _, c := range p[:n] {
if (c == '\n' || c == '\r') && prev == '\\' {
l.err = errMtreeLineContinued
return n, l.err
}
if c == '\n' {
l.lines++
}
prev = c
}
l.lastByte = prev
if l.lines > l.max {
l.err = fmt.Errorf("%w (max %d)", errTooManyMtreeLines, l.max)
return n, l.err
}
return n, err
}
func parseMtree(r io.Reader) ([]pkg.AlpmFileRecord, error) {
var err error
return parseMtreeWithLimits(r, maxMtreeSize, maxMtreeLines)
}
// parseMtreeWithLimits lets the boundary tests use small exact limits instead of the shipped caps.
// maxSize must be below math.MaxInt64, since the limiter carries a byte of headroom.
func parseMtreeWithLimits(r io.Reader, maxSize int64, maxLines int) ([]pkg.AlpmFileRecord, error) {
var entries []pkg.AlpmFileRecord
r, err = gzip.NewReader(r)
gzReader, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
specDh, err := mtree.ParseSpec(r)
defer internal.CloseAndLogError(gzReader, "mtree")
// the line cap trips mid-stream, the byte cap cannot: io.LimitedReader reports a drained budget
// as io.EOF, which the parser reads as a complete listing, so it is checked after the parse. The
// extra byte tells a drained budget from a listing ending exactly at the cap.
sizeLimited := &io.LimitedReader{R: gzReader, N: maxSize + 1}
specDh, err := mtree.ParseSpec(&lineLimitedReader{reader: sizeLimited, max: maxLines})
// before err: a drained budget means the parser saw only part of the listing
if sizeLimited.N <= 0 {
return nil, fmt.Errorf("%w (%d bytes)", errMtreeTooLarge, maxSize)
}
if err != nil {
return nil, err
}
@@ -2,13 +2,19 @@ package arch
import (
"bufio"
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
intFile "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
@@ -155,6 +161,26 @@ func TestDatabaseParser(t *testing.T) {
}
}
func Test_parseAlpmDBEntry_malformedDatabase(t *testing.T) {
t.Run("backup line with no tab", func(t *testing.T) {
// this used to panic, which drops every package rather than the one
entry, err := parseAlpmDBEntry(strings.NewReader("%NAME%\nfoo\n\n%BACKUP%\nnotabhere\n\n"))
require.NoError(t, err)
require.NotNil(t, entry)
require.Empty(t, entry.Backup)
})
t.Run("field over the scanner's token cap", func(t *testing.T) {
// everything after the oversize field is lost, so this has to error rather than look complete
db := "%NAME%\nfoo\n\n%DESC%\n" + strings.Repeat("x", 2*intFile.MB) + "\n\n%ARCH%\nx86_64\n\n"
_, err := parseAlpmDBEntry(strings.NewReader(db))
require.ErrorIs(t, err, bufio.ErrTooLong)
})
}
func parseTime(stime string) time.Time {
t, _ := time.Parse(time.RFC3339, stime)
return t
@@ -207,6 +233,7 @@ func TestMtreeParse(t *testing.T) {
reader := bufio.NewReader(f)
// a normal listing must not trip any of the bounds
entry, err := parseMtree(reader)
require.NoError(t, err)
@@ -217,3 +244,243 @@ func TestMtreeParse(t *testing.T) {
}
}
// mtreeSpec builds a valid listing of n files. Boundary tests need the parser to reach the end of
// it, which filler bytes never do.
func mtreeSpec(n int) []byte {
var buf bytes.Buffer
buf.WriteString("#mtree\n")
buf.WriteString("/set type=file uid=0 gid=0 mode=644\n")
for i := range n {
fmt.Fprintf(&buf, "./file%d time=1649595592.0 size=10 sha256digest=%064x\n", i, i)
}
return buf.Bytes()
}
func gzipOf(t *testing.T, data []byte) io.Reader {
t.Helper()
var buf bytes.Buffer
w := gzip.NewWriter(&buf)
_, err := w.Write(data)
require.NoError(t, err)
require.NoError(t, w.Close())
return bytes.NewReader(buf.Bytes())
}
// gzipOfRepeated builds a gzip member decompressing to n bytes of one repeated byte, so a few KB of
// input expands to whatever size the caller asks for.
func gzipOfRepeated(t *testing.T, payload byte, n int64) *bytes.Reader {
t.Helper()
var buf bytes.Buffer
w := gzip.NewWriter(&buf)
chunk := bytes.Repeat([]byte{payload}, 32*1024)
for remaining := n; remaining > 0; {
size := min(remaining, int64(len(chunk)))
written, err := w.Write(chunk[:size])
require.NoError(t, err)
remaining -= int64(written)
}
require.NoError(t, w.Close())
return bytes.NewReader(buf.Bytes())
}
func Test_parseMtree_boundsDecompressedSize(t *testing.T) {
// limits come from the fixture, so both sides of the boundary are exact
spec := mtreeSpec(50)
t.Run("a listing at the cap parses whole", func(t *testing.T) {
records, err := parseMtreeWithLimits(gzipOf(t, spec), int64(len(spec)), maxMtreeLines)
// records, not just no error: landing exactly on the cap must not truncate
require.NoError(t, err)
require.Len(t, records, 50)
require.Equal(t, "/file0", records[0].Path)
require.Equal(t, "/file49", records[49].Path)
})
t.Run("rejects a listing one byte past the cap", func(t *testing.T) {
_, err := parseMtreeWithLimits(gzipOf(t, spec), int64(len(spec))-1, maxMtreeLines)
require.ErrorIs(t, err, errMtreeTooLarge)
})
}
// Test_parseMtree_shippedConstants pins the production values. The boundary tests parameterize the
// limits, so they would pass unchanged if a constant regressed. Driving a real 64MB listing through
// the parser instead cost 12s of a 16s race run.
func Test_parseMtree_shippedConstants(t *testing.T) {
require.Equal(t, int64(64*intFile.MB), int64(maxMtreeSize))
require.Equal(t, 300_000, maxMtreeLines)
}
func Test_parseMtree_boundsLineCount(t *testing.T) {
// the byte cap does not bound memory: the parser keeps an entry per line, blanks included
spec := mtreeSpec(50)
lines := bytes.Count(spec, []byte("\n")) // the two header lines get entries of their own
t.Run("a listing at the cap parses whole", func(t *testing.T) {
records, err := parseMtreeWithLimits(gzipOf(t, spec), maxMtreeSize, lines)
require.NoError(t, err)
require.Len(t, records, 50)
})
t.Run("rejects a listing one line past the cap", func(t *testing.T) {
_, err := parseMtreeWithLimits(gzipOf(t, spec), maxMtreeSize, lines-1)
require.ErrorIs(t, err, errTooManyMtreeLines)
})
t.Run("rejects a bomb at the production limits without reading all of it", func(t *testing.T) {
// what the cap exists for: a few KB expanding past the line cap while inside the byte cap
bomb := gzipOfRepeated(t, '\n', 4*1024*1024)
require.Less(t, bomb.Size(), int64(64*1024), "payload should be small enough to be worth rejecting")
counted := &countingReader{reader: bomb}
_, err := parseMtree(counted)
require.ErrorIs(t, err, errTooManyMtreeLines)
// what matters is giving up mid-read, not after building the whole listing. Bytes consumed is
// the stable way to check that; an allocation delta drifts with GC and parallelism.
require.Less(t, counted.n, bomb.Size(),
"the whole bomb was read, so the cap tripped after the parse rather than during it")
})
}
// countingReader records how many bytes were pulled through it.
type countingReader struct {
reader io.Reader
n int64
}
func (c *countingReader) Read(p []byte) (int, error) {
n, err := c.reader.Read(p)
c.n += int64(n)
return n, err
}
// Test_parseMtree_sizeCapTakesPrecedenceOverLineCap pins which error wins when one read exceeds both
// bounds. No other test exceeds both at once, so a reorder would go unnoticed.
func Test_parseMtree_sizeCapTakesPrecedenceOverLineCap(t *testing.T) {
spec := mtreeSpec(50)
// maxLines=0 trips on the first newline; maxSize=20 is drained by that same read
_, err := parseMtreeWithLimits(gzipOf(t, spec), 20, 0)
require.ErrorIs(t, err, errMtreeTooLarge)
require.NotErrorIs(t, err, errTooManyMtreeLines)
}
func Test_parseMtree_rejectsLineContinuations(t *testing.T) {
// go-mtree strips a trailing carriage return before testing for the backslash, so CRLF continues
// a line too
for _, lineEnding := range []string{"\\\n", "\\\r\n"} {
t.Run(fmt.Sprintf("%q", lineEnding), func(t *testing.T) {
spec := mtreeSpec(5)
spec = bytes.Replace(spec, []byte("size=10 sha256digest="), []byte("size=10 "+lineEnding+"sha256digest="), 1)
_, err := parseMtree(gzipOf(t, spec))
require.ErrorIs(t, err, errMtreeLineContinued)
})
}
}
// scriptedReader replays a fixed sequence of Read results, driving the straddling case directly.
type scriptedReader struct {
chunks []string
}
func (r *scriptedReader) Read(p []byte) (int, error) {
if len(r.chunks) == 0 {
return 0, io.EOF
}
n := copy(p, r.chunks[0])
// keep what did not fit, so a short buffer still obeys io.Reader
if n < len(r.chunks[0]) {
r.chunks[0] = r.chunks[0][n:]
} else {
r.chunks = r.chunks[1:]
}
return n, nil
}
func Test_lineLimitedReader_rejectsStraddlingContinuation(t *testing.T) {
// the backslash and its line ending land in separate Read calls, which happens for real: the
// io.LimitedReader truncates the buffer and flate returns short reads.
tests := []struct {
name string
chunks []string
}{
{name: "lf", chunks: []string{"...\\", "\n..."}},
{name: "crlf", chunks: []string{"...\\", "\r\n..."}},
{name: "crlf split at the carriage return", chunks: []string{"...\\\r", "\n..."}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lr := &lineLimitedReader{reader: &scriptedReader{chunks: tt.chunks}, max: 100}
var err error
buf := make([]byte, 16)
for err == nil {
_, err = lr.Read(buf)
}
require.ErrorIs(t, err, errMtreeLineContinued)
})
}
}
func Test_lineLimitedReader_latchesTheError(t *testing.T) {
// a tripped bound must not come back clean on the next read
lr := &lineLimitedReader{reader: &scriptedReader{chunks: []string{"a\\\nb", "clean"}}, max: 100}
buf := make([]byte, 16)
_, first := lr.Read(buf)
require.ErrorIs(t, first, errMtreeLineContinued)
n, second := lr.Read(buf)
require.ErrorIs(t, second, errMtreeLineContinued)
require.Zero(t, n)
}
func Test_parseMtree_malformedInput(t *testing.T) {
t.Run("not gzip", func(t *testing.T) {
_, err := parseMtree(bytes.NewReader(mtreeSpec(2)))
require.ErrorIs(t, err, gzip.ErrHeader)
})
t.Run("truncated gzip", func(t *testing.T) {
spec := mtreeSpec(50)
var buf bytes.Buffer
_, err := io.Copy(&buf, gzipOf(t, spec))
require.NoError(t, err)
// the cap sits just above what the truncated stream delivers, so the size check and the
// truncation actually race. The shipped 64MB cap could never fire here.
_, err = parseMtreeWithLimits(bytes.NewReader(buf.Bytes()[:buf.Len()/2]), int64(len(spec)), maxMtreeLines)
// must fail, but not as a size-bound trip
require.Error(t, err)
require.NotErrorIs(t, err, errMtreeTooLarge)
})
t.Run("single line over the scanner's token cap", func(t *testing.T) {
// the third bound is go-mtree's scanner, at the default 64KB token limit. Nothing else here
// would catch a dependency bump that calls scanner.Buffer.
spec := append([]byte("#mtree\n./big "), bytes.Repeat([]byte("a"), 128*1024)...)
_, err := parseMtree(gzipOf(t, append(spec, '\n')))
require.ErrorContains(t, err, "token too long")
})
}
@@ -0,0 +1,2 @@
# generated by writeBombFixture, see cataloger_test.go
/mtree
@@ -0,0 +1,20 @@
%NAME%
bombpkg
%VERSION%
1.0-1
%BASE%
bombpkg
%DESC%
Package whose mtree listing exceeds the parser's line bound
%ARCH%
x86_64
%SIZE%
1
%REASON%
1
@@ -433,6 +433,16 @@ catalogers:
cpes:
- cpe:2.3:a:deno:deno:*:*:*:*:*:*:*:*
type: BinaryPkg
- method: glob
criteria:
- '**/bun'
packages:
- class: bun-binary
name: bun
purl: pkg:generic/bun
cpes:
- cpe:2.3:a:oven-sh:bun:*:*:*:*:*:*:*:*
type: BinaryPkg
- method: glob
criteria:
- '**/ghc*'
@@ -1499,6 +1499,28 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
Metadata: metadata("deno-binary"),
},
},
{
logicalFixture: "deno/1.11.3/linux-amd64",
expected: pkg.Package{
Name: "deno",
Version: "1.11.3",
Type: "binary",
PURL: "pkg:generic/deno@1.11.3",
Locations: locations("deno"),
Metadata: metadata("deno-binary"),
},
},
{
logicalFixture: "deno/1.11.4/linux-amd64",
expected: pkg.Package{
Name: "deno",
Version: "1.11.4",
Type: "binary",
PURL: "pkg:generic/deno@1.11.4",
Locations: locations("deno"),
Metadata: metadata("deno-binary"),
},
},
{
logicalFixture: "deno/1.16.4/linux-amd64",
expected: pkg.Package{
@@ -1565,6 +1587,61 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
Metadata: metadata("deno-binary"),
},
},
{
logicalFixture: "deno/2.9.2/linux-amd64",
expected: pkg.Package{
Name: "deno",
Version: "2.9.2",
Type: "binary",
PURL: "pkg:generic/deno@2.9.2",
Locations: locations("deno"),
Metadata: metadata("deno-binary"),
},
},
{
logicalFixture: "bun/0.5.9/linux-amd64",
expected: pkg.Package{
Name: "bun",
Version: "0.5.9",
Type: "binary",
PURL: "pkg:generic/bun@0.5.9",
Locations: locations("bun"),
Metadata: metadata("bun-binary"),
},
},
{
logicalFixture: "bun/1.0.0/linux-amd64",
expected: pkg.Package{
Name: "bun",
Version: "1.0.0",
Type: "binary",
PURL: "pkg:generic/bun@1.0.0",
Locations: locations("bun"),
Metadata: metadata("bun-binary"),
},
},
{
logicalFixture: "bun/1.1.0/linux-amd64",
expected: pkg.Package{
Name: "bun",
Version: "1.1.0",
Type: "binary",
PURL: "pkg:generic/bun@1.1.0",
Locations: locations("bun"),
Metadata: metadata("bun-binary"),
},
},
{
logicalFixture: "bun/1.3.5/linux-amd64",
expected: pkg.Package{
Name: "bun",
Version: "1.3.5",
Type: "binary",
PURL: "pkg:generic/bun@1.3.5",
Locations: locations("bun"),
Metadata: metadata("bun-binary"),
},
},
{
logicalFixture: "haskell-ghc/7.10.3/linux-amd64",
expected: pkg.Package{
@@ -2217,6 +2294,39 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
Metadata: metadata("grafana-binary"),
},
},
{
logicalFixture: "grafana/12.4.3-security-02/linux-arm",
expected: pkg.Package{
Name: "grafana",
Version: "12.4.3",
Type: "binary",
PURL: "pkg:generic/grafana@12.4.3",
Locations: locations("grafana"),
Metadata: metadata("grafana-binary"),
},
},
{
logicalFixture: "grafana/12.0.0-security-01/linux-amd64",
expected: pkg.Package{
Name: "grafana",
Version: "12.0.0",
Type: "binary",
PURL: "pkg:generic/grafana@12.0.0",
Locations: locations("grafana"),
Metadata: metadata("grafana-binary"),
},
},
{
logicalFixture: "grafana/11.0.5-security-01/linux-amd64",
expected: pkg.Package{
Name: "grafana",
Version: "11.0.5",
Type: "binary",
PURL: "pkg:generic/grafana@11.0.5",
Locations: locations("grafana"),
Metadata: metadata("grafana-binary"),
},
},
{
logicalFixture: "grafana/12.3.1/linux-amd64",
expected: pkg.Package{
@@ -2536,6 +2646,39 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
Metadata: metadata("ingress-nginx-binary"),
},
},
{
logicalFixture: "nginx-ingress-controller/1.9.6/linux-arm64",
expected: pkg.Package{
Name: "nginx-ingress-controller",
Version: "1.9.6",
Type: "binary",
PURL: "pkg:generic/nginx-ingress-controller@1.9.6",
Locations: locations("nginx-ingress-controller"),
Metadata: metadata("ingress-nginx-binary"),
},
},
{
logicalFixture: "nginx-ingress-controller/1.9.6/linux-arm",
expected: pkg.Package{
Name: "nginx-ingress-controller",
Version: "1.9.6",
Type: "binary",
PURL: "pkg:generic/nginx-ingress-controller@1.9.6",
Locations: locations("nginx-ingress-controller"),
Metadata: metadata("ingress-nginx-binary"),
},
},
{
logicalFixture: "nginx-ingress-controller/1.9.6/linux-s390x",
expected: pkg.Package{
Name: "nginx-ingress-controller",
Version: "1.9.6",
Type: "binary",
PURL: "pkg:generic/nginx-ingress-controller@1.9.6",
Locations: locations("nginx-ingress-controller"),
Metadata: metadata("ingress-nginx-binary"),
},
},
{
logicalFixture: "nginx-ingress-controller/1.7.1/linux-amd64",
expected: pkg.Package{
@@ -2992,8 +3135,8 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
Name: "busybox",
Version: "1.35.0",
PURL: "pkg:generic/busybox@1.35.0",
Locations: locations("/bin/["),
Metadata: metadata("busybox-binary", "/bin/[", "/bin/busybox"),
Locations: locations("/bin/busybox"),
Metadata: metadata("busybox-binary", "/bin/busybox"),
},
},
}
+38
View File
@@ -588,11 +588,20 @@ func DefaultClassifiers() []binutils.Classifier {
Class: "deno-binary",
FileGlob: "**/deno",
EvidenceMatcher: binutils.MatchAny(
m.FileContentsVersionMatcher(
// Deno/2.6.3Deno/
// Deno/1.41.0cli/
`Deno/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)(Deno/|cli/)`,
),
m.FileContentsVersionMatcher(
// Deno/2.6.3
// Deno/1.41.0
`Deno/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`,
),
m.FileContentsVersionMatcher(
// cli/tools/standalone.rsdeno-canary/f4bed1081456089559c82441a13c4fb700840cac1.11.3dlwindows
`deno-canary/[0-9a-z]{40}(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`,
),
m.FileContentsVersionMatcher(
// deno::tools::standalonedeno-65db94feba9d4d51a09b74629f566dbc90484fbarelease/v1.29.4windows
// cli/tools/standalone.rsdeno-74064c9d8c222b33b2a552ea0af1054f57002a96release/v1.28.3windows
@@ -611,6 +620,20 @@ func DefaultClassifiers() []binutils.Classifier {
PURL: mustPURL("pkg:generic/deno@version"),
CPEs: singleCPE("cpe:2.3:a:deno:deno:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
},
{
Class: "bun-binary",
FileGlob: "**/bun",
EvidenceMatcher: binutils.MatchAny(
// bun 1.1.0
// Bun v1.0.0
m.FileContentsVersionMatcher(`(?m)[Bb]un v?(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
// bun-1.1.0
m.FileContentsVersionMatcher(`(?m)bun-(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
),
Package: "bun",
PURL: mustPURL("pkg:generic/bun@version"),
CPEs: singleCPE("cpe:2.3:a:oven-sh:bun:*:*:*:*:*:*:*:*"),
},
{
Class: "haskell-ghc-binary",
FileGlob: "**/ghc*",
@@ -876,6 +899,11 @@ func DefaultClassifiers() []binutils.Classifier {
Class: "gzip-binary",
FileGlob: "**/gzip",
EvidenceMatcher: m.FileContentsVersionMatcher(
// GNU gzip keeps the version as a bare NUL-delimited token, so require an identifying
// string from the program as well. Without it any binary that happens to be named gzip
// (such as the busybox multicall binary behind a gzip applet symlink) would have an
// arbitrary NUL-delimited "N.N" token reported as a GNU gzip version.
`%s: %s: not in gzip format`,
`\x00(?P<version>[0-9]+\.[0-9]+)\x00`,
),
Package: "gzip",
@@ -1026,6 +1054,11 @@ func DefaultClassifiers() []binutils.Classifier {
EvidenceMatcher: binutils.MatchAny(
// [NUL][NUL][NUL][NUL]12.2.0-258092[NUL][NUL][NUL][NUL]
m.FileContentsVersionMatcher(`\x00+(?P<version>[0-9]{2}\.[0-9]+\.[0-9]+\-[0-9]{6,})\x00+`),
// security patch releases embed the raw version constant with no "release-" prefix
// and are not always preceded by NUL bytes (e.g. on arm builds):
// [NUL]11.0.5+security-01[NUL][NUL]call frame too large
// [NUL]12.4.3+security-02[NUL][NUL]
m.FileContentsVersionMatcher(`\x00(?P<version>[0-9]{1,2}\.[0-9]+\.[0-9]+)\+security-[0-9]+\x00`),
// [NUL][NUL][NUL][NUL]release-12.3.2+security-01[NUL][NUL][NUL][NUL]
// [NUL][NUL][NUL][NUL]release-12.3.1[NUL][NUL][NUL][NUL]
m.FileContentsVersionMatcher(`\x00+release-(?P<version>[0-9]{2}\.[0-9]+\.[0-9]+(-beta[0-9]|-test|-preview)?)(\+security-[0-9]+)?\x00+`),
@@ -1130,6 +1163,11 @@ func DefaultClassifiers() []binutils.Classifier {
Class: "ingress-nginx-binary",
FileGlob: "**/nginx-ingress-controller",
EvidenceMatcher: binutils.MatchAny(
// the release is injected with -ldflags -X, which lands it in its own NUL-padded data symbol.
// the surrounding bytes are an arch-specific float constant pool, so only the padding is portable.
// e.g. v1.9.6[NUL][NUL] on each of linux/amd64, linux/arm, linux/arm64, and linux/s390x
// note: one trailing NUL is not enough -- on s390x that matches a vendored "v1.19.0" earlier in the file
m.FileContentsVersionMatcher(`v(?P<version>[0-9]+\.[0-9]+\.[0-9]+(\-(alpha|beta)\.[0-9]+)?)\x00\x00`),
// [NUL][NUL]v1.15.1[NUL][NUL]@e[ETX][NUL][NUL][NUL][NUL]go1.26.1[NUL][NUL][NUL]
// �v1.15.1[NUL][NUL]�z[ETX][NUL][NUL][NUL][NUL]go1.24.4[NUL][NUL][NUL]
m.FileContentsVersionMatcher(`v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00+.{0,50}go[0-9]+\.[0-9]+(\-(alpha|beta)\.[0-9])?\.[0-9]+\x00+`),
@@ -3,7 +3,6 @@ package binary
import (
"bytes"
"context"
"debug/elf"
"encoding/binary"
"encoding/json"
"fmt"
@@ -14,6 +13,7 @@ import (
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/elfutil"
"github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/pkg"
)
@@ -146,7 +146,7 @@ func getELFNotes(r file.LocationReadCloser) (*elfBinaryPackageNotes, error) {
return nil, fmt.Errorf("unable to get union reader for binary: %w", err)
}
f, err := elf.NewFile(unionReader)
f, err := elfutil.NewFile(unionReader)
if f == nil || err != nil {
log.WithFields("file", r.Location.Path(), "error", err).Trace("unable to parse binary as ELF")
return nil, nil
@@ -13,6 +13,7 @@ import (
"github.com/spf13/cobra"
"go.yaml.in/yaml/v3"
"github.com/anchore/syft/syft/internal/elfutil"
"github.com/anchore/syft/syft/pkg/cataloger/binary/internal/manager/internal"
"github.com/anchore/syft/syft/pkg/cataloger/binary/internal/manager/internal/config"
)
@@ -203,7 +204,7 @@ const (
)
func getPlatformElf(f *os.File) string {
elfFile, err := elf.NewFile(f)
elfFile, err := elfutil.NewFile(f)
if err != nil {
return ""
}
@@ -214,6 +215,14 @@ func getPlatformElf(f *os.File) string {
arch = amd64
case elf.EM_AARCH64:
arch = arm64
case elf.EM_ARM:
arch = "arm"
case elf.EM_S390:
arch = "s390x"
case elf.EM_386:
arch = "386"
case elf.EM_RISCV:
arch = "riscv64"
// TODO...
default:
arch = fmt.Sprintf("unknown-%x", elfFile.Machine)
@@ -36,5 +36,11 @@ func parsePE(_ context.Context, _ file.Resolver, _ *generic.Environment, reader
p := newPEPackage(f.VersionResources, f.Location)
// the file was usable but not fully parsed, which for this cataloger means the package name and version
// may have come from the filename rather than from version resources. Worth reporting alongside it.
if f.ParseErr != nil {
return []pkg.Package{p}, nil, unknown.New(reader, f.ParseErr)
}
return []pkg.Package{p}, nil, nil
}
Binary file not shown.
@@ -0,0 +1,12 @@
name: bun
offset: 1000000
length: 100
snippetSha256: 0000000000000000000000000000000000000000000000000000000000000001
fileSha256: 0000000000000000000000000000000000000000000000000000000000000001
### byte snippet to follow ###
]
Bun v0.5.9
bun-0.5.9
JavaScript runtime for developing, testing, and bundling
@@ -0,0 +1,12 @@
name: bun
offset: 1000000
length: 100
snippetSha256: 0000000000000000000000000000000000000000000000000000000000000002
fileSha256: 0000000000000000000000000000000000000000000000000000000000000002
### byte snippet to follow ###
]
Bun v1.0.0
bun-1.0.0
JavaScript runtime for developing, testing, and bundling
@@ -0,0 +1,12 @@
name: bun
offset: 1000000
length: 100
snippetSha256: 0000000000000000000000000000000000000000000000000000000000000003
fileSha256: 0000000000000000000000000000000000000000000000000000000000000003
### byte snippet to follow ###
]
Bun v1.1.0
bun-1.1.0
JavaScript runtime for developing, testing, and bundling
@@ -0,0 +1,12 @@
name: bun
offset: 1000000
length: 100
snippetSha256: 0000000000000000000000000000000000000000000000000000000000000004
fileSha256: 0000000000000000000000000000000000000000000000000000000000000004
### byte snippet to follow ###
]
Bun v1.3.5
bun-1.3.5
JavaScript runtime for developing, testing, and bundling
@@ -0,0 +1,9 @@
name: deno
offset: 4221667
length: 120
snippetSha256: 89509d15b73fd2d901b20ebfe82c67fd27f70e7b01dcbf376e7e80e6c80f97d9
fileSha256: 530a95c93ddfa41bd4d72d665e677d5af9a4275f1520e638305e60af5083a6d6
### byte snippet to follow ###
g ctrl+d or close()
Error: Uncaught cli/tools/standalone.rsdeno-canary/f4bed1081456089559c82441a13c4fb700840cac1.11.3dlw
@@ -0,0 +1,9 @@
name: deno
offset: 4225763
length: 120
snippetSha256: b47e8cf36bdf93e4bc63a2d7bd0a5d759975f12751a66908661777eba7e90025
fileSha256: 1a55c23bb0b666d1b26d9d16f3a9d27210b6b13421031f7f18ff18abfcb8664f
### byte snippet to follow ###
g ctrl+d or close()
Error: Uncaught cli/tools/standalone.rsdeno-canary/ed16f4332263e81bf00a39ebc2200291b1fcbb8e1.11.4dlw
+57 -1
View File
@@ -140,6 +140,22 @@ from-images:
paths:
- /usr/bin/deno
- name: deno
version: 1.11.3
images:
- ref: denoland/deno:1.11.3@sha256:340d071dc71c8403c75713694db04f59b8cb7f6c7244798cd7447d55f2750596
platform: linux/amd64
paths:
- /usr/bin/deno
- name: deno
version: 1.11.4
images:
- ref: denoland/deno:1.11.4@sha256:8e9a449a50b71c779ad1c10ec79055b20a331e78a3852818fac47bc3e85b49f4
platform: linux/amd64
paths:
- /usr/bin/deno
- name: deno
version: 1.16.4
images:
@@ -188,6 +204,14 @@ from-images:
paths:
- /usr/bin/deno
- name: deno
version: 2.9.2
images:
- ref: denoland/deno:ubuntu-2.9.2@sha256:0d27941f871700ebd95953c36fb1a817135c20f5afe3a645935672dfab72b2e9
platform: linux/amd64
paths:
- /usr/bin/deno
# haskell:7.10.3 detail information is missing.
- name: haskell-ghc
version: 7.10.3
@@ -1299,6 +1323,30 @@ from-images:
paths:
- /usr/share/grafana/bin/grafana
- name: grafana
version: 12.4.3-security-02
images:
- ref: grafana/grafana:12.4.3-security-02@sha256:fe5bc62ae401120c48b4cd50db122a632bfca8d48a7115a32cc7c9b80c19dc48
platform: linux/arm
paths:
- /usr/share/grafana/bin/grafana
- name: grafana
version: 12.0.0-security-01
images:
- ref: grafana/grafana:12.0.0-security-01@sha256:6fc2fee54f8551a3ae452a2dd14fd524143026e46a0f06b0302c79117fe89fcf
platform: linux/amd64
paths:
- /usr/share/grafana/bin/grafana
- name: grafana
version: 11.0.5-security-01
images:
- ref: grafana/grafana:11.0.5-security-01@sha256:0ba037897c6f22f26b7d289eb1f651ed4d9d552ea454ace60307ecd0585cbee1
platform: linux/amd64
paths:
- /usr/share/grafana/bin/grafana
- name: grafana
version: 12.0.0
images:
@@ -1546,8 +1594,16 @@ from-images:
- version: 1.9.6
images:
- ref: registry.k8s.io/ingress-nginx/controller:v1.9.6@sha256:1405cc613bd95b2c6edd8b2a152510ae91c7e62aea4698500d23b2145960ab9c
# note: per-platform manifest digests (not the index digest) -- the extraction step creates a
# container from the ref alone, so each platform needs its own uniquely addressable ref
- ref: registry.k8s.io/ingress-nginx/controller:v1.9.6@sha256:0939639a1f338a9eaaa490fd38b4a7881e47a7fd1a473baf8749ce15952b55b8
platform: linux/amd64
- ref: registry.k8s.io/ingress-nginx/controller:v1.9.6@sha256:5fb823cc617cbfbee4b250ad5e059c23078ea63e9a7e6acb6d4d30e4456eb0be
platform: linux/arm64
- ref: registry.k8s.io/ingress-nginx/controller:v1.9.6@sha256:dcea8d021e15cf27f59249ee0cfd211559ef9eb60b10651c3af299499848cf8b
platform: linux/arm
- ref: registry.k8s.io/ingress-nginx/controller:v1.9.6@sha256:fc53a604eb2d17fa0cf7a3f266d052c2f32fb97209869057d6826d1815cf622d
platform: linux/s390x
paths:
- /nginx-ingress-controller
@@ -285,6 +285,7 @@ func TestDpkgArchiveCataloger(t *testing.T) {
pkgtest.NewCatalogTester().
WithImageResolver(t, tt.name).
IgnoreLocationLayer(). // this fixture can be rebuilt, thus the layer ID will change
WithErrorAssertion(require.NoError).
Expects(tt.expected, nil).
TestCataloger(t, c)
})
+10 -2
View File
@@ -202,7 +202,11 @@ func getAdditionalFileListing(resolver file.Resolver, dbLocation file.Location,
if md5Reader != nil && md5Location != nil {
defer internal.CloseAndLogError(md5Reader, md5Location.AccessPath)
// attach the file list
files = append(files, parseDpkgMD5Info(md5Reader)...)
records, err := parseDpkgMD5Info(md5Reader)
if err != nil {
log.Debugf("dpkg md5sums file %q: %v", md5Location.RealPath, err)
}
files = append(files, records...)
// keep a record of the file where this was discovered
locations = append(locations, *md5Location)
@@ -213,7 +217,11 @@ func getAdditionalFileListing(resolver file.Resolver, dbLocation file.Location,
if conffilesReader != nil && conffilesLocation != nil {
defer internal.CloseAndLogError(conffilesReader, conffilesLocation.AccessPath)
// attach the file list
files = append(files, parseDpkgConffileInfo(conffilesReader)...)
records, err := parseDpkgConffileInfo(conffilesReader)
if err != nil {
log.Debugf("dpkg conffiles file %q: %v", conffilesLocation.RealPath, err)
}
files = append(files, records...)
// keep a record of the file where this was discovered
locations = append(locations, *conffilesLocation)
+113 -14
View File
@@ -3,8 +3,10 @@ package debian
import (
"archive/tar"
"context"
"errors"
"fmt"
"io"
"math"
"path/filepath"
"regexp"
"strings"
@@ -13,6 +15,7 @@ import (
"github.com/mholt/archives"
"github.com/anchore/syft/internal"
intFile "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
@@ -33,6 +36,7 @@ func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen
var metadata *pkg.DpkgArchiveEntry
var licenses []string
var unknownErr error
var sawControl, sawData bool
for {
header, err := arReader.Next()
if err == io.EOF {
@@ -43,19 +47,28 @@ func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen
}
switch {
case strings.HasPrefix(header.Name, "control.tar"):
// a .deb has exactly one control.tar.* and one data.tar.* member; anything past the first of
// each is attacker-supplied filler that would otherwise buy a fresh decompression budget
case !sawControl && strings.HasPrefix(header.Name, "control.tar"):
sawControl = true
// Decompress the control.tar.* file
dcReader, err := decompressionStream(ctx, arReader, header.Name)
dcReader, err := decompressionStream(ctx, arReader, header.Name, maxControlTarSize)
if err != nil {
return nil, nil, unknown.New(reader.Location, fmt.Errorf("failed to decompress control.tar.* file: %w", err))
}
metadata, err = processControlTar(dcReader)
if err != nil {
switch {
case err != nil && metadata == nil:
return nil, nil, unknown.New(reader.Location, fmt.Errorf("failed to process control.tar.* file: %w", err))
case err != nil:
// metadata came back usable but incomplete (a clipped file listing); keep the package and
// record why it is partial
unknownErr = unknown.Append(unknownErr, reader.Location, fmt.Errorf("partial control.tar.* file: %w", err))
}
case strings.HasPrefix(header.Name, "data.tar"):
case !sawData && strings.HasPrefix(header.Name, "data.tar"):
sawData = true
// Decompress the data.tar.* file
dcReader, err := decompressionStream(ctx, arReader, header.Name)
dcReader, err := decompressionStream(ctx, arReader, header.Name, maxDataTarSize)
if err != nil {
return nil, nil, unknown.New(reader.Location, fmt.Errorf("failed to decompress data.tar.* file: %w", err))
}
@@ -64,15 +77,21 @@ func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen
unknownErr = unknown.Append(unknownErr, reader.Location, fmt.Errorf("failed to process data.tar.* file: %w", err))
}
}
if sawControl && sawData {
break
}
}
if metadata == nil {
return nil, nil, unknown.New(reader.Location, fmt.Errorf("no application found described in .dpkg archive"))
}
// a partial parse still yields a usable package, so report what went wrong alongside it rather than
// dropping either one
return []pkg.Package{
newDebArchivePackage(ctx, reader.Location, *metadata, licenses),
}, nil, nil
}, nil, unknownErr
}
// this is the pattern you'd expect to see in a tar header for a debian package license file ()
@@ -81,6 +100,7 @@ var archiveHeaderLicensePathPattern = regexp.MustCompile(`^\.?/usr/share/doc/[^/
func processDataTar(dcReader io.ReadCloser) ([]string, error) {
defer internal.CloseAndLogError(dcReader, "")
var licenses []string
var copyrightFiles int
tarReader := tar.NewReader(dcReader)
for {
@@ -96,6 +116,10 @@ func processDataTar(dcReader io.ReadCloser) ([]string, error) {
// TODO: in the future we can add archive sub indexes to the locations to see where within
// the dpkg archive the license was found
if archiveHeaderLicensePathPattern.MatchString(header.Name) {
copyrightFiles++
if copyrightFiles > maxCopyrightFiles {
return licenses, fmt.Errorf("copyright file count exceeds %d; remaining files not read", maxCopyrightFiles)
}
licenses = append(licenses, parseLicensesFromCopyright(tarReader)...)
}
}
@@ -103,6 +127,10 @@ func processDataTar(dcReader io.ReadCloser) ([]string, error) {
return licenses, nil
}
// processControlTar always returns whatever metadata it managed to parse, even alongside a non-nil
// error. A non-nil error with non-nil metadata means the package is usable but incomplete (e.g. a
// clipped file listing); nil metadata means nothing usable was found and the caller should treat it
// as fatal.
func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
defer internal.CloseAndLogError(dcReader, "")
@@ -111,6 +139,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
var metadata *pkg.DpkgArchiveEntry
var files []pkg.DpkgFileRecord
var confFileRecords []pkg.DpkgFileRecord
var listingErr error
for {
header, err := tarReader.Next()
@@ -118,7 +147,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
break
}
if err != nil {
return nil, fmt.Errorf("failed to read control tar: %w", err)
return metadata, fmt.Errorf("failed to read control tar: %w", err)
}
switch filepath.Base(header.Name) {
@@ -134,11 +163,15 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
entry := pkg.DpkgArchiveEntry(entries[0].toDpkgEntry())
metadata = &entry
case "md5sums":
// parseDpkgMD5Info already streams via bufio.Scanner
files = parseDpkgMD5Info(tarReader)
// parseDpkgMD5Info streams via bufio.Scanner and reports its own clipping/scan errors
var err error
files, err = parseDpkgMD5Info(tarReader)
listingErr = errors.Join(listingErr, err)
case "conffiles":
// parseDpkgConffileInfo already streams via bufio.Scanner
confFileRecords = parseDpkgConffileInfo(tarReader)
// parseDpkgConffileInfo streams via bufio.Scanner and reports its own clipping/scan errors
var err error
confFileRecords, err = parseDpkgConffileInfo(tarReader)
listingErr = errors.Join(listingErr, err)
}
}
@@ -159,10 +192,36 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
}
metadata.Files = files
return metadata, nil
// a clipped or unreadable file listing leaves the package usable, so hand both back and let the
// caller decide
return metadata, listingErr
}
func decompressionStream(ctx context.Context, r io.Reader, filePath string) (io.ReadCloser, error) {
// a .deb's member sizes are bounded by the archive, but what they decompress to is not: xz and zstd
// both exceed gzip's ~1032:1, so a few KB member can expand without limit. The two members get
// different budgets because they hold different things.
const (
// control.tar holds only metadata (control, md5sums, conffiles). Even a package shipping a hundred
// thousand files lands around 12MB of md5sums text.
maxControlTarSize int64 = 16 * intFile.MB
// data.tar is the package payload, which is legitimately large (texlive and cuda ship hundreds of
// MB), so this is only here to stop the tar walk from spinning forever on a bomb. It mirrors the
// perFileReadLimit already used for archive extraction in internal/file/copy.go.
maxDataTarSize int64 = 2 * intFile.GB
// a real .deb ships exactly one copyright file per package.
maxCopyrightFiles = 1000
)
// sentinels so callers (and tests) can match with errors.Is instead of substring matching.
var (
errDecompressedTooLarge = errors.New("decompressed stream is larger than the max allowed size")
errClippedFileListing = errors.New("file listing exceeds the max allowed entries")
)
func decompressionStream(ctx context.Context, r io.Reader, filePath string, maxDecompressed int64) (io.ReadCloser, error) {
format, stream, err := archives.Identify(ctx, filePath, r)
if err != nil {
return nil, fmt.Errorf("failed to identify compression format: %w", err)
@@ -178,5 +237,45 @@ func decompressionStream(ctx context.Context, r io.Reader, filePath string) (io.
return nil, fmt.Errorf("failed to create decompression reader: %w", err)
}
return rc, nil
return newBoundedReadCloser(rc, maxDecompressed), nil
}
// boundedReadCloser fails the read past a byte budget rather than truncating.
//
// io.LimitedReader is the obvious thing to reach for here and is wrong for this: past its limit it
// just stops returning bytes, and both consumers of this are tar readers, which read a truncated
// stream as a clean end of archive. Together that emits a package silently missing files, which is
// the exact failure the bound exists to catch. The signal is recoverable by checking
// LimitedReader.N after the walk finishes (the same shape as safeCopy in internal/file/copy.go,
// which counts bytes off io.Copy), but nothing obliges a caller to write that check and a missed
// one is silent all over again. Erroring at the Read is the version that cannot be forgotten.
//
// net/http's MaxBytesReader has precisely these semantics and is stdlib, but reaching into the HTTP
// package to parse a .deb leaves the next reader wondering what it is doing here.
type boundedReadCloser struct {
io.ReadCloser
remaining int64
limit int64
}
func newBoundedReadCloser(rc io.ReadCloser, limit int64) *boundedReadCloser {
// budget is one byte past the cap so a stream of exactly limit bytes still reaches EOF normally,
// and only the byte after the cap trips the error. Clamp so a limit near math.MaxInt64 can't wrap
// remaining negative and fail every read immediately.
if limit < 0 || limit > math.MaxInt64-1 {
limit = math.MaxInt64 - 1
}
return &boundedReadCloser{ReadCloser: rc, remaining: limit + 1, limit: limit}
}
func (b *boundedReadCloser) Read(p []byte) (int, error) {
if b.remaining <= 0 {
return 0, fmt.Errorf("%w (%d bytes)", errDecompressedTooLarge, b.limit)
}
if int64(len(p)) > b.remaining {
p = p[:b.remaining]
}
n, err := b.ReadCloser.Read(p)
b.remaining -= int64(n)
return n, err
}
@@ -3,11 +3,22 @@ package debian
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"errors"
"fmt"
"io"
"strings"
"testing"
"testing/iotest"
"github.com/blakesmith/ar"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
func TestProcessControlTar(t *testing.T) {
@@ -85,3 +96,273 @@ func writeTarEntry(t *testing.T, tw *tar.Writer, name, content string) {
_, err := tw.Write([]byte(content))
require.NoError(t, err)
}
func Test_boundedReadCloser(t *testing.T) {
payload := []byte("0123456789")
t.Run("a stream at exactly the cap reads through whole", func(t *testing.T) {
// the positive assertion is the point: it proves the one-byte-past budget does not truncate
// at-cap content, which a bare "did not error" check would not catch
r := newBoundedReadCloser(io.NopCloser(bytes.NewReader(payload)), int64(len(payload)))
got, err := io.ReadAll(r)
require.NoError(t, err)
assert.Equal(t, payload, got)
})
t.Run("a stream past the cap errors rather than truncating", func(t *testing.T) {
r := newBoundedReadCloser(io.NopCloser(bytes.NewReader(payload)), int64(len(payload)-1))
_, err := io.ReadAll(r)
require.ErrorIs(t, err, errDecompressedTooLarge)
})
t.Run("the underlying read error is not masked", func(t *testing.T) {
r := newBoundedReadCloser(io.NopCloser(iotest.ErrReader(errBoom)), 1024)
_, err := io.ReadAll(r)
require.ErrorIs(t, err, errBoom)
})
}
var errBoom = errors.New("boom")
func Test_decompressionStream_boundsDecompressedSize(t *testing.T) {
// a highly compressible payload standing in for a decompression bomb: tiny compressed, large out
var raw bytes.Buffer
raw.Write(make([]byte, 512*1024))
var gz bytes.Buffer
gw := gzip.NewWriter(&gz)
_, err := gw.Write(raw.Bytes())
require.NoError(t, err)
require.NoError(t, gw.Close())
require.Less(t, gz.Len(), 4096, "compressed payload should be tiny relative to what it expands to")
rc, err := decompressionStream(context.Background(), bytes.NewReader(gz.Bytes()), "control.tar.gz", 64*1024)
require.NoError(t, err, "the bound is enforced on read, not on open")
t.Cleanup(func() { _ = rc.Close() })
_, err = io.ReadAll(rc)
require.ErrorIs(t, err, errDecompressedTooLarge)
}
func Test_processControlTar_rejectsABombedControlMember(t *testing.T) {
// end to end through the real consumer: a tar reader treats a truncated stream as a clean end of
// archive, so this asserts the bomb surfaces as an error instead of a package missing its files.
// "control" parses fully before the md5sums stream dies, so this also covers the Fix C contract: a
// control tar read failure must not drop the package that was already parsed.
var tarBuf bytes.Buffer
tw := tar.NewWriter(&tarBuf)
writeTarEntry(t, tw, "control", "Package: test-package\nVersion: 1.0.0\n")
writeTarEntry(t, tw, "md5sums", strings.Repeat("d41d8cd98f00b204e9800998ecf8427e usr/bin/x\n", 20000))
require.NoError(t, tw.Close())
metadata, err := processControlTar(newBoundedReadCloser(io.NopCloser(bytes.NewReader(tarBuf.Bytes())), 4096))
require.ErrorIs(t, err, errDecompressedTooLarge)
require.NotNil(t, metadata, "a control tar read failure after \"control\" was parsed must not drop the package")
assert.Equal(t, "test-package", metadata.Package)
}
func Test_processControlTar_clippedListingIsUsableButReported(t *testing.T) {
// the record cap must not drop the package: metadata comes back usable AND an error explains why the
// file list is short, so the caller can record it as an unknown instead of discarding the package
var tarBuf bytes.Buffer
tw := tar.NewWriter(&tarBuf)
writeTarEntry(t, tw, "control", "Package: test-package\nVersion: 1.0.0\n")
writeTarEntry(t, tw, "md5sums", strings.Repeat("d41d8cd98f00b204e9800998ecf8427e usr/bin/x\n", maxDpkgFileRecords+10))
require.NoError(t, tw.Close())
metadata, err := processControlTar(io.NopCloser(bytes.NewReader(tarBuf.Bytes())))
require.Error(t, err, "clipping must be reported")
require.ErrorIs(t, err, errClippedFileListing)
require.NotNil(t, metadata, "the package must survive a clipped file listing")
assert.Equal(t, "test-package", metadata.Package)
assert.Len(t, metadata.Files, maxDpkgFileRecords)
}
// buildDeb assembles a minimal .deb (an ar archive of debian-binary + control.tar.gz + data.tar.gz).
func buildDeb(t *testing.T, controlTar, dataTar []byte) []byte {
t.Helper()
gzipped := func(b []byte) []byte {
var out bytes.Buffer
w := gzip.NewWriter(&out)
_, err := w.Write(b)
require.NoError(t, err)
require.NoError(t, w.Close())
return out.Bytes()
}
var buf bytes.Buffer
aw := ar.NewWriter(&buf)
require.NoError(t, aw.WriteGlobalHeader())
for _, m := range []struct {
name string
body []byte
}{
{"debian-binary", []byte("2.0\n")},
{"control.tar.gz", gzipped(controlTar)},
{"data.tar.gz", gzipped(dataTar)},
} {
require.NoError(t, aw.WriteHeader(&ar.Header{Name: m.name, Size: int64(len(m.body))}))
_, err := aw.Write(m.body)
require.NoError(t, err)
}
return buf.Bytes()
}
func Test_parseDebArchive_surfacesPartialParseAsUnknown(t *testing.T) {
// guards a silent failure: unknownErr was accumulated and then dropped on return, so a data.tar that
// failed to parse produced a clean-looking SBOM with no record that anything went wrong. The property
// that actually matters is that the returned error is a *unknown.CoordinateError: that's what decides
// between "recorded as an unknown" (this case) and "aborts the entire scan" (anything else), per
// internal/task/executor.go and syft/create_sbom.go. A plain substring match on "data.tar" would pass
// just as well if unknown.Append were swapped for errors.Join, which is the regression that matters.
var controlTar bytes.Buffer
ctw := tar.NewWriter(&controlTar)
writeTarEntry(t, ctw, "control", "Package: test-package\nVersion: 1.0.0\nArchitecture: all\n")
require.NoError(t, ctw.Close())
// a data.tar that is not a valid tar at all, so processDataTar errors
dataTar := []byte(strings.Repeat("not a tar file at all", 100))
debBytes := buildDeb(t, controlTar.Bytes(), dataTar)
loc := file.NewVirtualLocation("/test.deb", "/test.deb")
pkgs, _, err := parseDebArchive(context.Background(), nil, &generic.Environment{},
file.LocationReadCloser{
Location: loc,
ReadCloser: io.NopCloser(bytes.NewReader(debBytes)),
})
require.Len(t, pkgs, 1, "the package must still be emitted")
assert.Equal(t, "test-package", pkgs[0].Name)
require.Error(t, err, "a failed data.tar parse must not be swallowed")
coordinateErrors, remaining := unknown.ExtractCoordinateErrors(err)
require.NoError(t, remaining, "no non-coordinate errors should remain in the chain")
require.Len(t, coordinateErrors, 1)
assert.Equal(t, loc.Coordinates, coordinateErrors[0].Coordinates)
assert.Contains(t, coordinateErrors[0].Reason.Error(), "data.tar")
}
func Test_parseDebArchive_controlTarBombRejectedByRealSizeBound(t *testing.T) {
// end to end through the real entrypoint using the real maxControlTarSize (16MB), not a swapped-in
// test value: nothing else in this suite exercises that constant at its actual call site. The bomb
// entry is written before "control", so "control" is never reached and processControlTar returns nil
// metadata alongside the error -- this exercises parseDebArchive's fatal branch (metadata == nil).
var controlTar bytes.Buffer
ctw := tar.NewWriter(&controlTar)
writeTarEntry(t, ctw, "bomb", strings.Repeat("\x00", 20*1024*1024))
writeTarEntry(t, ctw, "control", "Package: test-package\nVersion: 1.0.0\n")
require.NoError(t, ctw.Close())
debBytes := buildDeb(t, controlTar.Bytes(), nil)
require.Less(t, len(debBytes), 64*1024, "compressed .deb should stay tiny relative to what it expands to")
loc := file.NewVirtualLocation("/bomb.deb", "/bomb.deb")
pkgs, _, err := parseDebArchive(context.Background(), nil, &generic.Environment{},
file.LocationReadCloser{
Location: loc,
ReadCloser: io.NopCloser(bytes.NewReader(debBytes)),
})
require.Empty(t, pkgs, "no package should be emitted when the control tar never parses")
require.Error(t, err)
coordinateErrors, remaining := unknown.ExtractCoordinateErrors(err)
require.NoError(t, remaining, "no non-coordinate errors should remain in the chain")
require.Len(t, coordinateErrors, 1)
require.ErrorIs(t, coordinateErrors[0].Reason, errDecompressedTooLarge)
}
// arMember is one named, already-encoded ar entry body, for tests that need control over the raw
// member list (order, duplicates) that buildDeb's fixed three-member shape does not allow.
type arMember struct {
name string
body []byte
}
// buildDebWithMembers is buildDeb generalized to an arbitrary member list.
func buildDebWithMembers(t *testing.T, members []arMember) []byte {
t.Helper()
var buf bytes.Buffer
aw := ar.NewWriter(&buf)
require.NoError(t, aw.WriteGlobalHeader())
for _, m := range members {
require.NoError(t, aw.WriteHeader(&ar.Header{Name: m.name, Size: int64(len(m.body))}))
_, err := aw.Write(m.body)
require.NoError(t, err)
}
return buf.Bytes()
}
func Test_parseDebArchive_ignoresDuplicateArMembers(t *testing.T) {
// a .deb has exactly one control.tar.* and one data.tar.*; a second data.tar.* member is
// attacker-supplied filler that would otherwise buy a fresh decompression budget, and here would also
// fail to parse as a tar at all -- so this proves the walk stops after the first of each kind rather
// than merely surviving a well-formed duplicate.
gzipped := func(b []byte) []byte {
var out bytes.Buffer
w := gzip.NewWriter(&out)
_, err := w.Write(b)
require.NoError(t, err)
require.NoError(t, w.Close())
return out.Bytes()
}
var controlTar bytes.Buffer
ctw := tar.NewWriter(&controlTar)
writeTarEntry(t, ctw, "control", "Package: test-package\nVersion: 1.0.0\n")
require.NoError(t, ctw.Close())
var firstDataTar bytes.Buffer
dtw := tar.NewWriter(&firstDataTar)
require.NoError(t, dtw.Close())
debBytes := buildDebWithMembers(t, []arMember{
{"debian-binary", []byte("2.0\n")},
// the duplicate sits before control.tar.gz on purpose: the loop cannot have stopped yet at that
// point, so only the per-member guard can keep it from being read
{"data.tar.gz", gzipped(firstDataTar.Bytes())},
{"data.tar.gz", []byte("not a tar file at all")}, // not even gzipped; would error if ever read
{"control.tar.gz", gzipped(controlTar.Bytes())},
})
pkgs, _, err := parseDebArchive(context.Background(), nil, &generic.Environment{},
file.LocationReadCloser{
Location: file.NewVirtualLocation("/test.deb", "/test.deb"),
ReadCloser: io.NopCloser(bytes.NewReader(debBytes)),
})
require.NoError(t, err, "the second data.tar.* member must never be reached")
require.Len(t, pkgs, 1)
}
func Test_processDataTar_capsCopyrightFiles(t *testing.T) {
// a real .deb ships exactly one copyright file per package; this asserts the walk stops and errors
// rather than accumulating an unbounded number of license reads
var tarBuf bytes.Buffer
tw := tar.NewWriter(&tarBuf)
for i := 0; i < maxCopyrightFiles+10; i++ {
writeTarEntry(t, tw, fmt.Sprintf("/usr/share/doc/pkg%d/copyright", i), "License: MIT\n")
}
require.NoError(t, tw.Close())
_, err := processDataTar(io.NopCloser(bytes.NewReader(tarBuf.Bytes())))
require.Error(t, err)
}
+6 -1
View File
@@ -141,7 +141,12 @@ func (d *dpkgExtractedMetadata) toDpkgEntry() pkg.DpkgDBEntry {
// there may be an optional conffiles section that we should persist as files
if d.Conffiles != "" {
entry.Files = parseDpkgConffileInfo(strings.NewReader(d.Conffiles))
var err error
entry.Files, err = parseDpkgConffileInfo(strings.NewReader(d.Conffiles))
if err != nil {
// toDpkgEntry has no error return, a log line is all that's available here
log.Debugf("dpkg status conffiles field for package %q: %v", d.Package, err)
}
}
if entry.Files == nil {
@@ -2,6 +2,7 @@ package debian
import (
"bufio"
"fmt"
"io"
"strings"
@@ -9,10 +10,27 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func parseDpkgMD5Info(reader io.Reader) (findings []pkg.DpkgFileRecord) {
// maxDpkgFileRecords bounds the file lists parsed out of md5sums and conffiles. A byte cap on the
// enclosing stream is not enough on its own: the shortest line that still yields a record is 4 bytes,
// so 16MB of control.tar buys millions of records.
//
// The number comes from measuring what a record actually retains, not from the struct size. A
// DpkgFileRecord is 32 bytes, but a populated one also heap-allocates a file.Digest and three string
// backing arrays: 121 bytes each with realistic Debian paths, 70 with minimal ones. At this bound that
// is ~24MB per package, and the effective ceiling is that times cataloger parallelism (NumCPU*4).
// The largest real Debian packages ship on the order of 10^4 files, so this leaves ~5x headroom.
const maxDpkgFileRecords = 200_000
// parseDpkgMD5Info returns the records it managed to parse, plus a non-nil error when the record bound
// was hit (wrapping errClippedFileListing) or the scanner failed partway through.
func parseDpkgMD5Info(reader io.Reader) ([]pkg.DpkgFileRecord, error) {
var findings []pkg.DpkgFileRecord
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
if len(findings) >= maxDpkgFileRecords {
return findings, fmt.Errorf("%w: dpkg md5sums listing exceeds %d entries", errClippedFileListing, maxDpkgFileRecords)
}
line := scanner.Text()
fields := strings.SplitN(line, " ", 2)
if len(fields) == 2 {
@@ -29,13 +47,22 @@ func parseDpkgMD5Info(reader io.Reader) (findings []pkg.DpkgFileRecord) {
})
}
}
return findings
if err := scanner.Err(); err != nil {
return findings, fmt.Errorf("failed to scan dpkg md5sums: %w", err)
}
return findings, nil
}
func parseDpkgConffileInfo(reader io.Reader) (findings []pkg.DpkgFileRecord) {
// parseDpkgConffileInfo returns the records it managed to parse, plus a non-nil error when the record
// bound was hit (wrapping errClippedFileListing) or the scanner failed partway through.
func parseDpkgConffileInfo(reader io.Reader) ([]pkg.DpkgFileRecord, error) {
var findings []pkg.DpkgFileRecord
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
if len(findings) >= maxDpkgFileRecords {
return findings, fmt.Errorf("%w: dpkg conffiles listing exceeds %d entries", errClippedFileListing, maxDpkgFileRecords)
}
line := strings.Trim(scanner.Text(), " \n")
fields := strings.SplitN(line, " ", 2)
@@ -70,5 +97,8 @@ func parseDpkgConffileInfo(reader io.Reader) (findings []pkg.DpkgFileRecord) {
findings = append(findings, record)
}
}
return findings
if err := scanner.Err(); err != nil {
return findings, fmt.Errorf("failed to scan dpkg conffiles: %w", err)
}
return findings, nil
}
@@ -1,10 +1,13 @@
package debian
import (
"bufio"
"os"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file"
@@ -45,7 +48,8 @@ func TestMD5SumInfoParsing(t *testing.T) {
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, f.Close()) })
actual := parseDpkgMD5Info(f)
actual, err := parseDpkgMD5Info(f)
require.NoError(t, err)
if diff := cmp.Diff(test.expected, actual); diff != "" {
t.Errorf("unexpected md5 files (-want +got):\n%s", diff)
@@ -79,7 +83,8 @@ func TestConffileInfoParsing(t *testing.T) {
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, f.Close()) })
actual := parseDpkgConffileInfo(f)
actual, err := parseDpkgConffileInfo(f)
require.NoError(t, err)
if diff := cmp.Diff(test.expected, actual); diff != "" {
t.Errorf("unexpected md5 files (-want +got):\n%s", diff)
@@ -88,3 +93,49 @@ func TestConffileInfoParsing(t *testing.T) {
})
}
}
func Test_parseDpkgInfoFiles_boundRecordCount(t *testing.T) {
// a byte cap on the enclosing stream cannot bound these on its own, since the shortest line that
// still yields a record is a few bytes. exercises the real maxDpkgFileRecords const directly, since
// the limit is no longer injectable.
t.Run("md5sums stops at the record bound", func(t *testing.T) {
buf := strings.Repeat("d41d8cd98f00b204e9800998ecf8427e usr/bin/x\n", maxDpkgFileRecords+10)
got, err := parseDpkgMD5Info(strings.NewReader(buf))
assert.Len(t, got, maxDpkgFileRecords)
require.ErrorIs(t, err, errClippedFileListing)
})
t.Run("conffiles stops at the record bound", func(t *testing.T) {
buf := strings.Repeat("/etc/x.conf\n", maxDpkgFileRecords+10)
got, err := parseDpkgConffileInfo(strings.NewReader(buf))
assert.Len(t, got, maxDpkgFileRecords)
require.ErrorIs(t, err, errClippedFileListing)
})
t.Run("a listing under the bound is returned whole", func(t *testing.T) {
// guards against the bound clipping legitimate input
got, err := parseDpkgMD5Info(strings.NewReader(
"d41d8cd98f00b204e9800998ecf8427e usr/bin/a\n" +
"d41d8cd98f00b204e9800998ecf8427e usr/bin/b\n"))
require.NoError(t, err)
require.Len(t, got, 2)
assert.Equal(t, "/usr/bin/a", got[0].Path)
assert.Equal(t, "/usr/bin/b", got[1].Path)
})
}
func Test_parseDpkgMD5Info_scannerError(t *testing.T) {
// a single line longer than bufio.MaxScanTokenSize with no trailing newline trips the scanner's
// internal error rather than yielding a clean (and silently incomplete) empty list
line := strings.Repeat("a", bufio.MaxScanTokenSize+1)
got, err := parseDpkgMD5Info(strings.NewReader(line))
assert.Empty(t, got)
require.Error(t, err)
}
@@ -2,7 +2,8 @@ FROM ubuntu:22.04 AS downloader
RUN apt-get update && apt-get install -y curl
RUN curl -o zlib1g.deb http://archive.ubuntu.com/ubuntu/pool/main/z/zlib/zlib1g_1.3.dfsg-3.1ubuntu2.1_amd64.deb
# launchpad's +files URLs are immutable, unlike pool/main which only keeps the current version
RUN curl -fsSL -o zlib1g.deb https://launchpad.net/ubuntu/+archive/primary/+files/zlib1g_1.3.dfsg-3.1ubuntu2.1_amd64.deb
FROM scratch

Some files were not shown because too many files have changed in this diff Show More