diff --git a/syft/file/cataloger/executable/testdata/elf/Makefile b/syft/file/cataloger/executable/testdata/elf/Makefile index 5130c8fac..5d859e067 100644 --- a/syft/file/cataloger/executable/testdata/elf/Makefile +++ b/syft/file/cataloger/executable/testdata/elf/Makefile @@ -22,7 +22,7 @@ tools-check: # docker buildx build --platform linux/amd64 -t $(TOOL_IMAGE) . tools: - @(docker inspect $(TOOL_IMAGE) > /dev/null && make tools-check) || (docker build -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256) + @(docker image inspect $(TOOL_IMAGE) > /dev/null 2>&1 && make tools-check) || (docker build -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256) build: tools mkdir -p $(BIN) @@ -31,7 +31,7 @@ build: tools verify: tools @rm -f $(VERIFY_FILE) docker run -i -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) make verify > $(VERIFY_FILE) - @python ./differ expected_verify $(VERIFY_FILE) + @python3 ./differ expected_verify $(VERIFY_FILE) debug: docker run -i --rm -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) bash diff --git a/syft/file/cataloger/executable/testdata/shared-info/Dockerfile b/syft/file/cataloger/executable/testdata/shared-info/Dockerfile index da598949e..d034b339c 100644 --- a/syft/file/cataloger/executable/testdata/shared-info/Dockerfile +++ b/syft/file/cataloger/executable/testdata/shared-info/Dockerfile @@ -1,10 +1,9 @@ # syntax=docker/dockerfile:1 ARG OSXCROSS_VERSION=13.1 -ARG BUILDPLATFORM=linux/amd64 -FROM --platform=$BUILDPLATFORM crazymax/osxcross:${OSXCROSS_VERSION}-ubuntu AS osxcross +FROM --platform=linux/amd64 crazymax/osxcross:${OSXCROSS_VERSION}-ubuntu AS osxcross -FROM --platform=$BUILDPLATFORM ubuntu:22.04 +FROM --platform=linux/amd64 ubuntu:22.04 RUN apt update -y && apt install -y \ curl wget \ diff --git a/syft/file/cataloger/executable/testdata/shared-info/Makefile b/syft/file/cataloger/executable/testdata/shared-info/Makefile index 8321e0ae0..cc5b03a77 100644 --- a/syft/file/cataloger/executable/testdata/shared-info/Makefile +++ b/syft/file/cataloger/executable/testdata/shared-info/Makefile @@ -19,7 +19,7 @@ tools-check: @sha256sum -c Dockerfile.sha256 || (echo "Tools Dockerfile has changed" && exit 1) tools: - @(docker inspect $(TOOL_IMAGE) > /dev/null && make tools-check) || (docker build -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256) + @(docker image inspect $(TOOL_IMAGE) > /dev/null 2>&1 && make tools-check) || (docker build --platform linux/amd64 -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256) build: tools @mkdir -p $(BIN) diff --git a/syft/pkg/cataloger/binary/internal/manager/internal/download_from_image.go b/syft/pkg/cataloger/binary/internal/manager/internal/download_from_image.go index 7eb502e90..6b8af8e6e 100644 --- a/syft/pkg/cataloger/binary/internal/manager/internal/download_from_image.go +++ b/syft/pkg/cataloger/binary/internal/manager/internal/download_from_image.go @@ -127,23 +127,83 @@ func pullDockerImage(imageReference, platform string) error { } func checkArchitecturesMatch(imageReference, platform string) (bool, string, error) { + // first check if the image exists locally cmd := exec.Command("docker", "image", "inspect", imageReference) - out, err := cmd.CombinedOutput() + if err := cmd.Run(); err != nil { + return false, "", err + } + + // prefer the manifest list for platform info — with Docker's containerd image store, + // platform metadata lives on the manifest list entry, not in the image config. + if found, err := platformInManifest(imageReference, platform); err == nil { + return found, platform, nil + } + + // fall back to image config for older Docker daemons that don't support "docker manifest inspect" + gotPlatform, err := platformFromImageInspect(imageReference) if err != nil { return false, "", err } + return gotPlatform == platform, gotPlatform, nil +} + +func platformFromImageInspect(imageReference string) (string, error) { + cmd := exec.Command("docker", "image", "inspect", imageReference) + out, err := cmd.CombinedOutput() + if err != nil { + return "", err + } + var inspect []imageInspect if err := json.Unmarshal(out, &inspect); err != nil { - return false, "", fmt.Errorf("unable to unmarshal image inspect: %w", err) + return "", fmt.Errorf("unable to unmarshal image inspect: %w", err) } if len(inspect) != 1 { - return false, "", fmt.Errorf("expected 1 image inspect, got %d", len(inspect)) + return "", fmt.Errorf("expected 1 image inspect, got %d", len(inspect)) } - gotPlatform := inspect[0].Platform() - return gotPlatform == platform, gotPlatform, nil + return inspect[0].Platform(), nil +} + +type manifestList struct { + Manifests []manifestEntry `json:"manifests"` +} + +type manifestEntry struct { + Platform manifestPlatform `json:"platform"` +} + +type manifestPlatform struct { + Architecture string `json:"architecture"` + OS string `json:"os"` +} + +// platformInManifest checks whether the wanted platform is available in the image's manifest list. +// With Docker's containerd image store, all images are distributed via manifest lists (indexes), +// and platform metadata may only be on the index entry — not in the image config that +// "docker image inspect" reads. "docker manifest inspect" reads the index directly. +func platformInManifest(imageReference, wantPlatform string) (bool, error) { + cmd := exec.Command("docker", "manifest", "inspect", imageReference) + out, err := cmd.CombinedOutput() + if err != nil { + return false, fmt.Errorf("manifest inspect failed: %w", err) + } + + var ml manifestList + if err := json.Unmarshal(out, &ml); err != nil { + return false, fmt.Errorf("unable to unmarshal manifest: %w", err) + } + + for _, m := range ml.Manifests { + p := fmt.Sprintf("%s/%s", m.Platform.OS, m.Platform.Architecture) + if p == wantPlatform { + return true, nil + } + } + + return false, nil } func copyBinariesFromDockerImages(config config.BinaryFromImage, destination string) (err error) {