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 }