Skip to content

Commit 9461d74

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

File tree

8 files changed

+159
-54
lines changed

8 files changed

+159
-54
lines changed

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

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,19 @@ import (
1414
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
1515
"golang.org/x/xerrors"
1616

17+
"github.com/aquasecurity/trivy/pkg/fanal/image/registry/intf"
1718
"github.com/aquasecurity/trivy/pkg/fanal/types"
1819
)
1920

20-
type Registry struct {
21+
type RegistryClient struct {
2122
domain string
2223
scope string
2324
cloud cloud.Configuration
2425
}
2526

27+
type Registry struct {
28+
}
29+
2630
const (
2731
azureURL = ".azurecr.io"
2832
chinaAzureURL = ".azurecr.cn"
@@ -31,23 +35,25 @@ const (
3135
scheme = "https"
3236
)
3337

34-
func (r *Registry) CheckOptions(domain string, _ types.RegistryOptions) error {
38+
func (r *Registry) CheckOptions(domain string, _ types.RegistryOptions) (intf.RegistryClient, error) {
3539
if strings.HasSuffix(domain, azureURL) {
36-
r.domain = domain
37-
r.scope = scope
38-
r.cloud = cloud.AzurePublic
39-
return nil
40+
return &RegistryClient{
41+
domain: domain,
42+
scope: scope,
43+
cloud: cloud.AzurePublic,
44+
}, nil
4045
} else if strings.HasSuffix(domain, chinaAzureURL) {
41-
r.domain = domain
42-
r.scope = chinaScope
43-
r.cloud = cloud.AzureChina
44-
return nil
46+
return &RegistryClient{
47+
domain: domain,
48+
scope: scope,
49+
cloud: cloud.AzureChina,
50+
}, nil
4551
}
4652

47-
return xerrors.Errorf("Azure registry: %w", types.InvalidURLPattern)
53+
return nil, xerrors.Errorf("Azure registry: %w", types.InvalidURLPattern)
4854
}
4955

50-
func (r *Registry) GetCredential(ctx context.Context) (string, string, error) {
56+
func (r *RegistryClient) GetCredential(ctx context.Context) (string, string, error) {
5157
opts := azcore.ClientOptions{Cloud: r.cloud}
5258
cred, err := azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{ClientOptions: opts})
5359
if err != nil {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func TestRegistry_CheckOptions(t *testing.T) {
3838
for _, tt := range tests {
3939
t.Run(tt.name, func(t *testing.T) {
4040
r := azure.Registry{}
41-
err := r.CheckOptions(tt.domain, types.RegistryOptions{})
41+
_, err := r.CheckOptions(tt.domain, types.RegistryOptions{})
4242
if tt.wantErr != "" {
4343
assert.EqualError(t, err, tt.wantErr)
4444
} else {

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

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ecr
33
import (
44
"context"
55
"encoding/base64"
6+
"regexp"
67
"strings"
78

89
"github.com/aws/aws-sdk-go-v2/aws"
@@ -11,48 +12,73 @@ import (
1112
"github.com/aws/aws-sdk-go-v2/service/ecr"
1213
"golang.org/x/xerrors"
1314

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

17-
const ecrURLSuffix = ".amazonaws.com"
18-
const ecrURLPartial = ".dkr.ecr"
19-
2020
type ecrAPI interface {
2121
GetAuthorizationToken(ctx context.Context, params *ecr.GetAuthorizationTokenInput, optFns ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error)
2222
}
2323

2424
type ECR struct {
25+
}
26+
27+
type ECRClient struct {
2528
Client ecrAPI
2629
}
2730

28-
func getSession(option types.RegistryOptions) (aws.Config, error) {
31+
func getSession(domain, region string, option types.RegistryOptions) (aws.Config, error) {
2932
// create custom credential information if option is valid
3033
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+
}
3137
return config.LoadDefaultConfig(
3238
context.TODO(),
33-
config.WithRegion(option.AWSRegion),
39+
config.WithRegion(region),
3440
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(option.AWSAccessKey, option.AWSSecretKey, option.AWSSessionToken)),
3541
)
3642
}
37-
return config.LoadDefaultConfig(context.TODO())
43+
return config.LoadDefaultConfig(context.TODO(), config.WithRegion(region))
3844
}
3945

40-
func (e *ECR) CheckOptions(domain string, option types.RegistryOptions) error {
41-
if !strings.HasSuffix(domain, ecrURLSuffix) && !strings.Contains(domain, ecrURLPartial) {
42-
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)
4350
}
4451

45-
cfg, err := getSession(option)
52+
cfg, err := getSession(domain, region, option)
4653
if err != nil {
47-
return err
54+
return nil, err
4855
}
4956

5057
svc := ecr.NewFromConfig(cfg)
51-
e.Client = svc
52-
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 ""
5379
}
5480

55-
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) {
5682
input := &ecr.GetAuthorizationTokenInput{}
5783
result, err := e.Client.GetAuthorizationToken(ctx, input)
5884
if err != nil {

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

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,20 @@ 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",
@@ -30,19 +36,71 @@ func TestCheckOptions(t *testing.T) {
3036
wantErr: types.InvalidURLPattern,
3137
},
3238
"NoOption": {
33-
domain: "xxx.ecr.ap-northeast-1.amazonaws.com",
39+
domain: "xxx.dkr.ecr.ap-northeast-1.amazonaws.com",
40+
expectedRegion: "ap-northeast-1",
41+
},
42+
"region-1": {
43+
domain: "xxx.dkr.ecr.region-1.amazonaws.com",
44+
expectedRegion: "region-1",
45+
},
46+
"region-2": {
47+
domain: "xxx.dkr.ecr.region-2.amazonaws.com",
48+
expectedRegion: "region-2",
49+
},
50+
"fips-region-1": {
51+
domain: "xxx.dkr.ecr-fips.fips-region.amazonaws.com",
52+
expectedRegion: "fips-region",
53+
},
54+
"cn-region-1": {
55+
domain: "xxx.dkr.ecr.region-1.amazonaws.com.cn",
56+
expectedRegion: "region-1",
57+
},
58+
"cn-region-2": {
59+
domain: "xxx.dkr.ecr.region-2.amazonaws.com.cn",
60+
expectedRegion: "region-2",
61+
},
62+
"sc2s-region-1": {
63+
domain: "xxx.dkr.ecr.sc2s-region.sc2s.sgov.gov",
64+
expectedRegion: "sc2s-region",
65+
},
66+
"c2s-region-1": {
67+
domain: "xxx.dkr.ecr.c2s-region.c2s.ic.gov",
68+
expectedRegion: "c2s-region",
69+
},
70+
"invalid-ecr": {
71+
domain: "xxx.dkrecr.region-1.amazonaws.com",
72+
wantErr: types.InvalidURLPattern,
73+
},
74+
"invalid-fips": {
75+
domain: "xxx.dkr.ecrfips.fips-region.amazonaws.com",
76+
wantErr: types.InvalidURLPattern,
77+
},
78+
"invalid-cn": {
79+
domain: "xxx.dkr.ecr.region-2.amazonaws.cn",
80+
wantErr: types.InvalidURLPattern,
81+
},
82+
"invalid-sc2s": {
83+
domain: "xxx.dkr.ecr.sc2s-region.sc2s.sgov",
84+
wantErr: types.InvalidURLPattern,
85+
},
86+
"invalid-cs2": {
87+
domain: "xxx.dkr.ecr.c2s-region.c2s.ic",
88+
wantErr: types.InvalidURLPattern,
3489
},
3590
}
3691

3792
for testname, v := range tests {
3893
a := &ECR{}
39-
err := a.CheckOptions(v.domain, types.RegistryOptions{})
94+
ecrClient, err := a.CheckOptions(v.domain, types.RegistryOptions{})
4095
if err != nil {
4196
if !errors.Is(err, v.wantErr) {
4297
t.Errorf("[%s]\nexpected error based on %v\nactual : %v", testname, v.wantErr, err)
4398
}
4499
continue
45100
}
101+
102+
client := (ecrClient.(*ECRClient)).Client.(testECRClient)
103+
require.Equal(t, v.expectedRegion, client.Options().Region)
46104
}
47105
}
48106

@@ -90,7 +148,7 @@ func TestECRGetCredential(t *testing.T) {
90148
}
91149

92150
for i, c := range cases {
93-
e := ECR{
151+
e := ECRClient{
94152
Client: mockedECR{Resp: c.Resp},
95153
}
96154
username, password, err := e.GetCredential(context.Background())

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,37 @@ import (
99
"github.com/GoogleCloudPlatform/docker-credential-gcr/store"
1010
"golang.org/x/xerrors"
1111

12+
"github.com/aquasecurity/trivy/pkg/fanal/image/registry/intf"
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 gcrURLDomain = "gcr.io"
2226
const gcrURLSuffix = ".gcr.io"
2327

2428
// Google artifact registry
2529
const garURLSuffix = "-docker.pkg.dev"
2630

27-
func (g *Registry) CheckOptions(domain string, option types.RegistryOptions) error {
31+
func (g *Registry) CheckOptions(domain string, option types.RegistryOptions) (intf.RegistryClient, error) {
2832
if domain != gcrURLDomain && !strings.HasSuffix(domain, gcrURLSuffix) && !strings.HasSuffix(domain, garURLSuffix) {
29-
return xerrors.Errorf("Google registry: %w", types.InvalidURLPattern)
33+
return nil, xerrors.Errorf("Google registry: %w", types.InvalidURLPattern)
3034
}
31-
g.domain = domain
35+
client := GoogleRegistryClient{domain: domain}
3236
if option.GCPCredPath != "" {
33-
g.Store = store.NewGCRCredStore(option.GCPCredPath)
37+
client.Store = store.NewGCRCredStore(option.GCPCredPath)
3438
}
35-
return nil
39+
return &client, nil
3640
}
3741

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

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func TestCheckOptions(t *testing.T) {
1414
var tests = map[string]struct {
1515
domain string
1616
opt types.RegistryOptions
17-
gcr *Registry
17+
grc *GoogleRegistryClient
1818
wantErr error
1919
}{
2020
"InvalidURL": {
@@ -27,12 +27,12 @@ func TestCheckOptions(t *testing.T) {
2727
},
2828
"NoOption": {
2929
domain: "gcr.io",
30-
gcr: &Registry{domain: "gcr.io"},
30+
grc: &GoogleRegistryClient{domain: "gcr.io"},
3131
},
3232
"CredOption": {
3333
domain: "gcr.io",
3434
opt: types.RegistryOptions{GCPCredPath: "/path/to/file.json"},
35-
gcr: &Registry{
35+
grc: &GoogleRegistryClient{
3636
domain: "gcr.io",
3737
Store: store.NewGCRCredStore("/path/to/file.json"),
3838
},
@@ -41,7 +41,7 @@ func TestCheckOptions(t *testing.T) {
4141

4242
for testname, v := range tests {
4343
g := &Registry{}
44-
err := g.CheckOptions(v.domain, v.opt)
44+
grc, err := g.CheckOptions(v.domain, v.opt)
4545
if v.wantErr != nil {
4646
if err == nil {
4747
t.Errorf("%s : expected error but no error", testname)
@@ -52,8 +52,8 @@ func TestCheckOptions(t *testing.T) {
5252
}
5353
continue
5454
}
55-
if !reflect.DeepEqual(v.gcr, g) {
56-
t.Errorf("[%s]\nexpected : %v\nactual : %v", testname, v.gcr, g)
55+
if !reflect.DeepEqual(v.grc, grc) {
56+
t.Errorf("[%s]\nexpected : %v\nactual : %v", testname, v.grc, grc)
5757
}
5858
}
5959
}
Lines changed: 15 additions & 0 deletions
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)