Commit Graph
25 Commits
Author SHA1 Message Date
Keith Zantow 3e4b036e22 fix: keep RealPath as top-level, use FullPath and utility functions FullNestedPath, LocalPath, Path
Signed-off-by: Keith Zantow <kzantow@gmail.com>
2026-10-19 15:44:37 -04:00
Keith Zantow 17d109e8b5 chore: remove custom index in favor of existing filetree.Index
Signed-off-by: Keith Zantow <kzantow@gmail.com>
2026-10-13 16:22:32 -04:00
Keith Zantow d95abbf6b5 chore: truncation reason to error type
Signed-off-by: Keith Zantow <keith.zantow@anchore.com>
2026-09-30 15:52:45 -04:00
Alex Goodman b735b615f0 fix: name nested files in SPDX by their archive chain
SPDX wrote `FileName` from `RealPath` only, so two archives' `META-INF/MANIFEST.MF` became two SPDX files with the same name and nothing saying which archive each came from. Worse, a consumer reading just the name would take a nested file for one at that path in the scan root.

- a file inside an archive is now named by its full chain, using the same `:` convention and escaping as `ArchivePath` / java's `VirtualPath`: `app.war:WEB-INF/lib/a.jar:META-INF/MANIFEST.MF`
- `FileComment` gains an `archivePath: ...` line (next to the existing `layerID: ...`, one per line), so decoding recovers `ArchivePath` and the entry's own `RealPath` without guessing where the chain ends
- files outside archives are unchanged

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-24 13:40:18 -04:00
Alex Goodman 22edfe66ca fix: link nested java packages to their enclosing jar when nested cataloging is on
the legacy recursion links a nested jar to the jar that holds it: a `DEPENDENCY_OF` edge from inner to outer, and `JavaArchive.Parent` on the inner package. Inside an extracted archive java called `parse(ctx, nil)`, so both went away as soon as the feature was on. What replaced them was `file CONTAINS package`, which SPDX and CycloneDX drop, so those formats lost all war-to-jar structure.

- `archive.Traversal` gains `Parent` and `Packages` (the packages found describing that archive, filled in by the task after its sub-pipeline runs)
- java picks the enclosing jar's main package from the parent traversal (the one whose virtual path is the archive's own; pom-derived aux packages carry a suffixed one) and passes it to `parse`, then sets `Parent` the same way the legacy path does

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-24 13:36:52 -04:00
Alex Goodman 9d820c6d94 fix: only look up owned-by-package files in the resolver's own frame
with `FilesOwnedByPackageSelection` the file catalogers take every package location in the SBOM and look it up by `RealPath` in the resolver they were given. Inside an archive, the java package's location is the archive itself *in its parent's frame* (`WEB-INF/lib/dep.jar`), so the sub-pipeline looked that path up inside `dep.jar`'s own resolver: a miss, or an unrelated same-named entry by coincidence. At the root, coordinates from inside archives were looked up against the scan root the same way.

