github.com/khulnasoft/trivy@v0.48.1-0.20231207234930-27df843a75e0/integration/registry_test.go (about)

     1  //go:build integration
     2  // +build integration
     3  
     4  package integration
     5  
     6  import (
     7  	"bytes"
     8  	"compress/gzip"
     9  	"context"
    10  	"crypto/tls"
    11  	"crypto/x509"
    12  	"encoding/json"
    13  	"fmt"
    14  	"io"
    15  	"net/http"
    16  	"net/url"
    17  	"os"
    18  	"path/filepath"
    19  	"testing"
    20  
    21  	dockercontainer "github.com/docker/docker/api/types/container"
    22  	"github.com/docker/go-connections/nat"
    23  	"github.com/google/go-containerregistry/pkg/authn"
    24  	"github.com/google/go-containerregistry/pkg/name"
    25  	"github.com/google/go-containerregistry/pkg/v1/remote"
    26  	"github.com/google/go-containerregistry/pkg/v1/tarball"
    27  	"github.com/stretchr/testify/assert"
    28  	"github.com/stretchr/testify/require"
    29  	testcontainers "github.com/testcontainers/testcontainers-go"
    30  	"github.com/testcontainers/testcontainers-go/wait"
    31  )
    32  
    33  const (
    34  	registryImage = "registry:2.7.0"
    35  	registryPort  = "5443/tcp"
    36  
    37  	authImage    = "cesanta/docker_auth:1"
    38  	authPort     = "5001/tcp"
    39  	authUsername = "admin"
    40  	authPassword = "badmin"
    41  )
    42  
    43  func setupRegistry(ctx context.Context, baseDir string, authURL *url.URL) (testcontainers.Container, error) {
    44  	req := testcontainers.ContainerRequest{
    45  		Name:         "registry",
    46  		Image:        registryImage,
    47  		ExposedPorts: []string{registryPort},
    48  		Env: map[string]string{
    49  			"REGISTRY_HTTP_ADDR":                 "0.0.0.0:5443",
    50  			"REGISTRY_HTTP_TLS_CERTIFICATE":      "/certs/cert.pem",
    51  			"REGISTRY_HTTP_TLS_KEY":              "/certs/key.pem",
    52  			"REGISTRY_AUTH":                      "token",
    53  			"REGISTRY_AUTH_TOKEN_REALM":          fmt.Sprintf("%s/auth", authURL),
    54  			"REGISTRY_AUTH_TOKEN_SERVICE":        "registry.docker.io",
    55  			"REGISTRY_AUTH_TOKEN_ISSUER":         "Trivy auth server",
    56  			"REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE": "/certs/cert.pem",
    57  			"REGISTRY_AUTH_TOKEN_AUTOREDIRECT":   "false",
    58  		},
    59  		Mounts: testcontainers.Mounts(
    60  			testcontainers.BindMount(filepath.Join(baseDir, "data", "certs"), "/certs"),
    61  		),
    62  		HostConfigModifier: func(hostConfig *dockercontainer.HostConfig) {
    63  			hostConfig.AutoRemove = true
    64  		},
    65  		WaitingFor: wait.ForLog("listening on [::]:5443"),
    66  	}
    67  
    68  	registryC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
    69  		ContainerRequest: req,
    70  		Started:          true,
    71  	})
    72  	return registryC, err
    73  }
    74  
    75  func setupAuthServer(ctx context.Context, baseDir string) (testcontainers.Container, error) {
    76  	req := testcontainers.ContainerRequest{
    77  		Name:         "docker_auth",
    78  		Image:        authImage,
    79  		ExposedPorts: []string{authPort},
    80  		Mounts: testcontainers.Mounts(
    81  			testcontainers.BindMount(filepath.Join(baseDir, "data", "auth_config"), "/config"),
    82  			testcontainers.BindMount(filepath.Join(baseDir, "data", "certs"), "/certs"),
    83  		),
    84  		HostConfigModifier: func(hostConfig *dockercontainer.HostConfig) {
    85  			hostConfig.AutoRemove = true
    86  		},
    87  		Cmd: []string{"/config/config.yml"},
    88  	}
    89  
    90  	authC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
    91  		ContainerRequest: req,
    92  		Started:          true,
    93  	})
    94  	return authC, err
    95  }
    96  
    97  func getURL(ctx context.Context, container testcontainers.Container, exposedPort nat.Port) (*url.URL, error) {
    98  	ip, err := container.Host(ctx)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	port, err := container.MappedPort(ctx, exposedPort)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  
   108  	urlStr := fmt.Sprintf("https://%s:%s", ip, port.Port())
   109  	return url.Parse(urlStr)
   110  }
   111  
   112  type registryOption struct {
   113  	AuthURL       *url.URL
   114  	Username      string
   115  	Password      string
   116  	RegistryToken bool
   117  }
   118  
   119  func TestRegistry(t *testing.T) {
   120  	ctx := context.Background()
   121  
   122  	baseDir, err := filepath.Abs(".")
   123  	require.NoError(t, err)
   124  
   125  	// disable Reaper for auth server and registry containers
   126  	t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true")
   127  
   128  	// set up auth server
   129  	authC, err := setupAuthServer(ctx, baseDir)
   130  	require.NoError(t, err)
   131  	defer authC.Terminate(ctx)
   132  
   133  	authURL, err := getURL(ctx, authC, authPort)
   134  	require.NoError(t, err)
   135  
   136  	// set up registry
   137  	registryC, err := setupRegistry(ctx, baseDir, authURL)
   138  	require.NoError(t, err)
   139  	defer registryC.Terminate(ctx)
   140  
   141  	registryURL, err := getURL(ctx, registryC, registryPort)
   142  	require.NoError(t, err)
   143  
   144  	auth := &authn.Basic{
   145  		Username: authUsername,
   146  		Password: authPassword,
   147  	}
   148  
   149  	tests := []struct {
   150  		name      string
   151  		imageName string
   152  		imageFile string
   153  		option    registryOption
   154  		golden    string
   155  		wantErr   string
   156  	}{
   157  		{
   158  			name:      "happy path with username/password",
   159  			imageName: "alpine:3.10",
   160  			imageFile: "testdata/fixtures/images/alpine-310.tar.gz",
   161  			option: registryOption{
   162  				AuthURL:  authURL,
   163  				Username: authUsername,
   164  				Password: authPassword,
   165  			},
   166  			golden: "testdata/alpine-310-registry.json.golden",
   167  		},
   168  		{
   169  			name:      "happy path with registry token",
   170  			imageName: "alpine:3.10",
   171  			imageFile: "testdata/fixtures/images/alpine-310.tar.gz",
   172  			option: registryOption{
   173  				AuthURL:       authURL,
   174  				Username:      authUsername,
   175  				Password:      authPassword,
   176  				RegistryToken: true,
   177  			},
   178  			golden: "testdata/alpine-310-registry.json.golden",
   179  		},
   180  		{
   181  			name:      "sad path",
   182  			imageName: "alpine:3.10",
   183  			imageFile: "testdata/fixtures/images/alpine-310.tar.gz",
   184  			wantErr:   "unexpected status code 401 Unauthorized: Auth failed",
   185  		},
   186  	}
   187  
   188  	for _, tc := range tests {
   189  		t.Run(tc.name, func(t *testing.T) {
   190  			s := fmt.Sprintf("%s/%s", registryURL.Host, tc.imageName)
   191  			imageRef, err := name.ParseReference(s)
   192  			require.NoError(t, err)
   193  
   194  			// 1. Load a test image from the tar file, tag it and push to the test registry.
   195  			err = replicateImage(imageRef, tc.imageFile, auth)
   196  			require.NoError(t, err)
   197  
   198  			// 2. Scan it
   199  			resultFile, err := scan(t, imageRef, baseDir, tc.golden, tc.option)
   200  
   201  			if tc.wantErr != "" {
   202  				require.Error(t, err)
   203  				require.Contains(t, err.Error(), tc.wantErr, err)
   204  				return
   205  			}
   206  			require.NoError(t, err)
   207  
   208  			// 3. Read want and got
   209  			want := readReport(t, tc.golden)
   210  			got := readReport(t, resultFile)
   211  
   212  			// 4 Update some dynamic fields
   213  			want.ArtifactName = s
   214  			for i := range want.Results {
   215  				want.Results[i].Target = fmt.Sprintf("%s (alpine 3.10.2)", s)
   216  			}
   217  
   218  			// 5. Compare want and got
   219  			assert.Equal(t, want, got)
   220  		})
   221  	}
   222  }
   223  
   224  func scan(t *testing.T, imageRef name.Reference, baseDir, goldenFile string, opt registryOption) (string, error) {
   225  	// Set up testing DB
   226  	cacheDir := initDB(t)
   227  
   228  	// Set a temp dir so that modules will not be loaded
   229  	t.Setenv("XDG_DATA_HOME", cacheDir)
   230  
   231  	// Setup the output file
   232  	outputFile := filepath.Join(t.TempDir(), "output.json")
   233  	if *update {
   234  		outputFile = goldenFile
   235  	}
   236  
   237  	// Setup env
   238  	if err := setupEnv(t, imageRef, baseDir, opt); err != nil {
   239  		return "", err
   240  	}
   241  
   242  	osArgs := []string{"-q", "--cache-dir", cacheDir, "image", "--format", "json", "--skip-update",
   243  		"--output", outputFile, imageRef.Name()}
   244  
   245  	// Run Trivy
   246  	if err := execute(osArgs); err != nil {
   247  		return "", err
   248  	}
   249  	return outputFile, nil
   250  }
   251  
   252  func setupEnv(t *testing.T, imageRef name.Reference, baseDir string, opt registryOption) error {
   253  	t.Setenv("TRIVY_INSECURE", "true")
   254  
   255  	if opt.Username != "" && opt.Password != "" {
   256  		if opt.RegistryToken {
   257  			// Get a registry token in advance
   258  			token, err := requestRegistryToken(imageRef, baseDir, opt)
   259  			if err != nil {
   260  				return err
   261  			}
   262  			t.Setenv("TRIVY_REGISTRY_TOKEN", token)
   263  		} else {
   264  			t.Setenv("TRIVY_USERNAME", opt.Username)
   265  			t.Setenv("TRIVY_PASSWORD", opt.Password)
   266  		}
   267  	}
   268  	return nil
   269  }
   270  
   271  func requestRegistryToken(imageRef name.Reference, baseDir string, opt registryOption) (string, error) {
   272  	// Create a CA certificate pool and add cert.pem to it
   273  	caCert, err := os.ReadFile(filepath.Join(baseDir, "data", "certs", "cert.pem"))
   274  	if err != nil {
   275  		return "", err
   276  	}
   277  	caCertPool := x509.NewCertPool()
   278  	caCertPool.AppendCertsFromPEM(caCert)
   279  
   280  	// Create a HTTPS client and supply the created CA pool
   281  	client := &http.Client{
   282  		Transport: &http.Transport{
   283  			TLSClientConfig: &tls.Config{
   284  				RootCAs: caCertPool,
   285  			},
   286  		},
   287  	}
   288  
   289  	// Get a registry token
   290  	req, err := http.NewRequest("GET", fmt.Sprintf("%s/auth", opt.AuthURL), nil)
   291  	if err != nil {
   292  		return "", err
   293  	}
   294  
   295  	// Set query parameters
   296  	values := req.URL.Query()
   297  	values.Set("service", "registry.docker.io")
   298  	values.Set("scope", imageRef.Scope("pull"))
   299  	req.URL.RawQuery = values.Encode()
   300  
   301  	req.SetBasicAuth(opt.Username, opt.Password)
   302  	resp, err := client.Do(req)
   303  	if err != nil {
   304  		return "", err
   305  	}
   306  	defer resp.Body.Close()
   307  
   308  	type res struct {
   309  		AccessToken string `json:"access_token"`
   310  	}
   311  
   312  	var r res
   313  	if err = json.NewDecoder(resp.Body).Decode(&r); err != nil {
   314  		return "", err
   315  	}
   316  
   317  	return r.AccessToken, nil
   318  }
   319  
   320  // ReplicateImage tags the given imagePath and pushes it to the given dest registry.
   321  func replicateImage(imageRef name.Reference, imagePath string, auth authn.Authenticator) error {
   322  	img, err := tarball.Image(func() (io.ReadCloser, error) {
   323  		b, err := os.ReadFile(imagePath)
   324  		if err != nil {
   325  			return nil, err
   326  		}
   327  		gr, err := gzip.NewReader(bytes.NewReader(b))
   328  		if err != nil {
   329  			return nil, err
   330  		}
   331  		return io.NopCloser(gr), nil
   332  	}, nil)
   333  	if err != nil {
   334  		return err
   335  	}
   336  
   337  	t := &http.Transport{
   338  		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
   339  	}
   340  
   341  	err = remote.Write(imageRef, img, remote.WithAuth(auth), remote.WithTransport(t))
   342  	if err != nil {
   343  		return err
   344  	}
   345  
   346  	return nil
   347  }