├── .github └── PULL_REQUEST_TEMPLATE.md ├── COPYING ├── LICENSE ├── NOTICE ├── README.md ├── application-account-initial-setup.yaml ├── compliance-account-initial-setup.yaml ├── docs └── images │ ├── AWS High-Level design for Compliance-as-code framework.pptx │ ├── engine_hl_design.png │ └── engine_ll_design.png ├── rules ├── CMK_BACKING_KEY_ROTATION_ENABLED │ └── parameters.json ├── COMPLIANCE_RULESET_LATEST_INSTALLED │ ├── COMPLIANCE_RULESET_LATEST_INSTALLED.py │ ├── COMPLIANCE_RULESET_LATEST_INSTALLED_test.py │ └── parameters.json ├── EBS_ENCRYPTED_VOLUMES_V2 │ ├── EBS_ENCRYPTED_VOLUMES_V2.py │ ├── EBS_ENCRYPTED_VOLUMES_V2_test.py │ └── parameters.json ├── GUARDDUTY_ENABLED_CENTRALIZED │ └── parameters.json ├── IAM_GROUP_NO_POLICY_FULL_STAR │ ├── IAM_GROUP_NO_POLICY_FULL_STAR.py │ ├── IAM_GROUP_NO_POLICY_FULL_STAR_test.py │ └── parameters.json ├── IAM_PASSWORD_POLICY │ └── parameters.json ├── IAM_ROLE_NO_POLICY_FULL_STAR │ ├── IAM_ROLE_NO_POLICY_FULL_STAR.py │ ├── IAM_ROLE_NO_POLICY_FULL_STAR_test.py │ └── parameters.json ├── IAM_USER_MFA_ENABLED │ └── parameters.json ├── IAM_USER_NO_POLICY_FULL_STAR │ ├── IAM_USER_NO_POLICY_FULL_STAR.py │ ├── IAM_USER_NO_POLICY_FULL_STAR_test.py │ └── parameters.json ├── IAM_USER_UNUSED_CREDENTIALS_CHECK │ └── parameters.json ├── INTERNET_GATEWAY_AUTHORIZED_ONLY │ ├── INTERNET_GATEWAY_AUTHORIZED_ONLY.py │ ├── INTERNET_GATEWAY_AUTHORIZED_ONLY_test.py │ ├── __pycache__ │ │ ├── INTERNET_GATEWAY_AUTHORIZED_ONLY.cpython-36.pyc │ │ └── INTERNET_GATEWAY_AUTHORIZED_ONLY_test.cpython-36.pyc │ └── parameters.json ├── RDS_INSTANCE_PUBLIC_ACCESS_CHECK │ └── parameters.json ├── ROOT_ACCOUNT_MFA_ENABLED │ └── parameters.json ├── ROOT_NO_ACCESS_KEY │ ├── ROOT_NO_ACCESS_KEY.py │ ├── ROOT_NO_ACCESS_KEY_test.py │ ├── __pycache__ │ │ ├── ROOT_NO_ACCESS_KEY.cpython-36.pyc │ │ └── ROOT_NO_ACCESS_KEY_test.cpython-36.pyc │ └── parameters.json ├── Readme.md ├── S3_BUCKET_PUBLIC_READ_PROHIBITED │ └── parameters.json ├── S3_BUCKET_PUBLIC_WRITE_PROHIBITED │ └── parameters.json ├── S3_BUCKET_SSL_REQUESTS_ONLY │ └── parameters.json ├── VPC_DEFAULT_SECURITY_GROUP_CLOSED │ └── parameters.json └── VPC_SG_OPEN_ONLY_TO_AUTHORIZED_PORTS │ └── parameters.json └── rulesets-build ├── buildspec_buildtemplates.yaml ├── buildspec_deploytemplates.yaml ├── compliance-account-analytics-setup.yaml ├── compliance-whitelist.json ├── deploy_datalake.sh ├── deploy_rule_templates.py ├── etl_evaluations.py ├── generate_rule_templates_per_account.sh └── multi-region ├── deploy_lambda.sh └── generate_default_template.sh /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Compliance-as-code Engine and RuleSets 2 | Copyright 2017-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NOTICE 2 | **This project is not maintained any more: please reachout to rdk-maintainers@amazon.com for any questions.** 3 | **Please checkout branch Version2 for latest features which support a more complicated use cases and this version will remain as a minimum vialbe product** 4 | 5 | # Engine for Compliance-as-code 6 | 7 | This package is a collaborative project to deploy and operate Config Rules at scale in an multi-account environment. 8 | 9 | ## Objectives of the package 10 | 1. Deploy automatically and operate configurable sets of AWS Config Rules in a multi-account environment. 11 | 2. Provide insights and records on the compliance status of all AWS Accounts and resources. 12 | 3. Provide an initial set of recommended AWS Config Rules. 13 | 14 | ## Key Features 15 | 1. Analyze current situation and trends from the compliance account as all data are pushed in a Datalake. 16 | 2. Use your favorite analytics tool (Amazon QuickSight, Tableau, Splunk, etc.) as the data is formatted to be directly consumable. 17 | 3. Classify your AWS accounts to deploy only relevant Config Rules depending of your classification (e.g. application type, resilience, stage, sensitvity, etc.). 18 | 4. Ensure that the deployed Rules in each Account are always up-to-date. 19 | 5. Store all historical data of all the changes by storing the compliance record in a centralized and durable Amazon S3 bucket. 20 | 6. Deploy easily in 100s of accounts: by having a 1-step process for any new application account via AWS CloudFormation. 21 | 7. Protect the code base: by centralizing the code base of all the compliance-as-code rules in a dedicated "Compliance Account". 22 | 8. Make use of the AWS Config Rules Dashboard to display the details of compliance status of your AWS resources by setting up Config Aggregator. 23 | 24 | # Getting Started 25 | 26 | ## In a single AWS Region (in a single or multi-account environment) 27 | 28 | You can follow the steps below to install the Compliance Engine. 29 | 30 | ### Requirements 31 | 1. Define an AWS Account to be the central location for the engine (Compliance Account). 32 | 2. Define the AWS Accounts to be verified by the engine (Application Accounts). Note: the Compliance Account can be verified to. 33 | 34 | ### In the Compliance Account 35 | 1. Deploy compliance-account-initial-setup.yaml in your centralized account. Change the MainRegion parameter to match the region where you are deploying this template, if required. 36 | 2. Zip the 2 directories "rules/" and "rulesets-built/" into "ruleset.zip", including the directories themselves. 37 | 3. Copy the "ruleset.zip" in the source bucket (i.e. by default "compliance-engine-codebuild-source-**account_id**-**region_name**") 38 | 4. Go to CodePipeline, then locate the pipeline named "Compliance-Engine-Pipeline". Wait that it auto-triggers (it might show "Failed" when you check for the first time). 39 | 40 | ### In the Application Accounts 41 | 1. Deploy application-account-initial-setup.yaml. 42 | 43 | ### Verify the deployment works 44 | 1. Verify in the Compliance Account that the CodePipeline pipeline named "Compliance-Engine-Pipeline" is executed succesfully 45 | 2. Verify in the Application Account that the Config Rules are deployed. 46 | 47 | ## In multiple AWS Region (in a single or multi-account environment) 48 | 49 | 1. Follow the "Getting Started" in a single AWS Region (above) 50 | 2. Follow the "Add a new Region" in the User Guide (below) 51 | 52 | # FAQ 53 | ### What are the benefits to use of this Compliance engine? 54 | This project assist you to manage, deploy and operate Config Rules in large AWS environment. It completely automate those tasks via a preconfigured pipeline. Additionally, it provides recommended Config Rules to be deployed as Security Baseline, mapped to the CIS Benchmark and PCI (named RuleSets). 55 | 56 | ### What is a RuleSet? 57 | A RuleSet is a collection of Rules. For any AWS accounts, you can decide which RuleSet you want to deploy. For example, you might have a RuleSet for highly confidential accounts, or for high-available accounts or for particular standards (e.g. CIS, PCI or NIST). 58 | 59 | ### Can I add new Rules or new RuleSets? 60 | Yes, we describe in the User Guide how to add new rules and new rulesets. 61 | 62 | ### What are the limits to expect from the Engine? 63 | We expect the engine to work for 100s of accounts, we are yet to hit the limit. The limit for the number of rules per account is about 65 rules, due to CloudFormation template size limits. 64 | 65 | ### Does the engine support multi-region? 66 | Yes, the engine is able to deploy different sets of rules between regions and accounts. By default, it deploys 2 different baselines of rules (avoid to deploy multiple rules with global scope only once, i.e. rules on AWS IAM). 67 | 68 | ### Does the engine use AWS Organizations? 69 | No, for simplicity of the deployment and due to the multiple dimensions of each account we decided not to use AWS Organizations. 70 | 71 | ### I am already using AWS Config today. Can I still use the Engine? 72 | Yes, the engine is compatible with an existing setup. 73 | 74 | # Overall Design 75 | 76 | ## High Level Design 77 | The engine for compliance-as-code design has the following key elements: 78 | - Application account(s): AWS account(s) which has a set of requirements in terms of compliance controls. The engine verifies the compliance controls implemented in this account. 79 | - Compliance account: the AWS account which contains the code representing the compliance requirements. It should be a restricted environment. Notification, Historical data storage and reporting are driven from this account. 80 | 81 | config-engine-high-level-design 82 | 83 | ## Low Level Design 84 | 85 | config-engine-low-level-design 86 | 87 | ## RuleSets 88 | 89 | The set of Rules deployed in each Aplication Account depends on: 90 | - initial deployment of compliance-account-initial-setup.yaml: the parameter "DefaultRuleSet" in the CloudFormation template represents the default RuleSet to be deployed in any Application Accounts (main Region), not registered in account_list.json. For other regions (not the main Region), the parameter "DefaultRuleSetOtherRegions" in the CloudFormation template represents the default RuleSet to be deployed. 91 | - account_list.json (optional): this file includes the metadata of the accounts and their classifications (via tags) 92 | - rules/RULE_NAME/parameters.json: those files are included in each rule folder. Those rule metadata are matched with account metadata to deploy the proper Ruleset in each account. 93 | 94 | ## Deployment Flow 95 | 1. When a new Application Account is added via the application-account-initial-setup.yaml, one rule is installed (by default named COMPLIANCE_RULESET_LATEST_INSTALLED) 96 | 2. This rule verifies if the correct Config rules are installed. 97 | 3. If not, the rule create an empty *account_id*.json file to register, and it triggers the CodePipeline in the Compliance Account. 98 | 4. The pipeline looks at all accounts installed (all json file) and matches with their metadata stored in *account_list.json*. 99 | 5. If the account has no metadata (ie. not registered), the pipeline create a default template with the default ruleset (by default: baseline). 100 | 6. The pipeline then deploy the account-specific AWS Config Rules via CloudFormation in all AWS accounts (registered or not in account_list.json). 101 | 7. The COMPLIANCE_RULESET_LATEST_INSTALLED rule is trigger every 24h (configurable) to verify that the installed ruleset is still current. 102 | 103 | # User Guide 104 | 105 | ## Add a new Application Account in scope in 1 step 106 | 107 | In Application Account, deploy (in the same region) the CloudFormation: application-account-initial-setup.yaml. 108 | 109 | This Cloudformation does the following: 110 | - enable and centralize Config 111 | - deploy an IAM role to allow the Compliance Engine to interact 112 | - deploy 1 Config Rule, used for verifying that the proper Rules are deployed. If non-compliant, it will trigger automatically the deployment of an update. 113 | 114 | After few minutes, all the Config Rules defined as "baseline" (configurable) will be deployed in this new Application Account. 115 | 116 | ## Add a whitelisted/exception resource from a particular Rule 117 | 118 | Certain resources may have a business need to not follow a particular rule. You can whitelist a resouce from being NON_COMPLIANT in the datalake, where you can query the compliance data. The resource will be then be noted as COMPLIANT, and the flag "WhitelistedComplianceType" will be set to "True" for traceability. 119 | 120 | To add a resource in the whitelist: 121 | 122 | 1. Update the file ./rulesets-build/compliance-whitelist.json (for model, there are dummy examples). 123 | 2. Ensure that the location of the whitelist is correct in the code ./rulesets-build/etl_evaluations.py 124 | 3. Ensure the WhitelistLocation parameter in compliance-account-initial-setup.yaml is correct 125 | 126 | Note: the resource will still be shown non-compliant in the AWS console of Config Rules. 127 | 128 | Note 2: certain Rules might have a whitelist/exception in the parameters.json, but only for custom Config rules. 129 | 130 | ## Add a new Region 131 | 132 | 1. In the Compliance Account, update compliance-account-initial-setup.yaml adding the region in the OtherActiveRegions parameter. You can add several regions. 133 | 2. In the Compliance Account, deploy (in the additional region) the CloudFormation: compliance-account-initial-setup.yaml. No change is required in your original parameters. 134 | 2. Run the pipeline in the main region. It deploys the supporting infrastructure (including buckets and lambdas) in the other region of your Compliance Account. 135 | 3. In the Application Account, deploy (in the additional region) the CloudFormation: application-account-initial-setup.yaml. No change is required in your original parameters. 136 | 137 | ## Deploy Rules differently depending of AWS Accounts (in a single Region scenario) 138 | 139 | This is an advanced scenario, where you want to deploy more than the default baseline. In this scenario, you can chose precisely which rule get deployed in which account(s) in the main Region. 140 | 141 | ### Add an Account list 142 | 1. Create an account_list.json, following the format: 143 | ``` 144 | { 145 | "AllAccounts": [{ 146 | "Accountname": "Test Account 1", 147 | "AccountID": "123456789012", 148 | "OwnerEmail": ["admin1@domain.com"], 149 | "RootEmail" : "root1@domain.com", 150 | "Tags": ["baseline", "confidentiality:high"] 151 | }] 152 | } 153 | ``` 154 | 2. Update the compliance-account-initial-setup with the account list location 155 | 156 | ### Create the link between Account and Rules 157 | The engine matches the Tags in the account_list.json with the Tags in the parameters.json of the Rules. When a match is detected, the Rule is deployed in the target account. 158 | 159 | ## Deploy rules differently depending of AWS Accounts and Regions (in a multiple Regions scenario) 160 | 161 | This is an advanced scenario, where you want to deploy more than 2 different regional baselines. In this scenario, you can chose precisely which rule get deployed in which account(s) and in which region(s). 162 | 163 | ### Add an Account list 164 | 1. Create an account_list.json, following the format (notice the "Region" key): 165 | ``` 166 | { 167 | "AllAccounts": [{ 168 | "Accountname": "Test Account 1", 169 | "AccountID": "123456789012", 170 | "OwnerEmail": ["admin1@domain.com"], 171 | "RootEmail" : "root1@domain.com", 172 | "Region": "us-west-1", 173 | "Tags": ["baseline", "confidentiality:high"] 174 | }, { 175 | "Accountname": "Test Account 1", 176 | "AccountID": "123456789012", 177 | "OwnerEmail": ["admin1@domain.com"], 178 | "RootEmail" : "root1@domain.com", 179 | "Region": "ap-southeast-1", 180 | "Tags": ["otherregionsbaseline", "confidentiality:high"] 181 | }] 182 | } 183 | ``` 184 | 2. Update the compliance-account-initial-setup with the account list location 185 | 186 | ### Create the link between Account and Rules 187 | The engine matches the Tags in the account_list.json with the Tags in the parameters.json of the Rules. When a match is detected, the Rule is deployed in the target region of the account. 188 | 189 | ## Add a new Config Rule in a RuleSet 190 | 191 | ### Add a custom Rule to a RuleSet 192 | 1. Create the rule with the RDK (https://github.com/awslabs/aws-config-rdk) 193 | 2. Copy the entire RDK rule *folder* into the ./rules/ (including the 2 python files (code and test) and the parameters.json) 194 | 3. Use the RDK feature for "RuleSets" to add the rules to the appropriate RuleSet. By default, no RuleSet is configured. If you don't use the *account_list*.json, tag the rule with the value of the parameter "DefaultRuleSet" (the one in the CloudFormation template) to deploy in the main region and/or tag the rule with the value of the parameter "DefaultRuleSetOtherRegions" to deploy in the other region(s) (not main). 195 | 196 | 4. Add it into the "ruleset.zip" (see initial deployment section for details) 197 | 5. Run the CodePipeline pipeline named "Compliance-Engine-Pipeline" 198 | 199 | ### Add a managed Rule to a RuleSet 200 | 1. Follow the RDK instructions to add a Managed Rules in particular RuleSets. 201 | 2. Add it into the "ruleset.zip" (see initial deployment section for details) 202 | 3. Run the CodePipeline pipeline named "Compliance-Engine-Pipeline" 203 | 204 | 205 | ## Visualize all the Compliance data using the Compliance-as-code Datalake 206 | 207 | ### Set up the Compliance Account 208 | 209 | Execute the saved Athena Queries that you can find in Athena > Saved Queries 210 | * 1-Database For ComplianceAsCode 211 | * 2-Table For ComplianceAsCode 212 | * 3-Table For Config in ComplianceAsCode 213 | * 4-Table For AccountList (if account_list.json is configured) 214 | 215 | ### Set up Amazon QuickSight 216 | See official documentation to import an Athena query in QuickSight: https://docs.aws.amazon.com/quicksight/latest/user/create-a-data-set-athena.html 217 | * Make sure you add the Athena Results bucket and the original bucket in QuickSight settings. 218 | * We recommend to use SPICE for best performance. 219 | * Remember to add a scheduler to refresh the SPICE Data Set(s) daily 220 | 221 | #### Prepare the data sets 222 | Change the data type for the enginerecordedtime, resultrecordedtime & configruleinvokedtime from String to Data: yyyy-MM-dd HH:mm:ss 223 | 224 | You need to create manually Calculated Fields. Here's some useful Formula examples: 225 | 226 | DataAge: dateDiff({enginerecordedtime},now()) 227 | 228 | Confidentiality: ifelse(isNull({accountid[accountlist]}),"NOT REGISTERED",toUpper(split({tag2},":",2))) 229 | 230 | WeightedConfidentiality: ifelse({Confidentiality} = "HIGH",3,{Confidentiality} = "MEDIUM",2,{Confidentiality} = "LOW",1,0) 231 | 232 | WeightedRuleCriticity: ifelse({rulecriticity} = "1_CRITICAL",4,{rulecriticity} = "2_HIGH",3,{rulecriticity} = "3_MEDIUM",2,{rulecriticity} = "4_LOW",1,0) 233 | 234 | ClassCriti: {WeightedClassification} * {WeightedRuleCriticity} 235 | 236 | KinesisProcessingError: ifelse(isNull({configrulearn}),"ERROR", "OK") 237 | 238 | ### Create Compliance dashboard on Amazon QuickSight 239 | #### Create Visuals 240 | The following are visual you can leverage. The format is: 241 | 242 | Name of the Visual : type of QuickSight Visual - configuration of the Visual - filter on the Visual. 243 | 244 | ##### Operational Metrics 245 | 246 | 60-day trend on Number of AWS Accounts by Classification : Line Chart - X Axis: DataAge; Value: AccountID (Count Distinct); Color: AccountClassification - Filter: DataAge <= 60 247 | 248 | Accounts with Critical Non-Compliant Rules : Horizontal Stack Bar Chart - Y Axis: AccountID; Value: RuleName (Count Distinct) - Filter: DataAge <= 1 & ClassCriti = [12,16] & ComplianceType = "NON_COMPLIANT" 249 | 250 | 60-day trend on Non-compliant Rule by ClassCriti : Line Chart - X Axis: DataAge; Value: AccountID (Count Distinct); Color: ClassCriti - Filter: DataAge <= 60 251 | 252 | Resources in all Accounts : Horizontal Stack Bar Chart - Y Axis: ResourceType; Value: ResourceID (Count Distinct) - Filter: DataAge <= 1 253 | 254 | Account Distribution by Account Classification : Horizontal Stack Bar Chart - Y Axis: accountclassification; Value: AccountID (Count Distinct) - Filter: DataAge = 0 255 | 256 | Rule Distribution by Rule Criticity : Horizontal Stack Bar Chart - Y Axis: rulecriticity; Value: RuleName (Count Distinct) - Filter: DataAge <= 1 257 | 258 | Non-Compliant Resources by RuleName and by ClassCriti : Heat Map - Row: RuleName ; Columns: ClassCriti; Values ResourceID (Count Distinct) - Filter: DataAge <= 1 & ComplianceType = "NON_COMPLIANT" 259 | 260 | Trend of Non-Compliant Resources by Account Classification : Line Chart - X Axis: RecordedInDDBTimestamp; Value: ResourceID (Count Distinct); Color: accountclassification - Filter: ComplianceType = "NON_COMPLIANT" 261 | 262 | List of Rules and Non-Compliant Resources: Table - Group by: rulename, resourceid; Value: ClassCriti (Max), AccountID (Count Distinct) - Filter: DataAge <= 1 263 | 264 | ##### Executive Metrics 265 | 266 | Overall Compliance of Rules by Account Classification: Horizontal stacked 100% bar chart - Y axis: AccountClassification; Value: RuleArn (Count Distinct); Group/Color: ComplianceType - Filter: DataAge <= 1 267 | 268 | Evolution of Compliance Status (last 50 days): Vertical stacked 100% bar chart - X axis: DataAge, Group/Color: ComplianceType - Filter: DataAge <= 50 269 | 270 | Top 3 Account Non Compliant (weighted): Horizontal stacked bar chart - Y axis: AccountID , Value: DurationClassCriti (Sum), Group/Color: ClassCriti - Filter: ClassCriti >= 8 271 | 272 | # Team 273 | * Jonathan Rault - Idea, Design, Coding and Feedback 274 | * Michael Borchert - Design, Coding and Feedback 275 | 276 | # License 277 | This project is licensed under the Apache 2.0 License 278 | 279 | # Acknowledgments 280 | * The RDK team makes everything so much smoother. 281 | 282 | # Related Projects 283 | * Rule Development Kit (https://github.com/awslabs/aws-config-rdk) 284 | * Rules repository (https://github.com/awslabs/aws-config-rules) 285 | -------------------------------------------------------------------------------- /application-account-initial-setup.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2017-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). 5 | # You may not use this file except in compliance with the License. 6 | # A copy of the License is located at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # or in the "license" file accompanying this file. This file is distributed 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | # express or implied. See the License for the specific language governing 13 | # permissions and limitations under the License. 14 | # 15 | 16 | AWSTemplateFormatVersion: '2010-09-09' 17 | Description: Initialize the Compliance-as-Code oversight in this Application Account 18 | 19 | Metadata: 20 | AWS::CloudFormation::Interface: 21 | ParameterGroups: 22 | - Label: 23 | default: Compliance-as-Code Engine Configuration 24 | Parameters: 25 | - MainRegion 26 | - ComplianceAccountId 27 | - ConfigAndComplianceAuditRoleName 28 | - CentralizedS3BucketConfigFullName 29 | - Label: 30 | default: (Advanced User Only) Deployment options 31 | Parameters: 32 | - DeployAWSConfig 33 | - ForceDeploymentRoleInMainRegionOnly 34 | - RuleFrequency 35 | - LambdaFunctionName 36 | - EngineComplianceRule 37 | 38 | Parameters: 39 | CentralizedS3BucketConfigFullName: 40 | ConstraintDescription: Enter DNS-compliant name 41 | Description: (Only if DeployAWSConfig is set to "true") Bucket name where Config logs are centrally stored. It is located in the Compliance Account. 42 | Default: centralized-config-112233445566 43 | MaxLength: 63 44 | MinLength: 10 45 | Type: String 46 | ComplianceAccountId: 47 | ConstraintDescription: 12 digits, no dashes 48 | Description: Account ID of the Compliance Account. The compliance-as-code engine must be installed in this account first. 49 | Default: "112233445566" 50 | MaxLength: 12 51 | MinLength: 12 52 | Type: String 53 | ConfigAndComplianceAuditRoleName: 54 | Description: (Only if DeployAWSConfig is set to "true") Role Name of the Compliance Account Cross Account Role 55 | Default: AWSConfigAndComplianceAuditRole-DO-NOT-DELETE 56 | Type: String 57 | MainRegion: 58 | Description: Region which is designated as main Region in your Compliance Account. 59 | Default: us-west-2 60 | AllowedValues: 61 | - us-east-1 62 | - us-east-2 63 | - us-west-1 64 | - us-west-2 65 | - ap-south-1 66 | - ap-northeast-1 67 | - ap-northeast-2 68 | - ap-southeast-1 69 | - ap-southeast-2 70 | - ca-central-1 71 | - eu-central-1 72 | - eu-west-1 73 | - eu-west-2 74 | - eu-west-3 75 | - sa-east-1 76 | AllowedPattern: ^.{0,14}$ 77 | ConstraintDescription: Select one AWS Region only. 78 | Type: String 79 | EngineComplianceRule: 80 | Description: Rule name which review the state of this deployment 81 | Default: COMPLIANCE_RULESET_LATEST_INSTALLED 82 | Type: String 83 | RuleFrequency: 84 | Description: Frequency to verify the compliance of this deployment 85 | AllowedValues: 86 | - One_Hour 87 | - Three_Hours 88 | - Six_Hours 89 | - Twelve_Hours 90 | - TwentyFour_Hours 91 | Default: One_Hour 92 | Type: String 93 | LambdaFunctionName: 94 | Description: Lambda name in the Compliance Account 95 | Default: RDK-Rule-Function-COMPLIANCERULESETLATESTINSTALLED 96 | Type: String 97 | DeployAWSConfig: 98 | Description: Set to "true" to configure AWS Config. If set to "false", you must give the compliance account to assume the AWS Config service Role, and as well permissions to deploy CloudFormation templates and control Config. 99 | Default: true 100 | AllowedValues: 101 | - true 102 | - false 103 | Type: String 104 | ForceDeploymentRoleInMainRegionOnly: 105 | Description: Set to "force" to deploy the ConfigAndComplianceAuditRoleName. This options only works in the main Region. 106 | Default: default 107 | AllowedValues: 108 | - default 109 | - force 110 | Type: String 111 | 112 | Conditions: 113 | IsMainRegion: !Equals [ !Ref 'AWS::Region', !Ref MainRegion ] 114 | NotMainRegion: !Not [!Equals [!Ref 'AWS::Region', !Ref MainRegion ]] 115 | SetConfig: !Equals [ !Ref DeployAWSConfig, 'true'] 116 | NoSetConfig: !Equals [ !Ref DeployAWSConfig, 'false'] 117 | ForceDeployRole: !Equals [ !Ref ForceDeploymentRoleInMainRegionOnly, 'force'] 118 | SetConfigMain: !And 119 | - !Condition IsMainRegion 120 | - !Condition SetConfig 121 | SetConfigNotMain: !And 122 | - !Condition NotMainRegion 123 | - !Condition SetConfig 124 | NoSetConfigMain: !And 125 | - !Condition IsMainRegion 126 | - !Condition NoSetConfig 127 | NoSetConfigNotMain: !And 128 | - !Condition NotMainRegion 129 | - !Condition NoSetConfig 130 | ForceDeployRoleMain: !And 131 | - !Condition IsMainRegion 132 | - !Condition ForceDeployRole 133 | DeployRole: !Or 134 | - !Condition SetConfigMain 135 | - !Condition ForceDeployRoleMain 136 | 137 | Resources: 138 | MainConfigurationRecorder: 139 | Condition: SetConfigMain 140 | Type: AWS::Config::ConfigurationRecorder 141 | Properties: 142 | RecordingGroup: 143 | AllSupported: true 144 | IncludeGlobalResourceTypes: true 145 | RoleARN: !Join ["", ["arn:aws:iam::", !Ref 'AWS::AccountId', ":role/service-role/", !Ref ConfigAndComplianceAuditRoleName]] 146 | DependsOn: 147 | - AWSConfigAndComplianceRole 148 | - ConfigS3WritePolicy 149 | 150 | NotMainConfigurationRecorder: 151 | Condition: SetConfigNotMain 152 | Type: AWS::Config::ConfigurationRecorder 153 | Properties: 154 | RecordingGroup: 155 | AllSupported: true 156 | IncludeGlobalResourceTypes: false 157 | RoleARN: !Join ["", ["arn:aws:iam::", !Ref 'AWS::AccountId', ":role/service-role/", !Ref ConfigAndComplianceAuditRoleName]] 158 | 159 | MainDeliveryChannel: 160 | Condition: SetConfigMain 161 | Type: AWS::Config::DeliveryChannel 162 | Properties: 163 | ConfigSnapshotDeliveryProperties: 164 | DeliveryFrequency: TwentyFour_Hours 165 | S3BucketName: !Ref CentralizedS3BucketConfigFullName 166 | DependsOn: 167 | - ConfigS3WritePolicy 168 | 169 | NotMainDeliveryChannel: 170 | Condition: SetConfigNotMain 171 | Type: AWS::Config::DeliveryChannel 172 | Properties: 173 | ConfigSnapshotDeliveryProperties: 174 | DeliveryFrequency: TwentyFour_Hours 175 | S3BucketName: !Ref CentralizedS3BucketConfigFullName 176 | 177 | AggregationAuthorization: 178 | Condition: SetConfig 179 | Type: "AWS::Config::AggregationAuthorization" 180 | Properties: 181 | AuthorizedAccountId: !Ref ComplianceAccountId 182 | AuthorizedAwsRegion: !Ref 'AWS::Region' 183 | 184 | AWSConfigAndComplianceRole: 185 | Condition: DeployRole 186 | Type: AWS::IAM::Role 187 | Properties: 188 | AssumeRolePolicyDocument: 189 | Statement: 190 | - Effect: Allow 191 | Action: 192 | - sts:AssumeRole 193 | Principal: 194 | Service: config.amazonaws.com 195 | - Effect: Allow 196 | Action: 197 | - sts:AssumeRole 198 | Principal: 199 | AWS: !Ref ComplianceAccountId 200 | Version: '2012-10-17' 201 | ManagedPolicyArns: 202 | - 'arn:aws:iam::aws:policy/service-role/AWSConfigRole' 203 | - 'arn:aws:iam::aws:policy/ReadOnlyAccess' 204 | Path: /service-role/ 205 | RoleName: !Ref ConfigAndComplianceAuditRoleName 206 | 207 | ConfigS3WritePolicy: 208 | Condition: DeployRole 209 | Type: 'AWS::IAM::Policy' 210 | Properties: 211 | Roles: 212 | - !Ref ConfigAndComplianceAuditRoleName 213 | PolicyName: !Join 214 | - '-' 215 | - - ConfigS3Write 216 | - !Ref 'AWS::AccountId' 217 | - !Ref 'AWS::Region' 218 | PolicyDocument: 219 | Statement: 220 | - Action: 221 | - s3:PutObject 222 | Effect: Allow 223 | Resource: 224 | - !Join [ "", [ "arn:aws:s3:::", !Ref CentralizedS3BucketConfigFullName, "/AWSLogs/", !Ref 'AWS::AccountId', "/*"] ] 225 | Sid: !Join ["", ["ConfigS3Write", !Ref 'AWS::AccountId'] ] 226 | Version: "2012-10-17" 227 | DependsOn: 228 | - AWSConfigAndComplianceRole 229 | 230 | ConfigDeployPolicy: 231 | Condition: DeployRole 232 | Type: 'AWS::IAM::Policy' 233 | Properties: 234 | Roles: 235 | - !Ref ConfigAndComplianceAuditRoleName 236 | PolicyName: !Join 237 | - '-' 238 | - - ConfigDeploy 239 | - !Ref 'AWS::AccountId' 240 | - !Ref 'AWS::Region' 241 | PolicyDocument: 242 | Statement: 243 | - Action: 244 | - cloudformation:* 245 | Effect: Allow 246 | Resource: 247 | - !Join [ "", [ "arn:aws:cloudformation:*:", !Ref 'AWS::AccountId', ":stack/Compliance-Engine-Benchmark-DO-NOT-DELETE*" ]] 248 | Sid: !Join ["", ["ConfigDeployCfn", !Ref 'AWS::AccountId'] ] 249 | - Action: 250 | - config:* 251 | Effect: Allow 252 | Resource: "*" 253 | Sid: !Join ["", ["ConfigAccess", !Ref 'AWS::AccountId'] ] 254 | Version: "2012-10-17" 255 | DependsOn: 256 | - AWSConfigAndComplianceRole 257 | 258 | MainCaCReporter: 259 | Condition: SetConfigMain 260 | Type: AWS::Config::ConfigRule 261 | Properties: 262 | ConfigRuleName: !Ref EngineComplianceRule 263 | Description: Check that the latest Compliance-as-code template is installed in this account. 264 | Source: 265 | Owner: CUSTOM_LAMBDA 266 | SourceIdentifier: !Join [ ":", [ 'arn:aws:lambda', !Ref "AWS::Region", !Ref ComplianceAccountId, 'function', !Ref LambdaFunctionName ] ] 267 | SourceDetails: 268 | - 269 | EventSource: "aws.config" 270 | MaximumExecutionFrequency: !Ref RuleFrequency 271 | MessageType: ScheduledNotification 272 | DependsOn: 273 | - MainConfigurationRecorder 274 | - MainDeliveryChannel 275 | 276 | MainCaCReporterNotConfig: 277 | Condition: NoSetConfigMain 278 | Type: AWS::Config::ConfigRule 279 | Properties: 280 | ConfigRuleName: !Ref EngineComplianceRule 281 | Description: Check that the latest Compliance-as-code template is installed in this account. 282 | Source: 283 | Owner: CUSTOM_LAMBDA 284 | SourceIdentifier: !Join [ ":", [ 'arn:aws:lambda', !Ref "AWS::Region", !Ref ComplianceAccountId, 'function', !Ref LambdaFunctionName ] ] 285 | SourceDetails: 286 | - 287 | EventSource: "aws.config" 288 | MaximumExecutionFrequency: !Ref RuleFrequency 289 | MessageType: ScheduledNotification 290 | 291 | NotMainCaCReporter: 292 | Condition: SetConfigNotMain 293 | Type: AWS::Config::ConfigRule 294 | Properties: 295 | ConfigRuleName: !Ref EngineComplianceRule 296 | Description: Check that the latest template for the RuleSet is installed in this account. 297 | Source: 298 | Owner: CUSTOM_LAMBDA 299 | SourceIdentifier: !Join [ ":", [ 'arn:aws:lambda', !Ref "AWS::Region", !Ref ComplianceAccountId, 'function', !Ref LambdaFunctionName ] ] 300 | SourceDetails: 301 | - 302 | EventSource: "aws.config" 303 | MaximumExecutionFrequency: !Ref RuleFrequency 304 | MessageType: ScheduledNotification 305 | DependsOn: 306 | - NotMainConfigurationRecorder 307 | - NotMainDeliveryChannel 308 | 309 | NotMainCaCReporterNotConfig: 310 | Condition: NoSetConfigNotMain 311 | Type: AWS::Config::ConfigRule 312 | Properties: 313 | ConfigRuleName: !Ref EngineComplianceRule 314 | Description: Check that the latest template for the RuleSet is installed in this account. 315 | Source: 316 | Owner: CUSTOM_LAMBDA 317 | SourceIdentifier: !Join [ ":", [ 'arn:aws:lambda', !Ref "AWS::Region", !Ref ComplianceAccountId, 'function', !Ref LambdaFunctionName ] ] 318 | SourceDetails: 319 | - 320 | EventSource: "aws.config" 321 | MaximumExecutionFrequency: !Ref RuleFrequency 322 | MessageType: ScheduledNotification 323 | -------------------------------------------------------------------------------- /docs/images/AWS High-Level design for Compliance-as-code framework.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-config-engine-for-compliance-as-code/0ea788038f74d9c8fc6fd28741af8d3d8bb8fd59/docs/images/AWS High-Level design for Compliance-as-code framework.pptx -------------------------------------------------------------------------------- /docs/images/engine_hl_design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-config-engine-for-compliance-as-code/0ea788038f74d9c8fc6fd28741af8d3d8bb8fd59/docs/images/engine_hl_design.png -------------------------------------------------------------------------------- /docs/images/engine_ll_design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-config-engine-for-compliance-as-code/0ea788038f74d9c8fc6fd28741af8d3d8bb8fd59/docs/images/engine_ll_design.png -------------------------------------------------------------------------------- /rules/CMK_BACKING_KEY_ROTATION_ENABLED/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "CMK_BACKING_KEY_ROTATION_ENABLED", 5 | "SourceRuntime": null, 6 | "CodeKey": null, 7 | "InputParameters": "{}", 8 | "OptionalParameters": "{}", 9 | "SourcePeriodic": "TwentyFour_Hours", 10 | "SourceIdentifier": "CMK_BACKING_KEY_ROTATION_ENABLED", 11 | "RuleSets": [ 12 | "confidentiality:high", 13 | "confidentiality:medium", 14 | "rulecriticity:medium" 15 | ] 16 | }, 17 | "Tags": "[]" 18 | } -------------------------------------------------------------------------------- /rules/COMPLIANCE_RULESET_LATEST_INSTALLED/COMPLIANCE_RULESET_LATEST_INSTALLED_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | try: 4 | from unittest.mock import MagicMock, patch, ANY 5 | except ImportError: 6 | import mock 7 | from mock import MagicMock, patch, ANY 8 | import botocore 9 | from botocore.exceptions import ClientError 10 | 11 | ############## 12 | # Parameters # 13 | ############## 14 | 15 | # Define the default resource to report to Config Rules 16 | DEFAULT_RESOURCE_TYPE = 'AWS::::Account' 17 | 18 | ############# 19 | # Main Code # 20 | ############# 21 | 22 | config_client_mock = MagicMock() 23 | sts_client_mock = MagicMock() 24 | 25 | class Boto3Mock(): 26 | def client(self, client_name, *args, **kwargs): 27 | if client_name == 'config': 28 | return config_client_mock 29 | elif client_name == 'sts': 30 | return sts_client_mock 31 | else: 32 | raise Exception("Attempting to create an unknown client") 33 | 34 | sys.modules['boto3'] = Boto3Mock() 35 | 36 | rule = __import__('COMPLIANCE_RULESET_LATEST_INSTALLED') 37 | 38 | class SampleTest(unittest.TestCase): 39 | 40 | rule_parameters = '{"SomeParameterKey":"SomeParameterValue","SomeParameterKey2":"SomeParameterValue2"}' 41 | 42 | invoking_event_iam_role_sample = '{"configurationItem":{"relatedEvents":[],"relationships":[],"configuration":{},"tags":{},"configurationItemCaptureTime":"2018-07-02T03:37:52.418Z","awsAccountId":"123456789012","configurationItemStatus":"ResourceDiscovered","resourceType":"AWS::IAM::Role","resourceId":"some-resource-id","resourceName":"some-resource-name","ARN":"some-arn"},"notificationCreationTime":"2018-07-02T23:05:34.445Z","messageType":"ConfigurationItemChangeNotification"}' 43 | 44 | def setUp(self): 45 | pass 46 | 47 | def test_sample(self): 48 | self.assertTrue(True) 49 | 50 | def test_sample_2(self): 51 | rule.ASSUME_ROLE_MODE = False 52 | response = rule.lambda_handler(build_lambda_configurationchange_event(self.invoking_event_iam_role_sample, self.rule_parameters), {}) 53 | resp_expected = [] 54 | resp_expected.append(build_expected_response('NOT_APPLICABLE', 'some-resource-id', 'AWS::IAM::Role')) 55 | assert_successful_evaluation(self, response, resp_expected) 56 | 57 | #################### 58 | # Helper Functions # 59 | #################### 60 | 61 | def build_lambda_configurationchange_event(invoking_event, rule_parameters=None): 62 | event_to_return = { 63 | 'configRuleName':'myrule', 64 | 'executionRoleArn':'roleArn', 65 | 'eventLeftScope': False, 66 | 'invokingEvent': invoking_event, 67 | 'accountId': '123456789012', 68 | 'configRuleArn': 'arn:aws:config:us-east-1:123456789012:config-rule/config-rule-8fngan', 69 | 'resultToken':'token' 70 | } 71 | if rule_parameters: 72 | event_to_return['ruleParameters'] = rule_parameters 73 | return event_to_return 74 | 75 | def build_lambda_scheduled_event(rule_parameters=None): 76 | invoking_event = '{"messageType":"ScheduledNotification","notificationCreationTime":"2017-12-23T22:11:18.158Z"}' 77 | event_to_return = { 78 | 'configRuleName':'myrule', 79 | 'executionRoleArn':'roleArn', 80 | 'eventLeftScope': False, 81 | 'invokingEvent': invoking_event, 82 | 'accountId': '123456789012', 83 | 'configRuleArn': 'arn:aws:config:us-east-1:123456789012:config-rule/config-rule-8fngan', 84 | 'resultToken':'token' 85 | } 86 | if rule_parameters: 87 | event_to_return['ruleParameters'] = rule_parameters 88 | return event_to_return 89 | 90 | def build_expected_response(compliance_type, compliance_resource_id, compliance_resource_type=DEFAULT_RESOURCE_TYPE, annotation=None): 91 | if not annotation: 92 | return { 93 | 'ComplianceType': compliance_type, 94 | 'ComplianceResourceId': compliance_resource_id, 95 | 'ComplianceResourceType': compliance_resource_type 96 | } 97 | return { 98 | 'ComplianceType': compliance_type, 99 | 'ComplianceResourceId': compliance_resource_id, 100 | 'ComplianceResourceType': compliance_resource_type, 101 | 'Annotation': annotation 102 | } 103 | 104 | def assert_successful_evaluation(testClass, response, resp_expected, evaluations_count=1): 105 | if isinstance(response, dict): 106 | testClass.assertEquals(resp_expected['ComplianceType'], response['ComplianceType']) 107 | testClass.assertEquals(resp_expected['ComplianceResourceType'], response['ComplianceResourceType']) 108 | testClass.assertEquals(resp_expected['ComplianceResourceId'], response['ComplianceResourceId']) 109 | testClass.assertTrue(response['OrderingTimestamp']) 110 | if 'Annotation' in resp_expected or 'Annotation' in response: 111 | testClass.assertEquals(resp_expected['Annotation'], response['Annotation']) 112 | elif isinstance(response, list): 113 | testClass.assertEquals(evaluations_count, len(response)) 114 | for i, response_expected in enumerate(resp_expected): 115 | testClass.assertEquals(response_expected['ComplianceType'], response[i]['ComplianceType']) 116 | testClass.assertEquals(response_expected['ComplianceResourceType'], response[i]['ComplianceResourceType']) 117 | testClass.assertEquals(response_expected['ComplianceResourceId'], response[i]['ComplianceResourceId']) 118 | testClass.assertTrue(response[i]['OrderingTimestamp']) 119 | if 'Annotation' in response_expected or 'Annotation' in response[i]: 120 | testClass.assertEquals(response_expected['Annotation'], response[i]['Annotation']) 121 | 122 | def assert_customer_error_response(testClass, response, customerErrorCode=None, customerErrorMessage=None): 123 | if customerErrorCode: 124 | testClass.assertEqual(customerErrorCode, response['customerErrorCode']) 125 | if customerErrorMessage: 126 | testClass.assertEqual(customerErrorMessage, response['customerErrorMessage']) 127 | testClass.assertTrue(response['customerErrorCode']) 128 | testClass.assertTrue(response['customerErrorMessage']) 129 | if "internalErrorMessage" in response: 130 | testClass.assertTrue(response['internalErrorMessage']) 131 | if "internalErrorDetails" in response: 132 | testClass.assertTrue(response['internalErrorDetails']) 133 | 134 | def sts_mock(): 135 | assume_role_response = { 136 | "Credentials": { 137 | "AccessKeyId": "string", 138 | "SecretAccessKey": "string", 139 | "SessionToken": "string"}} 140 | sts_client_mock.reset_mock(return_value=True) 141 | sts_client_mock.assume_role = MagicMock(return_value=assume_role_response) 142 | 143 | ################## 144 | # Common Testing # 145 | ################## 146 | 147 | class TestStsErrors(unittest.TestCase): 148 | 149 | def test_sts_unknown_error(self): 150 | rule.ASSUME_ROLE_MODE = True 151 | sts_client_mock.assume_role = MagicMock(side_effect=botocore.exceptions.ClientError( 152 | {'Error': {'Code': 'unknown-code', 'Message': 'unknown-message'}}, 'operation')) 153 | response = rule.lambda_handler(build_lambda_configurationchange_event('{}'), {}) 154 | assert_customer_error_response( 155 | self, response, 'InternalError', 'InternalError') 156 | 157 | def test_sts_access_denied(self): 158 | rule.ASSUME_ROLE_MODE = True 159 | sts_client_mock.assume_role = MagicMock(side_effect=botocore.exceptions.ClientError( 160 | {'Error': {'Code': 'AccessDenied', 'Message': 'access-denied'}}, 'operation')) 161 | response = rule.lambda_handler(build_lambda_configurationchange_event('{}'), {}) 162 | assert_customer_error_response( 163 | self, response, 'AccessDenied', 'AWS Config does not have permission to assume the IAM role.') -------------------------------------------------------------------------------- /rules/COMPLIANCE_RULESET_LATEST_INSTALLED/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "COMPLIANCE_RULESET_LATEST_INSTALLED", 5 | "SourceRuntime": "python3.6", 6 | "CodeKey": "COMPLIANCE_RULESET_LATEST_INSTALLED.zip", 7 | "InputParameters": "{}", 8 | "OptionalParameters": "{}", 9 | "SourcePeriodic": "TwentyFour_Hours" 10 | } 11 | } -------------------------------------------------------------------------------- /rules/EBS_ENCRYPTED_VOLUMES_V2/EBS_ENCRYPTED_VOLUMES_V2_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | import unittest 4 | try: 5 | from unittest.mock import MagicMock, patch, ANY 6 | except ImportError: 7 | import mock 8 | from mock import MagicMock, patch, ANY 9 | import botocore 10 | from botocore.exceptions import ClientError 11 | 12 | ############## 13 | # Parameters # 14 | ############## 15 | 16 | # Define the default resource to report to Config Rules 17 | DEFAULT_RESOURCE_TYPE = 'AWS::EC2::Volume' 18 | 19 | ############# 20 | # Main Code # 21 | ############# 22 | 23 | config_client_mock = MagicMock() 24 | sts_client_mock = MagicMock() 25 | ec2_mock = MagicMock() 26 | 27 | class Boto3Mock(): 28 | def client(self, client_name, *args, **kwargs): 29 | if client_name == 'config': 30 | return config_client_mock 31 | elif client_name == 'sts': 32 | return sts_client_mock 33 | elif client_name == 'ec2': 34 | return ec2_mock 35 | else: 36 | raise Exception("Attempting to create an unknown client") 37 | 38 | sys.modules['boto3'] = Boto3Mock() 39 | 40 | rule = __import__('EBS_ENCRYPTED_VOLUMES_V2') 41 | 42 | 43 | def getRuleParameters(validity, paramName=None): 44 | validParameters = { 45 | "VolumeExceptionList": "vol-01", 46 | "SubnetExceptionList": "subnet-01", 47 | "KmsIdList": "415ee9cc-9beb-4217-bec8-45cabmfrbee6f" 48 | } 49 | invalidVolumeParams = [ 50 | "vol-050607259f67717d5, asdef", 51 | "1234", 52 | "vol23r4ts", 53 | "vol-948567,vol- 246934, vol-235646 vol-35446" 54 | ] 55 | invalidKmsKeyIdParams = [ 56 | "-9beb-4217-bec8-45cab7ase6f", 57 | "415ee9cc-9beb-4217-bec8-45cab7abee6f,415ee9cc9beb4217bec845cab7abee6f", 58 | "415ee9cc-9beb-4217-bec8-", 59 | "415ee9cc-9beb-4217-bec8-asff--sdvrvbrv" 60 | ] 61 | invalidSubnetParams = [ 62 | 'subnetd2cd14ba', 63 | 'd2cd14ba', 64 | 'subnet-d2cd14ba subnet-d2cd1443', 65 | 'd2cd14ba-subnet' 66 | ] 67 | if not validity: 68 | if paramName == 'VolumeExceptionList': 69 | return invalidVolumeParams 70 | if paramName == 'SubnetExceptionList': 71 | return invalidSubnetParams 72 | if paramName == 'KmsIdList': 73 | return invalidKmsKeyIdParams 74 | return validParameters 75 | 76 | def constructConfiguration(encrypted, volumeId, kmsKeyId=None, attachments=''): 77 | return { 78 | "encrypted":encrypted, 79 | "kmsKeyId":kmsKeyId, 80 | "volumeId":volumeId, 81 | "attachments":attachments 82 | } 83 | 84 | def constructConfigItem(configuration, volumeId): 85 | configItem = { 86 | 'relatedEvents': [], 87 | 'relationships': [], 88 | 'configuration': configuration, 89 | 'configurationItemVersion': "1.3", 90 | 'configurationItemCaptureTime': "2018-07-02T03:37:52.418Z", 91 | 'supplementaryConfiguration': {}, 92 | 'configurationStateId': 1532049940079, 93 | 'awsAccountId': "SAMPLE", 94 | 'configurationItemStatus': "ResourceDiscovered", 95 | 'resourceType': "AWS::EC2::Volume", 96 | 'resourceId': volumeId, 97 | 'resourceName': None, 98 | 'ARN': "arn:aws:ec2:ap-south-1:822333706:volume/{}".format(volumeId), 99 | 'awsRegion': "ap-south-1", 100 | 'configurationStateMd5Hash': "", 101 | 'resourceCreationTime': "2018-07-19T06:27:28.289Z", 102 | 'tags': {} 103 | } 104 | return configItem 105 | 106 | def constructInvokingEvent(configItem): 107 | invokingEvent = { 108 | "configurationItemDiff": None, 109 | "configurationItem": configItem, 110 | "notificationCreationTime": "SAMPLE", 111 | "messageType": "ConfigurationItemChangeNotification", 112 | "recordVersion": "SAMPLE" 113 | } 114 | return invokingEvent 115 | 116 | class InvalidParametersTest(unittest.TestCase): 117 | 118 | def test_Scenario_1_invalid_kmsKeyParameters(self): 119 | params = {"KmsIdList": "-1,s30c-du4-3erdft-"} 120 | configuration = constructConfiguration(encrypted=True, kmsKeyId='sdf434-dsvfb3-4545-dfvfdv', volumeId="vol-w4t4434") 121 | invoking_event = constructInvokingEvent(constructConfigItem(configuration, "volumeId")) 122 | lambdaEvent = build_lambda_configurationchange_event(invoking_event=invoking_event, rule_parameters=params) 123 | response = rule.lambda_handler(lambdaEvent, {}) 124 | assert_customer_error_response(self, response, 'InvalidParameterValueException') 125 | 126 | def test_Scenario_2_invalid_volumeParameters(self): 127 | params = {"VolumeExceptionList": "oool-0003,vol--0sd4e"} 128 | configuration = constructConfiguration(encrypted=True, kmsKeyId='sdf434-dsvfb3-4545-dfvfdv', volumeId="vol-w4t4434") 129 | invoking_event = constructInvokingEvent(constructConfigItem(configuration, "volumeId")) 130 | lambdaEvent = build_lambda_configurationchange_event(invoking_event=invoking_event, rule_parameters=params) 131 | response = rule.lambda_handler(lambdaEvent, {}) 132 | assert_customer_error_response(self, response, 'InvalidParameterValueException') 133 | 134 | def test_Scenario_3_invalid_subnetParameters(self): 135 | params = {"SubnetExceptionList": "aaasssubnet-02,subnet03edfy45,dhu47dh-subnet"} 136 | configuration = constructConfiguration(encrypted=True, kmsKeyId='sdf434-dsvfb3-4545-dfvfdv', volumeId="vol-w4t4434") 137 | invoking_event = constructInvokingEvent(constructConfigItem(configuration, "volumeId")) 138 | lambdaEvent = build_lambda_configurationchange_event(invoking_event=invoking_event, rule_parameters=params) 139 | response = rule.lambda_handler(lambdaEvent, {}) 140 | assert_customer_error_response(self, response, 'InvalidParameterValueException') 141 | 142 | class ComplianceTest(unittest.TestCase): 143 | 144 | def test_Scenario_4_volumeinVolumeExceptionList(self): 145 | rule_parameters = getRuleParameters(True, '') 146 | configuration = constructConfiguration(encrypted=False, volumeId="vol-01") 147 | invoking_event = constructInvokingEvent(constructConfigItem(configuration, "vol-01")) 148 | event = build_lambda_configurationchange_event(invoking_event, rule_parameters) 149 | response = rule.lambda_handler(event, {}) 150 | resp_expected = [] 151 | resp_expected.append(build_expected_response( 152 | 'COMPLIANT', 153 | 'vol-01', 154 | annotation='This EBS volume is part of the exception list.')) 155 | assert_successful_evaluation(self, response, resp_expected) 156 | 157 | def test_Scenario_6_volumeencrypted_noKMSparam(self): 158 | rule_parameters = {"VolumeExceptionList": "vol-0003", "SubnetExceptionList": "subnet-01"} 159 | configuration = constructConfiguration(encrypted=True, kmsKeyId='sdf434-dsvfb3-4545-dfvfdv', volumeId="vol-01") 160 | invoking_event = constructInvokingEvent(constructConfigItem(configuration, "vol-01")) 161 | event = build_lambda_configurationchange_event(invoking_event, rule_parameters) 162 | response = rule.lambda_handler(event, {}) 163 | resp_expected = [] 164 | resp_expected.append(build_expected_response( 165 | 'COMPLIANT', 166 | 'vol-01')) 167 | assert_successful_evaluation(self, response, resp_expected) 168 | 169 | def test_Scenario_5_volumeNOTencrypted_noKMSparam(self): 170 | rule_parameters = {"VolumeExceptionList": "vol-0003", "SubnetExceptionList": "subnet-01"} 171 | configuration = constructConfiguration(encrypted=False, volumeId="vol-01") 172 | invoking_event = constructInvokingEvent(constructConfigItem(configuration, "vol-01")) 173 | event = build_lambda_configurationchange_event(invoking_event, rule_parameters) 174 | response = rule.lambda_handler(event, {}) 175 | resp_expected = [] 176 | resp_expected.append(build_expected_response( 177 | 'NON_COMPLIANT', 178 | 'vol-01')) 179 | assert_successful_evaluation(self, response, resp_expected) 180 | 181 | def test_Scenario_7_volumeencrypted_KMSKeyInvalid(self): 182 | rule_parameters = getRuleParameters(True, '') 183 | configuration = constructConfiguration( 184 | encrypted=True, 185 | kmsKeyId='arn:aws:kms:region-all-1:123456798877:key/sdf434-dsvfb3-4545-dfvfdv', 186 | volumeId="vol-02") 187 | invoking_event = constructInvokingEvent(constructConfigItem(configuration, "vol-02")) 188 | event = build_lambda_configurationchange_event(invoking_event, rule_parameters) 189 | response = rule.lambda_handler(event, {}) 190 | resp_expected = [] 191 | resp_expected.append(build_expected_response( 192 | 'NON_COMPLIANT', 193 | 'vol-02', 194 | annotation='This EBS volume is encrypted, but not with a KMS Key listed in the parameter KmsIdList.')) 195 | assert_successful_evaluation(self, response, resp_expected) 196 | 197 | def test_Scenario_8_volumeencrypted_KMSKeyValid(self): 198 | rule_parameters = getRuleParameters(True, '') 199 | configuration = constructConfiguration( 200 | encrypted=True, 201 | kmsKeyId='arn:aws:kms:region-all-1:123456798877:key/415ee9cc-9beb-4217-bec8-45cabmfrbee6f', 202 | volumeId="vol-02") 203 | invoking_event = constructInvokingEvent(constructConfigItem(configuration, "vol-02")) 204 | event = build_lambda_configurationchange_event(invoking_event, rule_parameters) 205 | response = rule.lambda_handler(event, {}) 206 | resp_expected = [] 207 | resp_expected.append(build_expected_response( 208 | 'COMPLIANT', 209 | 'vol-02')) 210 | assert_successful_evaluation(self, response, resp_expected) 211 | 212 | def test_Scenario_9_volumeSubnetinSubnetExceptionList(self): 213 | ec2_mock.describe_instances = MagicMock(return_value={"Reservations":[{"Instances":[{"SubnetId":"subnet-02"}]}]}) 214 | rule_parameters = { 215 | "VolumeExceptionList": "vol-0003", 216 | "SubnetExceptionList": "subnet-02", 217 | "KmsIdList": "115ff9cc-9beb-4517-bec8-45cabmfrbee6f" 218 | } 219 | configuration = constructConfiguration(encrypted=False, volumeId="vol-01", attachments=[{"instanceId":"i-02"}]) 220 | invoking_event = constructInvokingEvent(constructConfigItem(configuration, "vol-01")) 221 | event = build_lambda_configurationchange_event(invoking_event, rule_parameters) 222 | response = rule.lambda_handler(event, {}) 223 | resp_expected = [] 224 | resp_expected.append(build_expected_response( 225 | 'COMPLIANT', 226 | 'vol-01', 227 | annotation='This EBS volume is attached to an EC2 instance in a subnet which is part the exception list.')) 228 | assert_successful_evaluation(self, response, resp_expected) 229 | 230 | def test_Scenario_10_volumeNotEncrSubnetNotinSubnetList(self): 231 | ec2_mock.describe_instances = MagicMock(return_value={"Reservations":[{"Instances":[{"SubnetId":"subnet-02"}]}]}) 232 | rule_parameters = getRuleParameters(True, '') 233 | configuration = constructConfiguration(encrypted=False, volumeId="vol-02", attachments=[{"instanceId":"i-02"}]) 234 | invoking_event = constructInvokingEvent(constructConfigItem(configuration, "vol-02")) 235 | event = build_lambda_configurationchange_event(invoking_event, rule_parameters) 236 | response = rule.lambda_handler(event, {}) 237 | resp_expected = [] 238 | resp_expected.append(build_expected_response( 239 | 'NON_COMPLIANT', 240 | 'vol-02')) 241 | assert_successful_evaluation(self, response, resp_expected) 242 | 243 | def test_Scenario_11_volumeEncryptedNoKMSNoSubnetExceptionNoVolumeException(self): 244 | ec2_mock.describe_instances = MagicMock(return_value={"Reservations":[{"Instances":[{"SubnetId":"subnet-02"}]}]}) 245 | rule_parameters = {"VolumeExceptionList": "vol-0003", "SubnetExceptionList": "subnet-01"} 246 | configuration = constructConfiguration( 247 | encrypted=True, 248 | kmsKeyId='arn:aws:kms:region-all-1:123456798877:key/415ee9cc-9beb-4217-bec8-45cabmfrbee6f', 249 | volumeId="vol-02", 250 | attachments=[{"instanceId":"i-02"}]) 251 | invoking_event = constructInvokingEvent(constructConfigItem(configuration, "vol-02")) 252 | event = build_lambda_configurationchange_event(invoking_event, rule_parameters) 253 | response = rule.lambda_handler(event, {}) 254 | resp_expected = [] 255 | resp_expected.append(build_expected_response( 256 | 'COMPLIANT', 257 | 'vol-02')) 258 | assert_successful_evaluation(self, response, resp_expected) 259 | 260 | def test_Scenario_12_volumeEncryptedNotWithProperKMSNoSubnetExceptionNoVolumeException(self): 261 | ec2_mock.describe_instances = MagicMock(return_value={"Reservations":[{"Instances":[{"SubnetId":"subnet-02"}]}]}) 262 | rule_parameters = { 263 | "VolumeExceptionList": "vol-0003", 264 | "SubnetExceptionList": "subnet-01", 265 | "KmsIdList": "115ff9cc-9beb-4517-bec8-45cabmfrbee6f" 266 | } 267 | configuration = constructConfiguration( 268 | encrypted=True, 269 | kmsKeyId='arn:aws:kms:region-all-1:123456798877:key/415ee9cc-9beb-4217-bec8-45cabmfrbee6f', 270 | volumeId="vol-02", 271 | attachments=[{"instanceId":"i-02"}]) 272 | invoking_event = constructInvokingEvent(constructConfigItem(configuration, "vol-02")) 273 | event = build_lambda_configurationchange_event(invoking_event, rule_parameters) 274 | response = rule.lambda_handler(event, {}) 275 | resp_expected = [] 276 | resp_expected.append(build_expected_response( 277 | 'NON_COMPLIANT', 278 | 'vol-02', 279 | annotation='This EBS volume is encrypted, but not with a KMS Key listed in the parameter KmsIdList.')) 280 | assert_successful_evaluation(self, response, resp_expected) 281 | 282 | def test_Scenario_13_volumeEncryptedWithProperKMSNoSubnetExceptionNoVolumeException(self): #Scenario13 283 | ec2_mock.describe_instances = MagicMock(return_value={"Reservations":[{"Instances":[{"SubnetId":"subnet-02"}]}]}) 284 | rule_parameters = getRuleParameters(True, '') 285 | configuration = constructConfiguration( 286 | encrypted=True, 287 | kmsKeyId='arn:aws:kms:region-all-1:123456798877:key/415ee9cc-9beb-4217-bec8-45cabmfrbee6f', 288 | volumeId="vol-02", 289 | attachments=[{"instanceId":"i-02"}] 290 | ) 291 | invoking_event = constructInvokingEvent(constructConfigItem(configuration, "vol-02asd")) 292 | event = build_lambda_configurationchange_event(invoking_event, rule_parameters) 293 | response = rule.lambda_handler(event, {}) 294 | resp_expected = [] 295 | resp_expected.append(build_expected_response( 296 | 'COMPLIANT', 297 | 'vol-02asd')) 298 | assert_successful_evaluation(self, response, resp_expected) 299 | 300 | #################### 301 | # Helper Functions # 302 | #################### 303 | 304 | def build_lambda_configurationchange_event(invoking_event, rule_parameters=None): 305 | event_to_return = { 306 | 'configRuleName':'myrule', 307 | 'executionRoleArn':'roleArn', 308 | 'eventLeftScope': False, 309 | 'invokingEvent': json.dumps(invoking_event), 310 | 'accountId': '123456789012', 311 | 'configRuleArn': 'arn:aws:config:us-east-1:123456789012:config-rule/config-rule-8fngan', 312 | 'resultToken':'token' 313 | } 314 | if rule_parameters: 315 | event_to_return['ruleParameters'] = json.dumps(rule_parameters) 316 | return event_to_return 317 | 318 | def build_lambda_scheduled_event(rule_parameters=None): 319 | invoking_event = '{"messageType":"ScheduledNotification","notificationCreationTime":"2017-12-23T22:11:18.158Z"}' 320 | event_to_return = { 321 | 'configRuleName':'myrule', 322 | 'executionRoleArn':'roleArn', 323 | 'eventLeftScope': False, 324 | 'invokingEvent': invoking_event, 325 | 'accountId': '123456789012', 326 | 'configRuleArn': 'arn:aws:config:us-east-1:123456789012:config-rule/config-rule-8fngan', 327 | 'resultToken':'token' 328 | } 329 | if rule_parameters: 330 | event_to_return['ruleParameters'] = rule_parameters 331 | return event_to_return 332 | 333 | def build_expected_response(compliance_type, compliance_resource_id, compliance_resource_type=DEFAULT_RESOURCE_TYPE, annotation=None): 334 | if not annotation: 335 | return { 336 | 'ComplianceType': compliance_type, 337 | 'ComplianceResourceId': compliance_resource_id, 338 | 'ComplianceResourceType': compliance_resource_type 339 | } 340 | return { 341 | 'ComplianceType': compliance_type, 342 | 'ComplianceResourceId': compliance_resource_id, 343 | 'ComplianceResourceType': compliance_resource_type, 344 | 'Annotation': annotation 345 | } 346 | 347 | def assert_successful_evaluation(testClass, response, resp_expected, evaluations_count=1): 348 | if isinstance(response, dict): 349 | testClass.assertEquals(resp_expected['ComplianceType'], response['ComplianceType']) 350 | testClass.assertEquals(resp_expected['ComplianceResourceType'], response['ComplianceResourceType']) 351 | testClass.assertEquals(resp_expected['ComplianceResourceId'], response['ComplianceResourceId']) 352 | testClass.assertTrue(response['OrderingTimestamp']) 353 | if 'Annotation' in resp_expected or 'Annotation' in response: 354 | testClass.assertEquals(resp_expected['Annotation'], response['Annotation']) 355 | elif isinstance(response, list): 356 | testClass.assertEquals(evaluations_count, len(response)) 357 | for i, response_expected in enumerate(resp_expected): 358 | testClass.assertEquals(response_expected['ComplianceType'], response[i]['ComplianceType']) 359 | testClass.assertEquals(response_expected['ComplianceResourceType'], response[i]['ComplianceResourceType']) 360 | testClass.assertEquals(response_expected['ComplianceResourceId'], response[i]['ComplianceResourceId']) 361 | testClass.assertTrue(response[i]['OrderingTimestamp']) 362 | if 'Annotation' in response_expected or 'Annotation' in response[i]: 363 | testClass.assertEquals(response_expected['Annotation'], response[i]['Annotation']) 364 | 365 | def assert_customer_error_response(testClass, response, customerErrorCode=None, customerErrorMessage=None): 366 | if customerErrorCode: 367 | testClass.assertEqual(customerErrorCode, response['customerErrorCode']) 368 | if customerErrorMessage: 369 | testClass.assertEqual(customerErrorMessage, response['customerErrorMessage']) 370 | testClass.assertTrue(response['customerErrorCode']) 371 | testClass.assertTrue(response['customerErrorMessage']) 372 | if "internalErrorMessage" in response: 373 | testClass.assertTrue(response['internalErrorMessage']) 374 | if "internalErrorDetails" in response: 375 | testClass.assertTrue(response['internalErrorDetails']) 376 | 377 | def sts_mock(): 378 | assume_role_response = { 379 | "Credentials": { 380 | "AccessKeyId": "string", 381 | "SecretAccessKey": "string", 382 | "SessionToken": "string"}} 383 | sts_client_mock.reset_mock(return_value=True) 384 | sts_client_mock.assume_role = MagicMock(return_value=assume_role_response) 385 | 386 | ################## 387 | # Common Testing # 388 | ################## 389 | 390 | class TestStsErrors(unittest.TestCase): 391 | 392 | def test_sts_unknown_error(self): 393 | rule.ASSUME_ROLE_MODE = True 394 | sts_client_mock.assume_role = MagicMock(side_effect=botocore.exceptions.ClientError( 395 | {'Error': {'Code': 'unknown-code', 'Message': 'unknown-message'}}, 'operation')) 396 | response = rule.lambda_handler(build_lambda_configurationchange_event('{}'), {}) 397 | assert_customer_error_response( 398 | self, response, 'InternalError', 'InternalError') 399 | 400 | def test_sts_access_denied(self): 401 | rule.ASSUME_ROLE_MODE = True 402 | sts_client_mock.assume_role = MagicMock(side_effect=botocore.exceptions.ClientError( 403 | {'Error': {'Code': 'AccessDenied', 'Message': 'access-denied'}}, 'operation')) 404 | response = rule.lambda_handler(build_lambda_configurationchange_event('{}'), {}) 405 | assert_customer_error_response( 406 | self, response, 'AccessDenied', 'AWS Config does not have permission to assume the IAM role.') 407 | -------------------------------------------------------------------------------- /rules/EBS_ENCRYPTED_VOLUMES_V2/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "EBS_ENCRYPTED_VOLUMES_V2", 5 | "SourceRuntime": "python3.6", 6 | "CodeKey": "EBS_ENCRYPTED_VOLUMES_V2.zip", 7 | "InputParameters": "{}", 8 | "OptionalParameters": "{\"VolumeExceptionList\": \"\", \"SubnetExceptionList\": \"\"}", 9 | "SourceEvents": "AWS::EC2::Volume", 10 | "RuleSets": [ 11 | "baseline", 12 | "rulecriticity:medium", 13 | "otherregionsbaseline" 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /rules/GUARDDUTY_ENABLED_CENTRALIZED/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "GUARDDUTY_ENABLED_CENTRALIZED", 5 | "SourceRuntime": null, 6 | "CodeKey": null, 7 | "InputParameters": "{}", 8 | "OptionalParameters": "{\"CentralMonitoringAccount\": \"\"}", 9 | "SourcePeriodic": "TwentyFour_Hours", 10 | "SourceIdentifier": "GUARDDUTY_ENABLED_CENTRALIZED", 11 | "RuleSets": [ 12 | "baseline", 13 | "rulecriticity:high", 14 | "otherregionsbaseline" 15 | ] 16 | }, 17 | "Tags": "[]" 18 | } -------------------------------------------------------------------------------- /rules/IAM_GROUP_NO_POLICY_FULL_STAR/IAM_GROUP_NO_POLICY_FULL_STAR_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | try: 4 | from unittest.mock import MagicMock, patch, ANY 5 | except ImportError: 6 | import mock 7 | from mock import MagicMock, patch, ANY 8 | import botocore 9 | from botocore.exceptions import ClientError 10 | 11 | ############## 12 | # Parameters # 13 | ############## 14 | 15 | # Define the default resource to report to Config Rules 16 | DEFAULT_RESOURCE_TYPE = 'AWS::IAM::Group' 17 | 18 | ############# 19 | # Main Code # 20 | ############# 21 | 22 | config_client_mock = MagicMock() 23 | sts_client_mock = MagicMock() 24 | iam_client_mock = MagicMock() 25 | 26 | class Boto3Mock(): 27 | def client(self, client_name, *args, **kwargs): 28 | if client_name == 'config': 29 | return config_client_mock 30 | elif client_name == 'sts': 31 | return sts_client_mock 32 | elif client_name == 'iam': 33 | return iam_client_mock 34 | else: 35 | raise Exception("Attempting to create an unknown client") 36 | 37 | sys.modules['boto3'] = Boto3Mock() 38 | 39 | rule = __import__('IAM_GROUP_NO_POLICY_FULL_STAR') 40 | 41 | class ComplianceTest(unittest.TestCase): 42 | 43 | invoking_event = '{"configurationItemDiff":"SomeDifference", "notificationCreationTime":"SomeTime", "messageType":"ConfigurationItemChangeNotification", "recordVersion":"SomeVersion", "configurationItem":{ "resourceType":"AWS::IAM::Group","configurationItemStatus":"ResourceDiscovered", "resourceId":"AIDAICVB3PKAQMPEGDW2C", "configurationItemCaptureTime":"2018-02-20T06:56:55.533Z", "configuration":{"groupName": "somegroupname"}}}' 44 | 45 | list_group_policy_names = {'PolicyNames': ['policyname1', 'policyname2']} 46 | get_group_policy_doc = {'PolicyDocument': '{"Statement": [{"Effect": "Allow", "Action": "*"}]}'} 47 | no_list_group_policy_names = {'PolicyNames': []} 48 | list_attached_policy_arn = {'AttachedPolicies': [{'PolicyArn': 'arn1'},{'PolicyArn': 'arn2'}]} 49 | get_policy = {'Policy': {'DefaultVersionId': 'v2'}} 50 | get_managed_policy_doc_allow = {"PolicyVersion": {"Document": {"Statement": [{"Effect": "Allow", "Action": "*"}]}}} 51 | get_managed_policy_doc_deny = {"PolicyVersion": {"Document": {"Statement": [{"Effect": "Deny", "Action": "*"}]}}} 52 | 53 | def test_non_compliant_inline(self): 54 | iam_client_mock.list_group_policies = MagicMock(return_value=self.list_group_policy_names) 55 | iam_client_mock.get_group_policy = MagicMock(return_value=self.get_group_policy_doc) 56 | lambdaEvent = build_lambda_configurationchange_event(invoking_event=self.invoking_event) 57 | response = rule.lambda_handler(lambdaEvent, {}) 58 | resp_expected = [] 59 | resp_expected.append(build_expected_response('NON_COMPLIANT', 'AIDAICVB3PKAQMPEGDW2C', annotation='An inline policy attached to the group has full star allow permissions.')) 60 | assert_successful_evaluation(self, response, resp_expected) 61 | 62 | def test_non_compliant_managed(self): 63 | iam_client_mock.list_group_policies = MagicMock(return_value=self.no_list_group_policy_names) 64 | iam_client_mock.list_attached_group_policies = MagicMock(return_value=self.list_attached_policy_arn) 65 | iam_client_mock.get_policy = MagicMock(return_value=self.get_policy) 66 | iam_client_mock.get_policy_version = MagicMock(return_value=self.get_managed_policy_doc_allow) 67 | lambdaEvent = build_lambda_configurationchange_event(invoking_event=self.invoking_event) 68 | response = rule.lambda_handler(lambdaEvent, {}) 69 | resp_expected = [] 70 | resp_expected.append(build_expected_response('NON_COMPLIANT', 'AIDAICVB3PKAQMPEGDW2C', annotation='A managed policy attached to the group has full star allow permissions.')) 71 | assert_successful_evaluation(self, response, resp_expected) 72 | 73 | def test_compliant_managed(self): 74 | iam_client_mock.list_group_policies = MagicMock(return_value=self.no_list_group_policy_names) 75 | iam_client_mock.list_attached_group_policies = MagicMock(return_value=self.list_attached_policy_arn) 76 | iam_client_mock.get_policy = MagicMock(return_value=self.get_policy) 77 | iam_client_mock.get_policy_version = MagicMock(return_value=self.get_managed_policy_doc_deny) 78 | lambdaEvent = build_lambda_configurationchange_event(invoking_event=self.invoking_event) 79 | response = rule.lambda_handler(lambdaEvent, {}) 80 | resp_expected = [] 81 | resp_expected.append(build_expected_response('COMPLIANT', 'AIDAICVB3PKAQMPEGDW2C')) 82 | assert_successful_evaluation(self, response, resp_expected) 83 | 84 | #################### 85 | # Helper Functions # 86 | #################### 87 | 88 | def build_lambda_configurationchange_event(invoking_event, rule_parameters=None): 89 | event_to_return = { 90 | 'configRuleName':'myrule', 91 | 'executionRoleArn':'roleArn', 92 | 'eventLeftScope': False, 93 | 'invokingEvent': invoking_event, 94 | 'accountId': '123456789012', 95 | 'configRuleArn': 'arn:aws:config:us-east-1:123456789012:config-rule/config-rule-8fngan', 96 | 'resultToken':'token' 97 | } 98 | if rule_parameters: 99 | event_to_return['ruleParameters'] = rule_parameters 100 | return event_to_return 101 | 102 | def build_lambda_scheduled_event(rule_parameters=None): 103 | invoking_event = '{"messageType":"ScheduledNotification","notificationCreationTime":"2017-12-23T22:11:18.158Z"}' 104 | event_to_return = { 105 | 'configRuleName':'myrule', 106 | 'executionRoleArn':'roleArn', 107 | 'eventLeftScope': False, 108 | 'invokingEvent': invoking_event, 109 | 'accountId': '123456789012', 110 | 'configRuleArn': 'arn:aws:config:us-east-1:123456789012:config-rule/config-rule-8fngan', 111 | 'resultToken':'token' 112 | } 113 | if rule_parameters: 114 | event_to_return['ruleParameters'] = rule_parameters 115 | return event_to_return 116 | 117 | def build_expected_response(compliance_type, compliance_resource_id, compliance_resource_type=DEFAULT_RESOURCE_TYPE, annotation=None): 118 | if not annotation: 119 | return { 120 | 'ComplianceType': compliance_type, 121 | 'ComplianceResourceId': compliance_resource_id, 122 | 'ComplianceResourceType': compliance_resource_type 123 | } 124 | return { 125 | 'ComplianceType': compliance_type, 126 | 'ComplianceResourceId': compliance_resource_id, 127 | 'ComplianceResourceType': compliance_resource_type, 128 | 'Annotation': annotation 129 | } 130 | 131 | def assert_successful_evaluation(testClass, response, resp_expected, evaluations_count=1): 132 | if isinstance(response, dict): 133 | testClass.assertEquals(resp_expected['ComplianceResourceType'], response['ComplianceResourceType']) 134 | testClass.assertEquals(resp_expected['ComplianceResourceId'], response['ComplianceResourceId']) 135 | testClass.assertEquals(resp_expected['ComplianceType'], response['ComplianceType']) 136 | testClass.assertTrue(response['OrderingTimestamp']) 137 | if 'Annotation' in resp_expected or 'Annotation' in response: 138 | testClass.assertEquals(resp_expected['Annotation'], response['Annotation']) 139 | elif isinstance(response, list): 140 | testClass.assertEquals(evaluations_count, len(response)) 141 | for i, response_expected in enumerate(resp_expected): 142 | testClass.assertEquals(response_expected['ComplianceResourceType'], response[i]['ComplianceResourceType']) 143 | testClass.assertEquals(response_expected['ComplianceResourceId'], response[i]['ComplianceResourceId']) 144 | testClass.assertEquals(response_expected['ComplianceType'], response[i]['ComplianceType']) 145 | testClass.assertTrue(response[i]['OrderingTimestamp']) 146 | if 'Annotation' in response_expected or 'Annotation' in response[i]: 147 | testClass.assertEquals(response_expected['Annotation'], response[i]['Annotation']) 148 | 149 | def assert_customer_error_response(testClass, response, customerErrorCode=None, customerErrorMessage=None): 150 | if customerErrorCode: 151 | testClass.assertEqual(customerErrorCode, response['customerErrorCode']) 152 | if customerErrorMessage: 153 | testClass.assertEqual(customerErrorMessage, response['customerErrorMessage']) 154 | testClass.assertTrue(response['customerErrorCode']) 155 | testClass.assertTrue(response['customerErrorMessage']) 156 | if "internalErrorMessage" in response: 157 | testClass.assertTrue(response['internalErrorMessage']) 158 | if "internalErrorDetails" in response: 159 | testClass.assertTrue(response['internalErrorDetails']) 160 | 161 | def sts_mock(): 162 | assume_role_response = { 163 | "Credentials": { 164 | "AccessKeyId": "string", 165 | "SecretAccessKey": "string", 166 | "SessionToken": "string"}} 167 | sts_client_mock.reset_mock(return_value=True) 168 | sts_client_mock.assume_role = MagicMock(return_value=assume_role_response) 169 | 170 | ################## 171 | # Common Testing # 172 | ################## 173 | 174 | class TestStsErrors(unittest.TestCase): 175 | 176 | def test_sts_unknown_error(self): 177 | rule.ASSUME_ROLE_MODE = True 178 | sts_client_mock.assume_role = MagicMock(side_effect=botocore.exceptions.ClientError( 179 | {'Error': {'Code': 'unknown-code', 'Message': 'unknown-message'}}, 'operation')) 180 | response = rule.lambda_handler(build_lambda_configurationchange_event('{}'), {}) 181 | assert_customer_error_response( 182 | self, response, 'InternalError', 'InternalError') 183 | 184 | def test_sts_access_denied(self): 185 | rule.ASSUME_ROLE_MODE = True 186 | sts_client_mock.assume_role = MagicMock(side_effect=botocore.exceptions.ClientError( 187 | {'Error': {'Code': 'AccessDenied', 'Message': 'access-denied'}}, 'operation')) 188 | response = rule.lambda_handler(build_lambda_configurationchange_event('{}'), {}) 189 | assert_customer_error_response( 190 | self, response, 'AccessDenied', 'AWS Config does not have permission to assume the IAM role.') 191 | -------------------------------------------------------------------------------- /rules/IAM_GROUP_NO_POLICY_FULL_STAR/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "IAM_GROUP_NO_POLICY_FULL_STAR", 5 | "SourceRuntime": "python3.6", 6 | "CodeKey": "IAM_GROUP_NO_POLICY_FULL_STAR.zip", 7 | "InputParameters": "{}", 8 | "OptionalParameters": "{}", 9 | "SourceEvents": "AWS::IAM::Group", 10 | "RuleSets": [ 11 | "baseline", 12 | "rulecriticity:high" 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /rules/IAM_PASSWORD_POLICY/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "IAM_PASSWORD_POLICY", 5 | "SourceRuntime": null, 6 | "CodeKey": null, 7 | "InputParameters": "{\"RequireUppercaseCharacters\":\"true\",\"RequireLowercaseCharacters\":\"true\",\"RequireSymbols\":\"true\",\"RequireNumbers\":\"true\",\"MinimumPasswordLength\":\"14\",\"PasswordReusePrevention\":\"24\",\"MaxPasswordAge\":\"90\"}", 8 | "OptionalParameters": "{}", 9 | "SourcePeriodic": "TwentyFour_Hours", 10 | "SourceIdentifier": "IAM_PASSWORD_POLICY", 11 | "RuleSets": [ 12 | "baseline", 13 | "rulecriticity:high" 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /rules/IAM_ROLE_NO_POLICY_FULL_STAR/IAM_ROLE_NO_POLICY_FULL_STAR_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | try: 4 | from unittest.mock import MagicMock, patch, ANY 5 | except ImportError: 6 | import mock 7 | from mock import MagicMock, patch, ANY 8 | import botocore 9 | from botocore.exceptions import ClientError 10 | 11 | ############## 12 | # Parameters # 13 | ############## 14 | 15 | # Define the default resource to report to Config Rules 16 | DEFAULT_RESOURCE_TYPE = 'AWS::IAM::Role' 17 | 18 | ############# 19 | # Main Code # 20 | ############# 21 | 22 | config_client_mock = MagicMock() 23 | sts_client_mock = MagicMock() 24 | iam_client_mock = MagicMock() 25 | 26 | class Boto3Mock(): 27 | def client(self, client_name, *args, **kwargs): 28 | if client_name == 'config': 29 | return config_client_mock 30 | elif client_name == 'sts': 31 | return sts_client_mock 32 | elif client_name == 'iam': 33 | return iam_client_mock 34 | else: 35 | raise Exception("Attempting to create an unknown client") 36 | 37 | sys.modules['boto3'] = Boto3Mock() 38 | 39 | rule = __import__('IAM_ROLE_NO_POLICY_FULL_STAR') 40 | 41 | class ComplianceTest(unittest.TestCase): 42 | 43 | invoking_event = '{"configurationItemDiff":"SomeDifference", "notificationCreationTime":"SomeTime", "messageType":"ConfigurationItemChangeNotification", "recordVersion":"SomeVersion", "configurationItem":{ "resourceType":"AWS::IAM::Role","configurationItemStatus":"ResourceDiscovered", "resourceId":"AIDAICVB3PKAQMPEGDW2C", "configurationItemCaptureTime":"2018-02-20T06:56:55.533Z", "configuration":{"roleName": "somerolename"}}}' 44 | 45 | list_role_policy_names = {'PolicyNames': ['policyname1', 'policyname2']} 46 | get_role_policy_doc = {'PolicyDocument': '{"Statement": [{"Effect": "Allow", "Action": "*"}]}'} 47 | no_list_role_policy_names = {'PolicyNames': []} 48 | list_attached_policy_arn = {'AttachedPolicies': [{'PolicyArn': 'arn1'},{'PolicyArn': 'arn2'}]} 49 | get_policy = {'Policy': {'DefaultVersionId': 'v2'}} 50 | get_managed_policy_doc_allow = {"PolicyVersion": {"Document": {"Statement": [{"Effect": "Allow", "Action": "*"}]}}} 51 | get_managed_policy_doc_deny = {"PolicyVersion": {"Document": {"Statement": [{"Effect": "Deny", "Action": "*"}]}}} 52 | 53 | def test_non_compliant_inline(self): 54 | iam_client_mock.list_role_policies = MagicMock(return_value=self.list_role_policy_names) 55 | iam_client_mock.get_role_policy = MagicMock(return_value=self.get_role_policy_doc) 56 | lambdaEvent = build_lambda_configurationchange_event(invoking_event=self.invoking_event) 57 | response = rule.lambda_handler(lambdaEvent, {}) 58 | resp_expected = [] 59 | resp_expected.append(build_expected_response('NON_COMPLIANT', 'AIDAICVB3PKAQMPEGDW2C', annotation='An inline policy attached to the role has full star allow permissions.')) 60 | assert_successful_evaluation(self, response, resp_expected) 61 | 62 | def test_non_compliant_managed(self): 63 | iam_client_mock.list_role_policies = MagicMock(return_value=self.no_list_role_policy_names) 64 | iam_client_mock.list_attached_role_policies = MagicMock(return_value=self.list_attached_policy_arn) 65 | iam_client_mock.get_policy = MagicMock(return_value=self.get_policy) 66 | iam_client_mock.get_policy_version = MagicMock(return_value=self.get_managed_policy_doc_allow) 67 | lambdaEvent = build_lambda_configurationchange_event(invoking_event=self.invoking_event) 68 | response = rule.lambda_handler(lambdaEvent, {}) 69 | resp_expected = [] 70 | resp_expected.append(build_expected_response('NON_COMPLIANT', 'AIDAICVB3PKAQMPEGDW2C', annotation='A managed policy attached to the role has full star allow permissions.')) 71 | assert_successful_evaluation(self, response, resp_expected) 72 | 73 | def test_compliant_managed(self): 74 | iam_client_mock.list_role_policies = MagicMock(return_value=self.no_list_role_policy_names) 75 | iam_client_mock.list_attached_role_policies = MagicMock(return_value=self.list_attached_policy_arn) 76 | iam_client_mock.get_policy = MagicMock(return_value=self.get_policy) 77 | iam_client_mock.get_policy_version = MagicMock(return_value=self.get_managed_policy_doc_deny) 78 | lambdaEvent = build_lambda_configurationchange_event(invoking_event=self.invoking_event) 79 | response = rule.lambda_handler(lambdaEvent, {}) 80 | resp_expected = [] 81 | resp_expected.append(build_expected_response('COMPLIANT', 'AIDAICVB3PKAQMPEGDW2C')) 82 | assert_successful_evaluation(self, response, resp_expected) 83 | 84 | #################### 85 | # Helper Functions # 86 | #################### 87 | 88 | def build_lambda_configurationchange_event(invoking_event, rule_parameters=None): 89 | event_to_return = { 90 | 'configRuleName':'myrule', 91 | 'executionRoleArn':'roleArn', 92 | 'eventLeftScope': False, 93 | 'invokingEvent': invoking_event, 94 | 'accountId': '123456789012', 95 | 'configRuleArn': 'arn:aws:config:us-east-1:123456789012:config-rule/config-rule-8fngan', 96 | 'resultToken':'token' 97 | } 98 | if rule_parameters: 99 | event_to_return['ruleParameters'] = rule_parameters 100 | return event_to_return 101 | 102 | def build_lambda_scheduled_event(rule_parameters=None): 103 | invoking_event = '{"messageType":"ScheduledNotification","notificationCreationTime":"2017-12-23T22:11:18.158Z"}' 104 | event_to_return = { 105 | 'configRuleName':'myrule', 106 | 'executionRoleArn':'roleArn', 107 | 'eventLeftScope': False, 108 | 'invokingEvent': invoking_event, 109 | 'accountId': '123456789012', 110 | 'configRuleArn': 'arn:aws:config:us-east-1:123456789012:config-rule/config-rule-8fngan', 111 | 'resultToken':'token' 112 | } 113 | if rule_parameters: 114 | event_to_return['ruleParameters'] = rule_parameters 115 | return event_to_return 116 | 117 | def build_expected_response(compliance_type, compliance_resource_id, compliance_resource_type=DEFAULT_RESOURCE_TYPE, annotation=None): 118 | if not annotation: 119 | return { 120 | 'ComplianceType': compliance_type, 121 | 'ComplianceResourceId': compliance_resource_id, 122 | 'ComplianceResourceType': compliance_resource_type 123 | } 124 | return { 125 | 'ComplianceType': compliance_type, 126 | 'ComplianceResourceId': compliance_resource_id, 127 | 'ComplianceResourceType': compliance_resource_type, 128 | 'Annotation': annotation 129 | } 130 | 131 | def assert_successful_evaluation(testClass, response, resp_expected, evaluations_count=1): 132 | if isinstance(response, dict): 133 | testClass.assertEquals(resp_expected['ComplianceResourceType'], response['ComplianceResourceType']) 134 | testClass.assertEquals(resp_expected['ComplianceResourceId'], response['ComplianceResourceId']) 135 | testClass.assertEquals(resp_expected['ComplianceType'], response['ComplianceType']) 136 | testClass.assertTrue(response['OrderingTimestamp']) 137 | if 'Annotation' in resp_expected or 'Annotation' in response: 138 | testClass.assertEquals(resp_expected['Annotation'], response['Annotation']) 139 | elif isinstance(response, list): 140 | testClass.assertEquals(evaluations_count, len(response)) 141 | for i, response_expected in enumerate(resp_expected): 142 | testClass.assertEquals(response_expected['ComplianceResourceType'], response[i]['ComplianceResourceType']) 143 | testClass.assertEquals(response_expected['ComplianceResourceId'], response[i]['ComplianceResourceId']) 144 | testClass.assertEquals(response_expected['ComplianceType'], response[i]['ComplianceType']) 145 | testClass.assertTrue(response[i]['OrderingTimestamp']) 146 | if 'Annotation' in response_expected or 'Annotation' in response[i]: 147 | testClass.assertEquals(response_expected['Annotation'], response[i]['Annotation']) 148 | 149 | def assert_customer_error_response(testClass, response, customerErrorCode=None, customerErrorMessage=None): 150 | if customerErrorCode: 151 | testClass.assertEqual(customerErrorCode, response['customerErrorCode']) 152 | if customerErrorMessage: 153 | testClass.assertEqual(customerErrorMessage, response['customerErrorMessage']) 154 | testClass.assertTrue(response['customerErrorCode']) 155 | testClass.assertTrue(response['customerErrorMessage']) 156 | if "internalErrorMessage" in response: 157 | testClass.assertTrue(response['internalErrorMessage']) 158 | if "internalErrorDetails" in response: 159 | testClass.assertTrue(response['internalErrorDetails']) 160 | 161 | def sts_mock(): 162 | assume_role_response = { 163 | "Credentials": { 164 | "AccessKeyId": "string", 165 | "SecretAccessKey": "string", 166 | "SessionToken": "string"}} 167 | sts_client_mock.reset_mock(return_value=True) 168 | sts_client_mock.assume_role = MagicMock(return_value=assume_role_response) 169 | 170 | ################## 171 | # Common Testing # 172 | ################## 173 | 174 | class TestStsErrors(unittest.TestCase): 175 | 176 | def test_sts_unknown_error(self): 177 | rule.ASSUME_ROLE_MODE = True 178 | sts_client_mock.assume_role = MagicMock(side_effect=botocore.exceptions.ClientError( 179 | {'Error': {'Code': 'unknown-code', 'Message': 'unknown-message'}}, 'operation')) 180 | response = rule.lambda_handler(build_lambda_configurationchange_event('{}'), {}) 181 | assert_customer_error_response( 182 | self, response, 'InternalError', 'InternalError') 183 | 184 | def test_sts_access_denied(self): 185 | rule.ASSUME_ROLE_MODE = True 186 | sts_client_mock.assume_role = MagicMock(side_effect=botocore.exceptions.ClientError( 187 | {'Error': {'Code': 'AccessDenied', 'Message': 'access-denied'}}, 'operation')) 188 | response = rule.lambda_handler(build_lambda_configurationchange_event('{}'), {}) 189 | assert_customer_error_response( 190 | self, response, 'AccessDenied', 'AWS Config does not have permission to assume the IAM role.') 191 | -------------------------------------------------------------------------------- /rules/IAM_ROLE_NO_POLICY_FULL_STAR/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "IAM_ROLE_NO_POLICY_FULL_STAR", 5 | "SourceRuntime": "python3.6", 6 | "CodeKey": "IAM_ROLE_NO_POLICY_FULL_STAR.zip", 7 | "InputParameters": "{}", 8 | "OptionalParameters": "{}", 9 | "SourceEvents": "AWS::IAM::Role", 10 | "RuleSets": [ 11 | "baseline", 12 | "rulecriticity:high" 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /rules/IAM_USER_MFA_ENABLED/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "IAM_USER_MFA_ENABLED", 5 | "SourceRuntime": null, 6 | "CodeKey": null, 7 | "InputParameters": "{}", 8 | "OptionalParameters": "{}", 9 | "SourcePeriodic": "TwentyFour_Hours", 10 | "SourceIdentifier": "IAM_USER_MFA_ENABLED", 11 | "RuleSets": [ 12 | "baseline", 13 | "rulecriticity:high" 14 | ] 15 | }, 16 | "Tags": "[]" 17 | } -------------------------------------------------------------------------------- /rules/IAM_USER_NO_POLICY_FULL_STAR/IAM_USER_NO_POLICY_FULL_STAR_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | try: 4 | from unittest.mock import MagicMock, patch, ANY 5 | except ImportError: 6 | import mock 7 | from mock import MagicMock, patch, ANY 8 | import botocore 9 | from botocore.exceptions import ClientError 10 | 11 | ############## 12 | # Parameters # 13 | ############## 14 | 15 | # Define the default resource to report to Config Rules 16 | DEFAULT_RESOURCE_TYPE = 'AWS::IAM::User' 17 | 18 | ############# 19 | # Main Code # 20 | ############# 21 | 22 | config_client_mock = MagicMock() 23 | sts_client_mock = MagicMock() 24 | iam_client_mock = MagicMock() 25 | 26 | class Boto3Mock(): 27 | def client(self, client_name, *args, **kwargs): 28 | if client_name == 'config': 29 | return config_client_mock 30 | elif client_name == 'sts': 31 | return sts_client_mock 32 | elif client_name == 'iam': 33 | return iam_client_mock 34 | else: 35 | raise Exception("Attempting to create an unknown client") 36 | 37 | sys.modules['boto3'] = Boto3Mock() 38 | 39 | rule = __import__('IAM_USER_NO_POLICY_FULL_STAR') 40 | 41 | class ComplianceTest(unittest.TestCase): 42 | 43 | invoking_event = '{"configurationItemDiff":"SomeDifference", "notificationCreationTime":"SomeTime", "messageType":"ConfigurationItemChangeNotification", "recordVersion":"SomeVersion", "configurationItem":{ "resourceType":"AWS::IAM::User","configurationItemStatus":"ResourceDiscovered", "resourceId":"AIDAICVB3PKAQMPEGDW2C", "configurationItemCaptureTime":"2018-02-20T06:56:55.533Z", "configuration":{"userName": "someusername"}}}' 44 | 45 | list_user_policy_names = {'PolicyNames': ['policyname1', 'policyname2']} 46 | get_user_policy_doc = {'PolicyDocument': '{"Statement": [{"Effect": "Allow", "Action": "*"}]}'} 47 | no_list_user_policy_names = {'PolicyNames': []} 48 | list_attached_policy_arn = {'AttachedPolicies': [{'PolicyArn': 'arn1'},{'PolicyArn': 'arn2'}]} 49 | get_policy = {'Policy': {'DefaultVersionId': 'v2'}} 50 | get_managed_policy_doc_allow = {"PolicyVersion": {"Document": {"Statement": [{"Effect": "Allow", "Action": "*"}]}}} 51 | get_managed_policy_doc_deny = {"PolicyVersion": {"Document": {"Statement": [{"Effect": "Deny", "Action": "*"}]}}} 52 | 53 | def test_non_compliant_inline(self): 54 | iam_client_mock.list_user_policies = MagicMock(return_value=self.list_user_policy_names) 55 | iam_client_mock.get_user_policy = MagicMock(return_value=self.get_user_policy_doc) 56 | lambdaEvent = build_lambda_configurationchange_event(invoking_event=self.invoking_event) 57 | response = rule.lambda_handler(lambdaEvent, {}) 58 | resp_expected = [] 59 | resp_expected.append(build_expected_response('NON_COMPLIANT', 'AIDAICVB3PKAQMPEGDW2C', annotation='An inline policy attached to the user has full star allow permissions.')) 60 | assert_successful_evaluation(self, response, resp_expected) 61 | 62 | def test_non_compliant_managed(self): 63 | iam_client_mock.list_user_policies = MagicMock(return_value=self.no_list_user_policy_names) 64 | iam_client_mock.list_attached_user_policies = MagicMock(return_value=self.list_attached_policy_arn) 65 | iam_client_mock.get_policy = MagicMock(return_value=self.get_policy) 66 | iam_client_mock.get_policy_version = MagicMock(return_value=self.get_managed_policy_doc_allow) 67 | lambdaEvent = build_lambda_configurationchange_event(invoking_event=self.invoking_event) 68 | response = rule.lambda_handler(lambdaEvent, {}) 69 | resp_expected = [] 70 | resp_expected.append(build_expected_response('NON_COMPLIANT', 'AIDAICVB3PKAQMPEGDW2C', annotation='A managed policy attached to the user has full star allow permissions.')) 71 | assert_successful_evaluation(self, response, resp_expected) 72 | 73 | def test_compliant_managed(self): 74 | iam_client_mock.list_user_policies = MagicMock(return_value=self.no_list_user_policy_names) 75 | iam_client_mock.list_attached_user_policies = MagicMock(return_value=self.list_attached_policy_arn) 76 | iam_client_mock.get_policy = MagicMock(return_value=self.get_policy) 77 | iam_client_mock.get_policy_version = MagicMock(return_value=self.get_managed_policy_doc_deny) 78 | lambdaEvent = build_lambda_configurationchange_event(invoking_event=self.invoking_event) 79 | response = rule.lambda_handler(lambdaEvent, {}) 80 | resp_expected = [] 81 | resp_expected.append(build_expected_response('COMPLIANT', 'AIDAICVB3PKAQMPEGDW2C')) 82 | assert_successful_evaluation(self, response, resp_expected) 83 | 84 | #################### 85 | # Helper Functions # 86 | #################### 87 | 88 | def build_lambda_configurationchange_event(invoking_event, rule_parameters=None): 89 | event_to_return = { 90 | 'configRuleName':'myrule', 91 | 'executionRoleArn':'roleArn', 92 | 'eventLeftScope': False, 93 | 'invokingEvent': invoking_event, 94 | 'accountId': '123456789012', 95 | 'configRuleArn': 'arn:aws:config:us-east-1:123456789012:config-rule/config-rule-8fngan', 96 | 'resultToken':'token' 97 | } 98 | if rule_parameters: 99 | event_to_return['ruleParameters'] = rule_parameters 100 | return event_to_return 101 | 102 | def build_lambda_scheduled_event(rule_parameters=None): 103 | invoking_event = '{"messageType":"ScheduledNotification","notificationCreationTime":"2017-12-23T22:11:18.158Z"}' 104 | event_to_return = { 105 | 'configRuleName':'myrule', 106 | 'executionRoleArn':'roleArn', 107 | 'eventLeftScope': False, 108 | 'invokingEvent': invoking_event, 109 | 'accountId': '123456789012', 110 | 'configRuleArn': 'arn:aws:config:us-east-1:123456789012:config-rule/config-rule-8fngan', 111 | 'resultToken':'token' 112 | } 113 | if rule_parameters: 114 | event_to_return['ruleParameters'] = rule_parameters 115 | return event_to_return 116 | 117 | def build_expected_response(compliance_type, compliance_resource_id, compliance_resource_type=DEFAULT_RESOURCE_TYPE, annotation=None): 118 | if not annotation: 119 | return { 120 | 'ComplianceType': compliance_type, 121 | 'ComplianceResourceId': compliance_resource_id, 122 | 'ComplianceResourceType': compliance_resource_type 123 | } 124 | return { 125 | 'ComplianceType': compliance_type, 126 | 'ComplianceResourceId': compliance_resource_id, 127 | 'ComplianceResourceType': compliance_resource_type, 128 | 'Annotation': annotation 129 | } 130 | 131 | def assert_successful_evaluation(testClass, response, resp_expected, evaluations_count=1): 132 | if isinstance(response, dict): 133 | testClass.assertEquals(resp_expected['ComplianceResourceType'], response['ComplianceResourceType']) 134 | testClass.assertEquals(resp_expected['ComplianceResourceId'], response['ComplianceResourceId']) 135 | testClass.assertEquals(resp_expected['ComplianceType'], response['ComplianceType']) 136 | testClass.assertTrue(response['OrderingTimestamp']) 137 | if 'Annotation' in resp_expected or 'Annotation' in response: 138 | testClass.assertEquals(resp_expected['Annotation'], response['Annotation']) 139 | elif isinstance(response, list): 140 | testClass.assertEquals(evaluations_count, len(response)) 141 | for i, response_expected in enumerate(resp_expected): 142 | testClass.assertEquals(response_expected['ComplianceResourceType'], response[i]['ComplianceResourceType']) 143 | testClass.assertEquals(response_expected['ComplianceResourceId'], response[i]['ComplianceResourceId']) 144 | testClass.assertEquals(response_expected['ComplianceType'], response[i]['ComplianceType']) 145 | testClass.assertTrue(response[i]['OrderingTimestamp']) 146 | if 'Annotation' in response_expected or 'Annotation' in response[i]: 147 | testClass.assertEquals(response_expected['Annotation'], response[i]['Annotation']) 148 | 149 | def assert_customer_error_response(testClass, response, customerErrorCode=None, customerErrorMessage=None): 150 | if customerErrorCode: 151 | testClass.assertEqual(customerErrorCode, response['customerErrorCode']) 152 | if customerErrorMessage: 153 | testClass.assertEqual(customerErrorMessage, response['customerErrorMessage']) 154 | testClass.assertTrue(response['customerErrorCode']) 155 | testClass.assertTrue(response['customerErrorMessage']) 156 | if "internalErrorMessage" in response: 157 | testClass.assertTrue(response['internalErrorMessage']) 158 | if "internalErrorDetails" in response: 159 | testClass.assertTrue(response['internalErrorDetails']) 160 | 161 | def sts_mock(): 162 | assume_role_response = { 163 | "Credentials": { 164 | "AccessKeyId": "string", 165 | "SecretAccessKey": "string", 166 | "SessionToken": "string"}} 167 | sts_client_mock.reset_mock(return_value=True) 168 | sts_client_mock.assume_role = MagicMock(return_value=assume_role_response) 169 | 170 | ################## 171 | # Common Testing # 172 | ################## 173 | 174 | class TestStsErrors(unittest.TestCase): 175 | 176 | def test_sts_unknown_error(self): 177 | rule.ASSUME_ROLE_MODE = True 178 | sts_client_mock.assume_role = MagicMock(side_effect=botocore.exceptions.ClientError( 179 | {'Error': {'Code': 'unknown-code', 'Message': 'unknown-message'}}, 'operation')) 180 | response = rule.lambda_handler(build_lambda_configurationchange_event('{}'), {}) 181 | assert_customer_error_response( 182 | self, response, 'InternalError', 'InternalError') 183 | 184 | def test_sts_access_denied(self): 185 | rule.ASSUME_ROLE_MODE = True 186 | sts_client_mock.assume_role = MagicMock(side_effect=botocore.exceptions.ClientError( 187 | {'Error': {'Code': 'AccessDenied', 'Message': 'access-denied'}}, 'operation')) 188 | response = rule.lambda_handler(build_lambda_configurationchange_event('{}'), {}) 189 | assert_customer_error_response( 190 | self, response, 'AccessDenied', 'AWS Config does not have permission to assume the IAM role.') 191 | -------------------------------------------------------------------------------- /rules/IAM_USER_NO_POLICY_FULL_STAR/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "IAM_USER_NO_POLICY_FULL_STAR", 5 | "SourceRuntime": "python3.6", 6 | "CodeKey": "IAM_USER_NO_POLICY_FULL_STAR.zip", 7 | "InputParameters": "{}", 8 | "OptionalParameters": "{}", 9 | "SourceEvents": "AWS::IAM::User", 10 | "RuleSets": [ 11 | "baseline", 12 | "rulecriticity:high" 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /rules/IAM_USER_UNUSED_CREDENTIALS_CHECK/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "IAM_USER_UNUSED_CREDENTIALS_CHECK", 5 | "SourceRuntime": null, 6 | "CodeKey": null, 7 | "InputParameters": "{\"maxCredentialUsageAge\": \"90\"}", 8 | "OptionalParameters": "{}", 9 | "SourcePeriodic": "TwentyFour_Hours", 10 | "SourceIdentifier": "IAM_USER_UNUSED_CREDENTIALS_CHECK", 11 | "RuleSets": [ 12 | "baseline", 13 | "rulecriticity:medium" 14 | ] 15 | }, 16 | "Tags": "[]" 17 | } -------------------------------------------------------------------------------- /rules/INTERNET_GATEWAY_AUTHORIZED_ONLY/INTERNET_GATEWAY_AUTHORIZED_ONLY.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file made available under CC0 1.0 Universal (https://creativecommons.org/publicdomain/zero/1.0/legalcode) 3 | # 4 | # Created with the Rule Development Kit: https://github.com/awslabs/aws-config-rdk 5 | # Can be used stand-alone or with the Rule Compliance Engine: https://github.com/awslabs/aws-config-engine-for-compliance-as-code 6 | # 7 | ''' 8 | ##################################### 9 | ## Gherkin ## 10 | ##################################### 11 | 12 | Rule Name: 13 | internet-gateway-authorized-only 14 | 15 | Description: 16 | Check whether attached IGWs are attached to an authorized list of VPCs. 17 | 18 | Trigger: 19 | Configuration Change on AWS::EC2::InternetGateway 20 | 21 | Reports on: 22 | AWS::EC2::InternetGateway 23 | 24 | Parameters: 25 | | ----------------------|-----------|-------------------------------------------------| 26 | | Parameter Name | Type | Description | 27 | | ----------------------|-----------|-------------------------------------------------| 28 | | AuthorizedVpcIds | Optional | List of the authorized VPC Ids to have an IGW | 29 | | | | attached, separated by comma (,). | 30 | | ----------------------|-----------|-------------------------------------------------| 31 | 32 | Feature: 33 | In order to: limit access to the internet to specific VPCs 34 | As: a Security Officer 35 | I want: to ensure that all attached IGWs are authorized. 36 | 37 | Scenarios: 38 | Scenario 1: 39 | Given: the AuthorizedVpcIds list items are not starting with "vpc-" 40 | Then: return an Error 41 | 42 | Scenario 2: 43 | Given: the IGW is not attached to a VPC 44 | Then: return COMPLIANT 45 | 46 | Scenario 3: 47 | Given: the IGW is attached to a VPC 48 | And: the AuthorizedVpcIds parameter is not configured 49 | Then: return NON_COMPLIANT 50 | 51 | Scenario 4: 52 | Given: the IGW is attached to a VPC 53 | And: the AuthorizedVpcIds parameter is configured and valid 54 | And: the VPC Id where the IGW is attached is not in the AuthorizedVpcIds list 55 | Then: return NON_COMPLIANT with an annotation "This IGW is not attached to an authorized VPC." 56 | 57 | Scenario 5: 58 | Given: the IGW is attached to a VPC 59 | And: the AuthorizedVpcIds parameter is configured and valid 60 | And: the VPC Id where the IGW is attached is in the AuthorizedVpcIds list 61 | Then: return COMPLIANT 62 | ''' 63 | 64 | import json 65 | import datetime 66 | import boto3 67 | import botocore 68 | 69 | ############## 70 | # Parameters # 71 | ############## 72 | 73 | # Define the default resource to report to Config Rules 74 | DEFAULT_RESOURCE_TYPE = "AWS::EC2::InternetGateway" 75 | 76 | # Set to True to get the lambda to assume the Role attached on the Config Service (useful for cross-account). 77 | ASSUME_ROLE_MODE = False 78 | 79 | ############# 80 | # Main Code # 81 | ############# 82 | 83 | def evaluate_compliance(event, configuration_item, valid_rule_parameters): 84 | 85 | vpc_attachment = configuration_item['configuration']['attachments'] 86 | if not vpc_attachment: 87 | return 'COMPLIANT' 88 | 89 | vpc_id = vpc_attachment[0]['vpcId'] 90 | if vpc_id in valid_rule_parameters: 91 | return 'COMPLIANT' 92 | return build_evaluation_from_config_item(configuration_item, 'NON_COMPLIANT', annotation="This IGW is not attached to an authorized VPC.") 93 | 94 | def evaluate_parameters(rule_parameters): 95 | """Evaluate the rule parameters dictionary validity. Raise a ValueError for invalid parameters. 96 | 97 | Return: 98 | anything suitable for the evaluate_compliance() 99 | 100 | Keyword arguments: 101 | rule_parameters -- the Key/Value dictionary of the Config Rules parameters 102 | """ 103 | if rule_parameters: 104 | authorized_vpc_ids = rule_parameters['AuthorizedVpcIds'].split(',') 105 | for i,authorized_vpc_id in enumerate(authorized_vpc_ids): 106 | authorized_vpc_ids[i] = authorized_vpc_id.strip() 107 | for authorized_vpc_id in authorized_vpc_ids: 108 | if not authorized_vpc_id.startswith('vpc-'): 109 | raise ValueError('The parameter ({}) does not start with vpc-'.format(authorized_vpc_id)) 110 | rule_parameters = authorized_vpc_ids 111 | return rule_parameters 112 | 113 | #################### 114 | # Helper Functions # 115 | #################### 116 | 117 | # Build an error to be displayed in the logs when the parameter is invalid. 118 | def build_parameters_value_error_response(ex): 119 | """Return an error dictionary when the evaluate_parameters() raises a ValueError. 120 | 121 | Keyword arguments: 122 | ex -- Exception text 123 | """ 124 | return build_error_response(internalErrorMessage="Parameter value is invalid", 125 | internalErrorDetails="An ValueError was raised during the validation of the Parameter value", 126 | customerErrorCode="InvalidParameterValueException", 127 | customerErrorMessage=str(ex)) 128 | 129 | # This gets the client after assuming the Config service role 130 | # either in the same AWS account or cross-account. 131 | def get_client(service, event): 132 | """Return the service boto client. It should be used instead of directly calling the client. 133 | 134 | Keyword arguments: 135 | service -- the service name used for calling the boto.client() 136 | event -- the event variable given in the lambda handler 137 | """ 138 | if not ASSUME_ROLE_MODE: 139 | return boto3.client(service) 140 | credentials = get_assume_role_credentials(event["executionRoleArn"]) 141 | return boto3.client(service, aws_access_key_id=credentials['AccessKeyId'], 142 | aws_secret_access_key=credentials['SecretAccessKey'], 143 | aws_session_token=credentials['SessionToken'] 144 | ) 145 | 146 | # This generate an evaluation for config 147 | def build_evaluation(resource_id, compliance_type, event, resource_type=DEFAULT_RESOURCE_TYPE, annotation=None): 148 | """Form an evaluation as a dictionary. Usually suited to report on scheduled rules. 149 | 150 | Keyword arguments: 151 | resource_id -- the unique id of the resource to report 152 | compliance_type -- either COMPLIANT, NON_COMPLIANT or NOT_APPLICABLE 153 | event -- the event variable given in the lambda handler 154 | resource_type -- the CloudFormation resource type (or AWS::::Account) to report on the rule (default DEFAULT_RESOURCE_TYPE) 155 | annotation -- an annotation to be added to the evaluation (default None) 156 | """ 157 | eval_cc = {} 158 | if annotation: 159 | eval_cc['Annotation'] = annotation 160 | eval_cc['ComplianceResourceType'] = resource_type 161 | eval_cc['ComplianceResourceId'] = resource_id 162 | eval_cc['ComplianceType'] = compliance_type 163 | eval_cc['OrderingTimestamp'] = str(json.loads(event['invokingEvent'])['notificationCreationTime']) 164 | return eval_cc 165 | 166 | def build_evaluation_from_config_item(configuration_item, compliance_type, annotation=None): 167 | """Form an evaluation as a dictionary. Usually suited to report on configuration change rules. 168 | 169 | Keyword arguments: 170 | configuration_item -- the configurationItem dictionary in the invokingEvent 171 | compliance_type -- either COMPLIANT, NON_COMPLIANT or NOT_APPLICABLE 172 | annotation -- an annotation to be added to the evaluation (default None) 173 | """ 174 | eval_ci = {} 175 | if annotation: 176 | eval_ci['Annotation'] = annotation 177 | eval_ci['ComplianceResourceType'] = configuration_item['resourceType'] 178 | eval_ci['ComplianceResourceId'] = configuration_item['resourceId'] 179 | eval_ci['ComplianceType'] = compliance_type 180 | eval_ci['OrderingTimestamp'] = configuration_item['configurationItemCaptureTime'] 181 | return eval_ci 182 | 183 | #################### 184 | # Boilerplate Code # 185 | #################### 186 | 187 | # Helper function used to validate input 188 | def check_defined(reference, reference_name): 189 | if not reference: 190 | raise Exception('Error: ', reference_name, 'is not defined') 191 | return reference 192 | 193 | # Check whether the message is OversizedConfigurationItemChangeNotification or not 194 | def is_oversized_changed_notification(message_type): 195 | check_defined(message_type, 'messageType') 196 | return message_type == 'OversizedConfigurationItemChangeNotification' 197 | 198 | # Check whether the message is a ScheduledNotification or not. 199 | def is_scheduled_notification(message_type): 200 | check_defined(message_type, 'messageType') 201 | return message_type == 'ScheduledNotification' 202 | 203 | # Get configurationItem using getResourceConfigHistory API 204 | # in case of OversizedConfigurationItemChangeNotification 205 | def get_configuration(resource_type, resource_id, configuration_capture_time): 206 | result = AWS_CONFIG_CLIENT.get_resource_config_history( 207 | resourceType=resource_type, 208 | resourceId=resource_id, 209 | laterTime=configuration_capture_time, 210 | limit=1) 211 | configurationItem = result['configurationItems'][0] 212 | return convert_api_configuration(configurationItem) 213 | 214 | # Convert from the API model to the original invocation model 215 | def convert_api_configuration(configurationItem): 216 | for k, v in configurationItem.items(): 217 | if isinstance(v, datetime.datetime): 218 | configurationItem[k] = str(v) 219 | configurationItem['awsAccountId'] = configurationItem['accountId'] 220 | configurationItem['ARN'] = configurationItem['arn'] 221 | configurationItem['configurationStateMd5Hash'] = configurationItem['configurationItemMD5Hash'] 222 | configurationItem['configurationItemVersion'] = configurationItem['version'] 223 | configurationItem['configuration'] = json.loads(configurationItem['configuration']) 224 | if 'relationships' in configurationItem: 225 | for i in range(len(configurationItem['relationships'])): 226 | configurationItem['relationships'][i]['name'] = configurationItem['relationships'][i]['relationshipName'] 227 | return configurationItem 228 | 229 | # Based on the type of message get the configuration item 230 | # either from configurationItem in the invoking event 231 | # or using the getResourceConfigHistiry API in getConfiguration function. 232 | def get_configuration_item(invokingEvent): 233 | check_defined(invokingEvent, 'invokingEvent') 234 | if is_oversized_changed_notification(invokingEvent['messageType']): 235 | configurationItemSummary = check_defined(invokingEvent['configurationItemSummary'], 'configurationItemSummary') 236 | return get_configuration(configurationItemSummary['resourceType'], configurationItemSummary['resourceId'], configurationItemSummary['configurationItemCaptureTime']) 237 | elif is_scheduled_notification(invokingEvent['messageType']): 238 | return None 239 | return check_defined(invokingEvent['configurationItem'], 'configurationItem') 240 | 241 | # Check whether the resource has been deleted. If it has, then the evaluation is unnecessary. 242 | def is_applicable(configurationItem, event): 243 | try: 244 | check_defined(configurationItem, 'configurationItem') 245 | check_defined(event, 'event') 246 | except: 247 | return True 248 | status = configurationItem['configurationItemStatus'] 249 | eventLeftScope = event['eventLeftScope'] 250 | if status == 'ResourceDeleted': 251 | print("Resource Deleted, setting Compliance Status to NOT_APPLICABLE.") 252 | return (status == 'OK' or status == 'ResourceDiscovered') and not eventLeftScope 253 | 254 | def get_assume_role_credentials(role_arn): 255 | sts_client = boto3.client('sts') 256 | try: 257 | assume_role_response = sts_client.assume_role(RoleArn=role_arn, RoleSessionName="configLambdaExecution") 258 | return assume_role_response['Credentials'] 259 | except botocore.exceptions.ClientError as ex: 260 | # Scrub error message for any internal account info leaks 261 | print(str(ex)) 262 | if 'AccessDenied' in ex.response['Error']['Code']: 263 | ex.response['Error']['Message'] = "AWS Config does not have permission to assume the IAM role." 264 | else: 265 | ex.response['Error']['Message'] = "InternalError" 266 | ex.response['Error']['Code'] = "InternalError" 267 | raise ex 268 | 269 | # This removes older evaluation (usually useful for periodic rule not reporting on AWS::::Account). 270 | def clean_up_old_evaluations(latest_evaluations, event): 271 | 272 | cleaned_evaluations = [] 273 | 274 | old_eval = AWS_CONFIG_CLIENT.get_compliance_details_by_config_rule( 275 | ConfigRuleName=event['configRuleName'], 276 | ComplianceTypes=['COMPLIANT', 'NON_COMPLIANT'], 277 | Limit=100) 278 | 279 | old_eval_list = [] 280 | 281 | while True: 282 | for old_result in old_eval['EvaluationResults']: 283 | old_eval_list.append(old_result) 284 | if 'NextToken' in old_eval: 285 | next_token = old_eval['NextToken'] 286 | old_eval = AWS_CONFIG_CLIENT.get_compliance_details_by_config_rule( 287 | ConfigRuleName=event['configRuleName'], 288 | ComplianceTypes=['COMPLIANT', 'NON_COMPLIANT'], 289 | Limit=100, 290 | NextToken=next_token) 291 | else: 292 | break 293 | 294 | for old_eval in old_eval_list: 295 | old_resource_id = old_eval['EvaluationResultIdentifier']['EvaluationResultQualifier']['ResourceId'] 296 | newer_founded = False 297 | for latest_eval in latest_evaluations: 298 | if old_resource_id == latest_eval['ComplianceResourceId']: 299 | newer_founded = True 300 | if not newer_founded: 301 | cleaned_evaluations.append(build_evaluation(old_resource_id, "NOT_APPLICABLE", event)) 302 | 303 | return cleaned_evaluations + latest_evaluations 304 | 305 | # This decorates the lambda_handler in rule_code with the actual PutEvaluation call 306 | def lambda_handler(event, context): 307 | 308 | global AWS_CONFIG_CLIENT 309 | 310 | #print(event) 311 | check_defined(event, 'event') 312 | invoking_event = json.loads(event['invokingEvent']) 313 | rule_parameters = {} 314 | if 'ruleParameters' in event: 315 | rule_parameters = json.loads(event['ruleParameters']) 316 | 317 | try: 318 | valid_rule_parameters = evaluate_parameters(rule_parameters) 319 | except ValueError as ex: 320 | return build_parameters_value_error_response(ex) 321 | 322 | try: 323 | AWS_CONFIG_CLIENT = get_client('config', event) 324 | if invoking_event['messageType'] in ['ConfigurationItemChangeNotification', 'ScheduledNotification', 'OversizedConfigurationItemChangeNotification']: 325 | configuration_item = get_configuration_item(invoking_event) 326 | if is_applicable(configuration_item, event): 327 | compliance_result = evaluate_compliance(event, configuration_item, valid_rule_parameters) 328 | else: 329 | compliance_result = "NOT_APPLICABLE" 330 | else: 331 | return build_internal_error_response('Unexpected message type', str(invoking_event)) 332 | except botocore.exceptions.ClientError as ex: 333 | if is_internal_error(ex): 334 | return build_internal_error_response("Unexpected error while completing API request", str(ex)) 335 | return build_error_response("Customer error while making API request", str(ex), ex.response['Error']['Code'], ex.response['Error']['Message']) 336 | except ValueError as ex: 337 | return build_internal_error_response(str(ex), str(ex)) 338 | 339 | evaluations = [] 340 | latest_evaluations = [] 341 | 342 | if not compliance_result: 343 | latest_evaluations.append(build_evaluation(event['accountId'], "NOT_APPLICABLE", event, resource_type='AWS::::Account')) 344 | evaluations = clean_up_old_evaluations(latest_evaluations, event) 345 | elif isinstance(compliance_result, str): 346 | if configuration_item: 347 | evaluations.append(build_evaluation_from_config_item(configuration_item, compliance_result)) 348 | else: 349 | evaluations.append(build_evaluation(event['accountId'], compliance_result, event, resource_type=DEFAULT_RESOURCE_TYPE)) 350 | elif isinstance(compliance_result, list): 351 | for evaluation in compliance_result: 352 | missing_fields = False 353 | for field in ('ComplianceResourceType', 'ComplianceResourceId', 'ComplianceType', 'OrderingTimestamp'): 354 | if field not in evaluation: 355 | print("Missing " + field + " from custom evaluation.") 356 | missing_fields = True 357 | 358 | if not missing_fields: 359 | latest_evaluations.append(evaluation) 360 | evaluations = clean_up_old_evaluations(latest_evaluations, event) 361 | elif isinstance(compliance_result, dict): 362 | missing_fields = False 363 | for field in ('ComplianceResourceType', 'ComplianceResourceId', 'ComplianceType', 'OrderingTimestamp'): 364 | if field not in compliance_result: 365 | print("Missing " + field + " from custom evaluation.") 366 | missing_fields = True 367 | if not missing_fields: 368 | evaluations.append(compliance_result) 369 | else: 370 | evaluations.append(build_evaluation_from_config_item(configuration_item, 'NOT_APPLICABLE')) 371 | 372 | # Put together the request that reports the evaluation status 373 | resultToken = event['resultToken'] 374 | testMode = False 375 | if resultToken == 'TESTMODE': 376 | # Used solely for RDK test to skip actual put_evaluation API call 377 | testMode = True 378 | # Invoke the Config API to report the result of the evaluation 379 | AWS_CONFIG_CLIENT.put_evaluations(Evaluations=evaluations, ResultToken=resultToken, TestMode=testMode) 380 | # Used solely for RDK test to be able to test Lambda function 381 | return evaluations 382 | 383 | def is_internal_error(exception): 384 | return ((not isinstance(exception, botocore.exceptions.ClientError)) or exception.response['Error']['Code'].startswith('5') 385 | or 'InternalError' in exception.response['Error']['Code'] or 'ServiceError' in exception.response['Error']['Code']) 386 | 387 | def build_internal_error_response(internalErrorMessage, internalErrorDetails=None): 388 | return build_error_response(internalErrorMessage, internalErrorDetails, 'InternalError', 'InternalError') 389 | 390 | def build_error_response(internalErrorMessage, internalErrorDetails=None, customerErrorCode=None, customerErrorMessage=None): 391 | error_response = { 392 | 'internalErrorMessage': internalErrorMessage, 393 | 'internalErrorDetails': internalErrorDetails, 394 | 'customerErrorMessage': customerErrorMessage, 395 | 'customerErrorCode': customerErrorCode 396 | } 397 | print(error_response) 398 | return error_response 399 | -------------------------------------------------------------------------------- /rules/INTERNET_GATEWAY_AUTHORIZED_ONLY/INTERNET_GATEWAY_AUTHORIZED_ONLY_test.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file made available under CC0 1.0 Universal (https://creativecommons.org/publicdomain/zero/1.0/legalcode) 3 | # 4 | # Created with the Rule Development Kit: https://github.com/awslabs/aws-config-rdk 5 | # Can be used stand-alone or with the Rule Compliance Engine: https://github.com/awslabs/aws-config-engine-for-compliance-as-code 6 | # 7 | import sys 8 | import unittest 9 | try: 10 | from unittest.mock import MagicMock, patch, ANY 11 | except ImportError: 12 | import mock 13 | from mock import MagicMock, patch, ANY 14 | import botocore 15 | import json 16 | from botocore.exceptions import ClientError 17 | 18 | ############## 19 | # Parameters # 20 | ############## 21 | 22 | # Define the default resource to report to Config Rules 23 | DEFAULT_RESOURCE_TYPE = "AWS::EC2::InternetGateway" 24 | 25 | ############# 26 | # Main Code # 27 | ############# 28 | 29 | config_client_mock = MagicMock() 30 | sts_client_mock = MagicMock() 31 | 32 | class Boto3Mock(): 33 | def client(self, client_name, *args, **kwargs): 34 | if client_name == 'config': 35 | return config_client_mock 36 | elif client_name == 'sts': 37 | return sts_client_mock 38 | else: 39 | raise Exception("Attempting to create an unknown client") 40 | 41 | sys.modules['boto3'] = Boto3Mock() 42 | 43 | rule = __import__('INTERNET_GATEWAY_AUTHORIZED_ONLY') 44 | 45 | class SampleTest(unittest.TestCase): 46 | def setUp(self): 47 | pass 48 | 49 | def test_Scenario_1_not_starting_with_vpc(self): 50 | rule_parameters = "{\"AuthorizedVpcIds\":\"somename, vpc-shruti\"}" 51 | vpc_id_igw = 'vpc-abcde' 52 | response = rule.lambda_handler(build_lambda_configurationchange_event(build_invoking_event(vpc_id_igw), rule_parameters), {}) 53 | assert_customer_error_response(self, response, customerErrorCode='InvalidParameterValueException', customerErrorMessage='The parameter (somename) does not start with vpc-') 54 | 55 | def test_Scenario_2_igw_not_attached_vpc(self): 56 | rule_parameters = "{\"AuthorizedVpcIds\":\"vpc-paranshu, vpc-shruti\"}" 57 | vpc_id_igw = "" 58 | response = rule.lambda_handler(build_lambda_configurationchange_event(build_invoking_event(vpc_id_igw), rule_parameters), {}) 59 | resp_expected = [] 60 | resp_expected.append(build_expected_response('COMPLIANT', 'some-resource-id')) 61 | assert_successful_evaluation(self, response, resp_expected) 62 | 63 | def test_Scenario_3_AuthorizedVpcIds_not_configured(self): 64 | rule_parameters = "" 65 | vpc_id_igw = 'vpc-abcde' 66 | response = rule.lambda_handler(build_lambda_configurationchange_event(build_invoking_event(vpc_id_igw), rule_parameters), {}) 67 | resp_expected = [] 68 | resp_expected.append(build_expected_response('NON_COMPLIANT', 'some-resource-id', annotation='This IGW is not attached to an authorized VPC.')) 69 | assert_successful_evaluation(self, response, resp_expected) 70 | 71 | 72 | def test_scenario_4_igw_no_authorized_vpc(self): 73 | rule_parameters = "{\"AuthorizedVpcIds\":\"vpc-paranshu, vpc-shruti\"}" 74 | vpc_id_igw = 'vpc-abcde' 75 | response = rule.lambda_handler(build_lambda_configurationchange_event(build_invoking_event(vpc_id_igw), rule_parameters), {}) 76 | resp_expected = [] 77 | resp_expected.append(build_expected_response('NON_COMPLIANT', 'some-resource-id', annotation='This IGW is not attached to an authorized VPC.')) 78 | assert_successful_evaluation(self, response, resp_expected) 79 | 80 | def test_Scenario_5_compliant(self): 81 | rule_parameters = "{\"AuthorizedVpcIds\":\"vpc-paranshu, vpc-shruti\"}" 82 | vpc_id_igw = 'vpc-shruti' 83 | response = rule.lambda_handler(build_lambda_configurationchange_event(build_invoking_event(vpc_id_igw), rule_parameters), {}) 84 | resp_expected = [] 85 | resp_expected.append(build_expected_response('COMPLIANT', 'some-resource-id')) 86 | assert_successful_evaluation(self, response, resp_expected) 87 | 88 | def build_invoking_event(invoking_event_igw): 89 | attachments = [] 90 | if(len(invoking_event_igw)>0): 91 | attachments.append({ 92 | "vpcId": invoking_event_igw, 93 | "state": "available" 94 | }) 95 | invoking_event_iam_role_sample = { 96 | "configurationItem": { 97 | "relatedEvents": [], 98 | "relationships": [], 99 | "configuration": { 100 | "internetGatewayId": "igw-a5f227c1", 101 | "attachments": attachments, 102 | "tags": [] 103 | }, 104 | "tags": {}, 105 | "configurationItemCaptureTime": "2018-07-02T03:37:52.418Z", 106 | "awsAccountId": "633141505637", 107 | "configurationItemStatus": "ResourceDiscovered", 108 | "resourceType": "AWS::EC2::InternetGateway", 109 | "resourceId": "some-resource-id", 110 | "resourceName": "some-resource-name", 111 | "ARN": "some-arn" 112 | }, 113 | "notificationCreationTime": "2018-07-02T23:05:34.445Z", 114 | "messageType": "ConfigurationItemChangeNotification" 115 | } 116 | return json.dumps(invoking_event_iam_role_sample) 117 | 118 | #################### 119 | # Helper Functions # 120 | #################### 121 | 122 | def build_lambda_configurationchange_event(invoking_event, rule_parameters=None): 123 | event_to_return = { 124 | 'configRuleName':'myrule', 125 | 'executionRoleArn':'roleArn', 126 | 'eventLeftScope': False, 127 | 'invokingEvent': invoking_event, 128 | 'accountId': '123456789012', 129 | 'configRuleArn': 'arn:aws:config:us-east-1:123456789012:config-rule/config-rule-8fngan', 130 | 'resultToken':'token' 131 | } 132 | if rule_parameters: 133 | event_to_return['ruleParameters'] = rule_parameters 134 | return event_to_return 135 | 136 | def build_lambda_scheduled_event(rule_parameters=None): 137 | invoking_event = '{"messageType":"ScheduledNotification","notificationCreationTime":"2017-12-23T22:11:18.158Z"}' 138 | event_to_return = { 139 | 'configRuleName':'myrule', 140 | 'executionRoleArn':'roleArn', 141 | 'eventLeftScope': False, 142 | 'invokingEvent': invoking_event, 143 | 'accountId': '123456789012', 144 | 'configRuleArn': 'arn:aws:config:us-east-1:123456789012:config-rule/config-rule-8fngan', 145 | 'resultToken':'token' 146 | } 147 | if rule_parameters: 148 | event_to_return['ruleParameters'] = rule_parameters 149 | return event_to_return 150 | 151 | def build_expected_response(compliance_type, compliance_resource_id, compliance_resource_type=DEFAULT_RESOURCE_TYPE, annotation=None): 152 | if not annotation: 153 | return { 154 | 'ComplianceType': compliance_type, 155 | 'ComplianceResourceId': compliance_resource_id, 156 | 'ComplianceResourceType': compliance_resource_type 157 | } 158 | return { 159 | 'ComplianceType': compliance_type, 160 | 'ComplianceResourceId': compliance_resource_id, 161 | 'ComplianceResourceType': compliance_resource_type, 162 | 'Annotation': annotation 163 | } 164 | 165 | def assert_successful_evaluation(testClass, response, resp_expected, evaluations_count=1): 166 | if isinstance(response, dict): 167 | testClass.assertEquals(resp_expected['ComplianceResourceType'], response['ComplianceResourceType']) 168 | testClass.assertEquals(resp_expected['ComplianceResourceId'], response['ComplianceResourceId']) 169 | testClass.assertEquals(resp_expected['ComplianceType'], response['ComplianceType']) 170 | testClass.assertTrue(response['OrderingTimestamp']) 171 | if 'Annotation' in resp_expected or 'Annotation' in response: 172 | testClass.assertEquals(resp_expected['Annotation'], response['Annotation']) 173 | elif isinstance(response, list): 174 | testClass.assertEquals(evaluations_count, len(response)) 175 | for i, response_expected in enumerate(resp_expected): 176 | testClass.assertEquals(response_expected['ComplianceResourceType'], response[i]['ComplianceResourceType']) 177 | testClass.assertEquals(response_expected['ComplianceResourceId'], response[i]['ComplianceResourceId']) 178 | testClass.assertEquals(response_expected['ComplianceType'], response[i]['ComplianceType']) 179 | testClass.assertTrue(response[i]['OrderingTimestamp']) 180 | if 'Annotation' in response_expected or 'Annotation' in response[i]: 181 | testClass.assertEquals(response_expected['Annotation'], response[i]['Annotation']) 182 | 183 | def assert_customer_error_response(testClass, response, customerErrorCode=None, customerErrorMessage=None): 184 | if customerErrorCode: 185 | testClass.assertEqual(customerErrorCode, response['customerErrorCode']) 186 | if customerErrorMessage: 187 | testClass.assertEqual(customerErrorMessage, response['customerErrorMessage']) 188 | testClass.assertTrue(response['customerErrorCode']) 189 | testClass.assertTrue(response['customerErrorMessage']) 190 | if "internalErrorMessage" in response: 191 | testClass.assertTrue(response['internalErrorMessage']) 192 | if "internalErrorDetails" in response: 193 | testClass.assertTrue(response['internalErrorDetails']) 194 | 195 | def sts_mock(): 196 | assume_role_response = { 197 | "Credentials": { 198 | "AccessKeyId": "string", 199 | "SecretAccessKey": "string", 200 | "SessionToken": "string"}} 201 | sts_client_mock.reset_mock(return_value=True) 202 | sts_client_mock.assume_role = MagicMock(return_value=assume_role_response) 203 | -------------------------------------------------------------------------------- /rules/INTERNET_GATEWAY_AUTHORIZED_ONLY/__pycache__/INTERNET_GATEWAY_AUTHORIZED_ONLY.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-config-engine-for-compliance-as-code/0ea788038f74d9c8fc6fd28741af8d3d8bb8fd59/rules/INTERNET_GATEWAY_AUTHORIZED_ONLY/__pycache__/INTERNET_GATEWAY_AUTHORIZED_ONLY.cpython-36.pyc -------------------------------------------------------------------------------- /rules/INTERNET_GATEWAY_AUTHORIZED_ONLY/__pycache__/INTERNET_GATEWAY_AUTHORIZED_ONLY_test.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-config-engine-for-compliance-as-code/0ea788038f74d9c8fc6fd28741af8d3d8bb8fd59/rules/INTERNET_GATEWAY_AUTHORIZED_ONLY/__pycache__/INTERNET_GATEWAY_AUTHORIZED_ONLY_test.cpython-36.pyc -------------------------------------------------------------------------------- /rules/INTERNET_GATEWAY_AUTHORIZED_ONLY/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "INTERNET_GATEWAY_AUTHORIZED_ONLY", 5 | "SourceRuntime": "python3.6", 6 | "CodeKey": "INTERNET_GATEWAY_AUTHORIZED_ONLY.zip", 7 | "InputParameters": "{}", 8 | "OptionalParameters": "{\"AuthorizedVpcIds\":\"\"}", 9 | "SourceEvents": "AWS::EC2::InternetGateway", 10 | "RuleSets": [ 11 | "accountclassification:secret", 12 | "accountclassification:confidential", 13 | "rulecriticity:high", 14 | "pci" 15 | ] 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /rules/RDS_INSTANCE_PUBLIC_ACCESS_CHECK/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "RDS_INSTANCE_PUBLIC_ACCESS_CHECK", 5 | "SourceRuntime": null, 6 | "CodeKey": null, 7 | "InputParameters": "{}", 8 | "OptionalParameters": "{}", 9 | "SourceEvents": "AWS::RDS::DBInstance", 10 | "SourceIdentifier": "RDS_INSTANCE_PUBLIC_ACCESS_CHECK", 11 | "RuleSets": [ 12 | "baseline", 13 | "rulecriticity:critical" 14 | ] 15 | }, 16 | "Tags": "[]" 17 | } -------------------------------------------------------------------------------- /rules/ROOT_ACCOUNT_MFA_ENABLED/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "ROOT_ACCOUNT_MFA_ENABLED", 5 | "SourceRuntime": null, 6 | "CodeKey": null, 7 | "InputParameters": "{}", 8 | "OptionalParameters": "{}", 9 | "SourcePeriodic": "TwentyFour_Hours", 10 | "SourceIdentifier": "ROOT_ACCOUNT_MFA_ENABLED", 11 | "RuleSets": [ 12 | "baseline", 13 | "rulecriticity:critical" 14 | ] 15 | }, 16 | "Tags": "[]" 17 | } -------------------------------------------------------------------------------- /rules/ROOT_NO_ACCESS_KEY/ROOT_NO_ACCESS_KEY.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file made available under CC0 1.0 Universal (https://creativecommons.org/publicdomain/zero/1.0/legalcode) 3 | # 4 | # Created with the Rule Development Kit: https://github.com/awslabs/aws-config-rdk 5 | # Can be used stand-alone or with the Rule Compliance Engine: https://github.com/awslabs/aws-config-engine-for-compliance-as-code 6 | # 7 | ''' 8 | #################################### 9 | # Gherkin ## 10 | #################################### 11 | 12 | Rule Name: 13 | root-no-access-key 14 | 15 | Description: 16 | Ensure no root user access key exists 17 | 18 | Trigger: 19 | Periodic 20 | 21 | Reports on: 22 | AWS::::Account 23 | 24 | Rule Parameters: 25 | None 26 | 27 | Feature: 28 | In order to: restrict privileged user 29 | As: a Security Officer 30 | I want: to ensure that no access key for the root user exists 31 | 32 | Scenarios: 33 | Scenario 1: 34 | Given: Access key for root user present 35 | And: Access key is active 36 | Then: return NON_COMPLIANT 37 | 38 | Scenario 2: 39 | Given: Access key for root user present 40 | And: Access key is inactive 41 | Then: return NON_COMPLIANT 42 | 43 | Scenario 3: 44 | Given: Access Key for root user is not present 45 | Then: COMPLIANT 46 | 47 | ''' 48 | import json 49 | import datetime 50 | import boto3 51 | import botocore 52 | 53 | ############## 54 | # Parameters # 55 | ############## 56 | 57 | # Define the default resource to report to Config Rules 58 | DEFAULT_RESOURCE_TYPE = 'AWS::::Account' 59 | 60 | # Set to True to get the lambda to assume the Role attached on the Config Service (useful for cross-account). 61 | ASSUME_ROLE_MODE = False 62 | 63 | ############# 64 | # Main Code # 65 | ############# 66 | 67 | def evaluate_compliance(event, configuration_item, valid_rule_parameters): 68 | """Form the evaluation(s) to be return to Config Rules 69 | 70 | Return either: 71 | None -- when no result needs to be displayed 72 | a string -- either COMPLIANT, NON_COMPLIANT or NOT_APPLICABLE 73 | a dictionary -- the evaluation dictionary, usually built by build_evaluation_from_config_item() 74 | a list of dictionary -- a list of evaluation dictionary , usually built by build_evaluation() 75 | 76 | Keyword arguments: 77 | event -- the event variable given in the lambda handler 78 | configuration_item -- the configurationItem dictionary in the invokingEvent 79 | valid_rule_parameters -- the output of the evaluate_parameters() representing validated parameters of the Config Rule 80 | 81 | Advanced Notes: 82 | 1 -- if a resource is deleted and generate a configuration change with ResourceDeleted status, the Boilerplate code will put a NOT_APPLICABLE on this resource automatically. 83 | 2 -- if a None or a list of dictionary is returned, the old evaluation(s) which are not returned in the new evaluation list are returned as NOT_APPLICABLE by the Boilerplate code 84 | 3 -- if None or an empty string, list or dict is returned, the Boilerplate code will put a "shadow" evaluation to feedback that the evaluation took place properly 85 | """ 86 | iam_client = get_client('iam', event) 87 | acc_summary = iam_client.get_account_summary() 88 | 89 | if acc_summary['SummaryMap']['AccountAccessKeysPresent'] == 0: 90 | return build_evaluation(event['accountId'], 'COMPLIANT', event) 91 | 92 | return build_evaluation(event['accountId'], 'NON_COMPLIANT', event, annotation='The root user has access key(s).') 93 | 94 | def evaluate_parameters(rule_parameters): 95 | """Evaluate the rule parameters dictionary validity. Raise a ValueError for invalid parameters. 96 | 97 | Return: 98 | anything suitable for the evaluate_compliance() 99 | 100 | Keyword arguments: 101 | rule_parameters -- the Key/Value dictionary of the Config Rules parameters 102 | """ 103 | valid_rule_parameters = rule_parameters 104 | return valid_rule_parameters 105 | 106 | #################### 107 | # Helper Functions # 108 | #################### 109 | 110 | # Build an error to be displayed in the logs when the parameter is invalid. 111 | def build_parameters_value_error_response(ex): 112 | """Return an error dictionary when the evaluate_parameters() raises a ValueError. 113 | 114 | Keyword arguments: 115 | ex -- Exception text 116 | """ 117 | return build_error_response(internalErrorMessage="Parameter value is invalid", 118 | internalErrorDetails="An ValueError was raised during the validation of the Parameter value", 119 | customerErrorCode="InvalidParameterValueException", 120 | customerErrorMessage=str(ex)) 121 | 122 | # This gets the client after assuming the Config service role 123 | # either in the same AWS account or cross-account. 124 | def get_client(service, event): 125 | """Return the service boto client. It should be used instead of directly calling the client. 126 | 127 | Keyword arguments: 128 | service -- the service name used for calling the boto.client() 129 | event -- the event variable given in the lambda handler 130 | """ 131 | if not ASSUME_ROLE_MODE: 132 | return boto3.client(service) 133 | credentials = get_assume_role_credentials(event["executionRoleArn"]) 134 | return boto3.client(service, aws_access_key_id=credentials['AccessKeyId'], 135 | aws_secret_access_key=credentials['SecretAccessKey'], 136 | aws_session_token=credentials['SessionToken'] 137 | ) 138 | 139 | # This generate an evaluation for config 140 | def build_evaluation(resource_id, compliance_type, event, resource_type=DEFAULT_RESOURCE_TYPE, annotation=None): 141 | """Form an evaluation as a dictionary. Usually suited to report on scheduled rules. 142 | 143 | Keyword arguments: 144 | resource_id -- the unique id of the resource to report 145 | compliance_type -- either COMPLIANT, NON_COMPLIANT or NOT_APPLICABLE 146 | event -- the event variable given in the lambda handler 147 | resource_type -- the CloudFormation resource type (or AWS::::Account) to report on the rule (default DEFAULT_RESOURCE_TYPE) 148 | annotation -- an annotation to be added to the evaluation (default None) 149 | """ 150 | eval_cc = {} 151 | if annotation: 152 | eval_cc['Annotation'] = annotation 153 | eval_cc['ComplianceResourceType'] = resource_type 154 | eval_cc['ComplianceResourceId'] = resource_id 155 | eval_cc['ComplianceType'] = compliance_type 156 | eval_cc['OrderingTimestamp'] = str(json.loads(event['invokingEvent'])['notificationCreationTime']) 157 | return eval_cc 158 | 159 | def build_evaluation_from_config_item(configuration_item, compliance_type, annotation=None): 160 | """Form an evaluation as a dictionary. Usually suited to report on configuration change rules. 161 | 162 | Keyword arguments: 163 | configuration_item -- the configurationItem dictionary in the invokingEvent 164 | compliance_type -- either COMPLIANT, NON_COMPLIANT or NOT_APPLICABLE 165 | annotation -- an annotation to be added to the evaluation (default None) 166 | """ 167 | eval_ci = {} 168 | if annotation: 169 | eval_ci['Annotation'] = annotation 170 | eval_ci['ComplianceResourceType'] = configuration_item['resourceType'] 171 | eval_ci['ComplianceResourceId'] = configuration_item['resourceId'] 172 | eval_ci['ComplianceType'] = compliance_type 173 | eval_ci['OrderingTimestamp'] = configuration_item['configurationItemCaptureTime'] 174 | return eval_ci 175 | 176 | #################### 177 | # Boilerplate Code # 178 | #################### 179 | 180 | # Helper function used to validate input 181 | def check_defined(reference, reference_name): 182 | if not reference: 183 | raise Exception('Error: ', reference_name, 'is not defined') 184 | return reference 185 | 186 | # Check whether the message is OversizedConfigurationItemChangeNotification or not 187 | def is_oversized_changed_notification(message_type): 188 | check_defined(message_type, 'messageType') 189 | return message_type == 'OversizedConfigurationItemChangeNotification' 190 | 191 | # Check whether the message is a ScheduledNotification or not. 192 | def is_scheduled_notification(message_type): 193 | check_defined(message_type, 'messageType') 194 | return message_type == 'ScheduledNotification' 195 | 196 | # Get configurationItem using getResourceConfigHistory API 197 | # in case of OversizedConfigurationItemChangeNotification 198 | def get_configuration(resource_type, resource_id, configuration_capture_time): 199 | result = AWS_CONFIG_CLIENT.get_resource_config_history( 200 | resourceType=resource_type, 201 | resourceId=resource_id, 202 | laterTime=configuration_capture_time, 203 | limit=1) 204 | configurationItem = result['configurationItems'][0] 205 | return convert_api_configuration(configurationItem) 206 | 207 | # Convert from the API model to the original invocation model 208 | def convert_api_configuration(configurationItem): 209 | for k, v in configurationItem.items(): 210 | if isinstance(v, datetime.datetime): 211 | configurationItem[k] = str(v) 212 | configurationItem['awsAccountId'] = configurationItem['accountId'] 213 | configurationItem['ARN'] = configurationItem['arn'] 214 | configurationItem['configurationStateMd5Hash'] = configurationItem['configurationItemMD5Hash'] 215 | configurationItem['configurationItemVersion'] = configurationItem['version'] 216 | configurationItem['configuration'] = json.loads(configurationItem['configuration']) 217 | if 'relationships' in configurationItem: 218 | for i in range(len(configurationItem['relationships'])): 219 | configurationItem['relationships'][i]['name'] = configurationItem['relationships'][i]['relationshipName'] 220 | return configurationItem 221 | 222 | # Based on the type of message get the configuration item 223 | # either from configurationItem in the invoking event 224 | # or using the getResourceConfigHistiry API in getConfiguration function. 225 | def get_configuration_item(invokingEvent): 226 | check_defined(invokingEvent, 'invokingEvent') 227 | if is_oversized_changed_notification(invokingEvent['messageType']): 228 | configurationItemSummary = check_defined(invokingEvent['configurationItemSummary'], 'configurationItemSummary') 229 | return get_configuration(configurationItemSummary['resourceType'], configurationItemSummary['resourceId'], configurationItemSummary['configurationItemCaptureTime']) 230 | elif is_scheduled_notification(invokingEvent['messageType']): 231 | return None 232 | return check_defined(invokingEvent['configurationItem'], 'configurationItem') 233 | 234 | # Check whether the resource has been deleted. If it has, then the evaluation is unnecessary. 235 | def is_applicable(configurationItem, event): 236 | try: 237 | check_defined(configurationItem, 'configurationItem') 238 | check_defined(event, 'event') 239 | except: 240 | return True 241 | status = configurationItem['configurationItemStatus'] 242 | eventLeftScope = event['eventLeftScope'] 243 | if status == 'ResourceDeleted': 244 | print("Resource Deleted, setting Compliance Status to NOT_APPLICABLE.") 245 | return (status == 'OK' or status == 'ResourceDiscovered') and not eventLeftScope 246 | 247 | def get_assume_role_credentials(role_arn): 248 | sts_client = boto3.client('sts') 249 | try: 250 | assume_role_response = sts_client.assume_role(RoleArn=role_arn, RoleSessionName="configLambdaExecution") 251 | return assume_role_response['Credentials'] 252 | except botocore.exceptions.ClientError as ex: 253 | # Scrub error message for any internal account info leaks 254 | print(str(ex)) 255 | if 'AccessDenied' in ex.response['Error']['Code']: 256 | ex.response['Error']['Message'] = "AWS Config does not have permission to assume the IAM role." 257 | else: 258 | ex.response['Error']['Message'] = "InternalError" 259 | ex.response['Error']['Code'] = "InternalError" 260 | raise ex 261 | 262 | # This removes older evaluation (usually useful for periodic rule not reporting on AWS::::Account). 263 | def clean_up_old_evaluations(latest_evaluations, event): 264 | 265 | cleaned_evaluations = [] 266 | 267 | old_eval = AWS_CONFIG_CLIENT.get_compliance_details_by_config_rule( 268 | ConfigRuleName=event['configRuleName'], 269 | ComplianceTypes=['COMPLIANT', 'NON_COMPLIANT'], 270 | Limit=100) 271 | 272 | old_eval_list = [] 273 | 274 | while True: 275 | for old_result in old_eval['EvaluationResults']: 276 | old_eval_list.append(old_result) 277 | if 'NextToken' in old_eval: 278 | next_token = old_eval['NextToken'] 279 | old_eval = AWS_CONFIG_CLIENT.get_compliance_details_by_config_rule( 280 | ConfigRuleName=event['configRuleName'], 281 | ComplianceTypes=['COMPLIANT', 'NON_COMPLIANT'], 282 | Limit=100, 283 | NextToken=next_token) 284 | else: 285 | break 286 | 287 | for old_eval in old_eval_list: 288 | old_resource_id = old_eval['EvaluationResultIdentifier']['EvaluationResultQualifier']['ResourceId'] 289 | newer_founded = False 290 | for latest_eval in latest_evaluations: 291 | if old_resource_id == latest_eval['ComplianceResourceId']: 292 | newer_founded = True 293 | if not newer_founded: 294 | cleaned_evaluations.append(build_evaluation(old_resource_id, "NOT_APPLICABLE", event)) 295 | 296 | return cleaned_evaluations + latest_evaluations 297 | 298 | # This decorates the lambda_handler in rule_code with the actual PutEvaluation call 299 | def lambda_handler(event, context): 300 | 301 | global AWS_CONFIG_CLIENT 302 | 303 | #print(event) 304 | check_defined(event, 'event') 305 | invoking_event = json.loads(event['invokingEvent']) 306 | rule_parameters = {} 307 | if 'ruleParameters' in event: 308 | rule_parameters = json.loads(event['ruleParameters']) 309 | 310 | try: 311 | valid_rule_parameters = evaluate_parameters(rule_parameters) 312 | except ValueError as ex: 313 | return build_parameters_value_error_response(ex) 314 | 315 | try: 316 | AWS_CONFIG_CLIENT = get_client('config', event) 317 | if invoking_event['messageType'] in ['ConfigurationItemChangeNotification', 'ScheduledNotification', 'OversizedConfigurationItemChangeNotification']: 318 | configuration_item = get_configuration_item(invoking_event) 319 | if is_applicable(configuration_item, event): 320 | compliance_result = evaluate_compliance(event, configuration_item, valid_rule_parameters) 321 | else: 322 | compliance_result = "NOT_APPLICABLE" 323 | else: 324 | return build_internal_error_response('Unexpected message type', str(invoking_event)) 325 | except botocore.exceptions.ClientError as ex: 326 | if is_internal_error(ex): 327 | return build_internal_error_response("Unexpected error while completing API request", str(ex)) 328 | return build_error_response("Customer error while making API request", str(ex), ex.response['Error']['Code'], ex.response['Error']['Message']) 329 | except ValueError as ex: 330 | return build_internal_error_response(str(ex), str(ex)) 331 | 332 | evaluations = [] 333 | latest_evaluations = [] 334 | 335 | if not compliance_result: 336 | latest_evaluations.append(build_evaluation(event['accountId'], "NOT_APPLICABLE", event, resource_type='AWS::::Account')) 337 | evaluations = clean_up_old_evaluations(latest_evaluations, event) 338 | elif isinstance(compliance_result, str): 339 | evaluations.append(build_evaluation_from_config_item(configuration_item, compliance_result)) 340 | elif isinstance(compliance_result, list): 341 | for evaluation in compliance_result: 342 | missing_fields = False 343 | for field in ('ComplianceResourceType', 'ComplianceResourceId', 'ComplianceType', 'OrderingTimestamp'): 344 | if field not in evaluation: 345 | print("Missing " + field + " from custom evaluation.") 346 | missing_fields = True 347 | 348 | if not missing_fields: 349 | latest_evaluations.append(evaluation) 350 | evaluations = clean_up_old_evaluations(latest_evaluations, event) 351 | elif isinstance(compliance_result, dict): 352 | missing_fields = False 353 | for field in ('ComplianceResourceType', 'ComplianceResourceId', 'ComplianceType', 'OrderingTimestamp'): 354 | if field not in compliance_result: 355 | print("Missing " + field + " from custom evaluation.") 356 | missing_fields = True 357 | if not missing_fields: 358 | evaluations.append(compliance_result) 359 | else: 360 | evaluations.append(build_evaluation_from_config_item(configuration_item, 'NOT_APPLICABLE')) 361 | 362 | # Put together the request that reports the evaluation status 363 | resultToken = event['resultToken'] 364 | testMode = False 365 | if resultToken == 'TESTMODE': 366 | # Used solely for RDK test to skip actual put_evaluation API call 367 | testMode = True 368 | # Invoke the Config API to report the result of the evaluation 369 | AWS_CONFIG_CLIENT.put_evaluations(Evaluations=evaluations, ResultToken=resultToken, TestMode=testMode) 370 | # Used solely for RDK test to be able to test Lambda function 371 | return evaluations 372 | 373 | def is_internal_error(exception): 374 | return ((not isinstance(exception, botocore.exceptions.ClientError)) or exception.response['Error']['Code'].startswith('5') 375 | or 'InternalError' in exception.response['Error']['Code'] or 'ServiceError' in exception.response['Error']['Code']) 376 | 377 | def build_internal_error_response(internalErrorMessage, internalErrorDetails=None): 378 | return build_error_response(internalErrorMessage, internalErrorDetails, 'InternalError', 'InternalError') 379 | 380 | def build_error_response(internalErrorMessage, internalErrorDetails=None, customerErrorCode=None, customerErrorMessage=None): 381 | error_response = { 382 | 'internalErrorMessage': internalErrorMessage, 383 | 'internalErrorDetails': internalErrorDetails, 384 | 'customerErrorMessage': customerErrorMessage, 385 | 'customerErrorCode': customerErrorCode 386 | } 387 | print(error_response) 388 | return error_response 389 | -------------------------------------------------------------------------------- /rules/ROOT_NO_ACCESS_KEY/ROOT_NO_ACCESS_KEY_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | try: 4 | from unittest.mock import MagicMock, patch, ANY 5 | except ImportError: 6 | import mock 7 | from mock import MagicMock, patch, ANY 8 | import botocore 9 | from botocore.exceptions import ClientError 10 | 11 | ############## 12 | # Parameters # 13 | ############## 14 | 15 | # Define the default resource to report to Config Rules 16 | DEFAULT_RESOURCE_TYPE = 'AWS::::Account' 17 | 18 | ############# 19 | # Main Code # 20 | ############# 21 | 22 | config_client_mock = MagicMock() 23 | sts_client_mock = MagicMock() 24 | iam_client_mock = MagicMock() 25 | 26 | class Boto3Mock(): 27 | def client(self, client_name, *args, **kwargs): 28 | if client_name == 'config': 29 | return config_client_mock 30 | elif client_name == 'sts': 31 | return sts_client_mock 32 | elif client_name == 'iam': 33 | return iam_client_mock 34 | else: 35 | raise Exception("Attempting to create an unknown client") 36 | 37 | sys.modules['boto3'] = Boto3Mock() 38 | 39 | rule = __import__('ROOT_NO_ACCESS_KEY') 40 | 41 | class ComplianceTest(unittest.TestCase): 42 | 43 | def test_access_keys_present(self): 44 | iam_client_mock.reset_mock() 45 | summary = {'SummaryMap': { 'AccountAccessKeysPresent': 1}} 46 | iam_client_mock.get_account_summary = MagicMock(return_value=summary) 47 | response = rule.lambda_handler(build_lambda_scheduled_event(),{}) 48 | resp_expected = [] 49 | resp_expected.append(build_expected_response('NON_COMPLIANT', '123456789012', annotation='The root user has access key(s).')) 50 | assert_successful_evaluation(self, response, resp_expected) 51 | 52 | def test_access_keys_not_present(self): 53 | iam_client_mock.reset_mock() 54 | summary = {'SummaryMap': { 'AccountAccessKeysPresent': 0}} 55 | iam_client_mock.get_account_summary = MagicMock(return_value=summary) 56 | response=rule.lambda_handler(build_lambda_scheduled_event(),{}) 57 | resp_expected = [] 58 | resp_expected.append(build_expected_response('COMPLIANT', '123456789012')) 59 | assert_successful_evaluation(self, response, resp_expected) 60 | 61 | #################### 62 | # Helper Functions # 63 | #################### 64 | 65 | def build_lambda_configurationchange_event(invoking_event, rule_parameters=None): 66 | event_to_return = { 67 | 'configRuleName':'myrule', 68 | 'executionRoleArn':'roleArn', 69 | 'eventLeftScope': False, 70 | 'invokingEvent': invoking_event, 71 | 'accountId': '123456789012', 72 | 'configRuleArn': 'arn:aws:config:us-east-1:123456789012:config-rule/config-rule-8fngan', 73 | 'resultToken':'token' 74 | } 75 | if rule_parameters: 76 | event_to_return['ruleParameters'] = rule_parameters 77 | return event_to_return 78 | 79 | def build_lambda_scheduled_event(rule_parameters=None): 80 | invoking_event = '{"messageType":"ScheduledNotification","notificationCreationTime":"2017-12-23T22:11:18.158Z"}' 81 | event_to_return = { 82 | 'configRuleName':'myrule', 83 | 'executionRoleArn':'roleArn', 84 | 'eventLeftScope': False, 85 | 'invokingEvent': invoking_event, 86 | 'accountId': '123456789012', 87 | 'configRuleArn': 'arn:aws:config:us-east-1:123456789012:config-rule/config-rule-8fngan', 88 | 'resultToken':'token' 89 | } 90 | if rule_parameters: 91 | event_to_return['ruleParameters'] = rule_parameters 92 | return event_to_return 93 | 94 | def build_expected_response(compliance_type, compliance_resource_id, compliance_resource_type=DEFAULT_RESOURCE_TYPE, annotation=None): 95 | if not annotation: 96 | return { 97 | 'ComplianceType': compliance_type, 98 | 'ComplianceResourceId': compliance_resource_id, 99 | 'ComplianceResourceType': compliance_resource_type 100 | } 101 | return { 102 | 'ComplianceType': compliance_type, 103 | 'ComplianceResourceId': compliance_resource_id, 104 | 'ComplianceResourceType': compliance_resource_type, 105 | 'Annotation': annotation 106 | } 107 | 108 | def assert_successful_evaluation(testClass, response, resp_expected, evaluations_count=1): 109 | if isinstance(response, dict): 110 | testClass.assertEquals(resp_expected['ComplianceType'], response['ComplianceType']) 111 | testClass.assertEquals(resp_expected['ComplianceResourceType'], response['ComplianceResourceType']) 112 | testClass.assertEquals(resp_expected['ComplianceResourceId'], response['ComplianceResourceId']) 113 | testClass.assertTrue(response['OrderingTimestamp']) 114 | if 'Annotation' in resp_expected or 'Annotation' in response: 115 | testClass.assertEquals(resp_expected['Annotation'], response['Annotation']) 116 | elif isinstance(response, list): 117 | testClass.assertEquals(evaluations_count, len(response)) 118 | for i, response_expected in enumerate(resp_expected): 119 | testClass.assertEquals(response_expected['ComplianceType'], response[i]['ComplianceType']) 120 | testClass.assertEquals(response_expected['ComplianceResourceType'], response[i]['ComplianceResourceType']) 121 | testClass.assertEquals(response_expected['ComplianceResourceId'], response[i]['ComplianceResourceId']) 122 | testClass.assertTrue(response[i]['OrderingTimestamp']) 123 | if 'Annotation' in response_expected or 'Annotation' in response[i]: 124 | testClass.assertEquals(response_expected['Annotation'], response[i]['Annotation']) 125 | 126 | def assert_customer_error_response(testClass, response, customerErrorCode=None, customerErrorMessage=None): 127 | if customerErrorCode: 128 | testClass.assertEqual(customerErrorCode, response['customerErrorCode']) 129 | if customerErrorMessage: 130 | testClass.assertEqual(customerErrorMessage, response['customerErrorMessage']) 131 | testClass.assertTrue(response['customerErrorCode']) 132 | testClass.assertTrue(response['customerErrorMessage']) 133 | if "internalErrorMessage" in response: 134 | testClass.assertTrue(response['internalErrorMessage']) 135 | if "internalErrorDetails" in response: 136 | testClass.assertTrue(response['internalErrorDetails']) 137 | 138 | def sts_mock(): 139 | assume_role_response = { 140 | "Credentials": { 141 | "AccessKeyId": "string", 142 | "SecretAccessKey": "string", 143 | "SessionToken": "string"}} 144 | sts_client_mock.reset_mock(return_value=True) 145 | sts_client_mock.assume_role = MagicMock(return_value=assume_role_response) 146 | 147 | ################## 148 | # Common Testing # 149 | ################## 150 | 151 | class TestStsErrors(unittest.TestCase): 152 | 153 | def test_sts_unknown_error(self): 154 | rule.ASSUME_ROLE_MODE = True 155 | sts_client_mock.assume_role = MagicMock(side_effect=botocore.exceptions.ClientError( 156 | {'Error': {'Code': 'unknown-code', 'Message': 'unknown-message'}}, 'operation')) 157 | response = rule.lambda_handler(build_lambda_configurationchange_event('{}'), {}) 158 | assert_customer_error_response( 159 | self, response, 'InternalError', 'InternalError') 160 | 161 | def test_sts_access_denied(self): 162 | rule.ASSUME_ROLE_MODE = True 163 | sts_client_mock.assume_role = MagicMock(side_effect=botocore.exceptions.ClientError( 164 | {'Error': {'Code': 'AccessDenied', 'Message': 'access-denied'}}, 'operation')) 165 | response = rule.lambda_handler(build_lambda_configurationchange_event('{}'), {}) 166 | assert_customer_error_response( 167 | self, response, 'AccessDenied', 'AWS Config does not have permission to assume the IAM role.') -------------------------------------------------------------------------------- /rules/ROOT_NO_ACCESS_KEY/__pycache__/ROOT_NO_ACCESS_KEY.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-config-engine-for-compliance-as-code/0ea788038f74d9c8fc6fd28741af8d3d8bb8fd59/rules/ROOT_NO_ACCESS_KEY/__pycache__/ROOT_NO_ACCESS_KEY.cpython-36.pyc -------------------------------------------------------------------------------- /rules/ROOT_NO_ACCESS_KEY/__pycache__/ROOT_NO_ACCESS_KEY_test.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-config-engine-for-compliance-as-code/0ea788038f74d9c8fc6fd28741af8d3d8bb8fd59/rules/ROOT_NO_ACCESS_KEY/__pycache__/ROOT_NO_ACCESS_KEY_test.cpython-36.pyc -------------------------------------------------------------------------------- /rules/ROOT_NO_ACCESS_KEY/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "ROOT_NO_ACCESS_KEY", 5 | "SourceRuntime": "python3.6", 6 | "CodeKey": "ROOT_NO_ACCESS_KEY.zip", 7 | "InputParameters": "{}", 8 | "OptionalParameters": "{}", 9 | "SourcePeriodic": "TwentyFour_Hours", 10 | "RuleSets": [ 11 | "baseline", 12 | "rulecriticity:critical", 13 | "pci", 14 | "pci:7.1", 15 | "root" 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /rules/Readme.md: -------------------------------------------------------------------------------- 1 | Those rules are a mix of custom and managed Config Rules. 2 | 3 | Find more examples and use cases: https://github.com/awslabs/aws-config-rules/ -------------------------------------------------------------------------------- /rules/S3_BUCKET_PUBLIC_READ_PROHIBITED/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "S3_BUCKET_PUBLIC_READ_PROHIBITED", 5 | "SourceRuntime": null, 6 | "CodeKey": null, 7 | "InputParameters": "{}", 8 | "OptionalParameters": "{}", 9 | "SourceEvents": "AWS::S3::Bucket", 10 | "SourceIdentifier": "S3_BUCKET_PUBLIC_READ_PROHIBITED", 11 | "RuleSets": [ 12 | "baseline", 13 | "rulecriticity:critical", 14 | "otherregionsbaseline" 15 | ] 16 | } 17 | } -------------------------------------------------------------------------------- /rules/S3_BUCKET_PUBLIC_WRITE_PROHIBITED/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "S3_BUCKET_PUBLIC_WRITE_PROHIBITED", 5 | "SourceRuntime": null, 6 | "CodeKey": null, 7 | "InputParameters": "{}", 8 | "OptionalParameters": "{}", 9 | "SourceEvents": "AWS::S3::Bucket", 10 | "SourceIdentifier": "S3_BUCKET_PUBLIC_WRITE_PROHIBITED", 11 | "RuleSets": [ 12 | "baseline", 13 | "rulecriticity:medium", 14 | "otherregionsbaseline" 15 | ] 16 | } 17 | } -------------------------------------------------------------------------------- /rules/S3_BUCKET_SSL_REQUESTS_ONLY/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "S3_BUCKET_SSL_REQUESTS_ONLY", 5 | "SourceRuntime": null, 6 | "CodeKey": null, 7 | "InputParameters": "{}", 8 | "OptionalParameters": "{}", 9 | "SourceEvents": "AWS::S3::Bucket", 10 | "SourceIdentifier": "S3_BUCKET_SSL_REQUESTS_ONLY", 11 | "RuleSets": [ 12 | "confidentiality:high", 13 | "confidentiality:medium", 14 | "pci", 15 | "rulecriticity:high", 16 | "otherregionsbaseline" 17 | ] 18 | } 19 | } -------------------------------------------------------------------------------- /rules/VPC_DEFAULT_SECURITY_GROUP_CLOSED/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "VPC_DEFAULT_SECURITY_GROUP_CLOSED", 5 | "SourceRuntime": null, 6 | "CodeKey": null, 7 | "InputParameters": "{}", 8 | "SourceEvents": "AWS::EC2::SecurityGroup", 9 | "SourceIdentifier": "VPC_DEFAULT_SECURITY_GROUP_CLOSED", 10 | "RuleSets": [ 11 | "baseline", 12 | "rulecriticity:medium", 13 | "otherregionsbaseline" 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /rules/VPC_SG_OPEN_ONLY_TO_AUTHORIZED_PORTS/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "1.0", 3 | "Parameters": { 4 | "RuleName": "VPC_SG_OPEN_ONLY_TO_AUTHORIZED_PORTS", 5 | "SourceRuntime": null, 6 | "CodeKey": null, 7 | "InputParameters": "{}", 8 | "OptionalParameters": "{\"authorizedTcpPorts\": \"443\", \"authorizedUdpPorts\": \"\"}", 9 | "SourceEvents": "AWS::EC2::SecurityGroup", 10 | "SourceIdentifier": "VPC_SG_OPEN_ONLY_TO_AUTHORIZED_PORTS", 11 | "RuleSets": [ 12 | "baseline", 13 | "rulecriticity:high", 14 | "otherregionsbaseline" 15 | ] 16 | }, 17 | "Tags": "[]" 18 | } -------------------------------------------------------------------------------- /rulesets-build/buildspec_buildtemplates.yaml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | commands: 6 | - echo Entered the install phase... 7 | - apt-get update -y 8 | - apt-get install zip 9 | - pip install rdk 10 | - curl -O -L https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 11 | - chmod +x jq-linux64 12 | - sudo mv jq-linux64 /usr/bin/jq 13 | pre_build: 14 | commands: 15 | - echo Entered the pre_build phase... 16 | build: 17 | commands: 18 | - echo Entered the build phase... 19 | - echo Build started on `date` 20 | - echo [] Create lambda for all the rules 21 | - if [ "$OTHER_ACTIVE_REGIONS" != "none" ]; then chmod a+x ./rulesets-build/multi-region/deploy_lambda.sh; ./rulesets-build/multi-region/deploy_lambda.sh $OTHER_ACTIVE_REGIONS $ENGINE_RULE_NAME $AWS_DEFAULT_REGION; fi 22 | - cd rules 23 | - rdk deploy -f --all > ../result.txt 24 | - echo [] List all the rulesets 25 | - rdk rulesets list > rulesets_list.txt 26 | - aws s3 cp rulesets_list.txt s3://$OUTPUT_BUCKET/rulesets_list.txt 27 | - echo [] Create default template for main region 28 | - rdk create-rule-template --rulesets $DEFAULT_RULESET --output-file default.json --rules-only 29 | - aws s3 cp default.json s3://$OUTPUT_BUCKET/default.json 30 | - cd .. 31 | - echo [] Create default template for all other regions 32 | - if [ "$OTHER_ACTIVE_REGIONS" != "none" ]; then chmod a+x ./rulesets-build/multi-region/generate_default_template.sh; ./rulesets-build/multi-region/generate_default_template.sh $OTHER_ACTIVE_REGIONS $DEFAULT_RULESET_OTHER_REGIONS $OUTPUT_BUCKET_NO_REGION; fi 33 | - echo Copy Account_List if it exists 34 | - if [ "$ACCOUNT_LIST" != "none" ]; then aws s3 cp s3://$ACCOUNT_LIST account_list.json; chmod a+x ./rulesets-build/generate_rule_templates_per_account.sh; ./rulesets-build/generate_rule_templates_per_account.sh $OTHER_ACTIVE_REGIONS $OUTPUT_BUCKET $OUTPUT_BUCKET_NO_REGION >> result.txt; cat account_list.json | jq -r '.AllAccounts[] | ([.Accountname, .AccountID , (.OwnerEmail | join(";")), (.Tags| join(","))] | join(","))' > account_list.csv; aws s3 cp account_list.csv s3://$OUTPUT_BUCKET/csv/account_list.csv; fi 35 | - echo deploy/update ETL 36 | - zip -j etl_evaluations.zip ./rulesets-build/etl_evaluations.py 37 | - aws lambda update-function-code --function-name ComplianceEngine-ETL --zip-file fileb://etl_evaluations.zip 38 | - echo deploy/update Athena 39 | - if [ "$DATALAKE_QUERIES_BOOL" = "true" ] && [ "$FIREHOSE_KEY_LIST" != "none" ] && [ "$ATHENA_COLUMN_LIST" != "none" ]; then chmod a+x ./rulesets-build/deploy_datalake.sh; ./rulesets-build/deploy_datalake.sh "$CONFIG_CENTRAL_BUCKET" "$COMPLIANCE_EVENT_CENTRAL_BUCKET" "$FIREHOSE_KEY_LIST" "$ATHENA_COLUMN_LIST" "$ACCOUNT_LIST" "$OUTPUT_BUCKET"; fi 40 | post_build: 41 | commands: 42 | - echo Entered the post_build phase... 43 | - echo Build completed on `date` 44 | artifacts: 45 | files: 46 | - result.txt 47 | - ./rulesets-build/buildspec_deploytemplates.yaml 48 | - ./rulesets-build/deploy_rule_templates.py 49 | discard-paths: yes 50 | -------------------------------------------------------------------------------- /rulesets-build/buildspec_deploytemplates.yaml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | commands: 6 | - echo "No install needed." 7 | build: 8 | commands: 9 | - echo Entered the build phase... 10 | - echo Build started on `date` 11 | - python ./deploy_rule_templates.py $AWS_DEFAULT_REGION $OUTPUT_BUCKET_NO_REGION $ENGINE_RULE_NAME $OTHER_ACTIVE_REGIONS 12 | post_build: 13 | commands: 14 | - echo Entered the post_build phase... 15 | - echo Build completed on `date` 16 | -------------------------------------------------------------------------------- /rulesets-build/compliance-account-analytics-setup.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2017-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). 5 | # You may not use this file except in compliance with the License. 6 | # A copy of the License is located at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # or in the "license" file accompanying this file. This file is distributed 11 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | # express or implied. See the License for the specific language governing 13 | # permissions and limitations under the License. 14 | # 15 | 16 | AWSTemplateFormatVersion: 2010-09-09 17 | Description: Sets up the Datalake for the Compliance-as-Code. 18 | 19 | Metadata: 20 | AWS::CloudFormation::Interface: 21 | ParameterGroups: 22 | - Label: 23 | default: Compliance-as-Code Dalatake Configuration 24 | Parameters: 25 | - CentralizedS3BucketConfig 26 | - CentralizedS3BucketComplianceEventName 27 | 28 | Parameters: 29 | CentralizedS3BucketConfig: 30 | ConstraintDescription: Enter DNS-compliant prefix 31 | Description: Bucket prefix where Config logs are stored. A dash and the account ID (12-digit) will be appended to the name. 32 | MaxLength: 63 33 | MinLength: 10 34 | Type: String 35 | 36 | CentralizedS3BucketComplianceEventName: 37 | ConstraintDescription: Enter DNS-compliant prefix 38 | Description: Bucket prefix where Compliance Event are stored. A dash and the account ID (12-digit) will be appended to the name. 39 | MaxLength: 63 40 | MinLength: 10 41 | Type: String 42 | 43 | FolderWhereFireHoseIsSending: 44 | Description: Folder in the Centralized Bucket of Compliance event, where Firehose loads the data. 45 | Default: compliance-as-code-events 46 | MaxLength: 63 47 | MinLength: 10 48 | Type: String 49 | 50 | KeyListGeneratedByFirehose: 51 | Description: List of the key in the json generated by Kinesis Firehose. 52 | Type: String 53 | 54 | ColumnKeyList: 55 | Description: List of the columns in Athena. 56 | Type: String 57 | 58 | AccountList: 59 | Description: Verify if Account List is configured. 60 | Type: String 61 | 62 | LocationAccountListCSV: 63 | Description: Location where the account_list.csv is stored. 64 | Type: String 65 | 66 | Conditions: 67 | AccountList: !Not [ !Equals [!Ref AccountList, "none"]] 68 | 69 | Resources: 70 | AthenaNamedQueryInitDB: 71 | Type: AWS::Athena::NamedQuery 72 | Properties: 73 | Database: "default" 74 | Description: "(To be run 1st) A query to build database for advanced analytics" 75 | Name: "1-Database For ComplianceAsCode" 76 | QueryString: "CREATE DATABASE IF NOT EXISTS complianceascode" 77 | 78 | AthenaNamedQueryInitTable: 79 | Type: AWS::Athena::NamedQuery 80 | Properties: 81 | Database: "complianceascode" 82 | Description: "(To be run 2nd) A query to build table for advanced analytics" 83 | Name: "2-Table For ComplianceAsCode" 84 | QueryString: !Join 85 | - "" 86 | - - CREATE EXTERNAL TABLE IF NOT EXISTS complianceascode.events ( 87 | - !Ref ColumnKeyList 88 | - ") ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe' WITH SERDEPROPERTIES ('paths'='" 89 | - !Ref KeyListGeneratedByFirehose 90 | - "') STORED AS INPUTFORMAT 'org.apache.hadoop.mapred.TextInputFormat' OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat' LOCATION 's3://" 91 | - !Join [ "-", [ !Ref CentralizedS3BucketComplianceEventName, !Ref 'AWS::AccountId']] 92 | - / 93 | - !Ref FolderWhereFireHoseIsSending 94 | - "/' TBLPROPERTIES ('classification'='json', 'compressionType'='gzip', 'transient_lastDdlTime'='1521161215', 'typeOfData'='file')" 95 | 96 | AthenaNamedQueryConfigTable: 97 | Type: AWS::Athena::NamedQuery 98 | Properties: 99 | Database: "complianceascode" 100 | Description: "(To be run 3rd) A query to build table for query config" 101 | Name: "3-Table For Config in ComplianceAsCode" 102 | QueryString: !Join 103 | - "" 104 | - - "CREATE EXTERNAL TABLE IF NOT EXISTS complianceascode.config (fileVersion string, configSnapshotId string, configurationItems array>) ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe' WITH SERDEPROPERTIES ('serialization.format' = '1') LOCATION 's3://" 105 | - !Join [ "-", [ !Ref CentralizedS3BucketConfig, !Ref 'AWS::AccountId']] 106 | - "/'" 107 | 108 | AccountListTable: 109 | Type: AWS::Athena::NamedQuery 110 | Properties: 111 | Database: "complianceascode" 112 | Description: "(To be run 4th) A query to build table for listing all the accounts" 113 | Name: "4-Table For AccountList" 114 | QueryString: !Join 115 | - "" 116 | - - "CREATE EXTERNAL TABLE IF NOT EXISTS complianceascode.accountlist (accountname string, accountid string, owneremails string, tag1 string, tag2 string ) ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.OpenCSVSerde' WITH SERDEPROPERTIES ('separatorChar'=',') STORED AS INPUTFORMAT 'org.apache.hadoop.mapred.TextInputFormat' OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat' LOCATION 's3://" 117 | - !Ref LocationAccountListCSV 118 | - "/csv'" -------------------------------------------------------------------------------- /rulesets-build/compliance-whitelist.json: -------------------------------------------------------------------------------- 1 | { 2 | "Whitelist": [{ 3 | "ConfigRuleArn": "arn:aws:config:REGION:ACCOUNT_1:config-rule/config-rule-UUID_OF_THE_RULE_1", 4 | "WhitelistedResources": [{ 5 | "ResourceIds": [ 6 | "RESOURCE_ID" 7 | ], 8 | "ApprovalTicket": "OPTIONAL_FIELD", 9 | "ValidUntil": "2019-06-01" 10 | } 11 | ] 12 | }, { 13 | "ConfigRuleArn": "arn:aws:config:REGION:ACCOUNT_2:config-rule/config-rule-UUID_OF_THE_RULE_2", 14 | "WhitelistedResources": [{ 15 | "ResourceIds": [ 16 | "RESOURCE_ID_1", 17 | "RESOURCE_ID_2", 18 | "RESOURCE_ID_3" 19 | ], 20 | "ApprovalTicket": "OPTIONAL_LINK_OR_REFERENCE_NUMBER", 21 | "ValidUntil": "2019-06-01" 22 | } 23 | ] 24 | }], 25 | "ProcessToWhitelist": "OPTIONAL_LINK_TO_PROCESS" 26 | } -------------------------------------------------------------------------------- /rulesets-build/deploy_datalake.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | centralizedbucketconfig=("$1") 4 | centralizedcomplianceevent=("$2") 5 | keylistfirehose=("$3") 6 | columnkeylist=("$4") 7 | accountlist=("$5") 8 | locationaccountlist=("$6") 9 | 10 | aws cloudformation deploy --stack-name Compliance-Engine-Datalake-DO-NOT-DELETE --template-file ./rulesets-build/compliance-account-analytics-setup.yaml --no-fail-on-empty-changeset --parameter-overrides CentralizedS3BucketConfig="${centralizedbucketconfig[@]}" CentralizedS3BucketComplianceEventName="${centralizedcomplianceevent[@]}" KeyListGeneratedByFirehose="${keylistfirehose[@]}" ColumnKeyList="${columnkeylist[@]}" AccountList="${accountlist[@]}" LocationAccountListCSV="${locationaccountlist[@]}" 11 | 12 | response=$(aws cloudformation list-change-sets --stack-name Compliance-Engine-Datalake-DO-NOT-DELETE --query "Summaries[*].ChangeSetName" --output text) 13 | declare -a changesets=($response) 14 | for changeset in "${changesets[@]}"; do 15 | aws cloudformation delete-change-set --change-set-name $changeset --stack-name Compliance-Engine-Datalake-DO-NOT-DELETE 16 | done 17 | -------------------------------------------------------------------------------- /rulesets-build/deploy_rule_templates.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | import re 4 | import time 5 | import boto3 6 | 7 | main_region = sys.argv[1] 8 | template_bucket_name_prefix = sys.argv[2] 9 | initial_deployed_rule = sys.argv[3] 10 | other_regions = sys.argv[4] 11 | default_template_name = "default.json" 12 | remote_execution_role_name = "AWSConfigAndComplianceAuditRole-DO-NOT-DELETE" 13 | remote_execution_path_name = "service-role/" 14 | stack_name = "Compliance-Engine-Benchmark-DO-NOT-DELETE" 15 | 16 | central_sts_client = boto3.client('sts') 17 | central_account_id = central_sts_client.get_caller_identity()["Account"] 18 | 19 | all_region_list = [] 20 | all_region_list.append(main_region) 21 | if other_regions != 'none': 22 | other_regions_list = other_regions.split(',') 23 | all_region_list += other_regions_list 24 | 25 | s3 = boto3.resource('s3') 26 | s3_client = boto3.client('s3') 27 | 28 | for region in all_region_list: 29 | template_bucket_name = template_bucket_name_prefix + '-' + region 30 | default_template_obj = s3.Object(template_bucket_name, default_template_name) 31 | default_template = json.loads(default_template_obj.get()['Body'].read().decode('utf-8')) 32 | 33 | contents = s3_client.list_objects(Bucket=template_bucket_name)['Contents'] 34 | list_of_account_to_review = [] 35 | 36 | for s3_object in contents: 37 | #Assumes S3 template keys are of the form <12-digit-account-id>.json 38 | key = s3_object["Key"] 39 | 40 | if not re.match('^[0-9]{12}\.json$', key): 41 | #Skip this one 42 | print("Skipping " + key) 43 | continue 44 | 45 | remote_account_id = key.split(".")[0] 46 | 47 | obj = s3.Object(template_bucket_name, key) 48 | template = obj.get()['Body'].read().decode('utf-8') 49 | 50 | #Check if the remote Rule template is empty. If it is, use the default Rule template. 51 | #template_key = key 52 | if not template: 53 | default_obj = s3.Object(template_bucket_name, default_template_name) 54 | template = default_obj.get()['Body'].read().decode('utf-8') 55 | 56 | remote_session = None 57 | try: 58 | remote_sts_client = boto3.client('sts') 59 | response = remote_sts_client.assume_role( 60 | RoleArn='arn:aws:iam::'+remote_account_id+':role/' + remote_execution_path_name + remote_execution_role_name, 61 | RoleSessionName='ComplianceAutomationSession' 62 | ) 63 | 64 | remote_session = boto3.Session( 65 | aws_access_key_id=response['Credentials']['AccessKeyId'], 66 | aws_secret_access_key=response['Credentials']['SecretAccessKey'], 67 | aws_session_token=response['Credentials']['SessionToken'] 68 | ) 69 | except Exception as e3: 70 | print("Failed to assume role into remote account. " + str(e3)) 71 | continue 72 | 73 | cfn = remote_session.client("cloudformation", region_name=region) 74 | try: 75 | print("Attempting to update Rule stack.") 76 | update_response = cfn.update_stack( 77 | StackName=stack_name, 78 | TemplateBody=template, 79 | Parameters=[ 80 | { 81 | 'ParameterKey': 'LambdaAccountId', 82 | 'ParameterValue': central_account_id 83 | } 84 | ], 85 | Capabilities=['CAPABILITY_NAMED_IAM'] 86 | ) 87 | print("Update triggered for " + remote_account_id + ".") 88 | list_of_account_to_review.append(remote_account_id) 89 | except Exception as e: 90 | if "No updates are to be performed." in str(e): 91 | print("Stack already up-to-date.") 92 | continue 93 | 94 | if "does not exist" in str(e): 95 | try: 96 | print("Stack not found. Attempting to create Rule stack.") 97 | create_response = cfn.create_stack( 98 | StackName=stack_name, 99 | TemplateBody=template, 100 | Parameters=[ 101 | { 102 | 'ParameterKey': 'LambdaAccountId', 103 | 'ParameterValue': central_account_id 104 | } 105 | ], 106 | Capabilities=['CAPABILITY_NAMED_IAM'] 107 | ) 108 | print("Creation triggered for " + remote_account_id + ".") 109 | list_of_account_to_review.append(remote_account_id) 110 | continue 111 | except Exception as e2: 112 | print("Error creating new stack: " + str(e2)) 113 | 114 | print("Error no condition matched: " + str(e)) 115 | 116 | if not list_of_account_to_review: 117 | continue 118 | 119 | time.sleep(20) 120 | 121 | for remote_account_id in list_of_account_to_review: 122 | remote_session = None 123 | try: 124 | remote_sts_client = boto3.client('sts') 125 | response = remote_sts_client.assume_role( 126 | RoleArn='arn:aws:iam::'+remote_account_id+':role/' + remote_execution_path_name + remote_execution_role_name, 127 | RoleSessionName='ComplianceAutomationTriggerRuleSession' 128 | ) 129 | 130 | remote_session = boto3.Session( 131 | aws_access_key_id=response['Credentials']['AccessKeyId'], 132 | aws_secret_access_key=response['Credentials']['SecretAccessKey'], 133 | aws_session_token=response['Credentials']['SessionToken'] 134 | ) 135 | except Exception as e3: 136 | print("Failed to assume role into remote account. " + str(e3)) 137 | continue 138 | 139 | config_client = remote_session.client("config", region_name=region) 140 | try: 141 | print("Attempting to trigger the crawler Rule.") 142 | config_client.start_config_rules_evaluation(ConfigRuleNames=[initial_deployed_rule]) 143 | except Exception as e: 144 | print("Error when triggering the crawler Rule: " + str(e)) 145 | 146 | sys.exit(0) 147 | -------------------------------------------------------------------------------- /rulesets-build/etl_evaluations.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import datetime 4 | import os 5 | import zipfile 6 | import boto3 7 | 8 | # DEFINE WHITELIST & RULESET LOCATION 9 | # Define the Bucket prefix where the ruleset.zip and whitelist are posted in the Compliance Account. 10 | BUCKET_PREFIX = 'compliance-engine-codebuild-source' 11 | ORIGINAL_ZIP_RULES = 'ruleset.zip' 12 | ORIGINAL_ZIP_RULES_FOLDER = 'rules' 13 | FILE_NAME_RULE_PARAMETER = 'parameters.json' 14 | 15 | # RULESET PARAMETERS 16 | # Define the delimiter in the ruleset.txt 17 | BUCKET_PREFIX_RULESET_TXT = 'compliance-engine-codebuild-output' 18 | RULESET_LIST = 'rulesets_list.txt' 19 | DELIMITER_IN_RULESET_LIST = ' ' 20 | TITLE_IN_RDK_RULESET = 'RuleSets:' 21 | DELIMITER_IN_RULESET = ':' 22 | DELIMITER_MULTI = ',' 23 | 24 | # KEEPING ATHENA QUERIES UP TO DATE 25 | CODEBUILD_TEMPLATE_NAME = 'Compliance-Rule-Template-Build' 26 | CODEPIPELINE_NAME = 'Compliance-Engine-Pipeline' 27 | 28 | S3_CLIENT = boto3.client('s3') 29 | 30 | def is_compliance_result_whitelisted(result): 31 | try: 32 | whitelist_key = os.environ['ComplianceWhitelist'] 33 | if whitelist_key == 'none': 34 | return False 35 | bucket_wl = whitelist_key.split("/")[0] 36 | key_wl = "/".join(whitelist_key.split("/")[1:]) 37 | object_wl = S3_CLIENT.get_object(Bucket=bucket_wl, Key=key_wl) 38 | whitelist_json = json.loads(object_wl["Body"].read().decode("utf-8")) 39 | 40 | for whitelist_item in whitelist_json["Whitelist"]: 41 | if whitelist_item["ConfigRuleArn"] == result["ConfigRuleArn"]: 42 | for whitelisted_resources in whitelist_item["WhitelistedResources"]: 43 | if result["ResourceId"] in whitelisted_resources["ResourceIds"] \ 44 | and whitelisted_resources["ApprovalTicket"] \ 45 | and datetime.datetime.today().date() <= datetime.datetime.strptime(whitelisted_resources["ValidUntil"], '%Y-%m-%d').date(): 46 | print(result["ResourceId"] + " whitelisted for " + result["ConfigRuleArn"] + ".") 47 | return True 48 | return False 49 | except Exception as ex: 50 | print("Whitelisting review went wrong: {}".format(str(ex))) 51 | return False 52 | 53 | def download_rules_parameters_locally(bucket): 54 | s3_resource = boto3.resource('s3') 55 | local_file_name = '/tmp/' + ORIGINAL_ZIP_RULES 56 | s3_resource.Bucket(bucket).download_file(ORIGINAL_ZIP_RULES, local_file_name) 57 | 58 | with zipfile.ZipFile(local_file_name, 'r') as zip_ref: 59 | zip_ref.extractall('/tmp/') 60 | 61 | return True 62 | 63 | def get_ruleset_definition(bucket): 64 | object_rs = S3_CLIENT.get_object(Bucket=bucket, Key=RULESET_LIST) 65 | ruleset_str = object_rs["Body"].read().decode("utf-8") 66 | ruleset_list_unprocessed = ruleset_str.replace('\n', ' ').split(DELIMITER_IN_RULESET_LIST) 67 | ruleset_list = [] 68 | 69 | for ruleset in ruleset_list_unprocessed: 70 | ruleset_details_dict = {} 71 | if ruleset in [TITLE_IN_RDK_RULESET, '']: 72 | continue 73 | 74 | if DELIMITER_IN_RULESET in ruleset: 75 | ruleset_details = ruleset.split(DELIMITER_IN_RULESET) 76 | ruleset_details_dict["RulesetName"] = ruleset_details[0] 77 | ruleset_details_dict["MultiValue"] = True 78 | if ruleset_details_dict not in ruleset_list: 79 | ruleset_list.append(ruleset_details_dict) 80 | continue 81 | 82 | ruleset_details_dict["RulesetName"] = ruleset 83 | ruleset_details_dict["MultiValue"] = False 84 | ruleset_list.append(ruleset_details_dict) 85 | 86 | return ruleset_list 87 | 88 | def get_rule_rulesets(rule_name): 89 | with open('/tmp/' + ORIGINAL_ZIP_RULES_FOLDER + '/' + rule_name + '/' + FILE_NAME_RULE_PARAMETER) as infile: 90 | parameters = json.load(infile) 91 | 92 | #in case, multi-value 93 | all_ruleset_categories = [] 94 | return_ruleset = [] 95 | 96 | for ruleset in parameters['Parameters']['RuleSets']: 97 | if DELIMITER_IN_RULESET not in ruleset: 98 | return_ruleset.append(ruleset) 99 | 100 | if ruleset.split(DELIMITER_IN_RULESET)[0] in all_ruleset_categories: 101 | continue 102 | all_ruleset_categories.append(ruleset.split(DELIMITER_IN_RULESET)[0]) 103 | 104 | value_ruleset = [] 105 | for ruleset_second in parameters['Parameters']['RuleSets']: 106 | if DELIMITER_IN_RULESET not in ruleset_second: 107 | continue 108 | if ruleset.split(DELIMITER_IN_RULESET)[0] == ruleset_second.split(DELIMITER_IN_RULESET)[0]: 109 | value_ruleset.append(ruleset_second.split(DELIMITER_IN_RULESET)[1]) 110 | value_ruleset.sort() 111 | return_ruleset.append(ruleset.split(DELIMITER_IN_RULESET)[0]+DELIMITER_IN_RULESET+DELIMITER_MULTI.join(value_ruleset)) 112 | 113 | return return_ruleset 114 | 115 | def add_ruleset_fields(etl_data, ruleset_definition_list, rule_rulesets_list): 116 | for ruleset in ruleset_definition_list: 117 | etl_data[ruleset["RulesetName"]] = get_value_for_rule(rule_rulesets_list, ruleset) 118 | return etl_data 119 | 120 | def get_value_for_rule(rule_rulesets_list, ruleset): 121 | if not ruleset['MultiValue']: 122 | if ruleset['RulesetName'] not in rule_rulesets_list: 123 | return 'False' 124 | return 'True' 125 | 126 | for each_rule_ruleset in rule_rulesets_list: 127 | try: 128 | if each_rule_ruleset.split(DELIMITER_IN_RULESET)[0] == ruleset['RulesetName']: 129 | return each_rule_ruleset.split(DELIMITER_IN_RULESET)[1] 130 | except: 131 | # Not splitable, meaning not multivalue 132 | continue 133 | 134 | # no rule_ruleset matched, meaning not present 135 | return 'False' 136 | 137 | def update_codebuild_param(ruleset_definition_list): 138 | codebuild_client = boto3.client('codebuild') 139 | new_deployment_needed = False 140 | 141 | codebuild_template = codebuild_client.batch_get_projects(names=[CODEBUILD_TEMPLATE_NAME]) 142 | env_variables = codebuild_template['projects'][0]['environment']['environmentVariables'] 143 | 144 | env_variables_new_list = [] 145 | current_value = {} 146 | for env_var in env_variables: 147 | if env_var['name'] == 'DATALAKE_QUERIES_BOOL': 148 | if env_var['value'] == 'false': 149 | return False 150 | if env_var['name'] in ['FIREHOSE_KEY_LIST', 'ATHENA_COLUMN_LIST']: 151 | current_value[env_var['name']] = env_var['value'] 152 | continue 153 | env_variables_new_list.append(env_var) 154 | 155 | commun_col = [ 156 | 'ConfigRuleArn', 157 | 'EngineRecordedTime', 158 | 'ConfigRuleName', 159 | 'ResourceType', 160 | 'ResourceId', 161 | 'ResultRecordedTime', 162 | 'ConfigRuleInvokedTime', 163 | 'AccountId', 164 | 'AwsRegion', 165 | 'Annotation', 166 | 'ComplianceType', 167 | 'WhitelistedComplianceType'] 168 | 169 | all_col = [] 170 | all_col += commun_col 171 | for ruleset_definition in ruleset_definition_list: 172 | all_col.append(ruleset_definition['RulesetName']) 173 | new_value_firehose_key = ','.join(all_col) 174 | 175 | if current_value['FIREHOSE_KEY_LIST'] != new_value_firehose_key: 176 | new_deployment_needed = True 177 | 178 | key_list_env = { 179 | 'name': 'FIREHOSE_KEY_LIST', 180 | 'value': new_value_firehose_key 181 | } 182 | env_variables_new_list.append(key_list_env) 183 | 184 | athena_env_value_list = [] 185 | for col in all_col: 186 | athena_env_value_list.append("`" + col.lower() + "` string") 187 | new_value_athena = ','.join(athena_env_value_list) 188 | 189 | if current_value['ATHENA_COLUMN_LIST'] != new_value_athena: 190 | new_deployment_needed = True 191 | 192 | athena_env = { 193 | 'name': 'ATHENA_COLUMN_LIST', 194 | 'value': new_value_athena 195 | } 196 | env_variables_new_list.append(athena_env) 197 | 198 | if new_deployment_needed: 199 | env = { 200 | 'type': codebuild_template['projects'][0]['environment']['type'], 201 | 'image': codebuild_template['projects'][0]['environment']['image'], 202 | 'computeType': codebuild_template['projects'][0]['environment']['computeType'], 203 | 'environmentVariables': env_variables_new_list 204 | } 205 | codebuild_client.update_project(name=CODEBUILD_TEMPLATE_NAME, environment=env) 206 | return True 207 | 208 | return False 209 | 210 | def lambda_handler(event, context): 211 | compliance_account_id = context.invoked_function_arn.split(":")[4] 212 | compliance_account_region = context.invoked_function_arn.split(":")[3] 213 | artifact_bucket = "-".join([BUCKET_PREFIX, compliance_account_id, compliance_account_region]) 214 | ruleset_bucket = "-".join([BUCKET_PREFIX_RULESET_TXT, compliance_account_id, compliance_account_region]) 215 | 216 | download_rules_parameters_locally(artifact_bucket) 217 | 218 | ruleset_definition_list = [] 219 | ruleset_definition_list = get_ruleset_definition(ruleset_bucket) 220 | if update_codebuild_param(ruleset_definition_list): 221 | try: 222 | codepipeline_client = boto3.client('codepipeline') 223 | codepipeline_client.start_pipeline_execution(name=CODEPIPELINE_NAME) 224 | except Exception as e: 225 | print('Error not able to trigger the codepipeline: ' + str(e)) 226 | 227 | output = [] 228 | for record in event['records']: 229 | payload = base64.b64decode(record['data']) 230 | payload_data = json.loads(payload.decode("utf-8")) 231 | etl_data = { 232 | "ConfigRuleArn": payload_data['ConfigRuleArn'], 233 | "EngineRecordedTime": payload_data['EngineRecordedTime'], 234 | "ConfigRuleName": payload_data["ConfigRuleName"], 235 | "ResourceType": payload_data['ResourceType'], 236 | "ResourceId": payload_data['ResourceId'], 237 | "ComplianceType": payload_data['ComplianceType'], 238 | "ResultRecordedTime": payload_data['ResultRecordedTime'], 239 | "ConfigRuleInvokedTime": payload_data['ConfigRuleInvokedTime'], 240 | "AccountId": payload_data['AccountId'], 241 | "AwsRegion": payload_data['AwsRegion'], 242 | "Annotation": payload_data['Annotation'] 243 | } 244 | if is_compliance_result_whitelisted(etl_data): 245 | del etl_data['ComplianceType'] 246 | etl_data['ComplianceType'] = 'COMPLIANT' 247 | etl_data["WhitelistedComplianceType"] = 'True' 248 | else: 249 | etl_data["WhitelistedComplianceType"] = 'False' 250 | 251 | rule_rulesets_list = get_rule_rulesets(etl_data["ConfigRuleName"]) 252 | etl_data = add_ruleset_fields(etl_data, ruleset_definition_list, rule_rulesets_list) 253 | data_to_return = json.dumps(etl_data) + '\n' 254 | output_record = { 255 | 'recordId': record['recordId'], 256 | 'result': 'Ok', 257 | 'data': base64.b64encode(data_to_return.encode('utf-8')).decode("utf-8") 258 | } 259 | output.append(output_record) 260 | return {'records': output} 261 | -------------------------------------------------------------------------------- /rulesets-build/generate_rule_templates_per_account.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | OTHER_REGIONS=$1 4 | if [ "$OTHER_REGIONS" != "none" ]; then 5 | cat account_list.json | jq -r '.AllAccounts[] | ([.AccountID , .Region, (.Tags | join(","))] | join(" "))' > wellformedlist.txt 6 | 7 | cd rules 8 | while IFS=' ' read -ra line; do 9 | account_id="${line[0]}" 10 | template_file_name="${account_id}.json" 11 | IFS=',' read -r -a array <<< ${line[1]} 12 | rulesets="${line[2]}" 13 | regionname="${array[@]}" 14 | echo Generate in $regionname for $account_id 15 | rdk create-rule-template --rulesets ${rulesets} --output-file ${template_file_name} --rules-only 16 | aws s3 cp ${template_file_name} s3://$3-$regionname/${template_file_name} 17 | done < ../wellformedlist.txt 18 | else 19 | cat account_list.json | jq -r '.AllAccounts[] | ([.AccountID , (.Tags | join(","))] | join(" "))' > wellformedlist.txt 20 | 21 | cd rules 22 | while IFS=' ' read -ra line; do 23 | account_id="${line[0]}" 24 | template_file_name="${account_id}.json" 25 | rulesets="${line[1]}" 26 | rdk create-rule-template --rulesets ${rulesets} --output-file ${template_file_name} --rules-only 27 | aws s3 cp ${template_file_name} s3://$2/${template_file_name} 28 | 29 | done < ../wellformedlist.txt 30 | 31 | fi 32 | -------------------------------------------------------------------------------- /rulesets-build/multi-region/deploy_lambda.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd rules 4 | 5 | IFS=',' read -r -a array <<< $1 6 | for regionname in "${array[@]}"; do 7 | echo Deploy in $regionname 8 | rdk -r $regionname deploy -f --all 9 | funcname=${2//_/} 10 | aws lambda update-function-configuration --function-name RDK-Rule-Function-$funcname --environment Variables={MainRegion=$3} --region $regionname 11 | done 12 | 13 | cd .. 14 | -------------------------------------------------------------------------------- /rulesets-build/multi-region/generate_default_template.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd rules 4 | rdk create-rule-template --rulesets $2 --output-file otherregionsdefault.json --rules-only 5 | IFS=',' read -r -a array <<< $1 6 | for regionname in "${array[@]}"; do 7 | echo Generate in $regionname 8 | aws s3 cp otherregionsdefault.json s3://$3-$regionname/default.json 9 | done 10 | cd .. 11 | --------------------------------------------------------------------------------