`coordinatesForSelection` now keeps only coordinates whose `ArchivePath` matches the current frame (the traversal's `ContentsArchivePath()`, or blank at the root).

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-24 13:30:33 -04:00
Alex Goodman 0152e5b59c fix: a damaged entry truncates the archive instead of failing it
a read error while copying one entry (a zip CRC mismatch, a corrupt deflate stream) was returned from `add`. That aborted the walk, and `Extract` threw away every entry already stored. So a large war with one damaged, irrelevant entry produced zero packages with the feature on, while java's legacy path (which reads only the entries it needs) still cataloged it.

- a read error on one entry now drops that entry and marks the resolver truncated (`entry "x" could not be read: ...`), and the walk continues
- if the stream itself breaks partway (a truncated `.tar.gz`), whatever was read before the break is kept and the archive is truncated. With nothing read it is still an error, as before
- disk limit and decompression budget errors still stop the walk

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-24 13:28:53 -04:00
Alex Goodman 81ee189bf4 fix: bound total decompression under each top-level archive
the limiter bounds what's held *at once*, and each archive gives its bytes back when it's released. So a recursive bomb (42.zip shape) never trips a limit: every level fits on its own, and the scan just runs for effectively forever. Capping each archive at N× its own size wouldn't help either, since every level of such a bomb is high-ratio, so the work still grows about N-fold per level.

- each top-level archive gets one decompression budget of `max(100 × its size, 1 GiB)`, spent by every archive nested under it at any depth
- the budget is attached to the context when the walk enters a top-level archive, so `Extract` picks it up without a signature change
- once it's spent, the archive being extracted is truncated with a "possible decompression bomb" unknown and the walk moves on. The next top-level archive starts fresh

real trees (a war of jars, a layer of tarballs) expand a few times over, well under the ratio.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-24 13:23:32 -04:00
Alex Goodman d27b6ebb1b fix: cap each extracted archive entry at 2 GB
`put` had no per-entry cap, unlike the legacy java path which capped every file at 2 GB through `intFile.SafeCopy`. At depth 1, one entry of an overlapping-entry zip bomb could decompress all the way to the 100 GB disk limit before anything stopped it.

- entries are now read through the same 2 GB cap (`intFile.PerFileReadLimit`, now exported). An entry over it is dropped after cap+1 bytes and the rest of the archive is still extracted
- `Resolver` gains `TruncatedReason`, so the truncation unknown says what happened ("an entry larger than 2.0 GiB was skipped", or the existing disk-limit text) instead of always blaming the limits

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-24 13:21:08 -04:00
Alex Goodman 73397f1e8b fix: only extract zip and tar family archives
`identifyFormat` accepted any extractor mholt identified, which put the 7z and rar decoders (`bodgit/sevenzip`, `rardecode`) on untrusted input with no test coverage. Only zip, tar and compressed tar are opened now, until other formats are deliberately supported.

This also fixes java resource adapters: a `.rar` that is really a zip used to be identified by name as RAR and fail to extract. An unsupported identification now falls through to the zip end-of-central-directory check, so it's read as the zip it is.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-24 11:29:34 -04:00
Alex Goodman c9b4f9c085 fix: recover from panics while extracting an archive
sub-pipeline catalogers were already run under `runTaskSafely`, but extraction wasn't: the zip, tar and compressor decoders run on untrusted bytes, and a panic in one unwound the whole walk and failed `CreateSBOM` with "failed to run tasks".

- `processArchive` recovers and returns the panic (with its stack) as an error at the archive's coordinates, so it becomes an unknown and the sibling archives are still cataloged
- `Extract` now defers `Cleanup` on failure instead of calling it on each error path, so a panic no longer leaks the limiter charge or the spill file

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-24 11:28:26 -04:00
Alex Goodman f5bac4a026 fix: build ArchivePath from real paths, not access paths
each `ArchivePath` segment came from `loc.Path()`, so it was the link path when an archive was reached through a symlink and the real path otherwise. That depended on how discovery happened to find the archive. The package location said `store/3f9a1c` while the files inside the same jar were recorded under `...:app/lib/json-simple-1.1.1.jar`, so a consumer couldn't join a package to its files by prefix.

The chain for extracted files is now built from `RealPath`. `Traversal.Location` still carries the access path, so java's name check and `VirtualPath` metadata behave the same as before. The field comment and schema now say every segment has links resolved.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-24 11:26:38 -04:00
Alex Goodman eec86cf88d fix: escape % in ArchivePath segments
`:` was escaped as `%3A`, but `%` itself wasn't, so an entry named `a:b.jar` and one named `a%3Ab.jar` got the same `ArchivePath`. Their contents then collide in `FileMetadata` / `FileDigests` (last writer wins) and share artifact IDs.

`%` is now escaped as `%25` before `:`, which makes the encoding injective. The encoding is documented on `Coordinates.ArchivePath` and in the schema.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-24 11:23:54 -04:00
Keith Zantow b36234cfe7 feat: simplify+improve resolver implementation / glob search
Signed-off-by: Keith Zantow <keith.zantow@anchore.com>
2026-09-22 11:32:30 -04:00
Keith Zantow 47cafae01b chore: simplify and cleanup
Signed-off-by: Keith Zantow <keith.zantow@anchore.com>
2026-09-22 00:23:36 -04:00
Keith Zantow 53cd4254e4 chore: revert custom index in favor of key_Split
Signed-off-by: Keith Zantow <keith.zantow@anchore.com>
2026-09-20 11:22:10 -04:00
Keith Zantow 8c0d3747ce feat: initial recursive archive cataloging impl
Signed-off-by: Keith Zantow <keith.zantow@anchore.com>
2026-09-10 08:45:36 -04:00
Keith Zantow 0a93e60b16 chore: revert original recursive-archive implementations from PR #4044 and #4761
Signed-off-by: Keith Zantow <keith.zantow@anchore.com>
2026-09-09 17:22:51 -04:00
Thomas Bechtold 5e5760e132 fix(archive): use writable permissions for extracted directories
Archives may contain directory entries with read-only permissions
(e.g., META-INF/ with mode 0555 in JARs). When extracted to temp
directories, these restricted permissions prevented writing files
into those directories, causing "permission denied" errors for
subsequent file entries like META-INF/MANIFEST.MF.

Always use 0755 for extracted directories since they are temporary
working directories owned by the extraction process.

Signed-off-by: Thomas Bechtold <thomasbechtold@jpberlin.de>
2026-04-27 08:49:46 +02:00
Thomas Bechtold bb7dbf2ff8 test(archive): add integration tests for recursive archive cataloging
Add integration tests covering the full archive extraction pipeline:

- tar.gz containing Python package metadata (single-level)
- zip containing a JAR with MANIFEST.MF (two-level, same format)
- Mixed archive types (zip + tar.gz at same level)
- Depth limit enforcement (3-level zip nesting, limited to 1)
- Extension exclusion filtering
- 3-level mixed format nesting: tar.gz -> zip -> tar.gz -> files
  verifying cross-format extraction and full access path chains
- Reverse format crossing: tar.gz -> zip -> tar.gz verifying
  content readability at all levels and correct relationship count

Signed-off-by: Thomas Bechtold <thomasbechtold@jpberlin.de>
2026-04-27 08:49:00 +02:00
Thomas Bechtold 5d6ad0b9e8 test(archive): add edge case tests for Orchestrator
Add tests for important orchestrator edge cases:
- Nested archives (zip-inside-zip): verifies multi-round extraction
  discovers archives within previously-extracted archives
- Context cancellation: graceful handling without hanging
- Multiple archives at same level: both extracted in one round
- Corrupt archive: skipped without error, doesn't block other work
- Empty archive: not counted as extracted
- Per-archive file limit: partial extraction handled correctly
- Relationship structure: contains relationships emit the archive's
  coordinates as the source side

Signed-off-by: Thomas Bechtold <thomasbechtold@jpberlin.de>
2026-04-27 08:49:00 +02:00
Thomas Bechtold f7ea072900 feat(archive): add Orchestrator for iterative archive extraction
Implement the Orchestrator that manages the iterative
extract-catalog loop for recursive archive discovery:

- Scans the composite resolver for candidate archive locations,
  detecting format by peeking 512 bytes via FileContentsByLocation
  rather than copying every non-archive file to a probe-temp file
- Extracts identified archives to per-archive temp directories
- Builds child resolvers via an injectable ResolverFactory (kept
  pluggable so tests can substitute a mock resolver without
  importing syft/internal/fileresolver)
- Tracks extraction state to avoid reprocessing
- Enforces safety limits (depth, per-archive size, file count,
  total extraction budget)
- Generates ContainsRelationship entries linking each archive to
  its extracted-contents filesystem
- Manages temp directory lifecycle with Cleanup()

Signed-off-by: Thomas Bechtold <thomasbechtold@jpberlin.de>
2026-04-27 08:48:56 +02:00
Thomas Bechtold b26337759e test(archive): add edge case tests for CompositeResolver
Add tests for important edge cases:
- Nested children (child-in-child): verifies that archives within
  archives produce correct multi-level AccessPath chains like
  "/archive1.tar.gz:/inner-archive.zip:/deep-file.txt"
- Empty child resolver: no locations leak from empty archives
- Same file path in parent and child: both versions accessible
  with distinct FileSystemID routing
- Context cancellation in AllLocations: no hanging

Signed-off-by: Thomas Bechtold <thomasbechtold@jpberlin.de>
2026-04-27 08:47:22 +02:00
Thomas Bechtold 4240cb964f feat(archive): add CompositeResolver for transparent archive content access
Implement a file.Resolver decorator that composites a parent resolver
with child resolvers created from extracted archive contents. Files from
child resolvers appear transparently alongside parent files.

Key behaviors:
- FilesByPath/FilesByGlob/FilesByMIMEType delegate to parent AND children
- FileContentsByLocation routes to the correct resolver via FileSystemID
- Locations from children get archive-prefixed AccessPath (using ":"
  separator matching Java cataloger convention) and unique FileSystemID
- Thread-safe via RWMutex for concurrent access

Signed-off-by: Thomas Bechtold <thomasbechtold@jpberlin.de>
2026-04-27 08:47:22 +02:00
Thomas Bechtold 4df9a0ce10 feat(archive): add extractor interface and zip/tar implementations
Introduce the internal/archive package with a generic Extractor
interface for extracting archive contents to a destination directory.

Includes two implementations:
- ZipExtractor: handles zip-based archives (.zip, .jar, .war, etc.)
- TarExtractor: handles tar-based archives (.tar, .tar.gz, .tar.bz2, etc.)

Both extractors enforce configurable safety limits and protect against
common malicious-archive vectors:
- Per-archive extraction size limit
- Per-archive file count limit
- Zip-slip path traversal prevention via existing SafeJoin
- Symlink entries are written as real symlinks only when their target
  resolves inside destDir; symlinks pointing outside are dropped to
  avoid escape via symlink-then-write
- Non-regular non-symlink entries (devices, fifos, sockets) are skipped
  because their contents can't safely round-trip to disk

Also provides FindExtractor for auto-detecting archive format, and
IsExcludedExtension for filtering by extension.

Signed-off-by: Thomas Bechtold <thomasbechtold@jpberlin.de>
2026-04-27 08:47:16 +02:00