Skip to content

Commit 8b279c0

Browse files
authored
Merge pull request #55 from awslabs/fix/custom-resource-handling
Adding support and guidance for custom resources
2 parents 4a1d6e9 + 7cdeb3b commit 8b279c0

File tree

5 files changed

+98
-0
lines changed

5 files changed

+98
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,9 @@ Creating an access preview for a SecretsManager Secret requires a KMSKeyId. The
330330

331331
CheckNoPublicAccess custom policy checks differ from Access Previews because CheckNoPublicAccess checks do not require any account or external access analyzer context. Note that a charge is associated with each custom policy check.
332332

333+
### How are custom resources handled?
334+
335+
Custom resources (`Custom::*` and `AWS::CloudFormation::CustomResource`) have return values defined by the backing Lambda function or SNS topic, so the tool cannot determine what ARN they produce. If a custom resource exists in your template but is not referenced by any IAM policy, it is silently ignored. If an IAM policy references a custom resource via `Fn::GetAtt`, the tool will return an error because it cannot resolve the value and therefore cannot validate the policy. In this case, you can use `--exclude-resource-types Custom::MyResourceType` to skip validation for that resource type. Note that excluding a resource type means any policies referencing it will not be validated.
333336

334337
### Examples
335338

cfn_policy_validator/parsers/utils/arn_generator.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ def try_generate_arn(self, resource_name, resource, attribute_or_ref, visited_no
6666
visited_nodes = []
6767

6868
cfn_type = resource['Type']
69+
70+
# Custom resources (Custom::* and AWS::CloudFormation::CustomResource) have return values
71+
# defined by the backing Lambda/SNS, so we cannot generate an ARN for them.
72+
if cfn_type.startswith('Custom::') or cfn_type == 'AWS::CloudFormation::CustomResource':
73+
return None
74+
6975
split_cfn_type = cfn_type.split("::")
7076
if len(split_cfn_type) != 3:
7177
if len(split_cfn_type) == 4 and split_cfn_type[3].lower() == 'module':

cfn_policy_validator/parsers/utils/intrinsic_functions/fn_get_att_evaluator.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ def evaluate(self, get_att_lookup, visited_nodes=None):
8181
if arn is not None:
8282
return arn
8383

84+
# Custom resources have return values defined by the backing Lambda/SNS, so we cannot determine
85+
# what ARN they produce. Provide a helpful error directing users to --exclude-resource-types.
86+
if resource_type.startswith('Custom::') or resource_type == 'AWS::CloudFormation::CustomResource':
87+
raise ApplicationError(
88+
f'Unable to resolve Fn::GetAtt for custom resource: {logical_name_of_resource}.{attribute_name}. '
89+
f'Custom resource return values are defined by the backing Lambda function and cannot be resolved. '
90+
f'If this resource is referenced in an IAM policy, you can exclude it from validation using '
91+
f'--exclude-resource-types {resource_type}'
92+
)
93+
8494
# if the GetAtt does not reference an ARN, see if we have a custom evaluation for this get_att.
8595
# Useful in cases where an attribute returns something other than an ARN that's relevant to
8696
# IAM policies (canonical username)

cfn_policy_validator/tests/parsers_tests/utils_tests/intrinsic_functions_tests/test_get_att_evaluator.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,59 @@ def test_raises_exception_when_nested_property_does_not_exist(self):
291291
with self.assertRaises(ApplicationError) as context:
292292
node_evaluator.eval(template['Resources']['ResourceA']['Properties']['PolicyProperty'])
293293
self.assertEqual('Call to GetAtt not supported for: RdsDbCluster.MasterUserSecret.SecretArn', str(context.exception))
294+
295+
296+
class WhenEvaluatingAPolicyWithAGetAttToACustomResource(unittest.TestCase):
297+
@mock_node_evaluator_setup()
298+
def test_raises_helpful_error_for_custom_prefix(self):
299+
template = load_resources({
300+
'ResourceA': {
301+
'Type': 'AWS::Random::Service',
302+
'Properties': {
303+
'PropertyA': {
304+
'Fn::GetAtt': ['MyCustomResource', 'Arn']
305+
}
306+
}
307+
},
308+
'MyCustomResource': {
309+
'Type': 'Custom::MyLambdaBacked',
310+
'Properties': {
311+
'ServiceToken': 'arn:aws:lambda:us-east-1:123456789012:function:MyFunction'
312+
}
313+
}
314+
})
315+
316+
node_evaluator = build_node_evaluator(template)
317+
318+
with self.assertRaises(ApplicationError) as context:
319+
node_evaluator.eval(template['Resources']['ResourceA']['Properties']['PropertyA'])
320+
321+
self.assertIn('Unable to resolve Fn::GetAtt for custom resource', str(context.exception))
322+
self.assertIn('--exclude-resource-types', str(context.exception))
323+
324+
@mock_node_evaluator_setup()
325+
def test_raises_helpful_error_for_cloudformation_custom_resource(self):
326+
template = load_resources({
327+
'ResourceA': {
328+
'Type': 'AWS::Random::Service',
329+
'Properties': {
330+
'PropertyA': {
331+
'Fn::GetAtt': ['MyCustomResource', 'Arn']
332+
}
333+
}
334+
},
335+
'MyCustomResource': {
336+
'Type': 'AWS::CloudFormation::CustomResource',
337+
'Properties': {
338+
'ServiceToken': 'arn:aws:lambda:us-east-1:123456789012:function:MyFunction'
339+
}
340+
}
341+
})
342+
343+
node_evaluator = build_node_evaluator(template)
344+
345+
with self.assertRaises(ApplicationError) as context:
346+
node_evaluator.eval(template['Resources']['ResourceA']['Properties']['PropertyA'])
347+
348+
self.assertIn('Unable to resolve Fn::GetAtt for custom resource', str(context.exception))
349+
self.assertIn('--exclude-resource-types', str(context.exception))

cfn_policy_validator/tests/parsers_tests/utils_tests/test_arn_generator.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,29 @@ def test_should_raise_error(self):
7474
self.assertEqual('Unable to resolve Org::ServiceName::UseCase::MODULE. CloudFormation modules are not yet supported.', str(cm.exception))
7575

7676

77+
class WhenGeneratingAnArnForACustomResource(unittest.TestCase):
78+
def setUp(self):
79+
self.arn_generator = ArnGenerator(account_config)
80+
81+
@mock_node_evaluator_setup()
82+
def test_custom_prefix_returns_none(self):
83+
resource = build_resource({'Type': 'Custom::MyCustomResource'})
84+
arn = self.arn_generator.try_generate_arn('AnyName', resource, 'Ref')
85+
self.assertIsNone(arn)
86+
87+
@mock_node_evaluator_setup()
88+
def test_custom_prefix_returns_none_for_get_att(self):
89+
resource = build_resource({'Type': 'Custom::MyCustomResource'})
90+
arn = self.arn_generator.try_generate_arn('AnyName', resource, 'Arn')
91+
self.assertIsNone(arn)
92+
93+
@mock_node_evaluator_setup()
94+
def test_cloudformation_custom_resource_returns_none(self):
95+
resource = build_resource({'Type': 'AWS::CloudFormation::CustomResource'})
96+
arn = self.arn_generator.try_generate_arn('AnyName', resource, 'Ref')
97+
self.assertIsNone(arn)
98+
99+
77100
class WhenGeneratingAnArnForAnIAMRoleAndValidatingSchema(unittest.TestCase):
78101
@mock_node_evaluator_setup()
79102
def test_with_no_properties(self):

0 commit comments

Comments
 (0)