├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── configs └── main.go ├── ecr-continuous-scan-architecture.png ├── findings └── main.go ├── go.mod ├── go.sum ├── scan-config-amazonlinux.json ├── scan-config-centos.json ├── scan-config-ubuntu.json ├── scan-findindings-feed.png ├── start-scan └── main.go ├── summary └── main.go └── template.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | *.out 12 | .DS_Store 13 | /bin 14 | vendor 15 | /current-stack.yaml 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 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 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ECR_SCAN_STACK_NAME?=ecr-continuous-scan 2 | ECR_SCAN_SVC_BUCKET?=ecr-continuous-scan-svc 3 | ECR_SCAN_CONFIG_BUCKET?=ecr-continuous-scan-config 4 | 5 | .PHONY: build up deploy destroy status 6 | 7 | 8 | build: bconfigs bsscan bsummary bfindings 9 | 10 | bconfigs: 11 | GOOS=linux GOARCH=amd64 go build -v -ldflags '-d -s -w' -a -tags netgo -installsuffix netgo -o bin/configs ./configs 12 | 13 | bsscan: 14 | GOOS=linux GOARCH=amd64 go build -v -ldflags '-d -s -w' -a -tags netgo -installsuffix netgo -o bin/start-scan ./start-scan 15 | 16 | bsummary: 17 | GOOS=linux GOARCH=amd64 go build -v -ldflags '-d -s -w' -a -tags netgo -installsuffix netgo -o bin/summary ./summary 18 | 19 | bfindings: 20 | GOOS=linux GOARCH=amd64 go build -v -ldflags '-d -s -w' -a -tags netgo -installsuffix netgo -o bin/findings ./findings 21 | 22 | up: 23 | sam package --template-file template.yaml --output-template-file current-stack.yaml --s3-bucket ${ECR_SCAN_SVC_BUCKET} 24 | sam deploy --template-file current-stack.yaml --stack-name ${ECR_SCAN_STACK_NAME} --capabilities CAPABILITY_IAM --parameter-overrides ConfigBucketName="${ECR_SCAN_CONFIG_BUCKET}" 25 | 26 | deploy: build up 27 | 28 | destroy: 29 | aws cloudformation delete-stack --stack-name ${ECR_SCAN_STACK_NAME} 30 | 31 | status: 32 | aws cloudformation describe-stacks --stack-name ${ECR_SCAN_STACK_NAME} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ECR Container Image Re-Scan 2 | 3 | This repo shows how to use the [ECR image scanning](https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning.html) feature 4 | for a scheduled re-scan, that is, scanning images on a regular basis. We will walk you through the setup and usage of this demo. 5 | 6 | ## Installation 7 | 8 | In order to build and deploy the service, clone this repo and make sure you've got the following available, locally: 9 | 10 | - The [aws](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) CLI 11 | - The [SAM CLI](https://github.com/awslabs/aws-sam-cli) 12 | - Go 1.12 or above 13 | 14 | Additionally, having [jq](https://stedolan.github.io/jq/download/) installed it recommended. 15 | 16 | Preparing the S3 buckets (make sure that you pick different names for the `ECR_SCAN_*` buckets): 17 | 18 | ```sh 19 | export ECR_SCAN_SVC_BUCKET=ecr-continuous-scan-svc 20 | export ECR_SCAN_CONFIG_BUCKET=ecr-continuous-scan-config 21 | 22 | aws s3api create-bucket \ 23 | --bucket $ECR_SCAN_SVC_BUCKET \ 24 | --create-bucket-configuration LocationConstraint=$(aws configure get region) \ 25 | --region $(aws configure get region) 26 | 27 | aws s3api create-bucket \ 28 | --bucket $ECR_SCAN_CONFIG_BUCKET \ 29 | --create-bucket-configuration LocationConstraint=$(aws configure get region) \ 30 | --region $(aws configure get region) 31 | ``` 32 | 33 | Make sure that you have the newest [Go SDK](https://aws.amazon.com/sdk-for-go/) installed, 34 | supporting the image scanning feature. In addition, you need to `go get github.com/gorilla/feeds` 35 | as the one other dependency outside of the standard library. Then execute: 36 | 37 | ```sh 38 | make deploy 39 | ``` 40 | 41 | which will build the binaries and deploy the Lambda functions. 42 | 43 | You're now ready to use the demo. 44 | 45 | 46 | ## Architecture 47 | 48 | The overall architecture of the demo is as follows: 49 | 50 | ![ECR continuous scan demo architecture](ecr-continuous-scan-architecture.png) 51 | 52 | There are four Lambda functions and an S3 buckets to hold the scan configurations involved. 53 | 54 | The HTTP API is made up of the following three Lambda functions: 55 | 56 | * `ConfigsFunc` handles the management of scan configs, allowing you to store, list, and delete them. 57 | * `SummaryFunc` provides a summary of the scan findings across all scan configs. 58 | * `FindingsFunc` provides a detailed Atom feed of the scan findings per scan config. 59 | 60 | In addition, there is a `StartScanFunc` that is triggered by a CloudWatch event, kicking off the image scan. 61 | 62 | ### Scan configurations 63 | 64 | To specify which repositories should be re-scanned on a regular basis, one has to provide a scan configuration. 65 | 66 | This scan configuration has three required fields, `region`, `registry` (your AWS account ID), and 67 | the `repository` itself: 68 | 69 | ```json 70 | { 71 | "region": "us-west-2", 72 | "registry": "123456789012", 73 | "repository": "amazonlinux", 74 | "tags": [ 75 | "2018.03" 76 | ] 77 | } 78 | ``` 79 | 80 | Note that `tags` is optional and if not provided, all tags of the `repository` will be scanned. 81 | 82 | ### API 83 | 84 | The following HTTP API is exposed: 85 | 86 | Scan configurations: 87 | 88 | * `GET configs/` … lists all registered scan configurations, returns JSON 89 | * `POST configs/` … adds a scan configuration, returns scan ID 90 | * `DELETE configs/{scanid}` … removes a registered scan configuration by scan ID or `404` if it doesn't exist 91 | 92 | Scan findings: 93 | 94 | * `GET summary/` … provides high-level summary of findings across all registered scan configurations 95 | * `GET findings/{scanid}` … provides detailed findings on a scan configuration bases, returns an Atom feed 96 | 97 | 98 | ## Usage walkthrough 99 | 100 | The following walkthrough assumes that the ECR repositories have been set up 101 | (using `aws ecr create-repository`) and the container images have been pushed to 102 | the repositories, accordingly. 103 | 104 | First, in order to interact with the HTTP API, capture the base URL in an environment variable `ECRSCANAPI_URL` like so: 105 | 106 | ```sh 107 | export ECRSCANAPI_URL=$(aws cloudformation describe-stacks --stack-name ecr-continuous-scan | jq '.Stacks[].Outputs[] | select(.OutputKey=="ECRScanAPIEndpoint").OutputValue' -r) 108 | ``` 109 | 110 | Now, add some scan configurations (part of this repo): 111 | 112 | ```sh 113 | curl -s --header "Content-Type: application/json" --request POST --data @scan-config-amazonlinux.json $ECRSCANAPI_URL/configs/ 114 | curl -s --header "Content-Type: application/json" --request POST --data @scan-config-centos.json $ECRSCANAPI_URL/configs/ 115 | curl -s --header "Content-Type: application/json" --request POST --data @scan-config-ubuntu.json $ECRSCANAPI_URL/configs/ 116 | ``` 117 | 118 | List all registered scan configurations: 119 | 120 | ```sh 121 | $ curl $ECRSCANAPI_URL/configs/ 122 | [ 123 | { 124 | "id": "4471c156-29f5-40fe-883b-3cd26738d5a6", 125 | "created": "1569927812", 126 | "region": "us-west-2", 127 | "registry": "123456789012", 128 | "repository": "amazonlinux", 129 | "tags": [ 130 | "2018.03" 131 | ] 132 | }, 133 | { 134 | "id": "612fccea-9545-45d0-8feb-cdc20c4c3061", 135 | "created": "1569927820", 136 | "region": "us-west-2", 137 | "registry": "123456789012", 138 | "repository": "test/centos", 139 | "tags": null 140 | }, 141 | { 142 | "id": "fc41dda8-f15e-4826-8908-11603b01dac4", 143 | "created": "1569927828", 144 | "region": "us-west-2", 145 | "registry": "123456789012", 146 | "repository": "test/ubuntu", 147 | "tags": [ 148 | "16.04", 149 | "latest" 150 | ] 151 | } 152 | ] 153 | ``` 154 | 155 | Get an overview of the scan result findings across the registered scan configurations: 156 | 157 | ```sh 158 | $ curl $ECRSCANAPI_URL/summary 159 | Results for amazonlinux:2018.03 in us-west-2: 160 | 161 | 162 | Results for test/centos:7 in us-west-2: 163 | HIGH: 7 164 | LOW: 7 165 | MEDIUM: 20 166 | 167 | 168 | Results for test/ubuntu:16.04 in us-west-2: 169 | INFORMATIONAL: 19 170 | LOW: 24 171 | MEDIUM: 8 172 | 173 | 174 | Results for test/ubuntu:latest in us-west-2: 175 | MEDIUM: 7 176 | INFORMATIONAL: 9 177 | LOW: 13 178 | ``` 179 | 180 | Get a detailed feed of findings for `test/ubuntu` (with scan ID `fc41dda8-f15e-4826-8908-11603b01dac4`): 181 | 182 | ```sh 183 | $ curl $ECRSCANAPI_URL/findings/fc41dda8-f15e-4826-8908-11603b01dac4 184 | 185 | 186 | ECR repository test/ubuntu in us-west-2 187 | https://us-west-2.console.aws.amazon.com/ecr/repositories/test/ubuntu/ 188 | 189 | Details of the image scan findings across the tags: [16.04] [latest] 190 | 191 | 192 | ECR 193 | 194 | 195 | [MEDIUM] in image test/ubuntu:16.04 found CVE-2016-1585 196 | 2019-10-01T11:27:17Z 197 | 16.04 198 | 199 | In all versions of AppArmor mount rules are accidentally widened when compiled. 200 | 201 | ... 202 | 203 | ``` 204 | 205 | The Atom feeds can be consumed in a feed reader, for example: 206 | 207 | ![Scan findings feed](scan-findindings-feed.png) 208 | 209 | You can remove scan configs like so: 210 | 211 | ```sh 212 | curl --request DELETE $ECRSCANAPI_URL/configs/4471c156-29f5-40fe-883b-3cd26738d5a6 213 | ``` 214 | -------------------------------------------------------------------------------- /configs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/aws/aws-lambda-go/events" 13 | "github.com/aws/aws-lambda-go/lambda" 14 | "github.com/aws/aws-sdk-go-v2/config" 15 | "github.com/aws/aws-sdk-go-v2/service/s3" 16 | "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 17 | "github.com/aws/aws-sdk-go/aws" 18 | 19 | uuid "github.com/satori/go.uuid" 20 | ) 21 | 22 | // ScanSpec represents configuration for the target repository 23 | type ScanSpec struct { 24 | // ID is a unique identifier for the scan spec 25 | ID string `json:"id"` 26 | // CreationTime is the UTC timestamp of when the scan spec was created 27 | CreationTime string `json:"created"` 28 | // Region specifies the region the repository is in 29 | Region string `json:"region"` 30 | // RegistryID specifies the registry ID 31 | RegistryID string `json:"registry"` 32 | // Repository specifies the repository name 33 | Repository string `json:"repository"` 34 | // Tags to take into consideration, if empty, all tags will be scanned 35 | Tags []string `json:"tags"` 36 | } 37 | 38 | func serverError(err error) (events.APIGatewayProxyResponse, error) { 39 | fmt.Println(err.Error()) 40 | return events.APIGatewayProxyResponse{ 41 | StatusCode: http.StatusInternalServerError, 42 | Headers: map[string]string{ 43 | "Access-Control-Allow-Origin": "*", 44 | }, 45 | Body: fmt.Sprintf("%v", err.Error()), 46 | }, nil 47 | } 48 | 49 | // storeScanSpec stores the scan spec in a given bucket 50 | func storeScanSpec(configbucket string, scanspec ScanSpec) error { 51 | cfg, err := config.LoadDefaultConfig(context.TODO()) 52 | if err != nil { 53 | return err 54 | } 55 | ssjson, err := json.Marshal(scanspec) 56 | if err != nil { 57 | return err 58 | } 59 | // Create an S3 Client with the config 60 | client := s3.NewFromConfig(cfg) 61 | 62 | // Create an uploader passing it the client 63 | uploader := manager.NewUploader(client) 64 | 65 | // uploader := manager.NewUploader(cfg) 66 | _, err = uploader.Upload(context.TODO(), &s3.PutObjectInput{ 67 | Bucket: aws.String(configbucket), 68 | Key: aws.String(scanspec.ID + ".json"), 69 | Body: strings.NewReader(string(ssjson)), 70 | }) 71 | return err 72 | } 73 | 74 | // fetchScanSpec returns the scan spec 75 | // in a given bucket, with a given scan ID 76 | func fetchScanSpec(configbucket, scanid string) (ScanSpec, error) { 77 | ss := ScanSpec{} 78 | cfg, err := config.LoadDefaultConfig(context.TODO()) 79 | if err != nil { 80 | return ss, err 81 | } 82 | 83 | // Create an S3 Client with the config 84 | client := s3.NewFromConfig(cfg) 85 | 86 | // Create an uploader passing it the client 87 | downloader := manager.NewDownloader(client) 88 | 89 | buf := aws.NewWriteAtBuffer([]byte{}) 90 | 91 | _, err = downloader.Download(context.TODO(), buf, &s3.GetObjectInput{ 92 | Bucket: aws.String(configbucket), 93 | Key: aws.String(scanid + ".json"), 94 | }) 95 | if err != nil { 96 | return ss, err 97 | } 98 | err = json.Unmarshal(buf.Bytes(), &ss) 99 | if err != nil { 100 | return ss, err 101 | } 102 | return ss, nil 103 | } 104 | 105 | // rmClusterSpec deletes the scan spec in a given bucket 106 | func rmClusterSpec(configbucket, scanid string) error { 107 | cfg, err := config.LoadDefaultConfig(context.TODO()) 108 | if err != nil { 109 | return err 110 | } 111 | svc := s3.NewFromConfig(cfg) 112 | _, err = svc.DeleteObject(context.TODO(), &s3.DeleteObjectInput{ 113 | Bucket: aws.String(configbucket), 114 | Key: aws.String(scanid + ".json"), 115 | }) 116 | // _, err = req.Send(context.Background()) 117 | if err != nil { 118 | return err 119 | } 120 | return nil 121 | } 122 | 123 | func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 124 | configbucket := os.Getenv("ECR_SCAN_CONFIG_BUCKET") 125 | fmt.Printf("DEBUG:: config continuous scan start\n") 126 | 127 | cfg, err := config.LoadDefaultConfig(context.TODO()) 128 | if err != nil { 129 | return serverError(err) 130 | } 131 | svc := s3.NewFromConfig(cfg) 132 | 133 | switch request.HTTPMethod { 134 | case "POST": 135 | fmt.Printf("DEBUG:: adding scan config\n") 136 | ss := ScanSpec{} 137 | // Unmarshal the JSON payload in the POST: 138 | err := json.Unmarshal([]byte(request.Body), &ss) 139 | if err != nil { 140 | return serverError(err) 141 | } 142 | specID := uuid.NewV4() 143 | // if err != nil { 144 | // return serverError(err) 145 | // } 146 | ss.ID = specID.String() 147 | ss.CreationTime = fmt.Sprintf("%v", time.Now().Unix()) 148 | err = storeScanSpec(configbucket, ss) 149 | if err != nil { 150 | return serverError(err) 151 | } 152 | msg := fmt.Sprintf("Added scan config. ID=%v ", ss.ID) 153 | return events.APIGatewayProxyResponse{ 154 | StatusCode: http.StatusOK, 155 | Headers: map[string]string{ 156 | "Content-Type": "application/json", 157 | "Access-Control-Allow-Origin": "*", 158 | }, 159 | Body: msg, 160 | }, nil 161 | case "DELETE": 162 | fmt.Printf("DEBUG:: removing scan config\n") 163 | // validate ID in URL path: 164 | if _, ok := request.PathParameters["id"]; !ok { 165 | return serverError(fmt.Errorf("Unknown configuration")) 166 | } 167 | resp, err := svc.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{ 168 | Bucket: &configbucket, 169 | }, 170 | ) 171 | // resp, err := req.Send(context.TODO()) 172 | if err != nil { 173 | return serverError(err) 174 | } 175 | for _, obj := range resp.Contents { 176 | fn := *obj.Key 177 | scanID := strings.TrimSuffix(fn, ".json") 178 | if scanID == request.PathParameters["id"] { 179 | rmClusterSpec(configbucket, scanID) 180 | msg := fmt.Sprintf("Deleted scan config %v ", request.PathParameters["id"]) 181 | return events.APIGatewayProxyResponse{ 182 | StatusCode: http.StatusOK, 183 | Headers: map[string]string{ 184 | "Content-Type": "application/json", 185 | "Access-Control-Allow-Origin": "*", 186 | }, 187 | Body: msg, 188 | }, nil 189 | } 190 | } 191 | return events.APIGatewayProxyResponse{ 192 | StatusCode: http.StatusNotFound, 193 | Headers: map[string]string{ 194 | "Content-Type": "application/json", 195 | "Access-Control-Allow-Origin": "*", 196 | }, 197 | Body: "This scan config does not exist, no operation performed", 198 | }, nil 199 | case "GET": 200 | fmt.Printf("DEBUG:: listing scan config\n") 201 | resp, err := svc.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{ 202 | Bucket: &configbucket, 203 | }, 204 | ) 205 | // resp, err := req.Send(context.TODO()) 206 | if err != nil { 207 | return serverError(err) 208 | } 209 | scanspecs := []ScanSpec{} 210 | for _, obj := range resp.Contents { 211 | fn := *obj.Key 212 | scanID := strings.TrimSuffix(fn, ".json") 213 | scanspec, err := fetchScanSpec(configbucket, scanID) 214 | if err != nil { 215 | return serverError(err) 216 | } 217 | scanspecs = append(scanspecs, scanspec) 218 | 219 | } 220 | scanspecsjson, err := json.Marshal(scanspecs) 221 | if err != nil { 222 | return serverError(err) 223 | } 224 | return events.APIGatewayProxyResponse{ 225 | StatusCode: http.StatusOK, 226 | Headers: map[string]string{ 227 | "Content-Type": "application/json", 228 | "Access-Control-Allow-Origin": "*", 229 | }, 230 | Body: string(scanspecsjson), 231 | }, nil 232 | } 233 | fmt.Printf("DEBUG:: register continuous scan done\n") 234 | return events.APIGatewayProxyResponse{ 235 | StatusCode: http.StatusMethodNotAllowed, 236 | Headers: map[string]string{ 237 | "Access-Control-Allow-Origin": "*", 238 | }, 239 | }, nil 240 | } 241 | 242 | func main() { 243 | lambda.Start(handler) 244 | } 245 | -------------------------------------------------------------------------------- /ecr-continuous-scan-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ecr-continuous-scan/31c5088d394c077446b2c95dd49dd370ee0acd64/ecr-continuous-scan-architecture.png -------------------------------------------------------------------------------- /findings/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | _ "image/jpeg" 8 | _ "image/png" 9 | "net/http" 10 | "os" 11 | "strings" 12 | 13 | "github.com/aws/aws-lambda-go/events" 14 | "github.com/aws/aws-lambda-go/lambda" 15 | "github.com/aws/aws-sdk-go-v2/config" 16 | "github.com/aws/aws-sdk-go-v2/service/s3" 17 | "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 18 | "github.com/aws/aws-sdk-go/aws" 19 | "github.com/aws/aws-sdk-go/aws/session" 20 | "github.com/aws/aws-sdk-go/service/ecr" 21 | "github.com/gorilla/feeds" 22 | ) 23 | 24 | // ScanSpec represents configuration for the target repository 25 | type ScanSpec struct { 26 | // ID is a unique identifier for the scan spec 27 | ID string `json:"id"` 28 | // CreationTime is the UTC timestamp of when the scan spec was created 29 | CreationTime string `json:"created"` 30 | // Region specifies the region the repository is in 31 | Region string `json:"region"` 32 | // RegistryID specifies the registry ID 33 | RegistryID string `json:"registry"` 34 | // Repository specifies the repository name 35 | Repository string `json:"repository"` 36 | // Tags to take into consideration, if empty, all tags will be scanned 37 | Tags []string `json:"tags"` 38 | } 39 | 40 | func serverError(err error) (events.APIGatewayProxyResponse, error) { 41 | fmt.Println(err.Error()) 42 | return events.APIGatewayProxyResponse{ 43 | StatusCode: http.StatusInternalServerError, 44 | Headers: map[string]string{ 45 | "Access-Control-Allow-Origin": "*", 46 | }, 47 | Body: fmt.Sprintf("%v", err.Error()), 48 | }, nil 49 | } 50 | 51 | // fetchScanSpec returns the scan spec 52 | // in a given bucket, with a given scan ID 53 | func fetchScanSpec(configbucket, scanid string) (ScanSpec, error) { 54 | ss := ScanSpec{} 55 | cfg, err := config.LoadDefaultConfig(context.TODO()) 56 | if err != nil { 57 | return ss, err 58 | } 59 | 60 | // Create an S3 Client with the config 61 | client := s3.NewFromConfig(cfg) 62 | 63 | // Create an uploader passing it the client 64 | downloader := manager.NewDownloader(client) 65 | 66 | buf := aws.NewWriteAtBuffer([]byte{}) 67 | _, err = downloader.Download(context.TODO(), buf, &s3.GetObjectInput{ 68 | Bucket: aws.String(configbucket), 69 | Key: aws.String(scanid + ".json"), 70 | }) 71 | if err != nil { 72 | return ss, err 73 | } 74 | err = json.Unmarshal(buf.Bytes(), &ss) 75 | if err != nil { 76 | return ss, err 77 | } 78 | return ss, nil 79 | } 80 | 81 | func describeScan(scanspec ScanSpec) (map[string]ecr.ImageScanFindings, error) { 82 | s := session.Must(session.NewSession(&aws.Config{ 83 | Region: aws.String(scanspec.Region), 84 | })) 85 | svc := ecr.New(s) 86 | descinput := &ecr.DescribeImageScanFindingsInput{ 87 | RepositoryName: &scanspec.Repository, 88 | RegistryId: &scanspec.RegistryID, 89 | } 90 | results := map[string]ecr.ImageScanFindings{} 91 | switch len(scanspec.Tags) { 92 | case 0: // empty list of tags, describe all tags: 93 | fmt.Printf("DEBUG:: scanning all tags for repo %v\n", scanspec.Repository) 94 | lio, err := svc.ListImages(&ecr.ListImagesInput{ 95 | RepositoryName: &scanspec.Repository, 96 | RegistryId: &scanspec.RegistryID, 97 | Filter: &ecr.ListImagesFilter{ 98 | TagStatus: aws.String("TAGGED"), 99 | }, 100 | }) 101 | if err != nil { 102 | fmt.Println(err) 103 | return results, err 104 | } 105 | for _, iid := range lio.ImageIds { 106 | descinput.ImageId = iid 107 | result, err := svc.DescribeImageScanFindings(descinput) 108 | if err != nil { 109 | return results, err 110 | } 111 | results[*iid.ImageTag] = *result.ImageScanFindings 112 | // fmt.Printf("DEBUG:: result for tag %v: %v\n", *iid.ImageTag, result) 113 | } 114 | default: // iterate over the tags specified in the config: 115 | fmt.Printf("DEBUG:: scanning tags %v for repo %v\n", scanspec.Tags, scanspec.Repository) 116 | for _, tag := range scanspec.Tags { 117 | descinput.ImageId = &ecr.ImageIdentifier{ 118 | ImageTag: aws.String(tag), 119 | } 120 | result, err := svc.DescribeImageScanFindings(descinput) 121 | if err != nil { 122 | fmt.Println(err) 123 | return results, err 124 | } 125 | results[tag] = *result.ImageScanFindings 126 | // fmt.Printf("DEBUG:: result for tag %v: %v\n", tag, result) 127 | } 128 | } 129 | return results, nil 130 | } 131 | 132 | func buildFeed(scanspec ScanSpec) (string, error) { 133 | 134 | findings, err := describeScan(scanspec) 135 | if err != nil { 136 | return "", err 137 | } 138 | ecrlink := fmt.Sprintf("https://%v.console.aws.amazon.com/ecr/repositories/%v/", scanspec.Region, scanspec.Repository) 139 | feed := &feeds.Feed{ 140 | Title: fmt.Sprintf("ECR repository %v in %v", scanspec.Repository, scanspec.Region), 141 | Link: &feeds.Link{Href: ecrlink}, 142 | Description: "Details of the image scan findings across the tags: ", 143 | Author: &feeds.Author{Name: "ECR"}, 144 | } 145 | for tag, isfindings := range findings { 146 | for _, finding := range isfindings.Findings { 147 | title := fmt.Sprintf("[%v] in image %v:%v found %v", *finding.Severity, scanspec.Repository, tag, *finding.Name) 148 | link := *finding.Uri 149 | desc := *finding.Description 150 | item := &feeds.Item{ 151 | Title: title, 152 | Link: &feeds.Link{Href: link}, 153 | Description: desc, 154 | Id: tag, 155 | Created: *isfindings.ImageScanCompletedAt, 156 | } 157 | feed.Items = append(feed.Items, item) 158 | } 159 | feed.Description += "[" + tag + "] " 160 | } 161 | 162 | findingsfeed, err := feed.ToAtom() 163 | if err != nil { 164 | return "", err 165 | } 166 | return findingsfeed, nil 167 | } 168 | 169 | func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 170 | configbucket := os.Getenv("ECR_SCAN_CONFIG_BUCKET") 171 | fmt.Printf("DEBUG:: findings start\n") 172 | // validate ID in URL path: 173 | if _, ok := request.PathParameters["id"]; !ok { 174 | return serverError(fmt.Errorf("Unknown configuration")) 175 | } 176 | cfg, err := config.LoadDefaultConfig(context.TODO()) 177 | if err != nil { 178 | fmt.Println(err) 179 | return serverError(err) 180 | } 181 | svc := s3.NewFromConfig(cfg) 182 | fmt.Printf("Scanning bucket %v for scan specs\n", configbucket) 183 | resp, err := svc.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{ 184 | Bucket: &configbucket, 185 | }, 186 | ) 187 | // resp, err := req.Send(context.TODO()) 188 | if err != nil { 189 | fmt.Println(err) 190 | return serverError(err) 191 | } 192 | for _, obj := range resp.Contents { 193 | fn := *obj.Key 194 | scanID := strings.TrimSuffix(fn, ".json") 195 | if scanID == request.PathParameters["id"] { 196 | scanspec, err := fetchScanSpec(configbucket, scanID) 197 | if err != nil { 198 | fmt.Println(err) 199 | return serverError(err) 200 | } 201 | findingsfeed, err := buildFeed(scanspec) 202 | if err != nil { 203 | fmt.Println(err) 204 | return serverError(err) 205 | } 206 | fmt.Printf("DEBUG:: findings done\n") 207 | return events.APIGatewayProxyResponse{ 208 | StatusCode: http.StatusOK, 209 | Headers: map[string]string{ 210 | "Content-Type": "application/atom+xml", 211 | "Access-Control-Allow-Origin": "*", 212 | }, 213 | Body: findingsfeed, 214 | }, nil 215 | } 216 | } 217 | return events.APIGatewayProxyResponse{ 218 | StatusCode: http.StatusNotFound, 219 | Headers: map[string]string{ 220 | "Content-Type": "application/json", 221 | "Access-Control-Allow-Origin": "*", 222 | }, 223 | Body: "This scan config does not exist, no operation performed", 224 | }, nil 225 | } 226 | 227 | func main() { 228 | lambda.Start(handler) 229 | } 230 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module ecr.amazon.com 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/aws/aws-lambda-go v1.26.0 7 | github.com/aws/aws-sdk-go v1.40.25 8 | github.com/aws/aws-sdk-go-v2/config v1.6.0 9 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.4.0 10 | github.com/aws/aws-sdk-go-v2/service/s3 v1.12.0 11 | github.com/gorilla/feeds v1.1.1 12 | github.com/kr/pretty v0.3.0 // indirect 13 | github.com/satori/go.uuid v1.2.0 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/aws/aws-lambda-go v1.26.0 h1:6ujqBpYF7tdZcBvPIccs98SpeGfrt/UOVEiexfNIdHA= 3 | github.com/aws/aws-lambda-go v1.26.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= 4 | github.com/aws/aws-sdk-go v1.40.25 h1:Depnx7O86HWgOCLD5nMto6F9Ju85Q1QuFDnbpZYQWno= 5 | github.com/aws/aws-sdk-go v1.40.25/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= 6 | github.com/aws/aws-sdk-go-v2 v1.8.0 h1:HcN6yDnHV9S7D69E7To0aUppJhiJNEzQSNcUxc7r3qo= 7 | github.com/aws/aws-sdk-go-v2 v1.8.0/go.mod h1:xEFuWz+3TYdlPRuo+CqATbeDWIWyaT5uAPwPaWtgse0= 8 | github.com/aws/aws-sdk-go-v2/config v1.6.0 h1:rtoCnNObhVm7me+v9sA2aY+NtHNZjjWWC3ifXVci+wE= 9 | github.com/aws/aws-sdk-go-v2/config v1.6.0/go.mod h1:TNtBVmka80lRPk5+S9ZqVfFszOQAGJJ9KbT3EM3CHNU= 10 | github.com/aws/aws-sdk-go-v2/credentials v1.3.2 h1:Uud/fZzm0lqqhE8kvXYJFAJ3PGnagKoUcvHq1hXfBZw= 11 | github.com/aws/aws-sdk-go-v2/credentials v1.3.2/go.mod h1:PACKuTJdt6AlXvEq8rFI4eDmoqDFC5DpVKQbWysaDgM= 12 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.4.0 h1:SGqDJun6tydgsSIFxv9+EYBJVqVUwg2QMJp6PbNq8C8= 13 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.4.0/go.mod h1:Mj/U8OpDbcVcoctrYwA2bak8k/HFPdcLzI/vaiXMwuM= 14 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.4.0 h1:Iqp2aHeRF3kaaNuDS82bHBzER285NM6lLPAgsxHCR2A= 15 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.4.0/go.mod h1:eHwXu2+uE/T6gpnYWwBwqoeqRf9IXyCcolyOWDRAErQ= 16 | github.com/aws/aws-sdk-go-v2/internal/ini v1.2.0 h1:xu45foJnwMwBqSkIMKyJP9kbyHi5hdhZ/WiJ7D2sHZ0= 17 | github.com/aws/aws-sdk-go-v2/internal/ini v1.2.0/go.mod h1:Q5jATQc+f1MfZp3PDMhn6ry18hGvE0i8yvbXoKbnZaE= 18 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.2.2 h1:YcGVEqLQGHDa81776C3daai6ZkkRGf/8RAQ07hV0QcU= 19 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.2.2/go.mod h1:EASdTcM1lGhUe1/p4gkojHwlGJkeoRjjr1sRCzup3Is= 20 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.2.2 h1:Xv1rGYgsRRn0xw9JFNnfpBMZam54PrWpC4rJOJ9koA8= 21 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.2.2/go.mod h1:NXmNI41bdEsJMrD0v9rUvbGCB5GwdBEpKvUvIY3vTFg= 22 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.5.2 h1:ewIpdVz12MDinJJB/nu1uUiFIWFnvtd3iV7cEW7lR+M= 23 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.5.2/go.mod h1:QuL2Ym8BkrLmN4lUofXYq6000/i5jPjosCNK//t6gak= 24 | github.com/aws/aws-sdk-go-v2/service/s3 v1.12.0 h1:cxZbzTYXgiQrZ6u2/RJZAkkgZssqYOdydvJPBgIHlsM= 25 | github.com/aws/aws-sdk-go-v2/service/s3 v1.12.0/go.mod h1:6J++A5xpo7QDsIeSqPK4UHqMSyPOCopa+zKtqAMhqVQ= 26 | github.com/aws/aws-sdk-go-v2/service/sso v1.3.2 h1:b+U3WrF9ON3f32FH19geqmiod4uKcMv/q+wosQjjyyM= 27 | github.com/aws/aws-sdk-go-v2/service/sso v1.3.2/go.mod h1:J21I6kF+d/6XHVk7kp/cx9YVD2TMD2TbLwtRGVcinXo= 28 | github.com/aws/aws-sdk-go-v2/service/sts v1.6.1 h1:1Pls85C5CFjhE3aH+h85/hyAk89kQNlAWlEQtIkaFyc= 29 | github.com/aws/aws-sdk-go-v2/service/sts v1.6.1/go.mod h1:hLZ/AnkIKHLuPGjEiyghNEdvJ2PP0MgOxcmv9EBJ4xs= 30 | github.com/aws/smithy-go v1.7.0 h1:+cLHMRrDZvQ4wk+KuQ9yH6eEg6KZEJ9RI2IkDqnygCg= 31 | github.com/aws/smithy-go v1.7.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= 32 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 33 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 34 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 35 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 36 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 37 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 38 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 39 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 40 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 41 | github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY= 42 | github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA= 43 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 44 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 45 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 46 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 47 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 48 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 49 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 50 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 51 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 52 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 53 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 54 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 55 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 56 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 57 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 58 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 59 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 60 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 61 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 62 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 63 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 64 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 65 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 66 | github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 67 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 68 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 71 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 72 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 73 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 74 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 75 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 76 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 77 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 78 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 79 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 80 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 81 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 82 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 83 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= 84 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 85 | -------------------------------------------------------------------------------- /scan-config-amazonlinux.json: -------------------------------------------------------------------------------- 1 | { 2 | "region": "us-west-2", 3 | "registry": "148658015984", 4 | "repository": "amazonlinux", 5 | "tags": [ 6 | "2018.03" 7 | ] 8 | } -------------------------------------------------------------------------------- /scan-config-centos.json: -------------------------------------------------------------------------------- 1 | { 2 | "region": "us-west-2", 3 | "registry": "148658015984", 4 | "repository": "test/centos" 5 | } -------------------------------------------------------------------------------- /scan-config-ubuntu.json: -------------------------------------------------------------------------------- 1 | { 2 | "region": "us-west-2", 3 | "registry": "148658015984", 4 | "repository": "test/ubuntu", 5 | "tags": [ 6 | "16.04", 7 | "latest" 8 | ] 9 | } -------------------------------------------------------------------------------- /scan-findindings-feed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ecr-continuous-scan/31c5088d394c077446b2c95dd49dd370ee0acd64/scan-findindings-feed.png -------------------------------------------------------------------------------- /start-scan/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/aws/aws-lambda-go/lambda" 11 | "github.com/aws/aws-sdk-go-v2/config" 12 | "github.com/aws/aws-sdk-go-v2/service/s3" 13 | "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 14 | "github.com/aws/aws-sdk-go/aws" 15 | "github.com/aws/aws-sdk-go/aws/session" 16 | "github.com/aws/aws-sdk-go/service/ecr" 17 | ) 18 | 19 | // ScanSpec represents configuration for the target repository 20 | type ScanSpec struct { 21 | // ID is a unique identifier for the scan spec 22 | ID string `json:"id"` 23 | // CreationTime is the UTC timestamp of when the scan spec was created 24 | CreationTime string `json:"created"` 25 | // Region specifies the region the repository is in 26 | Region string `json:"region"` 27 | // RegistryID specifies the registry ID 28 | RegistryID string `json:"registry"` 29 | // Repository specifies the repository name 30 | Repository string `json:"repository"` 31 | // Tags to take into consideration, if empty, all tags will be scanned 32 | Tags []string `json:"tags"` 33 | } 34 | 35 | func startScan(scanspec ScanSpec) error { 36 | s := session.Must(session.NewSession(&aws.Config{ 37 | Region: aws.String(scanspec.Region), 38 | })) 39 | svc := ecr.New(s) 40 | scaninput := &ecr.StartImageScanInput{ 41 | RepositoryName: &scanspec.Repository, 42 | RegistryId: &scanspec.RegistryID, 43 | } 44 | switch len(scanspec.Tags) { 45 | case 0: // empty list of tags, scan all tags: 46 | fmt.Printf("DEBUG:: scanning all tags for repo %v\n", scanspec.Repository) 47 | lio, err := svc.ListImages(&ecr.ListImagesInput{ 48 | RepositoryName: &scanspec.Repository, 49 | RegistryId: &scanspec.RegistryID, 50 | Filter: &ecr.ListImagesFilter{ 51 | TagStatus: aws.String("TAGGED"), 52 | }, 53 | }) 54 | if err != nil { 55 | fmt.Println(err) 56 | return err 57 | } 58 | for _, iid := range lio.ImageIds { 59 | scaninput.ImageId = iid 60 | result, err := svc.StartImageScan(scaninput) 61 | if err != nil { 62 | fmt.Println(err) 63 | return err 64 | } 65 | fmt.Printf("DEBUG:: result for tag %v: %v\n", *iid.ImageTag, result) 66 | } 67 | 68 | default: // iterate over the tags specified in the config: 69 | fmt.Printf("DEBUG:: scanning tags %v for repo %v\n", scanspec.Tags, scanspec.Repository) 70 | for _, tag := range scanspec.Tags { 71 | scaninput.ImageId = &ecr.ImageIdentifier{ 72 | ImageTag: aws.String(tag), 73 | } 74 | result, err := svc.StartImageScan(scaninput) 75 | if err != nil { 76 | fmt.Println(err) 77 | return err 78 | } 79 | fmt.Printf("DEBUG:: result for tag %v: %v\n", tag, result) 80 | } 81 | } 82 | return nil 83 | } 84 | 85 | // fetchScanSpec returns the scan spec 86 | // in a given bucket, with a given scan ID 87 | func fetchScanSpec(configbucket, scanid string) (ScanSpec, error) { 88 | ss := ScanSpec{} 89 | cfg, err := config.LoadDefaultConfig(context.TODO()) 90 | if err != nil { 91 | return ss, err 92 | } 93 | 94 | // Create an S3 Client with the config 95 | client := s3.NewFromConfig(cfg) 96 | 97 | // Create an uploader passing it the client 98 | downloader := manager.NewDownloader(client) 99 | 100 | buf := aws.NewWriteAtBuffer([]byte{}) 101 | _, err = downloader.Download(context.TODO(), buf, &s3.GetObjectInput{ 102 | Bucket: aws.String(configbucket), 103 | Key: aws.String(scanid + ".json"), 104 | }) 105 | if err != nil { 106 | return ss, err 107 | } 108 | err = json.Unmarshal(buf.Bytes(), &ss) 109 | if err != nil { 110 | return ss, err 111 | } 112 | return ss, nil 113 | } 114 | 115 | func handler() error { 116 | configbucket := os.Getenv("ECR_SCAN_CONFIG_BUCKET") 117 | fmt.Printf("DEBUG:: scan start\n") 118 | cfg, err := config.LoadDefaultConfig(context.TODO()) 119 | if err != nil { 120 | fmt.Println(err) 121 | return err 122 | } 123 | svc := s3.NewFromConfig(cfg) 124 | fmt.Printf("Scanning bucket %v for scan specs\n", configbucket) 125 | resp, err := svc.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{ 126 | Bucket: &configbucket, 127 | }, 128 | ) 129 | // resp, err := req.Send(context.TODO()) 130 | if err != nil { 131 | fmt.Println(err) 132 | return err 133 | } 134 | for _, obj := range resp.Contents { 135 | fn := *obj.Key 136 | scanID := strings.TrimSuffix(fn, ".json") 137 | scanspec, err := fetchScanSpec(configbucket, scanID) 138 | if err != nil { 139 | fmt.Println(err) 140 | return err 141 | } 142 | err = startScan(scanspec) 143 | if err != nil { 144 | fmt.Println(err) 145 | return err 146 | } 147 | } 148 | fmt.Printf("DEBUG:: scan done\n") 149 | return nil 150 | } 151 | 152 | func main() { 153 | lambda.Start(handler) 154 | } 155 | -------------------------------------------------------------------------------- /summary/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | _ "image/jpeg" 8 | _ "image/png" 9 | "net/http" 10 | "os" 11 | "strings" 12 | 13 | "github.com/aws/aws-lambda-go/events" 14 | "github.com/aws/aws-lambda-go/lambda" 15 | "github.com/aws/aws-sdk-go-v2/config" 16 | "github.com/aws/aws-sdk-go-v2/service/s3" 17 | "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 18 | "github.com/aws/aws-sdk-go/aws" 19 | "github.com/aws/aws-sdk-go/aws/session" 20 | "github.com/aws/aws-sdk-go/service/ecr" 21 | ) 22 | 23 | // ScanSpec represents configuration for the target repository 24 | type ScanSpec struct { 25 | // ID is a unique identifier for the scan spec 26 | ID string `json:"id"` 27 | // CreationTime is the UTC timestamp of when the scan spec was created 28 | CreationTime string `json:"created"` 29 | // Region specifies the region the repository is in 30 | Region string `json:"region"` 31 | // RegistryID specifies the registry ID 32 | RegistryID string `json:"registry"` 33 | // Repository specifies the repository name 34 | Repository string `json:"repository"` 35 | // Tags to take into consideration, if empty, all tags will be scanned 36 | Tags []string `json:"tags"` 37 | } 38 | 39 | func serverError(err error) (events.APIGatewayProxyResponse, error) { 40 | fmt.Println(err.Error()) 41 | return events.APIGatewayProxyResponse{ 42 | StatusCode: http.StatusInternalServerError, 43 | Headers: map[string]string{ 44 | "Access-Control-Allow-Origin": "*", 45 | }, 46 | Body: fmt.Sprintf("%v", err.Error()), 47 | }, nil 48 | } 49 | 50 | // fetchScanSpec returns the scan spec 51 | // in a given bucket, with a given scan ID 52 | func fetchScanSpec(configbucket, scanid string) (ScanSpec, error) { 53 | ss := ScanSpec{} 54 | cfg, err := config.LoadDefaultConfig(context.TODO()) 55 | if err != nil { 56 | return ss, err 57 | } 58 | 59 | // Create an S3 Client with the config 60 | client := s3.NewFromConfig(cfg) 61 | 62 | // Create an uploader passing it the client 63 | downloader := manager.NewDownloader(client) 64 | 65 | buf := aws.NewWriteAtBuffer([]byte{}) 66 | _, err = downloader.Download(context.TODO(), buf, &s3.GetObjectInput{ 67 | Bucket: aws.String(configbucket), 68 | Key: aws.String(scanid + ".json"), 69 | }) 70 | if err != nil { 71 | return ss, err 72 | } 73 | err = json.Unmarshal(buf.Bytes(), &ss) 74 | if err != nil { 75 | return ss, err 76 | } 77 | return ss, nil 78 | } 79 | 80 | func describeScan(scanspec ScanSpec) (map[string]ecr.ImageScanFindings, error) { 81 | s := session.Must(session.NewSession(&aws.Config{ 82 | Region: aws.String(scanspec.Region), 83 | })) 84 | svc := ecr.New(s) 85 | descinput := &ecr.DescribeImageScanFindingsInput{ 86 | RepositoryName: &scanspec.Repository, 87 | RegistryId: &scanspec.RegistryID, 88 | } 89 | results := map[string]ecr.ImageScanFindings{} 90 | switch len(scanspec.Tags) { 91 | case 0: // empty list of tags, describe all tags: 92 | fmt.Printf("DEBUG:: scanning all tags for repo %v\n", scanspec.Repository) 93 | lio, err := svc.ListImages(&ecr.ListImagesInput{ 94 | RepositoryName: &scanspec.Repository, 95 | RegistryId: &scanspec.RegistryID, 96 | Filter: &ecr.ListImagesFilter{ 97 | TagStatus: aws.String("TAGGED"), 98 | }, 99 | }) 100 | if err != nil { 101 | fmt.Println(err) 102 | return results, err 103 | } 104 | for _, iid := range lio.ImageIds { 105 | descinput.ImageId = iid 106 | result, err := svc.DescribeImageScanFindings(descinput) 107 | if err != nil { 108 | return results, err 109 | } 110 | results[*iid.ImageTag] = *result.ImageScanFindings 111 | // fmt.Printf("DEBUG:: result for tag %v: %v\n", *iid.ImageTag, result) 112 | } 113 | default: // iterate over the tags specified in the config: 114 | fmt.Printf("DEBUG:: scanning tags %v for repo %v\n", scanspec.Tags, scanspec.Repository) 115 | for _, tag := range scanspec.Tags { 116 | descinput.ImageId = &ecr.ImageIdentifier{ 117 | ImageTag: aws.String(tag), 118 | } 119 | result, err := svc.DescribeImageScanFindings(descinput) 120 | if err != nil { 121 | fmt.Println(err) 122 | return results, err 123 | } 124 | results[tag] = *result.ImageScanFindings 125 | // fmt.Printf("DEBUG:: result for tag %v: %v\n", tag, result) 126 | } 127 | } 128 | return results, nil 129 | } 130 | 131 | func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 132 | configbucket := os.Getenv("ECR_SCAN_CONFIG_BUCKET") 133 | fmt.Printf("DEBUG:: summary start\n") 134 | cfg, err := config.LoadDefaultConfig(context.TODO()) 135 | if err != nil { 136 | fmt.Println(err) 137 | return serverError(err) 138 | } 139 | svc := s3.NewFromConfig(cfg) 140 | fmt.Printf("Scanning bucket %v for scan specs\n", configbucket) 141 | resp, err := svc.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{ 142 | Bucket: &configbucket, 143 | }, 144 | ) 145 | // resp, err := req.Send(context.TODO()) 146 | if err != nil { 147 | fmt.Println(err) 148 | return serverError(err) 149 | } 150 | ssresult := "" 151 | for _, obj := range resp.Contents { 152 | fn := *obj.Key 153 | scanID := strings.TrimSuffix(fn, ".json") 154 | scanspec, err := fetchScanSpec(configbucket, scanID) 155 | if err != nil { 156 | fmt.Println(err) 157 | return serverError(err) 158 | } 159 | results, err := describeScan(scanspec) 160 | if err != nil { 161 | fmt.Println(err) 162 | return serverError(err) 163 | } 164 | for tag, result := range results { 165 | sevcount := "" 166 | for sev, count := range result.FindingSeverityCounts { 167 | sevcount += fmt.Sprintf(" %v: %v\n", sev, *count) 168 | } 169 | ssresult += fmt.Sprintf("Results for %v:%v in %v:\n%v\n\n", scanspec.Repository, tag, scanspec.Region, sevcount) 170 | } 171 | } 172 | 173 | fmt.Printf("DEBUG:: summary done\n") 174 | return events.APIGatewayProxyResponse{ 175 | StatusCode: http.StatusOK, 176 | Headers: map[string]string{ 177 | "Content-Type": "application/json", 178 | "Access-Control-Allow-Origin": "*", 179 | }, 180 | Body: ssresult, 181 | }, nil 182 | } 183 | 184 | func main() { 185 | lambda.Start(handler) 186 | } 187 | -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: ECR Continuous Scan 4 | 5 | Globals: 6 | Function: 7 | Timeout: 30 8 | Api: 9 | Cors: 10 | AllowMethods: "'*'" 11 | AllowHeaders: "'*'" 12 | AllowOrigin: "'*'" 13 | 14 | Parameters: 15 | ConfigBucketName: 16 | Type: String 17 | 18 | Resources: 19 | ConfigsFunc: 20 | Type: AWS::Serverless::Function 21 | Properties: 22 | CodeUri: bin/ 23 | Handler: configs 24 | Runtime: go1.x 25 | Tracing: Active 26 | Environment: 27 | Variables: 28 | ECR_SCAN_CONFIG_BUCKET: !Sub "${ConfigBucketName}" 29 | Events: 30 | AddConfig: 31 | Type: Api 32 | Properties: 33 | Path: /configs 34 | Method: POST 35 | RemoveConfig: 36 | Type: Api 37 | Properties: 38 | Path: /configs/{id} 39 | Method: DELETE 40 | ListConfigs: 41 | Type: Api 42 | Properties: 43 | Path: /configs 44 | Method: GET 45 | Policies: 46 | - AWSLambdaExecute 47 | - Version: '2012-10-17' 48 | Statement: 49 | - Effect: Allow 50 | Action: 51 | - s3:* 52 | Resource: 53 | - !Sub "arn:aws:s3:::${ConfigBucketName}/*" 54 | - !Sub "arn:aws:s3:::${ConfigBucketName}" 55 | SummaryFunc: 56 | Type: AWS::Serverless::Function 57 | Properties: 58 | CodeUri: bin/ 59 | Handler: summary 60 | Runtime: go1.x 61 | Tracing: Active 62 | Environment: 63 | Variables: 64 | ECR_SCAN_CONFIG_BUCKET: !Sub "${ConfigBucketName}" 65 | Events: 66 | CatchAll: 67 | Type: Api 68 | Properties: 69 | Path: /summary 70 | Method: GET 71 | Policies: 72 | - AWSLambdaExecute 73 | - Version: '2012-10-17' 74 | Statement: 75 | - Effect: Allow 76 | Action: 77 | - ecr:* 78 | Resource: '*' 79 | - Effect: Allow 80 | Action: 81 | - s3:* 82 | Resource: 83 | - !Sub "arn:aws:s3:::${ConfigBucketName}/*" 84 | - !Sub "arn:aws:s3:::${ConfigBucketName}" 85 | FindingsFunc: 86 | Type: AWS::Serverless::Function 87 | Properties: 88 | CodeUri: bin/ 89 | Handler: findings 90 | Runtime: go1.x 91 | Tracing: Active 92 | Environment: 93 | Variables: 94 | ECR_SCAN_CONFIG_BUCKET: !Sub "${ConfigBucketName}" 95 | Events: 96 | CatchAll: 97 | Type: Api 98 | Properties: 99 | Path: /findings/{id} 100 | Method: GET 101 | Policies: 102 | - AWSLambdaExecute 103 | - Version: '2012-10-17' 104 | Statement: 105 | - Effect: Allow 106 | Action: 107 | - ecr:* 108 | Resource: '*' 109 | - Effect: Allow 110 | Action: 111 | - s3:* 112 | Resource: 113 | - !Sub "arn:aws:s3:::${ConfigBucketName}/*" 114 | - !Sub "arn:aws:s3:::${ConfigBucketName}" 115 | StartScanFunc: 116 | Type: AWS::Serverless::Function 117 | Properties: 118 | CodeUri: bin/ 119 | Handler: start-scan 120 | Runtime: go1.x 121 | Tracing: Active 122 | Environment: 123 | Variables: 124 | ECR_SCAN_CONFIG_BUCKET: !Sub "${ConfigBucketName}" 125 | Events: 126 | Timer: 127 | Type: Schedule 128 | Properties: 129 | Schedule: rate(24 hours) 130 | Policies: 131 | - AWSLambdaExecute 132 | - Version: '2012-10-17' 133 | Statement: 134 | - Effect: Allow 135 | Action: 136 | - ecr:* 137 | Resource: '*' 138 | - Effect: Allow 139 | Action: 140 | - s3:* 141 | Resource: 142 | - !Sub "arn:aws:s3:::${ConfigBucketName}/*" 143 | - !Sub "arn:aws:s3:::${ConfigBucketName}" 144 | 145 | 146 | Outputs: 147 | ECRScanAPIEndpoint: 148 | Description: "The ECR Continuous Scan HTTP API Gateway endpoint URL" 149 | Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod" 150 | 151 | --------------------------------------------------------------------------------