def match(self, cfn: Template) -> List[RuleMatch]: matches = [] projects = cfn.get_resources(["AWS::CodeBuild::Project"]) log_groups = cfn.get_resources(["AWS::Logs::LogGroup"]) for resource_name, resource in projects.items(): properties = resource.get("Properties", {}) logs_config = properties.get("LogsConfig", {}) cloud_watch_logs = logs_config.get("CloudWatchLogs", {}) path = [ "Resources", resource_name, "Properties", "LogsConfig", "CloudWatchLogs", "GroupName", ] if cloud_watch_logs.get( "Status", "ENABLED" ) == "ENABLED" and not cloud_watch_logs.get("GroupName"): message = f"Property {'/'.join(path)} is missing. CodeBuild projects with cloudwatch logs enabled, should have a GroupName defined." matches.append(RuleMatch(path, message)) group_name = cloud_watch_logs.get("GroupName") if group_name and (not isinstance(group_name, dict) or not group_name.get("Ref") in log_groups.keys()): message = f"Property {'/'.join(path)} should be a Ref to a LogGroup." matches.append(RuleMatch(path, message)) return matches
def test_get_object_without_nested_conditions(self): """ Test Getting condition names in an object/list """ template = cfnlint.helpers.convert_dict({ 'Conditions': { 'isProduction': {'Fn::Equals': [{'Ref': 'myEnvironment'}, 'prod']}, 'isDevelopment': {'Fn::Equals': [{'Ref': 'myEnvironment'}, 'dev']} }, 'Resources': { 'myInstance': { 'Type': 'AWS::EC2::Instance', 'Properties': { 'ImageId': { 'Fn::If': [ 'isProduction', 'ami-prod1234', { 'Fn::If': [ 'isDevelopment', 'ami-dev1234', 'ami-lab1234' ] } ] } } } } }) template = Template('test.yaml', template) results = template.get_object_without_conditions( template.template.get('Resources', {}).get('myInstance', {}).get('Properties', {}) ) self.assertEqual(len(results), 3) for result in results: if result['Scenario']['isProduction'] and not result['Scenario']['isDevelopment']: self.assertDictEqual( result['Object'], { 'ImageId': 'ami-prod1234' } ) elif result['Scenario'] == {'isProduction': False, 'isDevelopment': True}: self.assertDictEqual( result['Object'], { 'ImageId': 'ami-dev1234' } ) elif result['Scenario'] == {'isProduction': False, 'isDevelopment': False}: self.assertDictEqual( result['Object'], { 'ImageId': 'ami-lab1234' } ) else: # Blanket assertion error self.assertDictEqual(result['Scenario'], {})
def run_cli(filename, template, rules, regions, override_spec, build_graph, registry_schemas, mandatory_rules=None): """Process args and run""" if override_spec: cfnlint.helpers.override_specs(override_spec) if build_graph: template_obj = Template(filename, template, regions) template_obj.build_graph() if registry_schemas: for path in registry_schemas: if path and os.path.isdir(os.path.expanduser(path)): for f in os.listdir(path): with open(os.path.join(path, f)) as schema: REGISTRY_SCHEMAS.append(json.load(schema)) return run_checks(filename, template, rules, regions, mandatory_rules)
def test_get_object_without_conditions_for_list(self): """ Test Getting condition names in an object/list """ template = cfnlint.helpers.convert_dict({ 'Conditions': { 'CreateAppVolume': {'Fn::Equals': [{'Ref': 'CreateVolums'}, 'true']} }, 'Resources': { 'myInstance': { 'Type': 'AWS::EC2::Instance', 'Properties': { 'ImageId': 'ami-123456', 'BlockDeviceMappings': { 'Fn::If': [ 'CreateAppVolume', [ { 'DeviceName': '/dev/hda1', 'Ebs': { 'DeleteOnTermination': True, 'VolumeType': 'gp2' } }, { 'DeviceName': '/dev/xvdf', 'Ebs': { 'DeleteOnTermination': True, 'VolumeType': 'gp2' } } ], { 'Ref': 'AWS::NoValue' } ] } } } } }) template = Template('test.yaml', template) results = template.get_object_without_conditions( template.template.get('Resources').get('myInstance').get( 'Properties').get('BlockDeviceMappings') ) # handles IFs for a list self.assertEqual( results, [ { 'Scenario': {'CreateAppVolume': True}, 'Object': [ {'DeviceName': '/dev/hda1', 'Ebs': {'DeleteOnTermination': True, 'VolumeType': 'gp2'}}, {'DeviceName': '/dev/xvdf', 'Ebs': {'DeleteOnTermination': True, 'VolumeType': 'gp2'}} ] } ] )
def __init__( self, rules, filename, template, regions, verbosity=0, mandatory_rules=None): self.rules = rules self.filename = filename self.verbosity = verbosity self.mandatory_rules = mandatory_rules or [] self.cfn = Template(filename, template, regions)
def test_get_object_without_conditions_no_value(self): """ Test Getting condition names in an object/list """ template = { 'Conditions': { 'CreateAppVolume': {'Fn::Equals': [{'Ref': 'myAppVolume'}, 'true']} }, 'Resources': { 'myInstance': { 'Type': 'AWS::EC2::Instance', 'Properties': { 'ImageId': 'ami-123456', 'BlockDeviceMappings': [ { 'DeviceName': '/dev/hda1', 'Ebs': { 'DeleteOnTermination': True, 'VolumeType': 'gp2' } }, { 'Fn::If': [ 'CreateAppVolume', { 'DeviceName': '/dev/xvdf', 'Ebs': { 'DeleteOnTermination': True, 'VolumeType': 'gp2' } }, { 'Ref': 'AWS::NoValue' } ] } ] } } } } template = Template('test.yaml', template) results = template.get_object_without_conditions( template.template.get('Resources').get('myInstance').get( 'Properties').get('BlockDeviceMappings')[1].get('Fn::If')[2] ) self.assertEqual(results, []) results = template.get_object_without_conditions( template.template.get('Resources').get('myInstance').get( 'Properties').get('BlockDeviceMappings') ) # when item is a list return empty list self.assertEqual(results, []) # when item is a string return empty list self.assertEqual(template.get_object_without_conditions('String'), [])
def test_is_resource_available(self): """ Test is resource available """ temp_obj = cfnlint.helpers.convert_dict({ 'Mappings': {'location': {'us-east-1': {'primary': 'True'}}}, 'Conditions': { 'isPrimary': {'Fn::Equals': ['True', {'Fn::FindInMap': ['location', {'Ref': 'AWS::Region'}, 'primary']}]}, }, 'Resources': { 'LambdaExecutionRole': { 'Condition': 'isPrimary', 'Type': 'AWS::IAM::Role', 'Properties': {} }, 'AMIIDLookup': { 'Type': 'AWS::Lambda::Function', 'Properties': { 'Handler': 'index.handler', 'Role': { 'Fn::If': ['isPrimary', {'Fn::GetAtt': 'LambdaExecutionRole.Arn'}, {'Ref': 'AWS::NoValue'}] } } } }, 'Outputs': { 'lambdaArn': { 'Condition': 'isPrimary', 'Value': {'Fn::GetAtt': 'LambdaExecutionRole.Arn'} } } }) template = Template('test.yaml', temp_obj) # Doesn't fail with a Fn::If based condition self.assertEqual( template.is_resource_available( ['Resources', 'AMIIDLookup', 'Properties', 'Role', 'Fn::If', 1, 'Fn::GetAtt', ['LambdaExecutionRole', 'Arn']], 'LambdaExecutionRole' ), [] ) # Doesn't fail when the Output has a Condition defined self.assertEqual( template.is_resource_available( ['Outputs', 'lambdaArn', 'Value', 'Fn::GetAtt', ['LambdaExecutionRole', 'Arn']], 'LambdaExecutionRole' ), [] ) # Doesn't fail when the Resource doesn't exist self.assertEqual( template.is_resource_available( ['Outputs', 'lambdaArn', 'Value', 'Fn::GetAtt', ['LambdaExecutionRole', 'Arn']], 'UnknownResource' ), [] )
def run_cli(filename, template, rules, regions, override_spec, build_graph, mandatory_rules=None): """Process args and run""" if override_spec: cfnlint.helpers.override_specs(override_spec) if build_graph: template_obj = Template(filename, template, regions) template_obj.build_graph() return run_checks(filename, template, rules, regions, mandatory_rules)
def test_get_resources_bad(self): """Don't get resources that aren't properly configured""" template = { 'Resources': { 'Properties': { 'BucketName': "bucket_test" }, 'Type': "AWS::S3::Bucket" } } self.template = Template('test.yaml', template) resources = self.template.get_resources() assert resources == {}
def test_get_object_without_nested_conditions_basic(self): """ Test Getting condition names in an object/list """ template = { 'Resources': { 'Test': 'Test' } } template = Template('test.yaml', template) results = template.get_object_without_nested_conditions( template.template.get('Resources', {}).get('Test', {}), ['Resources', 'Test'] ) self.assertEqual(results, [])
def match(self, cfn: Template) -> List[RuleMatch]: matches = [] resources = cfn.get_resources(["AWS::ECS::Service"]) for resource_name, resource in resources.items(): properties = resource.get("Properties", {}) deployment_configuration = properties.get( "DeploymentConfiguration", {}) if not deployment_configuration: path = [ "Resources", resource_name, "Properties", "DeploymentConfiguration", ] message = ( f"{'/'.join(path)} Property DeploymentConfiguration is missing." ) matches.append(RuleMatch(path, message)) deployment_circuit_breaker = deployment_configuration.get( "DeploymentCircuitBreaker", {}) if not deployment_circuit_breaker: path = [ "Resources", resource_name, "Properties", "DeploymentConfiguration", "DeploymentCircuitBreaker", ] message = f"{'/'.join(path)} Property DeploymentConfiguration/DeploymentCircuitBreaker is missing." matches.append(RuleMatch(path, message)) if not deployment_circuit_breaker.get("Enable") is True: path = [ "Resources", resource_name, "Properties", "DeploymentConfiguration", "DeploymentCircuitBreaker", "Enable", ] message = f"{'/'.join(path)} Property DeploymentConfiguration/DeploymentCircuitBreaker/Enable should be true." matches.append(RuleMatch(path, message)) if not deployment_circuit_breaker.get("Rollback") is False: path = [ "Resources", resource_name, "Properties", "DeploymentConfiguration", "DeploymentCircuitBreaker", "Rollback", ] message = f"{'/'.join(path)} Property DeploymentConfiguration/DeploymentCircuitBreaker/Rollback should be false." matches.append(RuleMatch(path, message)) return matches
def test_success_run(self): """ Test Run Logic""" filename = 'test/fixtures/templates/good/generic.yaml' template = cfnlint.decode.cfn_yaml.load(filename) cfn = Template(filename, template, ['us-east-1']) matches = [] matches.extend(self.rules.run(filename, cfn)) assert (matches == [])
def run_tests(self, rulename): for _, values in self.filenames.items(): filename = values.get('filename') template = cfnlint.decode.cfn_yaml.load(filename) cfn = Template(filename, template, ['us-east-1']) rules = RulesCollection(None, None, None, False, None) rules.create_from_custom_rules_file(rulename) runner = cfnlint.runner.Runner(rules, filename, template, None, None) return runner.run()
def test_failure_get_conditions_from_path(self): """ Test get conditions from path when things arne't formatted correctly """ temp_obj = cfnlint.helpers.convert_dict({ 'Resources': { 'AMIIDLookup': { 'Type': 'AWS::Lambda::Function', 'Properties': { 'Role': { 'Fn::If': ['isPrimary', {'Fn::GetAtt': 'LambdaExecutionRole.Arn'}] } } }, 'myInstance4': { 'Type': 'AWS::EC2::Instance', 'Properties': { 'InstanceType': { 'Fn::If': { 'Fn::If': ['isPrimary', 't3.2xlarge', 't3.xlarge'] } } } } } }) template = Template('test.yaml', temp_obj) # Gets no condition names when it isn't a valid list self.assertEqual( template.get_conditions_from_path( template.template, ['Resources', 'AMIIDLookup', 'Properties', 'Role', 'Fn::If', 1, 'Fn::GetAtt', ['LambdaExecutionRole', 'Arn']] ), {} ) # Gets no condition names when it isn't really a list self.assertEqual( template.get_conditions_from_path( template.template, ['Resources', 'myInstance4', 'Properties', 'InstanceType', 'Fn::If', 'Fn::If', 0] ), {} )
def test_success_escape_character(self): """Test Successful JSON Parsing""" failures = 1 filename = 'test/fixtures/templates/good/decode/parsing.json' template = cfnlint.decode.cfn_json.load(filename) cfn = Template(filename, template, ['us-east-1']) matches = [] matches.extend(self.rules.run(filename, cfn)) assert len(matches) == failures, 'Expected {} failures, got {} on {}'.format( failures, len(matches), filename)
def test_fail_sub_properties_run(self): """Test failure run""" filename = 'test/fixtures/templates/bad/resources/properties/onlyone.yaml' template = cfnlint.decode.cfn_yaml.load(filename) cfn = Template(filename, template, ['us-east-1']) matches = [] matches.extend(self.rules.run(filename, cfn)) self.assertEqual( 5, len(matches), 'Expected {} failures, got {}'.format(5, len(matches)))
def match(self, cfn: Template) -> List[RuleMatch]: matches = [] resources = cfn.get_resources(["AWS::S3::Bucket"]) for resource_name, resource in resources.items(): properties = resource.get("Properties", {}) if not "BucketEncryption" in properties: path = ["Resources", resource_name, "Properties", "BucketEncryption"] message = f"Property {'/'.join(path)} is missing" matches.append(RuleMatch(path, message)) return matches
def test_success_parse(self): """Test Successful YAML Parsing""" for _, values in self.filenames.items(): filename = values.get('filename') failures = values.get('failures') template = cfnlint.decode.cfn_yaml.load(filename) cfn = Template(filename, template, ['us-east-1']) matches = [] matches.extend(self.rules.run(filename, cfn)) assert len(matches) == failures, 'Expected {} failures, got {} on {}'.format( failures, len(matches), filename)
def setUp(self): """ SetUp template object""" filename = 'test/fixtures/templates/good/generic.yaml' template = self.load_template(filename) self.template = Template(filename, template) self.resource_names = [ 'IamPipeline', 'RootInstanceProfile', 'RolePolicies', 'MyEC2Instance', 'RootRole', 'mySnsTopic', 'MyEC2Instance1', 'ElasticLoadBalancer' ] self.parameter_names = [ 'WebServerPort', 'Package', 'Package1', 'pIops' ]
def test_fail_run(self): """Test failure run""" filename = 'test/fixtures/templates/bad/generic.yaml' template = cfnlint.decode.cfn_yaml.load(filename) cfn = Template(filename, template, ['us-east-1']) expected_err_count = 36 matches = [] matches.extend(self.rules.run(filename, cfn)) assert len( matches ) == expected_err_count, 'Expected {} failures, got {}'.format( expected_err_count, len(matches))
def test_get_object_without_nested_conditions_ref(self): """ Test Getting condition names in an object/list """ template = { 'Resources': { 'Test': { 'Ref': 'AWS::NoValue' } } } template = Template('test.yaml', template) results = template.get_object_without_nested_conditions( template.template.get('Resources', {}).get('Test', {}), ['Resources', 'Test'] ) self.assertEqual(results, []) results = template.get_object_without_nested_conditions( template.template.get('Resources', {}), ['Resources'] ) self.assertEqual(results, [{'Object': {'Test': {'Ref': 'AWS::NoValue'}}, 'Scenario': None}])
def test_get_object_without_conditions(self): """ Test Getting condition names in an object/list """ template = cfnlint.helpers.convert_dict({ 'Conditions': { 'useAmiId': {'Fn::Not': [{'Fn::Equals': [{'Ref': 'myAmiId'}, '']}]} }, 'Resources': { 'myInstance': { 'Type': 'AWS::EC2::Instance', 'Properties': { 'ImageId': {'Fn::If': ['useAmiId', {'Ref': 'myAmiId'}, {'Ref': 'AWS::NoValue'}]}, 'LaunchConfiguration': {'Fn::If': ['useAmiId', {'Ref': 'AWS::NoValue'}, {'Ref': 'LaunchConfiguration'}]} } } } }) template = Template('test.yaml', template) results = template.get_object_without_conditions( template.template.get('Resources', {}).get('myInstance', {}).get('Properties', {}) ) for result in results: if result['Scenario'] == {'useAmiId': True}: self.assertDictEqual( result['Object'], { 'ImageId': {'Ref': 'myAmiId'} } ) elif result['Scenario'] == {'useAmiId': False}: self.assertDictEqual( result['Object'], { 'LaunchConfiguration': {'Ref': 'LaunchConfiguration'} } )
def test_success_parse_stdin(self): """Test Successful YAML Parsing through stdin""" for _, values in self.filenames.items(): filename = '-' failures = values.get('failures') with open(values.get('filename'), 'r') as fp: file_content = fp.read() with patch('sys.stdin', StringIO(file_content)): template = cfnlint.decode.cfn_yaml.load(filename) cfn = Template(filename, template, ['us-east-1']) matches = [] matches.extend(self.rules.run(filename, cfn)) assert len(matches) == failures, 'Expected {} failures, got {} on {}'.format( failures, len(matches), values.get('filename'))
def match(self, cfn: Template) -> List[RuleMatch]: matches = [] resources = cfn.get_resources( ["AWS::CloudFront::ResponseHeadersPolicy"]) for resource_name, resource in resources.items(): properties = resource.get("Properties", {}) strict_transpost_security = (properties.get( "ResponseHeadersPolicyConfig", {}).get("SecurityHeadersConfig", {}).get("StrictTransportSecurity", {})) two_years = 2 * 365 * 24 * 60 * 60 # strict_transpost_security.get("AccessControlMaxAgeSec") >= two_years path = [ "Resources", resource_name, "ResponseHeadersPolicyConfig", "SecurityHeadersConfig", "StrictTransportSecurity", "AccessControlMaxAgeSec", ] access_control_max_age_sec = strict_transpost_security.get( "AccessControlMaxAgeSec") if (not isinstance(access_control_max_age_sec, int) or not access_control_max_age_sec >= two_years): message = f"Property {'/'.join(path)} should be an integer bigger or equal to 2 years ({two_years})." matches.append(RuleMatch(path, message)) path = [ "Resources", resource_name, "ResponseHeadersPolicyConfig", "SecurityHeadersConfig", "StrictTransportSecurity", "Override", ] if not strict_transpost_security.get("Override") == True: message = f"Property {'/'.join(path)} should be true." matches.append(RuleMatch(path, message)) strict_transpost_security.get("Override") == True return matches
def match(self, cfn: Template) -> List[RuleMatch]: matches = [] valid_platform_versions = ["LATEST", "1.4.0"] resources = cfn.get_resources(["AWS::ECS::Service"]) for resource_name, resource in resources.items(): properties = resource.get("Properties", {}) platform_version = properties.get("PlatformVersion") if platform_version is None: continue if not platform_version in valid_platform_versions: path = [ "Resources", resource_name, "Properties", "PlatformVersion" ] message = f"{'/'.join(path)} {platform_version} is outdated. Set it to one of: {', '.join(valid_platform_versions)}" matches.append(RuleMatch(path, message)) return matches
def match(self, cfn: Template) -> List[RuleMatch]: matches = [] resources = cfn.get_resources(["AWS::CloudFront::Distribution"]) for resource_name, resource in resources.items(): properties = resource.get("Properties", {}) logging = properties.get("DistributionConfig", {}).get("Logging") path = [ "Resources", resource_name, "Properties", "DistributionConfig", "Logging", ] if not logging: message = f"Property {'/'.join(path)} is missing" matches.append(RuleMatch(path, message)) return matches
def match(self, cfn: Template) -> List[RuleMatch]: matches = [] resources = cfn.get_resources(["AWS::CertificateManager::Certificate"]) for resource_name, resource in resources.items(): properties = resource.get("Properties", {}) tags = properties.get("Tags", []) path = [ "Resources", resource_name, "Properties", "Tags", ] for tag in tags: if tag.get("Key") == "Name": break else: message = f"Property {'/'.join(path)} is missing 'Name' tag" matches.append(RuleMatch(path, message)) return matches
def match(self, cfn: Template) -> List[RuleMatch]: matches = [] resources = cfn.get_resources(["AWS::CloudFront::Distribution"]) for resource_name, resource in resources.items(): properties = resource.get("Properties", {}) default_cache_behavior = properties.get( "DistributionConfig", {}).get("DefaultCacheBehavior", {}) path = [ "Resources", resource_name, "Properties", "DistributionConfig", "DefaultCacheBehavior", ] if not default_cache_behavior.get("ResponseHeadersPolicyId"): message = ( f"Property {'/'.join(path)}/ResponseHeadersPolicyId is missing" ) matches.append(RuleMatch(path, message)) for index, cache_behavior in enumerate( properties.get("DistributionConfig", {}).get("CacheBehaviors", [])): path = [ "Resources", resource_name, "Properties", "DistributionConfig", "CacheBehaviors", index, ] if not cache_behavior.get("ResponseHeadersPolicyId"): message = f"Property ResponseHeadersPolicyId missing at {'/'.join([str(p) for p in path])}" matches.append(RuleMatch(path, message)) return matches
def match(self, cfn: Template) -> List[RuleMatch]: matches: List[RuleMatch] = [] template_metadata = cfn.template.get("Metadata", {}) if "AxChangesetAutoApprove" in template_metadata: template_ax_changeset_auto_approve = template_metadata[ "AxChangesetAutoApprove"] matches.extend( self.check_template_ax_changeset_auto_approve( template_ax_changeset_auto_approve)) for resource_name, resource in cfn.get_resources().items(): resource_metadata = resource.get("Metadata", {}) if "AxChangesetAutoApprove" not in resource_metadata: continue resource_ax_changeset_auto_approve = resource_metadata[ "AxChangesetAutoApprove"] matches.extend( self.check_resource_ax_changeset_auto_approve( resource_name, resource_ax_changeset_auto_approve)) return matches
def match(self, cfn: Template) -> List[RuleMatch]: matches = [] resources = cfn.get_resources(["AWS::S3::Bucket"]) for resource_name, resource in resources.items(): properties = resource.get("Properties", {}) public_access_block_configuration = properties.get( "PublicAccessBlockConfiguration" ) path = [ "Resources", resource_name, "Properties", "PublicAccessBlockConfiguration", ] if not public_access_block_configuration: message = f"Property {'/'.join(path)} is missing" matches.append(RuleMatch(path, message)) continue for key in [ "BlockPublicAcls", "BlockPublicPolicy", "IgnorePublicAcls", "RestrictPublicBuckets", ]: value = public_access_block_configuration.get(key) if value is None: detail_path = path + [key] message = f"Property {'/'.join(detail_path)} is missing and should be true" matches.append(RuleMatch(detail_path, message)) elif not (value is True or value == "true"): detail_path = path + [key] message = f"Property {'/'.join(detail_path)} should be true" matches.append(RuleMatch(detail_path, message)) return matches