├── .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 |
82 |
83 | ## Low Level Design
84 |
85 |
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 |
--------------------------------------------------------------------------------