From 9f128679de3ec5dbb7f4680b95b977300068fb23 Mon Sep 17 00:00:00 2001 From: Apoorv Munshi Date: Sat, 26 Sep 2020 18:37:28 +0000 Subject: [PATCH 1/5] feat(ecr): add imageTagMutability prop This property allows setting tag mutability on ECR repositoes. Tag mutability is useful to ensure image integrity and can prevent supply chain attacks. Closes #4640 --- packages/@aws-cdk/aws-ecr/lib/repository.ts | 29 ++++++++++++++++++ .../aws-ecr/test/integ.basic.expected.json | 3 +- .../test/integ.imagescan.expected.json | 3 ++ .../@aws-cdk/aws-ecr/test/test.repository.ts | 30 +++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index ed71c1caf3576..c0dd2f1782a1e 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -324,6 +324,13 @@ export interface RepositoryProps { * @default false */ readonly imageScanOnPush?: boolean; + + /** + * The tag mutability setting for the repository. If this parameter is omitted, the default setting of MUTABLE will be used which will allow image tags to be overwritten. + * + * @default TagMutability.MUTABLE + */ + readonly imageTagMutability?: TagMutability; } export interface RepositoryAttributes { @@ -407,6 +414,7 @@ export class Repository extends RepositoryBase { public readonly repositoryArn: string; private readonly lifecycleRules = new Array(); private readonly registryId?: string; + public readonly imageTagMutability?: string private policyDocument?: iam.PolicyDocument; constructor(scope: Construct, id: string, props: RepositoryProps = {}) { @@ -419,6 +427,7 @@ export class Repository extends RepositoryBase { // It says "Text", but they actually mean "Object". repositoryPolicyText: Lazy.anyValue({ produce: () => this.policyDocument }), lifecyclePolicy: Lazy.anyValue({ produce: () => this.renderLifecyclePolicy() }), + imageTagMutability: props.imageTagMutability || TagMutability.MUTABLE, }); resource.applyRemovalPolicy(props.removalPolicy); @@ -435,6 +444,10 @@ export class Repository extends RepositoryBase { resourceName: this.physicalName, }); + // repository image tag mutability + + this.imageTagMutability = props.imageTagMutability || TagMutability.MUTABLE; + // image scanOnPush if (props.imageScanOnPush) { new cr.AwsCustomResource(this, 'ImageScanOnPush', { @@ -607,3 +620,19 @@ const enum CountType { */ SINCE_IMAGE_PUSHED = 'sinceImagePushed', } + +/** + * The tag mutability setting for your repository. + */ +export enum TagMutability { + /** + * allow image tags to be overwritten. + */ + MUTABLE = 'MUTABLE', + + /** + * all image tags within the repository will be immutable which will prevent them from being overwritten. + */ + IMMUTABLE = 'IMMUTABLE', + +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecr/test/integ.basic.expected.json b/packages/@aws-cdk/aws-ecr/test/integ.basic.expected.json index 7fa399898aa8e..b8a6b86cc38ac 100644 --- a/packages/@aws-cdk/aws-ecr/test/integ.basic.expected.json +++ b/packages/@aws-cdk/aws-ecr/test/integ.basic.expected.json @@ -5,7 +5,8 @@ "Properties": { "LifecyclePolicy": { "LifecyclePolicyText": "{\"rules\":[{\"rulePriority\":1,\"selection\":{\"tagStatus\":\"any\",\"countType\":\"imageCountMoreThan\",\"countNumber\":5},\"action\":{\"type\":\"expire\"}}]}" - } + }, + "ImageTagMutability": "MUTABLE" }, "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" diff --git a/packages/@aws-cdk/aws-ecr/test/integ.imagescan.expected.json b/packages/@aws-cdk/aws-ecr/test/integ.imagescan.expected.json index 76cd5933128c1..e33284dd508b9 100644 --- a/packages/@aws-cdk/aws-ecr/test/integ.imagescan.expected.json +++ b/packages/@aws-cdk/aws-ecr/test/integ.imagescan.expected.json @@ -2,6 +2,9 @@ "Resources": { "Repo02AC86CF": { "Type": "AWS::ECR::Repository", + "Properties": { + "ImageTagMutability": "MUTABLE" + }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, diff --git a/packages/@aws-cdk/aws-ecr/test/test.repository.ts b/packages/@aws-cdk/aws-ecr/test/test.repository.ts index 73e5f21d28115..0abbb1d5a1f84 100644 --- a/packages/@aws-cdk/aws-ecr/test/test.repository.ts +++ b/packages/@aws-cdk/aws-ecr/test/test.repository.ts @@ -19,6 +19,9 @@ export = { Resources: { Repo02AC86CF: { Type: 'AWS::ECR::Repository', + Properties: { + ImageTagMutability: 'MUTABLE', + }, DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', }, @@ -59,6 +62,33 @@ export = { test.done(); }, + + 'image tag mutability can be set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + new ecr.Repository(stack, 'Repo', { imageTagMutability: ecr.TagMutability.IMMUTABLE }); + + // THEN + expect(stack).to(haveResource('AWS::ECR::Repository', { + ImageTagMutability: 'IMMUTABLE', + })); + + test.done(); + }, + + 'default tag mutability is set to MUTABLE'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + new ecr.Repository(stack, 'Repo'); + + // THEN + expect(stack).to(haveResource('AWS::ECR::Repository', { + ImageTagMutability: 'MUTABLE', + })); + + test.done(); + }, + 'add day-based lifecycle policy'(test: Test) { // GIVEN const stack = new cdk.Stack(); From be1423ee61d3c56270de5dbe81f6734ba627c254 Mon Sep 17 00:00:00 2001 From: Apoorv Munshi Date: Sat, 26 Sep 2020 11:55:46 -0700 Subject: [PATCH 2/5] docs(ecr): update README.md add tag immutability example in description --- packages/@aws-cdk/aws-ecr/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/@aws-cdk/aws-ecr/README.md b/packages/@aws-cdk/aws-ecr/README.md index 97115adc5f647..101dc88b43d0c 100644 --- a/packages/@aws-cdk/aws-ecr/README.md +++ b/packages/@aws-cdk/aws-ecr/README.md @@ -37,6 +37,13 @@ repository.onImageScanCompleted('ImageScanComplete') .addTarget(...) ``` +### Image tag immutability + +You can set tag immutability on images in our repository using the `imageTagMutability` construct prop. + +```ts +new ecr.Repository(stack, 'Repo', { imageTagMutability: ecr.TagMutability.IMMUTABLE }); +``` ### Automatically clean up repositories From f2d89d214a71225e67bda42aa1129f24eb10328d Mon Sep 17 00:00:00 2001 From: ap00rv Date: Sat, 26 Sep 2020 21:02:47 +0000 Subject: [PATCH 3/5] doc(ecr): add imageTagMutability prop exclusion to awslint --- packages/@aws-cdk/aws-ecr/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@aws-cdk/aws-ecr/package.json b/packages/@aws-cdk/aws-ecr/package.json index 0521418925521..de1e36989f2b3 100644 --- a/packages/@aws-cdk/aws-ecr/package.json +++ b/packages/@aws-cdk/aws-ecr/package.json @@ -98,6 +98,7 @@ "import:@aws-cdk/aws-ecr.Repository", "construct-base-is-private:@aws-cdk/aws-ecr.RepositoryBase", "docs-public-apis:@aws-cdk/aws-ecr.Repository.fromRepositoryArn", + "docs-public-apis:@aws-cdk/aws-ecr.Repository.imageTagMutability", "docs-public-apis:@aws-cdk/aws-ecr.Repository.fromRepositoryName", "props-default-doc:@aws-cdk/aws-ecr.LifecycleRule.maxImageAge", "props-default-doc:@aws-cdk/aws-ecr.LifecycleRule.maxImageCount", From c3317e7d37b7348369f9bc7b6cc8376acc51849e Mon Sep 17 00:00:00 2001 From: ap00rv Date: Sat, 26 Sep 2020 21:55:14 +0000 Subject: [PATCH 4/5] fix(ecr): revert setting imageTagMutability prop by default In earlier commits, the imageTagMutability prop was set to MUTABLE by default, this commit reverts that change and fixes relevant tests --- packages/@aws-cdk/aws-ecr/lib/repository.ts | 7 ++++--- .../aws-ecr/test/integ.basic.expected.json | 3 +-- .../aws-ecr/test/integ.imagescan.expected.json | 3 --- .../@aws-cdk/aws-ecr/test/test.repository.ts | 16 ---------------- 4 files changed, 5 insertions(+), 24 deletions(-) diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index c0dd2f1782a1e..7c7b3d8ad1fdb 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -427,7 +427,6 @@ export class Repository extends RepositoryBase { // It says "Text", but they actually mean "Object". repositoryPolicyText: Lazy.anyValue({ produce: () => this.policyDocument }), lifecyclePolicy: Lazy.anyValue({ produce: () => this.renderLifecyclePolicy() }), - imageTagMutability: props.imageTagMutability || TagMutability.MUTABLE, }); resource.applyRemovalPolicy(props.removalPolicy); @@ -445,8 +444,10 @@ export class Repository extends RepositoryBase { }); // repository image tag mutability - - this.imageTagMutability = props.imageTagMutability || TagMutability.MUTABLE; + if (props.imageTagMutability !== undefined) { + this.imageTagMutability = props.imageTagMutability; + resource.imageTagMutability = props.imageTagMutability; + } // image scanOnPush if (props.imageScanOnPush) { diff --git a/packages/@aws-cdk/aws-ecr/test/integ.basic.expected.json b/packages/@aws-cdk/aws-ecr/test/integ.basic.expected.json index b8a6b86cc38ac..7fa399898aa8e 100644 --- a/packages/@aws-cdk/aws-ecr/test/integ.basic.expected.json +++ b/packages/@aws-cdk/aws-ecr/test/integ.basic.expected.json @@ -5,8 +5,7 @@ "Properties": { "LifecyclePolicy": { "LifecyclePolicyText": "{\"rules\":[{\"rulePriority\":1,\"selection\":{\"tagStatus\":\"any\",\"countType\":\"imageCountMoreThan\",\"countNumber\":5},\"action\":{\"type\":\"expire\"}}]}" - }, - "ImageTagMutability": "MUTABLE" + } }, "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" diff --git a/packages/@aws-cdk/aws-ecr/test/integ.imagescan.expected.json b/packages/@aws-cdk/aws-ecr/test/integ.imagescan.expected.json index e33284dd508b9..76cd5933128c1 100644 --- a/packages/@aws-cdk/aws-ecr/test/integ.imagescan.expected.json +++ b/packages/@aws-cdk/aws-ecr/test/integ.imagescan.expected.json @@ -2,9 +2,6 @@ "Resources": { "Repo02AC86CF": { "Type": "AWS::ECR::Repository", - "Properties": { - "ImageTagMutability": "MUTABLE" - }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, diff --git a/packages/@aws-cdk/aws-ecr/test/test.repository.ts b/packages/@aws-cdk/aws-ecr/test/test.repository.ts index 0abbb1d5a1f84..db653928a6db1 100644 --- a/packages/@aws-cdk/aws-ecr/test/test.repository.ts +++ b/packages/@aws-cdk/aws-ecr/test/test.repository.ts @@ -19,9 +19,6 @@ export = { Resources: { Repo02AC86CF: { Type: 'AWS::ECR::Repository', - Properties: { - ImageTagMutability: 'MUTABLE', - }, DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', }, @@ -76,19 +73,6 @@ export = { test.done(); }, - 'default tag mutability is set to MUTABLE'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - new ecr.Repository(stack, 'Repo'); - - // THEN - expect(stack).to(haveResource('AWS::ECR::Repository', { - ImageTagMutability: 'MUTABLE', - })); - - test.done(); - }, - 'add day-based lifecycle policy'(test: Test) { // GIVEN const stack = new cdk.Stack(); From 31494b973070b9a67231af3b42c712bb4835a8f9 Mon Sep 17 00:00:00 2001 From: ap00rv Date: Sun, 18 Oct 2020 17:14:27 +0000 Subject: [PATCH 5/5] fix(ecr): set tag mutability in cfn resource Also removed it as a contstruct prop. --- packages/@aws-cdk/aws-ecr/lib/repository.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 59b4a99016729..48ea68f5e9849 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -415,7 +415,6 @@ export class Repository extends RepositoryBase { public readonly repositoryArn: string; private readonly lifecycleRules = new Array(); private readonly registryId?: string; - public readonly imageTagMutability?: string private policyDocument?: iam.PolicyDocument; constructor(scope: Construct, id: string, props: RepositoryProps = {}) { @@ -428,6 +427,7 @@ export class Repository extends RepositoryBase { // It says "Text", but they actually mean "Object". repositoryPolicyText: Lazy.anyValue({ produce: () => this.policyDocument }), lifecyclePolicy: Lazy.anyValue({ produce: () => this.renderLifecyclePolicy() }), + imageTagMutability: props.imageTagMutability || undefined, }); resource.applyRemovalPolicy(props.removalPolicy); @@ -444,11 +444,6 @@ export class Repository extends RepositoryBase { resourceName: this.physicalName, }); - // repository image tag mutability - if (props.imageTagMutability !== undefined) { - this.imageTagMutability = props.imageTagMutability; - resource.imageTagMutability = props.imageTagMutability; - } // image scanOnPush if (props.imageScanOnPush) {