diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..77f2a0a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2020-09-29 + +### Added + +- Initial version diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5b627cf..ec98f2b 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,4 +1,5 @@ ## Code of Conduct + This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 914e074..6ea00de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,24 +6,23 @@ documentation, we greatly value feedback and contributions from our community. Please read through this document before submitting any issues or pull requests to ensure we have all the necessary information to effectively respond to your bug report or contribution. - ## Reporting Bugs/Feature Requests We welcome you to use the GitHub issue tracker to report bugs or suggest features. -When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already +When filing an issue, please check [existing open](https://github.com/awslabs/horus/issues), or [recently closed](https://github.com/awslabs/horus/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: -* A reproducible test case or series of steps -* The version of our code being used -* Any modifications you've made relevant to the bug -* Anything unusual about your environment or deployment - +- A reproducible test case or series of steps +- The version of our code being used +- Any modifications you've made relevant to the bug +- Anything unusual about your environment or deployment ## Contributing via Pull Requests + Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: -1. You are working against the latest source on the *master* branch. +1. You are working against the latest source on the _master_ branch. 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. @@ -39,23 +38,22 @@ To send us a pull request, please: GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). - ## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/horus/labels/help%20wanted) issues is a great place to start. ## Code of Conduct + This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact opensource-codeofconduct@amazon.com with any additional questions or comments. - ## Security issue notifications -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. +If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. ## Licensing -See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. +See the [LICENSE](https://github.com/awslabs/horus/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 0000000..352e5d3 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,25 @@ +Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). +You may not use this file except in compliance with the License. +A copy of the License is located at + + http://www.apache.org/licenses/LICENSE-2.0 + +or in the "license" file accompanying this file. This file is distributed +on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +express or implied. See the License for the specific language governing +permissions and lithe Massachusetts Institute of Technology (MIT) licenseations under the License. + +********************** +THIRD PARTY COMPONENTS +********************** +This software includes third party software subject to the following copyrights: + +aws-sdk under Apache License 2.0 +aws-cdk under Apache License 2.0 +aws-solutions-constructs under Apache License 2.0 +got under MIT License +moment under MIT License +uuid under MIT License +winston under MIT License \ No newline at end of file diff --git a/README.md b/README.md index 847260c..eb7c2ba 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,178 @@ -## My Project +# AWS Centralized WAF and VPC Security Group Management -TODO: Fill this README out! +The AWS Centralized WAF and VPC Security Group Management solution is intended for customers looking to easily manage consistent security posture across their entire AWS Organization. The solution uses AWS Firewall Manager Service. -Be sure to: +Additionally, solution eases the installation process required to fulfill Firewall Manager prerequisites so customers can focus more on their organization security posture. -* Change the title in this README -* Edit your repository description on GitHub +_Note:_ For any relavant information outside the scope of this readme, please refer to the solution landing page and implementation guide. -## Security +**[🚀Solution Landing Page](https://aws.amazon.com/solutions/implementations/aws-centralized-waf-and-vpc-security-group-management)** | **[🚧Feature request](https://github.com/awslabs/aws-centralized-waf-and-vpc-security-group-management/issues/new?assignees=&labels=feature-request%2C+enhancement&template=feature_request.md&title=)** | **[🐛Bug Report](https://github.com/awslabs/aws-centralized-waf-and-vpc-security-group-management/issues/new?assignees=&labels=bug%2C+triage&template=bug_report.md&title=)** | **[📜Documentation Improvement](https://github.com/awslabs/aws-centralized-waf-and-vpc-security-group-management/issues/new?assignees=&labels=document-update&template=documentation_improvements.md&title=)** -See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. +## Table of content -## License +- [Installation](#installing-pre-packaged-solution-template) + - [Parameters](#parameters-for-prerequisite-template) +- [Customization](#customization) + - [Setup](#setup) + - [Changes](#changes) + - [Unit Test](#unit-test) + - [Build](#build) + - [Deploy](#deploy) +- [Sample Scenario](#sample-scenario) +- [File Structure](#file-structure) +- [License](#license) + +## Installing pre-packaged solution template + +- If you are already using Firewall Manager: [FMSStack.template](https://solutions-reference.s3.amazonaws.com/aws-centralized-waf-and-vpc-sg-management/latest/aws-centralized-waf-and-vpc-security-group-management.template) + +- If you are new to Firewall Manager: [PreReqStack.template](https://solutions-reference.s3.amazonaws.com/aws-centralized-waf-and-vpc-sg-management/latest/aws-fms-prereq.template) + +- If you want to create demo resources: [Demo.template](https://solutions-reference.s3.amazonaws.com/aws-centralized-waf-and-vpc-sg-management/latest/aws-fms-demo.template) + +#### Parameters for prerequisite template + +- **Firewall Admin:** Provide the account-id to be used for Firewall Manager admin account. If you have already configured Firewall Manager admin, provide that account-id. +- **Enable Config:** Do you want to enable AWS Config across your Organization as part of pre requisite installation. You may chose 'No' if you already have Config enabled. + +## Customization + +- Prerequisite: Node.js>10 + +### Setup + +Clone the repository and run the following commands to install dependencies, format and lint as per the project standards + +``` +npm i +npm run prettier-format +npm run lint +``` + +### Changes + +You may make any needed change as per your requirement. If you want to customize the Firewall Manager policy defaults, you can modify the [manifest file](./source/services/policyManager/lib/manifest.json). + +Addtionally, you can customize the code and add any extensibity to the solution. Please review our [feature request guidelines](./.github/ISSUE_TEMPLATE/feature_request.md), if you want to submit a PR. + +### Unit Test + +You can run unit tests with the following command from the root of the project + +``` + npm run test +``` + +### Build + +You can build lambda binaries with the following command from the root of the project + +``` + npm run build +``` -This project is licensed under the Apache-2.0 License. +### Deploy + +Run the following command from the root of the project + +``` +cd source/resources +npm i +``` + +The solution has 3 CDK Stacks + +- Primary FMS Stack: this stack deploys all the primary solution components needed to manage Firewall Manager security policies. **Deploy in Firewall Manager Admin Account** + +``` +cdk synth FMSStack +cdk deploy FMSStack --profile +``` + +- Prerequisite Stack: this stack can be used to fulfill solution prerequisites. **Deploy in Organizations Master Account** + +``` +cdk synth PreReqStack +cdk deploy PreReqStack --parameters FMSAdmin= --parameters EnableConfig= --profile +``` + +- Demo Stack: this stack can be used to provision minimal resources for demo purposes. You may deploy this stack in any account. **Deploy in us-east-1 only** + +``` +cdk synth DemoStack +cdk deploy DemoStack --profile +``` + +_Note:_ for PROFILE_NAME, substitute the name of an AWS CLI profile that contains appropriate credentials for deploying in your preferred region. + +## Sample Scenario + +The default deployment uses opinionated values as setup in [policy manifest file](./source/services/policyManager/lib/manifest.json). In this scenario let's say we want to update the global WAF policies default and turn-off the auto-remediation behavior. We can make the change as seen below and turn **remediationEnabled** to _false_. + +``` + "policyName": "FMS-WAF-01", + "policyScope": "Global", + "resourceType": "AWS::CloudFront::Distribution", + "remediationEnabled": false, +``` + +Additionally, if you want to control sending solution usage metrics to aws-solutions, you can refer to [solution manifest file](./source/resources/lib/manifest.json). + +``` +"solutionVersion": "%%VERSION%%", #provide a valid value eg. v1.0 +"sendMetric": "Yes", +``` + +## File structure + +AWS Centralized WAF & Security Group Management solution consists of: + +- cdk constructs to generate needed resources +- prereq manager to validate and install Firewall Manager prerequisites +- policy manager to install FMS security policies +- metrics manager to publish metrics to aws-solutions + +
+|-deployment/
+  |build-scripts/                 [ build scripts ]
+|-source/
+  |-resources
+    |-bin/
+      |-app.ts                    [ entry point for CDK app ]
+    |-__tests__/                  [ unit tests for CDK constructs ] 
+    |-lib/
+      |-fms.ts                    [ CDK construct for FMS stack and related resources ]
+      |-iam.ts                    [ CDK construct for iam resources]
+      |-prereq.ts                 [ CDK construct for Prerequisite stack and related resources ]  
+      |-manifest.json             [ manifest file for CDK resources ]
+    |-config_files                [ tsconfig, jest.config.js, package.json etc. ]
+  |-services/
+    |-helper/                     [ lambda backed helper custom resource to help with solution launch/update/delete ]
+    |-policyManager/              [ microservice to manage FMS security policies ]
+      |-__tests/                  [ unit tests for all policy managers ]   
+      |-lib/
+        |-clientConfig.json       [ config for AWS service clients ]
+        |-manifest.json           [ manifest file for FMS policy configurations ]
+        |-wafManager.ts           [ class for WAF policy CRUD operations]
+        |-shieldManager.ts        [ class for Shield policy CRUD operations]
+        |-securitygroupManager.ts [ class for Security Group policy CRUD operations]
+        |-fmsHelper.ts            [ helper functions for FMS policy]
+        |-policyManager.ts        [ entry point to process FMS policies]
+      |-index.ts                  [ entry point for lambda function]     
+      |-config_files              [ tsconfig, jest.config.js, package.json etc. ]
+    |-preReqManager
+      |-__tests/                  [ unit tests for pre req manager ] 
+      |-lib/ 
+        |-clientConfig.json       [ config for AWS service clients ]
+        |-preReqManager.ts        [ class for FMS pre-requisites validaion and installation ]
+      |-index.ts                  [ entry point for lambda function]     
+      |-config_files              [ tsconfig, jest.config.js, package.json etc. ]   
+    |-metricsManager
+      |-index.ts                  [ entry point for lambda function]     
+      |-config_files    
+  |-config_files                  [ eslint, prettier, tsconfig, jest.config.js, package.json etc. ]  
+
+ +## License +See license [here](./LICENSE.txt) diff --git a/deployment/add-license-header.sh b/deployment/add-license-header.sh new file mode 100755 index 0000000..23deb1a --- /dev/null +++ b/deployment/add-license-header.sh @@ -0,0 +1,9 @@ +#!/bin/bash +for i in $(find ./open-source/source -type d \( -name node_modules \) -prune -false -o -name '*.ts'); +do + if ! grep -q Copyright $i + then + cat license-header $i >$i.new && mv $i.new $i + fi +done + diff --git a/deployment/build-s3-dist.sh b/deployment/build-s3-dist.sh new file mode 100755 index 0000000..056620d --- /dev/null +++ b/deployment/build-s3-dist.sh @@ -0,0 +1,215 @@ +#!/bin/bash +# +# This assumes all of the OS-level configuration has been completed and git repo has already been cloned +# +# This script should be run from the repo's deployment directory +# cd deployment +# ./build-s3-dist.sh source-bucket-base-name trademarked-solution-name version-code +# +# Arguments: +# - source-bucket-base-name: Name for the S3 bucket location where the template will source the Lambda +# code from. The template will append '-[region_name]' to this bucket name. +# For example: ./build-s3-dist.sh solutions my-solution v1.0.0 +# The template will then expect the source code to be located in the solutions-[region_name] bucket +# +# - trademarked-solution-name: name of the solution for consistency +# +# - version-code: version of the package + +# Check to see if input has been provided: +if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] || [ -z "$4" ]; then + echo "Please provide the base source bucket name, trademark approved solution name and version where the lambda code will eventually reside." + echo "For example: ./build-s3-dist.sh solutions solutions-reference trademarked-solution-name v1.0.0" + exit 1 +fi + +# Get reference for all important folders +template_dir="$PWD" +staging_dist_dir="$template_dir/staging" +template_dist_dir="$template_dir/global-s3-assets" +build_dist_dir="$template_dir/regional-s3-assets" +resource_dir="$template_dir/../source/resources" +source_dir="$template_dir/../source/services" + +echo "------------------------------------------------------------------------------" +echo "[Init] Remove any old dist files from previous runs" +echo "------------------------------------------------------------------------------" +echo "rm -rf $template_dist_dir" +rm -rf $template_dist_dir +echo "mkdir -p $template_dist_dir" +mkdir -p $template_dist_dir +echo "rm -rf $build_dist_dir" +rm -rf $build_dist_dir +echo "mkdir -p $build_dist_dir" +mkdir -p $build_dist_dir +echo "rm -rf $staging_dist_dir" +rm -rf $staging_dist_dir +echo "mkdir -p $staging_dist_dir" +mkdir -p $staging_dist_dir + +echo "------------------------------------------------------------------------------" +echo "[Build] Build typescript microservices" +echo "------------------------------------------------------------------------------" +echo "cd $source_dir" +cd $source_dir + +# build helper function +echo "cd $source_dir/helper" +cd $source_dir/helper +echo "npm run build:all" +npm run build:all + +# build pre-req-manager function +echo "cd $source_dir/helper" +cd $source_dir/preReqManager +echo "npm run build:all" +npm run build:all + +# build policy-manager function +echo "cd $source_dir/policyManager" +cd $source_dir/policyManager +echo "npm run build:all" +npm run build:all + +# build metrics-manager function +echo "cd $source_dir/metricsManager" +cd $source_dir/metricsManager +echo "npm run build:all" +npm run build:all + +echo "------------------------------------------------------------------------------" +echo "[Init] Install dependencies for the cdk-solution-helper" +echo "------------------------------------------------------------------------------" +echo "cd $template_dir/cdk-solution-helper" +cd $template_dir/cdk-solution-helper +echo "npm install" +npm install + +echo "------------------------------------------------------------------------------" +echo "[Synth] CDK Project" +echo "------------------------------------------------------------------------------" +# Install the global aws-cdk package +echo "cd $resource_dir" +cd $resource_dir +echo "npm i" +npm i + +# Run 'cdk synth' to generate raw solution outputs +echo "cdk synth PreReqStack DemoStack--output=$staging_dist_dir" +./node_modules/aws-cdk/bin/cdk synth PreReqStack DemoStack --output=$staging_dist_dir + +# Remove unnecessary output files +echo "cd $staging_dist_dir" +cd $staging_dist_dir +echo "rm tree.json manifest.json cdk.out" +rm tree.json manifest.json cdk.out + +echo "------------------------------------------------------------------------------" +echo "[Packing] Templates" +echo "------------------------------------------------------------------------------" +# Move outputs from staging to template_dist_dir +echo "Move outputs from staging to template_dist_dir" +echo "cp $staging_dist_dir/*.template.json $template_dist_dir/" +cp $staging_dist_dir/*.template.json $template_dist_dir/ +rm *.template.json + +# Rename all *.template.json files to *.template +echo "Rename all *.template.json to *.template" +echo "copy templates and rename" +for f in $template_dist_dir/*.template.json; +do + if [[ $f == *"FMSStack"* ]] + then + mv "$f" "$template_dist_dir/aws-centralized-waf-and-vpc-security-group-management.template" + elif [[ $f == *"PreReqStack"* ]] + then + mv "$f" "$template_dist_dir/aws-fms-prereq.template" + else + mv "$f" "$template_dist_dir/aws-fms-demo.template" + fi +done + +# Run the helper to clean-up the templates and remove unnecessary CDK elements +echo "Run the helper to clean-up the templates and remove unnecessary CDK elements" +echo "node $template_dir/cdk-solution-helper/index" +node $template_dir/cdk-solution-helper/index +if [ "$?" = "1" ]; then + echo "(cdk-solution-helper) ERROR: there is likely output above." 1>&2 + exit 1 +fi + +# Find and replace bucket_name, solution_name, and version +if [[ "$OSTYPE" == "darwin"* ]]; then + # Mac OS + echo "Updating variables in template with $1" + replace="s/%%BUCKET_NAME%%/$1/g" + echo "sed -i '' -e $replace $template_dist_dir/*.template" + sed -i '' -e $replace $template_dist_dir/*.template + replace="s/%%TEMPLATE_BUCKET%%/$2/g" + echo "sed -i '' -e $replace $template_dist_dir/*.template" + sed -i '' -e $replace $template_dist_dir/*.template + replace="s/%%SOLUTION_NAME%%/$3/g" + echo "sed -i '' -e $replace $template_dist_dir/*.template" + sed -i '' -e $replace $template_dist_dir/*.template + replace="s/%%VERSION%%/$4/g" + echo "sed -i '' -e $replace $template_dist_dir/*.template" + sed -i '' -e $replace $template_dist_dir/*.template +else + # Other linux + echo "Updating variables in template with $1" + replace="s/%%BUCKET_NAME%%/$1/g" + echo "sed -i -e $replace $template_dist_dir/*.template" + sed -i -e $replace $template_dist_dir/*.template + replace="s/%%TEMPLATE_BUCKET%%/$2/g" + echo "sed -i -e $replace $template_dist_dir/*.template" + sed -i -e $replace $template_dist_dir/*.template + replace="s/%%SOLUTION_NAME%%/$3/g" + echo "sed -i -e $replace $template_dist_dir/*.template" + sed -i -e $replace $template_dist_dir/*.template + replace="s/%%VERSION%%/$4/g" + echo "sed -i -e $replace $template_dist_dir/*.template" + sed -i -e $replace $template_dist_dir/*.template +fi + +echo "------------------------------------------------------------------------------" +echo "[Packing] Lambdas" +echo "------------------------------------------------------------------------------" +# General cleanup of node_modules and package-lock.json files +echo "find $staging_dist_dir -iname "node_modules" -type d -exec rm -rf "{}" \; 2> /dev/null" +find $staging_dist_dir -iname "node_modules" -type d -exec rm -rf "{}" \; 2> /dev/null +echo "find $staging_dist_dir -iname "package-lock.json" -type f -exec rm -f "{}" \; 2> /dev/null" +find $staging_dist_dir -iname "package-lock.json" -type f -exec rm -f "{}" \; 2> /dev/null + +# ... For each asset.* source code artifact in the temporary /staging folder... +cd $staging_dist_dir +for i in `find . -mindepth 1 -maxdepth 1 -type f \( -iname "*.zip" \) -or -type d`; do + + # Rename the artifact, removing the period for handler compatibility + pfname="$(basename -- $i)" + fname="$(echo $pfname | sed -e 's/\.//')" + mv $i $fname + + if [[ $fname != *".zip" ]] + then + # Zip the artifact + echo "zip -r $fname.zip $fname/*" + zip -r $fname.zip $fname + fi + +# ... repeat until all source code artifacts are zipped +done + +# Copy the zipped artifact from /staging to /regional-s3-assets +echo "cp -R *.zip $build_dist_dir" +cp -R *.zip $build_dist_dir + +# Remove the old, unzipped artifact from /staging +echo "rm -rf *.zip" +rm -rf *.zip + +echo "------------------------------------------------------------------------------" +echo "[Cleanup] Remove temporary files" +echo "------------------------------------------------------------------------------" +# Delete the temporary /staging folder +echo "rm -rf $staging_dist_dir" +rm -rf $staging_dist_dir \ No newline at end of file diff --git a/deployment/license-header b/deployment/license-header new file mode 100644 index 0000000..d2fa6d8 --- /dev/null +++ b/deployment/license-header @@ -0,0 +1,15 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + + + \ No newline at end of file diff --git a/deployment/run-unit-tests.sh b/deployment/run-unit-tests.sh new file mode 100755 index 0000000..c5bd43f --- /dev/null +++ b/deployment/run-unit-tests.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# +# This assumes all of the OS-level configuration has been completed and git repo has already been cloned +# +# This script should be run from the repo's deployment directory +# cd deployment +# ./run-unit-tests.sh +# + +# Get reference for all important folders +template_dir="$PWD" +resource_dir="$template_dir/../source/resources" +source_dir="$template_dir/../source/services" + +echo "------------------------------------------------------------------------------" +echo "[Test] Resources" +echo "------------------------------------------------------------------------------" +cd $resource_dir +npm run test -- -u + +echo "------------------------------------------------------------------------------" +echo "[Test] pre-req-manager" +echo "------------------------------------------------------------------------------" +cd $source_dir/preReqManager +npm run test + +echo "------------------------------------------------------------------------------" +echo "[Test] policy-manager" +echo "------------------------------------------------------------------------------" +cd $source_dir/policyManager +npm run test + + +echo "------------------------------------------------------------------------------" +echo "[Test] helper" +echo "------------------------------------------------------------------------------" +cd $source_dir/helper/ +npm run test \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..1a93548 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "aws-centralized-waf-and-sg-management", + "version": "1.0.0", + "description": "AWS Centralized WAF and VPC Security Group Management", + "scripts": { + "docs": "./node_modules/typedoc/bin/typedoc --out docs --name \"AWS Centralized WAF & Security Group Management\"", + "lint": "eslint . --ext .ts", + "prettier-format": "./node_modules/prettier/bin-prettier.js --config .prettierrc.yml '**/*.ts' --write", + "build:helper": "cd source/services/helper && npm run build:all", + "build:policyManager": "cd source/services/policyManager && npm run build:all", + "build:prereqManager": "cd source/services/preReqManager && npm run build:all", + "build:metricsManager": "cd source/services/metricsManager && npm run build:all", + "build": "npm run build:helper && npm run build:policyManager && npm run build:prereqManager && npm run build:metricsManager", + "test": "cd ./deployment && chmod +x run-unit-tests.sh && ./run-unit-tests.sh" + }, + "author": "aws-solutions", + "license": "Apache-2.0", + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^3.9.1", + "@typescript-eslint/parser": "^3.9.1", + "eslint": "^7.7.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-prettier": "^3.1.4", + "prettier": "^2.0.5", + "typedoc": "^0.18.0", + "typedoc-plugin-no-inherit": "^1.1.10", + "typescript": "^4.0.2" + } +} diff --git a/source/resources/README.md b/source/resources/README.md new file mode 100644 index 0000000..1ee1609 --- /dev/null +++ b/source/resources/README.md @@ -0,0 +1,12 @@ +# AWS Centralized WAF & Security Group Management CDK Project + +The `cdk.json` file tells the CDK Toolkit how to execute your app. + +## Useful commands + +- `npm run build` compile typescript to js +- `npm run watch` watch for changes and compile +- `npm run test` perform the jest unit tests +- `cdk deploy` deploy this stack to your default AWS account/region +- `cdk diff` compare deployed stack with current state +- `cdk synth` emits the synthesized CloudFormation template diff --git a/source/resources/__tests__/fms.test.ts b/source/resources/__tests__/fms.test.ts new file mode 100644 index 0000000..fb63233 --- /dev/null +++ b/source/resources/__tests__/fms.test.ts @@ -0,0 +1,90 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import "@aws-cdk/assert/jest"; +import { + objectLike, + arrayWith, + anything, +} from "@aws-cdk/assert/lib/assertions/have-resource-matchers"; +import { FMSStack } from "../lib/fms"; +import { Stack } from "@aws-cdk/core"; + +describe("==FMS Stack Tests==", () => { + const mstack = new Stack(); + const stack: Stack = new FMSStack(mstack, "FMSStack"); + + describe("Test resources", () => { + test("snapshot test", () => { + expect(stack).toMatchSnapshot(); + }); + test("has 3 SSM paramters for region, OUs, tags", () => { + expect(stack).toCountResources("AWS::SSM::Parameter", 3); + }); + test("has lambda with dead letter queue", () => { + expect(stack).toHaveResource("AWS::SQS::Queue"); + expect(stack).toHaveResourceLike("AWS::Lambda::Function", { + DeadLetterConfig: objectLike({ TargetArn: objectLike(anything) }), + }); + }); + test("has events rule for ssm parameter change", () => { + expect(stack).toHaveResourceLike("AWS::Events::Rule", { + EventPattern: objectLike({ + source: arrayWith("aws.ssm"), + "detail-type": arrayWith("Parameter Store Change"), + }), + }); + }); + test("has policy manager lambda function", () => { + expect(stack).toHaveResource("AWS::Lambda::Function", { + Runtime: "nodejs12.x", + }); + }); + test("has policy dynamodb table", () => { + expect(stack).toHaveResource("AWS::DynamoDB::Table", { + SSESpecification: { + SSEEnabled: true, + }, + }); + }); + test("has dynamodb table with given schema", () => { + expect(stack).toHaveResource("AWS::DynamoDB::Table", { + KeySchema: [ + { + AttributeName: "PolicyName", + KeyType: "HASH", + }, + { + AttributeName: "Region", + KeyType: "RANGE", + }, + ], + AttributeDefinitions: [ + { + AttributeName: "PolicyName", + AttributeType: "S", + }, + { + AttributeName: "Region", + AttributeType: "S", + }, + ], + }); + }); + test("has cloudwatch log group", () => { + expect(stack).toHaveResource("AWS::Logs::LogGroup", { + RetentionInDays: 7, + }); + }); + }); +}); diff --git a/source/resources/__tests__/prereq.test.ts b/source/resources/__tests__/prereq.test.ts new file mode 100644 index 0000000..d6285ce --- /dev/null +++ b/source/resources/__tests__/prereq.test.ts @@ -0,0 +1,51 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import "@aws-cdk/assert/jest"; +import { PreReqStack } from "../lib/prereq"; +import { App } from "@aws-cdk/core"; + +describe("==Master Stack Tests==", () => { + const app = new App(); + const stack = new PreReqStack(app, "MasterStack"); + + describe("Test resources", () => { + test("snapshot test", () => { + expect(stack).toMatchSnapshot(); + }); + test("has helper, pre-req and provider lambda functions", () => { + expect(stack).toCountResources("AWS::Lambda::Function", 4); + }); + test("has helper & pre-req lambda function", () => { + expect(stack).toHaveResourceLike("AWS::Lambda::Function", { + Runtime: "nodejs12.x", + }); + }); + test("has custom resource for launch", () => { + expect(stack).toHaveResource("Custom::LaunchData"); + }); + test("has custom resource for UUID", () => { + expect(stack).toHaveResource("Custom::CreateUUID"); + }); + test("has custom resource for UUID", () => { + expect(stack).toHaveResource("Custom::PreReqChecker"); + }); + test("has output for UUID", () => { + expect(stack).toHaveOutput({ + outputName: "UUID", + outputValue: { + "Fn::GetAtt": ["CreateUUID", "UUID"], + }, + }); + }); + }); +}); diff --git a/source/resources/bin/app.ts b/source/resources/bin/app.ts new file mode 100644 index 0000000..f0e27fc --- /dev/null +++ b/source/resources/bin/app.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import * as cdk from "@aws-cdk/core"; +import { PreReqStack } from "../lib/prereq"; +import { DemoStack } from "../lib/demo"; +import { FMSStack } from "../lib/fms"; +const app = new cdk.App(); + +// Prerequisite Stack with nested FMS Stack +new PreReqStack(app, "PreReqStack"); + +// FMS Stack +const stack = new cdk.Stack(app, "FMSStack"); +new FMSStack(stack, "NestedFMSStack"); + +// Demo Stack +new DemoStack(app, "DemoStack"); diff --git a/source/resources/cdk.json b/source/resources/cdk.json new file mode 100644 index 0000000..97bafc2 --- /dev/null +++ b/source/resources/cdk.json @@ -0,0 +1,7 @@ +{ + "app": "npx ts-node bin/app.ts", + "context": { + "@aws-cdk/core:enableStackNameDuplicates": "true", + "aws-cdk:enableDiffNoFail": "true" + } +} diff --git a/source/resources/jest.config.js b/source/resources/jest.config.js new file mode 100644 index 0000000..11008c2 --- /dev/null +++ b/source/resources/jest.config.js @@ -0,0 +1,46 @@ +module.exports = { + // Automatically clear mock calls and instances between every test + clearMocks: true, + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of regexp pattern strings used to skip coverage collection + coveragePathIgnorePatterns: ["/node_modules/"], + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + // branches: 80, + functions: 80, + lines: 80, + statements: 80, + }, + }, + + // An array of directory names to be searched recursively up from the requiring module's location + moduleDirectories: ["node_modules"], + + // An array of file extensions your modules use + moduleFileExtensions: ["ts", "json", "jsx", "js", "tsx", "node"], + + // Automatically reset mock state between every test + resetMocks: true, + + // The glob patterns Jest uses to detect test files + testMatch: ["**/?(*.)+(spec|test).[t]s?(x)"], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + testPathIgnorePatterns: ["/node_modules/"], + + // A map from regular expressions to paths to transformers + transform: { + "^.+\\.(t|j)sx?$": "ts-jest", + }, + + // Indicates whether each individual test should be reported during the run + verbose: true, + + // This option allows the use of a custom results processor. + testResultsProcessor: "jest-sonar-reporter", +}; diff --git a/source/resources/lib/demo.ts b/source/resources/lib/demo.ts new file mode 100644 index 0000000..4920e8e --- /dev/null +++ b/source/resources/lib/demo.ts @@ -0,0 +1,113 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/** + * @description + * This is Firewall Manager Demo construct + * minimal resources for demo purpose ONLY + * @author @aws-solutions + */ + +import { App, CfnResource, Stack } from "@aws-cdk/core"; +import { Vpc, SecurityGroup, Peer, Port } from "@aws-cdk/aws-ec2"; +import manifest from "./manifest.json"; +const { + CloudFrontToS3, +} = require("@aws-solutions-constructs/aws-cloudfront-s3"); + +export class DemoStack extends Stack { + constructor(scope: App, id: string) { + super(scope, id); + + //============================================================================================= + // Metadata + //============================================================================================= + this.templateOptions.description = `(${manifest.demoSolutionId}) - The AWS CloudFormation template for deployment of the ${manifest.solutionName} demo resources. Version ${manifest.solutionVersion}`; + this.templateOptions.templateFormatVersion = manifest.templateVersion; + + //============================================================================================= + // Resources + //============================================================================================= + /** + * CloudFront - S3 resource + */ + new CloudFrontToS3(this, "test-cloudfront-s3", {}); + + /** + * Security Groups + */ + const vpc = new Vpc(this, "test-VPC", { + cidr: "10.0.0.0/16", + }); + + vpc.publicSubnets.forEach((s) => { + const cfnSubnet = s.node.defaultChild as CfnResource; + cfnSubnet.addPropertyOverride("MapPublicIpOnLaunch", false); + }); + + const sg = new SecurityGroup(this, "test-vpc-sg", { + vpc: vpc, + allowAllOutbound: true, + }); + sg.addIngressRule(Peer.anyIpv4(), Port.allTcp()); + + //============================================================================================= + // cfn_nag suppress rules + //============================================================================================= + const sgSuppress = sg.node.findChild("Resource") as CfnResource; + const vpcSuppress = vpc.node.findChild("Resource") as CfnResource; + sgSuppress.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: "W40", + reason: + "Demo template, need resources to trigger violation rules in the account", + }, + { + id: "W5", + reason: + "Demo template, need resources to trigger violation rules in the account", + }, + { + id: "W9", + reason: + "Demo template, need resources to trigger violation rules in the account", + }, + { + id: "W2", + reason: + "Demo template, need resources to trigger violation rules in the account", + }, + { + id: "W27", + reason: + "Demo template, need resources to trigger violation rules in the account", + }, + ], + }, + }; + + vpcSuppress.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: "W60", + reason: + "Demo template, need resources to trigger violation rules in the account", + }, + ], + }, + }; + } +} diff --git a/source/resources/lib/fms.ts b/source/resources/lib/fms.ts new file mode 100644 index 0000000..3e5a764 --- /dev/null +++ b/source/resources/lib/fms.ts @@ -0,0 +1,471 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/** + * @description + * This is Firewall Manager Stack for AWS Centralized WAF & Security Group Automations + * The stack should be deployed in Firewall Manager admin account + * @author @aws-solutions + */ + +import { + Stack, + Construct, + CfnMapping, + RemovalPolicy, + Duration, + NestedStack, + NestedStackProps, + CfnOutput, + CustomResource, +} from "@aws-cdk/core"; +import { Provider } from "@aws-cdk/custom-resources"; +import { StringListParameter, StringParameter } from "@aws-cdk/aws-ssm"; +import { Queue, QueueEncryption } from "@aws-cdk/aws-sqs"; +import { Table, AttributeType, BillingMode } from "@aws-cdk/aws-dynamodb"; +import { Code, Runtime, Function, CfnFunction } from "@aws-cdk/aws-lambda"; +import { SqsEventSource } from "@aws-cdk/aws-lambda-event-sources"; +import { LogGroup, RetentionDays } from "@aws-cdk/aws-logs"; +import { IAMConstruct } from "./iam"; +import manifest from "./manifest.json"; +import { CfnPolicy, Effect, Policy, PolicyStatement } from "@aws-cdk/aws-iam"; +const { + LambdaToDynamoDB, +} = require("@aws-solutions-constructs/aws-lambda-dynamodb"); +const { + EventsRuleToLambda, +} = require("@aws-solutions-constructs/aws-events-rule-lambda"); + +enum LogLevel { + ERROR = "error", + WARN = "warn", + INFO = "info", + DEBUG = "debug", +} + +export class FMSStack extends NestedStack { + readonly account: string; + readonly region: string; + /** + * @constructor + * @param {cdk.Construct} scope - parent of the construct + * @param {string} id - identifier for the object + */ + constructor(scope: Construct, id: string, props?: NestedStackProps) { + super(scope, id, props); + const stack = Stack.of(this); + + this.account = stack.account; // Returns the AWS::AccountId for this stack (or the literal value if known) + this.region = stack.region; // Returns the AWS::Region for this stack (or the literal value if known) + + //============================================================================================= + // Metadata + //============================================================================================= + this.templateOptions.description = `(${manifest.primarySolutionId}) - The AWS CloudFormation template for deployment of the ${manifest.solutionName}. Version ${manifest.solutionVersion}`; + this.templateOptions.templateFormatVersion = manifest.templateVersion; + + //============================================================================================= + // Map + //============================================================================================= + const map = new CfnMapping(this, "FMSStackMap", { + mapping: { + SSMParameters: { + Region: manifest.ssmParameters.Region, + OUs: manifest.ssmParameters.OUs, + Tags: manifest.ssmParameters.Tags, + }, + Metric: { + SendAnonymousMetric: manifest.sendMetric, + MetricsEndpoint: manifest.metricsEndpoint, // aws-solutions metrics endpoint + }, + Solution: { + SolutionId: manifest.primarySolutionId, + SolutionVersion: manifest.solutionVersion, + }, + }, + }); + + //============================================================================================= + // Resources + //============================================================================================= + /** + * @description lambda backed custom resource to validate and install pre-reqs + * @type {Function} + */ + const helperFunction: Function = new Function(this, "FMSHelperFunction", { + description: "DO NOT DELETE - FMS helper function", + runtime: Runtime.NODEJS_12_X, + code: Code.fromAsset( + "../../source/services/helper/dist/helperFunction.zip" + ), + handler: "index.handler", + memorySize: 512, + environment: { + METRICS_ENDPOINT: map.findInMap("Metric", "MetricsEndpoint"), + SEND_METRIC: map.findInMap("Metric", "SendAnonymousMetric"), + LOG_LEVEL: LogLevel.INFO, //change as needed + }, + }); + + /** + * @description custom resource for helper functions + * @type {Provider} + */ + const helperProvider: Provider = new Provider(this, "helperProvider", { + onEventHandler: helperFunction, + }); + + /** + * Get UUID for deployment + */ + const uuid = new CustomResource(this, "CreateUUID", { + resourceType: "Custom::CreateUUID", + serviceToken: helperProvider.serviceToken, + }); + + /** + * Check deployment account + */ + new CustomResource(this, "FMSAdminCheck", { + resourceType: "Custom::FMSAdminCheck", + serviceToken: helperProvider.serviceToken, + properties: { + Stack: "FMSStack", + Account: this.account, + Region: this.region, + }, + }); + + /** + * Send launch data to aws-solutions + */ + new CustomResource(this, "LaunchData", { + resourceType: "Custom::LaunchData", + serviceToken: helperProvider.serviceToken, + properties: { + SolutionId: map.findInMap("Solution", "SolutionId"), + SolutionVersion: map.findInMap("Solution", "SolutionVersion"), + SolutionUuid: uuid.getAttString("UUID"), + Stack: "FMSStack", + }, + }); + + /** + * @description - ssm parameter for org units + * @type {StringListParameter} + */ + const ou: StringListParameter = new StringListParameter(this, "FMSOUs", { + description: "FMS parameter store for OUs", + stringListValue: ["NOP"], + parameterName: map.findInMap("SSMParameters", "OUs"), + simpleName: false, + }); + + /** + * @description ssm parameter for tags + * @type {StringParameter} + */ + const tags: StringParameter = new StringParameter(this, "FMSTags", { + description: "fms parameter for fms tags", + parameterName: map.findInMap("SSMParameters", "Tags"), + stringValue: "NOP", + simpleName: false, + }); + + /** + * @description ssm parameter for regions + * @type {StringListParameter} + */ + const regions: StringListParameter = new StringListParameter( + this, + "FMSRegions", + { + description: "fms parameter for fms regions", + parameterName: map.findInMap("SSMParameters", "Region"), + stringListValue: ["NOP"], + simpleName: false, + } + ); + + /** + * @description dynamodb table for policy items + * @type {Table} + */ + const table: Table = new Table(this, "FMSTable", { + partitionKey: { + name: "PolicyName", + type: AttributeType.STRING, + }, + sortKey: { + name: "Region", + type: AttributeType.STRING, + }, + serverSideEncryption: true, + removalPolicy: RemovalPolicy.DESTROY, + billingMode: BillingMode.PAY_PER_REQUEST, + }); + + /** + * @description dead letter queue for lambda + * @type {Queue} + */ + const dlq: Queue = new Queue(this, `dlq`, { + encryption: QueueEncryption.KMS_MANAGED, + }); + + /** + * @description dead letter queue for lambda + * @type {Queue} + */ + const metricsQueue: Queue = new Queue(this, `metricsQueue`, { + encryption: QueueEncryption.KMS_MANAGED, + }); + + /** + * @description lambda function to create FMS security policy + * @type {Function} + */ + const policyManager: Function = new Function(this, "policyManager", { + description: + "Function to create/update/delete FMS security policies for the FMS solution", + runtime: Runtime.NODEJS_12_X, + code: Code.fromAsset( + "../../source/services/policyManager/dist/policyManager.zip" + ), + handler: "index.handler", + deadLetterQueue: dlq, + retryAttempts: 0, + maxEventAge: Duration.minutes(15), + deadLetterQueueEnabled: true, + memorySize: 512, + environment: { + FMS_OU: ou.parameterName, + FMS_TAGS: tags.parameterName, + FMS_REGIONS: regions.parameterName, + FMS_TABLE: table.tableName, + SEND_METRIC: map.findInMap("Metric", "SendAnonymousMetric"), + LOG_LEVEL: LogLevel.INFO, //change as needed + SOLUTION_ID: map.findInMap("Solution", "SolutionId"), + SOLUTION_VERSION: map.findInMap("Solution", "SolutionVersion"), + UUID: uuid.getAttString("UUID"), + METRICS_QUEUE: metricsQueue.queueUrl, + }, + timeout: Duration.minutes(15), + }); + + /** + * @description lambda function to publish metrics + * @type {Function} + */ + + const metricsManager: Function = new Function(this, "metricsManager", { + description: "Function to publish policy metrics to aws-solutions", + runtime: Runtime.NODEJS_12_X, + code: Code.fromAsset( + "../../source/services/metricsManager/dist/metricsManager.zip" + ), + handler: "index.handler", + memorySize: 128, + reservedConcurrentExecutions: 1, + environment: { + METRICS_ENDPOINT: map.findInMap("Metric", "MetricsEndpoint"), + LOG_LEVEL: LogLevel.INFO, //change as needed + }, + timeout: Duration.seconds(15), + }); + metricsManager.addEventSource( + new SqsEventSource(metricsQueue, { + batchSize: 1, + }) + ); + + /** + * @description Events rule to Lambda construct pattern + * @example ` + * { + "source": [ + "aws.ssm" + ], + "detail-type": [ + "Parameter Store Change" + ], + "resources": [ + "arn:aws:ssm:::parameter", + "arn:aws:ssm:::parameter", + ] + } + */ + new EventsRuleToLambda(this, "EventsRuleLambda", { + existingLambdaObj: policyManager, + eventRuleProps: { + ruleName: "FMSPolicyRule", + eventPattern: { + source: ["aws.ssm"], + detailType: ["Parameter Store Change"], + resources: [ + `${ou.parameterArn}`, + `${tags.parameterArn}`, + `${regions.parameterArn}`, + ], + }, + }, + }); + + /** + * Lambda to DynamoDB constuct pattern + */ + new LambdaToDynamoDB(this, "LambdaDDB", { + existingLambdaObj: policyManager, + existingTableObj: table, + tablePermissions: "ReadWrite", + }); + + /** + * @description log group for policy manager lambda function + * @type {LogGroup} + */ + const lg: LogGroup = new LogGroup(this, "PolicyMangerLogGroup", { + logGroupName: `/aws/lambda/${policyManager.functionName}`, + removalPolicy: RemovalPolicy.DESTROY, + retention: RetentionDays.ONE_WEEK, + }); + + /** + * @description iam permissions for the helper lambda function + * @type {Policy} + */ + const po: Policy = new Policy(this, "helperPolicy", { + policyName: manifest.helperPolicy, + roles: [helperFunction.role!], + }); + + /** + * @description iam policy for the helper lambda function + * @type {PolicyStatement} + */ + const po0: PolicyStatement = new PolicyStatement({ + effect: Effect.ALLOW, + sid: "VisualEditor1", + actions: ["fms:GetAdminAccount"], + resources: ["*"], + }); + po.addStatements(po0); + + /** + * @description iam permissions for the policy manager lambda function + * @type {IAMConstruct} + */ + new IAMConstruct(this, "LambdaIAM", { + dynamodb: table.tableArn, + sqs: dlq.queueArn, + logGroup: lg.logGroupArn, + role: policyManager.role!, + accountId: this.account, + region: this.region, + metricsQueue: metricsQueue.queueArn, + }); + + //============================================================================================= + // cfn_nag suppress rules + //============================================================================================= + const prRole = po.node.findChild("Resource") as CfnPolicy; + prRole.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: "W12", + reason: + "Resource * is required for IAM actions that do not support resource level permissions", + }, + ], + }, + }; + + const mm = metricsManager.node.findChild("Resource") as CfnFunction; + mm.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: "W58", + reason: + "CloudWatch logs write permissions added with managed role AWSLambdaBasicExecutionRole", + }, + ], + }, + }; + + const pm = policyManager.node.findChild("Resource") as CfnFunction; + pm.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: "W58", + reason: + "CloudWatch logs write permissions added with managed role AWSLambdaBasicExecutionRole", + }, + ], + }, + }; + + const hF = helperFunction.node.findChild("Resource") as CfnFunction; + hF.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: "W58", + reason: + "CloudWatch logs write permissions added with managed role AWSLambdaBasicExecutionRole", + }, + ], + }, + }; + + const hpP = helperProvider.node.children[0].node.findChild( + "Resource" + ) as CfnFunction; + hpP.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: "W58", + reason: + "CloudWatch logs write permissions added with managed role AWSLambdaBasicExecutionRole", + }, + ], + }, + }; + + //============================================================================================= + // Output + //============================================================================================= + new CfnOutput(this, "OU Parameter", { + description: "SSM Parameter for OUs", + value: map.findInMap("SSMParameters", "OUs"), + }); + + new CfnOutput(this, "Region Parameter", { + description: "SSM Parameter for Regions", + value: map.findInMap("SSMParameters", "Region"), + }); + + new CfnOutput(this, "Tag Parameter", { + description: "SSM Parameter for Tags", + value: map.findInMap("SSMParameters", "Tags"), + }); + + new CfnOutput(this, "UUID", { + description: "UUID for FMS Stack", + value: uuid.getAttString("UUID"), + }); + } +} diff --git a/source/resources/lib/iam.ts b/source/resources/lib/iam.ts new file mode 100644 index 0000000..6ea3c95 --- /dev/null +++ b/source/resources/lib/iam.ts @@ -0,0 +1,176 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/** + * @description + * This is construct for supplementary IAM resources + * @author @aws-solutions + */ +import { Construct } from "@aws-cdk/core"; +import { + IRole, + Policy, + PolicyStatement, + Effect, + CfnPolicy, +} from "@aws-cdk/aws-iam"; +import manifest from "./manifest.json"; + +interface IIam { + dynamodb: string; + logGroup: string; + sqs: string; + role: IRole; + accountId: string; + region: string; + metricsQueue: string; +} + +export class IAMConstruct extends Construct { + readonly dynamodb: string; + readonly sqs: string; + readonly logGroup: string; + readonly role: IRole; + readonly accountId: string; + readonly region: string; + readonly metricsQueue: string; + constructor(scope: Construct, id: string, props: IIam) { + super(scope, id); + + this.dynamodb = props.dynamodb; + this.logGroup = props.logGroup; + this.sqs = props.sqs; + this.role = props.role; + this.accountId = props.accountId; + this.region = props.region; + this.metricsQueue = props.metricsQueue; + + /** + * @description iam policy for lambda role + * @type {iam.Policy} + */ + const po: Policy = new Policy(this, "policyManagerPolicy", { + policyName: manifest.managerPolicy, + roles: [this.role], + }); + + /** + * @description iam policy statement for general permissions + * @type {PolicyStatement} + */ + const po0: PolicyStatement = new PolicyStatement({ + effect: Effect.ALLOW, + sid: "FMSEC2Read0", + actions: [ + "ec2:DescribeRegions", + "wafv2:*", + "shield:GetSubscriptionState", + ], + resources: ["*"], + }); + + /** + * @description iam policy statement for dynamodb permissions + * @type {PolicyStatement} + */ + const po1: PolicyStatement = new PolicyStatement({ + effect: Effect.ALLOW, + sid: "FMSDDBWrite01", + actions: [ + "dynamodb:GetItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + ], + resources: [this.dynamodb], + }); + + /** + * @description iam policy statement for firewall manager permissions + * @type {PolicyStatement} + */ + const po2: PolicyStatement = new PolicyStatement({ + effect: Effect.ALLOW, + sid: "FMSSecurityPolicyWrite02", + actions: ["fms:PutPolicy", "fms:DeletePolicy"], + resources: ["arn:aws:fms:*:*:policy/*"], + }); + + /** + * @description iam policy statement for CloudWatch logs + * @type {PolicyStatement} + */ + const po3: PolicyStatement = new PolicyStatement({ + effect: Effect.ALLOW, + sid: "FMSCloudWatchLogsWrite03", + actions: [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:CreateLogGroup", + ], + resources: [this.logGroup], + }); + + /** + * @description iam policy statement for sqs permissions + * @type {iam.PolicyStatement} + */ + const po4: PolicyStatement = new PolicyStatement({ + effect: Effect.ALLOW, + sid: "FMSSQSWrite04", + actions: ["sqs:SendMessage"], + resources: [this.sqs, this.metricsQueue], + }); + + /** + * @description iam policy statement for SSM parameter + * @type {PolicyStatement} + */ + const po5: PolicyStatement = new PolicyStatement({ + effect: Effect.ALLOW, + sid: "FMSSecurityPolicyRead05", + actions: ["ssm:GetParameter"], + resources: [ + `arn:aws:ssm:${this.region}:${this.accountId}:parameter${manifest.ssmParameters.OUs}`, + `arn:aws:ssm:${this.region}:${this.accountId}:parameter${manifest.ssmParameters.Region}`, + `arn:aws:ssm:${this.region}:${this.accountId}:parameter${manifest.ssmParameters.Tags}`, + ], + }); + + po.addStatements(po0); + po.addStatements(po1); + po.addStatements(po2); + po.addStatements(po3); + po.addStatements(po4); + po.addStatements(po5); + + /** + * cfn_nag suppress rules + */ + const pm = po.node.findChild("Resource") as CfnPolicy; + pm.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: "W12", + reason: + "* needed for ec2:DescribeRegions, does no support resource level permissions", + }, + { + id: "F4", + reason: "Read & Write permissions needed to create WAFv2 policies", + }, + ], + }, + }; + } +} diff --git a/source/resources/lib/manifest.json b/source/resources/lib/manifest.json new file mode 100644 index 0000000..87f8ac8 --- /dev/null +++ b/source/resources/lib/manifest.json @@ -0,0 +1,20 @@ +{ + "primarySolutionId": "SO0134", + "secondarySolutionId": "SO0134N", + "demoSolutionId": "SO0134D", + "solutionVersion": "%%VERSION%%", + "metricsEndpoint": "https://metrics.awssolutionsbuilder.com/generic", + "solutionName": "%%SOLUTION_NAME%%", + "templateVersion": "2010-09-09", + "ssmParameters": { + "Region": "/FMS/Regions", + "OUs": "/FMS/OUs", + "Tags": "/FMS/Tags" + }, + "sendMetric": "Yes", + "globalStackSetName": "FMS-EnableConfig-Global", + "regionalStackSetName": "FMS-EnableConfig-Regional", + "managerPolicy": "FMS-PolicyManager-Policy", + "prereqPolicy": "FMS-PreReqManager-Policy", + "helperPolicy": "FMS-Helper-Policy" +} diff --git a/source/resources/lib/prereq.ts b/source/resources/lib/prereq.ts new file mode 100644 index 0000000..9c5b8ad --- /dev/null +++ b/source/resources/lib/prereq.ts @@ -0,0 +1,365 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +/** + * @description + * This is Master Stack for AWS Centralized WAF & Security Group Automations + * The stack should be deployed in Organization master account + * @author @aws-solutions + */ + +import { + Stack, + App, + StackProps, + CfnParameter, + CustomResource, + CfnMapping, + CfnOutput, + Duration, + CfnCondition, + Fn, + NestedStack, + CfnResource, +} from "@aws-cdk/core"; +import { Provider } from "@aws-cdk/custom-resources"; +import { Policy, Effect, PolicyStatement, CfnPolicy } from "@aws-cdk/aws-iam"; +import { Code, Runtime, Function, CfnFunction } from "@aws-cdk/aws-lambda"; +import { FMSStack } from "./fms"; +import manifest from "./manifest.json"; + +enum LogLevel { + ERROR = "error", + WARN = "warn", + INFO = "info", + DEBUG = "debug", +} + +export class PreReqStack extends Stack { + readonly account: string; + readonly region: string; + + /** + * @constructor + * @param {cdk.Construct} scope - parent of the construct + * @param {string} id - identifier for the object + */ + constructor(scope: App, id: string, props?: StackProps) { + super(scope, id, props); + const stack = Stack.of(this); + + this.account = stack.account; // Returns the AWS::AccountId for this stack (or the literal value if known) + this.region = stack.region; // Returns the AWS::Region for this stack (or the literal value if known) + + //============================================================================================= + // Parameters + //============================================================================================= + const fmsAdmin = new CfnParameter(this, "FMSAdmin", { + description: "AWS Account Id for Firewall Manager admin account", + type: "String", + allowedPattern: "^[0-9]{1}\\d{11}$", + }); + + const enableConfig = new CfnParameter(this, "EnableConfig", { + description: + "Do you want to enable AWS Config across your AWS Organization? You may chose 'No' if you are already using Config", + type: "String", + allowedValues: ["Yes", "No"], + default: "Yes", + }); + + //============================================================================================= + // Metadata + //============================================================================================= + this.templateOptions.metadata = { + "AWS::CloudFormation::Interface": { + ParameterGroups: [ + { + Label: { default: "Pre-Requisite Configuration" }, + Parameters: [fmsAdmin.logicalId, enableConfig.logicalId], + }, + ], + ParameterLabels: { + [fmsAdmin.logicalId]: { + default: "FMS Admin Account", + }, + [enableConfig.logicalId]: { + default: "Enable Config", + }, + }, + }, + }; + this.templateOptions.description = `(${manifest.secondarySolutionId}) - The AWS CloudFormation template for deployment of the ${manifest.solutionName}. Version ${manifest.solutionVersion}`; + this.templateOptions.templateFormatVersion = manifest.templateVersion; + + //============================================================================================= + // Map + //============================================================================================= + const map = new CfnMapping(this, "FMSMap", { + mapping: { + Metric: { + SendAnonymousMetric: manifest.sendMetric, + MetricsEndpoint: manifest.metricsEndpoint, // aws-solutions metrics endpoint + }, + Solution: { + SolutionId: manifest.secondarySolutionId, + SolutionVersion: manifest.solutionVersion, + GlobalStackSetName: manifest.globalStackSetName, + RegionalStackSetName: manifest.regionalStackSetName, + }, + }, + }); + + //============================================================================================= + // Condition + //============================================================================================= + const accountCheck = new CfnCondition(this, "accountCheck", { + expression: Fn.conditionEquals(fmsAdmin.valueAsString, this.account), + }); + + //============================================================================================= + // Resources + //============================================================================================= + /** + * @description lambda backed custom resource to validate and install pre-reqs + * @type {Function} + */ + const helperFunction: Function = new Function(this, "FMSHelperFunction", { + description: "DO NOT DELETE - FMS helper function", + runtime: Runtime.NODEJS_12_X, + code: Code.fromAsset( + "../../source/services/helper/dist/helperFunction.zip" + ), + handler: "index.handler", + memorySize: 512, + environment: { + METRICS_ENDPOINT: map.findInMap("Metric", "MetricsEndpoint"), + SEND_METRIC: map.findInMap("Metric", "SendAnonymousMetric"), + LOG_LEVEL: LogLevel.INFO, //change as needed + }, + }); + + /** + * @description custom resource for helper functions + * @type {Provider} + */ + const helperProvider: Provider = new Provider(this, "helperProvider", { + onEventHandler: helperFunction, + }); + + /** + * Get UUID for deployment + */ + const uuid = new CustomResource(this, "CreateUUID", { + resourceType: "Custom::CreateUUID", + serviceToken: helperProvider.serviceToken, + }); + + /** + * Send launch data to aws-solutions + */ + new CustomResource(this, "LaunchData", { + resourceType: "Custom::LaunchData", + serviceToken: helperProvider.serviceToken, + properties: { + SolutionId: map.findInMap("Solution", "SolutionId"), + SolutionVersion: map.findInMap("Solution", "SolutionVersion"), + SolutionUuid: uuid.getAttString("UUID"), + Stack: "PreReqStack", + }, + }); + + /** + * @description lambda backed custom resource to validate and install pre-reqs + * @type {Function} + */ + const preReqManager: Function = new Function(this, "preReqManager", { + description: + "Function to validate and install pre-requisites for the FMS solution", + runtime: Runtime.NODEJS_12_X, + code: Code.fromAsset( + "../../source/services/preReqManager/dist/preReqManager.zip" + ), + handler: "index.handler", + memorySize: 512, + timeout: Duration.minutes(15), + environment: { + METRICS_ENDPOINT: map.findInMap("Metric", "MetricsEndpoint"), + SEND_METRIC: map.findInMap("Metric", "SendAnonymousMetric"), + LOG_LEVEL: LogLevel.INFO, //change as needed + }, + }); + + if (!preReqManager.role) throw new Error("no pre req lambda role found"); + const po: Policy = new Policy(this, "preReqManagerPolicy", { + policyName: manifest.prereqPolicy, + roles: [preReqManager.role], + }); + const po0: PolicyStatement = new PolicyStatement({ + effect: Effect.ALLOW, + sid: "VisualEditor0", + actions: [ + "cloudformation:CreateStackInstances", + "cloudformation:DeleteStackInstances", + ], + resources: [ + `arn:aws:cloudformation:*:*:*/${map.findInMap( + "Solution", + "GlobalStackSetName" + )}:*`, + `arn:aws:cloudformation:*:*:*/${map.findInMap( + "Solution", + "RegionalStackSetName" + )}:*`, + `arn:aws:cloudformation:*::type/resource/AWS-IAM-Role`, + `arn:aws:cloudformation:*::type/resource/AWS-SNS-Topic`, + `arn:aws:cloudformation:*::type/resource/AWS-S3-Bucket`, + `arn:aws:cloudformation:*::type/resource/AWS-SNS-TopicPolicy`, + `arn:aws:cloudformation:*::type/resource/AWS-SNS-Subscription`, + `arn:aws:cloudformation:*::type/resource/AWS-S3-BucketPolicy`, + `arn:aws:cloudformation:*::type/resource/AWS-Config-ConfigurationRecorder`, + `arn:aws:cloudformation:*::type/resource/AWS-Config-DeliveryChannel`, + ], + }); + const po1: PolicyStatement = new PolicyStatement({ + effect: Effect.ALLOW, + sid: "VisualEditor1", + actions: [ + "fms:AssociateAdminAccount", + "organizations:ListRoots", + "organizations:EnableAWSServiceAccess", + "organizations:DescribeAccount", + "organizations:DescribeOrganization", + "ec2:DescribeRegions", + "fms:GetAdminAccount", + "cloudformation:CreateStackSet", + ], + resources: ["*"], + }); + po.addStatements(po0); + po.addStatements(po1); + + /** + * @description custom resource for checking pre-requisites + * @type {Provider} + */ + const preReqProvider: Provider = new Provider(this, "PreReqProvider", { + onEventHandler: preReqManager, + }); + + const prereqManager = new CustomResource(this, "PreReqManager", { + serviceToken: preReqProvider.serviceToken, + resourceType: "Custom::PreReqChecker", + properties: { + FMSAdmin: fmsAdmin.valueAsString, + EnableConfig: enableConfig.valueAsString, + AccountId: this.account, + Region: this.region, + GlobalStackSetName: map.findInMap("Solution", "GlobalStackSetName"), + RegionalStackSetName: map.findInMap("Solution", "RegionalStackSetName"), + SolutionId: map.findInMap("Solution", "SolutionId"), + SolutionVersion: map.findInMap("Solution", "SolutionVersion"), + SolutionUuid: uuid.getAttString("UUID"), + }, + }); + + /** + * @description FMS stack + * @type {NestedStack} + */ + const fms: NestedStack = new FMSStack(this, "FMSStack"); + fms.nestedStackResource!.cfnOptions.condition = accountCheck; + fms.nestedStackResource!.addDependsOn( + prereqManager.node.defaultChild as CfnResource + ); + + //============================================================================================= + // cfn_nag suppress rules + //============================================================================================= + const prRole = po.node.findChild("Resource") as CfnPolicy; + prRole.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: "W12", + reason: + "Resource * is required for IAM actions that do not support resource level permissions", + }, + ], + }, + }; + + const prF = preReqManager.node.findChild("Resource") as CfnFunction; + prF.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: "W58", + reason: + "CloudWatch logs write permissions added with managed role AWSLambdaBasicExecutionRole", + }, + ], + }, + }; + + const hF = helperFunction.node.findChild("Resource") as CfnFunction; + hF.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: "W58", + reason: + "CloudWatch logs write permissions added with managed role AWSLambdaBasicExecutionRole", + }, + ], + }, + }; + + const hpP = helperProvider.node.children[0].node.findChild( + "Resource" + ) as CfnFunction; + hpP.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: "W58", + reason: + "CloudWatch logs write permissions added with managed role AWSLambdaBasicExecutionRole", + }, + ], + }, + }; + + const prP = preReqProvider.node.children[0].node.findChild( + "Resource" + ) as CfnFunction; + prP.cfnOptions.metadata = { + cfn_nag: { + rules_to_suppress: [ + { + id: "W58", + reason: + "CloudWatch logs write permissions added with managed role AWSLambdaBasicExecutionRole", + }, + ], + }, + }; + + //============================================================================================= + // Output + //============================================================================================= + new CfnOutput(this, "UUID", { + description: "UUID for deployment", + value: uuid.getAttString("UUID"), + }); + } +} diff --git a/source/resources/package.json b/source/resources/package.json new file mode 100644 index 0000000..4c3268f --- /dev/null +++ b/source/resources/package.json @@ -0,0 +1,45 @@ +{ + "name": "resources", + "version": "1.0.0", + "description": "cdk resources to provision needed infrastructure", + "bin": { + "app": "bin/app.js" + }, + "scripts": { + "pretest": "npm i", + "test": "./node_modules/jest/bin/jest.js --coverage ./__tests__" + }, + "devDependencies": { + "@aws-cdk/assert": "^1.56.0", + "@types/jest": "^25.2.3", + "@types/node": "10.17.5", + "aws-cdk": "^1.49.1", + "jest-sonar-reporter": "^2.0.0", + "jest": "^25.5.0", + "ts-jest": "^25.3.1", + "ts-node": "^8.1.0", + "typescript": "^4.0.2" + }, + "dependencies": { + "@aws-cdk/aws-dynamodb": "^1.49.1", + "@aws-cdk/aws-events": "^1.49.1", + "@aws-cdk/aws-events-targets": "^1.49.1", + "@aws-cdk/aws-iam": "^1.49.1", + "@aws-cdk/aws-logs": "^1.49.1", + "@aws-cdk/aws-sns": "^1.49.1", + "@aws-cdk/aws-sns-subscriptions": "^1.49.1", + "@aws-cdk/aws-sqs": "^1.49.1", + "@aws-cdk/aws-ssm": "^1.49.1", + "@aws-cdk/core": "^1.49.1", + "@aws-cdk/aws-ec2": "^1.49.1", + "@aws-solutions-constructs/aws-events-rule-lambda": "^1.56.0", + "@aws-solutions-constructs/aws-lambda-dynamodb": "^1.56.0", + "@aws-solutions-constructs/aws-lambda-sns": "^1.56.0", + "@aws-solutions-constructs/aws-cloudfront-s3": "^1.56.0" + }, + "jestSonar": { + "reportPath": "../reports", + "reportFile": "resources-reporter.xml", + "indent": 4 + } +} diff --git a/source/resources/tsconfig.json b/source/resources/tsconfig.json new file mode 100644 index 0000000..0432501 --- /dev/null +++ b/source/resources/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "declaration": true, + "strictNullChecks": true, + "alwaysStrict": true, + "outDir": "./dist", + "types": ["jest", "node"] + }, + "exclude": ["cdk.out", "node_modules"] +} diff --git a/source/services/helper/index.ts b/source/services/helper/index.ts new file mode 100644 index 0000000..36a8db5 --- /dev/null +++ b/source/services/helper/index.ts @@ -0,0 +1,165 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import { v4 as uuidv4 } from "uuid"; +import { logger } from "./lib/common/logger"; +import { Metrics } from "./lib/common/metrics"; +import moment from "moment"; +import { FMS } from "aws-sdk"; + +interface IEvent { + RequestType: string; + ResponseURL: string; + StackId: string; + RequestId: string; + ResourceType: string; + LogicalResourceId: string; + ResourceProperties: any; + PhysicalResourceId?: string; +} + +exports.handler = async (event: IEvent, context: any) => { + logger.debug({ + label: "helper", + message: `received event: ${JSON.stringify(event)}`, + }); + + let responseData: any = { + Data: "NOV", + }; + + let status = "SUCCESS"; + const properties = event.ResourceProperties; + + /** + * Generate UUID + */ + if (event.ResourceType === "Custom::CreateUUID") { + if (event.RequestType === "Create") { + responseData = { + UUID: uuidv4(), + }; + logger.debug({ + label: "helper/UUID", + message: `uuid create: ${responseData.UUID}`, + }); + } + } else if (event.ResourceType === "Custom::LaunchData") { + /** + * If stack created/deleted + * Send metric for the event + */ + if (process.env.SEND_METRIC === "Yes") { + logger.debug({ + label: "helper/LaunchData", + message: `sending launch data`, + }); + let eventType = ""; + if (event.RequestType === "Create") { + eventType = "SolutionLaunched"; + } else if (event.RequestType === "Delete") { + eventType = "SolutionDeleted"; + } + + const metric = { + Solution: properties.SolutionId, + UUID: properties.SolutionUuid, + TimeStamp: moment.utc().format("YYYY-MM-DD HH:mm:ss.S"), + Data: { + Event: eventType, + Stack: properties.Stack, + Version: properties.SolutionVersion, + }, + }; + await Metrics.sendAnonymousMetric( + process.env.METRICS_ENDPOINT, + metric + ); + + responseData = { + Data: metric, + }; + } + } else if ( + event.ResourceType === "Custom::FMSAdminCheck" && + event.RequestType === "Create" && + properties.Stack === "FMSStack" + ) { + logger.debug({ + label: "helper/FMSAdminCheck", + message: `validating deployment account is FMS Admin`, + }); + const fms = new FMS({ + apiVersion: "2018-01-01", + region: properties.Region, + }); + try { + const resp = await fms.getAdminAccount({}).promise(); + if (resp.AdminAccount != properties.Account) { + logger.error({ + label: "helper/FMSAdminCheck", + message: `deploy the stack in FMS Admin account`, + }); + throw new Error("please deploy the stack in FMS Admin account"); + } + } catch (e) { + responseData = { + Error: e.message, + }; + status = "FAILED"; + } + } + /** + * Send response back to custom resource + */ + return await sendResponse(event, context.logStreamName, status, responseData); +}; + +/** + * Sends a response to custom resource + * for Create/Update/Delete + * @param {any} event - Custom Resource event + * @param {string} logStreamName - CloudWatch logs stream + * @param {string} responseStatus - response status + * @param {any} responseData - response data + */ +const sendResponse = async ( + event: IEvent, + logStreamName: string, + responseStatus: string, + responseData: any +) => { + const responseBody = { + Status: responseStatus, + Reason: `${JSON.stringify(responseData)}`, + PhysicalResourceId: event.PhysicalResourceId + ? event.PhysicalResourceId + : logStreamName, + StackId: event.StackId, + RequestId: event.RequestId, + LogicalResourceId: event.LogicalResourceId, + Data: responseData, + }; + + logger.debug({ + label: "helper/sendResponse", + message: `Response Body: ${JSON.stringify(responseBody)}`, + }); + + if (responseStatus === "FAILED") { + logger.error({ + label: "helper/sendResponse", + message: responseBody.Reason, + }); + throw new Error(responseBody.Data.Error); + } else return responseBody; +}; diff --git a/source/services/helper/lib/common/logger/index.ts b/source/services/helper/lib/common/logger/index.ts new file mode 100644 index 0000000..140fcd8 --- /dev/null +++ b/source/services/helper/lib/common/logger/index.ts @@ -0,0 +1,92 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +/** + * { + emerg: 0, + alert: 1, + crit: 2, + error: 3, + warning: 4, + notice: 5, + info: 6, + debug: 7 + } + */ +import { createLogger, transports, format } from "winston"; +import { WinstonSNS } from "./winston-sns"; +const { combine, timestamp, printf } = format; + +/* + * Foramting the output as desired + */ +const myFormat = printf(({ level, label, message }) => { + const _level = level.toUpperCase(); + if (label) return `[${_level}] [${label}] ${message}`; + else return `[${_level}] ${message}`; +}); + +/* + * String mask + */ +const maskCardNumbers = (num: any) => { + const str = num.toString(); + const { length } = str; + + return Array.from(str, (n, i) => { + return i < length - 4 ? "*" : n; + }).join(""); +}; + +// Define the format that mutates the info object. +const maskFormat = format((info: any) => { + // You can CHANGE existing property values + if (info.message.securedNumber) { + info.message.securedNumber = maskCardNumbers(info.message.securedNumber); + } + + // You can also ADD NEW properties if you wish + //info.hasCreditCard = !!info.creditCard; + + return info; +}); + +export const logger = createLogger({ + format: combine( + // + // Order is important here, the formats are called in the + // order they are passed to combine. + // + maskFormat(), + timestamp(), + myFormat + ), + + transports: [ + //cw logs transport channel + new transports.Console({ + level: process.env.LOG_LEVEL, + handleExceptions: true, //handle uncaught exceptions + //format: format.splat() + }), + + //sns transport channel + ...(process.env.SNS_ERROR_NOTIFICATION == "true" + ? [ + new WinstonSNS({ + topic_arn: process.env.SNS_TOPIC_ARN, + level: "error", + }), + ] + : []), + ], +}); diff --git a/source/services/helper/lib/common/logger/winston-sns.ts b/source/services/helper/lib/common/logger/winston-sns.ts new file mode 100644 index 0000000..e99f53f --- /dev/null +++ b/source/services/helper/lib/common/logger/winston-sns.ts @@ -0,0 +1,67 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import Transport = require("winston-transport"); +import "util"; +import { SNS } from "aws-sdk"; + +// +// Inherit from `winston-transport` so you can take advantage +// of the base functionality and `.exceptions.handle()`. +// +export class WinstonSNS extends Transport { + readonly topic_arn: string; + readonly region: string; + private sns: any; + constructor(opts: any) { + super(opts); + this.topic_arn = opts.topic_arn; + this.region = opts.topic_arn.split(":")[3]; + // + // Consume any custom options here. e.g.: + // - Connection information for databases + // - Authentication information for APIs (e.g. loggly, papertrail, + // logentries, etc.). + // + } + formatter = async (info: any) => { + if (info.label) + return `[${info.level.toUpperCase()}] [${info.label}] ${ + info.timestamp + }: ${JSON.stringify(info.message, null, 2)}`; + else + return `[${info.level.toUpperCase()}] ${info.timestamp}: ${JSON.stringify( + info.message, + null, + 2 + )}`; + }; + + log = async (info: any) => { + try { + this.sns = new SNS({ + apiVersion: "2010-03-31", + region: this.region, + }); + const _txt = await this.formatter(info); + await this.sns + .publish({ + Message: _txt, + TopicArn: this.topic_arn, + }) + .promise(); + return "sns message published successfully"; + } catch (e) { + throw new Error(e.message); + } + }; +} diff --git a/source/services/helper/lib/common/metrics/index.ts b/source/services/helper/lib/common/metrics/index.ts new file mode 100644 index 0000000..2d04367 --- /dev/null +++ b/source/services/helper/lib/common/metrics/index.ts @@ -0,0 +1,58 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import got from "got"; +import { logger } from "../logger/index"; +/** + * Send metrics to solutions endpoint + * @class Metrics + */ +export class Metrics { + /** + * Sends anonymous metric + * @param {object} metric - metric JSON data + */ + static async sendAnonymousMetric(endpoint: string, metric: any) { + logger.debug({ + label: "metrics/sendAnonymousMetric", + message: `metrics endpoint: ${endpoint}`, + }); + logger.debug({ + label: "metrics/sendAnonymousMetric", + message: `sending metric:${JSON.stringify(metric)}`, + }); + try { + await got(endpoint, { + port: 443, + method: "POST", + headers: { + "Content-Type": "application/json", + "Content-Length": "" + JSON.stringify(metric).length, + }, + body: JSON.stringify(metric), + }); + logger.info({ + label: "metrics/sendAnonymousMetric", + message: `metric sent successfully`, + }); + return `Metric sent: ${JSON.stringify(metric)}`; + } catch (error) { + logger.warn({ + label: "metrics/sendAnonymousMetric", + message: `Error occurred while sending metric: ${JSON.stringify( + error + )}`, + }); + return `Error occurred while sending metric`; + } + } +} diff --git a/source/services/helper/package.json b/source/services/helper/package.json new file mode 100644 index 0000000..282662d --- /dev/null +++ b/source/services/helper/package.json @@ -0,0 +1,30 @@ +{ + "name": "fms-helper", + "version": "1.0.0", + "description": "helper function for FMS solution", + "main": "index.js", + "scripts": { + "pretest": "npm i", + "test": "echo \"nothing to do\"", + "build:clean": "rm -rf ./node_modules && rm -rf ./dist && rm -f ./package-lock.json", + "build:copy": "cp -r ./node_modules ./dist/node_modules", + "build:ts": "./node_modules/typescript/bin/tsc --project ./tsconfig.json", + "build:install": "npm i", + "watch": "tsc -w", + "build:zip": "cd ./dist && zip -rq helperFunction.zip .", + "build:all": "npm run build:clean && npm run build:install && npm run build:ts && npm prune --production && npm run build:copy && npm run build:zip" + }, + "author": "aws-solutions", + "license": "Apache-2.0", + "dependencies": { + "got": "^11.5.1", + "moment": "^2.27.0", + "uuid": "^8.2.0", + "winston": "^3.3.3", + "aws-sdk": "^2.714.0" + }, + "devDependencies": { + "typescript": "^4.0.2", + "@types/uuid": "^8.0.0" + } +} diff --git a/source/services/helper/tsconfig.json b/source/services/helper/tsconfig.json new file mode 100644 index 0000000..600d131 --- /dev/null +++ b/source/services/helper/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "declaration": false, + "noImplicitAny": true, + "noEmit": false + } +} diff --git a/source/services/metricsManager/index.ts b/source/services/metricsManager/index.ts new file mode 100644 index 0000000..1ee7974 --- /dev/null +++ b/source/services/metricsManager/index.ts @@ -0,0 +1,72 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +/** + * @description + * AWS Centralized WAF & Security Group Management + * Microservice to publish metrics to aws-solutions + * @author aws-solutions + */ + +import got from "got"; +import moment from "moment"; +import { logger } from "./logger"; +interface IEvent { + Records: [ + { + messageId: string; + receiptHandle: string; + body: string; + attributes: any; + messageAttributes: any; + md5OfBody: string; + eventSource: "aws:sqs"; + eventSourceARN: string; + awsRegion: string; + } + ]; +} +exports.handler = async (event: IEvent) => { + logger.debug({ + label: "metricsManager", + message: `received event: ${JSON.stringify(event)}`, + }); + const endpoint = process.env.METRICS_ENDPOINT; + const message = event.Records; + const _message = JSON.parse(message[0].body); + await delay(1000); // sleep for 1s + _message.TimeStamp = moment.utc().format("YYYY-MM-DD HH:mm:ss.S"); + try { + await got(endpoint, { + port: 443, + method: "POST", + headers: { + "Content-Type": "application/json", + "Content-Length": "" + JSON.stringify(_message).length, + }, + body: JSON.stringify(_message), + }); + logger.info({ + label: "metricsManager", + message: `metric sent successfully`, + }); + } catch (error) { + logger.warn({ + label: "metricsManager", + message: `Error occurred while sending metric: ${error.message}`, + }); + } +}; + +async function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/source/services/metricsManager/logger/index.ts b/source/services/metricsManager/logger/index.ts new file mode 100644 index 0000000..1f50bd2 --- /dev/null +++ b/source/services/metricsManager/logger/index.ts @@ -0,0 +1,81 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +/** + * { + emerg: 0, + alert: 1, + crit: 2, + error: 3, + warning: 4, + notice: 5, + info: 6, + debug: 7 + } + */ +import { createLogger, transports, format } from "winston"; +const { combine, timestamp, printf } = format; + +/* + * Foramting the output as desired + */ +const myFormat = printf(({ level, label, message }) => { + const _level = level.toUpperCase(); + if (label) return `[${_level}] [${label}] ${message}`; + else return `[${_level}] ${message}`; +}); + +/* + * String mask + */ +const maskCardNumbers = (num: any) => { + const str = num.toString(); + const { length } = str; + + return Array.from(str, (n, i) => { + return i < length - 4 ? "*" : n; + }).join(""); +}; + +// Define the format that mutates the info object. +const maskFormat = format((info: any) => { + // You can CHANGE existing property values + if (info.message.securedNumber) { + info.message.securedNumber = maskCardNumbers(info.message.securedNumber); + } + + // You can also ADD NEW properties if you wish + //info.hasCreditCard = !!info.creditCard; + + return info; +}); + +export const logger = createLogger({ + format: combine( + // + // Order is important here, the formats are called in the + // order they are passed to combine. + // + maskFormat(), + timestamp(), + myFormat + ), + + transports: [ + //cw logs transport channel + new transports.Console({ + level: process.env.LOG_LEVEL, + handleExceptions: true, //handle uncaught exceptions + //format: format.splat() + }), + ], +}); diff --git a/source/services/metricsManager/package.json b/source/services/metricsManager/package.json new file mode 100644 index 0000000..447ad4f --- /dev/null +++ b/source/services/metricsManager/package.json @@ -0,0 +1,31 @@ +{ + "name": "metricsmanager", + "version": "1.0.0", + "description": "microservice to read sqs and publish metrics to aws-solutions", + "main": "index.js", + "scripts": { + "pretest": "npm i", + "test": "echo \"no test\"", + "build:clean": "rm -rf ./node_modules && rm -rf ./dist && rm -f ./package-lock.json", + "build:copy": "cp -r ./node_modules ./dist/node_modules", + "build:ts": "./node_modules/typescript/bin/tsc --project ./tsconfig.json", + "build:install": "npm i", + "watch": "tsc -w", + "build:zip": "cd ./dist && zip -rq metricsManager.zip .", + "build:deployment": "npm run build:ts && npm run build:zip", + "build:all": "npm run build:clean && npm run build:install && npm run build:ts && npm prune --production && npm run build:copy && npm run build:zip" + }, + "dependencies": { + "got": "^11.5.2", + "moment": "^2.27.0", + "winston": "^3.3.3" + }, + "devDependencies": { + "@types/moment": "^2.13.0", + "@types/node": "^14.0.12", + "ts-node": "^8.10.2", + "typescript": "^4.0.2" + }, + "author": "aws-solutions", + "license": "Apache-2.0" +} diff --git a/source/services/metricsManager/tsconfig.json b/source/services/metricsManager/tsconfig.json new file mode 100644 index 0000000..233bd85 --- /dev/null +++ b/source/services/metricsManager/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "declaration": false, + "noImplicitAny": true, + "noEmit": false, + "experimentalDecorators": false + } +} diff --git a/source/services/policyManager/__tests__/fmsHelper.test.ts b/source/services/policyManager/__tests__/fmsHelper.test.ts new file mode 100644 index 0000000..b5c7a4f --- /dev/null +++ b/source/services/policyManager/__tests__/fmsHelper.test.ts @@ -0,0 +1,399 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import "jest"; +import { FMSHelper } from "../lib/fmsHelper"; + +const mockDDB = jest.fn(); +const mockSSM = jest.fn(); +const mockEC2 = jest.fn(); +const mockFMS = jest.fn(); + +const mockDDBItem = { + LastUpdatedAt: { + S: "07-09-2020::17-17-54", + }, + PolicyId: { + S: "a9369754-84e1-422b-ac7d-4d8608e92e27", + }, + PolicyName: { + S: "FMS-WAF-Global", + }, + PolicyUpdateToken: { + S: "1:SqIn3DqPUxtHAv0FqhzxMA==", + }, + Region: { + S: "Global", + }, +}; + +const mockSSMParameterSL = { + Parameter: { + Type: "StringList", + Value: "a,b,c", + Version: 1, + }, +}; +const mockSSMSL = mockSSMParameterSL.Parameter.Value.split(","); + +const mockSSMParameterS = { + Parameter: { + Type: "String", + Value: "a", + Version: 1, + }, +}; +const mockSSMS = mockSSMParameterS.Parameter.Value; + +const mockRegions = { + Regions: [ + { + Endpoint: "ec2.region-a.amazonaws.com", + RegionName: "region-a", + }, + { + Endpoint: "ec2.region-b.amazonaws.com", + RegionName: "region-b", + }, + { + Endpoint: "ec2.region-c.amazonaws.com", + RegionName: "region-c", + }, + ], +}; +const mockRegionsArr = mockRegions.Regions.map((region) => { + return region.RegionName; +}); + +const mockPolicy = { + PolicyName: "P1", + RemediationEnabled: false, + ResourceType: "fmsResourceTyp", + ResourceTags: [{ Key: "", Value: "" }], + ExcludeResourceTags: false, + SecurityServicePolicyData: { + Type: "fmsType", + ManagedServiceData: "PolicyData", + }, + IncludeMap: { + ORG_UNIT: [], + }, +}; + +jest.mock("aws-sdk", () => { + return { + DynamoDB: jest.fn(() => ({ + getItem: mockDDB, + updateItem: mockDDB, + deleteItem: mockDDB, + })), + SSM: jest.fn(() => ({ + getParameter: mockSSM, + })), + EC2: jest.fn(() => ({ + describeRegions: mockEC2, + })), + FMS: jest.fn(() => ({ + deletePolicy: mockFMS, + putPolicy: mockFMS, + })), + }; +}); + +describe("==FMS Helper Tests==", () => { + describe("[getDDBItem]", () => { + beforeEach(() => { + mockDDB.mockReset(); + }); + test("[TDD] successful api call", async () => { + mockDDB.mockImplementation((_) => { + return { + promise() { + return Promise.resolve({ Item: mockDDBItem }); + }, + }; + }); + try { + const data = await FMSHelper.getDDBItem( + "primaryKey", + "sortKey", + "table" + ); + expect(data).toEqual(mockDDBItem); + } catch (e) { + console.log(`negative test ${e.message}`); + } + }); + test("[TDD] failed api call", async () => { + mockDDB.mockImplementation(() => { + return { + promise() { + throw new Error(); + }, + }; + }); + try { + await FMSHelper.getDDBItem("primaryKey", "sortKey", "table"); + } catch (e) { + expect(e.message).toEqual("error getting ddb item"); + } + }); + }); + + describe("[updateDDBItem]", () => { + beforeEach(() => { + mockDDB.mockReset(); + }); + test("[TDD] successful api call", async () => { + mockDDB.mockImplementation(() => { + return { + promise() { + return Promise.resolve(); + }, + }; + }); + try { + await FMSHelper.updateDDBItem( + "primaryKey", + "sortKey", + { updateToken: "updateToken", policyId: "policyId" }, + "table" + ); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[TDD] failed api call", async () => { + mockDDB.mockImplementation(() => { + return { + promise() { + return Promise.reject(); + }, + }; + }); + try { + await FMSHelper.updateDDBItem( + "primaryKey", + "sortKey", + { updateToken: "", policyId: "" }, + "table" + ); + } catch (e) { + expect(e.message).toEqual("error updating ddb item"); + } + }); + }); + + describe("[deleteDDBItem]", () => { + beforeEach(() => { + mockDDB.mockReset(); + }); + test("[TDD] successful api call", async () => { + mockDDB.mockImplementation(() => { + return { + promise() { + return Promise.resolve(); + }, + }; + }); + try { + await FMSHelper.deleteDDBItem("primaryKey", "sortKey", "table"); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[TDD] failed api call", async () => { + mockDDB.mockImplementation(() => { + return { + promise() { + return Promise.reject(); + }, + }; + }); + try { + await FMSHelper.deleteDDBItem("primaryKey", "sortKey", "table"); + } catch (e) { + expect(e.message).toEqual("error deleting ddb item"); + } + }); + }); + + describe("[getSSMParameter]", () => { + beforeEach(() => { + mockSSM.mockReset(); + }); + test("[BDD] successful api call", async () => { + mockSSM.mockImplementation(() => { + return { + promise() { + return Promise.resolve(mockSSMParameterS); + }, + }; + }); + try { + const data = await FMSHelper.getSSMParameter("primaryKey"); + expect(data).toEqual(mockSSMS); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[BDD] successful api call", async () => { + mockSSM.mockImplementation(() => { + return { + promise() { + return Promise.resolve(mockSSMParameterSL); + }, + }; + }); + try { + const data = await FMSHelper.getSSMParameter("primaryKey"); + expect(data).toEqual(expect.arrayContaining(mockSSMSL)); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[TDD] failed api call", async () => { + mockSSM.mockImplementation(() => { + return { + promise() { + return Promise.reject(); + }, + }; + }); + try { + await FMSHelper.getSSMParameter("primaryKey"); + } catch (e) { + expect(e.message).toEqual("error fetching SSM parameter"); + } + }); + }); + + describe("[getRegions]", () => { + beforeEach(() => { + mockEC2.mockReset(); + }); + test("[BDD] successful api call", async () => { + mockEC2.mockImplementation(() => { + return { + promise() { + return Promise.resolve(mockRegions); + }, + }; + }); + try { + const data = await FMSHelper.getRegions(); + expect(data).toEqual(expect.arrayContaining(mockRegionsArr)); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[TDD] failed api call", async () => { + mockEC2.mockImplementation(() => { + return { + promise() { + return Promise.reject(); + }, + }; + }); + try { + await FMSHelper.getRegions(); + } catch (e) { + expect(e.message).toEqual("error fetching regions"); + } + }); + }); + + describe("[putPolicy]", () => { + beforeEach(() => { + mockFMS.mockReset(); + }); + test("[BDD] successful api call", async () => { + mockFMS.mockImplementation(() => { + return { + promise() { + return Promise.resolve(); + }, + }; + }); + try { + await FMSHelper.putPolicy(mockPolicy, "region-a"); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[BDD] successful api call", async () => { + mockFMS.mockImplementation(() => { + return { + promise() { + return Promise.resolve(); + }, + }; + }); + try { + await FMSHelper.putPolicy(mockPolicy, "Global"); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[TDD] failed api call", async () => { + mockFMS.mockRejectedValue(""); + try { + await FMSHelper.putPolicy(mockPolicy, "region-a"); + } catch (e) { + expect(e.message).toEqual("failed to save policy"); + } + }); + }); + + describe("[deletePolicy]", () => { + beforeEach(() => { + mockFMS.mockReset(); + }); + test("[BDD] successful api call", async () => { + FMSHelper.getDDBItem = jest.fn().mockImplementation(() => { + return Promise.resolve(mockDDBItem); + }); + mockFMS.mockImplementation(() => { + return { + promise() { + return Promise.resolve(); + }, + }; + }); + FMSHelper.deleteDDBItem = jest.fn().mockImplementation(() => { + return Promise.resolve(); + }); + try { + await FMSHelper.deletePolicy("", "", ""); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[TDD] failed api call", async () => { + FMSHelper.getDDBItem = jest.fn().mockImplementation(() => { + return Promise.resolve(mockDDBItem); + }); + mockFMS.mockImplementation(() => { + return { + promise() { + throw new Error(); + }, + }; + }); + try { + await FMSHelper.deletePolicy("policyX", "us-east-1", "table"); + } catch (e) { + expect(e.message).toEqual("error deleting policy"); + } + }); + }); +}); diff --git a/source/services/policyManager/__tests__/policyManager.test.ts b/source/services/policyManager/__tests__/policyManager.test.ts new file mode 100644 index 0000000..ec6194c --- /dev/null +++ b/source/services/policyManager/__tests__/policyManager.test.ts @@ -0,0 +1,212 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import "jest"; +import { PolicyManager } from "../lib/policyManager"; +import { FMSHelper } from "../lib/fmsHelper"; + +const mockInvalidOus = ["ou-xxxxx", "ou-yyyyy"]; +const mockValidOus = ["ou-xxxx-xxxxxx99"]; +const mockRegions = ["region-a", "region-b"]; +const mockTags = { + ResourceTags: [ + { + Key: "Environment", + Value: "Dev", + }, + { Key: "Application", Value: "App-01" }, + ], + ExcludeResourceTags: false, +}; + +const mockPolicy = { + PolicyName: "P1", + RemediationEnabled: false, + ResourceType: "fmsResourceTyp", + ResourceTags: [{ Key: "", Value: "" }], + ExcludeResourceTags: false, + SecurityServicePolicyData: { + Type: "fmsType", + ManagedServiceData: "PolicyData", + }, + IncludeMap: { + ORG_UNIT: [], + }, +}; + +describe("==Policy Manager Tests==", () => { + describe("[isOUValid]", () => { + beforeEach(() => { + jest.fn().mockReset(); + }); + test("invalid ou", async () => { + expect(await PolicyManager.isOUValid(mockInvalidOus)).toEqual(false); + }); + test("valid ou", async () => { + expect(await PolicyManager.isOUValid(mockValidOus)).toEqual(true); + }); + }); + + describe("[isOUDelete]", () => { + beforeEach(() => { + jest.fn().mockReset(); + }); + test("delete false", async () => { + expect(await PolicyManager.isOUValid(mockRegions)).toEqual(false); + }); + test("delete true", async () => { + expect(await PolicyManager.isOUDelete(["DeLeTE"])).toEqual(true); + }); + }); + + describe("[isRegionValid]", () => { + beforeEach(() => { + jest.fn().mockReset(); + }); + test("invalid region", async () => { + FMSHelper.getRegions = jest.fn().mockResolvedValue(mockRegions); + expect( + await PolicyManager.isRegionValid(["region-a", "region-c"]) + ).toEqual(false); + }); + test("valid region", async () => { + FMSHelper.getRegions = jest.fn().mockResolvedValue(mockRegions); + expect( + await PolicyManager.isRegionValid(["region-a", "region-b"]) + ).toEqual(true); + }); + test("failed get region call", async () => { + FMSHelper.getRegions = jest + .fn() + .mockRejectedValue({ message: "failed to fetch ec2 regions" }); + try { + await PolicyManager.isRegionValid(["region-a", "region-b"]); + } catch (e) { + expect(e.message).toEqual("failed to fetch ec2 regions"); + } + }); + test("invalid response from get region call", async () => { + FMSHelper.getRegions = jest.fn().mockResolvedValue("region-a"); + try { + await PolicyManager.isRegionValid(["region-a", "region-b"]); + } catch (e) { + expect(e.message).toEqual("no regions found"); + } + }); + }); + + describe("[isRegionDelete]", () => { + beforeEach(() => { + jest.fn().mockReset(); + }); + test("delete false", async () => { + expect(await PolicyManager.isRegionDelete(mockRegions)).toEqual(false); + }); + test("delete true", async () => { + expect(await PolicyManager.isRegionDelete(["delete"])).toEqual(true); + }); + }); + + describe("[isTagValid]", () => { + beforeEach(() => { + jest.fn().mockReset(); + }); + test("invalid tag", async () => { + expect( + await PolicyManager.isTagValid( + JSON.stringify({ + ResouceTags: [ + { Key: "Environment", Value: "Test" }, + { Key: "Application" }, + ], + ExcludeResourceTags: true, + }) + ) + ).toEqual(false); + }); + test("valid tag", async () => { + expect(await PolicyManager.isTagValid(JSON.stringify(mockTags))).toEqual( + true + ); + }); + }); + + describe("[workflowProcessor]", () => { + beforeEach(() => { + jest.fn().mockReset(); + }); + test("successfull call", async () => { + FMSHelper.getDDBItem = jest.fn().mockResolvedValue(""); + FMSHelper.putPolicy = jest.fn().mockResolvedValue({ + Policy: { + PolicyUpdateToken: "updateToken", + PolicyId: "policyId", + }, + }); + FMSHelper.updateDDBItem = jest.fn().mockResolvedValue(""); + try { + await PolicyManager.workflowProcessor("", "", mockPolicy); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("failed call, with get ddb item failure", async () => { + FMSHelper.getDDBItem = jest + .fn() + .mockRejectedValue({ message: "error in fetching ddb item" }); + + try { + await PolicyManager.workflowProcessor("", "", mockPolicy); + } catch (e) { + expect(e.message).toEqual("error in fetching ddb item"); + } + }); + test("failed call, with update ddb item failure", async () => { + FMSHelper.getDDBItem = jest.fn().mockResolvedValue(""); + FMSHelper.putPolicy = jest.fn().mockResolvedValue({ + Policy: { + PolicyUpdateToken: "updateToken", + PolicyId: "policyId", + }, + }); + FMSHelper.updateDDBItem = jest + .fn() + .mockRejectedValue({ message: "error updating ddb item" }); + try { + await PolicyManager.workflowProcessor("", "", mockPolicy); + } catch (e) { + expect(e.message).toEqual("error updating ddb item"); + } + }); + test("failed call, with put policy failure", async () => { + FMSHelper.getDDBItem = jest + .fn() + .mockRejectedValue({ message: "error in putting fms policy" }); + + try { + await PolicyManager.workflowProcessor("", "", mockPolicy); + } catch (e) { + expect(e.message).toEqual("error in putting fms policy"); + } + }); + test("failed call, with put policy invalid response", async () => { + FMSHelper.getDDBItem = jest.fn().mockResolvedValue(""); + FMSHelper.putPolicy = jest.fn().mockResolvedValue(""); + FMSHelper.updateDDBItem = jest.fn().mockResolvedValue(""); + try { + await PolicyManager.workflowProcessor("", "", mockPolicy); + } catch (e) { + expect(e.message).toEqual("error creating policy"); + } + }); + }); +}); diff --git a/source/services/policyManager/__tests__/securitygroupManager.test.ts b/source/services/policyManager/__tests__/securitygroupManager.test.ts new file mode 100644 index 0000000..1eb6836 --- /dev/null +++ b/source/services/policyManager/__tests__/securitygroupManager.test.ts @@ -0,0 +1,138 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import "jest"; +import { SecurityGroupManager } from "../lib/securitygroupManager"; +import { FMSHelper } from "../lib/fmsHelper"; +import { PolicyManager } from "../lib/policyManager"; + +const mockTags = { + ResourceTags: [{ Key: "Environment", Value: "Dev" }], + ExcludeResourceTags: false, +}; +const mockOus = ["ou-xxxxx", "ou-yyyyy"]; +const mockRegion = "region-a"; +const mockRegionG = "Global"; +const mockTable = "table-1"; +type mockPolicy = "USAGE_AUDIT" | "CONTENT_AUDIT"; + +describe("==Security Group Policy Manager Tests==", () => { + describe("[saveSecGrpPolicy]", () => { + beforeEach(() => { + jest.fn().mockReset()(); + }); + test("[TDD] successful save", async () => { + PolicyManager.workflowProcessor = jest.fn().mockResolvedValue(""); + try { + await SecurityGroupManager.saveSecGrpPolicy( + mockOus, + mockTags, + mockRegion, + mockTable, + "CONTENT_AUDIT" + ); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[TDD] successful save", async () => { + PolicyManager.workflowProcessor = jest.fn().mockResolvedValue(""); + try { + await SecurityGroupManager.saveSecGrpPolicy( + mockOus, + mockTags, + mockRegionG, + mockTable, + "USAGE_AUDIT" + ); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[TDD] failed save, invalid security group policy type", async () => { + PolicyManager.workflowProcessor = jest.fn().mockRejectedValue(""); + + try { + await SecurityGroupManager.saveSecGrpPolicy( + mockOus, + mockTags, + mockRegion, + mockTable, + "" + ); + } catch (e) { + expect(e.message).toEqual("Security Group Audit policy not found"); + } + }); + test("[TDD] failed save", async () => { + PolicyManager.workflowProcessor = jest + .fn() + .mockRejectedValue({ message: "error in running workflow processor" }); + + try { + await SecurityGroupManager.saveSecGrpPolicy( + mockOus, + mockTags, + mockRegion, + mockTable, + "CONTENT_AUDIT" + ); + } catch (e) { + expect(e.message).toEqual("error in running workflow processor"); + } + }); + }); + + describe("[deleteWAFPolicy]", () => { + beforeEach(() => { + jest.fn().mockReset(); + }); + test("[TDD] successful deletion", async () => { + FMSHelper.deletePolicy = jest.fn().mockResolvedValue(""); + try { + await SecurityGroupManager.deleteSecGrpPolicy( + mockTable, + mockRegion, + "CONTENT_AUDIT" + ); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[TDD] successful deletion", async () => { + FMSHelper.deletePolicy = jest.fn().mockResolvedValue(""); + try { + await SecurityGroupManager.deleteSecGrpPolicy( + mockTable, + mockRegion, + "USAGE_AUDIT" + ); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[TDD] failed deletion", async () => { + FMSHelper.deletePolicy = jest + .fn() + .mockRejectedValue({ message: "failed to delete policy" }); + try { + await SecurityGroupManager.deleteSecGrpPolicy( + mockTable, + mockRegion, + "CONTENT_AUDIT" + ); + } catch (e) { + expect(e.message).toEqual("failed to delete policy"); + } + }); + }); +}); diff --git a/source/services/policyManager/__tests__/shieldManager.test.ts b/source/services/policyManager/__tests__/shieldManager.test.ts new file mode 100644 index 0000000..f99b24d --- /dev/null +++ b/source/services/policyManager/__tests__/shieldManager.test.ts @@ -0,0 +1,129 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import "jest"; +import { ShieldManager } from "../lib/shieldManager"; +import { FMSHelper } from "../lib/fmsHelper"; +import { PolicyManager } from "../lib/policyManager"; + +const mockTags = { + ResourceTags: [{ Key: "Environment", Value: "Dev" }], + ExcludeResourceTags: false, +}; +const mockOus = ["ou-xxxxx", "ou-yyyyy"]; +const mockRegion = "region-a"; +const mockTable = "table-1"; + +const mockShield = jest.fn(); +jest.mock("aws-sdk", () => { + return { + Shield: jest.fn(() => ({ + getSubscriptionState: mockShield, + })), + }; +}); + +describe("==Shield Policy Manager Tests==", () => { + describe("[saveShieldPolicy]", () => { + beforeEach(() => { + mockShield.mockReset(); + jest.fn().mockReset(); + }); + test("[TDD] successful save", async () => { + mockShield.mockImplementation(() => { + return { + promise() { + return ""; + }, + }; + }); + PolicyManager.workflowProcessor = jest.fn().mockResolvedValue(""); + try { + await ShieldManager.saveShieldPolicy( + mockOus, + mockTags, + mockRegion, + mockTable + ); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[TDD] Shield subscription not active", async () => { + mockShield.mockImplementation(() => { + return { + promise() { + return { SubscriptionState: "INACTIVE" }; + }, + }; + }); + // PolicyManager.workflowProcessor = jest.fn().mockResolvedValue(""); + try { + await ShieldManager.saveShieldPolicy( + mockOus, + mockTags, + mockRegion, + mockTable + ); + } catch (e) { + expect(e.message).toEqual("Shield Advanced subscription not active"); + } + }); + test("[TDD] failed workflow processor call", async () => { + mockShield.mockImplementation(() => { + return { + promise() { + return { SubscriptionState: "ACTIVE" }; + }, + }; + }); + PolicyManager.workflowProcessor = jest + .fn() + .mockRejectedValue({ message: "error running workflow processor" }); + + try { + await ShieldManager.saveShieldPolicy( + mockOus, + mockTags, + mockRegion, + mockTable + ); + } catch (e) { + expect(e.message).toEqual("error running workflow processor"); + } + }); + }); + + describe("[deleteWAFPolicy]", () => { + beforeEach(() => { + jest.fn().mockReset(); + }); + test("[TDD] successful deletion", async () => { + FMSHelper.deletePolicy = jest.fn().mockResolvedValue(""); + try { + await ShieldManager.deleteShieldPolicy(mockTable, mockRegion); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[TDD] failed deletion", async () => { + FMSHelper.deletePolicy = jest + .fn() + .mockRejectedValue({ message: "failed to delete policy" }); + try { + await ShieldManager.deleteShieldPolicy(mockTable, mockRegion); + } catch (e) { + expect(e.message).toEqual("failed to delete policy"); + } + }); + }); +}); diff --git a/source/services/policyManager/__tests__/wafManager.test.ts b/source/services/policyManager/__tests__/wafManager.test.ts new file mode 100644 index 0000000..26357fb --- /dev/null +++ b/source/services/policyManager/__tests__/wafManager.test.ts @@ -0,0 +1,123 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import "jest"; +import { WAFManager } from "../lib/wafManager"; +import { FMSHelper } from "../lib/fmsHelper"; +import { PolicyManager } from "../lib/policyManager"; + +const mockTags = { + ResourceTags: [{ Key: "Environment", Value: "Dev" }], + ExcludeResourceTags: false, +}; +const mockOus = ["ou-xxxxx", "ou-yyyyy"]; +const mockRegion = "region-a"; +const mockRegionG = "Global"; +const mockTable = "table-1"; + +describe("==WAF Policy Manager Tests==", () => { + describe("[saveWAFPolicy]", () => { + beforeEach(() => { + jest.fn().mockReset()(); + }); + test("[TDD] successful save", async () => { + PolicyManager.workflowProcessor = jest.fn().mockResolvedValue(""); + try { + await WAFManager.saveWAFPolicy( + mockOus, + mockTags, + mockRegion, + mockTable + ); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[TDD] successful save", async () => { + PolicyManager.workflowProcessor = jest.fn().mockResolvedValue(""); + try { + await WAFManager.saveWAFPolicy( + mockOus, + mockTags, + mockRegionG, + mockTable + ); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[TDD] failed save", async () => { + PolicyManager.workflowProcessor = jest + .fn() + .mockRejectedValue({ message: "error in running workflow processor" }); + + try { + await WAFManager.saveWAFPolicy( + mockOus, + mockTags, + mockRegion, + mockTable + ); + } catch (e) { + expect(e.message).toEqual("error in running workflow processor"); + } + }); + test("[TDD] failed save", async () => { + PolicyManager.workflowProcessor = jest + .fn() + .mockRejectedValue({ message: "error in running workflow processor" }); + + try { + await WAFManager.saveWAFPolicy( + mockOus, + mockTags, + mockRegionG, + mockTable + ); + } catch (e) { + expect(e.message).toEqual("error in running workflow processor"); + } + }); + }); + + describe("[deleteWAFPolicy]", () => { + beforeEach(() => { + jest.fn().mockReset(); + }); + test("[TDD] successful deletion", async () => { + FMSHelper.deletePolicy = jest.fn().mockResolvedValue(""); + try { + await WAFManager.deleteWAFPolicy(mockTable, mockRegion); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[TDD] successful deletion", async () => { + FMSHelper.deletePolicy = jest.fn().mockResolvedValue(""); + try { + await WAFManager.deleteWAFPolicy(mockTable, mockRegionG); + } catch (e) { + console.log(`negative test: ${e.message}`); + } + }); + test("[TDD] failed deletion", async () => { + FMSHelper.deletePolicy = jest + .fn() + .mockRejectedValue({ message: "failed to delete policy" }); + try { + await WAFManager.deleteWAFPolicy(mockTable, mockRegion); + } catch (e) { + expect(e.message).toEqual("failed to delete policy"); + } + }); + }); +}); diff --git a/source/services/policyManager/index.ts b/source/services/policyManager/index.ts new file mode 100644 index 0000000..8162d81 --- /dev/null +++ b/source/services/policyManager/index.ts @@ -0,0 +1,543 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +/** + * @description + * AWS Centralized WAF & Security Group Management + * Microservice to trigger policy updates + * @author aws-solutions + */ + +import { WAFManager } from "./lib/wafManager"; +import { SecurityGroupManager } from "./lib/securitygroupManager"; +import { ShieldManager } from "./lib/shieldManager"; +import { PolicyManager } from "./lib/policyManager"; +import { FMSHelper } from "./lib/fmsHelper"; +import { logger } from "./lib/common/logger"; + +/** + * @description interface for triggering events + */ +interface IEvent { + version: string; + id: string; + "detail-type": "Parameter Store Change"; + source: "aws.ssm"; + account: string; + time: string; + region: string; + resources: string[]; + detail: { + operation: string; + name: string; + type: string; + description: string; + }; +} + +exports.handler = async (event: IEvent, _: any) => { + logger.debug({ label: "PolicyManager", message: "Loading event..." }); + logger.debug({ + label: "PolicyManager", + message: `event : ${JSON.stringify(event)}`, + }); + + // fetching env variables + const ous = process.env.FMS_OU; + const regions = process.env.FMS_REGIONS; + const tags = process.env.FMS_TAGS; + const table = process.env.FMS_TABLE; + + let _ous: string[], _regions: string[], _tags: any; + try { + _ous = await FMSHelper.getSSMParameter(ous); + _regions = await FMSHelper.getSSMParameter(regions); + _tags = await FMSHelper.getSSMParameter(tags); + } catch (e) { + throw new Error(`Failed to fetch SSM parameter: ${e.messsage}`); + } + + let oud: boolean, + ouv: boolean, + rd: boolean, + rv: boolean, + tv: boolean, + td: boolean; + try { + // validating the SSM parameters + oud = await PolicyManager.isOUDelete(_ous); + ouv = await PolicyManager.isOUValid(_ous); + rd = await PolicyManager.isRegionDelete(_regions); + rv = await PolicyManager.isRegionValid(_regions); + tv = await PolicyManager.isTagValid(_tags); + td = await PolicyManager.isTagDelete(_tags); + } catch (e) { + throw new Error(`Failed to validate SSM parameter: ${e.message}`); + } + + logger.debug({ + label: "PolicyManagaer", + message: JSON.stringify({ + OUDelete: oud, + OUValid: ouv, + RegionDelete: rd, + RegionValid: rv, + TagValid: tv, + TagDelete: td, + }), + }); + + if (!oud && !ouv) throw new Error("Invalid OU input provided"); + + // policies will be updated with NO tags if provided tags are invalid or set to 'delete' + if (!tv || td) { + _tags = { + ResourceTags: [], + ExcludeResourceTags: false, + }; + } else { + _tags = JSON.parse(_tags); + } + + const _e = event.detail.name; + logger.debug({ + label: "PolicyManager", + message: `triggering parameter: ${_e}`, + }); + + switch (_e) { + case ous: { + if (oud) { + /**************************************************************** + * Delete ALL Policies + ***************************************************************/ + logger.info({ + label: `PolicyManager${ous}`, + message: `initiating DELETE on ALL policies`, + }); + await WAFManager.deleteWAFPolicy(table, "Global").catch((e) => { + logger.warn({ + label: `PolicyManager/deleteWAFPolicy-Global`, + message: `${e.message}`, + }); + }); // global + await ShieldManager.deleteShieldPolicy(table, "Global").catch((e) => { + logger.warn({ + label: `PolicyManager/deleteShieldPolicy`, + message: `${e.message}`, + }); + }); + const _r = await FMSHelper.getRegions().catch((e) => { + logger.error(`${e.message}`); + }); + await Promise.allSettled( + _r.map(async (region) => { + await WAFManager.deleteWAFPolicy(table, region).catch((e) => { + logger.warn({ + label: `PolicyManager/deleteWAFPolicy-Regional`, + message: `${e.message}`, + }); + }); // regional + await ShieldManager.deleteShieldPolicy(table, region).catch((e) => { + logger.warn({ + label: `PolicyManager/deleteShieldPolicy-Regional`, + message: `${e.message}`, + }); + }); + await SecurityGroupManager.deleteSecGrpPolicy( + table, + region, + "USAGE_AUDIT" + ).catch((e) => { + logger.warn({ + label: `PolicyManager/deleteSecGrpPolicy-UsageAudit`, + message: `${e.message}`, + }); + }); + await SecurityGroupManager.deleteSecGrpPolicy( + table, + region, + "CONTENT_AUDIT" + ).catch((e) => { + logger.warn({ + label: `PolicyManager/deleteSecGrpPolicy-ContentAudit`, + message: `${e.message}`, + }); + }); + }) + ); + logger.info({ + label: `PolicyManager${ous}`, + message: "ALL FMS policies and related resources deleted", + }); + } else if (ouv && !rv) { + /**************************************************************** + * Save ALL Global Policies + ***************************************************************/ + logger.debug({ + label: `PolicyManager${ous}`, + message: `saving global policies`, + }); + await WAFManager.saveWAFPolicy(_ous, _tags, table, "Global").catch( + (e) => { + logger.error({ + label: `PolicyManager/saveWAFPolicy-Global`, + message: `${e.message}`, + }); + } + ); + await ShieldManager.saveShieldPolicy( + _ous, + _tags, + table, + "Global" + ).catch((e) => { + logger.error({ + label: `PolicyManager/saveShieldPolicy`, + message: `${e.message}`, + }); + }); + logger.info({ + label: `PolicyManager${ous}`, + message: `global policies saved`, + }); + } else if (ouv && rv) { + /**************************************************************** + * Save ALL Policies + ***************************************************************/ + logger.debug({ + label: `PolicyManager${ous}`, + message: `saving policies`, + }); + await WAFManager.saveWAFPolicy(_ous, _tags, table, "Global").catch( + (e) => { + logger.error({ + label: `PolicyManager/saveWAFPolicy-Global`, + message: `${e.message}`, + }); + } + ); + await ShieldManager.saveShieldPolicy( + _ous, + _tags, + table, + "Global" + ).catch((e) => { + logger.error({ + label: `PolicyManager/saveShieldPolicy-Global`, + message: `${e.message}`, + }); + }); + await Promise.allSettled( + _regions.map(async (region) => { + await WAFManager.saveWAFPolicy(_ous, _tags, table, region).catch( + (e) => { + logger.error({ + label: `PolicyManager/saveWAFPolicy-Regional`, + message: `${e.message}`, + }); + } + ); // regional + await ShieldManager.saveShieldPolicy( + _ous, + _tags, + table, + region + ).catch((e) => { + logger.error({ + label: `PolicyManager/saveShieldPolicy-Regional`, + message: `${e.message}`, + }); + }); + await SecurityGroupManager.saveSecGrpPolicy( + _ous, + _tags, + table, + region, + "USAGE_AUDIT" + ).catch((e) => { + logger.error({ + label: `PolicyManager/saveSecGrpPolicy-UsageAudit`, + message: `${e.message}`, + }); + }); + await SecurityGroupManager.saveSecGrpPolicy( + _ous, + _tags, + table, + region, + "CONTENT_AUDIT" + ).catch((e) => { + logger.error({ + label: `PolicyManager/saveSecGrpPolicy-ContentAudit`, + message: `${e.message}`, + }); + }); + }) + ); + logger.info({ + label: `PolicyManager${ous}`, + message: `policies saved`, + }); + } else { + logger.error({ + lable: `PolicyManager${ous}`, + message: "invalid OU input", + }); + throw new Error("Invalid OU input provided"); + } + break; + } + case regions: { + if (rd) { + /**************************************************************** + * Delete Regional Policies + ***************************************************************/ + logger.warn({ + label: `PolicyManager${regions}`, + message: `initiating DELETE on ALL regional policies`, + }); + const _r = await FMSHelper.getRegions().catch((e) => { + logger.error(`${e.message}`); + }); + await Promise.allSettled( + _r.map(async (region) => { + await WAFManager.deleteWAFPolicy(table, region).catch((e) => { + logger.warn({ + label: `PolicyManager/deleteWAFPolicy-Regional`, + message: `${e.message}`, + }); + }); // regional + await ShieldManager.deleteShieldPolicy(table, region).catch((e) => { + logger.warn({ + label: `PolicyManager/deleteShieldPolicy-Regional`, + message: `${e.message}`, + }); + }); + await SecurityGroupManager.deleteSecGrpPolicy( + table, + region, + "USAGE_AUDIT" + ).catch((e) => { + logger.warn({ + label: `PolicyManager/deleteSecGrpPolicy-UsageAudit`, + message: `${e.message}`, + }); + }); + await SecurityGroupManager.deleteSecGrpPolicy( + table, + region, + "CONTENT_AUDIT" + ).catch((e) => { + logger.warn({ + label: `PolicyManager/deleteSecGrpPolicy-ContentAudit`, + message: `${e.message}`, + }); + }); + }) + ); + logger.info({ + label: `PolicyManager${regions}`, + message: `ALL FMS regional policies and related resources deleted`, + }); + } else if (rv && ouv) { + /**************************************************************** + * Save Regional Policies + ***************************************************************/ + logger.debug({ + label: `PolicyManager${regions}`, + message: `saving regional policies`, + }); + const _r = await FMSHelper.getRegions(); + await Promise.allSettled( + _r.map(async (region) => { + if (!_regions.includes(region)) { + await WAFManager.deleteWAFPolicy(table, region).catch((e) => { + logger.warn({ + label: `PolicyManager/deleteWAFPolicy-Regional`, + message: `${e.message}`, + }); + }); + await ShieldManager.deleteShieldPolicy(table, region).catch( + (e) => { + logger.warn({ + label: `PolicyManager/deleteShieldPolicy-Regional`, + message: `${e.message}`, + }); + } + ); + await SecurityGroupManager.deleteSecGrpPolicy( + table, + region, + "USAGE_AUDIT" + ).catch((e) => { + logger.warn({ + label: `PolicyManager/deleteSecGrpPolicy-UsageAudit`, + message: `${e.message}`, + }); + }); + await SecurityGroupManager.deleteSecGrpPolicy( + table, + region, + "CONTENT_AUDIT" + ).catch((e) => { + logger.warn({ + label: `PolicyManager/deleteSecGrpPolicy-ContentAudit`, + message: `${e.message}`, + }); + }); + } else { + await WAFManager.saveWAFPolicy(_ous, _tags, table, region).catch( + (e) => { + logger.error({ + label: `PolicyManager/saveWAFPolicy-Regional`, + message: `${e.message}`, + }); + } + ); // regional + await ShieldManager.saveShieldPolicy( + _ous, + _tags, + table, + region + ).catch((e) => { + logger.error({ + label: `PolicyManager/saveShieldPolicy-Regional`, + message: `${e.message}`, + }); + }); + await SecurityGroupManager.saveSecGrpPolicy( + _ous, + _tags, + table, + region, + "USAGE_AUDIT" + ).catch((e) => { + logger.error({ + label: `PolicyManager/saveSecGrpPolicy-UsageAudit`, + message: `${e.message}`, + }); + }); + await SecurityGroupManager.saveSecGrpPolicy( + _ous, + _tags, + table, + region, + "CONTENT_AUDIT" + ).catch((e) => { + logger.error({ + label: `PolicyManager/saveSecGrpPolicy-ContentAudit`, + message: `${e.message}`, + }); + }); + } + }) + ); + logger.debug({ + label: `PolicyManager${regions}`, + message: "policies saved", + }); + } else { + logger.error({ + label: `PolicyManager${regions}`, + message: "Invalid region list provided", + }); + throw new Error("Invalid region list provided"); + } + break; + } + case tags: { + /**************************************************************** + * Save ALL Policies + ***************************************************************/ + logger.debug({ + label: "PolicyManagaer", + message: `tags: ${JSON.stringify(_tags)}`, + }); + logger.debug({ + label: `PolicyManager${tags}`, + message: "saving policies", + }); + await WAFManager.saveWAFPolicy(_ous, _tags, table, "Global").catch( + (e) => { + logger.error({ + label: `PolicyManager/saveWAFPolicy-Global`, + message: `${e.message}`, + }); + } + ); // global + await ShieldManager.saveShieldPolicy(_ous, _tags, table, "Global").catch( + (e) => { + logger.error({ + label: `PolicyManager/saveShieldPolicy-Global`, + message: `${e.message}`, + }); + } + ); + await Promise.allSettled( + _regions.map(async (region) => { + await WAFManager.saveWAFPolicy(_ous, _tags, table, region).catch( + (e) => { + logger.error({ + label: `PolicyManager/saveWAFPolicy-Regional`, + message: `${e.message}`, + }); + } + ); // regional + await ShieldManager.saveShieldPolicy( + _ous, + _tags, + table, + region + ).catch((e) => { + logger.error({ + label: `PolicyManager/saveShieldPolicy-Regional`, + message: `${e.message}`, + }); + }); + await SecurityGroupManager.saveSecGrpPolicy( + _ous, + _tags, + table, + region, + "USAGE_AUDIT" + ).catch((e) => { + logger.error({ + label: `PolicyManager/saveSecGrpPolicy-UsageAudit`, + message: `${e.message}`, + }); + }); + await SecurityGroupManager.saveSecGrpPolicy( + _ous, + _tags, + table, + region, + "CONTENT_AUDIT" + ).catch((e) => { + logger.error({ + label: `PolicyManager/saveSecGrpPolicy-ContentAudit`, + message: `${e.message}`, + }); + }); + }) + ); + logger.debug({ + label: `PolicyManager${tags}`, + message: "policies saved", + }); + break; + } + default: { + break; + } + } +}; diff --git a/source/services/policyManager/jest.config.js b/source/services/policyManager/jest.config.js new file mode 100644 index 0000000..f26569b --- /dev/null +++ b/source/services/policyManager/jest.config.js @@ -0,0 +1,58 @@ +module.exports = { + // Automatically clear mock calls and instances between every test + clearMocks: false, + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of regexp pattern strings used to skip coverage collection + coveragePathIgnorePatterns: ["/node_modules/"], + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 50, + functions: 50, + lines: 50, + statements: 50, + }, + }, + + // An array of directory names to be searched recursively up from the requiring module's location + moduleDirectories: ["node_modules"], + + // An array of file extensions your modules use + moduleFileExtensions: ["ts", "json", "jsx", "js", "tsx", "node"], + + // Automatically reset mock state between every test + resetMocks: false, + + // The glob patterns Jest uses to detect test files + testMatch: ["**/?(*.)+(spec|test).[t]s?(x)"], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + testPathIgnorePatterns: ["/node_modules/"], + + // A map from regular expressions to paths to transformers + transform: { + "^.+\\.(t)sx?$": "ts-jest", + }, + + // Indicates whether each individual test should be reported during the run + verbose: true, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + collectCoverageFrom: [ + "./lib/*", + "!./lib/common/*", + "!**.js", + "!**/*.d.ts", + "!**/*.json", + ], + + // A list of paths to modules that run some code to configure or set up the testing environment + setupFiles: ["./jest.setup.js"], + + // This option allows the use of a custom results processor. + testResultsProcessor: "jest-sonar-reporter", +}; diff --git a/source/services/policyManager/jest.setup.js b/source/services/policyManager/jest.setup.js new file mode 100644 index 0000000..6b195e9 --- /dev/null +++ b/source/services/policyManager/jest.setup.js @@ -0,0 +1,3 @@ +process.on("unhandledRejection", (reason) => { + throw reason; +}); diff --git a/source/services/policyManager/lib/PromiseConstructor.d.ts b/source/services/policyManager/lib/PromiseConstructor.d.ts new file mode 100644 index 0000000..459f652 --- /dev/null +++ b/source/services/policyManager/lib/PromiseConstructor.d.ts @@ -0,0 +1,43 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +export interface PromiseResolution { + status: "fulfilled"; + value: T; +} + +export interface PromiseRejection { + status: "rejected"; + reason: E; +} + +export type PromiseResult = + | PromiseResolution + | PromiseRejection; + +export type PromiseList = { + [P in keyof T]: Promise; +}; + +export type PromiseResultList = { + [P in keyof T]: PromiseResult; +}; + +declare global { + interface PromiseConstructor { + allSettled(): Promise<[]>; + allSettled( + list: PromiseList + ): Promise>; + allSettled(iterable: Iterable): Promise>>; + } +} diff --git a/source/services/policyManager/lib/clientConfig.json b/source/services/policyManager/lib/clientConfig.json new file mode 100644 index 0000000..83fae31 --- /dev/null +++ b/source/services/policyManager/lib/clientConfig.json @@ -0,0 +1,9 @@ +{ + "fms": "2018-01-01", + "ec2": "2016-11-15", + "ssm": "2014-11-06", + "dynamodb": "2012-08-10", + "shield": "2016-06-02", + "sqs": "2012-11-05", + "dataPlane": "us-east-1" +} diff --git a/source/services/policyManager/lib/common/logger/index.ts b/source/services/policyManager/lib/common/logger/index.ts new file mode 100644 index 0000000..140fcd8 --- /dev/null +++ b/source/services/policyManager/lib/common/logger/index.ts @@ -0,0 +1,92 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +/** + * { + emerg: 0, + alert: 1, + crit: 2, + error: 3, + warning: 4, + notice: 5, + info: 6, + debug: 7 + } + */ +import { createLogger, transports, format } from "winston"; +import { WinstonSNS } from "./winston-sns"; +const { combine, timestamp, printf } = format; + +/* + * Foramting the output as desired + */ +const myFormat = printf(({ level, label, message }) => { + const _level = level.toUpperCase(); + if (label) return `[${_level}] [${label}] ${message}`; + else return `[${_level}] ${message}`; +}); + +/* + * String mask + */ +const maskCardNumbers = (num: any) => { + const str = num.toString(); + const { length } = str; + + return Array.from(str, (n, i) => { + return i < length - 4 ? "*" : n; + }).join(""); +}; + +// Define the format that mutates the info object. +const maskFormat = format((info: any) => { + // You can CHANGE existing property values + if (info.message.securedNumber) { + info.message.securedNumber = maskCardNumbers(info.message.securedNumber); + } + + // You can also ADD NEW properties if you wish + //info.hasCreditCard = !!info.creditCard; + + return info; +}); + +export const logger = createLogger({ + format: combine( + // + // Order is important here, the formats are called in the + // order they are passed to combine. + // + maskFormat(), + timestamp(), + myFormat + ), + + transports: [ + //cw logs transport channel + new transports.Console({ + level: process.env.LOG_LEVEL, + handleExceptions: true, //handle uncaught exceptions + //format: format.splat() + }), + + //sns transport channel + ...(process.env.SNS_ERROR_NOTIFICATION == "true" + ? [ + new WinstonSNS({ + topic_arn: process.env.SNS_TOPIC_ARN, + level: "error", + }), + ] + : []), + ], +}); diff --git a/source/services/policyManager/lib/common/logger/winston-sns.ts b/source/services/policyManager/lib/common/logger/winston-sns.ts new file mode 100644 index 0000000..e99f53f --- /dev/null +++ b/source/services/policyManager/lib/common/logger/winston-sns.ts @@ -0,0 +1,67 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import Transport = require("winston-transport"); +import "util"; +import { SNS } from "aws-sdk"; + +// +// Inherit from `winston-transport` so you can take advantage +// of the base functionality and `.exceptions.handle()`. +// +export class WinstonSNS extends Transport { + readonly topic_arn: string; + readonly region: string; + private sns: any; + constructor(opts: any) { + super(opts); + this.topic_arn = opts.topic_arn; + this.region = opts.topic_arn.split(":")[3]; + // + // Consume any custom options here. e.g.: + // - Connection information for databases + // - Authentication information for APIs (e.g. loggly, papertrail, + // logentries, etc.). + // + } + formatter = async (info: any) => { + if (info.label) + return `[${info.level.toUpperCase()}] [${info.label}] ${ + info.timestamp + }: ${JSON.stringify(info.message, null, 2)}`; + else + return `[${info.level.toUpperCase()}] ${info.timestamp}: ${JSON.stringify( + info.message, + null, + 2 + )}`; + }; + + log = async (info: any) => { + try { + this.sns = new SNS({ + apiVersion: "2010-03-31", + region: this.region, + }); + const _txt = await this.formatter(info); + await this.sns + .publish({ + Message: _txt, + TopicArn: this.topic_arn, + }) + .promise(); + return "sns message published successfully"; + } catch (e) { + throw new Error(e.message); + } + }; +} diff --git a/source/services/policyManager/lib/common/metrics/index.ts b/source/services/policyManager/lib/common/metrics/index.ts new file mode 100644 index 0000000..a807671 --- /dev/null +++ b/source/services/policyManager/lib/common/metrics/index.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import { SQS } from "aws-sdk"; +import { logger } from "../logger/index"; +import clientConfig from "../../clientConfig.json"; + +/** + * Send metrics to solutions queueUrl + * @class Metrics + */ +export class Metrics { + /** + * Sends anonymous metric + * @param {string} queueURL - sqs queue URL + * @param {object} metric - metric JSON data + */ + static async sendAnonymousMetric(queueUrl: string, metric: any) { + logger.debug({ + label: "metrics/sendAnonymousMetric", + message: `metrics queueUrl: ${queueUrl}, sending metric:${JSON.stringify( + metric + )}`, + }); + const sqs = new SQS({ apiVersion: clientConfig.sqs }); + try { + await sqs + .sendMessage({ + QueueUrl: queueUrl, + MessageBody: JSON.stringify(metric), + }) + .promise(); + logger.info({ + label: "metrics/sendAnonymousMetric", + message: `metric sent to queue`, + }); + } catch (error) { + logger.warn({ + label: "metrics/sendAnonymousMetric", + message: `Error sending metric: ${error.messsage}`, + }); + } + } +} diff --git a/source/services/policyManager/lib/fmsHelper.ts b/source/services/policyManager/lib/fmsHelper.ts new file mode 100644 index 0000000..e8d7da9 --- /dev/null +++ b/source/services/policyManager/lib/fmsHelper.ts @@ -0,0 +1,402 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import { DynamoDB, SSM, EC2, FMS } from "aws-sdk"; +import awsClient from "./clientConfig.json"; +import { Policy } from "aws-sdk/clients/fms"; +import { logger } from "./common/logger"; + +interface IAttribute { + updateToken: string; + policyId: string; +} +export class FMSHelper { + constructor() { + /** + * nothing to do + */ + } + /** + * @description get dynamodb item for the policy + * @param {string} policyName - policy name to be queried + * @param {string} table - table name with FMS policy details + * @returns + * @example { + "LastUpdatedAt": { + "S": "07-09-2020::17-17-54" + }, + "PolicyId": { + "S": "a9369754-84e1-422b-ac7d-4d8608e92e27" + }, + "PolicyName": { + "S": "FMS-WAF-Global" + }, + "PolicyUpdateToken": { + "S": "1:SqIn3DqPUxtHAv0FqhzxMA==" + }, + "Region": { + "S": "Global" + } + } + */ + static async getDDBItem(primaryKey: string, sortKey: string, table: string) { + logger.debug({ + label: "fmsHelper/getDDBItem", + message: `policy item: ${JSON.stringify({ + policy: primaryKey, + region: sortKey, + })}`, + }); + try { + const ddb = new DynamoDB({ + apiVersion: awsClient.dynamodb, + }); + const params = { + Key: { + PolicyName: { + S: primaryKey, + }, + Region: { + S: sortKey, + }, + }, + TableName: table, + }; + const response = await ddb.getItem(params).promise(); + logger.debug({ + label: "fmsHelper/getDDBItem", + message: `ddb item fetched: ${JSON.stringify(response)}`, + }); + if (!response.Item) throw new Error("ResourceNotFound"); + else return response.Item; + } catch (e) { + if (e.message === "ResourceNotFound") { + logger.warn({ + label: "fmsHelper/getDDBItem", + message: "dynamo db item not found", + }); + throw new Error("ResourceNotFound"); + } + throw new Error("error getting ddb item"); + } + } + + /** + * @description update dynamodb item for the policy + * @param {string} primaryKey - primary key for the ddb table + * @param {string} sortKey - sory key for the ddb table + * @param {object} updateAttr - update attributes + * @param {string} table - table name with FMS policy details + * @returns + */ + static async updateDDBItem( + primaryKey: string, + sortKey: string, + updateAttr: IAttribute, + table: string + ) { + logger.debug({ + label: "fmsHelper/updateDDBItem", + message: `policy item: ${JSON.stringify({ + policy: primaryKey, + region: sortKey, + update: updateAttr, + })}`, + }); + try { + const ddb = new DynamoDB({ + apiVersion: awsClient.dynamodb, + }); + const params = { + ExpressionAttributeNames: { + "#AT": "LastUpdatedAt", + "#PO": "PolicyUpdateToken", + "#PI": "PolicyId", + }, + ExpressionAttributeValues: { + ":t": { + S: new Date().toISOString(), // current time + }, + ":p": { + S: updateAttr.updateToken, // update token + }, + ":pi": { + S: updateAttr.policyId, //policy id + }, + }, + Key: { + PolicyName: { + S: primaryKey, + }, + Region: { + S: sortKey, + }, + }, + TableName: table, + UpdateExpression: "SET #AT = :t, #PO = :p, #PI = :pi", + }; + logger.debug({ + label: "FMSHelper/updateDDBItem", + message: `ddb item details: ${JSON.stringify(params)}`, + }); + await ddb.updateItem(params).promise(); + logger.info({ + label: "FMSHelper/updateDDBItem", + message: `ddb item updated`, + }); + } catch (e) { + logger.error({ + label: "fmsHelper/updateDDBItem", + message: JSON.stringify(e), + }); + throw new Error("error updating ddb item"); + } + } + + /** + * @description delete dynamodB item + * @param {string} primaryKey - primary key for the ddb table + * @param {string} sortKey - sory key for the ddb table + * @param {string} table - table name with FMS policy details + */ + + static async deleteDDBItem( + primaryKey: string, + sortKey: string, + table: string + ) { + logger.debug({ + label: "fmsHelper/deleteDDBItem", + message: `deleting policy: ${JSON.stringify({ + policy: primaryKey, + region: sortKey, + })}`, + }); + try { + const ddb = new DynamoDB({ + apiVersion: awsClient.dynamodb, + }); + const params = { + Key: { + PolicyName: { + S: primaryKey, + }, + Region: { + S: sortKey, + }, + }, + TableName: table, + }; + logger.debug({ + label: "FMSHelper/deleteDDBItem", + message: `ddb item details: ${JSON.stringify(params)}`, + }); + await ddb.deleteItem(params).promise(); + logger.info({ + label: "FMSHelper/deleteDDBItem", + message: `ddb item deleted`, + }); + } catch (e) { + logger.error({ + label: "fmsHelper/deleteDDBItem", + messsage: JSON.stringify(e), + }); + throw new Error("error deleting ddb item"); + } + } + /** + * @description function to fetch ssm parameter value + * @param {string} ssmParameterName - name of the parameter to fetch + * @returns {Promise} + */ + static async getSSMParameter(ssmParameterName: string): Promise { + logger.debug({ + label: "FMSHelper/getSSMParameter", + message: `getting ssm parameter: ${ssmParameterName}`, + }); + try { + const ssm = new SSM({ apiVersion: awsClient.ssm }); + const response = await ssm + .getParameter({ Name: ssmParameterName }) + .promise(); + if (!response.Parameter?.Value || !response.Parameter?.Version) { + logger.error({ + label: "FMSHelper/getSSMParameter", + message: `parameter not found: ${ssmParameterName}`, + }); + throw new Error("parameter not found"); + } else { + logger.info({ + label: "FMSHelper/getSSMParameter", + message: `ssm parameter fetched: ${JSON.stringify(response)}`, + }); + if (response.Parameter.Type === "StringList") + return response.Parameter.Value.split(","); + // return string[] + else return response.Parameter.Value; + // return string + } + } catch (e) { + logger.error({ + label: "FMSHelper/getSSMParameter", + message: JSON.stringify(e), + }); + throw new Error("error fetching SSM parameter"); + } + } + + /** + * @description returns ec2 regions list + * @returns + */ + static async getRegions() { + logger.debug({ + label: "FMSHelper/getRegions", + message: `getting ec2 regions`, + }); + try { + const ec2 = new EC2({ + apiVersion: awsClient.ec2, + }); + + const _r = await ec2.describeRegions().promise(); + + if (!_r.Regions) throw new Error("failed to describe regions"); + + const regions = _r.Regions.filter((region) => { + return region.RegionName !== "ap-northeast-3"; + }).map((region) => { + return region.RegionName; + }); + logger.debug({ + label: "FMSHelper/getRegions", + message: `${JSON.stringify({ regions: regions })}`, + }); + return regions; + } catch (e) { + logger.error({ + label: "fmsHelper/getRegions", + message: JSON.stringify(e), + }); + throw new Error("error fetching regions"); + } + } + + /** + * @description put fms policy + * @param policyName + * @param region + * @param table + */ + static async putPolicy(policy: Policy, region: string) { + logger.debug({ + label: "fmsHelper/putPolicy", + message: `saving policy: ${JSON.stringify(policy)}`, + }); + try { + let fms: FMS; + // global or regional + if (region === "Global") { + fms = new FMS({ + apiVersion: awsClient.fms, + region: awsClient.dataPlane, + retryDelayOptions: { base: 500 }, // adjust as needed + maxRetries: 10, // adjust as needed + }); + } else { + fms = new FMS({ + apiVersion: awsClient.fms, + region: region, + retryDelayOptions: { base: 500 }, // adjust as needed + maxRetries: 10, // adjust as needed + }); + } + const resp = await fms + .putPolicy({ + Policy: policy, + }) + .promise(); + logger.info({ + label: "fmsHelper/putPolicy", + message: `policy saved`, + }); + return resp; + } catch (e) { + logger.error({ + label: "fmsHelper/putPolicy", + message: JSON.stringify(e), + }); + throw new Error(`failed to save policy`); + } + } + + /** + * @description delete fms policy + * @param policyName + * @param region + * @param table + */ + static async deletePolicy(policyName: string, region: string, table: string) { + logger.debug({ + label: "fmsHelper/deletePolicy", + message: `deleting policy: ${JSON.stringify({ + policy: policyName, + region: region, + })}`, + }); + try { + const resp = await FMSHelper.getDDBItem(policyName, region, table); + let fms: FMS; + // global or regional + if (region === "Global") { + fms = new FMS({ + apiVersion: awsClient.fms, + region: awsClient.dataPlane, + retryDelayOptions: { base: 500 }, + maxRetries: 10, + }); + } else { + fms = new FMS({ + apiVersion: awsClient.fms, + region: region, + retryDelayOptions: { base: 500 }, + maxRetries: 10, + }); + } + await fms + .deletePolicy({ + PolicyId: resp!.PolicyId.S!, + DeleteAllPolicyResources: true, + }) + .promise(); + await FMSHelper.deleteDDBItem(policyName, region, table); + logger.info({ + label: "fmsHelper/deletePolicy", + message: `policy deleted`, + }); + } catch (e) { + if (e.message === "ResourceNotFound") { + logger.warn({ + label: "fmsHelper/deletePolicy", + message: "policy does not exist", + }); + throw new Error("policy not found"); + } else { + logger.error({ + label: "fmsHelper/deletePolicy", + message: e.message, + }); + throw new Error("error deleting policy"); + } + } + } +} diff --git a/source/services/policyManager/lib/manifest.json b/source/services/policyManager/lib/manifest.json new file mode 100644 index 0000000..6fc46be --- /dev/null +++ b/source/services/policyManager/lib/manifest.json @@ -0,0 +1,206 @@ +{ + "fmsTableName": "FMS-Table", + "basic": { + "WAF": [ + { + "policyName": "FMS-WAF-01", + "policyScope": "Global", + "resourceType": "AWS::CloudFront::Distribution", + "remediationEnabled": true, + "excludeResourceTags": false, + "policyDetails": { + "type": "WAFV2", + "preProcessRuleGroups": [ + { + "ruleGroupArn": null, + "overrideAction": { "type": "NONE" }, + "managedRuleGroupIdentifier": { + "version": null, + "vendorName": "AWS", + "managedRuleGroupName": "AWSManagedRulesCommonRuleSet" + }, + "ruleGroupType": "ManagedRuleGroup", + "excludeRules": [] + }, + { + "ruleGroupArn": null, + "overrideAction": { "type": "NONE" }, + "managedRuleGroupIdentifier": { + "version": null, + "vendorName": "AWS", + "managedRuleGroupName": "AWSManagedRulesAdminProtectionRuleSet" + }, + "ruleGroupType": "ManagedRuleGroup", + "excludeRules": [] + }, + { + "ruleGroupArn": null, + "overrideAction": { "type": "NONE" }, + "managedRuleGroupIdentifier": { + "version": null, + "vendorName": "AWS", + "managedRuleGroupName": "AWSManagedRulesKnownBadInputsRuleSet" + }, + "ruleGroupType": "ManagedRuleGroup", + "excludeRules": [] + }, + { + "ruleGroupArn": null, + "overrideAction": { "type": "NONE" }, + "managedRuleGroupIdentifier": { + "version": null, + "vendorName": "AWS", + "managedRuleGroupName": "AWSManagedRulesSQLiRuleSet" + }, + "ruleGroupType": "ManagedRuleGroup", + "excludeRules": [] + } + ], + "postProcessRuleGroups": [], + "defaultAction": { "type": "ALLOW" }, + "overrideCustomerWebACLAssociation": false, + "loggingConfiguration": null + } + }, + { + "policyName": "FMS-WAF-02", + "policyScope": "Regional", + "policyDetails": { + "type": "WAFV2", + "preProcessRuleGroups": [ + { + "ruleGroupArn": null, + "overrideAction": { "type": "NONE" }, + "managedRuleGroupIdentifier": { + "version": null, + "vendorName": "AWS", + "managedRuleGroupName": "AWSManagedRulesCommonRuleSet" + }, + "ruleGroupType": "ManagedRuleGroup", + "excludeRules": [] + }, + { + "ruleGroupArn": null, + "overrideAction": { "type": "NONE" }, + "managedRuleGroupIdentifier": { + "version": null, + "vendorName": "AWS", + "managedRuleGroupName": "AWSManagedRulesAdminProtectionRuleSet" + }, + "ruleGroupType": "ManagedRuleGroup", + "excludeRules": [] + }, + { + "ruleGroupArn": null, + "overrideAction": { "type": "NONE" }, + "managedRuleGroupIdentifier": { + "version": null, + "vendorName": "AWS", + "managedRuleGroupName": "AWSManagedRulesKnownBadInputsRuleSet" + }, + "ruleGroupType": "ManagedRuleGroup", + "excludeRules": [] + }, + { + "ruleGroupArn": null, + "overrideAction": { "type": "NONE" }, + "managedRuleGroupIdentifier": { + "version": null, + "vendorName": "AWS", + "managedRuleGroupName": "AWSManagedRulesSQLiRuleSet" + }, + "ruleGroupType": "ManagedRuleGroup", + "excludeRules": [] + } + ], + "postProcessRuleGroups": [], + "defaultAction": { "type": "ALLOW" }, + "overrideCustomerWebACLAssociation": false, + "loggingConfiguration": null + }, + "resourceType": "ResourceTypeList", + "resourceTypeList": [ + "AWS::ApiGateway::Stage", + "AWS::ElasticLoadBalancingV2::LoadBalancer" + ], + "remediationEnabled": true, + "excludeResourceTags": false + } + ], + "SecurityGroup": [ + { + "policyName": "FMS-SecGroup-01", + "policyDetails": { + "type": "SECURITY_GROUPS_USAGE_AUDIT", + "deleteUnusedSecurityGroups": true, + "coalesceRedundantSecurityGroups": true, + "optionalDelayForUnusedInMinutes": 0 + }, + "remediationEnabled": false, + "resourceType": "AWS::EC2::SecurityGroup", + "excludeResourceTags": false + }, + { + "policyName": "FMS-SecGroup-02", + "policyDetails": { + "type": "SECURITY_GROUPS_CONTENT_AUDIT", + "preManagedOptions": [ + { + "denyProtocolAllValue": true + }, + { + "allowedPortCountPerSgRule": 1 + }, + { + "minIpv4CidrPrefixLenPerSgRule": 16 + }, + { + "minIpv6CidrPrefixLenPerSgRule": 48 + }, + { + "auditSgDirection": { + "type": "ALL" + } + } + ] + }, + "remediationEnabled": false, + "resourceType": "ResourceTypeList", + "resourceTypeList": [ + "AWS::EC2::Instance", + "AWS::EC2::NetworkInterface", + "AWS::EC2::SecurityGroup" + ], + "excludeResourceTags": false + } + ] + }, + "advanced": { + "Shield": [ + { + "policyName": "FMS-Shield-01", + "policyScope": "Global", + "remediationEnabled": true, + "resourceType": "AWS::CloudFront::Distribution", + "excludeResourceTags": false, + "policyDetails": { + "type": "SHIELD_ADVANCED" + } + }, + { + "policyName": "FMS-Shield-02", + "policyScope": "Regional", + "remediationEnabled": true, + "resourceType": "ResourceTypeList", + "resourceTypeList": [ + "AWS::ElasticLoadBalancingV2::LoadBalancer", + "AWS::ElasticLoadBalancing::LoadBalancer", + "AWS::EC2::EIP" + ], + "policyDetails": { + "type": "SHIELD_ADVANCED" + } + } + ] + } +} diff --git a/source/services/policyManager/lib/policyManager.ts b/source/services/policyManager/lib/policyManager.ts new file mode 100644 index 0000000..a459be7 --- /dev/null +++ b/source/services/policyManager/lib/policyManager.ts @@ -0,0 +1,282 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import { FMSHelper } from "./fmsHelper"; +import { Policy } from "aws-sdk/clients/fms"; +import { logger } from "./common/logger"; + +export type T = { + Key: string; + Value: string; +}; + +export interface ITags { + ResourceTags: Array; + ExcludeResourceTags: boolean; +} + +export interface IPolicyManager { + orgUnits: string; + tier?: string; + region: string; + table: string; + tags: string; + loglevel: string; +} + +export class PolicyManager { + static async workflowProcessor( + table: string, + region: string, + policy: Policy + ) { + /** + * Step 1. get dynamodb policy item + * Step 2. get PolicyUpdateToken + * Step 3. put fms security policy + * Step 4. update dynamodb item with new policy update token + */ + + let event = "Update"; + // Step 1. Get DDB item + try { + let item: any; + await FMSHelper.getDDBItem(policy.PolicyName, region, table) + .then((data) => { + item = data; + }) + .catch((e) => { + if (e.message === "ResourceNotFound") { + item = ""; + } else throw new Error(e.message); + }); + + // Step 2. Update item for region + if (item) { + Object.assign(policy, { + PolicyUpdateToken: item.PolicyUpdateToken.S, + PolicyId: item.PolicyId.S, + }); + } + if (!item) { + delete policy.PolicyUpdateToken; + delete policy.PolicyId; + event = "Create"; + } + + // Step 3. Save Policy + const resp = await FMSHelper.putPolicy(policy, region); + + // Step 4. Update DDB Item + if (!resp.Policy) throw new Error("error creating policy"); + if (!resp.Policy.PolicyUpdateToken || !resp.Policy.PolicyId) + throw new Error("policy update token not found"); + await FMSHelper.updateDDBItem( + policy.PolicyName, + region, + { + updateToken: resp.Policy.PolicyUpdateToken, + policyId: resp.Policy.PolicyId, + }, + table + ); + + // Step 5. Response + logger.info({ + label: "PolicyManagaer/workflowProcessor", + message: `FMS policy saved successfully`, + }); + return event; + } catch (e) { + throw new Error(e.message); + } + } + + /** + * @description check if OUs are valid + * @param {string[]} ous + * @returns {Promise} + */ + static async isOUValid(ous: string[]): Promise { + logger.debug({ + label: "PolicyManagaer/isOUValid", + message: `checking if OUs are valid`, + }); + const regex = "^ou-([0-9a-z]{4,32})-([0-9a-z]{8,32})$"; + try { + await Promise.all( + ous.map((ou) => { + if (!ou.match(regex)) throw new Error("invalid OU Id provided"); + }) + ); + return true; + } catch (e) { + logger.error({ + label: "PolicyManagaer/isOUValid", + message: `${e.message}`, + }); + return false; + } + } + + /** + * @description check if regions are valid + * @param {string[]} regions + * @returns {Promise} + */ + static async isRegionValid(regions: string[]): Promise { + logger.debug({ + label: "PolicyManagaer/isRegionValid", + message: `checking if region parameter is valid`, + }); + const ec2Regions = await FMSHelper.getRegions(); + if (!(ec2Regions instanceof Array)) throw new Error("no regions found"); + try { + await Promise.all( + regions.map((region) => { + if (!ec2Regions.includes(region)) + throw new Error("invalid region provided"); + }) + ); + return true; + } catch (e) { + logger.error({ + label: "PolicyManagaer/isRegionValid", + message: `${e.message}`, + }); + return false; + } + } + + /** + * @description check if OUs is set to delete + * @param {string[]} ous + * @returns {Promise} + */ + static async isOUDelete(ous: string[]): Promise { + logger.debug({ + label: "PolicyManagaer/isOUDelete", + message: `checking if OU is set to delete`, + }); + if (ous.length === 1 && ous[0].toLowerCase() === "delete") { + logger.debug({ + label: "PolicyManagaer/isOUDelete", + message: `OU set to delete`, + }); + return true; + } else { + logger.debug({ + label: "PolicyManagaer/isOUDelete", + message: `OU not set to delete`, + }); + return false; + } + } + + /** + * @description check if region list is set to delete + * @param {string[]} regions[] + * @returns {Promise} + */ + static async isRegionDelete(regions: string[]): Promise { + logger.debug({ + label: "PolicyManagaer/isRegionDelete", + message: `checking if region is set to delete`, + }); + if (regions.length === 1 && regions[0].toLowerCase() === "delete") { + logger.debug({ + label: "PolicyManagaer/isRegionDelete", + message: `region is set to delete`, + }); + return true; + } else { + logger.debug({ + label: "PolicyManagaer/isRegionDelete", + message: `region is not set to delete`, + }); + return false; + } + } + + /** + * @description check if tags are valid + * @param {string[]} tags[] + * @returns {Promise} + * @example {"ResourceTags":[{"Key":"Environment","Value":"Prod"}],"ExcludeResourceTags":false} + */ + static async isTagValid(tags: string): Promise { + logger.info({ + label: "PolicyManagaer/isTagValid", + message: `checking if tag is valid`, + }); + + try { + const t = JSON.parse(tags); + logger.debug({ + label: "PolicyManagaer/isTagValid", + message: `tag: ${JSON.stringify(tags)}`, + }); + if ( + Object.prototype.hasOwnProperty.call(t, "ResourceTags") && + Object.prototype.hasOwnProperty.call(t, "ExcludeResourceTags") && + typeof t.ExcludeResourceTags === "boolean" + ) { + await Promise.all( + t.ResourceTags.map((rt: ITags) => { + if ( + !Object.prototype.hasOwnProperty.call(rt, "Key") || + !Object.prototype.hasOwnProperty.call(rt, "Value") + ) + throw new Error("invalid tag"); + if (Object.keys(rt).length > 2) throw new Error("invalid tag"); + }) + ); + logger.debug({ + label: "PolicyManagaer/isTagValid", + message: `tag is valid`, + }); + return true; + } else throw new Error("invalid tag"); + } catch (e) { + logger.error({ + label: "PolicyManagaer/isTagValid", + message: `${e.message}`, + }); + return false; + } + } + + /** + * @description check if Tag parameter is set to delete + * @param {string} tags + * @returns {Promise} + */ + static async isTagDelete(tags: string): Promise { + logger.debug({ + label: "PolicyManagaer/isTagDelete", + message: `checking if Tag is set to delete`, + }); + if (tags.toLowerCase() === "delete") { + logger.debug({ + label: "PolicyManagaer/isTagDelete", + message: `Tags set to delete`, + }); + return true; + } else { + logger.debug({ + label: "PolicyManagaer/isTagDelete", + message: `Tags not set to delete`, + }); + return false; + } + } +} diff --git a/source/services/policyManager/lib/securitygroupManager.ts b/source/services/policyManager/lib/securitygroupManager.ts new file mode 100644 index 0000000..66426cb --- /dev/null +++ b/source/services/policyManager/lib/securitygroupManager.ts @@ -0,0 +1,123 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import { PolicyManager, ITags } from "./policyManager"; +import manifest from "./manifest.json"; +import { FMSHelper } from "./fmsHelper"; +import moment from "moment"; +import { Metrics } from "./common/metrics"; + +export class SecurityGroupManager { + /** + * @description save Security Group usage audit security policies + */ + static async saveSecGrpPolicy( + ous: string[], + tags: ITags, + table: string, + region: string, + type: string + ) { + const sgRules = manifest.basic.SecurityGroup; + let sgAudit: any; + if (type === "USAGE_AUDIT") + sgAudit = sgRules.find( + (rule) => rule.policyDetails.type === "SECURITY_GROUPS_USAGE_AUDIT" + ); + if (type === "CONTENT_AUDIT") + sgAudit = sgRules.find( + (rule) => rule.policyDetails.type === "SECURITY_GROUPS_CONTENT_AUDIT" + ); + if (!sgAudit) throw new Error("Security Group Audit policy not found"); + + const policy = { + PolicyName: sgAudit.policyName, + ResourceTags: tags.ResourceTags, + ExcludeResourceTags: tags.ExcludeResourceTags, + RemediationEnabled: sgAudit.remediationEnabled, + ResourceType: sgAudit.resourceType, + SecurityServicePolicyData: { + Type: sgAudit.policyDetails.type, + ManagedServiceData: JSON.stringify(sgAudit.policyDetails), + }, + IncludeMap: { + ORG_UNIT: ous, + }, + }; + if (type === "CONTENT_AUDIT") + Object.assign(policy, { + ResourceTypeList: sgAudit.resourceTypeList, + }); + + const _e = await PolicyManager.workflowProcessor(table, region, policy); + + // send metrics + if (process.env.SEND_METRIC === "Yes") { + const metric = { + Solution: process.env.SOLUTION_ID, + UUID: process.env.UUID, + TimeStamp: moment.utc().format("YYYY-MM-DD HH:mm:ss.S"), + Data: { + OUCount: ous.length, + Region: region, + Event: _e, + Type: `SG_` + type, + Version: process.env.SOLUTION_VERSION, + }, + }; + await Metrics.sendAnonymousMetric( + process.env.METRICS_QUEUE, + metric + ); + } + } + + /** + * @description delete Security Group usage audit security policies + * @param table + * @param region + */ + static async deleteSecGrpPolicy(table: string, region: string, type: string) { + const sgRules = manifest.basic.SecurityGroup; + let sgAudit: any; + if (type === "USAGE_AUDIT") + sgAudit = sgRules.find( + (rule) => rule.policyDetails.type === "SECURITY_GROUPS_USAGE_AUDIT" + ); + if (type === "CONTENT_AUDIT") + sgAudit = sgRules.find( + (rule) => rule.policyDetails.type === "SECURITY_GROUPS_CONTENT_AUDIT" + ); + if (!sgAudit) throw new Error("Security Group Audit policy not found"); + + await FMSHelper.deletePolicy(sgAudit.policyName, region, table); + + // send metrics + if (process.env.SEND_METRIC === "Yes") { + const metric = { + Solution: process.env.SOLUTION_ID, + UUID: process.env.UUID, + TimeStamp: moment.utc().format("YYYY-MM-DD HH:mm:ss.S"), + Data: { + Region: region, + Event: "Delete", + Type: `SG_` + type, + Version: process.env.SOLUTION_VERSION, + }, + }; + await Metrics.sendAnonymousMetric( + process.env.METRICS_QUEUE, + metric + ); + } + } +} diff --git a/source/services/policyManager/lib/shieldManager.ts b/source/services/policyManager/lib/shieldManager.ts new file mode 100644 index 0000000..ccfc984 --- /dev/null +++ b/source/services/policyManager/lib/shieldManager.ts @@ -0,0 +1,133 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import { PolicyManager, ITags } from "./policyManager"; +import manifest from "./manifest.json"; +import awsClient from "./clientConfig.json"; +import { FMSHelper } from "./fmsHelper"; +import { Shield } from "aws-sdk"; +import { Metrics } from "./common/metrics"; +import moment from "moment"; + +export class ShieldManager { + /** + * @description + * @param ous + * @param tags + * @param region + * @param table + */ + static async saveShieldPolicy( + ous: string[], + tags: ITags, + table: string, + region: string + ) { + const shield = new Shield({ + apiVersion: awsClient.shield, + region: awsClient.dataPlane, + }); + const sub = await shield.getSubscriptionState().promise(); + if (sub.SubscriptionState !== "ACTIVE") + throw new Error("Shield Advanced subscription not active"); + + const shieldRules = manifest.advanced.Shield; + let shieldRule: any; + if (region === "Global") { + shieldRule = shieldRules.find((rule) => rule.policyScope === "Global"); + if (!shieldRule) throw new Error("Shield global policy not found"); + } else { + shieldRule = shieldRules.find((rule) => rule.policyScope === "Regional"); + if (!shieldRule) throw new Error("Shield global policy not found"); + } + + const policy = { + PolicyName: shieldRule.policyName, + RemediationEnabled: shieldRule.remediationEnabled, + ResourceTags: tags.ResourceTags, + ExcludeResourceTags: tags.ExcludeResourceTags, + ResourceType: shieldRule.resourceType, + IncludeMap: { + ORG_UNIT: ous, + }, + SecurityServicePolicyData: { + Type: shieldRule.policyDetails.type, + ManagedServiceData: JSON.stringify(shieldRule.policyDetails), + }, + }; + if (region !== "Global") { + Object.assign(policy, { + ResourceTypeList: shieldRule.resourceTypeList, + }); + } + + const _e = await PolicyManager.workflowProcessor(table, region, policy); + + // send metrics + if (process.env.SEND_METRIC === "Yes") { + const metric = { + Solution: process.env.SOLUTION_ID, + UUID: process.env.UUID, + TimeStamp: moment.utc().format("YYYY-MM-DD HH:mm:ss.S"), + Data: { + OUCount: ous.length, + Region: region, + Event: _e, + Type: "Shield", + Version: process.env.SOLUTION_VERSION, + }, + }; + await Metrics.sendAnonymousMetric( + process.env.METRICS_QUEUE, + metric + ); + } + } + + /** + * @description delete shield security policies + * @param table + * @param region + */ + static async deleteShieldPolicy(table: string, region: string) { + const shieldRules = manifest.advanced.Shield; + let shieldRule: any; + if (region === "Global") { + shieldRule = shieldRules.find((rule) => rule.policyScope === "Global"); + if (!shieldRule) throw new Error("Shield global policy not found"); + } else { + shieldRule = shieldRules.find((rule) => rule.policyScope === "Regional"); + if (!shieldRule) throw new Error("Shield regional policy not found"); + } + + await FMSHelper.deletePolicy(shieldRule.policyName, region, table); + + // send metrics + if (process.env.SEND_METRIC === "Yes") { + const metric = { + Solution: process.env.SOLUTION_ID, + UUID: process.env.UUID, + TimeStamp: moment.utc().format("YYYY-MM-DD HH:mm:ss.S"), + Data: { + Region: region, + Event: "Delete", + Type: "Shield", + Version: process.env.SOLUTION_VERSION, + }, + }; + await Metrics.sendAnonymousMetric( + process.env.METRICS_QUEUE, + metric + ); + } + } +} diff --git a/source/services/policyManager/lib/wafManager.ts b/source/services/policyManager/lib/wafManager.ts new file mode 100644 index 0000000..dda75e7 --- /dev/null +++ b/source/services/policyManager/lib/wafManager.ts @@ -0,0 +1,123 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import { PolicyManager, ITags } from "./policyManager"; +import manifest from "./manifest.json"; +import { FMSHelper } from "./fmsHelper"; +import { Metrics } from "./common/metrics"; +import moment from "moment"; + +export class WAFManager { + /** + * @description create/update WAF global or regional security policies + */ + static async saveWAFPolicy( + ous: string[], + tags: ITags, + table: string, + region: string + ) { + const WAFRules = manifest.basic.WAF; + let WAFRule: any; + if (region === "Global") { + WAFRule = WAFRules.find((rule) => rule.policyScope === "Global"); + if (!WAFRule) { + throw new Error("WAF global policy not found"); + } + } else { + WAFRule = WAFRules.find((rule) => rule.policyScope === "Regional"); + if (!WAFRule) { + throw new Error("WAF regional policy not found"); + } + } + + const policy = { + PolicyName: WAFRule.policyName, + RemediationEnabled: WAFRule.remediationEnabled, + ResourceType: WAFRule.resourceType, + ResourceTags: tags.ResourceTags, + ExcludeResourceTags: tags.ExcludeResourceTags, + SecurityServicePolicyData: { + Type: WAFRule.policyDetails.type, + ManagedServiceData: JSON.stringify(WAFRule.policyDetails), + }, + IncludeMap: { + ORG_UNIT: ous, + }, + }; + if (region !== "Global") + Object.assign(policy, { + ResourceTypeList: WAFRule.resourceTypeList, + }); + + const _e = await PolicyManager.workflowProcessor(table, region, policy); + + // send metrics + if (process.env.SEND_METRIC === "Yes") { + const metric = { + Solution: process.env.SOLUTION_ID, + UUID: process.env.UUID, + TimeStamp: moment.utc().format("YYYY-MM-DD HH:mm:ss.S"), + Data: { + OUCount: ous.length, + Region: region, + Event: _e, + Type: "WAF", + Version: process.env.SOLUTION_VERSION, + }, + }; + + await Metrics.sendAnonymousMetric( + process.env.METRICS_QUEUE, + metric + ); + } + } + + /** + * @description delete WAF global or regional security policies + * @param table + * @param region + */ + static async deleteWAFPolicy(table: string, region: string) { + const WAFRules = manifest.basic.WAF; + let WAFRule: any; + if (region === "Global") { + WAFRule = WAFRules.find((rule) => rule.policyScope === "Global"); + if (!WAFRule) throw new Error("WAF global policy not found"); + } else { + WAFRule = WAFRules.find((rule) => rule.policyScope === "Regional"); + if (!WAFRule) throw new Error("WAF regional policy not found"); + } + + await FMSHelper.deletePolicy(WAFRule.policyName, region, table); + + // send metrics + if (process.env.SEND_METRIC === "Yes") { + const metric = { + Solution: process.env.SOLUTION_ID, + UUID: process.env.UUID, + TimeStamp: moment.utc().format("YYYY-MM-DD HH:mm:ss.S"), + Data: { + Region: region, + Event: "Delete", + Type: "WAF", + Version: process.env.SOLUTION_VERSION, + }, + }; + await Metrics.sendAnonymousMetric( + process.env.METRICS_QUEUE, + metric + ); + } + } +} diff --git a/source/services/policyManager/package.json b/source/services/policyManager/package.json new file mode 100644 index 0000000..7a749e9 --- /dev/null +++ b/source/services/policyManager/package.json @@ -0,0 +1,40 @@ +{ + "name": "policy-manager", + "version": "1.0.0", + "description": "microservice to configure pre-defined policies for firewall manager", + "main": "index.js", + "scripts": { + "pretest": "npm i", + "test": "./node_modules/jest/bin/jest.js --coverage ./__tests__", + "build:clean": "rm -rf ./node_modules && rm -rf ./dist && rm -f ./package-lock.json", + "build:copy": "cp -r ./node_modules ./dist/node_modules", + "build:ts": "./node_modules/typescript/bin/tsc --project ./tsconfig.json", + "build:install": "npm i", + "watch": "tsc -w", + "build:zip": "cd ./dist && zip -rq policyManager.zip .", + "build:deployment": "npm run build:ts && npm run build:zip", + "build:all": "npm run build:clean && npm run build:install && npm run build:ts && npm prune --production && npm run build:copy && npm run build:zip" + }, + "author": "@aws-solutions", + "license": "Apache-2.0", + "dependencies": { + "aws-sdk": "^2.692.0", + "winston": "^3.3.3", + "moment": "^2.27.0" + }, + "devDependencies": { + "@types/jest": "^25.2.3", + "@types/moment": "^2.13.0", + "@types/node": "^14.0.12", + "jest": "^26.0.1", + "ts-jest": "^26.1.0", + "ts-node": "^8.10.2", + "typescript": "^4.0.2", + "jest-sonar-reporter": "^2.0.0" + }, + "jestSonar": { + "reportPath": "../../../reports", + "reportFile": "policy-manager-reporter.xml", + "indent": 4 + } +} diff --git a/source/services/policyManager/tsconfig.json b/source/services/policyManager/tsconfig.json new file mode 100644 index 0000000..860b3df --- /dev/null +++ b/source/services/policyManager/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "declaration": false, + "noImplicitAny": true, + "noEmit": false, + "experimentalDecorators": true + } +} diff --git a/source/services/preReqManager/__tests__/prereqManager.test.ts b/source/services/preReqManager/__tests__/prereqManager.test.ts new file mode 100644 index 0000000..70b786d --- /dev/null +++ b/source/services/preReqManager/__tests__/prereqManager.test.ts @@ -0,0 +1 @@ +// TODO diff --git a/source/services/preReqManager/index.ts b/source/services/preReqManager/index.ts new file mode 100644 index 0000000..3be61fb --- /dev/null +++ b/source/services/preReqManager/index.ts @@ -0,0 +1,225 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +/** + * @description + * AWS Centralized WAF & Security Group Automation + * Microservice to validate and install pre-requisites for the solution + * This must be deployed in Organization Master account + * @author aws-solutions + */ + +import { PreReqManager } from "./lib/preReqManager"; +import { logger } from "./lib/common/logger"; +import { Metrics } from "./lib/common/metrics"; +import moment from "moment"; + +interface IEvent { + RequestType: string; + ResponseURL: string; + StackId: string; + RequestId: string; + ResourceType: string; + LogicalResourceId: string; + ResourceProperties: any; + PhysicalResourceId?: string; +} +/** + * @description Lambda event handler for pre-requisite manager + * @param {IEvent} event - invoking event + */ +exports.handler = async (event: IEvent, context: any) => { + logger.debug({ + label: "PreRegManager", + message: `event: ${JSON.stringify(event)}`, + }); + + let responseData: any = { + Data: "NOV", + }; + let status = "SUCCESS"; + const properties = event.ResourceProperties; + const metric = { + Solution: properties.SolutionId, + UUID: properties.SolutionUuid, + TimeStamp: "", + Data: {}, + }; + + // pre-req checker custom resource CREATE event + if (event.ResourceType === "Custom::PreReqChecker") { + const _pm = new PreReqManager({ + fmsAdmin: properties.FMSAdmin, + accountId: properties.AccountId, + region: properties.Region, + globalStackSetName: properties.GlobalStackSetName, + regionalStackSetName: properties.RegionalStackSetName, + }); + if (event.RequestType === "Create" || event.RequestType === "Update") { + try { + await _pm.orgFeatureCheck(); // check for all features enabled + await _pm.orgMasterCheck(); // check for deployment in master + await _pm.fmsAdminCheck(); // configure fms admin + await _pm.enableTrustedAccess(); // enable trusted access + + if ( + properties.EnableConfig === "No" && + event.RequestType === "Create" + ) { + logger.warn({ + label: "PreReqManager", + message: `skipping AWS Config check`, + }); + } else if (properties.EnableConfig === "Yes") { + let retry = true; + let retryCount = 0; + while (retry && retryCount < 10) { + await delay(1000 * Math.random()); + await _pm + .enableConfig() + .then((_) => { + retry = false; + }) + .catch((_) => { + retryCount++; + }); // enable config in organization + } + if (retryCount === 10) + logger.warn({ + label: "PreReqManager", + message: "stack set instance creation failed", + }); + else + logger.debug({ + label: "PreReqManager", + message: "stack set instance creation success", + }); + } else if ( + properties.EnableConfig === "No" && + event.RequestType === "Update" + ) { + // delete config + await _pm.deleteConfig().catch((e) => { + logger.warn({ + label: "PreReqManager", + message: e.message, + }); + }); + } + + logger.info({ + label: "PreRegManager", + message: `All pre requisites validated & installed`, + }); + + // send Metrics + if (process.env.SEND_METRIC === "Yes") { + metric.TimeStamp = moment.utc().format("YYYY-MM-DD HH:mm:ss.S"); + metric.Data = { + Event: "PreReqsInstalled", + Stack: "PreReqStack", + Version: properties.SolutionVersion, + }; + await Metrics.sendAnonymousMetric( + process.env.METRICS_ENDPOINT, + metric + ); + } + responseData = { + PreReqCheck: true, + }; + } catch (e) { + // send Metrics + if (process.env.SEND_METRIC === "Yes") { + metric.TimeStamp = moment.utc().format("YYYY-MM-DD HH:mm:ss.S"); + metric.Data = { + Event: "PreReqsInstallFailed", + Stack: "PreReqStack", + Version: properties.SolutionVersion, + }; + await Metrics.sendAnonymousMetric( + process.env.METRICS_ENDPOINT, + metric + ); + } + responseData = { + Error: e.message, + }; + status = "FAILED"; + } + } + if (event.RequestType === "Delete") { + await _pm.deleteConfig().catch((e) => { + logger.warn({ + label: "PreReqManager", + message: e.message, + }); + }); + + responseData = { + Data: "Delete Config initiated", + }; + } + } + + /** + * Send response back to custom resource + */ + return await sendResponse(event, context.logStreamName, status, responseData); +}; + +/** + * @description sends a response to custom resource + * for Create/Update/Delete + * @param {any} event - Custom Resource event + * @param {string} logStreamName - CloudWatch logs stream + * @param {string} responseStatus - response status + * @param {any} responseData - response data + */ +const sendResponse = async ( + event: IEvent, + logStreamName: string, + responseStatus: string, + responseData: any +) => { + const responseBody = { + Status: responseStatus, + Reason: `${JSON.stringify(responseData)}`, + PhysicalResourceId: event.PhysicalResourceId + ? event.PhysicalResourceId + : logStreamName, + StackId: event.StackId, + RequestId: event.RequestId, + LogicalResourceId: event.LogicalResourceId, + Data: responseData, + }; + + logger.debug({ + label: "PreRegManager/sendResponse", + message: `ResponseBody: ${JSON.stringify(responseBody)}`, + }); + if (responseStatus === "FAILED") { + logger.error({ + label: "PreRegManager/sendResponse", + message: responseBody.Reason, + }); + throw new Error(responseBody.Data.Error); + } else return responseBody; +}; + +/** + * @description sleep function + * @param {number} ms - delay in milliseconds + */ +async function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/source/services/preReqManager/jest.config.js b/source/services/preReqManager/jest.config.js new file mode 100644 index 0000000..f26569b --- /dev/null +++ b/source/services/preReqManager/jest.config.js @@ -0,0 +1,58 @@ +module.exports = { + // Automatically clear mock calls and instances between every test + clearMocks: false, + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of regexp pattern strings used to skip coverage collection + coveragePathIgnorePatterns: ["/node_modules/"], + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 50, + functions: 50, + lines: 50, + statements: 50, + }, + }, + + // An array of directory names to be searched recursively up from the requiring module's location + moduleDirectories: ["node_modules"], + + // An array of file extensions your modules use + moduleFileExtensions: ["ts", "json", "jsx", "js", "tsx", "node"], + + // Automatically reset mock state between every test + resetMocks: false, + + // The glob patterns Jest uses to detect test files + testMatch: ["**/?(*.)+(spec|test).[t]s?(x)"], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + testPathIgnorePatterns: ["/node_modules/"], + + // A map from regular expressions to paths to transformers + transform: { + "^.+\\.(t)sx?$": "ts-jest", + }, + + // Indicates whether each individual test should be reported during the run + verbose: true, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + collectCoverageFrom: [ + "./lib/*", + "!./lib/common/*", + "!**.js", + "!**/*.d.ts", + "!**/*.json", + ], + + // A list of paths to modules that run some code to configure or set up the testing environment + setupFiles: ["./jest.setup.js"], + + // This option allows the use of a custom results processor. + testResultsProcessor: "jest-sonar-reporter", +}; diff --git a/source/services/preReqManager/jest.setup.js b/source/services/preReqManager/jest.setup.js new file mode 100644 index 0000000..6b195e9 --- /dev/null +++ b/source/services/preReqManager/jest.setup.js @@ -0,0 +1,3 @@ +process.on("unhandledRejection", (reason) => { + throw reason; +}); diff --git a/source/services/preReqManager/lib/clientConfig.json b/source/services/preReqManager/lib/clientConfig.json new file mode 100644 index 0000000..a499f92 --- /dev/null +++ b/source/services/preReqManager/lib/clientConfig.json @@ -0,0 +1,10 @@ +{ + "cfn": "2010-05-15", + "fms": "2018-01-01", + "organization": "2016-11-28", + "sns": "2010-03-31", + "ec2": "2016-11-15", + "dataPlane": "us-east-1", + "globalStackSetName": "FMS-Global-EnableConfig", + "regionalStackSetName": "FMS-Regional-EnableConfig" +} diff --git a/source/services/preReqManager/lib/common/logger/index.ts b/source/services/preReqManager/lib/common/logger/index.ts new file mode 100644 index 0000000..c261be1 --- /dev/null +++ b/source/services/preReqManager/lib/common/logger/index.ts @@ -0,0 +1,92 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +/** + * { + emerg: 0, + alert: 1, + crit: 2, + error: 3, + warning: 4, + notice: 5, + info: 6, + debug: 7 + } + */ +import { createLogger, transports, format } from "winston"; +import { WinstonSNS } from "./winston-sns"; +const { combine, timestamp, printf } = format; + +/* + * Foramting the output as desired + */ +const myFormat = printf(({ level, label, message }) => { + const _level = level.toUpperCase(); + if (label) return `[${_level}] [${label}] ${message}`; + else return `[${_level}] ${message}`; +}); + +/* + * String mask + */ +const maskCardNumbers = (num: any) => { + const str = num.toString(); + const { length } = str; + + return Array.from(str, (n, i) => { + return i < length - 4 ? "*" : n; + }).join(""); +}; + +// Define the format that mutates the info object. +const maskFormat = format((info: any) => { + // You can CHANGE existing property values + if (info.message.securedNumber) { + info.message.securedNumber = maskCardNumbers(info.message.securedNumber); + } + + // You can also ADD NEW properties if you wish + //info.hasCreditCard = !!info.creditCard; + + return info; +}); + +export const logger = createLogger({ + format: combine( + // + // Order is important here, the formats are called in the + // order they are passed to combine. + // + maskFormat(), + timestamp(), + myFormat + ), + + transports: [ + //cw logs transport channel + new transports.Console({ + level: process.env.LOG_LEVEL, + handleExceptions: true, //handle uncaught exceptions + //format: format.splat() + }), + + //sns transport channel + ...(process.env.SNS_ERROR_NOTIFICATION == "true" + ? [ + new WinstonSNS({ + topic_arn: process.env.SNS_TOPIC_ARN, + level: "warn", + }), + ] + : []), + ], +}); diff --git a/source/services/preReqManager/lib/common/logger/winston-sns.ts b/source/services/preReqManager/lib/common/logger/winston-sns.ts new file mode 100644 index 0000000..e99f53f --- /dev/null +++ b/source/services/preReqManager/lib/common/logger/winston-sns.ts @@ -0,0 +1,67 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import Transport = require("winston-transport"); +import "util"; +import { SNS } from "aws-sdk"; + +// +// Inherit from `winston-transport` so you can take advantage +// of the base functionality and `.exceptions.handle()`. +// +export class WinstonSNS extends Transport { + readonly topic_arn: string; + readonly region: string; + private sns: any; + constructor(opts: any) { + super(opts); + this.topic_arn = opts.topic_arn; + this.region = opts.topic_arn.split(":")[3]; + // + // Consume any custom options here. e.g.: + // - Connection information for databases + // - Authentication information for APIs (e.g. loggly, papertrail, + // logentries, etc.). + // + } + formatter = async (info: any) => { + if (info.label) + return `[${info.level.toUpperCase()}] [${info.label}] ${ + info.timestamp + }: ${JSON.stringify(info.message, null, 2)}`; + else + return `[${info.level.toUpperCase()}] ${info.timestamp}: ${JSON.stringify( + info.message, + null, + 2 + )}`; + }; + + log = async (info: any) => { + try { + this.sns = new SNS({ + apiVersion: "2010-03-31", + region: this.region, + }); + const _txt = await this.formatter(info); + await this.sns + .publish({ + Message: _txt, + TopicArn: this.topic_arn, + }) + .promise(); + return "sns message published successfully"; + } catch (e) { + throw new Error(e.message); + } + }; +} diff --git a/source/services/preReqManager/lib/common/metrics/index.ts b/source/services/preReqManager/lib/common/metrics/index.ts new file mode 100644 index 0000000..2d04367 --- /dev/null +++ b/source/services/preReqManager/lib/common/metrics/index.ts @@ -0,0 +1,58 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import got from "got"; +import { logger } from "../logger/index"; +/** + * Send metrics to solutions endpoint + * @class Metrics + */ +export class Metrics { + /** + * Sends anonymous metric + * @param {object} metric - metric JSON data + */ + static async sendAnonymousMetric(endpoint: string, metric: any) { + logger.debug({ + label: "metrics/sendAnonymousMetric", + message: `metrics endpoint: ${endpoint}`, + }); + logger.debug({ + label: "metrics/sendAnonymousMetric", + message: `sending metric:${JSON.stringify(metric)}`, + }); + try { + await got(endpoint, { + port: 443, + method: "POST", + headers: { + "Content-Type": "application/json", + "Content-Length": "" + JSON.stringify(metric).length, + }, + body: JSON.stringify(metric), + }); + logger.info({ + label: "metrics/sendAnonymousMetric", + message: `metric sent successfully`, + }); + return `Metric sent: ${JSON.stringify(metric)}`; + } catch (error) { + logger.warn({ + label: "metrics/sendAnonymousMetric", + message: `Error occurred while sending metric: ${JSON.stringify( + error + )}`, + }); + return `Error occurred while sending metric`; + } + } +} diff --git a/source/services/preReqManager/lib/enableConfig.json b/source/services/preReqManager/lib/enableConfig.json new file mode 100644 index 0000000..8a845ec --- /dev/null +++ b/source/services/preReqManager/lib/enableConfig.json @@ -0,0 +1,245 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Enable AWS Config", + "Metadata": { + "AWS::CloudFormation::Interface": { + "ParameterGroups": [ + { + "Label": { "default": "Recorder Configuration" }, + "Parameters": [ + "AllSupported", + "IncludeGlobalResourceTypes", + "ResourceTypes" + ] + }, + { + "Label": { "default": "Delivery Channel Configuration" }, + "Parameters": ["DeliveryChannelName", "Frequency"] + }, + { + "Label": { "default": "Delivery Notifications" }, + "Parameters": ["TopicArn", "NotificationEmail"] + } + ], + "ParameterLabels": { + "AllSupported": { "default": "Support all resource types" }, + "IncludeGlobalResourceTypes": { + "default": "Include global resource types" + }, + "ResourceTypes": { + "default": "List of resource types if not all supported" + }, + "DeliveryChannelName": { + "default": "Configuration delivery channel name" + }, + "Frequency": { "default": "Snapshot delivery frequency" }, + "TopicArn": { "default": "SNS topic name" }, + "NotificationEmail": { "default": "Notification Email (optional)" } + } + } + }, + "Parameters": { + "AllSupported": { + "Type": "String", + "Default": true, + "Description": "Indicates whether to record all supported resource types.", + "AllowedValues": [true, false] + }, + "IncludeGlobalResourceTypes": { + "Type": "String", + "Default": true, + "Description": "Indicates whether AWS Config records all supported global resource types.", + "AllowedValues": [true, false] + }, + "ResourceTypes": { + "Type": "List", + "Description": "A list of valid AWS resource types to include in this recording group, such as AWS::EC2::Instance or AWS::CloudTrail::Trail.", + "Default": "" + }, + "DeliveryChannelName": { + "Type": "String", + "Default": "", + "Description": "The name of the delivery channel." + }, + "Frequency": { + "Type": "String", + "Default": "24hours", + "Description": "The frequency with which AWS Config delivers configuration snapshots.", + "AllowedValues": ["1hour", "3hours", "6hours", "12hours", "24hours"] + }, + "TopicArn": { + "Type": "String", + "Default": "", + "Description": "The Amazon Resource Name (ARN) of the Amazon Simple Notification Service (Amazon SNS) topic that AWS Config delivers notifications to." + }, + "NotificationEmail": { + "Type": "String", + "Default": "", + "Description": "Email address for AWS Config notifications (for new topics)." + } + }, + "Conditions": { + "IsAllSupported": { "Fn::Equals": [{ "Ref": "AllSupported" }, true] }, + "IsGeneratedDeliveryChannelName": { + "Fn::Equals": [{ "Ref": "DeliveryChannelName" }, ""] + }, + "CreateTopic": { "Fn::Equals": [{ "Ref": "TopicArn" }, ""] }, + "CreateSubscription": { + "Fn::And": [ + { "Condition": "CreateTopic" }, + { + "Fn::Not": [ + { + "Fn::Equals": [{ "Ref": "NotificationEmail" }, ""] + } + ] + } + ] + } + }, + "Mappings": { + "Settings": { + "FrequencyMap": { + "1hour": "One_Hour", + "3hours": "Three_Hours", + "6hours": "Six_Hours", + "12hours": "Twelve_Hours", + "24hours": "TwentyFour_Hours" + } + } + }, + "Resources": { + "ConfigBucket": { "DeletionPolicy": "Retain", "Type": "AWS::S3::Bucket" }, + "ConfigBucketPolicy": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { "Ref": "ConfigBucket" }, + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AWSConfigBucketPermissionsCheck", + "Effect": "Allow", + "Principal": { "Service": ["config.amazonaws.com"] }, + "Action": "s3:GetBucketAcl", + "Resource": [{ "Fn::Sub": "arn:aws:s3:::${ConfigBucket}" }] + }, + { + "Sid": "AWSConfigBucketDelivery", + "Effect": "Allow", + "Principal": { "Service": ["config.amazonaws.com"] }, + "Action": "s3:PutObject", + "Resource": [ + { + "Fn::Sub": "arn:aws:s3:::${ConfigBucket}/AWSLogs/${AWS::AccountId}/*" + } + ] + } + ] + } + } + }, + "ConfigTopic": { + "Condition": "CreateTopic", + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": { "Fn::Sub": "config-topic-${AWS::AccountId}" }, + "DisplayName": "AWS Config Notification Topic" + } + }, + "ConfigTopicPolicy": { + "Condition": "CreateTopic", + "Type": "AWS::SNS::TopicPolicy", + "Properties": { + "Topics": [{ "Ref": "ConfigTopic" }], + "PolicyDocument": { + "Statement": [ + { + "Sid": "AWSConfigSNSPolicy", + "Action": ["sns:Publish"], + "Effect": "Allow", + "Resource": { "Ref": "ConfigTopic" }, + "Principal": { "Service": ["config.amazonaws.com"] } + } + ] + } + } + }, + "EmailNotification": { + "Condition": "CreateSubscription", + "Type": "AWS::SNS::Subscription", + "Properties": { + "Endpoint": { "Ref": "NotificationEmail" }, + "Protocol": "email", + "TopicArn": { "Ref": "ConfigTopic" } + } + }, + "ConfigRecorderRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { "Service": ["config.amazonaws.com"] }, + "Action": ["sts:AssumeRole"] + } + ] + }, + "Path": "/", + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSConfigRole" + ] + } + }, + "ConfigRecorder": { + "Type": "AWS::Config::ConfigurationRecorder", + "DependsOn": ["ConfigBucketPolicy", "ConfigTopicPolicy"], + "Properties": { + "RoleARN": { "Fn::GetAtt": ["ConfigRecorderRole", "Arn"] }, + "RecordingGroup": { + "AllSupported": { "Ref": "AllSupported" }, + "IncludeGlobalResourceTypes": { "Ref": "IncludeGlobalResourceTypes" }, + "ResourceTypes": { + "Fn::If": [ + "IsAllSupported", + { "Ref": "AWS::NoValue" }, + { "Ref": "ResourceTypes" } + ] + } + } + } + }, + "ConfigDeliveryChannel": { + "Type": "AWS::Config::DeliveryChannel", + "DependsOn": ["ConfigBucketPolicy", "ConfigTopicPolicy"], + "Properties": { + "Name": { + "Fn::If": [ + "IsGeneratedDeliveryChannelName", + { "Ref": "AWS::NoValue" }, + { "Ref": "DeliveryChannelName" } + ] + }, + "ConfigSnapshotDeliveryProperties": { + "DeliveryFrequency": { + "Fn::FindInMap": [ + "Settings", + "FrequencyMap", + { "Ref": "Frequency" } + ] + } + }, + "S3BucketName": { "Ref": "ConfigBucket" }, + "SnsTopicARN": { + "Fn::If": [ + "CreateTopic", + { "Ref": "ConfigTopic" }, + { "Ref": "TopicArn" } + ] + } + } + } + } +} diff --git a/source/services/preReqManager/lib/preReqManager.ts b/source/services/preReqManager/lib/preReqManager.ts new file mode 100644 index 0000000..c4502da --- /dev/null +++ b/source/services/preReqManager/lib/preReqManager.ts @@ -0,0 +1,562 @@ +/** + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import { FMS, Organizations, CloudFormation, EC2 } from "aws-sdk"; +import enableConfigTemplate from "./enableConfig.json"; +import { logger } from "./common/logger"; +import awsClient from "./clientConfig.json"; + +interface IPreReq { + /** + * @description AccountId for FMS Admin + */ + fmsAdmin: string; + /** + * @description AccountId where the function is deployed + */ + accountId: string; + /** + * @description Deployment region for the stack + */ + region: string; + /** + * @description Name of stack set to enable config for global resources + */ + globalStackSetName: string; + /** + * @description Name of stack set to enable config for regional resources + */ + regionalStackSetName: string; +} + +/** + * @description + * The pre-requisite manager class has methods to support pre-req checks + */ + +export class PreReqManager { + readonly fmsAdmin: string; + readonly accountId: string; + readonly region: string; + readonly globalStackSetName: string; + readonly regionalStackSetName: string; + + /** + * @constructor + * @param {IPreReq} props + */ + constructor(props: IPreReq) { + this.fmsAdmin = props.fmsAdmin; + this.accountId = props.accountId; + this.region = props.region; + this.globalStackSetName = props.globalStackSetName; + this.regionalStackSetName = props.regionalStackSetName; + } + + /** + * @description returns regions list + * @returns + */ + async getRegions() { + logger.debug({ + label: "PreRegManager/getRegions", + message: `getting EC2 regions`, + }); + const ec2 = new EC2({ + apiVersion: awsClient.ec2, + }); + const _r = await ec2.describeRegions().promise(); + + if (!_r.Regions) { + logger.error({ + label: "PreRegManager/getRegions", + message: `failed to describe regions`, + }); + throw new Error("failed to describe regions"); + } + + const regions = _r.Regions.filter((region) => { + return region.RegionName !== "ap-northeast-3"; + }).map((region) => { + return region.RegionName; + }); + logger.debug({ + label: "PreRegManager/getRegions", + message: `fetched EC2 regions: ${regions}`, + }); + return regions; + } + + /** + * @description enable trusted access for aws services + */ + async enableTrustedAccess() { + logger.debug({ + label: "PreRegManager/enableTrustedAccess", + message: `enabling trusted access for FMS and StackSets`, + }); + const organization = new Organizations({ + apiVersion: awsClient.organization, + region: awsClient.dataPlane, + }); + try { + // enable trusted access for fms + await organization + .enableAWSServiceAccess({ + ServicePrincipal: "fms.amazonaws.com", + }) + .promise(); + // enable trusted access for stacksets + await organization + .enableAWSServiceAccess({ + ServicePrincipal: "member.org.stacksets.cloudformation.amazonaws.com", + }) + .promise(); + logger.info({ + label: "PreRegManager/enableTrustedAccess", + message: `trusted access enabled for stacksets`, + }); + } catch (e) { + logger.error({ + label: "PreRegManager/enableTrustedAccess", + message: e.message, + }); + throw new Error(e.message); + } + } + + /** + * @description validate the organization master account + */ + async orgMasterCheck() { + logger.debug({ + label: "PreRegManager/orgMasterCheck", + message: `initiating organization master check`, + }); + const organization = new Organizations({ + apiVersion: awsClient.organization, + region: awsClient.dataPlane, + }); + try { + const resp = await organization.describeOrganization().promise(); + logger.debug({ + label: "PreRegManager/orgMasterCheck", + message: `organization master check: ${resp}`, + }); + if ( + resp.Organization && + resp.Organization.MasterAccountId !== this.accountId + ) { + const _m = "The template must be deployed in Organization Master"; + logger.error({ + label: "PreRegManager/orgMasterCheck", + message: `organization master check error: ${_m}`, + }); + throw new Error(_m); + } + } catch (e) { + logger.error({ + label: "PreRegManager/orgMasterCheck", + message: `organization master check error: ${e.message}`, + }); + throw new Error(e.message); + } + logger.info({ + label: "PreRegManager/orgMasterCheck", + message: `organization master check success`, + }); + return "done"; + } + + /** + * @description validate the organization full features is enabled + */ + async orgFeatureCheck() { + logger.debug({ + label: "PreRegManager/orgFeatureCheck", + message: `initiating organization feature check`, + }); + const organization = new Organizations({ + apiVersion: awsClient.organization, + region: awsClient.dataPlane, + }); + + try { + const resp = await organization.describeOrganization().promise(); + logger.debug({ + label: "PreRegManager/orgFeatureCheck", + message: `organization feature check: ${JSON.stringify(resp)}`, + }); + if (resp.Organization && resp.Organization.FeatureSet !== "ALL") { + const _m = "Organization must be set with full-features"; + logger.error({ + label: "PreRegManager/orgFeatureCheck", + message: `organization feature check error: ${_m}`, + }); + throw new Error(_m); + } + } catch (e) { + logger.error({ + label: "PreRegManager/orgFeatureCheck", + message: `organization feature check error: ${e.message}`, + }); + throw new Error(`${e.message}`); + } + logger.info({ + label: "PreRegManager/orgFeatureCheck", + message: `organization feature pre-req success`, + }); + return "done"; + } + + /** + * @description validate the fms admin account and set up if no fms admin exists. + * fms admin can only be set from organization master account + */ + async fmsAdminCheck() { + logger.debug({ + label: "PreRegManager/fmsAdminCheck", + message: `initiating fms admin check`, + }); + const fms = new FMS({ + apiVersion: awsClient.fms, + region: awsClient.dataPlane, + }); + try { + const resp = await fms.getAdminAccount({}).promise(); + logger.debug({ + label: "PreRegManager/fmsAdminCheck", + message: `fms admin check: ${JSON.stringify(resp)}`, + }); + + if (resp.AdminAccount && resp.AdminAccount === this.fmsAdmin) { + logger.debug({ + label: "PreRegManager/fmsAdminCheck", + message: `fms admin already set up`, + }); + } else if (resp.AdminAccount && resp.AdminAccount !== this.fmsAdmin) { + const _m = + "provided fms admin account does not match with existing fms admin"; + logger.error({ + label: "PreRegManager/fmsAdminCheck", + message: _m, + }); + throw new Error(_m); + } + } catch (e) { + if (e.code === "ResourceNotFoundException") { + logger.debug({ + label: "PreRegManager/fmsAdminCheck", + message: `associating ${this.fmsAdmin} as fms admin`, + }); + await fms + .associateAdminAccount({ AdminAccount: this.fmsAdmin }) + .promise(); + } else { + logger.error({ + label: "PreRegManager/fmsAdminCheck", + message: `fms admin check error: ${e.message}`, + }); + throw new Error(e.message); + } + } + logger.info({ + label: "PreRegManager/fmsAdminCheck", + message: `organization fms admin pre-req success`, + }); + return "done"; + } + + /** + * @description enable Config in all accounts + */ + async enableConfig() { + logger.debug({ + label: "PreRegManager/enableConfig", + message: `initiating aws config check`, + }); + + const cloudformation = new CloudFormation({ + apiVersion: awsClient.cfn, + }); + const params = { + StackSetName: "NOP", + AutoDeployment: { + Enabled: true, + RetainStacksOnAccountRemoval: false, + }, + Capabilities: ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"], + Description: "stack set to enable Config in all member accounts", + Parameters: [ + { + ParameterKey: "AllSupported", + ParameterValue: "false", + }, + { + ParameterKey: "DeliveryChannelName", + ParameterValue: "", + }, + { + ParameterKey: "Frequency", + ParameterValue: "24hours", + }, + { + ParameterKey: "IncludeGlobalResourceTypes", + ParameterValue: "false", + }, + { + ParameterKey: "NotificationEmail", + ParameterValue: "", + }, + { + ParameterKey: "ResourceTypes", + ParameterValue: "", + }, + { + ParameterKey: "TopicArn", + ParameterValue: "", + }, + ], + PermissionModel: "SERVICE_MANAGED", + TemplateBody: JSON.stringify(enableConfigTemplate), + }; + try { + // global stack set + params.StackSetName = this.globalStackSetName; + params.Parameters!.find( + (v) => v.ParameterKey === "ResourceTypes" + )!.ParameterValue = + "AWS::CloudFront::Distribution,AWS::EC2::SecurityGroup,AWS::EC2::Instance,AWS::EC2::NetworkInterface,AWS::EC2::EIP,AWS::ElasticLoadBalancing::LoadBalancer,AWS::ElasticLoadBalancingV2::LoadBalancer,AWS::ApiGateway::Stage,AWS::WAFRegional::WebACL,AWS::WAF::WebACL,AWS::WAFv2::WebACL,AWS::Shield::Protection,AWS::ShieldRegional::Protection"; + await cloudformation + .createStackSet(params) + .promise() + .then((_) => { + logger.info({ + label: "PreReqManager/enableConfig", + message: `stack set created for ${params.StackSetName}`, + }); + }) + .catch((e) => { + if (e.code === "NameAlreadyExistsException") { + logger.warn({ + label: "PreReqManager/enableConfig", + message: `${params.StackSetName} stack set already exists`, + }); + } else throw new Error(e.message); + }); + + // regional stack set params + params.StackSetName = this.regionalStackSetName; + params.Parameters!.find( + (v) => v.ParameterKey === "ResourceTypes" + )!.ParameterValue = + "AWS::EC2::SecurityGroup,AWS::EC2::Instance,AWS::EC2::NetworkInterface,AWS::EC2::EIP,AWS::ElasticLoadBalancing::LoadBalancer,AWS::ElasticLoadBalancingV2::LoadBalancer,AWS::ApiGateway::Stage,AWS::ShieldRegional::Protection,AWS::WAFRegional::WebACL,AWS::WAFv2::WebACL"; + await cloudformation + .createStackSet(params) + .promise() + .then((_) => { + logger.info({ + label: "PreReqManager/enableConfig", + message: `stack set created for ${params.StackSetName}`, + }); + }) + .catch((e) => { + if (e.code === "NameAlreadyExistsException") { + logger.warn({ + label: "PreReqManager/enableConfig", + message: `${params.StackSetName} stack set already exists`, + }); + } else throw new Error(e.message); + }); + + const roots = await this.getOrgRoot(); + + logger.debug({ + label: "PreRegManager/enableConfig", + message: `Organization root list: ${JSON.stringify(roots)}`, + }); + + // create stack instances for global resources + await cloudformation + .createStackInstances({ + Regions: ["us-east-1"], + StackSetName: this.globalStackSetName, + DeploymentTargets: { + OrganizationalUnitIds: roots, + }, + OperationPreferences: { + FailureTolerancePercentage: 100, + MaxConcurrentPercentage: 25, + }, + }) + .promise(); + + // create stack instances for regional resources + const _r = (await this.getRegions()).filter((r) => { + return r != "us-east-1"; + }); + await cloudformation + .createStackInstances({ + Regions: _r, + StackSetName: this.regionalStackSetName, + DeploymentTargets: { + OrganizationalUnitIds: roots, + }, + OperationPreferences: { + FailureTolerancePercentage: 100, + MaxConcurrentPercentage: 25, + }, + }) + .promise(); + logger.info({ + label: "PreRegManager/enableConfig", + message: `Config stack set instances create initiated, please see in Config console for latest status`, + }); + } catch (e) { + logger.error({ + label: "PreRegManager/enableConfig", + message: e.message, + }); + throw new Error("failed to create stack set instances"); + } + } + + /** + * @description enable Config in all accounts + */ + async deleteConfig() { + logger.debug({ + label: "PreRegManager/enableConfig", + message: `initiating aws config delete`, + }); + const cloudformation = new CloudFormation({ + apiVersion: awsClient.cfn, + }); + try { + const roots = await this.getOrgRoot(); + + // delete global stack set instances + await cloudformation + .deleteStackInstances({ + StackSetName: this.globalStackSetName, + Regions: ["us-east-1"], + RetainStacks: false, + DeploymentTargets: { + OrganizationalUnitIds: roots, + }, + OperationPreferences: { + FailureTolerancePercentage: 100, + MaxConcurrentPercentage: 25, + }, + }) + .promise() + .then((_) => { + logger.info({ + label: "PreReqManager/deleteConfig", + message: `delete initiated on ${this.globalStackSetName} stack set instances`, + }); + }) + .catch((e) => { + logger.warn({ + label: "PreReqManager/deleteConfig", + message: `delete failed on ${ + this.globalStackSetName + } stack set instances: ${JSON.stringify(e)}`, + }); + }); + + // delete regional stack set instances + const _r = (await this.getRegions()).filter((r) => { + return r != "us-east-1"; + }); + await cloudformation + .deleteStackInstances({ + StackSetName: this.regionalStackSetName, + Regions: _r, + RetainStacks: false, + DeploymentTargets: { + OrganizationalUnitIds: roots, + }, + OperationPreferences: { + FailureTolerancePercentage: 100, + MaxConcurrentPercentage: 25, + }, + }) + .promise() + .then((_) => { + logger.info({ + label: "PreReqManager/deleteConfig", + message: `delete initiated on ${this.globalStackSetName} stack set instances`, + }); + }) + .catch((e) => { + logger.warn({ + label: "PreReqManager/deleteConfig", + message: `delete failed on ${ + this.globalStackSetName + } stack set instances: ${JSON.stringify(e)}`, + }); + }); + logger.info({ + label: "PreRegManager/deleteConfig", + message: `delete config stack set instances initiated, please see in Config console for latest status`, + }); + } catch (e) { + logger.warn({ + label: "PreRegManager/deleteConfig", + message: `${JSON.stringify(e)}`, + }); + throw new Error("failed to delete stack set instances"); + } + } + + /** + * @description get Organization root + */ + private async getOrgRoot() { + try { + const organization = new Organizations({ + apiVersion: awsClient.organization, + region: awsClient.dataPlane, + }); + + const _ro = await organization.listRoots().promise(); + logger.debug({ + label: "PreRegManager/enableConfig", + message: `organization root list: ${JSON.stringify(_ro)}`, + }); + if (!_ro || !_ro.Roots) { + const _m = "error fetching organization details"; + logger.error({ + label: "PreRegManager/enableConfig", + message: `${_m}`, + }); + throw new Error(_m); + } + + const roots = _ro.Roots.map((root) => { + return root.Id; + }); + logger.debug({ + label: "PreRegManager/getOrgRoot", + message: `organization roots: ${roots}`, + }); + return roots; + } catch (e) { + logger.error({ + label: "PreRegManager/getOrgRoot", + message: `error in getting Organization root: ${JSON.stringify(e)}`, + }); + throw new Error(e); + } + } +} diff --git a/source/services/preReqManager/package.json b/source/services/preReqManager/package.json new file mode 100644 index 0000000..8ca12c2 --- /dev/null +++ b/source/services/preReqManager/package.json @@ -0,0 +1,32 @@ +{ + "name": "pre-req-manager", + "version": "1.0.0", + "description": "microservice to validate pre-reqs for using firewall manager service", + "main": "index.js", + "scripts": { + "pretest": "npm i", + "test": "echo nothing to do", + "build:clean": "rm -rf ./node_modules && rm -rf ./dist && rm -f ./package-lock.json", + "build:copy": "cp -r ./node_modules ./dist/node_modules", + "build:ts": "./node_modules/typescript/bin/tsc --project ./tsconfig.json", + "build:install": "npm i", + "watch": "tsc -w", + "build:zip": "cd ./dist && zip -rq preReqManager.zip .", + "build:all": "npm run build:clean && npm run build:install && npm run build:ts && npm prune --production && npm run build:copy && npm run build:zip" + }, + "author": "@aws-solutions", + "license": "Apache-2.0", + "dependencies": { + "aws-sdk": "^2.714.0", + "moment": "^2.27.0", + "winston": "^3.3.3", + "got": "^11.5.2" + }, + "devDependencies": { + "jest": "^26.0.1", + "ts-jest": "^26.1.0", + "typescript": "^4.0.2", + "@types/node": "^14.0.23", + "@types/moment": "^2.13.0" + } +} diff --git a/source/services/preReqManager/tsconfig.json b/source/services/preReqManager/tsconfig.json new file mode 100644 index 0000000..600d131 --- /dev/null +++ b/source/services/preReqManager/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "declaration": false, + "noImplicitAny": true, + "noEmit": false + } +} diff --git a/source/tsconfig.json b/source/tsconfig.json new file mode 100644 index 0000000..2612a38 --- /dev/null +++ b/source/tsconfig.json @@ -0,0 +1,50 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "ES2018" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + "lib": [ + "DOM", + "ES2018" + ] /* Specify library files to be included in the compilation. */, + "declaration": false /* Generates corresponding '.d.ts' file. */, + "noEmit": true, + "removeComments": true /* Do not emit comments to output. */, + "resolveJsonModule": true /* Allows importing modules with a ‘.json’ extension */, + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, + // "strictNullChecks": true /* Enable strict null checks. */, + "strictFunctionTypes": true /* Enable strict checking of function types. */, + "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, + "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, + "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, + + /* Additional Checks */ + "noUnusedLocals": true /* Report errors on unused locals. */, + "noUnusedParameters": true /* Report errors on unused parameters. */, + "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, + "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, + + /* Module Resolution Options */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + + /* Experimental Options */ + "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + }, + "typedocOptions": { + "includes": "./services/", + "exclude": ["**/*+(.d.ts|.test.ts|node_modules)"], + "mode": "modules", + "externalPattern": ["**/resources/**", "**/deployment/**"], + "excludeExternals": true, + "ignoreCompilerErrors": true, + "out": "docs", + "includeVersion": true, + "readMe": "./README.md" + } +}