diff --git a/CHANGELOG.md b/CHANGELOG.md index 154cf77..95499b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,48 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.7.1] - 2024-05-30 +* Update dependencies & runtimes ([#186]((https://github.com/aws-solutions/aws-control-tower-customizations/issues/186)), [#193]((https://github.com/aws-solutions/aws-control-tower-customizations/issues/193))) + * Building the solution from source now requires Python 3.11 or higher + * Update Python Lambda runtimes to 3.11 + * Update Ruby version to 3.3 + * Update CodeBuild container image to `aws/codebuild/standard:7.0` +* Pinned version for `PyYAML` to 5.3.1 due to [yaml/pyyaml#724](https://github.com/yaml/pyyaml/issues/724) ([#183](https://github.com/aws-solutions/aws-control-tower-customizations/issues/183), [#184](https://github.com/aws-solutions/aws-control-tower-customizations/issues/184)) +* Pinned version for `moto` to 4.2.14. +* Add `UpdateReplacePolicy` and `DeletionPolicy` to lifecycle event queue and DLQ to improve deployment safety. + +## [2.7.0] - 2023-11-10 +- Resolve `ConcurrentModificationException` errors that occur during parallel SCP deployments due to a race condition when enabling SCPs [#175](https://github.com/aws-solutions/aws-control-tower-customizations/issues/175) +- Improve performance when querying for StackSet instance account IDs in large organizations [#174](https://github.com/aws-solutions/aws-control-tower-customizations/issues/174) +- The CFCT pipeline now triggers on `UpdateManagedAccount` Control Tower lifecycle events, in addition to `CreateManagedAccount` events [#173](https://github.com/aws-solutions/aws-control-tower-customizations/issues/173) +- Honor the `CodeCommitBranchName` stack parameter on the CFCT repo’s initial commit. The example code is now committed to your chosen branch instead of `main` [#117](https://github.com/aws-solutions/aws-control-tower-customizations/issues/117) +- Enable the use of privately registered CloudFormation resources in customization templates (for example, the `AWSUtility::CloudFormation::CommandRunner` resource type) [#76](https://github.com/aws-solutions/aws-control-tower-customizations/issues/76) +- CFCT now ignores non-existent OU targets when deploying SCPs, aligning with how non-existent OUs are treated when deploying StackSets [#126](https://github.com/aws-solutions/aws-control-tower-customizations/issues/126) + +## [2.6.0] - 2023-05-18 +- Now supported in the following regions: me-south-1, af-south-1, eu-south-1, ap-east-1, us-west-1. +- Manifest now allows the use of S3 global urls to download template files and uses regional urls as a fallback mechanism. +- Eventbased triggers for CodePipeline deployments now supported. + +## [2.5.3] - 2023-04-25 +- Bugfix: Add S3 bucket policy necessary for new CfCT deployments + +## [2.5.2] - 2022-12-12 +- Fix bug where adding a resource to the middle of the manifest file caused CFCT to submit step function executions for all remaining manifest resources even if those resources had no changes +- Drop polling wait time for step function execution status from 30s to 15s + +## [2.5.1] - 2022-10-19 +- Add support for AWS GovCloud +- Please note: using CFCT in AWS GovCloud requires the Control Tower home region to be AWS GovCloud West (us-gov-west-1) + +## [2.5.0] - 2022-08-26 +- Support for opt-in deletion of Stack Set resources. This functionality is only supported when using the manifest v2 schema. Opting in to the new functionality reduces the overhead of manually deleting resources provisioned by CfCT. + - In the manifest v2 schema, the `enable_stack_set_deletion` flag is set to `false` by default. In this configuration, when a resource is removed from Customizations for Control Tower's manifest, no actions will be taken against the StackSet removed. + - Once opting into `enable_stack_set_deletion` by setting its value to `true` in the manifest, Removing a resource in its entirety from the manifest will delete the StackSet and all owned resources. + - https://docs.aws.amazon.com/controltower/latest/userguide/cfct-delete-stack.html +>**Note:** With `enable_stack_set_deletion` set to `true`, on the next invocation of CfCT, **ALL** resources not declared in the manifest, that start with the prefix `CustomControlTower-` and have the associated Tag: `"Key": "AWS_Solutions", "Value": "CustomControlTowerStackSet"` will be deleted +- Bug Fix: Resolves a bug with CFCT versions >= 2.0.0 where using a v1 manifest format and defining a resource block without a parameter_file attribute (which is optional in v1 manifests) causes the CFCT pipeline to fail. + ## [2.4.0] - 2022-06-08 - Add support for CfCT pipeline to fail if any stack instances within a stack set deployment have failed - New template parameter `EnforceSuccessfulStackInstances` can be set to True to achieve this behaviour diff --git a/VERSION b/VERSION index 9b4b5fb..82de7e1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v2.7.1 +v2.7.2 diff --git a/customizations-for-aws-control-tower.template b/customizations-for-aws-control-tower.template index 2b4fdc1..677be5a 100644 --- a/customizations-for-aws-control-tower.template +++ b/customizations-for-aws-control-tower.template @@ -12,7 +12,7 @@ # permissions and limitations under the License. AWSTemplateFormatVersion: '2010-09-09' -Description: '(SO0089) - customizations-for-aws-control-tower Solution. Version: v2.7.1' +Description: '(SO0089) - customizations-for-aws-control-tower Solution. Version: v2.7.2' Parameters: PipelineApprovalStage: @@ -80,6 +80,11 @@ Parameters: - true - false + NoncurrentVersionExpirationDays: + Type: Number + Default: 90 + Description: Number of days after which non-current versions will be deleted + Metadata: AWS::CloudFormation::Interface: ParameterGroups: @@ -189,6 +194,11 @@ Resources: SSEAlgorithm: AES256 VersioningConfiguration: Status: Enabled + LifecycleConfiguration: + Rules: + - Id: DeleteNonCurrentVersions + Status: Enabled + NoncurrentVersionExpirationInDays: !Ref NoncurrentVersionExpirationDays LoggingConfiguration: DestinationBucketName: !Ref CustomControlTowerS3AccessLogsBucket PublicAccessBlockConfiguration: @@ -216,6 +226,11 @@ Resources: Properties: VersioningConfiguration: Status: Enabled + LifecycleConfiguration: + Rules: + - Id: DeleteNonCurrentVersions + Status: Enabled + NoncurrentVersionExpirationInDays: !Ref NoncurrentVersionExpirationDays LoggingConfiguration: DestinationBucketName: !Ref CustomControlTowerS3AccessLogsBucket BucketEncryption: @@ -249,10 +264,15 @@ Resources: cfn_nag: rules_to_suppress: - id: W35 - reason: "This S3 bucket is used as the destination for 'CustomControlTowerPipelineS3Bucket' and 'CustomControlTowerPipelineArtifactS3Bucket'" + reason: "This S3 bucket is used as the destination for 'CustomControlTowerPipelineS3Bucket', 'CustomControlTowerPipelineArtifactS3Bucket' and 'CustomControlTowerCloudTrailDataEventBucket'" Properties: VersioningConfiguration: Status: Enabled + LifecycleConfiguration: + Rules: + - Id: DeleteNonCurrentVersions + Status: Enabled + NoncurrentVersionExpirationInDays: !Ref NoncurrentVersionExpirationDays BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: @@ -298,6 +318,21 @@ Resources: "aws:SourceArn": !Sub "arn:${AWS::Partition}:s3:::${CustomControlTowerPipelineArtifactS3Bucket}" StringEquals: "aws:SourceAccount": !Ref AWS::AccountId + - !If + - IsS3PipelineSource + - Sid: EnableS3AccessLoggingForCustomControlTowerCloudTrailDataEventBucket + Effect: Allow + Principal: + Service: logging.s3.amazonaws.com + Action: + - s3:PutObject + Resource: !Sub "arn:${AWS::Partition}:s3:::${CustomControlTowerS3AccessLogsBucket}/*" + Condition: + ArnLike: + "aws:SourceArn": !Sub "arn:${AWS::Partition}:s3:::${CustomControlTowerCloudTrailDataEventBucket}" + StringEquals: + "aws:SourceAccount": !Ref AWS::AccountId + - !Ref AWS::NoValue CustomControlTowerCodeCommit: Type: AWS::CodeCommit::Repository @@ -311,7 +346,7 @@ Resources: BranchName: !Ref CodeCommitBranchName S3: Bucket: !Sub control-tower-cfct-assets-prod-${AWS::Region} - Key: !Sub customizations-for-aws-control-tower/v2.7.1/custom-control-tower-configuration-${AWS::Region}.zip + Key: !Sub customizations-for-aws-control-tower/v2.7.2/custom-control-tower-configuration-${AWS::Region}.zip # SSM Parameter to store the git repository name CustomControlTowerRepoNameParameter: @@ -572,7 +607,7 @@ Resources: - {KMSKeyName: !FindInMap [KMS, Alias, Name]} Source: Type: CODEPIPELINE - BuildSpec: "version: 0.2\nphases:\n install:\n runtime-versions:\n python: 3.11\n ruby: 3.3\n commands:\n - export current=$(pwd)\n - if [ -f manifest.yaml ];then export current=$(pwd);else if [ -f custom-control-tower-configuration/manifest.yaml ]; then export current=$(pwd)/custom-control-tower-configuration; else echo 'manifest.yaml does not exist at the root level of custom-control-tower-configuration.zip or inside custom-control-tower-configuration folder, please check the ZIP file'; exit 1; fi; fi;\n - apt-get -q update 1> /dev/null\n - apt-get -q install zip wget python3-pip libyaml-dev -y 1>/dev/null\n - export LC_ALL='en_US.UTF-8'\n - locale-gen en_US en_US.UTF-8\n - dpkg-reconfigure locales --frontend noninteractive\n pre_build:\n commands:\n - cd $current\n - echo 'Download CustomControlTower Scripts'\n - aws s3 cp --quiet s3://control-tower-cfct-assets-prod-${AWS_REGION}/customizations-for-aws-control-tower/v2.7.1/custom-control-tower-scripts.zip $current\n - unzip -q -o $current/custom-control-tower-scripts.zip -d $current\n - cp codebuild_scripts/* .\n - bash install_stage_dependencies.sh $STAGE_NAME\n build:\n commands:\n - echo 'Starting build $(date) in $(pwd)'\n - echo 'bash execute_stage_scripts.sh $STAGE_NAME $LOG_LEVEL $WAIT_TIME $SM_ARN $ARTIFACT_BUCKET $KMS_KEY_ALIAS_NAME $BOOL_VALUES $NONE_TYPE_VALUES'\n - bash execute_stage_scripts.sh $STAGE_NAME $LOG_LEVEL $WAIT_TIME $SM_ARN $ARTIFACT_BUCKET $KMS_KEY_ALIAS_NAME $BOOL_VALUES $NONE_TYPE_VALUES \n - echo 'Running build scripts completed $(date)'\n post_build:\n commands:\n - echo 'Starting post build $(date) in $(pwd)'\n - echo 'build completed on $(date)'\n\nartifacts:\n files:\n - '**/*'\n\n" + BuildSpec: "version: 0.2\nphases:\n install:\n runtime-versions:\n python: 3.11\n ruby: 3.3\n commands:\n - export current=$(pwd)\n - if [ -f manifest.yaml ];then export current=$(pwd);else if [ -f custom-control-tower-configuration/manifest.yaml ]; then export current=$(pwd)/custom-control-tower-configuration; else echo 'manifest.yaml does not exist at the root level of custom-control-tower-configuration.zip or inside custom-control-tower-configuration folder, please check the ZIP file'; exit 1; fi; fi;\n - apt-get -q update 1> /dev/null\n - apt-get -q install zip wget python3-pip libyaml-dev -y 1>/dev/null\n - export LC_ALL='en_US.UTF-8'\n - locale-gen en_US en_US.UTF-8\n - dpkg-reconfigure locales --frontend noninteractive\n pre_build:\n commands:\n - cd $current\n - echo 'Download CustomControlTower Scripts'\n - aws s3 cp --quiet s3://control-tower-cfct-assets-prod-${AWS_REGION}/customizations-for-aws-control-tower/v2.7.2/custom-control-tower-scripts.zip $current\n - unzip -q -o $current/custom-control-tower-scripts.zip -d $current\n - cp codebuild_scripts/* .\n - bash install_stage_dependencies.sh $STAGE_NAME\n build:\n commands:\n - echo 'Starting build $(date) in $(pwd)'\n - echo 'bash execute_stage_scripts.sh $STAGE_NAME $LOG_LEVEL $WAIT_TIME $SM_ARN $ARTIFACT_BUCKET $KMS_KEY_ALIAS_NAME $BOOL_VALUES $NONE_TYPE_VALUES'\n - bash execute_stage_scripts.sh $STAGE_NAME $LOG_LEVEL $WAIT_TIME $SM_ARN $ARTIFACT_BUCKET $KMS_KEY_ALIAS_NAME $BOOL_VALUES $NONE_TYPE_VALUES \n - echo 'Running build scripts completed $(date)'\n post_build:\n commands:\n - echo 'Starting post build $(date) in $(pwd)'\n - echo 'build completed on $(date)'\n\nartifacts:\n files:\n - '**/*'\n\n" Environment: ComputeType: BUILD_GENERAL1_SMALL Image: "aws/codebuild/standard:7.0" @@ -597,7 +632,7 @@ Resources: - Name: SOLUTION_ID Value: !FindInMap [ Solution, Metrics, SolutionID ] - Name: SOLUTION_VERSION - Value: v2.7.1 + Value: v2.7.2 - Name: AWS_STS_REGIONAL_ENDPOINTS Value: "regional" Artifacts: @@ -702,7 +737,7 @@ Resources: - {KMSKeyName: !FindInMap [KMS, Alias, Name]} Source: Type: CODEPIPELINE - BuildSpec: "version: 0.2\nphases:\n install:\n runtime-versions:\n python: 3.11\n ruby: 3.3\n commands:\n - export current=$(pwd)\n - if [ -f manifest.yaml ];then export current=$(pwd);else if [ -f custom-control-tower-configuration/manifest.yaml ]; then export current=$(pwd)/custom-control-tower-configuration; else echo 'manifest.yaml does not exist at the root level of custom-control-tower-configuration.zip or inside custom-control-tower-configuration folder, please check the ZIP file'; exit 1; fi; fi;\n - apt-get -q update 1> /dev/null\n - apt-get -q install zip wget python3-pip libyaml-dev -y 1> /dev/null \n pre_build:\n commands:\n - cd $current\n - echo 'Download CustomControlTower Scripts'\n - aws s3 cp --quiet s3://control-tower-cfct-assets-prod-${AWS_REGION}/customizations-for-aws-control-tower/v2.7.1/custom-control-tower-scripts.zip $current\n - unzip -q -o $current/custom-control-tower-scripts.zip -d $current\n - cp codebuild_scripts/* .\n - bash install_stage_dependencies.sh $STAGE_NAME\n build:\n commands:\n - echo 'Starting build $(date) in $(pwd)'\n - echo 'bash execute_stage_scripts.sh $STAGE_NAME $LOG_LEVEL $WAIT_TIME $SM_ARN $ARTIFACT_BUCKET $KMS_KEY_ALIAS_NAME $BOOL_VALUES $NONE_TYPE_VALUES'\n - bash execute_stage_scripts.sh $STAGE_NAME $LOG_LEVEL $WAIT_TIME $SM_ARN $ARTIFACT_BUCKET $KMS_KEY_ALIAS_NAME $BOOL_VALUES $NONE_TYPE_VALUES\n - echo 'Running build scripts completed $(date)'\n post_build:\n commands:\n - echo 'Starting post build $(date) in $(pwd)'\n - echo 'build completed on $(date)'\n\nartifacts:\n files:\n - '**/*'\n" + BuildSpec: "version: 0.2\nphases:\n install:\n runtime-versions:\n python: 3.11\n ruby: 3.3\n commands:\n - export current=$(pwd)\n - if [ -f manifest.yaml ];then export current=$(pwd);else if [ -f custom-control-tower-configuration/manifest.yaml ]; then export current=$(pwd)/custom-control-tower-configuration; else echo 'manifest.yaml does not exist at the root level of custom-control-tower-configuration.zip or inside custom-control-tower-configuration folder, please check the ZIP file'; exit 1; fi; fi;\n - apt-get -q update 1> /dev/null\n - apt-get -q install zip wget python3-pip libyaml-dev -y 1> /dev/null \n pre_build:\n commands:\n - cd $current\n - echo 'Download CustomControlTower Scripts'\n - aws s3 cp --quiet s3://control-tower-cfct-assets-prod-${AWS_REGION}/customizations-for-aws-control-tower/v2.7.2/custom-control-tower-scripts.zip $current\n - unzip -q -o $current/custom-control-tower-scripts.zip -d $current\n - cp codebuild_scripts/* .\n - bash install_stage_dependencies.sh $STAGE_NAME\n build:\n commands:\n - echo 'Starting build $(date) in $(pwd)'\n - echo 'bash execute_stage_scripts.sh $STAGE_NAME $LOG_LEVEL $WAIT_TIME $SM_ARN $ARTIFACT_BUCKET $KMS_KEY_ALIAS_NAME $BOOL_VALUES $NONE_TYPE_VALUES'\n - bash execute_stage_scripts.sh $STAGE_NAME $LOG_LEVEL $WAIT_TIME $SM_ARN $ARTIFACT_BUCKET $KMS_KEY_ALIAS_NAME $BOOL_VALUES $NONE_TYPE_VALUES\n - echo 'Running build scripts completed $(date)'\n post_build:\n commands:\n - echo 'Starting post build $(date) in $(pwd)'\n - echo 'build completed on $(date)'\n\nartifacts:\n files:\n - '**/*'\n" Environment: ComputeType: BUILD_GENERAL1_SMALL Image: "aws/codebuild/standard:7.0" @@ -723,7 +758,7 @@ Resources: - Name: SOLUTION_ID Value: !FindInMap [ Solution, Metrics, SolutionID ] - Name: SOLUTION_VERSION - Value: v2.7.1 + Value: v2.7.2 - Name: AWS_STS_REGIONAL_ENDPOINTS Value: "regional" Artifacts: @@ -880,7 +915,7 @@ Resources: - {KMSKeyName: !FindInMap [KMS, Alias, Name]} Source: Type: CODEPIPELINE - BuildSpec: "version: 0.2\nphases:\n install:\n runtime-versions:\n python: 3.11\n ruby: 3.3\n commands:\n - export current=$(pwd)\n - if [ -f manifest.yaml ];then export current=$(pwd);else if [ -f custom-control-tower-configuration/manifest.yaml ]; then export current=$(pwd)/custom-control-tower-configuration; else echo 'manifest.yaml does not exist at the root level of custom-control-tower-configuration.zip or inside custom-control-tower-configuration folder, please check the ZIP file'; exit 1; fi; fi;\n - apt-get -q update 1> /dev/null\n - apt-get -q install zip wget python3-pip libyaml-dev -y 1> /dev/null\n pre_build:\n commands:\n - cd $current\n - echo 'Download CustomControlTower Scripts'\n - aws s3 cp --quiet s3://control-tower-cfct-assets-prod-${AWS_REGION}/customizations-for-aws-control-tower/v2.7.1/custom-control-tower-scripts.zip $current\n - unzip -q -o $current/custom-control-tower-scripts.zip -d $current\n - cp codebuild_scripts/* .\n - bash install_stage_dependencies.sh $STAGE_NAME\n build:\n commands:\n - echo 'Starting build $(date) in $(pwd)'\n - echo 'bash execute_stage_scripts.sh $STAGE_NAME $LOG_LEVEL $WAIT_TIME $SM_ARN $ARTIFACT_BUCKET $KMS_KEY_ALIAS_NAME $BOOL_VALUES $NONE_TYPE_VALUES'\n - bash execute_stage_scripts.sh $STAGE_NAME $LOG_LEVEL $WAIT_TIME $SM_ARN $ARTIFACT_BUCKET $KMS_KEY_ALIAS_NAME $BOOL_VALUES $NONE_TYPE_VALUES\n - echo 'Running build scripts completed $(date)'\n post_build:\n commands:\n - echo 'Starting post build $(date) in $(pwd)'\n - echo 'build completed on $(date)'\n\nartifacts:\n files:\n - '**/*'\n" + BuildSpec: "version: 0.2\nphases:\n install:\n runtime-versions:\n python: 3.11\n ruby: 3.3\n commands:\n - export current=$(pwd)\n - if [ -f manifest.yaml ];then export current=$(pwd);else if [ -f custom-control-tower-configuration/manifest.yaml ]; then export current=$(pwd)/custom-control-tower-configuration; else echo 'manifest.yaml does not exist at the root level of custom-control-tower-configuration.zip or inside custom-control-tower-configuration folder, please check the ZIP file'; exit 1; fi; fi;\n - apt-get -q update 1> /dev/null\n - apt-get -q install zip wget python3-pip libyaml-dev -y 1> /dev/null\n pre_build:\n commands:\n - cd $current\n - echo 'Download CustomControlTower Scripts'\n - aws s3 cp --quiet s3://control-tower-cfct-assets-prod-${AWS_REGION}/customizations-for-aws-control-tower/v2.7.2/custom-control-tower-scripts.zip $current\n - unzip -q -o $current/custom-control-tower-scripts.zip -d $current\n - cp codebuild_scripts/* .\n - bash install_stage_dependencies.sh $STAGE_NAME\n build:\n commands:\n - echo 'Starting build $(date) in $(pwd)'\n - echo 'bash execute_stage_scripts.sh $STAGE_NAME $LOG_LEVEL $WAIT_TIME $SM_ARN $ARTIFACT_BUCKET $KMS_KEY_ALIAS_NAME $BOOL_VALUES $NONE_TYPE_VALUES'\n - bash execute_stage_scripts.sh $STAGE_NAME $LOG_LEVEL $WAIT_TIME $SM_ARN $ARTIFACT_BUCKET $KMS_KEY_ALIAS_NAME $BOOL_VALUES $NONE_TYPE_VALUES\n - echo 'Running build scripts completed $(date)'\n post_build:\n commands:\n - echo 'Starting post build $(date) in $(pwd)'\n - echo 'build completed on $(date)'\n\nartifacts:\n files:\n - '**/*'\n" Environment: ComputeType: BUILD_GENERAL1_SMALL Image: "aws/codebuild/standard:7.0" @@ -905,7 +940,7 @@ Resources: - Name: SOLUTION_ID Value: !FindInMap [Solution, Metrics, SolutionID] - Name: SOLUTION_VERSION - Value: v2.7.1 + Value: v2.7.2 - Name: METRICS_URL Value: !FindInMap [Solution, Metrics, MetricsURL] - Name: CONTROL_TOWER_BASELINE_CONFIG_STACKSET @@ -1031,10 +1066,10 @@ Resources: Variables: LOG_LEVEL: !FindInMap [LambdaFunction, Logging, Level] SOLUTION_ID: !FindInMap [Solution, Metrics, SolutionID] - SOLUTION_VERSION: v2.7.1 + SOLUTION_VERSION: v2.7.2 Code: S3Bucket: !Sub "control-tower-cfct-assets-prod-${AWS::Region}" - S3Key: customizations-for-aws-control-tower/v2.7.1/custom-control-tower-config-deployer.zip + S3Key: customizations-for-aws-control-tower/v2.7.2/custom-control-tower-config-deployer.zip FunctionName: CustomControlTowerDeploymentLambda Description: Custom Control Tower Deployment Lambda Handler: config_deployer.lambda_handler @@ -1053,7 +1088,7 @@ Resources: DestinationBucketName: !Ref CustomControlTowerPipelineS3Bucket DestinationS3Key: !If [IsBuildCustomControlTowerCondition, !FindInMap [BucketConfiguration, CustomControlTowerPipelineS3TriggerKey, Name], !FindInMap [BucketConfiguration, CustomControlTowerPipelineS3NonTriggerKey, Name]] SourceBucketName: !Sub control-tower-cfct-assets-prod-${AWS::Region} - SourceS3Key: customizations-for-aws-control-tower/v2.7.1/custom-control-tower-configuration.zip + SourceS3Key: customizations-for-aws-control-tower/v2.7.2/custom-control-tower-configuration.zip KMSConfig: KMSKeyAlias: !Sub - alias/${KMSKeyName} @@ -1301,14 +1336,14 @@ Resources: ADMINISTRATION_ROLE_ARN: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/service-role/AWSControlTowerStackSetRole EXECUTION_ROLE_NAME: !FindInMap [AWSControlTower, ExecutionRole, Name] SOLUTION_ID: !FindInMap [Solution, Metrics, SolutionID] - SOLUTION_VERSION: v2.7.1 + SOLUTION_VERSION: v2.7.2 METRICS_URL: !FindInMap [Solution, Metrics, MetricsURL] MAX_CONCURRENT_PERCENT: !Ref MaxConcurrentPercentage FAILED_TOLERANCE_PERCENT: !Ref FailureTolerancePercentage REGION_CONCURRENCY_TYPE: !Ref RegionConcurrencyType Code: S3Bucket: !Sub "control-tower-cfct-assets-prod-${AWS::Region}" - S3Key: customizations-for-aws-control-tower/v2.7.1/custom-control-tower-state-machine.zip + S3Key: customizations-for-aws-control-tower/v2.7.2/custom-control-tower-state-machine.zip FunctionName: CustomControlTowerStateMachineLambda Description: Custom Control Tower State Machine Handler Handler: state_machine_router.lambda_handler @@ -2900,10 +2935,10 @@ Resources: LOG_LEVEL: !FindInMap [LambdaFunction, Logging, Level] CODE_PIPELINE_NAME: !Ref CustomControlTowerCodePipeline SOLUTION_ID: !FindInMap [ Solution, Metrics, SolutionID ] - SOLUTION_VERSION: v2.7.1 + SOLUTION_VERSION: v2.7.2 Code: S3Bucket: !Sub "control-tower-cfct-assets-prod-${AWS::Region}" - S3Key: customizations-for-aws-control-tower/v2.7.1/custom-control-tower-lifecycle-event-handler.zip + S3Key: customizations-for-aws-control-tower/v2.7.2/custom-control-tower-lifecycle-event-handler.zip Description: Custom Control Tower Lifecyle event Lambda to handle lifecycle events Handler: lifecycle_event_handler.lambda_handler MemorySize: 512 @@ -2998,6 +3033,16 @@ Resources: Type: AWS::S3::Bucket Condition: IsS3PipelineSource DeletionPolicy: Retain + Properties: + VersioningConfiguration: + Status: Enabled + LifecycleConfiguration: + Rules: + - Id: DeleteNonCurrentVersions + Status: Enabled + NoncurrentVersionExpirationInDays: !Ref NoncurrentVersionExpirationDays + LoggingConfiguration: + DestinationBucketName: !Ref CustomControlTowerS3AccessLogsBucket CustomControlTowerCloudTrailDataEventBucketPolicy: Type: AWS::S3::BucketPolicy @@ -3200,6 +3245,6 @@ Outputs: Value: !Ref CustomControlTowerPipelineS3Bucket CustomControlTowerSolutionVersion: Description: Version Number - Value: "v2.7.1" + Value: "v2.7.2" Export: Name: Custom-Control-Tower-Version diff --git a/deployment/build-s3-dist.sh b/deployment/build-s3-dist.sh index 326e57f..adac0b5 100644 --- a/deployment/build-s3-dist.sh +++ b/deployment/build-s3-dist.sh @@ -136,6 +136,10 @@ declare -a region_list=( "us-gov-west-1" "us-west-1" "us-west-2" + "il-central-1" + "me-central-1" + "ap-south-2" + "ap-southeast-3" ) for region in "${region_list[@]}" do diff --git a/deployment/custom-control-tower-initiation.template b/deployment/custom-control-tower-initiation.template index 6e1a186..2786f18 100644 --- a/deployment/custom-control-tower-initiation.template +++ b/deployment/custom-control-tower-initiation.template @@ -80,6 +80,11 @@ Parameters: - true - false + NoncurrentVersionExpirationDays: + Type: Number + Default: 90 + Description: Number of days after which non-current versions will be deleted + Metadata: AWS::CloudFormation::Interface: ParameterGroups: @@ -189,6 +194,11 @@ Resources: SSEAlgorithm: AES256 VersioningConfiguration: Status: Enabled + LifecycleConfiguration: + Rules: + - Id: DeleteNonCurrentVersions + Status: Enabled + NoncurrentVersionExpirationInDays: !Ref NoncurrentVersionExpirationDays LoggingConfiguration: DestinationBucketName: !Ref CustomControlTowerS3AccessLogsBucket PublicAccessBlockConfiguration: @@ -216,6 +226,11 @@ Resources: Properties: VersioningConfiguration: Status: Enabled + LifecycleConfiguration: + Rules: + - Id: DeleteNonCurrentVersions + Status: Enabled + NoncurrentVersionExpirationInDays: !Ref NoncurrentVersionExpirationDays LoggingConfiguration: DestinationBucketName: !Ref CustomControlTowerS3AccessLogsBucket BucketEncryption: @@ -249,10 +264,15 @@ Resources: cfn_nag: rules_to_suppress: - id: W35 - reason: "This S3 bucket is used as the destination for 'CustomControlTowerPipelineS3Bucket' and 'CustomControlTowerPipelineArtifactS3Bucket'" + reason: "This S3 bucket is used as the destination for 'CustomControlTowerPipelineS3Bucket', 'CustomControlTowerPipelineArtifactS3Bucket' and 'CustomControlTowerCloudTrailDataEventBucket'" Properties: VersioningConfiguration: Status: Enabled + LifecycleConfiguration: + Rules: + - Id: DeleteNonCurrentVersions + Status: Enabled + NoncurrentVersionExpirationInDays: !Ref NoncurrentVersionExpirationDays BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: @@ -298,6 +318,21 @@ Resources: "aws:SourceArn": !Sub "arn:${AWS::Partition}:s3:::${CustomControlTowerPipelineArtifactS3Bucket}" StringEquals: "aws:SourceAccount": !Ref AWS::AccountId + - !If + - IsS3PipelineSource + - Sid: EnableS3AccessLoggingForCustomControlTowerCloudTrailDataEventBucket + Effect: Allow + Principal: + Service: logging.s3.amazonaws.com + Action: + - s3:PutObject + Resource: !Sub "arn:${AWS::Partition}:s3:::${CustomControlTowerS3AccessLogsBucket}/*" + Condition: + ArnLike: + "aws:SourceArn": !Sub "arn:${AWS::Partition}:s3:::${CustomControlTowerCloudTrailDataEventBucket}" + StringEquals: + "aws:SourceAccount": !Ref AWS::AccountId + - !Ref AWS::NoValue CustomControlTowerCodeCommit: Type: AWS::CodeCommit::Repository @@ -2998,6 +3033,16 @@ Resources: Type: AWS::S3::Bucket Condition: IsS3PipelineSource DeletionPolicy: Retain + Properties: + VersioningConfiguration: + Status: Enabled + LifecycleConfiguration: + Rules: + - Id: DeleteNonCurrentVersions + Status: Enabled + NoncurrentVersionExpirationInDays: !Ref NoncurrentVersionExpirationDays + LoggingConfiguration: + DestinationBucketName: !Ref CustomControlTowerS3AccessLogsBucket CustomControlTowerCloudTrailDataEventBucketPolicy: Type: AWS::S3::BucketPolicy diff --git a/source/codebuild_scripts/find_replace.py b/source/codebuild_scripts/find_replace.py index 9b09796..5cf03c9 100644 --- a/source/codebuild_scripts/find_replace.py +++ b/source/codebuild_scripts/find_replace.py @@ -22,6 +22,7 @@ import yaml from cfct.utils.logger import Logger +from cfct.utils.path_utils import is_safe_path from jinja2 import Environment, FileSystemLoader # initialise logger @@ -31,7 +32,6 @@ def find_replace(function_path, file_name, destination_file, parameters): try: - j2loader = FileSystemLoader(function_path) j2env = Environment(loader=j2loader, autoescape=True) # Compliant j2template = j2env.get_template(file_name) @@ -44,8 +44,12 @@ def find_replace(function_path, file_name, destination_file, parameters): dictionary.update({key: value}) logger.debug(dictionary) output = j2template.render(dictionary) - with open(destination_file, "w") as fh: - fh.write(output) + if is_safe_path(function_path, destination_file): + abs_destination_file = os.path.abspath(destination_file) + with open(abs_destination_file, "w") as fh: + fh.write(output) + else: + raise ValueError(f"Unsafe file path detected {destination_file}") except Exception as e: logger.log_general_exception(__file__.split("/")[-1], inspect.stack()[0][3], e) raise @@ -96,13 +100,17 @@ def sanitize_null_type(d, none_type_values): def generate_event(user_input_file, path, bools, none_types): logger.info("Generating Event") - with open(user_input_file) as f: - user_input = sanitize_boolean_type(f.read(), bools) - logger.info("Boolean values wrapped with quotes (if applicable)") - logger.info(user_input) - user_input = sanitize_null_type(user_input, none_types) - logger.info("Null values replaced with quotes (if applicable)") - logger.info(user_input) + if is_safe_path(path, user_input_file): + abs_user_path = os.path.abspath(user_input_file) + with open(abs_user_path) as f: + user_input = sanitize_boolean_type(f.read(), bools) + logger.info("Boolean values wrapped with quotes (if applicable)") + logger.info(user_input) + user_input = sanitize_null_type(user_input, none_types) + logger.info("Null values replaced with quotes (if applicable)") + logger.info(user_input) + else: + raise ValueError(f"Unsafe file path detected {user_input_file}") update_add_on_manifest(user_input, path) diff --git a/source/codebuild_scripts/merge_baseline_template_parameter.py b/source/codebuild_scripts/merge_baseline_template_parameter.py index 13ba1cf..f0fb323 100644 --- a/source/codebuild_scripts/merge_baseline_template_parameter.py +++ b/source/codebuild_scripts/merge_baseline_template_parameter.py @@ -19,14 +19,20 @@ import sys from cfct.utils.logger import Logger - +from cfct.utils.path_utils import is_safe_path def _read_file(file): if os.path.isfile(file): logger.info("File - {} exists".format(file)) logger.info("Reading from {}".format(file)) - with open(file) as f: - return json.load(f) + base_directory = os.getcwd() + if is_safe_path(base_directory, file): + abs_file = os.path.abspath(file) + with open(abs_file) as f: + return json.load(f) + else: + logger.error(f"Invalid file path detected for {file}") + sys.exit(1) else: logger.error("File: {} not found.".format(file)) sys.exit(1) diff --git a/source/src/cfct/lambda_handlers/config_deployer.py b/source/src/cfct/lambda_handlers/config_deployer.py index 0a9a446..082db96 100644 --- a/source/src/cfct/lambda_handlers/config_deployer.py +++ b/source/src/cfct/lambda_handlers/config_deployer.py @@ -39,13 +39,27 @@ kms = KMS(logger) ssm = SSM(logger) +def safe_extract(zip_file_name, output_path, max_files = 1000, max_size = 500 * 1024 * 1024): + with zipfile.ZipFile(zip_file_name, "r") as zip_file: + # Get the list of files + file_list = zip_file.infolist() + + # Check the number of files + if len(file_list) > max_files: + raise Exception(f"The number of files in the ZIP archive exceeds {max_files}.") + + # Check the total size of files + total_size = sum(file.file_size for file in file_list) + if total_size > max_size: + raise Exception(f"The total size of files in the ZIP archive exceeds {max_size}.") + + for file in file_list: + zip_file.extract(file, output_path) def unzip_function(zip_file_name, function_path, output_path): orig_path = os.getcwd() os.chdir(function_path) - zip_file = zipfile.ZipFile(zip_file_name, "r") - zip_file.extractall(output_path) - zip_file.close() + safe_extract(zip_file_name, output_path) os.chdir(orig_path) diff --git a/source/src/cfct/utils/path_utils.py b/source/src/cfct/utils/path_utils.py new file mode 100644 index 0000000..600b4db --- /dev/null +++ b/source/src/cfct/utils/path_utils.py @@ -0,0 +1,13 @@ +import os + +def is_safe_path(allowed_base_directory: str, target_path: str) -> bool: + # Normalize the paths to remove any '..' components + normalized_allowed_base_directory = os.path.normpath(allowed_base_directory) + normalized_target_path = os.path.normpath(target_path) + + # Convert the paths to absolute paths + abs_allowed_base_directory = os.path.abspath(normalized_allowed_base_directory) + abs_target_path = os.path.abspath(normalized_target_path) + + # Check if the resolved absolute path is within the base directory + return os.path.commonpath([abs_target_path, abs_allowed_base_directory]) == abs_allowed_base_directory \ No newline at end of file diff --git a/source/src/setup.py b/source/src/setup.py index 144ae19..47f2814 100644 --- a/source/src/setup.py +++ b/source/src/setup.py @@ -34,8 +34,8 @@ "requests==2.25.1", "markdown_to_json==1.0.0", "python-dateutil==2.8.1", - "boto3==1.20.15", - "botocore==1.23.15", + "boto3==1.28.17", + "botocore==1.31.17", ], extras_require={ "test": [