├── susscanner ├── .gitkeep ├── rules │ ├── .gitkeep │ ├── s3.guard │ ├── cloudwatch.guard │ ├── emr.guard │ ├── codeguru.guard │ ├── rds.guard │ ├── graviton.guard │ ├── glue.guard │ ├── test_cases │ │ ├── api_gw_tests.yaml │ │ ├── cloudwatch_tests.yaml │ │ ├── codeguru_tests.yaml │ │ ├── emr_tests.yaml │ │ ├── rds_tests.yaml │ │ ├── s3_tests.yaml │ │ ├── cloud_front_tests.yaml │ │ ├── glue_tests.yaml │ │ ├── ec2_tests.yaml │ │ └── graviton_tests.yaml │ ├── api_gw.guard │ ├── cloud_front.guard │ └── ec2.guard ├── __main__.py ├── __init__.py ├── config.py ├── cli.py ├── scan.py └── rules_metadata.json ├── requirements.txt ├── setup.cfg ├── demo.gif ├── CODE_OF_CONDUCT.md ├── LICENSE.txt ├── setup.py ├── tests ├── test-data │ ├── ecs-cluster-output.txt │ ├── rds-output.txt │ ├── wordpress-output.txt │ ├── test-output-1.txt │ └── lamp-output.txt └── test_scan.py ├── CONTRIBUTING.md ├── README.md └── demo_cloudformation.yaml /susscanner/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /susscanner/rules/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | typer==0.12.4 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/sustainability-scanner/HEAD/demo.gif -------------------------------------------------------------------------------- /susscanner/__main__.py: -------------------------------------------------------------------------------- 1 | import typer 2 | import susscanner as ss 3 | 4 | 5 | def main(): 6 | typer.run(ss.main) 7 | 8 | 9 | if __name__ == "__main__": 10 | main() 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /susscanner/rules/s3.guard: -------------------------------------------------------------------------------- 1 | # 2 | # Amazon S3 checks 3 | # This rules checks that your Amazon S3 buckets have a lifecycle configuration attached to them 4 | # to either delete data or move it to other tiers 5 | 6 | let s3_buckets = Resources.*[ Type == 'AWS::S3::Bucket' ] 7 | 8 | 9 | rule ensure_all_buckets_have_lifecycle_configuration when %s3_buckets !empty { 10 | let itConfigurations = %s3_buckets.Properties.LifecycleConfiguration 11 | %itConfigurations exists 12 | } 13 | -------------------------------------------------------------------------------- /susscanner/rules/cloudwatch.guard: -------------------------------------------------------------------------------- 1 | # 2 | # Amazon CloudWatch checks 3 | # This rule checks if log group retention is set 4 | # DEFAULT: This rule assumes that if retention time is not explicitly set then it is indefinite by default. 5 | # If this default will be changed in the future then rule should be changed. 6 | 7 | let log_groups = Resources.*[ Type == 'AWS::Logs::LogGroup' ] 8 | 9 | rule ensure_all_loggroups_have_retention when %log_groups !empty { 10 | let retention = %log_groups.Properties.RetentionInDays 11 | %retention exists 12 | } -------------------------------------------------------------------------------- /susscanner/rules/emr.guard: -------------------------------------------------------------------------------- 1 | # 2 | # Amazon EMR Checks 3 | # This rule focuses on using spot instances for task nodes 4 | 5 | let emr_instancegroup_config = Resources.*[Type == 'AWS::EMR::InstanceGroupConfig'] 6 | let emr_instancefleet_config = Resources.*[Type == 'AWS::EMR::InstanceFleetConfig'] 7 | 8 | rule emr_use_spot when %emr_instancegroup_config !empty { 9 | %emr_instancegroup_config.Properties.BidPrice exists 10 | } 11 | 12 | rule emr_target_spot_config_configured when %emr_instancefleet_config !empty { 13 | %emr_instancefleet_config.Properties { 14 | when TargetSpotCapacity exists { 15 | TargetSpotCapacity > 0 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /susscanner/rules/codeguru.guard: -------------------------------------------------------------------------------- 1 | # 2 | # Amazon CodeGuru Check 3 | # This rule checks if a AWS CodeCommit repository has Amazon CodeGuru Reviewer enabled. 4 | # Amazon CodeGuru can suggest improvements on code-level sustainability. 5 | # You will be charged for reviewing code using Amazon CodeGuru. 6 | # See pricing details at https://aws.amazon.com/codeguru/pricing/ 7 | 8 | let codecommit_repo = Resources.*[Type == 'AWS::CodeCommit::Repository'] 9 | let codeguru_reviewer = Resources.*[Type == 'AWS::CodeGuruReviewer::RepositoryAssociation'] 10 | 11 | rule check_codeguru_association when %codecommit_repo !empty { 12 | %codeguru_reviewer !empty { 13 | %codeguru_reviewer.Properties.Name == %codecommit_repo.Properties.RepositoryName 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /susscanner/rules/rds.guard: -------------------------------------------------------------------------------- 1 | # 2 | # Amazon RDS Checks 3 | # This rule checks if Amazon RDS Performance Insights is enabled that helps you optimize your database. 4 | # Performance insights offers a rolling seven days of performance data history at no charge. 5 | # You will be charged for analyzing longer-term performance trends. 6 | # See pricing at https://aws.amazon.com/rds/performance-insights/pricing/ 7 | 8 | let rds_db = Resources.*[Type == 'AWS::RDS::DBInstance'] 9 | 10 | rule check_rds_performanceinsights_enabled when %rds_db !empty { 11 | %rds_db.Properties.EnablePerformanceInsights exists 12 | when %rds_db.Properties.EnablePerformanceInsights exists { 13 | %rds_db.Properties.EnablePerformanceInsights in ['true', true] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /susscanner/rules/graviton.guard: -------------------------------------------------------------------------------- 1 | # 2 | # AWS Graviton Checks 3 | # These rules checks if Amazon RDS instances and AWS Lambda functions are using AWS Graviton based processors 4 | # that are more power-efficient. 5 | 6 | let rds_db = Resources.*[Type == 'AWS::RDS::DBInstance'] 7 | let valid_instance_classes = ['db.m6g', 'db.m6gd', 'db.m7g', 'db.x2g', 'db.r6g', 'db.r7g', 'db.r6gd', 'db.t4g'] 8 | 9 | 10 | rule check_graviton_instance_usage_in_rds when %rds_db !empty { 11 | %rds_db.Properties.DBInstanceClass in %valid_instance_classes 12 | } 13 | 14 | let lambda_function = Resources.*[Type == 'AWS::Lambda::Function'] 15 | 16 | rule check_graviton_architecture_usage_in_lambda when %lambda_function !empty { 17 | %lambda_function.Properties.Architectures == 'arm64' 18 | } 19 | -------------------------------------------------------------------------------- /susscanner/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pathlib import Path 4 | from susscanner.scan import Scan 5 | from susscanner.config import init_app 6 | from susscanner.cli import main 7 | 8 | 9 | __app_name__ = "susscanner" 10 | __version__ = "1.3.0" 11 | 12 | ( 13 | SUCCESS, 14 | FILE_NOT_FOUND, 15 | JSON_ERROR, 16 | TEMPLATE_ERROR, 17 | ID_ERROR, 18 | ) = range(5) 19 | 20 | ( 21 | _, 22 | SCORE_LOW, 23 | SCORE_MEDIUM, 24 | SCORE_HIGH, 25 | ) = range(4) 26 | 27 | ERRORS = { 28 | FILE_NOT_FOUND: "config file error", 29 | JSON_ERROR: "json error", 30 | TEMPLATE_ERROR: "CloudFormation Template error", 31 | ID_ERROR: "id error", 32 | } 33 | 34 | DIR_PATH = os.path.dirname(__file__) 35 | CONFIG_FILE_NAME = "rules_metadata.json" 36 | CONFIG_FILE_PATH = Path(os.path.join(DIR_PATH, CONFIG_FILE_NAME)) 37 | -------------------------------------------------------------------------------- /susscanner/rules/glue.guard: -------------------------------------------------------------------------------- 1 | # 2 | # AWS Glue Checks 3 | # These rules focus on optimizing AWS Glue jobs by limiting timeouts and allocated 4 | # and max capacity in case the jobs don't require the service defaults 5 | 6 | let glue_job = Resources.*[Type == 'AWS::Glue::Job'] 7 | 8 | rule check_glue_job_timeout when %glue_job !empty { 9 | %glue_job.Properties.Timeout exists 10 | } 11 | 12 | rule check_glue_job_workernodes when %glue_job !empty { 13 | %glue_job.Properties 14 | { 15 | when WorkerType exists 16 | { 17 | NumberOfWorkers exists 18 | } 19 | } 20 | } 21 | 22 | rule check_glue_job_allocatedcapacity when %glue_job !empty { 23 | %glue_job.Properties.AllocatedCapacity exists 24 | } 25 | 26 | rule check_glue_job_maxcapacity when %glue_job !empty { 27 | %glue_job.Properties.MaxCapacity exists 28 | } 29 | 30 | -------------------------------------------------------------------------------- /susscanner/rules/test_cases/api_gw_tests.yaml: -------------------------------------------------------------------------------- 1 | - name: TestCorrectConfig 2 | input: 3 | Resources: 4 | API-GW-1: 5 | Type: 'AWS::ApiGateway::RestApi' 6 | Properties: 7 | MinimumCompressionSize: 1024 8 | expectations: 9 | rules: 10 | rest_api_compression_exists: PASS 11 | rest_api_compression_min: PASS 12 | rest_api_compression_max: PASS 13 | 14 | - name: TestMissingCompression 15 | input: 16 | Resources: 17 | API-GW-1: 18 | Type: 'AWS::ApiGateway::RestApi' 19 | Properties: 20 | expectations: 21 | rules: 22 | verify_rest_api_compression_exists: FAIL 23 | 24 | - name: TestTooLargeConfig 25 | input: 26 | Resources: 27 | API-GW-1: 28 | Type: 'AWS::ApiGateway::RestApi' 29 | Properties: 30 | MinimumCompressionSize: 2049 31 | expectations: 32 | rules: 33 | rest_api_compression_max: FAIL 34 | -------------------------------------------------------------------------------- /susscanner/config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import susscanner as ss 3 | 4 | 5 | def init_app(cfn_template: list) -> int: 6 | """ 7 | check if the configuration file and CloudFormation template exists and if 8 | the configuration is valid json. 9 | 10 | Args: 11 | cfn_template (str): the CloudFormation template. 12 | 13 | Returns: 14 | int: the success code if the configuration file exists and is valid. 15 | and the CloudFormation template exists 16 | """ 17 | for path in cfn_template: 18 | # check if the config file exists 19 | if not path.is_file(): 20 | return ss.FILE_NOT_FOUND 21 | 22 | # check if the json inside the config file is valid 23 | if not json.loads(ss.CONFIG_FILE_PATH.read_text()): 24 | return ss.JSON_ERROR 25 | 26 | # check if the CloudFormation Template exists 27 | if not path.exists(): 28 | return ss.TEMPLATE_ERROR 29 | 30 | return ss.SUCCESS 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /susscanner/rules/api_gw.guard: -------------------------------------------------------------------------------- 1 | # 2 | # Amazon API Gateway checks 3 | # These rules checks Rest API compression configuration. 4 | # It checks that compression threshold is between 1 and 2 MB. 5 | # Setting too low compression threshold potentially can increase final data size and will consume 6 | # too much of CPU. Too high a threshold will consume extra network bandwidth where data could 7 | # have been compressed efficiently. 8 | 9 | let api_gw_rest = Resources.*[ Type == 'AWS::ApiGateway::RestApi' ] 10 | 11 | rule rest_api_compression_exists when %api_gw_rest !empty { 12 | %api_gw_rest.Properties { 13 | MinimumCompressionSize exists 14 | } 15 | } 16 | 17 | rule rest_api_compression_min when %api_gw_rest !empty { 18 | %api_gw_rest.Properties { 19 | when MinimumCompressionSize exists { 20 | MinimumCompressionSize >= 1024 21 | } 22 | } 23 | } 24 | 25 | rule rest_api_compression_max when %api_gw_rest !empty { 26 | %api_gw_rest.Properties { 27 | when MinimumCompressionSize exists { 28 | MinimumCompressionSize <= 2048 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name="sustainability-scanner", 5 | version="1.3.0", 6 | author="AWS", 7 | description="Sustainability Scanner", 8 | long_description_content_type="text/markdown", 9 | long_description=open("README.md").read(), 10 | packages=find_packages(include=["susscanner", "susscanner.*"]), 11 | package_data={ 12 | "": ["rules_metadata.json", "rules/*", "rules/test_cases/*", "static/*"] 13 | }, 14 | include_package_data=True, 15 | install_requires=["typer==0.12.4"], 16 | url="http://github.com/awslabs/sustainability-scanner", 17 | python_requires=">=3.6", 18 | license="MIT-0", 19 | entry_points={ 20 | "console_scripts": ["susscanner=susscanner.__main__:main"], 21 | }, 22 | classifiers=[ 23 | "Development Status :: 4 - Beta", 24 | "Intended Audience :: Developers", 25 | "Intended Audience :: System Administrators", 26 | "Natural Language :: English", 27 | "License :: OSI Approved :: MIT No Attribution License (MIT-0)", 28 | "Programming Language :: Python", 29 | "Programming Language :: Python :: 3", 30 | ], 31 | ) 32 | -------------------------------------------------------------------------------- /susscanner/rules/test_cases/cloudwatch_tests.yaml: -------------------------------------------------------------------------------- 1 | - name: TestEmptyResources 2 | input: 3 | Resources: {} 4 | expectations: 5 | rules: 6 | ensure_all_loggroups_have_retention: SKIP 7 | 8 | - name: TestOneGroupWithRetention 9 | input: 10 | Resources: 11 | LogGroup-1: 12 | Type: 'AWS::Logs::LogGroup' 13 | Properties: 14 | RetentionInDays: 100 15 | expectations: 16 | rules: 17 | ensure_all_loggroups_have_retention: PASS 18 | 19 | - name: TestTwoGroupsWithRetention 20 | input: 21 | Resources: 22 | LogGroup-1: 23 | Type: 'AWS::Logs::LogGroup' 24 | Properties: 25 | RetentionInDays: 100 26 | LogGroup-2: 27 | Type: 'AWS::Logs::LogGroup' 28 | Properties: 29 | RetentionInDays: 200 30 | expectations: 31 | rules: 32 | ensure_all_loggroups_have_retention: PASS 33 | 34 | - name: TestTwoGroupsWithoutRetention 35 | input: 36 | Resources: 37 | LogGroup-1: 38 | Type: 'AWS::Logs::LogGroup' 39 | Properties: 40 | RetentionInDays: 100 41 | LogGroup-2: 42 | Type: 'AWS::Logs::LogGroup' 43 | Properties: 44 | expectations: 45 | rules: 46 | ensure_all_loggroups_have_retention: FAIL -------------------------------------------------------------------------------- /susscanner/rules/test_cases/codeguru_tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: TestEmptyTemplate 3 | input: {} 4 | expectations: 5 | rules: 6 | check_codeguru_association: SKIP 7 | 8 | - name: TestEmptyResources 9 | input: 10 | Resources: {} 11 | expectations: 12 | rules: 13 | check_codeguru_association: SKIP 14 | 15 | - name: TestWithCodeGuruAssociation 16 | input: 17 | Resources: 18 | MyRepository: 19 | Type: AWS::CodeCommit::Repository 20 | Properties: 21 | RepositoryName: MyRepositoryName 22 | MyRepositoryAssociation: 23 | Type: AWS::CodeGuruReviewer::RepositoryAssociation 24 | Properties: 25 | Name: MyRepositoryName 26 | Type: CodeCommit 27 | expectations: 28 | rules: 29 | check_codeguru_association: PASS 30 | 31 | - name: TestWithoutCodeGuruAssociation 32 | input: 33 | Resources: 34 | MyRepository: 35 | Type: AWS::CodeCommit::Repository 36 | Properties: 37 | RepositoryName: MyRepositoryName 38 | expectations: 39 | rules: 40 | check_codeguru_association: FAIL 41 | 42 | - name: TestWithCodeGuruAssociation 43 | input: 44 | Resources: 45 | MyRepository: 46 | Type: AWS::CodeCommit::Repository 47 | Properties: 48 | RepositoryName: MyRepositoryName 49 | MyRepositoryAssociation: 50 | Type: AWS::CodeGuruReviewer::RepositoryAssociation 51 | Properties: 52 | Name: MyRepositoryName2 53 | Type: CodeCommit 54 | expectations: 55 | rules: 56 | check_codeguru_association: FAIL 57 | -------------------------------------------------------------------------------- /susscanner/rules/cloud_front.guard: -------------------------------------------------------------------------------- 1 | # 2 | # Amazon CloudFront checks 3 | # These rules check the configuration of your Amazon CloudFront Distribution 4 | # It checks that the compression is enabled and that the time to live (TTL) 5 | # for the objects is according to the recommended values. 6 | 7 | let cf = Resources.*[ Type == 'AWS::CloudFront::Distribution' ] 8 | 9 | let cf_def_ttl_policies = Resources.*[ 10 | Type == 'AWS::CloudFront::CachePolicy' 11 | ] 12 | 13 | rule ensure_default_compression_on when %cf !empty { 14 | let compression = %cf.Properties.DistributionConfig.DefaultCacheBehavior.Compress 15 | %compression exists 16 | %compression in ['true', true] 17 | } 18 | 19 | rule ensure_compression_on when %cf !empty { 20 | %cf { 21 | Properties.DistributionConfig { 22 | when CacheBehaviors exists { 23 | CacheBehaviors[*] { 24 | Compress exists 25 | Compress in ['true', true] 26 | } 27 | } 28 | } 29 | } 30 | } 31 | 32 | # It is recommended to keep DefaultTTL and MaxTTL above 24h 33 | rule check_default_ttl when %cf_def_ttl_policies !empty { 34 | let config = %cf_def_ttl_policies.Properties.CachePolicyConfig 35 | when %config.DefaultTTL exists { 36 | %config.DefaultTTL >= 86400 37 | } 38 | } 39 | 40 | # It is recommended to keep DefaultTTL and MaxTTL above 24h 41 | rule check_max_ttl when %cf_def_ttl_policies !empty { 42 | let config = %cf_def_ttl_policies.Properties.CachePolicyConfig 43 | when %config.MaxTTL exists { 44 | %config.MaxTTL >= 86400 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /susscanner/rules/test_cases/emr_tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: TestEmptyTemplate 3 | input: {} 4 | expectations: 5 | rules: 6 | emr_use_spot: SKIP 7 | emr_target_spot_config_configured: SKIP 8 | 9 | - name: TestEmptyResources 10 | input: 11 | Resources: {} 12 | expectations: 13 | rules: 14 | emr_use_spot: SKIP 15 | emr_target_spot_config_configured: SKIP 16 | 17 | - name: TestWithInstanceGroupAndNoSpot 18 | input: 19 | Resources: 20 | MyInstanceGroup: 21 | Type: AWS::EMR::InstanceGroupConfig 22 | Properties: 23 | InstanceCount: 5 24 | InstanceRole: TASK 25 | InstanceType: 1 26 | JobFlowId: 123 27 | expectations: 28 | rules: 29 | emr_use_spot: FAIL 30 | emr_target_spot_config_configured: SKIP 31 | 32 | - name: TestWithInstanceGroupAndSpot 33 | input: 34 | Resources: 35 | MyInstanceGroup: 36 | Type: AWS::EMR::InstanceGroupConfig 37 | Properties: 38 | BidPrice: 256 39 | InstanceCount: 5 40 | InstanceRole: TASK 41 | InstanceType: 1 42 | JobFlowId: 123 43 | expectations: 44 | rules: 45 | emr_use_spot: PASS 46 | emr_target_spot_config_configured: SKIP 47 | 48 | - name: TestWithInstanceFleetAndNoSpot 49 | input: 50 | Resources: 51 | MyInstanceFleet: 52 | Type: AWS::EMR::InstanceFleetConfig 53 | Properties: 54 | ClusterId: 1 55 | InstanceFleetType: TASK 56 | TargetSpotCapacity: 0 57 | expectations: 58 | rules: 59 | emr_use_spot: SKIP 60 | emr_target_spot_config_configured: FAIL 61 | 62 | - name: TestWithInstanceFleetAndSpot 63 | input: 64 | Resources: 65 | MyInstanceFleet: 66 | Type: AWS::EMR::InstanceFleetConfig 67 | Properties: 68 | ClusterId: 1 69 | InstanceFleetType: TASK 70 | TargetSpotCapacity: 5 71 | expectations: 72 | rules: 73 | emr_use_spot: SKIP 74 | emr_target_spot_config_configured: PASS 75 | -------------------------------------------------------------------------------- /susscanner/rules/test_cases/rds_tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: TestEmptyTemplate 3 | input: {} 4 | expectations: 5 | rules: 6 | check_rds_performanceinsights_enabled: SKIP 7 | 8 | - name: TestEmptyResources 9 | input: 10 | Resources: {} 11 | expectations: 12 | rules: 13 | check_rds_performanceinsights_enabled: SKIP 14 | 15 | - name: TestRDSWithPerfInsights 16 | input: 17 | Resources: 18 | RDSInstance: 19 | Type: AWS::RDS::DBInstance 20 | Properties: 21 | DBName: 'DBName' 22 | AllocatedStorage: '5' 23 | DBInstanceClass: db.m6g 24 | Engine: MySQL 25 | EngineVersion: 5.6.19 26 | MasterUsername: 'DBUser' 27 | MasterUserPassword: 'DBPassword' 28 | DBParameterGroupName: 'MyRDSParamGroup' 29 | EnablePerformanceInsights: 'true' 30 | expectations: 31 | rules: 32 | check_rds_performanceinsights_enabled: PASS 33 | 34 | - name: TestRDSWithPerfInsightsBoolean 35 | input: 36 | Resources: 37 | RDSInstance: 38 | Type: AWS::RDS::DBInstance 39 | Properties: 40 | DBName: 'DBName' 41 | AllocatedStorage: '5' 42 | DBInstanceClass: db.m6g 43 | Engine: MySQL 44 | EngineVersion: 5.6.19 45 | MasterUsername: 'DBUser' 46 | MasterUserPassword: 'DBPassword' 47 | DBParameterGroupName: 'MyRDSParamGroup' 48 | EnablePerformanceInsights: true 49 | expectations: 50 | rules: 51 | check_rds_performanceinsights_enabled: PASS 52 | 53 | - name: TestRDSWithoutPerfInsights 54 | input: 55 | Resources: 56 | RDSInstance: 57 | Type: AWS::RDS::DBInstance 58 | Properties: 59 | DBName: 'DBName' 60 | AllocatedStorage: '5' 61 | DBInstanceClass: db.t2 62 | Engine: MySQL 63 | EngineVersion: 5.6.19 64 | MasterUsername: 'DBUser' 65 | MasterUserPassword: 'DBPassword' 66 | DBParameterGroupName: 'MyRDSParamGroup' 67 | expectations: 68 | rules: 69 | check_rds_performanceinsights_enabled: FAIL -------------------------------------------------------------------------------- /tests/test-data/ecs-cluster-output.txt: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "metadata": {}, 4 | "status": "SKIP", 5 | "not_compliant": [], 6 | "not_applicable": [ 7 | "rest_api_compression_max", 8 | "rest_api_compression_exists", 9 | "rest_api_compression_min" 10 | ], 11 | "compliant": [] 12 | }{ 13 | "name": "", 14 | "metadata": {}, 15 | "status": "SKIP", 16 | "not_compliant": [], 17 | "not_applicable": [ 18 | "ensure_default_compression_on", 19 | "check_default_ttl", 20 | "ensure_compression_on", 21 | "check_max_ttl" 22 | ], 23 | "compliant": [] 24 | }{ 25 | "name": "", 26 | "metadata": {}, 27 | "status": "SKIP", 28 | "not_compliant": [], 29 | "not_applicable": [ 30 | "ensure_all_loggroups_have_retention" 31 | ], 32 | "compliant": [] 33 | }{ 34 | "name": "", 35 | "metadata": {}, 36 | "status": "SKIP", 37 | "not_compliant": [], 38 | "not_applicable": [ 39 | "check_codeguru_association" 40 | ], 41 | "compliant": [] 42 | }{ 43 | "name": "", 44 | "metadata": {}, 45 | "status": "SKIP", 46 | "not_compliant": [], 47 | "not_applicable": [ 48 | "from_port_is_ssh", 49 | "to_port_is_ssh", 50 | "port_range_includes_ssh", 51 | "user_data_to_AMI", 52 | "graphics_acceleration_instead_of_gpu" 53 | ], 54 | "compliant": [] 55 | }{ 56 | "name": "", 57 | "metadata": {}, 58 | "status": "SKIP", 59 | "not_compliant": [], 60 | "not_applicable": [ 61 | "emr_target_spot_config_configured", 62 | "emr_use_spot" 63 | ], 64 | "compliant": [] 65 | }{ 66 | "name": "", 67 | "metadata": {}, 68 | "status": "SKIP", 69 | "not_compliant": [], 70 | "not_applicable": [ 71 | "check_glue_job_allocatedcapacity", 72 | "check_glue_job_workernodes", 73 | "check_glue_job_maxcapacity", 74 | "check_glue_job_timeout" 75 | ], 76 | "compliant": [] 77 | }{ 78 | "name": "", 79 | "metadata": {}, 80 | "status": "SKIP", 81 | "not_compliant": [], 82 | "not_applicable": [ 83 | "check_graviton_instance_usage_in_rds", 84 | "check_graviton_architecture_usage_in_lambda" 85 | ], 86 | "compliant": [] 87 | }{ 88 | "name": "", 89 | "metadata": {}, 90 | "status": "SKIP", 91 | "not_compliant": [], 92 | "not_applicable": [ 93 | "check_rds_performanceinsights_enabled" 94 | ], 95 | "compliant": [] 96 | }{ 97 | "name": "", 98 | "metadata": {}, 99 | "status": "SKIP", 100 | "not_compliant": [], 101 | "not_applicable": [ 102 | "ensure_all_buckets_have_lifecycle_configuration" 103 | ], 104 | "compliant": [] 105 | }{ 106 | "name": "", 107 | "metadata": {}, 108 | "status": "SKIP", 109 | "not_compliant": [], 110 | "not_applicable": [ 111 | "iamuserpassword" 112 | ], 113 | "compliant": [] 114 | } -------------------------------------------------------------------------------- /susscanner/rules/ec2.guard: -------------------------------------------------------------------------------- 1 | # 2 | # Amazon EC2 checks 3 | # These rules focus on 4 | # 1/ removing Bastion hosts by using AWS Systems Manager Session Manager 5 | 6 | let sec_groups_ingress = Resources.*[ Type == 'AWS::EC2::SecurityGroupIngress' ] 7 | 8 | let sec_groups = Resources.*[ Type == 'AWS::EC2::SecurityGroup' 9 | Properties.SecurityGroupIngress exists 10 | ] 11 | 12 | let ec2_instances = Resources.*[ Type == 'AWS::EC2::Instance'] 13 | 14 | # This rule recommends using Systems Manager Session Manager instead of 15 | # creating bastion hosts. 16 | # 17 | rule from_port_is_ssh when %sec_groups_ingress !empty OR %sec_groups !empty { 18 | %sec_groups_ingress.Properties { 19 | when FromPort exists { 20 | FromPort !in ["22", 22] 21 | } 22 | } 23 | %sec_groups.Properties { 24 | SecurityGroupIngress[*] { 25 | when FromPort exists { 26 | FromPort !in ["22", 22] 27 | } 28 | } 29 | } 30 | } 31 | 32 | rule to_port_is_ssh when %sec_groups_ingress !empty OR %sec_groups !empty { 33 | %sec_groups_ingress.Properties { 34 | when ToPort exists { 35 | ToPort !in ["22", 22] 36 | } 37 | } 38 | %sec_groups.Properties { 39 | SecurityGroupIngress[*] { 40 | when ToPort exists { 41 | ToPort !in ["22", 22] 42 | } 43 | } 44 | } 45 | } 46 | 47 | rule port_range_includes_ssh when %sec_groups_ingress !empty OR %sec_groups !empty { 48 | %sec_groups_ingress.Properties { 49 | when FromPort exists { 50 | when ToPort exists ToPort !is_string FromPort !is_string { 51 | when FromPort != 22 ToPort != 22 { 52 | FromPort > 22 or 53 | ToPort < 22 54 | } 55 | } 56 | when ToPort exists ToPort is_string FromPort is_string { 57 | when FromPort != "22" ToPort != "22" { 58 | FromPort > "22" or 59 | ToPort < "22" 60 | } 61 | } 62 | } 63 | } 64 | 65 | %sec_groups.Properties { 66 | SecurityGroupIngress[*] { 67 | when FromPort exists { 68 | when ToPort exists ToPort !is_string FromPort !is_string { 69 | when FromPort != 22 ToPort != 22 { 70 | FromPort > 22 or 71 | ToPort < 22 72 | } 73 | } 74 | when ToPort exists ToPort is_string FromPort is_string { 75 | when FromPort != "22" ToPort != "22" { 76 | FromPort > "22" or 77 | ToPort < "22" 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /susscanner/rules/test_cases/s3_tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: TestEmptyTemplate 3 | input: {} 4 | expectations: 5 | rules: 6 | ensure_all_buckets_have_lifecycle_configuration: SKIP 7 | 8 | - name: TestEmptyResources 9 | input: 10 | Resources: {} 11 | expectations: 12 | rules: 13 | ensure_all_buckets_have_lifecycle_configuration: SKIP 14 | 15 | - name: TestOneBucketWithLC 16 | input: 17 | Resources: 18 | S3Bucket-1: 19 | Type: 'AWS::S3::Bucket' 20 | Properties: 21 | LifecycleConfiguration: 22 | Rules: 23 | - Id: GlacierRule 24 | Prefix: glacier 25 | Status: Enabled 26 | ExpirationInDays: 365 27 | Transitions: 28 | - TransitionInDays: 1 29 | StorageClass: GLACIER 30 | DeletionPolicy: Retain 31 | expectations: 32 | rules: 33 | ensure_all_buckets_have_lifecycle_configuration: PASS 34 | 35 | - name: TestOneBucketWithoutLC 36 | input: 37 | Resources: 38 | S3Bucket-1: 39 | Type: 'AWS::S3::Bucket' 40 | Properties: 41 | DeletionPolicy: Retain 42 | expectations: 43 | rules: 44 | ensure_all_buckets_have_lifecycle_configuration: FAIL 45 | 46 | - name: TestTwoBucketsWithLC 47 | input: 48 | Resources: 49 | S3Bucket-1: 50 | Type: 'AWS::S3::Bucket' 51 | Properties: 52 | LifecycleConfiguration: 53 | Rules: 54 | - Id: GlacierRule 55 | Prefix: glacier 56 | Status: Enabled 57 | ExpirationInDays: 365 58 | Transitions: 59 | - TransitionInDays: 1 60 | StorageClass: GLACIER 61 | DeletionPolicy: Retain 62 | S3Bucket-2: 63 | Type: 'AWS::S3::Bucket' 64 | Properties: 65 | LifecycleConfiguration: 66 | Rules: 67 | - Id: GlacierRule 68 | Prefix: glacier 69 | Status: Enabled 70 | ExpirationInDays: 365 71 | Transitions: 72 | - TransitionInDays: 1 73 | StorageClass: GLACIER 74 | DeletionPolicy: Retain 75 | expectations: 76 | rules: 77 | ensure_all_buckets_have_lifecycle_configuration: PASS 78 | 79 | - name: TestTwoBucketsWithoutLC 80 | input: 81 | Resources: 82 | S3Bucket-1: 83 | Type: 'AWS::S3::Bucket' 84 | Properties: 85 | IntelligentTieringConfigurations: 86 | - Id: Tier1 87 | Status: Enabled 88 | Tierings: 89 | - AccessTier: ARCHIVE_ACCESS 90 | Days: 90 91 | DeletionPolicy: Retain 92 | S3Bucket-2: 93 | Type: 'AWS::S3::Bucket' 94 | Properties: 95 | DeletionPolicy: Retain 96 | expectations: 97 | rules: 98 | ensure_all_buckets_have_lifecycle_configuration: FAIL 99 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE.txt) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /tests/test_scan.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pathlib import Path 3 | 4 | from susscanner import Scan 5 | 6 | TEST_DATA_DIR = Path(__file__).parent.joinpath("test-data") 7 | RULES_METADATA_DIR = ( 8 | Path(__file__).parent.parent.joinpath("susscanner").joinpath("rules_metadata.json") 9 | ) 10 | 11 | 12 | class TestScan(unittest.TestCase): 13 | def test_empty_input(self): 14 | result = Scan.parse_cfn_guard_output( 15 | Scan(), 16 | "", 17 | "empty.yaml", 18 | RULES_METADATA_DIR, 19 | ) 20 | self.assertEqual(len(result["failed_rules"]), 0) 21 | 22 | def test_input_with_1_violation(self): 23 | # Template source for this output is VPC_EC2_Instance_With_ENI from 24 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-ec2.html 25 | # with one modification - To-From ports changed from 22-22, to 20-23 26 | with open(TEST_DATA_DIR / "test-output-1.txt") as f: 27 | data = f.read() 28 | result = Scan.parse_cfn_guard_output( 29 | Scan(), 30 | data, 31 | "quickref-ec2.yaml", 32 | RULES_METADATA_DIR, 33 | ) 34 | fr = result["failed_rules"] 35 | self.assertEqual(1, len(fr)) 36 | self.assertEqual(fr[0]["rule_name"], "port_range_includes_ssh") 37 | 38 | def test_wordpress_output(self): 39 | # Template source for this output is 40 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/sample-templates-applications-eu-west-2.html 41 | with open(TEST_DATA_DIR / "wordpress-output.txt") as f: 42 | data = f.read() 43 | result = Scan.parse_cfn_guard_output( 44 | Scan(), 45 | data, 46 | "sample.yaml", 47 | RULES_METADATA_DIR, 48 | ) 49 | fr = result["failed_rules"] 50 | self.assertEqual(0, len(fr)) 51 | 52 | def test_ecs_cluster_output(self): 53 | # Template source for this output is 54 | # https://github.com/aws-samples/ecs-refarch-cloudformation/blob/master/infrastructure/ecs-cluster.yaml 55 | with open(TEST_DATA_DIR / "ecs-cluster-output.txt") as f: 56 | data = f.read() 57 | result = Scan.parse_cfn_guard_output( 58 | Scan(), 59 | data, 60 | "ecs-cluster.yaml", 61 | RULES_METADATA_DIR, 62 | ) 63 | fr = result["failed_rules"] 64 | self.assertEqual(0, len(fr)) 65 | 66 | def test_lamp_output(self): 67 | # Template source for this output is 68 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/sample-templates-appframeworks-eu-west-1.html 69 | with open(TEST_DATA_DIR / "lamp-output.txt") as f: 70 | data = f.read() 71 | result = Scan.parse_cfn_guard_output( 72 | Scan(), 73 | data, 74 | "appframeworks.yaml", 75 | RULES_METADATA_DIR, 76 | ) 77 | fr = result["failed_rules"] 78 | self.assertEqual(2, len(fr)) 79 | self.assertEqual(fr[0]["rule_name"], "check_graviton_instance_usage_in_rds") 80 | self.assertEqual(fr[1]["rule_name"], "check_rds_performanceinsights_enabled") 81 | 82 | def test_rds_output(self): 83 | # Template source for this output is 84 | # https://github.com/awslabs/aws-cloudformation-templates/blob/master/aws/services/RDS/RDS_with_DBParameterGroup.yaml 85 | # with one modification - Added `EnablePerformanceInsights: 'true'` 86 | with open(TEST_DATA_DIR / "rds-output.txt") as f: 87 | data = f.read() 88 | result = Scan.parse_cfn_guard_output( 89 | Scan(), 90 | data, 91 | "rds.yaml", 92 | RULES_METADATA_DIR, 93 | ) 94 | fr = result["failed_rules"] 95 | self.assertEqual(1, len(fr)) 96 | self.assertEqual(fr[0]["rule_name"], "check_graviton_instance_usage_in_rds") 97 | 98 | 99 | if __name__ == "__main__": 100 | unittest.main() 101 | -------------------------------------------------------------------------------- /susscanner/rules/test_cases/cloud_front_tests.yaml: -------------------------------------------------------------------------------- 1 | - name: TestDefaultCompressionOn 2 | input: 3 | Resources: 4 | CF-Distribution-1: 5 | Type: 'AWS::CloudFront::Distribution' 6 | Properties: 7 | DistributionConfig: 8 | DefaultCacheBehavior: 9 | Compress: true 10 | expectations: 11 | rules: 12 | ensure_default_compression_on: PASS 13 | 14 | - name: TestDefaultCompressionOff 15 | input: 16 | Resources: 17 | CF-Distribution-1: 18 | Type: 'AWS::CloudFront::Distribution' 19 | Properties: 20 | DistributionConfig: 21 | DefaultCacheBehavior: 22 | Compress: "false" 23 | expectations: 24 | rules: 25 | ensure_default_compression_on: FAIL 26 | 27 | - name: TestDefaultCompressionMissing 28 | input: 29 | Resources: 30 | CF-Distribution-1: 31 | Type: 'AWS::CloudFront::Distribution' 32 | Properties: 33 | DistributionConfig: 34 | DefaultCacheBehavior: 35 | expectations: 36 | rules: 37 | ensure_default_compression_on: FAIL 38 | 39 | - name: TestCacheCompressionOn 40 | input: 41 | Resources: 42 | CF-Distribution-1: 43 | Type: 'AWS::CloudFront::Distribution' 44 | Properties: 45 | DistributionConfig: 46 | CacheBehaviors: 47 | - PathPattern: pattern/1 48 | Compress: true 49 | expectations: 50 | rules: 51 | ensure_compression_on: PASS 52 | ensure_default_compression_on: FAIL 53 | 54 | - name: TestOneCacheCompressionOff 55 | input: 56 | Resources: 57 | CF-Distribution-1: 58 | Type: 'AWS::CloudFront::Distribution' 59 | Properties: 60 | DistributionConfig: 61 | CacheBehaviors: 62 | - PathPattern: pattern/1 63 | Compress: true 64 | - PathPattern: pattern/2 65 | expectations: 66 | rules: 67 | ensure_compression_on: FAIL 68 | ensure_default_compression_on: FAIL 69 | 70 | - name: TestOneCacheCompressionOff 71 | input: 72 | Resources: 73 | CF-Distribution-1: 74 | Type: 'AWS::CloudFront::Distribution' 75 | Properties: 76 | DistributionConfig: 77 | DefaultCacheBehavior: 78 | Compress: true 79 | CacheBehaviors: 80 | - PathPattern: pattern/1 81 | Compress: true 82 | - PathPattern: pattern/2 83 | Compress: "true" 84 | expectations: 85 | rules: 86 | ensure_compression_on: PASS 87 | ensure_default_compression_on: PASS 88 | 89 | - name: TestNoCustomTTL 90 | input: 91 | Resources: 92 | CF-Policy: 93 | Type: 'AWS::CloudFront::CachePolicy' 94 | Properties: 95 | CachePolicyConfig: 96 | Comment: 'Just a comment' 97 | expectations: 98 | rules: 99 | check_default_ttl: SKIP 100 | check_max_ttl: SKIP 101 | 102 | - name: TestSmallDefaultTTL 103 | input: 104 | Resources: 105 | CF-Policy: 106 | Type: 'AWS::CloudFront::CachePolicy' 107 | Properties: 108 | CachePolicyConfig: 109 | DefaultTTL: 1000 110 | Comment: 'Just a comment' 111 | expectations: 112 | rules: 113 | check_default_ttl: FAIL 114 | 115 | - name: TestSmallMaxTTL 116 | input: 117 | Resources: 118 | CF-Policy: 119 | Type: 'AWS::CloudFront::CachePolicy' 120 | Properties: 121 | CachePolicyConfig: 122 | DefaultTTL: 86400 123 | MaxTTL: 86399 124 | Comment: 'Just a comment' 125 | expectations: 126 | rules: 127 | check_max_ttl: FAIL 128 | 129 | - name: TestValidTTL 130 | input: 131 | Resources: 132 | CF-Policy: 133 | Type: 'AWS::CloudFront::CachePolicy' 134 | Properties: 135 | CachePolicyConfig: 136 | DefaultTTL: 86400 137 | MaxTTL: 86401 138 | Comment: 'Just a comment' 139 | expectations: 140 | rules: 141 | check_policy_ttl: PASS 142 | check_max_ttl: PASS 143 | -------------------------------------------------------------------------------- /susscanner/rules/test_cases/glue_tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: TestEmptyTemplate 3 | input: {} 4 | expectations: 5 | rules: 6 | check_glue_job_timeout: SKIP 7 | check_glue_job_workernodes: SKIP 8 | check_glue_job_allocatedcapacity: SKIP 9 | check_glue_job_maxcapacity: SKIP 10 | 11 | - name: TestEmptyResources 12 | input: 13 | Resources: {} 14 | expectations: 15 | rules: 16 | check_glue_job_timeout: SKIP 17 | check_glue_job_workernodes: SKIP 18 | check_glue_job_allocatedcapacity: SKIP 19 | check_glue_job_maxcapacity: SKIP 20 | 21 | - name: TestWithDefaultTimeout 22 | input: 23 | Resources: 24 | MyGlueJob: 25 | Type: AWS::Glue::Job 26 | Properties: 27 | Role: !Ref AWSGlueJobRole 28 | Command: { 29 | "Name" : "glueetl", 30 | "ScriptLocation": !Sub "s3://${ArtifactBucketName}/process_sales_data.py" 31 | } 32 | expectations: 33 | rules: 34 | check_glue_job_timeout: FAIL 35 | check_glue_job_workernodes: SKIP 36 | check_glue_job_allocatedcapacity: FAIL 37 | check_glue_job_maxcapacity: FAIL 38 | 39 | - name: TestWithoutDefaultTimeout 40 | input: 41 | Resources: 42 | MyGlueJob: 43 | Type: AWS::Glue::Job 44 | Properties: 45 | Role: !Ref AWSGlueJobRole 46 | Command: { 47 | "Name" : "glueetl", 48 | "ScriptLocation": !Sub "s3://${ArtifactBucketName}/process_sales_data.py" 49 | } 50 | Timeout: 60 51 | expectations: 52 | rules: 53 | check_glue_job_timeout: PASS 54 | check_glue_job_workernodes: SKIP 55 | check_glue_job_allocatedcapacity: FAIL 56 | check_glue_job_maxcapacity: FAIL 57 | 58 | - name: TestWithDefaultWorkerNodes 59 | input: 60 | Resources: 61 | MyGlueJob: 62 | Type: AWS::Glue::Job 63 | Properties: 64 | Role: !Ref AWSGlueJobRole 65 | Command: { 66 | "Name" : "glueetl", 67 | "ScriptLocation": !Sub "s3://${ArtifactBucketName}/process_sales_data.py" 68 | } 69 | Timeout: 60 70 | WorkerType: Standard 71 | expectations: 72 | rules: 73 | check_glue_job_timeout: PASS 74 | check_glue_job_workernodes: FAIL 75 | check_glue_job_allocatedcapacity: FAIL 76 | check_glue_job_maxcapacity: FAIL 77 | 78 | - name: TestWithoutDefaultWorkerNodes 79 | input: 80 | Resources: 81 | MyGlueJob: 82 | Type: AWS::Glue::Job 83 | Properties: 84 | Role: !Ref AWSGlueJobRole 85 | Command: { 86 | "Name" : "glueetl", 87 | "ScriptLocation": !Sub "s3://${ArtifactBucketName}/process_sales_data.py" 88 | } 89 | Timeout: 60 90 | WorkerType: Standard 91 | NumberOfWorkers: 5 92 | expectations: 93 | rules: 94 | check_glue_job_timeout: PASS 95 | check_glue_job_workernodes: PASS 96 | check_glue_job_allocatedcapacity: FAIL 97 | check_glue_job_maxcapacity: FAIL 98 | 99 | - name: TestWithAllocatedCapacity 100 | input: 101 | Resources: 102 | MyGlueJob: 103 | Type: AWS::Glue::Job 104 | Properties: 105 | Role: !Ref AWSGlueJobRole 106 | Command: { 107 | "Name" : "glueetl", 108 | "ScriptLocation": !Sub "s3://${ArtifactBucketName}/process_sales_data.py" 109 | } 110 | Timeout: 60 111 | WorkerType: Standard 112 | NumberOfWorkers: 5 113 | AllocatedCapacity: 10 114 | expectations: 115 | rules: 116 | check_glue_job_timeout: PASS 117 | check_glue_job_workernodes: PASS 118 | check_glue_job_allocatedcapacity: PASS 119 | check_glue_job_maxcapacity: FAIL 120 | 121 | - name: TestWithMaxCapacity 122 | input: 123 | Resources: 124 | MyGlueJob: 125 | Type: AWS::Glue::Job 126 | Properties: 127 | Role: !Ref AWSGlueJobRole 128 | Command: { 129 | "Name" : "glueetl", 130 | "ScriptLocation": !Sub "s3://${ArtifactBucketName}/process_sales_data.py" 131 | } 132 | Timeout: 60 133 | AllocatedCapacity: 10 134 | MaxCapacity: 10 135 | expectations: 136 | rules: 137 | check_glue_job_timeout: PASS 138 | check_glue_job_workernodes: SKIP 139 | check_glue_job_allocatedcapacity: PASS 140 | check_glue_job_maxcapacity: PASS -------------------------------------------------------------------------------- /tests/test-data/rds-output.txt: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "metadata": {}, 4 | "status": "SKIP", 5 | "not_compliant": [], 6 | "not_applicable": [ 7 | "rest_api_compression_max", 8 | "rest_api_compression_min", 9 | "rest_api_compression_exists" 10 | ], 11 | "compliant": [] 12 | }{ 13 | "name": "", 14 | "metadata": {}, 15 | "status": "SKIP", 16 | "not_compliant": [], 17 | "not_applicable": [ 18 | "ensure_compression_on", 19 | "ensure_default_compression_on", 20 | "check_default_ttl", 21 | "check_max_ttl" 22 | ], 23 | "compliant": [] 24 | }{ 25 | "name": "", 26 | "metadata": {}, 27 | "status": "SKIP", 28 | "not_compliant": [], 29 | "not_applicable": [ 30 | "ensure_all_loggroups_have_retention" 31 | ], 32 | "compliant": [] 33 | }{ 34 | "name": "", 35 | "metadata": {}, 36 | "status": "SKIP", 37 | "not_compliant": [], 38 | "not_applicable": [ 39 | "check_codeguru_association" 40 | ], 41 | "compliant": [] 42 | }{ 43 | "name": "", 44 | "metadata": {}, 45 | "status": "SKIP", 46 | "not_compliant": [], 47 | "not_applicable": [ 48 | "from_port_is_ssh", 49 | "to_port_is_ssh", 50 | "port_range_includes_ssh" 51 | ], 52 | "compliant": [] 53 | }{ 54 | "name": "", 55 | "metadata": {}, 56 | "status": "SKIP", 57 | "not_compliant": [], 58 | "not_applicable": [ 59 | "emr_target_spot_config_configured", 60 | "emr_use_spot" 61 | ], 62 | "compliant": [] 63 | }{ 64 | "name": "", 65 | "metadata": {}, 66 | "status": "SKIP", 67 | "not_compliant": [], 68 | "not_applicable": [ 69 | "check_glue_job_allocatedcapacity", 70 | "check_glue_job_timeout", 71 | "check_glue_job_workernodes", 72 | "check_glue_job_maxcapacity" 73 | ], 74 | "compliant": [] 75 | }RDS_with_DBParameterGroup.yaml Status = FAIL 76 | FAILED rules 77 | graviton.guard/check_graviton_instance_usage_in_rds FAIL 78 | --- 79 | { 80 | "name": "", 81 | "metadata": {}, 82 | "status": "FAIL", 83 | "not_compliant": [ 84 | { 85 | "Rule": { 86 | "name": "check_graviton_instance_usage_in_rds", 87 | "metadata": {}, 88 | "messages": { 89 | "custom_message": null, 90 | "error_message": null 91 | }, 92 | "checks": [ 93 | { 94 | "Clause": { 95 | "Binary": { 96 | "context": " %rds_db[*].Properties.DBInstanceClass IN %valid_instance_classes", 97 | "messages": { 98 | "custom_message": null, 99 | "error_message": "Check was not compliant as property [/Resources/MyDB/Properties/DBInstanceClass[L:40,C:23]] was not present in [(resolved, Path=[L:0,C:0] Value=[\"db.m6g\",\"db.m6gd\",\"db.m7g\",\"db.x2g\",\"db.r6g\",\"db.r7g\",\"db.r6gd\",\"db.t4g\"])]" 100 | }, 101 | "check": { 102 | "InResolved": { 103 | "from": { 104 | "path": "/Resources/MyDB/Properties/DBInstanceClass", 105 | "value": "db.t2.small" 106 | }, 107 | "to": [ 108 | { 109 | "path": "", 110 | "value": [ 111 | "db.m6g", 112 | "db.m6gd", 113 | "db.m7g", 114 | "db.x2g", 115 | "db.r6g", 116 | "db.r7g", 117 | "db.r6gd", 118 | "db.t4g" 119 | ] 120 | } 121 | ], 122 | "comparison": [ 123 | "In", 124 | false 125 | ] 126 | } 127 | } 128 | } 129 | } 130 | } 131 | ] 132 | } 133 | } 134 | ], 135 | "not_applicable": [ 136 | "check_graviton_architecture_usage_in_lambda" 137 | ], 138 | "compliant": [] 139 | }{ 140 | "name": "", 141 | "metadata": {}, 142 | "status": "PASS", 143 | "not_compliant": [], 144 | "not_applicable": [], 145 | "compliant": [ 146 | "check_rds_performanceinsights_enabled" 147 | ] 148 | }{ 149 | "name": "", 150 | "metadata": {}, 151 | "status": "SKIP", 152 | "not_compliant": [], 153 | "not_applicable": [ 154 | "ensure_all_buckets_have_lifecycle_configuration" 155 | ], 156 | "compliant": [] 157 | } -------------------------------------------------------------------------------- /susscanner/rules/test_cases/ec2_tests.yaml: -------------------------------------------------------------------------------- 1 | - name: TestToPortNoSSH 2 | input: 3 | Resources: 4 | EC2-1: 5 | Type: 'AWS::EC2::SecurityGroupIngress' 6 | Properties: 7 | ToPort: 80 8 | expectations: 9 | rules: 10 | to_port_is_ssh: PASS 11 | from_port_is_ssh: SKIP 12 | port_range_includes_ssh: SKIP 13 | 14 | - name: TestToPortNoSSH-SG 15 | input: 16 | Resources: 17 | EC2-1: 18 | Type: 'AWS::EC2::SecurityGroup' 19 | Properties: 20 | SecurityGroupIngress: 21 | - ToPort: "80" 22 | expectations: 23 | rules: 24 | to_port_is_ssh: PASS 25 | from_port_is_ssh: SKIP 26 | port_range_includes_ssh: SKIP 27 | 28 | - name: TestToPortSSH 29 | input: 30 | Resources: 31 | EC2-1: 32 | Type: 'AWS::EC2::SecurityGroupIngress' 33 | Properties: 34 | ToPort: "22" 35 | expectations: 36 | rules: 37 | to_port_is_ssh: FAIL 38 | from_port_is_ssh: SKIP 39 | port_range_includes_ssh: SKIP 40 | 41 | - name: TestToPortSSH-SG 42 | input: 43 | Resources: 44 | EC2-1: 45 | Type: 'AWS::EC2::SecurityGroup' 46 | Properties: 47 | SecurityGroupIngress: 48 | - ToPort: 22 49 | expectations: 50 | rules: 51 | to_port_is_ssh: FAIL 52 | from_port_is_ssh: SKIP 53 | port_range_includes_ssh: SKIP 54 | 55 | - name: TestFromPortNoSSH 56 | input: 57 | Resources: 58 | EC2-1: 59 | Type: 'AWS::EC2::SecurityGroupIngress' 60 | Properties: 61 | FromPort: "23" 62 | expectations: 63 | rules: 64 | to_port_is_ssh: SKIP 65 | from_port_is_ssh: PASS 66 | port_range_includes_ssh: SKIP 67 | 68 | - name: TestFromPortNoSSH-SG 69 | input: 70 | Resources: 71 | EC2-1: 72 | Type: 'AWS::EC2::SecurityGroup' 73 | Properties: 74 | SecurityGroupIngress: 75 | - FromPort: 23 76 | expectations: 77 | rules: 78 | to_port_is_ssh: SKIP 79 | from_port_is_ssh: PASS 80 | port_range_includes_ssh: SKIP 81 | 82 | - name: TestFromPortSSH 83 | input: 84 | Resources: 85 | EC2-1: 86 | Type: 'AWS::EC2::SecurityGroupIngress' 87 | Properties: 88 | FromPort: "22" 89 | expectations: 90 | rules: 91 | to_port_is_ssh: SKIP 92 | from_port_is_ssh: FAIL 93 | port_range_includes_ssh: SKIP 94 | 95 | - name: TestFromPortSSH-SG 96 | input: 97 | Resources: 98 | EC2-1: 99 | Type: 'AWS::EC2::SecurityGroup' 100 | Properties: 101 | SecurityGroupIngress: 102 | - FromPort: 22 103 | expectations: 104 | rules: 105 | to_port_is_ssh: SKIP 106 | from_port_is_ssh: FAIL 107 | port_range_includes_ssh: SKIP 108 | 109 | - name: TestRangeWithSSH 110 | input: 111 | Resources: 112 | EC2-1: 113 | Type: 'AWS::EC2::SecurityGroupIngress' 114 | Properties: 115 | FromPort: 20 116 | ToPort: 80 117 | expectations: 118 | rules: 119 | to_port_is_ssh: PASS 120 | from_port_is_ssh: PASS 121 | port_range_includes_ssh: FAIL 122 | 123 | - name: TestRangeWithSSH-SG 124 | input: 125 | Resources: 126 | EC2-1: 127 | Type: 'AWS::EC2::SecurityGroup' 128 | Properties: 129 | SecurityGroupIngress: 130 | - FromPort: "20" 131 | ToPort: "80" 132 | expectations: 133 | rules: 134 | to_port_is_ssh: PASS 135 | from_port_is_ssh: PASS 136 | port_range_includes_ssh: FAIL 137 | 138 | - name: TestRangeNoSSH 139 | input: 140 | Resources: 141 | EC2-1: 142 | Type: 'AWS::EC2::SecurityGroupIngress' 143 | Properties: 144 | FromPort: 23 145 | ToPort: 80 146 | expectations: 147 | rules: 148 | to_port_is_ssh: PASS 149 | from_port_is_ssh: PASS 150 | port_range_includes_ssh: PASS 151 | 152 | - name: TestRangeNoSSH-SG 153 | input: 154 | Resources: 155 | EC2-1: 156 | Type: 'AWS::EC2::SecurityGroup' 157 | Properties: 158 | SecurityGroupIngress: 159 | - FromPort: "23" 160 | ToPort: "80" 161 | expectations: 162 | rules: 163 | to_port_is_ssh: PASS 164 | from_port_is_ssh: PASS 165 | port_range_includes_ssh: PASS 166 | 167 | - name: TestParametrizedPort 168 | input: 169 | Parameters: 170 | ContainerPort: 171 | Description: The port on which the container is running 172 | Type: Number 173 | Default: 6001 174 | Resources: 175 | EC2-1: 176 | Type: 'AWS::EC2::SecurityGroup' 177 | Properties: 178 | SecurityGroupIngress: 179 | - FromPort: !Ref ContainerPort 180 | ToPort: !Ref ContainerPort 181 | expectations: 182 | rules: 183 | to_port_is_ssh: PASS 184 | from_port_is_ssh: PASS 185 | port_range_includes_ssh: PASS 186 | -------------------------------------------------------------------------------- /susscanner/rules/test_cases/graviton_tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: TestEmptyTemplate 3 | input: {} 4 | expectations: 5 | rules: 6 | check_graviton_instance_usage_in_rds: SKIP 7 | check_graviton_architecture_usage_in_lambda: SKIP 8 | 9 | - name: TestEmptyResources 10 | input: 11 | Resources: {} 12 | expectations: 13 | rules: 14 | check_graviton_instance_usage_in_rds: SKIP 15 | check_graviton_architecture_usage_in_lambda: SKIP 16 | 17 | - name: TestRDSWithGraviton 18 | input: 19 | Resources: 20 | RDSInstance: 21 | Type: AWS::RDS::DBInstance 22 | Properties: 23 | DBName: 'DBName' 24 | AllocatedStorage: '5' 25 | DBInstanceClass: db.m6g 26 | Engine: MySQL 27 | EngineVersion: 5.6.19 28 | MasterUsername: 'DBUser' 29 | MasterUserPassword: 'DBPassword' 30 | DBParameterGroupName: 'MyRDSParamGroup' 31 | expectations: 32 | rules: 33 | check_graviton_instance_usage_in_rds: PASS 34 | check_graviton_architecture_usage_in_lambda: SKIP 35 | 36 | - name: TestRDSWithoutGraviton 37 | input: 38 | Resources: 39 | RDSInstance: 40 | Type: AWS::RDS::DBInstance 41 | Properties: 42 | DBName: 'DBName' 43 | AllocatedStorage: '5' 44 | DBInstanceClass: db.t2 45 | Engine: MySQL 46 | EngineVersion: 5.6.19 47 | MasterUsername: 'DBUser' 48 | MasterUserPassword: 'DBPassword' 49 | DBParameterGroupName: 'MyRDSParamGroup' 50 | expectations: 51 | rules: 52 | check_graviton_instance_usage_in_rds: FAIL 53 | check_graviton_architecture_usage_in_lambda: SKIP 54 | 55 | - name: TestLambdaWithZipAndWithoutGraviton 56 | input: 57 | Resources: 58 | LambdaFunction: 59 | Type: AWS::Lambda::Function 60 | Properties: 61 | Architectures: 'x86' 62 | FunctionName: 63 | Fn::Sub: lambda-function-0200 64 | Description: LambdaFunctioni of nodejs10.x. 65 | Runtime: nodejs10.x 66 | Code: 67 | ZipFile: 68 | "exports.handler = function(event, context){\n 69 | var sample = sample;" 70 | Handler: ${LambdaHandlerPath} 71 | MemorySize: 128 72 | Timeout: 10 73 | PackageType: 'Zip' 74 | expectations: 75 | rules: 76 | check_graviton_instance_usage_in_rds: SKIP 77 | check_graviton_architecture_usage_in_lambda: FAIL 78 | 79 | - name: TestLambdaWithZipAndWithGraviton 80 | input: 81 | Resources: 82 | LambdaFunction: 83 | Type: AWS::Lambda::Function 84 | Properties: 85 | Architectures: 'arm64' 86 | FunctionName: 87 | Fn::Sub: lambda-function-0200 88 | Description: LambdaFunctioni of nodejs10.x. 89 | Runtime: nodejs10.x 90 | Code: 91 | ZipFile: 92 | "exports.handler = function(event, context){\n 93 | var sample = sample;" 94 | Handler: ${LambdaHandlerPath} 95 | MemorySize: 128 96 | Timeout: 10 97 | PackageType: 'Zip' 98 | expectations: 99 | rules: 100 | check_graviton_instance_usage_in_rds: SKIP 101 | check_graviton_architecture_usage_in_lambda: PASS 102 | 103 | - name: TestLambdaWithImageAndWithGraviton 104 | input: 105 | Resources: 106 | LambdaFunction: 107 | Type: AWS::Lambda::Function 108 | Properties: 109 | Architectures: 'arm64' 110 | FunctionName: 111 | Fn::Sub: lambda-function-0200 112 | Description: LambdaFunctioni of nodejs10.x. 113 | Runtime: nodejs10.x 114 | Code: 115 | ZipFile: 116 | "exports.handler = function(event, context){\n 117 | var sample = sample;" 118 | Handler: ${LambdaHandlerPath} 119 | MemorySize: 128 120 | Timeout: 10 121 | PackageType: 'Image' 122 | expectations: 123 | rules: 124 | check_graviton_instance_usage_in_rds: SKIP 125 | check_graviton_architecture_usage_in_lambda: PASS 126 | 127 | - name: TestLambdaWithImageAndWithoutGraviton 128 | input: 129 | Resources: 130 | LambdaFunction: 131 | Type: AWS::Lambda::Function 132 | Properties: 133 | Architectures: 'x86' 134 | FunctionName: 135 | Fn::Sub: lambda-function-0200 136 | Description: LambdaFunctioni of nodejs10.x. 137 | Runtime: nodejs10.x 138 | Code: 139 | ZipFile: 140 | "exports.handler = function(event, context){\n 141 | var sample = sample;" 142 | Handler: ${LambdaHandlerPath} 143 | MemorySize: 128 144 | Timeout: 10 145 | PackageType: 'Image' 146 | expectations: 147 | rules: 148 | check_graviton_instance_usage_in_rds: SKIP 149 | check_graviton_architecture_usage_in_lambda: FAIL 150 | 151 | -------------------------------------------------------------------------------- /tests/test-data/wordpress-output.txt: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "metadata": {}, 4 | "status": "SKIP", 5 | "not_compliant": [], 6 | "not_applicable": [ 7 | "rest_api_compression_exists", 8 | "rest_api_compression_min", 9 | "rest_api_compression_max" 10 | ], 11 | "compliant": [] 12 | }{ 13 | "name": "", 14 | "metadata": {}, 15 | "status": "SKIP", 16 | "not_compliant": [], 17 | "not_applicable": [ 18 | "check_default_ttl", 19 | "check_max_ttl", 20 | "ensure_default_compression_on", 21 | "ensure_compression_on" 22 | ], 23 | "compliant": [] 24 | }{ 25 | "name": "", 26 | "metadata": {}, 27 | "status": "SKIP", 28 | "not_compliant": [], 29 | "not_applicable": [ 30 | "ensure_all_loggroups_have_retention" 31 | ], 32 | "compliant": [] 33 | }{ 34 | "name": "", 35 | "metadata": {}, 36 | "status": "SKIP", 37 | "not_compliant": [], 38 | "not_applicable": [ 39 | "check_codeguru_association" 40 | ], 41 | "compliant": [] 42 | }wordpress.json Status = FAIL 43 | FAILED rules 44 | ec2.guard/user_data_to_AMI FAIL 45 | --- 46 | { 47 | "name": "", 48 | "metadata": {}, 49 | "status": "FAIL", 50 | "not_compliant": [ 51 | { 52 | "Rule": { 53 | "name": "user_data_to_AMI", 54 | "metadata": {}, 55 | "messages": { 56 | "custom_message": null, 57 | "error_message": null 58 | }, 59 | "checks": [ 60 | { 61 | "Clause": { 62 | "Unary": { 63 | "context": " %ec2_instances[*].Properties.UserData not EXISTS ", 64 | "messages": { 65 | "custom_message": "", 66 | "error_message": "Check was not compliant as property [/Resources/WebServer/Properties/UserData[L:344,C:21]] existed." 67 | }, 68 | "check": { 69 | "Resolved": { 70 | "value": { 71 | "path": "/Resources/WebServer/Properties/UserData", 72 | "value": { 73 | "Fn::Base64": { 74 | "Fn::Join": [ 75 | "", 76 | [ 77 | "#!/bin/bash -xe\n", 78 | "yum update -y aws-cfn-bootstrap\n", 79 | "/opt/aws/bin/cfn-init -v ", 80 | " --stack ", 81 | { 82 | "Ref": "AWS::StackName" 83 | }, 84 | " --resource WebServer ", 85 | " --configsets wordpress_install ", 86 | " --region ", 87 | { 88 | "Ref": "AWS::Region" 89 | }, 90 | "\n", 91 | "/opt/aws/bin/cfn-signal -e $? ", 92 | " --stack ", 93 | { 94 | "Ref": "AWS::StackName" 95 | }, 96 | " --resource WebServer ", 97 | " --region ", 98 | { 99 | "Ref": "AWS::Region" 100 | }, 101 | "\n" 102 | ] 103 | ] 104 | } 105 | } 106 | }, 107 | "comparison": [ 108 | "Exists", 109 | true 110 | ] 111 | } 112 | } 113 | } 114 | } 115 | } 116 | ] 117 | } 118 | } 119 | ], 120 | "not_applicable": [ 121 | "to_port_is_ssh", 122 | "from_port_is_ssh", 123 | "port_range_includes_ssh" 124 | ], 125 | "compliant": [ 126 | "graphics_acceleration_instead_of_gpu" 127 | ] 128 | }{ 129 | "name": "", 130 | "metadata": {}, 131 | "status": "SKIP", 132 | "not_compliant": [], 133 | "not_applicable": [ 134 | "emr_target_spot_config_configured", 135 | "emr_use_spot" 136 | ], 137 | "compliant": [] 138 | }{ 139 | "name": "", 140 | "metadata": {}, 141 | "status": "SKIP", 142 | "not_compliant": [], 143 | "not_applicable": [ 144 | "check_glue_job_timeout", 145 | "check_glue_job_allocatedcapacity", 146 | "check_glue_job_workernodes", 147 | "check_glue_job_maxcapacity" 148 | ], 149 | "compliant": [] 150 | }{ 151 | "name": "", 152 | "metadata": {}, 153 | "status": "SKIP", 154 | "not_compliant": [], 155 | "not_applicable": [ 156 | "check_graviton_instance_usage_in_rds", 157 | "check_graviton_architecture_usage_in_lambda" 158 | ], 159 | "compliant": [] 160 | }{ 161 | "name": "", 162 | "metadata": {}, 163 | "status": "SKIP", 164 | "not_compliant": [], 165 | "not_applicable": [ 166 | "check_rds_performanceinsights_enabled" 167 | ], 168 | "compliant": [] 169 | }{ 170 | "name": "", 171 | "metadata": {}, 172 | "status": "SKIP", 173 | "not_compliant": [], 174 | "not_applicable": [ 175 | "ensure_all_buckets_have_lifecycle_configuration" 176 | ], 177 | "compliant": [] 178 | }{ 179 | "name": "", 180 | "metadata": {}, 181 | "status": "SKIP", 182 | "not_compliant": [], 183 | "not_applicable": [ 184 | "iamuserpassword" 185 | ], 186 | "compliant": [] 187 | } -------------------------------------------------------------------------------- /susscanner/cli.py: -------------------------------------------------------------------------------- 1 | import shlex 2 | import subprocess 3 | import shutil 4 | from enum import Enum 5 | 6 | import typer 7 | import json 8 | import susscanner as ss 9 | 10 | from pathlib import Path 11 | from typing import Optional, List, Annotated 12 | 13 | app = typer.Typer(add_completion=False) 14 | 15 | 16 | class TemplateType(str, Enum): 17 | cloudformation = ("cf",) 18 | cdk = "cdk" 19 | 20 | 21 | def _version_callback(value: bool) -> None: 22 | """ 23 | Returns SusScanner version. 24 | 25 | Args: 26 | value (bool): Display the version Y/N. 27 | 28 | Raises: 29 | typer.Exit: exit the program 30 | """ 31 | if value: 32 | typer.echo(f"{ss.__app_name__} v{ss.__version__}") 33 | raise typer.Exit() 34 | 35 | 36 | def _rules_metadata_callback(rules_metadata: Path) -> Path: 37 | """ 38 | Checks if the provided rules metadata is valid 39 | 40 | Args: 41 | rules_metadata (Path): The path of the config file 42 | 43 | Raises: 44 | typer.Exit: exit the program 45 | """ 46 | if rules_metadata is None: 47 | return rules_metadata 48 | 49 | # check if the rules metadata exists 50 | if not rules_metadata.is_file(): 51 | raise typer.BadParameter( 52 | f"Specified rules metadata file '{rules_metadata}' is not a file" 53 | ) 54 | 55 | # check if the json inside the rules metadata is valid 56 | try: 57 | json.loads(rules_metadata.read_text()) 58 | except ValueError: 59 | raise typer.BadParameter( 60 | f"Specified rules metadata file '{rules_metadata}' is not valid json" 61 | ) 62 | return rules_metadata 63 | 64 | 65 | def run_command(command: str) -> str: 66 | """ 67 | Runs a command. 68 | 69 | Args: 70 | command (str): The command to run. 71 | """ 72 | args = shlex.split(command) 73 | 74 | # Get the full path of the command to ensure execution in Windows 75 | args[0] = shutil.which(args[0]) 76 | 77 | output = subprocess.Popen( 78 | args, 79 | shell=False, 80 | universal_newlines=True, 81 | stdout=subprocess.PIPE, 82 | ).stdout.read() 83 | return output 84 | 85 | 86 | def preprocess_cdk(stack_name: str) -> str: 87 | """ 88 | Preprocesses CDK file. 89 | 90 | Args: 91 | stack_name (str): CDK stack name. 92 | 93 | Raises: 94 | typer.Exit: exit the program 95 | """ 96 | 97 | template_name = "susscanner_template.yaml" 98 | with open(template_name, "w") as f: 99 | output = run_command(f"cdk synth {stack_name}") 100 | f.write(output) 101 | return template_name 102 | 103 | 104 | def main( 105 | cfn_template: Annotated[ 106 | List[Path], 107 | typer.Argument( 108 | help="List of template names (for CloudFormation format) or stack name (for CDK format)" 109 | ), 110 | ], 111 | version: Optional[bool] = typer.Option( 112 | None, 113 | "--version", 114 | "-v", 115 | help="Show the application's version and exit.", 116 | callback=_version_callback, 117 | is_eager=True, 118 | ), 119 | rules_metadata: Path = typer.Option( 120 | None, 121 | "--rules", 122 | "-r", 123 | help="Location for a custom rules metadata file.", 124 | callback=_rules_metadata_callback, 125 | show_default=False, 126 | ), 127 | template_format: Optional[TemplateType] = typer.Option( 128 | TemplateType.cloudformation.value, 129 | "--format", 130 | "-f", 131 | help="Template format", 132 | show_default=True, 133 | is_eager=False, 134 | ), 135 | ) -> int: 136 | """ """ # additional docstring to surpress the comments in the cli output 137 | """ 138 | This function constitutes the main flow of the program. First, it checks if 139 | a valid config file exists. It then checks whether the specified Cloudformation 140 | template can be found. Given the input checks passed, "cfn-guard validate" is run 141 | to execute our rules on the provided template. The output of Cloudformation Guard 142 | is structured and enriched to result in the Sustainability Scanner report. 143 | 144 | Args: 145 | cfn_template (str, optional): the CloudFormation template or stack name (for CDK format). 146 | version (Optional[bool], optional): the version of the program. 147 | 148 | Raises: 149 | typer.Exit: (1) closed because the configuration file was not found 150 | typer.Exit: (2) closed because the configuration is not valid JSON 151 | typer.Exit: (3) closed because the CloudFormation file was not found 152 | 153 | Returns: 154 | int: returns the exit status, 0 for successful execution 155 | """ 156 | app_init_error = ss.init_app(cfn_template) 157 | if ( 158 | app_init_error == ss.FILE_NOT_FOUND 159 | and template_format == TemplateType.cloudformation.value 160 | ): 161 | typer.secho( 162 | f'Template file not found "{ss.ERRORS[app_init_error]}"', 163 | fg=typer.colors.RED, 164 | ) 165 | raise typer.Exit(1) 166 | if app_init_error == ss.JSON_ERROR: 167 | typer.secho( 168 | f'Config file not valid "{ss.ERRORS[app_init_error]}"', 169 | fg=typer.colors.RED, 170 | ) 171 | raise typer.Exit(2) 172 | if app_init_error == ss.TEMPLATE_ERROR: 173 | typer.secho( 174 | f'CloudFormation template not found "{ss.ERRORS[app_init_error]}"', 175 | fg=typer.colors.RED, 176 | ) 177 | raise typer.Exit(3) 178 | 179 | rules = Path(ss.DIR_PATH).joinpath(Path("rules")).__str__() 180 | 181 | for t in cfn_template: 182 | if template_format == TemplateType.cdk: 183 | template = preprocess_cdk(str(t)) 184 | else: 185 | template = str(t) 186 | command = rf"cfn-guard validate -o json --rules '{rules}' --data '{template}'" 187 | cfn_guard_output = run_command(command) 188 | 189 | ss.Scan.filter_results( 190 | cfn_guard_output=cfn_guard_output, 191 | template_name=str(template), 192 | rules_metadata=rules_metadata, 193 | ) 194 | 195 | return 0 196 | -------------------------------------------------------------------------------- /tests/test-data/test-output-1.txt: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "metadata": {}, 4 | "status": "SKIP", 5 | "not_compliant": [], 6 | "not_applicable": [ 7 | "rest_api_compression_exists", 8 | "rest_api_compression_max", 9 | "rest_api_compression_min" 10 | ], 11 | "compliant": [] 12 | }{ 13 | "name": "", 14 | "metadata": {}, 15 | "status": "SKIP", 16 | "not_compliant": [], 17 | "not_applicable": [ 18 | "ensure_compression_on", 19 | "check_max_ttl", 20 | "check_default_ttl", 21 | "ensure_default_compression_on" 22 | ], 23 | "compliant": [] 24 | }{ 25 | "name": "", 26 | "metadata": {}, 27 | "status": "SKIP", 28 | "not_compliant": [], 29 | "not_applicable": [ 30 | "ensure_all_loggroups_have_retention" 31 | ], 32 | "compliant": [] 33 | }{ 34 | "name": "", 35 | "metadata": {}, 36 | "status": "SKIP", 37 | "not_compliant": [], 38 | "not_applicable": [ 39 | "check_codeguru_association" 40 | ], 41 | "compliant": [] 42 | }test.yaml Status = FAIL 43 | FAILED rules 44 | ec2.guard/port_range_includes_ssh FAIL 45 | ec2.guard/user_data_to_AMI FAIL 46 | ec2.guard/graphics_acceleration_instead_of_gpu FAIL 47 | --- 48 | { 49 | "name": "", 50 | "metadata": {}, 51 | "status": "FAIL", 52 | "not_compliant": [ 53 | { 54 | "Rule": { 55 | "name": "port_range_includes_ssh", 56 | "metadata": {}, 57 | "messages": { 58 | "custom_message": null, 59 | "error_message": null 60 | }, 61 | "checks": [ 62 | { 63 | "Disjunctions": { 64 | "checks": [ 65 | { 66 | "Clause": { 67 | "Binary": { 68 | "context": " FromPort GREATER THAN 22", 69 | "messages": { 70 | "custom_message": "", 71 | "error_message": "Check was not compliant as property value [Path=/Resources/SSHSecurityGroup/Properties/SecurityGroupIngress/0/FromPort[L:31,C:20] Value=20] not greater than value [Path=[L:0,C:0] Value=22]." 72 | }, 73 | "check": { 74 | "Resolved": { 75 | "from": { 76 | "path": "/Resources/SSHSecurityGroup/Properties/SecurityGroupIngress/0/FromPort", 77 | "value": 20 78 | }, 79 | "to": { 80 | "path": "", 81 | "value": 22 82 | }, 83 | "comparison": [ 84 | "Gt", 85 | false 86 | ] 87 | } 88 | } 89 | } 90 | } 91 | }, 92 | { 93 | "Clause": { 94 | "Binary": { 95 | "context": " ToPort LESS THAN 22", 96 | "messages": { 97 | "custom_message": "", 98 | "error_message": "Check was not compliant as property value [Path=/Resources/SSHSecurityGroup/Properties/SecurityGroupIngress/0/ToPort[L:33,C:18] Value=23] not less than value [Path=[L:0,C:0] Value=22]." 99 | }, 100 | "check": { 101 | "Resolved": { 102 | "from": { 103 | "path": "/Resources/SSHSecurityGroup/Properties/SecurityGroupIngress/0/ToPort", 104 | "value": 23 105 | }, 106 | "to": { 107 | "path": "", 108 | "value": 22 109 | }, 110 | "comparison": [ 111 | "Lt", 112 | false 113 | ] 114 | } 115 | } 116 | } 117 | } 118 | } 119 | ] 120 | } 121 | } 122 | ] 123 | } 124 | }, 125 | { 126 | "Rule": { 127 | "name": "user_data_to_AMI", 128 | "metadata": {}, 129 | "messages": { 130 | "custom_message": null, 131 | "error_message": null 132 | }, 133 | "checks": [ 134 | { 135 | "Clause": { 136 | "Unary": { 137 | "context": " %ec2_instances[*].Properties.UserData not EXISTS ", 138 | "messages": { 139 | "custom_message": "", 140 | "error_message": "Check was not compliant as property [/Resources/Ec2Instance/Properties/UserData[L:83,C:17]] existed." 141 | }, 142 | "check": { 143 | "Resolved": { 144 | "value": { 145 | "path": "/Resources/Ec2Instance/Properties/UserData", 146 | "value": { 147 | "Fn::Sub": "#!/bin/bash -xe\nyum install ec2-net-utils -y\nec2ifup eth1\nservice httpd start\n" 148 | } 149 | }, 150 | "comparison": [ 151 | "Exists", 152 | true 153 | ] 154 | } 155 | } 156 | } 157 | } 158 | } 159 | ] 160 | } 161 | }, 162 | { 163 | "Rule": { 164 | "name": "graphics_acceleration_instead_of_gpu", 165 | "metadata": {}, 166 | "messages": { 167 | "custom_message": null, 168 | "error_message": null 169 | }, 170 | "checks": [ 171 | { 172 | "Clause": { 173 | "Binary": { 174 | "context": " InstanceType not IN %list_of_gpu_instances", 175 | "messages": { 176 | "custom_message": "", 177 | "error_message": "Check was not compliant as property [InstanceType] to compare from is missing. Value traversed to [Path=/Resources/Ec2Instance/Properties[L:69,C:13] Value={\"ImageId\":{\"Fn::FindInMap\":[\"RegionMap\",{\"Ref\":\"AWS::Region\"},\"AMI\"]},\"KeyName\":{\"Ref\":\"KeyName\"},\"NetworkInterfaces\":[{\"NetworkInterfaceId\":{\"Ref\":\"controlXface\"},\"DeviceIndex\":0},{\"NetworkInterfaceId\":{\"Ref\":\"webXface\"},\"DeviceIndex\":1}],\"Tags\":[{\"Key\":\"Role\",\"Value\":\"Test Instance\"}],\"UserData\":{\"Fn::Sub\":\"#!/bin/bash -xe\nyum install ec2-net-utils -y\nec2ifup eth1\nservice httpd start\n\"}}]." 178 | }, 179 | "check": { 180 | "UnResolved": { 181 | "value": { 182 | "traversed_to": { 183 | "path": "/Resources/Ec2Instance/Properties", 184 | "value": { 185 | "ImageId": { 186 | "Fn::FindInMap": [ 187 | "RegionMap", 188 | { 189 | "Ref": "AWS::Region" 190 | }, 191 | "AMI" 192 | ] 193 | }, 194 | "KeyName": { 195 | "Ref": "KeyName" 196 | }, 197 | "NetworkInterfaces": [ 198 | { 199 | "NetworkInterfaceId": { 200 | "Ref": "controlXface" 201 | }, 202 | "DeviceIndex": 0 203 | }, 204 | { 205 | "NetworkInterfaceId": { 206 | "Ref": "webXface" 207 | }, 208 | "DeviceIndex": 1 209 | } 210 | ], 211 | "Tags": [ 212 | { 213 | "Key": "Role", 214 | "Value": "Test Instance" 215 | } 216 | ], 217 | "UserData": { 218 | "Fn::Sub": "#!/bin/bash -xe\nyum install ec2-net-utils -y\nec2ifup eth1\nservice httpd start\n" 219 | } 220 | } 221 | }, 222 | "remaining_query": "InstanceType", 223 | "reason": "Could not find key InstanceType inside struct at path /Resources/Ec2Instance/Properties[L:69,C:13]" 224 | }, 225 | "comparison": [ 226 | "In", 227 | true 228 | ] 229 | } 230 | } 231 | } 232 | } 233 | } 234 | ] 235 | } 236 | } 237 | ], 238 | "not_applicable": [], 239 | "compliant": [ 240 | "from_port_is_ssh", 241 | "to_port_is_ssh" 242 | ] 243 | }{ 244 | "name": "", 245 | "metadata": {}, 246 | "status": "SKIP", 247 | "not_compliant": [], 248 | "not_applicable": [ 249 | "emr_target_spot_config_configured", 250 | "emr_use_spot" 251 | ], 252 | "compliant": [] 253 | }{ 254 | "name": "", 255 | "metadata": {}, 256 | "status": "SKIP", 257 | "not_compliant": [], 258 | "not_applicable": [ 259 | "check_glue_job_workernodes", 260 | "check_glue_job_allocatedcapacity", 261 | "check_glue_job_maxcapacity", 262 | "check_glue_job_timeout" 263 | ], 264 | "compliant": [] 265 | }{ 266 | "name": "", 267 | "metadata": {}, 268 | "status": "SKIP", 269 | "not_compliant": [], 270 | "not_applicable": [ 271 | "check_graviton_architecture_usage_in_lambda", 272 | "check_graviton_instance_usage_in_rds" 273 | ], 274 | "compliant": [] 275 | }{ 276 | "name": "", 277 | "metadata": {}, 278 | "status": "SKIP", 279 | "not_compliant": [], 280 | "not_applicable": [ 281 | "check_rds_performanceinsights_enabled" 282 | ], 283 | "compliant": [] 284 | }{ 285 | "name": "", 286 | "metadata": {}, 287 | "status": "SKIP", 288 | "not_compliant": [], 289 | "not_applicable": [ 290 | "ensure_all_buckets_have_lifecycle_configuration" 291 | ], 292 | "compliant": [] 293 | }{ 294 | "name": "", 295 | "metadata": {}, 296 | "status": "SKIP", 297 | "not_compliant": [], 298 | "not_applicable": [ 299 | "iamuserpassword" 300 | ], 301 | "compliant": [] 302 | } -------------------------------------------------------------------------------- /tests/test-data/lamp-output.txt: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "metadata": {}, 4 | "status": "SKIP", 5 | "not_compliant": [], 6 | "not_applicable": [ 7 | "rest_api_compression_exists", 8 | "rest_api_compression_min", 9 | "rest_api_compression_max" 10 | ], 11 | "compliant": [] 12 | }{ 13 | "name": "", 14 | "metadata": {}, 15 | "status": "SKIP", 16 | "not_compliant": [], 17 | "not_applicable": [ 18 | "ensure_default_compression_on", 19 | "check_max_ttl", 20 | "ensure_compression_on", 21 | "check_default_ttl" 22 | ], 23 | "compliant": [] 24 | }{ 25 | "name": "", 26 | "metadata": {}, 27 | "status": "SKIP", 28 | "not_compliant": [], 29 | "not_applicable": [ 30 | "ensure_all_loggroups_have_retention" 31 | ], 32 | "compliant": [] 33 | }{ 34 | "name": "", 35 | "metadata": {}, 36 | "status": "SKIP", 37 | "not_compliant": [], 38 | "not_applicable": [ 39 | "check_codeguru_association" 40 | ], 41 | "compliant": [] 42 | }{ 43 | "name": "", 44 | "metadata": {}, 45 | "status": "SKIP", 46 | "not_compliant": [], 47 | "not_applicable": [ 48 | "port_range_includes_ssh", 49 | "user_data_to_AMI", 50 | "from_port_is_ssh", 51 | "graphics_acceleration_instead_of_gpu", 52 | "to_port_is_ssh" 53 | ], 54 | "compliant": [] 55 | }{ 56 | "name": "", 57 | "metadata": {}, 58 | "status": "SKIP", 59 | "not_compliant": [], 60 | "not_applicable": [ 61 | "emr_use_spot", 62 | "emr_target_spot_config_configured" 63 | ], 64 | "compliant": [] 65 | }{ 66 | "name": "", 67 | "metadata": {}, 68 | "status": "SKIP", 69 | "not_compliant": [], 70 | "not_applicable": [ 71 | "check_glue_job_maxcapacity", 72 | "check_glue_job_timeout", 73 | "check_glue_job_workernodes", 74 | "check_glue_job_allocatedcapacity" 75 | ], 76 | "compliant": [] 77 | }lamp.json Status = FAIL 78 | FAILED rules 79 | graviton.guard/check_graviton_instance_usage_in_rds FAIL 80 | --- 81 | { 82 | "name": "", 83 | "metadata": {}, 84 | "status": "FAIL", 85 | "not_compliant": [ 86 | { 87 | "Rule": { 88 | "name": "check_graviton_instance_usage_in_rds", 89 | "metadata": {}, 90 | "messages": { 91 | "custom_message": null, 92 | "error_message": null 93 | }, 94 | "checks": [ 95 | { 96 | "Clause": { 97 | "Binary": { 98 | "context": " %rds_db[*].Properties.DBInstanceClass IN %valid_instance_classes", 99 | "messages": { 100 | "custom_message": null, 101 | "error_message": "Check was not compliant as property [/Resources/MySQLDatabase/Properties/DBInstanceClass[L:502,C:27]] was not present in [(resolved, Path=[L:0,C:0] Value=[\"db.m6g\",\"db.m6gd\",\"db.x2g\",\"db.r6g\",\"db.r6gd\",\"db.t4g\"])]" 102 | }, 103 | "check": { 104 | "InResolved": { 105 | "from": { 106 | "path": "/Resources/MySQLDatabase/Properties/DBInstanceClass", 107 | "value": { 108 | "Ref": "DBInstanceClass" 109 | } 110 | }, 111 | "to": [ 112 | { 113 | "path": "", 114 | "value": [ 115 | "db.m6g", 116 | "db.m6gd", 117 | "db.x2g", 118 | "db.r6g", 119 | "db.r6gd", 120 | "db.t4g" 121 | ] 122 | } 123 | ], 124 | "comparison": [ 125 | "In", 126 | false 127 | ] 128 | } 129 | } 130 | } 131 | } 132 | } 133 | ] 134 | } 135 | } 136 | ], 137 | "not_applicable": [ 138 | "check_graviton_architecture_usage_in_lambda" 139 | ], 140 | "compliant": [] 141 | }lamp.json Status = FAIL 142 | FAILED rules 143 | rds.guard/check_rds_performanceinsights_enabled FAIL 144 | --- 145 | { 146 | "name": "", 147 | "metadata": {}, 148 | "status": "FAIL", 149 | "not_compliant": [ 150 | { 151 | "Rule": { 152 | "name": "check_rds_performanceinsights_enabled", 153 | "metadata": {}, 154 | "messages": { 155 | "custom_message": null, 156 | "error_message": null 157 | }, 158 | "checks": [ 159 | { 160 | "Clause": { 161 | "Unary": { 162 | "context": " %rds_db[*].Properties.EnablePerformanceInsights EXISTS ", 163 | "messages": { 164 | "custom_message": "", 165 | "error_message": "Check was not compliant as property [EnablePerformanceInsights] is missing. Value traversed to [Path=/Resources/MySQLDatabase/Properties[L:496,C:20] Value={\"Engine\":\"MySQL\",\"DBName\":{\"Ref\":\"DBName\"},\"MultiAZ\":{\"Ref\":\"MultiAZDatabase\"},\"MasterUsername\":{\"Ref\":\"DBUser\"},\"MasterUserPassword\":{\"Ref\":\"DBPassword\"},\"DBInstanceClass\":{\"Ref\":\"DBInstanceClass\"},\"AllocatedStorage\":{\"Ref\":\"DBAllocatedStorage\"},\"VPCSecurityGroups\":[{\"Fn::GetAtt\":[\"DBEC2SecurityGroup\",\"GroupId\"]}]}]." 166 | }, 167 | "check": { 168 | "UnResolved": { 169 | "value": { 170 | "traversed_to": { 171 | "path": "/Resources/MySQLDatabase/Properties", 172 | "value": { 173 | "Engine": "MySQL", 174 | "DBName": { 175 | "Ref": "DBName" 176 | }, 177 | "MultiAZ": { 178 | "Ref": "MultiAZDatabase" 179 | }, 180 | "MasterUsername": { 181 | "Ref": "DBUser" 182 | }, 183 | "MasterUserPassword": { 184 | "Ref": "DBPassword" 185 | }, 186 | "DBInstanceClass": { 187 | "Ref": "DBInstanceClass" 188 | }, 189 | "AllocatedStorage": { 190 | "Ref": "DBAllocatedStorage" 191 | }, 192 | "VPCSecurityGroups": [ 193 | { 194 | "Fn::GetAtt": [ 195 | "DBEC2SecurityGroup", 196 | "GroupId" 197 | ] 198 | } 199 | ] 200 | } 201 | }, 202 | "remaining_query": "EnablePerformanceInsights", 203 | "reason": "Could not find key EnablePerformanceInsights inside struct at path /Resources/MySQLDatabase/Properties[L:496,C:20]" 204 | }, 205 | "comparison": [ 206 | "Exists", 207 | false 208 | ] 209 | } 210 | } 211 | } 212 | } 213 | }, 214 | { 215 | "Clause": { 216 | "Binary": { 217 | "context": " %rds_db[*].Properties.EnablePerformanceInsights EQUALS \"true\"", 218 | "messages": { 219 | "custom_message": "", 220 | "error_message": "Check was not compliant as property [EnablePerformanceInsights] to compare from is missing. Value traversed to [Path=/Resources/MySQLDatabase/Properties[L:496,C:20] Value={\"Engine\":\"MySQL\",\"DBName\":{\"Ref\":\"DBName\"},\"MultiAZ\":{\"Ref\":\"MultiAZDatabase\"},\"MasterUsername\":{\"Ref\":\"DBUser\"},\"MasterUserPassword\":{\"Ref\":\"DBPassword\"},\"DBInstanceClass\":{\"Ref\":\"DBInstanceClass\"},\"AllocatedStorage\":{\"Ref\":\"DBAllocatedStorage\"},\"VPCSecurityGroups\":[{\"Fn::GetAtt\":[\"DBEC2SecurityGroup\",\"GroupId\"]}]}]." 221 | }, 222 | "check": { 223 | "UnResolved": { 224 | "value": { 225 | "traversed_to": { 226 | "path": "/Resources/MySQLDatabase/Properties", 227 | "value": { 228 | "Engine": "MySQL", 229 | "DBName": { 230 | "Ref": "DBName" 231 | }, 232 | "MultiAZ": { 233 | "Ref": "MultiAZDatabase" 234 | }, 235 | "MasterUsername": { 236 | "Ref": "DBUser" 237 | }, 238 | "MasterUserPassword": { 239 | "Ref": "DBPassword" 240 | }, 241 | "DBInstanceClass": { 242 | "Ref": "DBInstanceClass" 243 | }, 244 | "AllocatedStorage": { 245 | "Ref": "DBAllocatedStorage" 246 | }, 247 | "VPCSecurityGroups": [ 248 | { 249 | "Fn::GetAtt": [ 250 | "DBEC2SecurityGroup", 251 | "GroupId" 252 | ] 253 | } 254 | ] 255 | } 256 | }, 257 | "remaining_query": "EnablePerformanceInsights", 258 | "reason": "Could not find key EnablePerformanceInsights inside struct at path /Resources/MySQLDatabase/Properties[L:496,C:20]" 259 | }, 260 | "comparison": [ 261 | "Eq", 262 | false 263 | ] 264 | } 265 | } 266 | } 267 | } 268 | } 269 | ] 270 | } 271 | } 272 | ], 273 | "not_applicable": [], 274 | "compliant": [] 275 | }{ 276 | "name": "", 277 | "metadata": {}, 278 | "status": "SKIP", 279 | "not_compliant": [], 280 | "not_applicable": [ 281 | "ensure_all_buckets_have_lifecycle_configuration" 282 | ], 283 | "compliant": [] 284 | }{ 285 | "name": "", 286 | "metadata": {}, 287 | "status": "SKIP", 288 | "not_compliant": [], 289 | "not_applicable": [ 290 | "iamuserpassword" 291 | ], 292 | "compliant": [] 293 | } -------------------------------------------------------------------------------- /susscanner/scan.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | 4 | import susscanner as ss 5 | 6 | from pathlib import Path 7 | 8 | 9 | class Scan: 10 | """The class takes output of CFN Guard in JSON format, parses it and 11 | re-formats it with additional metadata. 12 | 13 | Even though, CFN Guard runs with JSON output option, the output it 14 | provides is not valid JSON, it includes some additional lines which are 15 | not JSON. If you run validation with multiple rule files, then for each 16 | failed rule file output will look like this: 17 | 18 | = FAIL 19 | FAILED rules 20 | 21 | --- 22 | { 23 | here comes JSON output with details of failed rules 24 | }{ 25 | JSON output of the next rule file(s) with compliant rules 26 | } = FAIL 27 | FAILED rules 28 | ..... 29 | 30 | So this class finds blocks with failed rules in the output and parses 31 | them. Each JSON block has the following structure (only relevant elements 32 | are shown below): 33 | 34 | { 35 | "not_compliant": [ 36 | { 37 | "Rule": { 38 | "name": "", 39 | "checks": [ 40 | { 41 | "Clause": { 42 | "Unary/Binary": { 43 | "messages": { 44 | "error_message": "" 45 | }, 46 | "check": { 47 | "Unresolved": { 48 | "value": { 49 | "traversed_to": { 50 | "path": "" 51 | } 52 | } 53 | }, 54 | "Resolved": { 55 | "from": { 56 | "path": "" 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | ] 64 | } 65 | } 66 | ] 67 | } 68 | 69 | Blocks Unresolved and Resolved are mutually exclusive, so for the same 70 | failed check only one can be present. Insid checks array there can be 71 | element Disjunctions if rule contained disjunction and inside it there 72 | will be one more checks block. 73 | """ 74 | 75 | def load_metadata(self, rules_metadata: Path): 76 | """Loads rules metadata from metadata file. Only enabled rules are loaded. 77 | 78 | Args: 79 | rules_metadata (Path): location of the rule metadata. 80 | Will be a Path value if specefied, otherwise None. 81 | 82 | Returns: 83 | [dictionary] - keys are rule names and values are metadata object 84 | as defined in the file. 85 | """ 86 | result = {} 87 | # check if a rules metadata location is specified with -r or --rules 88 | if rules_metadata: # rules metadata is specified 89 | data = json.loads(rules_metadata.resolve().read_text()) 90 | else: # not specified, using the defaults 91 | data = json.loads(ss.CONFIG_FILE_PATH.read_text()) 92 | 93 | for group in data["all_rules"].items(): 94 | if group[1]["enabled"]: 95 | for rule in group[1]["rules"]: 96 | if rule["enabled"]: 97 | result[rule["rule_name"]] = rule 98 | return result 99 | 100 | @staticmethod 101 | def get_path(obj: dict): 102 | """This method finds path key in a dictionary, even if path is located in nested dictionaries. 103 | 104 | Args: 105 | obj: dictionary to search in 106 | 107 | Returns: 108 | value of the path key or None 109 | """ 110 | for key in obj: 111 | value = obj[key] 112 | if key == "path": 113 | return value 114 | elif type(value) is dict: 115 | return Scan.get_path(value) 116 | 117 | def parse_checks(self, checks: list): 118 | """This method parses failed checks output and returned resources part of it. Each resource contains 119 | resource name and line number in CF template. 120 | 121 | Args: 122 | checks(list): list of failed checks 123 | 124 | Returns: 125 | generator object, which returns resources which has failed the check. Resources are dictionaries containing 126 | 2 keys: line and name. 127 | """ 128 | for check in checks: 129 | # If this is disjunction rule, then recursively parse checks inside it. 130 | if "Disjunctions" in check: 131 | for sub_parse in self.parse_checks(check["Disjunctions"]["checks"]): 132 | yield sub_parse 133 | else: 134 | if "Clause" in check: 135 | clause = check["Clause"] 136 | resource = {} 137 | rule_type = "" 138 | if "Unary" in clause: 139 | rule_type = "Unary" 140 | elif "Binary" in clause: 141 | rule_type = "Binary" 142 | error_msg = clause[rule_type]["messages"]["error_message"] 143 | typed_check = clause[rule_type]["check"] 144 | resource["name"] = Scan.get_path(typed_check) 145 | 146 | # error message contains line number in the format like this: 147 | # "Value traversed to [Path=/Resources/API-GW-1/Properties[L:12,C:3]" 148 | # so the regex below just finds it. 149 | line = re.findall(r"\[L\:([0-9]*),C\:[0-9]*\]", error_msg) 150 | resource["line"] = line[0] 151 | yield resource 152 | 153 | @staticmethod 154 | def get_object_to_parse(jsons): 155 | """Returns dictionary object which should be parsed for the results. Only the first one of the inout list 156 | will be returned because remaining ones are rules which passed. 157 | 158 | Args: 159 | jsons (list): List of strings, where each element represents result of a rule file evaluation. 160 | 161 | Returns: 162 | A dict which represents output of failed rule file. 163 | """ 164 | # Take only first json as others are not applicable 165 | if len(jsons) > 1: 166 | obj = json.loads(jsons[0] + "}") 167 | else: 168 | obj = json.loads(jsons[0]) 169 | return obj 170 | 171 | def parse_failed_rules(self, rules_obj, md, failed_rules): 172 | """ 173 | This method parses output of CFN Guard for 1 rule file and add dictionary with additional metadata for each 174 | rule to failed_rules list. 175 | Args: 176 | rules_obj (dict): output of CF guard for one rule file 177 | md (dict): rules metadata dictionary 178 | failed_rules (list): list to which this method adds a dictionary for each failed rule, containing: 179 | * rule_name - rule name 180 | * severity - rule severity 181 | * message - message explaining the rule 182 | * links - links to additional material 183 | * resources - list of resources, which point to all resources in a CF template which had failed this rule 184 | Returns: 185 | """ 186 | for not_comp_rule in rules_obj["not_compliant"]: 187 | rule_name = not_comp_rule["Rule"]["name"] 188 | if rule_name in md: 189 | meta = md[rule_name] 190 | failed_rule = { 191 | "rule_name": not_comp_rule["Rule"]["name"], 192 | "severity": meta["severity"], 193 | "message": meta["message"], 194 | "links": meta["links"], 195 | "resources": list( 196 | self.parse_checks(not_comp_rule["Rule"]["checks"]) 197 | ), 198 | } 199 | failed_rules.append(failed_rule) 200 | 201 | def calculate_sustainability_score(self, failed_rules: list) -> int: 202 | """Calculate the sustainability score by providing a penalty for each 203 | rule failed based on the severity of that rule. 204 | 205 | Args: 206 | failed_rules (list): a list of all failed rules 207 | 208 | Returns: 209 | sustainability_score (int): the sustainability score, lower is better. 210 | """ 211 | sustainability_score = 0 212 | 213 | for failed_rule in failed_rules: 214 | rule_severity = failed_rule["severity"] 215 | 216 | if rule_severity == "LOW": 217 | sustainability_score += ss.SCORE_LOW 218 | elif rule_severity == "MEDIUM": 219 | sustainability_score += ss.SCORE_MEDIUM 220 | elif rule_severity == "HIGH": 221 | sustainability_score += ss.SCORE_HIGH 222 | else: 223 | raise ValueError( 224 | f"The severity level of rule {failed_rule['rule_name']} was set " 225 | + f"to {rule_severity} this type is not supported. " 226 | + "Supported types are LOW, MEDIUM, HIGH" 227 | ) 228 | 229 | return sustainability_score 230 | 231 | def parse_cfn_guard_output( 232 | self, 233 | cfn_guard_output: str, 234 | template_name: str, 235 | rules_metadata: Path, 236 | ) -> dict: 237 | """This function parses JSON output of the cfn guard. 238 | 239 | Args: 240 | cfn_guard_output (str): output produced by CFN guard in JSON format. 241 | 242 | Returns: 243 | summary dictionary object which contains failed_rules array containing 244 | objects with following structure: 245 | { 246 | "rule_name": "", 247 | "severity": "", 248 | "message": "", 249 | "links": [], 250 | "resources": [{"name": "", "line": 123}] 251 | } 252 | """ 253 | md = self.load_metadata(rules_metadata) 254 | failed_rules = [] 255 | matcher = re.match( 256 | r".*(}|^)([\d\S]+) Status = FAIL", cfn_guard_output, re.DOTALL 257 | ) 258 | if matcher: 259 | file_name = matcher.group(2) 260 | failed_pieces = cfn_guard_output.split(file_name + " ") 261 | for p in failed_pieces: 262 | delim = p.find("---") 263 | if delim >= 0: 264 | json_text = p[delim + 3 :] 265 | jsons = json_text.split("}{") 266 | if len(jsons) > 0: 267 | obj = Scan.get_object_to_parse(jsons) 268 | self.parse_failed_rules(obj, md, failed_rules) 269 | 270 | sustainability_score = self.calculate_sustainability_score(failed_rules) 271 | 272 | return { 273 | "title": "Sustainability Scanner Report", 274 | "file": template_name, 275 | "version": ss.__version__, 276 | "sustainability_score": sustainability_score, 277 | "failed_rules": failed_rules, 278 | } 279 | 280 | @classmethod 281 | def filter_results(cls, cfn_guard_output, template_name, rules_metadata): 282 | parsed = cls().parse_cfn_guard_output( 283 | cfn_guard_output, template_name, rules_metadata 284 | ) 285 | print(json.dumps(parsed, indent=4)) 286 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sustainability Scanner (SusScanner) 2 | 3 | 4 | 5 | **Validate AWS CloudFormation templates against AWS Well-Architected Sustainability Pillar best practices.** 6 | 7 | Sustainability scanner is an open source tool that helps you create a more sustainable infrastructure on AWS. It takes in your Cloudformation template as input, evaluates it against a set of sustainability best practices and generates a report with a sustainability score and suggested improvements to apply to your template. 8 | SusScanner comes with a set of rule implementations aligned to the [AWS Well-Architected Pillar for Sustainability](https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sustainability-pillar.html). However, this is not an exhaustive list and new rules will come out as the tool evolves. Furthermore, you can extend these rules (located in the `susscanner/rules` dir) in accordance with your company-specific sustainability policies. 9 | 10 | **Sustainability Scanner in action** 11 | ![demo of susscanner][demo] 12 | 13 | [demo]: https://raw.githubusercontent.com/awslabs/sustainability-scanner/main/demo.gif 14 | 15 | Scroll down to the getting started section to get detailed examples on how to use the tool. 16 | 17 | ## Table of Contents 18 | 19 | * Installation 20 | * Prerequisites 21 | * Getting Started 22 | 1. Install via pip 23 | 2. Install from source 24 | * Sustainability Score 25 | * Rule Set 26 | * Disabling Rules 27 | * Extending the rule set 28 | * FAQs 29 | * Security 30 | * License 31 | 32 | ## Installation 33 | 34 | To install Sustainability Scanner please follow the following instructions. 35 | 36 | ### Prerequisites 37 | 38 | * [AWS CloudFormation Guard](https://github.com/aws-cloudformation/cloudformation-guard) 39 | * an open-source general-purpose policy-as-code evaluation tool which SusScanner builds on top of 40 | * Python 3.6 or later 41 | * check version with `python3 -V` 42 | * [AWS CDK] (https://docs.aws.amazon.com/cdk/v2/guide/home.html), if you use CDK to define your AWS infrastructure. 43 | 44 | ### Getting Started 45 | 46 | There are two options to install the tool: 47 | 48 | ### 1. Install via pip 49 | 50 | To install the project via pip, you simply have to call 51 | 52 | ```sh 53 | pip3 install sustainability-scanner 54 | ``` 55 | 56 | #### Scanning an AWS CloudFormation Template 57 | 58 | Run `susscanner --help` to get a list of options and arguments for the tool. 59 | You should see an output like below: 60 | 61 | ```sh 62 | susscanner --help 63 | Usage: susscanner [OPTIONS] CFN_TEMPLATE... 64 | 65 | ╭─ Arguments ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ 66 | │ * cfn_template CFN_TEMPLATE... List of template names (for CloudFormation format) or stack name (for CDK format) [default: None] [required] │ 67 | ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ 68 | ╭─ Options ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ 69 | │ --version -v Show the application's version and exit. │ 70 | │ --rules -r PATH Location for a custom rules metadata file. │ 71 | │ --format -f [cf|cdk] Template format [default: cf] │ 72 | │ --help Show this message and exit. │ 73 | ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ 74 | ``` 75 | 76 | You can scan a template by using the command: 77 | 78 | ```sh 79 | susscanner [path/to/cloudformation/template_or_templates] 80 | ``` 81 | 82 | Or you can scan a CDK stack by using the command, **NB! you have to run this command in your CDK application root directory**: 83 | 84 | ```sh 85 | susscanner -f cdk 86 | ``` 87 | 88 | You should see an output like below; 89 | 90 | ```sh 91 | susscanner test.yaml 92 | { 93 | "title": "Sustainability Scanner Report", 94 | "file": "test.yaml", 95 | "version": "1.3.0", 96 | "sustainability_score": 8, 97 | "failed_rules": [ 98 | { 99 | "rule_name": "rest_api_compression_max", 100 | "severity": "MEDIUM", 101 | "message": "Consider configuring the payload compression with MinimumCompressionSize. Compressing the payload will in general reduce the network traffic.", 102 | "links": [ 103 | "https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-gzip-compression-decompression.html", 104 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_data_a8.html" 105 | ], 106 | "resources": [ 107 | { 108 | "name": "/Resources/API-GW-2/Properties/MinimumCompressionSize", 109 | "line": "15" 110 | } 111 | ] 112 | } 113 | ] 114 | } 115 | ``` 116 | 117 | If you want to use your own `rules_metadata` file you can specify one using the `-r` or `--rules` options. 118 | 119 | ### 2. Install from source 120 | #### Clone this project 121 | 122 | ```sh 123 | git clone https://github.com/awslabs/sustainability-scanner.git 124 | ``` 125 | 126 | #### Move into the project directory 127 | 128 | ```sh 129 | cd sustainability-scanner 130 | ``` 131 | 132 | #### Create and activate virtual environment (optional) 133 | 134 | ```sh 135 | # from the root directory of the project 136 | python3 -m venv .venv 137 | source .venv/bin/activate 138 | ``` 139 | 140 | #### Install dependencies 141 | 142 | ```sh 143 | python3 -m pip install -r requirements.txt 144 | ``` 145 | 146 | That's it! You're ready to use Sustainability Scanner. 147 | 148 | #### Scanning an AWS CloudFormation Template 149 | 150 | You can scan a template by using the command; 151 | 152 | ```sh 153 | #from the root directory of the project 154 | python3 -m susscanner [path/to/cloudformation/template_or_templates] 155 | ``` 156 | 157 | ## Sustainability Score 158 | 159 | After you've scanned your AWS CloudFormation template, as part of the report, you will get a Sustainability Score. It follows inverted scoring and increases your score for each best practice you can improve; the lower the score the better. Higher severity rules have a greater scope for improvement e.g. Failing a HIGH SEV rule will increase your score more than a LOW SEV rule. If you are following all the best practices or none of the rules apply to your infrastructure this score will be 0. 160 | Find the scoring by severity in the table below 161 | 162 | | SEVERITY | SCORE | 163 | | ----------- | ----------- | 164 | | LOW | 1 | 165 | | MEDIUM | 2 | 166 | | HIGH | 3 | 167 | 168 | ## Rule set 169 | 170 | SusScanner comes with a set of best practices/rules that align with [best practices for sustainability in the cloud](https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/best-practices-for-sustainability-in-the-cloud.html). You can find the list of best practices, the service they apply to and their improvement actions in the [rules_metadata.json](https://github.com/awslabs/sustainability-scanner/blob/main/susscanner/rules_metadata.json) file. 171 | 172 | ### Disabling rules 173 | 174 | As mentioned before, the tool comes with a pre-defined set of rules, all of which are enabled by default. However, you can disable a rule if it is not applicable to your setup. 175 | In the susscanner directory you can find a file called `rules_metadata.json`. This configuration file can be used to specify which rules to include. The structure of this file is as follows: 176 | 177 | ``` 178 | 01:{ 179 | 02: "all_rules": 180 | 03: { 181 | 04: "rule_on_service_level": { 182 | 05: "enabled": true, 183 | 06: "rules": [ 184 | 07: { 185 | 08: "rule_name": "name_of_the_rule", 186 | 09: "severity": "MEDIUM", 187 | 10: "message": "message_of_the_rule", 188 | 11: "enabled": true, 189 | 12: "links": [ 190 | 13: "link_1", 191 | 14: "link_2" 192 | 15: ] 193 | 16: } 194 | 17: } 195 | 18: } 196 | 19:} 197 | ``` 198 | 199 | Rules can be enabled or disabled on both a service level and rule level. If you want to disable the checks for a service, for example Amazon Elastic Compute Cloud (EC2), you can set `enabled` to `false` on line 5 of the example above. Since a service can have multiple rules you can opt to disable rules on a per rule base. This can be done by setting `enabled` to `false`, in the example shown on line 11. 200 | 201 | ### Extending the rule set 202 | 203 | If you wish to extend the pre-existing set of rules you can define your own by adding AWS CloudFormation Guard rules to the `susscanner/rules` directory. For each rule that you add, don't forget to add test cases to validate it. You can [validate](https://docs.aws.amazon.com/cfn-guard/latest/ug/validating-rules.html) a rule by running: 204 | 205 | ```sh 206 | cfn-guard test --rules-file ./susscanner/rules/ --test-data ./susscanner/rules/test_cases/ 207 | ``` 208 | 209 | AWS CloudFormation Guard uses a domain-specific language (DSL) to define the rules. More information can be found at the [AWS CloudFormation Guard documentation page](https://docs.aws.amazon.com/cfn-guard/latest/ug/writing-rules.html). When defining a new rule there are 2 requirements to ensure compatibility with the Sustainability Scanner project. 210 | 211 | 1. Rules `FAIL` when the resulting state is not desirable in terms of sustainability and `PASS` when the outcome is sustainable. 212 | 2. Add the rule created in the `susscanner/rules` directory to the `rules_metadata.json` file. Define the name of the rule in the `rule_name` variable. 213 | 214 | ## FAQs 215 | 216 | ### Are all the recommendations mandatory to implement? 217 | 218 | No, the recommendations are not mandatory to implement, if you categorize a best practice as not applicable or prefer the status quo given your workload, you can choose to either ignore the failed rule or disable it. 219 | 220 | ### What happens if there are no suggested improvements? 221 | 222 | You will get a Sustainability Scanner Report without failed rules. This looks as follows: 223 | 224 | ``` 225 | { 226 | "title": "Sustainability Scanner Report", 227 | "file": "cloudformation.yaml", 228 | "version": "1.3.0", 229 | "sustainability_score": 0, 230 | "failed_rules": [] 231 | } 232 | ``` 233 | 234 | ### Can I use it as part of a Github workflow? 235 | 236 | Yes, a Github Action to run the scanner is available on the [marketplace](https://github.com/marketplace/actions/aws-sustainability-scanner-github-action). 237 | 238 | ## Security 239 | 240 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 241 | 242 | ## License 243 | This project is licensed under the MIT-0 License. 244 | -------------------------------------------------------------------------------- /susscanner/rules_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "all_rules": { 3 | "api_gw": { 4 | "enabled": true, 5 | "rules": [ 6 | { 7 | "rule_name": "rest_api_compression_exists", 8 | "severity": "MEDIUM", 9 | "message": "Consider configuring the payload compression with MinimumCompressionSize. Compressing the payload will in general reduce the network traffic. However, compressing data of a small size might actually increase final data size.", 10 | "enabled": true, 11 | "links": [ 12 | "https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-gzip-compression-decompression.html", 13 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_data_a8.html" 14 | ] 15 | }, 16 | { 17 | "rule_name": "rest_api_compression_min", 18 | "severity": "MEDIUM", 19 | "message": "Consider configuring the payload compression with MinimumCompressionSize. Compressing the payload will in general reduce the network traffic. However, compressing data of a small size might actually increase final data size.", 20 | "enabled": true, 21 | "links": [ 22 | "https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-gzip-compression-decompression.html", 23 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_data_a8.html" 24 | ] 25 | }, 26 | { 27 | "rule_name": "rest_api_compression_max", 28 | "severity": "MEDIUM", 29 | "message": "Consider configuring the payload compression with MinimumCompressionSize. Compressing the payload will in general reduce the network traffic.", 30 | "enabled": true, 31 | "links": [ 32 | "https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-gzip-compression-decompression.html", 33 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_data_a8.html" 34 | ] 35 | } 36 | ] 37 | }, 38 | "cloud_front": { 39 | "enabled": true, 40 | "rules" : [ 41 | { 42 | "rule_name": "ensure_default_compression_on", 43 | "severity": "MEDIUM", 44 | "message": "Amazon CloudFront can automatically compress certain type of files when serving them to clients. When requested objects are compressed, downloads can be faster and less network bandwidth is consumed. It is better to have compression of for default configuration.", 45 | "enabled": true, 46 | "links": [ 47 | "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/ServingCompressedFiles.html", 48 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_data_a8.html" 49 | ] 50 | }, 51 | { 52 | "rule_name": "ensure_compression_on", 53 | "severity": "MEDIUM", 54 | "message": "Amazon CloudFront can automatically compress certain type of files when serving them to clients. When requested objects are compressed, downloads can be faster and less network bandwidth is consumed.", 55 | "enabled": true, 56 | "links": [ 57 | "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/ServingCompressedFiles.html", 58 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_data_a8.html" 59 | ] 60 | }, 61 | { 62 | "rule_name": "check_default_ttl", 63 | "severity": "MEDIUM", 64 | "message": "For Amazon Cloudfront caching, it is recommended to keep DefaultTTL above 24h, if possible. To avoid too frequent data calls to origin.", 65 | "enabled": true, 66 | "links": [ 67 | "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Expiration.html", 68 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_user_a5.html" 69 | ] 70 | }, 71 | { 72 | "rule_name": "check_max_ttl", 73 | "severity": "MEDIUM", 74 | "message": "For Amazon Cloudfront caching, it is recommended to keep MaxTTL above 24h, if possible. To avoid too frequent data calls to origin.", 75 | "enabled": true, 76 | "links": [ 77 | "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Expiration.html", 78 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_user_a5.html" 79 | ] 80 | } 81 | ] 82 | }, 83 | "cloudwatch": { 84 | "enabled": true, 85 | "rules": [ 86 | { 87 | "rule_name": "ensure_all_loggroups_have_retention", 88 | "severity": "HIGH", 89 | "message": "Amazon CloudWatch by default keeps log indefinitely. Consider defining the retention time explicitly.", 90 | "enabled": true, 91 | "links": [ 92 | "https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/Working-with-log-groups-and-streams.html#SettingLogRetention", 93 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_data_a6.html" 94 | ] 95 | } 96 | ] 97 | }, 98 | "codeguru": { 99 | "enabled": true, 100 | "rules": [ 101 | { 102 | "rule_name": "check_codeguru_association", 103 | "severity": "LOW", 104 | "message": "With Amazon CodeGuru you can find areas of code that consume most time or resources. You will be charged for reviewing code using Amazon CodeGuru.", 105 | "enabled": true, 106 | "links": [ 107 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_software_a4.html", 108 | "https://aws.amazon.com/codeguru/pricing" 109 | ] 110 | } 111 | ] 112 | }, 113 | "ec2": { 114 | "enabled": true, 115 | "rules": [ 116 | { 117 | "rule_name": "from_port_is_ssh", 118 | "severity": "MEDIUM", 119 | "message": "Consider using AWS Systems Manager to access your Amazon EC2 instances instead of direct access or bastion host.", 120 | "enabled": true, 121 | "links": [ 122 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_dev_a4.html" 123 | ] 124 | }, 125 | { 126 | "rule_name": "to_port_is_ssh", 127 | "severity": "MEDIUM", 128 | "message": "Consider using AWS Systems Manager to access your Amazon EC2 instances instead of direct access or bastion host.", 129 | "enabled": true, 130 | "links": [ 131 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_dev_a4.html" 132 | ] 133 | }, 134 | { 135 | "rule_name": "port_range_includes_ssh", 136 | "severity": "MEDIUM", 137 | "message": "Consider using AWS Systems Manager to access your Amazon EC2 instances instead of direct access or bastion host.", 138 | "enabled": true, 139 | "links": [ 140 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_dev_a4.html" 141 | ] 142 | } 143 | ] 144 | }, 145 | "emr": { 146 | "enabled": true, 147 | "rules": [ 148 | { 149 | "rule_name": "emr_use_spot", 150 | "severity": "MEDIUM", 151 | "message": "Use spot instances, if possible to maximize the utilization of compute resources and reduce the impact of unused resources.", 152 | "enabled": true, 153 | "links": [ 154 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_hardware_a3.html" 155 | ] 156 | }, 157 | { 158 | "rule_name": "emr_target_spot_config_configured", 159 | "severity": "MEDIUM", 160 | "message": "Use spot instances, if possible to maximize the utilization of compute resources and reduce the impact of unused resources.", 161 | "enabled": true, 162 | "links": [ 163 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_hardware_a3.html" 164 | ] 165 | } 166 | ] 167 | }, 168 | "glue": { 169 | "enabled": true, 170 | "rules": [ 171 | { 172 | "rule_name": "check_glue_job_timeout", 173 | "severity": "MEDIUM", 174 | "message": "AWS Glue jobs' default timeout is 48 hrs, for shorter jobs, reduce the default timeout", 175 | "enabled": true, 176 | "links": [ 177 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_software_a4.html", 178 | "https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-jobs-job.html" 179 | ] 180 | }, 181 | { 182 | "rule_name": "check_glue_job_workernodes", 183 | "severity": "MEDIUM", 184 | "message": "By default number of workers is 10. You can reduce it, if your job does not need that many workers to reduce impact.", 185 | "enabled": true, 186 | "links": [ 187 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_hardware_a2.html", 188 | "https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-jobs-job.html" 189 | ] 190 | }, 191 | { 192 | "rule_name": "check_glue_job_allocatedcapacity", 193 | "severity": "MEDIUM", 194 | "message": "Consider limiting allocated capacity to use workers proportional to job demand to reduce impact. This field is now deprecated. For Glue version 1.0 jobs, Use MaxCapacity instead.", 195 | "enabled": true, 196 | "links": [ 197 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_hardware_a2.html", 198 | "https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-jobs-job.html" 199 | ] 200 | }, 201 | { 202 | "rule_name": "check_glue_job_maxcapacity", 203 | "severity": "MEDIUM", 204 | "message": "For Glue version 1.0 jobs you can limit max capacity to reduce impact. For Glue version 2.0 jobs, you cannot specify a Maximum capacity. Instead, you should specify a Worker type and the Number of workers.", 205 | "enabled": true, 206 | "links": [ 207 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_hardware_a2.html", 208 | "https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-jobs-job.html" 209 | ] 210 | } 211 | ] 212 | }, 213 | "graviton": { 214 | "enabled": true, 215 | "rules": [ 216 | { 217 | "rule_name": "check_graviton_instance_usage_in_rds", 218 | "severity": "MEDIUM", 219 | "message": "Consider selecting the AWS Graviton based instances in your usage of AWS managed services (e.g. Amazon Relational Database Service) to take advantage of energy efficiency improvements.", 220 | "enabled": true, 221 | "links": [ 222 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_hardware_a3.html", 223 | "https://aws.amazon.com/blogs/database/key-considerations-in-moving-to-graviton2-for-amazon-rds-and-amazon-aurora-databases/", 224 | "https://aws.amazon.com/blogs/aws/new-amazon-rds-on-graviton2-processors/" 225 | ] 226 | }, 227 | { 228 | "rule_name": "check_graviton_architecture_usage_in_lambda", 229 | "severity": "MEDIUM", 230 | "message": "Use AWS Graviton for Lambda functions, if possible, to take advantage of energy efficiency improvements.", 231 | "enabled": true, 232 | "links": [ 233 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_hardware_a3.html", 234 | "https://aws.amazon.com/blogs/aws/aws-lambda-functions-powered-by-aws-graviton2-processor-run-your-functions-on-arm-and-get-up-to-34-better-price-performance/" 235 | ] 236 | } 237 | ] 238 | }, 239 | "rds": { 240 | "enabled": true, 241 | "rules": [ 242 | { 243 | "rule_name": "check_rds_performanceinsights_enabled", 244 | "severity": "MEDIUM", 245 | "message": "Amazon RDS Performance insights helps you quickly assess the load on your database, and determine when and where to take action. You will be charged for analyzing longer-term (more than 7 days) performance trends.", 246 | "enabled": true, 247 | "links": [ 248 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_software_a6.html", 249 | "https://aws.amazon.com/rds/performance-insights/", 250 | "https://aws.amazon.com/rds/performance-insights/pricing/" 251 | ] 252 | } 253 | ] 254 | }, 255 | "s3": { 256 | "enabled": true, 257 | "rules": [ 258 | { 259 | "rule_name": "ensure_all_buckets_have_lifecycle_configuration", 260 | "severity": "MEDIUM", 261 | "message": "Amazon S3 Lifecycle policies will either delete data or move data between different storage classes based on data access patterns.", 262 | "enabled": true, 263 | "links": [ 264 | "https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_data_a4.html", 265 | "https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html" 266 | ] 267 | } 268 | ] 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /demo_cloudformation.yaml: -------------------------------------------------------------------------------- 1 | Resources: 2 | MyVpcF9F0CA6F: 3 | Type: AWS::EC2::VPC 4 | Properties: 5 | CidrBlock: 10.0.0.0/16 6 | EnableDnsHostnames: true 7 | EnableDnsSupport: true 8 | InstanceTenancy: default 9 | Tags: 10 | - Key: Name 11 | Value: USPythonStack/MyVpc 12 | Metadata: 13 | aws:cdk:path: USPythonStack/MyVpc/Resource 14 | MyVpcPublicSubnet1SubnetF6608456: 15 | Type: AWS::EC2::Subnet 16 | Properties: 17 | AvailabilityZone: 18 | Fn::Select: 19 | - 0 20 | - Fn::GetAZs: "" 21 | CidrBlock: 10.0.0.0/18 22 | MapPublicIpOnLaunch: true 23 | Tags: 24 | - Key: aws-cdk:subnet-name 25 | Value: Public 26 | - Key: aws-cdk:subnet-type 27 | Value: Public 28 | - Key: Name 29 | Value: USPythonStack/MyVpc/PublicSubnet1 30 | VpcId: 31 | Ref: MyVpcF9F0CA6F 32 | Metadata: 33 | aws:cdk:path: USPythonStack/MyVpc/PublicSubnet1/Subnet 34 | MyVpcPublicSubnet1RouteTableC46AB2F4: 35 | Type: AWS::EC2::RouteTable 36 | Properties: 37 | Tags: 38 | - Key: Name 39 | Value: USPythonStack/MyVpc/PublicSubnet1 40 | VpcId: 41 | Ref: MyVpcF9F0CA6F 42 | Metadata: 43 | aws:cdk:path: USPythonStack/MyVpc/PublicSubnet1/RouteTable 44 | MyVpcPublicSubnet1RouteTableAssociation2ECEE1CB: 45 | Type: AWS::EC2::SubnetRouteTableAssociation 46 | Properties: 47 | RouteTableId: 48 | Ref: MyVpcPublicSubnet1RouteTableC46AB2F4 49 | SubnetId: 50 | Ref: MyVpcPublicSubnet1SubnetF6608456 51 | Metadata: 52 | aws:cdk:path: USPythonStack/MyVpc/PublicSubnet1/RouteTableAssociation 53 | MyVpcPublicSubnet1DefaultRoute95FDF9EB: 54 | Type: AWS::EC2::Route 55 | Properties: 56 | DestinationCidrBlock: 0.0.0.0/0 57 | GatewayId: 58 | Ref: MyVpcIGW5C4A4F63 59 | RouteTableId: 60 | Ref: MyVpcPublicSubnet1RouteTableC46AB2F4 61 | DependsOn: 62 | - MyVpcVPCGW488ACE0D 63 | Metadata: 64 | aws:cdk:path: USPythonStack/MyVpc/PublicSubnet1/DefaultRoute 65 | MyVpcPublicSubnet1EIP096967CB: 66 | Type: AWS::EC2::EIP 67 | Properties: 68 | Domain: vpc 69 | Tags: 70 | - Key: Name 71 | Value: USPythonStack/MyVpc/PublicSubnet1 72 | Metadata: 73 | aws:cdk:path: USPythonStack/MyVpc/PublicSubnet1/EIP 74 | MyVpcPublicSubnet1NATGatewayAD3400C1: 75 | Type: AWS::EC2::NatGateway 76 | Properties: 77 | AllocationId: 78 | Fn::GetAtt: 79 | - MyVpcPublicSubnet1EIP096967CB 80 | - AllocationId 81 | SubnetId: 82 | Ref: MyVpcPublicSubnet1SubnetF6608456 83 | Tags: 84 | - Key: Name 85 | Value: USPythonStack/MyVpc/PublicSubnet1 86 | DependsOn: 87 | - MyVpcPublicSubnet1DefaultRoute95FDF9EB 88 | - MyVpcPublicSubnet1RouteTableAssociation2ECEE1CB 89 | Metadata: 90 | aws:cdk:path: USPythonStack/MyVpc/PublicSubnet1/NATGateway 91 | MyVpcPublicSubnet2Subnet492B6BFB: 92 | Type: AWS::EC2::Subnet 93 | Properties: 94 | AvailabilityZone: 95 | Fn::Select: 96 | - 1 97 | - Fn::GetAZs: "" 98 | CidrBlock: 10.0.64.0/18 99 | MapPublicIpOnLaunch: true 100 | Tags: 101 | - Key: aws-cdk:subnet-name 102 | Value: Public 103 | - Key: aws-cdk:subnet-type 104 | Value: Public 105 | - Key: Name 106 | Value: USPythonStack/MyVpc/PublicSubnet2 107 | VpcId: 108 | Ref: MyVpcF9F0CA6F 109 | Metadata: 110 | aws:cdk:path: USPythonStack/MyVpc/PublicSubnet2/Subnet 111 | MyVpcPublicSubnet2RouteTable1DF17386: 112 | Type: AWS::EC2::RouteTable 113 | Properties: 114 | Tags: 115 | - Key: Name 116 | Value: USPythonStack/MyVpc/PublicSubnet2 117 | VpcId: 118 | Ref: MyVpcF9F0CA6F 119 | Metadata: 120 | aws:cdk:path: USPythonStack/MyVpc/PublicSubnet2/RouteTable 121 | MyVpcPublicSubnet2RouteTableAssociation227DE78D: 122 | Type: AWS::EC2::SubnetRouteTableAssociation 123 | Properties: 124 | RouteTableId: 125 | Ref: MyVpcPublicSubnet2RouteTable1DF17386 126 | SubnetId: 127 | Ref: MyVpcPublicSubnet2Subnet492B6BFB 128 | Metadata: 129 | aws:cdk:path: USPythonStack/MyVpc/PublicSubnet2/RouteTableAssociation 130 | MyVpcPublicSubnet2DefaultRoute052936F6: 131 | Type: AWS::EC2::Route 132 | Properties: 133 | DestinationCidrBlock: 0.0.0.0/0 134 | GatewayId: 135 | Ref: MyVpcIGW5C4A4F63 136 | RouteTableId: 137 | Ref: MyVpcPublicSubnet2RouteTable1DF17386 138 | DependsOn: 139 | - MyVpcVPCGW488ACE0D 140 | Metadata: 141 | aws:cdk:path: USPythonStack/MyVpc/PublicSubnet2/DefaultRoute 142 | MyVpcPublicSubnet2EIP8CCBA239: 143 | Type: AWS::EC2::EIP 144 | Properties: 145 | Domain: vpc 146 | Tags: 147 | - Key: Name 148 | Value: USPythonStack/MyVpc/PublicSubnet2 149 | Metadata: 150 | aws:cdk:path: USPythonStack/MyVpc/PublicSubnet2/EIP 151 | MyVpcPublicSubnet2NATGateway91BFBEC9: 152 | Type: AWS::EC2::NatGateway 153 | Properties: 154 | AllocationId: 155 | Fn::GetAtt: 156 | - MyVpcPublicSubnet2EIP8CCBA239 157 | - AllocationId 158 | SubnetId: 159 | Ref: MyVpcPublicSubnet2Subnet492B6BFB 160 | Tags: 161 | - Key: Name 162 | Value: USPythonStack/MyVpc/PublicSubnet2 163 | DependsOn: 164 | - MyVpcPublicSubnet2DefaultRoute052936F6 165 | - MyVpcPublicSubnet2RouteTableAssociation227DE78D 166 | Metadata: 167 | aws:cdk:path: USPythonStack/MyVpc/PublicSubnet2/NATGateway 168 | MyVpcPrivateSubnet1Subnet5057CF7E: 169 | Type: AWS::EC2::Subnet 170 | Properties: 171 | AvailabilityZone: 172 | Fn::Select: 173 | - 0 174 | - Fn::GetAZs: "" 175 | CidrBlock: 10.0.128.0/18 176 | MapPublicIpOnLaunch: false 177 | Tags: 178 | - Key: aws-cdk:subnet-name 179 | Value: Private 180 | - Key: aws-cdk:subnet-type 181 | Value: Private 182 | - Key: Name 183 | Value: USPythonStack/MyVpc/PrivateSubnet1 184 | VpcId: 185 | Ref: MyVpcF9F0CA6F 186 | Metadata: 187 | aws:cdk:path: USPythonStack/MyVpc/PrivateSubnet1/Subnet 188 | MyVpcPrivateSubnet1RouteTable8819E6E2: 189 | Type: AWS::EC2::RouteTable 190 | Properties: 191 | Tags: 192 | - Key: Name 193 | Value: USPythonStack/MyVpc/PrivateSubnet1 194 | VpcId: 195 | Ref: MyVpcF9F0CA6F 196 | Metadata: 197 | aws:cdk:path: USPythonStack/MyVpc/PrivateSubnet1/RouteTable 198 | MyVpcPrivateSubnet1RouteTableAssociation56D38C7E: 199 | Type: AWS::EC2::SubnetRouteTableAssociation 200 | Properties: 201 | RouteTableId: 202 | Ref: MyVpcPrivateSubnet1RouteTable8819E6E2 203 | SubnetId: 204 | Ref: MyVpcPrivateSubnet1Subnet5057CF7E 205 | Metadata: 206 | aws:cdk:path: USPythonStack/MyVpc/PrivateSubnet1/RouteTableAssociation 207 | MyVpcPrivateSubnet1DefaultRouteA8CDE2FA: 208 | Type: AWS::EC2::Route 209 | Properties: 210 | DestinationCidrBlock: 0.0.0.0/0 211 | NatGatewayId: 212 | Ref: MyVpcPublicSubnet1NATGatewayAD3400C1 213 | RouteTableId: 214 | Ref: MyVpcPrivateSubnet1RouteTable8819E6E2 215 | Metadata: 216 | aws:cdk:path: USPythonStack/MyVpc/PrivateSubnet1/DefaultRoute 217 | MyVpcPrivateSubnet2Subnet0040C983: 218 | Type: AWS::EC2::Subnet 219 | Properties: 220 | AvailabilityZone: 221 | Fn::Select: 222 | - 1 223 | - Fn::GetAZs: "" 224 | CidrBlock: 10.0.192.0/18 225 | MapPublicIpOnLaunch: false 226 | Tags: 227 | - Key: aws-cdk:subnet-name 228 | Value: Private 229 | - Key: aws-cdk:subnet-type 230 | Value: Private 231 | - Key: Name 232 | Value: USPythonStack/MyVpc/PrivateSubnet2 233 | VpcId: 234 | Ref: MyVpcF9F0CA6F 235 | Metadata: 236 | aws:cdk:path: USPythonStack/MyVpc/PrivateSubnet2/Subnet 237 | MyVpcPrivateSubnet2RouteTableCEDCEECE: 238 | Type: AWS::EC2::RouteTable 239 | Properties: 240 | Tags: 241 | - Key: Name 242 | Value: USPythonStack/MyVpc/PrivateSubnet2 243 | VpcId: 244 | Ref: MyVpcF9F0CA6F 245 | Metadata: 246 | aws:cdk:path: USPythonStack/MyVpc/PrivateSubnet2/RouteTable 247 | MyVpcPrivateSubnet2RouteTableAssociation86A610DA: 248 | Type: AWS::EC2::SubnetRouteTableAssociation 249 | Properties: 250 | RouteTableId: 251 | Ref: MyVpcPrivateSubnet2RouteTableCEDCEECE 252 | SubnetId: 253 | Ref: MyVpcPrivateSubnet2Subnet0040C983 254 | Metadata: 255 | aws:cdk:path: USPythonStack/MyVpc/PrivateSubnet2/RouteTableAssociation 256 | MyVpcPrivateSubnet2DefaultRoute9CE96294: 257 | Type: AWS::EC2::Route 258 | Properties: 259 | DestinationCidrBlock: 0.0.0.0/0 260 | NatGatewayId: 261 | Ref: MyVpcPublicSubnet2NATGateway91BFBEC9 262 | RouteTableId: 263 | Ref: MyVpcPrivateSubnet2RouteTableCEDCEECE 264 | Metadata: 265 | aws:cdk:path: USPythonStack/MyVpc/PrivateSubnet2/DefaultRoute 266 | MyVpcIGW5C4A4F63: 267 | Type: AWS::EC2::InternetGateway 268 | Properties: 269 | Tags: 270 | - Key: Name 271 | Value: USPythonStack/MyVpc 272 | Metadata: 273 | aws:cdk:path: USPythonStack/MyVpc/IGW 274 | MyVpcVPCGW488ACE0D: 275 | Type: AWS::EC2::VPCGatewayAttachment 276 | Properties: 277 | InternetGatewayId: 278 | Ref: MyVpcIGW5C4A4F63 279 | VpcId: 280 | Ref: MyVpcF9F0CA6F 281 | Metadata: 282 | aws:cdk:path: USPythonStack/MyVpc/VPCGW 283 | MyVpcRestrictDefaultSecurityGroupCustomResourceA4FCCD62: 284 | Type: Custom::VpcRestrictDefaultSG 285 | Properties: 286 | ServiceToken: 287 | Fn::GetAtt: 288 | - CustomVpcRestrictDefaultSGCustomResourceProviderHandlerDC833E5E 289 | - Arn 290 | DefaultSecurityGroupId: 291 | Fn::GetAtt: 292 | - MyVpcF9F0CA6F 293 | - DefaultSecurityGroup 294 | Account: 295 | Ref: AWS::AccountId 296 | UpdateReplacePolicy: Delete 297 | DeletionPolicy: Delete 298 | Metadata: 299 | aws:cdk:path: USPythonStack/MyVpc/RestrictDefaultSecurityGroupCustomResource/Default 300 | CustomVpcRestrictDefaultSGCustomResourceProviderRole26592FE0: 301 | Type: AWS::IAM::Role 302 | Properties: 303 | AssumeRolePolicyDocument: 304 | Version: "2012-10-17" 305 | Statement: 306 | - Action: sts:AssumeRole 307 | Effect: Allow 308 | Principal: 309 | Service: lambda.amazonaws.com 310 | ManagedPolicyArns: 311 | - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 312 | Policies: 313 | - PolicyName: Inline 314 | PolicyDocument: 315 | Version: "2012-10-17" 316 | Statement: 317 | - Effect: Allow 318 | Action: 319 | - ec2:AuthorizeSecurityGroupIngress 320 | - ec2:AuthorizeSecurityGroupEgress 321 | - ec2:RevokeSecurityGroupIngress 322 | - ec2:RevokeSecurityGroupEgress 323 | Resource: 324 | - Fn::Join: 325 | - "" 326 | - - "arn:aws:ec2:us-west-1:" 327 | - Ref: AWS::AccountId 328 | - :security-group/ 329 | - Fn::GetAtt: 330 | - MyVpcF9F0CA6F 331 | - DefaultSecurityGroup 332 | Metadata: 333 | aws:cdk:path: USPythonStack/Custom::VpcRestrictDefaultSGCustomResourceProvider/Role 334 | CustomVpcRestrictDefaultSGCustomResourceProviderHandlerDC833E5E: 335 | Type: AWS::Lambda::Function 336 | Properties: 337 | Code: 338 | S3Bucket: 339 | Fn::Sub: cdk-hnb659fds-assets-${AWS::AccountId}-us-west-1 340 | S3Key: ee7de53d64cc9d6248fa6aa550f92358f6c907b5efd6f3298aeab1b5e7ea358a.zip 341 | Timeout: 900 342 | MemorySize: 128 343 | Handler: __entrypoint__.handler 344 | Role: 345 | Fn::GetAtt: 346 | - CustomVpcRestrictDefaultSGCustomResourceProviderRole26592FE0 347 | - Arn 348 | Runtime: nodejs18.x 349 | Description: Lambda function for removing all inbound/outbound rules from the VPC default security group 350 | DependsOn: 351 | - CustomVpcRestrictDefaultSGCustomResourceProviderRole26592FE0 352 | Metadata: 353 | aws:cdk:path: USPythonStack/Custom::VpcRestrictDefaultSGCustomResourceProvider/Handler 354 | aws:asset:path: asset.ee7de53d64cc9d6248fa6aa550f92358f6c907b5efd6f3298aeab1b5e7ea358a 355 | aws:asset:property: Code 356 | MyCluster4C1BA579: 357 | Type: AWS::ECS::Cluster 358 | Metadata: 359 | aws:cdk:path: USPythonStack/MyCluster/Resource 360 | MyFargateServiceLBDE830E97: 361 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 362 | Properties: 363 | LoadBalancerAttributes: 364 | - Key: deletion_protection.enabled 365 | Value: "false" 366 | Scheme: internet-facing 367 | SecurityGroups: 368 | - Fn::GetAtt: 369 | - MyFargateServiceLBSecurityGroup6FBF16F1 370 | - GroupId 371 | Subnets: 372 | - Ref: MyVpcPublicSubnet1SubnetF6608456 373 | - Ref: MyVpcPublicSubnet2Subnet492B6BFB 374 | Type: application 375 | DependsOn: 376 | - MyVpcPublicSubnet1DefaultRoute95FDF9EB 377 | - MyVpcPublicSubnet1RouteTableAssociation2ECEE1CB 378 | - MyVpcPublicSubnet2DefaultRoute052936F6 379 | - MyVpcPublicSubnet2RouteTableAssociation227DE78D 380 | Metadata: 381 | aws:cdk:path: USPythonStack/MyFargateService/LB/Resource 382 | MyFargateServiceLBSecurityGroup6FBF16F1: 383 | Type: AWS::EC2::SecurityGroup 384 | Properties: 385 | GroupDescription: Automatically created Security Group for ELB USPythonStackMyFargateServiceLB4CC9FCBD 386 | SecurityGroupIngress: 387 | - CidrIp: 0.0.0.0/0 388 | Description: Allow from anyone on port 80 389 | FromPort: 80 390 | IpProtocol: tcp 391 | ToPort: 80 392 | VpcId: 393 | Ref: MyVpcF9F0CA6F 394 | Metadata: 395 | aws:cdk:path: USPythonStack/MyFargateService/LB/SecurityGroup/Resource 396 | MyFargateServiceLBSecurityGrouptoUSPythonStackMyFargateServiceSecurityGroup09E9FC6380C400C276: 397 | Type: AWS::EC2::SecurityGroupEgress 398 | Properties: 399 | Description: Load balancer to target 400 | DestinationSecurityGroupId: 401 | Fn::GetAtt: 402 | - MyFargateServiceSecurityGroup7016792A 403 | - GroupId 404 | FromPort: 80 405 | GroupId: 406 | Fn::GetAtt: 407 | - MyFargateServiceLBSecurityGroup6FBF16F1 408 | - GroupId 409 | IpProtocol: tcp 410 | ToPort: 80 411 | Metadata: 412 | aws:cdk:path: USPythonStack/MyFargateService/LB/SecurityGroup/to USPythonStackMyFargateServiceSecurityGroup09E9FC63:80 413 | MyFargateServiceLBPublicListener61A1042F: 414 | Type: AWS::ElasticLoadBalancingV2::Listener 415 | Properties: 416 | DefaultActions: 417 | - TargetGroupArn: 418 | Ref: MyFargateServiceLBPublicListenerECSGroup4A3EDF05 419 | Type: forward 420 | LoadBalancerArn: 421 | Ref: MyFargateServiceLBDE830E97 422 | Port: 80 423 | Protocol: HTTP 424 | Metadata: 425 | aws:cdk:path: USPythonStack/MyFargateService/LB/PublicListener/Resource 426 | MyFargateServiceLBPublicListenerECSGroup4A3EDF05: 427 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 428 | Properties: 429 | Port: 80 430 | Protocol: HTTP 431 | TargetGroupAttributes: 432 | - Key: stickiness.enabled 433 | Value: "false" 434 | TargetType: ip 435 | VpcId: 436 | Ref: MyVpcF9F0CA6F 437 | Metadata: 438 | aws:cdk:path: USPythonStack/MyFargateService/LB/PublicListener/ECSGroup/Resource 439 | MyFargateServiceTaskDefTaskRole62C7D397: 440 | Type: AWS::IAM::Role 441 | Properties: 442 | AssumeRolePolicyDocument: 443 | Statement: 444 | - Action: sts:AssumeRole 445 | Effect: Allow 446 | Principal: 447 | Service: ecs-tasks.amazonaws.com 448 | Version: "2012-10-17" 449 | Metadata: 450 | aws:cdk:path: USPythonStack/MyFargateService/TaskDef/TaskRole/Resource 451 | MyFargateServiceTaskDef5DA17B39: 452 | Type: AWS::ECS::TaskDefinition 453 | Properties: 454 | ContainerDefinitions: 455 | - Essential: true 456 | Image: amazon/amazon-ecs-sample 457 | LogConfiguration: 458 | LogDriver: awslogs 459 | Options: 460 | awslogs-group: 461 | Ref: MyFargateServiceTaskDefwebLogGroup4A6C44E8 462 | awslogs-stream-prefix: MyFargateService 463 | awslogs-region: us-west-1 464 | Name: web 465 | PortMappings: 466 | - ContainerPort: 80 467 | Protocol: tcp 468 | Cpu: "512" 469 | ExecutionRoleArn: 470 | Fn::GetAtt: 471 | - MyFargateServiceTaskDefExecutionRoleD6305504 472 | - Arn 473 | Family: USPythonStackMyFargateServiceTaskDef6C73336F 474 | Memory: "2048" 475 | NetworkMode: awsvpc 476 | RequiresCompatibilities: 477 | - FARGATE 478 | TaskRoleArn: 479 | Fn::GetAtt: 480 | - MyFargateServiceTaskDefTaskRole62C7D397 481 | - Arn 482 | Metadata: 483 | aws:cdk:path: USPythonStack/MyFargateService/TaskDef/Resource 484 | MyFargateServiceTaskDefwebLogGroup4A6C44E8: 485 | Type: AWS::Logs::LogGroup 486 | UpdateReplacePolicy: Retain 487 | DeletionPolicy: Retain 488 | Metadata: 489 | aws:cdk:path: USPythonStack/MyFargateService/TaskDef/web/LogGroup/Resource 490 | MyFargateServiceTaskDefExecutionRoleD6305504: 491 | Type: AWS::IAM::Role 492 | Properties: 493 | AssumeRolePolicyDocument: 494 | Statement: 495 | - Action: sts:AssumeRole 496 | Effect: Allow 497 | Principal: 498 | Service: ecs-tasks.amazonaws.com 499 | Version: "2012-10-17" 500 | Metadata: 501 | aws:cdk:path: USPythonStack/MyFargateService/TaskDef/ExecutionRole/Resource 502 | MyFargateServiceTaskDefExecutionRoleDefaultPolicyEC22B20F: 503 | Type: AWS::IAM::Policy 504 | Properties: 505 | PolicyDocument: 506 | Statement: 507 | - Action: 508 | - logs:CreateLogStream 509 | - logs:PutLogEvents 510 | Effect: Allow 511 | Resource: 512 | Fn::GetAtt: 513 | - MyFargateServiceTaskDefwebLogGroup4A6C44E8 514 | - Arn 515 | Version: "2012-10-17" 516 | PolicyName: MyFargateServiceTaskDefExecutionRoleDefaultPolicyEC22B20F 517 | Roles: 518 | - Ref: MyFargateServiceTaskDefExecutionRoleD6305504 519 | Metadata: 520 | aws:cdk:path: USPythonStack/MyFargateService/TaskDef/ExecutionRole/DefaultPolicy/Resource 521 | MyFargateServiceF490C034: 522 | Type: AWS::ECS::Service 523 | Properties: 524 | Cluster: 525 | Ref: MyCluster4C1BA579 526 | DeploymentConfiguration: 527 | Alarms: 528 | AlarmNames: [] 529 | Enable: false 530 | Rollback: false 531 | MaximumPercent: 200 532 | MinimumHealthyPercent: 50 533 | DesiredCount: 6 534 | EnableECSManagedTags: false 535 | HealthCheckGracePeriodSeconds: 60 536 | LaunchType: FARGATE 537 | LoadBalancers: 538 | - ContainerName: web 539 | ContainerPort: 80 540 | TargetGroupArn: 541 | Ref: MyFargateServiceLBPublicListenerECSGroup4A3EDF05 542 | NetworkConfiguration: 543 | AwsvpcConfiguration: 544 | AssignPublicIp: DISABLED 545 | SecurityGroups: 546 | - Fn::GetAtt: 547 | - MyFargateServiceSecurityGroup7016792A 548 | - GroupId 549 | Subnets: 550 | - Ref: MyVpcPrivateSubnet1Subnet5057CF7E 551 | - Ref: MyVpcPrivateSubnet2Subnet0040C983 552 | TaskDefinition: 553 | Ref: MyFargateServiceTaskDef5DA17B39 554 | DependsOn: 555 | - MyFargateServiceLBPublicListenerECSGroup4A3EDF05 556 | - MyFargateServiceLBPublicListener61A1042F 557 | - MyFargateServiceTaskDefTaskRole62C7D397 558 | Metadata: 559 | aws:cdk:path: USPythonStack/MyFargateService/Service/Service 560 | MyFargateServiceSecurityGroup7016792A: 561 | Type: AWS::EC2::SecurityGroup 562 | Properties: 563 | GroupDescription: USPythonStack/MyFargateService/Service/SecurityGroup 564 | SecurityGroupEgress: 565 | - CidrIp: 0.0.0.0/0 566 | Description: Allow all outbound traffic by default 567 | IpProtocol: "-1" 568 | VpcId: 569 | Ref: MyVpcF9F0CA6F 570 | DependsOn: 571 | - MyFargateServiceTaskDefTaskRole62C7D397 572 | Metadata: 573 | aws:cdk:path: USPythonStack/MyFargateService/Service/SecurityGroup/Resource 574 | MyFargateServiceSecurityGroupfromUSPythonStackMyFargateServiceLBSecurityGroupA97A599F805D357F22: 575 | Type: AWS::EC2::SecurityGroupIngress 576 | Properties: 577 | Description: Load balancer to target 578 | FromPort: 80 579 | GroupId: 580 | Fn::GetAtt: 581 | - MyFargateServiceSecurityGroup7016792A 582 | - GroupId 583 | IpProtocol: tcp 584 | SourceSecurityGroupId: 585 | Fn::GetAtt: 586 | - MyFargateServiceLBSecurityGroup6FBF16F1 587 | - GroupId 588 | ToPort: 80 589 | DependsOn: 590 | - MyFargateServiceTaskDefTaskRole62C7D397 591 | Metadata: 592 | aws:cdk:path: USPythonStack/MyFargateService/Service/SecurityGroup/from USPythonStackMyFargateServiceLBSecurityGroupA97A599F:80 593 | CDKMetadata: 594 | Type: AWS::CDK::Metadata 595 | Properties: 596 | Analytics: v2:deflate64:H4sIAAAAAAAA/31S227CMAz9lr2HTMAXAGMICW0VoL1ObjCdR0mqxAGhqv8+t6WUXbQnH58cJ/ZxRno4HunhA5zDwOwOg5xSXW4YzEEJ9V6iGenyrTBqtrdvyUwlMc3JbGJqkWuuR2sXGbeQ5tjzPTcJwRkCJmdv4hrMl0kdXoAXwHiGi0o8nQT2Fy8toxfcCdpOrtmEpdePI1pWGzTRE18W3sWi6eFfYp55DOEXvbQNXyk0QZezPAZ5vVZ18Bl8BvVU4fCEe7LUzfSTcZaBLPo77lq7QX8i0/rUwua59wK4HjXoSVGIy41bKwe7KeRgDe5+lGMOgcnkokgbBdnsJOv6u7oZ4lt+ryOZzV41Hb4738rLsoHOyLu0UgRHXa5du/cmJk7qmkW1qFK5y8TNlctuV3S4qtQag4u+9kMcdsc+lev+Pkq8O9EO/RQCKvlayPJlMxm/rnmNXESuVHLhD2cfx3o41OOHz0A08NEyHVGv2/gFfPUv9/0CAAA= 597 | Metadata: 598 | aws:cdk:path: USPythonStack/CDKMetadata/Default 599 | Outputs: 600 | MyFargateServiceLoadBalancerDNS704F6391: 601 | Value: 602 | Fn::GetAtt: 603 | - MyFargateServiceLBDE830E97 604 | - DNSName 605 | MyFargateServiceServiceURL4CF8398A: 606 | Value: 607 | Fn::Join: 608 | - "" 609 | - - http:// 610 | - Fn::GetAtt: 611 | - MyFargateServiceLBDE830E97 612 | - DNSName 613 | Parameters: 614 | BootstrapVersion: 615 | Type: AWS::SSM::Parameter::Value 616 | Default: /cdk-bootstrap/hnb659fds/version 617 | Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip] 618 | 619 | --------------------------------------------------------------------------------