mirror of
https://github.com/anchore/syft.git
synced 2026-11-03 07:56:36 +01:00
* ci: run the full unit suite on windows via make unit Drops the curated `unit:windows` subset; the windows job now bootstraps like linux and runs `make unit`. Exceptions get added as failures show up. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * ci: skip fixture refresh for windows unit tests binny has no windows release, so the bootstrap action can't install the tools the fixture refresh needs. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * test: skip linux container fixtures when docker can't run them Windows CI runners only run windows containers, so image fixtures go through `testutils.SkipWithoutLinuxContainers`. No behavior change on linux/mac. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: close leaked file handles and posix squashfs paths on windows - output writers are now closed on early returns in `scan`/`convert` (open handles block cleanup on windows) - `newSBOMMultiWriter` used `path.Dir` on host paths, so output subdirectories were never created on windows - squashfs walk paths were built with `filepath.Join` - skip a few posix-only test cases (dir perms, xdg fallback, go-rpmdb ndb handle leak upstream) Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix(golang): resolve go source module dir natively on windows - `parseGoModFile` passed the resolver's posix (`/c/...`) path to the go toolchain, so go source resolution silently found nothing on windows - skip container-built fixtures (go archs, java builds) and golden image tarballs on windows; stereoscope's layer cache filenames contain `:`, which windows rejects Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: keep posix paths posix on windows - `ChrootContext.ToChrootPath` nested volumes (`D:\cwd\C:\root`) when the scan root was on another drive, so the prefix was never trimmed - bitnami, python wheel/egg, and alpm parsers used `filepath` on posix location paths - alpm backup-file lookups silently missed on windows Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * ci: pull the test fixture cache for windows unit tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: don't map virtual file tree paths through the host on windows `FiletreeResolver` without a chroot (snaps) ran request paths through `filepath.Abs`, rooting them on the current volume (`/payload.txt` -> `/d/payload.txt`), so snap scans on windows found nothing. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: match tar entry globs as posix on windows - `matchesAnyGlob` used `doublestar.PathMatch` (host separator) on tar entry names, so tar-wrapped java archives silently produced nothing on windows - test helpers now treat binary snippet keys and `FromFile` locations as posix Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * incorporate windows path fix Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * incorporate go-rpmdb close fix Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * close squashfs fixture writers before finalizing - windows dir listings can report a zero size for files still open, so the manifest was packed empty (also adds a temporary windows diagnostic for the binary cataloger) Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * temp: surface windows binary cataloger diagnostics Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: keep directory symlink targets posix on windows - the directory indexer joined symlink targets with native separators, so links in the posix file tree pointed nowhere and globs on link names found nothing - binary fixture logical keys used native separators, so nested snippets were picked as the fixture dir on windows Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * keep addSymlinkToIndex under the funlen limit Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * drop windows skips covered by the stereoscope and go-rpmdb bumps Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
143 lines
5.2 KiB
Go
143 lines
5.2 KiB
Go
package golang
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"testing"
|
|
|
|
"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/internal/testutils"
|
|
)
|
|
|
|
// every case here is one the magic scan must reject, asserted on parseUPXInfo's errNotUPX result.
|
|
//
|
|
// Every negative case that is meant to exercise the magic scan has to carry a real ELF64 little-endian
|
|
// ident, or the container gate rejects it first and the subtest passes without the scan running at all
|
|
// (the gate and a missing magic both return errNotUPX, so nothing in the assertion can tell them apart).
|
|
// TestParseUPXInfo_OnlyELF64LittleEndian owns the gate; these own the scan.
|
|
func TestParseUPXInfo_MagicDetection(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
data []byte
|
|
foundMagic bool // the magic was located (the header may still be rejected as implausible)
|
|
}{
|
|
{
|
|
name: "contains UPX magic at start",
|
|
data: append(append(packedELFStub(), []byte("UPX!")...), make([]byte, 100)...),
|
|
foundMagic: true,
|
|
},
|
|
{
|
|
name: "contains UPX magic with offset",
|
|
data: append(append(append(packedELFStub(), make([]byte, 500)...), []byte("UPX!")...), make([]byte, 100)...),
|
|
foundMagic: true,
|
|
},
|
|
{
|
|
// UPX packs PE and Mach-O with this same l_info/p_info layout, and nothing here can place
|
|
// their blocks, so the container has to be checked before the magic is trusted
|
|
name: "UPX magic but not an ELF container",
|
|
data: append([]byte("MZ\x90\x00UPX!"), make([]byte, 100)...),
|
|
},
|
|
{
|
|
name: "no UPX magic",
|
|
data: append(packedELFStub(), make([]byte, 100)...),
|
|
},
|
|
{
|
|
// three of the four magic bytes must not match
|
|
name: "partial UPX magic",
|
|
data: append(append(packedELFStub(), []byte("UPX")...), make([]byte, 100)...),
|
|
},
|
|
{
|
|
// never reaches the scan: the six byte ident read comes up short, so the container gate
|
|
// refuses it first. Kept because an empty reader is a shape the cataloger really is handed.
|
|
name: "empty data",
|
|
data: []byte{},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := parseUPXInfo(bytes.NewReader(tt.data), int64(len(tt.data)))
|
|
require.Error(t, err, "none of these fixtures is a usable UPX header")
|
|
if tt.foundMagic {
|
|
assert.NotErrorIs(t, err, errNotUPX, "the magic was found, so the header was parsed and rejected")
|
|
} else {
|
|
assert.ErrorIs(t, err, errNotUPX)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseUPXInfo_ValidHeader(t *testing.T) {
|
|
// construct a minimal valid UPX header matching actual format
|
|
// l_info: checksum (4) + magic (4) + lsize (2) + version (1) + format (1)
|
|
lInfo := []byte{
|
|
0, 0, 0, 0, // l_checksum (before magic)
|
|
'U', 'P', 'X', '!', // magic
|
|
0, 0, // l_lsize
|
|
14, // l_version
|
|
22, // l_format (ELF)
|
|
}
|
|
|
|
// p_info (12 bytes): progid + filesize + blocksize
|
|
pInfo := []byte{
|
|
0, 0, 0, 0, // p_progid
|
|
0, 0, 0x10, 0, // p_filesize = 0x100000 (1MB) little-endian
|
|
0, 0, 0x10, 0, // p_blocksize
|
|
}
|
|
|
|
// b_info (12 bytes): sz_unc + sz_cpr + method + filter info
|
|
bInfo := []byte{
|
|
0, 0, 0x10, 0, // sz_unc = 1MB
|
|
0, 0, 0x08, 0, // sz_cpr = 512KB (compressed)
|
|
14, 0, 0, 0, // method=LZMA, filter info
|
|
}
|
|
|
|
// padded so the declared 1MB stays within maxUPXExpansion of the fixture's own size; a real UPX file
|
|
// carries the compressed data this header describes, and the bound is measured against that.
|
|
data := append(append(append(packedELFStub(), lInfo...), pInfo...), bInfo...)
|
|
data = append(data, make([]byte, 0x100000/maxUPXExpansion)...)
|
|
|
|
reader := bytes.NewReader(data)
|
|
info, err := parseUPXInfo(reader, sizeOf(t, reader))
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint8(14), info.version)
|
|
assert.Equal(t, uint8(22), info.format)
|
|
assert.Equal(t, uint32(0x100000), info.originalSize)
|
|
}
|
|
|
|
// TestImageSmallUPXNeedsLoaderPadding checks the fixture rather than the code. UPX pads to a 4 byte
|
|
// boundary before its loader stub, and the cataloger tests over image-small-upx only catch an unpacker that
|
|
// ignores that padding if there is some. A build that needs none passes either way, which is how the bug
|
|
// went unnoticed. The case without padding is covered by TestDecompressUPX_TailExtentsArePlacedPastTheLoader.
|
|
func TestImageSmallUPXNeedsLoaderPadding(t *testing.T) {
|
|
testutils.SkipWithoutLinuxContainers(t)
|
|
img := imagetest.GetFixtureImage(t, "docker-archive", "image-small-upx")
|
|
rc, err := img.OpenPathFromSquash(stereoscopeFile.Path("/run-me"))
|
|
require.NoError(t, err)
|
|
data, err := io.ReadAll(rc)
|
|
require.NoError(t, rc.Close())
|
|
require.NoError(t, err)
|
|
|
|
r := bytes.NewReader(data)
|
|
info, err := parseUPXInfo(r, int64(len(data)))
|
|
require.NoError(t, err)
|
|
require.NotZero(t, info.loaderSize, "real UPX output carries a loader stub")
|
|
|
|
// walk the first run of extents to the end marker in front of the stub
|
|
extentsEnd := info.firstBlockOff
|
|
for blockNum := 0; ; blockNum++ {
|
|
block, _, err := readChainBlock(r, extentsEnd, blockNum, info.inputLen)
|
|
require.NoError(t, err)
|
|
if block == nil {
|
|
break
|
|
}
|
|
extentsEnd = block.dataOffset + int64(block.compressedSize)
|
|
}
|
|
require.NotZero(t, extentsEnd%4, "the fixture no longer needs padding: change the version string in its Dockerfile")
|
|
}
|