diff --git a/deploy/helm/templates/config.yaml b/deploy/helm/templates/config.yaml index 7bd551128..61b2daf4c 100644 --- a/deploy/helm/templates/config.yaml +++ b/deploy/helm/templates/config.yaml @@ -63,6 +63,12 @@ data: {{- if .ignoreUnfixed }} trivy.ignoreUnfixed: {{ .ignoreUnfixed | quote }} {{- end }} + {{- if .useEcrRoleCreds }} + trivy.useEcrRoleCreds: {{ .useEcrRoleCreds | quote }} + {{- end }} + {{- if .useEcrRoleCreds }} + trivy.ecrTokenRefreshTTL: {{ .ecrTokenRefreshTTL | quote }} + {{- end }} {{- if .timeout }} trivy.timeout: {{ .timeout | quote }} {{- end }} diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index 3447e936d..edbafa4af 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -118,6 +118,18 @@ trivy: # # noProxy: + # Allows to fetch the ECR Login Token if the Service is running in AWS and the EC2-VM has an Instance-Role assigned. + # It also needs k8s KIAM (https://github.com/uswitch/kiam) to be in place which allowed Assuming Roles to specify + # dedicated permissions for the Finale Role which has the rights to get login credentials. + # + # useEcrRoleCreds: false + + # Set the TokenExpiry for the next AWS-Tokengeneration + # AWS authorization token is valid for 12 hours by default + # Unit is hours(h) + # + # ecrTokenRefreshTTL = 11h + # Registries without SSL. There can be multiple registries with different keys. nonSslRegistries: {} # pocRegistry: poc.myregistry.harbor.com.pl @@ -435,6 +447,11 @@ serviceAccount: # podAnnotations annotations added to the operator's pod podAnnotations: {} +# podAnnotations example for to get the ECR-Credentials fetch working via AWS-Roles +# +# podAnnotations: + # iam.amazonaws.com/role: + podSecurityContext: {} # fsGroup: 2000 diff --git a/go.mod b/go.mod index b49ca5aa0..9b0a7a0cd 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/aquasecurity/starboard go 1.17 require ( + github.com/aws/aws-sdk-go v1.43.30 github.com/caarlos0/env/v6 v6.9.1 github.com/davecgh/go-spew v1.1.1 github.com/emirpasic/gods v1.12.0 @@ -51,6 +52,7 @@ require ( github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-errors/errors v1.0.1 // indirect + github.com/go-ini/ini v1.66.4 // indirect github.com/go-logr/zapr v1.2.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect @@ -67,6 +69,7 @@ require ( github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect @@ -87,6 +90,7 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xhit/go-str2duration/v2 v2.0.0 // indirect github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect diff --git a/go.sum b/go.sum index 0e82a9dcd..0a57c9d2f 100644 --- a/go.sum +++ b/go.sum @@ -122,7 +122,10 @@ github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.15.11 h1:m45+Ru/wA+73cOZXiEGLDH2d9uLN3iHqMc0/z4noDXE= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/aws/aws-sdk-go v1.43.30 h1:Q3lgrX/tz/MkEiPVVQnOQThBAK2QC2SCTCKTD1mwGFA= +github.com/aws/aws-sdk-go v1.43.30/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -610,6 +613,9 @@ github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6t github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -958,6 +964,8 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xhit/go-str2duration/v2 v2.0.0 h1:uFtk6FWB375bP7ewQl+/1wBcn840GPhnySOdcz/okPE= +github.com/xhit/go-str2duration/v2 v2.0.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca h1:1CFlNzQhALwjS9mBAUkycX616GzgsuYUOCHA5+HSlXI= github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= @@ -1162,6 +1170,7 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211111083644-e5c967477495/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= diff --git a/pkg/plugin/trivy/plugin.go b/pkg/plugin/trivy/plugin.go index 151612614..b575a1e40 100644 --- a/pkg/plugin/trivy/plugin.go +++ b/pkg/plugin/trivy/plugin.go @@ -2,17 +2,26 @@ package trivy import ( "context" + "encoding/base64" "encoding/json" + "errors" "fmt" "io" + "regexp" "strings" + "time" "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1" "github.com/aquasecurity/starboard/pkg/docker" "github.com/aquasecurity/starboard/pkg/ext" "github.com/aquasecurity/starboard/pkg/kube" "github.com/aquasecurity/starboard/pkg/starboard" + "github.com/aquasecurity/starboard/pkg/utils" "github.com/aquasecurity/starboard/pkg/vulnerabilityreport" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ecr" "github.com/google/go-containerregistry/pkg/name" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -21,6 +30,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +const ( + AWSECR_Image_Regex = "^\\d+\\.dkr\\.ecr\\.(\\w+-\\w+-\\d+)\\.amazonaws\\.com\\/" +) + const ( // Plugin the name of this plugin. Plugin = "Trivy" @@ -43,6 +56,8 @@ const ( keyTrivyGitHubToken = "trivy.githubToken" keyTrivySkipFiles = "trivy.skipFiles" keyTrivySkipDirs = "trivy.skipDirs" + keyTrivyUseECRRoleCreds = "trivy.useEcrRoleCreds" + keyTrivyECRTokenRefreshTTL = "trivy.ecrTokenRefreshTTL" keyTrivyServerURL = "trivy.serverURL" keyTrivyServerTokenHeader = "trivy.serverTokenHeader" @@ -64,6 +79,11 @@ const ( ClientServer Mode = "ClientServer" ) +type ecr_credentials struct { + username string + password string +} + // Command to scan image or filesystem. type Command string @@ -121,6 +141,17 @@ func (c Config) GetServerURL() (string, error) { return c.GetRequiredData(keyTrivyServerURL) } +func (c Config) UseECRCredentials() bool { + if val, ok := c.Data[keyTrivyUseECRRoleCreds]; ok && val == "true" { + return true + } + return false +} + +func (c Config) GetECRRefreshTTL() (string, error) { + return c.GetRequiredData(keyTrivyECRTokenRefreshTTL) +} + func (c Config) GetServerInsecure() bool { _, ok := c.Data[keyTrivyServerInsecure] return ok @@ -215,6 +246,8 @@ type plugin struct { clock ext.Clock idGenerator ext.IDGenerator objectResolver *kube.ObjectResolver + aws_creds [][]string + ecr_tokengen time.Time } // NewPlugin constructs a new vulnerabilityreport.Plugin, which is using an @@ -574,31 +607,58 @@ func (p *plugin) getPodSpecForStandaloneMode(ctx starboard.PluginContext, config }) } - if _, ok := credentials[c.Name]; ok && secret != nil { - registryUsernameKey := fmt.Sprintf("%s.username", c.Name) - registryPasswordKey := fmt.Sprintf("%s.password", c.Name) + TTLResult, err := config.GetECRRefreshTTL() + if err != nil { + TTLResult = "0" + } + + if config.UseECRCredentials() && CheckAwsEcrPrivateRegistry(c.Image) != "" { + fmt.Println("TOKEN PRINT: ", p.ecr_tokengen, " - ", TTLResult) + if utils.TokenTTLValidation(p.ecr_tokengen, TTLResult) { + p.aws_creds, err = GetAuthorizationToken(CheckAwsEcrPrivateRegistry(c.Image)) + if err != nil { + return corev1.PodSpec{}, nil, err + } + p.ecr_tokengen = p.clock.Now() + } + + var creds = ecr_credentials{p.aws_creds[0][1], p.aws_creds[0][2]} env = append(env, corev1.EnvVar{ - Name: "TRIVY_USERNAME", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: secret.Name, + Name: "TRIVY_USERNAME", + Value: creds.username, + }) + env = append(env, corev1.EnvVar{ + Name: "TRIVY_PASSWORD", + Value: creds.password, + }) + } else { + if _, ok := credentials[c.Name]; ok && secret != nil { + registryUsernameKey := fmt.Sprintf("%s.username", c.Name) + registryPasswordKey := fmt.Sprintf("%s.password", c.Name) + + env = append(env, corev1.EnvVar{ + Name: "TRIVY_USERNAME", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secret.Name, + }, + Key: registryUsernameKey, }, - Key: registryUsernameKey, }, - }, - }, corev1.EnvVar{ - Name: "TRIVY_PASSWORD", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: secret.Name, + }, corev1.EnvVar{ + Name: "TRIVY_PASSWORD", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secret.Name, + }, + Key: registryPasswordKey, }, - Key: registryPasswordKey, }, - }, - }) + }) + } } env, err = p.appendTrivyInsecureEnv(config, c.Image, env) @@ -839,31 +899,58 @@ func (p *plugin) getPodSpecForClientServerMode(ctx starboard.PluginContext, conf }, } - if _, ok := credentials[container.Name]; ok && secret != nil { - registryUsernameKey := fmt.Sprintf("%s.username", container.Name) - registryPasswordKey := fmt.Sprintf("%s.password", container.Name) + TTLResult, err := config.GetECRRefreshTTL() + if err != nil { + TTLResult = "0" + } + + if config.UseECRCredentials() && CheckAwsEcrPrivateRegistry(container.Image) != "" { + + if utils.TokenTTLValidation(p.ecr_tokengen, TTLResult) { + p.aws_creds, err = GetAuthorizationToken(CheckAwsEcrPrivateRegistry(container.Image)) + if err != nil { + return corev1.PodSpec{}, nil, err + } + p.ecr_tokengen = p.clock.Now() + } + + var creds = ecr_credentials{p.aws_creds[0][1], p.aws_creds[0][2]} env = append(env, corev1.EnvVar{ - Name: "TRIVY_USERNAME", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: secret.Name, + Name: "TRIVY_USERNAME", + Value: creds.username, + }) + env = append(env, corev1.EnvVar{ + Name: "TRIVY_PASSWORD", + Value: creds.password, + }) + } else { + if _, ok := credentials[container.Name]; ok && secret != nil { + registryUsernameKey := fmt.Sprintf("%s.username", container.Name) + registryPasswordKey := fmt.Sprintf("%s.password", container.Name) + + env = append(env, corev1.EnvVar{ + Name: "TRIVY_USERNAME", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secret.Name, + }, + Key: registryUsernameKey, }, - Key: registryUsernameKey, }, - }, - }, corev1.EnvVar{ - Name: "TRIVY_PASSWORD", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: secret.Name, + }, corev1.EnvVar{ + Name: "TRIVY_PASSWORD", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secret.Name, + }, + Key: registryPasswordKey, }, - Key: registryPasswordKey, }, - }, - }) + }) + } } env, err = p.appendTrivyInsecureEnv(config, container.Image, env) @@ -1365,3 +1452,48 @@ func constructEnvVarSourceFromConfigMap(envName, configName, configKey string) ( } return } + +func CheckAwsEcrPrivateRegistry(ImageUrl string) string { + if len(regexp.MustCompile(AWSECR_Image_Regex).FindAllStringSubmatch(ImageUrl, -1)) != 0 { + return regexp.MustCompile(AWSECR_Image_Regex).FindAllStringSubmatch(ImageUrl, -1)[0][1] + } + return "" +} + +func GetAuthorizationToken(AwsEcrRegion string) ([][]string, error) { + + svc := ecr.New(session.Must(session.NewSession()), aws.NewConfig().WithRegion(AwsEcrRegion)) + input := &ecr.GetAuthorizationTokenInput{} + + result, err := svc.GetAuthorizationToken(input) + if err != nil { + var errormsg string + if aerr, ok := err.(awserr.Error); ok { + switch aerr.Code() { + case ecr.ErrCodeServerException: + errormsg = "GetAuthorizationToken (AWS-API): " + ecr.ErrCodeServerException + ", " + aerr.Error() + case ecr.ErrCodeInvalidParameterException: + errormsg = "GetAuthorizationToken (AWS-API): " + ecr.ErrCodeInvalidParameterException + ", " + aerr.Error() + default: + errormsg = "GetAuthorizationToken (AWS-API): " + aerr.Error() + } + } else { + errormsg = err.Error() + } + return nil, errors.New(errormsg) + } + + var credentials = *result.AuthorizationData[0].AuthorizationToken + + if len(credentials) > 0 { + sDec, err := base64.StdEncoding.DecodeString(credentials) + if err != nil { + return nil, errors.New("GetAuthorizationToken: Error during retrival and base64 decoding operation.") + } else { + pattern := regexp.MustCompile("^(AWS):(.+)$") + return pattern.FindAllStringSubmatch(string(sDec), -1), nil + } + } else { + return nil, errors.New("GetAuthorizationToken: Error during retrival and base64 decoding operation.") + } +} diff --git a/pkg/utils/dateutil.go b/pkg/utils/dateutil.go index 3e87f1855..dae5e45e5 100644 --- a/pkg/utils/dateutil.go +++ b/pkg/utils/dateutil.go @@ -1,9 +1,12 @@ package utils import ( + "time" + "github.com/aquasecurity/starboard/pkg/ext" "github.com/gorhill/cronexpr" - "time" + + "github.com/xhit/go-str2duration/v2" ) // NextCronDuration check if next cron activation time has exceeded if so return true @@ -32,3 +35,16 @@ func IsTTLExpired(ttl time.Duration, creationTime time.Time, clock ext.Clock) (b durationToTTLExpiration := timeToExpiration(creationTime.Add(ttl), clock) return DurationExceeded(durationToTTLExpiration), durationToTTLExpiration } + +func TokenTTLValidation(TokenGen time.Time, TokenTTL string) bool { + duration, err := str2duration.ParseDuration(TokenTTL) + + if err != nil { + return false + } + if time.Now().Sub(TokenGen) >= duration { + return true + } + return false + +}