Files
trivy/pkg/fanal/image/image_test.go
Matthieu MOREL a19e0aa1ba fix: octalLiteral from go-critic (#8811)
Signed-off-by: Matthieu MOREL <matthieu.morel35@gmail.com>
2025-05-05 13:49:07 +00:00

561 lines
15 KiB
Go

package image
import (
"fmt"
"net/http/httptest"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/aquasecurity/testdocker/auth"
"github.com/aquasecurity/testdocker/engine"
"github.com/aquasecurity/testdocker/registry"
"github.com/aquasecurity/testdocker/tarfile"
"github.com/aquasecurity/trivy/pkg/fanal/types"
)
func setupEngineAndRegistry(t *testing.T) (*httptest.Server, *httptest.Server) {
imagePaths := map[string]string{
"alpine:3.10": "../test/testdata/alpine-310.tar.gz",
"alpine:3.11": "../test/testdata/alpine-311.tar.gz",
"a187dde48cd2": "../test/testdata/alpine-311.tar.gz",
}
opt := engine.Option{
APIVersion: "1.45",
ImagePaths: imagePaths,
}
te := engine.NewDockerEngine(opt)
images := map[string]v1.Image{
"v2/library/alpine:3.10": localImage(t),
}
tr := registry.NewDockerRegistry(registry.Option{
Images: images,
Auth: auth.Auth{},
})
t.Setenv("DOCKER_HOST", fmt.Sprintf("tcp://%s", te.Listener.Addr().String()))
return te, tr
}
func TestNewDockerImage(t *testing.T) {
te, tr := setupEngineAndRegistry(t)
defer func() {
te.Close()
tr.Close()
}()
serverAddr := tr.Listener.Addr().String()
type args struct {
imageName string
option types.ImageOptions
}
tests := []struct {
name string
args args
wantID string
wantConfigFile *v1.ConfigFile
wantRepoTags []string
wantRepoDigests []string
wantErr bool
}{
{
name: "happy path with Docker Engine (use pattern <imageName>:<tag> for image name)",
args: args{
imageName: "alpine:3.11",
},
wantID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72",
wantRepoTags: []string{"alpine:3.11"},
wantConfigFile: &v1.ConfigFile{
Architecture: "amd64",
OS: "linux",
Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 196162891, time.UTC)},
DockerVersion: "18.09.7",
History: []v1.History{
{
Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 0, time.UTC)},
CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]",
Comment: "",
EmptyLayer: true,
},
{
Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 0, time.UTC)},
CreatedBy: "/bin/sh -c #(nop) ADD file:0c4555f363c2672e350001f1293e689875a3760afe7b3f9146886afe67121cba in / ",
EmptyLayer: false,
},
},
RootFS: v1.RootFS{
Type: "layers",
DiffIDs: []v1.Hash{
{
Algorithm: "sha256",
Hex: "beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203",
},
},
},
Config: v1.Config{
Cmd: []string{"/bin/sh"},
Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
Image: "sha256:74df73bb19fbfc7fb5ab9a8234b3d98ee2fb92df5b824496679802685205ab8c",
ArgsEscaped: true,
},
OSVersion: "",
},
},
{
name: "happy path with Docker Engine (use pattern <ImageID> for image name)",
args: args{
imageName: "a187dde48cd2",
},
wantID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72",
wantRepoTags: []string{"alpine:3.11"},
wantConfigFile: &v1.ConfigFile{
Architecture: "amd64",
OS: "linux",
Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 196162891, time.UTC)},
DockerVersion: "18.09.7",
History: []v1.History{
{
Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 0, time.UTC)},
CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]",
Comment: "",
EmptyLayer: true,
},
{
Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 0, time.UTC)},
CreatedBy: "/bin/sh -c #(nop) ADD file:0c4555f363c2672e350001f1293e689875a3760afe7b3f9146886afe67121cba in / ",
EmptyLayer: false,
},
},
RootFS: v1.RootFS{
Type: "layers",
DiffIDs: []v1.Hash{
{
Algorithm: "sha256",
Hex: "beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203",
},
},
},
Config: v1.Config{
Cmd: []string{"/bin/sh"},
Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
Image: "sha256:74df73bb19fbfc7fb5ab9a8234b3d98ee2fb92df5b824496679802685205ab8c",
ArgsEscaped: true,
},
OSVersion: "",
},
},
{
name: "happy path with Docker Registry",
args: args{
imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr),
},
wantID: "sha256:af341ccd2df8b0e2d67cf8dd32e087bfda4e5756ebd1c76bbf3efa0dc246590e",
wantRepoTags: []string{serverAddr + "/library/alpine:3.10"},
wantRepoDigests: []string{
serverAddr + "/library/alpine@sha256:e10ea963554297215478627d985466ada334ed15c56d3d6bb808ceab98374d91",
},
wantConfigFile: &v1.ConfigFile{
Architecture: "amd64",
Container: "7f4a36a667d138b079b5ff059485ff65bfbb5ebc48f24a89f983b918e73f4f28",
Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 6, 686519038, time.UTC)},
DockerVersion: "18.06.1-ce",
History: []v1.History{
{
Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 6, 551172402, time.UTC)},
CreatedBy: "/bin/sh -c #(nop) ADD file:d48cac34fac385cbc1de6adfdd88300f76f9bbe346cd17e64fd834d042a98326 in / ",
EmptyLayer: false,
},
{
Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 6, 686519038, time.UTC)},
CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]",
Comment: "",
EmptyLayer: true,
},
},
OS: "linux",
RootFS: v1.RootFS{
Type: "layers",
DiffIDs: []v1.Hash{
{
Algorithm: "sha256",
Hex: "531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028",
},
},
},
Config: v1.Config{
Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
Cmd: []string{"/bin/sh"},
Image: "sha256:7c41e139ba64dd2eba852a2e963ee86f2e8da3a5bbfaf10cf4349535dbf0ff08",
ArgsEscaped: true,
},
OSVersion: "",
},
},
{
name: "happy path with insecure Docker Registry",
args: args{
imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr),
option: types.ImageOptions{
RegistryOptions: types.RegistryOptions{
Credentials: []types.Credential{
{
Username: "test",
Password: "test",
},
},
Insecure: true,
},
},
},
wantID: "sha256:af341ccd2df8b0e2d67cf8dd32e087bfda4e5756ebd1c76bbf3efa0dc246590e",
wantRepoTags: []string{serverAddr + "/library/alpine:3.10"},
wantRepoDigests: []string{
serverAddr + "/library/alpine@sha256:e10ea963554297215478627d985466ada334ed15c56d3d6bb808ceab98374d91",
},
wantConfigFile: &v1.ConfigFile{
Architecture: "amd64",
Container: "7f4a36a667d138b079b5ff059485ff65bfbb5ebc48f24a89f983b918e73f4f28",
Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 6, 686519038, time.UTC)},
DockerVersion: "18.06.1-ce",
History: []v1.History{
{
Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 6, 551172402, time.UTC)},
CreatedBy: "/bin/sh -c #(nop) ADD file:d48cac34fac385cbc1de6adfdd88300f76f9bbe346cd17e64fd834d042a98326 in / ",
EmptyLayer: false,
},
{
Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 6, 686519038, time.UTC)},
CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]",
Comment: "",
EmptyLayer: true,
},
},
OS: "linux",
RootFS: v1.RootFS{
Type: "layers",
DiffIDs: []v1.Hash{
{
Algorithm: "sha256",
Hex: "531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028",
},
},
},
Config: v1.Config{
Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
Cmd: []string{"/bin/sh"},
Image: "sha256:7c41e139ba64dd2eba852a2e963ee86f2e8da3a5bbfaf10cf4349535dbf0ff08",
ArgsEscaped: true,
},
},
},
{
name: "sad path with invalid tag",
args: args{
imageName: fmt.Sprintf("%s/library/alpine:3.11!!!", serverAddr),
},
wantErr: true,
},
{
name: "sad path with non-exist image",
args: args{
imageName: fmt.Sprintf("%s/library/alpine:100", serverAddr),
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.option.ImageSources = types.AllImageSources
img, cleanup, err := NewContainerImage(t.Context(), tt.args.imageName, tt.args.option)
defer cleanup()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
gotID, err := img.ID()
require.NoError(t, err)
assert.Equal(t, tt.wantID, gotID)
gotConfigFile, err := img.ConfigFile()
require.NoError(t, err)
assert.Equal(t, tt.wantConfigFile, gotConfigFile)
gotRepoTags := img.RepoTags()
assert.Equal(t, tt.wantRepoTags, gotRepoTags)
gotRepoDigests := img.RepoDigests()
assert.Equal(t, tt.wantRepoDigests, gotRepoDigests)
})
}
}
func setupPrivateRegistry(t *testing.T) *httptest.Server {
images := map[string]v1.Image{
"v2/library/alpine:3.10": localImage(t),
}
tr := registry.NewDockerRegistry(registry.Option{
Images: images,
Auth: auth.Auth{
User: "test",
Password: "testpass",
Secret: "secret",
},
})
return tr
}
func TestNewDockerImageWithPrivateRegistry(t *testing.T) {
tr := setupPrivateRegistry(t)
defer tr.Close()
serverAddr := tr.Listener.Addr().String()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iss": "testdocker",
})
registryToken, err := token.SignedString([]byte("secret"))
require.NoError(t, err)
type args struct {
imageName string
option types.ImageOptions
}
tests := []struct {
name string
args args
want v1.Image
wantErr string
}{
{
name: "happy path with private Docker Registry",
args: args{
imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr),
option: types.ImageOptions{
RegistryOptions: types.RegistryOptions{
Credentials: []types.Credential{
{
Username: "test",
Password: "testpass",
},
},
Insecure: true,
},
},
},
},
{
name: "happy path with registry token",
args: args{
imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr),
option: types.ImageOptions{
RegistryOptions: types.RegistryOptions{
RegistryToken: registryToken,
Insecure: true,
},
},
},
},
{
name: "sad path without a credential",
args: args{
imageName: fmt.Sprintf("%s/library/alpine:3.11", serverAddr),
},
wantErr: "unexpected status code 401",
},
{
name: "sad path with invalid registry token",
args: args{
imageName: fmt.Sprintf("%s/library/alpine:3.11", serverAddr),
option: types.ImageOptions{
RegistryOptions: types.RegistryOptions{
RegistryToken: registryToken + "invalid",
Insecure: true,
},
},
},
wantErr: "signature is invalid",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.option.ImageSources = types.AllImageSources
_, cleanup, err := NewContainerImage(t.Context(), tt.args.imageName, tt.args.option)
defer cleanup()
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestNewArchiveImage(t *testing.T) {
type args struct {
fileName string
}
tests := []struct {
name string
args args
want v1.Image
wantErr string
}{
{
name: "happy path",
args: args{
fileName: "../test/testdata/alpine-310.tar.gz",
},
},
{
name: "happy path with OCI Image Format",
args: args{
fileName: "../test/testdata/test.oci",
},
},
{
name: "happy path with OCI Image and tag Format",
args: args{
fileName: "../test/testdata/test_image_tag.oci:0.0.1",
},
},
{
name: "happy path with OCI Image only",
args: args{
fileName: "../test/testdata/test_image_tag.oci",
},
},
{
name: "sad path with OCI Image and invalid tagFormat",
args: args{
fileName: "../test/testdata/test_image_tag.oci:0.0.0",
},
wantErr: "invalid OCI image ref",
},
{
name: "sad path, oci image not found",
args: args{
fileName: "../test/testdata/invalid.tar.gz",
},
wantErr: "unable to open",
},
{
name: "sad path with OCI Image Format index.json directory",
args: args{
fileName: "../test/testdata/test_index_json_dir.oci",
},
wantErr: "unable to retrieve index.json",
},
{
name: "sad path with OCI Image Format invalid index.json",
args: args{
fileName: "../test/testdata/test_bad_index_json.oci",
},
wantErr: "invalid index.json",
},
{
name: "sad path with OCI Image Format no valid manifests",
args: args{
fileName: "../test/testdata/test_no_valid_manifests.oci",
},
wantErr: "no valid manifest",
},
{
name: "sad path with OCI Image Format with invalid oci image digest",
args: args{
fileName: "../test/testdata/test_invalid_oci_image.oci",
},
wantErr: "invalid OCI image",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
img, err := NewArchiveImage(tt.args.fileName)
switch {
case tt.wantErr != "":
require.ErrorContains(t, err, tt.wantErr, tt.name)
return
default:
require.NoError(t, err, tt.name)
}
// archive doesn't support RepoTags and RepoDigests
assert.Empty(t, img.RepoTags())
assert.Empty(t, img.RepoDigests())
})
}
}
func TestDockerPlatformArguments(t *testing.T) {
tr := setupPrivateRegistry(t)
defer tr.Close()
serverAddr := tr.Listener.Addr().String()
type args struct {
option types.ImageOptions
}
tests := []struct {
name string
args args
want v1.Image
wantErr string
}{
{
name: "happy path with valid platform",
args: args{
option: types.ImageOptions{
RegistryOptions: types.RegistryOptions{
Credentials: []types.Credential{
{
Username: "test",
Password: "testpass",
},
},
Insecure: true,
Platform: types.Platform{
Platform: &v1.Platform{
Architecture: "arm",
OS: "linux",
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
imageName := fmt.Sprintf("%s/library/alpine:3.10", serverAddr)
tt.args.option.ImageSources = types.AllImageSources
_, cleanup, err := NewContainerImage(t.Context(), imageName, tt.args.option)
defer cleanup()
if tt.wantErr != "" {
assert.ErrorContains(t, err, tt.wantErr, err)
} else {
require.NoError(t, err)
}
})
}
}
func localImage(t *testing.T) v1.Image {
img, err := tarfile.ImageFromPath("../test/testdata/alpine-310.tar.gz")
require.NoError(t, err)
return img
}