├── NOTICE ├── screenshots └── architecture.png ├── .gitignore ├── .wwhrd.yml ├── CODE_OF_CONDUCT.md ├── go.mod ├── deploy ├── mydeployment.yaml └── validatingwebhook.yaml ├── pkg ├── webhook │ ├── plugin.go │ ├── parse_test.go │ ├── response.go │ └── request.go └── function │ ├── middleware.go │ ├── container.go │ └── ecr.go ├── scripts └── publish.sh ├── main.go ├── Makefile ├── CONTRIBUTING.md ├── template.yaml ├── testdata └── testdata.go ├── main_test.go ├── LICENSE ├── README.md ├── THIRD-PARTY-LICENSES └── go.sum /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -------------------------------------------------------------------------------- /screenshots/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ecr-repository-compliance-webhook/HEAD/screenshots/architecture.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | main 15 | packaged.yaml 16 | .idea/ 17 | -------------------------------------------------------------------------------- /.wwhrd.yml: -------------------------------------------------------------------------------- 1 | # github.com/frapposelli/wwhrd 2 | # wwhrd check 3 | 4 | whitelist: 5 | - Apache-2.0 6 | - MIT 7 | - BSD-2-Clause 8 | - BSD-3-Clause 9 | 10 | exceptions: 11 | - github.com/aws/aws-xray-sdk-go/... # Uses Apache-2.0 12 | - sigs.k8s.io/yaml/... # Uses MIT 13 | - github.com/gogo/protobuf/... # Uses 3-clause BSD -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws-samples/amazon-ecr-repository-compliance-webhook 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/aws/aws-lambda-go v1.16.0 7 | github.com/aws/aws-sdk-go v1.30.26 8 | github.com/aws/aws-xray-sdk-go v1.0.1 9 | github.com/sirupsen/logrus v1.6.0 10 | github.com/stretchr/objx v0.1.1 // indirect 11 | github.com/stretchr/testify v1.7.0 12 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 13 | k8s.io/api v0.25.0 14 | k8s.io/apimachinery v0.25.0 15 | ) 16 | -------------------------------------------------------------------------------- /deploy/mydeployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: test 5 | namespace: test-namespace 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: test 10 | replicas: 2 11 | template: 12 | metadata: 13 | labels: 14 | app: test 15 | spec: 16 | containers: 17 | - name: test 18 | image: nginx:latest # Also test with ECR images you own: example aws_account_id.dkr.ecr.region.amazonaws.com/image-name:latest 19 | ports: 20 | - containerPort: 80 -------------------------------------------------------------------------------- /pkg/webhook/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package webhook contains resources for the ValidatingWebhookConfiguration. 5 | // Referenced: https://github.com/kubernetes/kubernetes/blob/v1.15.0/test/images/webhook 6 | package webhook 7 | 8 | import ( 9 | admissionv1beta1 "k8s.io/api/admission/v1beta1" 10 | admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" 11 | corev1 "k8s.io/api/core/v1" 12 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 13 | ) 14 | 15 | func init() { 16 | // Permissive plugin-or-panic for adding runtime schemas 17 | utilruntime.Must(corev1.AddToScheme(runtimeScheme)) 18 | utilruntime.Must(admissionv1beta1.AddToScheme(runtimeScheme)) 19 | utilruntime.Must(admissionregistrationv1beta1.AddToScheme(runtimeScheme)) 20 | } 21 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BUCKET_PREFIX=swoldemi 4 | APPLICATION=amazon-ecr-repository-compliance-webhook 5 | ACCOUNT=273450712882 6 | readonly REGIONS=( 7 | "us-east-2" 8 | "us-east-1" 9 | "us-west-1" 10 | "us-west-2" 11 | "ap-east-1" 12 | "ap-south-1" 13 | "ap-northeast-2" 14 | "ap-southeast-1" 15 | "ap-southeast-2" 16 | "ap-northeast-1" 17 | "ca-central-1" 18 | "eu-central-1" 19 | "eu-west-1" 20 | "eu-west-2" 21 | "eu-west-3" 22 | "eu-north-1" 23 | "sa-east-1" 24 | ) 25 | 26 | publish_all_regions() 27 | { 28 | for REGION in "${REGIONS[@]}" 29 | do 30 | echo Deploying to region "$REGION" 31 | sam package --template-file template.yaml --s3-bucket $BUCKET_PREFIX-$REGION --output-template-file packaged.yaml 32 | sam publish --region "$REGION" --template packaged.yaml 33 | done 34 | 35 | aws serverlessrepo put-application-policy \ 36 | --region us-east-1 \ 37 | --application-id arn:aws:serverlessrepo:us-east-1:$ACCOUNT:applications/$APPLICATION \ 38 | --statements Principals=*,Actions=Deploy 39 | } 40 | 41 | echo "On branch $(basename $CODEBUILD_WEBHOOK_HEAD_REF)" 42 | if [ "$(basename $CODEBUILD_WEBHOOK_HEAD_REF)" = "master" ] 43 | then 44 | publish_all_regions 45 | else 46 | echo Skipping publish 47 | fi 48 | -------------------------------------------------------------------------------- /pkg/function/middleware.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package function 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | 10 | "github.com/aws/aws-lambda-go/events" 11 | log "github.com/sirupsen/logrus" 12 | "k8s.io/api/admission/v1beta1" 13 | ) 14 | 15 | // Handler is a type alias for the Lambda handler's function signature. 16 | type Handler func(context.Context, events.APIGatewayProxyRequest) (*v1beta1.AdmissionReview, error) 17 | 18 | // ProxiedHandler is a handler that has been wrapped to respond with an API Gateway Proxy Integration. 19 | type ProxiedHandler func(ctx context.Context, event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) 20 | 21 | // WithLogging is a logging middleware for the Lambda handler. 22 | func (h Handler) WithLogging() Handler { 23 | return func(ctx context.Context, event events.APIGatewayProxyRequest) (*v1beta1.AdmissionReview, error) { 24 | review, err := h(ctx, event) 25 | log.Infof("Responding with AdmissionReview [%+v] and error [%v]", review, err) 26 | return review, err 27 | } 28 | } 29 | 30 | // WithProxiedResponse integrates the AdmissionReview response into an acceptable format 31 | // for API Gateway proxy integrated Lambda functions. 32 | func (h Handler) WithProxiedResponse() ProxiedHandler { 33 | return func(ctx context.Context, event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 34 | response := events.APIGatewayProxyResponse{ 35 | Headers: map[string]string{"Content-Type": "application/json"}, 36 | } 37 | review, err := h(ctx, event) 38 | if err != nil { 39 | response.Body = err.Error() 40 | response.StatusCode = 500 41 | return response, err 42 | } 43 | body, err := json.Marshal(review) 44 | if err != nil { 45 | response.Body = err.Error() 46 | response.StatusCode = 500 47 | return response, err 48 | } 49 | response.Body = string(body) 50 | response.StatusCode = 200 // Not to be confused with the status code in the AdmissionResponse 51 | return response, nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/webhook/parse_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package webhook 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/aws-samples/amazon-ecr-repository-compliance-webhook/testdata" 11 | corev1 "k8s.io/api/core/v1" 12 | ) 13 | 14 | func TestParseRepositories(t *testing.T) { 15 | var ( 16 | untaggedImagePod = newPodWithImage(testdata.UntaggedImage) 17 | taggedImagePod = newPodWithImage(testdata.TaggedImage) 18 | cnImagePod = newPodWithImage(testdata.CNImage) 19 | fipsImagePod = newPodWithImage(testdata.FIPSImage) 20 | duplicateImagesPod = newPodWithImage(testdata.TaggedImage) 21 | twoImagesPod = newPodWithImage(testdata.TaggedImage) 22 | noNamespacePod = newPodWithImage(testdata.NoNamespace) 23 | aliasedImagePod = newPodWithImage(testdata.AliasedImage) 24 | noImages = newPodWithImage("") 25 | badImage = newPodWithImage("elgoog/sselortsid") 26 | ) 27 | duplicateImagesPod.Spec.Containers = append(duplicateImagesPod.Spec.Containers, duplicateImagesPod.Spec.Containers...) 28 | twoImagesPod.Spec.Containers = append(twoImagesPod.Spec.Containers, untaggedImagePod.Spec.Containers...) 29 | tests := []struct { 30 | name string 31 | pod *corev1.Pod 32 | want []string 33 | }{ 34 | {"UntaggedImage", untaggedImagePod, []string{"namespace/repo@sha256:e5e2a3236e64483c50dd2811e46e9cd49c67e82271e60d112ca69a075fc23005"}}, 35 | {"TaggedImage", taggedImagePod, []string{"namespace/repo:40d6072"}}, 36 | {"CNImage", cnImagePod, []string{"namespace/repo:40d6072"}}, 37 | {"FIPSImage", fipsImagePod, []string{"namespace/repo:40d6072"}}, 38 | {"Duplicates", duplicateImagesPod, []string{"namespace/repo:40d6072"}}, 39 | {"TwoImages", twoImagesPod, []string{"namespace/repo:40d6072", "namespace/repo@sha256:e5e2a3236e64483c50dd2811e46e9cd49c67e82271e60d112ca69a075fc23005"}}, 40 | {"NoNamespace", noNamespacePod, []string{"repo:40d6072"}}, 41 | {"Aliased", aliasedImagePod, []string{"namespace/repo:40d6072"}}, 42 | {"NoImages", noImages, nil}, 43 | {"BadImage", badImage, nil}, 44 | } 45 | for _, tt := range tests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | if got := ParseImages(tt.pod); !reflect.DeepEqual(got, tt.want) { 48 | t.Errorf("ParseRepositories() = %v, want %v", got, tt.want) 49 | } 50 | }) 51 | } 52 | } 53 | 54 | func newPodWithImage(image string) *corev1.Pod { 55 | return &corev1.Pod{ 56 | Spec: corev1.PodSpec{ 57 | Containers: []corev1.Container{ 58 | { 59 | Image: image, 60 | }, 61 | }, 62 | }, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Licensed under the Apache License, Version 2.0 (the "License"). 4 | You may not use this file except in compliance with the License. 5 | A copy of the License is located at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | or in the "license" file accompanying this file. This file is distributed 8 | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 9 | express or implied. See the License for the specific language governing 10 | permissions and limitations under the License. 11 | */ 12 | 13 | package main 14 | 15 | import ( 16 | "os" 17 | 18 | "github.com/aws-samples/amazon-ecr-repository-compliance-webhook/pkg/function" 19 | "github.com/aws/aws-lambda-go/lambda" 20 | "github.com/aws/aws-sdk-go/aws" 21 | "github.com/aws/aws-sdk-go/aws/session" 22 | "github.com/aws/aws-sdk-go/service/ecr" 23 | "github.com/aws/aws-xray-sdk-go/xray" 24 | "github.com/aws/aws-xray-sdk-go/xraylog" 25 | log "github.com/sirupsen/logrus" 26 | ) 27 | 28 | func init() { 29 | xraylvl, loglvl := logLevels(os.Getenv("LOG_LEVEL")) 30 | log.SetFormatter(new(log.JSONFormatter)) 31 | log.Infof("Got log levels [%s, %s]", xraylvl, loglvl) 32 | log.SetLevel(loglvl) 33 | xray.SetLogger(xraylog.NewDefaultLogger(os.Stdout, xraylvl)) 34 | } 35 | 36 | var ( 37 | sess = xray.AWSSession(session.Must(session.NewSession())) 38 | svc = ecr.New(sess, &aws.Config{Region: getRegistryRegion()}) 39 | 40 | // Handler is the handler for the validating webhook. 41 | Handler = function.NewContainer(svc).Handler().WithLogging().WithProxiedResponse() 42 | 43 | // Version is the shortened git hash of the binary's source code. 44 | // It is injected using the -X linker flag when running `make` 45 | Version string 46 | ) 47 | 48 | func main() { 49 | log.Infof("Starting function version: %s", Version) 50 | lambda.Start(Handler) 51 | } 52 | 53 | func logLevels(lvl string) (xraylog.LogLevel, log.Level) { 54 | loglvl, err := log.ParseLevel(lvl) 55 | if err != nil { 56 | return xraylog.LogLevelInfo, log.InfoLevel 57 | } 58 | 59 | var xraylvl xraylog.LogLevel 60 | switch lvl { 61 | case "DEBUG": 62 | xraylvl = xraylog.LogLevelDebug 63 | case "INFO": 64 | xraylvl = xraylog.LogLevelInfo 65 | case "WARN": 66 | xraylvl = xraylog.LogLevelWarn 67 | case "ERROR": 68 | xraylvl = xraylog.LogLevelError 69 | default: 70 | xraylvl = xraylog.LogLevelInfo 71 | } 72 | return xraylvl, loglvl 73 | } 74 | 75 | func getRegistryRegion() *string { 76 | if value, ok := os.LookupEnv("REGISTRY_REGION"); ok { 77 | return aws.String(value) 78 | } 79 | return aws.String(os.Getenv("AWS_REGION")) 80 | } 81 | -------------------------------------------------------------------------------- /deploy/validatingwebhook.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1beta1 2 | kind: ValidatingWebhookConfiguration 3 | metadata: 4 | name: amazon-ecr-repository-compliance-webhook-config 5 | labels: 6 | app: amazon-ecr-repository-compliance-webhook 7 | webhooks: 8 | - name: admission.ecr.amazonaws.com 9 | timeoutSeconds: 8 10 | failurePolicy: Fail # v1beta1 default is Ignore 11 | clientConfig: 12 | url: https://.execute-api..amazonaws.com/Prod/check-image-compliance 13 | caBundle: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQ3ekNDQXRlZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBRENCbURFTE1Ba0dBMVVFQmhNQ1ZWTXgKRURBT0JnTlZCQWdUQjBGeWFYcHZibUV4RXpBUkJnTlZCQWNUQ2xOamIzUjBjMlJoYkdVeEpUQWpCZ05WQkFvVApIRk4wWVhKbWFXVnNaQ0JVWldOb2JtOXNiMmRwWlhNc0lFbHVZeTR4T3pBNUJnTlZCQU1UTWxOMFlYSm1hV1ZzClpDQlRaWEoyYVdObGN5QlNiMjkwSUVObGNuUnBabWxqWVhSbElFRjFkR2h2Y21sMGVTQXRJRWN5TUI0WERUQTUKTURrd01UQXdNREF3TUZvWERUTTNNVEl6TVRJek5UazFPVm93Z1pneEN6QUpCZ05WQkFZVEFsVlRNUkF3RGdZRApWUVFJRXdkQmNtbDZiMjVoTVJNd0VRWURWUVFIRXdwVFkyOTBkSE5rWVd4bE1TVXdJd1lEVlFRS0V4eFRkR0Z5ClptbGxiR1FnVkdWamFHNXZiRzluYVdWekxDQkpibU11TVRzd09RWURWUVFERXpKVGRHRnlabWxsYkdRZ1UyVnkKZG1salpYTWdVbTl2ZENCRFpYSjBhV1pwWTJGMFpTQkJkWFJvYjNKcGRIa2dMU0JITWpDQ0FTSXdEUVlKS29aSQpodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU5VTU9zUXErVTdpOWI0WmwxK09pRk94SHovTHo1OGdFMjBwCk9zZ1BmVHozYTNZNFk5azJZS2liWGx3QWdMSXZXWC8yaC9rbFE0Ym5hUnRTbXBEaGNlUFlMUTFPYi9iSVNkbTIKOHhwV3JpdTJkQlRyei9zbTR4cTZIWll1YWp0WWxJbEhWdjhsb0pOd1U0UGFoSFFVdzJlZUJHZzYzNDVBV2gxSwpUczlEa1R2blZ0WUFjTXRTN250OXJqcm52REg1UmZiQ1lNOFRXUUlyZ013MFI5KzUzcEJsYlFMUExKR21wdWZlCmhSaEpmR1pPb3pwdHFiWHVOQzY2RFFPNE05OUg2N0ZyalNYWm04NkIwVVZHTXBad2g5NENEa2xEaGJac2M3dGsKNm1GQnJNblVWTitITDhjaXNpYk1uMWxVYUovOHZpb3Z4RlVjZFVCZ0Y0VUNWVG1MZndVQ0F3RUFBYU5DTUVBdwpEd1lEVlIwVEFRSC9CQVV3QXdFQi96QU9CZ05WSFE4QkFmOEVCQU1DQVFZd0hRWURWUjBPQkJZRUZKeGZBTitxCkFkY3dLemlJb3JodFNwenlFWkdETUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFCTE5xYUVkMm5kT3htZlp5TUkKYnc1aHlmMkUzRi9ZTm9ITjJCdEJMWjlnM2NjYWFOblJib2JoaUNQUEU5NUR6K0kwc3dTZEh5blZ2L2hleU5YQgp2ZTZTYnpKMDhwR0NMNzJDUW5xdEtyY2dmVTI4ZWxVU3doWHF2ZmRxbFM1c2RKL1BITFR5eFFHamhkQnlQcTF6CnF3dWJkUXh0UmJlT2xLeVdON1dnMEk4VlJ3N2o2SVBkai8zdlFRRjN6Q2VwWW9VejhqY0k3M0hQZHdiZXlCa2QKaUVEUGZVWWQveDdINGM3L0k5dkcrbzFWVHFrQzUwY1JSajcwL2IxN0tTYTdxV0ZpTnlpMkxTcjJFSVpreVhDbgowcTIzS1hCNTZqemFZeVdmL1dpM01PeHcrM1dLdDIxZ1o3SWV5TG5wMktodkFvdG5EVTBtVjNIYUlQekJTbENOCnNTaTYKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==" 14 | rules: 15 | - apiGroups: [""] 16 | apiVersions: ["v1"] 17 | operations: ["CREATE"] 18 | resources: ["pods"] 19 | # namespaceSelector: # Optionally add a namespaceSelector and matchLabels 20 | # matchLabels: 21 | # amazon-ecr-repository-compliance-webhook: enabled 22 | -------------------------------------------------------------------------------- /pkg/webhook/response.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package webhook 5 | 6 | import ( 7 | "errors" 8 | 9 | v1beta1 "k8s.io/api/admission/v1beta1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ) 12 | 13 | // Errors returned when a bad request is received or a failure reason is not provided. 14 | var ( 15 | ErrMissingFailure = errors.New("webhook: reached invalid state, no failure reason found") 16 | ErrBadRequest = errors.New("webhook: bad request") 17 | ) 18 | 19 | // BadRequestResponse is the response returned to the cluster when a bad request is sent. 20 | func BadRequestResponse(err error) (*v1beta1.AdmissionReview, error) { 21 | response := &v1beta1.AdmissionResponse{ 22 | Allowed: false, 23 | Result: &metav1.Status{ 24 | Status: metav1.StatusFailure, 25 | Message: err.Error(), 26 | Reason: metav1.StatusReasonBadRequest, 27 | Code: 400, 28 | }, 29 | } 30 | return respond(response), nil 31 | } 32 | 33 | // Response encapsulates the AdmissionResponse sent to API Gateway. 34 | type Response struct { 35 | Admission *v1beta1.AdmissionResponse 36 | } 37 | 38 | // NewResponseFromRequest creates a Response from a Request. 39 | func NewResponseFromRequest(r *Request) (*Response, error) { 40 | if r == nil || r.Admission == nil { 41 | return nil, ErrBadRequest 42 | } 43 | if r.Admission != nil && r.Admission.UID == "" { 44 | return nil, ErrBadRequest 45 | } 46 | return &Response{ 47 | Admission: &v1beta1.AdmissionResponse{ 48 | UID: r.Admission.UID, 49 | }, 50 | }, nil 51 | } 52 | 53 | // FailValidation populates the AdmissionResponse with the failure contents 54 | // (message and error) and returns the AdmissionReview JSON body response for API Gateway. 55 | func (r *Response) FailValidation(code int32, failure error) (*v1beta1.AdmissionReview, error) { 56 | if failure == nil { 57 | return nil, ErrMissingFailure 58 | } 59 | 60 | r.Admission.Allowed = false 61 | r.Admission.Result = &metav1.Status{ 62 | Status: metav1.StatusFailure, 63 | Message: failure.Error(), 64 | // Need a better way to Code with Reason; maybe use gRPC code mappings? 65 | Reason: metav1.StatusReasonNotAcceptable, 66 | Code: code, 67 | } 68 | return respond(r.Admission), nil 69 | } 70 | 71 | // PassValidation populates the AdmissionResponse with the pass contents 72 | // (message) and returns the AdmissionReview JSON response for API Gateway. 73 | func (r *Response) PassValidation() *v1beta1.AdmissionReview { 74 | r.Admission.Allowed = true 75 | r.Admission.Result = &metav1.Status{ 76 | Status: metav1.StatusSuccess, 77 | Message: "pod contains compliant ecr repositories and images", 78 | Code: 200, 79 | } 80 | return respond(r.Admission) 81 | } 82 | 83 | func respond(admission *v1beta1.AdmissionResponse) *v1beta1.AdmissionReview { 84 | return &v1beta1.AdmissionReview{ 85 | TypeMeta: metav1.TypeMeta{ 86 | Kind: "AdmissionReview", 87 | APIVersion: "admission.k8s.io/v1beta1", 88 | }, 89 | Response: admission, 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | S3_BUCKET=swoldemi-tmp # Replace with your S3 bucket 2 | DEFAULT_REGION=us-east-2 # Replace with your region 3 | 4 | FUNCTION_NAME=amazon-ecr-repository-compliance-webhook 5 | COVERAGE=coverage.out 6 | COVERAGE_REPORT=report.html 7 | BINARY=main 8 | TEMPLATE=template.yaml 9 | PACKAGED_TEMPLATE=packaged.yaml 10 | unit-test=${COVERAGE} 11 | compile=${BINARY} 12 | sam-package=${PACKAGED_TEMPLATE} 13 | 14 | VERSION=$(shell git describe --always --tags) 15 | LINKER_FLAGS=-X main.Version=${VERSION} 16 | # RELEASE_BUILD_LINKER_FLAGS disables DWARF and symbol table generation to reduce binary size 17 | # See the `link` command's documentation here: https://golang.org/cmd/link/ 18 | RELEASE_BUILD_LINKER_FLAGS=-s -w 19 | 20 | .PHONY: all 21 | all: build 22 | 23 | .PHONY: build 24 | build: lint test compile 25 | 26 | compile: 27 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v \ 28 | -ldflags "${LINKER_FLAGS} ${RELEASE_BUILD_LINKER_FLAGS}" \ 29 | -o ${BINARY} main.go 30 | 31 | .PHONY: test 32 | test: unit-test 33 | 34 | unit-test: 35 | go test -v -timeout 30s -count 1 -covermode=count -coverprofile ${COVERAGE} ./... 36 | 37 | sam-package: $(BINARY) $(TEMPALTE) 38 | sam package --region ${DEFAULT_REGION} \ 39 | --template-file ${TEMPLATE} \ 40 | --s3-bucket ${S3_BUCKET} \ 41 | --output-template-file ${PACKAGED_TEMPLATE} 42 | 43 | generate-coverage: $(COVERAGE) 44 | go tool cover -html ${COVERAGE} -o ${COVERAGE_REPORT} 45 | python -m SimpleHTTPServer 8000 -o ${COVERAGE_REPORT} 46 | 47 | .PHONY: lint 48 | lint: 49 | sam validate --template-file ${TEMPLATE} 50 | gofumports -w -l -e . && gofumpt -s -w . 51 | 52 | .PHONY: sam-deploy 53 | sam-deploy: $(TEMPLATE) 54 | sam deploy --region ${DEFAULT_REGION} \ 55 | --template-file ${TEMPLATE} \ 56 | --s3-bucket ${S3_BUCKET} \ 57 | --stack-name ${FUNCTION_NAME} \ 58 | --capabilities CAPABILITY_IAM 59 | 60 | .PHONY: sam-publish 61 | sam-publish: $(PACKAGED_TEMPLATE) 62 | # See ./scripts/publish.sh 63 | sam publish --template ${PACKAGED_TEMPLATE} --region ${DEFAULT_REGION} 64 | 65 | .PHONY: sam-logs 66 | sam-logs: 67 | sam logs --name ${FUNCTION_NAME} --tail --region ${DEFAULT_REGION} 68 | 69 | .PHONY: destroy-stack 70 | destroy-stack: 71 | aws cloudformation delete-stack --stack-name ${FUNCTION_NAME} --region ${DEFAULT_REGION} 72 | aws cloudformation wait stack-delete-complete --stack-name ${FUNCTION_NAME} --region ${DEFAULT_REGION} 73 | 74 | .PHONY: get-deps 75 | get-deps: 76 | go get -d $(shell go list -f "{{if not (or .Main .Indirect)}}{{.Path}}{{end}}" -m all) 77 | go mod tidy && go mod verify 78 | 79 | .PHONY: clean 80 | clean: 81 | - rm -f ${BINARY} ${COVERAGE} ${COVERAGE_REPORT} ${PACKAGED_TEMPLATE} 82 | 83 | .PHONY: install-tools 84 | install-tools: 85 | pip3 install -U aws-sam-cli aws-sam-translator 86 | 87 | # Hacky workaround to prevent adding these tools to our go.mod; 88 | # see https://github.com/golang/go/issues/37225 and https://github.com/golang/go/issues/30515#issuecomment-582044819 89 | (cd ..; GO111MODULE=on go get mvdan.cc/gofumpt/gofumports) 90 | (cd ..; GO111MODULE=on go get mvdan.cc/gofumpt) 91 | (cd ..; GO111MODULE=on go get github.com/frapposelli/wwhrd) 92 | 93 | .PHONY: manual-qa 94 | manual-qa: 95 | kubectl delete deployment test -n test-namespace --ignore-not-found && \ 96 | kubectl apply -f deploy/mydeployment.yaml && \ 97 | kubectl get ev -n test-namespace --sort-by .metadata.creationTimestamp 98 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /pkg/function/container.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package function contains library units for the amazon-ecr-repository-compliance-webhook Lambda function. 5 | package function 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | 11 | "github.com/aws-samples/amazon-ecr-repository-compliance-webhook/pkg/webhook" 12 | "github.com/aws/aws-lambda-go/events" 13 | "github.com/aws/aws-sdk-go/service/ecr/ecriface" 14 | log "github.com/sirupsen/logrus" 15 | "k8s.io/api/admission/v1beta1" 16 | ) 17 | 18 | // Errors returned when a validation expectation fails. 19 | var ( 20 | ErrFailedCompliance = errors.New("webhook: repository fails ecr criteria") 21 | ErrImagesNotFound = errors.New("webhook: no ecr images found in pod specification") 22 | ) 23 | 24 | // Container contains the dependencies and business logic for the amazon-ecr-repository-compliance-webhook Lambda function. 25 | type Container struct { 26 | ECR ecriface.ECRAPI 27 | } 28 | 29 | // NewContainer creates a new function Container. 30 | func NewContainer(ecrSvc ecriface.ECRAPI) *Container { 31 | return &Container{ 32 | ECR: ecrSvc, 33 | } 34 | } 35 | 36 | // default HTTP status code to return on rejected admission 37 | const code = 406 // NotAcceptable 38 | 39 | // Handler returns the function handler for the amazon-ecr-repository-compliance-webhook. 40 | // 1. Extract the POST request's body that ValidatingWebhookConfiguration admission controller made to API Gateway 41 | // 2. Using the request, create a response. The response must contain the same UID that we received from the cluster 42 | // 3. Using the request, extract the Pod object into the same Go data type used by Kubernetes 43 | // 4. Using the Pod, check if the requested creation namespace is a critical one (e.g. kube-system). 44 | // 5. Using the Pod, extract all of the unique container images that are in the specification 45 | // - If no images in the specification come from ECR, deny the admission immediately 46 | // 6. For every image provided, check our 4 requirements 47 | // 7. If a single image didn't meet our requirements, deny the admission 48 | // 8. All requirements satisfied, allow the Pod for admission 49 | func (c *Container) Handler() Handler { 50 | return func(ctx context.Context, event events.APIGatewayProxyRequest) (*v1beta1.AdmissionReview, error) { 51 | request, err := webhook.NewRequestFromEvent(event) // 1 52 | if err != nil { 53 | log.Errorf("Error creating request from event: %v", err) 54 | return webhook.BadRequestResponse(err) 55 | } 56 | 57 | response, err := webhook.NewResponseFromRequest(request) // 2 58 | if err != nil { 59 | log.Errorf("Error crafting response from request: %v", err) 60 | return webhook.BadRequestResponse(err) 61 | } 62 | 63 | pod, err := request.UnmarshalPod() // 3 64 | if err != nil { 65 | log.Errorf("Error unmarshalling Pod: %v", err) 66 | return response.FailValidation(code, err) 67 | } 68 | 69 | if webhook.InCriticalNamespace(pod) { // 4 70 | log.Info("Pod is in critical namespace, automatically passing") 71 | return response.PassValidation(), nil 72 | } 73 | 74 | images := webhook.ParseImages(pod) // 5 75 | if len(images) == 0 { 76 | log.Error(ErrImagesNotFound) 77 | return response.FailValidation(code, ErrImagesNotFound) 78 | } 79 | 80 | compliant, err := c.BatchCheckRepositoryCompliance(ctx, images) // 6 81 | if err != nil { 82 | log.Errorf("Error during compliance check: %v", err) 83 | return response.FailValidation(code, err) 84 | } 85 | 86 | if !compliant { // 7 87 | log.Error("Repository is not compliant") 88 | return response.FailValidation(code, ErrFailedCompliance) 89 | } 90 | return response.PassValidation(), nil // 8 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: | 4 | A Kubernetes ValidatingWebhookConfiguration and serverless backend: 5 | Deny Pods with container images that don't meet your compliance requirements. 6 | 7 | Metadata: 8 | AWS::ServerlessRepo::Application: 9 | Name: amazon-ecr-repository-compliance-webhook 10 | Description: "A Kubernetes ValidatingWebhookConfiguration and serverless backend: Deny Pods with container images that don't meet your compliance requirements" 11 | Author: Simon Woldemichael 12 | SpdxLicenseId: Apache-2.0 13 | LicenseUrl: LICENSE 14 | ReadmeUrl: README.md 15 | Labels: ["kubernetes", "validating", "admission", "webhook", "eks", "ecr"] 16 | HomePageUrl: https://github.com/aws-samples/amazon-ecr-repository-compliance-webhook 17 | SemanticVersion: 1.5.0 18 | SourceCodeUrl: https://github.com/aws-samples/amazon-ecr-repository-compliance-webhook 19 | 20 | Parameters: 21 | RegistryRegion: 22 | Type: String 23 | Description: Optional. What AWS region should this Lambda function interact with ECR in? 24 | Default: "" 25 | LogLevel: 26 | Type: String 27 | Description: Optional. The log level to set. DEBUG, INFO, WARN, or ERROR. Default's to INFO. 28 | AllowedValues: ["DEBUG", "INFO", "WARN", "ERROR"] 29 | Default: INFO 30 | 31 | Conditions: 32 | HasRegistryRegion: !Not [!Equals [!Ref RegistryRegion, ""]] 33 | 34 | Resources: 35 | ECRRepositoryComplianceWebhookExecutionRole: 36 | Type: AWS::IAM::Role 37 | Properties: 38 | AssumeRolePolicyDocument: 39 | Statement: 40 | - Action: 41 | - sts:AssumeRole 42 | Effect: Allow 43 | Principal: 44 | Service: 45 | - lambda.amazonaws.com 46 | Version: "2012-10-17" 47 | ManagedPolicyArns: 48 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 49 | Policies: 50 | - PolicyDocument: 51 | Statement: 52 | - Action: 53 | - ecr:DescribeRepositories 54 | - xray:PutTraceSegments 55 | - ecr:DescribeImageScanFindings 56 | Effect: Allow 57 | Resource: "*" 58 | Version: "2012-10-17" 59 | PolicyName: ECRRepositoryComplianceWebhookLambdaPolicy 60 | 61 | ECRRepositoryComplianceWebhookAPIGateway: 62 | Type: AWS::Serverless::HttpApi 63 | Properties: 64 | StageName: Prod 65 | 66 | ECRRepositoryComplianceWebhookFunction: 67 | Type: AWS::Serverless::Function 68 | Description: Lambda handler for amazon-ecr-repository-compliance-webhook 69 | Properties: 70 | FunctionName: amazon-ecr-repository-compliance-webhook 71 | Handler: main 72 | Runtime: go1.x 73 | Tracing: Active 74 | MemorySize: 128 75 | Role: !GetAtt ECRRepositoryComplianceWebhookExecutionRole.Arn 76 | Timeout: 15 77 | Environment: 78 | Variables: 79 | REGISTRY_REGION: !If [HasRegistryRegion, !Ref RegistryRegion, !Ref "AWS::NoValue"] 80 | LOG_LEVEL: !Ref LogLevel 81 | Events: 82 | ValidationEvent: 83 | Type: HttpApi 84 | Properties: 85 | Path: /check-image-compliance 86 | Method: post 87 | ApiId: !Ref ECRRepositoryComplianceWebhookAPIGateway 88 | 89 | ConfigAPIGatewayLambdaInvoke: 90 | Type: AWS::Lambda::Permission 91 | Properties: 92 | Action: lambda:InvokeFunction 93 | FunctionName: !Ref ECRRepositoryComplianceWebhookFunction 94 | Principal: apigateway.amazonaws.com 95 | 96 | Outputs: 97 | WebhookURL: 98 | Description: "ValidatingWebhookConfiguration invocation URL" 99 | Value: !Sub "https://${ECRRepositoryComplianceWebhookAPIGateway}.execute-api.${AWS::Region}.amazonaws.com/Prod/check-image-compliance" 100 | -------------------------------------------------------------------------------- /pkg/function/ecr.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package function 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/aws/aws-sdk-go/aws" 12 | "github.com/aws/aws-sdk-go/service/ecr" 13 | log "github.com/sirupsen/logrus" 14 | "golang.org/x/sync/errgroup" 15 | ) 16 | 17 | const digestID = "@" 18 | 19 | // From repository:tag to repository, tag 20 | // Or repository@sha256:digest to repository, @sha256:digest 21 | func parts(image string) (repo, tagOrDigest string) { 22 | if strings.Contains(image, digestID) { 23 | segments := strings.Split(image, digestID) 24 | repo, tagOrDigest = segments[0], digestID+segments[1] // append ampersand for later 25 | log.Tracef("parts: repo [%s], tagOrHash [%s]", repo, tagOrDigest) 26 | return 27 | } 28 | segments := strings.Split(image, ":") 29 | repo, tagOrDigest = segments[0], segments[1] 30 | log.Tracef("parts: repo [%s], tagOrHash [%s]", repo, tagOrDigest) 31 | return 32 | } 33 | 34 | // CheckRepositoryCompliance checks if the container image that was sent to the webhook: 35 | // 1. Comes from an ECR repository 36 | // 2. Has image tag immutability enabled 37 | // 3. Has image scan on push enabled 38 | // 4. Does not contain any critical vulnerabilities 39 | func (c *Container) CheckRepositoryCompliance(ctx context.Context, image string) (bool, error) { 40 | repo, _ := parts(image) 41 | input := &ecr.DescribeRepositoriesInput{ 42 | RepositoryNames: []*string{aws.String(repo)}, 43 | } 44 | if err := input.Validate(); err != nil { 45 | return false, err 46 | } 47 | output, err := c.ECR.DescribeRepositoriesWithContext(ctx, input) 48 | if err != nil { 49 | return false, err 50 | } 51 | if len(output.Repositories) == 0 { 52 | return false, fmt.Errorf("no repositories named '%s' found", repo) 53 | } 54 | r := output.Repositories[0] 55 | if aws.StringValue(r.ImageTagMutability) == ecr.ImageTagMutabilityMutable { 56 | return false, fmt.Errorf("repository '%s' does not have image tag immutability enabled", repo) 57 | } 58 | if !aws.BoolValue(r.ImageScanningConfiguration.ScanOnPush) { 59 | return false, fmt.Errorf("repository '%s' does not have image scan on push enabled", repo) 60 | } 61 | critical, err := c.HasCriticalVulnerabilities(ctx, image) 62 | if err != nil { 63 | return false, err 64 | } 65 | if critical { 66 | return false, fmt.Errorf("image '%s' contains %s vulnerabilities", image, ecr.FindingSeverityCritical) 67 | } 68 | return true, nil 69 | } 70 | 71 | // BatchCheckRepositoryCompliance checks the compliance of a given set of ECR images. 72 | // False is returned if a single repository is not compliant. 73 | func (c *Container) BatchCheckRepositoryCompliance(ctx context.Context, images []string) (bool, error) { 74 | g, ctx := errgroup.WithContext(ctx) 75 | compliances := make([]bool, len(images)) 76 | 77 | for i, image := range images { 78 | i, image := i, image // shadow 79 | g.Go(func() error { 80 | compliant, err := c.CheckRepositoryCompliance(ctx, image) 81 | compliances[i] = compliant 82 | return err 83 | }) 84 | } 85 | if err := g.Wait(); err != nil { 86 | return false, err 87 | } 88 | 89 | for _, complaint := range compliances { 90 | if !complaint { 91 | return false, nil 92 | } 93 | } 94 | return true, nil 95 | } 96 | 97 | // HasCriticalVulnerabilities checks if a container image contains 'CRITICAL' vulnerabilities. 98 | func (c *Container) HasCriticalVulnerabilities(ctx context.Context, image string) (bool, error) { 99 | var ( 100 | repo, tagOrDigest = parts(image) 101 | found = false 102 | ) 103 | input := &ecr.DescribeImageScanFindingsInput{ 104 | ImageId: &ecr.ImageIdentifier{}, 105 | RepositoryName: aws.String(repo), 106 | } 107 | 108 | switch strings.Contains(tagOrDigest, digestID) { 109 | case true: 110 | input.ImageId.ImageDigest = aws.String(tagOrDigest[1:]) // omit ampersand 111 | default: 112 | input.ImageId.ImageTag = aws.String(tagOrDigest) 113 | } 114 | if err := input.Validate(); err != nil { 115 | return true, err 116 | } 117 | 118 | pager := func(out *ecr.DescribeImageScanFindingsOutput, lastPage bool) bool { 119 | for _, finding := range out.ImageScanFindings.Findings { 120 | if aws.StringValue(finding.Severity) == ecr.FindingSeverityCritical { 121 | found = true 122 | return found // break out of paging if we've already found a critical vuln. 123 | } 124 | } 125 | return lastPage 126 | } 127 | 128 | if err := c.ECR.DescribeImageScanFindingsPagesWithContext(ctx, input, pager); err != nil { 129 | return true, err 130 | } 131 | return found, nil 132 | } 133 | -------------------------------------------------------------------------------- /pkg/webhook/request.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package webhook 5 | 6 | import ( 7 | "encoding/json" 8 | "errors" 9 | "regexp" 10 | "strings" 11 | 12 | "github.com/aws/aws-lambda-go/events" 13 | v1beta1 "k8s.io/api/admission/v1beta1" 14 | corev1 "k8s.io/api/core/v1" 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/apimachinery/pkg/runtime" 17 | "k8s.io/apimachinery/pkg/runtime/schema" 18 | "k8s.io/apimachinery/pkg/runtime/serializer" 19 | ) 20 | 21 | // ECRImageRegex matches ECR images that come from registries in commercial regions, 22 | // regions in China, GovCloud, and registries using FIPS endpoints. 23 | // For endpoints, see: https://docs.aws.amazon.com/general/latest/gr/ecr.html 24 | var ECRImageRegex = regexp.MustCompile(`(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.(ecr|ecr-fips)\.([a-z][a-z0-9-_]*)\.amazonaws\.com(\.cn)?.*`) 25 | 26 | // Errors returned when a request or resource expectation fails. 27 | var ( 28 | ErrInvalidContentType = errors.New("webhook: invalid content type; expected application/json") 29 | ErrMissingContentType = errors.New("webhook: missing content-type header") 30 | ErrObjectNotFound = errors.New("webhook: request did not include object") 31 | ErrUnexpectedResource = errors.New("webhook: expected pod resource") 32 | ErrInvalidAdmission = errors.New("webhook: admission request was nil") 33 | ) 34 | 35 | var ( 36 | runtimeScheme = runtime.NewScheme() 37 | codecs = serializer.NewCodecFactory(runtimeScheme) 38 | deserializer = codecs.UniversalDeserializer() 39 | ignoredNamespaces = []string{ 40 | metav1.NamespaceSystem, 41 | } 42 | podDefault = &schema.GroupVersionKind{ 43 | Group: "core", 44 | Version: "v1", 45 | Kind: "Pod", 46 | } 47 | ) 48 | 49 | // Request encapsulates the AdmissionRequest from the 50 | // AdmissionReview proxied to the Lambda function. 51 | type Request struct { 52 | Admission *v1beta1.AdmissionRequest 53 | } 54 | 55 | // NewRequestFromEvent creates a Request from the APIGatewayProxyRequest. 56 | func NewRequestFromEvent(event events.APIGatewayProxyRequest) (*Request, error) { 57 | val, ok := event.Headers["content-type"] 58 | if !ok { 59 | return nil, ErrMissingContentType 60 | } 61 | if val != "application/json" { 62 | return nil, ErrInvalidContentType 63 | } 64 | var review v1beta1.AdmissionReview 65 | if _, _, err := deserializer.Decode([]byte(event.Body), nil, &review); err != nil { 66 | return nil, err 67 | } 68 | return &Request{Admission: review.Request}, nil 69 | } 70 | 71 | // UnmarshalPod unmarshals the raw object in the AdmissionRequest into a Pod. 72 | func (r *Request) UnmarshalPod() (*corev1.Pod, error) { 73 | if r.Admission == nil { 74 | return nil, ErrInvalidAdmission 75 | } 76 | if len(r.Admission.Object.Raw) == 0 { 77 | return nil, ErrObjectNotFound 78 | } 79 | if r.Admission.Kind.Kind != podDefault.Kind { 80 | // If the ValidatingWebhookConfiguration was given additional resource scopes. 81 | return nil, ErrUnexpectedResource 82 | } 83 | 84 | var pod corev1.Pod 85 | if err := json.Unmarshal(r.Admission.Object.Raw, &pod); err != nil { 86 | return nil, err 87 | } 88 | return &pod, nil 89 | } 90 | 91 | // InCriticalNamespace checks that the request was for a resource 92 | // that is being deployed into a critical namespace; e.g. kube-system. 93 | func InCriticalNamespace(pod *corev1.Pod) bool { 94 | for _, n := range ignoredNamespaces { 95 | if pod.Namespace == n { 96 | return true 97 | } 98 | } 99 | return false 100 | } 101 | 102 | // ParseImages returns the container images in the Pod spec 103 | // that originate from an Amazon ECR repository. 104 | func ParseImages(pod *corev1.Pod) []string { 105 | var ( 106 | images []string 107 | containers = append(pod.Spec.Containers, pod.Spec.InitContainers...) 108 | ) 109 | for _, c := range containers { 110 | if ECRImageRegex.MatchString(c.Image) { 111 | parsed := parse(c.Image) 112 | if !contains(images, parsed) { 113 | images = append(images, parsed) 114 | } 115 | } 116 | } 117 | return images 118 | } 119 | 120 | // From aws_account_id.dkr.ecr(-fips).aws_region.amazonaws.com(cn)/repository:tag to repository:tag 121 | // Or aws_account_id.dkr.ecr(-fips).aws_region.amazonaws.com(cn)/repository@sha256:hash to repository@sha256:hash 122 | func parse(image string) string { 123 | if !strings.Contains(image, "/") { 124 | return "" 125 | } 126 | base := strings.SplitN(image, "/", 2) 127 | if len(base) < 1 { 128 | return "" 129 | } 130 | return base[1] 131 | } 132 | 133 | // check that we already haven't seen this image before 134 | func contains(images []string, image string) bool { 135 | for _, r := range images { 136 | if r == image { 137 | return true 138 | } 139 | } 140 | return false 141 | } 142 | -------------------------------------------------------------------------------- /testdata/testdata.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package testdata contains data for testing. 5 | package testdata 6 | 7 | // ReviewWithOneImage is a test AdmissionReview used for templating 8 | // a single container image. 9 | // Referenced: https://github.com/alex-leonhardt/k8s-mutate-webhook/blob/master/pkg/mutate/mutate_test.go 10 | const ReviewWithOneImage = `{ 11 | "kind": "AdmissionReview", 12 | "apiVersion": "admission.k8s.io/v1beta1", 13 | "request": { 14 | "uid": "e77141b6-6033-11ea-8d6a-0ac25c990f4a", 15 | "kind": { 16 | "group": "", 17 | "version": "v1", 18 | "kind": "Pod" 19 | }, 20 | "resource": { 21 | "group": "", 22 | "version": "v1", 23 | "resource": "pods" 24 | }, 25 | "namespace": "echo-namespace", 26 | "operation": "CREATE", 27 | "userInfo": { 28 | "username": "system:serviceaccount:kube-system:replicaset-controller", 29 | "uid": "064184fd-5e6c-11ea-8d6a-0ac25c990f4a", 30 | "groups": ["system:serviceaccounts", "system:serviceaccounts:kube-system", "system:authenticated"] 31 | }, 32 | "object": { 33 | "kind": "Pod", 34 | "apiVersion": "v1", 35 | "metadata": { 36 | "name": "echo-68f4474876-dzsjm", 37 | "generateName": "echo-68f4474876-", 38 | "namespace": "echo-namespace", 39 | "uid": "e77136cf-6033-11ea-8d6a-0ac25c990f4a", 40 | "creationTimestamp": "2020-03-07T05:24:23Z", 41 | "labels": { 42 | "app": "echo", 43 | "pod-template-hash": "68f4474876" 44 | }, 45 | "annotations": { 46 | "kubernetes.io/psp": "eks.privileged" 47 | }, 48 | "ownerReferences": [{ 49 | "apiVersion": "apps/v1", 50 | "kind": "ReplicaSet", 51 | "name": "echo-68f4474876", 52 | "uid": "21494a34-6033-11ea-8d6a-0ac25c990f4a", 53 | "controller": true, 54 | "blockOwnerDeletion": true 55 | }] 56 | }, 57 | "spec": { 58 | "volumes": [{ 59 | "name": "default-token-qrv2v", 60 | "secret": { 61 | "secretName": "default-token-qrv2v" 62 | } 63 | }], 64 | "containers": [{ 65 | "name": "echo", 66 | "image": "%s", 67 | "ports": [{ 68 | "containerPort": 80, 69 | "protocol": "TCP" 70 | }], 71 | "resources": {}, 72 | "volumeMounts": [{ 73 | "name": "default-token-qrv2v", 74 | "readOnly": true, 75 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount" 76 | }], 77 | "terminationMessagePath": "/dev/termination-log", 78 | "terminationMessagePolicy": "File", 79 | "imagePullPolicy": "IfNotPresent" 80 | }], 81 | "restartPolicy": "Always", 82 | "terminationGracePeriodSeconds": 30, 83 | "dnsPolicy": "ClusterFirst", 84 | "serviceAccountName": "default", 85 | "serviceAccount": "default", 86 | "securityContext": {}, 87 | "schedulerName": "default-scheduler", 88 | "tolerations": [{ 89 | "key": "node.kubernetes.io/not-ready", 90 | "operator": "Exists", 91 | "effect": "NoExecute", 92 | "tolerationSeconds": 300 93 | }, { 94 | "key": "node.kubernetes.io/unreachable", 95 | "operator": "Exists", 96 | "effect": "NoExecute", 97 | "tolerationSeconds": 300 98 | }], 99 | "priority": 0, 100 | "enableServiceLinks": true 101 | }, 102 | "status": { 103 | "phase": "Pending", 104 | "qosClass": "BestEffort" 105 | } 106 | }, 107 | "oldObject": null, 108 | "dryRun": false 109 | } 110 | }` 111 | 112 | // ReviewWithBadRequest is an AdmissionReview with a bad request. 113 | const ReviewWithBadRequest = `{ 114 | "kind": "AdmissionReview", 115 | "apiVersion": "admission.k8s.io/v1beta1", 116 | "request": { 117 | "uid": "e77141b6-6033-11ea-8d6a-0ac25c990f4a" 118 | } 119 | }` 120 | 121 | // ReviewWithNoUID is an AdmissionReview with a bad request and no UID. 122 | const ReviewWithNoUID = `{ 123 | "kind": "AdmissionReview", 124 | "apiVersion": "admission.k8s.io/v1beta1", 125 | "request": { 126 | "uid": "" 127 | } 128 | }` 129 | 130 | // ECR container image URIs. 131 | // Valid references here: https://docs.aws.amazon.com/general/latest/gr/ecr.html 132 | // Repositories in China use a .com.cn API endpoint, but image references keep .com: https://docs.amazonaws.cn/general/latest/gr/ecr.html 133 | const ( 134 | UntaggedImage = "273450712882.dkr.ecr.us-east-2.amazonaws.com/namespace/repo@sha256:e5e2a3236e64483c50dd2811e46e9cd49c67e82271e60d112ca69a075fc23005" 135 | TaggedImage = "273450712882.dkr.ecr.us-east-2.amazonaws.com/namespace/repo:40d6072" 136 | NoNamespace = "273450712882.dkr.ecr.us-east-2.amazonaws.com/repo:40d6072" 137 | CNImage = "273450712882.dkr.ecr.cn-north-1.amazonaws.com/namespace/repo:40d6072" 138 | FIPSImage = "273450712882.dkr.ecr-fips.us-east-2.amazonaws.com/namespace/repo:40d6072" 139 | AliasedImage = "myaccountalias.dkr.ecr-fips.us-east-2.amazonaws.com/namespace/repo:40d6072" 140 | ) 141 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/aws-samples/amazon-ecr-repository-compliance-webhook/pkg/function" 13 | "github.com/aws-samples/amazon-ecr-repository-compliance-webhook/testdata" 14 | "github.com/aws/aws-lambda-go/events" 15 | "github.com/aws/aws-sdk-go/aws" 16 | "github.com/aws/aws-sdk-go/aws/request" 17 | "github.com/aws/aws-sdk-go/service/ecr" 18 | "github.com/aws/aws-sdk-go/service/ecr/ecriface" 19 | log "github.com/sirupsen/logrus" 20 | "github.com/stretchr/testify/mock" 21 | "github.com/stretchr/testify/require" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | ) 24 | 25 | type mockECRClient struct { 26 | mock.Mock 27 | ecriface.ECRAPI 28 | } 29 | 30 | // DescribeRepositoriesWithContext mocks the DescribeRepositories ECR API endpoint. 31 | func (_m *mockECRClient) DescribeRepositoriesWithContext(ctx aws.Context, input *ecr.DescribeRepositoriesInput, opts ...request.Option) (*ecr.DescribeRepositoriesOutput, error) { 32 | log.Debugf("Mocking DescribeRepositories API with input: %s\n", input.String()) 33 | args := _m.Called(ctx, input) 34 | return args.Get(0).(*ecr.DescribeRepositoriesOutput), args.Error(1) 35 | } 36 | 37 | // DescribeImageScanFindingsPagesWithContext mocks the DescribeImageScanFindingsP ECR API endpoint. 38 | func (_m *mockECRClient) DescribeImageScanFindingsPagesWithContext(ctx aws.Context, input *ecr.DescribeImageScanFindingsInput, fn func(*ecr.DescribeImageScanFindingsOutput, bool) bool, opts ...request.Option) error { 39 | log.Debugf("Mocking DescribeImageScanFindings API with input: %s\n", input.String()) 40 | args := _m.Called(ctx, input, fn) 41 | return args.Error(0) 42 | } 43 | 44 | func TestHandler(t *testing.T) { 45 | type args struct { 46 | image string 47 | repo *ecr.Repository 48 | shouldCheckVuln bool 49 | scanFindings *ecr.DescribeImageScanFindingsOutput 50 | event events.APIGatewayProxyRequest 51 | } 52 | ecrSvc := new(mockECRClient) 53 | h := function.NewContainer(ecrSvc).Handler() 54 | 55 | tests := []struct { 56 | name string 57 | args args 58 | status string 59 | wantErr bool 60 | }{ 61 | { 62 | name: "BadRequestFailure", 63 | args: args{ 64 | shouldCheckVuln: false, 65 | repo: nil, 66 | event: eventWithBadRequest(), 67 | }, 68 | status: metav1.StatusFailure, 69 | wantErr: true, 70 | }, 71 | { 72 | name: "BadRequestNoUIDFailure", 73 | args: args{ 74 | shouldCheckVuln: false, 75 | repo: nil, 76 | event: eventWithNoUID(), 77 | }, 78 | status: metav1.StatusFailure, 79 | wantErr: true, 80 | }, 81 | { 82 | name: "ImmutabilityAndScanningDisabledFailure", 83 | args: args{ 84 | image: "auth:notlatest", 85 | shouldCheckVuln: false, 86 | repo: &ecr.Repository{ 87 | RepositoryName: aws.String("auth"), 88 | ImageTagMutability: aws.String(ecr.ImageTagMutabilityMutable), 89 | ImageScanningConfiguration: &ecr.ImageScanningConfiguration{ScanOnPush: aws.Bool(false)}, 90 | }, 91 | event: eventWithImage("123456789012.dkr.ecr.region.amazonaws.com/auth:notlatest"), 92 | }, 93 | status: metav1.StatusFailure, 94 | wantErr: true, 95 | }, 96 | { 97 | name: "ScanningDisabledFailure", 98 | args: args{ 99 | image: "shipping:notlatest", 100 | shouldCheckVuln: false, 101 | repo: &ecr.Repository{ 102 | RepositoryName: aws.String("shipping"), 103 | ImageTagMutability: aws.String(ecr.ImageTagMutabilityImmutable), 104 | ImageScanningConfiguration: &ecr.ImageScanningConfiguration{ScanOnPush: aws.Bool(false)}, 105 | }, 106 | event: eventWithImage("123456789012.dkr.ecr.region.amazonaws.com/shipping:notlatest"), 107 | }, 108 | status: metav1.StatusFailure, 109 | wantErr: true, 110 | }, 111 | { 112 | name: "ImmutabilityDisabledFailure", 113 | args: args{ 114 | image: "ordering:notlatest", 115 | shouldCheckVuln: false, 116 | repo: &ecr.Repository{ 117 | RepositoryName: aws.String("ordering"), 118 | ImageTagMutability: aws.String(ecr.ImageTagMutabilityMutable), 119 | ImageScanningConfiguration: &ecr.ImageScanningConfiguration{ScanOnPush: aws.Bool(true)}, 120 | }, 121 | event: eventWithImage("123456789012.dkr.ecr.region.amazonaws.com/ordering:notlatest"), 122 | }, 123 | status: metav1.StatusFailure, 124 | wantErr: true, 125 | }, 126 | { 127 | name: "NotECRRepositoryFailure", 128 | args: args{ 129 | image: "nginx-ingress-controller:0.30.0", 130 | shouldCheckVuln: false, 131 | repo: nil, 132 | event: eventWithImage("quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.30.0"), 133 | }, 134 | status: metav1.StatusFailure, 135 | wantErr: true, 136 | }, 137 | { 138 | name: "HasCriticalVulnerabilitiesFailure", 139 | args: args{ 140 | image: "fakecompany/exploitedpush:notlatest", 141 | shouldCheckVuln: true, 142 | scanFindings: findingsWithCriticalVuln(), 143 | repo: &ecr.Repository{ 144 | RepositoryName: aws.String("fakecompany/exploitedpush"), 145 | ImageTagMutability: aws.String(ecr.ImageTagMutabilityImmutable), 146 | ImageScanningConfiguration: &ecr.ImageScanningConfiguration{ScanOnPush: aws.Bool(true)}, 147 | }, 148 | event: eventWithImage("123456789012.dkr.ecr.region.amazonaws.com/fakecompany/exploitedpush:notlatest"), 149 | }, 150 | status: metav1.StatusFailure, 151 | wantErr: true, 152 | }, 153 | { 154 | name: "ImmutabilityAndScanningEnabledAndNoCriticalVulnerabilitiesPass", 155 | args: args{ 156 | image: "costcenter1/payroll:notlatest", 157 | shouldCheckVuln: true, 158 | scanFindings: findingsWithNoVuln(), 159 | repo: &ecr.Repository{ 160 | RepositoryName: aws.String("costcenter1/payroll"), 161 | ImageTagMutability: aws.String(ecr.ImageTagMutabilityImmutable), 162 | ImageScanningConfiguration: &ecr.ImageScanningConfiguration{ScanOnPush: aws.Bool(true)}, 163 | }, 164 | event: eventWithImage("123456789012.dkr.ecr.region.amazonaws.com/costcenter1/payroll:notlatest"), 165 | }, 166 | status: metav1.StatusSuccess, 167 | wantErr: false, 168 | }, 169 | } 170 | 171 | for _, tt := range tests { 172 | t.Run(tt.name, func(t *testing.T) { 173 | ctx, cancel := context.WithCancel(context.Background()) 174 | defer cancel() 175 | if tt.args.repo != nil { 176 | ecrSvc.On("DescribeRepositoriesWithContext", 177 | ctx, 178 | &ecr.DescribeRepositoriesInput{ 179 | RepositoryNames: []*string{tt.args.repo.RepositoryName}, 180 | }, 181 | ).Return(&ecr.DescribeRepositoriesOutput{Repositories: []*ecr.Repository{tt.args.repo}}, nil) 182 | } 183 | 184 | if tt.args.shouldCheckVuln { 185 | // DescribeImageScanFindingsPagesWithContext will not be called if 186 | // the repository fails one of the first three checks 187 | tag := strings.Split(tt.args.image, ":")[1] 188 | ecrSvc.On("DescribeImageScanFindingsPagesWithContext", 189 | ctx, 190 | &ecr.DescribeImageScanFindingsInput{ 191 | ImageId: &ecr.ImageIdentifier{ 192 | ImageTag: aws.String(tag), 193 | }, 194 | RepositoryName: tt.args.repo.RepositoryName, 195 | }, 196 | mock.AnythingOfType("func(*ecr.DescribeImageScanFindingsOutput, bool) bool"), 197 | ).Return(nil).Run(func(args mock.Arguments) { 198 | arg := args.Get(2).(func(*ecr.DescribeImageScanFindingsOutput, bool) bool) 199 | arg(tt.args.scanFindings, true) 200 | }) 201 | } 202 | review, err := h(context.Background(), tt.args.event) 203 | if err != nil { 204 | t.Fatalf("Error during request for image: %v", err) 205 | } 206 | t.Logf("Got review body: %#+v", review) 207 | require.Nil(t, err) 208 | require.Equal(t, tt.status, review.Response.Result.Status) 209 | if tt.wantErr { 210 | require.GreaterOrEqual(t, review.Response.Result.Code, int32(400)) 211 | require.Less(t, review.Response.Result.Code, int32(500)) 212 | } 213 | ecrSvc.AssertExpectations(t) 214 | }) 215 | } 216 | } 217 | 218 | var base = events.APIGatewayProxyRequest{ 219 | Path: "/check-image-compliance", 220 | HTTPMethod: "POST", 221 | Headers: map[string]string{"content-type": "application/json"}, 222 | } 223 | 224 | func eventWithNoUID() events.APIGatewayProxyRequest { 225 | base.Body = testdata.ReviewWithNoUID 226 | return base 227 | } 228 | 229 | func eventWithBadRequest() events.APIGatewayProxyRequest { 230 | base.Body = testdata.ReviewWithBadRequest 231 | return base 232 | } 233 | 234 | func eventWithImage(image string) events.APIGatewayProxyRequest { 235 | base.Body = fmt.Sprintf(testdata.ReviewWithOneImage, image) 236 | return base 237 | } 238 | 239 | func findingsWithCriticalVuln() *ecr.DescribeImageScanFindingsOutput { 240 | return &ecr.DescribeImageScanFindingsOutput{ 241 | ImageScanFindings: &ecr.ImageScanFindings{ 242 | Findings: []*ecr.ImageScanFinding{ 243 | { 244 | Severity: aws.String(ecr.FindingSeverityCritical), 245 | }, 246 | }, 247 | }, 248 | } 249 | } 250 | 251 | func findingsWithNoVuln() *ecr.DescribeImageScanFindingsOutput { 252 | return &ecr.DescribeImageScanFindingsOutput{ 253 | ImageScanFindings: &ecr.ImageScanFindings{ 254 | Findings: []*ecr.ImageScanFinding{ 255 | { 256 | Severity: aws.String(ecr.FindingSeverityInformational), 257 | }, 258 | }, 259 | }, 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![][sar-logo]](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:273450712882:applications~amazon-ecr-repository-compliance-webhook) 2 | 3 | [sar-deploy]: https://img.shields.io/badge/Serverless%20Application%20Repository-Deploy%20Now-blue?style=flat-square 4 | [sar-logo]: https://img.shields.io/badge/Serverless%20Application%20Repository-View-blue?style=flat-square 5 | 6 | # Amazon ECR Repository Compliance Webhook for Kubernetes 7 | >A Kubernetes ValidatingWebhookConfiguration and serverless backend: Deny Pods with container images that don't meet your compliance requirements 8 | 9 | This AWS Serverless Application Repository app will create an Amazon API Gateway and an AWS Lambda Function that act as the backend for a Kubernetes ValidatingWebhookConfiguration. The function will deny Pods that create containers using images which: 10 | 1. Do not come from ECR 11 | 2. Come from ECR, but do not have image tag immutability enabled 12 | 3. Come from ECR, but do not have image scan on push enabled 13 | 4. Come from ECR, and have image scan on push enabled, but contain `CRITICAL` security vulnerabilities 14 | 15 | ![architecture](https://raw.githubusercontent.com/aws-samples/amazon-ecr-repository-compliance-webhook/master/screenshots/architecture.png) 16 | 17 | ## Usage 18 | To use this SAR application: 19 | 1. Deploy the serverless application 20 | 2. Configure and deploy the `ValidatingWebhookConfiguration` resource into your Kubernetes cluster (EKS or otherwise). The cluster must have this plugin enabled and have support for the admissionregistration.k8s.io/v1beta1 API. See the official Kubernetes documentation [here](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/) for details. Amazon Elastic Kubernetes Service has [supported Dynamic Admission Controllers since October 12, 2018](https://aws.amazon.com/about-aws/whats-new/2018/10/amazon-eks-enables-support-for-kubernetes-dynamic-admission-cont/). 21 | 22 | ### 1. Deploying the Application 23 | It is recommended that you deploy this Lambda function directly from the AWS Serverless Application Repository. It is also possible to deploy this function using: 24 | - The [SAM CLI](https://aws.amazon.com/serverless/sam/) 25 | - CloudFormation via the [AWS CLI](https://aws.amazon.com/cli/) 26 | - CloudFormation via the [CloudFormation management console](https://aws.amazon.com/cloudformation/) 27 | 28 | This function has been made available in 17 of the 18 commercial AWS regions that support AWS SAR. As of March 2020, Bahrain (me-south-1) does not yet support API Gateway. It is also possible to deploy the Lambda function in the GovCloud and China regions, if you have access to those regions. 29 | 30 | |Region |Click and Deploy | 31 | |----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| 32 | |**US East (Ohio) (us-east-2)** |[![][sar-deploy]](https://deploy.serverlessrepo.app/us-east-2/?app=arn:aws:serverlessrepo:us-east-1:273450712882:applications/amazon-ecr-repository-compliance-webhook) | 33 | |**US East (N. Virginia) (us-east-1)** |[![][sar-deploy]](https://deploy.serverlessrepo.app/us-east-1/?app=arn:aws:serverlessrepo:us-east-1:273450712882:applications/amazon-ecr-repository-compliance-webhook) | 34 | |**US West (N. California) (us-west-1)** |[![][sar-deploy]](https://deploy.serverlessrepo.app/us-west-1/?app=arn:aws:serverlessrepo:us-east-1:273450712882:applications/amazon-ecr-repository-compliance-webhook) | 35 | |**US West (Oregon) (us-west-2)** |[![][sar-deploy]](https://deploy.serverlessrepo.app/us-west-2/?app=arn:aws:serverlessrepo:us-east-1:273450712882:applications/amazon-ecr-repository-compliance-webhook) | 36 | |**Asia Pacific (Hong Kong) (ap-east-1)** |[![][sar-deploy]](https://deploy.serverlessrepo.app/ap-east-1/?app=arn:aws:serverlessrepo:us-east-1:273450712882:applications/amazon-ecr-repository-compliance-webhook) | 37 | |**Asia Pacific (Mumbai) (ap-south-1)** |[![][sar-deploy]](https://deploy.serverlessrepo.app/ap-south-1/?app=arn:aws:serverlessrepo:us-east-1:273450712882:applications/amazon-ecr-repository-compliance-webhook) | 38 | |**Asia Pacific (Seoul) (ap-northeast-2)** |[![][sar-deploy]](https://deploy.serverlessrepo.app/ap-northeast-2/?app=arn:aws:serverlessrepo:us-east-1:273450712882:applications/amazon-ecr-repository-compliance-webhook)| 39 | |**Asia Pacific (Singapore) (ap-southeast-1)** |[![][sar-deploy]](https://deploy.serverlessrepo.app/ap-southeast-1/?app=arn:aws:serverlessrepo:us-east-1:273450712882:applications/amazon-ecr-repository-compliance-webhook)| 40 | |**Asia Pacific (Sydney) (ap-southeast-2)** |[![][sar-deploy]](https://deploy.serverlessrepo.app/ap-southeast-2/?app=arn:aws:serverlessrepo:us-east-1:273450712882:applications/amazon-ecr-repository-compliance-webhook)| 41 | |**Asia Pacific (Tokyo) (ap-northeast-1)** |[![][sar-deploy]](https://deploy.serverlessrepo.app/ap-northeast-1?app=arn:aws:serverlessrepo:us-east-1:273450712882:applications/amazon-ecr-repository-compliance-webhook) | 42 | |**Canada (Central) (ca-central-1)** |[![][sar-deploy]](https://deploy.serverlessrepo.app/ca-central-1/?app=arn:aws:serverlessrepo:us-east-1:273450712882:applications/amazon-ecr-repository-compliance-webhook) | 43 | |**EU (Frankfurt) (eu-central-1)** |[![][sar-deploy]](https://deploy.serverlessrepo.app/eu-central-1/?app=arn:aws:serverlessrepo:us-east-1:273450712882:applications/amazon-ecr-repository-compliance-webhook) | 44 | |**EU (Ireland) (eu-west-1)** |[![][sar-deploy]](https://deploy.serverlessrepo.app/eu-west-1/?app=arn:aws:serverlessrepo:us-east-1:273450712882:applications/amazon-ecr-repository-compliance-webhook) | 45 | |**EU (London) (eu-west-2)** |[![][sar-deploy]](https://deploy.serverlessrepo.app/eu-west-2/?app=arn:aws:serverlessrepo:us-east-1:273450712882:applications/amazon-ecr-repository-compliance-webhook) | 46 | |**EU (Paris) (eu-west-3)** |[![][sar-deploy]](https://deploy.serverlessrepo.app/eu-west-3/?app=arn:aws:serverlessrepo:us-east-1:273450712882:applications/amazon-ecr-repository-compliance-webhook) | 47 | |**EU (Stockholm) (eu-north-1)** |[![][sar-deploy]](https://deploy.serverlessrepo.app/eu-north-1/?app=arn:aws:serverlessrepo:us-east-1:273450712882:applications/amazon-ecr-repository-compliance-webhook) | 48 | |**South America (Sao Paulo) (sa-east-1)** |[![][sar-deploy]](https://deploy.serverlessrepo.app/sa-east-1/?app=arn:aws:serverlessrepo:us-east-1:273450712882:applications/amazon-ecr-repository-compliance-webhook) | 49 | 50 | #### Parameters 51 | |Name |Default |Description |Required | 52 | |---------------|------------------|------------------------------------------------------------------|---------| 53 | |RegistryRegion |Function's Region |What AWS region should this Lambda function interact with ECR in? |False | 54 | |LogLevel |INFO |The log level to set. ["DEBUG", "INFO", "WARN", "ERROR"] |False | 55 | 56 | ### 2. Configuration 57 | After deploying the SAR application from the SAR console you need to: 58 | 1. Authenticate with your cluster. For example, for EKS you can use the AWS CLI: `aws eks update-kubeconfig --name your-clusters-name --region your-clusters-region` 59 | 2. Run `kubectl apply -f validatingwebhook.yaml` to deploy the `ValidatingWebhookConfiguration`. The YAML file is provided [here](https://github.com/aws-samples/amazon-ecr-repository-compliance-webhook/blob/master/deploy/validatingwebhook.yaml). Remember to update `webhooks.clientConfig.url` with your API Gateway endpoint. Make any necessary additions to match namespaces/labels for resources that are deployed. This webhook only validates Pods. 60 | 3. Run `kubectl create ns test-namespace && kubectl apply -f mydeployment.yaml` to create a sample `Deployment`. The sample is provided [here](https://github.com/aws-samples/amazon-ecr-repository-compliance-webhook/blob/master/deploy/mydeployment.yaml). Change the image to be any image you would like to test. Ensure your nodes have permission to pull from the ECR repository. 61 | 4. Run `kubectl get ev -n test-namespace` to see if there are any `FailedCreate` events as a result of the `Deployment`'s `ReplicaSet` triggering a failure from the `ValidatingWebhookConfiguration` when trying to create Pods. For example: `Error creating: admission webhook "admission.ecr.amazonaws.com" denied the request: webhook: no ecr images found in pod specification` 62 | 63 | ## Contributing 64 | Have an idea for a feature to enhance this serverless application? Open an [issue](https://github.com/aws-samples/amazon-ecr-repository-compliance-webhook/issues) or [pull request](https://github.com/aws-samples/amazon-ecr-repository-compliance-webhook/pulls)! 65 | 66 | ### Development 67 | This application has been developed, built, and tested against [Go 1.14](https://golang.org/dl/), the latest version of the [Serverless Application Model CLI](https://github.com/awslabs/aws-sam-cli), and the latest version of the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html), Kubernetes version 1.14, Kubernetes version 1.15, and [kubectl 1.17](https://kubernetes.io/docs/tasks/tools/install-kubectl/). A [Makefile](./Makefile) has been provided for convenience. 68 | 69 | ``` 70 | make install-tools # Install linting tools 71 | make lint # Run Go linting tools 72 | make test # Run Go tests 73 | make compile # Compile Go binary 74 | make sam-package # Package code and assets into S3 using SAM CLI 75 | make sam-deploy # Deploy application using SAM CLI 76 | make sam-logs # Tail the logs of the running Lambda function 77 | make destroy-stack # Destroy the CloudFormation stack tied to the SAR app 78 | ``` 79 | 80 | ### To Do 81 | 1. [Parameter.String] RegistryID - What registry should this Lambda verify container images for? Good for cross-account interactions. 82 | 2. [Parameter.CommaDelimitedList] IgnoredNamespaces - What namespaces should be ignored? It is also possible to set matchers on the [`ValidatingWebhookConfiguration`](./deploy/validatingwebhook.yaml). 83 | 3. Emit metric on deny/pass, to Amazon CloudWatch 84 | 4. Support the admissionregistration.k8s.io/v1 API 85 | 86 | ## References 87 | - BanzaiCloud - In-depth introduction to Kubernetes admission webhooks: https://banzaicloud.com/blog/k8s-admission-webhooks/ 88 | - ValidatingWebhookConfiguration API Documentation - https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/#validatingwebhookconfiguration-v1beta1-admissionregistration-k8s-io 89 | - Dynamic Admission Control - https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/ 90 | - Official Kubernetes example: https://github.com/kubernetes/kubernetes/blob/v1.15.0/test/images/webhook/ 91 | 92 | ## Acknowledgements 93 | [@jicowan](https://github.com/jicowan) for inspiration: https://github.com/jicowan/ecr-validation-webhook 94 | 95 | ## License 96 | This project is licensed under the [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html) -------------------------------------------------------------------------------- /THIRD-PARTY-LICENSES: -------------------------------------------------------------------------------- 1 | ** github.com/aws/aws-lambda-go; version v1.16.0 -- 2 | https://github.com/aws/aws-lambda-go 3 | ** github.com/aws/aws-sdk-go; version v1.30.19 -- 4 | https://github.com/aws/aws-sdk-go 5 | ** github.com/aws/aws-xray-sdk-go; version v1.0.1 -- 6 | https://github.com/aws/aws-xray-sdk-go 7 | ** github.com/kubernetes/kubernetes; version v0.18.2 -- https://k8s.io 8 | 9 | Apache License 10 | 11 | Version 2.0, January 2004 12 | 13 | http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND 14 | DISTRIBUTION 15 | 16 | 1. Definitions. 17 | 18 | "License" shall mean the terms and conditions for use, reproduction, and 19 | distribution as defined by Sections 1 through 9 of this document. 20 | 21 | "Licensor" shall mean the copyright owner or entity authorized by the 22 | copyright owner that is granting the License. 23 | 24 | "Legal Entity" shall mean the union of the acting entity and all other 25 | entities that control, are controlled by, or are under common control 26 | with that entity. For the purposes of this definition, "control" means 27 | (i) the power, direct or indirect, to cause the direction or management 28 | of such entity, whether by contract or otherwise, or (ii) ownership of 29 | fifty percent (50%) or more of the outstanding shares, or (iii) 30 | beneficial ownership of such entity. 31 | 32 | "You" (or "Your") shall mean an individual or Legal Entity exercising 33 | permissions granted by this License. 34 | 35 | "Source" form shall mean the preferred form for making modifications, 36 | including but not limited to software source code, documentation source, 37 | and configuration files. 38 | 39 | "Object" form shall mean any form resulting from mechanical 40 | transformation or translation of a Source form, including but not limited 41 | to compiled object code, generated documentation, and conversions to 42 | other media types. 43 | 44 | "Work" shall mean the work of authorship, whether in Source or Object 45 | form, made available under the License, as indicated by a copyright 46 | notice that is included in or attached to the work (an example is 47 | provided in the Appendix below). 48 | 49 | "Derivative Works" shall mean any work, whether in Source or Object form, 50 | that is based on (or derived from) the Work and for which the editorial 51 | revisions, annotations, elaborations, or other modifications represent, 52 | as a whole, an original work of authorship. For the purposes of this 53 | License, Derivative Works shall not include works that remain separable 54 | from, or merely link (or bind by name) to the interfaces of, the Work and 55 | Derivative Works thereof. 56 | 57 | "Contribution" shall mean any work of authorship, including the original 58 | version of the Work and any modifications or additions to that Work or 59 | Derivative Works thereof, that is intentionally submitted to Licensor for 60 | inclusion in the Work by the copyright owner or by an individual or Legal 61 | Entity authorized to submit on behalf of the copyright owner. For the 62 | purposes of this definition, "submitted" means any form of electronic, 63 | verbal, or written communication sent to the Licensor or its 64 | representatives, including but not limited to communication on electronic 65 | mailing lists, source code control systems, and issue tracking systems 66 | that are managed by, or on behalf of, the Licensor for the purpose of 67 | discussing and improving the Work, but excluding communication that is 68 | conspicuously marked or otherwise designated in writing by the copyright 69 | owner as "Not a Contribution." 70 | 71 | "Contributor" shall mean Licensor and any individual or Legal Entity on 72 | behalf of whom a Contribution has been received by Licensor and 73 | subsequently incorporated within the Work. 74 | 75 | 2. Grant of Copyright License. Subject to the terms and conditions of this 76 | License, each Contributor hereby grants to You a perpetual, worldwide, 77 | non-exclusive, no-charge, royalty-free, irrevocable copyright license to 78 | reproduce, prepare Derivative Works of, publicly display, publicly perform, 79 | sublicense, and distribute the Work and such Derivative Works in Source or 80 | Object form. 81 | 82 | 3. Grant of Patent License. Subject to the terms and conditions of this 83 | License, each Contributor hereby grants to You a perpetual, worldwide, 84 | non-exclusive, no-charge, royalty-free, irrevocable (except as stated in 85 | this section) patent license to make, have made, use, offer to sell, sell, 86 | import, and otherwise transfer the Work, where such license applies only to 87 | those patent claims licensable by such Contributor that are necessarily 88 | infringed by their Contribution(s) alone or by combination of their 89 | Contribution(s) with the Work to which such Contribution(s) was submitted. 90 | If You institute patent litigation against any entity (including a 91 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 92 | Contribution incorporated within the Work constitutes direct or contributory 93 | patent infringement, then any patent licenses granted to You under this 94 | License for that Work shall terminate as of the date such litigation is 95 | filed. 96 | 97 | 4. Redistribution. You may reproduce and distribute copies of the Work or 98 | Derivative Works thereof in any medium, with or without modifications, and 99 | in Source or Object form, provided that You meet the following conditions: 100 | 101 | (a) You must give any other recipients of the Work or Derivative Works a 102 | copy of this License; and 103 | 104 | (b) You must cause any modified files to carry prominent notices stating 105 | that You changed the files; and 106 | 107 | (c) You must retain, in the Source form of any Derivative Works that You 108 | distribute, all copyright, patent, trademark, and attribution notices 109 | from the Source form of the Work, excluding those notices that do not 110 | pertain to any part of the Derivative Works; and 111 | 112 | (d) If the Work includes a "NOTICE" text file as part of its 113 | distribution, then any Derivative Works that You distribute must include 114 | a readable copy of the attribution notices contained within such NOTICE 115 | file, excluding those notices that do not pertain to any part of the 116 | Derivative Works, in at least one of the following places: within a 117 | NOTICE text file distributed as part of the Derivative Works; within the 118 | Source form or documentation, if provided along with the Derivative 119 | Works; or, within a display generated by the Derivative Works, if and 120 | wherever such third-party notices normally appear. The contents of the 121 | NOTICE file are for informational purposes only and do not modify the 122 | License. You may add Your own attribution notices within Derivative Works 123 | that You distribute, alongside or as an addendum to the NOTICE text from 124 | the Work, provided that such additional attribution notices cannot be 125 | construed as modifying the License. 126 | 127 | You may add Your own copyright statement to Your modifications and may 128 | provide additional or different license terms and conditions for use, 129 | reproduction, or distribution of Your modifications, or for any such 130 | Derivative Works as a whole, provided Your use, reproduction, and 131 | distribution of the Work otherwise complies with the conditions stated in 132 | this License. 133 | 134 | 5. Submission of Contributions. Unless You explicitly state otherwise, any 135 | Contribution intentionally submitted for inclusion in the Work by You to the 136 | Licensor shall be under the terms and conditions of this License, without 137 | any additional terms or conditions. Notwithstanding the above, nothing 138 | herein shall supersede or modify the terms of any separate license agreement 139 | you may have executed with Licensor regarding such Contributions. 140 | 141 | 6. Trademarks. This License does not grant permission to use the trade 142 | names, trademarks, service marks, or product names of the Licensor, except 143 | as required for reasonable and customary use in describing the origin of the 144 | Work and reproducing the content of the NOTICE file. 145 | 146 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in 147 | writing, Licensor provides the Work (and each Contributor provides its 148 | Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 149 | KIND, either express or implied, including, without limitation, any 150 | warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or 151 | FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining 152 | the appropriateness of using or redistributing the Work and assume any risks 153 | associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, whether 156 | in tort (including negligence), contract, or otherwise, unless required by 157 | applicable law (such as deliberate and grossly negligent acts) or agreed to 158 | in writing, shall any Contributor be liable to You for damages, including 159 | any direct, indirect, special, incidental, or consequential damages of any 160 | character arising as a result of this License or out of the use or inability 161 | to use the Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all other 163 | commercial damages or losses), even if such Contributor has been advised of 164 | the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing the Work 167 | or Derivative Works thereof, You may choose to offer, and charge a fee for, 168 | acceptance of support, warranty, indemnity, or other liability obligations 169 | and/or rights consistent with this License. However, in accepting such 170 | obligations, You may act only on Your own behalf and on Your sole 171 | responsibility, not on behalf of any other Contributor, and only if You 172 | agree to indemnify, defend, and hold each Contributor harmless for any 173 | liability incurred by, or claims asserted against, such Contributor by 174 | reason of your accepting any such warranty or additional liability. END OF 175 | TERMS AND CONDITIONS 176 | 177 | APPENDIX: How to apply the Apache License to your work. 178 | 179 | To apply the Apache License to your work, attach the following boilerplate 180 | notice, with the fields enclosed by brackets "[]" replaced with your own 181 | identifying information. (Don't include the brackets!) The text should be 182 | enclosed in the appropriate comment syntax for the file format. We also 183 | recommend that a file or class name and description of purpose be included on 184 | the same "printed page" as the copyright notice for easier identification 185 | within third-party archives. 186 | 187 | Copyright [yyyy] [name of copyright owner] 188 | 189 | Licensed under the Apache License, Version 2.0 (the "License"); 190 | 191 | you may not use this file except in compliance with the License. 192 | 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | 203 | See the License for the specific language governing permissions and 204 | 205 | limitations under the License. 206 | 207 | * For github.com/aws/aws-lambda-go see also this required NOTICE: 208 | Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 209 | * For github.com/aws/aws-sdk-go see also this required NOTICE: 210 | Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. 211 | * For github.com/aws/aws-xray-sdk-go see also this required NOTICE: 212 | Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights 213 | Reserved. 214 | * For github.com/kubernetes/kubernetes see also this required NOTICE: 215 | Copyright 2014 The Kubernetes Authors. 216 | 217 | ------ 218 | 219 | ** github.com/stretchr/testify; version v1.5.1 -- 220 | https://github.com/stretchr/testify 221 | Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors 222 | ** golang.org/x/sync; version v0.0.0-20200317015054-43a5402ce75a -- 223 | https://github.com/golang/sync 224 | Copyright (c) 2009 The Go Authors. All rights reserved. 225 | ** sirupsen-logrus; version v1.5.0 -- https://github.com/sirupsen/logrus 226 | Copyright (c) 2014 Simon Eskildsen 227 | 228 | MIT License 229 | 230 | Copyright (c) 231 | 232 | Permission is hereby granted, free of charge, to any person obtaining a copy of 233 | this software and associated documentation files (the "Software"), to deal in 234 | the Software without restriction, including without limitation the rights to 235 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 236 | of the Software, and to permit persons to whom the Software is furnished to do 237 | so, subject to the following conditions: 238 | 239 | The above copyright notice and this permission notice shall be included in all 240 | copies or substantial portions of the Software. 241 | 242 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 243 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 244 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 245 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 246 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 247 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 248 | SOFTWARE. -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/DATA-DOG/go-sqlmock v1.4.1 h1:ThlnYciV1iM/V0OSF/dtkqWb6xo5qITT1TJBG1MRDJM= 4 | github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 5 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 6 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 7 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 8 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 9 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 10 | github.com/aws/aws-lambda-go v1.16.0 h1:9+Pp1/6cjEXYhwadp8faFXKSOWt7/tHRCnQxQmKvVwM= 11 | github.com/aws/aws-lambda-go v1.16.0/go.mod h1:FEwgPLE6+8wcGBTe5cJN3JWurd1Ztm9zN4jsXsjzKKw= 12 | github.com/aws/aws-sdk-go v1.17.12/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 13 | github.com/aws/aws-sdk-go v1.30.26 h1:wP0N6DBb/3EyHTtWNz4jzgGVi1l290zoFGfu4HFGeM0= 14 | github.com/aws/aws-sdk-go v1.30.26/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= 15 | github.com/aws/aws-xray-sdk-go v1.0.1 h1:En3DuQ3fAIlNPKoMcAY7bv0lINCJPV0lElK8kEEXsKM= 16 | github.com/aws/aws-xray-sdk-go v1.0.1/go.mod h1:tmxq1c+yeEbMh39OmRFuXOrse5ajRlMmDXJ6LrCVsIs= 17 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 18 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 19 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 20 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 21 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 22 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 23 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 24 | github.com/davecgh/go-spew v0.0.0-20160907170601-6d212800a42e/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 27 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 29 | github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 30 | github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 31 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 32 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 33 | github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 34 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 35 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 36 | github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= 37 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 38 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 39 | github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= 40 | github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 41 | github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= 42 | github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 43 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 44 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 45 | github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= 46 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 47 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 48 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 49 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 50 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 51 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 52 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 53 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 54 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 55 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 56 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 57 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 58 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 59 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 60 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 61 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 62 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 63 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 64 | github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= 65 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 66 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 67 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 68 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 69 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 70 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 71 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 72 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 73 | github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= 74 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 75 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 76 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 77 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 78 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 79 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 80 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 81 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 82 | github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= 83 | github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= 84 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 85 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 86 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 87 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 88 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 89 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 90 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 91 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 92 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 93 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 94 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 95 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 96 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 97 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 98 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 99 | github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= 100 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 101 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 102 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 103 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 104 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 105 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 106 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 107 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 108 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 109 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 110 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 111 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 112 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 113 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 114 | github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 115 | github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= 116 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 117 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 118 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 119 | github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= 120 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 121 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 122 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 123 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 124 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 125 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 126 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 127 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 128 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 129 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 130 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 131 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 132 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 133 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 134 | github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= 135 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 136 | github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 137 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 138 | github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 139 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 140 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 141 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 142 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 143 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 144 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 145 | github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 146 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 147 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 148 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 149 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 150 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 151 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 152 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 153 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 154 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 155 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 156 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 157 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 158 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 159 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 160 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 161 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 162 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 163 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 164 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 165 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 166 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 167 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 168 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 169 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 170 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 171 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 172 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 173 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 174 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 175 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 176 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= 177 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 178 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 179 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 180 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 181 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 182 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 183 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 184 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 185 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 186 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 187 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 188 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 189 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 190 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 191 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 192 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 193 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 194 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 195 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 196 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 197 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 198 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 199 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 200 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 201 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 202 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 203 | golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 204 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 205 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= 206 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 207 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 208 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 209 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 210 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 211 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 212 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 213 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 214 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 215 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 216 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 217 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 218 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 219 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 220 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 221 | golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 222 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 223 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 224 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 225 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 226 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 227 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 228 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 229 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 230 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 231 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 232 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 233 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 234 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 235 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 236 | google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 237 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 238 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 239 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 240 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 241 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 242 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 243 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 244 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 245 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 246 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 247 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 248 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 249 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 250 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 251 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 252 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 253 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 254 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 255 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 256 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 257 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 258 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 259 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 260 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 261 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 262 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 263 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 264 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 265 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 266 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 267 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 268 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 269 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 270 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 271 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 272 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 273 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 274 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 275 | k8s.io/api v0.25.0 h1:H+Q4ma2U/ww0iGB78ijZx6DRByPz6/733jIuFpX70e0= 276 | k8s.io/api v0.25.0/go.mod h1:ttceV1GyV1i1rnmvzT3BST08N6nGt+dudGrquzVQWPk= 277 | k8s.io/apimachinery v0.25.0 h1:MlP0r6+3XbkUG2itd6vp3oxbtdQLQI94fD5gCS+gnoU= 278 | k8s.io/apimachinery v0.25.0/go.mod h1:qMx9eAk0sZQGsXGu86fab8tZdffHbwUfsvzqKn4mfB0= 279 | k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= 280 | k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= 281 | k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= 282 | k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ= 283 | k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 284 | k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1/go.mod h1:C/N6wCaBHeBHkHUesQOQy2/MZqGgMAFPqGsGQLdbZBU= 285 | k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= 286 | k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= 287 | k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= 288 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= 289 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 290 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= 291 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= 292 | sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= 293 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 294 | --------------------------------------------------------------------------------