diff --git a/README.rst b/README.rst
index 84e8dd9d..30c5baec 100644
--- a/README.rst
+++ b/README.rst
@@ -160,6 +160,29 @@ Once you have completed your compliance validation code and set your Rule's conf
The exact output will vary depending on Lambda runtime. You can use the --all flag to deploy all of the rules in your working directory.
+Deploy Organization Rule
+------------------------
+You can also deploy the Rule to your AWS Orgnization using the ``deploy-organization`` command.
+For successful evaluation of custom rules in child accounts, please make sure you do one of the following:
+
+1. Set ASSUME_ROLE_MODE in Lambda code to True, to get the lambda to assume the Role attached on the Config Service and confirm that the role trusts the master account where the Lambda function is going to be deployed.
+2. Set ASSUME_ROLE_MODE in Lambda code to True, to get the lambda to assume a custom role and define an optional parameter with key as ExecutionRoleName and set the value to your custom role name; confirm that the role trusts the master account of the organization where the Lambda function will be deployed.
+
+::
+
+ $ rdk deploy-organization MyRule
+ Running deploy!
+ Zipping MyRule
+ Uploading MyRule
+ Creating CloudFormation Stack for MyRule
+ Waiting for CloudFormation stack operation to complete...
+ ...
+ Waiting for CloudFormation stack operation to complete...
+ Config deploy complete.
+
+The exact output will vary depending on Lambda runtime. You can use the --all flag to deploy all of the rules in your working directory.
+This command uses 'PutOrganizationConfigRule' API for the rule deployment. If a new account joins an organization, the rule is deployed to that account. When an account leaves an organization, the rule is removed. Deployment of existing organizational AWS Config Rules will only be retried for 7 hours after an account is added to your organization if a recorder is not available. You are expected to create a recorder if one doesn't exist within 7 hours of adding an account to your organization.
+
View Logs For Deployed Rule
---------------------------
Once the Rule has been deployed to AWS you can get the CloudWatch logs associated with your lambda function using the ``logs`` command.
diff --git a/integration/config-opscenter-integration-example/.DS_Store b/integration/config-opscenter-integration-example/.DS_Store
new file mode 100644
index 00000000..44117d1f
Binary files /dev/null and b/integration/config-opscenter-integration-example/.DS_Store differ
diff --git a/integration/config-opscenter-integration-example/AWS_Config_and_OpsCenter.pdf b/integration/config-opscenter-integration-example/AWS_Config_and_OpsCenter.pdf
new file mode 100644
index 00000000..d7b02010
Binary files /dev/null and b/integration/config-opscenter-integration-example/AWS_Config_and_OpsCenter.pdf differ
diff --git a/integration/config-opscenter-integration-example/AWS_Config_and_OpsCenter.xml b/integration/config-opscenter-integration-example/AWS_Config_and_OpsCenter.xml
new file mode 100644
index 00000000..3ca11922
--- /dev/null
+++ b/integration/config-opscenter-integration-example/AWS_Config_and_OpsCenter.xml
@@ -0,0 +1 @@
+7Vptc6I8FP01flwnhBfxo1q32+nL9qnddfeTEyEiUyBMCL7sr38SCAqEbq2LrdNpnbHkJsTknMO9N1c7+ijcXFIUL2+Ji4MOBO6mo190IOxrPf4uDNvcYBYGj/pubtL2hon/B0sjkNbUd3FSGcgICZgfV40OiSLssIoNUUrW1WELElQ/NUYeVgwTBwWqdeq7bJlbbRPs7d+w7y2LT9aA7AlRMVgakiVyybpk0scdfUQJYflVuBnhQGBX4JLf9/WZ3t3CKI7YITeA/4Lpj+uHX3Q2C4ez+6fNzU/ni5HPskJBKjcsF8u2BQKUpJGLxSSgow/XS5/hSYwc0bvmlHPbkoUBb2n8Ul2UXOcKU4Y3JZNc5CUmIWZ0y4fIXqMn17AtlCABXO/x10w7ty1L2Ot9ORBJzr3d3HtY+IVE5hUomeeHErTByyjpQEUJAngilKzzQ6l/diD1FJAG0wk3TLYJw2HCr25RxJ0KVbAjKQv8CI92jk5AuCARG5GA0GyMzl9fxWKGHkWuj/d9EYn4NMOEUfKESzcssj8xkR8ETRO5KFnuCBPA+9w33qA5Du5J4jOfRLwv9F1XLHM3YBD4nuiYE8ZIyDuQNAR4waosN4lA7EqGAq3Y5UQiIe5J+HA/8nhLF60likVPuPFECOqidWJ0Y0rc1GFXjljgMKb5RXVMkmM+CyXib+a+jAbJ2adSnP2M4riQFr73MWXm8DVlhJ6H0Jwc6nZcmnaUvHqnio19VV4h+sMx4AoLSOpOEXOWH1NlZ+XMHAH2OgO7pQTjSKGdyo8VJ4KS0r7HySh/0k+hr5qKhiNNNy1Fd3LwQdLaKaguLUbiF7xXWUWwpiLxkSiJ840u/I1YxzAmvphlvOKTJYXUmsRVi4IzEie7BbSgI1Or6agIdCUd9Rqy+cLWvo40RUdYgPSpoeM1lAE4y9/nKMlubkE7Oqzl77bxztqBinZGJIwDH0Xc5UNA0wAnn0I6XkgCwBlsRz27k91rPY91KvXoinoShlgqTn3OEkUeVqSDI3cgqliCiAAlie8ceEZmiHqY/W0xciB2KwUwFc0SWmZDvC9sFAeI+atq2awJQvkJ90ISpXRDr4YJWFQPiikSklIHy7vKZa76RLZdZb2ekOTIKBNljO62/Q8kqyU1h2LEhHvg+coVj7VvyTI4K5b1vl1jWTuOZV0HXQABAKYG+yYEenVaHXatXs/mnTYEVl1Kp1bAAeXCEuN1n60dyP2ZUGoYVS+rVGEPpVSZCLwta2r58uHHzVhUSm4Hd4PL8e347lHhkYei2hGwMSCXo7c0KfG1HoZ359CmQ2W1kNpCsOz1K+CbTTV3qIoInqqsoKmF0gdOwcXV4PHq+93H5cHQe2dGhFo/5GGMGx4xUiPZM4le/sUXqKemL9EV1FJWhaN/zWlrWbUJxKshNw0TB+Eu31oa4W6a8FOyx5mP23jurKrXg0DrmgrlTf67EEr7jKslPbzBTpqlMCjlAKMMbgge0mhOyNOr8xm88dkvoYEuMGXzd9YsWhcbqZCssS017jH1+TYFe39/5I5NjfhGsoD14iPxXgG3B2GhkCJSGqCwvDboWmZNftbb5kpQLcaUFfZ5kD72IF1CsZ0yHqgmCLqlBia76WutFuLS9PftlHkrO0onP7889K8naXrd8J08xQkJVpVzlhWIrGBO+ZXHMiAsFApSonki/s3FDnfO7ahTmeI+jvFF75vCm0aVW3hsCm/2axPxU5qhGz0bWsC2+qalt+RceHP/w5l8+P7XR/r4fw==
\ No newline at end of file
diff --git a/integration/config-opscenter-integration-example/README.md b/integration/config-opscenter-integration-example/README.md
new file mode 100644
index 00000000..6f6b0348
--- /dev/null
+++ b/integration/config-opscenter-integration-example/README.md
@@ -0,0 +1,36 @@
+### AWS Config and OpsCenter integration ###
+
+This is an example showing how we can create a CloudWatch event to monitoring
+a change of compliance status and create an OpsItem in OpsCenter.
+
+# Scenario
+
+User wants to leverage OpsCenter to have a central location where operations engineers and IT professionals
+can view, investigate, and resolve operational work items (OpsItems) related to AWS resources. User also wants
+to create OpsItem automatically on non-compliant resouces found by AWS Config. In addition, OpsCenter provides
+action action to trigger a runbook. Engineers/professionals can easily trigger the remediation process with this
+feature.
+
+
+# Example Walkthrough
+
+pre-requisite:
+ aws account,
+ awscli,
+ IAM role permission to create config rules, cloudwatch event and opsitem with cloudformation
+
+1. execute "sh build.sh"
+ - create an IAM role and a managed config rule that checks if server side encryption enabled for a S3 bucket
+
+2. [Optional] Create a non-encrypted S3 bucket if you do not have one
+
+3. Go to AWS Config > Rules > my-config-rule-S3BucketServerSideEncryptionEnabled in Console
+ - click action button and select re-evaluate
+
+4. Once the evaluation is done, go to AWS Systems Manager > OpsCenter in the console and user will see OpsItems created
+ - User can get the details for the non-compliant resources, suggested runbook for remediation
+ - User can execute the runbook to resolve the issue.
+ - Please check the doc for more information on OpsCenter
+ https://docs.aws.amazon.com/systems-manager/latest/userguide/OpsCenter.html
+
+5. execute "sh cleanup.sh"
diff --git a/integration/config-opscenter-integration-example/build.sh b/integration/config-opscenter-integration-example/build.sh
new file mode 100755
index 00000000..bc25224c
--- /dev/null
+++ b/integration/config-opscenter-integration-example/build.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+
+aws cloudformation deploy --stack-name my-opsitem-role \
+--template-file opsitem-role.yaml \
+--capabilities CAPABILITY_IAM
+
+aws cloudformation deploy --stack-name my-config-rule \
+--template-file s3EncryptedConfigRule.yaml
\ No newline at end of file
diff --git a/integration/config-opscenter-integration-example/cleanup.sh b/integration/config-opscenter-integration-example/cleanup.sh
new file mode 100755
index 00000000..7bb8e958
--- /dev/null
+++ b/integration/config-opscenter-integration-example/cleanup.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+
+aws cloudformation delete-stack --stack-name my-opsitem-role
+aws cloudformation delete-stack --stack-name my-config-rule
\ No newline at end of file
diff --git a/integration/config-opscenter-integration-example/opsitem-role.yaml b/integration/config-opscenter-integration-example/opsitem-role.yaml
new file mode 100644
index 00000000..7e0e7b75
--- /dev/null
+++ b/integration/config-opscenter-integration-example/opsitem-role.yaml
@@ -0,0 +1,43 @@
+# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
+# the License. A copy of the License is located at
+# http://aws.amazon.com/apache2.0/
+# or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+# CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and
+# limitations under the License.
+
+AWSTemplateFormatVersion: '2010-09-09'
+Description: Role to create OpsItem with CloudWatch event
+
+Resources:
+
+ OpsItemEventRole:
+ Type: AWS::IAM::Role
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: 2012-10-17
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service:
+ - events.amazonaws.com
+ Action:
+ - sts:AssumeRole
+ Path: /
+ Policies:
+ - PolicyName: create-opsitem-event
+ PolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Effect: Allow
+ Action:
+ - ssm:CreateOpsItem
+ Resource: "*"
+
+
+Outputs:
+ OpsItemEventRoleArn:
+ Value: !GetAtt OpsItemEventRole.Arn
+ Description: 'Role to create OpsItem with CloudWatch event'
+ Export:
+ Name: "OpsItemEventRoleArn"
\ No newline at end of file
diff --git a/integration/config-opscenter-integration-example/s3EncryptedConfigRule.yaml b/integration/config-opscenter-integration-example/s3EncryptedConfigRule.yaml
new file mode 100644
index 00000000..d2e89121
--- /dev/null
+++ b/integration/config-opscenter-integration-example/s3EncryptedConfigRule.yaml
@@ -0,0 +1,70 @@
+# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
+# the License. A copy of the License is located at
+# http://aws.amazon.com/apache2.0/
+# or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+# CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and
+# limitations under the License.
+
+AWSTemplateFormatVersion: 2010-09-09
+
+Parameters:
+ CloudWatchEventIAMRole:
+ Type: String
+ Description: The IAM role that grants CloudWatchEvent access to create OpsItems
+ Default: opscenter-role
+
+Resources:
+ S3BucketServerSideEncryptionEnabled:
+ Type: AWS::Config::ConfigRule
+ Properties:
+ Scope:
+ ComplianceResourceTypes:
+ - "AWS::S3::Bucket"
+ Source:
+ Owner: AWS
+ SourceIdentifier: "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED"
+
+ OpsItemGenForS3BucketServerSideEncryptionEnabled:
+ Type: 'AWS::Events::Rule'
+ Properties:
+ Description: "CloudWatch Rule which creates Ops Items for CloudTrail Compliance Events"
+ EventPattern:
+ source:
+ - aws.config
+ detail-type:
+ - 'Config Rules Compliance Change'
+ detail:
+ configRuleName:
+ - !Ref S3BucketServerSideEncryptionEnabled
+ newEvaluationResult:
+ complianceType:
+ - NON_COMPLIANT
+ State: "ENABLED"
+ Targets:
+ - Arn: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:opsitem
+ Id: SSM-OpsItem
+ RoleArn: !ImportValue OpsItemEventRoleArn
+ InputTransformer:
+ InputTemplate:
+ Fn::Sub:
+ '{ "title": "CloudTrail CloudWatch Logs Compliance Failure",
+ "description": "CloudWatch Event Rule was triggered for Config Compliance Rule Failure.",
+ "source": "Config Compliance",
+ "priority": "2",
+ "severity": "1",
+ "notifications": [{ "arn": "arn:${AWS::Partition}:sns:${AWS::Region}:${AWS::AccountId}:OpsCenterEventNotificationTopic"}],
+ "operationalData": {
+ "/aws/dedup": {"type": "SearchableString","value": "{\"dedupString\":\"SSMOpsItems-S3-Encrypted-enabled-failed\"}"},
+ "/aws/automations": { "value": "[ { \"automationType\": \"AWS:SSM:Automation\", \"automationId\": \"AWS-EnableS3BucketEncryption\" } ]" },
+ "/aws/resources": {"value": "[{\"arn\":\"arn:aws:s3:::\"}]","type": "SearchableString"},
+ "configRuleName": {"type": "SearchableString","value": },
+ "resourceType": {"type": "SearchableString","value": },
+ "resourceId": {"type": "SearchableString","value": }
+ }
+ }'
+ InputPathsMap:
+ resourceType: "$.detail.resourceType"
+ resourceId: "$.detail.resourceId"
+ configRuleName: "$.detail.configRuleName"
+
diff --git a/rdk/__init__.py b/rdk/__init__.py
index 14c7ba8a..a3ddc1c5 100644
--- a/rdk/__init__.py
+++ b/rdk/__init__.py
@@ -6,4 +6,4 @@
#
# or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
-MY_VERSION = "0.8.0"
+MY_VERSION = "0.8.1"
diff --git a/rdk/rdk.py b/rdk/rdk.py
index 239da4c9..656c3e21 100644
--- a/rdk/rdk.py
+++ b/rdk/rdk.py
@@ -48,7 +48,8 @@
test_ci_filename = 'test_ci.json'
event_template_filename = 'test_event_template.json'
-RDKLIB_LAYER_VERSION={'ap-southeast-1':'26', 'ap-south-1':'4', 'us-east-2':'4', 'us-east-1':'4', 'us-west-1':'3', 'us-west-2':'3', 'ap-northeast-2':'4', 'ap-southeast-2':'4', 'ap-northeast-1':'4', 'ca-central-1':'4', 'eu-central-1':'4', 'eu-west-1':'4', 'eu-west-2':'3', 'eu-west-3':'4', 'eu-north-1':'4', 'sa-east-1':'4', 'cn-north-1': '1', 'cn-northwest-1': '1'}
+RDKLIB_LAYER_VERSION={'ap-southeast-1':'28', 'ap-south-1':'5', 'us-east-2':'5', 'us-east-1':'5', 'us-west-1':'4', 'us-west-2':'4', 'ap-northeast-2':'5', 'ap-southeast-2':'5', 'ap-northeast-1':'5', 'ca-central-1':'5', 'eu-central-1':'5', 'eu-west-1':'5', 'eu-west-2':'4', 'eu-west-3':'5', 'eu-north-1':'5', 'sa-east-1':'5', 'cn-north-1': '1', 'cn-northwest-1': '1'}
+
RDKLIB_ARN_STRING = "arn:aws:lambda:{region}:711761543063:layer:rdklib-layer:{version}"
#this need to be update whenever config service supports more resource types : https://docs.aws.amazon.com/config/latest/developerguide/resource-config-reference.html
@@ -222,7 +223,7 @@ def get_command_parser():
parser.add_argument('-r','--region',help='Select the region to run the command in.')
#parser.add_argument('--verbose','-v', action='count')
#Removed for now from command choices: 'test-remote', 'status'
- parser.add_argument('command', metavar='', help='Command to run. Refer to the usage instructions for each command for more details', choices=['clean', 'create', 'create-rule-template', 'deploy', 'init', 'logs', 'modify', 'rulesets', 'sample-ci', 'test-local', 'undeploy', 'export'])
+ parser.add_argument('command', metavar='', help='Command to run. Refer to the usage instructions for each command for more details', choices=['clean', 'create', 'create-rule-template', 'deploy', 'deploy-organization', 'init', 'logs', 'modify', 'rulesets', 'sample-ci', 'test-local', 'undeploy', 'undeploy-organization', 'export'])
parser.add_argument('command_args', metavar='', nargs=argparse.REMAINDER, help="Run `rdk --help` to see command-specific arguments.")
parser.add_argument('-v','--version', help='Display the version of this tool', action="version", version='%(prog)s '+MY_VERSION)
@@ -292,6 +293,9 @@ def get_rule_parser(is_required, command):
def get_undeploy_parser():
return get_deployment_parser(ForceArgument=True, Command="undeploy")
+
+def get_undeploy_organization_parser():
+ return get_deployment_organization_parser(ForceArgument=True, Command="undeploy")
def get_deploy_parser():
return get_deployment_parser()
@@ -322,6 +326,33 @@ def get_deployment_parser(ForceArgument=False, Command="deploy"):
if ForceArgument:
parser.add_argument("--force", required=False, action='store_true', help='[optional] Remove selected Rules from account without prompting for confirmation.')
return parser
+
+def get_deployment_organization_parser(ForceArgument=False, Command="deploy-organization"):
+ direction = "to"
+ if Command=="undeploy":
+ direction = "from"
+
+ parser = argparse.ArgumentParser(
+ prog='rdk '+Command,
+ description="Used to " + Command + " the Config Rule " + direction + " the target Organization."
+ )
+ parser.add_argument('rulename', metavar='', nargs='*', help='Rule name(s) to deploy. Rule(s) will be pushed to AWS.')
+ parser.add_argument('--all','-a', action='store_true', help="All rules in the working directory will be deployed.")
+ parser.add_argument('-s','--rulesets', required=False, help='comma-delimited list of RuleSet names')
+ parser.add_argument('-f','--functions-only', action='store_true', required=False, help="[optional] Only deploy Lambda functions. Useful for cross-account deployments.")
+ parser.add_argument('--stack-name', required=False, help="[optional] CloudFormation Stack name for use with --functions-only option. If omitted, \"RDK-Config-Rule-Functions\" will be used." )
+ parser.add_argument('--custom-code-bucket', required=False, help="[optional] Provide the custom code S3 bucket name, which is not created with rdk init, for generated cloudformation template storage.")
+ parser.add_argument('--rdklib-layer-arn', required=False, help="[optional] Lambda Layer ARN that contains the desired rdklib. Note that Lambda Layers are region-specific.")
+ parser.add_argument('--lambda-role-arn', required=False, help="[optional] Assign existing iam role to lambda functions. If omitted, \"rdkLambdaRole\" will be created.")
+ parser.add_argument('--lambda-layers', required=False, help="[optional] Comma-separated list of Lambda Layer ARNs to deploy with your Lambda function(s).")
+ parser.add_argument('--lambda-subnets', required=False, help="[optional] Comma-separated list of Subnets to deploy your Lambda function(s).")
+ parser.add_argument('--lambda-security-groups', required=False, help="[optional] Comma-separated list of Security Groups to deploy with your Lambda function(s).")
+ parser.add_argument('--lambda-timeout', required=False, default=60, help="[optional] Timeout (in seconds) for the lambda function", type=str)
+ parser.add_argument('--boundary-policy-arn', required=False, help="[optional] Boundary Policy ARN that will be added to \"rdkLambdaRole\".")
+
+ if ForceArgument:
+ parser.add_argument("--force", required=False, action='store_true', help='[optional] Remove selected Rules from account without prompting for confirmation.')
+ return parser
def get_export_parser(ForceArgument=False, Command="export"):
@@ -915,6 +946,59 @@ def undeploy(self):
print("Rule removal complete, but local files have been preserved.")
print("To re-deploy, use the 'deploy' command.")
+ def undeploy_organization(self):
+ self.__parse_deploy_args(ForceArgument=True)
+
+ if not self.args.force:
+ confirmation = False
+ while not confirmation:
+ my_input = input("Delete specified Rules and Lambda Functions from your Organization? (y/N): ")
+ if my_input.lower() == "y":
+ confirmation = True
+ if my_input.lower() == "n" or my_input == "":
+ sys.exit(0)
+
+ #get the rule names
+ rule_names = self.__get_rule_list_for_command()
+
+ print("Running Organization un-deploy!")
+
+ #create custom session based on whatever credentials are available to us.
+ my_session = self.__get_boto_session()
+
+ #Collect a list of all of the CloudFormation templates that we delete. We'll need it at the end to make sure everything worked.
+ deleted_stacks = []
+
+ cfn_client = my_session.client('cloudformation')
+
+ if self.args.functions_only:
+ try:
+ cfn_client.delete_stack(StackName=self.args.stack_name)
+ deleted_stacks.append(self.args.stack_name)
+ except ClientError as ce:
+ print("Client Error encountered attempting to delete CloudFormation stack for Lambda Functions: " + str(ce))
+ except Exception as e:
+ print("Exception encountered attempting to delete CloudFormation stack for Lambda Functions: " + str(e))
+
+ return
+
+ for rule_name in rule_names:
+ try:
+ cfn_client.delete_stack(StackName=self.__get_stack_name_from_rule_name(rule_name))
+ deleted_stacks.append(self.__get_stack_name_from_rule_name(rule_name))
+ except ClientError as ce:
+ print("Client Error encountered attempting to delete CloudFormation stack for Rule: " + str(ce))
+ except Exception as e:
+ print("Exception encountered attempting to delete CloudFormation stack for Rule: " + str(e))
+
+ print("Rule removal initiated. Waiting for Stack Deletion to complete.")
+
+ for stack_name in deleted_stacks:
+ self.__wait_for_cfn_stack(cfn_client, stack_name)
+
+ print("Rule removal complete, but local files have been preserved.")
+ print("To re-deploy, use the 'deploy-organization' command.")
+
def deploy(self):
self.__parse_deploy_args()
@@ -1092,10 +1176,6 @@ def deploy(self):
'ParameterKey': 'RuleName',
'ParameterValue': rule_name,
},
- {
- 'ParameterKey': 'RuleLambdaName',
- 'ParameterValue': self.__get_lambda_name(rule_name, rule_params),
- },
{
'ParameterKey': 'Description',
'ParameterValue': rule_description,
@@ -1359,7 +1439,8 @@ def deploy(self):
my_params.append({
'ParameterKey': 'SecurityGroupIds',
'ParameterValue': self.args.lambda_security_groups
- },{
+ })
+ my_params.append({
'ParameterKey': 'SubnetIds',
'ParameterValue': self.args.lambda_subnets
})
@@ -1470,6 +1551,340 @@ def deploy(self):
return 0
+ def deploy_organization(self):
+ self.__parse_deploy_organization_args()
+
+ #get the rule names
+ rule_names = self.__get_rule_list_for_command()
+
+ #run the deploy code
+ print ("Running Organization deploy!")
+
+ #create custom session based on whatever credentials are available to us
+ my_session = self.__get_boto_session()
+
+ #get accountID
+ identity_details = self.__get_caller_identity_details(my_session)
+ account_id = identity_details['account_id']
+ partition = identity_details['partition']
+
+ if self.args.custom_code_bucket:
+ code_bucket_name = self.args.custom_code_bucket
+ else:
+ code_bucket_name = code_bucket_prefix + account_id + "-" + my_session.region_name
+
+ #If we're only deploying the Lambda functions (and role + permissions), branch here. Someday the "main" execution path should use the same generated CFN templates for single-account deployment.
+ if self.args.functions_only:
+ print ("We don't handle Function Only deployment for Organizations")
+ sys.exit(1)
+
+ #If we're deploying both the functions and the Config rules, run the following process:
+ for rule_name in rule_names:
+ rule_params, cfn_tags = self.__get_rule_parameters(rule_name)
+
+ #create CFN Parameters common for Managed and Custom
+ source_events = "NONE"
+ if "Remediation" in rule_params:
+ print(f"WARNING: Organization Rules with Remediation is not supported at the moment. {rule_name} will be deployed without auto-remediation.")
+
+ if 'SourceEvents' in rule_params:
+ source_events = rule_params['SourceEvents']
+
+ source_periodic = "NONE"
+ if 'SourcePeriodic' in rule_params:
+ source_periodic = rule_params['SourcePeriodic']
+
+ combined_input_parameters = {}
+ if 'InputParameters' in rule_params:
+ combined_input_parameters.update(json.loads(rule_params['InputParameters']))
+
+ if 'OptionalParameters' in rule_params:
+ #Remove empty parameters
+ keys_to_delete = []
+ optional_parameters_json = json.loads(rule_params['OptionalParameters'])
+ for key, value in optional_parameters_json.items():
+ if not value:
+ keys_to_delete.append(key)
+ for key in keys_to_delete:
+ del optional_parameters_json[key]
+ combined_input_parameters.update(optional_parameters_json)
+
+ if 'SourceIdentifier' in rule_params:
+ print("Found Managed Rule.")
+ #create CFN Parameters for Managed Rules
+
+ try:
+ rule_description = rule_params["Description"]
+ except KeyError:
+ rule_description = rule_name
+ my_params = [
+ {
+ 'ParameterKey': 'RuleName',
+ 'ParameterValue': rule_name,
+ },
+ {
+ 'ParameterKey': 'Description',
+ 'ParameterValue': rule_description,
+ },
+ {
+ 'ParameterKey': 'SourceEvents',
+ 'ParameterValue': source_events,
+ },
+ {
+ 'ParameterKey': 'SourcePeriodic',
+ 'ParameterValue': source_periodic,
+ },
+ {
+ 'ParameterKey': 'SourceInputParameters',
+ 'ParameterValue': json.dumps(combined_input_parameters),
+ },
+ {
+ 'ParameterKey': 'SourceIdentifier',
+ 'ParameterValue': rule_params['SourceIdentifier']
+ }]
+ my_cfn = my_session.client('cloudformation')
+
+ #deploy config rule
+ cfn_body = os.path.join(path.dirname(__file__), 'template', "configManagedRuleOrganization.json")
+
+ try:
+ my_stack_name = self.__get_stack_name_from_rule_name(rule_name)
+ my_stack = my_cfn.describe_stacks(StackName=my_stack_name)
+ #If we've gotten here, stack exists and we should update it.
+ print ("Updating CloudFormation Stack for " + rule_name)
+ try:
+ cfn_args = {
+ 'StackName': my_stack_name,
+ 'TemplateBody': open(cfn_body, "r").read(),
+ 'Parameters': my_params
+ }
+
+ # If no tags key is specified, or if the tags dict is empty
+ if cfn_tags is not None:
+ cfn_args['Tags'] = cfn_tags
+
+ response = my_cfn.update_stack(**cfn_args)
+ except ClientError as e:
+ if e.response['Error']['Code'] == 'ValidationError':
+ if 'No updates are to be performed.' in str(e):
+ #No changes made to Config rule definition, so CloudFormation won't do anything.
+ print("No changes to Config Rule.")
+ else:
+ #Something unexpected has gone wrong. Emit an error and bail.
+ print(e)
+ return 1
+ else:
+ raise
+ except ClientError as e:
+ #If we're in the exception, the stack does not exist and we should create it.
+ print ("Creating CloudFormation Stack for " + rule_name)
+ cfn_args = {
+ 'StackName': my_stack_name,
+ 'TemplateBody': open(cfn_body, "r").read(),
+ 'Parameters': my_params
+ }
+
+ if cfn_tags is not None:
+ cfn_args['Tags'] = cfn_tags
+
+ response = my_cfn.create_stack(**cfn_args)
+
+ #wait for changes to propagate.
+ self.__wait_for_cfn_stack(my_cfn, my_stack_name)
+
+ #Cloudformation is not supporting tagging config rule currently.
+ if cfn_tags is not None and len(cfn_tags) > 0:
+ print("WARNING: Tagging is not supported for organization config rules. Only the cloudformation template will be tagged.")
+
+ continue
+
+ print("Found Custom Rule.")
+
+ s3_src = ""
+ s3_dst = self.__upload_function_code(rule_name, rule_params, account_id, my_session, code_bucket_name)
+
+ #create CFN Parameters for Custom Rules
+ lambdaRoleArn = ""
+ if self.args.lambda_role_arn:
+ print ("Existing IAM Role provided: " + self.args.lambda_role_arn)
+ lambdaRoleArn = self.args.lambda_role_arn
+
+ if self.args.boundary_policy_arn:
+ print ("Boundary Policy provided: " + self.args.boundary_policy_arn)
+ boundaryPolicyArn = self.args.boundary_policy_arn
+ else:
+ boundaryPolicyArn = ""
+
+ try:
+ rule_description = rule_params["Description"]
+ except KeyError:
+ rule_description = rule_name
+
+ my_params = [
+ {
+ 'ParameterKey': 'RuleName',
+ 'ParameterValue': rule_name,
+ },
+ {
+ 'ParameterKey': 'RuleLambdaName',
+ 'ParameterValue': self.__get_lambda_name(rule_name, rule_params),
+ },
+ {
+ 'ParameterKey': 'Description',
+ 'ParameterValue': rule_description,
+ },
+ {
+ 'ParameterKey': 'LambdaRoleArn',
+ 'ParameterValue': lambdaRoleArn,
+ },
+ {
+ 'ParameterKey': 'BoundaryPolicyArn',
+ 'ParameterValue': boundaryPolicyArn,
+ },
+ {
+ 'ParameterKey': 'SourceBucket',
+ 'ParameterValue': code_bucket_name,
+ },
+ {
+ 'ParameterKey': 'SourcePath',
+ 'ParameterValue': s3_dst,
+ },
+ {
+ 'ParameterKey': 'SourceRuntime',
+ 'ParameterValue': self.__get_runtime_string(rule_params),
+ },
+ {
+ 'ParameterKey': 'SourceEvents',
+ 'ParameterValue': source_events,
+ },
+ {
+ 'ParameterKey': 'SourcePeriodic',
+ 'ParameterValue': source_periodic,
+ },
+ {
+ 'ParameterKey': 'SourceInputParameters',
+ 'ParameterValue': json.dumps(combined_input_parameters),
+ },
+ {
+ 'ParameterKey': 'SourceHandler',
+ 'ParameterValue': self.__get_handler(rule_name, rule_params)
+
+ },
+ {
+ 'ParameterKey': 'Timeout',
+ 'ParameterValue': str(self.args.lambda_timeout)
+ }]
+ layers = []
+ if 'SourceRuntime' in rule_params:
+ if rule_params['SourceRuntime'] in ['python3.6-lib', 'python3.7-lib', 'python3.8-lib']:
+ if self.args.rdklib_layer_arn:
+ layers.append(self.args.rdklib_layer_arn)
+ else:
+ rdk_lib_version = RDKLIB_LAYER_VERSION[my_session.region_name]
+ rdklib_arn = RDKLIB_ARN_STRING.format(region=my_session.region_name, version=rdk_lib_version)
+ layers.append(rdklib_arn)
+
+
+ if self.args.lambda_layers:
+ additional_layers = self.args.lambda_layers.split(',')
+ layers.extend(additional_layers)
+
+ if layers:
+ my_params.append({
+ 'ParameterKey': 'Layers',
+ 'ParameterValue': ",".join(layers)
+ })
+
+ if self.args.lambda_security_groups and self.args.lambda_subnets:
+ my_params.append({
+ 'ParameterKey': 'SecurityGroupIds',
+ 'ParameterValue': self.args.lambda_security_groups
+ })
+ my_params.append({
+ 'ParameterKey': 'SubnetIds',
+ 'ParameterValue': self.args.lambda_subnets
+ })
+
+ #create json of CFN template
+ cfn_body = os.path.join(path.dirname(__file__), 'template', "configRuleOrganization.json")
+ template_body = open(cfn_body, "r").read()
+ json_body = json.loads(template_body)
+
+ #debugging
+ #print(json.dumps(json_body, indent=2))
+
+ #deploy config rule
+ my_cfn = my_session.client('cloudformation')
+ try:
+ my_stack_name = self.__get_stack_name_from_rule_name(rule_name)
+ my_stack = my_cfn.describe_stacks(StackName=my_stack_name)
+ #If we've gotten here, stack exists and we should update it.
+ print ("Updating CloudFormation Stack for " + rule_name)
+ try:
+ cfn_args = {
+ 'StackName': my_stack_name,
+ 'TemplateBody': json.dumps(json_body),
+ 'Parameters': my_params,
+ 'Capabilities': ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM']
+ }
+
+ # If no tags key is specified, or if the tags dict is empty
+ if cfn_tags is not None:
+ cfn_args['Tags'] = cfn_tags
+
+ response = my_cfn.update_stack(**cfn_args)
+ except ClientError as e:
+ if e.response['Error']['Code'] == 'ValidationError':
+
+ if 'No updates are to be performed.' in str(e):
+ #No changes made to Config rule definition, so CloudFormation won't do anything.
+ print("No changes to Config Rule.")
+ else:
+ #Something unexpected has gone wrong. Emit an error and bail.
+ print('Validation Error on CFN')
+ print(json.dumps(cfn_args))
+ print(e)
+ return 1
+ else:
+ raise
+
+ my_lambda_arn = self.__get_lambda_arn_for_stack(my_stack_name)
+
+ print("Publishing Lambda code...")
+ my_lambda_client = my_session.client('lambda')
+ my_lambda_client.update_function_code(
+ FunctionName=my_lambda_arn,
+ S3Bucket=code_bucket_name,
+ S3Key=s3_dst,
+ Publish=True
+ )
+ print("Lambda code updated.")
+ except ClientError as e:
+ #If we're in the exception, the stack does not exist and we should create it.
+ print ("Creating CloudFormation Stack for " + rule_name)
+ cfn_args = {
+ 'StackName': my_stack_name,
+ 'TemplateBody': json.dumps(json_body),
+ 'Parameters': my_params,
+ 'Capabilities': ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM']
+ }
+
+ if cfn_tags is not None:
+ cfn_args['Tags'] = cfn_tags
+
+ response = my_cfn.create_stack(**cfn_args)
+
+ #wait for changes to propagate.
+ self.__wait_for_cfn_stack(my_cfn, my_stack_name)
+
+ #Cloudformation is not supporting tagging config rule currently.
+ if cfn_tags is not None and len(cfn_tags) > 0:
+ print("WARNING: Tagging is not supported for organization config rules. Only the cloudformation template will be tagged.")
+
+ print('Config deploy complete.')
+
+ return 0
+
def export(self):
self.__parse_export_args()
@@ -2434,6 +2849,44 @@ def __parse_deploy_args(self, ForceArgument=False):
if self.args.rulesets:
self.args.rulesets = self.args.rulesets.split(',')
+
+ def __parse_deploy_organization_args(self, ForceArgument=False):
+
+ self.args = get_deployment_organization_parser(ForceArgument).parse_args(self.args.command_args, self.args)
+
+ ### Validate inputs ###
+ if self.args.stack_name and not self.args.functions_only:
+ print("--stack-name can only be specified when using the --functions-only feature.")
+ sys.exit(1)
+
+ #Make sure we're not exceeding Layer limits
+ if self.args.lambda_layers:
+ layer_count = len(self.args.lambda_layers.split(","))
+ if layer_count > 5:
+ print("You may only specify 5 Lambda Layers.")
+ sys.exit(1)
+ if self.args.rdklib_layer_arn and layer_count > 4:
+ print("Because you have selected a 'lib' runtime You may only specify 4 additional Lambda Layers.")
+ sys.exit(1)
+
+ #RDKLib version and RDKLib Layer ARN are mutually exclusive.
+ if "rdk_lib_version" in self.args and "rdklib_layer_arn" in self.args:
+ print("Specify EITHER an RDK Lib version to use the official release OR a specific Layer ARN to use a custom implementation.")
+ sys.exit(1)
+
+ #Check rule names to make sure none are too long. This is needed to catch Rules created before length constraint was added.
+ if self.args.rulename:
+ for name in self.args.rulename:
+ if len(name) > 128:
+ print("Error: Found Rule with name over 128 characters: {} \n Recreate the Rule with a shorter name.".format(name))
+ sys.exit(1)
+
+ if self.args.functions_only and not self.args.stack_name:
+ self.args.stack_name = "RDK-Config-Rule-Functions"
+
+ if self.args.rulesets:
+ self.args.rulesets = self.args.rulesets.split(',')
+
def __parse_export_args(self, ForceArgument=False):
diff --git a/rdk/template/configManagedRuleOrganization.json b/rdk/template/configManagedRuleOrganization.json
new file mode 100644
index 00000000..c34e5775
--- /dev/null
+++ b/rdk/template/configManagedRuleOrganization.json
@@ -0,0 +1,65 @@
+{
+ "AWSTemplateFormatVersion": "2010-09-09",
+ "Description": "AWS CloudFormation template to create Managed AWS Config rules. You will be billed for the AWS resources used if you create a stack from this template.",
+
+ "Parameters": {
+ "RuleName": {
+ "Description": "Name of the Rule",
+ "Type": "String",
+ "MinLength": "1",
+ "MaxLength": "128"
+ },
+ "Description": {
+ "Description": "Description of the Rule",
+ "Type": "String",
+ "MinLength": "1",
+ "MaxLength": "255"
+ },
+ "SourceEvents": {
+ "Description": "Event Type",
+ "Type": "CommaDelimitedList",
+ "Default": "NONE"
+ },
+ "SourcePeriodic": {
+ "Description": "Execution Frequency",
+ "Type": "String",
+ "MinLength": "1",
+ "MaxLength": "255",
+ "Default": "NONE"
+ },
+ "SourceIdentifier": {
+ "Description": "Source Identifier of Managed Rule",
+ "Type": "String",
+ "MinLength": "1",
+ "MaxLength": "255"
+ },
+ "SourceInputParameters": {
+ "Description": "Input Parameters",
+ "Type": "String",
+ "Default": "{}"
+ },
+ },
+ "Conditions": {
+ "PeriodicTriggered" : { "Fn::Not": [{"Fn::Equals" : [{ "Ref": "SourcePeriodic" }, "NONE"]}]}
+ },
+ "Resources": {
+ "rdkConfigRule": {
+ "Type": "AWS::Config::OrganizationConfigRule",
+ "Properties": {
+ "OrganizationConfigRuleName": { "Ref": "RuleName" },
+ "OrganizationManagedRuleMetadata": {
+ "Description": { "Ref": "Description" },
+ "RuleIdentifier" : { "Ref": "SourceIdentifier" },
+ "InputParameters": { "Ref": "SourceInputParameters" },
+ "ResourceTypesScope": { "Ref": "SourceEvents" },
+ "MaximumExecutionFrequency": {
+ "Fn::If": [ "PeriodicTriggered",
+ { "Ref": "SourcePeriodic" },
+ { "Ref": "AWS::NoValue" }
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/rdk/template/configRuleOrganization.json b/rdk/template/configRuleOrganization.json
new file mode 100644
index 00000000..6629331b
--- /dev/null
+++ b/rdk/template/configRuleOrganization.json
@@ -0,0 +1,263 @@
+{
+ "AWSTemplateFormatVersion": "2010-09-09",
+ "Description": "AWS CloudFormation template to create custom AWS Config rules. You will be billed for the AWS resources used if you create a stack from this template.",
+
+ "Parameters": {
+ "RuleName": {
+ "Description": "Name of the Rule",
+ "Type": "String",
+ "MinLength": "1",
+ "MaxLength": "128"
+ },
+ "Description": {
+ "Description": "Description of the Rule",
+ "Type": "String",
+ "MinLength": "1",
+ "MaxLength": "255"
+ },
+ "RuleLambdaName": {
+ "Description": "Name of the Rule's Lambda function",
+ "Type": "String",
+ "MinLength": "1",
+ "MaxLength": "64"
+ },
+ "LambdaRoleArn": {
+ "Description": "ARN of the existing IAM role that you want to attach to the lambda function.",
+ "Type": "String",
+ "Default": ""
+ },
+ "BoundaryPolicyArn": {
+ "Description": "ARN of a Boundary Policy, will be used only if LambdaRoleArn is NOT set.",
+ "Type": "String",
+ "Default": ""
+ },
+ "SourceBucket": {
+ "Description": "Name of the S3 bucket that you have stored the rule zip files in.",
+ "Type": "String",
+ "MinLength": "1",
+ "MaxLength": "255"
+ },
+ "SourcePath": {
+ "Description": "Location in the S3 bucket where you have stored the rule zip files.",
+ "Type": "String",
+ "MinLength": "1",
+ "MaxLength": "255"
+ },
+ "SourceEvents": {
+ "Description": "Event Type",
+ "Type": "CommaDelimitedList",
+ "Default": "NONE"
+ },
+ "SourceRuntime": {
+ "Description": "Runtime Language",
+ "Type": "String",
+ "MinLength": "1",
+ "MaxLength": "255"
+ },
+ "SourcePeriodic": {
+ "Description": "Execution Frequency",
+ "Type": "String",
+ "MinLength": "1",
+ "MaxLength": "255",
+ "Default": "NONE"
+ },
+ "SourceInputParameters": {
+ "Description": "Input Parameters",
+ "Type": "String",
+ "Default": "{}"
+ },
+ "SourceHandler":{
+ "Description": "Lambda Function Handler",
+ "Type": "String"
+ },
+ "Layers": {
+ "Description": "Comma-separated list of Lambda layers to be included with Lambda Function deployment",
+ "Type": "String",
+ "Default": ""
+ },
+ "SecurityGroupIds": {
+ "Description": "Comma-separated list of Security Group Ids for Lambda Function deployment",
+ "Type": "String",
+ "Default": ""
+ },
+ "SubnetIds": {
+ "Description": "Comma-separated list of Subnet Ids for Lambda Function deployment",
+ "Type": "String",
+ "Default": ""
+ },
+ "Timeout":{
+ "Description": "Lambda Function timeout",
+ "Type": "String",
+ "Default": 60
+ }
+ },
+ "Conditions": {
+ "CreateNewLambdaRole" : { "Fn::Equals" : [{ "Ref": "LambdaRoleArn" }, ""]},
+ "UseBoundaryPolicyInRole" : {"Fn::Not":[{ "Fn::Equals" : [{ "Ref": "BoundaryPolicyArn" }, ""]}]},
+ "EventTriggered" : {"Fn::Not": [{ "Fn::Equals" : [{"Fn::Join": [",", { "Ref": "SourceEvents" }]}, "NONE"]}]},
+ "PeriodicTriggered" : { "Fn::Not": [{"Fn::Equals" : [{ "Ref": "SourcePeriodic" }, "NONE"]}]},
+ "UseAdditionalLayers": {"Fn::Not": [{"Fn::Equals": [{"Ref": "Layers"}, ""]}]},
+ "UseVpcConfig": {
+ "Fn::And": [
+ {"Fn::Not": [{"Fn::Equals": [{"Ref": "SecurityGroupIds"}, ""]}]},
+ {"Fn::Not": [{"Fn::Equals": [{"Ref": "SubnetIds"}, ""]}]}
+ ]
+ }
+ },
+ "Resources": {
+ "rdkRuleCodeLambda": {
+ "Type": "AWS::Lambda::Function",
+ "Properties": {
+ "FunctionName": { "Ref": "RuleLambdaName" },
+ "Code": {
+ "S3Bucket": { "Ref": "SourceBucket" },
+ "S3Key": { "Fn::Join" : [ "", [ { "Ref": "RuleName" }, "/", { "Ref": "RuleName" }, ".zip"]]}
+ },
+ "Description": "Create a new AWS lambda function for rule code",
+ "Handler": { "Ref": "SourceHandler"},
+ "MemorySize": "256",
+ "Role": {
+ "Fn::If": [ "CreateNewLambdaRole",
+ { "Fn::GetAtt": [ "rdkLambdaRole", "Arn" ]},
+ { "Ref": "LambdaRoleArn" }
+ ]
+ },
+ "Runtime": { "Ref": "SourceRuntime"},
+ "Timeout": { "Ref": "Timeout"},
+ "Layers":
+ {"Fn::If":
+ [ "UseAdditionalLayers",
+ { "Fn::Split": [",", {"Ref": "Layers"}]},
+ { "Ref": "AWS::NoValue"}
+ ]
+ },
+ "VpcConfig":
+ {"Fn::If":
+ [ "UseVpcConfig",
+ {
+ "SecurityGroupIds": {"Fn::Split": [",", {"Ref": "SecurityGroupIds"}]},
+ "SubnetIds": {"Fn::Split": [",", {"Ref": "SubnetIds"}]}
+ },
+ { "Ref": "AWS::NoValue"}
+ ]
+ }
+ }
+ },
+ "ConfigPermissionToCallrdkRuleCodeLambda": {
+ "Type": "AWS::Lambda::Permission",
+ "DependsOn": "rdkRuleCodeLambda",
+ "Properties":{
+ "FunctionName": { "Fn::GetAtt": [ "rdkRuleCodeLambda", "Arn" ] } ,
+ "Action": "lambda:InvokeFunction",
+ "Principal": "config.amazonaws.com"
+ }
+ },
+ "rdkConfigRule": {
+ "Type": "AWS::Config::OrganizationConfigRule",
+ "DependsOn": [
+ "ConfigPermissionToCallrdkRuleCodeLambda"
+ ],
+ "Properties": {
+ "OrganizationConfigRuleName": { "Ref": "RuleName" },
+ "OrganizationCustomRuleMetadata": {
+ "Description": { "Ref": "Description" },
+ "InputParameters": { "Ref": "SourceInputParameters" },
+ "LambdaFunctionArn": { "Fn::GetAtt": [ "rdkRuleCodeLambda", "Arn" ] },
+ "ResourceTypesScope": { "Ref": "SourceEvents" },
+ "OrganizationConfigRuleTriggerTypes": [ {"Fn::If": [
+ "PeriodicTriggered",
+ "ScheduledNotification" ,
+ "ConfigurationItemChangeNotification" ]} ],
+ "MaximumExecutionFrequency": {"Fn::If": [
+ "PeriodicTriggered",
+ { "Ref": "SourcePeriodic" },
+ { "Ref": "AWS::NoValue"}]}
+ }
+ }
+ },
+ "rdkLambdaRole": {
+ "Condition": "CreateNewLambdaRole",
+ "Type": "AWS::IAM::Role",
+ "Properties": {
+ "Path": "/rdk/",
+ "PermissionsBoundary": {"Fn::If": [ "UseBoundaryPolicyInRole",
+ { "Ref": "BoundaryPolicyArn" },
+ { "Ref": "AWS::NoValue" }
+ ]
+ },
+ "AssumeRolePolicyDocument": {
+ "Version": "2012-10-17",
+ "Statement": [ {
+ "Sid": "AllowLambdaAssumeRole",
+ "Effect": "Allow",
+ "Principal": { "Service": "lambda.amazonaws.com" },
+ "Action": "sts:AssumeRole"
+ } ]
+ },
+ "Policies": [ {
+ "PolicyName": "ConfigRulePolicy",
+ "PolicyDocument": {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "1",
+ "Action": [
+ "s3:GetObject"
+ ],
+ "Effect": "Allow",
+ "Resource": { "Fn::Sub": "arn:${AWS::Partition}:s3:::${SourceBucket}/${SourcePath}" }
+ },
+ {
+ "Sid": "2",
+ "Action": [
+ "logs:CreateLogGroup",
+ "logs:CreateLogStream",
+ "logs:PutLogEvents",
+ "logs:DescribeLogStreams"
+ ],
+ "Effect": "Allow",
+ "Resource": "*"
+ },
+ {
+ "Sid": "3",
+ "Action": [
+ "config:PutEvaluations"
+ ],
+ "Effect": "Allow",
+ "Resource": "*"
+ },
+ {
+ "Sid": "4",
+ "Action": [
+ "iam:List*",
+ "iam:Describe*",
+ "iam:Get*"
+ ],
+ "Effect": "Allow",
+ "Resource": "*"
+ },
+ {
+ "Sid": "5",
+ "Action": [
+ "sts:AssumeRole"
+ ],
+ "Effect": "Allow",
+ "Resource": "*"
+ }
+ ]
+ }
+ } ],
+ "ManagedPolicyArns": [
+ { "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess" }
+ ]
+ }
+ }
+ },
+
+ "Outputs": {
+ "RuleCodeLambda": {
+ "Description": "ARN for the Rule Code lambda",
+ "Value": { "Fn::GetAtt": [ "rdkRuleCodeLambda", "Arn" ] }
+ }
+ }
+}