├── VERSION ├── deployment ├── add-on │ └── .gitkeep ├── custom_control_tower_configuration │ ├── manifest.yaml.j2 │ └── example-configuration │ │ ├── policies │ │ ├── rcp-preventive-guardrails.json │ │ └── preventive-guardrails.json │ │ ├── parameters │ │ ├── create-ssm-parameter-keys-1.json │ │ └── create-ssm-parameter-keys-2.json │ │ ├── templates │ │ ├── create-ssm-parameter-keys-1.template │ │ └── create-ssm-parameter-keys-2.template │ │ └── manifest.yaml ├── run-unit-tests.sh ├── lambda_build.py └── build-s3-dist.sh ├── source ├── src │ ├── cfct │ │ ├── __init__.py │ │ ├── aws │ │ │ ├── __init__.py │ │ │ ├── utils │ │ │ │ ├── __init__.py │ │ │ │ ├── url_conversion.py │ │ │ │ └── boto3_session.py │ │ │ └── services │ │ │ │ ├── __init__.py │ │ │ │ ├── code_pipeline.py │ │ │ │ ├── ec2.py │ │ │ │ ├── state_machine.py │ │ │ │ ├── sts.py │ │ │ │ ├── kms.py │ │ │ │ ├── s3.py │ │ │ │ ├── organizations.py │ │ │ │ ├── ssm.py │ │ │ │ ├── scp.py │ │ │ │ └── rcp.py │ │ ├── utils │ │ │ ├── __init__.py │ │ │ ├── path_utils.py │ │ │ ├── datetime_encoder.py │ │ │ ├── list_comparision.py │ │ │ ├── password_generator.py │ │ │ ├── retry_decorator.py │ │ │ ├── parameter_manipulation.py │ │ │ ├── string_manipulation.py │ │ │ ├── logger.py │ │ │ └── crhelper.py │ │ ├── manifest │ │ │ ├── __init__.py │ │ │ ├── stage_to_s3.py │ │ │ ├── manifest.py │ │ │ └── sm_input_builder.py │ │ ├── metrics │ │ │ ├── __init__.py │ │ │ └── solution_metrics.py │ │ ├── validation │ │ │ ├── __init__.py │ │ │ ├── custom_validation.py │ │ │ ├── manifest.schema.yaml │ │ │ └── manifest-v2.schema.yaml │ │ ├── lambda_handlers │ │ │ ├── __init__.py │ │ │ └── lifecycle_event_handler.py │ │ ├── exceptions.py │ │ └── types.py │ ├── pyproject.toml │ └── setup.py └── codebuild_scripts │ ├── install_stage_dependencies.sh │ ├── execute_stage_scripts.sh │ ├── merge_manifest.py │ ├── find_replace.py │ ├── state_machine_trigger.py │ ├── run-validation.sh │ ├── merge_baseline_template_parameter.py │ └── merge_directories.sh ├── pytest.ini ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── NOTICE.txt ├── README.md └── LICENSE.txt /VERSION: -------------------------------------------------------------------------------- 1 | v2.8.3 2 | -------------------------------------------------------------------------------- /deployment/add-on/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/src/cfct/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/src/cfct/aws/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/src/cfct/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/src/cfct/aws/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/src/cfct/manifest/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/src/cfct/metrics/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/src/cfct/validation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/src/cfct/aws/services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/src/cfct/lambda_handlers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/src/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=70.0.0"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --verbose -ra -m unit 3 | log_cli = true 4 | log_level=INFO 5 | markers = 6 | unit 7 | integration 8 | e2e 9 | -------------------------------------------------------------------------------- /source/src/cfct/exceptions.py: -------------------------------------------------------------------------------- 1 | class StackSetHasFailedInstances(Exception): 2 | def __init__(self, stack_set_name: str, failed_stack_set_instances): 3 | self.stack_set_name = stack_set_name 4 | self.failed_stack_set_instances = failed_stack_set_instances 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. -------------------------------------------------------------------------------- /deployment/custom_control_tower_configuration/manifest.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | #Default region for deploying Custom Control Tower: Code Pipeline, Step functions, Lambda, SSM parameters, and StackSets 3 | region: {{ region }} 4 | version: 2021-03-15 5 | 6 | # Control Tower Custom Resources (Service Control Policies or CloudFormation) 7 | resources: [] -------------------------------------------------------------------------------- /deployment/custom_control_tower_configuration/example-configuration/policies/rcp-preventive-guardrails.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "GuardAccessToBucket", 6 | "Principal": "*", 7 | "Effect": "Deny", 8 | "Action": "s3:*", 9 | "Resource": [ 10 | "arn:aws:s3:::my-bucket", 11 | "arn:aws:s3:::my-bucket/*" 12 | ] 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /deployment/custom_control_tower_configuration/example-configuration/parameters/create-ssm-parameter-keys-1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ParameterKey": "ApplicationId", 4 | "ParameterValue": "App1" 5 | }, 6 | { 7 | "ParameterKey": "EnvironmentType", 8 | "ParameterValue": "EnvType1" 9 | }, 10 | { 11 | "ParameterKey": "EnvironmentNumber", 12 | "ParameterValue": "EnvNum1" 13 | } 14 | ] -------------------------------------------------------------------------------- /deployment/custom_control_tower_configuration/example-configuration/parameters/create-ssm-parameter-keys-2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ParameterKey": "ApplicationId", 4 | "ParameterValue": "App2" 5 | }, 6 | { 7 | "ParameterKey": "EnvironmentType", 8 | "ParameterValue": "EnvType2" 9 | }, 10 | { 11 | "ParameterKey": "EnvironmentNumber", 12 | "ParameterValue": "EnvNum2" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Customizations for AWS Control Tower (CfCT). 2 | 3 | Thank you for your interest in contributing to Customizations for AWS Control Tower (CfCT). 4 | 5 | At this time, we are not accepting contributions. If contributions are accepted in the future, Customizations for AWS Control Tower (CfCT) is released under the [Apache license](http://aws.amazon.com/apache2.0/) and any code submitted will be released under that license. 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Development 2 | .DS_Store 3 | .idea 4 | *.iml 5 | .vscode 6 | *.pyc 7 | *.so 8 | .eggs 9 | *.egg-info 10 | .cache 11 | *.sonarlint 12 | .python-version 13 | __pycache__ 14 | .pytest_cache 15 | .mypy_cache 16 | .coverage 17 | 18 | # Ignore virtual environments 19 | venv 20 | .venv 21 | testing-venv 22 | myenv 23 | 24 | # Ignore installed dependencies 25 | dist 26 | source/src/build 27 | build 28 | 29 | /deployment/open-source 30 | /deployment/state_machines/sample_events/ 31 | /deployment/global-s3-assets/ 32 | /deployment/regional-s3-assets/ 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this solution 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the feature you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Contributing to Customizations for AWS Control Tower (CfCT). 2 | 3 | Thank you for your interest in contributing to Customizations for AWS Control Tower (CfCT). 4 | 5 | At this time, we are not accepting contributions. If contributions are accepted in the future, Customizations for AWS Control Tower (CfCT) is released under the [Apache license](http://aws.amazon.com/apache2.0/) and any code submitted will be released under that license. 6 | 7 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 8 | -------------------------------------------------------------------------------- /source/src/cfct/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Literal, TypedDict 2 | 3 | 4 | class ResourcePropertiesTypeDef(TypedDict): 5 | """ 6 | Capabilities is expected to be a json.dumps of CloudFormation capabilities 7 | """ 8 | 9 | StackSetName: str 10 | TemplateURL: str 11 | Capabilities: str 12 | Parameters: Dict[str, Any] 13 | AccountList: List[str] 14 | RegionList: List[str] 15 | SSMParameters: Dict[str, Any] 16 | 17 | 18 | class StackSetRequestTypeDef(TypedDict): 19 | RequestType: Literal["Delete", "Create"] 20 | ResourceProperties: ResourcePropertiesTypeDef 21 | SkipUpdateStackSet: Literal["no", "yes"] 22 | 23 | 24 | class StackSetInstanceTypeDef(TypedDict): 25 | account: str 26 | region: str 27 | -------------------------------------------------------------------------------- /source/src/cfct/utils/path_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def is_safe_path(allowed_base_directory: str, target_path: str) -> bool: 5 | # Normalize the paths to remove any '..' components 6 | normalized_allowed_base_directory = os.path.normpath(allowed_base_directory) 7 | normalized_target_path = os.path.normpath(target_path) 8 | 9 | # Convert the paths to absolute paths 10 | abs_allowed_base_directory = os.path.abspath(normalized_allowed_base_directory) 11 | abs_target_path = os.path.abspath(normalized_target_path) 12 | 13 | # Check if the resolved absolute path is within the base directory 14 | return ( 15 | os.path.commonpath([abs_target_path, abs_allowed_base_directory]) 16 | == abs_allowed_base_directory 17 | ) 18 | -------------------------------------------------------------------------------- /deployment/run-unit-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script must be executed from the package root directory 3 | 4 | set -e 5 | 6 | if [ ! -d "testing-venv" ]; then 7 | echo "Creating testing-venv..." 8 | python3 -m venv testing-venv 9 | fi 10 | 11 | 12 | echo "Rebuilding package locally before testing..." 13 | source testing-venv/bin/activate 14 | echo "Using python: `type python3`" 15 | echo "Python version: `python3 --version`" 16 | echo "Installing pip" 17 | pip3 install --quiet -U pip 18 | echo "Installing CFCT library" 19 | # Upgrade setuptools, wheel 20 | # Install cython<3.0.0 and pyyaml 5.4.1 with build isolation 21 | # Ref: https://github.com/yaml/pyyaml/issues/724 22 | pip3 install --upgrade setuptools wheel 23 | pip3 install 'cython<3.0.0' && pip3 install --no-build-isolation pyyaml==5.4.1 24 | pip3 install "./source/src[test, dev]" 25 | echo "Running tests..." 26 | python3 -m pytest -m unit 27 | 28 | deactivate -------------------------------------------------------------------------------- /deployment/custom_control_tower_configuration/example-configuration/policies/preventive-guardrails.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "GuardPutAccountPublicAccessBlock", 6 | "Effect": "Deny", 7 | "Action": "s3:PutAccountPublicAccessBlock", 8 | "Resource": "arn:aws:s3:::*" 9 | }, 10 | { 11 | "Sid": "GuardEMRPutBlockPublicAccess", 12 | "Effect": "Deny", 13 | "Action": "elasticmapreduce:PutBlockPublicAccessConfiguration", 14 | "Resource": "*" 15 | }, 16 | { 17 | "Sid": "GuardGlacierDeletion", 18 | "Effect": "Deny", 19 | "Action": [ 20 | "glacier:DeleteArchive", 21 | "glacier:DeleteVault" 22 | ], 23 | "Resource": "arn:aws:glacier:*:*:vaults/*" 24 | }, 25 | { 26 | "Sid": "GuardKMSActions", 27 | "Effect": "Deny", 28 | "Action": [ 29 | "kms:DeleteAlias", 30 | "kms:DeleteImportedKeyMaterial", 31 | "kms:ScheduleKeyDeletion" 32 | ], 33 | "Resource": "*" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /source/src/cfct/validation/custom_validation.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance with the License. 6 | # A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is distributed # 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # 12 | # or implied. See the License for the specific language governing permissions# 13 | # and limitations under the License. # 14 | ############################################################################### 15 | 16 | import logging 17 | 18 | log = logging.getLogger(__name__) 19 | 20 | # This is a custom valiator specifically for pyKwlify Schema extensions 21 | log.info("No custom validations available") 22 | -------------------------------------------------------------------------------- /deployment/custom_control_tower_configuration/example-configuration/templates/create-ssm-parameter-keys-1.template: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Description: Dev Template to test overrideparams 4 | Parameters: 5 | ApplicationId: 6 | Description: APP ID for the account 7 | Type: String 8 | Default: 'APP00000' 9 | EnvironmentType: 10 | Description: EnvironmentType for the account 11 | Type: String 12 | Default: 'Prod' 13 | EnvironmentNumber: 14 | Description: EnvironmentNumber for the account 15 | Type: String 16 | Default: '00' 17 | Resources: 18 | ApplicationIdParam: 19 | Type: "AWS::SSM::Parameter" 20 | Properties: 21 | Description: APP ID for the account 22 | Name: /LG/core/ApplicationId 23 | Type: String 24 | Value: !Ref ApplicationId 25 | EnvironmentTypeParam: 26 | Type: "AWS::SSM::Parameter" 27 | Properties: 28 | Description: EnvironmentType for the account 29 | Name: /LG/core/EnvironmentType 30 | Type: String 31 | Value: !Ref EnvironmentType 32 | EnvironmentNumberParam: 33 | Type: "AWS::SSM::Parameter" 34 | Properties: 35 | Description: EnvironmentNumber for the account 36 | Name: /LG/core/EnvironmentNumber 37 | Type: String 38 | Value: !Ref EnvironmentNumber 39 | Outputs: 40 | ApplicationId: 41 | Description: App ID 42 | Value: !Ref ApplicationId -------------------------------------------------------------------------------- /deployment/custom_control_tower_configuration/example-configuration/templates/create-ssm-parameter-keys-2.template: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Description: Dev Template to test overrideparams 4 | Parameters: 5 | ApplicationId: 6 | Description: APP ID for the account 7 | Type: String 8 | Default: 'APP00000' 9 | EnvironmentType: 10 | Description: EnvironmentType for the account 11 | Type: String 12 | Default: 'Prod' 13 | EnvironmentNumber: 14 | Description: EnvironmentNumber for the account 15 | Type: String 16 | Default: '00' 17 | Resources: 18 | ApplicationIdParam: 19 | Type: "AWS::SSM::Parameter" 20 | Properties: 21 | Description: APP ID for the account 22 | Name: /LG/baseline/ApplicationId 23 | Type: String 24 | Value: !Ref ApplicationId 25 | EnvironmentTypeParam: 26 | Type: "AWS::SSM::Parameter" 27 | Properties: 28 | Description: EnvironmentType for the account 29 | Name: /LG/baseline/EnvironmentType 30 | Type: String 31 | Value: !Ref EnvironmentType 32 | EnvironmentNumberParam: 33 | Type: "AWS::SSM::Parameter" 34 | Properties: 35 | Description: EnvironmentNumber for the account 36 | Name: /LG/baseline/EnvironmentNumber 37 | Type: String 38 | Value: !Ref EnvironmentNumber 39 | Outputs: 40 | ApplicationId: 41 | Description: App ID 42 | Value: !Ref ApplicationId -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Please complete the following information about the solution:** 20 | - [ ] Version: [e.g. v1.0.0] 21 | 22 | To get the version of the solution, you can look at the description of the created CloudFormation stack. For example, "_(SO0089) - customizations-for-aws-control-tower Solution. Version: v1.0.0_". You can also find the version from [releases](https://github.com/aws-solutions/aws-control-tower-customizations/releases) 23 | 24 | - [ ] Region: [e.g. us-east-1] 25 | - [ ] Was the solution modified from the version published on this repository? 26 | - [ ] If the answer to the previous question was yes, are the changes available on GitHub? 27 | - [ ] Have you checked your [service quotas](https://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) for the sevices this solution uses? 28 | - [ ] Were there any errors in the CloudWatch Logs? 29 | 30 | **Screenshots** 31 | If applicable, add screenshots to help explain your problem (please **DO NOT include sensitive information**). 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /source/src/cfct/utils/datetime_encoder.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance # 6 | # with the License. A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is # 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # 12 | # KIND, express or implied. See the License for the specific language # 13 | # governing permissions and limitations under the License. # 14 | ############################################################################## 15 | 16 | # !/bin/python 17 | import json 18 | from datetime import date, datetime 19 | 20 | 21 | class DateTimeEncoder(json.JSONEncoder): 22 | def default(self, o): 23 | if isinstance(o, (datetime, date)): 24 | serial = o.isoformat() 25 | return serial 26 | raise TypeError("Type %s not serializable" % type(o)) 27 | -------------------------------------------------------------------------------- /source/src/cfct/validation/manifest.schema.yaml: -------------------------------------------------------------------------------- 1 | type: map 2 | mapping: 3 | "region": 4 | type: str 5 | required: True 6 | "version": 7 | type: date 8 | required: True 9 | enum: [2020-01-01] 10 | "organization_policies": 11 | type: seq 12 | sequence: 13 | - type: map 14 | required: True 15 | mapping: 16 | "name": &name 17 | "description": &description 18 | "policy_file": 19 | type: str 20 | required: True 21 | "apply_to_accounts_in_ou": 22 | required: True 23 | type: seq 24 | sequence: 25 | - type: str 26 | "cloudformation_resources": 27 | type: seq 28 | sequence: 29 | - type: map 30 | required: True 31 | mapping: 32 | "name": *name 33 | "deploy_to_account": 34 | type: seq 35 | required: False 36 | sequence: 37 | - type: any 38 | "deploy_to_ou": 39 | type: seq 40 | required: False 41 | sequence: 42 | - type: str 43 | "template_file": 44 | required: True 45 | type: str 46 | "parameter_file": ¶meter_file 47 | "deploy_method": &deploy_method 48 | "depends_on": 49 | type: seq 50 | sequence: 51 | - type: str 52 | "regions": ®ions 53 | type: seq 54 | sequence: 55 | - type: str 56 | unique: True 57 | "ssm_parameters": &ssm_parameters 58 | type: seq 59 | sequence: 60 | - type: map 61 | required: True 62 | mapping: 63 | "name": 64 | type: str 65 | required: True 66 | "value": 67 | type: str 68 | required: True -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Customizations for AWS Control Tower Solution 2 | 3 | Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except 5 | in compliance with the License. A copy of the License is located at http://www.apache.org/licenses/LICENSE-2.0 6 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 7 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the 8 | specific language governing permissions and limitations under the License. 9 | 10 | ********************** 11 | THIRD PARTY COMPONENTS 12 | ********************** 13 | This software includes third party software subject to the following copyrights: 14 | 15 | AWS SDK under the Apache License Version 2.0 16 | boto3 under the Apache License Version 2.0 17 | pip under the Massachusetts Institute of Technology (MIT) license 18 | setuptools under the Massachusetts Institute of Technology (MIT) license 19 | virtualenv under the Massachusetts Institute of Technology (MIT) license 20 | PyYAML under the Massachusetts Institute of Technology (MIT) license 21 | yorm under the Massachusetts Institute of Technology (MIT) license 22 | jinja2 under the Berkeley Software Distribution (BSD) license 23 | requests under the Apache Software License 24 | pykwalify under the Massachusetts Institute of Technology (MIT) license 25 | cfn_flip under the Apache License Version 2.0 26 | cfn-nag under the Massachusetts Institute of Technology (MIT) license 27 | mock under the Berkeley Software Distribution (BSD) license 28 | moto under the Apache License Version 2.0 29 | pytest under the Massachusetts Institute of Technology (MIT) license 30 | pytest-mock under the Massachusetts Institute of Technology (MIT) license 31 | pytest-runner under the Massachusetts Institute of Technology (MIT) license 32 | uuid under the Massachusetts Institute of Technology (MIT) license 33 | yq under the Massachusetts Institute of Technology (MIT) license -------------------------------------------------------------------------------- /source/src/setup.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All rights reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | # 4 | import setuptools 5 | 6 | with open("../../README.md", "r", encoding="utf-8") as fh: 7 | long_description = fh.read() 8 | 9 | 10 | with open("../../VERSION", "r", encoding="utf-8") as version_file: 11 | version = version_file.read() 12 | 13 | 14 | setuptools.setup( 15 | name="cfct", 16 | version=version, 17 | author="AWS", 18 | description="Customizations for Control Tower", 19 | long_description=long_description, 20 | url="https://github.com/aws-solutions/aws-control-tower-customizations", 21 | classifiers=[ 22 | "Programming Language :: Python :: 3.11", 23 | "License :: OSI Approved :: MIT License", 24 | "Operating System :: OS Independent", 25 | ], 26 | packages=setuptools.find_packages(exclude=["tests"]), 27 | package_data={"cfct": ["validation/*.yaml"]}, 28 | python_requires=">=3.11", 29 | install_requires=[ 30 | "yorm==1.6.2", 31 | "pyyaml==5.4.1", 32 | "Jinja2==3.1.6", 33 | "MarkupSafe==2.0.1", # https://github.com/pallets/jinja/issues/1585 34 | "requests==2.32.4", 35 | "markdown_to_json==1.0.0", 36 | "python-dateutil==2.8.1", 37 | "boto3==1.34.162", 38 | "botocore==1.34.162", 39 | ], 40 | extras_require={ 41 | "test": [ 42 | "mypy>=1.3.0", 43 | "mock==4.0.3", 44 | "moto==4.2.14", 45 | "pytest-mock==3.5.1", 46 | "pytest-runner==5.2", 47 | "uuid==1.30", 48 | "pytest == 6.2.4", 49 | "expecter==0.3.0", 50 | "pykwalify == 1.8.0", 51 | "cfn-flip>=1.3.0", 52 | ], 53 | "dev": [ 54 | "ipython", 55 | "isort", 56 | "ipdb", 57 | "black", 58 | "pre-commit", 59 | "pip", 60 | "setuptools", 61 | "virtualenv", 62 | ], 63 | } 64 | ) 65 | -------------------------------------------------------------------------------- /source/src/cfct/utils/list_comparision.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance with the License. 6 | # A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is distributed # 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # 12 | # or implied. See the License for the specific language governing permissions# 13 | # and limitations under the License. # 14 | ############################################################################### 15 | 16 | from cfct.utils.logger import Logger 17 | 18 | logger = Logger("info") 19 | 20 | 21 | def compare_lists(existing_list: list, new_list: list) -> bool: 22 | """Compares two list and return boolean flag if they match 23 | 24 | Args: 25 | existing_list: Input string 26 | Is there a space in the input string. Default to false. 27 | new_list: 28 | Character to replace the target character with. Default to '_'. 29 | 30 | Returns: 31 | boolean value 32 | 33 | Raises: 34 | """ 35 | added_values = list(set(new_list) - set(existing_list)) 36 | removed_values = list(set(existing_list) - set(new_list)) 37 | if len(added_values) == 0 and len(removed_values) == 0: 38 | logger.info("Lists matched.") 39 | return True 40 | else: 41 | logger.info("Lists didn't match.") 42 | return False 43 | -------------------------------------------------------------------------------- /source/src/cfct/utils/password_generator.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance with the License. 6 | # A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is distributed # 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # 12 | # or implied. See the License for the specific language governing permissions# 13 | # and limitations under the License. # 14 | ############################################################################### 15 | 16 | import random 17 | import string 18 | 19 | 20 | def random_pwd_generator(length, additional_str=""): 21 | """Generate random password. 22 | 23 | Args: 24 | length: length of the password 25 | additional_str: Optional. Input additonal string that is allowed in 26 | the password. Default to '' empty string. 27 | Returns: 28 | password 29 | """ 30 | chars = string.ascii_uppercase + string.ascii_lowercase + string.digits + additional_str 31 | # Making sure the password has two numbers and symbols at the very least 32 | password = ( 33 | "".join(random.SystemRandom().choice(chars) for _ in range(length - 4)) 34 | + "".join(random.SystemRandom().choice(string.digits) for _ in range(2)) 35 | + "".join(random.SystemRandom().choice(additional_str) for _ in range(2)) 36 | ) 37 | return password 38 | -------------------------------------------------------------------------------- /source/src/cfct/aws/services/code_pipeline.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance # 6 | # with the License. A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is # 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # 12 | # KIND, express or implied. See the License for the specific language # 13 | # governing permissions and limitations under the License. # 14 | ############################################################################## 15 | 16 | # !/bin/python 17 | import inspect 18 | 19 | from botocore.exceptions import ClientError 20 | 21 | from cfct.aws.utils.boto3_session import Boto3Session 22 | 23 | 24 | class CodePipeline(Boto3Session): 25 | """This class make code pipeline API calls such as starts code pipeline 26 | execution, etc. 27 | """ 28 | 29 | def __init__(self, logger, **kwargs): 30 | self.logger = logger 31 | __service_name = "codepipeline" 32 | super().__init__(logger, __service_name, **kwargs) 33 | self.code_pipeline = super().get_client() 34 | 35 | def start_pipeline_execution(self, code_pipeline_name): 36 | try: 37 | response = self.code_pipeline.start_pipeline_execution(name=code_pipeline_name) 38 | return response 39 | except ClientError as e: 40 | self.logger.log_unhandled_exception(e) 41 | raise 42 | -------------------------------------------------------------------------------- /source/src/cfct/validation/manifest-v2.schema.yaml: -------------------------------------------------------------------------------- 1 | type: map 2 | mapping: 3 | "region": 4 | type: str 5 | required: True 6 | "version": 7 | type: date 8 | required: True 9 | enum: [2021-03-15] 10 | "enable_stack_set_deletion": 11 | type: bool 12 | required: False 13 | "resources": 14 | type: seq 15 | sequence: 16 | - type: map 17 | required: True 18 | mapping: 19 | "name": 20 | type: str 21 | required: True 22 | "description": 23 | type: str 24 | required: False 25 | "resource_file": 26 | type: str 27 | required: True 28 | "deployment_targets": 29 | type: map 30 | required: True 31 | mapping: 32 | "accounts": 33 | required: False 34 | type: seq 35 | sequence: 36 | - type: any 37 | "organizational_units": 38 | required: False 39 | type: seq 40 | sequence: 41 | - type: str 42 | "parameter_file": 43 | type: str 44 | "parameters": 45 | type: seq 46 | sequence: 47 | - type: map 48 | required: True 49 | mapping: 50 | "parameter_key": 51 | type: str 52 | required: True 53 | "parameter_value": 54 | type: any 55 | required: True 56 | "deploy_method": 57 | type: str 58 | required: True 59 | enum: ['scp', 'stack_set', 'rcp'] 60 | "regions": 61 | type: seq 62 | sequence: 63 | - type: str 64 | unique: True 65 | "export_outputs": 66 | type: seq 67 | sequence: 68 | - type: map 69 | required: True 70 | mapping: 71 | "name": 72 | type: str 73 | required: True 74 | "value": 75 | type: str 76 | required: True -------------------------------------------------------------------------------- /source/src/cfct/utils/retry_decorator.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance with the License. 6 | # A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is distributed # 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # 12 | # or implied. See the License for the specific language governing permissions# 13 | # and limitations under the License. # 14 | ############################################################################### 15 | 16 | import time 17 | from functools import wraps 18 | from random import randint 19 | 20 | from cfct.utils.logger import Logger 21 | 22 | # initialise logger 23 | logger = Logger(loglevel="info") 24 | 25 | 26 | def try_except_retry(count=3, multiplier=2): 27 | def decorator(func): 28 | @wraps(func) 29 | def wrapper(*args, **kwargs): 30 | _count = count 31 | _seconds = randint(5, 10) 32 | while _count >= 1: 33 | try: 34 | return func(*args, **kwargs) 35 | except Exception as e: 36 | logger.warning("{}, Trying again in {} seconds".format(e, _seconds)) 37 | time.sleep(_seconds) 38 | _count -= 1 39 | _seconds *= multiplier 40 | if _count == 0: 41 | logger.error("Retry attempts failed, raising the exception.") 42 | raise 43 | return func(*args, **kwargs) 44 | 45 | return wrapper 46 | 47 | return decorator 48 | -------------------------------------------------------------------------------- /deployment/custom_control_tower_configuration/example-configuration/manifest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | #Default region for deploying Custom Control Tower: Code Pipeline, Step functions, Lambda, SSM parameters, and StackSets 3 | region: 4 | version: 2021-03-15 5 | 6 | # Control Tower Custom CloudFormation Resources 7 | resources: 8 | - name: stackset-1 9 | resource_file: templates/create-ssm-parameter-keys-1.template 10 | parameter_file: parameters/create-ssm-parameter-keys-1.json 11 | deploy_method: stack_set 12 | deployment_targets: 13 | accounts: # :type: list 14 | - # and/or 15 | - 16 | export_outputs: 17 | - name: /org/member/test-ssm/app-id 18 | value: $[output_ApplicationId] 19 | regions: 20 | - 21 | 22 | - name: stackset-2 23 | resource_file: s3:////create-ssm-parameter-keys-2.template # S3 bucket must be in the 24 | parameters: 25 | - parameter_key: ApplicationId 26 | parameter_value: App2 27 | - parameter_key: EnvironmentType 28 | parameter_value: EnvType2 29 | - parameter_key: EnvironmentNumber 30 | parameter_value: EnvNum2 31 | deploy_method: stack_set 32 | deployment_targets: 33 | accounts: 34 | - # and/or 35 | - 36 | organizational_units: 37 | - 38 | regions: # :type: list 39 | - 40 | 41 | - name: test-preventive-guardrails 42 | description: To prevent from deleting or disabling resources in member accounts 43 | resource_file: policies/preventive-guardrails.json 44 | deploy_method: scp 45 | #Apply to the following OU(s) 46 | deployment_targets: # accounts property is not supported for SCPs 47 | organizational_units: 48 | - 49 | 50 | - name: test-rcp-preventive-guardrails 51 | description: To prevent from deleting or disabling resources in member accounts 52 | resource_file: policies/rcp-preventive-guardrails.json 53 | deploy_method: rcp 54 | #Apply to the following OU(s) 55 | deployment_targets: # accounts property is not supported for RCPs 56 | organizational_units: 57 | - -------------------------------------------------------------------------------- /source/src/cfct/aws/services/ec2.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance # 6 | # with the License. A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is # 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # 12 | # KIND, express or implied. See the License for the specific language # 13 | # governing permissions and limitations under the License. # 14 | ############################################################################## 15 | 16 | # !/bin/python 17 | 18 | from botocore.exceptions import ClientError 19 | 20 | from cfct.aws.utils.boto3_session import Boto3Session 21 | 22 | 23 | class EC2(Boto3Session): 24 | def __init__(self, logger, region, **kwargs): 25 | self.logger = logger 26 | __service_name = "ec2" 27 | kwargs.update({"region": region}) 28 | super().__init__(logger, __service_name, **kwargs) 29 | self.ec2_client = super().get_client() 30 | 31 | def describe_availability_zones(self, name="state", value="available"): 32 | try: 33 | response = self.ec2_client.describe_availability_zones( 34 | Filters=[{"Name": name, "Values": [value]}] 35 | ) 36 | return [resp["ZoneName"] for resp in response["AvailabilityZones"]] 37 | except ClientError as e: 38 | self.logger.log_unhandled_exception(e) 39 | raise 40 | 41 | def create_key_pair(self, key_name): 42 | try: 43 | response = self.ec2_client.create_key_pair(KeyName=key_name) 44 | return response 45 | except ClientError as e: 46 | self.logger.log_unhandled_exception(e) 47 | raise 48 | -------------------------------------------------------------------------------- /source/codebuild_scripts/install_stage_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Check to see if input has been provided: 4 | if [ -z "$1" ]; then 5 | echo "Please provide the base source bucket name, trademark approved solution name and version where the lambda code will eventually reside." 6 | echo "For example: ./install_stage_dependencies.sh " 7 | echo "For example: ./install_stage_dependencies.sh build | scp | rcp | stackset" 8 | exit 1 9 | fi 10 | 11 | stage_name_argument=$1 12 | build_stage_name='build' 13 | scp_stage_name='scp' 14 | rcp_stage_name='rcp' 15 | stackset_stage_name='stackset' 16 | 17 | install_common_pip_packages () { 18 | # install pip packages 19 | pip install --quiet --upgrade pip==21.0.1 20 | pip install --quiet --upgrade setuptools 21 | pip install --quiet --upgrade wheel 22 | pip install --quiet --upgrade virtualenv==20.4.2 23 | pip install --quiet "cython<3.0.0" && pip install --quiet --no-build-isolation pyyaml==5.4.1 24 | pip install --quiet --upgrade yorm==1.6.2 25 | pip install --quiet --upgrade jinja2==3.1.6 26 | pip install --quiet --upgrade requests==2.32.4 27 | } 28 | 29 | build_dependencies () { 30 | # install linux packages 31 | apt-get -q install rsync -y 1> /dev/null 32 | VERSION=v4.8.0 33 | BINARY=yq_linux_amd64 34 | wget --quiet https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY} -O /usr/bin/yq && chmod +x /usr/bin/yq 35 | 36 | # install pip packages 37 | install_common_pip_packages 38 | pip install --quiet --upgrade pykwalify==1.8.0 39 | pip install --quiet cfn_flip>=1.3.0 40 | 41 | # Install CFN Nag 42 | gem install --quiet cfn-nag -v 0.7.2 43 | } 44 | 45 | scp_dependencies () { 46 | # install pip packages 47 | install_common_pip_packages 48 | } 49 | 50 | rcp_dependencies () { 51 | # install pip packages 52 | install_common_pip_packages 53 | } 54 | 55 | stackset_dependencies () { 56 | # install pip packages 57 | install_common_pip_packages 58 | } 59 | 60 | if [ $stage_name_argument == $build_stage_name ]; 61 | then 62 | echo "Installing Build Stage Dependencies." 63 | build_dependencies 64 | elif [ $stage_name_argument == $scp_stage_name ]; 65 | then 66 | echo "Installing SCP Stage Dependencies." 67 | scp_dependencies 68 | elif [ $stage_name_argument == $rcp_stage_name ]; 69 | then 70 | echo "Installing RCP Stage Dependencies." 71 | rcp_dependencies 72 | elif [ $stage_name_argument == $stackset_stage_name ]; 73 | then 74 | echo "Installing StackSet Stage Dependencies." 75 | stackset_dependencies 76 | else 77 | echo "Could not install dependencies. Argument didn't match one of the allowed values. 78 | >> build | scp | rcp | stackset" 79 | fi 80 | -------------------------------------------------------------------------------- /source/src/cfct/utils/parameter_manipulation.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance with the License. 6 | # A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is distributed # 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # 12 | # or implied. See the License for the specific language governing permissions# 13 | # and limitations under the License. # 14 | ############################################################################### 15 | 16 | 17 | def transform_params(params_in): 18 | """Splits input parameter into parameter key and value. 19 | Args: 20 | params_in (dict): Python dict of input params e.g. 21 | { 22 | "principal_role": "$[alfred_ssm_/org/primary/service_catalog/ 23 | principal/role_arn]" 24 | } 25 | 26 | Return: 27 | params_out (list): Python list of output params e.g. 28 | { 29 | "ParameterKey": "principal_role", 30 | "ParameterValue": "$[alfred_ssm_/org/primary/service_catalog/ 31 | principal/role_arn]" 32 | } 33 | """ 34 | params_list = [] 35 | for key, value in params_in.items(): 36 | param = {} 37 | param.update({"ParameterKey": key}) 38 | param.update({"ParameterValue": value}) 39 | params_list.append(param) 40 | return params_list 41 | 42 | 43 | def reverse_transform_params(params_in): 44 | """Merge input parameter key and value into one-line string 45 | Args: 46 | params_in (list): Python list of output params e.g. 47 | { 48 | "ParameterKey": "principal_role", 49 | "ParameterValue": "$[alfred_ssm_/org/primary/service_catalog/ 50 | principal/role_arn]" 51 | } 52 | Return: 53 | params_out (dict): Python dict of input params e.g. 54 | { 55 | "principal_role": "$[alfred_ssm_/org/primary/service_catalog/ 56 | principal/role_arn]" 57 | } 58 | """ 59 | params_out = {} 60 | for param in params_in: 61 | key = param.get("ParameterKey") 62 | value = param.get("ParameterValue") 63 | params_out.update({key: value}) 64 | return params_out 65 | -------------------------------------------------------------------------------- /source/src/cfct/utils/string_manipulation.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance with the License. 6 | # A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is distributed # 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # 12 | # or implied. See the License for the specific language governing permissions# 13 | # and limitations under the License. # 14 | ############################################################################### 15 | 16 | import re 17 | 18 | 19 | def sanitize(name, space_allowed=False, replace_with_character="_"): 20 | """Sanitizes input string. 21 | 22 | Replaces any character other than [a-zA-Z0-9._-] in a string 23 | with a specified character (default '_'). 24 | 25 | Args: 26 | name: Input string 27 | space_allowed (optional): 28 | Is there a space in the input string. Default to false. 29 | replace_with_character (optional): 30 | Character to replace the target character with. Default to '_'. 31 | 32 | Returns: 33 | Sanitized string 34 | 35 | Raises: 36 | """ 37 | if space_allowed: 38 | sanitized_name = re.sub(r"([^\sa-zA-Z0-9._-])", replace_with_character, name) 39 | else: 40 | sanitized_name = re.sub(r"([^a-zA-Z0-9._-])", replace_with_character, name) 41 | return sanitized_name 42 | 43 | 44 | def trim_string_from_front(string, remove_starts_with_string): 45 | """Remove string provided in the search_string 46 | and returns remainder of the string. 47 | :param string: 48 | :param remove_starts_with_string: 49 | :return: trimmed string 50 | """ 51 | if string.startswith(remove_starts_with_string): 52 | return string[len(remove_starts_with_string) :] 53 | else: 54 | raise ValueError("The beginning of the string does " "not match the string to be trimmed.") 55 | 56 | 57 | def strip_list_items(array): 58 | return [item.strip() for item in array] 59 | 60 | 61 | def remove_empty_strings(array): 62 | return [x for x in array if x != ""] 63 | 64 | 65 | def list_sanitizer(array): 66 | stripped_array = strip_list_items(array) 67 | return remove_empty_strings(stripped_array) 68 | 69 | 70 | def empty_separator_handler(delimiter, nested_ou_name): 71 | if delimiter == "": 72 | nested_ou_name_list = [nested_ou_name] 73 | else: 74 | nested_ou_name_list = nested_ou_name.split(delimiter) 75 | return nested_ou_name_list 76 | -------------------------------------------------------------------------------- /source/src/cfct/aws/services/state_machine.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance # 6 | # with the License. A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is # 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # 12 | # KIND, express or implied. See the License for the specific language # 13 | # governing permissions and limitations under the License. # 14 | ############################################################################## 15 | 16 | # !/bin/python 17 | 18 | import json 19 | 20 | import boto3 21 | from botocore.exceptions import ClientError 22 | 23 | from cfct.aws.utils.boto3_session import Boto3Session 24 | from cfct.utils.string_manipulation import sanitize 25 | 26 | 27 | class StateMachine(Boto3Session): 28 | def __init__(self, logger, **kwargs): 29 | self.logger = logger 30 | __service_name = "stepfunctions" 31 | super().__init__(logger, __service_name, **kwargs) 32 | self.state_machine_client = super().get_client() 33 | 34 | def start_execution(self, state_machine_arn, input, name): 35 | try: 36 | self.logger.info( 37 | "Starting execution of state machine: {} with " 38 | "input: {}".format(state_machine_arn, input) 39 | ) 40 | response = self.state_machine_client.start_execution( 41 | stateMachineArn=state_machine_arn, 42 | input=json.dumps(input), 43 | name=sanitize(name), 44 | ) 45 | self.logger.info("State machine Execution ARN: {}".format(response["executionArn"])) 46 | return response.get("executionArn") 47 | except ClientError as e: 48 | self.logger.log_unhandled_exception(e) 49 | raise 50 | 51 | def check_state_machine_status(self, execution_arn) -> str: 52 | try: 53 | self.logger.info("Checking execution of state machine: {}".format(execution_arn)) 54 | response = self.state_machine_client.describe_execution(executionArn=execution_arn) 55 | self.logger.info("State machine Execution Status: {}".format(response["status"])) 56 | if response["status"] == "RUNNING": 57 | return "RUNNING" 58 | elif response["status"] == "SUCCEEDED": 59 | return "SUCCEEDED" 60 | else: 61 | return "FAILED" 62 | except ClientError as e: 63 | self.logger.log_unhandled_exception(e) 64 | raise 65 | -------------------------------------------------------------------------------- /source/src/cfct/aws/services/sts.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance with the License. 6 | # A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is distributed # 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # 12 | # or implied. See the License for the specific language governing permissions# 13 | # and limitations under the License. # 14 | ############################################################################### 15 | 16 | # !/bin/python 17 | 18 | from os import environ 19 | 20 | from boto3.session import Session 21 | from botocore.exceptions import ClientError 22 | 23 | from cfct.aws.utils.boto3_session import Boto3Session 24 | 25 | 26 | class AssumeRole(object): 27 | def __call__(self, logger, account): 28 | try: 29 | sts = STS(logger) 30 | # assume role 31 | session_name = "custom-control-tower-session" 32 | partition = sts.sts_client.meta.partition 33 | role_arn = f"arn:{partition}:iam::{account}:role/{environ['EXECUTION_ROLE_NAME']}" 34 | credentials = sts.assume_role(role_arn, session_name) 35 | return credentials 36 | except ClientError as e: 37 | logger.log_unhandled_exception(e) 38 | raise 39 | 40 | 41 | class STS(Boto3Session): 42 | def __init__(self, logger, **kwargs): 43 | self.logger = logger 44 | __service_name = "sts" 45 | kwargs.update({"region": self.get_sts_region}) 46 | kwargs.update({"endpoint_url": self.get_sts_endpoint()}) 47 | super().__init__(logger, __service_name, **kwargs) 48 | self.sts_client = super().get_client() 49 | 50 | @property 51 | def get_sts_region(self): 52 | return environ.get("AWS_REGION") 53 | 54 | @staticmethod 55 | def get_sts_endpoint(): 56 | return "https://sts.%s.amazonaws.com" % environ.get("AWS_REGION") 57 | 58 | def assume_role(self, role_arn, session_name, duration=900): 59 | try: 60 | response = self.sts_client.assume_role( 61 | RoleArn=role_arn, RoleSessionName=session_name, DurationSeconds=duration 62 | ) 63 | return response["Credentials"] 64 | except ClientError as e: 65 | self.logger.log_unhandled_exception(e) 66 | raise 67 | 68 | def get_caller_identity(self): 69 | try: 70 | response = self.sts_client.get_caller_identity() 71 | return response 72 | except ClientError as e: 73 | self.logger.log_unhandled_exception(e) 74 | raise 75 | -------------------------------------------------------------------------------- /source/src/cfct/metrics/solution_metrics.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance with the License. 6 | # A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is distributed # 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # 12 | # or implied. See the License for the specific language governing permissions# 13 | # and limitations under the License. # 14 | ############################################################################### 15 | 16 | import os 17 | from datetime import datetime 18 | from json import dumps 19 | 20 | import requests 21 | 22 | from cfct.aws.services.ssm import SSM 23 | 24 | 25 | class SolutionMetrics(object): 26 | """This class is used to send anonymous metrics from customer using 27 | the solution to the Solutions Builder team when customer choose to 28 | have their data sent during the deployment of the solution. 29 | """ 30 | 31 | def __init__(self, logger): 32 | self.logger = logger 33 | self.ssm = SSM(logger) 34 | 35 | def _get_parameter_value(self, key): 36 | response = self.ssm.describe_parameters(key) 37 | # get parameter if key exist 38 | if response: 39 | value = self.ssm.get_parameter(key) 40 | else: 41 | value = "ssm-param-key-not-found" 42 | return value 43 | 44 | def solution_metrics( 45 | self, 46 | data, 47 | solution_id=os.environ.get("SOLUTION_ID"), 48 | url=os.environ.get("METRICS_URL"), 49 | ): 50 | """Sends anonymous customer metrics to s3 via API gateway owned and 51 | managed by the Solutions Builder team. 52 | 53 | Args: 54 | data: anonymous customer metrics to be sent 55 | solution_id: unique id of the solution 56 | url: url for API Gateway via which data is sent 57 | 58 | Return: status code returned by https post request 59 | """ 60 | try: 61 | send_metrics = self._get_parameter_value("/org/primary/" "metrics_flag") 62 | if send_metrics.lower() == "yes": 63 | uuid = self._get_parameter_value("/org/primary/customer_uuid") 64 | time_stamp = {"TimeStamp": str(datetime.utcnow().isoformat())} 65 | params = {"Solution": solution_id, "UUID": uuid, "Data": data} 66 | metrics = dict(time_stamp, **params) 67 | json_data = dumps(metrics) 68 | headers = {"content-type": "application/json"} 69 | r = requests.post(url, data=json_data, headers=headers) 70 | code = r.status_code 71 | return code 72 | except: 73 | pass 74 | -------------------------------------------------------------------------------- /source/codebuild_scripts/execute_stage_scripts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Check to see if input has been provided: 4 | if [ -z "$1" ]; then 5 | echo "Please provide the base source bucket name, trademark approved solution name and version where the lambda code will eventually reside." 6 | echo "For example: ./execute_stage_scripts.sh " 7 | echo "For example: ./execute_stage_scripts.sh build | scp | rcp | stackset" 8 | exit 1 9 | fi 10 | 11 | STAGE_NAME_ARGUMENT=$1 12 | LOG_LEVEL=$2 13 | WAIT_TIME=$3 14 | SM_ARN=$4 15 | ARTIFACT_BUCKET=$5 16 | KMS_KEY_ALIAS_NAME=$6 17 | BOOL_VALUES=$7 18 | NONE_TYPE_VALUES=$8 19 | BUILD_STAGE_NAME="build" 20 | SCP_STAGE_NAME="scp" 21 | RCP_STAGE_NAME="rcp" 22 | STACKSET_STAGE_NAME="stackset" 23 | CURRENT=$(pwd) 24 | MANIFEST_FILE_PATH=$CURRENT/manifest.yaml 25 | 26 | build_scripts () { 27 | echo "Date: $(date) Path: $(pwd)" 28 | echo "bash merge_directories.sh $NONE_TYPE_VALUES $BOOL_VALUES" 29 | bash merge_directories.sh "$NONE_TYPE_VALUES" "$BOOL_VALUES" 30 | echo "Executing validation tests" 31 | echo "bash run-validation.sh $ARTIFACT_BUCKET" 32 | bash run-validation.sh "$ARTIFACT_BUCKET" 33 | if [ $? == 0 ] 34 | then 35 | echo "Exit code: $? returned from the validation script." 36 | echo "INFO: Validation test(s) completed." 37 | else 38 | echo "Exit code: $? returned from the validation script." 39 | echo "ERROR: One or more validation test(s) failed." 40 | exit 1 41 | fi 42 | echo "Printing Merge Report" 43 | cat merge_report.txt 44 | } 45 | 46 | scp_scripts () { 47 | echo "Date: $(date) Path: $(pwd)" 48 | echo "python state_machine_trigger.py $LOG_LEVEL $WAIT_TIME $MANIFEST_FILE_PATH $SM_ARN $ARTIFACT_BUCKET $SCP_STAGE_NAME $KMS_KEY_ALIAS_NAME" 49 | python state_machine_trigger.py "$LOG_LEVEL" "$WAIT_TIME" "$MANIFEST_FILE_PATH" "$SM_ARN" "$ARTIFACT_BUCKET" "$SCP_STAGE_NAME" "$KMS_KEY_ALIAS_NAME" 50 | } 51 | 52 | rcp_scripts () { 53 | echo "Date: $(date) Path: $(pwd)" 54 | echo "python state_machine_trigger.py $LOG_LEVEL $WAIT_TIME $MANIFEST_FILE_PATH $SM_ARN $ARTIFACT_BUCKET $RCP_STAGE_NAME $KMS_KEY_ALIAS_NAME" 55 | python state_machine_trigger.py "$LOG_LEVEL" "$WAIT_TIME" "$MANIFEST_FILE_PATH" "$SM_ARN" "$ARTIFACT_BUCKET" "$RCP_STAGE_NAME" "$KMS_KEY_ALIAS_NAME" 56 | } 57 | 58 | stackset_scripts () { 59 | echo "Date: $(date) Path: $(pwd)" 60 | echo "python state_machine_trigger.py $LOG_LEVEL $WAIT_TIME $MANIFEST_FILE_PATH $SM_ARN $ARTIFACT_BUCKET $STACKSET_STAGE_NAME $KMS_KEY_ALIAS_NAME" 61 | python state_machine_trigger.py "$LOG_LEVEL" "$WAIT_TIME" "$MANIFEST_FILE_PATH" "$SM_ARN" "$ARTIFACT_BUCKET" "$STACKSET_STAGE_NAME" "$KMS_KEY_ALIAS_NAME" "$ENFORCE_SUCCESSFUL_STACK_INSTANCES" 62 | } 63 | 64 | if [ "$STAGE_NAME_ARGUMENT" == $BUILD_STAGE_NAME ]; 65 | then 66 | echo "Executing Build Stage Scripts." 67 | build_scripts 68 | elif [ "$STAGE_NAME_ARGUMENT" == $SCP_STAGE_NAME ]; 69 | then 70 | echo "Executing SCP Stage Scripts." 71 | scp_scripts 72 | elif [ "$STAGE_NAME_ARGUMENT" == $RCP_STAGE_NAME ]; 73 | then 74 | echo "Executing RCP Stage Scripts." 75 | rcp_scripts 76 | elif [ "$STAGE_NAME_ARGUMENT" == $STACKSET_STAGE_NAME ]; 77 | then 78 | echo "Executing StackSet Stage Scripts." 79 | stackset_scripts 80 | else 81 | echo "Could not execute scripts. Argument didn't match one of the allowed values. 82 | >> build | scp | stackset" 83 | fi 84 | -------------------------------------------------------------------------------- /source/src/cfct/manifest/stage_to_s3.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance # 6 | # with the License. A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is # 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # 12 | # KIND, express or implied. See the License for the specific language # 13 | # governing permissions and limitations under the License. # 14 | ############################################################################## 15 | 16 | # !/bin/python 17 | import os 18 | 19 | from cfct.aws.services.s3 import S3 20 | from cfct.aws.utils.url_conversion import build_http_url, convert_s3_url_to_http_url 21 | 22 | 23 | class StageFile(S3): 24 | """This class uploads the file to S3 for staging. 25 | 26 | Example: 27 | boto_3 = Boto3Session(logger, region, service_name, **kwargs) 28 | client = boto_3.get_client() 29 | """ 30 | 31 | def __init__(self, logger, relative_file_path): 32 | """ 33 | Parameters 34 | ---------- 35 | logger : object 36 | The logger object 37 | relative_file_path : str 38 | Relative Path of the file. 39 | """ 40 | self.logger = logger 41 | self.relative_file_path = relative_file_path 42 | super().__init__(logger) 43 | 44 | def get_staged_file(self): 45 | """Returns S3 URL for the local file 46 | 47 | :return: S3 URL, type: String 48 | """ 49 | 50 | if self.relative_file_path.lower().startswith("s3"): 51 | return self.convert_url() 52 | elif self.relative_file_path.lower().startswith("http"): 53 | return self.relative_file_path 54 | else: 55 | return self.stage_file() 56 | 57 | def convert_url(self): 58 | """Convert the S3 URL s3://bucket-name/object 59 | to HTTP URL https://bucket-name.s3.Region.amazonaws.com/key-name 60 | """ 61 | return convert_s3_url_to_http_url(self.relative_file_path) 62 | 63 | def stage_file(self): 64 | """Uploads local file to S3 bucket and returns S3 URL 65 | for the local file. 66 | 67 | :return: S3 URL, type: String 68 | """ 69 | local_file = os.path.join(os.environ.get("MANIFEST_FOLDER"), self.relative_file_path) 70 | key_name = "{}/{}".format(os.environ.get("TEMPLATE_KEY_PREFIX"), self.relative_file_path) 71 | self.logger.info( 72 | "Uploading the template file: {} to S3 bucket: {} " 73 | "and key: {}".format(local_file, os.environ.get("STAGING_BUCKET"), key_name) 74 | ) 75 | super().upload_file(os.environ.get("STAGING_BUCKET"), local_file, key_name) 76 | http_url = build_http_url(os.environ.get("STAGING_BUCKET"), key_name) 77 | return http_url 78 | -------------------------------------------------------------------------------- /source/src/cfct/lambda_handlers/lifecycle_event_handler.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance # 6 | # with the License. A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is # 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # 12 | # KIND, express or implied. See the License for the specific language # 13 | # governing permissions and limitations under the License. # 14 | ############################################################################## 15 | 16 | import inspect 17 | import os 18 | 19 | from cfct.aws.services.code_pipeline import CodePipeline 20 | from cfct.utils.logger import Logger 21 | 22 | # initialise logger 23 | log_level = "info" if os.environ.get("LOG_LEVEL") is None else os.environ.get("LOG_LEVEL") 24 | logger = Logger(loglevel=log_level) 25 | init_failed = False 26 | 27 | 28 | def invoke_code_pipeline(event): 29 | """Invokes code pipeline execution if there are any control tower 30 | lifecycle events in the SQS FIFO queue. 31 | 32 | Note: 33 | Here validates that the event source is aws control tower. 34 | The filtering of specific control tower lifecycle events is done 35 | by a CWE rule, which is configured to deliver only 36 | the matching events to the SQS queue. 37 | 38 | Args: 39 | event 40 | Returns: 41 | response from starting pipeline execution 42 | """ 43 | msg_count = 0 44 | for record in event["Records"]: 45 | # Here validates that the event source is aws control tower. 46 | # The filtering of specific control tower lifecycle events is done 47 | # by a CWE rule, which is configured to deliver only 48 | # the matching events to the SQS queue. 49 | if record["body"] is not None and record["body"].find('"source":"aws.controltower"') >= 0: 50 | msg_count += 1 51 | 52 | if msg_count > 0: 53 | logger.info( 54 | str(msg_count) + " Control Tower lifecycle event(s) found in the queue." 55 | " Start invoking code pipeline..." 56 | ) 57 | 58 | cp = CodePipeline(logger) 59 | response = cp.start_pipeline_execution(os.environ.get("CODE_PIPELINE_NAME")) 60 | else: 61 | logger.info("No lifecycle events in the queue!") 62 | 63 | return response 64 | 65 | 66 | def lambda_handler(event, context): 67 | """This lambda is invoked by a SQS FIFO queue as the lambda trigger. 68 | A CWE rule is defined to deliver only matching AWS control tower 69 | lifecycle events to the queue. Once the queue receives the events, 70 | it will trigger the lambda to start code pipeline execution. 71 | 72 | Args: 73 | event 74 | context 75 | """ 76 | try: 77 | logger.info("<<<<<<<<<< Poll Control Tower lifecyle events from" " SQS queue >>>>>>>>>>") 78 | logger.info(event) 79 | logger.debug(context) 80 | 81 | response = invoke_code_pipeline(event) 82 | 83 | logger.info("Response from Code Pipeline: ") 84 | logger.info(response) 85 | except Exception as e: 86 | logger.log_general_exception(__file__.split("/")[-1], inspect.stack()[0][3], e) 87 | raise 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Customizations for AWS Control Tower Solution 2 | The Customizations for AWS Control Tower solution combines AWS Control Tower and other highly-available, trusted AWS services to help customers more quickly set up a secure, multi-account AWS environment based on AWS best practices. Customers can easily add customizations to their AWS Control Tower landing zone using an AWS CloudFormation template, service control policies (SCPs), and resource control policies (RCPs). Customers can deploy their custom template and policies to both individual accounts and organizational units (OUs) within their organization. Customizations for AWS Control Tower integrates with AWS Control Tower lifecycle events to ensure that resource deployments stay in sync with the customer's landing zone. For example, when a new account is created using the AWS Control Tower account factory, the solution ensures that all resources attached to the account's OUs will be automatically deployed. Before deploying this solution, customers need to have an AWS Control Tower landing zone deployed in their account. 3 | 4 | ## Getting Started 5 | To get started with Customizations for AWS Control Tower, please review the [documentation](https://docs.aws.amazon.com/controltower/latest/userguide/customize-landing-zone.html) 6 | 7 | ## Running unit tests for customization 8 | * Clone the repository, then make the desired code changes 9 | * Next, run unit tests to make sure added customization passes the tests 10 | 11 | ``` 12 | chmod +x ./deployment/run-unit-tests.sh 13 | ./deployment/run-unit-tests.sh 14 | ``` 15 | 16 | ## Building the customized solution 17 | * Building the solution from source requires Python 3.6 or higher 18 | * Configure the solution name, version number, bucket name and (optional) opt-in region support of your target Amazon S3 distribution bucket 19 | 20 | ``` 21 | export DIST_OUTPUT_BUCKET_PREFIX=my-bucket-prefix # Prefix for the S3 bucket where customized code will be stored 22 | export TEMPLATE_OUTPUT_BUCKET=my-bucket-name # Name for the S3 bucket where the template will be stored 23 | export SOLUTION_NAME=my-solution-name # name of the solution (e.g. customizations-for-aws-control-tower) 24 | export VERSION=my-version # version number for the customized code (e.g. 2.1.0) 25 | export ENABLE_OPT_IN_REGION_SUPPORT=true # Optional flag to build with opt-in region support 26 | ``` 27 | 28 | * Update pip version to latest 29 | ``` 30 | python3 -m pip install -U pip 31 | ``` 32 | 33 | 34 | * Now build the distributable 35 | ``` 36 | chmod +x ./deployment/build-s3-dist.sh 37 | ./deployment/build-s3-dist.sh $DIST_OUTPUT_BUCKET_PREFIX $TEMPLATE_OUTPUT_BUCKET $SOLUTION_NAME $VERSION $ENABLE_OPT_IN_REGION_SUPPORT 38 | ``` 39 | 40 | * Upload the distributable to an Amazon S3 bucket in your account. 41 | 42 | * Upload the AWS CloudFormation template to your global bucket in the following pattern 43 | ``` 44 | s3://my-bucket-name/$SOLUTION_NAME/$VERSION/ 45 | ``` 46 | 47 | * Upload the customized source code zip packages to your regional bucket in the following pattern 48 | ``` 49 | s3://my-bucket-name-$REGION/$SOLUTION_NAME/$VERSION/ 50 | ``` 51 | 52 | ## Deploying the customized solution 53 | * Get the link of the custom-control-tower-initiation.template loaded to your Amazon S3 bucket. 54 | * Deploy the Customizations for AWS Control Tower solution to your account by launching a new AWS CloudFormation stack using the link of the custom-control-tower-initiation.template. 55 | 56 | ## Collection of operational metrics 57 | 58 | This solution collects anonymous operational metrics to help AWS improve the quality and features of the solution. For more information, including how to disable this capability, please see the [documentation here](https://docs.aws.amazon.com/controltower/latest/userguide/cfct-metrics.html). 59 | 60 | ## License 61 | 62 | See license [here](https://github.com/aws-solutions/aws-control-tower-customizations/blob/main/LICENSE.txt) 63 | -------------------------------------------------------------------------------- /source/src/cfct/aws/utils/url_conversion.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance with the License. 6 | # A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is distributed # 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # 12 | # or implied. See the License for the specific language governing permissions# 13 | # and limitations under the License. # 14 | ############################################################################### 15 | 16 | from os import environ 17 | from urllib.parse import urlparse 18 | 19 | from boto3.session import Session 20 | 21 | 22 | def convert_s3_url_to_http_url(s3_url): 23 | """Convert s3 url to http url. 24 | 25 | Converts the S3 URL s3://bucket-name/object 26 | to HTTP URL https://bucket-name.s3.Region.amazonaws.com/key-name 27 | 28 | Args: 29 | s3_url 30 | 31 | Returns: 32 | http url 33 | 34 | Raises: 35 | """ 36 | u = urlparse(s3_url) 37 | s3bucket = u.netloc 38 | s3key = u.path[1:] 39 | http_url = build_http_url(s3bucket, s3key) 40 | return http_url 41 | 42 | 43 | def build_http_url(bucket_name, key_name): 44 | """Builds http url for the given bucket and key name 45 | 46 | :param bucket_name: 47 | :param key_name: 48 | :return HTTP URL: 49 | example: https://bucket-name.s3.Region.amazonaws.com/key-name 50 | """ 51 | return "{}{}{}{}{}{}".format( 52 | "https://", 53 | bucket_name, 54 | ".s3.", 55 | environ.get("AWS_REGION"), 56 | ".amazonaws.com/", 57 | key_name, 58 | ) 59 | 60 | 61 | def parse_bucket_key_names(http_url): 62 | """Convert http url to s3 url. 63 | 64 | Convert the HTTP URL https://bucket-name.s3.Region.amazonaws.com/key-name or 65 | https://s3.Region.amazonaws.com/bucket-name/key-name 66 | to S3 URL s3://bucket-name/key-name. 67 | Args: 68 | http_url 69 | 70 | Returns: 71 | bucket_name, key_name, region 72 | """ 73 | # Handle Amazon S3 path-style URL 74 | # Needed to handle response from describe_provisioning_artifact API - response['Info']['TemplateUrl'] 75 | # example: https://s3.Region.amazonaws.com/bucket-name/key-name 76 | if http_url.startswith("https://s3."): 77 | parsed_url = urlparse(http_url) 78 | bucket_name = parsed_url.path.split("/", 2)[1] 79 | key_name = parsed_url.path.split("/", 2)[2] 80 | region = parsed_url.netloc.split(".")[1] 81 | # Handle Amazon S3 virtual-hosted–style URL 82 | # example: https://bucket-name.s3.Region.amazonaws.com/key-name 83 | else: 84 | parsed_url = urlparse(http_url) 85 | bucket_name = parsed_url.netloc.split(".")[0] 86 | key_name = parsed_url.path[1:] 87 | region = parsed_url.netloc.split(".")[2] 88 | session = Session() 89 | partition_name = session.get_partition_for_region(region_name=session.region_name) 90 | if region not in session.get_available_regions( 91 | partition_name=partition_name, service_name="s3" 92 | ): 93 | raise ValueError( 94 | f"URL: {http_url} is missing a 'Region' or is using a region you are not opted into.\nExpected URL format https://s3.Region.amazonaws.com/bucket-name/key-name or https://bucket-name.s3.Region.amazonaws.com/key-name" 95 | ) 96 | return bucket_name, key_name, region 97 | -------------------------------------------------------------------------------- /source/src/cfct/utils/logger.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance # 6 | # with the License. A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is # 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # 12 | # KIND, express or implied. See the License for the specific language # 13 | # governing permissions and limitations under the License. # 14 | ############################################################################## 15 | 16 | import json 17 | import logging 18 | 19 | from cfct.utils.datetime_encoder import DateTimeEncoder 20 | 21 | 22 | class Logger(object): 23 | def __init__(self, loglevel="warning"): 24 | """Initializes logging""" 25 | self.config(loglevel=loglevel) 26 | 27 | def config(self, loglevel="warning"): 28 | loglevel = logging.getLevelName(loglevel.upper()) 29 | main_logger = logging.getLogger() 30 | main_logger.setLevel(loglevel) 31 | 32 | logfmt = ( 33 | '{"time_stamp": "%(asctime)s",' 34 | '"log_level": "%(levelname)s",' 35 | '"log_message": %(message)s}\n' 36 | ) 37 | if len(main_logger.handlers) == 0: 38 | main_logger.addHandler(logging.StreamHandler()) 39 | main_logger.handlers[0].setFormatter(logging.Formatter(logfmt)) 40 | self.log = logging.LoggerAdapter(main_logger, {}) 41 | 42 | def _format(self, message): 43 | """formats log message in json, return string as is 44 | 45 | Args: 46 | message (str): log message, can be a dict, list, string, or json blob 47 | """ 48 | if isinstance(message, str): 49 | return message 50 | try: 51 | message = json.loads(message) 52 | except Exception: 53 | pass 54 | try: 55 | return json.dumps(message, indent=4, cls=DateTimeEncoder) 56 | except Exception: 57 | return json.dumps(str(message)) 58 | 59 | def debug(self, message, **kwargs): 60 | """wrapper for logging.debug call""" 61 | self.log.debug(self._format(message), **kwargs) 62 | 63 | def info(self, message, **kwargs): 64 | # type: (object, object) -> object 65 | """wrapper for logging.info call""" 66 | self.log.info(self._format(message), **kwargs) 67 | 68 | def warning(self, message, **kwargs): 69 | """wrapper for logging.warning call""" 70 | self.log.warning(self._format(message), **kwargs) 71 | 72 | def error(self, message, **kwargs): 73 | """wrapper for logging.error call""" 74 | self.log.error(self._format(message), **kwargs) 75 | 76 | def critical(self, message, **kwargs): 77 | """wrapper for logging.critical call""" 78 | self.log.critical(self._format(message), **kwargs) 79 | 80 | def exception(self, message, **kwargs): 81 | """wrapper for logging.exception call""" 82 | self.log.exception(self._format(message), **kwargs) 83 | 84 | def log_unhandled_exception(self, message): 85 | """log unhandled exception""" 86 | self.log.exception("Unhandled Exception: {}".format(message)) 87 | 88 | def log_general_exception(self, file, method, exception): 89 | """log general exception""" 90 | message = {"FILE": file, "METHOD": method, "EXCEPTION": str(exception)} 91 | self.log.exception(self._format(message)) 92 | -------------------------------------------------------------------------------- /source/src/cfct/aws/services/kms.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance with the License. 6 | # A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is distributed # 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # 12 | # or implied. See the License for the specific language governing permissions# 13 | # and limitations under the License. # 14 | ############################################################################### 15 | 16 | # !/bin/python 17 | 18 | from botocore.exceptions import ClientError 19 | 20 | from cfct.aws.utils.boto3_session import Boto3Session 21 | 22 | 23 | class KMS(Boto3Session): 24 | """This class makes KMS API calls as needed.""" 25 | 26 | def __init__(self, logger, **kwargs): 27 | self.logger = logger 28 | __service_name = "kms" 29 | super().__init__(logger, __service_name, **kwargs) 30 | self.kms_client = super().get_client() 31 | 32 | def describe_key(self, alias_name): 33 | try: 34 | key_id = "alias/" + alias_name 35 | response = self.kms_client.describe_key(KeyId=key_id) 36 | return response 37 | except ClientError as e: 38 | self.logger.log_unhandled_exception(e) 39 | raise 40 | 41 | def create_key(self, policy, description, tag_key, tag_value): 42 | try: 43 | response = self.kms_client.create_key( 44 | Policy=policy, 45 | Description=description, 46 | KeyUsage="ENCRYPT_DECRYPT", 47 | Origin="AWS_KMS", 48 | BypassPolicyLockoutSafetyCheck=True, 49 | Tags=[ 50 | {"TagKey": tag_key, "TagValue": tag_value}, 51 | ], 52 | ) 53 | return response 54 | except ClientError as e: 55 | self.logger.log_unhandled_exception(e) 56 | raise 57 | 58 | def create_alias(self, alias_name, key_name): 59 | try: 60 | response = self.kms_client.create_alias(AliasName=alias_name, TargetKeyId=key_name) 61 | return response 62 | except ClientError as e: 63 | self.logger.log_unhandled_exception(e) 64 | raise 65 | 66 | def list_aliases(self, marker=None): 67 | try: 68 | if marker: 69 | response = self.kms_client.list_aliases(Marker=marker) 70 | else: 71 | response = self.kms_client.list_aliases() 72 | return response 73 | except ClientError as e: 74 | self.logger.log_unhandled_exception(e) 75 | raise 76 | 77 | def put_key_policy(self, key_id, policy): 78 | try: 79 | response = self.kms_client.put_key_policy( 80 | KeyId=key_id, 81 | Policy=policy, 82 | # Per API docs, the only valid value is default. 83 | PolicyName="default", 84 | BypassPolicyLockoutSafetyCheck=True, 85 | ) 86 | return response 87 | except ClientError as e: 88 | self.logger.log_unhandled_exception(e) 89 | raise 90 | 91 | def enable_key_rotation(self, key_id): 92 | try: 93 | response = self.get_key_rotation_status(key_id) 94 | 95 | # Enable auto key rotation only if it hasn't been enabled 96 | if not response.get("KeyRotationEnabled"): 97 | self.kms_client.enable_key_rotation(KeyId=key_id) 98 | return response 99 | except ClientError as e: 100 | self.logger.log_unhandled_exception(e) 101 | raise 102 | 103 | def get_key_rotation_status(self, key_id): 104 | try: 105 | response = self.kms_client.get_key_rotation_status(KeyId=key_id) 106 | return response 107 | except ClientError as e: 108 | self.logger.log_unhandled_exception(e) 109 | raise 110 | -------------------------------------------------------------------------------- /source/src/cfct/aws/services/s3.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance # 6 | # with the License. A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is # 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # 12 | # KIND, express or implied. See the License for the specific language # 13 | # governing permissions and limitations under the License. # 14 | ############################################################################## 15 | 16 | # !/bin/python 17 | 18 | import tempfile 19 | 20 | from botocore.exceptions import ClientError 21 | 22 | from cfct.aws.utils.boto3_session import Boto3Session 23 | 24 | 25 | class S3(Boto3Session): 26 | def __init__(self, logger, **kwargs): 27 | self.logger = logger 28 | __service_name = "s3" 29 | super().__init__(logger, __service_name, **kwargs) 30 | self.s3_client = super().get_client() 31 | self.s3_resource = super().get_resource() 32 | 33 | def get_bucket_policy(self, bucket_name): 34 | try: 35 | response = self.s3_client.get_bucket_policy(Bucket=bucket_name) 36 | return response 37 | except ClientError as e: 38 | self.logger.log_unhandled_exception(e) 39 | raise 40 | 41 | def put_bucket_policy(self, bucket_name, bucket_policy): 42 | try: 43 | response = self.s3_client.put_bucket_policy(Bucket=bucket_name, Policy=bucket_policy) 44 | return response 45 | except ClientError as e: 46 | self.logger.log_unhandled_exception(e) 47 | raise 48 | 49 | def upload_file(self, bucket_name, local_file_location, remote_file_location): 50 | try: 51 | self.s3_resource.Bucket(bucket_name).upload_file( 52 | local_file_location, remote_file_location 53 | ) 54 | except ClientError as e: 55 | self.logger.log_unhandled_exception(e) 56 | raise 57 | 58 | def download_file(self, bucket_name, key_name, local_file_location): 59 | """This function downloads the file from the S3 bucket for a given 60 | S3 path in the method attribute. 61 | 62 | Use Cases: 63 | - download the S3 object on a given local file path 64 | 65 | :param bucket_name: 66 | :param key_name: 67 | :param local_file_location: 68 | :return None: 69 | """ 70 | try: 71 | self.logger.info( 72 | "Downloading {}/{} from S3 to {}".format(bucket_name, key_name, local_file_location) 73 | ) 74 | self.s3_resource.Bucket(bucket_name).download_file(key_name, local_file_location) 75 | except ClientError as e: 76 | self.logger.log_unhandled_exception(e) 77 | raise 78 | 79 | def put_bucket_encryption(self, bucket_name, key_id): 80 | try: 81 | self.s3_client.put_bucket_encryption( 82 | Bucket=bucket_name, 83 | ServerSideEncryptionConfiguration={ 84 | "Rules": [ 85 | { 86 | "ApplyServerSideEncryptionByDefault": { 87 | "SSEAlgorithm": "aws:kms", 88 | "KMSMasterKeyID": key_id, 89 | } 90 | }, 91 | ] 92 | }, 93 | ) 94 | 95 | except ClientError as e: 96 | self.logger.log_unhandled_exception(e) 97 | raise 98 | 99 | def list_buckets(self): 100 | return self.s3_client.list_buckets() 101 | 102 | def get_s3_object(self, remote_s3_url): 103 | """This function downloads the file from the S3 bucket for a given 104 | S3 path in the method attribute. 105 | 106 | :param remote_s3_url: s3://bucket-name/key-name 107 | :return: remote S3 file 108 | 109 | Use Cases: 110 | - manifest file contains template and parameter file as s3://bucket-name/key in SM trigger lambda 111 | """ 112 | try: 113 | _file = tempfile.mkstemp()[1] 114 | parsed_s3_path = remote_s3_url.split("/", 3) # s3://bucket-name/key 115 | remote_bucket = parsed_s3_path[2] # Bucket name 116 | remote_key = parsed_s3_path[3] # Key 117 | self.s3_client.download_file(remote_bucket, remote_key, _file) 118 | return _file 119 | except ClientError as e: 120 | self.logger.log_unhandled_exception(e) 121 | raise 122 | -------------------------------------------------------------------------------- /source/src/cfct/aws/services/organizations.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance # 6 | # with the License. A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is # 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # 12 | # KIND, express or implied. See the License for the specific language # 13 | # governing permissions and limitations under the License. # 14 | ############################################################################## 15 | 16 | # !/bin/python 17 | 18 | from botocore.exceptions import ClientError 19 | 20 | from cfct.aws.utils.boto3_session import Boto3Session 21 | from cfct.utils.retry_decorator import try_except_retry 22 | 23 | 24 | class Organizations(Boto3Session): 25 | def __init__(self, logger, **kwargs): 26 | self.logger = logger 27 | __service_name = "organizations" 28 | super().__init__(logger, __service_name, **kwargs) 29 | self.org_client = super().get_client() 30 | self.next_token_returned_msg = "Next Token Returned: {}" 31 | 32 | def list_roots(self): 33 | try: 34 | response = self.org_client.list_roots() 35 | return response 36 | except ClientError as e: 37 | self.logger.log_unhandled_exception(e) 38 | 39 | def list_organizational_units_for_parent(self, parent_id): 40 | try: 41 | response = self.org_client.list_organizational_units_for_parent(ParentId=parent_id) 42 | 43 | ou_list = response.get("OrganizationalUnits", []) 44 | next_token = response.get("NextToken", None) 45 | 46 | while next_token is not None: 47 | self.logger.info(self.next_token_returned_msg.format(next_token)) 48 | response = self.org_client.list_organizational_units_for_parent( 49 | ParentId=parent_id, NextToken=next_token 50 | ) 51 | self.logger.info("Extending OU List") 52 | ou_list.extend(response.get("OrganizationalUnits", [])) 53 | next_token = response.get("NextToken", None) 54 | 55 | return ou_list 56 | except ClientError as e: 57 | self.logger.log_unhandled_exception(e) 58 | raise 59 | 60 | def list_accounts_for_parent(self, parent_id): 61 | try: 62 | response = self.org_client.list_accounts_for_parent(ParentId=parent_id) 63 | 64 | account_list = response.get("Accounts", []) 65 | next_token = response.get("NextToken", None) 66 | 67 | while next_token is not None: 68 | self.logger.info(self.next_token_returned_msg.format(next_token)) 69 | response = self.org_client.list_accounts_for_parent( 70 | ParentId=parent_id, NextToken=next_token 71 | ) 72 | self.logger.info("Extending Account List") 73 | account_list.extend(response.get("Accounts", [])) 74 | next_token = response.get("NextToken", None) 75 | 76 | return account_list 77 | except ClientError as e: 78 | self.logger.log_unhandled_exception(e) 79 | raise 80 | 81 | def list_accounts(self, **kwargs): 82 | try: 83 | response = self.org_client.list_accounts(**kwargs) 84 | return response 85 | except Exception as e: 86 | self.logger.log_unhandled_exception(e) 87 | raise 88 | 89 | def get_accounts_in_org(self): 90 | try: 91 | response = self.org_client.list_accounts() 92 | 93 | account_list = response.get("Accounts", []) 94 | next_token = response.get("NextToken", None) 95 | 96 | while next_token is not None: 97 | self.logger.info(self.next_token_returned_msg.format(next_token)) 98 | response = self.org_client.list_accounts(NextToken=next_token) 99 | self.logger.info("Extending Account List") 100 | account_list.extend(response.get("Accounts", [])) 101 | next_token = response.get("NextToken", None) 102 | 103 | return account_list 104 | except ClientError as e: 105 | self.logger.log_unhandled_exception(e) 106 | raise 107 | 108 | def describe_organization(self): 109 | try: 110 | response = self.org_client.describe_organization() 111 | return response 112 | except ClientError as e: 113 | self.logger.log_unhandled_exception(e) 114 | raise 115 | -------------------------------------------------------------------------------- /source/codebuild_scripts/merge_manifest.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance # 6 | # with the License. A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is # 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # 12 | # KIND, express or implied. See the License for the specific language # 13 | # governing permissions and limitations under the License. # 14 | ############################################################################## 15 | 16 | import sys 17 | 18 | import yaml 19 | from cfct.utils.logger import Logger 20 | 21 | log_level = "info" 22 | logger = Logger(loglevel=log_level) 23 | 24 | 25 | # Iterate through the first level keys and add them if not found in the 26 | # existing manifest file 27 | def update_level_one_list(existing, add_on, level_one_dct_key, decision_key): 28 | if add_on.get(level_one_dct_key): 29 | for add_on_key_level_one_list in add_on.get(level_one_dct_key): 30 | flag = False 31 | if existing.get(level_one_dct_key): 32 | for existing_key_level_one_list in existing.get(level_one_dct_key): 33 | if add_on_key_level_one_list.get( 34 | decision_key 35 | ) == existing_key_level_one_list.get(decision_key): 36 | flag = False 37 | # break the loop if same name is found in the list 38 | break 39 | else: 40 | # Setting the flag to add the value after scanning 41 | # the full list 42 | flag = True 43 | else: 44 | flag = True 45 | if flag and add_on_key_level_one_list not in existing.get( 46 | level_one_dct_key 47 | ): 48 | # to avoid duplication append check to see if value in 49 | # the list already exist 50 | logger.info( 51 | "(Level 1) Adding new {} > {}: {}".format( 52 | type(add_on_key_level_one_list).__name__, 53 | decision_key, 54 | add_on_key_level_one_list.get(decision_key), 55 | ) 56 | ) 57 | existing.get(level_one_dct_key).append(add_on_key_level_one_list) 58 | logger.debug(existing.get(level_one_dct_key)) 59 | return existing 60 | 61 | 62 | def _reload(add_on, original): 63 | # return original manifest if updated manifest is None 64 | update = add_on if add_on is not None else original 65 | return update 66 | 67 | 68 | def _json_to_yaml(json, filename): 69 | # Convert json to yaml 70 | # logger.debug(json) 71 | yml = yaml.safe_dump(json, default_flow_style=False, indent=2) 72 | # print(yml) 73 | 74 | # create new manifest file 75 | file = open(filename, "w") 76 | file.write(yml) 77 | file.close() 78 | 79 | 80 | def update_scp_policies(add_on, original): 81 | level_1_key = "organization_policies" 82 | decision_key = "name" 83 | 84 | # process new scp policy addition 85 | updated_manifest = update_level_one_list( 86 | original, add_on, level_1_key, decision_key 87 | ) 88 | original = _reload(updated_manifest, original) 89 | 90 | return original 91 | 92 | 93 | def update_cloudformation_resources(add_on, original): 94 | level_1_key = "cloudformation_resources" 95 | decision_key = "name" 96 | 97 | # process new baseline addition 98 | updated_manifest = update_level_one_list( 99 | original, add_on, level_1_key, decision_key 100 | ) 101 | original = _reload(updated_manifest, original) 102 | 103 | return original 104 | 105 | 106 | def main(): 107 | manifest = yaml.safe_load(open(master_manifest_file_path)) 108 | logger.debug(manifest) 109 | 110 | add_on_manifest = yaml.safe_load(open(add_on_manifest_file_path)) 111 | logger.debug(add_on_manifest) 112 | 113 | manifest = update_scp_policies(add_on_manifest, manifest) 114 | 115 | manifest = update_cloudformation_resources(add_on_manifest, manifest) 116 | 117 | _json_to_yaml(manifest, output_manifest_file_path) 118 | 119 | 120 | if __name__ == "__main__": 121 | if len(sys.argv) > 3: 122 | master_manifest_file_path = sys.argv[1] 123 | add_on_manifest_file_path = sys.argv[2] 124 | output_manifest_file_path = sys.argv[3] 125 | main() 126 | else: 127 | print( 128 | "No arguments provided. Please provide the existing and" 129 | " new manifest files names." 130 | ) 131 | print( 132 | "Example: merge_manifest.py" 133 | " " 134 | ) 135 | sys.exit(2) 136 | -------------------------------------------------------------------------------- /source/codebuild_scripts/find_replace.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance with the License. 6 | # A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is distributed # 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # 12 | # or implied. See the License for the specific language governing permissions# 13 | # and limitations under the License. # 14 | ############################################################################### 15 | 16 | # !/bin/python 17 | 18 | import inspect 19 | import json 20 | import os 21 | import sys 22 | 23 | import yaml 24 | from cfct.utils.logger import Logger 25 | from cfct.utils.path_utils import is_safe_path 26 | from jinja2 import Environment, FileSystemLoader 27 | 28 | # initialise logger 29 | log_level = "info" 30 | logger = Logger(loglevel=log_level) 31 | 32 | 33 | def find_replace(function_path, file_name, destination_file, parameters): 34 | try: 35 | j2loader = FileSystemLoader(function_path) 36 | j2env = Environment(loader=j2loader, autoescape=True) # Compliant 37 | j2template = j2env.get_template(file_name) 38 | dictionary = {} 39 | for key, value in parameters.items(): 40 | if "json" in file_name and not isinstance(value, list): 41 | value = '"%s"' % value 42 | elif "json" in file_name and isinstance(value, list): 43 | value = json.dumps(value) 44 | dictionary.update({key: value}) 45 | logger.debug(dictionary) 46 | output = j2template.render(dictionary) 47 | if is_safe_path(function_path, destination_file): 48 | abs_destination_file = os.path.abspath(destination_file) 49 | with open(abs_destination_file, "w") as fh: 50 | fh.write(output) 51 | else: 52 | raise ValueError(f"Unsafe file path detected {destination_file}") 53 | except Exception as e: 54 | logger.log_general_exception(__file__.split("/")[-1], inspect.stack()[0][3], e) 55 | raise 56 | 57 | 58 | def update_add_on_manifest(event, path): 59 | extract_path = path 60 | exclude_j2_files = [] 61 | 62 | # Find and replace the variable in Manifest file 63 | for item in event.get("input_parameters"): 64 | f = item.get("file_name") 65 | exclude_j2_files.append(f) 66 | filename, file_extension = os.path.splitext(f) 67 | destination_file_path = ( 68 | extract_path + "/" + filename 69 | if file_extension == ".j2" 70 | else extract_path + "/" + f 71 | ) 72 | find_replace(extract_path, f, destination_file_path, item.get("parameters")) 73 | 74 | 75 | def sanitize_boolean_type(s, bools): 76 | s = " " + s 77 | logger.info("Adding quotes around the boolean values: {}".format(bools)) 78 | logger.info("Print original string: {}".format(s)) 79 | for w in [x.strip() for x in bools.split(",")]: 80 | s = s.replace(":" + " " + w, ': "' + w + '"') 81 | logger.info( 82 | "If found, wrapped '{}' with double quotes, printing" 83 | " the modified string: {}".format(w, s) 84 | ) 85 | return yaml.safe_load(s[1:]) 86 | 87 | 88 | def sanitize_null_type(d, none_type_values): 89 | s = json.dumps(d) 90 | s = " " + s 91 | logger.info("Replacing none_type/null with empty quotes.") 92 | for w in [x.strip() for x in none_type_values.split(",")]: 93 | s = s.replace(":" + " " + w, ': ""') 94 | logger.info( 95 | "If found, replacing '{}' with double quotes, printing" 96 | " the modified string: {}".format(w, s) 97 | ) 98 | return yaml.safe_load(s[1:]) 99 | 100 | 101 | def generate_event(user_input_file, path, bools, none_types): 102 | logger.info("Generating Event") 103 | if is_safe_path(path, user_input_file): 104 | abs_user_path = os.path.abspath(user_input_file) 105 | with open(abs_user_path) as f: 106 | user_input = sanitize_boolean_type(f.read(), bools) 107 | logger.info("Boolean values wrapped with quotes (if applicable)") 108 | logger.info(user_input) 109 | user_input = sanitize_null_type(user_input, none_types) 110 | logger.info("Null values replaced with quotes (if applicable)") 111 | logger.info(user_input) 112 | else: 113 | raise ValueError(f"Unsafe file path detected {user_input_file}") 114 | update_add_on_manifest(user_input, path) 115 | 116 | 117 | if __name__ == "__main__": 118 | if len(sys.argv) > 4: 119 | path = sys.argv[2] 120 | file_name = sys.argv[1] 121 | none_type_values = sys.argv[4] 122 | boolean_type_values = sys.argv[3] 123 | generate_event(file_name, path, boolean_type_values, none_type_values) 124 | else: 125 | print( 126 | "Not enough arguments provided. Please provide the path and" 127 | " user input file names." 128 | ) 129 | print("Example: merge_manifest.py " " ") 130 | sys.exit(2) 131 | -------------------------------------------------------------------------------- /source/src/cfct/manifest/manifest.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance # 6 | # with the License. A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is # 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # 12 | # KIND, express or implied. See the License for the specific language # 13 | # governing permissions and limitations under the License. # 14 | ############################################################################## 15 | 16 | import yorm 17 | from yorm.types import AttributeDictionary, Boolean, List, String 18 | 19 | 20 | @yorm.attr(name=String) 21 | @yorm.attr(value=String) 22 | class SSM(AttributeDictionary): 23 | def __init__(self, name, value): 24 | super().__init__() 25 | self.name = name 26 | self.value = value 27 | 28 | 29 | @yorm.attr(all=SSM) 30 | class SSMList(List): 31 | def __init__(self): 32 | super().__init__() 33 | 34 | 35 | @yorm.attr(all=String) 36 | class RegionsList(List): 37 | def __init__(self): 38 | super().__init__() 39 | 40 | 41 | @yorm.attr(all=String) 42 | class AccountList(List): 43 | def __init__(self): 44 | super().__init__() 45 | 46 | 47 | @yorm.attr(all=String) 48 | class OUList(List): 49 | def __init__(self): 50 | super().__init__() 51 | 52 | 53 | @yorm.attr(parameter_key=String) 54 | @yorm.attr(parameter_value=String) 55 | class Parameter(AttributeDictionary): 56 | def __init__(self, key, value): 57 | super().__init__() 58 | self.parameter_key = key 59 | self.parameter_value = value 60 | 61 | 62 | @yorm.attr(all=Parameter) 63 | class Parameters(List): 64 | def __init__(self): 65 | super().__init__() 66 | 67 | 68 | @yorm.attr(accounts=AccountList) 69 | @yorm.attr(organizational_units=OUList) 70 | class DeployTargets(AttributeDictionary): 71 | def __init__(self): 72 | super().__init__() 73 | self.accounts = [] 74 | self.organizational_units = [] 75 | 76 | 77 | @yorm.attr(all=String) 78 | class ApplyToOUList(List): 79 | def __init__(self): 80 | super().__init__() 81 | 82 | 83 | @yorm.attr(name=String) 84 | @yorm.attr(template_file=String) 85 | @yorm.attr(parameter_file=String) 86 | @yorm.attr(deploy_method=String) 87 | @yorm.attr(ssm_parameters=SSMList) 88 | @yorm.attr(regions=RegionsList) 89 | @yorm.attr(deploy_to_account=AccountList) 90 | @yorm.attr(deploy_to_ou=OUList) 91 | class CfnResource(AttributeDictionary): 92 | def __init__(self, name, template_file, parameter_file, deploy_method): 93 | super().__init__() 94 | self.name = name 95 | self.template_file = template_file 96 | self.parameter_file = parameter_file 97 | self.deploy_method = deploy_method 98 | self.deploy_to_account = [] 99 | self.deploy_to_ou = [] 100 | self.regions = [] 101 | self.ssm_parameters = [] 102 | 103 | 104 | @yorm.attr(all=CfnResource) 105 | class CfnResourcesList(List): 106 | def __init__(self): 107 | super().__init__() 108 | 109 | 110 | @yorm.attr(name=String) 111 | @yorm.attr(policy_file=String) 112 | @yorm.attr(description=String) 113 | @yorm.attr(apply_to_accounts_in_ou=ApplyToOUList) 114 | class Policy(AttributeDictionary): 115 | def __init__(self, name, policy_file, description, apply_to_accounts_in_ou): 116 | super().__init__() 117 | self.name = name 118 | self.description = description 119 | self.policy_file = policy_file 120 | self.apply_to_accounts_in_ou = apply_to_accounts_in_ou 121 | 122 | 123 | @yorm.attr(all=Policy) 124 | class PolicyList(List): 125 | def __init__(self): 126 | super().__init__() 127 | 128 | 129 | @yorm.attr(name=String) 130 | @yorm.attr(resource_file=String) 131 | @yorm.attr(parameter_file=String) 132 | @yorm.attr(deploy_method=String) 133 | @yorm.attr(export_outputs=SSMList) 134 | @yorm.attr(regions=RegionsList) 135 | @yorm.attr(deployment_targets=DeployTargets) 136 | @yorm.attr(parameters=Parameters) 137 | class ResourceProps(AttributeDictionary): 138 | def __init__( 139 | self, 140 | name, 141 | resource_file, 142 | parameters, 143 | parameter_file, 144 | deploy_method, 145 | deployment_targets, 146 | export_outputs, 147 | regions, 148 | ): 149 | super().__init__() 150 | self.name = name 151 | self.resource_file = resource_file 152 | self.parameter_file = parameter_file 153 | self.parameters = parameters 154 | self.deploy_method = deploy_method 155 | self.deployment_targets = deployment_targets 156 | self.regions = regions 157 | self.export_outputs = export_outputs 158 | 159 | 160 | @yorm.attr(all=ResourceProps) 161 | class Resources(List): 162 | def __init__(self): 163 | super().__init__() 164 | 165 | 166 | @yorm.attr(region=String) 167 | @yorm.attr(version=String) 168 | @yorm.attr(enable_stack_set_deletion=Boolean) 169 | @yorm.attr(cloudformation_resources=CfnResourcesList) 170 | @yorm.attr(organization_policies=PolicyList) 171 | @yorm.attr(resources=Resources) 172 | @yorm.sync("{self.manifest_file}", auto_create=False) 173 | class Manifest: 174 | def __init__(self, manifest_file): 175 | self.manifest_file = manifest_file 176 | self.enable_stack_set_deletion = False 177 | self.organization_policies = [] 178 | self.cloudformation_resources = [] 179 | self.resources = [] 180 | -------------------------------------------------------------------------------- /source/src/cfct/aws/utils/boto3_session.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance # 6 | # with the License. A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is # 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # 12 | # KIND, express or implied. See the License for the specific language # 13 | # governing permissions and limitations under the License. # 14 | ############################################################################## 15 | 16 | from os import getenv 17 | 18 | # !/bin/python 19 | import boto3 20 | from botocore.config import Config 21 | 22 | 23 | class Boto3Session: 24 | """This class initialize boto3 client for a given AWS service name. 25 | 26 | Example: 27 | class EC2(Boto3Session): 28 | def __init__(self, logger, region, **kwargs): 29 | self.logger = logger 30 | __service_name = 'ec2' 31 | kwargs.update({'region': region}) # optional 32 | super().__init__(logger, __service_name, **kwargs) 33 | self.ec2_client = super().get_client() 34 | """ 35 | 36 | def __init__(self, logger, service_name, **kwargs): 37 | """ 38 | Parameters 39 | ---------- 40 | logger : object 41 | The logger object 42 | region : str 43 | AWS region name. Example: 'us-east-1' 44 | service_name : str 45 | AWS service name. Example: 'ec2' 46 | credentials = dict, optional 47 | set of temporary AWS security credentials 48 | endpoint_url : str 49 | The complete URL to use for the constructed client. 50 | """ 51 | self.logger = logger 52 | self.service_name = service_name 53 | self.credentials = kwargs.get("credentials", None) 54 | self.region = kwargs.get("region", None) 55 | self.endpoint_url = kwargs.get("endpoint_url", None) 56 | self.solution_id = getenv("SOLUTION_ID", "SO0089") 57 | self.solution_version = getenv("SOLUTION_VERSION", "undefined") 58 | user_agent = f"AwsSolution/{self.solution_id}/{self.solution_version}" 59 | self.boto_config = Config( 60 | user_agent_extra=user_agent, 61 | retries={"mode": "standard", "max_attempts": 20}, 62 | ) 63 | 64 | def get_client(self): 65 | """Creates a boto3 low-level service client by name. 66 | 67 | Returns: service client, type: Object 68 | """ 69 | if self.credentials is None: 70 | if self.endpoint_url is None: 71 | return boto3.client( 72 | self.service_name, region_name=self.region, config=self.boto_config 73 | ) 74 | else: 75 | return boto3.client( 76 | self.service_name, 77 | region_name=self.region, 78 | config=self.boto_config, 79 | endpoint_url=self.endpoint_url, 80 | ) 81 | else: 82 | if self.region is None: 83 | return boto3.client( 84 | self.service_name, 85 | aws_access_key_id=self.credentials.get("AccessKeyId"), 86 | aws_secret_access_key=self.credentials.get("SecretAccessKey"), 87 | aws_session_token=self.credentials.get("SessionToken"), 88 | config=self.boto_config, 89 | ) 90 | else: 91 | return boto3.client( 92 | self.service_name, 93 | region_name=self.region, 94 | aws_access_key_id=self.credentials.get("AccessKeyId"), 95 | aws_secret_access_key=self.credentials.get("SecretAccessKey"), 96 | aws_session_token=self.credentials.get("SessionToken"), 97 | config=self.boto_config, 98 | ) 99 | 100 | def get_resource(self): 101 | """Creates a boto3 resource service client object by name 102 | 103 | Returns: resource service client, type: Object 104 | """ 105 | if self.credentials is None: 106 | if self.endpoint_url is None: 107 | return boto3.resource( 108 | self.service_name, region_name=self.region, config=self.boto_config 109 | ) 110 | else: 111 | return boto3.resource( 112 | self.service_name, 113 | region_name=self.region, 114 | config=self.boto_config, 115 | endpoint_url=self.endpoint_url, 116 | ) 117 | else: 118 | if self.region is None: 119 | return boto3.resource( 120 | self.service_name, 121 | aws_access_key_id=self.credentials.get("AccessKeyId"), 122 | aws_secret_access_key=self.credentials.get("SecretAccessKey"), 123 | aws_session_token=self.credentials.get("SessionToken"), 124 | config=self.boto_config, 125 | ) 126 | else: 127 | return boto3.resource( 128 | self.service_name, 129 | region_name=self.region, 130 | aws_access_key_id=self.credentials.get("AccessKeyId"), 131 | aws_secret_access_key=self.credentials.get("SecretAccessKey"), 132 | aws_session_token=self.credentials.get("SessionToken"), 133 | config=self.boto_config, 134 | ) 135 | -------------------------------------------------------------------------------- /deployment/lambda_build.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance # 6 | # with the License. A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is # 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # 12 | # KIND, express or implied. See the License for the specific language # 13 | # governing permissions and limitations under the License. # 14 | ############################################################################## 15 | 16 | # !/usr/bin/env python3 17 | import glob 18 | import os 19 | import shutil 20 | import subprocess 21 | import sys 22 | from pathlib import Path 23 | 24 | LIB_PATH = "source/src" 25 | DIST_PATH = "deployment/dist" 26 | HANDLERS_PATH = "source/src/cfct/lambda_handlers" 27 | S3_OUTPUT_PATH = "deployment/regional-s3-assets/" 28 | CODEBUILD_SCRIPTS_PATH = "source/codebuild_scripts" 29 | 30 | LAMBDA_BUILD_MAPPING = { 31 | "state_machine_lambda": "custom-control-tower-state-machine", 32 | "deployment_lambda": "custom-control-tower-config-deployer", 33 | "build_scripts": "custom-control-tower-scripts", 34 | "lifecycle_event_handler": "custom-control-tower-lifecycle-event-handler", 35 | "state_machine_trigger": "custom-control-tower-state-machine-trigger", 36 | } 37 | 38 | 39 | def install_dependencies( 40 | dist_folder: str, 41 | lib_path: str, 42 | handlers_path: str, 43 | codebuild_script_path: str, 44 | clean: bool = True, 45 | ) -> None: 46 | if os.path.exists(dist_folder) and clean: 47 | shutil.rmtree(dist_folder) 48 | subprocess.run( 49 | ["pip", "install", "--quiet", lib_path, "--target", dist_folder], check=True 50 | ) 51 | 52 | # Capture all installed package versions into requirements.txt 53 | requirements_path = os.path.join(dist_folder, "requirements.txt") 54 | subprocess.run( 55 | ["pip", "freeze", "--path", dist_folder], 56 | check=True, 57 | stdout=open(requirements_path, "w") 58 | ) 59 | 60 | # Include lambda handlers in distributables 61 | for file in glob.glob(f"{handlers_path}/*.py"): 62 | shutil.copy(src=file, dst=dist_folder) 63 | 64 | # Include Codebuild scripts in distributables 65 | shutil.copytree(src=codebuild_script_path, dst=f"{dist_folder}/codebuild_scripts") 66 | 67 | # Remove *.dist-info from distributables 68 | for file in glob.glob(f"{dist_folder}/*.dist-info"): 69 | shutil.rmtree(path=file) 70 | 71 | 72 | def create_lambda_archive(zip_file_name: str, source: str, output_path: str) -> None: 73 | # Create archive for specific lambda 74 | print( 75 | f"Contents of {source} will be zipped in {zip_file_name}" 76 | f" and saved in the {S3_OUTPUT_PATH}" 77 | ) 78 | archive_path = shutil.make_archive( 79 | base_name=zip_file_name, format="zip", root_dir=source 80 | ) 81 | destination_archive_name = f"{output_path}/{Path(archive_path).name}" 82 | if os.path.exists(destination_archive_name): 83 | os.remove(destination_archive_name) 84 | 85 | shutil.move(src=archive_path, dst=output_path) 86 | 87 | 88 | def main(argv): 89 | print(argv) 90 | if "help" in argv: 91 | print( 92 | "Help: Please provide either or all the arguments as shown in" 93 | " the example below." 94 | ) 95 | print( 96 | "lambda_build.py avm_cr_lambda state_machine_lambda" 97 | " trigger_lambda deployment_lambda" 98 | ) 99 | sys.exit(2) 100 | else: 101 | os.makedirs(S3_OUTPUT_PATH, exist_ok=True) 102 | print(" Installing dependencies...") 103 | install_dependencies( 104 | dist_folder=DIST_PATH, 105 | lib_path=LIB_PATH, 106 | handlers_path=HANDLERS_PATH, 107 | codebuild_script_path=CODEBUILD_SCRIPTS_PATH, 108 | ) 109 | 110 | for arg in argv: 111 | if arg in LAMBDA_BUILD_MAPPING: 112 | print("\n Building {} \n ===========================\n".format(arg)) 113 | zip_file_name = LAMBDA_BUILD_MAPPING[arg] 114 | else: 115 | print( 116 | "Invalid argument... Please provide either or all the" 117 | " arguments as shown in the example below." 118 | ) 119 | print( 120 | "lambda_build.py avm_cr_lambda state_machine_lambda" 121 | " trigger_lambda deployment_lambda" 122 | " add_on_deployment_lambda" 123 | ) 124 | sys.exit(2) 125 | 126 | print(f" Creating archive for {zip_file_name}..") 127 | create_lambda_archive( 128 | zip_file_name=zip_file_name, 129 | source=DIST_PATH, 130 | output_path=S3_OUTPUT_PATH, 131 | ) 132 | 133 | 134 | if __name__ == "__main__": 135 | if len(sys.argv) > 1: 136 | main(sys.argv[1:]) 137 | else: 138 | print( 139 | "No arguments provided. Please provide any combination of OR" 140 | " all 4 arguments as shown in the example below." 141 | ) 142 | print( 143 | "lambda_build.py avm_cr_lambda state_machine_lambda" 144 | " trigger_lambda deployment_lambda add_on_deployment_lambda" 145 | ) 146 | print("Example 2:") 147 | print("lambda_build.py avm_cr_lambda state_machine_lambda" " trigger_lambda") 148 | print("Example 3:") 149 | print("lambda_build.py avm_cr_lambda state_machine_lambda") 150 | print("Example 4:") 151 | print("lambda_build.py avm_cr_lambda") 152 | sys.exit(2) 153 | -------------------------------------------------------------------------------- /source/src/cfct/aws/services/ssm.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance # 6 | # with the License. A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is # 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # 12 | # KIND, express or implied. See the License for the specific language # 13 | # governing permissions and limitations under the License. # 14 | ############################################################################## 15 | 16 | # !/bin/python 17 | import os 18 | 19 | from botocore.exceptions import ClientError 20 | 21 | from cfct.aws.utils.boto3_session import Boto3Session 22 | from cfct.utils.retry_decorator import try_except_retry 23 | 24 | ssm_region = os.environ.get("AWS_REGION") 25 | 26 | 27 | class SSM(Boto3Session): 28 | def __init__(self, logger, region=ssm_region, **kwargs): 29 | self.logger = logger 30 | __service_name = "ssm" 31 | kwargs.update({"region": region}) 32 | super().__init__(logger, __service_name, **kwargs) 33 | self.ssm_client = super().get_client() 34 | self.description = "This value was stored by Custom Control " 35 | "Tower Solution." 36 | 37 | def put_parameter( 38 | self, 39 | name, 40 | value, 41 | description="This value was stored by Custom Control " "Tower Solution.", 42 | type="String", 43 | overwrite=True, 44 | ): 45 | try: 46 | response = self.ssm_client.put_parameter( 47 | Name=name, 48 | Value=value, 49 | Description=description, 50 | Type=type, 51 | Overwrite=overwrite, 52 | ) 53 | return response 54 | except ClientError as e: 55 | self.logger.log_unhandled_exception(e) 56 | raise 57 | 58 | def put_parameter_use_cmk( 59 | self, 60 | name, 61 | value, 62 | key_id, 63 | description="This value was stored by Custom " "Control Tower Solution.", 64 | type="SecureString", 65 | overwrite=True, 66 | ): 67 | try: 68 | response = self.ssm_client.put_parameter( 69 | Name=name, 70 | Value=value, 71 | Description=description, 72 | KeyId=key_id, 73 | Type=type, 74 | Overwrite=overwrite, 75 | ) 76 | return response 77 | except ClientError as e: 78 | self.logger.log_unhandled_exception(e) 79 | raise 80 | 81 | def get_parameter(self, name): 82 | try: 83 | response = self.ssm_client.get_parameter(Name=name, WithDecryption=True) 84 | return response.get("Parameter", {}).get("Value") 85 | except ClientError as e: 86 | if e.response["Error"]["Code"] == "ParameterNotFound": 87 | self.logger.log_unhandled_exception( 88 | "The SSM Parameter {} was not found".format(name) 89 | ) 90 | self.logger.log_unhandled_exception(e) 91 | raise 92 | 93 | def delete_parameter(self, name): 94 | try: 95 | response = self.ssm_client.delete_parameter( 96 | # Name (string) 97 | Name=name 98 | ) 99 | return response 100 | except ClientError as e: 101 | self.logger.log_unhandled_exception(e) 102 | raise 103 | 104 | def get_parameters_by_path(self, path): 105 | try: 106 | response = self.ssm_client.get_parameters_by_path( 107 | Path=path if path.startswith("/") else "/" + path, 108 | Recursive=False, 109 | WithDecryption=True, 110 | ) 111 | params_list = response.get("Parameters", []) 112 | next_token = response.get("NextToken", None) 113 | 114 | while next_token is not None: 115 | response = self.ssm_client.get_parameters_by_path( 116 | Path=path if path.startswith("/") else "/" + path, 117 | Recursive=False, 118 | WithDecryption=True, 119 | NextToken=next_token, 120 | ) 121 | params_list.extend(response.get("Parameters", [])) 122 | next_token = response.get("NextToken", None) 123 | 124 | return params_list 125 | except ClientError as e: 126 | self.logger.log_unhandled_exception(e) 127 | raise 128 | 129 | def delete_parameters_by_path(self, name): 130 | try: 131 | params_list = self.get_parameters_by_path(name) 132 | if params_list: 133 | for param in params_list: 134 | self.delete_parameter(param.get("Name")) 135 | except ClientError as e: 136 | self.logger.log_unhandled_exception(e) 137 | raise 138 | 139 | @try_except_retry() 140 | def describe_parameters(self, parameter_name, begins_with=False): 141 | try: 142 | response = self.ssm_client.describe_parameters( 143 | ParameterFilters=[ 144 | { 145 | "Key": "Name", 146 | "Option": "BeginsWith" if begins_with else "Equals", 147 | "Values": [parameter_name], 148 | } 149 | ] 150 | ) 151 | parameters = response.get("Parameters", []) 152 | if parameters: 153 | return parameters[0] 154 | else: 155 | return None 156 | except ClientError as e: 157 | self.logger.log_unhandled_exception(e) 158 | raise 159 | -------------------------------------------------------------------------------- /source/src/cfct/utils/crhelper.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance with the License. 6 | # A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is distributed # 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # 12 | # or implied. See the License for the specific language governing permissions# 13 | # and limitations under the License. # 14 | ############################################################################### 15 | 16 | import json 17 | import threading 18 | 19 | import requests 20 | 21 | 22 | def send( 23 | event, 24 | context, 25 | response_status, 26 | response_data, 27 | physical_resource_id, 28 | logger, 29 | reason=None, 30 | ): 31 | """This function sends status and response data to cloudformation.""" 32 | response_url = event["ResponseURL"] 33 | logger.debug("CFN response URL: " + response_url) 34 | 35 | response_body = {} 36 | response_body["Status"] = response_status 37 | msg = "See details in CloudWatch Log Stream: " + context.log_stream_name 38 | if not reason: 39 | response_body["Reason"] = msg 40 | else: 41 | response_body["Reason"] = str(reason)[0:255] + "... " + msg 42 | response_body["PhysicalResourceId"] = physical_resource_id or context.log_stream_name 43 | response_body["StackId"] = event["StackId"] 44 | response_body["RequestId"] = event["RequestId"] 45 | response_body["LogicalResourceId"] = event["LogicalResourceId"] 46 | if ( 47 | response_data 48 | and response_data != {} 49 | and response_data != [] 50 | and isinstance(response_data, dict) 51 | ): 52 | response_body["Data"] = response_data 53 | 54 | logger.debug("<<<<<<< Response body >>>>>>>>>>") 55 | logger.debug(response_body) 56 | json_response_body = json.dumps(response_body) 57 | 58 | headers = {"content-type": "", "content-length": str(len(json_response_body))} 59 | 60 | try: 61 | if response_url == "http://pre-signed-S3-url-for-response": 62 | logger.info( 63 | "CloudFormation returned status code:" " THIS IS A TEST OUTSIDE OF CLOUDFORMATION" 64 | ) 65 | else: 66 | response = requests.put(response_url, data=json_response_body, headers=headers) 67 | logger.info("CloudFormation returned status code: " + response.reason) 68 | except Exception as e: 69 | logger.error("send(..) failed executing requests.put(..): " + str(e)) 70 | raise 71 | 72 | 73 | def timeout(event, context, logger): 74 | """This function is executed just before lambda excecution time out 75 | to send out time out failure message. 76 | """ 77 | logger.error("Execution is about to time out, sending failure message") 78 | send( 79 | event, 80 | context, 81 | "FAILED", 82 | None, 83 | None, 84 | reason="Execution timed out", 85 | logger=logger, 86 | ) 87 | 88 | 89 | def cfn_handler(event, context, create, update, delete, logger, init_failed): 90 | """This handler function calls stack creation, update or deletion 91 | based on request type and also sends status and response data 92 | from any of the stack operations back to cloudformation, 93 | as applicable. 94 | """ 95 | logger.info( 96 | "Lambda RequestId: %s CloudFormation RequestId: %s" 97 | % (context.aws_request_id, event["RequestId"]) 98 | ) 99 | 100 | # Define an object to place any response information you would like to send 101 | # back to CloudFormation (these keys can then be used by Fn::GetAttr) 102 | response_data = {} 103 | 104 | # Define a physicalId for the resource, if the event is an update and the 105 | # returned phyiscalid changes, cloudformation will then issue a delete 106 | # against the old id 107 | physical_resource_id = None 108 | 109 | logger.debug("EVENT: " + str(event)) 110 | # handle init failures 111 | if init_failed: 112 | send( 113 | event, 114 | context, 115 | "FAILED", 116 | response_data, 117 | physical_resource_id, 118 | logger, 119 | reason="Initialization Failed", 120 | ) 121 | raise Exception("Initialization Failed") 122 | 123 | # Setup timer to catch timeouts 124 | t = threading.Timer( 125 | (context.get_remaining_time_in_millis() / 1000.00) - 0.5, 126 | timeout, 127 | args=[event, context, logger], 128 | ) 129 | t.start() 130 | 131 | try: 132 | # Execute custom resource handlers 133 | logger.info("Received a %s Request" % event["RequestType"]) 134 | if event["RequestType"] == "Create": 135 | physical_resource_id, response_data = create(event, context) 136 | elif event["RequestType"] == "Update": 137 | physical_resource_id, response_data = update(event, context) 138 | elif event["RequestType"] == "Delete": 139 | delete(event, context) 140 | 141 | # Send response back to CloudFormation 142 | logger.info("Completed successfully, sending response to cfn") 143 | send( 144 | event, 145 | context, 146 | "SUCCESS", 147 | response_data, 148 | physical_resource_id, 149 | logger=logger, 150 | ) 151 | 152 | # Catch any exceptions, log the stacktrace, send a failure back to 153 | # CloudFormation and then raise an exception 154 | except Exception as e: 155 | logger.error(e, exc_info=True) 156 | send( 157 | event, 158 | context, 159 | "FAILED", 160 | response_data, 161 | physical_resource_id, 162 | reason=e, 163 | logger=logger, 164 | ) 165 | finally: 166 | t.cancel() 167 | -------------------------------------------------------------------------------- /source/codebuild_scripts/state_machine_trigger.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance with the License. 6 | # A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is distributed # 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # 12 | # or implied. See the License for the specific language governing permissions# 13 | # and limitations under the License. # 14 | ############################################################################### 15 | 16 | import os 17 | import sys 18 | 19 | import cfct.manifest.manifest_parser as parse 20 | from cfct.exceptions import StackSetHasFailedInstances 21 | from cfct.manifest.sm_execution_manager import SMExecutionManager 22 | from cfct.utils.logger import Logger 23 | 24 | 25 | def main(): 26 | """ 27 | This function is triggered by CodePipeline stages (ServiceControlPolicy, 28 | ResourceControlPolicy and CloudFormationResource). 29 | Each stage triggers the following workflow: 30 | 1. Parse the manifest file. 31 | 2. Generate state machine input. 32 | 3. Start state machine execution. 33 | 4. Monitor state machine execution. 34 | 35 | SCP & RCP State Machine currently supports parallel deployments only 36 | Stack Set State Machine currently support sequential deployments only. 37 | 38 | :return: None 39 | """ 40 | try: 41 | if len(sys.argv) > 7: 42 | 43 | # set environment variables 44 | manifest_name = "manifest.yaml" 45 | file_path = sys.argv[3] 46 | os.environ["WAIT_TIME"] = sys.argv[2] 47 | os.environ["MANIFEST_FILE_PATH"] = file_path 48 | os.environ["SM_ARN"] = sys.argv[4] 49 | os.environ["STAGING_BUCKET"] = sys.argv[5] 50 | os.environ["TEMPLATE_KEY_PREFIX"] = "_custom_ct_templates_staging" 51 | os.environ["MANIFEST_FILE_NAME"] = manifest_name 52 | os.environ["MANIFEST_FOLDER"] = file_path[: -len(manifest_name)] 53 | stage_name = sys.argv[6] 54 | os.environ["STAGE_NAME"] = stage_name 55 | os.environ["KMS_KEY_ALIAS_NAME"] = sys.argv[7] 56 | os.environ[ 57 | "CAPABILITIES" 58 | ] = '["CAPABILITY_NAMED_IAM","CAPABILITY_AUTO_EXPAND"]' 59 | enforce_successful_stack_instances = None 60 | if len(sys.argv) > 8: 61 | enforce_successful_stack_instances = ( 62 | True if sys.argv[8] == "true" else False 63 | ) 64 | 65 | sm_input_list = [] 66 | if stage_name.upper() == "SCP": 67 | # get SCP state machine input list 68 | os.environ["EXECUTION_MODE"] = "parallel" 69 | sm_input_list = get_scp_inputs() 70 | logger.info("SCP sm_input_list:") 71 | logger.info(sm_input_list) 72 | elif stage_name.upper() == "RCP": 73 | # get RCP state machine input list 74 | os.environ["EXECUTION_MODE"] = "parallel" 75 | sm_input_list = get_rcp_inputs() 76 | logger.info("RCP sm_input_list:") 77 | logger.info(sm_input_list) 78 | elif stage_name.upper() == "STACKSET": 79 | os.environ["EXECUTION_MODE"] = "sequential" 80 | sm_input_list = get_stack_set_inputs() 81 | logger.info("STACKSET sm_input_list:") 82 | logger.info(sm_input_list) 83 | 84 | if sm_input_list: 85 | logger.info("=== Launching State Machine Execution ===") 86 | launch_state_machine_execution( 87 | sm_input_list, enforce_successful_stack_instances 88 | ) 89 | else: 90 | logger.info("State Machine input list is empty. No action " "required.") 91 | else: 92 | print("No arguments provided. ") 93 | print( 94 | "Example: state_machine_trigger.py " 95 | " " 96 | " " 97 | ) 98 | sys.exit(2) 99 | except Exception as e: 100 | logger.log_unhandled_exception(e) 101 | raise 102 | 103 | 104 | def get_scp_inputs() -> list: 105 | return parse.scp_manifest() 106 | 107 | 108 | def get_rcp_inputs() -> list: 109 | return parse.rcp_manifest() 110 | 111 | 112 | def get_stack_set_inputs() -> list: 113 | return parse.stack_set_manifest() 114 | 115 | 116 | def launch_state_machine_execution( 117 | sm_input_list, enforce_successful_stack_instances=False 118 | ): 119 | if isinstance(sm_input_list, list): 120 | manager = SMExecutionManager( 121 | logger, sm_input_list, enforce_successful_stack_instances 122 | ) 123 | try: 124 | status, failed_list = manager.launch_executions() 125 | except StackSetHasFailedInstances as error: 126 | logger.error(f"{error.stack_set_name} has following failed instances:") 127 | for instance in error.failed_stack_set_instances: 128 | message = { 129 | "StackID": instance["StackId"], 130 | "Account": instance["Account"], 131 | "Region": instance["Region"], 132 | "StatusReason": instance["StatusReason"], 133 | } 134 | logger.error(message) 135 | sys.exit(1) 136 | 137 | else: 138 | raise TypeError("State Machine Input List must be of list type") 139 | 140 | if status == "FAILED": 141 | logger.error( 142 | "\n********************************************************" 143 | "\nState Machine Execution(s) Failed. \nNavigate to the " 144 | "AWS Step Functions console \nand review the following " 145 | "State Machine Executions.\nARN List:\n" 146 | "{}\n********************************************************".format( 147 | failed_list 148 | ) 149 | ) 150 | sys.exit(1) 151 | 152 | 153 | if __name__ == "__main__": 154 | os.environ["LOG_LEVEL"] = sys.argv[1] 155 | logger = Logger(loglevel=os.getenv("LOG_LEVEL", "info")) 156 | main() 157 | -------------------------------------------------------------------------------- /source/src/cfct/aws/services/scp.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance # 6 | # with the License. A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is # 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # 12 | # KIND, express or implied. See the License for the specific language # 13 | # governing permissions and limitations under the License. # 14 | ############################################################################## 15 | 16 | # !/bin/python 17 | 18 | import time 19 | from typing import List 20 | 21 | from botocore.exceptions import ClientError 22 | 23 | from cfct.aws.utils.boto3_session import Boto3Session 24 | 25 | 26 | class ServiceControlPolicy(Boto3Session): 27 | def __init__(self, logger, **kwargs): 28 | self.logger = logger 29 | __service_name = "organizations" 30 | super().__init__(logger, __service_name, **kwargs) 31 | self.org_client = super().get_client() 32 | 33 | def list_policies(self, page_size=20): 34 | try: 35 | paginator = self.org_client.get_paginator("list_policies") 36 | response_iterator = paginator.paginate( 37 | Filter="SERVICE_CONTROL_POLICY", 38 | PaginationConfig={"PageSize": page_size}, 39 | ) 40 | return response_iterator 41 | except ClientError as e: 42 | self.logger.log_unhandled_exception(e) 43 | raise 44 | 45 | def list_policies_for_target(self, target_id, page_size=20): 46 | try: 47 | paginator = self.org_client.get_paginator("list_policies_for_target") 48 | response_iterator = paginator.paginate( 49 | TargetId=target_id, 50 | Filter="SERVICE_CONTROL_POLICY", 51 | PaginationConfig={"PageSize": page_size}, 52 | ) 53 | return response_iterator 54 | except ClientError as e: 55 | self.logger.log_unhandled_exception(e) 56 | raise 57 | 58 | def list_targets_for_policy(self, policy_id, page_size=20): 59 | try: 60 | paginator = self.org_client.get_paginator("list_targets_for_policy") 61 | response_iterator = paginator.paginate( 62 | PolicyId=policy_id, PaginationConfig={"PageSize": page_size} 63 | ) 64 | return response_iterator 65 | except ClientError as e: 66 | self.logger.log_unhandled_exception(e) 67 | raise 68 | 69 | def create_policy(self, name, description, content): 70 | try: 71 | response = self.org_client.create_policy( 72 | Content=content, 73 | Description=description, 74 | Name=name, 75 | Type="SERVICE_CONTROL_POLICY", 76 | ) 77 | return response 78 | except ClientError as e: 79 | self.logger.log_unhandled_exception(e) 80 | raise 81 | 82 | def update_policy(self, policy_id, name, description, content): 83 | try: 84 | response = self.org_client.update_policy( 85 | PolicyId=policy_id, Name=name, Description=description, Content=content 86 | ) 87 | return response 88 | except ClientError as e: 89 | self.logger.log_unhandled_exception(e) 90 | raise 91 | 92 | def delete_policy(self, policy_id): 93 | try: 94 | self.org_client.delete_policy(PolicyId=policy_id) 95 | except ClientError as e: 96 | self.logger.log_unhandled_exception(e) 97 | raise 98 | 99 | def attach_policy(self, policy_id, target_id): 100 | try: 101 | self.org_client.attach_policy(PolicyId=policy_id, TargetId=target_id) 102 | except ClientError as e: 103 | if e.response["Error"]["Code"] == "DuplicatePolicyAttachmentException": 104 | self.logger.exception( 105 | "Caught exception " 106 | "'DuplicatePolicyAttachmentException', " 107 | "taking no action..." 108 | ) 109 | return 110 | else: 111 | self.logger.log_unhandled_exception(e) 112 | raise 113 | 114 | def detach_policy(self, policy_id, target_id): 115 | try: 116 | self.org_client.detach_policy(PolicyId=policy_id, TargetId=target_id) 117 | except ClientError as e: 118 | if e.response["Error"]["Code"] == "PolicyNotAttachedException": 119 | self.logger.exception( 120 | "Caught exception " "'PolicyNotAttachedException'," " taking no action..." 121 | ) 122 | return 123 | else: 124 | self.logger.log_unhandled_exception(e) 125 | raise 126 | 127 | def enable_policy_type(self, root_id, wait_time_sec=5) -> None: 128 | max_retries = 3 129 | attempts = 0 130 | 131 | while attempts < max_retries: 132 | # https://awscli.amazonaws.com/v2/documentation/api/latest/reference/organizations/list-roots.html#examples 133 | # Before trying to enable, check what policy types are already enabled 134 | policy_type_metadata: List[dict] = self.org_client.list_roots()["Roots"][0].get( 135 | "PolicyTypes", [] 136 | ) 137 | for policy_metadata in policy_type_metadata: 138 | if policy_metadata["Type"] == "SERVICE_CONTROL_POLICY": 139 | if policy_metadata["Status"] == "ENABLED": 140 | self.logger.info("SCPs are already enabled, exiting without action") 141 | return 142 | 143 | # SCPs are not enabled - enable them 144 | try: 145 | self.org_client.enable_policy_type( 146 | RootId=root_id, PolicyType="SERVICE_CONTROL_POLICY" 147 | ) 148 | return 149 | except ClientError as e: 150 | if e.response["Error"]["Code"] == "PolicyTypeAlreadyEnabledException": 151 | self.logger.exception( 152 | "Caught PolicyTypeAlreadyEnabledException, taking no action..." 153 | ) 154 | return 155 | elif e.response["Error"]["Code"] == "ConcurrentModificationException": 156 | # Another instance of the CFCT SFN is enabling SPCs, sleep and retry 157 | attempts += 1 158 | time.sleep(wait_time_sec) 159 | continue 160 | else: 161 | self.logger.log_unhandled_exception(e) 162 | raise 163 | 164 | # Exceeded retries without finding SCPs enabled 165 | error_msg = f"Unable to enable SCPs in the organization after {max_retries} attempts" 166 | self.logger.log_unhandled_exception(error_msg) 167 | raise Exception(error_msg) 168 | -------------------------------------------------------------------------------- /source/src/cfct/aws/services/rcp.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance # 6 | # with the License. A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is # 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # 12 | # KIND, express or implied. See the License for the specific language # 13 | # governing permissions and limitations under the License. # 14 | ############################################################################## 15 | 16 | # !/bin/python 17 | 18 | import time 19 | from typing import List 20 | 21 | from botocore.exceptions import ClientError 22 | 23 | from cfct.aws.utils.boto3_session import Boto3Session 24 | 25 | 26 | class ResourceControlPolicy(Boto3Session): 27 | def __init__(self, logger, **kwargs): 28 | self.logger = logger 29 | __service_name = "organizations" 30 | super().__init__(logger, __service_name, **kwargs) 31 | self.org_client = super().get_client() 32 | 33 | def list_policies(self, page_size=20): 34 | try: 35 | paginator = self.org_client.get_paginator("list_policies") 36 | response_iterator = paginator.paginate( 37 | Filter="RESOURCE_CONTROL_POLICY", 38 | PaginationConfig={"PageSize": page_size}, 39 | ) 40 | return response_iterator 41 | except ClientError as e: 42 | self.logger.log_unhandled_exception(e) 43 | raise 44 | 45 | def list_policies_for_target(self, target_id, page_size=20): 46 | try: 47 | paginator = self.org_client.get_paginator("list_policies_for_target") 48 | response_iterator = paginator.paginate( 49 | TargetId=target_id, 50 | Filter="RESOURCE_CONTROL_POLICY", 51 | PaginationConfig={"PageSize": page_size}, 52 | ) 53 | return response_iterator 54 | except ClientError as e: 55 | self.logger.log_unhandled_exception(e) 56 | raise 57 | 58 | def list_targets_for_policy(self, policy_id, page_size=20): 59 | try: 60 | paginator = self.org_client.get_paginator("list_targets_for_policy") 61 | response_iterator = paginator.paginate( 62 | PolicyId=policy_id, PaginationConfig={"PageSize": page_size} 63 | ) 64 | return response_iterator 65 | except ClientError as e: 66 | self.logger.log_unhandled_exception(e) 67 | raise 68 | 69 | def create_policy(self, name, description, content): 70 | try: 71 | response = self.org_client.create_policy( 72 | Content=content, 73 | Description=description, 74 | Name=name, 75 | Type="RESOURCE_CONTROL_POLICY", 76 | ) 77 | return response 78 | except ClientError as e: 79 | self.logger.log_unhandled_exception(e) 80 | raise 81 | 82 | def update_policy(self, policy_id, name, description, content): 83 | try: 84 | response = self.org_client.update_policy( 85 | PolicyId=policy_id, Name=name, Description=description, Content=content 86 | ) 87 | return response 88 | except ClientError as e: 89 | self.logger.log_unhandled_exception(e) 90 | raise 91 | 92 | def delete_policy(self, policy_id): 93 | try: 94 | self.org_client.delete_policy(PolicyId=policy_id) 95 | except ClientError as e: 96 | self.logger.log_unhandled_exception(e) 97 | raise 98 | 99 | def attach_policy(self, policy_id, target_id): 100 | try: 101 | self.org_client.attach_policy(PolicyId=policy_id, TargetId=target_id) 102 | except ClientError as e: 103 | if e.response["Error"]["Code"] == "DuplicatePolicyAttachmentException": 104 | self.logger.exception( 105 | "Caught exception " 106 | "'DuplicatePolicyAttachmentException', " 107 | "taking no action..." 108 | ) 109 | return 110 | else: 111 | self.logger.log_unhandled_exception(e) 112 | raise 113 | 114 | def detach_policy(self, policy_id, target_id): 115 | try: 116 | self.org_client.detach_policy(PolicyId=policy_id, TargetId=target_id) 117 | except ClientError as e: 118 | if e.response["Error"]["Code"] == "PolicyNotAttachedException": 119 | self.logger.exception( 120 | "Caught exception " "'PolicyNotAttachedException'," " taking no action..." 121 | ) 122 | return 123 | else: 124 | self.logger.log_unhandled_exception(e) 125 | raise 126 | 127 | def enable_policy_type(self, root_id, wait_time_sec=5) -> None: 128 | max_retries = 3 129 | attempts = 0 130 | 131 | while attempts < max_retries: 132 | # https://awscli.amazonaws.com/v2/documentation/api/latest/reference/organizations/list-roots.html#examples 133 | # Before trying to enable, check what policy types are already enabled 134 | policy_type_metadata: List[dict] = self.org_client.list_roots()["Roots"][0].get( 135 | "PolicyTypes", [] 136 | ) 137 | for policy_metadata in policy_type_metadata: 138 | if policy_metadata["Type"] == "RESOURCE_CONTROL_POLICY": 139 | if policy_metadata["Status"] == "ENABLED": 140 | self.logger.info("RCPs are already enabled, exiting without action") 141 | return 142 | 143 | # RCPs are not enabled - enable them 144 | try: 145 | self.org_client.enable_policy_type( 146 | RootId=root_id, PolicyType="RESOURCE_CONTROL_POLICY" 147 | ) 148 | return 149 | except ClientError as e: 150 | if e.response["Error"]["Code"] == "PolicyTypeAlreadyEnabledException": 151 | self.logger.exception( 152 | "Caught PolicyTypeAlreadyEnabledException, taking no action..." 153 | ) 154 | return 155 | elif e.response["Error"]["Code"] == "ConcurrentModificationException": 156 | # Another instance of the CFCT SFN is enabling RCPs, sleep and retry 157 | attempts += 1 158 | time.sleep(wait_time_sec) 159 | continue 160 | else: 161 | self.logger.log_unhandled_exception(e) 162 | raise 163 | 164 | # Exceeded retries without finding RCPs enabled 165 | error_msg = f"Unable to enable RCPs in the organization after {max_retries} attempts" 166 | self.logger.log_unhandled_exception(error_msg) 167 | raise Exception(error_msg) 168 | -------------------------------------------------------------------------------- /source/codebuild_scripts/run-validation.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ARTIFACT_BUCKET=$1 4 | CURRENT_PATH=$(pwd) 5 | SUCCESS=0 6 | FAILED=1 7 | EXIT_STATUS=$SUCCESS 8 | VERSION_1='2020-01-01' 9 | VERSION_2='2021-03-15' 10 | 11 | set_failed_exit_status() { 12 | echo "^^^ Caught an error: Setting exit status flag to $FAILED ^^^" 13 | EXIT_STATUS=$FAILED 14 | } 15 | 16 | exit_shell_script() { 17 | echo "Exiting script with status: $EXIT_STATUS" 18 | if [[ $EXIT_STATUS == 0 ]] 19 | then 20 | echo "INFO: Validation test(s) completed." 21 | exit $SUCCESS 22 | else 23 | echo "ERROR: One or more validation test(s) failed." 24 | exit $FAILED 25 | fi 26 | } 27 | 28 | validate_template_file() { 29 | echo "Running aws cloudformation validate-template on $template_url" 30 | # TODO: Verify if this works if resource file is homed in opt-in region, and CT mgmt is homed in commercial region 31 | aws cloudformation validate-template --template-url "$template_url" --region "$AWS_REGION" 32 | if [ $? -ne 0 ] 33 | then 34 | echo "ERROR: CloudFormation template failed validation - $template_url" 35 | set_failed_exit_status 36 | fi 37 | 38 | echo "Print file encoding: $tmp_file " 39 | file -i "$tmp_file" 40 | echo "Running cfn_nag_scan on $tmp_file" 41 | cfn_nag_scan --input-path "$tmp_file" 42 | if [ $? -ne 0 ] 43 | then 44 | echo "ERROR: CFN Nag failed validation - $file_name" 45 | set_failed_exit_status 46 | fi 47 | } 48 | 49 | validate_parameter_file() { 50 | echo "Running json validation on $tmp_file" 51 | python -m json.tool < "$tmp_file" 52 | if [ $? -ne 0 ] 53 | then 54 | echo "ERROR: CloudFormation parameter file failed validation - $file_name" 55 | set_failed_exit_status 56 | else 57 | echo "NO ISSUE WITH JSON" 58 | fi 59 | } 60 | 61 | echo "Printing artifact bucket name: $ARTIFACT_BUCKET" 62 | 63 | python3 -c 'import yaml,sys;yaml.safe_load(sys.stdin)' < manifest.yaml 64 | if [ $? -ne 0 ] 65 | then 66 | echo "ERROR: Manifest file is not valid YAML" 67 | set_failed_exit_status 68 | fi 69 | 70 | echo "Manifest file is a valid YAML" 71 | 72 | # Validate manifest schema 73 | MANIFEST_VERSION=$(/usr/bin/yq eval '.version' manifest.yaml) 74 | echo "Found current manifest version: $MANIFEST_VERSION" 75 | if [[ "$MANIFEST_VERSION" == "$VERSION_1" ]] 76 | then 77 | echo "WARNING: You are using older version $VERSION_1 of the schema. We recommend you to update your manifest file schema. See Developer Guide for details." 78 | pykwalify -d manifest.yaml -s cfct/validation/manifest.schema.yaml -e cfct/validation/custom_validation.py 79 | if [ $? -ne 0 ] 80 | then 81 | echo "ERROR: Manifest file failed V1 schema validation" 82 | set_failed_exit_status 83 | fi 84 | elif [[ "$MANIFEST_VERSION" == "$VERSION_2" ]] 85 | then 86 | echo "Validating manifest with schema version: $VERSION_2" 87 | pykwalify -d manifest.yaml -s cfct/validation/manifest-v2.schema.yaml -e cfct/validation/custom_validation.py 88 | if [ $? -ne 0 ] 89 | then 90 | echo "ERROR: Manifest file failed V2 schema validation" 91 | set_failed_exit_status 92 | fi 93 | else 94 | echo "ERROR: Invalid manifest schema version." 95 | set_failed_exit_status 96 | fi 97 | 98 | echo "Manifest file validated against the schema successfully" 99 | 100 | # check each file in the manifest to make sure it exists 101 | check_files=$(grep '_file:' < manifest.yaml | grep -v '^ *#' | tr -s ' ' | tr -d '\r' | cut -d ' ' -f 3) 102 | for file_name in $check_files ; do 103 | # run aws cloudformation validate-template, cfn_nag_scan and json validate on all **remote** templates / parameters files 104 | if [[ $file_name == s3* ]]; then 105 | echo "S3 URL path found: $file_name" 106 | tmp_file=$(mktemp) 107 | echo "Downloading $file_name to $tmp_file" 108 | aws s3 cp "$file_name" "$tmp_file" --only-show-errors 109 | if [[ $? == 0 ]]; then 110 | echo "S3 URL exists: $file_name" 111 | if [[ $file_name == *template ]]; then 112 | # Reformat the S3 URL from s3://bucket/key to https://bucket.s3.amazonaws.com/key 113 | IFS='/' read -ra TOKENS <<< "$file_name" 114 | BUCKET=${TOKENS[2]} 115 | KEY="" 116 | for i in "${!TOKENS[@]}"; do 117 | if [[ i -gt 2 ]]; then 118 | KEY="$KEY/${TOKENS[$i]}" 119 | fi 120 | done 121 | template_url="https://$BUCKET.s3.amazonaws.com/${KEY:1}" 122 | validate_template_file 123 | elif [[ $file_name == *json ]]; then 124 | validate_parameter_file 125 | fi 126 | else 127 | echo "ERROR: S3 URL does not exist: $file_name" 128 | set_failed_exit_status 129 | fi 130 | # check if the resource file path is starting with http 131 | elif [[ $file_name == http* ]]; then 132 | echo "HTTPS URL path exists: $file_name" 133 | tmp_file=$(mktemp) 134 | echo "Downloading $file_name" 135 | curl --fail -o "$tmp_file" "$file_name" 136 | if [[ $? == 0 ]]; then 137 | echo "HTTPS URL exists: $file_name" 138 | if [[ $file_name == *template ]]; then 139 | template_url=$file_name 140 | validate_template_file 141 | elif [[ $file_name == *json ]]; then 142 | validate_parameter_file 143 | fi 144 | else 145 | echo "ERROR: HTTPS URL does not exist: $file_name" 146 | set_failed_exit_status 147 | fi 148 | elif [ -f "$CURRENT_PATH"/"$file_name" ]; then 149 | echo "File $file_name exists" 150 | else 151 | echo "ERROR: File $file_name does not exist" 152 | set_failed_exit_status 153 | fi 154 | done 155 | 156 | # run aws cloudformation validate-template and cfn_nag_scan on all **local** templates 157 | cd templates 158 | TEMPLATES_DIR=$(pwd) 159 | export TEMPLATES_DIR 160 | echo "Changing path to template directory: $TEMPLATES_DIR/" 161 | for template_name in $(find . -type f | grep -E '.template$|.yaml$|.yml$|.json$' | sed 's/^.\///') ; do 162 | echo "Uploading template: $template_name to s3" 163 | aws s3 cp "$TEMPLATES_DIR"/"$template_name" s3://"$ARTIFACT_BUCKET"/validate/templates/"$template_name" 164 | if [ $? -ne 0 ] 165 | then 166 | echo "ERROR: Uploading template: $template_name to S3 failed" 167 | set_failed_exit_status 168 | fi 169 | done 170 | 171 | #V110556787: Intermittent CodeBuild stage failure due to S3 error: Access Denied 172 | sleep_time=30 173 | echo "Sleeping for $sleep_time seconds" 174 | sleep $sleep_time 175 | 176 | for template_name in $(find . -type f | grep -E '.template$|.yaml$|.yml$|.json$' | sed 's/^.\///') ; do 177 | echo "Running aws cloudformation validate-template on $template_name" 178 | aws cloudformation validate-template --template-url https://s3."$AWS_REGION".amazonaws.com/"$ARTIFACT_BUCKET"/validate/templates/"$template_name" --region "$AWS_REGION" 179 | if [ $? -ne 0 ] 180 | then 181 | echo "ERROR: CloudFormation template failed validation - $template_name" 182 | set_failed_exit_status 183 | fi 184 | # delete objects in bucket 185 | aws s3 rm s3://"$ARTIFACT_BUCKET"/validate/templates/"$template_name" 186 | echo "Print file encoding: $template_name" 187 | file -i "$TEMPLATES_DIR"/"$template_name" 188 | echo "Running cfn_nag_scan on $template_name" 189 | cfn_nag_scan --input-path "$TEMPLATES_DIR"/"$template_name" 190 | if [ $? -ne 0 ] 191 | then 192 | echo "ERROR: CFN Nag failed validation - $template_name" 193 | set_failed_exit_status 194 | fi 195 | done 196 | 197 | # run json validation on all the **local** parameter files 198 | 199 | cd ../parameters 200 | echo "Changing path to parameters directory: $(pwd)" 201 | for parameter_file_name in $(find . -type f | grep '.json' | grep -v '.j2' | sed 's/^.\///') ; do 202 | echo "Running json validation on $parameter_file_name" 203 | python -m json.tool < "$parameter_file_name" 204 | if [ $? -ne 0 ] 205 | then 206 | echo "ERROR: CloudFormation parameter file failed validation - $parameter_file_name" 207 | set_failed_exit_status 208 | fi 209 | done 210 | cd .. 211 | 212 | # calling return_code function 213 | exit_shell_script -------------------------------------------------------------------------------- /deployment/build-s3-dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This assumes all of the OS-level configuration has been completed and git repo has already been cloned 3 | # 4 | # Usage: This script should be executed from the package root directory 5 | # ./deployment/build-s3-dist.sh source-bucket-base-name template-bucket-base-name trademarked-solution-name version-code enable-opt-in-region-support 6 | # 7 | # Parameters: 8 | # - source-bucket-base-name: Name for the S3 bucket location where the template will source the Lambda 9 | # code from. The template will append '-[region_name]' to this bucket name. 10 | # For example: ./build-s3-dist.sh solutions template-bucket my-solution v1.0.0 11 | # The template will then expect the source code to be located in the solutions-[region_name] bucket 12 | # 13 | # - template-bucket-base-name: Name for the S3 bucket location where the template will be located 14 | # 15 | # - trademarked-solution-name: name of the solution for consistency 16 | # 17 | # - version-code: version of the package 18 | # 19 | # - enable-opt-in-region-support: (Optional Boolean) Flag to enable opt-in region support. Pass `true` to set this argument. 20 | 21 | 22 | # Hard exit on failure 23 | set -e 24 | 25 | # Check to see if input has been provided: 26 | if [ $# -lt 4 ]; then 27 | echo "Please provide the base source bucket name, template-bucket, trademark approved solution name, version and (Optional) enable-opt-in-region-support flag" 28 | echo "For example: ./deployment/build-s3-dist.sh solutions template-bucket trademarked-solution-name v1.0.0 true" 29 | exit 1 30 | fi 31 | 32 | # declare variables 33 | template_dir="$PWD" 34 | template_dist_dir="$template_dir/deployment/global-s3-assets" 35 | build_dist_dir="$template_dir/deployment/regional-s3-assets" 36 | CODE_BUCKET_NAME=$1 37 | TEMPLATE_BUCKET_NAME=$2 38 | SOLUTION_NAME=$3 39 | VERSION_NUMBER=$4 40 | ENABLE_OPT_IN_REGION_SUPPORT=$5 41 | 42 | # Handle opt-in region builds in backwards compatible way, 43 | # Requires customer to set IS_OPT_IN_REGION parameter 44 | SCRIPT_BUCKET_NAME=$(echo "${TEMPLATE_BUCKET_NAME}") 45 | DISTRIBUTION_BUCKET_NAME=$(echo "${TEMPLATE_BUCKET_NAME}") 46 | if [[ "${ENABLE_OPT_IN_REGION_SUPPORT}" = "true" ]]; then 47 | echo "Building with opt-in region support" 48 | SCRIPT_BUCKET_NAME+='-${AWS_REGION}' # Regionalized Buildspec 49 | DISTRIBUTION_BUCKET_NAME+='-${AWS::Region}' # Regionalized CFN Template 50 | fi 51 | 52 | echo "------------------------------------------------------------------------------" 53 | echo "[Init] Clean old dist and recreate directories" 54 | echo "------------------------------------------------------------------------------" 55 | echo "rm -rf $template_dist_dir" 56 | rm -rf "$template_dist_dir" 57 | echo "mkdir -p $template_dist_dir" 58 | mkdir -p "$template_dist_dir" 59 | echo "rm -rf $build_dist_dir" 60 | rm -rf "$build_dist_dir" 61 | echo "mkdir -p $build_dist_dir" 62 | mkdir -p "$build_dist_dir" 63 | 64 | # Upgrade setuptools, wheel 65 | # Install cython<3.0.0 and pyyaml 5.4.1 with build isolation 66 | # Ref: https://github.com/yaml/pyyaml/issues/724 67 | pip3 install --upgrade setuptools wheel 68 | pip3 install 'cython<3.0.0' && pip3 install --no-build-isolation pyyaml==5.4.1 69 | 70 | # Create zip file for AWS Lambda functions 71 | echo -e "\n Creating all lambda functions for Custom Control Tower Solution" 72 | python3 deployment/lambda_build.py state_machine_lambda deployment_lambda build_scripts lifecycle_event_handler state_machine_trigger 73 | 74 | # Move custom-control-tower-initiation.template to global-s3-assets 75 | echo "cp -f deployment/custom-control-tower-initiation.template $template_dist_dir" 76 | cp -f deployment/custom-control-tower-initiation.template "$template_dist_dir" 77 | 78 | #COPY deployment/add-on to $build_dist_dir/add-on 79 | mkdir "$template_dist_dir"/add-on/ 80 | cp -f -R deployment/add-on/. "$template_dist_dir"/add-on 81 | 82 | #COPY custom_control_tower_configuration to global-s3-assets 83 | #Please check to see if this is the correct location or template_dist_dir 84 | cp -f -R deployment/custom_control_tower_configuration "$build_dist_dir"/custom_control_tower_configuration/ 85 | 86 | echo -e "\n Updating code source bucket in the template with $CODE_BUCKET_NAME" 87 | replace="s/%DIST_BUCKET_NAME%/$CODE_BUCKET_NAME/g" 88 | echo "sed -i -e $replace $template_dist_dir/custom-control-tower-initiation.template" 89 | sed -i -e "$replace" "$template_dist_dir"/custom-control-tower-initiation.template 90 | 91 | echo -e "\n Updating template bucket in the template with $DISTRIBUTION_BUCKET_NAME" 92 | replace="s/%TEMPLATE_BUCKET_NAME%/$DISTRIBUTION_BUCKET_NAME/g" 93 | echo "sed -i -e $replace $template_dist_dir/custom-control-tower-initiation.template" 94 | sed -i -e "$replace" "$template_dist_dir"/custom-control-tower-initiation.template 95 | 96 | echo -e "\n Updating template bucket in the template with $SCRIPT_BUCKET_NAME" 97 | replace="s/%SCRIPT_BUCKET_NAME%/$SCRIPT_BUCKET_NAME/g" 98 | echo "sed -i -e $replace $template_dist_dir/custom-control-tower-initiation.template" 99 | sed -i -e "$replace" "$template_dist_dir"/custom-control-tower-initiation.template 100 | 101 | # Replace solution name with real value 102 | echo -e "\n Updating solution name in the template with $SOLUTION_NAME" 103 | replace="s/%SOLUTION_NAME%/$SOLUTION_NAME/g" 104 | echo "sed -i -e $replace $template_dist_dir/custom-control-tower-initiation.template" 105 | sed -i -e "$replace" "$template_dist_dir"/custom-control-tower-initiation.template 106 | 107 | echo -e "\n Updating version number in the template with $VERSION_NUMBER" 108 | replace="s/%VERSION%/$VERSION_NUMBER/g" 109 | echo "sed -i -e $replace $template_dist_dir/custom-control-tower-initiation.template" 110 | sed -i -e "$replace" "$template_dist_dir"/custom-control-tower-initiation.template 111 | 112 | # Create configuration zip file 113 | echo -e "\n Creating zip file with Custom Control Tower configuration" 114 | cd "$build_dist_dir"/custom_control_tower_configuration/ 115 | zip -Xr "$build_dist_dir"/custom-control-tower-configuration.zip ./* 116 | 117 | # build regional config zip file 118 | echo -e "\n*** Build regional config zip file" 119 | # Support all regions in https://docs.aws.amazon.com/controltower/latest/userguide/region-how.html + GovCloud regions 120 | declare -a region_list=( 121 | "af-south-1" 122 | "ap-east-1" 123 | "ap-northeast-1" 124 | "ap-northeast-2" 125 | "ap-northeast-3" 126 | "ap-south-1" 127 | "ap-southeast-1" 128 | "ap-southeast-2" 129 | "ap-southeast-3" 130 | "ca-central-1" 131 | "eu-central-1" 132 | "eu-north-1" 133 | "eu-south-1" 134 | "eu-west-1" 135 | "eu-west-2" 136 | "eu-west-3" 137 | "me-south-1" 138 | "sa-east-1" 139 | "us-east-1" 140 | "us-east-2" 141 | "us-gov-east-1" 142 | "us-gov-west-1" 143 | "us-west-1" 144 | "us-west-2" 145 | "il-central-1" 146 | "me-central-1" 147 | "ap-south-2" 148 | "ap-southeast-3" 149 | ) 150 | for region in "${region_list[@]}" 151 | do 152 | echo -e "\n Building config zip for $region region" 153 | echo -e " Updating region name in the manifest to: $region \n" 154 | replace="s/{{ region }}/$region/g" 155 | cp ./manifest.yaml.j2 ./manifest.yaml 156 | echo "sed -i -e $replace ./manifest.yaml" 157 | sed -i -e "$replace" ./manifest.yaml 158 | echo -e "\n Zipping configuration..." 159 | zip -Xr "$build_dist_dir"/custom-control-tower-configuration-"$region".zip ./manifest.yaml ./example-configuration/* 160 | done 161 | cd - 162 | #Copy Lambda Zip Files to the Global S3 Assets 163 | echo -e "\n Copying lambda zip files to Global S3 Assets" 164 | cp "$build_dist_dir"/*.zip "$template_dist_dir"/ 165 | 166 | -------------------------------------------------------------------------------- /source/src/cfct/manifest/sm_input_builder.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance with the License. 6 | # A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is distributed # 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # 12 | # or implied. See the License for the specific language governing permissions# 13 | # and limitations under the License. # 14 | ############################################################################### 15 | 16 | import os 17 | from abc import ABC, abstractmethod 18 | 19 | 20 | class StateMachineInput(ABC): 21 | """ 22 | The State Machine input class that declares a set of methods that returns 23 | abstract input. 24 | """ 25 | 26 | @abstractmethod 27 | def input_map(self): 28 | pass 29 | 30 | 31 | class InputBuilder(StateMachineInput): 32 | """ 33 | This class wraps the specific state machine input with 34 | common required keys. 35 | 36 | """ 37 | 38 | def __init__(self, resource_properties, request_type="Create", skip_stack_set="no"): 39 | self._request_type = request_type 40 | self._resource_properties = resource_properties 41 | self._skip_stack_set = skip_stack_set 42 | 43 | def input_map(self) -> dict: 44 | input_map = { 45 | "RequestType": self._request_type, 46 | "ResourceProperties": self._resource_properties, 47 | } 48 | if os.environ["STAGE_NAME"].upper() == "STACKSET": 49 | input_map.update({"SkipUpdateStackSet": self._skip_stack_set}) 50 | return input_map 51 | 52 | 53 | class SCPResourceProperties: 54 | """ 55 | This class helps create and return input needed to execute SCP state 56 | machine. This also defines the required keys to execute the state machine. 57 | 58 | Example: 59 | 60 | resource_properties = SCPResourceProperties(name, description, policy_url, 61 | policy_list, account_id, 62 | operation, ou_list, 63 | delimiter, scp_parameters) 64 | scp_input = InputBuilder(resource_properties.get_scp_input_map()) 65 | sm_input = scp_input.input_map() 66 | 67 | """ 68 | 69 | def __init__( 70 | self, 71 | policy_name, 72 | policy_description, 73 | policy_url, 74 | ou_list, 75 | policy_list=None, 76 | account_id="", 77 | operation="", 78 | ou_name_delimiter=":", 79 | ): 80 | self._policy_name = policy_name 81 | self._policy_description = policy_description 82 | self._policy_url = policy_url 83 | self._policy_list = [] if policy_list is None else policy_list 84 | self._account_id = account_id 85 | self._operation = operation 86 | self._ou_list = ou_list 87 | self._ou_name_delimiter = ou_name_delimiter 88 | 89 | def get_scp_input_map(self): 90 | return { 91 | "PolicyDocument": self._get_policy_document(), 92 | "AccountId": self._account_id, 93 | "PolicyList": self._policy_list, 94 | "Operation": self._operation, 95 | "OUList": self._ou_list, 96 | "OUNameDelimiter": self._ou_name_delimiter, 97 | } 98 | 99 | def _get_policy_document(self): 100 | return { 101 | "Name": self._policy_name, 102 | "Description": self._policy_description, 103 | "PolicyURL": self._policy_url, 104 | } 105 | 106 | 107 | class RCPResourceProperties: 108 | """ 109 | This class helps create and return input needed to execute RCP state 110 | machine. This also defines the required keys to execute the state machine. 111 | 112 | Example: 113 | 114 | resource_properties = RCPResourceProperties(name, description, policy_url, 115 | policy_list, account_id, 116 | operation, ou_list, 117 | delimiter, rcp_parameters) 118 | rcp_input = InputBuilder(resource_properties.get_rcp_input_map()) 119 | sm_input = rcp_input.input_map() 120 | 121 | """ 122 | 123 | def __init__( 124 | self, 125 | policy_name, 126 | policy_description, 127 | policy_url, 128 | ou_list, 129 | policy_list=None, 130 | account_id="", 131 | operation="", 132 | ou_name_delimiter=":", 133 | ): 134 | self._policy_name = policy_name 135 | self._policy_description = policy_description 136 | self._policy_url = policy_url 137 | self._policy_list = [] if policy_list is None else policy_list 138 | self._account_id = account_id 139 | self._operation = operation 140 | self._ou_list = ou_list 141 | self._ou_name_delimiter = ou_name_delimiter 142 | 143 | def get_rcp_input_map(self): 144 | return { 145 | "PolicyDocument": self._get_policy_document(), 146 | "AccountId": self._account_id, 147 | "PolicyList": self._policy_list, 148 | "Operation": self._operation, 149 | "OUList": self._ou_list, 150 | "OUNameDelimiter": self._ou_name_delimiter, 151 | } 152 | 153 | def _get_policy_document(self): 154 | return { 155 | "Name": self._policy_name, 156 | "Description": self._policy_description, 157 | "PolicyURL": self._policy_url, 158 | } 159 | 160 | 161 | class StackSetResourceProperties: 162 | """ 163 | This class helps create and return input needed to execute Stack Set 164 | state machine. This also defines the required keys to execute the state 165 | machine. 166 | 167 | Example: 168 | 169 | resource_properties = StackSetResourceProperties(stack_set_name, 170 | template_url, 171 | parameters, 172 | capabilities, 173 | account_list, 174 | region_list, 175 | ssm_parameters) 176 | ss_input = InputBuilder(resource_properties.get_stack_set_input_map()) 177 | sm_input = ss_input.input_map() 178 | """ 179 | 180 | def __init__( 181 | self, 182 | stack_set_name, 183 | template_url, 184 | parameters, 185 | capabilities, 186 | account_list, 187 | region_list, 188 | ssm_parameters, 189 | ): 190 | self._stack_set_name = stack_set_name 191 | self._template_url = template_url 192 | self._parameters = parameters 193 | self._capabilities = capabilities 194 | self._account_list = account_list 195 | self._region_list = region_list 196 | self._ssm_parameters = ssm_parameters 197 | 198 | def get_stack_set_input_map(self): 199 | return { 200 | "StackSetName": self._stack_set_name, 201 | "TemplateURL": self._template_url, 202 | "Capabilities": self._capabilities, 203 | "Parameters": self._get_cfn_parameters(), 204 | "AccountList": self._get_account_list(), 205 | "RegionList": self._get_region_list(), 206 | "SSMParameters": self._get_ssm_parameters(), 207 | } 208 | 209 | def _get_cfn_parameters(self): 210 | if isinstance(self._parameters, dict): 211 | return self._parameters 212 | else: 213 | raise TypeError("Parameters must be of dict type") 214 | 215 | def _get_account_list(self): 216 | if isinstance(self._account_list, list): 217 | return self._account_list 218 | else: 219 | raise TypeError("Account list value must be of list type") 220 | 221 | def _get_ssm_parameters(self): 222 | if isinstance(self._ssm_parameters, dict): 223 | return self._ssm_parameters 224 | else: 225 | raise TypeError("SSM Parameter value must be of dict type") 226 | 227 | def _get_region_list(self): 228 | if isinstance(self._region_list, list): 229 | return self._region_list 230 | else: 231 | raise TypeError("Region list value must be of list type") 232 | -------------------------------------------------------------------------------- /source/codebuild_scripts/merge_baseline_template_parameter.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). # 5 | # You may not use this file except in compliance with the License. 6 | # A copy of the License is located at # 7 | # # 8 | # http://www.apache.org/licenses/LICENSE-2.0 # 9 | # # 10 | # or in the "license" file accompanying this file. This file is distributed # 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express # 12 | # or implied. See the License for the specific language governing permissions# 13 | # and limitations under the License. # 14 | ############################################################################### 15 | 16 | import json 17 | import os 18 | import subprocess 19 | import sys 20 | 21 | from cfct.utils.logger import Logger 22 | from cfct.utils.path_utils import is_safe_path 23 | 24 | def _read_file(file): 25 | if os.path.isfile(file): 26 | logger.info("File - {} exists".format(file)) 27 | logger.info("Reading from {}".format(file)) 28 | base_directory = os.getcwd() 29 | if is_safe_path(base_directory, file): 30 | abs_file = os.path.abspath(file) 31 | with open(abs_file) as f: 32 | return json.load(f) 33 | else: 34 | logger.error(f"Invalid file path detected for {file}") 35 | sys.exit(1) 36 | else: 37 | logger.error("File: {} not found.".format(file)) 38 | sys.exit(1) 39 | 40 | 41 | def _write_file(data, file, mode="w"): 42 | logger.info("Writing to {}".format(file)) 43 | with open(file, mode) as outfile: 44 | json.dump(data, outfile, indent=2) 45 | outfile.close() 46 | 47 | 48 | def _flip_to_json(yaml_file): 49 | json_file = os.path.join(yaml_file + json_extension) 50 | logger.info("Flipping YAML > {} to JSON > {}".format(yaml_file, json_file)) 51 | subprocess.run(["cfn-flip", "-j", yaml_file, json_file]) 52 | return _read_file(json_file) 53 | 54 | 55 | def _flip_to_yaml(json_file): 56 | yaml_file = json_file[: -len(updated_flag)] 57 | logger.info("Flipping JSON > {} to YAML > {}".format(json_file, yaml_file)) 58 | # final stage - convert json avm template to yaml format 59 | subprocess.run(["cfn-flip", "-y", json_file, yaml_file]) 60 | 61 | 62 | def file_matcher(master_data, add_on_data, master_key="master", add_on_key="add_on"): 63 | for item in master_data.get(master_key): 64 | logger.info("Iterating Master AVM File List") 65 | for key, value in item.items(): 66 | logger.info("{}: {}".format(key, value)) 67 | master_file_name = value.split("/")[-1] 68 | logger.info("master_value: {}".format(value.split("/")[-1])) 69 | for i in add_on_data.get(add_on_key): 70 | logger.info("Iterating Add-On AVM File List for comparision.") 71 | for k, v in i.items(): 72 | logger.info("{}: {}".format(k, v)) 73 | add_on_file_name = v.split("/")[-1] 74 | logger.info("add_on_value: {}".format(v.split("/")[-1])) 75 | if master_file_name == add_on_file_name: 76 | logger.info("Matching file names found - " "full path below") 77 | logger.info("File in master list: {}".format(value)) 78 | logger.info("File in add-on list: {}".format(v)) 79 | # Pass value and v to merge functions 80 | if master_file_name.lower().endswith(".template"): 81 | logger.info("Processing template file") 82 | # merge master avm template with add_on template 83 | # send json data 84 | final_json = update_template( 85 | _flip_to_json(value), _flip_to_json(v) 86 | ) 87 | # write the json data to json file 88 | updated_json_file_name = os.path.join(value + updated_flag) 89 | _write_file(final_json, updated_json_file_name) 90 | _flip_to_yaml(updated_json_file_name) 91 | if master_file_name.lower().endswith(".json"): 92 | logger.info("Processing parameter file") 93 | update_parameters(value, v) 94 | 95 | 96 | def update_level_1_dict(master, add_on, level_1_key): 97 | for key1, value1 in add_on.items(): 98 | if isinstance(value1, dict) and key1 == level_1_key: 99 | # Check if primary key matches 100 | logger.info("Level 1 keys matched ADDON {} == {}".format(key1, level_1_key)) 101 | # Iterate through the 2nd level dicts in the value 102 | for key2, value2 in value1.items(): 103 | logger.info("----------------------------------") 104 | # Match k with master dict keys - add if not present 105 | for k1, v1 in master.items(): 106 | if isinstance(v1, dict) and k1 == level_1_key: 107 | logger.info( 108 | "Level 1 keys matched MASTER " 109 | "{} == {}".format(k1, level_1_key) 110 | ) 111 | flag = False 112 | # Iterate through the 2nd level dicts in 113 | # the value 114 | for k2, v2 in v1.items(): 115 | logger.info("Is {} == {}".format(key2, k2)) 116 | if key2 == k2: 117 | logger.info("Found matching keys") 118 | flag = False 119 | logger.info("Setting flag value to {}".format(flag)) 120 | break 121 | else: 122 | flag = True 123 | logger.info( 124 | "Add-on key not found in existing" 125 | " dict, setting flag value to {}" 126 | " to update dict.".format(flag) 127 | ) 128 | if flag: 129 | logger.info("Adding key {}".format(key2)) 130 | d2 = {key2: value2} 131 | v1.update(d2) 132 | logger.debug(master) 133 | return master 134 | 135 | 136 | def _reload(add_on, original): 137 | # return original manifest if updated manifest is None 138 | update = add_on if add_on is not None else original 139 | return update 140 | 141 | 142 | def _keys(json_data): 143 | # dynamically build key list to process the add-on avm template 144 | keys = list() 145 | for k, v in json_data.items(): 146 | keys.append(k) 147 | return keys 148 | 149 | 150 | def update_template(master, add_on): 151 | logger.info("Merging template files.") 152 | # get keys for iteration 153 | keys = _keys(add_on) 154 | for key in keys: 155 | # Iterate through the keys in add_on baseline template 156 | updated_temp = update_level_1_dict(master, add_on, key) 157 | master = _reload(updated_temp, master) 158 | return master 159 | 160 | 161 | def update_parameters(master, add_on, decision_key="ParameterKey"): 162 | logger.info("Merging parameter files.") 163 | m_list = _read_file(master) 164 | add_list = _read_file(add_on) 165 | if add_list: 166 | for item in add_list: 167 | logger.info(item.get(decision_key)) 168 | if m_list: 169 | flag = False 170 | for i in m_list: 171 | logger.info(i.get(decision_key)) 172 | if item.get(decision_key) == i.get(decision_key): 173 | logger.info( 174 | "Keys: '{}' matched, skipping".format( 175 | item.get(decision_key) 176 | ) 177 | ) 178 | flag = False 179 | logger.info( 180 | "Setting flag value to {} and stopping" 181 | " the loop.".format(flag) 182 | ) 183 | break 184 | else: 185 | flag = True 186 | logger.info("Setting flag value to {}".format(flag)) 187 | if flag and item not in m_list: 188 | # avoid appending same parameter in the parameter list 189 | m_list.append(item) 190 | logger.info("Printing updated parameter file.") 191 | logger.info(m_list) 192 | return m_list 193 | 194 | 195 | if __name__ == "__main__": 196 | if len(sys.argv) > 3: 197 | log_level = sys.argv[1] 198 | master_baseline_file = sys.argv[2] 199 | add_on_baseline_file = sys.argv[3] 200 | 201 | json_extension = ".json" 202 | updated_flag = ".update" 203 | logger = Logger(loglevel=log_level) 204 | 205 | master_list = _read_file(master_baseline_file) 206 | add_on_list = _read_file(add_on_baseline_file) 207 | file_matcher(master_list, add_on_list) 208 | 209 | else: 210 | print( 211 | "No arguments provided. Please provide the existing and " 212 | "new manifest files names." 213 | ) 214 | print( 215 | "Example: merge_baseline_template_parameter.py " 216 | " " 217 | ) 218 | sys.exit(2) 219 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /source/codebuild_scripts/merge_directories.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | timestamp() { 6 | date +"%Y-%m-%d_%H-%M-%S" 7 | } 8 | 9 | # change directory to add-on directory. 10 | none_type_values=$1 11 | bool_type_values=$2 12 | add_on_directory='add-on' 13 | merge_script_report='merge_report.txt' 14 | user_input_file='user-input.yaml' 15 | add_on_manifest='add_on_manifest.yaml' 16 | 17 | echo "Merge script started." >> $merge_script_report 18 | 19 | # create directories if they don't exist 20 | mkdir -p templates 21 | mkdir -p parameters 22 | mkdir -p policies 23 | 24 | # create duplicate copy of all existing assets 25 | mkdir -p original 26 | rsync -qa --exclude=original * original 27 | 28 | echo "Check python version" 29 | which pip && pip --version 30 | which python3 && python3 --version 31 | 32 | #create add-on-manifests directory 33 | add_on_manifest_directory='add-on-manifests' 34 | mkdir -p $add_on_manifest_directory 35 | 36 | # Check if add-on directory exist in the configuration 37 | if [ -d $add_on_directory ]; 38 | then 39 | # Iterate through all the zip files in alphabetical order and unzip them all 40 | # Check of $add_on_directory" is empty 41 | if [ "$(ls $add_on_directory)" ]; then 42 | echo "$add_on_directory directory is not Empty" 43 | ls | grep "zip$" | sort 44 | 45 | # Processing zip files 46 | for x in `ls $add_on_directory | grep "zip$" | sort`; 47 | do 48 | # create new directory for each zip 49 | sub_add_on_dir=`echo $x | cut -d. -f1` 50 | echo "Creating directory file" $add_on_directory/$sub_add_on_dir 51 | echo "$ mkdir -p $add_on_directory/$sub_add_on_dir" 52 | mkdir -p $add_on_directory/$sub_add_on_dir 53 | 54 | # find all the zip files and unzip 55 | 56 | echo "Unzipping file:" $add_on_directory/$x >> $merge_script_report 57 | echo "$ unzip -o $add_on_directory/$x -d $add_on_directory/$sub_add_on_dir" 58 | unzip -o $add_on_directory/$x -d $add_on_directory/$sub_add_on_dir 59 | 60 | # track processed zip file after running required tasks 61 | echo "Finished unzipping $add_on_directory/$x." >> $merge_script_report 62 | echo "-----------------------------------------------------------------------------" >> $merge_script_report 63 | done 64 | else 65 | echo "$add_on_directory directory is Empty" 66 | fi 67 | else 68 | echo "Could not find 'add-on directory'" 69 | fi 70 | 71 | # Move add-on manifest files to the add-on folder 72 | # Check if add-on directory exist in the configuration 73 | if [ -d $add_on_directory ]; 74 | then 75 | # Iterate through all the directories in alphabetical order and sync the files. 76 | ls $add_on_directory 77 | # Check of $add_on_directory" is empty 78 | if [ "$(ls $add_on_directory)" ]; then 79 | echo "$add_on_directory directory is not Empty" 80 | ls | grep "zip$" | sort 81 | 82 | # Processing all the directories in the 'add-on' directory 83 | counter=0 84 | for y in `ls -d $add_on_directory/* | grep -v 'zip$' | sort`; 85 | do 86 | counter=$((counter+1)) 87 | timestamp >> $merge_script_report 88 | 89 | # Update files with user input 90 | echo "Updating files based on user's input" 91 | echo "python3 find_replace.py $y/$user_input_file $y $bool_type_values $none_type_values" 92 | python3 find_replace.py $y/$user_input_file $y $bool_type_values $none_type_values 93 | # Check python script exit code 94 | if [ $? -ne 0 ] 95 | then 96 | echo "Find-Replace with user input failed." 97 | exit 1 98 | fi 99 | 100 | echo "Processing $y directory" 101 | 102 | # Copy add-on manifest to a manifest directory 103 | echo "Copying $add_on_manifest to $add_on_manifest_directory directory as add_on_manifest_$counter.yaml" >> $merge_script_report 104 | # copying the manifest from the add-on directory to the separate directory for processing. 105 | echo "cp $y/$add_on_manifest $add_on_manifest_directory/"add_on_manifest_$counter.yaml"" 106 | cp $y/$add_on_manifest $add_on_manifest_directory/"add_on_manifest_$counter.yaml" 107 | echo "-----------------------------------------------------------------------------" >> $merge_script_report 108 | done 109 | else 110 | echo "$add_on_directory directory is Empty" 111 | fi 112 | else 113 | echo "Could not find 'add-on directory'" 114 | fi 115 | 116 | # Merge master manifest file with the add-on manifest files 117 | # iterate through the add-on manifest files 118 | if [ -d $add_on_manifest_directory ]; 119 | then 120 | # Iterate through all the add-on manifest files in alphabetical order 121 | if [ "$(ls $add_on_manifest_directory)" ]; 122 | then 123 | # Check if add_on_manifest_directory exist 124 | echo "$add_on_manifest_directory directory is not Empty" 125 | dir_name_length=`echo $add_on_manifest_directory | wc -m` 126 | # the length sorts following x1 x10 x11 x2 x3 to x1 x2 x3 x10 x11 127 | for x in `ls $add_on_manifest_directory | sort -nk1.$(($dir_name_length))`; 128 | do 129 | timestamp >> $merge_script_report 130 | echo $add_on_manifest_directory/$x 131 | if [ "$(grep '{{' $add_on_manifest_directory/$x)" ]; 132 | then 133 | echo "Found jinja pattern '{{ }}' in file: `grep '{{' $add_on_manifest_directory/* | cut -f1 -d: | uniq`" 134 | echo "Please check user-input.yaml to make sure all jinja keys are replaced with user value." 135 | exit 1 136 | else 137 | echo "Processing file:" $add_on_manifest_directory/$x >> $merge_script_report 138 | echo "python3 merge_manifest.py manifest.yaml $add_on_manifest_directory/$x manifest.yaml" 139 | python3 merge_manifest.py manifest.yaml $add_on_manifest_directory/$x manifest.yaml 140 | # Check python script exit code 141 | if [ $? -ne 0 ] 142 | then 143 | echo "Merging manifest files failed." 144 | exit 1 145 | fi 146 | # track processed manifest file after merging the manifests 147 | echo "Finished merging manifest file: $add_on_manifest_directory/$x" >> $merge_script_report 148 | echo "-----------------------------------------------------------------------------" >> $merge_script_report 149 | fi 150 | done 151 | else 152 | echo "$add_on_manifest_directory directory is Empty" 153 | fi 154 | else 155 | echo "'add-on-manifest directory' not found, nothing to merge." 156 | fi 157 | 158 | # Check if add-on directory exist in the configuration 159 | if [ -d $add_on_directory ]; 160 | then 161 | # Iterate through all the directories in alphabetical order and sync the files. 162 | ls $add_on_directory 163 | # Check of $add_on_directory" is empty 164 | if [ "$(ls $add_on_directory)" ]; 165 | then 166 | echo "$add_on_directory directory is not Empty" 167 | ls | grep "zip$" | sort 168 | 169 | # Processing all the directories in the 'add-on' directory 170 | for y in `ls -d $add_on_directory/* | grep -v 'zip$' | sort`; 171 | do 172 | timestamp >> $merge_script_report 173 | echo "Copying only new files to the existing templates, parameters or policies directory" 174 | if [ -d $y/templates ]; 175 | then 176 | echo "rsync --ignore-existing --verbose --recursive $y/templates/* templates" 177 | rsync --ignore-existing --verbose --recursive $y/templates/* templates 178 | echo "Templates synced." >> $merge_script_report 179 | else 180 | echo "'templates' directory not found in $y." 181 | echo "'templates' directory not found in $y." >> $merge_script_report 182 | fi 183 | if [ -d $y/parameters ]; 184 | then 185 | echo "rsync --ignore-existing --verbose --recursive $y/parameters/* parameters" 186 | rsync --ignore-existing --verbose --recursive $y/parameters/* parameters 187 | echo "Parameters synced." >> $merge_script_report 188 | else 189 | echo "'parameters' directory not found in $y." 190 | echo "'parameters' directory not found in $y." >> $merge_script_report 191 | fi 192 | if [ -d $y/policies ]; 193 | then 194 | echo "rsync --ignore-existing --verbose --recursive $y/policies/* policies" 195 | rsync --ignore-existing --verbose --recursive $y/policies/* policies 196 | echo "Policies synced." >> $merge_script_report 197 | else 198 | echo "'policies' directory not found in $y." 199 | echo "'policies' directory not found in $y." >> $merge_script_report 200 | fi 201 | echo "-----------------------------------------------------------------------------" >> $merge_script_report 202 | done 203 | else 204 | echo "$add_on_directory directory is Empty" 205 | fi 206 | else 207 | echo "Could not find 'add-on directory'" 208 | fi 209 | 210 | # Check if add-on directory exist in the configuration 211 | if [ -d $add_on_directory ]; 212 | then 213 | # Iterate through all the directories in alphabetical order and sync the files. 214 | ls $add_on_directory 215 | # Check of $add_on_directory" is empty 216 | if [ "$(ls $add_on_directory)" ]; 217 | then 218 | echo "$add_on_directory directory is not Empty" 219 | ls | grep "zip$" | sort 220 | 221 | # Processing all the directories in the 'add-on' directory 222 | for y in `ls -d $add_on_directory/* | grep -v 'zip$' | sort`; 223 | do 224 | timestamp >> $merge_script_report 225 | echo "-----------------------------------------------------------------------------" >> $merge_script_report 226 | done 227 | else 228 | echo "$add_on_directory directory is Empty" 229 | fi 230 | else 231 | echo "Could not find 'add-on directory'" 232 | fi 233 | 234 | if [ -a add_on_avm_files.json ]; then 235 | # iterate through avm template and parameter file and merge them 236 | echo "Merging AVM template and parameter files started." >> $merge_script_report 237 | echo "python3 merge_baseline_template_parameter.py info master_avm_files.json add_on_avm_files.json" 238 | python3 merge_baseline_template_parameter.py info master_avm_files.json add_on_avm_files.json 239 | #Check python script exit code 240 | if [ $? -ne 0 ] 241 | then 242 | echo "Merging AVM template and parameter files failed." >> $merge_script_report 243 | echo "Merging AVM template and parameter files failed." 244 | exit 1 245 | else 246 | echo "Merging AVM template and parameter files finished." >> $merge_script_report 247 | fi 248 | else 249 | echo "No Add-On AVM templates to merge" 250 | fi 251 | echo "-----------------------------------------------------------------------------" >> $merge_script_report 252 | echo "Merge script finished." 253 | echo "Merge script finished." >> $merge_script_report 254 | --------------------------------------------------------------------------------