Skip to content

Commit d0f4e93

Browse files
committed
bug(aws): handle ECR repositories in different regions
Signed-off-by: Kevin Conner <[email protected]>
1 parent 13e72ec commit d0f4e93

File tree

8 files changed

+145
-54
lines changed

8 files changed

+145
-54
lines changed

pkg/fanal/image/registry/azure/azure.go

+9-6
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,33 @@ import (
1010
"github.com/Azure/azure-sdk-for-go/profiles/preview/preview/containerregistry/runtime/containerregistry"
1111
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
1212
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
13+
"github.com/aquasecurity/trivy/pkg/fanal/image/registry/intf"
1314
"golang.org/x/xerrors"
1415

1516
"github.com/aquasecurity/trivy/pkg/fanal/types"
1617
)
1718

18-
type Registry struct {
19+
type RegistryClient struct {
1920
domain string
2021
}
2122

23+
type Registry struct {
24+
}
25+
2226
const (
2327
azureURL = "azurecr.io"
2428
scope = "https://management.azure.com/.default"
2529
scheme = "https"
2630
)
2731

28-
func (r *Registry) CheckOptions(domain string, _ types.RegistryOptions) error {
32+
func (r *Registry) CheckOptions(domain string, _ types.RegistryOptions) (intf.RegistryClient, error) {
2933
if !strings.HasSuffix(domain, azureURL) {
30-
return xerrors.Errorf("Azure registry: %w", types.InvalidURLPattern)
34+
return nil, xerrors.Errorf("Azure registry: %w", types.InvalidURLPattern)
3135
}
32-
r.domain = domain
33-
return nil
36+
return &RegistryClient{domain: domain}, nil
3437
}
3538

36-
func (r *Registry) GetCredential(ctx context.Context) (string, string, error) {
39+
func (r *RegistryClient) GetCredential(ctx context.Context) (string, string, error) {
3740
cred, err := azidentity.NewDefaultAzureCredential(nil)
3841
if err != nil {
3942
return "", "", xerrors.Errorf("unable to generate acr credential error: %w", err)

pkg/fanal/image/registry/azure/azure_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func TestRegistry_CheckOptions(t *testing.T) {
2828
for _, tt := range tests {
2929
t.Run(tt.name, func(t *testing.T) {
3030
r := azure.Registry{}
31-
err := r.CheckOptions(tt.domain, types.RegistryOptions{})
31+
_, err := r.CheckOptions(tt.domain, types.RegistryOptions{})
3232
if tt.wantErr != "" {
3333
assert.EqualError(t, err, tt.wantErr)
3434
} else {

pkg/fanal/image/registry/ecr/ecr.go

+40-13
Original file line numberDiff line numberDiff line change
@@ -3,55 +3,82 @@ package ecr
33
import (
44
"context"
55
"encoding/base64"
6+
"regexp"
67
"strings"
78

9+
"github.com/aquasecurity/trivy/pkg/fanal/image/registry/intf"
810
"github.com/aws/aws-sdk-go-v2/aws"
911
"github.com/aws/aws-sdk-go-v2/config"
1012
"github.com/aws/aws-sdk-go-v2/credentials"
1113
"github.com/aws/aws-sdk-go-v2/service/ecr"
1214
"golang.org/x/xerrors"
1315

1416
"github.com/aquasecurity/trivy/pkg/fanal/types"
17+
"github.com/aquasecurity/trivy/pkg/log"
1518
)
1619

17-
const ecrURL = "amazonaws.com"
18-
1920
type ecrAPI interface {
2021
GetAuthorizationToken(ctx context.Context, params *ecr.GetAuthorizationTokenInput, optFns ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error)
2122
}
2223

2324
type ECR struct {
25+
}
26+
27+
type ECRClient struct {
2428
Client ecrAPI
2529
}
2630

27-
func getSession(option types.RegistryOptions) (aws.Config, error) {
31+
func getSession(domain, region string, option types.RegistryOptions) (aws.Config, error) {
2832
// create custom credential information if option is valid
2933
if option.AWSSecretKey != "" && option.AWSAccessKey != "" && option.AWSRegion != "" {
34+
if region != option.AWSRegion {
35+
log.Warnf("The region from AWS_REGION (%s) is being overridden. The region from domain (%s) was used.", option.AWSRegion, domain)
36+
}
3037
return config.LoadDefaultConfig(
3138
context.TODO(),
32-
config.WithRegion(option.AWSRegion),
39+
config.WithRegion(region),
3340
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(option.AWSAccessKey, option.AWSSecretKey, option.AWSSessionToken)),
3441
)
3542
}
36-
return config.LoadDefaultConfig(context.TODO())
43+
return config.LoadDefaultConfig(context.TODO(), config.WithRegion(region))
3744
}
3845

39-
func (e *ECR) CheckOptions(domain string, option types.RegistryOptions) error {
40-
if !strings.HasSuffix(domain, ecrURL) {
41-
return xerrors.Errorf("ECR : %w", types.InvalidURLPattern)
46+
func (e *ECR) CheckOptions(domain string, option types.RegistryOptions) (intf.RegistryClient, error) {
47+
region := determineRegion(domain)
48+
if region == "" {
49+
return nil, xerrors.Errorf("ECR : %w", types.InvalidURLPattern)
4250
}
4351

44-
cfg, err := getSession(option)
52+
cfg, err := getSession(domain, region, option)
4553
if err != nil {
46-
return err
54+
return nil, err
4755
}
4856

4957
svc := ecr.NewFromConfig(cfg)
50-
e.Client = svc
51-
return nil
58+
return &ECRClient{Client: svc}, nil
59+
}
60+
61+
// Endpoints take the form
62+
// <registry-id>.dkr.ecr.<region>.amazonaws.com
63+
// <registry-id>.dkr.ecr-fips.<region>.amazonaws.com
64+
// <registry-id>.dkr.ecr.<region>.amazonaws.com.cn
65+
// <registry-id>.dkr.ecr.<region>.sc2s.sgov.gov
66+
// <registry-id>.dkr.ecr.<region>.c2s.ic.gov
67+
// see
68+
// - https://docs.aws.amazon.com/general/latest/gr/ecr.html
69+
// - https://docs.amazonaws.cn/en_us/aws/latest/userguide/endpoints-arns.html
70+
// - https://github.com/boto/botocore/blob/1.34.51/botocore/data/endpoints.json
71+
var ecrEndpointMatch = regexp.MustCompile(`^[^.]+\.dkr\.ecr(?:-fips)?\.([^.]+)\.(?:amazonaws\.com(?:\.cn)?|sc2s\.sgov\.gov|c2s\.ic\.gov)$`)
72+
73+
func determineRegion(domain string) string {
74+
matches := ecrEndpointMatch.FindStringSubmatch(domain)
75+
if matches != nil {
76+
return matches[1]
77+
}
78+
return ""
5279
}
5380

54-
func (e *ECR) GetCredential(ctx context.Context) (username, password string, err error) {
81+
func (e *ECRClient) GetCredential(ctx context.Context) (username, password string, err error) {
5582
input := &ecr.GetAuthorizationTokenInput{}
5683
result, err := e.Client.GetAuthorizationToken(ctx, input)
5784
if err != nil {

pkg/fanal/image/registry/ecr/ecr_test.go

+63-5
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,91 @@ import (
88
"github.com/aws/aws-sdk-go-v2/aws"
99
"github.com/aws/aws-sdk-go-v2/service/ecr"
1010
awstypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
11+
"github.com/stretchr/testify/require"
1112

1213
"github.com/aquasecurity/trivy/pkg/fanal/types"
1314
)
1415

16+
type testECRClient interface {
17+
Options() ecr.Options
18+
}
19+
1520
func TestCheckOptions(t *testing.T) {
1621
var tests = map[string]struct {
17-
domain string
18-
wantErr error
22+
domain string
23+
expectedRegion string
24+
wantErr error
1925
}{
2026
"InvalidURL": {
2127
domain: "alpine:3.9",
2228
wantErr: types.InvalidURLPattern,
2329
},
2430
"NoOption": {
25-
domain: "xxx.ecr.ap-northeast-1.amazonaws.com",
31+
domain: "xxx.dkr.ecr.ap-northeast-1.amazonaws.com",
32+
expectedRegion: "ap-northeast-1",
33+
},
34+
"region-1": {
35+
domain: "xxx.dkr.ecr.region-1.amazonaws.com",
36+
expectedRegion: "region-1",
37+
},
38+
"region-2": {
39+
domain: "xxx.dkr.ecr.region-2.amazonaws.com",
40+
expectedRegion: "region-2",
41+
},
42+
"fips-region-1": {
43+
domain: "xxx.dkr.ecr-fips.fips-region.amazonaws.com",
44+
expectedRegion: "fips-region",
45+
},
46+
"cn-region-1": {
47+
domain: "xxx.dkr.ecr.region-1.amazonaws.com.cn",
48+
expectedRegion: "region-1",
49+
},
50+
"cn-region-2": {
51+
domain: "xxx.dkr.ecr.region-2.amazonaws.com.cn",
52+
expectedRegion: "region-2",
53+
},
54+
"sc2s-region-1": {
55+
domain: "xxx.dkr.ecr.sc2s-region.sc2s.sgov.gov",
56+
expectedRegion: "sc2s-region",
57+
},
58+
"c2s-region-1": {
59+
domain: "xxx.dkr.ecr.c2s-region.c2s.ic.gov",
60+
expectedRegion: "c2s-region",
61+
},
62+
"invalid-ecr": {
63+
domain: "xxx.dkrecr.region-1.amazonaws.com",
64+
wantErr: types.InvalidURLPattern,
65+
},
66+
"invalid-fips": {
67+
domain: "xxx.dkr.ecrfips.fips-region.amazonaws.com",
68+
wantErr: types.InvalidURLPattern,
69+
},
70+
"invalid-cn": {
71+
domain: "xxx.dkr.ecr.region-2.amazonaws.cn",
72+
wantErr: types.InvalidURLPattern,
73+
},
74+
"invalid-sc2s": {
75+
domain: "xxx.dkr.ecr.sc2s-region.sc2s.sgov",
76+
wantErr: types.InvalidURLPattern,
77+
},
78+
"invalid-cs2": {
79+
domain: "xxx.dkr.ecr.c2s-region.c2s.ic",
80+
wantErr: types.InvalidURLPattern,
2681
},
2782
}
2883

2984
for testname, v := range tests {
3085
a := &ECR{}
31-
err := a.CheckOptions(v.domain, types.RegistryOptions{})
86+
ecrClient, err := a.CheckOptions(v.domain, types.RegistryOptions{})
3287
if err != nil {
3388
if !errors.Is(err, v.wantErr) {
3489
t.Errorf("[%s]\nexpected error based on %v\nactual : %v", testname, v.wantErr, err)
3590
}
3691
continue
3792
}
93+
94+
client := (ecrClient.(*ECRClient)).Client.(testECRClient)
95+
require.Equal(t, v.expectedRegion, client.Options().Region)
3896
}
3997
}
4098

@@ -82,7 +140,7 @@ func TestECRGetCredential(t *testing.T) {
82140
}
83141

84142
for i, c := range cases {
85-
e := ECR{
143+
e := ECRClient{
86144
Client: mockedECR{Resp: c.Resp},
87145
}
88146
username, password, err := e.GetCredential(context.Background())

pkg/fanal/image/registry/google/google.go

+11-7
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,38 @@ import (
77
"github.com/GoogleCloudPlatform/docker-credential-gcr/config"
88
"github.com/GoogleCloudPlatform/docker-credential-gcr/credhelper"
99
"github.com/GoogleCloudPlatform/docker-credential-gcr/store"
10+
"github.com/aquasecurity/trivy/pkg/fanal/image/registry/intf"
1011
"golang.org/x/xerrors"
1112

1213
"github.com/aquasecurity/trivy/pkg/fanal/types"
1314
)
1415

15-
type Registry struct {
16+
type GoogleRegistryClient struct {
1617
Store store.GCRCredStore
1718
domain string
1819
}
1920

21+
type Registry struct {
22+
}
23+
2024
// Google container registry
2125
const gcrURL = "gcr.io"
2226

2327
// Google artifact registry
2428
const garURL = "docker.pkg.dev"
2529

26-
func (g *Registry) CheckOptions(domain string, option types.RegistryOptions) error {
30+
func (g *Registry) CheckOptions(domain string, option types.RegistryOptions) (intf.RegistryClient, error) {
2731
if !strings.HasSuffix(domain, gcrURL) && !strings.HasSuffix(domain, garURL) {
28-
return xerrors.Errorf("Google registry: %w", types.InvalidURLPattern)
32+
return nil, xerrors.Errorf("Google registry: %w", types.InvalidURLPattern)
2933
}
30-
g.domain = domain
34+
client := GoogleRegistryClient{domain: domain}
3135
if option.GCPCredPath != "" {
32-
g.Store = store.NewGCRCredStore(option.GCPCredPath)
36+
client.Store = store.NewGCRCredStore(option.GCPCredPath)
3337
}
34-
return nil
38+
return &client, nil
3539
}
3640

37-
func (g *Registry) GetCredential(_ context.Context) (username, password string, err error) {
41+
func (g *GoogleRegistryClient) GetCredential(_ context.Context) (username, password string, err error) {
3842
var credStore store.GCRCredStore
3943
if g.Store == nil {
4044
credStore, err = store.DefaultGCRCredStore()

pkg/fanal/image/registry/google/google_test.go

+1-13
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,15 @@ package google
22

33
import (
44
"errors"
5-
"reflect"
65
"testing"
76

8-
"github.com/GoogleCloudPlatform/docker-credential-gcr/store"
9-
107
"github.com/aquasecurity/trivy/pkg/fanal/types"
118
)
129

1310
func TestCheckOptions(t *testing.T) {
1411
var tests = map[string]struct {
1512
domain string
1613
opt types.RegistryOptions
17-
gcr *Registry
1814
wantErr error
1915
}{
2016
"InvalidURL": {
@@ -23,21 +19,16 @@ func TestCheckOptions(t *testing.T) {
2319
},
2420
"NoOption": {
2521
domain: "gcr.io",
26-
gcr: &Registry{domain: "gcr.io"},
2722
},
2823
"CredOption": {
2924
domain: "gcr.io",
3025
opt: types.RegistryOptions{GCPCredPath: "/path/to/file.json"},
31-
gcr: &Registry{
32-
domain: "gcr.io",
33-
Store: store.NewGCRCredStore("/path/to/file.json"),
34-
},
3526
},
3627
}
3728

3829
for testname, v := range tests {
3930
g := &Registry{}
40-
err := g.CheckOptions(v.domain, v.opt)
31+
_, err := g.CheckOptions(v.domain, v.opt)
4132
if v.wantErr != nil {
4233
if err == nil {
4334
t.Errorf("%s : expected error but no error", testname)
@@ -48,8 +39,5 @@ func TestCheckOptions(t *testing.T) {
4839
}
4940
continue
5041
}
51-
if !reflect.DeepEqual(v.gcr, g) {
52-
t.Errorf("[%s]\nexpected : %v\nactual : %v", testname, v.gcr, g)
53-
}
5442
}
5543
}
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package intf
2+
3+
import (
4+
"context"
5+
6+
"github.com/aquasecurity/trivy/pkg/fanal/types"
7+
)
8+
9+
type RegistryClient interface {
10+
GetCredential(ctx context.Context) (string, string, error)
11+
}
12+
13+
type Registry interface {
14+
CheckOptions(domain string, option types.RegistryOptions) (RegistryClient, error)
15+
}

0 commit comments

Comments
 (0)